← Chương trước: Phần 4: MariaDB vs. MySQL | Mục lục Series | Chương tiếp theo: Phần 6: Apache Kafka vs. NATS JetStream →

Sharded MySQL (Vitess) vs. TiDB NewSQL: ACID Phân Tán & Thuế Độ Trễ Mạng


Answer-first: Sharded MySQL (Vitess) áp đảo về độ trễ ghi (sub-2ms) và cô lập lỗi (Blast Radius) cho hệ thống E-commerce / SaaS có Sharding Key sạch. Ngược lại, TiDB NewSQL là giải pháp cứu cánh cho schema phức tạp nhờ cơ chế tự động chia tách Region 96MB, chấp nhận mức sàn độ trễ ghi 8–15ms do Percolator 2PC.

Điều kiện tiên quyết: Nắm vững mô hình phân mảnh cơ sở dữ liệu (sharding), giao thức 2-Phase Commit (2PC), thuật toán đồng thuận Paxos/Raft và cơ chế nén LSM-tree.


Phiên bản đa ngôn ngữ: Bài viết này có phiên bản tiếng Anh chuẩn quốc tế tại Part 5: Sharded MySQL (Vitess) vs. TiDB NewSQL Showdown (tanhdev.com).

📖 Bản tiếng Anh (English Edition)

Để xây dựng kiến trúc backend đồng bộ và tối ưu hóa hệ thống microservices chịu tải cao, bạn có thể tham khảo thêm các bài phân tích chuyên sâu tại Hướng Dẫn Kiến Trúc Go Microservices.


1. Bối Cảnh: Ngưỡng Vỡ 10TB & Cuộc Chạm Trán Giữa 2 Trường Phái

Khi một hệ thống cơ sở dữ liệu quan hệ (RDBMS) chạm ngưỡng 10TB dữ liệu và 50,000+ Write QPS, các giải pháp truyền thống như nâng cấp cấu hình máy chủ (Vertical Scaling) hoặc bổ sung Read Replicas chính thức rơi vào bế tắc:

  • Giới hạn phần cứng: Máy chủ lớn nhất trên AWS/GCP (ví dụ r6i.32xlarge với 1TB RAM) có chi phí đắt đỏ theo hàm mũ ($> $8,000/\text{tháng}$) nhưng vẫn bị nghẽn I/O bus và lock contention trên một Primary duy nhất.
  • Ác mộng Replication Lag: Khi master gánh 30,000 write ops/sec, luồng single-threaded SQL applier trên replica không thể bắt kịp, gây trễ dữ liệu từ vài chục giây đến hàng chục phút, phá vỡ tính nhất quán read-your-own-writes.
  • Rủi ro Dừng Hệ Thống (Maintenance Window): Câu lệnh ALTER TABLE ADD INDEX trên bảng 5 tỷ dòng khiến bảng bị khóa nhiều giờ, rủi ro làm sập toàn bộ dịch vụ.

Tại thời điểm này, các kỹ sư trưởng và kiến trúc sư hệ thống bắt buộc phải lựa chọn giữa hai trường phái mở rộng ngang (Horizontal Scale-Out):

