Một nhà phát triển đã tạo ra một công cụ debug sáng tạo sử dụng eBPF ( Extended Berkeley Packet Filter ) để theo dõi việc phân bổ bộ nhớ trong các chương trình Go theo loại dữ liệu. Điều này giải quyết một khoảng trống đáng kể trong các công cụ profiling hiện có của Go , vốn có thể hiển thị nơi xảy ra phân bổ nhưng không thể biết loại dữ liệu nào đang được phân bổ.
Vấn đề với Profiling tích hợp sẵn của Go
Các công cụ profiling tiêu chuẩn của Go cung cấp những hiểu biết có giá trị về vị trí phân bổ bộ nhớ nhưng lại thiếu sót khi các nhà phát triển cần hiểu loại dữ liệu nào tiêu thụ nhiều bộ nhớ nhất. Hạn chế này trở nên đặc biệt có vấn đề khi các loại cụ thể gây ra phân bổ nặng trên nhiều vị trí mã, khiến việc xác định nguyên nhân gốc rễ của áp lực bộ nhớ trở nên khó khăn.
Cộng đồng đã từ lâu gặp khó khăn với các vấn đề phân bổ chuỗi trong Go . Một nhà phát triển lưu ý rằng việc tránh phân bổ heap cho chuỗi là cực kỳ khó khăn, đặc biệt khi sử dụng các hàm định dạng, vì các chuỗi được truyền cho các hàm fmt thường thoát ra heap do các hạn chế xử lý interface{}.
Triển khai kỹ thuật sử dụng eBPF
Giải pháp bao gồm việc gắn một uprobe eBPF vào hàm mallocgc
nội bộ của Go , hàm này xử lý tất cả các phân bổ heap. Bằng cách chặn lời gọi hàm này, công cụ thu thập cả thông tin kích thước phân bổ và loại từ các thanh ghi CPU. Thách thức nằm ở việc giải mã biểu diễn loại nội bộ của Go , vốn sử dụng các offset số nguyên thay vì tên chuỗi trực tiếp.
Việc triển khai đòi hỏi phải phân tích các phần thực thi ELF và tái tạo logic giải quyết loại nội bộ của Go để chuyển đổi các offset này trở lại thành tên loại có thể đọc được. Quá trình này bao gồm việc điều hướng qua các danh sách liên kết của dữ liệu module được lưu trữ trong các phần hằng số của tệp thực thi.
Cấu trúc kiểu Go (abi.Type):
- Size: Kích thước bộ nhớ của kiểu dữ liệu
- PtrBytes: Số byte có thể chứa con trỏ
- Hash: Mã băm kiểu dữ liệu để tối ưu hóa bảng băm
- TFlag: Cờ thông tin kiểu dữ liệu bổ sung
- Align: Yêu cầu căn chỉnh biến
- FieldAlign: Căn chỉnh trường struct
- Kind: Liệt kê kiểu dữ liệu cho khả năng tương tác C
- Equal: Hàm so sánh đối tượng
- GCData: Dữ liệu kiểu dữ liệu cho bộ thu gom rác
- Str: Offset tên (NameOff) - offset số nguyên đến tên kiểu dữ liệu
- PtrToThis: Tham chiếu kiểu con trỏ
Khám phá các nguồn phân bổ ẩn
Công cụ này tiết lộ rằng nhiều phân bổ xảy ra với con trỏ loại null khi dữ liệu được phân bổ không chứa con trỏ. Để nắm bắt các phân bổ vô hình này, các probe bổ sung đã được gắn vào các hàm runtime như makechan
, makeslicecopy
, và growslice
, với các định danh loại tùy chỉnh được gán để theo dõi các nguồn phân bổ khác nhau.
Các Hàm Runtime Go Chính Được Theo Dõi:
runtime.mallocgc
- Hàm cấp phát heap chínhruntime.makechan
- Tạo channelruntime.makeslicecopy
- Sao chép sliceruntime.growslice
- Mở rộng sliceruntime.slicebytetostring
- Chuyển đổi chuỗiruntime.rawbyteslice
- Cấp phát raw byte slice
Hiểu biết hiệu suất thực tế
Cuộc điều tra xác nhận những nghi ngờ về các mẫu mã có vấn đề, đặc biệt là các hàm trả về con trỏ tới chuỗi thay vì trả về chuỗi trực tiếp. Một antipattern phổ biến xuất hiện khi các phương thức trả về *string
để xử lý các trường hợp nil, gây ra phân bổ heap không cần thiết và thêm gián tiếp con trỏ.
Vấn đề lớn nhất là bất kỳ chuỗi nào bạn truyền làm đối số cho các hàm fmt đều được chuyển lên heap vì interface{} luôn được tính là thoát khỏi stack.
Cải tiến tương lai và các phương án thay thế
Mặc dù phương pháp eBPF này cung cấp những hiểu biết tức thì, cộng đồng Go nhận ra nhu cầu về các giải pháp tích hợp tốt hơn. Các cuộc thảo luận đang diễn ra về việc thêm thông tin loại vào hồ sơ phân bổ thông qua nhãn pprof, mặc dù vẫn còn những thách thức xung quanh việc triển khai điều này mà không gây rò rỉ bộ nhớ trong các hồ sơ chạy dài.
Bản phát hành Go 1.25 sắp tới bao gồm các cải tiến cho việc xử lý chuỗi sẽ giảm một số áp lực phân bổ, giải quyết các vấn đề lâu dài với phân tích escape cho các tham số interface{}.
Công cụ này chứng minh cả sức mạnh của eBPF cho việc kiểm tra runtime và sự phát triển liên tục của hệ sinh thái công cụ hiệu suất Go . Mặc dù thừa nhận là dễ vỡ, nó cung cấp những hiểu biết có giá trị có thể hướng dẫn các nỗ lực tối ưu hóa trong các ứng dụng nhạy cảm với bộ nhớ.
Tham khảo: Go allocation probe