개발/ETC

Backend System Architecture 구성 경험담 정리

bitofsky 2021. 9. 2. 20:40

대규모, 특히 글로벌 서비스 규모나 그 규모로 확장이 예상될때 시스템 플랫폼에 어떤 장비나 오픈소스, 상용 서비스를 검토하면 좋은 선택이 될 수 있는가에 대한 선택과 사용 경험을 정리해 보려고 합니다.

주로 Kubernetes에서 운영되는 것과 AWS 서비스로 경험하였기 때문에 이것들 위주로 정리했습니다.

여기 나열된 것은 정답지가 아닙니다. 100가지 상황에는 100가지 해답이 있다고 생각합니다.


Container Orchestration

Kubernetes

대규모 서비스의 인프라는 현재 쿠버네티스 이외의 선택지가 존재하지 않습니다. AWS의 경우 EKS, Azure는 AKS, Google의 경우 GKE를 사용하면 됩니다.

 

온프레미스로 직접 구축하는 경우... 글로벌 서비스를 이걸로 할 생각 하는 거면 이미 전문화된 조직일 테니 알아서 선택 잘하면 됩니다.

Kubernetes = 쿠버네티스 = K8s

K8s는 컨테이너(docker 상위 개념)를 실행하고, 배포하고, 자동으로 관리해주는 시스템입니다.

컨테이너들이 어떻게 떠있어야 한다만 잘 정의해주면 그 정의대로 계속 유지하려는 기능을 가지고 있습니다.

 

예를 들어 컨테이너 이미지 A (ver 1.0)에서 vCPU 2 core와 1GB Ram으로 구성된 프로세스가 8080 포트를 열고 이걸 aaa.com에서 트래픽을 받으라고 짜두면 그걸 유지하기 위해 알아서 최선을 다해줍니다.

죽으면 알아서 살려주고 등등...

 

중요한건, 서비스 manifest 정의만 올바르게하면 정의대로 동작시켜주고 유지해준다는 점입니다.

물론 코드가 쓰레기면 쓰레기 프로그램이 돌아가듯 정의가 쓰레기면 서비스도 쓰레기가 되는건 매한가지니 공부가 많이 필요합니다.


Infrastructure as Code

Terraform

별생각 없으면 그냥 선택하면 됩니다. 현시점 1 Top

ArgoCD

Kubernetes 내부의 배포 자동화하는 솔루션입니다. Git과 연동되어 형상을 관리합니다. 많이들 쓰는데 비슷한 대안도 몇 개 있지만 아르고가 현시점에선 제일 많이 쓰입니다.

Pulumi

저희는 AWS 리소스와 EKS 내부의 형상관리를 Pulumi로 일원화하기 위해 플루미를 사용했습니다.

 

Terraform과 달리 프로그래밍 언어로 IaC를 짤 수 있기 때문에 모듈화를 잘할 수 있다면 재사용 빈도가 높은 시스템이나 여러 Region, 여러 K8s Cluster들을 비교적 손쉽게 관리할 수 있습니다.

 

Pulumi를 사용하면 예를 들어 AWS S3에 버킷과 EKS를 생성하게 하고 이 EKS에 Deployment 생성하면서 환경변수에 S3 버킷 경로를 주입해 pod를 띄우는 코드가 모두 한 곳에 공존하고 상호 의존성을 갖게 할 수 있습니다.

 

예를들어 S3 버킷 명이 변경되면 이걸 사용하는 pod의 환경 변수 주입 값도 바뀌고 버킷이 삭제되면 적용 전에 오류가 생기게 할 수도 있습니다.

 

이를 통해 적용하기 전 Preview 단계에서 문제를 확인해 잘못된 인프라가 배포되는 상황을 막을 수 있게 됩니다.


RDB

Oracle, MSSQL

비용과 상관없이 금융권 수준의 미션 크리티컬 한 DB가 필요할 때.

Mysql, Maria, Postgress

작은 트래픽이라 클러스터링이 지금도, 앞으로도 절대 필요 없다고 생각될 때 선택.

운영 툴용 DB라던지... 조그마한 사내 시스템이라던지...

장비도 K8s에 Statefulset으로 띄우거나 K8s Operator 하나 설치해서 대충 띄워서 막 굴려도 안 죽고 잘 돕니다.

AWS AuroraDB

합리적인 대안이라고 생각합니다.

