工程

Canonical、Elastic 和 Google 联手防止 Linux 中出现数据损坏

在 Elastic,我们一直在不断创新并发布新功能。 在我们发布新功能的同时,也在努力确保让这些功能经过测试,并达到安全、稳定和可靠的运行状态,但有时确实会出现这样或那样的问题。

在测试一项新功能时,我们发现了一个 Linux 内核故障,会影响到某些 Linux 内核上的 SSD 磁盘。在这篇博客文章中,我们将介绍这项调查的来龙去脉,以及我们是如何与 Google Cloud 和 Canonical 这两个密切合作伙伴携手展开调查的。

调查结果是发布了新的 Ubuntu 内核来解决这个问题

调查的来龙去脉

早在 2019 年 2 月,我们使用标准的 Elasticsearch 基准测试工具 Rally,对跨集群复制 (CCR) 进行了为期 10 天的稳定性测试,期间使用的是更新量繁重的采集工作负载。Elasticsearch 版本是(当时)尚未发布的 7.0 分支提交的最新版本。几天后,测试失败,Elasticsearch 日志报告显示:

… 
Caused by: org.apache.lucene.index.CorruptIndexException: checksum failed (hardware problem?)

一旦检测到[1]索引损坏,Elasticsearch 将尝试从副本中恢复。在许多情况下,恢复是无缝进行的,除非用户查看日志,否则不会注意到损坏。即使副本提供了冗余,在某些情况下也可能造成数据丢失。因此,我们希望了解损坏发生的原因,并探究如何补救这一问题。

起初,我们怀疑是 CCR/软删除这项新的 Elasticsearch 功能导致了这个问题。但这种怀疑很快就被打消了,因为我们发现在使用单个集群并禁用 CCR/软删除功能的情况下也可以重现索引损坏问题。经过进一步的研究,我们发现,在 CCR/软删除公测版启动之前的 Elasticsearch 早期版本中,我们可以重现这个问题,这说明 CCR/软删除并不是导致索引损坏的原因。

不过,我们也确实注意到,出现索引损坏的测试和研究是在 Google Cloud Platform (GCP) 上进行的,使用的是 Canonical Ubuntu Xenial 映像本地 SSD,内核为 4.15-0-*-gcp,并且我们无法在具有相同操作系统和 SSD 磁盘的裸机环境(使用 4.13 或 4.15 HWE 内核)中重现这一问题。

接下来我们的注意力立即转向了 Elastic Cloud,希望获取有关该问题及其影响的更多数据。在并行工作流中,我们通过测试不同的文件系统、禁用 RAID,最后使用较新的主线内核,排除了许多疑点,但这些都不能避免发生损坏。

在 Google Cloud 的支持下进一步挖掘

此时,我们创建了一套简单的复制脚本来简化复制,并与我们的合作伙伴 Google 一起开始深入研究环境问题。

GCP 支持人员能够使用提供的脚本可靠地重现问题,并提供了一条错误消息:

org.apache.lucene.index.CorruptIndexException: codec footer mismatch (file 
truncated?): actual footer=-2016340526 vs expected footer=-1071082520 
(resource=BufferedChecksumIndexInput(MMapIndexInput(path="/home/dl/es/data/nodes/0/indices/IF1vmFH6RY-MZuNfx2IO4w/0/index/_iya_1e_Lucene80_0.dvd")))

在问题发生的时间段内,IO 吞吐量超过 160MB/s,IOPS 为 16,000。在好几次测试中都出现了上述观察结果。由于 Elasticsearch(默认情况下)使用内存映射文件来存储部分内容,因此,我们怀疑某些文件访问导致了更多严重的页面错误,进而导致磁盘的 IO 操作量增加,并最终触发了该问题。为了减少页面错误的发生,我们尝试将 GCP 实例的内存从 32GB 增加到 52GB。随着内存的增加,这个问题不再出现,IO 吞吐量为 50MB/s,IOPS 为 4,000。 

当我们观察到 GCP 和裸机环境之间的差异时,这个问题首次有了突破:GCP 内核启用了一个叫作多队列块层 (blk_mq) 的功能,而裸机内核没有[2]。更复杂的是,在特定版本[3]之后,无法再通过内核选项在 Ubuntu Linux -gcp 映像上禁用 blk_mq[4]。GCP 支持人员向我们展示了如何通过导出 GCP 的 Ubuntu 映像并在不使用 VIRTIO_SCSI_MULTIQUEUE guestOS 功能(该功能可启用多队列 SCSI[5])的情况下,重新创建它来禁用 blk_mq。

第二个突破是设法在裸机上重现损坏:只有通过显式启用 blk_mq,甚至使用旧的内核 4.13.0-38-generic,才有可能实现。我们还验证了 NVMe 磁盘,确认不会显示此问题。

至此,我们知道,当同时满足以下两个条件时,就会发生损坏:

  • SSD 驱动器使用 SCSI 接口(NVMe 磁盘不受影响)
  • 启用了 blk_mq

