당근마켓 검색 엔진, 쿠버네티스로 쉽게 운영하기

Kideok Kim
당근 테크 블로그
30 min readMay 9, 2023

안녕하세요. 당근마켓 검색 플랫폼팀 Teddy 에요. 당근마켓 검색 플랫폼 팀은 검색 인프라와 검색 서비스를 운영하면서 당근마켓의 모든 문서를 검색 가능하게 하고, 검색 트래픽을 안정적으로 소화하며, 나아가 더 나은 검색 경험을 제공할 수 있는 튼튼한 플랫폼을 만드는 팀이에요. 이를 위해 팀에서는 다양한 노력을 하고 있는데요, 최근에는 검색 인프라를 쿠버네티스(Kubernetes)로 이관하는 작업을 진행하게 됐어요. 이관 전에는 검색 엔진 작업에 시간이 오래 걸리고 까다로웠지만, 쿠버네티스로 옮겨가면서 많은 문제가 해결되었어요. 그래서 오늘은 과거의 검색 인프라와 거기서 겪었던 이슈, 그리고 쿠버네티스 도입까지의 전반적인 내용을 공유 드리면서 어떤 식으로 문제를 정의하고 해결했는지 이야기해 볼게요.

당근마켓의 검색 엔진

당근마켓은 검색 엔진으로 엘라스틱서치(Elastic)를 사용하고 있어요. 엘라스틱서치는 다양한 형태의 데이터를 가공, 분석, 그리고 검색할 수 있게 해주는 루씬 기반의 분산형 검색 엔진이에요. 오픈 소스인 엘라스틱서치는 확장성과 고가용성을 제공해 줄 뿐만 아니라, 엘라스틱 생태계에 존재하는 다양한 솔루션들과 함께 사용할 수 있어서 보다 강력한 검색 및 분석 기능을 사용할 수 있어요.

당근마켓은 주로 풀 텍스트 검색, 준실시간 검색, 그리고 로그 및 매트릭 수집 등의 목적으로 엘라스틱서치를 Metricbeat, Filebeat, 그리고 Kibana 와 함께 사용하고 있어요. Metricbeat 는 검색 엔진의 실시간 매트릭을 수집 및 모니터링하기 위해 사용하고 있고, Filebeat 는 쿼리 및 시스템 로그를 수집하는 목적으로 사용하고 있어요. 그리고 이를 시각화하기 위한 도구로서 Kibana 를 사용하고 있어요.

당근마켓의 과거 검색 인프라

초당 평균 1K의 검색 요청과 중고 거래 문서 기준 약 2.7억 건 이상의 문서들을 안정적으로 서빙하기 위해서는 탄탄한 인프라를 기반으로 검색 엔진을 운영해야 해요. 이를 위해 검색 플랫폼 팀은 초기 당근마켓 검색 인프라를 다음과 같이 구성했어요.

당근마켓 검색 인프라 초기 구성도 예

AWS ASG(Auto Scaling Group)에서 EC2 기반으로 동작하는 검색 클러스터는 메인 ES 클러스터와 모니터링 ES 클러스터 2개로 구성되어 있어요. 각 클러스터는 여러 개의 ES 노드들로 구성되어 있고, ES 노드들은 모두 각자의 EC2 인스턴스에서 도커 컨테이너로 실행되고 있어요. 두 ES 클러스터는 목적과 역할이 다른데요, 아래에서 좀 더 자세히 설명해 볼게요.

메인 ES 클러스터

메인 ES 클러스터는 검색 기능을 제공하기 위해 사용되는 핵심 클러스터에요. 이 클러스터는 중고 거래 컬렉션을 포함한 다양한 컬렉션들을 가지고 있고, 검색과 색인 모두 해당 클러스터를 대상으로 하고 있어요. 또한, 클러스터의 노드들을 역할에 따라 3가지로 구분해서 운영하고 있어요. 각각의 역할은 다음과 같아요.

  • 마스터 노드: 클러스터의 상태 정보나 인덱스의 메타 데이터를 관리하는 노드에요. Split-brain을 방지하기 위해 홀수 개로 운영하고 있어요.
  • 코디네이팅 노드: Client 노드라고도 불리며 실제 검색 요청을 받게 되는 노드에요. 받은 검색 요청을 데이터 노드로 전달하고 결과를 받아 취합 후 반환하게 돼요. 참고로 모든 ES 노드는 기본적으로 코디네이팅 노드의 역할을 할 수 있어요.
  • 데이터 노드: 인덱스 데이터를 가지고 있는 노드에요. 그래서 검색과 집계 등 데이터를 핸들링하기 위한 연산들이 실제로 수행되는 곳이기도 해요.

모니터링 ES 클러스터

모니터링 ES 클러스터는 메인 ES 클러스터에서 발생하는 쿼리 로그, 시스템 로그 및 시스템 매트릭을 수집하는 클러스터에요. 로그 및 매트릭 데이터는 메인 ES 클러스터에서 쌓아서 관리할 수도 있어요. 하지만, 서비스의 규모가 점차 커지면서 검색 데이터와 요청량이 지속적으로 증가하고 있는 상황이기 때문에, 로그 및 매트릭 데이터를 별도의 ES 클러스터에서 관리하는게 메인 ES 클러스터를 보다 안정적으로 운영하는 데에 도움이 된다고 판단했어요.

