朱曄和你聊Spring系列S1E11:小測Spring Cloud Kubernetes @ 阿里雲K8S

有關Spring Cloud Kubernetes(如下簡稱SCK)詳見https://github.com/spring-cloud/spring-cloud-kubernetes,在本文中咱們主要測試三個功能:php

  • 使用Kubernetes服務發現配合Spring Cloud Ribbon作服務調用
  • 讀取Kubernetes的ConfigMap配置而且支持修改後動態刷新
  • Spring Boot Actuator對Kubernates Pod信息的感知

編寫測試程序

首先,咱們來建立pom文件,注意幾點:java

  • Spring Boot版本不能過高
  • 引入了 Spring Boot Web以及Actuator兩個模塊,咱們開發一個Web項目進行測試
  • 引入了 Spring Cloud的Ribbon模塊,咱們須要測試一下服務調用
  • 引入了spring-cloud-starter-kubernetes-all依賴,咱們的主要測試對象
  • 額外引入了docker-maven-plugin插件用於幫助咱們構建鏡像
  • 設置了finalName

文件以下:git

<?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文件:github

FROM openjdk:11-jdk-slim
VOLUME /tmp
ADD k8sdemo.jar app.jar
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar
複製代碼

值得注意的是,JVM參數咱們但願從環境變量注入。web

來看看代碼,咱們首先定義一個配置類:spring

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的配置方式。下面咱們定義一個控制器扮演服務端的角色:docker

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();
    }
}

複製代碼

能夠看到這裏定義了兩個接口:apache

  • servers 用於返回服務發現找到的全部服務(K8S的服務)
  • 根路徑返回了當前節點的IP地址

接下去定義另外一個控制器扮演客戶端的角色:bootstrap

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方式)

image_1dm8j8hhpa1jsi1brb1gb81nh8m.png-374.9kB

再定義一個bootstrap.yaml用於打開actuator的一些端點:

management:
  endpoint:
    restart:
      enabled: true
    health:
      enabled: true
    info:
      enabled: true
複製代碼

整個代碼源碼參見 github.com/JosephZhu19…

配置阿里雲K8S集羣

集羣購買過程我就略去了,這些選項均可以勾上,Ingress特別記得須要,咱們以後要在公網上進行測試。

image_1dm8jqua649k1hp811vb1nu31osr9.png-84.1kB

差很少30秒就有了一個K8S集羣,這鬼東西要本身從頭搭建一套高可用的沒一天搞不下來,這裏能夠看到我買了一個3節點的託管版K8S,所謂託管版也就是K8S的管理節點咱們直接用阿里雲本身的,只須要買工做節點,省錢省心。

image_1dm8jgiu51v7mkkd14ks1fdt18vo1g.png-59.5kB

買好後記得配置下kubeconfig,這樣才能經過kubectl訪問集羣。

image_1dm8jfosa1kfcg2a5cd1oi5p0013.png-291.9kB

注意下,阿里雲給出的配置別一股腦直接複製覆蓋了原來的配置(好比你可能還有本地集羣),也別直接粘貼到文件的最後,文件是有格式的,你須要把cluster、context和user三個配置分別複製到對應的地方。

配置好後,運行以下命令來爲咱們的服務帳戶授予管理員角色,不然應用將不能訪問API Server來讀取相關信息:

kubectl create clusterrolebinding admin-bind --clusterrole=cluster-admin --serviceaccount=default:default
複製代碼

參考 yq.aliyun.com/articles/67…

構建鏡像

咱們知道在K8S部署程序不像虛擬機,惟一的交付是鏡像,所以咱們須要把鏡像上傳到阿里雲。 首先,本地構建鏡像:

mvn package docker:build -DskipTests
複製代碼

完成後查看鏡像:

image_1dm8k69r61uiu1efvc711eeq118u13.png-77.4kB

而後在阿里雲開通鏡像服務,建立本身的倉庫:

image_1dm8k8bgh1q3c1g7g1hogp319701g.png-128.4kB

根據裏面的說明,給鏡像打上標籤後推送鏡像到倉庫:

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
複製代碼

