上线评审会上,老板问了一句:“客服机器人扛得住双十一吗?”会议室安静了十秒。没人答得上来——并发设计是有的,队列是有的,但没人真的往里灌过洪峰。

于是定了规矩:上线前压测一晚。这一晚没测出什么漂亮的性能数字,倒是测出了三个上线后一定会炸、Demo 阶段永远看不见的真问题。

一、压测目标先定清楚

压测最忌讳“跑个工具看看 QPS”。出发前先写下要回答的三个问题,产出物就是这三个答案:

  1. 限频边界在哪:发送速率提到多快时,平台侧开始出现异常
  2. 失败长什么样:过载时是全量失败还是部分失败,错误返回长什么样
  3. 恢复是什么样:洪峰停掉之后,系统需要多久回到正常,期间还会发生什么

QPS 数字本身不重要,三个问题的答案才决定上线方案。

二、压测环境:三条隔离原则

压测等于主动制造故障,环境不隔离就是拿生产开刀:

  1. 实例隔离:用测试实例压,别碰生产实例——压测打出的问题会连累真实用户的会话
  2. 对象隔离:发送目标全部走 filehelper(文件传输助手),测的是发送链路的吞吐,不碰任何真实用户
  3. 环境隔离:代理用公用测试档就行,压测不需要好环境,需要的是稳定可复现的环境

这三条里第一条最容易被心存侥幸地跳过——“就压五分钟没事的”。别跳,压测的事故案例基本都是从这五分钟开始的。

三、洪峰怎么灌:递增速率,不是一步到位

模拟真实业务的关键是 ramp-up(递增加压),从一个正常速率起步,每一轮往上加一档,观察系统在哪一档开始变形:

# WTAPI 微信机器人接口平台 | 文档: https://weiti.apifox.cn
# 本文依赖:
#   - 发送: postText (appId/toWxid/content, 压测目标全部为 filehelper)
import time
import threading
import requests
from dataclasses import dataclass, field

BASE_URL = "https://wx.chuapi.com"
HEADERS = {"Content-Type": "application/json", "X-finder-TOKEN": "<your_token>"}

@dataclass
class Probe:
    rate: int                       # 本轮目标速率: 条/分钟
    latencies: list = field(default_factory=list)
    errors: int = 0

def fire_once(app_id: str, probe: Probe) -> None:
    start = time.monotonic()
    try:
        resp = requests.post(
            f"{BASE_URL}/finder/v2/api/message/postText",
            headers=HEADERS, timeout=10,
            json={"appId": app_id, "toWxid": "filehelper",
                  "content": f"[loadtest] {probe.rate}"},
        ).json()
        probe.latencies.append(time.monotonic() - start)
        if resp.get("ret") != 200:
            probe.errors += 1
    except requests.RequestException:
        probe.errors += 1

def ramp_up(app_id: str, rates: list[int]) -> list[Probe]:
    results = []
    for rate in rates:              # 例如 [10, 30, 60, 120, 200]
        probe = Probe(rate=rate)
        interval = 60.0 / rate
        for _ in range(rate):
            threading.Thread(target=fire_once, args=(app_id, probe)).start()
            time.sleep(interval)
        time.sleep(30)              # 每档之间留观察窗
        results.append(probe)
        print(f"速率{rate}/分钟: p95={sorted(probe.latencies)[int(len(probe.latencies)*0.95)]:.2f}s, 错误={probe.errors}")
    return results

每一档结束打一行摘要,变形点在哪一目了然。注意 content 里带上速率标记——出问题时能从记录里反查出当时在压哪一档。

四、三个真问题

发现一:限频不是悬崖,是斜坡。 原本以为速率超限的表现是“突然全是错误”。实际是:错误率还没动,p95 延迟先从 0.3 秒爬到 3 秒,再爬一段,部分失败才开始出现。也就是说,延迟是比错误率早得多的预警信号。这直接改写了发送端的设计——客户端要有延迟自适应降速(连续几条延迟超过阈值就主动降速),而不是等错误出现再重试。

发现二:无脑重试会把部分失败放大成全量失败。 一档速率里出现 10% 失败后,重试逻辑立刻把这些任务重新打出去,恰好叠加在下一档的加压上,10% 部分失败滚成 60% 全面过载。压测前的重试代码看起来很合理,压测后才看清:没有退避和全局限速闸的重试,本质是第二个洪峰源。改法:指数退避 + 全局并发闸(在途请求超过阈值就暂停重试放行)。

发现三:恢复期比故障期更危险。 洪峰停掉的那一刻系统并没有恢复——积压的重试、队列里排着的任务还在往外打,错误率反而在洪峰结束后第三分钟创了新高。这和掉线排查里“恢复实例先观察再回池”是同一条原理:恢复必须限速回放,不能放开闸门。压测后把“恢复期降速系数”写进了发送器配置。

五、写进上线检查单

  1. 发送器有延迟自适应降速吗?触发阈值是多少?
  2. 重试有指数退避和全局在途闸吗?——没有就是埋着第二个洪峰源
  3. 故障恢复后的回放速率,降速系数定了吗?
  4. 三个压测问题的答案(边界、失败形态、恢复时间)写进上线文档了吗?
  5. 限频红线以接口平台公开文档写明的规范为准——压测的加压档位别越红线,压出问题不是目的,看清边界才是

接口路径与限频规范以 WTAPI 文档为准。


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