基於阿里雲服務網格的 GRPC 服務部署實踐

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

image

繼MicroServices之後,ServiceMesh是又一個推動軟件工業的革命性技術。其服務治理的方法論,不僅改變了技術實現的方式,也將深入影響社會分工。

運行於數據平面的用戶服務與治理服務的各種規則徹底解耦。運行於控制平面的規則定義組件,將流量控制的具體規則推送給運行於數據平面的proxy,proxy通過對用戶服務的ingress和egress的實際控制,最終實現服務治理。

原本需要服務開發者編程實現的服務發現、容錯、灰度、流量複製等能力,被ServiceMesh非侵入的方式實現。此外,ServiceMesh還提供了訪問控制、認證授權等功能,進一步減輕了用戶服務的開發成本。

阿里雲提供的服務網格是基於容器服務之上的託管版ServiceMesh,在提供完整的ServiceMesh能力的同時(ASM還在底層橫向拉通了阿里云云原生的各種能力,不在本篇講述範圍),免去了用戶搭建和運維ServiceMesh平臺istio的繁瑣工作。本篇將分享如何將我們自己的GRPC服務,託管到阿里雲的服務網格中。

1. grpc服務

grpc協議相比http而言,既具備http跨操作系統和編程語言的好處,又提供了基於流的通信優勢。而且,grpc逐漸成爲工業界的標準,一旦我們的grpc服務可以mesh化,那麼更多的非標準協議就可以通過轉爲grpc協議的方式,低成本地接入服務網格,實現跨技術棧的服務通信。

grpc服務的示例部分使用最普遍的編程語言Java及最高效的編程框架SpringBoot。示例的拓撲示意如下:

image

1.1 springboot

common——proto2java

示例工程包含三個模塊,分別是common、provider、consumer。其中,common負責將定義grpc服務的protobuf轉換爲java的rpc模板代碼;後兩者對其依賴,分別實現grpc的服務端和客戶端。

示例工程的protobuf定義如下,實現了兩個方法SayHello和SayBye。SayHello的入參是一個字符串,返回一個字符串;SayBye只有一個字符串類型的出參。

syntax = "proto3";
import "google/protobuf/empty.proto";
package org.feuyeux.grpc;
option java_multiple_files = true;
option java_package = "org.feuyeux.grpc.proto";
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayBye (google.protobuf.Empty) returns (HelloReply) {}
}
message HelloRequest {
  string name = 1;
}
message HelloReply {
  string reply = 1;
}

common構建過程使用protobuf-maven-plugin自動生成rpc模板代碼。

provider——grpc-spring-boot-starter

provider依賴grpc-spring-boot-starter包以最小化編碼,實現grpc服務端邏輯。示例實現了兩套grpc方法,以在後文演示不同流量的返回結果不同。

第一套方法示意如下:

@GRpcService
public class GreeterImpl extends GreeterImplBase {
    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        String message = "Hello " + request.getName() + "!";
        HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
        responseObserver.onNext(helloReply);
        responseObserver.onCompleted();
    }
    @Override
    public void sayBye(com.google.protobuf.Empty request, StreamObserver<HelloReply> responseObserver) {
        String message = "Bye bye!";
        HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
        responseObserver.onNext(helloReply);
        responseObserver.onCompleted();
    }
}

第二套方法示意如下:

@GRpcService
public class GreeterImpl2 extends GreeterImplBase {
    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        String message = "Bonjour " + request.getName() + "!";
        HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
        responseObserver.onNext(helloReply);
        responseObserver.onCompleted();
    }
    @Override
    public void sayBye(com.google.protobuf.Empty request, StreamObserver<HelloReply> responseObserver) {
        String message = "au revoir!";
        HelloReply helloReply = HelloReply.newBuilder().setReply(message).build();
        responseObserver.onNext(helloReply);
        responseObserver.onCompleted();
    }
}

consumer——RESTful

