![[번역] Shopify - 탄력 있는 결제 시스템을 위한 10가지 팁](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbErCvY%2FbtsLqPF4ha0%2F7LC3EUj2GSVVoVprtjCjl0%2Fimg.webp)
10 Tips for Building Resilient Payment Systems - Shopify
10 Tips for Building Resilient Payment Systems - Shopify
Top ten tips and tricks for building resilient payment systems from a Staff Developer working on Shopify’s payment infrastructure.
shopify.engineering
위의 문서를 번역 및 정리했습니다.
1. Timeout을 줄여라
Ruby 언어의 서버와의 Connection을 연결 하기 위한 HTTP Client timeout은 60초이고, write data, read response를 위한 timeout도 60초입니다.
온라인 웹/애플리케이션에서 이 값은 유저가 기다리기엔 너무 긴 시간이죠.
Go나 Node js의 http client는 default timeout이 심지어는 없습니다!
이 문장의 의미는 응답하지 않는 서버는 리소스를 무기한으로 묶고 인프라 비용을 불필요하게 증가시킬 수 있습니다.
timeout은 데이터 저장에도 설정 될 수 있습니다.
MySQL의 MAX_EXECUTION_TIME 같은 옵션들을 활용할 수 있습니다.
독자들이 본 포스트에서 단 한가지 배울점이 있다면 timeout에 대해서 조사하고 사용 가능한 가장 작은 값의 timeout을 설정하라는 것입니다.
2. Circuit Breaker 설치
Timeout은 서버가 얼마나 오랫동안 기다릴지를 정하는 상한 값을 정할 수 있습니다.
그러나, 다운된 서비스는 잠시동안 다운되는 경향이 있으므로, 짧은 시간에 많은 timeout이 난다면 우리는 그 시간동안 요청하지 않는 방법으로 개선할 수 있습니다.
집이나 아파트에서 볼 수 있는 Circuit Breaker 같이요
Shopify는 Ruby내의 HTTP, MySQL, Redis 그리고 gRPC 서비스를 Circuit breaker로 보호하기 위해서 Semian을 개발했습니다.
우리의 서비스가 다운 됐을 때 바로 exception을 raise함으로써 우리는 발생할 수 있는 timeout을 없애고 resource를 아낄 수 있습니다.
아래와 같은 케이스를 참조 링크로 추가했습니다.
In some cases rescuing these exceptions allows you to provide a fallback. Building and Testing Resilient Ruby on Rails Applications describes how we design and unit tests such fallbacks using Toxiproxy.
Semian은 보호중인 식별자를 생성하기 위해 호스트와 HTTP endpoint를 연결할것을 추천합니다.
(정확한 의미는 잘 모르겠네요)
Worldwide한 결제 과정은 일반적으로 single endpoint를 사용합니다만, 종종 승인율과 낮은 비용을 위해서 현지 업체를 사용하는 경우가 있습니다.
Shopify의 카드 결제 시스템에는 조금 더 정확한 Semian ID를 부여하기 위해 거래자의 국가코드를 endpoint host와 port에 추가했습니다.
이를 통해서 특정 국가에서 지역 정전/마비 등이 있어도 다른 나라에는 영향을 주지 않을 수 있습니다.
3. Capacity를 이해하기
"시스템의 평균적인 고객의 수는 그들의 평균 arrival rate와 시스템에서 머무는 평균 시간을 곱한것과 같습니다"
arrival rate는 시스템을 접속한 수입니다.
요약하자면 예상되는 고객의 수를 잘 정의하고 rate limiting와 부하 분산을 통해서 이를 해결하라 입니다.
4. Monitoring과 Alerting을 추가하라
Google SRE 팀에는 4가지 golden signal을 언급하고 있습니다.
- Latency : 처리가 실행되고 완료/실패되는 시간의 단위입니다. circuit breaker는 실패를 더 빠르게 처리하고 misleading graph로 이끌 수 있습니다.
- Traffic : 새로운 작업이 시스템에 추가되는 비율입니다. (주로 Requests per minute 단위)
- Errors : 예상치 못한 결과의 비율입니다. 결제시스템에서는 결제 실패와 에러를 구분합니다.
실패는 계좌의 금액이 부족할때를 주로 의미합니다. (의도된 결과입니다.)
500 response는 뭔가 예상치 못한 문제를 의미합니다. - Saturation: 전체 Capacity와 비교했을때 시스템이 받는 Load를 의미합니다.
5. Structured Logging을 구현하라
시스템이 동작하고 있을때 메트릭이 high-level overview를 제공하는경우, Logging은 하나의 web request 혹은 background job에서 어떤일이 일어나고 있는지 이해시켜줍니다.
즉시 사용가능한 Ruby의 로그는 human-friendly 하지만 기계가 파싱하기는 힘듭니다.
이런 로그는 하나의 application 서버만 동작할때 유용하고 로그를 중앙에서 관리하기 시작하면 검색하기 쉬운 뭔가가 필요할겁니다.
이런 요구사항은 Structured Logging, 예를들면 key=value pair를 쓰고있는 JSON을 활용하면 쉽게 가능합니다.
분산 시스템에서는 correlation ID를 사용해서 넘겨주면 유용합니다.
가상의 예시로는 구매자가 구매 checkout을 하게되면 correlation_id가 생성됩니다.
이 ID는 구매 서비스 API Call을 만드는 background 서비스 (신용카드등의 민감 정보를 다루고 있습니다) 로 전달됩니다.
그래서 이 correlation_id를 통해서 관련 로그를 한번에 모두 볼 수 있게됩니다.
6. Use Idempotency Keys (멱등성 키 사용)
분산 시스템에서는 신뢰할 수 없는 네트워크를 사용합니다. 그게 대부분의 시간에 reliable 하다고 할지라도요.
Shopify 스케일에서는 100만개중에 1번정도 그런 경우가 발생하는데, 하루에도 몇번씩 발생하는 일입니다.
만약 그 오류가 결제 API call이 타임아웃이고, 우리가 안전하게 재시도하기를 원한다고 가정해봅시다.
두번의 결제 부과는 고객 뿐만 아니라 판매자에게도 환불 과정을 요구하기에 상당한 불편을 유발 할 수 있습니다.
요약하면, 우리는 위와 같은 불편 없이 한번의 payment/refund로 이 불편을 없애도록 하고 싶습니다.
우리의 중앙 결제 서비스는 최소 한번 혹은 더 많은 동일한 API reuqest 시도들을 track 할 수 있습니다.
각각에 멱등성 키를 부여함으로써 말이죠
만약 특정 키를 가지고 있는 single request가 특정 step에서 실패했고, 동일 키로 재시도된 request가 온다면 recovery step이 실행되고 same state를 재생성합니다.
아래 참조에서 실제 예시를 확인할 수 있습니다.
Building Resilient GraphQL APIs Using Idempotency
describes how our idempotency mechanism works in more detail.
우리는 request가 24시간 혹은 그 이하 시간 내에 재시도 되어야 하기에, 그 시간동안 멱등성 키는 unique 해야합니다.
그래서 우리는 ULID (Universally Unique Lexicographically Sortable Identifier)를 사용합니다.
랜덤 버전 UUID대신말이죠
ULID는 80bit의 random data앞의 첫 48-bit에 timestamp를 포함합니다. (총 128bit)
이 timestamp가 ULID들을 b-tree 구조의 데이터 베이스 내에서 Sorted 될 수 있게 합니다.
UUID는 Sort에 도움되는 데이터가 앞쪽에 포함 되어 있지 않아 ULID를 사용한것으로 생각됩니다.
우리는 UUIDV4 에서 ULID로 키를 바꿈을 통해서 50%의 Insert duration을 줄였습니다.
7. 일관성 있는 조정 (Be Consistent With Reconcilation)
조정을 통해 우리는 우리의 정보와 파트너사의 정보가 일치하는지 확인합니다.
정확한 기록을 보유하는 것은 표시 목적 뿐 아니라 일부 관할 구역의 판매자를 위해 생성해야하는 세금 양식의 입력으로도 사용됩니다.
불일치가 발생하는 경우 데이터베이스에 이상 징후를 기록합니다.
가능한 경우 자동 수정을 시도하더라도 시스템이 무엇을 얼마나 자주 수행했는지 알 수 있도록 불일치를 추적하려고 합니다.
8. 부하 테스트 통합
위에서 정의한 Capacity는 유용하기는 하지만, 작업 처리 시간이 균일하지 않아서 100% 포화 달성하는것은 거의 불가능합니다.
실제로의 대기열의 크기는 70~80%에서 증가하기 시작하며 대기열에서 대기하는 데 소요시간이 클라이언트 제한 시간을 초과하면 클라이언트 관점에서 서비스가 중단됩니다.
대기열의 크기를 제어할 수 있는 다양한 방법이 있습니다.
예를 들어 스크립트 가능한 로드밸런서를 사용하여 주어진 시간에 발생하는 체크아웃의 양을 제한합니다.
구매자에게 좋은 사용자 경험을 제공하기 위해 결제를 원하는 구매자의 수가 용량을 초과하는 경우 구매자를 대기열에 배치한 후 주문 비용을 지불 할 수 있도록 돕습니다.
Surviving Flashes of High-Write Traffic Using Scriptable Load Balancers
describes this system in more detail.
Shopify는 벤치마크 스토어를 만들어 대량 플래시 판매를 시뮬레이션 하여 시스템의 한계와 보호 매커니즘을 정기적으로 테스트합니다.
Pummelling the Platform–Performance Testing Shopify
describes our load testing tooling and philosophy.
9. 사고 관리의 최우선 설정
오류가 나는것을 완전히 피할 수는 없습니다.
사고는 일반적으로 대기중인 서비스 소유자가 모니터링을 기반으로하는 자동 Alert를 통해 호출되거나 누군가가 문제를 발견한 경우 수동으로 호출될 때 시작됩니다.
문제가 확인되면 Slack 봇으로 전송된 명령으로 사고 프로세스를 시작합니다.
그런 대화들은 아래와 같은 Role을 가진 담당자에게 할당됩니다.
- Incident Manager On Call (IMOC): 사고 처리 담당자입니다.
- Support Response Manager (SRM): public communication 담당자입니다.
- 서비스 Owner : 안정성 복구를 위한 작업을 합니다.
10. 사고 발생에 대한 회고를 하라
우리는 사고에 대한 회고를 합니다. 미팅에서는 아래와 같은 것들을 논의합니다.
- 발생 원인에 대한 deep dive
- 우리가 가지고 있던 시스템에 대한 잘못된 가정
- 같은 문제를 방지하기 위한 대책
'관심 분야 센싱 > 다른 사람 포스팅 구경하기' 카테고리의 다른 글
[번역] PageRank의 시대는 끝났다 (0) | 2025.01.26 |
---|---|
[번역] 대부분 golang과 sqlite로 충분하다 (2) | 2024.08.31 |
개발 및 IT 관련 포스팅을 작성 하는 블로그입니다.
IT 기술 및 개인 개발에 대한 내용을 작성하는 블로그입니다. 많은 분들과 소통하며 의견을 나누고 싶습니다.