如果在学校里教公路,那么有关这一主题的教科书将有这样的任务。 “ N个社交网络有2,000台服务器,其中150,000个文件(每个PHP代码900 MB)和一个用于50台计算机的暂存群集。 该代码每天两次部署到服务器,在暂存群集上每隔几分钟更新一次代码,并且还有其他“修补程序”-小文件集,这些文件在服务器的全部或选定部分上不按顺序排列,而无需等待完整的计算。 问题:这种情况是否被认为是高负载?如何部署它们? 编写至少5个部署选项。” 我们只能梦见有关hyload问题的书,但是现在我们已经知道
Yuri Nasretdinov (
youROCK )肯定会解决此问题并获得“五个”。
Yuri并没有停留在简单的解决方案上,而是另外发表了一份报告,在其中他公开了“代码部署”概念的概念,讨论了用于大型PHP部署的经典和替代解决方案,分析了它们的性能,并介绍了MDK部署系统。
“部署代码”的概念
在英语中,“部署”一词意味着使部队保持警惕,而在俄语中,我们有时会说“将规则填入战斗”,这是同一回事。 您可以使用已编译的代码或原始代码中的代码(如果是PHP),将其下载到服务于用户流量的服务器,然后通过魔术将负载从一种版本切换到另一种版本。 所有这些都包含在“代码部署”的概念中。
部署过程通常包括几个阶段。
- 以任何您喜欢的方式从存储库中获取代码 :克隆,获取,签出。
- 组装建造 。 对于PHP代码,可能缺少构建阶段。 在我们的案例中,通常是自动生成翻译文件,将静态文件上传到CDN以及其他一些操作。
- 交付到终端服务器 -部署。
一切组装完成后,立即部署阶段开始-
代码被
注入到生产服务器中 。 即将在此阶段讨论
Badoo 。
Badoo中的旧部署系统
如果您的文件带有文件系统映像,那么如何挂载它? 在Linux中,您需要创建
一个中间Loop设备 ,向其附加文件,然后您就可以挂载此块设备。
循环设备是Linux挂载文件系统映像所需的拐杖。 在某些操作系统中,不需要此拐杖。

部署过程如何使用文件(为简便起见,我们也称为“循环”)? 有一个目录,其中包含源代码和自动生成的内容。 我们为文件系统拍摄了一个空映像-现在是EXT2,而我们之前使用的是ReiserFS。 我们在临时目录中挂载文件系统的空映像,然后将所有内容复制到该目录中。 如果我们不需要投入生产,那么我们就不会复制所有内容。 之后,卸载设备,并获取必需文件所在的文件系统的映像。 接下来,我们
将映像存档并上传到所有服务器 ,然后在其中解压缩并安装它。
其他现有解决方案
首先,我们要感谢
Richard Stallman-没有他的许可,我们使用的大多数实用程序都不存在。

我通常将部署PHP代码的方法分为4类。
- 基于版本控制系统 :svn up,git pull,hg up。
- 基于rsync实用程序 -到新目录或“在最上面”。
- 部署一个文件 -无论如何:phar,hhbc,循环。
- Rasmus Lerdorf建议的特殊方式是rsync,2目录和realpath_root 。
每种方法都有优点和缺点,因此我们放弃了它们。 更详细地考虑这4种方法。
基于svn up版本控制系统的部署
我选择SVN并不是偶然的-根据我的观察,这种形式的部署恰好在SVN的情况下存在。 该系统非常
轻巧 ,它使您
可以快速轻松地进行部署-只需运行svn即可完成。
但是此方法有一个很大的缺点:如果您加快了速度,并且在更新源代码的过程中,当新请求来自存储库时,他们将看到存储库中不存在的文件系统状态。 您将拥有部分新文件和部分旧文件-这是一种
非原子部署方法 ,不适合高负载,仅适用于小型项目。 尽管如此,我知道仍以这种方式部署的项目,到目前为止,一切都为它们工作。
基于rsync实用程序的部署
有两种方法可以执行此操作:使用该实用程序直接将文件上传到服务器,并在“顶部”上传-更新。
rsync到新目录
由于您首先将所有代码完全倒入服务器上尚不存在的目录中,然后才切换流量,因此此方法是
原子性的 -没有人看到中间状态。 在我们的情况下,创建150,000个文件并删除旧目录(该目录也有150,000个文件)会给
磁盘子系统造成
很大的负担 。 我们非常积极地使用硬盘,经过这样的操作一分钟后,服务器感觉不太舒服。 由于我们有2000台服务器,因此需要填充900 MB 2000次。
如果首先上载到一定数量的中间服务器(例如50个),然后将它们添加到其余中间服务器,则可以改进此方案。 这解决了网络的可能问题,但是创建和删除大量文件的问题并没有消失。