consumer的作用有兩個,一個是對外暴露RESTful服務,一個是作爲grpc的客戶端調用grpc服務端provider。示意代碼如下:

@RestController
public class GreeterController {
    private static String GRPC_PROVIDER_HOST;
    static {
        GRPC_PROVIDER_HOST = System.getenv("GRPC_PROVIDER_HOST");
        if (GRPC_PROVIDER_HOST == null || GRPC_PROVIDER_HOST.isEmpty()) {
            GRPC_PROVIDER_HOST = "provider";
        }
        LOGGER.info("GRPC_PROVIDER_HOST={}", GRPC_PROVIDER_HOST);
    }
    @GetMapping(path = "/hello/{msg}")
    public String sayHello(@PathVariable String msg) {
        final ManagedChannel channel = ManagedChannelBuilder.forAddress(GRPC_PROVIDER_HOST, 6565)
                .usePlaintext()
                .build();
        final GreeterGrpc.GreeterFutureStub stub = GreeterGrpc.newFutureStub(channel);
        ListenableFuture<HelloReply> future = stub.sayHello(HelloRequest.newBuilder().setName(msg).build());
        try {
            return future.get().getReply();
        } catch (InterruptedException | ExecutionException e) {
            LOGGER.error("", e);
            return "ERROR";
        }
    }
    @GetMapping("bye")
    public String sayBye() {
        final ManagedChannel channel = ManagedChannelBuilder.forAddress(GRPC_PROVIDER_HOST, 6565)
                .usePlaintext()
                .build();
        final GreeterGrpc.GreeterFutureStub stub = GreeterGrpc.newFutureStub(channel);
        ListenableFuture<HelloReply> future = stub.sayBye(Empty.newBuilder().build());
        try {
            return future.get().getReply();
        } catch (InterruptedException | ExecutionException e) {
            LOGGER.error("", e);
            return "ERROR";
        }
    }
}

這裏需要注意的是GRPC_PROVIDER_HOST變量,我們在ManagedChannelBuilder.forAddress(GRPC_PROVIDER_HOST, 6565)中使用到這個變量,以獲得provider服務的地址。相信你已經發現,服務開發過程中,我們沒有進行任何服務發現能力的開發,而是從系統環境變量裏獲取這個值。而且,在該值爲空時,我們使用了一個hardcode值provider。沒錯,這個值將是後文配置在isito中的provider服務的約定值。

1.2 curl&grpcurl

本節將講述示例工程的本地啓動和驗證。首先我們通過如下腳本構建和啓動provider和consumer服務:

# terminal 1
mvn clean install -DskipTests -U
java -jar provider/target/provider-1.0.0.jar
# terminal 2
export GRPC_PROVIDER_HOST=localhost
java -jar consumer/target/consumer-1.0.0.jar

我們使用curl以http的方式請求consumer:

# terminal 3
$ curl localhost:9001/hello/feuyeux
Hello feuyeux!
$ curl localhost:9001/bye
Bye bye!

最後我們使用grpcurl直接測試provider:

$ grpcurl -plaintext -d @ localhost:6565 org.feuyeux.grpc.Greeter/SayHello <<EOM   
{
  "name":"feuyeux"
}
EOM
{
  "reply": "Hello feuyeux!"
}
$ grpcurl -plaintext localhost:6565 org.feuyeux.grpc.Greeter/SayBye                                                                                                 
{
  "reply": "Bye bye!"
}

1.2 docker

服務驗證通過後,我們製作三個docker鏡像,以作爲deployment部署到kubernetes上。這裏以provider的dockerfile爲例:

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=provider-1.0.0.jar
COPY ${JAR_FILE} provider.jar
COPY grpcurl /usr/bin/grpcurl
ENTRYPOINT ["java","-jar","/provider.jar"]

構建鏡像和推送到遠端倉庫的腳本示意如下:

