LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


Jenkins에서 Kubernetes 플러그인을 이용해 보다 쉽고 효율적으로 성능 테스트하기

성능 테스트는 모든 LINE 서비스에서 없어서는 안되는 필수 과정입니다. 하지만 다음과 같은 이유로 성능 테스트의 환경을 설정하고 관리하는 작업이 항상 쉽고 효율적인 것은 아닙니다.

먼저, 기능과 컴포넌트, 이벤트 등에 따라 서버 워크로드의 기준이 다릅니다. 컴포넌트의 종류나 이벤트의 규모에 따라 해당 기능의 성능 기준이 달라지기 때문에 RPS(Request Per Second) 역시 천차만별입니다. 또한, 대부분의 오픈소스 성능 툴에는 다수의 테스트 실행자(executor)를 통제하거나 테스트별 결과 보고서를 통합해 주는 기능이 없습니다. 테스터가 직접 단계 별로 테스트를 진행하고 성능 테스트 스크립트를 실행하기에 적합한 수준의 장비를 찾아야 합니다. 이를 위해 신규 장비 할당과 필요한 라이브러리 설치, 스크립트 업로드, 스크립트가 테스트 장비에 과부하가 걸리지 않는 범위에서 필요한 워크로드를 생성하는지 확인하기 위한 여러 번의 시도를 거쳐야 합니다.

두 번째로 서비스나 팀별로 성능 테스트 환경이 다릅니다. 워크로드 기준 및 릴리스 일정이 다르기 때문에 대만에 위치한 각 팀이나 서비스는 워크로드 장비와 InfluxDB, Grafana 대시보드 등 자체적으로 성능 테스트 환경을 관리합니다. 기능적인 면에서 거의 동일한 서버를 팀별로 각각 설정하고 관리하는 것은 매우 번거로울 뿐만 아니라 개발자 경험 측면에서도 부정적입니다.

세 번째로 장비 사용률이 매우 낮습니다. 대규모 성능 테스트는 그리 빈번하게 발생하지 않습니다. 보통 신규 기능을 구현하거나 구조적인 변경이 발생한 경우에만 시행합니다. 자주 발생하지 않는 테스트를 위해 각 팀에 서버를 할당하면 전체 사용률은 매우 낮으면서 유지 보수 자원은 각각 필요합니다.

마지막으로 통합된 모니터링 대시보드가 없습니다. 성능을 테스트하기 위해서는 테스트 실행 자원과 서버 자원 모니터링이 동시에 필요합니다. 예전에는 한 명의 엔지니어가 데스크톱 창에 여러 개의 터미널 콘솔을 실행해 놓고 CPU와 메모리, 디스크, 네트워크 I/O 등을 모니터링하는 명령어를 실행했습니다. 이런 방식은 설정도 쉽지 않지만 테스트 시행 결과를 통합하고 관리하기도 어렵습니다.

이번 글에서는 Jenkins Kubernetes 플러그인의 동적 자원 관리를 이용해 위에서 언급한 문제를 해결할 수 있는 방법에 대해 이야기해 보겠습니다. LINE Taiwan에서는 대부분 k6를 테스트 툴로 사용하기 때문에 글의 내용은 k6를 기반으로 하고 있지만 다른 툴을 사용하는 경우에도 큰 차이는 없을 거라고 생각합니다.

아키텍처 다이어그램

핵심은 각 팀이 인프라 설정이나 관리에 따로 신경 쓰지 않으면서 성능 테스트 스크립트를 실행할 수 있도록 Jenkins의 Kubernetes 플러그인을 이용한 플랫폼을 준비하는 것입니다. 단일 장비에서 컴퓨팅 자원과 네트워크 대역폭에 부하를 걸어 테스트하는 대신 해당 플랫폼을 통해 클러스터에서 가용 가능한 노드에 워크로드를 균등하게 분배합니다. 이를 통해 실제와 가까운 시나리오를 실행할 수 있습니다.