顶部的rsync
如果您使用rsync,那么您将知道此实用程序不仅可以填充整个目录,还可以更新现有目录。 仅发送更改是一个加号,但是由于我们将更改上传到提供战斗代码的同一目录中,因此也会存在某种中间状态-这是减号。
提交更改的工作原理如下。 Rsync在执行部署的服务器端和接收端列出文件列表。 之后,它会统计所有文件中的stat,然后将整个列表发送到接收方。 在正在进行部署的服务器上,考虑这些值之间的差异,并确定应发送哪些文件。
在我们的条件下,此过程大约需要
3 MB的流量和1秒的处理器时间 。 似乎并不多,但是我们有2,000台服务器,并且所有结果至少需要一分钟的处理器时间。 这不是一个快速的方法,但是绝对比通过rsync发送整个事件更好。 它仍然可以解决原子性问题,并且几乎是完美的。
部署一个文件
无论您上传单个文件,使用BitTorrent或UFTP实用程序都相对简单。 一个文件更容易解压缩,可以在Unix上自动替换,并且可以通过计算文件中的MD5或SHA-1数量来检查在构建服务器上生成并交付给目标计算机的文件的完整性(对于rsync,您不知道目标服务器上的内容)
对于硬盘驱动器,顺序记录是一个很大的优势-900 MB的文件将在大约10秒内写入空闲的硬盘驱动器。 但是,仍然需要记录这900 MB的相同内容并通过网络传输它们。
关于UFTP的抒情离题
创建此开放源实用程序最初是为了长时间延迟通过网络(例如,通过基于卫星的网络)传输文件。 但是事实证明,UFTP适用于将文件上传到大量计算机,因为它使用基于多播的UDP协议来工作。 创建一个多播地址,所有要接收文件的机器都订阅了该文件,并且交换机向每台机器提供了数据包副本的传递。 因此,我们转移了将数据传输到网络的负担。 如果您的网络可以处理此问题,则此方法比BitTorrent更好。
您可以在群集上尝试使用此开源实用程序。 尽管它可以在UDP上工作,但它具有NACK机制-否定确认,该机制强制重新转发在传送时丢失的数据包。
这是一种可靠的部署方式 。
单文件部署选项
tar.gz结合了两种方法的缺点的选项。 您不仅需要顺序地将900 MB写入磁盘,然后还需要通过随机读写再次写入相同的900 MB并创建150,000个文件。 该方法的性能甚至比rsync更差。
harPHP支持phar格式的档案(PHP档案),知道如何提供其内容和包含文件。 但是,并非所有项目都易于集成在一起-您需要代码自适应。 只是因为此存档中的代码不起作用。 此外,您无法更改档案中的一个文件(
将来的Yuri:从理论上讲,您仍然可以 ),您需要重新加载整个档案。 同样,尽管phar存档可与OPCache一起使用,但是在部署时,必须丢弃高速缓存,因为否则,旧phar文件中的OPCache中将出现垃圾。
hhbc此方法是HHVM-HipHop虚拟机固有的,由Facebook使用。 这有点像phar存档,但是它不包含源代码,而是HHVM虚拟机的已编译字节代码-来自Facebook的PHP解释器。 禁止更改此文件中的任何内容:您无法创建新的类,函数,并且此模式下的某些其他动态功能被禁用。 由于这些限制,虚拟机可以使用其他优化。 根据Facebook的说法,这可以使代码执行速度提高30%。 对于他们来说,这可能是一个不错的选择。 在这里更改一个文件也是不可能的(
未来的Yuri:实际上是可能的,因为它是基于sqlite的 )。 如果要更改一行,则需要再次重做整个存档。
对于此方法,
禁止使用eval和动态包含。 是这样,但并非完全如此。 可以使用Eval,但如果它不创建新的类或函数,并且不能从此存档之外的目录进行包含,则可以使用Eval。
循环这是我们的旧版本,它有两个很大的优点。 首先,它看起来像一个常规目录
。 您安装了循环,而对于代码而言则无关紧要-它可以在开发环境和生产环境中与文件一起使用。 如果仍需要为生产紧急更改某些内容,则可以在读写模式下挂载第二个循环,并更改一个文件。
但是循环有缺点。 首先,它与docker怪异地工作。 稍后再说。
其次,如果在最后一个循环中将symlink用作document_root,则OPCache会出现问题。 在路径中具有符号链接不是很好,并且开始混淆要使用的文件版本。 因此,在部署时必须重置OPCache。
另一个问题是挂载文件系统
需要超级用户特权 。 而且您一定不要忘记在计算机的启动/重新启动时挂载它们,否则将会有一个空目录而不是代码。
码头工人的问题
如果创建一个Docker容器并在其中放置一个用于安装“循环”或其他块设备的文件夹,则会立即出现两个问题:新的安装点不属于Docker容器,以及那些在创建时已存在的“循环”
无法卸载 Docker容器,因为它们已被Docker容器占用。
自然,这通常与部署不兼容,因为循环设备的数量是有限的,并且不清楚如何将新代码放入容器中。
我们尝试做一些奇怪的事情,例如,提高本地
NFS服务器或使用SSHFS挂载目录,但是由于种种原因,这没有扎根于我们。 结果,在cron中,我们将rsync从最后一个“循环”注册到当前目录,并且每分钟运行一次命令:
rsync /var/loop/<N>/ /var/www/
/var/www/
是提升到容器的目录。 但是,在具有docker容器的计算机上,我们不需要经常运行PHP脚本,因此rsync并不是原子的,它适合我们。 但是,当然,这种方法非常糟糕。 我想制作一个与docker搭配良好的部署系统。
rsync,2个目录和realpath_root
此方法由PHP的作者Rasmus Lerdorf提出,他知道如何部署。
如何进行原子部署,以及以我所谈论的任何方式进行部署? 进行symlink并将其注册为document_root。 在每个时间点,symlink都指向两个目录之一,然后使rsync进入相邻目录,即该代码未指向的目录。

