为什么远程 GUI 总是很卡

这是「远程 GUI 与跳板机链路」系列的篇②。

本篇的第一性问题:链路已经通了,窗口也出来了,带宽看着还很闲 ——
那为什么拖一下窗口像放幻灯片?以及,为什么换了新版软件之后突然更卡了?

回链总述:本篇负责总述里的推论三(必然有 round-trip)。
为什么「传命令」这个选择必然带来往返,在总述的 2.3。
如果你对 RTT 这个词没把握,先看总述的 3.3。

本篇能解释故障表里的哪几行:带宽很闲但窗口拖动像幻灯片、换了新版软件后突然变卡、
复制一大段文字粘贴时卡住、中文显示成方块、3D 视图一开就崩。

本篇最想让你带走的一句话:慢有两个互相独立的原因,
它们的药方完全相反。分不清是哪个,就一定会开错药。

全文四部分:一 延迟(卡的不是带宽)→ 二 数据量(工具包变了)→
三 对症(先分清,再下药)→ 四 3D 与那几样小东西。

第一部分 卡的不是带宽:延迟乘以次数

1.1 先把最贵的那个方案排除掉

遇到远程 GUI 卡,最常见的第一反应是「网不够快」,然后去申请带宽。

这个方案值得先排除,因为它最贵、周期最长,而且在这条链路上大概率无效。
理由不需要实测也能推出来 —— 往下看两节就够了。

先说结论,省得你悬着:这条链路上最典型的卡,和带宽没什么关系。
真正卡住你的那个量,叫延迟。

1.2 带宽和 RTT 是两个正交的量

这两个词经常被混着用,但它们量的是完全不同的东西:

是什么 由什么决定 加钱能改吗
带宽 单位时间能塞过去多少字节 链路容量 能,这是个采购问题
RTT 一个包发出去、对方回复、回复到达,这一来一回花的时间 物理距离、中间设备的处理 基本不能

一句话记法:带宽是管子多粗,RTT 是管子多长。

北京到上海的光纤,RTT 大约 30 毫秒。你把带宽从 100M 升到 1000M,
这 30 毫秒一点不变 —— 光在玻璃里跑那么远就是要那么久,钱买不动物理。

所以问题变成:X11 在这条链路上,到底要来回跑多少趟?

1.3 一次窗口绘制里,上百个往返排着队

%%{init: {"theme":"base","themeVariables":{"fontSize":"16px","primaryTextColor":"#333333","lineColor":"#888888","clusterBkg":"#fafafa","clusterBorder":"#cccccc","titleColor":"#555555"},"flowchart":{"nodeSpacing":34,"rankSpacing":48,"padding":12,"useMaxWidth":true}}}%% flowchart TD H["程序要画一个窗口,先得问 X server 几件事:<br/>这窗口多大?父窗口在哪?某个属性现在是什么值?<br/>这些只有 X server 知道 —— 而 X server 在我这边"] H --> B1["服务器 server-a X client 发出 GetGeometry"] B1 ==>|"请求出去"| B2["跳板机 jump01 收下、转投"] B2 ==> B3["办公机(X server) 查一下,答 1024 x 768"] B3 -.->|"回复原路走回来"| B4["跳板机 jump01 收下、转投"] B4 -.-> B5["服务器 server-a 收到答案,才能接着画"] B5 --> C1["一个端到端 RTT = 三段来回之和<br/>每一跳的 sshd 还要在用户态收一次、发一次<br/>就算它只有 30 毫秒"] C1 -->|"乘以"| C2["一次稍复杂的窗口绘制要问上百次<br/>而且是同步的 —— 答案没回来,下一步就没法算"] C2 -->|"等于"| C3["100 × 30 ms ≈ 3 秒<br/>窗口「像幻灯片」就是这么来的"] C3 --> F["把带宽从 100M 升到 1000M,上面这 3 秒一点不变<br/>带宽是管子多粗,RTT 是管子多长 —— 这里堵的是长度,不是粗细"] class H accent class B1 proc class B2 net class B3 host class B4 net class B5 proc class C1 idea class C2 idea class C3 bad class F accent classDef host fill:#ffe6cc,stroke:#d79b00,color:#333333 classDef net fill:#dae8fc,stroke:#6c8ebf,color:#333333 classDef proc fill:#d5e8d4,stroke:#82b366,color:#333333 classDef idea fill:#e1d5e7,stroke:#9673a6,color:#333333 classDef bad fill:#f8cecc,stroke:#b85450,color:#333333 classDef plain fill:#f5f5f5,stroke:#999999,color:#333333 classDef accent fill:#fff2cc,stroke:#d6b656,color:#333333

