2017年1月31日,我们的在线服务 GitLab.com 发生了严重的事故。这次事故由误删引起,导致了我们的主数据库数据丢失。

这次事故导致了GitLab服务长时间中断。我们还永久损失了部分生产数据,无法恢复。更严重的是,我们还损失了数据库的相关记录数据,包括项目、注释、用户账户、问题和代码段,这些事情都是在2017年1月31日17:20至00:00发生的。即使是乐观地估计,本次事故也影响到了约5000个项目,5000个评论和700个新用户账户。在事故发生之后,GitLab.com上的代码仓库和wiki都无法使用,不过这些服务并没有丢失数据。GitLab.com的企业级用户,GitHost用户以及自托管的GitLab CE用户则没有收到此次事故的影响,也没有数据丢失。

丢失生产数据是不可接受的。为了确保以后不再发生这种事故,我们对GitLab.com上的操作和数据恢复流程做了多项调整。在本文中,我们会分析发生事故的问题出在哪里,我们如何补救,以及我们要如何才能在以后的时间杜绝此类事故再次发生。

对所有在此次GitLab.com事故中受到影响或丢失数据的用户:我们万分抱歉。我本人作为GitLab的CEO,代表GitLab的每一个人,向大家真诚道歉。

数据库设置

GitLab.com目前使用的是一台主服务器和一台备用服务器的双机热备模式。备用服务器只在主机失效时实现援备。在这种设置下,所有的负载平时都是由主数据库服务器来完成的,这并不是一个理想的架构。主数据库服务器的域名是db1.cluster.gitlab.com,备用数据库服务器的域名是db2.cluster.gitlab.com

其实早在以前,因为采用db1.cluster.gitlab.com一台主服务器为主的架构(单点故障模式),我们就遇到过很多问题。例如:

事件经过

1月31日,一名工程师在我们的工作环境中对多台PostgreSQL服务器进行了设置。原本的计划是使用 pgpool-II 来试试看能不能通过把查询请求分配给可用主机(负载均衡),从而降低数据库整体的负载。这个操作要解决的问题如这里所示:Infrastructure#259

± 17:20 UTC: 在开始操作之前,我们的工程师做了一个生产数据库的LVM快照,然后把它导入到了工作环境里。这么做的目的是为了确保工作数据库备份最新,从而能得到更加精确的负载测试结果。本来这项操作每24小时都要执行一次(定在每天01:00 UTC),而工程师没有用自动操作的版本,因为他想要和生产数据库最接近的备份。

± 19:00 UTC: GitLabs.com开始产生数据库负载上升的情况,我们怀疑是spam造成的。这种现象在本次事件之前也发生过,只是没有这么严重。这次负载上升造成的问题是,一些用户无法对问题进行评论,或者无法进行merge request。我们花了几个小时才让负载重新得到控制。

后来我们发现,造成这次负载飙升的原因是,后台有个作业在移除一个GitLib员工和他相关的用户数据。这些账户数据被错误标记,然后错误地被计划删除,这是负载上升的导火索。这个问题的具体信息可以在这里查到:“移除spam用户时不应该使用硬删除”.

± 23:00 UTC: 因为负载上升了,我们的备用PostgreSQL数据库的备份进程就被拖得非常缓慢。因为备份服务器需要WAL段才能完成备份过程,但是WAL段已经从主服务器上移除了,所以备份失败。GitLab.com没有使用WAL存档,所以备用服务器只能手动重新同步。而这又需要从备用服务器上移除现有的数据和目录,并运行 pg_basebackup,才能实现将数据库从主服务器拷贝到备用服务器。

我们的一个工程师于是把备用服务器上的数据目录全部删除了,然后运行了pg_basebackup。很不幸的是pg_basebackup运行以后就挂起不动了,尽管已经加了–verbose 选项,但还是没有产生任何有意义的输出。尝试了几次pg_basebackup之后,因为没有足够的备份连接数量(由max_wal_senders限制),就无法连接到主服务器了。

为了解决这个问题,我们的工程师决定临时增加 max_wal_senders 的数量,将默认数量从 3 调整到了 32。可是应用这一改变时,PostgreSQL又不能重启了,说创建了太多 semaphores。这种情况很可能在例如 max_connection 设置数量太高的时候发生。在我们的案例里面,这个数值设置是8000。这个值显然设置得太高了,可是它是在一年之前设置的,而且之后运行也一直没问题。为了解决这个问题,我们又把这个设置的值调到了2000,这回PostgreSQL终于可以正常重启了。

