của James Beswick | vào ngày 4 tháng 3 năm 2024 | trong AWS Lambda, Serverless | Permalink | Share
Bài viết này được viết bởi Luca Mezzalira, Principal SA, và Matt Diamond, Principal, SA.
Thiết kế khối lượng công việc với AWS Lambda tạo ra câu hỏi cho các nhà phát triển do tính module mà có thể được thể hiện ở cấp độ code hoặc infrastructure. Sử dụng serverless để code yêu cầu lập kế hoạch bổ sung để trích xuất những business logic từ những thành phần chức năng cơ bản. Sự tách biệt có chủ ý các mối quan tâm này đảm bảo tính module mạnh mẽ, mở đường cho kiến trúc tiến hóa.
Bài viết này tập trung vào synchronous workloads, nhưng những cân nhắc tương tự cũng có thể áp dụng với những loại workload khác. Sau khi xác định bối cảnh giới hạn API của bạn và thống nhất hợp đồng với khách hàng, đã đến lúc để cấu trúc kiến trúc của bối cảnh giới hạn của bạn và những infrastructure đi kèm.
Có 2 cách phổ biến để cấu trúc một API sử dụng Lambda functions là single responsibility và Lambda-lith. Tuy nhiên, bài viết này sẽ khám phá một phương pháp thay thế cho những giải pháp trên, có thể mang lại hiệu quả tốt cho cả hai.
Single responsibility Lambda functions
Single responsibility Lambda functions được thiết kế để chạy một task nhất định hoặc xử lý những hoạt động event-triggered cụ thể trong một serverless architecture:
Cách tiếp cận này cung cấp sự tách biệt mạnh mẽ của những mối quan tâm giữa business logic và khả năng. Bạn có thể kiểm tra các khả năng cụ thể một cách riêng biệt, triển khai một Lambda function một cách độc lập, cắt giảm bề mặt để giảm lỗi và cho phép dễ dàng debugging vấn đề trong Amazon CloudWatch.
Thêm vào đó, Single purpose functions cho phép phân bổ tài nguyên hiệu quả như tự động scales Lambda một cách tự động dựa trên nhu cầu, tối ưu lượng tài nguyên tiêu thụ và tối ưu giá cả. Điều này có nghĩa bạn có thể sửa đổi memory size, architecture và bất kì cấu hình nào khác có sẵn cho mỗi function. Hơn thế nữa, việc yêu cầu update việc thực thi hàm đồng thời thông qua phiếu hỗ trợ trở nên dễ dàng hơn vì bạn không cần tổng hợp lưu lượng truy cập vào một Lambda function duy nhất để xử xử lý mọi yêu cầu, mà bạn có thể yêu cầu tăng dựa trên lưu lượng của một tác vụ
Một ưu điểm thuận lợi khác là thời gian thực thi nhanh chóng. Xem xét business logic cho một single-purpose Lambda function được thiết kế cho một nhiệm vụ duy nhất, bạn có thể tối ưu kích thước của một function dễ dàng hơn rất nhiều, không cần thêm một thư viện bổ sung như trong những cách khác. Điều này giúp cắt giảm cold start time do kích thước bó nhỏ hơn.
Bất chấp những lợi ích này, một vài vấn đề vẫn tồn tại khi chỉ dựa vào single-purpose Lambda functions. trong khi cold start time được giảm bớt, bạn có thể gặp phải số lần cold starts nhiều hơn, đặc biệt đối với các chức năng có lệnh gọi lẻ tẻ hoặc không thường xuyên. Ví dụ, một function xóa users trong Amazon DynamoDB table có thể sẽ không được kích hoạt thường xuyên như đọc dữ liệu người dùng. Phụ thuộc nhiều vào single-purpose Lambda functions cũng có thể dẫn đến tăng sự phức tạp của hệ thống, đặc biệt khi số lượng functions tăng lên.
Việc phân tách tốt các mối quan tâm giúp duy trì duy trì cơ sở mã code base của bạn, và cái giá là sự thiếu gắn kết. Trong các functions với nhiệm vụ tương tự, giống như là thao tác ghi của một API (POST, PUT, DELETE), bạn có thể lặp lại code và những hành vi qua nhiều functions. Hơn thế nữa, việc cập nhật những thư viện phổ biến được chia sẻ thông qua nhiều Lambda Layer hoặc những hệ thống quản lý phụ thuộc khác yêu cầu nhiều thay đổi qua mỗi function thay vì một thay đổi atomic trên a file. Điều này cũng đúng với bất kỳ thay đổi nào khác qua nhiều functions, ví dụ cập nhật runtime version.
Lambda-lith: Sử dụng một single Lambda function
Khi nhiều workloads 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 một tài khoản AWS. Một trong những thách thức chính mà các lập trình viên phải đối mặt là cập nhật các dependencies phổ biến hoặc cấu hình functions. Trừ khi có một chiến lược quản lý rõ ràng thực hiện để giải quyết vấn đề này (chẳng hạn như sử dụng Dependabot để củng thực hiện update các dependencies hoặc tham số hóa các parameters được truy xuất ở thời điểm cung cấp), các lập trình viên có thể chọn những chiến lược khác nhau
Kết quả là có rất nhiều development teams đi theo hướng ngược lại, tổng hợp tất cả những code liên quan đến API vào trong cùng Lambda function.
Cách tiếp cận này thường biết đến là Lambda-lith, vì nó tập hợp tất cả những hoạt động HTTP tạo nên API và đôi khi là nhiều API trong cùng một function.
Điều này cho phép bạn có code với sự gắn kết cao hơn và đặt chúng gần nhau hơn trong 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 các pattern như single responsibility, dependency injection và facade được áp dụng để cấu trúc code của bạn. Những nguyên tắc và code best practice được áp dụng bởi những development teams là quan trọng để duy trì số lượng lớn code bases.
Tuy nhiên, xem xét số lượng hàm Lambda giảm, cập nhật cấu hình hoặc thực hiện triển khai một tiêu chuẩn mới thông qua nhiều APIs có thể đạt được dễ dàng hơn so với cách single responsibility.
Hơn thế nữa, vì mọi request đều gọi trong cùng một Lambda function, có nhiều khả năng những phần code ít được sử dụng sẽ có thời gian phản hồi tốt hơn vì môi trường thực thi có khả năng sẵn sàng đáp ứng cao hơn.
Một nhân tố khác cần được xem xét là kích thước function. Điều này tăng lên khi đặt các hoạt động cùng một chức năng với tất cả dependencies và business logic của một API. Điều này có thể ảnh hưởng đến cold start của Lambda function của bạn với workloads tăng vọt. Khách hàng nên đánh giá lợi ích của cách tiếp cận này, đặc biệt khi ứng dụng có giới hạn SLAs bị tác động bởi cold start. Các lập trình viên có thể giảm thiểu vấn đề này bằng cách tập trung vào những dependencies được sử dụng và thực hiện những kỹ thuật như tree-shaking, minification, và dead code elimination nếu ngôn ngữ lập trình cho phép
Cách tiếp cận chi tiết thô này sẽ không cho phép bạn điều chỉnh các cấu hình chức năng của mình một cách riêng lẻ. Tuy nhiên, bạn phải tìm một cấu hình phù hợp với tất cả các khả năng của mã với kích thước bộ nhớ có thể cao hơn và các quyền bảo mật lỏng lẻo hơn có thể xung đột với các yêu cầu do nhóm bảo mật xác định.
Đọc và ghi function
Hai cách tiếp cận trên đều có những đánh đổi, nhưng có một lựa chọn thứ 3 là phối hợp lợi ích của 2 cách trên.
Thông thường, lưu lượng API nghiêng về read hoặc ghi nhiều hơn và điều đó buộc các lập trình viên phải tối ưu code và cấu hình nhiều hơn ở một phía.
Ví dụ, hãy xem xét xây dựng một user API cho phép khách hàng create, update và delete user cũng như tìm user hoặc danh sách users. Trong ngữ cãnh này, bạn có thể thay đổi từng user mà không cần thực hiện nhiều thao tác hàng loạt, nhưng bạn có thể lấy một hoặc nhiều user với mỗi API request. Việc chia thiết kế API thành các hoạt động đọc và ghi dẫn đến kiến trúc này:
Sự gắn kết của code cho các thao tác ghi (create, update và delete) có lợi ích vì nhiều lý do. Ví dụ, bạn có thể cần xác thực request body, đảm bảo nó chứa tất cả những parameter bắt buộc. Nếu workload viết nhiều hơn, những thao tác ít sử dụng như delete hưởng lợi từ môi trường thực thi nóng. Việc đặt mã code cùng một vị trí cho phép tái sử dụng mã trên các hành động tương tự, giảm tải tư duy khi cấu trúc dự án của bạn với thư viện chia sẻ hoặc các Lambda Layer.
Khi nhìn vào phía thao tác đọc, bạn có thể cắt giảm code đi kèm với function này, có cold start nhanh hơn và tối ưu hóa hiệu suất rất nhiều so với thao tác ghi. Bạn cũng có thể lưu một 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ếp tục với bản chất tiến hóa của nó. Tưởng tượng răng platform này trở nên phổ biến hơn. Bây giờ, bạn phải tối ưu API hơn nữa bằng cách cải thiện hiệu năng đọc và thêm vào cache aside pattern với ElastiCache and Redis. Hơn nữa, bạn đã quyết định tối ưu hóa các truy vấn đọc bằng cơ sở dữ liệu thứ hai được tối ưu hóa cho khả năng đọc khi thiếu bộ đệm.
Về mặt viết, bạn đã đồng ý với người tiêu dùng API rằng việc nhận và thừa nhận việc tạo hoặc xóa người dùng là phù hợp, 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 thao tác ghi bằng cách thêm vào SQS queue trước Lambda function. Bạn có thể cập nhật cơ sở dữ liệu ghi theo đợt để giảm số lượng lệnh gọi cần thiết để xử lý các thao tác ghi thay vì xử lý từng yêu cầu riêng lẻ.
Command query responsibility segregation (CQRS) là một pattern được thiết lập tốt để phân tách dữ liệu đột biến hoặc tách biệt thao tác đọc và ghi. Bạn có thể sử dụng CQRS pattern để phân tách updates và queries nếu chúng yêu cầu thông lượng, độ trễ và tính nhất quán khác nhau.
Mặc dù không bắt buộc phải bắt đầu với mẫu CQRS đầy đủ, nhưng bạn có thể phát triển từ cơ sở hạ tầng được đánh dấu dễ dàng hơn trong quá trình triển khai đọc và ghi ban đầu mà không cần phải tái cấu trúc lớn API.
So sánh 3 cách tiếp cận
| Single responsibility | Lambda-lith | Read and write | |
| Lợi ích | Tách biệt các mối bận tâmCấu hình chi tiếtDễ dàng sửa lỗiThời gian thực thi nhanh chóng | Ít lần cold start timeTính gắn kết của Code cao hơnBảo trì dễ dàng | Tính gắn kết code khi cầnKiến trúc có tính tiến hóa caoTối ưu các thao tác đọc ghi |
| Vấn đề | Lặp lại codeBảo trì phức tạpCold start time cao | Corse grain configurationCold start time cao hơn | Sử dụng CQRS với 2 mô hình dữ liệuCQRS làm giảm bớt tính nhất quán của dữ liệu |
Kết luận
Các lập trình viên thường chuyển từ single responsibility qua Lambda-lith khi kiến trúc của họ phát triển, nhưng cả hai cách tiếp cận đều có những đánh đổi của riêng nó. Bài đăng này cho thấy cách tận dụng tối đa cả hai phương pháp bằng cách chia khối lượng công việc của bạn cho mỗi thao tác đọc và ghi.
Cả ba phương pháp đều khả thi để thiết kế serverless API và việc hiểu những gì bạn đang tối ưu hóa là chìa khóa để đưa ra quyết định tốt nhất. Hãy nhớ rằng, việc hiểu ngữ cảnh và các yêu cầu kinh doanh để thể hiện trong ứng dụng sẽ giúp bạn đạt được sự đánh đổi có thể chấp nhận được để chỉ định bên trong một khối lượng công việc cụ thể. Hãy luôn cởi mở và tìm ra giải pháp giải quyết vấn đề, đồng thời cân bằng giữa bảo mật, trải nghiệm của nhà phát triển, chi phí và khả năng bảo trì.
Để có thêm tài liệu học tập serverless, hay truy cập Serverless Land.