天天看點

基于ASM的GRPC服務部署實踐

繼MicroServices之後,ServiceMesh是又一個推動軟體工業的革命性技術。其服務治理的方法論,不僅改變了技術實作的方式和社會分工。

運作于資料平面的使用者服務與治理服務的各種規則徹底解耦。運作于控制平面的規則定義元件,将流量控制的具體規則推送給運作于資料平面的proxy,proxy通過對使用者服務的ingress和egress的實際控制,最終實作服務治理。

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

阿裡雲提供的服務網格(

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

1. grpc服務

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

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

基于ASM的GRPC服務部署實踐

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

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

基于ASM的GRPC服務部署實踐

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 > 快速入門 > 使用流程

服務網格執行個體建立成功後,確定資料平面已經添加容器服務叢集。然後開始資料平面的配置。

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           

可以通過通路容器服務的

命名空間

界面進行驗證。

部署deployment和service

export DEMO_HOME=

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

端口。

基于ASM的GRPC服務部署實踐

建立完成後,我們就有了公網的IP。餘文測試驗證環節将使用到這裡配置的入口網關IP

39.102.37.176

基于ASM的GRPC服務部署實踐

2.4 控制平面

部署Gateway

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

部署完畢後,在ASM控制台的控制平面-服務網關界面下,可以看到這個Gateway執行個體。也可以直接使用該界面建立和删除服務網格的Gateway執行個體。

基于ASM的GRPC服務部署實踐

部署VirtualService

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

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/consumer-virtual-service.yaml           

部署完畢後,在ASM控制台的控制平面-虛拟服務界面下,可以看到VirtualService執行個體清單。也可以直接使用界面建立和删除服務網格的VirtualService執行個體。

基于ASM的GRPC服務部署實踐

部署DestinationRule

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-destination-rule.yaml           

部署完畢後,在ASM控制台的控制平面-目标規則界面下,可以看到DestinationRule執行個體清單。也可以直接使用界面建立和删除服務網格的DestinationRule執行個體。

基于ASM的GRPC服務部署實踐

2.5 流量驗證

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

基于ASM的GRPC服務部署實踐
HOST=39.102.37.176
for ((i=1;i<=10;i++)) ;  
do   
curl ${HOST}:9001/hello/feuyeux
echo
done           

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

基于ASM的GRPC服務部署實踐
# 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