Making CDNs a High-Performance GraphQL Gateway

Introduction: We believe that GraphQL is the most suitable scenario as the gateway layer of BFF (Backend for Frontend), that is, according to the actual needs of the client, the original HSF interface and third-party RESTful interface of the back-end are integrated and encapsulated to form its own Service Façade layer . GraphQL's own characteristics make it very easy to integrate with existing HTTP-based gateways such as RESTful and MTOP/MOPEN. On the other hand, many foreign articles have mentioned that GraphQL is very suitable as a gateway layer for Serverless/FaaS. You even only need a single HTTP Trigger to proxy all the APIs behind it.



1. [Making CDNs a High-Performance GraphQL Gateway]Preface



1.1 GraphQL as a gateway layer

If you are not familiar with GraphQL, you can learn more about it through our team's lectures and articles:

•Official website
oGraphQL official website ( https://graphql.cn/ )
oApollo official website ( https://apollographql.com/ )
•Lectures and Demos (Session 3)
oControl your real Tesla car with GraphQL
https://www.yuque.com/zaotalk/posts/s9
•technical article
oTalk about GraphQL and Apollo's workflow
https://zhuanlan.zhihu.com/p/115068436
oStill using Redux, why not try GraphQL and Apollo?
https://zhuanlan.zhihu.com/p/34238617
oTypeScript + GraphQL = TypeGraphQL
https://zhuanlan.zhihu.com/p/56516614
oGraphQL-based data export
https://zhuanlan.zhihu.com/p/46141806

Through the continuous efforts of our team for 4 years, now in the CCO technical department, GraphQL has become the only standard for API internal and external description, exposure and invocation. In foreign countries, companies such as Facebook, Netflix, Github, Paypal, Microsoft, Volkswagen, and Walmart are also using GraphQL on a large scale , and even Apollo, which makes a living with GraphQL, successfully won a $130 million Series D financing. In the survey questionnaire for global front-end developers, GraphQL has also become the most concerned technology and the most wanted to learn technology. There is a continuously updated list of GraphQL exposed services on Github.




We believe that GraphQL is the most suitable scenario as the gateway layer of BFF (Backend for Frontend), that is, according to the actual needs of the client, the original HSF interface and third-party RESTful interface of the backend are integrated and encapsulated to form its own Service Façade layer. GraphQL's own characteristics make it very easy to integrate with existing HTTP-based gateways such as RESTful and MTOP/MOPEN. On the other hand, many foreign articles have mentioned that GraphQL is very suitable as a gateway layer for Serverless/FaaS. You even only need a single HTTP Trigger to proxy all the APIs behind it.

1.2 GraphQL Gateway and CDN Edge Computing

EdgeRoutine is a new-generation serverless computing platform launched by the Alibaba Cloud CDN team. It provides a ServiceWorker container similar to the W3C standard. High-performance distributed elastic computing, and more importantly, it is completely free for users in the bullet. Of course, as of the time of writing, EdgeRoutine is still in the trial stage. EdgeRoutine will be officially released at the end of August and early September!

In Section 1.1, we mentioned that GraphQL is very suitable as a BFF gateway layer, and combined with the characteristics of the e-commerce backend business, we found that:

Query class requests account for a large proportion, and these read-only class query requests, usually the response results will not change for a long time range or even forever, despite this, we still send the request to the API for each API call. On the backend application/server.

This gave us a whole new idea:



As shown in the figure above, the CDN EdgeRoutine is used as the proxy layer for the GraphQL Query class request. When Query is executed for the first time, we will first proxy the request from the CDN to the GraphQL gateway layer, and then proxy it to the actual application service through the gateway layer (for example, through HSF call) , and then cache the obtained return results on the CDN, and subsequent requests can dynamically decide to go to the cache or to the GraphQL gateway layer according to the TTL business rules. In this way, we can make full use of the characteristics of CDN, and distribute query requests to nodes all over the world, which significantly reduces the QPS of the main application.


2.[ Making CDNs a High-Performance GraphQL Gateway] Migrating Apollo GraphQL Server




Apollo GraphQL Server is currently the most widely used open source GraphQL service, and its Node.js version is widely used by BFF applications. But unfortunately apollo-server is a project for Node.js technology stack development, and as mentioned above, EdgeRoutine provides a Serverless container similar to Service Worker, so the first thing we need to do is to transplant apollo-server-core into EdgeRoutine. To this end, I developed apollo-server-edge-routine, this chapter will briefly describe the design and implementation ideas.

2.1 Build the TypeScript development environment and scaffolding

First, we need to build a TypeScript environment for the EdgeRoutine container. I have already developed the EdgeRoutine TypeScript description and EdgeRoutine TypeScript scaffolding and local simulator (I will open source it to Github after EdgeRoutine is officially launched), so I can quickly build a local development environment. Here's a brief explanation, I actually use the TypeScript library of Service Worker to simulate the compile-time environment, and use Webpack as a local debugging server, and use the browser's Service Worker to simulate running edge.js scripts, and use Webpack's socket communication to implement Hot Reload effect.

2.2 Implement your own ApolloServer for the EdgeRoutine environment

Apollo official does not seem to give documentation on how to transplant Apollo Server, but after a brief study of the code of ApolloServerBase, it is not difficult to find that it is already a fully functional server, but it lacks the connection to the HTTP server. Therefore, we only need to integrate this class and implement our own listen(path: string) method. The listen() method here is different from the traditional HTTP server. What we need to specify is not a port but a path , that is, we need to detect The path to listen to for GraphQL requests. Here is a simple version of my implementation:


import { ApolloServerBase } from 'apollo-server-core';
import { handleGraphQLRequest } from './handlers';
/**
* Implementation of Apollo GraphQL Server on EdgeRoutine.
*/
export class ApolloServer extends ApolloServerBase {
/**
* On the specified path, listen for GraphQL Post requests.
* @param path Specifies the path to listen on.
*/
async listen(path = '/graphql') {
// Throws an exception if the `listen()` method is incorrectly used before the `start()` method is called.
this.assertStarted('listen');
// addEventListenr('fetch', (FetchEvent) => void) provided by EdgeRoutine.
addEventListener('fetch', async (event: FetchEvent) => {
// Listen for all requests from EdgeRoutine.
const { request } = event;
if (request.method === 'POST') {
// only handle POST requests
const url = new URL(request.url);
if (url.pathname === path) {
// When the path matches, pass the request to `handleGraphQLRequest()` for processing
const options = await this.graphQLServerOptions();
event.respondWith(handleGraphQLRequest(this, request, options));
}
}
});
}
}

Next, we need to implement the core handleGraphQLRequest() method, which is actually a channel pattern responsible for converting HTTP requests into GraphQL requests to Apollo Server, and converting the GraphQL responses it returns back into HTTP responses. Apollo officially has a similar method called runHttpQuery() , but this method uses the built-in modules of the Node.js environment such as buffer , so it cannot be compiled in the Service Worker environment. Here is a simple implementation of my own:


import { GraphQLOptions, GraphQLRequest } from 'apollo-server-core';
import { ApolloServer } from './ApolloServer';
/**
* Parse the GraphQL query from the HTTP request, execute it, and return the execution result.
*/
export async function handleGraphQLRequest(
server: ApolloServer,
request: Request,
options: GraphQLOptions,
): Promise {
let gqlReq: GraphQLRequest;
try {
// Parse the request in JSON format from the HTTP request body.
// The request is a GraphQLRequest type, containing query, variables, operationName, etc.
gqlReq = await request.json();
} catch (e) {
throw new Error('Error occurred when parsing request body to JSON.');
}
// Execute the GraphQL operation request.
// No exception is thrown when execution fails, but a response containing `errors` is returned.
const gqlRes = await server.executeOperation(gqlReq);
const response = new Response(JSON.stringify({ data: gqlRes.data, errors: gqlRes.errors }), {
// Always make sure content-type is in JSON format.
headers: { 'content-type': 'application/json' },
});
// Copy the headers from the GraphQLResponse into the HTTP Response.
for (const [key, value] of Object.entries(gqlRes.http.headers)) {
response.headers.set(key, value);
}
return response;
}


3. A simple weather query GraphQL CDN proxy gateway example

3.1 What do we do

In this Demo, we assume that the third-party weather service needs to be encapsulated twice. We will develop a GraphQL CDN proxy gateway for the Weather API Network (tianqiapi.com). The Weather API website has certain restrictions on the QPS of free users, and can only query 300 times a day. Since the weather forecast generally changes less frequently, we assume that when you query the weather of a certain city for the first time, you will actually visit the Weather API website. service, and subsequent weather queries for the same city will go to the CDN cache.

3.2 Introduction to Weather API Web Interface

The Weather API website (tianqiapi.com) provides commercial-grade weather forecast services, and it is said that there are tens of millions of QPS every day. You can also imagine how much convenience it will bring if they use GraphQL to define and expose API interfaces, and there is no need to write API interface documents.

According to its official API document, we can get the current weather of a certain city through the following API (here takes the author's city Nanjing as an example):

HTTP request


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

Where {APP_ID} and {APP_SECRET} are the API accounts you applied for.

HTTP response


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: "Nanjing",
cityid: "101190101",
tem: "31",
tem_day: "31",
tem_night: "24",
update_time: "14:12",
wea: "cloudy",
wea_img: "yun",
win: "southeast wind",
win_meter: "9km/h",
win_speed: "Level 2"
}
The naming and capitalization here really need to spit.

Here is a simple API client implementation:


export async function fetchWeatherOfCity(city: string) {
// The URL class has a corresponding implementation in EdgeRoutine.
const url = new URL('http://www.tianqiapi.com/free/day');
// Here we directly use the free account in the official example.
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 Defining our GraphQL SDL

Let's use the GraphQL SDL language to define the Schema of the interface to be implemented next:

type Query {
"Query the version information of the current API."
versions: Versions!
"Query the real-time weather data of the specified city."
weatherOfCity(name: String!): Weather!
}
"""
city information
"""
type City {
"""
Unique ID of the city
"""
id: ID!
"""
city name
"""
name: String!
}
"""
Version Information
"""
type Versions {
"""
API version number.
"""
api: String!
"""
`graphql` NPM version number.
"""
graphql: String!
}
"""
weather data
"""
type Weather {
"Current City"
city: City!
"Last update time"
updateTime: String!
"Weather Condition Code"
code: String!
"Localized (Chinese) Weather Status"
localized: String!
"Daytime Temperature"
tempOfDay: Float!
"Night Temperature"
tempOfNight: Float!
}

3.4 Implementing GraphQL Resolvers

The implementation idea of Resolvers is very simple, see the comments for details:


import { version as graphqlVersion } from 'graphql';
import { apiVersion } from '../api-version';
import { fetchWeatherOfCity } from '../tianqi-api';
export function versions() {
return {
// EdgeRoutine deployment is not as timely as FaaS.
// So before each deployment, I manually modify the version number in `api-version.ts`,
// If you see that the api version number has changed during the query, it means that the CDN has been successfully deployed.
api: apiVersion,
graphql: graphqlVersion,
};
}
export async function weatherOfCity(parent: any, args: { name: string }) {
// Call the API and convert the returned format to JSON.
const raw = await fetchWeatherOfCity(args.name).then((res) => res.json());
// Map the original return result to the interface object we defined.
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 Create and start the server

Now that we have the GraphQL interface outline and resolvers, we can create and start our server just like in Node.js.


// Note that this is no longer `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';
// create our server
const server = new ApolloServer({
// `typeDefs` is a GraphQL `DocumentNode` object.
// `*.graphql` files become `DocumentNode` objects after being loaded by `webpack-graphql-loader`.
typeDefs,
// Resolvers in Section 3.4
resolvers,
});
// Start the server first, then listen, and one line of code is all done!
server.start().then(() => server.listen());

Yes, it's that simple, create a server object, then start it and make it listen on the specified path (in this case no path parameter is passed, the default /graphql is used ).

So far, the main TypeScript and GraphQL code is all done!

3.6 Engineering Configuration

In order for TypeScript to understand that we are writing code in the EdgeRoutine environment, we need to specify lib and types in tsconfig.json :


{
"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"]
}

Again, unlike Serverless / FaaS, our programs are not running in a Node.js environment, but in a ServiceWorker-like environment. Starting from Webpack 5 , polyfills for Node.js built-in modules are no longer automatically injected in the browser target environment, so we need to manually add:


{
...
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/'),
},
...
}
...
}

Of course, you also need to manually install polyfills including assert , buffer , crypto-browserify , os-browserify , stream-browserify , browserify-zlib and util .

3.7 Add CDN cache

Finally, let's add the CDN cache. Since EdgeRoutine is still in the beta stage before the author's deadline, we can only use the Experimental API to implement the cache. Let's reimplement the fetchWeatherOfCity() method.


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;
}

