Series Go Backend: Phân tích Garbage Collector (GC) trong Go hoạt động như thế nào
Trong các ngôn ngữ cấp thấp như C/C++, lập trình viên phải tự xin cấp phát bộ nhớ (malloc) và tự tay giải phóng nó (free). Nếu quên giải phóng, hệ thống sẽ bị rò rỉ bộ nhớ (Memory Leak). Để giải phóng gánh nặng này, Go tích hợp sẵn một hệ thống quản lý bộ nhớ tự động gọi là Garbage Collector (GC).
Với Backend Engineer, hiểu cách GC của Go hoạt động là chìa khóa để viết ra các API có độ trễ cực thấp (Low Latency) và tối ưu hóa tài nguyên server.
1. Stack, Heap và hiện tượng "Heap Escape"
Trước khi hiểu GC dọn rác ở đâu, ta cần biết Go lưu trữ biến như thế nào. Bộ nhớ ứng dụng chia làm hai vùng chính:
-
Stack: Vùng nhớ cực kỳ nhanh, dùng để lưu trữ các biến cục bộ bên trong hàm. Khi hàm chạy xong, vùng nhớ Stack lập tức được hệ điều hành dọn sạch mà không cần GC can thiệp.
-
Heap: Vùng nhớ dùng để lưu trữ các biến có vòng đời sống lâu hơn hàm tạo ra nó (ví dụ: một biến được khởi tạo trong hàm, nhưng hàm lại return một con trỏ chỉ đến biến đó ra ngoài).
Khi trình biên dịch của Go phát hiện một biến cục bộ "thoát" ra khỏi phạm vi hàm, nó sẽ tự động đẩy biến đó từ Stack lên Heap. Hiện tượng này gọi là Heap Escape.
GC của Go CHỈ hoạt động trên vùng nhớ Heap. Số lượng dữ liệu đẩy lên Heap càng nhiều, GC càng phải chạy vất vả, dẫn đến tốn CPU và làm chậm ứng dụng.
2. Thuật toán của Go GC: Concurrent Mark and Sweep
Go sử dụng thuật toán Đánh dấu và Dọn dẹp đồng thời (Concurrent Mark and Sweep), kết hợp với mô hình Tricolor (3 màu). Khác với Java thường có khuynh hướng gom rác theo thế hệ (Generational GC), Go chọn hướng đi đơn giản nhưng tối ưu hóa tối đa thời gian tạm dừng ứng dụng.
Quá trình GC diễn ra theo 3 giai đoạn chính dựa trên thuật toán Tricolor:
Bước 1: Phân loại 3 màu (Trắng, Xám, Đen)
-
Màu Trắng: Đối tượng nghi ngờ là rác (chưa được kiểm tra tới). Ban đầu, tất cả bộ nhớ trên Heap đều có màu trắng.
-
Màu Xám: Đối tượng còn đang được sử dụng, nhưng các con trỏ bên trong nó thì chưa được kiểm tra.
-
Màu Đen: Đối tượng còn đang sử dụng, và toàn bộ con trỏ bên trong nó cũng đã được quét qua. Chắc chắn giữ lại.
Bước 2: Quá trình Mark (Đánh dấu)
-
GC bắt đầu quét từ các Root (Biến toàn cục, biến đang nằm trên Stack). Các Root này được tô màu Xám.
-
GC nhặt một đối tượng Xám, kiểm tra các con trỏ mà nó chứa.
-
Tô Xám các đối tượng bị trỏ tới, và chuyển đối tượng gốc thành Đen.
-
Lặp lại quá trình này cho đến khi không còn đối tượng màu Xám nào.
Bước 3: Quá trình Sweep (Dọn dẹp) Khi không còn màu Xám, trên Heap lúc này chỉ còn hai màu: Đen (Đang dùng) và Trắng (Không ai dùng tới nữa, tức là Rác). GC sẽ tiến hành giải phóng toàn bộ vùng nhớ của các đối tượng màu Trắng trả lại cho hệ điều hành.
3. Nỗi ám ảnh Stop-The-World (STW)
Trong quá trình Đánh dấu (Mark), nếu ứng dụng vẫn tiếp tục chạy và liên tục thay đổi con trỏ, thuật toán Tricolor sẽ bị sai lệch. Do đó, GC bắt buộc phải tạm dừng toàn bộ chương trình trong một khoảnh khắc ngắn. Khoảnh khắc này gọi là Stop-The-World (STW).
-
Ở các ngôn ngữ khác, STW có thể kéo dài hàng chục, thậm chí hàng trăm mili-giây, gây ra hiện tượng API bị "đơ" (Latency spike).
-
Sức mạnh của Go: Go GC chạy đồng thời (Concurrent) cùng với code của bạn. Thời gian STW trong Go được tối ưu hóa cực kỳ khủng khiếp, thường dưới 1 mili-giây (sub-millisecond) kể cả với các Heap khổng lồ hàng chục GB. Đây là lý do Go cực kỳ được ưa chuộng để làm Microservices.
4. Kiểm soát GC với biến môi trường GOGC
Bên cạnh việc viết code tối ưu (tránh truyền con trỏ bừa bãi, tái sử dụng bộ nhớ với sync.Pool), bạn có thể trực tiếp can thiệp vào hành vi của GC thông qua biến môi trường GOGC.
GOGC xác định tỷ lệ phần trăm Heap tăng lên trước khi kích hoạt một vòng quét rác mới. Mặc định GOGC = 100.
-
GOGC=100(Mặc định): Nếu lần dọn rác trước Heap còn lại 10MB, thì vòng dọn rác tiếp theo sẽ kích hoạt khi Heap phình lên 20MB (+100%). -
Tăng
GOGC(VD:GOGC=200): GC sẽ lười chạy hơn (Heap phải tăng 200% mới chạy). Ứng dụng sẽ tốn nhiều RAM hơn nhưng CPU rảnh rỗi hơn để xử lý Request. -
Tắt luôn GC (
GOGC=off): Dùng trong các kịch bản chạy script siêu nhanh rồi tự tắt, không cần tốn công dọn rác.
Từ phiên bản Go 1.19, Go còn cung cấp thêm cấu hình GOMEMLIMIT để giới hạn mức RAM cứng tối đa (Soft memory limit) mà ứng dụng được phép ăn trước khi GC buộc phải dọn dẹp cấp tốc, giúp ứng dụng sống sót cực tốt trong môi trường container (Docker/Kubernetes).
Tổng kết Phần 1: Khép lại 10 bài đầu tiên, bạn đã nắm vững toàn bộ nền tảng cốt lõi của Golang từ biến, điều khiển luồng, cấp phát bộ nhớ, hàm, con trỏ cho đến cơ chế dọn rác. Đây là bệ phóng vững chắc để tiến vào các khái niệm thiết kế phần mềm.
All rights reserved