跳到主内容

限流与配额

API 参考

限流与配额

EnerOS 通过 eneros-gateway 实施分层限流,保护内核稳定并实现多租户公平调度。限流策略覆盖 IP、租户、用户、端点四个维度,可在配置文件中调整,运行时通过 eneros-tenant 动态下发配额。所有限流响应都附带标准 X-RateLimit-* 响应头,便于客户端实现自适应降速。

限流架构

EnerOS 采用四层限流模型,每一层都有独立的速率与突发上限:

请求 → ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
       │  IP 限流  │ →  │ 租户限流  │ →  │ 用户限流  │ →  │ 端点限流  │ → 内核
       └─────────┘    └─────────┘    └─────────┘    └─────────┘
         令牌桶          令牌桶         令牌桶         令牌桶
层级维度算法作用
L1客户端 IP令牌桶防恶意刷接口、DDoS 缓解
L2租户(Tenant)令牌桶多租户公平调度、配额管理
L3用户/Agent令牌桶单用户限流、防滥用
L4端点(Endpoint)漏桶保护计算密集型端点

每层独立计数,任意一层触发即返回 429,响应头中携带触发层的详细信息。

限流策略

1. 全局 IP 限流

按 IP 维度统计,防止恶意刷接口与 DDoS 攻击。所有 IP 共享同一策略,黑名单 IP 直接拒绝。

