構建無縫集成的gRPC-Web和Istio的雲原生應用教程

做者:Venil Noronha
譯者:王全根
原文: venilnoronha.io/seamless-cl…

gRPC-Web使Web應用可以經過相似於Envoy的代理訪問gRPC後端。Envoy是Istio的默認代理,所以,咱們能夠利用Istio的EnvoyFilter構件來建立無縫鏈接的雲原生應用。html

介紹

在這篇文章中,我將引導你構建一個簡單的Web應用,使用emoji替換用戶輸入文本中的關鍵字,並使用gRPC-Web和Istio與gRPC後端進行通訊。前端

如下是咱們建立emoji應用的步驟大綱:python

  1. 使用Protobuf定義協議格式;linux

  2. 編譯Protobuf定義文件,來生成Go和JavaScript文件;webpack

  3. 構建並測試基於Go的gRPC服務,該服務使用emoji替換輸入文本中的關鍵字;git

  4. 使用gRPC-Web爲emoji服務建立Web界面;github

  5. 配置EnvoyFilter並經過Istio部署後端;golang

  6. 部署Web應用程序並測試咱們的emoji服務。web

架構

讓咱們進一步理解emoji服務的最終架構是什麼樣子。docker

簡而言之,只要用戶提供一些文本,Web應用就會利用gRPC-Web庫向Istio Gatway發送HTTP請求。而後,Istio網關將HTTP請求路由到emoji服務旁運行的Proxy sidecar,後者使用Envoy的gRPC-Web filter將HTTP調用轉換成gRPC調用。

定義協議格式

首先,讓咱們使用Protobuf定義協議格式。

syntax = "proto3";
  ​
  package emoji;
  ​
  service EmojiService {
    rpc Emojize (EmojizeRequest) returns (EmojizeReply);
  }
  ​
  message EmojizeRequest {
    string text = 1;
  }
  ​
  message EmojizeReply {
    string emojized_text = 1;
  }複製代碼

咱們定義一個名爲EmojiServiceservice,處理名爲Emojizerpc調用,該調用接受EmojizeRequest對象參數並返回一個EmojizeReply實例。

EmojizeRequest消息參數包含一個名爲textstring類型的字段,表示用戶輸入的文本。一樣,EmojizeReply包含一個名爲emojized_textstring類型的字段,表示最終輸出的字符,也即服務端將emoji關鍵字替換爲emoji表情符號的輸出內容。

編譯Protobuf定義文件

咱們先建立一個名爲grpc-web-emoji/emoji/的項目目錄結構,而後把前面的定義內容寫入名爲emoji.proto的文件。

而後編譯emoji.proto文件並生成所須要的Go文件。

$ protoc -I emoji/ emoji/emoji.proto --go_out=plugins=grpc:emoji複製代碼

一樣,咱們也生成JavaScript文件。

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

此時,您將得到以下所示的目錄結構。

── grpc-web-emoji
     └── emoji
         ├── emoji.pb.go
         ├── emoji.proto
         ├── emoji_grpc_web_pb.js
         └── emoji_pb.js複製代碼

構建和測試Go後端程序

如今讓咱們建立一個實現EmojiService API的Go程序。爲此,咱們使用如下內容建立一個名爲main.go的文件。

package main
  ​
  import (
      "context"
      "log"
      "net"
  ​
      proto "github.com/venilnoronha/grpc-web-emoji/emoji"
      "google.golang.org/grpc"
      "google.golang.org/grpc/reflection"
      emoji "gopkg.in/kyokomi/emoji.v1"
  )
  ​
  // server is used to implement the EmojiService interface
  type server struct{}
  ​
  // Emojize takes a input string via EmojizeRequest, replaces known keywords with
  // actual emoji characters and returns it via a EmojizeReply instance.
  func (s *server) Emojize(c context.Context, r *proto.EmojizeRequest)
              (*proto.EmojizeReply, error) {
      return &proto.EmojizeReply{EmojizedText: emoji.Sprint(r.Text)}, nil
  }
  ​
  func main() {
      // listen to TCP requests over port 9000
      lis, err := net.Listen("tcp", ":9000")
      if err != nil {
          log.Fatalf("failed to listen: %v", err)
      }
      log.Printf("listening on %s", lis.Addr())
  ​
      // register the EmojiService implementation with the gRPC server
      s := grpc.NewServer()
      proto.RegisterEmojiServiceServer(s, &server{})
      reflection.Register(s)
      if err := s.Serve(lis); err != nil {
          log.Fatalf("failed to serve: %v", err)
      }
  }複製代碼

