TungDaDev's Blog

vận hành database cluster & tính Idempotent

Database operations at scale.webp
Published on
/8 mins read/

Bất kỳ đoạn script tự động hóa nào không được thiết kế có tính Idempotent đều là một rủi ro tiềm ẩn chực chờ đánh sập cụm database của bạn trong môi trường production.

# bài toán thực tế: Sự cố đứt gãy giữa chu trình bảo trì

Trong quản trị hạ tầng phân tán quy mô lớn (hàng chục đến hàng trăm node database như ScyllaDB, Cassandra hay Elasticsearch), việc bảo trì, cập nhật kernel OS hay nâng cấp phiên bản phần mềm thường được thực hiện tự động qua script (Ansible, Python, Bash hoặc Go operator).

Kịch bản sự cố điển hình thường diễn ra như sau:

  1. Script chạy tuần tự rolling restart để cập nhật cụm 100 node.
  2. Đến node thứ 46, tiến trình script bị đứt kết nối SSH hoặc bị SIGKILL do timeout của runner CI/CD.
  3. Node thứ 46 rơi vào trạng thái dở dang: Dịch vụ database đã dừng (SIGTERM), gói phần mềm mới đang giải nén dở, cấu hình chưa được reload.

Lúc này, câu hỏi then chốt đối với kỹ sư SRE là: Nếu kích hoạt lại script từ đầu, hệ thống có tự phục hồi an toàn không? Hay nó sẽ chạy đè lên các node đang phục vụ lưu lượng và gây mất Quorum của cả cluster?


# nguyên lý Idempotent trong tự động hóa hạ tầng

Một thao tác được gọi là Idempotent khi thực thi nó một lần hay nhiều lần liên tiếp đều dẫn tới cùng một trạng thái mong muốn cuối cùng:

f(f(x)) = f(x)

Trong tự động hóa cơ sở dữ liệu, một tác vụ đạt chuẩn Idempotent phải tuân thủ nghiêm ngặt 3 nguyên tắc:

  1. Check-Before-Act (Thăm dò trước khi hành động): Không bao giờ giả định trạng thái của node dựa vào thứ tự thực thi trong quá khứ. Luôn truy vấn trực tiếp vào hệ thống (thông qua JMX, REST API, systemd D-Bus hoặc /proc) để xác định trạng thái thực tế.
  2. Atomic Transitions (Chuyển đổi trạng thái nguyên tử): Mọi thay đổi cấu hình hoặc cập nhật file thực thi phải được ghi vào file tạm rồi hoán đổi nguyên tử (mv temp target), tránh trường hợp file cấu hình bị rỗng khi script bị ngắt giữa chừng.
  3. No-Op khi đã thỏa mãn: Nếu một node đã chạy đúng phiên bản, đã ở đúng cấu hình và đang phục vụ lưu lượng bình thường, script phải lập tức bỏ qua (No-Op) mà không khởi động lại node đó.

# thiết kế Finite State Machine (FSM) cho từng Node

Thay vì viết script tuần tự kiểu mệnh lệnh (Imperative scripting), các hệ thống tự động hóa tin cậy chuyển sang mô hình Khai báo (Declarative Reconciliation) với Finite State Machine.

Mỗi node trong cluster được gán một trạng thái chính thức được lưu trữ trên một nguồn dữ liệu phân tán đáng tin cậy (như etcd hoặc Consul):

Trạng thái NodeÝ nghĩa hoạt độngĐiều kiện chuyển tiếp hợp lệ
HEALTHYNode đang phục vụ lưu lượng đọc ghiChỉ chuyển sang DRAINING khi cả cluster đủ Quorum
DRAININGĐang ngắt kết nối client từ từChờ connection pool giảm về 0
UPDATINGĐang thay đổi binary hoặc configThực thi trong phạm vi local
STARTINGDịch vụ database đang khởi động lạiChờ socket lắng nghe và SSTable load xong
CONVERGINGĐang đồng bộ gossip và token ringChờ nodetool status báo UN (Up/Normal)
VERIFIEDĐã kiểm tra tính toàn vẹn dữ liệuChuyển lại về HEALTHY

