엔지니어링

Linux에서의 데이터 손상을 예방하기 위한 Canonical, Elastic, Google 팀의 공동 작업

Elastic은 끊임없이 새로운 기능을 혁신하고 출시하고 있습니다. 새로운 기능을 출시할 때는 반드시 테스트를 거쳐 견고하며 안정적으로 작동하도록 작업 중입니다. 때로는 버그나 다른 문제를 발견하기도 합니다.

새로운 기능을 테스트하면서 특정 Linux 커널의 SSD 디스크에 영향을 미치는 Linux 커널 버그를 발견했습니다. 이 블로그 포스팅에서는 그 조사 작업과 긴밀한 두 파트너 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는 복제본으로부터 복구를 시도하게 됩니다. 많은 경우에 복구는 원활하게 이루어지며, 사용자가 로그를 검토하지 않는 한 손상은 눈치채지 못한 상태로 넘어가게 됩니다. 복제본이 제공하는 중복을 통해서도 특정 상황에서는 데이터 손실이 가능합니다. 따라서 왜 손상이 발생하고 있으며 어떻게 그 문제를 해결할 수 있는지를 이해해보고자 했습니다.

처음에는 새로운 Elasticsearch 기능인 CCR/일시 삭제가 문제를 일으키고 있다고 생각했습니다. 그러나 이러한 의심은 단일 클러스터를 사용하고 CCR/일시 삭제 기능을 비활성화하여 인덱스 손상을 재현할 수 있다는 것을 알게 되자 곧 사라졌습니다. 추가로 리서치를 진행한 후, CCR/일시 삭제 베타 출시에 선행하는 Elasticsearch의 이전 버전에서 그 문제를 재현할 수 있다는 것을 알게 되었습니다. 이것은 CCR/일시 삭제가 인덱스 손상의 원인이 아니라는 것을 보여줍니다.

그러나 우리는 인덱스 손상이라는 결과를 낳은 테스트와 리서치가 Google Cloud Platform(GCP)에서 Canonical Ubuntu Xenial 이미지4.15-0-*-gcp 커널의 로컬 SSD를 사용해 실시되었었고, 동일한 운영 체제와 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 처리량은 16,000 IOPS로 초당 160MB를 넘어갔습니다. 이러한 관측은 여러 테스트에 걸쳐 일관되게 나타났습니다. Elasticsearch(기본 설정)는 일부 저장 공간의 경우 메모리 매핑된 파일을 사용하기 때문에, 일부 파일 액세스가 더 많은 주요 페이지 폴트를 야기하는 원인이며, 디스크에 대한 IO 작업의 증가로 이어지고, 결국 문제를 촉발하게 되는 것이라는 생각이 들었습니다. 페이지 폴트의 발생을 줄이기 위해, 우리는 GCP 인스턴스의 메모리를 32GB에서 52GB로 늘려보았습니다. 메모리가 늘어나자마자, 문제는 더 이상 발생하지 않았고 IO 처리량은 4000 IOPS로 초당 50MB였습니다. 

GCP와 베어 메탈 환경 간의 다른 점을 관측했을 때 첫 번째 돌파구가 생겼습니다. GCP 커널은 멀티 큐 블록 레이어(blk_mq)라고 하는 기능을 활성화했고, 베어 메탈 커널은 그렇지 않았습니다[2]. 문제가 더 복잡해진 것은, 특정 버전 이후에[3] 더 이상 커널 옵션을 통해 Ubuntu Linux -gcp 이미지에서 blk_mq[4]를 비활성화할 수 없게 되었기 때문입니다. GCP 지원팀은 GCP의 Ubuntu 이미지를 내보내서 멀티 큐 SCSI[5]를 활성화하는 VIRTIO_SCSI_MULTIQUEUE guestOS 기능 없이 그것을 다시 생성함으로써 blk_mq를 비활성화하는 방법을 보여주었습니다.

두 번째 돌파구는 베어 메탈에서의 손상을 재현할 수 있게 된 것이었습니다. 즉, 보다 이전 커널인 4.13.0-38-generic을 사용해서도 blk_mq를 명시적으로 활성화함으로써만 손상을 재현할 수 있게 되었습니다. 우리는 또한 NVMe 디스크에서는 이러한 문제가 나타나지 않는다는 것을 확인했습니다.

