尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring Boot + Docker + K8s:构建低成本高可用现代应用技术栈实战

Spring Boot + Docker + K8s:构建低成本高可用现代应用技术栈实战 最近在技术社区看到不少关于“未来一切或将免费”的讨论这让我联想到在软件开发领域我们其实已经部分实现了这种“免费”的愿景——通过开源技术、云原生架构和自动化工具极大地降低了软件构建和分发的边际成本。对于开发者而言掌握如何高效利用这些近乎“免费”的资源是提升个人和团队生产力的关键。本文将从一个后端开发者的视角系统性地拆解如何构建一个低成本、高可用的现代应用技术栈涵盖从架构选型、环境搭建到部署上线的完整闭环。无论你是想快速验证一个想法还是为成熟业务寻找降本增效的方案都能从中获得可直接复用的实践代码和配置。1. 背景与核心概念理解现代应用的“免费”基石在深入实操之前我们有必要厘清几个核心概念。所谓“未来一切或将免费”在技术语境下并非指零成本而是指边际成本趋近于零。这主要得益于以下几大技术浪潮的推动开源软件运动Linux, Kubernetes, PostgreSQL, Redis, Nginx 等基础设施软件完全免费构成了现代互联网的基石。云计算与容器化Docker 和 Kubernetes 实现了应用环境的标准化和隔离使得一次构建、随处运行成为可能大幅降低了环境管理和部署的成本。CI/CD 与自动化GitHub Actions, GitLab CI 等工具提供了免费的自动化流水线将开发人员从重复的构建、测试、部署工作中解放出来。云服务的免费额度各大云厂商如 AWS, GCP, Azure以及国内的云平台都为新用户提供了丰富的免费额度足以支撑个人项目或小流量应用的早期运行。对于开发者来说我们的目标就是组合这些免费或低成本的技术组件搭建一个健壮、可扩展且维护成本低的应用系统。本文将演示一个典型的 Web 应用后端技术栈它可能包含以下组件后端框架Spring Boot (Java) 或 FastAPI (Python)提供 RESTful API。数据库PostgreSQL功能强大的开源关系型数据库。缓存Redis高性能键值存储。消息队列RabbitMQ用于应用解耦和异步处理。容器化Docker实现环境一致性。编排与部署Kubernetes (Minikube 或 Kind 用于本地开发)管理容器化应用。CI/CDGitHub Actions实现代码提交自动构建和部署。接下来我们将从零开始一步步搭建这个技术栈。2. 环境准备与版本说明在开始编码之前请确保你的本地开发环境已就绪。以下版本是撰写本文时的稳定版本你可以根据实际情况调整。操作系统macOS, Linux (如 Ubuntu 20.04) 或 WSL2 (Windows)。本文命令以 Linux/macOS 为例。JavaJDK 17 或 21 (LTS 版本)。Spring Boot 3.x 需要 JDK 17。java -version # 输出应类似openjdk version 17.0.10 2024-01-16Maven3.8 或 Gradle 7.x用于 Java 项目构建。mvn -v # 或 gradle -vPython3.9 (如果你选择 FastAPI)。python3 --versionDockerDocker Compose用于服务容器化。docker --version docker-compose --versionGit代码版本管理。IDEIntelliJ IDEA (社区版免费), VS Code 或你喜欢的任何编辑器。可选但推荐kubectlKubernetes 命令行工具。Minikube或Kind用于在本地运行 Kubernetes 集群。Postman或curl用于 API 测试。3. 核心组件原理与选型拆解3.1 为什么选择 Spring Boot Docker Kubernetes这是一个经典组合。Spring Boot 提供了快速创建生产级独立应用的能力极大简化了 Spring 应用的初始搭建和开发过程。Docker 将应用及其所有依赖打包成一个标准化的镜像解决了“在我机器上能跑”的难题。Kubernetes 则负责管理这些 Docker 容器提供自动部署、扩展和管理容器化应用的能力。三者结合实现了从开发到部署的标准化和自动化。3.2 PostgreSQL vs. MySQL两者都是优秀的开源关系型数据库。PostgreSQL 在高级特性如对 JSON 的支持、更丰富的索引类型、更强大的 SQL 标准兼容性上通常更胜一筹非常适合复杂业务场景。MySQL 则在简单读写、互联网应用中有广泛的生态和优化经验。对于新项目如果业务模型复杂或需要用到 GIS、全文搜索等扩展PostgreSQL 是很好的选择。3.3 Redis 作为缓存的角色Redis 将数据存储在内存中读写速度极快。它常被用作数据库查询结果的缓存层缓存穿透、击穿、雪崩问题需要处理也可以用于存储会话Session、实现分布式锁、作为简单消息队列等。它是提升应用性能的利器。3.4 消息队列RabbitMQ的应用解耦在微服务或复杂应用中服务A不需要同步等待服务B的处理结果。服务A只需将消息发送到 RabbitMQ服务B监听队列并异步处理。这提高了系统的响应速度、可扩展性和可靠性。4. 完整实战案例构建一个待办事项TodoAPI 后端我们将构建一个简单的待办事项 REST API涵盖用户认证、任务增删改查并使用上述技术栈。4.1 创建 Spring Boot 项目使用 Spring Initializr 或 IDE 创建项目。Project: MavenLanguage: JavaSpring Boot: 3.2.xDependencies:Spring WebSpring Data JPAPostgreSQL DriverSpring Security (用于基础认证)Lombok (减少样板代码)Validation生成并下载项目解压后用 IDE 打开。4.2 使用 Docker Compose 启动基础设施在项目根目录创建docker-compose.yml文件一键启动 PostgreSQL, Redis, RabbitMQ。version: 3.8 services: postgres: image: postgres:15-alpine container_name: todo-postgres environment: POSTGRES_DB: todo_db POSTGRES_USER: todo_user POSTGRES_PASSWORD: todo_pass ports: - 5432:5432 volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U todo_user -d todo_db] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: todo-redis ports: - 6379:6379 command: redis-server --appendonly yes volumes: - redis_data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 5s retries: 5 rabbitmq: image: rabbitmq:3-management-alpine container_name: todo-rabbitmq environment: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest ports: - 5672:5672 # AMQP协议端口 - 15672:15672 # 管理界面端口 volumes: - rabbitmq_data:/var/lib/rabbitmq healthcheck: test: [CMD, rabbitmq-diagnostics, ping] interval: 10s timeout: 5s retries: 5 volumes: postgres_data: redis_data: rabbitmq_data:在终端运行docker-compose up -d使用docker ps检查三个容器是否正常运行。访问http://localhost:15672可以使用guest/guest登录 RabbitMQ 管理界面。4.3 配置应用连接在src/main/resources/application.yml中配置数据源、Redis 和 RabbitMQ。spring: datasource: url: jdbc:postgresql://localhost:5432/todo_db username: todo_user password: todo_pass driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: update # 开发环境使用生产环境应为 validate 或使用 Flyway/Liquibase show-sql: true properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true data: redis: host: localhost port: 6379 # password: 如果Redis有密码则配置 rabbitmq: host: localhost port: 5672 username: guest password: guest logging: level: org.springframework.security: DEBUG # 方便调试生产环境应关闭4.4 编写核心领域模型和 API实体类 (Todo.java):package com.example.todo.entity; import jakarta.persistence.*; import lombok.Data; import java.time.LocalDateTime; Entity Data // Lombok 注解自动生成 getter/setter/toString 等 Table(name todos) public class Todo { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String title; private String description; private boolean completed; private LocalDateTime createdAt; private LocalDateTime updatedAt; PrePersist protected void onCreate() { createdAt LocalDateTime.now(); updatedAt LocalDateTime.now(); } PreUpdate protected void onUpdate() { updatedAt LocalDateTime.now(); } }Repository 接口 (TodoRepository.java):package com.example.todo.repository; import com.example.todo.entity.Todo; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; Repository public interface TodoRepository extends JpaRepositoryTodo, Long { }Service 层 (TodoService.java):package com.example.todo.service; import com.example.todo.entity.Todo; import com.example.todo.repository.TodoRepository; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; Service RequiredArgsConstructor // Lombok 为 final 字段生成构造函数 public class TodoService { private final TodoRepository todoRepository; public Todo createTodo(Todo todo) { return todoRepository.save(todo); } Cacheable(value todos, key #id) // 使用Redis缓存 public OptionalTodo getTodoById(Long id) { return todoRepository.findById(id); } public ListTodo getAllTodos() { return todoRepository.findAll(); } public Todo updateTodo(Long id, Todo todoDetails) { return todoRepository.findById(id).map(todo - { todo.setTitle(todoDetails.getTitle()); todo.setDescription(todoDetails.getDescription()); todo.setCompleted(todoDetails.isCompleted()); return todoRepository.save(todo); }).orElseThrow(() - new RuntimeException(Todo not found with id id)); } public void deleteTodo(Long id) { todoRepository.deleteById(id); } }Controller 层 (TodoController.java):package com.example.todo.controller; import com.example.todo.entity.Todo; import com.example.todo.service.TodoService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/todos) RequiredArgsConstructor public class TodoController { private final TodoService todoService; PostMapping public ResponseEntityTodo createTodo(Valid RequestBody Todo todo) { Todo savedTodo todoService.createTodo(todo); return new ResponseEntity(savedTodo, HttpStatus.CREATED); } GetMapping(/{id}) public ResponseEntityTodo getTodoById(PathVariable Long id) { return todoService.getTodoById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } GetMapping public ResponseEntityListTodo getAllTodos() { return ResponseEntity.ok(todoService.getAllTodos()); } PutMapping(/{id}) public ResponseEntityTodo updateTodo(PathVariable Long id, Valid RequestBody Todo todoDetails) { try { Todo updatedTodo todoService.updateTodo(id, todoDetails); return ResponseEntity.ok(updatedTodo); } catch (RuntimeException e) { return ResponseEntity.notFound().build(); } } DeleteMapping(/{id}) public ResponseEntityVoid deleteTodo(PathVariable Long id) { todoService.deleteTodo(id); return ResponseEntity.noContent().build(); } }启用缓存在主应用类TodoApplication.java上添加EnableCaching注解。4.5 集成消息队列异步邮件通知示例假设任务完成时需要异步发送一封通知邮件这里模拟发送逻辑。配置 RabbitMQSpring Boot 已通过spring.rabbitmq配置自动配置了连接工厂。定义消息模型和队列配置 (RabbitMQConfig.java):package com.example.todo.config; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class RabbitMQConfig { public static final String TODO_COMPLETION_QUEUE todo.completion.queue; Bean public Queue todoCompletionQueue() { return new Queue(TODO_COMPLETION_QUEUE, true); // true 表示持久化 } }消息生产者 (TodoCompletionProducer.java):package com.example.todo.service; import lombok.RequiredArgsConstructor; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Service; Service RequiredArgsConstructor public class TodoCompletionProducer { private final RabbitTemplate rabbitTemplate; private static final String ROUTING_KEY RabbitMQConfig.TODO_COMPLETION_QUEUE; public void sendCompletionNotification(Long todoId, String title) { String message String.format(Todo [ID:%d, Title:%s] has been marked as completed., todoId, title); rabbitTemplate.convertAndSend(ROUTING_KEY, message); System.out.println( [x] Sent message: message); } }在 Service 中触发消息发送修改TodoService.updateTodo方法当任务被标记为完成时发送消息。public Todo updateTodo(Long id, Todo todoDetails) { return todoRepository.findById(id).map(todo - { boolean wasCompleted todo.isCompleted(); todo.setTitle(todoDetails.getTitle()); todo.setDescription(todoDetails.getDescription()); todo.setCompleted(todoDetails.isCompleted()); Todo savedTodo todoRepository.save(todo); // 如果任务状态变为完成发送通知 if (!wasCompleted savedTodo.isCompleted()) { todoCompletionProducer.sendCompletionNotification(savedTodo.getId(), savedTodo.getTitle()); } return savedTodo; }).orElseThrow(() - new RuntimeException(Todo not found with id id)); }消息消费者 (TodoCompletionConsumer.java):package com.example.todo.service; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Service; Service Slf4j public class TodoCompletionConsumer { RabbitListener(queues RabbitMQConfig.TODO_COMPLETION_QUEUE) public void receiveMessage(String message) { log.info( [x] Received completion notification: {}, message); // 在这里实现实际的邮件发送逻辑例如调用邮件服务SDK // sendEmail(userEmail, Your todo is completed, message); System.out.println(模拟已处理完成通知 - message); } }4.6 运行与验证确保 Docker Compose 服务正在运行。在 IDE 中运行TodoApplication主类或使用命令行mvn spring-boot:run。应用启动后使用Postman或curl测试 API创建任务:curl -X POST http://localhost:8080/api/todos \ -H Content-Type: application/json \ -d {title:Learn Kubernetes,description:Study basic concepts,completed:false}查询任务:curl http://localhost:8080/api/todos/1更新任务为完成:curl -X PUT http://localhost:8080/api/todos/1 \ -H Content-Type: application/json \ -d {title:Learn Kubernetes,description:Study basic concepts,completed:true}执行此操作后查看应用控制台日志应该能看到生产者发送和消费者接收消息的日志。查询所有任务:curl http://localhost:8080/api/todos删除任务:curl -X DELETE http://localhost:8080/api/todos/15. 容器化与 Kubernetes 部署5.1 创建 Dockerfile在项目根目录创建Dockerfile用于构建应用镜像。# 使用多阶段构建减小镜像体积 # 第一阶段构建 FROM maven:3.8.7-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 从构建阶段复制jar包 COPY --frombuild /app/target/*.jar app.jar # 设置非root用户运行增强安全性 RUN addgroup -S spring adduser -S spring -G spring USER spring:spring EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar]5.2 构建并运行 Docker 镜像# 构建镜像 docker build -t todo-app:latest . # 运行容器并连接到之前创建的 Docker Compose 网络 # 首先找到docker-compose创建的网络名docker network ls通常为 项目目录名_default docker run -d --name todo-app-container \ --networktodo_default \ # 替换为你的网络名 -p 8080:8080 \ -e SPRING_DATASOURCE_URLjdbc:postgresql://postgres:5432/todo_db \ -e SPRING_DATASOURCE_USERNAMEtodo_user \ -e SPRING_DATASOURCE_PASSWORDtodo_pass \ -e SPRING_REDIS_HOSTredis \ -e SPRING_RABBITMQ_HOSTrabbitmq \ todo-app:latest注意这里通过环境变量覆盖了application.yml中的配置使应用能通过服务名postgres,redis,rabbitmq访问其他容器这是 Docker 网络内 DNS 自动解析的功能。5.3 Kubernetes 部署示例使用 Minikube如果你在本地安装了 Minikube可以尝试部署到 Kubernetes。启动 Minikube 并设置 Docker 环境:minikube start eval $(minikube docker-env) # 让当前shell使用Minikube内部的Docker守护进程在 Minikube 环境中重新构建镜像:docker build -t todo-app:latest .创建 Kubernetes 部署文件k8s-deployment.yaml:apiVersion: apps/v1 kind: Deployment metadata: name: todo-app spec: replicas: 2 # 两个副本 selector: matchLabels: app: todo-app template: metadata: labels: app: todo-app spec: containers: - name: todo-app image: todo-app:latest imagePullPolicy: Never # 因为使用本地镜像 ports: - containerPort: 8080 env: - name: SPRING_DATASOURCE_URL value: jdbc:postgresql://postgres-service:5432/todo_db - name: SPRING_DATASOURCE_USERNAME value: todo_user - name: SPRING_DATASOURCE_PASSWORD value: todo_pass - name: SPRING_REDIS_HOST value: redis-service - name: SPRING_RABBITMQ_HOST value: rabbitmq-service resources: requests: memory: 256Mi cpu: 250m limits: memory: 512Mi cpu: 500m --- apiVersion: v1 kind: Service metadata: name: todo-app-service spec: selector: app: todo-app ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancer # Minikube中会映射到一个NodePort可通过 minikube service 访问部署应用:kubectl apply -f k8s-deployment.yaml查看状态和访问:kubectl get pods,svc minikube service todo-app-service --url # 获取访问URL6. 常见问题与排查思路在整合这套技术栈时你可能会遇到以下典型问题问题现象可能原因排查步骤与解决方案应用启动失败报Connection refused数据库/Redis/RabbitMQ1. Docker Compose 服务未启动。2. 应用配置的主机名或端口错误。3. 网络不互通如 Docker 容器网络 vs 宿主机。1.docker ps检查服务容器状态。2. 确认application.yml或环境变量中的主机名。在 Docker 容器内连接其他服务应使用 Docker Compose 中定义的服务名**如postgres而非localhost**。3. 确保应用容器与数据库容器在同一 Docker 网络中。Spring Boot 应用启动时 JPA 报错如Table “xxx” not found1. 数据库连接成功但表不存在。2.ddl-auto配置为none或validate但表结构未手动创建。1. 开发环境可暂时设为update但生产环境绝对禁止。2. 生产环境应使用数据库迁移工具如 Flyway, Liquibase来管理 DDL。Redis 缓存未生效1. 未在启动类添加EnableCaching。2. 缓存配置错误如 key 生成策略。3. 方法不是public导致 Spring AOP 代理无法切入。1. 检查启动类注解。2. 检查Cacheable的value和key。3. 确保缓存注解标注在public方法上。RabbitMQ 消息发送或接收失败1. 连接信息错误用户名/密码/虚拟主机。2. 队列未声明或名称不匹配。3. 消费者方法异常导致消息被拒绝并丢弃。1. 检查spring.rabbitmq配置。2. 确认生产者和消费者使用的队列名完全一致。3. 在消费者方法中添加try-catch并记录日志或配置死信队列DLX处理失败消息。Docker 构建镜像时下载依赖超慢Maven 中央仓库网络问题。1. 在Dockerfile的RUN mvn dependency:go-offline前可添加阿里云等国内镜像仓库配置复制settings.xml。2. 使用本地.m2目录作为卷挂载适用于开发阶段。Kubernetes Pod 一直处于Pending或CrashLoopBackOff状态1. 资源不足CPU/内存。2. 镜像拉取失败ImagePullBackOff。3. 应用启动失败。1.kubectl describe pod pod-name查看详细事件。2.kubectl logs pod-name查看应用日志。3. 检查 Deployment 中资源配置requests/limits是否合理。7. 最佳实践与工程建议将多个开源组件整合成一个生产就绪的系统需要遵循一些工程最佳实践配置外部化绝不将数据库密码、API密钥等敏感信息硬编码在代码或配置文件中。使用环境变量、Kubernetes Secrets 或专业的配置中心如 Spring Cloud Config, Apollo来管理。在application.yml中敏感信息应引用环境变量password: ${DB_PASSWORD}。日志标准化使用 SLF4J 门面配合 Logback 或 Log4j2。日志要结构化JSON 格式便于收集并区分不同级别DEBUG, INFO, WARN, ERROR。记录关键业务操作、外部调用和异常。健康检查与就绪探针在 Kubernetes 中为你的应用添加livenessProbe和readinessProbe。Spring Boot Actuator 提供了/actuator/health端点可以方便地集成。# 在K8s Deployment中配置 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 30 periodSeconds: 5数据库迁移永远不要依赖ddl-auto: update在生产环境。使用 Flyway 或 Liquibase 来版本化你的数据库 schema确保所有环境的数据库结构一致且变更可追溯。缓存策略明确缓存粒度对象缓存、查询结果缓存、页面缓存。设置合理的过期时间TTL。处理缓存穿透缓存空值、缓存击穿互斥锁、缓存雪崩随机过期时间问题。考虑使用 Redis 的分布式锁实现复杂同步逻辑。消息队列可靠性消息持久化确保消息在 RabbitMQ 重启后不丢失。生产者确认启用publisher confirms确保消息到达 Broker。消费者确认手动确认Manual Ack只有在业务处理成功后才确认消息防止消息丢失。死信队列将处理失败的消息转移到 DLX便于后续排查和重试。容器镜像优化使用多阶段构建减小最终镜像体积。使用.dockerignore文件排除构建上下文中的无关文件如target/,.git/。使用特定标签而非latest便于版本回滚。以非 root 用户运行容器。安全加固API 安全使用 Spring Security 实现认证JWT/OAuth2和授权基于角色的访问控制。依赖安全定期使用mvn dependency:check或OWASP Dependency-Check扫描项目依赖中的已知漏洞。网络策略在 Kubernetes 中使用 NetworkPolicy 限制 Pod 间的网络流量。监控与告警集成 Prometheus 收集指标Spring Boot Actuator 暴露/actuator/prometheus使用 Grafana 进行可视化。对应用错误率、响应时间、系统资源设置告警。CI/CD 自动化将上述 Docker 构建、镜像推送、Kubernetes 部署等步骤编写成 GitHub Actions 或 GitLab CI 的 Pipeline 脚本实现提交代码后自动测试、构建和部署到开发/测试环境。通过遵循这些实践你构建的将不仅仅是一个能跑起来的 demo而是一个具备可维护性、可观测性、安全性和弹性的生产级应用原型。这套以开源和免费资源为核心的技术栈能够以极低的初始成本支撑项目从零到一的快速启动和早期发展这正是“技术边际成本趋近于零”带来的巨大红利。
返回列表