完成後在鏡像倉庫查看鏡像:

image_1dm8kd82h1jld133i12685oo1t1t.png-87.2kB

部署應用

經過鏡像建立無狀態應用:

image_1dm8kfhvkolh1opdj79hoc1p2g2a.png-54.2kB

建立的時候注意下面幾點:

  • 選擇正確的鏡像和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)、元數據區等,並且容器自己也有開銷。

我這裏展現的是編輯界面,建立界面略有不一樣可是相似:

image_1dm8khth11mvcob91jcv1rotark2n.png-103.6kB

建立應用的時候你能夠把Service和Ingress一併建立。

image_1dm8lkjpr16mqihdlvgvc7e5134.png-12kB

完成後能夠進入應用詳情看到2個節點狀態都是運行中:

image_1dm8lmgfpcoj0e10jk1mm31f0q3h.png-95.2kB

測試應用啓動狀況

來到Ingress界面能夠看到咱們的公網Ingress記錄,能夠直接點擊訪問:

image_1dm8ls39f12f2lra1cbq1v751ft74o.png-136.8kB

根節點輸出的是IP,在以前的截圖中咱們能夠看到服務運行在1.13和0.137兩個IP上:

image_1dm8lqnph1tv31uqgpnv11p31ee4b.png-22.1kB

多刷新幾回瀏覽器能夠看到負載均衡的效果。

訪問services能夠查看到全部K8S的服務:

image_1dm8m0cmu36uqof7fs1ptso75v.png-27.8kB

訪問actuator/info能夠看到有關K8S的詳情(感謝SCK),顯然咱們代碼裏獲取到的IIP是PodIP:

image_1dm8lvlj347vg7d1e42b1613om5i.png-65.6kB

測試讀取K8S配置

接下去咱們來到配置項來配置ConfigMap:

image_1dm8m52su1o111dod1edci5i6sg6c.png-181kB
這裏配置項的名稱須要和配置文件中的對應起來,也就是k8sdemo。而後配置項的Key須要和代碼中的對應:
image_1dm8m7jjbm0o166c11v41pg0fug6p.png-35.6kB

咱們來看看應用的日誌:

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,隨後再來看看日誌:

image_1dm8md75p151ei939ud10l31fns7j.png-346.5kB
能夠看到程序發現了配置的變動,刷新了上下文,而後獲取到了最新的配置。

測試經過K8S服務發現進行服務調用:

訪問client接口能夠看到1.13正常從0.137獲取到了數據:

image_1dm8mfnon1nprljq1gel1pdt1o7n80.png-25.5kB
多刷新幾回:
image_1dm8mhs2r1r9l1lfj1q0p4g8bqb8d.png-25.1kB

咱們訪問到應用的負載均衡是由Ingress實現的,應用訪問服務端的負載均衡是由Ribbon實現的。

查看JVM內存狀況

還記得嗎,咱們在建立應用的時候給的內存是1.4GB,而後咱們設置了JVM使用50%的內存(初始和最大都是50%),如今咱們來看看是否是這樣。

首先來看看pod的狀況:

image_1dm8mm02iuqj1pom1due1l441odm8q.png-72.3kB

而後執行以下命令在Pod內運行jinfo

kubectl exec k8sdemo-7b44d9fbff-c4jkf -- jinfo 1
複製代碼

能夠看到以下結果,初始和最大堆是700M左右,說明參數起做用了:

image_1dm8mpup4nb91van1c2bse12nl97.png-290.1kB

小結

本文咱們簡單展現了一下Spring Cloud Kubernetes的使用,以及如何經過阿里雲的K8S集羣來部署咱們的微服務,咱們看到:

  • 如何經過SCK來讀取ConfigMap的配置,支持動態刷新
  • 如何經過SCK來使用K8S的服務發現進行服務調用
  • JVM內存參數設置問題
  • 如何把鏡像推到阿里雲而且在阿里雲的K8S跑起來咱們的鏡像(全程都是圖形界面操做,不須要手動寫yml)

有關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),審查健康檢測的配置是否合理。

相關文章
相關標籤/搜索