루다 선톡을 대비하는법

트래픽이 갑자기 증가할 때 어떻게 대응할까요?"
Oct 19, 2023
루다 선톡을 대비하는법
먼저 메시지를 보내는 기능(aka 선톡)은 서비스의 중요한 기능 중 하나로, 루다를 조금 더 사람처럼 느껴질 수 있도록합니다. 이 때문에 선톡을 보내면 순간적으로 많은 사용자가 루다와 대화하려고 하죠.
핑퐁팀은 순간적으로 사용자가 증가하는 상황에서도 안정적인 서비스 운영을 하기 위해 다양한 노력을 하고 있습니다. 이번 글에서는 선톡을 보내기 전에 증가할 트래픽에 대비하는 방법에 대해 설명하겠습니다.

기존 시스템의 문제점


선톡으로 인해 사용자가 증가하게 되면 서버는 부하가 높아지게 됩니다. 이를 방치한다면 사용자가 예상치 못한 장애를 겪을 수 있기 때문에 적절한 대응을 해야 합니다. 하지만 다음과 같은 이유로 기존 시스템에서는 대응할 수 없었습니다.

불규칙한 선톡 전송

루다는 다양한 상황에서 선톡을 전송하고 있습니다. 사용자가 가입한 지 일주일이 지난 경우나 기념일에 선톡을 보내기도 하며, 루다 모델이 선톡을 생성하기도 합니다.
선톡이 발송될 경우 메세지를 받는 사용자 수와 응답률에 따라 트래픽이 갑자기 증가하여 서버가 큰 부하를 받습니다. 이러한 부하에 대응하기 위해 서버를 적절히 스케일아웃 해야하지만, 선톡을 받는 사용자 수와 선톡이 나가는 시간이 불규칙하기 때문에 고정적인 스케일아웃 전략을 설정하기 어렵습니다.

기존 HPA의 문제

