Lỗi Trình Biên Dịch Go Trên ARM64 Gây Ra Sự Cố Treo Máy Bí Ẩn Ở Quy Mô Cloudflare

Nhóm Cộng đồng BigGo
Lỗi Trình Biên Dịch Go Trên ARM64 Gây Ra Sự Cố Treo Máy Bí Ẩn Ở Quy Mô Cloudflare

Trong thế giới của điện toán quy mô lớn, ngay cả những lỗi hiếm gặp nhất cũng trở nên không thể tránh khỏi khi bạn xử lý hàng trăm triệu yêu cầu HTTP. Các kỹ sư tại Cloudflare gần đây đã đối mặt với một vấn đề khó nắm bắt như vậy - những sự cố treo máy bí ẩn xuất hiện ngẫu nhiên trên cơ sở hạ tầng ARM64 của họ. Những gì bắt đầu như những lỗi panic ngẫu nhiên cuối cùng đã lộ ra là một lỗi cơ bản trong trình biên dịch ARM64 của Go, minh chứng cho việc độ phức tạp của phần mềm hiện đại có thể che giấu những vấn đề tinh vi nhưng quan trọng như thế nào.

Bài đăng blog này thảo luận về việc phát hiện một lỗi cơ bản trong trình biên dịch ARM64 của Go, một vấn đề nghiêm trọng mà các kỹ sư Cloudflare gặp phải khi xử lý hàng triệu yêu cầu HTTP
Bài đăng blog này thảo luận về việc phát hiện một lỗi cơ bản trong trình biên dịch ARM64 của Go, một vấn đề nghiêm trọng mà các kỹ sư Cloudflare gặp phải khi xử lý hàng triệu yêu cầu HTTP

Bóng Ma Trong Cỗ Máy

Trong nhiều tuần, các kỹ sư Cloudflare quan sát thấy các lỗi segmentation fault kỳ lạ và lỗi panic gây chết chương trình xảy ra trên các máy chủ ARM64 của họ. Các sự cố treo máy đặc biệt khó hiểu vì chúng dường như hoàn toàn ngẫu nhiên - xuất hiện không theo một mẫu hình rõ ràng nào và chỉ ảnh hưởng đến một tỷ lệ nhỏ trong lượng truy cập khổng lồ chảy qua mạng lưới toàn cầu của Cloudflare. Các cuộc điều tra ban đầu chỉ ra hướng liên quan đến lỗi hỏng bộ nhớ, nhưng nguyên nhân gốc rễ vẫn khó nắm bắt bất chấp những nỗ lực gỡ lỗi kỹ lưỡng.

Vấn đề biểu hiện dưới dạng lỗi segmentation fault trong quá trình thu gom rác (garbage collection) và các thao tác unwinding ngăn xếp. Các kỹ sư nhận thấy rằng những sự cố treo máy này liên tục xảy ra trong quá trình async preemption - cơ chế của Go để ngắt các goroutine chạy lâu nhằm duy trì việc lập lịch trình công bằng. Manh mối này đã trở thành sợi chỉ đầu tiên trong hành trình gỡ lỗi phức tạp sắp tới.

Một điều thường bị bỏ lỡ là việc nghi ngờ trình biên dịch là nguyên nhân gốc rễ khó khăn đến thế nào. Hầu hết các kỹ sư lãng phí hàng giờ để truy tìm lỗi trong chính code của họ bởi vì chúng ta được rèn luyện để tin tưởng vào các công cụ của mình.

Phát Hiện Điều Kiện Race

Bước đột phá đến khi các kỹ sư nhận ra sự cố treo máy đang xảy ra trong một khoảng thời gian rất cụ thể - khi Go runtime thực hiện preempt các goroutine ngay giữa lúc chúng đang điều chỉnh con trỏ ngăn xếp. Trên kiến trúc ARM64, các thao tác điều chỉnh con trỏ ngăn xếp lớn đôi khi được chia thành nhiều lệnh bởi bộ dịch hợp ngữ của Go. Nếu async preemption xảy ra giữa các lệnh bị chia nhỏ này, nó sẽ để lại con trỏ ngăn xếp ở một trạng thái không nhất quán.

Điều này tạo ra một điều kiện race, nơi mà garbage collection sẽ cố gắng unwind các ngăn xếp với các con trỏ không hợp lệ, dẫn đến lỗi segmentation fault. Lỗi này đặc biệt tinh vi vì nó chỉ ảnh hưởng đến các hàm có khung ngăn xếp (stack frame) lớn hơn 4KB, và chỉ trên kiến trúc ARM64, nơi các lệnh có độ dài cố định đôi khi yêu cầu các thao tác phức tạp phải được chia thành nhiều bước.

Cuộc thảo luận trong cộng đồng đã nêu bật cách mà loại lỗi này đại diện cho một vấn đề kinh điển trong lập trình hệ thống. Một số người bình luận lưu ý về những trải nghiệm tương tự với lỗi trình biên dịch trong suốt sự nghiệp của họ, nhấn mạnh rằng quy mô và sự khác biệt về kiến trúc có thể phơi bày những vấn đề vẫn bị ẩn giấu trong hầu hết các môi trường phát triển.