flowchart TD
    subgraph ClientLayer ["Microservices Client Layer (Golang / Dapr / gRPC)"]
        Client["Application Workload (100k writes/sec)"]
    end

    subgraph ShardedTrack ["Trường Phái 1: Sharded MySQL (Vitess Architecture)"]
        direction TB
        VTGate["VTGate Stateless L7 Proxy (VSchema Router)"]
        Shard1["Shard 1 (-80): Primary MySQL InnoDB (Sub-2ms Local ACID)"]
        Shard2["Shard 2 (80-): Primary MySQL InnoDB (Sub-2ms Local ACID)"]
        VRep["VReplication Engine (Online Zero-Downtime Split/Merge)"]
    end

    subgraph NewSQLTrack ["Trường Phái 2: TiDB NewSQL Architecture"]
        direction TB
        TiDB_SQL["TiDB Stateless SQL Nodes (Parser/Cost Optimizer)"]
        PD["Placement Driver (PD Raft Cluster: TSO Allocator & Region Scheduler)"]
        TiKV1["TiKV Node 1 (Raft Group: 96MB Regions, RocksDB LSM)"]
        TiKV2["TiKV Node 2 (Raft Group: 96MB Regions, RocksDB LSM)"]
        TiFlash["TiFlash Columnar Store (Real-time Raft Learner OLAP)"]
    end

    Client -->|"Query with Shard Key (tenant_123)"| VTGate
    VTGate -->|"Direct Routing (Single RTT)"| Shard1
    
    Client -->|"Standard MySQL Protocol"| TiDB_SQL
    TiDB_SQL <-->|"Fetch Monotonic Timestamp (TSO)"| PD
    TiDB_SQL <-->|"Percolator 2PC (Prewrite + Commit + Raft Heartbeats)"| TiKV1
    TiDB_SQL <-->|"Percolator 2PC"| TiKV2
    TiKV1 -.->|"Raft Learner Replication"| TiFlash
  1. Trường phái 1: Sharded MySQL với Proxy Vitess: Giữ nguyên các node MySQL tiêu chuẩn ở tầng dưới, đưa logic định tuyến và phân mảnh lên tầng proxy L7 (VTGate) kết hợp sơ đồ VSchema khai báo.
  2. Trường phái 2: Distributed NewSQL (TiDB): Đập bỏ hoàn toàn kiến trúc lưu trữ MySQL cũ, xây dựng hệ thống cơ sở dữ liệu phân tán gốc (Distributed-Native) với tầng SQL phi trạng thái (TiDB), cụm điều phối Placement Driver (PD), và động cơ lưu trữ đồng thuận Raft (TiKV / RocksDB).

2. So Sánh Kiến Trúc Giao Dịch: Local ACID vs. Google Percolator 2PC

Sự đánh đổi lớn nhất quyết định độ trễ của hệ thống nằm ở Giao thức Thực thi Giao dịch (Transaction Protocol Overhead).

[Vitess Single-Shard Write Flow: ~1.2ms P99]
App ──> VTGate ──> Shard 1 MySQL InnoDB (Local Redo Log Write + Flush) ──> App

[TiDB Percolator Write Flow: ~10.5ms P99]
App ──> TiDB Node ──(1) Get StartTS (Network RTT)──> PD Cluster
                  ──(2) Prewrite Lock (Network RTT)──> TiKV Raft Leader ──(Raft Log to Quorum)──> Follower
                  ──(3) Get CommitTS (Network RTT)──> PD Cluster
                  ──(4) Commit Primary Lock (Network RTT)──> TiKV Raft Leader ──> App

2.1. Vitess: Giao Dịch ACID Cục Bộ Trên Single-Shard

Khi các câu truy vấn mang theo khóa phân mảnh (Shard Key, ví dụ: tenant_id = 'tenant_99'):

  • VTGate phân tích bảng băm VSchema và định tuyến trực tiếp kết nối TCP tới Shard đích tương ứng.
  • Shard thực hiện Giao dịch Local ACID trên InnoDB, ghi vào Redo Log Buffer cục bộ trong duy nhất 1 chặng mạng (Single RTT).
  • Mức sàn độ trễ: Độ trễ ghi P99 duy trì ổn định ở mức 0.8 ms – 2.0 ms, ngang ngửa hiệu năng MySQL bare-metal nguyên bản.

2.2. TiDB: Thuế Độ Trễ Giao Dịch Phân Tán (Percolator 2PC)

TiDB điều phối giao dịch thông qua Giao thức Cam Kết 2 Pha Google Percolator trên nền Multi-Raft:

  1. Lấy Start Timestamp (TSO): Node TiDB SQL gửi request qua mạng tới cụm Placement Driver (PD) để lấy dấu thời gian bắt đầu logic tăng đơn điệu StartTS.
  2. Giai đoạn Prewrite: TiDB chỉ định một Primary Lock và gửi yêu cầu prewrite tới các TiKV Raft leaders liên quan. Mỗi leader ghi lock vào Raft log cục bộ và nhân bản đồng thuận qua đa số quorum followers.
  3. Lấy Commit Timestamp: TiDB tiếp tục gửi request mạng thứ hai tới PD để lấy CommitTS.
  4. Giai đoạn Commit: TiDB gửi lệnh commit khóa chính tới Raft leader của Primary Lock. Khi khóa chính commit thành công, giao dịch được xem là cam kết logic hoàn tất. Các khóa phụ được dọn dẹp bất đồng bộ sau đó.
  • Mức sàn độ trễ vật lý: Vì mỗi thao tác ghi, dù chỉ cập nhật 1 dòng, đòi hỏi tối thiểu 4 đến 6 chặng mạng (Network RTTs) giữa các tầng máy chủ độc lập, TiDB có mức sàn độ trễ ghi vật lý tối thiểu từ 6 ms – 15 ms.

