返回 登录
1

Reddit代码部署演变史

原文:The Evolution of Code Deploys at Reddit
作者:Neil Williams & Saurabh Sharma
翻译:Vincent

译者注:本文重点讲述Reddit部署工作从最初的小规模到现在的演变过程,在这过程中的每次调整都做了哪些,以及起到了什么样的效果,使读者能够更加清晰的了解Reddit的演变史。以下为译文。

我们经常在Reddit上面部署代码。每位工程师都要写代码,进行检查,检查,并定期将其推广到生产。 通常情况下每周发生200次,部署通常需要不到10分钟。

这一体系已经演变了多年。让我们来看看它发生了什么改变(以及没发生的)。

故事开始的地方:持续性和可重复部署(2007-2010)

目前这套系统是从push这个Perl脚本发展而来的。这个脚本诞生的时间与Reddit现在的历史时间有很大不同。当时整个开发团队小的可怜,用一间很小的会议室就可以装满了。甚至Reddit还没有在AWS上。该站点运行在固定数量的服务器上,需要手动添加额外的容量,并且由一个称为r2的大型单片Python应用程序组成。

在这么多年的发展过程中,还没有变过的一件事就是请求在负载均衡器上进行分类,然后分配给不同的应用服务器的特定“池”。比如,列表页面和评价页面是由单独的服务器池提供的。尽管给定任意的r2进程都可以处理任何类型的请求,但是单独的池从流量的峰值再到其他的池,并且当它们有不同的依赖关系时,有可能就会处理失败。

push工具有一套硬编码的服务器列表,并且是围绕着整体的部署进程进行搭建的。它将遍历所有应用程序服务器,SSH到机器,运行预设的命令序列,以通过git更新服务器上的代码副本,然后重新启动所有应用程序进程。本质上(也就是说经过大量的净化,而不是真正的代码):

部署是按照顺序逐步进行的。它通过逐个服务器进行工作。听起来好像很简单,但这其实是一种很好的思路:它允许使用canary式的部署形式。如果您仅部署到少量的服务器,并注意到出现了一个新的异常,你应该就会知道代码里面可能有bug,你会终止部署(快捷键是Ctrl + C),并且在出现影响之前尽快回滚。因为这种部署很简单,因此在生产环境中能很轻松的发现问题,并且在没有产生影响之前也可以轻而易举的对代码进行回滚。这也意味着有必要一次就部署一下,以确保新的错误来自本次部署,而不是另一个,因此更容易知道什么时候恢复以及恢复什么。

这样部署可以很好地确保部署一致性和可重复。它部署的很快,并且情况也很容易了解。

新员工的到来 (2011)

之后我们又招聘了一批新的员工,总工程师变成6位了,看起来似乎需要一间稍微大一点的会议室了。在这之后我们开始觉得需要有更好的合作开发的机制了,尤其是每个人在家里工作的时候。于是我们便修改了一下push工具,当开始部署或者结束部署时,它会通过IRC的聊天机器人软件通知到每个人。机器人软件是在IRC里面,也是由它去发送通知的。事实上,部署的形式看上去还是没有改变,但是现在是系统代替了员工进行工作,并且也是由它去通知其他人你正在做什么。

也就是从这时候开始,我们使用了通过聊天去通知部署这种工作流程。其实在这段期间我们使用了很多交流类的软件去管理部署,但是由于我们使用的是第三方软件IRC服务,我们没办法完全信任由产品控制的聊天室,因此它仍然是单向信息流。

随着网站流量的增长,基础设施的支持也显得愈发重要。我们有时不得不推出一批新的应用服务器并投入使用。这项工作还是非常的人工化,包括还需要去手工更新push里面的主机列表。

当我们增加规模时,我们通常是通过一次性增加若干台服务器,从而增加池的数量。其结果是通过顺序的迭代服务器列表,可以在同一池子中快速连续地访问多个服务器,而不是多样化的组合进行访问。

