从零放弃学习 Spring - Spring Cloud 与 Kubernetes

在 Kubernetes 中运行 Spring Boot 应用

Kubernetes (以下简称 K8s) 可以直接运行 Spring Boot 应用,这部分不需要 Spring Cloud 支持。

代码参考 spring-boot-k8s

K8s 环境准备

使用 kind 来创建一个 K8s 集群:

1
2
kind create cluster
kubectl cluster-info --context kind-kind

构建应用镜像

创建一个简单的 Spring Boot 项目,选择 Spring WebSpring Boot Actuator 依赖,然后随便写两个接口方便测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@SpringBootApplication
public class SbkApplication {

@GetMapping("/hello")
String hello() {
return "Hello World";
}

@GetMapping("/now")
String now() {
var sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
return sdf.format(new Date()) + " @" + sdf.getTimeZone().getID();
}

public static void main(String[] args) {
SpringApplication.run(SbkApplication.class, args);
}

}

之所以加一个 /now 是为了后续测试时区的问题,Spring Boot Actuator 用来为应用提供探针。

Spring Boot 构建插件支持直接从项目构建出 Docker 镜像:

1
mvn spring-boot:build-image

构建容器会去下载一个 JRE 环境,非常抽象,所以我选通过 Dockerfile 来构建镜像:

1
2
3
4
5
6
7
8
9
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
ADD . /app
WORKDIR /app
RUN mvn -s settings.xml -f pom.xml -Dmaven.test.skip=true package

