사전 GitLab 데이터 삭제 사고
사고사례

GitLab 데이터 삭제 사고

gabury1

2017년 1월 31일 GitLab.com 의 운영 데이터베이스 디렉토리가 실수로 지워진 사고입니다. 서비스는 여러 시간 동안 멈췄습니다. 갖춰 뒀다고 믿은 백업과 복제 수단이 하나도 듣지 않았습니다. 결국 여섯 시간 분량의 데이터베이스 데이터를 되찾지 못했습니다.

상세

GitLab 은 포스트모템에서 이 중단의 원인을 주 데이터베이스 서버에서 데이터가 우발적으로 제거된 것이라고 적습니다. 대상은 온라인 서비스인 GitLab.com 한 곳입니다.

사라진 것은 데이터베이스 안의 변경분입니다. 프로젝트, 댓글, 사용자 계정, 이슈, 스니펫 같은 데이터베이스 데이터의 변경이 여기에 들어갑니다. 포스트모템의 요약 대목은 그 구간을 1월 31일 17:20 부터 00:00 UTC(Coordinated Universal Time, 협정 세계시) 사이라고 적습니다. 같은 글의 「Data loss impact」 절은 17:20 부터 23:30 UTC 사이에 만들어진 것이 사라졌다고 적습니다. 사고 당일 공개된 글은 여섯 시간 분량의 데이터베이스 데이터를 잃었다고 적습니다.

분량은 추정치입니다. GitLab 은 정확히 얼마나 잃었는지 가늠하기 어렵다고 적으면서, 최소한 프로젝트 약 5,000건, 댓글 약 5,000건, 사용자 약 700명으로 추정한다고 적습니다.

남은 것의 경계도 뚜렷합니다. GitLab.com 에 올려둔 Git 저장소와 위키는 별도로 저장되기 때문에 제거되지 않았습니다. 중단 동안 접근은 되지 않았지만 데이터 유실의 영향은 받지 않았습니다. GitLab Enterprise 고객, GitHost 고객, 자체 호스팅 GitLab CE 사용자는 중단도 데이터 유실도 겪지 않았습니다.

경과

시각은 모두 UTC 입니다. 포스트모템과 사고 당일 공개된 문서가 적은 시각을 그대로 옮깁니다.

sequenceDiagram
    participant 주서버 as 주 서버
    participant 보조서버 as 보조 서버
    participant 스테이징 as 스테이징 환경
    주서버->>스테이징: 17:20 LVM 스냅샷을 실음
    주서버--x보조서버: 23:00 복제 실패
    보조서버->>보조서버: 데이터 디렉토리를 비우고 pg_basebackup
    주서버->>주서버: 23:00 무렵 데이터 디렉토리 삭제
    Note over 주서버: 23:27 중단. 300 GB 중 4.5 GB 남음
    스테이징->>주서버: 02-01 스냅샷을 복사해 복구

1월 31일

17:20. 작업을 시작하기 전에 엔지니어가 운영 데이터베이스의 LVM 스냅샷을 떠서 스테이징 환경에 실었습니다. 스테이징 데이터베이스를 최신으로 맞춰 부하 시험을 더 정확히 하려는 것이었습니다. 이 절차는 보통 24시간마다 01:00 에 자동으로 돕니다. 이번에는 더 최신인 사본이 필요했습니다.

18:00. 스패머가 스니펫을 만들어 데이터베이스를 두드리는 것을 감지했습니다. 데이터베이스가 불안정해졌습니다. 무엇이 문제이고 어떻게 대응할지 알아내는 작업이 시작되었습니다.

19:00 무렵. 스팸으로 의심되는 것 때문에 데이터베이스 부하가 늘기 시작했습니다. 많은 사용자가 이슈와 병합 요청에 댓글을 달지 못했습니다. 부하를 잡는 데 몇 시간이 걸렸습니다.

21:00. 상황이 커졌습니다. 데이터베이스의 쓰기가 잠겼고 일부 다운타임이 생겼습니다.

22:00. 복제 지연이 너무 벌어져 사실상 멈춘 상태가 되어 호출이 울렸습니다. 보조 데이터베이스가 제때 처리하지 못한 쓰기 급증 때문이었습니다. 이 시점에 보조는 약 4 GB 뒤처져 있었습니다.

