saturn30
헤츠너 쿠버네티스

cloudnative-pg 성능 테스트

쿠버네티스에 데이터베이스를 올려도 될지 테스트

왜하는지

이전 포스팅에서 Hetzner 볼륨 성능 이슈를 확인했다.
볼륨 스토리지의 성능이 데디케이티드 서버의 로컬 스토리지에 비해 매우 떨어진다.
그래서 데디케이티드 서버에서 postgresql을 설치하고 운영해야겠다 생각했다.

막상 하려니 어려운 부분들이 많았다.
일단 데디케이티드 서버 가장 저렴한게 2cpu 8gb짜리 15달러이다.
가용성 확보를 위해서는 최소 3개의 서버가 필요하다는데, 매달 45달러를 쓰기엔 부담스럽다.

더 큰 문제는 내가 쿠버네티스의 도움 없이 여러개의 서버를 운영할 줄 모른다.
3개의 서버를 돌리고, 직접 모니터링 툴과 백업을 설정하는건 어려운 일이다.
쿠버네티스 비스무리하게 오케스트레이션해주는 디비용 라이브러리가 있지 않을까 했는데 못찾았다.
오히려 내가 찾는것과 유사한 프레임워크는 쿠버네티스용 오퍼레이터로 있었다.

찾아본건 CloudNativePG으로 꽤나 성숙한 오퍼레이터로 보였다.
그리고 data on kubernetes라고 쿠버네티스에 statefull한 데이터들을 운영하는 커뮤니티도 있었다.
프로덕션에서 데이터베이스를 쿠버네티스에 올려서 운영하는 것은 요즘엔 이상하지 않은 일인가보다.
gpt 힘을 빌려서 성능 테스트 한번 해보고 나쁘지 않으면 나도 쿠버네티스에 데이터베이스를 올려서 운영해보려고 한다.

성능 테스트

테스트는 pgbench를 사용했다.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-main-cluster
  namespace: cnpg-system
spec:
  instances: 3
  storage:
    size: 10Gi
    storageClass: "hcloud-volumes"
  bootstrap:
    initdb:
      database: saturn30
      owner: saturn30
  postgresql:
    parameters:
      max_connections: "200"

테스트용 postgresql 클러스트를 생성한다. (cloudnative-pg는 미리 설치함)

apiVersion: batch/v1
kind: Job
metadata:
  name: pgbench-combined
  namespace: cnpg-system
spec:
  template:
    spec:
      containers:
        - name: pgbench
          image: postgres:15
          command: ["/bin/sh", "-c"]
          args:
            - |
              echo "[1/2] Initializing pgbench..." &&
              export PGPASSWORD=$(cat /secrets/password) &&
              pgbench -i -s 10 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30 &&
              echo "[2/2] Running benchmark..." &&
              pgbench -c 10 -j 4 -T 60 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30
          volumeMounts:
            - name: secret-volume
              mountPath: /secrets
              readOnly: true
      restartPolicy: Never
      volumes:
        - name: secret-volume
          secret:
            secretName: pg-main-cluster-app
  backoffLimit: 1

pgbench를 실행하는 job이다.

pgbench -i -s 10 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30
먼저 pgbench 커맨드만 보면, 초기화 작업으로 pgbench_accounts 테이블에 1,000,000(-s 10, 1당 10만)개의 데이터를 미리 생성한다.

pgbench -c 10 -j 4 -T 60 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30
그리고 10개의 동시접속 클라이언트(-c 10), 4개의 쓰레드 job(-j 4), 60초동안(-T 60) 테스트한다.

해당 yaml을 apply 하고, 60초 후에 kubectl logs job/pgbench-combined -n cnpg-system으로 결과를 확인한다.

결과는 아래와 같다.

transaction type: <builtin: TPC-B (sort of)>
scaling factor: 10
query mode: simple
number of clients: 10
number of threads: 4
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 43443
number of failed transactions: 0 (0.000%)
latency average = 13.807 ms
initial connection time = 55.496 ms
tps = 724.273704 (without initial connection time)

60초동안 43443개의 트랜잭션을 처리했고, 실패율은 0%, 평균 응답시간은 13.8ms였다.
초당 처리량은 724개다.

TPS 수준설명예시
1~10 TPS매우 낮음개인 블로그, 테스트 서버, 저부하 내부 시스템
10~100 TPS낮음소규모 스타트업 서비스, 초기 MVP
100~1,000 TPS중간중간 규모의 서비스, 사용자 수천~수만명
1,000~10,000 TPS높음대형 커머스, 게임 서버, SNS 등
10,000+ TPS매우 높음글로벌 서비스, 금융 시스템, 실시간 메시징 서비스 (예: WhatsApp, WeChat 등)

