Rolling deploy, tương thích ngược, migration DBRolling deploys, backward compatibility, DB migrations
Ingress chia traffic giữa pod stable và pod mới theo trọng số do Argo Rollouts điều khiển. Argo Rollouts hỏi Prometheus về tỉ lệ lỗi sau mỗi bước. Migration chạy trước rollout và chỉ mở rộng schema. Session nằm ở Redis nên pod nào cũng phục vụ được. Chọn “Canary lỗi” hoặc “Đổi tên cột qua 3 release”.The ingress splits traffic between stable and new pods by weights that Argo Rollouts controls. After each step Argo Rollouts asks Prometheus for the error rate. Migrations run before the rollout and only expand the schema. Sessions live in Redis, so any pod can serve any user. Pick “Canary fails” or “Column rename in 3 releases”.
1Migration chạy trước như một PreSync hook và chỉ thêm, không xoá hay đổi tên, nên bản v1 đang chạy không bị ảnh hưởng.The migration runs first as a PreSync hook and only adds things, never drops or renames, so the running v1 is unaffected.
Kubernetes gửi SIGTERM và rút endpoint song song, nên vài giây sau SIGTERM vẫn có request tới. Dùng preStop sleep 5–10s, rồi graceful shutdown (server.shutdown=graceful, http.Server.Shutdown) với thời hạn nhỏ hơn terminationGracePeriodSeconds.Kubernetes sends SIGTERM and removes the endpoint in parallel, so requests keep arriving for a few seconds after SIGTERM. Use a preStop sleep of 5–10s, then graceful shutdown (server.shutdown=graceful, http.Server.Shutdown) with a deadline below terminationGracePeriodSeconds.
Readiness quyết định có nhận traffic không và được phép kiểm tra DB, warm-up. Liveness chỉ kiểm tra process còn chạy; nếu liveness kiểm tra DB thì một sự cố DB làm restart cả 40 pod cùng lúc. JVM khởi động chậm thì thêm startupProbe.Readiness decides whether a pod takes traffic and may check the DB and warm-up. Liveness only checks that the process runs; if liveness checked the DB, one DB incident would restart all 40 pods at once. Add a startupProbe for slow JVM starts.
Trong lúc rolling, v1 và v2 cùng chạy trên một database. Migration phải tương thích ngược: chỉ thêm trong expand, đổi tên hoặc xoá qua nhiều release, chạy migration trước code và không bao giờ cùng lúc xoá cột với deploy code ngừng dùng nó.During a rollout v1 and v2 run against the same database. Migrations must be backward compatible: only add during expand, rename or drop over several releases, run migrations before code, and never drop a column in the same deploy that stops using it.
App mobile cũ còn chạy hàng tháng. Chỉ thêm field tuỳ chọn, không đổi nghĩa hay xoá field; client bỏ qua field lạ. Thay đổi lớn thì version API (/v2) và giữ v1 tới khi lượng dùng gần bằng 0.Old mobile apps run for months. Only add optional fields, never change meaning or remove fields; clients ignore unknown fields. For bigger changes version the API (/v2) and keep v1 until its usage is close to zero.
Rolling rẻ và mặc định trong Kubernetes; blue-green chuyển 100% một lần và quay lui tức thì nhưng tốn gấp đôi tài nguyên; canary đưa bản mới cho 1–5% trước và rollback tự động theo SLO. Nhiều team dùng canary cho service quan trọng, rolling cho phần còn lại.Rolling is cheap and the Kubernetes default; blue-green switches 100% at once and rolls back instantly but doubles capacity; canary gives the new version 1–5% first and rolls back automatically on the SLO. Many teams use canary for critical services and rolling for the rest.
Feature flag cho phép deploy code mới ở trạng thái tắt, rồi bật dần theo phần trăm hoặc nhóm người dùng. Lỗi logic thì tắt flag trong vài giây thay vì rollback image; nhớ dọn flag sau khi ổn định.Feature flags let you deploy new code switched off and turn it on gradually by percentage or user segment. A logic bug means flipping the flag in seconds instead of rolling back the image; remember to clean the flag up once stable.
| BảngTable | Cột chínhKey columns | Ghi chúNotes |
|---|---|---|
Rollout (Argo CRD) | strategy.canary.steps, trafficRouting, analysis.templates, abortScaleDownDelaySeconds | Các bước setWeight / pause và AnalysisTemplate quyết định tiến hay rollbacksetWeight / pause steps and the AnalysisTemplate decide whether to advance or roll back |
Deployment probes | startupProbe, readinessProbe, livenessProbe, preStop, terminationGracePeriodSeconds | Ví dụ: readiness 5s/3 lần, liveness 10s/6 lần, preStop sleep 10, grace 45sFor example: readiness 5s × 3, liveness 10s × 6, preStop sleep 10, grace 45s |
flyway_schema_history | installed_rank, version, description, checksum, success | Migration expand chạy trước rollout; contract là migration riêng ở release sauExpand migrations run before the rollout; contract is its own migration in a later release |
feature_flag | key, enabled, strategy, rollout_percent, segments | checkout.new_voucher_flow: tắt khi deploy, bật 1% → 100% saucheckout.new_voucher_flow: off at deploy, then 1% → 100% |
spring:session:sessions:<id> (Redis) | creationTime, lastAccessedTime, maxInactiveInterval, sessionAttr:* | Session ngoài pod nên không cần sticky session; payload phải đọc được bởi cả hai phiên bảnSessions live outside the pod, so no sticky sessions are needed; the payload must be readable by both versions |
Pod chỉ báo sẵn sàng khi đã warm-up, tắt thì báo không sẵn sàng trước, đợi load balancer rút rồi mới xử lý nốt request. Rollout canary dựa vào truy vấn Prometheus để tự rollback.A pod reports ready only after warming up; on shutdown it reports not ready first, waits for the load balancer to drop it, then finishes in-flight requests. The canary rollout relies on a Prometheus query to roll back on its own.
@Component("warmup")
@RequiredArgsConstructor
public class WarmupHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
private final ProductCache cache;
private final AtomicBoolean warm = new AtomicBoolean();
@EventListener(ApplicationReadyEvent.class)
public void warmUp() throws SQLException {
try (Connection c = dataSource.getConnection()) {
c.isValid(2); // open the Hikari pool
}
cache.preload(5_000); // hottest products into memory
warm.set(true);
}
@Override
public Health health() {
return warm.get()
? Health.up().build()
: Health.outOfService().withDetail("warmup", "in progress").build();
}
}
/* application.yml
server:
shutdown: graceful # stop accepting, finish in-flight requests
spring.lifecycle:
timeout-per-shutdown-phase: 30s # then close whatever is left
management.endpoint.health:
probes.enabled: true # /actuator/health/liveness and /readiness
group:
readiness.include: readinessState, db, warmup
liveness.include: livenessState # never the DB
# Deployment: preStop sleep 10, terminationGracePeriodSeconds: 45
*/func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()
store := mustOpenDB()
var ready atomic.Bool
mux := http.NewServeMux()
mux.HandleFunc("GET /livez", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK) // the process is alive, nothing more
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, _ *http.Request) {
if !ready.Load() {
http.Error(w, "not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
mux.Handle("/", api.Routes(store))
srv := &http.Server{Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
if err := warmUp(ctx, store); err != nil { // pool, caches
log.Fatal(err)
}
ready.Store(true)
<-ctx.Done() // SIGTERM from the kubelet
ready.Store(false) // readiness fails, endpoints drop the pod
time.Sleep(10 * time.Second) // or a preStop hook: let load balancers catch up
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // waits for in-flight requests
log.Printf("forced shutdown: %v", err)
}
store.Close()
}| Blue-greenBlue-green | CanaryCanary | |
|---|---|---|
| Tài nguyênCapacity | Gấp đôi trong lúc chuyểnDoubled during the switch | Thêm vài pod cho bản mớiA few extra pods for the new version |
| Phạm vi ảnh hưởngBlast radius | 100% người dùng ngay khi chuyển100% of users the moment you switch | 1–5% trước, tăng dần theo kết quả1–5% first, more as results allow |
| RollbackRollback | Chuyển lại môi trường cũ, tức thìSwitch back to the old environment, instantly | Trọng số về 0, tự động theo SLOWeight to 0, automatically on the SLO |
| Chọn khiPick it when | Cần kiểm thử toàn bộ môi trường mới trước khi đổi, traffic ítYou must test the whole new environment before switching, or traffic is low | Traffic đủ lớn để đo lỗi có ý nghĩa ở vài phần trămTraffic is high enough to measure errors meaningfully at a few percent |
Mỗi pattern mở bài học tương ứng trong menu Pattern.Each one opens its lesson in the Patterns menu.
Kubernetes gửi SIGTERM và cập nhật endpoint song song; kube-proxy, ingress và load balancer cloud cần vài giây để biết pod đã rời. Nếu pod đóng listener ngay, request tới trong khoảng đó bị từ chối. Thêm preStop sleep 5–10s trước khi tắt, và đặt keep-alive timeout của app dài hơn của load balancer.Kubernetes sends SIGTERM and updates endpoints in parallel; kube-proxy, the ingress and the cloud load balancer need a few seconds to learn the pod is gone. If the pod closes its listener at once, requests arriving in that gap are refused. Add a 5–10s preStop sleep before shutting down, and make the app's keep-alive timeout longer than the load balancer's.
Expand: thêm cột mới, code ghi cả hai, backfill. Release sau đọc cột mới, vẫn ghi cả hai để còn rollback được. Release sau nữa ngừng ghi cột cũ. Cuối cùng contract: xoá cột cũ. Ở mỗi thời điểm, hai phiên bản liền kề đều chạy đúng trên schema hiện tại.Expand: add the new column, write both, backfill. The next release reads the new column and still writes both so rollback works. The one after stops writing the old column. Finally contract: drop the old column. At every point, two adjacent versions both work on the current schema.
Tín hiệu so sánh được giữa canary và stable: tỉ lệ 5xx, p99 latency, có khi là chỉ số nghiệp vụ như tỉ lệ thanh toán thành công. Cần đủ traffic để có ý nghĩa thống kê, cửa sổ đo vài phút và cho phép một lần nhiễu. Chỉ dùng CPU hay số pod restart thì phát hiện muộn.Signals you can compare between canary and stable: 5xx rate, p99 latency, sometimes a business metric such as checkout success rate. You need enough traffic for statistical meaning, a window of a few minutes and tolerance for one noisy reading. CPU or pod restarts alone detect problems too late.
Expand chạy trước, vì code cũ phải chạy được trên schema mới và code mới cần schema mới ngay khi khởi động. Contract chạy sau, ở release riêng, khi không còn phiên bản nào dùng phần bị xoá. Không để mỗi pod tự chạy migration lúc khởi động, vì 40 pod sẽ tranh nhau khoá.Expand runs before, because old code must work on the new schema and new code needs it from the first start. Contract runs after, in its own release, once no version uses what is being removed. Don't let every pod run migrations at startup, or 40 pods will fight over the lock.
Phím 1–4 để chọn đáp án, Enter để kiểm tra.Press 1–4 to choose an answer, Enter to check.
Ôn tập ngắt quãng (spaced repetition): thẻ được lặp lại sau 1 → 3 → 7 → 14 → 30 ngày để củng cố trí nhớ dài hạn.Spaced repetition: cards are repeated after 1 → 3 → 7 → 14 → 30 days to build long-term memory.