gRPC-Web及其与Istio Envoy的无缝集成

目录

  • 1. gRPC-Web
    • 1.1 gRPC与REST
    • 1.2 REST转gRPC模式
    • 1.3 gRPC-Web模式
  • 2. Istio集成gRPC-Web
    • 2.1 生成gRPC-Web浏览器端JavaScript代码
    • 2.2 浏览器端集成gRPC-Web生成代码
    • 2.3 Istio部署后端gRPC服务
    • 2.4 部署和测试Web前端
  • 3. 结论

1. gRPC-Web

1.1 gRPC与REST

在web领域(浏览器端、API端),REST API无疑是目前最流行同时也是非常成熟的架构解决方案,而gRPC作为新兴的RPC框架(Http/2 + Protobuf),由于浏览器对gRPC的支持不足使其很难匹敌 REST 的通用支持能力,现阶段gRPC更适合:

  • 后端微服务(跨语言)之间的调用
  • IoT场景(轻量级消息传输,使用protobuf序列化,支持双向流)
  • 移动端程序(gRPC移动客户端 与 gRPC服务端通信,即不需要浏览器的场景)

关于gRPC与REST API间的对比,可以参见下表:

特性 gRPC REST
HTTP协议 Http 2
客户端-响应通信模型,
支持流通信和双向流,
继承Http 2的优越特性(header压缩、多路复用、双向流…)
Http 1.1
请求-响应模型(队头阻塞…)
浏览器支持 浏览器原生不支持
需借助grpc-web + envoy代理来处理Http1.1 和gRPC Http2之间的转换
通用的浏览器支持
负载数据结构 默认使用Protobuf来序列化负载数据,
更高的压缩率及传输效率(更高的性能),
可读性差
主要依赖JSON或XML格式来收发数据,
可读性好,
传输效率差
代码生成功能 带有原生代码(支持多语言)的代码生成功能 开发人员必须使用Swagger或Postman这样的第三方工具来为API生成代码

1.2 REST转gRPC模式

可以类比其他RPC协议,例如国内比较流行的Dubbo,也有相同的问题,
例如:

  • 我们后端服务间通信使用RPC协议(Dubbo、gRPC等),这都没有问题,
  • 但是想要将后端服务暴露给Web端(浏览器端、移动端),通常都会使用更通用的REST API形式,
  • 这时就需要将REST协议转换成服务间的RPC通信,因此就需要引入一个协议转换层(手写代码转换) 或者 API 网关转换层(借助其他开源的独立部署的API网关),

而 手写 或者 配置 协议间转换(例如SpringMVC Controller调用@DubboReference、@GrpcClient服务)的过程是个重复且费时的过程,会浪费开发人员大量的精力,

使用REST转gRPC模式的架构如下图:
gRPC-Web及其与Istio Envoy的无缝集成_第1张图片
即浏览器端(JS环境)通过HTTP 1.1 + REST协议与REST API server通信,
然后由REST API server将HTTP请求(JSON格式)转换为gRPC请求(Protobuf格式),
而REST API server的工作是需要开发人员来实现或者配置的。

1.3 gRPC-Web模式

gRPC强制采用HTTP/2协议,但是目前在浏览器中通过JS API很难实现gRPC规范,主要原因如下:

  • 浏览器中无法强制使用HTTP/2协议
  • 即使使用了HTTP/2协议,在浏览器中也无权访问原始数据帧frame

鉴于此,gRPC社区推出了gRPC-Web规范(JavaScript实现的gRPC浏览器客户端),相较于原生gRPC其主要不同在于:

  • 同时支持HTTP/1.1和HTTP/2
  • 在每个reqeust /response消息体结尾处发送gRPC trailers(通过gRPC消息header的一个新增加的位Bit表示)
  • 强制使用代理Proxy对gRPC-Web请求和gRPC HTTP/2响应进行转换
  • 默认仅支持Unary模式
    • 开启grpcwebtext模式可支持Server-side streams
    • 均不支持Client-side & bi-directional streaming)
      Client / Feature Transport Unary Server-side streams Client-side & bi-directional streaming
      Google (grpcweb) XHR ️ ✔️
      Google (grpcwebtext) XHR ️ ✔️ ✔️
      Improbable Fetch/XHR ️ ✔️ ✔️

