本文介绍如何使用函数计算+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:

  1. const { Server } = require('@webserverless/fc-express')
  2. const Koa = require('koa')
  3. const app = require('./src/server');
  4. let server
  5. const app = new Koa();
  6. module.exports.init = function (context, cb) {
  7. function callback() {
  8. cb(null, 'finish init');
  9. }
  10. if (!server) {
  11. server = new Server(app.callback(), callback);
  12. server.startServer()
  13. }
  14. }
  15. // http trigger entry
  16. module.exports.handler = async function (req, res, context) {
  17. server.httpProxy(req, res, context);
  18. };

注意,函数计算会涉及到冷启动问题,exports.init 就是为了解决这种问题而存在的。冷启动时,只有cb(null, ‘finish’)执行完成后,才会进入exports.handler执行。

业务

微信公众号的后台其实就是一个web服务。我们只需要基于koajs实现两个接口。看代码:

  1. wechatRouter.get('/:privatekey/wechat-token', async function (ctx) {
  2. if (!ctx.request.query) {
  3. ctx.request.query = {}
  4. }
  5. const { privatekey } = ctx.params;
  6. const { echostr, signature, timestamp, nonce } = ctx.request.query;
  7. if (check([genToken(privatekey), timestamp, nonce], signature)) {
  8. ctx.body = echostr;
  9. } else {
  10. ctx.status = 400;
  11. ctx.body = 'invalid req.'
  12. }
  13. })
  1. wechatRouter.post('/:privatekey/wechat-token', async function(ctx) {
  2. if (!ctx.request.query) {
  3. ctx.request.query = {}
  4. }
  5. const { privatekey } = ctx.params;
  6. const { signature, timestamp, nonce } = ctx.request.query;
  7. if (check([genToken(privatekey), timestamp, nonce], signature)) {
  8. await wechatMsgHandler(ctx, privatekey);
  9. } else {
  10. ctx.status = 400;
  11. ctx.body = 'invalid req.'
  12. }
  13. })

其中get接口处理微信公众平台的校验,post接口处理消息响应。

存储

FaaS需要搭配BaaS才能实现完整的web服务,BaaS往往是一些状态存储相关的服务,如表格存储、oss、消息队列、缓存服务等。
大部分情况下,阿里云的TableStore可以取代传统的SQL数据库。相比目前SQL数据库服务,TableStore能够按量付费(近似bai piao)。它也为Nodejs平台提供了简单的SDK。
我使用TableStore存储了公众号用户的订阅/退订记录:

  1. const { client } = require('../lib/tablestore');
  2. const TableStore = require('tablestore');
  3. const log = require('../lib/log');
  4. async function putUser(appName, openid, enabled) {
  5. const currentTimeStamp = Date.now();
  6. const params = {
  7. tableName: 'user',
  8. condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
  9. primaryKey: [{ openid, appName } ],
  10. attributeColumns: [
  11. { enabled, 'timestamp': currentTimeStamp },
  12. ],
  13. returnContent: { returnType: TableStore.ReturnType.Primarykey }
  14. };
  15. try {
  16. const result = await client.putRow(params);
  17. } catch(e) {
  18. log.error(e)
  19. }
  20. return true;
  21. }
  22. async function putUnsub(appName, openid) {
  23. const currentTimeStamp = Date.now();
  24. const params = {
  25. tableName: 'unsub_log',
  26. condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
  27. primaryKey: [{ openid }, { appName }, { id: TableStore.PK_AUTO_INCR } ],
  28. attributeColumns: [
  29. { 'time': currentTimeStamp },
  30. ],
  31. returnContent: { returnType: TableStore.ReturnType.Primarykey }
  32. };
  33. try {
  34. const result = await client.putRow(params);
  35. } catch(e) {
  36. log.error(e)
  37. }
  38. return true;
  39. }

结合这些数据,后期可以为公众号订阅用户提供独特的增值服务。

部署

使用funcli可以实现快捷的函数计算部署,下面是一个配置文件demo
template.yml

  1. ROSTemplateFormatVersion: '2015-09-01'
  2. Transform: 'Aliyun::Serverless-2018-04-03'
  3. Resources:
  4. wx.roughwin.com:
  5. Type: 'Aliyun::Serverless::CustomDomain'
  6. Properties:
  7. Protocol: HTTP,HTTPS
  8. CertConfig:
  9. CertName: 'Cert1'
  10. PrivateKey: './cert/privkey.pem'
  11. Certificate: './cert/cert.pem'
  12. RouteConfig:
  13. routes:
  14. '/*':
  15. ServiceName: app
  16. FunctionName: api
  17. app:
  18. Type: 'Aliyun::Serverless::Service'
  19. Properties:
  20. Description: This is app service
  21. Role: 'acs:ram::1234:role/fc-log-role'
  22. LogConfig:
  23. Project: logtest
  24. Logstore: backend-log
  25. VpcConfig:
  26. VpcId: vpc-123
  27. VSwitchIds:
  28. - vsw-123
  29. SecurityGroupId: sg-123
  30. InternetAccess: true
  31. api:
  32. Type: 'Aliyun::Serverless::Function'
  33. Properties:
  34. Initializer: index.init
  35. InitializationTimeout: 3
  36. Handler: index.handler
  37. Runtime: nodejs8
  38. Timeout: 10
  39. MemorySize: 128
  40. CodeUri: ./web
  41. Events:
  42. httpTrigger:
  43. Type: HTTP
  44. Properties:
  45. AuthType: ANONYMOUS
  46. Methods:
  47. - GET
  48. - POST

总结

基于函数计算、Koajs和表格存储搭建的微信公众号后台服务,可以做到成本0元起、无需运维、易开发、灵活部署、安全稳定等众多优势。
serverless是为云计算而生的一种技术架构,优势明显,前景不错。目前各大云厂商提供的serverless技术方案都已经非常成熟、易用。对创业团队来说,将现有技术和业务迁移到serverless已经完全可行。serverless的成本、效率优势也十分可观。