VietTechiesHIỂU CÔNG NGHỆ. MỞ GÓC NHÌN.
☰ Chuyên mục

Giây nhuận năm 2017 khiến hệ thống DNS của Cloudflare đo thời lượng thành số âm, gây lỗi tra cứu tên miền cho một số trang web

23/10/2026 · VietTechies · Biên tập

Bộ chuyển mạch mạng Ethernet với các dây cáp nối nhiều màu được cắm vào, minh họa hạ tầng mạng


Đúng nửa đêm ngày 1/1/2017 theo giờ phối hợp quốc tế (UTC), các cơ quan phụ trách giờ chuẩn thêm một giây nhuận để giữ UTC đồng bộ với tốc độ quay đang chậm lại của Trái Đất. Trên những server chịu ảnh hưởng, hệ điều hành lùi đồng hồ một giây. Trong RRDNS, phần mềm DNS có thẩm quyền do Cloudflare tự phát triển, bước lùi một giây ấy khiến phép đo khoảng thời gian đã trôi qua cho ra số âm. DNS (Domain Name System, hệ thống tên miền) chuyển tên miền thành địa chỉ mà máy tính dùng để kết nối tới server. Vài phút sau, giá trị âm đi vào các hàm chọn server, gây ra panic — lỗi làm gián đoạn luồng thực thi — và khiến một số trang web do Cloudflare quản lý gặp lỗi phân giải DNS.

Sự cố không làm sập toàn bộ Internet, cũng không vô hiệu hóa mọi hệ thống của Cloudflare. Theo báo cáo phân tích sự cố của Cloudflare do John Graham-Cumming công bố, lỗi này chỉ ảnh hưởng đến các lượt tra cứu gửi tới những server DNS khác cho tên miền khách hàng dùng bản ghi tên chính tắc (CNAME), loại bản ghi trỏ một tên miền sang tên miền khác. Trên 102 trung tâm dữ liệu của Cloudflare lúc đó, khoảng 0,2% truy vấn DNS bị ảnh hưởng ở thời điểm cao nhất. Chưa đến 1% yêu cầu HTTP (Hypertext Transfer Protocol, giao thức truyền siêu văn bản), dùng để lấy nội dung web, gặp lỗi. Dù vậy, sự cố này bộc lộ một sai sót căn bản trong cách các hệ thống phần mềm đo khoảng thời gian: lấy các mốc giờ trên đồng hồ hệ thống để tính thời lượng.

Khi phép đo thời gian đi ngược

Để phân giải các bản ghi CNAME trỏ tới máy chủ gốc bên ngoài, RRDNS gửi truy vấn tới các resolver DNS đệ quy nội bộ, tức các bộ phân giải chạy trong từng trung tâm dữ liệu. Nhằm tối ưu định tuyến, RRDNS theo dõi thời gian khứ hồi của từng resolver và dùng thuật toán lựa chọn có trọng số để chuyển truy vấn đến những server phản hồi nhanh nhất. Cách đo khá đơn giản: trước khi gửi truy vấn qua UDP (User Datagram Protocol, giao thức gói dữ liệu người dùng), chương trình ghi lại mốc bắt đầu bằng thư viện chuẩn của Go. Khi nhận phản hồi, chương trình tính hiệu hai mốc thời gian: rtt := time.Now().Sub(start).

Trong điều kiện bình thường, các resolver nội bộ phản hồi trong vài mili giây, nên thời lượng đo được là một số dương nhỏ. Nhưng khi hệ điều hành trên máy chủ lùi đồng hồ một giây để xử lý giây nhuận, time.Now() trả về một mốc thời gian sớm hơn start. Phép trừ cho kết quả âm, khoảng −990 mili giây. RRDNS không dựa vào một kết quả đo riêng lẻ mà duy trì giá trị trung bình đã làm mượt để đánh giá hiệu năng của resolver. Khi các thời lượng âm được đưa vào chỉ số hiệu năng đã làm mượt này, cuối cùng chính chỉ số ấy cũng giảm xuống dưới 0.

Từ trung bình đã làm mượt đến panic khi chọn số ngẫu nhiên

Lỗi chưa bộc lộ cho đến khi RRDNS chạy thuật toán chọn server có trọng số. Để chọn bộ phân giải nhận truy vấn theo tỷ lệ tương ứng với trọng số hiệu năng, đoạn code lựa chọn đưa các giá trị đã tính vào bộ sinh số giả ngẫu nhiên của Go qua lời gọi rand.Int63n(). Theo đặc tả, rand.Int63n() lập tức gây panic nếu đối số nhỏ hơn hoặc bằng 0.