플랫폼을 이용하기 위해 각 필드에 필요한 정보를 입력합니다.

  • POD_COUNT: 테스트를 실행하고자 하는 파드(pod) 개수 입력
  • GIT_RAW_FILE: Git에 위치한 k6 성능 스크립트 파일 위치 입력
  • DURATION, VIRTUAL_USER: 공식 k6 문서에서 'Duration'과 'VU'의 정의 확인 후 입력
  • INFLUX_DB: 테스트 데이터를 호스팅하는 InfluxDB URL(모든 팀을 위해 하나의 서버 설정 추천) 입력

성능 테스트 플랫폼 설정

아래와 같이 Jenkins 파이프라인(Jenkinsfile)과 PodTemplate을 이용해 플러그인이 워크로드 리소스를 프로비저닝하도록 지시합니다. 

KubernetesPod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: k6node ## since this file is cached as Jenkins node template, change this name when below attributes are updated, otherwise it will keep using old ones!!! Need to update Jenkinsfile also.
  labels:
    app: k6
spec:
  namespace: default
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          namespace: default
          topologyKey: kubernetes.io/hostname
          labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - k6
                  - k6node
  containers:
  - name: k6
    image: your.docker_registry.com/your_org/k6:your_image_version
    resources:
      requests:
          cpu: "100m"
          memory: "256Mi"
    tty: true
    securityContext: ## <-- When define USER in Dockerfile, securityContext should be added with root user, so that shell script will not hang in container
      runAsUser: 0

Jenkinsfile

pipeline {
  parameters {
    string(name: 'POD_COUNT', defaultValue: '2', description: 'number of Pods runs k6 scripts')
    string(name: 'GIT_RAW_FILE', defaultValue: 'https://raw.githubusercontent.com/loadimpact/k6/master/samples/http_get.js', description: 'raw file of the k6 performance script in git')
    string(name: 'DURATION', defaultValue: '5m', description: 'this will overwrite duration value in script')
    string(name: 'VIRTUAL_USER', defaultValue: '10', description: 'this will overwrite VUs value in script')
    string(name: 'INFLUX_DB', defaultValue: 'http://your_influxDB_IP:_PORT/your_influxDB_name', description: 'change the influx URL or DB name as you wish')
  }
  environment {
    GIT_TOKEN = credentials('github-token')
  }
  agent {
    kubernetes {
      label 'k6node'
      yamlFile 'KubernetesPod.yaml'
    }
  }
  stages {
    stage('Performance Test') {
      steps {
        script {
          def stages = [: ]
          echo "Pods count: ${params.POD_COUNT}"
          echo "VUs: ${params.VIRTUAL_USER}"
          echo "Duration: ${params.DURATION}"
          for (int i = 0; i < params.POD_COUNT.toInteger(); i++) {
            stages[i] = {
              node('k6node') {
                stage("Stage-${i}") {
                  container('k6') {
                    sh "wget --header='Authorization: token $GIT_TOKEN' --header='Accept: application/vnd.github.v3.raw' ${params.GIT_RAW_FILE} --output-document=pt.js"
                    sh "k6 run pt.js --duration ${params.DURATION} --vus ${params.VIRTUAL_USER} --out influxdb=${params.INFLUX_DB}"
                  }
                }
              }
            }
          }
          parallel stages
        }
      }
    }
  }
}

Dockerfile

FROM golang:1.14-alpine as builder
WORKDIR $GOPATH/src/github.com/loadimpact/k6
ADD . .
RUN apk --no-cache add git
RUN CGO_ENABLED=0 go install -a -trimpath -ldflags "-s -w -X github.com/loadimpact/k6/lib/consts.VersionDetails=$(date -u +"%FT%T%z")/$(git describe --always --long --dirty)"
 
FROM alpine:3.11
RUN apk add --no-cache ca-certificates && 
    adduser -D -u 12345 -g 12345 k6
COPY --from=builder /go/bin/k6 /usr/bin/k6
 
USER 12345

검증

k9s(Kubernetes 관리 콘솔)에서 프로비저닝 결과를 확인할 수 있습니다. 아래 이미지에서 가용 가능한 모든 노드에 파드가 균등하게 배분된 것을 확인할 수 있습니다.

k6 컨테이너 쉘에 k6 스크립트가 실행되도록 설정하고 표준 출력값을 확인하면 각 파드가 해당 스크립트를 실행하는지 확인할 수 있습니다. 아래는 Docker 콘솔에 표시된 내용입니다. 

