{"id":"IL-0326","org":"MailPace","date":"2023-08-11","docs":["https://apidock.com/rails/ActiveRecord/Batches/BatchEnumerator/destroy_all"],"title":{"en":"A retention clean-up job filled the disk of MailPace's managed Postgres, which stayed offline for just over two hours","ko":"MailPace 의 관리형 Postgres 데이터베이스 디스크가 가득 차 데이터베이스가 두 시간 넘게 내려갔다"},"domain":["database"],"impact":{"scope":"All database transactions failed across MailPace's services and emails were not sent; during the migration users could log in but not send emails","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Several causes came together: a clean-up job that failed silently, a naive deletion that grew huge once data had piled up, a database plan without headroom for growth, and status pages that missed the outage, presumably because https://app.mailpace.com still answered 200 while the database was down. MailPace also advises that critical services such as email sending be set up with more than one provider.","ko":"장애는 여러 원인이 겹쳐 났다. 오래된 데이터를 지우는 cron 작업이 조용히 실패해 데이터가 쌓였다. 이 작업은 평소 하루치 양이었다면 문제없었겠지만 지나치게 단순하게 짜여 있었다. 데이터베이스 요금제는 처음부터 성장 여유가 부족했다. 상태 페이지는 장애를 잡지 못했다. MailPace 는 중요한 서비스마다 제공자를 여럿 두라고 권했다. 또 장애를 감지해 메일을 예비 제공자로 보내는 오픈 소스 아웃바운드 SMTP 프록시 Outgoer 를 만들고 있다고 밝혔다."},"related":[],"sources":[{"url":"https://mailpace.com/blog/musings/postgres-outage-post-mortem","kind":"blog","archived":"https://web.archive.org/web/20260918212844/https://mailpace.com/blog/musings/postgres-outage-post-mortem"}],"summary":{"en":"MailPace's clean-up cron job, which removes data older than each organisation's retention period, had been failing silently, so old data piled up. Under a normal daily load the task would have succeeded, but each destroy_all call removes all related records in more than 10 tables holding responses, attachments and other data, and accounts send thousands of emails a day, so a single call could be very large. Given 8gb of memory the Ruby code ran, and the database filled quickly; MailPace assumes the space went to Write Ahead Logs and was waiting for the provider to confirm. The database plan had also been sized without enough headroom for growth, and an upgrade was already planned.\n\nAt 13:37 MailPace contacted its managed database provider, and at 13:45 it began its tested restoration procedure, preparing an offsite backup from six hours earlier on a new database instance, which would have meant some data loss. At 14:30, just before the switch, the provider reported that it had been automatically notified at 99% storage and was moving the database to a larger instance; two of its support engineers working on the issue at the same time, one from the automated check and one from MailPace's ticket, slowed the migration. The database was read-only from 15:25 and fully back on the larger instance at 15:50. Afterwards MailPace replicated the problem on a separate instance restored from backup, removed the old data in production day by day with a revised query while watching memory and database size, and changed the job to delete in batches of 50 records four times a day.","ko":"MailPace 는 매일 도는 Rake cron 작업으로 보존 기간 30일이 지난 데이터를 지웠다. 이 작업이 조용히 실패하면서 데이터가 쌓이고 있었다. 팀은 2023-08-11 13:00 BST 정기 점검에서 데이터베이스가 있어야 할 크기의 약 1.5배인 것을 보고, 정리 작업이 killed 메시지로 끝나는 것을 찾았다. 13:15 에 cron 인스턴스 메모리를 1gb 에서 8gb 로 늘리자 작업은 돌았다. 그러나 destroy_all 은 10개가 넘는 테이블에 걸친 응답과 첨부 파일 같은 연결 레코드까지 지웠다. 계정 하나가 하루에 메일을 수천 통 보낼 수 있었으므로 destroy_all 호출 하나하나가 매우 컸다. 데이터베이스가 빠르게 찼고 13:36 에 모든 데이터베이스 트랜잭션이 실패했다. MailPace 는 Write Ahead Log 때문일 것으로 추정했고, 업스트림 제공자가 정확한 경위를 확인해 주기를 기다리고 있다고 적었다. 데이터베이스 요금제도 처음부터 성장 여유가 모자랐다.\n\nPostgres 가 관리형 서비스였으므로 팀은 13:37 에 업스트림 제공자 지원팀에 연락했다. 13:45 에 6시간 전 오프사이트 백업을 새 데이터베이스 인스턴스에 준비하는 복원 절차를 시작했다. 이 절차는 여러 번 시험해 본 것이었고 약 1시간이 걸렸다. 다만 데이터 일부를 잃고 그동안 장애가 이어지는 방법이었다. 14:30 에 새 데이터베이스로 옮기려던 참에, 제공자가 저장 공간이 99% 찼을 때 자동 알림을 받았고 더 큰 데이터베이스로 옮기는 중이라고 알려 왔다. 제공자 쪽 소통 착오로 지원 엔지니어 두 명이 동시에 작업해 이전이 늦어졌다. 15:25 에 이전 중인 데이터베이스가 읽기 전용이 돼 로그인은 됐지만 메일은 보낼 수 없었다. 15:50 에 더 큰 인스턴스로 옮겨 완전히 복구됐다. 상태 페이지 두 개는 장애를 잡지 못했다.\n\n그 뒤 MailPace 는 백업으로 만든 별도 데이터베이스 인스턴스에서 문제를 재현해 메모리 사용량을 보고 수정안을 시험했다. 운영에서는 고친 쿼리로 오래된 데이터를 하루 단위로 직접 지웠다."},"trigger":{"en":"The daily retention clean-up job, which had been failing with killed, was rerun with its memory raised from 1gb to 8gb, and its large destroy_all deletions filled the database disk.","ko":"13:15 BST 에 cron 인스턴스 메모리를 1gb 에서 8gb 로 늘려 밀려 있던 보존 기간 정리 작업을 돌렸다. destroy_all 이 연결된 레코드까지 한꺼번에 많이 지우면서 데이터베이스 디스크가 빠르게 찼다."},"severity":"unknown","symptoms":["node-disk-full","service-unavailable","writes-rejected"],"timeline":{"detected_after":null,"resolved_after":"2h 14m"},"date_kind":"incident","detection":{"en":"Notifications that all database transactions were failing across multiple services arrived as soon as the outage began; the provider was automatically notified at 99% storage. MailPace's two status pages did not detect the outage.","ko":"13:00 BST 정기 점검에서 데이터베이스가 평소보다 큰 것을 먼저 봤다. 13:36 에는 여러 서비스에서 모든 데이터베이스 트랜잭션이 실패한다는 알림이 곧바로 들어왔다. 상태 페이지 두 개는 장애를 잡지 못했다. 업스트림 제공자는 저장 공간이 99% 찼을 때 자동으로 알림을 받았다."},"components":["database"],"root_cause":"capacity","remediation":[{"text":{"en":"Contacted the managed database provider's support","ko":"Postgres 가 관리형 서비스였으므로 13:37 BST 에 곧바로 업스트림 제공자 지원팀에 연락했다."}},{"text":{"en":"Notified users on Twitter and Mastodon","ko":"13:40 에 Twitter 와 Mastodon 으로 사용자에게 장애를 알렸다. 상태 페이지가 자동으로 바뀔 것으로 생각했지만 바뀌지 않았다."}},{"text":{"en":"Prepared an offsite backup from six hours earlier on a new database instance as a fallback","ko":"제공자의 답을 기다리는 동안 13:45 에 전체 장애 복원 절차를 시작해 6시간 전 오프사이트 백업을 새 데이터베이스 인스턴스에 준비했다."}},{"text":{"en":"The provider migrated the database to a larger instance","ko":"15:50 에 제공자의 이전이 끝나 더 큰 데이터베이스 인스턴스로 옮겨졌고 메일이 다시 나갔다."}},{"text":{"en":"Removed the old data in production by hand, day by day, with an updated query","ko":"백업에서 시험해 고친 쿼리로 운영의 오래된 데이터를 하루 단위로 직접 지웠다. 그동안 메모리, 데이터베이스 크기, 지표를 가까이 지켜봤다."}}],"action_items":[{"text":{"en":"Delete old data in batches of 50 records with in_batches in the clean-up job","ko":"매일 도는 정리 작업을 in_batches(of: 50) 로 바꿔 한 번에 50건씩만 지우게 했다. ruby 메모리 요구량과 postgres 부하가 줄었다."}},{"text":{"en":"Run the clean-up job four times a day instead of once","ko":"정리 작업을 하루 한 번이 아니라 네 번 돌게 바꿔 한 번 실행의 영향을 줄였다."}},{"text":{"en":"Stay on a much larger database instance with room to grow","ko":"성장 여유가 훨씬 많은 더 큰 데이터베이스 인스턴스로 옮겼다."}},{"text":{"en":"Add cron monitoring to all cron jobs","ko":"모든 cron 작업에 cron 모니터링을 적용했다."}},{"text":{"en":"Point the status pages at a /health endpoint that checks database access and returns a 500 error on failure","ko":"상태 페이지가 헬스 체크 엔드포인트 /health 를 보게 바꿨다. 이 엔드포인트는 데이터베이스 접근을 포함해 여러 가지를 확인하고 하나라도 실패하면 500 오류로 응답한다."}},{"text":{"en":"Investigate a hot-replica database for automated failover","ko":"자동 장애 조치를 위한 핫 레플리카 데이터베이스를 검토한다."}}],"contributing":["inefficient-query","monitoring-blind-spot","no-spare-capacity"],"recurrence_of":null,"symptoms_text":{"en":"At 13:36 BST on Friday August 11th 2023, minutes after MailPace reran its data clean-up job with more memory, notifications arrived that all database transactions were failing across multiple services. The managed Postgres database had gone offline, did not come back when restarted, and reported that it was out of space.\n\nThe outage was preceded by signs found in routine checks at 13:00: the database that stores sent emails and API details was about 1.5x larger than it should have been and at about 80% capacity, and it held data older than the 30-day retention policy. The logs showed that the daily clean-up cron job, a Rake task that deletes each domain's old outgoing emails with destroy_all, had not been completing and was exiting with the message killed. Taking killed to mean the job ran out of memory, the team raised the job's memory from 1gb to 8gb at 13:15; the job then ran for a few minutes and the database filled.\n\nThe team could not see how a database it believed to be at 70% capacity could run out of space while data was being deleted. MailPace's status pages did not show the outage, presumably because https://app.mailpace.com still returned a 200 while the database was down. At 15:25, while the provider migrated the database, it was put in read-only mode: users could log in but not send emails, and the team at first thought it had fully recovered, then realised its mistake and updated Twitter. Emails went out again after full recovery at 15:50.","ko":"2023-08-11 13:36 BST 에 여러 서비스에서 모든 데이터베이스 트랜잭션이 실패한다는 알림이 들어왔다. 데이터베이스를 다시 시작하려 했지만 공간이 없다며 올라오지 않았다. 메일도 나가지 않았다.\n\n그날 13:00 정기 점검에서 보니, 보낸 메일과 API 정보를 저장하는 데이터베이스가 있어야 할 크기의 약 1.5배였고 용량의 약 80% 를 쓰고 있었다. 30일 보존 기간보다 오래된 데이터가 남아 있었다. 로그를 보니 매일 도는 정리 cron 작업이 끝나지 못하고 killed 메시지를 남기며 종료되고 있었다.\n\nkilled 는 보통 메모리 부족을 뜻하므로 13:15 에 cron 인스턴스 메모리를 1gb 에서 8gb 로 늘렸다. 작업은 몇 분 동안 돌다가 갑자기 끝났고, 그 직후 장애가 시작됐다. 데이터를 지우는 중이었고 70% 정도만 쓰던 데이터베이스였기 때문에 공간이 바닥났다는 것을 처음에는 믿기 어려웠다.\n\n상태 페이지 두 개 모두 장애를 잡지 못했다. 데이터베이스가 내려갔는데도 https://app.mailpace.com 이 여전히 200 으로 응답하고 있었기 때문으로 추정했다.\n\n15:25 에 이전 중인 데이터베이스가 읽기 전용 모드가 되자 사용자는 로그인할 수 있었지만 메일은 보낼 수 없었다. 팀은 처음에 완전히 복구됐다고 생각했다가 곧 잘못을 알고 Twitter 에 다시 알렸다."},"updated_at":"2026-11-14T12:33:57.928443Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0326","org_slug":"mailpace"}