使用 Serverless 实现日志报警

最近尝试将应用的页面 JS 错误报警功能经过 Serverless 来实现。本文主要介绍一下具体实现过程,以及遇到的一些问题。html

报警功能的需求也很简单,就是定时(如每隔 1 分钟)去读取 ARMS 的错误日志,若是有错误日志,则经过钉钉消息发送错误详情进行报警。前端

在这以前,我经过定时任务实现了该功能。从成本上来讲,这种方案就须要单独申请一台服务器资源;并且定时任务只在对应的时间才执行,这件意味着,服务器有很长的时间都是空闲的,这就形成了资源的浪费。而使用 Serverless,就不须要再申请服务器,函数只须要在须要的时候执行,这就大大节省了成本。node

总的来讲,我以为函数计算的优点就是:git

  • 对于开发者,只须要关系业务逻辑的实现,不须要关心代码所运行的环境、硬件资源、以及运维
  • 节省成本

经过 Serverless 实现前端日志报警,依赖的云服务是阿里云函数计算,依赖的其余工具还有:github

  • 函数计算的命令行工具 fun,用于本地调试、部署函数
  • 函数计算的可交互式工具 fcli,用于本地测试
  • 阿里云 JS SDK aliyun-sdk-js,用于读取 SLS 日志,ARMS 的日志是存储在 SLS 中的
  • 编程语言使用 Node.js

安装和配置 fun

初次使用须要先安装 fundocker

$ 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数组

![Aliyun Account ID]
accountidbash

Aliyun Access Key ID Aliyun Secret Access Key

accesskey

函数初始化

先经过 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.jstemplate.yml

其中 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'

首先定义了规范文档的版本 ROSTemplateFormatVersionTransform,这两个都不用修改。

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.js 文件就是函数的调用入口了。index.handler 就表示,函数的调用的是 index.[extension] 文件中的 handler 函数。

module.exports.handler = function(event, context, callback) { 
  console.log('hello world');
  callback(null, 'hello world'); 
};

初始化以后的代码就上面这几行,很简单。主要是理解上面的几个参数。

  • event 调用函数时传入的参数
  • context 函数运行时的一些信息
  • callback 函数执行以后的回调

      1. 必需要要调用 callback 函数,才会被认为函数执行结束。若是没有调用,则函数会一直运行到超时
      1. callback 调用以后,函数就结束了
      1. callback 的第一个参数是 error 对象,这和 JS 回调编程的思想一致

关于 eventcontext,详见 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); 
        })
};

CodeUri

若是函数里面引入了自定义的其余模块,好比在 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>,其中 optionsservice 均可以忽略。好比调试上面的报警功能的命令就是 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

部署成功以后,就能够在函数计算的控制台中看到对应的函数服务和函数了。目前尚未配置触发器,能够手动在控制台中点击“执行”按钮来执行函数。

fcshow2

触发器

对于应用到生产环境的函数,确定不会像上面同样手动去执行它,而是经过配置触发器去执行。触发器就至关因而一个特定的事件,当函数计算接收到该事件的时候,就去调用对应的函数。

阿里云的函数计算支持 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 就能够发布函数及触发器到函数计算上。

trigger

这里须要注意的是,阿里云函数计算服务目前支持的触发器,最小的间隔时间为 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-storelog-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

slslog

若是日志库已经存在,且定义了日志资源,则 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.

其余问题

子帐号 AccessDenied

若是是使用 RAM 子帐号来开发、部署函数计算,则 fun 工具的配置中 Aliyun Access Key ID Aliyun Secret Access Key
是对应子帐户的信息,但 Aliyun Account ID 仍是主帐号的信息。RAM 子帐号有一个 UID,这个不是 Account ID。

若是 Aliyun Account ID 写错了,则使用 funfcli 的时候,可能会遇到下面的错误

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
相关文章
相关标签/搜索