影刀RPA店群自动化稳定性工程:异常熔断、自愈机制与全链路可观测性实战

上一篇文章我们聊了店群自动化的架构设计和浏览器实例池。

文章发出后,不少同行在后台问我:架构搭好了,任务能跑了,但跑着跑着就挂了,怎么办?

picture.image

这个问题问到了点子上。

能跑和能稳定跑,中间隔着一整个工程团队的距离。

picture.image

今天这篇文章,我们不谈架构设计了。

我们聊聊自动化系统上线之后,怎么让它活下去、活得久。

picture.image 从异常熔断到自愈机制,从全链路监控到无人值守——全是线上环境里拿真金白银换来的经验。


一、先说说“稳定性”到底指什么

picture.image

很多团队对稳定性的理解停留在“脚本不报错”这个层面。

这太浅了。

picture.image 企业级自动化系统的稳定性,至少包含四个维度:

第一,执行稳定性。 任务能正常跑完,不卡死、不中断、不超时。

第二,数据稳定性。 读取的数据准确,写入的数据完整,不会因为异常导致数据错乱。

picture.image

第三,资源稳定性。 内存不泄漏、进程不残留、磁盘不写满。

第四,业务稳定性。 店铺不被风控、账号不被关联、操作频率在安全阈值内。

picture.image 这四个维度任何一个出问题,系统都算不上“稳定”。

而我们今天要聊的,就是怎么在这四个维度上做工程化落地。


picture.image

二、异常处理:别让一颗老鼠屎坏了一锅粥

先说最基础也最容易被忽视的:异常处理。

新手写RPA流程,最常见的毛病是两种极端。

一种是完全不处理异常。脚本跑着跑着遇到一个弹窗、一个加载超时、一个元素找不到,直接崩溃退出。整个任务队列全部停摆。

另一种是到处塞异常捕获。每个指令外面都包一层Try-Catch,代码臃肿不堪,出了错也不知道到底哪里出了问题。

这两种都不对。

正确的做法是分层处理。

第一层:可预见的异常,用重试机制解决。

页面加载超时、网络波动、元素暂时不可见——这类问题本质上是临时性的。重试几次大概率就能过去。

我们的原则是:对于读取数据、页面跳转这类操作,设置3次重试就够用了。

但重试不是无脑循环。每一次重试之间要有退避间隔,避免在系统繁忙时雪上加霜。

# retry_with_backoff.py - 带退避的重试装饰器
import time
from functools import wraps
from typing import Type, Tuple

