使用 Zalando 将 PostgreSQL 部署到 GKE


本指南介绍如何使用 Zalando Postgres 运算符将 Postgres 集群部署到 Google Kubernetes Engine (GKE)。

PostgreSQL 是一种功能强大的开源对象关系型数据库系统,已持续数十年积极开发,使其在可靠性、功能稳健性和性能方面享有盛誉。

本指南适用于有兴趣将 PostgreSQL 作为数据库应用在 GKE 上运行(而不是使用 Cloud SQL for PostgreSQL)的平台管理员、云架构师和运维专业人员。

目标

  • 为 Postgres 规划和部署 GKE 基础架构
  • 部署和配置 Zalando Postgres 运算符
  • 使用运算符配置 Postgres,以确保可用性、安全性、可观测性和性能

优势

Zalando 具有以下优势:

  • 用于管理和配置 PostgreSQL 集群的声明式 Kubernetes 原生方法
  • Patroni 提供的高可用性
  • 使用 Cloud Storage 存储桶的备份管理支持
  • 对 Postgres 集群更改的滚动更新,包括快速次要版本更新
  • 使用自定义资源进行密码生成和轮替的声明式用户管理
  • TLS、证书轮替和连接池的支持
  • 集群克隆和数据复制

部署架构

在本教程中,您将使用 Zalando Postgres 运算符将一个高可用性 Postgres 集群部署和配置到 GKE。该集群具有一个主副本和两个备用(只读)副本(由 Patroni 管理)。Patroni 是由 Zalando 维护的开源解决方案,可为 Postgres 提供高可用性和自动故障切换功能。如果主副本发生故障,则一个备用副本会自动升级为主角色。

您还将部署 Postgres 的高可用性区域级 GKE 集群,其中多个 >Kubernetes 节点分布在多个可用区中。此设置有助于确保容错、可伸缩性和地理冗余。这样可支持滚动更新和维护,同时提供 SLA 以保证正常运行时间和可用性。如需了解详情,请参阅区域级集群

下图显示了在 GKE 集群中的多个节点和可用区上运行的 Postgres 集群:

在该图中,Postgres StatefulSet 部署在三个不同可用区的三个节点上。您可以通过以下方式控制 GKE 如何部署到节点:在 postgresql 自定义资源规范中设置所需的 Pod 亲和性和反亲和性规则。如果一个可用区发生故障,GKE 将使用推荐的配置在集群中的其他可用节点上重新调度 Pod。为了持久保留数据,您可使用 SSD 磁盘 (premium-rwo StorageClass),由于这类磁盘具备低延迟,高 IOPS 特征,因而在大多数情况下推荐用于高负载数据库。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

Cloud Shell 预安装了本教程所需的软件,包括 kubectlgcloud CLIHelmTerraform。如果您不使用 Cloud Shell,则必须安装 gcloud CLI。

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. Install the Google Cloud CLI.
  3. To initialize the gcloud CLI, run the following command:

    gcloud init
  4. Create or select a Google Cloud project.

    • Create a Google Cloud project:

      gcloud projects create PROJECT_ID

      Replace PROJECT_ID with a name for the Google Cloud project you are creating.

    • Select the Google Cloud project that you created:

      gcloud config set project PROJECT_ID

      Replace PROJECT_ID with your Google Cloud project name.

  5. Make sure that billing is enabled for your Google Cloud project.

  6. Enable the Compute Engine, IAM, GKE, Backup for GKE APIs:

    gcloud services enable compute.googleapis.com iam.googleapis.com container.googleapis.com gkebackup.googleapis.com
  7. Install the Google Cloud CLI.
  8. To initialize the gcloud CLI, run the following command:

    gcloud init
  9. Create or select a Google Cloud project.

    • Create a Google Cloud project:

      gcloud projects create PROJECT_ID

      Replace PROJECT_ID with a name for the Google Cloud project you are creating.

    • Select the Google Cloud project that you created:

      gcloud config set project PROJECT_ID

      Replace PROJECT_ID with your Google Cloud project name.

  10. Make sure that billing is enabled for your Google Cloud project.

  11. Enable the Compute Engine, IAM, GKE, Backup for GKE APIs:

    gcloud services enable compute.googleapis.com iam.googleapis.com container.googleapis.com gkebackup.googleapis.com
  12. Grant roles to your user account. Run the following command once for each of the following IAM roles: roles/storage.objectViewer, roles/container.admin, roles/iam.serviceAccountAdmin, roles/compute.admin, roles/gkebackup.admin, roles/monitoring.viewer

    gcloud projects add-iam-policy-binding PROJECT_ID --member="USER_IDENTIFIER" --role=ROLE
    • Replace PROJECT_ID with your project ID.
    • Replace USER_IDENTIFIER with the identifier for your user account. For example, user:myemail@example.com.

    • Replace ROLE with each individual role.

