Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 21 — Rollback Strategy

Đã đọc 0/14 mục · còn khoảng 13 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng thế này: 8 giờ tối thứ Sáu, sàn thương mại điện tử của bạn vừa deploy một bản cập nhật giỏ hàng. Mười lăm phút sau, đội chăm sóc khách hàng bắt đầu nhận điện thoại dồn dập — khách bấm "Thanh toán" nhưng đơn hàng không được tạo. Doanh thu đang chảy máu theo từng phút. Trong khoảnh khắc đó, câu hỏi quan trọng nhất không phải là "Ai gây ra lỗi này?" hay "Chính xác dòng code nào sai?", mà là: "Làm sao đưa hệ thống về trạng thái hoạt động nhanh nhất có thể?"

Đó chính là bản chất của rollback. Trong toàn bộ hành trình CI/CD, chúng ta đã học cách xây dựng pipeline, cách deploy bằng nhiều chiến lược khác nhau. Nhưng deploy chỉ là một nửa câu chuyện. Nửa còn lại — nửa mà nhiều đội bỏ quên cho đến khi sự cố xảy ra — là khả năng quay lui một cách an toàn và có kiểm soát.

Một sự thật phũ phàng của nghề DevOps: mọi hệ thống đủ phức tạp đều sẽ có sự cố production. Không phải chuyện "nếu", mà là chuyện "khi nào". Đội ngũ giỏi không phải là đội không bao giờ gây ra lỗi, mà là đội có thể phục hồi trong vài phút thay vì vài giờ. Chỉ số MTTR (Mean Time To Recovery — thời gian trung bình để khôi phục) là một trong bốn chỉ số DORA quan trọng nhất đánh giá năng lực của một tổ chức kỹ thuật. Và rollback là vũ khí số một để kéo MTTR xuống thấp.

Trong bài này, tôi sẽ không dạy bạn cách viết code không lỗi (điều đó bất khả thi). Tôi sẽ dạy bạn cách thiết kế một chiến lược rollback để khi lỗi xảy ra — và nó sẽ xảy ra — bạn có thể bình tĩnh bấm một nút và ngủ ngon.

Khái niệm cốt lõi

Rollback là gì và khác gì với fix-forward

Rollback là hành động đưa hệ thống production trở về một phiên bản ổn định đã biết trước đó — thường là phiên bản ngay trước lần deploy gây lỗi. Bạn không sửa bug, bạn chỉ đơn giản "tua ngược" thời gian về lúc mọi thứ còn chạy tốt.

Fix-forward (hay "roll forward") là hướng ngược lại: bạn giữ nguyên phiên bản mới đang lỗi, tìm nguyên nhân, viết bản vá và deploy tiếp một phiên bản mới hơn để sửa. Bạn tiến về phía trước thay vì lùi lại.

Hai chiến lược này không phải kẻ thù của nhau. Chúng là hai công cụ cho hai tình huống khác nhau. Người kỹ sư giỏi biết chọn đúng công cụ.

Khi nào chọn Rollback, khi nào chọn Fix-forward

Đây là phần cốt lõi nhất của bài. Theo kinh nghiệm, hãy ưu tiên rollback khi:

  • Bug ảnh hưởng nhiều người dùng và gây thiệt hại tức thời: mất doanh thu, mất dữ liệu, hệ thống down toàn phần. Mỗi phút chậm trễ là tiền thật mất đi.
  • Nguyên nhân chưa rõ ràng: bạn thấy lỗi nhưng chưa hiểu vì sao. Debug dưới áp lực production là công thức cho những sai lầm còn tệ hơn.
  • Bản vá sẽ tốn nhiều thời gian: nếu ước lượng fix mất hơn 15–30 phút, rollback gần như luôn là lựa chọn đúng. Đưa hệ thống về ổn định trước, điều tra sau trong yên bình.