모니터링 ES 클러스터는 각각의 노드를 역할에 따라 구분하지 않아요. 그래서 모든 노드가 마스터, 코디네이팅, 그리고 데이터 노드 역할을 해요. 이 클러스터의 주요 역할은 다음과 같아요.

  • 메인 ES 클러스터의 로그 수집: 메인 ES 클러스터는 Logstash를 사용해서 모든 쿼리 로그 및 시스템 로그들을 모니터링 ES 클러스터로 보내게 돼요. 이렇게 수집된 로그는 Kibana의 Discover 기능을 통해 조회 및 분석할 수 있어요.
  • 메인 ES 클러스터의 매트릭 수집: 메인 ES 클러스터는 Metricbeat를 사용해서 모든 시스템 매트릭을 모니터링 ES 클러스터로 보내게 돼요. 이렇게 수집된 시스템 매트릭은 Kibana의 Stack Monitoring 기능을 통해 모든 노드와 인덱스들의 상태를 실시간으로 모니터링할 수 있게 해줘요.

그 밖에 구성

  • LB(Load Balancer): 코디네이팅 노드와 연결된 검색 로드 밸런서와 데이터 노드와 연결된 색인 로드 밸런서가 있어요. 검색 로드 밸런서는 검색 요청을 처리하고 색인 로드 밸런서는 색인 요청을 처리해요. 검색 로드 밸런서를 통해서 색인 요청을 보내도 되지만, 검색 요청의 Latency에 영향을 줄 수 있기 때문에 색인 요청을 별도로 분리해서 관리하고 있어요.
  • EFS(Elastic File System): 사전(Dictionary) 파일 업데이트를 위해 EFS 을 사용하고 있어요. 모든 ES 노드는 EFS와 연결되어 있어서 로컬 디스크에서 사전 파일을 읽어가는 것처럼 동작할 수 있어요.
  • S3: ES 스냅샷 데이터를 저장 및 관리하기 위해 사용하고 있어요.
  • Grafana: 모든 ES 노드에서 실행되고 있는 Prometheus Exporter를 통해 매트릭을 수집하고 있고, Grafana를 통한 모니터링 및 알람 기능을 사용하고 있어요.

검색 플랫폼 팀은 위와 같은 구성의 검색 인프라를 한국, 일본, 영국, 그리고 캐나다까지 총 4개의 지역, Alpha와 Production 총 2개의 환경에서 구성하고 있어요. 즉, 총 8개의 각기 다른 환경에서 위와 같은 검색 인프라를 구축하고 있어요. 또한, 8개의 검색 인프라의 구성이 거의 동일하기 때문에 보다 쉽고 빠르게 검색 인프라를 구축 및 확장하기 위해서 Terraform을 통해 코드로서 검색 인프라를 관리했어요.

과거 검색 인프라를 운영하면서 느낀 문제점

이처럼 검색 플랫폼 팀은 안정적으로 검색 인프라를 구축하여 꽤 오랫동안 운영해 왔어요. 그러나, 점차 당근마켓 서비스가 커지고 검색 엔진의 규모도 커지면서 ES 플러그인 업데이트, ES 버전 업데이트, 사이드카 컨테이너 업데이트 등 검색 클러스터 배포가 빈번하게 발생했고, 이 과정에서 몇 가지 문제점들이 눈에 띄기 시작했어요.

첫 번째 문제점은 검색 클러스터 배포 방식이 매우 까다로워서 사람이 직접 모든 배포 과정에 관여해야 한다는 점이에요. 위에서 말씀드린 대로, 검색 클러스터의 각 ES 노드는 ASG(Auto Scaling Group) 로 관리되고 있고, 배포는 ASG 의 desired count를 조절하여 수동으로 배포 하고 있어요. 배포 과정을 조금 더 구체적으로 설명해 보자면 아래와 같아요.

  1. Docker Compose 파일에 새로운 ES docker image를 적용한다.
  2. ASG desired count를 2배로 늘려서 새로운 마스터, 코디네이팅, 그리고 데이터 노드를 위한 EC2 인스턴스를 생성한다. 이때, 생성되는 EC2 인스턴스들은 1번에서 정의한 Docker Compose에 의해 새로운 ES docker image를 실행하게 된다.
  3. 기존 데이터 노드에 있던 모든 인덱스를 새로운 데이터 노드로 옮긴다. 이때 이동 중인 샤드들은 일시적으로 요청을 처리할 수 없게 되므로, 총 18대의 데이터 노드들을 한 대씩 순차적으로 옮긴다.
  4. ASG desired count를 2배로 줄여서 과거에 사용했던 마스터, 코디네이팅, 그리고 데이터 노드의 EC2 인스턴스들을 종료한다. 이때 ASG의 기본 종료 정책에 의해 오래된 인스턴스부터 종료되기 때문에 과거 인스턴스들이 종료됨을 보장할 수 있다.

위의 모든 과정은 사람이 직접 수동으로 진행해야 해요. 뿐만 아니라, 배포를 진행하는 사람은 3번과 같이 relocating 되는 샤드들은 일시적으로 요청을 처리할 수 없기 때문에 한 번에 모든 샤드를 relocating 시키면 장애가 발생할 수 있다는 엘라스틱서치의 특성까지도 모두 이해하고 있어야만 배포를 할 수 있어요. 즉, 배포에 대한 진입장벽도 꽤 높은 편이에요.

