基于ASM的GRPC服务部署实践

简介: 继MicroServices之后,ServiceMesh是又一个推动软件工业的革命性技术。其服务治理的方法论,不仅改变了技术实现的方式和社会分工。 运行于数据平面的用户服务与治理服务的各种规则彻底解耦。运行于控制平面的规则定义组件,将流量控制的具体规则推送给运行于数据平面的proxy,proxy通过对用户服务的ingress和egress的实际控制,最终实现服务治理。 原本需要服务开发

继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。示例的拓扑示意如下:

grpc

1.1 springboot

common——proto2java

示例工程包含三个模块,分别是commonproviderconsumer。其中,common负责将定义grpc服务的protobuf转换为java的rpc模板代码;后两者对其依赖,分别实现grpc的服务端和客户端。

示例工程的protobuf定义如下,实现了两个方法SayHelloSayByeSayHello的入参是一个字符串,返回一个字符串;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

验证完镜像后,我们进入重点。本节将完整讲述如下拓扑的服务治理配置:
mesh-arch

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端口。

ingressgateway

创建完成后,我们就有了公网的IP。余文测试验证环节将使用到这里配置的入口网关IP 39.102.37.176

2.4 控制平面

部署Gateway

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

部署完毕后,在ASM控制台的控制平面-服务网关界面下,可以看到这个Gateway实例。也可以直接使用该界面创建和删除服务网格的Gateway实例。

部署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实例。

部署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实例。

2.5 流量验证

完成grpc服务在ASM的部署后,我们首先验证如下链路的流量:

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

最后再来验证我如下链路的流量:

# 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
目录
相关文章
|
30天前
|
存储 机器学习/深度学习 负载均衡
模型服务网格:云原生下的模型服务管理
模型服务网格:云原生下的模型服务管理
78393 1
模型服务网格:云原生下的模型服务管理
|
8月前
|
监控 安全 大数据
阿里服务的ASM、MSE和ARMS都有其各自的应用场景
阿里服务的ASM、MSE和ARMS都有其各自的应用场景
220 39
|
8月前
|
安全 数据安全/隐私保护 开发者
实现安全的服务通信:探索如何使用服务网格来确保服务间的安全通信
实现安全的服务通信:探索如何使用服务网格来确保服务间的安全通信
67 0
|
自然语言处理 运维 监控
《云原生架构容器&微服务优秀案例集》——01 互联网——站酷 基于 ASM 解决多语言技术栈下服务管理难题,实现运维提效
《云原生架构容器&微服务优秀案例集》——01 互联网——站酷 基于 ASM 解决多语言技术栈下服务管理难题,实现运维提效
212 0
|
自然语言处理 运维 监控
《2023云原生实战案例集》——04 互联网——站酷 基于ASM解决多语言技术栈下服务管理难题,实现运维提效
《2023云原生实战案例集》——04 互联网——站酷 基于ASM解决多语言技术栈下服务管理难题,实现运维提效
ASM中GRPC/HTTP2流式传输场景的Sidecar资源占用调优
当应用使用GRPC流式传输时,数据经由Sidecar进行转发,由于Sidecar工作于7层,所以Sidecar会对GRPC消息元信息进行解析后在转发,在一些场景下(例如在单连接上建立大规模Stream)可能会造成Sidecar内存占用提升。本文将介绍Sidecar控制GRPC流式传输性能的相关参数和其作用机制,用户可以通过这些参数对Sidecar资源占用进行调整,已取得运行性能和资源消耗的平衡。S
160 0
ASM中GRPC/HTTP2流式传输场景的Sidecar资源占用调优
|
算法 Serverless 测试技术
使用ASM管理Knative服务(6):基于流量请求数实现服务自动扩缩容
Knative on ASM中提供了开箱即用、基于流量请求的自动扩缩容KPA(Knative Pod Autoscaler)功能。本文介绍如何基于流量请求数实现服务自动扩缩容。
7812 0
使用ASM管理Knative服务(6):基于流量请求数实现服务自动扩缩容
|
测试技术 Serverless
使用ASM管理Knative服务(5):在Knative on ASM中基于流量灰度发布服务
Knative on ASM提供了基于流量的灰度发布能力。当创建Knative服务时,Knative会自动为服务创建第一个修订版本Revision。此后当Knative服务的配置发生变化时,Knative都会创建一个新的修订版本。通过修改流量发往不同修订版本的分配比例,即可实现灰度发布功能。本文介绍如何在Knative on ASM中基于流量灰度发布服务。
218 0
使用ASM管理Knative服务(5):在Knative on ASM中基于流量灰度发布服务
|
Serverless Perl 测试技术
使用ASM管理Knative服务(4):使用ASM网关实现HTTPS访问Knative服务
ASM网关支持HTTPS协议和动态加载证书功能。在使用Knative on ASM时,可以通过ASM网关来实现HTTPS访问。本文将演示如何使用ASM网关来实现HTTPS访问Knative服务。
234 0
使用ASM管理Knative服务(4):使用ASM网关实现HTTPS访问Knative服务
|
网络协议 Serverless 测试技术
使用ASM管理Knative服务(3):在Knative on ASM中使用自定义域名
对于Knative服务,服务的DNS名默认格式为:{服务名}.{服务所在命名空间}.{默认域名},Knative会默认使用example.com作为服务的域名。Knative on ASM支持使用自定义域名作为默认域名。本文介绍如何在Knative Serving中使用自定义域名。
192 0
使用ASM管理Knative服务(3):在Knative on ASM中使用自定义域名