Skip to content

Commit fca0c40

Browse files
committed
Merge branch '20241203-merge-train' into 'main'
🚆 Merge Train for #2640, #2906, #2838 Closes #2640, #2906, and #2838 See merge request ongresinc/stackgres!1627
2 parents b66cf77 + c694edf commit fca0c40

65 files changed

Lines changed: 1608 additions & 307 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
---
2+
Title: Maintenance with Zero-Downtime
3+
weight: 12
4+
url: /runbooks/zero-downtime
5+
description: Achieve Zero-Downtime for Maintenance Tasks
6+
showToc: true
7+
---
8+
9+
Usually maintenance task for Postgres requires some degree of disruption of the service that may convert,
10+
in some cases, into an issue for your business.
11+
12+
In this runbook we'll demonstrate a technique to achieve Zero-Downtime for your maintenance task using
13+
StackGres and an external PgBouncer instance in [transaction pooling mode](https://www.pgbouncer.org/config.html#pool_mode).
14+
15+
>**IMPORTANT:** When transaction pooling is used clients must not use any session-based features, since each transaction ends up in a different connection and thus gets a different session state. From version 1.21.0
16+
PgBouncer added some support to allow use of prepared statement in transaction pooling mode.
17+
18+
## Restart and Switchover with Zero-Downtime
19+
20+
In some cases Postgres have to be restarted in order to allow some parameter changes. This can be performed
21+
manually using `patronictl restart` command. In other cases you need to re-create the Pod in order for
22+
some changes in the configuration to take place like after an upgrade of the StackGres operator or when any
23+
change in the SGCluster modify the Pod. To re-create a Pod you may simply delete it. But in both cases, a
24+
manual operation, may be dangerous and not taking into account the order of how to re-create each Pod of the
25+
SGCluster. In general a restart, security upgrade or minor version upgrade SGDbOps are the way to go. They
26+
will handle smoothly the operation performing a controlled switchover of the primary instance when needed.
27+
And following the right order to update all of your Pods that needs to be updated.
28+
29+
The switchover operation is not perfect since it disconnect all the Postgres clients forcing them to return
30+
an error to the final user. PgBouncer offer a mechanism to avoid that when configured with transaction
31+
pooling mode with the `PAUSE` and `RESUME` commands. Sending the `PAUSE` command with transaction pooling
32+
will wait for all the connection to complete the current transaction and pause all of them. Sending the
33+
`RESUME` command will resume the connection in order for them to continue to send transactions to the
34+
target Postgres instance. While connections are paused we can change the target Postgres instance achieving
35+
a zero-downtime experience for our users.
36+
37+
To achieve this feature we create a PgBouncer instance (using the same image used by SGCluster for the connection pooling) that will target the read-write Service of an SGCluster. The following snippet allow to create it together with a Service that will be used by applications to connect to PgBouncer:
38+
39+
```yaml
40+
cat << 'EOF' | kubectl replace --force -f -
41+
---
42+
apiVersion: apps/v1
43+
kind: Deployment
44+
metadata:
45+
name: pgbouncer
46+
spec:
47+
selector:
48+
matchLabels:
49+
app: pgbouncer
50+
template:
51+
metadata:
52+
labels:
53+
app: pgbouncer
54+
spec:
55+
terminationGracePeriodSeconds: 0
56+
containers:
57+
- name: pgbouncer
58+
image: quay.io/ongres/pgbouncer:v1.22.1-build-6.33
59+
command:
60+
- sh
61+
- /usr/local/bin/start-pgbouncer.sh
62+
ports:
63+
- containerPort: 5432
64+
name: pgbouncer
65+
protocol: TCP
66+
volumeMounts:
67+
- name: dynamic
68+
mountPath: /etc/pgbouncer
69+
- name: config
70+
mountPath: /etc/pgbouncer/pgbouncer.ini
71+
subPath: pgbouncer.ini
72+
- name: config
73+
mountPath: /usr/local/bin/start-pgbouncer.sh
74+
subPath: start-pgbouncer.sh
75+
volumes:
76+
- name: dynamic
77+
emptyDir: {}
78+
- name: config
79+
configMap:
80+
defaultMode: 0444
81+
name: pgbouncer
82+
optional: false
83+
---
84+
apiVersion: v1
85+
kind: Service
86+
metadata:
87+
name: pgbouncer
88+
spec:
89+
type: ClusterIP
90+
selector:
91+
app: pgbouncer
92+
ports:
93+
- name: pgbouncer
94+
port: 5432
95+
protocol: TCP
96+
targetPort: pgbouncer
97+
EOF
98+
```
99+
100+
The PgBouncer instance reference a ConfigMap containing the configuration that targets the SGCluster primary
101+
Service, it also contains an initialization scripts that create the credentials for pgbouncer users and to
102+
query the Postgres instance in order to authenticate other users:
103+
104+
```yaml
105+
cat << 'EOF' | kubectl replace --force -f -
106+
apiVersion: v1
107+
kind: ConfigMap
108+
metadata:
109+
name: pgbouncer
110+
data:
111+
pgbouncer.ini: |
112+
[databases]
113+
114+
* = host=cluster port=5432
115+
116+
[pgbouncer]
117+
listen_addr=0.0.0.0
118+
listen_port=5432
119+
120+
pool_mode=transaction
121+
max_client_conn=1000
122+
default_pool_size=100
123+
max_db_connections=0
124+
max_user_connections=0
125+
126+
auth_type=md5
127+
auth_file=/etc/pgbouncer/userlist.txt
128+
auth_user=postgres
129+
auth_query=SELECT usename, passwd FROM pg_shadow WHERE usename=$1
130+
131+
admin_users=pgbouncer_admin
132+
stats_users=pgbouncer_stats
133+
application_name_add_host=1
134+
ignore_startup_parameters=extra_float_digits
135+
136+
server_check_query=;
137+
start-pgbouncer.sh: |
138+
#!/bin/sh
139+
printf '"%s" "%s"\n' "postgres" "sup3rus3r" >> /etc/pgbouncer/userlist.txt
140+
printf '"%s" "%s"\n' "pgbouncer_admin" "pgb0unc3r" >> /etc/pgbouncer/userlist.txt
141+
printf '"%s" "%s"\n' "pgbouncer_stats" "pgb0unc3r" >> /etc/pgbouncer/userlist.txt
142+
exec pgbouncer /etc/pgbouncer/pgbouncer.ini
143+
EOF
144+
```
145+
146+
We then create an SGCluster that has the logic to send the `PAUSE` and `RESUME` command thanks to the
147+
`before_stop` guard script and `on_role_change` callback:
148+
149+
```yaml
150+
cat << 'EOF' | kubectl replace --force -f -
151+
---
152+
apiVersion: stackgres.io/v1
153+
kind: SGCluster
154+
metadata:
155+
name: cluster
156+
spec:
157+
configurations:
158+
patroni:
159+
initialConfig:
160+
postgresql:
161+
callbacks:
162+
on_role_change: /callbacks/on_role_change
163+
before_stop: /callbacks/before_stop
164+
credentials:
165+
users:
166+
superuser:
167+
password:
168+
name: credentials
169+
key: superuser-password
170+
instances: 2
171+
profile: development
172+
pods:
173+
customVolumeMounts:
174+
patroni:
175+
- name: custom-callbacks
176+
mountPath: /callbacks
177+
customVolumes:
178+
- name: callbacks
179+
configMap:
180+
defaultMode: 0775
181+
name: callbacks
182+
optional: false
183+
persistentVolume:
184+
size: 5Gi
185+
postgres:
186+
version: latest
187+
---
188+
apiVersion: v1
189+
kind: Secret
190+
metadata:
191+
name: credentials
192+
stringData:
193+
superuser-password: sup3rus3r
194+
EOF
195+
```
196+
197+
The scripts are mounted in a custom volume mount from a ConfigMap. The `before_stop` script is executed
198+
by Patroni synchronously and blocks the primary instance from being stopped by a switchover or a restart
199+
until the PAUSE command is sent to the PgBouncer instance. This allows the connection to complete the
200+
ongoing transactions before the primary goes offline. The `on_role_change` script is executed
201+
asynchronically by Patroni and do not block the promotion of a primary. It actually waits for the instance
202+
to be converted to primary and then sends the RESUME command so that connection sent to the instance
203+
will be able to write to the primary:
204+
205+
206+
```yaml
207+
cat << 'EOF' | kubectl replace --force -f -
208+
apiVersion: v1
209+
kind: ConfigMap
210+
metadata:
211+
name: callbacks
212+
data:
213+
before_stop: |
214+
#!/bin/sh
215+
set -x
216+
PATRONI_NAME="$(cat /etc/hostname)"
217+
PATRONI_HISTORY="$(patronictl history -f tsv | tail -n +2)"
218+
PATRONI_LIST="$(patronictl list -f tsv | tail -n +2)"
219+
if {
220+
[ "x$PATRONI_HISTORY" = x ] \
221+
&& ! printf %s "$PATRONI_LIST" | grep -v $'^[^\t]\+\t'"$PATRONI_NAME"$'\t' | grep -q $'^[^\t]\+\t[^\t]\+\t[^\t]\+\tLeader\t'
222+
} \
223+
|| printf %s "$PATRONI_HISTORY" | grep -q $'^[^\t]\+\t[^\t]\+\t[^\t]\+\t[^\t]\+\t'"$PATRONI_NAME"'$'
224+
then
225+
psql postgresql://pgbouncer_admin:pgb0unc3r@pgbouncer/pgbouncer -c PAUSE
226+
fi
227+
exit 0
228+
on_role_change: |
229+
#!/bin/sh
230+
set -x
231+
if [ "$#" = 0 ] || [ "x$2" = xmaster ]
232+
then
233+
until psql -tA -c 'SELECT pg_is_in_recovery()' | grep -qxF f
234+
do
235+
true
236+
done
237+
psql postgresql://pgbouncer_admin:pgb0unc3r@pgbouncer/pgbouncer -c RESUME
238+
psql postgresql://pgbouncer_admin:pgb0unc3r@pgbouncer/pgbouncer -tA -c 'SHOW STATE' | grep -q 'paused|no'
239+
fi
240+
EOF
241+
```
242+
243+
Now, to demonstrate the effectivity of this deployment let's create the following Job that will launch a
244+
pgbench against the PgBouncer instance:
245+
246+
```yaml
247+
cat << 'EOF' | kubectl replace --force -f -
248+
apiVersion: batch/v1
249+
kind: Job
250+
metadata:
251+
name: pgbench
252+
spec:
253+
template:
254+
spec:
255+
restartPolicy: OnFailure
256+
terminationGracePeriodSeconds: 0
257+
containers:
258+
- name: pgbench
259+
image: quay.io/ongres/postgres-util:v16.3-build-6.34
260+
command:
261+
- sh
262+
- -c
263+
- |
264+
pgbench postgresql://postgres:sup3rus3r@pgbouncer/postgres -i
265+
pgbench postgresql://postgres:sup3rus3r@pgbouncer/postgres -T 300 -c 4 -j 4 -P 2 --progress-timestamp
266+
EOF
267+
```
268+
269+
Wait for the Job to be started and print some progress of the benchmark, then create an restart SGDbOps that
270+
will restart the replica, perform a switchover and then will restart the primary.
271+
272+
```yaml
273+
cat << 'EOF' | kubectl replace --force -f -
274+
apiVersion: stackgres.io/v1
275+
kind: SGDbOps
276+
metadata:
277+
name: restart
278+
spec:
279+
op: restart
280+
sgCluster: cluster
281+
restart:
282+
method: InPlace
283+
EOF
284+
```
285+
286+
After the operation is completed wait for the completion of the Job and check no errors were raised:
287+
288+
```
289+
kubectl wait sgdbops restart --for=condition=Completed
290+
kubectl wait job pgbench --for=condition=Completed
291+
```
292+
293+
Check the pgbench Job's logs and you should not be able to find any failed connection!

stackgres-k8s/e2e/spec/abstract/patroni

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/bin/sh
2+
23
patroni_ports_check() {
34
if patroni_liveness_check
45
then
@@ -38,11 +39,11 @@ patroni_switchover(){
3839
}
3940

4041
patroni_direct_switchover_check(){
41-
patroni_curl -e "switchover" -p 8009 -XPOST -d '{"leader":"'"$CLUSTER_NAME-0"'"}' > /dev/null
42+
patroni_curl -e "switchover" -p 8009 -XPOST -d '{"leader":"'"$CLUSTER_NAME-0"'"}' > /dev/null
4243
}
4344

4445
patroni_liveness_check() {
45-
patroni_curl -e "cluster" > /dev/null
46+
patroni_curl -e "cluster" > /dev/null
4647
}
4748

4849
patroni_readiness_check() {
@@ -54,7 +55,7 @@ get_pod_ip(){
5455
}
5556

5657
patroni_curl(){
57-
PORT=8008
58+
PORT="${PATRONI_PORT:-8008}"
5859
POD_IP="$(get_pod_ip)"
5960
ENPOINT="cluster"
6061

stackgres-k8s/e2e/spec/default-mutator

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ e2e_test_install() {
1616

1717
PGCONFIG_FIELDS=36
1818
LOGS_PGCONFIG_FIELDS=37
19-
PGCONFIG_DEFAULTS_PARAMETERS=53
19+
PGCONFIG_DEFAULTS_PARAMETERS=52
2020

2121
PGBOUNCER_FIELDS=6
22-
PGBOUNCER_DEFAULTS_PARAMETERS=14
22+
PGBOUNCER_DEFAULTS_PARAMETERS=13
2323

2424
kubectl create namespace "$CLUSTER_NAMESPACE"
2525
deploy_curl_pod "$CLUSTER_NAMESPACE"

0 commit comments

Comments
 (0)