docker build -f grpc.provider.dockerfile -t feuyeux/grpc_provider_v1:1.0.0 .
docker build -f grpc.provider.dockerfile -t feuyeux/grpc_provider_v2:1.0.0 .
docker build -f grpc.consumer.dockerfile -t feuyeux/grpc_consumer:1.0.0 .
docker push feuyeux/grpc_provider_v1:1.0.0
docker push feuyeux/grpc_provider_v2:1.0.0
docker push feuyeux/grpc_consumer:1.0.0

本地啓動服務驗證,示意如下:

# terminal 1
docker run --name provider2 -p 6565:6565 feuyeux/grpc_provider_v2:1.0.0
# terminal 2
docker exec -it provider2 sh
grpcurl -v -plaintext localhost:6565 org.feuyeux.grpc.Greeter/SayBye
exit
# terminal 3
export LOCAL=$(ipconfig getifaddr en0)
docker run --name consumer -e GRPC_PROVIDER_HOST=${LOCAL} -p 9001:9001 feuyeux/grpc_consumer
# terminal 4
curl -i localhost:9001/bye

1.3 istio

驗證完鏡像後,我們進入重點。本節將完整講述如下拓撲的服務治理配置:

image

Deployment

consumer的deployment聲明示意如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: consumer
    version: v1
...
      containers:
        - name: consumer
          image: feuyeux/grpc_consumer:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 9001

provider1的deployment聲明示意如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: provider-v1
  labels:
    app: provider
    version: v1
...
      containers:
        - name: provider
          image: feuyeux/grpc_provider_v1:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 6565

provider2的deployment聲明示意如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: provider-v2
  labels:
    app: provider
    version: v2
...
      containers:
        - name: provider
          image: feuyeux/grpc_provider_v2:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 6565

Deployment中使用到了前文構建的三個鏡像。在容器服務中不存在時(IfNotPresent)即會拉取。

這裏需要注意的是,provider1和provider2定義的labels.app都是provider,這個標籤是provider的唯一標識,只有相同才能被Service的Selector找到並認爲是一個服務的兩個版本。

服務發現

provider的Service聲明示意如下:

apiVersion: v1
kind: Service
metadata:
  name: provider
  labels:
    app: provider
    service: provider
spec:
  ports:
    - port: 6565
      name: grpc
      protocol: TCP
  selector:
    app: provider

前文已經講到,服務開發者並不實現服務註冊和服務發現的功能,也就是說示例工程不需要諸如zookeeper/etcd/Consul等組件的客戶端調用實現。Service的域名將作爲服務註冊的名稱,服務發現時通過這個名稱就能找到相應的實例。因此,前文我們直接使用了hardcode的provider。

grpc路由