但是问题出现了:PHP代码不知道它在哪个目录中启动。 因此,例如,您需要使用一个变量,该变量将在配置开头的某个地方写入-它可以修复代码从哪个目录运行以及应该从哪个目录包含新文件。 在幻灯片上,它称为
ROOT_DIR
。
访问生产中使用的代码内的所有文件时,请使用此常量。 因此,您获得了atomicity属性:切换符号链接之前到达的请求将继续包含旧目录中的文件,而您在其中未进行任何更改,而符号链接切换之后的新请求将从新目录开始工作并得到服务新代码。

但这需要写在代码中。 并非所有项目都为此做好了准备。
拉斯穆斯风格
Rasmus建议不要手动修改代码并创建常量以稍微修改Apache或使用nginx。

对于document_root,指定到最新版本的符号链接。 如果您有nginx,则可以注册
root $realpath_root
,对于Apache,您将需要一个单独的模块,其设置可以在幻灯片上看到。 它的工作方式是这样-当请求到达时,nginx或Apache偶尔会考虑路径中的realpath(),将其保存在符号链接中,然后将此路径作为document_root传递。 在这种情况下,document_root将始终指向没有符号链接的常规目录,并且您的PHP代码可能不必考虑从哪个目录进行调用。
该方法具有有趣的优势-真正的路径来自OPCache PHP,它们不包含符号链接。 甚至是请求到达的第一个文件都已满,OPCache也不会有问题。 由于使用了document_root,因此它适用于任何PHP项目。 您无需进行任何调整。
它不需要重新加载fpm,在部署期间无需重置OPCache,这就是处理器服务器非常繁忙的原因,因为它必须再次解析所有文件。 在我的实验中,将OPCache重置大约半分钟会使处理器消耗增加2-3倍。 重用它会很好,并且此方法允许您执行此操作。
现在的缺点。 由于不重复使用OPCache,并且有2个目录,因此需要为每个目录在内存中存储文件的副本-在OPCache下,需要的内存要多2倍。
还有另一个限制可能看起来很奇怪-
每个max_execution_time最多只能部署一次 。 否则,将发生相同的问题,因为当rsync进入目录之一时,仍然可以处理来自它的请求。
如果出于某种原因使用Apache,则需要Rasmus也编写
的第三方模块 。
Rasmus说该系统很好,我也向您推荐。 对于99%的项目,它都适用于新项目和现有项目。 但是,当然,我们不是那样的,因此决定写下我们自己的决定。
新系统-MDK
基本上,我们的要求与大多数Web项目的要求没有什么不同。 我们只希望在登台和生产上
快速部署 ,
低资源消耗 ,OPCache重用和快速回滚。
但是还有另外两个需求可能与其他需求不同。 首先,它是
原子应用补丁的能力。 我们将补丁称为对一个或多个文件的更改,这些更改会在生产中产生某些影响。 我们想快点做。 原则上,Rasmus提供的系统正在处理补丁程序任务。
我们也有
可以运行几个小时的 CLI脚本 ,它们仍应使用一致版本的代码。 在这种情况下,上述解决方案很不幸,要么不适合我们,要么我们必须有很多目录。
可能的解决方案:
- 循环xN(-staging,-docker,-opcache);
- rsync xN(-production,-opcache xN);
- SVN xN(-生产,-opcache xN)。
N是几个小时内发生的计算次数。 我们可以有数十个,这意味着需要花费大量空间来存储其他代码副本。
因此,我们提出了一个新系统,并将其称为
MDK。 它代表
Multiversion Deployment Kit (多版本部署工具)。 我们基于以下假设进行了此操作。
我们采用了Git的树存储架构。 我们需要脚本在其中工作的代码具有一致的版本,即我们需要快照。 快照由LVM支持,但在那里,实验文件系统(如Btrfs和Git)无法有效地实现快照。 我们采用了Git快照的实现。
将所有文件从file.php重命名为file.php。 由于我们拥有的所有文件都只是存储在磁盘上,因此如果要存储同一文件的多个版本,则必须在该版本后添加后缀。
我喜欢Go,因此为了提高速度,我在Go上编写了一个系统。Multiversion部署工具包如何工作
我们采用了Git快照的想法。 我稍微简化了一下,并告诉您如何在MDK中实现它。
MDK中有两种类型的文件。 首先是
卡。 下面的图片标记为绿色,并与存储库中的目录相对应。 第二种类型是
直接文件,它们与通常位于同一位置,但具有文件版本形式的后缀。 文件和映射根据其内容进行版本控制,在我们的示例中,简称为MD5。

