Được viết bởi Luca Mezzalira, Principal SA, and Matt Diamond, Principal, SA.
Thiết kế khối lượng công việc với AWS Lambda đặt ra câu hỏi cho lập trình viên bởi vì tính module có thể được thể hiện tại code hoặc cấp độ kiến trúc hạ tầng. Việc sử dụng serverless để chạy code yêu cầu thêm kế hoạch bổ sung để tách logic kinh doanh khỏi các thành phần chức năng cơ bản. Sự tách biệt rõ ràng này đảm bảo tính module mạnh mẽ, mở đường phát triển kiến trúc.
Bài viết này tập trung đồng bộ khối lượng công việc, nhưng những cân nhắc tương tự cũng có thể áp dụng cho các thể loại khối lượng công việc khác. Sau xác nhận ngữ cảnh giới hạn của API của bạn và thống nhất với hợp đồng API với người dùng, đã đến lúc cấu trúc kiến trúc của ngữ cảnh giới hạn đó và cơ sở hạ tầng liên quan.
Có 2 cách để cấu trúc API sử dụng Lambda function là trách nghiệm đơn lẻ và Lambda-lith. Tuy nhiên, bài đăng trên blog này sẽ khám phá một cách tiếp cận thay thế cho các phương pháp này, có thể cung cấp những điều tốt nhất của cả hai.
Trách nhiệm duy nhất Lambda functions
Trách nhiệm duy nhất của lambda function được thiết kế để chạy một tác vụ cụ thể hoặc xử lý hoạt động các phần event-triggered trong kiến trúc serverless:
Cách tiếp cận này mang lại sự phân tách rõ ràng giữa logic kinh doanh và các khả năng mở rộng. Bạn có thể kiểm thử độc lập các khả năng mở rộng cụ thể, triển khai Lambda function độc lập, giảm bề mặt để đưa các lỗi vào, và thiết lập gỡ lỗi dễ dàng hơn cho các issue ở Amazon CloudWatch.
Ngoài ra, các hàm đơn mục đích cho phép phân bổ tài nguyên hiệu quả vì Lambda tự động thu phóng dựa trên nhu cầu, tối ưu nguồn tài nguyên tiêu thụ, và giảm thiểu chi phí. Điều này bạn có thể chỉnh sửa kích thước bộ nhớ, kiến trúc hệ thống, và bất kỳ cấu hình khác có sẵn trên chức năng. Hơn thế nữa, việc yêu cầu cập nhật thực thi đồng thời các chức năng thông qua phiếu hỗ trợ trở nên dễ dàng hơn vì bạn không tổng hợp lưu lượng truy cập vào function Lambda đơn lẻ để xử lý mọi yêu cầu mà bạn có thể yêu cầu tăng cụ thể dựa trên lưu lượng truy cập của một tác vụ duy nhất.
Lợi thế khác là thời gian thực thi nhanh. Xét về logic kinh doanh cho function Lambda đơn mục đích được thiết kế cho 1 tác vụ duy nhất, bạn có thể tối ưu kích thước của chức năng dễ dàng hơn, mà không cần thêm bất kỳ thư viện bổ sung cần thiết trong các cách tiếp cận khác. Điều này giúp giảm thời gian cold start vì kích thước gói gọn nhỏ hơn.
Mặc dù có nhiều lợi ích, nhưng vẫn tồn tại 1 số vấn đề khi phụ thuộc vào single-purpose Lambda functions. Mặc dù thời gian cold start bị giảm nhẹ, bạn có thể gặp nhiều lần với số lượng lớn cold starts, đặc biệt đối với các function có lượt gọi không thường xuyên hoặc hiếm khi. Ví dụ, một hàm xóa người dùng trong bảng Amazon DynamoDB có thể sẽ không được kích hoạt thường xuyên như hàm đọc dữ liệu người dùng. Hơn nữa, việc phụ thuộc mạnh vào single-purpose Lambda functions có thể dẫn tới tăng độ phức tạp hệ thống, đặc biệt là số lượng của chức năng tăng.
Sự phân tách mối quan tâm tốt giúp duy trì cơ sở code của bạn, nhưng đánh đổi là thiếu tính gắn kết. Trong các chức năng với các nhiệm vụ giống nhau, như viết vận hành API(POST, PUT, DELETE), bạn có thể sao chép code và hành vi giữa nhiều chức năng. Hơn thế nữa, cập nhật các thư viện chung được chia sẻ thông qua Lambda Layers, hoặc các hệ thống quản lý dependency khác, yêu cầu nhiều thay đổi trên mỗi chức năng thay vì thay đổi 1 file duy nhất. Điều này cũng đúng cho bất kỳ sự thay đổi chức năng nào, ví dụ như cập nhật phiên bản runtime.
Lambda-lith: Using one single Lambda function
Khi nhiều khối lượng công việc sử dụng single purpose Lambda functions, các lập trình viên sẽ có rất nhiều Lambda functions trên tài khoản AWS. Một trong những thách thức chính mà nhiều lập trình đối mặt là cập nhật dependencies chung hoặc các cấu hình chức năng. Trừ khi có một chiến lược quản trị rõ ràng được triển khai để giải quyết vấn đề này (chẳng hạn như sử dụng Dependabot để thực thi việc cập nhật các phụ thuộc hoặc các tham số được tham số hóa được truy xuất tại thời điểm cung cấp), các lập trình viên có thể lựa chọn các chiến lược khác.
Do đó, nhiều đội ngũ phát triển chuyển sang hướng ngược lại, tất cả code liên quan tới 1 API bên trong cùng Lambda function
Cách tiếp cận này thường liên quan tới Lambda-lith, bởi vì thu thập toàn bộ HTTP verbs soạn thành 1 API và thỉnh thoảng nhiều APIs trong cùng 1 function.
Điều này cho phép bạn có sự gắn kết code tốt hơn và cùng vị trí trên các phần khác nhau của ứng dụng. Tính module trong trường hợp này được thể hiện ở cấp độ code nơi mà các mẫu như single responsibility, dependency injection, và façade được áp dụng vào cấu trúc code của bạn. Kỷ luật và code best practices được áp dụng bởi đội ngũ phát triển là rất quan trọng để duy trì các cơ sở code lớn.
Tuy nhiên, xét đến việc giảm số lượng hàm Lambda, việc cập nhật cấu hình hoặc triển khai một tiêu chuẩn mới trên nhiều API có thể đạt được dễ dàng hơn so với cách tiếp cận trách nhiệm đơn lẻ.
Hơn nữa, vì mỗi yêu cầu kích hoạt cùng Lambda function cho mọi HTTP verb, nên các phần ít được sử dụng trong code của bạn có khả năng phản hồi tốt hơn vì môi trường thực thi có nhiều khả năng sẵn sàng để thực hiện yêu cầu hơn.
Một yếu tố khác tới sự cân nhắc là kích thước function. Điều này tăng lên khi đặt tất cả các verb trong cùng function với toàn bộ dependencies và logic kinh doanh của API. Điều này có thể ảnh hưởng đến thời gian khởi động cold start của Lambda functions với khối lượng công việc đột ngột. Khách hàng nên đánh giá những lợi ích của cách tiếp cận này, đặc biệt khi các ứng dụng có SLA hạn chế điều này ảnh hưởng bởi thời gian cold starts. Nhiều lập trình viên có thể giảm thiểu vấn đề này bởi chú ý tới dependencies được sử dụng và thực hiện các kỹ thuật như tree-shaking, minification, và dead code elimination,ở những trường hợp mà ngôn ngữ lập trình cho phép.
Cách tiếp cận cấp độ cao (coarse grain) này sẽ không cho phép bạn điều chỉnh cấu hình chức năng riêng lẻ. Nhưng bạn phải tìm cấu hình phù hợp với toàn bộ các khả năng code với kích thước bộ nhớ có thể cao hơn và nới lỏng quyền bảo mật, có thể xung đột với các yêu cầu đội ngũ an ninh đặt ra.
Read and write functions
2 cách tiếp cận này đều có những đánh đổi, nhưng có một sự lựa chọn thứ 3 có thể kết hợp với lợi ích của chúng.
Thông thường, lưu lượng truy cập API nghiêng về đọc và ghi và bắt buộc các lập trình viên tối ưu code và cấu hình nhiều hơn ở một phía so với phía khác.
Ví dụ, xây dựng một API người dùng cho phép người dùng tạo, cập nhật và xóa user nhưng cũng cho tìm user hoặc danh sách user. Trong trường hợp này, bạn có thể thay đổi 1 user một lần mà không có sẵn các hoạt động hàng loạt, nhưng bạn có thể lấy 1 hoặc nhiều user trên mỗi yêu cầu API. Việc phân chia thiết kế của API thành các hoạt động đọc và ghi dẫn đến kiến trúc sau:
Sự gắn kết code cho các hoạt động ghi(create, update, and delete) có lợi ích cho nhiều lý do. Ví dụ bạn có thể cần xác thực body yêu cầu, đảm bảo nó chứa toàn bộ các tham số bắt buộc. Nếu khối lượng công việc thiên về ghi, thì các hoạt động ít được ít sử dụng (ví dụ delete) sẽ được hưởng lợi từ môi trường thực thi ấm. Vị trí đồng vị trí của code cho phép sử dụng lại của code trên các hành động tương tự, giảm tải nhận thức để cấu trúc của project của bạn với các thư viện dùng chung hoặc Lambda layers, ví dụ.
Khi nhìn vào phía hoạt động đọc, bạn có thể giảm lượng code được đóng gói với function này, có thời gian khởi động cold start nhanh hơn, và tối ưu hóa hiệu suất đáng kể so với hoạt động ghi. Bạn có thể lưu trữ 1 phần hoặc toàn bộ kết quả truy vấn trong bộ nhớ của môi trường thực thi để cải thiện thời gian thực thi của Lambda function.
Cách tiếp cận này giúp bạn tiến xa hơn với bản chất tiến hóa của nó. Hãy tưởng tượng nếu nền tảng này trở nên phổ biến hơn. Bây giờ, bạn phải tối ưu API thậm chí hơn nữa bằng cách cải thiện các hoạt động đọc và thêm cache aside pattern với ElastiCache and Redis. Hơn thế nữa, bạn đã quyết định tối ưu hóa các truy vấn đọc với database thứ 2 được tối ưu cho khả năng đọc khi cache bị bỏ lỡ.
Ở phía ghi, bạn đã thống nhất với người dùng API rằng việc nhận và xác nhận tạo hoặc xóa người dùng là đủ, vì họ hoàn toàn chấp nhận bản chất nhất quán cuối cùng của các hệ thống phân tán.
Bây giờ bạn có thể cải thiện thời gian phản hồi của các hoạt động ghi bằng cách thêm hàng đợi SQS trước Lambda function. Bạn có thể cập nhật ghi database trong các batches để giảm số lượng gọi cần thiết để xử lý các hoạt động ghi, thay vì giải quyết mỗi yêu cầu riêng biệt.
Command query responsibility segregation (CQRS) là mô hình được thiết lập tốt để tách biệt dữ liệu biến đổi hoặc phần lệnh của hệ thống, từ phần truy vấn. Bạn có thể sử dụng CQRS pattern để tách biệt cập nhật và truy vấn nếu hộ có yêu cầu khác nhau về thông lượng, độ trễ hoặc tính nhất quán.
Mặc dù không bắt buộc bắt đầu với CQRS pattern đầy đủ, bạn có thể dễ dàng phát triển từ cơ sở hạ tầng được nêu bật hơn trong việc triển khai đọc và ghi ban đầu, mà không cần phải cấu trúc lại API một cách massive.
So sánh của 3 cách tiếp cận
Đây là sự so sánh của 3 cách tiếp cận
| Single responsibility | Lambda-lith | Read and write | |
| Benefits | Phân tách rõ ràng các mối quan tâmCấu hình chi tiếtGỡ lỗi dễ dàng hơnThời gian thực thi nhanh | Ít khởi động cold start hơnSự gắn kết code cao hơnBảo trì đơn giản hơn | Sự gắn kết code khi cần thiếtKiến trúc phát triểnTối ưu hóa các hoạt động đọc và ghi |
| Issues | Lặp lại codeBảo trì phức tạpSố lần khởi động cold start nhiều h | Cấu hình Corse grain Thời gian khởi động cold start cao hơn | Sử dụng CQRS với 2 data modelsCQRS bổ sung tính nhất quán cho hệ thống của bạn |
Kết luận
Nhiều lập trình viên thường chuyển từ single responsibility functions sang Lambda-lith vì kiến trúc của họ phát triển, nhưng cả 2 cách tiếp cận này có những đánh đổi tương đối. Bài viết này cho thấy cách tận dụng cả hai phương pháp tốt nhất bằng cách phân chia khối lượng công việc của bạn theo các hoạt động đọc và ghi.
Toàn bộ 3 cách tiếp cận đều khả thi cho thiết kế serverless APIs, và hiểu điều gì bạn đang tối ưu hóa là chìa khóa để đưa ra quyết định tốt nhất. Nhớ rằng, hiểu ngữ cảnh và yêu cầu kinh doanh để thể hiện các ứng dụng của bạn sẽ dẫn đến những đánh đổi có thể chấp nhận được để xác định trong khối lượng công việc cụ thể. Giữ tư duy cởi mở và tìm giải pháp để giải quyết vấn đề cân bằng bảo mật, kinh nghiệm lập trình viên, chi phí, và bảo trì
For more serverless learning resources, visit Serverless Land.
Bài viết được dịch từ bài viết gốc ở Đây