Bạn đã bao giờ làm project, nhắm mắt set cái id AUTO_INCREMENT trong database rồi kê cao gối ngủ chưa? Nghe thì chill phết đúng không? Nhưng ngó sang các hệ thống cỡ bự, nơi hàng chục ngàn lượt tương tác nổ ra mỗi giây, thì hóa ra cái kiểu auto-increment đó lại là... một thảm họa!
Snowflake là cái quái gì?
Khi hệ thống càng bự, việc bắt một con database duy nhất đứng ra phát ID sẽ tạo ra điểm nghẽn cổ chai và có nguy cơ sập toàn tập.
Dùng UUID thì sao? Khứa này giải quyết được vụ đụng hàng, nhưng nó to chà bá 128 bits và hoàn toàn ngẫu nhiên lộn xộn.
Các pháp sư Twitter đã tạo ra Snowflake. Thay vì dựa vào database, mỗi server sẽ tự đúc ID bằng 1 số 64-bit độc nhất vô nhị và đặc biệt có thể sort được theo thời gian.
Nghe ảo không? Các pháp sư đã cắt 64 bits như này:
- 1 bit: Sign. Luôn là 0 để đảm bảo ID sinh ra luôn dương. Việc này nhằm đảm bảo tính nhất quán của việc sort.
Vậy sao không dùng luôn số unsigned luôn đi?
- Thứ nhất Snowflake viết bằng Scala chạy trên JVM của Java, mà Java thì bảo thủ không hỗ trợ số unsigned.
- Thứ hai ID có thể được sử dụng trong hệ thống sử dụng nhiều ngôn ngữ lập trình, một số xử lý số unsigned không tốt nên thôi phí 1 bit không ảnh hưởng nhiều.
-
41 bits: Lưu timestamp. Nhờ cái này mà ID sinh sau luôn to hơn ID sinh trước.
-
10 bits: Căn cước định danh cho server đó. Cho phép tận 1024 anh em servers tha hồ phát ID cùng lúc mà không sợ dẫm chân nhau.
-
12 bits: Bộ nhảy số. Nếu một con server phát quá nhiều ID trong cùng 1 mili-giây, bộ đếm này sẽ tăng dần lên (4096 IDs/mili-giây).
Góc khuất
Custom epoch
Nếu tinh ý sẽ thấy 41 bits timestamp có giới hạn tối đa là 2^64 - 1, số này quy ra đâu đó khoảng 70 năm.
Timestamp thường được đếm bắt đầu từ ngày 01/01/1970 (Unix Epoch). Nếu dùng mốc này thì Snowflake chỉ xài được tới năm 2040 sẽ hết ID.
Cách giải quyết khá đơn giản. Đổi mốc tính thành ngày thành lập dự án. Ví dụ Twitter lưu ngày thành lập dự án mỗi lần phát ID thì trừ cái số đó để lấy mốc mới. Source
val twepoch = 1288834974657LClock backward
Vì Snowflake phụ thuộc hoàn toàn vào thời gian thực của server để tạo ID, kẻ thù lớn nhất của nó chính là sự sai lệch thời gian.
Các servers thường dùng giao thức NTP (Network Time Protocol) để đồng bộ giờ với nhau. Đôi khi do lỗi đồng bộ, đồng hồ của server có thể bị giật lùi về quá khứ vài mili-giây. Nếu server sinh ID trong này, sẽ tạo ra những ID trùng với những ID vừa được sinh ra trước đó.
Cách khắc phục: Thuật toán bắt buộc phải lưu lại "Timestamp của lần tạo ID cuối". Source
if (timestamp < lastTimestamp) {
exceptionCounter.incr(1)
log.error(
"clock is moving backwards. Rejecting requests until %d.",
lastTimestamp
);
throw new InvalidSystemClock("...".format(lastTimestamp - timestamp));
}
lastTimestamp = timestamp
genCounter.incr()Ngoài ra, nếu server bị vắt kiệt sức, phải sinh nhiều hơn 4096 ID/mili-giây, bộ đếm sẽ bị tràn về 0. Lúc này thuật toán bắt server phải "ngủ đông" chờ đến mili-giây tiếp theo. Source
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp)
}
} else {
sequence = 0
}