카카오뱅크는 온프레미스 환경에서 운영 작업의 자동화를 추진해오며, 멀티데이터센터 환경에서 안정적인 서비스 확장을 위한 ‘서비스 디스커버리 시스템’을 구축했는데요. 이를 통해 서비스 간 의존성과 트래픽 증가 문제를 어떻게 현명하게 해결했는지 소개드리고자 합니다. 카카오뱅크의 인프라 운영과 확장 전략에 관심 있는 분들께 강력히 추천드립니다!

안녕하세요. 카카오뱅크의 서비스 레지스트리 운영 및 연관 시스템을 개발하는 Donggeuri입니다. 저희 서비스아키팀은 카카오뱅크 서비스의 안정적 운영을 최우선 목표로 삼고, 인프라 운영의 신뢰성과 효율성을 극대화하기 위해 노력하고 있습니다.

카카오뱅크 역시 초기에는 온프레미스 VM(Virtual Machine)과 BM(Bare-Metal) 환경에서 모놀리식(Monolithic) 애플리케이션을 배포했습니다. 이러한 환경은 서비스나 인프라 변경 시 긴 절차로 인해 서비스 확장의 속도가 제한되는 단점이 있습니다. 이를 극복하기 위해 Ansible 기반의 자동화를 도입했고, 현재는 Terraform과 Kubernetes(이하 K8s)를 활용한 근본적인 해결책을 마련하고 있습니다.

이 글에서는 유연하고 연속적인 서비스 확장을 위한 시도로 저희가 구축한 서비스 디스커버리 시스템에 대해 소개드리려 합니다. 이 글이 서비스 디스커버리를 도입했거나 고민 중인 분들께 유익한 정보가 되기를 바라면서 시작하겠습니다.

서비스 디스커버리란?

서비스 디스커버리(Service Discovery)를 간단히 설명하면 ‘서비스 이름으로 서비스의 위치(IP주소와 포트)를 동적으로 찾는 기능’입니다. 뛰어난 확장성을 가진 환경에서는 단기간에 서비스가 추가되고 삭제되기 때문에, 서비스 디스커버리의 역할은 사실상 필수적입니다. 특히 Scale In/Out을 통해 서비스 주소가 동적으로 변경될 수 있는 MSA 환경에서는 거의 항상 같이 언급됩니다.

서비스 디스커버리의 기본적인 흐름은 [그림 1]의 도식과 같이 진행됩니다. 각 서비스는 서비스 레지스트리(Service Registry)에 자신의 주소를 등록하고, 필요한 서비스의 주소를 서비스 레지스트리에서 조회하여 호출합니다.

1-service-discovery-image
[그림 1] 서비스 디스커버리의 기본 흐름

서비스 디스커버리는 ‘디스커버리 주체’에 따라 2가지 유형 즉, Client-side Discovery와 Server-Side Discovery로 구분됩니다.

Client-Side Discovery

Client-Side Discovery는 개별 서비스(Service Consumer)에서 디스커버리 및 호출 책임을 가집니다. 해당 패턴은 클라이언트(Client) 코드상에 디스커버리 기능을 구현하거나, 클라이언트와 함께 실행되는 프록시(Proxy)를 통해 구성되기도 합니다. 이처럼 중앙 컴포넌트를 사용하지 않기 때문에 부하 집중으로 인한 장애 발생 가능성이 적다는 장점이 있습니다.

2-service-discovery-client-image
[그림 2] Client-Side Discovery 구성도

Server-Side Discovery

반면 Server-Side Discovery는 게이트웨이(Gateway) 같은 중앙 컴포넌트에서 디스커버리 및 호출 책임을 가집니다. 이 패턴은 서비스에 공통적인 제어를 적용하는 작업이 쉽고, 개별 서비스에서 직접 디스커버리 로직을 구현하지 않아도 된다는 장점이 있습니다.

3-service-discovery-server-image
[그림 3] Server-Side Discovery 구성도

두 패턴 모두 각각의 고유한 장단점이 있기 때문에, 서비스 디스커버리 적용 시 주어진 환경에 적절한 패턴을 선택하여 적용하면 됩니다.

서비스 디스커버리 솔루션 비교

