最近嘗試將應用的頁面 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