Giới thiệu
Trong phần 1 của loạt bài viết về cách phát triển, chạy và tối ưu hóa ứng dụng web Micronaut trên AWS Lambda, chúng ta đã trình bày cách viết một ứng dụng mẫu sử dụng framework Micronaut, AWS Lambda, Amazon API Gateway và Amazon DynamoDB. Chúng ta cũng đã thực hiện những đo lường đầu tiên về hiệu suất Lambda (thời gian cold start và warm start) và nhận thấy thời gian cold start khá lớn.
Trong phần 2 của loạt bài, chúng ta đã giới thiệu Lambda SnapStart và đo lường cách mà việc kích hoạt nó giúp giảm thời gian cold start của Lambda hơn 50% tùy thuộc vào mức phần trăm. Chúng ta cũng đã thấy rõ ảnh hưởng của bộ nhớ đệm cấp bậc SnapStart Snapshot trong tất cả các phép đo cho đến nay.
Trong phần này của loạt bài viết, chúng ta sẽ trình bày cách áp dụng kỹ thuật priming với Lambda SnapStart, bắt đầu với việc priming yêu cầu DynamoDB nhằm cải thiện hơn nữa hiệu suất của các hàm Lambda.
Ứng dụng mẫu với AWS Lambda SnapStart được kích hoạt và sử dụng yêu cầu DynamoDB priming
Chúng ta sẽ tận dụng lại ứng dụng mẫu đã được giới thiệu trong phần 1 của loạt bài.
Kích hoạt Lambda SnapStart là điều kiện tiên quyết cho phương pháp này.
yaml
Globals:
Function:
CodeUri: target/aws-lambda-micronaut-4.9-1.0.0-SNAPSHOT.jar
Runtime: java21
SnapStart:
ApplyOn: PublishedVersions
Điều này có thể được thực hiện trong phần Globals của các hàm Lambda, trong trường hợp này SnapStart sẽ được áp dụng cho tất cả các hàm Lambda được định nghĩa trong mẫu AWS SAM, hoặc bạn có thể thêm 2 dòng
yaml
SnapStart:
ApplyOn: PublishedVersions
để kích hoạt SnapStart chỉ cho hàm Lambda riêng lẻ.
Bạn có thể tìm hiểu thêm về các khái niệm đằng sau Lambda SnapStart trong phần 2.
SnapStart và các runtime hooks cung cấp cho bạn những khả năng mới để tạo ra các hàm Lambda với độ trễ khởi động thấp. Với hook pre-snapshot, chúng ta có thể chuẩn bị ứng dụng Java của mình càng nhiều càng tốt cho cuộc gọi đầu tiên. Chúng ta tải và khởi tạo càng nhiều càng tốt những gì mà hàm Lambda của chúng ta cần trước khi snapshot được tạo ra. Kỹ thuật này được gọi là priming.
Trong phương pháp này, tôi sẽ giới thiệu cho bạn về việc priming yêu cầu DynamoDB, được thực hiện trong lớp AmazonDynamoDBPrimingResource
.
java
@Singleton
public class AmazonDynamoDBRequestPrimingResource implements OrderedResource
{
@Inject
private ProductDao productDao;
@Override
public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
this.productDao.getProduct("0");
}
@Override
public void afterRestore(Context<? extends Resource> context) throws Exception {
}
}
Chúng ta sử dụng Lambda SnapStart CRaC runtime hooks ở đây. Để làm điều này, chúng ta cần khai báo các phụ thuộc Micronaut CRaC sau trong pom.xml
:
xml
<dependency>
<groupId>io.micronaut.crac</groupId>
<artifactId>micronaut-crac</artifactId>
</dependency>
Lớp AmazonDynamoDBPrimingResource
được đánh dấu bằng annotation jakarta.inject.Singleton và triển khai io.micronaut.crac.OrderedResource. Việc priming thực sự diễn ra trong phương thức nơi chúng ta tìm kiếm sản phẩm với ID bằng 0 trong bảng DynamoDB. Phương thức beforeCheckpoint là một CRaC runtime hook được gọi trước khi tạo snapshot microVM. Chúng ta không quan tâm đến kết quả của cuộc gọi productDao.getProduct("0")
, nhưng với cuộc gọi này, tất cả các lớp cần thiết được tải và khởi tạo và việc khởi tạo tốn kém một lần cho HTTP Client (mặc định là Apache HTTP Client) và Jackson Marshallers (để chuyển đổi các đối tượng Java thành JSON và ngược lại) được thực hiện. Bởi vì điều này xảy ra trong giai đoạn triển khai của hàm Lambda khi SnapStart được kích hoạt và trước khi snapshot được tạo, snapshot sẽ chứa tất cả những điều này. Sau giai đoạn phục hồi snapshot nhanh trong quá trình gọi Lambda, chúng ta sẽ đạt được rất nhiều về hiệu suất trong trường hợp cold start bằng cách priming theo cách này (xem các phép đo bên dưới). Chúng ta do đó sẽ tiến hành priming yêu cầu DynamoDB.
Để đảm bảo rằng chỉ việc priming này có hiệu lực, hãy tạm thời xóa lớp AmazonAPIGatewayProxyRequestPrimingResource
, nếu không sẽ có thêm priming diễn ra mà chúng ta sẽ đề cập trong phần tiếp theo.
Đo lường thời gian cold và warm start của ứng dụng với Lambda SnapStart và yêu cầu DynamoDB priming
Trong phần tiếp theo, chúng ta sẽ đo lường hiệu suất của hàm Lambda GetProductByIdFunction
, mà chúng ta sẽ kích hoạt bằng cách sử dụng lệnh curl -H "X-API-Key: a6ZbcDefQW12BN56WEM49" https://{$API_GATEWAY_URL}/prod/products/1.
Kết quả của thí nghiệm dựa trên việc tái tạo hơn 100 cold starts và khoảng 100.000 warm starts với hàm Lambda GetProductByIdFunction
(chúng ta yêu cầu sản phẩm đã tồn tại với ID=1) trong khoảng thời gian khoảng 1 giờ. Chúng ta cấp cho hàm Lambda 1024 MB bộ nhớ, đây là một sự cân bằng tốt giữa hiệu suất và chi phí. Chúng ta cũng sử dụng (mặc định) kiến trúc x86 Lambda. Để thử nghiệm tải, tôi đã sử dụng công cụ thử nghiệm tải hey, nhưng bạn có thể sử dụng bất kỳ công cụ nào bạn muốn, như Serverless-artillery hoặc Postman.
Chúng ta sẽ đo lường với biên dịch theo cấp bậc (mặc định trong Java 21, không cần thiết phải thiết lập gì thêm) và tùy chọn biên dịch XX:+TieredCompilation -XX:TieredStopAtLevel=1. Để sử dụng tùy chọn cuối cùng này, bạn phải đặt nó trong template.yaml
trong biến môi trường JAVA_OPTIONS
như sau:
yaml
Globals:
Function:
...
Environment:
Variables:
JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
Xin lưu ý cũng về hiệu ứng của bộ nhớ đệm cấp bậc SnapStart Snapshot của AWS. Điều này có nghĩa là trong trường hợp kích hoạt SnapStart, chúng ta sẽ có những cold start lớn nhất trong các phép đo đầu tiên. Nhờ vào bộ nhớ đệm cấp bậc, các cold start tiếp theo sẽ có giá trị thấp hơn. Để biết thêm chi tiết về cài đặt kỹ thuật của AWS SnapStart và bộ nhớ đệm cấp bậc của nó, tôi khuyên bạn nên xem bài thuyết trình của Mike Danilov: "AWS Lambda Under the Hood". Do đó, tôi sẽ trình bày các phép đo hiệu suất Lambda với SnapStart được kích hoạt cho tất cả khoảng 100 thời gian cold start (được đánh dấu là all trong bảng), nhưng cũng cho khoảng 70 thời gian cold start cuối cùng (được đánh dấu là last 70 trong bảng), để bạn có thể thấy được ảnh hưởng của bộ nhớ đệm cấp bậc Snapshot. Tùy thuộc vào việc hàm Lambda cụ thể được cập nhật bao nhiêu lần và do đó một số lớp của bộ nhớ đệm bị vô hiệu hóa, một hàm Lambda có thể trải qua hàng nghìn hoặc hàng chục nghìn cold start trong vòng đời của nó, vì vậy các cold start lâu hơn ban đầu không còn quan trọng nhiều.
Để cho thấy tác động của SnapStart với yêu cầu DynamoDB priming, chúng tôi cũng sẽ trình bày các phép đo hiệu suất Lambda mà không kích hoạt SnapStart từ phần 1 và với SnapStart được kích hoạt nhưng không áp dụng các kỹ thuật priming như đã đo lường trong phần 2.
Thời gian cold (c) và warm (w) start với tiered compilation trong ms:
Số kịch bản | c p50 | c p75 | c p90 | c p99 | c p99.9 | c max | w p50 | w p75 | w p90 | w p99 | w p99.9 | w max |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Không kích hoạt SnapStart | 4948 | 5038 | 5155 | 5387 | 5403 | 5404 | 5.37 | 6.01 | 7.10 | 16.01 | 52.05 | 1535 |
Kích hoạt SnapStart nhưng không áp dụng priming, tất cả | 1926 | 1981 | 3213 | 3232 | 3242 | 3245 | 5.33 | 5.96 | 6.93 | 14.43 | 38.76 | 2617 |
Kích hoạt SnapStart nhưng không áp dụng priming, 70 gần nhất | 1900 | 1959 | 2001 | 2063 | 2063 | 2063 | 5.29 | 5.91 | 6.93 | 14.66 | 37.84 | 1588 |
Kích hoạt SnapStart và áp dụng yêu cầu DynamoDB priming, tất cả | 743 | 787 | 879 | 1300 | 1798 | 1798 | 5.42 | 6.10 | 7.27 | 14.90 | 36.08 | 1095 |
Kích hoạt SnapStart và áp dụng yêu cầu DynamoDB priming, 70 gần nhất | 730 | 787 | 878 | 1301 | 1301 | 1301 | 5.37 | 6.10 | 7.33 | 15.02 | 33.85 | 433 |
Thời gian cold (c) và warm (w) start với -XX:+TieredCompilation -XX:TieredStopAtLevel=1 biên dịch trong ms:
Số kịch bản | c p50 | c p75 | c p90 | c p99 | c p99.9 | c max | w p50 | w p75 | w p90 | w p99 | w p99.9 | w max |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Không kích hoạt SnapStart | 4993 | 5145 | 5392 | 5697 | 5852 | 5856 | 5.33 | 5.91 | 6.88 | 15.50 | 52.47 | 1616 |
Kích hoạt SnapStart nhưng không áp dụng priming, tất cả | 1895 | 1947 | 2025 | 2154 | 3368 | 3369 | 5.55 | 5.82 | 6.72 | 14.86 | 104.68 | 2609 |
Kích hoạt SnapStart nhưng không áp dụng priming, 70 gần nhất | 1891 | 1923 | 1989 | 2066 | 2066 | 2066 | 5.13 | 5.73 | 6.61 | 14.17 | 35.01 | 1637 |
Kích hoạt SnapStart và áp dụng yêu cầu DynamoDB priming, tất cả | 696 | 755 | 1611 | 1632 | 1632 | 1632 | 5.21 | 5.92 | 7.16 | 15.09 | 43.72 | 952 |
Kích hoạt SnapStart và áp dụng yêu cầu DynamoDB priming, 70 gần nhất | 663 | 693 | 759 | 826 | 826 | 826 | 5.13 | 5.82 | 7.05 | 14.39 | 35.57 | 370 |
Kết luận
Trong phần này của loạt bài, chúng ta đã giới thiệu cách áp dụng kỹ thuật priming với Lambda SnapStart bằng cách bắt đầu với yêu cầu DynamoDB priming nhằm mục đích cải thiện hơn nữa hiệu suất của các hàm Lambda. Chúng ta đã thấy rằng bằng cách thực hiện loại priming này thông qua một số mã đơn giản, chúng ta có thể giảm đáng kể (tùy thuộc vào các phần trăm lên tới hơn 50%) thời gian cold start của Lambda so với việc chỉ kích hoạt SnapStart. Hơn nữa, chúng ta có thể giảm đáng kể giá trị tối đa cho thời gian warm start của Lambda bằng cách tải trước các lớp (do Java tải lớp theo cách lười biếng khi chúng được yêu cầu lần đầu tiên) và thực hiện một số công việc khởi tạo trước (bằng cách gọi phương thức để lấy sản phẩm từ bảng DynamoDB theo ID của nó), điều này sẽ chỉ xảy ra một lần trong lần thực thi ấm đầu tiên của hàm Lambda.
Chúng ta cũng đã thấy rằng việc chọn biên dịch -XX:+TieredCompilation -XX:TieredStopAtLevel=1 dẫn đến thời gian cold start thấp hơn rất nhiều so với biên dịch tiered compilation cho loại priming này.
Chúng ta cũng đã thấy rõ ảnh hưởng của bộ nhớ đệm cấp bậc SnapStart Snapshot của AWS trong các phép đo của chúng ta.
Trong phần tiếp theo của loạt bài viết, chúng ta sẽ giới thiệu một kỹ thuật priming SnapStart khác là API Gateway Request Event priming. Sau đó, chúng ta sẽ đo lường hiệu suất Lambda bằng cách áp dụng nó và so sánh kết quả với các phương pháp đã được giới thiệu trước đó.