--- title: Tanzu for Postgres on Kubernetes 4.3をインストールしてSpring Bootアプリからアクセスするメモ tags: ["Kubernetes", "Tanzu", "PostgreSQL", "Spring Boot", "Helm", "Patroni"] categories: ["Dev", "CaaS", "Kubernetes", "TanzuSQL", "PostgreSQL"] date: 2025-11-29T12:44:31Z updated: 2025-11-29T12:49:38Z --- [以前の記事](/entries/844)でTanzu for Postgres on Kubernetes 4.0を試しました。 [Tanzu for Postgres on Kubernetes 4.3](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres-on-kubernetes/4-3/tnz-postgres-k8s/release-notes.html)がリリースされ、 インストールの方法が若干変わったため、ほぼ同じ内容で再度メモします。 **目次** ### Tanzu for Postgres on Kubernetes Docker Registryのアクセストークン取得 まずはこのHelmチャートが配布されているDocker Registryにアクセスするためのアクセストークンを取得します。 [Broadcom Support](https://support.broadcom.com/)にログインして、"My Downloads"を選択し、"postgres"で検索します。 検索結果から"VMware Tanzu for Postgres on Kubernetes"をクリックします。 image バージョン"4.3.1"の"Token Download"アイコンをクリックします。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/9636ad9c-5fb5-430c-9325-689d6e66a9d5.png) 次のようなダイアログが現れます。 image 下にスクロールするとRegistryの情報の記載が見えます。 image 次のコマンドで`tanzu-sql-postgres.packages.broadcom.com`にログインします。認証情報を自分のものに置き換えてください。 ```bash BC_SUPPORT_USERNAME=toshiaki.maki@broadcom.com BC_SUPPORT_PASSWORD=eyJ2ZXI************************************* docker login tanzu-sql-postgres.packages.broadcom.com -u ${BC_SUPPORT_USERNAME} -p ${BC_SUPPORT_PASSWORD} ``` これで次のコマンドで`tanzu-sql-postgres.packages.broadcom.com`からHelm Chartをpullできます。 ```bash helm pull oci://tanzu-sql-postgres.packages.broadcom.com/vmware-sql-postgres-operator --version v4.3.1 --untar --untardir /tmp ``` ただし、このトークンは有効期限があり、期限が切れるたびにローテートするのが面倒くさいので、今回はこのRegistryから直接Pullするのではなく、内部ネットワーク内のPrivate Registryである`registry.example.com`にリロケートした上で使用することとします。`registry.example.com`は自分の環境に変えてください。 > [!WARNING] > ライセンスの期限を超えて、このソフトウェアを使用することは禁止されています。リロケートした後も、ライセンス条件に従って使用してください。 ### Helm ChartとコンテナイメージをPrivate Registryへリロケート 次のコマンドで`registry.example.com`にログインします。認証情報を自分のものに置き換えてください。`registry.example.com`以外のRegistryでも良いです。 ```bash REGISTRY_USERNAME=username REGISTRY_PASSWORD=changeme docker login registry.example.com -u ${REGISTRY_USERNAME} -p ${REGISTRY_PASSWORD} ``` リロケートには[imgpkg](https://carvel.dev/imgpkg/)を使用します。 ```bash imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/vmware-sql-postgres-operator:v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator ``` リロケート後のHelm Chartにアクセスして設定可能なvaluesを確認します。 ```yaml $ helm show values oci://registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator --version v4.3.1 Pulled: registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator:v4.3.1 Digest: sha256:20cd8e22a7d0c903e608c3b21f9b344f81f19730047372e7bc499f5f0425b638 --- # specify the url for the docker image for the operator, e.g. gcr.io//postgres-operator operatorImage: tanzu-sql-postgres.packages.broadcom.com/postgres-operator:v4.3.1 # specify the docker image repository for postgres instance, e.g. gcr.io//postgres-instance instanceRegistryRepo: tanzu-sql-postgres.packages.broadcom.com/postgres-instance # specify the docker image repository for ferretdb instances (optional - only needed if deploying FerretDB instances) # e.g. gcr.io//ferret-db ferretDBImageRepo: "" # specify the name of the docker-registry secret to allow the cluster to authenticate with the container registry for pulling images dockerRegistrySecretName: regsecret # override the default self-signed cert-manager cluster issuer certManagerClusterIssuerName: postgres-operator-ca-certificate-cluster-issuer # override the namespace of the cert-manager installation namespace certManagerNamespace: cert-manager # set the resources for the postgres operator deployment resources: { } # limits: # cpu: 500m # memory: 300Mi # requests: # cpu: 500m # memory: 300Mi # enabled security context for the postgres-operator deployment and the managed instances, typically disabled on OpenShift clusters enableSecurityContext: true ``` OperatorのImageとPostgres InstanceのImageも`tanzu-sql-postgres.packages.broadcom.com`から`registry.example.com`にリロケートします。 4.3から、バージョン毎にPostgresインスタンス用のコンテナイメージが用意されるようになったため、必要なバージョン分をリロケートします。各バージョン毎に、full版とlite版のイメージがあります。 > [!NOTE] > full版とlite版ではインストールされているextensionに違いがあります。違いは[こちらのドキュメント](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres-on-kubernetes/4-3/tnz-postgres-k8s/supported_extension_v17.html)を参照してください。 次のコマンドでイメージをリロケートします。ここでは最新のPostgres 17.5のfull版とlite版をリロケートしています。 ```bash imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-operator:v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-operator imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v17.5-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v17.5-lite-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance ``` 他にサポートされているバージョンは[リリースノート](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres-on-kubernetes/4-3/tnz-postgres-k8s/release-notes.html)に記載されています。 必要であれば、次のようにまとめてリロケートします。 ```bash for ver in 17.4 16.9 16.8 15.13 15.12 14.18 14.17 13.21 13.20; do imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v${ver}-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v${ver}-lite-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance done ``` 本記事では扱いませんが、FerretDBも使えるようになったので、FerretDB用のコンテナイメージもリロケートしておきます。 ```bash imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/ferretdb:v2.3.1 --to-repo registry.example.com/tanzu-sql-postgres/ferretdb ``` ### Postgres Operatorのインストール ここでは`postgres-system` namespaceにPostgres Operatorをインストールします。 Docker Pull Secretの名前はデフォルトで`regsecret`です。 次のコマンドでnamespaceとsecretを作成します。 ```bash kubectl create namespace postgres-system kubectl create secret docker-registry regsecret \ --docker-server=registry.example.com \ --docker-username=${REGISTRY_USERNAME} \ --docker-password="${REGISTRY_PASSWORD}" \ -n postgres-system ``` Postgres Operatorの前提条件として[cert-manager](https://github.com/cert-manager)が必要なので、cert-managerが未インストールの場合は次のコマンドでインストールします。 ```bash kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.1/cert-manager.yaml ``` 次のコマンドでPostgres Operatorをインストールします。リロケート後のChartとコンテナイメージを指定します。 ```bash helm upgrade --install \ -n postgres-system \ postgres-operator \ oci://registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator \ --version v4.3.1 \ --set operatorImage=registry.example.com/tanzu-sql-postgres/postgres-operator:v4.3.1 \ --set instanceRegistryRepo=registry.example.com/tanzu-sql-postgres/postgres-instance \ --wait ``` しばらくするとインストールが完了します。`helm list`でインストール状態を確認できます。 ```bash $ helm list -n postgres-system NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION postgres-operator postgres-system 1 2025-11-28 11:25:22.277037 +0900 JST deployed vmware-sql-postgres-operator-v4.3.1 latest ``` 次のコマンドで、Postgres OperatorのPodの状態を確認します。 ```bash $ kubectl get pod -n postgres-system NAME READY STATUS RESTARTS AGE postgres-operator-559d4b6478-qs6rz 1/1 Running 0 35s ``` 次のコマンドで、このPostgres Operatorで利用可能なカスタムリソースを確認します。 ```bash $ kubectl api-resources --api-group=sql.tanzu.vmware.com NAME SHORTNAMES APIVERSION NAMESPACED KIND postgres pg sql.tanzu.vmware.com/v1 true Postgres postgresbackuplocations pgbkpl sql.tanzu.vmware.com/v1 true PostgresBackupLocation postgresbackups pgbkp sql.tanzu.vmware.com/v1 true PostgresBackup postgresbackupschedules sql.tanzu.vmware.com/v1 true PostgresBackupSchedule postgresferretdocumentdbs sql.tanzu.vmware.com/v1 true PostgresFerretDocumentDB postgresmigrations pgmigr sql.tanzu.vmware.com/v1 true PostgresMigration postgresrestores pgrstr sql.tanzu.vmware.com/v1 true PostgresRestore postgresversions pgver sql.tanzu.vmware.com/v1 false PostgresVersion postgresversionupgrades sql.tanzu.vmware.com/v1 true PostgresVersionUpgrade ``` 次のコマンドで、利用可能なPostgresのバージョンを確認します。 ```bash $ kubectl get postgresversion NAME DB VERSION postgres-13.20 13.20 postgres-13.20-lite 13.20 postgres-13.21 13.21 postgres-13.21-lite 13.21 postgres-14.17 14.17 postgres-14.17-lite 14.17 postgres-14.18 14.18 postgres-14.18-lite 14.18 postgres-15.12 15.12 postgres-15.12-lite 15.12 postgres-15.13 15.13 postgres-15.13-lite 15.13 postgres-16.8 16.8 postgres-16.8-lite 16.8 postgres-16.9 16.9 postgres-16.9-lite 16.9 postgres-17.4 17.4 postgres-17.4-lite 17.4 postgres-17.5 17.5 postgres-17.5-lite 17.5 ``` 次のコマンドで、Postgres 17.5のPostgresVersionリソースの内容を確認します。`instanceImage`にリロケート後のコンテナイメージが指定されていることを確認してください。 ```yaml $ kubectl get postgresversion postgres-17.5 -oyaml apiVersion: sql.tanzu.vmware.com/v1 kind: PostgresVersion metadata: annotations: meta.helm.sh/release-name: postgres-operator meta.helm.sh/release-namespace: postgres-system creationTimestamp: "2025-11-28T02:25:25Z" generation: 2 labels: app.kubernetes.io/managed-by: Helm name: postgres-17.5 resourceVersion: "34248" uid: abd8b13b-63b3-4a16-af3e-4dd8999e0248 spec: dbVersion: "17.5" instanceImage: registry.example.com/tanzu-sql-postgres/postgres-instance:v17.5-v4.3.1 ``` ### Postgresインスタンスの作成 Postgres Operatorを使って`demo` namespaceにPostgresインスタンスの作成します。 次のコマンドでnamespaceとdocker pull secretを作成します。 ```bash kubectl create namespace demo kubectl create secret docker-registry regsecret \ --docker-server=registry.example.com \ --docker-username=${REGISTRY_USERNAME} \ --docker-password="${REGISTRY_PASSWORD}" \ -n demo ``` 次のコマンドでPostgresインスタンスを作成します。Postgresバージョンは17.5を指定し、ストレージサイズは1Gi、HAモードを有効にしています。 ```yaml cat < /tmp/demo-postgres.yaml --- apiVersion: sql.tanzu.vmware.com/v1 kind: Postgres metadata: name: demo spec: postgresVersion: name: postgres-17.5 storageSize: 1Gi highAvailability: enabled: true --- EOF kubectl apply -f /tmp/demo-postgres.yaml -n demo ``` しばらくすると次のリソースが作成されます。 ```bash $ kubectl get postgres,sts,pod,svc,secret,certificate,pvc -n demo -l app=postgres NAME STATUS POSTGRES VERSION BACKUP LOCATION AGE postgres.sql.tanzu.vmware.com/demo Running 17.5 11m NAME READY AGE statefulset.apps/demo 2/2 11m NAME READY STATUS RESTARTS AGE pod/demo-0 4/4 Running 0 11m pod/demo-1 4/4 Running 0 11m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/demo ClusterIP 10.43.31.204 5432/TCP 11m service/demo-agent ClusterIP None 11m service/demo-read-only ClusterIP 10.43.243.24 5432/TCP 11m NAME TYPE DATA AGE secret/demo-additional-db-creds Opaque 1 11m secret/demo-app-user-db-secret servicebinding.io/postgresql 8 11m secret/demo-ca-bundle-secret Opaque 3 11m secret/demo-db-secret Opaque 5 11m secret/demo-empty-secret Opaque 0 11m secret/demo-internal-ssl-secret kubernetes.io/tls 3 11m secret/demo-metrics-secret Opaque 4 11m secret/demo-metrics-tls-secret kubernetes.io/tls 3 11m secret/demo-mounted-replication-ssl-secret Opaque 3 11m secret/demo-pgbackrest-secret Opaque 3 11m secret/demo-read-only-user-db-secret servicebinding.io/postgresql 8 11m secret/demo-read-write-user-db-secret servicebinding.io/postgresql 8 11m secret/demo-replication-ssl-secret kubernetes.io/tls 3 11m NAME READY SECRET AGE certificate.cert-manager.io/demo-internal-ssl-certificate True demo-internal-ssl-secret 11m certificate.cert-manager.io/demo-metrics-tls-certificate True demo-metrics-tls-secret 11m certificate.cert-manager.io/demo-replication-ssl-certificate True demo-replication-ssl-secret 11m NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE persistentvolumeclaim/demo-pgdata-demo-0 Bound pvc-89b7ef87-610c-4f5d-9abe-03c806c797e7 1Gi RWO synology-iscsi-storage 11m persistentvolumeclaim/demo-pgdata-demo-1 Bound pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134 1Gi RWO synology-iscsi-storage 11m persistentvolumeclaim/demo-pgwal-demo-0 Bound pvc-fee7725c-8943-4c8c-856f-8cb54298e680 1Gi RWO synology-iscsi-storage 11m persistentvolumeclaim/demo-pgwal-demo-1 Bound pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e 1Gi RWO synology-iscsi-storage 11m ``` 作成したPostgresインスタンスにコンテナの中から`psql`でアクセスします。 ```bash $ kubectl exec -it -n demo demo-0 -c pg-container -- psql demo psql (17.5 (VMware Postgres 17.5.0)) Type "help" for help. demo=# ``` 初期状態は次の通りです。 ``` demo=# \l List of databases Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges -----------+----------+----------+-----------------+---------+---------+--------+-----------+------------------------------ demo | postgres | UTF8 | libc | C.UTF-8 | C.UTF-8 | | | postgres=CTc/postgres + | | | | | | | | postgres_exporter=c/postgres+ | | | | | | | | pgappuser=CTc/postgres + | | | | | | | | pgrouser=c/postgres + | | | | | | | | pgrwuser=c/postgres postgres | postgres | UTF8 | libc | C.UTF-8 | C.UTF-8 | | | postgres=CTc/postgres + | | | | | | | | replication=CTc/postgres + | | | | | | | | postgres_exporter=c/postgres template0 | postgres | UTF8 | libc | C.UTF-8 | C.UTF-8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | libc | C.UTF-8 | C.UTF-8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres (4 rows) demo=# \d Did not find any relations. ``` 利用可能なExtensionは次の通りです。ベクトル検索のための[pgvector](https://github.com/pgvector/pgvector)やUberで使われている地理インデックスシステムである[h3](https://github.com/zachasme/h3-pg)などが利用可能です。 その他、時系列データベースの[TimescaleDB](https://github.com/timescale/timescaledb)やMongoDB互換機能を提供する[DocumentDB](https://github.com/documentdb/documentdb)も含まれています。 ``` demo=# SELECT * FROM pg_available_extensions ORDER BY name; name | default_version | installed_version | comment ------------------------------+-----------------+-------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- address_standardizer | 3.5.0 | | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. address_standardizer_data_us | 3.5.0 | | Address Standardizer US dataset example advanced_password_check | 1.4 | | Advanced Password Check amcheck | 1.4 | | functions for verifying relation integrity autoinc | 1.0 | | functions for autoincrementing fields bloom | 1.0 | | bloom access method - signature file based index bool_plperl | 1.0 | | transform between bool and plperl bool_plperlu | 1.0 | | transform between bool and plperlu btree_gin | 1.3 | | support for indexing common datatypes in GIN btree_gist | 1.7 | | support for indexing common datatypes in GiST citext | 1.6 | | data type for case-insensitive character strings cube | 1.5 | | data type for multidimensional cubes dblink | 1.2 | | connect to other PostgreSQL databases from within a database dict_int | 1.0 | | text search dictionary template for integers dict_xsyn | 1.0 | | text search dictionary template for extended synonym processing diskquota | 3.1 | | diskquota documentdb | 0.104-0 | | API surface for DocumentDB for PostgreSQL documentdb_core | 0.104-0 | | Core API surface for DocumentDB on PostgreSQL earthdistance | 1.2 | | calculate great-circle distances on the surface of the Earth file_fdw | 1.0 | | foreign-data wrapper for flat file access fuzzystrmatch | 1.2 | | determine similarities and distance between strings h3 | 4.2.2 | | H3 bindings for PostgreSQL h3_postgis | 4.2.2 | | H3 PostGIS integration hstore | 1.8 | | data type for storing sets of (key, value) pairs hstore_plperl | 1.0 | | transform between hstore and plperl hstore_plperlu | 1.0 | | transform between hstore and plperlu hstore_plpython3u | 1.0 | | transform between hstore and plpython3u insert_username | 1.0 | | functions for tracking who changed a table intagg | 1.1 | | integer aggregator and enumerator (obsolete) intarray | 1.5 | | functions, operators, and index support for 1-D arrays of integers isn | 1.2 | | data types for international product numbering standards jsonb_plperl | 1.0 | | transform between jsonb and plperl jsonb_plperlu | 1.0 | | transform between jsonb and plperlu jsonb_plpython3u | 1.0 | | transform between jsonb and plpython3u libversion | 2.0.0 | | libversion extension lo | 1.1 | | Large Object maintenance login_hook | 1.6 | | login_hook - hook to execute login_hook.login() at login time ltree | 1.3 | | data type for hierarchical tree-like structures ltree_plpython3u | 1.0 | | transform between ltree and plpython3u moddatetime | 1.0 | | functions for tracking last modification time orafce | 4.10 | | Functions and operators that emulate a subset of functions and packages from the Oracle RDBMS pageinspect | 1.12 | | inspect the contents of database pages at a low level pg_buffercache | 1.5 | | examine the shared buffer cache pg_cron | 1.6 | | Job scheduler for PostgreSQL pg_freespacemap | 1.2 | | examine the free space map (FSM) pg_graphql | 1.5.8 | | pg_graphql: GraphQL support pg_hint_plan | 1.7.0 | | optimizer hints for PostgreSQL pg_partman | 5.1.0 | | Extension to manage partitioned tables by time or ID pg_prewarm | 1.2 | | prewarm relation data pg_stat_monitor | 2.1 | | The pg_stat_monitor is a PostgreSQL Query Performance Monitoring tool, based on PostgreSQL contrib module pg_stat_statements. pg_stat_monitor provides aggregated statistics, client information, plan details including plan, and histogram information. pg_stat_statements | 1.11 | | track planning and execution statistics of all SQL statements executed pg_surgery | 1.0 | | extension to perform surgery on a damaged relation pg_trgm | 1.6 | | text similarity measurement and index searching based on trigrams pg_visibility | 1.2 | | examine the visibility map (VM) and page-level visibility info pg_wait_sampling | 1.1 | | sampling based statistics of wait events pg_walinspect | 1.1 | | functions to inspect contents of PostgreSQL Write-Ahead Log pgagent | 4.2 | | A PostgreSQL job scheduler pgaudit | 17.0 | | provides auditing functionality pgautofailover | 2.2 | | pg_auto_failover pgcrypto | 1.3 | | cryptographic functions pgrowlocks | 1.2 | | show row-level locking information pgstattuple | 1.5 | | show tuple-level statistics plperl | 1.0 | | PL/Perl procedural language plperlu | 1.0 | | PL/PerlU untrusted procedural language plpgsql | 1.0 | 1.0 | PL/pgSQL procedural language plpython3u | 1.0 | | PL/Python3U untrusted procedural language plr | 8.4.7 | | load R interpreter and execute R script from within a database postgis | 3.5.0 | | PostGIS geometry and geography spatial types and functions postgis_raster | 3.5.0 | | PostGIS raster types and functions postgis_tiger_geocoder | 3.5.0 | | PostGIS tiger geocoder and reverse geocoder postgis_topology | 3.5.0 | | PostGIS topology spatial types and functions postgres_fdw | 1.1 | | foreign-data wrapper for remote PostgreSQL servers refint | 1.0 | | functions for implementing referential integrity (obsolete) rum | 1.3 | | RUM index access method seg | 1.4 | | data type for representing line segments or floating-point intervals set_user | 4.1.0 | | similar to SET ROLE but with added logging sslinfo | 1.2 | | information about SSL certificates tablefunc | 1.0 | | functions that manipulate whole tables, including crosstab tcn | 1.0 | | Triggered change notifications timescaledb | 2.19.3 | | Enables scalable inserts and complex queries for time-series data tsm_system_rows | 1.0 | | TABLESAMPLE method which accepts number of rows as a limit tsm_system_time | 1.0 | | TABLESAMPLE method which accepts time in milliseconds as a limit unaccent | 1.1 | | text search dictionary that removes accents uuid-ossp | 1.1 | | generate universally unique identifiers (UUIDs) vector | 0.8.0 | | vector data type and ivfflat and hnsw access methods xml2 | 1.1 | | XPath querying and XSLT (86 rows) ``` 一旦quitします。 ``` demo=# \q ``` 次にPatroniのクラスタ情報を確認します。`demo-0`がRead Replica、`demo-1`がPrimaryであることがわかります。 ```bash $ kubectl exec -it -n demo demo-0 -c pg-container -- patronictl list + Cluster: demo (7577598800927174712) --------------+--------------+-----------+----+-----------+--------------------+ | Member | Host | Role | State | TL | Lag in MB | Tags | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ | demo-0 | demo-0.demo-agent.demo.svc.cluster.local | Leader | running | 1 | | cluster-name: demo | | demo-1 | demo-1.demo-agent.demo.svc.cluster.local | Sync Standby | streaming | 1 | 0 | cluster-name: demo | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ ``` 次のコマンドでRead Replicaの数を2に増やします。 ```yaml cat < /tmp/demo-postgres.yaml --- apiVersion: sql.tanzu.vmware.com/v1 kind: Postgres metadata: name: demo spec: postgresVersion: name: postgres-17.5 storageSize: 1Gi highAvailability: readReplicas: 2 enabled: true --- EOF kubectl apply -f /tmp/demo-postgres.yaml -n demo ``` 次のコマンドでリソースを確認します。まだReplicaを増やしている途中です。 ```bash $ kubectl get postgres,sts,pod,svc,secret,certificate,pvc -n demo -l app=postgres NAME STATUS POSTGRES VERSION BACKUP LOCATION AGE postgres.sql.tanzu.vmware.com/demo Pending 17.5 163m NAME READY AGE statefulset.apps/demo 2/3 163m NAME READY STATUS RESTARTS AGE pod/demo-0 4/4 Running 0 163m pod/demo-1 4/4 Running 0 163m pod/demo-2 0/4 Init:1/2 0 18s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/demo ClusterIP 10.43.31.204 5432/TCP 163m service/demo-agent ClusterIP None 163m service/demo-read-only ClusterIP 10.43.243.24 5432/TCP 163m NAME TYPE DATA AGE secret/demo-additional-db-creds Opaque 1 163m secret/demo-app-user-db-secret servicebinding.io/postgresql 8 163m secret/demo-ca-bundle-secret Opaque 3 162m secret/demo-db-secret Opaque 5 163m secret/demo-empty-secret Opaque 0 163m secret/demo-internal-ssl-secret kubernetes.io/tls 3 162m secret/demo-metrics-secret Opaque 4 163m secret/demo-metrics-tls-secret kubernetes.io/tls 3 162m secret/demo-mounted-replication-ssl-secret Opaque 3 162m secret/demo-pgbackrest-secret Opaque 3 163m secret/demo-read-only-user-db-secret servicebinding.io/postgresql 8 163m secret/demo-read-write-user-db-secret servicebinding.io/postgresql 8 163m secret/demo-replication-ssl-secret kubernetes.io/tls 3 162m NAME READY SECRET AGE certificate.cert-manager.io/demo-internal-ssl-certificate True demo-internal-ssl-secret 163m certificate.cert-manager.io/demo-metrics-tls-certificate True demo-metrics-tls-secret 163m certificate.cert-manager.io/demo-replication-ssl-certificate True demo-replication-ssl-secret 163m NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE persistentvolumeclaim/demo-pgdata-demo-0 Bound pvc-89b7ef87-610c-4f5d-9abe-03c806c797e7 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgdata-demo-1 Bound pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgdata-demo-2 Bound pvc-fbf5a41e-19ac-4b81-befa-3b1e215ba0c4 1Gi RWO synology-iscsi-storage 18s persistentvolumeclaim/demo-pgwal-demo-0 Bound pvc-fee7725c-8943-4c8c-856f-8cb54298e680 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgwal-demo-1 Bound pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgwal-demo-2 Bound pvc-7ab78448-701e-4de5-a343-812c098f5d08 1Gi RWO synology-iscsi-storage 18s ``` 途中の状態ではPatroniは次の状態になっています。 ```bash $ kubectl exec -it -n demo demo-0 -c pg-container -- patronictl list + Cluster: demo (7577598800927174712) --------------+--------------+-----------+----+-----------+--------------------+ | Member | Host | Role | State | TL | Lag in MB | Tags | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ | demo-0 | demo-0.demo-agent.demo.svc.cluster.local | Leader | running | 1 | | cluster-name: demo | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ | demo-1 | demo-1.demo-agent.demo.svc.cluster.local | Sync Standby | streaming | 1 | 0 | cluster-name: demo | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ | demo-2 | demo-2.demo-agent.demo.svc.cluster.local | Replica | stopped | | unknown | cluster-name: demo | | | | | | | | nosync: true | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ ``` しばらくするとクラスタの更新が完了します。 ```bash $ kubectl get postgres,sts,pod,svc,secret,certificate,pvc -n demo -l app=postgres NAME STATUS POSTGRES VERSION BACKUP LOCATION AGE postgres.sql.tanzu.vmware.com/demo Running 17.5 163m NAME READY AGE statefulset.apps/demo 3/3 163m NAME READY STATUS RESTARTS AGE pod/demo-0 4/4 Running 0 163m pod/demo-1 4/4 Running 0 163m pod/demo-2 4/4 Running 0 72s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/demo ClusterIP 10.43.31.204 5432/TCP 163m service/demo-agent ClusterIP None 163m service/demo-read-only ClusterIP 10.43.243.24 5432/TCP 163m NAME TYPE DATA AGE secret/demo-additional-db-creds Opaque 1 163m secret/demo-app-user-db-secret servicebinding.io/postgresql 8 163m secret/demo-ca-bundle-secret Opaque 3 163m secret/demo-db-secret Opaque 5 163m secret/demo-empty-secret Opaque 0 163m secret/demo-internal-ssl-secret kubernetes.io/tls 3 163m secret/demo-metrics-secret Opaque 4 163m secret/demo-metrics-tls-secret kubernetes.io/tls 3 163m secret/demo-mounted-replication-ssl-secret Opaque 3 163m secret/demo-pgbackrest-secret Opaque 3 163m secret/demo-read-only-user-db-secret servicebinding.io/postgresql 8 163m secret/demo-read-write-user-db-secret servicebinding.io/postgresql 8 163m secret/demo-replication-ssl-secret kubernetes.io/tls 3 163m NAME READY SECRET AGE certificate.cert-manager.io/demo-internal-ssl-certificate True demo-internal-ssl-secret 163m certificate.cert-manager.io/demo-metrics-tls-certificate True demo-metrics-tls-secret 163m certificate.cert-manager.io/demo-replication-ssl-certificate True demo-replication-ssl-secret 163m NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE persistentvolumeclaim/demo-pgdata-demo-0 Bound pvc-89b7ef87-610c-4f5d-9abe-03c806c797e7 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgdata-demo-1 Bound pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgdata-demo-2 Bound pvc-fbf5a41e-19ac-4b81-befa-3b1e215ba0c4 1Gi RWO synology-iscsi-storage 72s persistentvolumeclaim/demo-pgwal-demo-0 Bound pvc-fee7725c-8943-4c8c-856f-8cb54298e680 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgwal-demo-1 Bound pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e 1Gi RWO synology-iscsi-storage 163m persistentvolumeclaim/demo-pgwal-demo-2 Bound pvc-7ab78448-701e-4de5-a343-812c098f5d08 1Gi RWO synology-iscsi-storage 72s ``` Patroniクラスタは次の状態になっています。 ```bash $ kubectl exec -it -n demo demo-0 -c pg-container -- patronictl list + Cluster: demo (7577598800927174712) --------------+--------------+-----------+----+-----------+--------------------+ | Member | Host | Role | State | TL | Lag in MB | Tags | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ | demo-0 | demo-0.demo-agent.demo.svc.cluster.local | Leader | running | 1 | | cluster-name: demo | | demo-1 | demo-1.demo-agent.demo.svc.cluster.local | Sync Standby | streaming | 1 | 0 | cluster-name: demo | | demo-2 | demo-2.demo-agent.demo.svc.cluster.local | Replica | streaming | 1 | 0 | cluster-name: demo | +--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+ ``` 次のコマンドで各PostgresインスタンスのIPを確認します。 ```bash $ kubectl get pod -n demo -l app=postgres -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demo-0 4/4 Running 0 165m 10.42.1.23 192.168.11.51 demo-1 4/4 Running 0 165m 10.42.2.10 192.168.11.52 demo-2 4/4 Running 0 3m8s 10.42.0.7 192.168.11.150 ``` クラスタの外から、このクラスタにアクセスしてみます。次のコマンドで接続情報を確認します。 ```bash $ kubectl get secret -n demo demo-app-user-db-secret -ojson | jq '.data | map_values(@base64d)' { "database": "demo", "host": "demo.demo", "password": "Y08CQIn2SOOpRQwhCQO892p9Muf692", "port": "5432", "provider": "vmware", "type": "postgresql", "uri": "postgresql://pgappuser:Y08CQIn2SOOpRQwhCQO892p9Muf692@demo.demo:5432/demo", "username": "pgappuser" } ``` 次のコマンドでクラスタの外から`psql`を使用する準備をします。 ```bash $ kubectl run pg-client --rm -it --image=postgres:alpine -- bash If you don't see a command prompt, try pressing enter. pg-client:/# ``` 取得した接続情報を使い、次のように`psql`でクラスタにアクセスします。まずはread-writeなエンドポイントにアクセスします。 ```bash psql postgresql://pgappuser:Y08CQIn2SOOpRQwhCQO892p9Muf692@demo.demo:5432/demo ``` 次のコマンドで接続先のサーバー情報を表示します。`demo-0`のIPが返るので、Leaderに接続していることがわかります。 ``` demo=> SELECT inet_server_addr() AS server_ip, inet_server_port() AS server_port; server_ip | server_port ------------+------------- 10.42.1.23 | 5432 (1 row) ``` 次のSQLを実行し、テストデータをinsertします。 ```sql CREATE TABLE IF NOT EXISTS organization ( organization_id BIGINT PRIMARY KEY, organization_name VARCHAR(255) NOT NULL ); INSERT INTO organization(organization_id, organization_name) VALUES(1, 'foo'); INSERT INTO organization(organization_id, organization_name) VALUES(2, 'bar'); ``` insertしたデータを検索します。 ``` demo=> select organization_id,organization_name from organization; organization_id | organization_name -----------------+------------------- 1 | foo 2 | bar (2 rows) ``` いったんquitします。 ``` demo=> \q ``` 次にread-onlyなエンドポイントに接続します。 ```bash psql postgresql://pgappuser:Y08CQIn2SOOpRQwhCQO892p9Muf692@demo-read-only.demo:5432/demo ``` 次のコマンドで接続先のサーバー情報を表示します。`demo-2`のIPが返るので、Read Replicaに接続していることがわかります。 ``` demo=> SELECT inet_server_addr() AS server_ip, inet_server_port() AS server_port; server_ip | server_port -----------+------------- 10.42.0.7 | 5432 (1 row) ``` 次のコマンドで、先ほどinsertしたデータが検索できることを確認します。 ``` demo=> select organization_id,organization_name from organization; organization_id | organization_name -----------------+------------------- 1 | foo 2 | bar (2 rows) ``` insertを試みるとエラーになります。read-onlyなので期待通りです。 ``` demo=> INSERT INTO organization(organization_id, organization_name) VALUES(3, 'baz'); ERROR: cannot execute INSERT in a read-only transaction ``` quitします。 ``` demo=> \q ``` `psql`を実行したコンテナからexitします。 ```bash pg-client:/# exit ``` 作成したPostgresインスタンスを一旦削除します。 ```bash kubectl delete -f /tmp/demo-postgres.yaml -n demo ``` ### サンプルアプリのデプロイ 次にPostgresインスタンスにアプリから接続してみます。ここではサンプルアプリとして https://github.com/categolj/room-reservation を使用します。 動作確認したrevisionは[`cbcf650054bfac1562d473eed1aded84b020a4e7`](https://github.com/categolj/room-reservation/tree/cbcf650054bfac1562d473eed1aded84b020a4e7)です。 次のコマンドでこのアプリ向けのPostgresインスタンスを作成します。 ```yaml cat < /tmp/room-reservation-db.yaml --- apiVersion: sql.tanzu.vmware.com/v1 kind: Postgres metadata: name: room-reservation-db spec: postgresVersion: name: postgres-17.5 storageSize: 1Gi highAvailability: readReplicas: 2 enabled: true resources: data: limits: memory: 1024Mi requests: memory: 1024Mi --- EOF kubectl apply -f /tmp/room-reservation-db.yaml -n demo ``` 次のコマンドでPostgresインスタンスが作成されたことを確認します。 ```bash $ kubectl get postgres,pod,secret -n demo -l app=postgres NAME STATUS POSTGRES VERSION BACKUP LOCATION AGE postgres.sql.tanzu.vmware.com/room-reservation-db Running 17.5 50s NAME READY STATUS RESTARTS AGE pod/room-reservation-db-0 4/4 Running 0 50s pod/room-reservation-db-1 4/4 Running 0 50s pod/room-reservation-db-2 4/4 Running 0 50s NAME TYPE DATA AGE secret/room-reservation-db-additional-db-creds Opaque 1 50s secret/room-reservation-db-app-user-db-secret servicebinding.io/postgresql 8 50s secret/room-reservation-db-ca-bundle-secret Opaque 3 42s secret/room-reservation-db-db-secret Opaque 5 50s secret/room-reservation-db-empty-secret Opaque 0 50s secret/room-reservation-db-internal-ssl-secret kubernetes.io/tls 3 42s secret/room-reservation-db-metrics-secret Opaque 4 50s secret/room-reservation-db-metrics-tls-secret kubernetes.io/tls 3 41s secret/room-reservation-db-mounted-replication-ssl-secret Opaque 3 41s secret/room-reservation-db-pgbackrest-secret Opaque 3 50s secret/room-reservation-db-read-only-user-db-secret servicebinding.io/postgresql 8 50s secret/room-reservation-db-read-write-user-db-secret servicebinding.io/postgresql 8 50s secret/room-reservation-db-replication-ssl-secret kubernetes.io/tls 3 41s ``` 次にアプリをデプロイします。まずはシンプルにread-writeなエンドポイントにアクセスします。 `room-reservation-db-app-user-db-secret`は[Service Binding Specification for Kubernetes](https://github.com/servicebinding/spec)に対応しているので、[Spring Cloud Bindings](https://github.com/spring-cloud/spring-cloud-bindings)により、マウントした情報から自動で`spring.datasource.*`プロパティを設定します。 ```bash helm repo add stakater https://stakater.github.io/stakater-charts ``` ```yaml cat < room-reservation-values.yaml --- applicationName: room-reservation deployment: image: repository: ghcr.io/categolj/room-reservation tag: jvm pullPolicy: Always ports: - name: http containerPort: 8080 protocol: TCP env: SERVICE_BINDING_ROOT: value: /bindings spring.datasource.hikari.data-source-properties.sslmode: value: require logging.structured.format.console: value: ecs logging.level.root: value: error volumeMounts: room-reservation-db: mountPath: /bindings/room-reservation-db volumes: room-reservation-db: secret: secretName: room-reservation-db-app-user-db-secret resources: limits: memory: 1024Mi requests: memory: 1024Mi livenessProbe: enabled: true httpGet: path: /actuator/health/liveness port: 8080 scheme: HTTP readinessProbe: enabled: true httpGet: path: /actuator/health/readiness port: 8080 scheme: HTTP containerSecurityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false runAsNonRoot: true runAsUser: 1002 seccompProfile: type: RuntimeDefault affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchLabels: app.kubernetes.io/part-of: room-reservation topologyKey: kubernetes.io/hostname weight: 1 ingress: enabled: true annotations: cert-manager.io/cluster-issuer: letsencrypt hosts: - host: room-reservation.lan.ik.am paths: - path: / pathType: Prefix tls: - hosts: - room-reservation.lan.ik.am secretName: room-reservation-tls --- EOF ``` 次のコマンドでテンプレートを確認します。 ```bash helm template room-reservation stakater/application -n demo -f room-reservation-values.yaml ``` 次のコマンドでデプロイします。 ```bash helm upgrade --install room-reservation stakater/application -n demo -f room-reservation-values.yaml --wait ``` 次のPodとService、Ingressが作成されます。 ```bash $ kubectl get pod,svc,ing -n demo NAME READY STATUS RESTARTS AGE pod/room-reservation-677496b99d-f82wt 1/1 Running 0 18s pod/room-reservation-db-0 4/4 Running 0 97s pod/room-reservation-db-1 4/4 Running 0 97s pod/room-reservation-db-2 4/4 Running 0 97s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/room-reservation ClusterIP 10.43.67.214 8080/TCP 18s service/room-reservation-db ClusterIP 10.43.13.215 5432/TCP 97s service/room-reservation-db-agent ClusterIP None 97s service/room-reservation-db-read-only ClusterIP 10.43.41.99 5432/TCP 97s NAME CLASS HOSTS ADDRESS PORTS AGE ingress.networking.k8s.io/room-reservation traefik room-reservation.lan.ik.am 192.168.11.240 80, 443 18s ``` Ingressに設定したドメイン(この例では https://room-reservation.lan.ik.am )にアクセスすると、次の画面が表示されます: ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/d90a5daf-ecb8-40a6-9a00-30f87ba11865.png) Ingressがない場合は、Serviceに対するport-forwardingでこのアプリ (http://localhost:8080) にアクセスします。 ```bash kubectl port-forward -n demo svc/room-reservation 8080:8080 ``` ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/bab7297c-f6ff-4dd6-ac02-3c40da5d41b8.png) 部屋の予約ができることを確認します。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/93efb8a7-e3a8-4049-b829-e450622c371a.png) この部屋の予約情報をREST APIで取得します。(`date`と`roomId`パラメータを変更してください。) ``` $ curl "https://room-reservation.lan.ik.am/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" # or $ curl "http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" [{"reservationId":"019ac989-d981-7d1c-acf0-aed4ea19035e","roomId":"018422b2-4843-7a62-935b-b4e65649de3f","date":"2025-11-28","startTime":"09:00:00","endTime":"11:00:00","purpose":"Team Meeting","userId":"018422b2-4843-7a62-935b-b4e65649de46"}] ``` このAPIに対して、[`vegeta`](https://github.com/tsenart/vegeta)を使って負荷をかけてみます。`vegeta`は次のコマンドでインストールできます。 ```bash brew install vegeta ``` 次のコマンドで負荷をかけます。`kubectl port-forward -n demo svc/room-reservation 8080:8080`を実行している前提です。 ``` echo "GET http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" | vegeta attack -duration=10s | tee results.bin | vegeta report ``` 全てステータスコード200で返ればOKです。 ``` Requests [total, rate, throughput] 500, 50.10, 49.98 Duration [total, attack, wait] 10.005s, 9.979s, 25.081ms Latencies [min, mean, 50, 90, 95, 99, max] 12.846ms, 20.438ms, 18.752ms, 26.455ms, 28.479ms, 46.112ms, 89.53ms Bytes In [total, mean] 1000, 2.00 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:500 Error Set: ``` 次に負荷をかけている最中にこのPatroniクラスタをアップデートしてみます。次のコマンドで120秒負荷をかけます。 ```bash echo "GET http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" | vegeta attack -duration=120s | tee results.bin | vegeta report ```` `/tmp/room-reservation-db.yaml`を編集し、例えば、次のようにPostgresのメモリを少しだけ変更してみます。 ```yaml resources: data: limits: memory: 1025Mi requests: memory: 1025Mi ``` 別のターミナルでPatroniクラスタを更新します。 ```bash kubectl apply -f /tmp/room-reservation-db.yaml -n demo ``` vegetaの負荷テスト結果は次の通りです。5%ほどが500エラーです。 ``` Requests [total, rate, throughput] 6000, 50.01, 47.38 Duration [total, attack, wait] 2m0s, 2m0s, 13.718ms Latencies [min, mean, 50, 90, 95, 99, max] 6.81ms, 207.614ms, 21.618ms, 657.208ms, 2.011s, 2.07s, 2.506s Bytes In [total, mean] 1440990, 240.16 Bytes Out [total, mean] 0, 0.00 Success [ratio] 94.75% Status Codes [code:count] 200:5685 500:315 Error Set: 500 ``` 次のようなエラーログを確認できました。 ```json { "@timestamp": "2025-11-29T10:12:08.453672643Z", "log": { "level": "ERROR", "logger": "com.example.problem.ProblemControllerAdvice" }, "process": { "pid": 1, "thread": { "name": "tomcat-handler-11865" } }, "service": { "name": "room-reservation", "version": "0.0.1.cbcf650054bfac1562d473eed1aded84b020a4e7", "node": {} }, "message": "Unexpected runtime error occurred!", "traceId": "d3a7566524db016b5cce9a155bd1d6e0", "spanId": "115a387db4dd469b", "error": { "type": "org.springframework.transaction.CannotCreateTransactionException", "message": "Could not open JDBC Connection for transaction", "stack_trace": "org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction\n\tat org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:313)\n\tat org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:532)\n\tat org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:405)\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:639)\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:374)\n\tat org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationViewRepository$$SpringCGLIB$$0.findByRoomIdAndDate()\n\tat com.example.reservation.query.ReservationService.findByRoomIdAndDate(ReservationService.java:74)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)\n\tat com.example.config.ServiceLogAspect.logServiceMethod(ServiceLogAspect.java:37)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.config.ServiceLogAspect$$SpringCGLIB$$0.logServiceMethod()\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632)\n\tat org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)\n\tat org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationService$$SpringCGLIB$$0.findByRoomIdAndDate()\n\tat com.example.reservation.web.ReservationController.findReservation(ReservationController.java:47)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.web.ReservationController$$SpringCGLIB$$0.findReservation()\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.zalando.logbook.servlet.LogbookFilter.doFilter(LogbookFilter.java:76)\n\tat org.zalando.logbook.servlet.HttpFilter.doFilter(HttpFilter.java:32)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:114)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:732)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1769)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat java.base/java.lang.VirtualThread.run(Unknown Source)\nCaused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 2000ms (total=0, active=0, idle=0, waiting=100)\n\tat com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:709)\n\tat com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:188)\n\tat com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:146)\n\tat com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:127)\n\tat org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:269)\n\t... 99 common frames omitted\nCaused by: org.postgresql.util.PSQLException: Connection attempt timed out.\n\tat org.postgresql.Driver$ConnectThread.getResult(Driver.java:412)\n\tat org.postgresql.Driver.connect(Driver.java:305)\n\tat com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:139)\n\tat com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:368)\n\tat com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:205)\n\tat com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:483)\n\tat com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator.call(HikariPool.java:747)\n\tat com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator.call(HikariPool.java:726)\n\tat java.base/java.util.concurrent.FutureTask.run(Unknown Source)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)\n\tat java.base/java.lang.Thread.run(Unknown Source)\n" }, "ecs": { "version": "8.11" } } ``` 次のコマンドで負荷テストの結果をグラフ化します。 ```bash cat results.bin | vegeta plot > plot.html ``` ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/311de3c7-6d2a-4e98-959f-bfa7c28f9173.png) * primaryが切断され、read replica(1)がprimaryに昇格。primaryだったノードはread replica(3)に降格 * 昇格したprimaryが切断され、read replica(2)がprimaryに昇格 * 昇格したprimaryが切断され、read replica(3)がprimaryに昇格 の間にエラーが発生していたと思われます。 ### Read Replicaに対するRoutingを設定 アプリのダウンタイムを短くするため、参照形のアクセスはRead Replicaにroutingされるようにアプリ側を設定します。 このアプリには次の定義が含まれています。 * https://github.com/categolj/room-reservation/blob/cbcf650054bfac1562d473eed1aded84b020a4e7/src/main/java/com/example/config/ReadOnlyTransactionRoutingDataSource.java * https://github.com/categolj/room-reservation/blob/cbcf650054bfac1562d473eed1aded84b020a4e7/src/main/java/com/example/config/DataSourceConfig.java `spring.datasource.*`プロパティ同様に`read-replica.datasource.*`プロパティを設定すると`@Transactional(readOnly = true)`アノテーションが設定されたコードはRead Replicaにアクセスするようになります。 そこで`spring.datasource.*`プロパティは引き続きSpring Cloud Bindingで自動設定させつつ、 * `read-replica.datasource.url` * `read-replica.datasource.username` * `read-replica.datasource.password` プロパティはread-onlyなエンドポイントに対する接続情報が設定されるように環境変数を設定します。 ```yaml cat <<'EOF' > room-reservation-values.yaml --- applicationName: room-reservation deployment: image: repository: ghcr.io/categolj/room-reservation tag: jvm pullPolicy: Always ports: - name: http containerPort: 8080 protocol: TCP env: SERVICE_BINDING_ROOT: value: /bindings read-replica.datasource.url: value: jdbc:postgresql://${k8s.bindings.room-reservation-db.database}-read-only:${k8s.bindings.room-reservation-db.port}/${k8s.bindings.room-reservation-db.database} read-replica.datasource.username: value: ${spring.datasource.username} read-replica.datasource.password: value: ${spring.datasource.password} jdbc.datasource-proxy.enabled: value: "false" spring.datasource.hikari.data-source-properties.sslmode: value: require logging.structured.format.console: value: ecs logging.level.root: value: error volumeMounts: room-reservation-db: mountPath: /bindings/room-reservation-db volumes: room-reservation-db: secret: secretName: room-reservation-db-app-user-db-secret resources: limits: memory: 1024Mi requests: memory: 1024Mi livenessProbe: enabled: true httpGet: path: /actuator/health/liveness port: 8080 scheme: HTTP readinessProbe: enabled: true httpGet: path: /actuator/health/readiness port: 8080 scheme: HTTP containerSecurityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false runAsNonRoot: true runAsUser: 1002 seccompProfile: type: RuntimeDefault affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchLabels: app.kubernetes.io/part-of: room-reservation topologyKey: kubernetes.io/hostname weight: 1 ingress: enabled: true annotations: cert-manager.io/cluster-issuer: letsencrypt hosts: - host: room-reservation.lan.ik.am paths: - path: / pathType: Prefix tls: - hosts: - room-reservation.lan.ik.am secretName: room-reservation-tls --- EOF ``` 次のコマンドでデプロイします。 ```bash helm upgrade --install room-reservation stakater/application -n demo -f room-reservation-values.yaml --wait ``` > [!TIP] このアプリは[datasource-proxy](https://jdbc-observations.github.io/datasource-proxy/docs/snapshot/user-guide/index.html)を使用していましたが、データソースのroutingが想定通りに動作しなかったため、`jdbc.datasource-proxy.enabled=false`を設定してProxyを無効にしました。 設定変更後に、再びvegetaで負荷を与えます。 ```bash echo "GET http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" | vegeta attack -duration=90s | tee results.bin | vegeta report ```` 別のターミナルでPatroniクラスタを更新します。変更したメモリを元に戻して、applyしましょう。 ```bash kubectl apply -f /tmp/room-reservation-db.yaml -n demo ``` vegetaの負荷テスト結果は次の通りです。エラーが503の2件になりました。 (件数は実行時の条件によって変わるでしょう。) ``` Requests [total, rate, throughput] 6000, 50.01, 49.99 Duration [total, attack, wait] 2m0s, 2m0s, 12.602ms Latencies [min, mean, 50, 90, 95, 99, max] 6.813ms, 29.719ms, 19.347ms, 35.162ms, 67.051ms, 287.953ms, 609.905ms Bytes In [total, mean] 12382, 2.06 Bytes Out [total, mean] 0, 0.00 Success [ratio] 99.97% Status Codes [code:count] 200:5998 503:2 Error Set: 503 ``` グラフは次の通りです。80-90秒あたりに一瞬エラーが発生しています。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/8a719059-79c4-46f8-87df-7be75b05da15.png) 次のエラーログを確認できました。 ```json { "@timestamp": "2025-11-29T12:03:42.527566205Z", "log": { "level": "ERROR", "logger": "com.example.problem.ProblemControllerAdvice" }, "process": { "pid": 1, "thread": { "name": "tomcat-handler-4863" } }, "service": { "name": "room-reservation", "version": "0.0.1.cbcf650054bfac1562d473eed1aded84b020a4e7", "node": {} }, "message": "There is a problem with database access.", "traceId": "bc811b274e300785c0e5e3afff39dbba", "spanId": "b9816fca5faa3788", "error": { "type": "org.springframework.dao.DataAccessResourceFailureException", "message": "PreparedStatementCallback; SQL [SELECT reservation_id,\n room_id,\n date,\n start_time,\n end_time,\n purpose,\n user_id\nFROM reservation_view\nWHERE room_id = ? AND date = ?]; FATAL: terminating connection due to administrator command", "stack_trace": "org.springframework.dao.DataAccessResourceFailureException: PreparedStatementCallback; SQL [SELECT reservation_id,\n room_id,\n date,\n start_time,\n end_time,\n purpose,\n user_id\nFROM reservation_view\nWHERE room_id = ? AND date = ?]; FATAL: terminating connection due to administrator command\n\tat org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:121)\n\tat org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:107)\n\tat org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:116)\n\tat org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1556)\n\tat org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:677)\n\tat org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:723)\n\tat org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:748)\n\tat org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:804)\n\tat org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.query(NamedParameterJdbcTemplate.java:218)\n\tat org.springframework.jdbc.core.simple.DefaultJdbcClient$DefaultStatementSpec$NamedParamMappedQuerySpec.list(DefaultJdbcClient.java:366)\n\tat com.example.reservation.query.ReservationViewRepository.findByRoomIdAndDate(ReservationViewRepository.java:49)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380)\n\tat org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationViewRepository$$SpringCGLIB$$0.findByRoomIdAndDate()\n\tat com.example.reservation.query.ReservationService.findByRoomIdAndDate(ReservationService.java:74)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)\n\tat com.example.config.ServiceLogAspect.logServiceMethod(ServiceLogAspect.java:37)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.config.ServiceLogAspect$$SpringCGLIB$$0.logServiceMethod()\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632)\n\tat org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)\n\tat org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationService$$SpringCGLIB$$0.findByRoomIdAndDate()\n\tat com.example.reservation.web.ReservationController.findReservation(ReservationController.java:47)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.web.ReservationController$$SpringCGLIB$$0.findReservation()\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.zalando.logbook.servlet.LogbookFilter.doFilter(LogbookFilter.java:76)\n\tat org.zalando.logbook.servlet.HttpFilter.doFilter(HttpFilter.java:32)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:114)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:732)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1769)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat java.base/java.lang.VirtualThread.run(Unknown Source)\nCaused by: org.postgresql.util.PSQLException: FATAL: terminating connection due to administrator command\n\tat org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2734)\n\tat org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2421)\n\tat org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372)\n\tat org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:518)\n\tat org.postgresql.jdbc.PgStatement.execute(PgStatement.java:435)\n\tat org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:196)\n\tat org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:139)\n\tat com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)\n\tat com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)\n\tat org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:732)\n\tat org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)\n\t... 109 common frames omitted\n" }, "ecs": { "version": "8.11" } } ``` `FATAL: terminating connection due to administrator command`というエラーメッセージからわかるように、アクセスしているread replicaへのコネクションが再起動によって切断された"瞬断"が数件発生しただけのようです。 別のread replicaへのコネクションの復旧は瞬時に行われているようです。アクセスしているread replicaが先に再起動するか、後に再起動するかでエラーの回数は変わると思います。 ちなみにこのエラー(`DataAccessResourceFailureException`)はRetryで救済可能でしょう。 参照系のみですが、ダウンタイムはかなり改善されました。 --- Patroniを使ったTanzu for Postgres on Kubernetesを試しました。アプリ側の設定次第ではPostgresアップデート時のダウンタイムをかなり小さく抑えられそうです。