ZUKATEK JOURNAL

Tối ưu server: đo trước, sửa sau — và những con số thật từ một VPS 3GB

Chúng tôi đo toàn bộ đường đi của một yêu cầu, từ trình duyệt tới cơ sở dữ liệu. Máy chủ trả lời trong 3 mili giây, người dùng chờ 130. Bài này chỉ ra cách tách thời gian xử lý, đường truyền và tải tài nguyên.

Câu hỏi "làm sao cho web nhanh hơn" gần như luôn được trả lời sai, vì nó được trả lời trước khi đo. Người ta nâng CPU, thêm RAM, đổi sang máy chủ mạnh hơn — rồi phát hiện trang vẫn chậm y như cũ.

Bài này kể lại đúng những gì chúng tôi đo được trên hệ thống của mình, theo đúng thứ tự đã đo, kèm con số.

Hệ thống được đo

Bản thảo gốc ghi các số liệu dưới đây vào ngày 18/10/2026. Chưa có log gốc trong thư mục để xác minh độc lập. Đây là quan sát ở một cấu hình, không phải benchmark đại diện; cần đo lại dưới tải thực tế.

  • Một VPS: 4 nhân, 2.9 GB RAM, đặt tại Việt Nam

  • Trên đó: trang web công khai dựng phía máy chủ (Node), màn hình quản trị (tệp tĩnh), API (Go), PostgreSQL 16

  • Và một ứng dụng của một công ty khác chạy chung máy

Bước 1: đo từ trong ra ngoài, không đo từ ngoài vào

Sai lầm thường gặp là mở trang web, thấy chậm, rồi kết luận "server yếu". Phép đo đó gộp chung bốn thứ: máy chủ, đường truyền, CDN, và cân nặng trang. Phải tách ra.

Đo ngay trên máy chủ, qua loopback — loại bỏ hoàn toàn mạng:

SSR nội bộ:   3.1 ms   3.3 ms   2.9 ms
API nội bộ /members:       5.1 ms
API nội bộ /menus:         4.3 ms
API nội bộ /settings:      3.7 ms
Tải trung bình:            0.06
RAM còn trống:             1.9 GB

Ở các mẫu này, chưa thấy dấu hiệu máy chủ là nút thắt. Load average 0.06 không có nghĩa CPU rảnh 99%. Cần thêm CPU, I/O, bộ nhớ, số yêu cầu đồng thời và p95/p99 dưới tải trước khi kết luận về năng lực máy.

Nếu bạn chỉ làm một việc trong bài này, hãy làm phép đo trên. Nó mất ba phút và nó quyết định toàn bộ phần còn lại.

Bước 2: đo từ ngoài, cùng một thứ

Từ máy người dùng (Việt Nam), nối thẳng:
  bắt tay TLS:   0.06–0.08 s
  TTFB:          0.08–0.10 s
  tổng:          0.10–0.13 s

Không nên lấy tổng thời gian tải bên ngoài trừ thời gian phản hồi nội bộ rồi gọi phần dư là độ trễ mạng. Hãy đo cùng URL, cùng nội dung và tách DNS, TCP, TLS, TTFB, tải body; đo nhiều lần với cache và kết nối được kiểm soát.

Bước 3: cân nặng trang — nơi tiền thật sự nằm

Đây là chỗ chúng tôi tìm ra vấn đề lớn nhất, và nó không liên quan gì tới máy chủ:

Tài sản

Gốc

Sau nén

entry-client.js

482 KB

152 KB

esm.js

318 KB

—

CSS

135 KB

30 KB

Ảnh đại diện một người

697 KB

516 KB (WebP)

logo.png

140 KB

—

favicon.png

49 KB

—

18 tệp phông chữ

290 KB

—

Ảnh chân dung này nặng hơn tệp entry-client.js sau nén. Chưa đủ dữ liệu để kết luận về tổng JavaScript của trang. 516 KB cho một khung hiển thị rộng khoảng 320 điểm ảnh.

Tệ hơn: khi chúng tôi thử yêu cầu bản nhỏ bằng tham số ?w=320, máy chủ bỏ qua tham số và vẫn trả ảnh gốc. Nghĩa là có sẵn cơ chế thay đổi kích thước nhưng nó không hoạt động — loại lỗi chỉ lộ ra khi đo, vì trang vẫn hiển thị đúng.

Bài học

Thứ tự tối ưu theo mức hiệu quả trên công sức, với một trang web điển hình:

  1. Ảnh — gần như luôn là thứ nặng nhất, và gần như luôn dễ sửa nhất

  2. Nén — kiểm tra xem máy chủ có thật sự nén không, đừng tin cấu hình

  3. Cache ở biên cho tệp tĩnh

  4. Mã JavaScript — khó và lâu nhất, để sau cùng

  5. Phần cứng — khi số đo cho thấy CPU, RAM hoặc I/O là giới hạn

Bước 4: kiểm tra nén có thật sự chạy không

