Khám phá lộ trình mở rộng hệ thống từ single server đến kiến trúc phục vụ hàng triệu người dùng. Hướng dẫn chi tiết về vertical scaling, horizontal scaling, Load Balancer, database replication, caching, message queue, microservices, cloud và container hóa.
Khả năng mở rộng hệ thống (scalability) là yếu tố then chốt giúp một hệ thống có thể đáp ứng lượng người dùng ngày càng tăng mà vẫn duy trì hiệu suất ổn định[1]. Nói cách khác, một hệ thống có tính mở rộng sẽ tiếp tục hoạt động tốt khi khối lượng công việc, số lượng người dùng hoặc dữ liệu tăng lên, mà không cần thay đổi lớn về kiến trúc. Đối với các startup hay ứng dụng web mới ra đời, hệ thống thường bắt đầu từ quy mô nhỏ (một máy chủ đơn lẻ phục vụ tất cả). Tuy nhiên, khi lượng người dùng tăng dần tới hàng triệu, kiến trúc ban đầu sẽ bộc lộ nhiều hạn chế. Bài viết này trình bày lộ trình mở rộng hệ thống từ cơ bản đến nâng cao, bắt đầu từ kiến trúc máy chủ đơn giản, sau đó áp dụng dần các kỹ thuật mở rộng như vertical scaling, horizontal scaling, triển khai Load Balancer, sử dụng database replication, tích hợp tầng caching và hàng đợi message queue. Ngoài ra, bài viết cũng thảo luận các công nghệ hiện đại như điện toán đám mây (cloud), container (Docker, Kubernetes) và kiến trúc microservices trong bối cảnh hệ thống lớn. Mục tiêu là cung cấp một cái nhìn tổng quan có hệ thống về cách nâng cấp kiến trúc hệ thống để phục vụ từ vài người dùng đầu tiên đến hàng triệu người dùng.
Mọi hệ thống lớn đều khởi đầu từ những bước đi nhỏ. Ở giai đoạn ban đầu, kiến trúc đơn giản nhất là chạy toàn bộ ứng dụng trên một máy chủ duy nhất, bao gồm cả máy chủ web (ứng dụng) và cơ sở dữ liệu (database). Hình 1 mô tả mô hình này, trong đó một máy chủ vật lý duy nhất xử lý mọi thứ: từ logic ứng dụng, xử lý request của người dùng cho đến lưu trữ dữ liệu trong cơ sở dữ liệu nội bộ.
Hình 1: Kiến trúc hệ thống ban đầu với một máy chủ đảm nhiệm cả ứng dụng web và cơ sở dữ liệu.
Ở mô hình single server này, luồng xử lý diễn ra như sau: người dùng gửi request từ trình duyệt web hoặc ứng dụng di động đến máy chủ thông qua một tên miền nhất định (ví dụ: <www.mysite.com> hoặc api.mysite.com). Hệ thống DNS sẽ phân giải tên miền này thành địa chỉ IP của máy chủ ứng dụng[2]. Sau đó, request được gửi thẳng tới máy chủ, máy chủ xử lý logic nghiệp vụ, truy xuất hoặc cập nhật dữ liệu trong cơ sở dữ liệu cục bộ, rồi trả về kết quả (trang HTML hoặc dữ liệu JSON) cho người dùng[3][4]. Kiến trúc một máy chủ rất đơn giản, triển khai dễ dàng và phù hợp khi lưu lượng ban đầu còn thấp. Tuy nhiên, khi số lượng người dùng tăng lên, một máy chủ đơn lẻ sẽ nhanh chóng trở thành nút thắt cổ chai: CPU, RAM, hoặc ổ đĩa của máy sẽ quá tải, dẫn đến thời gian phản hồi chậm hoặc thậm chí sập dịch vụ. Hơn nữa, tất cả các thành phần phụ thuộc vào cùng một máy chủ nghĩa là tồn tại điểm lỗi duy nhất (single point of failure - SPOF)[5]: nếu máy chủ này gặp sự cố, toàn bộ hệ thống sẽ ngừng hoạt động.
Để tiếp tục phục vụ thêm người dùng, kiến trúc hệ thống cần được thay đổi. Bước cải tiến đầu tiên thường là tách biệt tầng ứng dụng và tầng dữ liệu. Cụ thể, ta có thể sử dụng hai máy chủ riêng biệt: một máy chủ chạy ứng dụng web (web server) và một máy chủ dành cho cơ sở dữ liệu (database server)[4]. Việc tách thành hai tier (web tier và data tier) giúp mỗi thành phần có thể mở rộng độc lập: máy chủ ứng dụng có thể được nâng cấp hoặc nhân bản riêng, trong khi máy chủ cơ sở dữ liệu có thể tối ưu hóa riêng cho việc lưu trữ và truy vấn dữ liệu. Kiến trúc hai tầng này giảm tải cho máy chủ ban đầu và chuẩn bị cho các bước mở rộng tiếp theo.
Vertical scaling (mở rộng chiều dọc, hay scale-up) là phương pháp nâng cao năng lực xử lý của hệ thống bằng cách tăng cường tài nguyên phần cứng cho một máy chủ đơn lẻ[6][7]. Điều này có thể thực hiện bằng cách nâng cấp CPU mạnh hơn, bổ sung thêm RAM, sử dụng ổ cứng nhanh hơn (SSD/NVMe) hoặc thậm chí chuyển sang một máy chủ mới có cấu hình cao hơn. Ví dụ, thay vì chạy trên máy chủ 4 nhân CPU và 16GB RAM, ta có thể chuyển ứng dụng sang máy chủ 16 nhân CPU và 64GB RAM để đáp ứng lượng người dùng lớn hơn.
Vertical scaling dễ thực hiện vì về cơ bản kiến trúc phần mềm không thay đổi; ta chỉ cải thiện "sức mạnh" của máy chủ hiện tại. Trên các dịch vụ đám mây như AWS/GCP, việc scale-up có thể đơn giản như chuyển sang một instance loại lớn hơn (ví dụ nâng từ AWS t2.medium lên t2.xlarge). Trên thực tế, phương pháp này có thể phục vụ khá nhiều người dùng trước khi chạm giới hạn. Chẳng hạn, vào năm 2013 trang Stack Overflow với hơn 10 triệu người truy cập hàng tháng vẫn hoạt động chủ yếu trên một cơ sở dữ liệu chính duy nhất nhờ chạy trên máy chủ rất mạnh[8]. Điều này cho thấy mở rộng dọc có thể đi khá xa trong một số trường hợp.
Tuy nhiên, nhược điểm của vertical scaling là nó có giới hạn trần: không thể nâng cấp phần cứng mãi mãi một cách vô hạn[5]. Mỗi máy chủ đều có giới hạn vật lý, và chi phí cho các máy chủ cực mạnh tăng rất cao so với hiệu năng thu được. Hơn nữa, vertical scaling không giải quyết được vấn đề dự phòng: hệ thống vẫn phụ thuộc vào một máy chủ đơn (SPOF) - nếu máy chủ này gặp lỗi, hệ thống sẽ ngừng phục vụ[9]. Do đó, vertical scaling chỉ phù hợp ở giai đoạn đầu hoặc để tăng sức mạnh tức thời, còn về dài hạn, để phục vụ hàng triệu người dùng, chúng ta cần kết hợp với các phương pháp mở rộng khác.
Khi hệ thống vượt qua giới hạn của một máy chủ đơn, ta cần chuyển sang horizontal scaling (mở rộng theo chiều ngang, hay scale-out). Horizontal scaling nghĩa là thêm nhiều máy chủ hơn vào cụm hệ thống thay vì chỉ nâng cấp một máy chủ[6][7]. Thay vì một máy chủ "siêu to khổng lồ", ta sử dụng một cụm máy chủ (cluster) gồm nhiều máy chủ thông thường để cùng chia sẻ tải. Mỗi máy chủ mới thêm vào sẽ tăng khả năng xử lý song song, cho phép hệ thống phục vụ nhiều người dùng hơn bằng cách phân chia công việc.
Ví dụ, nếu một máy chủ có thể phục vụ tối đa 100 nghìn người dùng, thì hai máy chủ tương tự có thể phục vụ xấp xỉ gấp đôi (200 nghìn người) khi được tổ chức hợp lý. Horizontal scaling thường tiết kiệm chi phí hiệu quả hơn so với vertical scaling khi đạt đến quy mô lớn, do sử dụng nhiều máy chủ phổ thông thay vì ít máy chủ cao cấp đắt tiền. Quan trọng hơn, nó còn cải thiện tính sẵn sàng: nhiều máy chủ sẽ tạo ra dư thừa (redundancy), một máy gặp sự cố vẫn có máy khác tiếp tục phục vụ.
Tuy nhiên, mở rộng ngang đòi hỏi thay đổi đáng kể về kiến trúc ứng dụng. Cần đảm bảo các máy chủ có thể hoạt động đồng thời và phối hợp nhịp nhàng. Một thách thức lớn là việc phân phối lưu lượng giữa các máy chủ: làm sao để người dùng truy cập không biết (và không cần biết) máy chủ nào đang xử lý yêu cầu của họ. Để giải quyết vấn đề này, kiến trúc mở rộng ngang luôn đi kèm với thành phần Load Balancer (bộ cân bằng tải) ở phía trước cụm máy chủ.
Load Balancer là một thành phần trung gian có nhiệm vụ phân phối đều lưu lượng (các request từ người dùng) tới một trong số nhiều máy chủ phía sau nó[10][11]. Thay vì kết nối trực tiếp đến một máy chủ cụ thể, các người dùng sẽ gửi yêu cầu đến địa chỉ của Load Balancer, và bộ cân bằng tải sẽ quyết định chuyển mỗi yêu cầu tới máy chủ hiện có ít tải hoặc gần vị trí người dùng nhất (tùy thuật toán). Điều này đảm bảo không một máy chủ nào bị quá tải trong khi các máy khác nhàn rỗi, đồng thời tăng khả năng phục vụ tổng thể của hệ thống.
Load Balancer sau đó chuyển tiếp các yêu cầu HTTP đến một trong các web server phía sau (Server 1 hoặc Server 2). Nhờ đó, nếu một web server gặp sự cố hoặc phải bảo trì, Load Balancer có thể chuyển hướng toàn bộ lưu lượng sang các server còn lại, tránh tình trạng dịch vụ bị ngừng hẳn[13]. Tương tự, nếu lưu lượng tăng cao đột biến vượt quá khả năng của hai máy chủ, ta chỉ việc bổ sung thêm máy chủ thứ ba, thứ tư vào cụm và cập nhật trong Load Balancer - ngay lập tức tải sẽ được san sẻ sang các máy chủ mới này[13]. Load Balancer thường có các thuật toán phân phối như Round Robin (xoay vòng), Least Connections (ưu tiên server ít kết nối), hoặc IP Hash (phân theo địa chỉ IP khách) để quyết định gửi request đi đâu.
Một lưu ý khi mở rộng ngang là vấn đề trạng thái phiên làm việc (session state). Nếu ứng dụng web lưu thông tin phiên (session) của người dùng trực tiếp trên bộ nhớ máy chủ (phổ biến trong mô hình truyền thống), khi triển khai nhiều server song song, người dùng có thể bị mất trạng thái khi chuyển request sang server khác. Có hai cách giải quyết: (1) cấu hình Load Balancer theo chế độ "dính phiên" (sticky session) sao cho mỗi người dùng luôn được điều hướng về cùng một server cố định, hoặc (2) chuyển sang kiến trúc stateless - tức là không lưu trạng thái trên server mà lưu trong một kho dùng chung (ví dụ: lưu session trong database hoặc cache chung). Cách (2) thường được khuyến khích hơn ở hệ thống lớn[14][15], vì nó cho phép tự do thêm bớt máy chủ mà không ảnh hưởng phiên người dùng. Trong các hệ thống hiện đại, kiến trúc stateless giúp việc mở rộng ngang linh hoạt và đáng tin cậy hơn nhiều.
Với cụm web server và Load Balancer, tầng ứng dụng (web tier) của chúng ta đã có thể phục vụ lượng lớn người dùng mà vẫn đảm bảo tính sẵn sàng (không còn SPOF tại máy chủ ứng dụng). Tiếp theo, ta cần giải quyết nút thắt tiềm năng ở tầng dữ liệu (data tier) - cụ thể là máy chủ cơ sở dữ liệu.
Trong kiến trúc ban đầu, ta chỉ có một máy chủ cơ sở dữ liệu duy nhất. Điều này tương tự một điểm lỗi đơn: nếu database gặp sự cố, toàn bộ ứng dụng sẽ không thể truy xuất dữ liệu. Hơn nữa, khi số lượng giao dịch tăng lên (đặc biệt là đọc dữ liệu), một máy chủ database sẽ khó đáp ứng kịp. Để mở rộng tầng dữ liệu và tăng tính sẵn sàng, giải pháp phổ biến là triển khai database replication - sao chép dữ liệu sang nhiều máy chủ cơ sở dữ liệu khác nhau[16].
Cơ chế phổ biến nhất là mô hình Master-Slave replication (nhân bản chủ - tớ)[17]. Trong đó, một máy chủ Master đóng vai trò cơ sở dữ liệu chính, chịu mọi yêu cầu ghi (thêm, cập nhật, xóa dữ liệu). Một hoặc nhiều máy chủ Slave sẽ sao chép dữ liệu từ Master (thường qua cơ chế log sao chép) và chủ yếu phục vụ các yêu cầu đọc. Ứng dụng sẽ được cấu hình để: tất cả các lệnh ghi (INSERT, UPDATE, DELETE) gửi đến Master, còn các truy vấn đọc (SELECT) có thể gửi đến một trong các Slave. Vì hầu hết ứng dụng thực tế có tỷ lệ đọc cao hơn nhiều lần so với ghi, mô hình master-slave giúp chia tải rất hiệu quả - Master gánh việc ghi, các Slave chia nhau phục vụ đọc[18][19].
Lợi ích của việc nhân bản cơ sở dữ liệu thể hiện trên cả hiệu năng lẫn độ tin cậy:
Triển khai replication thường đi kèm với việc điều chỉnh ứng dụng hoặc tầng giao tiếp để routing đúng loại query đến đúng server. Nhiều hệ quản trị CSDL quan hệ (MySQL, PostgreSQL, etc.) hỗ trợ sẵn chức năng replication. Bên cạnh master-slave, còn có các cấu hình nâng cao như multi-master replication (nhiều master đồng thời) hoặc circular replication (chu trình master-master)[24], nhưng các mô hình này phức tạp và nằm ngoài phạm vi bàn luận ở đây. Trong hầu hết các trường hợp, master-slave là đủ để mở rộng khả năng phục vụ đọc cho hệ thống. Lưu ý rằng việc mở rộng ghi (write) khó khăn hơn, bởi tất cả ghi đều tập trung về Master - nếu lượng ghi quá lớn, có thể cần tính đến các chiến lược khác như sharding (phân mảnh dữ liệu, sẽ đề cập sau).
Tóm lại, với replication, tầng dữ liệu của chúng ta không chỉ nhanh hơn mà còn an toàn hơn. Một cụm database có master và nhiều slave kết hợp với cụm web server + Load Balancer tạo thành kiến trúc đa máy chủ vững vàng, loại bỏ được nhiều điểm nghẽn so với kiến trúc ban đầu.
Sau khi mở rộng cả web tier và data tier, một vấn đề thường nảy sinh khi lượng người dùng ngày càng lớn là độ trễ truy cập dữ liệu. Cho dù đã có nhiều bản sao database, việc truy vấn cơ sở dữ liệu (đặc biệt khi phải tính toán phức tạp hoặc join nhiều bảng) vẫn tốn thời gian và tài nguyên. Thống kê cho thấy truy cập dữ liệu chậm là nguyên nhân chính khiến ứng dụng bị giảm throughput khi tải tăng. Giải pháp để cải thiện thời gian phản hồi là bổ sung một tầng bộ nhớ đệm (cache) nằm giữa ứng dụng và database[25].
Cache là một hệ thống lưu trữ tạm thời các kết quả dữ liệu hay được truy cập hoặc tốn chi phí tính toán, thường đặt trong bộ nhớ RAM tốc độ cao, nhằm trả lời các yêu cầu lặp lại nhanh hơn[26]. Nguyên tắc hoạt động rất đơn giản: khi nhận một request, ứng dụng trước tiên kiểm tra xem kết quả cho request đó có sẵn trong cache chưa. Nếu cache hit (tìm thấy dữ liệu trong cache), ứng dụng có thể lấy trực tiếp dữ liệu từ cache và trả về cho người dùng, bỏ qua việc hỏi database[27]. Ngược lại, nếu cache miss (dữ liệu chưa có trong cache), ứng dụng sẽ truy vấn database như bình thường, sau đó lưu kết quả vào cache để lần sau truy cập nhanh hơn[27]. Bằng cách này, số lần truy cập cơ sở dữ liệu thực sự giảm đi đáng kể, giảm tải cho database và tăng tốc độ phản hồi cho người dùng.
Có hai kiểu cache chính trong kiến trúc web: cache phía client (trình duyệt có thể lưu lại các tài nguyên tĩnh) và cache phía server. Ở đây ta tập trung vào cache phía server - ví dụ điển hình là sử dụng một máy chủ cache riêng như Redis hoặc Memcached. Những hệ thống này lưu dữ liệu dưới dạng key-value trong RAM và có thể được đặt trên một server độc lập hoặc cùng máy với ứng dụng. Việc tích hợp cache server khá đơn giản do hầu hết cache server phổ biến đều cung cấp sẵn API/clients cho nhiều ngôn ngữ lập trình, giúp ứng dụng đọc/ghi cache dễ dàng. Chẳng hạn, với Memcached hoặc Redis, ta có các lệnh GET/SET khóa-giá trị để thao tác trên cache.
Quan trọng là lựa chọn chiến lược cache hiệu quả. Cần xác định dữ liệu nào nên đưa vào cache - lý tưởng là những dữ liệu truy cập nhiều lần, trong khi dữ liệu ít dùng hoặc thay đổi liên tục thì không nên cache lâu. Bên cạnh đó, cache hoạt động trên RAM nên thường là lưu trữ tạm, dữ liệu có thể mất khi cache server khởi động lại; do đó không dùng cache cho dữ liệu cần lưu bền vững (persistency) mà chỉ để tăng tốc tạm thời. Ta cũng cần thiết lập chính sách hết hạn (TTL) cho cache: mỗi mục dữ liệu cache nên có thời hạn để tự động xóa sau một khoảng thời gian, đảm bảo dữ liệu không quá cũ[28]. Nếu đặt TTL quá ngắn, cache sẽ bị làm trống thường xuyên và giảm hiệu quả; nếu quá dài, cache có nguy cơ trả dữ liệu lỗi thời. Một vấn đề khác là duy trì tính nhất quán giữa cache và nguồn dữ liệu: khi dữ liệu gốc thay đổi, cần cập nhật hoặc vô hiệu hóa cache tương ứng, nếu không hệ thống có thể phục vụ dữ liệu cũ[29]. Các chiến lược nhất quán phổ biến gồm: xóa cache ngay khi ghi dữ liệu mới (write-through cache), hoặc gắn cờ hết hạn ngắn cho dữ liệu vừa thay đổi.
Mặc dù cache có thể là một "vũ khí bí mật" giúp hệ thống tăng sức chịu tải, nó cũng cần được thiết kế dự phòng. Cache server đơn lẻ cũng có thể trở thành SPOF - nếu cache hỏng, hệ thống đột ngột bị dồn truy cập xuống database, dễ gây quá tải. Vì vậy, trong môi trường lớn thường triển khai cụm cache phân tán (nhiều node Redis/Memcached tạo thành cluster, hoặc sử dụng dịch vụ cache managed trên cloud có tính năng tự động failover). Các kỹ thuật như consistent hashing được áp dụng để phân bố key trên nhiều node cache và giảm thiểu ảnh hưởng khi một node bị mất[30]. Ngoài ra, khi dung lượng cache đầy, ta cần áp dụng chính sách loại bỏ (eviction) - ví dụ phổ biến là LRU (Least Recently Used - loại bỏ mục lâu không dùng nhất)[31].
Nhìn chung, thêm tầng cache có thể cải thiện lớn hiệu năng hệ thống. Một nghiên cứu chỉ ra rằng nếu không có cache, mọi request đều phải truy vấn database, dẫn đến tải DB nhanh chóng bị quá tải, độ trễ tăng cao và khả năng mở rộng kém; còn với cache, dữ liệu phổ biến được phục vụ ở tốc độ mili-giây ngay tại bộ nhớ, giúp hệ thống đáp ứng được lượng người dùng lớn hơn rất nhiều[32].
Ngoài cache dữ liệu động, một kỹ thuật khác để giảm tải cho server là sử dụng CDN (Content Delivery Network) cho nội dung tĩnh. CDN là mạng lưới các máy chủ phân bố trên toàn cầu, lưu bản sao các nội dung tĩnh như hình ảnh, video, file CSS/JS... Gần giống cache nhưng ở cấp độ mạng, CDN sẽ trả nội dung từ máy chủ gần người dùng nhất, giảm độ trễ do khoảng cách địa lý[33]. Việc sử dụng CDN giúp giảm lượng request tĩnh đổ về server nguồn, dành tài nguyên cho xử lý logic động, và cải thiện trải nghiệm người dùng ở các khu vực xa trung tâm dữ liệu chính. Hiện nay, CDN và caching là những thành phần không thể thiếu để các hệ thống lớn phục vụ hàng triệu người dùng một cách nhanh chóng và ổn định.
Khi hệ thống phát triển đến mức phục vụ hàng triệu người dùng, không phải tất cả tác vụ đều nên thực hiện một cách đồng bộ (synchronous) trong quá trình trả lời request người dùng. Nhiều công việc có thể được tách riêng để xử lý bất đồng bộ (asynchronous) nhằm giải phóng nhanh luồng phản hồi cho người dùng và tăng khả năng mở rộng. Ví dụ, trong một ứng dụng web, khi người dùng đăng tải một bức ảnh, server có thể cần tạo các phiên bản ảnh thu nhỏ, chạy thuật toán lọc ảnh, hoặc gửi email thông báo... Những tác vụ này thường tốn thời gian và không cần thiết phải hoàn thành ngay lập tức trước khi trả về kết quả cho người dùng. Đây là lúc chúng ta cần đến Message Queue (hàng đợi thông điệp).
Message Queue là một hệ thống đóng vai trò như một kênh trung gian cho phép các thành phần trong hệ thống giao tiếp một cách bất đồng bộ thông qua việc gửi và nhận thông điệp (messages) qua một hàng đợi chung[34][35]. Thành phần gửi thông điệp gọi là producer (hoặc publisher), thành phần nhận và xử lý thông điệp gọi là consumer (hoặc subscriber)[35]. Khi ứng dụng chính cần thực hiện một tác vụ dài, thay vì xử lý ngay, nó sẽ đóng gói thông tin tác vụ vào một message và đẩy vào queue rồi ngay lập tức tiếp tục công việc khác. Ở phía bên kia, một hoặc nhiều worker (tiến trình xử lý nền) sẽ liên tục lắng nghe queue, lấy ra message khi có và tiến hành xử lý tác vụ tương ứng. Kiến trúc này mang lại hai lợi ích quan trọng:
Có nhiều hệ thống hàng đợi thông điệp phổ biến, như RabbitMQ, Apache Kafka, Amazon SQS, v.v. Lựa chọn giải pháp phù hợp tùy vào trường hợp sử dụng. RabbitMQ hỗ trợ tốt cho mô hình tác vụ ngắn, đảm bảo tin nhắn được giao nhận đáng tin cậy (ACK/NACK) - phù hợp làm hàng đợi tác vụ như gửi email, xử lý ảnh…[38]. Kafka thiết kế cho thông lượng cực cao và khả năng lưu trữ dòng sự kiện (event stream) bền vững, phù hợp cho các hệ thống log, analytics hoặc làm hàng đợi trong kiến trúc microservices lớn. Dù công nghệ gì, việc tích hợp message queue cũng hướng đến mục tiêu tách rời các thành phần, giúp hệ thống "mềm dẻo" hơn trước tải lớn. Thực tế tại DoorDash, việc chuyển từ RabbitMQ sang Kafka đã giúp loại bỏ điểm nghẽn và dừng các sự cố quá tải, đạt được hệ thống ổn định hơn ở quy mô lớn[39].
Tóm lại, tầng message queue cho phép kiến trúc hệ thống chuyển nhiều công việc sang chế độ bất đồng bộ, tăng thông lượng và độ linh hoạt khi mở rộng. Đây là thành phần quan trọng trong nhiều hệ thống quy mô web lớn hiện nay.
Các kỹ thuật mở rộng trình bày ở trên thường được triển khai dần dần khi hệ thống phát triển. Đến một ngưỡng nhất định (hàng triệu người dùng hoặc hơn), kiến trúc nguyên khối (monolithic) ban đầu có thể không còn phù hợp để phát triển thêm. Lúc này, nhiều công ty lựa chọn chuyển đổi sang kiến trúc Microservices - tức là tách hệ thống thành nhiều dịch vụ nhỏ, độc lập và phối hợp với nhau thông qua API hoặc message queue. Mỗi microservice phụ trách một nghiệp vụ hoặc thành phần cụ thể (ví dụ dịch vụ người dùng, dịch vụ thanh toán, dịch vụ tìm kiếm...) và có thể được triển khai, mở rộng một cách độc lập. Điều này giúp đội ngũ phát triển có thể làm việc song song trên các phần khác nhau, triển khai tính năng mới nhanh hơn mà ít ảnh hưởng lẫn nhau. Về mặt hiệu năng, microservices cho phép tinh chỉnh và scale riêng từng thành phần: dịch vụ nào tải nặng có thể chạy trên nhiều instance hơn, dịch vụ ít tải chỉ cần vài instance, thay vì tất cả dồn trong một khối. Tuy nhiên, microservices cũng mang lại thách thức về độ phức tạp (quản lý nhiều dịch vụ, giao tiếp liên dịch vụ, giám sát, phát hiện lỗi...). Do đó, nhiều hệ thống lớn chuyển sang microservices từng bước, kết hợp với việc xây dựng các công cụ quan trắc (monitoring), logging tập trung và tự động triển khai (CI/CD) để quản lý hiệu quả hệ thống phân tán.
Bên cạnh kiến trúc ứng dụng, cơ sở hạ tầng triển khai cũng đóng vai trò quan trọng trong khả năng mở rộng. Xu hướng hiện nay là sử dụng điện toán đám mây (cloud) thay vì tự quản lý máy chủ vật lý. Các nhà cung cấp cloud như AWS, Google Cloud, Azure cung cấp sẵn nhiều dịch vụ hỗ trợ scalability: từ Load Balancer dưới dạng dịch vụ (AWS ELB), cơ sở dữ liệu phân tán (Amazon RDS, Aurora), cache phân tán (Amazon ElastiCache - Redis/Memcached managed), đến hàng đợi và stream (Amazon SQS, Kafka trên AWS MSK). Đặc biệt, cloud cho phép thiết lập auto-scaling - tự động thêm/bớt instance máy chủ ứng với mức tải thực tế, nhờ đó hệ thống có thể co giãn linh hoạt mà không tốn chi phí dư thừa khi lưu lượng giảm. Việc triển khai đa khu vực (multi-region) cũng trở nên dễ dàng với cloud, giúp hệ thống phục vụ người dùng toàn cầu với độ trễ thấp cũng như dự phòng thảm họa (nếu một data center gặp sự cố, có data center ở khu vực khác thay thế)[40][41].
Song song với cloud, công nghệ container hóa (tiêu biểu là Docker) và bộ điều phối container như Kubernetes đã cách mạng hóa cách chúng ta triển khai và mở rộng ứng dụng. Docker cho phép đóng gói ứng dụng cùng môi trường chạy vào các container nhẹ, nhất quán, giúp việc triển khai trên nhiều máy trở nên đơn giản và loại bỏ hiện tượng "chạy được trên máy tôi". Kubernetes (K8s) hỗ trợ quản lý cụm container, tự động phân phối container lên các node, tái khởi động container khi lỗi, cân bằng tải nội bộ và quan trọng là tự động scale số lượng container dựa trên nhu cầu (Horizontal Pod Autoscaling). Với Kubernetes trên cloud (như GKE, EKS), ta có thể thiết lập các deployment cho từng microservice, quy định số bản sao tối thiểu/tối đa, CPU/RAM giới hạn... Hệ thống sẽ tự động tạo thêm container khi tải tăng đến ngưỡng và giảm đi khi tải giảm, đảm bảo sử dụng hiệu quả tài nguyên và đáp ứng kịp thời nhu cầu người dùng.
Tựu trung, việc ứng dụng microservices kết hợp với hạ tầng cloud và container mang lại một kiến trúc linh hoạt và dễ mở rộng cho hệ thống lớn. Các kỹ thuật này tuy không trực tiếp "tăng sức mạnh" phần cứng, nhưng giúp chúng ta tổ chức hệ thống tốt hơn, dễ dàng áp dụng các mô hình scale (như đã bàn ở trên) cho từng phần của hệ thống một cách tối ưu.
Hành trình mở rộng hệ thống từ 0 đến hàng triệu người dùng là một quá trình liên tục học hỏi và cải tiến. Qua từng giai đoạn, chúng ta đã thấy cần áp dụng những thay đổi kiến trúc khác nhau:
Để hệ thống quy mô lớn vận hành ổn định, ngoài các thành phần chính kể trên, không thể thiếu các công cụ giám sát (monitoring) và tự động hóa. Việc giám sát liên tục hiệu năng, log và hành vi hệ thống giúp sớm phát hiện điểm nghẽn hay sự cố tiềm ẩn. Tự động hóa trong CI/CD, quản lý cấu hình, auto-scaling... giúp phản ứng nhanh trước thay đổi và giảm thiểu lỗi do thao tác thủ công. Như kinh nghiệm thực tế cho thấy, xây dựng được một hệ thống stateless tối đa, có dự phòng ở mọi lớp, cache thật nhiều khi có thể, phân vùng dữ liệu hợp lý và theo dõi hệ thống chặt chẽ chính là nền tảng để mở rộng tới hàng triệu người dùng và hơn thế nữa[42]. Mỗi hệ thống cụ thể có thể cần điều chỉnh chi tiết khác nhau, nhưng những nguyên tắc kiến trúc và kỹ thuật mở rộng trình bày trong bài viết này sẽ cung cấp một khung định hướng vững chắc trên con đường từ zero đến millions.
Tài liệu tham khảo: System Design Interview - An insider's guide (Alex Xu), blog Viblo.asia (Ren0503 dịch)[43], Medium GeeksforGeeks[44][45], cùng nhiều nguồn kỹ thuật uy tín khác như tài liệu AWS, IBM về scalable architecture.
[1] Khả năng mở rộng hệ thống website (Scalability) - Wecan GroupWecan Group
https://wecan-group.com/Kha-nang-mo-rong-he-thong-website-Scalability/
[2] [3] [4] [5] [7] [8] [9] [11] [12] [13] [16] [17] [18] [19] [20] [21] [22] [23] [24] [28] [29] [31] [42] [43] Thiết Kế Hệ Thống Cho Hàng Triệu Người Dùng - Viblo
https://viblo.asia/p/bai-dich-thiet-ke-he-thong-cho-hang-trieu-nguoi-dung-ByEZkA7W5Q0
[6] [10] [14] [15] [25] [26] [27] [33] [34] [35] [36] [37] [40] [41] System Design - Scaling from Zero to Millions Of Users | by Rahul Kapoor | Geek Culture | Medium
https://medium.com/geekculture/system-design-scaling-from-zero-to-millions-of-users-deca270ef784
[30] [32] Designing a Distributed Cache: Redis and Memcached at Scale - DEV Community
https://dev.to/sgchris/designing-a-distributed-cache-redis-and-memcached-at-scale-1if3
[38] When to use RabbitMQ over Kafka? [closed] - Stack Overflow
https://stackoverflow.com/questions/42151544/when-to-use-rabbitmq-over-kafka
[39] Eliminating Task Processing Outages by Replacing RabbitMQ with ...
https://careersatdoordash.com/blog/eliminating-task-processing-outages-with-kafka/
[44] [45] Scale From Zero To Million of Users - GeeksforGeeks
https://www.geeksforgeeks.org/system-design/scale-from-zero-to-million-of-users/
Học cách viết prompt hiệu quả cho ChatGPT, Gemini, Claude, Mistral. Tăng độ chính xác, sáng tạo và năng suất khi làm việc với AI.
Nghiên cứu thực nghiệm so sánh hiệu năng, chi phí và độ tin cậy giữa Monolith, Modular Monolith và Microservices. Khám phá ngưỡng tối ưu để chuyển đổi kiến trúc, tránh scale quá sớm gây lãng phí tài nguyên hoặc quá muộn làm giảm hiệu suất. Bao gồm case study từ Amazon Prime Video cắt giảm 90% chi phí khi quay về monolith.
Khám phá quá trình chuyển từ Waterfall sang Agile/Scrum và tác động thực tế đến hiệu suất, chất lượng phần mềm, khách hàng và đội ngũ phát triển.
© 2025 devhouse. All rights reserved.