English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

khách
1 / ?
trở lại bài học

Mẫu Bốn Bước

Lỗi nằm trong bốn bước tuần tự, không nguyên tử:

// DEFECT
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;

Bước 1: kiểm tra cache. Bước 2: miss. Bước 3: tính toán. Bước 4: lưu trữ. Cả bốn bước: không nguyên tử. Giữa bước 1 & bước 4, bất kỳ số lượng luồng nào cũng có thể thực thi bước 1 & tất cả đều thấy null.

Bẫy Idempotency

Lý do bảo vệ pattern này: 'OK nếu hai luồng cùng tính toán & lưu cùng một giá trị. Kết quả là idempotent. Không xảy ra hỏng dữ liệu.'

Lý do này: đúng về tính đúng đắn. Sai lầm về chi phí.

Với 1.000 luồng khi cache miss: 1.000 luồng mỗi luồng thực thi expensiveCompute(key). Nếu expensiveCompute truy vấn cơ sở dữ liệu, 1.000 truy vấn cơ sở dữ liệu được thực hiện đồng thời. Nếu nó gọi dịch vụ bên ngoài, 1.000 yêu cầu HTTP được gửi đồng thời. Hệ thống tạo ra kết quả đúng sẽ sụp đổ vì chi phí tạo ra chúng.

Ba Kích Hoạt

Một Thundering Herd xảy ra khi một cache key chuyển từ warm sang cold đồng thời trên nhiều luồng:

Cold start: dịch vụ khởi động lại với cache trống. Đợt request đầu tiên: mọi key đều miss. Tất cả tính toán đồng thời.

Service restart: rolling restart làm reset cache trên các instance. Traffic được phân phối lại đến các instance lạnh.

TTL expiry: một key có traffic cao hết hạn. N luồng cùng kiểm tra, cùng miss, cùng tính toán trước khi luồng đầu tiên lưu kết quả.

Cả ba trigger đều tương quan với các đợt tăng đột biến lưu lượng truy cập. Herd kích hoạt khi lưu lượng đạt đỉnh và cache đang lạnh. Đây là thời điểm tồi tệ nhất.

Ví dụ Elasticsearch EnrichCache

Elasticsearch EnrichCache: nhận xét trong tài liệu ghi rõ “intentionally non-locking for simplicity…OK if we re-put the same key/value in a race condition.” Với tốc độ 10.000 tài liệu mỗi giây và enrich cache đang lạnh: cả 10.000 request đều truy cập enrich index cùng lúc. Enrich index vốn được thiết kế cho các truy vấn không thường xuyên, giờ phải xử lý 10.000 truy vấn đồng thời. Cluster trở nên không ổn định.

Lý do về idempotency: đúng trong comment mã nguồn. Nhưng thảm họa khi xử lý 10.000 tài liệu mỗi giây.

Liên kết với MOAD-0001

MOAD-0001 (Sedimentary Defect) tạo ra nút thắt O(N²) trong các hệ thống có lưu lượng cao. Việc sửa MOAD-0001 (từ O(N²) sang O(N)) loại bỏ nút thắt tại workstation. Lưu lượng nhanh hơn sẽ đẩy nhiều request hơn xuống phía sau. Các cache phía sau, trước đây được bảo vệ bởi nút thắt MOAD-0001, giờ nhận các đợt tăng đột biến lưu lượng tương quan. MOAD-0005 kích hoạt ở những cache trước đây chưa từng gặp. Sửa một MOAD; chuẩn bị cho MOAD khác.

Idempotency Trap Sai Ở Đâu

Nhận xét của Elasticsearch thể hiện tư duy kỹ thuật cẩn trọng nhưng áp dụng cho câu hỏi sai. Idempotency là một đặc tính thực sự đáng để phân tích. Sai lầm nằm ở việc dừng lại ở tính đúng đắn mà không xem xét thêm chi phí.

Tại sao lập luận “không sao nếu hai luồng ghi cùng một kết quả” lại dẫn đến lỗi? Nó đúng ở điểm nào và bỏ sót điều gì?

computeIfAbsent & singleflight

Giải pháp: làm cho thao tác kiểm tra-và-tính toán trở nên atomic. Chỉ một luồng thực hiện tính toán. Tất cả các luồng khác chờ kết quả đó.

Java: computeIfAbsent

// DEFECT: bốn bước không nguyên tử
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;

// FIX: atomic check-and-compute
return cache.computeIfAbsent(key, k -> expensiveCompute(k));

computeIfAbsent: nếu key chưa tồn tại, tính toán đúng một lần, lưu trữ và trả về. Tất cả các luồng khác gọi computeIfAbsent với cùng key sẽ chờ kết quả tính toán đầu tiên. Không tính toán N lần. Không xảy ra stampede.

Go: singleflight.Group

var g singleflight.Group

func getOrCompute(key string) (Value, error) {
v, err, _ := g.Do(key, func() (interface{}, error) {
return expensiveCompute(key)
})
return v.(Value), err
}

singleflight: nếu một computation cho key đã đang chạy, tất cả các caller cùng key sẽ chờ và chia sẻ kết quả duy nhất. Một lần tính toán, N waiter cùng nhận kết quả. 'flight' abstraction: khử trùng lặp các request đang in-flight.

Lock vs singleflight

Một per-key lock ngây thơ sẽ tuần tự hóa: thread 1 tính toán, thread 2 chờ, thread 3 chờ. Sau khi thread 1 xong, thread 2 vào và kiểm tra cache (hit). Thread 3 vào và kiểm tra cache (hit). N-1 lần acquire lock và đọc cache.

singleflight khử trùng lặp: thread 1 tính toán, các thread 2 đến N đều chờ kết quả của thread 1. Không có các lần acquire lock riêng lẻ. Không có các lần đọc cache riêng lẻ. Một lần tính toán, một kết quả, phân phối cho N waiter. Ít thao tác hơn so với per-key lock.

Cả hai đều ngăn stampede. singleflight ngăn công việc thừa triệt để hơn.

Viết lại Pattern

Áp dụng fix vào một tình huống cụ thể.

// Bộ nhớ đệm hồ sơ người dùng trong một dịch vụ Java có lưu lượng truy cập cao
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId);  // tốn kém: truy vấn DB 50ms
profileCache.put(userId, profile);
}
return profile;
}

Dịch vụ khởi động lại mỗi sáng lúc 2 AM. Lúc 8 AM, 10.000 người dùng yêu cầu profile của họ cùng lúc.

Xác định lỗi, nêu thời điểm nó xảy ra, và viết lại bằng computeIfAbsent.