LY Corporation Tech Blog

LY Corporation과 LY Corporation Group(LINE Plus, LINE Taiwan and LINE Vietnam)의 기술과 개발 문화를 알립니다.

신뢰성 향상을 위한 SLI/SLO 도입 2편 - 플랫폼 적용 사례

시작하며

안녕하세요. Enablement Engineering 팀에서 SRE(site reliability engineer)로 일하고 있는 어다희입니다.

저희 팀은 LINE 서비스를 보다 높은 성능으로 효율적이고 안전하게 사용자에게 제공할 수 있도록 다방면에서 'Enablement'를 지원하는 역할을 수행합니다. 구체적으로 미디어 플랫폼에 대한 사이트 안정성 엔지니어링(Site Reliability Engineering)과 더불어 트래픽의 시작점이라고 할 수 있는 GSLB(global server load balancing)와 CDN(content delivery network) 관련 업무를 담당하고 있습니다.

이번 글에서는 신뢰성 향상을 위한 SLI/SLO 도입 1편 - 소개와 필요성에 이어서 미디어 플랫폼에 SLI/SLO를 정의해 운영 업무에 적용한 후기를 공유해 보려 합니다. 

서비스가 아닌 플랫폼에 SLI/SLO를 도입하기

LY에는 LINE 및 LINE 패밀리 서비스에서 사용되는 사진과 동영상 등을 저장 및 가공하고 딜리버리하는 미디어 플랫폼인 OBS가 있습니다. OBS는 LINE의 많은 서비스 중 미디어를 사용하는 대부분의 서비스에서 사용하는 대표 플랫폼 중 하나입니다. 예를 들어 LINE 앱 대화방에서 주고받는 미디어 메시지는 전부 OBS를 사용합니다. 그러므로 이 플랫폼의 신뢰성(reliability)은 굉장히 중요하며, 최종 사용자에게 더 좋은 품질의 서비스를 제공할 수 있도록 SLI(service level indicator)/SLO(service level objective)를 정의하게 되었습니다.

대부분의 SLI/SLO는 '서비스'를 기준으로 설정합니다. 사용자 입장에서 신뢰성을 측정하기 위해서는 서비스 내부에서 사용되는 '플랫폼'보다는 서비스를 기준으로 측정하는 것이 용이하기 때문입니다. 하지만 OBS의 경우 약 160개에 이르는 다양한 LINE 서비스에서 사용하고 있으며, 이 플랫폼의 문제는 곧 각 서비스의 문제로 직결될 수 있기 때문에 별도로 SLI/SLO를 정의하게 되었습니다.

플랫폼의 CUJ를 어떻게 정의할 것인가?

위와 같은 배경으로 시작했으나 CUJ(critical user journey) 설정부터 서비스와 다른 벽에 부딪혔습니다. CUJ에 대한 자세한 설명은 1편에서 확인할 수 있는데요. 이 글을 시작으로 접하시는 분들을 위해 간단히 설명하면 아래와 같습니다.

  • 사용자가 서비스를 사용하는 과정 혹은 흐름을 정의한 것을 '사용자 여정(user journey)'이라고 부르며, 사용자 여정 중 핵심 기능 및 서비스에 대한 여정을 '핵심 사용자 여정(critical user journey, CUJ)'이라고 부릅니다.
  • 동영상 서비스에서 '사용자가 서비스에서 동영상을 재생하기 위해 거치는 과정'이나, 메시징 서비스에서 '사용자가 메시지를 전송하기 위해 거치는 흐름' 혹은 '친구를 추가할 때 거치는 과정' 등이 사용자 여정입니다.

플랫폼은 API를 통해 기능을 제공하는데 이를 각 서비스에서 어느 로직에 어떻게 사용하는지 일일이 확인하는 것은 굉장히 까다롭고 어렵습니다. 그래서 1차적으로 주요 API 자체를 CUJ로 설정해 각 기능별 SLI/SLO를 설정하기로 했습니다. 

OBS에서 제공하는 다양한 기능 중 이 글에서는 세 가지 주요 기능만 간단히 소개하겠습니다.

API설명
DOWNLOADCDN 또는 OBS로부터 오브젝트를 다운로드하기 위한 API
UPLOADOBS에 오브젝트를 업로드하기 위한 API
OBJECT_INFOOBS에 저장된 오브젝트의 메타 정보를 조회하는 API

1편에서 말씀드린 것처럼 SLI로 사용할 수 있는 메트릭에는 가용성(availabiliity), 처리량(throughput), 대기 시간(latency) 등이 있습니다. SLI/SLO 측정 원칙에 따라 각 CUJ별 특성에 맞게 아래와 같이 한두 개 정도의 SLI를 선정했습니다.