Cấu hình gzip on; trong nginx không có nghĩa là nó đang nén thứ bạn nghĩ. text/html luôn được module gzip hỗ trợ; muốn nén CSS và JavaScript, cần khai báo MIME type phù hợp trong gzip_types.

Đừng đọc file cấu hình rồi kết luận. Hãy đo.

Cách kiểm tra đúng là hỏi như một trình duyệt thật và nhìn số byte tải về:

curl -s -o /dev/null --compressed -w '%{size_download}' https://ten-mien/

Đối chiếu thêm header Content-Encoding và cùng một response không nén. Số byte nhỏ hơn chưa tự chứng minh thuật toán nén nào được dùng. Bản thảo ghi HTML khoảng 91 KB và body truyền về khoảng 21 KB; chưa có header gốc để xác nhận zstd.

Bước 5: cẩn thận với phép đo tự lừa mình

Đây là sai lầm tôi đã mắc trong chính lần đo này, và nó đáng kể lại.

Lần đầu tôi đo tốc độ qua CDN bằng cách chạy curl từ chính máy chủ. Kết quả: 348–838 mili giây. Tôi suýt kết luận CDN làm chậm gấp tám lần.

Nhưng phép đo đó sai về bản chất: yêu cầu đi từ máy chủ → ra Internet → tới CDN → quay ngược lại chính máy chủ đó. Nó đo một đường vòng mà không người dùng nào đi.

Đo lại từ một máy người dùng thật ở Việt Nam, bản thảo ghi 0.31–0.49 giây. Tuy nhiên hai đường khi đó đo hai ứng dụng khác nhau; chưa thể dùng chênh lệch này để định lượng chi phí của CDN.

Nguyên tắc: đo ở đúng vị trí người dùng đứng. Một phép đo tiện tay thường là một phép đo sai.

Một yêu cầu đi qua đâu — và đo được gì ở mỗi chặngTrình duyệt → mạngĐo từ máy người dùng thật100–130 msnginxChuyển tiếpdưới 1 msỨng dụng (SSR / API)Đo trên loopback3 msPostgreSQLĐã nằm trong con số phía trêndưới 2 msTải tài sản tĩnhẢnh, JavaScript, phông chữchưa đo riêngMáy chủ trả lời 3 ms trên loopback; người dùng chờ 100–130 ms. Đây là haiphép đo khác điều kiện — phải đo riêng đường truyền và tài sản tĩnhtrước khi quy thời gian chờ cho một nguyên nhân.

Bước 6: những thứ không nằm trong tầm kiểm soát

Có những nút thắt bạn đo ra nhưng không sửa được, và biết sớm thì đỡ mất thời gian. Chúng tôi gặp một cái rất lạ: kết nối cổng 80 từ một số mạng quốc tế không tới được máy.

Triệu chứng là chứng chỉ SSL không cấp được, báo "connection reset". Mọi dấu hiệu đều chỉ về phía cấu hình máy chủ. Nhưng bắt gói tin cho thấy sự thật:

Từ mạng nước ngoài gọi vào cổng 80:  KHÔNG một gói SYN nào tới máy
Từ mạng trong nước gọi vào cổng 80:  SYN tới, máy trả SYN-ACK

Gói tin bị chặn trước khi tới máy. Không cấu hình nào trên máy sửa được điều đó.

Bài học ở đây không phải về cổng 80. Là về phép đối chứng: nếu tôi chỉ chạy phép đo từ nước ngoài và thấy không có gói nào, tôi có thể kết luận bộ lọc gói tin của mình hỏng. Chính phép đo đối chứng từ trong nước — thấy gói tin tới bình thường — giúp khoanh vùng vấn đề có thể nằm ở đường đi hoặc bộ lọc phía trước máy. Chưa đủ bằng chứng để khẳng định nhà cung cấp chủ động chặn cổng.

Mọi phép đo cho kết quả "không có gì" đều cần một phép đối chứng cho kết quả "có". Nếu không, bạn không phân biệt được "không có gì xảy ra" với "dụng cụ đo hỏng".

Danh sách kiểm tra

Chạy theo thứ tự này, dừng lại ngay khi tìm ra nút thắt:

  1. Đo trên loopback — máy chủ có chậm không? (thường là không)

  2. Đo từ máy người dùng thật — chênh lệch nằm ở đâu?

  3. Liệt kê tài sản tĩnh theo kích thước giảm dần — ảnh có nằm đầu không?

  4. Kiểm tra nén bằng --compressed, nhìn số byte

  5. Kiểm tra cache ở biên: header cf-cache-status hoặc tương đương

  6. Chỉ khi cả năm bước trên đều sạch, mới nghĩ tới phần cứng


Số liệu được giữ từ bản thảo về Zukatek, tháng 10/2026; chưa xác minh độc lập trong lần rà soát này. Con số của bạn sẽ khác. Hãy giữ phương pháp và lưu log của từng lần đo.

Nguồn đối chiếu: nginx gzip module.

BUILD SIMPLE. GROW BEYOND.

Cảm ơn bạn đã đọc và đồng hành cùng Zukatek.

← Trở về Blog