2.2 多服务协同应用部署


2.2 多服务协同应用部署

本节摘要:多服务协同部署指把相互依赖的多个进程放进同一个 compose 项目,最经典的组合是 Web 应用加数据库。本节给出 Nginx 加 Flask API 加 PostgreSQL 三服务完整示例,解释服务名如何被内置 DNS 解析成容器地址,重点辨析 depends_on 短语法与长语法的语义差异,并给出开发环境覆盖配置的变体。多服务模式是 2.3 到 2.7 所有能力登场的舞台。

本节地图

  1. 能写出 Web 加数据库的完整 compose 文件,并解释每个服务的职责。
  2. 能说明服务名为什么能当主机名使用,以及它在连接串里的写法。
  3. 能区分 depends_on 短语法与长语法的行为差异并正确选用。
  4. 能用 profile 或覆盖文件区分开发与生产配置。
  5. 能定位多服务启动时常见的连接失败问题。

一、为什么要把 Web 和数据库拆开

单服务模式里我们说过"拆开要有收益",Web 加数据库正是收益最明显的一对。浏览器请求打到 Web 应用,Web 应用读写数据库,两者是典型的"一快一稳"组合:Web 层流量波动大,需要频繁更新、随时伸缩;数据库层要稳定、要持久化、要严格控制权限。

拆成两个容器后,每层独立获得三样东西:独立升级——前端加接口只重建 Web 镜像,数据库不动;独立伸缩——压力大时把 Web 扩到三个实例,数据库不用跟着动(能不能扩见 2.7 节);故障隔离——数据库崩了 Web 报错但宿主机其他服务不受牵连,反之亦然。

资源隔离的收益在故障时最直观:数据库连接耗尽、Web 进程卡死,都只影响自己的容器,宿主机的其它项目照常运行。我们把"一容器一进程"当作纪律,nginx 容器里绝不塞应用代码,每层镜像保持最小,出问题时日志边界也清晰。

代价是要管理它们之间的通信。好消息是 Compose 把这件事做成了默认行为:同一项目内的服务,用服务名就能互相访问。

二、完整示例:Nginx + Flask API + PostgreSQL

我们搭一个三层结构:Nginx 做静态入口兼反向代理,Flask 应用提供接口,PostgreSQL 存数据。目录下放 docker-compose.yml,Flask 代码和 Dockerfile 放同级目录。

version: "3.9" services: nginx: image: nginx:1.21 ports: - "8080:80" depends_on: - web web: build: ./web ports: - "5000:5000" depends_on: - db environment: - DATABASE_URL=postgresql://user:password@db:5432/mydb restart: always db: image: postgres:14 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=password - POSTGRES_DB=mydb volumes: - db_data:/var/lib/postgresql/data restart: always volumes: db_data:

Flask 应用只做一件事:连接数据库,查出版本号返回给浏览器。

from flask import Flask import os import psycopg2 app = Flask(__name__) DATABASE_URL = os.environ.get("DATABASE_URL") @app.route("/") def hello_world(): try: conn = psycopg2.connect(DATABASE_URL) cur = conn.cursor() cur.execute("SELECT version();") db_version = cur.fetchone() conn.close() return f"<p>Hello, World! Database version: {db_version}</p>" except Exception as e: return f"<p>Error connecting to database: {e}</p>" if __name__ == "__main__": app.run(debug=True, host="0.0.0.0")

配套的 Dockerfile 与依赖清单:

FROM python:3.9-slim-buster WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]
Flask psycopg2-binary

启动命令带上 --build,先把 Web 镜像构建出来再拉起全部服务:

docker-compose up --build

up 不带 -d 时日志直接滚屏输出,三个服务的日志混在同一终端,开发时方便看整体状态;确认没问题后 Ctrl+C 会停掉所有服务,再换 docker-compose up -d 后台运行。docker-compose ps 查看每个服务的状态与端口映射,谁起来了、谁在重启,一眼可见。

浏览器访问本机 8080 端口,Nginx 把请求转给 Flask,页面会打出 PostgreSQL 的版本号。这一行输出意味着三条网络通路全部打通:宿主机到 Nginx、Nginx 到 Flask、Flask 到数据库。

三、服务名互访的原理

