Building well-architected serverless applications: Optimizing application performance – part 1

Julian Wood | vào ngày 17 tháng 8 năm 2021

Loạt bài viết này sử dụng công cụ AWS Well-Architected với Lens Serverless để giúp khách hàng xây dựng và vận hành ứng dụng theo best practice. Trong mỗi bài, mình sẽ giải đáp các câu hỏi cụ thể về serverless được xác định bởi Lens Serverless cùng với các best practices được khuyến nghị. Xem post này để biết thêm về việc xây dựng ứng dụng well-architected serverless.

PERF 1. Tối ưu hóa hiệu suất của ứng dụng serverless của bạn

Đánh giá và tối ưu hóa hiệu suất của ứng dụng serverless của bạn dựa trên các access patterns, cơ chế scaling và tích hợp native. Điều này cho phép bạn liên tục đạt được nhiều giá trị hơn cho mỗi giao dịch. Bạn có thể cải thiện trải nghiệm tổng thể của mình và sử dụng nền tảng hiệu quả hơn cả về giá trị và tài nguyên.

Cách làm tốt: Đo lường và tối ưu hóa thời gian khởi động function

Đánh giá thời gian khởi động AWS Lambda function của bạn về cả hiệu suất và chi phí.

Tận dụng khả năng tái sử dụng môi trường thực thi để cải thiện hiệu suất cho function của bạn.

Lambda gọi function của bạn trong một môi trường thực thi an toàn và hoàn toàn cô lập, nó quản lý các tài nguyên cần thiết để chạy function của bạn. Khi một function được gọi lần đầu tiên, dịch vụ Lambda tạo một instance của function để xử lý sự kiện. Điều này được gọi là cold start. Sau khi hoàn thành, function vẫn có sẵn trong một khoảng thời gian để xử lý các sự kiện tiếp theo. Đây được gọi là warm start.

Các Lambda function phải chứa một method để xử lý các sự kiện. Trong quá trình khởi cold start, Lambda chạy mã khởi tạo của function, tức là mã bên ngoài phương thức xử lý, và sau đó chạy mã xử lý (code của bạn). Trong quá trình khởi warm start, Lambda chạy mã xử lý.

Image link

Lambda function cold start và warm starts

Khởi tạo các SDK clients, object và kết nối cơ sở dữ liệu được khởi đầu trong quá trình cold start. Những kết nối này sau đó sẽ được duy trì trong các lần khởi warm start sau, từ đó cải thiện hiệu suất và chi phí của function.

Lambda cung cấp một hệ thống tập tin cục bộ có thể ghi được có sẵn tại /tmp. Đây là cục bộ cho mỗi function nhưng được chia sẻ giữa các lần gọi sau trong cùng một môi trường thực thi. Bạn có thể tải xuống và lưu trữ tài nguyên cục bộ trong thư mục /tmp trong quá trình cold start. Dữ liệu này sau đó sẽ được sử dụng cục bộ trong tất cả các lần warm start sau để cải thiện hiệu suất.

Trong ví dụ về serverless airline được sử dụng trong loạt bài này, Lambda function sẽ xác nhận đặt vé (confirm booking) khởi tạo một số thành phần trong quá trình cold start. Điều này bao gồm các tiện ích Lambda Powertools và tạo một session đến Amazon DynamoDB table `BOOKING_TABLE_NAME`.

“`Python

import boto3

from aws_lambda_powertools import Logger, Metrics, Tracer

from aws_lambda_powertools.metrics import MetricUnit

from botocore.exceptions import ClientError

logger = Logger()

tracer = Tracer()

metrics = Metrics()

session = boto3.Session()

dynamodb = session.resource(“dynamodb”)

table_name = os.getenv(“BOOKING_TABLE_NAME”, “undefined”)

table = dynamodb.Table(table_name)

“`

Phân tích và cải thiện thời gian khởi động

Có một số bước bạn có thể thực hiện để đo lường và tối ưu hóa thời gian khởi tạo Lambda function.