gRPC-Web及其与Istio Envoy的无缝集成_第2张图片

该组件使Web应用(浏览器端JS环境)能够:

  • 通过gRPC JavaScript Stub方式(API同Node.js for gRPC API, 但不依赖Node环境)
  • 浏览器发起的通信并不是真正的gRPC Http2协议,可以看做浏览器端通过Http 1.1 对gRPC的封装
  • 通过Envoy代理接入grpc-web请求并负责转换grpc-web请求到gRPC后端服务

使用grpc-web的架构如下图:
gRPC-Web及其与Istio Envoy的无缝集成_第3张图片
gRPC-Web模式相较于之前的REST转gRPC模式:

  • 即兼顾浏览器环境的grpc-web通信(且后端服务仍可通过原生gRPC协议通信)
  • 且支持生成gRPC JS Stub且提供原生gRPC编程体验(前后端使用统一规范的proto定义)
  • 全栈gRPC编程(无需手工实现REST和gRPC间的转换)
  • 但需要额外引入Envoy代理(由Envoy负责gRPC协议转换)

如上使用grpc-web协议,配套的后端gRPC服务就需要前置一个Envoy代理,这无疑增加了此方案落地的复杂程度。而熟悉Istio的都知道,Envoy是Istio的默认代理,所以在Istio环境下落地grpc-web会很方便,利用Istio的Envoy及EnvoyFilter构件可以无缝连接grpc-web应用。

2. Istio集成gRPC-Web

Istio通过Envoy集成gRPC-Web具体过程可参见:
ServiceMesher - 构建无缝集成的gRPC-Web和Istio的云原生应用教程

Istio通过Envoy集成gRPC-Web的整体架构如下图:
gRPC-Web及其与Istio Envoy的无缝集成_第4张图片
概括起来如下:

  • 浏览器端应用利用gRPC-Web JS库向Istio Gatway发送HTTP请求
  • Istio Gateway将HTTP请求路由到后端gRPC服务旁运行的Proxy sidecar,即Envoy代理
  • Envoy使用envoy.grpc_web filter将HTTP调用转换成gRPC调用

示例中的emoji服务的整体创建步骤如下:

  1. 使用Protobuf定义协议格式;
  2. 编译Protobuf定义文件,来生成Go和JavaScript文件;
  3. 构建并测试基于Go的gRPC服务端,该服务使用emoji替换输入文本中的关键字;
  4. 使用gRPC-Web为emoji服务创建Web界面;
  5. 配置EnvoyFilter并通过Istio部署后端;
  6. 部署Web应用程序并测试我们的emoji服务。

其中第1, 2, 3步中定义proto和生成服务端(Go)代码,这个是gRPC编程的基础,不做过多讲解,
其中的proto也可换成任意其他服务定义(仅支持Unary或Server Stream方法),亦可生成其他语言的服务端实现,如Java实现,
关于gRPC Java编程可参见我之前的文章:gRPC Java入门示例。

接下来讲讲需要重点关注的步骤。

2.1 生成gRPC-Web浏览器端JavaScript代码

第2步中除了生成后端服务端gRPC代码,还需要生成gRPC-Web对应的客户端浏览器端JavaSrcript代码:

生成命令格式:

$ protoc -I=$DIR echo.proto \
    --js_out=import_style=commonjs:$OUT_DIR \
    --grpc-web_out=import_style=commonjs,mode=grpcwebtext:$OUT_DIR

-I 指定proto定义文件输入目录
–js_out 生成JavaScript文件相关配置
–grpc-web_out 生成grpc-web框架相关JavaScript文件配置
$DIR 为proto定义文件的输入目录(使用时需替换为实际目录)
$OUT_DIR 为生成代码的输出路径(使用时需替换为实际目录)

import_style 指定js导入方式

  • closure 使用goog.require()导入
  • commonjs 使用require()导入
  • commonjs+dts 实验特性,除了支持commonjs导入,还支持生成*.d.ts类型文件
  • typescript 实验特性,使用TypeScript生成Service stub(客户端代码)
  • 注: 最后两个commonjs+dts和typescript仅支持在grpc-web_out中使用,不可在js_out中使用

