引言:从传统部署到云原生的必然之路
在数字化转型加速的今天,企业应用架构正经历着从传统单体到云原生的深刻变革。根据 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;
@Override
public 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);
}
@Override
public 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());
}
@Override
public 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);
}
@Override
public 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 = "新增用户信息")
@PostMapping
public Result<Long> createUser(
@Parameter(description = "用户信息", required = true)
@RequestBody User user) {
return userService.createUser(user);
}
@Operation(summary = "更新用户", description = "修改用户信息")
@PutMapping
public 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字符串
*/
@Override
public 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-config
namespace: 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-secret
namespace: 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:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0.36
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "root"
- name: MYSQL_DATABASE
value: "user_db"
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
readinessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
initialDelaySeconds: 60
periodSeconds: 30
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-pvc
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
clusterIP: 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-app
labels:
app: springboot-app
spec:
replicas: 3 # 3个副本,实现高可用
selector:
matchLabels:
app: springboot-app
strategy:
rollingUpdate:
maxSurge: 1 # 滚动更新时最大可超出期望副本数的数量
maxUnavailable: 0 # 滚动更新时最大不可用的副本数
type: RollingUpdate # 滚动更新策略
template:
metadata:
labels:
app: springboot-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "8080"
spec:
containers:
- name: springboot-app
image: yourusername/springboot-k8s-demo:1.0.0 # 替换为你的镜像地址
imagePullPolicy: Always
ports:
- containerPort: 8080
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
env:
# 从ConfigMap获取配置
- name: SPRING_APPLICATION_NAME
valueFrom:
configMapKeyRef:
name: springboot-app-config
key: application.name
- name: LOGGING_LEVEL_COM_KEN_DEMO
valueFrom:
configMapKeyRef:
name: springboot-app-config
key: logging.level.com.ken.demo
- name: SERVER_PORT
valueFrom:
configMapKeyRef:
name: springboot-app-config
key: server.port
- name: SPRING_PROFILES_ACTIVE
valueFrom:
configMapKeyRef:
name: springboot-app-config
key: spring.profiles.active
- name: SPRING_DATASOURCE_DRIVER_CLASS_NAME
valueFrom:
configMapKeyRef:
name: springboot-app-config
key: spring.datasource.driver-class-name
- name: MYBATIS_PLUS_CONFIGURATION_MAP_UNDERSCORE_TO_CAMEL_CASE
valueFrom:
configMapKeyRef:
name: springboot-app-config
key: mybatis-plus.configuration.map-underscore-to-camel-case
# 从Secret获取敏感配置
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: springboot-app-secret
key: spring.datasource.url
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: springboot-app-secret
key: spring.datasource.username
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: springboot-app-secret
key: spring.datasource.password
# 健康检查
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 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-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP # 集群内部访问
创建 Ingress 资源(需要集群已安装 Ingress 控制器):
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: springboot-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: springboot-app.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: springboot-app-service
port:
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: livenessState
readiness:
include: readinessState
shutdown:
enabled: true # 启用优雅关闭
endpoints:
web:
exposure:
include: health,info,prometheus,shutdown
health:
livenessState:
enabled: true
readinessState:
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;
}
@Override
public 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();
}
}
/**
* 数据库连接检查器
*/
@Component
public 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-demo
cloud:
kubernetes:
config:
name: springboot-app-config # 对应ConfigMap的名称
namespace: springboot-app
secrets:
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对象
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
4.4 自动伸缩配置
配置 HPA(Horizontal Pod Autoscaler)实现基于 CPU 和内存使用率的自动伸缩:
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: springboot-app-hpa
namespace: springboot-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: springboot-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU使用率超过70%时扩容
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80 # 内存使用率超过80%时扩容
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 扩容稳定窗口
policies:
- type: Percent
value: 50 # 每次扩容50%
periodSeconds: 60 # 至少60秒才能再次扩容
scaleDown:
stabilizationWindowSeconds: 300 # 缩容稳定窗口(5分钟)
policies:
- type: Percent
value: 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: info
com.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.1
zipkin:
base-url: http://zipkin:9411 # Zipkin服务地址
- 部署 Zipkin:
# zipkin-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: zipkin
namespace: springboot-app
spec:
replicas: 1
selector:
matchLabels:
app: zipkin
template:
metadata:
labels:
app: zipkin
spec:
containers:
- name: zipkin
image: openzipkin/zipkin:2.24.3
ports:
- containerPort: 9411
---
apiVersion: v1
kind: Service
metadata:
name: zipkin
namespace: springboot-app
spec:
selector:
app: zipkin
ports:
- port: 9411
targetPort: 9411
type: NodePort
nodePort: 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
- deploy
variables:
PROJECT_NAME: "springboot-k8s-demo"
VERSION: "1.0.0"
DOCKER_REGISTRY: "your-registry-url"
K8S_NAMESPACE: "springboot-app"
# 构建阶段
build:
stage: build
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn clean compile
artifacts:
paths:
- target/classes/
expire_in: 1 hour
# 测试阶段
test:
stage: test
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn test
artifacts:
paths:
- target/surefire-reports/
expire_in: 1 day
# 打包镜像阶段
package:
stage: package
image: docker:26.1.4
services:
- docker:26.1.4-dind
script:
- 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:$VERSION
only:
- main
# 部署阶段
deploy:
stage: deploy
image: bitnami/kubectl:1.28
script:
- 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_NAMESPACE
only:
- 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 集群中部署和运维的完整流程。