⚡ Tối Ưu Tốc Độ - Performance Optimization
🎯 Mục Tiêu Bài Học
Trong bài này, bạn sẽ học cách tối ưu hóa hiệu suất ứng dụng Go, giống như một nhà hàng cần tối ưu tốc độ phục vụ để khách hàng hài lòng.
Sau bài học này, bạn sẽ:
- Hiểu cách đo lường hiệu suất (profiling)
- Biết cách tối ưu bộ nhớ
- Áp dụng best practices cho goroutines
- Sử dụng công cụ pprof để phân tích
🏪 Khái Niệm Nhà Hàng
Performance là gì?
Performance giống như tốc độ phục vụ trong nhà hàng:
Nhà Hàng Tốt:
┌─────────────────────────┐
│ Đặt món → 2 phút        │
│ Nấu ăn → 10 phút        │
│ Phục vụ → 1 phút        │
│ TỔNG: 13 phút ✓         │
└─────────────────────────┘
Nhà Hàng Chậm:
┌─────────────────────────┐
│ Đặt món → 5 phút        │
│ Nấu ăn → 30 phút        │
│ Phục vụ → 5 phút        │
│ TỔNG: 40 phút ✗         │
└─────────────────────────┘
📊 Profiling - Đo Thời Gian Nấu
CPU Profiling
CPU Profiling giống như đo thời gian mỗi món ăn:
package main
import (
    "fmt"
    "os"
    "runtime/pprof"
    "time"
)
// Món ăn mất nhiều thời gian
func nauMonCham() {
    for i := 0; i < 1000000; i++ {
        _ = fmt.Sprintf("Món %d", i)
    }
}
// Món ăn nhanh
func nauMonNhanh() {
    for i := 0; i < 1000; i++ {
        _ = fmt.Sprintf("Món %d", i)
    }
}
func main() {
    // Bắt đầu đo thời gian
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    start := time.Now()
    // Nấu các món
    nauMonCham()
    nauMonNhanh()
    elapsed := time.Since(start)
    fmt.Printf("Tổng thời gian: %s\n", elapsed)
}
Phân tích kết quả:
go run main.go
go tool pprof cpu.prof
# Trong pprof console:
(pprof) top
# Hiển thị các function tốn nhiều CPU nhất
Memory Profiling
Memory Profiling giống như kiểm tra nguyên liệu còn lại:
package main
import (
    "fmt"
    "os"
    "runtime"
    "runtime/pprof"
)
type MonAn struct {
    Ten        string
    NguyenLieu []string
    GhiChu     string
}
// Cách lãng phí nguyên liệu
func cheBienLangPhi() []MonAn {
    menu := make([]MonAn, 0) // Không định trước kích thước
    for i := 0; i < 10000; i++ {
        mon := MonAn{
            Ten:        fmt.Sprintf("Món %d", i),
            NguyenLieu: []string{"Rau", "Thịt", "Gia vị"},
            GhiChu:     "Ghi chú dài dài dài...",
        }
        menu = append(menu, mon)
    }
    return menu
}
// Cách tiết kiệm nguyên liệu
func cheBienTietKiem() []MonAn {
    menu := make([]MonAn, 0, 10000) // Định trước kích thước
    for i := 0; i < 10000; i++ {
        mon := MonAn{
            Ten:        fmt.Sprintf("Món %d", i),
            NguyenLieu: []string{"Rau", "Thịt", "Gia vị"},
            GhiChu:     "Ghi chú ngắn",
        }
        menu = append(menu, mon)
    }
    return menu
}
func main() {
    // Đo memory
    f, _ := os.Create("mem.prof")
    cheBienTietKiem()
    runtime.GC() // Thu dọn rác
    pprof.WriteHeapProfile(f)
    f.Close()
    fmt.Println("Memory profile saved to mem.prof")
}
🧵 Goroutine Pool - Đội Bếp Hiệu Quả
Vấn đề: Quá nhiều đầu bếp
package main
import (
    "fmt"
    "time"
)
// CÁCH SAI: Tạo quá nhiều goroutines
func phucVuKhongHieuQua(orders []string) {
    for _, order := range orders {
        go func(o string) {
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Phục vụ: %s\n", o)
        }(order)
    }
    time.Sleep(2 * time.Second)
}
// Với 10,000 orders sẽ tạo 10,000 goroutines!
Giải pháp: Worker Pool Pattern
package main
import (
    "fmt"
    "sync"
    "time"
)
type DonHang struct {
    ID   int
    Ten  string
}
// Worker pool - Đội bếp cố định
func workerPool(orders <-chan DonHang, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    for order := range orders {
        // Đầu bếp nấu món
        time.Sleep(100 * time.Millisecond)
        result := fmt.Sprintf("Đầu bếp đã hoàn thành: %s (ID: %d)",
            order.Ten, order.ID)
        results <- result
    }
}
func phucVuHieuQua() {
    soDauBep := 5 // 5 đầu bếp
    soDonHang := 20
    orders := make(chan DonHang, soDonHang)
    results := make(chan string, soDonHang)
    var wg sync.WaitGroup
    // Khởi tạo đội bếp
    for i := 0; i < soDauBep; i++ {
        wg.Add(1)
        go workerPool(orders, results, &wg)
    }
    // Gửi đơn hàng
    go func() {
        for i := 1; i <= soDonHang; i++ {
            orders <- DonHang{
                ID:  i,
                Ten: fmt.Sprintf("Món %d", i),
            }
        }
        close(orders)
    }()
    // Đóng results khi tất cả workers xong
    go func() {
        wg.Wait()
        close(results)
    }()
    // Nhận kết quả
    for result := range results {
        fmt.Println(result)
    }
}
func main() {
    fmt.Println("=== Nhà hàng với Worker Pool ===")
    start := time.Now()
    phucVuHieuQua()
    fmt.Printf("Thời gian: %s\n", time.Since(start))
}
💾 Tối Ưu Bộ Nhớ - Tiết Kiệm Nguyên Liệu
String Builder cho hiệu suất cao
package main
import (
    "fmt"
    "strings"
    "time"
)
// CÁCH CHẬM: Nối string bằng +
func taoMenuCham(soMon int) string {
    menu := ""
    for i := 1; i <= soMon; i++ {
        menu += fmt.Sprintf("Món %d: Phở\n", i)
    }
    return menu
}
// CÁCH NHANH: Dùng strings.Builder
func taoMenuNhanh(soMon int) string {
    var builder strings.Builder
    builder.Grow(soMon * 20) // Dự trữ bộ nhớ trước
    for i := 1; i <= soMon; i++ {
        builder.WriteString(fmt.Sprintf("Món %d: Phở\n", i))
    }
    return builder.String()
}
func main() {
    soMon := 10000
    // Test cách chậm
    start := time.Now()
    _ = taoMenuCham(soMon)
    fmt.Printf("Cách chậm: %s\n", time.Since(start))
    // Test cách nhanh
    start = time.Now()
    _ = taoMenuNhanh(soMon)
    fmt.Printf("Cách nhanh: %s\n", time.Since(start))
}
Slice preallocation
package main
import (
    "fmt"
    "time"
)
type NguyenLieu struct {
    Ten     string
    SoLuong int
}
// CÁCH CHẬM: Không định trước kích thước
func chuanBiCham() []NguyenLieu {
    kho := make([]NguyenLieu, 0)
    for i := 0; i < 100000; i++ {
        kho = append(kho, NguyenLieu{
            Ten:     fmt.Sprintf("NL-%d", i),
            SoLuong: i,
        })
    }
    return kho
}
// CÁCH NHANH: Định trước kích thước
func chuanBiNhanh() []NguyenLieu {
    kho := make([]NguyenLieu, 0, 100000)
    for i := 0; i < 100000; i++ {
        kho = append(kho, NguyenLieu{
            Ten:     fmt.Sprintf("NL-%d", i),
            SoLuong: i,
        })
    }
    return kho
}
func main() {
    start := time.Now()
    _ = chuanBiCham()
    fmt.Printf("Không preallocate: %s\n", time.Since(start))
    start = time.Now()
    _ = chuanBiNhanh()
    fmt.Printf("Có preallocate: %s\n", time.Since(start))
}
🔧 Benchmarking - So Sánh Hiệu Suất
Viết Benchmark Tests
// performance_test.go
package main
import (
    "strings"
    "testing"
)
// Benchmark nối string bằng +
func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        menu := ""
        for j := 0; j < 100; j++ {
            menu += "Món ăn "
        }
    }
}
// Benchmark dùng strings.Builder
func BenchmarkStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var builder strings.Builder
        for j := 0; j < 100; j++ {
            builder.WriteString("Món ăn ")
        }
        _ = builder.String()
    }
}
// Benchmark không preallocate
func BenchmarkSliceNoPrealloc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice := make([]int, 0)
        for j := 0; j < 1000; j++ {
            slice = append(slice, j)
        }
    }
}
// Benchmark có preallocate
func BenchmarkSliceWithPrealloc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice := make([]int, 0, 1000)
        for j := 0; j < 1000; j++ {
            slice = append(slice, j)
        }
    }
}
Chạy benchmark:
go test -bench=. -benchmem
# Kết quả mẫu:
# BenchmarkStringConcat-8         20000    85000 ns/op   500000 B/op   100 allocs/op
# BenchmarkStringBuilder-8       100000    15000 ns/op    10000 B/op     5 allocs/op
🎯 Best Practices
1. Tránh cấp phát bộ nhớ không cần thiết
// SAI
func layTenMon(mon string) string {
    return strings.ToUpper(mon) // Tạo string mới
}
// ĐÚNG (nếu có thể)
func layTenMonHieuQua(mon []byte) {
    for i := range mon {
        if mon[i] >= 'a' && mon[i] <= 'z' {
            mon[i] -= 32
        }
    }
}
2. Sử dụng sync.Pool để tái sử dụng objects
package main
import (
    "fmt"
    "sync"
)
type DiaAn struct {
    MonAn string
    Size  int
}
var diaPool = sync.Pool{
    New: func() interface{} {
        return &DiaAn{}
    },
}
func phucVuMonAn(ten string) {
    // Lấy đĩa từ pool
    dia := diaPool.Get().(*DiaAn)
    dia.MonAn = ten
    dia.Size = 10
    // Sử dụng đĩa
    fmt.Printf("Phục vụ %s trên đĩa\n", dia.MonAn)
    // Trả đĩa về pool
    dia.MonAn = ""
    diaPool.Put(dia)
}
func main() {
    for i := 1; i <= 5; i++ {
        phucVuMonAn(fmt.Sprintf("Món %d", i))
    }
}
3. Defer có chi phí - dùng có chọn lọc
package main
import (
    "fmt"
    "time"
)
// Có defer - chậm hơn một chút
func voidDefer() {
    start := time.Now()
    defer func() {
        fmt.Println("Đóng bếp")
    }()
    // Làm việc
}
// Không defer - nhanh hơn
func khongDefer() {
    start := time.Now()
    // Làm việc
    fmt.Println("Đóng bếp")
}
// Lưu ý: Chỉ tối ưu trong hot path
🔍 Công Cụ Profiling
1. pprof CPU Profile
package main
import (
    "net/http"
    _ "net/http/pprof"
)
func main() {
    // Mở http://localhost:6060/debug/pprof/
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()
    // Ứng dụng của bạn...
}
2. Trace
# Tạo trace file
go test -trace=trace.out
# Xem trace
go tool trace trace.out
📝 Bài Tập Thực Hành
Bài 1: Tối ưu xử lý menu
package main
import (
    "fmt"
    "strings"
)
// TODO: Tối ưu hàm này
func taoMenu(soMon int) string {
    menu := ""
    for i := 1; i <= soMon; i++ {
        menu += fmt.Sprintf("%d. Phở Bò - 50,000đ\n", i)
    }
    return menu
}
func main() {
    menu := taoMenu(1000)
    fmt.Println(strings.Split(menu, "\n")[0])
}
Bài 2: Implement worker pool
package main
// TODO: Tạo worker pool xử lý 1000 đơn hàng
// với 10 workers
func main() {
    // Code của bạn
}
🎓 Tóm Tắt
| Kỹ Thuật | Ví Dụ Nhà Hàng | Lợi Ích | 
|---|---|---|
| Profiling | Đo thời gian nấu từng món | Tìm bottleneck | 
| Worker Pool | Đội bếp cố định 5 người | Kiểm soát tài nguyên | 
| Preallocate | Chuẩn bị đủ nguyên liệu trước | Giảm memory allocation | 
| sync.Pool | Tái sử dụng đĩa | Giảm GC pressure | 
| strings.Builder | Ghép menu hiệu quả | Nhanh hơn string concat | 
🔗 Bài Tiếp Theo
Học cách bảo mật ứng dụng Go: Security →
Ghi nhớ: Performance optimization giống như điều hành nhà hàng - đo lường mọi thứ, tối ưu quy trình, và luôn tìm cách phục vụ nhanh hơn!