English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

khách
1 / ?
trở lại bài học

Hai cách để xử lý tải cao hơn

Chào mừng

Khi một dịch vụ bắt đầu quá tải, người vận hành phải đối mặt với một lựa chọn. Làm cho máy hiện có lớn hơn (nhiều CPU hơn, nhiều RAM hơn, ổ đĩa nhanh hơn). Hoặc thêm nhiều máy hơn, mỗi máy thực hiện cùng một công việc.

Con đường đầu tiên được gọi là mở rộng dọc (scale up). Con đường thứ hai được gọi là mở rộng ngang (scale out).

Bài học này dạy tại sao hầu như mọi kiến trúc web hiện đại đều chọn mở rộng ngang & thuộc tính nào của khối lượng công việc khiến lựa chọn đó khả thi. Câu trả lời ẩn trong một từ: trạng thái.

Đến cuối bài, bạn sẽ hiểu:

- Các đường cong chi phí của việc mở rộng theo chiều dọc so với chiều ngang & khi nào mỗi phương pháp phù hợp

- Ý nghĩa thực tế của 'có trạng thái' (stateful) & 'không trạng thái' (stateless), & tại sao một trong hai có thể nhân bản với chi phí thấp

- Công thức để xác định kích thước của một đội bản sao (replica fleet) dưới tải trọng dự kiến & tải trọng đột biến

- Quy tắc dự phòng (headroom) giúp một tầng không sụp đổ vượt qua điểm khuỷu hàng đợi (queueing knee)

- Trạng thái (state) phải được đặt ở đâu (nó không bao giờ biến mất) & cách đẩy nó ra khỏi các tầng cần mở rộng

Tại sao Mở rộng theo chiều ngang chiến thắng khi vượt qua ngưỡng nhất định

Mở rộng theo chiều dọc: Một hộp lớn hơn

Ưu điểm: đơn giản. Không cần thay đổi mã. Không cần phối hợp. Cùng một tiến trình giờ có nhiều CPU hơn.

Nhược điểm: có trần. VM thương mại lớn nhất có bộ nhớ RAM & lõi CPU hữu hạn. Vượt qua mức đó, không có tiền nào mua thêm được dung lượng. Chi phí tăng phi tuyến tính khi vượt qua điểm tối ưu của gói dịch vụ nhà cung cấp. Một sự cố trên máy duy nhất đó sẽ khiến toàn bộ dịch vụ ngừng hoạt động.

Mở rộng theo chiều ngang: Nhiều máy nhỏ hơn

Ưu điểm: không có trần (tùy thuộc vào mức độ sẵn sàng chi trả & phối hợp máy của bạn). Dung lượng tăng tuyến tính theo số bản sao, có thể dự đoán được. Sự cố của một bản sao chỉ làm mất 1/N dung lượng, không phải 100%.

Nhược điểm: đòi hỏi khối lượng công việc phải hỗ trợ điều này. Một số khối lượng công việc (một cơ sở dữ liệu lớn duy nhất, một máy chủ trò chơi có trạng thái đang giữ các phiên hoạt động) chống lại việc mở rộng theo chiều ngang. Việc phối hợp & phân phối tải trở thành mối quan tâm về mặt vận hành.

Điểm giao nhau: bất kỳ dịch vụ sản xuất nào cần sống sót sau sự cố của bất kỳ máy nào cũng phải chạy trên ít nhất hai máy. Một khi bạn chấp nhận hai máy, bạn đã chọn mở rộng theo chiều ngang. Từ đó, câu hỏi không còn là 'chúng ta có nên làm không?' mà là 'chúng ta có thể thêm bản sao tiếp theo với chi phí rẻ đến mức nào?'

Yếu tố then chốt: một khối lượng công việc không giữ trạng thái theo từng yêu cầu trên chính máy đó. Khi đó, bất kỳ bản sao nào cũng có thể trả lời bất kỳ yêu cầu nào, & việc thêm một bản sao sẽ tăng dung lượng mà không cần phối hợp.

Mở rộng theo chiều dọc so với chiều ngang: đường cong chi phí & trần

