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