SpringCloud微服务部署环境参数动态设置?

SpringCloud微服务运维最佳实践,如何动态定义各种启动参数?

笔者是一名Java服务端程序员,学习微服务后,在部署时发现过程复杂,且做的重复工作非常多,因此学习基本运维。目标是,使用Kubernetes和容器技术进行微服务编排和部署。

本文只讨论服务容器化,不涉及K8s和Jenkins相关内容。

问题概述

在服务容器化时,一些参数必须动态传入,以适应不同的部署环境。变化的参数包括:

  • 部署环境(开发环境、测试环境、预发环境、生成环境)
  • 服务版本(0.0.1、1.0.2、1.0.1-beta)
  • 服务发现(部署时将服务注册到注册中心如Nacos)

我的Java项目(Example)由多个微服务构成:如Example-core,Example-auth,Example-gateway等。以Example-auth这个微服务作为切入点,打包为Docker容器的代码为:

# 使用官方的OpenJDK 17作为基础镜像
FROM openjdk:17

# 镜像环境变量:
# 开发环境:dev
# 测试环境:test
# 预发环境:staging
# 生产环境:prod, 默认生产环境
ARG ENVIRONMENT=prod

# Jar包的版本,默认0.0.1-SNAPSHOT
ARG JAR_VERSION=0.0.1-SNAPSHOT

# 注册中心服务地址
ARG SERVER_NAME=www.nacos-server.cn

# 维护者
MAINTAINER xlxing@bupt.edu.cn

# 拷贝文件到Docker容器中
COPY target/auth-${JAR_VERSION}.jar /app/auth-${JAR_VERSION}.jar

# 暴露服务端口
EXPOSE 8999

# 容器启动项
ENTRYPOINT ["java", "-jar", "/app/auth-${JAR_VERSION}.jar", "--spring.profiles.active=${ENVIRONMENT}", "--spring.cloud.nacos.discovery.ip=${SERVER_NAME}"]

在构建容器时可以动态传入参数(jar包版本为1.1.0,环境是dev,服务发现地址www.my-auth.cn,构建的容器命名为myapp/example:auth-dev-1.1.0):

docker build --build-arg JAR_VERSION=1.1.0 ENVIRONMENT=dev SERVER_NAME=www.my-auth.cn -t myapp/example:auth-dev-1.1.0 .

该内容产生了以下两个具体问题:

1. 如何动态获取JAR_VERSION

在构建项目时,仍然需要手动传入项目版本,实际上该信息存在于项目中,如该微服务auth的pom.xml文件:

    <artifactId>auth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>auth</name>
    <description>权限微服务</description>

理想情况是:后端程序员来定义项目的版本,而流水线只需要依赖项目即可。

但是在当前情况下:当auth项目的version变更时,构建Docker镜像的指令也需要同步变化。该问题可以总结为,构建容器时如何从项目中获取JAR_VERSION?

docker build --build-arg JAR_VERSION=1.1.0 ENVIRONMENT=dev SERVER_NAME=www.my-auth.cn -t myapp/example:auth-dev-1.1.0 .

2. 如何自动获取宿主机的SERVER_NAME

微服务启动时,将该服务注册到Nacos上,声明自己的服务地址。当部署到具体的服务器上时,该地址才被绑定,如auth服务可以部署在server-a上,也可以部署到server-b上。如何在运行时确定服务地址而不是构建时

补充内容:
auth服务的配置文件

spring:
  application:
    name: auth

  # Nacos配置
  cloud:
    nacos:
      server-addr: www.nacos-server.cn

      # 服务发现
      discovery:
        cluster-name: BEIJING
        # 设置为非临时实例
        ephemeral: true
        # 设置命名空间
        namespace: e35500e1-2441-4001-b60f-3f7d55bxxxxx

      # 配置中心
      config:
        file-extension: yaml # 文件后缀名
        namespace: e35500e1-2441-4001-b60f-3f7d55bxxxxx
        group: DEFAULT_GROUP

dubbo:
  # 将Dubbo注册到Nacos中,这样可供消费者直接使用
  application:
    name: dubbo-auth
  protocol:
    name: dubbo
    port: -1
  registry:
    address: nacos://www.nacos-server.cn?namespace=228df068-54b7-405e-9e32-72c759d79ed9
    group: DEV_DUBBO_GROUP

拓展问题:使用K8s和容器在分布式环境下部署微服务项目最佳实践?

其中涉及到很多配置相关的问题,缺乏一个系统性的文章探讨如何部署和运维。

一些尝试:

  1. 我有两台服务器A和B,我在部署前已经计划将auth服务部署到B上,因此构建时参数SERVER_NAME=B,构建好的容器无法直接在其他服务器上启动。
    一条可行的方法是,在构建容器时不指定SERVER_NAME,在docker run时动态传入参数。
    ARG SERVER_NAME=www.nacos-server.cn 改为ENV SERVER_NAME=www.nacos-server.cn
  2. JAR_VERSION目前是手动更改。

