[关闭]
@xuemingdeng 2017-08-09T09:12:13.000000Z 字数 4659 阅读 780

Reddit的代码部署演化进程(deprecated)

运维

Reddit的资深工程师Neil WilliamsSaurabh Sharma在Reddit官方博客上分享了Reddit代码部署的演化进程。原作者已经授权翻译本文,原文链接 The Evolution of Code Deploys at Reddit

Reddit 从来没有停止过部署代码。工程师们编写代码,代码经过审查、提交,最后定期发布到生产环境。几乎每周都会发生200次这样的部署,一次端到端的部署大概不到10分钟。

这套部署系统这些年一直在演化,下面就让我们一起来回顾一下整个演化过程。

故事开端:可重复的一致性部署(2007年-2010年)

当前这套系统的鼻祖是一个叫作 push 的 Perl 脚本。在写这个脚本的时候,Reddit 还处在初级的发展阶段,当时的整个过程团队可以塞到一个小型的会议室里。那个时候,Reddit 还没有部署到 AWS 上。整个站点就是一个使用 Python 开发的大单体,叫作 r2,它运行在固定的几台服务器上,通过手动增加容量。

这些年一直保持不变的是我们使用了负载均衡器,负载均衡器对请求进行分类,并把它们分配给不同类型的应用服务器组(pool)。例如,listing和comment页面的请求分别使用不同的服务器组来处理。虽然r2进程可以处理任意一种请求,不过通过分组,可以分流流量高峰,如果发生故障也不会彼此影响。

此处输入图片的描述

push工具包含了一个硬编码的服务器清单,用于部署单体应用。它会遍历所有的应用服务器,通过SSH登录服务器,运行一系列预设的命令来更新服务器的代码,然后重启服务器。下面是精简的代码示例。

  1. # 构建静态文件,并将它们放到静态资源服务器上
  2. `make -C /home/reddit/reddit static`
  3. `rsync /home/reddit/reddit/static public:/var/www/`
  4. # 遍历应用服务器,更新代码
  5. # 然后重启服务器
  6. foreach $h (@hostlist) {
  7. `git push $h:/home/reddit/reddit master`
  8. `ssh $h make -C /home/reddit/reddit`
  9. `ssh $h /bin/restart-reddit.sh`
  10. }

部署过程是串行化进行的,逐台服务器部署。这种简单性给我们带来了一些好处,因为我们可以进行canary部署。如果在部署了几台服务器之后发生了一些异常,我们就知道代码里引入了一些缺陷,我们通过Ctrl+C的方式退出部署,并尽快回滚,以免影响到整个环境。因为部署的简单性,我们可以很容易地在生产环境尝试新的功能,如果有问题,我们可以很快地回退。当然,我们要保证每次只进行一个部署,如果出现新的错误就一定是部署引起的,这样就可以知道在什么时候回退些什么内容。

这些可以保证部署的一致性和可重复性,而且速度很快,一切看起来都很好。

团队的增长(2011年)

后来我们的工程师人数增长到了6个人,需要大一点的会议室才能容得下。我们开始感觉在部署方面需要更好的协调,特别是当有人在家里远程办公的时候。我们修改了 push 工具,通过 IRC 聊天机器人来决定部署的开始和结束。机器人做的事情很简单,只要负责触发部署事件。部署流程似乎没有什么变化,只不过现在系统替你做了这些事情,而且会让所有人都知道你做的事情。

我们就是从这个时候开始在部署流程里使用机器人的。在这个阶段,我们的系统通过聊天机器人进行大量的部署,不过因为我们使用的是第三方的 IRC 服务器,在生产环境我们不能完全信任聊天系统,所以信息的交流是单向的。

随着网站流量的增长,基础设施也需要随之增长。我们不得不启用一批新的应用服务器。这个过程仍然需要人工的参与,包括更新 push 工具的主机清单。