Một nhóm đang chạy một dịch vụ trên một VM duy nhất với 8 CPU, xử lý 800 yêu cầu/giây ở mức sử dụng CPU 60%. Họ dự kiến lưu lượng truy cập sẽ tăng 4 lần trong năm tới. Họ đang tranh luận: thuê một VM 32-CPU (thang dọc), hoặc chạy bốn VM 8-CPU giống hệt nhau sau một bộ cân bằng tải (thang ngang). Bạn khuyến nghị phương án nào, & nêu hai lý do vượt ra ngoài phạm vi dung lượng thô.

Có trạng thái (Stateful) so với Không trạng thái (Stateless) trong thực tế

Trạng thái không bao giờ biến mất, nó chỉ di chuyển

Thành phần có trạng thái (Stateful component): lưu trữ thông tin mà việc mất đi sẽ làm thay đổi hành vi. Một cơ sở dữ liệu lưu trữ tài khoản người dùng. Một bộ nhớ đệm (cache) lưu trữ token phiên. Một worker (quá trình làm việc) cố định kết nối streaming dài hạn cho một người dùng cụ thể.

Thành phần không có trạng thái (Stateless component): không lưu trữ thông tin nào mà việc mất đi sẽ gây ảnh hưởng. Một tầng web (web tier) đọc yêu cầu, truy vấn cơ sở dữ liệu, & ghi phản hồi. Mỗi yêu cầu đều độc lập; tầng này không ghi nhớ gì giữa các yêu cầu.

Nhận thức quan trọng: trạng thái không bao giờ biến mất khỏi hệ thống. Nó di chuyển đến một tầng được thiết kế để lưu trữ nó (một cơ sở dữ liệu, một cụm Redis, một kho lưu trữ đối tượng). Các tầng tiếp nhận lưu lượng truy cập (traffic) sau đó có thể trở nên không có trạng thái, & các tầng không có trạng thái có thể mở rộng theo chiều ngang (scale horizontally) vì bất kỳ bản sao (replica) nào cũng có thể trả lời bất kỳ yêu cầu nào.

Kiểm tra thực tế: nếu bạn ngẫu nhiên giết (kill) một tiến trình trong tầng này & khởi động lại nó, liệu có người dùng nào trải nghiệm câu trả lời sai hoặc mất phiên không? Nếu có, nó giữ trạng thái. Nếu không, nó không giữ.

Ví dụ

- Một tiến trình web Python đọc yêu cầu, truy vấn Postgres, trả về JSON: không có trạng thái. Trạng thái nằm trong Postgres.

- Một tiến trình web Python giữ giỏ mua hàng của người dùng trong bộ nhớ cục bộ: có trạng thái. Giết tiến trình sẽ làm mất giỏ hàng.

- Một máy chủ WebSocket giữ các kết nối mở với người dùng chat: có trạng thái theo nghĩa kết nối. Nếu tiến trình bị kết thúc, các kết nối sẽ bị ngắt; các client phải kết nối lại. Thông thường, những hệ thống này vẫn có thể mở rộng ngang một cách cẩn thận (sticky sessions, consistent hashing).

- Một bộ nhớ đệm Redis đặt trước Postgres: có trạng thái đối với nội dung bộ nhớ đệm, nhưng có thể chấp nhận được nếu việc bỏ lỡ bộ nhớ đệm (cache miss) là có thể dung nạp được. Sự cố ở một bản sao (replica) đồng nghĩa với việc bỏ lỡ bộ nhớ đệm, chứ không phải mất dữ liệu.

Thiết kế cho việc mở rộng ngang = đẩy trạng thái ra khỏi lớp cần được mở rộng.

Kiểm toán một Tầng Nghi Ngờ

Một nhóm vận hành API khuyến nghị trên 6 VM backend phía sau một reverse proxy. Ứng dụng: đọc ID người dùng từ yêu cầu, lấy hoạt động gần đây của người dùng từ Postgres, chạy thuật toán chấm điểm, và trả về danh sách các mục được khuyến nghị. Hai hành vi không tiêu chuẩn:

- Ứng dụng giữ một bộ nhớ đệm 'hoạt động người dùng gần đây' trong bộ nhớ tiến trình, được nạp khi có yêu cầu đầu tiên cho một người dùng, và được tái sử dụng cho các yêu cầu tiếp theo.

