基于 Docker Compose 的微服务架构部署实践
在现代软件开发中,微服务架构已成为主流趋势。本文将深入探讨如何使用 Docker Compose 来部署和管理一个复杂的微服务系统,重点分析技术要点和实现方式,以便后续复用。
核心技术要点
1. Docker Compose 编排技术
Docker Compose 是定义和运行多容器 Docker 应用程序的工具。通过 YAML 文件配置应用程序的服务,然后使用单个命令即可创建并启动所有服务。
在该项目中,使用了多个 docker-compose 文件来管理不同层次的服务:
文件 | 用途 |
---|---|
docker-compose.middleware.yaml | 管理基础中间件服务(Redis、MySQL、Elasticsearch等) |
docker-compose.app.yaml | 管理应用服务(网关、认证、业务应用等) |
这种方式的优势在于:
- 服务分层管理,职责清晰
- 可以独立启动和停止不同层次的服务
- 便于开发、测试和生产环境的差异化配置
2. 环境变量管理
项目采用 .env 文件统一管理环境变量,这种方式具有以下优点:
- 配置集中化:所有配置参数集中在一个文件中,便于维护
- 环境隔离:不同环境(开发、测试、生产)可以使用不同的 .env 文件
- 安全性:敏感信息(如密码)不会硬编码在配置文件中
例如,在 docker-compose.middleware.yaml 中使用环境变量:
redis:
env_file:
- .env # 通用配置文件
image: redis:latest
container_name: ${REDIS_CONTAINER_NAME}
restart: always
volumes:
- ${DATA_DIR}/redis:/data
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
ports:
- "${REDIS_PORT:-6379}:6379"
3. 微服务注册与发现
项目使用 Nacos 作为服务注册与发现中心。在 bootstrap-nacos.yml 配置文件中可以看到相关配置:
spring:
cloud:
nacos:
discovery:
server-addr: ${MID_NODE}:${MICRO_SERVICE_PORT}
namespace: ${MICRO_SERVICE_NAMESPACE}
group: ${MICRO_SERVICE_GROUP}
ip: ${APP_NODE}
port: ${服务端口}
这种配置方式使得服务能够动态注册到注册中心,并通过服务名进行调用,实现了服务间的解耦。
实现方式详解
1. 环境配置文件生成
项目使用 pre-start.sh 脚本从 .env.tmp 模板生成实际的 .env 文件,支持端口偏移和容器名称后缀功能:
# 设置端口偏移量
OFFSET_PORT=500 ./pre-start.sh
# 设置容器名后缀
SUFFIX="-test" ./pre-start.sh
该脚本的主要功能包括:
- 端口偏移计算:对以 _PORT 结尾的变量进行数值偏移
- 容器名后缀:对以 _CONTAINER_NAME 结尾的变量添加指定后缀
- 环境隔离:通过不同后缀区分不同环境的容器
.env.tmp 示例(脱敏后):
#===============================中间件部署环境变量===================================
MID_NODE=中间件节点IP
DATA_DIR=./data
######################redis######################
REDIS_PORT=6379
REDIS_PASSWORD=Redis密码
REDIS_CONTAINER_NAME=redis
######################mysql#####################
MYSQL_PORT=3306
MYSQL_PASSWORD=MySQL密码
MYSQL_CONTAINER_NAME=mysql
######################es#####################
ES_PORT=9200
ES_CONTAINER_NAME=es
######################nacos#####################
NACOS_PORT1=8848
NACOS_PORT2=9848
NACOS_CONTAINER_NAME=nacos
######################minio#####################
MINIO_PORT1=MinIO端口1
MINIO_PORT2=MinIO端口2
MINIO_CONTAINER_NAME=minio
MINIO_USER=MinIO用户名
MINIO_PASSWORD=MinIO密码
######################mongo#####################
MONGO_PORT=27017
MONGO_CONTAINER_NAME=mongo
MONGO_INITDB_ROOT_USERNAME=Mongo用户名
MONGO_INITDB_ROOT_PASSWORD=Mongo密码
#===============================普通应用部署环境变量==================================
# 应用部署在机器的信息
APP_NODE=应用节点IP
# 微服务NACOS信息
MICRO_SERVICE_NODE=Nacos节点IP
MICRO_SERVICE_PORT=Nacos端口
MICRO_SERVICE_NAMESPACE=命名空间
MICRO_SERVICE_GROUP=组名
######################服务1######################
服务1_PORT=服务端口
服务1_CONTAINER_NAME=服务容器名
服务1_IMAGE_TAG=镜像标签
######################服务2######################
服务2_PORT=服务端口
服务2_CONTAINER_NAME=服务容器名
服务2_IMAGE_TAG=镜像标签
#=================================================================================
2. 镜像管理
项目提供了 save-images.sh 和 load-images.sh 脚本用于镜像的批量保存和加载:
- save-images.sh:将所有需要的 Docker 镜像保存为 .tar 文件,适用于离线环境部署
- load-images.sh:从 .tar 文件批量加载 Docker 镜像,用于离线环境的镜像恢复
这两个脚本使得在无网络连接的环境中也能完成系统部署,提高了部署的灵活性。
3. 服务编排策略
项目采用分层服务编排策略:
- 基础服务层:包括数据库、缓存、消息队列等基础设施
- 核心服务层:包括网关、认证、业务服务等核心应用
这种分层策略的优势在于:
- 启动顺序可控,避免服务依赖问题
- 便于资源分配和管理
- 支持按需启动和扩展
4. 网络隔离设计
项目通过自定义网络实现服务间通信:
networks:
自定义网络名:
driver: bridge
所有服务都连接到同一个网络,实现服务间的互联互通,同时与宿主机网络隔离,提高安全性。
5. 数据持久化方案
通过 volume 映射实现数据持久化:
volumes:
- ${DATA_DIR}/服务名:/数据目录
这种方式确保容器重启或删除后数据不会丢失。
6. CI/CD 集成
项目通过 Jenkins 实现持续集成和部署,Jenkinsfile 示例:
pipeline {
agent {
label '构建节点标签'
}
parameters {
string(
name: 'BRANCH_NAME',
defaultValue: '默认分支',
description: '分支参数说明'
)
}
stages {
stage("下载代码") {
steps {
script {
// 设置 Git 代理和 SSL 验证
sh 'git config --global http.proxy http://代理地址:端口'
sh 'git config --global http.sslVerify false'
checkout([
$class: 'GitSCM',
branches: [[name: "${BRANCH_NAME}"]],
extensions: [[
$class: 'CloneOption',
depth: 1,
noTags: false,
reference: '',
shallow: true
]],
userRemoteConfigs: [[
credentialsId: "凭证ID",
url: "仓库地址"
]]
])
}
}
}
stage("设置相关环境变量") {
steps {
script {
env.commitId = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
}
stage('构建代码') {
steps {
script {
sh "mvn clean package -U"
}
}
}
stage('构建镜像') {
steps {
script {
// 打包镜像
sh "ls -alh ."
sh "ls -alh 服务目录"
sh "docker rmi -f 镜像仓库/镜像名:${BRANCH_NAME}"
sh "cd 服务目录 && docker build -t 镜像仓库/镜像名:${BRANCH_NAME} -f Dockerfile ./"
}
}
}
stage('启动容器') {
steps {
script {
sh "cd 部署目录 && docker-compose -f docker-compose.app.yaml -p 项目名 up -d --force-recreate --build 服务名"
}
}
}
}
}
该流水线实现了以下功能:
- 代码拉取和环境配置
- 应用构建(Maven)
- Docker 镜像构建
- 服务部署(通过 docker-compose)
相关脚本
pre-start.sh
#!/bin/bash
# ==============================================================================
# 环境变量预处理脚本
# 用于从 .env.tmp 模板生成实际环境变量文件 .env
# 功能:
# 1. 对 _PORT 结尾的变量进行端口偏移计算
# 2. 对 _CONTAINER_NAME 结尾的变量添加后缀
# 3. 支持通过环境变量覆盖默认偏移量和后缀
# ==============================================================================
# 启用严格错误检查模式
# -e: 命令失败时立即退出
# -u: 使用未定义变量时报错
# -o pipefail: 管道中任意命令失败时整个管道失败
set -euo pipefail
# ====================== 配置参数 ======================
# 定义默认端口偏移量(可通过环境变量 OFFSET_PORT 覆盖)
# 示例:OFFSET_PORT=500 ./pre-start.sh
: ${OFFSET_PORT:=0}
# 定义默认容器名后缀(可通过环境变量 SUFFIX 覆盖)
# 注意:变量名疑似应为 SUFFIX,当前保持 SUFFIX 以兼容旧版本
: ${SUFFIX:="-dev"}
# ====================== 文件处理 ======================
# 创建临时文件(原子化操作保障)
# 使用 mktemp 避免文件名冲突,XXXXX 模板生成随机后缀
TMP_FILE=$(mktemp) || { echo "无法创建临时文件"; exit 1; }
# ====================== 核心处理逻辑 ======================
# 使用 awk 进行模板处理,支持以下特性:
# 1. 端口偏移计算(支持直接数值和变量引用)
# 2. 容器名后缀追加(自动处理引号包裹的值)
# 3. 保留原始注释和格式
awk -v offset="$OFFSET_PORT" -v suffix="$SUFFIX" '
# ================= 端口处理模块 =================
# 匹配模式:_PORT 或 _PORT+数字 结尾的变量(如 DB_PORT, REDIS_PORT2)
/^[^=]*_PORT([0-9]+)?=/ {
# 分割变量名和值
split($0, a, "=")
var_name = a[1]
var_value = a[2]
# 数值校验逻辑
if (var_value ~ /^[0-9]+$/ || var_value ~ /^\$\{.*\}$/) {
# 执行算术运算(支持变量引用如 ${BASE_PORT})
new_value = var_value + offset
printf "%s=%s\n", var_name, new_value
} else {
# 非数值内容保持原样(如注释行)
print
}
next # 跳过后续处理
}
# ================= 容器名处理模块 =================
# 匹配模式:_CONTAINER_NAME 结尾的变量
/^[^=]*_CONTAINER_NAME=/ {
# 分割变量名和值
split($0, a, "=")
var_name = a[1]
var_value = a[2]
# 引号处理逻辑
if (var_value ~ /^".*"$/) { # 双引号包裹的值
gsub(/"/, "", var_value) # 去除引号
printf "%s=\"%s%s\"\n", var_name, var_value, suffix
} else if (var_value ~ /^'\''/) { # 单引号包裹的值
gsub(/'\''/, "", var_value) # 去除引号
printf "%s=''%s%s'\''\n", var_name, var_value, suffix
} else { # 无引号包裹的值
printf "%s=%s%s\n", var_name, var_value, suffix
}
next # 跳过后续处理
}
# ================= 默认处理模块 =================
# 保留所有其他内容(包括注释、空行、未匹配变量等)
{ print }
' .env.tmp > "$TMP_FILE"
# ====================== 后处理操作 ======================
# 原子化文件替换(确保操作完整性)
mv -f "$TMP_FILE" .env || { echo "文件替换失败"; exit 1; }
# ====================== 输出结果信息 ======================
cat << EOF
成功生成环境变量文件 .env
配置信息:
- 端口偏移量: +${OFFSET_PORT}
- 容器名后缀: ${SUFFIX}
EOF
# 根据.env中的DATA_DIR变量,创建相关路径:
set -x
# 读取DATA_DIR变量并展开其中的环境变量
data_dir=$(bash -c "source .env && echo \$DATA_DIR")
echo "创建相关路径: $data_dir"
mkdir -p "$data_dir"
# 创建各个服务的数据子目录
mkdir -p "$data_dir"/{redis,mysql,nacos,minio,mongo,elasticsearch}
exit 0
save-images.sh
#!/bin/bash
# 批量保存Docker镜像脚本
# 根据.env文件中的变量和docker-compose文件保存所有需要的镜像
set -euo pipefail
echo "开始保存Docker镜像..."
# 检查Docker是否运行
if ! command -v docker &> /dev/null; then
echo "错误: 未找到Docker命令,请确保Docker已安装并正在运行"
exit 1
fi
# 检查.env文件是否存在
if [ ! -f ".env" ]; then
echo "未找到.env文件,运行pre-start.sh生成..."
./pre-start.sh
fi
# 创建保存镜像的目录
IMAGES_DIR="./images"
mkdir -p "$IMAGES_DIR"
echo "镜像将保存到: $IMAGES_DIR"
# 保存镜像的函数
save_image() {
local image_name=$1
local image_tag=$2
local output_name=$3
full_image_name="${image_name}:${image_tag}"
output_file="${IMAGES_DIR}/${output_name}.tar"
echo "正在保存镜像: $full_image_name -> $output_file"
# 检查镜像是否存在
if docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${full_image_name}$"; then
if docker save "$full_image_name" -o "$output_file"; then
echo "成功保存镜像: $full_image_name"
else
echo "错误: 无法保存镜像 $full_image_name"
fi
else
echo "警告: 镜像 $full_image_name 不存在于本地,跳过保存"
fi
}
load-images.sh
#!/bin/bash
# 批量加载Docker镜像脚本
# 用于加载保存在./images目录下的所有Docker镜像
set -euo pipefail
echo "开始加载Docker镜像..."
# 检查Docker是否运行
if ! command -v docker &> /dev/null; then
echo "错误: 未找到Docker命令,请确保Docker已安装并正在运行"
exit 1
fi
# 检查images目录是否存在
IMAGES_DIR="./images"
if [ ! -d "$IMAGES_DIR" ]; then
echo "错误: 镜像目录 $IMAGES_DIR 不存在"
exit 1
fi
# 检查images目录是否为空
if [ -z "$(ls -A "$IMAGES_DIR")" ]; then
echo "错误: 镜像目录 $IMAGES_DIR 为空"
exit 1
fi
# 加载镜像的函数
load_image() {
local image_file=$1
echo "正在加载镜像: $image_file"
if docker load -i "$image_file"; then
echo "成功加载镜像: $image_file"
else
echo "错误: 无法加载镜像 $image_file"
fi
}
# 遍历images目录下的所有tar文件并加载
echo "=== 开始加载镜像 ==="
for image_file in "$IMAGES_DIR"/*.tar; do
# 检查是否有匹配的文件
if [ -f "$image_file" ]; then
load_image "$image_file"
else
echo "未找到任何.tar镜像文件"
break
fi
done
echo "所有镜像加载完成!"
echo "使用 'docker images' 命令查看已加载的镜像"
总结
基于 Docker Compose 的微服务部署方案具有以下优势:
- 简化部署:通过声明式配置文件定义服务,一条命令即可部署整个系统
- 环境一致性:开发、测试、生产环境使用相同的配置,避免环境差异问题
- 可扩展性:支持水平扩展和垂直扩展,满足不同规模的业务需求
- 可观测性:统一的日志和监控方案,便于问题排查和性能优化
在实际应用中,需要根据业务特点和资源情况合理设计服务架构,选择合适的技术组件,制定完善的部署和运维策略,才能充分发挥微服务架构的优势。
评论区