云原生改造实战:Spring Boot 应用的 Kubernetes 迁移全指南
引言:从传统部署到云原生的必然之路
在数字化转型加速的今天,企业应用架构正经历着从传统单体到云原生的深刻变革。根据 CNCF(Cloud Native Computing Foundation)2024 年调查报告,全球已有超过 96% 的组织正在使用或评估 Kubernetes,云原生技术已成为企业级应用部署的标准选择。
Spring Boot 作为 Java 生态中最流行的应用开发框架,其应用的云原生改造成为许多企业的迫切需求。将 Spring Boot 应用迁移到 Kubernetes(简称 K8s)不仅能实现弹性伸缩、自愈能力等云原生特性,还能显著提升资源利用率、简化运维流程、加速迭代速度。
本文将带你走完 Spring Boot 应用云原生改造的完整旅程,从架构分析到最终部署,涵盖容器化、配置管理、服务发现、监控告警等核心环节,通过可落地的实战案例,让你真正掌握 Java 应用的 K8s 迁移技术。
一、云原生基础与改造前准备
1.1 云原生核心概念解析
云原生应用架构具有以下核心特征:
- 容器化:应用及其依赖被打包在容器中,保证环境一致性
- 弹性伸缩:根据负载自动调整实例数量
- 自愈能力:自动检测并替换故障实例
- 基础设施即代码:环境配置通过代码管理
- 微服务架构:应用拆分为小型、自治的服务
- 持续交付:自动化构建、测试和部署流程
1.2 改造前的评估与准备
在开始迁移前,需要对现有 Spring Boot 应用进行全面评估:
-
应用架构评估
- 是否存在状态(如本地缓存、文件存储)
- 外部依赖情况(数据库、消息队列等)
- 会话管理方式(是否使用本地会话)
-
技术栈兼容性
- Spring Boot 版本(建议 2.7.x 以上或 3.x)
- JDK 版本(推荐 JDK 17,支持容器感知)
- 第三方库兼容性
-
团队技能准备
- Kubernetes 基础概念与操作
- 容器化技术(Docker)
- 云原生监控方案
-
环境准备
- Kubernetes 集群(最小化推荐 3 节点)
- 容器仓库(Docker Hub 或私有仓库)
- CI/CD 流水线工具(Jenkins/GitLab CI)
1.3 环境搭建
1.3.1 安装 Docker
# 安装Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh# 启动Docker服务
sudo systemctl start docker
sudo systemctl enable docker# 将当前用户添加到docker组
sudo usermod -aG docker $USER
1.3.2 搭建 Kubernetes 集群
使用 k3d 快速搭建本地 K8s 集群:
# 安装k3d
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash# 创建包含3个节点的集群
k3d cluster create springboot-cluster \--agents 2 \--port "8080:80@loadbalancer" \--port "8443:443@loadbalancer"# 验证集群状态
kubectl cluster-info
kubectl get nodes
1.3.3 安装必要工具
# 安装kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/# 安装Helm
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
二、Spring Boot 应用容器化改造
2.1 容器化改造原则
将 Spring Boot 应用容器化时,应遵循以下原则:
- 单一职责:一个容器只运行一个应用进程
- 无状态设计:应用不应依赖本地存储的状态
- 非 root 用户运行:增强容器安全性
- 优雅启动与关闭:实现健康检查和优雅退出
- 日志处理:日志输出到标准输出流
- 适当的基础镜像:平衡安全性和镜像大小
2.2 准备示例应用
我们将以一个简单的用户管理系统为例,展示完整的改造过程。
2.2.1 项目结构
plaintext
springboot-k8s-demo/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── ken/
│ │ │ └── demo/
│ │ │ ├── controller/
│ │ │ ├── entity/
│ │ │ ├── mapper/
│ │ │ ├── service/
│ │ │ └── SpringbootK8sDemoApplication.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── mapper/
│ └── test/
├── pom.xml
└── Dockerfile
2.2.2 Maven 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.6</version><relativePath/></parent><groupId>com.ken.demo</groupId><artifactId>springboot-k8s-demo</artifactId><version>1.0.0</version><name>Spring Boot K8s Demo</name><description>Spring Boot应用云原生改造示例</description><properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><mybatis-plus.version>3.5.6</mybatis-plus.version><lombok.version>1.18.30</lombok.version><fastjson2.version>2.0.47</fastjson2.version><knife4j.version>4.4.0</knife4j.version><mysql.version>8.0.36</mysql.version></properties><dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Boot Actuator --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- MyBatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!-- MySQL 驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>${mysql.version}</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><scope>provided</scope></dependency><!-- Fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>${fastjson2.version}</version></dependency><!-- Knife4j (Swagger3) --><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId><version>${knife4j.version}</version></dependency><!-- Spring Boot Test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><finalName>${project.artifactId}</finalName><plugins><!-- Spring Boot Maven插件 --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes><!-- 构建分层镜像支持 --><layers><enabled>true</enabled></layers></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin><!-- 编译插件 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version><configuration><source>${java.version}</source><target>${java.version}</target><encoding>${project.build.sourceEncoding}</encoding></configuration></plugin></plugins></build>
</project>
2.2.3 核心代码实现
实体类:
package com.ken.demo.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;import java.time.LocalDateTime;/*** 用户实体类*/
@Data
@TableName("user")
@Schema(description = "用户实体")
public class User {@TableId(type = IdType.AUTO)@Schema(description = "用户ID", example = "1")private Long id;@Schema(description = "用户名", example = "zhangsan")private String username;@Schema(description = "密码", example = "123456")private String password;@Schema(description = "姓名", example = "张三")private String name;@Schema(description = "邮箱", example = "zhangsan@example.com")private String email;@Schema(description = "手机号", example = "13800138000")private String phone;@Schema(description = "状态:0-禁用,1-正常", example = "1")private Integer status;@Schema(description = "创建时间")private LocalDateTime createTime;@Schema(description = "更新时间")private LocalDateTime updateTime;
}
Mapper 接口:
package com.ken.demo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;/*** 用户Mapper接口*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
Service 接口:
package com.ken.demo.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.ken.demo.entity.User;
import com.ken.demo.result.Result;/*** 用户服务接口*/
public interface UserService extends IService<User> {/*** 根据ID查询用户** @param id 用户ID* @return 用户信息*/Result<User> getUserById(Long id);/*** 创建用户** @param user 用户信息* @return 创建结果*/Result<Long> createUser(User user);/*** 更新用户** @param user 用户信息* @return 更新结果*/Result<Boolean> updateUser(User user);/*** 删除用户** @param id 用户ID* @return 删除结果*/Result<Boolean> deleteUser(Long id);
}
Service 实现类:
package com.ken.demo.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ken.demo.entity.User;
import com.ken.demo.mapper.UserMapper;
import com.ken.demo.result.Result;
import com.ken.demo.result.ResultCode;
import com.ken.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;/*** 用户服务实现类*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {private final UserMapper userMapper;@Overridepublic Result<User> getUserById(Long id) {log.info("查询用户信息,用户ID:{}", id);if (id == null || id <= 0) {log.warn("查询用户信息失败,用户ID不合法:{}", id);return Result.fail(ResultCode.PARAM_ERROR, "用户ID不合法");}User user = userMapper.selectById(id);if (user == null) {log.warn("查询用户信息失败,用户不存在,用户ID:{}", id);return Result.fail(ResultCode.RESOURCE_NOT_FOUND, "用户不存在");}log.info("查询用户信息成功,用户ID:{}", id);return Result.success(user);}@Overridepublic Result<Long> createUser(User user) {log.info("创建用户,用户信息:{}", user);if (user == null) {log.warn("创建用户失败,用户信息为空");return Result.fail(ResultCode.PARAM_ERROR, "用户信息不能为空");}if (!StringUtils.hasText(user.getUsername())) {log.warn("创建用户失败,用户名为空");return Result.fail(ResultCode.PARAM_ERROR, "用户名不能为空");}// 检查用户名是否已存在LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUsername, user.getUsername());long count = userMapper.selectCount(queryWrapper);if (count > 0) {log.warn("创建用户失败,用户名已存在:{}", user.getUsername());return Result.fail(ResultCode.PARAM_ERROR, "用户名已存在");}int rows = userMapper.insert(user);if (rows <= 0) {log.error("创建用户失败,数据库操作失败");return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, "创建用户失败");}log.info("创建用户成功,用户ID:{}", user.getId());return Result.success(user.getId());}@Overridepublic Result<Boolean> updateUser(User user) {log.info("更新用户,用户信息:{}", user);if (user == null || user.getId() == null) {log.warn("更新用户失败,用户ID为空");return Result.fail(ResultCode.PARAM_ERROR, "用户ID不能为空");}// 检查用户是否存在User existingUser = userMapper.selectById(user.getId());if (existingUser == null) {log.warn("更新用户失败,用户不存在,用户ID:{}", user.getId());return Result.fail(ResultCode.RESOURCE_NOT_FOUND, "用户不存在");}int rows = userMapper.updateById(user);if (rows <= 0) {log.error("更新用户失败,数据库操作失败,用户ID:{}", user.getId());return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, "更新用户失败");}log.info("更新用户成功,用户ID:{}", user.getId());return Result.success(true);}@Overridepublic Result<Boolean> deleteUser(Long id) {log.info("删除用户,用户ID:{}", id);if (id == null || id <= 0) {log.warn("删除用户失败,用户ID不合法:{}", id);return Result.fail(ResultCode.PARAM_ERROR, "用户ID不合法");}// 检查用户是否存在User user = userMapper.selectById(id);if (user == null) {log.warn("删除用户失败,用户不存在,用户ID:{}", id);return Result.fail(ResultCode.RESOURCE_NOT_FOUND, "用户不存在");}int rows = userMapper.deleteById(id);if (rows <= 0) {log.error("删除用户失败,数据库操作失败,用户ID:{}", id);return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, "删除用户失败");}log.info("删除用户成功,用户ID:{}", id);return Result.success(true);}
}
Controller:
package com.ken.demo.controller;import com.ken.demo.entity.User;
import com.ken.demo.result.Result;
import com.ken.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;/*** 用户控制器*/
@Slf4j
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户CRUD接口")
public class UserController {private final UserService userService;@Operation(summary = "根据ID查询用户", description = "通过用户ID获取用户详细信息")@GetMapping("/{id}")public Result<User> getUserById(@Parameter(description = "用户ID", required = true, example = "1")@PathVariable Long id) {return userService.getUserById(id);}@Operation(summary = "创建用户", description = "新增用户信息")@PostMappingpublic Result<Long> createUser(@Parameter(description = "用户信息", required = true)@RequestBody User user) {return userService.createUser(user);}@Operation(summary = "更新用户", description = "修改用户信息")@PutMappingpublic Result<Boolean> updateUser(@Parameter(description = "用户信息", required = true)@RequestBody User user) {return userService.updateUser(user);}@Operation(summary = "删除用户", description = "根据ID删除用户")@DeleteMapping("/{id}")public Result<Boolean> deleteUser(@Parameter(description = "用户ID", required = true, example = "1")@PathVariable Long id) {return userService.deleteUser(id);}
}
统一结果类:
package com.ken.demo.result;import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.annotation.JSONField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;/*** 统一响应结果** @param <T> 响应数据类型*/
@Data
@Schema(description = "统一响应结果")
public class Result<T> {@Schema(description = "状态码", example = "200")private int code;@Schema(description = "消息", example = "操作成功")private String msg;@Schema(description = "响应数据")private T data;@JSONField(serialize = false)private boolean success;/*** 私有构造方法,防止直接实例化*/private Result() {}/*** 成功响应** @param <T> 数据类型* @return 成功响应结果*/public static <T> Result<T> success() {return success(null);}/*** 成功响应** @param data 响应数据* @param <T> 数据类型* @return 成功响应结果*/public static <T> Result<T> success(T data) {Result<T> result = new Result<>();result.setCode(ResultCode.SUCCESS.getCode());result.setMsg(ResultCode.SUCCESS.getMsg());result.setData(data);result.setSuccess(true);return result;}/*** 失败响应** @param code 错误码* @param msg 错误消息* @param <T> 数据类型* @return 失败响应结果*/public static <T> Result<T> fail(int code, String msg) {Result<T> result = new Result<>();result.setCode(code);result.setMsg(msg);result.setData(null);result.setSuccess(false);return result;}/*** 失败响应** @param resultCode 错误码枚举* @param <T> 数据类型* @return 失败响应结果*/public static <T> Result<T> fail(ResultCode resultCode) {return fail(resultCode.getCode(), resultCode.getMsg());}/*** 转为JSON字符串** @return JSON字符串*/@Overridepublic String toString() {return JSONObject.toJSONString(this);}
}
结果状态码:
package com.ken.demo.result;import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;/*** 响应状态码枚举*/
@Getter
@AllArgsConstructor
@Schema(description = "响应状态码枚举")
public enum ResultCode {/*** 成功*/SUCCESS(200, "操作成功"),/*** 服务器内部错误*/INTERNAL_SERVER_ERROR(500, "服务器内部错误"),/*** 请求参数错误*/PARAM_ERROR(400, "请求参数错误"),/*** 未授权*/UNAUTHORIZED(401, "未授权"),/*** 资源不存在*/RESOURCE_NOT_FOUND(404, "资源不存在");/*** 状态码*/private final int code;/*** 状态描述*/private final String msg;
}
启动类:
package com.ken.demo;import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;/*** Spring Boot应用启动类*/
@SpringBootApplication
@MapperScan("com.ken.demo.mapper")
@OpenAPIDefinition(info = @Info(title = "用户管理API",version = "1.0.0",description = "用户管理系统的API文档")
)
public class SpringbootK8sDemoApplication {public static void main(String[] args) {SpringApplication.run(SpringbootK8sDemoApplication.class, args);}
}
2.3 编写 Dockerfile 实现容器化
为 Spring Boot 应用编写优化的 Dockerfile:
# 构建阶段
FROM maven:3.9.6-eclipse-temurin-17 AS builder# 设置工作目录
WORKDIR /app# 复制pom.xml和源代码
COPY pom.xml .
COPY src ./src# 构建应用
RUN mvn clean package -DskipTests# 运行阶段,使用轻量级JRE镜像
FROM eclipse-temurin:17-jre-alpine# 添加非root用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup# 设置工作目录
WORKDIR /app# 从构建阶段复制jar包
COPY --from=builder /app/target/springboot-k8s-demo.jar app.jar# 赋予执行权限
RUN chmod 644 app.jar && chown -R appuser:appgroup /app# 切换到非root用户
USER appuser# JVM参数优化,适配容器环境
ENV JAVA_OPTS="\-XX:+UseContainerSupport \-XX:MaxRAMPercentage=75.0 \-XX:+UseG1GC \-XX:+ExitOnOutOfMemoryError \-XX:+HeapDumpOnOutOfMemoryError \-XX:HeapDumpPath=/app/logs/heapdump.hprof"# 暴露端口
EXPOSE 8080# 健康检查脚本
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \CMD wget -q --spider http://localhost:8080/actuator/health || exit 1# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
这个 Dockerfile 具有以下特点:
- 多阶段构建:减小最终镜像大小
- 使用轻量级基础镜像:alpine 版本体积更小
- 非 root 用户运行:提高容器安全性
- JVM 容器感知:-XX:+UseContainerSupport 让 JVM 适应容器环境
- 资源限制优化:-XX:MaxRAMPercentage=75.0 设置 JVM 最大内存为容器内存的 75%
- 健康检查:集成健康检查命令
- OOM 处理:配置 OOM 时退出并生成堆转储
2.4 构建并测试容器镜像
# 构建镜像
docker build -t springboot-k8s-demo:1.0.0 .# 查看构建的镜像
docker images | grep springboot-k8s-demo# 本地运行容器测试
docker run -d \--name springboot-app \-p 8080:8080 \-e SPRING_PROFILES_ACTIVE=dev \-e SPRING_DATASOURCE_URL=jdbc:mysql://host.docker.internal:3306/user_db \-e SPRING_DATASOURCE_USERNAME=root \-e SPRING_DATASOURCE_PASSWORD=root \springboot-k8s-demo:1.0.0# 查看容器日志
docker logs -f springboot-app# 测试应用是否正常运行
curl http://localhost:8080/actuator/health# 停止并删除测试容器
docker stop springboot-app
docker rm springboot-app
2.5 推送镜像到仓库
# 登录Docker Hub
docker login# 为镜像打标签
docker tag springboot-k8s-demo:1.0.0 yourusername/springboot-k8s-demo:1.0.0# 推送镜像
docker push yourusername/springboot-k8s-demo:1.0.0# 如果使用私有仓库
# docker tag springboot-k8s-demo:1.0.0 your-registry-url/springboot-k8s-demo:1.0.0
# docker push your-registry-url/springboot-k8s-demo:1.0.0
三、Kubernetes 资源配置与部署
3.1 应用配置外部化
在 K8s 环境中,应用配置不应硬编码在镜像中,而应通过 ConfigMap 和 Secret 管理。
3.1.1 创建 ConfigMap
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:name: springboot-app-confignamespace: default
data:# 应用名称application.name: "springboot-k8s-demo"# 日志级别logging.level.com.ken.demo: "info"# 服务器端口server.port: "8080"# 应用环境spring.profiles.active: "prod"# 数据库驱动spring.datasource.driver-class-name: "com.mysql.cj.jdbc.Driver"# MyBatis配置mybatis-plus.configuration.map-underscore-to-camel-case: "true"# actuator配置management.endpoints.web.exposure.include: "health,info,prometheus,metrics"management.endpoint.health.show-details: "always"
3.1.2 创建 Secret
# secret.yaml
apiVersion: v1
kind: Secret
metadata:name: springboot-app-secretnamespace: default
type: Opaque
data:# 数据库连接信息(需要Base64编码)spring.datasource.url: amRiYzpteXNxbDovL215c3FsOi84MDM2L3VzZXJfZGJ8dXNlZV9kYg==spring.datasource.username: cm9vdA==spring.datasource.password: cm9vdA==
注意:Secret 中的值需要进行 Base64 编码,可以使用以下命令生成:
echo -n "jdbc:mysql://mysql:3306/user_db" | base64
3.2 部署 MySQL 数据库
在 K8s 中部署 MySQL 作为应用的数据库:
# mysql-deployment.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: mysql-pvc
spec:accessModes:- ReadWriteOnceresources:requests:storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:name: mysql
spec:replicas: 1selector:matchLabels:app: mysqltemplate:metadata:labels:app: mysqlspec:containers:- name: mysqlimage: mysql:8.0.36ports:- containerPort: 3306env:- name: MYSQL_ROOT_PASSWORDvalue: "root"- name: MYSQL_DATABASEvalue: "user_db"volumeMounts:- name: mysql-datamountPath: /var/lib/mysqlreadinessProbe:exec:command: ["mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]initialDelaySeconds: 30periodSeconds: 10livenessProbe:exec:command: ["mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]initialDelaySeconds: 60periodSeconds: 30volumes:- name: mysql-datapersistentVolumeClaim:claimName: mysql-pvc
---
apiVersion: v1
kind: Service
metadata:name: mysql
spec:selector:app: mysqlports:- port: 3306targetPort: 3306clusterIP: None # 无头服务,适合StatefulSet,但这里简化使用
创建数据库表结构:
-- 创建用户表
CREATE TABLE IF NOT EXISTS `user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(100) NOT NULL COMMENT '密码',`name` varchar(50) DEFAULT NULL COMMENT '姓名',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`phone` varchar(20) DEFAULT NULL COMMENT '手机号',`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
可以通过临时 Pod 连接到 MySQL 执行上述 SQL:
# 创建临时MySQL客户端Pod
kubectl run -it --rm --image=mysql:8.0.36 mysql-client -- mysql -h mysql -u root -p# 输入密码root后,执行上述SQL语句
3.3 部署 Spring Boot 应用
创建 Deployment 资源配置:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: springboot-applabels:app: springboot-app
spec:replicas: 3 # 3个副本,实现高可用selector:matchLabels:app: springboot-appstrategy:rollingUpdate:maxSurge: 1 # 滚动更新时最大可超出期望副本数的数量maxUnavailable: 0 # 滚动更新时最大不可用的副本数type: RollingUpdate # 滚动更新策略template:metadata:labels:app: springboot-appannotations:prometheus.io/scrape: "true"prometheus.io/path: "/actuator/prometheus"prometheus.io/port: "8080"spec:containers:- name: springboot-appimage: yourusername/springboot-k8s-demo:1.0.0 # 替换为你的镜像地址imagePullPolicy: Alwaysports:- containerPort: 8080resources:requests:memory: "512Mi"cpu: "500m"limits:memory: "1Gi"cpu: "1000m"env:# 从ConfigMap获取配置- name: SPRING_APPLICATION_NAMEvalueFrom:configMapKeyRef:name: springboot-app-configkey: application.name- name: LOGGING_LEVEL_COM_KEN_DEMOvalueFrom:configMapKeyRef:name: springboot-app-configkey: logging.level.com.ken.demo- name: SERVER_PORTvalueFrom:configMapKeyRef:name: springboot-app-configkey: server.port- name: SPRING_PROFILES_ACTIVEvalueFrom:configMapKeyRef:name: springboot-app-configkey: spring.profiles.active- name: SPRING_DATASOURCE_DRIVER_CLASS_NAMEvalueFrom:configMapKeyRef:name: springboot-app-configkey: spring.datasource.driver-class-name- name: MYBATIS_PLUS_CONFIGURATION_MAP_UNDERSCORE_TO_CAMEL_CASEvalueFrom:configMapKeyRef:name: springboot-app-configkey: mybatis-plus.configuration.map-underscore-to-camel-case# 从Secret获取敏感配置- name: SPRING_DATASOURCE_URLvalueFrom:secretKeyRef:name: springboot-app-secretkey: spring.datasource.url- name: SPRING_DATASOURCE_USERNAMEvalueFrom:secretKeyRef:name: springboot-app-secretkey: spring.datasource.username- name: SPRING_DATASOURCE_PASSWORDvalueFrom:secretKeyRef:name: springboot-app-secretkey: spring.datasource.password# 健康检查livenessProbe:httpGet:path: /actuator/health/livenessport: 8080initialDelaySeconds: 60periodSeconds: 10timeoutSeconds: 3failureThreshold: 3readinessProbe:httpGet:path: /actuator/health/readinessport: 8080initialDelaySeconds: 30periodSeconds: 5timeoutSeconds: 3# 优雅关闭lifecycle:preStop:exec:command: ["sh", "-c", "curl -X POST http://localhost:8080/actuator/shutdown"]
创建 Service 资源:
# service.yaml
apiVersion: v1
kind: Service
metadata:name: springboot-app-service
spec:selector:app: springboot-appports:- port: 80targetPort: 8080type: ClusterIP # 集群内部访问
创建 Ingress 资源(需要集群已安装 Ingress 控制器):
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:name: springboot-app-ingressannotations:nginx.ingress.kubernetes.io/rewrite-target: /nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:rules:- host: springboot-app.localhttp:paths:- path: /pathType: Prefixbackend:service:name: springboot-app-serviceport:number: 80
3.4 执行部署
# 创建命名空间
kubectl create namespace springboot-app# 部署配置
kubectl apply -f configmap.yaml -n springboot-app
kubectl apply -f secret.yaml -n springboot-app# 部署MySQL
kubectl apply -f mysql-deployment.yaml -n springboot-app# 部署应用
kubectl apply -f deployment.yaml -n springboot-app
kubectl apply -f service.yaml -n springboot-app
kubectl apply -f ingress.yaml -n springboot-app# 查看部署状态
kubectl get pods -n springboot-app
kubectl get deployments -n springboot-app
kubectl get services -n springboot-app
kubectl get ingress -n springboot-app# 查看应用日志
kubectl logs -f <pod-name> -n springboot-app
3.5 验证部署结果
# 查看服务访问地址
kubectl get ingress -n springboot-app# 添加hosts解析(替换为实际的IP地址)
echo "127.0.0.1 springboot-app.local" | sudo tee -a /etc/hosts# 测试应用健康状态
curl http://springboot-app.local/actuator/health# 测试API接口
# 创建用户
curl -X POST http://springboot-app.local/api/v1/users \-H "Content-Type: application/json" \-d '{"username":"testuser","password":"123456","name":"测试用户","email":"test@example.com","phone":"13800138000"}'# 查询用户
curl http://springboot-app.local/api/v1/users/1
四、云原生特性增强
4.1 健康检查与优雅关闭
Spring Boot Actuator 提供了健康检查和优雅关闭的支持,需要在 application.yml 中配置:
# application.yml
management:endpoint:health:probes:enabled: true # 启用K8s探针支持group:liveness:include: livenessStatereadiness:include: readinessStateshutdown:enabled: true # 启用优雅关闭endpoints:web:exposure:include: health,info,prometheus,shutdownhealth:livenessState:enabled: truereadinessState:enabled: true
实现自定义健康检查指示器:
package com.ken.demo.health;import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;/*** 自定义健康检查指示器*/
@Component
public class DatabaseHealthIndicator implements HealthIndicator {private final DatabaseConnectionChecker connectionChecker;public DatabaseHealthIndicator(DatabaseConnectionChecker connectionChecker) {this.connectionChecker = connectionChecker;}@Overridepublic Health health() {if (connectionChecker.isDatabaseConnected()) {return Health.up().withDetail("database", "MySQL").withDetail("status", "connected").build();} else {return Health.down().withDetail("database", "MySQL").withDetail("status", "disconnected").withDetail("error", "无法连接到数据库").build();}}/*** 数据库连接检查器*/@Componentpublic static class DatabaseConnectionChecker {private final javax.sql.DataSource dataSource;public DatabaseConnectionChecker(javax.sql.DataSource dataSource) {this.dataSource = dataSource;}/*** 检查数据库连接是否正常** @return 连接正常返回true,否则返回false*/public boolean isDatabaseConnected() {try (var connection = dataSource.getConnection()) {return connection.isValid(5);} catch (Exception e) {return false;}}}
}
4.2 配置动态刷新
使用 Spring Cloud Kubernetes 实现配置的动态刷新:
- 添加依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-kubernetes-config</artifactId><version>3.1.3</version>
</dependency>
- 创建 bootstrap.yml 配置:
spring:application:name: springboot-k8s-democloud:kubernetes:config:name: springboot-app-config # 对应ConfigMap的名称namespace: springboot-appsecrets:name: springboot-app-secret # 对应Secret的名称namespace: springboot-app
- 在需要动态刷新配置的类上添加 @RefreshScope 注解:
package com.ken.demo.config;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;import lombok.Data;/*** 应用配置类,支持动态刷新*/
@Data
@Component
@ConfigurationProperties(prefix = "app")
@RefreshScope
public class AppConfig {/*** 缓存启用开关*/private boolean cacheEnabled = false;/*** 缓存过期时间(秒)*/private int cacheExpireSeconds = 300;/*** 最大并发数*/private int maxConcurrentRequests = 100;
}
- 更新 ConfigMap 后触发配置刷新:
# 编辑ConfigMap
kubectl edit configmap springboot-app-config -n springboot-app# 查看配置是否已更新
kubectl describe configmap springboot-app-config -n springboot-app# 配置会自动刷新,无需重启应用
4.3 服务发现与注册
在 K8s 环境中,可以使用 Spring Cloud Kubernetes 实现服务发现:
- 添加依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-kubernetes-discovery</artifactId><version>3.1.3</version>
</dependency>
- 启用服务发现:
@SpringBootApplication
@EnableDiscoveryClient // 启用服务发现
public class SpringbootK8sDemoApplication {// ...
}
- 使用服务发现调用其他服务:
package com.ken.demo.service;import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;import com.ken.demo.result.Result;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;/*** 服务调用示例*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ServiceInvocationService {private final DiscoveryClient discoveryClient;private final RestTemplate restTemplate;/*** 调用其他服务** @param serviceName 服务名称* @param path 接口路径* @return 服务返回结果*/public Result<Object> invokeService(String serviceName, String path) {log.info("调用服务:{},路径:{}", serviceName, path);// 获取服务实例var instances = discoveryClient.getInstances(serviceName);if (org.springframework.util.CollectionUtils.isEmpty(instances)) {log.error("服务{}未找到", serviceName);return Result.fail(com.ken.demo.result.ResultCode.RESOURCE_NOT_FOUND, "服务未找到");}// 简单负载均衡:选择第一个实例var instance = instances.get(0);String url = String.format("http://%s:%d%s", instance.getHost(), instance.getPort(), path);log.info("服务调用URL:{}", url);try {return restTemplate.getForObject(url, Result.class);} catch (Exception e) {log.error("调用服务{}失败", serviceName, e);return Result.fail(com.ken.demo.result.ResultCode.INTERNAL_SERVER_ERROR, "服务调用失败");}}
}
- 创建 RestTemplate Bean:
package com.ken.demo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;/*** Web配置类*/
@Configuration
public class WebConfig {/*** 创建RestTemplate实例** @return RestTemplate对象*/@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
4.4 自动伸缩配置
配置 HPA(Horizontal Pod Autoscaler)实现基于 CPU 和内存使用率的自动伸缩:
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:name: springboot-app-hpanamespace: springboot-app
spec:scaleTargetRef:apiVersion: apps/v1kind: Deploymentname: springboot-appminReplicas: 2maxReplicas: 10metrics:- type: Resourceresource:name: cputarget:type: UtilizationaverageUtilization: 70 # CPU使用率超过70%时扩容- type: Resourceresource:name: memorytarget:type: UtilizationaverageUtilization: 80 # 内存使用率超过80%时扩容behavior:scaleUp:stabilizationWindowSeconds: 60 # 扩容稳定窗口policies:- type: Percentvalue: 50 # 每次扩容50%periodSeconds: 60 # 至少60秒才能再次扩容scaleDown:stabilizationWindowSeconds: 300 # 缩容稳定窗口(5分钟)policies:- type: Percentvalue: 30 # 每次缩容30%periodSeconds: 120 # 至少120秒才能再次缩容
应用 HPA 配置:
kubectl apply -f hpa.yaml -n springboot-app# 查看HPA状态
kubectl get hpa -n springboot-app
五、监控与日志
5.1 集成 Prometheus 和 Grafana
- 使用 Helm 安装 Prometheus 和 Grafana:
# 添加Helm仓库
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update# 安装Prometheus和Grafana
helm install prometheus prometheus-community/kube-prometheus-stack \--namespace monitoring \--create-namespace \--set grafana.service.type=NodePort \--set grafana.service.nodePort=30000
- 配置 Prometheus 监控 Spring Boot 应用:
Prometheus 会自动发现带有以下注解的 Pod:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "8080"
- 访问 Grafana 并导入 Spring Boot 监控面板:
# 获取Grafana管理员密码
kubectl get secret prometheus-grafana -n monitoring -o jsonpath="{.data.admin-password}" | base64 --decode# 访问Grafana
# 地址:http://<node-ip>:30000
# 用户名:admin
# 密码:上述命令获取的密码
导入 ID 为 12856 的 Spring Boot 监控面板,即可查看 JVM、请求量、响应时间等指标。
5.2 日志收集与分析
- 部署 ELK Stack 或 EFK Stack:
# 使用Helm安装ELK
helm repo add elastic https://helm.elastic.co
helm repo update# 安装Elasticsearch
helm install elasticsearch elastic/elasticsearch \--namespace logging \--create-namespace \--set replicas=1 \--set minimumMasterNodes=1# 安装Kibana
helm install kibana elastic/kibana \--namespace logging \--set service.type=NodePort \--set service.nodePort=30001# 安装Filebeat
helm install filebeat elastic/filebeat \--namespace logging \--set filebeat.inputs[0].type=container \--set filebeat.inputs[0].paths=[/var/log/containers/*.log] \--set output.elasticsearch.hosts[0]=elasticsearch-master:9200
- 配置 Spring Boot 日志输出格式:
# 在application.yml中添加
logging:pattern:console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"level:root: infocom.ken.demo: info
- 在 Kibana 中查看日志:
访问 Kibana(http://<node-ip>:30001),创建索引模式filebeat-*
,即可在 Discover 页面查看和搜索日志。
5.3 分布式追踪
集成 Spring Cloud Sleuth 和 Zipkin 实现分布式追踪:
- 添加依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-sleuth</artifactId><version>3.1.8</version>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-sleuth-zipkin</artifactId><version>3.1.8</version>
</dependency>
- 配置 Zipkin:
# application.yml
spring:sleuth:sampler:probability: 1.0 # 开发环境采样率100%,生产环境可调整为0.1zipkin:base-url: http://zipkin:9411 # Zipkin服务地址
- 部署 Zipkin:
# zipkin-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: zipkinnamespace: springboot-app
spec:replicas: 1selector:matchLabels:app: zipkintemplate:metadata:labels:app: zipkinspec:containers:- name: zipkinimage: openzipkin/zipkin:2.24.3ports:- containerPort: 9411
---
apiVersion: v1
kind: Service
metadata:name: zipkinnamespace: springboot-app
spec:selector:app: zipkinports:- port: 9411targetPort: 9411type: NodePortnodePort: 30002
- 部署 Zipkin:
kubectl apply -f zipkin-deployment.yaml
- 访问 Zipkin 控制台(http://<node-ip>:30002),查看分布式追踪信息。
六、CI/CD 流水线实现
使用 GitLab CI/CD 实现自动化构建、测试和部署:
- 创建
.gitlab-ci.yml
文件:
stages:- build- test- package- deployvariables:PROJECT_NAME: "springboot-k8s-demo"VERSION: "1.0.0"DOCKER_REGISTRY: "your-registry-url"K8S_NAMESPACE: "springboot-app"# 构建阶段
build:stage: buildimage: maven:3.9.6-eclipse-temurin-17script:- mvn clean compileartifacts:paths:- target/classes/expire_in: 1 hour# 测试阶段
test:stage: testimage: maven:3.9.6-eclipse-temurin-17script:- mvn testartifacts:paths:- target/surefire-reports/expire_in: 1 day# 打包镜像阶段
package:stage: packageimage: docker:26.1.4services:- docker:26.1.4-dindscript:- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY- docker build -t $DOCKER_REGISTRY/$PROJECT_NAME:$VERSION .- docker push $DOCKER_REGISTRY/$PROJECT_NAME:$VERSIONonly:- main# 部署阶段
deploy:stage: deployimage: bitnami/kubectl:1.28script:- kubectl config use-context your-k8s-context- kubectl set image deployment/$PROJECT_NAME $PROJECT_NAME=$DOCKER_REGISTRY/$PROJECT_NAME:$VERSION -n $K8S_NAMESPACE- kubectl rollout status deployment/$PROJECT_NAME -n $K8S_NAMESPACEonly:- main
- 流水线执行流程:
七、最佳实践与常见问题
7.1 云原生改造最佳实践
-
容器化最佳实践
- 采用多阶段构建减小镜像大小
- 避免在容器中运行多个进程
- 不要使用 latest 标签,始终使用固定版本
- 配置适当的健康检查和就绪探针
- 以非 root 用户运行容器
-
资源管理最佳实践
- 为所有容器设置资源请求和限制
- 合理设置 JVM 内存参数,避免内存溢出
- 根据应用特性调整 HPA 参数
- 对有状态应用使用 StatefulSet
-
配置管理最佳实践
- 敏感配置使用 Secret 存储
- 非敏感配置使用 ConfigMap 存储
- 利用 Spring Cloud Kubernetes 实现配置动态刷新
- 避免在代码中硬编码配置
-
监控与日志最佳实践
- 实现全面的健康检查
- 规范日志格式,便于集中收集和分析
- 暴露关键业务指标
- 配置适当的告警阈值
7.2 常见问题及解决方案
-
JVM 内存溢出
- 原因:容器环境中 JVM 默认不会感知容器内存限制
- 解决方案:使用 - XX:+UseContainerSupport 和 - XX:MaxRAMPercentage 参数
-
应用启动缓慢
- 原因:资源限制不足或初始化逻辑复杂
- 解决方案:调整资源请求、优化初始化逻辑、延长就绪探针初始延迟
-
配置更新不生效
- 原因:未使用 @RefreshScope 注解或配置映射错误
- 解决方案:在配置类上添加 @RefreshScope、检查配置映射是否正确
-
服务发现失败
- 原因:Service 配置错误或网络策略限制
- 解决方案:检查 Service 标签选择器、检查网络策略
-
自动伸缩不触发
- 原因:HPA 配置错误或指标采集问题
- 解决方案:检查 HPA 配置、确认 metrics-server 正常运行
八、总结与展望
将 Spring Boot 应用迁移到 Kubernetes 是一个系统性工程,涉及容器化改造、配置管理、服务发现、监控告警等多个方面。通过本文的实战指南,我们了解了从应用容器化到最终在 K8s 集群中部署和运维的完整流程。
附录:参考资源
- Spring Boot 官方文档
- Kubernetes 官方文档
- Spring Cloud Kubernetes 文档
- Docker 官方文档
- CNCF 云原生定义
- Spring Boot 应用的 Docker 最佳实践
- Prometheus 监控 Spring Boot 应用
- GitLab CI/CD 文档