Khởi động nhanh hơn với AWS Lambda SnapStart

Bài viết này được viết bởi Tarun Rai Madan, Sr. Product Manager của AWS Lambda, Mike Danilov, Sr. Principal Engineer của AWS Lambda, và Colm MacCárthaigh, VP/Distinguished Engineer của EC2.

AWS Lambda SnapStart là một tối ưu hiệu suất mới được phát triển bởi AWS có thể cải thiện đáng kể thời gian khởi động cho các ứng dụng. Được công bố tại AWS re:Invent 2022, khả năng đầu tiên có tính năng SnapStart là Lambda SnapStart cho Java. Tính năng này cung cấp tới 10 lần tốc độ khởi động nhanh hơn cho các ứng dụng Java nhạy cảm với độ trễ mà không tốn thêm chi phí, và với sự thay đổi mã tối thiểu hoặc không thay đổi gì cả.

Tổng quan

Khi ứng dụng khởi động, dù đó là một ứng dụng trên điện thoại của bạn hay một hàm Lambda không máy chủ, chúng sẽ trải qua quá trình khởi tạo. Quá trình khởi tạo có thể khác nhau tùy thuộc vào ứng dụng và ngôn ngữ lập trình, nhưng ngay cả các ứng dụng nhỏ nhất được viết bằng các ngôn ngữ lập trình hiệu quả nhất cũng cần một loại khởi tạo trước khi chúng có thể làm bất cứ điều gì hữu ích. Đối với một hàm Lambda, giai đoạn khởi tạo liên quan đến tải mã của chức năng, khởi động thời gian chạy và các phụ thuộc bên ngoài và chạy mã khởi tạo của chức năng. Thông thường, đối với một hàm Lambda, khởi tạo này xảy ra mỗi khi ứng dụng của bạn mở rộng để tạo một môi trường thực thi mới.

Với SnapStart, việc khởi tạo của chức năng được thực hiện trước khi bạn xuất bản một phiên bản chức năng. Lambda lấy một bản chụp microVM Firecracker của bộ nhớ và trạng thái đĩa của môi trường thực thi đã khởi tạo, mã hóa bản chụp và lưu trữ nó để truy cập với độ trễ thấp. Khi ứng dụng của bạn khởi động và mở rộng để xử lý lưu lượng, Lambda khôi phục các môi trường thực thi mới từ bản chụp được lưu trữ thay vì khởi tạo chúng từ đầu, cải thiện hiệu suất khởi động.

Sơ đồ sau so sánh vòng đời yêu cầu khởi động lạnh cho một chức năng không có SnapStart và một chức năng SnapStart. Thời gian mà nó mất để khởi tạo chức năng, đó là nguyên nhân chính gây ra độ trễ khởi động cao, được thay thế bằng một giai đoạn tiếp tục nhanh hơn với SnapStart.

Vòng đời yêu cầu cho một hàm non-SnapStart và một hàm SnapStart

Việc front-load giai đoạn khởi tạo có thể cải thiện đáng kể hiệu suất khởi động cho các hàm Lambda nhạy cảm với độ trễ, chẳng hạn như các microservices đồng bộ nhạy cảm với thời gian khởi động. Vì Java là một ngôn ngữ động với một runtime và garbage collector riêng, các hàm Lambda viết bằng Java có thể là những hàm khởi động chậm nhất. Đối với các ứng dụng yêu cầu tần suất scaling, sự trì hoãn được giới thiệu bởi giai đoạn khởi tạo, thường được gọi là khởi động lạnh, có thể dẫn đến trải nghiệm không tối ưu cho người dùng cuối. Các ứng dụng này có thể khởi động nhanh hơn với SnapStart.