mode 指定payload的格式(即发送数据格式)

  • grpcwebtext
    • Content-type: application/grpc-web-text
    • base64编码格式
    • 同时支持Unary和Server Stream调用
  • grpcweb
    • Content-type: application/grpc-web+proto
    • 二进制protobuf格式
    • 仅支持Unary调用

示例中的emoji.proto文件目录如下:

── grpc-web-emoji
   └── emoji
       └── emoji.proto

进入grpc-web-emoji目录后,执行如下生成命令:

$ protoc -I emoji/ emoji/emoji.proto \
         --js_out=import_style=commonjs:emoji \
         --grpc-web_out=import_style=commonjs,mode=grpcwebtext:emoji

即读取emoji目录下的emoji.proto文件定义,
以commonjs格式生成JS文件到emoji目录下(生成empoji_pb.js文件),
以commonjs格式、数据采用grpcwebtext格式,生成grpc-web框架JS文件到emoji目录下(生成emoji_grpc_web_pb.js文件),
执行命令后目录结构如下所示:

── grpc-web-emoji
   └── emoji
       ├── emoji.proto
       ├── emoji_grpc_web_pb.js
       └── emoji_pb.js

2.2 浏览器端集成gRPC-Web生成代码

定义前端界面index.html
首先,让我们创建一个名为index.html的HTML页面。该页面向用户显示一个文本编辑器,并调用一个emojize函数(我们稍后将定义)将用户输入发送到后端emoji服务。emojize函数还将消费后端服务返回的gRPC响应,并使用服务端返回的数据更新用户输入框。

DOCTYPE html>
<html>
<body>
  <div id="editor" contentEditable="true" hidefocus="true" onkeyup="emojize()">div>
  <script src="dist/main.js">script>
body>
html>

创建自定义client.js,导入grpc-web生成文件进行grpc编程
我们将如下所示的JavaScript代码放入名为client.js的前端文件。

//此处导入之前生成的emoji目录下的相关grpc-web文件
const {EmojizeRequest, EmojizeReply} = require('emoji/emoji_pb.js');
const {EmojiServiceClient} = require('emoji/emoji_grpc_web_pb.js');

//以grpc stub方式编程(同Node.js API)
//定义客户端stub,此处服务端地址为envoy代理入口地址,此例为Istio Gateway HTTP入口
var client = new EmojiServiceClient('http://192.168.99.100:31380');
var editor = document.getElementById('editor');

window.emojize = function() {
  //以grpc stub方式编程(构建请求参数)
  var request = new EmojizeRequest();
  request.setText(editor.innerText);
  
  //以grpc stub方式编程(调用gRPC服务emojize方法)
  client.emojize(request, {}, (err, response) => {
    //处理gRPC响应信息
    editor.innerText = response.getEmojizedText();
  });
}

注意:
EmojiServiceClient后端emoji服务的连接地址
http://192.168.99.100:31380
非http://localhost:9000(不是后端服务的直接访问地址)

这是因为gRPC-Web应用程序无法直接与gRPC后端通信,需要借助Enovy进行转换,
本例中使用Istio部署后端emoji服务(借助Istio Sidecar Enovy),所以可以理解为后端服务的地址为:
Istio Ingress Gateway暴露的IP地址及Istio Ingress Gateway HTTP端口
示例中Istio在Minikube上运行,其IP地址为192.168.99.100,默认的Istio Ingress HTTP端口为31380

使用npm生成main.js文件
最后,需要一些库来生成index.html中引用的dist/main.js文件。为此,我们使用如下的npm package.json配置。

{
  "name": "grpc-web-emoji",
  "version": "0.1.0",
  "description": "gRPC-Web Emoji Sample",
  "devDependencies": {
    "@grpc/proto-loader": "^0.3.0",
    "google-protobuf": "^3.6.1",
    "grpc": "^1.15.0",
    "grpc-web": "^1.0.0",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0"
  }
}

