📖 Bản tiếng Anh (English Edition)
Bài trước: Bài 4 — Mở Rộng Cơ Sở Dữ Liệu: Từ Sharded MySQL Sang TiDB NewSQL | Mục lục Series
Answer-first: Shopee vượt qua thách thức giám sát bằng cách thay thế Elasticsearch bằng đường ống thu thập Vector SIMD, Kafka và lưu trữ cột ClickHouse. Việc kết hợp lấy mẫu OpenTelemetry tail-based với eBPF profiling giúp giảm 12 lần chi phí lưu trữ, giữ trọn vẹn mọi lỗi hệ thống với chi phí CPU dưới một phần trăm.
Điều kiện tiên quyết: Nắm vững mô hình dữ liệu quan sát (metrics, logs, traces), cơ chế lưu trữ chỉ mục dạng cột (ClickHouse MergeTree), giao thức lan truyền ngữ cảnh vết OpenTelemetry (W3C) và kỹ thuật phân tích hiệu năng nhân Linux với eBPF.
1. Bài Toán Giám Sát Cực Đại Tại Quy Mô Siêu Lớn
Vận hành hàng ngàn microservices phân tán phục vụ hàng trăm triệu đơn hàng mỗi ngày trên khắp Đông Nam Á đặt ra thách thức giám sát kỹ thuật khổng lồ. Trong các ngày hội mua sắm cao điểm như 11.11, hạ tầng của Shopee phát sinh hơn 100 tỷ bản ghi nhật ký (log records), 500 triệu dấu vết phân tán (trace spans) và 80 triệu chuỗi số liệu thời gian (metric streams) mỗi ngày.
Ở quy mô này, các kiến trúc giám sát truyền thống được xây dựng trên bộ công cụ ELK (Elasticsearch, Logstash, Kibana) chắc chắn sẽ sụp đổ hoàn toàn về cả mặt chi phí hạ tầng lẫn hiệu năng xử lý:
Chi Phí & Dung Lượng Lưu Trữ Ổ Cứng (SSD NVMe)
▲
/ \
/ \
/ \
Độ Toàn Vẹn Dữ Liệu ◄───────► Độ Trễ Phản Hồi Truy Vấn
(Lưu 100% Logs & Traces) (Truy Vấn Sự Cố Dưới 1 Giây)
Ba Hạn Chế Cốt Tử Của Ngăn Xếp ELK Truyền Thống
- Bùng Nổ Dung Lượng Chỉ Mục Đảo (Inverted Index Bloat): Elasticsearch bắt buộc phải tạo chỉ mục đảo Lucene cho từng token văn bản trên tất cả các trường dữ liệu JSON. Trong các chiến dịch khuyến mãi lớn, dung lượng chỉ mục này phình to gấp 1,5 lần dung lượng dữ liệu thô ban đầu. Việc lưu trữ 500 Terabyte log mỗi ngày đòi hỏi hàng ngàn ổ cứng NVMe cao cấp, tiêu tốn hàng triệu USD chi phí hạ tầng máy chủ hàng năm.
- Áp Lực Bộ Nhớ Heap JVM Và Ngừng Hoạt Động Do Gom Rác (GC Pauses): Cả Logstash và Elasticsearch đều phụ thuộc hoàn toàn vào máy ảo Java (JVM). Dưới áp lực tiếp nhận hơn 2 triệu sự kiện mỗi giây, chu kỳ gom rác của Java thường xuyên kích hoạt các đợt đóng băng toàn hệ thống (Stop-The-World) kéo dài từ 15 đến 30 giây. Hậu quả là các node bị rớt khỏi cụm Elasticsearch, kích hoạt bão tái phân bổ phân mảnh (shard reallocation) làm tê liệt hoàn toàn hệ thống cảnh báo sự cố.
- Nghịch Lý Của Cơ Chế Lấy Mẫu Ban Đầu (Head-Based Sampling): Để cắt giảm chi phí lưu vết (tracing), các giải pháp APM truyền thống sử dụng kỹ thuật lấy mẫu tại đầu nguồn — quyết định giữ lại hay loại bỏ trace ngay tại API Gateway trước khi request bắt đầu chạy. Trong một sàn thương mại điện tử nơi 99,9% giao dịch thành công êm đẹp trong 15 mili-giây, việc lấy mẫu ngẫu nhiên 1% tại đầu nguồn sẽ thu về hàng ngàn vết bình thường vô giá trị nhưng lại bỏ sót 99% các trường hợp trễ đuôi p99.9, nghẽn khóa cơ sở dữ liệu và đứt đoạn kết nối cổng thanh toán.
Để vượt qua các giới hạn vật lý này, Shopee đã thiết kế một nền tảng giám sát đa tầng thế hệ mới kết hợp agent Vector viết bằng Rust, hồ chứa đệm Apache Kafka, kho lưu trữ theo cột ClickHouse, cơ chế lấy mẫu vết theo đuôi OpenTelemetry và công nghệ phân tích hiệu năng liên tục không can thiệp bằng eBPF.
2. Kiến Trúc Đường Ống Giám Sát Telemetry Toàn Diện
Hệ thống quan sát hợp nhất của Shopee phân tách độc lập các tầng thu thập dữ liệu, đệm luồng, lưu trữ dạng cột và trực quan hóa phân tích thành các khối chịu tải cao riêng biệt:
flowchart TD
subgraph MayChuVatLy ["Cụm Máy Chủ Kubernetes Bare-Metal (5.000+ Node)"]
AppPod["Pod Microservice (Go Kitex / Ghi stdout)"] --> NodeLog["Socket Log Container (/var/log/pods)"]
NodeLog --> VectorAgent["Vector DaemonSet (Ngôn Ngữ Rust / Phân Tích SIMD)"]
eBPFAgent["Agent eBPF (Beyla / Coroot Đo Lường Trong Nhân Kernel)"]
end
subgraph HoDemLuong ["Hồ Chứa Đệm Giảm Áp Lực Apache Kafka"]
VectorAgent -->|"Gom Mẻ Đẩy Vào (Nén Snappy)"| KafkaLogs["Topic: 'telemetry.logs.raw' (128 Partition)"]
eBPFAgent -->|Luồng Số Liệu Bất Đồng Bộ| KafkaMetrics["Topic: 'telemetry.metrics.raw'"]
OTelApp["OTel Go SDK (Truyền Ngữ Cảnh Vết W3C)|gRPC OTLP (Cửa Sổ 30s)"] --> OTelCol["Nhóm OTel Collector (Bộ Lấy Mẫu Theo Đuôi)"]
OTelCol -->|"Bảo Lưu 100% Lỗi & Trễ p99"| KafkaTraces["Topic: 'telemetry.traces.sampled'"]
end
subgraph KhoLuuTruCot ["Kho Lưu Trữ Dạng Cột Phân Tán (Cụm ClickHouse)"]
KafkaLogs --> GoIngest["Động Cơ Nạp Mẻ Golang (clickhouse-go/v2)"]
KafkaTraces --> GoIngest
GoIngest -->|"Ghi Khối Lớn: 100k Dòng / 2 Giây"| CH1["ClickHouse Shard 1 (MergeTree / Nén ZSTD)"]
GoIngest -->|"Ghi Khối Lớn: 100k Dòng / 2 Giây"| CH2["ClickHouse Shard 2 (MergeTree / Nén ZSTD)"]
GoIngest -->|"Ghi Khối Lớn: 100k Dòng / 2 Giây"| CHN["ClickHouse Shard N (ReplicatedMergeTree)"]
end
subgraph GiaoDienTrucQuan ["Giao Diện Trực Quan & Cảnh Báo"]
CH1 & CH2 & CHN --> Grafana["Dashboard Grafana (Truy Vấn Phân Tích Dưới 500ms)"]
CH1 & CH2 & CHN --> SREAlert["Alertmanager / Phát Hiện Bất Thường Thời Gian Thực"]
end
classDef node fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;
classDef kafka fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
classDef ch fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
classDef ui fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
class MayChuVatLy node;
class HoDemLuong kafka;
class KhoLuuTruCot ch;
class GiaoDienTrucQuan ui;
Các Thành Phần Kiến Trúc Cốt Lõi
- Vector DaemonSet Thu Thập Log: Được viết bằng Rust, Vector chạy dưới dạng Kubernetes DaemonSet trên từng máy chủ vật lý. Vector đọc trực tiếp các tệp log container từ thư mục
/var/log/pods, loại bỏ hoàn toàn chi phí truyền qua mạng nội bộ. Nhờ tận dụng tập lệnh vector hóa SIMD (simd-json), một agent Vector có thể phân tích cú pháp hơn 150.000 log JSON mỗi giây trong khi chỉ ngốn chưa đầy 1,5% CPU và 45MB RAM của máy chủ. - Hệ Thống Thu Thập OpenTelemetry Collector (Lấy Mẫu Theo Đuôi): Các ứng dụng Go được nhúng SDK OpenTelemetry phát các span vết phân tán về cụm OTel Collector qua giao thức gRPC OTLP. Thay vì quyết định sớm, collector giữ toàn bộ các span của một trace trong bộ đệm vòng tròn (ring buffer) trong vòng 30 giây, đánh giá toàn bộ vòng đời của trace trước khi quyết định lưu hay hủy.
- Cơ Sở Dữ Liệu Dạng Cột ClickHouse: ClickHouse thay thế hoàn toàn vai trò của Elasticsearch. Nhờ tổ chức dữ liệu theo từng cột nhị phân riêng biệt nén bằng thuật toán ZSTD và LZ4, ClickHouse đạt tỷ lệ nén lên tới 12:1 so với văn bản JSON gốc. Động cơ thực thi truy vấn vector hóa quét qua hàng tỷ dòng mỗi giây trên các vi xử lý đa nhân, phản hồi các báo cáo phân tích phức tạp trong chưa đầy 500 mili-giây.
3. Cấu Trúc Bảng ClickHouse Lưu Trữ Telemetry Tốc Độ Cao
Dưới đây là cấu trúc bảng DDL chuẩn doanh nghiệp được triển khai trên các cụm ClickHouse của Shopee. Bảng sử dụng động cơ ReplicatedMergeTree, phân vùng theo ngày, chỉ mục thưa (sparse index) và các bộ nén chuyên biệt cho từng kiểu dữ liệu.
-- DDL Chuẩn Doanh Nghiệp Cho Bảng Lưu Trữ Log Phân Tán Trong ClickHouse
CREATE DATABASE IF NOT EXISTS shopee_telemetry ON CLUSTER ch_cluster;
CREATE TABLE shopee_telemetry.application_logs_local ON CLUSTER ch_cluster
(
timestamp DateTime64(6, 'UTC') CODEC(DoubleDelta, ZSTD(1)),
service_name LowCardinality(String) CODEC(ZSTD(1)),
environment LowCardinality(String) CODEC(ZSTD(1)),
pod_name LowCardinality(String) CODEC(ZSTD(1)),
log_level LowCardinality(String) CODEC(ZSTD(1)),
trace_id String CODEC(ZSTD(3)),
span_id String CODEC(ZSTD(3)),
user_id Int64 CODEC(DoubleDelta, ZSTD(1)),
sku_id Int64 CODEC(DoubleDelta, ZSTD(1)),
duration_ms Float32 CODEC(Gorilla, ZSTD(1)),
message String CODEC(ZSTD(6)),
attributes Map(String, String) CODEC(ZSTD(3)),
INDEX idx_trace_id trace_id TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_user_id user_id TYPE minmax GRANULARITY 4
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/application_logs', '{replica}')
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (service_name, log_level, toUnixTimestamp(timestamp), user_id)
TTL toDateTime(timestamp) + INTERVAL 30 DAY DELETE
SETTINGS
index_granularity = 8192,
min_bytes_for_wide_part = 10485760,
parts_to_throw_insert = 300;
-- Bảng Distributed Để Truy Vấn Toàn Cụm Qua Một Đầu Mối
CREATE TABLE shopee_telemetry.application_logs ON CLUSTER ch_cluster AS shopee_telemetry.application_logs_local
ENGINE = Distributed(ch_cluster, shopee_telemetry, application_logs_local, rand());
Các Kỹ Thuật Tối Ưu Hóa Lưu Trữ Trong ClickHouse
LowCardinality(String): Ánh xạ các chuỗi văn bản có tính lặp lại cao (như tên dịch vụservice_name, cấp độ loglog_level, môi trườngenvironment) thành các từ điển số nguyên 8-bit hoặc 16-bit trong RAM, giúp tăng tốc độ so khớp chuỗi lên gấp 10 lần và tiết kiệm 80% bộ nhớ.- Chuẩn Nén
DoubleDeltaVàGorilla: Nén các chuỗi nhãn thời gian tăng dần và số thực độ trễ xuống chỉ còn 1 đến 2 bit cho mỗi giá trị, tiết kiệm 90% dung lượng ổ cứng so với lưu trữ 64-bit thô. Chỉ Mục Bloom Filter: Chỉ mục lọc Bloom filter trên trườngtrace_idcho phép ClickHouse bỏ qua 99% các khối dữ liệu trên đĩa trong quá trình tra cứu, tìm thấy chính xác một trace trong số 100 tỷ dòng chỉ trong vòng chưa đầy 200 mili-giây.
4. Cài Đặt Thực Tế Động Cơ Nạp Mẻ Bằng Golang (clickhouse-go/v2)
ClickHouse là cơ sở dữ liệu phân tích được tối ưu hóa cho các thao tác chèn dữ liệu theo khối lớn (bulk batch insert). Việc chèn từng dòng đơn lẻ (mỗi log gọi một lệnh INSERT) sẽ gây ra lỗi nghiêm trọng Too many parts in all data parts in table, làm sập hoàn toàn cụm cơ sở dữ liệu.
Đoạn mã Golang dưới đây là dịch vụ nạp dữ liệu mẻ chuẩn sản xuất sử dụng thư viện clickhouse-go/v2. Dịch vụ này gom dữ liệu lấy từ Kafka, quản lý worker đồng thời, thực thi chèn khối nguyên tử hàng ngàn dòng và tự động xả đệm theo chu kỳ thời gian:
package ingest
import (
"context"
"crypto/tls"
"database/sql"
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
)
// TelemetryRecord đại diện cho bản ghi log định kiểu chặt chẽ cần ghi nhận.
type TelemetryRecord struct {
Timestamp time.Time
ServiceName string
Environment string
PodName string
LogLevel string
TraceID string
SpanID string
UserID int64
SKUID int64
DurationMS float32
Message string
Attributes map[string]string
}
// ClickHouseBatchIngester quản lý bộ đệm có giới hạn và chu kỳ xả dữ liệu.
type ClickHouseBatchIngester struct {
conn driver.Conn
batchQueue chan *TelemetryRecord
batchSize int
flushTimeout time.Duration
workerCount int
wg sync.WaitGroup
activeRows int64
insertedRows int64
droppedRows int64
}
func NewClickHouseBatchIngester(addr string, dbName, user, password string, batchSize, queueCap, workers int, flushTimeout time.Duration) (*ClickHouseBatchIngester, error) {
conn, err := clickhouse.Open(&clickhouse.Options{
Addr: []string{addr},
Auth: clickhouse.Auth{
Database: dbName,
Username: user,
Password: password,
},
Settings: clickhouse.Settings{
"max_execution_time": 60,
},
DialTimeout: 5 * time.Second,
MaxOpenConns: workers * 2,
MaxIdleConns: workers,
ConnMaxLifetime: 1 * time.Hour,
TLS: nil, // Cấu hình &tls.Config{} trong môi trường production mTLS
})
if err != nil {
return nil, fmt.Errorf("khong the ket noi toi clickhouse: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := conn.Ping(ctx); err != nil {
return nil, fmt.Errorf("ping clickhouse that bai: %w", err)
}
ingester := &ClickHouseBatchIngester{
conn: conn,
batchQueue: make(chan *TelemetryRecord, queueCap),
batchSize: batchSize,
flushTimeout: flushTimeout,
workerCount: workers,
}
return ingester, nil
}
// Start khởi chạy nhóm worker xử lý gom mẻ đồng thời.
func (ing *ClickHouseBatchIngester) Start(ctx context.Context) {
for i := 0; i < ing.workerCount; i++ {
ing.wg.Add(1)
go ing.workerLoop(ctx, i)
}
}
// Ingest đưa một bản ghi vào hàng đợi với cơ chế kiểm soát áp lực ngược.
func (ing *ClickHouseBatchIngester) Ingest(record *TelemetryRecord) bool {
select {
case ing.batchQueue <- record:
atomic.AddInt64(&ing.activeRows, 1)
return true
default:
atomic.AddInt64(&ing.droppedRows, 1)
return false // Tự ngắt bỏ bớt dữ liệu khi bộ nhớ RAM bị quá tải
}
}
func (ing *ClickHouseBatchIngester) workerLoop(ctx context.Context, workerID int) {
defer ing.wg.Done()
buffer := make([]*TelemetryRecord, 0, ing.batchSize)
ticker := time.NewTicker(ing.flushTimeout)
defer ticker.Stop()
flush := func() {
if len(buffer) == 0 {
return
}
if err := ing.flushBatch(ctx, buffer); err != nil {
log.Printf("[ERROR] Worker %d ghi batch that bai: %v", workerID, err)
atomic.AddInt64(&ing.droppedRows, int64(len(buffer)))
} else {
atomic.AddInt64(&ing.insertedRows, int64(len(buffer)))
}
buffer = buffer[:0]
}
for {
select {
case record, ok := <-ing.batchQueue:
if !ok {
flush()
return
}
buffer = append(buffer, record)
if len(buffer) >= ing.batchSize {
flush()
}
case <-ticker.C:
flush()
case <-ctx.Done():
flush()
return
}
}
}
func (ing *ClickHouseBatchIngester) flushBatch(ctx context.Context, batch []*TelemetryRecord) error {
chBatch, err := ing.conn.PrepareBatch(ctx, `
INSERT INTO shopee_telemetry.application_logs_local (
timestamp, service_name, environment, pod_name, log_level,
trace_id, span_id, user_id, sku_id, duration_ms, message, attributes
)
`)
if err != nil {
return fmt.Errorf("chuan bi batch clickhouse that bai: %w", err)
}
for _, rec := range batch {
err := chBatch.Append(
rec.Timestamp,
rec.ServiceName,
rec.Environment,
rec.PodName,
rec.LogLevel,
rec.TraceID,
rec.SpanID,
rec.UserID,
rec.SKUID,
rec.DurationMS,
rec.Message,
rec.Attributes,
)
if err != nil {
return fmt.Errorf("them dong vao batch that bai: %w", err)
}
}
// Gửi một khối nhị phân gom duy nhất qua giao thức TCP Native
return chBatch.Send()
}
// Close xả sạch dữ liệu tồn và ngắt kết nối an toàn.
func (ing *ClickHouseBatchIngester) Close() error {
close(ing.batchQueue)
ing.wg.Wait()
return ing.conn.Close()
}
5. Kỹ Thuật Lấy Mẫu Vết Theo Đuôi OpenTelemetry (Tail-Based Sampling)
Mô hình lấy mẫu tại đầu nguồn (head-based sampling) truyền thống đưa ra quyết định giữ hay bỏ trace ngay khi request vừa chạm vào hệ thống. Nếu một request gặp sự cố nghẽn khóa cơ sở dữ liệu kéo dài 5 giây ở chặng thứ 10, nhưng trước đó đã bị đánh dấu ngẫu nhiên là “không lấy mẫu” tại API Gateway, toàn bộ thông tin dấu vết chẩn đoán quý giá sẽ bị biến mất vĩnh viễn.
Shopee giải quyết bài toán này bằng cách triển khai cụm OpenTelemetry Collector với cơ chế lấy mẫu theo đuôi (Tail-Based Sampling):
flowchart LR
subgraph TiepNhanSpan ["Tiếp Nhận Luồng Span Đầu Vào"]
direction TB
App1["Dịch Vụ Đơn Hàng (Kitex)"] -->|gRPC OTLP| LB["Cân Bằng Tải OTel (Băm Theo TraceID)"]
App2["Cổng Thanh Toán (Go)"] -->|gRPC OTLP| LB
App3["Lưu Trữ Tồn Kho (TiKV)"] -->|gRPC OTLP| LB
end
subgraph DongCoDanhGia ["Động Cơ Đánh Giá Lấy Mẫu Theo Đuôi"]
direction TB
LB --> RingBuf["Bộ Đệm Vòng Tròn 30 Giây Trong RAM"]
RingBuf --> Rule1{"Quy Tắc 1: Trace Có Chứa Mã Lỗi HTTP 5xx?"}
Rule1 -->|Có| Keep1["Giữ Lại 100% (Ưu Tiên Lưu Trữ Tuyệt Đối)"]
Rule1 -->|Không| Rule2{"Quy Tắc 2: Độ Trễ Có Vượt 250ms (Đỉnh Nhọn p99)?"}
Rule2 -->|Có| Keep2["Giữ Lại 100% (Lưu Trữ Bất Thường Độ Trễ)"]
Rule2 -->|Không| Rule3{"Quy Tắc 3: Lấy Mẫu Xác Suất Nền (Tỷ Lệ 1%)"}
Rule3 -->|Được Chọn| Keep3["Giữ Lại 1% (Làm Số Liệu Thống Kê So Sánh)"]
Rule3 -->|Loại Bỏ| Drop["Hủy Bỏ (Giải Phóng RAM Ngay)"]
end
classDef span fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;
classDef engine fill:#fff3e0,stroke:#ef6c00,stroke-width:2px;
class TiepNhanSpan span;
class DongCoDanhGia engine;
Cấu Hình OTel Collector Cho Lấy Mẫu Theo Đuôi
Dưới đây là tệp cấu hình YAML triển khai trên cụm OpenTelemetry Collector của Shopee:
# Cấu Hình OpenTelemetry Collector Cho Lấy Mẫu Theo Đuôi
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
send_batch_size: 8192
timeout: 1s
tail_sampling:
decision_wait: 30s
num_traces: 250000
expected_new_traces_per_sec: 10000
policies:
# Quy tắc 1: Luôn luôn giữ lại 100% trace bị lỗi hoặc panic
- name: giu-lai-loi
type: status_code
status_code: { status_codes: [ ERROR ] }
# Quy tắc 2: Giữ lại toàn bộ các vết có độ trễ p99 bất thường
- name: giu-lai-trace-cham
type: latency
latency: { threshold_ms: 250 }
# Quy tắc 3: Lấy mẫu xác suất 1% cho các đơn hàng bình thường
- name: lay-mau-xac-suat
type: probabilistic
probabilistic: { sampling_percentage: 1.0 }
exporters:
clickhouse:
endpoint: tcp://clickhouse-cluster.internal:9000?database=shopee_telemetry
ttl: 720h
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, batch]
exporters: [clickhouse]
Nhờ trì hoãn quyết định lấy mẫu cho tới khi toàn bộ chuỗi trace đã kết thúc, Shopee bảo lưu 100% các ngoại lệ, lỗi deadlock cơ sở dữ liệu và các giao dịch chậm p99, trong khi hủy bỏ 99% các giao dịch bình thường không có vấn đề gì. Chiến lược này giúp cắt giảm tới 92% chi phí lưu trữ dấu vết mà vẫn nâng cao tối đa hiệu quả điều tra sự cố của các kỹ sư SRE.
6. Phân Tích Hiệu Năng Bằng eBPF: Giám Sát Không Suy Hao Nhân Linux
Các phương pháp phân tích hiệu năng truyền thống (như công cụ Go pprof lấy mẫu CPU hoặc Java JVMTI sửa đổi mã bytecode) đều gây ra suy hao hiệu năng đáng kể và đòi hỏi phải chỉnh sửa mã nguồn ứng dụng. Trong ngày hội siêu sale 11.11, việc bật pprof trên các pod thanh toán đang chịu tải cực cao có thể làm tăng thời gian gom rác GC và làm giảm từ 4% đến 8% thông lượng giao dịch.
Shopee triển khai công nghệ eBPF (Extended Berkeley Packet Filter) trên toàn bộ các máy chủ Kubernetes:
sequenceDiagram
autonumber
actor User as Giao Dịch Thanh Toán Của Khách Hàng
participant UserSpace as Tiến Trình Dịch Vụ Go (Không Gian Người Dùng)
participant Kernel as Nhân Hệ Điều Hành Linux (Máy Ảo eBPF)
participant PerfMap as Vùng Nhớ Đệm eBPF Ring Buffer
participant Profiler as Daemon Phân Tích Hiệu Năng eBPF
User->>UserSpace: Gửi Request Đặt Hàng Qua Mạng
UserSpace->>Kernel: Gọi System Call write() / epoll_wait()
Note over Kernel: Điểm Móc eBPF Tracepoint (sys_enter_write)
Kernel->>Kernel: Lấy Mẫu Con Trỏ Lệnh (RIP) & Khung Ngăn Xếp Hàm
Kernel->>PerfMap: Ghi Khối Stack Trace Vào Kernel Ring Buffer
Note over PerfMap: Bộ Nhớ Vòng Tròn Zero-Copy Không Cần Khóa
Profiler->>PerfMap: Đọc Dữ Liệu FlameGraph Đã Được Nén Gọn
Profiler->>ClickHouse: Đẩy Hồ Sơ Ngăn Xếp Hàm Về ClickHouse Mỗi 60 Giây
Ưu Điểm Đột Phá Của eBPF Trên Môi Trường Production
- Tuyệt Đối Không Sửa Đổi Mã Nguồn: Chương trình eBPF được nạp thẳng vào các điểm móc kprobe, uretprobe và tracepoint của nhân Linux. Mã nguồn microservice hoàn toàn không cần nhúng thư viện phụ trợ, không cần sidecar và không cần khởi động lại container.
- Suy Hao Hiệu Năng Dưới 0,5%: Việc lấy mẫu diễn ra ở tần số định sẵn (ví dụ 99Hz hoặc 49Hz) trực tiếp bên trong máy ảo eBPF của nhân Linux. Dữ liệu ngăn xếp hàm được tổng hợp trong các bảng băm nội bộ (
BPF_MAP_TYPE_STACK_TRACE) trước khi chuyển lên không gian người dùng, giữ mức chiếm dụng CPU tổng thể dưới 0,5%. - Khả Năng Quan Sát Toàn Diện Xuyên Suốt Tầng Nhân: Khác với các profiler người dùng chỉ thấy các hàm ứng dụng, eBPF liên kết chặt chẽ mã ứng dụng với độ trễ điều phối luồng của nhân Linux, các đợt truyền lại gói tin TCP socket, hiện tượng đóng băng ghi đệm trang Page Cache và các đợt tranh chấp khóa trong tầng hệ thống tệp VFS.
7. Ma Trận Đánh Đổi Kỹ Thuật Và Các Giải Pháp Bị Bác Bỏ
Cân bằng bài toán giám sát quy mô hàng Petabyte đòi hỏi các quyết định đánh đổi dứt khoát:
| Quyết Định Kiến Trúc | Phương Án Bị Bác Bỏ | Lý Do Đánh Đổi & Căn Cứ Thực Tế |
|---|---|---|
| Lưu trữ cột ClickHouse | Dùng chỉ mục đảo Elasticsearch | Chỉ mục đảo làm phình dữ liệu gấp 1,5 lần và gây đóng băng JVM nghiêm trọng dưới tải QPS cao. |
| Agent Vector SIMD viết bằng Rust | Dùng Sidecar Fluentd hoặc Logstash | Fluentd/Logstash ngốn tới 15% CPU của pod và thường xuyên bị nhân Linux tiêu diệt do tràn bộ nhớ (OOM). |
| Lấy mẫu theo đuôi (Tail-Based) | Lấy mẫu tại đầu nguồn (Head-Based) | Lấy mẫu đầu nguồn bỏ sót 99% các trường hợp trễ đuôi p99 và deadlock trước khi chúng kịp bộc lộ. |
| Phân tích hiệu năng bằng eBPF | Bật pprof CPU lấy mẫu trong Go | pprof làm suy giảm 5% thông lượng và hoàn toàn mù tịt trước các độ trễ nằm bên trong nhân hệ điều hành. |
Câu Hỏi Thường Gặp (FAQ)
ClickHouse xử lý chỉ mục thưa (sparse index) khác biệt như thế nào so với cây B-Tree truyền thống?
index_granularity = 8192). Khi truy vấn, ClickHouse đọc chỉ mục thưa siêu nhỏ gọn này để xác định những khối dữ liệu 8.192 dòng nào có khả năng chứa kết quả, bỏ qua hoàn toàn 99,9% các khối dữ liệu còn lại trên đĩa. Cơ chế này cho phép quét qua hàng tỷ bản ghi với tốc độ cực nhanh trong khi tiêu tốn lượng RAM vô cùng khiêm tốn.Điều gì xảy ra với bộ đệm OpenTelemetry nếu pod collector bị cạn kiệt bộ nhớ RAM?
num_traces (thường được cấu hình là 250.000 trace). Nếu một đợt bùng nổ lưu lượng bất thường đẩy mức chiếm dụng RAM lên sát ngưỡng giới hạn của container, bộ giới hạn bộ nhớ khẩn cấp (memory-limiter) sẽ tự động kích hoạt: nó hủy các trace chưa hoàn tất cũ nhất và tạm thời chuyển sang chế độ lấy mẫu xác suất 1% tại đầu nguồn. Cơ chế tự vệ này bảo đảm collector không bao giờ bị sập hoặc làm gián đoạn luồng tiếp nhận telemetry trong lúc hệ thống đang xảy ra sự cố lớn.Tại sao việc chèn log vào ClickHouse theo từng dòng đơn lẻ lại gây sập máy chủ?
INSERT trong ClickHouse sẽ tạo ra một phần dữ liệu (data part) vật lý bất biến trên ổ cứng. Các luồng nền của ClickHouse liên tục gom các part nhỏ này thành các part lớn hơn. Nếu ứng dụng gửi 5.000 lệnh insert từng dòng mỗi giây, hàng ngàn part nhỏ sẽ sinh ra nhanh hơn tốc độ gom của động cơ nền. Máy chủ sẽ ném ra lỗi nghiêm trọng Too many parts in all data parts in table và từ chối nhận toàn bộ các lệnh ghi mới. Bằng cách gom ít nhất từ 10.000 đến 100.000 dòng trong Go trước khi gửi một mẻ chèn duy nhất, các part được tạo ra gọn gàng và giữ vững độ ổn định của máy chủ.Làm thế nào chương trình eBPF liên kết được ngăn xếp nhân Linux với mã nguồn ứng dụng Golang?
ví dụ: github.com/shopee/order/engine.ProcessOrder:142).Tài Liệu Tham Khảo Kỹ Thuật
Để nghiên cứu sâu hơn về kiến trúc hạ tầng và kỹ thuật tối ưu hóa hiệu năng hệ thống:
- Dịch vụ tư vấn kiến trúc doanh nghiệp & giải pháp tải cao
- Thiết kế hệ thống vi dịch vụ hiệu năng cao với Golang
- Chiến lược mở rộng quy mô MySQL theo chiều ngang
- Kiến trúc giao dịch chịu tải đỉnh cao Alipay Double 11
Tổng Kết Toàn Bộ Series
Chúc mừng bạn đã hoàn thành trọn vẹn Series Chuyên Sâu Kiến Trúc Shopee. Qua 5 bài học toàn diện này, chúng ta đã mổ xẻ toàn bộ bản thiết kế kỹ thuật đằng sau sàn thương mại điện tử có lượng truy cập lớn nhất Đông Nam Á:
- Bài 1: Nền Tảng Microservices — Go, gRPC và API Gateway
- Bài 2: Động Cơ Flash Sale — Redis Lua & Chống Bán Quá Kho
- Bài 3: Tấm Khiên Lưu Lượng — WAF, Rate Limiting & Kafka Peak Shaving
- Bài 4: Mở Rộng Cơ Sở Dữ Liệu — Từ Sharded MySQL Sang TiDB NewSQL
- Bài 5: Giám Sát Cực Đại — OpenTelemetry, ClickHouse & Tracing
