# 团队应要求桌面执行网关达到怎样的延迟？

桌面执行网关只有在机器延迟低于交互式工作的自然波动，并且把审批延迟单独报告时，才值得部署。我不会根据平均值、一次预热后的调用，或把人伸手触碰 Touch ID 的时间与软件执行时间混在一起的图表批准上线。这些数字或许整齐，却无法说明网关是否会打断编辑、测试、再编辑的循环。

对于普通 HTTP 调用和已经连通的 SSH 操作，我会先把采用门槛设为：在目标开发 Mac 上，p50 增量不超过 25 毫秒，p95 增量不超过 75 毫秒。新建 SSH 连接应使用单独且更宽的预算：p50 增量不超过 50 毫秒，p95 不超过 150 毫秒。这些是起始要求，不是放之四海而皆准的常数。团队应根据自己的直连基线、调用频率、网络和感知测试来收紧或放宽。下面的测试方法能给出做决定所需的证据，不会让不错的中位数遮住缓慢的尾部。

## 百分位必须描述同一种操作

只有当每个样本都覆盖相同边界时，p50 和 p95 才有意义。p50 是中位数，一半观测调用在该值以内完成。p95 表示 95% 的调用在该值以内完成。按最近秩方法排列 200 个样本时，第 190 个值就是 p95。这样最慢的十个观测仍留在尾部，而不是被平均值抹掉。

把边界定义为调用方开始操作，到调用方收到完整结果的总时间。这才是编程代理感受到的等待。它包含本地进程工作、到目标的传输、远端执行、响应传输，以及这条路径中的所有网关工作。在无人值守测试中，它不包含测试准备，也不包含人工确认。

团队经常比较不同的边界。直连 HTTP 图表可能采用 curl 的 `time_starttransfer`，而网关图表却在进程调用外测量总墙钟时间。前者在第一个响应字节到达时停止，后者要等结果序列化并返回。测试开始前，网关已经吃亏。两条路径都应使用完成全部操作的时间，再把各阶段计时保留为诊断列。

SSH 也一样。新建 TCP 连接、SSH 握手、认证、远程命令和断开合在一起才是一类操作。通过已有复用连接发送命令是另一类操作。必须分行报告。把两者混在一起后，p50 多半描述连接复用，p95 多半描述连接建立。任何门槛都无法修正一个定义混乱的样本总体。

每类操作至少报告以下字段：

- 直连墙钟时间的 p50 和 p95
- 代理墙钟时间的 p50 和 p95
- 根据配对样本计算的增量 p50 和 p95
- 样本数、失败数和超时数
- 主机、网络条件、网关版本和审批模式

配对延迟增量比两个总览百分位相减更重要。对第 `i` 个样本，在相同测试条件下计算 `delta_i = brokered_i - direct_i`，再求这些差值的 p50 和 p95。`p95(brokered) - p95(direct)` 可以作为补充信息，但它不是 p95 开销，甚至可能掩盖网络波动与网关工作之间的相关性。

## 预先写好测试约定，防止结果偏向一方

运行前先写好测试约定，因为几乎每一种无意中的优化都会改变问题。记录 Mac 型号和 macOS 版本、是否接通电源、温度状态、网络路径、目标主机区域、HTTP 协议、SSH 密码套件协商、请求与响应大小、超时、并发度和连接复用。后台负载要接近开发者日常使用的机器。关闭编辑器和构建进程的干净笔记本回答的是实验室问题。

使用一个本地目标和一个符合实际的远程目标。本地目标的网络波动很小，能暴露机器开销。远程目标则说明这些开销在真实传输延迟中是否仍然重要。网关给 3 毫秒回环请求增加 40 毫秒，与给 300 毫秒远程调用增加同样时间，体验完全不同，但两项结果都应写进报告。

不要用一套混合测试覆盖所有情况，而应建立操作类别。最低限度应包括：复用连接的小型 HTTP 响应、新建连接的小型 HTTP 响应、新建 SSH 连接并运行 `true`，以及在已可用连接上执行 SSH 命令。再加入团队编程流程中一种典型请求体和一种典型响应。不要拿有破坏性的 API 或生产 SSH 主机做延迟测试。