- Ứng dụng sử dụng sticky sessions: một khi một người dùng truy cập VM #3, tất cả các yêu cầu tiếp theo của họ sẽ được chuyển đến VM #3 (proxy được cấu hình để định tuyến dính (sticky routing) dựa trên cookie).

Xác định hành vi nào trong số hai hành vi này khiến tầng này có trạng thái & giải thích điều gì sẽ bị hỏng nếu nhóm cố gắng mở rộng từ 6 VM lên 12. Sau đó, đề xuất một thiết kế lại cho phép tầng này mở rộng ngang mà không mất đi lợi ích của bộ nhớ đệm người dùng.

Công thức bản sao (Replica)

Công thức dung lượng đơn giản nhất

Một khi một tầng (tier) trở nên không trạng thái (stateless), việc xác định kích thước của nó trở thành bài toán đại số. Bạn cần đủ bản sao (replicas) để tải trạng thái ổn định (steady-state load) đến và đi với cùng tốc độ, đồng thời có phần dự phòng (headroom) cho các đợt tăng đột biến.

Công thức:

replicas = ⌈ (peak_load × surge_factor) / per_replica_capacity ⌉ + headroom

Trong đó:

- peak_load: số yêu cầu tối đa mỗi giây (requests/second) được duy trì mà bạn kỳ vọng trong hoạt động bình thường

- surge_factor: hệ số nhân bao phủ các đợt tăng vọt ngắn hạn vượt quá mức đỉnh (thường là 1,5 lần đến 2 lần đối với lưu lượng truy cập có thể dự đoán được, 3 lần trở lên đối với lưu lượng truy cập lan truyền / không thể dự đoán)

- per_replica_capacity: số yêu cầu mỗi giây (requests/second) mà một bản sao xử lý được ở mức độ trễ và mức sử dụng chấp nhận được (thường được đo ở mức CPU 70%, không phải ở mức bão hòa)

- headroom: số bản sao bổ sung để một vài bản sao bị lỗi không làm sụp đổ toàn bộ tầng (thường là 1-2 bản sao cho các cụm nhỏ, 10-20% cho các cụm lớn hơn)

Ví dụ minh họa: một backend xử lý 100 req/s ở mức 70% CPU cho mỗi replica. Tải đỉnh là 600 req/s. Bạn dự kiến sẽ có các đợt tăng đột biến 2x thỉnh thoảng. Bạn muốn hệ thống vẫn hoạt động được khi 2 replica bị lỗi.

replicas = ⌈ (600 × 2) / 100 ⌉ + 2 = 12 + 2 = 14 replicas

Quy tắc 80%

Khả năng xử lý của mỗi replica không phải là điểm bão hòa. Hãy đo lường khả năng xử lý ở mức CPU 70-80%, không bao giờ đo ở mức 100%.

Vượt quá mức sử dụng khoảng 80%, các đường cong hàng đợi sẽ tăng lên mạnh mẽ: một hàng đợi chạy trong 10 ms ở mức sử dụng 60% sẽ chạy trong 80 ms ở mức sử dụng 90%. Độ trễ, chứ không phải thông lượng, là thứ bị phá vỡ trước tiên. (Bài học đồng hành geometry_of_stateless_horizontal_scaling sẽ chứng minh đường cong này về mặt toán học.)

Tự động mở rộng (Autoscaling) so với Cấp phát tĩnh (Static Provisioning)

Tĩnh: cấp phát tài nguyên cho tải đỉnh × phần dự phòng cho các đợt tăng đột biến & chấp nhận chi phí chạy ở mức sử dụng thấp ngoài giờ cao điểm.

Tự động mở rộng: một bộ điều khiển (controller) thêm & xóa các replica dựa trên mức sử dụng quan sát được, độ trễ mục tiêu, hoặc độ sâu của hàng đợi.

Lưu ý về tự động mở rộng (Autoscaling): thời gian khởi động lạnh (cold-start) rất quan trọng. Nếu một bản sao (replica) mới mất 2 phút để khởi động, hệ thống tự động mở rộng không thể phản ứng kịp với một đợt tăng đột biến kéo dài 30 giây. Các hệ thống tự động mở rộng trưởng thành sẽ duy trì một nhóm các bản sao đã được khởi động sẵn (warm pool) ở mức vừa dưới ngưỡng mở rộng (scale-up threshold).

