Kubernetes-native 로그 플랫폼

carl
WATCHA
Published in
10 min readAug 2, 2023

WATCHA server-platform팀에서 서버를 개발하고 있는 rogi, carl이라고 합니다. 개발한 로그 플랫폼에 대해 공유하고자 합니다.

개요

WATCHA에는 이미 잘 설계된 로그 플랫폼이 개발되어 사용 중인 바 있습니다. 그러나 시간이 지나면서 개발/비개발 양쪽 모두의 상황이 달라짐에 따라 기존 로그 플랫폼이 설계되었던 당시와는 다른 요구사항들이 생겼고, 그에 따른 개선이 필요하게 되었습니다. 요구사항이라 함은 크게 아래와 같은 것들이 있습니다.

  1. 서비스별 로그 플랫폼과의 통합에 소모되는 비용 해소

WATCHA는 지난 몇 년간 Kubernetes를 기반으로 한 MSA 환경을 적극 도입함에 따라, 현재는 Kubernetes 클러스터 내 수십 개의 서비스들이 동작하고 있습니다. 기존 로그 플랫폼은 Kubernetes 환경을 고려하지 않은 상태로 설계되었고, Kubernetes 도입 이후 로그 플랫폼 agent를 sidecar 패턴을 적용하여 사용하고 있었습니다. 이 경우 로그 플랫폼 agent의 설정 및 구성은 각 서비스 개발자의 책임이 되므로 로그 플랫폼이 동작하는 환경을 자세히 알고 있지 않은 서비스 개발자의 입장에서는 로그 플랫폼과의 통합에 불필요한 비용을 사용하게 되는 결과를 낳게 되었습니다. 개선된 로그 플랫폼에서는 로그 플랫폼 agent를 daemonset 형태로 배포하고 각 서비스들은 pod annotation을 통해 쉽게 로그 플랫폼과 통합 가능한 형태로 구성함으로써 이러한 문제점을 해소하고자 하였습니다.

2. 확장에 좀 더 열려있는 로그 구조로의 이전

기존 로그 플랫폼은 로그 구조를 protobuf로 정의하여 사용하고 있었습니다. 이에 따라 자체적인 IDL, 유연한 schema 발전, 효율적인 serialization 등의 장점을 누릴 수 있었지만, 반대로 JSON 등을 로그 구조로 사용하는 서드파티 플랫폼과의 통합 불가, 개발자가 직접 읽을 수 없는 형태의 raw 로그, 이에 따라 발생한 로그 사용처에 따른 로그 파편화 등의 단점도 직면하게 되었습니다. 개선된 로그 플랫폼에서는 protobuf를 내부적인 로그 구조로 사용하긴 하지만, 이를 기반으로 한 다양한 형태의 serialization을 지원함으로써 이러한 문제점을 해소하고자 하였습니다.

3. 지나친 BigQuery 의존성 제거 및 로그 성격에 따른 로그 처리 파이프라인 분리

기존 로그 플랫폼은 서버 요청 로그 뿐만 아니라 서버 이벤트 로그, 클라이언트 이벤트 로그, 데이터베이스 스냅샷 등의 데이터들을 전부 BigQuery로 통합하여 다양한 조합의 분석이 가능케 하는 것을 목표로 설계되었었습니다. 이러한 구조는 두 가지의 의도치 않은 문제점을 낳았는데, 하나는 데이터 분석과는 직접적인 연관이 없는 플랫폼 성격의 서버 로그들도 전부 BigQuery로 직접 전송되어 비용 및 유연성의 측면에서 비효율이 발생하는 점, 다른 하나는 서버 개발자가 매 건 명시적인 의도를 갖고 저장하는 것이 아니므로 따라서 semantic이 고정되어 있지 않은 서버 요청 로그를 데이터 분석에 직접 사용하게 되면서 서버의 기술 스택 및 미들웨어 구성과 데이터 분석 쿼리 간에 커플링이 발생하는 점이었습니다. 개선된 로그 플랫폼에서는 서버 로그를 성격에 따라 서버 이벤트 로그와 서버 요청 로그로 구분하고, 후자를 BigQuery에 저장, 전자를 ClickHouse에 저장하고 선택적으로 BigQuery로 migration할 수 있는 형태로 구성함으로써 이러한 문제점을 해소하고자 하였습니다.