오픈소스 DB를 직접 운용하려면 서비스 이후 장애 대응이나 트러블슈팅을 할 수 있는 리소스와 인력이 필요한데 그런 부분을 저렴한 비용으로 대체 가능하게 됩니다.


NoSQL

MongoDB

DB가 필요한데 라이브 서비스 등에 연동되어 수평 확장이 예상될 때 선택.

첫 시작은 1기를 K8s에 Statefulset으로 띄우거나 Percona MongoDB Operator를 설치한 다음 CRD로 Replicaset이나 Sharding을 구성해 확장하면서 사용하면 좋습니다.

 

https://www.percona.com/doc/kubernetes-operator-for-psmongodb/index.html

 

Percona Distribution for MongoDB Operator

Percona Distribution for MongoDB Operator

www.percona.com

 

MongoDB 공식 Operator의 경우 Enterprise 버전이 아닌 커뮤니티 버전은 기능이 쓰레기(샤딩도 지원 안 함)니 괜히 써서 내상 입지 말고 그냥 Percona 쓰는 게 낫습니다.

(Percona 버전의 경우도 오퍼레이터를 쓰레기 같이 해둔 구석이 몇 군데 있는데 일례로 Secret을 변경해 클러스터를 띄우면 Secret의 value만 MongoDB Config collection으로 복사한 뒤 업데이트하지 않는다거나... 이럴 거면 Secret을 왜 쓰나)

AWS DocumentDB

AWS에서 MongoDB의 Driver만 호환성을 유사하게 한 호환 DB입니다.

이건 몽고 DB가 아닙니다. 지원하는 명령체계도 다르고 인덱스 동작 방식도 다릅니다. 그냥 드라이버 통신 인터페이스만 몽고에 맞춘 전혀 다른 물건이기 때문에 아주 단순한 CRUD 동작만 사용하는 데는 문제가 없지만 좀 복잡한 쿼리, 컴파운드 인덱스, TTL 인덱스 등을 사용하면 MongoDB와 완전히 다르기 때문에 생고생할 수 있습니다.

 

장점은 스토리지 제한이 딱히 없이 확장되는 것. 단점은 인스턴스 비용이 비싸고 데이터 액세스에 별도 비용이 청구됩니다. 심지어 TTL 인덱스로 레코드를 자동 삭제할 때조차 CPU 연산과 비용이 추가되니 높은 I/O가 발생하는 DB로는 적합하지 않습니다.

 

독특한 호환 명령어 셋 때문에 로컬 개발환경에서부터 테스트가 필요한데 Public 접근을 지원하지 않기 때문에 VPC 안으로 터널링 등을 사용해 개발망을 구성해야 하는 것도 귀찮은 부분입니다. (올해안에 Public Endpoint가 나온다는 소문은 있습니다)

차라리 Atlas 쓰는 게 이득일 듯...

ETCD

많이 알려지지는 않았지만 Kubernetes의 상태 저장용 DB로도 사용됩니다. ETCD 서버를 설치해 Kubernetes와 같이 특수 목적용 DB로 사용할 수 있습니다.

 

ETCD는 Redis와 비슷한 Key-Value 스토리지이지만 가장 큰 차이점은 단일 장애점 없는 분산 합의를 기반으로 한 초-강력한 일관성을 유지하는데 목적이 있습니다.

저장 같은 Get/Set 기능은 Redis와 유사합니다.

 

그럼 Redis 쓰지 왜 굳이 다른 장비와 Driver를 써서 꼭 ETCD를 따로 운용하느냐?

ETCD에 저장된 데이터는 저장이 허용되었다면 그게 최신이고 값을 읽어간 시스템은 현재 읽은 값이 올바른 값이라는 보장이 됩니다.

 

ETCD는 Watch 기능이 있는데 이 기능이 특별함을 제공합니다. Watch로 전달되어오는 값은 항상 최신의 올바른 값이라는 것을 ETCD가 보장합니다.

 

예를 들어 500대로 수평 확장된 클러스터 서버들이 있다고 가정합니다. 이 서버들의 일부 설정은 환경변수로 주어지겠지만 라이브에서 실시간으로 변경되어야 하는 복잡한 Config가 있다면 이걸 ETCD를 Watch 하게 해 두면 코드가 간결하고 심플하면서 안전합니다.

 

