Tuesday, May 18, 2021

Instagram đã xử lý vấn đề Thundering Herd Problem như thế nào ?

Caching là một vấn đề kinh điển khi xây dựng hệ thống để tăng tốc độ tải. Thông thường thì trong một hệ thống backend, cơ chế caching được xử tất cả hoặc một trong các cách như sau:|
  • Reversed proxy cache: cache theo từng request
  • Application cache: truy vấn dữ liệu từ đĩa thay vì gửi request đến phía backend (redis, memcached)

Phía backend thường có kỹ thuật “warming cache” - cho cache trước bằng cách chạy giả lập trước các request có tần suất gọi lớn. Chuyện gì sẽ xảy ra khi backend nhận request nhưng cache vẫn chưa kịp tạo ra ? Trong trường hợp này, giả sử 100 kết nối gọi đến cùng 1 api với cùng tham số trong cùng thời điểm. Theo cách tiếp cận naiive nhất, vì cache chưa kịp tạo, backend sẽ xử lí 100 request này như là 100 request riêng biệt. Có nghĩa là 100 query giống nhau sẽ được gửi đồng thời vào trong database. Nếu mỗi truy vấn có độ phức tạp không đáng kể thì ta có thể bỏ qua, nhưng nếu nó là một truy vấn được hội từ vài chục ngàn truy vấn con trên rất nhiều bảng (như Microsat thì hệ thống backend của chúng ta sẽ quá tải hoặc không thể xử lý hết kịp thời do vấn đề tài nguyên). CPU phải chia sức mạnh đi 100 lần để xử lí 100 task giống nhau. Đó là một sự lãng phí rất lớn, vì data trả về của 100 request này là giống nhau. Người ta định nghĩa tình huống này bằng thuật ngữ Thundering Herd.

Trong nguyên gốc, Thundering Herd có nguồn gốc lịch sử bắt nguồn từ ngành chăn nuôi, khi những người chăn bò cố lùa một số lượng bò chăn thả hơn vài ngàn con về chuồng. Chuồng gia súc thường chỉ có một cửa vào và cổng nhỏ - so với số lượng đàn bò. 

Hiểu được nguồn gốc vấn đề thì sẽ có cách xử lí thôi. Giờ sao nhỉ ? Ý tưởng để giải quyết vấn đề này chính là làm sao cho với 100 request này, backend chỉ xử lí 1 task và đưa kết quả của task này cho 100 request. Giống như hồi xưa thằng lớp trưởng đi lấy tài liệu về photo cho cả đám. Chứ đi làm mọe gì nhiều mất công. Cụ thể, mình liệt kê một số cách như sau:
  • Tầng Revered proxy cache: cho 100 request này vào hàng đợi và chỉ relay 1 cái về backend xử lí. Trả kết quả đã xử lí sau khi có kết quả. Tham khảo: mitigating-thundering-herd-problem-pbs-nginx
  • Tầng Application: dùng 1 khóa đồng bộ - mutex lock. Process nào yêu cầu cùng một tài nguyên (mỗi process là một yêu cầu truy vấn vào db) vào một hàng đợi và chỉ để 1 process để truy vấn. Kết quả của truy vấn này sẽ được trả cho các process trong hàng đợi và trả về client. 

Ở instagram, các kĩ sư cũng dùng một nguyên lí tương tự để giải quyết vấn đề này. Thay vì chứa dữ liệu chính trong cache thì họ bọc dữ liệu này trong Promise rồi lưu trữ Promise trên cache. Nếu một request tới mà không xuất hiện trong cache thì hệ thống sẽ kiểm tra thử nếu Promise của dữ liệu cần được truy xuất có trong cache hay không và tạo ra một Promise mới nếu không có. Do đó, khi các requests khác được gửi tới thì chúng có thể chia sẽ cái Promise này cho cùng một dữ liệu trong lúc chờ đợi hệ thống backend truyền dữ liệu về. Việc tận dụng Promise đã giúp Instagram hạn chế việc thundering herd problem xảy ra nhiều và giúp họ triển khai hoặc thêm/ xóa cluster dễ dàng hơn.

Happy reading.