2.4.2 如何对Serverless应用进行调试
在应用开发过程中,或者应用开发完成后,当执行结果不符合预期时,通常要进行一定的调试。但是在Serverless架构下,调试往往会受到极大的考验,尤其在受环境因素限制时,通常会出现这样的情况:所开发的应用在本地可以健康、符合预期地运行,但是在FaaS平台上则有一些不可预测的问题;或者在一些特殊环境下,本地没有办法模拟线上环境,难以进行应用的调试。
图2-39 通过Serverless Devs桌面客户端进行Yaml可视化配置
Serverless应用的调试一直备受诟病,但是各个云厂商并没有因此放弃在调试方向上的深入探索。
1.在线调试
(1)简单调试
所谓的简单调试,就是在控制台进行调试。以阿里云函数计算为例,可以在控制台通过“代码执行”按钮进行基本的调试,如图2-40所示。
图2-40 函数计算代码编辑页面
如图2-41所示,必要的时候也可以通过设置Event来模拟一些事件。
图2-41 阿里云函数计算事件页面
在线调试的好处是可以使用一些线上环境进行代码的测试。当线上环境拥有VPC等资源时,在本地环境是很难进行调试的。
(2)断点调试
除了简单的在线调试之外,部分云厂商还支持断点调试,例如阿里云函数计算的远程调试、腾讯云云函数的远程调试等。以阿里云函数计算远程调试为例,我们可以通过控制台进行函数的在线调试。当创建好函数之后,可以选择远程调试,并单击“开启调试”按钮,如图2-42所示。
图2-42 函数计算远程调试页面
开启调试之后,稍等片刻,系统将会进入远程调试界面,如图2-43所示。
图2-43 函数计算远程调试开始页面
当出现图2-44所示界面,我们可以进行断点调试。
图2-44 函数计算远程调试断点调试页面
2.端云联调
在本地进行Serverless应用开发时,往往会涉及一些线上资源,例如通过对象存储触发器触发函数执行,通过VPC访问数据库等,此时线上和线下环境不一致会让线下开发、调试面临极大的挑战。Serverless Devs开发者工具通过搭建Proxy辅助函数的方法将线上和线下资源打通,可以快速帮助开发者在本地进行应用的开发与调试,这种调试方式称为端云联调。
如图2-45所示,Serverless Devs开发者工具会根据Yaml配置文件,创建辅助服务和辅助函数,并通过辅助服务和辅助函数实现线上和线下资源打通,以及完整的端云联调。
图2-45 Serverless Devs端云联调原理示意图
·Serverless Devs开发者工具会根据Yaml配置文件的内容,创建辅助服务和辅助函数(辅助服务和Yaml中所声明的业务服务配置是一致的)。
·通过触发器(包括通过SDK、API、s proxied invoke命令,或者其他触发器)触发辅助函数(函数计算C),请求流量回到本地调试实例(本地环境A),这时本地调试实例(本地函数执行环境容器)收到的event和context是真实来自线上的。
·本地调试实例(本地环境A)可以直接访问以下内容:
〇VPC内网资源,比如RDS、Kafka内网地址等;
〇一些云服务的内网地址;
〇硬盘挂载服务(直接访问NAS)。
端云联调流程如下:
1)执行s proxied setup命令准备端云联调所需的辅助资源以及本地环境;
2)对于无触发器的普通事件函数或者HTTP触发器,准备工作完成后,启动另一个新的终端,切换到该项目路径下,执行s proxied invoke命令调用本地函数;
3)完成调试任务后,执行s proxied cleanup命令清理端云联调所需的辅助资源以及本地环境。
除了通过命令使用端云联调功能外,我们也可以在VSCode开发者工具中使用端云联调功能,如图2-46所示。
图2-46 在VSCode中使用端云联调功能
3.远程调试
端云联调在本地除了有一个通道服务容器外,还有一个函数计算容器,用来执行本地函数;远程辅助函数只是单纯将远程流量发送到本地。在实际调试过程中,需要登录到实例进行项目调试,此时可以选择使用远程调试。相比于端云联调,远程调试在本地只有一个通道服务容器,执行过程全部依赖线上;远程函数将执行结果返回。远程调试整体架构简图如图2-47所示。
除了通过构建图2-47所示的通道服务登录线上环境进行代码调试或问题定位之外,部分云厂商还提供了直接登录实例进行代码调试的功能。以Serverless Devs为例,当使用阿里云函数计算时,我们就可以直接通过instance命令进行线上实例登录。
尽管实例登录命令已经提供了便捷的登录体验,能帮助用户解决复杂场景下的应用异常定位等问题,但是登录实例后,用户无法直接通过函数日志、监控指标来具体定位问题,还需要借助例如coredump、tcpdump、jmap等工具进行问题的深入排查。
例如,某用户发现自己的线上程序最近出现一些函数错误提示,报错内容都是连接远程某服务时超时。该用户怀疑是函数实例与远端服务的网络连接不稳定,因此想进入实例内部,分析实例与远端服务的网络情况。
图2-47 远程调试架构简图
此时,我们可以按照以下步骤进行问题的排查。
1)如图2-48所示,登录实例内部后,需要执行apt-get update和apt-get install tcpdump两条命令,进行tcpdump工具的安装。
2)安装完毕后,执行tcpdump命令,对远端服务IP的请求进行抓包,并将抓包结果保存在tcpdump.cap文件中。
3)抓包完毕后,借助OSS命令行工具ossutil64,将tcpdump.cap文件上传到自己的OSS,然后下载到本地借助分析工具Wireshark进行分析。
图2-48 实例登录与安装软件效果图
4.本地调试
(1)命令行工具
大部分FaaS平台会为用户提供相对完备的命令行工具,包括AWS的SAM CLI、阿里云的Funcraft,同时也有一些开源项目如Serverless Framework、Serverless Devs等支持多云厂商的FaaS平台。通过命令行工具进行代码调试的方法很简单。以Serverless Devs为例,本地调试阿里云函数计算方法为:首先确保本地拥有一个函数计算的项目,然后在项目下执行调试指令,例如在Docker中进行调试,如图2-49所示。
图2-49 通过命令行进行本地调试
(2)编辑器插件
以VSCode插件为例,下载好阿里云函数计算的VSCode插件,并且配置好账号信息之后,在本地新建函数,并且在打点之后进行断点调试,如图2-50所示。
图2-50 编辑器插件中进行调试
5.其他调试方案
(1)Web框架的本地调试
以Python语言Bottle框架为例,若在阿里云FaaS平台开发传统Web框架,可以增加如下代码:
app = bottle.default_app()
并且对run()方法进行条件限制(if __name__=='__main__'):
if __name__ == '__main__': bottle.run(host='localhost', port=8080, debug=True)
例如:
# index.py import bottle @bottle.route('/hello/<name>') def index(name): return "Hello world" app = bottle.default_app() if __name__ == '__main__': bottle.run(host='localhost', port=8080, debug=True)
和传统开发思路一样,我们可以在本地开发并在本地进行调试。当部署到线上时,只需要在入口方法处设置index.app,即可实现平滑的部署。
(2)本地模拟事件调试
针对非Web框架,可以在本地构建一个方法,例如要调试对象存储触发器,代码如下:
import json def handler(event, context): print(event) def test(): event = { "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" } } ] } handler(json.dumps(event), None) if __name__ == "__main__": print(test())
这样通过构造一个event对象,即可实现模拟事件触发。