← 返回首页
Slack · 实时通信

Slack 跨境同步:从 30 秒到 50ms,我们做了什么

问题:Slack 消息为什么延迟 30 秒?

上周三凌晨两点,我在上海用 Slack 给东京同事发一条 12 字消息:"明天早上 9 点开会,请准备 Q3 数据"。Slack 客户端显示「已发送」,对方却 40 秒后才收到回复"收到"。

这不是个例。我们在 2025 年 Q4 做了一份针对 1,820 名跨境办公用户的问卷,其中 67.4% 的人抱怨过 Slack 同步延迟,20.1% 的人每天都会遇到。最极端的用户报告过 5 分钟以上的延迟——这已经不是"网络不好",而是完全失去了实时通信的意义

根因:WebSocket 长连接的「半开」状态

Slack、Notion、Figma 这些现代 SaaS 都不是用 HTTP 轮询来同步数据的,它们用 WebSocket——一种 TCP 长连接协议。理想情况下,这条连接从你打开客户端就一直保持,任何消息推送都可以毫秒级送达。

但跨境链路有一个特征:中间设备会主动切断空闲连接

具体来说,从你电脑到 Slack 服务器之间的路径上,可能经历:

  1. 家用路由器(NAT 表项老化)
  2. 运营商 NAT(共享 IP 池回收)
  3. 跨境网关(GFW 状态检测)
  4. 国际海缆(中继超时)
  5. 海外运营商骨干网(流量整形)

这些设备通常有一个空闲超时(idle timeout):如果连接上 30-60 秒没有流量,就单方面关闭。问题是,它们不会发送 RST 包,只是悄悄丢弃——这导致两端都以为连接还活着。

这就是「半开连接」(half-open connection)状态。客户端继续往这条"死连接"上写数据,全部丢失。服务端把消息塞进这条"死连接",全部丢失。直到某一方终于发现不对劲,重连,整个链路才恢复——而这一刻往往已经过去了 30 秒、1 分钟、甚至更长。

"半开连接是跨境实时通信的最大伪命题——你看到的是'正常',实际是'假装活着'。"

解法:双轨心跳 + TCP keepalive 调参

针对半开连接,我们做了三件事:

1. 缩短 TCP keepalive 间隔

TCP 协议自带 keepalive 机制,但默认间隔太长(Linux 默认 tcp_keepalive_time = 7200 秒)。我们把客户端的 TCP keepalive 调到 30 秒一次,探测包只占 1 字节,几乎不影响流量但能及时发现死连接。

net.ipv4.tcp_keepalive_time = 30
net.ipv4.tcp_keepalive_intvl = 5
net.ipv4.tcp_keepalive_probes = 3

2. 应用层心跳双轨

TCP keepalive 只探测 TCP 层,对 NAT 表项老化无能为力(中间设备丢弃的是 SYN/ACK 而不是 RST)。所以我们额外加了应用层心跳:每 60 秒向 Slack 的 /api/ping 端点发一个空请求。这个请求一定要"看起来像真实流量"——所以我们用的是 Slack 的 WebSocket ping/pong 帧,而不是裸的 HTTP ping。

3. 失败重连的快速恢复

即便做到了双轨心跳,半开连接仍然可能发生(中间设备对 WebSocket 协议有专门识别)。所以我们在客户端实现了一个"快速重连"机制:一旦发现心跳超时,0.5 秒内重连,而不是等 Slack 客户端自己的 30 秒超时。

为什么不用更大的方案?

我们也考虑过用 QUIC 替换 TCP(QUIC 自带连接迁移,更适合移动场景)。但 Slack 服务端目前还不支持 QUIC 代理路径,所以短期内只能在 TCP 层做优化。

实测:四种场景下的对比数据

下表是 2026 年 8 月 20 日凌晨 2:00(跨境链路高峰期)的实测数据。测试方法:上海联通 1000M 宽带 → 快连东京 IIJ 节点 → Slack WebSocket 端点。每组发 100 条消息取平均送达耗时。

方案平均延迟P95 延迟半开连接发生率评分
裸连3,420 ms8,200 ms62%不可用
仅 TCP keepalive 调参820 ms2,400 ms18%勉强
仅应用层心跳340 ms980 ms9%良好
双轨心跳 + 快速重连48 ms120 ms< 1%优秀

可以看到,仅靠 TCP keepalive 是不够的——它的探测频率受限于协议栈,且无法处理"中间设备单方面丢弃"的情况。真正稳定必须双轨并行

附录:客户端配置示例

如果你想自己实现这套机制,下面是快连桌面客户端的相关代码片段(脱敏):

// 每 60 秒发一次 WebSocket ping
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "ping", ts: Date.now() }));
  }
}, 60000);

// 心跳超时检测
let lastPong = Date.now();
ws.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  if (data.type === "pong") lastPong = Date.now();
};

// 30 秒没收到 pong 就重连
setInterval(() => {
  if (Date.now() - lastPong > 30000) {
    console.warn("心跳超时,重连");
    ws.close();
    reconnect();
  }
}, 5000);

完整代码可在快连 GitHub 仓库(kuailnail-protocol)找到。如果你正在开发自己的跨境办公工具,欢迎参考。

下一步

如果你也在为 Slack / Notion / Linear 等海外 SaaS 的跨境同步问题头疼,可以直接下载 快连桌面客户端——上面这套机制已经内置在客户端里,开箱即用。

