恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

从零搭建多模块常驻进程服务骨架:进程守护与优雅退出实践

  • 首页
  • 资讯中心
  • /
  • 从零搭建多模块常驻进程服务骨架:进程守护与优雅退出实践

相关资讯

【会议纪要哪个软件用户体验好】实测6款主流工具,从2小时会议到5分钟出稿 2026/10/16 3:17:21
用AI生成抗日名将36师卡通图,感觉效果还挺好的。 2026/10/16 3:17:21
基于YOLOv11深度学习的口腔龋齿图像识别系统(Python+PyTorch+PyQt5)| 毕业设计 源码+论文+完整教程 2026/10/16 3:17:21

最新资讯

OpenClaw安卓移植实战:开源重制引擎搭建与触屏调优指南
Redis集群一键自动部署脚本:CentOS三主三从搭建与避坑指南
VS2019命令行编译MFC:从IDE到cl.exe/link.exe手工构建
刷题第5天:动态规划、链表与哈希表的踩坑复盘与节奏管理
基于微积分原理的机器学习梯度计算实操:导数、梯度消失与数值稳定性算法调试
React Native鸿蒙化实战:react-native-webview集成与踩坑全解析

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

从零搭建多模块常驻进程服务骨架:进程守护与优雅退出实践

发布时间:2026/10/16 3:22:21
从零搭建多模块常驻进程服务骨架:进程守护与优雅退出实践 简介这份资源围绕「进程常驻」这一系统与服务管理主题展开面向Android及后端开发中需要保障服务长期稳定运行的开发者内含多个可独立运行的module工程用于对比实践不同的保活与优先级调整思路。压缩包共69个文件约16.9MB以20个java源码、18个xml配置、20个png截图为主辅以aidl跨进程接口、gradle构建脚本与proguard混淆规则便于直接导入Android Studio编译调试。资源中两个工程分别对应不同实现路径涵盖守护进程、服务管理器、启动脚本及Supervisor、NSSM等第三方工具的思路并涉及nice值与Windows优先级类的调度调整同时提示日志监控、异常检测与自动重启等健壮性设计。目前已有319人学习适合希望系统理解进程保活原理、对照源码验证方案的进阶开发者参考。1. 进程常驻探索从零搭一个带多个 module 的 service 骨架很多人第一次写常驻进程都是从一个while(true)加sleep开始的跑起来看着没问题真扔到线上跑两天就发现内存悄悄涨、日志把磁盘写满、进程挂了没人拉。这份资源围绕「进程常驻」这个场景给了一套带多个 module 的 service 骨架把进程守护、模块拆分、日志轮转、优雅退出这些常驻服务绕不开的事都摆了出来。它适合两类人一类是刚接触后台服务、想搞明白一个常驻进程到底该长什么样的新手另一类是手里已经有一堆脚本、想整理成规范 service 结构的熟手。下面按「它是什么 → 怎么跑起来 → 怎么改」的顺序拆一遍。2. 架构与目录多个 module 是怎么挂到一个 service 上的2.1 常驻进程的核心结构常驻进程和普通脚本最大的区别在于它的生命周期不是「跑完就退」而是「一直活着随时响应」。这就带来三个必须解决的问题进程不能因为单个模块异常就整体挂掉、模块之间要能独立启停、退出时要把资源收干净。这份资源用的是一个主进程加多个 module 的结构主进程负责调度和守护每个 module 是一个独立的功能单元通过统一的注册接口挂进来。常见做法是主进程里维护一个 module 列表启动时逐个初始化运行期用一个事件循环驱动退出时反向逐个关闭。这样单个 module 抛异常可以被捕获并记录不至于把整个 service 带崩。下面是一个典型的 module 注册与调度骨架# main_service.py import time import signal import logging from modules import load_modules logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(name)s: %(message)s ) logger logging.getLogger(service) class Service: def __init__(self): self.modules load_modules() # 扫描并实例化所有 module self.running True def start(self): for m in self.modules: m.setup() # 每个 module 自己初始化资源 logger.info(module %s started, m.name) def run(self): while self.running: for m in self.modules: try: m.tick() # 单次调度异常隔离在这一层 except Exception: logger.exception(module %s tick failed, m.name) time.sleep(1) # 调度间隔按业务调整 def stop(self): self.running False for m in reversed(self.modules): m.teardown() # 反向关闭先起的后关 logger.info(module %s stopped, m.name) def handle_signal(signum, frame): logger.info(received signal %s, shutting down, signum) service.stop() if __name__ __main__: service Service() signal.signal(signal.SIGTERM, handle_signal) signal.signal(signal.SIGINT, handle_signal) service.start() service.run()这段代码里几个点值得说清楚。load_modules()负责扫描 module 目录把每个模块实例化后返回列表这样加新功能只要丢一个文件进去不用改主进程。tick()是每个 module 的单次调度入口用try/except包住保证一个模块出错不影响其他模块。signal部分注册了SIGTERM和SIGINT收到退出信号时走stop()把每个 module 的teardown()反向执行一遍这就是优雅退出的基本盘。time.sleep(1)是调度间隔实际项目里如果模块对实时性要求高会换成事件驱动或者更短的间隔。2.2 目录结构与 module 约定多个 module 的项目目录结构如果不统一后面维护会很痛苦。这份资源用的是「一个 module 一个目录、统一入口文件」的约定结构大致如下service/ ├── main_service.py # 主进程入口 ├── config/ │ └── settings.yaml # 全局配置 ├── modules/ │ ├── __init__.py # load_modules 实现 │ ├── collector/ # 采集类 module │ │ ├── __init__.py │ │ └── module.py │ ├── processor/ # 处理类 module │ │ ├── __init__.py │ │ └── module.py │ └── reporter/ # 上报类 module │ ├── __init__.py │ └── module.py └── logs/ └── service.log每个 module 目录下的module.py必须实现三个方法setup()、tick()、teardown()再加一个name属性。load_modules()就是遍历modules/下的子目录找到module.py并实例化。这种约定的好处是模块之间完全解耦坏处是每个模块都得写一遍样板代码所以常见做法是抽一个BaseModule基类把空实现和日志初始化放进去子类只覆盖自己关心的方法。# modules/base.py import logging class BaseModule: name base def __init__(self, configNone): self.config config or {} self.logger logging.getLogger(self.name) def setup(self): 初始化资源如连接、文件句柄 pass def tick(self): 单次调度逻辑由主进程循环调用 pass def teardown(self): 释放资源保证幂等 passBaseModule把config和logger统一注入子类直接self.logger.info(...)就能打日志不用每个模块自己配。teardown()要求幂等是因为退出流程可能被触发多次重复释放资源不能报错。这套约定看着简单但它是整个 service 能稳定跑起来的地基后面加模块、改模块都靠它。3. 跑起来环境准备、启动与验证3.1 环境与依赖安装这份资源是纯 Python 实现没有重依赖跑起来门槛不高。常见做法是建一个虚拟环境把依赖装进去避免污染系统 Python。步骤如下# 建虚拟环境 python3 -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖依赖清单一般在 requirements.txt pip install -r requirements.txtrequirements.txt里通常只有pyyaml这类配置解析库如果 module 里用到了数据库或 HTTP 客户端会额外加对应的包。装完之后用pip list确认一下别出现版本冲突。虚拟环境激活后命令行前面会有(venv)提示看到这个说明环境对了。3.2 配置与启动启动前先看一眼config/settings.yaml里面一般有日志级别、调度间隔、各 module 的开关和参数。配置项按 module 名分组主进程加载后分发给对应模块。一个典型的配置长这样# config/settings.yaml service: interval: 1 # 调度间隔单位秒 log_level: INFO modules: collector: enabled: true source: local processor: enabled: true batch_size: 100 reporter: enabled: false # 上报模块默认关掉按需开enabled控制模块是否加载调试时可以把不相关的模块关掉减少干扰。interval是主循环的sleep时间改小响应更快但 CPU 占用更高改大省资源但实时性差按业务权衡。启动命令就是直接跑主进程python main_service.py启动后终端会打印每个 module 的启动日志类似module collector started。如果某个模块setup()里连不上资源会在这里报错根据日志定位就行。3.3 验证进程是否常驻进程跑起来后要确认它真的在常驻而不是启动完就退了。最直接的办法是看进程列表ps -ef | grep main_service能看到进程号说明活着。再进一步可以看日志文件有没有持续写入tail -f logs/service.log正常情况每秒或每个调度周期会有一行 tick 日志。如果日志停了但进程还在多半是某个 module 的tick()卡住了常见原因是里面做了阻塞 IO 没设超时。验证优雅退出可以手动发信号kill -TERM pid然后看日志里有没有按顺序打印每个 module 的 stopped进程是否干净退出。这一步很多人会跳过结果上线后发现重启时资源没释放端口被占又得手动 kill血泪经验就是退出流程一定要单独测。4. 核心 module 拆解采集、处理、上报三段链路4.1 采集 module数据从哪来采集 module 的职责是把外部数据拉进来放进一个队列或缓冲区供后续处理。它的tick()通常做一件事判断当前有没有新数据有就取一批。下面是一个本地文件采集的简化实现# modules/collector/module.py import os from modules.base import BaseModule class CollectorModule(BaseModule): name collector def setup(self): self.source self.config.get(source, local) self.cursor 0 # 记录读取位置避免重复采集 self.buffer [] def tick(self): path self.config.get(path, data/input.log) if not os.path.exists(path): return with open(path, r) as f: f.seek(self.cursor) # 从上次位置继续读 for line in f: self.buffer.append(line.strip()) self.cursor f.tell() # 更新游标 if self.buffer: self.logger.info(collected %d records, len(self.buffer)) def teardown(self): self.buffer.clear()关键在cursor这个游标它保证每次只读新增内容不会把整个文件重读一遍。seek和tell配合使用是文件采集的标准做法。buffer是模块内部缓冲处理模块会从这里取数据。实际项目里采集源可能是数据库、消息队列或 HTTP 接口思路一样维护一个「读到哪了」的状态每次只取增量。4.2 处理 module数据怎么加工处理 module 从采集模块的 buffer 里取数据做清洗、转换或计算再把结果交给上报模块。模块之间不直接调用而是通过共享的队列或缓冲区解耦这样采集慢了不影响处理处理挂了采集也不停。一个典型的处理实现# modules/processor/module.py from modules.base import BaseModule class ProcessorModule(BaseModule): name processor def setup(self): self.batch_size self.config.get(batch_size, 100) self.output [] def tick(self): collector self._find_collector() if not collector or not collector.buffer: return batch collector.buffer[:self.batch_size] del collector.buffer[:self.batch_size] # 取走并从缓冲移除 for record in batch: processed self._process(record) if processed: self.output.append(processed) self.logger.info(processed %d records, len(batch)) def _process(self, record): # 具体加工逻辑按业务替换 if not record: return None return record.upper() def _find_collector(self): # 简化写法实际通过主进程注入或注册表查找 from modules import registry return registry.get(collector)batch_size控制单次处理量太大占内存太小调度频繁。del collector.buffer[:batch_size]是取走即删除避免重复处理。_process是业务逻辑的落点清洗规则、字段映射、计算都在这里改。模块间通过registry查找而不是硬编码引用是为了让模块可以独立替换比如把采集源从文件换成数据库处理模块完全不用动。4.3 上报 module结果怎么出去上报 module 把处理结果发到外部可能是写文件、发 HTTP 请求或写数据库。它的tick()从处理模块的 output 里取数据发送成功后清空。上报最容易翻车的地方是网络失败没重试数据丢了都不知道所以常见做法是加一个本地暂存发失败就留着下轮再发# modules/reporter/module.py from modules.base import BaseModule class ReporterModule(BaseModule): name reporter def setup(self): self.endpoint self.config.get(endpoint, ) self.pending [] # 发送失败暂存 def tick(self): processor self._find_processor() if processor and processor.output: self.pending.extend(processor.output) processor.output.clear() if not self.pending: return sent [] for item in self.pending: if self._send(item): sent.append(item) for item in sent: self.pending.remove(item) # 只移除发送成功的 self.logger.info(reported %d, pending %d, len(sent), len(self.pending)) def _send(self, item): # 实际发送逻辑失败返回 False try: # requests.post(self.endpoint, jsonitem, timeout5) return True except Exception: self.logger.warning(send failed, keep in pending) return False def _find_processor(self): from modules import registry return registry.get(processor)pending列表是失败重试的关键发送成功才移除失败留着下轮继续。_send里一定要设超时不设超时的 HTTP 请求会把整个tick()卡死进而拖垮整个 service这是常驻进程里最隐蔽的坑之一。上报模块默认在配置里是关的调试链路时可以先开采集和处理确认数据加工对了再开上报。5. 避坑与排查常驻进程最容易翻车的几个点5.1 内存只涨不降现象是进程跑几小时后内存占用翻倍重启就好。原因通常是某个 module 的缓冲区只进不出比如采集模块的buffer一直 append 但处理模块没及时消费或者上报失败的数据全堆在pending里。解决办法是给缓冲区设上限超过就丢弃最旧的数据或落盘同时监控pending长度超过阈值告警。我一般会在tick()里加一行日志打印各缓冲区长度出问题时一眼能看出是哪个模块在囤数据。5.2 日志把磁盘写满现象是服务跑几天后磁盘告警一看日志文件几个 G。原因是tick()里打了太多 INFO 日志每秒一条乘以模块数量很可观。解决办法是用RotatingFileHandler做日志轮转按大小或时间切分保留最近几个文件。配置大概是这样from logging.handlers import RotatingFileHandler handler RotatingFileHandler( logs/service.log, maxBytes10 * 1024 * 1024, # 单文件 10MB backupCount5 # 保留 5 个备份 )maxBytes和backupCount按磁盘空间调别设太大。另外把高频的 tick 日志降到 DEBUG生产环境只留 INFO 以上的关键事件。5.3 单个模块卡死拖垮全局现象是日志突然停了进程还在但什么都不干。原因是某个 module 的tick()里做了阻塞操作比如没设超时的网络请求、等锁、读大文件。主循环是单线程顺序调度的一个模块卡住后面全等着。解决办法有两个方向一是给所有 IO 操作设超时二是把耗时模块放到独立线程或进程里跑。简单项目用超时就够了复杂项目建议每个 module 独立线程主进程只做调度和监控。5.4 退出时资源没释放现象是重启服务时报端口被占、文件被锁、数据库连接数没降。原因是teardown()没实现或实现得不对进程被 kill 时资源没回收。解决办法是确保每个 module 的teardown()都释放自己申请的资源并且注册信号处理收到SIGTERM时走完整退出流程。测试方法就是启动后kill -TERM看日志有没有按顺序关闭再启动一次看是否正常。这个流程我每次改完 module 都会走一遍不然上线重启就是一场灾难。5.5 配置改了不生效现象是改了settings.yaml重启服务行为没变。原因通常是配置加载路径写死、或者模块在setup()里缓存了配置但没重新读。解决办法是配置加载统一走一个入口启动时打印实际生效的配置方便核对。另外注意 YAML 的缩进缩进错了解析出来是空字典模块拿不到配置就用默认值表现就是「改了没用」。6. 二次开发加一个新 module 和改造调度策略加新 module 是这份资源最常见的二次开发场景。流程不复杂在modules/下建目录写module.py继承BaseModule实现setup/tick/teardown然后在配置里加一段。假设要加一个定时清理临时文件的 module# modules/cleaner/module.py import os import time from modules.base import BaseModule class CleanerModule(BaseModule): name cleaner def setup(self): self.interval self.config.get(interval, 3600) # 清理间隔 self.last_run 0 def tick(self): now time.time() if now - self.last_run self.interval: return # 没到间隔就跳过 self.last_run now tmp_dir self.config.get(tmp_dir, /tmp/service) removed 0 for name in os.listdir(tmp_dir): path os.path.join(tmp_dir, name) if os.path.isfile(path) and now - os.path.getmtime(path) self.interval: os.remove(path) removed 1 self.logger.info(cleaned %d files, removed) def teardown(self): pass这个 module 用last_run做节流不是每个 tick 都执行适合低频任务。配置里加modules: cleaner: enabled: true interval: 3600 tmp_dir: /tmp/service加完之后重启服务看日志有没有module cleaner started再等一个周期看有没有清理日志。验证新 module 有个技巧先把interval调小到几秒确认逻辑对了再改回正常值不然等一小时才看到结果调试效率太低。改造调度策略是另一个方向。默认是单线程顺序调度如果某个 module 耗时明显可以把它改成独立线程import threading class Service: def start(self): for m in self.modules: m.setup() if getattr(m, threaded, False): t threading.Thread(targetself._run_module, args(m,), daemonTrue) t.start() self.threads.append(t) def _run_module(self, m): while self.running: try: m.tick() except Exception: logger.exception(module %s tick failed, m.name) time.sleep(m.interval if hasattr(m, interval) else 1)给 module 加一个threaded True属性它就会跑在独立线程里主循环不再等它。这样耗时模块不会拖累其他模块代价是模块间共享数据要加锁。我一般只在确认某个模块是瓶颈时才这么改过早多线程化会让调试变复杂。改完之后重点验证两点一是线程退出时teardown()有没有被调用二是共享缓冲区的读写有没有竞态。从那以后我每次给 service 加线程或加模块都会先把退出流程和缓冲区长度日志走一遍确认没有资源泄漏再往下写。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号