前后端分离项目的容器编排实战

前后端分离项目的容器编排实战

背景

  • 在实际的生产环境中,往往一套硬件设备需要部署多个服务,这对运维以及开发者提出了一些要求

    • 硬件网络/计算资源的调配(端口/CPU/内存)需要进行统一管理
    • 开发环境下的项目需要面向多种类型硬件实现自动化部署与管理
      • windows与linux环境的业务部署
      • 不同指令集服务器的业务部署
      • 联网与离线环境的业务部署
    • 各服务的拓展性与灵活性的实现(依据实际硬件/业务需求拓展与关闭相关服务)
  • 实际生产环境一般会选择使用docker/podman进行相关服务的部署,但简单使用docker run无法实现服务间的通信与自动化部署

  • 本文通过容器编排解决这一问题

  • 容器编排作为概念,主要指容器化应用的部署/管理/扩展和网络配置等操作的自动化.

  • 容器编排作为一系列技术的总称,主要涉及多种部署工具和平台,常见的编排平台如K8S/docker swarm则提供了一系列实现容器编排的技术与平台

  • 本文面向实际业务,利用dockerfile以及docker-compose实现了容器的编排

实验环境

  • nobara
  • i5-11300H+16G+2T

事前工作

  1. 所需材料
  • 一个开发完毕的业务系统
  • docker以及docker-compose环境
  1. 业务系统技术栈及其容器
  • 技术栈:spring-boot+redis+react+mysql
  • 容器:redis:latest\mysql:8\java:openjdk-8-jre-alpine\nginx:latest

实施方法

拉取相关镜像

$> sudo docker pull redis:latest
$> sudo docker pull mysql:8
$> sudo dcoker pull java:openjdk-8-jre-alpine
$> sudo docker pull nginx:latest  

使用docker run 实现相关业务上线

使用docker run方法适合在开发与测试阶段对系统进行部署

  • 好处在于较为简单,无需重复构建镜像
  • 缺点在于无法自动化部署,且单一命令往往很长,无法维护与阅读

实际项目目录

$> tree -L 1
.
├── 2nd.sql # 数据库文件
├── docker-compose.yml
├── Dockerfile
├── xxxx # 前端项目目录
├── xxxx_java # 后端项目目录
├── init_db.sh
└── nginx.conf # nginx配置文件

方案一

$> sudo docker network create soap-networks 
$> sudo docker run --name soap-redis --network soap-networks -d redis:latest
$> sudo docker run -p 3306:3306 --name soap-mysql -e MYSQL_DATABASE=${数据库名称} -e MYSQL_ROOT_PASSWORD=123456 --network soap-networks -d mysql:8 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 
$> sudo docker exec -i soap-mysql sh -c 'exec mysql -uroot -p123456 ${数据库名称}' < ./2nd.sql
$> sudo docker run  --name soap-app --network soap-networks -v ${pwd}/xxxx_java/xxxx_main/target:/app/  java:openjdk-8-jre-alpine java -jar /app/xxxx_main.jar
$> sudo docker run --rm --entrypoint=cat nginx /etc/nginx/nginx.conf > ./nginx.conf

更改nginx.conf的内容

...
http{
  ...
  server{
+    listen 8088;
    ...
+    location /${API路径} {
+      rewrite ^/${API路径}/(.*)$ /$1 break;
+      proxy_pass ${API_URL};
+      proxy_set_header Host $host;
+      proxy_set_header X-Real-IP $remote_addr;
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+      proxy_set_header X-Forwarded-Proto $scheme;
    }
    ...
  }
}

启动nginx容器

$> sudo docker run -p 8088:8088 --name soap-nginx -v ./nginx.conf:/etc/nginx/nginx.conf:ro -v ./xxxx/build:/usr/share/nginx/html:ro --network soap-networks nginx

方案二

$> sudo docker run --name soap-redis --net host -d redis 
$> sudo docker run --network host --name soap-mysql -e MYSQL_DATABASE=famsdb -e MYSQL_ROOT_PASSWORD=123456 -d mysql:8 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 
$> sudo docker run --network host --name soap-app -v ${pwd}/xxxx_java/xxxx_main/target:/app/  java:openjdk-8-jre-alpine java -jar /app/xxxx_main.jar
...
# 省略下载nginx.conf及其更改的步骤,详情参考方案一
...
$> sudo docker run --network host --name soap-nginx -v ./nginx.conf:/etc/nginx/nginx.conf:ro -v ./xxxx/build:/usr/share/nginx/html:ro --network soap-networks nginx