使用如下命令来安装库并生成dist/main.js

$ npm install
$ npx webpack client.js

注:
本人并非专业前端编程,此处仅是引用原文中示例进行讲解,若有问题感谢指正。

可以发现通过gRPC-Web可以:

  • 浏览器端使用类似Node.js的gRPC API进行编程
  • 浏览器端根据生成的代码进行编程,无需关心方法、数据的定义,可以直接通过API调用,
  • 前后端可以使用统一的gRPC proto定义生成各自代码(规范接口定义、减少协作沟通)

2.3 Istio部署后端gRPC服务

相关资源文件汇总如下表:

资源平台 资源类型 资源名称 资源说明
K8s Depoyment backend.yaml > Delopyment.backend 定义服务部署,生成实际的工作负载Pod(具体到Docker容器),
需要先将后端gRPC服务构建成Docker镜像后方可在K8s上部署,
示例中将后端服务构建成镜像vnoronha/grpc-web-emoji
K8s Service backend.yaml > Service.backend 定义集群内服务域名、端口及负载均衡
Istio Gateway gateway.yaml > Gateway.gateway 定义Istio网关入口
Istio VirtualService gateway.yaml > VirtualService.backend 定义Istio服务路由规则
Istio DestinationRule gateway.yaml > DestinationRule.backend 定义Istio服务目标及其版本
Istio EnvoyFilter filter.yaml > EnvoyFilter.grpc-web-filter 设置Istio Envoy过滤器规则,
本示例中用于在Envoy中集成grpc-web转换过滤器

接下来,我们定义Kubernetes ServiceDeployment配置,如下所示,并命名为backend.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  template:
    metadata:
      # 定义工作负载labels
      labels:
        app: backend
        version: v1
    spec:
      containers:
      - name: backend
        # 后端工作负载镜像
        image: vnoronha/grpc-web-emoji
        imagePullPolicy: Always
        ports:
        # 后端工作负载暴露端口
        - containerPort: 9000
---
apiVersion: v1
kind: Service
metadata:
  # 后端gRPC服务名
  name: backend
  labels:
    app: backend
spec:
  ports:
  # 此处port name需以grpc-开头
  - name: grpc-port
    port: 9000
  # 关联后端工作负载labels
  selector:
    app: backend

通过Istio部署此服务,由于Service ports name中的grpc-前缀,Istio会将其识别为gRPC服务
由于我们希望将gRPC-Web filter安装在backend sidecar代理上,因此我们需要在部署backend服务之前安装它
EnvoyFilter配置如下所示,我们将其命名为filter.yaml

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: grpc-web-filter
spec:
  # 选择需要设置此EnvoyFilter的工作负载labels,
  # 本例中对应之前定义的Deployment.backend
  workloadLabels:
    app: backend
  filters:
  # 插入envoy.grpc_web过滤器,
  # 用于转换浏览器端gRPC-Web请求到后端的gRPC服务
  - listenerMatch:
      listenerType: SIDECAR_INBOUND
      listenerProtocol: HTTP
    insertPosition:
      index: FIRST
    filterType: HTTP
    filterName: "envoy.grpc_web"
    filterConfig: {}

接下来,我们需要定义Istio Gateway -> VirtualServcie - > DestinationRule来将HTTP流量路由到后端服务,写入名为gateway.yaml的文件。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    # 使用默认istio ingressgateway
    istio: ingressgateway
  servers:
  # 监听Http协议入口端口80(实际对应31380 NodePort)
  - port:
      number: 80
      name: http
      protocol: HTTP
    # 适用于所有请求域名
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: backend
spec:
  # 入口匹配host域名
  hosts:
  - "*"
  # 关联的网关名(上面定义的gateway)
  gateways:
  - gateway
  http:
  # 匹配80端口
  - match:
    - port: 80
    route:
    # 路由到backend服务的9000端口的v1版本
    - destination:
        host: backend
        port:
          number: 9000
        subset: v1
    # 请求的跨域设置(放开gRPC请求的跨域访问)
    corsPolicy:
      allowOrigin:
        - "*"
      allowMethods:
        - POST
        - GET
        - OPTIONS
        - PUT
        - DELETE
      allowHeaders:
        - grpc-timeout
        - content-type
        - keep-alive
        - user-agent
        - cache-control
        - content-type
        - content-transfer-encoding
        - custom-header-1
        - x-accept-content-transfer-encoding
        - x-accept-response-streaming
        - x-user-agent
        - x-grpc-web
      maxAge: 1728s
      exposeHeaders:
        - custom-header-1
        - grpc-status
        - grpc-message
      allowCredentials: true
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: backend
spec:
  # 目标服务的host(对应K8s Service Name,即之前的Service.backend)
  host: backend
  subsets:
  # 定义v1版本(对应labels version:v1)
  - name: v1
    labels:
      version: v1

