☰
Python后端爬虫专题27:六个容器一起跑才叫项目——Docker Compose部署、迁移与恢复演练
2026/10/8 23:39:36 网站建设 项目流程

Python后端爬虫专题27:六个容器一起跑才叫项目——Docker Compose部署、迁移与恢复演练

上一篇练习完整答案

解析器层最适合发现 CSS 必需字段失效;HTTP 层发现 Retry-After 或恶意重定向处理错误;Repository 发现重复运行产生两行;API 层发现跨租户读取;进程内旅程发现 ETag 虽保存却没在下次传回;Compose 旅程发现 Celery 任务名、Redis、PostgreSQL 或 MinIO 装配错误。

三条详情时,正常一条 created=1;503 下载失败一条;缺 title 解析失败一条,因此 discovered=3、created=1、failed=2、errors 长度2,数据库一行,两个有正文的页面可留下快照(列表页也有快照)。全量 pytest 的实际数字应以你当次终端输出为准;它不能证明 Docker DNS、环境变量、真实 broker 投递、PostgreSQL 驱动、MinIO bucket 和跨进程状态都正确。

先看拓扑,不要先敲up

Compose 包含 postgres、redis、minio、targetlab、一次性 migrate、api 和 worker。migrate 等 PostgreSQL healthy 后执行 Alembic;api 等迁移完成、Redis 与 TargetLab healthy;worker 再等 API 与 MinIO。depends_on的健康条件减少启动竞态,但运行中依赖崩溃仍需应用重连和告警。

API 对宿主开放 8010,TargetLab 8011,PostgreSQL 5435,Redis 6381,MinIO API/控制台 9010/9011,均绑定 127.0.0.1,避免课程密码暴露到局域网。生产应删除数据库/Redis宿主端口,使用 Secret 和 TLS。

先验证配置,再构建

cd project docker compose config--quiet docker compose up-d--build docker composeps

期待 postgres、redis、minio、targetlab、api、worker 为 healthy,migrate 为 exited (0)。migrate 正常退出不是故障;它的职责是把空库升级到 head。查看日志:

docker compose logs migrate docker compose logs--tail 80 api worker

若 API healthy 而 worker 不 healthy,检查celery inspect ping、broker URL 和任务 app;若 migrate 失败,不要让 API 以create_all偷偷建表,先修迁移。

一条公开接口旅程贯穿全部服务

