使用 Spring Boot-gRPC 構建微服務並部署到 Istio
作爲Service Mesh
和雲原生技術的忠實擁護者,我卻一直沒有開發過 Service Mesh 的應用。正好最近受夠了 Spring Cloud 的 “折磨”,對 Kubernetes 也可以熟練使用了,而且網上幾乎沒有 Spring Boot 微服務部署到 Istio 的案例,我就開始考慮用 Spring Boot 寫個微服務的 Demo 並且部署到 Istio。項目本身不復雜,就是發送一個字符串並且返回一個字符串的最簡單的 Demo。
題外話:我本來是想用 Spring MVC 寫的——因爲周圍有的同學不相信 Spring MVC 也可以開發微服務,但是 Spring MVC 的各種配置和依賴問題把我整的想吐,爲了少掉幾根頭髮,還是用了方便好用的 Spring Boot。
爲什麼要用 Istio?
目前,對於 Java 技術棧來說,構建微服務的最佳選擇是Spring Boot
而 Spring Boot 一般搭配目前落地案例很多的微服務框架Spring Cloud
來使用。
Spring Cloud 看似很完美,但是在實際上手開發後,很容易就會發現 Spring Cloud 存在以下比較嚴重的問題:
-
服務治理相關的邏輯存在於 Spring Cloud Netflix 等 SDK 中,與業務代碼緊密耦合。
-
SDK 對業務代碼侵入太大,SDK 發生升級且無法向下兼容時,業務代碼必須做出改變以適配 SDK 的升級——即使業務邏輯並沒有發生任何變化。
-
各種組件令人眼花繚亂,質量也參差不齊,學習成本太高,且組件之間代碼很難完全複用,僅僅爲了實現治理邏輯而學習 SDK 也並不是很好的選擇。
-
綁定於 Java 技術棧,雖然可以接入其他語言但要手動實現服務治理相關的邏輯,不符合微服務 “可以用多種語言進行開發” 的原則。
-
Spring Cloud 僅僅是一個開發框架,沒有實現微服務所必須的服務調度、資源分配等功能,這些需求要藉助 Kubernetes 等平臺來完成。但 Spring Cloud 與 Kubernetes 功能上有重合,且部分功能也存在衝突,二者很難完美配合。
替代 Spring Cloud 的選擇有沒有呢?有!它就是 Istio。
Istio 徹底把治理邏輯從業務代碼中剝離出來,成爲了獨立的進程(Sidecar)。部署時兩者部署在一起,在一個 Pod 裏共同運行,業務代碼完全感知不到 Sidecar 的存在。這就實現了治理邏輯對業務代碼的零侵入——實際上不僅是代碼沒有侵入,在運行時兩者也沒有任何的耦合。這使得不同的微服務完全可以使用不同語言、不同技術棧來開發,也不用擔心服務治理問題,可以說這是一種很優雅的解決方案了。
所以,“爲什麼要使用 Istio” 這個問題也就迎刃而解了——因爲 Istio 解決了傳統微服務諸如業務邏輯與服務治理邏輯耦合、不能很好地實現跨語言等痛點,而且非常容易使用。只要會用 Kubernetes,學習 Istio 的使用一點都不困難。
爲什麼要使用 gRPC 作爲通信框架?
在微服務架構中,服務之間的通信是一個比較大的問題,一般採用 RPC 或者 RESTful API 來實現。
Spring Boot 可以使用RestTemplate
調用遠程服務,但這種方式不直觀,代碼也比較複雜,進行跨語言通信也是個比較大的問題;而gRPC
相比 Dubbo 等常見的 Java RPC 框架更加輕量,使用起來也很方便,代碼可讀性高,並且與 Istio 和 Kubernetes 可以很好地進行整合,在 Protobuf 和 HTTP2 的加持下性能也還不錯,所以這次選擇了 gRPC 來解決 Spring Boot 微服務間通信的問題。並且,雖然 gRPC 沒有服務發現、負載均衡等能力,但是 Istio 在這方面就非常強大,兩者形成了完美的互補關係。
由於考慮到各種grpc-spring-boot-starter
可能會對 Spring Boot 與 Istio 的整合產生不可知的副作用,所以這一次我沒有用任何的grpc-spring-boot-starter
,而是直接手寫了 gRPC 與 Spring Boot 的整合。不想借助第三方框架整合 gRPC 和 Spring Boot 的可以簡單參考一下我的實現。
編寫業務代碼
首先使用Spring Initializr
建立父級項目spring-boot-istio
,並引入gRPC
的依賴。pom 文件如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>spring-boot-istio-api</module>
<module>spring-boot-istio-server</module>
<module>spring-boot-istio-client</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>
<groupId>site.wendev</groupId>
<artifactId>spring-boot-istio</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-istio</name>
<description>Demo project for Spring Boot With Istio.</description>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.28.1</version>
</dependency>
</dependencies>
</dependencyManagement></project>
然後建立公共依賴模塊spring-boot-istio-api
,pom 文件如下,主要就是 gRPC 的一些依賴:
<?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">
<parent>
<artifactId>spring-boot-istio</artifactId>
<groupId>site.wendev</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-istio-api</artifactId>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
<build>
<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.11.3:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.28.1:exe:${os.detected.classifier}</pluginArtifact>
<protocExecutable>/Users/jiangwen/tools/protoc-3.11.3/bin/protoc</protocExecutable>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build></project>
建立 src/main/proto 文件夾,在此文件夾下建立hello.proto
,定義服務間的接口如下:
syntax = "proto3";option java_package = "site.wendev.spring.boot.istio.api";option java_outer_classname = "HelloWorldService";package helloworld;service HelloWorld { rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
string name = 1;
}message HelloResponse { string message = 1;
}
很簡單,就是發送一個name
返回一個帶name
的message
。
然後生成服務端和客戶端的代碼,並且放到 java 文件夾下。這部分內容可以參考 gRPC 的官方文檔。
有了 API 模塊之後,就可以編寫服務提供者(服務端)和服務消費者(客戶端)了。這裏我們重點看一下如何整合 gRPC 和 Spring Boot。
服務端
業務代碼非常簡單:
/**
* 服務端業務邏輯實現
*
* @author 江文
* @date 2020/4/12 2:49 下午
*/@Slf4j@Componentpublic class HelloServiceImpl extends HelloWorldGrpc.HelloWorldImplBase { @Override
public void sayHello(HelloWorldService.HelloRequest request,
StreamObserver<HelloWorldService.HelloResponse> responseObserver) { // 根據請求對象建立響應對象,返回響應信息
HelloWorldService.HelloResponse response = HelloWorldService.HelloResponse
.newBuilder()
.setMessage(String.format("Hello, %s. This message comes from gRPC.", request.getName()))
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
log.info("Client Message Received:[{}]", request.getName());
}
}
光有業務代碼還不行,我們還需要在應用啓動時把 gRPC Server 也給一起啓動起來。首先寫一下 Server 端的啓動、關閉等邏輯:
/**
* gRPC Server的配置——啓動、關閉等
* 需要使用<code>@Component</code>註解註冊爲一個Spring Bean
*
* @author 江文
* @date 2020/4/12 2:56 下午
*/@Slf4j@Componentpublic class GrpcServerConfiguration { @Autowired
HelloServiceImpl 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 的啓動、停止等邏輯後,就可以使用CommandLineRunner
把它加入到 Spring Boot 的啓動中去了:
/**
* 加入gRPC Server的啓動、停止等邏輯到Spring Boot的生命週期中
*
* @author 江文
* @date 2020/4/12 3:10 下午
*/@Componentpublic class GrpcCommandLineRunner implements CommandLineRunner { @Autowired
GrpcServerConfiguration configuration; @Override
public void run(String... args) throws Exception {
configuration.start();
configuration.block();
}
}
之所以要把 gRPC 的邏輯註冊成 Spring Bean,就是因爲在這裏要獲取到它的實例並進行相應的操作。
這樣,在啓動 Spring Boot 時,由於 CommandLineRunner 的存在,gRPC 服務端也就可以一同啓動了。
客戶端
業務代碼同樣非常簡單:
/**
* 客戶端業務邏輯實現
*
* @author 江文
* @date 2020/4/12 3:26 下午
*/@RestController@Slf4jpublic class HelloController { @Autowired
GrpcClientConfiguration configuration; @GetMapping("/hello") public String hello(@RequestParam(name = "name", defaultValue = "JiangWen", required = false) String name) { // 構建一個請求
HelloWorldService.HelloRequest request = HelloWorldService.HelloRequest
.newBuilder()
.setName(name)
.build(); // 使用stub發送請求至服務端
HelloWorldService.HelloResponse response = configuration.getStub().sayHello(request);
log.info("Server response received: [{}]", response.getMessage()); return response.getMessage();
}
}
在啓動客戶端時,我們需要打開 gRPC 的客戶端,並獲取到channel
和stub
以進行 RPC 通信,來看看 gRPC 客戶端的實現邏輯:
/**
* gRPC Client的配置——啓動、建立channel、獲取stub、關閉等
* 需要註冊爲Spring Bean
*
* @author 江文
* @date 2020/4/12 3:27 下午
*/@Slf4j@Componentpublic class GrpcClientConfiguration { /** gRPC Server的地址 */
@Value("${server-host}") private String host; /** gRPC Server的端口 */
@Value("${server-port}") private int port; private ManagedChannel channel; private HelloWorldGrpc.HelloWorldBlockingStub stub; public void start() { // 開啓channel
channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); // 通過channel獲取到服務端的stub
stub = HelloWorldGrpc.newBlockingStub(channel);
log.info("gRPC client started, server address: {}:{}", host, port);
} public void shutdown() throws InterruptedException { // 調用shutdown方法後等待1秒關閉channel
channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
log.info("gRPC client shut down successfully.");
} public HelloWorldGrpc.HelloWorldBlockingStub getStub() { return this.stub;
}
}
比服務端要簡單一些。
最後,仍然需要一個 CommandLineRunner 把這些啓動邏輯加入到 Spring Boot 的啓動過程中:
/**
* 加入gRPC Client的啓動、停止等邏輯到Spring Boot生命週期中
*
* @author 江文
* @date 2020/4/12 3:36 下午
*/@Component@Slf4jpublic class GrpcClientCommandLineRunner implements CommandLineRunner { @Autowired
GrpcClientConfiguration configuration; @Override
public void run(String... args) { // 開啓gRPC客戶端
configuration.start();
// 添加客戶端關閉的邏輯
Runtime.getRuntime().addShutdownHook(new Thread(() -> { try {
configuration.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
}
}
編寫 Dockerfile
業務代碼跑通之後,就可以製作 Docker 鏡像,準備部署到 Istio 中去了。
在開始編寫 Dockerfile 之前,先改動一下客戶端的配置文件:
server:
port: 19090spring:
application:
name: spring-boot-istio-clientserver-host: ${server-host}server-port: ${server-port}
接下來編寫 Dockerfile:
服務端:
FROM openjdk:8u121-jdkRUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezoneADD /target/spring-boot-istio-server-0.0.1-SNAPSHOT.jar /ENV SERVER_PORT="18080"ENTRYPOINT java -jar /spring-boot-istio-server-0.0.1-SNAPSHOT.jar
主要是規定服務端應用的端口爲 18080,並且在容器啓動時讓服務端也一起啓動。
客戶端:
FROM openjdk:8u121-jdkRUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezoneADD /target/spring-boot-istio-client-0.0.1-SNAPSHOT.jar /ENV GRPC_SERVER_HOST="spring-boot-istio-server"ENV GRPC_SERVER_PORT="18888"ENTRYPOINT java -jar /spring-boot-istio-client-0.0.1-SNAPSHOT.jar \
--server-host=$GRPC_SERVER_HOST \
--server-port=$GRPC_SERVER_PORT
可以看到這裏添加了啓動參數,配合前面的配置,當這個鏡像部署到 Kubernetes 集羣時,就可以在 Kubernetes 的配合之下通過服務名找到服務端了。
同時,服務端和客戶端的 pom 文件中添加:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.13</version>
<dependencies>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>default</id>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
<configuration>
<repository>wendev-docker.pkg.coding.net/develop/docker/${project.artifactId} </repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
這樣執行mvn clean package
時就可以同時把 docker 鏡像構建出來了。
編寫部署文件
有了鏡像之後,就可以寫部署文件了:
服務端:
apiVersion: v1kind: Servicemetadata:
name: spring-boot-istio-serverspec:
type: ClusterIP
ports:
- name: http
port: 18080
targetPort: 18080
- name: grpc
port: 18888
targetPort: 18888
selector:
app: spring-boot-istio-server---apiVersion: apps/v1kind: Deploymentmetadata:
name: spring-boot-istio-serverspec:
replicas: 1
selector:
matchLabels:
app: spring-boot-istio-server
template:
metadata:
labels:
app: spring-boot-istio-server
spec:
containers:
- name: spring-boot-istio-server
image: wendev-docker.pkg.coding.net/develop/docker/spring-boot-istio-server:0.0.1-SNAPSHOT
imagePullPolicy: Always
tty: true
ports:
- name: http
protocol: TCP
containerPort: 18080
- name: grpc
protocol: TCP
containerPort: 18888
主要是暴露服務端的端口:18080 和 gRPC Server 的端口 18888,以便可以從 Pod 外部訪問服務端。
客戶端:
apiVersion: v1kind: Servicemetadata:
name: spring-boot-istio-clientspec:
type: ClusterIP
ports:
- name: http
port: 19090
targetPort: 19090
selector:
app: spring-boot-istio-client---apiVersion: apps/v1kind: Deploymentmetadata:
name: spring-boot-istio-clientspec:
replicas: 1
selector:
matchLabels:
app: spring-boot-istio-client
template:
metadata:
labels:
app: spring-boot-istio-client
spec:
containers:
- name: spring-boot-istio-client
image: wendev-docker.pkg.coding.net/develop/docker/spring-boot-istio-client:0.0.1-SNAPSHOT
imagePullPolicy: Always
tty: true
ports:
- name: http
protocol: TCP
containerPort: 19090
主要是暴露客戶端的端口 19090,以便訪問客戶端並調用服務端。
如果想先試試把它們部署到 k8s 可不可以正常訪問,可以這樣配置 Ingress:
apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:
name: nginx-web
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/use-reges: "true"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/rewrite-target: /spec:
rules:
- host: dev.wendev.site
http:
paths:
- path: /
backend:
serviceName: spring-boot-istio-client
servicePort: 19090
Istio 的網關配置文件與 k8s 不大一樣:
apiVersion: networking.istio.io/v1alpha3kind: Gatewaymetadata:
name: spring-boot-istio-gatewayspec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"---apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata:
name: spring-boot-istiospec:
hosts:
- "*"
gateways:
- spring-boot-istio-gateway
http:
- match:
- uri:
exact: /hello
route:
- destination:
host: spring-boot-istio-client
port:
number: 19090
主要就是暴露/hello
這個路徑,並且指定對應的服務和端口。
部署應用到 Istio
首先搭建 k8s 集羣並且安裝 istio。我使用的 k8s 版本是1.16.0
,Istio 版本是最新的1.6.0-alpha.1
,使用istioctl
命令安裝 Istio。建議跑通官方的bookinfo
示例之後再來部署本項目。
注:以下命令都是在開啓了自動注入 Sidecar 的前提下運行的
我是在虛擬機中運行的 k8s,所以istio-ingressgateway
沒有外部 ip:
$ kubectl get svc istio-ingressgateway -n istio-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
istio-ingressgateway NodePort 10.97.158.232 <none> 15020:30388/TCP,80:31690/TCP,443:31493/TCP,15029:32182/TCP,15030:31724/TCP,15031:30887/TCP,15032:30369/TCP,31400:31122/TCP,15443:31545/TCP 26h
所以,需要設置 IP 和端口,以 NodePort 的方式訪問 gateway:
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.)].nodePort}')export SECURE_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.)].nodePort}')export INGRESS_HOST=127.0.0.1export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT
這樣就可以了。
接下來部署服務:
$ kubectl apply -f spring-boot-istio-server.yml
$ kubectl apply -f spring-boot-istio-client.yml
$ kubectl apply -f istio-gateway.yml
必須要等到兩個 pod 全部變爲 Running 而且 Ready 變爲 2/2 纔算部署完成。
接下來就可以通過
curl -s http://${GATEWAY_URL}/hello
訪問到服務了。如果成功返回了Hello, JiangWen. This message comes from gRPC.
的結果,沒有出錯則說明部署完成。
本項目的所有代碼都上傳到了 GitHub,地址: https://github.com/WenDev/spring-boot-istio-demo
作者:江文
來源:www.wendev.site/dcae41fd9142.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/fs4grD_nD0KiT09FmbtEng