사용자가 증가할 때 스케이아웃하는 가장 좋은 방법은 쿠버네티스가 제공하는 HPA(Horizontal Pod Autoscaler)를 사용하는 것입니다. HPA를 사용하면 특정 리소스 메트릭을 확인하고 임계점을 넘을 시 스케일아웃 할 수 있습니다. 핑퐁팀에서는 이미 [HPA와 Custom Metric Server](https://tech.scatterlab.co.kr/kubernetes-hpa-custom-metric/)를 사용하여 AutoScaling 정책을 관리해왔습니다.
그러나 선톡과 같이 트래픽이 짧은 시간 내에 급격하게 증가하는 경우, 기존 HPA 구성으로는 다음과 같은 이유로 효과적으로 대응하기 어려워집니다.
  • 현재 사용 중인 사용자 지정 메트릭은 **현재 RPS(초당 요청 수)**를 기준으로 하고 있습니다.
  • 모델 서버는 모델 가중치 다운로드로 인해 새로 배포되는 데 다른 서비스보다 더 많은 시간이 소요됩니다.
현재 RPS를 기준으로 하고 있기 때문에 트래픽이 급증한 후에야 서버들이 새로 배포되게 됩니다. 그러나 모델 서버는 배포에 시간이 오래 걸리기에, 이 시간동안 사용자들은 장애를 겪을 수 있습니다.

그 외 문제점

또 다른 문제로, 현재까지 Kubernetes에서는 Custom Metrics Server를 하나만 지원하고 있기 때문에 이미 Prometheus Metrics Server가 배포된 핑퐁팀 클러스터에 추가적인 Metrics Server를 배포할 수 없습니다.
또한 루다, 다온, 세중이는 다양한 모델 및 백엔드 서버로 구성되어 있고, 각 캐릭터가 선톡을 보내는 상황에서 서로 다른 서버가 각기 다른 트래픽을 받게 됩니다. 기존의 HPA 및 prometheus metric server로는 이러한 상황에서 적절한 스케일아웃 전략을 설정하기 어려웠습니다.

트래픽 증가 대응


기존 시스템으로는 선톡으로 인한 트래픽 증가에 대응하기 어려워서 다음과 같은 접근으로 문제를 해결하려 했습니다.
  1. 선톡을 발송하는 서비스에서 발송될 메시지 개수를 파악합니다.
  1. Prometheus Metrics으로 발송될 메세지 개수를 export합니다.
  1. 해당 메트릭을 기반으로 한 HPA를 구성합니다.
Prometheus Metrics Server에서 사용할 발송될 메시지 개수를 메트릭으로 활용하면 HPA마다 스케일링이 필요한 수를 계산하는 방식이 다를 수 있고, 기존 사용 중인 메트릭과의 충돌 가능성이 있는 등 유연한 대응이 어려워졌습니다. 이 때문에 추가적인 Custom Metrics Server를 배포하려 했지만, Kubernetes에서는 Custom Metrics Server를 하나만 지원하고 있어서 이를 배포하는 것은 불가능했습니다. 따라서 직접 HPA의 minReplicas 값을 수정하는 방식으로 Custom Resource를 활용하여 문제를 해결하였습니다.

Prescaler

기존 시스템으로는 선톡, 이벤트 등 예측가능한 요인으로 인한 트래픽 급증에 대응하기 어려워 Prescaler라는 새로운 시스템을 개발하게 되었습니다.
Prescaler는 3가지 개념으로 스케일링 전략을 정의합니다.
  • VirtualMetric VirtualMetric은 예측하고자하는 값을 정의합니다. 선톡 시스템에서는 RPS (초당 요청 수)를 정의하여 사용합니다.
  • VirtualMetricEvent VirtualMetricEvent는 VirtualMetric에 변화를 줄 수 있는 요인을 정의합니다. 선톡 시스템에서는 발송 예정 선톡 메세지 개수를 사용하고 있습니다.
  • Horizontal Pod Prescaler Horizontal Pod Prescaler는 예측한 VirtualMetric으로 어떤 서버가 얼마나 스케일아웃 되어야하는지를 정의합니다.

발송될 메시지 개수 파악

발송될 메시지 개수는 얼마나 많은 사용자가 접속할지를 예측할 수 있게 해주는 중요한 데이터입니다. 발송될 메시지 개수를 미리 알고 있으면 기존의 지표를 고려하여 어느 정도로 스케일링이 필요한지 판단할 수 있습니다.
기존에는 선톡 받을 유저를 검색하고 메시지를 발송하는 두 과정을 한 번에 처리하여 실제로 발송될 메시지를 예측하고 대응하기 어려웠습니다. 따라서 해당 두 과정을 10분 간격으로 분리하여, 유저를 검색하는 시점에 발송될 메시지 개수를 파악하고 대응할 시간을 만들었습니다.
또한 Prescaler에서 발송할 메시지의 개수를 파악할 수 있도록 이벤트 시작 시각, 이벤트 종료 시각 그리고 해당 이벤트에 대한 변숫값을 받을 수 있도록 아래와 같이 API를 열어두었습니다.
{ "items": [ { "start": 1700545800, "end": 1700545860, "values": { "scheduledMessage": 50 } } ] }
 

Custom Resource 정의

Custom Resource는 쿠버네티스에서 제공하는 익스텐션입니다. Custom Resource를 사용하면 구조화된 데이터를 저장하고 검색할 수 있기 때문에 Custom Controller와 결합하여 쿠버네티스가 기존에 제공하는 기능뿐만 아니라 개발자가 원하는 기능도 추가할 수 있습니다. 조금 더 자세한 내용이 필요한 경우 k8s 공식 문서를 참고해주세요
Custom Resource는 Custom Resource Definition을 통해서 정의해야 합니다. Custom Resource Definition은 다음 코드와 같이 Resource에 대한 정의만 하면 되기 때문에 큰 어려움 없이 작성할 수 있습니다.
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: testresource.test.pingpong.ai spec: group: test.pingpong.ai scope: Namespaced names: plural: testresource singular: testresource kind: TestResource versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: target: type: string replicas: type: integer
 
위와 같이 정의된 Custom Resource Definition은 쿠버네티스에 등록하면 아래와 같은 리소스를 사용할 수 있게 됩니다.
apiVersion: "test.pingpong.ai/v1" kind: TestResource metadata: name: test-resource spec: target: service-a replicas: 3
 
먼저 Custom Resource를 정의하는 데 필요한 기능을 정리하였습니다.
  • 기존의 HPA처럼 특정 Metric Source를 통해 스케일링할 값을 정할 수 있어야 합니다.
  • 이미 HPA에서 사용하고 있는 Custom Metrics Server 말고도 추가적인 Metric Source를 사용할 수 있어야 합니다.
  • 스케일링 될 Resource에 대한 관리가 간편해야 하고 중복으로 설정된 경우에도 전략을 정할 수 있어야 합니다.
위의 요구사항을 바탕으로 정의된 Custom Resource는 다음과 같습니다.
 

#### Virtual Metric

예측해야하는 어떠한 메트릭을 정의합니다. 이 리소스로 정의된 metric으로 HorizontalPodPrescaler가 스케일링합니다.
하나의 Resource에 대한 스케일링 전략을 다양한 곳에서 설정할 수 있기 때문에 mergeStrategy를 통해 중복되는 Resource들의 값에 대하여 병합 전략을 설정할 수 있어야 합니다.
apiVersion: prescaler.pingpong.ai/v1 kind: VirtualMetric metadata: name: rps spec: name: rps # 메트릭 이름 defaultValue: 0 # 기본 값 mergeStrategy: Sum # 리소스가 중복될 시 메트릭 병합 처리 전략
 

#### Virtual Metric Event

Virtual Metric의 이벤트를 정의합니다. 이벤트는 예측해야 하는 메트릭의 변화를 의미합니다.
이곳에서 Metric을 가져올 Source를 지정할 수 있습니다. 또한, 값에 대한 expression을 제공하여 각 서비스의 요구사항에 따라 다르게 값을 변경할 수 있어야 합니다.
apiVersion: prescaler.pingpong.ai/v1 kind: VirtualMetricEvent metadata: name: stage-vme spec: source: api: # 메트릭을 가져올 주소 url: https://stage.pingpong.ai/service-a/metrics metrics: - name: rps # 메트릭 이름 resource: # 적용 할 리소스 정보 name: service-a namespace: service-a value: expression: scheduledMessage * 2 # expression 사용도 가능하도록 정의 - name: rps resource: name: service-b namespace: service-b value: expression: scheduledMessage
 

#### Horizontal Pod Prescaler

autoscaling할 HPA 및 해당 HPA의 목표 metric을 지정합니다. 전체적인 구조는 HPA와 유사하지만, scaleTargetRef로 HPA를 지정합니다.
averageValue로 보장되어야 할 Metric을 지정할 수 있어야 합니다.
apiVersion: prescaler.pingpong.ai/v1 kind: HorizontalPodPrescaler metadata: name: service-a namespace: service-a spec: metrics: - metric: name: rps # 메트릭 이름 target: averageValue: 10 # 보장되어야 할 메트릭 scaleTargetRef: # 실제 변경 할 기존 HPA 정보 apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler name: service-a-hpa
 
이처럼 Custom Resource에 대한 정의를 했지만, 동작까지 하는 것은 아닙니다. Custom Resource에 대한 정의를 하게 되면 쿠버네티스에서 사용할 수 있는 리소스의 형태로만 존재합니다. 이 리소스가 실제로 동작하려면 Custom Controller를 통해 해당 리소스에 대한 구현을 직접 해주어야 합니다.
 

Prescaler 구현

Prescaler는 Custom Controller로 Custom Resource에 대한 관리와 실제 동작을 담당합니다. 지정된 시간마다 다음과 같은 작업으로 실제 스케일링을 진행하고 있습니다.
 

#### 현재 Virtual Metrics 가져오기 // # 4개 Jekyll에서 지원합니다

먼저 VirtualMetricsVirtualMetricEvent에 정의된 source를 통해 가져옵니다. 핑퐁팀 서비스의 Source Metric API는 이벤트 시작 시각, 이벤트 종료 시각 그리고 값으로 이루어진 타임시리즈로 반환할 수 있게 열어두었습니다. 가져온 Metric과 Resource에 설정한 기본값 및 expression에 대해서도 같이 계산을 진행해줍니다.
fun getCurrentMetrics(): List<Metric> { val virtualMetricEvents = virtualMetricEventService.list() return virtualMetricEvents.map { val value = virtualMetricEventSourceService.resolve(it.source) it.metrics.map { metric -> Metric( name = metric.name, resource = metric.resource, value = expressionService.resolve(metric.value, value) ) } } }
 

#### 데이터 병합

여러 VirtualMetricEvent를 사용하다 보면 같은 Resource를 각각다른 VirtualMetricEvent에 등록할 수도 있습니다. 상기에서 계산된 값과 mergeStrategy를 바탕으로 중복된 값을 처리하였습니다.
fun merge(metrics: List<Metric>): Map<Resource, Metric> { val virtualMetrics: List<VirtualMetric> = virtualMetricService.list() metrics.groupBy { it.resource }.mapValues { (_, metrics) -> mergeStrategyService.resolve(virtualMetrics, metrics) } }
 

#### 실제 값 계산

메트릭을 통해 Replicas로 설정할 수를 먼저 계산합니다. 계산 공식은 k8s 공식 페이지를 참고하였습니다.
desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]
가져온 모든 Prescaler에 대해 위의 공식을 바탕으로 반영되어야 할 minReplicas를 계산하였습니다.
fun calculateReplica(currentMetrics: Map<Resource, List<Metric>>): Map<Resource, Int> { val prescalers = horizontalPodPrescalerRepository.list() return prescalers.associate { prescaler -> val metrics = currentMetrics.getValue(prescaler.metadata).associateBy { it.name } val replicas: Int = prescaler.metrics.maxOf { metric -> val currentMetric = metrics.getValue(metric.name) val targetMetric = metric.value calculateReplica(currentMetric, targetMetric) } prescaler.metadata to replicas } } fun calculateReplica( currentMetric: Metric, targetMetric: Double ): Int { return ceil(currentMetric.value / targetMetric).toInt() }
 

