Cái bẫy Benchmark

Những người quảng bá benchmark không bao giờ cho bạn thấy bức tranh toàn cảnh. Họ đo lường một handler hầu như không làm gì cả. Một chuỗi JSON Hello World. Một chuỗi tĩnh. Trong môi trường chân không đó, Fiber thắng thế rõ rệt. Nó phục vụ nhiều yêu cầu mỗi giây hơn và tiêu thụ ít bộ nhớ hơn Gin. Đó là kỹ thuật thực thụ. Nhưng ứng dụng của bạn không phải là một môi trường chân không.

Tôi đang xây dựng một ứng dụng an ninh thời gian thực tại Kenya. Nó tiếp nhận tọa độ GPS từ người dùng khắp Nairobi và Mombasa và đẩy các cảnh báo về các điểm nóng tội phạm hoặc tai nạn giao thông. Điều đó đồng nghĩa với việc có các lệnh ghi vào PostgreSQL, các lệnh gọi Firebase Cloud Messaging và các truy vấn reverse geocoding. Khi stack của bạn bao gồm các bước nhảy mạng (network hops) và các lượt truy cập cơ sở dữ liệu (database round-trips), một framework tiết kiệm được vài nano giây trong việc định tuyến là điều không đáng kể. Nút thắt cổ chai không bao giờ nằm ở router. Nó nằm ở việc cổng SMS bị hết thời gian chờ (timeout). Nó nằm ở truy vấn PostGIS đang quét một bảng báo cáo sự cố. Thông lượng thô trông có vẻ hấp dẫn cho đến khi nó đối mặt với môi trường production.

Sự trung thành với Thư viện chuẩn

Gin được xây dựng trực tiếp trên net/http. Điều này quan trọng hơn những gì nó thể hiện.

Mọi lập trình viên Go đều biết net/http. Việc gỡ lỗi tuân theo các stack trace quen thuộc. Các middleware được viết từ năm năm trước vẫn có thể đưa vào sử dụng ngay lập tức mà không cần thủ tục rườm rà. Các đối tượng request và response hoạt động chính xác như tài liệu của Go mô tả. Gin duy trì được tính dự đoán được vì nó bám sát chính ngôn ngữ này.

Fiber thay thế nền tảng đó bằng fasthttp, một engine HTTP tùy chỉnh được tối ưu hóa cho hiệu suất zero-allocation. Để đạt được điều đó, nó sử dụng Request/Response Pooling. Thay vì để garbage collector thu hồi bộ nhớ sau mỗi request, Fiber tái sử dụng các khối bộ nhớ. Nó xóa sạch chúng và chuyển cho kết nối tiếp theo. Thủ thuật đó là nguồn gốc của tốc độ. Nhưng nó cũng là nguồn gốc của sự nguy hiểm.

Nguy hiểm tiềm ẩn từ việc tái sử dụng bộ nhớ

Pooling nghe có vẻ an toàn về mặt lý thuyết. Trong thực tế, nó đưa ra một quy ước ngầm: bạn không bao giờ được giữ tham chiếu đến dữ liệu request sau khi handler của bạn kết thúc. Nếu một goroutine tồn tại lâu hơn request, hoặc nếu bạn nắm giữ một slice của request body để xử lý sau, Fiber sẽ thu hồi bộ nhớ đó và chuyển cho người dùng tiếp theo.

Kết quả là sự nhiễm bẩn dữ liệu. Lỗi này không xuất hiện trong unit test. Nó xuất hiện dưới dạng tọa độ GPS của một tài xế ở Nairobi đột nhiên xuất hiện trên bản đồ ở Kisumu. Nó xuất hiện khi một JWT token từ người dùng này bị rò rỉ vào context của người dùng khác. Đây là những lỗi mang tính thời điểm. Chúng biến mất khi bạn thêm logging vì hành động logging sẽ cấp phát bộ nhớ mới và làm thay đổi thời điểm thực thi. Cuối cùng, bạn sẽ phải đi săn đuổi những "bóng ma".

Với Gin, thư viện chuẩn sẽ cấp phát một đối tượng request mới. Không có pool nào để bị "nhiễm độc". Sự an toàn đó rất quan trọng khi ứng dụng của bạn xử lý các vị trí thực tế gắn liền với sự an toàn thực sự.

Sức hút của Hệ sinh thái

Ngoài ra còn có "chi phí ẩn" về tính tương thích.

Hầu hết các middleware của Go đều giả định sử dụng net/http. Các trình xác thực JWT, bộ xuất OpenTelemetry, trình xử lý CORS và bộ giới hạn tốc độ (rate limiter) đều sử dụng giao diện chuẩn. Gin hiểu giao diện đó một cách tự nhiên. Chỉ cần đưa một middleware vào là nó hoạt động.

Fiber có kiểu context và chữ ký handler riêng. Bạn cần các bộ chuyển đổi (adapters). Đôi khi bộ chuyển đổi là chính thức. Đôi khi nó chậm hơn thư viện gốc cả năm trời. Đôi khi nó hoạt động hơi khác một chút dưới tải nặng. Khi bạn đang điều hành một đội ngũ nhỏ, bạn không có thời gian rảnh để gỡ lỗi tại sao một middleware xác thực hoạt động trên laptop của bạn nhưng lại từ chối các token hợp lệ trên máy chủ. Bạn muốn go get phải thật "nhàm chán". Gin giữ cho mọi thứ nhàm chán, và đó chính xác là những gì bạn muốn vào lúc 3 giờ sáng.

Những gì ứng dụng thực sự cần

Dịch vụ an ninh của tôi xử lý các tín hiệu vị trí (location pings) sau mỗi vài giây từ những người dùng đồng thời. Nó thiết lập hàng rào địa lý (geofencing) cho các con đường có rủi ro cao. Nó kích hoạt các thông báo. Một cảnh báo chậm trễ gây khó chịu. Một cảnh báo sai lệch thì cực kỳ nguy hiểm. Việc gửi ai đó đến hiện trường một vụ tai nạn đang diễn ra chỉ vì bộ nhớ pool bị lỗi là điều không thể chấp nhận được.

Ở đây, độ tin cậy quan trọng hơn thông lượng. Tôi cần các stack trace có ý nghĩa khi tôi gắn pprof. Tôi cần các hồ sơ bộ nhớ (memory profiles) mà không yêu cầu tôi phải hiểu vòng đời nội bộ của một memory pool tùy chỉnh. Tôi cần lập trình viên tiếp theo kế thừa dự án này có thể bắt nhịp công việc chỉ trong một buổi chiều mà không cần phải học mô hình đối tượng của fasthttp. Gin mang lại cho tôi điều đó. Sự ổn định trong vận hành không được phản ánh qua các bản benchmark, nhưng đó là thứ giữ cho một dịch vụ