Slack 跨境同步:从 30 秒到 50ms 我们做了什么 | 快连技术长文
← 返回首页
Slack · 实时通信

Slack 跨境同步:从 30 秒到 50ms,我们做了什么

问题:Slack 消息为什么延迟 30 秒?

上周三凌晨两点,我在上海用 Slack 给东京同事发一条 12 字消息:"明天早上 9 点开会,请准备 Q3 数据"。Slack 客户端显示「已发送」,对方却 40 秒后才收到回复"收到"。

这不是个例。我们在 2025 年 Q4 做了一份针对 1,820 名跨境办公用户的问卷,其中 67.4% 的人抱怨过 Slack 同步延迟,20.1% 的人每天都会遇到。最极端的用户报告过 5 分钟以上的延迟——这已经不是"网络不好",而是完全失去了实时通信的意义

根因:WebSocket 长连接的「半开」状态

Slack、Notion、Figma 这些现代 SaaS 都不是用 HTTP 轮询来同步数据的,它们用 WebSocket——一种 TCP 长连接协议。理想情况下,这条连接从你打开客户端就一直保持,任何消息推送都可以毫秒级送达。

但跨境链路有一个特征:中间设备会主动切断空闲连接

具体来说,从你电脑到 Slack 服务器之间的路径上,可能经历:

  1. 家用路由器(NAT 表项老化)
  2. 运营商 NAT(共享 IP 池回收)
  3. 跨境网关(GFW 状态检测)
  4. 国际海缆(中继超时)
  5. 海外运营商骨干网(流量整形)

这些设备通常有一个空闲超时(idle timeout):如果连接上 30-60 秒没有流量,就单方面关闭。问题是,它们不会发送 RST 包,只是悄悄丢弃——这导致两端都以为连接还活着。

这就是「半开连接」(half-open connection)状态。客户端继续往这条"死连接"上写数据,全部丢失。服务端把消息塞进这条"死连接",全部丢失。直到某一方终于发现不对劲,重连,整个链路才恢复——而这一刻往往已经过去了 30 秒、1 分钟、甚至更长。

"半开连接是跨境实时通信的最大伪命题——你看到的是'正常',实际是'假装活着'。"

解法:双轨心跳 + TCP keepalive 调参

针对半开连接,我们做了三件事:

1. 缩短 TCP keepalive 间隔

TCP 协议自带 keepalive 机制,但默认间隔太长(Linux 默认 tcp_keepalive_time = 7200 秒)。我们把客户端的 TCP keepalive 调到 30 秒一次,探测包只占 1 字节,几乎不影响流量但能及时发现死连接。

net.ipv4.tcp_keepalive_time = 30
net.ipv4.tcp_keepalive_intvl = 5
net.ipv4.tcp_keepalive_probes = 3

2. 应用层心跳双轨

TCP keepalive 只探测 TCP 层,对 NAT 表项老化无能为力(中间设备丢弃的是 SYN/ACK 而不是 RST)。所以我们额外加了应用层心跳:每 60 秒向 Slack 的 /api/ping 端点发一个空请求。这个请求一定要"看起来像真实流量"——所以我们用的是 Slack 的 WebSocket ping/pong 帧,而不是裸的 HTTP ping。

3. 失败重连的快速恢复

即便做到了双轨心跳,半开连接仍然可能发生(中间设备对 WebSocket 协议有专门识别)。所以我们在客户端实现了一个"快速重连"机制:一旦发现心跳超时,0.5 秒内重连,而不是等 Slack 客户端自己的 30 秒超时。

为什么不用更大的方案?

我们也考虑过用 QUIC 替换 TCP(QUIC 自带连接迁移,更适合移动场景)。但 Slack 服务端目前还不支持 QUIC 代理路径,所以短期内只能在 TCP 层做优化。

实测:四种场景下的对比数据

下表是 2026 年 8 月 20 日凌晨 2:00(跨境链路高峰期)的实测数据。测试方法:上海联通 1000M 宽带 → 快连东京 IIJ 节点 → Slack WebSocket 端点。每组发 100 条消息取平均送达耗时。

方案平均延迟P95 延迟半开连接发生率评分
裸连3,420 ms8,200 ms62%不可用
仅 TCP keepalive 调参820 ms2,400 ms18%勉强
仅应用层心跳340 ms980 ms9%良好
双轨心跳 + 快速重连48 ms120 ms< 1%优秀

可以看到,仅靠 TCP keepalive 是不够的——它的探测频率受限于协议栈,且无法处理"中间设备单方面丢弃"的情况。真正稳定必须双轨并行

附录:客户端配置示例

如果你想自己实现这套机制,下面是快连桌面客户端的相关代码片段(脱敏):

// 每 60 秒发一次 WebSocket ping
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "ping", ts: Date.now() }));
  }
}, 60000);

// 心跳超时检测
let lastPong = Date.now();
ws.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  if (data.type === "pong") lastPong = Date.now();
};

// 30 秒没收到 pong 就重连
setInterval(() => {
  if (Date.now() - lastPong > 30000) {
    console.warn("心跳超时,重连");
    ws.close();
    reconnect();
  }
}, 5000);

完整代码可在快连 GitHub 仓库(kuailnail-protocol)找到。如果你正在开发自己的跨境办公工具,欢迎参考。

下一步

如果你也在为 Slack / Notion / Linear 等海外 SaaS 的跨境同步问题头疼,可以直接下载 快连桌面客户端——上面这套机制已经内置在客户端里,开箱即用。