2.3. Cấu Hình Phân Mảnh Khai Báo Vitess VSchema Chuẩn Production

Trong kiến trúc Vitess, toàn bộ chiến lược phân mảnh được quản lý tập trung thông qua tệp khai báo vschema.json. Cấu hình production dưới đây minh họa việc sử dụng hàm băm chính (hash vindex theo customer_id), chỉ mục tra cứu phụ (lookup_hash_unique theo email) và bảng sinh mã ID tăng dần (sequence):

{
  "sharded": true,
  "vindexes": {
    "hash_index": {
      "type": "hash"
    },
    "lookup_customer_email": {
      "type": "lookup_hash_unique",
      "params": {
        "table": "customer_email_lookup",
        "from": "email",
        "to": "customer_id",
        "autocommit": "true"
      },
      "owner": "customers"
    }
  },
  "tables": {
    "customers": {
      "column_vindexes": [
        {
          "column": "customer_id",
          "name": "hash_index"
        },
        {
          "column": "email",
          "name": "lookup_customer_email"
        }
      ],
      "auto_increment": {
        "column": "customer_id",
        "sequence": "customer_seq"
      }
    },
    "orders": {
      "column_vindexes": [
        {
          "column": "customer_id",
          "name": "hash_index"
        }
      ]
    }
  }
}

2.4. Chiến Lược Phân Tầng Lưu Trữ TiDB Placement Policy & DDL

Khác với việc quản trị shard thủ công, TiDB NewSQL cho phép kỹ sư thiết lập Quy Tắc Đặt Vị Trí (Placement Rules) bằng câu lệnh SQL chuẩn để ghim các phân vùng nóng lên ổ SSD NVMe hiệu năng cao và đẩy dữ liệu lịch sử về ổ HDD/S3 giá rẻ:

-- Thiết lập chính sách lưu trữ NVMe IOPS cao cho dữ liệu nóng
CREATE PLACEMENT POLICY nvme_tier 
    PRIMARY_REGION="us-east-1" 
    REGIONS="us-east-1" 
    CONSTRAINTS="[+disk=nvme]";

-- Thiết lập chính sách lưu trữ giá rẻ cho phân vùng lịch sử
CREATE PLACEMENT POLICY cold_archive_tier 
    PRIMARY_REGION="us-east-1" 
    CONSTRAINTS="[+disk=hdd]";

-- Phân vùng bảng đơn hàng: dữ liệu năm hiện tại ghim trên NVMe, dữ liệu cũ đưa vào HDD
CREATE TABLE enterprise_orders (
    order_id BIGINT PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    order_date DATE NOT NULL,
    total_amount DECIMAL(12, 2) NOT NULL,
    status VARCHAR(32) NOT NULL
) PARTITION BY RANGE COLUMNS(order_date) (
    PARTITION p2025 VALUES LESS THAN ('2026-01-01') PLACEMENT POLICY = cold_archive_tier,
    PARTITION p2026 VALUES LESS THAN ('2027-01-01') PLACEMENT POLICY = nvme_tier,
    PARTITION p_future VALUES LESS THAN (MAXVALUE) PLACEMENT POLICY = nvme_tier
);

3. Cơ Chế Resharding: Vùng Dữ Liệu Động 96MB (TiDB) vs. VReplication (Vitess)

Độ phức tạp trong công tác vận hành khi mở rộng dung lượng là thước đo phân biệt rõ ràng nhất giữa hai công nghệ:

flowchart TD
    subgraph TiDB_Split ["TiDB: Tự Động Chia Tách Region 96MB"]
        R1["Region 1: Dải Khóa [0x00 - 0x80)<br/>Kích thước: 96MB (Bình thường)"] -->|"Ghi dữ liệu vượt ngưỡng 144MB"| Split["Kích hoạt tách Region"]
        Split --> R1A["Region 1A: [0x00 - 0x40)<br/>Kích thước: 72MB (Đang chạy)"]
        Split --> R1B["Region 1B: [0x40 - 0x80)<br/>Kích thước: 72MB (Đang chạy)"]
        R1B -->|"Bộ Điều Phối PD Heartbeat"| Rebalance["Raft Learner Đón Dữ Liệu -> Chuyển Giao Leader"]
        Rebalance --> TiKV_New["Di Trú Sang Node TiKV Rảnh Rỗi<br/>(Không Gián Đoạn Dịch Vụ)"]
    end