相关步骤解释

  1. 方案一
  • 相关命令解析
    • docker network create soap-network 创建一个可供各容器间互相通信的网络环境
    • docker run --name soap-redis --network soap-networks -d redis:latest 创建一个redis容器
      • 由于已设置soap-network,redis可监听该网络内的所有6379端口请求.所以无需向宿主机暴露6379端口
      • --name为设置容器名字
      • --network为设置容器网络环境
      • -d redis:latest为使用redis:latest镜像启动容器,-d为后台运行容器,直观体现为不打log
    • docker run -p 3306:3306 --name soap-mysql -e MYSQL_DATABASE=${数据库名称} -e MYSQL_ROOT_PASSWORD=123456 --network soap-networks -d mysql:8 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 为创建mysql数据库
      • -p 3306:3306为面向宿主机暴露容器的端口,容器的3306端口暴露于宿主机的3306端口中,暴露该端口是为了能在宿主机通过相关工具链接该mysql
      • -e为设置容器系统变量,关于环境变量的设置需要具体参考mysql文档Environment Variables章节
        • MYSQL_ROOT_PASSWORD为设置数据库root用户的密码
        • MYSQL_DATABASE为创建容器数据库,当然后续也可继续创建数据库
      • --character-set-server--collation-server参考mysql文档Configuration without a cnf file章节,为设置数据库字符集以及字符排序
    • docker exec -i soap-mysql sh -c 'exec mysql -uroot -p123456 ${数据库名称}' < ./2nd.sql为导入相关sql文件
      • docker exec为在up状态的容器中执行相关命令
        • -i为保持STDIN输入,允许向终端的持续输入
        • sh -c 'exec ...'为使用sh执行'...'内的命令;exec mysql ....为执行相关mysql命令后关闭shell进程
    • docker run --name soap-app --network soap-networks -v ${pwd}/xxxx_java/xxxx_main/target:/app/ java:openjdk-8-jre-alpine java -jar /app/xxxx_main.jar为运行相关java程序
      • -v为将本地路径挂载于容器之中,在本命令中将target目录挂载于容器的/app路径下
      • java -jar /app/xxxx_main.jar镜像启动时会在默认入口点中自动执行该命令
      • docker run --rm --entrypoint=cat nginx /etc/nginx/nginx.conf > ./nginx.conf为下载nginx镜像中的配置文件,下载了配置模板文件才好进行相关配置修改
        • --rm当容器退出时自动删除容器
        • --entrypoint重写入口点脚本
    • docker run -p 8088:8088 --name soap-nginx -v ./nginx.conf:/etc/nginx/nginx.conf:ro -v ./xxxx/build:/usr/share/nginx/html:ro --network soap-networks nginx为运行前端程序
  • 知识点解析
    • 容器入口点
      • 定义:docker容器在启动的时候所默认执行的脚本以及程序

      • 查看镜像的相关entrypoint,可以可以看到java镜像的入口点为空,而nginx镜像的入口点为docker-entrypoint.sh

        $> sudo docker images
        nginx                   latest                 a8758716bb6a   5 months ago   187MB 
        java                    openjdk-8-jre-alpine   fdc893b19a14   7 years ago    108MB 
        $> sudo docker inspect a87 --format='{{.Config.Entrypoint}}'
        [/docker-entrypoint.sh]
        $> sudo docker inspect fdc --format='{{.Config.Entrypoint}}'
        sudo docker inspect fdc --format='{{.Config.Entrypoint}}'
        []
        

        启动java程序,其'java -jar /app/xxxx_main.jar'作为参数放在了命令的最后,是因为这个参数会以参数的形式传递给入口点,但又因该镜像入口点为空,所以相当于重写了入口点

        下载nginx镜像中的配置文件,覆盖入口点为cat, '/etc/nginx/nginx.conf'作为参数传递给入口点,可以理解为容器在初始化的时候执行 cat /etc/nginx/ngixn.con,在执行完毕后退出并删除该容器(--rm),并将cat的结果存入./nignx.conf中

    • nginx相关配置
      • 参考nginx文档章节,需要挂载配置文件以及前端项目的编译文件

      • 其中,在本项目中需要配置nginx代理

        location /${API路径} {
          rewrite ^/${API路径}/(.*)$ /$1 break; # 重写api路径,该指令将URL重写,会去掉URL中${API路径}的部分
          proxy_pass http://${API_URL}:8888; # 结合上一个配置,当前端发出/${API路径}/usr/login等的post请求时,nginx会代理该请求,发送到http://${API_URL}:8888/usr/login
          proxy_set_header Host $host; # 设置Host头部为原始请求的主机名
         proxy_set_header X-Real-IP $remote_addr; # 设置X-Real-IP头部为客户端的IP地址
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 参考上文
          proxy_set_header X-Forwarded-Proto $scheme;
        }
        
  1. 方案二