FROM eclipse-temurin:17-jre-alpine
COPY --from=builder /app/target/*.jar /app/
WORKDIR /app
CMD java -jar app.jar --spring.config.location=./config/application.yml

这里我用了 alpine 镜像,纯粹是个人爱好。然后就是构建 Docker 镜像和在 Docker 中运行的命令:

1
2
3
4
5
6
7
8
9
10
cd path/to/project

# 构建镜像
docker build -t app .

# 运行容器
docker run -v $(pwd)/src/main/resources/application.yml:/app/config/application.yml --network=host app:latest

# 测试访问
curl localhost:8080/hello

部署

在 K8s 部署应用是常规操作,除了内存开大一点以外没有什么区别,我在 Demo 项目里给了对应文件,说起来这步要上传镜像到 Registry,不能直接使用本地镜像,我没有深究是不是 namespace 的原因。

Spring Boot Actuator 提供了开箱即用的 Health Endpoint:/actuator/health/liveness/actuator/health/readiness,作为应用部署在 K8s 需要的健康检测探针,如果有更复杂需求也可以实现 HealthIndicator

NOTE: 当 Spring 应用运行在 K8s 中时,Spring Boot Actuator 会自动启用这两个探针,你可以通过 management.health.probes.enabled=true 主动启用探针

Java 时区问题

在 K8s 中给一个容器配置时区最简单的方法是用 hostPath/etc/localtime 到容器中 /etc/localtime,绝大部分应用有这个文件就正确显示时区了,但是 Java 应用不行。Java 应用的时区配置还需要 /etc/timezone 或者环境变量 TZ 设为对应时区,例如 Asia/Shanghai

Spring Cloud Kubernetes

Spring Cloud 本身是一个微服务框架,在云原生大趋势下也对 K8s 做了支持。Java 有两个 K8s 客户端实现 Fabric8 Kubernetes Java ClientKubernetes Java Client,它们有各自对应的 Spring Cloud Kubernetes 生态,我这边示例用到的是后者。

新建一个项目,添加依赖 Spring WebSpring Boot ActuatorOpenFeign。不知道为什么 Spring Initializr 上不提供添加 K8s 有关的依赖,手动添加 spring-cloud-starter-kubernetes-client-all

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-all</artifactId>
</dependency>

spring-cloud-starter-kubernetes-client-all 依赖包含 discovery、loadbalancer 和 config,引入后 Spring 框架会自动激活 kubernetes profile。

代码参考 spring-cloud-k8s

Service Account

K8s 通过 Service Account 为 Pod 访问 K8s API 提供身份信息,如果没有指定,默认是 default

因为 Spring Cloud Kubernetes 需要访问 API,所以我们要给 Service Account 配置一些权限,这里直接用了 Spring Cloud Kubernetes 文档中的配置,它为 default Account 增加了几个资源的的 getlistwatch 权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: namespace-reader
rules:
- apiGroups: [""]
resources: ["configmaps", "pods", "services", "endpoints", "secrets"]
verbs: ["get", "list", "watch"]

---

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: namespace-reader-binding
namespace: default
subjects:
- kind: ServiceAccount
name: default
apiGroup: ""
roleRef:
kind: Role
name: namespace-reader
apiGroup: ""

这么配置只是为了方便使用,不是一个正规的配置,如果在生产环境用还是要按需配置的。

服务发现和 LoadBalancer

K8s 本身提供了 Service 管理网络流量,Service 通过 Selector 关联 Endpoint。Spring Cloud 框架也自带了服务发现功能,所以为了结合 K8s,Spring Cloud Kubernetes 服务发现通过调用 K8s API 获得 Service 的 Endpoint,如果 Endpoint 有多个端口,默认选择第一个。

对服务的访问和其他 Spring Cloud 的区别不大,注意注解 @FeignClient 要写成服务名,而不是应用名称,例如我们使用 OpenFeign 去调用上一节提到的 Spring Boot 应用。

接口代码:

1
2
3
4
5
6
@FeignClient("sbk-app-svc")
public interface SbkClient {

@GetMapping("/hello")
String hello();
}

调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
SbkClient sbkClient;

@GetMapping("/hello")
String hello() {
log.info("call by OpenFeign");
return sbkClient.hello();
}

@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}

@GetMapping("/now")
String now() {
log.info("call by RestTemplate");
return restTemplate().getForObject("http://sbk-app-svc:8080/now", String.class);
}

代码还展示了用 RestTemplate 访问其他服务的方式,这种方式下负载均衡只会提供 K8s Service 完成。

默认情况下,负载均衡使用 POD 模式,Spring Cloud Kubernetes Discovery 通过服务名获取的 Pod IP 列表,服务调用直接访问 Pod(类似 Headless Service 的作法),这是兼容 Spring Cloud 机制的,如果想要直接使用 K8s Service 自带的流量管理,可以把模式改为 SERVICE

1
2
3
4
5
spring:
cloud:
kubernetes:
loadbalancer:
mode: SERVICE

服务发现默认工作在当前命名空间,可以通过配置指定命名空间跨命名空间服务发现:

1
2
3
4
5
6
7
spring:
cloud:
kubernetes:
discovery:
all-namespaces: false
namespaces:
- default

另外,因为 Service 的 Endpoint 的注册和取消注册由 Pod 生命周期控制,所以 Spring Cloud Kubernetes 不再需要注册服务,这部分机制会被自动屏蔽。

配置

之前已经演示过了从 spring.config.location 加载配置文件,接入 K8s 后,框架可以从 ConfigMap 获取应用配置(我这里只谈 ConfigMap,Secret 基本相同)。

通过设置 spring.config.import=kubernetes: 让应用从 Spring Cloud Kubernetes Config 获得配置,这部分和用其他配置管理中间件很像。加入配置中设置了 spring.application.name,配置客户端会去获取同名的 ConfigMap,如果没有设置,客户端获取 application ConfigMap,如果你希望指定其他名字,可以通过 spring.cloud.kubernetes.config.name 直接指定 ConfigMap 名称。

创建两个 ConfigMap,一个用于启动配置(这一步不是必须的),一个用会被配置客户端自动读取。

startup-cm.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: ConfigMap
metadata:
name: sck-app-conf-cm
namespace: default
data:
application.yml: |-
spring:
application:
name: 'sck'
config:
import: 'kubernetes:'
cloud:
kubernetes:
config:
name: sck-ext-conf-cm

app-cm.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: v1
kind: ConfigMap
metadata:
name: sck-ext-conf-cm
namespace: default
data:
application.yaml: |-
app:
version: '1.0'
logging:
level:
root: info
management:
endpoints:
web:
exposure:
include: '*'
server:
port: 8080
spring:
cloud:
kubernetes:
discovery:
all-namespaces: false
namespaces:
- default
loadbalancer:
mode: SERVICE
reload:
enabled: true

通过执行命令查看配置生效情况:

1
2
$ kubectl exec -it pods/sck-app-7b9d868847-6g4zn -- /bin/sh
/app # wget -O - localhost:8080/actuator/env && cat env

虽然 K8s 会 Pod 挂载的 ConfigMap 文件同步改动,但是 Spring 应用并不监控本地文件,而是使用了 API 获取 configmap。如果应用需要及时配置变更就需要配置 spring.cloud.kubernetes.reload.enabled=true,这样配置客户端就回去 watch configmap,别忘了代码中加上注解 @RefreshScope。此外,配置客户端也支持通过配置 spring.cloud.kubernetes.reload.period 周期性从配置检查变更,文档提到这个功能在文件挂载时是无需权限的。

discovery-server 和 config-server

除了框架本身的组件外,Spring Cloud Kubernetes 生态还提供配置服务 spring-cloud-kubernetes-configserver 和发现服务 spring-cloud-kubernetes-discoveryserver。

spring-cloud-kubernetes-configserver 在 Config Server 基础上增加了对 K8s ConfigMap 和 Secret 支持。有一点比较令人迷惑,Kubernetes Config Server 不支持配置的 watch,有这方面需求的话就要结合 Kubernetes Configuration Watcher 一起使用。

spring-cloud-kubernetes-discoveryserver 封装了对 K8s Service 访问,你可以直接访问 HTTP API 获取服务 Endpoint,也能使用对应的客户端 spring-cloud-starter-kubernetes-discoveryclient 把服务集成到 Spring Cloud Discovery 能力。

这两个服务虽然可选,但是我还是比较推荐使用,因为抛开功能还有一点很重要,就是它可以减少对 K8s API Server 的压力。

最后

Spring Cloud 到底是一个微服务框架,它和 K8s 结合的方式是通过 K8s 机制的 API 和调整了自身部分的机制,而一个普通的云原生应用应该是在低状态的前提下,也避免和 API 有所交流,所以这是一件很违和的事情。

参考