in the global ( globalThis ) is essentially a cache implemented by Swift, its key must be an HTTP Request object or an HTTP protocol (non-HTTPS) URL string, and the value must be an HTTP Response object (can come from fetch() method). Although the serverless program of EdgeRoutine will restart every few minutes or an hour, our global variables will be destroyed, but with the help of the cahce object, it can help us implement CDN-level caching.

After Alibaba Cloud's EdgeRoutine KV database goes live, we will update this example to implement a more powerful cache. Unfortunately, as of the time of writing, this function has not been launched yet, and I am looking forward to it!
3.8 Add Playground Debugger

To better debug GraphQL we can also add an official Playground debugger, which is a single page application, so we can load it in via Webpack's html-loader .


addEventListener('fetch', (event) => {
const response = handleRequest(event.request);
if (response) {
event.respondWith(response);
}
});
function handleRequest(request: Request): Promise | void {
const url = new URL(request.url);
const path = url.pathname;
// To facilitate debugging, we handle all GET requests to `/graphql` as returning the playground.
// while the POST request is the actual GraphQL call
if (request.method === 'GET' && path === '/graphql') {
return Promise.resolve(new Response(rawPlaygroundHTML, { status: 200, headers: { 'content-type': 'text/html' } }));
}
}

Finally, let's visit /graphql in the browser and see the following interface:

Enter a query in it:


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

Set Variables to { "name": "Hangzhou" } and click the Play button in the middle.

3.9 Complete project code

After the official release of EdgeRoutine, I will open source the above NPM package and Demo on my Github.


4. Future-proof


In this simple public example, we have no way to fully demonstrate how to use EdgeRoutine as a secondary proxy gateway to a GraphQL gateway, you can visit graphcdn.io to learn more about GrpahQL CDN gateway through video. In the foreseeable future, we will use CDN's edge KV database to cache query results, and use GraphQL's syntax parsing and unique ID features in a single type to automatically invalidate the cache of related data entities when Mutations occur. .

Copyright statement: The content of this article is contributed by Alibaba Cloud's real-name registered users. The copyright belongs to the original author. The Alibaba Cloud developer community does not own the copyright and does not assume the corresponding legal responsibility. For specific rules, please refer to the " Alibaba Cloud Developer Community User Service Agreement " and " Alibaba Cloud Developer Community Intellectual Property Protection Guidelines ". If you find any content suspected of plagiarism in this community, fill out the infringement complaint form to report it. Once verified, this community will delete the allegedly infringing content immediately.

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00