方案二与方案一的区别在于network模式为主机的host模式,使用该模式进行相关开发较方案一更为便利

使用dockerfile实现相关业务上线

在本项目中需要编写nginx/mysql/java的Dockerfile

### java
FROM java:openjdk-8-jre-alpine # 基础镜像为java:openjdk-8-jre-alpine 

ENV LANG=LANG=en_US.utf8 LC_ALL=en_US.utf8
ENV MYSQL_HOST=172.22.10.3
ENV MYSQL_PORT=3306
ENV MYSQL_ROOT_PASSWORD=123456
ENV REDIS_HOST=172.22.10.2
ENV REDIS_PORT=6379

COPY ./fams_main/target /app/ # 拷贝java项目的编译文件进入宿主机中

ENTRYPOINT  ["java", "-jar", "/app/fams_main.jar"] # 写入入口点命令

EXPOSE 8888 # 对外暴露8888端口

### nginx
FROM nginx:latest

ENV LANG=en_US.utf8 LC_ALL=en_US.utf8

COPY ./build /usr/share/nginx/html # 拷贝前端编译文件到镜像内
COPY ./nginx.conf /etc/nginx/templates/nginx.conf.template # 拷贝nginx配置文件到镜像内

EXPOSE 8088

### mysql
FROM mysql:8

EXPOSE 3306

COPY ./init_db.sh /docker-entrypoint-initdb.d/
COPY ./2nd.sql /2nd.sql

使用Dockerfile的优势在于方便将编译文件打包为镜像,使用docker save命令即可将相关镜像文件上传到内网进行部署

java

Dockerfile的编写要依据将具体项目配置,本项目的java程序配置文件如下

server
  port: 8888

