[1] CloudNativePG 란?
PostgreSQL은 세계 4위의 ORDBMS이다. 국제 표준화 기구 ISO의 SQL 표준을 가장 잘 준수하는 RDBMS이라고 한다. 그래서 그런지 PostgreSQL 구조는 타 RDBMS과 비슷하고, 오픈소스임에도 안정적이고 꾸준히 발전중이다.
CloudNativePG는 Apache의 EDB가 개발하여 공개했다. 쿠버네티스 환경에서 PostgreSQL 워크로드를 관리해주고, 이미지 보안 검증을 하기 때문에 보안적으로 안전하다고 한다. 이 포스팅에서는 이 CloudNativePG, PostgreSQL의 Operator를 테스트 해보겠다.
[2] CloudNativePG 설치
• (공개) 바닐라 쿠베네티스 실습 환경 배포 가이드
가시다님이 공개해주신 쿠버네티스 실습 환경 위에 설치 했다.
CloudNativePG 공식 홈페이지에 helm chart를 제공하고 있어 helm chart로 배포가 가능하다. 마스터 노드에 오퍼레이터 Pod가 올라오도록 nodeSelector, tolerations을 구성했다.
(🚴|DOIK-Lab:default) root@k8s-m:~# cat ~/DOIK/5/values.yaml
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────
│ File: /root/DOIK/5/values.yaml
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────
1 │ nodeSelector: {kubernetes.io/hostname: k8s-m}
2 │ tolerations: [{key: node-role.kubernetes.io/master, operator: Exists, effect: NoSchedule}]
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────
helm chart로 CloudNativePG를 배포 한다.
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm install cnpg cnpg/cloudnative-pg -f ~/DOIK/5/values.yaml
# 출력
NAME: cnpg
LAST DEPLOYED: Sun Jun 19 21:15:24 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
CloudNativePG operator should be installed in namespace "default".
You can now create a PostgreSQL cluster with 3 nodes in the current namespace as follows:
cat <<EOF | kubectl apply -f -
# Example of PostgreSQL cluster
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
spec:
instances: 3
storage:
size: 1Gi
EOF
클러스터도 배포한다. 아래와 같이 3대의 파드로 구성되며, 업데이트 전략은 unsupervised 이다. 특이한 점은 오퍼레이터가 직접 개별파드를 생성해준다.
(🚴|DOIK-Lab:default) root@k8s-m:~# cat ~/DOIK/5/mycluster1.yaml
───────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: /root/DOIK/5/mycluster1.yaml
───────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ # Example of PostgreSQL cluster
2 │ apiVersion: postgresql.cnpg.io/v1
3 │ kind: Cluster
4 │ metadata:
5 │ name: mycluster
6 │ spec:
7 │ imageName: ghcr.io/cloudnative-pg/postgresql:14.2
8 │ instances: 3
9 │ storage:
10 │ size: 3Gi
11 │ postgresql:
12 │ parameters:
13 │ max_worker_processes: "40"
14 │ timezone: "Asia/Seoul"
15 │ pg_hba:
16 │ - host all postgres all trust
17 │ primaryUpdateStrategy: unsupervised
18 │ enableSuperuserAccess: true
19 │ bootstrap:
20 │ initdb:
21 │ database: app
22 │ encoding: UTF8
23 │ localeCType: C
24 │ localeCollate: C
25 │ owner: app
───────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────
모두 배포가 되면 기본 리소스를 확인 해본다.
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl get pod,deploy
kubectl get svc,ep,endpointslices -l cnpg.io/cluster=mycluster
kubectl get cm,secret
kubectl get pdb
NAME READY STATUS RESTARTS AGE
pod/cnpg-cloudnative-pg-5f8cc75df5-zql8s 1/1 Running 0 34m
pod/mycluster-1 1/1 Running 0 17m
pod/mycluster-2 1/1 Running 0 15m
pod/mycluster-3 1/1 Running 0 14m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/cnpg-cloudnative-pg 1/1 1 1 34m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/mycluster-any ClusterIP 10.200.1.152 <none> 5432/TCP 17m
service/mycluster-r ClusterIP 10.200.1.2 <none> 5432/TCP 17m
service/mycluster-ro ClusterIP 10.200.1.98 <none> 5432/TCP 17m
service/mycluster-rw ClusterIP 10.200.1.125 <none> 5432/TCP 17m
NAME ENDPOINTS AGE
endpoints/mycluster-any 172.16.1.11:5432,172.16.2.13:5432,172.16.3.11:5432 17m
endpoints/mycluster-r 172.16.1.11:5432,172.16.2.13:5432,172.16.3.11:5432 17m
endpoints/mycluster-ro 172.16.2.13:5432,172.16.3.11:5432 17m
endpoints/mycluster-rw 172.16.1.11:5432 17m
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
endpointslice.discovery.k8s.io/mycluster-any-4xhpd IPv4 5432 172.16.1.11,172.16.3.11,172.16.2.13 17m
endpointslice.discovery.k8s.io/mycluster-r-ntm4j IPv4 5432 172.16.1.11,172.16.3.11,172.16.2.13 17m
endpointslice.discovery.k8s.io/mycluster-ro-q8ttz IPv4 5432 172.16.3.11,172.16.2.13 17m
endpointslice.discovery.k8s.io/mycluster-rw-dd62d IPv4 5432 172.16.1.11 17m
NAME DATA AGE
configmap/cnpg-controller-manager-config 0 34m
configmap/cnpg-default-monitoring 1 34m
configmap/kube-root-ca.crt 1 58m
NAME TYPE DATA AGE
secret/cnpg-ca-secret Opaque 2 34m
secret/cnpg-cloudnative-pg-token-rppdn kubernetes.io/service-account-token 3 34m
secret/cnpg-webhook-cert kubernetes.io/tls 2 34m
secret/default-token-prc6b kubernetes.io/service-account-token 3 58m
secret/mycluster-app kubernetes.io/basic-auth 3 17m
secret/mycluster-ca Opaque 2 17m
secret/mycluster-replication kubernetes.io/tls 2 17m
secret/mycluster-server kubernetes.io/tls 2 17m
secret/mycluster-superuser kubernetes.io/basic-auth 3 17m
secret/mycluster-token-mlhqm kubernetes.io/service-account-token 3 17m
secret/sh.helm.release.v1.cnpg.v1 helm.sh/release.v1 1 34m
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
mycluster 1 N/A 1 17m
mycluster-primary 1 N/A 0 17m
[3] PostgreSQL DB 접근
2개의 자격증명이 secret (app, superuser) 에 저장 되어 있으며, 계정명과 암호가 담겨있다. myclient Pod 2대를 배포한다.
${PODNAME}가 치환되어서 쉽게 다수의 Pod를 배포할 수 있다.
(🚴|DOIK-Lab:default) root@k8s-m:~# cat ~/DOIK/5/myclient.yaml
───────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: /root/DOIK/5/myclient.yaml
───────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ apiVersion: v1
2 │ kind: Pod
3 │ metadata:
4 │ name: ${PODNAME}
5 │ labels:
6 │ app: myclient
7 │ spec:
8 │ nodeName: k8s-m
9 │ containers:
10 │ - name: ${PODNAME}
11 │ image: bitnami/postgresql:${VERSION}
12 │ command: ["tail"]
13 │ args: ["-f", "/dev/null"]
14 │ terminationGracePeriodSeconds: 0
───────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────
(🚴|DOIK-Lab:default) root@k8s-m:~# for ((i=1; i<=2; i++)); do PODNAME=myclient$i VERSION=14.3.0 envsubst < ~/DOIK/5/myclient.yaml | kubectl apply -f - ; done
pod/myclient1 created
pod/myclient2 created
서비스 도메인 주소로 접속하여 - 수퍼유저로 접근했다. 이러면 TLSv1.3 연결을 사용하지만, 암호를 묻는 과정없이 접속한다.
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl exec -it myclient1 -- psql -U postgres -h mycluster-rw -p 5432
psql (14.3, server 14.2 (Debian 14.2-1.pgdg110+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
postgres=#
연결 정보 확인, 데이터베이스 조회를 해본다.
커넥션 된 DB 이름을 볼 수 있다.
# 연결정보 조회
postgres-# \conninfo
You are connected to database "postgres" as user "postgres" on host "mycluster-rw" (address "10.200.1.125") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
# DB조회
postgres-# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+---------+-------+-----------------------
app | app | UTF8 | C | C |
postgres | postgres | UTF8 | C | C |
template0 | postgres | UTF8 | C | C | =c/postgres +
| | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | C | C | =c/postgres +
| | | | | postgres=CTc/postgres
(4 rows)
# 타임존
postgres=# SELECT * FROM pg_timezone_names WHERE name = current_setting('TIMEZONE');
name | abbrev | utc_offset | is_dst
------------+--------+------------+--------
Asia/Seoul | KST | 09:00:00 | f
(1 row)
DVD Rental Sample Database을 다운 받는다. 그 후uperuser 계정으로 mycluster-rw 서비스 접속, 데이터베이스 생성하여 조회해본다.
# 다운로드, 압축풀기
(🚴|DOIK-Lab:default) root@k8s-m:~# curl -LO https://www.postgresqltutorial.com/wp-content/uploads/2019/05/dvdrental.zip
(🚴|DOIK-Lab:default) root@k8s-m:~# apt install unzip -y && unzip dvdrental.zip
Reading package lists... Done
Building dependency tree... Done
...
...
# myclient1 파드에 dvdrental.tar 복사
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl cp dvdrental.tar myclient1:/tmp
# [myclient1] superuser 계정으로 mycluster-rw 서비스 접속 후 데이터베이스 생성
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl exec -it myclient1 -- createdb -U postgres -h mycluster-rw -p 5432 dvdrental
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl exec -it myclient1 -- psql -U postgres -h mycluster-rw -p 5432 -l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+---------+-------+-----------------------
app | app | UTF8 | C | C |
dvdrental | postgres | UTF8 | C | C |
postgres | postgres | UTF8 | C | C |
template0 | postgres | UTF8 | C | C | =c/postgres +
| | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | C | C | =c/postgres +
| | | | | postgres=CTc/postgres
(5 rows)
# DVD Rental Sample Database 불러오고, actor 테이블 조회
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl exec -it myclient1 -- pg_restore -U postgres -d dvdrental /tmp/dvdrental.tar -h mycluster-rw -p 5432
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl exec -it myclient1 -- psql -U postgres -h mycluster-rw -p 5432 -d dvdrental -c "SELECT * FROM actor"
actor_id | first_name | last_name | last_update
----------+-------------+--------------+------------------------
1 | Penelope | Guiness | 2013-05-26 14:47:57.62
2 | Nick | Wahlberg | 2013-05-26 14:47:57.62
3 | Ed | Chase | 2013-05-26 14:47:57.62
4 | Jennifer | Davis | 2013-05-26 14:47:57.62
5 | Johnny | Lollobrigida | 2013-05-26 14:47:57.62
6 | Bette | Nicholson | 2013-05-26 14:47:57.62
7 | Grace | Mostel | 2013-05-26 14:47:57.62
8 | Matthew | Johansson | 2013-05-26 14:47:57.62
9 | Joe | Swank | 2013-05-26 14:47:57.62
...
...
rw vs ro vs r(any) 차이 확인을 해볼 수 있다. 접근에 따라 어떻게 분배될까?
파드의 권한에 따라 접근할 수 있는 파드가 정해지고, rw은 1개, ro는 2개, r과 any는 3개의 파드로 분배된다.
# 접속 주소 변수처리
POD1=$(kubectl get pod mycluster-1 -o jsonpath={.status.podIP})
POD2=$(kubectl get pod mycluster-2 -o jsonpath={.status.podIP})
POD3=$(kubectl get pod mycluster-3 -o jsonpath={.status.podIP})
# rw
(🚴|DOIK-Lab:default) root@k8s-m:~# for i in {1..30}; do kubectl exec -it myclient1 -- psql -U postgres -h mycluster-rw -p 5432 -c "select inet_server_addr();"; done | sort | uniq -c | sort -nr | grep 172
30 172.16.1.11
# ro
(🚴|DOIK-Lab:default) root@k8s-m:~# for i in {1..30}; do kubectl exec -it myclient1 -- psql -U postgres -h mycluster-ro -p 5432 -c "select inet_server_addr();"; done | sort | uniq -c | sort -nr | grep 172
20 172.16.2.13
10 172.16.3.11
# r
(🚴|DOIK-Lab:default) root@k8s-m:~# for i in {1..30}; do kubectl exec -it myclient1 -- psql -U postgres -h mycluster-r -p 5432 -c "select inet_server_addr();"; done | sort | uniq -c | sort -nr | grep 172
12 172.16.3.11
10 172.16.2.13
8 172.16.1.11
# any
(🚴|DOIK-Lab:default) root@k8s-m:~# for i in {1..30}; do kubectl exec -it myclient1 -- psql -U postgres -h mycluster-any -p 5432 -c "select inet_server_addr();"; done | sort | uniq -c | sort -nr | grep 172
12 172.16.3.11
10 172.16.1.11
8 172.16.2.13
[4] 장애 테스트
4-1 프라이머리 파드
파드(인스턴스) 1대 강제 삭제, 노드 drain을 해서 장애상황을 테스트 해본다.
모니터링을 세팅하고 파드를 삭제하면 insert가 끊겼다가 복구되는 것을 볼 수 있다. (스크린샷)
# [터미널1] 모니터링
watch kubectl get pod -l cnpg.io/cluster=mycluster
# [터미널2] 모니터링
while true; do kubectl exec -it myclient2 -- psql -U postgres -h mycluster-ro -p 5432 -d test -c "SELECT COUNT(*) FROM t1"; date;sleep 1; done
# [터미널3] test 데이터베이스에 다량의 데이터 INSERT
for ((i=301; i<=10000; i++)); do kubectl exec -it myclient1 -- psql -U postgres -h mycluster-rw -p 5432 -d test -c "INSERT INTO t1 VALUES ($i, 'Luis$i');";echo; done
# [터미널4] 파드 삭제 >> INSERT 가 중간에 끊어지는지 확인
kubectl delete pvc/mycluster-1 pod/mycluster-1
4-2 프라이머리 파드가 올라간 노드 drain
프라이머리 파드를 조회한다.
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl get cluster
NAME AGE INSTANCES READY STATUS PRIMARY
mycluster 91m 3 3 Cluster in healthy state mycluster-2
2번 파드가 올라간 1번 노드를 drain 한다. 아래처럼 노드가 SchedulingDisabled으로 변하고, insert가 끊겼다가 정상작동된다.
kubectl drain k8s-w1 --delete-emptydir-data --force --ignore-daemonsets && kubectl get node -w
NAME STATUS ROLES AGE VERSION
k8s-m Ready control-plane,master 135m v1.23.6
k8s-w1 Ready,SchedulingDisabled <none> 134m v1.23.6
k8s-w2 Ready <none> 134m v1.23.6
k8s-w3 Ready <none> 134m v1.23.6
uncordon 명령어로 노드를 정상화한다.
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl uncordon k8s-w1w1
node/k8s-w1 uncordoned
#모니터링을 하면 파드가 다시 정상작동한다.
kubectl get pod -l cn... k8s-m: Sun Jun 26 04:05:46 2022
NAME READY STATUS RESTARTS AGE
mycluster-2 1/1 Running 0 4m58s
mycluster-3 1/1 Running 0 95m
mycluster-4 1/1 Running 0 21m
[5] Rolling update
14.2버전인데 14.3으로 업그레이드 해본다. 스탠바이 부터 업그레이드 시작해서 프라이머리 갱신하게된다.
kubectl patch cluster mycluster --type=merge -p '{"spec":{"imageName":"ghcr.io/cloudnative-pg/postgresql:14.3"}}' && kubectl get pod -l postgresql=mycluster -w
업그레이드 후 확인을 해보면 프라이머리였던 3번 Pod가 가장 마지막에 업그레이드 되었다.
Every 2.0s: kubectl get pod -l cnpg.io/cluster=mycluster
NAME READY STATUS RESTARTS AGE
mycluster-2 1/1 Running 0 2m4s
mycluster-3 1/1 Running 0 105s
mycluster-4 1/1 Running 0 2m8s
14.3버전으로 업그레이드 된 것을 확인 할 수 있다.
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl cnpg status mycluster | grep Image
PostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:14.3
[6] Scale test
클러스터 크기를 확인해본다.
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl get cluster mycluster
NAME AGE INSTANCES READY STATUS PRIMARY
mycluster 112m 3 3 Cluster in healthy state mycluster-2
Every 2.0s: kubectl get pod -l postgresql=mycluster
NAME READY STATUS RESTARTS AGE
mycluster-2 1/1 Running 0 11m
mycluster-3 1/1 Running 0 10m
mycluster-4 1/1 Running 0 11m
크기를 5개로 증가시키고 any로 접속 테스트를 하면 5개의 파드로 분배되어 접근한다.
# 5개로 증가
(🚴|DOIK-Lab:default) root@k8s-m:~# kubectl patch cluster mycluster --type=merge -p '{"spec":{"instances":5}}' && kubectl get pod -l postgresql=mycluster -w
# any 접근 테스트
(🚴|DOIK-Lab:default) root@k8s-m:~# for i in {1..30}; do kubectl exec -it myclient1 -- psql -U postgres -h mycluster-any -p 5432 -c "select inet_server_addr();"; done | sort | uniq -c | sort -nr | grep 172
10 172.16.3.14
7 172.16.1.17
6 172.16.1.20
4 172.16.2.15
3 172.16.3.17
마치며
여러 sql DB의 장애 테스트, update, scale등의 테스트를 하면서 조금씩의 차이는 있지만 같은 개념으로 동작한다는 걸 알게 되었다. 프라이머리, 세컨더리의 승격하는 개념을 가지고 위 동작들을 보면, 이해가 더 잘가는 것같다. 다양한 DB를 다뤄보면서 개념이 더 두터워지는 걸 느낄 수 있었다.
'스터디' 카테고리의 다른 글
[DOIK] MySQL Operator for k8s - 장애상황 테스트 (0) | 2022.06.25 |
---|---|
[DOIK] MySQL Operator for k8s 이해하고 설치하기 (0) | 2022.06.12 |
[DOIK] MySQL innoDB 클러스터 (0) | 2022.06.12 |