일반적으로는 기존 로그 플랫폼을 전술한 요구사항을 만족하도록 확장하는 것이 합리적이겠으나, 아키텍처, 데이터베이스, 프로토콜 등 많은 부분에서 기술 스택의 변경이 필요했으므로 새로 구현하는 것이 좋겠다고 판단하였습니다.

구현

최종적으로 구현된 구성은 아래와 같습니다.

overall architecture

개발된 로그 플랫폼은 크게 discovery, agent, server, ingester 네 개의 하위 서비스로 구성됩니다. discovery는 서비스의 로그 플랫폼 통합 설정을 감지하여 agent를 위한 설정을 생성하는 역할, agent는 discovery가 생성한 설정을 기반으로 로그 파일을 읽어 Message Queue로 로그 메시지를 전송하는 역할, server는 agent가 전송한 로그 메시지를 파싱 및 후가공하여 스토리지로 저장하는 역할, ingester는 스토리지에 저장된 로그 파일을 쿼리 가능한 형태의 데이터베이스로 저장하는 역할을 합니다. agent — server 간 로그 메시지 전송을 위한 Message Queue로는 NATS JetStream을 사용하였고, 로그는 server에 의해 S3로 저장된 이후 ingester를 통해 ClickHouse로 저장됩니다. 최종 사용자는 Redash를 통해 로그를 다양한 패턴으로 쿼리할 수 있습니다.

discovery

discovery architecture

discovery는 서비스의 로그 플랫폼 통합 설정을 감지하여 agent를 위한 설정을 생성하는 역할을 합니다. 로그 플랫폼 통합 설정은 pod annotation의 형태로 제공되고, agent를 위한 설정은 configmap의 형태로 제공합니다. pod 이벤트를 받아 annotation의 변경을 추적하며 그 변경에 따라 configmap을 수정하도록 구현되었고, Kubernetes informer/leaderelection 패턴을 기반으로 Kubernetes control plane에 큰 부하를 주지 않으며 고가용성을 갖도록 구성되었습니다.

로그 플랫폼 통합 설정은 pod 단위로 이루어지지만 로그 수집은 node단위로 이루어지므로 discovery는 pod annotation들을 모아 node당 하나의 configmap을 생성합니다. configmap data는 로그 수집을 위한 기본 정보 및 해당 로그에 대한 부가적인 태그 정보로 구성됩니다. 태그는 각 서비스 개발자가 pod annotation의 형태로 제공 또는 discovery가 autodiscover하여 제공합니다. pod annotation은 go template 형태로 pod 정보들을 참조하여 유연한 방식으로 로그 플랫폼 통합 설정을 구성할 수 있도록 설계되었습니다.

agent

agent architecture

agent는 discovery가 생성한 설정을 기반으로 로그 파일을 읽어 Message Queue로 로그 메시지를 전송하는 역할을 합니다. Fluent Bit를 기반으로, 개발한 NATS JetStream output plugin을 붙여 실제 로그를 전송하는 container와 Kubernetes informer 패턴을 기반으로, configmap 이벤트를 받아 그 변경에 따라 Fluent Bit 설정을 생성하는 container 두 개로 구성되어 있습니다. daemonset 형태로 배포되므로 Kubernetes 클러스터 내 모든 node에 배포되고, 따라서 모든 서비스들은 pod annotation 설정만으로 언제든 로그를 수집할 수 있습니다.

server

server architecture

