<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
大家好,搞微服務也有好幾年時間,從16年開始就一直關注微服務,到現在一直在使用的還是SpringCloud原生那套,雖然後來出現了SpringCloud Alibaba,但由於前面的所有系統框架都已定,就沒有在變化,而在微服務的實施過程,為了降運維的服務度,先後使用了jenkins,docker, kubernetes等,就還是沒有使用過Istio。
基於這個原因,一直想嘗試下開發基於Istio的Service Mesh的應用。正好最近受夠了Spring Cloud的“折磨”,對Kubernetes也可以熟練使用了,而且網上幾乎沒有Spring Boot微服務部署到Istio的案例,我就開始考慮用Spring Boot寫個微服務的Demo並且部署到Istio。專案本身不復雜,就是傳送一個字串並且返回一個字串的最簡單的Demo。
目前,對於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的使用一點都不困難。
在微服務架構中,服務之間的通訊是一個比較大的問題,一般採用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。
1) 伺服器端
業務程式碼非常簡單:
/** * 伺服器端業務邏輯實現 * * @author 江文 * @date 2020/4/12 2:49 下午 */ @Slf4j @Component public 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 下午 */ @Component public 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伺服器端也就可以一同啟動了。
2) 使用者端
業務程式碼同樣非常簡單:
/** * 使用者端業務邏輯實現 * * @author 江文 * @date 2020/4/12 3:26 下午 */ @RestController @Slf4j public 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 @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獲取到伺服器端的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 @Slf4j public 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(); } })); } }
業務程式碼跑通之後,就可以製作Docker映象,準備部署到Istio中去了。
在開始編寫Dockerfile之前,先改動一下使用者端的組態檔:
server: port: 19090 spring: application: name: spring-boot-istio-clientserver-host: ${server-host}server-port: ${server-port}
接下來編寫Dockerfile:
1) 伺服器端:
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
主要是規定伺服器端應用的埠為18080,並且在容器啟動時讓伺服器端也一起啟動。
2) 使用者端:
FROM openjdk:8u121-jdk RUN /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映象構建出來了。
有了映象之後,就可以寫部署檔案了:
1) 伺服器端:
apiVersion: v1 kind: Servicemetadata: 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: Deploymentmetadata: 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
主要是暴露伺服器端的埠:18080和gRPC Server的埠18888,以便可以從Pod外部存取伺服器端。
2) 使用者端:
apiVersion: v1 kind: Servicemetadata: 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: Deploymentmetadata: 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,以便存取使用者端並呼叫伺服器端。
如果想先試試把它們部署到k8s可不可以正常存取,可以這樣設定Ingress:
apiVersion: networking.k8s.io/v1beta1 kind: 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/v1alpha3 kind: Gatewaymetadata: name: spring-boot-istio-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - "*" ---apiVersion: networking.istio.io/v1alpha3 kind: Virtual Servicemetadata: 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這個路徑,並且指定對應的服務和埠。
首先搭建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) AGEistio-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[?(@.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.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地址: github.com/WenDev/spri…
到此這篇關於使用Spring Boot+gRPC構建微服務並部署的文章就介紹到這了,更多相關Spring Boot gRPC微服務內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45