비슷하게 Redis Pub-Sub 등으로 Config 변경을 Subscribe 해두고 변경자가 Publish 하게 하여 구현할 수도 있습니다. 이경우 동시간에 Publish 하거나, Redis가 다중화되어있는 등의 이유로 전파 타이밍에 의해 Race Condition에 의한 서버 간 Config의 차이가 발생할 가능성이 생깁니다. 대형 시스템 구성에서 확률이 zero인 것과 낮지만 zero가 아닌 것은 크게 다르므로 ETCD를 사용하면 이런 골치 아픈 문제로부터 벗어나게 해 줍니다.

 

Lock이나 Leader를 선정하거나, 워커 등을 등록하거나, 특정 리소스의 사용권을 임대하는 형태의 시스템을 구성하는데 ETCD를 사용하는 것도 좋은 선택입니다. 변경 감지를 활용해야 하는 곳에서 좋습니다.

 

다만 특성상 Write가 매우 빈번한 경우(초당 수천에서 만단위)는 많은 주의가 필요하니 무작정 Redis 대용으로 쓰면 안 됩니다.


Cache / Lock

Redis

쉽고 빠르고 안 죽고 잘 복구됩니다.

기본적인 Get/Set을 사용한 캐시나 단순 저장 기능 외에 Sorted Set 등을 사용한 랭킹 시스템 등을 구성하는 데 사용하기도 합니다. Set과 Expire를 같이 사용해 Lock을 구현하거나 아래에서 다룰 RedLock을 구성할 수도 있습니다.

 

클러스터를 사용하는 경우 성능에 일정 비율 손실이 있으며, 사용할 수 없는 기능 제약사항들이 많이 생기니 이 경우 특성을 잘 알아보고 도입해야 합니다. 클러스터는 Redis 고유의 Hash Slot을 통해 키를 여러 노드로 분산합니다.

 

아니면 Application Level에서 Consistent Hashing 등으로 분산시키면 클러스터의 성능 손실 없이 꽤 잘 분배시킬 수 있습니다.

Redis Distribution Lock

레디스 분산 락. 줄여 RedLock은 복수의 Redis를 사용한 분산 합의 알고리즘으로 Lock을 구성합니다.

프로세스 단위의 애플리케이션에서는 Mutex를 쓰겠지만 여러 서버들로 구성된 시스템들이 동일한 리소스에 경합을 하면 안 되는 상황에서 RedLock을 사용할 수 있습니다.

 

예를 들어 Redis 3대로 구성된 RedLock 클러스터를 구성하면 잠금을 획득하기 위해 n/2+1만큼. 즉 2대의 Redis에 Lock을 먼저 획득 한자가 소유권을 얻게 됩니다. 소유권은 Key의 TTL에 의해 만료시간 이후 자동으로 Quorum이 소멸하며 풀리거나 소유자가 스스로 Unlock(Key DEL)해 풀 수 있습니다.


RPC

gRPC

이기종, 여러 언어 간 데이터를 교환할 때 gRPC와 Protobuf가 있다면 상호 연동과 적용이 쉬운 편이고 빠릅니다.

 

단점은 proto gdl 이 변경될 경우 메시지가 망가질 위험이 있지만 프로퍼티가 추가되는 것에는 문제가 없기 때문에 대부분 큰 문제는 발생하지 않는 편이며 JSON과 같이 아예 정의가 없는 것보다는 불편하지만 정의가 있음으로 인해 편해지는 부분도 존재하니 이 부분은 장단이 반반입니다.

Message Queue

Kafka / AWS MSK

대량의 스트리밍형 메시지 혹은 로그 전송과 수신 처리가 필요할 때 사용합니다.

 

대량이 필요 없다면 구축에 품이 많이 드는 Kafka보다는 SQS나 기타 MQTT 호환(RabbitMQ, AMQP 등) 메세지큐를 쓰는 게 더 편리합니다.

 

또한 각 메시지 별이 아닌 offset과 같은 cursor 기반 처리밖에 할 수 없으므로 순차처리가 곤란한 시스템에 채택하지 말아야 합니다.

AWS Kinesis

Kafka를 대체 가능한 AWS 서비스입니다. 대부분의 특성과 제약이 kafka와 비슷합니다. 대신 파티션 개념이 없고 데이터 샤드로 트래픽 증감에 대응합니다. 샤드당 비용입니다.

 

장점은 데이터 생산 후 1일간은 데이터 소비를 위해 전송하는 전송 비용이 무료 인 점이므로 단일 생산 복수 소비 패턴에서 kafka보다 유리합니다. 또한 kafka와 달리 인스턴스 구성이 필요 없고 키네시스 스트림을 바로 다른 데이터 관련 AWS 서비스에 연결할 경우 그 비용 역시 절약이 가능합니다.

 

