{"id":"IL-0218","org":"GitLab","date":"2017-01-31","docs":[],"title":{"en":"GitLab.com's primary PostgreSQL data was wiped by mistake and, with no working backup, the site was down about 18 hours","ko":"복제를 다시 맞추다 PostgreSQL 프라이머리의 데이터 디렉터리가 지워지고 백업이 작동하지 않아 GitLab.com 이 약 18시간 멈추고 데이터를 잃었다"},"domain":["database","azure"],"impact":{"scope":"GitLab.com unavailable for about 18 hours; database changes made after 17:20 UTC on 31 January, such as projects, comments, user accounts, issues and snippets, were lost (GitLab gives the end of the window as 00:00 UTC in its summary and 23:30 UTC in its data loss section); Git repositories and wikis were unavailable but not lost","users":"GitLab.com users; roughly 5,000 projects, 5,000 comments and 700 new user accounts lost; GitLab Enterprise, GitHost and self-managed GitLab CE users not affected","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"GitLab split the incident into two problems: GitLab.com went down because the primary's data directory was removed by mistake during a manual, undocumented replication rebuild, and recovery took over 18 hours because the backups had failed silently, disk snapshots were not enabled and the only copy sat on slow staging disks. The backup procedure was never tested because no one owned it. GitLab chose to focus on fast, regularly practised recovery and on making it obvious which host an engineer is on, rather than on blocking commands, since a safer rm only protects against one of many ways to lose data.","ko":"GitLab 은 장애를 GitLab.com 이 약 18시간 멈춘 문제와 복원이 18시간 넘게 걸린 문제로 나눠 5 Whys 로 분석했다. 세컨더리를 다시 만드는 작업은 자동화되지도 제대로 문서화되지도 않아 손으로 해야 했다. 표준 백업은 PostgreSQL 버전이 맞지 않아 조용히 실패했고, 실패 알림 메일은 DMARC 서명이 없어 거부됐다. 다른 백업 절차를 믿고 Azure 디스크 스냅샷을 켜지 않았다. 백업 절차를 맡은 사람이 없어 아무도 정기적으로 시험하지 않았다.\n\nGitLab 은 운영 엔지니어가 특정 명령을 못 쓰게 막는 것보다 재해 복구를 고치고 지금 어느 호스트를 쓰는지 더 분명히 보이게 하는 데 집중하기로 했다. rm 을 더 안전한 별칭으로 바꾸면 실수로 지우는 것은 막지만 디스크 손상처럼 데이터를 잃는 다른 경우는 막지 못한다. 이상적인 환경은 실수해도 영향이 거의 없이 빠르게 복구할 수 있는 곳이고, 그러려면 복구 절차를 정기적으로 해 보고 변경을 쉽게 시험하고 되돌릴 수 있어야 한다."},"related":[],"sources":[{"url":"https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/","kind":"blog","archived":"https://web.archive.org/web/20261103213046/https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/"}],"summary":{"en":"GitLab.com ran one PostgreSQL primary, db1.cluster.gitlab.com, and one hot-standby secondary, db2.cluster.gitlab.com, used only for failover, so a single database carried all the load; GitLab had already had several other problems from this single point of failure. On 31 January an engineer was setting up PostgreSQL servers in staging to try pgpool-II, and around 17:20 UTC took an LVM snapshot of the production database for staging. From about 19:00 UTC database load rose from what was suspected to be spam, together with a background job removing a GitLab employee whose account a troll had reported for abuse and which had been scheduled for removal by mistake. Around 23:00 UTC the secondary's replication failed because GitLab.com did not use WAL archiving and the primary had removed the WAL segments the secondary needed. Re-synchronising it by hand, which was neither automated nor properly documented, led to the hung pg_basebackup runs and the max_wal_senders and max_connections changes, and around 23:30 UTC to the accidental wipe of about 300 GB on the primary.\n\nThe recovery procedures then failed one after another. The pg_dump backups uploaded to S3 every 24 hours had been failing silently: the job ran on an application server without a PostgreSQL data directory, so the Omnibus package defaulted to pg_dump 9.2, which errors against a 9.6 database, and the failure emails were rejected for lack of DMARC. Azure disk snapshots, which can take days to restore across storage accounts, had not been enabled for the database servers on the assumption that the other backups were enough. Replication was meant for failover, not disaster recovery, and the secondary's data had already been wiped. Nobody owned the backup procedure, so nobody tested it.\n\nGitLab restored from the LVM snapshot taken about 6 hours before the outage, losing less than the 24-hour snapshot would have. Copying the PostgreSQL data directory from the staging host, on Azure classic storage without Premium Storage throttled to around 60Mbps, took around 18 hours. On 1 February at 17:00 UTC the database was restored without webhooks, which the staging copy procedure removes; webhooks were restored from a separate database built from the snapshot, and the final steps were finished around 18:00 UTC. Database sequences were incremented by 100,000 so earlier IDs could not be reused, GitLab.com was re-enabled gradually, and projects created after 17:20 UTC were rebuilt in basic form from their Git repositories on the NFS cluster. GitLab kept public notes in a Google document and streamed the recovery on YouTube.\n\nDatabase changes such as projects, comments, user accounts, issues and snippets made after 17:20 UTC on 31 January were lost; GitLab gives the end of that window as 00:00 UTC at the top of the post and as 23:30 UTC in its data loss section, and estimated roughly 5,000 projects, 5,000 comments and 700 new user accounts were affected. Git repositories and wikis were stored separately and not lost.","ko":"GitLab.com 은 프라이머리 db1.cluster.gitlab.com 과 핫 스탠바이 세컨더리 db2.cluster.gitlab.com 으로 PostgreSQL 을 운영했고, 세컨더리는 페일오버에만 썼다. 2017년 1월 31일 한 엔지니어가 pgpool-II 를 시험하려고 17:20 UTC 무렵 운영 데이터베이스의 LVM 스냅샷을 떠 스테이징에 올렸다. 19:00 UTC 무렵 스팸으로 의심되는 부하가 올랐고, 어뷰징 신고 때문에 실수로 삭제 예약된 GitLab 직원 계정과 데이터를 지우는 백그라운드 작업도 부하를 보탰다. 23:00 UTC 무렵 세컨더리의 복제가 뒤처지다 멈췄는데, 필요한 WAL 세그먼트를 프라이머리가 이미 지웠고 WAL 아카이빙이 없어 세컨더리를 손으로 다시 맞춰야 했다.\n\n엔지니어는 세컨더리 데이터 디렉터리를 지우고 pg_basebackup 을 돌렸지만 출력 없이 멈췄다. max_wal_senders 를 3 에서 32 로 올렸다가 세마포어 문제로 PostgreSQL 이 재시작을 거부하자 max_connections 를 8000 에서 2000 으로 낮췄다. 그래도 pg_basebackup 이 바로 시작하지 않자 23:30 UTC 무렵 엔지니어는 데이터 디렉터리를 다시 지웠는데, 그곳은 세컨더리가 아니라 프라이머리였다. 1~2초 만에 멈췄지만 약 300 GB 가 지워졌다. 백업을 찾아보니 pg_dump 백업은 9.6 데이터베이스를 pg_dump 9.2 로 뜨다 오류로 끝나 S3 버킷이 비어 있었다. 백업 작업이 데이터 디렉터리가 없는 애플리케이션 서버에서 돌았기 때문에 Omnibus 가 9.2 를 골랐다. 실패 알림 메일은 DMARC 서명이 없어 받는 서버가 거부했다. 데이터베이스 서버에는 Azure 디스크 스냅샷도 켜져 있지 않았다.\n\nGitLab 은 데이터 손실을 줄이려고 6시간 전에 손으로 뜬 LVM 스냅샷을 쓰기로 했다. 스테이징의 PostgreSQL 데이터 디렉터리를 운영으로 복사하는 데 약 60Mbps 로 제한된 느린 디스크 때문에 약 18시간이 걸렸다. 스테이징 복사본에서는 웹훅을 지워 두었으므로 웹훅은 따로 띄운 데이터베이스에서 SQL 덤프로 가져왔고, 이전 ID 를 다시 쓰지 않게 시퀀스를 100,000 씩 올렸다. 2월 1일 17:00 UTC 에 웹훅 없이 데이터베이스를 복원했고, 18:00 UTC 무렵 웹훅 복원과 확인을 마쳤다. 1월 31일 17:20 UTC 이후의 데이터베이스 변경은 잃었다. GitLab 은 이 구간의 끝을 글 첫머리에서는 00:00 UTC 로, 데이터 손실 절에서는 23:30 UTC 로 적었고, 프로젝트 약 5,000개, 댓글 5,000개, 새 사용자 계정 약 700개가 영향을 받은 것으로 추정했다. Git 저장소와 위키는 따로 저장돼 지워지지 않았다. GitLab 은 복구 과정을 공개 Google 문서와 YouTube 생중계, Twitter 로 알렸다."},"trigger":{"en":"While trying to restore replication to the secondary, an engineer ran the removal of the PostgreSQL data directory on the primary, db1.cluster.gitlab.com, believing it was the secondary.","ko":"1월 31일 23:00 UTC 무렵 데이터베이스 부하로 세컨더리의 복제가 멈췄고, 세컨더리를 다시 맞추던 엔지니어가 세컨더리가 아니라 프라이머리에서 PostgreSQL 데이터 디렉터리를 지웠다."},"severity":"unknown","symptoms":["database-overloaded","replication-lag","data-loss","service-unavailable"],"timeline":{"detected_after":null,"resolved_after":"18h"},"date_kind":"incident","detection":{"en":"The engineer who ran the deletion on the primary noticed the mistake and stopped it a second or two later.","ko":"프라이머리에서 데이터 디렉터리를 지운 엔지니어가 스스로 실수를 알아채고 1~2초 만에 삭제를 멈췄다."},"components":["database","database-replication","database-backup","block-storage"],"root_cause":"human-operation","remediation":[{"text":{"en":"Raised max_wal_senders from 3 to 32 and lowered max_connections from 8000 to 2000 so PostgreSQL would restart","ko":"pg_basebackup 이 복제 연결이 모자라다고 알리자 max_wal_senders 를 기본값 3 에서 32 로 잠시 올렸고, PostgreSQL 이 재시작을 거부하자 max_connections 를 8000 에서 2000 으로 낮췄다."}},{"text":{"en":"Stopped the deletion on the primary a second or two after noticing it","ko":"엔지니어가 프라이머리에서 데이터를 지우고 있다는 것을 알아채고 1~2초 만에 삭제 작업을 멈췄지만, 약 300 GB 가 이미 지워졌다."}},{"text":{"en":"Restored the database from the LVM snapshot taken about 6 hours before the outage by copying the PostgreSQL data directory from staging","ko":"데이터 손실을 가장 줄일 수 있는 유일한 방법으로 장애 6시간 전에 만든 LVM 스냅샷을 쓰기로 하고, 스테이징의 PostgreSQL 데이터 디렉터리를 약 18시간 동안 운영으로 복사해 복원했다."}},{"text":{"en":"Restored the webhooks from a separate database built from the snapshot and imported them as a SQL dump","ko":"웹훅 삭제를 거치지 않은 별도 스테이징 데이터베이스를 LVM 스냅샷으로 띄워 웹훅 테이블의 SQL 덤프를 만들고, 복원한 GitLab.com 데이터베이스에 가져왔다."}},{"text":{"en":"Incremented all database sequences by 100,000 so IDs used before the outage could not be reused","ko":"장애 전에 쓰였을 수 있는 ID 를 다시 쓰지 않도록 모든 데이터베이스 시퀀스를 100,000 씩 올렸다."}},{"text":{"en":"Re-enabled GitLab.com gradually","ko":"GitLab.com 을 차례로 다시 열었다."}},{"text":{"en":"Rebuilt projects created after 17:20 UTC in basic form from their Git repositories on the NFS cluster","ko":"17:20 UTC 이후에 만든 프로젝트는 NFS 클러스터에 남은 Git 저장소로 가장 기본적인 형태만 복원했고, 이슈와 머지 리퀘스트 같은 데이터는 되살리지 못했다."}}],"action_items":[{"text":{"en":"Change the shell prompt (PS1) on all hosts to show more clearly which host and environment is in use","ko":"모든 호스트의 셸 프롬프트인 PS1 을 고쳐 호스트와 환경을 더 분명히 구분한다."}},{"text":{"en":"Monitor backups with Prometheus","ko":"백업을 Prometheus 로 모니터링한다."}},{"text":{"en":"Set PostgreSQL's max_connections to a sane value","ko":"PostgreSQL 의 max_connections 를 알맞은 값으로 둔다."}},{"text":{"en":"Investigate point-in-time recovery and continuous archiving for PostgreSQL","ko":"PostgreSQL 의 시점 복구와 연속 아카이빙을 검토한다."}},{"text":{"en":"Take LVM snapshots of the production databases every hour","ko":"운영 데이터베이스의 LVM 스냅샷을 매시간 뜬다."}},{"text":{"en":"Take Azure disk snapshots of the production databases","ko":"운영 데이터베이스에 Azure 디스크 스냅샷을 켠다."}},{"text":{"en":"Move staging to the ARM environment","ko":"스테이징을 ARM 환경으로 옮긴다."}},{"text":{"en":"Recover the production replicas","ko":"운영 복제본을 되살린다."}},{"text":{"en":"Test recovery of PostgreSQL database backups automatically","ko":"PostgreSQL 백업 복원을 자동으로 시험한다."}},{"text":{"en":"Improve the PostgreSQL replication documentation and runbooks","ko":"PostgreSQL 복제 문서와 런북을 고친다."}},{"text":{"en":"Investigate pgbarman for PostgreSQL backups","ko":"PostgreSQL 백업에 pgbarman 을 쓸지 검토한다."}},{"text":{"en":"Investigate WAL-E for database backup and realtime replication","ko":"데이터베이스 백업과 실시간 복제에 WAL-E 를 쓸지 검토한다."}},{"text":{"en":"Build a streaming database restore","ko":"스트리밍 방식의 데이터베이스 복원을 만든다."}},{"text":{"en":"Assign an owner for data durability","ko":"데이터 내구성을 책임질 담당자를 정한다."}},{"text":{"en":"Set up multiple secondaries and balance load among them with pgpool-II","ko":"세컨더리를 여러 대 두고 부하를 나누도록 pgpool-II 3.6.1 을 묶고 PostgreSQL 연결 풀링과 부하 분산을 구성한다."}},{"text":{"en":"Provide a tool for developers to run and revert Rails migrations on staging","ko":"개발자가 데이터베이스 마이그레이션을 시험할 수 있도록 스테이징에서 Rails 마이그레이션을 실행하고 되돌리는 도구를 만든다."}},{"text":{"en":"Build owned recovery procedures for the rest of the GitLab.com infrastructure, not only the database","ko":"데이터베이스뿐 아니라 GitLab.com 인프라 전체의 복구 절차를 만들고 그 절차의 담당자를 정한다."}},{"text":{"en":"Publish a backup monitoring dashboard and add more backup data to it","ko":"공개 백업 모니터링 대시보드를 만들었고, 지금은 pg_dump 백업 데이터만 있지만 데이터를 더 넣는다."}},{"text":{"en":"Improve the abuse reporting and response system so removal for spam does not hard delete","ko":"어뷰징 신고와 대응 시스템을 고쳐 스팸 신고로 사용자를 지울 때 데이터를 바로 영구 삭제하지 않게 한다."}},{"text":{"en":"Redact engineers' names in future public incident documents","ko":"앞으로 공개하는 장애 문서에서는 엔지니어의 이름을 가린다."}}],"contributing":["backup-unverified","single-point-of-failure","no-runbook","paging-failed","assumption-unverified","default-config-unchanged"],"recurrence_of":null,"symptoms_text":{"en":"From around 19:00 UTC on 31 January 2017, load on GitLab.com's PostgreSQL database rose, which GitLab suspected was spam, and many users could not post comments on issues and merge requests. Around 23:00 UTC the replication of the PostgreSQL secondary fell behind and then failed, because WAL segments it still needed had already been removed from the primary.\n\nRebuilding the secondary meant emptying its data directory and copying the database over with pg_basebackup. pg_basebackup hung without meaningful output even with --verbose, and after several tries reported that it could not connect because the master did not have enough replication connections, a limit set by max_wal_senders. When max_wal_senders was raised from its default of 3 to 32, PostgreSQL refused to restart, complaining that too many semaphores were being created, because max_connections had been set to 8000 almost a year earlier. With max_connections at 2000 PostgreSQL restarted, but pg_basebackup still did not start replicating, and strace only showed it hanging in a poll call. pg_basebackup normally waits silently until the primary starts sending data, but neither GitLab's runbooks nor the pg_basebackup documentation said so clearly.\n\nAround 23:30 UTC an engineer, thinking that earlier attempts had left files behind, wiped the PostgreSQL data directory again, but on the primary instead of the secondary. The engineer stopped the command a second or two later, after about 300 GB had already been removed, and GitLab.com went down with data now wiped from both hosts.\n\nNone of the backups could be used quickly. The S3 bucket for the daily pg_dump backups was empty, because the backup job ran pg_dump 9.2 against PostgreSQL 9.6 and failed, and the cron failure emails were rejected by the receiving mail server because they were not signed with DMARC. Azure disk snapshots were not enabled for the database servers. The only usable copies were LVM snapshots made for the staging environment, one almost 24 hours old and one taken by hand about 6 hours before the outage, and copying it from the staging disks, throttled to around 60Mbps, took about 18 hours.\n\nDatabase changes such as projects, comments, user accounts, issues and snippets made after 17:20 UTC on 31 January were lost. GitLab gives the end of that window as 00:00 UTC at the top of the post and as 23:30 UTC in its data loss section. It estimated roughly 5,000 projects, 5,000 comments and 700 new user accounts were affected. Git repositories and wikis were unavailable during the outage but not lost.\n\nThe public monitoring website dashboards.gitlab.com could not handle the load from users during the outage.","ko":"2017년 1월 31일 19:00 UTC 무렵 GitLab.com 의 데이터베이스 부하가 올랐고, 많은 사용자가 이슈와 머지 리퀘스트에 댓글을 달지 못했다. 스팸으로 의심되는 부하였고, 잡는 데 몇 시간이 걸렸다.\n\n23:00 UTC 무렵 부하 때문에 PostgreSQL 세컨더리의 복제가 뒤처지다가 멈췄다. 세컨더리에 필요한 WAL 세그먼트를 프라이머리가 이미 지운 뒤였고, WAL 아카이빙을 쓰지 않았으므로 세컨더리를 손으로 다시 동기화해야 했다.\n\n엔지니어가 세컨더리의 데이터 디렉터리를 지우고 pg_basebackup 을 돌렸지만, --verbose 옵션을 켰는데도 아무 의미 있는 출력 없이 멈춰 있었다. 몇 번 다시 시도하자 pg_basebackup 은 max_wal_senders 로 정하는 복제 연결이 master 에 모자라 접속할 수 없다고 알렸다.\n\nmax_wal_senders 를 3 에서 32 로 올리자 PostgreSQL 은 세마포어를 너무 많이 만든다며 재시작을 거부했다. max_connections 가 8000 이었고, 2000 으로 낮추자 재시작됐다. 그래도 pg_basebackup 은 바로 복제를 시작하지 않았고, strace 로 보니 poll 호출에서 멈춰 있었을 뿐 다른 단서는 없었다.\n\n23:30 UTC 무렵 엔지니어는 앞선 시도에서 pg_basebackup 이 세컨더리 데이터 디렉터리에 파일을 남겼다고 생각해 디렉터리를 다시 지웠는데, 실제로는 프라이머리에서 지웠다. 1~2초 만에 멈췄지만 약 300 GB 가 이미 지워졌다. pg_basebackup 은 프라이머리가 복제 데이터를 보낼 때까지 조용히 기다리는 것이 정상 동작이었지만, 이 동작은 런북에도 공식 문서에도 분명히 적혀 있지 않았다.\n\n복원하려고 백업을 찾자 pg_dump 백업을 올리는 S3 버킷이 비어 있었고 최근 백업은 어디에도 없었다. 데이터베이스 서버에는 Azure 디스크 스냅샷이 켜져 있지 않았고, 프라이머리와 세컨더리 모두 데이터가 지워져 어느 쪽에서도 복원할 수 없었다. pg_dump 백업은 백업 작업이 PostgreSQL 9.6 에 pg_dump 9.2 를 돌려 오류로 끝나고 있었다. cron 실패 알림 메일은 DMARC 서명이 없어 받는 메일 서버가 거부했다. 쓸 수 있는 사본은 스테이징용 LVM 스냅샷뿐이었다. 하나는 거의 24시간 전 것이었고 하나는 장애 약 6시간 전에 엔지니어가 손으로 뜬 것이었다. 약 60Mbps 로 제한된 스테이징 디스크에서 복사하는 데 약 18시간이 걸렸다.\n\nGitLab.com 은 1월 31일 17:20 UTC 이후의 프로젝트, 댓글, 사용자 계정, 이슈, 스니펫 같은 데이터베이스 변경을 잃었다. GitLab 은 이 구간의 끝을 글 첫머리에서는 00:00 UTC 로, 데이터 손실 절에서는 23:30 UTC 로 적었다. 프로젝트 약 5,000개, 댓글 5,000개, 새 사용자 계정 약 700개가 영향을 받은 것으로 추정했다. Git 저장소와 위키는 장애 중 쓸 수 없었지만 지워지지 않았다.\n\n공개 모니터링 사이트 dashboards.gitlab.com 은 장애 중 몰린 사용자 부하를 견디지 못했다."},"updated_at":"2026-11-14T05:53:42.977167Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0218","org_slug":"gitlab"}