99 Ngày Spring — Ngày 96: Performance Tuning
Tối ưu hiệu năng mà không đo là đoán mò có hệ thống. Với phần lớn ứng dụng Spring Boot, nút thắt nằm ở tầng dữ liệu và ở các lời gọi ra ngoài, không nằm ở framework. Bài này đi theo đúng thứ tự nên làm: đặt mục tiêu và đo, sửa pool kết nối và truy vấn, rồi mới tới JVM, và luôn kiểm chứng lại bằng load test.

1. Mục tiêu và số liệu trước tiên
Một mục tiêu hiệu năng phải đo được: ví dụ p99 dưới 300 mili giây ở 200 request mỗi giây với tỷ lệ lỗi dưới 1%. Dùng metric của ngày 93 để biết thời gian đang đi đâu: độ trễ theo endpoint, thời gian chờ lấy kết nối, thời gian GC. Sau đó chỉ thay đổi một thứ mỗi lần và so với mốc cũ. Tối ưu thứ không phải nút thắt chỉ làm code phức tạp hơn.
Đo ở phân vị cao; trung bình che mất vấn đề.
Tái hiện bằng tải giống thật trước khi sửa.
Giữ lại kết quả trước và sau cho mỗi thay đổi.
2. HikariCP: pool nhỏ hơn bạn nghĩ
Database chỉ xử lý song song được một số truy vấn có hạn theo số nhân CPU và ổ đĩa; kết nối vượt quá con số đó chỉ xếp hàng bên trong database. Vì vậy pool vài chục kết nối thường nhanh hơn pool vài trăm. Mặc định của HikariCP là 10. Tổng pool của mọi instance cộng lại không được vượt max_connections của database. Chỉ số cần theo dõi là số luồng đang chờ kết nối: nếu nó khác 0 thường xuyên, hãy tìm truy vấn chậm trước khi tăng pool.
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.connection-timeout=2000
spring.datasource.hikari.max-lifetime=1500000
# release connections before rendering the response
spring.jpa.open-in-view=false3. Nút thắt quen thuộc ở tầng dữ liệu
Trước khi chạm vào JVM, hãy đếm số câu SQL của một request. N+1 truy vấn, thiếu index, lấy cả bảng rồi lọc trong Java và giữ transaction mở trong lúc gọi API ngoài là bốn nguyên nhân gặp nhiều nhất. Open Session in View, bật mặc định, giữ kết nối suốt quá trình render response; tắt nó buộc bạn nạp dữ liệu có chủ đích trong tầng service.
Luôn phân trang các danh sách.
Dùng projection hoặc DTO khi không cần cả entity.
Cache thứ đọc nhiều, đổi ít, và đặt thời hạn hết hạn.
Mọi lời gọi ra ngoài có timeout; không gọi API ngoài bên trong transaction.
4. JVM và load test
Trong container, đặt heap theo tỷ lệ bộ nhớ bằng MaxRAMPercentage và giữ G1 mặc định cho tới khi GC log cho thấy lý do đổi. Bật virtual thread nếu ứng dụng chờ I/O nhiều. Cuối cùng, kiểm chứng bằng load test: tăng tải dần, giữ ổn định vài phút, và đặt ngưỡng đạt hoặc trượt ngay trong kịch bản. Chạy trên môi trường giống production về dữ liệu và cấu hình.
import http from "k6/http";
import { check } from "k6";
export const options = {
stages: [
{ duration: "1m", target: 50 }, // ramp up
{ duration: "3m", target: 50 }, // hold
],
thresholds: {
http_req_duration: ["p(99)<300"],
http_req_failed: ["rate<0.01"],
},
};
export default function () {
const res = http.get("http://localhost:8080/api/orders?page=0&size=20");
check(res, { "status is 200": (r) => r.status === 200 });
}Thứ tự đúng là đo, sửa tầng dữ liệu, rồi mới chỉnh JVM, và mỗi bước đều có số liệu trước sau. Ngày 97 siết lại phần bảo mật trước khi lên production.
Tài liệu đối chiếu
https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
https://docs.spring.io/spring-boot/reference/data/sql.html
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