Ngược lại, hãy cân nhắc fix-forward khi:

  • Không thể rollback về mặt kỹ thuật: điển hình là khi bạn đã chạy một database migration không thể đảo ngược (ví dụ đã xóa một cột dữ liệu). Rollback code cũ nhưng schema mới sẽ khiến mọi thứ tệ hơn.
  • Lỗi nhỏ, ít ảnh hưởng, nguyên nhân đã rõ: ví dụ một nút bị sai màu, một typo trên trang. Deploy hotfix nhanh gọn hợp lý hơn việc rollback cả hệ thống.
  • Rollback sẽ mất dữ liệu người dùng đã tạo trên phiên bản mới: nếu người dùng đã bắt đầu tạo dữ liệu theo định dạng mới, quay lui có thể làm hỏng dữ liệu đó.
Có một quy tắc vàng tôi luôn nhắc học viên: "Rollback là hành động mặc định trong khủng hoảng." Khi nghi ngờ, hãy rollback. Bạn luôn có thể fix-forward sau khi hệ thống đã ổn định.

Các cấp độ rollback

Không phải mọi rollback đều giống nhau. Chúng ta cần phân biệt rõ:

1. Rollback ở tầng ứng dụng (application rollback): Đây là dạng phổ biến và dễ nhất. Bạn chỉ đơn giản deploy lại image/artifact của phiên bản trước. Với Kubernetes, đó là kubectl rollout undo. Với một hệ thống dùng image tag, đó là trỏ lại về tag cũ. Điều kiện tiên quyết: artifact phiên bản cũ phải còn được lưu trữ (chúng ta đã bàn về Artifact Management ở bài trước).

2. Rollback ở tầng cấu hình (config rollback): Đôi khi lỗi không đến từ code mà từ một thay đổi biến môi trường, feature flag, hay file cấu hình. Rollback loại này thường nhanh hơn cả — chỉ cần đảo ngược config, không cần deploy lại.

3. Rollback ở tầng dữ liệu (data/schema rollback): Đây là ác mộng khó nhất. Database migration thường không đối xứng — thêm cột thì dễ, xóa cột đã đầy dữ liệu thì gần như không thể quay lui an toàn. Đây là lý do nguyên tắc "backward-compatible migration" cực kỳ quan trọng, chúng ta sẽ đào sâu ở bài Database Migration.

Điều kiện tiên quyết để rollback được

Rollback không phải phép màu tự có. Để nút "rollback" thực sự hoạt động khi bạn cần, hệ thống phải thỏa mãn:

  • Artifact bất biến (immutable) và được đánh version rõ ràng: mỗi lần build tạo ra một image có tag cố định (ví dụ myapp:1.4.2), không bao giờ dùng lại tag latest cho production.
  • Lưu giữ các phiên bản trước: registry phải còn giữ ít nhất vài phiên bản gần nhất, đừng xóa quá sớm.
  • Rollback đã được kiểm thử: một chiến lược rollback chưa từng được diễn tập thì cũng vô dụng như không có. Chúng ta sẽ nói về diễn tập Disaster Recovery ở bài 49.
  • Tự động hóa hoặc ít nhất là có runbook rõ ràng: khi khủng hoảng, không ai muốn mò mẫm nhớ lại câu lệnh.

Tình huống thực tế

Tình huống 1: Sàn thương mại điện tử Việt Nam và deadlock lúc cao điểm

Một công ty thương mại điện tử tại TP.HCM (gọi là ShopViet) deploy một tối ưu hóa truy vấn giỏ hàng vào lúc 20h — ngay trước khung giờ vàng flash sale. Bản cập nhật vô tình tạo ra một database lock contention khiến 30% giao dịch thanh toán bị timeout.