서비스 디스커버리 도입을 준비하면서 먼저 오픈소스 솔루션들을 살펴 보았습니다. 대표적으로 Netflix Eureka, Apache Zookeeper, Hashicorp Consul 등이 있었습니다. 각 솔루션의 아키텍처 특성 / 장점 / 단점 / 카카오뱅크 환경에서의 적합성 등을 비교해본 결과는 다음과 같습니다.

  • ☑️ Eureka
    • • 진입장벽이 낮아 Spring Cloud 애플리케이션에 연동하기 쉽습니다.
    • • HA(High Availability) 구성을 제공하지만, 네트워크 단절 시 Split-brain 문제와 Anti-Entropy 메커니즘에 대한 고려가 필요합니다.
    • • 대규모 시스템 운영에 필요한 부가 기능들이 부족합니다.
  • ☑️ Zookeeper
    • • 역시 HA 구성을 제공하며, Leader-Follower 아키텍처와 Broadcast 방식의 동기화 메커니즘을 사용해 Split-brain 문제를 완화하고 일관성을 개선합니다.
    • • Spring Cloud Zookeeper로 애플리케이션을 연동할 수 있지만, 단일 노드에 Blue/Green 인스턴스를 배포하는 사내 환경에서는 사용이 어렵습니다.
  • ☑️ Consul
    • • HA 구성을 제공하며, Leader-Follower 아키텍처와 Gossip Protocol을 사용해 높은 수준의 일관성을 보장합니다.
    • • 멀티 데이터센터 등 대규모 클러스터 운영 환경에서 유용한 기능들을 다수 보유하고 있습니다.
    • • Spring Cloud Consul로 애플리케이션 연동이 가능하며, 사내 Blue/Green 서비스 구성 방식에 적합합니다.

결론적으로 저희는 Consul을 선택하여 서비스 디스커버리를 구축했습니다.

Netflix Eureka Apache Zookeeper Hashicorp Consul
HA 구성 Peering (단순 Replica) Leader-Follower Leader-Follower
가용성 + + ++
결함에 대한 내성 + ++ ++
동기화 메커니즘 - Broadcast (Zookeeper Atomic Broadcast) Peer-to-Peer (Gossip Protocol)
멀티 데이터센터 지원 X X O
ACL 지원 X O O
부가 기능 - + ++
애플리케이션 연동 편의성 + - +

우리의 선택 👉 Hashicorp Consul

그럼 본격적으로 Consul에 대해 조금 더 설명드리겠습니다. Terraform으로 유명한 Hashicorp 사에서는 Consul을 ‘온프레미스 및 멀티 클라우드 환경에서 네트워크 연결 관리를 지원하는 서비스 네트워킹 솔루션’ 이라고 소개합니다. 창업자의 말에 따르면, 처음에는 서비스 디스커버리 문제를 해결하기 위해 시작했다가 각종 네트워킹 문제를 하나의 제품에서 통합적으로 해결하는 것으로 방향을 바꾸어 지금의 다양한 기능을 갖추게 되었다고 합니다.

우선 Consul의 핵심 구성 요소를 살펴보고, 개별 구성 요소들이 어떻게 상호작용하며 안정적인 디스커버리 기능을 지원하는지 알아보겠습니다.

4-consul-component-image
[그림 4] Consul 구성 요소

위의 [그림 4]에서 보여드린 Consul은 ‘데이터센터(Datacenter)’ 라는 이름의 ‘고수준의 논리적인 네트워킹 단위’를 제공하고, 하나의 Consul 데이터센터 내에서 대다수의 데이터 통신이 이루어집니다. 따라서 방화벽으로 구분된 물리적 데이터센터에 하나의 Consul 데이터센터가 구성되는 것이 일반적입니다.

데이터센터 내에서는 각 호스트(VM/BM 서버)마다 설치된 Consul Agent들이 클러스터를 형성합니다. 각 호스트에 설치되어 있는 Agent는 Consul의 모든 기능을 담고 있는 실행파일로, 설정에 따라 Server 모드Client 모드로 구성됩니다. Client는 호스트에서 실행되는 서비스 정보를 Server로 RPC(Remote Procedure Call) 통신을 통해 등록하는 역할을 하고, Server는 서비스 레지스트리인 카탈로그(Catalog)를 관리합니다.

