Chương 2. Một Hệ Thống Web Nhìn Từ Trên Cao
Chương này không bắt đầu bằng một sơ đồ lớn đầy DNS, CDN, proxy, queue, worker, cache, database. Nhìn như vậy rất dễ ngợp.
Ta sẽ bắt đầu bằng một ví dụ nhỏ: một website bán bánh online.
Ban đầu website rất đơn giản. Người dùng vào xem bánh, đặt hàng, thanh toán khi nhận hàng. Sau đó website có nhiều người dùng hơn, hình ảnh tải chậm, đơn hàng nhiều, email gửi lâu, database bắt đầu chậm, có người upload ảnh lỗi, rồi hệ thống cần theo dõi lỗi.
Mỗi lần có một vấn đề xuất hiện, ta sẽ thêm một mảnh ghép kiến trúc. Như vậy ta không học công nghệ như một danh sách tên gọi, mà học vì sao nó xuất hiện.
---
2.1. Ngày đầu tiên: website rất đơn giản
Ở ngày đầu tiên, hệ thống chỉ cần ba phần:
Người dùng -> Backend -> Database
Ví dụ:
- Người dùng mở trang danh sách bánh.
- Backend lấy danh sách bánh từ database.
- Backend trả HTML/JSON về cho trình duyệt.
- Người dùng đặt hàng.
- Backend lưu đơn hàng vào database.
Mô hình này cực kỳ bình thường và đủ tốt cho rất nhiều sản phẩm ban đầu.
Browser
|
Backend App
|
Database
Lúc này chưa cần microservices, chưa cần Kafka, chưa cần Kubernetes, chưa cần CDN phức tạp.
Nếu mỗi ngày chỉ có vài chục đơn hàng, hệ thống đơn giản là lợi thế. Ít thành phần nghĩa là ít thứ có thể hỏng.
Điều quan trọng là: đơn giản không có nghĩa là cẩu thả. Backend vẫn nên có code rõ ràng, database thiết kế hợp lý, backup cơ bản, log cơ bản.
---
2.2. Người dùng gõ tên miền: DNS xuất hiện
Người dùng không gõ địa chỉ IP. Họ gõ:
banhngon.com
Máy tính cần biết domain này trỏ đến server nào. Đó là việc của DNS.
DNS giống như danh bạ điện thoại:
banhngon.com -> địa chỉ server
Nếu DNS sai, người dùng không vào được website, dù code backend vẫn chạy tốt.
Ta có sơ đồ:
Browser
|
DNS
|
Backend App
|
Database
Ở mức học kiến trúc, chỉ cần nhớ:
- DNS giúp domain trỏ đến hệ thống.
- DNS có cache.
- Đổi DNS có thể mất thời gian lan truyền.
- DNS thường trỏ đến CDN, load balancer hoặc server.
---
2.3. Hình ảnh tải chậm: CDN xuất hiện
Website bán bánh có rất nhiều ảnh đẹp. Ban đầu ảnh được backend trả trực tiếp:
Browser -> Backend -> File ảnh
Khi ít người dùng, không sao. Nhưng khi nhiều người cùng xem ảnh:
- Backend tốn băng thông để gửi ảnh.
- Người dùng ở xa server tải ảnh chậm.
- Server phải lo cả logic đặt hàng lẫn gửi file tĩnh.
Lúc này ta dùng CDN.
CDN giống như nhiều kho hàng nhỏ đặt gần người dùng. Ảnh bánh được copy ra nhiều nơi. Người dùng ở gần đâu thì tải từ đó.
Browser
|
CDN
|
Backend / Origin Storage
CDN phù hợp cho:
- Hình ảnh
- CSS
- JavaScript
- Video
- File download
- Font
CDN không làm mọi thứ nhanh hơn. Nếu trang chậm vì query database tệ, CDN không cứu được. CDN chủ yếu giúp nội dung tĩnh hoặc nội dung có thể cache.
Sai lầm phổ biến: cache nhầm dữ liệu động. Ví dụ trang "đơn hàng của tôi" mà bị CDN cache nhầm, người này có thể thấy dữ liệu người khác. Vì vậy cần biết cái gì được cache và cái gì không.
---
2.4. Không muốn backend lộ trực tiếp: reverse proxy xuất hiện
Ban đầu browser có thể gọi thẳng backend. Nhưng trong production, ta thường đặt một server đứng trước backend, ví dụ Nginx hoặc Caddy.
Đó là reverse proxy.
Browser
|
Reverse Proxy
|
Backend App
|
Database
Reverse proxy giống như quầy lễ tân trước nhà bếp.
Nó có thể:
- Nhận request từ internet.
- Xử lý HTTPS/TLS.
- Chuyển request vào backend.
- Giới hạn kích thước upload.
- Cấu hình timeout.
- Ghi access log.
- Phục vụ file tĩnh.
Ví dụ, backend của bạn chạy ở cổng nội bộ:
localhost:8000
Người dùng không gọi trực tiếp cổng đó. Họ gọi:
https://banhngon.com
Reverse proxy nhận request public rồi chuyển vào backend.
Khi upload file bị lỗi, WebSocket không kết nối được, request bị cắt sau 60 giây, đôi khi lỗi không nằm ở backend mà nằm ở reverse proxy.
---
2.5. Một server không đủ: load balancer xuất hiện
Website bắt đầu đông khách. Một backend server không gánh nổi.
Ta chạy nhiều backend giống nhau:
Backend 1
Backend 2
Backend 3
Nhưng người dùng gọi vào đâu? Không thể bắt browser tự chọn server.
Ta đặt load balancer ở trước:
Browser
|
Load Balancer
|------ Backend 1
|------ Backend 2
|------ Backend 3
|
Database
Load balancer giống người chia khách vào nhiều quầy.
Nó giúp:
- Chia tải request.
- Bỏ qua server đang chết.
- Cho phép deploy từng server một.
- Tăng khả năng chịu tải web.
Nhưng load balancer không phải thuốc tiên.
Nếu cả ba backend đều chậm vì database chậm, thêm backend thứ tư có thể làm database nghẽn hơn. Nếu mỗi request gọi API ngoài mất 90 giây, thêm backend cũng chưa chắc giải quyết đúng vấn đề.
Load balancer giải quyết vấn đề nhiều web server, không giải quyết mọi bottleneck.
---
2.6. Có request xấu: firewall và WAF xuất hiện
Khi website public, không chỉ khách hàng thật truy cập. Còn có bot, scanner, crawler lạ, request độc hại.
Firewall và WAF là các lớp bảo vệ.
Firewall giống cổng bảo vệ ở tầng mạng:
- Cổng nào được mở?
- IP nào được vào?
- Database có bị lộ public không?
- Redis có bị lộ ra internet không?
WAF giống bảo vệ hiểu HTTP hơn:
- Request này có dấu hiệu SQL injection không?
- Có pattern XSS không?
- Có quá nhiều request bất thường không?
- Có phải bot độc hại không?
Sơ đồ lúc này:
Browser
|
CDN / WAF
|
Load Balancer / Reverse Proxy
|
Backend App
|
Database
Nhưng đừng hiểu sai: WAF không thay thế code an toàn. Nếu code tự ghép SQL bằng string và dính SQL injection, WAF có thể chặn một số request, nhưng không phải lá chắn tuyệt đối.
Nguyên tắc:
- Database không nên public.
- Redis/queue không nên public.
- Admin nội bộ nên được bảo vệ kỹ hơn trang thường.
- API public phải có rate limit và authentication phù hợp.
---
2.7. Database bắt đầu bị đọc quá nhiều: cache xuất hiện
Trang danh sách bánh được rất nhiều người xem. Mỗi lần xem, backend lại query database:
SELECT * FROM cakes WHERE active = true
Nếu dữ liệu này không đổi liên tục, query database mỗi lần là lãng phí.
Ta thêm cache:
Backend
|
Cache
|
Database
Luồng đọc:
Backend hỏi cache: có danh sách bánh không?
-> Có: trả ngay
-> Không: query database, lưu vào cache, rồi trả
Cache giống ghi chú tạm trên bàn làm việc. Thay vì mở kho hồ sơ mỗi lần, ta giữ bản sao gần mình.
Cache giúp:
- Giảm tải database.
- Giảm latency.
- Chịu được lượng đọc lớn hơn.
Nhưng cache có rủi ro:
- Dữ liệu có thể cũ.
- Khi database đổi, cache phải được xóa/cập nhật.
- Nếu cache sai, người dùng thấy dữ liệu sai.
Ví dụ:
- Danh sách bánh có thể cache 1 phút.
- Giá khuyến mãi có thể cache ngắn hơn.
- Số dư ví người dùng không nên cache tùy tiện.
- Quyền truy cập nhạy cảm cần rất cẩn thận khi cache.
Cache không phải "cứ thêm là tốt". Cache tốt khi ta biết dữ liệu được phép cũ bao lâu.
---
2.8. Gửi email làm đặt hàng chậm: queue và worker xuất hiện
Khi người dùng đặt bánh, hệ thống cần:
1. Lưu đơn hàng. 2. Gửi email xác nhận. 3. Gửi thông báo cho cửa hàng. 4. Có thể gọi dịch vụ giao hàng.
Nếu làm tất cả trong request:
User đặt hàng
-> Backend lưu DB
-> Backend gửi email
-> Backend gọi dịch vụ giao hàng
-> Backend trả response
Người dùng phải chờ email provider và dịch vụ giao hàng phản hồi. Nếu email chậm 10 giây, đặt hàng chậm 10 giây.
Ta dùng queue.
Backend
|
Queue
|
Worker
Luồng mới:
User đặt hàng
-> Backend lưu đơn hàng
-> Backend đẩy job gửi email vào queue
-> Backend trả response ngay
Worker
-> Lấy job từ queue
-> Gửi email
-> Retry nếu lỗi tạm thời
Queue giống quầy nhận việc. Backend chỉ ghi phiếu: "Gửi email cho đơn hàng #123". Worker xử lý sau.
Queue phù hợp cho:
- Gửi email/SMS/push
- Xử lý ảnh/video
- Generate báo cáo
- Gọi API bên ngoài lâu
- Import/export file lớn
- Chấm AI hoặc tác vụ mất nhiều giây/phút
Nhưng queue kéo theo trách nhiệm mới:
- Job có bị kẹt không?
- Worker có đang chạy không?
- Lỗi thì retry thế nào?
- Retry có tạo tác dụng phụ trùng không?
- Người dùng xem trạng thái job ở đâu?
Queue không làm độ phức tạp biến mất. Nó chuyển độ phức tạp sang xử lý nền, nơi phù hợp hơn.
---
2.9. Người dùng upload ảnh bánh: object storage xuất hiện
Ban đầu ảnh có thể nằm trong thư mục trên server backend:
/uploads/cake-1.jpg
Nhưng khi có nhiều ảnh:
- Server đầy ổ đĩa.
- Deploy có thể làm mất file nếu không cẩn thận.
- Nhiều backend server không cùng thấy một file.
- Backup file phức tạp.
- Gửi file qua backend tốn tài nguyên.
Ta dùng object storage.
Ví dụ:
- Amazon S3
- Google Cloud Storage
- Azure Blob Storage
- Cloudflare R2
- MinIO
Sơ đồ:
Browser
|
Backend cấp quyền upload
|
Object Storage
|
CDN
Backend có thể cấp presigned URL để browser upload trực tiếp lên object storage. Backend không cần nhận toàn bộ file.
Object storage phù hợp cho:
- Ảnh
- Video
- Audio
- File export
- Backup
Nguyên tắc đơn giản: dữ liệu dạng file lớn nên để ở object storage, database chỉ lưu metadata và đường dẫn.
---
2.10. Cần tìm bánh nhanh: search engine xuất hiện
Ban đầu, tìm kiếm có thể dùng database:
WHERE name LIKE '%socola%'
Nhưng khi dữ liệu nhiều hơn và yêu cầu tìm kiếm phức tạp hơn:
- Tìm gần đúng.
- Gợi ý autocomplete.
- Xếp hạng kết quả.
- Lọc theo nhiều tiêu chí.
- Tìm tiếng Việt không dấu/có dấu.
- Tìm trong mô tả dài.
Database thông thường bắt đầu không còn thoải mái.
Ta thêm search engine:
Database -> đồng bộ dữ liệu -> Search Index
Backend -> query Search Index
Search engine như Elasticsearch/OpenSearch không phải source of truth. Source of truth vẫn là database chính.
Search index là bản sao phục vụ tìm kiếm. Nó có thể chậm đồng bộ vài giây.
Vì vậy:
- Tạo/sửa sản phẩm: ghi vào database.
- Sau đó đồng bộ sang search index.
- Người dùng tìm kiếm: query search index.
Sai lầm phổ biến: xem search index như database chính. Khi index lệch dữ liệu, hệ thống sẽ khó sửa.
---
2.11. Cần gọi dịch vụ ngoài: third-party API xuất hiện
Website bán bánh có thể tích hợp:
- Cổng thanh toán.
- Dịch vụ giao hàng.
- Email provider.
- SMS provider.
- Bản đồ.
- AI provider.
Các dịch vụ ngoài không nằm trong quyền kiểm soát của ta. Chúng có thể:
- Chậm.
- Timeout.
- Rate limit.
- Lỗi tạm thời.
- Thay đổi API.
- Tốn tiền theo request.
Vì vậy khi gọi third-party API, cần có:
- Timeout.
- Retry có kiểm soát.
- Rate limit.
- Log.
- Error handling.
- Idempotency nếu có tác dụng phụ như thanh toán.
Ví dụ thanh toán:
Nếu request thanh toán timeout, không được đơn giản retry mù quáng rồi trừ tiền hai lần. Cần idempotency key hoặc cơ chế kiểm tra trạng thái giao dịch.
Ví dụ AI:
Nếu gọi AI mất 90 giây, không nên giữ web request nếu người dùng không cần ngồi chờ. Hãy lưu job, đưa vào queue, để worker xử lý, rồi báo kết quả sau.
---
2.12. Cần realtime: polling, SSE hoặc WebSocket xuất hiện
Sau khi đặt hàng, người dùng muốn thấy trạng thái:
Đã nhận đơn -> Đang làm bánh -> Đang giao -> Hoàn tất
Có vài cách cập nhật UI.
Polling
Browser hỏi backend mỗi vài giây:
Đơn #123 sao rồi?
Đơn #123 sao rồi?
Đơn #123 sao rồi?
Dễ làm, nhưng nhiều request nếu quá đông người dùng.
SSE
Server giữ một kết nối một chiều để đẩy update xuống browser.
Phù hợp khi server chỉ cần báo trạng thái cho client.
WebSocket
Kết nối hai chiều giữa client và server.
Phù hợp cho:
- Chat
- Realtime collaboration
- Game
- Tracking realtime
- UI cần gửi/nhận liên tục
Không phải cứ realtime là phải WebSocket. Nếu chỉ cần kiểm tra kết quả job mỗi 3-5 giây, polling có thể đủ cho MVP.
Nguyên tắc: chọn cách đơn giản nhất đáp ứng trải nghiệm.
---
2.13. Cần nhiều backend khác nhau: API Gateway xuất hiện
Ban đầu chỉ có một backend. Sau này có thể có nhiều service:
- User service
- Order service
- Payment service
- Notification service
- Search service
Frontend không nên phải biết địa chỉ từng service.
API Gateway đứng trước các service:
Frontend
|
API Gateway
|---- User Service
|---- Order Service
|---- Payment Service
|---- Search Service
API Gateway có thể:
- Routing.
- Authentication.
- Rate limiting.
- Request aggregation.
- Logging.
- Transform request/response.
Nhưng API Gateway cũng có thể trở thành điểm nghẽn nếu nhét quá nhiều logic vào nó.
Nguyên tắc:
- Gateway nên điều phối ở biên.
- Business logic chính không nên nằm hết trong gateway.
- Nếu hệ thống còn một backend, có thể chưa cần API Gateway riêng.
---
2.14. Cần biết hệ thống đang bị gì: observability xuất hiện
Khi website nhỏ, bạn có thể SSH vào server xem log. Khi hệ thống lớn hơn, cách đó không đủ.
Ta cần observability:
- Logs: chuyện gì đã xảy ra.
- Metrics: các con số đang như thế nào.
- Traces: một request đi qua các bước nào.
- Alerts: khi nào cần gọi người dậy.
Ví dụ người dùng báo:
Đặt hàng rất chậm.
Ta cần biết:
- Request vào server nào?
- Backend xử lý bao lâu?
- Query database nào chậm?
- Có gọi payment provider không?
- Email có bị gửi trong request không?
- Queue có bị kẹt không?
- Load balancer có timeout không?
Không có observability, ta chỉ đoán.
Một hệ thống càng nhiều thành phần thì càng cần observability. Microservices mà không có tracing/logs/metrics tốt sẽ rất khó debug.
---
2.15. Sơ đồ hệ thống sau khi lớn hơn
Sau nhiều lần tiến hóa, website bán bánh có thể trông như sau:
Browser / Mobile App
|
DNS
|
CDN
|
Firewall / WAF
|
Reverse Proxy / Load Balancer
|
Backend App
/ | | \
Cache Database Queue Search Index
|
Worker
/ | \
Object Storage Email/SMS Third-party API
Logs / Metrics / Traces quan sát toàn bộ hệ thống
Đừng nhìn sơ đồ này như danh sách bắt buộc. Hãy nhìn nó như lịch sử các vấn đề đã xuất hiện:
- Ảnh tải chậm -> CDN.
- Backend cần đứng sau lớp public -> reverse proxy.
- Một server không đủ -> load balancer.
- Có request xấu -> firewall/WAF.
- Database bị đọc nhiều -> cache.
- Việc gửi email/gọi API lâu -> queue/worker.
- File nhiều -> object storage.
- Tìm kiếm phức tạp -> search engine.
- Nhiều service -> API Gateway.
- Khó debug -> observability.
Kiến trúc tốt thường là kết quả của việc giải từng vấn đề đúng lúc.
---
2.16. Cách đọc một sơ đồ hệ thống
Khi nhìn vào một sơ đồ hệ thống, đừng chỉ đếm bao nhiêu hộp. Hãy hỏi các câu sau.
Request vào từ đâu?
- Browser?
- Mobile app?
- Third-party API?
- Worker?
- Internal service?
Request đi qua những lớp nào?
- DNS?
- CDN?
- WAF?
- Proxy?
- Load balancer?
- Gateway?
Logic nằm ở đâu?
- Backend monolith?
- Modular monolith?
- Microservice?
- Worker?
- Workflow engine?
Dữ liệu nào là sự thật?
- Database chính?
- Cache?
- Search index?
- Object storage?
- Warehouse?
Cache, search index và warehouse thường là bản sao phục vụ mục đích riêng. Đừng nhầm chúng với source of truth.
Việc lâu được xử lý thế nào?
- Có xử lý ngay trong request không?
- Có queue không?
- Worker có đủ concurrency không?
- Job lỗi thì retry thế nào?
- Người dùng biết trạng thái bằng cách nào?
Nếu một phần chết thì sao?
- Request fail ngay?
- Có retry không?
- Có fallback không?
- Job có mất không?
- Dữ liệu có bị ghi trùng không?
- Có alert không?
Đọc sơ đồ tốt là đọc được dòng chảy, bottleneck và failure mode.
---
2.17. Những lỗi tư duy phổ biến
Lỗi 1: Chỉ nhìn backend
Nhiều người nghĩ hệ thống chậm là do code backend. Nhưng lỗi có thể ở:
- CDN cache sai.
- Proxy timeout.
- Load balancer health check sai.
- Database chậm.
- Queue kẹt.
- API ngoài timeout.
- Browser tải asset quá nặng.
Backend quan trọng, nhưng không phải toàn bộ hệ thống.
Lỗi 2: Thêm server web để chữa mọi loại chậm
Nếu chậm vì database, thêm backend server có thể làm database bị query nhiều hơn và chết nhanh hơn.
Nếu chậm vì API ngoài, thêm backend cũng chỉ tạo thêm nhiều request chờ API ngoài.
Trước khi scale, phải biết bottleneck.
Lỗi 3: Đưa việc lâu vào request
Nếu request phải gửi email, xử lý file, gọi API ngoài mất lâu rồi mới trả response, throughput sẽ giảm mạnh.
Nên hỏi:
Người dùng có thật sự cần chờ kết quả này ngay không?
Nếu không, hãy đưa việc đó ra queue.
Lỗi 4: Dùng queue nhưng không theo dõi queue
Queue không làm lỗi biến mất. Nó chỉ làm lỗi xảy ra ở nền.
Nếu không theo dõi:
- Queue dài bao nhiêu?
- Job chờ bao lâu?
- Worker có chết không?
- Job retry bao nhiêu lần?
- Dead letter có tăng không?
Thì job có thể kẹt hàng giờ mà không ai biết.
Lỗi 5: Dùng cache mà không biết dữ liệu được phép cũ bao lâu
Cache sai có thể nguy hiểm hơn không cache.
Ví dụ:
- Cache danh sách bánh 1 phút: thường ổn.
- Cache trạng thái thanh toán sai: nguy hiểm.
- Cache quyền truy cập sai: rất nguy hiểm.
Lỗi 6: Thêm nhiều thành phần hơn khả năng vận hành
Mỗi thành phần mới cần:
- Cấu hình.
- Deploy.
- Monitoring.
- Backup hoặc recovery.
- Security.
- Người hiểu nó.
Nếu team không thể vận hành Kafka/Kubernetes/service mesh, dùng chúng quá sớm sẽ làm hệ thống yếu hơn chứ không mạnh hơn.
---
2.18. Kết luận của chương
Một hệ thống web không xuất hiện đầy đủ các thành phần ngay từ đầu. Nó lớn lên theo vấn đề:
- Domain giúp người dùng tìm hệ thống.
- CDN giúp tải nội dung tĩnh nhanh hơn.
- Reverse proxy đứng trước backend.
- Load balancer chia tải cho nhiều server.
- Firewall/WAF bảo vệ lớp biên.
- Cache giảm tải đọc.
- Queue/worker xử lý việc lâu.
- Object storage giữ file lớn.
- Search engine phục vụ tìm kiếm phức tạp.
- API Gateway điều phối khi có nhiều service.
- Observability giúp nhìn thấy hệ thống đang hoạt động ra sao.
Tư duy quan trọng nhất của chương này là:
> Đừng học các thành phần như một danh sách công nghệ. Hãy học chúng như câu trả lời cho từng vấn đề cụ thể.
Khi hiểu được một hệ thống web từ trên cao, ta sẽ bớt hoang mang trước các sơ đồ lớn. Mỗi hộp trong sơ đồ đều có lý do tồn tại. Nếu không tìm thấy lý do đó, rất có thể hộp ấy chưa cần xuất hiện.