图 1 卡在哪:一次窗口绘制里,上百个往返一个接一个地排队

X11 的请求分两种。一种是**发了就走**的,比如「把这块填成灰的」—— 程序不需要回答。 另一种是**必须等回答**的:
  • GetGeometry —— 这个窗口现在多大?
  • QueryTree —— 它的父窗口是谁?
  • GetProperty —— 某个属性现在是什么值?
  • InternAtom —— 我这个名字对应的 atom id 是多少?

这类请求在 X11 里数量很大,而且它们是同步的 ——
程序拿不到答案就算不出下一步该画什么,只能停在那儿等。

关键在于:答案只有 X server 知道,而 X server 在我这边。
于是每问一次,就是一个完整的端到端往返。

一次稍复杂一点的窗口绘制,这样的往返可能有上百个。
就算端到端 RTT 只有 30 毫秒,100 × 30 ms 也已经是 3 秒了 ——
而这 3 秒里,链路几乎是空的。

这就是「带宽很闲但窗口拖动像幻灯片」的全部解释。
它也解释了一个很多人观察到但说不清的现象:卡得很有规律。
不是时快时慢那种拥塞式的卡,而是每个动作都稳定地慢一拍 —— 因为它数的是次数,不是流量。

1.4 跳板机在这里的作用:又加了一段

我这条链路是三段:办公机 → 跳板机 jump01 → 服务器 server-a。

端到端 RTT 不是某一段的 RTT,是三段之和。而且还不止 ——
每一跳的 sshd / ssh 是在用户态做转发的:收进来、解密、判断这是哪个 channel、
再加密、再发出去。这一来一回在每一跳上都要做两次。

所以跳板机在这一篇里的角色,和篇①里不一样 ——
在篇①它是个会不会断的问题,在这里它是个加多少延迟的问题。它不会让你连不上,
它只是让上面那个乘法里的 30 变成 40、50。

想知道这个数在你这条链路上是多少,不用猜:

# 从办公机量到跳板机
ping -c 20 jump01

# 登上跳板机,再量到服务器
ping -c 20 server-a

两段加起来,再留一点余量给用户态转发,就是那个要乘一百次的数。

第二部分 第二个原因:工具包已经不发绘图命令了

上面那个原因只跟次数有关,跟每次传多少字节无关。
现在说第二个原因,它跟字节数有关,而且和第一个原因完全独立 ——
两者可以单独出现,也可以叠加。

2.1 X11 原本的绘图原语长什么样

X11 设计的时候,「传命令」是字面意思。协议里有一整套绘图原语:

  • PolyLine —— 从这几个点连成折线
  • PolyFillRectangle —— 把这几块填上
  • ImageText8 —— 在这个位置写这串字
  • CopyArea —— 把这块搬到那儿

一条这样的请求是几十个字节。一整屏 xterm 的界面,加起来也就几 KB。
在 1984 年那种网络上都够用 —— 这正是篇① 1.2说的,
绘图命令比位图小得多,所以才选了传命令。

2.2 现代工具包改成了客户端渲染

