Phần 10 đo lỗi trang. Có một cách giảm số lỗi trang xuống 512 lần: dùng trang 2 MB thay vì 4 KB. Bài này đo cả hai mặt của việc đó.
Trang lớn giải quyết vấn đề gì
Mỗi lần CPU dịch một địa chỉ ảo sang địa chỉ vật lý, nó tra bảng trang. Bộ đệm cho việc tra đó gọi là TLB, và nó rất nhỏ — vài nghìn mục.
Với trang 4 KB, vài nghìn mục TLB phủ được vài chục megabyte. Vùng dữ liệu lớn hơn thế thì mỗi lần chạm vào chỗ mới là một lần trượt TLB: CPU phải đi bộ qua bảng trang bốn tầng, mỗi tầng là một lần đọc bộ nhớ.
Trang 2 MB làm một mục TLB phủ 2 MB thay vì 4 KB — gấp 512 lần.
Đo phần lợi
Chương trình duyệt ngẫu nhiên theo trang, mỗi trang chứa chỉ số trang tiếp theo nên các bước phụ thuộc nhau (như phần 9 đã chỉ ra, đây là điều kiện bắt buộc để thấy độ trễ thật). Ba lần đo mỗi cấu hình:
| Vùng nhớ | Trang 4 KB | Trang 2 MB | Lợi |
|---|---|---|---|
| 64 MB | 86,8 / 86,1 / 81,7 ns | 45,1 / 44,6 / 49,4 ns | 1,8× |
| 256 MB | 140,4 / 115,7 / 122,4 ns | 82,3 / 81,6 / 112,8 ns | ~1,4× |
| 1.024 MB | 181,9 / 170,3 / 172,0 ns | 163,5 / 161,4 / 155,6 ns | 1,09× |
Vùng càng lớn, lợi càng nhỏ — ngược với trực giác
Đáng lẽ vùng càng lớn thì áp lực TLB càng cao và trang lớn càng đáng giá. Kết quả ngược lại.
Câu trả lời nằm ở dòng AnonHugePages của chính chương trình:
| Vùng yêu cầu | Thực sự được trang lớn |
|---|---|
| 64 MB | 65.536 KB — 100% |
| 256 MB | 262.144 KB — 100% |
| 1.024 MB | 495.616 KB — 47% |
Ở 1 GB, nhân chỉ gom được chưa tới một nửa. Một trang 2 MB cần 2 MB bộ nhớ vật lý liên tục, và trên một máy đã chạy được vài giờ, những khối liên tục như vậy trở nên hiếm.
Nhân không báo lỗi. Nó chỉ lặng lẽ cấp trang 4 KB cho phần còn lại. Chương trình xin trang lớn, nhận được một nửa, và không có tín hiệu nào ngoài con số trong smaps.
Nói lại lần thứ hai vì đây là điểm hay bị bỏ qua: MADV_HUGEPAGE là lời đề nghị, không phải lời hứa, và tỷ lệ được đáp ứng phụ thuộc vào mức phân mảnh của máy tại thời điểm đó — nghĩa là kết quả đo lúc máy vừa khởi động không dự đoán được kết quả sau ba ngày chạy.
Đo phần giá
Chương trình cấp 512 MB rồi chạm đúng 1 byte trong mỗi vùng 2 MB — tổng cộng 256 byte dữ liệu thật:
| RSS | AnonHugePages |
|
|---|---|---|
MADV_NOHUGEPAGE |
2.088 KB | 0 KB |
MADV_HUGEPAGE |
457.904 KB | 456.704 KB |
219 lần bộ nhớ cho cùng 256 byte dữ liệu.
Cơ chế đơn giản: chạm vào 1 byte trong một vùng 2 MB làm nhân cấp cả trang 2 MB. Với trang 4 KB, chạm 1 byte chỉ cấp 4 KB.
Đây chính là lý do Redis, MongoDB, và nhiều cơ sở dữ liệu khác in cảnh báo khởi động yêu cầu tắt trang lớn trong suốt. Chúng cấp phát rải rác nhiều vùng nhỏ, và mỗi vùng vài KB kéo theo nguyên một trang 2 MB.
Một cái giá thứ hai, khó thấy hơn: khi bộ nhớ phân mảnh, nhân phải dồn nén để tạo ra khối 2 MB liên tục, và quá trình đó dừng tiến trình đang xin lại. Trong phép đo của tôi lần chạy đầu tiên mất 0,042 giây, lần thứ hai 0,009 giây — chênh gần 5 lần cho cùng một việc.
Ba chế độ
cat /sys/kernel/mm/transparent_hugepage/enabled
| Chế độ | Nghĩa |
|---|---|
always |
Mọi vùng ẩn danh đủ lớn đều được gom thành trang lớn |
madvise |
Chỉ vùng nào gọi madvise(MADV_HUGEPAGE) |
never |
Tắt hẳn |
madvise là lựa chọn mặc định đúng cho máy chủ. Nó giao quyền quyết định cho ứng dụng — thứ duy nhất biết vùng nhớ của nó sẽ được chạm dày hay thưa.
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
Nút defrag quyết định nhân có được chặn tiến trình lại để dồn nén bộ nhớ hay không. Đặt always ở đây là nguồn gốc của những khoảng đình trệ vài trăm mili giây không lý do — tiến trình đang xin bộ nhớ bị treo trong khi nhân đi dọn dẹp.
Trang lớn tĩnh
Ngoài trang lớn trong suốt còn có trang lớn cấp phát sẵn — dành riêng lúc khởi động, không bao giờ bị hoán đổi hay dồn nén:
sysctl -w vm.nr_hugepages=1024 # danh 2 GB
grep HugePages /proc/meminfo
Ứng dụng lấy chúng qua hugetlbfs hoặc cờ MAP_HUGETLB. Đây là cách PostgreSQL (huge_pages = on) và Oracle dùng cho vùng đệm dùng chung.
Ưu điểm: không phân mảnh, không đình trệ, hiệu quả ổn định. Nhược điểm: bộ nhớ đó bị khoá cứng — không ai khác dùng được, kể cả khi bộ đệm trang đang thiếu.
Khi nào dùng
Trang lớn đáng dùng khi cả ba điều kiện cùng đúng:
- Vùng nhớ liên tục và lớn (từ vài trăm MB trở lên).
- Được chạm gần hết, không rải rác.
- Sống lâu, để chi phí gom trả góp được.
Vùng đệm của cơ sở dữ liệu quan hệ, mảng số học của tính toán khoa học, heap của JVM khai cố định — cả ba đều khớp. Bộ nhớ của một ứng dụng web thông thường thì không.
Thử ba mươi giây
Xem máy bạn đang ở chế độ nào và ai đang dùng trang lớn:
echo "che do: $(cat /sys/kernel/mm/transparent_hugepage/enabled)"
echo "defrag: $(cat /sys/kernel/mm/transparent_hugepage/defrag)"
grep -E 'AnonHugePages|HugePages_Total|Hugepagesize' /proc/meminfo
echo "--- tien trinh dung trang lon nhieu nhat ---"
for p in $(ls /proc | grep -E '^[0-9]+$'); do
h=$(awk '/^AnonHugePages:/{print $2}' /proc/$p/smaps_rollup 2>/dev/null)
[ "${h:-0}" -gt 0 ] && printf "%9s KB %s\n" "$h" \
"$(tr -d '\0' < /proc/$p/cmdline | cut -c1-45)"
done 2>/dev/null | sort -rn | head -5
Nếu chế độ là always và một tiến trình có AnonHugePages lớn hơn nhiều so với lượng dữ liệu bạn biết nó thực sự giữ, bạn đang trả đúng cái giá 219 lần ở trên — chỉ là ở tỷ lệ nhẹ hơn.
Phần sau: malloc và cấp phát bộ nhớ — đo phân mảnh.