很不幸,之前做的事情并没能解决 pg_basebackup无法马上执行备份的问题。一名工程师决定运行一下strace,看看到底卡在哪里。strace显示, pg_basebackup在一个poll call里面挂起了,但是除此之外也提供不了什么更有用的信息了。

± 23:30 UTC: 一名工程师认为,可能是 pg_basebackup在上一次运行的时候,在备份服务器的数据目录里上创建了一些数据文件而导致运行无反应。可是如果是这种情况的话,pg_basebackup应该会报错并打印出错误信息(然而并没有)。所以这名工程师也不敢肯定自己是对的。这个问题稍后才被另外一个工程师解决(一开始他没在周围),说这是正常行为: pg_basebackup会等待主服务器发送备份数据时才会有反应,否则它就这么一直安静地等待。很遗憾,这些东西在工程师运维手册里都没写, pg_basebackup官方文档里也没有。

为了让备份进程能够恢复,一名工程师清空了PostgreSQL的数据库目录,他漫不经心地以为这个操作是在备份服务器上做的。可要命的是,恰恰这个进程是在主服务器上运行的。这名工程师花了一两秒的时间发现了错误,但是这是已经有300G的数据被删除了。

工程师们各处寻找了数据库备份,并且在Slack上求助。遗憾的是,所有的寻找备份的努力都彻底失败了。

Broken Recovery Procedures 事故后的恢复过程

这种局面,我们只能尝试恢复数据了。通常对于这样的事情,人们只需要用最近的数据库备份来恢复就好了,尽管并不能保证100%没有数据丢失。对于GitLab.com,我们本来是有下列手段来支持数据恢复的:

  1. 每24小时使用pg_dum进行一次备份,配备上传到Amazon S3。旧的备份则在某时自动删除。
  2. 每24小时对生产数据库做一次LVM快照。将快照导入工作环境,这样使得我们能够在不影响生产环境的情况下进行安全的测试。不允许直接操作工作数据库和生产数据库。
  3. 我们针对多个服务(比如用来存储Git数据的NFS服务)采用了Azure磁盘快照。这些快照每24小时获取一次。
  4. 对PostgreSQL主机之间的数据拷贝,主要目的是失效备援,而不是灾备。

这次事故的情况,因为备份过程失败,而且数据已经从主服务器和备份服务器上都删除了,所以我们无法从任何一个服务器恢复数据了。

Database Backups Using pg_dump 使用pg_dump进行数据库备份

这次事故发生后我们想通过pg_dump备份来恢复,但是根本没找到备份数据。S3 bucket是空的,里面没有可以用来恢复的近期备份。通过更新一步的检查,我们发现在备份过程中使用的是pg_dump 9.2版本,因为沃恩的的数据库是PostgreSQL 9.6版本(对于Postgres来说,9.x版本号是目前发行的主力版本)。就是这个版本号的微小差异造成了pg_dump备份过程中产生错误,从而也终止了备份过程。

之所以会有数据库版本的差异,是源自于我们的Omnibus packages的工作方式。Omnibus packages目前同时支持PostgreSQL 9.2和9.6两个版本,并允许用户升级(同时支持手动升级或者通过包提供的命令来升级)。为了侦测正确的数据库版本号,Omnibus package需要去检查数据库集群对应的PostgreSQL 版本(通过$PGDIR/PG_VERSION,以及数据目录下的$PGDIR来获取)。当发现是PostgreSQL 9.6版本时,Omnibus就会让所有二进制代码使用PostgreSQL 9.6,否则就用 PostgreSQL 9.2。

pg_dump备份过程是在常规应用服务器上执行的,而不是数据库服务器。因为PostgreSQL数据肯定不在应用服务器上运行,Omnibus(没找到PostgreSQL版本号)所以自动设置成了PostgreSQL 9.2。这一行为就造成了(版本差异)导致pg_dump因错误而终止。

本来这类错误发生后,cronjob是可以通过邮件把错误通知发送出来的。但对于GitLab.com来说,因为我们用的是DMARC。很不幸的是DMARC不允许cronjob发邮件,结果就导致了所有错误消息通知的邮件被拒。这就意味着我们从来就没能意识到备份过程失败了,直到大事不好。

Azure Disk Snapshots Azure 磁盘快照

