探索容器化部署Spring Boot应用

本文主要探索容器化部署Spring Boot应用的一些方案,包括Docker Compose、Kubernates等。Dokcer相对来说更简单直接,K8s则更强大复杂。

Demo应用

先大致说一下这个Demo应用,就是一个基于Spring Boot开发的Hello World,在本文中这个Demo是无关紧要的:

WebController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  
@RestController
public class WebController {
@GetMapping("/hello")
public String hello(HttpServletRequest request, @RequestParam(required = false) Integer sleep) throws InterruptedException {
if (sleep != null && sleep > 0 && sleep < 10000) {
Thread.sleep(sleep);
}

return "Hello World! " + request.getLocalName() + ":" + request.getLocalAddr();
}

@GetMapping("/health-check")
public String healthCheck() {
return "OK";
}
}
  • /hello 接口,可以通过sleep参数来模拟接口响应时间,单位毫秒。另外,返回的字符串中包含了响应的主机名和IP地址,方便测试时区分不同的实例。
  • /health-check 接口,用于健康检查,返回OK没有其他含义,主要是通过Http的状态码(200)来判断服务是否正常。
application.yml
1
2
3
4
5
server:
port: 8088
servlet:
context-path: /web
shutdown: graceful
  • graceful 优雅关闭,即等待正在处理的请求处理完毕之后再关闭。

Docker

Dockerfile

Dockerfile
1
2
3
4
5
6
7
8
9
10
FROM openjdk:17-jdk-slim
LABEL maintainer="[email protected]"

WORKDIR /app
ADD ./demo-0.0.1-SNAPSHOT.jar app.jar
ADD ./startup.sh startup.sh

EXPOSE 8088

ENTRYPOINT sh startup.sh
startup.sh
1
2
3
4
5
6
7
#!/bin/bash

java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED \
--add-opens java.base/java.math=ALL-UNNAMED \
-jar app.jar
  • --add-opens 用于解决JDK17中的模块化问题。

构建镜像: docker build -t demo:v1.2 .

Docker Compose

适合单机部署,所以主要是开发、测试环境,或者小型项目的生产环境。

可以进行scale, 但是意义不大,毕竟是单机部署,资源是固定的。

下面的这个例子,是一个简单的demo,包含一个web服务,一个traefik反向代理服务。

docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: "3"
services:
web:
image: demo:v1.2
ports:
- "8088"
labels:
- "traefik.http.routers.web.rule=PathPrefix(`/web`)"
- "traefik.http.services.web.loadbalancer.healthcheck.path=/web/health-check"
- "traefik.http.services.web.loadbalancer.healthcheck.interval=100ms"
- "traefik.http.services.web.loadbalancer.healthcheck.timeout=75ms"
- "traefik.http.services.web.loadbalancer.healthcheck.scheme=http"

reverse-proxy:
image: traefik:v2.10
command: --api.insecure=true --providers.docker
ports:
- "80:80"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock

启动: docker-compose up --scale web=3 -d

Scale的时候,要避免端口冲突,所以指定端口的时候没有采用8088:8088这种格式,而是8088,这样docker会自动分配端口,实例化之后,可以通过docker ps查看端口映射情况。

1
2
3
4
NAME                 IMAGE        COMMAND                  SERVICE    CREATED          STATUS          PORTS
demo-docker-web-1 demo:v1.2 "/bin/sh -c 'java -X…" web 9 seconds ago Up 4 seconds 0.0.0.0:29067->8088/tcp
demo-docker-web-2 demo:v1.2 "/bin/sh -c 'java -X…" web 9 seconds ago Up 5 seconds 0.0.0.0:29066->8088/tcp
demo-docker-web-3 demo:v1.2 "/bin/sh -c 'java -X…" web 9 seconds ago Up 6 seconds 0.0.0.0:29061->8088/tcp

所以启动一个Traefik来反向代理后端的web服务,Traefik可以获取到自动为web实例分配的那些端口,即29067、29066和29061,并且自动配置负载均衡。
这样就解决了端口冲突的问题。

