I. Giới thiệu
Mình từng tự hỏi "Mình viết logic thì cần đếch gì biết đến garbage collection hoạt động như nào?". Nhưng thực tế thì biết về Go garbage collection sẽ giúp bạn:
- Biết điểm mạnh, điểm yếu của ngôn ngữ này, từ đó có thể cân nhắc xem liệu Golang có phù hợp cho ứng dụng bạn sắp build không?
- Thiết kế hệ thống phù hợp hơn với đặc điểm của Go.
- Tối ưu hiệu suất của ứng dụng hiệu quả hơn.
- Biết cách xử lý cho các thách thức tiềm ẩn liên quan đến quản lý bộ nhớ.
II. Garbage collection là gì?
Garbage Collection (GC) là một cơ chế trong quản lý bộ nhớ, tự động giải phóng tài nguyên mà chương trình không còn sử dụng. Mục tiêu là giảm thiểu memory leaks (rò rỉ bộ nhớ) và đảm bảo rằng bộ nhớ được sử dụng hiệu quả.
Ở các ngôn ngữ bậc thấp C, C++, Rust thì không có cơ chế tự động dọn rác, lập trình viên sẽ phải tự thực hiện cơ chế thủ công. Điều này sẽ có lợi với những "chuyên gia" code, họ có thể quản lý sâu hơn, tối ưu riêng cho tác vụ của mình nhưng sẽ khó với những người lập trình viên khác.
Golang được thiết kế với việc tận dụng khả năng xử lý nhanh gần như C, C++ nhưng đơn giản dễ học, nên nó được tích hợp sẵn GC giúp lập trình viên chỉ cần tập chung vào logic ứng dụng thay vì phải quản lý thủ công.
III. Cơ chế hoạt động
GC đã được tích hợp vào golang từ đầu thời kì phát triển. Tuy nhiên, một bước cải tiến lớn diễn ra với Go 1.5 (phát hành vào năm 2015) khi Go chuyển từ một hệ thống GC "stop-the-world" sang "concurrent garbage collection," giúp giảm thời gian dừng ứng dụng trong quá trình dọn rác. Góp một phần lớn giúp golang được sử dụng rộng rãi hơn.
1. Cơ chế concurrent
Stop-the-world (STW): Tạm dừng toàn bộ chương trình trong thời gian ngắn để thực hiện một số tác vụ. Concurrent: Chương trình dọn rác được thực hiện đồng thời với ứng dụng, giúp giảm sự gián đoạn của ứng dụng.
Tưởng tượng một nhà hàng đang phục vụ rất đông khách, sau một thời gian có vài bàn đã ăn xong cần dọn dẹp. Cách thức của STW sẽ là đóng cửa hàng, ngừng phục vụ khách, huy động toàn bộ nhân viên đi dọn dẹp. Còn với concurrent thì sẽ có một đội dọn dẹp liên tục mà không ảnh hưởng đến việc chính.
2. Mark, sweep, compact
Có khá nhiều thuật toán được lựa chọn và sử dụng trong GC, nhưng để nói nổi bật nhất thì đó là cơ chế "mark, sweep, compact":
- Mark: Xác định và đánh dấu các đối tượng còn đang được sử dụng
- Sweep: Giải phóng bộ nhớ của các đối tượng không được đánh dấu
- Compact (tùy chọn): Sắp xếp lại bộ nhớ để giảm phân mảnh
3. So sánh golang vs .NET core
Muốn biết hiệu suất của GoGC như nào thì phải đem nó đi đo lường và so sánh với một thằng khác mới đánh giá khách quan được. Có 1 bài viết khá hay trên medium mà người tác giả đã tiến hành đo hiệu suát của go vs .NET core. Mình đã tóm tắt bài viết đó thành bảng so sánh như sau cho bạn thấy được tổng quan (mình để link bài viết ở phần tham khảo nếu bạn cần coi chi tiết).
Tiêu chí | Go | .NET Core |
---|---|---|
Tốc độ cấp phát bộ nhớ | Chậm hơn | Nhanh hơn 3-12x |
Throughput cấp phát bền vững | Chậm hơn 20-50% | Nhanh hơn |
Thời gian tạm dừng GC (STW) | Rất ngắn, hầu hết < 1ms | Dài, có thể lên đến vài giây hoặc phút |
Tỷ lệ STW với kích thước bộ nhớ | Không tỷ lệ thuận rõ rệt | O(alive_object_count) |
Pause time tối đa (trên bộ nhớ ~200GB) | ~1.3 giây | ~125 giây |
Xử lý bộ nhớ lớn | Có thể gặp OOM ở 50-75% RAM | Xử lý được nhưng có pause time dài |
Hiệu suất trên Windows | Chậm hơn so với Linux | Nhanh hơn so với Linux |
Khả năng mở rộng trên nhiều core | Không tốt | Không tốt |
GC modes | Chỉ có một mode chính | Nhiều mode (Server, Workstation, Batch, Concurrent) |
Hiệu suất với bộ nhớ tĩnh nhỏ | Tốt | Tốt |
Hiệu suất với bộ nhớ tĩnh lớn | Tốt về pause time, có thể gặp OOM | Tốt về throughput, pause time dài |
Phù hợp cho ứng dụng yêu cầu độ trễ thấp | Rất tốt | Kém hơn với bộ nhớ lớn |
Phù hợp cho ứng dụng cần throughput cao | Tốt | Rất tốt |
Xử lý bộ nhớ rò rỉ | Có thể dẫn đến OOM nhanh chóng | Có thể chạy lâu hơn nhưng với pause time tăng dần |
Qua đó có thể rút ra một số kết luận về GoGC như sau:
Ưu điểm
- Thời gian tạm dừng ngắn: GoGC có thời gian tạm dừng (pause time) rất ngắn, hầu hết dưới 1ms, ngay cả với bộ nhớ lớn.
- Độ trễ thấp: Phù hợp cho các ứng dụng yêu cầu độ trễ thấp như IoT, robotics, game servers, và high-frequency trading.
- Khả năng mở rộng tốt: GoGC hoạt động hiệu quả trên các hệ thống có bộ nhớ lớn mà không gây ra pause time dài.
- GC stream: GoGC hoạt động song song với chương trình chính, giảm thiểu ảnh hưởng đến hiệu suất tổng thể.
- Đơn giản hóa: Chỉ có một chế độ GC chính, đơn giản hóa việc cấu hình và tối ưu hóa.
- Cải tiến liên tục: Go team đã liên tục cải thiện GC từ năm 2014, loại bỏ hầu hết các pause O(alive_set_size).
Nhược điểm:
- Tốc độ cấp phát bộ nhớ chậm hơn: GC của Go tập trung vào việc giảm độ trễ (latency) hơn là tăng thông lượng (throughput), điều này có thể ảnh hưởng đến việc duy trì FPS ổn định ở mức cực cao. Nếu bạn muốn làm một game AAA với fps cực cao như black myth wukong thì khả năng nó không phù hợp đâu.
- Vấn đề Out of Memory (OOM): Trong các trường hợp bộ nhớ lớn (50-75% RAM), GoGC có thể gặp vấn đề OOM nếu không thể theo kịp tốc độ cấp phát.
- Hiệu suất kém trên Windows: GoGC có hiệu suất kém hơn trên Windows so với trên Linux.
- Sử dụng nhiều bộ nhớ hơn: Để tránh OOM, có thể cần cung cấp nhiều bộ nhớ hơn cho ứng dụng Go so với các ngôn ngữ khác.
- Khả năng mở rộng hạn chế trên nhiều core: Throughput cấp phát bền vững không tăng tuyến tính với số lượng core.
- Có thể không phù hợp cho một số ứng dụng đặc biệt: GoGC có ít tùy chọn cấu hình hơn, không cho kiểm soát bộ nhớ 1 cách chi tiết có thể hạn chế khả năng tối ưu hóa cho các ứng dụng yêu cầu kiểm soát bộ nhớ chi tiết. (Đây là một trong những lý do mà go bị ghét bới các lão làng Rust, C, C++)
4. Dự án nào nên dùng Go?
Ứng dụng phù hợp:
-
Web services và API backends: Hiệu suất cao, xử lý đồng thời tốt, thời gian phát triển nhanh (Ví dụ: Docker, Kubernetes, Dropbox)
-
Microservices: Nhẹ, khởi động nhanh, quản lý dependencies tốt (Ví dụ: Google Cloud, Netflix)
-
Ứng dụng CLI: Biên dịch nhanh, tạo ra binary độc lập, hiệu suất tốt (Ví dụ: Hugo, Docker CLI)
-
Công cụ DevOps và tự động hóa: Cross-platform, xử lý concurrent tốt, tích hợp dễ dàng (Ví dụ: Terraform, Prometheus)
-
Ứng dụng mạng và distributed systems: Hỗ trợ concurrency mạnh mẽ, thư viện mạng tốt (Ví dụ: Ethereum, IPFS)
-
Xử lý dữ liệu quy mô vừa: Hiệu suất tốt, quản lý bộ nhớ hiệu quả (Ví dụ: InfluxDB, Dgraph)
Ứng dụng không phù hợp:
-
Hệ thống real-time cứng (hard real-time systems): Vì GC không đảm bảo thời gian phản hồi cố định (Ví dụ: Hệ thống điều khiển máy bay, robot công nghiệp)
-
Ứng dụng đòi hỏi kiểm soát bộ nhớ ở mức thấp: Vì Không có quyền truy cập trực tiếp vào quản lý bộ nhớ (Ví dụ: Trình điều khiển thiết bị cấp thấp)
-
Game engines đòi hỏi hiệu suất cực cao: Vì GC có thể gây ra độ trễ, thiếu các tính năng tối ưu hóa đặc biệt (Ví dụ: AAA game engines)
-
Hệ thống nhúng với tài nguyên rất hạn chế: Vì Binary size tương đối lớn, yêu cầu runtime (Ví dụ: Các thiết bị IoT cực nhỏ, vi điều khiển)