边缘计算:让 CDN 成为高性能 GraphQL 网关

作者|亨睿Alibaba F2E8月26日

边缘计算:让 CDN 成为高性能 GraphQL 网关

1.前言

1.1 GraphQL 作为网关层

如果你对 GraphQL 还不了解,可以通过我们团队的讲座和文章进一布了解:

  • 官方网站
    • GraphQL 官网(https://graph啊拼音ql.cn/)
    • Apollo 官网(https://apollographql.com/)
  • 讲座与 Demo(第 3 场)
    • 用 GraphQL 控制你的特斯拉真车
      • https://www.yuque.com/zaotalk/posts/s9
  • 技术文章
      • https://zhuanlan.zhihu.com/p/115068436
    • 还在用 Redux,要不要试试 GraphQL 和 Apollo?
      • https://zhuanlan.zhihu边缘计算和云计算的区别.com/p/34238617
    • TypeScript + GraphQL = TypeGraphQL
      • https://zhuanlan.zhihu.com/p/56516614
    • 基于 GraphQL 的数据导出
      • https://zhuanlan.zhiNOSQLhu.com/p/46141806

通过我们团队 4 年的持续努力,现如今在 CCO 技术部,GraphQL 已经成为了 API 对内对外描述、暴露及调用的唯一标准。而在国外,F边缘计算应用场景acebook、Netflix、Github、P阿平赶海aserverless突然火了ypal、微软、大众、沃尔玛等企业也在大规模使用 GraphQL 中,甚至让以 GraphQL 为阿飘军人老公二爷生的 Apollo 公司成功拿下了 1.3 亿美元的 D 轮融资。在面向全球前端开发者调研问卷中,GraphQL 也成为最受关注的技术和最想学习的技术。Github 上有一份持续更新的 GraphQL 公开服务列表。

边缘计算:让 CDN 成为高性能 GraphQL 网关

我们认为 GraphQL 最适合的场景莫过于作为 BFF(Backend for Frontend)的网关褐色分泌物层,即根据客户容器端的实际需要,将后端的原始 HSF 接口、第三方 RESTful 接口进行整合和封装形成自己的 Service Faad边缘计算是什么e 层。GraphQL 自身的特性由、使得其非常容易与 RESTful、MTOP/MOPEN 等基于 HTTP 的现有网关进行集成,而另一方面,在国外很多文章中都提到 GraphQL 非常适合作为 Sjavascript数据类型erverless/FaaS 的网关层,你甚至只需要唯一一个 HTTP Trigger 就能实现代理所有背后的 API

1.2 GraphQL 网关与 CDN 边缘计算

EdgeR阿里应用中间件outine 边缘计算 是阿里云 CDN 团队推出的新一代 Server啊拼音less 计算平台容器是什么,它提供了一javascript浏览器个类似 W3C 标准的 ServiceWorker 容器,可以充分利用 CDN 遍布全球的节点空闲计算资源以及强大的加速与缓存能力,实现高可用性、高性能的分布式弹性计算,更重要的是目前对于弹内用户来说它是完全免费的,当然截止至笔者发稿时 EdgeRoutine 还处在试用阶段。Edgserverless框架eRoutine 将容器中的溶剂或易燃化学品在 8 月底 9nosql是什么意思 月初正式对外发布!

在 1.1 节中我们提到 GraphQL 非常适合作为 BFF 网关层,而结合电商后台业务的特点我们发现:

Query 类的请求占了大量的比例,缓存缓存视频合并app下载这些只读类查询请求,通常HSF响应结果在相当长的时间范围甚至是永远都不会发生变化,尽管如此,每一次 API 调用时我们还是将请求发送到了后端的应用 / 服务器上。

这让我们产生了一个全新的思路:

边缘计算:让 CDN 成为高性能 GraphQL 网关

如上图所示,将 CDN EdgeRoutine 作为 GraphQL Query 类请求的代理层,首次执行 Q穿到年代文中养娃娃uer缓存视频变成本地视频y 时,我们将请求先从 CDN 代理到 GraphQL 网关层,再通过网关层代理到实际的应用服务(例如通过 HSF 调用),然后将获容器英语得的缓存视频怎样转入本地视频返回结果缓存在阿飘重生五年前军人老公 CDN 上,之后的请求可以根据 TTL 业务规则动态决定走缓存还是去 GraphQL 网关层。这样我们可以充分利用 CD缓存视频变成本地视频N 的特性,将查询类请求分散到遍布全球的节点中,显著nosql数据库的特点降低主应用程序的 QPS。

2.移植 Apollo GraphQL Server

Apo此电脑怎么放在桌面上llo GraphQL Server 是目前使用最广泛的开源 GraphQL 服务,它的 Nod成都农商银行e.js 版本 更是被 BFF 类应用广为使用。但是遗憾的是啊拼音 apollo-sejavascript高级程序设计rver 是一个面向 Node.js 技术栈开发的项目,而前文中提到 EdgeRoutine 提供的是一个类似 Service Worker 的 Serverless 容器,因此我们首先需要做的就是将 apollo-server-core 移植到 EdgeRo中间件为上层应用提供utine 中。为此,我开发了 apollo-server-edge-routine,本章节将简述设计和实现思路。

2.1 构建 TypeSc容器是什么ript 开发环境和脚手架

首先,我们需要构建一个 EdgeRoutine 容器的 TypeScript 环境,此前我已经开发了 Edgjavascript菜鸟教程eRoutine Tyserverless使用peScript 描述和 EdgeRouti缓存视频怎样转入相册ne TypeScript 脚手架及本地模拟器(在 EdgserverlesseRoutine 正式上线后,我会开源到 Github 上),因此可以快速构建边缘计算一个本地开发环境。这里简单解释一缓存视频怎样转入相册下,我实际上是用 Service Worker 的 TypeScript 库来模拟编译时环境,同时将 Webpa容器ck 作为本地调试服务器,并用浏览器的 Serv吃掉你的信息素ice Worker 来模拟运行 edge.js 脚本,用 Webpack 的 socket 通讯实缓存视频怎样转入相册现 Hot Reload 效果。

2.2 为 EdgeRoutine 环境实现自己的 ApolloServer

Apollo 官方似乎并没有给出如何移植 Apollo Servserverless faaser 的文档,不过简单研究了一下 ApolloServerBase 的代码,不难发现其实它已经是一个功能完备的服务器了,只是缺少与 HTTP 服务器的连接。因此缓存的视频怎么保存到本地,我们只要集成该类,并实现一个自己的listen(path: string)方法javascript数据类型即可,这里的listen()方法与传统 HTTP 服务器不同,我们需容器中的溶剂或易燃化学品要指定的不缓存视频在手机哪里找port而是一个path,也就是需要侦听 GraphQL 请求的路径。下面是我实现的一个简单版本:

import { ApolloServerBase } from 'apollo-server-core';
import { handleGraphQLRequest } from './handlers';
/**
 * Apollo GraphQL Server 在 EdgeRoutine 上的实现。
 */
export class ApolloServer extends ApolloServerBase {
  /**
   * 在指定的路径上,侦听 GraphQL Post 请求。
   * @param path 指定要侦听的路径。
   */
  async listen(path = '/graphql') {
    // 如果在未调用 `start()` 方法前,错误的先使用了 `listen()` 方法,则抛出异常。
    this.assertStarted('listen');
    // addEventListenr('fetch', (FetchEvent) => void) 由 EdgeRoutine 提供。
    addEventListener('fetch', async (event: FetchEvent) => {
      // 侦听 EdgeRoutine 的所有请求。
      const { request } = event;
      if (request.method === 'POST') {
        // 只处理 POST 请求
        const url = new URL(request.url);
        if (url.pathname === path) {
          // 当路径相符合时,将请求交给 `handleGraphQLRequest()` 处理
          const options = await this.graphQLServerOptions();
          event.respondWith(handleGraphQLRequest(this, request, options));
        }
      }
    });
  }
}

接下来,我们需要实现核心的handleGraphQLRequest()方法,该方法实际上是一个通道模式,负责将 HTTP 请求转换成 Gra阿飘是什么意思phQL 请求发送到 Apollo Server,并将其返回的 GraphQL 响应转换回 HTTP 响应。Apollo 官方其实是有一个名为runHttpQuery()的类似方法,但是该方法用到了buffer等 Node.js 环境内置的javascript菜鸟教程模块,因此无法在 Service Worker 环境中编译通过。这里给出一个我自己的简单实现:

import { GraphQLOptions, GraphQLRequest } from 'apollo-server-core';
import { ApolloServer } from './ApolloServer';
/**
 * 从 HTTP 请求中解析出 GraphQL 查询并执行,再将执行的结果返回。
 */
export async function handleGraphQLRequest(
  server: ApolloServer,
  request: Request,
  options: GraphQLOptions,
): Promise<Response> {
  let gqlReq: GraphQLRequest;
  try {
    // 从 HTTP request body 中解析出 JSON 格式的请求。
    // 该请求是一个 GraphQLRequest 类型,包含 query、variables、operationName 等。
    gqlReq = await request.json();
  } catch (e) {
    throw new Error('Error occurred when parsing request body to JSON.');
  }
  // 执行 GraphQL 操作请求。
  // 当执行失败时不会抛出异常,而是返回一个包含 `errors` 的响应。
  const gqlRes = await server.executeOperation(gqlReq);
  const response = new Response(JSON.stringify({ data: gqlRes.data, errors: gqlRes.errors }), {
    // 永远确保 content-type 为 JSON 格式。
    headers: { 'content-type': 'application/json' },
  });
  // 将 GraphQLResponse 中的消息头复制到 HTTP Response 中。
  for (const [key, value] of Object.entries(gqlRes.http.headers)) {
    response.headers.set(key, value);
  }
  return response;
}

3.一个简单的天气查询 GraphQL CDN 代理网关示例

3.1 我们要做什么

在这个 Demo 里,我们假设要对第三方天气服务进行二次封装。我们将会为天气nosql是什么意思 API 网(tianqiapi.com)开发一个 G缓存视频怎样转入本地视频raphQL CDN 代理网关。天气 AP黑色风暴电视连续剧I 网对免费用户的 QPS 有一定的限制,边缘计算是什么意思每天只能 300 次查询,既然天气预报一般变化频率较低,我们假设希望在首次查询某一个城市天气的时候,将会真正访问到天气 API 网的服务,而此后的同一城市天气查询将走 CDN 缓存。

3.2 天气 API 网接口简介

天气 API 网(tianqiapi.coAPIm)对外提供商业级的天气预报服务,据说每天有千万级的 QPS。这里也可以设想一下如果它们使用 GraphQL 来定义、暴露 API 接口将会带来多大的便利性,并且都没有必要写 API 接口文档了。

根据它的官方 API 文档,我们可以通过下面的 API海上繁花电视剧免费观看 获得当前某一个城市的天气(这里以笔者所在城市南京为例):

HTTP 请求

Request URL: https://www.tianqiapi.com/free/day?appid={APP_ID}&appsecret={APP_SECRET}&city=%E5%8D%97%E4%BA%AC
Request Method: GET
Status Code: 200 OK
Remote Address: 127.0.0.1:7890
Referrer Policy: strict-origin-when-cross-origin

其中 {APP_ID} 和 {APP_SECRET} 为你申请的 API 账号。

HTTP 响应

HTTP/1.1 200 OK
Server: nginx
Date: Thu, 19 Aug 2021 06:21:45 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Content-Encoding: gzip
{
  air: "94",
  city: "南京",
  cityid: "101190101",
  tem: "31",
  tem_day: "31",
  tem_night: "24",
  update_time: "14:12",
  wea: "多云",
  wea_img: "yun",
  win: "东南风",
  win_meter: "9km/h",
  win_speed: "2级"
}

这里的命nosql数据库技术名和大小写实在要吐槽一下。

这里给出一份最简单的 API 客户端实现:

export async function fetchWeatherOfCity(city: string) {
  // URL 类在 EdgeRoutine 中有对应的实现。
  const url = new URL('http://www.tianqiapi.com/free/day');
  // 这里我们直接采用官方示例中的免费账户。
  url.searchParams.set('appid', '23035354');
  url.searchParams.set('appsecret', '8YvlPNrz');
  url.searchParams.set('city', city);
  const response = await fetch(url.toString);
  return response;
}

3.3 定义我们的 GraphQL SDL

让我们用 GraphQL SDL 语言定接下来服务器中间件是什么意思要实现接口的 Schema:

type Query {
    "查询当前 API 的版本信息。"
  versions: Versions!
    "查询指定城市的实时天气数据。"
  weatherOfCity(name: String!): Weather!
}
"""
城市信息
"""
type City {
  """
  城市的唯一标识
  """
  id: ID!
  """
  城市的名称
  """
  name: String!
}
"""
版本信息
"""
type Versions {
  """
  API 版本号。
  """
  api: String!
  """
  `graphql` NPM 版本号。
  """
  graphql: String!
}
"""
天气数据
"""
type Weather {
  "当前城市"
  city: City!
  "最后更新时间"
  updateTime: String!
  "天气状况代码"
  code: String!
  "本地化(中文)的天气状态"
  localized: String!
  "白天气温"
  tempOfDay: Float!
  "夜晚气温"
  tempOfNight: Float!
}

3.4 实现 Gra褐色分泌物是什么原因引起的phQLapi是什么 Resolvers

Resolvers 的实现思路很简单,详见注释:

import { version as graphqlVersion } from 'graphql';
import { apiVersion } from '../api-version';
import { fetchWeatherOfCity } from '../tianqi-api';
export function versions() {
  return {
    // EdgeRoutine 的部署不像 FaaS 那么及时。
    // 因此每次部署前,我都会手工的修改 `api-version.ts` 中的版本号,
    // 查询时看到 api 版本号变了,就说明 CDN 端已经部署成功了。
    api: apiVersion,
    graphql: graphqlVersion,
  };
}
export async function weatherOfCity(parent: any, args: { name: string }) {
  // 调用 API 并将返回的格式转换为 JSON。
  const raw = await fetchWeatherOfCity(args.name).then((res) => res.json());
  // 将原始的返回结果映射到我们定义的接口对象中。
  return {
    city: {
      id: raw.cityid,
      name: raw.city,
    },
    updateTime: raw.update_time,
    code: raw.wea_img,
    localized: raw.wea,
    tempOfDay: raw.tem_day,
    tempOfNight: raw.tem_night,
  };
}

3.5 创建并启动服务器

nosql与mysql的区别在我们已经有了 GraphQL 的接口大纲和 Resolvers,接下来就可以像 Node.js 里那样创建和启动我们的 Serjavascript是干什么的ver 了。

// 注意这里不再是 `import { ApolloServer } from 'apollo-server'` 了。
import { ApolloServer } from '@ali/apollo-server-edge-routine';
import { default as typeDefs } from '../graphql/schema.graphql';
import * as resolvers from '../resolvers';
// 创建我们的服务器
const server = new ApolloServer({
  // `typeDefs` 是一个 GraphQL 的 `DocumentNode` 对象。
  // `*.graphql` 文件被 `webpack-graphql-loader` 加载后就变成了 `DocumentNode` 对象。
  typeDefs,
  // 即 3.4 章节中的 Resolvers
  resolvers,
});
// 先启动服务器,然后监听,一行代码全部搞定!
server.start().then(() => server.listen());

是的,就是这么简单,创建一个server对象,然后将它启动并使其侦听指定的路径(在本例中没有传递path参数,使用的是默认的/graphql)。

到目前为止,主要的 TypeScript 和 GraphQL 代码已经全部完成了!

3.6 工程化配置

为了让 TypeScript 明白我们在 EdgeRoutine 环境中写穿到年代虐文喜当妈代码,我们需应用服务中间件要在tsconfig.json中交代libtypes

{
  "compilerOptions": {
    "alwaysStrict": true,
    "esModuleInterop": true,
    "lib": ["esnext", "webworker"],
    "module": "esnext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "preserveConstEnums": true,
    "removeComments": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext",
    "types": ["@ali/edge-routine-types"]
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

再次强调一遍,javascript面试题与 Serverless / FaaS 不同,我们的程序并不是跑在 Node.js 环境中,而是跑在类似 Serv黑色风暴电视连续剧iceWorker 环境 中。从 Webpack 5 开始,在browser目标环境中不再会自动注入 Node.js 内置模块的 polyfills,因此在 Webpack 的配置中我们需要手工加上:

{
  ...
  resolve: {
      fallback: {
      assert: require.resolve('assert/'),
      buffer: require.resolve('buffer/'),
      crypto: require.resolve('crypto-browserify'),
      os: require.resolve('os-browserify/browser'),
      stream: require.resolve('stream-browserify'),
      zlib: require.resolve('browserify-zlib'),
      util: require.resolve('util/'),
    },
    ...
  }
  ...
}

当然,你还需要手工安装包括assertbuffercrypto-browserifyos-browserify容器中有某种酒精含量的酒精溶液stream-browserifybrowsnosql数据库的特点erify-zlibutil等在内的 polyfills 包。

3.7 添加 CDN 缓存

最后,让我们把 CDN 缓存加上,javascript是干什么的由于 EdgeRoutine 在笔者截稿前还处于 beta 阶段,因此我们只能用 Experimental 的 API 来实现缓存,让我们重新实现一下fetchWeatherOfCity()方法。

export async function fetchWeatherOfCity(city: string) {
  const url = new URL('http://www.tianqiapi.com/free/day');
  url.searchParams.set('appid', '23035354');
  url.searchParams.set('appsecret', '8YvlPNrz');
  url.searchParams.set('city', city);
  const urlString = url.toString();
  if (isCacheSupported()) {
    const cachedResponse = await cache.get(urlString);
    if (cachedResponse) {
      return cachedResponse;
    }
  }
  const response = await fetch(urlString);
  if (isCacheSupported()) {
    cache.put(urlString, response);
  }
  return response;
}

在全局(globalT容器云his)中提供的 cache 对象,本质上是一个通过 Swift 实现的nosql指的是缓存器,它的键必须是一个 HTTP Request 对象或一个 H穿到年代虐文喜当妈TTP 协议(非 HTTPS)的 URL 字符串,而值必须是一个 HTTP Response 对象(可以来自fetch()方法)。虽然 EdgeRoutine 的 Serverless 程序每隔几分钟或者 1 小时就会重启,我们的全局变量会随之销毁,但是有了cahce对象的帮助,可以帮我们实现 CDN 级别的serverless入门缓存。

在阿里云的 EdgeRoutine KV 数据库上线后阿飘,我们会更新这个示例,实现更强大的缓存服务器中间件是什么意思。遗憾的是,截止至笔者发稿时该功能暂未上线,本人十分期待!

3.8 添加 Playground 调试器

为了更好的调试 GrcdnaphQL 我们还可以添加一个官方的 Playground 调试器,它是一个单页面应用,因此我们可以通过 Webpack 的html-loader加载进来。

addEventListener('fetch', (event) => {
  const response = handleRequest(event.request);
  if (response) {
    event.respondWith(response);
  }
});
function handleRequest(request: Request): Promise<Response> | void {
  const url = new URL(request.url);
  const path = url.pathname;
  // 为了方便调试,我们把所有对 `/graphql` 的 GET 请求都处理为返回 playground。
  // 而 POST 请求则为实际的 GraphQL 调用
  if (request.method === 'GET' && path === '/graphql') {
    return Promise.resolve(new Response(rawPlaygroundHTML, { status: 200, headers: { 'content-type': 'text/html' } }));
  }
}

最后让我们在浏览器中访问/graphql,看到的就是下面的界面:

在其中输入一段查询语句:

query CityWeater($name: String!) {
  versions {
    api
    graphql
  }
  weatherOfCity(name: $name) {
    city {
      id
      name
    }
    code
    updateTime
    localized
    tempOfDay
    tempOfNight
  }
}

Variables设置为{ "name": "杭州" },点击中间的 Play 按钮即可。

3.9 完整的项目代码

在 Edgnosql数据库技术eRoutine 正式发布后,我会将上述 NPM 包和 Demo 在 我缓存视频怎样转入相册的 Github 上开源。

4.面向未缓存视频合并app下载

在这个简单的公开示例中,我们没有办法完整的演示如何将 EdgeRoutine缓存视频在手机哪里找 作为 GraphQL 网关的二消息中间件应用场景级代理网关,你可以访问 graphcdn.io 通过视频了解更多关于 GrpahQL CDN 网关的信息。在可预见的将来,换身份证需要什么材料我们nosql数据库与关系型数据库的区别将利用 CDN 的边缘 KV 数据库实现对 Query 结果的缓存,并通过对 GraphQL 的语法解析和单类型中 ID 唯一的特性实现当发生 Mujavascript菜鸟教程tati缓存视频在手机哪里找ons 时,自动使相关数据实体的缓存失效。