Bước tới nội dung

Thành viên:Plantaest/Blog/Thuật toán xếp lịch lệnh gọi Lambda

Bách khoa toàn thư mở Wikipedia

Bối cảnh[sửa | sửa mã nguồn]

Vì sao chọn AWS Lambda?[sửa | sửa mã nguồn]

Trong dự án Feverfew, API createCheck có 3 bước chính:

  1. Extract: Trích xuất các liên kết từ văn bản Parsoid
  2. Request: Gửi yêu cầu GET đến các liên kết và thu thập dữ liệu từ việc này
  3. Classify: Phân loại các liên kết dùng mô hình máy học

Bước request là bước khó khăn nhất do mất thời gian gọi đến các trang web, và điều quan trọng là nó để lộ IP của máy chủ đang chạy Feverfew (ở đây là Toolforge).

Khi lộ IP, ví dụ như IP của máy chủ Toolforge, thì các website có thể chặn IP này do đã gọi quá nhiều lần đến họ. Như vậy, sẽ ảnh hưởng đến các dịch vụ của những người khác đang triển khai trên Toolforge, do Toolforge là một máy chủ dùng chung cho rất nhiều người. Vì vậy, bước request đã được triển khai thông qua AWS Lambda, do hạ tầng của AWS có lượng IP rất lớn (IP pool), như vậy sẽ giúp tránh tình trạng IP của Toolforge có thể bị chặn.

AWS API Gateway và AWS Lambda[sửa | sửa mã nguồn]

Từ ý tưởng của một repo Python là requests-ip-rotator, ý định ban đầu là sử dụng dịch vụ AWS API Gateway để thực hiện việc gọi đến các website.

Tuy nhiên, AWS API Gateway có nhược điểm là chỉ có thể tạo REST API cho một hostname cụ thể. Trong khi đó, use case của Feverfew là phải gọi đến rất nhiều liên kết cùng một lúc, từ đó dẫn đến cũng có nhiều hostname trong mỗi lần gọi.

Vì vậy, AWS Lambda đã được lựa chọn để thực hiện bước request. AWS Lambda có một ưu điểm quan trọng là có thể viết mã tùy chỉnh, từ đó giúp tổng hợp dữ liệu, và gửi trả dữ liệu với định dạng thích hợp để sử dụng.

Nhược điểm của AWS Lambda[sửa | sửa mã nguồn]

Không chắc chắn là nhược điểm hay không? Nhưng trong quá trình chạy thử ứng dụng Lambda, thì có vẻ mức tiêu thụ bộ nhớ ngày càng tăng dần, đến một số lần nhất định thì sẽ gây ra tình trạng thiếu bộ nhớ, và trả về lỗi Runtime.ExitError.

{
  "errorType": "Runtime.ExitError",
  "errorMessage": "RequestId: ecef32df-a02b-4723-bdeb-458f6208eed0 Error: Runtime exited with error: signal: killed"
}

Ứng dụng Lambda được sử dụng cho dự án Feverfew là một ứng dụng Quarkus Native, viết bằng Java, và build bằng GraalVM CE, với kiến trúc arm64 để phù hợp với runtime Amazon Linux 2023. Theo như AWS công bố thì arm64 có chi phí thấp hơn so với x86. Do là một native image, nên ứng dụng Quarkus Native có ưu điểm tuyệt đối là thời gian cold start rất nhanh (0.25s) so với phiên bản JVM của chính Quarkus (10s).

Từ thực nghiệm, nếu như gọi liên tục một batch với 50 liên kết (batch 50) đến một Lambda có mức bộ nhớ 512 MB, thì sau khoảng 24–26 lần gọi sẽ gây ra lỗi Runtime.ExitError, và Lambda được khởi động lại (cold start).

Thực nghiệm cũng đã kiểm tra một số batch như batch 20, batch 25, batch 30 với một số mức bộ nhớ như 128, 192, 256 MB. Kết quả cho thấy batch 50 cho 512 MB là tương đối ổn vì một số lý do:

  1. Tối đa 50 liên kết cho một lần request Lambda là tương đối hợp lý. Một là có thể đáp ứng phần lớn nhu cầu kiểm tra thông thường chỉ trong một request duy nhất, vì các bài viết thường có số liên kết cần kiểm tra dưới 50. Hai là đủ lớn để giữ số lần request Lambda đồng thời ở mức vừa phải khi số liên kết cần kiểm tra vượt trội so với bình thường. Ví dụ, nếu cần kiểm tra 500 liên kết (đối với những bài viết rất dài), thì batch 50 chỉ tạo ra 10 request, trong khi batch 20 cần đến 25 request. Như vậy, sẽ giúp giảm số lượng request đồng thời đến Lambda. Hiện tại, AWS Lambda chỉ cho phép hạn mức 1000 đối với hạng mục “Concurrent executions”, sau khi xin tăng từ mức 10.
  2. Bộ nhớ 512 MB có hiệu suất tốt đáng kể so với mức tối thiểu 128 MB, vì khi tăng bộ nhớ, thì cũng đồng nghĩa với việc Lambda phân bổ thêm sức mạnh tính toán của CPU. Như vậy, việc gọi đến các website không bị quá chậm. Thực tế kiểm tra batch 20 cho 128 MB cho thấy nó có thể thực hiện được (ở một vài lần gọi đầu cho đến khi bị lỗi Runtime.ExitError), nhưng thời gian gửi lệnh GET đến các website thường khá chậm, không đạt được kết quả tương đương môi trường local như batch 50 cho 512 MB. Việc tạo lệnh GET là tiêu tốn tài nguyên đáng kể, vì mỗi lệnh GET được đặt trong một thread để có thể xử lý đồng thời, dù đã có sử dụng Virtual Thread, một tính năng rất mới của Java 21.
  3. Giá cho bộ nhớ 512 MB là đủ thấp, dù không thấp bằng các mức bộ nhớ nhỏ hơn, nhưng hiệu suất tốt hơn sẽ cho chi phí tổng thể là tương đương. Xem thêm: Lower Your AWS Lambda Bill by Increasing Memory Size.
  4. Số lần gọi liên tục thành công cho đến khi gặp lỗi Runtime.ExitError là tương đối đủ, khoảng 24–26 lần. Thử nghiệm batch 20 cho 128 MB thì gặp lỗi sau 6–10 lần gọi liên tục, và hiệu suất cũng kém khi các lệnh GET thường tải khá lâu.