先预热，但不要把预热调用放进样本。然后每类至少收集 200 个实测调用，交替安排直连和代理的顺序。如果先跑完全部直连调用，之后发生的温度变化、网络变化、DNS 缓存或服务端缓存就会被算作网关开销。随机顺序也可以，但严格交替更容易检查。

交互测试的并发度设为一。等待工具结果的代理是串行用户。如果多个代理会共用网关，可以另做负载测试，但要明确标为容量测试。20 路并发能暴露排队上限，不能替代单次调用的延迟结果。

失败必须留在报告里。把超时记录为失败，并保存其超时前的耗时；在百分位旁公布失败率。悄悄删掉错误会奖励快速失败或丢弃最慢调用的路径。如果任一路径失败太多，导致 p95 不稳定，就先修好可靠性，不要继续争论毫秒。

## 测量 HTTP 时不要改变请求语义

直连与代理 HTTP 必须使用相同的方法、地址、请求头、正文、超时和响应读取方式。凭据来源可以不同，因为隔离凭据正是网关的用途，但服务器收到的请求语义必须等价。采集计时数据前，先确认两条路径得到相同的状态码、选定响应头和正文哈希。

curl 文档中的 write-out 变量提供了有用的阶段时钟。`time_connect` 在网络连接完成时结束，`time_appconnect` 包含 TLS 握手，`time_starttransfer` 到第一个响应字节，`time_total` 到全部完成。这些值能帮助定位回退，但调用方墙钟时间仍是比较边界，因为代理可能在 curl 启动前或上游响应后继续工作。

下面的直连探针会输出四个以秒为单位的字段，不显示进度，也不需要解析额外信息：

```sh
curl -sS -o /dev/null -w '%{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}
' -H "$AUTH_HEADER" "$ENDPOINT"
```

进行配对测量时，把完整直连命令放进 `DIRECT_CMD`，把完整网关命令放进 `BROKER_CMD`。下面的运行器交替执行顺序，使用单调时钟，保留失败，并为每个观测输出一个 JSON 对象。它有意把命令当作参数向量，因此 shell 运算符和展开不会悄悄进入测试。

```python
import json
import os
import shlex
import subprocess
import time

samples = int(os.environ.get("SAMPLES", "200"))
timeout = float(os.environ.get("TIMEOUT", "10"))
commands = {
    "direct": shlex.split(os.environ["DIRECT_CMD"]),
    "brokered": shlex.split(os.environ["BROKER_CMD"]),
}

for n in range(10):
    label = "direct" if n % 2 == 0 else "brokered"
    subprocess.run(commands[label], stdout=subprocess.DEVNULL,
                   stderr=subprocess.DEVNULL, timeout=timeout)

for n in range(samples):
    order = ("direct", "brokered") if n % 2 == 0 else ("brokered", "direct")
    for label in order:
        started = time.monotonic_ns()
        try:
            result = subprocess.run(commands[label], capture_output=True,
                                    timeout=timeout)
            ok = result.returncode == 0
            code = result.returncode
        except subprocess.TimeoutExpired:
            ok = False
            code = None
        elapsed_ms = (time.monotonic_ns() - started) / 1_000_000
        print(json.dumps({"pair": n, "path": label, "ms": elapsed_ms,
                          "ok": ok, "code": code}, separators=(",", ":")))
```

把输出保存为 JSON Lines 文件。计算延迟前，在计时循环之外比较成功代码和响应哈希。代理请求如果返回更少数据，看起来就会更快；直连请求如果跟随重定向而代理不跟随，两边测量的工作就不同。

还要明确控制连接复用。如果代理协议和网关保留上游 HTTP 连接，就应与同样保留连接的直连客户端比较。每次直连样本都启动一个新的 curl 进程可能失去连接复用，反而让代理路径显得更好。基于进程的测试要么强制双方都新建连接，要么编写两个小型持久客户端。无论选择哪种，都要公布。

## SSH 必须分别测量新建和复用连接

SSH 建立过程常常主导一次短命令，因此单一 SSH 百分位提供不了多少信息。OpenSSH 客户端手册说明了通过 `ControlMaster` 和 `ControlPersist` 共享连接的方式。启用后，后续会话可以复用现有网络连接，不必重新建立传输和认证。这是有效的生产优化，但两条路径必须获得同等的复用机会。

