Chuyển tới nội dung chính

6 bài viết được gắn thẻ "Hiệu năng"

Đo trước khi tối ưu — bộ nhớ, độ trễ, thời gian build và những con số đo được.

Xem tất cả thẻ

Thêm 4 index làm INSERT chậm 6 lần: cái giá không ai nhắc khi bảo bạn đánh index

· 13 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Index tăng tốc đọc bằng cách trả giá ở mỗi lần ghi, và cái giá đó lớn hơn hầu hết người ta hình dung. Đo trên PostgreSQL 16 với cùng 200.000 dòng: bảng không index chèn xong trong 287,8 ms và chiếm 10,2 MB; bảng có bốn index mất 1.722,3 ms và chiếm 27 MB. Tức là chậm gấp 6 lần và tốn gấp 2,6 lần dung lượng — chỉ để thêm bốn cấu trúc mà có thể chẳng câu truy vấn nào dùng tới. Câu hỏi đúng không phải "cột này có nên có index không" mà là "phần đọc tiết kiệm được có bù nổi phần ghi phải trả không".

Mọi bài viết về tối ưu database đều kết thúc bằng lời khuyên thêm index. Rất ít bài nói về hoá đơn đi kèm, và càng ít bài đưa con số.

Bài này đào sâu bài Thiết kế database và bài Index trong series học SQL 30 ngày. Mọi số liệu đo thật trên PostgreSQL 16.11.

Có index rồi mà truy vấn vẫn quét toàn bảng? Sargability và một hàm bọc quanh cột

· 14 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Index là một cấu trúc sắp xếp theo giá trị của cột. Khi bạn bọc một hàm quanh cột — date_part('year', tao_luc), LOWER(email), CAST(...) — thì thứ bạn đang so sánh không còn là giá trị đã được sắp xếp nữa, nên database không dùng index được và phải quét toàn bảng. Điều kiện dùng được index gọi là sargable. Đo trên PostgreSQL 16 với 200.000 dòng: bản bọc hàm chạy 29,6 ms với Seq Scan, bản viết lại thành khoảng chạy 5,4 ms với Bitmap Index Scan — nhanh hơn ~5,4 lần và trả về cùng 37.518 dòng.

Tình huống quen thuộc: truy vấn chậm, bạn tạo index lên đúng cột đang lọc, chạy lại — vẫn chậm y như cũ. Index nằm đó, \d thấy rõ, nhưng execution plan không hề nhắc tới nó.

Bài này đào sâu bài Index và bài tối ưu truy vấn trong series học SQL 30 ngày. Mọi con số là kết quả chạy thật trên PostgreSQL 16.11.

Gọi .Result khi nào thì deadlock, khi nào thì không?

· 14 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

.Result và .Wait() block thread hiện tại. Khi await tạm dừng, nó bắt lại SynchronizationContext đang hiện hành để chạy phần code phía sau đúng trên context đó. Nếu context chỉ cho phép một thread tại một thời điểm — UI thread của WPF, request context của ASP.NET Framework — mà thread đó lại đang bị .Result block, thì continuation không bao giờ vào được: deadlock. Console và ASP.NET Core không có SynchronizationContext, nên continuation chạy trên thread pool và không kẹt. Cách sửa thật sự là async all the way.

Đoạn code dưới đây chạy hoàn hảo trong một console app, và treo cứng khi bạn copy nguyên xi vào một controller của ASP.NET Framework hoặc một nút bấm trong WPF:

public string GetCustomerName(int id)
{
return GetCustomerAsync(id).Result; // treo ở đây
}

private async Task<string> GetCustomerAsync(int id)
{
var response = await _http.GetStringAsync($"/customers/{id}");
return Parse(response).Name;
}

Không có exception, không có stack trace, không có timeout. Thread đứng im vĩnh viễn. Chuyện này không phải do HttpClient, cũng không phải do async "bị lỗi" — nó là hệ quả trực tiếp của một quyết định thiết kế trong cách await hoạt động.

Vì sao EF Core bắn 201 query cho 1 màn hình danh sách? Cách sửa N+1

· 13 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Một danh sách 200 dòng mà log ghi 201 câu SQL chính là N+1: một truy vấn lấy danh sách, rồi mỗi dòng lại thêm một truy vấn để nạp dữ liệu liên quan. Gốc rễ là navigation property bị nạp rời rạc thay vì nạp chung. Cách sửa mặc định là projection bằng Select xuống DTO. Include chỉ hợp khi cần entity thật, và nếu Include từ hai collection trở lên thì phải thêm AsSplitQuery để khỏi nổ cartesian.

Bạn mở trang danh sách khách hàng, API trả về sau bốn giây, nhưng CPU của database gần như không nhúc nhích và câu SQL nào nhìn riêng lẻ cũng chạy trong vài mili-giây. Bật log lên thì hoá ra vấn đề không nằm ở một câu chậm, mà ở 201 câu nhanh xếp hàng nối đuôi nhau.

Đó là N+1 query. Nó không làm code sai, không ném exception, không bị unit test bắt, và trên máy local với hai mươi dòng dữ liệu seed thì nó thậm chí còn nhanh hơn bản sửa đúng. Nó chỉ lộ ra khi bảng có dữ liệu thật và độ trễ mạng tới database khác không.

Vì sao .NET ngốn 96% RAM container dù app đang rảnh? Server GC và cách giảm 65% bộ nhớ

· 12 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Một container ASP.NET Core 9 chiếm 987 MiB trên trần 1 GiB (96%) dù suốt 2 giờ không nhận request nào. Đây không phải rò rỉ bộ nhớ: mức RAM đứng yên tuyệt đối qua nhiều lần đo, dấu hiệu của heap bị giữ lại chứ không phải rò rỉ. Nguyên nhân là Server GC — mặc định của ASP.NET Core, tạo một heap cho mỗi CPU core và rất ít trả bộ nhớ về hệ điều hành. Đặt DOTNET_gcServer=0 và DOTNET_GCConserveMemory=5 giảm bộ nhớ anon từ 822 MB xuống 280 MB (−65%), đo ổn định sau 30 phút. Đánh đổi: Workstation GC cho throughput thấp hơn khi tải cao và nhiều core.

Bài này ghi lại một buổi chẩn đoán thật trên VPS 4 core / 8 GB RAM đang chạy song song 9 site WordPress và một API .NET. Mọi con số bên dưới là số đo thật, không phải ví dụ minh hoạ.