Promise Pool - Giảm "down" hiệu quả cho server

May 26, 2021

## ❖ Giới thiệu Lại là Tâm Angel đây, các bạn đã biết mình qua bài viết ["API Request, to và dài quá cũng khổ!"](https://www.techbasevn.com/blog-technical/api-request-to-va-dai-qua-cung-kho.html) vừa rồi và hôm nay mình xin được phép tiếp tục bài thứ 2 của series 2 bài kỹ thuật trong Javascript rất hay và khá hiệu quả trong mô hình Micro-services, bài toán khi gửi request đến 1 API nào đó đế lấy dữ liệu lớn mà bài viết trước đã được mình đề cập.
#### Kiến thức nên có trước khi đọc bài viết - Đọc bài "API Request, to và dài quá cũng khổ!" trước nhé. - Khái niệm Object Pool Design Pattern
## ❖ Mở đầu Để tiếp tục chương trình, sau khi thông qua 2 giải pháp ở bài viết trước, các bạn thấy đó, kênh đào Suez vẫn còn đang bị stress với tình hình 1 con tàu lớn chia thành hàng trăm con tàu nhỏ và gửi đi cùng 1 lúc, gây tắc nghẽn giao thông đường thuỷ, dẫn tới việc các quan chức nhà nước họ phải căng thẳng tột độ để đáp ứng nhu cầu của người sử dụng. Đúng là tránh vỏ dưa gặp vỏ dừa, vừa mới tưởng là giải quyết được rồi ai ngờ lại sinh ra vấn đề khác. Vậy thì mình làm sao để cho giảm stress được cho họ đây? #### ▷ Bài toán Mình xin mượn lại tấm hình mình chụp của bài cũ để nói lên bài toán của bài này nhé:
Vâng, chia request parameters ra thành nhiều request nhỏ hơn, mỗi request đi chứa 500 movieIds. Sẽ thuận lợi và êm đẹp biết bao nếu server chúng ta khá mạnh và có khả năng auto-scale resource để xử lý một lúc nhiều request. Nhưng, đời không đẹp như ta tưởng, 1 ngày đẹp trời như bao ngày khác, vẫn phương án đó, nhưng array của movieIds lại tăng lên một xí, thế là chạm ngưỡng chịu đựng của con server thương mến của chúng ta, và việc gì đến cũng phải đến, nó đã về với tổ tiên sau khi hiện lên màn hình HTTP 502 Bad Gateway. "Nó đi thật rồi ông giáo ạ!". Vậy bây giờ chúng ta giải quyết thế nào? ## ❖ Cách giảm đau ### ▷ Cách giải quyết giảm đau nhất thời Theo như suy nghĩ thông thường, khi các anh chị em ta giải quyết bằng phương pháp thuần tuý ở bài trước sẽ chợt giật mình nhận ra với 1 câu hỏi tu từ là: ”Ủa? Mình request đi 1 lượt vậy, với 10000 Movies chia làm 20 request thì tạm ổn, nhưng nhiều hơn có khi nào mình đang tự DDoS mình không ta?”. Vâng, chính xác là nó đó mọi người ơi, vì là ví dụ với 10000 movies thôi thì cắt 500 ids ra thành 20 requests không thành vấn đề, nhưng nếu trong trường hợp của các anh chị em mình làm Micro-services, có khi rơi vào tình thế phải lấy cả triệu dòng records thì sao? Với những server không có cơ chế tự động scale resource thì rõ ràng là Server sẽ trả 502 Bad Gateway ngay. Vậy để tránh trường hợp mình chia tàu lớn ra nhiều tàu nhỏ và gửi đi cùng 1 lúc gây tắc nghẽn giao thông đường thuỷ nước bạn, thì mình chia từng đợt cho bớt kẹt tàu nước người ta chứ nhỉ? Mình sẽ thêm 1 điều kiện để phân khúc lượt request đi như đoạn code sau:
const movieIds = [
  1,
  2,
  3,
  ....
  10000,
]
 
const requestAll = [];
while (movieIds.lenght > 0) {
  const chunkMovieIds = movieIds.splice(0, 500);
  requestAll.push(fetch('https://movie.api-example.com/movies?moviesId=' + chunkMovieIds.join(',')));
  // Giả định API trả về JSON như sau
  // {results: [{moiveId: 1, movieName: 'phim 1'}, {moiveId: 2, movieName: 'phim 2'}, {moiveId: 3, movieName: 'phim 3'}, ...]}
 
  if(requestAll.lenght >= 3){
    const tmpResults = await Promise.all(requestAll);
    // map kết quả cho từng đợt request ở đây
    requestAll = [];
  }
}
Giải thích cho đoạn code trên thì mình inspect thử network xem nó gửi thế nào nhé Nhìn hình ta thấy rằng, server đã được giảm đau một tí nhờ tiếp sức theo từng tốp, nhưng hình như có tác dụng phụ thì phải, là tốc độ response của tổng các request của chúng ta sẽ bị chậm đi. Ui da! Server thì ok nhưng hình như client có gì chầm chậm thì phải? Có cách nào hay hơn không? ### ▷ Cách giải quyết giảm đau ổn định Tới đây thì chắc anh chị em develop lâu năm, dày dặn kinh nghiệm sẽ bảo rằng: “Chia ra từng đợt vậy cũng ok, nhưng vì phải chờ hết một lượt này mới bắt đầu một lượt khác. Chậm hàng người ta rồi sao?“. Thì bây giờ chúng ta sẽ tới với khái niệm Promise Pool. Nghe quen quen thì phải? Đúng vậy! Ý tưởng này được chấp cánh từ khái niệm Object Pool Design Pattern. Thay vì chia tàu chạy tiếp sức theo từng tốp, mà vì COVID-19 và đảm bảo giao thông xứ bạn nên chúng ta như chạy tiếp sức theo từng tàu, hễ một tàu về thì tàu khác xuất phát và một thời điểm có nhiều làn tàu chạy. Nghĩa là với 10000 movieIds và 3 lượt request 1 lần với 500 movieIds cho mỗi request, chúng ta sẽ sử dụng cơ chế của iterator để giải quyết như sau:
function chunkBlock(data, numberPerBlock){
    const blocks = [];
    while (data.length > 0) {
        const chunked = data.splice(0, numberPerBlock);
        blocks.push(chunked);
    }
    return blocks;
}
 
