Free Arch:给 GraphQL 增加 CDN 缓存
FBI 警告:这是一篇极具参考价值的文章,它利用免费架构,将 GraphQL 请求响应加快到令人发指的程度,而且具有一个反常的特性,那就是访问越频繁,流量分布越宽广,其整体性能越好。
极具参考价值
因为网上的文档极少或者完全没有,而仅存的少量文档在关键部分却一笔带过,含糊其辞,对于我这样的小白极不友好!
免费架构的反常特性
一般的应用服务,访问量一大,其性能急剧下降。这是因为一般的应用,使用了服务器,不仅昂贵,而且扛不住大流量。免费架构其实就是利用了 CDN,而这个反常特性,其实是 CDN 的正常特性。
问题背景
前面几篇文章都是针对万能 BFF,其中的一能,就是利用 gatsby-source-yuque 插件,将语雀文章 GraphQL 服务化。但是这个插件的实现是非常简单粗暴的,即一篇一篇下载语雀文章,并保存为一个 json 文件。由于它的本来目的是服务于静态站点的生成,即只在站点构建阶段运行,实际运行时是很快的。当新增语雀文章时,通过 webhook 触发站点重新构建,生成全新的站点,所以简单粗暴的实现并没有问题。
但是万能 BFF 把它的使用场景扩大了,比如给小程序提供服务。由于小程序需要审核,不像 Web 站点发布那般自由,于是需要将这个服务动态化,从而在不需要发布新的小程序的前提下,也能在小程序里看到最新的文章。
免费架构薅了 AWS Lambda 的羊毛,将万能 BFF 部署在 AWS lambda 上,于是让原本就慢的服务雪上加霜,因为 lambda 的冷启动本身就很耗时。另外因为小程序需要连接备案域名的问题,采用了代理服务绕过,而这个代理服务也是免费的 Heroku 服务,也存在冷启动问题,因此是三慢合一!
免费架构的问题解决思路
如果说优化 gatsby-source-yuque 插件,让其性能提升,是有很多办法的,比如存数据库,增量拉取更新、用 Redis 做一层缓存等等。
但这都不是免费架构的问题解决思路。
FBI 警告:什么是架构?如果通过代码优化提升系统性能,可能会遮盖架构的光辉。好的架构,就是在烂代码的前提下,提升系统的整体表现。
再说,数据库、Redis 等等资源成本是我等穷困程序员所不能接受的,更别提开发成本了。
免费架构准备绕过所有后端优化,直接将 GraphQL 响应放在 CDN 边缘节点上,不仅节省了后端资源成本,而且其性能也是秒杀所有后端优化方案。
免费架构的实现细节
利用 Cloudflare 的全球 CDN 网络,加上其页面规则,在 GraphQL 服务被第一次请求时被缓存在离用户最近的边缘服务器,从而使该用户周围的用户收益,在发起请求时直接从边缘节点获取到响应,实现页面秒开效果。当有新的语雀文章更新时,可以利用 Cloudflare 的 API 删除缓存。
免费架构的缺点
仍需少量开发
后面的详细步骤会讲解
不解决第一次请求速度问题
第一次请求会很慢,这只能通过后端代码优化解决。
FBI 警告:如果你打开“哈德韦”小程序,碰到了十几秒才看到文章的话,那么我感谢你,因为你的宝贵时间没有浪费,提高了你周围很多小伙伴的用户体验以及你后续再次访问的速度。
给 GraphQL 增加 CDN 缓存的具体步骤
一、少量开发:启用 APQ
因为 GraphQL 本质上是一个 HTTP POST 请求,通过启用 APQ,能够将缓存过的请求,转为 GET 请求。从而为后面利用 Cloudflare 设置页面规则(GET 请求)埋下了伏笔。
服务器端:
https://github.com/Jeff-Tian/serverless-space/blob/c566d9ca16913952142d6c9caae07e2a130319b3/src/app.module.ts?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L15
import {Module} from '@nestjs/common'
import {GraphQLModule} from '@nestjs/graphql'
import {ApolloServerPluginCacheControl, ApolloServerPluginLandingPageLocalDefault} from 'apollo-server-core'
import {CatsModule} from "./cats/cats.module"
import {RecipesModule} from "./recipes/recipes.module"
import {YuqueModule} from './yuque/yuque.module'
const ONE_DAY_IN_SECONDS = 60 * 60 * 24
@Module({
imports: [CatsModule, RecipesModule, YuqueModule, GraphQLModule.forRoot({
autoSchemaFile: true,
sortSchema: true,
playground: false,
// 这里!
persistedQueries: {
ttl: ONE_DAY_IN_SECONDS
},
plugins: [ApolloServerPluginLandingPageLocalDefault(), ApolloServerPluginCacheControl({defaultMaxAge: ONE_DAY_IN_SECONDS})]
})],
})
export class AppModule {
}
小程序端
https://github.com/Jeff-Tian/weapp/blob/94c0a22ab54b579ec6991e33c3a8d327bd0f31d8/src/apollo-client.ts?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L27
import {ApolloClient, ApolloLink, createHttpLink, InMemoryCache} from "@apollo/client"
import Taro from "@tarojs/taro"
import crypto from 'crypto'
import {createPersistedQueryLink} from "@apollo/client/link/persisted-queries"
const graphQLServerUrl = 'https://sls.pa-ca.me/nest/graphql'
const httpLink = createHttpLink({
uri: graphQLServerUrl,
async fetch(url, options) {
console.log('url = ', url, options)
const res = await Taro.request({
url: url.toString(),
method: (options?.method || 'POST') as 'POST' | 'GET',
header: {
'content-type': 'application/json'
},
data: options?.body,
success: console.log
})
return {text: async () => JSON.stringify(res.data)} as any
}
})
const queryLink = createPersistedQueryLink({
useGETForHashedQueries: true,
sha256: async (document: string) => crypto.createHash('sha256').update(document).digest('hex')
})
export const client = new ApolloClient({
link: ApolloLink.from([queryLink, httpLink]),
cache: new InMemoryCache()
})
二、去掉代理、启用 AWS API 网关的自定义域名功能(极具参考价值!)
看过前面几篇文章的同学会了解,免费架构为了求快,在解决小程序的安全域名要求备案的限制时,使用了代理方案。而且代理的代码很粗糙,存在被滥用的风险,因此这里去掉它,这是为了安全。
但是,更重要的是,我们需要加快响应速度,要知道这个代理也是一个免费服务,当冷启动时,是非常慢的,再加上 AWS lambda 的冷启动,以及这个插件本身的慢,所以是三慢合一!
要去掉它,就需要将自定义域名指向 AWS lambda 自动生成的长长的域名。
极具参考价值,主要指这里。因为要利用 Cloudflare 的 CDN 网络和页面规则,自然,这个自定义域名需要交给 Cloudflare 托管。但是如何将 Cloudflare 托管的域名,指向 AWS lambda,文档少,关键步骤缺失。这让小白我,通过长时间的试错,才最终成功。
开通自定义域名
在使用 serverless 部署好 lambda 后,会自动生成相关的 API 网关。但是并未开通自定义域名,需要另外去开通:
这里的难点是申请证书。
验证域名
申请证书前,需要验证域名。
选择 DNS 验证
然后根据提示,在 Cloudflare 的控制面板添加相应的 CNAME 以及 CAA 记录完成验证。
注意!这里的 CAA 记录,请参考如上截图全部加上,而不要相信 AWS 文档里说的只需要添加一种!更要注意 issuewild 和 issue 记录各添加 4 个!
在域名验证验证通过后,才可以进行下一步。
证书导入
AWS 控制面板提供了两种证书申请方式,即 AWS 颁发,或者自行导入。这里选择自行导入证书。然后就进入了非常迷惑的面板:
我们准备导入的是 Cloudflare 的证书,证书正文和私钥,都可以轻易地从 Cloudflare 控制面板获取。
点击创建证书后,通过下载即可以获取到证书正文和私钥,分别贴入 AWS ACM 控制面板。让人傻眼的是这个证书链,没有任何文档说明如何获取这个证书链。
通过各种信息的拼凑和反复试错,以下是正确的获取姿势:
证书链
虽然 AWS 控制面板上说这是可选项,但是没有的话,根本导入不了。
证书链需要将上一步生成的源服务器证书内容,和 Cloudflare 根证书文本内容,拼在一起,并且要注意顺序!
从这个链接获取 Cloudflare 根证书的内容(RSA 格式):https://developers.cloudflare.com/ssl/e2b9968022bf23b071d95229b5678452/origin_ca_rsa_root.pem
-----BEGIN CERTIFICATE-----
MIIEADCCAuigAwIBAgIID+rOSdTGfGcwDQYJKoZIhvcNAQELBQAwgYsxCzAJBgNV
BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91
ZEZsYXJlIE9yaWdpbiBTU0wgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQH
Ew1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMB4XDTE5MDgyMzIx
MDgwMFoXDTI5MDgxNTE3MDAwMFowgYsxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBD
bG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91ZEZsYXJlIE9yaWdpbiBTU0wg
Q2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMw
EQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAwEiVZ/UoQpHmFsHvk5isBxRehukP8DG9JhFev3WZtG76WoTthvLJFRKFCHXm
V6Z5/66Z4S09mgsUuFwvJzMnE6Ej6yIsYNCb9r9QORa8BdhrkNn6kdTly3mdnykb
OomnwbUfLlExVgNdlP0XoRoeMwbQ4598foiHblO2B/LKuNfJzAMfS7oZe34b+vLB
yrP/1bgCSLdc1AxQc1AC0EsQQhgcyTJNgnG4va1c7ogPlwKyhbDyZ4e59N5lbYPJ
SmXI/cAe3jXj1FBLJZkwnoDKe0v13xeF+nF32smSH0qB7aJX2tBMW4TWtFPmzs5I
lwrFSySWAdwYdgxw180yKU0dvwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYD
VR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUJOhTV118NECHqeuU27rhFnj8KaQw
HwYDVR0jBBgwFoAUJOhTV118NECHqeuU27rhFnj8KaQwDQYJKoZIhvcNAQELBQAD
ggEBAHwOf9Ur1l0Ar5vFE6PNrZWrDfQIMyEfdgSKofCdTckbqXNTiXdgbHs+TWoQ
wAB0pfJDAHJDXOTCWRyTeXOseeOi5Btj5CnEuw3P0oXqdqevM1/+uWp0CM35zgZ8
VD4aITxity0djzE6Qnx3Syzz+ZkoBgTnNum7d9A66/V636x4vTeqbZFBr9erJzgz
hhurjcoacvRNhnjtDRM0dPeiCJ50CP3wEYuvUzDHUaowOsnLCjQIkWbR7Ni6KEIk
MOz2U0OBSif3FTkhCgZWQKOOLo1P42jHC3ssUZAtVNXrCk3fw9/E15k8NPkBazZ6
0iykLhH1trywrKRMVw67F44IE8Y=
-----END CERTIFICATE-----
然后和自己生成的源服务器证书内容拼在一起粘贴到 AWS ACM 控制面板,注意上下顺序!
-----BEGIN CERTIFICATE-----
Cloudflare 根证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
源服务器证书内容
-----END CERTIFICATE-----
导入成功后,你就能在自定义域名开通面板选择到它了!
三、设置域名 CNAME 记录指向 API 网关的自定义域名
注意是 CNAME 到终端节点配置显示的 API Gateway 域名:
四、Cloudflare 页面 SSL 规则
注意需要设置这个新的 CNAME 域名所有的路径(*)的 SSL 规则为“完全”,否则,直接访问会报错。
五、Cloudflare 页面缓存规则
经历了以上九九八十一难,你的自定义域名终于通了,也就是说,可以通过你的已备案域名访问到 AWS lambda 的服务了!
这个时候,不要忘记了我们的初衷,我们费了这么大劲,是要给 GraphQL 响应加上 CDN 缓存!
这很简单,再加上两个规则即可,对于缓存级别,选择缓存所有内容;对于 TTL,选最长的。因为对于这个使用场景,是不需要它过期的,但是最长只能选到一个月。当有语雀文章更新时,是可以通过 Cloudflare API 主动清除缓存的。
大功告成!
总结
少量开发(大量配置,好在一劳永逸)结合少量费用(基本免费),将极其缓慢的接口响应,变得快得不能再快,这就是免费架构!
通过少量开发使得原本的 GraphQL HTTP Post 请求转变为 GET 请求,从而可以利用 Cloudflare 的页面规则来实现 CDN 缓存;又通过 API Gateway 的自定义域名,去掉了代理服务,最终将三慢合一中的两慢解决掉了,实现了小程序页面的秒开效果!