文章内容

TickDB 的延迟 SLA:99% 的请求在多少毫秒内返回?

开篇

"SLA 不是承诺给你的数字,是你在生产环境中实际量出来的分布。"

做量化系统的工程师大概都见过这样的场景:供应商拍着胸脯说"延迟 < 50ms",结果盘前试了一下还行,财报季一跑——连接断了,数据丢了,P99 直接飙到两秒开外。合同里的 SLA 是一回事,生产环境里真实的尾延迟分布是另一回事。

本文不对任何竞品做对比,只做一件事:把 TickDB 官方延迟 SLA 的条款拆干净,把极端行情下的实测方法讲清楚,让你自己验证。代码会给出完整的延迟监控工具,拿来就能跑。


一、为什么"P99 < Xms"是一个有误导性的指标

在进入 TickDB 的 SLA 之前,有必要先说清楚延迟这个指标本身的问题。

均值和 P50 是安慰剂,P99 才是生产环境的真实门槛。 一家 API 服务商说"平均延迟 20ms",你拿到手会发现 99% 的请求确实在 20ms 以内——但剩下的 1% 恰好是你策略最需要数据的时刻:财报发布的前三秒、期权到期日的最后十分钟。

量化场景里的关键问题是路径相关性:你的策略在市场波动最剧烈的 1% 时间窗口里亏的钱,往往能抹掉全年收益。延迟 SLA 如果只看平均值或 P50,等于用晴天的平均车速来评估F1赛道的通过能力。

延迟指标的正确认知框架应该是这样的:

指标 含义 量化场景下的参考价值
P50(中位数) 一半请求在此之下 日常监控基线,低值无意义
P95 95% 请求在此之下 个人开发环境够用
P99 99% 请求在此之下 生产环境最低门槛
P99.9 99.9% 请求在此之下 高频/事件驱动策略必须关注
最大延迟 样本中的最坏情况 排查异常,不用于 SLA 评估

TickDB 官方在文档中给出的延迟承诺,锚定的是 P99 指标。以下是中国区与全球区的核心承诺区间。


二、TickDB 延迟 SLA 条款拆解

2.1 服务等级承诺

TickDB 的 SLA 按服务区域和调用方式划分,不同接口的延迟基线存在差异:

接口类型 调用方式 中国区 P99 基线 全球区 P99 基线 测量窗口
REST /kline 同步 HTTP < 80ms < 120ms 滚动 30 天
REST /symbols/available 同步 HTTP < 50ms < 80ms 滚动 30 天
WebSocket 推送 长连接 < 100ms < 150ms 滚动 30 天

这里的 P99 是服务端统计,即从 TickDB 接收到请求到发出响应的时间,不包含网络链路延迟。换言之,你在办公室测到的端到端延迟,要在这组数字上叠加 10-60ms 的公网传输开销(视地理位置而定)。

2.2 限频机制的延迟惩罚

SLA 承诺有一个重要的前提条件:调用频率在协议限制之内。当请求触发限频响应(错误码 3001)时,延迟不再受 SLA 保护,响应时间由 Retry-After 头指定的重试间隔决定。

这意味着两件事:

  1. 在设计阶段就要算清楚你的并发量和 QPS,确保落在免费层或付费层的配额以内。
  2. 限频时的延迟是确定性的,可以预测和规划,不存在随机抖动。

免费层的限频阈值约为每分钟 60 次请求。如果你的策略在财报前集中发请求,很可能触发限频,此时延迟从 <100ms 退化为手动重试间隔(默认 5 秒起)。

2.3 SLA 保障的排除条款

标准 SLA 存在以下排除项,了解这些边界对生产设计至关重要:

  • 网络层故障:你的客户端到 TickDB 服务端之间的网络抖动不在 SLA 范围内。这是为什么监控端到端延迟比依赖服务端 SLA 更有价值。
  • 极端行情触发系统保护:当市场出现短时间内的极端波动(如美股个股 5 分钟内涨跌超过 20%),TickDB 可能启动临时限流机制,此时 SLA 暂停适用。
  • 计划内维护窗口:官方提前 72 小时公告的维护窗口,期间服务降级不计入 SLA。
  • 不可抗力:网络运营商故障、区域级断网等。

对于量化团队而言,排除条款实际上划定了"需要你自己兜底"的场景边界。比如网络抖动这一项,意味着你的客户端必须有重连和重试逻辑,否则 SLA 写得再漂亮也白搭。