/ # ps -ef
PID USER TIME COMMAND
1 root 0:00 /bin/sh
14 root 0:00 sh -c ({ while [ -d '/home/jenkins/agent/workspace/pt_job_name@tmp/durable-7cf10e10' -a ! -f '/home/jenkins/agent/workspace/pt_job_name@tmp/durable-7cf10e10/jenkins-result.txt' ]; do touch '/home/jenkins/age
15 root 0:00 sh -xe /home/jenkins/agent/workspace/pt_job_name@tmp/durable-7cf10e10/script.sh
18 root 0:56 k6 run https://raw.githubusercontent.com/loadimpact/k6/master/samples/http_get.js --duration 25m --out influxdb=http://influxDB_IP:PORT/myk6db
622 root 0:00 sh
632 root 0:00 sleep 3
633 root 0:00 ps -ef
/ # tail -f /proc/18/fd/1
running (15m25.9s), 10/10 VUs, 1191 complete and 0 interrupted iterations
default [ 62% ] 10 VUs 15m25.9s/25m0s
 
time="2020-11-30T07:38:15Z" level=info msg="{"_csrf":"vanOzuma-44CvDHIcLmlUdvZmGnToUVFioJ4","payload":{"email":"user5@gmail.com","password":"*******","_csrf":"vanOzuma-44CvDHIcLmlUdvZmGnToUVFioJ4"},"prods":["5f87f24002ed26b65ffd9005","5f87f24002ed26b65ffd9006","5f87f24002ed26b65ffd9008","5f87f24002ed26b65ffd9004","5f87f24002ed26b65ffd9007","5f87f24002ed26b65ffd9003"]}" source=console
time="2020-11-30T07:38:15Z" level=info msg="{"prods":
 
....

통합 대시보드 설정

성능 테스트를 시행할 때 클라이언트(k6)와 서버(애플리케이션) 측 파드와 노드 자원을 모니터링해야 합니다. 모든 워크로드가 Kubernetes에서 실행되기 때문에 하나의 Grafana 통합 대시보드에서 파드 및 노드 자원 지표와 k6 성능 지표를 함께 확인할 수 있습니다.

파드와 노드 관련 대시보드

아래는 파드와 노드 관련 지표 모니터링 대시보드입니다. 

k6 성능 테스트 대시보드

아래와 같이 설정하면 Grafana 대시보드에서 k6 성능 테스트 지표를 확인할 수 있습니다.

Grafana 인스턴스 DB 설정하기

  1. Grafana 왼쪽 사이드바에서 Configuration → Data Source를 선택한 후 Add data source 버튼 클릭
  2. Data Sources / New 페이지에서 Name에 DB 이름을 입력하고 Type에서 'InfluxDB' 선택
  3. HTTP 메뉴 아래에서 InfluxDB 인스턴스 URL 입력
  4. InfluxDB Details 메뉴의 Database에 DB 이름 입력(단, INFLUX_DB name과 동일해야 함)

k6 성능 테스트 대시보드 가져오기

  1. Grafana 왼쪽 사이드바에서 + → Import 클릭
  2. k6 Load Testing Result board를 확인하고 오른쪽에서 Copy ID to Clipboard 버튼 클릭
  3. Import via grafana.com 필드에 ID를 붙여 넣고 Load를 클릭하면 대시보드 가져오기 완료(대시보드 이름 추후 변경 가능)
  4. 드롭다운 목록에서 InfluxDB 선택
  5. Import 클릭

성능 테스트에서 수집한 지표 기반의 그래프를 대시보드에서 확인할 수 있습니다.

마치며

파드와 노드의 수를 동적으로 늘리면 팀별로 다른 워크로드와 사용 시나리오에 부합하는 환경에 맞춰 수직 혹은 수평적으로 간단하게 확장할 수 있습니다. 또한 성능 테스트 환경을 구축하고 관리하기 위해 각 팀이 거쳐야 했던 수고를 덜면서 장비도 효율적으로 사용할 수 있습니다. 저희 팀은 Kubernetes 플랫폼으로 전환한 후 개발 및 테스트의 컴퓨팅 자원을 통합하기 시작했습니다. 부디 저희의 경험이 여러분들에게 도움이 되시길 바랍니다.

참고 문헌