Công thức tính kích thước bản sao kèm ví dụ thực tế

Tính toán quy mô đội ngũ bản sao cho một dịch vụ mới

Đội ngũ của bạn dự kiến ra mắt một API cho metadata video. Các bài kiểm tra hiệu năng (benchmark) cho thấy một bản sao duy nhất xử lý được 250 req/s với mức CPU 70% và độ trễ p99 là 50 ms. Bộ phận marketing dự báo tải đỉnh (peak load) đạt 4.000 req/s trong khung giờ vàng. Một sự kiện quảng bá được lên kế hoạch có thể khiến lưu lượng tăng đột biến lên 3 lần mức đỉnh trong thời gian ngắn. Bạn muốn dịch vụ có thể chịu được 3 bản sao bị hỏng đồng thời mà không vượt quá mức sử dụng 80% trên các bản sao còn lại.

Áp dụng công thức tính bản sao để xác định quy mô đội ngũ cho đợt ra mắt. Hãy trình bày các bước tính toán của bạn, & sau đó giải thích một lý do khiến con số của bạn vẫn có thể là sai (khởi động lạnh, làm ấm bộ nhớ đệm, độ trễ của các phụ thuộc, hoặc bất kỳ lý do nào bạn có thể biện minh được).

Khởi Động Lạnh, Thoát Chậm & Các Rào Cản Thực Tế Khác

Các Hạm Đội Thực Tế Có Các Rào Cản Thực Tế

Công thức giả định rằng các bản sao (replicas) xuất hiện ngay lập tức, nhận lưu lượng ngay lập tức & loại bỏ lưu lượng ngay lập tức. Không có điều nào trong số đó đúng trong môi trường sản xuất.

Khởi động lạnh (Cold start): một bản sao mới cần khởi động hệ điều hành, khởi chạy tiến trình, tải cấu hình, làm ấm bộ nhớ đệm (warm caches) & vượt qua các kiểm tra sức khỏe. Thời gian có thể dao động từ 5 giây (khởi động lại container) đến 5 phút (khởi động đầy đủ VM + kéo ảnh). Tự động mở rộng (Autoscaling) không thể phản ứng với các đợt tăng đột biến (bursts) ngắn hơn độ trễ này.

Thoát chậm (Slow drain): một bản sao đang bị loại bỏ khỏi nhóm cần thời gian để hoàn thành các yêu cầu đang xử lý (in-flight) trước khi kết thúc. Nếu không, người dùng sẽ thấy các phản hồi bị cắt cụt. Các bộ đại lý ngược (reverse proxies) hỗ trợ chế độ thoát (dừng nhận yêu cầu mới, hoàn thành các yêu cầu đang hoạt động) nhưng quá trình này mất từ vài giây đến vài phút.

Warm pool: các đội ngũ sản xuất duy trì một nhóm các bản sao (replicas) đã được cấp sẵn tài nguyên nhưng đang ở trạng thái chờ, sẵn sàng nhận lưu lượng khi có tín hiệu. Đổi một chi phí ổn định nhỏ để lấy khả năng phản ứng nhanh khi lưu lượng tăng đột biến.

Connection draining so với giết tiến trình ngay lập tức: việc tắt dịch vụ một cách êm đẹp (graceful shutdown) là rất quan trọng. Một tín hiệu SIGTERM kích hoạt quá trình thoát kết nối (drain) sẽ mất nhiều thời gian hơn so với SIGKILL nhưng không làm gián đoạn các yêu cầu của người dùng.

Cửa sổ kiểm tra sức khỏe: một bản sao vừa khởi động có thể vượt qua lần kiểm tra sức khỏe đầu tiên trước khi nhóm kết nối cơ sở dữ liệu của nó trở nên "ấm" (sẵn sàng tối ưu); khi đó, proxy sẽ chuyển lưu lượng thực tế & khoảng mười hai yêu cầu đầu tiên sẽ bị chậm. Hãy điều chỉnh các kiểm tra sức khỏe để kiểm tra đường dẫn thực tế, chứ không chỉ là sự tồn tại của tiến trình.