두 번째 문제점은 이 수동 배포가 시간이 매우 오래 걸린다는 점이에요. 배포는 평균 5시간 정도 소요돼요. 특히 데이터 노드 배포의 경우, 노드를 종료하기 전에 디스크에 저장된 샤드들을 모두 옮겨야 하기 때문에 시간이 매우 오래 걸려요. 물론, indices recovery max bytes 설정 또는 shard concurrent rebalancing 설정 등 몇 가지 엘라스틱서치 설정을 통해 샤드를 옮기는 작업의 속도를 향상시킬 수는 있지만, 이는 결국 데이터 노드의 리소스 사용량을 늘리기 때문에 트레이드 오프가 발생해서 속도 향상에도 제한이 있었어요.

데이터 노드 배포가 09:30 ~ 14:30 (5h) 소요되는 것을 보여주는 매트릭 지표

데이터 노드를 배포할 때는 샤드를 옮기는 작업 때문에 CPU 사용률이 일시적으로 높아지게 돼요. 그래서 다시 잠잠해지는 데까지 얼마나 걸리는지를 보면 대략 배포 소요 시간을 확인할 수 있어요. 위 매트릭은 데이터 노드 배포 시점에 CPU 사용률을 보여주는 매트릭이에요. 이를 통해서 배포가 09:30부터 14:30까지 약 5시간이 소요됐다는 것을 알 수 있어요. 따라서, 검색 플랫폼 팀은 이 문제를 해결하고자 다음과 같이 문제를 정의하고 목표를 세우게 됐어요.

문제:

빈번하게 발생하는 검색 클러스터 배포에 대해 매번 평균 5시간 이상씩 사람이 직접 수동으로 진행해야 한다.

목표:

  1. 배포 자동화를 통해, 누구나 안전하게 검색 클러스터를 배포할 수 있도록 하자.
  2. 현재 평균 5시간 소요되는 검색 클러스터 배포 시간을 30분 이내로 개선하자.
kubernetes logo

ECK (Elastic Cloud of Kubernetes) 를 만나다

검색 플랫폼 팀은 어떻게 하면 이 문제를 해결할 수 있을지 여러 방면으로 조사했고, 그 결과 쿠버네티스 도입을 고려하게 됐어요. 쿠버네티스는 컨테이너 기반으로 동작하는 워크로드와 서비스에 대한 배포, 관리, 그리고 확장을 자동화해 주는 오픈 소스 플랫폼이에요. 특히, 컨테이너 오케스트레이션을 제공하기 때문에 저희가 목표로 하는 배포 자동화를 도입할 수 있다는 게 가장 큰 매력이었어요.

엘라스틱은 지난 2020년 1월에 ECK (Elastic Cloud of Kubernetes) 1.0을 정식 출시했어요. 쿠버네티스 오퍼레이터를 기반으로 구축된 ECK는 기본 쿠버네티스의 오케스트레이션 기능을 확장하여 쿠버네티스에서 엘라스틱 스택을 배포, 관리, 그리고 운영할 수 있게 해주는 솔루션이에요. ECK는 롤링 배포 과정에서 새롭게 실행되는 노드(Pod)를 기존 샤드가 저장되어 있는 디스크에 연결하는 구조이기 때문에, 샤드를 새로운 노드로 옮기지 않아도 되므로, 배포 자동화뿐만 아니라 배포 시간도 크게 단축할 수 있을 거라고 예상했어요.

ECK PoC (Proof of Concept)

ECK 도입을 위해 검색 플랫폼 팀은 약 3달 정도 ECK PoC를 진행했어요. PoC 에서는 ECK 가 예상대로 동작하는지, 현재 당근마켓 검색 클러스터에 도입했을 때 부작용이나 예기치 못한 동작은 없는지 등을 면밀하게 살펴보는 것이 목표였어요.

환경 구성

본격적인 PoC를 진행하기에 앞서 먼저 쿠버네티스 클러스터에 ECK를 설치해야 했어요. ECK를 설치하는 방법은 두 가지가 있는데, 첫 번째 방법은 YAML manifest를 사용해서 쿠버네티스 클러스터에 직접 CRDs(Custom Resource Definitions) 과 오퍼레이터를 설치하는 것이고, 두 번째 방법은 Helm chart를 이용하는 방법이에요. 저희는 쿠버네티스 클러스터 운영을 담당하는 SRE 팀의 도움을 받아 Helm Chart를 사용해서 ECK를 설치했어요. 아래는 Helm을 이용해서 elastic-system 네임스페이스에 ECK를 설치하는 명령어 예시에요.

helm install elastic-operator elastic/eck-operator -n elastic-system --create-namespace

쿠버네티스 클러스터에 ECK를 설치했다면, 다음으로는 검색 클러스터를 구성해야 해요. 클러스터 구성은 쿠버네티스의 YAML manifest 파일을 통해 할 수 있어요. 아래는 검색 클러스터 구성도와 YAML manifest 파일 예시에요.