Về khả năng memory leak[sửa | sửa mã nguồn]

Vì sao có tình trạng bộ nhớ tiêu thụ trong Lambda tăng dần sau mỗi lần gọi (theo chỉ số “Max memory used”)? Trên mạng có nêu nhiều nguyên nhân, nhưng nhìn chung là không rõ ràng trong trường hợp của Quarkus Native.

Dù đã thực nghiệm việc gọi nhiều loại batch đến nhiều mức bộ nhớ như 128, 192, 256, 512 MB; thì tình trạng này vẫn xảy ra, dù là JVM hay GraalVM Native Image. Có thể những mức bộ nhớ cao như 1024 MB sẽ cho kết quả khác biệt, nhưng tạm thời sẽ chưa cho tăng đến những mức bộ nhớ lớn hơn 512 MB vì chi phí sẽ cao hơn khá nhiều.

Việc bộ nhớ bị chiếm dụng và không được giải phóng trong Lambda là có thể do sau mỗi lần gọi, các dữ liệu được lưu trong bộ nhớ bằng cách nào đó không được garbage collector thu gom, và rác cứ tăng dần dần cho đến khi còn đủ bộ nhớ để chạy chương trình. Như vậy, đây có thể là vấn đề của JVM/GraalVM. Dù thực tế đã dùng thử System.gc(), nhưng không thể thay đổi hiện trạng.

Một bài đăng trên Stack Overflow cũng đã nêu ra vấn đề này: Troubleshooting AWS Lambda's Climbing Memory Usage Between Invocations.

Vấn đề memory leak của mã nguồn là có thể xảy ra, và khi đó nó sẽ không là vấn đề của JVM/GraalVM. Tuy nhiên, để kiểm tra xem có xảy ra memory leak hay không thì giờ chưa có thời gian, do cần tìm hiểu cách profiling, logging, visualing.

Giải pháp tạm thời[sửa | sửa mã nguồn]

Từ những dữ kiện trên, với thời gian có hạn, thì một giải pháp tạm thời để thực hiện bước request là xây dựng một thuật toán xếp lịch lệnh gọi Lambda.

Một số ý tưởng cho thuật toán:

  • Giả sử ở một thời điểm nào đó, Feverfew nhận được 5 lượt gọi đồng thời đến API createCheck cho 10, 50, 100, 150, 300 liên kết. Giả định rằng không có liên kết nào thuộc diện được bỏ qua, tức cần chuyển hết những liên kết này cho Lambda; và mỗi lượt gọi API createCheck sẽ nằm trên mỗi thread khác nhau (chưa chắc chắn do chưa tìm hiểu cơ chế xử lý request của Quarkus).
  • Giới hạn:
    • Kích thước batch là 50, tức là tối đa 50 liên kết trên 1 lần gọi Lambda.
    • Hiện tại có 4 Lambda Function, cấu hình như nhau (memory 512 MB, timeout 30s), được triển khai ở region us-west-2, dùng chung số lần gọi đồng thời là 1000. Việc dùng nhiều Lambda sẽ giúp tạo ra các lệnh GET có IP khác nhau trên mỗi Lambda, và cũng giúp giãn thời gian thực hiện force cold start.
    • Với mỗi Lambda, sau khi gọi liên tục 18 lần, cần được ép buộc cold start để giải phóng bộ nhớ, tránh tình trạng gặp lỗi Runtime.ExitError (lỗi này thường khá mất thời gian để trả về, chứ không phải trả về được ngay). Con số 18 được căn cứ dựa trên số lần gọi liên tục thành công là 24–26 của batch 50 cho bộ nhớ 512 MB. Thời gian cold start của Quarkus Native là không đáng kể nên khá thoải mái để triển khai việc này.
      • Như vậy, mỗi Lambda sẽ được gắn với một bộ đếm (counter) để tính số lần gọi liên tục kể từ lúc tạo một request Lambda thành công có cold start (lần 1). Cứ đạt 18 lần, thì tiến hành force cold start, và đặt lại bộ đếm (0).
      • Tạm thời chưa quan tâm đến vấn đề request Lambda gặp lỗi?

Cơ chế[sửa | sửa mã nguồn]

Sau nhiều ngày suy nghĩ và thử nghiệm liên tục, thì cơ chế của thuật toán xếp lịch lệnh gọi Lambda được triển khai như sau.