注意,为了能让gRPC-Web正常工作,我们在这里定义了一个复杂的corsPolicy

我们现在可以按以下顺序简单地部署上述配置。

# 部署Istio EnvoyFilter,需在Depoyment之前部署
$ kubectl apply -f filter.yaml
# 部署K8s Depoyment、Servie,且Depoyment开启Istio Sidecar注入
$ kubectl apply -f <(istioctl kube-inject -f backend.yaml)
# 部署Istio Gateway、VirtualService、DestinationRule
$ kubectl apply -f gateway.yaml

backend pod启动之后,我们可以验证gRPC-Web filter在sidecar代理中的配置是否正确,如下所示:

# 此处backend-7bf6c8f67c-8lbm7即为启动后的backend pod名称
$ istioctl proxy-config listeners backend-7bf6c8f67c-8lbm7 --port 9000 -o json
...
    "http_filters": [
        {
            "config": {},
            "name": "envoy.grpc_web"
        },
...

2.4 部署和测试Web前端

到了实验的最后阶段,原示例中通过Python启动一个HTTP服务,来为前端Web应用提供服务。

$ python2 -m SimpleHTTPServer 8080
Serving HTTP on 0.0.0.0 port 8080 ...

注:
通过Python启动前端服务,之前我也没尝试过,不过这个不是重点,
可以选择自己熟悉的、成熟的前端技术栈,如使用Nginx等来部署启动前端服务均可以。

让我们前往emoji web页面http://localhost:8080.

如果一切顺利,你将拥有一个功能完整的基于gRPC-Web的Web应用,如下所示。
在这里插入图片描述
如果你在Chrome等浏览器上打开开发者工具,你将会看到如下所示的gRPC-Web HTTP请求。
gRPC-Web及其与Istio Envoy的无缝集成_第5张图片

3. 结论

gRPC-Web提供了一种浏览器端JS环境兼容(HTTP/1.1, HTTP/2)的模式来进行gRPC服务调用,
使得浏览器端也可以直接使用gRPC stub进行编程,无需编码服务及数据的定义,
前后端使用统一的proto定义,更易于组织间统一规范且减少沟通成本,
但是相较于REST协议(HTTP/1.1 + JSON)其数据可读性差(BASE64或者Protobuf二进制),不易于前端调试,
且gRPC-Web目前需要一个中间代理,即Envoy代理,以便将数据在HTTP和gRPC之间转换,
由于目前Istio的默认Sidecar Proxy即为Enovy,所以在Istio架构下开发人员可以更方便的集成gRPC-Web应用,
即使用Istio中的Envoy来为后端服务端原生gRPC服务添加envoy.grpc_web过滤器转换。

后续…
之前也看到过HTTP+JSON直接转gRPC的相关介绍,
即前端使用转换后的REST API进行调用,后端通过Envoy代理根据proto定义转换为原生gRPC通信,后续会再整理相关介绍。


参考:
CNCF - gRPC-Web is gonig GA - 2018/10/24
CNCF官微 - gRPC-Web迈向GA【翻译版】
https://www.grpc.io/docs/platforms/web/basics/
https://grpc.io/blog/state-of-grpc-web/
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
InfoQ - gRPC 与 REST:对比
ServiceMesher - 构建无缝集成的gRPC-Web和Istio的云原生应用教程

你可能感兴趣的:(istio,grpc,grpc,grpc-web,istio)