Vì chỉ số hiệu năng đã làm mượt trở thành số âm, hàm này gây panic. Các kỹ sư Cloudflare đã bọc các hàm xử lý yêu cầu bằng cơ chế recover() của Go để bắt panic lúc chạy và tránh cho tiến trình server dừng đột ngột. Tuy nhiên, khi bắt được panic, quá trình phân giải cho yêu cầu đó cũng bị hủy. Máy khách nhận lỗi tra cứu DNS. Khi ngày càng nhiều resolver tích lũy trọng số âm, các lượt tra cứu liên tục thất bại tại những trung tâm dữ liệu chịu ảnh hưởng.

Khoanh vùng sự cố và bản vá tức thời

Đội kỹ thuật Cloudflare nhận cảnh báo tỷ lệ lỗi tăng lúc 00:10 UTC ngày 1/1, mười phút sau khi đồng hồ được điều chỉnh. Đến 00:34 UTC, các kỹ sư xác nhận RRDNS gặp lỗi trong đoạn code chọn server có trọng số. Vì đồng hồ đã ngừng chạy lùi, ưu tiên vận hành trước mắt là ngăn trọng số âm đi vào rand.Int63n().

Lúc 00:55 UTC, các kỹ sư triển khai biện pháp giảm thiểu trên một nút canary, tức nút dùng để thử trước khi triển khai rộng. Bản vá kiểm tra xem thời lượng đo được có âm hay không. Nếu phát hiện thời gian chạy lùi, code bỏ kết quả đo hiện tại, chặn giá trị âm đi vào trọng số của server nhận truy vấn và để các giá trị trung bình đã làm mượt trở lại bình thường. Đến 01:23 UTC — khoảng 90 phút sau khi sự cố bắt đầu — bản sửa đã được áp dụng tại trung tâm dữ liệu chịu ảnh hưởng nặng nhất, rồi được triển khai trên toàn bộ trung tâm dữ liệu vào 06:45 UTC. Khởi động lại các instance RRDNS đã xóa trạng thái sai trong bộ nhớ và khôi phục hoạt động DNS bình thường.

Sửa cách dùng đồng hồ ngay trong ngôn ngữ

Sự cố Cloudflare phơi bày một ngộ nhận phổ biến trong giới kỹ sư phần mềm: cho rằng đọc đồng hồ hệ thống hai lần rồi lấy hiệu là cách an toàn để đo khoảng thời gian đã trôi qua. Khi đó, time.Now() của Go chỉ trả về giờ theo đồng hồ hệ thống, vốn có thể được điều chỉnh bởi NTP (Network Time Protocol, giao thức đồng bộ đồng hồ máy tính) và giây nhuận. Từ lâu, nhân hệ điều hành đã duy trì những đồng hồ đơn điệu (monotonic clock) riêng — các bộ đếm chỉ tiến về phía trước với tốc độ ổn định — chuyên dùng để đo khoảng thời gian. Nhưng Go chưa cung cấp thời gian đơn điệu qua các hàm đo thời lượng chính của ngôn ngữ.

Đề xuất thiết kế của Russ Cox cho issue 12914 của Go, cập nhật lần cuối ngày 26/1/2017, giải quyết vấn đề qua gói time của Go. Cox lưu ý rằng đồng hồ hệ thống dùng để cho biết ngày giờ, còn đồng hồ đơn điệu dùng để đo khoảng thời gian đã trôi qua. Thay vì tạo một giao diện lập trình (API) riêng mà lập trình viên phải nhớ dùng, nhóm Go chọn đưa thêm số đọc từ đồng hồ đơn điệu vào ngay cấu trúc time.Time hiện có; thành phần này có thể có hoặc không.

Khi phát hành ngày 24/8/2017, Go 1.9 đã hỗ trợ thời gian đơn điệu mà lập trình viên không cần đổi cách gọi hàm. Theo cách triển khai mới, lời gọi time.Now() ghi nhận cả giờ hiện tại trên đồng hồ hệ thống lẫn số đọc từ đồng hồ đơn điệu. Khi lấy hiệu hai giá trị time.Time có chứa số đọc đơn điệu bằng t.Sub(u) hoặc time.Since(t), Go tự động tính thời lượng bằng bộ đếm đơn điệu, giúp phép đo khoảng thời gian không bị ảnh hưởng khi đồng hồ hệ thống dịch chuyển. Tài liệu thư viện chuẩn lưu ý rằng nếu một mốc thời gian được tuần tự hóa, được phân tích từ chuỗi hoặc bị loại bỏ số đọc đơn điệu, phép trừ sẽ quay về dùng đồng hồ hệ thống.

Sự cố giây nhuận năm 2017 cho thấy một khác biệt nhỏ giữa ngày giờ và khoảng thời gian thực sự đã trôi qua có thể đi qua phép làm mượt toán học rồi dẫn đến lỗi vận hành. Go 1.9 đưa sự phân biệt ấy vào kiểu dữ liệu thời gian sẵn có: mốc ngày giờ cho phần mềm biết sự kiện xảy ra khi nào; số đọc đơn điệu đo sự kiện kéo dài bao lâu.

Bài liên quan

Đọc bằng ngôn ngữ khác: English

Nguồn