设置您的环境

如需设置您的环境,请按以下步骤操作

  1. 设置环境变量:

    export PROJECT_ID=PROJECT_ID
    export KUBERNETES_CLUSTER_PREFIX=postgres
    export REGION=us-central1
    

    PROJECT_ID 替换为您的 Google Cloud 项目 ID

  2. 克隆 GitHub 代码库:

    git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples
    
  3. 切换到工作目录:

    cd kubernetes-engine-samples/databases/postgres-zalando
    

创建集群基础架构

在本部分中,您将运行 Terraform 脚本以创建高可用性专用区域级 GKE 集群。

您可以使用 Standard 或 Autopilot 集群安装操作器。

Standard

下图显示了部署在三个不同可用区中的专用区域级 Standard GKE 集群:

部署此基础设施:

export GOOGLE_OAUTH_ACCESS_TOKEN=$(gcloud auth print-access-token)
terraform -chdir=terraform/gke-standard init
terraform -chdir=terraform/gke-standard apply \
  -var project_id=${PROJECT_ID} \
  -var region=${REGION} \
  -var cluster_prefix=${KUBERNETES_CLUSTER_PREFIX}

出现提示时,请输入 yes。完成此命令并使集群显示就绪状态可能需要几分钟时间。

Terraform 会创建以下资源:

  • Kubernetes 节点的 VPC 网络和专用子网
  • 用于通过 NAT 访问互联网的路由器
  • 专用 GKE 集群(在 us-central1 区域中)
  • 启用了自动扩缩的节点池(每个可用区一个到两个节点,每个可用区最少一个节点)
  • 具有日志记录和监控权限的 ServiceAccount
  • 用于灾难恢复的 Backup for GKE
  • Google Cloud Managed Service for Prometheus,用于监控集群

输出类似于以下内容:

...
Apply complete! Resources: 14 added, 0 changed, 0 destroyed.
...

Autopilot

下图显示了专用区域级 Autopilot GKE 集群:

部署基础架构:

export GOOGLE_OAUTH_ACCESS_TOKEN=$(gcloud auth print-access-token)
terraform -chdir=terraform/gke-autopilot init
terraform -chdir=terraform/gke-autopilot apply \
  -var project_id=${PROJECT_ID} \
  -var region=${REGION} \
  -var cluster_prefix=${KUBERNETES_CLUSTER_PREFIX}

出现提示时,请输入 yes。完成此命令并使集群显示就绪状态可能需要几分钟时间。

Terraform 会创建以下资源:

  • Kubernetes 节点的 VPC 网络和专用子网
  • 用于通过 NAT 访问互联网的路由器
  • 专用 GKE 集群(在 us-central1 区域中)
  • 具有日志记录和监控权限的 ServiceAccount
  • Google Cloud Managed Service for Prometheus,用于监控集群

输出类似于以下内容:

...
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
...

连接到集群

配置 kubectl 以与集群通信:

gcloud container clusters get-credentials ${KUBERNETES_CLUSTER_PREFIX}-cluster --region ${REGION}

将 Zalando 运算符部署到您的集群

使用 Helm 图表将 Zalando 运算符部署到您的 Kubernetes 集群。

  1. 添加 Zalando 运算符 Helm 图表代码库:

    helm repo add postgres-operator-charts https://opensource.zalando.com/postgres-operator/charts/postgres-operator
    
  2. 为 Zalando 运算符和 Postgres 集群创建命名空间:

    kubectl create ns postgres
    kubectl create ns zalando
    
  3. 使用 Helm 命令行工具部署 Zalando 运算符:

    helm install postgres-operator postgres-operator-charts/postgres-operator -n zalando \
        --set configKubernetes.enable_pod_antiaffinity=true \
        --set configKubernetes.pod_antiaffinity_preferred_during_scheduling=true \
        --set configKubernetes.pod_antiaffinity_topology_key="topology.kubernetes.io/zone" \
        --set configKubernetes.spilo_fsgroup="103"
    

    您无法直接在表示 Postgres 集群的自定义资源上配置 podAntiAffinity 设置。请改为在运算符设置中以全局方式为所有 Postgres 集群设置 podAntiAffinity 设置。

  4. 使用 Helm 检查 Zalando 运算符的部署状态:

    helm ls -n zalando
    

    输出类似于以下内容:

    NAME                 NAMESPACE    REVISION    UPDATED                                STATUS      CHART                       APP VERSION
    postgres-operator    zalando     1           2023-10-13 16:04:13.945614 +0200 CEST    deployed    postgres-operator-1.10.1    1.10.1
    