# 数据库配置
spring:
  datasource:
    url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/famsdb?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: ${MYSQL_ROOT_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
  batch:
    table-prefix: sys_
  # redis 配置
  redis:
    # 地址
    host: ${REDIS_HOST}
    # 端口,默认为6379
    port: ${REDIS_PORT}
    # 密码
    # password: 123456
    # 连接超时时间
    timeout: 10s
    jedis:
      pool:
        max-active: 8 #最大连接数据库连接数,设 0 为没有限制
        max-idle: 8 #最大等待连接中的数量,设 0 为没有限制
        max-wait: -1ms #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
        min-idle: 0 #最小等待连接中的数量,设 0 为没有限制

在实际生产环境中应尽量避免将相关配置信息进行硬编码,所以application.yml里的关键信息会以变量的形式进行定义

因此,在Dockerfile中需要定义相关环境变量

mysql

为了达到在容器初始化阶段即完成相关数据库以及数据库表的初始化,本项目编写了init_db.sh以及导出了sql文件2nd.sql来进行数据库的初始化

set - e

mysql -uroot -p123456 <<-EOSQL
    CREATE DATABASE IF NOT EXISTS famsdb CHARACTER SET utf8mb4;
EOSQL

mysql -uroot -p123456 famsdb <  /2nd.sql

set -e:在sh文件出错时立即退出脚本

<<-EOSQL ... EOSQL这里指示的是HereDoc特性,表明在进入mysql客户端后以CREATE DATABASE...为后续输入,从而创建相关数据库

mysql -uroot -p123456 famsdb < /2nd.sql导入容器sql文件

通过覆盖入口点脚本,当容器初始化时即可完成数据库初始化

具体实施方案

$> sudo docker build -t soap-mysql:latest . # 构建mysql镜像,-t 参数为该镜像标签
$> cd xxxx_java
$> sudo docker build -t soap-javabackend:latest .
$> cd ../xxxx
$> sudo docker build -t soap-nginx:latest .
$> sudo docker network create --driver bridge --subnet=172.22.10.0/24 --gateway=172.22.10.1 soap-network # 依据Dockerfile,设置docker network的网关\子网以及网络类型
$> sudo docker -p 6379:6379 --name soap-redis --network soap-network --ip 172.22.10.2 -d redis:alpine # 依据Dockerfile,设置redis的静态ip为172.22.10.2,该静态ip地址应该与docker network所设定的子网相对应,否则该命令会报错
$> sudo docker -p 3306:3306 --name soap-mysql --network soap-network --ip 172.22.10.3 -d soap-mysql:latest # 依据上文的镜像名新建容器
$> sudo docker -p 8888:8888 --name soap-javabackend --network soap-network -d soap-javabackend:latest
$> sudo docker -p 8888:8888 --name soap-nginx --network soap-network -e NGINX_ENVSUBST_OUTPUT_DIR="/etc/nginx" -e API_URL="http://soap-javabackend:8888" -d soap-nginx:latest 

其中,nginx的启动需要额外配置NGINX_ENVSUBST_OUTPUT_DIR以及API_URL,nginx容器无法支持直接读取环境变量,所以需要通过envsubst在文本文件中替换环境变量的值

在本项目中需要替换nginx.conf中的${API_URL}的值,具体的原理可以参考nginx文档Using environment variables in nginx configuration (new in 1.19)章节的解释

使用docker-compose实现相关业务上线

在前文中,我们使用了docker run 以及dockerfile的方式进行了业务的上线

从业务部署角度来讲,三者具有以下区别:

  1. docker run的方式比较方便,适合在项目开发过程中使用;但是也有着配置项太多不好记忆,不利于项目的交接,无法对多个容器进行维护,挂载卷通常在本地,无法进行交付等问题

  2. dockerfile与docker run的区别在于将编译好的项目以及一些持久化的文件进行打包并输出成镜像.其运行命令也简单了很多,交付过程中所需要的工作量也少了很多,但是对于被交接人员来讲,仍然需要有一定的容器管理基础

  3. 使用docker-compose方式,使用docker-compose up以及docker-compose down两条命令即可完成所有业务的上线与下线,并且省去了build的过程,更加的便于交付

docker-compose是用于定义和运行多容器的一种工具,他可以很轻松的控制所有的容器栈,网络以及挂载卷,通过配置文件(docker-compose.yml)就可以创建,启动,销毁所有的服务

从三者的关系来讲,使用docker-compsoe需要了解docker run以及dockerfile的写法才能编写相关脚本,下文围绕docker-compose.yml文件讲解docker-compsoe的相关知识点

version: '3' # docker-compose版本号
services: # 顶级元素,用于定义每个服务容器的配置
  xxxx-redis: # 定义单个容器的名字
    image: redis:alpine # 指定容器的来源为 redis:alpine
    container_name: xxx-redis # 指定容器名字
    networks: # 指定容器网络配置
      xxxx-network: # 指定网络名称
        ipv4_address: ${REDIS_HOST} # 指定容器在该网络内的静态ip地址,但是指定该地址需要在顶级元素network中配置ipam属性,ipam中的子网配置必须覆盖该地址
    restart: always

  xxxx-app:
    build: # docker-compose既可以从现成的镜像中启动,也可以从用户自定义的镜像中启动
      context: ./xxxx_java # 定义Dockerfile的具体路径
      dockerfile: Dockerfile # 指定dockerfile的命名.结合上一个配置,意味从./xxxx_java/Dockerfile中进行镜像的构建.如果没有现成的dockerfile,则可以使用dockerfile_inline属性进行定义  
    container_name: fams-app
    ports: # 对应于 docker run里的-p 参数,只有当network是host模式的时候才可进行该项配置
      - "8888:8888"
    networks:
      xxxx-network:
        ipv4_address: ${JAVABACKEND_HOST}
    depends_on: # 定义该服务的依赖关系
      - xxxx-redis
      - xxxx-mysql
    restart: always
    environment: # 定义容器环境变量
      - MYSQL_HOST=${MYSQL_HOST}
      - MYSQL_PORT=${MYSQL_PORT}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}

  xxxx-mysql:
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: xxxx-mysql
    networks:
      xxxx-network:
        ipv4_address: ${MYSQL_HOST}
    ports:
      - "3306:3306"
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}  

  xxxx-nginx:
    build: 
      context: ./xxxx
      dockerfile: Dockerfile
    ports:
      - "8088:8088"
    volumes: # 设置挂载卷
      - ./access.log:/var/log/nginx/access.log
    working_dir: /usr/share/nginx/html
    environment:
      NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx
      API_URL: ${API_URL}
    networks:
      - xxxx-network
    depends_on:
      - xxxx-app
      - xxxx-mysql
      - xxxx-redis

networks:
  xxxx-network:
    ipam:
      driver: default
      config:
        - subnet: ${SUBNET}
          gateway: ${GATEWAY_HOST}

完成配置文件的编写,搭配'docker-compose up'命令,即可完成相关业务的部署

posted @ 2024-04-23 00:01  五花肉炒河粉  阅读(62)  评论(0)    收藏  举报