关键在连接串这一行:postgresql://user:password@db:5432/mydb。注意主机名部分是 db,不是 IP,不是 localhost。Compose 为每个项目创建默认网络,所有服务加入后,Docker 内置 DNS 会把服务名 db 解析成对应容器的 IP。容器 IP 是动态分配的,重启一次可能就变了,但服务名永远稳定——这正是"用名字不用 IP"的原因。

同理,nginx 服务里可以用 web 这个主机名访问 Flask,web 可以访问 db。应用代码里写死服务名即可,换机器、换环境都不用改。

服务名解析还有一个隐藏能力:同一服务扩展出多个实例后,内置 DNS 会把多个 IP 同时注册在同一个服务名下,调用方轮询访问,天然获得一层简单的负载均衡。细节见 2.7 节,这里先记住结论:服务名是稳定的逻辑入口,实例数量怎么变,对调用方透明。

端口要不要暴露,取决于谁访问。 用户从宿主机访问 nginx,所以 8080 映射出去;web 只被 nginx 访问,5000 可以留在网络内部;db 只被 web 访问,5432 干脆不映射。多服务场景的默认原则是"最小暴露",每多一个宿主机端口就多一个攻击面,这是 2.4 节网络隔离的起点。

四、depends_on:短语法与长语法

示例里两个 depends_on 都是短语法:depends_on: - db,含义是"启动顺序上 db 先于 web"。Compose 会先创建并启动 db,再启动 web。但注意,它只保证 db 的容器起来了,不保证 PostgreSQL 已经能接受连接——数据库初始化可能要几秒,web 抢先连接就会失败。

长语法能补上这个缺口:

services: web: depends_on: db: condition: service_healthy
对比项 短语法 depends_on: - db 长语法 condition: service_healthy
控制内容 启动顺序 启动顺序加就绪条件
依赖方何时启动 db 容器创建即启动 db 健康检查通过才启动
需要配套 依赖服务必须配置 healthcheck
适用阶段 开发期够用 生产环境推荐

短语法适合开发期,起得快、配置少;长语法适合生产,代价是 db 必须写 healthcheck,具体写法在 2.6 节展开。

长语法还有一个额外收益:依赖关系带条件后,配置即文档,新同事看文件就能明白"db 健康之前 web 不会起来"。开发期图省事用短语法没问题,但要在注释里标记"上线前换成条件依赖"——我们吃过不少忘记换的亏。

五、变体:开发环境覆盖配置

生产环境不挂源码目录,开发环境要热更新,同一份文件怎么兼顾?用覆盖文件或 profile。我们常用 profile 方案:基础文件保持生产形态,单独一份开发覆盖文件追加卷挂载和调试开关。

services: web: volumes: - ./web:/app environment: - FLASK_DEBUG=1
docker-compose --profile dev up --build

FLASK_DEBUG=1 打开 Flask 调试模式,./web:/app 把宿主机源码挂进容器,改代码立刻生效,不用重建镜像。生产部署不带 profile,走的就是文件里默认的生产配置。

生产与开发的差异不止调试开关:开发要源码热更新,生产要固定版本镜像;开发可以接受容器内直接跑源码,生产要的是可复现的构建产物。把这些差异全部收进 profile,基础文件就不需要为环境做任何妥协,docker compose config 一渲染,两套配置的差异清清楚楚。

profile 之外,多环境还可以用多个 compose 文件叠加:docker-compose -f docker-compose.yml -f docker-compose.prod.yml up,后列的文件覆盖前列的同名配置,适合环境差异较大的项目,比如生产要挂外部网络、要开资源限制这类 profile 表达不了的改动。

六、多服务常见的坑

连接串写错主机名。 最常见的是写成 localhost。容器里的 localhost 是容器自己,数据库在另一个容器里,永远连不上。服务名 db 才是正确的网络地址。

端口冲突。 示例里 db 没映射宿主机端口,这是有意为之——只有 Web 需要对外,数据库端口暴露到宿主机等于多一个攻击面。开发时想看数据库,临时加一行 "5432:5432" 即可,生产别留。

depends_on 误当就绪。 短语法启动后 web 立即连库,大概率赶上初始化窗口报错。要么应用层加重试,要么升级长语法加健康检查。

环境变量硬编码进镜像。 密码写死在 Dockerfile 或代码里,换环境就得改镜像。连接串、账号密码一律走 environment 或 .env,这是 2.5 节的原则。