ECK 기반 검색 클러스터 구성도 예
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: search-cluster
namespace: elastic-system
spec:
version: 7.15.1
...
nodeSets:
- name: master
count: 3
config: &CONFIG
node.roles: [ "master" ]
...
...
volumeClaimTemplates: &VOLUME_CLAIM_TEMPLATES
- metadata:
name: elasticsearch-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
- name: coordinate
count: 2
config:
<<: *CONFIG
node.roles: [ ]
podTemplate: *POD_TEMPLATE
volumeClaimTemplates: *VOLUME_CLAIM_TEMPLATES
- name: data
count: 3
config:
<<: *CONFIG
node.roles: [ "data" ]
...
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi

ECK를 설치하면서 함께 정의한 CRD 로부터 Elasticsearch 라는 이름의 리소스를 사용하여 엘라스틱서치 검색 클러스터를 구성할 수 있어요. 그래서 위 YAML manifest 파일을 보면 kind 부분에 Elasticsearch를 사용했어요. version은 엘라스틱서치의 버전을 의미하는데, 저희는 현재 7.15.1 버전을 사용하고 있어요. nodeSets은 엘라스틱서치 클러스터 구성의 핵심적인 부분이라고 할 수 있어요. 여기서 배열 형태로 master, coordinate, 그리고 data 라는 이름의 노드 셋이 설정된 것을 볼 수 있는데, 각각은 node.roles에 의해 역할을 부여받게 되며 쿠버네티스는 이들을 StatefulSet으로 구성하게 돼요. 각각의 StatefulSet은 count 설정만큼의 노드를 생성해요. 이렇게 설정했을 때 최종적으로 위의 구성도와 같은 형태의 클러스터를 만들 수 있어요.

이 밖에도 ulimit, swapoff, virtual memory 설정 등 엘라스틱서치를 효율적으로 사용하기 위한 다양한 시스템 설정도 적용하고 ECK의 리소스인 Kibana와 쿠버네티스의 리소스인 Service, Config 등 다양한 리소스들을 추가로 사용했지만, 핵심적인 부분은 아니라서 이 부분에 대한 설명은 생략하도록 할게요.

개념 검증

이렇게 ECK PoC를 위한 환경을 구성한 뒤 본격적인 PoC를 시작했어요. PoC는 크게 개념 검증과 규모 검증 두 부분으로 나누어서 진행했어요. 우선, 개념 검증에서는 소규모의 인덱스와 약 100 QPS 정도의 트래픽을 바탕으로 이론적으로 알고 있는 ECK 의 동작을 검증하는 게 목표였어요. 특히, 배포 등 클러스터 운영과 관련된 부분들을 중점적으로 검증하고, 트래픽을 주입했을 때 예기치 못한 이슈가 발생하는지도 확인하기로 했어요. 다양한 관점으로 많은 검증을 진행했는데, 그 중 대표적인 검증 케이스는 다음과 같아요.

‘롤링 배포시 검색 서빙이나 색인 중단이 발생하지 않음을 검증’

기본적으로 ECK를 통한 검색 클러스터 배포는 쿠버네티스 롤링 배포를 기반으로 동작하기 때문에, 롤링 배포가 예상대로 잘 동작하는지를 우선 검증했어요. 롤링 배포를 진행했을 때 코디네이팅 노드 -> 데이터 노드 -> 마스터 노드 순서로 배포가 진행됐고, 데이터 노드 배포 시에만 클러스터 상태가 Yellow로 변경되는 걸 확인할 수 있었어요. 그 이유는 배포로 인해 데이터 노드가 잠시 종료될 때 해당 데이터 노드가 가지고 있던 샤드들에 접근할 수 없어서 Unassigned Shard가 발생하기 때문이에요. 아래는 당시 상황을 보여주는 스크린샷이에요.

데이터터 노드 롤링 배포시 Unassigned Shard로 인해 클러스터 상태가 Yellow로 바뀜

여기서 주의해야 할 점은 ECK가 기본적으로 OnDelete StatefulSet update strategy 방식을 채택하기 때문에, 기존에 실행 중인 데이터 노드를 먼저 삭제하고 새로운 데이터 노드를 실행하는 순서로 배포를 진행한다는 점이에요. 따라서, 삭제되는 데이터 노드가 서빙하던 샤드를 대체할 수 있는 레플리카 샤드가 반드시 존재해야 해요. 그렇지 않으면, 해당 샤드에 접근할 수 없게 되어서 클러스터 상태는 Red 가 돼요.

‘노드 강제 종료시 검색 서빙 중단이 발생하지 않음을 검증’

다음으로는 강제로 마스터, 데이터, 코디네이팅 노드를 한 대씩 종료했을 때 서빙 중단이 발생하는지를 점검했어요. 배포 상황이 아니더라도, 어떠한 이유에서든 예기치 못하게 노드가 종료될 수 있기 때문에 이런 상황에서도 클러스터가 문제 없이 동작하는지를 검증할 필요가 있었어요.

마스터와 코디네이팅 노드는 강제 종료 후에 쿠버네티스가 노드 개수에 맞게 다시 복구시켜 주는 것을 (Self Healing) 확인할 수 있었고, 매트릭에도 큰 변화는 없었어요. 그러나, 데이터 노드의 경우 엘라스틱 서치가 해당 데이터 노드에서 서빙하고 있던 샤드들을 Unassigned Shard로 인식하고 다른 노드에 새로운 레플리카 샤드를 만드는 상황을 발견했어요. 아래는 당시 상황을 나타내는 스크린샷인데, 주황색 라인으로 체크된 곳을 보면 Initializing shards 라고 되어 있는 부분이 순간 올라갔다가 내려간 것을 확인하실 수 있어요.