部署 Postgres

Postgres 集群实例的基本配置包括以下组件:

  • 三个 Postgres 副本:一个主副本和两个备用副本。
  • 一个 CPU 请求和两个 CPU 限制的 CPU 资源分配,具有 4 GB 内存请求和限制。
  • 为每个工作负载配置容忍设置、nodeAffinitiestopologySpreadConstraints,利用它们各自的节点池和不同的可用区来确保跨 Kubernetes 节点进行适当分布。

此配置表示创建可直接用于生产环境的 Postgres 集群所需的最少设置。

以下清单描述了一个 Postgres 集群:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  dockerImage: ghcr.io/zalando/spilo-15:3.0-p1
  teamId: "my-team"
  numberOfInstances: 3
  users:
    mydatabaseowner:
    - superuser
    - createdb
    myuser: []
  databases:
    mydatabase: mydatabaseowner
  postgresql:
    version: "15"
    parameters:
      shared_buffers: "32MB"
      max_connections: "10"
      log_statement: "all"
      password_encryption: scram-sha-256
  volume:
    size: 5Gi
    storageClass: premium-rwo
  enableShmVolume: true
  podAnnotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
  tolerations:
  - key: "app.stateful/component"
    operator: "Equal"
    value: "postgres-operator"
    effect: NoSchedule
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 1
      preference:
        matchExpressions:
        - key: "app.stateful/component"
          operator: In
          values:
          - "postgres-operator"
  resources:
    requests:
      cpu: "1"
      memory: 4Gi
    limits:
      cpu: "2"
      memory: 4Gi
  sidecars:
    - name: exporter
      image: quay.io/prometheuscommunity/postgres-exporter:v0.14.0
      args:
      - --collector.stat_statements
      ports:
      - name: exporter
        containerPort: 9187
        protocol: TCP
      resources:
        limits:
          cpu: 500m
          memory: 256M
        requests:
          cpu: 100m
          memory: 256M
      env:
      - name: "DATA_SOURCE_URI"
        value: "localhost/postgres?sslmode=require"
      - name: "DATA_SOURCE_USER"
        value: "$(POSTGRES_USER)"
      - name: "DATA_SOURCE_PASS"
        value: "$(POSTGRES_PASSWORD)"

此清单包含以下字段:

  • spec.teamId:您选择的集群对象的前缀
  • spec.numberOfInstances:集群的实例总数
  • spec.users:具有权限的用户列表
  • spec.databases:数据库列表,格式为 dbname: ownername
  • spec.postgresql:postgres 参数
  • spec.volume:永久性磁盘参数
  • spec.tolerations:容忍 Pod 模板,允许在 pool-postgres 节点上调度集群 Pod
  • spec.nodeAffinitynodeAffinity Pod 模板,向 GKE 告知首选在 pool-postgres 节点上调度集群 Pod。
  • spec.resources:集群 Pod 的请求和限制
  • spec.sidecars:Sidecar 容器列表,其中包含 postgres-exporter

如需了解详情,请参阅 Postgres 文档中的集群清单参考文档

创建基本 Postgres 集群

  1. 使用基本配置创建新的 Postgres 集群:

    kubectl apply -n postgres -f manifests/01-basic-cluster/my-cluster.yaml
    

    此命令会创建 Zalando 运算符的 PostgreSQL 自定义资源,其中包含以下内容:

    • CPU 和内存请求和限制
    • 在 GKE 节点之间分发预配的 Pod 副本的污点及亲和性。
    • 一个数据库
    • 两个具有数据库所有者权限的用户
    • 一个没有任何权限的用户
  2. 等待 GKE 启动所需工作负载:

    kubectl wait pods -l cluster-name=my-cluster  --for condition=Ready --timeout=300s -n postgres
    

    此命令可能需要几分钟时间才能完成。

  3. 验证 GKE 是否已创建 Postgres 工作负载:

    kubectl get pod,svc,statefulset,deploy,pdb,secret -n postgres
    

    输出类似于以下内容:

    NAME                                    READY   STATUS  RESTARTS   AGE
    pod/my-cluster-0                        1/1     Running   0         6m41s
    pod/my-cluster-1                        1/1     Running   0         5m56s
    pod/my-cluster-2                        1/1     Running   0         5m16s
    pod/postgres-operator-db9667d4d-rgcs8   1/1     Running   0         12m
    
    NAME                        TYPE        CLUSTER-IP  EXTERNAL-IP   PORT(S)   AGE
    service/my-cluster          ClusterIP   10.52.12.109   <none>       5432/TCP   6m43s
    service/my-cluster-config   ClusterIP   None        <none>      <none>  5m55s
    service/my-cluster-repl     ClusterIP   10.52.6.152 <none>      5432/TCP   6m43s
    service/postgres-operator   ClusterIP   10.52.8.176 <none>      8080/TCP   12m
    
    NAME                        READY   AGE
    statefulset.apps/my-cluster   3/3   6m43s
    
    NAME                                READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/postgres-operator   1/1     1           1           12m
    
    NAME                                                MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
    poddisruptionbudget.policy/postgres-my-cluster-pdb   1              N/A             0                   6m44s
    
    NAME                                                            TYPE                DATA   AGE
    secret/my-user.my-cluster.credentials.postgresql.acid.zalan.do  Opaque              2   6m45s
    secret/postgres.my-cluster.credentials.postgresql.acid.zalan.do   Opaque            2   6m44s
    secret/sh.helm.release.v1.postgres-operator.v1                  helm.sh/release.v1   1      12m
    secret/standby.my-cluster.credentials.postgresql.acid.zalan.do  Opaque              2   6m44s
    secret/zalando.my-cluster.credentials.postgresql.acid.zalan.do  Opaque              2   6m44s
    

