Có một loại việc rất dễ bắt đầu: thấy API chậm, thêm cache. Thấy database nặng, thêm index. Thấy hàng chờ dài, thêm worker. Mỗi việc đó đều có lúc đúng. Vấn đề là làm chúng trước khi biết chậm ở đâu thì hệ thống có thêm trạng thái, mà độ trễ người dùng nhìn thấy thì chưa chắc đổi.

Mình dùng bài này như một danh sách kiểm trước khi sửa. Không có con số của một hệ thống thật. Những ví dụ chỉ để có chỗ đặt câu hỏi. Nếu một mục chưa trả lời được, mình chưa tối ưu.

Đo cái người dùng đang chờLiên kết tới mục này

Câu đầu tiên nên là: chậm với ai, ở thao tác nào, chậm cỡ nào. “API thanh toán chậm” không đủ. Chậm lúc tạo giao dịch, lúc đọc lịch sử, hay lúc một job nền chạy xong? Trung bình 80ms mà cái đuôi 2 giây là một bệnh khác với trung bình 800ms. Hai bệnh đó không dùng cùng một cách sửa.

Mình muốn ít nhất một phân vị cao, không chỉ số trung bình. p95 hoặc p99 của đúng endpoint, trong đúng khoảng thời gian người ta phàn nàn. Kèm theo số lần gọi và tỉ lệ lỗi. Một endpoint bị gọi mười lần một ngày không cần cùng một buổi tối ưu với endpoint đứng trên đường đi của mọi phiên làm việc.

  • Tên thao tác và đường đi của nó, từ cửa API tới chỗ chạm database hoặc dịch vụ khác.
  • Độ trễ ở phân vị cao, không thay bằng cảm giác “hơi chậm”.
  • Tỉ lệ lỗi và timeout, vì một hệ thống nhanh mà hay thất bại thì client vẫn thử lại.
  • Một mốc để dừng: nhanh hơn bao nhiêu thì coi là đủ cho việc này.

Latency không phải một con sốLiên kết tới mục này

Độ trễ của một request là tổng của nhiều đoạn: chờ kết nối, chờ khoá, chờ query, chờ một dịch vụ bên cạnh, rồi mới tới thời gian tự thân của code. Tối ưu đoạn chiếm 5% tổng thời gian sẽ không đổi trải nghiệm, dù biểu đồ của đoạn đó đẹp đi. Mình thích nhìn một request chậm và hỏi đoạn nào nuốt phần lớn khoảng chờ.

Có đoạn không nằm trong process của mình. Một round-trip mạng trong cùng khu vực có thể nhỏ, nhưng một cuộc gọi tuần tự nối tiếp năm dịch vụ thì năm lần làm tròn cộng lại. Gộp cuộc gọi, chạy song song chỗ nào độc lập, hoặc bỏ hẳn một bước không cần trên đường nóng, thường đáng hơn là vi chỉnh một vòng lặp.

Database thường là chỗ đáng nghi đầu tiênLiên kết tới mục này

Với backend nghiệp vụ, database hay đứng trên đường nóng. Trước khi thêm cache, mình muốn biết query nào chạy, nó đọc bao nhiêu dòng, và nó có đang chờ khoá hay không. Một index đúng chỗ có thể rẻ hơn một tầng cache, vì cache kéo theo câu hỏi dữ liệu cũ đến mức nào là chấp nhận được.

Cũng đừng thêm index theo phản xạ. Mỗi index làm ghi chậm hơn và chiếm chỗ. Index có ích khi nó khớp điều kiện truy vấn thật sự xảy ra. Mình muốn thấy kế hoạch truy vấn của đúng câu lệnh đang chậm, với lượng dữ liệu gần với production hơn là một bảng mười dòng trên máy cá nhân.

Ngoài query, còn kích thước giao dịch. Giữ một transaction mở trong lúc gọi mạng là cách chắc chắn để khoá nằm lâu. Phần tính toán dài nên ở ngoài. Phần ghi nên ngắn và có điều kiện để biết lần ghi đó có phải lần ghi đầu hay không. Bài về idempotency nằm cạnh chuyện này: gửi lại request không được biến thành hai lần ghi.

Cache là một bản sao, không phải một phép tăng tốc miễn phíLiên kết tới mục này

Cache hợp lý khi một kết quả được đọc nhiều, đắt để tính, và chịu được độ cũ mà mình gọi tên được. “Chịu được năm giây” là một quyết định sản phẩm. “Xoá cache khi có gì đó đổi” là một cơ chế phải thiết kế, không phải một lời hứa trong comment.

Những chỗ mình ngại cache nhất là số liệu tiền và trạng thái giao dịch vừa ghi. Người dùng vừa thực hiện một thao tác thường cần đọc lại đúng thứ họ vừa làm, không phải một bản cách đó vài giây. Cache danh mục ít đổi thì dễ biện minh hơn cache số dư.

  • Key là gì, và hai request khác nhau có đang vô tình dùng chung một key không?
  • Hết hạn khi nào, và ai xoá khi dữ liệu nguồn đổi?
  • Cache chết thì sao. Đường đi gốc có chịu nổi lượng truy cập thật, hay cache đang che một query không sống được nếu đứng một mình?
Chỗ dành cho sơ đồ hàng đợi và cache

Bản mẫu chỉ giữ một khung minh hoạ. Khi nội dung được đưa vào CMS, block embed có thể được ánh xạ sang một tài nguyên đã kiểm duyệt. Trang này không tải iframe bên ngoài.

Mỗi lần làm nhanh hơn, một thứ khác đổi theoLiên kết tới mục này

Thêm bản sao thì đọc nhanh hơn và ghi phức tạp hơn. Thêm index thì đọc một kiểu query nhanh hơn và ghi chậm hơn. Thêm worker thì hàng chờ ngắn hơn cho đến khi database phía sau nghẽn. Thêm dịch vụ riêng thì một đội có thể triển khai độc lập, đồng thời một request có thêm một điểm để timeout.

Mình viết trade-off thành một câu trước khi sửa: mình đang đổi cái gì để lấy cái gì, và dấu hiệu nào cho biết cuộc đổi này thất bại. Nếu không có dấu hiệu thất bại, tối ưu không có cửa để dừng. Nó sẽ tiếp tục thành một tầng trừu tượng nữa.

Tối ưu là chọn một chi phí khác, không phải xoá chi phí. Chi phí mới cần một chỗ để nhìn thấy.

Một thứ tự mình đang dùngLiên kết tới mục này

  • Ghi lại thao tác chậm, phân vị, và mốc đủ tốt.
  • Tìm đoạn chiếm phần lớn thời gian chờ trên một request thật.
  • Xem query, khoá và các cuộc gọi nối tiếp trước khi thêm thành phần mới.
  • Nếu thêm cache hoặc hàng đợi, viết rõ độ cũ chấp nhận được và cách biết nó đang sai.
  • Đo lại đúng chỉ số ban đầu. Giữ thay đổi chỉ khi con số và độ phức tạp còn cân.

Không có bước nào trong danh sách này cần một công cụ đặc biệt. Cần một request cụ thể và sự sẵn sàng để không sửa đoạn code mình thích sửa nhất. Nhiều lần, kết quả của buổi tối ưu là xoá một cuộc gọi không cần, hoặc thêm một index đúng. Những lần đó hệ thống ít phần động hơn, và đó vẫn là một tối ưu.