단점은 AWS Kinesis Client Library가 JAVA만 제공된다는 점이며, JAVA 이외의 언어는 JAVA 데몬에 타 언어를 fork로 실행 후 stdio로 송수신하는 비 할 데 없이 쓰레기 같은 기능으로 돌아가는 Multilang Client가 최대 단점입니다.

이를 해결하기 위해 많은 개발자들이 머리를 쥐어뜯고 있으며 최근 VMWare에서 vmware-go-kcl client를 공개해 golang을 사용하는 경우 쓸만합니다. 여기에 연동해 쓸 수 있는 PUBG의 Redis Checkpoint도 있습니다.

 

최근 저희는 일평균 2 ~ 3억 개의 메시지를 Kinesis로 수신 중인 스트림에 AWS KCL JAVA를 제거하고 모두 vmware-go-kcl과 redis checkpoint로 전환했는데 한달 이상 문제 없이 동작하고 있습니다.

 

평균 CPU 사용률은 3배, Memory는 100배가량 사용량이 감소했습니다.

AWS SQS

싸고, 쉽고, 빠르고, 좀 불편하고, 좀 멍청하고, 대부분 잘 동작하는 메세지큐


Proxy / Traffic Management

NginX

K8s Cluster에 ingress를 사용한다면 nginx ingress가 가장 쉽고 편리한 축에 속합니다. 사내 서비스 구축 시 OAuth로 SSO Proxy를 사용하면 손쉽게 내부 서비스에 SSO를 연동시킬 수도 있습니다.

 

nginx의 configure 대부분이 annotation으로 설정 가능하게 되어있어 다양한 니즈를 소화할 수 있는 것도 장점입니다.

Envoy

gRPC를 라우팅 하는 경우 1 픽입니다. K8s를 쓰는 기업에서 많이 사용하며 기능이 계속 추가되고 있습니다.

 

deprecated 되는 config도 많아서 버전 업데이트하는 게 짜증 나는 건 단점입니다.

Istio

K8s 안에서 네트워크 트래픽을 ISTIO CRD (Custom Resource Definition)로 정의하고 관리하는 매니지먼트 레이어입니다. ISTIO를 클러스터에 설치하면 모든 pod에 envoy proxy가 sidecar로 설치되며 ISTIO에 의해 관리됩니다.

 

모든 in-out 트래픽이 이 sidecar를 통하게 되어 보안, 접근제어, 라우팅, 로드밸런싱 등을 설정에 따라 자동화하게 됩니다. 이로 인해 n% 단위의 성능 저하 및 메모리 사용량 증가가 발생하는 건 단점입니다.

 

대규모 서비스라면 당연히 필요한 항목이지만 중-소규모까지는 구성과 성능 오버헤드에 의한 단점이 더 부각될 수 있으니 초기부터 도입하는 건 비추천합니다. 넣는 건 언제라도 넣으니 나중에 필요해지면 넣으면 됩니다.

Log / Metrics

ELK

ElasticSearch - Logstash - Kibana

Elastic.co의 분산 로그 수집 및 처리 분석 시스템
오픈소스 버전도 꽤 강력한 기능을 제공하며 특히 7.10부터 익명 로그인과 SSO 등의 기능이 유료에서 무료로 포함되면서 상당히 좋아졌습니다.

특히 쓰레기같은 Mongodb Kubernetes Operator - Community 버전과 달리 ECK (Elastic Cloud on Kubernetes) Operator는 오퍼레이터에 기능제약이 없습니다. 다만 설치되는 ElasticSearch와 Kibana에 유료 기능을 쓸 수 없는 것 뿐입니다.

 

단점은 여전히 무료 기능에는 상대적으로 빈약한 기능 위주라는 건데, 일반적인 용도의 로그 분석용으로는 쓸만합니다만 핵심 기능은 유료 버전이고 비쌉니다.

 

(Self-Hosting 가격정책은 좀 싸게 해 주면 안 되나? Self-Hosting 조차 가격이 너무 비쌉니다.)

Prometheus

Kubernetes의 기본 모니터링 시스템이자 Time Series DB입니다. 현시점 대체 불가 필수입니다.

 

K8s에 서비스를 올릴 때 Sidecar로 Prometheus metrics 형태를 지원하는 exporter를 배치해 서비스의 상태를 스크랩해 가도록 구성합니다.

 