23:00 무렵. 늘어난 부하로 PostgreSQL 보조의 복제 프로세스가 뒤처지기 시작했습니다. 보조가 필요로 하는 WAL 세그먼트가 주 서버에서 이미 지워져 복제가 실패했습니다. GitLab.com 은 WAL 아카이빙을 쓰지 않고 있었으므로 보조를 손으로 다시 맞춰야 했습니다. 그 작업은 보조의 기존 데이터 디렉토리를 지우고 pg_basebackup 으로 주 서버의 데이터베이스를 복사해 오는 것입니다.

한 엔지니어가 보조로 가서 데이터 디렉토리를 지우고 pg_basebackup 을 돌렸습니다. pg_basebackup 은 --verbose 를 붙였는데도 아무 출력 없이 매달렸습니다. 몇 번 시도한 뒤에야 주 서버의 복제 연결이 모자라 접속할 수 없다는 메시지가 나왔습니다. 그 수는 max_wal_senders 가 정합니다.

엔지니어들은 max_wal_senders 를 기본값 3 에서 32 로 잠시 올리기로 했습니다. 설정을 적용하자 PostgreSQL 이 세마포어가 너무 많이 만들어진다며 재시작을 거부했습니다. max_connections 가 너무 높게 잡혀 있을 때 이런 일이 생길 수 있습니다. 당시 값은 8000 이었습니다. 이 값은 거의 1년 전에 적용되어 그때까지 문제 없이 돌던 것입니다. 값을 2000 으로 낮추자 PostgreSQL 이 재시작되었습니다.

23:00 무렵. 한 엔지니어가 앞선 pg_basebackup 시도들이 보조의 PostgreSQL 데이터 디렉토리에 파일을 남겨 놓았을 것이라고 생각했습니다. 디렉토리가 비어 있는데도 존재한다는 것 때문에 pg_basebackup 이 거부하는 것으로 본 것입니다. 복제를 되살리려고 PostgreSQL 데이터베이스 디렉토리를 지웠습니다. 보조에서 지운다고 생각했지만 실행된 곳은 주 서버였습니다. 포스트모템은 이 대목을 23:30 무렵으로 적습니다.

23:27. 실수를 알아차리고 1~2초 뒤에 삭제를 중단했습니다. 그 시점에 이미 약 300 GB 가 지워진 뒤였습니다. 약 300 GB 중 약 4.5 GB 만 남았습니다. 엔지니어들은 데이터베이스 백업을 찾아 나섰고 Slack 에 도움을 청했습니다. 백업을 찾는 일과 쓰는 일이 모두 실패했습니다.

2월 1일

00:36. db1.staging.gitlab.com 의 데이터를 백업했습니다.

00:55. db1.staging.gitlab.com 을 db1.cluster.gitlab.com 에 마운트했습니다. 스테이징의 /var/opt/gitlab/postgresql/data/ 에서 운영의 같은 자리로 데이터를 복사하기 시작했습니다.

01:05. nfs-share01 서버를 임시 저장 자리로 징발해 /var/opt/gitlab/db-meltdown 에 두었습니다.

01:18. pg_xlog 를 포함해 남아 있던 운영 데이터를 20170131-db-meltodwn-backup.tar.gz 로 묶었습니다.

스테이징에서 운영 호스트로 데이터를 복사하는 데 약 18시간이 걸렸습니다. 이 디스크들은 네트워크 디스크이고 약 60Mbps 로 제한되어 있습니다. 값싼 저장소에서 프리미엄으로 옮길 방법이 없어 그 성능이 낼 수 있는 전부였습니다. 네트워크나 프로세서 병목은 없었고 병목은 드라이브에 있었습니다.

17:00. 웹훅을 뺀 상태로 GitLab.com 데이터베이스를 복구했습니다. 복구된 상태는 1월 31일 17:20 시점입니다. 웹훅은 LVM 스냅샷으로 별도의 스테이징 데이터베이스를 만들되 웹훅 제거가 일어나지 않게 해서 되살렸습니다. 그 테이블의 SQL 덤프를 떠서 복구된 데이터베이스에 넣었습니다.

18:00 무렵. 웹훅 복원을 비롯한 마지막 복구 절차를 마치고 모든 것이 예상대로 도는지 확인했습니다.

18:14. GitLab.com 이 다시 온라인이라고 공지했습니다.

뿌리 원인