测试新建连接时，关闭共享，运行 `true` 这样的无害命令，然后断开。在专用测试主机上，直连命令可以这样写：

```sh
ssh -F /dev/null -o BatchMode=yes -o ControlMaster=no -o ConnectTimeout=10 "$TEST_HOST" true
```

配对的代理调用应使用网关正常的 SSH 操作，主机、用户和远程命令保持相同。不要为了测试强迫网关走实际代理永远不会使用的人造流程。应让直连路径符合网关文档所述的连接行为，或者在结果中明确说明架构差异。

测试复用连接时，在预热前建立连接，采样期间只运行远程 `true` 命令。必须验证复用，不能想当然。OpenSSH 的详细诊断会说明客户端是否联系了控制套接字；网关也应提供足够的诊断计时或日志，证明传输是否复用。如果无法证明，就把类别标为“连接状态未知”，不要与已知复用路径比较。

然后用一个真实但无破坏性的任务重复测试，例如读取一个固定的小文件，或向测试服务查询状态。`true` 能隔离连接和执行管道，却没有覆盖输出传输、解码和结果上限。文件内容要固定，并对返回字节计算哈希。

SSH 认证也会改变比较。不能只为方便直连基线，就把私钥交给代理。应在网络访问条件等价的隔离测试工具中运行直连路径，并把其中的凭据暴露视为测试夹具，而不是推荐的设计。延迟只说明中介带来的成本，不能判断把密钥交给自治进程是否可以接受。

## 在网关内部围绕实际工作插桩

外部秒表证明用户看到的结果，内部时间戳解释原因。在收到请求、完成请求验证、完成授权决定、完成凭据操作、发送上游请求、收到第一个上游字节、上游完成、提交审计记录和交付结果这些位置记录单调时间。记录时长或共用的跟踪标识，不要记录密钥和请求正文。

这些时间戳把机器增量拆成团队可以处理的部分：

- 本地协议解析与序列化
- 授权查询及其排队等待
- 保险库或凭据操作
- 上游客户端准备与连接获取
- 审计持久化与结果返回

除非测量设计处理了时钟差异，否则不要比较不同进程的墙钟时间戳。在同一台 Mac 上，每个进程中的单调时钟都能稳定计算时长，但不同运行时的起点未必一致。最稳妥的做法是让每个组件报告自己的时长，再用不透明的测试标识合并记录。

如果网关承诺先记录操作再报告成功，审计工作就属于测量边界。把写入移到响应之后会靠削弱保证赢得测试。批量写入可能合理，但公布的持久化时点必须与生产一致。测试对象应是人们真正运行的产品，并启用日常使用的日志和安全控制。

只有阶段数据指出可疑位置后才做性能剖析。如果 p50 随响应大小上升，应检查复制和序列化。如果 p50 稳定而 p95 上升，应检查锁、队列等待、审计刷新、连接池未命中和操作系统调度。安静的中位数调用的 CPU 剖析很少能解释尾部停顿。

还要防止观测本身拖慢测试。向终端输出详细日志可能产生阻塞 I/O。把紧凑记录写入文件或内存缓冲区，开启诊断事件测量一次，并确认差异可以忽略。条件允许时，直连与代理运行应保持相同诊断模式。

## 人工确认需要单独的服务指标

确认时间从审批卡片可见开始，到决定返回受阻调用为止。它不是网关机器延迟。把它与自动调用混合，会让 p95 取决于手的位置、注意力、屏幕状态和中断。这可能是重要的流程指标，但它回答的是审批是否适合工作，而不是执行路径是否高效。

对受保护操作报告三个分布。第一是提示前的网关机器时间。第二是提示可见到做出决定的时间。第三是决定后到结果交付的机器时间。端到端数字可以放在旁边，但不能用来诊断传输开销。

不要自动点击后把结果称为人工延迟。自动化只能测量界面呈现和决定传递，这些属于机器时间。真人研究需要获得同意的参与者、明确任务，以及不会捕获敏感输入的事件时间戳。报告参与人数，并说明他们是否预期提示。脚本测试中预期出现的 Touch ID 确认，会比真实编程中突然出现的审批更快。