APISLI
DOWNLOAD가용성(+처리량)
UPLOAD가용성(+처리량)
OBJECT_INFO가용성, 대기 시간

최초에는 처리량도 DOWNLOAD와 UPLOAD API의 SLI로 사용하기로 했지만 결론적으로 제외했습니다. 이유는 두 API의 다음과 같은 특성 때문입니다.

  • 미디어 유형이 오디오, 파일, 이미지, 비디오로 다양합니다.
  • 한 번에 업/다운로드되는 미디어 크기 역시 다양합니다.

위와 같은 특성 때문에 서비스별로 유형과 크기에 따른 성능 편차가 심해 일관된 기준을 적용하기 어려웠습니다. 그래서 처리량을 측정하기는 하지만 SLI에서는 제거해 참고 메트릭으로만 활용할 수 있도록 설정했습니다.

SLI/SLO 메트릭 수집하기

SLI/SLO 메트릭 수집은 모든 요청과 응답이 통과하면서 사용자와 가장 가까운 곳인 API 게이트웨이와 같은 엔드포인트에서 수집하는 것이 가장 이상적입니다. 하지만 OBS 특성상 서비스에서 SLI/SLO 관련 메트릭을 수집하기 어려운 상황이었기에 로그에서 필요한 메트릭을 생성해 사용하기로 했습니다.

메트릭 수집 아키텍처는 KafkaVector, Prometheus를 사용해 아래와 같이 설계했습니다.

위 아키텍처의 전체적인 흐름은 다음과 같습니다. 

  • OBS 서버에서 출력되는 실시간 로그를 메시지 큐인 Kafka로 보내 메시지를 생산합니다.
    • OBS는 매우 많은 요청과 트래픽을 처리하는 대규모 글로벌 서버를 보유하고 있습니다. 이에 따라 출력되는 실시간 로그의 양 또한 굉장히 많기 때문에 Kafka를 사용해 이를 처리합니다.
  • Kafka에서 생산된 로그를 관찰가능성(observability) 파이프라인인 Vector가 소비해 SLI/SLO에 필요한 메트릭으로 변환합니다.
  • Vector가 변환된 메트릭을 익스포트하면 Prometheus가 이를 수집해 저장합니다.
    • SLI/SLO는 1개월 이상의 장기 데이터가 필요하며 쿼리가 복잡하기 때문에 Recording Rules를 적용해 성능을 최적화했습니다.
  • 마지막으로 이 데이터 소스를 Grafana에서 조회해 시각화합니다.

전체 흐름은 간단해 보이지만 구현하면서 많은 시행착오를 거쳤는데요. 그중 두 가지를 공유하겠습니다. 

로그양이 너무 많아요! Vector 최적화

SLI/SLO의 궁극적인 목적은 사용자가 불편함 없이 신뢰를 느끼며 서비스를 사용하고 있는지 측정하는 것입니다. 따라서 메트릭을 실시간으로 수집해야 하며, SLI/SLO와 오류 예산(error budget) 수치가 설정한 기준보다 떨어질 경우 알람을 보내 담당자가 대응할 수 있도록 만드는 게 중요합니다. 하지만 하루 약 350TB 수준의 많은 로그 때문에 Vector가 Kafka에서 소비하는 과정에 문제가 발생했습니다.

여기서 잠시 Vector를 처음 들어보는 분들을 위해 간단히 설명하면, Vector는 DataDog의 오픈소스 프로젝트로 홈페이지에서 Vector를 'A lightweight, ultra-fast tool for building observability pipelines'라고 소개하고 있습니다. 이를 직역하면 '관찰가능한(observability) 파이프라인을 구축하기 위한 가볍고 매우 빠른 도구'라고 할 수 있는데요. 모든 로그와 메트릭, 트레이스를 수집하고(sources), 변환해서(transforms), 라우팅하는(sinks)하는 역할을 담당합니다.

기존에 저희 팀에는 앞서 Vector를 활용해 멀티 CDN 로그 및 트래픽 관리하기라는 글에서 소개했던 Vector 클러스터가 존재했습니다. 이 클러스터가 더 많은 로그를 처리할 수 있도록 아래와 같이 고도화하고 확장했으며, 기존에 클러스터 하나로 운영하던 것을 대규모 처리를 위해 두 개로 증설했습니다. 

