背景信息
### 开发需求
1. 单服务器的多服务管理
多服务器的集群管理可参考博主 docker swarm 的技术分享[《基于 docker swarm 和 NVIDIA MIG 部署并行 AI 推理服务》](https://blog.csdn.net/Tuna____/article/details/140575040?spm=1001.2014.3001.5502)
2. 根据服务请求量和服务器规模的匹配程度,多服务器集群的性能对于目前来说过剩,故有了本研究的分享。
### 核心概念
1. docker compose 运行时,默认会创建独立于主机网络的桥接网络(bridge network),用于多个容器间的通信,同时提供与未连接到该桥接网络的容器的隔离。因此其非常适合接入如 Nginx 给 docker 的网络赋能,如本文将要示例的单服务器的多个 AI 推理服务的负载均衡和基于 compose 的服务伸缩。
2. docker compose 的开发宗旨之一就是面向生产的单主机部署,比如程序服务或计算资源的隔离性和拓展性,停机恢复后的服务自动重启等功能。
3. 在 docker compose 中,服务(Service)可以理解为容器的功能和相关计算机资源的集合。
### Docker 版本
1. Docker Compose version v2.24.2
2. Docker version 25.0.1, build 29cf629
### 文档信息
作者:penguido
初稿完稿日期:2024.10.09
具体实现
### 示例的文件结构
本文重点用到 `nginx/nginx.conf` 和 `compose_nginx.yaml` 两个配置文件。
![示例文件结构](https://i-blog.csdnimg.cn/direct/807ca9cecda848bd974bceab45bfed4a.jpeg)
### 基础应用
1. 准备后端服务的镜像
由于不是本文的重点,故省略。后文以制作好的AI服务的镜像为例,其服务端口是 `8000`
2. `Nginx` 镜像选型
由于本文重点需要其负载均衡的功能,故选用 `nginx:stable-alpine` 镜像
3. 准备 docker compose 的配置文件 `compose_nginx.yaml`
本 compose 配置文件和本文重点相关的说明如下,详情请参见官网 <https://docs.docker.com/reference/compose-file/services/>
1. 配置了两个服务(services),分别叫 `api` 和 `nginx`
2. `api` 服务即本例通过 docker compose 用 `Dockerfile` 制作的AI推理服务镜像。
在配置文件中为其运行的容器配置了相关的 GPU 资源和监控机制,如停机恢复的自启动和文件变动的热重启。
另外该服务的单元测试代码通过绑定挂载(bind mounts)的方式实现和正式服务的分隔管理。🧐PS:笔者后续计划用上`profiles`的属性进行管理。
3. `api` 服务的 `deploy.replicas` 属性配置服务对应容器的初始副本数量
4. `nginx` 服务的 `depends_on` 属性表示在 `api` 服务启动好之后再启动 `nginx` 服务,关闭服务(services)时先关 `nginx` 再关 `api`
5. `nginx` 服务的 `command` 属性表示在 `nginx` 服务对应的容器启动时所执行的命令,其作用是配置和启动 nginx
```yaml
name: detect_nginx_dev
services:
api:
build:
context: .
dockerfile: Dockerfile
restart: always
# 代码或模型变动后的热重启
develop:
watch:
- action: sync+restart
path: .
target: /app
ignore:
- data/
- Dockerfile
- action: rebuild
path: ./Dockerfile
# 支持使用 GPU
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
driver: nvidia
count: all
replicas: 3
volumes:
- "./unit_test:/app/unit_test"
nginx:
image: nginx:stable-alpine
restart: always
ports:
- '8000:80'
depends_on:
- api
volumes:
- ./nginx/nginx.conf:/tmp/nginx.conf
environment:
- TZ=Asia/Shanghai
command: /bin/sh -c "envsubst < /tmp/nginx.conf > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
```
4. 准备 Nginx 配置文件 `nginx.conf`
由于本文 AI 推理服务完成一个请求的时间较长(处理时长主要分布在 20s\~90s),故选用 `least_conn` "最少连接"的负载均衡算法。另外,所有服务副本会被 docker 分配不同的 IP 地址,而共用同一个服务名,如 compose 配置文件配置的 `api`,Nginx 会解析出服务相应的 IP 地址。
若 compose 配置有变化而重新启动,则服务对应容器在 docker 桥接网络中的 IP 地址也会变化,故建议尽可能用容器对应的服务名去访问容器。
Note ❗ 如果负载均衡算法选用默认的轮询(Round-Robin),则客户端请求会被依次分发给不同的后端服务。如果后端服务器能够成功响应,Nginx 会认为该服务器是健康的,并继续向其发送请求,所以在本文示例的后端服务会立即响应请求状态,且不同请求的处理时长明显不同的情况下,弃用该算法。
```plaintext
upstream backend {
least_conn;
server api:8000;
}
server {
listen 80;
location / {
proxy_pass http://backend/;
}
}
```
🧐PS:读者可通过
1. `docker inspect [OPTIONS] NAME|ID [NAME|ID...]` 查看容器的详细信息,如网络相关的 IP。
2. `docker exec <nginx容器名|ID> nginx -T` 查看 nginx 的详细信息,如配置文件。
5. 服务启动
`docker compose -f compose_nginx.yaml up -d && docker compose -f compose_nginx.yaml logs -f`
🧐PS: docker compose 默认去找的配置文件名是 `compose.yaml`,所以如果读者的 `compose` 配置文件同名则可以不用 compose 后面的 `-f` 选项
### 服务伸缩
1. `docker compose scale [SERVICE=REPLICAS...]`
1. 指定好服务名 `SERVICE` 如本例中的 `api`
2. 指定好 `REPLICAS` 即服务伸缩的目标数量
2. `docker compose -f nginx_compose.yaml restart --no-deps nginx`
* `--no-deps` 表示不重启 `nginx_compose.yaml` 中配置的 nginx 服务的依赖服务
* 如果伸缩后不重启 nginx 服务,则新增的服务 IP 不会被 nginx 分布请求;缩减的服务会因找不到原来缓存过的 IP 而报错,从而传递到存活的服务 IP
* 另外,商业版的 Nginx 支持动态配置组,有条件的读者可以试验是否不用重启 nginx 容器
效果演示
读者通过本文基础应用之后,客户端请求服务器 IP 和 8000
端口对应的服务路径,则会得到类似如下图所示的日志信息。
客户端发送请求到宿主机后,根据 compose 配置文件,宿主机的 8000
端口会映射到 docker 网络中的 80
端口,Nginx 在 docker 网络中监听 80
端口,并把请求根据负载均衡算法,路由到 docker 网络中对应的后端服务,即 api
服务的某个容器的 IP 地址和 docker 网络中的 8000
端口。
api
服务的日志由笔者封装的镜像容器呈现,nginx
服务的日志由相关大佬封装的镜像容器呈现。
图例最左边如 api-1
和 nginx-1
即容器对应的副本编号,其通常由 compose 自动生成。
bash
api-2 | INFO: 172.23.0.5:48350 - "POST /detect/xxx HTTP/1.0" 200 OK
api-2 | [10.08.2024 17:02:54] server INFO: id: 866 开始下载视频: xxx
nginx-1 | 10.172.104.96 - - [08/Oct/2024:17:02:55 +0800] "POST /detect/xxx HTTP/1.1" 200 53 "-" "python-requests/2.31.0" "-"
api-2 | [10.08.2024 17:02:54] server INFO: xxx 正在处理中
api-2 | [10.08.2024 17:02:54] server_utils.timer DEBUG: worker函数耗时: 0.0005984306335449219s
api-1 | INFO: 172.23.0.5:38048 - "POST /detect/xxx HTTP/1.0" 200 OK
api-1 | [10.08.2024 17:02:55] server INFO: 推理 API 收到请求参数: xxx
nginx-1 | 10.172.104.96 - - [08/Oct/2024:17:02:55 +0800] "POST /detect/xxx HTTP/1.1" 200 53 "-" "python-requests/2.31.0" "-"
api-1 | [10.08.2024 17:02:55] server INFO: id: 864 开始下载视频: xxx
api-1 | [10.08.2024 17:02:55] server INFO: xxx 正在处理中
api-1 | [10.08.2024 17:02:55] server_utils.timer DEBUG: worker函数耗时: 0.00019598007202148438s
api-3 | [10.08.2024 17:02:55] server INFO: 推理 API 收到请求参数: xxx
🧐PS:后端服务采用不同 IP 却相同端口和相同 IP 不同端口有什么区别?(由通义千问 2.5 生成和笔者修改)
-
结论
选择哪种方式取决于具体的应用场景、网络架构需求以及组织的具体要求。如果强调的是服务的独立性和可扩展性,那么不同 IP 但相同端口可能是更好的选择;如果考虑的是资源效率和配置简便性,那么相同 IP 但不同端口可能更适合。在实际应用中,还需要考虑到网络环境、安全因素和服务本身的特性来做出最合适的选择。
-
不同 IP 但相同端口
特点:
- 每个后端服务都有自己的独立 IP 地址。
- 所有服务都监听相同的端口号(例如,都是 80 或 443)。
优势:
- 水平扩展性:可以很容易地在不同的物理或虚拟服务器上部署服务,这有助于分散负载并提高系统的可用性和容错性。
- 隔离性:每个服务运行在独立的机器上,可以减少单一故障点的影响。
- 易于管理:如果每个服务有自己的 IP 地址,那么在网络层面管理和区分各个服务会更加直接。
劣势:
- 资源消耗:可能需要更多的网络接口或 IP 地址,这可能会增加成本,尤其是在公有云环境中,但 docker 网络避免了该劣势。
- 复杂度:当涉及到防火墙规则、安全组或其他网络策略时,需要为每个 IP 地址单独配置。
-
相同 IP 但不同端口
特点:
- 后端服务共享同一个 IP 地址。
- 每个服务监听不同的端口号。
优势:
- 节省资源:只需要一个 IP 地址就可以支持多个服务,这对于 IP 地址有限的情况是有利的。
- 简化配置:对于防火墙和其他网络设备来说,配置规则可能会更简单,因为它们只需针对单个 IP 进行设置。
劣势:
- 可维护性:随着服务数量的增加,端口冲突的风险也会增加,同时管理大量端口可能变得复杂。
- 安全性:如果某个服务存在安全漏洞,攻击者可能会尝试通过其他端口访问同一 IP 上的其他服务。
- 灵活性:在同一台机器上运行多个服务可能会限制系统在面对硬件故障时的弹性。
参考链接
- https://docs.docker.com/compose/how-tos/networking/
- https://docs.docker.com/reference/compose-file/services/
- https://nginx.org/en/docs/http/ngx_http_upstream_module.html
- https://nginx.org/en/docs/http/load_balancing.html#nginx_load_balancing_methods
- https://github.com/docker/awesome-compose
- https://github.com/docker/awesome-compose/tree/master/nginx-wsgi-flask
- https://medium.com/@aedemirsen/load-balancing-with-docker-compose-and-nginx-b9077696f624
- https://docs.docker.com/compose/intro/features-uses/