Sự lan rộng của tính dính (Stickiness creep): ngay cả các tầng được coi là không có trạng thái (stateless) cũng sẽ tích lũy tính dính theo thời gian (bộ nhớ đệm CDN, bộ nhớ đệm trình phân giải DNS, nhóm kết nối). Hãy nghi ngờ những "bản sao giống hệt nhau" nhưng lại hoạt động khác nhau.

Warm Pool hay Tự động mở rộng quy mô phản ứng (Reactive Autoscaling)?

API metadata video của bạn (cùng API từ câu hỏi trước, được thiết kế với 51 bản sao cho đỉnh ổn định + tăng đột biến) trải qua một đợt tăng đột biến 30 giây lên 5 lần so với tải thông thường mỗi khi một video viral mới được tải lên. Tự động mở rộng quy mô hiện tại mất 90 giây để thêm một bản sao mới từ trạng thái lạnh (kéo hình ảnh + khởi động). Trong khoảng thời gian 90 giây đó, độ trễ tăng mạnh & một số yêu cầu bị hết thời gian chờ (time out).

Đề xuất một giải pháp. Chọn: (a) giữ tự động mở rộng quy mô phản ứng nhưng điều chỉnh nó khác đi, (b) cấp sẵn một warm pool các bản sao đang chờ, hoặc (c) cấp sẵn tĩnh cho đỉnh 5 lần vĩnh viễn. Lập luận cho lựa chọn của bạn & nêu tên một chi phí mà hai lựa chọn còn lại sẽ gây ra.

Thiết kế một tầng không trạng thái dưới các ràng buộc

Tổng hợp

Bạn đã học được lý do tại sao mở rộng ngang lại vượt trội khi vượt qua một ngưỡng nhỏ, ý nghĩa thực tế của trạng thái, cách xác định kích thước đội máy dưới tải trọng dự kiến & tải trọng đột biến, & nơi mà mở rộng ngang gặp giới hạn ở các rìa.

Áp dụng cả bốn.

Thiết kế tầng backend cho feed.example.com, một API cho dòng tin xã hội. Ràng buộc: năng lực mỗi bản sao (replica) là 200 req/s ở mức CPU 70%; tải đỉnh dự kiến là 1500 req/s; hệ số đột biến (surge factor) là 2.5x (do các câu chuyện đang thịnh hành thỉnh thoảng); chịu được 2 bản sao bị lỗi đồng thời; thời gian khởi động lạnh (cold-start) là 60 giây; các đợt đột biến có thể kéo dài 45 giây; ngân sách cho phép một số năng lực dự phòng (idle capacity) nhưng không cho phép triển khai cố định 2.5x.

Tính toán kích thước đội máy ổn định (hiển thị phép tính), chọn chiến lược xử lý đột biến (warm pool / tự động mở rộng phản ứng / cả hai) & xác định một phần trạng thái theo người dùng (per-user state) mà ứng dụng có thể đang giữ & nơi nó phải di chuyển đến để giữ cho tầng này không có trạng thái (stateless).

Khóa học này sẽ đi tiếp đến đâu

Khóa học này sẽ tiếp tục đi đến đâu

Bạn hiện đã có một mô hình tư duy hoạt động về tầng không trạng thái (stateless tier): tại sao nó có thể mở rộng quy mô, cách để xác định kích thước cho nó, điều gì sẽ bị hỏng ở các ranh giới của nó, & trạng thái (state) phải được di chuyển đến đâu khi bạn đẩy nó ra khỏi lớp cần phải mở rộng.

Bài học tiếp theo trong khóa học này (cs_distsys_ingress_egress_separation) giải quyết một vấn đề tinh tế hơn: ngay cả một tầng không trạng thái được xác định kích thước hoàn hảo cũng có thể gặp sự cố theo những cách bất ngờ khi lưu lượng truy cập đến & đi cùng chia sẻ một đường dẫn mạng. Ví dụ điển hình liên quan đến một proxy cố gắng kết nối với chính nó; giải pháp liên quan đến việc tách một tầng thành hai với các trách nhiệm khác nhau.

Bài học đồng hành: geometry_of_stateless_horizontal_scaling suy ra đường cong hàng đợi (queueing curve), Định luật Little áp dụng cho một đội ngũ bản sao (replica fleet), & ý nghĩa hình học của điểm khuỷu (knee) ở mức sử dụng 80%.

Làm tốt lắm. Hãy tiến lên.