AWS Lambda的各種優秀實踐
譯文【51CTO.com快譯】概述
如今,無服務器已經成為了各種云應用的最常見部署模式。而在這個領域中,AWS Lambda可謂最為“骨灰級”的工具了。大多數開發人員都或多或少地有過,運用Lambda來快速構建并運行某個云端函數代碼的經歷。
然而,AWS在管理和處理可擴展性、高可用性(HA)、安全性、以及性能等方面,卻不像AI機器人那樣,通過自我學習和優化配置,來改進所有的云原生(cloud-native)指標。因此,開發人員需要在設計時,尤其注意并學習如何在成本和性能之間達到平衡。在本文中,我將分享:如何通過了解Lambda的工作原理,來合理并充分地使用Lambda。
高可用性
當我們在運行某個Lambda函數時,它實際上是默認運行在一個可以訪問外網的VPC(Virtual Private Cloud)之上。不過,它卻無法同時訪問到其他任何私有的VPC。此處所謂“訪問外網”是指:它只能訪問S3和DynamoDB的AWS服務;而對于那些運行在其他VPC下的AWS資源(如:RDS,Elasticsearch等),則無法訪問到。
如果某個函數運行在Lambda所管理的VPC上,那么Lambda將負責它在該VPC區域的多個AZ(Availability Zone)中的可用性。但在大多數企業應用場景中,我們的確需要同時訪問到RDS和其他的VPC資源。因此,我們需要確保如下兩個方面:
- 通過在不同的AZ中選擇多個子網,來設計Lambda、并實現高可用性。
- 如果某個AZ發生故障,則其他AZ需要被分配足夠的IP地址,來處理并發的Lambda請求。(注意,每個Lambda的執行都需要有一個私有的IP地址,來處理請求。)因此,我們需要在子網中分配足夠多的IP地址,以達到HA。
并發性
雖說AWS Lambda會以自己的方式來實現可伸縮性,但是對于有限的資源而言,Lambda會遵循如下的并發執行限制:
- 帳戶級別 - 默認情況下,它會參照每個區域內的所有函數,將該值定為1000。
- 函數級別 - 默認情況下,它會使用“Unreserved Account Concurrency limit”,但是這并不是一種很好的實現方式。為了避免耗盡所有帳戶級別的并發數,它會限制其他的功能函數。因此,我們應該為每一個函數保留單獨的并發數,以便在事件數量因為某種原因出現激增時,僅影響并隔離在該函數之中。
注意 - AWS始終保留一個具有至少100個并發執行量的無保留并發池(unreserved concurrency pool),以處理那些未做特殊設置的函數請求。也就是說,您最多只能分配900個。
如果我們是在一個專有的VPC上運行Lambda呢?
在這種情況下,我們需要根據函數的ENI(Elastic Network Interfaces,彈性網絡接口)擴展性,去請求足夠多的IP地址。您可以使用如下公式來估算ENI的近似容量:
- Concurrent executions * (Memory in GB / 3 GB)
其中:
- 發執行 - 是工作負載的預計并發數(用每秒調用次數*平均執行的時長,以秒為單位)。
- 內存大小 - 是為Lambda函數配置的內存數量(以GB為單位)。
在設計Lambda的并發性時,我們還應該始終考慮,諸如DynamoDB、RDS等其他集成服務的限制。我們需要根據這些服務能夠處理的***連接,來調整函數的并發限制。
節流
正如前文在“并發性”中提到的,一旦函數事件出現激增,并超過了并發數的限制,那么Lambda將無法再處理任何新的請求。如果我們不及時予以處理的話,業務系統就會受到影響。
- 如果Lambda的調用是同步模式的話,它會馬上接收到429類型的錯誤代碼。同時,如果節流被設定為函數級別或是帳戶級別,那么它還能接收其他一些信息。因此,諸如API網關之類的調用服務,則需要處理此類重試問題。
- 如果Lambda的調用是異步模式的話,Lambda只會在丟棄事件之前嘗試兩次。因此,如果函數無法處理該事件,我們就應該使用SQS或SNS所定義的DLQ(Dead Letter Queue),來做稍后調試與處理。而如果我們忘記了定義DLQ,那些消息則會被直接丟棄掉。
- 如果Lambda調用是基于輪詢模式的話,我們進一步細分兩種情況:
- 如果是流式(Kinesis),它將繼續重試,直到超時(最多為7天)。
- 如果是非流式(SQS),它將把消息放回到隊列之中,并僅在Visibility時限到期后才開始重試,并持續執行下去,直到它能夠成功地完成處理、或是超過保留期。
內存與成本之間的平衡
在Lambda里,內存和CPU息息相關,也就是說,如果您增加了內存,那么CPU分配也應該有所增加。因此,如果需要減少Lambda的執行時間,那么我們就應當增加內存和CPU。但是,如果您進行過詳細的實驗就會發現:在一定的限制情況下,單憑增加內存只會增加購置成本,而并不會大幅減少執行的時間。
目前市面上很少有開源的工具,能夠幫助我們找到***的資源配置。我個人傾向使用CloudWatch的各種日志,來監控內存的使用情況和執行時間,進而調整相應的配置。對內存進行參數微調,便可對AWS的整體成本產生較大的影響,我籍此來找到***平衡點。
性能 - 冷啟動與熱啟動
當我們***次調用Lambda時,它會從S3那里下載代碼和所有依賴項,以創建容器,并在執行代碼之前先啟動對應的應用程序。整個過程的耗時(代碼的執行除外)被稱為冷啟動時間。而一旦容器被啟動并運行起來后,Lambda就已經為后續的調用完成了初始化,它只需要執行應用程序的邏輯便可。因此,這段時長就被稱為熱啟動時間。
那么問題來了,我們應該縮短冷啟動時間、還是熱啟動時間呢?原則上說,作為完整執行時長的一部分,冷啟動占據了大部分時間,因此需要想辦法予以減少。但是,在實踐中,我們卻可以通過優質的代碼,來減少熱啟動的時間。
下面,讓我們討論如何才能提高Lambda的整體性能:
- 選擇諸如Nodejs和Python之類的解釋性語言,而不是Java或C ++,來減少冷啟動時間。
- 如果出于某種原因不得不選用Java的話,請使用Spring Cloud Functions,而不是Spring Boot Web框架。
- 由于我們設置ENI的耗時較長、并且會增加冷啟動時間,因此除非您需要帶有專有IP地址的VPC資源,否則請使用默認的網絡環境。我個人判讀:隨著新版AWS Lambda的即將發布,此方面應該有所改進。
- 刪除所有與運行該函數無關的依賴項。僅保留那些必需的。
- 使用各種全局/靜態變量、以及Singleton對象,這些變量能夠在容器發生故障之前,一直保持活動狀態。因此,任何后續調用都不必再重新初始化這些變量與對象了。
- 請使用全局定義的數據庫連接,以便它們能夠被重用到后續的調用中。
- 如果您選用的是Java,那么請使用諸如Dagger和Guice之類的簡單IoC依賴注入,而不是Spring框架。
- 同樣,如果您選用了Java,那么請將依賴項.jar文件與函數的代碼相分離,以便對解包程序加速。
- 如果您選用的是Nodejs,請控制Function js文件的體積小于600字符,并使用V8的運行環境(runtime)。V8優化器能夠內聯主體小于600字符(包括各種注釋)的函數。
- 同樣,如果您選用了Nodejs,則可以使用代碼的minification和/或uglification,來減小包的大小,進而大幅減少下載包的耗時。在某些情況下,我曾經看到有將包的體積從10MB減少到1MB的案例。
- Minification – 會刪除掉所有的空格、換行符、以及注釋。
- Uglification – 會對所有變量進行混淆和簡化。
示例,原代碼:
- var organizationname = “xyz”
- var bigArray = [1,2,3,4,5,6]
- //write some code
- for(var index = 0; index < 6; index++){
- console.log(bigArray[index]);
- }
Minification之后:
- var organizationname = “xyz”, bigArray = [1,2,3,4,5,6] for(var index = 0; index < 6; index++) console.log(bigArray[index]);
Uglification之后:
- for(var o=”myname”,a=[1,2,3,4,5,6],e=0;e<6;e++)console.log(a[e])
網上有不少的文章都提到:Lambda的執行環境已經具有了適用于Nodejs和Python的AWS SDK。因此,我們不必在依賴項中添加它們。此特性雖然有利于提高性能,但是潛藏著一個問題:該SDK庫將定期使用***的修補程序來進行升級,為了不影響Lambda的各種行為,您***采用自己的依賴項管理方式。
安全性
- 為每個函數分配一個IAM角色。即使有多個函數需要相同的IAM策略,單個IAM角色也應該只映射一個函數。當特定的函數安全策略需要加固時,這將有助于保持最小權限的策略。
- 由于Lambda會在共享的VPC上運行,因此將AWS的憑據保留在代碼中并不可取。
- 在大多數情況下,IAM的執行角色已足以通過使用AWS SDK,去連接到AWS的各種服務。
- 如果函數需要調用跨帳戶的服務,則可能會使用到不同的憑據。因此我們需要在AWS的Security Token Service(請參見https://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html)中使用Assume Role API,并檢索各種臨時憑據。
- 如果函數需要存儲長期憑據(如DB憑據、訪問密鑰),請使用帶有加密助手或AWS System Manager的環境變量。
可測性
由于AWS Lambda讓用戶的代碼運行在云端,那么我們該如何在本地進行測試呢?
雖然Lambda并不提供任何直接測試的URL,但是我們可以根據要啟動的事件源系統來開展測試。
- 我們可以使用AWS SAM(請參見https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html)進行Lambda函數的本地測試。它為CLI提供了類似本地Lambda的執行環境。我們可以獲得API Gateway的localhost URL,它會在本地調用Lambda函數。
- 我們可以使用localstack(請參見https://github.com/localstack/localstack)開源項目,來創建具有大量AWS資源/服務的本地環境。它可以與其他AWS服務一起運行Lambda。而且由于它能夠以API的形式提供所有的服務,并能夠在后端作為Docker容器運行,因此您也可以將AWS SAM與localstack相集成。
- 將業務邏輯放到Lambda Handler之外。Handler函數應當僅用于檢索各項輸入,然后將它們傳遞給其他函數/方法。這些函數/方法會將它們解析成為與我們的應用程序相關的變量,然后進一步使用。該過程不但實現了將業務邏輯與處理程序相分離,而且可以在我們創建的對象和函數的上下文中進行測試。
藍/綠部署
通過Lambda附帶的Versioning和Alias功能,我們可以發布一個函數的多個版本。同時,我們可以在單獨的容器中并行地調用每個版本。默認情況下,版本特征是由$LATEST來表示的。在開發的過程中,我們可以使用這些版本,來創建諸如dev/UAT等多個環境。但是,由于我們在每一次上傳新的代碼時,版本都會遞增,而客戶則會被指向***的版本。因此,我們***不要直接將Versioning用于生產環境,而可以用到Alias。
Alias可以指向函數的某個特定版本。因此,如果您的代碼發生了更改,并且發布了更新的版本時,事件源仍將指向原來相同的Alias。我們只需管理好Alias何時需要被指向新的版本便可。這便實現了藍/綠部署。我們可以使用一些樣本事件來測試新的版本,確認其工作正常之后,再通過修改Alias的指向,來切換訪問的流量。與此同時,如果發現出現任何問題,我們還可以迅速回滾到原始的版本上。
監控
個人以為:CloudWatch能夠很好地與Lambda配合使用,并為用戶提供Lambda執行的各種詳細信息。Lambda能夠自動跟蹤請求數、每個請求的執行時間、導致錯誤的請求數、以及發布相關的CloudWatch指標。同時,您也可以利用這些指標,來自定義各種CloudWatch的警報功能。
另外,我們還可以使用X-Ray來識別Lambda執行中的各種潛在瓶頸。它對于我們試圖可視化那些耗費在函數執行上的時間,是非常實用的。而且,X-Ray還有助于跟蹤那些與整個流程相連接的所有下游系統。
其他建議
- 請勿使用AWS Lambda Console開發那些直接被用在生產環境中的代碼。
- 由于代碼版本控制并非自動生效的,因此如果您誤點了Save按鈕,那么生產環境中的工作代碼就會被***覆蓋掉。
- 它并沒有與GitHub、或其他代碼存儲庫相集成。
- 它無法被導入AWS SDK之外的模塊中。因此,如果您需要某種特定的庫,則必須從一開始就在本地開發自己的函數、創建.zip文件、然后將其上傳到AWS Lambda中。
- 請使用AWS SAM或無服務器框架來進行開發。
- 就Lambda部署的CI/CD計劃而言,它其實與其他可交付式的計劃并無不同。
- 可使用各種環境變量(Environment Variables)和參數存儲(Parameter Store),來將代碼與配置相分離。
總結
在本文中,我們討論了在設計和部署Lambda時,各種值得參考和使用的***實踐。我們可以根據實際應用的編碼語言和用例,來不斷改進業務系統的性能。當然,我們也可以在其他的云平臺,以及Kubernetes的無服務器平臺中借鑒這些***實踐。希望您能夠將這些實踐總結運用到自己成熟的生產環境與應用之中。
原文標題:AWS Lambda Best Practices,作者:Rajesh Bhojwani
【51CTO譯稿,合作站點轉載請注明原文譯者和出處為51CTO.com】