接口类型速率突发说明
公开端点(/health60 req/min10无需鉴权
认证端点(/auth/token30 req/min5防爆破
认证端点(其他)600 req/min50默认
计算类端点(/powerflow/opf30 req/min5保护算力
时序写入(/timeseries/batch1000 req/min100高吞吐
WebSocket 连接建立10 req/min2防连接洪水
SSE 连接建立20 req/min5防连接洪水

2. 租户配额

按租户维度分配资源配额,超限返回 429 或排队等待。配额可在 eneros.toml 中配置,也可通过 eneros-tenant 动态调整。

资源默认配额超限行为是否可调
API 请求5000 req/h排队等待(最多 100)
WebSocket 连接20 并发拒绝新连接
SSE 连接50 并发拒绝新连接
潮流计算任务100 并发排队(FIFO)
时序写入1M 点/天拒绝写入
时序存储10 GB/天拒绝写入
审计日志保留365 天自动归档
Agent 数量100拒绝创建
网络模型数量50拒绝创建

3. 用户/Agent 限流

针对单个用户或 Agent 的限流,防止单一实体占用过多资源。

资源默认限制说明
单用户 API 请求1200 req/min普通用户
单 Agent 指令频率10 cmd/s防止 Agent 失控
单用户 WebSocket5 并发
单用户 SSE10 并发

4. 端点限流

针对特定端点的细粒度限流,保护计算密集型或高资源消耗操作。

端点速率突发说明
POST /networks/{id}/powerflow30 req/min5潮流计算
POST /networks/{id}/opf10 req/min2最优潮流
POST /networks/{id}/validate60 req/min10拓扑校验
POST /agents/{id}/command100 req/min20Agent 指令
POST /timeseries/batch1000 req/min100批量写入
DELETE /networks/{id}10 req/min2删除保护

5. 指令限流

Agent 指令下发受安全网关二次校验:

指令类型限制说明
普通指令≤ 10 cmd/s单 Agent
控制类指令(开关/调节)100ms 最小间隔 + 双因子确认高危操作
紧急停机指令绕过限流,立即执行应急场景
批量指令≤ 50 cmd/batch单次批量上限

响应头说明

所有限流相关的响应头遵循 IETF 草案 draft-ietf-httpapi-ratelimit-headers

响应头类型说明
X-RateLimit-Limitint当前窗口内允许的最大请求数
X-RateLimit-Remainingint当前窗口内剩余请求数
X-RateLimit-Resetint窗口重置时间(Unix 时间戳,秒)
X-RateLimit-Policystring限流策略描述,如 60;w=60 表示 60 秒窗口内 60 次
Retry-Afterint建议重试等待秒数(仅 429 响应)
X-RateLimit-Tenant-Limitint租户配额上限
X-RateLimit-Tenant-Remainingint租户配额剩余
X-RateLimit-Concurrentint当前并发连接数(仅 WS/SSE)

成功响应示例:

HTTP/1.1 200 OK
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 1783277460
X-RateLimit-Policy: 600;w=60
X-RateLimit-Tenant-Limit: 5000
X-RateLimit-Tenant-Remaining: 4998
Content-Type: application/json

限流响应

触发限流时返回 HTTP 429,响应体包含详细的限流信息:

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded: 600 req/min",
    "details": {
      "limit": 600,
      "window_seconds": 60,
      "retry_after": 12,
      "reset_at": "2026-07-06T08:31:00Z",
      "scope": "ip",
      "ip": "192.168.1.10"
    },
    "trace_id": "trace_abc123"
  },
  "request_id": "req_20260706_001"
}
字段类型说明
error.codestring固定为 RATE_LIMITED
error.details.limitint当前窗口允许的最大请求数
error.details.window_secondsint限流窗口(秒)
error.details.retry_afterint建议重试等待秒数
error.details.reset_atstring窗口重置时间(RFC 3339)
error.details.scopestring触发层级:ip/tenant/user/endpoint
error.details.ipstring触发 IP(仅 scope=ip)
error.details.tenant_idstring触发租户(仅 scope=tenant)

配额管理

查询当前配额

通过 GET /api/v1/tenant/quota 查询当前租户的配额与使用情况:

请求参数:无

响应字段

字段类型说明
tenant_idstring租户 ID
planstring套餐:free/pro/enterprise
quotasobject各项配额上限
usageobject当前使用量
reset_atstring配额重置时间

cURL 示例

curl http://localhost:8080/api/v1/tenant/quota \
  -H "Authorization: Bearer <token>"

响应示例

{
  "data": {
    "tenant_id": "tenant_default",
    "plan": "pro",
    "quotas": {
      "api_requests_per_hour": 5000,
      "ws_connections": 20,
      "sse_connections": 50,
      "concurrent_powerflow": 100,
      "timeseries_daily_gb": 10,
      "agent_count": 100,
      "network_count": 50
    },
    "usage": {
      "api_requests_this_hour": 1234,
      "ws_connections": 3,
      "sse_connections": 12,
      "concurrent_powerflow": 2,
      "timeseries_today_gb": 2.3,
      "agent_count": 15,
      "network_count": 8
    },
    "reset_at": "2026-07-06T09:00:00Z"
  },
  "request_id": "req_20260706_002",
  "timestamp": "2026-07-06T08:30:00Z"
}

配额申请

当默认配额不足时,可通过以下方式申请提升:

套餐API 请求WS 连接SSE 连接时序存储适用场景
free1000/h5101 GB/天试用、开发
pro5000/h205010 GB/天小型生产
enterprise50000/h100200100 GB/天大型生产
custom定制定制定制定制特殊需求

申请流程:

  1. 联系 EnerOS 商务团队(sales@openeneros.com
  2. 提供租户 ID、预期用量、业务场景
  3. 签署补充协议
  4. 运维通过 eneros-tenant 动态下发新配额,无需重启

动态调整配额

管理员可通过 API 动态调整租户配额:

curl -X PUT http://localhost:8080/api/v1/admin/tenants/tenant_default/quota \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "api_requests_per_hour": 10000,
    "ws_connections": 50,
    "concurrent_powerflow": 200,
    "reason": "promotion_campaign",
    "expires_at": "2026-07-13T00:00:00Z"
  }'
字段类型必填默认值说明
api_requests_per_hourintAPI 请求配额
ws_connectionsintWebSocket 连接配额
sse_connectionsintSSE 连接配额
concurrent_powerflowint并发潮流任务
timeseries_daily_gbint时序存储配额
reasonstring调整原因(审计)
expires_atdatetime临时配额过期时间

配置示例

eneros.toml 中配置限流策略:

[gateway.rate_limit]
# 全局 IP 限流
global_rpm = 600              # 每分钟最大请求数
global_burst = 50             # 突发上限
auth_rpm = 30                 # 认证端点
auth_burst = 5
compute_rpm = 30              # 计算类端点
compute_burst = 5
timeseries_rpm = 1000         # 时序写入
timeseries_burst = 100

# 租户配额默认值
[gateway.tenant_default]
api_requests_per_hour = 5000
ws_connections = 20
sse_connections = 50
concurrent_powerflow = 100
timeseries_daily_gb = 10
agent_count = 100
network_count = 50

# 端点级限流
[gateway.endpoint_limits]
"/networks/{id}/powerflow" = { rpm = 30, burst = 5 }
"/networks/{id}/opf" = { rpm = 10, burst = 2 }
"/agents/{id}/command" = { rpm = 100, burst = 20 }

# IP 黑白名单
[gateway.ip_filter]
whitelist = ["127.0.0.1", "10.0.0.0/8"]
blacklist = []
blacklist_ttl_seconds = 3600

# WebSocket 限流
[gateway.ws]
max_connections_per_tenant = 20
max_connections_per_user = 5
max_subscriptions_per_connection = 50
message_rate_per_second = 100

# SSE 限流
[gateway.sse]
max_connections_per_tenant = 50
max_connections_per_user = 10
max_topics_per_connection = 10

客户端最佳实践

1. 指数退避重试

import time
import random
import requests

def request_with_retry(url, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            # 添加抖动,避免重试风暴
            jitter = random.uniform(0, 0.5)
            sleep_time = retry_after + jitter
            print(f"限流,{sleep_time:.1f}s 后重试(第 {attempt + 1} 次)")
            time.sleep(sleep_time)
            continue
        return response
    raise Exception(f"达到最大重试次数: {max_retries}")

2. 自适应降速

import time
import requests

class AdaptiveClient:
    def __init__(self, base_url, token):
        self.base_url = base_url
        self.headers = {"Authorization": f"Bearer {token}"}
        self.min_interval = 0.01  # 10ms
        self.max_interval = 1.0   # 1s
        self.current_interval = self.min_interval

    def request(self, path):
        time.sleep(self.current_interval)
        response = requests.get(f"{self.base_url}{path}", headers=self.headers)

        remaining = int(response.headers.get("X-RateLimit-Remaining", 100))
        if remaining < 10:
            # 剩余配额不足,降速
            self.current_interval = min(self.current_interval * 1.5, self.max_interval)
        else:
            # 配额充足,加速
            self.current_interval = max(self.current_interval * 0.9, self.min_interval)

        return response

3. 批量请求减少调用次数

# 错误:逐条写入
for point in points:
    client.timeseries.write(point)  # 1000 次请求

# 正确:批量写入
client.timeseries.batch_write(points)  # 1 次请求

4. 长连接复用

# 错误:反复建立 WebSocket 连接
for alert in alerts:
    ws = connect_websocket()
    ws.send(alert)
    ws.close()

# 正确:复用长连接
ws = connect_websocket()
for alert in alerts:
    ws.send(alert)
# 连接保持,订阅事件

5. 监控剩余配额

response = client.get("/networks")
remaining = response.headers.get("X-RateLimit-Remaining")
tenant_remaining = response.headers.get("X-RateLimit-Tenant-Remaining")

if int(remaining) < 50:
    logger.warning(f"IP 限流即将触发,剩余: {remaining}")
if int(tenant_remaining) < 500:
    logger.warning(f"租户配额即将耗尽,剩余: {tenant_remaining}")

Rust 客户端示例

use std::time::{Duration, Instant};
use tokio::time::sleep;

pub struct RateLimitedClient {
    client: reqwest::Client,
    base_url: String,
    token: String,
    last_request: Instant,
    min_interval: Duration,
}

impl RateLimitedClient {
    pub fn new(base_url: &str, token: &str, rpm: u32) -> Self {
        Self {
            client: reqwest::Client::new(),
            base_url: base_url.to_string(),
            token: token.to_string(),
            last_request: Instant::now() - Duration::from_secs(1),
            min_interval: Duration::from_secs(60) / rpm,
        }
    }

    pub async fn get(&mut self, path: &str) -> anyhow::Result<reqwest::Response> {
        let elapsed = self.last_request.elapsed();
        if elapsed < self.min_interval {
            sleep(self.min_interval - elapsed).await;
        }
        self.last_request = Instant::now();

        let resp = self.client
            .get(format!("{}{}", self.base_url, path))
            .bearer_auth(&self.token)
            .send()
            .await?;

        if resp.status() == 429 {
            let retry_after = resp.headers()
                .get("Retry-After")
                .and_then(|v| v.to_str().ok())
                .and_then(|v| v.parse::<u64>().ok())
                .unwrap_or(60);
            sleep(Duration::from_secs(retry_after)).await;
            return self.get(path).await;
        }

        Ok(resp)
    }
}

监控与告警

EnerOS 暴露以下限流指标,可通过 Prometheus 抓取:

指标类型说明
eneros_rate_limit_requests_totalcounter请求总数(按 status、scope 维度)
eneros_rate_limit_rejected_totalcounter被限流拒绝的请求数
eneros_rate_limit_remaininggauge当前剩余配额
eneros_ws_connectionsgauge当前 WebSocket 连接数
eneros_sse_connectionsgauge当前 SSE 连接数
eneros_powerflow_runninggauge运行中的潮流任务数
eneros_tenant_quota_usage_ratiogauge租户配额使用率(0-1)

推荐告警规则:

告警条件严重级别
限流拒绝率高rate(rejected_total[5m]) / rate(requests_total[5m]) > 0.1warn
租户配额即将耗尽quota_usage_ratio > 0.9warn
WebSocket 连接数高ws_connections > 0.8 * limitwarn
潮流任务排队powerflow_running > 0.8 * limitwarn

相关文档