GitLab 은 5 Whys 라는 기법으로 원인을 따라갑니다. 사고를 두 문제로 나눕니다. GitLab.com 이 멈춘 것과 GitLab.com 을 되살리는 데 오래 걸린 것입니다.

flowchart TD
    A[스팸과 계정 삭제 작업으로 부하 급증] --> B[주 서버가 WAL 세그먼트를 먼저 지워 복제 중단]
    B --> C[보조 서버를 손으로 다시 만들어야 함]
    C --> D[데이터 디렉토리를 비우는 작업이 주 서버에서 실행됨]
    D --> E[보조 서버로 페일오버 불가]
    D --> F[정기 백업과 디스크 스냅샷이 비어 있음]
    E --> G[6시간 전 LVM 스냅샷으로 복구]
    F --> G

멈추기까지 뚫린 겹

GitLab.com 이 멈춘 이유는 보조의 데이터베이스 디렉토리 대신 주 데이터베이스의 디렉토리가 실수로 제거되었기 때문입니다. 디렉토리를 지우는 작업 자체는 필요한 것이었습니다. 데이터베이스 복제가 멈춰 보조를 초기화하고 다시 만들어야 했고, 그러려면 PostgreSQL 데이터 디렉토리가 비어 있어야 했습니다.

여기서 첫 겹이 뚫립니다. 이 복구 작업은 자동화되어 있지 않았고 문서화도 제대로 되어 있지 않아 손으로 해야 했습니다. 사고 당시 공개된 문서는 복제 절차가 매우 취약하고 오류가 나기 쉬우며, 임의의 셸 스크립트 몇 개에 기대고 있고 문서화가 부실하다고 적었습니다.

두 번째 겹은 어느 호스트에 서 있는지가 눈에 안 들어왔다는 것입니다. 주 서버의 호스트명은 db1.cluster.gitlab.com, 보조는 db2.cluster.gitlab.com 입니다. GitLab 이 내놓은 재발 방지 항목 가운데 하나는 모든 호스트의 PS1 을 고쳐 호스트와 환경을 더 뚜렷이 구별하는 것입니다.

GitLab 은 초점을 어디에 둘지도 적습니다. 운영 엔지니어가 특정 명령을 못 쓰게 막는 쪽이 아니라, 재해 복구를 개선하고 지금 어느 호스트를 쓰고 있는지를 더 분명하게 만드는 쪽입니다. 예를 들어 rm 에 더 안전한 것을 별칭으로 걸 수는 있지만, 그렇게 해도 rm -rf /important-data 를 실수로 돌리는 것을 자기 자신에게서만 막을 뿐입니다. 디스크 손상이나 데이터를 잃는 다른 많은 방식은 막지 못합니다.

부하가 왜 늘었는지도 5 Whys 가 따라갑니다. 두 가지가 같은 시간에 겹쳤습니다. 스팸이 늘어난 것과, 한 GitLab 직원과 그에 딸린 데이터를 제거하려는 프로세스가 돌고 있던 것입니다.

되살리기까지 뚫린 겹

복구가 18시간 넘게 걸린 이유는 스테이징 데이터베이스의 사본으로 되살려야 했기 때문입니다. 그 사본은 다른 리전의 Azure VM 위에 있었고 속도가 나오지 않았습니다.

왜 스테이징 데이터베이스가 필요했나. 데이터베이스 서버에 Azure 디스크 스냅샷이 켜져 있지 않았고, pg_dump 를 쓰는 정기 데이터베이스 백업이 동작하지 않고 있었기 때문입니다.

왜 보조 데이터베이스 호스트로 페일오버할 수 없었나. 보조의 데이터가 복제를 되살리는 과정에서 지워졌기 때문입니다. 그래서 재해 복구에 쓸 수 없었습니다.

왜 표준 백업 절차를 쓸 수 없었나. 표준 절차는 pg_dump 로 논리 백업을 뜹니다. 이 절차는 PostgreSQL 9.2 를 쓰고 있었고 GitLab.com 은 PostgreSQL 9.6 위에서 돌고 있었습니다. 그래서 조용히 실패했습니다.

왜 조용히 실패했나. 실패하면 알림이 나갔지만 그 메일이 거부되어 실패 표시가 남지 않았습니다. 보낸 쪽은 다른 오류 보고 수단이 없는 자동 프로세스였습니다. 메일이 거부된 이유는 DMARC 로 서명되지 않았기 때문입니다.