애플리케이션에도 Custom Metrics를 추가해 내가 원하는 지표를 마음대로 추가해 관찰시킬 수 있고 Grafana와 연계해 라이브 서비스용 Dashboard, Alert 등을 구성할 수 있습니다.

Grafana

Kubernetes의 기본 모니터링 시스템이자 Observability 솔루션입니다. 쉽게 말해 그래프 보여주는 툴입니다.
역시 현시점 대체제가 없습니다.

 

Prometheus 뿐 아니라 ElasticSearch의 데이터나 AWS CloudWatch의 데이터도 가져와 그래프와 Alert을 구성할 수 있습니다.

AWS CloudWatch

AWS 서비스를 사용한다면 어쩔 수 없이 써야 합니다.


Load Balancer

ELB - Classic

EKS에서 기본 LoadBalancer입니다. 그냥 무난하고 급격한 트래픽 증가가 예상되면 미리 웜업을 해둬야 하는 단점이 있으나 크게 신경 안 써도 되는 게 장점입니다.

ELB - Application

EKS에서 쓰려면 LoadBalancer로는 못쓰고 ALB Ingress Controller로만 사용이 가능합니다.

사실 크게 상관없는 게, 기본 동작은 Classic과 별 차이 없는 L7이기 때문에...

 

Ingress를 추가할때마다 ALB가 추가되는 비용에 답없는 방식이라 쓰고싶지 않습니다.

이걸 쓸바엔 nginx ingress가 더 나은 것 같습니다.

ELB - Network

L4 계층이며 초기 이니셜 라이즈가 다소 오래 걸리는 것 빼고는 딱히 단점은 별로 없습니다.

한번 설치되면 Classic과 달리 급격히 트래픽이 증가하더라도 웜업 없이 모든 트래픽을 다 소화할 수 있습니다.

 

ELB를 LoadBalancer로 사용할 때 주의점은 HTTP가 아닌 TCP 통신에서 원본 소스 IP를 획득하고 싶은 경우 뒷단 Pod까지 원본 IP를 어떻게 보존할 것인가가 고민될 수 있습니다.

 

간단한 두 가지 방법을 설명해보면 다음과 같지만 실제 적용을 위해서는 공식 매뉴얼을 참고하기 바랍니다.

  1. LoadBalancer의 service.spec.externalTrafficPolicy를 local로 변경
    • K8s 노드에 직접 소스 트래픽을 받을 수 있는 방법으로 가장 간단한 해결책입니다.
    • K8s 노드에 해당 서비스에 연결된 pod의 숫자가 불균등한 경우 트래픽 역시 불균등하게 분배됨. 예를 들어 10개의 pod가 모인 노드는 5개가 모인 노드에 비해 각 pod가 수신하는 트래픽은 1/2만큼 적어짐. 이는 LB에서 각 노드별로 균등하게 트래픽을 분산되고 노드 안에서 다시 pod로 균등하게 분산되기 때문입니다.
    • LB에 Target으로 묶인 노드에 트래픽이 수신받는 pod가 0이 될 때 1/전체 노드수만큼의 트래픽이 실패하게 됨. 이는 service.spec.healthCheckNodePort에 설정된 노드 포트로 LB가 노드의 활성여부 체크를 일정 주기를 가지고 체크하게 되는데 pod가 먼저 빠지고 노드가 빠지는 게 느려서 이런 문제가 발생합니다. 다음 체크 주기까지 해당 수신할 pod가 없는 노드로 트래픽이 들어가는 것입니다.
    • 이런저런 문제가 귀찮게 발생하기 때문에 단순히 Source ip를 획득하기 위한 목적이라면 이 방법은 비추천합니다.
  2. LoadBalancer의 Proxy Protocol Annotation 추가 후 proxy protocol 인식
    • service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
    • 위와 같은 annotation을 추가해 ELB에 proxy protocol을 활성화시키면 클라이언트 연결 후 첫 패킷에 소스 IP가 protocol에 맞게 들어오는데 이를 인지하도록 중간의 nginx 나 app connection 수신부를 수정해주면 됨. 이 경우 protocol이 안 맞으면 문제가 생길 수 있기 때문에 다소 귀찮은 방법이 됩니다.
    • externalTrafficPolicy: local과 같은 트래픽 불균형은 kube-proxy로 인해 원천적으로 발생하지 않습니다.