基于阿里云服务网格(ASM)的 GRPC 服务部署实践

用户头像
韩陆
关注
发布于: 2020 年 06 月 26 日
基于阿里云服务网格(ASM)的GRPC服务部署实践

继MicroServices之后,ServiceMesh是又一个推动软件工业的革命性技术。其服务治理的方法论,不仅改变了技术实现的方式,也将深入影响社会分工。



运行于数据平面的用户服务与治理服务的各种规则彻底解耦。运行于控制平面的规则定义组件,将流量控制的具体规则推送给运行于数据平面的proxy,proxy通过对用户服务的ingress和egress的实际控制,最终实现服务治理。



原本需要服务开发者编程实现的服务发现、容错、灰度、流量复制等能力,被ServiceMesh非侵入的方式实现。此外,ServiceMesh还提供了访问控制、认证授权等功能,进一步减轻了用户服务的开发成本。



阿里云提供的服务网格(ASM)(https://www.aliyun.com/product/servicemesh)是基于容器服务(https://www.aliyun.com/product/kubernetes)之上的托管版ServiceMesh,在提供完整的ServiceMesh能力的同时(ASM还在底层横向拉通了阿里云云原生的各种能力,不在本篇讲述范围),免去了用户搭建和运维ServiceMesh平台istio的繁琐工作。本篇将分享如何将我们自己的GRPC服务,托管到阿里云的服务网格中。

1. grpc服务



grpc协议相比http而言,既具备http跨操作系统和编程语言的好处,又提供了基于流的通信优势。而且,grpc逐渐成为工业界的标准,一旦我们的grpc服务可以mesh化,那么更多的非标准协议就可以通过转为grpc协议的方式,低成本地接入服务网格,实现跨技术栈的服务通信。



grpc服务的示例部分使用最普遍的编程语言Java及最高效的编程框架SpringBoot。示例的拓扑示意如下:





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



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



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



可以通过访问容器服务的命名空间界面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



可以通过访问容器服务的如下界面进行验证:

  • 无状态应用 https://cs.console.aliyun.com/#/k8s/deployment/list

  • 容器组 https://cs.console.aliyun.com/#/k8s/pod/list

  • 服务 https://cs.console.aliyun.com/#/k8s/service/list

通过如下命令,确认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端口。

余文测试验证环节将使用到这里配置的入口网关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的部署后,我们首先验证如下链路的流量:



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



到此,基于ASM的GRPC服务部署实践分享完毕。欢迎技术交流。

发布于: 2020 年 06 月 26 日 阅读数: 1431
用户头像

韩陆

关注

https://github.com/feuyeux 2012.12.21 加入

阿里云云原生小学生

评论 (4 条评论)

发布
用户头像
写的很详细。
详细代码请clone。这里的link有点问题,能否fix一下。
2020 年 06 月 28 日 10:33
回复
非常感谢你的反馈!已经修正。是发布的时候系统自动转移了_字符:
https://github.com/feuyeux/asm-best-practises/tree/master/grpc_springboot_demo
2020 年 06 月 28 日 16:23
回复
用户头像
感谢六爷分享干货,InfoQ首页推荐。
2020 年 06 月 27 日 19:30
回复
用户头像
“也将深入影响社会分工这个观点”,感觉很深刻
2020 年 06 月 27 日 03:08
回复
没有更多了
基于阿里云服务网格(ASM)的GRPC服务部署实践