{"id":"IL-0279","org":"Grab","date":"2019-02-15","docs":[],"title":{"en":"One failed Redis Cluster replica left only about 1 in 21 calls to Grab's Apollo service succeeding for a minute","ko":"Redis Cluster 의 슬레이브 노드 하나가 죽자 파이프라인 읽기가 실패해 Grab 의 운송 예약 서비스 Apollo 호출이 1분 동안 21번에 1번꼴로만 성공했다"},"domain":["database","aws"],"impact":{"scope":"Apollo, Grab's driver-side state machine on the critical path of transport bookings: about 1 in 21 calls succeeded for one minute and Grab rides were significantly down","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Having several replicas did not protect Apollo, because the client side, not the Redis Cluster server, caused the failure. Grab suggests three ways to avoid it: a separate Redis Cluster client used only for pipelining with RouteByLatency set to true, so pipelined reads go through the master nodes when latency is under 1ms, watching master CPU because they take more load; checking the error of each command in a pipeline instead of failing the whole call, so successful queries are not affected; and changing the Go-Redis library so that the pipeline path reloads the nodes' state when an error happens, as defaultProcessor already does.","ko":"Redis Cluster 는 서비스 성능을 높이는 좋은 도구지만 쓰는 데 위험이 있다. 파이프라인 전용 Redis Cluster 클라이언트를 따로 만들고 RouteByLatency 를 true 로 두면, 지연이 1ms 아래일 때 파이프라인 읽기가 마스터 노드로 가서 파티션의 과반이 살아 있는 한 기대한 결과를 받는다. 이렇게 하면 마스터로 가는 부하가 늘어나므로 마스터 노드의 CPU 사용량을 살펴야 한다. 마스터가 그만한 트래픽을 감당하지 못하면, 파이프라인 호출에 오류가 나더라도 질의마다 오류를 따로 확인하면 된다. 그러면 성공한 질의는 영향을 받지 않아 피해가 줄어든다. Go-Redis 라이브러리는 오류가 나면 노드 상태를 다시 읽도록 고칠 수 있다. defaultProcessor 에 있는 이 로직을 defaultProcessPipeline 에 적용하면 된다."},"related":[],"sources":[{"url":"https://engineering.grab.com/preventing-pipeline-calls-from-crashing-redis-clusters","kind":"blog","archived":"https://web.archive.org/web/20260128093403/https://engineering.grab.com/preventing-pipeline-calls-from-crashing-redis-clusters"}],"summary":{"en":"Apollo stores the availability of individual drivers in an AWS ElastiCache Redis Cluster of 3 shards, each with 2 slaves. Apollo uses the Go-Redis client: writes go to the master nodes, and for reads the ReadOnly flag is on and RouteByLatency is off, which in Grab's gredis3 library turns on ReadOnlyFromSlaves so that reads go to slave nodes and spare the masters' CPU. When the slave failed, the Go-Redis pipeline code kept mapping read commands to the slave nodes from its cached cluster state, which the client refreshes only once a minute, so for up to a minute it kept sending reads to the unreachable slave and all commands assigned to it failed. Grab's Redis wrapper returned an error for the whole pipeline when any single command failed, and Apollo's code treated any error as a failure, which produced the high failure rate. Writes kept working because the client watches the master nodes and knows their state at once. The Redis Cluster recovered on its own after about one minute.","ko":"Apollo 는 Grab 의 기사 쪽 상태 머신이다. 거의 모든 요청의 중요 경로에 있고, 기사별 가용 상태를 AWS ElastiCache Redis Cluster 에 저장해 예약 서비스가 예약을 기사에게 배정하게 한다. 이 Redis Cluster 는 샤드 3개에 샤드마다 슬레이브 2대를 둔다. Apollo 는 Go-Redis 클라이언트를 쓰며 쓰기 질의는 마스터 노드로 보낸다. 읽기에는 ReadOnly 플래그를 켜고 RouteByLatency 플래그를 끈다. 그러면 Grab 의 gredis3 라이브러리에서 ReadOnlyFromSlaves 가 켜져 모든 읽기가 슬레이브 노드로 간다. 마스터 노드의 CPU 부하를 덜기 위해서다.\n\n2월 15일 슬레이브 노드 하나가 죽자 그 노드에 배정된 명령은 모두 실패했다. Grab 의 Redis 라이브러리 코드는 파이프라인 안의 명령 하나가 오류를 내면 파이프라인 전체가 실패했다고 알렸고, 엔지니어들은 err != nil 이면 실패로 처리했다. 그래서 HMGET 파이프라인 호출 대부분이 실패해 Apollo 호출이 21번에 1번꼴로만 성공했다.\n\nRedis Cluster 서버는 노드에 닿을 수 없게 되면 곧바로 다른 노드에 알리지만, Go-Redis 의 Redis Cluster 클라이언트는 노드 상태를 1분에 한 번만 새로 읽는다. 그래서 그 1분 동안 클라이언트가 닿을 수 없는 슬레이브 노드로 질의를 계속 보냈고, Apollo 가 회복하는 데 1분 가까이 걸렸다. 쓰기 질의는 마스터 노드로만 가고 클라이언트가 마스터 노드를 감시하므로 그 1분 동안 새 데이터를 잃지는 않았다."},"trigger":{"en":"A slave node of Apollo's Redis Cluster failed and needed to be replaced.","ko":"2019년 2월 15일 Apollo 가 쓰는 AWS ElastiCache Redis Cluster 의 슬레이브 노드 하나가 죽었다."},"severity":"unknown","symptoms":["high-error-rate","stale-service-routing"],"timeline":{"detected_after":null,"resolved_after":"1m"},"date_kind":"incident","detection":null,"components":["cache"],"root_cause":"software-bug","remediation":[],"action_items":[],"contributing":["assumption-unverified"],"recurrence_of":null,"symptoms_text":{"en":"On 15 February 2019 a slave node of the AWS ElastiCache Redis Cluster behind Apollo, Grab's driver-side state machine for transport bookings, failed and had to be replaced. For about one minute only roughly 1 in 21 calls to Apollo succeeded, a call failure rate above 95% against the Redis Cluster, and Grab rides were significantly down.\n\nThe errors came from pipeline GET calls: when Apollo sent a pipeline of HMGET commands to the Redis Cluster, the pipeline returned errors. The cluster has 3 shards with 2 slaves each, so a single slave failure should have left it fully available, and the Redis Cluster server broadcast the unreachable node to the other nodes at once. Writes, which go only to master nodes, were not affected and no new data was lost.\n\nGrab first looked at the Redis Cluster server and, after reproducing node failures on a local cluster, concluded that the server handled the failure correctly and was not the cause of the failure rate. The investigation then moved to the client side, the Go-Redis library and Apollo's queries, and to why recovery took almost a minute when the cluster had broadcast the change immediately.","ko":"2019년 2월 15일 Apollo 가 쓰는 Redis Cluster 의 슬레이브 노드 하나가 죽어 교체해야 했다. 그동안 운송 예약의 핵심 서비스인 Apollo 로 가는 호출이 대략 21번에 1번꼴로만 성공했고, Redis Cluster 가 스스로 복구하는 1분 동안 Grab 의 승차 이용이 크게 줄었다.\n\n슬레이브 노드 하나가 죽었는데 Redis Cluster 로 가는 호출 가운데 95% 넘는 호출이 실패했다. 샤드마다 복제본을 여럿 둔 의도와 완전히 어긋나는, 전혀 예상하지 못한 동작이었다.\n\n장애 동안 일부 코드가 파이프라인 GET 호출에서 오류를 많이 냈다. Apollo 가 Redis Cluster 에 HMGET 호출을 파이프라인으로 보내면 파이프라인이 오류를 냈다.\n\nRedis Cluster 서버는 노드 하나에 닿을 수 없게 되면 곧바로 다른 노드에 알린다. 이론대로라면 Redis Cluster 는 100% 가용해야 했으므로 Grab 은 서버 쪽은 원인이 아니라고 결론 내리고 클라이언트 쪽과 Apollo 의 질의를 살폈다."},"updated_at":"2026-11-14T12:33:53.278644Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0279","org_slug":"grab"}