我已經使用 kyokomi/emoji 庫來完成繁重的工做,即將輸入文本中的關鍵字轉換爲表情符號。

啓動服務後以下所示:

$ go run -v main.go
  2018/11/12 10:45:12 listening on [::]:9000複製代碼

咱們建立一個名爲emoji_client.go的客戶端,來實現經過程序測試emoji服務。

package main
  ​
  import (
      "log"
      "time"
  ​
      proto "github.com/venilnoronha/grpc-web-emoji/emoji"
      "golang.org/x/net/context"
      "google.golang.org/grpc"
  )
  ​
  func main() {
      // connect to the server
      conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
      if err != nil {
          log.Fatalf("could not connect to the service: %v", err)
      }
      defer conn.Close()
  ​
      // send a request to the server
      ctx, cancel := context.WithTimeout(context.Background(), time.Second)
      defer cancel()
  ​
      c := proto.NewEmojiServiceClient(conn)
      resp, err := c.Emojize(ctx, &proto.EmojizeRequest{
          Text: "I like :pizza: and :sushi:!",
      })
      if err != nil {
          log.Fatalf("could not call service: %v", err)
      }
      log.Printf("server says: %s", resp.GetEmojizedText())
  }複製代碼

咱們如今能夠運行emoji服務客戶端,以下所示。

$ go run emoji_client.go
  2018/11/12 10:55:52 server says: I like 🍕  and 🍣 !複製代碼

瞧!gRPC版本的emoji服務如期工做了,如今是時候讓Web前端啓動並運行了。

使用gRPC-Web建立Web界面

首先,讓咱們建立一個名爲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>複製代碼

咱們將以下所示的JavaScript代碼放入名爲client.js的前端文件。

const {EmojizeRequest, EmojizeReply} = require('emoji/emoji_pb.js');
  const {EmojiServiceClient} = require('emoji/emoji_grpc_web_pb.js');
  ​
  var client = new EmojiServiceClient('http://192.168.99.100:31380');
  var editor = document.getElementById('editor');
  ​
  window.emojize = function() {
    var request = new EmojizeRequest();
    request.setText(editor.innerText);
  ​
    client.emojize(request, {}, (err, response) => {
      editor.innerText = response.getEmojizedText();
    });
  }複製代碼

請注意,EmojiServiceClient與後端emoji服務的鏈接地址是http://192.168.99.100:31380,而非http://localhost:9000。這是由於Web應用程序沒法直接與gRPC後端通訊,所以,咱們將經過Istio部署咱們的後端emoji服務。Istio將在Minikube上運行,其IP地址爲192.168.99.100,默認的Istio Ingress HTTP端口爲31380

如今,咱們須要一些庫來生成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複製代碼

經過Istio部署後端服務

咱們如今能夠將後端emoji服務打包到一個容器,並經過Istio進行部署。咱們須要安裝gRPC-Web EnvoyFilter,以便將後端gRPC服務的調用在gRPC和HTTP間轉換。

咱們使用以下內容的Dockerfile構建Docker image。

FROM golang:1.11 as builder
  WORKDIR /root/go/src/github.com/venilnoronha/grpc-web-emoji/
  COPY ./ .
  RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -v -o emoji-service main.go
  ​
  FROM scratch
  WORKDIR /bin/
  COPY --from=builder /root/go/src/github.com/venilnoronha/grpc-web-emoji/emoji-service .
  ENTRYPOINT [ "/bin/emoji-service" ]
  CMD [ "9000" ]
  EXPOSE 9000複製代碼