Azure磁盘快照服务是用来对整盘生成快照的。其实要从这些快照中恢复特定的数据块(比如丢失的用户账户信息)是并不容易的,尽管理论上可以恢复。快照的主要作用是当磁盘坏掉的时候直接对整盘进行恢复。

在Azure中,快照有一个对应的存储账户,然后存储账户又和一个或多个主机依次对应。每个存储账户有大约30T的空间限制。当从恢复快照到对应相同存储账户的主机时,这是很快就能完成的。但是如果你用不同存储账户的主机来恢复快照的时候,这个过程估计要几小时甚至几天才能搞定了。例如,我们就遇到一次这种情况结果花了一个星期才恢复。这也让我们不敢太依赖于快照这种方式。

NFS服务开了快照功能,但是其他的数据库服务器则没开。因为我们觉得我们的其他备份手段足以让我们完成所有的备份和恢复工作了。

LVM Snapshots LVM快照

LVM快照主要是用来从生产坏境拷贝数据到工作环境的。正式因为出于这样的目的,所以产生的快照从来就不是用来做灾备恢复的。在宕机发生的时候,我们手头有两个快照:

  1. 一个快照是我们的工作环境每24小时自动创建的,但是基本上是在宕机发生前24小时左右创建的。
  2. 一个快照是我们的工程师在宕机前6小时左右创建的。

在生成快照时,我们会采取以下步骤:

  1. 对生产环境产生一个快照
  2. 把生产环境快照拷贝到工作环境
  3. 使用这个快照新建一个磁盘
  4. 针对快照产生的数据库,断掉一切网络连接,防止意外事件发生。

恢复GitLab.com

为了恢复GitLab.com,我们决定用事故前6小时产生的那个LVM快照,其实这也是我们减少数据损失唯一的选择(因为另外一个快照是事故前24小时左右产生的,恢复效果肯定不如6小时这个好)。这个恢复过程我们采取了以下步骤:

  1. 将现有的工作数据库拷贝到生产环境,但是断掉所有网络连接。
  2. 同步地,拷贝曾经用来配置数据库的快照,并假设这个快照还包含网络连接信息(其实包含不包含我们也不确认)。
  3. 使用步骤1中的快照设置生产数据库。
  4. 使用步骤2中的快照设置分库。
  5. 使用上一步中的数据库设置来恢复网络连接。
  6. 将所有数据库序列上调100000,确保重新产生的用户ID不会和宕机前的用户ID重复。
  7. 逐渐恢复GitLab.com。

我们的工作环境使用的是Azure classic,并没有开通Premium Storage。这么选择是因为Premium Storage确实很贵。这样的结果是让磁盘操作非常慢,慢到它成为了恢复过程的主要瓶颈。因为LVM快照本身就存储在其产生的主机上,所以我们有两个恢复数据的选择:

  1. 从LVM快照恢复
  2. 从PostgreSQL 数据目录中恢复

无论哪种情况都会涉及到大量的数据拷贝,所以其实也没什么差别。因为拷贝数据目录要简单一些,所以我们选择了第二种。

把数据从工作环境拷贝到生产环境主机花了大约18小时。因为这些存储磁盘都是网盘的形式,所以通信速度非常低(大约60Mbps),而且也没法把这些便宜的存储服务变成premium了,所以就是性能的问题。我们的网络带宽和处理器都不是瓶颈,(造成这么慢的)瓶颈就在存储性能上。2017年1月31日 17:20 UTC ,拷贝完数据以后,我们终于能开始进行数据库恢复(包括网络连接)了。

2017年2月1日 17:00 UTC , 我们努力恢复了Gitlab.com数据库,但是网络连接还没搞好。对于网络连接的恢复是在单独创建的工作数据库环境里用LVM快照来实现的,这样不会触发对网络连接的移除。这种方法能产生一个针对数据库表的SQL dump,然后可以把它导入到我们恢复的GitLab.com数据库里。

大约在18:00 UTC左右,我们完成了最终的恢复操作,包括网络连接操作,并最终确认一切都如预期一样。

关于宕机事故的公开说明

本着开诚布公的目的,我们把所有的过程都记录在了公开的Google文档里。我们还把恢复的过程在Youtube上进行了直播,最高的时候有5000人同时在线收看(收看人数创下了当时段第二)。视频的方式很好地给我们用户传递了整个恢复过程的最新情况。最后,我们还是用了Twitter来向那些看不了直播的受众传递信息。

