Hiệu năng Linux cho lập trình viên

Sê-ri 50 phần đo tầng hệ điều hành, giải thích vì sao nhiều kết quả ở các sê-ri trước lại như thế.

50/50 phần đã đăng Linux
1 Một hệ thống nghẽn đĩa hiện CPU rảnh 3% — nhìn nhanh tưởng CPU đang bận, thực ra 94% thời gian đó nó ngồi không chờ đĩa trả lời Mở đầu sê-ri 50 phần về hiệu năng Linux. Bốn kiểu nghẽn — CPU, đĩa, bộ nhớ, chuyển ngữ cảnh — trông giống nhau nếu chỉ nhìn một con số, và cái này giả trang thành cái kia. Đo thật cả bốn: nghẽn đĩa cho CPU rảnh 3% (nhưng 94% là chờ), nghẽn khoá đẩy chuyển ngữ cảnh lên 964.760 lần/giây. Một quy trình sáu bước đọc vmstat để không mua nhầm CPU cho một máy nghẽn đĩa. 01/09/2026 · 8 phút đọc 2 Tải đĩa thuần đẩy load average từ 8,8 lên 12,88 trong khi CPU người dùng chỉ 1% — và load vẫn hiện 12,65 khi máy đã rảnh hoàn toàn Ba con số uptime trả về gần như ai cũng đọc sai theo hai kiểu. Thứ nhất: Linux đếm cả tiến trình đợi đĩa, nên load cao không có nghĩa CPU bận — đo được load tăng 4 đơn vị trong khi CPU rảnh 45%. Thứ hai: chúng là trung bình trượt 1/5/15 phút, nói vừa rồi thế nào chứ không nói bây giờ — load còn 12,65 trên máy đã tắt sạch tải. Load để thấy xu hướng, không để chẩn đoán tức thời. 01/09/2026 · 8 phút đọc 3 Một tiến trình hiện VIRT 2 GB nhưng chỉ tốn 107 MB RAM — và cộng cột %CPU của mọi tiến trình ra 1.600% mà không phải lỗi: mỗi cột trong top trả lời một câu hỏi khác top là công cụ ai cũng mở đầu tiên, và cột nào cũng có một cách đọc sai. VIRT là chỗ đã đặt trước, không phải RAM đang dùng; %CPU tính theo một nhân nên tổng có thể vượt 100% nhiều lần; và cả RES lẫn %MEM đều đếm trùng bộ nhớ dùng chung nên cộng lại luôn thổi phồng. Con số không trùng lặp là PSS — thứ top không hiện vì tính nó tốn kém. 01/09/2026 · 8 phút đọc 4 Cùng một lượng đọc đĩa, iowait đi từ 35% xuống 4% chỉ vì tôi cho CPU thêm việc — chỉ số 'chờ I/O' thấp không hề chứng minh đĩa không nghẽn Ba con số us, sy, wa có trong mọi công cụ đo Linux, và mỗi loại tải để lại một dấu vết riêng. Nhưng phép đo quan trọng nhất bài là chỗ wa nói dối: giữ nguyên tải đĩa, thêm 14 tiến trình tính toán, iowait tụt từ 35 xuống 4 trong khi số tiến trình bị chặn ở I/O không đổi một chút nào. Trên máy chủ đầy tải, wa gần như luôn thấp — đúng lúc cần nó nhất thì nó im. 01/09/2026 · 8 phút đọc 5 Chuyển ngữ cảnh Hai tiến trình trên cùng một nhân: 0,63 µs. Trên hai nhân khác nhau: 12,4 µs. Và luồng không rẻ hơn tiến trình. 01/09/2026 · 5 phút đọc 6 nice không làm gì cả cho tới khi tôi ghim hai tiến trình vào cùng một nhân — rồi tỉ lệ nhảy thành 98,7 trên 1,3; đây là điều kiện không tài liệu nào nhấn mạnh CFS chia CPU đều 1/N như nó hứa. Nhưng nice 19 — mức ưu tiên thấp nhất — vẫn nhận đúng một nửa CPU, cho tới khi ép cả hai tiến trình vào cùng một nhân; lúc đó tỉ lệ khớp chính xác bảng trọng số của CFS. Lý do: nice là trọng số trong MỘT hàng đợi chạy, và hai tiến trình ở hai nhân khác nhau không có gì để cân. Trên máy nhiều nhân, nice chỉ có tác dụng khi số tiến trình vượt số nhân. 01/09/2026 · 7 phút đọc 7 SCHED_BATCH nghe như 'chạy khi rảnh' nhưng không nhường CPU một chút nào — còn SCHED_IDLE hạ ưu tiên mạnh gấp bốn lần nice 19, và thời gian thực dừng đúng ở 95% Bốn cách đổi ưu tiên tiến trình, đo trong đúng điều kiện chúng có tác dụng (hai tiến trình cùng một nhân). Kết quả lật vài trực giác: BATCH vẫn 50/50 vì nó không đụng trọng số, IDLE xuống 0,3% vì nằm ở lớp riêng, và tiến trình thời gian thực bị nhân cố ý chặn ở 95% — tấm lưới duy nhất ngăn một vòng lặp FIFO treo cứng cả máy. 01/09/2026 · 7 phút đọc 8 Nhảy giữa các nhân CPU làm chậm gấp đôi khi dữ liệu vừa bộ nhớ đệm — và không ảnh hưởng một chút nào khi dữ liệu vượt bộ nhớ đệm; đó là lý do 'ghim CPU cho nhanh' thường vô ích Ghim tiến trình vào một nhân là lời khuyên tinh chỉnh quen thuộc, nhưng đo thật cho thấy nó chỉ đáng khi hai điều kiện cùng đúng. Với tập 1 MB vừa L2, nhảy nhân làm chậm 2,04 lần; với tập 256 MB thì 1,00 lần — vì lúc đó nhân nào cũng phải ra RAM. Bộ nhớ đệm L1/L2 thuộc về từng nhân, và bỏ nhân là bỏ lại toàn bộ cache đã nóng. 01/09/2026 · 8 phút đọc 9 Cùng một lần đọc RAM tốn 156 ns hoặc 5 ns — chênh 28 lần chỉ tuỳ vào việc CPU có đoán được địa chỉ tiếp theo hay không; tối ưu bộ nhớ đệm không phải đọc ít đi Con trỏ nối đuôi cho thấy cầu thang L1→RAM chênh 108 lần. Nhưng phép đo thứ hai lật kèo: cùng 256 MB, cùng kiểu trượt cache, các lần đọc độc lập nhanh gấp 28 lần các lần đọc phụ thuộc nhau — vì CPU hiện đại giữ hàng chục yêu cầu bộ nhớ cùng lúc. Nên std::vector<Item> và std::vector<Item*> chứa cùng dữ liệu chênh nhau một bậc độ lớn, và không profiler nào chỉ vào dòng khai báo đó. 01/09/2026 · 9 phút đọc 10 malloc 512 MB xong trong 0,1 mili giây và cấp đúng 0 byte — bộ nhớ thật chỉ xuất hiện khi bạn chạm vào, và lần chạm đầu đắt gấp 90 lần lần sau mmap 512 MB trả về tức thì với 2 lỗi trang, vì nhân chỉ ghi vào sổ 'vùng này hợp lệ'. Bộ nhớ thật đến từng trang một khi bạn chạm: lỗi trang nhẹ tốn 990 ns so với 11 ns lần sau — 90 lần, và đây là lý do tiến trình vừa khởi động luôn chậm hơn. Cộng thêm hai bất ngờ: nạp sẵn nhanh hơn nạp lười 2,5 lần, và bộ đếm lỗi trang nặng nói dối. 01/09/2026 · 8 phút đọc 11 Hai tiến trình cho RSS giống hệt nhau — 4.105 MB — nhưng một bên tốn 526 MB thật, bên kia 4.050 MB; RSS không sai, nó trả lời một câu hỏi khác câu bạn đang hỏi Khi cần biết một nhóm tiến trình ăn bao nhiêu RAM, RSS là con số sai — và nó sai theo cách nguy hiểm: hai thí nghiệm khác nhau đúng một dòng (con đọc vs con ghi) cho RSS y hệt trong khi bộ nhớ thật chênh 8 lần. Lý do là copy-on-write của fork, và con số cộng-lại-được là PSS. Đây là vì sao cộng RSS của các worker prefork luôn vượt xa RAM máy. 01/09/2026 · 8 phút đọc 12 Đọc ngẫu nhiên qua bộ đệm trang nhanh gấp 15 lần đi thẳng xuống NVMe — và một máy chủ khoẻ mạnh luôn có RAM trống gần bằng 0, đó là điều đúng đắn chứ không phải triệu chứng Bộ đệm trang là thứ làm 'available' khác 'free'. Đo thật: cùng lần đọc 4 KB ngẫu nhiên, qua bộ đệm 425.874 IOPS còn O_DIRECT chỉ 28.655 — 15 lần. Và bộ đệm ăn thêm nửa gigabyte trong khi RAM khả dụng gần như không đổi, vì trang bộ đệm sạch vứt đi lúc nào cũng được. RAM trống là RAM lãng phí. Cộng thêm một sai lầm đo lường: drop_caches không chạm tới máy chủ bên dưới. 01/09/2026 · 8 phút đọc 13 dd báo ghi xong 512 MB sau 0,15 giây ở 3,6 GB/s — năm giây sau, toàn bộ dữ liệu vẫn nằm nguyên trong RAM và chưa một byte nào chạm đĩa Ở chiều ghi, lời hứa 'đã xong' của nhân là lời hứa suông cho tới khi dữ liệu thật sự xuống đĩa. Đo thật: dd trả về tức thì trong khi 512 MB còn kẹt trong page cache 5–10 giây nữa — cúp điện lúc đó là mất sạch dù ứng dụng đã được báo thành công. Đây cũng là lý do đo tốc độ ghi bằng một lần ghi ngắn không fsync chỉ đo tốc độ của RAM, và luôn cho con số đẹp đến khó tin. 01/09/2026 · 8 phút đọc 14 Cùng một lời gọi fsync, cùng một ổ đĩa: 750.000 IOPS trên RAM, 686 khi bắt nó thật sự bền — hơn một nghìn lần chênh nhau, và bạn chọn được mình đứng đâu fsync là chỗ trả tiền cho lời hứa của write. Tôi đo nó trên bốn đích đến (tmpfs, ext4 tắt/bật rào chắn, overlay container) và thấy khoảng cách nghìn lần; rồi cho thấy một đòn bẩy trong tay ứng dụng — dồn ghi trước khi fsync — kéo thông lượng lên 79 lần. Đây là cùng cơ chế commit theo nhóm mà mọi cơ sở dữ liệu đều dùng. 01/09/2026 · 7 phút đọc 15 Vượt giới hạn bộ nhớ đúng 17% làm giá mỗi trang nhảy từ 0,01 lên 4,82 µs — gần 500 lần, không có bậc trung gian; và cái bẫy thật của swap là nó giữ tiến trình sống thoi thóp thay vì cho chết hẳn Có nên tắt swap không — câu này được tranh cãi nhiều hơn được đo. Đo thật: không swap thì vượt giới hạn là bị giết ngay; có swap thì tiến trình sống, health check vẫn 200, dashboard vẫn xanh, mà mỗi yêu cầu chậm 500 lần. Không cảnh báo nhị phân nào bắt được trạng thái đó. Đây là lý do Kubernetes mặc định đòi tắt swap — và vì sao vẫn nên giữ nó với swappiness thấp. 01/09/2026 · 8 phút đọc 16 Tiến trình làm tràn bộ nhớ sống sót và giữ trọn 300 MB nó vừa xin — kẻ bị giết là một tiến trình khác hoàn toàn vô can, và đây là lý do log OOM luôn trỏ nhầm thủ phạm Container 700 MB, một tác vụ xin thêm 300 MB làm tràn giới hạn. Ai chết? Không phải nó — mà là tiến trình lớn nhất tại thời điểm đó. OOM killer không trừng phạt kẻ gây họa, nó ném thứ nặng nhất xuống để nổi thuyền. Cộng thêm: oom_score gần như vô dụng trong container, và đưa một tiến trình nhỏ lên đầu danh sách hy sinh chỉ làm mất hai tiến trình thay vì một. 01/09/2026 · 8 phút đọc 17 Trang 2 MB làm chương trình nhanh 1,8 lần — và làm một chương trình chỉ dùng 256 byte chiếm 447 MB; cùng một tính năng, hai mặt đối nghịch tới 219 lần Dùng trang 2 MB thay 4 KB cắt số lỗi trang và trượt TLB xuống 512 lần — nhưng đo thật cho thấy vùng càng lớn lợi càng nhỏ (ở 1 GB nhân chỉ gom được 47%), và chạm 1 byte trong mỗi vùng 2 MB làm 256 byte dữ liệu ngốn 447 MB. Đây là lý do Redis và MongoDB in cảnh báo khởi động đòi tắt trang lớn trong suốt. 01/09/2026 · 9 phút đọc 18 Chương trình giải phóng sạch 400.000 khối, hệ điều hành vẫn thấy nó ôm 410 MB — một lời gọi malloc_trim trả lại 95 lần, và chênh 1 KB kích thước khối quyết định 200 MB có về hay không Phần lớn 'rò rỉ bộ nhớ' được báo trong dịch vụ chạy lâu ngày thực ra không rò rỉ gì: bộ nhớ đã được free đúng cách, chỉ nằm lại trong bộ cấp phát của tiến trình thay vì quay về nhân. Đo thật cho thấy một ngưỡng 128 KB khiến chênh đúng 1 KB kích thước khối tạo khác biệt 175 lần, và cách phân biệt rò rỉ thật với phân mảnh trong vài dòng mã. 01/09/2026 · 9 phút đọc 19 'Đọc tuần tự nhanh hơn ngẫu nhiên' — đo trên SSD thì hai kiểu cho cùng một con số; cái quy tắc ba mươi năm ấy chết cùng cây kim đĩa than, và thứ thật sự quyết định là độ sâu hàng đợi Quy tắc kinh điển của thiết kế lưu trữ hoá ra là quy tắc của đĩa quay: nó xoay quanh cây kim phải nhấc sang rãnh khác. Trên NVMe không có bộ phận nào chuyển động, nên tuần tự và ngẫu nhiên đo ra bằng nhau. Cái đòn bẩy thật bây giờ là số yêu cầu đang bay cùng lúc — đo được 8,6 lần thông lượng — và có một điểm bão hoà mà vượt qua chỉ làm mọi thứ chậm đi. 01/09/2026 · 8 phút đọc 20 Đổi bộ lập lịch I/O — lời khuyên tinh chỉnh phổ biến nhất — cho đúng 0%: chín lần đo ba bộ lập lịch không cái nào tách khỏi nhiễu; nút thật sự cho 10,7 lần thì gần như không ai đụng tới Ai cũng khuyên đổi bộ lập lịch I/O cho nhanh. Đo thật trên NVMe: none, mq-deadline, kyber cho cùng một dải IOPS và p99, thứ hạng còn đổi giữa các lần chạy — vì bài toán chúng sinh ra để giải đã biến mất cùng đĩa quay. Trong khi đó read_ahead_kb, cái nút gần như không ai chỉnh, tạo chênh lệch 10,7 lần. Bạn đang chỉnh chỗ sáng, không phải chỗ hỏng. 01/09/2026 · 8 phút đọc 21 Đọc 512 byte tốn đúng bằng đọc 16 KB — ba mươi hai lần dữ liệu, cùng một cái giá; còn căn chỉnh sai với O_DIRECT thì không làm chậm, nó khiến lời gọi hỏng thẳng Hai câu hỏi hay bị gộp làm một — đọc mỗi lần bao nhiêu byte, và có cần căn chỉnh không — hoá ra là hai câu chuyện khác hẳn. Cái giá của một thao tác I/O nằm ở chính thao tác đó (~35 µs cố định), không ở số byte, nên khối dưới 16 KB là tiền vứt đi. Còn O_DIRECT thì căn chỉnh sai không chậm mà trả EINVAL — và ngưỡng là 512 hay 4096 tuỳ phần cứng, một lớp lỗi chỉ lộ khi đổi ổ. 01/09/2026 · 9 phút đọc 22 Đo một lần, ext4 nhanh gấp 3,2 lần xfs — đo ba lần, con số đó bốc hơi thành nhiễu; bốn phép đo cho bốn thứ hạng khác nhau, và không hệ tệp nào thắng cả bốn Ba hệ tệp, cùng một máy, cùng một bộ phép đo. Lần chạy đầu tiên cho một tỷ lệ đẹp, dễ nhớ, dễ giật tít — và sai hoàn toàn: chạy lại ba lần thì hai dải trùm lên nhau. btrfs kém rõ ở fsync vì sao chép khi ghi, ext4 ổn định nhất nên vẫn là mặc định cho cơ sở dữ liệu, còn câu hỏi 'hệ tệp nào nhanh hơn' thì không có câu trả lời — phải hỏi nhanh hơn ở việc gì. 01/09/2026 · 8 phút đọc 23 ext4 báo 'hết ổ đĩa' khi vẫn còn trống 71% — nó hết một thứ khác mà df -h không hề nhắc tới, và đây là một trong những sự cố khó chẩn đoán nhất trên máy chủ Tạo tệp rỗng trên ext4 tới khi No space left on device — mà dung lượng còn dư 71%. Thủ phạm là inode: ext4 chốt cứng số lượng lúc mkfs, hết inode là hết tệp bất kể còn bao nhiêu GB. Cộng thêm bảng đo chi phí stat theo tầng liên kết mềm, trần chuỗi đúng 40, và một sai lầm đo lường tôi tự vấp: stat không đi theo liên kết. 01/09/2026 · 9 phút đọc 24 Đọc 64 byte từ một tệp đã nằm sẵn trong RAM: pread mất 1.019 ns, mmap mất 19 ns — 53 lần, mà không bên nào chạm đĩa; toàn bộ khác biệt là cái giá đi vào nhân mmap cho phép đọc một tệp như đọc một mảng trong bộ nhớ. Đo thật: với nhiều lần đọc nhỏ ngẫu nhiên trên tệp nóng, mmap nhanh 53 lần vì đọc một byte là đúng một lệnh máy, không lời gọi hệ thống nào. Nhưng nó không phải mặc định — lỗi I/O đến dưới dạng SIGBUS giữa một lệnh truy cập trông vô hại, và đó là lý do SQLite lẫn PostgreSQL từ chối dùng nó cho tệp dữ liệu chính. 01/09/2026 · 9 phút đọc 25 Bật io_uring lên mà không đổi gì khác thì chậm hơn pread 20% — bật đúng chế độ gom, nó nhanh gấp 10 lần; đây không phải công tắc, nó là một cơ chế bạn phải biết dùng io_uring được giới thiệu như bước nhảy của I/O trên Linux, nhưng đo thật cho thấy ở cấu hình mặc định nó tốn đúng bằng pread, và ở độ sâu 1 còn chậm hơn. Số lời gọi hệ thống chỉ tụt 32 lần khi bạn bảo nó gom — cái vòng chung cho phép gom chứ không tự gom. Đổi một dòng cấu hình rồi báo 'đã nâng cấp lên io_uring' là làm hệ thống chậm đi 20%. 01/09/2026 · 8 phút đọc 26 Bộ đệm gửi mặc định chỉ 46 KB nhưng nhanh bằng hoặc hơn mọi giá trị bạn đặt tay — và đặt một con số 'an toàn' 64 KB làm chậm gần ba lần vì nó tắt cơ chế tự điều chỉnh của nhân Hai nút mạng người ta hay chỉnh, đo thật cả hai. Kích thước mỗi lần send tạo chênh lệch 36 lần và gãy rõ ở 64 KB — đúng chuyện chi phí lời gọi hệ thống. Còn SO_SNDBUF thì ngược đời: đặt tay làm chậm đi, vì bạn khoá cứng một con số trong khi nhân đang tự co giãn theo mạng thật. Để yên là lựa chọn đúng gần như mọi lúc. 01/09/2026 · 9 phút đọc 27 Mở kết nối mới rồi hỏi một câu tốn đúng hai vòng khứ hồi — hỏi cùng câu đó trên kết nối có sẵn tốn một; giữa hai châu lục, khác biệt là 21 mili giây mỗi lần gọi 'Dùng bể kết nối' là lời khuyên ai cũng biết — bài này đo nó đáng bao nhiêu, bằng đơn vị duy nhất mang đi được: vòng khứ hồi. Đo thật cho ba con số rơi đúng vào bội số nguyên của RTT. Trên máy local nó chỉ tiết kiệm một phần tư mili giây, nên không đánh giá nổi ở đó; trong sản phẩm khác vùng, một dịch vụ gọi 20 lượt xuống DB cộng thêm 420 ms chỉ vì không tái dùng kết nối. 01/09/2026 · 9 phút đọc 28 Ghi một thông điệp thành hai lần thay vì một làm gấp đôi độ trễ — nhưng chỉ trên mạng thật; trên máy lập trình viên lỗi này vô hình hoàn toàn Hai thuật toán TCP, mỗi cái hợp lý khi đứng riêng — Nagle giữ gói nhỏ chờ ACK, delayed ACK giữ ACK chờ dữ liệu — gặp nhau trong mẫu ghi–ghi–đọc thì khoá chéo nhau như hai người quá lịch sự ở cửa. Đo thật: trên cùng máy bốn cấu hình giống hệt, thêm 20 ms độ trễ thì một dòng gấp đôi ba dòng kia. Độ trễ mạng không làm lộ lỗi có sẵn — nó là điều kiện để lỗi tồn tại. 01/09/2026 · 9 phút đọc 29 Thêm 20 ms độ trễ mà không mất gói nào: 10.403 tụt còn 150 MB/s — rồi làm mất đúng 1% số gói, nó về 4 MB/s; mất gói không lấy đi 1% dữ liệu, nó đánh sập cửa sổ tắc nghẽn Cùng máy, cùng phần mềm, chỉ đổi đường truyền bằng tc netem. Độ trễ một mình lấy đi 69 lần thông lượng vì TCP phải chờ ACK sau mỗi cửa sổ. Rồi mất một phần nghìn số gói làm mọi thứ chậm 40 lần — vì CUBIC cắt đôi cửa sổ mỗi lần mất gói và không kịp lớn lại. Câu 'chỉ mất có 1% thôi' trong báo cáo sự cố nguy hiểm hơn nó nghe. 01/09/2026 · 9 phút đọc 30 Cạn sạch 28.232 cổng chỉ trong 22 giây — rồi thêm đúng một dòng recv() làm 'vấn đề biến mất', theo cái cách tệ nhất: đổi lỗi cạn cổng lấy lỗi mất dữ liệu im lặng Một vòng lặp mở-đóng kết nối tới cùng một đích cạn hết dải cổng phù du trong 22 giây, vì mỗi cổng bị giữ ở TIME_WAIT 60 giây — tiêu nhanh gấp ba lần được trả lại. Ngưỡng thật chỉ ~470 kết nối/giây, thấp hơn trực giác nhiều. Và một dòng mã tưởng như chữa được hoá ra chỉ thay RST vào FIN: một cái bẫy đo lường kinh điển. 01/09/2026 · 10 phút đọc 31 Một lần tra tên mất 24 giây trong pod Kubernetes — thêm đúng một dấu chấm ở cuối tên, còn 1,2 mili giây; hai mươi nghìn lần nhanh hơn chỉ bằng một ký tự getaddrinfo trên IP mất 2,2 µs; trên một tên không tồn tại mất 1.046 lần thế. Rồi cấu hình ndots:5 mặc định của Kubernetes biến một lần tra thành sáu câu hỏi, và trong môi trường tôi đo, năm câu đầu im lặng chờ hết hạn — cộng lại 24 giây. Dấu chấm cuối bảo trình phân giải 'đây là tên tuyệt đối, đừng đoán'. Cộng thêm cái bẫy JVM cache DNS vĩnh viễn. 01/09/2026 · 9 phút đọc 32 Bắt tay TLS thêm 995 µs, và người ta bảo đó là 'một vòng khứ hồi' — tôi đo ra nó tốn CPU gấp 33 lần một vòng mạng TLS 1.3 hay được mô tả là 'thêm một vòng khứ hồi'. Tôi đo cùng máy, cùng chương trình, chỉ khác cổng: bắt tay thêm 995 µs còn mỗi vòng sau đó chỉ thêm 18 µs. Và 995 µs ấy gần như toàn bộ là CPU — chữ ký RSA, không phải độ trễ mạng. Đó đổi hẳn cách bạn tối ưu HTTPS. 01/09/2026 · 9 phút đọc 33 Cách chữa 'Too many open files' được nhắc nhiều nhất — ulimit -n 65536 — thường không làm điều bạn nghĩ: giới hạn mềm ai cũng tự nâng, và nâng nó lên có khi làm chương trình hỏng nặng hơn Giới hạn mềm không phải một hàng rào, nó là giá trị khởi đầu — tiến trình tự nâng lên tới giới hạn cứng mà không cần quyền gì (đo được: gấp 1.027 lần). Một triệu mô tả tệp chỉ tốn 38 MB nên đặt thấp để 'tiết kiệm bộ nhớ' là vô nghĩa. Và cú bất ngờ nhất: nâng ulimit khiến một dịch vụ dùng select() hỏng ở tận đâu đâu, hàng giờ sau, chỉ khi tải cao. 01/09/2026 · 9 phút đọc 34 nproc bên trong container báo 16 nhân trong khi quota chỉ cho chạy bằng 1 — và vì tin lời nó, runtime tạo 32 luồng, làm chậm 44% và giãn đuôi trễ gấp đôi cgroup không giới hạn bạn thấy bao nhiêu nhân, nó giới hạn bạn được chạy bao nhiêu thời gian. Nhưng nginx, Go, Node, JVM cũ đều tự chọn số worker theo nproc — và với quota 1 nhân, chúng đẻ ra 16. Đo thật: 32 luồng cho 1 nhân làm p99 xấu gấp đôi. Cộng thêm cái bẫy bộ nhớ: bộ đệm trang bẩn khiến OOM dù anon gần bằng 0. 01/09/2026 · 9 phút đọc 35 Bảng điều khiển báo CPU 62,6% — 'còn 37% dư địa' — trong khi p99 đã 113 ms và tiến trình bị ghim 60 lần: con số trung bình mù tịt với điều tiết Tôi dựng lại đúng cảnh 'ứng dụng chậm mà CPU chỉ 60%' và đo: ở 62,6% CPU trung bình, tiến trình đã bị throttle 60 lần và p99 nhảy lên 113 ms. Lý do là quota tính theo từng chu kỳ 100 ms chứ không theo phút — nên p99 chênh tới 44 lần trong khi CPU y hệt nhau, và chữ ký của nó là các yêu cầu chậm dồn đúng bội số 100 ms. 01/09/2026 · 8 phút đọc 36 Bốn loại không gian tên cộng lại chỉ tốn 93 µs — riêng cái mạng ngốn 540 µs, gấp gần sáu lần cả bốn cái kia; và tháo nó ra còn lâu hơn dựng nó lên Không phải mọi vách ngăn trong container đều đắt ngang nhau. Đổi tên máy, đánh số lại bảng tiến trình gần như miễn phí; nhưng dựng riêng một ngăn xếp mạng — bảng định tuyến, netfilter, veth — tốn 540 µs mỗi lần tạo và 1,7 lần thông lượng mỗi gói tin. Nếu tác vụ không cần cách ly mạng, đừng trả cái giá đó. 01/09/2026 · 9 phút đọc 37 strace -c làm chương trình chậm 446 lần dù nó không in ra một dòng nào — cái giá không nằm ở việc ghi chép, mà ở chỗ mỗi lời gọi phải dừng lại bốn lần Ai cũng nghĩ 'chỉ đếm thôi' thì nhẹ hơn 'in đầy đủ'. Đo thật: strace -c chậm 446 lần, bản in đầy đủ 501 lần — chênh vỏn vẹn 12%, vì tiền không đi vào chỗ bạn tưởng. Mà trên chương trình không gọi hệ thống thì strace lại miễn phí. Chi phí tính theo số lời gọi, không theo thời gian chạy. 01/09/2026 · 8 phút đọc 38 perf báo 62,65% cho hàm thật sự chiếm 62,50% — sai đúng 0,15 điểm mà không quấy rầy chương trình một nhịp nào; đây là điều tra chọn mẫu áp vào code strace làm chậm 446 lần vì nó chặn từng lời gọi. perf đứng ở đầu kia của thang: nó chỉ thỉnh thoảng ngắt ra ghi con trỏ lệnh đang ở đâu, rồi đếm — chi phí không đo được, mà bốn hàm vẫn xếp hạng đúng tới 0,15 điểm phần trăm. Cùng logic thăm dò ý kiến: đủ mẫu ngẫu nhiên là tỷ lệ hội tụ về sự thật. 01/09/2026 · 9 phút đọc 39 Hồ sơ phẳng nói '99,90% CPU nằm trong hàm tinh()' — chính xác tới 0,15 điểm, và hoàn toàn vô dụng; cùng số liệu ấy tách theo đường gọi thì chỉ thẳng vào chỗ phải sửa Một con số đúng vẫn có thể chẳng giúp được gì. Hồ sơ phẳng bảo toàn bộ CPU nằm trong một hàm tiện ích gọi từ khắp nơi — biết tên mà không biết ai gọi. Tách cùng số liệu ấy theo ngăn xếp: nén ngốn 85%, và một hàm nen() hiện ra hai lần với hai tỷ lệ khác nhau tuỳ ai gọi nó. 01/09/2026 · 9 phút đọc 40 Công cụ này chỉ chậm 2 lần thay vì 446 lần, thấy cả máy chứ không phải một tiến trình, và moi ra được lần gọi chậm gấp nghìn lần trung vị mà trung bình, p99 đều giấu strace muốn nhìn phải dừng tiến trình rồi đọc thanh ghi từ bên ngoài — chậm 446 lần. eBPF nạp thẳng chương trình đo vào trong nhân, chạy tại chỗ. Kết quả: 2 lần chi phí, thấy cả máy trong một lần đo, và một cái đuôi 128–256 micro giây mà không trung bình hay phân vị nào bắt được. 01/09/2026 · 10 phút đọc 41 Định dạng một dòng log gần như miễn phí — fsync mỗi dòng đắt gấp 11.464 lần, và một dòng DEBUG đã tắt vẫn ngốn 62 lần chi phí Ghi log nằm trên đường đi của mọi yêu cầu. Tôi đo sáu cách ghi cùng một dòng: định dạng chuỗi tốn 0,052 µs, còn fsync mỗi dòng tốn 515,903 µs — chi phí không nằm ở chỗ ai cũng tối ưu (định dạng) mà ở mỗi lần dữ liệu *ra khỏi tiến trình*. Kèm cái bẫy đắt nhất: một log.debug đã tắt vẫn dựng chuỗi trước khi bị bỏ, tốn 62 lần so với truyền tham số rời. 01/09/2026 · 8 phút đọc 42 Cùng một giá trị giờ, cùng một nguồn: 13,59 ns khi qua vDSO, 154,77 ns khi gọi syscall thẳng — chênh 11 lần, và clock_getres nói dối 24.000 lần Lấy giờ là thứ mọi log, mọi số liệu, mọi phép đo đều làm. Tôi đo chín cách và tìm ra chênh lệch 11 lần giữa hai cách gọi cùng một hàm (vDSO hay không), một biến thể COARSE rẻ 6 lần đổi lấy độ phân giải 1 ms, và một hàm — clock_getres — báo sai độ phân giải thật tới 24.000 lần. Kèm lỗi kinh điển: dùng REALTIME cho thời hạn. 01/09/2026 · 8 phút đọc 43 Đọc 100 MB từ /dev/random mất 138 ms và entropy_avail không giảm một bit — cái bẫy 'random sẽ chặn' đã chết từ nhân 5.6 'Đừng dùng /dev/random, nó sẽ chặn' là một trong những lời khuyên Linux được lặp nhiều nhất — và sai từ nhân 5.6. Tôi đo: /dev/random và /dev/urandom cho cùng 780 MB/s, entropy không tiêu hao, còn rand() nhanh gấp mười ở 16 byte nhưng CHẬM nhất ở 4 KB và không đoán nổi ai dùng nó cho bảo mật. Lập luận 'dùng RNG yếu cho nhanh' không còn cơ sở đo lường. 01/09/2026 · 8 phút đọc 44 Cấu hình container dừng nhanh nhất — 113 ms — lại là cấu hình duy nhất mất dữ liệu: nó bỏ qua sạch bước dọn dẹp Mọi container bị dừng thường xuyên hơn bị lỗi. Tôi đo sáu cách đóng gói cùng một ứng dụng: cái dừng trong 113 ms trông đẹp nhất trên bảng điều khiển nhưng là cái duy nhất không chạy dọn dẹp. Dừng nhanh không phải dừng êm — chỉ số đúng là mã thoát 0 cộng bằng chứng trong log, và hai lỗi đóng gói khác nhau cho cùng một triệu chứng. 01/09/2026 · 8 phút đọc 45 4.997 zombie cạn sạch giới hạn PID của container trong 0,17 giây — chúng không tốn byte RAM nào, chỉ giữ đúng một thứ: số hiệu tiến trình Zombie không phải lỗi của tiến trình con mà của cha không đọc mã thoát. Tôi đo: một vòng waitpid dọn sạch 200 con, còn fork không thu hồi thì cạn 5.000 PID trong 0,17 giây và mọi tiến trình trong container nhận EAGAIN — triệu chứng là 'không tạo được tiến trình' trong khi RAM và CPU vẫn bình thường, đúng chỗ người trực đi tìm sai hướng. 01/09/2026 · 8 phút đọc 46 fork() ở RSS 1 GB chậm gấp 20 lần khi RSS gần 0 — copy-on-write vẫn phải sao bảng trang, và Transparent Huge Page giấu 16 lần chi phí đó fork() không copy dữ liệu nhờ copy-on-write, ai cũng biết — nhưng nó vẫn sao bảng trang, và chi phí đó tỉ lệ với RSS của cha. Tôi đo fork/vfork/posix_spawn khi cha giữ tới 1 GB: fork lên 3.850 µs còn vfork/posix_spawn phẳng lì, và tắt THP làm fork chậm thêm 9,3 lần. Vì sao THP che gần hết chi phí, đo bằng con số. 01/09/2026 · 12 phút đọc 47 Chi phí một lời gọi hệ thống close(-1) không làm gì cả và vẫn tốn 128 ns. Đó là sàn, và nó gấp 174 lần một lời gọi hàm. 01/09/2026 · 6 phút đọc 48 Máy ảo, container và chạy thẳng Ảo hoá làm CPU chậm 1,05 lần và I/O chậm 11 lần. Và lần đo đầu tiên của tôi đổ lỗi nhầm cho ảo hoá. 01/09/2026 · 6 phút đọc 49 Gỡ lỗi khi máy chậm docker stats báo CPU 100% và câu trả lời không phải là cấp thêm nhân. Bốn lệnh tìm ra nguyên nhân thật. 01/09/2026 · 6 phút đọc 50 Danh sách kiểm hiệu năng Linux Chốt sê-ri 50 phần: mọi con số đáng nhớ, và bảy bài học về cách đo mà tôi học được bằng cách đo sai. 01/09/2026 · 7 phút đọc