那次我误删了生产数据库
注意:这篇文章发表至今已超过两年,因此其中包含的信息可能已过时。如果您发现了问题,请留下评论,我公司会尽力更正。
2024年2月4日 - 阅读时长28分钟
最近,我想起了一位GitLab工程师误删生产数据库的事情,这也让我回想起自己最严重的一次(生产方面的)失误。
这件事至少发生在5年前了,所以我觉得现在讲述我误删一位客户生产数据库的故事应该没问题。
当时,我公司正在为一家国际组织进行Drupal开发,打造一个相当庞大的Drupal网站。该网站有一个内容丰富的区域,允许用户通过网站进行匿名购买,每天晚上这些购买记录会被发送到客户关系管理系统(CRM)中。网站托管在Acquia平台上,使用BLT进行日常开发操作。
(如果您不了解的话)BLT本质上是一个开发环境、Drush(Drupal命令行工具)及其他一些工具的封装。其理念是,您只需输入一个命令,就可以执行一系列操作。这样一来,您可以运行单元测试、创建部署构件,甚至获取生产环境的最新副本。
同步命令(即 blt sync)在获取生产环境的最新副本方面节省了大量时间。它实际上会删除您的本地数据库,获取生产数据库的副本,并将其导入到本地。此外,还有一些清理步骤,会从数据库中删除所有个人身份信息。
复制数据库需要一些时间,但这意味着您只需一个命令,就能从一无所有到完整安装Drupal。
注意:我已经有几年没用过BLT或Acquia平台了,所以下面展示的是该平台以前的情况。我不会提及客户的名称,但我必须提及Acquia和BLT,因为它们是此次事件背景的一部分。BLT在这5年中经历了许多更新,而且我相信如今的Acquia平台已经大不相同了。
一、问题所在
BLT有一些配置设置,同时还有一个名为 example.local.blt.yml 的文件。这个文件会被重命名为 local.blt.yml,并根据您的网站详情进行调整。它允许您为项目设置Drush别名,这样您就可以在本地和远程平台之间执行同步操作。
local.blt.yml 文件允许对BLT的核心配置进行各种覆盖,下面是该文件中Drush部分的样子:
#drush: # aliases: # local: local.mysite.dev
这个文件不仅在您首次设置项目时会被创建,在执行某些操作时也会被删除。例如,有一个“blt nuke”命令会重置您的工作空间,这个操作会删除该文件;所以有不止一种情况需要编辑这个文件。
默认情况下,该文件不会被纳入版本控制,BLT的 .gitignore 文件会故意忽略它。这意味着有时需要重新创建该文件,以便连接您的不同环境。
我公司为这个网站要做的,就是在 local.blt.yml 文件中添加以下详细信息。这样可以将“local”Drush别名指向我的本地环境,将“remote”别名指向远程环境。
drush:
aliases:
local: local.mysite.dev
remote: remote
由于我总是记不住这些属性,所以经常需要从项目文档中复制粘贴它们。尽管这个文件不是一成不变的,但实际上我们每隔几个月(如果有那么频繁的话)才需要编辑一次,所以记住这些设置的可能性微乎其微。
二、事件经过
一天早上,我公司正在为该网站编写一些设置文档和流程。作为这个过程的一部分,我重置了这个文件,以确保新员工的设置流程能够正常工作。在重新设置时,我错误地复制了该文件的值,导致我的 local.blt.yml 文件最终变成了这样:
drush:
aliases:
local: mysite.prod
remote: self
如果您熟悉BLT,就会发现这里的问题。如果您不熟悉,让我来解释一下。
我在这里告诉BLT的是,“local”别名指向生产网站,而“remote”别名是“self”,也就是“本地”的另一种说法。实际上,我 把本地和生产环境的别名弄反了,所以对本地执行的任何操作都会联系生产网站来执行(反之亦然)。
然后,像之前做过无数次那样,我运行了同步命令,以获取生产数据库的最新副本。
blt sync
这个命令接着开始删除“本地”数据库,并尝试将“远程”数据库复制到该实例中。由于我的配置错误,它删除了生产数据库,并开始将我的(空的)本地数据库上传到生产网站。
同步命令的输出提示我“正在从远程 mysite.prod 实例删除数据库”。看到这个提示,我立刻觉得不对劲,于是赶紧按下Ctrl + C停止了这个过程。
我顿时感到一阵寒意爬上脊背,查看实时网站时,发现出现了Drupal安装界面,这意味着生产数据库已经完全没了。
我停止操作已经太晚了。我刚刚误删了生产数据库。
哎呀……
三、赶紧修复!
意识到发生了什么之后,我立即通知了项目经理,项目经理联系了客户,告知他们情况。
说实话,当时我有那么一瞬间,听到了尖叫声,还在想这叫声是从哪儿来的,然后才意识到是自己在尖叫。这引起了大家的注意,事情也开始有了进展。
好在我们有备份。于是我开始恢复数据库,并查看刚刚丢失了多少数据。
Acquia每天晚上都会对数据库进行完整备份,并将备份文件放在平台上。我们一直使用这些备份将数据库复制到各个非生产环境中,所以我们知道这些备份是可用的。
不幸的是,由于一个小漏洞,网站界面上显示的最新备份与文件系统中实际的最新备份存在细微差异。这意味着网站界面显示的最新备份实际上比实际的最新备份晚了24小时。我在寻找恢复备份时知道这个情况。
然而,由于急于(且慌乱地)让网站恢复在线,我直接去了网站界面,恢复了界面上显示的最新可用备份。结果恢复的是一份已经过期一天的备份。
停机约20 - 30分钟后,网站恢复了在线并正常运行。
然后,我开始尝试从当天早上的网站日志中恢复丢失的交易数据。这时我才意识到,我恢复的数据是一天前的(而不是几小时前的),也就是说我恢复错了备份。
由于网站已经恢复在线并开始接收流量,我不能再进行另一次恢复操作了;已经太晚了。相反,我不得不获取实际的最新备份,并在本地进行恢复,以提取最近一天的内容编辑和用户交易数据。
在确保网站上的数据是最新的之后,接下来就是利用当天早上的日志数据(包括电子邮件日志)填补数据空白。然后,这些数据会交给客户,以便他们将其添加到自己的CRM系统中,这个过程通常是由网站在夜间自动完成的。
尘埃落定后,我们的数据中仅丢失了几笔交易,但由于我们有电子邮件日志,客户能够联系到相关人员并向他们道歉。
四、后续影响
网站恢复运行且数据恢复后,我得向客户解释发生了什么,以及我们将如何防止此类事件再次发生。不幸的是,我不得不承认,问题是由一个文件中的配置值设置错误导致的,而且我们当时还不太确定如何防止此类问题再次发生。
所幸的是,客户非常通情达理,理解这是一个失误,但他们仍然对我公司的处理方式有些担忧。我一直秉持着对客户尽可能坦诚的原则,所以我只能承认这是我的错误,并表示会研究一些预防措施。
恢复了错误的数据库备份是另一个需要解决的问题。但即使我恢复了正确的数据库,我仍然需要从日志中查找丢失的数据。在这种情况下,只是需要向客户提供更多的数据。我能够使用Drupal内置的版本控制工具,查看过去24小时内的内容更新情况,并自行进行恢复。
Acquia的技术支持团队非常乐于助人(且富有同情心)。我和他们就这个网站以及其他问题交流了几个月,所以他们知道这不是技术能力的问题。事实上,他们理解了事情的经过,并提出会向他们的BLT团队咨询,看看能否采取一些措施。
我们还在BLT的问题队列中详细描述了这个问题。我认为这是一个相当严重的问题,因为任何人都可能不小心配置本地环境,从而导致多个生产网站出现故障。
不幸的是,得到的回复基本上是这样的:
呃,别这么做……
谢谢各位,这反馈可真有用。
五、防止未来的灾难
所幸的是,我找到了一个解决方案。
Drush能够创建钩子,您可以在某些操作触发之前运行这些钩子。毕竟这是一个Drupal工具,使用钩子是很常见的。由于BLT是基于Drush构建的,我能够创建一个钩子,拦截Drush的“sql:sync”命令(“blt sync”命令的第一步),并检查是否有任何迹象表明“local”指向了其他地方。
以下是我创建的Drush钩子:
<?php
namespace Drush\Commands;
use Consolidation\AnnotatedCommand\CommandData;
/**
* Drush策略类,用于挂钩Drush命令。
*
* @package Drush\Commands
*/
class PolicyCommands extends DrushCommands {
/**
* 防止灾难性的同步命令。
*
* @hook validate sql:sync
*
* @throws \Exception
*/
public function sqlSyncValidate(CommandData $commandData) {
$target = $commandData->input()->getArgument('target');
if ($this->localTargetIsLocal($target) == FALSE) {
throw new \Exception(dt('您的目标未设置为 "@self" 或 "@site.local"。这可能会导致覆盖生产数据库的灾难性问题。请确保您知道自己在做什么。 (!file)', ['!file' => __FILE__]));
}
}
/**
* 验证此目标是否正确。
*
* @param string $target
* 目标字符串。
*
* @return bool
* 如果目标是本地,则返回 true。
*/
protected function localTargetIsLocal($target) {
if ($target == '@self') {
// 目标是自身,我们假设这是正确的。
return TRUE;
}
if (preg_match('/\.local$/', $target) == 1) {
// 目标是 'local' 别名,这是正确的。
return TRUE;
}
return FALSE;
}
}
我通过断开计算机的网络连接,并使用XDebug逐步调试Drush的执行过程来测试这个钩子,以确保在配置错误的情况下它能正常发出警告。故意将本地环境设置为错误状态让我非常紧张!
经过(非常谨慎的)测试后,我将这个类提交到了代码仓库,并将其纳入了项目。
事实证明,添加这个小类是个非常好的主意,因为仅仅一个月后(真的只有一个月!),团队中的一名开发人员就带着一个问题来找我。他们试图运行“blt sync”操作,但遇到了一个奇怪的“灾难性问题”错误。
仔细检查后发现,他们犯了和我一样的错误,把连接细节弄反了,从而触发了保护类。
可以想象,这个简单的类很快就成为了我们所有项目的标准配置。
在第二次类似事件险些发生后,我们还添加了一个不同的 example.local.blt.yml 文件,其中包含了一些更合理的默认值,以及内置的文档(和警告),这样就不会轻易错误配置系统。
六、结论
我完全承认这是我的错,但好在我能在半小时内让网站恢复在线,这对情况有所帮助。
停机造成了一些收入损失,虽然也有一些数据丢失,但情况本可能更糟。这次事件发生在一个工作日的安静上午,所以我很幸运,当时网站没有处于活动高峰期,否则影响会更严重。
让我震惊的是,这次事件是如此容易发生。在此之前(以及之后),我从未误删过生产数据库,我很惊讶,仅仅是将两个值放错了位置,就能让一个网站瘫痪。当时,BLT团队似乎不太想解决这个漏洞,这让我有点恼火。我不知道当前版本的BLT是否还有同样的问题,但我建议您检查一下。特别是在进行Drupal模块开发、Drupal升级或者使用Drupal11等相关操作时,更要注意这些问题。
我设置的保护机制对于我们使用BLT来说至关重要,并成为了所有BLT项目的标准配置。我在这里发布的类在创建后又进行了多次扩展,以涵盖不同的命令和配置错误类型。
团队中的另一名开发人员在仅仅一个月后就犯了同样的错误,这证明我们找到了正确的解决方案。所幸的是,这次Drush钩子类发挥了作用。
在您的开发职业生涯中,难免会犯错误,重要的是犯错后如何应对。尽量保持冷静。在网页开发中,您的首要任务通常是让网站恢复在线并完全正常运行,之后您可以放松一下,填补数据中的任何空白。
确保与相关人员沟通,让他们了解情况以及您正在采取的修复措施。这是您处理过程中的关键一步,因为它能让客户放心,问题正在解决,但不要一味催促开发人员给出答案;让他们安心工作。
尘埃落定后,有必要审视整个事件,从中吸取教训。这次事件是由配置选项错误导致的,但也可能是流程失误或编码错误造成的。
您在这里花费时间吸取教训,意味着同样的错误未来不会再犯。即使从小错误中吸取教训,也能帮助您避免犯下更大的错误。
更重要的是,要勇于承担错误。我相信,我对客户坦诚相待,如实告知问题,有助于缓和局面。几天后,我带着问题的解决方案(即Drush钩子类)再次与客户沟通,这也表明我们理解了问题所在,并采取了防范措施。
最后,我认为重要的是(我是说 真正 理解)您日常使用的工具;尤其是那些开源工具。一定要深入研究它们,了解其底层原理。在这次事件之前,我曾查看过BLT的内部机制,但这次事件让我对它有了更深入的理解。我真正明白了配置文件的作用,以及它们与Drush的交互方式。我也能够对问题进行排查,确切地找出问题所在。
您是否有愿意分享的灾难故事呢?当然,不会提及任何名称或能识别相关网站的信息。我很想创作更多类似的文章,所以请告诉我。


