FBI 警告:这是一篇极具参考价值的文章,它利用免费架构,将 GraphQL 请求响应加快到令人发指的程度,而且具有一个反常的特性,那就是访问越频繁,流量分布越宽广,其整体性能越好。

极具参考价值

因为网上的文档极少或者完全没有,而仅存的少量文档在关键部分却一笔带过,含糊其辞,对于我这样的小白极不友好!


免费架构的反常特性

一般的应用服务,访问量一大,其性能急剧下降。这是因为一般的应用,使用了服务器,不仅昂贵,而且扛不住大流量。免费架构其实就是利用了 CDN,而这个反常特性,其实是 CDN 的正常特性。

问题背景


前面几篇文章都是针对万能 BFF,其中的一能,就是利用 gatsby-source-yuque 插件,将语雀文章 GraphQL 服务化。但是这个插件的实现是非常简单粗暴的,即一篇一篇下载语雀文章,并保存为一个 json 文件。由于它的本来目的是服务于静态站点的生成,即只在站点构建阶段运行,实际运行时是很快的。当新增语雀文章时,通过 webhook 触发站点重新构建,生成全新的站点,所以简单粗暴的实现并没有问题。

但是万能 BFF 把它的使用场景扩大了,比如给小程序提供服务。由于小程序需要审核,不像 Web 站点发布那般自由,于是需要将这个服务动态化,从而在不需要发布新的小程序的前提下,也能在小程序里看到最新的文章。

扫码_搜索联合传播样式-微信标准绿版.png

免费架构薅了 AWS Lambda 的羊毛,将万能 BFF 部署在 AWS lambda 上,于是让原本就慢的服务雪上加霜,因为 lambda 的冷启动本身就很耗时。另外因为小程序需要连接备案域名的问题,采用了代理服务绕过,而这个代理服务也是免费的 Heroku 服务,也存在冷启动问题,因此是三慢合一

免费架构的问题解决思路


如果说优化 gatsby-source-yuque 插件,让其性能提升,是有很多办法的,比如存数据库,增量拉取更新、用 Redis 做一层缓存等等。

但这都不是免费架构的问题解决思路。

FBI 警告:什么是架构?如果通过代码优化提升系统性能,可能会遮盖架构的光辉。好的架构,就是在烂代码的前提下,提升系统的整体表现。

再说,数据库、Redis 等等资源成本是我等穷困程序员所不能接受的,更别提开发成本了。

免费架构准备绕过所有后端优化,直接将 GraphQL 响应放在 CDN 边缘节点上,不仅节省了后端资源成本,而且其性能也是秒杀所有后端优化方案。

免费架构的实现细节

利用 Cloudflare 的全球 CDN 网络,加上其页面规则,在 GraphQL 服务被第一次请求时被缓存在离用户最近的边缘服务器,从而使该用户周围的用户收益,在发起请求时直接从边缘节点获取到响应,实现页面秒开效果。当有新的语雀文章更新时,可以利用 Cloudflare 的 API 删除缓存。

image.png

免费架构的缺点

仍需少量开发


后面的详细步骤会讲解

不解决第一次请求速度问题


第一次请求会很慢,这只能通过后端代码优化解决。

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 typescript 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 ONEDAYIN_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 typescript 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 网关的自定义域名功能(极具参考价值!)


看过前面几篇文章的同学会了解,免费架构为了求快,在解决小程序的安全域名要求备案的限制时,使用了代理方案。而且代理的代码很粗糙,存在被滥用的风险,因此这里去掉它,这是为了安全。

在小程序里接入 GraphQL

image.png

但是,更重要的是,我们需要加快响应速度,要知道这个代理也是一个免费服务,当冷启动时,是非常慢的,再加上 AWS lambda 的冷启动,以及这个插件本身的慢,所以是三慢合一

要去掉它,就需要将自定义域名指向 AWS lambda 自动生成的长长的域名。

极具参考价值,主要指这里。因为要利用 Cloudflare 的 CDN 网络和页面规则,自然,这个自定义域名需要交给 Cloudflare 托管。但是如何将 Cloudflare 托管的域名,指向 AWS lambda,文档少,关键步骤缺失。这让小白我,通过长时间的试错,才最终成功。

开通自定义域名

在使用 serverless 部署好 lambda 后,会自动生成相关的 API 网关。但是并未开通自定义域名,需要另外去开通:
image.png
这里的难点是申请证书。

验证域名


申请证书前,需要验证域名。
image.png
选择 DNS 验证
image.png
然后根据提示,在 Cloudflare 的控制面板添加相应的 CNAME 以及 CAA 记录完成验证。
image.png
注意!这里的 CAA 记录,请参考如上截图全部加上,而不要相信 AWS 文档里说的只需要添加一种!更要注意 issuewild 和 issue 记录各添加 4 个!

在域名验证验证通过后,才可以进行下一步。

证书导入


AWS 控制面板提供了两种证书申请方式,即 AWS 颁发,或者自行导入。这里选择自行导入证书。然后就进入了非常迷惑的面板:

image.png

我们准备导入的是 Cloudflare 的证书,证书正文和私钥,都可以轻易地从 Cloudflare 控制面板获取。
image.png
点击创建证书后,通过下载即可以获取到证书正文和私钥,分别贴入 AWS ACM 控制面板。让人傻眼的是这个证书链,没有任何文档说明如何获取这个证书链。

通过各种信息的拼凑和反复试错,以下是正确的获取姿势:

证书链

虽然 AWS 控制面板上说这是可选项,但是没有的话,根本导入不了。

证书链需要将上一步生成的源服务器证书内容,和 Cloudflare 根证书文本内容,拼在一起,并且要注意顺序!

从这个链接获取 Cloudflare 根证书的内容(RSA 格式):https://developers.cloudflare.com/ssl/e2b9968022bf23b071d95229b5678452/origincarsa_root.pem

shell -----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 控制面板,注意上下顺序!

shell -----BEGIN CERTIFICATE----- Cloudflare 根证书内容 -----END CERTIFICATE-----

-----BEGIN CERTIFICATE----- 源服务器证书内容 -----END CERTIFICATE-----


导入成功后,你就能在自定义域名开通面板选择到它了!

三、设置域名 CNAME 记录指向 API 网关的自定义域名


注意是 CNAME 到终端节点配置显示的 API Gateway 域名:

image.png
image.png

四、Cloudflare 页面 SSL 规则

注意需要设置这个新的 CNAME 域名所有的路径(*)的 SSL 规则为“完全”,否则,直接访问会报错。
image.png

五、Cloudflare 页面缓存规则

经历了以上九九八十一难,你的自定义域名终于通了,也就是说,可以通过你的已备案域名访问到 AWS lambda 的服务了!

这个时候,不要忘记了我们的初衷,我们费了这么大劲,是要给 GraphQL 响应加上 CDN 缓存!

这很简单,再加上两个规则即可,对于缓存级别,选择缓存所有内容;对于 TTL,选最长的。因为对于这个使用场景,是不需要它过期的,但是最长只能选到一个月。当有语雀文章更新时,是可以通过 Cloudflare API 主动清除缓存的。

image.png

大功告成!

总结

少量开发(大量配置,好在一劳永逸)结合少量费用(基本免费),将极其缓慢的接口响应,变得快得不能再快,这就是免费架构!

通过少量开发使得原本的 GraphQL HTTP Post 请求转变为 GET 请求,从而可以利用 Cloudflare 的页面规则来实现 CDN 缓存;又通过 API Gateway 的自定义域名,去掉了代理服务,最终将三慢合一中的两慢解决掉了,实现了小程序页面的秒开效果!