本文介绍如何使用函数计算+Koajs+表格存储实现一个serverless web服务。
背景
基于搭建微信公众号的需要,我希望自己动手实现一个后台服务,一方面积累一些开发经验,另一方面想实现一些个性化的功能。作为穷屌丝一枚,为了找到一个廉价、稳定、易用的服务器,本着不怕折腾的精神,游荡(bai piao)在各大云计算厂商之间。
偶然的机会,试用了阿里云的函数计算,经过数十天尝试,把新浪SAE的一堆web服务浩浩荡荡迁移到函数计算之后,才发现Serverless架构已经如此成熟,开始飞入寻常百姓家了。
言归正传
作为Serverless的重要一环,FaaS服务也是各大云厂商必争之地。 函数计算 就是阿里云推出一款FaaS服务。它的优点是无运维、动态扩容、按量计费,缺点是目前支持的平台还不够多(Nodejs、python、php、java)。对于屌丝而且,这几个缺点已经可以忽略,而它的优点简直让人无法拒绝。
实现
技术栈
微信公众号的后台其实就是一个简单的web服务,我的技术栈选择是:
- koajs,轻量、成熟,特别适合FaaS
- 函数计算(FaaS),廉价、稳定
- 表格存储(BaaS),廉价、稳定,高性能,FaaS的完美搭配
配置
函数计算弱化了服务器的概念。要部署一个web服务,我们只需把业务代码上传到阿里云,配置访问入口,就完成了。不需要计算几核几G,也不需要考虑容灾报警,这些琐事都交给阿里云。
函数计算的部署主要有两种方式
- 借助阿里官方的fun cli,可以一键部署
- 使用阿里云控制台手动上传代码,手动填写配置
- 代码的上传方式有:在线编辑,OSS上传,代码包上传,文件夹上传
- 需要配置内容有:函数名、运行环境(nodejs8)、超时时间、环境变量、触发器……
调试初期我用的是阿里云控制台,上手比较简单,流程跑通后可以导出配置文件(一个template.yml文件),再改为funcli部署。
需要注意的是,只有配置了Http触发器才能实现公网的http访问,而且只有绑定了自定义域名才能像网站一样打开页面,否则原始url的http响应头有限制,浏览器是无法正常打开页面的。
函数
函数计算的入口是一个js函数,它的参数根据不同的触发器会有所不同。
http出发器的入口参数有req,resp,context。基于这些参数即可完成一次http请求的响应,但是它们与Nodejs的HttpServer并不完全兼容。不过前人帮我们做了一些转换,可以直接拿来用 —- @webserverless/fc-express
从名字fc-express即可看出,它是为express.js做的,还好Koa和express很有渊源,想要兼容也很简单,下面是代码。
index.js:
const { Server } = require('@webserverless/fc-express')const Koa = require('koa')const app = require('./src/server');let serverconst app = new Koa();module.exports.init = function (context, cb) {function callback() {cb(null, 'finish init');}if (!server) {server = new Server(app.callback(), callback);server.startServer()}}// http trigger entrymodule.exports.handler = async function (req, res, context) {server.httpProxy(req, res, context);};
注意,函数计算会涉及到冷启动问题,exports.init 就是为了解决这种问题而存在的。冷启动时,只有cb(null, ‘finish’)执行完成后,才会进入exports.handler执行。
业务
微信公众号的后台其实就是一个web服务。我们只需要基于koajs实现两个接口。看代码:
wechatRouter.get('/:privatekey/wechat-token', async function (ctx) {if (!ctx.request.query) {ctx.request.query = {}}const { privatekey } = ctx.params;const { echostr, signature, timestamp, nonce } = ctx.request.query;if (check([genToken(privatekey), timestamp, nonce], signature)) {ctx.body = echostr;} else {ctx.status = 400;ctx.body = 'invalid req.'}})
wechatRouter.post('/:privatekey/wechat-token', async function(ctx) {if (!ctx.request.query) {ctx.request.query = {}}const { privatekey } = ctx.params;const { signature, timestamp, nonce } = ctx.request.query;if (check([genToken(privatekey), timestamp, nonce], signature)) {await wechatMsgHandler(ctx, privatekey);} else {ctx.status = 400;ctx.body = 'invalid req.'}})
其中get接口处理微信公众平台的校验,post接口处理消息响应。
存储
FaaS需要搭配BaaS才能实现完整的web服务,BaaS往往是一些状态存储相关的服务,如表格存储、oss、消息队列、缓存服务等。
大部分情况下,阿里云的TableStore可以取代传统的SQL数据库。相比目前SQL数据库服务,TableStore能够按量付费(近似bai piao)。它也为Nodejs平台提供了简单的SDK。
我使用TableStore存储了公众号用户的订阅/退订记录:
const { client } = require('../lib/tablestore');const TableStore = require('tablestore');const log = require('../lib/log');async function putUser(appName, openid, enabled) {const currentTimeStamp = Date.now();const params = {tableName: 'user',condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),primaryKey: [{ openid, appName } ],attributeColumns: [{ enabled, 'timestamp': currentTimeStamp },],returnContent: { returnType: TableStore.ReturnType.Primarykey }};try {const result = await client.putRow(params);} catch(e) {log.error(e)}return true;}async function putUnsub(appName, openid) {const currentTimeStamp = Date.now();const params = {tableName: 'unsub_log',condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),primaryKey: [{ openid }, { appName }, { id: TableStore.PK_AUTO_INCR } ],attributeColumns: [{ 'time': currentTimeStamp },],returnContent: { returnType: TableStore.ReturnType.Primarykey }};try {const result = await client.putRow(params);} catch(e) {log.error(e)}return true;}
结合这些数据,后期可以为公众号订阅用户提供独特的增值服务。
部署
使用funcli可以实现快捷的函数计算部署,下面是一个配置文件demo
template.yml
ROSTemplateFormatVersion: '2015-09-01'Transform: 'Aliyun::Serverless-2018-04-03'Resources:wx.roughwin.com:Type: 'Aliyun::Serverless::CustomDomain'Properties:Protocol: HTTP,HTTPSCertConfig:CertName: 'Cert1'PrivateKey: './cert/privkey.pem'Certificate: './cert/cert.pem'RouteConfig:routes:'/*':ServiceName: appFunctionName: apiapp:Type: 'Aliyun::Serverless::Service'Properties:Description: This is app serviceRole: 'acs:ram::1234:role/fc-log-role'LogConfig:Project: logtestLogstore: backend-logVpcConfig:VpcId: vpc-123VSwitchIds:- vsw-123SecurityGroupId: sg-123InternetAccess: trueapi:Type: 'Aliyun::Serverless::Function'Properties:Initializer: index.initInitializationTimeout: 3Handler: index.handlerRuntime: nodejs8Timeout: 10MemorySize: 128CodeUri: ./webEvents:httpTrigger:Type: HTTPProperties:AuthType: ANONYMOUSMethods:- GET- POST
总结
基于函数计算、Koajs和表格存储搭建的微信公众号后台服务,可以做到成本0元起、无需运维、易开发、灵活部署、安全稳定等众多优势。
serverless是为云计算而生的一种技术架构,优势明显,前景不错。目前各大云厂商提供的serverless技术方案都已经非常成熟、易用。对创业团队来说,将现有技术和业务迁移到serverless已经完全可行。serverless的成本、效率优势也十分可观。