위 아키텍처의 전체적인 흐름은 다음과 같습니다.

  • 트랜잭션 클러스터(Transaction Cluster)
    • Filter를 활용해 Kafka로부터 받은 수많은 OBS 로그 중 SLI에 필요한 로그만 조건에 맞게 필터링
    • Remap을 활용해 원하는 형태로 변형
    • Log to metric을 활용해 가용성(availability), 처리량(throughput), 대기 시간(latency)을 측정하기 위한 메트릭으로 변환
    • 변환된 메트릭을 Vector의 Sinks 중 하나인 Vector를 활용해 애그리게이트 클러스터로 전달
      • Vector의 Sinks는 Vector에서 외부 서비스로 데이터를 전송하는 역할
  • 애그리게이트 클러스터(Aggregate Cluster)
    • 트랜잭션 클러스터로부터 받은 메트릭 볼륨을 Aggregate를 활용해 감소
    • prometheus_exporter를 활용해 SLI에 필요한 메트릭을 내보내면 Prometheus가 가져가 저장 

메트릭 최적화

Vector를 최적화해 메트릭이 실시간으로 밀림 없이 생성되면서 SLI/SLO 대시보드 생성에 필요한 모든 준비가 끝났다고 생각했습니다. 하지만 이는 저의 희망 사항이었을 뿐이었습니다. 대시보드에서 일주일 치 메트릭을 조회하면 로딩하는 데 1분 정도 걸렸기 때문입니다. 그도 그럴 것이 메트릭의 카디널리티(cardinaliry)가 높은데 PromQL(prometheus query)마저 매우 복잡했습니다.

이를 해결하기 위해 Recording Rules를 적용했습니다. Recording Rules는 자주 사용하거나 계산에 많은 비용이 드는 표현식을 미리 계산해 그 결과를 별도의 메트릭으로 저장하는 기능입니다. 미리 계산된 결과를 쿼리하면 필요할 때마다 원래 표현식을 실행하는 것보다 훨씬 빠르게 결과를 얻을 수 있습니다.

저희는 SLI/SLO와 오류 예산에 필요한 표현식을 Recording Rules를 이용해 미리 집계 후 저장해서 메트릭 쿼리의 성능을 높일 수 있었습니다. 아래는 Recording Rules를 정의한 코드의 일부를 가져온 것입니다.

  • obs-rules.yaml
