很多人聊店群自动化,上来就讲调度、并发、高可用。
但我做了三年电商自动化,发现一个很残酷的事实:
大部分系统不是被高并发压垮的,而是被资源泄漏和异常累积拖死的。
脚本跑得再快,如果浏览器进程永远不关、临时文件不清理、异常不重试,七天之内必然崩。
这篇文章不讲高大上的集群,也不讲花哨的调度算法。
就聊聊我们在拼多多店群项目里,如何用影刀RPA + Python,构建一套能长期自愈的稳定性工程。
内容偏运维视角,全是线上踩过的坑和对应的解法。
适用场景:拼多多店群、TEMU、TikTok Shop 通用。 核心主题:资源回收、异常恢复、生命周期管理、监控自愈。
一、稳定性崩塌的三种常见模式
先说一下我们遇到过的情况。
模式一:内存缓慢增长
第一天启动系统,内存占用8GB。第三天变成12GB。第七天直接OOM,节点宕机。
排查后发现是Chromium进程没有被正确回收,或者影刀RPA脚本跑完后浏览器窗口还挂在后台。
模式二:僵尸任务堆积
某个任务因为网络超时卡住了,超时设置不合理,一直不返回。调度器还在等它完成,队列后面的任务全部堵死。
运营发现三个小时没有新订单被处理,已经来不及了。
模式三:磁盘写满
浏览器profile目录每天增长几百MB,三个月后500GB硬盘写满。更离谱的是日志文件没有轮转,单文件撑到100GB。
这些问题的本质是什么?
不是脚本写错了,而是没有设计“死亡”机制。
自动化系统不能只关心怎么“生”,更要设计好怎么“死”。
二、资源回收:从“尽量”到“强制”
资源回收不能靠“运气”或者“自觉”。必须做到强制兜底。
我们设计了一个三层回收机制:
第一层:正常释放
任务正常结束后,主动调用浏览器关闭接口。
# resource_manager.py
class GracefulRecycler:
def release_browser(self, browser_instance):
try:
# 先尝试正常关闭标签页
browser_instance.close_all_tabs()
# 再关闭浏览器进程
browser_instance.quit()
# 等待进程退出,最多5秒
browser_instance.process.wait(timeout=5)
except subprocess.TimeoutExpired:
# 正常关闭失败,进入强制回收
self.force_release(browser_instance)
第二层:强制回收
如果进程不响应,直接kill。
def force_release(self, browser_instance):
pid = browser_instance.process.pid
# 先优雅终止
browser_instance.process.terminate()
time.sleep(2)
# 如果还活着,强制杀死
if browser_instance.process.poll() is None:
import signal
os.kill(pid, signal.SIGKILL)
# 清理残留的共享内存和临时文件
self._cleanup_shm(pid)
第三层:巡检守护
无论前两层是否成功,我们还有一个独立的守护线程,每分钟扫描一次。
class ResourceReaper:
def __init__(self):
self.known_browser_pids = set()
self.running = True
def scan_and_reap(self):
# 获取所有Chromium进程
all_chrome_pids = self._get_all_chrome_pids()
# 找出不在已知列表中的僵尸进程
zombie_pids = all_chrome_pids - self.known_browser_pids
for pid in zombie_pids:
# 判断进程启动时间,超过1小时则杀掉
if self._is_process_older_than(pid, 3600):
os.kill(pid, signal.SIGKILL)
这个巡检机制救过我们很多次。有些浏览器进程因为某些边缘情况没有被正确记录,但巡检会兜底回收。
一个经验:永远不要相信“脚本退出时会自己清理”。设计一个独立的、不依赖业务逻辑的回收器。
三、异常恢复:任务不能“死不见尸”
任务执行过程中,任何环节都可能失败:
- 网络超时
- 元素找不到
- 浏览器崩溃
- 代理失效
- 平台返回验证码
我们的原则是:任何任务都必须有一个确定的终态(成功、失败、重试耗尽)。
下面这个状态机管理了任务的一生:
# task_lifecycle.py
from enum import Enum
import time
import traceback
class TaskState(Enum):
PENDING = "pending"
RUNNING = "running"
SUSPENDED = "suspended" # 临时挂起(比如遇到验证码)
RETRY = "retry"
SUCCESS = "success"
FINAL_FAILED = "final_failed"
class TaskOrchestrator:
def __init__(self, max_retries=3, retry_backoff_base=2):
self.max_retries = max_retries
self.backoff_base = retry_backoff_base
self.task_timeout = 120 # 单任务超时2分钟
def run_with_recovery(self, task):
state = TaskState.PENDING
retry_count = 0
while state not in (TaskState.SUCCESS, TaskState.FINAL_FAILED):
try:
# 执行前检查环境
self._pre_check(task)
# 启动超时计时器
future = self._execute_with_timeout(task, self.task_timeout)
result = future.result()
if result.success:
state = TaskState.SUCCESS
else:
# 可恢复的错误
if result.error_type in ("network_timeout", "element_not_found"):
state = TaskState.RETRY
else:
state = TaskState.FINAL_FAILED
except TimeoutError:
# 任务超时,强制终止影刀RPA进程
self._kill_rpa_script(task)
state = TaskState.RETRY
except BrowserCrashError:
# 浏览器崩溃,重启环境后重试
self._restart_browser(task.shop_id)
state = TaskState.RETRY
if state == TaskState.RETRY:
retry_count += 1
if retry_count > self.max_retries:
state = TaskState.FINAL_FAILED
else:
delay = self.backoff_base ** retry_count
time.sleep(delay)
return state == TaskState.SUCCESS
这里最容易被忽略的是超时时的强制终止。
我们曾经有一个影刀RPA脚本因为等待某个弹窗而无限卡住,调度器一直阻塞。后来加入了future.timeout和_kill_rpa_script逻辑,才解决了这个问题。
强制终止的实现方式:影刀RPA脚本在启动时会记录自己的进程ID,调度器超时后直接terminate。
四、浏览器实例池的内存控制
拼多多店群中,一个浏览器实例长期运行后,内存占用会从300MB涨到1GB以上。
解决方案不是无限增加内存,而是主动轮换。
# browser_pool_with_rotation.py
import psutil
import threading
class RotatingBrowserPool:
def __init__(self, max_instances=5, max_task_per_instance=30, max_memory_mb=800):
self.max_instances = max_instances
self.max_task_per_instance = max_task_per_instance
self.max_memory_mb = max_memory_mb
self.instance_task_counts = {}
self.instance_memory = {}
def get_browser(self, shop_id):
instance = self._find_existing_instance(shop_id)
if instance:
# 检查任务计数或内存是否超标
if (self.instance_task_counts.get(instance.id, 0) >= self.max_task_per_instance or
self.instance_memory.get(instance.id, 0) > self.max_memory_mb):
# 轮换掉这个实例
self._close_instance(instance)
instance = self._create_new_instance(shop_id)
else:
instance = self._create_new_instance(shop_id)
self.instance_task_counts[instance.id] = self.instance_task_counts.get(instance.id, 0) + 1
return instance
def _update_memory_stats(self):
"""定期更新每个实例的内存占用"""
for instance in self._all_instances():
try:
proc = psutil.Process(instance.pid)
self.instance_memory[instance.id] = proc.memory_info().rss / 1024 / 1024
except:
pass
这个轮换策略让我们的内存占用长期稳定在一个水平线,不会无限增长。
注意,轮换的时候不能粗暴关闭正在执行任务的浏览器。我们只在任务间隙进行轮换,或者标记为“待轮换”,下一次获取时重建。
坑:有些浏览器退出后,user-data-dir目录下的锁文件没有清理干净,导致下次启动失败。我们在关闭浏览器后,强制删除该目录下的SingletonLock文件。
五、日志轮转与磁盘保护
店群系统跑久了,日志和浏览器缓存会吃掉大量磁盘。
我们做了三件事:
5.1 应用日志轮转
使用logging.handlers.RotatingFileHandler,每个日志文件最大100MB,保留10个。
import logging.handlers
handler = logging.handlers.RotatingFileHandler(
'automation.log', maxBytes=100 * 1024 * 1024, backupCount=10
)
5.2 浏览器profile自动清理
每个店铺的profile目录,如果超过7天没有被使用,就整体删除。
同时,每个profile目录下的Cache子目录单独设置最大容量(比如500MB)。
def clean_old_profiles(profile_root, max_age_days=7):
cutoff = time.time() - max_age_days * 86400
for shop_dir in Path(profile_root).iterdir():
if shop_dir.is_dir():
# 检查最后访问时间
last_used = shop_dir.stat().st_atime
if last_used < cutoff:
shutil.rmtree(shop_dir)
5.3 磁盘用量监控
我们写了一个简单的守护脚本,每5分钟检查一次磁盘使用率,超过85%就触发告警,超过95%则自动删除最老的日志和缓存。
有一次磁盘突然100%是因为Chromium的崩溃日志(.dmp文件)积累了几万个。之后我们在/etc/systemd/coredump.conf里限制了core dump的生成。
六、异常注入测试:提前发现脆弱点
为了让系统真正稳定,我们在测试环境做了混沌工程式的异常注入:
- 随机kill浏览器进程
- 模拟网络延迟和丢包
- 随机让影刀RPA脚本卡死
- 填满磁盘
- 重启节点
每次注入后,观察系统能否自动恢复。
有一次测试发现:当浏览器进程被kill时,我们的调度器会一直等待它的quit()方法返回,导致任务永不超时。
修复方式:所有对浏览器进程的等待操作都加上超时,超时后直接kill。
经验:不要等到线上出问题才去验证恢复机制。在生产环境做混沌实验太危险,但测试环境一定要做。
七、监控指标与自愈策略
我们只监控四个核心指标,但每个指标都配置了自愈动作:
| 指标 | 告警阈值 | 自愈动作 |
|---|---|---|
| 任务队列堆积长度 | >20 持续5分钟 | 增加并发worker数;如果已达上限,发送扩容建议 |
| 任务失败率(滚动10分钟) | >15% | 自动暂停该店铺任务,检查代理或cookie有效性 |
| 节点内存使用率 | >90% | 主动触发浏览器轮换,释放不活跃实例 |
| 无成功任务超过2小时的店铺 | - | 发送告警给运营,同时尝试重新登录 |
自愈动作通过一个独立的监督进程执行,不依赖调度器本身(防止调度器假死)。
# supervisor.py
class SelfHealingSupervisor:
def run(self):
while True:
# 检查调度器进程是否活着
if not self.is_scheduler_alive():
self.restart_scheduler()
# 检查任务队列积压
queue_depth = self.get_queue_depth()
if queue_depth > 20:
self.increase_workers()
# 检查僵尸浏览器进程
zombie_count = self.count_zombie_browsers()
if zombie_count > 0:
self.kill_zombie_browsers()
time.sleep(30)
这个监督进程很简单,但它保证即使主调度器挂了,系统也能在1分钟内自愈。
八、实战总结:稳定性不是功能,是习惯
回到开头那句话。
大部分店群自动化系统死掉的直接原因是资源泄漏或异常累积,而不是高并发。
解决这些问题没有银弹。靠的是一个一个地加保护措施:
- 超时兜底
- 强制回收
- 巡检守护
- 轮换机制
- 磁盘保护
- 自愈监控
如果你正在做拼多多或者TEMU的店群自动化,建议你花一周时间专门做“稳定性加固”。
不要只盯着新功能开发。因为一个三天崩一次的系统,没人敢用。
我们现在的系统已经连续跑了三个月没有人工干预,靠的就是上面这些“枯燥”的工程实践。
希望对你有所帮助。
作者:林焱