第一个问题:
我们在构建开发流水线的时候,会写一个脚本(比如Jenkins)。类似于这样的一个过程:

  1. 从pom.xml中获取version值,记为VERSION
  2. 使用脚本更改Dockerfile中的JAR_VERSION,此时Dockerfile中的JAR_VERSION=${VERSION}
  3. 生成新的Docker Image,例如 docker build -t myapp/auth:${VERSION} .

总的来说,我们无法直接在Dockerfile中获取pom.xml中的VERSION,但是可以在流水线脚本中往Dockerfile中插入静态值。

第二个问题:
我们可以将流水线部署分为两个部分,构建部署
你的回复没有解决部署时的环境变量问题,SERVER_NAME是与所属的服务器相关的,在使用K8s时构建的配置文件如k8s-prod.yaml中,env也是一些动态参数。使用环境变量只是解决了可以在运行时变更参数的作用。
比如k8s架构有3个子节点k8s-node01、k8s-node02、k8s-node03,在部署的时候将auth这个微服务部署到哪一个节点,对运维工程师来说是透明的。
此时想到了一个笨重的方法:在k8s-node01、k8s-node02、k8s-node03的系统中提前准备好环境变量MY_IPV4,然后在docker run的时候参数除了在k8s的yaml文件中定义,也可以从服务器的环境变量中获取。例如,gateway将auth服务路由到lb://auth,而lb://auth信息是从注册中心拿到的,如http://111.229.38.208:8999
在传统的微服务部署中,我可以基本使用nacos作为注册中心使用。K8s中的注册中心原理是DNS服务器。我现在尝试将传统微服务半自动化部署迁移到k8s部署上来,对于服务注册与发现可能有一些理解不充分。希望前辈指正。
主要问题是:传统部署微服务时,微服务启动时总是将自身(自身所在节点的ip)注册到Nacos,如何使得微服务部署是无状态的。

spring:
  application:
    name: gateway

  cloud:
  # SpringCloudGateway配置
    gateway:
      default-filters:
        # http请求的Header中增加 k, v
        - AddRequestHeader=gateway-env, gateway-dev

      # 前端个请求uri会拼接一个版本前缀发送到特定的服务
      routes:
        - id: auth
          uri: lb://auth
          filters:
            # 服务版本前缀
            - PrefixPath=/api/v1
          predicates:
            - Path=/auth/**

        - id: multimedia
          uri: lb://multimedia
          filters:
            - PrefixPath=/api/v1
          predicates:
            - Path=/multimedia/**

        - id: core
          uri: lb://core
          filters:
            - PrefixPath=/api/v1
          predicates:
            - Path=/core/**

  # Gateway不使用Servlet而是WebFlux
  main:
    web-application-type: reactive

  # 微服务分布式鉴权专用Redis
  # Gateway和Auth共享一个权限管理Redis数据库
  data:
    redis:
      client-type: lettuce
      database: 0
      host: www.xingxiaolin.cn
      port: 6379
      lettuce:
        pool:
          enabled: true
          max-active: 8
          max-idle: 8
阅读 1.3k
avatarAI BotBETA

SpringCloud微服务部署环境参数动态设置

对于你的问题,有几种可能的解决方案。首先,对于JAR_VERSION的动态获取,一种可能的方法是使用Maven的属性。你可以在pom.xml文件中定义一个属性,然后在构建Docker镜像时使用这个属性。例如:

在pom.xml中:

<project>
    ...
    <properties>
        <version>0.0.1-SNAPSHOT</version>
    </properties>
    ...
</project>

然后在Dockerfile中:

FROM openjdk:17
ARG JAR_VERSION=${version}
...
COPY target/auth-${JAR_VERSION}.jar /app/auth-${JAR_VERSION}.jar
...

这样,当你在构建Docker镜像时,Maven会自动替换${version}为实际的版本号。

至于如何自动获取宿主机的SERVER_NAME,一种可能的方法是使用环境变量。你可以在部署时设置环境变量,然后在你的程序中读取这个环境变量。例如,你可以在Docker run时设置环境变量:

docker run -e SERVER_NAME=www.my-auth.cn -t myapp/example:auth-dev-1.1.0 .

然后在你的Java程序中读取这个环境变量:

String serverName = System.getenv("SERVER_NAME");

这样,你可以在部署时动态地设置服务器地址,而不需要修改Dockerfile或程序代码。

1 个回答

更新:
我大概理解了你的问题了。

  1. 你的问题应该是服务注册到nacos的时候,需要把实例的IP端口信息提交上去。问题出在每次部署的时候你需要手动指定这个上报的IP端口,比较麻烦。

这里可能会有问题:

  1. gateway在转发请求到其他服务时,应该走“内网”传输。也就是服务应该使用内网IP去注册。所以你可以在启动的时候直接拿到节点的IP/端口(这个是完全可行的,和是不是传统部署都没有关系)。
  2. 不知道你是不是使用了自动获取的方式去获取实例的IP,但是注册的时候出了问题,注册的服务都是docker容器内部的172.x开头的地址,这个问题导致了gateway没法正常工作。所以你才想到了去手动指定实例的IP。通过手动指定的方式也可以实现正常注册,但是没有必要多绕一圈。

如果你要用传统的方式部署微服务

  1. 使用docker启动容器的时候,建议把网络模式改成host,这样在容器内,能直接获取到节点的IP,注册服务、在多节点的环境,gateway在转发时也都能正常工作。启动实例的时候直接 docker run --network host -d image:tag ,就不需要指定额外的参数了。
  2. docker 默认情况下是只能单个节点运行的,即使 docker-compose 也是一样的。所以在每个节点上,不管怎么样都要考虑到端口分配问题,它需要手动去维护,所以是会有一些麻烦在。
  3. 还是建议你试试用 docker swarm 来做容器编排,简单方便,虽然现在貌似用的不多,但还是docker官方推荐的生产环境部署方式之一。:https://docs.docker.com/get-started/orchestration/

问题1: 如何动态获取 VERSION
通常情况下,在构建的时候就能拿到版本号,通过脚本读取pom.xml文件(或者通过插件,但是实际上也是读取pom.xml)

问题2: 如何自动获取宿主机的SERVER_NAME
可以通过 “环境变量”


我举一个使用 docker swarm 部署微服务的例子(k8s也类似):

docker-compose.yml ,会启动 gateway / auth / core 三个服务

services:
  gateway:
    image: example/gateway:v1
    environment:       # 给这个服务添加环境变量
        - ENVIRONMENT=prod        #当前环境/服务名字/nacos注册信息
        - SERVER_NAME=gateway
        - NACOS_SERVER=nacos.domain.com
        - NACOS_namespace=e35500e1-2441-4001-b60f-3f7d55bxxxxx
    ports:
      - "8080:8080"    # 暴露网关的端口
    deploy:
      mode: replicated
      replicas: 2        # 给这个服务部署两个实例,下面的操作都类似。
  auth:
    image: example/auth:v1
    environment:
        - ENVIRONMENT=prod
        - SERVER_NAME=auth
        - NACOS_SERVER=nacos.domain.com
        - NACOS_namespace=e35500e1-2441-4001-b60f-3f7d55bxxxxx
    deploy:
      mode: replicated
      replicas: 2
  core:
    image: example/core:v1
    environment:
        - ENVIRONMENT=prod
        - SERVER_NAME=core
        - NACOS_SERVER=nacos.domain.com
        - NACOS_namespace=e35500e1-2441-4001-b60f-3f7d55bxxxxx
    deploy:
      mode: replicated
      replicas: 2

环境变量,通常都是在“运行时”获取的,但是你可以把一些默认的环境变量,在“构建时”时写入到镜像中。

使用“ARG”也是可以的,但是感觉不太方便。

如果你已经用了微服务,就不建议用 纯docker 去手动管理容器和服务了,你应该把部署的工作交给 “容器编排系统” 来做,比如 k8s / docker-swarm 。


nacos读取配置文件的时候,是可以直接从环境变量中取值的,可以不用手动传参数。


补充一个k8s部署的例子:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      containers:
      - name: gateway
        image: example/gateway:v1
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        env:
          - name: ENVIRONMENT
            value: prod
          - name: SERVER_NAME
            value: gateway
        ports:
        - containerPort: 8000

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth
spec:
  selector:
    matchLabels:
      app: auth
  template:
    metadata:
      labels:
        app: auth
    spec:
      containers:
      - name: auth
        image: example/auth:v1
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        env:
          - name: ENVIRONMENT
            value: prod
          - name: SERVER_NAME
            value: auth
        ports:
        - containerPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: core
spec:
  selector:
    matchLabels:
      app: core
  template:
    metadata:
      labels:
        app: core
    spec:
      containers:
      - name: core
        image: example/core:v1
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        env:
          - name: ENVIRONMENT
            value: prod
          - name: SERVER_NAME
            value: core
        ports:
        - containerPort: 8000


---
apiVersion: v1
kind: Service
metadata:
  name: gateway
spec:
  selector:
    app: gateway
  ports:
  - port: 8000
    targetPort: 8000

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
  labels:
    name: example-ingress
spec:
  rules:
  - host: example.domain.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: gateway
            port: 
              number: 8000
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题