上述的文档一开始是只对GitLab员工开放的私有文件,因为它包含了当初误删数据导致事故的工程师的名字。名字是工程师自己加上去的(因为他能够直面公众),不过我们还是会对其他名字做了编辑,以免其他工程师因为看到自己的名字出现在公众视野而感到不快。

数据丢失造成的影响

日期范围在2017年1月31日17:20 UTC 到23:30 UTC 之间创建的项目、问题、代码段等数据因本次事故而丢失。Git仓库和WiKi因为是单独存放的,所以没有受到影响。

目前很难估计到数据丢失的精确值,但是我们预计至少丢失了5000个项目,5000条注释,预计大约700个用户的数据信息。本次事故只影响GitLab.com的用户,而对自托管的实例或GitHost实例并无影响。

对GitLab自身的影响

因为GitLab本身也是依托于GitLab.com在管理开发工作,所以宕机事故从某种意义上也给我们的工作带来了困难。部分开发者可以用本地的代码仓库继续工作,但是创建问题等行为却不得不推迟。我们使用了私有GitLab实例发布了一篇博文《GitLab.com数据库事件,这种私有实例一般在我们涉及到私有/敏感工作流程时就会启用(比如涉及到安全发布事宜)。这样能让我们在GitLab.com无法运行时继续新版本网站的开发工作。

我们还有一个对外开放的站点 http://monitor.gitlab.net/ 来做监控。目前这个站点不能处理网站在服务停用期间所产生的用户负载。但幸运的是,我们的内部监控系统(开放站点也是基于我们的内部系统开发的)没有收到此次事故的影响。

根源分析

为了对产生的各种问题进行根源分析,我们采用了一种叫做“5个为什么“的技术。我们把整个事件分成两个主要问题:“GitLab.com宕机”和“花了很长时间才恢复GitLab.com”。

问题1:GitLab.com宕机长达18小时

  1. 为什么Gitlab.com会宕机?——因为主服务器上数据库目录下的主数据库被人为错误地删除了。本来是要删备份服务器上的数据库的,结果不小心删成了主服务器上的数据库。
  2. 为什么数据库目录被删除了?——因为数据库备份过程停掉了,要求重启或重新构建备份服务器。因此就要求PostgreSQL数据库目录必须为空。这一清空操作系统不能自动完成,必须手动,而且在运维文档里也没有明确说明。
  3. 为什么备份过程会停止?——是因为数据库的一次异常负载峰值造成了数据库备份进程停止工作。这很可能是因为主服务器的WAL段在备份之前就被删除而造成的。
  4. 为什么数据库会出现负载突然上升的情况?——这一现象是由于两个事件同时发生造成的:spam数量的增多,以及后台进程企图删除一个GitLab员工数据以及相关的数据。
  5. 为什么GitLab员工数据会被删除?——被删除的员工账号被举报滥用。我们当前用来处理这类举报的系统太忽视报告和细节,因此对该员工信息产生了误删。

问题2:恢复GitLab.com花了18个小时。

  1. 为什么恢复GitLab需要花这么长的时间?——GitLab.com的恢复使用的是工作数据库的拷贝。这个拷贝是托管在速度较慢的Azure虚拟机上的,并且还是不同区(因此更慢)。
  2. 为什么要从工作数据库中去恢复GitLab.com ? —— 因为数据库服务器没开Azure磁盘快照,使用pg_dump从常规的数据库备份中恢复又不成功。
  3. 为什么我们无法从备份服务器数据库中实现恢复?—— 为了让备份过程正常运行,备份数据库服务器上的数据内容被清空了。所以这样就无法从备份服务器上实现灾备恢复了。
  4. 为什么不使用标准的恢复流程? —— 标准的恢复流程需要使用pg_dump来执行数据库的逻辑备份。这个过程因为使用了PostgreSQL 9.2版本而失败,并且没给出任何提示。GitLab.com使用的是PostgreSQL 9.6版本(与9.2有版本差异)。
  5. 为什么备份过程失败却没有任何提示? —— 备份失败后系统是发出了错误消息的,但是这个消息是通过邮件发出,由于邮件被拒收而造成了没人收到错误提示。邮件是系统自动发送的,而且除了邮件之外没有别的方式能接到错误信息了。
  6. 为什么邮件会被拒收? —— 邮件被拒收是因为接收邮件的服务器的问题,这些邮件并不符合DMARC的签收要求。
  7. 为什么Azure磁盘快照功能没有(对数据库)打开? —— 因为我们认为我们其他的备份方式足以应付各种情况了。还有一个原因是,从快照中恢复数据库会非常慢(可能要花几天时间)。
  8. 为什么备份过程(的问题)没有在日常的测试中反应出来? —— 因为这个测试过程根本就没有主人,也就是说在日常的工作中没有人对检验备份过程是否有效负责。

