最近尝试将应用的页面 JS 错误报警功能经过 Serverless 来实现。本文主要介绍一下具体实现过程,以及遇到的一些问题。html
报警功能的需求也很简单,就是定时(如每隔 1 分钟)去读取 ARMS 的错误日志,若是有错误日志,则经过钉钉消息发送错误详情进行报警。前端
在这以前,我经过定时任务实现了该功能。从成本上来讲,这种方案就须要单独申请一台服务器资源;并且定时任务只在对应的时间才执行,这件意味着,服务器有很长的时间都是空闲的,这就形成了资源的浪费。而使用 Serverless,就不须要再申请服务器,函数只须要在须要的时候执行,这就大大节省了成本。node
总的来讲,我以为函数计算的优点就是:git
经过 Serverless 实现前端日志报警,依赖的云服务是阿里云函数计算,依赖的其余工具还有:github
初次使用须要先安装 fun
docker
$ npm install @alicloud/fun -g
安装完成以后,须要经过 fun config
配置一下帐号信息 Aliyun Account ID
Aliyun Access Key ID
Aliyun Secret Access Key
以及默认的地域。地域这里有个须要注意的是,若是须要使用 SLS 记录函很多天志,则须要 SLS 和函数服务在同一个地域。这里稍后会详细介绍。npm
$ fun config ? Aliyun Account ID ****** ? Aliyun Access Key ID ****** ? Aliyun Secret Access Key ****** ? Default region name cn-shanghai ? The timeout in seconds for each SDK client invoking 60 ? The maximum number of retries for each SDK client 6
Aliyun Account ID
Aliyun Access Key ID
Aliyun Secret Access Key
能够在阿里云的控制台中查找和设置。编程
![Aliyun Account ID]bash
Aliyun Access Key ID
Aliyun Secret Access Key
先经过 fun
建立一个 Node.js 的 demo,以后能够在这个 demo 的基础上进行开发。
$ fun init -n alarm helloworld-nodejs8 Start rendering template... + /Users/jh/inbox/func/code/alarm + /Users/jh/inbox/func/code/alarm/index.js + /Users/jh/inbox/func/code/alarm/template.yml finish rendering template.
执行成功后,分别建立了两个文件 index.js
和 template.yml
。
其中 template.yml
是函数的规范文档,在里面定义了函数须要的资源、触发函数的事件等等。
接下来简单看看生成的默认的 template.yml
配置文件。
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: alarm: Type: 'Aliyun::Serverless::Service' Properties: Description: 'helloworld' alarm: Type: 'Aliyun::Serverless::Function' Properties: Handler: index.handler Runtime: nodejs8 CodeUri: 'index.js'
首先定义了规范文档的版本 ROSTemplateFormatVersion
和 Transform
,这两个都不用修改。
Resources
里面定义了一个名为 alarm
的函数服务(Type: Aliyun::Serverless::Service
表示该属性为函数服务),而且该服务里面定义了名为 alarm
的函数(Type: 'Aliyun::Serverless::Function'
表示该属性为函数)。
函数服务里面能够包含多个函数,就至关因而一个函数组。后面咱们会提到的函很多天志,是配置到函数服务上的。函数服务里面的全部函数,都用同一个日志。
能够根据实际状况修改函数服务名和函数名。下面就将函数服务名称改成 yunzhi
,函数名依旧保留为 alarm
。
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: yunzhi: # 函数服务的名称 Type: 'Aliyun::Serverless::Service' # 表示 yunzhi 是一个函数服务 Properties: Description: 'helloworld' # 函数服务的描述 alarm: # 函数的名称 Type: 'Aliyun::Serverless::Function' # 表示 alarm 是一个函数 Properties: Handler: index.handler # 函数的调用入口 Runtime: nodejs8 # 函数的运行环境 CodeUri: 'index.js' # 代码的目录
alarm
函数里面的 Properties
定义了函数的调用入口、运行环境等,如上面的注释所示。
关于 template.yml
的配置详见 Serverless Application Model。
index.js
文件就是函数的调用入口了。index.handler
就表示,函数的调用的是 index.[extension]
文件中的 handler
函数。
module.exports.handler = function(event, context, callback) { console.log('hello world'); callback(null, 'hello world'); };
初始化以后的代码就上面这几行,很简单。主要是理解上面的几个参数。
event
调用函数时传入的参数context
函数运行时的一些信息callback
函数执行以后的回调
callback
函数,才会被认为函数执行结束。若是没有调用,则函数会一直运行到超时callback
调用以后,函数就结束了callback
的第一个参数是 error
对象,这和 JS 回调编程的思想一致关于 event
和 context
,详见 Nodejs 函数入口。
实现报警功能的主要逻辑,就写在 index.js
里面。具体的实现,就不细说,下面用伪代码来描述:
alarm/alarm.js
// alarm/alarm.js // 实现报警功能 module.exports = function() { return new Promise((resolve, reject) => { // 查询 SLS 日志 // - 若是没有错误日志,则 resolve // - 若是有错误日志,则发送钉钉消息 // - 若是钉钉消息发送失败,则 reject // - 若是钉钉消息发送成功,则 resolve resolve(); }) }
alarm/index.js
// alarm/index.js // 调用报警函数 const alarm = require('./alarm'); module.exports.handler = function(event, context, callback) { alarm() .then(() => { callback(null, 'success'); }) .catch(error => { callback(error); }) };
若是函数里面引入了自定义的其余模块,好比在 index.js
里面引入了 alarm.js
const alarm = require('./alarm');
,则须要修改默认的 codeUri
为当前代码目录 ./
。不然默认的 codeUri
只定义了 index.js
,部署的时候只会部署 index.js
。
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: yunzhi: # 函数服务的名称 Type: 'Aliyun::Serverless::Service' # 表示 yunzhi 是一个函数服务 Properties: Description: 'helloworld' # 函数服务的描述 alarm: # 函数的名称 Type: 'Aliyun::Serverless::Function' # 表示 alarm 是一个函数 Properties: Handler: index.handler # 函数的调用入口 Runtime: nodejs8 # 函数的运行环境 CodeUri: './' # 代码的目录
若是没有修改 CodeUri
,则会有相似下面的报错
$ fun local invoke alarm FC Invoke End RequestId: 16e3099e-6a40-43cb-99a0-f0c75f3422c6 { "errorMessage": "Cannot find module './alarm'", "errorType": "Error", "stackTrace": [ "Error: Cannot find module './alarm'", "at Module._resolveFilename (module.js:536:15)", "at Module._load (module.js:466:25)", "at Module.require (module.js:579:17)", "at require (internal/module.js:11:18)", "at (/code/index.js:9:15)", "at Module._compile (module.js:635:30)", "at Module._extensions..js (module.js:646:10)", "at Module.load (module.js:554:32)", "at tryModuleLoad (module.js:497:12)", "at Module._load (module.js:489:3)" ] }
fun local invoke alarm
是本地调试的命令,接下来会讲到。
在开发过程当中,确定须要本地调试。fun
提供了 fun local
支持本地调试。
fun local
的命令格式为 fun local invoke [options] <[service/]function>
,其中 options
和 service
均可以忽略。好比调试上面的报警功能的命令就是 fun local invoke alarm
。
须要注意的是,本地调试须要先安装 docker。
$ brew cask install docker
安装成功后启动 docker。
若是 docker 没有启动,运行 fun local
可能会有以下报错
$ fun local invoke alarm Reading event data from stdin, which can be ended with Enter then Ctrl+D (you can also pass it from file with -e) connect ENOENT /var/run/docker.sock
正常的输出以下
$ fun local invoke alarm Reading event data from stdin, which can be ended with Enter then Ctrl+D (you can also pass it from file with -e) skip pulling image aliyunfc/runtime-nodejs8:1.5.0... FC Invoke Start RequestId: 9360768c-5c52-4bf5-978b-774edfce9e40 load code for handler:index.handler FC Invoke End RequestId: 9360768c-5c52-4bf5-978b-774edfce9e40 success RequestId: 9360768c-5c52-4bf5-978b-774edfce9e40 Billed Duration: 79 ms Memory Size: 1998 MB Max Memory Used: 54 MB
第一次调试的话,会安装 runtime 的镜像,可能须要点时间。默认的 Docker 镜像下载会很慢,可使用国内的加速站点加速下载。
出现 Reading event data from stdin, which can be ended with Enter then Ctrl+D
的提示时,若是不须要输入,能够按 ctrl+D
跳过。
开发完成以后,就须要将函数部署到阿里云的函数计算上面了。部署能够经过 fun deploy
命令。
前面已经在安装 fun
以后,经过 fun config
命令配置了阿里云的帐号和地域信息,fun deploy
会将函数自动部署到对应的帐号和地域下。
在 template.yml
中,也配置了函数的服务名和函数名。若是在函数计算中没有对应的服务或函数,fun deploy
会自动建立;若是已经存在,则会更新。
$ fun deploy using region: cn-shanghai using accountId: ***********4698 using accessKeyId: ***********UfpF using timeout: 60 Waiting for service yunzhi to be deployed... Waiting for function alarm to be deployed... Waiting for packaging function alarm code... package function alarm code done function alarm deploy success service yunzhi deploy success
部署成功以后,就能够在函数计算的控制台中看到对应的函数服务和函数了。目前尚未配置触发器,能够手动在控制台中点击“执行”按钮来执行函数。
对于应用到生产环境的函数,确定不会像上面同样手动去执行它,而是经过配置触发器去执行。触发器就至关因而一个特定的事件,当函数计算接收到该事件的时候,就去调用对应的函数。
阿里云的函数计算支持 HTTP 触发器(接收到 HTTP 请求以后调用函数)、定时触发器(定时调用函数)、OSS 触发器等等。详见 触发器列表。
对于报警功能,须要用到的是定时触发器,由于须要间隔必定的时间就调用函数。
触发器是配置到函数中的,能够经过函数的 Event
属性去配置
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: yunzhi: Type: 'Aliyun::Serverless::Service' Properties: Description: 'helloworld' alarm: Type: 'Aliyun::Serverless::Function' Properties: Handler: index.handler Runtime: nodejs8 CodeUri: './' Events: # 配置 alarm 函数的触发器 TimeTrigger: # 触发器的名称 Type: Timer # 表示该触发器是定时触发器 Properties: CronExpression: "0 0/1 * * * *" # 每 1 分钟执行一次 Enable: true # 是否启用该定时触发器
上面的配置,就为 alarm
配置了一个名为 TimeTrigger
的定时触发器,触发器每隔 1 分钟执行一次,也就是每隔 1 分钟调用一次函数。
配置完成以后,再执行 fun deploy
就能够发布函数及触发器到函数计算上。
这里须要注意的是,阿里云函数计算服务目前支持的触发器,最小的间隔时间为 1 分钟。若是小于 1 分钟,则没法设置成功。定时触发器的详细介绍可参考文档 定时触发函数。
对于 serverless 应用,虽然不用关心运维了,其实咱们也并不知道咱们的函数运行在哪台服务器上。这个时候,函数的日志就尤其重要了。没有日志,咱们很难知道程序运行状态,遇到问题更是无从下手。
因此接下来须要对函数配置日志。阿里云的函数计算可使用阿里云日志服务 SLS来存储日志。若是要存储日志,则须要先开通 日志服务。
若是是第一次使用日志服务,则确定不存在日志库。能够在 template.yml
像定义函数服务同样,经过 Resource 来定义日志资源。
前面也提到,函很多天志是配置到对应的服务上的,具体配置也很简单,就是经过函数服务的 LogConfig
属性来配置。
完整的 template.yml
以下
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: log-yunzhi: # 日志项目名称为 log-yunzhi Type: 'Aliyun::Serverless::Log' # 表示该资源是阿里云的日志服务 Properties: Description: 'yunzhi function service log project' log-yunzhi-store: # 日志的 logstore Type: 'Aliyun::Serverless::Log::Logstore' Properties: TTL: 10 ShardCount: 1 log-another-logstore: # 日志的另外一个 logstore Type: 'Aliyun::Serverless::Log::Logstore' Properties: TTL: 10 ShardCount: 1 yunzhi: Type: 'Aliyun::Serverless::Service' Properties: Description: 'helloworld' LogConfig: # 配置函数的日志 Project: 'log-yunzhi' # 存储函很多天志 SLS 项目: log-yunzhi Logstore: 'log-yunzhi-store' # 存储函很多天志的 SLS logstore: log-yunzhi-store alarm: Type: 'Aliyun::Serverless::Function' Properties: Handler: index.handler Runtime: nodejs8 CodeUri: './' Events: TimeTrigger: Type: Timer Properties: CronExpression: "0 0/1 * * * *" Enable: true
在上面的配置中,就定义了名为 log-yunzhi
的日志项目(Project),而且在该 Project 中建立了两个日志仓库(LogStore):log-yunzhi-store
和 log-yunzhi-store
。一个 Project 能够包含多个 LogStore。
注意:日志项目的名称必须全局惟一。 即配置中,og-yunzhi
这个项目名称是全局惟一的。
执行 fun deploy
以后,就会自动在函数服务对应的地域建立日志 Project 及日志 logstore,同时也会自动为 logstore 加上全文索引,而后自动为函数服务配置日志仓库。
以后函数的运行日志都会存储在对应的 logstore 里。
$ fun deploy using region: cn-shanghai using accountId: ***********4698 using accessKeyId: ***********UfpF using timeout: 60 Waiting for log service project log-yunzhi to be deployed... Waiting for log service logstore log-yunzhi-store to be deployed... retry 1 times Waiting for log service logstore log-yunzhi-store default index to be deployed... log service logstore log-yunzhi-store default index deploy success log serivce logstore log-yunzhi-store deploy success Waiting for log service logstore log-another-logstore to be deployed... Waiting for log service logstore log-another-logstore default index to be deployed... log service logstore log-another-logstore default index deploy success log serivce logstore log-another-logstore deploy success log serivce project log-yunzhi deploy success Waiting for service yunzhi to be deployed... Waiting for function alarm to be deployed... Waiting for packaging function alarm code... package function alarm code done Waiting for Timer trigger TimeTrigger to be deployed... function TimeTrigger deploy success function alarm deploy success service yunzhi deploy success
若是日志库已经存在,且定义了日志资源,则 fun deploy
会按照 template.yml
中的配置更新日志库。
若是日志库已经存在,即已经在日志服务中建立了日志项目 Project 和日志库 Logstore ,就能够直接为函数服务添加 LogConfig,不用再定义日志资源。
注意,日志库须要和函数服务在同一个地域 Region。不然不能部署成功。
下面是一个配置函很多天志到已经存在的 Project 和 Logstore 中的例子。
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: yunzhi: Type: 'Aliyun::Serverless::Service' Properties: Description: 'helloworld' LogConfig: # 配置函数的日志 Project: 'log-yunzhi-exist' # 存储函很多天志到已经存在的 Project: log-yunzhi-exist Logstore: 'logstore-exist' # 存储函很多天志到已经存在的 logstore: logstore-exist alarm: Type: 'Aliyun::Serverless::Function' Properties: Handler: index.handler Runtime: nodejs8 CodeUri: './' Events: TimeTrigger: Type: Timer Properties: CronExpression: "0 0/1 * * * *" Enable: true
若是日志库和函数服务不在同一个地域,函数服务就会找不到日志库,fun deploy
也会报错。以下所示,yunzhi-log-qingdao
是我建立的一个青岛地域的日志 Project。
$ fun deploy using region: cn-shanghai using accountId: ***********4698 using accessKeyId: ***********UfpF using timeout: 60 Waiting for service yunzhi to be deployed... retry 1 times retry 2 times retry 3 times retry 4 times retry 5 times retry 6 times retry 7 times PUT /services/yunzhi failed with 400. requestid: 6af2afb8-cbd9-0d3e-bf16-fe623834b4ee, message: project 'yunzhi-log-qingdao' does not exist.
若是是使用 RAM 子帐号来开发、部署函数计算,则 fun
工具的配置中 Aliyun Access Key ID
Aliyun Secret Access Key
是对应子帐户的信息,但 Aliyun Account ID
仍是主帐号的信息。RAM 子帐号有一个 UID,这个不是 Account ID。
若是 Aliyun Account ID
写错了,则使用 fun
或 fcli
的时候,可能会遇到下面的错误
Error: { "HttpStatus": 403, "RequestId": "b8eaff86-e0c1-c7aa-a9e8-2e7893acd545", "ErrorCode": "AccessDenied", "ErrorMessage": "The service or function doesn't belong to you." }
在实现报警功能的过程当中,我依旧使用了 GitLab 来存储代码。每次开发完成以后,将代码 push 到 GitLab,而后再将代码部署到函数计算上。不过这两个过程是独立的,仍是不那么方便。
通常咱们开发的时候,须要平常、预发、线上多个环境部署、测试。阿里云函数计算是一个云产品,没有环境的区分。但对于报警整个功能,我也没有去区分环境,只是本地开发的时候,将报警消息发到一个测试的钉钉群,因此也没有特别去关注。
使用函数计算的经济成本,相比于购买云服务器部署应用,成本低了很是多。
本文中涉及到函数计算和日志服务两个云产品,都有必定的免费额度。其中函数计算每个月前 100 万次函数调用免费,日志服务每个月也有 500M 的免费存储空间和读写流量。因此只用来测试或者实现一些调用量很小的功能,基本是免费的。
配置了函数的日志以后,将函数部署到函数计算上,就算是正式发布上线了。
如今回过头来看,整个流程还算比较简单。但从零开始一步一步到部署上线的过程,仍是遇到了不少问题。好比文中的许多注意事项,都是在不断尝试中得出的结论。
最近 serverless 这个话题也很火热,也期待这个技术即将带来的变革。
more https://github.com/nodejh/nodejh.github.io/issues