-
Notifications
You must be signed in to change notification settings - Fork 512
docs: guide for zero-downtime PostgreSQL major-version upgrade #37795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tylerhartwig
wants to merge
1
commit into
MaterializeInc:main
Choose a base branch
from
tylerhartwig:docs/postgres-major-version-upgrade
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+255
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
254 changes: 254 additions & 0 deletions
254
doc/user/content/ingest-data/postgres/major-version-upgrade.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| --- | ||
| title: "Guide: Upgrade the major version of your PostgreSQL source" | ||
| description: "How to upgrade the major version of the PostgreSQL database behind a Materialize source with zero downtime." | ||
| menu: | ||
| main: | ||
| parent: "postgresql" | ||
| name: "Upgrade the major version" | ||
| weight: 90 | ||
| --- | ||
|
|
||
| This guide shows you how to upgrade the **major version** (for example, | ||
| PostgreSQL 15 to 16) of the upstream database behind a Materialize | ||
| [PostgreSQL source](/sql/create-source/postgres/) while keeping Materialize | ||
| serving fresh results. | ||
|
|
||
| The challenge specific to Materialize is that the source holds an active | ||
| [logical replication slot](https://www.postgresql.org/docs/current/logical-replication.html) | ||
| on the upstream primary. A major-version upgrade replaces the primary with a | ||
| new instance running the new version, and the replication slot does **not** | ||
| carry over automatically. How you handle the slot determines whether reads stay | ||
| fresh through the upgrade. | ||
|
|
||
| This guide covers two approaches: | ||
|
|
||
| | Approach | Reads stay fresh? | Who moves the data? | Use when | | ||
| |----------|-------------------|---------------------|----------| | ||
| | [Parallel source (self-managed)](#approach-a-parallel-source-self-managed) | **Yes, continuously** | You (native logical replication) | Zero read downtime is a hard requirement. | | ||
| | [Amazon RDS Blue/Green](#approach-b-amazon-rds-bluegreen-managed) | No — bounded freshness gap | AWS | A brief staleness window is acceptable and you want AWS to manage the cutover. | | ||
|
|
||
| {{< note >}} | ||
| Both approaches involve a brief **write** freeze on the application at the | ||
| moment of cutover, which is inherent to any major-version upgrade. The | ||
| difference is whether Materialize keeps serving fresh **reads** throughout. | ||
| {{< /note >}} | ||
|
|
||
| ## Approach A: Parallel source (self-managed) | ||
|
|
||
| This is the only approach that keeps Materialize continuously fresh. You keep | ||
| the existing source running against the old primary the entire time, build the | ||
| new-version primary in parallel, and hydrate a second source against it before | ||
| cutting consumers over. | ||
|
|
||
| ```text | ||
| app writes ──▶ PG (old major) ──native logical replication──▶ PG (new major) | ||
| │ │ | ||
| ▼ ▼ | ||
| existing source new source | ||
| (serving fresh) (snapshotting + catching up) | ||
| ``` | ||
|
|
||
| ### 1. Stand up the new-version primary | ||
|
|
||
| Provision a new PostgreSQL instance running the target major version, with | ||
| logical replication enabled (for RDS, `rds.logical_replication = 1`; see | ||
| [enable logical replication](/ingest-data/postgres/amazon-rds/#1-enable-logical-replication)). | ||
|
|
||
| Because logical replication does **not** replicate DDL, pre-create the schema of | ||
| the replicated tables on the new instance to match the old one, including | ||
| `REPLICA IDENTITY FULL`: | ||
|
|
||
| ```sql | ||
| -- On the new-version primary, recreate the table definitions. | ||
| ALTER TABLE my_table REPLICA IDENTITY FULL; | ||
| ``` | ||
|
|
||
| ### 2. Replicate old ▶ new with native logical replication | ||
|
|
||
| On the **old** primary, create (or reuse) a publication for the tables you | ||
| replicate to Materialize: | ||
|
|
||
| ```sql | ||
| -- On the old primary. | ||
| CREATE PUBLICATION upgrade_pub FOR TABLE my_table; | ||
| ``` | ||
|
|
||
| On the **new** primary, subscribe to it: | ||
|
|
||
| ```sql | ||
| -- On the new primary. | ||
| CREATE SUBSCRIPTION upgrade_sub | ||
| CONNECTION 'host=OLD_PRIMARY_HOST port=5432 dbname=DB user=REPL_USER password=...' | ||
| PUBLICATION upgrade_pub; | ||
| ``` | ||
|
|
||
| A single publication can feed **multiple** subscribers, so the old primary now | ||
| has two logical consumers at once — Materialize's slot and this subscription's | ||
| slot — coexisting without conflict. Confirm both are active: | ||
|
|
||
| ```sql | ||
| -- On the old primary. | ||
| SELECT slot_name, plugin, active FROM pg_replication_slots; | ||
| ``` | ||
|
|
||
| {{< warning >}} | ||
| Ensure the new primary can reach the old primary on the PostgreSQL port. In a | ||
| cloud VPC, the upstream endpoint typically resolves to a **private** IP, so the | ||
| security group must allow instance-to-instance traffic — not just the client's | ||
| IP. | ||
| {{< /warning >}} | ||
|
|
||
| ### 3. Create a parallel source in Materialize | ||
|
|
||
| Leave your existing source untouched and serving. Create a **second** | ||
| connection and source pointed at the new primary, in its own schema so the | ||
| subsources and downstream objects don't collide: | ||
|
|
||
| ```mzsql | ||
| CREATE SCHEMA upgrade; | ||
|
|
||
| CREATE SOURCE upgrade.my_source | ||
| FROM POSTGRES CONNECTION pg_new (PUBLICATION 'upgrade_pub') | ||
| FOR ALL TABLES; | ||
| ``` | ||
|
|
||
| The new source begins [snapshotting](/ingest-data/#snapshotting) and then | ||
| catches up. Snapshot time scales with data volume and is the long pole of the | ||
| upgrade — but it happens **in the background** while the existing source keeps | ||
| serving fresh. Recreate your downstream views and materialized views in the | ||
| `upgrade` schema on top of the new source. | ||
|
|
||
| ### 4. Cut over | ||
|
|
||
| Once the new source has caught up, perform a coordinated cutover: | ||
|
|
||
| 1. **Freeze application writes** to the old primary. | ||
| 1. **Drain replication:** wait until the new primary matches the old primary | ||
| exactly. Comparing an order-independent fingerprint across both databases is | ||
| a reliable check: | ||
|
|
||
| ```sql | ||
| SELECT count(*), sum(id), min(id), max(id), count(DISTINCT id) FROM my_table; | ||
| ``` | ||
|
|
||
| 1. **Synchronize sequences.** Native logical replication does **not** advance | ||
| sequences on the target. Copy each sequence's value forward, or post-cutover | ||
| inserts will collide on the primary key: | ||
|
|
||
| ```sql | ||
| -- Read on the old primary... | ||
| SELECT last_value FROM my_table_id_seq; | ||
| -- ...then set on the new primary. | ||
| SELECT setval('my_table_id_seq', <last_value>); | ||
| ``` | ||
|
|
||
| 1. **Drain both sources** in Materialize so they reflect the frozen state, then | ||
| verify the fingerprint matches across the old primary, new primary, existing | ||
| source, and new source. | ||
| 1. **Repoint consumers** from the existing source's objects to the `upgrade` | ||
| schema's objects. | ||
| 1. **Resume application writes**, now pointed at the new primary. | ||
|
|
||
| Throughout the cutover, reads against Materialize stay live and fresh — the | ||
| existing source serves until the flip, and the new source is already caught up | ||
| at the flip. | ||
|
|
||
| ### 5. Decommission | ||
|
|
||
| Drop the subscription on the new primary, drop the old source in Materialize, | ||
| and decommission the old primary. | ||
|
|
||
| ## Approach B: Amazon RDS Blue/Green (managed) | ||
|
|
||
| [Amazon RDS Blue/Green deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html) | ||
| let AWS build the upgraded "green" instance, keep it in sync, and swap | ||
| endpoints at switchover. This is operationally simpler, but it **cannot keep | ||
| Materialize continuously fresh** because of the constraint below. | ||
|
|
||
| {{< warning >}} | ||
| **A Blue/Green deployment cannot be created while a Materialize source is | ||
| attached to the blue instance.** Materialize's logical replication slot counts | ||
| as *external replication*, and RDS refuses to create the deployment | ||
| (`CREATING_READ_REPLICA_OF_SOURCE` fails with *"external replication on the blue | ||
| primary instance"*). You must [drop the | ||
| source](/sql/drop-source/) first, which starts an unavoidable **freshness | ||
| gap**. No RDS flag bypasses this. | ||
| {{< /warning >}} | ||
|
|
||
| ### 1. Detach Materialize and create the deployment | ||
|
|
||
| 1. `DROP SOURCE` in Materialize to release the replication slot. Confirm | ||
| `pg_replication_slots` no longer lists Materialize's slot. **The freshness | ||
| gap starts here.** | ||
| 1. Create the Blue/Green deployment, targeting the new major version with a | ||
| parameter group that has `rds.logical_replication = 1`. Wait for the green | ||
| instance to become **Available** (a major-version upgrade can take tens of | ||
| minutes; it scales with database size). | ||
|
|
||
| The publication and `REPLICA IDENTITY` settings are copied from blue into green. | ||
|
|
||
| ### 2. Attach Materialize to green | ||
|
|
||
| The green instance is a logical replica that is itself a primary | ||
| (`pg_is_in_recovery()` returns `false`), so it can host Materialize's slot. | ||
| Create a source against **green's temporary endpoint** and let it snapshot and | ||
| catch up: | ||
|
|
||
| ```mzsql | ||
| CREATE SOURCE my_source | ||
| FROM POSTGRES CONNECTION pg_green (PUBLICATION 'mz_source') | ||
| FOR ALL TABLES; | ||
| ``` | ||
|
|
||
| **The freshness gap ends when this source has caught up.** As in Approach A, | ||
| snapshot time scales with data volume. | ||
|
|
||
| ### 3. Switch over and repoint the source | ||
|
|
||
| Trigger the Blue/Green switchover. AWS promotes green **in place** — it takes | ||
| over the production endpoint, while the old blue is demoted and renamed. The | ||
| switchover completes in tens of seconds, during which the application has a | ||
| brief write pause. | ||
|
|
||
| Because green is promoted in place (same instance, same LSN timeline), its | ||
| replication slot is preserved. Repoint the source's connection at the | ||
| production endpoint so Materialize resumes from the **same slot with no | ||
| re-snapshot**: | ||
|
|
||
| ```mzsql | ||
| ALTER CONNECTION pg_green SET (HOST = 'PRODUCTION_ENDPOINT') WITH (VALIDATE = true); | ||
| ``` | ||
|
|
||
| After the switchover, green's temporary endpoint is removed; a source restart | ||
| would otherwise fail to resolve it, so run the `ALTER CONNECTION` promptly. | ||
|
|
||
| {{< note >}} | ||
| This seamless resume works **only** because a Blue/Green switchover promotes | ||
| green in place, preserving the slot and LSN timeline. Repointing a source at an | ||
| unrelated instance (one that doesn't carry the same slot) forces a fresh | ||
| snapshot instead. | ||
| {{< /note >}} | ||
|
|
||
| ### 4. Clean up | ||
|
|
||
| Delete the Blue/Green deployment and the demoted old instance. | ||
|
|
||
| ## Considerations | ||
|
|
||
| - **DDL is not replicated.** For Approach A, pre-create the target schema on the | ||
| new instance before subscribing. | ||
| - **Sequences are not advanced on the target** by native logical replication. | ||
| Synchronize them at cutover (Approach A) or RDS handles it at switchover | ||
| (Approach B). | ||
| - **`REPLICA IDENTITY FULL`** must be set on replicated tables so Materialize | ||
| captures all column values on updates and deletes. | ||
| - **`ALTER CONNECTION ... SET (HOST = ...)`** resumes from the existing slot | ||
| only when the target instance preserves that slot (an in-place promotion). | ||
| Otherwise Materialize re-snapshots. | ||
|
|
||
| ## Related pages | ||
|
|
||
| - [Ingest data from Amazon RDS](/ingest-data/postgres/amazon-rds/) | ||
| - [Guide: Ingest from a dedicated PostgreSQL replica](/ingest-data/postgres/logical-replica/) | ||
| - [Guide: Handle upstream schema changes with zero downtime](/ingest-data/postgres/source-versioning/) | ||
| - [`ALTER CONNECTION`](/sql/alter-connection/) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let alone freshness gap, won't this cascade drop all the downstream objects and cause an availability incident?