Chương 5. Đừng Tối Ưu Khi Chưa Biết Nghẽn Ở Đâu
Khi hệ thống chậm, phản xạ tự nhiên là muốn sửa ngay.
Ta có thể nghĩ:
- Thêm server.
- Dùng cache.
- Đổi database.
- Tách microservices.
- Chuyển sang async.
- Dùng queue.
- Dùng Kubernetes.
- Viết lại bằng framework khác.
Nhưng nếu chưa biết hệ thống nghẽn ở đâu, mọi giải pháp đều có thể là đoán mò.
Kiến trúc thực dụng bắt đầu bằng câu hỏi:
> Bottleneck thật sự nằm ở đâu?
---
5.1. Ví dụ quán bánh bị chậm
Quán bánh đông khách. Khách than chờ lâu.
Chủ quán có thể vội kết luận:
- Cần thuê thêm nhân viên bán hàng.
- Cần mở thêm quầy.
- Cần đổi máy tính tiền.
- Cần thuê bếp mới.
- Cần thuê thêm shipper.
Nhưng trước khi quyết định, phải quan sát.
Khách chờ lâu vì lý do nào?
Trường hợp 1: Quầy order chậm
Khách xếp hàng dài trước quầy, nhưng bếp phía sau vẫn rảnh.
Vấn đề là khâu nhận đơn.
Giải pháp có thể là:
- Thêm quầy order.
- Cho khách đặt trước.
- Làm menu rõ hơn.
- Dùng kiosk/self-order.
Trường hợp 2: Bếp chậm
Quầy order nhận rất nhanh, nhưng phiếu đơn chất đống trong bếp.
Vấn đề là khâu làm bánh.
Thêm nhân viên order không giúp gì, thậm chí làm bếp ngập đơn nhanh hơn.
Trường hợp 3: Giao hàng chậm
Bánh làm xong nhưng shipper đến muộn.
Vấn đề nằm ở bên ngoài quán.
Thêm bếp cũng không giải quyết được.
Trường hợp 4: Máy tính tiền chậm
Bánh có sẵn, nhân viên có sẵn, nhưng thanh toán bị nghẽn.
Vấn đề là payment.
Mỗi loại chậm cần một cách xử lý khác nhau.
Hệ thống web cũng vậy. Người dùng chỉ nói "chậm", nhưng "chậm" có thể nằm ở DNS, CDN, proxy, backend, database, cache, queue, worker, API ngoài, network hoặc browser.
---
5.2. Bottleneck là gì?
Bottleneck là điểm giới hạn năng lực của toàn hệ thống.
Nếu một dây chuyền có nhiều bước:
A -> B -> C -> D
Và bước C chậm nhất, toàn bộ dây chuyền sẽ bị giới hạn bởi C.
Tăng tốc A, B hoặc D có thể không làm throughput tăng nếu C vẫn là điểm nghẽn.
Ví dụ:
Frontend -> Backend -> Database
Nếu database đang là bottleneck, thêm backend server có thể không giúp. Backend mới chỉ tạo thêm nhiều query đổ vào database.
Ví dụ khác:
Backend -> Queue -> Worker -> External API
Nếu External API chỉ cho 100 request/phút, tăng worker lên quá cao có thể chỉ tạo thêm lỗi rate limit.
Tư duy:
> Hệ thống nhanh bằng điểm chậm nhất trong luồng quan trọng.
---
5.3. Dấu hiệu nghẽn CPU
CPU-bound xảy ra khi hệ thống thật sự đang tính toán nhiều.
Dấu hiệu:
- CPU usage cao liên tục.
- Load average cao.
- Request/job chậm khi CPU tăng.
- Worker xử lý tác vụ tính toán nặng.
- Thêm thread không giúp nhiều.
- Machine nóng, fan quay, process chiếm CPU rõ ràng.
Ví dụ workload:
- Encode video.
- Resize ảnh hàng loạt.
- Tính toán báo cáo lớn.
- Chạy thuật toán nặng.
- Render PDF phức tạp.
- Inference model trên CPU.
Cách xử lý:
- Tối ưu thuật toán.
- Tách tác vụ CPU-bound sang worker riêng.
- Tăng số process theo số core.
- Dùng machine mạnh hơn.
- Dùng GPU nếu phù hợp.
- Batch hoặc precompute.
- Không chạy CPU-bound nặng trong web request.
Sai lầm:
- Dùng async để xử lý CPU-bound và kỳ vọng nhanh hơn.
- Tăng concurrency quá cao làm context switching tăng.
- Cho job nặng chạy chung worker với job nhẹ.
---
5.4. Dấu hiệu nghẽn RAM
RAM là nơi chứa dữ liệu đang được xử lý. Khi RAM thiếu, hệ thống trở nên rất tệ.
Dấu hiệu:
- Memory usage tăng đều.
- Process bị OOM kill.
- Server bắt đầu dùng swap.
- Response chậm dần theo thời gian.
- Worker chết khi xử lý file lớn.
- Restart xong nhanh lại, sau đó chậm dần.
Ví dụ nguyên nhân:
- Load file lớn vào memory.
- Query quá nhiều dòng rồi xử lý trong app.
- Tạo quá nhiều worker process nặng.
- Memory leak.
- Cache trong memory quá lớn.
- Load model AI lớn.
- Giữ buffer ảnh/video lớn.
Cách xử lý:
- Stream dữ liệu thay vì load hết.
- Xử lý theo batch nhỏ.
- Giới hạn file size.
- Giới hạn concurrency.
- Theo dõi memory per job.
- Tách worker nặng sang machine riêng.
- Tìm memory leak.
Sai lầm:
- Chỉ tăng timeout trong khi process đang phình RAM.
- Tăng worker concurrency khiến RAM cạn nhanh hơn.
- Không đo memory theo từng loại job.
---
5.5. Dấu hiệu nghẽn database
Database thường là nơi đau nhất của hệ thống web.
Dấu hiệu:
- Slow query tăng.
- CPU database cao.
- Database connection pool cạn.
- Lock wait tăng.
- Deadlock xuất hiện.
- Request backend chờ database lâu.
- p95/p99 latency tăng khi traffic tăng.
- Báo cáo nặng làm app chính chậm.
Nguyên nhân thường gặp:
- Thiếu index.
- Query sai.
- N+1 query.
- Transaction quá dài.
- Ghi quá nhiều.
- Đọc quá nhiều.
- Join nặng.
- Bảng quá lớn.
- Connection mở quá nhiều.
- Báo cáo/analytics chạy trên database production.
Cách xử lý theo thứ tự thực dụng:
1. Xem slow query. 2. Xem query plan. 3. Thêm/sửa index đúng chỗ. 4. Giảm N+1. 5. Giảm dữ liệu query không cần thiết. 6. Cache dữ liệu đọc nhiều nếu được phép. 7. Tách report sang read replica/warehouse nếu cần. 8. Tối ưu transaction. 9. Scale database sau khi đã hiểu vấn đề.
Sai lầm:
- Thêm web server khi database đã nghẽn.
- Dùng cache để che query lỗi mà không hiểu tính đúng dữ liệu.
- Tách microservices nhưng vẫn dùng chung database và query chéo lung tung.
---
5.6. Dấu hiệu nghẽn queue/worker
Queue giúp đưa việc dài ra khỏi request, nhưng queue cũng có thể nghẽn.
Dấu hiệu:
- Queue length tăng liên tục.
- Queue age tăng.
- Job chờ lâu mới được xử lý.
- Worker luôn busy.
- Dead letter tăng.
- Retry tăng.
- Người dùng thấy trạng thái "đang xử lý" quá lâu.
Cần phân biệt:
- Job xử lý lâu.
- Job chờ queue lâu.
- Worker chết.
- Worker quá ít concurrency.
- External API chậm.
- Rate limit từ provider.
Ví dụ:
Job duration = 90 giây
Queue wait time = 20 phút
Vấn đề người dùng cảm nhận là hơn 21 phút, không phải chỉ 90 giây.
Cách xử lý:
- Tách queue theo loại workload.
- Tăng worker/concurrency đúng loại việc.
- Giới hạn rate nếu provider có quota.
- Theo dõi queue age.
- Thêm dead letter queue.
- Tối ưu job duration nếu có thể.
- Scale worker nếu bottleneck là worker.
- Dùng backpressure nếu nhận việc nhanh hơn khả năng xử lý.
Sai lầm:
- Dùng queue nhưng không dashboard.
- Tất cả job chung một queue.
- Job nặng làm kẹt job nhẹ.
- Retry liên tục làm queue càng nghẽn.
---
5.7. Dấu hiệu nghẽn external API
Hệ thống hiện đại thường phụ thuộc bên thứ ba:
- Payment provider.
- Email/SMS provider.
- AI provider.
- Map API.
- OAuth provider.
- Shipping provider.
Dấu hiệu nghẽn:
- Request chậm ở bước gọi API ngoài.
- Timeout tăng.
- Lỗi 429/rate limit.
- Lỗi 5xx từ provider.
- Retry tăng.
- Chi phí API tăng bất thường.
- Worker bị giữ lâu vì chờ provider.
Cách xử lý:
- Timeout rõ ràng.
- Retry có backoff và jitter.
- Rate limit phía mình.
- Circuit breaker nếu provider lỗi nhiều.
- Queue cho tác vụ dài.
- Fallback nếu nghiệp vụ cho phép.
- Cache kết quả nếu được.
- Idempotency với API có side effect.
- Theo dõi latency/error theo từng provider.
Sai lầm:
- Không timeout.
- Retry ngay lập tức hàng loạt.
- Giữ web request chờ provider quá lâu.
- Không phân biệt lỗi tạm thời và lỗi vĩnh viễn.
- Không có idempotency với payment/order.
---
5.8. Dấu hiệu nghẽn network/CDN/proxy
Không phải mọi chậm đều nằm trong app.
Dấu hiệu:
- Người dùng ở một khu vực chậm hơn khu vực khác.
- File tĩnh tải lâu.
- Time to first byte cao.
- Upload hay bị cắt.
- WebSocket hay disconnect.
- Request bị 413 Payload Too Large.
- Request bị 504 Gateway Timeout.
- Backend không thấy log request dù user báo lỗi.
Có thể lỗi nằm ở:
- DNS.
- CDN.
- WAF.
- Reverse proxy.
- Load balancer.
- Region server.
- Network route.
- Body size limit.
- Proxy timeout.
Cách xử lý:
- Kiểm tra access log ở proxy/load balancer.
- Kiểm tra CDN cache hit/miss.
- Kiểm tra timeout/body size.
- Kiểm tra region và latency mạng.
- Dùng CDN cho static file.
- Upload trực tiếp object storage nếu file lớn.
- Cấu hình WebSocket proxy đúng.
Sai lầm:
- Chỉ xem log backend.
- Không biết request đã bị chặn ở lớp trước.
- Đổ lỗi cho code khi proxy timeout.
---
5.9. Dấu hiệu nghẽn frontend/browser
Đôi khi backend nhanh nhưng người dùng vẫn thấy chậm.
Dấu hiệu:
- API nhanh nhưng trang render chậm.
- JavaScript bundle quá lớn.
- Ảnh quá nặng.
- Quá nhiều request nhỏ.
- Main thread browser bị block.
- Layout shift.
- Mobile yếu chạy chậm hơn desktop.
Cách xử lý:
- Giảm bundle size.
- Lazy load.
- Tối ưu ảnh.
- Dùng CDN.
- Giảm số request.
- Cache asset.
- Server-side rendering hoặc static generation nếu phù hợp.
- Đo Core Web Vitals.
Sai lầm:
- Chỉ đo API latency.
- Không đo trải nghiệm thật trên thiết bị yếu.
- Tải ảnh/video không tối ưu.
---
5.10. Bottleneck có thể di chuyển
Sau khi xử lý một bottleneck, bottleneck mới có thể xuất hiện.
Ví dụ:
1. Backend chỉ có một server nên chậm. 2. Thêm server backend. 3. Database bắt đầu nghẽn. 4. Thêm cache. 5. Worker gửi email bắt đầu nghẽn. 6. Tăng worker. 7. Email provider rate limit.
Đây là chuyện bình thường.
Tối ưu hệ thống giống mở rộng một con đường. Khi mở rộng đoạn hẹp nhất, đoạn khác trở thành hẹp nhất.
Vì vậy tối ưu không phải làm một lần là xong. Ta cần:
- Đo.
- Sửa điểm nghẽn hiện tại.
- Đo lại.
- Xem bottleneck mới.
- Lặp lại.
---
5.11. Quy trình điều tra bottleneck
Khi hệ thống chậm, có thể đi theo quy trình sau.
Bước 1: Xác định triệu chứng
Chậm ở đâu?
- Một endpoint?
- Toàn hệ thống?
- Một nhóm user?
- Một khu vực địa lý?
- Một loại job?
- Một thời điểm trong ngày?
Bước 2: Xác định thời gian
Chậm từ khi nào?
- Sau deploy?
- Sau tăng traffic?
- Sau đổi database?
- Sau thêm tính năng?
- Sau khi provider bên ngoài lỗi?
Bước 3: Xem request path
Request đi qua:
- CDN?
- Proxy?
- Load balancer?
- Backend?
- Database?
- Cache?
- External API?
Nghẽn ở bước nào?
Bước 4: Xem metrics
Cần xem:
- CPU
- RAM
- Disk
- Network
- Request latency p95/p99
- Error rate
- Database slow query
- Connection pool
- Queue length/age
- Worker busy
- External API latency
Bước 5: Xem logs và traces
Metrics cho biết có vấn đề. Logs và traces giúp hiểu chuyện gì xảy ra.
Hỏi:
- Request nào chậm?
- Chậm ở span nào?
- Có retry không?
- Có timeout không?
- Có lỗi provider không?
- Có lock database không?
Bước 6: Đưa ra giả thuyết nhỏ
Ví dụ:
- Query X thiếu index.
- Worker queue AI thiếu concurrency.
- Email provider rate limit.
- Proxy timeout quá thấp.
- Cache miss tăng sau deploy.
Bước 7: Sửa nhỏ và đo lại
Đừng thay đổi quá nhiều cùng lúc. Nếu sửa 5 thứ và hệ thống nhanh hơn, ta không biết thứ nào thật sự có tác dụng.
---
5.12. Checklist đo đạc tối thiểu
Một hệ thống production nên có ít nhất các số sau.
Web/API
- Request count.
- Error rate.
- Latency p50/p95/p99.
- Timeout count.
- Status code distribution.
Database
- Slow queries.
- Query time.
- Connection count.
- Lock wait.
- CPU/RAM/disk.
- Replication lag nếu có replica.
Queue/worker
- Queue length.
- Queue age.
- Job duration p50/p95/p99.
- Retry count.
- Dead letter count.
- Worker busy/idle.
External API
- Latency theo provider.
- Error rate theo provider.
- Timeout count.
- Rate limit count.
- Cost nếu tính theo usage.
System
- CPU.
- RAM.
- Disk usage.
- Network.
- Process restart.
- OOM kill.
Không cần mọi thứ phải hoàn hảo từ ngày đầu, nhưng nếu không có các số này, tối ưu sẽ rất mù.
---
5.13. Ví dụ: endpoint đặt hàng chậm
Người dùng báo đặt hàng mất 8 giây.
Đừng vội thêm server. Ta phân tích.
Trace cho thấy:
Validate input: 20ms
Save order DB: 80ms
Call payment API: 2s
Send email: 4s
Call shipping API: 1.5s
Return response: 20ms
Vấn đề không nằm ở database hay backend framework. Vấn đề là request đang làm quá nhiều việc phụ thuộc bên ngoài.
Cách sửa:
- Payment có thể vẫn cần xử lý trong luồng chính nếu cần xác nhận ngay.
- Email nên đưa vào queue.
- Shipping có thể đưa vào queue hoặc trạng thái pending nếu nghiệp vụ cho phép.
- Thêm timeout cho API ngoài.
- Có idempotency cho payment.
Sau khi sửa:
Validate input: 20ms
Save order DB: 80ms
Create payment intent: 500ms
Push email job: 5ms
Push shipping job: 5ms
Return response: 20ms
Request còn khoảng 630ms thay vì 8 giây.
Đây là tối ưu đúng vì ta tìm được bottleneck thật.
---
5.14. Ví dụ: job xử lý nền chậm
Hệ thống có job xử lý ảnh.
Người dùng báo ảnh upload xong rất lâu mới hiện thumbnail.
Metrics:
Job duration trung bình: 5 giây
Queue wait time p95: 12 phút
Worker concurrency: 2
Queue length tăng mạnh giờ cao điểm
Vấn đề chính không phải mỗi job xử lý chậm. Vấn đề là quá ít worker/concurrency so với lượng job đến.
Cách sửa:
- Tăng worker cho queue ảnh.
- Tách queue ảnh khỏi queue email/report.
- Giới hạn upload nếu quá tải.
- Theo dõi queue age.
- Nếu resize ảnh CPU-bound, tăng process/machine theo CPU.
- Nếu upload/download file chậm, xem object storage/network.
Nếu chỉ tối ưu thuật toán resize từ 5 giây xuống 4 giây nhưng queue wait vẫn 12 phút, người dùng gần như không thấy khác biệt.
---
5.15. Ví dụ: database bị đổ lỗi oan
Trang danh sách sản phẩm chậm 3 giây.
Mọi người nghĩ database chậm.
Nhưng trace cho thấy:
Database query: 80ms
Backend render: 100ms
Call recommendation API: 2.5s
Serialize response: 50ms
Database không phải bottleneck. Recommendation API mới là bottleneck.
Cách xử lý:
- Không block trang chính vì recommendation.
- Load recommendation sau bằng request riêng.
- Cache recommendation.
- Có fallback nếu recommendation API chậm.
- Đặt timeout thấp hơn.
Nếu lúc này thêm index database, không giúp gì.
---
5.16. Những câu hỏi trước khi đổi kiến trúc
Trước khi nói "phải đổi sang X", hãy hỏi:
- Chậm ở request path hay job nền?
- Latency p95/p99 là bao nhiêu?
- Queue age có tăng không?
- CPU/RAM có bão hòa không?
- Database có slow query không?
- Connection pool có cạn không?
- Có external API nào chậm không?
- Có retry storm không?
- Có rate limit không?
- Có đo trước và sau khi sửa không?
Nếu chưa trả lời được các câu này, đổi kiến trúc là đánh cược.
---
5.17. Khi nào tối ưu là sai thời điểm?
Không phải bottleneck nào cũng cần sửa ngay.
Ví dụ:
- Một job nội bộ chạy 10 phút mỗi đêm nhưng không ảnh hưởng ai.
- Một endpoint admin hơi chậm nhưng dùng mỗi tháng một lần.
- Một trang ít người dùng có latency 2 giây.
- Một báo cáo nặng nhưng có thể chờ.
Tối ưu cũng có chi phí. Nếu tối ưu một phần ít quan trọng, ta có thể lãng phí thời gian đáng ra dành cho phần ảnh hưởng người dùng hoặc doanh thu nhiều hơn.
Hỏi:
- Ai bị ảnh hưởng?
- Bị ảnh hưởng bao nhiêu lần/ngày?
- Có gây mất tiền/mất dữ liệu không?
- Có workaround đơn giản không?
- Sửa có rủi ro không?
Tối ưu đúng không chỉ là sửa cái chậm, mà là sửa cái chậm đáng sửa.
---
5.18. Kết luận của chương
Đừng tối ưu khi chưa biết nghẽn ở đâu.
Một hệ thống có thể chậm vì:
- CPU.
- RAM.
- Database.
- Queue/worker.
- External API.
- Network/CDN/proxy.
- Frontend/browser.
- Sai cách xử lý workload.
Mỗi loại nghẽn cần cách xử lý khác nhau. Thêm server, đổi framework, dùng cache, tách microservices hay thêm queue đều có thể đúng hoặc sai tùy bottleneck.
Tư duy quan trọng nhất:
> Đo, xác định điểm nghẽn, sửa nhỏ, đo lại.
Khi biết bottleneck thật, giải pháp thường đơn giản và chính xác hơn nhiều. Khi không biết bottleneck, mọi kiến trúc đều chỉ là đoán.