#5 - Một triệu lon bia
Tất cả mọi người sinh ra đều có quyền bình đẳng, request tới server thì không
Bài viết thuộc series 50 Days of System Design, một series về thiết kế hệ thống dành cho người đọc chậm và muốn hiểu sâu.
Vào một ngày hè nóng nực năm 2021, trước trận đấu giữa Việt Nam - UAE, Budweiser quyết định gửi tặng người hâm mộ Việt Nam một triệu lon bia để cùng hòa nhịp với niềm hân hoan chiến thắng của cả nước. Đối tác phân phối của Budweiser là Shopee. Hôm đầu tiên của chiến dịch, chúng tôi đang họp thì bỗng dưng alert noti bắn loạn xạ, latency tăng đột biến và rồi điều tồi tệ nhất xảy ra: database sập! Nguyên nhân là 1 lượng lớn fan hâm mộ đã rủ nhau đổ bộ “lấy” bia khiến server quá tải, một phần của Shopee tê liệt. Hôm đó là một ngày dài của các kỹ sư sàn S.
Overload
Overload hay “quá tải” là hiện tượng xảy ra khi lượng truy cập tăng đột biến, server nhận được nhiều request hơn khả năng chịu đựng thông thường của nó. Có nhiều nguyên nhân dẫn đến hiện tượng này:
Số lượng người dùng tăng quá nhanh trong 1 khoảng thời gian ngắn, ví dụ như những dịp flash sale của các sàn thương mại điện tử.
Server bị hacker tấn công bão hòa (DDoS attack)
Hiệu ứng Domino: 1 server trong cluster gặp sự cố, lưu lượng truy cập từ server bị lỗi sẽ được load balancer chuyển hướng sang các server còn lại. Điều này dẫn đến sự gia tăng đột ngột về tải cho các server này; và tới lượt chúng, quá tải.
Triệu chứng ban đầu của overload là CPU và/hoặc memory utilization tăng cao, đồng thời latency tăng đột ngột. Có 2 thứ khiến hiện tượng overload thường khuếch đại chính nó và leo thang nhanh chóng đến kết cục bi thảm sập server:
khi latency vượt quá một giới hạn, client bắt đầu nhận được lỗi timeout. Mọi tài nguyên được sử dụng để xử lý những request bị timeout này đều đổ sông đổ bể.
the last thing a system should do in an overload situation, where resource is constrained, is waste work.
khi nhận được lỗi timeout, client thường sẽ retry. Giả sử một service retry 3 lần mỗi khi timeout, một chuỗi 4 service gọi nhau có thể dẫn đến service cuối cùng nhận một lượng request gấp 64 lần (hiện tượng retry storm):
Ba phương pháp phổ biến để giảm thiểu tác động của Overload là:
Auto-scaling
Load shedding
Circuit breaker
Trong bài viết này, chúng ta sẽ cùng tìm hiểu chi tiết về cơ chế hoạt động của phương pháp Load shedding.
Load Shedding
Ý tưởng của Load shedding rất tự nhiên: khi server “sắp” quá tải, chúng ta sẽ bắt đầu từ chối xử lý các request mới từ client. Mục đích là để server tập trung resource xử lý các request nó đã chấp nhận trước đó. Với những request bị từ chối, client sẽ nhận được lỗi service unavailable. Nói cách khác, với Load shedding, chúng ta chấp nhận đánh đổi một phần availability để duy trì hiệu năng và bảo vệ server khỏi crash.
CPU utilization
Phương pháp phổ biến để phát hiện tình trạng “sắp” quá tải của server là liên tục giám sát CPU utilization - mức độ tiêu thụ CPU. Trong Linux, mọi thứ đều là file, chúng ta có thể đọc chỉ số này từ file /proc/stat. Tuy nhiên, phương pháp này có điểm yếu là khó mà chọn được một ngưỡng CPU utilization hoàn hảo để quyết định khi nào nên từ chối xử lý request mới. Nó có thể là 30%, 50% hay 70%, và thường ngưỡng này sẽ thay đổi theo thời gian:
Chọn ngưỡng quá thấp sẽ ảnh hưởng đến availability.
Chọn ngưỡng quá cao có thể làm tăng latency, giảm throughput.
Để chọn được một ngưỡng hợp lý, thường cần tiến hành stress testing hệ thống nhiều lần. Brendan Gregg, tác giả của cuốn sách "Systems Performance", có một bài viết chi tiết về những hạn chế của chỉ số CPU, với tiêu đề "CPU Utilization is Wrong".
Throughput
Một cách load shedding phổ biến khác là dựa vào throughput - QPS (query per second). Cách này phù hợp với các service có latency tương đối ổn định và đồng đều. Tương tự như cách dùng CPU utilization, chúng ta cần chạy stress test cẩn thận để xác định được mức QPS cao nhất hệ thống chịu được mà không làm tăng latency quá nhiều.
In-flight request
Với các hệ thống có latency không ổn định, ví dụ khi latency phụ thuộc vào request hoặc kích thước payload của response, việc chỉ dựa vào QPS để load shedding không còn chính xác. Chúng ta cần 1 chỉ số kết hợp được cả throughput và latency.
Queuing theory một lần nữa trở nên hữu ích: Little’s Law trong queuing theory phát biểu rằng
độ dài trung bình của một hàng đợi bằng arrival rate nhân với thời gian xếp hàng trung bình.
Với bài toán load shedding: L chính là trung bình số lượng request đang được xử lý ở server (in-flight request), λ là throughput, còn W là latency. Biết được λ và W, ta có thể ước lượng được số lượng in-flight request trung bình. Ví dụ: nếu latency trung bình của hệ thống là 100 (ms), throughput trung bình là 1000 (qps), thì số lượng in-flight request trung bình sẽ là 0.1 (s) x 1000 (qps) = 100. Khi số lượng in-flight request thực tế lớn hơn 100 rất nhiều, ta biết rằng hệ thống đang bị overload và cần được load shed. Phần khó nhất là chọn được in-flight request limit phù hợp, một lần nữa đòi hỏi phải tiến hành stress test. Nhược điểm của phương pháp này là:
Latency trung bình có xu hướng thay đổi theo thời gian, đòi hỏi ta phải tính lại in-flight request limit thường xuyên.
không phù hợp với các service phụ thuộc nhiều vào I/O (I/O bound), ví dụ các service cần gọi nhiều tới API của bên thứ 3, hoặc dành nhiều thời gian access database hoặc file system. Những service kiểu này dành phần lớn thời gian để chờ đợi response từ network, nên số lượng in-flight request thường không phản ánh chính xác CPU utilization.
Chúng ta cần một chỉ số tốt hơn, có thể loại trừ được những in-flight request chờ I/O này.
Runnable process queue length
Ôn lại kiến thức hệ điều hành (OS) một chút: các process (tiến trình) của OS có 5 trạng thái: new, ready, running, waiting, terminated.
Khi server đọc/ghi dữ liệu từ disk hoặc network, process sẽ chuyển trạng thái từ running sang waiting. Ở trạng thái này, process tạm dừng và không sử dụng CPU.
Khi quá trình đọc/ghi hoàn thành, process sẽ “tỉnh dậy” và chuyển sang trạng thái ready. Các process ready này được lưu trữ trong 1 hàng đợi có tên là ready queue, sẵn sàng để được thực thi.
CPU scheduler lựa chọn process từ ready queue để thực thi dựa theo một thuật toán nào đó (ví dụ Completely Fair Scheduler).
Sự chuyển đổi trạng thái của process trong OS cho ta một chỉ số hoàn hảo để dự đoán hiện tượng overload: runnable process number = ready process number + running process number. Số lượng runnable process này không bao gồm các process đang chờ I/O nên đã khắc phục được nhược điểm của phương pháp đếm in-flight request.
Ví dụ: giả sử server có 16 CPU core, nếu số lượng runnable process trung bình lớn hơn 16, ta có thể kết luận server đang bị overload, và cần được load shed. Tương tự như CPU utilization, ta có thể tính số lượng runnable process đơn giản bằng cách đọc file /proc/stat.
Prioritizing request
Tất cả mọi người sinh ra đều có quyền bình đẳng, request tới server thì không. Khi server quá tải và bắt đầu load shedding, nó có cơ hội để lựa chọn request nào để từ chối xử lý. Ví dụ:
request /health-check từ load balancer là request mà (bằng mọi giá) server phải ưu tiên xử lý: nếu server không phản hồi kịp thời, load balancer sẽ dừng gửi request mới tới nó.
tùy thuộc vào business model, một vài loại request sẽ được ưu tiên xử lý. Ví dụ, với một sàn thương mại điện tử, đó là những request liên quan đến authentication, catalog listing và order. Tương tự, Netflix phân loại request thành 3 nhóm: NON_CRITICAL, DEGRADED_EXPERIENCE và CRITICAL dựa vào mức độ ảnh hưởng tới trải nghiệm người dùng.
Kết
Hành trình đọc và tìm hiểu về Load shedding khiến tôi càng thấm thía tầm quan trọng của việc hiểu và vận dụng những kiến thức cơ bản của khoa học máy tính.
Mùa hè năm đó, sàn S chưa có load shedding, gần 5 năm rồi, không rõ bây giờ đã có chưa?
Bài viết mở mang đầu óc ạ.
Mong chờ bài tiếp theo của anh.
In my opinion, Load shedding is bad solution for strict SLA system. can't buy some products, it's okay I can buy it later. Drop my money or can't access to my resources during important demonstration/events? Unacceptable.