토스 | SLASH 21 - 토스 서비스를 구성하는 서버 기술
- Infra : 데이터센터 2중화, active-active로 운영 + aws 일부 서비스 이용
- K8S + Istio + Ceph
- K8S : 효율적으로 서버를 관리하기 위해 Container Orchestration
- Istio : Calico cni, Service mesh
- Ceph : 민감자료 저장 용도, 내부 스토리지 서비스
- Cache, Monitoring :
- Memcached : 이전에는 자주 사용하던 캐시 스토리지 였으나 최근에는 데이터 보전 문제와 optimistic lock 구현 편이성 때문에 Redis Cluster를 더 많이 사용ㅇ중
- Kafka :
- 로그용 클러스터 : 로그 데이터 파이프라인으로 사용 중
- 메시지큐 클러스터 : 서비스에서 메시지 큐로 사용중
- ELK + filebeat + thanos + grafana : 로깅 & 모니터링
- Spring :
- 송금, 채팅 서비스, 카드사 알림, 은행 대출 추천서비스와 같은 외부 연동 서비스, 여러 서비스의 데이터를 통합해서 보여주는 서비스
- 이전에는 Java를 많이 사용했지만 최근엔 Kotlin을 많이 사용
- AWS : DNS설정, 이미지 검수, 정적 파일 serving
- 평상시에는 2개의 데이터 센터에 트래픽이 50:50으로 들어가도록 하지만, 한 쪽이 100%가 되는 트래픽 옮기는 작업을 자주함 → 자동화
-
- 장애가 발생했을 때, 원인 파악과 해결이 오래걸리기에
해결
보다는복구
를 중심으로 대응해, 장애가 나지 않는 반대편 센터로 트래픽 옮김 - 새로운 시스템 도입, 쿠버네티스 설정 변경 등의 작업시에 최대한 장애 여파를 줄이기 위해 트래픽을 한쪽으로 100% 옮기고 나서, 다시 반대편에는 트래픽의 1%정도만 준다.
- 적은 트래픽으로 문제가 없을시 점진적으로 트래픽 늘려남. 카나리 배포를 데이터 센터에 적용
- 트래픽을 조절할 수 있는 구간은 두 곳 : Route53, L7
- L7에서는 모든 트래픽이 곧바로 옮겨짐, Route53은 다른 라우터로 전파가 되어야 하기 때문에 시간이 소요됨
- 장애가 발생했을 때, 원인 파악과 해결이 오래걸리기에
-
- 19년도 쯤 DC/OS에서 K8S로 마이그레이션 함
- 컨테이너 오케스트레이션에서 가장 중요한 것 : service discovery + container lifecycle management
- Vamp : dc/os의 Service Discovery 기능 → 비효율적 한계
- DC2에서는 쿠버네티스 먼저 도입, DC1에서는 DC/OS를 운영함
- DC1에서도 점진적으로 DC/OS 노드를 줄여가고 점진적으로 쿠버네티스를 돌입
- 두 군데서 안정적인 운영이 가능하다고 판단한 뒤 전체 쿠버네티스 도입
- K8S를 도입하면서 함께 도입
- Infrastructure Concern :
- 모놀리틱 구조에서 MSA로 넘어가면서, 서버들간의 네트워크 처리가 필요하게 되었고 Circuit Breaker, Retry, Fallback등을 애플리케이션에서 처리
- Istio를 도입하면 istio-proxy가 sidecar로 붙어서 모든 네트워크를 proxy하게 됨
- proxy하면서 어플리케이션에서 하는 일을 대신할 수 있으니 어플리케이션의 언어와 상관없이, 개별 어플리케이션에서 하는것이 아닌 인프라 차원에서 해결
- Client Side Load Balancing
- MSA는 외부 → 내부 요청보다 내부<→내부 요청이 더 많음
- 내부 요청을 중앙집중식으로 처리하다보면, 장애에 취약하고 부하도 많이 받게 됨
- Istio는 내부 요청을 클라이언트 사이드에서 처리하게 해줌
- DC/OS보다 더 넓은 생태계, 더 큰 서비스에서 검증됨
- 더 높은 Observability
- Istio는 모든 서비스에 sidecar로 붙어서 실행되고 iptable을 통해 모든 트래픽을 제어
- 제어된 트래픽은 istio-proxy를 통해 나가고 받게 됨
- Istio-proxy : envoy-proxy를 Istio에서 래핑한 것, proxy가 어떻게 동작하는지 명확히 보여줌
- connection pool 동작방식, 커넥션이 유실된 곳 등을 통계로 확인할 수 있음
- → 문제점 파악이 쉬워짐
- 기존에는 네으퉈크 문제를 해결할 때 tcpdump로 보내는 쪽 받는 쪽 확인 했던 작업을 envoy가 대신 해줌
- Application Concern을 Infrastructure Concern으로 변경하기 힘든 부분도 존재
- 네트워크 처리를 인프라쪽으로 넘겨보려 시도했으나 아래와 같은 이유로 어려움
- Circuit breaker : host별 설정이라 세부 설정이 힘듬
- retry도 API의 트랜잭션 처리가 어떻게 되어있는지와 응답값에 따라 retry 필요성이 다름
- 대신,
mTLS
설정을 통해 A서비는 B를 호출하고, C는 B호출이 안되는 권한을 적용하거나 envoy filter를 작성해 애플리케이션 변경없이 dynamic하게 네트워크 기능을 추가할 수 있는 장점 - envoy filter로 헤더 처리, 접근 제어등을 하고 있음
- 1% Canary 가능
- Istio Route weight로 1% weight canary 배포 가능
- 인스턴스 단위로 카나리 배포를 하면 세밀한 트래픽 조절이 어려움
- 10개 인스턴스 단위에서 한개의 카나리 배포시 → 10% 카나리
- 하지만 Istio는 라우터의 weight로 카나리를 할 수 있어 인스턴스의 개수와 상관없이 조절
- Failure Injection Test & Squeeze Test
- Failure Injection Test : 특정 조건에 맞는 요청일 경우 실패/늦은 응답을 보내 서비스에서 실패해 대한 처리가 잘 되어있는지 확인
- Squeeze Test : 하나의 인스턴스에 요청을 높여 부하를 주는 테스트
- → 라이브(프로덕션?)에서 진행되기 때문에 서비스의 최대성능이 아닌 어느 정도 요청부터 서비스가 부하를 느끼는지 확인 용도
- 클라이언트의 인증, 암복호화를 담당하는 서비스가 있었는데 트래픽이 커지면서 api gateway의 필요성이 커짐
- zuul, kong, Spring Cloud Gateway 중 Spring Cloud Gateway선택
- Spring Cloud Gateway 선택 이유 :reactor에 대한 운영 자신감 상승
- 인증, oauth2, 암복호화 : custom filter를 만들어 구현
- Static / dynamic routing : dynamic은 라우팅 검증 프로세스가 확립되지 않아 대부분 static으로 추가
- 스프링 내부 코드는 reactor이지만 유지보수의 편리성을 위해 coroutine 사용
- 서비스가 커지면서 생기는 API Gateway에 관한 고민들
- 늘어나기 시작하는 클라이언트의 종류
- GateWay는 누가 관리할 것인가?
- 어느팀이 관리할지? 공통로직을 어떻게 처리할지?
- → 토스에서는 플랫폼팀이 관리하고 각 팀의 PR을 받아 플랫폼팀에서 승인, 공통로직은 모듈화해서 처리
- 토스의 대부분의 서비스들은 MVC로 되어있으나 홈탭 및 내소비와 같이 여러 데이터를 모아 보여주는 I/O가 많은 프로젝트에서는 Webflux사용
- 최근에는 스프링이 Kotlin dsl을 잘 지원하고 R2DBC를 사용하면서 많은 프로젝트가 webflux로 시작하는 중
- WebFlux를 처음에는 Reactor로 개발했으나 러닝커브가 높아 코루틴 도입
- Reactor를 사용하려면 reactor가 제공하는 operator를 활용해서 적절한 오퍼레이터 활용, 동작 확인해야하기 때문에 러닝커브가 높음
- 코루틴을 사용하면서 기존처럼 다이렉트 스타일로 개발이 가능해짐
- MSA 구조이다보니, 로그의 중앙집중화 필요
- logback, filebeat을 통해 어플리케이션 로그를 카프카로 보내고 Kibana에서 검색
- 컨테이너ID, 서비스ID : 어느 데이터센터에서 어떤 컨테이너가 로그를 남겼는지를 확인
- 이 정보로 특정 서비스의 문제인지 혹은 특정 장비의 문제인지를 확인
- 배포 ID : 어느 배포 버전에서 발생하는 로그인지 확인, 신규 배포의 로그에만 에러가 발생하면 바로 롤백
- Request ID : MSA 구조에서는 요청을 주는 쪽, 받는 쪽 어느쪽이 문제인지를 확인할 필요
- PinPoint ID : 해당 요청이 핀포인트의 어떤 요청과 매칭되는지 확인
- Hadoop : 5년이상 장기보관해야하는 데이터 Kafka → Hadoop
- 데이터센터 이중화
- 데이터센터급 장애가 발생하는 상황에서도 서비스 개발자가 확인할 수 있어야함
- ES : 각각 50대, 하루에 15TB
- 쿠버네티스와 잘 맞는 prometheus, HA와 데이터 장기보관을 위해서 thanos 사용 중
- 데이터 센터별 타노스에 쿼리하기 위해서 글로벌 타노스 따로 존재
- ceph를 내부저장소로 사용하고, grafana대시보드로 확인
- 저장소로 사용하는 mysql도 이중화
- sentry : 개별서비스에서는 sentry활용
- Toss ES Alert : 전체적으로 es 로그를 활용하는 프로젝트를 만들어 사용 → 슬랙 알림
- grafana : http error code / Istio error flag 발생 시 슬랙 알림
- flink : 서비스들의 응답을 파싱해서 응답시간이 튀거나 응댑 실패율이 높아질 때도 알림
이중화된 데이터센터 환경에서 카프카를 어떻게 안정적으로 운영할것인가에 대한 고민
- 카프카 자체적인 HA가 있으니, 하나의 클러스터로 두 개의 클러스터를 묶을까? VS 다른 클러스터로 구성해 Replication 적용
- 두 개의 클러스터로 구성하는 방식이 장애의 전파 없이 빠르게 복구 및 안정적인 운영 가능
- 그러나 이렇게 두개의 클러스터로 구성할때는 active/standby인 컨슈머가 다른 데이터 센터로 active하게 넘어갈 때
- 두 개의 카프카 컨슈머는 offset이 다르기 때문에 어느 시점부터 consume할지 모르게 됨
- 두 개의 카프카 offset을 싱크해주는 어플리케이션이 필요 → wakuwaku라는 어플리케이션 개발
- producer는 active/active로 운영해서 장애시 추가대응은 필요 없음
- consumer는 active/standby로 운영해서 평상시는 한쪽 데이터센터 kafka를 바라보다 장애시에는 반대편 데이터센터 kafka를 바라보도록 변경하여 장애대응
- 우버에서 사용하는 <토픽별 retry + dead letter queue> 구성을 본 떠 스프링 카프카를 래핑해 구현
- 같은 토픽에서 리트라이로 처리시간이 오래걸리면 뒤에 있는 메시지 처리가 오래걸리기 때문에 다른 토픽에서 리트라이를 하는 것이 성능적인 이점
- delay 처리 : 컨슈머가 아직 정상화가 되지 않았는데 retry하면 다시 에러가 발생하니 딜레이 처리가 중요
- topic이 나누어져야 delay처리도 자유로움
- 토픽별로 자동 delay requeue를 설정할 수 있고
- dead letter topic에서는 수동배치로 리큐를 하고 있음
- Kafka A에서 실패 → 약간의 딜레이 후 Kafka B로 보내서 retry → dlq로 이동 & 수동배치로 requeue
- 프로듀서는 서로 다른 클러스터 2개로 Retry를 구현하지만
- 완벽한 트랜잭션이 필요한 경우 : OutBox 패턴 활용해서 동일 트랜잭션처리 후 Retry
- Redis Cluster 사용 : 데이터 보존이 용이해 장기보관이 필요없는 단발성 데이터 저장 및 캐시로 활용
- 데이터 센터가 2개밖에 없어 한쪽 센터에 마스터가 많은 상황. 데이터 센터간 네트워크가 끊겼을 떄 split brain 발생 해 두 개다 모두 마스터가 될 가능성
- slot reblance : 운영 중 메모리 확장이 필요할때 활용
- redlock (AWS) : 분산락으 redlock 이용해 구현
- lettuce client : Redis클라이언트로는 Webflux, mvc에서 공통으로 활용할 수 있는 lettuce 활용