咱們能夠以下所示build image,並將其推送到Docker Hub:

$ docker build -t vnoronha/grpc-web-emoji .
  $ docker push vnoronha/grpc-web-emoji複製代碼

接下來,咱們定義Kubernetes ServiceDeployment配置,以下所示,並命名爲backend.yaml

apiVersion: v1
  kind: Service
  metadata:
    name: backend
    labels:
      app: backend
  spec:
    ports:
    - name: grpc-port
      port: 9000
    selector:
      app: backend
  ---
  apiVersion: extensions/v1beta1
  kind: Deployment
  metadata:
    name: backend
  spec:
    replicas: 1
    template:
      metadata:
        labels:
          app: backend
          version: v1
      spec:
        containers:
        - name: backend
          image: vnoronha/grpc-web-emoji
          imagePullPolicy: Always
          ports:
          - containerPort: 9000複製代碼

注意,一旦咱們經過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:
    workloadLabels:
      app: backend
    filters:
    - listenerMatch:
        listenerType: SIDECAR_INBOUND
        listenerProtocol: HTTP
      insertPosition:
        index: FIRST
      filterType: HTTP
      filterName: "envoy.grpc_web"
      filterConfig: {}複製代碼

接下來,咱們須要定義一個Istio Gateway來將HTTP流量路由到後端服務。爲此,咱們將如下配置寫入名爲gateway.yaml的文件。

apiVersion: networking.istio.io/v1alpha3
  kind: DestinationRule
  metadata:
    name: backend
  spec:
    host: backend
    subsets:
    - name: v1
      labels:
        version: v1
  ---
  apiVersion: networking.istio.io/v1alpha3
  kind: Gateway
  metadata:
    name: gateway
  spec:
    selector:
      istio: ingressgateway
    servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
      - "*"
  ---
  apiVersion: networking.istio.io/v1alpha3
  kind: VirtualService
  metadata:
    name: vs
  spec:
    hosts:
    - "*"
    gateways:
    - gateway
    http:
    - match:
      - port: 80
      route:
      - destination:
          host: backend
          port:
            number: 9000
          subset: v1
      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複製代碼

注意,爲了能讓gRPC-Web正常工做,咱們在這裏定義了一個複雜的corsPolicy

咱們如今能夠按如下順序簡單地部署上述配置。

$ kubectl apply -f filter.yaml
  $ kubectl apply -f <(istioctl kube-inject -f backend.yaml)
  $ kubectl apply -f gateway.yaml複製代碼

backend pod啓動以後,咱們能夠驗證gRPC-Web filter在sidecar代理中的配置是否正確,以下所示:

$ istioctl proxy-config listeners backend-7bf6c8f67c-8lbm7 --port 9000 -o json
  ...
      "http_filters": [
          {
              "config": {},
              "name": "envoy.grpc_web"
          },
  ...複製代碼

部署和測試Web前端

咱們如今已經到了實驗的最後階段。咱們經過Python啓動一個HTTP服務,來爲咱們的Web應用提供服務。

$ python2 -m SimpleHTTPServer 8080
  Serving HTTP on 0.0.0.0 port 8080 ...複製代碼

讓咱們前往emoji web頁面http://localhost:8080.

若是一切順利,你將擁有一個功能完整的基於gRPC-Web的Web應用,以下所示。

若是你在Chrome等瀏覽器上打開開發者工具,你將會看到以下所示的gRPC-Web HTTP請求。

結論

gRPC-Web提供了一種將gRPC服務的優點帶給Web應用的好方法。它目前須要一箇中間代理,如Istio數據平面(即Envoy代理),以便將數據在HTTP和gRPC之間轉換。然而,一旦咱們準備好了基礎架構,開發人員就能夠無縫使用gRPC構建Web應用。

參考

相關文章
相關標籤/搜索