그렇다면, Consul은 왜 이렇게 설계되었을까요? 만약 여러분이 서비스 디스커버리 솔루션을 개발한다면 어디부터 설계를 시작할까요? 여러 가지 설계 방식과 접근법이 있겠지만, 저라면 아마 ‘분산 환경’을 가정하고 설계를 시작할 것 같습니다. 이러한 분산 시스템 아키텍처를 위해 고려해야 할 점들을 좀 더 파고들어 보겠습니다.

분산시스템 아키텍처와 CAP 이론

분산시스템 아키텍처에서 일반적으로 언급되는 대표적인 특성 3가지가 있습니다. MSA 공부를 해보신 분이라면 CAP 이론을 한번쯤 들어보셨을 겁니다. CAP는 일관성(Consistency), 가용성(Availability), 네트워크 분단 내성(Partition Tolerance)의 약자로, 분산 시스템에서는 이 3가지 요소들 중 최대 2가지만 보장할 수 있다는 이론입니다. 하나를 챙기면 다른 하나는 어느정도 포기해야한다는 Trade-off의 개념이라고 볼 수 있습니다.

예를 들어 많은 트랜잭션을 처리하는 즉, 가용성을 높이기 위해 노드의 개수를 늘리면 이 노드들 간의 정보 동기화 문제가 발생합니다. 정보 동기화가 충분히 고려되지 않은 시스템에서는 인과성이 역전되거나, 쓰고(Write) 나서 바로 읽은(Read) 값이 쓴 값과 일치하지 않는 현상 등이 발생할 수 있습니다.

5-cap-for-distributed-system-image
[그림 5] 분산시스템 아키텍처 설계에서 반드시 고려해야하는 CAP 이론

이러한 특징을 반영하여 Consul Server에서는 강한 일관성(Strict Consistency)과 네트워크 분단 내성을 갖는 CP(Consistency-Partition Tolerance) 시스템을 사용합니다. 반면, Consul Client에서는 가용성을 높이고 최종적 일관성(Eventual Consistency)으로 타협하는 AP(Availability-Partition Tolerance) 시스템을 사용합니다.

Consul 아키텍처

계속해서 Consul을 직접 만든다는 가정하에, 앞서 [그림 4]에서 보여드린 Consul의 구성 요소였던 Server 모드와 Client 모드의 아키텍처를 살펴보려고 합니다. 각 아키텍처 별로 클러스터의 정보가 어떻게 관리되고, 정보 동기화를 위해 어떤 프로토콜(Protocol) 방식을 사용하며, Leader 선출을 위해 어떤 알고리즘을 적용했는지 이어서 설명드리겠습니다.

Consul Server 아키텍처와 Raft 알고리즘

Consul Server에서는 소수의 Agent가 핵심 정보를 관리합니다. 이 정보는 강한 일관성이 필요하고, 네트워크가 불안정해도 읽기와 쓰기가 가능해야 합니다. 이를 위해 Consul Server는 단일 Leader 아키텍처를 사용하며, Raft 알고리즘을 통해 Leader를 선출하고 Leader 노드가 정보를 Follower 노드로 복제합니다. Raft 알고리즘은 모든 Server 노드가 Follower 상태에서 시작하여, 먼저 타임아웃에 도달한 노드가 Candidate로 승격됩니다. 다른 노드들이 투표해 정족수를 넘기면 Leader로 승격되며, 실패 시 과정을 반복합니다. Leader 노드에 장애가 발생하면 Raft 알고리즘을 통해 새로운 Leader를 선출합니다.

참고로 Paxos와 Zookeeper Atomic Broadcast 같은 합의 알고리즘도 있지만, Consul은 간단한 Raft 알고리즘을 선택했습니다. Raft 알고리즘은 K8s의 Etcd와 일부 Kafka 버전에서도 사용되고 있습니다.

6-consul-raft-image
[그림 6] Consul에 적용된 Raft 알고리즘의 동작 모습

Consul Client 아키텍처와 Gossip Protocol

그럼 Consul Client는 어떨까요? Consul 클러스터 내 대부분의 노드가 Consul Client입니다. Client 노드가 많아 CP 시스템으로는 성능에 부담이 크기 때문에 Consul은 가용성을 높인 AP 시스템을 채택하고 Gossip Protocol을 사용합니다. Gossip Protocol은 P2P(Peer to Peer) 방식으로 동작하여 각 노드가 임의의 노드와 통신함으로써 빠르게 정보를 전파합니다. 이로 인해 노드 수가 증가해도 정보 수렴 속도가 거의 유지됩니다. 이러한 특성으로 Consul은 Gossip Protocol을 이용해 노드 장애를 빠르게 감지할 수 있습니다.