Diễn giải: Đội on-call phát hiện lỗi qua cảnh báo tỷ lệ lỗi 5xx tăng vọt trong 4 phút. Kỹ sư trực ca ban đầu định "chỉ cần thêm một index là xong" — tư duy fix-forward. Nhưng trưởng nhóm can thiệp: nguyên nhân chưa chắc chắn 100%, thiệt hại đang là hàng chục triệu đồng mỗi phút, và thêm index trên bảng lớn lúc cao điểm còn nguy hiểm hơn. Họ quyết định rollback. Vì ứng dụng chạy trên Kubernetes với image được version rõ ràng, một lệnh kubectl rollout undo deployment/checkout đưa hệ thống về phiên bản cũ trong 90 giây. Tỷ lệ lỗi về bình thường ngay sau đó.

Bài học rút ra: Trong khủng hoảng, đừng để cái tôi kỹ thuật ("tôi biết sửa mà") đánh bại nguyên tắc. Rollback trước, điều tra sau. Chính vì rollback nhanh mà tổng thời gian ảnh hưởng chỉ khoảng 6 phút thay vì có thể là cả tiếng debug.

Tình huống 2: Knight Capital — bài học kinh điển 440 triệu USD

Đây là câu chuyện thật, kinh điển trong ngành. Năm 2012, công ty giao dịch chứng khoán Knight Capital tại Mỹ deploy một cập nhật lên 8 máy chủ. Do lỗi triển khai, một máy chủ vẫn chạy code cũ tái sử dụng một cờ (flag) mang ý nghĩa mới. Kết quả: hệ thống bắt đầu đặt hàng loạt lệnh giao dịch sai lầm với tốc độ chóng mặt.

Diễn giải: Điều gây thảm họa không chỉ là bug, mà là họ không có chiến lược rollback rõ ràng và phản ứng sai hướng. Trong cơn hoảng loạn, đội kỹ thuật đã rollback code mới trên tất cả máy chủ — nhưng điều đó vô tình kích hoạt code lỗi trên nhiều máy hơn, làm tình hình tệ đi. Trong 45 phút, công ty lỗ khoảng 440 triệu USD, gần như phá sản.

Bài học rút ra: Rollback phải được thiết kế và diễn tập, không phải hành động ngẫu hứng. Nếu rollback của bạn không nhất quán trên toàn bộ cụm máy, hoặc bạn không hiểu rõ tác động của nó, rollback có thể nguy hiểm hơn cả bug gốc. Đây cũng là lý do các chiến lược deploy an toàn (Blue-Green, Canary ở bài 18–19) kết hợp với rollback tự động lại quý giá đến vậy: chúng biến rollback thành thao tác chuyển traffic đơn giản, không rủi ro.

Tình huống 3: Startup fintech và cái bẫy database migration

Một startup ví điện tử ở Đông Nam Á deploy tính năng lịch sử giao dịch mới, kèm một migration đổi tên cột amount thành transaction_amount trong bảng chính. Ứng dụng mới chạy tốt vài giờ. Sau đó phát hiện một bug tính toán phí, đội quyết định rollback code.

Diễn giải: Nhưng rollback thất bại thảm hại. Code cũ vẫn tìm cột amount — cột đã bị đổi tên. Toàn bộ dịch vụ ví sập vì không đọc được dữ liệu. Giờ họ mắc kẹt: không rollback được vì schema đã đổi, cũng không chạy được vì có bug. Cuối cùng họ buộc phải fix-forward vội vàng dưới áp lực khủng khiếp.

Bài học rút ra: Rollback code và rollback schema là hai câu chuyện khác nhau. Nguyên tắc sống còn: luôn thiết kế migration theo kiểu tương thích ngược (expand-contract). Ở bước "expand", bạn thêm cột mới nhưng giữ cột cũ, để cả code cũ lẫn code mới đều chạy được. Chỉ sau khi phiên bản mới đã ổn định nhiều ngày, bạn mới "contract" — xóa cột cũ. Làm vậy thì rollback code luôn an toàn vì schema vẫn tương thích.

Hướng dẫn từng bước

Hãy cùng dựng một quy trình rollback thực tế. Tôi sẽ đi từ chuẩn bị đến thực thi.