.\.venv\Scripts\python.exe-m scripts.demo_journey `--api http://127.0.0.1:8010 `--target"http://targetlab:8011/jobs?page=1"`--tenant course-demo

脚本只通过 API 创建任务、轮询状态和查询职位,不读取容器内部数据库。首次应 completed、created=3、jobs=3;具体 task UUID 每次不同。Worker 访问的是 Compose DNStargetlab,其私网解析只有在 seed_hosts 和 private_hosts 都显式授权时通过。

第二次运行验证的不是速度

用相同 tenant 再跑一次,期望职位总数仍3,详情 report 主要为not_modified=3、created=0。它证明 PostgreSQL 保留 validators、TargetLab ETag 生效、Worker 发送条件头、数据库幂等,而不是简单重复请求成功。若换 tenant,第一次仍应 created=3,因为缓存和事实不能跨租户偷用。

MinIO 控制台可查看jobradar-snapshots中按快照标识配对的.html与.json对象;也可从容器日志确认 bucket 初始化。快照存在不代表内容可公开,控制台凭据只用于本地课程。

重启恢复演练

先记录/api/jobstotal;执行:

docker compose restart api worker docker composeps$headers= @{"X-Tenant-ID"="course-demo"}Invoke-RestMethod-Uri"http://127.0.0.1:8010/api/jobs?size=100"-Headers$headers

重启后仍有三条,证明数据在 PostgreSQL volume,不在 API 内存。再运行旅程仍应增量。Redis 启用 AOF,MinIO 与 PostgreSQL 都有命名卷;但 volume 不是备份,宿主磁盘损坏或down -v都会丢失。

普通停止使用docker compose down,会保留卷。docker compose down -v会删除课程 PostgreSQL、Redis、MinIO 数据,只能在明确重置实验时使用。课程验收不会替你执行删除卷的命令。

数据库迁移为什么单独验收

单测在全新 SQLite 文件执行alembic upgrade head,检查表和版本;Compose 则在 PostgreSQL 上真实执行。同一迁移脚本跨两种数据库不代表所有未来 DDL 都兼容,新增列、索引和 enum 必须分别验证。上线流程应先备份、评估锁表、迁移、验证,再滚动应用。

.\.venv\Scripts\python.exe-m pytest tests\test_migrations.py::test_alembic_upgrades_empty_database_to_current_schema-q

配置与秘密的边界

Settings从JOBRADAR_前缀环境变量读取数据库、broker、allowlist 和快照后端。Compose 中课程密码是可见开发值,不可直接用于生产。生产把 secrets 从部署平台注入,限制日志、轮换,并给 PostgreSQL/MinIO 最小账号权限。

auto_create_schema=false是重要防线:容器环境只认 Alembic。SQLite 本地默认 true 是为了降低练习门槛,两个模式必须在文档中明确,避免开发环境掩盖迁移缺失。

本篇完整运行时装配

runtime.py把环境设置转成 Session factory、快照实现、Celery queue 和 FastAPI。Worker 使用相同构建函数,模块没有写死 Compose 地址。

"""从环境变量装配数据库、Celery、快照存储和 FastAPI。"""frompathlibimportPathfromceleryimportCeleryfromminioimportMiniofrompydanticimportFieldfrompydantic_settingsimportBaseSettings,SettingsConfigDictfromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportSession,sessionmakerfrom.apiimportcreate_appfrom.repositoryimportcreate_schemafrom.snapshotsimportFileSnapshotStore,MinioSnapshotStorefrom.tasksimportcreate_celery_appclassSettings(BaseSettings):model_config=SettingsConfigDict(env_prefix="JOBRADAR_",env_file=".env",extra="ignore")database_url:str="sqlite+pysqlite:///./data/jobradar.db"broker_url:str="memory://"result_backend:str|None="cache+memory://"allowed_seed_hosts:str="target.test"allowed_private_hosts:str=""snapshot_backend:str="file"snapshot_path:Path=Path("data/snapshots")minio_endpoint:str="minio:9000"minio_access_key:str="jobradar"minio_secret_key:str="jobradar-development-only"minio_secure:bool=Falseminio_bucket:str="jobradar-snapshots"auto_create_schema:bool=True@propertydefseed_hosts(self)->set[str]:return{host.strip().casefold()forhostinself.allowed_seed_hosts.split(",")ifhost.strip()}@propertydefprivate_hosts(self)->set[str]:return{host.strip().casefold()forhostinself.allowed_private_hosts.split(",")ifhost.strip()}classCeleryTaskQueue:"""API 只传 JSON 标量;数据库会话和爬虫对象由 Worker 自己创建。"""def__init__(self,app:Celery)->None:self._app=appdefenqueue(self,*,task_id:str,seed_url:str,tenant_id:str,max_pages:int)->str:result=self._app.send_task("jobradar.crawl",kwargs={"task_id":task_id,"seed_url":seed_url,"tenant_id":tenant_id,"max_pages":max_pages,},)returnstr(result.id)defbuild_session_factory(settings:Settings)->sessionmaker[Session]:connect_args=({"check_same_thread":False}ifsettings.database_url.startswith("sqlite")else{})engine=create_engine(settings.database_url,pool_pre_ping=True,connect_args=connect_args)ifsettings.auto_create_schema:create_schema(engine)returnsessionmaker(engine,expire_on_commit=False)defbuild_snapshot_store(settings:Settings)->FileSnapshotStore|MinioSnapshotStore:ifsettings.snapshot_backend=="file":returnFileSnapshotStore(settings.snapshot_path)ifsettings.snapshot_backend=="minio":client=Minio(settings.minio_endpoint,access_key=settings.minio_access_key,secret_key=settings.minio_secret_key,secure=settings.minio_secure,)returnMinioSnapshotStore(client,settings.minio_bucket)raiseValueError("snapshot_backend must be 'file' or 'minio'")defcreate_runtime_app(settings:Settings|None=None):settings=settingsorSettings()sessions=build_session_factory(settings)celery_app=create_celery_app(settings.broker_url,settings.result_backend)returncreate_app(sessions,CeleryTaskQueue(celery_app),allowed_seed_hosts=settings.seed_hosts,allowed_private_hosts=settings.private_hosts,)

本篇课后练习

  1. 启动 Compose,记录六个长期服务与 migrate 的状态;运行首次、第二次旅程,保存两份 report 并解释差异。
  2. 重启 api/worker 后查询旧职位,再运行一次增量任务;写出证据为什么数据不是存在内存。
  3. 检查 MinIO 快照对象和 Worker 末尾日志,确认 task_id、状态、report 可关联且没有 Cookie/密码。下一篇将用这些证据完成毕业验收、简历描述和面试答辩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询