![picture.image](https://p3-volc-community-sign.byteimg.com/tos-cn-i-tlddhu82om/807a386f81db478fa32ead50971ddc89~tplv-tlddhu82om-image.image?=&rk3s=8031ce6d&x-expires=1785835595&x-signature=wf0Zrad8QFgpO3If6LfqjcEsMbM%3D)

class RetryExhaustedError(Exception):
    pass

def retry_with_backoff(
    max_retries: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 30.0,
    exceptions: Tuple[Type[Exception], ...] = (Exception,)
):
    """带指数退避的重试装饰器 - 工程化异常恢复"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            delay = base_delay
            
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt == max_retries:
                        break
                    
                    # 指数退避 + 随机抖动,避免惊群效应
                    sleep_time = min(delay * (2 ** attempt), max_delay)
                    sleep_time *= (0.8 + 0.4 * __import__('random').random())
                    
                    # 记录重试日志
                    
![picture.image](https://p3-volc-community-sign.byteimg.com/tos-cn-i-tlddhu82om/7be144cbbe444d2084b75430b48f9640~tplv-tlddhu82om-image.image?=&rk3s=8031ce6d&x-expires=1785835595&x-signature=%2Fd1NG5q%2B72rLMl0tT%2F%2BFQbJoufs%3D)
                    __import__('logging').getLogger(__name__).warning(
                        f"{func.__name__}{attempt+1} 次重试, 等待 {sleep_time:.2f}s, 异常: {e}"
                    )
                    time.sleep(sleep_time)
            
            raise RetryExhaustedError(
                f"{func.__name__} 重试 {max_retries} 次后仍然失败"
            ) from last_exception
        return wrapper
    return decorator

# 使用示例
@retry_with_backoff(max_retries=3, exceptions=(TimeoutError, ConnectionError))
def fetch_order_page(shop_id: str):
    # 影刀RPA流程调用
    pass

第二层:不可恢复的异常,熔断+告警。

有些异常不是重试能解决的。比如平台界面大改版、账号被限制登录、API密钥失效。

这类问题重试一万次也没用,只会浪费资源和时间。

我们的策略是:连续失败超过阈值(比如某个店铺的任务连续失败5次),自动触发熔断——该店铺的所有任务暂时挂起,不再调度。

同时通过飞书/钉钉推送告警,通知人工介入。

第三层:关键操作,必须加事务性保障。

数据写入、文件保存、订单状态变更——这些不可逆的操作,必须确保要么全做、要么全不做。

我们会在影刀流程中加校验点:写入完成后立即回读验证,确认数据一致才继续;不一致则回滚并告警。


三、自愈机制:让系统自己救自己

异常处理是被动的——出了问题再反应。

自愈机制是主动的——系统自己发现问题、自己尝试修复。

我们构建了一套三层自愈体系:

第一层:进程级自愈。

浏览器实例池的巡检线程每30秒扫描一次所有活跃的浏览器进程。

发现僵尸进程?杀死。

发现内存占用异常?重启该实例。

发现进程假死(超过阈值没有响应)?强制回收。

这套机制我们跑了大半年,帮我们避免了至少80%的内存泄漏事故。

第二层:任务级自愈。

每个任务都有超时阈值。

一个订单同步任务正常30秒能跑完,我们设置超时时间为120秒。

超过120秒还没结束?系统自动判定为“卡死”,强制终止该任务,释放所有资源,然后重新入队。

注意,不是所有任务都适合自动重跑。涉及支付、下单、发货等敏感操作的任务,超时后只告警不重跑,由人工判断。

第三层:节点级自愈。

当执行节点的CPU使用率连续5分钟超过85%、或者内存使用率超过90%时,系统自动将该节点标记为“不健康”。

新的任务不再调度到这个节点上。已经运行的任务继续跑完,但新任务全部绕行。

等节点资源恢复后,自动重新加入调度池。

# node_health_check.py - 节点健康检查与自愈
import psutil
import time
from threading import Thread
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class NodeHealth:
    node_id: str
    is_healthy: bool = True
    cpu_percent: float = 0.0
    memory_percent: float = 0.0
    last_check: float = field(default_factory=time.time)
    consecutive_unhealthy: int = 0
    unhealthy_threshold: int = 5  # 连续5次不健康则标记为down

class NodeHealthChecker:
    """节点健康检查与自愈服务"""
    
    def __init__(self, scheduler, alert_service):
        self.scheduler = scheduler
        self.alert = alert_service
        self._nodes: dict[str, NodeHealth] = {}
        self._running = True
        self._check_interval = 30  # 每30秒检查一次
        
    def register_node(self, node_id: str):
        self._nodes[node_id] = NodeHealth(node_id=node_id)
    
    def start(self):
        Thread(target=self._check_loop, daemon=True).start()
    
    def _check_loop(self):
        while self._running:
            for node_id, health in self._nodes.items():
                self._check_node(health)
            time.sleep(self._check_interval)
    
    def _check_node(self, health: NodeHealth):
        """检查单个节点的健康状态"""
        try:
            # 获取系统资源使用率
            health.cpu_percent = psutil.cpu_percent(interval=1)
            health.memory_percent = psutil.virtual_memory().percent
            health.last_check = time.time()
            
            # 判断是否健康
            is_unhealthy = (
                health.cpu_percent > 85 or 
                health.memory_percent > 90
            )
            
            if is_unhealthy:
                health.consecutive_unhealthy += 1
                if health.consecutive_unhealthy >= health.unhealthy_threshold:
                    self._mark_node_down(health)
                elif health.consecutive_unhealthy >= 2:
                    # 预警级别告警
                    self.alert.warning(
                        f"节点 {health.node_id} 资源异常: "
                        f"CPU={health.cpu_percent:.1f}%, MEM={health.memory_percent:.1f}%"
                    )
            else:
                health.consecutive_unhealthy = 0
                if not health.is_healthy:
                    self._mark_node_up(health)
                    
        except Exception as e:
            # 检查本身异常时,不盲目标记down
            self.alert.error(f"节点健康检查失败: {health.node_id}, {e}")
    
    def _mark_node_down(self, health: NodeHealth):
        """标记节点下线 - 触发自愈"""
        if health.is_healthy:
            health.is_healthy = False
            self.scheduler.drain_node(health.node_id)  # 停止向该节点分配新任务
            self.alert.critical(f"节点 {health.node_id} 已下线,触发自愈流程")
            # 尝试重启节点上的浏览器实例池
            self._attempt_node_recovery(health.node_id)
    
    def _mark_node_up(self, health: NodeHealth):
        """标记节点恢复"""
        health.is_healthy = True
        self.scheduler.enable_node(health.node_id)
        self.alert.info(f"节点 {health.node_id} 已恢复")
    
    def _attempt_node_recovery(self, node_id: str):
        """尝试恢复节点 - 重启浏览器池"""
        # 实际实现中:清理残留进程、重启服务
        pass

四、全链路可观测性:别等用户告诉你系统挂了

很多团队的自动化系统有一个共通的毛病:运行状态是个黑盒

任务在跑吗?不知道。

跑得顺利吗?不知道。

出问题了吗?等用户打电话来才知道。

这种状态做个人项目可以,做企业级系统完全不行。

我们构建了一套“全链路可观测性”体系,覆盖三个层次:

第一层:基础设施监控。

CPU、内存、磁盘、网络——这些基础指标每30秒采集一次,推送到监控大盘。

设定阈值告警:CPU超过80%告警、磁盘使用率超过85%告警、内存持续增长告警。

第二层:应用层监控。

任务队列深度、任务成功率、平均执行时长、各店铺的失败分布。

这些是业务层面的核心指标。

我们会在每个影刀流程的关键节点埋点——开始执行、页面加载完成、数据读取成功、操作完成——每个节点都上报状态和时间戳。

这样任何一个环节出问题,都能在监控大盘上精准定位。

第三层:业务层监控。

这是最容易忽略的一层。

系统跑得再稳,如果业务数据不对,一切都是白搭。

我们每天凌晨会跑一套“对账巡检”流程:

  • 拼多多后台的订单数 vs 本地数据库的订单数
  • TEMU的库存同步是否完整
  • 各店铺的登录态是否有效
  • 是否有异常的风控通知

对不上?立刻告警。


五、日志系统:事故排查的最后一道防线

监控解决的是“现在发生了什么”。

日志解决的是“刚才到底发生了什么”。

这两者缺一不可。

我们的日志规范很简单,但执行得很严格:

每条日志必须包含三个东西:时间戳、任务ID、操作描述。

少了任何一个,这条日志在排查问题时就是废的。

日志级别分四种:

  • INFO:正常流程节点,比如“开始执行订单同步”“页面加载完成”
  • WARNING:可恢复的异常,比如“元素定位失败,第2次重试”
  • ERROR:不可恢复的异常,比如“账号登录失败”“数据写入异常”
  • CRITICAL:系统级故障,比如“浏览器实例池耗尽”“节点宕机”

日志的存储我们用了滚动策略:保留最近7天的详细日志,更早的压缩归档。

为什么是7天?因为我们发现超过80%的事故排查只需要看最近3天的日志,7天已经留了充足的冗余。


六、无人值守的最后一公里

监控有了、告警有了、自愈机制也有了。

但还有一个问题:半夜出事了怎么办?

我们的做法是分三级响应:

第一级:自动修复。 绝大多数问题——进程残留、内存超标、任务超时——系统自己就能处理。不需要人介入。

第二级:静默告警。 一些需要关注但不紧急的问题——某个店铺成功率下降、某个节点资源偏高——通过飞书/钉钉推送消息,白天上班处理。

第三级:紧急呼叫。 系统级故障——所有节点同时宕机、数据库连接断开、核心业务流程全部中断——通过电话+短信+飞书三通道同时推送,确保有人响应。

这套分级响应机制跑了半年多,真正触发第三级的情况只发生过两次。

一次是机房网络割接,一次是数据库迁移时的配置错误。

其他所有问题,都在第一级和第二级消化掉了。


七、写在最后

做自动化系统,最难的不是让它跑起来。

最难的是让它一直跑下去。

从异常处理到自愈机制,从监控告警到日志系统——每一层都是在为“长期稳定运行”这四个字做铺垫。

很多团队最开始都会忽略这些“非功能性”的工作。

觉得能跑就行,出了问题再修。

但真正跑到几十个店铺、几百个任务并发的时候,你会发现:

没有稳定性工程支撑的自动化系统,本质上是一颗定时炸弹。

希望这篇文章能帮你少踩一些坑。


作者:林焱

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