# iDoneThis-从头开始扩展基于电子邮件的应用程序

> 原文： [http://highscalability.com/blog/2012/6/20/idonethis-scaling-an-email-based-app-from-scratch.html](http://highscalability.com/blog/2012/6/20/idonethis-scaling-an-email-based-app-from-scratch.html)

![](img/d32057557a700194022c0bb38a729ee5.png)

*这是 [iDoneThis](http://idonethis.com) 的首席技术官 Rodrigo Guzman 的来宾帖子，它使您的公司状态报告可以最轻松地进行。*

**iDoneThis** 是一个简单的管理应用程序，每天结束时都会通过电子邮件向您的团队发送电子邮件，询问您：“今天您做了什么？” 只需回答几行即可完成。 第二天早上，您团队中的每个人都了解了团队在前一天的成就，以使每个人都参与其中，并开始新的一天。

在我们推出之前，我们在一个周末以最基本的方式构建了 iDoneThis。 不好意思，我们使用 Gmail 收件箱的“密件抄送”字段发送了前几批每日电子邮件。 结果是，从网站存在的第 3 天开始，我们就已经在该网站上吸引了用户。

从 2011 年 1 月推出以来，我们每天手动发送数百封电子邮件，到每月发送超过 100 万封电子邮件和处理超过 20 万封传入电子邮件。 总共，客户记录了 170 万次完成。

## 统计资料

*   每天 1 万封入站电子邮件
*   每天发送 40k 电子邮件，其中大部分发生在美国高峰时段，时间是美国的 6 便士，预计会如期到达。
*   每秒几个 Web 请求，突发。 Web 服务器大部分闲置。
*   1GB 的用户内容，5GB 的数据库
*   为文档建立索引以进行实时搜索
*   所有这些都使用单个 xlarge EC2 实例处理。

## 叠放

*   python 几乎所有内容
*   django 用于网络界面
*   apache + mod_wsgi
*   PostgreSQL
*   传入的电子邮件由 sendgrid 解析 API 和内部用 python 编写的自定义电子邮件处理器处理
*   coffeescript，骨干，jquery 和 sass
*   lucene + solr 用于在码头实例后面搜索运行

## 电子邮件-从 Gmail 到 SendGrid

我们从 Gmail 中的密件抄送开始，因为如果我们从应用程序服务器发送电子邮件，则它们都将被标记为垃圾邮件。 Mailchimp 不允许我们在电子邮件中进行大量更改，因为它是为传统电子邮件营销而构建的。

当令人惊讶的是，成百上千的人注册了该服务时，我们使用 [SendGrid](http://sendgrid.com) 在 cronjob 上切换到了自定义脚本（“ sendmail”）。 Sendmail 是我们今天仍然使用的，通过迭代改进来处理出现的错误情况。

它曾经每周崩溃一次，现在几乎从未发生过。 为了实现这一目标，sendmail 从简单的 for 循环变为使用数据库来跟踪每封电子邮件的状态，并具有可以优雅地处理不同状态转换的逻辑。 不过，起初让它成为一个简单的 for 循环对我们很有帮助-在观察了所有常见的失败方式之后，设计机械使其可靠的要容易得多。

为了处理传入的电子邮件，我们从 200 行脚本（称为“ getmail”）开始，以通过 IMAP 访问 Gmail 收件箱。 该过程不可靠，在引发异常后会使数据库处于不良状态，因此必须由保姆亲自操作。 不仅如此，还要有人从条目中删除不需要的行（签名，回复文本等）。 我们使用标准库构建了一个传入解析器，这使我们的 getmail 过程更加可靠。 800 行 Python 代码非常混乱，这些 Python 代码专用于正确处理编码，部分和启发式方法，从而使不需要的行成为可能。

我们从 getmail 迁移到 SendGrid 的传入解析 API，该 API 基本上将传入电子邮件转换为 HTTP POST 请求，因此我们只需要担心编写 Web 应用程序。 我们之所以进行切换是因为由于 Google 的速率限制，getmail 无法足够快地处理传入的电子邮件。 更糟糕的是，getmail 的设计目的不是让它同时运行多个副本，当传入电子邮件的速率太大时，getmail 会开始引发很多问题。

## 扩展流程

可伸缩性是一件有趣的事。 早期，我们面临的主要瓶颈本身不是技术性的，而是开发人员的时间-面对大量电子邮件，这仅仅是我的问题！ 今天基本上也是这样。 这意味着性能和可伸缩性基于代码设计中的简单性，周到的抽象性和敏捷性的原则。

首先要毫不留情地将功能范围限制到最低限度或将其全部外包。 只有在该功能发布并且我们看到它被使用后，我们才对其进行优化以确保其性能（例如，我们添加了 [Celery](http://celeryproject.org/) 以异步发送通知电子邮件，之后该通知已成为 UX 的重要组成部分） 。

其次，使代码尽可能简单（写和读）。 这通常涉及牺牲性能。 例如，对于复杂的查询，ORM 并不总是产生最高效的 SQL，但是我们将其搁置一旁，直到该功能投入生产为止。 诚然，这导致了一些尴尬的时刻，但是更多的时候它使我们远离过早的优化，并且我们的代码库相对干净并且易于使用。

随着传入和传出电子邮件的数量增加，我们考虑切换到多服务器体系结构。 但是，这开始给我们的连续部署库增加了很多复杂性。 因此，我们没有进行优化，而是购买了一台更大的计算机，并构建了一个负载和性能监视系统。 当前，iDoneThis 在单个 xlarge EC2 实例上运行。 我们可以通过做一些工作来摆脱一些小实例，但我们更愿意为简单起见和开发人员时间进行优化。

但是，我们流程中最重要的部分可能是我们拥有非常好的连续部署。 每个开发人员都可以使用一个简短的命令将 git repo 的任何分支部署到生产中，该过程需要几秒钟。 这意味着生产中和我们开发中的产品永远不会有太大差异。 这也意味着我们可以在实时观察发生的情况后快速进行迭代。

## 重新构建现代网络

iDoneThis 的 Web 界面最初是一个非常简单的 Django 项目。 只是一些模型，视图和模板。 随着产品的发展，我们继续采用 Web 应用程序开发的旧范式：所有事情都与页面加载和表单提交有关。 这在一段时间内为我们提供了很好的服务，但是当我们的用户将数据放入我们的系统时，他们需要更好的界面来访问它。

我们慢慢开始堆积 jQuery 意大利面条以及支持它的后端功能。 结果是一团糟。 一种有效，但是很难调试和迭代。

同样，选择只引入绝对必要的复杂性，我们使用 CoffeeScript 和 Backbone.js 重新编写了前端，从而产生了更易管理的组织代码。 当我们采取这一步骤时，我们很大程度上将后端留给了自己，仅根据需要添加了对前端新功能的支持。 事实证明这是有问题的。

由于 iDoneThis 主要基于电子邮件，后来又推出了 iPhone 应用程序，因此我们最终获得了三个用于用户数据 I / O 的渠道：电子邮件，Web 和 iPhone 应用程序-每个渠道都有其自己的细微差别。 对我们来说，这使得我们的一些用户交互和流程不能像您在普通网络应用程序上所期望的那样完全正常工作，而 Django 抽象使这种工作变得容易。 例如，当一个用户被邀请加入一个团队时，他开始接收来自我们的电子邮件，并且可以像其他任何人一样回复它们，但是他从未访问过该站点来设置帐户。

随着我们构建后端以支持所有这些功能，代码变得越来越不规则。 我们尽可能地接受 TDD 来帮助解决这一问题，但更重要的是，我们决定切换到基于 API 的体系结构-我们仍在进行此切换。

看起来，方式是 Django 后端主要由两个组件组成：一个负责提供可由 Backbone.js 和 iPhone 应用程序使用的 json API，以及一组负责提供给前端的简单视图和模板。 代码和 html 占位符。

到目前为止，用这种风格编写的代码比老式的代码要简单得多，并且更易于测试。 但是，弄清进行切换的来龙去脉是一笔相当大的时间投资，因为 django 并不是针对这种范例而设计的。 新代码更简洁的事实使我认为，最终它们在迭代速度方面都将获得回报。

## 得到教训

*   **外包辛勤工作，尽可能外包**。 即使对于基于电子邮件的产品，也要对发送和接收的电子邮件都使用 sendgrid，这就是我们要做的。 当然，在某些时候，内部做这些事情是有意义的，但是对于一家初创公司而言，那一点可能就是 t =无穷大。
*   **性能上的简化**：我们可能会放弃使用较小的 EC2 实例并使用 nginx，但是使用 mod_wsgi 的默认 apache 配置工作得很好，并且更易于自动化。 我们经常做这样的事情：让最简单最容易的事情出现，担心它的性能，然后再进行抛光。
*   短时间内，我们朝着将组件拆分为在其自己的服务器中运行的方向发展。 这开始造成的麻烦超出其应有的价值，因此，我们最终获得了**单个 EC2 实例**。

单个 EC2 实例使我感到恐惧。 当您丢失该实例时会发生什么？

@布兰登：我认为他们应该有某种备份，并有恢复计划。
即使您有多个实例，也存在“丢失实例”问题。 归结为拥有可恢复的备份。

很抱歉...一个 5GB 的数据库和每秒几个 Web 请求*这些天*是“高可扩展性”？ 新闻日慢多少？

那么，告诉我，如何运行单个 EC2 实例“可伸缩”？

@Hrish：备份将使您得以恢复，但它们并不会采取任何措施来防止服务中断。

AWS 有时会发生中断，但是它们几乎总是被隔离到一个实例区域中-也就是说，区域故障彼此独立。 如果任何区域发生故障的概率为 p，则由于区域发生故障而导致单个实例设置脱机的概率也为 p。 但是，如果应用程序是从两个区域提供服务的，并且可以在那些区域之一的消失中幸存下来，则应用程序因区域故障而脱机的可能性为 p ^ 2。 通过扩展到第二个可用性区域，可以将故障风险降低一个数量级。

使用 Django 应用程序，这应该相当容易。 Web 服务器不应存储状态，因此丢失服务器不会使应用程序脱机。 可以使用 Elastic Load Balancer 来放置 Web 服务器的前端，而 ELB 可以在区域中断中幸存下来。 可以跨区域以镜像配置设置 PostgreSQL，或者可以将 PostgreSQL 换成 MySQL，并为数据库使用多可用区 RDS 实例。

对于像我们在 6 月 14 日看到的那样的故障可以恢复的应用程序，差异可能是每月数百美元。