Bằng cách cải thiện thời gian khởi động cho các hàm, SnapStart giảm đáng kể số lượng môi trường thực thi (và do đó là số lần khởi động lạnh) cần được tạo ra khi hàm của bạn scaling. Ví dụ, giả sử hàm của bạn phục vụ 100 yêu cầu trong một khoảng thời gian ba giây và có thời gian khởi động lạnh là sáu giây nếu không sử dụng SnapStart. Điều này yêu cầu Lambda tạo 100 môi trường thực thi đồng thời, dẫn đến 100 lần khởi động lạnh. Trong kịch bản này, giả sử SnapStart giảm khởi động lạnh xuống một giây. Điều này chỉ yêu cầu Lambda tạo chỉ 33 môi trường thực thi đồng thời có thể phục vụ 33 yêu cầu trong giây thứ hai. Cùng 33 môi trường thực thi được sử dụng để phục vụ các yêu cầu còn lại trong giây thứ hai và thứ ba.

Công việc của AWS trong Firecracker làm cho việc sử dụng SnapStart trở nên đơn giản. Bởi vì SnapStart sử dụng bản snapshot của micro Virtual Machine (microVM) để kiểm tra và khôi phục ứng dụng đầy đủ, phương pháp này có tính linh hoạt và phổ quát. Nó có thể được sử dụng để tăng tốc khởi động nhiều loại ứng dụng. Trong khi microVM đã được sử dụng trong thời gian dài để cô lập mạnh mẽ giữa các ứng dụng và môi trường, khả năng front-load initialization với SnapStart có nghĩa là microVM cũng có thể tăng cường tiết kiệm hiệu suất ở quy mô.

SnapStart và tính độc nhất

Lambda SnapStart giúp tăng tốc ứng dụng bằng cách sử dụng lại một bản snapshot đã được khởi tạo để khôi phục nhiều môi trường thực thi. Do đó, nội dung độc nhất được bao gồm trong snapshot trong quá trình khởi tạo được sử dụng lại trên nhiều môi trường thực thi và do đó có thể không còn độc nhất nữa. Một loại ứng dụng mà tính độc nhất của trạng thái là một yếu tố quan trọng là phần mềm mật mã, giả sử rằng các số ngẫu nhiên là thực sự ngẫu nhiên (ngẫu nhiên và không thể dự đoán). Nếu nội dung như một giá trị khởi tạo ngẫu nhiên được lưu trong snapshot, nó sẽ được sử dụng lại khi nhiều môi trường thực thi khác nhau khôi phục và có thể tạo ra các chuỗi ngẫu nhiên dự đoán được.

Để duy trì tính độc nhất, trước khi sử dụng SnapStart, bạn phải xác minh rằng bất kỳ nội dung độc nhất nào được tạo ra trước đó trong quá trình khởi tạo giờ đây được tạo ra sau quá trình khởi tạo đó. Điều này bao gồm các ID độc nhất, các bí mật độc nhất và entropy được sử dụng để tạo ra ngẫu nhiên giả.

Nhiều môi trường thực thi được khôi phục từ một snapshot được chia sẻ

Tuy nhiên, chúng tôi đã triển khai một số điều để giúp khách hàng duy trì sự độc nhất.

Đầu tiên, đó không phải là một thực hành thông thường hoặc tốt nhất để các ứng dụng tạo ra các mục đích độc nhất này trực tiếp. Tuy nhiên, nó đáng xác nhận rằng ứng dụng của bạn xử lý tính độc nhất đúng cách. Điều này thường liên quan đến kiểm tra bất kỳ ID, key, timestamp hoặc entropy “tự làm” nào trong các phương thức khởi tạo cho hàm của bạn.

Lambda cung cấp một công cụ quét SnapStart để kiểm tra các danh mục mã nhất định mà giả định tính độc nhất, để khách hàng có thể thực hiện các thay đổi nếu cần thiết. Công cụ quét SnapStart là một plugin SpotBugs mã nguồn mở thực thi phân tích tĩnh theo một tập hợp quy tắc và báo cáo “lỗi SnapStart tiềm năng”. Chúng tôi cam kết tương tác với cộng đồng để mở rộng tập quy tắc mà công cụ quét kiểm tra mã.