참고로 Consul Client 뿐만 아니라 Consul Server Agent도 Gossip Protocol에 참여합니다. Consul에서 이 프로토콜은 UDP/TCP 메시지 규격을 통해 통신하며, Go 언어로 작성된 Memberlist 라이브러리로 구현되어 있습니다. 실제 구현은 좀 더 복잡하지만, 핵심적인 Gossip Protocol의 핵심적인 흐름은 아래와 같습니다.

1. 한 노드는 설정된 개수의 특정 노드를 선정하여 UDP ping 메시지를 전송하고, ack 메시지를 받으면 타겟 노드를 Alive 상태로 간주한다.
2. 일정 시간 동안 UDP ping의 ack 응답을 받지 못하면 타임아웃(Timeout)이 발생하며, 다른 노드에게 타겟 노드에 메시지를 대신 보내달라는 Indirect ping 메시지를 전송한다.
3. Indirect ping에서도 타임아웃이 발생하면, TCP ping 메시지를 타겟 노드로 전송하여 ack 메시지를 받으면 타겟 노드를 Alive로 판단한다.
4. TCP ping에서도 타임아웃이 발생하면 타겟 노드가 죽은 것으로 간주하고 Suspect 메시지를 다른 노드들에게 전송한다.
5. Suspect 노드가 Alive라는 메시지를 받지 못하면, 타겟 노드는 Dead 노드로 전환되고 일정 시간이 지난 후 Gossip Pool에서 제외된다.

멀티데이터센터(Multi DC) 환경을 지원하는 WAN Federation

Consul은 독립적인 데이터센터들을 연동하는 WAN(Wide Area Network) Federation 기능을 지원합니다. 각 데이터센터는 자체 서비스와 노드 정보를 관리하지만, 다른 데이터센터의 서비스 정보를 조회하려면 Consul Server 간의 통신을 통해서만 가능합니다. 데이터센터 간 통신은 Consul Server를 통해 이루어지며, WAN Federation을 사용할 때 Server Agent를 Gossip Pool로 구성하여 Gossip 통신을 합니다.

다른 데이터센터의 서비스를 조회할 때는 항상 Server Agent를 통해 이루어져 데이터센터 간 트래픽은 오직 Server Agent 간의 트래픽으로 제한됩니다. 이를 위해 저희는 데이터센터를 방화벽으로 격리된 존으로 구성하고, Server Agent 간의 방화벽만 최소한으로 오픈하고 있습니다.

7-consul-wan-federeration-image
[그림 7] Consul의 WAN Federation 기능

서비스 디스커버리 도입 Timeline

2020년 카카오뱅크 채널계에 Consul을 첫 도입한 이후, 저희는 서비스 디스커버리 적용 범위를 점진적으로 확장해왔습니다. 카카오뱅크는 고객의 소중한 개인정보를 지키기 위해 여러 네트워크 존으로 격리되어 있습니다. 이에 따라 Consul 데이터센터를 구성할 때, 네트워크 존별로 하나의 Consul 데이터센터를 설치했습니다. 또한, 각 데이터센터의 Server Agent 간에만 방화벽을 최소로 오픈하여 Consul 데이터센터 간에 WAN Federation을 구성했습니다. 이를 통해 안전한 네트워크 환경에서 효과적으로 서비스 디스커버리를 구현할 수 있었습니다.

그럼 이어서, 어떤 단계를 거쳐 서비스 디스커버리를 도입했는지 단계별로 설명드리겠습니다.

Step1. API Gateway에 동적 트래픽 제어 적용

서비스 디스커버리를 처음 도입한 목적은 대고객 트래픽을 받는 카카오뱅크의 API Gateway에서 동적 트래픽 제어를 구현하기 위해서였습니다. 당시 API Gateway에는 트래픽이 일시에 몰리거나 지연이 발생할 경우에 대비한 적절한 장치가 필요했습니다. Server는 동시에 처리할 수 있는 커넥션 수에 한계가 있어, 이를 초과하는 요청은 정상적으로 처리될 수 없기 때문입니다.