服務治理的經典場景是對http協議的服務,通過匹配方法路徑前綴來路由不同的RESTful方法。grpc的路由方式與此類似,它是通過http2實現的。grpc的service接口及方法名與 http2的對應形式是`Path : /Service-Name/{method name}。因此,我們可以爲Gateway的VirtualService定義如下的匹配規則:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: grpc-gw-vs
spec:
  hosts:
    - "*"
  gateways:
    - grpc-gateway
  http:
...
    - match:
        - uri:
            prefix: /org.feuyeux.grpc.Greeter/SayBye
        - uri:
            prefix: /org.feuyeux.grpc.Greeter/SayHello

AB流量

掌握了grpc通過路徑的方式路由,定義AB流量便水到渠成:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: provider
spec:
  gateways:
    - grpc-gateway
  hosts:
    - provider
  http:
    - match:
        - uri:
            prefix: /org.feuyeux.grpc.Greeter/SayHello
      name: hello-routes
      route:
        - destination:
            host: provider
            subset: v1
          weight: 50
        - destination:
            host: provider
            subset: v2
          weight: 50
    - match:
        - uri:
            prefix: /org.feuyeux.grpc.Greeter/SayBye
      name: bye-route
...

到此,示例工程的核心能力簡單扼要地講述完畢。詳細代碼請clone本示例工程。接下來,我將介紹如何將我們的grpc服務實例部署到阿里雲服務網格。

2. 服務網格實踐

2.1 託管集羣

首先使用阿里雲賬號登錄,進入容器服務控制檯(https://cs.console.aliyun.com),創建Kubernetes集羣-標準託管集羣。詳情見幫助文檔:快速創建Kubernetes託管版集羣。

2.2 服務網格

進入服務網格控制檯(https://servicemesh.console.aliyun.com/),創建服務網格實例。詳情見幫助文檔:服務網格 ASM > 快速入門 > 使用流程。

服務網格實例創建成功後,確保數據平面已經添加容器服務集羣。然後開始數據平面的配置。

image

2.3 數據平面

kubeconfig

在執行數據平面的部署前,我們先確認下即將用到的兩個kubeconfig。

進入容器實例界面,獲取kubconfig,並保存到本地~/shop/bj_config。

進入服務網格實例界面,點擊連接配置,獲取kubconfig,並保存到本地~/shop/bj_asm_config。

請注意,在數據平面部署過程中,我們使用~/shop/bj_config這個kubeconfig;在控制平面的部署中,我們使用~/shop/bj_asm_config這個kubeconfig。

設置自動注入

kubectl \
--kubeconfig ~/shop/bj_config \
label namespace default istio-injection=enabled

可以通過訪問容器服務的命名空間界面https://cs.console.aliyun.com/#/k8s/namespace進行驗證。

部署deployment和service

kubectl \
--kubeconfig ~/shop/bj_config \
apply -f $DEMO_HOME/istio/kube/consumer.yaml
kubectl \
--kubeconfig ~/shop/bj_config \
apply -f $DEMO_HOME/istio/kube/provider1.yaml
kubectl \
--kubeconfig ~/shop/bj_config \
apply -f $DEMO_HOME/istio/kube/provider2.yaml

可以通過訪問容器服務的如下界面進行驗證:

通過如下命令,確認pod的狀態是否符合預期:

$ kubectl \
--kubeconfig ~/shop/bj_config \
get pod
NAME                           READY   STATUS    RESTARTS   AGE
consumer-v1-5c565d57f-vb8qb    2/2     Running   0          7h24m
provider-v1-54dbbb65d8-lzfnj   2/2     Running   0          7h24m
provider-v2-9fdf7bd6b-58d4v    2/2     Running   0          7h24m

入口網關服務

最後,我們通過ASM管控臺配置入口網關服務,以對外公開http協議的9001端口和grpc協議的6565端口。

image

余文測試驗證環節將使用到這裏配置的入口網關IP 39.102.37.176。

2.4 控制平面

部署Gateway

kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/gateway.yaml

部署Gateway的VirtualService

kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/gateway-virtual-service.yaml

部署VirtualService和DestinationRule

kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/provider-virtual-service.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/provider-destination-rule.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/consumer-virtual-service.yaml
kubectl \
--kubeconfig ~/shop/bj_asm_config \
apply -f $DEMO_HOME/istio/networking/consumer-destination-rule.yaml

2.5 流量驗證

完成grpc服務在ASM的部署後,我們首先驗證如下鏈路的流量:

image

HOST=39.102.37.176
for ((i=1;i<=10;i++)) ;  
do   
curl ${HOST}:9001/hello/feuyeux
echo
done

最後再來驗證我如下鏈路的流量:

image

# terminal 1
export GRPC_PROVIDER_HOST=39.102.37.176
java -jar consumer/target/consumer-1.0.0.jar
# terminal 2
for ((i=1;i<=10;i++)) ;  
do   
curl localhost:9001/bye
echo
done

到此,基於ASM的GRPC服務部署實踐分享完畢。歡迎技術交流。

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/live

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-06-26
本文作者:韓陸
本文來自:“infoq”,瞭解相關信息可以關注“infoq

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章