接口能跑通,但一批图跑起来忽快忽慢,通常不是接口坏了,而是三件事叠在一起:单张延迟本身多大、并发开多少合适、失败重试放大了多少调用量。可行的顺序是先固定参数连跑单张调用拿到基准延迟,再把并发从 1 逐步抬到 5,看平均单张耗时和失败数在哪里变差,最后把失败按特征分组,只对可重试的那部分加次数上限和退避间隔。三件事分开测,才判断得出瓶颈是在服务端排队、并发设置还是重试叠加。
适合已经能成功调用 FLUX 接口、但批量任务耗时和失败率不稳定的人。先测单张基线(同一参数连跑 3 次并保留原始输出),再按 1 到 5 的并发梯度各跑固定张数,同时记录成功数、失败数与平均单张耗时;失败先按 4xx、超时、429、5xx、返回体不合法分组,只有超时和 429/5xx 才进入有限重试。判断依据是自己的时间戳和日志,不是一次手感。边界:并发上限与超时阈值取决于所用服务的配额和排队策略,需要结合环境确认,不要照搬别人的数字。
单张调用前后各打一个时间戳,拿到基准延迟
基准延迟的作用是给后面所有对比提供参照:同一 prompt、同一尺寸、同一步数、同一超时设置,连跑 3 次,把每次耗时和状态码原样打出来。计时只包住 HTTP 请求这一段,别把写盘、图片解码、后处理算进去,否则量到的是本地处理时间,跟服务端延迟没关系。
脚本骨架(替换 URL、TOKEN、payload 后可直接跑)
import time
import requests
URL = 'https://<your-endpoint>/v1/flux'
HEADERS = {'Authorization': 'Bearer <TOKEN>', 'Content-Type': 'application/json'}
PAYLOAD = {'prompt': 'a red apple on a wooden table', 'steps': 28, 'width': 1024, 'height': 1024}
for i in range(3):
t0 = time.perf_counter()
r = requests.post(URL, json=PAYLOAD, headers=HEADERS, timeout=120)
cost = time.perf_counter() - t0
print('[run %d] status=%s cost=%.3fs bytes=%d' % (i + 1, r.status_code, cost, len(r.content)))
原始输出按下面这种格式直接贴进记录表,数值换成你自己跑出来的:
[run 1] status=200 cost=<耗时>s bytes=<字节数>
[run 2] status=200 cost=<耗时>s bytes=<字节数>
[run 3] status=200 cost=<耗时>s bytes=<字节数>
看这三次要抓两个点:第一次是否明显比后两次慢(首次调用可能包含连接建立、模型加载或服务端冷启动);三次之间的波动幅度有多大。如果波动很大,后面的并发测试至少要重复一轮,否则拐点会被噪声盖住。timeout 建议显式写死,不要用默认值——没有超时的请求会把一次卡住变成整批卡住。
把并发从 1 逐步抬到 5,记录每次的总耗时和失败数
并发测试的目的不是找出最大并发,而是找出收益开始变差的拐点。做法是固定总请求数(例如每档 10 张)和固定 payload,只改并发数,从 1 跑到 5,每档记四个数:总耗时、成功数、失败数、平均单张耗时(总耗时除以总请求数)。
import time
import requests
from concurrent.futures import ThreadPoolExecutor
def one(_):
t0 = time.perf_counter()
try:
r = requests.post(URL, json=PAYLOAD, headers=HEADERS, timeout=120)
return r.status_code == 200, time.perf_counter() - t0
except Exception:
return False, time.perf_counter() - t0
N = 10
for c in (1, 2, 3, 4, 5):
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=c) as ex:
res = list(ex.map(one, range(N)))
total = time.perf_counter() - t0
ok = sum(1 for r, _ in res if r)
print('并发=%d 总耗时=%.2fs 成功=%d 失败=%d 平均单张=%.2fs'
% (c, total, ok, N - ok, sum(t for _, t in res) / N))
记录表字段固定成这五列,每档一行,数值从上面的 print 行抄:
| 并发数 | 总耗时 | 成功数 | 失败数 | 平均单张耗时 |
|---|---|---|---|---|
| 1 | 待填 | 待填 | 待填 | 待填 |
| 2 | 待填 | 待填 | 待填 | 待填 |
| 3 | 待填 | 待填 | 待填 | 待填 |
| 4 | 待填 | 待填 | 待填 | 待填 |
| 5 | 待填 | 待填 | 待填 | 待填 |
判断方式:如果从 1 到 2 平均单张耗时明显下降,说明还有并行空间;到某一档平均单张耗时不再下降甚至回升,或者失败数开始出现,这一档就是拐点,实际使用取拐点的前一档更稳。注意客户端并发数不等于服务端并行数,服务端可能在排队或限流,所以要看端到端耗时,而不是线程数。
区分失败类型:参数被拒、超时、服务端返回错误
把所有失败混成一类,会让重试变成放大错误的工具:参数错的请求重试一百次还是错,只会白耗配额。建议按响应特征先分组,每组配一个明确的处理动作,让脚本按状态码和异常类型自动分流。
- 4xx(400、401、403、404、422 等参数或鉴权被拒):动作是不重试,记录原始响应体,修正 prompt、尺寸、步数或令牌后单独重发。这类失败进重试队列只会浪费调用量。
- 连接超时、读超时(客户端抛异常,无状态码):动作是允许重试,但必须带退避间隔。边界在于服务端可能已经开始计算,重发存在重复计费或产生重复任务的可能,需要在业务侧做幂等或去重。
- 429 限流:动作是降低并发加拉长间隔,不要维持原并发硬重试,否则只会拿到更多 429。
- 5xx 服务端错误:动作是有限重试,例如 1 到 2 次,仍失败就落进失败清单,并保留响应体。
- 2xx 但返回体不合法(空内容、JSON 缺字段、图片解码失败):动作是按业务失败处理,重试一次并记录原始响应,用来区分是服务端返回异常还是本地解析写错了。
给重试加次数上限和退避间隔
重试必须能停下来。两个参数不可或缺:最大重试次数和退避间隔。次数可以先从 2 次起,退避用指数加随机抖动,避免一批请求在同一时刻集中重发把服务端再打一次。
import time
import random
import requests
MAX_RETRY = 2
BASE = 1.0
def call(payload):
for attempt in range(MAX_RETRY + 1):
try:
r = requests.post(URL, json=payload, headers=HEADERS, timeout=120)
if r.status_code == 200:
return r
if r.status_code == 429 or r.status_code >= 500:
pass # 可重试
else:
return r # 4xx 直接返回,不重试
except (requests.Timeout, requests.ConnectionError):
pass # 可重试
if attempt < MAX_RETRY:
time.sleep(BASE * (2 ** attempt) + random.random() * 0.5)
return None # 达到上限,交给失败清单
达到上限后的做法是放弃自动重发:把任务 ID、失败类型、最后一次响应体写进失败清单,人工或下一次任务再处理。单批次内可以设一条止损线,例如连续 5 张失败就暂停整批,先确认是参数问题还是服务端问题,再决定是否继续。统计时把首次请求数和实际重试次数分开记,否则重试悄悄放大调用量,记录表上却看不出来。
在同一张记录表里同时看延迟、并发、重试三个数
只看一次耗时会误判,只看并发数会忽略失败,只看重试次数会掩盖延迟。记录表字段建议固定成下面这些,字段名提前定好,后面几轮测试才能直接横向对比:
- 批次 ID、任务 ID:能定位到具体哪一张图
- 模式:单张基线 / 并发梯度 / 批量正式任务
- 并发数:单张基线填 1
- 请求总数:这一档或这一批发了多少张
- 总耗时、平均单张耗时
- 成功数、失败数
- 失败类型:4xx / 超时 / 429 / 5xx / 返回体不合法
- 实际重试次数、是否最终成功
- 备注:prompt、尺寸、步数、timeout、测试期间是否改过并发
一次测试从头填到尾的填写示例(只示范格式,数值替换成你自己的日志值):
| 批次 | 模式 | 并发 | 请求总数 | 总耗时 | 平均单张 | 成功/失败 | 失败类型 | 重试次数 | 最终成功 |
|---|---|---|---|---|---|---|---|---|---|
| base-01 | 单张基线 | 1 | 3 | 待填 | 待填 | 3 / 0 | 无 | 0 | 是 |
| grad-1 | 并发梯度 | 1 | 10 | 待填 | 待填 | 待填 / 待填 | 待填 | 0 | 待填 |
| grad-3 | 并发梯度 | 3 | 10 | 待填 | 待填 | 待填 / 待填 | 待填 | 0 | 待填 |
| grad-5 | 并发梯度 | 5 | 10 | 待填 | 待填 | 待填 / 待填 | 待填 | 待填 | 待填 |
| batch-a | 批量正式任务 | 取拐点前一档 | 按实际 | 待填 | 待填 | 待填 / 待填 | 待填 | 待填 | 待填 |
这张表跑完一遍之后,判断顺序就固定了:先看单张基线三次是否稳定,再看并发梯度里哪一档平均单张耗时开始回升或失败数开始出现,最后看实际重试次数有没有把调用量悄悄放大。三处对得上,才说得清批量任务忽快忽慢是卡在服务端排队、并发设置,还是失败重试叠加。