+1

[Phỏng vấn Golang] Tại sao Go's Garbage Collector là chìa khóa cho hiệu suất cao?

I. Giới thiệu

Mình từng tự hỏi "Mình viết logic thì cần biết đếch gì đến Go garbage collection (GoGC) hoạt động như nào?". Nhưng thực tế thì nó cũng có một số lợi ích. Hiểu về GoGC 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ế 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 nhằm 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 thì đượ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 lại đơ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ừ "stop-the-world" sang "concurrent" 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, 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":

  1. Mark: Xác định và đánh dấu các đối tượng còn đang được sử dụng (có thể truy cập được từ các điểm gốc như stack, global variables)
  2. Sweep: Giải phóng bộ nhớ của các đối tượng không được đánh dấu
  3. Compact (tùy lúc sẽ dùng): GC có thể thực hiện việc sắp xếp lại bộ nhớ để giảm phân mảnh. Tuy nhiên, Go thường không thực hiện bước này vì mục tiêu của GoGC là giảm thiểu gián đoạn cho ứng dụng, và việc sắp xếp bộ nhớ có thể làm tăng độ trễ.

Go sẽ quyết định thực hiện việc thu gom rác dựa trên một ngưỡng bộ nhớ đã được cấp phát kể từ lần cuối cùng GC được chạy. Ngưỡng này được xác định bởi biến môi trường GOGC, mặc định có giá trị là 100. Điều này có nghĩa là GC sẽ chạy khi lượng bộ nhớ được cấp phát đã tăng thêm 100% so với khi GC lần trước đó hoàn tất. Bạn có thể thay đổi giá trị GOGC để kiểm soát tần suất GC:

GOGC = 100: GC chạy khi bộ nhớ đã cấp phát tăng gấp đôi.

GOGC > 100: GC sẽ chạy ít thường xuyên hơn, tức là sau khi đã cấp phát thêm nhiều bộ nhớ hơn.

GOGC < 100: GC sẽ chạy thường xuyên hơn, tức là sau khi cấp phát ít bộ nhớ hơn.

3.Ưu & Nhược điểm

Ư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: Mặc dù GoGC mở rộng tốt trên nhiều lõi CPU, nhưng througout (tốc độ cấp phát và thu gom bộ nhớ) không tăng tuyến tính khi tăng số lõi, vì đòi hỏi việc đồng bộ hóa giữa các luồng xử lý (goroutine) để đảm bảo rằng không có dữ liệu bị thu gom khi vẫn còn đang được sử dụng. Việc đồng bộ hóa này gây ra điểm nghẽn (bottleneck) trong việc quản lý bộ nhớ đồng thời.
    • Tức là nếu bạn tăng gấp đôi số core của CPU, bạn không thể mong đợi tốc độ cấp phát và gom rác của GoGC cũng tăng gấp đôi. Việc này có thể dẫn đến giới hạn trong khả năng mở rộng của ứng dụng, đặc biệt là đối với các hệ thống có nhiều core và yêu cầu cao về hiệu suất bộ nhớ.
  • 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, 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. 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 đánh giá như sau (link bài viết ở phần tham khảo nếu bạn cần).

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

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 như USB driver)

  • 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ì Go thường complile ra các file nhị phân lớn hơn các ngôn ngữ như C/C++ do việc liên kết tĩnh với thư viện chuẩn và tích hợp garbage collection. Điều này có thể không phù hợp với các hệ thống nhúng có bộ nhớ flash nhỏ.

    • Go yêu cầu một runtime environment với GC để quản lý bộ nhớ. Việc này có thể làm tăng việc sử dụng tài nguyên, đặc biệt trong các hệ thống không có nhiều RAM hoặc CPU.
    • Ví dụ: Các thiết bị IoT cực nhỏ, vi điều khiển

IV. Kết luận & Tham khảo

Với cơ chế concurrent GC, đã mang đến nhiều cải tiến đáng kể trong việc giảm thiểu thời gian dừng ứng dụng, giúp Go trở thành lựa chọn hàng đầu cho các hệ thống yêu cầu tính khả dụng cao. Tuy nhiên, như bất kỳ hệ thống quản lý bộ nhớ nào, GoGC vẫn tiêu tốn một phần tài nguyên hệ thống, đặc biệt là CPU và bộ nhớ. Việc hiểu rõ cơ chế hoạt động của GC sẽ giúp bạn biết được khi nào nên sử dụng go cho dự án cũng như cách tối ưu hoá ứng dụng, đảm bảo sự cân bằng giữa hiệu suất và khả năng quản lý tài nguyên.

Ở phần tiếp theo, mình sẽ trình bày các phương pháp tối ưu hóa hiệu suất ứng dụng Go, từ việc tinh chỉnh GoGC đến những chiến lược tối ưu hóa khác nhằm giúp hệ thống đạt hiệu suất cao nhất trong môi trường sản xuất.

Tham khảo

Group discord 2k+ mems: chém gió về lập trình và làm pet project cùng nhau

Go vs .NET part 2: garbage collection


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí