Let Everything Be Serverless!
前言
某日在 Github Feeds 上闲逛的时候看到原作者 Giuem 挂在 Github 上的项目 UptimeRobot-Page
自己本来早就有打算自建状态监测页的想法,毕竟之前自用的主要服务的 SLA 十分感人,想要建一个状态监测页以督促自己。
问了一下可爱羊驼,他的目前方案是把 UptimeRobot-Page 以 Docker 形式部署在了 Heroku 上,但是 Heroku 的免费套餐就必须要求再用一个状态监测服务每隔一段时间主动访问一下 UptimeRobot-Page 防止 Heroku 容器因为过长时间 idle 而休眠。
感觉这个操作过为强势想想还是用别的吧,正好最近处于 Serverless 狂热时期,不如就把状态监测页移植到 AWS Lambda 上吧。
小小分析
UptimeRobot-Page 原项目采用的是 Node.js + Koa 框架的模式,启动时先注册一个 Cron 服务每隔 5 分钟定时从 UptimeRobot API 拉取监控数据并缓存在内存中(?),当有访客访问时读取缓存并调用 pugjs 渲染后返回给访客。
这种长期运行类似于 Web server 的架构显然不适用于 Serverless,因此需要小改一下以适配 AWS Lambda。
魔改
有那么几个问题有待处理
1. 原项目依赖的运行环境是 Node.js 10+,而 AWS Lambda 对于 Node.js 的支持版本最高为 Node.js 8
2. 需要将原项目由持续运行模式改为类函数式,以及解决缓存持久化等问题
对于第一个问题已经有很多的解决方案,AWS Lambda 支持自行构建运行环境,非 AWS Lambda 原生支持的语言亦可运行,可参考这一篇 编译 PHP for AWS Lambda
同时 AWS Lambda 在前几个月宣布推出 Layers 功能,以方便我们进行环境部署,这样每次部署源码无需将运行环境一同打包上传,省时省力方便调试。
对于第二个问题,我的想法是设置两个 Lambda 函数,一个每隔十分钟定时运行从 UptimeRobot 拉取数据,并将监测数据通过调用 pugjs 渲染为 HTML 后存入 AWS DynamoDB,后者则充当 Web server 的角色,访客访问监测页面时会通过 AWS API Gateway 调用该 Lambda 函数,此时这个函数将会从 DynamoDB 中取出 HTML 并直接返回给用户。
在经过一个下午的折腾,大致将项目糊出来了
项目地址:MilkiceForks/uptimerobot-page-serverless
挂在 Github 上接受大佬的批评(
部署流程
1. 部署 Node.js 10 运行环境
先从 Node.js 下载界面 下载 Node.js 10 LTS Linux x64 Binaries,下载完之后解压(推荐在 *nix系统下解压),并重新打包为 zip 文档。
转到 AWS 控制台,进入 Lambda,左侧侧边栏有个「层」或者「Layer」,点开,新建层,名称与描述任意,兼容运行时选择 Node 8.10,上传 zip 包,完成
2. 部署 node_modules 环境
如果每次都部署都把 node_modules 打包进去怕不是会烦死
先在本地构建 node_modules 后打包上传
(请确保本地已安装 Node.js 10 运行环境)
git clone https://github.com/MilkiceForks/uptimerobot-page-serverless
cd uptimerobot-page-serverless
yarn install
yarn cache clean
mkdir nodejs
mv node_modules nodejs/
zip -r uptime_modules.zip nodejs/
文件夹下将生成一个 uptime_modules.zip
我们很容易就会发现这个 zip 包大于 50 MB,超出了 Layer 可以直接上传的大小,因此我们需要先上传到 S3 Bucket 再把 URL 复制到 Layer 这里
转到 S3,新建一个桶(Bucket),名称依旧任意,一路默认配置「下一步」完成桶的创建
接着进入这个桶,点击「上传」,选择 uptime_modules.zip 之后直接点击左下角上传
上传完毕之后,选中这个文件,右侧会弹出一个小框,将其中的 URL 地址复制一下
回到 Layer 界面,新建一个层,代码输入种类选择「从 Amazon S3 上传文件」,把刚才复制的 URL 地址丢进去就行,运行环境依旧为 Node.js 8.10
3. 创建 DynamoDB 表
打开 DynamoDB,创建新表,名称任意,主键请设置名为 id 的字符串类型,创建完成即可
4. 配置 IAM 权限
打开 AWS IAM,点击左侧栏的策略(Policies),首先先创建策略
服务选择 DynamoDB,服务一栏勾选「所有 DynamoDB 操作 (dynamodb:)」,资源一栏勾选所有资源,请求条件保持默认
而后在右下角点击添加其他权限,服务选择 Cloudwatch Logs,服务一栏勾选「所有 CloudWatch Logs 操作 (logs:)」,资源一栏勾选所有资源,请求条件保持默认
最后打开 JSON 编辑器,里面显示的内容应该如下
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:*",
"dynamodb:*"
],
"Resource": "*"
}
]
}
进入下一步「查看策略」,名称任意,填写好名称之后就可以「创建策略」了
接下来回到 IAM 主页面,点击左侧侧边栏上的角色(Role),点击创建角色,「将使用此角色的服务」选择 Lambda,进入下一步附加策略
在筛选策略中选择「客户托管」,选择刚才创建的策略,下一步标签+审核,自定义角色名后,完成角色创建
5. 创建 Lambda Functions
上文有提到原项目要拆成两个 Function,所以现在先创建第一个 Function 用于更新监测数据,姑且称第一个 Function 为 Writer 吧
转到 AWS Lambda 界面,创建函数,名称任意,运行时选择 Node.js 8.10,角色选择「选择现有角色」,并在下一个下拉栏中选择刚刚创建的角色,最后「创建函数」
完成创建后点击函数下的「Layers」按钮,点击添加层,因为我们有两个运行时环境(Node.js 10 + node_modules),所以要添加两层,第一个添加的为 node.js 10 层,添加完毕后再其之上再添加一个层,选择刚刚上传的,包含 node_modules 的那一层
Layers 部署完毕之后回到刚才克隆好的 Github 仓库,执行下列命令
yarn build
zip -r UptimeRobot_src.zip build/ config/ UptimeWriterEntry.js
将生成的 UptimeRobot_src.zip 上传至 AWS Lambda,并将处理程序改为 UptimeWriterEntry.fake_handler
而后需要添加环境变量,UPTIME_ROBOT_API
需要自行在 UptimeRobot 官网获取 API Key,TABLE_NAME
为之前创建的 DynamoDB 表名,KEY_NAME
为键值名可自定义,TZ
指定当前时区为 CST,WEBSITE_COPYRIGHT
和 WEBSITE_TITLE
即为版权与网页标题设定
如图所示
而后保存,点击右上角的「测试」,以「Hello World」为模板建立新测试,如果出现如图所示且没有报错的话应该就没问题了
可以去 DynamoDB 上看一下数据情况
还有一件事就是设置定时触发器触发这个 Writer 函数执行更新操作
回到函数设置界面,左侧栏选择 Cloudwatch Events,在下方选择创建新规则,名称任意,选择计划表达式,计划表达式内容输入 cron(0/10 * * * ? *)
代表每隔 10 分钟更新一次,可以根据自身需求不同设定更新频率,记得保存
接下来就是创建第二个函数啦,姑且称其为 Reader 函数
继续转到 AWS Lambda 界面,创建函数,名称任意,运行时选择 Python 3.7,角色选择「选择现有角色」,并在下一个下拉栏中选择刚刚创建的角色,最后「创建函数」
因为 Reader 是单独写了一个小脚本实现,所以不需要设置 Layers,直接把项目下的 UptimePythonReader.py
单文件打包为 zip 上传即可
同时处理程序请设置为 UptimePythonReader.lambda_handler
Reader 也需要设置环境变量,只不过只需要设置 TABLE_NAME
和 KEY_NAME
,且这两个环境变量的值应与上文的保持一致
而后保存,点击右上角的「测试」,以「Hello World」为模板建立新测试,如果出现如图所示且没有报错的话应该就没问题了
6. 配置 API Gateway
Lambda 唯一能与外网访问的途径便是经过 AWS API Gateway 了,所以必须要把这两者结合起来
打开 API Gateway,新建一个 REST 类型的 API,名称任意,端点类型为区域型避免一些麻烦的事情
完毕后打开此 API 的「资源」界面,操作->创建方法,创建一个 GET 方法,右侧选择 Lambda 函数 + 勾选 Lambda 代理集成,选择 Lambda 所在的区域之后在下面的文本框输入 Lambda Function 名会自动搜索备选项,请输入 Reader 的 Function 名并选择 Reader,而不要误选了 Writer
下一步将提示是否授权此 API Gateway 调用该 Lambda Function,点击允许即可
接下来继续点击 操作->部署 API,阶段选择新阶段,阶段名任意,部署完毕之后将能看到一个调用地址,如果前期配置无误,点开应该就是 状态页的界面
到此为止部署就成功了,然而换作谁肯定都不愿意拖着那个长长的 URL,所以需要设置自定义域名
7. 设置自定义域名
在申请自定义域名之前需要验证下自己的域名使用权,实际上就是在 AWS Certificate Manager 上为域名签发证书,否则访客就无法通过 HTTPS 访问状态页啦
先跳转到 ACM(AWS Certificate Manager),注意对应区域,申请证书的区域需要和 API Gateway 所在区域一致
点击请求证书->请求公有证书,填上要签发证书的域名,可以选择 DNS 验证或是邮件认证,具体认证过程就不再赘述,验证完毕之后可以在 ACM 证书列表里看到自己的域名
回到 API Gateway
点击左侧栏的自定义域名,填写好域名选择好证书,端点配置推荐选择 Regional(区域型),边缘优化实际上是调用了 CloudFront 作为 CDN,点击完成
之后请别急,在刚刚创建的域名下方点击编辑,选择映射路径,添加映射,路径为
/
,目标和阶段选择相应的 Lambda Function 和刚刚创建的阶段最后把域名的 CNAME 记录解析到 AWS 给出的地址(
************.execute-api.**-******-*.amazonaws.com
)
这样就可以啦
8. 添加监测项目
访问 https://uptimerobot.com/dashboard ,添加几个监测项目,可以是 HTTP/HTTPS 也可以是端口或是ping监测,只是有一点需要注意
关于 UptimeRobot 控制面板中 Monitor 命名的问题
这一点原作者 Giuem 也没有着重强调,没有好好看 README 的小伙伴(包括奶冰)全部都掉坑里去了
README 的最后有写 Monitor 的命名规则,原文如下
We use the parser to analyze groups name, monitors name and group index (optional) which included in your original monitor’s name. Default Parser is
%group/%name
, there are 3 variables now:
%group
%name
%index
You can change the backslash / to any separator you like, as long as it won’t used in your group & monitor name (and index). To put index into your parser, you will able to sort your group in page manually. Easily write index for each group once (also for all if you like), and leave the blank of the index for other monitors in the same group.
换句话说,对于默认的配置需要以<组名>/<监视项目名>
的规则为 Monitor 命名
比如奶冰的状态监测页有一组的组名为Osu
,包括三个监测项目Demo Page
,Milkice API
,Genteure API
,那么在 UptimeRobot 管理面板中这三个监测项目的命名就是
不同的组,组名只要保证不重复即可
别急 还没结束 接着往下看
杂项
- 关于 Writer 运行时间过长的原因
有小伙伴反馈说 Writer 运行时间过长甚至可达 20s 以上,有一些原因会导致这种情况
- 如果是刚刚部署好源码就执行函数,第一次执行的确会比较慢,试试看第一次执行完毕后执行第二第三次,耗时应有所减少
- AWS Lambda 是根据分配的内存来分配 CPU 时长的,分配内存越多 CPU 资源越多,可以在 Lambda Function 配置界面自行调整 RAM 分配大小
奶冰大致分配了 512 MB,虽然实际使用内存远不到 512 MB,但是出于性能考虑只能调高内存,调整内存后 Writer 耗时大约在 7-10s 左右
同时对于高耗时操作记得别忘了调整超时选项,对于 Writer 15s 最佳,Reader 理论情况应小于 1s,最坏情况不超过 5s
具体原因之前有过提及,因为 UptimeRobotPage 项目依赖于 Node.js 10,而 AWS Lambda 最高只支持到 8.10,因而实际上每次执行 Writer Function 都是调用外部 node 来执行 index.js 执行各种操作,因而在 Lambda Runtime 之下冷启动 Node Binary 是导致高耗时的主要原因
奶冰搭建在 Lambda 上的状态监测页现在在这个地址
https://s.milkice.me
最初采用的方案是,当有访客访问时调用 Reader Function 读取缓存并调用 pugjs 渲染后返回给访客,因为这种方案会外部调用 Node.js 10 导致效率异常低下,RTT一度达到 3s,后来想了下完全可以把渲染工作交给 Writer 处理,稍许优化现在访客打开状态监测页的平均用时应小于 1s
其实本篇的内容不光光是部署 UptimePage-Serverless,因为状态页不是特别适合搭建在 Serverless 上,而更多的是提供了一些折腾 Serverless 的新思路吧,针对于主流的 Web 框架(Express, Koa)已经有中间件可以实现 Web Server to Serverless 的转化了,相信适用于高并发应用场景的 Serverless 环境能给最终解决方案带来更多可能
-EOF-