gpt가 말하는 TPS별 판단 기준이다. 724면 내 서비스에서는 나쁘지 않은 값인 것 같다.

성능 테스트2

apiVersion: batch/v1
kind: Job
metadata:
  name: pgbench-combined
  namespace: cnpg-system
spec:
  template:
    spec:
      containers:
        - name: pgbench
          image: postgres:15
          command: ["/bin/sh", "-c"]
          args:
            - |
              echo "[1/2] Initializing pgbench..." &&
              export PGPASSWORD=$(cat /secrets/password) &&
              pgbench -i -s 300 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30 &&
              echo "[2/2] Running benchmark..." &&
              pgbench -c 100 -j 16 -T 600 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30
          volumeMounts:
            - name: secret-volume
              mountPath: /secrets
              readOnly: true
      restartPolicy: Never
      volumes:
        - name: secret-volume
          secret:
            secretName: pg-main-cluster-app
  backoffLimit: 1

pgbench -i -s 300 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30
pgbench -c 100 -j 16 -T 600 -h pg-main-cluster-rw.cnpg-system.svc -U saturn30 -d saturn30

더 높은 부하로 다시 테스트해봤다.
3000만개의 행을 가진 테이블을 생성하고, 100개의 동시접속 클라이언트(-c 100), 16개의 쓰레드 job(-j 16), 600초동안(-T 600) 테스트한다.

pgbench-cpu-usage cpu를 엄청 쓴다..

transaction type: <builtin: TPC-B (sort of)>
scaling factor: 300
query mode: simple
number of clients: 100
number of threads: 16
maximum number of tries: 1
duration: 600 s
number of transactions actually processed: 600303
number of failed transactions: 0 (0.000%)
latency average = 99.908 ms
initial connection time = 448.927 ms
tps = 1000.923845 (without initial connection time)

결과는 TPS는 1000, 평균 응답시간은 99.9ms였다.
클라이언트 커넥션과 쓰레드 job이 늘어나면서 TPS는 늘었지만, 레이턴시도 크게 늘어났다.
cpu 사용량이 높아져서 그런것일 수 있고, 테이블이 3000만개로 크게 설정해서 그런것 일수도 있다.
테이블 행을 줄여서 다시 테스트해보자.


scaling factor: 10
query mode: simple
number of clients: 100
number of threads: 16
maximum number of tries: 1
duration: 600 s
number of transactions actually processed: 433064
number of failed transactions: 0 (0.000%)
latency average = 138.551 ms
initial connection time = 501.577 ms
tps = 721.753603 (without initial connection time)

-s 옵셜을 10으로 줄였다.(테이블 행 10만개)
결과는 TPS는 721, 평균 응답시간은 138.5ms였다. 오히려 레이턴시가 늘었다.
cpu 사용량도 90%까지는 올라가지는 않았는데 이상한 결과다.
gpt는 100개의 클라이언트가 작은 데이터셋에 대해 극심한 CPU 및 잠금 경합을 벌이게 되었을 가능성이 있다고 한다.


transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1000
query mode: simple
number of clients: 20
number of threads: 4
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 36254
number of failed transactions: 0 (0.000%)
latency average = 33.046 ms
initial connection time = 135.371 ms
tps = 605.224338 (without initial connection time)

-s를 1000개로 늘리고, 클라이언트 커넥션과 쓰레드 job을 줄였다.
결과는 TPS는 605, 평균 응답시간은 33ms였다.

평균 응답시간이 튄건 커넥션 수를 너무 많이 설정해서 그런가보다.
내 클러스터 내 디비에서 사용될 것 같은 일반적인 수준의 클라이언트 커넥션과 스레드를 설정하니 응답시간이 다시 줄었다.
스케일링은 좀 크게 설정해봤는데 그래도 사용할만한 수준은 된다.

결론

사용해도 될 것 같다.
엄청나게 좋은 성능은 아니지만 감수할 수 있는 정도다.

cloudnative-pg에서 자동으로 read only db도 생성해준다.
지금까지 read/write 모두 수행하는 DB에 테스트를 했는데, 읽기 트래픽이 분산되는 실 사용에서는 좀 더 낫지 않을까 생각한다.
다만 read only db 운영에 대해서는 여태 전혀 고려하지 않았다.
애플리케이션 레벨에서 read/write db를 구분하는 로직을 추가해야 할 것 같다.

Last updated on

On this page