API Request, to và dài quá cũng khổ!

May 26, 2021

## ❖ Giới thiệu Mình là Tâm, anh em đồng nghiệp thương mến thường gọi là Tâm Angel hay Tâm Thần Thái, mình cũng chỉ là một tay developer quèn thôi nhưng hôm nay, một ngày đẹp trời, mình mạo muội xin viết bài này để chia sẻ với tất cả các anh chị em developer mình về một series gồm hai bài kỹ thuật trong Javascript rất hay và khá hiệu quả trong mô hình micro-services, cụ thể là khi gửi request đến một API nào đó đế lấy dữ liệu, mà trong thời gian qua mình được trải nghiệm trong dự án.
#### Kiến thức nên có trước khi đọc bài viết - Khái niệm Callback trong Javascript - Khái niệm Promise và Promise.all() trong Javascript - Khái niệm Async/Await
## ❖ Mở đầu Mở đầu bằng một bài báo đưa tin ở Suez Canal. Nếu có để ý tin tức thì các bạn sẽ thấy hình ảnh chiếc tàu thuỷ khổng lồ ấy đang mang trên mình khá nhiều container nặng nề và cồng kềnh bị kẹt lại giữa lòng một con kênh nhỏ bé trên khắp các trang mạng xã hội. Hình ảnh này làm chúng ta liên tưởng ngay đến một request mang theo vài chục ngàn values của request parameter hoặc là request body một cách vô tình mà đến khi bị lỗi **431 Request Header Fields Too Large** hoặc **414 URI Too Long** thì mới giật mình tìm hướng giải quyết. #### Bài toán thực tế Đến đây chắc mọi người cũng hình dung được vấn đề mình muốn nói tới là gì rồi phải không? Mình sử dụng từ "vô tình", vì trong quá trình develop có thể vì một hoặc nhiều lý do nào đó mà developer chúng ta cứ hay cho một list các ID vào parameter để gọi đến API và lấy kết quả, ví dụ:
// Movies ID có thể được lấy từ nguồn khác như function hoặc API khác
// Mình ví dụ là một array để mọi người dể hình dung
const movieIds = [
  1,
  2,
  3,
  ....
  10000,
]
 
const userAction = async () => {
  const response = await fetch('https://movie.api-example.com/movies?moviesId=' + movieIds.join(',') );
  const myJson = await response.json(); //extract JSON from the http response
}
Việc làm này khiến chúng ta gặp phải những lỗi từ server về request quá lớn như HTTP **414** hoặc **431**, vì request mang theo giá trị truyền lên khá to, mà dù cho là may mắn mà config của server cho phép request đến có lượng giá trị "to" và "dài" hơn mặc định thì kết quả trả ra sẽ bị chậm. Vậy câu hỏi đặt ra là: "Làm sao để có thể lấy dữ liệu movie từ API Movie theo list các Movies ID hiệu quả hơn?" ## ❖ Cách giải quyết thuần tuý và đơn giản #### #1: Nén request paramters Tới đây một số anh chị em sẽ nghĩ đến giải pháp: "Tàu to thì mình nén lại cho nhỏ là lọt chứ gì", nói tới nén thì chắc mấy anh chị em có thể làm như sau:
const movieIds = [
  1,
  2,
  3,
  ....
  10000,
]
 
