1. 项目概述:为什么一个轻量级数据看板需要 Redis + Docker 的组合拳?
你有没有遇到过这样的场景:花半天用 Streamlit 快速搭出一个销售数据实时看板,本地跑得飞起,但一发给同事,对方点开就报错——“ModuleNotFoundError: No module named 'pymysql'”;再换个环境部署,又卡在“Redis connection refused”;最后干脆把整个 Python 环境打包成 zip 发过去,结果对方电脑上连 conda 都没装……这种“本地能跑,上线即崩”的窘境,不是 Streamlit 的锅,而是缺了一套可复现、可隔离、可交付的最小闭环。而本项目标题里提到的三个关键词——Streamlit、Redis、Docker——恰好构成这个闭环的黄金三角:Streamlit 负责“快”,5分钟写出交互界面;Redis 负责“活”,提供毫秒级响应的键值缓存与状态同步;Docker 负责“稳”,把 Python 环境、依赖、配置、服务全部打包进一个镜像,运行时不再依赖宿主机的任何预装软件。这不是炫技,而是解决真实协作痛点的务实方案。它适合三类人:一是数据分析师想把临时脚本变成团队可用的轻量工具;二是后端新手想绕过复杂 API 开发,直接交付带状态管理的 Web 应用;三是 DevOps 初学者需要一个低门槛、高可见性的容器化入门项目。整套流程不碰 Nginx 反向代理、不配 Kubernetes、不写 YAML 编排,只用Dockerfile+docker-compose.yml两份文件,就能让一个带实时数据刷新、用户输入记忆、后台异步更新的 Streamlit 应用,在任意一台装了 Docker 的机器上一键启动。我实测过,从 clone 代码到浏览器打开看板,全程不超过 90 秒——这背后不是魔法,是每个环节都经过取舍与验证的工程选择。
2. 整体架构设计与技术选型逻辑:为什么不是 SQLite?为什么不用 PostgreSQL?为什么非得是 Redis?
2.1 架构分层:三层解耦,各司其职
整个应用采用清晰的三层结构:前端展示层(Streamlit)→ 数据中间层(Redis)→ 数据源层(模拟/外部 API)。这不是为了画架构图好看,而是为了解决实际开发中的三类典型冲突:
- 时间冲突:Streamlit 默认每次用户交互(如点击按钮、滑动滑块)都会重跑整个脚本,若每次操作都去查一次数据库或调用一次外部 API,体验会卡顿。Redis 作为中间缓存层,把高频读取的数据(如最新 100 条日志、当前仪表盘配置)存在内存里,Streamlit 直接
redis.get(),耗时从几百毫秒压到 0.3 毫秒以内; - 状态冲突:多个用户同时访问同一个 Streamlit 页面时,如何记住 A 用户筛选了“华东区”,B 用户筛选了“华北区”?Streamlit 本身不维护会话状态,硬编码全局变量会导致数据串扰。Redis 的
hash结构天然支持按 session_id 存储用户私有状态,redis.hset("session:abc123", "region", "华东区"),干净利落; - 更新冲突:后台数据每 30 秒自动更新(比如爬虫抓取新订单),但 Streamlit 页面不能自己刷新。我们用 Redis 的
publish/subscribe机制:后台更新完数据后PUBLISH data_updated "timestamp",Streamlit 前端通过redis.pubsub()订阅该频道,收到消息后触发st.rerun(),实现真正的“无感刷新”。
提示:这个架构刻意回避了“Streamlit 直连数据库”的常见做法。不是不能,而是不该——一旦数据库连接字符串暴露在前端代码里(哪怕只是本地开发),就埋下安全和维护隐患。Redis 作为中间层,既做了权限收敛(只开放
GET/SET/HSET/PUBLISH等必要命令),又为后续扩展留了余地(未来换成 Kafka 做事件总线,只需改中间层,Streamlit 代码零修改)。
2.2 Redis 为何不可替代?对比 SQLite 与 PostgreSQL 的硬伤
很多人第一反应是:“我用 SQLite 不就完了?轻量、单文件、免安装。” 但真正在生产边缘场景跑起来,SQLite 会暴露出三个致命短板:
| 对比维度 | SQLite | Redis | PostgreSQL |
|---|---|---|---|
| 并发读写 | 写锁全库,多用户同时提交表单会阻塞 | 读写完全并行,万级 QPS 无压力 | 行级锁优秀,但启动慢、资源占用高 |
| 实时通知 | 无原生 pub/sub,需轮询或外部触发 | 内置PUBLISH/SUBSCRIBE,毫秒级事件分发 | 需搭配 LISTEN/NOTIFY,配置复杂 |
| 数据结构 | 仅支持关系表,存用户配置需建额外表 | 原生支持 string/hash/list/set/zset,存配置、缓存、队列一把抓 | 同样需建表,JSONB 字段虽灵活但查询性能不如 Redis hash |
我做过实测:用 SQLite 存 1000 个用户的个性化筛选配置,当第 5 个用户提交修改时,其他用户的页面加载延迟从 80ms 涨到 1.2s;换成 Redis hash 后,所有用户平均延迟稳定在 0.4ms。这不是理论差距,是肉眼可见的卡顿消失。至于 PostgreSQL,它当然更强大,但为一个日活几十人的内部看板引入完整的数据库集群,就像用航空母舰送外卖——过度设计。Redis 的优势在于:它不强迫你建 schema,不强制你写 migration,不让你操心连接池大小,甚至不需要你手动清理过期数据(TTL 机制开箱即用)。本项目中,我们用redis.set("last_update_time", time.time(), ex=300)设置 5 分钟过期,5 分钟后键自动消失,完全不用写定时任务。
2.3 Docker 为何是唯一解?告别“在我机器上是好的”陷阱
Streamlit 官方文档推荐的部署方式是streamlit run app.py --server.port=8501,这在个人开发时没问题,但一旦涉及协作,立刻暴露三大脆弱性:
- 环境漂移:你的机器装了
pandas==2.0.3,同事的是pandas==1.5.3,某个.dt.to_period()方法行为不一致,看板日期显示错乱; - 依赖隐式:你本地装了
redis-py,但requirements.txt漏写了,CI 流水线构建失败,错误提示却是ModuleNotFoundError: No module named 'streamlit',排查 2 小时才发现是 Redis 客户端没装导致初始化异常; - 端口冲突:Streamlit 默认占 8501,但公司内网策略禁止该端口外放,你得手动改
--server.port=8080,改完发现同事的 nginx 配置又得同步更新。
Docker 的价值,就是用声明式的方式把“运行时契约”固化下来。Dockerfile里明确写着:
FROM python:3.11-slim COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . /app WORKDIR /app EXPOSE 8501 CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]这意味着:只要 Docker daemon 在,无论 Ubuntu、CentOS、macOS 还是 Windows WSL,执行docker build -t my-streamlit-app . && docker run -p 8501:8501 my-streamlit-app,出来的就是完全一致的运行环境。没有“在我机器上是好的”,只有“在 Docker 镜像里是确定的”。更关键的是,Docker Compose 把 Redis 和 Streamlit 当作两个独立服务编排,它们之间用内部网络通信(redis://redis:6379),彻底解耦——Streamlit 不关心 Redis 是单机还是集群,Redis 也不关心前端是 Streamlit 还是 Flask,双方只认这个地址和端口。这种松耦合,正是现代应用可维护性的基石。
3. 核心模块拆解与实操要点:从 Redis 连接池到 Streamlit Session State 的深度绑定
3.1 Redis 连接管理:为什么不用redis.Redis(host='localhost')?
初学者常犯的错误,是在 Streamlit 脚本顶部直接写:
import redis r = redis.Redis(host='localhost', port=6379, db=0) # ❌ 危险!这看似简单,实则埋下三颗雷:
- 连接泄漏:Streamlit 每次 rerun 都会重新执行脚本,
r = redis.Redis(...)创建新连接对象,旧连接未关闭,长此以往耗尽 Redis 连接数(默认 10000); - 跨容器失效:Docker 中 Streamlit 容器和 Redis 容器是不同网络命名空间,
localhost指向容器自身,而非 Redis 容器,必然连接拒绝; - 无重试机制:Redis 服务短暂抖动(如重启),脚本直接抛
ConnectionError,页面白屏。
正确做法是使用连接池(Connection Pool)+ 环境变量注入 + 延迟初始化:
import redis import os from redis import ConnectionPool # 1. 从环境变量读取配置,支持本地开发与 Docker 两种模式 REDIS_HOST = os.getenv("REDIS_HOST", "localhost") # 本地开发用 localhost REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) REDIS_DB = int(os.getenv("REDIS_DB", "0")) # 2. 创建全局连接池(注意:pool 是单例,避免重复创建) _pool = None def get_redis_connection(): global _pool if _pool is None: _pool = ConnectionPool( host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, max_connections=20, # 控制最大并发连接数 retry_on_timeout=True, socket_keepalive=True, health_check_interval=30, # 每30秒探活 ) return redis.Redis(connection_pool=_pool) # 3. 在 Streamlit 中安全使用 try: r = get_redis_connection() r.ping() # 主动探测连接是否健康 except redis.ConnectionError: st.error("❌ Redis 连接失败,请检查服务是否启动") st.stop()这段代码的关键在于:ConnectionPool复用底层 TCP 连接,get_redis_connection()确保全局唯一实例,health_check_interval自动剔除失效连接。我在某次压测中发现,当 Redis 服务重启时,未加健康检查的连接池会持续返回ConnectionResetError达 2 分钟之久;加上该参数后,30 秒内自动恢复,用户无感知。
3.2 Streamlit Session State 与 Redis 的协同:超越st.session_state
Streamlit 的st.session_state是个好东西,但它本质是内存变量,生命周期随脚本 rerun 结束而销毁,且不跨用户共享。要实现“用户 A 修改了图表主题,下次打开还是深色模式”,必须把状态持久化到 Redis。但直接r.set(f"user:{user_id}:theme", "dark")太原始,我们封装一个RedisSessionState类:
class RedisSessionState: def __init__(self, redis_client, session_id): self.r = redis_client self.session_id = session_id self.key_prefix = f"session:{session_id}:" def __getitem__(self, key): value = self.r.get(self.key_prefix + key) return value.decode('utf-8') if value else None def __setitem__(self, key, value): self.r.set(self.key_prefix + key, str(value)) def get(self, key, default=None): return self[key] or default def update(self, **kwargs): pipe = self.r.pipeline() for k, v in kwargs.items(): pipe.set(self.key_prefix + k, str(v)) pipe.execute() # 在 Streamlit 中使用 if 'session_id' not in st.session_state: st.session_state.session_id = str(uuid.uuid4()) # 生成唯一 ID redis_state = RedisSessionState(r, st.session_state.session_id) # 绑定 UI 组件 theme = redis_state.get("theme", "light") theme = st.selectbox("主题风格", ["light", "dark"], index=["light", "dark"].index(theme)) redis_state["theme"] = theme # 自动保存 # 读取时直接用 if redis_state["theme"] == "dark": st.markdown("<style>body{color:white;background:#1e1e1e;}</style>", unsafe_allow_html=True)这个封装的价值在于:把 Redis 操作对业务代码透明化。开发者仍用熟悉的[]和get()语法,底层自动处理 key 拼接、类型转换、批量写入。更重要的是,它解决了st.session_state的核心缺陷——跨页面丢失。比如用户从/dashboard跳转到/settings,st.session_state会重置,但RedisSessionState的数据还在 Redis 里,/settings页面初始化时redis_state.get("theme")依然能拿到上次的选择。
3.3 实时数据刷新:用 Redis Pub/Sub 实现“零轮询”看板
传统做法是st.experimental_rerun()配合time.sleep(5),页面每 5 秒强制刷新,用户体验差(屏幕闪动)、服务器压力大(无效请求)。Redis Pub/Sub 提供真正的事件驱动:
后台数据更新服务(data_updater.py):
import redis import time import json r = redis.Redis(host="redis", port=6379, db=0) # 注意:这里 host 是 'redis',Docker 内部服务名 def update_sales_data(): # 模拟从 API 获取新数据 new_data = {"total": 12540, "today": 321, "updated_at": time.time()} r.set("sales_data", json.dumps(new_data), ex=300) # 缓存 5 分钟 r.publish("data_updated", json.dumps({"type": "sales", "timestamp": time.time()})) if __name__ == "__main__": while True: update_sales_data() time.sleep(30) # 每30秒更新一次Streamlit 前端监听(app.py 片段):
import streamlit as st import redis import json import threading import time # 初始化 Redis 连接(同前) r = get_redis_connection() # 创建 Pub/Sub 实例(注意:必须在主线程外创建,否则阻塞 Streamlit) pubsub = r.pubsub() pubsub.subscribe("data_updated") # 用 st.cache_resource 缓存 Pub/Sub 实例,避免重复订阅 @st.cache_resource def get_pubsub(): p = r.pubsub() p.subscribe("data_updated") return p # 启动监听线程(关键!) def listen_for_updates(): for message in get_pubsub().listen(): if message["type"] == "message": try: data = json.loads(message["data"]) if data.get("type") == "sales": # 触发 Streamlit 重载 st.session_state.data_updated = time.time() st.rerun() except Exception as e: print(f"Pub/Sub 解析错误: {e}") # 在后台启动监听(Streamlit 1.28+ 支持 st.background) if "listener_started" not in st.session_state: st.session_state.listener_started = True threading.Thread(target=listen_for_updates, daemon=True).start() # 页面主体:读取并显示数据 try: sales_data = json.loads(r.get("sales_data") or "{}") st.metric("今日销售额", f"¥{sales_data.get('today', 0):,}") st.caption(f"最后更新: {time.strftime('%H:%M:%S', time.localtime(sales_data.get('updated_at', 0)))}") except Exception as e: st.warning("数据加载中...")这个方案的精妙之处在于:监听线程是 daemon(守护线程),Streamlit 主线程不受影响;st.rerun()由监听线程触发,但实际重绘仍在主线程完成。我测试过,即使后台每秒发布 10 条消息,Streamlit 页面也只在真正有数据更新时才刷新,CPU 占用率比轮询方案低 67%。而且,Pub/Sub 是 Redis 原生支持,无需额外安装插件,redis-py库开箱即用。
4. 完整实操流程:从零开始构建、测试、部署,每一步都有截图级细节
4.1 项目目录结构与初始化:5 分钟搭起骨架
先创建标准项目结构(这是 Docker 友好的关键):
streamlit-redis-docker/ ├── app.py # Streamlit 主程序 ├── data_updater.py # 后台数据更新服务 ├── requirements.txt # Python 依赖 ├── Dockerfile # Streamlit 镜像构建指令 ├── docker-compose.yml # Redis + Streamlit 服务编排 ├── .dockerignore # 排除不必要的文件 └── README.mdrequirements.txt内容(精简且精准):
streamlit==1.32.0 redis==4.6.0 pandas==2.0.3 numpy==1.24.3 # 注意:不装 flask、fastapi 等无关框架,保持镜像纯净提示:版本号必须锁定!不要写
streamlit>=1.0,否则某天 CI 构建时拉到streamlit==2.0,API 变更导致st.experimental_rerun()报错。我吃过亏——上周五下午 4 点,线上看板突然白屏,排查 3 小时发现是 Streamlit 1.31 升级到 1.32 后,st.cache_data的ttl参数默认值从None改为3600,导致缓存提前失效。锁定版本是生产环境的铁律。
.dockerignore必须包含:
__pycache__/ *.pyc *.pyo *.pyd .Python env/ venv/ .venv/ pip-log.txt pip-delete-this-directory.txt .git .gitignore README.md忽略.git和README.md很关键:Docker 构建时COPY . /app会把整个目录复制进镜像,如果包含.git,镜像体积凭空增加 10MB+,且泄露代码历史。我见过最离谱的案例:某公司镜像因未忽略.git,被扫描工具发现含敏感 commit message,直接被安全团队下线。
4.2 Docker Compose 编排:两行命令启动全栈
docker-compose.yml是本项目的灵魂,它定义了服务间的网络、依赖、端口映射:
version: '3.8' services: # Redis 服务:官方镜像,开箱即用 redis: image: redis:7.2-alpine # 用 alpine 版本,镜像仅 5MB container_name: redis-db restart: unless-stopped ports: - "6379:6379" # 本地调试时可映射,生产环境建议不暴露 command: redis-server --appendonly yes # 启用 AOF 持久化 volumes: - ./redis-data:/data # 持久化数据到宿主机 # Streamlit 服务:基于自定义 Dockerfile streamlit: build: . container_name: streamlit-app restart: unless-stopped ports: - "8501:8501" environment: - REDIS_HOST=redis # 关键!Docker 内部服务发现名 - REDIS_PORT=6379 - REDIS_DB=0 depends_on: - redis # 健康检查:确保 Redis 就绪后再启动 Streamlit healthcheck: test: ["CMD", "redis-cli", "-h", "redis", "ping"] interval: 10s timeout: 5s retries: 5注意三个魔鬼细节:
REDIS_HOST=redis:Docker Compose 会自动为每个 service 创建 DNS 记录,redis这个 hostname 在streamlit容器内解析为 Redis 容器的 IP,无需硬编码172.18.0.2;command: redis-server --appendonly yes:启用 AOF(Append Only File)持久化,避免容器意外退出时数据丢失。虽然 Redis 内存数据快,但 AOF 能保证 99.9% 的数据不丢;healthcheck:防止 Streamlit 启动时 Redis 还没完全初始化(Redis 启动约需 2 秒),depends_on只保证容器启动,不保证服务就绪,健康检查才是真保障。
启动命令极其简单:
# 第一次运行(会下载镜像、构建、启动) docker compose up -d # 查看日志,确认无报错 docker compose logs -f streamlit # 浏览器打开 http://localhost:8501我实测过,从docker compose up -d执行到浏览器显示 Streamlit 页面,平均耗时 12.3 秒(Mac M1 Pro),其中 Redis 启动 2.1 秒,Streamlit 构建 4.5 秒,健康检查等待 5.7 秒。这个时间可控、可预测,远胜于手动启 Redis、再启 Streamlit、再配环境变量的混乱流程。
4.3 Streamlit 主程序(app.py):一个完整可运行的看板示例
以下是app.py的完整内容,已通过严格测试,可直接复制使用:
import streamlit as st import redis import json import os import time import uuid from redis import ConnectionPool # ==================== 1. Redis 连接初始化 ==================== def get_redis_connection(): REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) REDIS_DB = int(os.getenv("REDIS_DB", "0")) pool = ConnectionPool( host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, max_connections=10, retry_on_timeout=True, health_check_interval=30, ) return redis.Redis(connection_pool=pool) try: r = get_redis_connection() r.ping() except Exception as e: st.error(f"❌ Redis 连接失败: {e}") st.stop() # ==================== 2. Session State 管理 ==================== class RedisSessionState: def __init__(self, redis_client, session_id): self.r = redis_client self.session_id = session_id self.key_prefix = f"session:{session_id}:" def __getitem__(self, key): value = self.r.get(self.key_prefix + key) return value.decode('utf-8') if value else None def __setitem__(self, key, value): self.r.set(self.key_prefix + key, str(value)) def get(self, key, default=None): return self[key] or default if 'session_id' not in st.session_state: st.session_state.session_id = str(uuid.uuid4()) redis_state = RedisSessionState(r, st.session_state.session_id) # ==================== 3. 页面 UI ==================== st.set_page_config( page_title="Sales Dashboard", page_icon="📊", layout="wide" ) st.title("📈 实时销售数据看板") # 主题选择(持久化到 Redis) theme = redis_state.get("theme", "light") theme = st.radio("主题风格", ["light", "dark"], index=["light", "dark"].index(theme), horizontal=True) redis_state["theme"] = theme if theme == "dark": st.markdown(""" <style> .stApp { background-color: #1e1e1e; color: white; } .stMetricValue { color: #4CAF50 !important; } </style> """, unsafe_allow_html=True) # 数据区域 col1, col2, col3 = st.columns(3) try: # 从 Redis 读取缓存数据 sales_data = json.loads(r.get("sales_data") or '{"total":0,"today":0,"updated_at":0}') with col1: st.metric("总销售额", f"¥{sales_data['total']:,}") with col2: st.metric("今日销售额", f"¥{sales_data['today']:,}") with col3: last_update = time.strftime("%H:%M:%S", time.localtime(sales_data['updated_at'])) st.metric("最后更新", last_update) except Exception as e: st.warning("数据加载中...") # 模拟数据更新按钮(演示用) if st.button("🔄 手动刷新数据"): # 模拟后台更新逻辑 new_data = { "total": sales_data.get("total", 0) + 100, "today": sales_data.get("today", 0) + 50, "updated_at": time.time() } r.set("sales_data", json.dumps(new_data), ex=300) r.publish("data_updated", json.dumps({"type": "sales", "timestamp": time.time()})) st.success("数据已更新!") # ==================== 4. 后台监听线程 ==================== import threading @st.cache_resource def get_pubsub(): p = r.pubsub() p.subscribe("data_updated") return p def listen_for_updates(): for message in get_pubsub().listen(): if message["type"] == "message": try: data = json.loads(message["data"]) if data.get("type") == "sales": st.session_state.data_updated = time.time() st.rerun() except Exception as e: pass # 忽略解析错误 if "listener_started" not in st.session_state: st.session_state.listener_started = True threading.Thread(target=listen_for_updates, daemon=True).start()这个app.py已覆盖所有核心功能:Redis 连接池、Session State 持久化、主题切换、实时数据刷新、手动触发更新。关键是它完全不依赖外部数据库或 API,所有数据都存在 Redis 中,开箱即用。你可以把它当作模板,把sales_data替换为你自己的业务数据结构(如user_stats,inventory_levels),逻辑完全复用。
4.4 构建与部署全流程:从本地测试到云服务器一键上线
本地开发与测试(推荐工作流)
- 启动服务:
docker compose up -d - 访问看板:
http://localhost:8501 - 测试 Redis 连接:在另一个终端执行
docker exec -it redis-db redis-cli,然后SET test "hello"→GET test,确认返回"hello" - 测试 Pub/Sub:新开终端
docker exec -it redis-db redis-cli,执行PUBLISH data_updated '{"type":"sales"}',观察 Streamlit 页面是否自动刷新
生产环境部署(以 Ubuntu 22.04 云服务器为例)
# 1. 登录服务器,安装 Docker curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER newgrp docker # 刷新组权限 # 2. 克隆项目(假设已推送到 GitHub) git clone https://github.com/yourname/streamlit-redis-docker.git cd streamlit-redis-docker # 3. 启动(无需修改任何配置,Docker Compose 自动适配) docker compose up -d # 4. (可选)配置反向代理(Nginx) # 编辑 /etc/nginx/sites-available/streamlit,添加: # location / { # proxy_pass http://127.0.0.1:8501; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # } # 5. 访问公网地址:http://your-server-ip:8501整个过程无需安装 Python、无需配置虚拟环境、无需担心端口冲突。我部署过 12 个类似项目,平均部署时间 4 分钟 37 秒。最关键的是,升级时只需改一行:docker compose pull && docker compose up -d --force-recreate,旧容器自动停止,新镜像无缝接管,用户无感知。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 “Connection refused” 错误的 5 种真实原因与定位方法
这是 Docker 环境下最高频的报错,但原因千差万别。我整理了真实发生过的 5 种场景及排查命令:
| 场景 | 现象 | 根本原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|---|
| Redis 未启动 | redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379. | docker compose ps显示 redis 状态为exited | docker compose logs redis | tail -10 | 检查redis-data目录权限,sudo chown -R 999:999 ./redis-data |
| 网络不通 | ConnectionError: Error 111 connecting to redis:6379. | Streamlit 容器内ping redis失败 | docker exec -it streamlit-app ping -c 3 redis | 确认docker-compose.yml中 service 名为redis,且REDIS_HOST=redis |
| 端口被占 | Bind for 0.0.0.0:6379 failed: port is already allocated | 宿主机已运行 Redis 服务 | sudo lsof -i :6379或netstat -tuln | grep :6379 | sudo systemctl stop redis-server或改docker-compose.yml端口为6380:6379 |
| AOF 文件损坏 | Redis 容器反复重启,日志出现Bad file format reading the append only file | redis-data/appendonly.aof文件损坏 | docker exec -it redis-db ls -l /data/ | 删除appendonly.aof,Redis 会从dump.rdb恢复 |
| 连接池耗尽 | Streamlit 页面偶发卡顿,Redis 日志出现max number of clients reached | max_connections=20设太小,高并发时不够用 | docker exec -it redis-db redis-cli INFO clients | grep connected_clients | 在Dockerfile中增大max_connections=50 |
实操心得:我养成了一个习惯——每次遇到 ConnectionError,第一反应不是改代码,而是执行
docker compose logs redis和docker compose logs streamlit,90% 的问题答案就藏在日志的最后 5 行里。比如某次客户反馈“看板打不开”,日志里赫然写着Can't open the append only file: Permission denied,原来是他用root用户克隆了项目,redis-data目录属主是 root,而 Redis 容器以非 root 用户(uid 999)运行,无权写入。一句sudo chown -R 999:999 ./redis-data解决。
5.2 Streamlit 页面白屏的 3 个隐蔽元凶
白屏是前端最头疼的问题,但 Streamlit 的白屏往往有迹可循:
st.rerun()在非主线程调用:上面的 Pub/Sub 示例中,st.rerun()是在子线程里触发的。Streamlit 1.28+ 已支持此用法,但如果你用的是 1.27 或更早版本,会静默失败,页面卡住。验证方法:在st.rerun()前加print("rerun triggered"),看日志是否有输出。解决方案:升级 Streamlitpip install --upgrade streamlit;st.cache_data修饰的函数返回了不可序列化对象:比如函数返回了一个redis.Redis实例,Streamlit 缓存时尝试 pickle 它,失败后白屏。验证方法:注释掉所有@st.cache_data,页面恢复正常。解决方案:确保缓存函数只返回 dict/list/str/int 等基础类型;- Docker 内存不足:Streamlit 启动时需加载 pandas、numpy 等大库,若服务器内存 < 2GB,容器可能被 OOM Killer 杀死。验证方法:
docker stats查看streamlit-app内存使用率是否长期 > 95%。解决方案:在docker-compose.yml中限制内存mem_limit: 1g,或升级服务器配置。
5.3 Docker 镜像体积优化实战:从 1.2GB 到 320MB
初始构建的镜像可能高达 1.2GB,主要来自python:3.11基础镜像(约 900MB)和pip install缓存。优化步骤:
- 换用 slim/alpine 镜像:
FROM python:3.11-slim(280MB)或python:3.11-alpine(65MB); - 多阶段构建(Multi-stage Build):
# 构建阶段:安装依赖 FROM python:3