0

Go Zero to Hero - Bài 63: Package sync.Pool - Tối ưu hoá cấp phát bộ nhớ

1. Nỗi đau của hệ thống: Cơn ác mộng Garbage Collector (GC)

Hãy tưởng tượng bạn đang viết một API nhận dữ liệu quét thẻ từ cổng xoay AFC. Mỗi lần có khách quẹt thẻ, hệ thống lại tạo ra một biến struct hoặc một mảng []byte để chứa cục dữ liệu JSON gửi lên.

Nếu có 10.000 lượt quẹt thẻ mỗi giây, hệ thống của bạn sẽ liên tục "khai sinh" ra 10.000 đối tượng trên RAM. Xử lý xong, các đối tượng này bị vứt bỏ. Lúc này, Bộ thu gom rác (Garbage Collector - GC) của Golang phải è cổ ra chạy để dọn dẹp hàng vạn cái xác chết đó nhằm giải phóng RAM.

Quá trình dọn rác này ngốn cực kỳ nhiều CPU. Tệ hơn nữa, nó có thể gây ra hiện tượng giật lag hệ thống (Spike), làm cho độ trễ API của bạn tăng vọt từ 10ms lên tận 500ms!


2. sync.Pool: Nghệ thuật "Rửa bát dùng lại"

Thay vì dùng ly nhựa dùng 1 lần rồi vứt (cấp phát vùng nhớ mới), sync.Pool hoạt động như một cái chạn bát đĩa dùng nhiều lần trong nhà hàng.

  • Lấy ra (Get): Khi cần một vùng nhớ, bạn thò tay vào Pool để lấy. Nếu Pool trống, nó sẽ chạy hàm New() để đúc ra một cái bát mới.
  • Dùng xong trả lại (Put): Sau khi xử lý xong dữ liệu, thay vì ném cái bát đó vào thùng rác cho GC dọn, bạn rửa sạch nó và cất ngược lại vào Pool để luồng (Goroutine) tiếp theo xài.

Bằng cách này, dù có 10.000 lượt quẹt thẻ, hệ thống có thể chỉ cần tạo ra đúng 100 vùng nhớ và cứ thế tái sử dụng xoay vòng. GC hoàn toàn thất nghiệp!


3. Thực chiến: Tối ưu bộ đệm xử lý gói tin (Buffer)

Kịch bản kinh điển nhất của sync.Pool là quản lý các bytes.Buffer dùng để nối chuỗi hoặc đọc luồng dữ liệu byte thô từ thiết bị ngoại vi.

package main

import (
    "bytes"
    "fmt"
    "sync"
    "time"
)

// 1. Khai báo một Pool toàn cục
var bufferPool = sync.Pool{
    // Hàm New sẽ tự động được gọi nếu có người Get() mà Pool đang trống
    New: func() any {
        fmt.Println("✨ [Hệ thống] Đúc thêm một Buffer mới (Tốn RAM)")
        return new(bytes.Buffer)
    },
}

func processRFIDData(workerID int, data string) {
    // 2. LẤY đồ từ Pool ra dùng (Phép ép kiểu là bắt buộc vì Pool trả về 'any')
    buf := bufferPool.Get().(*bytes.Buffer)
    
    // LUẬT SINH TỬ 1: LUÔN PHẢI RESET TRƯỚC KHI DÙNG HOẶC TRƯỚC KHI TRẢ
    buf.Reset() 

    // LUẬT SINH TỬ 2: LUÔN NHỚ TRẢ ĐỒ LẠI VÀO POOL SAU KHI XONG VIỆC
    defer bufferPool.Put(buf)

    // Xử lý logic nghiệp vụ
    buf.WriteString("MÃ_TRẠM_A_")
    buf.WriteString(data)
    
    fmt.Printf("👷 [Worker %d] Đã xử lý xong: %s\n", workerID, buf.String())
}

func main() {
    var wg sync.WaitGroup

    // Tung ra 5 Goroutines chạy đồng thời để xử lý 5 chiếc thẻ
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            processRFIDData(id, fmt.Sprintf("CARD_%d", id))
        }(i)
    }

    wg.Wait()
    fmt.Println("--- KẾT THÚC LÔ 1 ---")

    // Đợi 1 chút, giả lập đợt khách thứ 2 tới
    time.Sleep(1 * time.Second)
    fmt.Println("\n--- BẮT ĐẦU LÔ 2 (Kỳ vọng không tốn RAM khởi tạo) ---")
    
    for i := 6; i <= 8; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            processRFIDData(id, fmt.Sprintf("CARD_%d", id))
        }(i)
    }
    
    wg.Wait()
}

Nếu chạy đoạn code trên, bạn sẽ thấy dòng chữ ✨ [Hệ thống] Đúc thêm một Buffer mới chỉ xuất hiện vài lần ở đợt đầu tiên. Sang đợt khách thứ 2, hệ thống hoàn toàn câm lặng vì nó đã tái sử dụng lại bộ đệm từ đợt 1. RAM và CPU được bảo toàn tuyệt đối.


4. Ranh giới hắc ám: Đừng nhầm Pool với Cache!

Sức mạnh càng lớn, cái bẫy càng sâu. sync.Pool có 2 quy luật thép mà nếu bạn phá vỡ, dữ liệu toàn hệ thống sẽ rò rỉ chéo cho nhau:

A. Rò rỉ dữ liệu nhạy cảm (Không Reset)

Khi một Request A trả cái Buffer vào Pool, dữ liệu cũ của Request A (chứa thông tin cá nhân khách hàng, số dư thẻ) VẪN CÒN NGUYÊN trong đó. Khi Request B rút cái Buffer đó ra dùng, nếu bạn không gọi hàm .Reset(), Request B sẽ đọc được toàn bộ thông tin mật của Request A. Đây là thảm họa bảo mật.

👉 Luật: Luôn "rửa bát" (Reset(), gán các field về 0 hoặc rỗng) ngay trước khi Put hoặc ngay sau khi Get.

B. Ngộ nhận nó là Database thu nhỏ

Nhiều kỹ sư thấy sync.Pool lưu được đồ bèn mang nó ra làm nơi... lưu trữ Kết nối Database (DB Connection) hoặc Token đăng nhập. Đây là sai lầm trí mạng! Golang định nghĩa sync.Pool là một vùng nhớ "tạm bợ". Hệ thống Garbage Collector có quyền âm thầm xóa sạch mọi thứ bên trong sync.Pool bất cứ lúc nào nó muốn mà không thèm báo trước cho bạn một tiếng.

👉 Luật: Chỉ dùng Pool để chứa các cấu trúc dữ liệu dùng một lần ([]byte, bytes.Buffer, các struct trung gian chuyển đổi JSON).


📊 Bảng đối chiếu: Khi nào nên rút vũ khí?

Tiêu chí Dùng sync.Pool Không dùng (Tạo mới bình thường)
Bản chất Object Lớn, tốn chi phí khởi tạo, chiếm nhiều RAM ([]byte lớn). Nhỏ, nguyên thủy (int, string, struct nhẹ vài byte).
Vòng đời Cực ngắn (Sinh ra và chết đi ngay trong 1 Request API). Dài, cần sống sót qua nhiều Request (User Session).
Tần suất Hàng ngàn lần mỗi giây (Stress test ép tải cực độ). Rải rác vài lần mỗi phút.

💡 Tổng kết

Với sync.Pool, kỹ năng tối ưu tài nguyên phần cứng của bạn đã chính thức đạt chuẩn Senior. Băng thông bộ nhớ không còn là nút thắt cổ chai nữa!


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í