#### HPA 값 변경

이제 HPA마다 배포되어야 할 Replica 개수가 정해졌으므로 실제 배포를 해줍니다. 다만 Prescaler는 급격히 트래픽이 증가할 때를 맞춰 설계되었으므로 일반적인 상황에서는 기존 HPA의 규칙을 따라야 합니다.
Prescaler는 현재 상태를 따로 저장하고 있지 않기 때문에 기존 Min Replicas 정보를 HPA의 메타데이터 어노테이션에 저장하고, 만약 이보다 작을면 기존 HPA 설정을 따라가도록 하였습니다.
 

Prescaler 배포

Prescaler는 배포를 위해 Helm Chart를 사용하였습니다. 다만 몇 가지 유의해야 할 점들이 있습니다.
Prescaler는 앞서 정의한 Custom Resource인 HorizontalPodPrescalers, VirtualMetrics, VirtualMetricEvents에 대한 Get, List 권한이 필요하고 실제 HPA의 minReplica를 변경해야 하기 때문에 HorizontalPodAutoscalers에 대한 Get, Update 권한을 지정해주어야 합니다.
따라서 ClusterRole을 추가하여 해당 권한에 대해 지정해주었습니다.
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "prescaler.name" . }} rules: - verbs: - get - list apiGroups: - prescaler.pingpong.ai resources: - horizontalpodprescalers - virtualmetrics - virtualmetricevents - verbs: - get - update apiGroups: - autoscaling resources: - horizontalpodautoscalers
 
만든 ClusterRole은 ServiceAccount를 생성한 후 ClusterRoleBinding을 통해 바인딩 하였으며 Deployment에서 사용하도록 처리하였습니다.
{% raw %} # service-account.yaml {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "prescaler.serviceAccountName" . }} labels: {{- include "prescaler.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} {{- end }} --- # cluster-role-binding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ include "prescaler.name" . }} namespace: {{ .Release.Namespace }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ include "prescaler.name" . }} subjects: - kind: ServiceAccount name: {{ include "prescaler.serviceAccountName" . }} namespace: {{ .Release.Namespace }} --- # deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "prescaler.fullname" . }} labels: {{- include "prescaler.labels" . | nindent 4 }} spec: template: metadata: ... spec: serviceAccountName: {{ include "prescalerpr.serviceAccountName" . }} ... {% endraw %}

마무리


지금까지의 과정을 통하여 선톡으로 인한 급격한 사용자 증가에 상황에 대응할 수 있게 되었습니다. 물론 글에서 다루지는 않았지만, 현재 선톡 응답률에 따라서 Prescaler target value및 Event expression또한 조정하였습니다.
이처럼 Custom Resource와 Custom Controller를 이용하면 쿠버네티스가 지원하는 기능뿐만 아니라 더 다양한 것들을 쉽게 구현할 수 있으니 여러 문제상황에서 사용해보시는 것을 추천해 드립니다.
핑퐁팀은 더욱 더 안정적인 서비스를 구축하기 위해 같이 고민하실 분들을 기다리고 있습니다. 저희와 같이 고민하고 싶으신 분들은 [채용공고](https://scatterlab.co.kr/recruiting/)를 확인해주세요!
 
 
Share article

Scatter Lab