Bạn có thể xem thời gian cold start của function bằng cách sử dụng Amazon CloudWatch Logs và AWS X-Ray. Dòng REPORT nhật ký cho quá trình khởi động nguội bao gồm giá trị Init Duration. Đây là thời gian mã khởi tạo chạy trước trình xử lý.

Image link

CloudWatch Logs cold start

Khi tính năng theo dõi X-Ray được bật cho một function, nó sẽ có phần Initialization.

Image link

X-Ray trace cold start hiển thị phần khởi tạo

Dòng REPORT  warm start tiếp theo không bao gồm giá trị Duration ban đầu và không có trong dấu vết X-Ray:

Image link

CloudWatch Logs warm start

Image link

X-Ray trace warm start mà không hiển thị phần khởi tạo

CloudWatch Logs Insights cho phép bạn tìm kiếm và phân tích dữ liệu CloudWatch Logs qua nhiều log groups. Ở đây sẽ có một số tìm kiếm hữu ích để hiểu về cold start.

Phần trăm khởi động nguội theo thời gian:

“`

filter @type = “REPORT”

| stats

  sum(strcontains(

    @message,

    “Init Duration”))

  / count(*)

  * 100

  as coldStartPercentage,

  avg(@duration)

  by bin(5m)

“`

Image link

Tỷ lệ khởi động nguội theo thời gian

Số lần cold start và `InitDuration`:

“`

filter @type=”REPORT” 

| fields @memorySize / 1000000 as memorySize

| filter @message like /(?i)(Init Duration)/

| parse @message /^REPORT.*Init Duration: (?<initDuration>.*) ms.*/

| parse @log /^.*\/aws\/lambda\/(?<functionName>.*)/

| stats count() as coldStarts, median(initDuration) as avgInitDuration, max(initDuration) as maxInitDuration by functionName, memorySize

“`

Image link

Số lần cold start và `InitDuration`

Khi bạn đã đo được hiệu suất cold start, có một số cách để tối ưu hóa thời gian khởi động. Đối với Python, bạn có thể sử dụng biến môi trường `PYTHONPROFILEIMPORTTIME=1`.

Image link

PYTHONPROFILEIMPORTTIME environment variable

HÌnh dưới cho biết mỗi lần import package mất bao lâu để giúp bạn hiểu các package tác động như thế nào đến thời gian khởi động.

Image link

Thời gian Python import 

Trước đây, khi sử dụng AWS Node.js SDK, bạn đã bật HTTP keep-alive trong mã nguồn của mình để duy trì các kết nối TCP. Bật keep-alive giúp bạn tránh việc thiết lập một kết nối TCP mới cho mỗi yêu cầu. Từ phiên bản AWS SDK 2.463.0 trở đi, bạn cũng có thể đặt biến môi trường `AWS_NODEJS_CONNECTION_REUSE_ENABLED=1` cho Lambda function để SDK tự động sử dụng lại các kết nối.

Bạn có thể cấu hình tính năng provisioned concurrency của Lambda để tiền khởi tạo một số lượng xác định các môi trường thực thi. Điều này giúp chạy mã khởi tạo cold start để sẵn sàng phản hồi ngay lập tức cho các lời gọi của function của bạn.

Sử dụng Amazon RDS Proxy để gom nhóm và chia sẻ các kết nối cơ sở dữ liệu để cải thiện hiệu suất của function. Để biết thêm tùy chọn sử dụng RDS với Lambda, bạn có thể xem bài viết trên blog AWS Serverless Hero về “Cách quản lý kết nối RDS từ các Serverless function của AWS Lambda”.

Chọn các framework khởi động nhanh khi khởi tạo function. Ví dụ, ưu tiên các framework đơn giản về dependency injection cho Java như Dagger hoặc Guice thay vì các framework phức tạp như Spring. Khi sử dụng AWS SDK cho Java, có một số gợi ý tối ưu hóa hiệu suất cold start trong tài liệu. Để biết thêm mẹo tối ưu hóa hiệu suất cho Java, bạn có thể xem hội thảo AWS re:Invent về “Các biện pháp thực hành tốt nhất cho AWS Lambda và Java”.

