Spring Boot 项目转容器化 K8S 部署实用经验分享

我们知道 Kubernetes 是 Google 开源的容器集群管理系统,它构建在目前流行的 Docker 技术之上,为容器化的应用提供资源调度、部署运行、服务发现、扩容缩容等一整套功能,用于容器集群的自动化部署、扩容以及运维的开源平台。Spring Boot 是 Spring 框架的集成,通过该框架可以大大简化应用的搭建、开发及部署过程,从而深受大家喜爱。之前 Spring Boot 项目大多以传统虚拟机或物理机部署方式,转容器化 K8S 集群部署的话,也是非常简单的,这里给大家分享一下我在工作中,实际操作中的一些使用经验。

1、服务配置文件处理方式

对于各个项目分环境部署,最麻烦的就是配置文件的问题,不同的环境需要加载不同的配置,好在 Spring Boot 框架加载配置是非常方便的,具体如何加载配置文件可以参考 这里,我们可以针对不同的环境分别配置不同的配置文件,这里有两个地方要注意一下:

  • 构建镜像的时候,尽量实现一个镜像支持所有环境(即所有配置都打到一个镜像里面去),在容器启动时指定加载哪个环境配置即可,例如:在部署 Deployment 时指定 args: ["--spring.profiles.active=prod"] 参数启动。
  • 尽量不要每个环境打出来一个镜像版本,传统方式在构建的时候指定 -D prod 配置 Profile 来指定加载哪个配置,来生成不同的产物 jar,容器化部署后不需要这样,那样后期控制各镜像版本发布会比较麻烦。

2、服务镜像相关配置

容器化部署服务,肯定少不了镜像制作这一步,镜像可以分为基础镜像和应用镜像。

2.1 基础镜像

基础镜像要求体积尽量小,方便拉取,同时安装一些必要的软件,方便后期进入容器内排查问题,我们需要准备好服务运行的底层系统镜像,比如 Centos、Ubuntu 等常见 Linux 操作系统,然后基于该系统镜像,构建服务运行需要的环境镜像,比如一些常见组合:Centos + JdkCentos + Jdk + TomcatCentos + nginx 等,由于不同的服务运行依赖的环境版本不一定一致,所以还需要制作不同版本的环境镜像,例如如下基础镜像版本。

  • Centos6.5 + Jdk1.8: registry.docker.com/baseimg/centos-jdk:6.5_1.8
  • Centos7.5 + Jdk1.8: registry.docker.com/baseimg/centos-jdk:7.5_1.8
  • Centos7.5 + Jdk1.7: registry.docker.com/baseimg/centos-jdk:7.5_1.7
  • Centos7 + Tomcat8 + Jdk1.8: registry.docker.com/baseimg/centos-tomcat-jdk:7.5_8.5_1.8
  • Centos7 + Nginx: registry.docker.com/baseimg/centos-tomcat-jdk:7.5_1.10.2

这样,就可以标识该基础镜像的系统版本及软件版本,方便后边选择对应的基础镜像来构建应用镜像。基础镜像的制作方法之一,可以参考 使用 febootstrap 制作自定义基础镜像 方式。

2.2 应用镜像

有了上边的基础镜像后,就很容易构建出对应的应用镜像了,例如一个简单的应用镜像 Dockerfile 如下:

FROM registry.docker.com/baseimg/centos-jdk:7.5_1.8

COPY app-name.jar /opt/project/app.jar
EXPOSE 8080
ENTRYPOINT ["/java", "-jar", "/opt/project/app.jar"]

当然,这里我建议使用另一种方式来启动服务,将启动命令放在统一 shell 启动脚本执行,例如如下Dockerfile 示例:

FROM registry.docker.com/baseimg/centos-jdk:7.5_1.8

COPY app-name.jar /opt/project/app.jar
COPY entrypoint.sh /opt/project/entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/bin/sh", "/opt/project/entrypoint.sh"]

将服务启动命令配置到 entrypoint.sh,这样我们可以扩展做很多事情,比如启动服务前做一些初始化操作等,还可以向容器传递参数到脚本执行一些特殊操作,而且这里变成脚本来启动,这样后续构建镜像基本不需要改 Dockerfile 了。

#!/bin/bash
# do other things here
java -jar $JAVA_OPTS /opt/project/app.jar $1  > /dev/null 2>&1

上边示例中,我们就注入 $JAVA_OPTS 环境变量,来优化 JVM 参数,还可以传递一个变量,这个变量大家应该就猜到了,就是服务启动加载哪个配置文件参数,例如:--spring.profiles.active=prod 那么,在 Deployment 中就可以通过如下方式配置了:

...
spec:
  containers:
    - name: project-name
      image: registry.docker.com/project/app:v1.0.0
      args: ["--spring.profiles.active=prod"]
      env:
	   - name: JAVA_OPTS
	     value: "-XX:PermSize=512M -XX:MaxPermSize=512M -Xms1024M -Xmx1024M..."
...

是不是很方便,这里可扩展做的东西还很多,根据项目需求来配置。

3、服务日志输出处理

对于日志处理,之前我们一般会使用 Log4jLogstash 等日志框架将日志输出到服务器指定目录,容器化部署后,日志会生成到容器内某个配置的目录上,外部是没法访问的,所以需要将容器内日志挂载到宿主机某个目录 (例如:/opt/logs 目录),这样方便直接查看,或者配置 FilebeatFluent 等工具抓取到 Elasticsearch 来提供日志查询分析。在 Deployment 配置日志挂载方式也很简单,配置如下:

...
    volumeMounts:
    - name: app-log
      mountPath: /data/logs/serviceA  #log4j 配置日志输出到指定目录
...
	volumes:
    - name: app-log
      hostPath:
        path: /opt/logs #宿主机指定目录

这里有个地方需要特别注意一下:服务日志要关闭 Console 输出,避免直接输出到控制台。默认 Docker 会记录控制台日志到宿主机指定目录,日志默认输出到 /var/lib/docker/containers/<container_id>/<container_id>-json.log,为了避免出现日志太多,占用磁盘空间,需要关闭 Console 输出并定期清理日志文件。

4、容器服务访问处理

4.1、配置容器服务暴露目标端口

首先需要提供容器服务需要暴露的目标端口号,例如 HttpHttpsGrpc 等服务端口,创建 Service 时需要指定匹配的容器端口号,Deployment 中配置容器暴露端口配置如下:

	ports:
    - containerPort: 8080
      name: http
      protocol: TCP
    - containerPort: 443
      name: https
      protocol: TCP
    - containerPort: 18989
      name: dubbo
      protocol: TCP

4.2、服务对内对外访问方式选择

K8S Service 暴露服务类型有三种:ClusterIPNodePortLoadBalancer,三种类型分别有不同的应用场景。

  • 对内服务发现,可以使用 ClusterIP 方式对内暴露服务,因为存在 Service 重新创建 IP 会更改的情况,所以不建议直接使用分配的 ClusterIP 方式来内部访问,可以使用 K8S DNS 方式解析,DNS 命名规则为:<svc_name>.<namespace_name>.svc.cluster.local,按照该方式可以直接在集群内部访问对应服务。
  • 对外服务暴露,可以采用 NodePortLoadBalancer 方式对外暴露服务,NodePort 方式使用集群固定 IP,但是端口号是指定范围内随机选择的,每次更新 Service 该 Port 就会更改,不太方便,当然也可以指定固定的 NodePort,但是需要自己维护 Port 列表,也不方便。LoadBalancer 方式使用集群固定 IPNodePort,会额外申请申请一个负载均衡器来转发到对应服务,但是需要底层平台支撑。如果使用 AliyunGCE 等云平台商,可以使用该种方式,他们底层会提供 LoadBalancer 支持,直接使用非常方便。

以上方式或多或少都会存在一定的局限性,所以建议如果在公有云上运行,可以使用 LoadBalancerIngress 方式对外提供服务,私有云的话,可以使用 Ingress 通过域名解析来对外提供服务。Ingress 配置使用,可以参考 初试 Kubernetes 暴漏服务类型之 Nginx Ingress初试 Kubernetes 集群中使用 Traefik 反向代理 文章。

5、服务健康监测配置

K8s 提供存活探针和就绪探针,来实时检测服务的健康状态,如果健康检测失败,则会自动重启该 Pod 服务,检测方式支持 exechttpGettcpSocket 三种。对于 Spring Boot 后端 API 项目,建议采用 httpGet 检测接口的方式,服务提供特定的健康检测接口,如果服务正常则返回 200 状态码,一旦检测到非 200 则会触发自动重启机制。K8S 健康监测配置示例如下:

 livenessProbe: # 是否存活检测
    failureThreshold: 3
    httpGet:
      path: /api/healthz
      port: 8080
      scheme: HTTP
    initialDelaySeconds: 300
    periodSeconds: 60
    successThreshold: 1
    timeoutSeconds: 2
  readinessProbe: # 是否就绪检测
    failureThreshold: 1
    httpGet:
      path: /api/healthz
      port: 8080
      scheme: HTTP
    periodSeconds: 5
    successThreshold: 1
    timeoutSeconds: 2 

其中一些参数的作用,这里就不解释了,可以参照 官网文档 来了解。

6、服务 CPU & Mem 请求/最大值配置

K8S 在部署 Deployment 时,可以为每个容器配置最小及最大 CPU & Mem 资源限制,这个是很有必要的,因为不配置资源限制的话,那么默认该容器服务可以无限制使用系统资源,这样如果服务异常阻塞或其他原因,导致占用系统资源过多而影响其他服务的运行,同时 K8S 集群资源不足时,会优先干掉那些没有配置资源限制的服务。当然,请求资源量和最大资源量要根据服务启动实际需要来配置,如果不清楚需要配置多少,可以先将服务部署到 K8S 集群中,看正常调用时监控页面显示的请求值,在合理配置。

