grpc协议相比http而言,既具备http跨操作系统和编程语言的好处,又提供了基于流的通信优势。而且,grpc逐渐成为工业界的标准,一旦我们的grpc服务可以mesh化,那么更多的非标准协议就可以通过转为grpc协议的方式,低成本地接入服务网格,实现跨技术栈的服务通信。
grpc服务的示例部分使用最普遍的编程语言Java及最高效的编程框架SpringBoot。示例的拓扑示意如下:
1.1 springboot
common——proto2java
示例工程包含三个模块,分别是common、provider、consumer。其中,common负责将定义grpc服务的protobuf转换为java的rpc模板代码;后两者对其依赖,分别实现grpc的服务端和客户端。
示例工程的protobuf定义如下,实现了两个方法SayHello和SayBye。SayHello的入参是一个字符串,返回一个字符串;SayBye只有一个字符串类型的出参。
common构建过程使用protobuf-maven-plugin自动生成rpc模板代码。
provider——grpc-spring-boot-starter
provider依赖grpc-spring-boot-starter包以最小化编码,实现grpc服务端逻辑。示例实现了两套grpc方法,以在后文演示不同流量的返回结果不同。
第一套方法示意如下:
第二套方法示意如下:
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 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 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 |
| 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! |
|
|
|
|
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找到并认为是一个服务的两个版本。