前言
微服務架構有別於傳統的單體式應用方案,我們可將單體應用拆分成多個核心功能。每個功能都被稱為一項服務,可以單獨構建和部署,這意味著各項服務在工作時不會互相影響
這種設計理念被進一步應用,就變成了無服務(Serverless)。「無服務」看似挺荒唐的,其實伺服器依舊存在,只是我們不需要關注或預置伺服器。這讓開發人員的精力更集中——只關注功能實現
Serverless 的典型便是 AWS Lambda
AWS Lambda
如果你是 Java 開發人員,你應該聽說過或使用過 JDK 1.8 裡面的 Lambda,但是 AWS 中的 Lambda 和 JDK 中的 Lambda 沒有任何關係
這裡的 AWS Lambda 就是一種計算服務,無需預置或管理伺服器即可執行程式碼,藉助 Lambda,我們幾乎可以為任何型別的應用程式或後端服務執行程式碼,而且完全無需管理,我們要做的只是上傳相應的程式碼,Lambda 會處理執行和擴充套件 HA 程式碼所需的一切工作
說的直白一點
Lambda 就好比實現某一個功能的方法 (現實中,通常會讓 Lambda 功能儘可能單一),我們將這個方法做成了一個服務供呼叫
到這裡你可能會有個困惑,Lambda 既然就是一個「方法」,那誰來呼叫?或怎麼來呼叫呢?
如何呼叫 Lambda
為了回答上面這個問題,我們需要登陸到 AWS,開啟 Lambda 服務,然後建立一個 Lambda Function (hello-lambda)
Lambda 既然是個方法,就要選擇相應的 Runtime 環境,如下圖所示,總有一款適合你的(最近在用 Node.js, 這裡就用這個吧)
點選右下角的 Create function 按鈕進入配置頁面
在上圖紅色框線的位置就可以配置出發 Lambda 的觸發器了,點選 Add trigger
從上圖可以看出,AWS 內建的很多服務都可以觸發 Lambda,我在工作中常用的有:
- API Gateway (一會的 demo 會用到,也是最常見的呼叫方式)
- ALB - Application Loac Balancer
- CloudFront
- DynamoDB
- S3
- SNS - Simple Notification Service
- SQS - Simple Queue Service
上面只是 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 實現經典的分散式訂單服務案例
訂單服務 Demo
為了增強使用者使用體驗,或者為了提升程式吞吐量,亦或是為了架構設計程式解耦,考慮到以上這些情況,我們通常都會藉助訊息中介軟體來完成
假設有一常見場景,使用者下訂單時如果選擇開具發票,則需要呼叫發票服務,很顯然呼叫發票服務不是程式執行的關鍵路徑,這種場景,我們就可以通過訊息中介軟體來解耦。這裡有兩個服務:
- 訂單服務
- 發票服務
如果用 Lambda 來實現兩個服務,整體設計思想就是這樣滴:
現實中,我們不可能在 AWS console 通過點選按鈕來建立各個服務的,在 AWS 實際開發中, 我們通過寫 CloudFormation Template (以下會簡稱 CFT,其實就是一種 YAML 或者 JSON 格式的定義)來建立相關 AWS 服務,如果上述這個 Demo,從圖中可以看出,我們要建立的服務還是非常多的:
- Lambda * 2
- API Gateway
- SQS
如果寫 AWS 原生的 CFT,要實現的內容還是挺多的
但是...... 懶惰的程式設計師總是能帶來很多驚喜
Serverless Framework
寫 JDBC 麻煩,就有了各種持久層框架的出現,同樣寫 AWS 原生 CFT 麻煩,就有了 Serverless Framework (以下會簡稱 SF)的出現幫助我們定義相關 Serverless 元件 (順便問一下,GraphQL 你們有在用嗎?)
SF 不但簡化了 AWS 原生 CFT 的編寫,還簡化了跨雲服務的定義,就好比設計模式當中的 Facade,在上面建立了一層門面,隱藏了底部不同服務的細節,降低了跨雲並用雲的門檻,目前支援的雲服務有下面這些
這裡暫時不會對 SF 展開深入的說明,在我們的 demo 中只不過是要應用 SF 來定義
安裝 Serverless Framework
如果你有安裝 Node,那隻需要一條 npm 命令全域性安裝即可:
npm update -g serverless
安裝過後檢查一下安裝版本是否成功
sls -version
配置 Serverless Framework
由於要使用 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
- --provider 雲服務商
- --key 你的AK
- --secret 你的SK
- --profile 如果你有多個賬戶時,你可以新增這個 profile 做快速區分
執行上述命令後,就會在 ~/.aws/目錄建立一個名為 credentials 的檔案儲存上述配置,就像這樣:
到這裡準備工作就都完成了,開始寫我們的定義就好了
建立 Serverless 應用
通過下面一條命令建立 serverless 應用
sls create --template aws-nodejs --path ./demo --name lambda-sqs-lambda
- --template 指定建立的模版
- --path 指定建立的目錄
- --name 指定建立的服務名稱
執行上述命令後,進入 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 的程式碼邏輯了
Order 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,
}),
};
};
Invoice Lambda Function
發票服務邏輯同樣很簡單,消費 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
釋出 Lambda 應用
在釋出之前,編譯一下應用,安裝必須的 package「uuid 和 aws-sdk」
npm install
釋出應用非常簡單,只需要一條命令:
sls deploy -v
執行上述命令後大概需要等帶幾十秒鐘, 在構建的最後,會列印出我們的構建服務資訊:
上圖的 endpoints 就是我們一會要訪問的 API gateway 觸發 lambda 的入口,在呼叫之前,我們先到 AWS console 看一下我們定義的服務
lambda functions
SQS-receverQueue
API Gateway
S3
從上圖的構建資訊中你應該還看到一個 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 : https://dayarch.top/p/aws-lambda-with-serverless-framework.html
個人部落格:https://dayarch.top
加我微信好友, 進群娛樂學習交流,備註「進群」
歡迎持續關注公眾號:「日拱一兵」
- 前沿 Java 技術乾貨分享
- 高效工具彙總 | 回覆「工具」
- 面試問題分析與解答
- 技術資料領取 | 回覆「資料」
以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本著將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......