Bối cảnh & Lý do
Sáu tháng trước, cụm cơ sở dữ liệu chính của chúng tôi gặp phải một rào cản vô hình. Mức sử dụng CPU duy trì ổn định ở mức dễ chịu 42%, I/O ổ cứng NVMe còn dư thừa rất nhiều dung lượng, nhưng độ trễ truy vấn p99 lại tăng vọt từ 12ms lên hơn 180ms vào các khung giờ cao điểm ban ngày.
Trong khi đó, tầng caching Redis thường xuyên xuất hiện các đợt tăng vọt độ trễ lên đến 200ms mỗi khi quá trình snapshot chạy ngầm bắt đầu. Chúng tôi đã rà soát kỹ các bộ đếm kernel và hồ sơ đo kiểm hiệu năng (perf profiles). Thủ phạm nhanh chóng lộ diện: overhead từ việc dịch địa chỉ bộ nhớ và hiện tượng thrashing nghiêm trọng ở Translation Lookaside Buffer (TLB).
Theo mặc định, Linux chia bộ nhớ ảo thành các trang 4 KB. Cơ chế này hoạt động tốt với các tác vụ nhẹ, nhưng đối với các cơ sở dữ liệu quy mô lớn, mô hình này nhanh chóng bộc lộ hạn chế. Khi bạn cấp phát 64 GB RAM cho shared memory của database, kernel phải theo dõi tới 16 triệu mục bảng trang (page entries) riêng biệt. Các CPU hiện đại không thể lưu trữ hết ngần ấy bản dịch trong các bộ nhớ đệm TLB L1 và L2 siêu nhanh. Mỗi lần TLB miss, hệ thống buộc phải thực hiện một tiến trình duyệt bộ nhớ chính (page table walk) gồm nhiều bước tốn kém, kéo tụt hiệu năng truy vấn xuống.
Linux cung cấp cho chúng ta hai cách để xử lý các vùng bộ nhớ lớn:
- HugePages tĩnh (Static HugePages): Vùng nhớ được cấp phát trước thành các khối 2 MB hoặc 1 GB lúc khởi động hoặc ngay đầu thời gian chạy. Ứng dụng sẽ ánh xạ trực tiếp vùng nhớ này vào không gian địa chỉ ảo của mình. PostgreSQL hoạt động cực kỳ hiệu quả với HugePages tĩnh. Do kích thước shared buffer pool của PostgreSQL là cố định, HugePages giúp thu nhỏ các bảng trang từ hàng triệu mục xuống chỉ còn vài nghìn mục. Trong hệ thống của chúng tôi, chỉ riêng thay đổi này đã giúp giảm 85% tỷ lệ TLB miss.
- Transparent HugePages (THP): Một subsystem tự động của kernel cố gắng cấp phát động các trang 2 MB ngầm bên dưới mà không yêu cầu ứng dụng phải chỉnh sửa mã nguồn. Tuy nhiên, THP lại gây ra thảm họa cho Redis. Khi Redis fork một tiến trình chạy ngầm để xuất bản RDB dump hoặc ghi lại AOF (AOF rewrite), Linux sẽ dựa vào cơ chế Copy-on-Write (CoW). Với các trang chuẩn 4 KB, thao tác ghi vào một key duy nhất chỉ sao chép đúng 4 KB. Nhưng khi bật THP, việc chạm vào cùng một key đó buộc kernel phải sao chép toàn bộ khối 2 MB. Điều này làm nghẽn event loop của Redis và khiến mức tiêu thụ bộ nhớ phình to từ 30% đến 50%.
Việc tinh chỉnh bộ nhớ ảo tác động trực tiếp vào tầng lõi của Linux kernel. Một tham số cấu hình sai có thể kích hoạt cơ chế Out-of-Memory (OOM) killer hạ gục tiến trình ngay lập tức hoặc khiến hệ thống không thể khởi động. Hãy luôn benchmark kỹ lưỡng các thay đổi này trên môi trường staging trước khi triển khai lên các instance production.
Cài đặt
Các bản phân phối chính thống như Debian, Ubuntu, Rocky Linux và AlmaLinux đều tích hợp sẵn hỗ trợ HugePages ngay trong kernel. Bạn không cần bất kỳ kernel module bên thứ ba nào. Tuy nhiên, bạn sẽ cần các công cụ userland để kiểm tra cấu trúc liên kết bộ nhớ (memory topology), trạng thái trang và giám sát mức cân bằng giữa các node NUMA.
Cài đặt các gói công cụ chẩn đoán bộ nhớ tiêu chuẩn cho bản phân phối của bạn:
# Trên Debian / Ubuntu
sudo apt-get update
sudo apt-get install -y procps numactl sysfsutils
# Trên RHEL / AlmaLinux / Rocky Linux
sudo dnf install -y procps-ng numactl sysfsutils
Kiểm tra mức cấp phát HugePages hiện tại và kích thước trang mặc định của hệ thống:
grep -i huge /proc/meminfo
Trên một máy chủ chưa được cấu hình, bạn sẽ thấy kết quả đầu ra tương tự như sau:
AnonHugePages: 0 kB
ShmemPagemap: 0 kB
FileHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
Kernel báo cáo Hugepagesize mặc định là 2048 kB (2 MB). Lưu ý rằng HugePages_Total đang bằng 0, nghĩa là chưa có trang tĩnh nào được cấp phát trước.
Cấu hình
PostgreSQL và Redis yêu cầu hai chiến lược hoàn toàn trái ngược nhau. PostgreSQL cần HugePages tĩnh chuyên dụng được cấp phát trước, trong khi Redis lại cần vô hiệu hóa hoàn toàn Transparent HugePages.
1. Cấu hình HugePages tĩnh cho PostgreSQL
Tính toán số lượng trang cần thiết trực tiếp từ tham số shared_buffers của PostgreSQL. Tuyệt đối không cấp phát toàn bộ dung lượng RAM vật lý cho HugePages, vì các tiến trình backend phục vụ client của PostgreSQL, bộ nhớ làm việc (work_mem) và chính hệ điều hành vẫn phải dựa vào các trang 4 KB tiêu chuẩn.
Giả sử một máy chủ chuyên dụng có 32 GB RAM và shared_buffers = 16GB. Hãy cộng thêm khoảng 10% dung lượng dự phòng để phục vụ việc theo dõi tiến trình và bảng khóa (lock tables):
# Bộ nhớ chia sẻ mục tiêu: 16 GB * 1024 MB = 16.384 MB
# Tính toán số trang cơ bản: 16.384 MB / 2 MB mỗi trang = 8.192 trang
# Cộng thêm 10% biên an toàn: 8.192 * 1,10 = 9.011 trang (~17,6 GB)
Lưu cấu hình cấp phát trang vĩnh viễn vào tệp cấu hình sysctl của kernel:
sudo tee /etc/sysctl.d/90-hugepages.conf <<'EOF'
# Cấp phát 9011 trang x 2MB (~17.6 GB)
vm.nr_hugepages = 9011
EOF
sudo sysctl --system
Tiếp theo, cấp quyền cho người dùng hệ thống postgres để khóa phân vùng shared memory này vào RAM. Nếu không có quyền này, máy chủ sẽ không thể khóa pool bộ nhớ và sẽ khởi động thất bại:
sudo tee /etc/security/limits.d/99-postgres-hugepages.conf <<'EOF'
postgres soft memlock unlimited
postgres hard memlock unlimited
EOF
Cấu hình postgresql.conf để bắt buộc sử dụng HugePage thay vì âm thầm quay về dùng các trang 4 KB tiêu chuẩn:
# Trong postgresql.conf
huge_pages = on # Bắt buộc dùng HugePages tĩnh; không khởi động được nếu không khả dụng
shared_buffers = 16GB
Việc đặt huge_pages = on đóng vai trò như một chốt an toàn (fail-safe). Nếu các tham số sysctl không được áp dụng sau khi reboot, PostgreSQL sẽ từ chối khởi động. Cảnh báo đó giúp bạn nhận biết ngay lập tức hệ thống đang gặp sự cố, thay vì che giấu hiệu năng bị suy giảm khi chạy trên các trang 4 KB. Khởi động lại dịch vụ để áp dụng thay đổi:
sudo systemctl restart postgresql
2. Vô hiệu hóa Transparent HugePages (THP) cho Redis
Redis đưa ra cảnh báo rõ ràng về THP trong quá trình khởi động. Dưới lưu lượng ghi lớn, việc cấp phát động các trang 2 MB sẽ gây ra hiện tượng trễ do dồn phân mảnh (compaction delays) và phóng đại mức sử dụng bộ nhớ trong suốt các tác vụ fork.
Kiểm tra trạng thái THP đang hoạt động trên máy chủ của bạn:
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
Nếu dấu ngoặc vuông bao quanh [always] hoặc [madvise], THP đang được bật. Vì các giá trị ghi vào sysfs thông thường sẽ bị mất sau khi reboot, hãy vô hiệu hóa THP vĩnh viễn thông qua một systemd unit chạy sớm lúc khởi động:
sudo tee /etc/systemd/system/disable-thp.service <<'EOF'
[Unit]
Description=Vô hiệu hóa Transparent HugePages (THP)
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=redis.service redis-server.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled && echo never > /sys/kernel/mm/transparent_hugepage/defrag'
[Install]
WantedBy=basic.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now disable-thp.service
Redis cũng yêu cầu bật tính năng memory overcommit. Điều này giúp ngăn chặn các lỗi fork khi tác vụ lưu dữ liệu chạy ngầm của Redis đòi hỏi không gian địa chỉ ảo tương đương với tiến trình cha:
sudo tee /etc/sysctl.d/90-redis-overcommit.conf <<'EOF'
# Cho phép memory overcommit cho các tác vụ lưu dữ liệu chạy ngầm của Redis
vm.overcommit_memory = 1
EOF
sudo sysctl --system
sudo systemctl restart redis-server || sudo systemctl restart redis
Kiểm tra & Giám sát
Xác thực lại trạng thái bộ nhớ sau khi khởi động giúp ngăn ngừa các lỗi hồi quy (regressions) do việc cập nhật các gói kernel hoặc sai lệch cấu hình gây ra.
Kiểm tra mức sử dụng HugePages của PostgreSQL
Kiểm tra tình trạng cấp phát bộ nhớ khi PostgreSQL đang xử lý các kết nối:
grep -i hugepages /proc/meminfo
Kết quả đầu ra dự kiến trên môi trường production:
HugePages_Total: 9011
HugePages_Free: 782
HugePages_Rsvd: 782
HugePages_Surp: 0
Hugepagesize: 2048 kB
HugePages_Total thể hiện số lượng trang bạn đã cấp phát qua sysctl. Khi HugePages_Rsvd khớp với HugePages_Free, PostgreSQL đã nhận và sử dụng thành công phân vùng shared memory từ pool đã dành riêng. Phần chênh lệch chính là vùng đệm an toàn của chúng ta.
Kiểm tra thiết lập đang hoạt động trực tiếp từ psql:
sudo -u postgres psql -c "SHOW huge_pages;"
sudo -u postgres psql -c "SELECT name, setting, unit FROM pg_settings WHERE name IN ('huge_pages', 'shared_buffers');"
Kiểm tra trạng thái THP của Redis
Xác nhận kernel đã vô hiệu hóa THP và các vòng lặp chống phân mảnh:
cat /sys/kernel/mm/transparent_hugepage/enabled
# Dự kiến: always madvise [never]
cat /sys/kernel/mm/transparent_hugepage/defrag
# Dự kiến: always defer defer+madvise madvise [never]
Kiểm tra chẩn đoán trực tiếp của Redis để phát hiện hiện tượng phân mảnh bộ nhớ và các bất thường về độ trễ:
redis-cli info memory | grep -E 'used_memory_human|mem_fragmentation_ratio'
redis-cli latency doctor
Nếu THP vẫn còn bật, Redis sẽ cảnh báo trong log: WARNING you have Transparent Huge Pages (THP) enabled in your kernel. Khi đã vô hiệu hóa thành công, dòng log đó sẽ biến mất và latency doctor sẽ xác nhận độ trễ fork bình thường ở mức dưới 5ms.
Kinh nghiệm thực chiến trong môi trường Production
Hãy ghi nhớ các nguyên tắc vận hành thực tế sau:
- Phân mảnh NUMA (NUMA Fragmentation): Trên các hệ thống đa socket, việc cấp phát HugePages khi máy chủ đang chịu tải cao sẽ gây ra tình trạng phân bổ không đều giữa các node NUMA. Hãy luôn đặt trước HugePages tĩnh lúc khởi động máy chủ thông qua
sysctlđể đảm bảo các khối bộ nhớ liên tục và cân bằng trên tất cả các socket CPU. - Cập nhật các gói Kernel: Việc nâng cấp kernel hoặc chạy các profile tinh chỉnh (như
tuned-adm) có thể âm thầm đặt lại các cờ sysfs. Hãy luôn quản lý các systemd unit và các tệp trong/etc/sysctl.d/bằng hệ thống quản lý phiên bản với Ansible, Puppet hoặc Terraform.
HugePages tĩnh giúp giải quyết triệt để nút thắt cổ chai trong việc dịch địa chỉ bộ nhớ cho các buffer pool cố định của PostgreSQL. Trong khi đó, việc tắt THP mang lại cho Redis cơ chế Copy-on-Write tinh gọn cần thiết để duy trì độ trễ phản hồi dưới 1 mili-giây.