%%{init: {"theme":"base","themeVariables":{"fontSize":"16px","primaryTextColor":"#333333","lineColor":"#888888","clusterBkg":"#fafafa","clusterBorder":"#cccccc","titleColor":"#555555"},"flowchart":{"nodeSpacing":34,"rankSpacing":48,"padding":12,"useMaxWidth":false}}}%% flowchart TD subgraph L["老派程序(xterm、xclock、老 Motif 工具)"] L1["程序发的是绘图命令<br/>PolyLine、FillRectangle、ImageText8<br/>「从这画到那」「这块填成灰的」"] L2["一条命令几十字节<br/>整屏界面加起来几 KB"] L3["X server 拿到命令<br/>用我这边的显卡真的去画"] L1 --> L2 L2 --> L3 end subgraph R["现代工具包(Qt、GTK3+、Electron)"] R1["程序在自己那边把界面画成一张位图<br/>抗锯齿、圆角、阴影、主题、字体渲染<br/>全在客户端做完"] R2["再用 PutImage 把整块像素传过去<br/>一次刷新几百 KB 起,拖动窗口就是连着传"] R3["X server 只负责把这块位图贴到屏幕上<br/>那些绘图原语一条也没用上"] R1 --> R2 R2 --> R3 end L3 --> F["同一个界面,过网络的数据量差一到两个数量级<br/>X11 被从「画图协议」用成了「图片传输协议」—— 而它一点也不擅长后者<br/>于是:越新的程序远程越慢,二十年前的 xterm 反而流畅"] R3 --> F class L1 proc class L2 net class L3 host class R1 proc class R2 bad class R3 host class F accent classDef host fill:#ffe6cc,stroke:#d79b00,color:#333333 classDef net fill:#dae8fc,stroke:#6c8ebf,color:#333333 classDef proc fill:#d5e8d4,stroke:#82b366,color:#333333 classDef idea fill:#e1d5e7,stroke:#9673a6,color:#333333 classDef bad fill:#f8cecc,stroke:#b85450,color:#333333 classDef plain fill:#f5f5f5,stroke:#999999,color:#333333 classDef accent fill:#fff2cc,stroke:#d6b656,color:#333333

图 2 第二个原因:工具包不再发绘图命令了,改成整块传像素

问题是,现在的界面长得跟 1984 年不一样了:圆角、阴影、渐变、抗锯齿、 亚像素字体渲染、主题皮肤、动画过渡。X11 那套原语**画不出来**这些东西。

于是 Qt、GTK3+、Electron 这些工具包做了同一个选择:
不再用 X11 的绘图原语,改成自己在客户端把界面渲染成一张位图,
然后整块传给 X server 贴上去。

注意这里的「客户端」是 X11 语汇里的 client —— 也就是跑在 server-a 上的那个程序。
这个词很容易绕,篇① 第一部分专门讲过角色反转。

站在工具包的角度这个决定完全正确:它拿回了对每一个像素的控制权,
本机跑的时候还有共享内存可以走(这一条 3.4 再说)。
代价是,X11 从「发几条画线命令」变成了「传图片」——
一次刷新几百 KB 起步,拖动窗口就是连续地传。

同一个界面,过网络的数据量差一到两个数量级。
X11 被从「画图协议」用成了「图片传输协议」,而它一点也不擅长后者:
它既没有帧间差分,也没有视频编码,就是老老实实把像素塞进去。

2.3 于是有了那个反直觉的结论

越新的程序,远程越慢。

一个二十年前的 xterm 在这条链路上可能相当流畅,
而一个现代的 Qt 工具、一个 Electron 应用,能卡到没法用。
这不是因为老程序「优化得好」,是因为老程序还在用这个协议原本的用法。

这条结论有一个很实用的推论,也是故障表里那一行的来源:

换了新版软件后突然变卡 —— 多半不是新版更臃肿,
而是它的工具包从 GTK2 升到了 GTK3、从旧 Qt 升到了新 Qt,
绘图路径从「发命令」换成了「传位图」。
版本号变了一位,过网络的数据量涨了一到两个数量级。