3.1. TiDB: Tự Động Chia Tách Region 96MB & Cân Bằng Tải Qua Multi-Raft

  1. Phân Vùng Theo Dải Byte Liên Tục: Toàn bộ không gian dữ liệu được chia thành các dải byte liên tục gọi là Regions, với kích thước mục tiêu mặc định là 96MB.
  2. Ngưỡng Tự Động Phân Tách (Auto-Split): Khi khối lượng dữ liệu ghi vượt quá 144MB (hoặc vượt 1.44 triệu keys), TiKV tự động bẻ đôi Region thành hai Region con 72MB tại điểm khóa trung vị.
  3. Cân Bằng Tải Không Can Thiệp Thủ Công (Zero-Touch): Cụm Placement Driver (PD) nhận thông tin nhịp tim (Heartbeat) từ các Raft leaders định kỳ mỗi 10 giây. Khi phát hiện mất cân bằng CPU hoặc dung lượng đĩa:
    • PD khởi tạo thêm một thành viên Raft Learner trên node TiKV còn trống.
    • Dữ liệu SSTable snapshot được truyền ngầm qua mạng mà không hề khóa bảng.
    • Khi Learner bắt kịp tiến độ, PD thăng cấp nó thành Voter đầy đủ và hoán đổi vai trò Raft Leader trong vài mili-giây mà người dùng không hề hay biết.

3.2. Vitess: Quy Trình Resharding Trực Tuyến Qua VReplication

Trong khi đó, việc phân mảnh lại (Resharding) trên Vitess đòi hỏi người quản trị phải chủ động can thiệp theo một quy trình kiểm soát chặt chẽ:

sequenceDiagram
    autonumber
    actor Admin as Quản Trị Viên DB / Pipeline CI-CD
    participant VTGate as VTGate Proxy Router
    participant SourceShard as Shard Cũ (-80)
    participant TargetShards as Shard Mới (-40, 40-80)
    participant App as Ứng Dụng Microservices

    Admin->>TargetShards: Khởi Tạo Các Node Shard Mới
    Admin->>VTGate: Reshard Create -source -80 -target -40,40-80
    TargetShards->>SourceShard: VReplication: Sao Chép Snapshot Dữ Liệu
    loop Đồng Bộ Hóa Thời Gian Thực
        SourceShard-->>TargetShards: Đọc Luồng MySQL Binlog Row Events
    end
    Admin->>VTGate: SwitchTraffic (Chuyển Luồng Đọc: -40, 40-80)
    VTGate->>TargetShards: Định Tuyến Lệnh SELECT
    Admin->>VTGate: SwitchTraffic (Chuyển Luồng Ghi: Cutover)
    Note over VTGate,SourceShard: Khóa Ghi Chớp Nhoáng (< 2 giây)
    VTGate->>TargetShards: Định Tuyến INSERT / UPDATE / DELETE
    Admin->>SourceShard: Hủy Bỏ Shard Cũ (-80)
  1. Khai Báo Phân Vùng Đích: Quản trị viên chỉ định ranh giới phân mảnh mới (ví dụ: tách Shard -80 thành hai Shard -40 và 40-80).
  2. Đồng Bộ Dữ Liệu Qua VReplication: Động cơ VReplication sao chép dữ liệu tĩnh của các bảng, đồng thời liên tục đọc và áp dụng các dòng binlog phát sinh từ Shard nguồn.
  3. Cắt Chuyển Lưu Lượng An Toàn (Cutover): Khi độ trễ replication giảm xuống dưới 1 giây, lệnh SwitchTraffic được kích hoạt. VTGate tạm dừng ghi trong một cửa sổ cực ngắn ($< 2$ giây) để xả sạch queue đang chờ, sau đó tráo đổi bảng định tuyến nội bộ sang các Shard mới mà không làm mất mát giao dịch.

3.3. Tinh Chỉnh Nén RocksDB LSM-Tree Trên TiKV Để Khử Giật Độ Trễ Đuôi

