前一篇《基于 Kubernetes+Istio 治理 Java 微服务》介绍了基于Kubernetes 及 Istio 如何一步一步把 Service Mesh 微服务架构玩起来。在该文章中,我们演示了一个非常贴近实战的案例,这里回顾下该案例的结构:
该案例所演示的就是我们日常使用微服务架构开发时,服务间最普遍的通信场景。在 Spring Cloud 微服务体系中,服务间可以通过 Fegin+Ribbon 组合的方式,实现服务间负载均衡方式的 Http 接口调用;但在 Service Mesh 架构中,服务发现及负载均衡等治理逻辑已经由 SideCar 代理,如果还希望延续 Spring Cloud 场景下服务间接口调用的代码体验,一般可以通过改写 Feign 组件,去掉其中关于服务治理的逻辑,只保留简单的接口声明式调用逻辑来实现。
上述案例中“micro-api->micro-order”之间的服务通信调用,就是基于该方式实现的(可参考之前的文章)。但在微服务架构中除了采用 Http 协议通信外,对于某些对性能有着更高要求的系统来说,采用通信效率更高的 RPC 协议往往是更合适的选择。
在基于 Spring Cloud 框架的微服务体系中,服务之间也可以通过 RPC 协议通信,但由于服务治理的需要,也需要一套类似于 Fegin+Ribbon 组合的 SDK 支持。例如 gRPC 框架就有针对 Spring Boot 框架的“grpc-client-spring-boot-starter”依赖支持!该项目是一个 gRPC 的 Spring Boot 模块,可以在 Spring Boot 中内嵌一个 gRPC Server 对外提供服务,并支持 Spring Cloud 的服务发现、注册、链路跟踪等等。
那么在 Service Mesh 微服务体系下,服务间基于 gRPC 框架的通信应该怎么实现呢?接下来,我将以案例中 “micro-order->micro-pay” 之间的服务调用为例,演示在 Service Mesh 微服务架构下实现服务间的 gRPC 通信调用,并将案例中 Http+gRPC 服务间通信的完整场景串起来。
1. gRPC 概述
在演示 Service Mesh 微服务架构下的 gRPC 通信场景之前,我们先简单介绍下 RPC 协议及 gRPC 框架的基本知识。
RPC(Remote Procedure Call),又称远程过程调用,是一种通过掩藏底层网络通信复杂性,从而屏蔽远程和本地调用区别的通信方式。相比于 Http 协议,RPC 协议属于一种自定义的 TCP 协议,从而在实现时避免了一些 Http 协议信息的臃肿问题,实现了更高效率的通信。
在主流实现 RPC 协议的框架中,比较著名的有 Dubbo、Thrift 及 gRPC 等。因为目前主流的容器发布平台Kubernetes,以及 Service Mesh 开源平台 Istio 都是通过 gRPC 协议来实现内部组件之间的交互,所以在 Service Mesh 微服务架构中,服务间通信采用 gRPC 协议,从某种角度上说会更具有原生优势。况且在此之前,gRPC 框架已经在分布式、多语言服务场景中得到了大量应用,因此可以预测在 Service Mesh 微服务架构场景下,基于 gRPC 框架的微服务通信方式会逐步成为主流。
gRPC 是 Google 发布的基于 HTTP/2.0 传输层协议承载的高性能开源软件框架,提供了支持多种编程语言的、对网络设备进行配置和纳管的方法。由于是开源框架,通信的双方可以进行二次开发,所以客户端和服务器端之间的通信会更加专注于业务层面的内容,减少了对由 gRPC 框架实现的底层通信的关注。
接下来的内容就具体演示在 Service Mesh 微服务架构下,实现微服务“micro-order->micro-pay”的 gRPC 通信调用。
2. 构建 gRPC 服务端程序 (micro-pay)
首先从 gRPC 服务端的角度,在微服务 micro-pay 项目中集成 gRPC-Java,并实现一个 gRPC 服务端程序。具体如下:
2.1 构建 Spring Boot 基本工程(micro-pay/micro-pay-client)
使用 Spring Boot 框架构建基本的 Maven 工程,为了工程代码的复用,这里单独抽象一个 micro-pay-client 工程,并定义 micro-pay 微服务 gRPC 服务接口的 protobuf 文件(*/proto/paycore.proto),代码如下:
syntax = "proto3";package com.wudimanong.pay.client;option java_multiple_files = true;option java_package = "com.wudimanong.micro.pay.proto";service PayService {//定义支付rpc方法rpc doPay (PayRequest) returns (PayResponse);}message PayRequest {string orderId = 1;int32 amount=2;}message PayResponse {int32 status = 1;}
如上所示,创建了一个基于 protobuf 协议的支付接口定义文件,其中定义了支付服务 PayService 及其中的 doPay 支付 rpc 方法,并定义了其请求和返回参数对象,具体的语法遵循“proto3”协议。 为了能够正常编译和生成 protobuf 文件所定义服务接口的代码,需要在项目 pom.xml 文件中引入 jar 包依赖及 Maven 编译插件配置,代码如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">....<dependencies>....<!--gRPC通信类库(截止目前的最新版本)--><dependency><groupId>io.grpc</groupId><artifactId>grpc-all</artifactId><version>1.36.1</version></dependency></dependencies><build><!--引入gRpc框架proto文件编译生产插件--><extensions><extension><groupId>kr.motd.maven</groupId><artifactId>os-maven-plugin</artifactId><version>1.6.2</version></extension></extensions><plugins><plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.6.1</version><configuration><protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact><pluginId>grpc-java</pluginId><pluginArtifact>io.grpc:protoc-gen-grpc-java:1.36.0:exe:${os.detected.classifier}</pluginArtifact></configuration><executions><execution><goals><goal>compile</goal><goal>compile-custom</goal></goals></execution></executions></plugin></plugins></build></project>
这是单独关于 gRPC 接口 proto 文件定义的工程,定义后编译工程,maven 就会根据前面定义的 paycore.proto 文件生成 gRPC 服务端/客户端相关代码。
完成后,继续构建 micro-pay 微服务的 springboot 工程代码,并在其 pom.xml 文件中引入上述 gRPC 协议文件定义的依赖,例如:
<!--引入支付服务gRPC ProtoBuf定义依赖--><dependency><groupId>com.wudimanong</groupId><artifactId>micro-pay-client</artifactId><version>1.0-SNAPSHOT</version></dependency>
在 micro-pay-client 工程中所引入的 gRPC 相关的依赖及插件配置会自动继承至 micro-pay 工程。
2.2 编写 gRPC 支付服务代码
在 micro-pay 代码工程中创建一个 PayCoreProvider 接口代码,用于表示支付 gRPC 服务的入口(类似于 Controller ),其代码如下:
package com.wudimanong.micro.pay.provider;import com.wudimanong.micro.pay.proto.PayRequest;import com.wudimanong.micro.pay.proto.PayResponse;import com.wudimanong.micro.pay.proto.PayServiceGrpc;import io.grpc.stub.StreamObserver;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;@Slf4j@Componentpublic class PayCoreProvider extends PayServiceGrpc.PayServiceImplBase {/*** 实现ProtoBuf中定义的服务方法** @param request* @param responseStreamObserver*/@Overridepublic void doPay(PayRequest request, StreamObserver<PayResponse> responseStreamObserver) {//逻辑处理(简单模拟打印日志)log.info("处理gRPC支付处理请求,orderId->{};payAmount{}", request.getOrderId(), request.getAmount());//构建返回对象(构建处理状态)PayResponse response = PayResponse.newBuilder().setStatus(2).build();//设置数据响应responseStreamObserver.onNext(response);responseStreamObserver.onCompleted();}}
上述代码所引入的一些依赖代码如 PayServiceGrpc 等,就是前面定义 paycore.proto 文件所生成的桩文件代码!由于只是简单测试,这里仅仅打印了下日志就返回了,如果涉及复杂业务还是可以按照 MVC 分层架构思想进行代码拆分!
2.3 编写 gRPC 与 Spring Boot 框架集成配置代码
在 Spring Cloud 微服务中集成 gRPC 可以通过前面提到的“grpc-client-spring-boot-starter”来实现,但目前还没有现成的支持 Service Mesh 架构下的集成 SDK,所以这里通过手工配置定义的方式实现集成。先创建一个配置类,代码如下:
package com.wudimanong.micro.pay.config;import com.wudimanong.micro.pay.provider.PayCoreProvider;import io.grpc.Server;import io.grpc.ServerBuilder;import java.io.IOException;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;@Slf4j@Componentpublic class GrpcServerConfiguration {@AutowiredPayCoreProvider service;/*** 注入配置文件中的端口信息*/@Value("${grpc.server-port}")private int port;private Server server;public void start() throws IOException {// 构建服务端log.info("Starting gRPC on port {}.", port);server = ServerBuilder.forPort(port).addService(service).build().start();log.info("gRPC server started, listening on {}.", port);// 添加服务端关闭的逻辑Runtime.getRuntime().addShutdownHook(new Thread(() -> {log.info("Shutting down gRPC server.");GrpcServerConfiguration.this.stop();log.info("gRPC server shut down successfully.");}));}private void stop() {if (server != null) {// 关闭服务端server.shutdown();}}public void block() throws InterruptedException {if (server != null) {// 服务端启动后直到应用关闭都处于阻塞状态,方便接收请求server.awaitTermination();}}}
如上所示,在该配置代码中,通过 gRPC-Java 依赖所提供的 Server 对象构建了 gRPC 服务端启动、停止、阻塞的方法,并在启动时将前面定义的服务端类通过“.addService()”方法进行了加入(可考虑封装更优雅的方式)。
为了让该配置类与 Spring Boot 集成,再定义一个集成类,代码如下:
package com.wudimanong.micro.pay.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;@Componentpublic class GrpcCommandLineRunner implements CommandLineRunner {@AutowiredGrpcServerConfiguration configuration;@Overridepublic void run(String... args) throws Exception {configuration.start();configuration.block();}}
上述代码会在 Spring Boot 应用启动时自动加载,其中的逻辑就是启动 gRPC 服务,并阻塞等待连接。
接下来在配置文件中定义服务所开启的 gRPC 端口,配置如下:
spring:application:name: micro-payserver:port: 9092#定义gRPC服务开放的端口grpc:server-port: 18888
该配置所定义的参数在前面的服务配置类中引用,表示 gRPC 服务开启的端口,这里定义的是 18888。
到这里 gRPC 服务端工程代码就构建完成了,从整体上看就是 Spring Boot+gRPC 的集成与整合,这其中没有引入 Spring Boot 定制的 gRPC 集成 SDK,目的在于避免其中所涉及的客户端服务治理逻辑(与前面 Http 调用不直接引入 Open Feign 一样)。
3. 构建 gRPC 客户端程序(micro-order)
接下来我们改造 micro-order 微服务,使其成为调用 micro-pay 微服务的 gRPC 客户端程序。
3.1 引入 gRPC 客户端依赖包
引入前面定义 micro-pay gRPC 服务时构建的 micro-pay-client protobuf 工程依赖,代码如下:
<!--引入支付服务gRPC ProtoBuf定义依赖--><dependency><groupId>com.wudimanong</groupId><artifactId>micro-pay-client</artifactId><version>1.0-SNAPSHOT</version></dependency>
3.2 业务逻辑中实现 gRPC 服务调用
接下来在 micro-order 逻辑中调用 gRPC 支付服务,代码示例如下:
@Slf4j@Servicepublic class OrderServiceImpl implements OrderService {/*** 引入gRPC客户端配置依赖*/@AutowiredGrpcClientConfiguration gRpcClent;@Overridepublic CreateOrderBO create(CreateOrderDTO createOrderDTO) {log.info("现在开始处理下单请求.....");//生成订单号String orderId = String.valueOf(new Random(100).nextInt(100000) + System.currentTimeMillis());//构建支付请求(gRPC调用)PayRequest payRequest = PayRequest.newBuilder().setOrderId(orderId).setAmount(createOrderDTO.getAmount()).build();//使用stub发送请求到服务端PayResponse payResponse = gRpcClent.getStub().doPay(payRequest);log.info("pay gRpc response->" + payResponse.toString());return CreateOrderBO.builder().orderId(orderId).status(payResponse.getStatus()).build();}}
如上所示,该业务逻辑在接收 micro-api 通过 Http 调用的请求后,会在逻辑实现过程中通过 gRPC 协议访问支付服务,其中涉及的接口定义代码,由 protobuf 文件所定义。
3.3 gRPC 客户端配置
上述逻辑是通过定义“GrpcClientConfiguration”gRPC 客户端配置类来实现 gRPC 服务调用的,该配置类代码如下:
@Slf4j@Componentpublic class GrpcClientConfiguration {/*** 支付gRPC Server的地址*/@Value("${server-host}")private String host;/*** 支付gRPC Server的端口*/@Value("${server-port}")private int port;private ManagedChannel channel;/*** 支付服务stub对象*/private PayServiceGrpc.PayServiceBlockingStub stub;public void start() {//开启channelchannel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();//通过channel获取到服务端的stubstub = PayServiceGrpc.newBlockingStub(channel);log.info("gRPC client started, server address: {}:{}", host, port);}public void shutdown() throws InterruptedException {//调用shutdown方法后等待1秒关闭channelchannel.shutdown().awaitTermination(1, TimeUnit.SECONDS);log.info("gRPC client shut down successfully.");}public PayServiceGrpc.PayServiceBlockingStub getStub() {return this.stub;}}
如上所示配置代码,通过依服务配置文件指定的 gRPC 服务端地址+端口,实现对 gRPC 客户端的配置,其中主要包括启动和停止方法,并在启动的过程中初始化 gRPC 服务客户端的桩代码的实例(可考虑更优雅地实现)。 在该配置类中所依赖的 gRPC 服务端地址+端口配置,依赖于服务配置文件的定义,代码如下:
spring:application:name: micro-orderserver:port: 9091#支付微服务Grpc服务地址、端口配置server-host: ${grpc_server_host}server-port: ${grpc_server_port}
如果是本地测试可以直接指定 grpc_server_host 及端口的值,但在 Service Mesh 微服务架构中,直接在应用的配置文件中指定其他微服务的地址及端口可能并不是很灵活,这个配置信息将在发布 Kubernetes 集群时,通过 Kubernetes 发布文件注入。
为了让 gRPC 客户端配置与 Spring Boot 集成,这里也需要定义一个 Spring Boot 加载类,代码如下:
@Component@Slf4jpublic class GrpcClientCommandLineRunner implements CommandLineRunner {@AutowiredGrpcClientConfiguration configuration;@Overridepublic void run(String... args) throws Exception {//开启gRPC客户端configuration.start();//添加客户端关闭的逻辑Runtime.getRuntime().addShutdownHook(new Thread(() -> {try {configuration.shutdown();} catch (InterruptedException e) {e.printStackTrace();}}));}}
该代码将在 Spring Boot 应用自动时自动加载!到这里 micro-order gRPC 客户端配置就完成了。
4. 将部署服务至 Service Mesh 架构环境
前面基于“micro-order->micro-pay”微服务间的 gRPC 调用场景,分别将两个微服务改造成了 gRPC 服务端/客户端。但此时从代码上是很难看出来它们二者之间应该怎么实现调用!而这也恰恰就印证了 Service Mesh 架构的优势,服务的发现、及负载均衡调用之类的服务治理逻辑,已经完全不用微服务自己管了!
在 Istio 中,它们是基于 Kubernetes 的 Service 发现机制 + Istio-proxy(SideCar 代理)来实现的。而具体的操作就是通过微服务 Kubernetes 服务发布文件的定义,接下来分别定义 micro-order 及 micro-pay 的 Kubernetes 发布文件。
先看下作为 gRPC 服务端的 micro-pay 的发布文件(micro-pay.yaml),代码如下:
apiVersion: v1kind: Servicemetadata:name: micro-paylabels:app: micro-payservice: micro-payspec:type: ClusterIPports:- name: http#容器暴露端口port: 19092#目标应用端口targetPort: 9092#设置gRPC端口- name: grpcport: 18888targetPort: 18888selector:app: micro-pay---apiVersion: apps/v1kind: Deploymentmetadata:name: micro-pay-v1labels:app: micro-payversion: v1spec:replicas: 2selector:matchLabels:app: micro-payversion: v1template:metadata:labels:app: micro-payversion: v1spec:containers:- name: micro-payimage: 10.211.55.2:8080/micro-service/micro-pay:1.0-SNAPSHOTimagePullPolicy: Alwaystty: trueports:- name: httpprotocol: TCPcontainerPort: 19092#指定服务gRPC端口- name: grpcprotocol: TCPcontainerPort: 18888
如上所示 k8s 发布文件,主要是定义了 Service 服务访问资源及 Deployment 容器编排资源,这两种资源都是 Kubernetes 的资源类型,在容器编排资源和服务资源中分别定义了 gRPC 的访问端口,通过这种设置,后续 gRPC 客户端通过 Service 资源访问服务时,就能够进行端口映射了!
而其他配置则是基本的 Kubernetes 发布部署逻辑,其中涉及的镜像,需要在发布之前,通过构建的方式对项目进行 Docker 镜像打包并上传私有镜像仓库(如果有疑问,可以参考本号之前的文章)。
接下来继续看看作为 gRPC 客户端的 micro-order 微服务的 k8s 发布文件(micro-order.yaml),代码如下:
apiVersion: v1kind: Servicemetadata:name: micro-orderlabels:app: micro-orderservice: micro-orderspec:type: ClusterIPports:- name: http#此处设置80端口的原因在于改造的Mock FeignClient代码默认是基于80端口进行服务调用port: 80targetPort: 9091selector:app: micro-order---apiVersion: apps/v1kind: Deploymentmetadata:name: micro-order-v1labels:app: micro-orderversion: v1spec:replicas: 2selector:matchLabels:app: micro-orderversion: v1template:metadata:labels:app: micro-orderversion: v1spec:containers:- name: micro-orderimage: 10.211.55.2:8080/micro-service/micro-order:1.0-SNAPSHOTimagePullPolicy: Alwaystty: trueports:- name: httpprotocol: TCPcontainerPort: 19091#环境参数设置(设置微服务返回gRPC服务端的地址+端口)env:- name: GRPC_SERVER_HOSTvalue: micro-pay- name: GRPC_SERVER_PORTvalue: "18888"
在该发布文件中,需要说明的主要就是通过容器 env 环境参数的设置,指定了之前 gRPC 客户端服务配置中所依赖的参数变量“GRPC_SERVER_HOST 及 GRPC_SERVER_PORT”,其中服务地址就是 micro-pay 微服务在 Kubernetes 中 Service 资源定义的名称,端口则是 gRPC 服务端所开启的端口。
这样在 gRPC 客户端在 Kubernetes 集群中根据 Service 名称发起微服务调用时,Kubernetes 集群自身的服务发现逻辑就能自动将请求映射到相应的 Pod 资源了!这其实就是 Service Mesh 微服务架构服务发现的基本逻辑。
接下来将微服务进行发布,这里假设你已经部署了一套 Kubernetes 集群并安装了基于 Istio 的 Service Mesh 微服务架构环境,最终的部署效果如下所示:
$ kubectl get podsNAME READY STATUS RESTARTS AGEmicro-api-6455654996-9lsxr 2/2 Running 2 43mmicro-order-v1-744d469d84-rnqq8 2/2 Running 0 6m28smicro-order-v1-744d469d84-vsn5m 2/2 Running 0 6m28smicro-pay-v1-7fd5dd4768-txq9d 2/2 Running 0 43smicro-pay-v1-7fd5dd4768-wqw6b 2/2 Running 0 43s
如上所示,可以看到案例所涉及的微服务都被部署了,并且对应的 SideCar 代理 (istio-proxy) 也被正常启动了!为了演示负载均衡效果,这里 micro-order 及 micro-pay 都分别被部署了两个副本。
5. 微服务多副本负载均衡调用演示
如果环境都没啥问题,此时可以通过调用 Istio Gateway 来访问 micro-api 服务,然后 micro-api 服务会通过 Http 的方式访问 micro-order 服务,之后 micro-order 服务通过 gRPC 协议调用 micro-pay 服务。
通过 curl 命令访问 Istio Gateway 网关服务,效果如下:
curl -H "Content-Type:application/json" -H "Data_Type:msg" -X POST --data '{"businessId": "202012102", "amount": 100, "channel": 2}' http://10.211.55.12:30844/api/order/create
如果正常返回响应结果,则说明上述调用链路走通了!此时分别通过观察服务的业务日志和 istio-proxy 代理日志来加以观测!
其中 micro-pay 两个实例 (PodA~PodB) 业务日志信息:
//支付微服务接口访问日志(POD-A)$ kubectl logs micro-pay-v1-7fd5dd4768-txq9d micro-pay....2021-04-01 14:46:15.818 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : Starting gRPC on port 18888.2021-04-01 14:46:18.859 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : gRPC server started, listening on 18888.2021-04-01 15:07:36.709 INFO 1 --- [ault-executor-0] c.w.micro.pay.provider.PayCoreProvider : 处理gRPC支付处理请求,orderId->1617289656289;payAmount100//支付微服务接口访问日志(POD-B)$ kubectl logs micro-pay-v1-7fd5dd4768-wqw6b micro-pay...2021-04-01 15:34:59.673 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : Starting gRPC on port 18888.2021-04-01 15:35:06.175 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : gRPC server started, listening on 18888.2021-04-01 15:40:22.019 INFO 1 --- [ault-executor-0] c.w.micro.pay.provider.PayCoreProvider : 处理gRPC支付处理请求,orderId->1617291624127;payAmount1002021-04-01 15:44:31.630 INFO 1 --- [ault-executor-2] c.w.micro.pay.provider.PayCoreProvider : 处理gRPC支付处理请求,orderId->1617291867537;payAmount100
可以看到,多次访问接口,基于gRPC的微服务调用也实现了负载均衡调用!接下来分别看下这两个微服务的 istio-proxy(SideCar代理) 的日志,具体如下:
--istio-proxy代理日志(POD-A)$ kubectl logs micro-pay-v1-7fd5dd4768-txq9d istio-proxy...2021-04-01T15:34:48.009972Z info Envoy proxy is ready[2021-04-01T15:40:26.240Z] "POST /com.wudimanong.pay.client.PayService/doPay HTTP/2" 200 - "-" 22 7 498 477 "-" "grpc-java-netty/1.36.1" "8eb318e5-ac09-922d-9ca7-603a5c14bdd5" "micro-pay:18888" "127.0.0.1:18888" inbound|18888|| 127.0.0.1:57506 10.32.0.10:18888 10.32.0.12:36844 outbound_.18888_._.micro-pay.default.svc.cluster.local default2021-04-01T15:45:18.377555Z info xdsproxy disconnected...[2021-04-01T15:45:34.885Z] "POST /com.wudimanong.pay.client.PayService/doPay HTTP/2" 200 - "-" 22 7 1200 171 "-" "grpc-java-netty/1.36.1" "c08d540e-db46-9228-b381-0808ac08377e" "micro-pay:18888" "127.0.0.1:18888" inbound|18888|| 127.0.0.1:33218 10.32.0.10:18888 10.32.0.2:42646 outbound_.18888_._.micro-pay.default.svc.cluster.local default...2021-04-01T15:52:49.825955Z info xdsproxy connecting to upstream XDS server: istiod.istio-system.svc:15012
如上所示,可以看到 istio-proxy 代理日志中显示了通过 post 方式转发 gRPC 服务的情况,而且可以看出 gRPC 是采用 Http/2 实现的!
来源: https://mp.weixin.qq.com/s/rLaviarJAfoJuKUUhR67gg https://github.com/manongwudi/istio-micro-service-demo
