(点击
上方公众号
,可快速关注)
编译:伯乐在线 - 黄小非
如有好文章投稿,请点击 → 这里了解详情
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上求助。遗憾的是,所有的寻找备份的努力都彻底失败了。
事故后的恢复过程
这种局面,我们只能尝试恢复数据了。通常对于这样的事情,人们只需要用最近的数据库备份来恢复就好了,尽管并不能保证100%没有数据丢失。对于GitLab.com,我们本来是有下列手段来支持数据恢复的:
-
每24小时使用pg_dum进行一次备份,配备上传到Amazon S3。旧的备份则在某时自动删除。
-
每24小时对生产数据库做一次LVM快照。将快照导入工作环境,这样使得我们能够在不影响生产环境的情况下进行安全的测试。不允许直接操作工作数据库和生产数据库。
-
我们针对多个服务(比如用来存储Git数据的NFS服务)采用了Azure磁盘快照。这些快照每24小时获取一次。
-
对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 磁盘快照
Azure磁盘快照服务是用来对整盘生成快照的。其实要从这些快照中恢复特定的数据块(比如丢失的用户账户信息)是并不容易的,尽管理论上可以恢复。快照的主要作用是当磁盘坏掉的时候直接对整盘进行恢复。
在Azure中,快照有一个对应的存储账户,然后存储账户又和一个或多个主机依次对应。每个存储账户有大约30T的空间限制。当从恢复快照到对应相同存储账户的主机时,这是很快就能完成的。但是如果你用不同存储账户的主机来恢复快照的时候,这个过程估计要几小时甚至几天才能搞定了。例如,我们就遇到一次这种情况结果花了一个星期才恢复。这也让我们不敢太依赖于快照这种方式。
NFS服务开了快照功能,但是其他的数据库服务器则没开。因为我们觉得我们的其他备份手段足以让我们完成所有的备份和恢复工作了。
LVM快照
LVM快照主要是用来从生产坏境拷贝数据到工作环境的。正式因为出于这样的目的,所以产生的快照从来就不是用来做灾备恢复的。在宕机发生的时候,我们手头有两个快照:
-
一个快照是我们的工作环境每24小时自动创建的,但是基本上是在宕机发生前24小时左右创建的。
-
一个快照是我们的工程师在宕机前6小时左右创建的。
在生成快照时,我们会采取以下步骤:
-
对生产环境产生一个快照
-
把生产环境快照拷贝到工作环境
-
使用这个快照新建一个磁盘
-
针对快照产生的数据库,断掉一切网络连接,防止意外事件发生。
恢复GitLab.com
为了恢复GitLab.com,我们决定用事故前6小时产生的那个LVM快照,其实这也是我们减少数据损失唯一的选择(因为另外一个快照是事故前24小时左右产生的,恢复效果肯定不如6小时这个好)。这个恢复过程我们采取了以下步骤:
-
将现有的工作数据库拷贝到生产环境,但是断掉所有网络连接。
-
同步地,拷贝曾经用来配置数据库的快照,并假设这个快照还包含网络连接信息(其实包含不包含我们也不确认)。
-
使用步骤1中的快照设置生产数据库。
-
使用步骤2中的快照设置分库。
-
使用上一步中的数据库设置来恢复网络连接。
-
将所有数据库序列上调100000,确保重新产生的用户ID不会和宕机前的用户ID重复。
-
逐渐恢复GitLab.com。
我们的工作环境使用的是Azure classic,并没有开通Premium Storage。这么选择是因为Premium Storage确实很贵。这样的结果是让磁盘操作非常慢,慢到它成为了恢复过程的主要瓶颈。因为LVM快照本身就存储在其产生的主机上,所以我们有两个恢复数据的选择: