服务接入容器化时遇到的Springboot读取配置的优先级问题

212 阅读7分钟

背景:

最近有个服务要接入容器化,过程不太顺利,特别是因为一个Springboot读取配置的优先级的问题和运维同事搞了很久,归根结底还是因为对这块知识的不熟悉,因为决定补下相关知识,并在这里描述下这个问题记录下。服务名字不重要,假设服务名为sky。

在这里简述下服务接入容器化过程:

  1. 众所周知,Springboot服务要接入容器化的话,需要准备添加Dockerfile文件和启动脚本,Dockerfile文件定义了用于构建Docker镜像的命令,然后将添加了Dockerfile和启动脚本的分支推送到远程仓库。
  2. Jenkins从代码仓库中拉取指定的代码分支。通过执行Dockerfile文件中的命令,打包出可执行的服务jar包,根据jar包构建Docker镜像,构建完成后,Jenkins将生成的Docker镜像推送到容器镜像仓库
  3. ArgoCD是管理k8s容器环境的工具,在ArgoCD中定义了Kubernetes的部署文件(如DeploymentService),这些文件描述了如何从镜像仓库拉取镜像并运行容器,当ArgoCD检测到新镜像或配置更改时,它会触发Kubernetes拉取最新的Docker镜像。Kubernetes根据Deployment配置创建新的Pod,并使用指定的Docker镜像启动容器,然后对外暴露服务提供服务名,从而使网关可以将流量路由到新启动的Pod。

相关配置:

服务启动脚本 start.sh:

#!/bin/sh  
java -Dreactor.netty.ioWorkerCount=200 \  
-Dreactor.netty.pool.leasingStrategy=lifo\  
-Dreactor.netty.pool.maxIdleTime=1000 \  
-Dspring.cloud.nacos.config.server-addr=${NACOS_HOST}:${NACOS_PORT} \  
-Dspring.cloud.nacos.config.namespace=${NACOS_TENANT} \  
-Dspring.cloud.nacos.config.file-extension=yaml \  
-Dspring.cloud.nacos.config.username=${NACOS_USERNAME} \  
-Dspring.cloud.nacos.config.password=${NACOS_PASSWORD} \  
-Dfile.encoding=UTF-8\  
-server \  
${JAVA_OPTS} -jar application.jar

通过该启动脚本可以知道需要在容器里配置Nacos和Java参数相关环境变量,然后在服务启动时环境变量注入进来。顺便补上nacos环境变量定义:

NACOS_DATA_ID: sky-test.yaml
NACOS_HOST: xxxx
NACOS_PORT: xxx
NACOS_USERNAME: xx
NACOS_PASSWORD: xx
NACOS_TENANT: api

既然已经有这些环境变量了,说明运维已经配置好了,namespace为api,data-id为sky-test.yaml的nacos配置文件已经存在

application.yaml文件:

sky:
  web:
    base-url:
      value: /api
spring:
  application:
    name: sky
  codec:
    max-in-memory-size: 10MB
  messages:
    basename: i18n/messages
  jackson:
    default-property-inclusion: non_null # 全局jackson配置
  profiles:
    active: dev
server:
  port: 8080

---
# dev环境配置
logging:
  config: classpath:log4j2-dev.xml
spring:
  config:
    activate:
      on-profile: dev
    import:
      - nacos:sky-dev.yaml
  cloud:
    nacos:
      #nacos配置
      username: xxx
      password: xxx
      config:
        enabled: true
        server-addr: xxxx
        group: DEFAULT_GROUP
        file-extension: yaml
        namespace: test
        
---
# test环境配置
logging:
  config: classpath:log4j2-dev.xml
spring:
  config:
    activate:
      on-profile: test
    import:
      - :nacos:sky-test.yaml
  cloud:
    nacos:
      #nacos配置
      username: xxx
      password: xxxx
      config:
      enabled: true
      server-addr: xxxx
      group: DEFAULT_GROUP
      file-extension: yaml
      namespace: test

通过application.yaml文件我们知道当环境为dev时会启动dev的nacos配置,当环境为test时会启动test的nacos配置。
并且存在了namespace为test,data-id为sky-dev.yaml的nacos配置和namespace为test,data-id为sky-test.yaml的nacos配置
nacos配置的引入方式大家可以了解下,以前Springboot服务配置nacos需要添加bootstrap.yaml配置文件,但在Nacos 2021 版本及以上版本默认取消了bootstrap依赖,可以基于Springboot 2.4.x引入的spring.config.imports方式配置,以上配置就是这样引入的。

问题出现:

在Jenkins成功构建出镜像后,k8s pod却启动失败了。在argocd上查看日志看到报错信息,这里报错信息就不提供了,具体是因为缺少了某个类bean的定义,这是因为nacos配置没有成功注入到该bean导致的启动失败。由此我确定启动失败原因是读取nacos配置有问题.

那为啥读取nacos配置会有问题呢?要看是怎么读取nacos配置的,这就得看启动脚本了
用启动脚本执行相当于用命令行启动服务,而Springboot读取配置命令行的优先级是高于配置文件
由启动脚本我们知道读取nacos的namespace为api,data-id环境变量虽然配置了为sky-dev.yaml,却没在脚本里指定,另外启动脚本没有指定profile,命令行参数里如果profile没有指定就会读取application.yaml文件里默认的,spring.profiles.active=dev指定了profile为dev,在application.yaml文件里dev环境配置里spring.config.import[0]=nacos:sky-dev.yaml可以知道指定了读取的nacos配置data-id为sky-dev.yaml。
综合以上分析可以知道最终服务读取的nacos namespace为api,data-id为sky-dev.yaml的配置文件。

这样就不对了,因为如果有仔细从头读过来的话就会知道,存在的nacos配置文件只有三种:
namespace为api,data-id为sky-test.yaml的配置文件
namespace为test,data-id为sky-dev.yaml的配置文件
namespace为test,data-id为sky-test.yaml的配置文件
不存在namespace为api,data-id为sky-dev.yaml的配置文件,所以这会导致从nacos读取的配置文件为empty,从而导致启动失败

如何解决:

知道了问题原因如何解决就变得很简单了。

  • 在启动脚本start.sh加上两行参数:
    1.表示开启nacos配置,这点原本读取的是application.yaml里的;
    2.表示读取的nacos配置的data-id为sky-test.yaml
-Dspring.cloud.nacos.config.enabled=true \
-Dspring.config.import[0]=nacos:sky-test.yaml \
  • 将application.yaml里的spring.profiles.active=dev注释掉,这样就不会激活profiles,也可以在start.sh指定profiles覆盖,不过这个脚本启动不需要激活profiles,所以还是注释掉吧 image.png

经验总结:Springboot读取配置的优先级

从以上问题分析排查我们可以从中知道Springboot读取配置优先级的特性有这么以下两个:

  • 同样的配置项,高优先级配置源会覆盖低优先级配置源,就像上面命令行参数namespace配置项会覆盖application.yaml文件里的namespace配置项
  • 如果某个配置项如果高优先级配置源没有就会读取低优先级配置源,比如上面命令行参数没有nacos data-id配置项,就会读取application.yaml文件里的data-id配置项

除此两点之外,再另外再加上一点:

  • springboot读取配置的方式不是按优先级读取然后读取到就停止,而是按照优先级从高到低的顺序,所有位置的文件都会被加载,高优先级配置内容会覆盖低优先级配置内容

以上三点应该作为日常开发常识去了解,也都很容易理解

在Spring Boot生态系统中,配置属性可以从各种来源获取,比如:Java属性文件、YAML文件、环境变量、命令行参数等。这些配置属性能够在运行时动态注入到Bean中,极大地提高了系统的可扩展性和可配置性。

SpringBoot Externalized Configuration(SpringBoot外部化配置)

我们一般都是将Spring Boot服务配置外部化,这使得应用程序能够在不改变代码的情况下适应不同的运行环境。外部化配置意味着将应用程序的关键配置信息移至应用程序代码之外,便于根据不同环境(如开发、测试、生产等)进行定制化配置。

Config Data(配置数据)是Spring Boot中用于外部化应用配置的核心部分。主要由内部配置文件以及外部配置文件。

(1)这里提供下全局配置文件加载优先级,指的是读取application.properties或者application.yml文件:

  • file:./config/ (当前项目路径config目录下);
  • file:./ (当前项目路径下);
  • classpath:/config/ (类路径config目录下);
  • classpath:/ (类路径下).

以上是按照优先级从高到低的顺序,所有位置的文件都会被加载,高优先级配置内容会覆盖低优先级配置内容。 可以通过使用命令行参数的形式,启动项目的时候来指定配置文件的新位置,因为命令行参数优先级更高

(2)也提供下Spring Boot对来自不同配置源配置项优先级顺序,优先级从上到下变高,即后面的配置源将覆盖前面的配置源:

  1. 默认属性(通过SpringApplication.setDefaultProperties方法设置)
  2. @PropertySource注解加载的配置
  3. Config Data(配置数据)(本地文件系统或打包在jar中的application.properties和application-{profile}.properties)
  4. 特殊属性源(如随机数生成器、环境变量、系统属性、JNDI属性等)
  5. Servlet容器相关的初始化参数
  6. SPRING_APPLICATION_JSON格式的环境变量或系统属性
  7. 命令行参数
  8. 测试相关的属性注入方式(如@SpringBootTest@DynamicPropertySource@TestPropertySource

更详细的配置加载优先级可以去看Springboot文档。Spring Boot Reference Documentation

参考引用