왜 Azure 디스크 스냅샷을 켜 두지 않았나. 다른 백업 절차로 충분하다고 가정했기 때문입니다. 게다가 이 스냅샷의 복원에는 며칠이 걸릴 수 있습니다.

왜 백업 절차를 정기적으로 시험하지 않았나. 소유가 없었기 때문입니다. 그 결과 그 절차를 시험할 책임을 지는 사람이 아무도 없었습니다.

한 번의 삭제 명령은 이렇게 복제, 호스트 구별, 백업 검증, 실패 알림, 소유의 겹을 차례로 지나 마지막 복구 수단까지 닿았습니다. 그 끝에 남아 있던 것은 사고 약 6시간 전에 손으로 뜬 스테이징용 스냅샷 하나였습니다.

배경

GitLab.com 은 당시 주 하나와 보조 하나를 핫 스탠바이 모드로 두고 있었습니다. 보조는 페일오버 목적으로만 씁니다. 이 구성에서는 데이터베이스 하나가 모든 부하를 받아내야 합니다. GitLab 은 이것이 이상적이지 않다고 적습니다. 주의 호스트명은 db1.cluster.gitlab.com, 보조는 db2.cluster.gitlab.com 입니다.

사고 직전에 들어온 부하는 두 갈래였습니다. 하나는 스팸으로 의심되는 것입니다. 사고 앞의 한 주 동안에도 GitLab.com 은 비슷한 문제를 겪고 있었지만 이 정도로 심하지는 않았습니다. 나머지 하나는 나중에 밝혀졌습니다. 부하의 일부는 한 GitLab 직원과 그에 딸린 데이터를 제거하려던 백그라운드 작업이 만든 것이었습니다. 그 계정이 어뷰즈로 표시되어 실수로 제거 예약이 잡힌 결과입니다.

이 부하가 복제를 밀어냈습니다. 보조가 아직 복제하지 못한 WAL 세그먼트를 주가 먼저 지워 복제가 실패했습니다. WAL 아카이빙을 쓰고 있지 않았으므로 되돌릴 방법은 보조를 손으로 다시 맞추는 것뿐이었습니다. 그 작업이 사고가 일어난 자리입니다.

보장과 가정

포스트모템은 이런 일이 났을 때 보통은 최근 백업으로 비교적 짧은 시간 안에 데이터베이스를 복구할 수 있어야 한다고 적습니다. 다만 어떤 형태의 데이터 유실은 언제나 막을 수 있는 것은 아니라고 덧붙입니다. GitLab.com 에 갖춰져 있던 절차는 네 가지입니다. 사고 당일 문서는 배치된 다섯 가지 백업·복제 기법 가운데 어느 것도 믿을 만하게 돌거나 애초에 설정되어 있지 않았다고 적습니다.

pg_dump 백업과 S3

보장은 24시간마다 pg_dump 로 백업을 만들어 Amazon S3 에 올린다는 것입니다. 오래된 백업은 일정 시간이 지나면 자동으로 지워집니다.

가정은 백업 절차가 데이터베이스와 같은 메이저 버전의 pg_dump 를 쓴다는 것입니다. 이 가정이 깨져 있었습니다. 백업 절차는 pg_dump 9.2 를 쓰고 있었고 데이터베이스는 PostgreSQL 9.6 이었습니다. PostgreSQL 에서 9.x 판올림은 메이저로 칩니다. 메이저 버전이 다르면 pg_dump 가 오류를 내고 백업 절차가 거기서 끝납니다. 백업을 찾으러 갔을 때 S3 버킷은 비어 있었고 어디에도 최근 백업이 없었습니다.

사고 당일 문서는 그 아래를 한 겹 더 적습니다. omnibus 는 data/PG_VERSION 이 9.6 으로 되어 있을 때만 9.6 을 쓰는데 워커에는 그 파일이 없어 9.2 로 떨어졌습니다. Fog gem 이 오래된 백업들을 정리해 버렸을 수도 있다고 적습니다.

두 번째 가정은 절차가 실패하면 사람이 알게 된다는 것입니다. 오류가 나는 cron 작업에는 알림이 켜져 있었고 그 알림은 메일로 나갔습니다. GitLab.com 은 DMARC 를 씁니다. cron 메일에는 DMARC 가 적용되지 않아 받는 쪽에서 거부되었습니다. 그래서 백업이 실패하고 있다는 것을 너무 늦을 때까지 알지 못했습니다.

