Scale hạ tầng lên 1 Triệu người dùng từ kinh nghiệm của DevOps Quèn

Khi mới ra mắt, chúng tôi thấy khá ổn áp khi có 100 người dùng hàng ngày. Nhưng chỉ trong một thời gian sau đó, con số tăng vọt lên 10.000, rồi 100.000. Và các vấn đề về khả năng mở rộng (scaling) thì “ùn ùn kéo đến” nhanh hơn cả 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 cái kiến trúc hoạt động tốt cho 1.000 người dùng lại không thể “gánh” nổi. Nhìn lại, đâ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 phải mở rộng dưới lượng traffic tăng cao.

Nói vậy chứ thật ra cũng hơi khó vì đâu ai nghĩ sẽ lên được hàng trăm nghìn người sử dụng và áp dụng kiến trúc ngay từ ban đầu thì có thể chưa tối ưu. Tuy nhiên, cứ chia sẻ để mọi người tham khảo nhé. Mong sẽ hữu ích.

Giai đoạn 1: Monolith “ngon lành” (Cho đến khi nó “tạch”)

Stack ban đầu của chúng tôi rất đơn giản (chắc anh em đều biết và làm với hạ tầng nhỏ rồi):

  • Ứng dụng Spring Boot
  • Cơ sở dữ liệu MySQL
  • Load balancer NGINX
  • Tất cả triển khai trên một VM duy nhất
[ 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 khi đạt 5.000 người dùng đồng thời:

  • CPU “full tải”
  • Các truy vấn chậm dần
  • Uptime (thời gian hoạt động) giảm xuống dưới 99% (Có tuần thấp nhất nó xuống 91% – sếp nói kinh luôn)

Giám sát cho thấy DB bị khóa, các đợt dừng GC (Garbage Collection pauses), và tranh chấp luồng (thread contention).

Giai đoạn 2: “Quẳng” thêm Server (Nhưng bỏ lỡ điểm nghẽn thực sự)

Chúng tôi thêm nhiều app server phía sau NGINX (điều này thì quen thuộc khi nghĩ đến scale rồi):
[ Client ] → [ NGINX ] → [ App1 | App2 | App3 ] → [ MySQL ]

Nó mở rộng phần đọc thì ổn. Nhưng các thao tác ghi vẫn bị “dồn” vào một instance MySQL duy nhất.

Dưới các bài kiểm thử tải:

Người dùng Thời gian phản hồi trung bình
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: Thêm Cache

Chúng tôi thêm Redis làm một tầng caching 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;
}

Điều này giúp giảm tải DB 60% và cắt giảm thời gian phản hồi xuống dưới 200ms cho các lần đọc đã được cache.

Benchmark cho 1.000 yêu cầu profile người dùng đồng thời:

Cách tiếp cận Độ trễ trung bình Truy vấn DB
Không Cache 150ms 1000
Có 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 service có schema cơ sở dữ liệu riêng (ban đầu vẫn dùng chung instance DB).

Giao tiếp giữa các service 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 gọi chuỗi các lệnh REST API lại gây ra tăng độ trễ. Một yêu cầu “fan out” thành 3-4 yêu cầu nội bộ.

Ở quy mô lớn, điều này làm “giết chết” hiệu suất.

Giai đoạn 5: Messaging và Xử lý Bất đồng bộ

Chúng tôi thêm Kafka cho các workflow bất đồng bộ:

  • Đăng ký người dùng kích hoạt một Kafka event
  • Các service downstream tiêu thụ event thay vì dùng REST đồng bộ
// Publish (Xuất bản)
kafkaTemplate.send("user-signed-up", newUserId);

// Consume (Tiêu thụ)
@KafkaListener(topics = "user-signed-up")
public void handleSignup(String userId) {
    recommendationService.prepareWelcomeRecommendations(userId);
}

Với Kafka, độ trễ khi đăng ký giảm từ 1.2s xuống 300ms, vì các tác vụ downstream “nặng nề” giờ đây chạy ngoài luồng chính (out of band).

