|
| 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! |
0 commit comments