Bước 1 — Đảm bảo artifact có thể rollback. Mỗi lần build trong pipeline phải tạo ra artifact bất biến với version rõ ràng. Đừng bao giờ deploy production bằng tag động như latest. Thay vào đó, gắn tag theo commit hoặc semantic version:

docker build -t registry.example.com/checkout:1.4.2 .
docker push registry.example.com/checkout:1.4.2

Bước 2 — Lưu lại "phiên bản ổn định hiện tại". Trước mỗi lần deploy, ghi lại version đang chạy để biết cần rollback về đâu. Nhiều đội lưu biến này trong pipeline hoặc trong một bảng deployment history.

Bước 3 — Có sẵn lệnh rollback một dòng. Với Kubernetes, tận dụng cơ chế revision có sẵn:

# Xem lịch sử các bản deploy
kubectl rollout history deployment/checkout

# Quay về bản ngay trước đó
kubectl rollout undo deployment/checkout

# Hoặc quay về một revision cụ thể
kubectl rollout undo deployment/checkout --to-revision=7

Bước 4 — Tích hợp rollback vào pipeline dưới dạng job thủ công. Trong GitHub Actions, bạn có thể tạo một workflow rollback.yml với workflow_dispatch nhận input là version đích:

name: Rollback Production
on:
  workflow_dispatch:
    inputs:
      version:
        description: "Version cần rollback về (vd: 1.4.1)"
        required: true
jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy phiên bản cũ
        run: |
          kubectl set image deployment/checkout \
            app=registry.example.com/checkout:${{ inputs.version }}
          kubectl rollout status deployment/checkout --timeout=120s

Lưu ý environment: production — điều này cho phép bạn bật approval gate, tránh rollback nhầm.

Bước 5 — Tự động hóa rollback dựa trên tín hiệu sức khỏe. Đây là cấp độ trưởng thành cao nhất. Sau khi deploy, pipeline chạy smoke test và theo dõi metrics trong vài phút. Nếu tỷ lệ lỗi vượt ngưỡng, hệ thống tự động rollback mà không cần con người. Các công cụ progressive delivery như Argo Rollouts hay Flagger làm việc này rất tốt:

# Ví dụ ngưỡng phân tích với Argo Rollouts
analysis:
  metrics:
    - name: error-rate
      successCondition: result < 0.01   # lỗi dưới 1%
      failureLimit: 2                     # vượt ngưỡng 2 lần thì tự rollback

Bước 6 — Xác nhận và ghi lại. Sau khi rollback, luôn kiểm tra lại bằng smoke test rằng hệ thống đã ổn, thông báo lên kênh chung (ChatOps), và mở một ticket điều tra nguyên nhân gốc để không lặp lại.

Lỗi thường gặp & mẹo

Lỗi 1: Không kiểm thử rollback bao giờ. Rất nhiều đội có "kế hoạch rollback" trên giấy nhưng chưa từng bấm thử. Đến lúc khẩn cấp mới phát hiện lệnh không chạy, artifact cũ đã bị xóa, hoặc quyền truy cập thiếu. Mẹo: đưa việc rollback vào diễn tập định kỳ (game day), rollback thật trên staging mỗi tháng.

Lỗi 2: Migration không tương thích ngược. Như tình huống fintech ở trên, đây là nguyên nhân số một khiến rollback bất khả thi. Mẹo: áp dụng nghiêm ngặt pattern expand-contract, tách deploy schema và deploy code thành các bước riêng, luôn đảm bảo code cũ chạy được với schema mới.

Lỗi 3: Rollback nhưng quên rollback config và feature flag. Bạn quay code về cũ nhưng một feature flag mới bật vẫn kích hoạt code path lỗi. Mẹo: coi feature flag như một cách rollback "mềm" — tắt flag thường nhanh và an toàn hơn cả rollback code. Chúng ta sẽ bàn kỹ ở bài Feature Flags.