假设我们具有某种文件层次结构,其中
根映射引用其他映射中文件的某些版本 ,而它们又引用其他文件和映射并修复某些版本。 我们要更改某种文件。

也许您已经看过类似的图片:我们在第二个嵌套级别更改文件,并在相应的映射-map *中更新了三个*文件的版本,对其内容进行了修改,版本也进行了更改-并且版本也在根映射中进行了更改。 如果我们更改了某些内容,则总会得到一个新的根映射,但是所有未更改的文件都将被重用。
链接保持与原来相同的文件。 这是以任何方式创建快照的主要思想,例如,在
ZFS中,它的实现方式几乎相同。
MDK如何位于磁盘上

我们在磁盘上:
与最新的根图的符号链接 -将从Web提供的代码,
根图的多个版本,多个文件(可能具有不同的版本)以及子目录中的对应目录。
我预见到一个问题:“
如何处理Web请求?用户代码将到达哪些文件? ”
是的,我欺骗了您-还有一些没有版本的文件,因为如果您收到对index.php的请求,并且目录中没有该文件,则该站点将无法运行。

所有PHP文件都有文件,我们称其为
存根(Stub) ,因为它们包含两行:从文件中声明require,在文件中声明了知道如何使用这些卡的函数,从文件的所需版本中声明。
<?php require_once "mdk.inc"; require mdk_resolve_path("a.php");
这样做是为了避免与最新版本建立符号链接,因为如果从没有版本的
a.php文件中排除
b.php ,则由于写入了require_once,系统将记住它从哪个根卡开始使用,它将使用它,并且获取文件的一致版本。
对于其余文件,我们只有符号链接到最新版本。
如何使用MDK进行部署
该模型与git push非常相似。
- 发送根图的内容。
- 在接收方,我们查看哪些文件丢失。 由于文件的版本由内容决定,因此我们不需要第二次下载(以后的Yuri:除非缩短的MD5发生冲突,这种情况在生产中仍然会发生 )。
- 请求丢失的文件。
- 我们传递到第二点,然后再转一圈。
例子
假设服务器上有一个名为“ one”的文件。 向其发送根地图。

