日本熟妇hd丰满老熟妇,中文字幕一区二区三区在线不卡 ,亚洲成片在线观看,免费女同在线一区二区

讓CDN成為高性能GraphQL網關

本文通過一個簡單的天氣查詢GraphQL CDN代理網關示例,讓您快速了解邊緣計算Serverless技術方案EdgeRoutine結合GraphQL網關可以實現高性能的API網關服務。實踐方案同樣適用于DCDN服務。

背景信息

隨著互聯網飛速發展,國內外越來越多的企業在大規模使用GraphQL,當前在阿里巴巴集團CCO技術部,GraphQL已經成為了API對內對外描述、暴露及調用的唯一標準,而在國外,Facebook、Netflix、Github、Paypal、微軟、大眾、沃爾瑪等企業也在大規模使用GraphQL中,在面向全球前端開發者調研問卷中,GraphQL也成為最受關注的技術和最想學習的技術。GraphQL最適合的場景莫過于作為BFF(Backend for Frontend)的網關層,即根據客戶端的實際需要,將后端的原始HSF接口、第三方RESTful接口進行整合和封裝形成自己的Service Facade層。GraphQL自身的特性使得其非常容易與RESTful、MTOP/MOPEN等基于HTTP的現有網關進行集成。而另一方面,GraphQL非常適合作為Serverless/FaaS的網關層,只需要唯一一個HTTP Trigger就能實現代理所有背后的API。

GraphQL既是一種用于API的查詢語言,也是一個滿足您數據查詢的運行時。GraphQL對您的API中的數據提供了一套易于理解的完整描述,使得客戶端能夠準確地獲得它需要的數據,而且沒有任何冗余,也讓API更容易地隨著時間推移而演進,還能用于構建強大的開發者工具。更多關于GraphQL的詳細信息,請參見GraphQL官網

GraphQL網關與CDN邊緣計算

EdgeRoutine邊緣計算是阿里云CDN團隊推出的新一代Serverless計算平臺,它為您提供了一個類似W3C標準的ServiceWorker容器,可以充分利用CDN遍布全球的節點空閑計算資源以及強大的加速與緩存能力,實現高可用性、高性能的分布式彈性計算。想了解更多信息請參見什么是邊緣程序

讓CDN成為高性能GraphQL網關

GraphQL非常適合作為BFF網關層,Query類的請求占了大量的比例,而這些只讀類查詢請求,通常響應結果在相當長的時間范圍甚至是永遠都不會發生變化。

如上圖所示,將CDN EdgeRoutine作為GraphQL Query類請求的代理層,首次執行Query時,系統將請求先從CDN代理到GraphQL網關層,再通過網關層代理到實際的應用服務(例如,通過HSF調用),然后將獲得的返回結果緩存在CDN上,之后的請求可以根據TTL業務規則動態決定走緩存還是去GraphQL網關層。這樣我們可以充分利用CDN的特性,將查詢類請求分散到遍布全球的節點中,顯著降低主應用程序的QPS。

移植Apollo GraphQL Server

Apollo GraphQL Server是目前使用最廣泛的開源GraphQL服務,它的Node.js版本更是被BFF類應用廣為使用。但遺憾的是apollo-server是一個面向Node.js技術棧開發的項目,而EdgeRoutine提供的是一個類似Service Worker的Serverless容器,因此我們首先需要將apollo-server-core移植到EdgeRoutine中。

步驟一:構建TypeScript開發環境和腳手架

首先,需要構建一個EdgeRoutine容器的TypeScript環境,用Service Worker的TypeScript庫來模擬編譯時環境,同時將Webpack作為本地調試服務器,并用瀏覽器的Service Worker來模擬運行edge.js腳本,用Webpack的socket通訊實現Hot Reload效果。

步驟二:建立與HTTP服務器的連接

通過以下方法,集成ApolloServerBase類建立與HTTP服務器的連接,為EdgeRoutine環境實現自己的ApolloServer:

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請求轉換成GraphQL請求發送到Apollo Server,并將其返回的GraphQL響應轉換回HTTP響應。Apollo官方有一個名為runHttpQuery()的類似方法,但是該方法用到了buffer等Node.js環境內置的模塊,因此無法在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;
}

天氣查詢GraphQL CDN代理網關示例

在這個Demo里對第三方天氣服務進行二次封裝,為天氣API網(tianqiapi.com)開發一個GraphQL CDN代理網關。

天氣API網對免費用戶的QPS有一定的限制,每天只能查詢300次。天氣預報一般變化頻率較低,我們假設希望在首次查詢某一個城市天氣的時候,將會真正訪問到天氣API網的服務,而此后的同一城市天氣查詢將通過CDN緩存通道。

天氣API網簡介

天氣API網(tianqiapi.com)對外提供商業級的天氣預報服務,每天有千萬級的QPS??梢酝ㄟ^下面的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級"
}

API客戶端實現

export async function fetchWeatherOfCity(city: string) {
  // URL 類在 EdgeRoutine 中有對應的實現。
  const url = new URL('http://www.tianqiapi.com/free/day');
  // 這里我們直接采用官方示例中的免費賬戶。
  url.searchParams.set('appid', '2303****');
  url.searchParams.set('appsecret', '8Yvl****');
  url.searchParams.set('city', city);
  const response = await fetch(url.toString);
  return response;
}

步驟一:自定義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!
}

步驟二:實現GraphQL 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,
  };
}

步驟三:創建并啟動服務器

創建一個server對象,然后將它啟動并使其偵聽指定的路徑/graphql。

// 注意這里不再是 `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,
  // 即步驟二中的 Resolvers
  resolvers,
});
// 先啟動服務器,然后監聽,一行代碼全部搞定!
server.start().then(() => server.listen());

步驟四:工程化配置

為了讓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"]
}

與Serverless/FaaS不同,該程序并不是運行在Node.js環境中,而是運行在類似ServiceWorker環境中。從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/'),
    },
    ...
  }
  ...
}

此外,您還需要手動安裝包括assertbuffer、crypto-browserify、os-browserify、stream-browserifybrowserify-zlibutil等在內的polyfills 包。

步驟五:添加CDN緩存

通過Experimental的API添加緩存,重新實現fetchWeatherOfCity()方法:

export async function fetchWeatherOfCity(city: string) {
  const url = new URL('http://www.tianqiapi.com/free/day');
  url.searchParams.set('appid', '2303****');
  url.searchParams.set('appsecret', '8Yvl****');
  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;
}

在全局globalThis中提供的cache對象,本質上是一個通過Swift實現的緩存器,它的鍵必須是一個HTTP Request對象或一個HTTP協議(非HTTPS)的URL字符串,而值必須是一個HTTP Response對象(可以來自fetch()方法)。雖然EdgeRoutine的Serverless程序每隔幾分鐘或1小時就會重啟,全局變量會隨之銷毀,但是有了cache對象的幫助,可以實現CDN級別的緩存。

步驟六:添加Playground調試器

為了更好的調試GraphQL,您還可以添加一個官方的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按鈕即可。