이 시점에서 다음 두 가지 조건이 모두 부합될 때 손상이 발생한다는 것을 알게 되었습니다.

  • SCSI 인터페이스를 사용하는 SSD 드라이브(NVMe 디스크는 영향을 받지 않음)
  • blk_mq 활성화

GCP 지원팀은 (NVMe 디스크만 사용한다는 것 이외에) 두 가지 추가 해결 방법을 공유해 주었습니다. 인스턴스 메모리를 늘리거나 멀티 큐 SCSI를 비활성화한 사용자 정의 인스턴스 이미지를 생성하는 것입니다.

Canonical의 동참

몇 가지 해결 방법을 찾았지만, 아직 만족스럽지는 않았습니다.

  • 문제는 GCP의 Ubuntu 이미지에만 특정한 것이 아니었습니다. 베어 메탈에서도 발생했습니다.
  • 우리는 어느 커널 커밋이 문제를 도입하는지 알 수 없었습니다.
  • 보다 새로운 커널에서 수정이 이미 이루어진 것인지도 알 수 없었습니다.

이러한 점들을 해결하기 위해 우리는 파트너인 Canonical에게 요청하여 좀 더 심층적으로 조사해보았습니다.

Canonical은 Elastic 재현 스크립트를 이용해 방대한 테스트 작업을 시작했고, 먼저 손상이 SSD 드라이브를 사용하는 Ubuntu 메인라인 커널 >=5.0에서는 발생하지 않는다는 것을 확인했습니다(아무 것도 사용하지 않거나 mq-deadline 멀티 큐 I/O 스케줄러 사용).

다음 단계는 커널 버전을 다시 뒤로 돌려 손상을 나타내는 커널과 그렇지 않은 커널 간의 최소 변화량을 찾아내는 것이었습니다. 여러 병행 테스트 환경을 사용해(전체 테스트 실행이 최대 5일까지 걸릴 수 있으므로), Canonical은 4.19.8이 손상 수정이 포함되는 최초의 Ubuntu 메인라인 커널이라는 것을 찾아냈습니다[6].

4.15.0 커널과 그 파생 개체에 대한 백포트 누락은 Canonical의 버그 트래커의 LP#1848739에 설명되어 있습니다. 더 자세한 사항은 이 포스팅kernel.org 버그에서 찾아보실 수 있습니다.

Elastic과 Canonical이 모든 필요한 백포트가 포함된 패치가 적용된 GCP 커널이 문제를 수정한다고 확인한 후에, 이를 Ubuntu 4.15.0 메인 커널로 병합했고 결과적으로 모든 파생 커널(-gcp 포함)에서 문제가 수정되었습니다.

결론

Elastic은 세 개의 주요 솔루션 각각을 개선하는 새로운 Elastic Stack 기능을 개발하고자 최선의 노력을 기울이고 있습니다. 이러한 노력은 사용자가 아무 걱정도 할 필요가 없도록 언제나 깨어서 작업하는 대단히 뛰어난 엔지니어 및 파트너들의 지원을 받아 이루어집니다. 테스트 중에 문제를 발견하는 경우, Elastic과 그 긴밀한 파트너 네트워크는 사용자에게 가능한 최상의 환경을 제공하기 위해 모든 것을 샅샅이 조사하여 해결합니다.

Google 및 Canonical과의 긴밀한 협조를 통해, 우리는 문제의 근본 원인을 파악할 수 있었고, 이는 다음의 수정판 HWE Ubuntu 커널 출시로 이어졌습니다.

위의 버전이나 그 이후 버전을 사용하시면 SSD 디스크가 활성화된 SCSI blk-mq와 함께 사용되는 경우 손상을 피하게 됩니다.

이 데이터 손상으로부터 현재 환경이 보호되고 있는지 여부에 대해 걱정하고 싶지 않으시면, 저희 Elastic Cloud를 사용해 보세요. 저희 사용자는 이미 보호를 받고 계십니다.

각주

[1] Elasticsearch는 비용이 많이 드는 작업이기 때문에 체크섬을 늘 확인하지는 않습니다. 샤드 재배치와 같은 특정 행위가 체크섬 확인을 좀더 빈번하게 촉발할 수는 있습니다. 또는 스냅샷을 찍는 동안 이러한 행위가 원인이 되는 것처럼 밑에서 조용히 손상이 나타나기도 합니다.

[2] SCSI 또는 기기 매퍼 디스크에 대해 blk_mq가 사용 중인지 여부를 확인하려면 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