# toán học Quorum và bài toán tính toán Failure Domain

Trong các hệ thống phân tán sử dụng thuật toán đồng thuận hoặc mô hình Quorum (Cassandra, ScyllaDB, CockroachDB):

Quorum = ⌊R / 2⌋ + 1

Với Replication Factor R = 3, số node tối thiểu cần thiết để duy trì tính khả dụng (Availability) cho câu lệnh đọc ghi ở mức LOCAL_QUORUM là 2 nodes.

Số lượng node tối đa được phép bảo trì đồng thời (M) trong cùng một tập bản sao mà không làm gián đoạn hệ thống được tính bằng công thức:

M = R - Quorum = 3 - 2 = 1

Điều này đồng nghĩa: Bạn tuyệt đối không bao giờ được phép dừng đồng thời từ 2 node trở lên trong cùng một Replica Set.

Script tự động hóa phải có khả năng nhận biết cấu trúc vật lý (Topology Awareness):

  • Tự động gom nhóm các node theo Availability Zone (AZ) hoặc Rack.
  • Hoàn tất 100% việc bảo trì trên một Rack và đợi dữ liệu đồng bộ hoàn toàn trước khi chuyển sang Rack kế tiếp.

# mẫu hiện thực Idempotent Node Reconciler

Dưới đây là cấu trúc mã nguồn minh họa một bộ điều phối bảo trì có tính Idempotent:

import time
import requests
 
class NodeMaintenanceReconciler:
    def __init__(self, node_ip, target_version, cluster_state_client):
        self.node_ip = node_ip
        self.target_version = target_version
        self.state_client = cluster_state_client
 
    def reconcile(self):
        # 1. Check-Before-Act: Kiểm tra phiên bản thực tế hiện tại
        current_version = self.get_node_version()
        if current_version == self.target_version and self.is_node_healthy():
            print(f"Node {self.node_ip} đã ở đúng phiên bản {self.target_version}. NO-OP.")
            return True
 
        # 2. Kiểm tra tính toàn vẹn Quorum của toàn cluster trước khi dừng node
        if not self.state_client.can_safely_take_down(self.node_ip):
            raise RuntimeError(f"Không thể bảo trì {self.node_ip}: Quorum sẽ bị vi phạm!")
 
        # 3. Tiến hành Drain kết nối có timeout
        self.drain_traffic()
 
        # 4. Cập nhật phần mềm nguyên tử
        self.apply_update_atomically()
 
        # 5. Khởi động lại và chờ đợi Gossip protocol đồng thuận
        self.restart_service()
        self.wait_for_convergence(timeout_seconds=300)
 
        # 6. Xác thực tính nhất quán và hoàn tất
        if self.is_node_healthy():
            self.state_client.mark_completed(self.node_ip)
            return True
        else:
            raise RuntimeError(f"Node {self.node_ip} không đạt chuẩn sức khỏe sau cập nhật.")
 
    def get_node_version(self):
        # Gọi trực tiếp qua API endpoint local của node
        res = requests.get(f"http://{self.node_ip}:8080/api/v1/version", timeout=3)
        return res.json().get("version")

# Circuit Breaker: Tự động ngắt khi phát hiện sai hỏng chuỗi

Ngay cả khi script có tính Idempotent, lỗi cấu hình sai (Bad Configuration) vẫn có thể lan rộng ra toàn bộ cụm nếu tiếp tục chạy tự động.

Do đó, một hệ thống tự động hóa chuẩn production bắt buộc phải tích hợp Automation Circuit Breaker:

  • Ngưỡng thất bại liên tiếp (Failure Threshold): Nếu có 2 node liên tiếp gặp lỗi sau khi khởi động lại, kịch bản tự động hóa phải lập tức ngắt toàn bộ tiến trình và kích hoạt cảnh báo khẩn cấp tới đội ngũ trực vận hành.
  • Bảo lưu trạng thái (State Preservation): Không tự ý quay lui (rollback) một cách mù quáng trên các node đã cập nhật thành công nếu việc rollback đó có nguy cơ tiếp tục làm gián đoạn Quorum của cluster.

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.

← Previous postCritical Thinking