TiKV sử dụng RocksDB (LSM-tree) làm động cơ lưu trữ cấp node. Mặc dù cấu trúc LSM tối ưu hóa ghi tuần tự vào MemTable (RAM), nó tạo ra khoản nợ nén dữ liệu (Compaction Debt) trên đĩa. Nếu không được tinh chỉnh, các đợt nén nặng giữa tầng L0 và L1 sẽ gây hiện tượng nghẽn I/O (Write Stall), đẩy độ trễ đuôi $P99.9$ vọt từ 15ms lên hơn 500ms:

[Quy Trình Ghi RocksDB Trong TiKV]
Client Write ──> Write-Ahead Log (WAL) + MemTable Đang Hoạt Động (RAM)
                         │ (Flush khi đầy 128MB)
                         ▼
                   Level 0 SSTables (Đĩa - Dải Khóa Bị Gối Lên Nhau)
                         │ (Kích hoạt nén khi số tệp L0 >= 4)
                         ▼
                   Level 1 SSTables (Đĩa - Dải Khóa Không Trùng Lặp)
                         │ (Nén nấc thang xuống Level 6 bằng Zstd)
                         ▼
                   Level 2 ... Level 6 SSTables

Cấu Hình TiKV Tối Ưu Nén LSM Chuẩn Production (tikv.toml):

[rocksdb]
# Số luồng nền tối đa dành riêng cho thao tác flush và compaction
max-background-jobs = 8

[rocksdb.defaultcf]
# Kích thước MemTable trong RAM trước khi kích hoạt flush ra đĩa
write-buffer-size = "128MB"
max-write-buffer-number = 5

# Tinh chỉnh ngưỡng nén L0 để tránh đột biến Write Stall
level0-file-num-compaction-trigger = 4
level0-slowdown-writes-trigger = 20
level0-stop-writes-trigger = 36

# Kích thước Block Cache (chiếm 45-50% tổng dung lượng RAM của node)
block-cache-size = "32GB"
cache-index-and-filter-blocks = true
pin-l0-filter-and-index-blocks-in-cache = true

# Nén Zstandard ở các tầng sâu nhằm tối ưu hóa 70% dung lượng SSD
compression-per-level = ["no", "no", "lz4", "lz4", "zstd", "zstd", "zstd"]

4. Phân Lập Miền Lỗi (Blast Radius) & Tính Độc Lập Hạ Tầng

Sự bền bỉ của tầng cơ sở dữ liệu khi đối mặt với sự cố phần cứng hoặc mã độc hại phụ thuộc trực tiếp vào cách phân chia miền lỗi của từng hệ thống:

[Vitess Blast Radius: Lỗi Shard Được Phân Lập Tuyệt Đối]
Shard 1 (-40) ────> [SẬP NGUỒN / LỖI ĐĨA]  ──> Chỉ ảnh hưởng 25% người dùng (Tenant A)
Shard 2 (40-80) ──> [HOẠT ĐỘNG BÌNH THƯỜNG] ──> 75% người dùng vẫn chạy 100% SLA

[TiDB Blast Radius: Rủi Ro Phụ Thuộc Cụm Tập Trung]
Nghẽn Latency PD Leader / Bão Mạng Raft ──> TOÀN BỘ CỤM BỊ NGHẼN (Ảnh Hưởng 100%)
  • Vitess (Phân Lập Miền Lỗi Tuyệt Đối):
    • Mỗi Shard là một cụm MySQL độc lập hoàn toàn với RAM, CPU, Buffer Pool và ổ đĩa riêng biệt.
    • Một lỗi tràn bộ nhớ (OOM), câu lệnh khóa bảng tai hại hoặc lỗi hư hỏng ổ cứng trên Shard 1 chỉ làm tê liệt đúng tập hợp khách hàng nằm trên Shard đó. Toàn bộ các Shard còn lại vẫn vận hành ở mức SLA 100%.
  • TiDB (Rủi Ro Lan Tỏa Trong Cụm Dùng Chung):
    • TiDB phụ thuộc vào cụm Placement Driver (PD) tập trung và mạng lưới Multi-Raft liên kết chéo.
    • Một nút thắt cổ chai trên node PD Leader, sự cố rớt gói mạng giữa các Availability Zones (cross-AZ packet loss) hoặc một câu truy vấn phân tích không đánh chỉ mục nuốt cạn tài nguyên TiDB có thể dẫn đến hiện tượng nghẽn domino ảnh hưởng tới 100% người dùng trên toàn hệ thống.

