Spring Boot+gRPC構(gòu)建微服務(wù)并部署到Istio(詳細(xì)教程)
點擊上方[全棧開發(fā)者社區(qū)]→右上角[...]→[設(shè)為星標(biāo)?
點擊領(lǐng)取全棧資料:全棧資料
作為Service Mesh和云原生技術(shù)的忠實擁護(hù)者,我卻一直沒有開發(fā)過Service Mesh的應(yīng)用。正好最近受夠了Spring Cloud的“折磨”,對Kubernetes也可以熟練使用了,而且網(wǎng)上幾乎沒有Spring Boot微服務(wù)部署到Istio的案例,我就開始考慮用Spring Boot寫個微服務(wù)的Demo并且部署到Istio。項目本身不復(fù)雜,就是發(fā)送一個字符串并且返回一個字符串的最簡單的Demo。
題外話:我本來是想用Spring MVC寫的——因為周圍有的同學(xué)不相信Spring MVC也可以開發(fā)微服務(wù),但是Spring MVC的各種配置和依賴問題把我整的想吐,為了少掉幾根頭發(fā),還是用了方便好用的Spring Boot。
為什么要用Istio?
目前,對于Java技術(shù)棧來說,構(gòu)建微服務(wù)的最佳選擇是Spring Boot而Spring Boot一般搭配目前落地案例很多的微服務(wù)框架Spring Cloud來使用。
Spring Cloud看似很完美,但是在實際上手開發(fā)后,很容易就會發(fā)現(xiàn)Spring Cloud存在以下比較嚴(yán)重的問題:
服務(wù)治理相關(guān)的邏輯存在于Spring Cloud Netflix等SDK中,與業(yè)務(wù)代碼緊密耦合。 SDK對業(yè)務(wù)代碼侵入太大,SDK發(fā)生升級且無法向下兼容時,業(yè)務(wù)代碼必須做出改變以適配SDK的升級——即使業(yè)務(wù)邏輯并沒有發(fā)生任何變化。 各種組件令人眼花繚亂,質(zhì)量也參差不齊,學(xué)習(xí)成本太高,且組件之間代碼很難完全復(fù)用,僅僅為了實現(xiàn)治理邏輯而學(xué)習(xí)SDK也并不是很好的選擇。 綁定于Java技術(shù)棧,雖然可以接入其他語言但要手動實現(xiàn)服務(wù)治理相關(guān)的邏輯,不符合微服務(wù)“可以用多種語言進(jìn)行開發(fā)”的原則。 Spring Cloud僅僅是一個開發(fā)框架,沒有實現(xiàn)微服務(wù)所必須的服務(wù)調(diào)度、資源分配等功能,這些需求要借助Kubernetes等平臺來完成。但Spring Cloud與Kubernetes功能上有重合,且部分功能也存在沖突,二者很難完美配合。
替代Spring Cloud的選擇有沒有呢?有!它就是Istio。
Istio徹底把治理邏輯從業(yè)務(wù)代碼中剝離出來,成為了獨立的進(jìn)程(Sidecar)。部署時兩者部署在一起,在一個Pod里共同運行,業(yè)務(wù)代碼完全感知不到Sidecar的存在。這就實現(xiàn)了治理邏輯對業(yè)務(wù)代碼的零侵入——實際上不僅是代碼沒有侵入,在運行時兩者也沒有任何的耦合。這使得不同的微服務(wù)完全可以使用不同語言、不同技術(shù)棧來開發(fā),也不用擔(dān)心服務(wù)治理問題,可以說這是一種很優(yōu)雅的解決方案了。
所以,“為什么要使用Istio”這個問題也就迎刃而解了——因為Istio解決了傳統(tǒng)微服務(wù)諸如業(yè)務(wù)邏輯與服務(wù)治理邏輯耦合、不能很好地實現(xiàn)跨語言等痛點,而且非常容易使用。只要會用Kubernetes,學(xué)習(xí)Istio的使用一點都不困難。
為什么要使用gRPC作為通信框架?
在微服務(wù)架構(gòu)中,服務(wù)之間的通信是一個比較大的問題,一般采用RPC或者RESTful API來實現(xiàn)。
Spring Boot可以使用RestTemplate調(diào)用遠(yuǎn)程服務(wù),但這種方式不直觀,代碼也比較復(fù)雜,進(jìn)行跨語言通信也是個比較大的問題;而gRPC相比Dubbo等常見的Java RPC框架更加輕量,使用起來也很方便,代碼可讀性高,并且與Istio和Kubernetes可以很好地進(jìn)行整合,在Protobuf和HTTP2的加持下性能也還不錯,所以這次選擇了gRPC來解決Spring Boot微服務(wù)間通信的問題。并且,雖然gRPC沒有服務(wù)發(fā)現(xiàn)、負(fù)載均衡等能力,但是Istio在這方面就非常強(qiáng)大,兩者形成了完美的互補關(guān)系。
由于考慮到各種grpc-spring-boot-starter可能會對Spring Boot與Istio的整合產(chǎn)生不可知的副作用,所以這一次我沒有用任何的grpc-spring-boot-starter,而是直接手寫了gRPC與Spring Boot的整合。不想借助第三方框架整合gRPC和Spring Boot的可以簡單參考一下我的實現(xiàn)。
編寫業(yè)務(wù)代碼
首先使用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/> <!-- lookup parent from repository -->
</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,定義服務(wù)間的接口如下:
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;
}
很簡單,就是發(fā)送一個name返回一個帶name的message。
然后生成服務(wù)端和客戶端的代碼,并且放到j(luò)ava文件夾下。這部分內(nèi)容可以參考gRPC的官方文檔。
有了API模塊之后,就可以編寫服務(wù)提供者(服務(wù)端)和服務(wù)消費者(客戶端)了。這里我們重點看一下如何整合gRPC和Spring Boot。
服務(wù)端
業(yè)務(wù)代碼非常簡單:
/**
* 服務(wù)端業(yè)務(wù)邏輯實現(xiàn)
*
* @author 江文
*/
@Slf4j
@Component
public class HelloServiceImpl extends HelloWorldGrpc.HelloWorldImplBase {
@Override
public void sayHello(HelloWorldService.HelloRequest request,
StreamObserver<HelloWorldService.HelloResponse> responseObserver) {
// 根據(jù)請求對象建立響應(yīng)對象,返回響應(yīng)信息
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());
}
}
光有業(yè)務(wù)代碼還不行,我們還需要在應(yīng)用啟動時把gRPC Server也給一起啟動起來。首先寫一下Server端的啟動、關(guān)閉等邏輯:
/**
* gRPC Server的配置——啟動、關(guān)閉等
* 需要使用<code>@Component</code>注解注冊為一個Spring Bean
*
* @author 江文
*/
@Slf4j
@Component
public class GrpcServerConfiguration {
@Autowired
HelloServiceImpl service;
/** 注入配置文件中的端口信息 */
@Value("${grpc.server-port}")
private int port;
private Server server;
public void start() throws IOException {
// 構(gòu)建服務(wù)端
log.info("Starting gRPC on port {}.", port);
server = ServerBuilder.forPort(port).addService(service).build().start();
log.info("gRPC server started, listening on {}.", port);
// 添加服務(wù)端關(guān)閉的邏輯
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) {
// 關(guān)閉服務(wù)端
server.shutdown();
}
}
public void block() throws InterruptedException {
if (server != null) {
// 服務(wù)端啟動后直到應(yīng)用關(guān)閉都處于阻塞狀態(tài),方便接收請求
server.awaitTermination();
}
}
}
定義好gRPC的啟動、停止等邏輯后,就可以使用CommandLineRunner把它加入到Spring Boot的啟動中去了:
/**
* 加入gRPC Server的啟動、停止等邏輯到Spring Boot的生命周期中
*
* @author 江文
*/
@Component
public class GrpcCommandLineRunner implements CommandLineRunner {
@Autowired
GrpcServerConfiguration configuration;
@Override
public void run(String... args) throws Exception {
configuration.start();
configuration.block();
}
}
之所以要把gRPC的邏輯注冊成Spring Bean,就是因為在這里要獲取到它的實例并進(jìn)行相應(yīng)的操作。
這樣,在啟動Spring Boot時,由于CommandLineRunner的存在,gRPC服務(wù)端也就可以一同啟動了。
客戶端
業(yè)務(wù)代碼同樣非常簡單:
/**
* 客戶端業(yè)務(wù)邏輯實現(xiàn)
*
* @author 江文
*/
@RestController
@Slf4j
public class HelloController {
@Autowired
GrpcClientConfiguration configuration;
@GetMapping("/hello")
public String hello(@RequestParam(name = "name", defaultValue = "JiangWen", required = false) String name) {
// 構(gòu)建一個請求
HelloWorldService.HelloRequest request = HelloWorldService.HelloRequest
.newBuilder()
.setName(name)
.build();
// 使用stub發(fā)送請求至服務(wù)端
HelloWorldService.HelloResponse response = configuration.getStub().sayHello(request);
log.info("Server response received: [{}]", response.getMessage());
return response.getMessage();
}
}
在啟動客戶端時,我們需要打開gRPC的客戶端,并獲取到channel和stub以進(jìn)行RPC通信,來看看gRPC客戶端的實現(xiàn)邏輯:
/**
* gRPC Client的配置——啟動、建立channel、獲取stub、關(guān)閉等
* 需要注冊為Spring Bean
*
* @author 江文
*/
@Slf4j
@Component
public 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獲取到服務(wù)端的stub
stub = HelloWorldGrpc.newBlockingStub(channel);
log.info("gRPC client started, server address: {}:{}", host, port);
}
public void shutdown() throws InterruptedException {
// 調(diào)用shutdown方法后等待1秒關(guān)閉channel
channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
log.info("gRPC client shut down successfully.");
}
public HelloWorldGrpc.HelloWorldBlockingStub getStub() {
return this.stub;
}
}
比服務(wù)端要簡單一些。
最后,仍然需要一個CommandLineRunner把這些啟動邏輯加入到Spring Boot的啟動過程中:
/**
* 加入gRPC Client的啟動、停止等邏輯到Spring Boot生命周期中
*
* @author 江文
*/
@Component
@Slf4j
public class GrpcClientCommandLineRunner implements CommandLineRunner {
@Autowired
GrpcClientConfiguration configuration;
@Override
public void run(String... args) {
// 開啟gRPC客戶端
configuration.start();
// 添加客戶端關(guān)閉的邏輯
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
configuration.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
}
}
編寫Dockerfile
業(yè)務(wù)代碼跑通之后,就可以制作Docker鏡像,準(zhǔn)備部署到Istio中去了。
在開始編寫Dockerfile之前,先改動一下客戶端的配置文件:
server:
port: 19090
spring:
application:
name: spring-boot-istio-client
server-host: ${server-host}
server-port: ${server-port}
接下來編寫Dockerfile:
服務(wù)端:
FROM openjdk:8u121-jdk
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezone
ADD /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
可以看到這里添加了啟動參數(shù),配合前面的配置,當(dāng)這個鏡像部署到Kubernetes集群時,就可以在Kubernetes的配合之下通過服務(wù)名找到服務(wù)端了。
同時,服務(wù)端和客戶端的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>
這樣執(zhí)行mvn clean package時就可以同時把docker鏡像構(gòu)建出來了。
編寫部署文件
有了鏡像之后,就可以寫部署文件了:
服務(wù)端:
apiVersion: v1
kind: Service
metadata:
name: spring-boot-istio-server
spec:
type: ClusterIP
ports:
- name: http
port: 18080
targetPort: 18080
- name: grpc
port: 18888
targetPort: 18888
selector:
app: spring-boot-istio-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-istio-server
spec:
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
主要是暴露服務(wù)端的端口:18080和gRPC Server的端口18888,以便可以從Pod外部訪問服務(wù)端。
客戶端:
apiVersion: v1
kind: Service
metadata:
name: spring-boot-istio-client
spec:
type: ClusterIP
ports:
- name: http
port: 19090
targetPort: 19090
selector:
app: spring-boot-istio-client
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-istio-client
spec:
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,以便訪問客戶端并調(diào)用服務(wù)端。
如果想先試試把它們部署到k8s可不可以正常訪問,可以這樣配置Ingress:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
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的網(wǎng)關(guān)配置文件與k8s不大一樣:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: spring-boot-istio-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: spring-boot-istio
spec:
hosts:
- "*"
gateways:
- spring-boot-istio-gateway
http:
- match:
- uri:
exact: /hello
route:
- destination:
host: spring-boot-istio-client
port:
number: 19090
主要就是暴露/hello這個路徑,并且指定對應(yīng)的服務(wù)和端口。
部署應(yīng)用到Istio
首先搭建k8s集群并且安裝istio。我使用的k8s版本是1.16.0,Istio版本是最新的1.6.0-alpha.1,使用istioctl命令安裝Istio。建議跑通官方的bookinfo示例之后再來部署本項目。
注:以下命令都是在開啟了自動注入Sidecar的前提下運行的
我是在虛擬機(jī)中運行的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
所以,需要設(shè)置IP和端口,以NodePort的方式訪問gateway:
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}')
export SECURE_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}')
export INGRESS_HOST=127.0.0.1
export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT
必須要等到兩個pod全部變?yōu)镽unning而且Ready變?yōu)?/2才算部署完成。
接下來就可以通過
curl -s http://${GATEWAY_URL}/hello
訪問到服務(wù)了。如果成功返回了Hello, JiangWen. This message comes from gRPC.的結(jié)果,沒有出錯則說明部署完成。
覺得本文對你有幫助?請分享給更多人
關(guān)注「全棧開發(fā)者社區(qū)」加星標(biāo),提升全棧技能
本公眾號會不定期給大家發(fā)福利,包括送書、學(xué)習(xí)資源等,敬請期待吧!
如果感覺推送內(nèi)容不錯,不妨右下角點個在看轉(zhuǎn)發(fā)朋友圈或收藏,感謝支持。
好文章,留言、點贊、在看和分享一條龍