Chi Tiết Kỹ Thuật Chính:

  • Kiến trúc: ARM64 (bộ lệnh có độ dài cố định)
  • Vấn đề: Việc điều chỉnh con trỏ ngăn xếp bị chia thành nhiều lệnh
  • Tác động: Con trỏ ngăn xếp không hợp lệ trong quá trình thu gom rác
  • Giải pháp: Sử dụng thanh ghi tạm thời để cập nhật con trỏ ngăn xếp một cách nguyên tử
  • Phương pháp phát hiện: Phân tích mẫu lỗi ở quy mô lớn (hàng trăm triệu yêu cầu)

Bản Sửa Lỗi Và Những Hệ Quả Của Nó

Các kỹ sư Cloudflare đã phát triển một bản tái hiện lỗi tối giản mà không cần bất kỳ phụ thuộc bên ngoài nào. Điều này cho phép họ xác nhận vấn đề thực sự nằm trong Go runtime chứ không phải trong mã ứng dụng của họ. Bản sửa lỗi liên quan đến việc thay đổi cách trình biên dịch Go xử lý các điều chỉnh ngăn xếp lớn trên ARM64 - đảm bảo rằng các sửa đổi con trỏ ngăn xếp xảy ra một cách nguyên tử trong một lệnh duy nhất thay vì bị chia nhỏ thành nhiều thao tác.

Lỗi này đã nhanh chóng được đội ngũ Go xử lý và sửa trong các phiên bản 1.21.3, 1.20.10 và 1.19.13. Giải pháp ngăn chặn điều kiện race bằng cách sử dụng một thanh ghi tạm thời để xây dựng các giá trị offset lớn, sau đó áp dụng chúng vào con trỏ ngăn xếp trong một thao tác không thể chia cắt. Điều này đảm bảo rằng các goroutine có thể bị preempt trước hoặc sau khi sửa đổi con trỏ ngăn xếp, nhưng không bao giờ xảy ra trong giai đoạn điều chỉnh quan trọng.

Các thành viên cộng đồng đã thảo luận về những hệ quả rộng lớn hơn của các lỗi như vậy, một số lưu ý rằng điều này làm nổi bật tầm quan trọng của việc hiểu biết ngôn ngữ assembly ngay cả trong các môi trường lập trình cấp cao. Những người khác chỉ ra rằng các vấn đề tương tự đã xuất hiện trong suốt lịch sử máy tính, thường liên quan đến các sửa đổi con trỏ ngăn xếp không nguyên tử trên các kiến trúc khác nhau.

Các phiên bản Go bị ảnh hưởng và bản vá:

  • Go 1.19.x: Đã được sửa trong phiên bản 1.19.13
  • Go 1.20.x: Đã được sửa trong phiên bản 1.20.10
  • Go 1.21.x: Đã được sửa trong phiên bản 1.21.3
  • Nguyên nhân gốc rễ: Điều chỉnh con trỏ stack không nguyên tử trên ARM64
  • Điều kiện kích hoạt: Preemption bất đồng bộ giữa các lệnh phân tách cho stack frame lớn hơn 4KB

Bài Học Cho Phát Triển Phần Mềm Hiện Đại

Sự cố này nhấn mạnh một số bài học quan trọng cho các hoạt động phần mềm quy mô lớn. Đầu tiên, nó chứng minh giá trị của các chính sách điều tra sự cố triệt để - Cloudflare yêu cầu điều tra mọi sự cố treo máy sau khi trước đây học được rằng những sự cố treo máo không giải thích được có thể là dấu hiệu cảnh báo sớm của những vấn đề nghiêm trọng. Thứ hai, nó cho thấy sự khác biệt về kiến trúc quan trọng như thế nào - những lỗi không bao giờ xuất hiện trên hệ thống x86 có thể trở thành nghiêm trọng trên các triển khai ARM64.

Quá trình gỡ lỗi cũng làm nổi bật tầm quan trọng của việc có những kỹ sư có thể tư duy xuyên qua nhiều cấp độ trừu tượng, từ mã ứng dụng cấp cao xuống đến bên trong trình biên dịch và kiến trúc bộ xử lý. Như một thành viên cộng đồng đã nhận xét, lỗi trình biên dịch ngày càng trở nên hiếm gặp khi các công cụ được cải thiện, nhưng chúng vẫn xảy ra và đòi hỏi các kỹ thuật điều tra tinh vi.

Khám phá này đóng vai trò như một lời nhắc nhở rằng trong các hệ thống phân tán hoạt động ở quy mô lớn, ngay cả những sự kiện có tỷ lệ một phần triệu cũng xảy ra thường xuyên. Những gì có thể được coi là trường hợp biên (edge case) trong hầu hết các môi trường lại trở thành một sự cố trong sản xuất khi bạn xử lý lưu lượng truy cập ở quy mô internet. Nó cũng cho thấy giá trị của các hệ sinh thái mã nguồn mở, nơi những lỗi như vậy có thể được nhanh chóng xác định, báo cáo và sửa chữa thông qua sự hợp tác giữa các công ty và những người bảo trì ngôn ngữ.

Khi phần mềm tiếp tục phát triển và các kiến trúc mới ngày càng nổi bật, những tương tác tinh vi tương tự giữa trình biên dịch, runtime và phần cứng có khả năng sẽ tiếp tục xuất hiện. Cách tiếp cận có hệ thống của nhóm Cloudflare để gỡ lỗi cung cấp một khuôn mẫu cho việc các tổ chức kỹ thuật có thể giải quyết những vấn đề đầy thách thức như thế nào.

Tham khảo: How we found a bug in Go’s arm64 compiler