我们使用uWSGI来管理员工的工作进程,因此如果重启它的话,会导致现有的进程被杀掉,并且启动一批新的进程。新的进程需要花费部分时间来准备服务请求,这将会影响到服务请求池的数量。因此我们把速度限制到了只要安全部署到服务器上面就可以了。

重复利用的部署工具 (2012)

我们对部署工具进行了一次彻头彻尾的调整,现在用的是Python开发出来的,但是没人知道为啥还是叫push。新版本在某些功能上做了一些改善。

首先,现在是从DNS去获取所有的主机列表了,而不是在通过硬编码的形式。这个调整就避免再出现那种更新主机列表的时候,忘记去更新部署工具了,这才是一个基本的服务发现系统。

为了解决需要按照顺序进行重启的问题,我们在重启之前会打乱主机列表。因为这么做会使得服务器池变得混乱,这样就可以以更快的速度回滚,因此部署也就变得更快。

最开始的时候,每次都是通过打乱顺序去解决,但这么做就增加了快速恢复代码的难度,因为你不会每次都部署,第一台服务器不可能每次都相同,因此我们又调整了这种机制,使用了seed技术,在恢复代码时,它可以在第二次部署的时候重新使用。

另一个虽然小但是也很重要的调整点就是始终部署固定版本的代码。这个工具的前一个版本会去更新指定主机上面的master,但是如果有人不小心提交了代码而使得master改变了中期部署的方式,会怎么样呢?通过部署特定的git修订而不是分支名称,我们确保了每次部署的时候,无论在生产环境的哪个环节,都能获取到相同的版本。

最后,新版本的工具区分了代码(主要针对主机列表和SSHing)和正在运行的命令。它仍然与r2的需求有很大的偏差,但它有了排序的原始的API。这允许r2控制自己的部署步骤,对建立和发布流的更改更加容易。例如,以下可能是在单个服务器上运行的功能。确切的命令是隐藏的,但是序列仍然是特定于r2的工作流程的。

fetch-names这一步是r2唯一重点关注的。

Autoscaler (2013)

于是我们决定使用云端和autoscale(单独博客文章的主题)。这可以帮我们节省一大笔钱,当网站处于没有大量的访问量时,自动增长可以满足意料之外的需求。。

先前改为从DNS获取主机列表的调整使得这是一个自然的过渡。主机列表的更改频率比以前更块,但与该工具没有什么不同。本来是生活的一部分,只不过现在变成了自动启动autoscaler。

然而,autoscaling的确是引发了一些有趣的边缘案例。就像没有免费的午餐一样。如果一台服务器运行的同时,也发生了部署,那么会产生什么情况呢?我们必须确保每个新启动的服务器都登记过了,以获取新的代码,如果存在的话。服务器如何中途部署呢?工具必须做得更智能,以检测服务器何时可以合理地移除,而不是在部署过程本身中出现问题的时候才进行提醒。

自然而然,由于各种各样的原因,这次我们也把uWSG换成了Gunicorn。就部署而言,这并没有带来什么不同。

所以革命还得继续。

太多的服务器(2014)

随着时间的流逝,高峰的流量也需要越来越多的服务器进行支撑。也就意味着每次部署所需要的时间也越来越长。最糟糕的情况下,正常的部署需要将近一个小时。这简直糟透了。

我们重写了部署工具来并行处理主机。新版本的名称叫rollingpin。旧工具所花的大部分时间是消耗在了初始化连接ssh以及等待命令被执行完的时候,所以在一定的安全数量上进行并行的话是可以缩短时间的。部署的时间也立即被降到了5分钟。

为了降低一次重启多台服务器带来的影响,工具在打乱顺序时也变得更明智了。它将以最大程度地将服务器从每个池中分离出来,而不仅仅是盲目的打乱主机的顺序。这样做也是有意地减少对网站的影响。

新工具最重要的调整使部署工具和在每个服务器上的工具之间API定义的更加清晰,并与r2的需求分离了。这最初是为了考虑开源而关注的,但最终在不久之后的确是非常有用。以下是部署示例,突出显示的命令是远程执行的API。