Lỗi 4: Xóa artifact cũ quá sớm. Chính sách dọn dẹp registry quá hung hăng xóa mất chính phiên bản bạn cần quay về. Mẹo: giữ lại tối thiểu 5–10 phiên bản production gần nhất, hoặc mọi phiên bản trong 30 ngày.

Lỗi 5: Ra quyết định trong hoảng loạn, không có tiêu chí sẵn. Giữa cơn khủng hoảng, người ta tranh cãi "rollback hay fix" trong khi hệ thống đang cháy. Mẹo: viết sẵn một runbook rollback với tiêu chí quyết định rõ ràng (như phần "khi nào chọn rollback" ở trên), và chỉ định rõ ai có quyền bấm nút. Quyết định nhanh quan trọng hơn quyết định hoàn hảo.

Mẹo vàng cuối cùng: Kết hợp rollback với chiến lược deploy an toàn. Blue-Green cho phép rollback tức thì bằng cách chuyển traffic về môi trường cũ vẫn đang chạy. Canary cho phép phát hiện lỗi sớm khi mới chỉ 5% traffic bị ảnh hưởng. Rollback tốt nhất là rollback bạn hiếm khi phải dùng đến vì đã chặn lỗi từ sớm.

Bài tập thực hành

  • Xây workflow rollback thủ công: Tạo một GitHub Actions workflow rollback.yml sử dụng workflow_dispatch, nhận input version và deploy lại phiên bản đó. Thêm approval gate qua environment: production. Chạy thử trên một repo demo.
  • Diễn tập rollback trên Kubernetes: Deploy một ứng dụng đơn giản, tạo 3 revision khác nhau. Dùng kubectl rollout history và kubectl rollout undo để quay về từng revision. Đo xem mất bao lâu để hệ thống trở lại ổn định.
  • Phân tích tình huống — quyết định rollback hay fix-forward: Với mỗi kịch bản sau, hãy viết ra quyết định của bạn và lý do: (a) một typo trên trang giới thiệu; (b) API thanh toán trả lỗi 500 cho 40% request, nguyên nhân chưa rõ; (c) một migration vừa xóa cột old_status và ứng dụng mới đang tính toán sai.
  • Thiết kế migration tương thích ngược: Cho yêu cầu đổi tên cột phone thành phone_number. Hãy viết ra các bước expand-contract để đảm bảo rollback code luôn an toàn ở mọi giai đoạn.

Tóm tắt

Rollback là năng lực phân biệt một đội DevOps trưởng thành với một đội chỉ biết deploy. Những điểm cốt lõi cần khắc ghi:

  • Mục tiêu tối thượng là khôi phục service nhanh nhất, không phải tìm ra thủ phạm hay sửa bug ngay lập tức. Ưu tiên MTTR thấp.
  • Rollback là hành động mặc định trong khủng hoảng: khi bug ảnh hưởng nhiều người dùng, nguyên nhân chưa rõ, hoặc fix sẽ tốn thời gian — hãy rollback trước, điều tra sau.
  • Fix-forward hợp lý khi lỗi nhỏ, nguyên nhân đã rõ, hoặc khi rollback bất khả thi về mặt kỹ thuật (điển hình là migration không thể đảo ngược).
  • Điều kiện để rollback được: artifact bất biến có version, lưu giữ phiên bản cũ, migration tương thích ngược, và quan trọng nhất — rollback đã được diễn tập thật.
  • Cái bẫy lớn nhất là database schema: luôn dùng pattern expand-contract để code cũ và mới cùng chạy được.
  • Bài học từ Knight Capital nhắc ta: rollback không được thiết kế cẩn thận có thể nguy hiểm hơn cả bug gốc.
Hãy nhớ, một hệ thống không có nút rollback đáng tin cậy giống như lái xe không có phanh — bạn có thể đi rất nhanh, cho đến khi bạn không thể dừng lại. Trong bài tiếp theo, chúng ta sẽ mở rộng góc nhìn sang việc quản lý nhiều môi trường trong pipeline.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi