本文主要探索容器化部署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-slimLABEL 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的情况。