在根图中,虚线箭头表示指向我们没有的文件的链接。 我们知道它们的名称和版本,因为它们在地图上。 我们从服务器请求它们。 服务器发送,结果证明文件之一也是卡。

我们看起来-我们根本没有一个文件。 同样,我们要求丢失的文件。 服务器发送它们。 剩余的存储卡不多-部署过程已完成。

您可以轻松猜测如果文件为150,000,但其中一个已更改,将会发生什么。 我们将在根地图中看到一个地图丢失了,让我们按嵌套级别进行操作并获取文件。 在计算复杂性方面,该过程与直接复制文件几乎没有什么不同,但是同时,保留了代码的一致性和快照。
MDK没有缺点:)它允许您
快速原子地部署小的更改 ,并且
脚本可以工作数天 ,因为我们可以保留一周内部署的所有文件。 它们将占用相当大的空间。 您还可以重用OPCache,而CPU几乎不会吃东西。
监视是相当困难的,但是可能的 。 所有文件均按内容进行版本控制,您可以编写cron,它将检查所有文件并验证名称和内容。 您还可以检查根映射是否引用了所有文件,其中是否有断开的链接。 此外,在部署期间检查完整性。
您可以
轻松地回滚更改 ,因为所有旧卡都已安装到位。 我们可以扔掉卡,一切都将立即存在。
对我而言,再加上
MDK是用Go编写的事实意味着它可以快速运行。
我再次欺骗了你,仍然存在弊端。 为了使该项目与系统一起工作,
需要对代码进行重大修改,但是比起乍看起来来说,它更简单。
该系统非常复杂 ,如果您没有Badoo这样的要求,我不建议您实施它。 另外,无论如何,地方早晚要结束,因此
需要垃圾收集器 。
我们编写了特殊的实用程序来编辑文件-实际文件,而不是存根文件,例如mdk-vim。 您指定文件,它会找到所需的版本并进行编辑。
MDK数量
我们在暂存阶段有50台服务器,在这些服务器上部署3-5 s
。 与除rsync之外的所有内容相比,它非常快。 在
生产中,我们部署约
2分钟的小补丁
-5-10 s 。
如果由于某种原因您丢失了所有服务器上的代码的整个文件夹(这是永远不会发生的:)),那么
完全上载的过程大约需要40分钟 。 它发生在我们身上一次,尽管在夜间人流很少。 因此,没有人受伤。 第二个文件在一对服务器上放置了5分钟,因此不值得一提。
该系统不是开放源代码的,但是,如果您有兴趣,请在注释中写-它可以被布局(
未来的Yuri:撰写本文时,该系统仍不在开放源代码中 )。
结论
听拉斯姆斯,他没有说谎 。 我认为,尽管循环也可以很好地工作,但它的rsync方法与realpath_root一起使用是最好的。
用脑袋思考 :确切地看项目需要什么,不要试图在有足够“玉米”的地方创建太空飞船。 但是,如果您仍然有类似的要求,那么类似于MDK的系统将适合您。
我们决定返回这个话题,这个话题在HighLoad ++上进行了讨论,也许没有得到应有的重视,因为它只是实现高性能的众多基础之一。 但是,现在我们有一个单独的专门针对PHP的PHP俄罗斯专业会议。 在这里,我们真正发挥了最大作用。 我们将彻底讨论性能 , 标准和工具 ,其中包括重构 。
订阅包含会议计划更新的Telegram频道 ,并于5月17日见。