5. Đo Lường Thực Nghiệm Giao Dịch Phân Tán Với Go 1.25

Để đánh giá định lượng độ trễ giao dịch và cơ chế xử lý xung đột ghi (Write Conflict) dưới tải cao, chúng tôi xây dựng đoạn mã kiểm thử chuẩn Go 1.25 mô phỏng giao dịch chuyển tiền giữa 2 tài khoản:

package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log"
	"math/rand"
	"sync"
	"sync/atomic"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

type BenchmarkStats struct {
	CommittedTx   uint64
	ConflictRetry uint64
	FailedTx      uint64
	LatencySumUs  uint64
}

// ExecuteTransactionalTransfer thực thi giao dịch chuyển khoản 2 chiều kèm cơ chế retry có jitter
func ExecuteTransactionalTransfer(ctx context.Context, db *sql.DB, fromID, toID int64, amount float64, stats *BenchmarkStats) error {
	maxRetries := 5
	backoff := 2 * time.Millisecond

	for attempt := 0; attempt < maxRetries; attempt++ {
		start := time.Now()
		err := func() error {
			txCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
			defer cancel()

			tx, err := db.BeginTx(txCtx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
			if err != nil {
				return err
			}
			defer tx.Rollback()

			// Trừ tiền tài khoản nguồn
			res1, err := tx.ExecContext(txCtx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
			if err != nil {
				return err
			}
			if rows, _ := res1.RowsAffected(); rows == 0 {
				return errors.New("không tìm thấy tài khoản nguồn")
			}

			// Cộng tiền tài khoản đích
			res2, err := tx.ExecContext(txCtx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
			if err != nil {
				return err
			}
			if rows, _ := res2.RowsAffected(); rows == 0 {
				return errors.New("không tìm thấy tài khoản đích")
			}

			return tx.Commit()
		}()

		elapsed := time.Since(start).Microseconds()

		if err == nil {
			atomic.AddUint64(&stats.CommittedTx, 1)
			atomic.AddUint64(&stats.LatencySumUs, uint64(elapsed))
			return nil
		}

		// Xử lý xung đột ghi Percolator trong TiDB hoặc Deadlock trong MySQL
		atomic.AddUint64(&stats.ConflictRetry, 1)
		jitter := time.Duration(rand.Int63n(int64(backoff)))
		select {
		case <-ctx.Done():
			atomic.AddUint64(&stats.FailedTx, 1)
			return ctx.Err()
		case <-time.After(backoff + jitter):
			backoff *= 2
		}
	}

	atomic.AddUint64(&stats.FailedTx, 1)
	return fmt.Errorf("giao dịch thất bại sau %d lần thử lại", maxRetries)
}

func main() {
	// Kết nối tới VTGate hoặc TiDB SQL qua giao thức chuẩn MySQL
	dsn := "root@tcp(127.0.0.1:4000)/test_db?charset=utf8mb4&parseTime=True"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Khởi tạo kết nối thất bại: %v", err)
	}
	defer db.Close()

	db.SetMaxOpenConns(500)
	db.SetMaxIdleConns(50)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	var stats BenchmarkStats
	var wg sync.WaitGroup
	workers := 100

	log.Printf("Bắt đầu bài kiểm thử giao dịch với %d workers đồng thời...", workers)
	benchStart := time.Now()

	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			for {
				select {
				case <-ctx.Done():
					return
				default:
					from := rand.Int63n(10000) + 1
					to := rand.Int63n(10000) + 1
					if from == to {
						to = (to % 10000) + 1
					}
					_ = ExecuteTransactionalTransfer(ctx, db, from, to, 10.0, &stats)
				}
			}
		}(i)
	}

	wg.Wait()
	duration := time.Since(benchStart).Seconds()

	committed := atomic.LoadUint64(&stats.CommittedTx)
	retries := atomic.LoadUint64(&stats.ConflictRetry)
	failed := atomic.LoadUint64(&stats.FailedTx)
	avgLat := float64(atomic.LoadUint64(&stats.LatencySumUs)) / float64(committed)

	fmt.Printf("--- Kết Quả Benchmark (Thời gian: %.2fs) ---\n", duration)
	fmt.Printf("Giao dịch thành công:   %d (%.2f TPS)\n", committed, float64(committed)/duration)
	fmt.Printf("Số lần thử lại xung đột: %d\n", retries)
	fmt.Printf("Giao dịch thất bại:     %d\n", failed)
	fmt.Printf("Độ trễ Cam Kết Trung Bình: %.2f µs (%.3f ms)\n", avgLat, avgLat/1000.0)
}

