Ngay ngồi lướt medium, tình cờ thấy bài viết của một anh bạn outsource cũ từng onsite ở VN, anh ấy viết về quá trình mà đội nhóm của anh ấy thay đổi qua từng thời kỳ để ứng dụng của anh ấy có thể tải được lượng người dùng lớn hơn. Nội dung này khá hay với người mới nên tôi xin phép đăng lại cho mọi người tham khảo.

Khi mới ra mắt, chúng tôi đã rất vui mừng khi chỉ có 100 người dùng hàng ngày. Nhưng chỉ trong vòng vài tháng, con số đó đã lên đến 10.000, rồi 100.000. Và các vấn đề về khả năng mở rộng tăng lên nhanh hơn số lượng người dùng.
Chúng tôi đã đặt mục tiêu 1 triệu người dùng, nhưng kiến trúc hoạt động tốt cho 1.000 người đã không thể đáp ứng được. Khi nghĩ lại mọi thứ, thì đây là kiến trúc mà tôi ước mình đã xây dựng ngay từ đầu — và những gì chúng tôi học được khi mở rộng quy mô dưới áp lực.
Giai đoạn 1: Monolith đã hoạt động (Cho đến khi nó không còn đáp ứng)
Stack ban đầu của chúng tôi rất đơn giản:
- Spring Boot app
- MySQL database
- NGINX load balancer
- Everything deployed on one VM
[ Client ] → [ NGINX ] → [ Spring Boot App ] → [ MySQL ]
Thiết lập này xử lý 500 người dùng đồng thời một cách dễ dàng. Nhưng ở mức 5.000 người dùng đồng thời:
- CPU đạt mức tối đa
- Truy vấn chậm lại
- Thời gian hoạt động giảm xuống dưới 99%
- Giám sát cho thấy DB locks, GC pauses và thread contention.
Giai đoạn 2: Thêm máy chủ (Nhưng quên mất nguyên nhân gốc rễ)
Chúng tôi đã thêm nhiều máy chủ ứng dụng phía sau NGINX:
[ Client ] → [ NGINX ] → [ App1 | App2 | App3 ] → [ MySQL ]
Nó đọc tốt khi mở rộng. Nhưng việc ghi vẫn đổ dồn vào một instance MySQL duy nhất.
Trong các bài kiểm tra tải:
| Users | Avg Response Time |
| ----- | ----------------- |
| 1000 | 120ms |
| 5000 | 480ms |
| 10000 | 3.2s |
Điểm nghẽn không phải là CPU mà là cơ sở dữ liệu.
Giai đoạn 3: Giới thiệu bộ nhớ cache
Chúng tôi đã thêm Redis làm lớp bộ nhớ cache cho các truy vấn đọc nhiều:
public User getUser(String id) { User cached = redisTemplate.opsForValue().get(id); if (cached != null) return cached; User user = userRepository.findById(id).orElseThrow(); redisTemplate.opsForValue().set(id, user, 10, TimeUnit.MINUTES); return user; }
Việc này giảm tải DB 60% và giảm thời gian phản hồi xuống dưới 200ms cho các truy vấn đã được lưu vào bộ nhớ đệm.
Điểm chuẩn cho 1.000 yêu cầu hồ sơ người dùng đồng thời:
| Approach | Avg Latency | DB Queries |
| ---------- | ----------- | ---------- |
| No Cache | 150ms | 1000 |
| With Cache | 20ms | 50 |
Giai đoạn 4: Phá vỡ Monolith
Chúng tôi đã tách các tính năng cốt lõi thành các microservices:
- User Service
- Post Service
- Feed Service
Mỗi cái có một sơ đồ cơ sở dữ liệu riêng (ban đầu cùng một phiên bản DB).
Giao tiếp giữa các dịch vụ sử dụng REST APIs:
@RestController public class FeedController { @GetMapping("/feed/{userId}") public Feed getFeed(@PathVariable String userId) { User user = userService.getUser(userId); List<Post> posts = postService.getPostsForUser(userId); return new Feed(user, posts); } }
Nhưng việc kết nối REST call đã làm tăng độ chễ response. Một request đã phân tán thành 3–4 internal request.
ở quy mô này đã làm cho ứng dụng của tôi giảm hiệu suất rõ rệt.
Giai đoạn 5: Messaging and Asynchronous Processing
Chúng tôi thêm Kafka cho async workflows:
- Kafka event được active khi người dùng đăng ký.
- Downstream services hỗ trợ xử lý sự kiện thay vì synchronous REST
// Publish kafkaTemplate.send("user-signed-up", newUserId); // Consume @KafkaListener(topics = "user-signed-up") public void handleSignup(String userId) { recommendationService.prepareWelcomeRecommendations(userId); }
Với Kafka, độ trễ đăng ký giảm từ 1,2 giây xuống 300ms, vì các tác vụ downstream tốn kém đã chạy out of band..
Giai đoạn 6: Mở rộng Cơ sở dữ liệu
https://freedium.cfd/https://medium.com/@kanishks772/scaling-to-1-million-users-the-architecture-i-wish-i-knew-sooner-39c688ded2f1