Để giảm kích thước packages triển khai, hãy chọn các framework web nhẹ được tối ưu hóa cho Lambda. Ví dụ, bạn có thể sử dụng MiddyJS, Lambda API JS và Python Chalice thay vì Node.js Express, Python Django hoặc Flask.

Nếu function của bạn có nhiều đối tượng và kết nối, hãy xem xét chia function thành nhiều function chuyên biệt. Các function này nhỏ hơn và có ít mã khởi tạo hơn. Mình đã đề cập đến việc thiết kế các function nhỏ, đơn mục đích từ góc độ bảo mật trong bài viết “Quản lý ranh giới bảo mật ứng dụng – phần 2”.

Giảm thiểu kích thước package triển khai của bạn xuống mức cần thiết trong runtime 

Function nhỏ cũng cho phép bạn tách biệt các chức năng. Hãy nhập các libraries và dependencies cần thiết cho ứng dụng của bạn. Sử dụng code bundling khi có thể, để giảm tác động của các cuộc gọi tìm kiếm hệ thống tệp. Điều này cũng bao gồm kích thước package triển khai.

Ví dụ, nếu bạn chỉ sử dụng Amazon DynamoDB trong AWS SDK, thay vì importing toàn bộ SDK, bạn có thể importing một dịch vụ cụ thể. So sánh ba ví dụ sau được hiển thị trong Hướng dẫn Sử dụng Lambda:

“`JavaScript

// Instead of const AWS = require(‘aws-sdk’), use: +

const DynamoDB = require(‘aws-sdk/clients/dynamodb’)

// Instead of const AWSXRay = require(‘aws-xray-sdk’), use: +

const AWSXRay = require(‘aws-xray-sdk-core’)

// Instead of const AWS = AWSXRay.captureAWS(require(‘aws-sdk’)), use: +

const dynamodb = new DynamoDB.DocumentClient() +

AWSXRay.captureAWSClient(dynamodb.service)

“`

Trong quá trình thử nghiệm, việc import thư viện DynamoDB thay vì toàn bộ SDK AWS nhanh hơn 125ms. Việc import thư viện cơ bản của X-Ray nhanh hơn 5ms so với SDK X-Ray. Tương tự, khi wrapping một quá trình khởi tạo dịch vụ, việc chuẩn bị một `DocumentClient` trước khi wrapping đã cho 140 ms. Phiên bản 3 của SDK AWS cho JavaScript hỗ trợ các import theo mô-đun, có thể giúp giảm bớt các phụ thuộc không sử dụng.

Đối với các tùy chọn bổ sung khi tối ưu hóa việc import SDK Node.js của AWS, xem bài đăng blog của AWS Serverless Hero.

Phần kết luận

Đánh giá và tối ưu hiệu suất ứng dụng serverless của bạn dựa trên các access patterns, cơ chế scaling và tích hợp native . Bạn có thể cải thiện trải nghiệm tổng thể và sử dụng nền tảng hiệu quả hơn về cả giá trị và tài nguyên.

Trong bài viết này, mình đã đề cập đến việc đo lường và tối ưu hóa thời gian khởi động function. Mình đã giải thích về cold start và warm start, cũng như cách tái sử dụng môi trường thực thi Lambda để cải thiện hiệu suất. Mình cũng đã trình bày một số cách để phân tích và tối ưu thời gian khởi động khởi tạo. Mình cũng đã giải thích làm thế nào việc import các libraries và dependencies cần thiết giúp tăng hiệu suất ứng dụng.

Phần 2 về well-architected này sẽ tiếp tục với việc thiết kế function của bạn để tận dụng concurrency thông qua các lời gọi bất đồng bộ và dựa trên luồng. Mình sẽ đề cập đến việc đo lường, đánh giá và chọn các capacity units tối ưu.

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