# Linux 部署 术核 AI · 后端工程课件 ## 01 SSH、账户与服务目录 在独立 Ubuntu 24.04 练习虚拟机上建立管理入口与低权限运行账户。 ### 本节目标 - 核对 SSH 目标与主机密钥指纹 - 区分登录管理账号与不可登录服务账号 - 建立发布目录和可写业务目录的权限边界 ### 必要概念 #### 练习环境必须明确 本模块只在自己控制、可从控制台恢复的独立 Ubuntu 24.04 VM 执行。示例主机 notes-vm、IP 与域名均需替换成真实练习目标;不在已有生产主机套用整段账户、防火墙和服务配置。先保存VM快照并确认带外控制台可用。 #### 两个账号的职责 deploy 用 SSH 登录并执行经过核对的 sudo 操作,notes 只运行应用,shell 为 nologin,不保存 SSH 私钥。应用被利用后不应具备替换代码、修改 systemd 单元或读取其他服务秘密的权限。 #### 目录所有权 /srv/notes-service/releases 保存不可由运行用户修改的版本;current 指向当前版本;shared 保存需要跨版本保留的导出文件。代码、配置和业务输出分开授权,不能对整个 /srv 递归 chmod 777。 ### 动手实现 #### 1. 确认系统与目标 通过虚拟机控制台或已验证的管理 SSH 会话执行。记录 hostname、Ubuntu 版本和 SSH 主机公钥指纹,再从本机连接时对照。不要用 StrictHostKeyChecking=no 隐藏主机变化。 文件:终端 / 当前课时 ````bash hostnamectl cat /etc/os-release sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub whoami ```` #### 2. 在本机生成独立登录密钥 在自己的电脑执行,并使用口令保护私钥。路径为本次练习新文件,若已存在不要覆盖。将 .pub 公钥交给VM管理员安装,私钥不复制到服务器。 文件:终端 / 当前课时 ````bash ssh-keygen -t ed25519 -f ~/.ssh/notes_lab_ed25519 -C notes-lab # 核对指纹后连接;VM_IP替换成实际练习地址 # 完成下一步deploy账户与authorized_keys配置后,才执行: # ssh -i ~/.ssh/notes_lab_ed25519 deploy@VM_IP ```` #### 3. 在虚拟机建立管理和运行账户 使用现有管理账号执行,adduser deploy 会交互设置管理账号。用编辑器将刚生成的公钥内容写入 /home/deploy/.ssh/authorized_keys,再设置文件权限;不要粘贴私钥。notes 无交互登录权限。 文件:终端 / 当前课时 ````bash sudo adduser deploy sudo usermod -aG sudo deploy sudo install -d -o deploy -g deploy -m 700 /home/deploy/.ssh sudoedit /home/deploy/.ssh/authorized_keys sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys sudo chmod 600 /home/deploy/.ssh/authorized_keys sudo useradd --system --user-group --home-dir /srv/notes-service --shell /usr/sbin/nologin notes ```` #### 4. 建立明确目录边界 releases 只有管理员可写,notes 组可读和进入;shared/artifacts 给运行账号写。部署文件后仍需核对权限,不能因为目录存在就把所有文件设可执行。 文件:终端 / 当前课时 ````bash sudo install -d -o root -g notes -m 750 /srv/notes-service sudo install -d -o root -g notes -m 750 /srv/notes-service/releases sudo install -d -o root -g notes -m 750 /srv/notes-service/shared sudo install -d -o notes -g notes -m 750 /srv/notes-service/shared/artifacts sudo -u notes test -w /srv/notes-service/shared/artifacts sudo -u notes test ! -w /srv/notes-service/releases ```` #### 5. 验证第二个会话再收紧 SSH 保留原管理会话和控制台,从本机新开 deploy 密钥会话并运行 sudo -v。成功后才考虑在 /etc/ssh/sshd_config.d/60-notes-lab.conf 写入下面配置。先 sshd -t,再 reload;已有配置和云初始化可能覆盖行为,sshd -T 检查实际值。 第二个会话使用 ssh -i ~/.ssh/notes_lab_ed25519 deploy@VM_IP;账户和公钥在前一步已配置好。 文件:/etc/ssh/sshd_config.d/60-notes-lab.conf ````text # /etc/ssh/sshd_config.d/60-notes-lab.conf PubkeyAuthentication yes PasswordAuthentication no PermitRootLogin no # 保存后在原管理会话执行 # sudo sshd -t # sudo systemctl reload ssh # sudo sshd -T | rg 'passwordauthentication|permitrootlogin' ```` ### 验收结果 - 从新终端能用独立密钥登录 deploy 并执行 sudo -v。 - notes 不能交互登录,不能修改 releases,但可写 shared/exports。 - SSH 配置先校验再 reload,保留原会话和控制台恢复路径。 ### 生产约束与故障演练 服务长期运行在远程主机,必须保留可靠管理入口并限制应用写权限。 #### 实现决策 - 管理账号与服务账号分离。 - SSH变更先验证新会话,再关闭旧入口。 #### 隔离环境演练 在隔离VM故意给一份测试authorized_keys设置错误权限,观察新会话失败,用保留会话修复;不对真实唯一入口试错。 #### 恢复验收 - 密钥指纹与权限有记录。 - 账户错误可通过预留入口恢复。 ### 常见错误 #### 修改 SSH 后新连接失败 原因:公钥权限错误、配置顺序覆盖或改动未经新会话验证。 处理:使用保留会话或控制台恢复该练习配置,检查 auth 日志与 sshd -T,再用新终端验证。 #### 服务账号能改 Dockerfile 或 systemd 单元 原因:代码和系统配置错误归运行用户所有。 处理:将发布物与单元归 root 管理,只允许 notes 写特定输出目录。 ### 扩展练习 为什么不能让 Web 服务直接使用 deploy 账号运行? 提示:deploy 具备管理能力与登录凭据。 参考答案: 应用漏洞会扩大到发布权限或 sudo 能力。独立 notes 账号将运行权限限制在服务需要的配置、网络和输出目录,deploy 只用于受控部署。 ### 技术文档 - [Ubuntu:OpenSSH](https://ubuntu.com/server/docs/how-to/security/openssh-server/) - [OpenSSH:ssh-keygen](https://man.openbsd.org/ssh-keygen) - [Ubuntu:用户管理](https://documentation.ubuntu.com/server/how-to/security/user-management/) --- ## 02 监听地址、DNS 与防火墙 从进程监听到云网络逐层检查连通性,在开启防火墙前验证 SSH 保留与回退。 ### 本节目标 - 区分服务监听、主机防火墙与云安全组 - 使用DNS和TCP检查定位访问失败 - 安全地测试UFW规则并保留SSH会话 ### 必要概念 #### 网络路径有多层 客户端先解析DNS,连接公网或私网IP,经云安全组/路由到主机防火墙,再到监听端口。服务没监听时放开防火墙没有作用;DNS解析正确也不能证明端口可达。 #### 公开端口最小化 Nginx对外提供80/443,API只监听127.0.0.1:8000;PostgreSQL与Redis继续使用本机回环调试端口或受控私网。Docker发布端口可能经过不同于普通INPUT的转发规则,不能假定UFW会拦截所有Docker ports。 #### 防止锁死 SSH端口可能不是22,来源IP也可能经过VPN或跳板机。启用UFW前读取实际SSH监听、添加保留规则、准备限时自动回退,并保留当前会话和控制台。关闭旧会话不是验证新规则的方法。 ### 动手实现 #### 1. 核对当前监听与路由 在VM执行只读命令;没有rg则用grep。识别真实SSH端口与监听地址,后续规则使用实际值,不盲目套22。 文件:终端 / 当前课时 ````bash sudo ss -lntp ip -brief address ip route sudo ufw status verbose ```` #### 2. 检查域名解析与本机端点 尚无域名时用VM IP或本机Host头完成后续HTTP课程。notes.example.com为文档占位,不能注册证书。DNS A与AAAA都应指向能提供服务的地址,错误AAAA可能导致部分用户访问失败。 文件:终端 / 当前课时 ````bash getent ahosts notes.example.com # API启动后再执行: curl --connect-timeout 2 --max-time 5 -i http://127.0.0.1:8000/ready ```` #### 3. 先添加SSH和HTTP规则 仅在已确认SSH为22且有控制台的独立VM执行。若使用非22端口,修改下列规则。已有UFW规则不要 reset;先阅读并保留其他必要访问。自动回退任务3分钟后关闭UFW,只用于这次隔离练习。 文件:终端 / 当前课时 ````bash sudo ufw allow 22/tcp comment 'keep verified SSH' sudo ufw allow 80/tcp comment 'HTTP and ACME' sudo ufw allow 443/tcp comment 'HTTPS' sudo systemd-run --unit=notes-firewall-rollback --on-active=3m /usr/sbin/ufw disable sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw enable ```` #### 4. 用新会话验证后取消回退 保持旧会话,在本机另开SSH并执行一个只读命令。成功后检查UFW状态,取消尚未执行的回退timer;若失败,不继续收紧规则,使用保留会话或等待自动回退。 文件:终端 / 当前课时 ````bash # 新SSH会话成功后执行 sudo ufw status numbered sudo systemctl stop notes-firewall-rollback.timer sudo systemctl status notes-firewall-rollback.timer --no-pager ```` #### 5. 核对Docker与云侧规则 若依赖运行在Docker,docker ps检查端口只能为127.0.0.1:5433、缓存6380、任务broker6384。云安全组按本练习仅开放已验证SSH和80/443;此课不代替在真实云账号创建规则。 文件:终端 / 当前课时 ````bash docker ps --format 'table {{.Names}} {{.Ports}}' sudo ss -lntp # 从另一台获准练习客户端检测VM的80/443;数据库端口不应对公网开放。 ```` ### 验收结果 - 新SSH会话在UFW启用后成功,回退timer已确认取消。 - API/数据库/Redis监听地址与外部公开端口列表一致。 - DNS、TCP、HTTP失败可以分层描述,未对数据库开放公网入口。 ### 生产约束与故障演练 只公开HTTPS入口,数据库与队列保留在内网或本机。 #### 实现决策 - 防火墙变更保留已验证SSH路径与自动回退。 - 主机和云网络规则分别核对,Docker端口单独检查。 #### 隔离环境演练 在隔离VM启用规则并从新会话验证;若配置错误,使用定时回退或控制台恢复。 #### 恢复验收 - 没有失去管理入口。 - 公开端口清单符合服务架构。 ### 常见错误 #### 本机curl成功,外部浏览器超时 原因:云安全组、主机防火墙或监听绑定地址阻断。 处理:按路径逐层检查,不同时关闭全部安全规则掩盖问题。 #### UFW拒绝端口但Docker服务仍能被外部访问 原因:Docker发布端口涉及NAT/转发规则。 处理:优先取消公开发布或绑定回环,按Docker官方防火墙集成文档核对实际路径。 ### 扩展练习 域名同时有A和AAAA记录,只检查IPv4是否足够? 提示:客户端可能优先选择IPv6。 参考答案: 不够。需要验证两种地址都可到达正确服务;未配置IPv6服务时移除错误AAAA,不能让部分用户随机遇到超时。 ### 技术文档 - [Ubuntu:防火墙](https://ubuntu.com/server/docs/security-firewall/) - [Docker:防火墙与UFW](https://docs.docker.com/engine/network/packet-filtering-firewalls/) - [systemd-run:临时任务(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man1/systemd-run.1.html) --- ## 03 Python 环境、配置与最小权限 把验证过的源码安装到独立发布目录,使用系统支持的Python与专用环境文件启动。 ### 本节目标 - 在发布目录创建独立venv并安装锁定依赖 - 区分API与任务驱动的数据库连接串 - 让运行账号只读代码和配置,单独写输出 ### 必要概念 #### Ubuntu默认Python版本 Ubuntu24.04通常提供Python3.12,满足课程Python3.11+要求;不要为了匹配旧命令替换系统/usr/bin/python3。若必须精确3.11,使用经过组织批准的运行时或Docker3.11镜像,并分别生成目标版本依赖锁。 #### 部署输入完整 发布物要包含app、迁移、锁文件和最终tasks_lab,而不是旧api-lab下载包。任务模块的DDL需纳入迁移并在空库验证,任务入口为tasks_lab/celery_app.py中的app,队列为exports.fast/bulk。 #### 环境文件不是Shell脚本 systemd EnvironmentFile读取KEY=value,不执行$(command)或source逻辑。API使用SQLAlchemy URL,任务直接psycopg使用PostgreSQL DSN,分别存文件。秘密文件root管理,运行用户通过所属组只读,诊断时不输出全文。 ### 动手实现 #### 1. 安装Ubuntu提供的运行依赖 只在独立VM安装包;升级已有生产主机应走维护流程。本模块进程主线使用Ubuntu默认python3,不安装到系统site-packages。若数据库继续使用Docker,宿主只安装postgresql-client用于检查与备份。 文件:终端 / 当前课时 ````bash sudo apt update sudo apt install -y python3 python3-venv nginx postgresql-client curl rsync python3 --version ```` #### 2. 部署已验证发布物 将本地完成的notes-service源码包上传到deploy自己的staging目录,先校验文件清单与摘要。不要以root解压不明tar。下列命令假定~/staging/notes-service已是经检查的目录;r001是本次练习明确版本,不能覆盖已有目录。 文件:终端 / 当前课时 ````bash sudo install -d -o root -g notes -m 750 /srv/notes-service/releases/r001 sudo rsync -a --exclude=.env --exclude='.env.*' --exclude=.venv --exclude=secrets --exclude=backups ~/staging/notes-service/ /srv/notes-service/releases/r001/ sudo chown -R root:notes /srv/notes-service/releases/r001 sudo chmod -R u=rwX,g=rX,o= /srv/notes-service/releases/r001 sudo python3 -m venv /srv/notes-service/releases/r001/.venv sudo /srv/notes-service/releases/r001/.venv/bin/python -m pip install --require-hashes -r /srv/notes-service/releases/r001/requirements-lock.txt ```` #### 3. 建立两个专用环境文件 用sudoedit创建下列文件,填VM实际依赖地址与URL编码密码。若沿用Docker练习依赖:PG宿主5433,缓存6380,任务broker6384;若迁移到受控私网数据库按实际地址改,绝不能在主机进程中使用Docker服务名postgres/broker。 文件:终端 / 当前课时 ````dotenv # /etc/notes-service.env(API) DATABASE_URL=postgresql+psycopg://app_user:ENCODED_PASSWORD@127.0.0.1:5433/notes_lab DB_POOL_SIZE=5 DB_MAX_OVERFLOW=0 DB_POOL_TIMEOUT=2 # /etc/notes-tasks.env(tasks_lab) DATABASE_URL=postgresql://app_user:ENCODED_PASSWORD@127.0.0.1:5433/notes_lab CELERY_BROKER_URL=redis://:ENCODED_TASK_BROKER_PASSWORD@127.0.0.1:6384/0 CELERY_RESULT_BACKEND=redis://:ENCODED_TASK_BROKER_PASSWORD@127.0.0.1:6384/1 ```` #### 4. 限制配置和输出权限 先确认文件是刚编辑的两个目标,再设置所有权。任务工程默认相对artifacts目录改为指向shared/artifacts的软链;已有目录若包含结果先核对并迁移,不能直接rm -rf。新发布物应不包含旧导出文件。 文件:终端 / 当前课时 ````bash sudo chown root:notes /etc/notes-service.env /etc/notes-tasks.env sudo chmod 640 /etc/notes-service.env /etc/notes-tasks.env sudo ln -s /srv/notes-service/shared/artifacts /srv/notes-service/releases/r001/tasks_lab/artifacts sudo ln -s releases/r001 /srv/notes-service/current sudo -u notes test -r /etc/notes-service.env sudo -u notes test ! -w /srv/notes-service/releases/r001/app/main.py sudo -u notes test -w /srv/notes-service/shared/artifacts ```` #### 5. 检查环境与导入 不source秘密文件到共享交互终端。下面只验证包和文件路径;带环境启动由下一课systemd读取。若依赖锁与Python版本不兼容,返回目标Linux/Python版本重新生成并验证锁,不去掉固定版本强行安装。 文件:终端 / 当前课时 ````bash sudo -u notes /srv/notes-service/current/.venv/bin/python -c "import sys, fastapi, psycopg; print(sys.version); print(fastapi.__version__, psycopg.__version__)" sudo -u notes readlink -f /srv/notes-service/current sudo namei -l /srv/notes-service/current/app/main.py ```` ### 验收结果 - API与任务环境文件使用不同DSN前缀,宿主端口与实际依赖一致。 - notes可读配置但不能写发布代码,导出路径指向shared。 - venv来自当前VM支持的Python,锁定依赖安装和导入成功。 ### 生产约束与故障演练 服务器重启后进程需要用确定的解释器、代码和秘密恢复。 #### 实现决策 - 发布目录不可由服务修改。 - API与任务配置分开,秘密不进入源码包。 #### 隔离环境演练 用notes账号尝试写代码文件应失败,写shared/exports应成功;误配DSN在隔离环境修复后再启动。 #### 恢复验收 - 部署输入和权限边界可复现。 - 无全局pip安装和明文秘密日志。 ### 常见错误 #### 直接psycopg报invalid connection option或scheme 原因:将SQLAlchemy的postgresql+psycopg://交给直接驱动。 处理:任务环境使用postgresql://,API保留SQLAlchemy方言URL。 #### systemd找不到包但登录终端能导入 原因:服务未使用venv解释器,或发布路径不一致。 处理:ExecStart指定绝对venv Python路径,不依赖交互shell激活。 ### 扩展练习 为什么不能在服务器执行sudo pip install -U覆盖系统Python依赖? 提示:Ubuntu工具也可能依赖系统Python。 参考答案: 这样会污染系统包管理和其他应用。每个发布目录使用独立venv,升级形成新发布物,系统Python由发行版包管理维护。 ### 技术文档 - [Python:venv](https://docs.python.org/3/library/venv.html) - [Ubuntu:Python开发环境](https://documentation.ubuntu.com/ubuntu-for-developers/howto/python-setup/) - [systemd:EnvironmentFile(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man5/systemd.exec.5.html) --- ## 04 systemd 管理 API、Worker 与维护进程 把服务交给systemd启动、重启和终止,明确任务循环与单实例调度器。 ### 本节目标 - 编写可验证的systemd单元 - 分别管理API、fast/bulk worker、publisher/recovery - 设置重启节流、资源限制和SIGTERM退出预算 ### 必要概念 #### 进程监督 systemd知道主进程PID、退出码和重启次数,可以开机启动并收集日志。Type=simple服务应前台运行,不nohup、不后台&。Wants network-online不是数据库一定可用,应用仍需有界连接超时。 #### 真实任务入口 worker工作目录为current/tasks_lab,命令python -m celery -A celery_app:app;fast/bulk分别订阅exports.fast与exports.bulk。publisher.py/recovery.py本身只执行一次,需要Docker模块提供的run_loops.py或同等有界循环,不能将一次性脚本伪称常驻服务。 #### 重启与资源 Restart=on-failure适合异常退出重启,StartLimit限制崩溃循环。MemoryMax、CPUQuota、TasksMax是cgroup边界,不是性能承诺。Celery warm shutdown可能等待当前任务,TimeoutStopSec应覆盖已定义的任务最大时长,仍需恢复租约处理强杀。 ### 动手实现 #### 1. 新建API服务单元 保存到/etc/systemd/system/notes-api.service。使用Uvicorn内置进程主线,未采用已弃用的uvicorn.workers。API只监听回环,代理头仅信任本机Nginx。数据库迁移不放进此单元ExecStartPre。 文件:/etc/systemd/system/notes-api.service ````ini [Unit] Description=notes-service API Wants=network-online.target After=network-online.target StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=simple User=notes Group=notes WorkingDirectory=/srv/notes-service/current EnvironmentFile=/etc/notes-service.env Environment=PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 ExecStart=/srv/notes-service/current/.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --proxy-headers --forwarded-allow-ips=127.0.0.1 --timeout-graceful-shutdown 25 Restart=on-failure RestartSec=3 TimeoutStopSec=35 KillSignal=SIGTERM MemoryMax=512M CPUQuota=100% TasksMax=128 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true UMask=0027 [Install] WantedBy=multi-user.target ```` #### 2. 新建fast/bulk Worker模板 保存notes-worker@.service,实例参数fast和bulk映射两个既定队列。共享输出需要ReadWritePaths;先验证共享目录存在。实际任务上限若超过60秒,应先调整任务协议与退出预算,不照抄75秒。 文件:/etc/systemd/system/notes-worker@.service ````ini [Unit] Description=notes-service %i export worker Wants=network-online.target After=network-online.target StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=simple User=notes Group=notes WorkingDirectory=/srv/notes-service/current/tasks_lab EnvironmentFile=/etc/notes-tasks.env Environment=PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 ExecStart=/srv/notes-service/current/.venv/bin/python -m celery -A celery_app:app worker -l INFO -Q exports.%i --concurrency=1 -n %i@%%h Restart=on-failure RestartSec=5 TimeoutStopSec=75 KillSignal=SIGTERM KillMode=mixed MemoryMax=768M CPUQuota=100% TasksMax=128 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/srv/notes-service/shared/artifacts UMask=0027 [Install] WantedBy=multi-user.target ```` #### 3. 为publisher/recovery创建独立模板 需要Docker第9课已给出的tasks_lab/run_loops.py,或复制那份受控循环到发布物。模板传publisher/recovery选择单步函数;不把维护任务与worker塞入同进程。EnvironmentFile仍为tasks专用,日志仅记录异常类别与关联id。 文件:/etc/systemd/system/notes-maintenance@.service ````ini [Unit] Description=notes-service %i loop Wants=network-online.target After=network-online.target StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=simple User=notes Group=notes WorkingDirectory=/srv/notes-service/current/tasks_lab EnvironmentFile=/etc/notes-tasks.env Environment=PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 ExecStart=/srv/notes-service/current/.venv/bin/python run_loops.py %i Restart=on-failure RestartSec=5 TimeoutStopSec=20 MemoryMax=256M TasksMax=64 NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true UMask=0027 [Install] WantedBy=multi-user.target ```` #### 4. 可选新增单实例beat 只有完成任务模块周期任务后才启用。沿用Worker单元但Description改为notes-service beat、ExecStart替换如下、移除原ReadWritePaths并加StateDirectory=notes-service。保存独立notes-beat.service,只运行一个实例;本地调度文件不是分布式领导选举。 文件:/etc/systemd/system/notes-beat.service ````ini ExecStart=/srv/notes-service/current/.venv/bin/python -m celery -A celery_app:app beat -l INFO --schedule=/var/lib/notes-service/celerybeat-schedule StateDirectory=notes-service ReadWritePaths=/var/lib/notes-service ```` #### 5. 验证配置并按顺序启动 先完成数据库模块和任务模块最终DDL迁移,数据库及认证broker必须可达。systemd-analyze verify可发现字段/路径问题,不能代替真实数据库验收;readiness与真实任务完成后再enable开机启动。 文件:终端 / 当前课时 ````bash sudo systemd-analyze verify /etc/systemd/system/notes-api.service /etc/systemd/system/notes-worker@.service /etc/systemd/system/notes-maintenance@.service sudo systemctl daemon-reload sudo systemctl start notes-api notes-worker@fast notes-worker@bulk notes-maintenance@publisher notes-maintenance@recovery sudo systemctl status notes-api notes-worker@fast --no-pager curl --fail --max-time 5 http://127.0.0.1:8000/ready sudo systemctl enable notes-api notes-worker@fast notes-worker@bulk notes-maintenance@publisher notes-maintenance@recovery ```` ### 验收结果 - API和各任务进程有独立unit、PID与日志,退出原因可查询。 - Celery节点名展开正确,fast/bulk只消费对应队列。 - SIGTERM后进程在预算内退出,超过预算的业务任务按租约恢复协议处理。 ### 生产约束与故障演练 主机重启与应用崩溃后,各角色需要按各自预算恢复运行。 #### 实现决策 - 监督前台主进程,迁移独立执行。 - 任务维护循环和调度器与worker分开,beat单实例。 #### 隔离环境演练 在独立VM正常stop/start API与一组worker,核对日志、任务状态和恢复时间。 #### 恢复验收 - 开机启动可验证。 - 重启不会无界循环或静默丢失业务状态。 ### 常见错误 #### systemd启动立即报找不到celery_app 原因:WorkingDirectory未指向tasks_lab或源文件未部署。 处理:核对最终任务工程路径及绝对venv,不能靠登录终端PYTHONPATH临时修复。 #### 节点名出现错误或systemd拒绝ExecStart 原因:systemd把%h当成自身specifier展开。 处理:传给Celery的%h需在unit中写%%h;%i保留用于systemd实例名。 ### 扩展练习 数据库短暂故障时要不要让API进程退出,以便systemd不断重启? 提示:所有实例依赖同一数据库时会发生什么? 参考答案: 通常不应把readiness失败直接变成所有进程退出。API保持可观测并返回503,修复依赖后恢复;只有进程自身无法继续工作才退出。重启策略设置节流,避免掩盖真实故障。 ### 技术文档 - [systemd:service(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man5/systemd.service.5.html) - [systemd:资源控制(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man5/systemd.resource-control.5.html) - [Celery:Worker关闭](https://docs.celeryq.dev/en/stable/userguide/workers.html) - [Uvicorn:部署](https://uvicorn.dev/deployment/) --- ## 05 Nginx HTTP 入口与 TLS 先把回环API接到Nginx,再在拥有域名与DNS控制权时配置受信任HTTPS。 ### 本节目标 - 建立可验证的HTTP反向代理配置 - 设置请求体与上游超时边界 - 在明确域名与证书前置条件下配置TLS续期 ### 必要概念 #### 代理负责公网连接 Nginx终止客户端连接,将HTTP转发到127.0.0.1:8000。API不直接公开端口,转发Host与真实协议;只有可信代理可以影响应用识别的客户端地址与HTTPS状态。 #### 错误发生的层级 502通常表示上游连接或协议失败,504通常表示等待上游超时,413来自请求体限制。应结合Nginx error log与API request_id判断,不能看到所有5xx都重启数据库。 #### 证书需要真实控制权 notes.example.com是占位名,不能申请证书。只有持有实际域名、DNS正确指向练习主机且验证路径可达时,才使用Certbot完成ACME。没有这些条件时先用HTTP本机探针学习代理,不关闭客户端TLS校验伪造完成。 ### 动手实现 #### 1. 新建站点配置 保存/etc/nginx/sites-available/notes-service。在独立VM确认没有同名站点或80端口冲突;server_name先用文档占位配合本机Host验证,正式申请证书前替换为自己持有的域名。 文件:/etc/nginx/sites-available/notes-service ````nginx server { listen 80; server_name notes.example.com; client_max_body_size 2m; access_log /var/log/nginx/notes-access.log; error_log /var/log/nginx/notes-error.log warn; location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 3s; proxy_read_timeout 30s; proxy_send_timeout 30s; } } ```` #### 2. 校验再加载站点 只新建本次站点软链,不删除现有默认或其他用户站点。nginx -t失败时不reload,修复配置后重试。用Host头选中本次server块,验证/ready与/notes。 文件:终端 / 当前课时 ````bash sudo ln -s /etc/nginx/sites-available/notes-service /etc/nginx/sites-enabled/notes-service sudo nginx -t sudo systemctl reload nginx curl --fail --max-time 5 -H 'Host: notes.example.com' http://127.0.0.1/ready sudo tail -n 30 /var/log/nginx/notes-error.log ```` #### 3. 具备真实域名后申请证书 此步骤需要实际域名控制权和公网验证条件,本课不替用户注册域名或申请证书。按Certbot官网Ubuntu/Nginx当前安装说明准备工具,替换YOUR_DOMAIN后执行;先验证DNS与HTTP访问。签发会联系外部CA并修改站点配置,应在自己的练习域名操作。 文件:终端 / 当前课时 ````bash # 前提已完成:真实域名DNS/80端口验证、Certbot安装、Nginx配置校验 sudo certbot --nginx -d YOUR_DOMAIN sudo nginx -t sudo certbot certificates ```` #### 4. 验证TLS与续期 使用真实域名而不是curl -k检查证书主机名、链与有效期。检查系统中的续期timer,执行dry-run验证续期流程;测试必须保留ACME挑战访问或对应DNS验证能力。 文件:终端 / 当前课时 ````bash curl --fail --max-time 10 https://YOUR_DOMAIN/ready sudo certbot renew --dry-run systemctl list-timers --all | grep -E 'certbot|snap.certbot' ```` #### 5. 演练上游失败并确认日志 仅在隔离VM停API,Nginx应返回网关错误,同时error log报告上游连接失败;启动API后恢复。不要把异常SQL、密码或Authorization记录进Nginx自定义日志。 文件:终端 / 当前课时 ````bash sudo systemctl stop notes-api curl --max-time 5 -i -H 'Host: notes.example.com' http://127.0.0.1/ready sudo tail -n 20 /var/log/nginx/notes-error.log sudo systemctl start notes-api ```` ### 验收结果 - HTTP代理能到达回环API,nginx -t通过。 - 真实域名条件具备时,HTTPS无需跳过校验且续期dry-run成功;否则明确未配置TLS。 - 停API产生可定位的502类上游错误,恢复后/ready成功。 ### 生产约束与故障演练 用户通过HTTPS访问API,证书和代理错误必须持续可观测。 #### 实现决策 - 应用只信任本机代理,公网由Nginx统一入口。 - 证书申请与续期都验证,缺域名条件时明确未完成项。 - 本例笔记接口用于隔离练习,尚无完整用户身份与数据归属校验。TLS只加密传输;投入真实业务前必须补齐身份、操作权限、数据归属和审计,并验证未授权请求无法读写。 #### 隔离环境演练 停止练习API观察Nginx错误,再恢复;对自己域名执行续期dry-run并记录结果。 #### 恢复验收 - 代理错误能定位到上游层。 - TLS状态不靠忽略证书错误通过。 ### 常见错误 #### Nginx默认欢迎页而非API 原因:请求Host没有匹配server_name,或站点链接未加载。 处理:用nginx -T在可信终端定位server块,检查Host和软链;不要随意删除所有默认配置。 #### 证书首次申请成功,数月后过期 原因:续期任务未运行或验证路径被防火墙/代理改动阻断。 处理:检查timer、续期日志与dry-run,证书剩余天数加入监控。 ### 扩展练习 把client_max_body_size调到1GB能否解决所有上传需求? 提示:应用、代理、磁盘和超时仍有各自限制。 参考答案: 不能。应按业务定义大小、流式处理和对象存储上传方案,避免单请求在API内存中聚集大文件;代理限制必须与应用校验和资源预算一致。 ### 技术文档 - [Nginx:反向代理](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) - [Nginx:HTTPS配置](https://nginx.org/en/docs/http/configuring_https_servers.html) - [Certbot:Nginx安装与续期](https://certbot.eff.org/instructions?ws=nginx&os=snap) --- ## 06 SSE、WebSocket 与代理超时 为长连接选择独立代理设置,验证SSE逐条到达和WebSocket协议升级。 ### 本节目标 - 关闭SSE响应缓冲并区分请求缓冲 - 正确传递WebSocket Upgrade与Connection - 协调心跳、代理空闲超时和就绪排空 ### 必要概念 #### SSE是持续HTTP响应 服务端不断发送text/event-stream,若代理缓冲,用户会很晚才看到一整块内容。proxy_buffering off影响响应,proxy_request_buffering影响请求上传,二者不能互相替代。心跳间隔应小于经过的各层空闲超时。 #### WebSocket需要显式升级 Upgrade和Connection是逐跳头,反向代理需显式传递。上游返回101后转为双向隧道。HTTP的普通30秒响应预算不适用于长连接,但无限超时会隐藏失联客户端,仍需ping/pong和断线重连。 #### 就绪与已建立连接 从负载均衡摘除实例通常只阻止新连接,已有SSE/WS不会自动搬到另一进程。发布需要排空窗口、合理的连接寿命和客户端重连/续传。不要在长流期间持续占用数据库事务。 ### 动手实现 #### 1. 明确本课端点前置 API+数据库模块本身没有SSE/WS。先将API服务开发模块完成的流式与WebSocket示例路由整合到notes-service,确认实际路径;下文使用/stream与/ws作为本次教学适配路径,若原代码不同需同时改代理和客户端,不能声称下载包自动有端点。 文件:终端 / 当前课时 ````bash # 先绕过Nginx验证实际应用端点;服务必须已经整合这些路由 curl -N --max-time 10 http://127.0.0.1:8000/stream # WebSocket用API模块已有客户端连接 ws://127.0.0.1:8000/ws ```` #### 2. 在http上下文新增Upgrade映射 Ubuntu默认nginx.conf的http块会include /etc/nginx/conf.d/*.conf。新建此文件仅定义map,不包额外http块;nginx -t确认上下文正确。 文件:/etc/nginx/conf.d/notes-upgrade.conf ````nginx map $http_upgrade $connection_upgrade { default upgrade; '' close; } ```` #### 3. 给SSE路径增加独立location 将此location添加到本次notes-service的server块,和已有location /并列。代理重写规则保持路径原样;示例读取空闲超时75s需配合应用每15–25秒心跳,实际周期以API模块实现为准。 文件:/etc/nginx/sites-available/notes-service ````nginx location /stream { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; proxy_cache off; proxy_read_timeout 75s; proxy_send_timeout 75s; gzip off; } ```` #### 4. 给WebSocket路径增加升级配置 同样添加到server块。TLS站点客户端使用wss://。proxy_read_timeout衡量两次读取之间的间隔,不是整条连接绝对存活时间;应用层需要心跳和会话过期规则。 文件:/etc/nginx/sites-available/notes-service ````nginx location /ws { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 75s; proxy_send_timeout 75s; } ```` #### 5. 验证逐条到达与断线恢复 先nginx -t再reload;curl -N避免客户端输出缓冲混淆。使用API课已有WS客户端确认101和双向消息,然后在隔离VM重启API,记录客户端如何重连。SSE若没有event id/续传实现,应明确重连不能恢复已漏事件。 文件:终端 / 当前课时 ````bash sudo nginx -t sudo systemctl reload nginx curl -N --max-time 20 -H 'Host: notes.example.com' http://127.0.0.1/stream # 另一个终端观察本次站点日志与客户端重连 sudo journalctl -u notes-api -f ```` ### 验收结果 - SSE通过Nginx仍逐条到达,未被缓冲成结束时一次输出。 - WebSocket成功返回101并双向传输,TLS环境使用wss。 - 重启时长连接会断开且客户端有明确恢复行为,未宣称无损切换。 ### 生产约束与故障演练 Agent流式结果和交互会话经过反向代理后仍应及时、可恢复地传输。 #### 实现决策 - SSE与WS按各自协议配置,不套普通短请求超时。 - 客户端恢复协议与代理排空共同设计。 #### 隔离环境演练 在隔离VM通过代理建立流和WS,再重启API,记录最后收到事件、断线时间与重连结果。 #### 恢复验收 - 逐条传输有实际观测。 - 没有把连接重连等同业务状态自动恢复。 ### 常见错误 #### 服务端日志逐条输出,浏览器最后才看到全部内容 原因:Nginx或其他代理/客户端缓冲。 处理:逐层绕过代理对比,设置proxy_buffering off并用curl -N;检查上游应用是否正确发送SSE边界。 #### WS连接约60秒断开 原因:代理空闲超时与应用心跳不匹配。 处理:测量实际心跳周期,协调Nginx和前置负载均衡超时,不能只无限延长一层。 ### 扩展练习 readiness变为503后,已有WebSocket是否自动迁移到健康实例? 提示:TCP连接状态绑定原进程。 参考答案: 不会。需要客户端断线重连到健康实例;若要续接会话,状态和事件序号必须保存在可共享存储中,而非只在单进程内存。 ### 技术文档 - [Nginx:WebSocket代理](https://nginx.org/en/docs/http/websocket.html) - [Nginx:响应缓冲](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering) - [Nginx:读取超时](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout) --- ## 07 发布目录、原子切换与兼容回退 用新版本目录准备完整环境,先迁移后切换current,并明确单实例中断和蓝绿切换边界。 ### 本节目标 - 在独立版本目录准备和验证发布物 - 通过原子软链替换选择新版本 - 判断数据库兼容性和在途任务是否允许回退 ### 必要概念 #### 目录切换不替换运行进程 current软链原子替换只影响之后打开的路径,已经运行的Python仍持有旧代码、工作目录和内存。必须明确重启API与各任务进程,不能以软链已更新推断服务已更新。 #### 迁移先兼容 新迁移在切换前运行,旧进程仍可能访问数据库,所以结构必须向后兼容。任务名和消息参数也属于跨版本协议:旧消息可能在新worker上执行,新消息可能被旧worker拿到,升级时必须保留兼容或分版本队列。 #### 可用性选择 单API实例systemctl restart有短暂中断。本课先完成可回退的单实例发布;要平滑切换可在8001启动候选实例、验收后改Nginx upstream并reload,再排空旧实例。长连接仍需要重连协议,不能称为绝对零中断。 ### 动手实现 #### 1. 准备r002完整目录 按第3课创建新目录r002、复制已验证源码、建立venv并安装目标锁,创建artifacts软链。不要覆盖r001,也不要在current目录边写代码边处理请求。下面只检查新目录满足条件。 deploy不加入运行组,路径检查通过sudo -u notes执行,保持发布权限与运行身份分开。 文件:终端 / 当前课时 ````bash sudo -u notes test -f /srv/notes-service/releases/r002/app/main.py sudo -u notes test -f /srv/notes-service/releases/r002/tasks_lab/celery_app.py sudo -u notes test -x /srv/notes-service/releases/r002/.venv/bin/python sudo -u notes readlink -f /srv/notes-service/current ```` #### 2. 用一次性systemd作业运行迁移 仅对专用练习数据库执行已审核迁移;EnvironmentFile使用API SQLAlchemy URL,迁移账号若已分离应改为独立/etc/notes-migrate.env。systemd-run --wait会报告进程结果;失败不继续切换。先保存当前revision与备份记录。 文件:终端 / 当前课时 ````bash sudo systemd-run --unit=notes-migrate-r002 --wait --collect --property=User=notes --property=Group=notes --property=WorkingDirectory=/srv/notes-service/releases/r002 --property=EnvironmentFile=/etc/notes-service.env /srv/notes-service/releases/r002/.venv/bin/python -m alembic upgrade head ```` #### 3. 原子替换current并重启 在/srv/notes-service同一文件系统内先创建临时软链,再mv -T替换current。先检查current确实是软链且指向已记录的r001,禁止覆盖普通目录。root拥有该目录,运行用户不能劫持切换目标。 文件:终端 / 当前课时 ````bash sudo -u notes test -L /srv/notes-service/current sudo -u notes readlink -f /srv/notes-service/current sudo ln -s releases/r002 /srv/notes-service/current.next sudo mv -Tf /srv/notes-service/current.next /srv/notes-service/current sudo systemctl restart notes-api notes-worker@fast notes-worker@bulk notes-maintenance@publisher notes-maintenance@recovery curl --fail --max-time 5 http://127.0.0.1:8000/ready ```` #### 4. 验证新版本并演练应用回退 使用真实创建读取、版本冲突和持久导出链路验收。若触发预先定义的错误率/延迟阈值且数据库仍兼容r001,则用同样原子软链方式切回r001并重启全部角色。不自动downgrade数据库,不删除新数据。 文件:终端 / 当前课时 ````bash # 仅在兼容性已验证且current为软链时回退 sudo ln -s releases/r001 /srv/notes-service/current.rollback sudo mv -Tf /srv/notes-service/current.rollback /srv/notes-service/current sudo systemctl restart notes-api notes-worker@fast notes-worker@bulk notes-maintenance@publisher notes-maintenance@recovery curl --fail --max-time 5 http://127.0.0.1:8000/ready ```` #### 5. 升级到两端口切换时先验证候选 复制API unit为notes-api-candidate,WorkingDirectory与venv绝对固定到r002,端口改8001。先直接访问8001/ready与业务探针,再把Nginx proxy_pass由8000改8001,nginx -t后reload。观察旧连接排空再停止旧服务;这一步是可选演练,不用current软链同时控制两个版本。 文件:终端 / 当前课时 ````bash curl --fail --max-time 5 http://127.0.0.1:8001/ready sudo nginx -t sudo systemctl reload nginx # 确认流量已切换并等待已定义排空窗口后,停止旧notes-api。 ```` ### 验收结果 - r001与r002是独立完整发布物,current切换前后路径可核对。 - 迁移失败停止发布,应用回退只在数据库兼容范围内执行。 - 实际进程已重启或代理已切换,版本验证不只检查软链。 ### 生产约束与故障演练 发布不仅换代码,还要协调数据库、队列协议和已有连接。 #### 实现决策 - 每版独立目录与依赖,迁移先兼容。 - 记录可回退版本和明确触发条件。 #### 隔离环境演练 在隔离VM让r002故意缺一个必要配置,观察失败后按兼容路径切回r001并验证数据。 #### 恢复验收 - 恢复耗时与业务验证有记录。 - 不兼容数据库变更明确阻止机械回退。 ### 常见错误 #### current已变但请求仍返回旧行为 原因:旧Python进程没有退出,仍持有旧代码。 处理:检查systemd MainPID与启动时间,执行受控重启或候选端口切换。 #### 旧worker无法读取新消息参数 原因:任务协议变更未考虑队列中的存量消息。 处理:保持稳定任务名与兼容参数,必要时用新版本队列并先排空旧队列。 ### 扩展练习 新版本删除了一列,发现问题后还能直接切r001吗? 提示:旧代码是否仍读取或写入该列? 参考答案: 通常不能保证。应采用expand-contract保留回退窗口;已执行不兼容删除时优先前向修复,或走经验证的数据恢复方案并评估数据损失,不把软链切回当完整回滚。 ### 技术文档 - [Alembic:迁移命令](https://alembic.sqlalchemy.org/en/latest/tutorial.html) - [Nginx:控制与reload](https://nginx.org/en/docs/control.html) - [systemd-run:一次性作业(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man1/systemd-run.1.html) --- ## 08 journald、日志轮转与可执行告警 建立从请求到任务的关联日志,控制磁盘占用,并定义能触发具体行动的监控条件。 ### 本节目标 - 用journalctl按服务和时间定位故障 - 限制journald与Nginx日志占用 - 区分存活、业务就绪与队列健康指标 ### 必要概念 #### 日志回答一次事件 API记录request_id、方法、路由、状态码与耗时,任务记录job_id、阶段、attempt与异常类别。数据库密码、Authorization、笔记正文和完整提示词不进普通日志。关联ID连接各进程信息,不靠长堆栈猜同一次请求。 #### 指标回答是否持续异常 错误率、延迟分位数、数据库池等待、最老outbox年龄和队列等待时间要看时间窗口。只看worker存活会漏掉队列不消费、publisher停滞和结果文件失效。告警需阈值、持续时间、负责人和runbook。 #### 日志本身会占资源 无限日志可填满磁盘导致数据库写入失败。journald设容量和保留期,Nginx使用发行版logrotate配置并核对新文件路径是否匹配;不要直接截断数据库或正在写入的日志文件。 ### 动手实现 #### 1. 按服务与时间查日志 这些命令只读。先定位首次异常时间和部署版本,再扩大范围;生产收集日志时脱敏并限制导出范围,不直接输出EnvironmentFile。 文件:终端 / 当前课时 ````bash sudo journalctl -u notes-api --since '15 minutes ago' --no-pager sudo journalctl -u notes-worker@fast -u notes-maintenance@publisher --since today --no-pager sudo systemctl show notes-api -p MainPID -p NRestarts -p ExecMainStatus sudo journalctl --disk-usage ```` #### 2. 配置journald容量 在独立VM新建drop-in。值为小型练习机起点,实际保留期由故障调查需求和磁盘预算决定;修改只限制未来保留行为,不能代替异机日志归档。 文件:/etc/systemd/journald.conf.d/notes-limits.conf ````ini [Journal] Storage=persistent SystemMaxUse=500M SystemKeepFree=1G MaxRetentionSec=14day ```` #### 3. 加载并检查Nginx轮转 先使用sudo install -d创建journald.conf.d目录,再保存配置并restart journald。Nginx发行版通常已有/var/log/nginx/*.log规则,确认覆盖notes-access/error;避免重复建立两个规则处理同一文件。logrotate -d仅调试,不执行轮转。 文件:终端 / 当前课时 ````bash sudo systemctl restart systemd-journald sudo journalctl --disk-usage sudo cat /etc/logrotate.d/nginx sudo logrotate -d /etc/logrotate.conf df -h /var /srv ```` #### 4. 定义服务指标与告警动作 把下列初始规则保存monitoring-runbook.md并按实际SLO调整。探针应在独立监控主机执行,避免本机整体宕机时连监控一起消失;课件没有配置真实通知渠道,不能声称告警已接通。 文件:monitoring-runbook.md ````text API就绪:/ready 连续3次失败(间隔10s)→ 检查依赖/发布,暂停继续放量 HTTP错误率:5分钟窗口5xx超过业务阈值 → 按request_id查首次异常 队列:最老queued/outbox等待超过任务SLO → 检查publisher、broker与worker 磁盘:剩余空间低于容量预算,且持续下降 → 排查日志/备份,禁止先删数据库文件 证书:剩余天数低于续期排障窗口 → 查看Certbot定时任务与验证路径 告警必须包含:环境、版本、首次时间、当前值、负责人、runbook链接 ```` #### 5. 用一次故障验证告警流程 隔离VM暂停publisher,提交一个持久导出任务,等待超过练习阈值,手工执行监控SQL观察最老未发事件。恢复publisher后应完成投递,记录从发现到恢复用时;真实通知渠道尚未接通则明确标记为手工演练。 文件:终端 / 当前课时 ````bash sudo systemctl stop notes-maintenance@publisher # 按任务模块提交一个新job,并查询export_outbox最老未发送时间 sudo systemctl start notes-maintenance@publisher sudo journalctl -u notes-maintenance@publisher --since '5 minutes ago' --no-pager ```` ### 验收结果 - 能够按request_id/job_id定位跨进程阶段,不打印业务正文与凭据。 - journald容量可查询,Nginx日志匹配一个有效轮转规则。 - 至少一个故障从指标触发到恢复完成演练,未接通通知的部分明确说明。 ### 生产约束与故障演练 故障必须在磁盘耗尽或用户集中反馈前被发现,并能关联到具体发布与任务。 #### 实现决策 - 告警对应可执行排查步骤。 - 日志容量和敏感字段有明确限制。 #### 隔离环境演练 暂停练习publisher形成积压,检查最老待发年龄,恢复后核对任务完成与指标下降。 #### 恢复验收 - 告警条件对应真实业务影响。 - 发现、定位、恢复耗时都有记录。 ### 常见错误 #### 接口变慢但没有报错日志 原因:日志只覆盖异常,未记录耗时和排队。 处理:加入请求/任务阶段耗时指标,区分数据库等待、执行与队列延迟。 #### 磁盘满后直接删除大文件仍未释放空间 原因:文件可能仍由进程持有,或误删了重要数据。 处理:先用lsof等确认持有者和文件类型,按服务支持方式轮转;不能删除PGDATA文件排空间。 ### 扩展练习 worker进程active就说明任务系统健康吗? 提示:任务经过哪些独立组件? 参考答案: 不说明。要同时检查outbox最老年龄、broker连通、队列等待、任务成功率和输出可读性;publisher停掉时worker仍可以一直active。 ### 技术文档 - [systemd:journalctl(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man1/journalctl.1.html) - [systemd:journald配置(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man5/journald.conf.5.html) - [Nginx:日志模块](https://nginx.org/en/docs/http/ngx_http_log_module.html) --- ## 09 数据库备份、异机加密与恢复目标 从一致性备份走到异机副本与隔离恢复,明确RPO和RTO是需要测量的目标。 ### 本节目标 - 创建可验证的PostgreSQL逻辑备份 - 对备份加密并传到获准的独立存储 - 测量恢复时间并核对应用级数据 ### 必要概念 #### RPO与RTO RPO描述可接受的数据回退时长,RTO描述可接受的恢复耗时。每天一次逻辑备份通常无法支持分钟级RPO,需WAL归档和PITR等方案。恢复时间包含取回备份、解密、建库、恢复、索引和业务验收,不只是pg_restore命令。 #### 权限与密钥分离 备份包含业务数据,应使用专用备份账号与最小读取权限,传输加密、静态加密、访问审计。解密私钥不能只放在同一台被备份主机,否则主机丢失后备份也无法解密。 #### 恢复隔离 恢复到新的notes_restore数据库或独立恢复主机,不能覆盖线上notes_lab来试验。行数、关键约束、迁移版本、任务状态与文件引用都要检查;数据库备份不自动包含shared/artifacts,输出文件需独立备份或可重建规则。 ### 动手实现 #### 1. 为练习备份建立目录与凭据文件 在VM创建root专用备份目录。使用.pgpass格式的/etc/notes-backup.pgpass保存专用备份凭据,权限600;示例可用练习app_user作为自己表的所有者读取,生产另建备份角色并验证所需权限。密码中的:与\要按libpq规则转义。 文件:终端 / 当前课时 ````bash sudo install -d -o root -g root -m 700 /var/backups/notes-service sudoedit /etc/notes-backup.pgpass # 文件格式:127.0.0.1:5433:notes_lab:BACKUP_USER:ESCAPED_PASSWORD sudo chown root:root /etc/notes-backup.pgpass sudo chmod 600 /etc/notes-backup.pgpass ```` #### 2. 生成备份并检查可读目录 下方命令中的BACKUP_USER替换为已验证的备份角色。使用PostgreSQL16客户端备份16服务器;Ubuntu24.04默认客户端通常为16,先检查。sudo env只指定凭据文件路径,命令行不写密码。备份文件名本次固定lab-01,存在时换新编号,不能覆盖唯一好备份。 文件:终端 / 当前课时 ````bash pg_dump --version sudo env PGPASSFILE=/etc/notes-backup.pgpass pg_dump -h 127.0.0.1 -p 5433 -U BACKUP_USER -d notes_lab -Fc --no-owner --no-acl -f /var/backups/notes-service/notes-lab-01.dump sudo pg_restore --list /var/backups/notes-service/notes-lab-01.dump sudo sha256sum /var/backups/notes-service/notes-lab-01.dump ```` #### 3. 使用接收方公钥加密 本步骤示例使用age,先按官方文档安装受信任版本。在恢复端生成并保管age私钥,备份VM仅保存接收方公钥;AGE_RECIPIENT替换真实公钥。此课不实际向外部发送数据,只有自己拥有且已获准的备份主机/存储才执行传输。 文件:终端 / 当前课时 ````bash # 备份VM,仅持有接收方公钥 sudo age -r AGE_RECIPIENT -o /var/backups/notes-service/notes-lab-01.dump.age /var/backups/notes-service/notes-lab-01.dump sudo sha256sum /var/backups/notes-service/notes-lab-01.dump.age # 使用已验证主机密钥的独立备份目标;替换主机、账号、路径后再执行 # scp notes-lab-01.dump.age backup@AUTHORIZED_BACKUP_HOST:/approved/notes-service/ ```` #### 4. 在独立恢复环境解密与恢复 只在独立恢复VM或数据库执行,先createdb创建notes_restore并验证名称。解密私钥文件必须具有合适权限;不把私钥写在命令参数正文。记录开始与结束时间,恢复账号需在该恢复库有建表权限。 文件:终端 / 当前课时 ````bash age -d -i /secure/recovery-age-key.txt -o notes-lab-01-restored.dump notes-lab-01.dump.age sha256sum notes-lab-01-restored.dump createdb -h RESTORE_HOST -U RESTORE_ADMIN -W notes_restore date -u pg_restore -h RESTORE_HOST -U RESTORE_ADMIN -W -d notes_restore --no-owner --no-acl --exit-on-error notes-lab-01-restored.dump date -u ```` #### 5. 核对恢复内容并计算差距 在恢复库执行下列SQL,再将隔离API指向恢复库完成读取与约束验收。不要让恢复API连接生产队列并重新执行旧任务;禁用外发或使用独立broker,处理已完成/未完成任务的恢复语义。 文件:终端 / 当前课时 ````sql SELECT current_database(); SELECT count(*) FROM notes; SELECT version_num FROM alembic_version; SELECT status, count(*) FROM export_jobs GROUP BY status; -- 另行核对shared/artifacts备份或对象存储中的result_ref文件是否存在。 ```` ### 验收结果 - 备份文件目录可读,解密后摘要与原始dump一致。 - 恢复发生在独立notes_restore,原数据库未覆盖。 - 记录实际恢复耗时、备份时间点及任务输出核验,能判断是否满足目标RPO/RTO。 ### 生产约束与故障演练 主机或数据库损坏后,需要在业务可接受时间内恢复数据且避免重复外部副作用。 #### 实现决策 - 备份异机加密,密钥另行保管。 - 恢复库和队列隔离,业务验收计入RTO。 #### 隔离环境演练 在独立恢复环境取回、解密、恢复一份练习备份,验证行数、约束、任务状态与输出文件。 #### 恢复验收 - 备份可恢复而非仅存在。 - 恢复不连接生产外发渠道。 ### 常见错误 #### 备份在但恢复时没有解密私钥 原因:密钥只存在已损坏的原主机。 处理:把恢复密钥放在独立受控保管位置并演练访问,备份机只需公钥。 #### 恢复数据库后任务重复发送邮件或调用外部接口 原因:恢复环境连上生产broker或仍启用外发调度。 处理:恢复环境使用独立依赖并禁用外发,先核对任务状态和恢复策略,再受控恢复业务。 ### 扩展练习 每天备份一次,能否承诺最多丢5分钟数据? 提示:最后成功备份到故障发生之间可能多长? 参考答案: 不能。要达到分钟级RPO,应设计并验证WAL归档/PITR或其他受控方案,同时监控归档延迟;逻辑备份仍可作为补充,但目标必须有实际恢复证据。 ### 技术文档 - [PostgreSQL16:备份恢复](https://www.postgresql.org/docs/16/backup.html) - [PostgreSQL16:持续归档与PITR](https://www.postgresql.org/docs/16/continuous-archiving.html) - [PostgreSQL16:密码文件](https://www.postgresql.org/docs/16/libpq-pgpass.html) - [age官方用法](https://github.com/FiloSottile/age) --- ## 10 完整发布、回退与故障 Runbook 把主机、代理、应用、数据库和任务链路组成可执行验收清单,记录恢复边界。 ### 本节目标 - 按层完成新版本发布验收 - 依据故障症状选择检查与恢复步骤 - 用隔离故障演练证明回退和备份方案 ### 必要概念 #### 验收从用户路径出发 /live只证明进程回应,/ready验证部分依赖,真正的验收要创建、读取、条件更新以及提交持久任务并读取结果。每项记录环境、版本、时间、预期与实际,不能把课件命令当作已执行证据。 #### 恢复先找边界 API代码错误可回退兼容版本;数据库锁等待需找阻塞事务;磁盘不足要处理明确的数据来源;队列积压先查publisher与worker。没有证据时重启所有服务可能放大故障并丢失现场。 #### 进程与Docker部署差异 systemd模式检查unit、绝对venv和主机端口;Docker模式检查镜像digest、Compose服务DNS、容器资源和卷。二者共享迁移、备份、幂等、代理与告警要求,不能因容器化省略数据库验收。 ### 动手实现 #### 1. 建立发布检查单 新建/srv/notes-service/shared/release-runbook.md供管理员维护,运行用户无需写此文件。填入真实版本、依赖地址和负责人,不在文档写秘密。 文件:release-runbook.md ````text 发布前:目标VM/域名核对;当前版本与revision记录;备份恢复证据;兼容窗口 发布准备:新目录/镜像验证;锁定依赖;配置权限;迁移SQL审核 执行:唯一迁移成功 → 启动候选/切换版本 → 业务探针 → 观察窗口 验收:API CRUD/version409;幂等重试;job/outbox/worker/结果文件一致 回退:触发条件、旧版本、数据库兼容限制、任务协议限制、恢复负责人 证据:命令结果、日志时间、指标曲线、未验证项目 ```` #### 2. 执行不带秘密的基础探针 在VM检查服务和磁盘,客户端检查真正入口。HTTPS仅在已配置自己域名时执行;未配置证书则通过本机HTTP Host探针,不使用-k冒充TLS成功。 文件:终端 / 当前课时 ````bash sudo systemctl is-active notes-api notes-worker@fast notes-worker@bulk notes-maintenance@publisher notes-maintenance@recovery nginx curl --fail --max-time 5 http://127.0.0.1:8000/ready curl --fail --max-time 10 https://YOUR_DOMAIN/ready df -h /srv /var sudo journalctl -u notes-api --since '5 minutes ago' --no-pager ```` #### 3. 执行数据库与任务业务验收 API+数据库最终脚本先验证创建、PATCH version与409;任务侧使用以下一次性systemd作业从真实notes表选一条并调用最终state_store.submit_snapshot。该函数包含job/outbox原子写入,不手工再delay。输出job_id后等worker完成,再在同一工作目录和环境运行verify_terminal.py JOB_ID,检查artifact与固定payload一致。 文件:终端 / 当前课时 ````bash sudo systemd-run --unit=notes-submit-probe --wait --collect --pipe \ --property=User=notes --property=Group=notes \ --property=WorkingDirectory=/srv/notes-service/current/tasks_lab \ --property=EnvironmentFile=/etc/notes-tasks.env \ /srv/notes-service/current/.venv/bin/python -c "from state_store import connect,submit_snapshot; conn=connect(); row=conn.execute('SELECT id FROM notes ORDER BY id LIMIT 1').fetchone(); conn.close(); assert row, 'create a note first'; print(submit_snapshot([row['id']]))" # 记录上一步实际job_id,完成后替换JOB_ID sudo systemd-run --unit=notes-verify-probe --wait --collect --pipe \ --property=User=notes --property=Group=notes \ --property=WorkingDirectory=/srv/notes-service/current/tasks_lab \ --property=EnvironmentFile=/etc/notes-tasks.env \ /srv/notes-service/current/.venv/bin/python verify_terminal.py JOB_ID ```` #### 4. 逐个注入隔离故障并恢复 一次只注入一个故障,先记录预期,完成恢复后再下一个。不在生产或唯一远程管理入口执行;SSH/UFW练习必须保留控制台和回退timer。 文件:终端 / 当前课时 ````text 故障 | 首先检查 | 预期恢复 API停止 | systemctl status / Nginx上游日志 | 启动已验证版本,/ready恢复 错误DB凭据 | API异常类别 / 脱敏配置来源 | 修正配置并重启对应进程 publisher停止 | 最老outbox年龄 / unit状态 | 启动publisher,任务完成 worker超时退出 | job租约 / attempts / recovery日志 | 过期恢复,幂等处理 新版本不兼容 | revision与旧版兼容表 | 停止机械回退,前向修复或恢复计划 备份恢复 | 独立恢复库 / 解密与结果引用 | 满足已测RPO/RTO ```` #### 5. 对比Docker部署同一验收 若选择Docker路线,API仍由Nginx代理回环发布端口;用下列只读命令替代unit检查。不得同时让systemd API与Compose API争抢8000,先明确使用哪条运行主线。最终报告列出已实机验证和未验证项目。 文件:终端 / 当前课时 ````bash docker compose --env-file .env.compose ps -a docker compose --env-file .env.compose logs --tail 100 api worker worker-bulk publisher recovery docker compose --env-file .env.compose images docker compose --env-file .env.compose stats --no-stream ```` ### 验收结果 - 有一次完整发布与兼容回退的实际执行记录,而不只是计划。 - API与任务验收核对数据库最终状态和输出文件,失败后恢复验证完成。 - 未进行的远程部署、证书、异机备份或告警通知明确标记未验证。 ### 生产约束与故障演练 学员需要独立发布一个可排错、可回退、可恢复的后端服务。 #### 实现决策 - 验收覆盖用户路径与持久状态。 - 故障按层定位,恢复动作最小且有证据。 #### 隔离环境演练 在专用VM完成API停止、publisher停止和兼容版本回退三项,记录发现、恢复时间与数据结果;剩余矩阵逐项标记。 #### 恢复验收 - 有可交付runbook和执行证据。 - 没有把未实机验证的命令称为已部署成功。 ### 常见错误 #### 所有组件进程都active但用户任务一直queued 原因:存活检查未覆盖业务链路与积压年龄。 处理:核对job/outbox、publisher投递、队列路由和worker注册任务,最后验证result_ref。 #### 为了快速恢复反复重启所有组件 原因:未区分故障层,重启破坏现场并触发重复任务。 处理:一次验证一个假设,先记录错误、版本和状态,再执行最小恢复动作。 ### 扩展练习 向同事交付部署方案时,至少需要哪些可审查证据? 提示:让对方不依赖你的终端历史也能复现。 参考答案: 提供版本/镜像标识、依赖锁、配置项名称与权限、迁移状态、探针结果、任务最终状态、日志时间、回退边界和恢复耗时。秘密只给受控存储位置,不复制值。未执行的步骤标为待验证。 ### 技术文档 - [Ubuntu Server文档](https://documentation.ubuntu.com/server/) - [systemd:systemctl(Ubuntu 24.04)](https://manpages.ubuntu.com/manpages/noble/man1/systemctl.1.html) - [Docker:生产Compose](https://docs.docker.com/compose/how-tos/production/)