朱曄和你聊Spring系列S1E11:小測Spring Cloud Kubernetes @ 阿里雲K8S
朱曄和你聊Spring系列S1E11:小測Spring Cloud Kubernetes @ 阿里雲K8S
有關Spring Cloud Kubernates(以下簡稱SCK)詳見https://github.com/spring-cloud/spring-cloud-kubernetes,在本文中我們主要測試三個功能:
- 使用Kubernates服務發現配合Spring Cloud Ribbon做服務呼叫
- 讀取Kubernates的ConfigMap配置並且支援修改後動態重新整理
- Spring Boot Actuator對Kubernates Pod資訊的感知
編寫測試程式
首先,我們來建立pom檔案,注意幾點:
- Spring Boot版本不能太高
- 引入了 Spring Boot Web以及Actuator兩個模組,我們開發一個Web專案進行測試
- 引入了 Spring Cloud的Ribbon模組,我們需要測試一下服務呼叫
- 引入了spring-cloud-starter-kubernetes-all依賴,我們的主要測試物件
- 額外引入了docker-maven-plugin外掛用於幫助我們構建映象
- 設定了finalName
檔案如下:
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.9.RELEASE</version> <relativePath/> </parent> <groupId>me.josephzhu</groupId> <artifactId>springcloudk8sdemo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springcloudk8sdemo</name> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-kubernetes-all</artifactId> <version>1.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <finalName>k8sdemo</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>1.0.0</version> <configuration> <imageName>zhuye/${project.artifactId}</imageName> <dockerDirectory>src/main/docker</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> </plugins> </build> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
接下去在src\main\docker目錄下建立Dockerfile檔案:
FROM openjdk:11-jdk-slim
VOLUME /tmp
ADD k8sdemo.jar app.jar
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar
值得注意的是,JVM引數我們希望從環境變數注入。
來看看程式碼,我們首先定義一個配置類:
package me.josephzhu.springcloudk8sdemo; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "bean") @Data public class TestConfig { private String message; private String serviceName; }
有了SCK的幫助,配置可以從ConfigMap載入,之後我們會看到ConfigMap的配置方式。下面我們定義一個控制器扮演服務端的角色:
package me.josephzhu.springcloudk8sdemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
@RestController
public class TestServer {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("servers")
public List<String> servers() {
return discoveryClient.getServices();
}
@GetMapping
public String ip() throws UnknownHostException {
return InetAddress.getLocalHost().getHostAddress();
}
}
可以看到這裡定義了兩個介面:
- servers 用於返回服務發現找到的所有服務(K8S的服務)
- 根路徑返回了當前節點的IP地址
接下去定義另一個控制器扮演客戶端的角色:
package me.josephzhu.springcloudk8sdemo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.net.InetAddress;
import java.net.UnknownHostException;
@RestController
@Slf4j
public class TestClient {
@Autowired
private RestTemplate restTemplate;
@Autowired
private TestConfig testConfig;
@GetMapping("client")
public String client() throws UnknownHostException {
String ip = InetAddress.getLocalHost().getHostAddress();
String response = restTemplate.getForObject("http://"+testConfig.getServiceName()+"/", String.class);
return String.format("%s -> %s", ip, response);
}
}
這裡就一個介面client介面,訪問後通過RestTemplate來訪問服務端根路徑的介面,然後輸出了客戶端和服務端的IP地址。
然後我們定義一個全域性的異常處理器,在出錯的時候我們直接看到是什麼錯:
package me.josephzhu.springcloudk8sdemo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class GlobalAdvice {
@ExceptionHandler(Exception.class)
public String exception(Exception ex){
log.error("error:", ex);
return ex.toString();
}
}
最後我們定義啟動程式:
package me.josephzhu.springcloudk8sdemo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.client.RestTemplate;
import java.lang.management.ManagementFactory;
import java.util.stream.Collectors;
@SpringBootApplication
@EnableDiscoveryClient
@EnableScheduling
@Slf4j
@RibbonClient(name = "k8sdemo")
public class Springcloudk8sdemoApplication {
public static void main(String[] args) {
log.info("jvm:{}",
ManagementFactory.getRuntimeMXBean().getInputArguments().stream().collect(Collectors.joining(" ")));
SpringApplication.run(Springcloudk8sdemoApplication.class, args);
}
@Autowired
private TestConfig testConfig;
@Scheduled(fixedDelay = 5000)
public void hello() {
log.info("config:{}", testConfig);
}
@LoadBalanced
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
在這個啟動程式中我們做了幾件事情:
- 定義了一個定時器,5秒一次輸出配置(隨後用於觀察ConfigMap配置動態重新整理)
- 定義了RestTemplate和Ribbon配合使用
- 在啟動的時候輸出下JVM引數,以便證明JVM引數(通過環境變數)注入成功
配置檔案方面,首先是application.yaml:
spring:
application:
name: k8sdemo
cloud:
kubernetes:
reload:
enabled: true
config:
sources:
- name: ${spring.application.name}
幹了三件事情:
- 定義應用程式名稱
- 指定ConfigMap名稱為應用程式名,也就是k8sdemo
- 啟用ConfigMap配置自動重新整理(見下圖,預設是event方式)
再定義一個bootstrap.yaml用於開啟actuator的一些端點:
management:
endpoint:
restart:
enabled: true
health:
enabled: true
info:
enabled: true
整個程式碼原始碼參見 https://github.com/JosephZhu1983/SpringCloudK8S
配置阿里雲K8S叢集
叢集購買過程我就略去了,這些選項都可以勾上,Ingress特別記得需要,我們之後要在公網上進行測試。
差不多30秒就有了一個K8S叢集,這鬼東西要自己從頭搭建一套高可用的沒一天搞不下來,這裡可以看到我買了一個3節點的託管版K8S,所謂託管版也就是K8S的管理節點我們直接用阿里雲自己的,只需要買工作節點,省錢省心。
買好後記得配置下kubeconfig,這樣才能通過kubectl訪問叢集。
注意下,阿里雲給出的配置別一股腦直接複製覆蓋了原來的配置(比如你可能還有本地叢集),也別直接貼上到檔案的最後,檔案是有格式的,你需要把cluster、context和user三個配置分別複製到對應的地方。
構建映象
我們知道在K8S部署程式不像虛擬機器,唯一的交付是映象,因此我們需要把映象上傳到阿里雲。
首先,本地構建映象:
mvn package docker:build -DskipTests
完成後檢視映象:
然後在阿里雲開通映象服務,建立自己的倉庫:
根據裡面的說明,給映象打上標籤後推送映象到倉庫:
docker login --username=【你的賬號】 registry.cn-shanghai.aliyuncs.com
docker tag 80026bb476ce registry.cn-shanghai.aliyuncs.com/zhuyedocker/test:v6
docker push registry.cn-shanghai.aliyuncs.com/zhuyedocker/test:v6
完成後在映象倉庫檢視映象:
部署應用
通過映象建立無狀態應用:
建立的時候注意下面幾點:
- 選擇正確的映象和Tag
- 我這裡給予一個應用1C CPU 1.4G記憶體的配置
- 埠和應用一致,設定為8080
- 通過環境變數注入額外的JVM引數:-server -XX:+UseContainerSupport -XX:MaxRAMPercentage=50.0 -XX:InitialRAMPercentage=50.0 -XX:MinRAMPercentage=50.0 -XX:MaxMetaspaceSize=256M -XX:ThreadStackSize=256 -XX:+DisableExplicitGC -XX:+AlwaysPreTouch
這裡我配置了JVM動態根據容器的資源限制來設定堆記憶體大小(此特性在部分版本的JDK8上支援,在9以後都支援),這比直接設定死Xms和Xmx好很多(設定死的話不方便進行擴容),這裡我設定了50%,不建議設定更高(比如如果是2GB的記憶體限制,給堆設定為1.5GB顯然是不合適的),畢竟Java程序所使用的記憶體除了堆之外還有堆外、執行緒棧(執行緒數*ThreadStackSize)、元資料區等,而且容器本身也有開銷。
我這裡展示的是編輯介面,建立介面略有不同但是類似:
建立應用的時候你可以把Service和Ingress一併建立。
完成後可以進入應用詳情看到2個節點狀態都是執行中:
測試應用啟動情況
來到Ingress介面可以看到我們的公網Ingress記錄,可以直接點選訪問:
根節點輸出的是IP,在之前的截圖中我們可以看到服務執行在1.13和0.137兩個IP上:
多重新整理幾次瀏覽器可以看到負載均衡的效果。
訪問services可以檢視到所有K8S的服務:
訪問actuator/info可以看到有關K8S的詳情(感謝SCK),顯然我們程式碼裡獲取到的IIP是PodIP:
測試讀取K8S配置
接下去我們來到配置項來配置ConfigMap:
這裡配置項的名稱需要和配置檔案中的對應起來,也就是k8sdemo。然後配置項的Key需要和程式碼中的對應:
我們來看看應用的日誌:
2019-10-03 11:30:33.442 INFO 1 --- [pool-1-thread-1] m.j.s.Springcloudk8sdemoApplication : config:TestConfig(message=8888, serviceName=k8sdemo-svc)
的確正確獲取到了配置,我們修改下配置項bean.message為9999,隨後再來看看日誌:
可以看到程式發現了配置的變更,重新整理了上下文,然後獲取到了最新的配置。
測試通過K8S服務發現進行服務呼叫:
訪問client介面可以看到1.13正常從0.137獲取到了資料:
多重新整理幾次:
我們訪問到應用的負載均衡是由Ingress實現的,應用訪問服務端的負載均衡是由Ribbon實現的。
檢視JVM記憶體情況
還記得嗎,我們在建立應用的時候給的記憶體是1.4GB,然後我們設定了JVM使用50%的記憶體(初始和最大都是50%),現在我們來看看是不是這樣。
首先來看看pod的情況:
然後執行如下命令在Pod內執行jinfo
kubectl exec k8sdemo-7b44d9fbff-c4jkf -- jinfo 1
可以看到如下結果,初始和最大堆是700M左右,說明引數起作用了:
小結
本文我們簡單展示了一下Spring Cloud Kubernetes的使用,以及如何通過阿里雲的K8S叢集來部署我們的微服務,我們看到:
- 如何通過SCK來讀取ConfigMap的配置,支援動態重新整理
- 如何通過SCK來使用K8S的服務發現進行服務呼叫
- JVM記憶體引數設定問題
- 如何把映象推到阿里雲並且在阿里雲的K8S跑起來我們的映象
有關K8S和基於Spring Boot/Spring Cloud的微服務結合使用,有幾點需要注意:
- Spring Cloud 有自己的服務註冊中心,比如Eureka。如果你希望統一使用K8S做服務發現,那麼可以使用Spring Cloud Kubernetes。如果你希望使用Eureka作為服務發現,那麼服務之間呼叫都建議通過Feign或Ribbon呼叫,而不是使用K8S的Service域名或Ingress呼叫,兩套服務發現體系混用的話比較混亂而且有協同性問題。
- 在K8S而不是VM中部署應用,最主要的區別是不能認為服務的IP是固定的,因為Pod隨時可能重新排程,對於某些框架,需要依賴有狀態的應用IP,比如XXL Job這可能是一個問題,需要改造。
- Pod的生命週期和VM不同,考慮各種日誌和OOM Dump的收集和保留問題。
- 應用無故重啟,考慮健康檢測、資源不足等問題,在K8S部署應用需要觀察應用的重啟問題,合理設定reques和limit配置以及JVM引數(比如-XX:+UseContainerSupport -XX:MaxRAMPercentage=50.0 -XX:InitialRAMPercentage=50.0 -XX:MinRAMPercentage=50.0),審查健康檢測的配置是否合理。