在扩展容量时,我们每次会新增一组服务器。在遍历主机的时候,每一组服务器里的主机都是连续的,而不是混杂了来自不同组的主机。

此处输入图片的描述

我们使用 uWSGI 来管理worker,当我们需要重启应用程序时,它会杀掉现有的进程并启动新的进程。新进程在启动过程中需要一点时间,不能立即处理请求。如果重启的刚好是整个服务器组,就会导致整个组在一段时间内无法处理请求。于是,我们在服务器部署方面做出了一些限制。随着服务器数量的增长,部署的时间也随着增加。

重构过的部署工具(2012年)

我们对部署工具进行了彻底的重构,我们使用 Python 来重写部署工具,为了避免混淆,这个工具仍然叫作 push。我们在新版本的工具上做出了一些改进。

首先,它从 DNS 服务器上获取主机清单,而不是使用硬编码的清单。我们在更新主机清单的时候就不需要再去修改这个工具了,这也算得上是一个初步的服务发现系统。

为了解决服务器串行化重启的问题,在部署之前我们把清单的顺序打乱。来自不同组的主机就混杂在一起,我们就可以更快地遍历主机,从而加快了部署速度。

此处输入图片的描述

最开始,我们每次都会打乱主机清单,但这样就无法很快地回退代码,因为我们不能保证每次部署的都是同一批服务器。于是,我们使用种子(seed)来打乱清单顺序,那么在回退代码的时候就可以重用之前的顺序。

另一个比较重要的变化是只部署固定版本的代码。之前的工具会拉取 master 分支的代码,但如果有人在部署过程中提交了代码,导致 master 发生变化,那该怎么办?于是我们使用 git 的修订版(revision)代替了分支,这样可以确保部署的总是相同的版本。

最后,新工具将它的代码(这部分代码用于获取主机清单并通过 SSH 登录这些主机)和运行命令区分开来。它仍然主要用于满足 r2 的需求,不过也提供了各种各样的 proto-API。r2 可以控制自己的部署步骤,发布新的变更也变得更加容易。下面的例子列出了主要的部署命令。

  1. sudo /opt/reddit/deploy.py fetch reddit
  2. sudo /opt/reddit/deploy.py deploy reddit f3bbbd66a6
  3. sudo /opt/reddit/deploy.py fetch-names
  4. sudo /opt/reddit/deploy.py restart all

其中 fetch-names 这一步是 r2 独有的步骤。

自动伸缩器(2013年)

后来我们决定使用云服务,通过云服务实现自动伸缩。在网站流量不是很大的时候可以帮我们省下不少钱,而在流量增长时又可以自动伸缩。

从DNS获取主机清单的能力让这个迁移过程变得相当顺畅。主机清单频繁发生变动,较之前有过之而无不及,但对于工具来说没有任何区别,而自动伸缩器的启用才真正带来实质性的改观。

不过,自动伸缩也带来了一些有趣的问题。天下没有免费的午餐。比如,如果在部署过程中刚好有一台服务器启动,我们该怎么办?我们必须确保新启动的服务器能够获得新的代码。又或者,在部署过程中服务器被关闭了呢?那么此时工具必须能够检到测服务器是什么时候被关闭的,而不只是报告部署过程出现了问题。

后来出于各种原因,我们从uWSGI切换到Gunicorn,不过这对于我们来说并没有什么差别。

服务器增长(2014年)

随着时间推移,用于处理高峰期流量的服务器数量越来越多,这意味着部署的时间越来越长。最糟糕的情况下,一个正常的部署需要将近一个小时。这实在太糟糕了。

我们重写了部署工具,新版的部署工具叫作rollingpin。旧工具把很多时间花在初始化ssh连接和等待执行命令上,而新工具的并行处理可以加快部署速度,把部署时间又降回到5分钟。

为了降低一次性重启多个服务器所带来的影响,部署工具在挑选服务器时要更加智能。新工具并不是盲目地挑选服务器,而是交错地从不同的服务器组中挑选服务器,尽量减小对整个网站的影响。