三、极端行情下的延迟表现:预期与实测方法

3.1 理论预期

理论上,TickDB 作为数据分发层,其延迟表现受两层因素制约:

服务端层:订单簿深度快照和 K 线聚合在 TickDB 内部完成,处理逻辑相对稳定,极端行情下不会产生额外的服务端计算瓶颈。但推送量会增加,如果你的连接是共享带宽的,可能感受到间接影响。

网络传输层:这才是极端行情下延迟波动的主要来源。交易所行情在极端波动期间,网络带宽争抢加剧,P99 延迟会有 15-30% 的增幅。

因此,在极端行情下,P99 从基线 80ms 上升至 100-120ms 是一个合理的预期区间,如果超过这个区间,通常意味着你的网络链路本身存在瓶颈。

3.2 实测方法:延迟监控工具

下面给出一套完整的延迟监控工具,可直接用于生产环境的数据采集。

import os
import time
import json
import requests
import statistics
from datetime import datetime, timedelta
from collections import defaultdict
import threading
import websocket


class TickDBSLAMonitor:
    """
    TickDB 延迟 SLA 监控工具
    用于验证服务端 P99 延迟承诺,并监控 WebSocket 推送延迟
    
    使用方式:
    1. 设置环境变量 TICKDB_API_KEY
    2. python tickdb_sla_monitor.py
    """
    
    def __init__(self, api_key: str = None):
        self.api_key = api_key or os.environ.get("TICKDB_API_KEY")
        if not self.api_key:
            raise ValueError("请设置环境变量 TICKDB_API_KEY")
        
        self.headers = {"X-API-Key": self.api_key}
        self.base_url = "https://api.tickdb.ai/v1"
        
        # 延迟存储,单位毫秒
        self.rest_latencies = []
        self.ws_push_latencies = []
        self._lock = threading.Lock()
        
        # 监控配置
        self.monitored_symbols = ["AAPL.US", "NVDA.US", "BTC.USDT"]
        self.test_interval = 5  # 秒,每次 REST 测试的间隔
        self.test_duration = 300  # 秒,测试总时长(5分钟)
        
    def _record_latency(self, latency_list: list, latency_ms: float):
        """线程安全地记录单次延迟"""
        with self._lock:
            latency_list.append(latency_ms)
    
    def test_rest_latency(self, symbol: str, interval: str = "1h") -> float:
        """
        测试 REST API 单次请求延迟
        
        Returns:
            延迟(毫秒),-1 表示请求失败
        """
        url = f"{self.base_url}/market/kline"
        params = {
            "symbol": symbol,
            "interval": interval,
            "limit": 100
        }
        
        start = time.perf_counter()
        try:
            response = requests.get(
                url,
                headers=self.headers,
                params=params,
                timeout=(3.05, 10)
            )
            elapsed_ms = (time.perf_counter() - start) * 1000
            
            if response.status_code == 200:
                return elapsed_ms
            elif response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 5))
                print(f"[WARN] 触发限频,等待 {retry_after} 秒后重试")
                time.sleep(retry_after)
                return -1
            else:
                print(f"[ERROR] HTTP {response.status_code}: {response.text[:100]}")
                return -1
                
        except requests.exceptions.Timeout:
            print(f"[ERROR] 请求超时(>{10}s)")
            return -1
        except Exception as e:
            print(f"[ERROR] 请求异常: {e}")
            return -1
    
    def run_rest_latency_test(self, duration_seconds: int = None) -> dict:
        """
        运行持续 REST 延迟测试
        
        Returns:
            包含 P50/P95/P99/最大延迟的统计字典
        """
        duration = duration_seconds or self.test_duration
        end_time = time.time() + duration
        symbol_idx = 0
        
        print(f"[INFO] 开始 REST 延迟测试,预计持续 {duration} 秒")
        print(f"[INFO] 测试标的: {self.monitored_symbols}")
        
        while time.time() < end_time:
            symbol = self.monitored_symbols[symbol_idx % len(self.monitored_symbols)]
            latency = self.test_rest_latency(symbol)
            
            if latency > 0:
                self._record_latency(self.rest_latencies, latency)
                status = "OK" if latency < 80 else "SLOW"
                print(f"[{datetime.now().strftime('%H:%M:%S')}] {symbol} | "
                      f"延迟: {latency:.1f}ms | {status}")
            
            # 轮换测试标的
            symbol_idx += 1
            
            # 控制请求频率,避免触发限频
            time.sleep(self.test_interval)
        
        return self._compute_rest_stats()
    
    def _compute_rest_stats(self) -> dict:
        """计算 REST 延迟统计指标"""
        if not self.rest_latencies:
            return {"error": "没有有效的延迟数据"}
        
        sorted_latencies = sorted(self.rest_latencies)
        n = len(sorted_latencies)
        
        def percentile(data, p):
            idx = int(len(data) * p / 100)
            # 边界处理
            idx = min(idx, len(data) - 1)
            return data[idx]
        
        stats = {
            "sample_count": n,
            "P50": round(percentile(sorted_latencies, 50), 2),
            "P95": round(percentile(sorted_latencies, 95), 2),
            "P99": round(percentile(sorted_latencies, 99), 2),
            "max": round(max(sorted_latencies), 2),
            "mean": round(statistics.mean(sorted_latencies), 2),
            "std": round(statistics.stdev(sorted_latencies), 2) if n > 1 else 0,
        }
        
        # SLA 对齐检查
        sla_p99_target = 80  # 中国区基线
        stats["sla_check"] = "PASS" if stats["P99"] <= sla_p99_target else "FAIL"
        
        return stats
    
    def on_ws_message(self, ws, message):
        """WebSocket 消息回调,估算推送延迟"""
        receive_time = time.perf_counter()
        
        try:
            data = json.loads(message)
            
            # 消息类型过滤:只统计 K 线快照
            msg_type = data.get("type") or data.get("data", {}).get("type", "")
            
            if msg_type in ("snapshot", "kline_snapshot"):
                # 从消息中提取服务器时间戳(如果存在)
                server_ts = data.get("ts") or data.get("data", {}).get("ts", 0)
                if server_ts:
                    # 服务器时间戳为毫秒级 Unix 时间
                    push_latency_ms = (receive_time * 1000) - server_ts
                    if 0 < push_latency_ms < 5000:  # 过滤异常值
                        self._record_latency(self.ws_push_latencies, push_latency_ms)
                        
        except json.JSONDecodeError:
            pass  # pong 消息等非 JSON 数据直接忽略
    
    def run_ws_latency_test(self, duration_seconds: int = 120):
        """
        运行 WebSocket 推送延迟监控
        
        说明:
        - 精确的推送延迟需要服务端时间戳,本工具采用客户端接收时间作为近似
        - 真实推送延迟 = 客户端接收延迟 - 网络链路延迟
        - 建议配合 _compute_rest_stats 的结果对比,以分离服务端贡献和网络贡献
        """
        print(f"[INFO] 开始 WebSocket 延迟测试,预计持续 {duration_seconds} 秒")
        
        # ⚠️ 生产环境高频场景建议使用 aiohttp/asyncio
        ws_url = f"wss://api.tickdb.ai/ws?api_key={self.api_key}"
        
        ws = websocket.WebSocketApp(
            ws_url,
            on_message=self.on_ws_message,
            on_error=lambda ws, err: print(f"[WS ERROR] {err}"),
            on_close=lambda ws, code, msg: print(f"[WS CLOSE] code={code} {msg}"),
        )
        
        def on_open(ws):
            print("[WS OPEN] 连接建立,开始订阅 K 线快照")
            for symbol in self.monitored_symbols:
                sub_msg = json.dumps({
                    "cmd": "sub",
                    "params": {
                        "type": "kline_snapshot",
                        "symbol": symbol,
                        "interval": "1m"
                    }
                })
                ws.send(sub_msg)
                print(f"[WS SUB] 订阅 {symbol} 成功")
            
            # 发送心跳(保持连接活跃)
            def send_ping():
                while ws.sock and ws.sock.connected:
                    ws.send(json.dumps({"cmd": "ping"}))
                    time.sleep(25)  # 25 秒心跳间隔
            
            ping_thread = threading.Thread(target=send_ping, daemon=True)
            ping_thread.start()
        
        ws.on_open = on_open
        
        # 运行指定时长后主动关闭
        timer = threading.Timer(duration_seconds, ws.close)
        timer.start()
        
        ws.run_forever(ping_timeout=30, ping_interval=25)
        timer.cancel()
        
        return self._compute_ws_stats()
    
    def _compute_ws_stats(self) -> dict:
        """计算 WebSocket 推送延迟统计"""
        if not self.ws_push_latencies:
            return {
                "error": "没有收到有效的推送消息(可能为订阅失败或无交易时段)",
                "hint": "建议在交易时段运行测试"
            }
        
        sorted_latencies = sorted(self.ws_push_latencies)
        
        def percentile(data, p):
            idx = int(len(data) * p / 100)
            idx = min(idx, len(data) - 1)
            return data[idx]
        
        return {
            "sample_count": len(sorted_latencies),
            "P50": round(percentile(sorted_latencies, 50), 2),
            "P95": round(percentile(sorted_latencies, 95), 2),
            "P99": round(percentile(sorted_latencies, 99), 2),
            "max": round(max(sorted_latencies), 2),
            "mean": round(statistics.mean(sorted_latencies), 2),
        }
    
    def generate_report(self, rest_stats: dict, ws_stats: dict) -> str:
        """生成 SLA 验证报告"""
        report = []
        report.append("=" * 60)
        report.append("TickDB 延迟 SLA 验证报告")
        report.append(f"生成时间: {datetime.now().isoformat()}")
        report.append("=" * 60)
        
        report.append("\n## REST API 延迟测试结果")
        if "error" not in rest_stats:
            report.append(f"  样本量: {rest_stats['sample_count']}")
            report.append(f"  P50:    {rest_stats['P50']} ms")
            report.append(f"  P95:    {rest_stats['P95']} ms")
            report.append(f"  P99:    {rest_stats['P99']} ms")
            report.append(f"  最大值: {rest_stats['max']} ms")
            report.append(f"  均值:   {rest_stats['mean']} ms")
            report.append(f"  标准差: {rest_stats['std']} ms")
            report.append(f"\n  SLA 对齐检查(P99 ≤ 80ms): {rest_stats['sla_check']}")
        else:
            report.append(f"  错误: {rest_stats['error']}")
        
        report.append("\n## WebSocket 推送延迟测试结果")
        if "error" not in ws_stats:
            report.append(f"  样本量: {ws_stats['sample_count']}")
            report.append(f"  P50:    {ws_stats['P50']} ms")
            report.append(f"  P95:    {ws_stats['P95']} ms")
            report.append(f"  P99:    {ws_stats['P99']} ms")
            report.append(f"  最大值: {ws_stats['max']} ms")
        else:
            report.append(f"  错误: {ws_stats['error']}")
            if "hint" in ws_stats:
                report.append(f"  建议: {ws_stats['hint']}")
        
        report.append("\n" + "=" * 60)
        report.append("注:以上为客户端视角的端到端延迟,包含网络链路开销。")
        report.append("服务端 SLA 基线(中国区 <80ms)需减去约 10-30ms 的网络传输时间。")
        report.append("=" * 60)
        
        return "\n".join(report)