8-gateway-conn-connection-problem-image
[그림 8] 동시 커넥션 문제를 겪고 있는 API Gateway

그리하여 저희는 API Gateway 뒷단의 Server 최대 동시 커넥션 수 총합을 넘지 않도록 Bulkhead를 API Gateway에 도입했습니다. Bulkhead는 선체 일부가 파손되더라도 전체가 침몰되지 않게 막아주는 ‘격벽’으로, 일종의 ‘장애 전파 격리 장치’라고 볼 수 있습니다. Server들을 구획으로 나누어 각각 Bulkhead를 구성하면 특정 Server에 장애가 발생해도 해당 Server의 서비스 이외의 다른 서비스는 영향을 받지 않습니다. 저희는 API Gateway에 Resilience4j를 서킷 브레이커(Circuit Breaker)로 활용하여 뒷단 Server가 동시 처리할 수 있는 커넥션 수를 넘지 않도록 요청을 제한했습니다.

9-gateway-conn-connection-bulkhead-image
[그림 9] Bulkhead 적용으로 동적 트래픽 제어가 가능한 API Gateway

# 게이트웨이 bulkhead 내부 구현을 위한 설정
gateway:
  bulkhead:
    defaultMaxConcurrentCallsPerInstance: 500 # 뒷단 서버의 최대 동시 커넥션수
    instances:
      serviceA:
        serviceName: A
        serviceWeight: 1.0
        ...
      serviceB:
        serviceName: B
        serviceWeight: 1.0
        ...
      serviceC:
        ...
        serviceName: C
        serviceWeight: 0.5
        ...

# resilience4j bulkhead 설정
resilience4j:
  bulkhead:
    configs:
      default-config:
        # 주의: maxWaitDuration 설정(0 이상) 시 thread blocking 발생
        maxWaitDuration: 0
        maxConcurrentCalls: 500
        writableStackTraceEnabled: true
    instances:
      serviceA:
        baseConfig: default-config
      serviceB:
        baseConfig: default-config
      serviceC:
        baseConfig: default-config

이때 API Gateway에 연결된 Bulkhead는 뒷단 Server와 API Gateway용 Server의 대수를 모두 고려해야 합니다. 예를 들어, API Gateway용 Server가 1대, 최대 동시 커넥션 수가 500개인 뒷단 Server가 10대 있다면, Gateway의 Bulkhead는 5000으로 설정해야 합니다. 만약 서비스가 성장하여 뒷단 Server 10대를 추가하면, Bulkhead도 10000으로 조정해야 합니다. API Gateway용 Server를 2대로 증설하면, 각 Server의 Bulkhead는 5000이 됩니다.

Bulkhead 값을 정적으로 설정 시, 잘못 설정한다면 이로 인해 카카오뱅크 서비스 전체에 영향을 줄 위험이 있습니다. 지나치게 낮게 설정 시 고객 요청이 과도하게 제한되고, 반대로 지나치게 높게 설정하면 뒷단 Server에 부하가 걸립니다. 또한, 서버 증설에 따른 설정 변경을 위해 API Gateway 재배포가 필요해 운영 비효율성이 생깁니다.

이 문제를 해결하기 위해, 저희는 Consul 데이터센터를 구축하고 API Gateway와 뒷단 Server들을 Consul 서비스 레지스트리에 등록했습니다. 이를 통해 Server 인스턴스 개수를 실시간으로 조회하여 Bulkhead를 자동으로 계산하고 설정할 수 있게 되었습니다. 덕분에 API Gateway에서 장애가 발생해도 내부 서비스를 안전하게 보호할 수 있었습니다.

@Component
class 서비스_인스턴스_변경_EventListener(
    private val bulkheadConfigService: BulkheadConfigService,
    private val serviceRegistry: ServiceRegistry,
    private val bulkheadRegistry: BulkheadRegistry
){
    @EventListener(서비스_인스턴스_변경_Event::class)
    fun bulkhead_업데이트(event: 서비스_인스턴스_변경_Event){
        게이트웨이_인스턴스_개수 = serviceRepository.findBy서비스명("게이트웨이")?.인스턴스_개수
        bulkheadConfig_목록조회.forEach {
            val bulkhead_별_최대_동시접속수 = 서비스_인스턴스당_최대_동시접속수 * it.서비스_인스턴스_개수 / 게이트웨이_인스턴스_개수
            bulkhead_별_최대_동시접속수_설정(it.bulkheadName, bulkhead_별_최대_동시접속수)
        }       
    }
    
    fun bulkhead_별_최대_동시접속수_설정(bulkheadName: String, maxConcurrentCalls: Int) {
        val bulkhead = bulkheadRegistry.조회(bulkheadName)        
        bulkhead.최대_동시접속수_설정(maxConcurrentCalls)
    }
}