6. Đánh Giá FinOps & Chi Phí Hạ Tầng Tối Thiểu

Yêu cầu về hạ tầng phần cứng tối thiểu để duy trì tính sẵn sàng cao (High Availability) giữa hai hệ thống có sự chênh lệch đáng kể:

Tiêu Chí FinOps & Hạ TầngSharded MySQL (Vitess - 2 Shards)TiDB NewSQL Cụm HA Tối ThiểuPhân Tích Thực Chiến
Số Lượng Node Tối Thiểu6 Nodes (2 VTGate + 2 Primary + 2 Replica)11 Nodes (3 PD + 3 TiDB + 5 TiKV)TiDB đòi hỏi số lượng máy chủ gần gấp đôi để đạt quorum Raft an toàn.
Bộ Nhớ RAM Node Lưu Trữ16GB RAM / MySQL Node64GB RAM / TiKV NodeRocksDB LSM-Tree đòi hỏi RAM lớn cho MemTables và Block Cache.
Đột Biến Trễ Do Nén Đĩa❌ Không có (InnoDB B+ Tree flush đều)⚠️ Thường xuyên (RocksDB Level Compaction)Các đợt nén nặng làm phát sinh đột biến trễ P99.9 khi chịu tải ghi liên tục.
Chi Phí Máy Chủ Hàng Tháng$1,800 / tháng$4,600 / thángVitess giúp tiết kiệm 60% chi phí hạ tầng ở quy mô dưới 20TB.

7. Bảng Kết Quả Benchmark Đối Đầu (10,000 Kết Nối)

Được đo lường thực nghiệm bằng sysbench-tpcc trên cụm máy chủ: 32 vCPUs, 64GB RAM, ổ cứng NVMe SSD:

Chỉ Số Vận HànhSharded MySQL (Vitess - 4 Shards)TiDB NewSQL (3 TiDB + 5 TiKV)Đánh Giá Kỹ Thuật
P99 Ghi Single-Shard (ms)1.8 ms11.4 msVitess nhanh hơn 6.3 lần
P99 Đọc Single-Shard (ms)0.6 ms2.4 msVitess nhanh hơn 4.0 lần
Cross-Shard Distributed Join45.0 ms (Proxy 2PC Join)8.2 ms (TiDB Coprocessor)TiDB nhanh hơn 5.5 lần
Thông Lượng Đỉnh (QPS)128,000 QPS84,000 QPSVitess thông lượng cao hơn 52%
Thời Gian Phục Hồi Khi Sập Node3.5s (Orchestrator)< 1.0s (Bầu Raft Leader)TiDB phục hồi tự động nhanh hơn

8. Ma Trận Cây Quyết Định Lựa Chọn Kiến Trúc

flowchart TD
    Start{"Lựa Chọn Kiến Trúc Mở Rộng Cơ Sở Dữ Liệu Nào?"}
    
    Start --> CheckShard{"Dữ liệu có phân mảnh sạch theo Shard Key không?<br/>(ví dụ: tenant_id, user_id, organization_id)"}
    
    CheckShard -->|"CÓ (95%+ Truy vấn đơn Shard)"| CheckLat{"Độ trễ ghi sub-2ms có bắt buộc không?<br/>(ví dụ: Cổng thanh toán, Đặt hàng)"}
    CheckLat -->|"BẮT BUỘC"| R_Vitess["<b>CHỌN SHARDED MYSQL (VITESS)</b><br/>• Độ trễ ghi cực thấp sub-2ms<br/>• Miền lỗi phân lập độc lập<br/>• Giảm 60% chi phí FinOps hạ tầng"]
    CheckLat -->|"KHÔNG (Chấp nhận 8-15ms)"| CheckAuto{"Có ưu tiên tự động resharding không?"}
    CheckAuto -->|"CÓ"| R_TiDB_Auto["<b>CHỌN TIDB NEWSQL</b><br/>Tự động chia tách Region 96MB"]
    
    CheckShard -->|"KHÔNG (Cần Cross-Shard Join & Schema phức tạp)"| R_TiDB_Join["<b>CHỌN TIDB NEWSQL</b><br/>• Động cơ SQL phân tán hoàn chỉnh<br/>• Phân tích HTAP thời gian thực qua TiFlash"]

