本文介绍存在突发流量的业务如何通过简单改造业务代码,避免因为突发流量导致限流,保障请求成功率。
什么是限流#
方舟模型服务 API 会对单位时间内的请求次数、token 使用量、使用量的增长幅度及同时发起的请求数量进行限制,以保障服务的稳定运行和资源的合理分配。当调用方发起的请求触发 API 的限流,会返回 “429: 'Too Many Requests'” 错误。
为什么有限流#
大模型服务因模型参数量庞大(数十亿至千亿级),算力成本高昂及扩容周期更长,导致业界模型服务平台面临流量承接能力(尤其是突发流量)不足的问题。在此背景下,平台会尽最大努力提升请求成功率,保障模型服务的整体可用性。限流作为请求的第一层流量防护机制,主要作用如下:
- 减少 API 滥用或者误用:恶意攻击会给服务器发送大量请求,试图让服务过载导致服务中断。方舟设置速率限制,可以有效防止此类情况。
- 保障用户公平地访问:避免个人或者组织发送过多请求,导致其他人的服务访问和响应速度减慢。通过限制单个用户的请求通量(单位时间请求量),确保尽可能多的人能正常使用API,减少 API 服务速度变慢情况。
- 帮助控制平台总体负载:如果 API 服务请求极速增加,可能会给服务端资源带来很大扩容压力,导致性能问题。通过设置限流,可以帮助所有用户保持流畅、稳定的体验。
限流对象#
当前方舟的限流对象主要有:
- 模型限流:同主账号下,同模型(不区分版本)限流,由方舟设定,不可手动调整,各个模型默认限流信息请参见 模型列表。如有超额的流量需求,请通过 工单 提交提额申请。
- 推理接入点限流:推理接入点(Endpoint)限流,作用于推理接入点,用户可以自行调整,用于灵活控制应用的模型服务使用量。
限流类型#
方舟提供了不同的限流指标。
| 限流类型 | 解释说明 | 适用对象 |
|---|---|---|
| TPM | 每分钟可以处理的 token 量限制,包含模型的输入和输出。 | 通常是在线推理中按 token 计费模型的模型服务的限流指标。 |
| TPD | 每天可以处理的 token 量限制,包含模型的输入和输出。 | 通常是批量处理的模型服务的限流指标。 |
| RPM | 每分钟可发起的请求量限制,用以防 Dos 攻击。 | 几乎所有模型都有对应 RPM 指标。 |
| IPM | 每分钟可生成的图片数量限制,面向生图模型。 | 通常是图片生成模型的模型服务的限流指标。 |
| Inflight Batchsize | 并行在途请求数,同主账号下指定模型同时在处理的请求数量限制,在途请求包括排队、数据传输、计算处理、数据回传全流程的请求。 | 几乎所有模型都有对应的并发数指标。 |
限流错误码#
详细方舟错误码见 错误码。
什么是突发流量#
在单位时间内请求量产生剧烈波动,常见在定时数据处理任务、热点事件等场景,平台会根据请求特征、资源水位、模型特征、用户历史流量等因素认定突发流量(通常情况下,建议您将 Token 用量的增长速率控制在每 3 分钟 20% 以内)。
| 突发流量示意图 | 说明 |
|---|---|
常见行为
|
平台处理策略#
针对突发流量场景,平台采取以下策略提升推理请求成功率:
- 快速扩容指定模型的服务集群,以逐步承载住服务。
- 方舟会在用户可容忍的超时时间内,通过服务端重试爬坡策略提升请求成功率。
- 当突发流量抢占过多资源影响整体服务可用性时,平台将优先限制/熔断突发流量,保障其他用户的可用性。
- 在触发限制和熔断时,方舟会对突增的请求返回 “429: 'Too Many Requests'” 错误信息,并在资源扩容到一定程度后(分钟级)逐步恢复。
错误码信息
| 状态码 | 错误类型 | 错误码 | 错误信息 | 含义 |
|---|---|---|---|---|
| 429 | TooManyRequests | ServerOverloaded | The service is currently unable to handle additional requests due to server overload. Please retry later. Request ID: | 服务资源紧张,请您稍后重试。常出现在调用流量突增或刚开始调用长时间未使用的推理接入点。 |
| 429 | TooManyRequests | RequestBurstTooFast | System protection triggered by request burst. Please slow down traffic growth and increase requests gradually before retrying. | 请求量激增触发系统保护,请放缓流量提升速度,逐步增加请求量后再尝试 说明调用 doubao-seed-2-0及之后版本模型触发突增流量限制时,返回此错误码。 |
客户端处理策略#
业务评估存在突发流量限流情况,推荐以下主动优化策略,进一步提升请求成功率并规避限流风险。
- 客户端整流,主动平滑流量曲线:通过缓冲机制(如消息队列、本地任务队列)将瞬时突发流量转化为低波动请求,控制对平台的流量增长斜率。从源头避免因斜率过高触发
ServerOverloaded错误。 - 客户端分流,跨模型负载均衡:利用方舟多模型的独立突发容忍能力,将流量分散至多个具备冗余容量的模型,避免单一模型资源耗尽。
- 购买模型单元,主动扩容资源池:购买模型单元,预测自己的流量波动趋势,提前15分钟下单扩容购买模型单元,用扩容到的模型单元来承接自己的突发增量。
策略选择建议
| 业务场景 | 推荐组合策略 | 方舟能力支撑点 |
|---|---|---|
| 不可预测瞬时峰值(热点事件) | 整流+分流 | 动态容限曲线+多模型资源隔离 |
| 可预测周期性突发(定时任务) | 分流+购买模型单元 | 提前15分钟扩容+独立资源池 |
| 高稳定性要求核心业务 | 整流为主+购买备用单元 | 稳定承载区间+资源优先级保障 |
实践 通过 header 配置(推荐)#
对于无需即时完成的推理请求,可以通过请求 header 字段传入标识。方舟服务端会检测对应标识,尽可能在指定时间响应请求。
平台对配置了 header 排队标识的突发流量请求,会按照策略重试排队中的请求,直到请求开始处理或者排队超时。对比普通请求来说,在触发突发流量限流时,不是直接报错,而是在排队时间内帮用户进行重试。
核心配置#
| header 字段 | 示例 | 说明 |
|---|---|---|
| X-Ark-Max-Wait-Timeout-Ms | 300000 | 突发请求的最大排队时间。 单位:毫秒 建议值:[60000,600000],1~10分钟 常见配置:
|
超时时间配置#
长请求(深度思考、长输入输出)需关注超时时间,避免因叠加了排队时间,客户端超时关闭连接,导致请求失败。
非流式请求(stream:false)
- 超时时间:
原基础超时时间(非突发场景)+突发请求最大排队时间
流式请求(stream:true)
- 超时时间:>
突发请求最大排队时间
注意:如使用方舟 Go SDK ,流式输出请求超时时间也需设置
原基础超时时间(非突发场景)+突发请求最大排队时间
举例
请求原基础超时时间(非突发场景下) 30 分钟,突发请求最大排队时间 5 分钟。
-
非流式输出超时时间: 35 分钟
-
流式输出的请求超时时间
- 方舟 Python/Java SDK:大于 5 分钟( SDK 默认超时时间 10 分钟,无需重新配置)
注意:如使用方舟 Go SDK ,流式输出请求超时时间也需设置 35 分钟
示例代码#
示例代码
import os
from arkruntime import Ark
client = Ark(
api_key=os.environ.get("ARK_API_KEY"),
base_url="https://ark.cn-beijing.volces.com/api/v3",
# 非流式输出:超时时间 = 排队时间 + 基础超时时间 = 300 秒 + 1800秒
timeout=2100,
)
completion = client.chat.completions.create(
# Replace with Model ID
model="doubao-seed-2-1-pro-260915",
messages = [
{"role": "user", "content": "常见的十字花科植物有哪些?"},
],
# 自定义request,突发流量最大排队时间 300000 毫秒(5分钟)
extra_headers={"X-Ark-Max-Wait-Timeout-Ms": "300000"},
stream = False,
)
print(completion.choices[0].message.content)
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/volcengine/ark-runtime-go/arkruntime"
"github.com/volcengine/ark-runtime-go/arkruntime/model/chat"
)
const (
ArkModel = "doubao-seed-2-1-pro-260915"
ArkBaseUrl = "https://ark.cn-beijing.volces.com/api/v3"
)
var (
ArkApiKey = os.Getenv("ARK_API_KEY")
)
func callArk(msg string) (string, error) {
client := arkruntime.NewClientWithApiKey(
ArkApiKey,
arkruntime.WithBaseUrl(ArkBaseUrl),
)
// 非流式输出:超时时间 = 排队时间 + 基础超时时间 = 300 秒 + 1800秒
ctx, cancel := context.WithTimeout(context.Background(), 2100*time.Second)
defer cancel()
resp, err := client.CreateChatCompletion(
ctx,
&chat.ChatCompletionRequest{
Model: ArkModel,
Messages: []chat.ChatCompletionRequestMessage{
userMessage(msg),
},
},
arkruntime.WithCustomHeaders(map[string]string{
// 自定义request,突发流量最大排队时间 300000 毫秒 (5分钟)
"X-Ark-Max-Wait-Timeout-Ms": "300000",
}),
)
if err != nil {
return "", err
}
if len(resp.Choices) > 0 && resp.Choices[0].Message.Content != "" {
return resp.Choices[0].Message.Content, nil
}
return "", fmt.Errorf("empty response")
}
func main() {
result, err := callArk("常见的十字花科植物有哪些?")
if err != nil {
log.Fatalf("调用失败: %v", err)
}
fmt.Println(result)
}
func userMessage(content string) chat.ChatCompletionRequestMessage {
return chat.ChatCompletionRequestMessage{
OneOf: chat.NewChatCompletionRequestUserMessageChatCompletionRequestMessageSum(
chat.ChatCompletionRequestUserMessage{
Role: chat.ChatCompletionRequestUserMessageRoleUser,
Content: chat.NewOptNilChatCompletionMessageContent(chat.NewStringChatCompletionMessageContent(content)),
},
),
}
}
package com.ark.sample;
import com.volcengine.ark.runtime.models.chat.*;
import com.volcengine.ark.runtime.service.ArkService;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Header {
private static final String ARK_MODEL = "doubao-seed-2-1-pro-260915";
private static final String ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
private static final String ARK_API_KEY = System.getenv("ARK_API_KEY");
public static String callArk(String msg) {
ArkService service = ArkService.builder()
.apiKey(ARK_API_KEY)
.baseUrl(ARK_BASE_URL)
// 非流式输出:超时时间 = 排队时间 + 基础超时时间 = 300 秒 + 1800秒
.timeout(Duration.ofSeconds(2100))
.build();
final List<ChatCompletionRequestMessage> messages = new ArrayList<>();
final ChatCompletionRequestMessage userMessage = ChatCompletionRequestUserMessage.builder()
.role(ChatCompletionRequestMessageType.USER)
.content(ChatCompletionMessageContent.ofString(msg))
.build();
messages.add(userMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(ARK_MODEL)
.messages(messages)
.build();
Map<String, String> customHeaders = new HashMap<>();
// 自定义request,突发流量最大排队时间 300000 毫秒 (5分钟)
customHeaders.put("X-Ark-Max-Wait-Timeout-Ms", "300000");
String result = (String) service.createChatCompletion(chatCompletionRequest, customHeaders)
.getChoices()
.get(0)
.getMessage()
.getContent();
service.shutdownExecutor();
return result;
}
public static void main(String[] args) {
try {
String result = callArk("常见的十字花科植物有哪些?");
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
实践 客户端削峰填谷#
设计逻辑#
Client端突发峰值请求 → MQ无限制接收(削峰) → 消费端斜率可控匀速消费(填谷) → 方舟SDK调用大模型
- 削峰:RabbitMQ 做流量蓄水池,生产者无任何速率限制,承接所有瞬时峰值请求,彻底隔离峰值与大模型服务;
- 填谷:消费端以平稳速率消费 MQ 积压请求,充分利用大模型服务能力,无资源浪费;
- 斜率可控:消费速率从
5QPS开始,每 5 秒增长 2QPS,直到 30QPS 封顶,增长斜率恒定,无任何陡增,不会打满大模型服务; - 可靠性:MQ 队列 + 消息持久化、手动 ACK 确认,消息零丢失;失败请求自动重回队列重试。
示例代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import json
import os
import threading
# 安装SDK: python -m pip install --upgrade arkruntime pika
from arkruntime import Ark
import pika
# ===================== 全局极简配置【所有参数可一键修改】=====================
MQ_QUEUE = "client_ark_queue" # MQ削峰队列
MQ_CONF = pika.ConnectionParameters('127.0.0.1', 5672, credentials=pika.PlainCredentials('guest', 'guest'))
ARK_API_KEY = os.getenv('ARK_API_KEY') # 获取API Key: https://ark.volcengine.com/region:cn-beijing/apikey
ARK_MODEL = "doubao-seed-2-1-pro-260915"
# 🔥 斜率可控核心配置 🔥 斜率=步长/间隔 = 2/5 = 0.4QPS/秒,绝对无陡增
FLOW = {"curr_qps": 5, "max_qps": 30, "step": 2, "interval": 5, "count": 0}
LOCK = threading.Lock()
# ===================== 1. 极简斜率可控流控【核心】=====================
def flow_schedule():
"""后台线程:匀速增长消费速率,控制斜率"""
while True:
time.sleep(FLOW["interval"])
if FLOW["curr_qps"] < FLOW["max_qps"]:
FLOW["curr_qps"] += FLOW["step"]
def reset_count():
"""后台线程:每秒重置请求计数器,精准控QPS"""
while True:
time.sleep(1)
with LOCK: FLOW["count"] = 0
def allow():
"""流控校验:是否允许发起本次请求"""
with LOCK:
if FLOW["count"] < FLOW["curr_qps"]:
FLOW["count"] += 1
return True
return False
# ===================== 2. MQ生产者【Client端请求投递,削峰】=====================
def producer(client_req):
"""投递Client请求到MQ,无速率限制,承接峰值流量"""
conn = pika.BlockingConnection(MQ_CONF)
ch = conn.channel()
ch.queue_declare(queue=MQ_QUEUE, durable=True) # 队列持久化
ch.basic_publish(exchange='', routing_key=MQ_QUEUE, body=json.dumps(client_req),
properties=pika.BasicProperties(delivery_mode=2)) # 消息持久化
conn.close()
# ===================== 3. 方舟SDK调用【大模型请求核心】=====================
def call_ark(msg):
"""✅ 标准使用指定方舟SDK,极简调用大模型"""
client = Ark(
api_key=ARK_API_KEY,
base_url='https://ark.cn-beijing.volces.com/api/v3',
)
resp = client.chat.completions.create(model=ARK_MODEL, messages=[{"role": "user", "content": msg}])
return resp.choices[0].message.content
# ===================== 4. MQ消费者【填谷+斜率控速+调用方舟,核心主逻辑】=====================
def consumer():
"""消费端核心:削峰后的流量,斜率可控匀速消费,调用方舟大模型"""
conn = pika.BlockingConnection(MQ_CONF)
ch = conn.channel()
ch.queue_declare(queue=MQ_QUEUE, durable=True)
ch.basic_qos(prefetch_count=1) # 防止消费积压
def callback(_ch, method, _, body):
req = json.loads(body)["msg"]
if not allow(): # 流控校验,不通过则消息重回队列
_ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
return
try:
ark_resp = call_ark(req) # 调用方舟大模型
print(f"请求:{req[:15]} | 响应:{ark_resp[:20]}")
_ch.basic_ack(delivery_tag=method.delivery_tag) # 消费成功手动ACK
except Exception as e:
_ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True) # 失败重试
ch.basic_consume(queue=MQ_QUEUE, on_message_callback=callback, auto_ack=False)
print(f"消费启动|初始QPS:{FLOW['curr_qps']} 最大QPS:{FLOW['max_qps']} 斜率可控")
ch.start_consuming()
# ===================== 启动入口 =====================
if __name__ == '__main__':
# 启动流控后台线程
threading.Thread(target=flow_schedule, daemon=True).start()
threading.Thread(target=reset_count, daemon=True).start()
# 方式1: 模拟Client端峰值流量【生产端】- 按需执行
# for i in range(100): producer({"msg": f"大模型请求{i}: 技术方案总结"})
# 方式2: 启动消费端【填谷+控速+调用方舟】- 主运行
consumer()
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"sync"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
// ===================== 全局配置 =====================
const (
MQQueue = "client_ark_queue"
MQUrl = "amqp://guest:guest@127.0.0.1:5672/"
ArkModel = "doubao-seed-2-1-pro-260915"
ArkBaseUrl = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
)
var (
// Get API Key: https://ark.volcengine.com/region:cn-beijing/apikey
ArkApiKey = os.Getenv("ARK_API_KEY")
MockMode = false
)
// FlowConfig 流控配置
type FlowConfig struct {
CurrQPS int
MaxQPS int
Step int
Interval time.Duration
Count int
mu sync.Mutex
}
// 全局流控对象
var flow = &FlowConfig{
CurrQPS: 5,
MaxQPS: 30,
Step: 2,
Interval: 5 * time.Second,
Count: 0,
}
// ===================== 1. 极简斜率可控流控【核心】=====================
// startFlowSchedule 后台协程:匀速增长消费速率,控制斜率
func startFlowSchedule() {
ticker := time.NewTicker(flow.Interval)
defer ticker.Stop()
for range ticker.C {
flow.mu.Lock()
if flow.CurrQPS < flow.MaxQPS {
flow.CurrQPS += flow.Step
// 确保不超过最大值
if flow.CurrQPS > flow.MaxQPS {
flow.CurrQPS = flow.MaxQPS
}
log.Printf("QPS Limit Increased: Current=%d, Max=%d", flow.CurrQPS, flow.MaxQPS)
}
flow.mu.Unlock()
}
}
// startResetCount 后台协程:每秒重置请求计数器,精准控QPS
func startResetCount() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
flow.mu.Lock()
flow.Count = 0
flow.mu.Unlock()
}
}
// allow 流控校验:是否允许发起本次请求
func allow() bool {
flow.mu.Lock()
defer flow.mu.Unlock()
if flow.Count < flow.CurrQPS {
flow.Count++
return true
}
return false
}
// ===================== 3. 方舟API调用【大模型请求核心】=====================
type ArkRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ArkResponse struct {
Choices []struct {
Message Message `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error,omitempty"`
}
// callArk 调用方舟大模型API
func callArk(msg string) (string, error) {
if MockMode {
time.Sleep(50 * time.Millisecond) // 模拟网络延迟
return "Mocked Response: " + msg, nil
}
reqBody := ArkRequest{
Model: ArkModel,
Messages: []Message{
{Role: "user", Content: msg},
},
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", ArkBaseUrl, bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+ArkApiKey)
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API error: status=%d, body=%s", resp.StatusCode, string(body))
}
var arkResp ArkResponse
if err := json.Unmarshal(body, &arkResp); err != nil {
return "", err
}
if arkResp.Error != nil {
return "", fmt.Errorf("API error: %s", arkResp.Error.Message)
}
if len(arkResp.Choices) > 0 {
return arkResp.Choices[0].Message.Content, nil
}
return "", fmt.Errorf("empty choices in response")
}
// ===================== 4. MQ消费者【填谷+斜率控速+调用方舟,核心主逻辑】=====================
// startConsumer 消费端核心:削峰后的流量,斜率可控匀速消费,调用方舟大模型
func startConsumer() {
conn, err := amqp.Dial(MQUrl)
if err != nil {
log.Fatalf("Failed to connect to RabbitMQ: %v", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatalf("Failed to open a channel: %v", err)
}
defer ch.Close()
// 声明队列 (Durable=true)
q, err := ch.QueueDeclare(
MQQueue, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
log.Fatalf("Failed to declare a queue: %v", err)
}
// 设置QoS (PrefetchCount=1)
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
if err != nil {
log.Fatalf("Failed to set QoS: %v", err)
}
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack (设置为false,手动ACK)
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
log.Fatalf("Failed to register a consumer: %v", err)
}
log.Printf("消费启动|初始QPS:%d 最大QPS:%d 斜率可控", flow.CurrQPS, flow.MaxQPS)
forever := make(chan bool)
go func() {
for d := range msgs {
// 解析消息
var reqData map[string]string
if err := json.Unmarshal(d.Body, &reqData); err != nil {
log.Printf("Error parsing JSON: %v", err)
d.Nack(false, false) // 无法解析,丢弃或放入死信队列
continue
}
msgContent := reqData["msg"]
// 流控校验
if !allow() {
// 不通过则消息重回队列
d.Nack(false, true)
// 稍微sleep一下避免空转太快
time.Sleep(10 * time.Millisecond)
continue
}
// 调用方舟大模型
respContent, err := callArk(msgContent)
if err != nil {
log.Printf("调用方舟失败: %v", err)
d.Nack(false, true) // 失败重试
} else {
// 打印日志 (截取部分长度)
reqSnippet := msgContent
if len(reqSnippet) > 15 {
reqSnippet = reqSnippet[:15]
}
respSnippet := respContent
if len(respSnippet) > 20 {
respSnippet = respSnippet[:20]
}
log.Printf("请求:%s | 响应:%s", reqSnippet, respSnippet)
// 消费成功手动ACK
d.Ack(false)
}
}
}()
<-forever
}
// ===================== 2. MQ生产者【Client端请求投递,削峰】=====================
// startProducer 投递Client请求到MQ,无速率限制,承接峰值流量
func startProducer(count int) {
conn, err := amqp.Dial(MQUrl)
if err != nil {
log.Fatalf("Failed to connect to RabbitMQ: %v", err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatalf("Failed to open a channel: %v", err)
}
defer ch.Close()
q, err := ch.QueueDeclare(
MQQueue, // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
if err != nil {
log.Fatalf("Failed to declare a queue: %v", err)
}
log.Printf("开始投递 %d 条消息...", count)
for i := 0; i < count; i++ {
msgContent := fmt.Sprintf("大模型请求%d: 技术方案总结", i)
reqData := map[string]string{"msg": msgContent}
body, _ := json.Marshal(reqData)
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "application/json",
Body: body,
})
if err != nil {
log.Fatalf("Failed to publish a message: %v", err)
}
}
log.Printf("成功投递 %d 条消息", count)
}
// ===================== 启动入口 =====================
func main() {
mode := flag.String("mode", "consumer", "运行模式: consumer | producer")
count := flag.Int("count", 100, "生产者发送消息数量")
mock := flag.Bool("mock", false, "是否开启Mock模式(不真实调用API)")
flag.Parse()
if *mock {
MockMode = true
log.Println("Mock模式已开启")
}
if *mode == "producer" {
startProducer(*count)
return
}
// 启动流控后台协程
go startFlowSchedule()
go startResetCount()
// 启动消费端
startConsumer()
}
实践 QPS 爬坡策略#
客户端通过实时监测服务端响应状态,动态调整请求速率,逐步提升吞吐量,从而有效避免服务限流、确保服务稳定性。
设计思路#
QPS 爬坡工作原理如下图所示,统计固定时间间隔的限流错误,来评估线上服务负载。
- 逐步提升请求发送速率(QPS);
- 当发现错误率提升时,按照策略降低或稳定 QPS;
- 当服务稳定后继续提升QPS,并循环1、2;
- 最终达到平台对应模型的总限流(TPM、RPM)。
核心机制1:动态调整速率#
QPS Climber (每秒查询率爬坡器)采用"试错-调整"策略,通过周期性评估请求成功率,动态调整请求速率。核心实现在 adjustQPS() 方法中。
示例代码
func (q *QPSClimber) adjustQPS() {
// 计算请求总数与错误率
acceptCount := float64(q.counter.Count(Accept))
serverOverloadedCount := float64(q.counter.Count(ServerOverloadedCode))
unknownCount := float64(q.counter.Count(UnknownCode))
errorAll := serverOverloadedCount + unknownCount
errorRatio := errorAll / acceptCount
// 根据错误率动态调整QPS
oldRate, newRate := float64(q.limiter.Limit()), float64(0)
switch {
case errorRatio >= q.options.StrictErrorRatio:
// 高错误率场景:降低请求速率
newRate = oldRate - oldRate*q.options.ProbeRatio
case errorRatio >= q.options.RelaxErrorRatio:
// 中等错误率场景:维持当前请求速率
newRate = oldRate
case errorRatio > 0:
// 低错误率场景:小幅提升请求速率进行探测
newRate = oldRate + oldRate*q.options.ProbeRatio
case errorRatio == 0:
// 零错误场景:更积极地提升请求速率
newRate = oldRate + oldRate*q.options.FastProbeRatio
// 防止请求量不足时速率无限增长
if acceptCount*1.5 < oldRate*q.options.AdjustInterval.Seconds() {
newRate = oldRate
}
}
// 应用速率调整并设置边界限制
finalRate := minMax(1, float64(q.options.MaxRate), newRate)
if finalRate != oldRate {
q.limiter.SetLimit(rate.Limit(finalRate))
q.limiter.SetBurst(int(finalRate))
}
}
核心机制2:客户端限流与重试策略#
QPS Climber 实现了两层保护机制:
- 客户端令牌桶限流:通过
golang.org/x/time/rate包实现令牌桶算法,确保请求不会超过设定的QPS上限 - 指数退避重试:对失败请求采用指数退避策略进行重试,避免短时间内反复冲击服务端
示例代码
核心实现在 DoRequest() 方法中:
func (q *QPSClimber) DoRequest(ctx context.Context, req *model.CreateChatCompletionRequest) (model.ChatCompletionResponse, error) {
// 初始化指数退避器
backOff := newExponentialBackoff(q.options.RetryBaseDelay, q.options.RetryMaxDelay)
for {
select {
case <-ctx.Done():
return model.ChatCompletionResponse{}, ctx.Err()
default:
}
// 客户端令牌桶限流
if !q.limiter.Allow() {
backOff.wait()
continue
}
// 记录已接受的请求并执行
q.Record(Accept)
resp, err := q.client.CreateChatCompletion(ctx, req)
if err == nil {
return resp, nil
}
// 错误处理与重试决策
statusCode, retry := needRetryError(err)
q.Record(convertToCode(statusCode))
if !retry {
return resp, err
}
// 指数退避等待
backOff.wait()
}
}
核心机制3:错误分类后智能路由#
将服务端响应错误分为下面几类:
ServerOverloadedCode:服务过载(如HTTP 429)InternalServerError:服务器内部错误(如HTTP 500)UnknownCode:其他请求错误(如 HTTP 400)
通过 convertToCode() 方法实现错误智能分类,为动态速率调整提供决策依据:
示例代码
func convertToCode(statusCode int) Code {
switch {
case statusCode >= http.StatusInternalServerError:
return InternalServerError
case statusCode == http.StatusTooManyRequests:
return ServerOverloadedCode
case statusCode == http.StatusOK:
return SuccessCode
default:
return UnknownCode
}
}
代码配置#
配置示例
func defaultOptions() *Options {
return &Options{
AdjustInterval: 10 * time.Second,
InitialRate: 10,
MaxRate: 1000,
StrictErrorRatio: 0.05,
RelaxErrorRatio: 0.01,
ProbeRatio: 0.05,
FastProbeRatio: 0.1,
RetryBaseDelay: 100 * time.Millisecond,
RetryMaxDelay: 3 * time.Second,
}
}
参数说明
| 参数名 | 默认值 | 推荐值范围 | 调优建议 |
|---|---|---|---|
| InitialRate | 10 | 5-50 | 根据服务初始承载能力设置,新服务建议从低值开始 |
| MaxRate | 1000 | 需根据服务配额估算 | 可根据业务请求使用量(TPM)和请求频率(RPM)来估算,建议设置为服务额定QPS的80%-90%,留有余量。 |
| AdjustInterval | 10s | 5-15s | 流量波动大时可适当缩短间隔。 |
| StrictErrorRatio | 0.05 | 0.03-0.1 | 错误率阈值1,错误率高于该阈值,会降低请求频率。 对稳定性要求高时可降低该值。 |
| RelaxErrorRatio | 0.01 | 0.005-0.05 | 允许少量错误时可适当提高该值。 |
| ProbeRatio | 0.05 | 0.03-0.1 | 速率调整步长,波动大时可降低步长。 |
| RetryBaseDelay | 100 ms | 50ms-500ms | 单位 ms,初始重试延迟,根据服务响应速度调整。 |
| RetryMaxDelay | 3 s | 3s-10s | 最大重试等待时间。 |
使用建议#
-
单例模式使用:在应用中创建单一的
QPSClimber实例,所有API调用共享该实例,避免资源浪费。 -
统一错误处理:通过
needRetryError()方法统一处理各类错误,确保重试决策一致性。 -
并发请求控制:结合Go语言的
sync.WaitGroup实现高效并发请求,同时通过QPS Climber控制总体速率。// 请求总量 var total atomic.Int64 total.Store(100000) // 并发请求示例 var wg sync.WaitGroup concurrency := 1000 wg.Add(concurrency) for i := 0; i < concurrency; i++ { go func() { defer wg.Done() for total.Add(-1) >= 0 { // 业务请求代码 req := &model.CreateChatCompletionRequest{...} _, err := qpsClimber.DoRequest(ctx, req) // 错误处理 } }() } wg.Wait() -
预热机制:应用启动时设置较低的初始QPS,通过
adjustInterval逐步提升,避免冷启动时的流量冲击,导致被熔断。 -
自适应调整:利用QPS Climber的动态调整能力,让系统自动适应流量变化。
-
监控告警:关注QPS调整日志,设置错误率、成功率等关键指标的监控告警。
-
降级预案:在极端情况下,考虑实现请求降级或熔断机制,确保核心功能可用。
完整项目代码#
- Go
- Python
实践 退避重试策略#
使用退避算法进行请求重试,是避免限流的简单且有效方法(不仅仅在突发流量场景),其中随机指数退避算法实现自动重试请求。
核心设计思路#
当遇到速率限制错误时,先进行短暂休眠,随后重新发起未成功的请求。若请求依旧失败,系统会增加休眠时长,再次尝试,该过程会持续进行,直至请求成功或达到最大重试次数。
具备如下优势:
- 自动重试功能可使系统从速率限制错误中恢复,避免崩溃和数据丢失。
- 指数退避特性能快速进行首次重试。若前几次重试失败,较长的延迟时间可减少速率限制的触发。
- 在等待时间中加入随机抖动,避免大量请求同时重试。
示例代码#
示例代码
Tenacity 是一个第三方 Python 库,可用于简化重试的实现。下面示例是如何为一个请求增加随机指数退避。
import os
# 升级方舟 SDK 到最新版本 pip install -U arkruntime
from arkruntime import Ark
# 引入指数退避(exponential backoff)算法包
from tenacity import retry, stop_after_attempt, wait_random_exponential
client = Ark(
# 从环境变量中读取您的方舟API Key
api_key=os.environ.get("ARK_API_KEY"),
base_url="https://ark.cn-beijing.volces.com/api/v3",
)
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def completion_with_backoff(**kwargs):
return client.chat.completions.create(**kwargs)
resp = completion_with_backoff(
model="doubao-seed-2-1-pro-260915", messages=[{"role": "user", "content": "hello"}]
)
print(resp.choices[0].message.content)
实践 使用TPM保障包#
在可预测或者重点保障的业务,为了应对流量突增,可考虑购买 TPM 保障包来覆盖突发流量,方舟允许 TPM 保障包额度内的突发流量,提供更高的 SLA 和资源优先级。用户无需变更任何代码,只需在指定的接入点上购买 TPM 保障包即可。详细介绍及购买指引见 在线推理(TPM 保障包) 。