데이터 노드 한 대 강제 종료 후 쿠버네티스가가 자동으로 복구시켜주는 상황

사실 이러한 동작이 엘라스틱 서치의 기본 동작이기는 하지만, 쿠버네티스에서는 ECK 오퍼레이터가 컨트롤러를 통해서 항상 Desired State를 만들어 주기 때문에 종료된 노드는 곧바로 새로 실행된다는 것을 보장할 수 있어요. 따라서, 실행 중인 다른 노드에 샤드의 레플리카를 새롭게 만들었다가 종료됐던 노드가 복구되면 다시 그 레플리카를 옮기는 등의 작업은 불필요하기 때문에 이러한 동작은 가능한 지연시키는 게 좋다고 판단해서 검색 클러스터에 다음 설정을 추가했어요.

PUT _all/_settings
{
"settings": {
"index.unassigned.node_left.delayed_timeout": "10m"
}
}

‘하루 이상 검색 클러스터를 운영하면서 안정적으로 동작하는지 검증’

엘라스틱서치 성능을 위한 기본적인 설정을 적용하고 위에서 진행한 검증을 모두 마친 뒤에는 하루 이상 트래픽을 주입해 보면서 안정적으로 동작하는지 검증하는 단계를 거쳤어요. 이때는 조금 더 실제와 가까운 상황을 재현하기 위해서 검색 클러스터 규모를 약간 늘려서 테스트를 진행했어요. 그런데, 약 500 QPS 로 트래픽을 주입하며 테스트를 진행하던 중에 클러스터가 이상하게 동작하는 상황을 발견했어요. 배포도 하지 않았는데 갑자기 Unassigned Shard가 발생하면서 데이터 노드의 CPU 사용률과 Latency가 급증하는 현상이 반복해서 나타났어요. 아래는 당시 상황을 나타내는 매트릭이에요.

갑자기 unassigned shard가 발생하면서 데이터 노드 CPU 사용률과 Latency가 급증하는 현상

검색 클러스터의 노드에 접속해서 로그를 확인해 보니 아래와 같은 로그가 있었어요. 갑자기 데이터 노드가 disconnected 되면서 이런 현상이 발생했다는 로그였고, 갑자기 TCP connection이 끊긴 것으로 보였어요.