审批模式也必须分行。已经授权会话中的允许操作、首次调用时要求授权进程的操作，以及每次调用都确认的操作，本来就走不同路径。把它们混合后，结果只取决于各模式在样本中的偶然频率。

有效的流程预算应描述中断，不要假装每次审批都必须低于 100 毫秒。例如可以要求审批卡片在调用到达网关后 150 毫秒内出现，且 p95 达标；人工决定时间暂时只测量，不设通过标准，直到团队观察过真实使用。之后再根据放弃、误批和任务中断设定目标。如果卡片没有提供判断所需的身份或操作信息，再快的审批也不好。

## 画图前先计算配对差值

结果计算器应配对观测，从延迟分布中排除不完整配对，并把这些不完整配对报告为失败。这样，成功的直连调用不会与其真正伙伴超时后的下一次代理调用错误配对。使用运行器保存的配对编号作为连接字段。

所有地方使用同一种百分位定义。最近秩方法先排序，再选择 `ceil(p * n)` 位置的值，秩从一开始。有些库会用插值百分位，返回一个从未实际观测到的数。插值用于某些分析并没有错，但笔记本和仪表板使用不同方法，会在门槛附近制造无意义争论。报告中要写明方法。

下面的小型计算器读取前一个运行器输出的 JSON Lines。它会输出计数，以及直连时间、代理时间和配对增量的最近秩 p50 与 p95：

```python
import json
import math
import sys

rows = [json.loads(line) for line in sys.stdin if line.strip()]
by_pair = {}
failures = {"direct": 0, "brokered": 0}

for row in rows:
    if not row["ok"]:
        failures[row["path"]] += 1
        continue
    by_pair.setdefault(row["pair"], {})[row["path"]] = row["ms"]

direct = []
brokered = []
deltas = []
for pair in sorted(by_pair):
    values = by_pair[pair]
    if "direct" not in values or "brokered" not in values:
        continue
    direct.append(values["direct"])
    brokered.append(values["brokered"])
    deltas.append(values["brokered"] - values["direct"])

def percentile(values, fraction):
    ordered = sorted(values)
    rank = max(1, math.ceil(fraction * len(ordered)))
    return ordered[rank - 1]

result = {"complete_pairs": len(deltas), "failures": failures}
for name, values in (("direct", direct), ("brokered", brokered),
                     ("added", deltas)):
    result[name] = {
        "p50_ms": percentile(values, 0.50),
        "p95_ms": percentile(values, 0.95),
        "max_ms": max(values),
    }
print(json.dumps(result, separators=(",", ":")))
```

输出格式有意保持简单：`complete_pairs`、两条路径的失败计数，以及每个分布中包含 `p50_ms`、`p95_ms` 和 `max_ms` 的区块。如果团队已经使用置信区间，可以加入，但它不能代替检查原始尾部。百分位在不同测试中移动，可能是系统改变，也可能是采样波动；原始记录才能说明哪种解释成立。

负差值有效。代理复用了直连命令重新建立的连接，或普通网络噪声偏向某一对中的代理调用时，都可能出现。不要把负数截为零。应该修复不公平的连接设置，或让配对分布保留这种方差。截断会让每个百分位向上偏，也让计算更难审计。

数据中保留完整精度，只在展示时取整，通常保留一位毫秒小数。门槛判断必须使用未取整值。如果先格式化，一个显示为 75.0 毫秒的值实际上可能刚刚超过 75 毫秒门槛。

## 尾部延迟决定信任在何处消失

交互式编程更难容忍不规则等待，而不是稳定的小额开销。开发者可以适应每次工具调用固定增加的 15 毫秒。随机出现的 800 毫秒停顿会让代理像是坏了，连续多次调用时尤其明显。因此 p95 是采用要求，不是图表装饰。

检查原始尾部观测。标记每次是否伴随 HTTP 连接未命中、SSH 握手、保险库解锁、审计刷新、应用唤醒、CPU 压力或网络尖峰。不要因为能解释一个异常值就删除它。如果生产环境会出现这个条件，它就属于分布。只有证明是测试故障时才能删除，并且要保留原数据、公布排除规则。