def main():
    """
    主函数:运行完整 SLA 监控流程
    
    测试计划:
    1. REST API P99 延迟测试(5 分钟)
    2. WebSocket 推送延迟测试(2 分钟)
    3. 输出综合报告
    """
    monitor = TickDBSLAMonitor()
    
    print("[INFO] === TickDB SLA 监控启动 ===")
    print(f"[INFO] 测试标的: {monitor.monitored_symbols}")
    
    # 第一阶段:REST API 延迟测试
    rest_stats = monitor.run_rest_latency_test(duration_seconds=300)
    
    # 等待 10 秒,避免连接过快切换
    time.sleep(10)
    
    # 第二阶段:WebSocket 推送延迟测试
    ws_stats = monitor.run_ws_latency_test(duration_seconds=120)
    
    # 生成并打印报告
    report = monitor.generate_report(rest_stats, ws_stats)
    print("\n" + report)
    
    # 可选:保存报告到文件
    report_file = f"tickdb_sla_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
    with open(report_file, "w", encoding="utf-8") as f:
        f.write(report)
    print(f"\n[INFO] 报告已保存至: {report_file}")


if __name__ == "__main__":
    main()

工具说明

  • REST 测试通过轮换标的(AAPL.USNVDA.USBTC.USDT)避免单标的高频调用触发限频
  • WebSocket 测试通过心跳机制(25 秒间隔)保持连接活跃,防止空闲断开
  • 延迟统计剔除了网络抖动较大的异常值(阈值设为 5000ms)
  • 报告同时输出客户端视角的端到端延迟和 SLA 对齐检查结果

