springcloud docker 部署问题排查与解决方案

本文档记录了 service 项目在华为云 Docker 环境部署过程中遇到的所有问题及解决方案。

目录

  • [1. Docker 网络配置问题](#1. Docker 网络配置问题)
  • [2. 端口占用问题](#2. 端口占用问题)
  • [3. Docker 多阶段构建问题](#3. Docker 多阶段构建问题)
  • [4. Alpine 镜像源问题](#4. Alpine 镜像源问题)
  • [5. Nacos 认证问题](#5. Nacos 认证问题)
  • [6. Spring Boot 配置问题](#6. Spring Boot 配置问题)
  • [7. 多数据源配置问题](#7. 多数据源配置问题)
  • [8. MyBatis 配置问题](#8. MyBatis 配置问题)
  • [9. Spring Boot 版本兼容性问题](#9. Spring Boot 版本兼容性问题)

1. Docker 网络配置问题

问题描述

Nacos 也部署在 mlm Docker 网络中,但配置文件使用的是外部域名 ****.*****.com:8848,导致容器内无法访问。

解决方案

修改配置使用 Docker 内部容器名访问:

docker-compose.yml

yaml 复制代码
networks:
  default:
    name: mlm
    external: true

application.yml

yaml 复制代码
spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848  # 使用容器名而非域名
      config:
        server-addr: nacos:8848

2. 端口占用问题

问题描述

主机 8080 端口已被其他服务占用,导致服务无法启动。

解决方案

修改 docker-compose.yml 端口映射:

yaml 复制代码
services:
  service:
    ports:
      - "8081:8080"  # 修改为 8081

3. Docker 多阶段构建问题

问题描述

原 Dockerfile 需要本地先执行 gradle build,增加了部署复杂度。

解决方案

实现 Docker 多阶段构建,将编译集成到镜像构建过程中:

Dockerfile

dockerfile 复制代码
# 第一阶段:使用 Gradle 镜像构建
FROM gradle:8.5-jdk21-alpine AS builder

WORKDIR /build

# 复制构建文件
COPY build.gradle settings.gradle ./

# 复制源代码
COPY src ./src

# 构建项目
RUN gradle clean build -x test --no-daemon

# 第二阶段:运行阶段
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

# 从构建阶段复制编译好的jar文件
COPY --from=builder /build/build/libs/*.jar app.jar

# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 暴露端口
EXPOSE 8080

# 启动应用
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

4. Alpine 镜像源问题

问题描述

在华为云环境构建时,访问 Alpine 官方软件包仓库超时:

复制代码
fetching https://dl-cdn.alpinelinux.org/alpine/v3.22/main: temporary error

解决方案

添加华为云镜像源配置:

Dockerfile

dockerfile 复制代码
# 使用华为云镜像源并安装curl用于健康检查
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.huaweicloud.com/g' /etc/apk/repositories && \
    apk add --no-cache curl

5. Nacos 认证问题

问题描述

应用启动时报错:

复制代码
ErrCode:403, ErrMsg:http error, code=403,msg=user not found!

原因分析

  1. Nacos 配置了 jiahe 用户,但应用使用了错误的认证配置
  2. 命名空间使用了名称而非 ID

解决方案

application.yml

yaml 复制代码
spring:
  cloud:
    nacos:
      username: <your-nacos-username>
      password: <your-nacos-password>
      discovery:
        server-addr: nacos:8848
        namespace: <your-namespace-id>  # 使用 namespace ID
      config:
        server-addr: nacos:8848
        namespace: <your-namespace-id>
        username: <your-nacos-username>
        password: <your-nacos-password>
  config:
    import:
      - optional:nacos:${spring.application.name}.properties

6. Spring Boot 配置问题

问题 6.1: spring.config.import 缺失

错误信息

复制代码
No spring.config.import property has been defined

解决方案

yaml 复制代码
spring:
  config:
    import:
      - optional:nacos:${spring.application.name}.properties

问题 6.2: Bean 定义冲突

错误信息

复制代码
Cannot register bean definition 'hikariPoolDataSourceMetadataProvider'

解决方案

yaml 复制代码
spring:
  main:
    allow-bean-definition-overriding: true

7. 多数据源配置问题

问题描述

使用 dynamic-datasource-spring-boot-starter:4.3.1 库时遇到多个版本兼容性问题:

  1. YAML 配置格式无法识别
  2. DataSourceProperty 类无法访问
  3. AbstractDataSourceProvider 构造参数不兼容
  4. DynamicRoutingDataSource API 不可用

解决方案

完全自定义实现多数据源切换机制,不依赖第三方库。

7.1 创建动态数据源类

DynamicDataSource.java

java 复制代码
package com.jiahetng.service.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static void setDataSource(String dataSourceKey) {
        contextHolder.set(dataSourceKey);
    }
    
    public static String getDataSource() {
        return contextHolder.get();
    }
    
    public static void clearDataSource() {
        contextHolder.remove();
    }
    
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = getDataSource();
        return dataSource != null ? dataSource : "primary";
    }
}
7.2 创建自定义 @DS 注解

DS.java

java 复制代码
package com.jiahetng.service.config;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
    String value() default "primary";
}
7.3 创建 AOP 切面

DynamicDataSourceAspect.java

java 复制代码
package com.jiahetng.service.config;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Slf4j
@Aspect
@Order(1)
@Component
public class DynamicDataSourceAspect {

    @Around("@annotation(com.jiahetng.service.config.DS) || @within(com.jiahetng.service.config.DS)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        
        DS ds = method.getAnnotation(DS.class);
        if (ds == null) {
            ds = method.getDeclaringClass().getAnnotation(DS.class);
        }
        
        if (ds != null) {
            String dataSourceKey = ds.value();
            DynamicDataSource.setDataSource(dataSourceKey);
            log.debug("Switching to datasource: {}", dataSourceKey);
        }
        
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            log.debug("Cleared datasource context");
        }
    }
}
7.4 配置数据源 Bean

DataSourceConfig.java

java 复制代码
package com.jiahetng.service.config;

import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Configuration
public class DataSourceConfig {

    @Value("${spring.datasource.primary.driver-class-name}")
    private String primaryDriverClassName;
    
    @Value("${spring.datasource.primary.url}")
    private String primaryUrl;
    
    @Value("${spring.datasource.primary.username}")
    private String primaryUsername;
    
    @Value("${spring.datasource.primary.password}")
    private String primaryPassword;
    
    @Value("${spring.datasource.secondary.driver-class-name}")
    private String secondaryDriverClassName;
    
    @Value("${spring.datasource.secondary.url}")
    private String secondaryUrl;
    
    @Value("${spring.datasource.secondary.username}")
    private String secondaryUsername;
    
    @Value("${spring.datasource.secondary.password}")
    private String secondaryPassword;

    @Bean(name = "primaryDataSource")
    public DataSource primaryDataSource() {
        log.info("Creating primary datasource: url={}", primaryUrl);
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(primaryDriverClassName);
        dataSource.setJdbcUrl(primaryUrl);
        dataSource.setUsername(primaryUsername);
        dataSource.setPassword(primaryPassword);
        log.info("Primary datasource created successfully");
        return dataSource;
    }

    @Bean(name = "secondaryDataSource")
    public DataSource secondaryDataSource() {
        log.info("Creating secondary datasource: url={}", secondaryUrl);
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(secondaryDriverClassName);
        dataSource.setJdbcUrl(secondaryUrl);
        dataSource.setUsername(secondaryUsername);
        dataSource.setPassword(secondaryPassword);
        log.info("Secondary datasource created successfully");
        return dataSource;
    }

    @Primary
    @Bean(name = "dataSource")
    public DataSource dynamicDataSource(
            @Qualifier("primaryDataSource") DataSource primaryDataSource,
            @Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        
        log.info("Creating dynamic datasource");
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("primary", primaryDataSource);
        targetDataSources.put("secondary", secondaryDataSource);
        
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
        try {
            dynamicDataSource.afterPropertiesSet();
            log.info("Dynamic datasource created and initialized successfully");
        } catch (Exception e) {
            log.error("Failed to initialize dynamic datasource", e);
            throw new RuntimeException("Failed to initialize dynamic datasource", e);
        }
        
        return dynamicDataSource;
    }
}
7.5 更新 Service 类

所有 Service 类的 import 需要更新:

java 复制代码
// 旧的 import
import com.baomidou.dynamic.datasource.annotation.DS;

// 新的 import
import com.jiahetng.service.config.DS;

使用示例:

java 复制代码
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
    
    @DS("primary")
    public User findByUsername(String username) {
        return userMapper.findByUsername(username);
    }
}

@Service
@RequiredArgsConstructor
@Slf4j
public class LoginLogService {
    
    @DS("secondary")
    public List<LoginLog> findByUserId(Long userId, int page, int size) {
        return loginLogMapper.findByUserId(userId, offset, size);
    }
}
7.6 配置文件

application.yml

yaml 复制代码
spring:
  datasource:
    primary:
      driver-class-name: org.mariadb.jdbc.Driver
      url: jdbc:mariadb://mariadb:3306/service_primary?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
      username: <your_username>
      password: <your_password>
    secondary:
      driver-class-name: org.mariadb.jdbc.Driver
      url: jdbc:mariadb://mariadb:3306/service_secondary?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
      username: <your_username>
      password: <your_password>     

8. MyBatis 配置问题

问题 8.1: @MapperScan 注解缺失

错误信息

复制代码
error: cannot find symbol @MapperScan

解决方案

在 ServiceApplication.java 中添加 import:

java 复制代码
import org.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.jiahetng.service.mapper")
public class ServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceApplication.class, args);
    }
}

问题 8.2: SqlSessionFactory 初始化失败

错误信息

复制代码
Error creating bean with name 'sqlSessionFactory': 
Factory method 'sqlSessionFactory' threw exception with message: null

原因分析

  1. 动态数据源 Bean 名称不是 dataSource,MyBatis 无法自动发现
  2. 配置了 mapper-locations 但项目使用注解方式

解决方案

  1. 重命名动态数据源 Bean:
java 复制代码
@Primary
@Bean(name = "dataSource")  // 必须命名为 dataSource
public DataSource dynamicDataSource(...) {
    // ...
    dynamicDataSource.afterPropertiesSet();  // 必须调用此方法
    return dynamicDataSource;
}
  1. 移除 mapper-locations 配置:
yaml 复制代码
# MyBatis配置
mybatis:
  type-aliases-package: com.jiahetng.service.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
  # 不配置 mapper-locations(使用注解方式)
  1. 手动配置 SqlSessionFactory(可选,用于调试):

MyBatisConfig.java

java 复制代码
package com.jiahetng.service.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Slf4j
@Configuration
public class MyBatisConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
        log.info("Creating SqlSessionFactory with dataSource: {}", dataSource.getClass().getName());
        
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        
        // 配置 MyBatis
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setLogImpl(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
        factoryBean.setConfiguration(configuration);
        
        // 设置类型别名包
        factoryBean.setTypeAliasesPackage("com.jiahetng.service.entity");
        
        try {
            SqlSessionFactory sessionFactory = factoryBean.getObject();
            log.info("SqlSessionFactory created successfully");
            return sessionFactory;
        } catch (Exception e) {
            log.error("Failed to create SqlSessionFactory", e);
            throw e;
        }
    }
}

9. Spring Boot 版本兼容性问题

问题描述

应用启动失败,报版本不兼容错误:

复制代码
Spring Boot [3.5.9] is not compatible with this Spring Cloud release train
Action: Change Spring Boot version to one of the following versions [3.4.x]

原因分析

Spring Cloud Alibaba 2023.0.3.2 要求 Spring Boot 3.4.x,但项目使用了 3.5.9。

解决方案

降级 Spring Boot 并同步调整相关依赖版本:

build.gradle

gradle 复制代码
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.1'  // 降级到 3.4.1
    id 'io.spring.dependency-management' version '1.1.7'
    id 'com.google.protobuf' version '0.9.5'
}

ext {
    set('springGrpcVersion', "0.9.0")  // 从 1.0.1 降级到 0.9.0
}

dependencies {
    // MyBatis 版本调整
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'  // 从 4.0.1 降级
    
    // 其他依赖保持不变
    implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery:2023.0.3.2'
    implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config:2023.0.3.2'
    implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer:4.2.0'
}

版本兼容性矩阵:

组件 原版本 调整后版本 说明
Spring Boot 3.5.9 3.4.1 兼容 Spring Cloud 2023.0.x
Spring gRPC 1.0.1 0.9.0 兼容 Spring Boot 3.4.x
MyBatis 4.0.1 3.0.4 兼容 Spring Boot 3.4.x
Spring Cloud Alibaba 2023.0.3.2 2023.0.3.2 无需修改
Spring Cloud LoadBalancer 4.2.0 4.2.0 无需修改

10. 部署验证

完整部署流程

  1. 停止现有容器
bash 复制代码
docker compose down
  1. 构建镜像
bash 复制代码
docker compose build
  1. 启动服务
bash 复制代码
docker compose up -d
  1. 查看日志
bash 复制代码
docker compose logs -f service

成功启动标志

看到以下日志表示启动成功:

复制代码
Creating primary datasource: url=jdbc:mariadb://<db-host>:3306/<primary-db-name>
Primary datasource created successfully
Creating secondary datasource: url=jdbc:mariadb://<db-host>:3306/<secondary-db-name>
Secondary datasource created successfully
Creating dynamic datasource
Dynamic datasource created and initialized successfully
Creating SqlSessionFactory with dataSource: com.jiahetng.service.config.DynamicDataSource
SqlSessionFactory created successfully
Started ServiceApplication in X.XXX seconds

健康检查

bash 复制代码
# 检查服务状态
curl http://localhost:8081/actuator/health

# 检查 Nacos 注册
# 访问 Nacos 控制台查看服务列表

11、注册成功后, 服务实例地址是 Docker 内部 IP,

✅ 解决方案:让 Spring Boot 服务注册真实 IP 地址

✅ 使用 spring.cloud.nacos.discovery.ip 强制指定 IP

修改 application.yml:

复制代码
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
        namespace: <namespace_id>
        username: <your_username>
        password: <your_password>
        # 关键配置:强制使用宿主机 IP
        ip: 192.168.1.100   # 替换为你的服务器真实 IP
        port: 8080
        group: DEFAULT_GROUP

⚠️ 注意:ip 和 port 需要与实际暴露的地址一致。

12. 关键要点总结

  1. Docker 网络: 容器间使用容器名访问,不要用外部域名
  2. 端口映射: 注意主机端口占用问题
  3. 多阶段构建: 将编译集成到 Docker 构建中
  4. 华为云镜像: 使用华为云 Alpine 镜像源
  5. Nacos 配置: 使用命名空间 ID 而非名称
  6. 多数据源: 自定义实现比使用第三方库更可控
  7. MyBatis :
    • 动态数据源必须命名为 dataSource
    • 必须调用 afterPropertiesSet()
    • 注解方式不要配置 mapper-locations
  8. 版本兼容: Spring Boot 降级时需同步调整 gRPC 和 MyBatis 版本

附录:常用命令

bash 复制代码
# Docker 相关
docker compose down                    # 停止容器
docker compose build                   # 构建镜像
docker compose up -d                   # 后台启动
docker compose logs -f service         # 查看日志
docker compose ps                      # 查看容器状态
docker builder prune -f                # 清理构建缓存

# Gradle 相关(本地调试)
./gradlew clean build -x test         # 本地构建
./gradlew dependencies                 # 查看依赖树

# 日志查看
docker compose logs service | grep ERROR      # 查看错误日志
docker compose logs service | grep -A 20 ERROR # 查看错误及后续20行
相关推荐
宋情写2 小时前
docker-compose安装Redis
redis·docker·容器
qqqahhh2 小时前
xml文件的动态化配置,导入
xml·spring·springboot
BullSmall2 小时前
SEDA (Staged Event-Driven Architecture, 分阶段事件驱动架构
java·spring·架构
装不满的克莱因瓶3 小时前
【2026最新 架构环境安装篇三】Docker安装RabbitMQ4.x详细教程
linux·运维·docker·容器·架构·rabbitmq
蓝眸少年CY3 小时前
(第七篇)spring cloud之Hystrix断路器
spring·spring cloud·hystrix
技术宅星云5 小时前
0x00.Spring AI Agent开发指南专栏简介
java·人工智能·spring
蓝眸少年CY5 小时前
(第八篇)spring cloud之zuul路由网关
后端·spring·spring cloud
long3165 小时前
弗洛伊德·沃肖算法 Floyd Warshall Algorithm
java·后端·算法·spring·springboot·图论
杨浦老苏6 小时前
轻量级自托管笔记与清单利器jotty·page
笔记·docker·markdown·todo·群晖