groups:
  - name: obs
    rules:
    - record: obs_download_availability
      expr: |
        sum by (service_code, space_id) (rate(obs_client_http_status{api="DOWNLOAD", error_code="null"}[1m]))
        /
        sum by (service_code, space_id) (rate(obs_client_http_status{api="DOWNLOAD"}[1m]))
    - record: obs_download_throughput_p50
      expr: | 
        histogram_quantile(0.50, sum by(le, service_code, space_id) (rate(obs_client_http_throughput_summary_bucket{api="DOWNLOAD""}[1m])))
    - record: obs_download_throughput_p90
      expr: | 
        histogram_quantile(0.90, sum by(le, service_code, space_id) (rate(obs_client_http_throughput_summary_bucket{api="DOWNLOAD"}[1m]))) 
    - record: obs_download_throughput_p99
      expr: | 
        histogram_quantile(0.99, sum by(le, service_code, space_id) (rate(obs_client_http_throughput_summary_bucket{api="DOWNLOAD"}[1m]))) 
    # ... 중략 ...
    - record: obs_object_info_latency_p50
      expr: | 
        histogram_quantile(0.50, sum by(le, service_code, space_id) (rate(obs_client_http_duration_seconds_bucket{api="OBJECT_INFO"}[1m])))
    - record: obs_object_info_latency_p90
      expr: | 
        histogram_quantile(0.90, sum by(le, service_code, space_id) (rate(obs_client_http_duration_seconds_bucket{api="OBJECT_INFO"}[1m])))
    - record: obs_object_info_latency_p99
      expr: | 
        histogram_quantile(0.99, sum by(le, service_code, space_id) (rate(obs_client_http_duration_seconds_bucket{api="OBJECT_INFO"}[1m])))

Recording Rules를 적용한 메트릭은 크게 세 가지로 구분됩니다.

  • 가용성(availability)
    • 가용성은 (성공 요청 수 / 전체 요청 수) x 100%로 계산합니다.
    • Vector를 통해 생성된 메트릭에 위 계산식을 적용합니다.
  • 처리량(throughput)
    • 처리량은 Vector의 log_to_metric에서 히스토그램 타입으로 생성됩니다.
    • 여기에 50퍼센타일, 90퍼센타일, 99퍼센타일을 계산해 적용합니다.
  • 대기 시간(latency)
    • 대기 시간은 처리량과 유사하게 Vector의 log_to_metric에서 히스토그램 타입으로 생성됩니다.
    • 여기에 50퍼센타일, 90퍼센타일, 99퍼센타일을 계산해 적용합니다.

다음은 오류 예산 계산에 사용된 PromQL의 Recording Rules 적용 전/후 예시입니다. 적용 후 PromQL이 한결 간단해진 것을 확인할 수 있습니다. 가독성뿐 아니라 미리 계산된 메트릭을 사용하면서 쿼리 조회 성능 또한 개선됐습니다.

  • Recording Rules 적용 전
    (($slo_period*24*60*(1-$slo_object_availability))
    -
     (
      count_over_time(
        (
         (
          (
          sum(rate(obs_client_http_status{api="DOWNLOAD", service_code=~"$service_code", space_id=~"$space_id", error_code="null"}[$__interval]))
          /
          sum(rate(obs_client_http_status{api="DOWNLOAD", service_code=~"$service_code", space_id=~"$space_id"}[$__interval]))
          )
         ) < ($sli_criterion_availability / 100)
        )[${slo_period}d:1m]
      ) or on() vector(0)
     )
  • Recording Rules 적용 후
    (($slo_period*24*60*(1-$slo_object_availability))
    -
    (
      (count_over_time((obs_download_availability{service_code=~"$service_code", space_id=~"$space_id"} < ($sli_criterion_availability / 100))[${slo_period}d:1m]))
      or on() vector(0)
    ))

SLI/SLO 활용 방법

저희는 구현된 메트릭을 활용해 다음과 같이 요약용과 각 CUJ별로 대시보드를 생성했습니다.

SLI 요약OBJECT_INFO CUJ

SLI 요약 대시보드에서는 현재 서비스에 특이 사항이 있는지 한 눈에 파악할 수 있고, CUJ별 상세 대시보드에서는 SLI/SLO/오류 예산뿐 아니라 요청수나 발생 오류에 대해 보다 세부적으로 확인할 수 있습니다.

각 CUJ별로 목표한 SLO에 맞춰 알람을 설정해 뒀기 때문에 플랫폼을 다양한 서비스에서 사용하는 상황에서도 어느 서비스, 어느 기능에서 사용자가 불편을 겪는지 확인할 수 있습니다.

알람은 Slack과 연동해 놓았으며, 다음과 같이 대시보드에 들어가지 않아도 상황을 확인할 수 있도록 스레드 댓글로 관련 패널을 캡처해 보여주도록 설정했습니다.

알람이 발생하며 오류 예산이 계속해서 줄어드는 경우 즉각적으로 문제의 원인을 찾아 해결해야 하지만, 오류 예산이 허용된 범위에 있는 경우라면 긴급성을 판단해 대응 수준을 선택할 수도 있습니다. 이는 운영 피로도를 줄이는 데 많은 도움을 줄 수 있습니다. 또한 보다 장기적으로는 오류 예산의 상태에 따라 신규 기능 개발과 안정성 중 어느 것에 더 초점을 맞출지 결정할 수 있습니다.

향후 방향

향후 방향은 'OBS SLI/SLO 측면'과 제가 속한 팀의 역할인 '미디어 플랫폼 SRE 측면'의 두 가지로 나눠볼 수 있을 것 같습니다.

OBS SLI/SLO 측면에서 현재 상태는 OBS의 자체적인 SLI/SLO를 정의한 단계로 아직 플랫폼을 사용하는 서비스에까지는 공유되지 않았습니다. 따라서 향후 플랫폼을 사용하는 서비스들에게 이를 알리고 함께 활용함으로써 사용자에게 더 신뢰성 높은 서비스를 제공할 수 있는 방향으로 나아가고자 합니다.

미디어 플랫폼 SRE 측면에서는 OBS 외에 다양하게 존재하는 미디어 플랫폼에 SLI/SLO를 적용하는 것입니다. 단순히 플랫폼 하나에서 끝내지 않고 LY 내 다양한 서비스와 플랫폼에 적용함으로써 LY 서비스의 신뢰성에 대해 공통의 언어로 이야기할 수 있는 방향으로 나아갈 것입니다.

마치며

이 프로젝트는 사용자 입장에서 고민하면서 SRE 역할에 대해 다시 한번 돌이켜보고 마음을 다잡는 계기가 되었습니다. 그뿐만 아니라 Vector를 활용해 멀티 CDN 로그 및 트래픽 관리하기에서 소개했던 Vector 클러스터를 더욱 확장해 CDN과 OBS에 적용함으로써 관련 노하우도 많이 쌓을 수 있었습니다.

저희는 OBS 외에도 다양한 플랫폼과 서비스에 SLI/SLO를 적용하고 고도화해 나가고 있습니다. 이 글이 여러 SRE 분들에게 도움이 되길 바라며 이만 마치겠습니다. 긴 글 읽어주셔서 감사합니다.