人员的增长(2015)

忽然就感觉到好像有很多人一下就使用r2了。这真是太伟大了,当然也意味着需要更多的部署。在同一个时间规则下,保持一个部署速度会变得越来越困难,因为每个工程师都需要在口头上协调他们会在什么顺序上部署代码。为了解决这个问题,我们又新加了一个功能,就是可以自动协调部署队列。工程师将要求部署锁定,并获取或放入队列。这有助于维持部署秩序,让人们在等待的时候放松一下。

随着团队的发展,另一个重要的补充是在核心位置跟踪部署。我们修改了部署工具以将指标发送给Graphite,因此很容易将部署与指标更改相关联。

服务的增长 (2015年同样的问题)

就像突然间,我们有第二个服务上线了。网站的新移动版本正在上线。这是一个完全不同的堆栈,并有自己的服务器和构建过程。这是部署工具解耦API的第一个真正考验。随着对每个项目不同位置构建步骤的增加,它保持不变,并且我们能够在相同的系统下管理这两个服务。

25项服务(2016)

在接下来的一年中,我们看到了Reddit团队的爆炸性增长。我们从这两个服务发展到了二十几个,从两个团队到十五个,我们的大部分服务都建立在Baseplate,后端服务框架,或类似于移动网络的节点应用也都是。基础部分是所有部署的基础,而且会上线越来越多,因为rollpin不在乎它的部署内容。这使得人们可以使用熟悉的工具轻松地开发新的服务。

安全网 (2017)

随着服务器数量的增加,这一块也就变得越来越庞大,部署所需要的时间也越来越多。我们想要用高并行计数来进行部署,但是这样做会导致应用服务器带出现大量的的重启。于是,我们降低了规模,对于传进来的请求也不能及时处理,其它的应用服务器也出现了超载的情况。

Gunicorn的主进程使用了跟uWSGI相同的模型,也会一次重启所有员工的工作进程。当新的工作进程启动时,您无法满足任何请求。我们整体的启动时间为10-30秒,这意味着在此期间,我们将无法提供任何请求。为了解决这个问题,我们把Gunicorn的主进程换成了Stripe的工作经理Einhorn,同时保留了Gunicorn的HTTP stack和WSGI container。Einhorn是通过创建新的进程从而完成重启的,等待它自己准备好,然后收获老的进程,并重复,直到所有的升级。这种机制创建了一个安全网,并允许我们在部署期间可以响应请求。

新模式当然也带来了新问题。就像前面提到的那样,它需要30秒的时间完成进程的替代和启动。这也就是说如果的你代码里面有bug,它不会立即显示,您可以回滚多台服务器。为了防止这种情况产生,我们引入了一种方法,可以阻止部署到另一台服务器上,直到所有的进程都重新启动才可以。很简单,只需要调整einhorn的状态,并且等到所有的机器都准备好就可以了。为了保证速度,我们增加了并发,这现在已经很安全了。

新的机制可以让我们同时对多台服务器进行部署,部署800台机器的时间降低到了7分钟,尽管需要额外的等待。

回顾

部署的基础部分是多年的逐步改进积累而来的,绝不是任何一次大的调整导致的。在当前系统和过去的任何时候,都可以看到历史的阴影和每一次选择的权衡。这样一种演变的方法是有利弊的:在任何一个期间,都不需要花费太多的代价,但是我们最终可能会陷入绝境。重要的是要关注你的发展方向,这样你就可以继续朝着有用的方向前进。

未来

Reddit的基础部分需要能够跟得上团队以及新事物的发展。公司的成长速度达到了Reddit历史上的峰值,与以前相比,我们所从事的项目也越来越有趣。现在我们面临的问题有两部分:提高工程师自主权,同时维护生产基础设施中的系统安全,以及为他们搭建更好的网络环境,让他们更有信心进行部署。

评论