const userAction = async () => {
  const response = await fetch('https://movie.api-example.com/movies?moviesId=' + compress(movieIds.join(','));
  //extract JSON from the http response
  const myJson = await response.json(); 
}
Giải pháp này cũng là một ý hay, thay vì gửi lên một string có độ dài khá cao thì mình nén request parameter lại cho nhỏ rồi ở server mình giải nén ra hoặc là chúng ta sử dụng một số thư viện để nén payload gửi đi như: - https://www.npmjs.com/package/fastify-compress - https://www.npmjs.com/package/shrink-string Nhưng đời không đẹp như là mơ, giá như cục xuất nhập cảnh nước người ta tân tiến và hiện đại có thể giải nén được con tàu to và dài của mình thì đỡ quá, đằng này nước họ không có "công nghệ cao" để làm chuyện đó thì mình phải làm sao? #### #2: Chia nhiều request À, nói tới đây một số các anh chị em lành nghề sẽ nói là: "Tàu to quá mà vào kênh nhỏ kẹt là đúng rồi, sao không chia ra thành nhiều tàu nhỏ rồi mang hàng đi cho dễ". Cũng giống như việc chúng ta nên cắt mảng array của movieIds ra thành nhiều mảng nhỏ cho nhiều đợt request và dùng await Promise.all() như sau:
const movieIds = [
  1,
  2,
  3,
  ....
  10000,
]
 
const requestAll = []
while (movieIds.length > 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'}, ...]}
}
 
const promiseAllResult = await Promise.all(requestAll);
// Map kết quả ở đây thôi
Giải thích cho đoạn code trên thì mình inspect thử network xem nó gửi thế nào nhé.
Qua hình ảnh, ta thấy được là mình cắt request với 50 moviesId thì các request được gửi cùng một lúc đến server. Bằng cách này, một số server mạnh mẽ hỗ trợ phân luồng và có load balancing sẽ dễ dàng thực hiện, nhưng với những server API bình thường thì chẳng khác nào mình đang DDoS người ta, dẫn tới việc server bị stress cao, đau đớn và có thể gây "ngủm" server. Vì vậy làm sao để giảm stress hiệu quả cho server mà vẫn giải quyết được bài toán đặt ra, mình xin hẹn các bạn ở bài sau, [**"Promise Pool: Giảm 'down' hiệu quả cho server"**](https://www.techbasevn.com/blog-technical/promise-pool---gim-down-hiu-qu-cho-server.html) . ## ❖ Tổng kết Mình xin được tạm ngưng tại điểm này để anh chị em ta có thể ngẫm nghĩ đôi chút về bài toán và giải pháp cho nó, đồng thời mình cũng xin được tổng kết so sánh 2 phương án trên mà mình đề cập: | **Solution** | **Advantage** | **Disadvantage** | **Evaluation** | | :----------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | | Nén parameters | Bảo mật thông tin qua network trafficRequest length luôn nằm trong tầm kiểm soát tuỳ thuộc vào độ dài của cơ chế nén được chọn | Tốc độ response chậm hơn, phụ thuộc vào cơ chế giải nén ở server và lượng thông tin cần xử lý một lúc khá nhiều gây stress cho server | Giải quyết được bài toán nhưng không đảm bảo về thời gian phản hồi của server. Tuỳ vào các yêu cầu khác nhau mà server có độ chịu stress nhất định
→ Phương án này có thể sử dụng khi developer tự làm chủ được cả server-side và client-side. Biết được mức độ cần xử lý của cả 2 để đảm bảo tốt cho request và response. | | Chia nhiều request | Request length nằm trong phạm vi kiểm soát tuỳ thuộc vào biến chunk parameter.
Tốc độ response nhanh | Gây stress cho server cao | Giải quyết được bài toán, nhanh nhưng không đảm bảo về hệ thống, vì tăng khả năng chịu tải của server lên cao.
→ Cách này khả thi cho những server có khả năng auto scale resource và có cơ chế lọc request để tránh nhầm với DDoS | ## ❖ Lời kết Những gì mình chia sẻ ở trên là từ vấn đề thực tế của dự án mình đang hoạt động, từ những bài học ngờ nghệch, ngu ngốc cho đến những ý tưởng rất gì và này nọ qua thời gian và cũng nhờ ý tưởng và sự giúp đỡ của các anh đồng nghiệp là anh Kiệt và anh Trung (Chiêm) đã hỗ trợ mình rất nhiều để giải quyết được vấn đề trên. Hai giải pháp ở trên là minh chứng cho những ngày tháng ngây ngô và thao thức tìm ra được giải pháp tốt hơn trong những ngày đầu mình gặp vấn đề, bài viết sau sẽ là phần giải pháp cho những ngày trưởng thành và hấp dẫn hơn. 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 ở bài viết sau. Mình là Tâm, follow câu chuyện của mình để cùng trau dồi và chia sẻ kiến thức 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é: - https://www.npmjs.com/package/fastify-compress - https://www.npmjs.com/package/shrink-string