当我们通过Serverless架构,建立了一个函数,并输出了Hello World之后,表明我们已经完成了Serverless架构的“初体验”,接下来,我们可以以其中一个云厂商为例(例如阿里云),进行基础的小工具开发和建设。
在我们日常的生产中,需要获取客户端外网IP是一个非常常见的需求,但是在客户端直接获取外网IP实际上是一个比较困难的事情,这个时候就会有一个简单的方法:在服务端开一个接口来做这个事情,即当客户端请求该接口之后,该接口返回其IP地址。
如果我们选择用Python语言来开发这个项目,在传统的项目中,这个看似简单的应用,实际上并不简单,它通常需要我们有:
这里面包括Nginx,Python的环境,还要有我们所需要的Web Framework的框架(例如Django、Flask、Bottle、Web2py等),除此之外还要有我们的业务逻辑。在项目完成开发,上线之后,我们可能还需要对服务器的健康等进行持续关系和关注,必要时还要做一些高可用方案等。但是在Serverless架构下,这个过程将会变得非常简单:
在整个项目中,我们无需关注Nginx这些服务器软件,无需关注Python等环境的安装配置,更不用关心一些服务器的运维操作,在很多时候也无需关注高可用,我们关心的只是我们的函数代码,至于一些服务器软件全都交给云厂商来搞定,API网管等产品提供类似Nginx的能力,函数计算提供Python的运行环境,至于高可用、极致弹性则是函数计算所必备的“基础能力”。当然,让使用者更关注自身的业务逻辑,这也是Serverless的优势之一。
知识准备
为了更好的完成项目,以阿里云函数计算为例,在开始项目之前,需要进行一些概念的明确,基础知识的储备。
什么是Runtime
所谓的Runtime实际上就是所谓的运行时,可以认为是一种环境或者说是一种支持,例如阿里云函数计算提供了Python2.7的Runtime,可以认为你的Python2.7的应用是可以运行在这个环境下,针对不同的Runtime在文档中都会相关的描述,以Python相关的Runtime为例,在官网上,我们可以看到相关Runtime的描述信息,包括日志的输出方法,所支持的编程语言及版本,所运行的系统类型及版本,以及默认集成的工具/模块/依赖等。
什么是触发器
众所周知,函数计算是通过事件进行触发的,触发器是触发函数执行的方式。在事件驱动的计算模型中,事件源是事件的生产者,函数是事件的处理者,而触发器提供了一种集中、统一的方式来管理不同的事件源。在事件源中,当事件发生时,如果满足触发器定义的规则,事件源会自动调用触发器所对应的函数。
当然不同的事件源和函数进行一个事件的数据结构的规约,当事件源因为某些规则触发了函数,那么这个预先规约好的数据结构,将会作为参数之一传递给函数,例如阿里云对象存储与函数计算规约的事件数据结构:
{
"events": [
{
"eventName": "ObjectCreated:PutObject",
"eventSource": "acs:oss",
"eventTime": "2017-04-21T12:46:37.000Z",
"eventVersion": "1.0",
"oss": {
"bucket": {
"arn": "acs:oss:cn-shanghai:123456789:bucketname",
"name": "testbucket",
"ownerIdentity": "123456789",
"virtualBucket": ""
},
"object": {
"deltaSize": 122539,
"eTag": "688A7BF4F233DC9C88A80BF985AB7329",
"key": "image/a.jpg",
"size": 122539
},
"ossSchemaVersion": "1.0",
"ruleId": "9adac8e253828f4f7c0466d941fa3db81161****"
},
"region": "cn-shanghai",
"requestParameters": {
"sourceIPAddress": "140.205.***.***"
},
"responseElements": {
"requestId": "58F9FF2D3DF792092E12044C"
},
"userIdentity": {
"principalId": "123456789"
}
}
]
}
当用户的函数设置了OSS触发器,并绑定了某个对象存储的存储桶,当这个存储桶满足绑定操作时,即会生成一个事件,并触发函数。例如,我们可以为我们的函数计算设置一个OSS触发器,绑定存储桶“MyServerlessBook”(这个存储桶通常需要和用户的函数在同一个账号下,同一个地域下),设置了一个触发条件“oss:ObjectCreated:PutObject”(调用PutObject接口上传文件即会触发该函数),所以一旦该存储桶收到以PutObject接口上传的文件,就会按照之前规约好的数据结构生成一个事件,触发当前函数并将事件作为参数传递给函数的对方方法。
什么是函数入口
在我们学习C语言的时候,都会会知道一个叫main()的函数,main()函数称之为主函数,一个C程序总是从main()函数开始执行的,例如:
#include <stdio.h>
int main(void)
{
printf("HelloWorld!\n");
return 0;
}
其实在函数计算中也是这样,在我们创建函数的时候,我们也需要告知系统,我们的入口方法是什么。通常情况下,函数入口的格式为[文件名].[函数名]
。以Python为例,创建函数时指定的Handler为index.handler,那么函数计算会去加载index.py中定义的handler函数。通常情况下一个函数计算的入口方法会有两个参数:
def handler(event, context):
return 'hello world'
一个是event,一个是context:
event: 是用户自定义的函数入参,以字节流的形式传给函数,数据结构由您自行定义,它可以是一个简单的字符串 、一个JSON对象、一张图片(二进制数据)。函数计算不对event参数的内容进行任何解释。
对于不同的函数触发情况,event参数的值会有以下区别:
- 事件源服务触发函数时,事件源服务会将事件以一种平台预定义的格式作为event参数传给函数,您可以根据此格式编写代码并从event参数中获取信息。例如使用OSS触发器触发函数时会将Bucket及文件的具体信息以JSON格式传递给event参数。
- 函数通过SDK直接调用时,您可以在调用方和函数代码之间自定义event参数。调用方按照定义好的格式传入数据,函数代码按格式获取数据。例如定义一个JSON类型的数据结构
{"key":"val"}
作为event
,当调用方传入数据{"key":"val"}
时,函数代码先将字节流转换成JSON,再通过event["key"]
来获得值val
。
context: context参数是函数计算平台定义的函数入参,它的数据结构由函数计算设计,包含函数运行时的信息,使用场景通常有两种,一种是用户的临时密钥信息可以通过context.credentials
获取,通过context
中的临时密钥去访问阿里云的其他服务(使用示例中以访问OSS为例),避免了在代码中使用密钥硬编码。另一种是在context
中可以获取本次执行的基本信息,例如requestId、serviceName、functionName、qualifier等。在阿里云函数计算中,关于context的结构基本如下:
{
requestId: '9cda63c3-1ac9-45ba-8a59-2593bb9bc101',
credentials: {
accessKeyId: 'xxx',
accessKeySecret: 'xxx',
securityToken: 'xxx'
},
function: {
name: 'xxx',
handler: 'index.handler',
memory: 512,
timeout: 60,
initializer: 'index.initializer',
initializationTimeout: 10
},
service: {
name: 'xxx',
logProject: 'xxx',
logStore: 'xxx',
qualifier: 'xxx',
versionId: 'xxx'
},
region: 'xxx',
accountId: 'xxx'
}
当然,在上一章中,细心的读者应该已经发现,阿里云的函数计算相对于其他云厂商的函数计算,在创建函数的时候多了一个选项:除了事件函数,还有一个选择叫HTTP函数。与普通的事件函数不同的是,HTTP函数更适合快速构建Web服务等场景。HTTP触发器支持HEAD、POST、PUT、GET和DELETE方式触发函数,同时与普通的事件函数不同的是,HTTP函数的入参和Response也略微不同,以官方例子为例:
# -*- coding: utf-8 -*-
import json
HELLO_WORLD = b"Hello world!\n"
def handler(environ, start_response):
request_uri = environ['fc.request_uri']
response_body = {
'uri':environ['fc.request_uri'],
'method':environ['REQUEST_METHOD']
}
# do something here
status = '200 OK'
response_headers = [('Content-type', 'text/json')]
start_response(status, response_headers)
# Python2
return [json.dumps(response_body)]
# Python3 tips: When using Python3, the str and bytes types cannot be mixed.
# Use str.encode() to go from str to bytes
# return [json.dumps(response_body).encode()]
当然,HTTP函数的一个优势是更加容易与传统的Web框架进行结合,以Python的轻量级Web框架Flask为例:
# index.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
此时,我们只需要将函数的入口方法设置为:index.app即可实现一个Flask项目运行在函数计算上。这个过程相对于很多在函数计算层面将JSON对象转换成Request对象的方案要方便得多。当然除了这一点之外,HTTP函数的优势还有:
- 简化了开发人员的学习成本和调试过程,帮助开发人员快速使用函数计算搭建Web service和API。
- 支持选择熟悉的HTTP测试工具验证函数计算侧的功能和性能。
- 减少请求处理环节,HTTP触发器支持更高效的请求、响应格式,不需要编码或解码成JSON格式,性能更优。
- 方便对接其他支持Webhook回调的服务,例如CDN回源、MNS等。
项目开发
原生FaaS开发
想要获得用户的IP地址,那么我们就要根据函数计算的特性,寻找到事件的数据结构,在这个数据结构中找到客户端IP的字段。我们可以先创建一个HTTP函数,输出environ:
# -*- coding: utf-8 -*-
import json
def handler(environ, start_response):
print(environ)
response_body = {}
response_headers = [('Content-type', 'text/json')]
start_response('200 OK', response_headers)
return [json.dumps(response_body).encode()]
完成之后,我们执行该函数,可以看到输出的日志中有REMOTE_ADDR字段,存放的是客户端的IP地址:
所以此刻,我们可以通过environ['REMOTE_ADDR']来获得这个IP地址:
# -*- coding: utf-8 -*-
import json
def handler(environ, start_response):
response_body = {
'IP':environ['REMOTE_ADDR']
}
response_headers = [('Content-type', 'text/json')]
start_response('200 OK', response_headers)
return [json.dumps(response_body).encode()]
测试代码,可以看到已经正确输出我们的IP地址:
通过函数计算为我们生成的地址:
在客户端用命令行工具测试:
基于Web框架开发
首先在本地创建项目一个Flask项目,并且新建文件index.py:
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
return {"IP": request.remote_addr}
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=int("8001")
)
安装项目所需要的依赖到当前项目下,例如本项目只需要一个flask即可:
pip3 install flask -t ./
安装完成之后,我们可以本地启动该项目进行基本测试:
此时,我们在阿里云函数计算上创建一个HTTP函数,选择我们刚才的项目文件夹上传,同时函数入口要改成index.app:
然后可以点击“新建”按钮,进行函数创建,创建完成之后,我们可以在控制台点击一下“测试”按钮,可以看到,已经成功输出我们的IP地址:
当然,我们也可以使用默认生成URL,在本地进行测试:
此时结果:
举一反三
AWS Lambda
上文是以阿里云函数计算为例,当然在其他云厂商中实现该服务的方法也是类似的,例如在AWS的Lambda中,代码可以为:
import json
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': json.dumps({"IP": event['requestContext']['http']['sourceIp']})
}
腾讯云云函数
腾讯云云函数中,代码为:
# -*- coding: utf8 -*-
import json
def main_handler(event, context):
response_body = {
'IP': event['requestContext']['sourceIp']
}
return response_body
华为云函数工作流
华为云的可以是:
# -*- coding:utf-8 -*-
import json
def handler (event, context):
return {
"statusCode": 200,
"isBase64Encoded": False,
"body": json.dumps({"IP": event['headers']['x-real-ip']}),
"headers": {
"Content-Type": "application/json"
}
}
至此我们完成了一个简单的查询客户端IP的API服务的开发。相对于传统的自建服务器,安装各种软件,后期不断运维而言,Serverless架构的优势非常明显,它让我们仅关注自身的业务代码即可,无论是运行环境还是API网关等,都交给云厂商来统一管理和维护,这对研发效率的提升,人力资源投入的降低,是有巨大帮助和推进作用的。除此之外,传统云主机情况下,我们需要让我们的机器一直在运行,哪怕很长一段时间没有流量也要持续运行,而只要运行就会有费用的产生,就会有成本的产生,但是在Serverless架构下,我们即便开发、测试完项目,他也会处于一个静默状态,只有当请求到来的时候,函数被触发的时候,系统才会进行计费,也就是说只有使用才有费用,不使用无费用产生,这对于整个资源成本的压缩,具有极大的帮助和促进作用。