操作器创建以下资源:

  • 一个 Postgres StatefulSet,控制 Postgres 的三个 Pod 副本
  • 一个 PodDisruptionBudgets,确保至少有一个可用副本
  • my-cluster Service,仅以主副本为目标
  • my-cluster-repl Service,公开 Postgres 端口以用于传入连接以及 Postgres 副本之间的复制
  • my-cluster-config 无头 Service,用于获取正在运行的 Postgres Pod 副本的列表
  • 包含用户凭据的 Secret,用于访问数据库以及在 Postgres 节点之间进行复制

向 Postgres 进行身份验证

您可以创建 Postgres 用户并为其分配数据库权限。例如,以下清单描述了一个分配用户和角色的自定义资源:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  ...
  users:
    mydatabaseowner:
    - superuser
    - createdb
    myuser: []
  databases:
    mydatabase: mydatabaseowner

在此清单中:

  • mydatabaseowner 用户具有 SUPERUSERCREATEDB 角色,这些角色允许获得完整管理员权限(例如管理 Postgres 配置、创建新数据库、表和用户)。您不应与客户端共享此用户。例如,Cloud SQL 不允许客户访问具有 SUPERUSER 角色的用户。
  • myuser 用户尚未分配角色。这遵循了使用 SUPERUSER 创建具有最小权限的用户的最佳实践。精细权限由 mydatabaseowner 授予给 myuser。为了维护安全性,您应该只与客户端应用共享 myuser 凭据。

存储密码

您应该使用 scram-sha-256 推荐方法存储密码。例如,以下清单描述了一个使用 postgresql.parameters.password_encryption 字段指定 scram-sha-256 加密的自定义资源:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  ...
  postgresql:
    parameters:
      password_encryption: scram-sha-256

轮替用户凭据

您可以使用 Zalando 轮替存储在 Kubernetes Secret 中的用户凭据。例如,以下清单描述了一个使用 usersWithSecretRotation 字段定义用户凭据轮替的自定义资源:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  ...
  usersWithSecretRotation:
  - myuser
  - myanotheruser
  - ...

身份验证示例:连接到 Postgres