第三部分 对症下药:先分清是 (a) 还是 (b)

现在有两个原因:

  • (a) 延迟型:往返次数 × RTT。链路很闲,但每个动作都慢一拍。
  • (b) 数据量型:整块位图往过传。链路被打满,动作是「一卡一卡」地拖。

它们的药方几乎相反。所以第一步不是动手改,是分清楚。

3.1 一个几分钟就能做完的判别法

三步,都不需要改配置:

第一步:卡的时候,看链路占用。

# 在服务器上盯着这条 SSH 连接的流量,或者在办公机上用任意流量监控工具
nload

拖动窗口、切换标签页,让它卡给你看。

  • 链路基本空着 → 以 (a) 为主
  • 链路被打满 → 以 (b) 为主

第二步:拿 xterm 做对照。

xterm &
  • xterm 也卡(比如敲一个字符要等一下)→ 有 (a),这是纯延迟
  • xterm 很流畅,只有那个 Qt / GTK 程序卡 → 有 (b),问题在绘图路径

第三步:量一下 RTT,估个上限。

用 1.4 的 ping 拿到端到端 RTT,乘以一百。
如果这个乘积已经接近你观察到的卡顿时长,那 (a) 就是主因,
剩下的优化空间很小 —— 这一步能帮你早点放弃在 (a) 上折腾。

两个原因经常同时存在。判别法给的不是二选一,是谁是主要矛盾。

3.2 -C 压缩:治 (b),对 (a) 几乎无效

ssh -C -X user@server-a

-C 打开 SSH 的 zlib 压缩。

对 (b) 有用,而且效果不小:位图里有大量重复像素(纯色背景、整齐的边框),
压缩比通常很可观。

对 (a) 基本无效,道理很直白:一个 GetGeometry 请求本来就几十字节,
压缩完还是几十字节 —— 它不占带宽,它占的是一趟往返。
压缩不能让光跑得更快。

而且压缩要花 CPU。在延迟型的场景里,-C 甚至可能让单次往返略微更慢。
所以:链路被打满时开 -C,链路空着时别开。

3.3 ControlMaster:省的是建链,不是每次往返

# ~/.ssh/config
Host server-a
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m

连接复用省掉的是每开一个新会话时的 TCP 握手、密钥交换和认证 ——
在多跳链路上这几趟往返加起来不少,所以「开第二个终端快很多」是真的。

但请把期望放对位置:它不减少已经建立好的那条连接上、每个 X11 请求的往返。
窗口卡的是 1.3 那一百趟,那一百趟在哪条连接上都得跑。

ControlMaster 值得配 —— 它让「再开一个窗口」这件事变快。
它不治 (a),也不治 (b)。

3.4 MIT-SHM:本机的解药,远程的空头支票

有个扩展叫 MIT-SHM,它正是为 2.2 那种「整块传位图」准备的解药。

它的动机很实在:本机跑 GUI 的时候,一帧位图经 socket 交给 X server 要过
「用户态 → 内核 → 用户态」两次拷贝。改成共享内存之后,程序直接往那块内存里写,
只发一个「看这块内存」的通知 —— 零拷贝。

问题在于:共享内存的前提是两个进程能映射到同一块物理内存。
程序在 server-a 上,X server 在我的办公机上 —— 这个前提根本不成立。

所以在这条链路上,MIT-SHM 必然失效,而且有两种失效方式:

  • 友好版:X server 干脆不宣告这个扩展,程序自动退回走 socket,只是慢。
    你可能看到一行 Xlib: extension "MIT-SHM" missing on display "localhost:10.0"。
  • 难看版:扩展被宣告了、程序信了、然后一用就报 BadAccess。

想知道自己是哪种,一条命令:

xdpyinfo | grep -i shm

列出来了就是宣告了,什么都没有就是没宣告。