对恢复过程的改进

我们目前的主要工作是要对我们的各种恢复流程进行修订和改进。这项工作包括了以下几个问题:

  1. Update PS1 across all hosts to more clearly differentiate between hosts and environments (#1094) 在所有主机上更新PS1,使得主机与环境之间的差异更加明显
  2. Prometheus monitoring for backups (#1095) 使用普罗米修斯系统来监测备份过程(缩短备份周期以及解决DMARC报警邮件发不出去的问题)
  3. Set PostgreSQL’s max_connections to a sane value (#1096) 将PostgreSQL的最大连接数设置到合理的范围
    1. Investigate Point in time recovery & continuous archiving for PostgreSQL (#1097)  启用PostgreSQL检查点恢复以及持续归档功能
  4. Hourly LVM snapshots of the production databases (#1098) 对生产数据库开启周期为一小时的LVM快照
  5. Azure disk snapshots of production databases (#1099)  对上产数据库开启Azure磁盘快照
  6. Move staging to the ARM environment (#1100) 将工作环境移至ARM(Azure Resource Manager )环境
  7. Recover production replica(s) (#1101) 恢复生产环境备份
  8. Automated testing of recovering PostgreSQL database backups (#1102) 自动测试对PostgreSQL数据库备份的恢复
  9. Improve PostgreSQL replication documentation/runbooks (#1103) 改进PostgreSQL备份文档/操作手册
  10. Investigate pgbarman for creating PostgreSQL backups (#1105) 调研评估使用pgbarman来创建PostgreSQL备份的方法(宕机过程中,有人建议GitLab使用pgbarman来进行数据恢复,但是当时GitLab并没有人用过这种方法)
  11. Investigate using WAL-E as a means of Database Backup and Realtime Replication (#494) 调研使用WAL-E来做数据库备份和实时备份的方案
  12. Build Streaming Database Restore 构建流式数据库恢复机制
  13. Assign an owner for data durability 指定专人负责数据的持久性

我们目前还着手搭建多个备用服务器以及配置负载均衡,你可以在下面的列表中找到细节:

我们关注的重点在于如何改进我们的灾备机制,并在主机上凸显出数据恢复的作用。所以,我们并不会从“阻止工程师在生产主机上运行某个命令“这种角度来实现安全。因为,即使我们把禁用rm命令,也只能是阻止工程师不要犯运行 rm -rf /important-data 命令的错误,但是这种方式并不能阻止诸如磁盘损坏,或者其他可能导致数据丢失的情况发生。

我们认为理想的环境,应该是那种即使你犯了错误删了数据,也能轻易恢复,并保证对系统影响最小的环境。这就要求你要日常执行一些流程,并且要容易测试,容易回滚。例如,我们现在就在着手建立一些流程,让开发者可以对数据库迁移进行测试。具体细节可见:“在工作环境中执行和撤销Rials迁移的工具”.

我们还有一个研究目标,就是针对整个GitLab.com基础设施(不仅仅是数据库),如何构建一个更好的恢复机制。并保证该机制有专人负责。详细信息见:“如何灾备恢复除数据库以外的其他资源”.

为了让备份监控更智能,我们开始创建监控仪表板,你可以在http://monitor.gitlab.net/dashboard/db/backups看到。当前的仪表板仅包含我们对pg_dump备份的监控,但是我们打算把更多的数据加进来。

人们可能会注意到,目前我们的pg_dump备份的日期还是3天前。我们是在备用服务器上执行这些pg_dump备份的,所以可能会给数据库造成很大的压力。因为我们目前正在着手重建我们的备用服务器,所以pg_dump流程现在会停用一段时间。不过也不用过于担心,因为我们现在把LVM快照的时间从原来的一天一次提高到一小时一次。启用Azure磁盘快照也是我们最近在考虑的事情。

最后,我们还研究了如何改进我们的举报和响应系统。详细信息可见:“被spam的用户移除不应该使用硬删除”

如果你还有关于如何阻止类似事件再发生的好办法,请在评论中留言。