本部分介绍如何部署示例 Postgres 客户端,并使用来自 Kubernetes Secret 的密码连接到数据库。

  1. 运行客户端 Pod 以与您的 Postgres 集群进行交互:

    kubectl apply -n postgres -f manifests/02-auth/client-pod.yaml
    

    myusermydatabaseowner 用户的凭据从相关 Secret 中获取,并作为环境变量装载到 Pod。

  2. 在 Pod 准备就绪后连接到 Pod:

    kubectl wait pod postgres-client --for=condition=Ready --timeout=300s -n postgres
    kubectl exec -it postgres-client -n postgres -- /bin/bash
    
  3. 使用 myuser 凭据连接到 Postgres 并尝试创建新表:

    PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "CREATE TABLE test (id serial PRIMARY KEY, randomdata VARCHAR ( 50 ) NOT NULL);"
    

    该命令应该失败,并显示类似于以下内容的错误:

    ERROR:  permission denied for schema public
    LINE 1: CREATE TABLE test (id serial PRIMARY KEY, randomdata VARCHAR...
    

    该命令失败是因为默认情况下,未分配权限的用户只能登录 Postgres 和列出数据库。

  4. 使用 mydatabaseowner 凭据创建表,并将表的所有权限都授予给 myuser

    PGPASSWORD=$OWNERPASSWORD psql \
      -h my-cluster \
      -U $OWNERUSERNAME \
      -d mydatabase \
      -c "CREATE TABLE test (id serial PRIMARY KEY, randomdata VARCHAR ( 50 ) NOT NULL);GRANT ALL ON test TO myuser;GRANT ALL ON SEQUENCE test_id_seq TO myuser;"
    

    输出类似于以下内容:

    CREATE TABLE
    GRANT
    GRANT
    
  5. 使用 myuser 凭据将随机数据插入表中:

    for i in {1..10}; do
      DATA=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13)
      PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "INSERT INTO test(randomdata) VALUES ('$DATA');"
    done
    

    输出类似于以下内容:

    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    
  6. 获取插入的值:

    PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "SELECT * FROM test;"
    

    输出类似于以下内容:

    id |  randomdata
    ----+---------------
      1 | jup9HYsAjwtW4
      2 | 9rLAyBlcpLgNT
      3 | wcXSqxb5Yz75g
      4 | KoDRSrx3muD6T
      5 | b9atC7RPai7En
      6 | 20d7kC8E6Vt1V
      7 | GmgNxaWbkevGq
      8 | BkTwFWH6hWC7r
      9 | nkLXHclkaqkqy
     10 | HEebZ9Lp71Nm3
    (10 rows)
    
  7. 退出 Pod shell:

    exit
    

了解 Prometheus 如何收集 Postgres 集群的指标

下图展示了 Prometheus 指标收集的工作原理:

在该图中,GKE 专用集群包含:

  • Postgres Pod,用于通过路径 / 和端口 9187 收集指标
  • 基于 Prometheus 的收集器,用于处理 Postgres Pod 中的指标
  • 将指标发送到 Cloud Monitoring 的 PodMonitoring 资源

Google Cloud Managed Service for Prometheus 支持 Prometheus 格式的指标收集。Cloud Monitoring 使用集成式信息中心来处理 Postgres 指标。

Zalando 使用 postgres_exporter 组件作为 Sidecar 容器,采用 Prometheus 格式公开集群指标。

  1. 创建 PodMonitoring 资源,以按 labelSelector 爬取指标:

    kubectl apply -n postgres -f manifests/03-prometheus-metrics/pod-monitoring.yaml
    
  2. 在 Google Cloud 控制台中,转到 GKE 集群信息中心页面。

    转到 GKE 集群信息中心

    信息中心显示非零指标提取率。

  3. 在 Google Cloud 控制台中,转到信息中心页面。

    转到“信息中心”

  4. 打开 PostgreSQL Prometheus 概览信息中心。信息中心会显示提取的行数。信息中心可能需要几分钟时间才能完成自动预配。

  5. 连接到客户端 Pod:

    kubectl exec -it postgres-client -n postgres -- /bin/bash
    
  6. 插入随机数据:

    for i in {1..100}; do
      DATA=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13)
      PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "INSERT INTO test(randomdata) VALUES ('$DATA');"
    done
    
  7. 刷新页面。 图表会更新,以显示实际数据库状态。

  8. 退出 Pod shell:

    exit
    

清理

删除项目

    Delete a Google Cloud project:

    gcloud projects delete PROJECT_ID

删除各个资源

  1. 设置环境变量。

    export PROJECT_ID=${PROJECT_ID}
    export KUBERNETES_CLUSTER_PREFIX=postgres
    export REGION=us-central1
    
  2. 运行 terraform destroy 命令:

    export GOOGLE_OAUTH_ACCESS_TOKEN=$(gcloud auth print-access-token)
    terraform  -chdir=terraform/FOLDER destroy \
      -var project_id=${PROJECT_ID} \
      -var region=${REGION} \
      -var cluster_prefix=${KUBERNETES_CLUSTER_PREFIX}
    

    FOLDER 替换为 gke-autopilotgke-standard

    出现提示时,请输入 yes

  3. 查找所有未挂接的磁盘:

    export disk_list=$(gcloud compute disks list --filter="-users:* AND labels.name=${KUBERNETES_CLUSTER_PREFIX}-cluster" --format "value[separator=|](name,zone)")
    
  4. 删除磁盘:

    for i in $disk_list; do
      disk_name=$(echo $i| cut -d'|' -f1)
      disk_zone=$(echo $i| cut -d'|' -f2|sed 's|.*/||')
      echo "Deleting $disk_name"
      gcloud compute disks delete $disk_name --zone $disk_zone --quiet
    done
    
  5. 删除 GitHub 代码库:

    rm -r ~/kubernetes-engine-samples/
    

后续步骤