Azure 디스크 스냅샷

보장은 여러 서버의 디스크 스냅샷을 24시간마다 뜬다는 것입니다. Git 데이터를 담는 NFS 서버가 그런 서버입니다.

가정은 데이터베이스 서버도 그 대상에 들어 있다는 것입니다. 스냅샷은 NFS 서버에는 켜져 있었지만 데이터베이스 서버 어디에도 켜져 있지 않았습니다. 다른 백업 절차로 충분하다고 가정했기 때문입니다.

LVM 스냅샷

보장은 24시간마다 운영 데이터베이스 데이터를 담은 디스크의 LVM 스냅샷을 떠서 스테이징 환경에 싣는다는 것입니다. 이것으로 운영에 영향을 주지 않고 변경을 시험할 수 있습니다. 스테이징 데이터베이스로의 직접 접근은 운영 데이터베이스와 비슷하게 제한됩니다.

가정은 이 스냅샷이 재해 복구에 쓸 물건이라는 것입니다. 이 가정은 애초에 성립하지 않습니다. LVM 스냅샷은 주로 운영에서 스테이징으로 데이터를 옮기는 데 씁니다. 이 절차는 의도대로 돌고 있었지만 만들어진 스냅샷은 재해 복구에 쓰라고 만든 것이 아닙니다.

사고 시점에 쓸 수 있는 스냅샷은 둘이었습니다. 24시간마다 도는 스테이징용 스냅샷이 사고 거의 24시간 전 것이었고, 엔지니어가 손으로 만든 것이 사고 약 6시간 전 것이었습니다. 손으로 뜬 쪽은 그가 데이터베이스 부하 분산 작업을 하고 있어서 마침 돌린 것입니다. GitLab 은 데이터 유실을 최대한 줄일 유일한 선택지로 6시간 전 스냅샷을 골랐습니다. 다른 쪽을 고르면 거의 24시간치를 잃습니다.

여기에 딸린 가정이 하나 더 있습니다. 동기화 절차는 스테이징으로 데이터를 옮긴 뒤 웹훅을 제거합니다. 지난 24시간의 정기 백업에서 웹훅을 꺼내 오지 못하면 잃게 됩니다.

복제본

보장은 PostgreSQL 호스트 사이의 복제입니다. 이것은 주로 페일오버 목적이고 재해 복구용이 아니라고 포스트모템이 못 박습니다.

가정은 두 호스트 가운데 하나에는 데이터가 남아 있다는 것입니다. 이 시점에 복제 프로세스는 이미 깨져 있었고 데이터는 주와 보조 양쪽에서 지워진 뒤였습니다. 어느 호스트에서도 복구할 수 없었습니다.

관련 항목

사고가 딛고 선 데이터베이스 기술

데이터베이스 · PostgreSQL · SQL · 테이블 · WAL · 세그먼트 · pg_xlog · PG_VERSION · 복제 · 지연 · 핫 스탠바이 · LVM · 스냅샷 · 세마포어 · 덤프 · 백업 · 부하 분산

복구 중 조정한 PostgreSQL 설정값

max_wal_senders · max_connections

복구 중에 실행한 명령

pg_dump · pg_basebackup · rm

복구·백업 절차에 관여한 도구

omnibus · Fog gem · cron · 셸

데이터가 오간 서비스와 저장소

GitLab.com · Amazon S3 · Azure · NFS · Slack

사고 중 사라지거나 멈춘 GitLab 기능

이슈 · 병합 · 병합 요청(Merge Request, MR) · 스니펫 · 웹훅 · 프로젝트 · 댓글 · 사용자 계정

이 사고를 비껴간 GitLab 상품

GitLab Enterprise · GitHost · GitLab CE

사고를 되짚은 분석 기법

포스트모템 · 5 Whys

복구를 가로막은 표준과 설정

DMARC · PS1

복구 대응 중 오간 신호

호출 · 메시지 · 알림

복구 속도를 늦춘 하드웨어 조건

가상 머신 · 디스크 · 네트워크 · 성능

다른 이름: GitLab.com database incident · GitLab 데이터베이스 장애 · GitLab rm -rf 사고