async function PromisePool(handler, data, concurency){
    const iterator = data.entries();
    const workers = new Array(concurency)
        .fill(iterator)
        .map(async (iterator) => {
            for(const [index, item] of iterator){
                await handler(item, index);
            }
        });
    await Promise.all(workers);
}
 
(async () => {
  const movieIds = [
    1,
    2,
    3,
    // ...
    10000,
  ]
   
  const movieBlockIds = chunkBlock(movieIds, 500);
 
  const resultsAll = [];
  await PromisePool( async (moveIds, index) => {
    const res = await fetch('https://movie.api-example.com/movies?moviesId=' + moveIds.join(','))
    resultsAll[index] = (await res.json()).results;
  }, movieBlockIds, 3);
 
  // map kết quả cho từng đợt request ở đây
})()
Giải thích cho đoạn code trên thì mình inspect thử network xem nó gửi thế nào nhé Các bạn có thể thấy rằng trong một lượt chỉ gửi 3 request tới API Movies thôi, và tuỳ vào thời gian response của request nào nhanh nhất sẽ được dồn thêm vào bằng request của lượt tiếp theo, và cứ thế nối đuôi nhau tiếp sức cho đến khi lấy hết movieIds. Cuối cùng chúng ta chỉ việc mapping kết quả lại cho đúng với mục đích của mình. Thế là chúng ta đã giải quyết được bài toán ở trên. Vừa giảm đau, vừa ổn định, vừa đạt được mục đích như ý.
## ❖ Tổng kết Thông qua 2 giải pháp mình phát triển lên từ những ngày đầu còn ngô nghê thì mình xin tổng kết lại 2 phương án trên nhé | **Solutions** | **Advantage** | **Disadvantage** | **Evaluation** | | ------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | Cắt parameter nhưng chia đợt request | Giảm tải cho server tốt | Tốc độ của response bị chậm | Giải quyết được bài toán, nhưng thời gian response của request bị chậm, vì phải chờ kết quả của từng đợt request, và trong mỗi đợt request thì kết quả quyết định bằng response chậm nhất.
→ Cách này khả thi cho những nghiệp vụ chạy Batch nền hoặc những nghiệp vụ không đòi hỏi thời gian response nhanh. | | Cắt parameter và cho request tiếp sức bằng Promise Pool | Tốc độ của response: Trung bình
Khả năng chịu tải của server: Trung bình | Buộc phải tính toán kỹ lưỡng để có tỉ lệ thích hợp giữa số lượt cho từng đợt request và số lượng parameter request | Giải quyết được bài toán ổn định về 2 tiêu chí, nhưng buộc developer phải tính toán và đo ra tỉ lệ hợp lý.
→ Cách này khả thi cho những nghiệp vụ không đòi hỏi cao quá về thời gian response và những server bị hạn chế resource |
## ❖ Lời kết Đến đây mình xin kết thúc 2 bài trong phần series vừa qua. Cho đến thời điểm này tuy sử dụng Promise Pool để giải quyết bài toán khi gửi request lớn đến server có tính ổn định nhất định, nhưng mình và các anh chị em đồng nghiệp vẫn còn nghiên cứu thêm để tối ưu hoá hơn. Các bạn đọc có ideas nào hay hơn và tốt hơn thì cũng đừng ngần ngại chia sẻ với bọn mình nhé. Mình xin gửi lời cám ơn đến anh Kiệt và anh Trung (chiêm) vì đã chấp cánh và hỗ trợ cho câu chuyện của mình. Bài viết cũng hơi bị dài rồi, nên mình xin chào tạm biệt các bạn và hẹn các bạn ở một bài viết khác, nếu may mắn hơn biết đâu chúng ta có thể gặp nhau tại Techbase Vietnam để cùng chia sẻ và nâng cao kiến thức tay nghề nhé. ## ❖ Reference Về những ideas và kiến thức mình xin viết nguồn tham khảo lại đây cho mọi người nhé: - [Object pool pattern](https://en.wikipedia.org/wiki/Object_pool_pattern) - [What is the best way to limit concurrency when using ES6's Promise.all()?](https://stackoverflow.com/a/51020535) - [@supercharge/promise-pool](https://www.npmjs.com/package/@supercharge/promise-pool) - https://medium.com/pipedrive-engineering/javascript-weighted-promises-pool-8153c9688f35 - [431 Request Header Fields Too Large - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431) - [502 Bad Gateway - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) - [Promise - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) - [async function - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)