Giai đoạn 6: Mở rộng Cơ sở dữ liệu

Ở mức 500.000 người dùng, instance MySQL của chúng tôi không thể theo kịp ngay cả khi đã có caching.

Chúng tôi đã thêm:

  • Read replicas → Tách biệt các thao tác đọc/ghi
  • Sharding → Chia partition dựa trên người dùng (người dùng 0–999k, 1M-2M, v.v.)
  • Archive tables → Di chuyển dữ liệu “nguội” ra khỏi các đường dẫn “nóng”

Ví dụ bộ định tuyến truy vấn:

if (userId < 1000000) {
    return jdbcTemplate1.query(...);
} else {
    return jdbcTemplate2.query(...);
}

Điều này giúp giảm tranh chấp ghi (write contention) và thời gian truy vấn trên các shard.

Giai đoạn 7: Khả năng Quan sát (Observability)

Ở mức 100.000+ người dùng, việc gỡ lỗi trở thành một cơn ác mộng nếu không có giám sát.

Chúng tôi đã thêm:

  • Distributed tracing (Jaeger + OpenTelemetry)
  • Centralized logs (ELK stack)
  • Monitoring (Prometheus + Grafana dashboards)

Ví dụ metrics trên Grafana:

Metric Giá trị
Độ trễ P95 280ms
Kết nối DB 120/200
Kafka lag 0

Trước khi có observability, chẩn đoán các đợt tăng độ trễ mất hàng giờ. Sau đó, chỉ còn vài phút.

Giai đoạn 8: CDN và Edge Caching

Ở mức 1 triệu người dùng (Thật ra là Max chứ loanh quanh vài trăm ngàn), 40% lưu lượng truy cập là các tệp tĩnh (ảnh, avatar, JS bundles).

Chúng tôi đã chuyển chúng sang Cloudflare CDN với caching “khủng”:

Tài sản Độ trễ Origin Độ trễ CDN
/static/app.js 400ms 40ms
/images/avatar.png 300ms 35ms

Điều này giúp giảm tải 70% traffic từ các máy chủ gốc.

Kiến trúc cuối cùng (biết sớm chuẩn bị)

Nếu được làm lại từ đầu, tôi sẽ bỏ qua các giai đoạn và xây dựng kiến trúc này sớm hơn:

[ Client ]  
   ↓  
[ CDN + Edge Caching ]  
   ↓  
[ API Gateway → Service Mesh ]  
   ↓  
[ Microservices + Kafka + Redis Cache ]  
   ↓  
[ Sharded Database + Read Replicas ]

Những bài học quan trọng:

  • Caching không phải là tùy chọn. Nó là “must-have”.
  • Mở rộng DB cần được thiết kế từ sớm.
  • Xử lý bất đồng bộ là cực kỳ quan trọng.
  • Observability mang lại lợi ích từ rất sớm.
  • Mở rộng không phải là “thêm nhiều server” mà là gỡ bỏ các nút thắt cổ chai ở mọi lớp.

Benchmark cuối cùng (1 Triệu người dùng, 1.000 RPS):

Metric Giá trị
Độ trễ API P95 210ms
Tỷ lệ lỗi <0.1%
Tỷ lệ Cache Hit 85%
Tốc độ truy vấn DB 50 qps
Kafka Consumer Lag 0

Lời kết

Mở rộng lên một triệu người dùng không phải là về công nghệ “xịn xò” mà là về việc giải quyết đúng vấn đề, theo đúng thứ tự. Kiến trúc phục vụ 1.000 người dùng đầu tiên của bạn sẽ không phục vụ được 10.000 tiếp theo (chưa nói tới hàng trăm hàng triệu người dùng). Hãy lên kế hoạch cho các chế độ thất bại (failure modes) trước khi bạn gặp phải chúng.

Chia sẻ bài viết:
Theo dõi
Thông báo của
0 Góp ý
Được bỏ phiếu nhiều nhất
Mới nhất Cũ nhất
Phản hồi nội tuyến
Xem tất cả bình luận