这件事很重要但不属于本篇 —— 它是整个系列里唯一一个「抽象承诺被打破」的例子,
总述 2.4 埋了钩子,
完整的四分法和那一整页报错对照在篇④。

这里只需要记住一条:在远程链路上,不要指望共享内存帮你省掉 (b)。
本机能省的那两次拷贝,跨机器时一次也省不下来。

3.5 真要治 (a),只能换模型

把上面三样加起来能做的事很有限。原因在 1.3 就已经写死了:
(a) 的量纲是「次数 × RTT」,而 X11 转发既不能减少次数(那是程序和工具包决定的),
也不能减少 RTT(那是物理决定的)。

要真正减少往返,只有一条路:在链路中间放一个懂 X 协议的代理,
让它在服务器那一侧就地回答那些同步请求,不要每次都跑回来问。
这正是 NX / Xpra 这类方案做的事 —— 它们不是「压缩得更好」,
是把往返吃掉了。

再往前一步就是 VNC / RDP / VirtualGL 那一端:干脆不传命令,只传画面。
往返归零,代价是别的东西。

这两类方案的完整取舍,落在篇③。本篇只把话说到这儿:

如果判别法告诉你主要矛盾是 (a),那你在 ssh -X 这个框架里已经到头了。
继续调参数是浪费时间,该考虑换方案了。

第四部分 3D,以及那些「看起来该好使」的东西

4.1 3D:显卡在服务器,屏幕在这边,谁来渲染

%%{init: {"theme":"base","themeVariables":{"fontSize":"16px","primaryTextColor":"#333333","lineColor":"#888888","clusterBkg":"#fafafa","clusterBorder":"#cccccc","titleColor":"#555555"},"flowchart":{"nodeSpacing":34,"rankSpacing":48,"padding":12,"useMaxWidth":false}}}%% flowchart TD T1["① DRI 直接渲染<br/>本机的正常路径"] T2["② GLX indirect rendering<br/>把 GL 也塞进 X 协议"] T3["③ VirtualGL 这一类<br/>服务器侧渲染,传结果"] T1 --> B1["程序直接打开 /dev/dri 的设备节点<br/>自己驱动 GPU,渲染结果直接进显存"] T2 --> B2["GL 命令流被打包成 X 协议请求<br/>顺着这条链路发到我这边<br/>由我办公机上的显卡去渲染"] T3 --> B3["服务器上的 GPU 正常渲染<br/>渲染完把结果帧读回来、压成图像流<br/>当普通图片发给我"] B1 --> V1["远程根本不可用<br/>GPU 在服务器,屏幕在我这边<br/>设备节点是个本机概念,跨不过网络"] B2 --> V2["能跑,但很难用<br/>数据量大、往返多;只支持很老的 GL 版本<br/>现代驱动大多默认关掉它<br/>「一开 3D 视图就崩」多半停在这儿"] B3 --> V3["真能用的那条<br/>代价:它已经不是「传命令」了"] V1 --> F["第三条路能用,恰恰因为它放弃了 X 的网络透明性<br/>滑到了总述那根轴的最右端:传画面<br/>3D 远程这件事,在「传命令」这一侧没有解"] V2 --> F V3 --> F class T1 plain class T2 plain class T3 plain class B1 proc class B2 net class B3 accent class V1 bad class V2 bad class V3 proc class F idea classDef host fill:#ffe6cc,stroke:#d79b00,color:#333333 classDef net fill:#dae8fc,stroke:#6c8ebf,color:#333333 classDef proc fill:#d5e8d4,stroke:#82b366,color:#333333 classDef idea fill:#e1d5e7,stroke:#9673a6,color:#333333 classDef bad fill:#f8cecc,stroke:#b85450,color:#333333 classDef plain fill:#f5f5f5,stroke:#999999,color:#333333 classDef accent fill:#fff2cc,stroke:#d6b656,color:#333333

