
你有一個 PostgreSQL 跑在 EC2 上,半夜三點 disk full 你被叫起來修。或者你的 Redis 掛了整個 App 跟著掛。託管資料庫能救你的睡眠——但買了睡眠之後,還有三件事會在上線那週把你叫醒。
結論:選型三十秒,上線三件事
選型:大多數 App 的起點是 RDS PostgreSQL + ElastiCache Redis。需要 JOIN 就 RDS;查詢模式固定、流量大就 DynamoDB;session、快取、限流、排行榜就 Redis。三十秒可以決定。
上線後真正的問題:
- Multi-AZ 不是零停機,切換那一到兩分鐘 App 會斷,而且可能斷更久
- 容器一擴,資料庫連線數跟著爆
- 裝了 RDS Proxy 想解第二題,結果 PostgreSQL 一用 prepared statement 就全部釘住,等於沒裝
第三件是這篇的重點。多數教學講到「裝 RDS Proxy」就停了。
一、託管買的是睡眠
自己在 EC2 上裝 PostgreSQL,你要管 OS patch、每日備份、主庫掛了切備庫、加讀取複本、版本升級。RDS 把這些全包了,你專心寫 SQL。省下來的時間拿去優化 query,比半夜修 disk full 有價值。
aws rds create-db-subnet-group --db-subnet-group-name prod-db-subnet \
--db-subnet-group-description "Production DB subnets" \
--subnet-ids subnet-private-1a subnet-private-1c
aws rds create-db-instance \
--db-instance-identifier prod-postgres \
--db-instance-class db.t3.medium \
--engine postgres --engine-version 16.4 \
--master-username dbadmin \
--manage-master-user-password \
--allocated-storage 100 --storage-type gp3 \
--vpc-security-group-ids sg-db \
--db-subnet-group-name prod-db-subnet \
--multi-az \
--backup-retention-period 14 \
--storage-encrypted \
--performance-insights-enabled兩個旗標一開始就要開、之後很難補:--storage-encrypted(建好之後不能對既有實例開加密)和 --multi-az。密碼用 --manage-master-user-password 讓 RDS 直接存進 Secrets Manager,不要寫在指令裡。
二、Multi-AZ 不是零停機
Multi-AZ 在另一個 AZ 維持一個同步的備庫。主庫掛了,RDS 自動切過去,你的 App 不用改連線字串——DNS 會指向新的主庫。
聽起來很美好,官方文件寫的是:切換通常 60–120 秒,大交易或冗長的復原會更久。這段時間 App 會看到連線錯誤。
所以 App 的 DB driver 一定要有重連和連線池重建的機制。沒有的話,切換完了 App 還抱著一堆已經死掉的連線。
還有一個坑,文件特別拉出來講:DNS 切了,但 JVM 會快取 DNS 查詢結果。某些 JVM 設定下預設是「永遠不刷新,直到重啟」——切換完了你的 Java App 還在連舊 IP,怎麼重試都連不上。官方建議把 networkaddress.cache.ttl(JVM 把 DNS 結果留幾秒)設在 60 秒以內。這不只是 Java 的問題,任何會快取 DNS 的層(自己包的連線池、某些 sidecar)都要檢查。
另一個常被誤會的:Multi-AZ 的備庫不能拿來讀。要分散讀流量是 Read Replica,那是另一個東西。
aws rds describe-db-instances --db-instance-identifier prod-postgres \
--query "DBInstances[0].[MultiAZ,SecondaryAvailabilityZone]"三、連線數爆炸,RDS Proxy 只解一半
這段是我最近整理架構卡、對著官方文件逐條看才搞懂的。
問題怎麼發生
PostgreSQL 每條連線是一個作業系統 process,貴。你的 App 跑在 ECS 上,每個 task 開一組連線池,假設 20 條。流量來了自動擴到 50 個 task——
50 個 task × 每 task 20 條 = 1,000 條連線打到資料庫
→ 超過 max_connections → 新連線全部被拒資料庫沒壞、CPU 也沒滿,就是連不上。而且這是擴容造成的,流量越大擴越多、爆得越徹底。
標準答案:RDS Proxy
RDS Proxy 是 AWS 託管的連線池,站在 App 和資料庫中間。App 開一千條連線到 Proxy,Proxy 只用幾十條連到資料庫,靠的是同一條資料庫連線在不同交易之間給不同 App 連線輪流用。順帶解掉切換時間(Proxy 幫你擋住,App 不用等 DNS)和帳密不落地(Proxy 直接接 Secrets Manager)。
它不是免費的。照定價頁的算法,費用跟著 Proxy 後面那個資料庫的大小走。後面是 provisioned 執行個體(你先選好規格、一直開著的資料庫),就照那台的 vCPU 數每小時計費。後面是 Aurora Serverless,就照 ACU 每小時計費;ACU 是 Aurora Serverless 衡量容量的單位,代表一份 CPU 加記憶體的組合。以秒計費,但建立、啟動、修改這類狀態變更之後,至少收 10 分鐘。這筆錢只要 Proxy 開著就在算,所以後面才說要量過有用再留著。
多數教學到這裡就停了。
沒講的那一半:pinning
輪流用的前提是連線上沒有殘留狀態。一旦 App 做了會改變 session 狀態的事,Proxy 就把那條資料庫連線釘死給那個 App 連線,直到它斷開為止。釘住的連線不能共用——釘住的比例一高,Proxy 等於沒裝,你還多付了錢和一跳延遲。
官方文件把會釘住的情況逐引擎列出來,PostgreSQL 那份特別長:
| 會釘住的操作 | PostgreSQL | MySQL / MariaDB |
|---|---|---|
SET 設定 session 變數 | ✅ 釘 | ✅ 釘(但可以用過濾器豁免) |
| Prepared statement | ✅ 釘(PREPARE / EXECUTE / DEALLOCATE) | ✅ 釘 |
| 暫存表 | ✅ 釘 | ✅ 釘 |
| 游標 | ✅ 釘 | — |
| Session 層級的 advisory lock | ✅ 釘(交易層級的不釘) | GET_LOCK 釘 |
連線池用 DISCARD ALL 當 reset query | ✅ 每次歸還連線都釘 | — |
| 語句文字超過 16 KB | ✅ 釘 | ✅ 釘 |
看到第二列了嗎。多數 ORM 和驅動預設就用 prepared statement。 你什麼都沒做,Proxy 就已經在釘了。
最陰的是倒數第二列:很多 PostgreSQL 連線池函式庫用 DISCARD ALL 當「歸還連線時的清理指令」——結果每一次歸還都觸發釘住。裝了 Proxy、指標一看全是釘住的,等於白裝。
而且MySQL 有「session pinning filter」可以把 SET 類操作豁免掉,PostgreSQL 沒有。官方原文直說:對 PostgreSQL,設定變數就會導致釘住。
所以怎麼辦
先量,再決定。 CloudWatch 有一個指標叫 DatabaseConnectionsCurrentlySessionPinned,裝完 Proxy 第一件事是看它:
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS --metric-name DatabaseConnectionsCurrentlySessionPinned \
--dimensions Name=ProxyName,Value=prod-postgres-proxy \
--start-time "$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 300 --statistics Average接近 0 才算有在共用。如果一半以上被釘住,三條路:
- 把每條連線都要跑的
SET搬進 Proxy 的初始化查詢設定,從 App 拿掉——這是官方建議的做法 - 驅動層關掉 prepared statement(多數驅動有開關,代價是每次查詢都重新 parse)
- 放棄 RDS Proxy,自己跑 PgBouncer 用 transaction mode(每個交易一結束,就把資料庫連線還回池子給別人用)——它對這些狀況的處理更成熟,代價是要自己維護
我的看法:PostgreSQL + 重度 ORM 的專案,先假設 RDS Proxy 幫不上忙,量了指標證明它有用再留。反過來假設會讓你在尖峰那天才發現連線還是爆。
四、ElastiCache:不需要 proxy,需要的是四件事
講完 RDS Proxy 你可能會問 Redis 要不要也放一個。不用。 一般自己選節點規格建起來的 ElastiCache(官方叫 node-based cluster,相對於 Serverless),AWS 沒有一個像 RDS Proxy 那樣能單獨掛在前面的產品。(未查證:官方文件裡沒找到對應產品,但沒有做系統性查證。)有 proxy 的是 ElastiCache Serverless,而且 proxy 是內建的,下面講。
原因是連線模型不同:PostgreSQL 每條連線一個 process,Redis 是單執行緒事件迴圈,連線只是一個 file descriptor,一萬條同時連著也還好。RDS Proxy 要解的「連線太貴」在 Redis 這邊不成立。
真的會用到 Redis proxy 的場景,多半跟 cluster 模式有關。cluster 模式是把資料按 key 分散到好幾組節點上:每個 key 算出來會落在一個固定的 slot(槽位),每個 slot 由某一組節點負責。客戶端要自己算出 key 在哪個 slot、該連哪台,這叫 slot 路由。
會需要 proxy 的是這三種:
- 用 cluster 模式,但客戶端函式庫不懂 slot 路由(老舊語言或自製 client)
- 多租戶要在 proxy 層做 key 前綴隔離
- 連線數真的破萬,要讓很多條客戶端連線共用少數幾條到 Redis 的連線
要自己做就是自架 Envoy 的 Redis 代理,或 twemproxy(已經很舊)。場景 1 也可以換支援 cluster 的客戶端函式庫解掉——ioredis、go-redis、Lettuce 都內建 slot 路由和重導向處理(連錯節點時,Redis 會回覆「去另一台找」,函式庫會自動改連)。
另一條路是 ElastiCache Serverless。官方文件寫,它給你單一個 DNS 端點,所有連線先經過一層 proxy(一組 proxy 節點,前面是 network load balancer)。底層擴縮、換節點、升版時,客戶端都不用重連,也不用自己重新探索拓撲(有哪些節點、哪個 slot 在哪台)。
但它一律跑 cluster 模式,只接受懂 cluster 協定的客戶端。多 key 操作、交易、Lua 腳本裡用到的 key,都要落在同一個 hash slot;不然會回 CROSSLOT 錯誤,意思是「這幾個 key 分散在不同 slot,不能一起處理」。所以它不能當單機 Redis 用。
繞了一圈,結論還是開頭那句:除了上面三種情況,Redis 前面不用另外放 proxy。
aws elasticache create-replication-group \
--replication-group-id prod-redis \
--replication-group-description "Production Redis" \
--engine redis --engine-version 7.1 \
--cache-node-type cache.t3.medium \
--num-cache-clusters 2 --automatic-failover-enabled \
--cache-subnet-group-name prod-redis-subnet \
--security-group-ids sg-redis \
--at-rest-encryption-enabled --transit-encryption-enabledElastiCache 真正該顧的是這四件,每一件都是程式碼問題不是設定問題:
- 故障切換時客戶端會不會自己重連——跟 RDS 那段同一個坑,DNS 換了 App 要跟上。還有一條:
BLPOP這類 blocking read(佇列沒資料時,指令會停著等到有資料或逾時)要用獨立連線,不要跟一般指令共用,否則一條卡住全部跟著卡 - 快取同時過期造成的驚群——一批 key 同秒過期,全部打穿到資料庫。TTL 加隨機抖動(jitter,每個 key 的過期時間各多加一點隨機秒數),把過期時間打散。同一個 key 要重建時用 single-flight(同時進來的請求只放一個去做,其他人等它的結果),只讓一個請求去回源(回資料庫撈資料)
- 記憶體滿了怎麼逐出——
maxmemory-policy要配用途:當快取用可以隨便丟,當佇列用不能丟 - 多台 App 共用 session——這是 Redis 在這種架構裡最基本的用途,也是最容易漏的:session 寫在容器本地檔,換一個 task 就登出
這四件在 Database Landscape 有更完整的展開。
快取的讀寫順序只記結論:
- 讀用 cache-aside:先讀 cache;miss(cache 裡沒有)就讀 DB,再把結果寫回 cache,這步叫回填。回填時一定要給 TTL(過期時間)
- 寫是先更新 DB,再刪掉 cache,不是更新 cache
寫入為什麼是刪、不是更新?因為兩個寫入可能同時發生。A 把 DB 改成 1,B 接著改成 2,DB 最後是 2,沒問題。但如果接著各自去更新 cache,B 可能先寫入 cache=2,A 慢一步才寫入 cache=1,cache 就留著舊值 1。改成刪掉 cache,兩個人都只是刪,下一次讀會從 DB 拿到 2 再回填。
五、DynamoDB 與 Aurora:一段講完
DynamoDB 適合查詢模式固定、流量高、要自動擴的場景。不適合需要 JOIN、查詢模式還沒定、要事後 ad-hoc 分析的。新專案先用 On-Demand 計費,流量穩了再考慮 Provisioned。
Aurora 是 RDS 的自研儲存版,相容 PostgreSQL 和 MySQL。什麼時候選?資料量或讀取複本數大到標準 RDS 撐不住的時候。先別為了「比較快」選它——快多少沒有可靠的公開數字,所以這篇不寫倍數。
選型樹:
需要 JOIN / 複雜查詢?
├─ 是 → RDS PostgreSQL;撐不住再看 Aurora
└─ 否 → 查詢模式固定?
├─ 是 → DynamoDB
└─ 否 → 還是 RDS,彈性最大
另外需要快取 / session / 限流 → ElastiCache Redis
全文搜尋 → OpenSearch實戰 Checklist
- RDS 建立時就開
--storage-encrypted和--multi-az,之後補不了 - App 的 DB driver 有重連機制;Java 的 DNS TTL 設 60 秒以內
- 算過「task 數 × 連線池大小」會不會超過
max_connections - 裝了 RDS Proxy 就看
DatabaseConnectionsCurrentlySessionPinned,接近 0 才算有效 - Redis 的 session 外置、TTL 打散、逐出策略配用途
從 K8s 過來的話
K8s 上跑 StatefulSet PostgreSQL 可以,但備份、切換、升版全要自己來——在 AWS 上用 RDS 然後讓 Pod 連 RDS endpoint 是最省心的。連線數爆炸那題在 K8s 一模一樣:Pod 擴 50 個一樣打爆資料庫,差別只是你會用 PgBouncer 而不是 RDS Proxy——而 PgBouncer 對 PostgreSQL 反而比 RDS Proxy 成熟,這是少數「自架比託管好」的例子。
相關文章
- 上一篇 S3,下一篇 Container Insights
- 連線數、逐出策略這些指標怎麼看:第 7 篇
- 自架 PostgreSQL 的那些事:PostgreSQL、PostgreSQL Advanced
資料庫是你的 App 最值錢的部分——省什麼都不要省 DB 的穩定性。
