???: 송금을 했는데, 송금이 안된 것 같아요 돈이 안 줄어요!!!
개발자 : 어? 분명히 상대한테 돈은 갔다는 로그가 있는데... 일단 롤백부터 하고 올게요
로그를 보면서
상대에게 돈이 갈 때도 있고, 안 갈 때도 있네요 왜 갈 때도 있고, 안 갈 때도 있었을까...
바꾼 것은 테이블밖에 없는데, 테이블에 무슨 일이 났는지 확인해 봐야겠네요
어떤 일이 발생했을까요?
기존 로직과 쿼리입니다
A로부터 5000원을 차감하고, B에는 5000원을 추가합니다
어느 날 테이블을 보면서, 왜 column 명이 old_amount 지? 그냥 amount로 바꿔버려야지~ 하고 테이블을 바꾸기 시작합니다
기존 코드에서 amount라는 column을 추가하고
update를 할 때, amount와 old_amount 모두에 쓰기 작업을 해주니,
예전 서버에서 읽는 요청을 해도 문제가 없겠다는 생각을 하면서 배포를 하게 됩니다
여기서 모든 문제가 시작됩니다
고려할 사항
문제 상황은 A 계좌만으로 충분히 표현할 수 있기에, 앞으로는 A 계좌만을 통해서 설명드리도록 하겠습니다
서비스는 무중단배포를 구축한 상황이고, BlueGreen 배포를 진행하고 있습니다
읽는 것은 새로운 버전은 amount를 읽고, 과거 버전은 old_amount를 읽고 있는데요
여기서 문제가 발생했습니다
문제 상황
예전 서버로 가던 트래픽을 한순간에 모두 새 서버로 보내는 것은 불가능하기에, 일정 시간은 양쪽 모두에 요청이 갑니다. 편의를 위해서 amount는 100,000원이고, old_amount 도 100,000원이라고 생각해 봅시다
예전 서버에 트래픽이 와서 A 계좌에서 100,000원 에서 5,000원을 출금하는 것을 마무리했다고 생각해 봅시다
amount는 100,000원이고, old_amount는 95_000원입니다
새 서버에서 계좌를 읽어오면 amount를 읽기에 100,000원이고, 100,000원 출금에 성공합니다.
총금액은 100,000원이 이었지만, 105,000원이 출금되는 현상을 볼 수 있습니다(예전 서버에서 5,000원, 새 서버에서 100,000원만큼 출금이 됩니다)
당연하지만 그런 일이 발생하면 안 되겠죠?
반대로 새 서버에 트래픽이 와서 A 계좌에서 100,000원 에서 5,000원을 출금하는 것을 마무리했다고 생각해 봅시다
amount column 에도 95,000이 있고, old_amount 도 95,000이 되면서 올바르게 처리되게 됩니다.
배포가 진행되는 도중에 어떤 계좌는 정상적으로 출금되고, 어떤 계좌는 이상하게 처리되었던 원인이 발견되었습니다
지금부터 하나하나 이 문제를 해결하기 위해서 진행해 보도록 하겠습니다
새 서버에서도 old_amount를 읽는다
배포 과정에서 어떤 일이 발생할까?
예전 서버로 요청이 오는 경우
old_amount 가 95,000원으로 잘 변경됩니다. amount는 변경되지 않았지만, 잔액을 읽어올 때도 95,000원으로 잘 읽어오게 됩니다.
새 서버로 요청이 오는 경우
old_amount 가 95,000원으로 잘 변경됩니다. amount 도 95,000원으로 잘 설정됩니다. 양쪽 모두에서 old_amount를 변경하기에, 95,000원으로 잘 읽어오게 됩니다.
이 과정을 dual write라고 합니다.
첫 번째 단계가 끝났습니다. 지금부터는 빈 amount를 채우도록 하겠습니다
모든 계좌의 old_amount와 amount를 같게 맞춘다
하나하나 계좌를 받아서, old_amount와 amount를 동일하게 맞춰주는 과정이 필요합니다.
계좌 총금액이 변경되지 않으면, amount column 이 업데이트되지 않기에, null로 비어있는 부분을 채워주는 과정입니다.
이때 db에 lock을 거는 작업을 하거나, redis 같은 캐시라면 lua script 같이 atomic 한 연산을 활용할 수 있습니다
끝나게 되면, 모든 계좌에 amount는 앞으로도 old_amount와 동일하게 됩니다.
새 서버에서 old_amount와 amount 모두에 쓰기 연산을 하게 되니까요
지금부터는 새 서버가 예전 서버가 됩니다. 명칭을 바꾸다 보면 헷갈릴 테니 새새 서버(Green Blue)라고 명명하겠습니다.
새로운 서버에서 amount와 old_amount를 모두 읽는다
배포 과정에서 어떤 일이 발생할까?
새 옛날 서버에 요청이 갈 경우
old_amount와 amount 모두 변경 작업이 진행됩니다.
따라서 어떠한 경우에도 잘못된 데이터를 쓸 수 없고, 읽을 때도, old_amount를 잘 읽어옵니다
새 새 서버에 요청이 갈 경우
old_amount와 amount 모두 변경 작업이 진행됩니다.
따라서 어떠한 경우에도 잘못된 데이터를 쓸 수 없고, 읽을 때도 amount를 잘 읽어옵니다.
새로운 서버에서 amount 만을 읽고 쓴다
위 그림과 같이, 이제부터는 amount 만을 업데이트하고, 읽어올 수 있게 됩니다.
배포 과정에서 어떤 일이 발생할까?
새새 옛날 서버에 요청이 갔을 경우
old_amount와 amount 모두 변경작업을 하고, amount 만 읽어오기 때문에, amount와 old_amount 모두 최신 데이터가 보장됩니다.
amount를 읽어오기에, 최신 데이터를 잘 읽어온다고 할 수 있죠
새새 새 서버에 요청이 갔을 경우
amount에 변경작업을 하고 amount 만 읽어오기 때문에, amount는 최신 데이터가 보장이 됩니다.
amount를 읽어오기 때문에, 최신 데이터를 잘 읽어온다고 할 수 있죠
이제 정말로 끝이 났습니다. 최신 버전에서 amount라는 column 만 쓰고, 읽는 버전이 되었고, 그 과정에서 데이터 손실은 발생하지 않습니다.
데이터베이스 migration 요약
1. 첫 번째 배포에서는 새 column과 old column 모두 write 하고, old column을 읽는다.
2. 배치를 통해서 새 column과 old column을 동일하게 맞춰준다
3. 두 번째 배포에서는 새 column과 old column 모두 write 하고, 새 column을 읽는다
4. 세 번째 배포에서는 새 column 만 write 하고, 새 column을 읽는다
이 과정들을 모두 거치지 않으면, 데이터 정합성이 깨지거나 서비스에 장애가 발생하게 됩니다.
대부분의 데이터의 경우에는 정합성이 중요하지 않기에, 그냥 바로 새 column 만 읽고 쓰도록 해도 괜찮을 수 있습니다.
하지만 진짜 중요한 기능에서는 이런 과정들을 통해서 데이터를 안정적으로 바꿀 수 있습니다
긴 글을 읽어주셔서 감사합니다
'프로젝트' 카테고리의 다른 글
장애 없는 코드를 만드려면 어떻게 해야할까? (7) | 2023.12.24 |
---|---|
카페인팀 서버 아키텍처를 설명해드리겠습니다 (7) | 2023.07.14 |
프로젝트 git branch 전략 어떤 것이 있을까? (2) | 2023.06.28 |
쿠키로 Jwt RefreshToken 관리하기! (내 쿠키는 어디갔지?) (2) | 2023.05.15 |
ElasticCache 에 SpringDataRedis 에서 키 동기화 문제 (0) | 2023.04.30 |