记一次从LNMP迁移到Serverless
Samersions ZHONG Lv1

构想

图1 最早的LNMP方案
笔者之前的一个私人微博,来源于少数派一个网友的设计。该网友通过LeanCloud作为私人微博的后端,而将前端托管在对象存储或类似于GitHub Pages的静态页面托管服务上。而两年前笔者部署该私人微博时,考虑到LeanCloud这类BaaS服务的免费订阅版对请求数量和存储数量的限制,而为该私人微博搞了一个后端页面,并将前端和后端统一放置在一台VPS的LNMP里面。当时整个服务的访问架构图如图1所示,VPS内部的NGINX、PHP Driver和MySQL通过LNMP直接安装到系统上,是一种非常古旧的部署方案。

图2 升级的DNMP方案
后来过了半年,笔者想引入一些稍微时尚的架构,便尝试将LNMP的组件分别容器化,最终形成了如图2所示的容器化部署方案。该方案将NGINX、PHP Driver、MySQL分别运行在不同的Docker容器中,容器间通过Docker网关相互连接。由于MySQL容器只与PHP Driver容器连接,不暴露到主机内,具有一定的安全性。而NGINX和PHP Driver则通过另外的网关连接。虽然使用了容器化部署,但该方案中仍将多个站点在同一个NGINX容器内运行,相当于只是将各个服务容器化而并非根据应用程序容器化,感觉像一种四不像的折中方案。

这个私人微博经过两年的运行,用户量趋于稳定。而笔者经过两年的观察,感觉无服务计算才是未来前进的方向,遂萌生将此私人微博迁移到某种Serverless服务上进行托管。最开始考虑使用原作者使用的LeanCloud作为后端,并将前端直接托管到GitHub Pages上。但LeanCloud的国内版需要备案使用,而国际版的价格页面整个都消失不见了,不知是否已经不打算细心维护了。笔者又考察了一下Parse,发现它只展示了它开源的BaaS管理平台而没有提供BaaS服务。

最后笔者只能把目光转移到云计算平台提供的数据存储服务上,对于前端与数据存储服务的访问交互,笔者设想的方案有两种:

  1. 前端直接访问云平台提供的数据存储服务,对数据存储服务的访问控制则利用云平台提供的用户和权限控制功能实现;
  2. 使用云平台提供的云函数功能与数据存储服务交互,前端则通过云函数获取数据,权限控制由云函数处理。

笔者一开始倾向于使用方案1,因为大多数的云计算平台的云函数有定量的免费额度,而对于用户和权限控制功能则采取无限量免费供应的方式。因此单独使用数据存储服务可以避免对云函数的消费。因此笔者注册了AWS的试用,其IAM等用户和权限管理功能处于可长期免费的状态,而Amazon DynamoDB和Amazon Lambda也是可以长期免费使用。原本的打算是使用Amazon DynamoDB存储数据,并利用IAM和Amazon Cognito对用户和角色权限进行精心的配置使得前端能安全地访问Amazon DynamoDB的数据。但笔者小看了AWS服务的复杂性,光是配置IAM和Cognito就有无穷无尽的文档,更不用说对Cognito配置还涉及到Amazon SES和SMS等邮件和电话服务的配置,笔者看了看厚厚的文档,感觉一天之内想完成迁移可能性不大,于是萌生起更换其他平台的打算。

Cloudflare Worker

笔者决定使用方案2,并最后选择Cloudflare Worker,Cloudflare本身财大气粗,对Worker提供的免费额度是每天10万次请求,而Amazon Lambda的免费额度是每月100万次请求。而Cloudflare提供了Cloudflare Worker KV这一个KV Store,刚好可以用来存储数据。Cloudflare Worker KV的免费额度是每天10万次读取、1000次增删改和列出全表,对于笔者的私人微博以访问者居多,发微博者只有笔者自己这种需求非常合适,Cloudflare的官方文档也建议Worker KV最好用于经常读取而偶尔修改的情况。

Cloudflare Worker云函数的开发十分简单,只需安装Cloudflare提供的wrangler-cli就可以进行项目的初始化、调试及发布。Cloudflare Worker目前只支持JavaScript和TypeScript开发云函数,但胜在提供的API简单,可以轻松对接Cloudflare Worker KV存储数据,详细开发过程不再赘述,用于私人微博的Worker已经发布到了GitHub,朋友们可直接使用部署。

用Cloudflare Worker的另一个好处是可以充分利用Cloudflare提供的DNS路由功能,例如可以实现访问https://example.com/时返回一个部署在其他地方的静态页面,而访问https://example.com/api时触发Cloudflare Worker,这样部署使得https://example.com/的前端页面无需跨域也可以访问Cloudflare Worker,从而避开了CORS限制。

使用Cloudflare Worker KV的一个缺陷是,它是最终一致性的,而笔者的私人微博在每次发帖时,都需要获取一次last_id键的值并将其自增值作为下一个帖子的id,同时需要获取all_ids键的值并将新增的id写入到该键中并刷新。如果笔者发帖的速度非常快,就会导致上一次更新的键值未及时同步,出现不一致的情况。好在笔者发帖的速度并不频繁,且只有笔者自己能进行写操作,同时根据观察,Cloudflare Worker KV同步的时间在一分钟之内,也就是不一致窗口时间在一分钟以内,因此此处出现的一致性问题笔者选择将其忽略。

图3 无服务方案

最终笔者的私人微博完成迁移后的流程架构图如图3所示。当用户访问https://example.com/index.html时,Cloudflare CDN会从托管了静态页面的GitHub Pages服务获取页面返回给客户端。而前端页面的JavaScript发起https://example.com/tucao的RESTful API访问时,Cloudflare DNS匹配到了该URL,并将请求重定向到Cloudflare Worker,Worker再从KV获取数据返回给前端,实现数据的访问。