生产环境还挂着开发配置。 覆盖文件只在带 profile 时生效,但有人把 FLASK_DEBUG=1 直接写进基础文件,生产也跟着调试模式跑。调试开关、源码挂载这类开发配置一律放 profile 或覆盖文件,基础文件保持生产形态。

⚠️ docker-compose updocker-compose down 的清理范围是整个项目:down 会把三个服务的容器连同默认网络一起删掉,但命名卷 db_data 默认保留,数据不会丢。

💡 想确认服务间是否真的通了,进容器里试一把最直接:docker exec -it <web容器> sh,然后在容器内 getent hosts db,能看到解析出的 IP 就说明 DNS 链路正常。

七、排查手册:三个真实案例

案例一:web 容器启动后立刻退出。 现象是 up 之后 docker-compose ps 里看不到 web。第一步 docker-compose logs web 看退出原因,九成落在连接串上:密码错、库名错、主机名拼错。注意容器日志只显示应用输出,数据库侧的问题要看 docker-compose logs db,两边对照就能分清是"连不上"还是"认证失败"。还有一种情况是启动脚本本身报错,比如应用依赖的初始化 SQL 失败,日志会给出具体堆栈,顺着堆栈往上查即可。

案例二:Nginx 返回 502。 Nginx 起来了但转不到后端。进 nginx 容器实测:docker exec -it <nginx容器> sh,在容器里 `curl 「相关地址请参见官方文档」 Nginx 配置;不通就回到案例一的路径继续查。502 的大头是上游地址写错,以及后端还没就绪。nginx 的 error.log 会记录每次失败转发的具体原因,是排查 502 的第二手资料。

案例三:重启后数据库回到初始状态。 数据消失先查 compose 文件里 db 的 volumes 段还在不在,再回忆最近有没有执行过带 -v 的 down。卷删除不可逆,这正是 2.3 节反复强调备份的原因。没有备份时,先 docker volume ls 找找有没有没被清理的孤儿卷,也许还能抢救。

八、更多变体

纯镜像组合。 很多成熟应用官方直接给了组合镜像,比如 WordPress 配 MySQL,compose 里清一色 image 加 environment,一行 build 都没有,十分钟起一个完整站点。这类"镜像组合型"部署是模板库的主力形态,第三章会大量出现。

数据库初始化脚本。 PostgreSQL 与 MySQL 镜像首次启动时会按文件名顺序执行挂载目录里的初始化脚本:

services: db: image: postgres:14 volumes: - db_data:/var/lib/postgresql/data - ./init:/docker-entrypoint-initdb.d:ro

脚本只在数据目录为空时执行一次,数据卷已有内容后不会再跑,很适合初始化表结构与测试数据。注意脚本执行顺序依赖文件名排序,需要先后关系时在文件名前加数字序号。

服务职责速查表:

服务 镜像来源 对外端口 数据 依赖
nginx 公共镜像 8080 web
web 本地构建 5000 db
db 公共镜像 不暴露 db_data 卷

九、规模再大的方向

单机 Compose 的边界是宿主机资源。需要更高可用性与跨机伸缩时,升级路径有两条:Docker Swarm 把同样的 compose 概念搬到多机,配置几乎无缝;Kubernetes 功能全但学习曲线陡。Swarm 的部署文件与 compose 高度兼容,docker stack deploy 直接消费 compose 文件,迁移成本主要在运维侧。CI/CD 管道把构建、测试、部署串起来,配合监控与日志体系,多服务应用才谈得上生产级。这些方向各有专章,先把单机跑稳,再谈集群。

要点串联

  • 拆分的收益是独立:升级、伸缩、故障隔离,三层各管各的。
  • 服务名就是网络地址:连接串写 db 不写 localhost,内置 DNS 完成解析。
  • 短语法管顺序:depends_on 短语法只保证先启动,不保证就绪。
  • 长语法管就绪:condition: service_healthy 等健康检查通过才放行,需要配套 healthcheck。
  • 端口按需暴露:只有对外服务映射宿主机端口,数据库留在内网。
  • 开发环境用覆盖:profile 或覆盖文件注入卷挂载与调试开关,生产配置保持干净。
  • 重启策略与卷别漏:restart: always 加 db_data 命名卷,是生产级的最小配置。

Web 与数据库协同跑通后,下一个问题立刻浮现:数据库的数据存在哪、容器删了还在不在——这正是 2.3 节持久化存储要解决的。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U