运行一次完整的测试需要约 7 分钟。建议在交易日的盘前(美东 09:15 左右)和盘中(美东 14:30-15:00)各跑一次,对比两个时段的数据,才能真正验证极端行情下的延迟承诺是否成立。


四、读懂 SLA 的正确方式:工程师的实践建议

SLA 文档是合同层面的承诺,而工程师真正需要关心的是可观测性。以下是在 TickDB 上构建可靠数据管道的三个实践建议。

4.1 在客户端埋点,不依赖服务端 SLA 报告

服务端 SLA 是 TickDB 对所有用户承诺的平均水平,但你的策略在实际交易中感受到的延迟,取决于你的网络路径、并发量和请求模式。

建议在客户端记录每一次 API 调用的延迟,建立自己的延迟日志:

import logging
from functools import wraps
import time

logger = logging.getLogger(__name__)

def log_api_latency(func):
    """装饰器:自动记录 API 调用的延迟"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        latency_ms = (time.perf_counter() - start) * 1000
        
        # 延迟分级告警
        if latency_ms > 200:
            logger.warning(f"[SLA] 延迟告警 | {func.__name__} | {latency_ms:.1f}ms > 200ms")
        elif latency_ms > 100:
            logger.info(f"[SLA] 延迟偏高 | {func.__name__} | {latency_ms:.1f}ms > 100ms")
        else:
            logger.debug(f"[SLA] 延迟正常 | {func.__name__} | {latency_ms:.1f}ms")
        
        return result
    return wrapper

这套客户端埋点的核心价值在于:当你的策略在某个时间窗口亏损时,你可以回溯当时的 API 延迟是否异常。 这比事后查 TickDB 的服务端报告要高效得多。

4.2 区分"延迟敏感"和"延迟容忍"的数据请求

TickDB 的不同接口设计天然适合不同的场景,合理分配请求类型能有效降低尾延迟的影响。

数据需求 推荐接口 延迟预期 说明
策略信号计算 REST /kline < 100ms P99 稳定,适合非实时决策
盘中监控展示 WebSocket kline_snapshot < 150ms 推送式,无需轮询
订单簿深度分析 WebSocket depth < 120ms 港股 10 档,美股 1 档
盘前预热/策略初始化 REST /symbols/available < 50ms 延迟最低,适合启动阶段

一个常见的错误是把所有数据请求都压在 REST 接口上,导致初始化阶段延迟叠加。把"盘中实时监控"和"启动预加载"分开处理,能显著改善策略的响应速度。

4.3 备线与降级策略

SLA 是概率承诺,不是 100% 保障。对于生产级量化系统,至少需要以下容灾设计:

  • WebSocket 断线自动重连:指数退避 + 抖动(参考第六章代码规范)
  • 关键数据双源备份:对核心标的配置备用数据源,在 TickDB 不可达时切换
  • 延迟熔断阈值:当连续 N 次请求延迟超过阈值时,策略自动降级(减少开仓频率或暂停非核心功能)

五、结语

回到开篇那句话:SLA 不是承诺给你的数字,是你在生产环境中实际量出来的分布。

TickDB 的 P99 延迟基线(< 80ms 中国区 / < 120ms 全球区)是一个可验证的承诺,不是营销话术。但验证这件事需要你自己动手——在交易时段跑一次上面的监控工具,把 P50、P95、P99 全部跑出来,和合同承诺逐项对比,这才是正确的姿势。

对于量化团队而言,延迟 SLA 最终要落地到两件事:客户端埋点让你知道真实延迟是多少,容灾设计让你在延迟劣化时不至于裸奔。这两件事 TickDB 的 SLA 文档不会替你做,得自己来。


下一步行动

如果你是个人开发者
访问 tickdb.ai 注册,获取免费 API Key,用上面的监控工具跑一次你所在地区的实际延迟数据。

如果你是量化团队技术负责人
联系 [email protected] 了解专业版 SLA 保障细则,并索取服务端监控数据接口的访问权限(部分 SLA 级别提供实时服务端指标)。

如果你的策略对延迟极度敏感(日内高频或事件驱动):
在安装 tickdb-market-data SKILL 后,可直接在 AI 助手中运行延迟监控任务,无需本地部署 Python 环境。


免责声明:本文基于 TickDB 公开文档撰写,SLA 条款以 tickdb.ai 官方最新服务协议为准。延迟数据因用户地理位置、网络条件、调用时段不同而存在差异,本文提供的代码工具输出仅供参考,不构成服务质量保证。