Ví dụ, hàm Lambda sau đây tạo một luồng nhật ký duy nhất cho mỗi môi trường thực thi trong quá trình khởi tạo. Giá trị độc nhất này được sử dụng lại trên các môi trường thực thi khi chúng tái sử dụng một bản snapshot.

Java

public class LambdaUsingUUID {

    private AWSLogsClient logs;

    private final UUID sandboxId;

    public LambdaUsingUUID() {

       sandboxId = UUID.randomUUID(); // <– unique content created

       logs = new AWSLogsClient();

    }

    @Override

    public String handleRequest(Map<String,String> event, Context context) {

       CreateLogStreamRequest request = new CreateLogStreamRequest(

         “myLogGroup”, sandboxId + “.log9.txt”);

         logs.createLogStream(request);     

         return “Hello world!”;

    }

Khi bạn chạy công cụ quét mã trên mã nguồn trước đó, thông báo sau đây sẽ giúp xác định một triển khai tiềm năng giả định sự duy nhất. Một cách để giải quyết các trường hợp như vậy là di chuyển việc tạo ID độc nhất vào trong phương thức xử lý của chức năng của bạn.

Text

H C SNAP_START: Detected a potential SnapStart bug in Lambda function initialization code. At LambdaUsingUUID.java: [line 7]

Một phương pháp tốt được sử dụng bởi nhiều ứng dụng là dựa vào thư viện hệ thống và kernel để đảm bảo tính duy nhất. Điều này đã được xử lý từ lâu cho các trường hợp khác nhau, như khi chia tách hoặc nhân bản các tiến trình. AWS đã cùng các nhà duy trì kernel và các nhà phát triển mã nguồn mở làm việc để các cơ chế bảo vệ hiện có sử dụng vmgenid, một chuẩn mở được SnapStart hỗ trợ. Vmgenid là một thiết bị được giả lập, hiển thị một giá trị định danh số nguyên ngẫu nhiên 128 bit cho kernel và độc đáo theo thống kê trên tất cả các microVM được tiếp tục khôi phục.

Các phiên bản  Amazon Linux 2, OpenSSL (1.0.2) và java.security.SecureRandom được bao gồm trong Lambda sẽ tự động khởi tạo lại độ ngẫu nhiên và các bí mật của chúng sau khi SnapStart. Phần mềm luôn lấy các số ngẫu nhiên từ hệ điều hành (ví dụ: từ /dev/random hoặc /dev/urandom) không cần cập nhật nào để duy trì tính ngẫu nhiên. Bởi vì Lambda luôn khởi tạo lại /dev/random và /dev/urandom khi khôi phục một snapshot, các số ngẫu nhiên không bị lặp lại ngay cả khi nhiều môi trường thực thi tiếp tục từ cùng một snapshot.

Request ID của Lambda đã là duy nhất cho mỗi lần gọi và có sẵn thông qua phương thức getAwsRequestId() của đối tượng yêu cầu Lambda. Hầu hết các hàm Lambda không cần chỉnh sửa để chạy với SnapStart được kích hoạt. Nó thường được khuyến nghị rằng cho SnapStart, bạn không nên bao gồm trạng thái duy nhất trong mã khởi tạo của hàm và sử dụng bộ sinh số ngẫu nhiên bảo mật mật mã (CSPRNG) khi cần thiết.

Thứ hai, nếu bạn muốn tạo dữ liệu duy nhất trực tiếp trong giai đoạn khởi tạo hàm Lambda, Lambda hỗ trợ hai điểm neo runtime mới. Điểm neo runtime có sẵn như một phần của dự án Coordinated Restore at Checkpoint (CRaC) mã nguồn mở. Bạn có thể sử dụng điểm neo beforeCheckpoint để chạy mã ngay trước khi một snapshot được tạo và sử dụng điểm neo afterRestore để chạy mã ngay sau khi một snapshot được khôi phục. Điều này giúp bạn xóa bất kỳ nội dung duy nhất trước khi snapshot được tạo và khôi phục bất kỳ nội dung duy nhất nào sau khi snapshot được khôi phục. Để biết cách sử dụng CRaC với một ứng dụng tham chiếu, hãy xem kho CRaC trên GitHub.

Tính thời gian khởi động và thời gian thanh toán cho Lambda

Khi dịch vụ Lambda tạo môi trường thực thi mới khi tăng tỷ lệ với SnapStart được kích hoạt, chức năng được khôi phục từ snapshot thay vì được khởi tạo từ đầu. Để giúp tính toán thời gian khởi động và thời gian thanh toán với SnapStart, AWS đã cập nhật các số liệu xuất hiện trên CloudWatch để bao gồm thời gian cần thiết để khôi phục một snapshot (Thời lượng khôi phục), trong khi thời gian khởi tạo chức năng (Thời lượng khởi tạo) là một phần của báo cáo riêng. Thời gian khởi động chức năng Lambda (hoặc thời gian khởi động lạnh) với SnapStart là tổng của Thời lượng khôi phục và thời lượng chạy mã của bạn trong trình xử lý chức năng (Thời lượng).

Restore Duration bao gồm thời gian mà Lambda cần để khôi phục lại một bản snapshot, tải runtime (JVM) và chạy bất kỳ hook nào được khai báo trong afterRestore. Bạn sẽ không bị tính phí cho thời gian khôi phục snapshot. Tuy nhiên, thời gian mà runtime (JVM) tải và thực thi bất kỳ hook afterRestore nào cũng như đánh dấu sẵn sàng phục vụ các lời gọi sẽ được tính vào thời gian bị tính phí của hàm Lambda. AWS đang làm việc để phân tách thành phần này trong các metric được báo cáo. Bạn cũng sẽ bị tính phí cho thời gian khởi tạo một lần xảy ra khi triển khai hoặc cập nhật hàm của bạn, cùng với bất kỳ hook beforeCheckpoint nào được khai báo bên ngoài handler.

Để minh họa cấu trúc tính phí, tôi sử dụng ví dụ được trình bày trong bài đăng trên blog AWS News giới thiệu AWS Lambda SnapStart. Trong ví dụ này, thời gian khởi động tổng thể giảm từ 6.334ms (6.308,87ms + 25,2ms) xuống còn 181,9ms (142,59ms + 39,31ms). Với SnapStart được kích hoạt, Billed Duration là 125ms. Đây là tổng của Duration của hàm (39,31ms) và thời gian cần để tải runtime (không có hook khôi phục nào được triển khai), là 85,69ms (125ms – 39,31ms).

Kết luận

Trong bài đăng này, chúng tôi mô tả cách SnapStart tối ưu hóa hiệu suất khởi động và đưa ra các yếu tố cần xem xét về tính duy nhất. Chúng tôi cũng giới thiệu các giao diện mới mà AWS Lambda cung cấp (qua công cụ quét và hook runtime) để giữ tính duy nhất cho các chức năng SnapStart của khách hàng. Bên cạnh đó, chúng tôi giúp bạn hiểu cách tính toán thời gian khởi động và thời gian được tính phí với SnapStart, sử dụng một ví dụ.

Nhiều dự án mã nguồn mở đã làm cho SnapStart trở thành một điều có thể, bao gồm Firecracker, Linux, CraC, OpenSSL và nhiều hơn nữa. AWS rất biết ơn các nhà bảo trì và nhà phát triển đã làm điều này. Với công việc này, chúng tôi rất hào hứng khi ra mắt Lambda SnapStart cho Java như là điều mà chúng tôi hy vọng là cái đầu tiên trong nhiều khả năng khác để hưởng lợi từ tiết kiệm hiệu suất và tăng cường bảo mật mà các microVMs SnapStart cung cấp.

Để biết thêm tài nguyên học tập về serverless, hãy truy cập Serverless Land.