{"id":"IL-0219","org":"GoCardless","date":"2017-10-10","docs":[],"title":{"en":"GoCardless's API and Dashboard failed every request for 1 hour 50 minutes when Pacemaker did not promote a replica","ko":"Postgres 프라이머리의 디스크 어레이가 망가진 뒤 Pacemaker 가 새 프라이머리를 승격하지 못해 GoCardless API 와 대시보드가 1시간 50분 멈췄다"},"domain":["database"],"impact":{"scope":"GoCardless API and Dashboard: every request failed with an error for 1 hour and 50 minutes; no data corruption found","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A rule that two resources must not run together does not behave like the opposite of a rule that they must run together, and in some failure conditions the difference stopped Pacemaker from promoting a replica. A misconfiguration that shows only when two processes crash at almost the same time will not appear in basic tests or daily operations, only through fault injection. After two years in which the automation had handled every failure, the team's skill at controlling the cluster by hand had faded, and a runbook unused for years would likely have been out of date.","ko":"GoCardless 는 단순해 보이는 Pacemaker 설정이 매우 이상한 동작을 부를 수 있다는 것을 배웠다. 두 자원이 함께 돌면 안 된다는 규칙과 함께 돌아야 한다는 규칙은 반대말처럼 보이지만, 특정 장애 조건에서는 시스템이 완전히 다르게 움직였다.\n\n프로세스 두 개가 거의 동시에 죽을 때만 드러나는 설정 오류는 기본 시험이나 평소 운영으로는 찾을 수 없고, 장애 주입으로만 드러나는 버그가 있다. 자동화가 2년 동안 장애를 잘 처리하자 그 아래 인프라를 손으로 다루는 기술이 녹슬었다. 런북을 써 두어도 몇 년 뒤에 꺼내 보면 거의 확실히 낡아 있다."},"related":[],"sources":[{"url":"https://gocardless.com/blog/incident-review-api-and-dashboard-outage-on-10th-october/","kind":"blog","archived":"https://web.archive.org/web/20261007021208/https://gocardless.com/blog/incident-review-api-and-dashboard-outage-on-10th-october"}],"summary":{"en":"GoCardless keeps its most critical data, such as payment records and merchant and customer details, in a Postgres cluster of 3 nodes: a primary, a synchronous replica and an asynchronous replica. Pacemaker runs on each node, promotes the synchronous replica when the primary fails, and manages the virtual IP address (VIP) that the Ruby on Rails applications use to reach the primary. Normally such a failover would cost 1 or 2 minutes. When the primary's disk array failed on 10 October, Pacemaker did not promote the synchronous replica, and the team spent about an hour trying to make it do so. At 16:18 BST the team put the cluster into maintenance mode one last time, configured the synchronous replica as the primary and started Postgres by hand. Because Pacemaker in maintenance mode no longer moved the VIP, the backend applications were reconfigured to connect to the new primary's actual IP address; the manually promoted primary was working at 16:46 and monitoring confirmed the API and Dashboard were back at 16:59.\n\nAfterwards the team bootstrapped a third node as an asynchronous replica, added a second VIP that the infrastructure team would move by hand in a failover, and found no evidence of data corruption. Using a version of the cluster that runs in Docker containers, one subteam scripted failures until it reproduced the production behaviour. Three conditions were all needed: default-resource-stickiness set to a non-zero value so Pacemaker avoids moving running resources; a Backup VIP, added so backups always connect to a replica, with a colocation rule of -INF preference against the Postgres primary, and running at the time on the synchronous replica; and a Postgres subprocess crashing on the synchronous replica almost at the same time as the primary failed. The invalid WAL file turned out to be a red herring: the failing primary had archived it to the backup server as a final act, Postgres discarded it, and the replica already had those writes through synchronous streaming replication. Writing the rule the other way round, an INF preference between the Backup VIP and a replica, let the cluster promote the synchronous replica under the same failures.\n\nBecause so much manual intervention had gone into the old cluster, the team provisioned a new Pacemaker-managed cluster and moved to it by adapting its zero-downtime failover procedure, in which PgBouncer on each node, behind its own VIP, pauses incoming queries while the Postgres VIP moves to a new primary and then resumes them. After practice runs, the migration was done in an announced maintenance window without problems, the old cluster was decommissioned and the incident closed.","ko":"GoCardless 는 가장 중요한 데이터를 프라이머리, 동기 복제본, 비동기 복제본 세 노드의 Postgres 클러스터에 두고, 각 노드의 Pacemaker 가 장애 때 새 프라이머리를 승격하고 클라이언트가 접속하는 가상 IP 를 옮긴다. 평소라면 프라이머리가 망가져도 1~2분이면 동기 복제본이 승격된다. 2017년 10월 10일 15:09 BST 에 API 와 대시보드가 완전히 멈췄다. 프라이머리의 RAID 컨트롤러가 디스크 3개를 동시에 잃었지만 클러스터는 동기 복제본을 승격하지 않았다.\n\n대응 팀은 15:17 BST 에 망가진 프라이머리를 끄고, 15:18 BST 에 crm resource cleanup 으로 오류 횟수를 지웠지만 승격은 일어나지 않았다. 한 시간 동안 여러 방법을 해 봤고, 유지보수 모드에서 recovery.conf 의 복제본 설정을 지워도 Pacemaker 가 원래 설정으로 되돌렸다. 16:18 BST 에 마지막으로 유지보수 모드로 두고 동기 복제본을 프라이머리로 설정해 Postgres 를 직접 띄웠고, 가상 IP 대신 새 프라이머리의 실제 IP 로 백엔드 애플리케이션을 다시 설정했다. 16:46 BST 에 수동 승격한 프라이머리가 동작했고, 16:59 BST 에 API 와 대시보드가 돌아와 장애는 1시간 50분 이어졌다. 그 뒤 세 번째 노드를 비동기 복제본으로 띄우고, 손으로 관리할 가상 IP 를 하나 더 두고, 데이터 무결성을 점검해 손상이 없음을 확인했다.\n\n팀은 Docker 컨테이너로 띄운 클러스터에서 장애를 재현했다. 동기 복제본이 유효하지 않은 WAL 파일을 받은 것은 망가진 프라이머리가 마지막으로 백업 서버에 보낸 파일 때문이었고, 승격 실패와는 관계가 없었다. 승격을 막은 조건은 셋이었다. 첫째는 0이 아닌 default-resource-stickiness 설정이었다. 둘째는 Backup VIP 에 건 -INF colocation 규칙이었다. 이 규칙은 백업 프로세스를 복제본에서 돌리려고 Backup VIP 를 Postgres 프라이머리와 같은 서버에 두지 않게 했다. 셋째는 프라이머리와 동기 복제본의 프로세스가 거의 동시에 죽었다는 점이었다. 장애 때 Backup VIP 는 승격돼야 할 동기 복제본에 있었다. 규칙을 Backup VIP 와 복제본 사이의 INF 선호로 뒤집자 같은 조건에서도 승격이 됐다. 팀은 기존 클러스터에 손을 너무 많이 댔다고 보고 Pacemaker 가 관리하는 새 클러스터를 만들었다. PgBouncer 로 쿼리를 잠시 멈추는 무중단 페일오버 절차를 고쳐 써서 새 클러스터로 옮겼고, 옛 클러스터를 내렸다."},"trigger":{"en":"The RAID controller of the primary Postgres node lost 3 disks of its array at once, so all reads and writes on the primary failed.","ko":"10월 10일 오후 Postgres 프라이머리 노드의 RAID 컨트롤러가 디스크 3개를 동시에 잃어 디스크 어레이가 망가졌다. 거의 같은 때 동기 복제본에서 Postgres 하위 프로세스 하나가 죽었다."},"severity":"unknown","symptoms":["service-unavailable","failover-failed"],"timeline":{"detected_after":null,"resolved_after":"1h 50m"},"date_kind":"incident","detection":{"en":"GoCardless's monitoring detected the total outage of the API and Dashboard at 15:09 BST, and engineers began investigating.","ko":"15:09 BST 에 모니터링이 API 와 대시보드의 완전한 중단을 찾았고 엔지니어들이 조사를 시작했다."},"components":["database","failover-manager"],"root_cause":"hardware-failure","remediation":[{"text":{"en":"Powered off the broken primary Postgres node","ko":"15:17 BST 에 디스크 어레이가 망가진 채 켜져 있는 노드를 클러스터에 두지 않으려고 망가진 Postgres 프라이머리 노드의 전원을 껐다. 클러스터는 여전히 새 프라이머리를 승격하지 않았다."}},{"text":{"en":"Cleared the Pacemaker error counts with crm resource cleanup, which had no effect","ko":"15:18 BST 에 Pacemaker 가 Postgres 인스턴스 상태를 다시 파악하게 하려고 crm resource cleanup 으로 오류 횟수를 지웠지만 효과가 없었다."}},{"text":{"en":"Edited recovery.conf on the synchronous replica under maintenance mode several times, which Pacemaker reverted","ko":"Pacemaker 클러스터를 유지보수 모드로 두고 동기 복제본의 recovery.conf 에서 복제본 설정을 지운 뒤 유지보수 모드를 풀어 직접 승격하려 했지만, 매번 원래 recovery.conf 로 돌아갔다."}},{"text":{"en":"Put the cluster into maintenance mode, configured the synchronous replica as primary and started Postgres by hand","ko":"16:18 BST 에 클러스터를 마지막으로 유지보수 모드로 두고 동기 복제본을 프라이머리로 설정해 Postgres 를 직접 띄웠다."}},{"text":{"en":"Reconfigured the backend applications to connect to the new primary's actual IP address","ko":"클러스터가 유지보수 모드라 가상 IP 를 옮길 수 없었으므로 백엔드 애플리케이션이 새 프라이머리의 실제 IP 주소로 접속하게 다시 설정했다."}},{"text":{"en":"Bootstrapped a third node as an asynchronous replica","ko":"평소 수준의 이중화를 되찾으려고 세 번째 노드를 비동기 복제본으로 띄웠다."}},{"text":{"en":"Added a VIP managed by hand by the infrastructure team for manual failover","ko":"Pacemaker 가 유지보수 모드라 자동 페일오버가 없었으므로 인프라 팀이 손으로 관리할 가상 IP 를 하나 더 두어, 프라이머리가 망가지면 동기 복제본을 승격하고 가상 IP 를 직접 옮길 수 있게 했다."}},{"text":{"en":"Verified data integrity and found no corruption","ko":"디스크 어레이 장애였으므로 생각할 수 있는 모든 시험으로 데이터 무결성을 확인했고 데이터 손상의 증거는 없었다."}},{"text":{"en":"Migrated to a new Pacemaker-managed database cluster in a maintenance window and decommissioned the old one","ko":"Pacemaker 가 다시 관리하는 새 데이터베이스 클러스터를 만들고, 유지보수 시간을 공지한 뒤 문제없이 옮겼다. 옛 클러스터는 내렸다."}}],"action_items":[{"text":{"en":"Express the Backup VIP rule as an INF preference for a replica instead of a -INF preference against the primary","ko":"Backup VIP 의 colocation 규칙을 Postgres 프라이머리와의 -INF 선호 대신 복제본과의 INF 선호로 바꿔, 같은 장애 조건에서도 동기 복제본이 승격되게 했다."}},{"text":{"en":"Replace the VIPs with proxies on the application servers that route traffic by the state of the cluster","ko":"특정 Postgres 인스턴스로 트래픽을 보내는 가상 IP 를 없애고, 애플리케이션 서버에서 클러스터 상태에 따라 알맞은 노드로 트래픽을 보내는 프록시를 돌려 Pacemaker 가 관리하는 자원 수를 줄인다."}},{"text":{"en":"Run harsher fault injection tests of the Postgres cluster and consider automation that injects failures continually","ko":"Postgres 클러스터를 더 거칠게 시험하고, Chaos Monkey 처럼 계속 장애를 주입하는 자동화를 검토한다."}},{"text":{"en":"Consider adding arbitrary restrictions to game day exercises, such as automation that has failed and a problem that cannot be diagnosed, so engineers practise bringing the service back another way","ko":"장애 대응 훈련인 게임 데이에 임의의 제약을 넣는 방안을 검토한다. 예를 들어 자동화가 실패하고 원인도 찾을 수 없는 상황에서 다른 방법으로 서비스를 되살리게 한다."}}],"contributing":["overlapping-failures","assumption-unverified"],"recurrence_of":null,"symptoms_text":{"en":"At 15:09 BST on 10 October 2017, GoCardless's monitoring detected a total outage of its API and Dashboard: every request failed and returned an error. By 15:11 engineers saw evidence of a disk array failure on the primary Postgres node, but the Pacemaker cluster had not failed over to the synchronous replica as it should have.\n\nAt 15:17 the broken primary was powered off, in the belief that with only the two replicas online Pacemaker would promote one of them; it did not. At 15:18 clearing the error counts with crm resource cleanup, to make Pacemaker rediscover the state of the Postgres instances, had no effect. For the next hour engineers tried other ways to promote a primary, ending with putting the cluster into maintenance mode (crm configure property maintenance-mode=true), removing the flag in recovery.conf that makes Postgres a replica, and taking the cluster out of maintenance mode. Every time, Pacemaker brought the replica back with its original recovery.conf, leaving no primary.\n\nThe logs examined afterwards showed that the RAID controller had lost 3 disks of the array at the same moment, after which every read and write against it failed and the Linux kernel set the filesystem to read-only. Pacemaker had correctly seen that Postgres on the primary was unhealthy and kept trying to promote a new primary, but each time could not decide where it should run. On the synchronous replica one Postgres subprocess had crashed at about the same time, making Postgres restart, and afterwards the replica kept failing to restore a WAL file through its restore_command with a message that the file was invalid. That message later turned out to have nothing to do with the failed promotion.","ko":"2017년 10월 10일 15:09 BST 에 모니터링이 API 와 대시보드가 완전히 멈춘 것을 찾았다. 그동안 들어온 요청은 모두 실패하고 오류를 받았다.\n\n15:11 BST 에 프라이머리의 디스크 어레이가 망가진 증거가 보였다. RAID 컨트롤러는 어레이에서 디스크 3개를 동시에 잃었다고 기록했고, 그 뒤의 읽기와 쓰기는 모두 실패했다. Linux 커널은 그 컨트롤러 위의 파일시스템을 읽기 전용으로 바꿨지만 읽기조차 되지 않았다.\n\n클러스터는 동기 복제본으로 페일오버하지 않았다. Pacemaker 는 프라이머리의 Postgres 가 비정상이라는 것을 제대로 알아채고 새 프라이머리를 거듭 승격하려 했지만, 매번 프라이머리를 어디서 돌릴지 정하지 못했다.\n\n동기 복제본에서는 프라이머리의 디스크 어레이가 망가질 무렵 Postgres 하위 프로세스 하나가 죽어 Postgres 가 재시작했다. 재시작 뒤 동기 복제본은 restore_command 로 WAL 파일을 복원하려 했고, 시도마다 파일이 유효하지 않다는 메시지를 내며 실패했다. 이 메시지는 나중에 승격 실패와 관계가 없는 것으로 밝혀졌다.\n\n대응 팀은 15:17 BST 에 망가진 프라이머리 노드의 전원을 끄면 클러스터가 새 프라이머리를 승격할 것으로 봤지만 그렇지 않았다. 15:18 BST 에 crm resource cleanup 으로 Pacemaker 의 오류 횟수를 지워도 아무 효과가 없었다.\n\n대응 팀이 유지보수 모드에서 동기 복제본의 recovery.conf 에서 복제본 설정을 지우고 유지보수 모드를 풀 때마다, Pacemaker 는 원래 recovery.conf 로 복제본을 다시 띄웠다. 클러스터에는 프라이머리가 없는 채로 남았다."},"updated_at":"2026-11-14T05:53:43.149049Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0219","org_slug":"gocardless"}