비록 동적으로 서비스 주소를 찾아 호출하는 사례는 아니었지만, Consul 서비스 레지스트리를 구축하고 운영하면서 많은 운영 경험을 쌓았습니다. 이를 통해 향후 서비스 디스커버리 시스템을 도입하는 중요한 초석을 마련할 수 있었습니다.

Step 2. MSA 환경을 위한 동적 라우팅 API Gateway 신설

MSA의 큰 장점 중 하나는 서비스의 독립적인 배포와 빠른 배포가 가능하다는 점입니다. 그러나 모든 서비스가 MSA로 완전히 전환되기 전까지는 기존 모놀리식 구조의 레거시 시스템을 함께 운영해야 합니다. 이로 인해 신규 서비스 배포와 함께 거대한 중앙 서비스를 수정하고 배포해야 하는 일이 잦아지며, 필연적으로 병목이 발생합니다.

10-before-msa-api-gateway-image
[그림 10] 서비스/기능 추가 시, 기존 모놀리식 서비스와의 의존성 문제

MSA 전환 과정에서 발생하는 문제들을 해결하기 위해 저희는 새로운 API Gateway를 추가 개발했습니다. 기존 API Gateway와 유사한 구성이지만, 이번에는 API Gateway에 연동된 Consul 서비스 레지스트리를 통해 서비스를 동적으로 라우팅(Routing)하도록 구현했습니다.

API Gateway는 주기적으로 Consul 서비스 레지스트리를 조회하여, Healthy 서비스 인스턴스 목록을 기반으로 Route 목록을 생성합니다. API 경로에 호출할 서비스 이름을 포함해 API Gateway를 호출하면, Gateway는 해당 서비스 이름의 서비스 인스턴스 주소 목록을 찾아 목적지 서비스로 요청을 연결합니다. 이를 통해 동적 라우팅을 실현하고, MSA 환경에서 효율적 서비스 운영을 가능하게 했습니다.

11-after-msa-api-gateway-image
[그림 11] MSA API Gateway 도입에 따른 모놀리식 서비스 의존성 완화

// MSA API 게이트웨이의 Spring Cloud Gateway 설정
spring:
  cloud:
    gateway:
      discovery:
        locator:
          predicates:
          - name: Path
            args:
              pattern: "'/'+serviceId+'/**'"
          filters:
          - name: RewritePath
            args:
              regexp: "'/' + serviceId + '/?(?<remaining>.*)'"
              replacement: "'/${remaining}'"

추가되는 모든 서비스가 Consul 서비스 레지스트리에 등록될 수 있도록, 저희 팀에서는 공통 스켈레톤(Skeleton) 코드에 Consul과 API Gateway 연동 설정을 제공하고 있습니다. 이를 통해 새로운 서비스가 쉽게 Consul 서비스 레지스트리, API Gateway와 연동되도록 지원하고 있습니다.

// 마이크로 서비스의 consul 및 게이트웨이 연동 설정
spring:
  cloud:
    consul:
      host: ${hostname}.kbin.io 
      discovery:
        service-name: ${spring.application.name}
        instance-id: ${spring.application.name}-${hostname}-${blue_green}
        port: ${consul_instance_port:12290}
        acl-token: some-uuid…
        metadata:
          msa_gateway_enabled: true
          service_prefix: ${msa_api_gateway_service_prefix}
          blue_green: ${blue_green}

API Gateway를 통해 서비스를 동적 라우팅함으로써, 마이크로서비스 개발 시 레거시 모놀리식 코드 변경을 최소화할 수 있었습니다. 또한, 신규 서비스를 추가, 변경, 삭제할 때 타 서비스와의 불필요한 의존성을 최소화하여 개발 및 테스트 생산성을 높일 수 있었습니다. 그리고 서버 증설 시 반드시 필요했던 로드밸런서 장비 설정 작업도 덜어낼 수 있어 운영의 편의성도 증가했습니다.

