1. 项目概述与实验目标
1.1 核心需求解析
这是一个经典的容器化入门实验:用 Flask 写一个 Web 应用,每次访问首页时通过 Redis 的自增命令记录访问次数并在页面上展示。整个服务通过 docker-compose 一键编排启动,Flask 容器和 Redis 容器各自独立运行,通过 Docker 内部网络互通。
别看这个实验规模不大,它几乎涵盖了容器化部署的核心要素:多服务编排、跨容器通信、数据持久化、依赖顺序控制、故障排查。这段时间我在本地搭这套环境时踩了不少坑,从 Docker Desktop 启动失败到容器间网络不通,再到 Redis 数据丢失,一路排查下来收获很多。这篇博文把完整过程和调优细节整理出来,涉及的工具和思路同样适用于其他 Web 应用容器化项目。
先说结论:这个实验非常适合三类人来动手做。一类是刚接触 Docker、想搞清楚 compose 到底怎么组织多容器的人;一类是已经能跑单个容器、但没搞明白容器间如何通信和配置依赖关系的人;还有一类是准备把自己的 Flask 应用容器化、但不确定 Redis 连接、数据持久化这些点怎么处理的人。做完这个实验,你至少能回答三个问题:compose 文件里每个字段干什么用、两个容器之间怎么互相访问、容器重启后数据去了哪里。
1.2 方案选型考量
为什么选 Flask + Redis 而不是 Flask + MySQL?因为计数访问这个场景本质上是高频读写的键值操作,Redis 的INCR命令是单线程原子操作,天然适合计数器场景。MySQL 当然也能做,但需要建表、写 SQL、维护连接,对实验来说负担太重。Redis 的另一个优势是部署轻量,官方镜像几百 MB 就能跑起来,适合实验环境。
为什么非要 docker-compose 而不是两个docker run分别启动?compose 的价值在于用声明式配置取代命令式操作。两个容器分开跑不是不行,但你得手动创建自定义网络、手动指定容器 IP 或者用--link参数(已废弃),还要记住启动顺序。compose 把这些固化到文件里,一条命令拉起全部服务,停止时也能按依赖关系逆序清理。对于团队协作来说,这份 YAML 文件就是环境部署的唯一真相来源,换台机器docker-compose up -d就复现了。
顺便说一个容易混淆的点:现在 Docker 官方推荐用docker compose(带空格,V2 插件)而不是docker-compose(带横线,V1 独立二进制)。但网上大量教程和文档还在用 V1 的写法,而且某些 CI 环境里依然只有 V1。这篇实验我统一使用docker-compose命令,因为兼容性更好、网上资料也最丰富,如果你的环境装的是新版 Docker Desktop,它内置了 compose 插件,直接执行即可。
2. 环境准备与基础服务搭建
2.1 Docker 环境安装与验证
这里只说 Linux 和 Windows 两套主流场景。Linux 环境安装 Docker Engine,不同发行版的差异主要在软件源配置和包管理器上。我线上服务器用的 Ubuntu,习惯直接走官方脚本:
curl -fsSL https://get.docker.com | sh systemctl enable --now docker如果你对官方脚本有顾虑,也可以走包管理器:添加 Docker 官方 GPG key、配置 apt 源,然后apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin。这里有个最容易踩的坑——很多教程只让你装docker-ce,结果发现 compose 命令不存在,就是因为少了docker-compose-plugin这个包。
Windows 环境需要装 Docker Desktop。我一开始装完后启动一直失败,报错信息是virtualization support not detected或者Docker Desktop failed to start because virtualisation support is disabled,这通常集中在几个原因上:WSL2 没启用、BIOS 里虚拟化没打开、或者 Hyper-V 没有开启。排查顺序建议这样来:
- 在 Windows 功能里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”,重启。
- 以管理员身份执行
bcdedit /set hypervisorlaunchtype auto,再重启。 - 去 BIOS 确认 Intel VT-x 或 AMD-V 处于 Enabled 状态。
- 执行
wsl --set-default-version 2,确认 WSL 默认版本是 2。
这套组合拳打完,Docker Desktop 基本就能起来了。验证安装是否成功,用一条命令:
docker version docker compose version注意区分:能显示 Client 和 Server 两段信息才算真正可用,如果只有 Client 没有 Server,说明守护进程没起来。
2.2 Redis 镜像与 Flask 基础镜像选择
Redis 镜像用官方redis镜像就够了,不要用redis:alpine之外的奇怪变体。版本选择上,redis:7和redis:6都是稳定选择,实验环境直接redis:7-alpine可以减小拉取体积。对于 Linux 内核版本较旧的环境要注意,Redis 7 在某些老内核上可能遇到 IO 线程相关问题,保守起见用redis:6.2更稳妥。
Flask 镜像的选型上有个思路值得说一下。你可以用python:3.11-slim作为基础镜像自己装 Flask,也可以直接用现成的python:3.11-alpine。我实际用的是前者,因为 alpine 虽然小,但在编译某些 Python 依赖时需要装gcc musl-dev这类编译工具,Python 基础镜像是 Debian 系的,apt install装构建依赖更容易。
还有一个容易忽视的点:镜像 tag 一定要固定。用latest标签图省事的后果是,某天团队里有人pull到一个新版本,Redis 或 Python 版本变了,整个环境行为就不可预测了。生产环境锁 tag,实验环境哪怕不锁,心里也要有数。
3. 核心服务实现与细节打磨
3.1 Flask 应用代码编写
应用逻辑本身很简单:建立 Redis 连接、在路由中执行INCR命令、获取当前计数并返回页面。但“简单”不等于“随便写”,有几个细节直接影响后续容器化部署的稳定性。
先看基础版本的app.py:
import os import redis from flask import Flask app = Flask(__name__) # 从环境变量读取 Redis 连接信息,这是容器化部署的关键 redis_host = os.getenv("REDIS_HOST", "localhost") redis_port = int(os.getenv("REDIS_PORT", "6379")) # 创建 Redis 连接 r = redis.Redis(host=redis_host, port=redis_port, db=0, decode_responses=True) @app.route("/") def index(): count = r.incr("page_visits") return f"Total visits: {count}" if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)这里有两个关键点。第一,host="0.0.0.0"是必须的。默认 Flask 只监听 127.0.0.1,在容器里这样写意味着外部根本访问不到。很多新手容器启动成功但访问不了,排查半天发现是这里的问题。第二,Redis 地址不能硬编码成localhost。因为 Flask 容器和 Redis 容器是独立的两个容器,localhost指向的是 Flask 容器自身,这个地方必须通过环境变量注入,compose 文件里会相应配置。
第二个版本加入连接池,这是生产环境更合理的做法:
import os import redis from flask import Flask app = Flask(__name__) redis_host = os.getenv("REDIS_HOST", "localhost") redis_port = int(os.getenv("REDIS_PORT", "6379")) # 连接池配置 pool = redis.ConnectionPool( host=redis_host, port=redis_port, db=0, max_connections=20, decode_responses=True ) r = redis.Redis(connection_pool=pool)为什么要单独讲连接池?Flask 默认是多线程处理请求的,如果每个请求都创建新的 Redis 连接,高并发下会大量产生 TCP 连接,Redis 端的文件描述符会很快耗尽,响应也跟着变慢。连接池复用一定数量的连接,性能提升非常明显。但要注意max_connections不是越大越好——每个连接在 Redis 服务端都要占用内存,理论上单机几万个连接没问题,但实验项目开到 20 就够了。
3.2 Redis 计数器实现原理
访问计数用 Redis 的INCR命令,这条命令的语义是对 key 的值做原子加 1 操作。为什么强调“原子”?因为如果多个请求同时到达,两个线程同时读到一个值N,各自加 1 后写回,结果可能还是N+1而不是N+2。Redis 是单线程执行命令的,INCR在执行时不会被其他命令打断,天然避免了竞态问题。
存储结构上,计数器的 key 就是一个普通的 Redis 字符串。如果之前 key 不存在,INCR会将其初始化为 0,再执行加 1 操作。这里涉及到一个值得考虑的点:key 命名规范。用page_visits作为 key 名在实验环境没问题,但真实项目建议用命名空间前缀,比如stat:page:visits,方便后续和其他统计 key 区分。
如果要看当前计数而不增加它,用GET:
current = r.get("page_visits")这里有个类型陷阱。当 Redis 设置了decode_responses=True时,GET返回的是字符串类型,比如"42",不是整数。做数值比较或计算时记得转换。我在实验里吃过这个亏:直接把GET结果和整数比较,Python 里字符串和整数比较不会报错,但结果永远是 False,排查了半天才发现类型问题。
4. 编写 docker-compose 编排文件与启动部署
4.1 工程目录结构与 requirements 清单
开始编写 compose 文件之前,先整理好项目目录结构。一个清晰的目录结构能让后续维护省很多事,也方便别人看懂项目:
flask-redis-counter/ ├── docker-compose.yml ├── app/ │ ├── Dockerfile │ ├── requirements.txt │ └── app.pyrequirements.txt是 Python 依赖清单,这个实验只需要两个依赖:
flask==3.0.3 redis==5.0.7锁版本号是必须的。如果写成flask>=3.0,哪天 Flask 出了新版本把接口改动了,应用很可能起不来。锁版本看似保守,实则是可控性的保障,部署环境的确定性永远优先于新功能的追逐。
Dockerfile这样写:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 5000 CMD ["python", "app.py"]--no-cache-dir参数可以显著减小镜像体积,尤其在使用 slim 基础镜像时,不清理 pip 缓存会让镜像膨胀不少。WORKDIR /app设定了容器内的工作目录,后续执行命令都以这里为基准。如果后续要加非 Python 依赖,改造时还要注意一个顺序问题:把依赖安装放在源码复制前面,因为 Docker 构建是有层缓存的,requirements.txt没变时,pip install这层可以复用缓存,加快多次构建速度。
4.2 docker-compose.yml 细节逐行解析
compose 文件是整个实验的核心配置,我用 V3 版本来写:
version: "3.8" services: redis: image: redis:7-alpine container_name: flask-redis-counter-redis restart: unless-stopped ports: - "6379:6379" volumes: - redis_data:/data command: ["redis-server", "--appendonly", "yes"] web: build: ./app container_name: flask-redis-counter-web restart: unless-stopped ports: - "5000:5000" environment: - REDIS_HOST=redis - REDIS_PORT=6379 depends_on: - redis volumes: redis_data:每个字段都不是随便写的,逐个来说:
version: "3.8"是 compose 文件规范版本。现在 Docker 官方对新项目更推荐省略version字段直接写结构,因为新版本 compose 已经不再依赖这个字段。如果你用的是 Docker Desktop 自带的 compose V2,省略version完全没问题。但考虑到网上 V1 资料还很多,保留version: "3.8"的兼容性更好。
container_name给容器固定一个名字。不指定的话 compose 会自动生成项目名_服务名_序号这种格式,排查日志时不好记。固定名字的代价是不能用同一个 compose 文件在同一台机器上启动多套环境,但实验场景完全够用。
restart: unless-stopped设置了容器的重启策略。容器进程崩溃或服务器重启后,守护进程会自动拉起容器。对于依赖 Redis 的场景,Redis 容器先启动、Flask 后启动,这套策略在后面會讲一个关键坑。
ports端口映射是重点。"6379:6379"表示把容器内的 6379 端口映射到宿主机的 6379 端口。生产环境要慎重——数据无加密时,库被直接暴露在宿主机网络上,扫描到端口就能连上来。实验环境图方便可以这样做,但我更建议 Redis 端口只做容器间通信,不映射到宿主机。改动也很简单,ports那行注释掉或删掉,web 容器通过 compose 内部网络照样能访问 Redis。Flask 的 5000 端口必须映射出来,否则外部访问不到。
volumes数据卷挂载是持久化的关键。Redis 容器用volumes把宿主机上的redis_data卷挂载到容器内/data目录。Redis 的 RDB 快照和 AOF 日志默认写入/data目录,只要这个目录落盘到持久化存储,容器删了重建数据都在。compose 文件末尾的顶级volumes声明,是告诉 Docker“我要创建一个命名卷”,命名卷的优点是 Docker 自动管理存储位置,不需要你手动指定宿主机路径。
command: ["redis-server", "--appendonly", "yes"]覆盖了镜像默认的启动命令。这里做了两件事:以redis-server启动,并且开启 AOF 持久化。官方镜像默认只开了 RDB 快照,RDB 的缺点是两次快照之间的数据可能丢失,AOF 则记录每一次写操作。对于计数器这种高频繁的小写入,AOF 更合适,即使容器重启或崩溃,最多丢一两个计数。
environment环境变量是容器间通信的关键联系。REDIS_HOST=redis的值为 Redis 服务的服务名,compose 会自动创建一个默认网络,服务名在 DNS 层面对应到容器 IP。所以 Flask 应用代码里的os.getenv("REDIS_HOST", "localhost")会拿到redis这个值,再通过这个值解析到 Redis 容器的内网 IP。
depends_on指定依赖关系。compose 会先启动 Redis、再启动 Flask。然而这个字段只保证启动顺序,不保证 Redis 已经处于可服务状态——Redis 容器启动到真正能接受连接,中间还有几秒初始化时间。如果你实验时 Flask 一直报Connection refused,大概率不是 Redis 没启动,而是 Redis 还没准备好。
4.3 解决依赖启动顺序问题
针对depends_on的局限,有几种常见解法。最轻量的是在 Flask 启动脚本里加一个“等待 Redis 就绪”的循环逻辑。在app.py中增加一段启动检查:
import time import redis def wait_for_redis(client, timeout=30): start = time.time() while time.time() - start < timeout: try: client.ping() return True except redis.ConnectionError: time.sleep(1) return False然后在初始化代码里调用:
if not wait_for_redis(r): raise RuntimeError("Redis connection failed after timeout")对实验项目来说这个方案简单直接——应用启动进程不会被立即 kill,而是等到依赖可用再继续。更完整的做法是用healthcheck命令配合 compose 的depends_on条件判断:
services: redis: ... healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 web: ... depends_on: redis: condition: service_healthy这种方式下,compose 会定期执行redis-cli ping,只有返回PONG才认为健康,web 服务才会开始启动。生产环境推荐后者,实验环境用启动等待逻辑就足够了。
4.4 启动、验证与日志跟踪
文件都准备好后,在项目的根目录执行:
docker-compose up -d-d表示后台运行。建议第一次启动时去掉-d直接前台启动:
docker-compose up前台启动能看到所有容器的实时日志输出,方便直观确认两个容器都正常起来了。确认没问题后再Ctrl+C停掉,重新执行docker-compose up -d转后台。
启动过程中需要关注的几种状态:
Creating network "flask-redis-counter_default":说明 compose 自动创建了默认网络,两个容器都会连进来。Creating volume "flask-redis-counter_redis_data":命名卷创建成功。Creating flask-redis-counter-redis和Creating flask-redis-counter-web:顺序应该是 Redis 先、web 后。
容器都启动后,用docker-compose ps查看状态,两行都显示Up就算成功。然后用浏览器访问http://localhost:5000,每次刷新页面,访问次数都会递增。
验证 Redis 里的实际数据,用宿主机上的redis-cli连接:如果在宿主机装了 redis-tools,直接redis-cli GET page_visits就可以查看。没装的话进容器操作:
docker exec -it flask-redis-counter-redis redis-cli GET page_visits docker exec -it flask-redis-counter-redis redis-cli INCR page_visits docker exec -it flask-redis-counter-redis redis-cli TTL page_visits用 Redis Desktop Manager 可视化查看也可以。连接时注意 Host 填127.0.0.1(因为做了端口映射),Port 填6379。如果连不上,先排查防火墙是否放行了 6379 端口,或者 Redis 是否真的在监听。
5. 性能调优与实际踩坑排查
5.1 Redis 持久化策略与数据可靠性调试
这套实验里最容易翻车的就是数据持久化。我前面提到容器删除或重建后,计数归零的问题,归零的根源在于Redis 默认启动配置不满足你的数据安全预期。
先理解 RDB 和 AOF 的区别。RDB 是定时把内存数据全量快照到磁盘,默认配置是“如果 900 秒内至少有 1 次写操作,或者 300 秒内至少有 10 次写操作,或者 60 秒内至少有 10000 次写操作,就触发快照”。这种策略下,如果 Redis 突然崩溃,最后一次快照之后的写入全部丢失,对于访问计数场景,你损失的是“最近一小时甚至几分钟的访问数据”。
AOF 则不同——每个写命令先追加到日志文件,Redis 重启时按日志重放恢复数据。AOF 有三个刷盘级别:always每命令都刷盘,性能最差但数据最安全;everysec每秒刷一次,性能和数据安全比较均衡;no交给操作系统决定刷盘时机,数据可能丢最多。我在 compose 文件里用的--appendonly yes,默认策略就是everysec,丢数据的窗口控制在 1 秒内,对计数场景完全够用。
但还有一个细节值得展开:持久化策略不只是开不开的问题,还涉及触发频率。如果访问量极高,每分钟上万次写入,RDB 快照策略里的“60 秒内 10000 次写操作”条件会频繁触发,每次快照都 fork 一个子进程把全量内存写到硬盘,性能会有明显波动。AOF 的日志文件会持续增长,容器长期运行后要注意文件膨胀问题。Redis 的BGREWRITEAOF命令可以重写日志文件,压缩掉中间废弃的写命令。更省心的做法是在 compose 命令里覆盖 Redis 配置:
command: > redis-server --appendonly yes --appendfsync everysec --auto-aof-rewrite-percentage 100 --auto-aof-rewrite-min-size 64mb这样配置完后,即使 AOF 日志里充满了大量INCR page_visits命令,重写发生时 Redis 会读当前内存状态,直接生成一条“当前计数是多少”的记录,日志体积得到有效控制。
5.2 Redis 内存淘汰策略与 key 过期机制
计数器的 key 会不会无限增长导致内存溢出?这个担心要分情况讨论。单个计数器 key 无论加多少次,这个 key 都只占一条记录的内存空间,不会膨胀。但如果你在实验基础上扩展了功能——比如按用户 IP 分别计数、按日期分别计数——key 数量会随维度增长,这时就要考虑内存淘汰策略了。
Redis 的maxmemory参数控制最大内存,超过后根据淘汰策略清理 key。Redis 7 默认策略是noeviction,即内存满了后写入操作直接报错。实验环境触发这种情况很不友好——你的 Flask 应用调用INCR会突然抛OutOfMemory,页面直接 500。推荐在 compose 里设置--maxmemory 128mb --maxmemory-policy allkeys-lru,这样即使后续扩展了大量 key,Redis 也会自动淘汰最久没访问的 key 腾出空间,服务不会中断。
再说 key 过期。如果你把计数按天统计,比如visits:2025-01-15,就涉及到了EXPIRE命令:
r.incr("visits:2025-01-15") r.expire("visits:2025-01-15", 86400 * 2)expire设置过期时间,单位为秒。Redis 删除过期 key 有两种机制:惰性删除(访问时发现已过期才删除)和定期删除(后台每隔一段时间扫描过期 key)。两种机制下,过期 key 都不会立即物理消失,如果你短时间内用DBSIZE查看,可能会看到过期 key 还在,这是正常现象。
5.3 高频并发下的 Redis 连接调优
实验环境在本地跑,并发量不大,但如果你把这个实验搬到线上环境压测,有几个参数值得提前调好。
先是 Redis 连接数上限。Redis 配置里的maxclients默认是 10000,看起来很多,但要注意 Redis 是单线程处理命令的,连接数越多,上下文切换和 IO 多路复用的压力越大。实际经验是,压测到几千并发连接时,Redis 的吞吐量会先下降。这时候不是调大maxclients的事,而是从应用侧减少连接占用——用连接池并且合理控制max_connections,每个线程复用连接而不是新建连接。
Flask 侧的连接池参数也要根据并发量调整。max_connections=20在本地够用,但如果压测时模拟了 50 个并发请求,连接池不够就会排队等待。池不是越大越好,如果同时有 100 个并发请求,每个请求都拿着连接做 Redis 操作,Redis 服务端同时处理 100 个连接的命令,效果比 20 个连接排队差。合适的策略是把max_connections设置为并发数的 1/4 左右,或者干脆根据压测结果来定——从 20 开始,逐步加压,观察响应时间拐点在哪里。
还有一个容易被忽视的参数是 Redis 的timeout,默认是 0,表示空闲连接从不关闭。如果应用和 Redis 之间有防火墙或负载均衡设备,空闲连接可能被中间设备断开,但 Redis 端不知道,后续使用这个连接时会报Connection reset by peer。设置--timeout 300,让空闲 5 分钟以上的连接被 Redis 主动关闭,应用侧连接池会自动重建连接,反而更稳定。
5.4 常见问题与排查技巧实录
搭建和调试过程中我整理了高频问题,基本都是前置踩坑总结出来的,直接上个速查表:
| 现象 | 可能原因 | 排查命令 | 解决方式 |
|---|---|---|---|
| Docker 启动失败,报 virtualization 错误 | BIOS 未开启虚拟化,WSL2 未启用 | systeminfo或 BIOS 检查 | 开启 BIOS 虚拟化,安装 WSL2 |
docker-compose命令不存在 | 未安装 compose 插件 | docker compose version | 安装 docker-compose-plugin |
| Flask 容器启动后立即退出 | Redis 未就绪时 Flask 连不上,进程崩溃 | docker-compose logs web | 加入等待 Redis 逻辑 |
宿主机访问localhost:5000拒绝连接 | Flask 未监听 0.0.0.0 | docker exec -it web python -c "import socket; print(socket.gethostbyname(socket.gethostname()))" | 修改app.run(host="0.0.0.0") |
Redis 连接报Connection refused | 端口未映射或防火墙拦截 | docker-compose ps查看端口映射 | 检查ports配置和防火墙规则 |
| Redis 容器重启后计数全部丢失 | RDB 快照丢失两快照之间数据 | docker exec redis redis-cli info persistence | 开启 AOF 持久化 |
多个 Redis 连接报MISCONF错误 | maxmemory满了且淘汰策略为 noeviction | redis-cli info memory | 设置maxmemory-policy allkeys-lru |
| 页面报 500,日志里有 ModuleNotFoundError | requirements.txt漏掉依赖或版本冲突 | docker logs web查看 traceback | 补齐依赖,锁定版本 |
除了表格里的清单,有几个排查思路值得单独分享。
第一个是查看日志的方法。docker-compose logs -f web可以实时跟踪 Flask 容器日志,--tail=100显示最后 100 行。排查启动问题时先看 web 容器日志,因为 Redis 容器一般不会挂,错误信息几乎都出现在 Flask 容器里。如果日志被清空了,用docker inspect flask-redis-counter-web查看容器状态和重启次数,RestartCount的值如果大于 1,说明容器反复重启,通常意味着启动命令瞬间失败。
第二个是网络连通性排查。容器间网络不通时,在 Flask 容器里直接 ping Redis 服务名:
docker exec -it flask-redis-counter-web ping redis如果 ping 不通或者解析不了redis这个主机名,说明 web 容器和 redis 容器不在同一个 compose 网络里。检查docker-compose ps里两个容器的 NETWORK 字段是否为同一个网络。有时候手贱给容器单独执行了docker network disconnect,就会导致这种问题。
第三个是宿主机端口冲突。ports映射的宿主机端口如果已经被别的进程占用,compose 会启动失败。执行docker-compose logs redis会看到类似bind: address already in use的报错。解决方式是把宿主机端口改掉,比如"5001:5000",容器内走 5000 不变,外部访问改走 5001。这个思路也可以推广到多套环境共存的情况。
第四个是数据卷残留问题。我之前多次docker-compose down再up,发现旧容器虽然删了,但数据卷还在。down默认不会删除命名卷,这是为了数据安全的设计。但如果代码里持久化了错误的测试数据,需要用docker-compose down -v连数据卷一起删掉。这个命令属于破坏性操作,执行前确认你真的不需要卷里的数据。
6. 优化方向与扩展场景
实验项目跑通只是起点,这个架构可以横向扩展出很多实用功能。
把访问计数做成多维度的统计,在原来的基础上加个按用户 IP 计数的功能:
from flask import request @app.route("/") def index(): user_ip = request.remote_addr total = r.incr("stat:page:total") user_count = r.incr(f"stat:page:user:{user_ip}") r.expire(f"stat:page:user:{user_ip}", 3600) return f"Total: {total}, Your visits in this hour: {user_count}"这里涉及到两个稍微进阶的用法:字符串拼接的复杂 key、expire设置过期时间。stat:page:user:192.168.1.100这种 key 在 Redis 里存的是字符串,冒号分割是 Redis key 的常见命名习惯,便于可视化工具里按层级查看。
再进一步,把 Flask 容器做成可扩展的 Web 服务,需要引入 Nginx 反向代理和 Gunicorn 多进程。原来的python app.py用的是 Flask 内置的开发服务器,单进程性能有限,不适合真实流量场景。生产级别的 Dockerfile 里你会看到类似这样的启动命令:
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]-w 4开 4 个 worker 进程,每个 worker 是独立进程,配合连接池才能充分利用多核 CPU。这也是为什么连接池要设置成 20——4 个 worker 每个平均 5 个连接,总数合理。如果不开连接池,每个 worker 每个请求新建连接,高并发下崩溃的概率呈指数增长。
前面这些都是围绕同样的架构做增强,你还可以往里面加 Celery 做异步任务、加 Nginx 做负载均衡,但核心的容器编排思路——服务拆分、网络互通、依赖管理、数据持久化——一套实验全部涵盖。把这套思路想透了,后续做任何多容器项目,你都会自然地先思考容器间怎么通信、数据怎么持久化、服务怎么编排,这些比任何单一技术都更值钱。
按照我自己的迭代经验,这类实验有一个特别适合的演进路径:先把单机版跑顺,接着加反向代理容器,再做服务发现,最后上 Kubernetes。每一步的坑都不一样,但踩坑的方式是相通的——多看日志,按依赖顺序排查,理解每个配置参数背后的真实行为,基本上都能自己解决。