图 3 3D 的三条路:两条走不通,能走通的那条已经不是「传命令」了

3D 是把第一部分和第二部分的矛盾同时顶到极限的场景,所以它基本不能用。 问一个问题就清楚了:**渲染这件事,到底在哪台机器上发生?**

只有三个答案:

① 在程序那边,用服务器的 GPU(DRI 直接渲染)。
这是本机跑 3D 的正常路径 —— 程序直接打开 /dev/dri 下的设备节点,
自己驱动 GPU,渲染结果直接进显存。
远程根本走不通:设备节点是个彻头彻尾的本机概念,跨不过网络;
而且就算渲染出来了,那块显存也在服务器上,不在我面前这块屏幕后面。

② 在我这边,用我的显卡(GLX indirect rendering)。
把 GL 命令流也打包成 X 协议请求发过来,由我办公机上的显卡去渲染。
听上去很符合「传命令」的精神,实际上很难用:数据量大、往返多,
而且只支持很老的 GL 版本 —— 现代驱动大多默认把它关掉了
(Xorg 这边需要显式 +iglx 才开)。
「远程打开 CAD / 仿真工具,一点开 3D 视图就崩」,多半就停在这儿。

③ 在服务器那边渲染,把结果发过来(VirtualGL 这一类)。
服务器上的 GPU 正常渲染,渲染完把结果帧读回来、压成图像流,当普通图片发给我。
这条是真能用的。

第三条能用的原因,恰恰是它放弃了 X 的网络透明性 ——
它已经滑到总述那根轴的最右端:传画面。

所以这一节的结论不是「3D 远程要怎么调优」,是:

3D 远程这件事,在「传命令」这一侧没有解。
不是实现得不够好,是模型本身到头了。要用就得换到另一侧去。

4.2 剪贴板:惰性协议,卡在粘贴那一刻

现象:在远程程序里复制一大段文字,复制的那一下很快,
过一会儿在本地粘贴,卡住好几秒。

很多人以为是粘贴的目标程序有问题。不是。

X11 的 selection(剪贴板)是个惰性协议:

  1. 你按复制 —— 那个程序只是向 X server 登记一句「CLIPBOARD 这个 selection 现在归我」。
    一个字节的内容都没有传。
  2. 你按粘贴 —— 接收方这才向 X server 打听「谁拿着 CLIPBOARD」,
    然后直接去问持有者要内容,双方现场协商格式(纯文本?HTML?图片?),
    然后才真正搬运。

也就是说,那一大段文字是在你按下粘贴的那一刻才跨越这三段链路的。
卡在粘贴,是设计使然。

这个设计在本机上很聪明 —— 复制一张大图不占任何代价,除非真有人要。
跨三跳链路时它就变成了一个惊喜。

同一个机制还能解释另外两件事:

  • 复制完把源程序关了,就粘不出来了。 因为内容一直在持有者手里,
    持有者没了,内容就没了。(本地剪贴板管理器做的就是「抢在它退出前要一份」。)
  • 粘到不同程序里得到的东西不一样。 因为格式是粘贴时现场协商的,
    文本编辑器要 UTF8_STRING,办公软件可能先要 HTML。

4.3 字体:方块字是服务器侧的事,和本机装了什么无关

现象:远程程序里中文显示成一串方块,本机明明装了一堆中文字体。

这件事在 2.2 里其实已经答完了,但它太容易搞反,值得单拎出来。

早年的 X11 确实是 server-side font:字体装在 X server 那一侧,
程序发一个 ImageText8 说「在这儿写这串字」,由 X server 去找字形。
那个年代,本机装字体是有用的。

但自从 Xft / fontconfig 之后,这条路就废了。现在是:
程序自己去找字体文件、自己把字形渲染成像素,
然后通过 RENDER 扩展把渲染好的字形传给 X server。

所以字体必须装在跑程序的那台机器上 —— 也就是 server-a。

