VKontakte 创建的历史可以在维基百科上找到;这是 Pavel 自己讲述的。 看来大家都已经认识她了。 关于 HighLoad++ Pavel 网站的内部、架构和结构
阿列克谢·阿库洛维奇 (
四年多来,我一直在处理与后端相关的各种任务。
- 上传、存储、处理、分发媒体:视频、直播、音频、照片、文档。
- 基础设施、平台、开发者监控、日志、区域缓存、CDN、专有 RPC 协议。
- 与外部服务集成:推送通知、外部链接解析、RSS feed。
- 帮助同事解决各种问题,而这些问题的答案需要深入研究未知的代码。
在此期间,我参与了该网站的许多组件。 我想分享这个经验。
通用架构
像往常一样,一切都从接受请求的服务器或服务器组开始。
前台服务器
前端服务器通过 HTTPS、RTMP 和 WSS 接受请求。
HTTPS - 这些是对网站的主网络版本和移动网络版本的请求:vk.com 和 m.vk.com,以及我们 API 的其他官方和非官方客户端:移动客户端、即时通讯工具。 我们有接待处 RTMP- 具有独立前端服务器的直播流量和 WSS- 流 API 的连接。
对于服务器上的 HTTPS 和 WSS 来说,这是值得的 nginx的。 对于 RTMP 广播,我们最近改用了我们自己的解决方案 基夫,但这超出了报告的范围。 为了容错,这些服务器通告公共 IP 地址并分组运行,这样如果其中一台服务器出现问题,用户请求就不会丢失。 对于 HTTPS 和 WSS,这些相同的服务器会对流量进行加密,以便自行承担部分 CPU 负载。
我们不会进一步讨论 WSS 和 RTMP,而只讨论标准的 HTTPS 请求,这些请求通常与 Web 项目相关。
后端
前端后面通常有后端服务器。 它们处理前端服务器从客户端收到的请求。
它 kPHP 服务器,HTTP 守护进程正在其上运行,因为 HTTPS 已解密。 kPHP 是一个运行在 预叉型号:启动一个主进程,一堆子进程,将侦听套接字传递给它们,然后它们处理它们的请求。 在这种情况下,进程不会在用户的每个请求之间重新启动,而只是将其状态重置为原始零值状态 - 一个又一个请求,而不是重新启动。
负载分配
我们所有的后端都不是一个可以处理任何请求的巨大机器池。 我们他们 分成不同的组:一般、移动、api、视频、staging...单独一组机器上的问题不会影响所有其他机器。 如果视频出现问题,听音乐的用户甚至不会知道问题所在。 将请求发送到哪个后端由前端的nginx根据配置决定。
指标收集和重新平衡
为了了解每组中需要有多少辆车,我们 不依赖QPS。 后端不同,它们有不同的请求,每个请求计算QPS的复杂度不同。 这就是为什么我们 我们将服务器负载的概念作为一个整体进行操作 - CPU 和性能.
我们有数千台这样的服务器。 每个物理服务器运行一个kPHP组来回收所有核心(因为kPHP是单线程的)。
内容服务器
CS或Content Server是一个存储。 CS 是一个服务器,用于存储文件并处理上传的文件以及主 Web 前端分配给它的各种后台同步任务。
我们有数以万计的物理服务器来存储文件。 用户喜欢上传文件,我们喜欢存储和共享它们。 其中一些服务器被特殊的 pu/pp 服务器关闭。
聚氨酯/聚丙烯
如果您在 VK 中打开网络选项卡,您会看到 pu/pp。
什么是pu/pp? 如果我们关闭一台又一台服务器,则有两种选项可以将文件上传和下载到关闭的服务器: 直 通过 http://cs100500.userapi.com/path
или 通过中间服务器 - http://pu.vk.com/c100500/path
.
Pu是照片上传的历史名称,pp是照片代理。 即一台服务器用于上传照片,另一台服务器用于上传。 现在不仅加载了照片,而且还保留了名称。
这些服务器 终止 HTTPS 会话从存储中删除处理器负载。 此外,由于用户文件是在这些服务器上处理的,因此这些计算机上存储的敏感信息越少越好。 例如,HTTPS 加密密钥。
由于这些机器被我们的其他机器关闭,我们可以不给它们“白色”外部IP,并且 给“灰色”。 通过这种方式,我们节省了 IP 池并保证保护机器免受外部访问 - 根本没有 IP 可以进入其中。
共享 IP 的弹性。 在容错方面,该方案的工作原理是相同的——几台物理服务器有一个公共的物理IP,它们前面的硬件选择将请求发送到哪里。 稍后我会讨论其他选项。
争议点在于,在本案中 客户端保持较少的连接。 如果多台机器有相同的 IP - 具有相同的主机:pu.vk.com 或 pp.vk.com,则客户端浏览器对一台主机的同时请求数量有限制。 但在 HTTP/2 无处不在的时代,我相信这不再那么重要了。
该方案的明显缺点是它必须 泵送所有流量,通过另一台服务器进入存储。 由于我们通过机器泵送流量,因此我们还无法使用相同的方案泵送大量流量,例如视频。 我们直接传输它 - 专门用于视频的单独存储的单独直接连接。 我们通过代理传输较轻的内容。
不久前我们得到了 proxy 的改进版本。 现在我将告诉您它们与普通的有何不同以及为什么这是必要的。
周日
2017年XNUMX月,此前收购了Sun的Oracle,
pp 有一些问题。 每组一个 IP - 缓存无效。 多台物理服务器共享一个公共IP地址,无法控制请求将发送到哪台服务器。 因此,如果不同的用户访问同一个文件,那么如果这些服务器上有缓存,则该文件最终会出现在每个服务器的缓存中。 这是一个非常低效的方案,但却无能为力。
最后 - 我们无法分割内容,因为我们无法为该组选择特定的服务器 - 他们有一个共同的 IP。 另外,由于一些内部原因,我们有 不可能在地区安装此类服务器。 他们只站在圣彼得堡。
对于太阳队,我们改变了选拔制度。 现在我们有 选播路由:动态路由、任播、自检守护进程。 每台服务器都有自己的独立 IP,但有一个公共子网。 一切都以这样的方式配置:如果一台服务器发生故障,流量会自动分布到同一组的其他服务器上。 现在可以选择特定的服务器, 无冗余缓存,并且可靠性没有受到影响。
重量支撑。 现在我们可以根据需要安装不同功率的机器,而且,一旦出现临时问题,可以改变正在工作的“太阳”的重量,以减轻它们的负载,让它们“休息”并重新开始工作。
按内容 id 分片。 关于分片的一个有趣的事情是:我们通常对内容进行分片,以便不同的用户通过同一个“sun”访问同一个文件,这样他们就有一个共同的缓存。
我们最近推出了“Clover”应用程序。 这是直播中的在线问答,主持人提出问题,用户实时回答,选择选项。 该应用程序有一个聊天功能,用户可以在其中聊天。 可以同时连接广播 超过100万人。 他们都会编写发送给所有参与者的消息,并且头像会随消息一起出现。 如果十万人来追求一个“太阳”中的一个化身,那么它有时可以滚到云端。
为了承受对同一文件的突发请求,我们针对某种类型的内容启用了一种愚蠢的方案,将文件分布在该地区所有可用的“太阳”上。
阳光从里面来
nginx 上的反向代理,缓存在 RAM 或快速 Optane/NVMe 磁盘上。 例子: http://sun4-2.userapi.com/c100500/path
— 指向“太阳”的链接,该链接位于第四个区域,第二个服务器组。 它关闭物理上位于服务器 100500 上的路径文件。
缓存
我们在我们的架构方案中添加了一个节点——缓存环境。
下面是布局图 区域缓存,大约有20个。 这些是缓存和“太阳”所在的地方,它们可以通过自身缓存流量。
这是多媒体内容的缓存;这里不存储任何用户数据——仅存储音乐、视频、照片。
为了确定用户所在的区域,我们 我们收集各地区公布的 BGP 网络前缀。 在回退的情况下,如果我们无法通过前缀找到 IP,我们还必须解析 geoip 数据库。 我们通过用户的IP来确定区域。 在代码中,我们可以查看用户的一个或多个区域 - 这些点是他在地理位置上最接近的点。
它是如何工作的呢?
我们按地区统计文件的受欢迎程度。 有多个用户所在的区域缓存和一个文件标识符 - 我们采用这一对并在每次下载时增加评级。
与此同时,恶魔——区域中的服务——时不时地来到 API 中并说:“我是某某缓存,给我一份我所在区域中尚未出现的最流行文件的列表。 ” API 提供一堆按评级排序的文件,守护进程下载它们,将它们带到区域并从那里传送文件。 这是 pu/pp 和 Sun 与缓存之间的根本区别:它们立即通过自身提供文件,即使该文件不在缓存中,并且缓存首先将文件下载到自身,然后开始将其返回。
在这种情况下我们得到 内容更贴近用户 并分散网络负载。 例如,仅在莫斯科缓存中,我们在高峰时段分发的数据量就超过 1 Tbit/s。
但也有问题—— 缓存服务器不是橡胶。 对于超级流行的内容,有时没有足够的网络来容纳单独的服务器。 我们的缓存服务器是 40-50 Gbit/s,但有些内容完全堵塞了这样的通道。 我们正在努力实现该地区流行文件的多个副本的存储。 我希望我们能在今年年底前实施它。
我们研究了总体架构。
- 接受请求的前端服务器。
- 处理请求的后端。
- 由两种类型的代理关闭的存储。
- 区域缓存。
这张图中缺少什么? 当然是我们存储数据的数据库。
数据库或引擎
我们称它们不是数据库,而是引擎——引擎,因为我们实际上没有普遍接受的意义上的数据库。
这是必要的措施。。 发生这种情况是因为在 2008-2009 年 VK 流行度爆发式增长时,该项目完全运行在 MySQL 和 Memcache 上,出现了问题。 MySQL 喜欢崩溃和损坏文件,之后就无法恢复,而 Memcache 的性能逐渐下降,必须重新启动。
事实证明,这个日益流行的项目有持久存储,会损坏数据,还有缓存,会减慢速度。 在这种情况下,很难开发一个不断增长的项目。 我们决定尝试重写该项目在我们自己的自行车上关注的关键问题。
解决成功了。 这样做是有机会的,也是极其必要的,因为当时不存在其他扩展方式。 那时还没有一堆数据库,NoSQL 还不存在,只有 MySQL、Memcache、PostrgreSQL——仅此而已。
通用操作。 开发由我们的 C 开发团队领导,一切都以一致的方式完成。 无论使用何种引擎,它们都具有大致相同的写入磁盘的文件格式、相同的启动参数、以相同的方式处理信号,并且在边缘情况和问题的情况下表现大致相同。 随着引擎的增长,管理员操作系统变得方便——没有需要维护的动物园,每一个新的第三方数据库都需要重新学习如何操作,这使得快速、便捷地操作系统成为可能。方便地增加他们的数量。
发动机类型
该团队编写了相当多的引擎。 这里只是其中的一些:朋友、提示、图像、ipdb、信件、列表、日志、memcached、meowdb、新闻、nostradamus、照片、播放列表、pmemcached、沙箱、搜索、存储、喜欢、任务……
对于每个需要特定数据结构或处理非典型请求的任务,C 团队都会编写一个新引擎。 为什么不。
我们有一个单独的引擎 memcached,它与普通的类似,但有很多好东西,并且不会减慢速度。 不是 ClickHouse,但它也可以工作。 单独提供 内存缓存 - 持久化内存缓存,它还可以将数据存储在磁盘上,而且不适合存储在RAM中,以免重新启动时丢失数据。 对于各个任务有不同的引擎:队列、列表、集合——我们的项目需要的一切。
集群
从代码的角度来看,无需将引擎或数据库视为进程、实体或实例。 该代码专门适用于集群和引擎组 - 每个簇一种类型。 假设有一个 memcached 集群 - 它只是一组机器。
代码根本不需要知道服务器的物理位置、大小或数量。 他使用特定的标识符进入集群。
为此,您需要在代码和引擎之间添加一个实体 - 代理.
RPC代理
代理人 连接巴士,几乎整个网站都在其上运行。 同时我们有 没有服务发现 — 相反,该代理有一个配置,它知道所有集群和该集群的所有分片的位置。 这就是管理员所做的。
程序员根本不关心花费多少、在哪里以及花费什么——他们只关心集群。 这让我们有很多。 当接收到请求时,代理会重定向请求,知道在哪里 - 它自己决定这一点。
在这种情况下,代理是防止服务故障的一个保护点。 如果某个引擎变慢或崩溃,则代理会理解这一点并向客户端做出相应的响应。 这允许您删除超时 - 代码不会等待引擎响应,而是了解它不工作并且需要以某种不同的方式表现。 必须为数据库并不总是有效的事实准备代码。
具体实现
有时我们仍然非常希望有某种非标准的解决方案作为引擎。 同时,决定不使用专门为我们的引擎创建的现成的 rpc-proxy,而是为该任务创建一个单独的代理。
对于 MySQL(我们仍然到处都有),我们使用 db-proxy,对于 ClickHouse - 小猫屋.
它的工作原理一般是这样的。 有一个特定的服务器,它运行 kPHP、Go、Python - 一般来说,任何可以使用我们的 RPC 协议的代码。 该代码在 RPC 代理上本地运行 - 代码所在的每个服务器都运行其自己的本地代理。 根据请求,代理知道要去哪里。
如果一个引擎想要到另一个引擎,即使它是邻居,也会通过代理,因为邻居可能在另一个数据中心。 引擎不应依赖于了解除自身以外的任何物体的位置——这是我们的标准解决方案。 但当然也有例外:)
所有引擎均按照 TL 方案运行的示例。
memcache.not_found = memcache.Value;
memcache.strvalue value:string flags:int = memcache.Value;
memcache.addOrIncr key:string flags:int delay:int value:long = memcache.Value;
tasks.task
fields_mask:#
flags:int
tag:%(Vector int)
data:string
id:fields_mask.0?long
retries:fields_mask.1?int
scheduled_time:fields_mask.2?int
deadline:fields_mask.3?int
= tasks.Task;
tasks.addTask type_name:string queue_id:%(Vector int) task:%tasks.Task = Long;
这是一个二进制协议,最接近的类似物是 原型缓冲区。 该模式预先描述了可选字段、复杂类型 - 内置标量的扩展和查询。 一切都按照这个协议进行。
基于 TCP/UDP 的 TL 上的 RPC...UDP?
我们有一个 RPC 协议,用于执行在 TL 方案之上运行的引擎请求。 这一切都通过 TCP/UDP 连接进行。 TCP很好理解,但是为什么我们经常需要UDP呢?
UDP 有帮助 避免服务器之间大量连接的问题。 如果每台服务器都有一个 RPC 代理,并且一般情况下它可以到任何引擎,那么每台服务器就有数以万计的 TCP 连接。 有负载,但是没用。 在UDP 的情况下,这个问题不存在。
无冗余 TCP 握手。 这是一个典型的问题:当启动新引擎或新服务器时,会立即建立许多 TCP 连接。 对于小型轻量级请求,例如 UDP 有效负载,代码和引擎之间的所有通信都是 两个UDP数据包: 一只朝一个方向飞,第二只朝另一个方向飞。 一次往返 - 代码在没有握手的情况下收到了引擎的响应。
是的,一切正常 丢包率非常低。 该协议支持重传和超时,但是如果我们丢失很多,我们将得到几乎 TCP,这并不是有利的。 我们不会跨洋推动 UDP。
我们有几千台这样的服务器,方案是一样的:每台物理服务器上安装一组引擎。 它们大多是单线程的,以便尽可能快地运行而不会阻塞,并作为单线程解决方案进行分片。 同时,我们没有什么比这些引擎更可靠的了,并且非常关注持久数据存储。
持久数据存储
引擎写入二进制日志。 binlog 是一个文件,在其末尾添加了状态或数据更改的事件。 在不同的解决方案中它的调用方式不同:二进制日志,
为了防止引擎在重启时多年重新读取整个binlog,引擎会写入 快照 - 当前状态。 如果有必要,他们会先读取它,然后再从 binlog 中读取完毕。 所有二进制日志都根据 TL 方案以相同的二进制格式编写,以便管理员可以使用他们的工具平等地管理它们。 不需要快照。 有一个通用标头,指示谁的快照是 int、引擎的魔力以及哪个主体对任何人都不重要。 这是记录快照的引擎的问题。
我将快速描述操作原理。 有一台运行引擎的服务器。 他打开一个新的空二进制日志进行写入,并写入一个更改事件。
在某个时刻,他要么决定自己拍摄快照,要么收到信号。 服务器创建一个新文件,将其整个状态写入其中,将当前的 binlog 大小(偏移量)附加到文件末尾,然后继续进一步写入。 不会创建新的 binlog。
在某个时刻,当引擎重新启动时,磁盘上将会同时存在 binlog 和快照。 引擎读取整个快照并在某个点提升其状态。
读取创建快照时的位置以及 binlog 的大小。
读取 binlog 的末尾以获取当前状态并继续写入更多事件。 这是一个简单的方案;我们所有的引擎都按照它工作。
数据复制
因此,我们的数据复制 基于语句的 — 我们在 binlog 中写入的不是任何页面更改,而是 变更请求。 与网络上的非常相似,仅略有修改。
相同的方案不仅用于复制,还用于 创建备份。 我们有一个引擎——一个写入binlog的写入主机。 在管理员设置的任何其他地方,都会复制此二进制日志,就是这样 - 我们有一个备份。
如果需要 阅读副本为了减少CPU的读取负载,只需启动读取引擎,读取binlog的末尾并在本地执行这些命令。
这里的滞后非常小,并且可以找出副本落后主服务器多少。
RPC代理中的数据分片
分片是如何工作的? 代理如何了解要发送到哪个集群分片? 代码并没有说:“发送 15 个分片!” - 不,这是由代理完成的。
最简单的方案是firstint — 请求中的第一个数字。
get(photo100_500) => 100 % N.
这是一个简单的内存缓存文本协议的示例,但是,当然,查询可以是复杂的和结构化的。 该示例采用查询中的第一个数字以及除以簇大小后的余数。
当我们想要拥有单个实体的数据局部性时,这非常有用。 假设 100 是一个用户或组 ID,我们希望一个实体的所有数据都位于一个分片上以进行复杂查询。
如果我们不关心请求如何在集群中传播,还有另一种选择 - 散列整个分片.
hash(photo100_500) => 3539886280 % N
我们还获得哈希值、除法的余数和分片编号。
只有当我们准备好这样一个事实时,这两个选项才有效:当我们增加集群的大小时,我们会将其拆分或增加数倍。 例如,我们有 16 个分片,我们还不够,我们想要更多 - 我们可以安全地获得 32 个分片而无需停机。 如果我们不想增加倍数,就会出现停机,因为我们无法在没有损失的情况下准确地分割所有内容。 这些选项很有用,但并不总是有用。
如果我们需要添加或删除任意数量的服务器,我们使用 像 Ketama 一样在环上进行一致的哈希处理。 但与此同时,我们完全失去了数据的局部性;我们必须将请求合并到集群,以便每个部分返回自己的小响应,然后将响应合并到代理。
有非常具体的要求。 看起来像这样:RPC代理接收请求,确定去哪个集群并确定分片。 然后,要么有写入主节点,要么,如果集群有副本支持,则它会按需发送到副本。 代理完成这一切。
日志
我们以多种方式写入日志。 最明显和简单的一个是 将日志写入内存缓存.
ring-buffer: prefix.idx = line
有一个关键前缀 - 日志的名称,一行,还有该日志的大小 - 行数。 我们从 0 到行数减 1 之间取一个随机数。memcache 中的键是与该随机数连接的前缀。 我们将日志行和当前时间保存到该值中。
当需要读取日志时,我们执行 多获取 所有key,按时间排序,从而实时获取生产日志。 当您需要实时调试生产中的某些内容时,可以使用该方案,而不会破坏任何内容,也不会停止或允许到其他机器的流量,但该日志不会持续很长时间。
为了可靠地存储日志,我们有一个引擎 日志引擎。 这正是它被创建并在大量集群中广泛使用的原因。 据我所知,最大的集群可存储 600 TB 的打包日志。
发动机很旧,有些集群已经有6-7年的历史了。 它有一些问题我们正在努力解决,例如我们开始积极使用ClickHouse来存储日志。
ClickHouse中收集日志
该图显示了我们如何走进我们的引擎。
有一些代码通过 RPC 在本地发送到 RPC 代理,并且它了解去往引擎的位置。 如果我们想在ClickHouse中写入日志,我们需要改变这个方案中的两部分:
- 用ClickHouse替换一些引擎;
- 将无法访问 ClickHouse 的 RPC 代理替换为可以通过 RPC 访问的解决方案。
该引擎很简单 - 我们将其替换为 ClickHouse 的服务器或服务器集群。
为了去 ClickHouse,我们做了 小猫屋。 如果我们直接从KittenHouse转到ClickHouse,它就应付不了了。 即使没有请求,它也是由大量机器的 HTTP 连接累加起来的。 为了使该方案发挥作用,请在具有 ClickHouse 的服务器上 引发本地反向代理,其编写方式使其能够承受所需的连接量。 它还可以相对可靠地在自身内部缓冲数据。
有时我们不想在非标准解决方案中实现RPC方案,例如在nginx中。 因此,KittenHouse具有通过UDP接收日志的能力。
如果日志的发送者和接收者在同一台机器上工作,则在本地主机内丢失 UDP 数据包的可能性非常低。 作为在第三方解决方案中实现RPC的需要和可靠性之间的折衷,我们简单地使用UDP发送。 稍后我们将回到这个方案。
监控
我们有两种类型的日志:管理员在其服务器上收集的日志和开发人员通过代码编写的日志。 它们对应于两种类型的指标: 系统和产品.
系统指标
它适用于我们所有的服务器
产品指标
为了方便,我们写了很多东西。 例如,有一组普通函数允许您将 Counts、UniqueCounts 值写入统计数据,然后将其发送到更远的地方。
statlogsCountEvent ( ‘stat_name’, $key1, $key2, …)
statlogsUniqueCount ( ‘stat_name’, $uid, $key1, $key2, …)
statlogsValuetEvent ( ‘stat_name’, $value, $key1, $key2, …)
$stats = statlogsStatData($params)
随后,我们可以使用排序和分组过滤器,并从统计中执行我们想要的所有操作 - 构建图表、配置看门狗。
我们写得很 许多指标 每天的事件数量为 600 亿到 1 万亿个。 然而,我们想保留它们 至少几年了解指标的趋势。 把它们放在一起是一个我们尚未解决的大问题。 我会告诉你过去几年它是如何运作的。
我们有编写这些指标的函数 到本地内存缓存以减少条目数量。 曾在短时间内在本地推出 统计守护进程 收集所有记录。 接下来,恶魔将指标合并到两层服务器中 日志收集器,它汇总了我们一堆机器的统计数据,这样它们后面的层就不会消失。
如果有必要,我们可以直接写入日志收集器。
但是绕过 stas-daemonm 直接从代码写入收集器是一种可扩展性很差的解决方案,因为它增加了收集器的负载。 仅当由于某种原因我们无法在计算机上启动 memcache stats-daemon,或者它崩溃了而我们直接使用时,该解决方案才适用。
接下来,日志收集器将统计数据合并到 喵数据库 - 这是我们的数据库,它也可以存储指标。
然后我们可以从代码中进行二进制“near-SQL”选择。
实验
2018 年夏天,我们举办了一次内部黑客马拉松,想到了尝试用可以在 ClickHouse 中存储指标的东西来替换图中的红色部分。 我们在 ClickHouse 上有日志 - 为什么不尝试一下呢?
我们有一个通过 KittenHouse 写入日志的方案。
我们决定 在图表中添加另一个“*House”,它将按照我们的代码通过 UDP 写入的格式准确接收指标。 然后这个 *House 将它们变成插入物,比如 KittenHouse 可以理解的原木。 他可以完美地将这些日志传递给ClickHouse,ClickHouse应该能够读取它们。
带有memcache、stats-daemon 和logs-collectors 数据库的方案被替换为这个方案。
带有memcache、stats-daemon 和logs-collectors 数据库的方案被替换为这个方案。
- 这里有一个来自代码的调度,它是在 StatsHouse 本地编写的。
- StatsHouse 将已转换为 SQL 插入的 UDP 指标批量写入 KittenHouse。
- KittenHouse 将它们发送到 ClickHouse。
- 如果我们想读取它们,那么我们可以绕过 StatsHouse 来读取它们 - 使用常规 SQL 直接从 ClickHouse 读取它们。
还是 实验,但我们喜欢它的结果。 如果我们解决了该方案的问题,那么也许我们会完全转向它。 就我个人而言,我希望如此。
方案 不节省铁。 需要更少的服务器,不需要本地统计守护进程和日志收集器,但 ClickHouse 需要比当前方案更大的服务器。 需要更少的服务器,但它们必须更昂贵、更强大.
部署
首先,我们来看一下PHP的部署。 我们正在发展于 混帐: 使用 GitLab и 团队城市 用于部署。 开发分支合并到主分支,从主分支合并到测试分支,然后从测试分支合并到生产分支。
在部署之前,将获取当前生产分支和前一个生产分支,并在其中考虑差异文件 - 更改:创建、删除、更改。 此更改记录在特殊的 Copyfast 引擎的 binlog 中,该引擎可以快速将更改复制到我们的整个服务器群。 这里用的不是直接复制,而是 八卦复制,当一台服务器将更改发送到其最近的邻居时,将更改发送到其邻居时,依此类推。 这使您可以在数十秒内更新整个队列的代码。 当更改到达本地副本时,它将这些补丁应用到其本地副本 本地文件系统。 回滚也是按照同样的方案进行。
我们也大量部署了 kPHP,并且它也有自己的开发 混帐 根据上图。 从此 HTTP 服务器二进制文件,那么我们就无法生成 diff - 发布的二进制文件有数百 MB。 因此,这里还有另一个选择——版本被写入 二进制日志快速复制。 随着每次构建,它都会增加,并且在回滚期间它也会增加。 版本 复制到服务器。 本地 copyfast 看到新版本已进入 binlog,并通过相同的八卦复制,他们为自己获取最新版本的二进制文件,而不会让我们的主服务器感到疲倦,而是小心地在网络上分散负载。 接下来是什么 优雅地重新启动 对于新版本。
对于我们的引擎来说,本质上也是二进制的,该方案非常相似:
- git master 分支;
- 二进制输入 deb文件。;
- 该版本被写入binlog copyfast;
- 复制到服务器;
- 服务器取出一个新的.dep;
- dpkg-i;
- 优雅地重新启动到新版本。
不同之处在于我们的二进制文件打包在档案中 deb文件。,当抽出它们时 dpkg-i 被放置在系统上。 为什么 kPHP 部署为二进制文件,而引擎部署为 dpkg? 事情就是这样发生的。 它有效 - 不要碰它。
相关链接:
- 安东·基留什金的报告
“Vkontakte 系统管理员。 如何?” 包含有关 Copyfast 和八卦的详细信息。 - 尤里·纳斯雷迪诺夫的报告
“VK 如何将数万台服务器的数据插入到 CLickHouse 中” . - 我的报告
“使用 VKontakte 示例的不断发展的项目的架构” ,但从发展的角度来看,不是硬件。
阿列克谢·阿库洛维奇 (Alexey Akulovich) 是作为程序委员会的一员,帮助帮助
PHP 俄罗斯 17月XNUMX日将成为近期PHP开发者最盛大的盛会。 看看我们有一台多么酷的电脑,什么讲者 (其中两个正在开发 PHP 核心!) - 如果您编写 PHP,这似乎是您不能错过的东西。
来源: habr.com