9. Hỏi & Đáp Kỹ Thuật (Frequently Asked Questions)

Tại sao TiDB lại có mức sàn độ trễ ghi từ 8–15ms?

TiDB thực thi giao thức Cam Kết 2 Pha Google Percolator trên nền Multi-Raft. Mỗi giao dịch ghi bắt buộc phải trải qua nhiều chặng mạng tuần tự tới cụm Placement Driver (để lấy StartTS và CommitTS) và nhân bản đồng thuận qua đa số các node TiKV Raft leaders. Ngay cả trên hạ tầng mạng 10Gbps nội bộ giữa các vùng, các chặng mạng tuần tự này tạo ra mức sàn độ trễ vật lý từ 8–15ms, không thể nào đạt được độ trễ sub-2ms như thao tác flush đĩa cục bộ của MySQL InnoDB.

Vitess đạt được sự cô lập miền lỗi (Blast Radius) như thế nào giữa các Shards?

Mỗi Shard trong Vitess hoạt động như một cụm MySQL độc lập với CPU, RAM và ổ đĩa riêng biệt. Một sự cố nghẽn khóa bảng, lỗi tràn bộ nhớ hoặc hư hỏng đĩa trên Shard 1 chỉ làm ảnh hưởng tới những người dùng được phân bổ vào phân vùng đó, trong khi toàn bộ các Shard còn lại vẫn vận hành ở mức SLA 100%. Ngược lại, trong TiDB, sự cố nghẽn node PD Leader hoặc bão mạng phân tán có thể làm tê liệt toàn bộ cụm.

Secondary Lookup Vindex trong Vitess ảnh hưởng thế nào đến độ trễ giao dịch?

Khi truy vấn theo một trường phụ (ví dụ tra cứu theo email thay vì customer_id), Vitess phải tra cứu bảng chỉ mục phụ Lookup Vindex. Nếu bảng tra cứu này nằm trên một Shard khác, Vitess phải gửi thêm một RPC mạng trung gian trước khi định tuyến câu lệnh chính. Nếu chỉ mục phụ được thiết lập đồng bộ giao dịch với autocommit: false, Vitess phải kích hoạt giao thức 2PC phân tán trên cả hai Shard, làm tăng độ trễ từ 1.5ms lên 15–30ms.

Tại sao cơ chế nén RocksDB LSM-Tree trên TiKV lại gây ra hiện tượng giật độ trễ (P99.9 Tail Latency Spikes)?

TiKV lưu trữ dữ liệu thông qua cấu trúc cây LSM của RocksDB. Khi khối lượng ghi dồn dập làm đầy các MemTable trong bộ nhớ RAM, các luồng nền sẽ flush dữ liệu ra các tệp Level 0 SSTables trên đĩa. Khi số lượng tệp Level 0 vượt ngưỡng kích hoạt (mặc định là 4), RocksDB phải thực hiện nén dữ liệu dung lượng lớn xuống các tầng sâu hơn. Nếu băng thông I/O của ổ cứng NVMe bị bão hòa, cơ chế Write Stall sẽ tự động hãm tốc độ ghi của ứng dụng, khiến độ trễ P99.9 đột ngột tăng vọt từ 10ms lên tới hơn 500ms.

Kiến trúc kết hợp đa tầng (Hybrid Tiered Architecture) nào tối ưu nhất cho doanh nghiệp lớn?

Mô hình chuẩn mực tại các tập đoàn công nghệ lớn là triển khai Sharded MySQL (Vitess) làm tầng giao dịch nóng OLTP chịu tải cao (với độ trễ ghi sub-2ms cho luồng thanh toán và giỏ hàng cốt lõi), đồng thời truyền dữ liệu thay đổi theo thời gian thực qua CDC (Debezium / Kafka / TiCDC) sang cụm TiDB + TiFlash để phục vụ tầng báo cáo phân tích toàn cầu và các truy vấn liên miền dữ liệu phức tạp.

🔗 Điều Hướng Bài Học & Masterclass Navigation