这里有个必须说破的绕口地方:这台「服务器」在 X11 的语汇里是 client。
篇①讲的角色反转,在这儿就是个实打实的坑 ——
「字体要装在客户端」这句话,指的是装在远端那台服务器上。

所以排查方向是反直觉但唯一正确的:

# 在 server-a 上,不是在办公机上
fc-list :lang=zh | head

空的,就是它了。装字体也装在 server-a 上。

4.4 输入法:候选框是另一个 X client

现象:远程程序里打中文,候选框出不来,或者出来了但迟钝得没法用。

拆开看就不奇怪了。输入法这套东西在这条链路上有两个独立的部件:

  • 输入法引擎(fcitx / ibus 等)—— 它得和程序在同一台机器上,
    通过 XIM 或 D-Bus 之类的协议对接。也就是说,它得跑在 server-a 上。
  • 候选框那个小窗口 —— 它是另一个 X client。

第二条是关键:那个候选框不是程序画的,是输入法进程画的,
而输入法进程也在 server-a 上 —— 所以候选框本身也要顺着这三段链路转发过来。

于是链路变成:我敲一个键 → 传到 server-a 的 X client →
转给输入法引擎 → 引擎算出候选 → 候选框作为另一个 X client 把自己画出来 →
再顺着三段链路传回我的屏幕。

每敲一个字母都走一遍。1.3 那个乘法在这里乘的是击键次数。

这一节没有什么妙招可给,只有一句话让你知道该往哪看:
候选框出不来,先确认输入法引擎跑在 server-a 上(而不是你本机上);
候选框很迟钝,那就是 (a),没别的。

4.5 可以自己反推的练习

盖住答案,先想一遍。这几个都能用本篇的模型直接推出来:

  1. 同事说「我们把专线从 100M 升到 1000M,远程 CAD 还是一样卡」。
    在不问任何其他信息的情况下,你能判断他们撞的是 (a) 还是 (b) 吗?
    (提示:1.2。注意这题的答案是「不能,但能排除一个」。)
  2. 同一个程序,我直连服务器时能用,走跳板机就卡。带宽两条路一样。为什么?
    (提示:1.4 那个「三段之和」。)
  3. 我给 ssh 加了 -C,结果更卡了一点。这说明我撞的是 (a) 还是 (b)?
    (提示:3.2 最后一段。)
  4. 一个程序在本机跑得飞快,远程卡成幻灯片,而它的 CPU 占用两边都不高。
    3.4 那个扩展在这里起了什么作用?
  5. 我在办公机上装了一堆中文字体,远程程序还是显示方块。
    如果我反过来在 server-a 上装字体,本地的 X server 需要重启吗?
    (提示:4.3 —— 想清楚字形是在哪一侧变成像素的。)
  6. 远程程序里复制了 50 MB 的文本,我一直不粘贴。这 50 MB 在链路上传输了吗?
    如果此刻我把那个程序关掉,再去粘贴,会发生什么?

第 1 题值得多想一层:带宽升级无效这个观察,能证明它不是纯 (b),
但不能证明它是纯 (a) —— 也可能是升级之后链路仍然被打满(100M 就没打满过),
或者瓶颈压根不在这条链路上。这正是 3.1 要用 nload 和 xterm 两步来判的原因:
一个观察排除一种可能,不等于确定了另一种。

第 5 题的答案是不需要 —— 而这个「不需要」恰恰是本篇最该带走的那种思维方式:
先问清楚这件事发生在哪一侧,再决定去哪台机器上动手。
这条链路上大部分的时间浪费,都花在了在错误的那台机器上排查。

下一篇换个问题:把 display 借出去,到底等于交出了什么?
以及,既然「传命令」这条路有这么多硬伤,为什么它还活着 —— 又为什么正在死去。
那是篇③。

posted @ 2026-09-18 18:34  起飞的乌龟  阅读(24)  评论(0)    收藏  举报