新工具最重要的变化在于它与服务器之间的API有更清晰的定义,而且解除了与r2之间的耦合。最初我们只是想着让它对开源友好,后来发现它真的很有用。这里有一个部署的例子,高亮的命令部分表示在远程执行API。

此处输入图片的描述

员工增长(2015年)

突然间,好像有很多人在做r2,这意味着需要更多的部署。遵循一次只进行一个部署的规则变得越来越困难,工程师们需要协调发布的顺序。为了解决这个问题,我们给聊天机器人增加了一个新特性,让它来协调部署队列。工程师向机器人申请部署锁,要么获得部署锁,要么把部署放入队列。这样就可以确保部署的次序,工程师在等待锁的过程中可以做一些其他的事情。

另一个问题是在团队增长时如何在中心位置对部署进行跟踪。我们修改了部署工具,让它向Graphite发送度量指标,这样就可以很容易地跟踪部署的变更。

两种服务(2015年)

后来,移动版的网站上线了,它采用了完全不同的技术栈,有自己的服务器和构建流程。部署工具的解耦API在这个时候才真正派上用场。因为每个项目可以在不同的位置进行构建,我们就可以通过同一个系统来管理所有的服务。

25种服务(2016年)

在接下来的一年,Reddit团队呈现出爆发式的增长。我们从两种服务发展到几十种服务,也从两个团队发展到15个团队。我们的大部分服务是通过我们的后端服务框架Baseplate来构建的,或者它们是类似移动Web的nodejs应用。部署基础设施对于它们来说已经具卑足够的通用性,因为rollingpin并不关心部署的是哪一种应用。人们可以很容易地使用他们熟悉的工具来部署新的服务。

安全网(2017年)

因为单体的服务器越来越多,部署时间也变得越来越长。我们想进行高度并行的部署,但这样会导致多台服务器同时重启,降低了处理请求的能力,而且会让其他服务器超载。

Gunicorn的主进程使用了与uWSGI相同的模型,会一次性重启所有的worker。新worker在启动过程中无法处理任何请求。我们单体的启动时间从10秒到30秒不等,这意味着在这段时间内,我们无法处理任何请求。为了解决这个问题,我们使用Stripe的Einhorn替代了Gunicorn,但保留了Gunicorn的HTTP模块和WSGI容器。Einhorn在重启worker时会先创建新的worker,等到新的worker准备就绪再把旧的worker回收掉,直到所有的worker都升级成最新的。我们因此拥有了一个安全网,在部署过程当中仍然具备服务能力。

不过新的模型也引入了一个问题。就像之前提到的,替换一个worker需要30秒钟的时间,也就是说,如果代码里有缺陷,在它暴露出来之前你已经将它部署到多台服务器上。为了避免这种情况,我们引入了一种机制,我们轮询Einhorn的状态,并等待新worker准备就绪,在所有worker准备就绪之前不允许部署下一台服务器。为了加快速度,我们加大了并行数,因为现在进行并行部署是安全的。

新的机制允许我们进行大量的并行部署,部署时间下降了很多,800台服务器只需要7分钟。

回顾

这套部署基础设施是经过多年渐进式的改进才得到的,并不是一蹴而就的事情。从当前的系统就可以看出历史发展的痕迹以及在发展的每一步所作出的权衡。这种演变方式有好也有坏,虽然每一次改变都能为我们节省时间,但一旦出了问题就会让我们全军覆没。我们要时刻小心我们所走的路,保证朝着正确的方向前进。

未来

Reddit的基础设施在支持团队增长的同时,还要不断地进行构建部署。Reddit的增长速度处于历史制高点,我们的项目比以往任何时候都要来得有趣。我们现今面临的问题主要来自两个方面:提高工程师的自制能力,同时保证生产环境基础设施的安全,并为工程师提供一个安全网,让他们能够放心地进行快速的部署。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注