resources:
  limits:
    cpu: "1000m"
    memory: "1024Mi"
  requests:
    cpu: "500m"
    memory: "512Mi"

7、K8S 集群部署其它注意事项

7.1、部署前的一些准备工作

K8S 在部署服务前,需要做一些准备工作,例如提前创建好对应的 Namespace,避免首次直接创建 Deployment 出现 Namespace 不存在而创建失败。如果我们使用的私有镜像仓库,那么还需要生成 Docker Repository 登录认证 Secret,用来注入到 Pod 内拉取镜像时认证需要。

# 包含登录认证信息的 Secret
apiVersion: v1
kind: Secret
metadata:
  name: docker-regsecret
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: InN1bmRhbmRhbjE4MDUyOEBjcmVkaXRoYdfe3JhdXRocyI6eyJyZWdpc3RyeS1pb

# Deployment 中注入该 Secret
	imagePullSecrets:
    - name: docker-regsecret

Secret 的生成方式可参考 官网文档

7.2、灵活使用 ConfigMap 资源类型

K8S 提供 ConfigMap 资源类型来方便灵活控制配置信息,我们可以将服务需要的一些 ENV 信息或者配置信息放到 ConfigMap 中,然后注入到 Pod 中即可使用,非常方便。ConfigMap 使用方式有很多种,这里建议大家可以将一些经常更改的配置放到 ConfigMap 中,例如我在实际操作中,就发现有的项目 nginx.conf 配置,还有配置的 ENV 环境变量信息经常变动,那么就可以放在 ConfigMap 中配置,这样 Deployment 就不需要重新部署了。

# 包含 nginx.conf 配置的 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf-configmap
  data:
    www.conf: |
      server {
        listen 80;
        server_name 127.0.0.1

        root /opt/project/nginx/html/;
        error_page 405 =200 $uri;

        access_log  /opt/project/nginx/logs/http_accesss.log  main;
        error_log   /opt/project/nginx/logs/http_error.log;
      }

# 将 ConfigMap 挂载到容器指定目录
volumes:
- name: nginx-config
  configMap:
    defaultMode: 420
    name: nginx-conf-configmap

这里有一个使用 ConfigMap 优雅加载 Spring Boot 配置文件实现方式的示例,可以参考 这里

7.3、Deployment 资源部署副本数及滚动更新策略

K8S 建议使用 Deployment 资源类型启动服务,使用 Deployment 可以很方便的进行滚动更新、扩缩容/比例扩容、回滚、以及查看更新版本历史记录等。所以建议副本数至少 2 个,保证服务的可用性,要根据服务实际访问量,来合理配置副本数,过多造成资源浪费,过少造成服务负荷高响应慢的问题,当然也可以根据服务访问量,灵活扩缩容副本数。

Deployment 更新策略有 RecreateRollingUpdate 两种,Recreate 方式在创建出新的 Pod 之前会先杀掉所有已存在的 Pod,这种方式不友好,会存在服务中断,中断的时间长短取决于新 Pod 的启动就绪时间。RollingUpdate 滚动更新方式,通过配合指定 maxUnavailablemaxSurge 参数来控制更新过程,使用该策略更新时会新启动 replicas 数量的 Pod,新 Pod 启动完毕后,在干掉旧 Pod,如果更新过程中,新 Pod 启动失败,旧 Pod 依旧可以提供服务,直到启动完成,服务才会切到新 Pod,保证服务不会中断,建议使用该策略。

replicas: 2
strategy:
  rollingUpdate:
    maxSurge: 1  #也可以按比例配置,例如:20%
    maxUnavailable: 0 #也可以按比例配置,例如:20%
  type: RollingUpdate

7.4、要保证 K8S 资源 CPU & Mem & Disk 资源够用

要时刻关注 K8S 集群资源使用情况,保证系统资源够集群使用,否则会出现因为 CPU 、Mem、Disk 不够用导致 Deployment 调度失败的情况。

7.5、K8S 集群配置项优化

K8S 集群创建每个 Namespaces 时默认会创建一个名称为 default 的 ServiceAccount,该 ServiceAccount 包含了名称为 default-token-xxxx 的 Secret,该 Secret 包含集群 api-server 使用的根 CA 证书以及认证用的令牌 Token,而且默认新创建 Pod 时会自动将该 ServiceAccount 包含的信息自动注入到 Pod 中,在 Pod 中可以直接使用这些认证信息连接集群执行 api 相关操作,这样会存在一定的风险,所以建议使用 automountServiceAccountToken: false 配置来关闭自动注入。

另一个配置 progressDeadlineSeconds,该配置用来指定在升级或部署时,由于各种原因导致卡住(还没有表明升级或部署失败),等待的 deadline 秒数,如果超过该 deadline 时间,那么将上报并标注 Deployment 状态为 False 并注明失败原因,然后 Deployment 继续执行后续操作。默认为 600 秒,如果觉得改时间太长,可以按照可接受的时间来修改配置,例如配置为 120 秒 progressDeadlineSeconds: 120

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券