Sự Cố Sụp Đổ Do Hàm Select Gây Ra - nói dối e blog

Sự Cố Sụp Đổ Do Hàm Select Gây Ra

Vào ngày 13 tháng 2, kế hoạch bảo trì định kỳ của tựa game Momo đã được lên lịch vào lúc 16:00 chiều. Trước đó, hệ thống đã vận hành ổn định trong thời gian dài. Tuy nhiên, bất ngờ xảy ra vào khoảng 14:30, một máy phụ (standby) bất ngờ sụp đổ. Trước đây, khi máy phụ gặp sự cố, hệ thống vẫn hoạt động bình thường chỉ cần khởi động lại một máy phụ mới. Lần này lại khác biệt - toàn bộ người chơi không thể đăng nhập vào game. Ban điều hành buộc phải tiến hành bảo trì khẩn cấp kéo dài 2 tiếng (kết hợp luôn công việc bảo trì định kỳ) trước thời hạn.

Sự cố nghiêm trọng này bắt nguồn từ việc 5.000 người chơi trên máy phụ gặp sự cố đồng loạt ngắt kết nối, tạo ra làn sóng kết nối dồn dập sang các máy phụ khác. Một lỗi nhỏ khác trong hệ thống khiến trạng thái người dùng không được reset kịp thời, dẫn đến việc họ không thể đăng nhập lại ngay lập tức và liên tục thử lại. Mỗi máy phụ đều có giới hạn kết nối tối đa ở cổng gateway quá thấp, khiến dòng đăng nhập bị tắc nghẽn, chặn đứng người chơi khác.

Sau bảo trì, các vấn đề trên đã được khắc phục. Tuy nhiên do thời gian gấp gáp, nguyên nhân gốc rễ của sự cố sụp đổ chưa được điều tra kỹ lưỡng. Một tình huống phát sinh khác là tệp core dump bị ghi vào phân vùng có dung lượng nhỏ, khiến tệp bị cắt ngắn và gây khó khăn cho việc phân tích. Khi hệ thống khởi động lại, chưa đầy nửa tiếng lại tiếp tục xảy ra sự cố trên một máy phụ khác. Lần này, đội ngũ kỹ thuật nhanh chóng thay thế bằng máy dự phòng, nhưng chỉ 5 phút sau, một sự cố tương tự lại xuất hiện trên máy phụ thứ ba. Điều này cho thấy mức độ nghiêm trọng của vấn đề.

Lần sụp đổ sau cùng cung cấp tệp core dump đầy đủ, cho thấy lỗi xảy ra tại giao diện kết nối giữa máy chủ game và hệ thống nền tảng vận hành (đặt ở hai trung tâm dữ liệu khác nhau). Do trạng thái mạng giữa hai trung tâm không ổn định, quá trình reconnect đã xảy ra lỗi. Tuy nhiên, stack trace chương trình lúc này đã bị hư hỏng một phần. Trong nhật ký lỗi, xuất hiện hàng loạt ghi nhận thất bại khi người chơi nhận phần thưởng. Dù chưa thể khẳng định chắc chắn, nhưng khả năng cao hai vấn đề này có liên quan. Đáng chú ý, mã nguồn C đã không có bất kỳ thay đổi nào trong 2 tháng qua và vận hành ổn định ngay cả trong dịp Tết khi không có kỹ thuật viên trực. Gần đây chỉ có cập nhật logic nghiệp vụ bằng Lua liên quan đến việc phát thưởng.

Khi bữa tối ngày 13 đến gần mà vẫn chưa tìm ra nguyên nhân, tôi quyết định tạm thời vô hiệu hóa chức năng nhận thưởng để giảm nguy cơ sụp đổ vào ban đêm, đồng thời tiếp tục điều tra kỹ lưỡng. Việc stack trace bị phá hủy khiến phân tích từ tệp core trở nên phức tạp. Dựa trên quy trình điều tra thông thường, chúng tôi giả định vấn đề có thể liên quan đến quy trình phát thưởng. Qua phân tích hiện tượng ba lần sụp đổ, chúng tôi nhận thấy khả năng cao một người chơi nào đó đã kích hoạt một nhánh logic cực kỳ hiếm gặp. Sau khi sàng lọc hàng chục nghìn ID người chơi trong 20 phút trước mỗi sự cố, chỉ có duy nhất một người chơi xuất hiện trong cả ba danh sách. Tuy nhiên, về sau xác định đây chỉ là sự trùng hợp ngẫu nhiên.

Ba lần sụp đổ đều xảy ra trong quá trình reconnect giữa máy chủ game và nền tảng vận hành, khiến đoạn mã liên quan trở thành nghi phạm số một. Người viết đoạn code này - kỹ sư Ngô Niên (tên thân mật: “Con Rùa”) - đang đi hưởng tuần trăng mật ở Malaysia, nên chúng tôi không tiện làm phiền. Đêm hôm đó, tôi cùng đồng nghiệp Tô Hiểu Tĩnh mày mò toàn bộ mã对接 nền tảng do nhóm tự viết nhưng không phát hiện điểm bất thường.

Ngày hôm sau, tôi chuyển hướng kiểm tra thư viện C của rabbitmq do Ngô Niên sử dụng, nhưng ban đầu không tìm thấy vấn đề gì. Trong tình trạng bế tắc, chúng tôi quay lại phân tích kỹ lưỡng từng chi tiết trong tệp core. Qua so sánh dữ liệu stack chưa bị hư hỏng với mã biên dịch ngược, phát hiện tại hàm amqp_open_socket_noblock, mảng địa phương portnumber đều chứa giá trị 0, trong khi sockfd lại không phải -1. Điều này cho thấy socket đã được tạo thành công nhưng mảng portnumber bị ghi đè.

Phân tích sâu hơn, hóa ra thư viện rabbitmq-c sử dụng cơ chế connect không đồng bộ (non-blocking connect) và hàm select để kiểm tra khả năng ghi của fd nhằm kiểm soát timeout kết nối. Tuy nhiên, hàm select không tính đến trường hợp sockfd lớn hơn FD_SETSIZE. Theo tài liệu chính thức của Linux:

“Một fd_set là bộ đệm kích thước cố định. Việc thực thi FD_CLR() hoặc FD_SET() với giá trị fd âm hoặc bằng/lớn hơn FD_SETSIZE sẽ dẫn đến hành vi không xác định. Ngoài ra, POSIX yêu cầu fd phải là một file descriptor hợp lệ.”

Đây là một lỗi phổ biến bị nhiều lập trình viên bỏ qua. FD_SET thực chất là một mảng bit với kích thước mặc định 1024 bit trên Linux, đơn giản sử dụng fd như chỉ số để ghi dữ liệu bit. Khi fd ≥ 1024, việc ghi sẽ vượt quá giới hạn bộ đệm. Giải pháp đúng đắn là khi fd ≥ 1024, cần cấp phát FD_SET trên heap với dung lượng tối thiểu fd/8 + 1, hoặc tốt hơn là sử dụng poll/WSAPoll thay thế.

Thông thường, các ứng dụng ít gặp vấn đề này vì số lượng file descriptor không vượt quá 1024. Tuy nhiên, hệ thống của chúng tôi quản lý hàng loạt kết nối bên ngoài, việc vượt quá 1024 là điều dễ dàng xảy ra. Trước đây, mạng giữa hai trung tâm dữ liệu ổn định, kết nối chỉ được tạo duy nhất lúc khởi động chương trình - khi đó chưa có kết nối bên

0%