p99 在工程阶段有帮助，但小样本会让它很不稳定。200 次调用中，p99 大致取决于最慢的两个观测。我会把 p50 和 p95 作为采用要求，再检查最大值和原始尾部行寻找故障模式。交互门槛通过后，可以用更长的通宵测试支持 p99 分析。

预热和冷启动状态必须分开显示。如果用户会遇到应用启动或保险库解锁，应把它们作为单独操作测量，不要混入稳定调用。Mac 睡眠与唤醒恢复也应单独报告。一个常驻桌面应用预热后可能表现很好，却仍可能处理不好午休后的第一次操作。

操作序列的成本也应进入决策。如果一个编程任务连续发出十二次串行调用，增量会累积。回放经过清理的捕获操作序列，可以暴露孤立请求看不到的成本，例如连接池反复更换，或日志随会话增长而变慢。保留各类单独分布，再把任务完成时间差作为次要指标。

## 根据交互成本设定门槛

只有当机器增量在绝对值上很小，相对直连路径也很小时，网关才通过。我会为具有代表性负载的当前桌面先设四个上限：

- 复用连接的 HTTP：p50 为 25 毫秒，p95 为 75 毫秒
- 新建连接的 HTTP：p50 为 30 毫秒，p95 为 100 毫秒
- 复用连接的 SSH：p50 为 25 毫秒，p95 为 75 毫秒
- 新建连接的 SSH：p50 为 50 毫秒，p95 为 150 毫秒

直连 p95 至少为 100 毫秒时，再应用不超过 20% 的相对检查。对 3 毫秒回环调用使用 20% 规则，会要求不到一毫秒，测到的主要是噪声。对缓慢的远程调用只设绝对上限，则可能容许实现增加直连时间的一大部分。两种检查覆盖不同情况。

失败率不能差于直连加上一项很小且事先声明的容差，任何由网关引发的超时都应自动进入调查。我不会用可靠性换取 10 毫秒的中位数优势。还要要求结果语义等价，并符合预期审计持久性。快速路径如果改变响应或延后记录，就在更严重的测试上失败了。

测试应在团队实际使用的最慢一档受支持 Mac 上运行，而不只是测试台上最新的机器。接通电源时做一次，在编辑器和构建负载下再做一次。远程目标应在不同日期或网络时段至少重复三次。这不是统计仪式，它能防止一次友好的网络窗口变成产品承诺。

团队可以用盲感知测试调整初始数值。在模拟网关中注入固定延迟，回放典型代理任务，不告诉开发者具体延迟，让他们评价中断感。找出最小且持续引发抱怨的延迟，再把 p95 上限放到它以下并留出余量。只有当直连握手本来就占主导，且整个任务仍感觉即时，才保留新建 SSH 的 150 毫秒预算。

只要一种常用操作没有达到 p95 上限，就应拒绝上线，即使综合图表通过也一样。加权汇总会让大量快速 HTTP 调用掩盖缓慢的 SSH 操作。先按操作类别批准，再检查捕获的任务序列，确认多次可接受的小开销没有累积成不可接受的停顿。

## 让采用决定可以撤回，也能复核

上线应先从小范围试用组开始，并提前记录回滚条件。把测试脚本、测试夹具、网关配置和结果计算器固定在版本控制中。保存原始观测，不要只存截图。任何复核决定的人都应能重新计算百分位并看到每一次失败。

对于 Sallyport，应通过它的 HTTP 和 SSH 通道运行同一套测试工具，并让保险库、会话授权、逐次确认设置和审计行为与计划部署完全一致。它的加密哈希链审计日志可以用 `sp audit verify` 离线检查；应在测试后运行该命令，让延迟报告和完整性检查描述同一批调用。

只有每类操作都在有代表性的 Mac 上通过、结果哈希相同、审计验证成功，而且开发者接受完整任务回放时，才扩大部署。试用期间保留直连退路，但记录每次使用及原因。如果人们因为 p95 不稳定而绕过网关，说明基准测试漏掉了真实流程。先修正测量，再扩大范围。