GCP 支持人员分享了(除了仅使用 NVMe 磁盘外的)另外两种解决方法:增加实例内存或在禁用多队列 SCSI 的情况下创建自定义实例映像。

Canonical 加入了我们的调查行列

尽管我们有了一些解决方法,但我们仍不满意:

  • 这个问题并不是 GCP 上的 Ubuntu 映像特有的问题,它也发生在裸机上。
  • 我们不知道是哪个内核提交导致了这个问题。
  • 我们不知道较新的内核中是否已经有了修复程序。

为了解决这些问题,我们联系了我们的合作伙伴 Canonical 一道进行深入的研究。

Canonical 使用 Elastic 复制脚本开始了大规模的测试工作,首先确认了在使用 SSD 驱动器(不使用或使用 mq-deadline 多队列 I/O 调度程序)且版本 >=5.0 的 Ubuntu 主线内核上没有发生损坏。

下一步是倒推内核版本,找出出现损坏的内核和没有损坏的内核之间的最小增量。通过使用多个并行测试环境(因为整个测试运行最多可能需要五天),Canonical 发现 4.19.8 是第一个包含损坏修复程序的 Ubuntu 主线内核[6]。

Canonical 的故障跟踪器在 LP#1848739 下描述了 4.15.0 内核及其派生版本缺少的反向端口,更多细节可以在本文kernel.org bug 中找到。

在 Elastic 和 Canonical 确认打已修补的 GCP 内核(包含所有必要的反向端口)修复这个问题之后,它们被合并到 Ubuntu 4.15.0 主内核中,因此,所有派生内核(包括 -gcp)都收到了修复程序。

结论

Elastic 始终致力于开发新的 Elastic Stack 功能,不断改进我们的三大主要解决方案。这些努力得到了一些才华横溢的工程师和合作伙伴的鼎力支持,我们时刻保持警惕,所以您不必过于担心。如果我们在测试过程中发现问题,请记住,Elastic 及其密切的合作伙伴网络将不遗余力地查证问题,确保您拥有最好的体验。

通过与 Google 和 Canonical 的密切合作,我们得以弄清问题的真相,从而发布了以下装有修复程序的 HWE Ubuntu 内核:

如果在启用 SCSI blk-mq 的情况下使用 SSD 磁盘,使用上述或更高版本可避免发生损坏。

如果您想保护自己的环境不受数据损坏,那么请试试我们的 Elastic Cloud 吧,我们的用户已经受到很好地保护了。

脚注

[1] Elasticsearch 不会一直验证校验和,因为这是一项昂贵的操作。某些操作可能会更频繁地触发校验和验证,例如分片重定位或在拍摄快照时,使潜在的静默损坏看起来好像是由这些操作引起的。

[2] 要检查 blk_mq 是否正在用于 SCSI 或设备映射器磁盘,请使用 cat /sys/module/{scsi_mod,dm_mod}/parameters/use_blk_mq

[3] 在 https://patchwork.kernel.org/patch/10198305 之后,blk_mq 被强制用于 SCSI 设备,并且无法通过内核选项禁用。这个补丁在 Ubuntu linux-gcp 上进行了向后移植。

[4] 要禁用 blk_mq,需要通过 grub 将以下参数传递给内核:GRUB_CMDLINE_LINUX="scsi_mod.use_blk_mq=N dm_mod.use_blk_mq=N"。可以通过将这些选项设置为 N 来启用,但要注意 [3]。

[5] 通过重新创建 Ubuntu 映像来禁用 VIRTIO_SCSI_MULTIQUEUE guestOS 功能的示例 gcloud 命令:

# gcloud compute images export --image-project ubuntu-os-cloud --image-family ubuntu-1604-lts --destination-uri=gs://YOUR_BUCKET/ubuntu_16.04.tar.gz 
# gcloud compute images create ubuntu-1604-test --family ubuntu-1604-lts --source-uri=gs://YOUR_BUCKET/ubuntu_16.04.tar.gz

[6] 反向端口

    - blk-mq: quiesce queue during switching io sched and updating nr_requests 
    - blk-mq: move hctx lock/unlock into a helper 
    - blk-mq: factor out a few helpers from __blk_mq_try_issue_directly 
    - blk-mq: improve DM's blk-mq IO merging via blk_insert_cloned_request feedback 
    - dm mpath: fix missing call of path selector type->end_io 
    - blk-mq-sched: remove unused 'can_block' arg from blk_mq_sched_insert_request 
    - blk-mq: don't dispatch request in blk_mq_request_direct_issue if queue is busy 
    - blk-mq: introduce BLK_STS_DEV_RESOURCE 
    - blk-mq:Rename blk_mq_request_direct_issue() into 
      blk_mq_request_issue_directly() 
    - blk-mq: don't queue more if we get a busy return 
    - blk-mq: dequeue request one by one from sw queue if hctx is busy 
    - blk-mq: issue directly if hw queue isn't busy in case of 'none' 
    - blk-mq: fix corruption with direct issue 
    - blk-mq: fail the request in case issue failure 
    - blk-mq: punt failed direct issue to dispatch list