微服務架構有別於傳統的單體式應用方案,咱們可將單體應用拆分紅多個核心功能。每一個功能都被稱爲一項服務,能夠單獨構建和部署,這意味着各項服務在工做時不會互相影響javascript
這種設計理念被進一步應用,就變成了無服務(Serverless)。「無服務」看似挺荒唐的,其實服務器依舊存在,只是咱們不須要關注或預置服務器。這讓開發人員的精力更集中——只關注功能實現java
Serverless 的典型即是 AWS Lambdanode
若是你是 Java 開發人員,你應該據說過或使用過 JDK 1.8 裏面的 Lambda,可是 AWS 中的 Lambda 和 JDK 中的 Lambda 沒有任何關係程序員
這裏的 AWS Lambda 就是一種計算服務,無需預置或管理服務器便可運行代碼,藉助 Lambda,咱們幾乎能夠爲任何類型的應用程序或後端服務運行代碼,並且徹底無需管理,咱們要作的只是上傳相應的代碼,Lambda 會處理運行和擴展 HA 代碼所需的一切工做web
說的直白一點sql
Lambda 就比如實現某一個功能的方法 (現實中,一般會讓 Lambda 功能儘量單一),咱們將這個方法作成了一個服務供調用shell
到這裏你可能會有個困惑,Lambda 既然就是一個「方法」,那誰來調用?或怎麼來調用呢?npm
爲了回答上面這個問題,咱們須要登錄到 AWS,打開 Lambda 服務,而後建立一個 Lambda Function (hello-lambda)json
Lambda 既然是個方法,就要選擇相應的 Runtime 環境,以下圖所示,總有一款適合你的(最近在用 Node.js, 這裏就用這個吧)後端
點擊右下角的 Create function 按鈕進入配置頁面
在上圖紅色框線的位置就能夠配置出發 Lambda 的觸發器了,點擊 Add trigger
從上圖能夠看出,AWS 內置的不少服務均可以觸發 Lambda,我在工做中經常使用的有:
上面只是 AWS 內置的一些服務,向下滑動,你會發現,你也能夠配置不少非 AWS 的事件源
到這裏,上面的問題你應該已經有了答案了。這裏暫時先無需任何 trigger,先點擊右上角的 Test 測試一下 Lambda
一個簡單的 Lambda Function 就實現了,紅色框線的 response 只是告訴你們,每一個請求都會有相應的 Request ID,更有 START/END 標識快速定位 Log 內容 (能夠經過 CloudWatch 查看,這裏暫不展開說明)
你也可能已經開始發散你的思惟了,如何運用 AWS Lambda,其實在 AWS 官網有不少樣例:
好比爲了適應多平臺圖片展現,一張原始圖片上傳到 S3 後,會經過 Lambda resize 適應不一樣平臺大小的圖片
好比使用 AWS Lambda 和 Amazon API Gateway 構建後端,以驗證和處理 API 請求,當某一個用戶發佈一條動態,訂閱用戶將收到相應的通知
接下來咱們就用 Lambda 實現經典的分佈式訂單服務案例
爲了加強用戶使用體驗,或者爲了提高程序吞吐量,亦或是爲了架構設計程序解耦,考慮到以上這些狀況,咱們一般都會藉助消息中間件來完成
假設有一常見場景,用戶下訂單時若是選擇開具發票,則須要調用發票服務,很顯然調用發票服務不是程序運行的關鍵路徑,這種場景,咱們就能夠經過消息中間件來解耦。這裏有兩個服務:
若是用 Lambda 來實現兩個服務,總體設計思想就是這樣滴:
現實中,咱們不可能在 AWS console 經過點擊按鈕來建立各個服務的,在 AWS 實際開發中, 咱們經過寫 CloudFormation Template (如下會簡稱 CFT,其實就是一種 YAML 或者 JSON 格式的定義)來建立相關 AWS 服務,若是上述這個 Demo,從圖中能夠看出,咱們要建立的服務仍是很是多的:
若是寫 AWS 原生的 CFT,要實現的內容仍是挺多的
可是...... 懶惰的程序員老是能帶來不少驚喜
寫 JDBC 麻煩,就有了各類持久層框架的出現,一樣寫 AWS 原生 CFT 麻煩,就有了 Serverless Framework (如下會簡稱 SF)的出現幫助咱們定義相關 Serverless 組件 (順便問一下,GraphQL 大家有在用嗎?)
SF 不但簡化了 AWS 原生 CFT 的編寫,還簡化了跨雲服務的定義,就比如設計模式當中的 Facade,在上面創建了一層門面,隱藏了底部不一樣服務的細節,下降了跨雲並用雲的門檻,目前支持的雲服務有下面這些
這裏暫時不會對 SF 展開深刻的說明,在咱們的 demo 中只不過是要應用 SF 來定義
若是你有安裝 Node,那隻須要一條 npm 命令全局安裝便可:
npm update -g serverless
安裝事後檢查一下安裝版本是否成功
sls -version
因爲要使用 AWS 的 Lambda,因此要對 SF 作基本的配置,至少要讓 SF 有權限建立 AWS 服務,當你建立一個 AWS 用戶時,你能夠獲取 AK 「access_key_id」和 SK 「secret_access_key」(不是 SKII 哦),其實就是一種用戶名和密碼形式
而後經過下面一條命令添加配置就能夠了:
serverless config credentials --provider aws --key 1234 --secret 5678 --profile custom-profile
運行上述命令後,就會在 ~/.aws/目錄建立一個名爲 credentials 的文件存儲上述配置,就像這樣:
到這裏準備工做就都完成了,開始寫咱們的定義就行了
經過下面一條命令建立 serverless 應用
sls create --template aws-nodejs --path ./demo --name lambda-sqs-lambda
運行上述命令後,進入 demo 目錄就是下面這個結構和內容了
➜ demo tree . ├── handler.js └── serverless.yml 0 directories, 2 files
由於咱們是用 Node.js 來編寫 Serverless 應用,一樣在 demo 目錄下執行下面命令來初始化該目錄,由於咱們後面要用到兩個 npm package
npm init -y
如今的結構是這樣的(其實就多了一個 package.json):
➜ demo tree . ├── handler.js ├── package.json └── serverless.yml 0 directories, 3 files
至此,準備工做都已就緒,接下來就在 serverless.yml 中寫相應的定義就能夠了 (門檻很低:按照相應的 key 寫 YAML 便可,是否是很簡單?),打開 serverless.yml 文件來看一下,瞬間懵逼?
# Welcome to Serverless! # # This file is the main config file for your service. # It's very minimal at this point and uses default values. # You can always add more config options for more control. # We've included some commented out config examples here. # Just uncomment any of them to get that config option. # # For full config options, check the docs: # docs.serverless.com # # Happy Coding! service: lambda-sqs-lambda # app and org for use with dashboard.serverless.com #app: your-app-name #org: your-org-name # You can pin your service to only deploy with a specific Serverless version # Check out our docs for more details # frameworkVersion: "=X.X.X" provider: name: aws runtime: nodejs12.x # you can overwrite defaults here # stage: dev # region: us-east-1 # you can add statements to the Lambda function's IAM Role here # iamRoleStatements: # - Effect: "Allow" # Action: # - "s3:ListBucket" # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } # - Effect: "Allow" # Action: # - "s3:PutObject" # Resource: # Fn::Join: # - "" # - - "arn:aws:s3:::" # - "Ref" : "ServerlessDeploymentBucket" # - "/*" # you can define service wide environment variables here # environment: # variable1: value1 # you can add packaging information here #package: # include: # - include-me.js # - include-me-dir/** # exclude: # - exclude-me.js # - exclude-me-dir/** functions: hello: handler: handler.hello # The following are a few example events you can configure # NOTE: Please make sure to change your handler code to work with those events # Check the event documentation for details # events: # - http: # path: users/create # method: get # - websocket: $connect # - s3: ${env:BUCKET} # - schedule: rate(10 minutes) # - sns: greeter-topic # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 # - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx # - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx # - iot: # sql: "SELECT * FROM 'some_topic'" # - cloudwatchEvent: # event: # source: # - "aws.ec2" # detail-type: # - "EC2 Instance State-change Notification" # detail: # state: # - pending # - cloudwatchLog: '/aws/lambda/hello' # - cognitoUserPool: # pool: MyUserPool # trigger: PreSignUp # - alb: # listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ # priority: 1 # conditions: # host: example.com # path: /hello # Define function environment variables here # environment: # variable2: value2 # you can add CloudFormation resource templates here #resources: # Resources: # NewResource: # Type: AWS::S3::Bucket # Properties: # BucketName: my-new-bucket # Outputs: # NewOutput: # Description: "Description for the output" # Value: "Some output value"
乍一看,你可能以爲眼花繚亂,其實這是一個相對完整的 Lambda 配置全集,咱們不須要這麼詳細的內容,不過這個文件做爲咱們的參考
接下來咱們就定義 demo 所須要的一切 (關鍵註釋已經寫在代碼中)
service: name: lambda-sqs-lambda # 定義服務的名稱 provider: name: aws # 雲服務商爲 aws runtime: nodejs12.x # 運行時 node 的版本 region: ap-northeast-1 # 發佈到 northeast region,其實就是東京 region stage: dev # 發佈環境爲 dev iamRoleStatements: # 建立 IAM role,容許 lambda function 向隊列發送消息 - Effect: Allow Action: - sqs:SendMessage Resource: - Fn::GetAtt: [ receiverQueue, Arn ] functions: # 定義兩個 lambda functions order: handler: app/order.checkout # 第一個 lambda function 程序入口是 app 目錄下的 order.js 裏面的 checkout 方法 events: # trigger 觸發器是 API Gateway 的方式,當接收到 /order 的 POST 請求時觸發該 lambda function - http: method: post path: order invoice: handler: app/invoice.generate # 第二個 lambda function 程序入口是 app 目錄下的 invoice.js 裏面的 generate 方法 timeout: 30 events: # trigger 觸發器是 SQS 服務,消息隊列有消息時觸發該 lambda function 消費消息 - sqs: arn: Fn::GetAtt: - receiverQueue - Arn resources: Resources: receiverQueue: # 定義 SQS 服務,也是 Lambda 須要依賴的服務 Type: AWS::SQS::Queue Properties: QueueName: ${self:custom.conf.queueName} # package: # exclude: # - node_modules/** custom: conf: ${file(conf/config.json)} # 引入外部定義的配置變量
config.json 內容僅僅定義了 queue 的名稱,只是爲了說明配置的靈活性
{ "queueName": "receiverQueue" }
由於咱們要模擬訂單的生成,這裏用 UUID 來模擬訂單號,
由於咱們要調用 AWS 服務API,因此要使用 aws-sdk,
因此要安裝這兩個 package (這兩個理由夠充分嗎?)
{ "name": "lambda-sqs-lambda", "version": "1.0.0", "description": "demo for lambda", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "license": "MIT", "dependencies": { "uuid": "^8.1.0" }, "devDependencies": { "aws-sdk": "^2.6.15" } }
接下來,咱們就能夠編寫兩個 Lambda function 的代碼邏輯了
訂單服務很簡單,接收一個下單請求,下單成功後快速返回給用戶,同時將訂單下單成功的消息發送到 SQS 中,供下游發票服務開具發票使用
'use strict'; const config = require('../conf/config.json') const AWS = require('aws-sdk'); const sqs = new AWS.SQS(); const { v4: uuidv4 } = require('uuid'); module.exports.checkout = async (event, context, callback) => { console.log(event) let statusCode = 200 let message if (!event.body) { return { statusCode: 400, body: JSON.stringify({ message: 'No order body was found', }), }; } const region = context.invokedFunctionArn.split(':')[3] const accountId = context.invokedFunctionArn.split(':')[4] const queueName = config['queueName'] // 組裝 SQS 服務的 URL const queueUrl = `https://sqs.${region}.amazonaws.com/${accountId}/${queueName}` const orderId = uuidv4() try { // 調用 SQS 服務 await sqs.sendMessage({ QueueUrl: queueUrl, MessageBody: event.body, MessageAttributes: { orderId: { StringValue: orderId, DataType: 'String', }, }, }).promise(); message = 'Order message is placed in the Queue!'; } catch (error) { console.log(error); message = error; statusCode = 500; } // 快速返回訂單 ID return { statusCode, body: JSON.stringify({ message, orderId, }), }; };
發票服務邏輯一樣很簡單,消費 SQS 指定隊列中的消息,並將開具出的發票發送到客戶訂單信息的 email 中
module.exports.generate = (event, context, callback) => { console.log(event) try { for (const record of event.Records) { const messageAttributes = record.messageAttributes; console.log('OrderId is --> ', messageAttributes.orderId.stringValue); console.log('Message Body --> ', record.body); const reqBody = JSON.parse(record.body) // 睡眠 20 秒,模擬生成發票的耗時過程 setTimeout( () => { console.log("Receipt is generated and sent to :" + reqBody.email) }, 20000) } } catch (error) { console.log(error); } }
到此 demo 的代碼就所有實現了,從中你能夠看到:
咱們沒有關注 lambda 的底層服務細節,沒有關注 sqs 的服務,只是簡單的代碼邏輯實現以及服務之間的串聯定義
最後咱們看一下總體的目錄結構吧:
. ├── app │ ├── invoice.js │ └── order.js ├── conf │ └── config.json ├── package.json └── serverless.yml 2 directories, 5 files
在發佈以前,編譯一下應用,安裝必須的 package「uuid 和 aws-sdk」
npm install
發佈應用很是簡單,只須要一條命令:
sls deploy -v
運行上述命令後大概須要等帶幾十秒鐘, 在構建的最後,會打印出咱們的構建服務信息:
上圖的 endpoints 就是咱們一會要訪問的 API gateway 觸發 lambda 的入口,在調用以前,咱們先到 AWS console 看一下咱們定義的服務
從上圖的構建信息中你應該還看到一個 S3 bucket 的名稱,咱們並無建立 S3, 這是 SF 自動幫咱們建立,用來存儲 lambda zip package 的
調用 API gateway 的 endpoint 來測試 lambda
打開 SQS 服務,你會發現,接收到一條消息:
接下來咱們看看 Invoice Lambda function 的消費狀況,打開 CloudWatch 查看 log:
從 log 中能夠看出程序「耗費」 20 秒後打印了向客戶郵件的 log(郵件也能夠藉助 AWS SES 郵件服務來實現)
至此,一個完整的 demo 就完成了,實際編寫的代碼並無多少,就搞定了這麼緊密的串聯
Lambda 是按照調用次數進行收取費用的,爲了防止形成額外的開銷,demo 結束後一般都會將服務銷燬,使用 SF 銷燬剛剛建立的服務也很是簡單,只須要在 serverless.yml 文件目錄執行這條命令:
sls remove
AWS Lambda 是 Serverless 的典型,藉助 Lambda 能夠實現更小粒度的「服務」,無需服務搭建也加快了開發速度。Lambda 一樣能夠結合 AWS 不少其服務,接收請求,將計算結果傳遞給下游服務等。另外不少第三方合做夥伴也在加入 Lambda 的 trigger 大部隊,給 Lambda 更多觸發可能,同時,藉助 CI/CD,能夠快速實現功能閉環
開通 AWS free tier,足夠你玩轉 Lambda