后端自定义管理命令
本文档记录当前后端项目中的自定义 Django management commands。命令均在 back_end/saolei 目录下执行:
python manage.py <command>本地公开数据快照
dangerzone.init_local_test(独立脚本)
下载和初始化入口分别位于 dangerzone/download_public_data.py、dangerzone/init_local_test.py。在 back_end/saolei 目录使用 python -m 启动,后端根目录不再保留同名入口脚本。
仅用于替换本地测试数据,会清空本地数据库和 saolei_website 对应的 Redis 数据库。此功能不注册为 Django 管理命令,manage.py 不提供 import_public_data。执行前停止本地后台 worker、定时任务和其他写入;不需要启动 HTTP 服务。要求不存在项目配置的 .production 标记、DEBUG=True、E2E_TEST=True,数据库及 Redis 均为回环地址。即使误开调试选项,存在生产标记仍会拒绝导入。正式导入前检查快照校验和、引用关系和数据库迁移状态。
# 仅 GET 生产公开 API,每次响应完成后至少等待 1.25 秒;失败后重复执行可续传。
python -m dangerzone.download_public_data
# detailbulk 不可用时,使用已有详情及公开录像列表完成快照:
python -m dangerzone.download_public_data --skip-details
python manage.py migrate
# 读取快照,清库、导入,并创建当天经典周赛。
python -m dangerzone.init_local_test
# 只导入,不创建周赛:
python -m dangerzone.init_local_test --no-weekly默认快照目录是 back_end/saolei/tmp/public-data,已由 Git 忽略。下载脚本使用 --output-dir,导入脚本使用 --snapshot-dir 指定其他目录。已完成的快照不会自动重新下载,获取新快照请换一个目录。
下载通过 infoupdated 获取用户 ID,调用 userprofile/infobulk 下载用户资料,通过每个用户的 videolist 建立公开录像索引,再调用 video/detailbulk 下载详情。保留 ID 空洞,不会因空页提前停止。索引后被删除或变成不可见的录像列入 manifest.json 的 unavailable_video_ids。这不是跨请求一致的数据库备份。TLS 使用 certifi 的 CA 包验证,不跳过证书检查;遇到 429 或服务端临时错误会等待重试。
请求串行执行,上一响应完成后至少等待 1.25 秒再发送下一请求,主动控制 videolist 在每秒一次以内。--interval 可增加间隔,不能低于 1.25 秒;慢请求结束后也不会立即发送下一个请求。
--skip-details 不请求 detailbulk,已有的详情仍然优先使用,其他录像直接使用 get_user_videos 的列表数据;只有列表数据的录像列入 missing_detail_video_ids。可空的缺失指标保持 NULL,录像内标识为空字符串。列表里的 cl、ce 不能直接写入生成列,缺少分项点击数时,相关生成值也为 NULL。生产列表已返回 right_ce,列表回退也可以按 right_ce == 0 重建 NF 榜;详情缺少该字段或为 NULL 时保留列表值。旧快照缺失该值时保持 NULL,不凭旧模式编码推测。完成的快照不会重新下载,补充新字段或详情请使用新的 --output-dir 下载,再通过 --snapshot-dir 导入。
导入保留原用户/录像 ID、上传时间和公开属性,旧 NF 模式 12 转成 STD 00,不修改 right_ce。批量写入不会触发录像信号,随后重建录像计数、上传额度、扫雷网普通/NF 榜、PB 普通/NF 榜、top 普通/NF 榜及锚点、效率 Thrp 榜、pluck 纪录缓存和录像状态队列,不创建 pluck 计算任务。
- 管理员:ID
2,密码admin123456;普通账号:ID48,密码user123456。用户名保留生产数据;可通过--admin-password/--user-password指定其他密码。 - 其他用户禁用密码登录,且所有用户中仅 ID
2获得本地is_staff权限;邮箱统一为user-{id}@example.invalid。生产账号不受影响。 - 这两个公开详情 API 不提供原密码、邮箱、录像文件、头像文件、标识绑定和比赛关联;不会推测这些数据。录像可以用于列表和排行测试,但不能在本地播放或重新解析。录像计数只能基于本次导入的公开录像计算,无法排除缺失比赛关联的已公开比赛录像。
is_lucky当前未由这两个详情 API 提供,导入保留模型默认值。已有pluck从公开录像列表补齐,缺失时保持 NULL,不重新解析录像。- 再次初始化会丢弃之前的本地数据;应使用独立的本地数据库及 Redis 数据库。不要对生产配置运行这些命令。
缓存重建
rebuild_eff_ranks
位置:effranking/management/commands/rebuild_eff_ranks.py
从录像数据库按玩家分段重建效率 Thrp 榜的个人纪录 hash 和 b/i/e/sum 四个 zset。--ranking-name 当前仅支持 thrp,默认即为该榜;--batch-size 默认 1000,必须为正数。
python manage.py rebuild_eff_ranks --ranking-name thrp --batch-size 1000执行前暂停录像上传、审核、修改、删除、标识绑定/解绑、比赛公开等写入。命令在独立临时命名空间构建全部五个 key,成功后用一个 Redis 事务原子替换正式榜;空的小榜删除对应旧 key,构建失败保留正式榜。结束时清理已知临时 key,不扫描其他 Redis 数据。不会修改录像数据库或其他排行榜。
首次部署先完成数据库迁移,再执行建榜;API 不回源数据库。后台刷新入口和管理员修复不能替代首次完整建榜。本地 python -m dangerzone.init_local_test 已自动执行重建,--no-weekly 同样会执行。单用户修复使用 POST /api/effranking/thrp/admin/rebuild_record,其中 sum 仅使用缓存中的三个原始单项计算。具体规则见 back_end/saolei/effranking/README.md。
rebuild_speed_ranks
位置:speedranking/management/commands/rebuild_speed_ranks.py
从 VideoModel 按玩家分段重建竞速排行榜。--ranking-name 可选 saolei 或 saolei_nf,省略则重建两者;--batch-size 默认 1000,必须为正数。
python manage.py rebuild_speed_ranks
python manage.py rebuild_speed_ranks --ranking-name saolei_nf --batch-size 1000执行前暂停录像上传、修改、删除、标识绑定/解绑及比赛公开等写入。命令先构建临时榜,成功后原子发布单个大榜,最后清理临时缓存。构建失败保留该大榜的原缓存;两个大榜依次替换。排行 API 不自动回源,因此首次部署及缓存丢失后都需要执行此命令。重建不恢复旧新闻。
从旧的“微秒时间戳 + 玩家 id” member 升级为纯玩家 id、分钟级合成 score 时,应暂停相关读写,以新代码执行不带 --ranking-name 的重建命令,两个大榜都完成后再恢复服务。超出编码范围的单项或总值仍保存在个人纪录 hash,仅不写入对应 zset;总榜独立判断,不修改录像数据库和个人最佳选取规则。
rebuild_pb_ranks
位置:speedranking/management/commands/rebuild_pb_ranks.py
从录像数据库重建 PB 普通/NF 榜、用户 rank hash 和小榜人数 hash。--batch-size 默认 100,必须为正数;按玩家 id 分段,每批输出累计用户数和最后玩家 id,最后逐小榜刷新排名并同步人数。清空 PB 命名空间时也会删除旧人数 hash;重建后未出现的小榜按 0 人处理。
python manage.py rebuild_pb_ranks --batch-size 100首次部署及 PB 缓存丢失后需要手动执行,API 不回源。执行前暂停 PB 相关读写,包括上传、审核、绑定/解绑、删除及比赛公开。命令直接清空 PB 命名空间后重建,不使用临时榜或原子发布;失败会记录日志并中断,可能留下部分缓存,排除错误后重新运行。录像数据库和扫雷网缓存不受影响。rebuild_speed_ranks 仍仅重建扫雷网规则。
本地快照初始化 python -m dangerzone.init_local_test 已在扫雷网重建后自动调用 PB 重建,使用 --no-weekly 时也会执行,无需另外运行上述命令。
rebuild_top_ranks
位置:speedranking/management/commands/rebuild_top_ranks.py
从录像数据库按玩家分段构建 top 的十八个小榜、前二十名均值锚点及九个软实力/LSF 派生榜。--batch-size 默认 100,必须为正数;每批输出累计用户数和最后玩家 id。
python manage.py rebuild_top_ranks --batch-size 100命令使用独立临时命名空间,构建明细、全部索引与锚点后,用一个 Redis 事务原子切换正式榜的三十个 key。构建失败保留旧榜,正常退出会清理临时 key;强制终止可能遗留临时数据。命令持单机独占锁:等待在途写操作完成,相关新 HTTP 写请求返回带原因的 503,后台业务等待后继续,旧榜只读服务继续。不需要停整个网站,但所有进程必须共享可写的 TOP_RANKING_LOCK_PATH(默认 BASE_DIR/runtime/top-lock.sqlite3);不要删除活动锁文件。自定义写入脚本也必须遵守准入。此命令不修改数据库,也不清理扫雷网或 PB 缓存。
首次部署和缓存丢失后手动执行,查询不自动回源。本地 python -m dangerzone.init_local_test 已自动调用此命令的重建服务,--no-weekly 不影响重建。单用户小榜修复使用 POST /api/speedranking/top/admin/rebuild_record。具体缓存与单位约定统一见 back_end/saolei/speedranking/README.md。
rebuild_top_soft_ranks
位置:speedranking/management/commands/rebuild_top_soft_ranks.py
仅从现有十八个原始 top zset 和实时锚点重建全部九个软实力/LSF 索引,发布相应锚点快照、版本和更新时间;不查录像数据库、不重新选取前五,也不修复原始榜。与后台刷新共用独占准入和临时榜发布逻辑,失败保留旧版并记录日志。首次部署或升级原始 score 编码应使用 rebuild_top_ranks,不能只运行此命令。
python manage.py rebuild_top_soft_ranksrebuild_tournament_cache
位置:tournament/management/commands/rebuild_tournament_cache.py
用途:重建比赛 Redis 缓存,包括当前 NORMAL 状态的比赛和这些比赛的参赛关系。
主要行为:
- 清空
tournament:normal和tournament:normal:participants。 - 读取
NORMAL状态的GSCTournament和WeeklyTournament。 - 将比赛基础信息写入
tournament:normal。 - 将当前
NORMAL比赛的参赛关系按用户分组写入tournament:normal:participants。
常用命令:
python manage.py rebuild_tournament_cacherebuild_tournament_user_cache
位置:tournament/management/commands/rebuild_tournament_user_cache.py
用途:重建 TournamentUser 的 Redis 排行缓存。
当前积分统一折算到当年 1 月 1 日 00:00 UTC,其余字段按数据库值写入;数据库记录不变。重建会同时清理旧的 7 个 zset 和时间基准 key。重建期间暂停积分结算及其他排行缓存写入;如果只需修复当前积分排序,使用下面的 rebuild_tournament_current_score_cache。
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--batch-size | 1000 | 每批处理的缓存行数 |
常用命令:
python manage.py rebuild_tournament_user_cache
python manage.py rebuild_tournament_user_cache --batch-size 500rebuild_tournament_current_score_cache
位置:tournament/management/commands/rebuild_tournament_current_score_cache.py
用途:从 TournamentUser.score_current / last_updated 重算当前积分 zset,统一使用当年 1 月 1 日 00:00 UTC 锚点。不修改数据库,不重放积分发放,不影响另外 6 个排行 zset。
积分折算沿用两年(730 天)减半的公式,允许负时间差:最后计分时间晚于年初时,反向折算会放大分数。所有用户使用同一锚点,排序等价于实时衰减后的排序。API 仍返回数据库原始积分与 last_updated,前端实时显示不变。
重算先将删除、锚点设置和分批写入加入 Redis 事务,完成后原子替换。期间读请求仍读取旧排行;并发缓存写入或锚点变化会触发重试。为避免读取尚未提交的数据库积分,重算期间应暂停积分结算和历史积分修正任务。空表同样清理旧排行并建立年初锚点。重复执行不会累计衰减。
| 参数 | 默认值 | 说明 |
|---|---|---|
--batch-size | 1000 | 每批读取的用户数,必须大于 0;各批最终在同一 Redis 事务提交 |
python manage.py rebuild_tournament_current_score_cache
python manage.py rebuild_tournament_current_score_cache --batch-size 500部署年度锚点方案后先执行一次此命令,修复已有当前积分缓存,并重启 runapscheduler 注册年度任务。若其他 6 个排行也需要重建,则改用 rebuild_tournament_user_cache。
rebuild_custom_pluck_cache
位置:customranking/management/commands/rebuild_custom_pluck_cache.py
用途:重建自定义 pLuck 排行的 Redis 缓存。
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--level | 无 | 只重建指定自定义级别;省略则重建全部配置 |
--batch-size | 1000 | 每批写入 Redis 的纪录数量 |
当前 --level 的合法值来自 CUSTOM_PLUCK_CONFIGS:
| 参数值 | 配置 |
|---|---|
c8_8_40 | 8x8 40 雷 |
c16_16_100 | 16x16 100 雷 |
c16_30_150 | 16x30 150 雷 |
c24_30_200 | 24x30 200 雷 |
常用命令:
python manage.py rebuild_custom_pluck_cache
python manage.py rebuild_custom_pluck_cache --level c16_30_150
python manage.py rebuild_custom_pluck_cache --batch-size 500数据刷新
refresh_videos
位置:videomanager/management/commands/refresh_videos.py
用途:替代旧 refresh_stnb 的录像重解析入口,对指定 ID 范围内的 VideoModel 实例逐条调用 videomanager.view_utils.refresh_video,默认刷新全部录像。
- 按主键顺序遍历,不限制状态、模式、级别或比赛标记。
- 使用
iterator()遍历录像 ID,避免 QuerySet 缓存所有实例;写入仍逐条执行,不使用update()或bulk_update()。 - 每条录像使用独立的
transaction.atomic(),通过select_for_update()加锁并重新读取该条主表记录后执行刷新,不连带锁住查询关联的用户等记录。解析与主表、扩展表保存均在该事务内;完成或失败后释放锁,不对全部录像使用一个长事务。其他事务修改或删除同一主表记录时会等待,因此耗时解析期间仍可能阻塞该条录像的写入。 - 沿用
refresh_video的差异保存逻辑,通过save(update_fields=...)触发信号;没有变化的字段不会强制保存。 - 已知的录像解析异常会包装为
VideoParseError,报告录像 ID 并跳过,继续刷新后续录像;结束时汇总成功和跳过数量。 - 数据库、文件读写、保存或信号接收器等其他错误会停止命令;当前条的数据库写入回滚,此前完成的刷新不会整体回滚。不会把刷新全过程中的
ValueError等异常都视为解析错误。事务不回滚已经直接写入 Redis 的副作用。 - 此命令不是无条件的排行榜或缓存全量重建。刷新模式等字段后如需重算录像计数,仍使用
refresh_video_counts。 - 每处理
step条录像输出并立即刷新进度,包括已处理数量、成功数量、跳过数量和当前录像 ID。解析失败也计入已处理数量;不足step的剩余部分在最终汇总中体现。
| 参数 | 默认值 | 说明 |
|---|---|---|
--start | 0 | 起始录像 ID,包含,必须非负 |
--end | 不限制 | 结束录像 ID,不包含,不能小于 start |
--step | 100 | 每处理多少条录像输出一次进度,必须为正整数 |
区间采用 [start, end),按录像 ID 筛选而非列表偏移;ID 存在空缺时,step 仍按实际处理条数计数。可以仅指定一个边界,或使用相邻区间连续分段刷新。
python manage.py refresh_videos
python manage.py refresh_videos --start 10000 --end 20000 --step 50
python manage.py refresh_videos --start 20000 --end 30000 --step 50refresh_video_counts
位置:msuser/management/commands/refresh_video_counts.py
用途:刷新全部 UserMS 的录像总数,以及各级别、模式的录像计数。
- 排除
ongoing_tournament=True的录像,以及通过Tournament.videos关联的所有比赛录像(包括已结束比赛)。 - 普通录像不按审核状态筛选;没有普通录像的用户计数归零。
- 按用户分批聚合并写入,不修改
video_num_limit或个人纪录,也不更新 Redis。 - 可重复执行。为避免与上传、删除并发造成计数覆盖,执行时应暂停录像写入。
| 参数 | 默认值 | 说明 |
|---|---|---|
--batch-size | 1000 | 每批处理的用户数量,必须为正整数 |
python manage.py refresh_video_counts
python manage.py refresh_video_counts --batch-size 500refresh_tournament_scores
位置:tournament/management/commands/refresh_tournament_scores.py
用途:同步重算指定已颁奖比赛的成绩、排名、积分和参赛用户的历史最佳成绩,无需后台 worker。支持 GSC 和经典周赛。
- 参数为比赛 ID,不是 GSC 届数或周数;只接受已颁奖、有结束时间的 GSC 或经典周赛。
- 从关联的有效录像重新计算成绩,再刷新
rank和rank_score。缺少的成绩恢复默认值,不保留已失效录像的旧成绩。 - 按新旧
rank_score的差额修正历史积分总量,不重复累计。当前积分加入从该届结束时间衰减到last_updated的差额;若该届结束得更晚,则先将原当前积分衰减到该届结束时间。 last_updated表示最后结束的比赛时间,不因重算历史比赛而回退。积分及个人历史最佳成绩的 Redis 缓存同步刷新;历史最佳成绩支持变好和变差。- 不删除参赛者、不修改比赛状态、不公开录像、不重新解析录像文件。
- 执行期间应避免同时修改该届的录像或参赛者,并暂停涉及这些用户的积分结算。该届的数据库写入在一个事务内,中途失败会回滚,可在排除故障后重跑;Redis 已写入的内容不会随数据库回滚,失败后需重跑以恢复缓存一致性。
| 参数 | 默认值 | 说明 |
|---|---|---|
tournament_id | 必填 | 比赛 ID,不是 GSC 届数或周数 |
--batch-size | 1000 | 批量写入大小,必须为正整数 |
python manage.py refresh_tournament_scores 123
python manage.py refresh_tournament_scores 123 --batch-size 500refresh_tournament_user_stats
位置:tournament/management/commands/refresh_tournament_user_stats.py
用途:刷新 TournamentUser 的历史 total 和 best 字段,不修改实时 score_current。
主要行为:
- 根据已有
TournamentUser和已颁奖比赛的参赛关系补齐缺失的TournamentUser。 - 刷新比赛积分历史总分。
- 刷新金羊杯和打卡赛历史最好成绩。
- 将更新后的历史字段同步到 Redis 缓存。
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--batch-size | 1000 | 批量创建、批量更新和缓存写入的批大小 |
常用命令:
python manage.py refresh_tournament_user_stats
python manage.py refresh_tournament_user_stats --batch-size 500缓存清理
delete_legacy_speedranking_cache
位置:common/management/commands/delete_legacy_speedranking_cache.py
用途:竞速排行榜重构第一步中,清理旧排行榜和新闻的 Redis key。应在停用旧功能的写入逻辑后执行,否则旧代码会重新生成缓存。
- 清理
player_{stat}_{mode}_{用户ID}和player_{stat}_{mode}_ids:指标限于timems、bvs、stnb、ioe、path,模式限于std、nf、ng、dg。 - 清理
news_queue,不依赖该key是list还是zset。 - 保留自定义pluck排行、比赛缓存、录像队列及其他Redis数据,不修改数据库。
- 使用
saolei_websiteRedis连接,通过SCAN遍历,每批最多1000个key,用UNLINK删除。可重复执行。
python manage.py delete_legacy_speedranking_cache --dry-run
python manage.py delete_legacy_speedranking_cache--dry-run仅列出匹配的key并统计数量,不删除数据。
delete_newest_queue
位置:videomanager/management/commands/delete_newest_queue.py
用途:调用 videomanager.services.delete_newest_queue,清理 Redis 最新录像队列。
队列不超过 100 条时不处理;超过 100 条时删除所有超过 7 天的记录,因此清理后可能少于 100 条。该逻辑也由 runapscheduler 每天 01:08 调用。
常用命令:
python manage.py delete_newest_queue后台任务与定时任务
db_worker_robust
位置:common/management/commands/db_worker_robust.py
用途:安全启动 django-tasks-db worker。启动前会处理孤儿 RUNNING 任务,并通过 pidfile 防止重复 worker。
主要行为:
- 使用 pidfile 防止重复启动。
- 检测当前是否已有
db_worker或db_worker_robust进程。 - 若没有正在运行的 worker,则将遗留的
RUNNING任务标记为FAILED。 - 调用 Django 内置的
db_worker开始处理任务。
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--queue-name | 默认任务队列 | 要处理的队列;多个队列用逗号分隔,* 表示全部 |
--interval | 2 | worker 轮询间隔,单位秒 |
--backend | 默认任务后端 | 使用的 task backend |
--batch | False | 传递给 db_worker 的批处理模式 |
--no-startup-delay | False | 关闭 db_worker 的启动延迟 |
--max-tasks | 无 | 最多处理的任务数 |
--worker-id | 自动生成 | 覆盖自动生成的 worker id |
--pidfile | logs/db_worker_robust.pid | 用于防止重复启动的 pidfile 路径 |
常用命令:
python manage.py db_worker_robust
python manage.py db_worker_robust --queue-name default --interval 2生产启动:
start.sh通过nohup bash run_background.sh db-worker启动,日志写入logs/db_worker.log。START_DB_WORKER=0可跳过启动;DB_WORKER_START_DELAY、DB_WORKER_NICE、DB_WORKER_INTERVAL默认分别为20、10、2。- 该命令的恢复逻辑仅在启动时处理遗留任务,运行期间退出后不会自动重启。
runapscheduler
位置:common/management/commands/runapscheduler.py
用途:在 common APP 的单一常驻进程中运行定时清理和比赛当前积分年度锚点更新。
任务注册位置:common/apscheduler.py
定时任务:
| 任务 | 频率 | 说明 |
|---|---|---|
delete_old_job_executions | 每周一 00:03 | 清理旧的 APScheduler job execution 记录 |
delete_newest_queue | 每天 01:08 | 队列超过 100 条时删除超过 7 天的记录 |
delete_freezed_video | 每天 01:28 | 删除 7 天以前冻结状态的录像 |
delete_overdue_emailverifyrecord | 每周一 01:03 | 清理 1 小时以前的邮箱验证码 |
delete_overdue_captcha | 每周一 01:05 | 清理过期图形验证码 |
refresh_tournament_score_reference | 每年 1 月 1 日 00:00 UTC | 将当前积分 zset 整体折算到新年年初,并原子更新锚点;数据库不变 |
年度锚点任务还会在调度器启动时执行一次幂等检查,补上跨年停机或错过执行时间的更新;重复检查同一年不会再次衰减。普通结算只读取已有锚点,不会根据比赛结束时间或当前年份自行推进,因此调度器暂时停机也不会混用不同时间基准。此任务设置 coalesce=True、misfire_grace_time=None 和单实例运行。
旧的 World 监控页面和 CPU/网络 IO 采集任务已移除;monitor APP 改为承载服务器统计 Ninja API,不再注册定时采集。新增 /api/monitor/memoryusage 仅在请求缓存未命中时采集,完整结果缓存 300 秒,详见服务器内存统计。runapscheduler 启动注册任务时会删除数据库中残留的 refresh_state_always 任务,避免任务存储恢复已删除的采集函数;其余五项清理任务保持运行。更新后需重启原 APScheduler 进程。
参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--pidfile | logs/apscheduler.pid | 用于防止重复启动 APScheduler 进程的 pidfile 路径 |
常用命令:
python manage.py runapscheduler
python manage.py runapscheduler --pidfile logs/apscheduler.pid生产启动:
start.sh会在数据库迁移完成后,通过nohup bash run_background.sh apscheduler启动该命令,日志写入logs/apscheduler.log。START_APSCHEDULER=0可跳过启动 APScheduler。APSCHEDULER_START_DELAY控制启动延迟,默认10秒。APSCHEDULER_NICE控制进程 nice 值,默认10。
WARNING
生产环境只应运行一个 APScheduler 进程。旧的 runapschedulermonitor、runapscheduleruserprofile 和 runapschedulervideomanager 命令已合并到 runapscheduler,不应再单独启动。
后台进程退出排查
run_background.sh 保留启动延迟和进程优先级设置,使用 nohup 忽略挂断信号,并将标准输入重定向到 /dev/null。Python 使用 -u 即时输出日志,使用 -X faulthandler 输出部分原生崩溃的 traceback。
启动日志包含时间、launcher PID 和 Python 进程 PID。子进程结束后,launcher 会记录 exit_status 和对应的信号名称。exit_status=0 表示正常退出;非零状态需要结合此前日志分析。大于 128 的状态通常表示信号终止,例如 137/KILL、143/TERM。worker 会处理 SIGTERM 并可能以状态 0 退出,因此状态 0 不能排除外部停止,需要查看此前的停止日志。仅凭 137 不能认定 OOM,也可能是手动或脚本发送了 SIGKILL。stop.sh 当前使用 SIGKILL 停止这两个服务。
Python 无法记录 SIGKILL;若 launcher 也被杀死,退出记录同样可能缺失。挂断保护不防止 systemd 会话清理等机制终止进程。这时需要对照退出时段的系统日志:
tail -n 100 logs/apscheduler.log
tail -n 100 logs/db_worker.log
sudo journalctl -k --since today --no-pager | grep -Ei 'oom|out of memory|killed process|segfault'
sudo journalctl -u systemd-logind --since today --no-pager应用的部分日志还会写入 logs/root.log 及各 APP 的日志文件;服务日志为空不代表这些文件也没有记录。
维护建议
- 新增管理命令后,应同步更新本文档。
- 改动缓存结构后,应检查对应的重建命令是否仍能从数据库恢复 Redis 状态。
- 全量刷新类命令应在低流量时执行,并在执行前确认是否需要备份。
- 定时任务命令是常驻进程,生产环境应由进程管理工具启动和守护。