[INFO][o.e.c.s.MasterService ] [search-main-alpha-kr-es-master-1] node-left[{search-main-alpha-kr-es-data-15} … reason: **disconnected**], term: 10, version: 5479, delta: removed {{search-main-alpha-kr-es-data-15}

엘라스틱서치는 기본적으로 노드 간 TCP connection이 idle 상태이더라도 클러스터링을 위해 connection을 계속 유지하려고 하기 때문에, 외부의 다른 요인에 의한 문제일거라고 생각했어요. 그래서 검색 클러스터의 쿠버네티스 환경 구성을 다시 한번 확인했어요. 그러던 중에 Pod에 기본적으로 생성되는 istio sidecar를 발견했어요. istio sidecar는 proxy container로서 Pod로 들어오는 모든 요청을 먼저 수신하기 때문에 connection에도 직접적인 관여를 해요. 특히, idle connection에 대해서 1시간이 지나면 기본적으로 timeout을 발생시키도록 설정되어 있기 때문에 이게 원인일 수 있겠다고 생각했어요.

조금 더 조사해 보니 비슷한 사례를 엘라스틱서치 커뮤니티에서도 발견할 수 있었고, 이를 바탕으로 당근마켓 SRE 팀에 이슈업 해서 도움을 요청했어요. 그래서 SRE 팀도 함께 이슈를 디버깅해 주셨고 결론적으로 istio sidecar를 제거해 보는 게 좋겠다는 피드백을 주셨어요. 그래서 SRE 팀의 가이드에 따라, 아래의 sidecar injection: false 설정을 YAML manifest 파일에 추가해서 검색 클러스터를 다시 배포했어요.

podTemplate:
metadata:
annotations:
sidecar.istio.io/inject: "false"

배포 후 다시 테스트해 보니 문제가 발생하지 않았어요. 아래는 istio sidecar 유무에 따른 검색 클러스터 CPU 사용량을 비교한 매트릭이에요. 왼쪽은 istio sidecar로 인해 위에서 언급한 이슈가 발생하면서 데이터 노드 CPU 사용량이 치솟는 모습이고, 오른쪽은 istio sidecar를 제거한 뒤에 CPU 사용량이 안정화된 모습이에요. 이를 계기로 쿠버네티스에서 엘라스틱서치를 운영할 때는 istio sidecar와 같은 프록시 서버를 두지 않아야 한다는 것을 배울 수 있었어요.

istio sidecar 제거 전/후 검색 클러스터 CPU 사용량 매트릭 차이

‘롤백이 잘 동작하는지 검증’

롤백이 잘 동작하는지도 개념 검증에서 확인해야 할 중요한 요소 중 하나였어요. 검색 클러스터 배포 시 문제가 발생한다면 빠르게 이전 상태로 되돌릴 필요가 있기 때문이에요. 여기서는 ArgoCD의 History & Rollback 기능을 사용했어요. ArgoCD는 기본적으로 배포 리비전 히스토리를 기억하고 있기 때문에, 이를 이용해서 빠르게 롤백할 수 있어요. 검색 클러스터 배포를 진행하는 중간에 롤백 버튼을 눌렀을 때, 현재 배포가 진행되고 있는 노드까지만 배포를 완료한 뒤, 새롭게 배포된 노드 모두를 다시 이전 버전으로 배포하는 것을 확인할 수 있었고, ArgoCD 상태도 Synced에서 OutOfSync로 변경됐어요.

규모 검증

규모 검증에서는 실제 프로덕션과 동일한 환경을 구축해서 이슈를 미리 점검하고 운영에 문제가 없는지 확인하는 게 목표였어요. 따라서, 검색 클러스터 노드 수도 프로덕션과 동일하게 증설하고 인덱스도 프로덕션 환경의 엘라스틱서치 스냅샷 데이터를 가져와서 사용했어요. 뿐만 아니라, 수집 중인 실시간 쿼리 로그를 이용하여 피크 타임 트래픽과 동일한 트래픽을 보내보면서 검증을 진행했어요. 이 단계에서도 개념 검증에서 진행했던 대부분의 검증을 포함하여 다양한 관점에서 검증을 진행했고, 그 중 대표적인 케이스는 다음과 같아요.

피크 타임 트래픽일 때 검색 클러스터 리소스 사용량 및 레이턴시 비교’

당근마켓 검색 클러스터는 피크 타임에 약 1K QPS의 트래픽을 소화하고 있어요. 그래서 ECK 기반의 검색 클러스터를 운영 환경에서 사용하기 전에 피크 타임 트래픽을 잘 소화해 내는지에 대해 반드시 검증할 필요가 있었어요. 이를 위해 피크 타임 트래픽을 주입했을 때의 평소 상태와 배포 상황의 상태를 테스트했어요. 피크 타임만큼의 트래픽을 테스트 클러스터에 주입하기 위해서는 부하 테스터가 필요했어요. 마침 검색 플랫폼 팀에는 자체적으로 만든 부하 테스터가 있어서 이를 활용해서 쉽게 테스트를 진행할 수 있었어요. 부하 테스터는 카프카로 수집하고 있는 실시간 검색 쿼리를 원하는 만큼의 QPS로 검색 클러스터로 보내게 돼요. 그래서 파라미터값으로 QPS 값만 넘겨주면 쉽게 검색 클러스터에 부하를 줄 수 있어요.

왼쪽: 1K QPS 트래픽일 때 검색 클러스터 상태 / 오른쪽: 1K QPS 트래픽일 때 배포 중인 검색 클러스터 상태

위 이미지에서 왼쪽은 1K QPS로 테스트를 진행한 결과 매트릭이에요. CPU 사용률은 최대 약 40%였고, 레이턴시는 약 P50:25ms/P90:90ms/P99:330ms 로 현재 운영 환경과 비교했을 때 거의 유사한 수준임을 확인했어요. 그러나, 오른쪽 이미지와 같이 피크 타임 트래픽이 발생할 때 배포하면 CPU 사용률과 레이턴시가 튀는 현상이 발생하는 것을 확인할 수 있었어요. 그 이유는 크게 두 가지가 있는데, 첫 번째는 데이터 노드가 롤링 배포 중일 때 종료된 데이터 노드에서 서빙 중이었던 레플리카 샤드를 일시적으로 사용할 수 없어서 트래픽이 프라이머리 샤드를 가지고 있는 노드로 몰리기 때문이에요. 그리고 두 번째는 테스트 클러스터에 순간적으로 트래픽을 주입하다 보니 검색 캐시 데이터가 충분히 쌓이지 않기 때문이에요.

이 문제를 해결하기 위해서는 노드의 readiness probe 설정을 통해 충분히 캐시를 쌓고 웜업이 된 이후에 노드가 트래픽을 받게 하거나, 샤드의 레플리카 수를 2개 이상으로 늘리는 방법들을 적용해볼 수 있어요. 이와 같이 문제의 원인이 분명하고 피크 타임을 피해서 배포하는 등의 단기적으로 쉽게 해결할 수 있는 방안도 있기 때문에, 이 문제는 논이슈로 감안하고 진행하기로 했어요.

‘검색 클러스터가 어느 정도의 부하까지 견딜 수 있는지 검증’

위의 검증 과정을 통해서 ECK 기반의 검색 클러스터가 피크 타임 트래픽인 1K QPS를 잘 소화해 낸다는 것은 확인할 수 있었지만, 최대 얼마큼의 트래픽까지 견딜 수 있는지를 아는 것 또한 매우 중요했어요. 그래야 어느 시점에 클러스터의 확장이 필요하다는 판단을 할 수 있기 때문이에요. 그래서 우리는 1.5K QPS와 2K QPS에 대해서도 부하 테스트를 진행하기로 했어요.

왼쪽: 1.5K QPS 부하테스트 결과 / 오른쪽: 2K QPS 부하테스트 결과

왼쪽은 1.5K QPS에 대한 부하 테스트 결과에요. CPU 사용률과 Latency가 상승한 것을 볼 수 있지만, 서비스에 영향을 끼칠 정도는 아니라서 이 정도의 트래픽을 감당 가능하다고 판단했어요. 그러나, 오른쪽 2K QPS 를 보면 CPU 사용률이 거의 100%에 가깝게 올라가고 Latency도 크게 증가한 것을 보실 수 있어요. 이러한 결과를 바탕으로, 저희는 트래픽이 2K QPS에 가까워지면 알람을 받고 노드를 증설하거나 CPU 리소스를 더 투입하는 등의 액션을 취하기로 했어요.

ECK 도입

위에서 설명해 드린 내용들을 포함하여 PoC 기간 동안 다양한 검증 절차를 거쳤고, 최종적으로 저희 팀은 다음과 같은 이점들을 가져가고자 ECK 기반의 검색 클러스터를 도입하기로 결정했어요.

  1. 배포 안정성: 쿠버네티스의 컨테이너 오케스트레이션은 배포 자동화를 지원한다. 이를 통해 사람이 직접 배포하는 수동 작업을 없앨 수 있다. 뿐만 아니라, Pod만 종료됐다가 다시 실행되기 때문에 샤드를 새로운 노드로 이동하지 않아도 되므로 네트워크 비용도 없앨 수 있고 특정 노드로 샤드가 몰리는 상황을 방지할 수 있으며 배포 시간도 크게 단축할 수 있다.
  2. 손쉬운 배포: ECK를 사용하면 앞으로는 누구나 ES 클러스터를 배포할 수 있다. 가령, 검색 품질팀에서 토크나이저 플러그인을 설치하기 위해 클러스터 배포를 해야 할 때 항상 검색 플랫폼 팀에 요청했지만 앞으로는 요청 없이 직접 진행할 수 있다.
  3. 손쉬운 버전 업그레이드: ECK를 사용하면 minor 버전 업그레이드를 손쉽게 할 수 있다. YAML manifest 에서 spec.version만 변경해주면 ECK가 알아서 롤링 배포를 통해 버전 업그레이드를 적용한다.
  4. AutoScaling 적용: ECK를 사용하면 HPA(HorizontalPodAutoscaler) 를 적용할 수 있어서 리소스를 더욱 효과적으로 사용할 수 있고 클러스터 운영 비용도 최적화할 수 있다. 검색 클러스터 노드의 상태는 ECK가 알아서 모니터링 하기 때문에 우리는 신경쓸 필요가 없다.
  5. 역할과 책임 분리: ECK를 사용하면 AWS 및 인프라에 대한 운영은 전문 지식을 가지고 있는 SRE 팀에게 위임하고 우리는 우리가 더 잘하는 검색 클러스터 운영과 검색 서비스에 집중할 수 있다.

결정 이후에는 당근마켓 SRE 팀 분들과 함께 이관 작업을 계획하고 진행했어요. 실제 운영 중인 검색 클러스터에서 ECK 기반의 검색 클러스터로 중단 없이 교체하는 작업은 수많은 열차가 사용하고 있는 레일을 열차 운행에 영향 없이 교체하는 것과 같아요. 열차는 아무런 변화를 느끼지 못하고 평소처럼 레일을 사용할 수 있어야 하므로 매우 신중하게 작업해야 했어요. 그래서 여러 단계로 세분화된 이관 프로세스를 만들어서 진행하게 됐어요. 이때 최우선으로 고려했던 것은 무중단 배포와 빠른 롤백이었어요. 무중단 배포는 당근마켓 고객에게 영향을 주지 않고 적용하기 위함이고, 빠른 롤백은 배포 중에 발생할 수 있는 혹시 모를 상황에 대비하기 위함이에요. 어떤 과정을 통해 이관을 진행했는지 순서대로 간략히 소개해 드릴게요.

ECK 이관 과정을 나타낸 그림
  1. ECK 기반 검색 클러스터 실행: 운영 환경과 동일한 규모의 ECK 기반 검색 클러스터를 생성해요.
  2. 실시간 색인 및 전체 색인 작업 일시 정지: 문서 누락에 대비하고 색인 데이터의 일관성을 유지하기 위해 이관 중에는 전체 색인 및 실시간 색인을 잠시 정지해요.
  3. 기존 검색 클러스터에서 스냅샷 생성: 새로운 검색 클러스터에 색인 데이터를 주입하기 위해 기존 검색 클러스터에서 스냅샷을 생성해요.
  4. 새로운 검색 클러스터에 스냅샷 저장: 위에서 생성한 스냅샷 데이터를 새로운 검색 클러스터에 주입해요. 색인 작업이 멈춘 후에 스냅샷을 생성했기 때문에 기존 검색 클러스터와의 데이터 일관성을 보장할 수 있어요.
  5. 라우터가 새로운 검색 클러스터를 바라보도록 변경: 검색 로드밸런서 앞에는 AWS의 Route53을 이용한 라우터가 있는데, 이 라우터가 트래픽을 전달하는 대상을 새로운 검색 클러스터로 변경해요. 이렇게 하면 사용자는 기존 도메인 이름을 그대로 사용하면서 새로운 검색 클러스터로부터 검색 결과를 받을 수 있어요. 색인 쪽 라우터도 마찬가지로 새로운 검색 클러스터를 바라보도록 변경해서 색인 작업의 엔드포인트를 변경하지 않고 그대로 사용할 수 있게 했어요.
  6. 실시간 색인 및 전체 색인 작업 실행: 위 작업 동안 새로운 문서가 일시적으로 업데이트되지 않았기 때문에, 빠르게 실시간 색인 작업을 실행해서 새로운 ES 클러스터에서 검색 가능하도록 해요. 전체 색인 작업도 정상 작동을 확인할 목적으로 재실행했어요.
  7. 기존 검색 클러스터 종료: 하루 정도 모니터링을 해보고 문제가 없다고 판단됐을 때 기존 검색 클러스터를 종료해요.

위 과정을 통해, 저희는 총 8개의 검색 클러스터를 쿠버네티스로 안전하게 이관할 수 있었어요.

이관 후에는 ‘누구나 안전하게 검색 클러스터 배포가 가능하게 만들자’라는 목표를 달성하기 위해, 배포 파이프라인을 만드는 작업을 곧바로 진행했어요. 쿠버네티스로 검색 클러스터를 옮겨오면서 오케스트레이션 기능을 통한 자동 배포가 가능했지만, 엘라스틱서치 도커 이미지 생성, YAML manifest 파일에서 이미지 태그 수정, ArgoCD Sync 버튼 누르기 등 본격적인 배포 전에 알아야 할 내용들이 여전히 많았어요. 그래서 배포 파이프라인을 만들어서 이 부분을 자동화하여 누구나 배포할 수 있도록 하려고 했어요. 뿐만 아니라, 배포 파이프라인을 만들면 검색 클러스터의 상태에 따라 배포를 컨트롤할 수도 있다는 점도 큰 장점이었어요. 가령, 검색 클러스터의 CPU 사용량이 너무 높은 상황이면 배포 파이프라인에서 배포를 막을 수 있어요.

검색 클러스터 배포 파이프라인 구성도

배포 파이프라인은 Github Action을 사용해서 만들었어요. 이를 이용하면 원하는 버전의 ES Docker Image를 생성해 주고, 쿠버네티스의 Kustomize 도구를 이용하여 YAML manifest 파일에서 image를 자동으로 업데이트 해줘요. 뿐만 아니라, 클러스터의 상태를 체크해서 배포가 가능하다고 판단되면 ArgoCD API 를 통해 자동으로 Sync 도 해줘요. 그래서 배포를 하는 사람은 Github Action 버튼 클릭 한 번으로 검색 클러스터를 배포할 수 있게 돼요.

결과

결과적으로, 문제 정의부터 PoC를 거쳐 실제 검색 클러스터 운영 환경에 쿠버네티스를 도입하는 것까지 약 5개월에 걸친 프로젝트가 다행히 큰 이슈 없이 마무리됐어요. 그리고 현재 당근마켓의 검색 클러스터는 쿠버네티스 위에서 안정적으로 잘 운영되고 있어요. 마지막으로 저희 팀은 목표한 성과를 얼마나 달성했는지 판단하기 위해 성과 지표를 작성했어요. 다음은 성과 지표 작성 결과에요.

당근마켓은 OKR을 기반으로 목표와 액션 아이템을 정해요. 저희 팀은 배포 시간 단축과 배포 자동화라는 두 가지의 목표를 가지고 있었고, 이 목표를 달성하기 위한 액션 아이템으로 ECK 기반의 검색 클러스터를 도입하고 배포 파이프라인을 만들었어요. 목표 달성률은 7~80% 정도로 100% 달성을 하진 못했지만, 달성하기 어려운 공격적인 목표를 설정하는 게 목적인 OKR의 특성을 고려하면, 나쁘지 않은 결과라고 생각해요.

다음 단계

물론, 아직 해야 할 일들이 많이 남아있어요. 쿠버네티스를 도입하면서 검색 클러스터의 기반 인프라가 완전히 달라졌기 때문에 팀 내부적으로도 계속해서 학습하고 적응해 나가야 해요. 또한, 달라진 기반 인프라를 바탕으로 누구나 더 쉽게 배포할 수 있고 운영할 수 있을 때까지 앞으로 계속해서 고도화해 나갈 계획이에요. 아래는 다음 단계로 작업을 계획하고 있는 액션 아이템들이에요.

  • HPA 기반의 Elasticsearch Autoscaling 적용
  • 데이터 노드 재시작시 Warm Up 적용
  • 쿠버네티스 인스턴스를 Graviton 으로 교체
  • 엘라스틱서치 버전 8.7.0 업그레이드

함께해요

당근마켓 검색 플랫폼 팀은 앞으로도 쿠버네티스 기반의 검색 엔진을 보다 효율적으로 운영하기 위해 끊임없이 개선해 나가며, 당근마켓 검색의 수많은 트래픽을 잘 소화하고 더 좋은 검색 결과를 제공할 수 있는 튼튼한 플랫폼을 만들기 위해 꾸준히 노력해 나갈 거에요. 이러한 저희의 여정에 함께하실 분을 찾고 있어요. 많은 관심 부탁드려요.

🥕 Software Engineer, Backend - 검색 플랫폼

읽어주셔서 감사해요!

--

--