13 Flow Control for Users
来风 edited this page 2023-08-18 17:50:50 +08:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

如何避免缓存积累延迟

不管使用 TCP 还是 KCP你都不可能超越信道限制的发送数据。TCP 的发送窗口 SNDBUF 决定了最多可以同时发送多少数据KCP的也一样。

当前发送且没有得到 ACK/UNA确认的数据都会滞留在发送缓存中一旦滞留数据超过了发送窗口大小限制则该链接的 tcp send 调用将会 被阻塞或者返回EAGAIN / EWOULDBLOCK这时候说明当前 tcp 信道可用带宽已经赶不上你的发送速度了。

可用带宽 = min(本地可用发送窗口大小,远端可用接收窗口大小) * (1 - 丢包率) / RTT

当你持续调用 ikcp_send首先会填满kcp的 snd_buf如果 snd_buf 的大小超过发送窗口 snd_wnd 限制,则会停止向 snd_buf 里追加 数据包,只会放在 snd_queue 里面滞留着,等待 snd_buf 有新位置了(因为收到远端 ack/una而将历史包从 snd_buf中移除才会从 snd_queue 转移到 snd_buf等待发送。

TCP发送窗口满了不能发送了会给你阻塞住或者 EAGAIN/EWOULDBLOCKKCP发送窗口满了ikcp_send 并不会给你返回 -1而是让数据滞留 在 snd_queue 里等待有能力时再发送。

因此,千万不要以为 ikcp_send 可以无节制的调用,为什么 KCP在发送窗口满的时候不返回错误呢这个问题当年设计时权衡过如果返回希望发送时返回错误的 EAGAIN/EWOULDBLOCK 你势必外层还需要建立一个缓存,等到下次再测试是否可以 send。那么还不如 kcp直接把这一层缓存做了让上层更简单些而且具体要如何处理 EAGAIN可以让上层通过检测 ikcp_waitsnd 函数来判断还有多少包没有发出去,灵活抉择是否向 snd_queue 缓存追加数据包还是其他。

重设窗口大小

要解决上面的问题首先对你的使用带宽有一个预计并根据上面的公式重新设置发送窗口和接收窗口大小你写后端想追求tcp的性能也会需要重新设置tcp的 sndbuf, rcvbuf 的大小KCP 默认发送窗口和接收窗口大小都比较小而已默认32个包你可以朝着 64, 128, 256, 512, 1024 等档次往上调kcptun默认发送窗口 1024用来传高清视频已经足够游戏的话32-256 应该满足。

不设置的话,如果默认 snd_wnd 太小,网络不是那么顺畅,你越来越多的数据会滞留在 snd_queue里得不到发送你的延迟会越来越大。

设定了 snd_wnd远端的 rcv_wnd 也需要相应扩大,并且不小于发送端的 snd_wnd 大小,否则设置没意义。

其次对于成熟的后端业务,不管用 TCP还是 KCP你都需要实现相关缓存控制策略

缓存控制:传送文件

你用 tcp传文件的话当网络没能力了你的 send调用要不就是阻塞掉要不就是 EAGAIN然后需要通过 epoll 检查 EPOLL_OUT事件来决定下次什么时候可以继续发送。

KCP 也一样,如果 ikcp_waitsnd 超过阈值比如2倍 snd_wnd那么停止调用 ikcp_sendikcp_waitsnd的值降下来当然期间要保持 ikcp_update 调用。

缓存控制:实时视频直播

视频点播和传文件一样,而视频直播,一旦 ikcp_waitsnd 超过阈值了,除了不再往 kcp 里发送新的数据包,你的视频应该进入一个 “丢帧” 状态,直到 ikcp_waitsnd 降低到阈值的 1/2这样你的视频才不会有积累延迟。

这和使用 TCP推流时碰到 EAGAIN 期间,要主动丢帧的逻辑时一样的。

同时如果你能做的更好点waitsnd 超过阈值了,代表一段时间内网络传输能力下降了,此时你应该动态降低视频质量,减少码率,等网络恢复了你再恢复。

缓存控制:游戏控制数据

大部分逻辑严密的 TCP游戏服务器都是使用无阻塞的 tcp链接配套个 epoll之类的东西当后端业务向用户发送数据时会追加到用户空间的一块发送缓存比如 ring buffer 之类,当 epoll 到 EPOLL_OUT 事件时其实也就是tcp发送缓存有空余了不会EAGAIN/EWOULDBLOCK的时候再把 ring buffer 里面暂存的数据使用 send 传递给系统的 SNDBUF直到再次 EAGAIN。

那么 TCP SERVER的后端业务持续向客户端发送数据而客户端又迟迟没能力接收怎么办呢此时 epoll 会长期不返回 EPOLL_OUT事件数据会堆积在该用户的 ring buffer 之中如果堆积越来越多ring buffer 会自增长的话就会把 server 的内存给耗尽。因此成熟的 tcp 游戏服务器的做法是当客户端应用层发送缓存非tcp的sndbuf中待发送数据超过一定阈值就断开 TCP链接因为该用户没有接收能力了无法持续接收游戏数据。

使用 KCP 发送游戏数据也一样,当 ikcp_waitsnd 返回值超过一定限度时,你应该断开远端链接,因为他们没有能力接收了。

但是需要注意的是KCP的默认窗口都是32比tcp的默认窗口低很多实际使用时应提前调大窗口但是为了公平性也不要无止尽放大不要超过1024

总结

缓存积累这个问题,不管是 TCP还是 KCP你都要处理因为TCP默认窗口比较大因此可能很多人并没有处理的意识。

当你碰到缓存延迟时:

  1. 检查 snd_wnd, rcv_wnd 的值是否满足你的要求,根据上面的公式换算,每秒钟要发多少包,当前 snd_wnd满足条件么
  2. 确认打开了 ikcp_nodelay让各项加速特性得以运转并确认 nc参数是否设置以关闭默认的类 tcp保守流控方式。
  3. 确认 ikcp_update 调用频率是否满足要求比如10ms一次

如果你还想更激进:

  1. 确认 minrto 是否设置,比如设置成 10ms, nodelay 只是设置成 30ms更激进可以设置成 10ms 或者 5ms。
  2. 确认 interval是否设置可以更激进的设置成 5ms让内部始终循环更快。
  3. 每次发送完数据包后,手动调用 ikcp_flush
  4. 降低 mtu 到 470同样数据虽然会发更多的包但是小包在路由层优先级更高。

如果你还想更快,可以在 KCP下层增加前向纠错协议。详细见协议分层最佳实践

更多见讨论记录:

https://github.com/skywind3000/kcp/issues/4

https://github.com/skywind3000/kcp/issues/93