这个例子,没有解决应用下线、升级等情况下,短暂的服务不可用问题,虽然试图通过Traefik的health check来解决,但是还是会出现bad gateway或者service unavailable的情况。

查了一下Traefik的api,没有发现可以通知Traefik某个实例下线的接口,所以这个问题暂时没有解决。(可能在Traefik的企业版里有这个功能。)

Docker Swarm

笔者对Swarm只是纸上谈兵,看了一些资料,没有实际测试,感觉它要解决的问题跟Kubernetes差不多,所以就把重点放在K8s上。

Kubernetes

K3s

K3s的安装使用请参考官方文档:https://docs.k3s.io/zh/quick-start
这里简单记录一下测试环境的安装:

1
curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -s - server --docker
  • 采用了官方建议的国内加速安装方式
  • --docker 表示使用docker作为容器运行时,如果不指定,则默认使用containerd

配置

demo应用的deployment配置:

demo-deployment.yml
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
31
32
33
34
35
36
37
38
39
40
41
42
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: demo-v1
name: demo-v1
spec:
replicas: 2
selector:
matchLabels:
app: demo-v1
template:
metadata:
labels:
app: demo-v1
spec:
containers:
- image: demo:v1.2
name: demo-v1
ports:
- containerPort: 8088
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "512Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /web/health-check
port: 8088
initialDelaySeconds: 20
timeoutSeconds: 5
failureThreshold: 5
readinessProbe:
httpGet:
scheme: HTTP
path: /web/health-check
port: 8088
initialDelaySeconds: 30
timeoutSeconds: 5
  • livenessProde: 用于检测应用是否存活,如果检测失败,则会重启容器。这个参数的配置要谨慎,当应用的启动时间较长时,要给一个合理的initialDelaySeconds,否则判定应用失活,多次重启之后,就直接标记为不可用了。
  • readinessProbe: 用于检测应用是否就绪,如果检测失败,则会将容器从service的endpoint中移除,即不会将请求转发到该容器。

Service配置:

demo-service.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
labels:
app: demo-v1
name: demo-v1
spec:
ports:
- port: 8088
protocol: TCP
targetPort: 8088
selector:
app: demo-v1
type: ClusterIP
  • kind: Service 用于将请求转发到后端的Pod。这是因为在Kubernetes中,Pod的IP地址是动态分配的,而且Pod的数量也是动态变化的,所以不能直接将请求转发到Pod的IP地址上,而是通过Service来转发。Service之所以能够转发请求到Pod,是因为Service通过selector那一节的配置自动发现后端的Pod,本例中的筛选规则就是app名称为demo-v1
  • type: ClusterIP 表示只能在集群内部访问,如果要从集群外部访问,需要使用NodePort或者LoadBalancer类型的Service。

Ingress配置:

demo-ingress.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.k8s.io/v1 
kind: Ingress
metadata:
name: demo-ingress
spec:
ingressClassName: traefik
rules:
- host: demo-v1.xiaoboey.top
http:
paths:
- path: /web
pathType: Prefix
backend:
service:
name: demo-v1
port:
number: 8088
  • kind: Ingress: 用于将请求转发到Service,大概类似传统架构中nginx的作用。
  • host: 用于指定域名,如果不指定,则表示匹配所有域名。

部署及测试

部署应用:

1
2
3
kubectl apply -f demo-deployment.yml
kubectl apply -f demo-service.yml
kubectl apply -f demo-ingress.yml

查看应用状态:

1
2
3
kubectl get pods
kubectl get svc
kubectl get ingress

使用Postman测试:

  • 可以增加一个Host Header,来指定请求的域名,这样测试起来比较省事,不用改主机的host配置,也不用非得要个域名。

扩缩容:

1
kubectl scale deployment demo-v1 --replicas=3

观察扩缩容:

1
watch kubectl get pods -o wide

总结: 配置了readinessProbe之后,扩缩容的过程中,不会出现bad gateway或者service unavailable的情况。