Step 3. 서비스메시 자체 개발

API Gateway의 동적 라우팅으로 MSA 전환 시 서비스 간 의존성 문제를 해결했지만, 마이크로서비스 전환이 진행될 수록 개별 서비스 간 트래픽이 증가하므로 API Gateway에 과도한 부하가 예상됐습니다. 이를 방지하기 위해 내부 트래픽은 API Gateway를 거치지 않고 서비스 간 직접 통신하도록 서비스메시(Service Mesh)를 도입하기로 했습니다.

추후 K8s로의 전환을 고려하면서 온프레미스 구성을 크게 변경하지 않고 사용할 수 있는 서비스 메시 솔루션을 조사한 결과, Consul Connect, Istio, Linkerd 등에서 제공하는 사이드카(Sidecar) 기능은 저희 배포 환경에 맞지 않았습니다. 따라서 Envoy 사이드카를 사용하되, 컨트롤 플레인(Control Plane)을 직접 구현하기로 결정했습니다.

12-canaria-service-mesh-image
[그림 12] 온프레미스 환경을 위한 서비스메시(Service Mesh)

자체 구축한 서비스메시의 컨트롤 플레인(Control Plane)에서는 데이터 플레인(Data Plane)인 Envoy 프록시를 동적으로 설정하기 위해 Consul 서비스 레지스트리를 사용합니다. 주기적으로 서비스 레지스트리를 모니터링하며, Healthy 서비스 목록의 변경 사항을 각 Envoy 프록시에 전파합니다. Envoy는 이 설정을 통해 서비스 라우팅(Routing), 서킷 브레이크(Circuit Break), 재시도(Retry), 타임아웃(Timeout) 등의 네트워크 설정을 일관되게 관리할 수 있습니다.

저희는 이 서비스메시를 이용해 온프레미스 환경에서 카나리 배포를 준비하고 있습니다. Consul에서 제공하는 Key-Value 기능을 연동하여 Envoy의 서비스 가중치를 동적으로 설정함으로써 카나리 배포를 구현하고 있습니다. 이때 Rollout 서버가 Prometheus 메트릭스 규칙을 임계값으로 설정하고, Envoy의 Blue/Green 서비스 가중치를 조절하는 방식으로 카나리 배포가 진행됩니다.

13-canaria-service-mesh-canary-deployment-image
[그림 13] 자체 개발 서비스메시를 이용한 온프레미스 카나리 배포

[그림 13]에 등장하는 요소들과 동작 방식을 설명드리면 다음과 같습니다.

  1. Rollout 서버가 Consul에 저장된 Blue/Green 가중치를 변경한다.
  2. Control Plane이 Consul에서 변경된 Blue/Green 가중치를 읽는다.
  3. Control Plane이 Blue/Green 가중치 변경내역이 Data Plane인 Envoy에 전파되고, Envoy는 Blue/Green 가중치 설정을 동적으로 변경하여 소수점 단위로 트래픽을 전환한다.
  4. Prometheus에 Blue/Green 서비스 인스턴스의 HTTP 에러율을 질의하며, 특정 임계치를 기준으로 롤백 또는 가중치 전환을 이어 진행한다.

Step 4. 멀티데이터센터를 위한 API Gateway 신설

카카오뱅크는 다양한 클라우드 환경의 장점을 결합하여 유연성과 안정성을 극대화 하면서도 비용 효율성을 추구하기 위해 하이브리드 멀티 클라우드 전략을 추구 하고 있습니다. 또, 재해 발생시 기존 DR(Disaster Recovery) 센터 기반의 Active-Standby 방식보다 훨씬 빠르고 연속적인 재해복구를 위해 Active-Active 센터로의 전환을 준비하고 있습니다. 이에 따라 다수의 데이터센터, VM/k8s, 온프레미스/클라우드 동시 운영 시 복잡한 트래픽을 효율적으로 관리할 수 있는 수단이 필요해졌습니다.

