{"id":"IL-0572","org":"Mercari","date":"2026-06-16","docs":[],"title":{"en":"About 10 million Pub/Sub messages backed up and Merpay accounting data was not written to Cloud Spanner for over a day","ko":"Merpay 회계 이벤트 처리 시스템에서 Cloud Pub/Sub 메시지 1000만 건이 밀려 새 회계 데이터가 하루 넘게 Cloud Spanner에 들어가지 못했다"},"domain":["gcp","database"],"impact":{"scope":"Merpay accounting event pipeline: new accounting data was not registered in Cloud Spanner and stayed unreconciled, so it could not go on to merchant settlement and monthly closing; falling on a closing day, this caused wide business impact","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"When moving to a new generation of a managed service, defaults that the team never changed can surface as problems under load, so parameters that only matter at peak, such as scale limits, concurrency and timeouts, should be set explicitly at every migration. An outage rarely has one cause; the fact that the previous month passed under the same conditions pointed to traffic continuity, and prevention needs to look at the trigger, the amplification and what turned it into damage. Capacity settings and the monitoring that shows when they are approached have to be designed together, and teams that run systems they inherited should review these settings and assumptions regularly.","ko":"관리형 서비스의 세대를 옮길 때는 직접 바꾸지 않은 기본값이 부하가 걸릴 때 문제로 드러난다. 스케일 상한, 동시 처리 수, 타임아웃처럼 평소에는 효과가 보이지 않는 파라미터는 옮길 때마다 명시해야 한다. 장애 원인은 하나가 아닐 수 있으므로 촉발, 증폭, 실제 피해로 이어진 요인을 각각 봐야 한다. 설정 최적화와 그 한계에 가까워지는 것을 알아채는 모니터링은 늘 함께 갖춰야 하고, 물려받아 운영하는 시스템일수록 설정과 전제를 정기적으로 다시 살펴야 한다."},"related":[],"sources":[{"url":"https://engineering.mercari.com/blog/entry/20260615-7283dc59d4/","kind":"blog","archived":"https://web.archive.org/web/20260805133436/https://engineering.mercari.com/blog/entry/20260615-7283dc59d4/"}],"summary":{"en":"Merpay's accounting system collects accounting events published by internal microservices and records them in Cloud Spanner. The pipeline is asynchronous: senders do not wait for a response, Pub/Sub pushes the events to Cloud Run, and the capacity of Cloud Run and Spanner sets the throughput of the whole pipeline. Load concentrates at month end and month start, with spikes of tens of times normal traffic. Accounting data is the basis for reconciliation with the sending services, merchant settlement and monthly closing, so delays hit the business directly. A separate pull subscription read by Dataflow also saves every message to Cloud Storage (GCS).\n\nMore than a month before the incident the team moved the Cloud Functions from 1st gen to 2nd gen without specifying --max-instances, so the scale limit fell from effectively unlimited to 100 with no change to code or deploy scripts. 100 instances were enough in normal times, so nothing showed for a while, and without monitoring nobody noticed that some months had already come close to the limit. Three factors combined: the limit of 100 started the backlog, the continuous traffic of the day kept it from clearing, and eventMaxAge turned it into dropped messages. The Spanner autoscaler, the open-source mercari/spanner-autoscaler, watched only High priority CPU and had no way to scale on Total CPU, so it did not add capacity while Total CPU sat at 100%.\n\nData loss was avoided by re-ingesting the messages saved in GCS, but data that had not reached Spanner stayed unreconciled, and because this fell on a closing day the business impact was wide. Afterwards the team set every scaling parameter of Cloud Run explicitly in the deploy definition, raised max-instances to 1000 in steps with Spanner load in mind, and raised concurrency after confirming the handler was safe for concurrent requests. The SRE team that maintains spanner-autoscaler added a dual CPU scaling mode that scales when either High priority or Total CPU crosses its threshold, released as v0.8.0, and new Datadog monitors watch the Cloud Run instance count and undelivered Pub/Sub messages.","ko":"Merpay 회계 시스템은 사내 마이크로서비스가 보내는 회계 이벤트를 Cloud Pub/Sub에서 push 구독으로 Cloud Run에 넘기고 Cloud Spanner에 기록한다. 기록된 데이터는 보낸 서비스와 대조하는 리컨사일을 거쳐 가맹점 정산과 매달 하는 회계 마감에 쓰인다. 모든 처리가 비동기라 Cloud Run과 Spanner의 처리 능력이 파이프라인 전체의 처리량 상한이 되고, 월말과 월초에는 평소의 수십 배 부하가 몰린다. Pub/Sub 메시지는 별도의 pull 구독으로 Dataflow가 읽어 Cloud Storage에도 저장한다.\n\n장애 한 달여 전 Cloud Functions를 2nd gen으로 옮길 때 --max-instances 를 명시하지 않아 상한이 100으로 내려갔지만, 평소에는 100 인스턴스로 충분해 드러나지 않았다. 이전 월말도 maxInstances 100, eventMaxAge 21분, 3시간 트래픽 총량 약 17~19GB로 조건이 거의 같았지만, 부하 사이사이에 쉬는 구간이 있어 Spanner CPU 부하와 ack되지 않은 메시지의 적체가 그때마다 풀렸다. 장애 당일에는 쉬는 구간 없이 피크가 약 40분 이어졌다. 상한에 걸린 Cloud Run은 메시지를 다 처리하지 못했다. 인스턴스가 한꺼번에 쓰는 탓에 Spanner CPU가 치솟아 처리가 더 느려졌고, eventMaxAge 를 넘긴 메시지는 버려졌다. Spanner autoscaler는 High priority CPU만 봐서 Total CPU가 100%인데도 스케일업하지 않았다.\n\n데이터 손실은 Cloud Storage의 데이터를 다시 넣어 피했다. 그러나 Spanner에 등록되지 않은 데이터는 리컨사일이 끝나지 않은 채 남았고, 마감일과 겹쳐 업무 전반에 영향을 줬다. 팀은 Cloud Run 스케일 파라미터를 배포 정의에 모두 명시하고 값을 올렸으며, mercari/spanner-autoscaler에 Total CPU 기준 스케일링을 넣고, 상한 접근과 메시지 적체를 알리는 모니터링을 더했다."},"trigger":{"en":"More than a month earlier the Cloud Functions of the pipeline had been moved from 1st gen to 2nd gen (based on Cloud Run) without setting --max-instances in the deploy command, which lowered the scale limit from effectively unlimited, the 1st gen default, to 100, the 2nd gen default. The month-end peak, which on that day continued for about 40 minutes, reached that limit.","ko":"장애 한 달여 전 Cloud Functions를 1st gen에서 Cloud Run 기반 2nd gen으로 옮기면서 --max-instances 를 지정하지 않아, 스케일 상한이 1st gen 기본값인 사실상 무제한에서 2nd gen 기본값인 100으로 내려갔다. 월말 아침 부하가 쉬는 구간 없이 약 40분 이어지자 이 상한에 걸렸다."},"severity":"unknown","symptoms":["throughput-degraded","database-overloaded","autoscaler-misbehaving"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":null,"components":["serverless-functions","message-broker","database"],"root_cause":"upgrade-breaking-change","remediation":[{"text":{"en":"Re-ingested the messages saved to GCS by the Dataflow subscription, avoiding data loss","ko":"별도 pull 구독으로 Dataflow가 Cloud Storage에 저장해 둔 데이터를 다시 넣어 데이터 손실을 피했다."}}],"action_items":[{"text":{"en":"Set max-instances and the other scaling parameters of Cloud Run explicitly in the deploy definition, raising max-instances from 100 to 1000 in steps","ko":"max-instances 를 비롯한 스케일 관련 파라미터를 배포 정의에 모두 명시해 세대나 기본값이 바뀌어도 영향을 받지 않게 했다. max-instances 는 Spanner 부하를 생각해 100에서 1000까지 단계적으로 올렸다."}},{"text":{"en":"Set min-instances to 1 to avoid cold starts, raise concurrency from 1 to 10, and set CPU to 1 and memory from 512MB to 1Gi","ko":"콜드 스타트를 피하려고 min-instances 를 1로 두고, 핸들러의 공유 상태가 안전한지 확인한 뒤 concurrency 를 1에서 10으로 올렸다. 이에 맞춰 cpu는 1, memory는 512MB에서 1Gi로 늘렸다."}},{"text":{"en":"Use the dual CPU scaling mode of spanner-autoscaler v0.8.0 with a new Total CPU threshold of 70%","ko":"mercari/spanner-autoscaler를 관리하는 SRE 팀이 High priority CPU와 Total CPU를 OR 조건으로 보는 dual CPU scaling mode를 구현했다. 팀은 이 기능이 담긴 v0.8.0을 도입해 Total CPU 임계값 70%를 새로 뒀다."}},{"text":{"en":"Lower the High priority CPU threshold from 60% to 55% so scale-up starts earlier","ko":"스케일업이 더 빨리 시작되도록 High priority CPU 임계값을 60%에서 55%로 내렸다."}},{"text":{"en":"Forbid Spanner scale-down between 5:00 and 7:00","ko":"월말 피크가 시작될 때 확보한 PU가 줄지 않도록 아침 5시부터 7시까지 Spanner 스케일다운을 막는다."}},{"text":{"en":"Reserve Spanner processing units on a schedule for periods with predictable load","ko":"월말 아침이나 대규모 리컨사일이 도는 달처럼 부하를 예상할 수 있는 기간에는 일정에 따라 PU를 미리 확보한다."}},{"text":{"en":"Alert in Datadog when the Cloud Run instance count reaches 40% and 60% of its limit","ko":"Datadog에 Cloud Run 인스턴스 수가 상한의 40%와 60%에 이르면 각각 경고 단계와 심각 단계로 알리는 모니터를 더했다."}},{"text":{"en":"Alert in Datadog when undelivered Pub/Sub messages exceed a threshold, lowering it step by step","ko":"Datadog에 Pub/Sub의 전달되지 않은 메시지 수가 임계값을 넘으면 알리는 모니터를 더했다. 오탐을 피하려고 보수적인 임계값에서 시작해 평소 상태를 알아 가며 단계적으로 낮춘다."}}],"contributing":["default-config-unchanged","limit-unknown","delayed-effect-change","monitoring-blind-spot"],"recurrence_of":null,"symptoms_text":{"en":"From about 6:00 on the last business day of a month, Cloud Pub/Sub messages of the Merpay accounting event pipeline began to back up, new accounting data stopped being registered in Cloud Spanner, and the accounting system intermittently could not accept new data. The backlog grew to about 10 million messages.\n\nOnce the backlog started it fed itself. Cloud Run, which receives the messages through a push subscription, was capped at maxInstances=100 and could not keep up; the capped instances writing all at once pushed Spanner CPU up, each write took longer, throughput fell further and the backlog grew. Messages that waited longer than eventMaxAge, the maximum time a message is kept before it is discarded, were dropped without ever reaching Spanner. The system recovered and failed again repeatedly, and it took more than a full day to settle.\n\nSpanner had an autoscaler, but during the incident its Processing Units (PU, Spanner's unit of compute capacity) did not scale up at all even though CPU was high. Total CPU was stuck at 100% while High priority CPU alone did not stay above the autoscaler's 60% threshold. No alert warned that Cloud Run had reached its scale limit.\n\nThe conditions had looked the same at the previous month-end peak after the migration, without a failure: maxInstances was 100, eventMaxAge was the same 21 minutes and total traffic was about 17 to 19 GB in 3 hours. The difference was continuity. At the earlier peak short troughs between bursts let Spanner CPU and the unacknowledged messages reset, while on the incident day the peak ran for about 40 minutes without a trough.","ko":"월말 마지막 영업일 아침 6시쯤부터 Cloud Pub/Sub 메시지가 밀리기 시작했고, Cloud Spanner에 새 회계 데이터를 등록하지 못하는 상태가 간헐적으로 이어졌다. 밀린 메시지는 1000만 건 규모에 이르렀다. 복구와 재발을 거듭하며 수습하기까지 하루가 넘게 걸렸다.\n\nCloud Run 인스턴스 수는 maxInstances=100 에 걸려 더 늘지 않았고, 상한까지 찬 인스턴스가 한꺼번에 쓰면서 Spanner CPU가 치솟았다. 한 건을 쓰는 시간이 길어지자 처리량이 더 떨어지고 밀린 메시지가 더 쌓였다. 밀린 시간이 eventMaxAge 를 넘은 메시지는 Spanner에 등록되지 못한 채 버려졌다.\n\nSpanner의 Total CPU는 100%에서 내려오지 않았지만, autoscaler는 Spanner의 계산 용량 단위인 PU, 곧 Processing Unit을 전혀 늘리지 않았다. autoscaler가 보는 High priority CPU는 임계값 60%를 계속 넘지 않아 스케일업 조건을 채우지 못했다.\n\nCloud Run 인스턴스가 스케일 상한에 닿았을 때 알려 주는 알림이 없었다. 팀에게는 어느 월말에 갑자기 장애가 난 것처럼 보였지만, 나중에 보니 상한에 가까워진 달도 이미 있었다."},"updated_at":"2026-11-14T12:34:21.941143Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0572","org_slug":"mercari"}