影刀RPA拼多多店群自动化:资源回收、异常恢复与稳定性工程实战

影刀RPA拼多多店群自动化:资源回收、异常恢复与稳定性工程实战

很多人聊店群自动化,上来就讲调度、并发、高可用。

但我做了三年电商自动化,发现一个很残酷的事实:

大部分系统不是被高并发压垮的,而是被资源泄漏和异常累积拖死的。

脚本跑得再快,如果浏览器进程永远不关、临时文件不清理、异常不重试,七天之内必然崩。

这篇文章不讲高大上的集群,也不讲花哨的调度算法。

就聊聊我们在拼多多店群项目里,如何用影刀RPA + Python,构建一套能长期自愈的稳定性工程

内容偏运维视角,全是线上踩过的坑和对应的解法。

picture.image

适用场景:拼多多店群、TEMU、TikTok Shop 通用。 核心主题:资源回收、异常恢复、生命周期管理、监控自愈。


一、稳定性崩塌的三种常见模式

picture.image

先说一下我们遇到过的情况。

模式一:内存缓慢增长

第一天启动系统,内存占用8GB。第三天变成12GB。第七天直接OOM,节点宕机。

排查后发现是Chromium进程没有被正确回收,或者影刀RPA脚本跑完后浏览器窗口还挂在后台。

picture.image

模式二:僵尸任务堆积

某个任务因为网络超时卡住了,超时设置不合理,一直不返回。调度器还在等它完成,队列后面的任务全部堵死。

运营发现三个小时没有新订单被处理,已经来不及了。

模式三:磁盘写满

picture.image

浏览器profile目录每天增长几百MB,三个月后500GB硬盘写满。更离谱的是日志文件没有轮转,单文件撑到100GB。

这些问题的本质是什么?

不是脚本写错了,而是没有设计“死亡”机制。

自动化系统不能只关心怎么“生”,更要设计好怎么“死”。

picture.image

picture.image

二、资源回收:从“尽量”到“强制”

资源回收不能靠“运气”或者“自觉”。必须做到强制兜底。

我们设计了一个三层回收机制:

第一层:正常释放

任务正常结束后,主动调用浏览器关闭接口。

# 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的店群自动化,建议你花一周时间专门做“稳定性加固”。

不要只盯着新功能开发。因为一个三天崩一次的系统,没人敢用。

我们现在的系统已经连续跑了三个月没有人工干预,靠的就是上面这些“枯燥”的工程实践。

希望对你有所帮助。


作者:林焱

0
0
0
0
评论
未登录
暂无评论