server는 agent가 전송한 로그 메시지를 파싱 및 후가공하여 스토리지로 저장하는 역할을 합니다. NATS JetStream에서 지속적으로 로그 메시지를 fetch하고 후가공하여 S3로 저장하도록 구현되었습니다. 내부적인 로그 구조로 protobuf를 사용한 까닭은 각 서비스들과 로그 플랫폼 server 간 불필요한 커뮤니케이션 없이 로그 schema를 통일하기 위함이고, 이 시점에서 server는 쉽게 로그 schema가 유효함을 판단하여 로그를 JSON, CSV, Parquet 등의 범용적인 형태로 변환하여 저장합니다.

로그 플랫폼 개발자는 유연한 schema 발전을 지원하는 protobuf의 장점을 십분 활용하여 서비스 애플리케이션과는 최대한 coupling을 생성하지 않는 방식으로 최종 로그 schema를 변경할 수 있습니다. 이를테면 언제든 로그 schema의 필드를 추가 및 제거할 수 있고 기존 필드를 제거, 새로운 필드를 추가하는 방식으로 필드의 데이터 타입을 쉽게 변경할 수도 있는데, 이 때 로그 플랫폼과 서비스 애플리케이션의 배포 사이클을 다르게 가져갈 수 있습니다. 결국 최종 로그 schema는 로그 플랫폼 server에 의해 결정되므로 이후 로그 파이프라인은 서비스 애플리케이션과는 의존성 없이 독립적인 방식으로 구성할 수 있습니다.

ingester

ingester architecture

ingester는 스토리지에 저장된 로그 파일을 쿼리 가능한 형태의 데이터베이스로 저장하는 역할을 합니다. S3 로그 스토리지 버킷에 오브젝트 생성 이벤트가 발생하는 경우 해당 이벤트는 SNS를 통해 SQS에 enqueue되는데, ingester는 SQS에서 이벤트를 지속적으로 dequeue하며 이벤트 대상 파일을 읽어 table schema 형태로 변환하여 ClickHouse로 저장하도록 구현되었습니다. 이전 로그 파이프라인은 각 환경 Kubernetes 클러스터들에 독립적으로 구성된 반면, ingester 및 이후 로그 파이프라인은 공통 환경 Kubernetes 클러스터에 여러 환경의 로그를 모아 저장할 수 있도록 구성되었습니다.

배포 및 모니터링

개발된 로그 플랫폼은 Helm을 이용하여 패키지를 생성, Terraform을 이용하여 배포합니다.

로그 플랫폼의 동작은 Prometheus 및 Grafana를 통해 모니터링하고 있습니다. 플랫폼을 구성하는 workload들의 리소스 레벨 metric 뿐만 아니라 서비스들의 애플리케이션 레벨 metric들도 수집하고 있으므로 여러 가지 레벨에서 플랫폼의 동작을 확인할 수 있습니다.

결과

Kubernetes-native한 방식을 통해 로그 플랫폼과의 통합을 용이하게 하는 것을 주요한 목표 중의 하나로 설계하였으므로 로그 플랫폼이 배포된 이후 여러 서비스들이 빠르게 로그 플랫폼과 통합될 수 있었습니다. 수집된 로그를 Redash를 통해 다양한 패턴으로 쿼리하여 대시보드를 구성하였고, 이를 통해 이전에 비해 서비스 애플리케이션 로그의 이상징후를 좀 더 빠르게 확인할 수 있는 환경을 만들 수 있게 되었습니다. 뿐만 아니라 하나의 로그 구조를 통해 사용 중인 다른 서드파티 플랫폼과의 통합 및 개발자가 직접 서버에 접근하여 로그를 live tail하는 것 또한 가능해졌고, 불필요한 로그의 BigQuery전송도 정리되어 전반적인 로그 플랫폼 서버 비용이 감소했습니다.

Redash dashboard

현재의 구성이 최선은 아닐 수 있습니다. 다만 당장의 상황에 가장 알맞은 구성이라고 판단하였고, 로그 플랫폼의 외부와 접하는 인터페이스를 최대한 간단하면서도 유연한 형태로 설계하였으므로 리팩토링에 얼마든지 열려있는 상태입니다. 이후 로그 플랫폼을 더 발전된 형태로 개발하여 공유드릴 수 있기를 바랍니다.

--

--