데이터센터 간 대역폭 제한과 지연을 고려하면, 하나의 트랜잭션은 최초 인입된 데이터센터 내에서 처리되는 것이 바람직합니다. 그러나 멀티데이터센터는 다음과 같은 상황이 발생할 수 있습니다.

  1. 데이터센터 별 서비스 유무 또는 운영 규모가 다른 경우
  2. 데이터센터 장애로, 한 데이터센터의 일부 또는 전체 서비스 이용이 불가해진 경우

멀티데이터센터는 이러한 다양한 상황 속에서 들어오는 트래픽을 처리할 수 있어야 합니다.

또한, K8s로 이관하는 과정에서도 서비스는 일관된 방식으로 트래픽을 관리할 수 있어야 합니다. Consul에서는 Consul 서비스와 K8s 서비스를 동기화해주는 Consul Sync 기능을 제공하므로, 별도 K8s Operator 없이 이를 달성할 수 있습니다. 저희는 Consul Sync를 사용해 K8s와 온프레미스 서비스를 일관된 서비스 레지스트리에서 관리할 수 있게 되었습니다.

14-multi-datacenter-gateway-image
[그림 14] 멀티데이터센터 서비스를 오케스트레이션 해주는 API Gateway

현재 저희는 멀티데이터센터 환경에서 K8s와 non-K8s 서비스를 동일하게 운영할 수 있는 API Gateway를 개발했습니다. 이 API Gateway는 Consul Sync를 사용해 K8s와 non-K8s 서비스를 통합하여 Consul 서비스 레지스트리와 연동합니다. 이를 통해 데이터센터별 서비스 목록을 식별하고, API Gateway가 위치한 데이터센터 내의 서비스로 우선적으로 라우팅합니다. 만약 해당 데이터센터 내에 서비스가 없으면, 다른 데이터센터의 API Gateway로 요청을 전달하여 처리될 수 있게 합니다.

마무리하며

카카오뱅크는 기존 모놀리식 아키텍처에서 마이크로서비스 아키텍처로 전환하는 과정에서 서비스 간 의존성과 트래픽 증가로 인해 발생하는 문제들을 해결하기 위해 다양한 기술을 도입하여 최적화하고 있습니다. 특히 Consul을 활용한 서비스 레지스트리와 서비스메시를 통해 안정성과 유연성을 확보했습니다.

전환 과정에서 API Gateway의 역할을 확장하여 K8s와 non-K8s 환경 모두에서 일관되게 운영할 수 있도록 했습니다. 초기에는 서비스 라우팅과 동적 트래픽 제어를 구현하는 데 어려움이 있었으나, Consul Sync와 Envoy 프록시를 도입하여 해결할 수 있었습니다. 이 과정에서 멀티데이터센터와 VM, K8s를 아우르는 통합된 트래픽 관리를 구현한 것이 주요한 성과였습니다.

지금까지 저희가 구축한 서비스 레지스트리와 디스커버리 시스템들에 대해 살펴보았습니다. 카카오뱅크는 인프라 환경의 큰 변화를 앞두고 있고, MSA로 재구성되어가고 있습니다. 최종적으로는 모두가 바라는 이상적인 아키텍처에 도달하는 것이 목표지만, 현재의 모놀리식 서비스와 온프레미스 시스템을 안정적으로 운영하며 개선할 책임도 가지고 있습니다. 무엇보다 기존 시스템을 새로운 시스템으로 전환해나가는 과정에서 연속성을 유지할 수 있도록 많은 노력을 기울이고 있으며, 이에 서비스 디스커버리가 핵심적인 역할을 담당하고 있습니다.

현재는 이렇게 구축한 서비스 디스커버리 시스템을 사내 개발부서에 전파하기 위해 개발자들을 대상으로 사용 방법과 장점을 안내드리고, 기능 추가 및 개선에 대한 의견을 청취하며 더 많은 팀에서 기존의 시스템을 더 유연하고 확장성 있는 환경으로 이관하는 작업을 돕고 있습니다. 실제로 이관의 니즈를 가진 개발부서에서 먼저 연락을 주시는 경우도 있어, 직접 구축한 시스템이 필요한 곳에 사용되는 모습을 보면 뿌듯하기도 합니다.

물론 아직 완벽하진 않지만, 앞으로도 지속적으로 개선해 나가며 카카오뱅크의 서비스 디스커버리 시스템이 IT 서비스 아키텍처의 모범이 되도록 노력하겠습니다. 긴 글 읽어주셔서 감사합니다.