Bạn đã biết Unix timestamp chỉ là số giây tính từ 1 tháng 1 năm 1970, 00:00:00 UTC. Nghe đơn giản. Nhưng rồi bạn phát hiện hai sự kiện có thể cùng một timestamp, đồng hồ máy tính có thể chạy lùi, hai máy chủ có thể không thống nhất về giờ hiện tại, và một đồng hồ báo tới nanosecond vẫn có thể sai vài mili giây.
Đó là lúc việc giữ thời gian trên Linux trở thành một bài toán kỹ thuật khó hơn vẻ ngoài của một số nguyên tưởng chừng nhàm chán. Hãy cùng lần theo mạch đó.
Chọn đồng hồ theo mục đích: thời gian thực hay đơn điệu?
Hãy hình dung chương trình của bạn ghi lại thời điểm bắt đầu tải một tệp, tải xong rồi ghi thời điểm kết thúc. Giữa chừng, đồng hồ hệ thống được chỉnh lại. Kết quả: chương trình tưởng rằng việc tải đã kết thúc trước khi nó bắt đầu.
Tệp không hề du hành ngược thời gian. Vấn đề nằm ở chỗ bạn đã dùng một đồng hồ có thể bị điều chỉnh để đo khoảng thời gian đã trôi qua.
Linux cung cấp nhiều loại đồng hồ cho các mục đích khác nhau. Hai loại hữu ích nhất:
CLOCK_REALTIME(CLOCK_REALTIME) biểu diễn thời gian thực (wall-clock) hiện tại.CLOCK_MONOTONIC(CLOCK_MONOTONIC) cung cấp một đồng hồ tiến đều để đo thời gian trôi qua.
Đồng hồ thời gian thực có thể nhảy tới hoặc nhảy lùi khi hệ thống điều chỉnh. Đồng hồ đơn điệu không chạy lùi, dù trên Linux tốc độ của nó vẫn có thể được điều chỉnh.
Sự khác biệt này cũng thể hiện trong Python:
import time # import time
print("Wall clock:", time.time()) # print("Wall clock:", time.time())
print("Monotonic clock:", time.monotonic()) # print("Monotonic clock:", time.monotonic())
Các giá trị này mang ý nghĩa khác nhau. time.time() (time.time()) trả về thời gian dựa trên kỷ nguyên Unix. time.monotonic() (time.monotonic()) dùng một mốc tham chiếu riêng của đồng hồ, nên giá trị thô của nó không phải là timestamp lịch.
Bài học: dùng thời gian thực để trả lời “việc này xảy ra khi nào?”, dùng đồng hồ đơn điệu để trả lời “việc này mất bao lâu?”.
NTP và giới hạn của việc đồng bộ đồng hồ
Máy tính có đồng hồ, nhưng điều đó không có nghĩa nó luôn biết giờ chính xác. Đồng hồ phần cứng bị trôi: có cái chạy hơi nhanh, có cái chạy hơi chậm. Để hai máy chạy đủ lâu, đồng hồ của chúng có thể lệch nhau.
Đây là lúc NTP (Network Time Protocol) phát huy tác dụng: nó giúp máy tính đồng bộ đồng hồ với các nguồn thời gian bên ngoài.
Nhưng đồng bộ không phải phép màu. Độ trễ mạng, nguồn thời gian lỗi và các lần hiệu chỉnh đồng hồ đều có thể ảnh hưởng tới kết quả. Tùy cấu hình, phần mềm đồng bộ thời gian có thể điều chỉnh dần tốc độ đồng hồ, hoặc nhảy đồng hồ tới hoặc lùi.
Trên một bản phân phối Linux dùng systemd điển hình, bạn có thể xem cấu hình thời gian bằng lệnh:
timedatectl status # timedatectl status
Kết quả khác nhau tùy bản phân phối và cấu hình. Hãy chú ý tới đồng hồ hệ thống và trạng thái đồng bộ thời gian.
Bài học: một timestamp cho biết đồng hồ đã báo gì, chứ không tự động cho biết giá trị đó gần với giờ đúng đến mức nào.
Lên lịch theo giờ địa phương: vì sao không chỉ là cộng 86,400 giây
Giả sử bạn muốn lên lịch cho một script chạy mỗi ngày lúc 9 giờ sáng. Cách làm hiển nhiên có vẻ là chờ 86,400 giây (24 giờ) sau mỗi lần chạy.
Nhưng một ngày theo lịch địa phương không phải lúc nào cũng có đúng 86,400 giây trôi qua. Việc chuyển đổi giờ mùa hè có thể khiến một ngày địa phương dài 23 hoặc 25 giờ. Trong lịch sử, giây nhuận cũng từng làm phức tạp mối quan hệ giữa UTC và một phép đếm giây đơn giản.
Và múi giờ không chỉ là các độ lệch cố định so với UTC. Chính phủ có thể thay đổi quy tắc, và phần mềm cần thông tin được cập nhật để xử lý những thay đổi đó.
Hãy thử hai lệnh sau trên hệ thống Linux của bạn:
TZ=UTC date # TZ=UTC date
TZ=Asia/Kolkata date # TZ=Asia/Kolkata date
Hai lệnh này hiển thị ngày và giờ hiện tại theo múi giờ được chỉ định.
Với một sự kiện chỉ xảy ra một lần, Unix timestamp có thể chính là thứ bạn cần. Nhưng hãy xét yêu cầu sau:
Chạy script này vào mỗi thứ Hai lúc 9 giờ sáng tại New York.
Chỉ một timestamp không diễn đạt được ý định đó. Bạn cần một quy tắc lặp lại, một giờ địa phương và một định danh múi giờ.
Để tìm hiểu thêm về các quy tắc này, hãy xem IANA Time Zone Database.
Bài học: một thời điểm cụ thể và một quy tắc về giờ theo lịch địa phương là hai thứ khác nhau.
Thứ tự sự kiện giữa các máy: timestamp không đủ
Đây là lúc mọi thứ trở nên thú vị hơn.
Máy chủ A ghi nhận một sự kiện lúc 12:00:00.100 (12:00:00.100). Máy chủ B ghi nhận một sự kiện khác lúc 12:00:00.080 (12:00:00.080). Sự kiện nào xảy ra trước?
Bạn có thể kết luận sự kiện của máy chủ B xảy ra trước vì timestamp của nó nhỏ hơn. Nhưng nếu đồng hồ của máy chủ B đang chạy nhanh hơn đồng hồ của máy chủ A thì sao?
Đó chính là một trong những vấn đề nền tảng của hệ phân tán: chỉ dựa vào timestamp thì không thể xác lập thứ tự sự kiện giữa các máy một cách đáng tin cậy. Đồng bộ đồng hồ có thể giúp ích, nhưng không thể bảo đảm mọi máy chủ báo cùng một thời điểm chính xác như nhau.
Các hệ phân tán dùng những cơ chế khác để suy luận về thứ tự sự kiện. Tùy bài toán, đó có thể là số thứ tự, đồng hồ logic, vị trí log trong cơ sở dữ liệu hoặc các giao thức phối hợp.
Cũng cần nhớ rằng hai sự kiện có thể cùng một timestamp ngay cả khi cả hai đồng hồ đều hoạt động hoàn hảo: đơn giản là chúng xảy ra trong cùng một độ phân giải của đồng hồ.
Bài học: đừng mặc định timestamp nhỏ nhất luôn thuộc về sự kiện xảy ra sớm nhất.
Dùng timestamp làm khóa chính: lỗi trùng lặp
Hãy hình dung bạn xây dựng một dịch vụ thanh toán và quyết định dùng Unix timestamp hiện tại làm khóa chính cho mỗi khoản thanh toán. Ban đầu mọi thứ có vẻ ổn.
Rồi hai khách hàng thanh toán trong cùng một giây. Cả hai yêu cầu sinh ra cùng một timestamp, cả hai cùng cố chèn một dòng với cùng khóa chính. Một lần chèn thất bại.
Bạn chuyển từ giây sang mili giây. Khả năng trùng giảm, nhưng vẫn có thể xảy ra. Chuyển sang nanosecond cũng không tự động bảo đảm tính duy nhất.
Vấn đề không hẳn là độ chính xác không đủ, mà là bạn đã yêu cầu một đồng hồ cung cấp một bảo đảm mà nó không được thiết kế để đưa ra.
Một thiết kế tốt hơn tách định danh khỏi thời gian:
CREATE TABLE payments ( -- CREATE TABLE payments (
id UUID PRIMARY KEY, -- id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL, -- created_at TIMESTAMPTZ NOT NULL,
amount NUMERIC NOT NULL -- amount NUMERIC NOT NULL
); -- );
Ví dụ kiểu PostgreSQL này dùng UUID để định danh mỗi khoản thanh toán và timestamp để ghi lại thời điểm nó được tạo. Ứng dụng hoặc cơ sở dữ liệu của bạn vẫn phải tự sinh UUID.
Mỗi trường có một trách nhiệm rõ ràng.
Bài học: timestamp mô tả việc gì đó xảy ra khi nào; định danh phân biệt nó với những thứ khác.
Thực hành: kiểm tra đồng hồ trên Linux
Đủ lý thuyết rồi. Mở terminal và chạy các lệnh sau:
# # What time does your system report?
date # date
# # What time is it in UTC?
date -u # date -u
# # What is the current Unix timestamp?
date +%s # date +%s
# # What does the system report about time synchronization?
timedatectl status # timedatectl status
Lệnh cuối cùng dành cho hệ thống Linux dùng systemd. Các lệnh còn lại phổ biến trên các hệ thống giống Unix, tuy tùy chọn có thể khác nhau giữa các bản triển khai.
Nếu đã cài Python 3, hãy chạy:
import time # import time
print("Wall clock:", time.time()) # print("Wall clock:", time.time())
print("Monotonic clock:", time.monotonic()) # print("Monotonic clock:", time.monotonic())
print("Performance counter:", time.perf_counter()) # print("Performance counter:", time.perf_counter())
Ba lời gọi này phục vụ các mục đích khác nhau:
time.time()(time.time()) cho bạn thời gian thực dựa trên kỷ nguyên.time.monotonic()(time.monotonic()) hữu ích để đo thời gian trôi qua.time.perf_counter()(time.perf_counter()) được thiết kế để đo hiệu năng trong khoảng thời gian ngắn.
Kết quả mẫu:
Wall clock: 1792679694.0232277 # Wall clock: 1792679694.0232277
Monotonic clock: 445.774622884 # Monotonic clock: 445.774622884
Performance counter: 445.774647309 # Performance counter: 445.774647309
Đồng hồ đơn điệu và bộ đếm hiệu năng không cung cấp timestamp lịch thông thường. Đừng so sánh giá trị thô của chúng với Unix timestamp.
Để hiểu chi tiết bên dưới của Linux, hãy đọc trang man clock_gettime().
Những gì còn ở phía sau timestamp
Bạn bắt đầu từ một con số đếm giây kể từ năm 1970. Giờ bạn đã gặp đồng hồ không chính xác, đồng bộ thời gian, chuyển đổi giờ mùa hè, thứ tự sự kiện trong hệ phân tán và định danh trong cơ sở dữ liệu.
Điều đáng ngạc nhiên không phải là việc giữ thời gian phức tạp, mà là rất nhiều phần mềm phụ thuộc vào nó trong khi vẫn coi đó là bài toán đã được giải quyết.
Vẫn còn nhiều thứ để khám phá: giây nhuận, vấn đề năm 2038, độ chính xác của timestamp, và những cách tinh tế mà các ngôn ngữ lập trình xử lý ngày tháng trước kỷ nguyên Unix.




