Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

smux 阻塞问题 #722

Open
papwin opened this issue Sep 10, 2019 · 24 comments
Open

smux 阻塞问题 #722

papwin opened this issue Sep 10, 2019 · 24 comments

Comments

@papwin
Copy link

papwin commented Sep 10, 2019

目前来看,如果是应用层没有控制流量的场景,如 HTTP 大文件下载,服务器发送的数据会倾向于把所有中间节点的缓冲区塞满,导致其他连接阻塞。换言之,只要下载的文件大小大于中间节点的最小 smuxbuf,就不可避免有阻塞的现象。我使用 32MB 的 smuxbuf,并观测到在下载 100MB 测试文件的起初的几秒钟内,其他连接几乎完全被阻塞,连 Google 首页也无法打开。

  1. 我对此现象的分析是否正确?
  2. 如果正确,可否考虑加个针对每条连接的缓冲区上限,或是有其他更好的解决方案?

期待与您讨论 @xtaci

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

有限的内存,并行,充分的带宽利用,三者不可兼得。这是一个困境。-- "smux dilemma"

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

可以参考这里的最后一段:https://zhuanlan.zhihu.com/p/53849089

@papwin
Copy link
Author

papwin commented Sep 10, 2019

我就是看了您这篇文章,才推测前述使用场景会有阻塞问题,然后验证了下果然如此。内存不可能加得无限大,却可能有极大的文件的下载需求,阻塞不可避免。另一个角度考虑,数据中心间的网络质量远好于客户端,缓冲区通常会塞满,如果缓冲区设得太大,中途突然放弃下载,其实也是白白浪费了许多流量的。

所以我有个疑问,为什么要把多路的数据缓存在一个大缓冲区里?为每条连接设立独立的小缓冲区是否可行?

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

多条链接,需要开启拥塞控制算法来避免同一底层物理链路的相互竞争。
(你可以试试在kcptun里面 -nc 0,-conn 4来开启。)

然而开启拥塞控制算法的结果就是,在高丢包链路下,不可能做到稳定传输,因为窗口不够刚性。

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

我在想是否可以在smux做一个流量整形,这样也许会从发送的角度来控制发送者的公平性

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

xtaci/smux@78fdaa9

@papwin
Copy link
Author

papwin commented Sep 10, 2019

流量整形后,对于收方 smux 缓冲区会有什么影响呢?

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

流量整形后,发送端的某个流不会霸占整个带宽,发送带宽竞争更加公平。

这样产生的效果是,对某个流量高的stream,产生了write()阻塞,阻塞会反馈到发送源头,通过拥塞控制的传导,使其发送速度减慢。 当然,这个减慢是在窗口(带宽)满了后的行为,窗口不满的时候不会产生。

https://github.com/xtaci/kcptun/releases/tag/v20190910

@papwin
Copy link
Author

papwin commented Sep 10, 2019

已试验,结论是……没观测到改善(可能改善实在太有限)。

说下我的架构吧,手机/电脑 <==ss over TCP==> 上海VPS <==ss over KCP==> 硅谷VPS(kcptun接到本机 v2ray 进程) <====> 代理目标

上海(客户端)配置:/usr/local/bin/kcptun_client -r "xxxxxxx:443" -l ":443" -mode manual -nodelay 1 -interval 10 -resend 2 -nc 1 --sndwnd 3072 --rcvwnd 3072 --smuxbuf 33554432 --ds 7 --ps 3 --nocomp --crypt xor --key "密码" --dscp 46 --autoexpire 600 --scavengettl -1 --tcp

硅谷(服务端)配置:/usr/local/bin/kcptun_server -t "127.0.0.1:339" -l ":443" -mode manual -nodelay 1 -interval 10 -resend 2 -nc 1 --sndwnd 3072 --rcvwnd 3072 --smuxbuf 33554432 --ds 7 --ps 3 --nocomp --crypt xor --key "密码" --dscp 46 --tcp

其中,上海到硅谷 30-100 Mbps,代理目标在硅谷同机房,带宽很大(1Gbps 左右)

我在个人电脑上,wget 限制一个极低的速率(10k)下载代理目标的文件,开始下载后立刻不断刷新 Google 首页,观察到:刚开始下载时,还能流畅刷新,大约四秒钟后,出现阻塞,阻塞持续很长一段时间,然后恢复畅通。

我推测:开始下载后的前四秒钟,不阻塞是因为上海机子上的 32MB smuxbuf 还没有填满(上海到硅谷 30-100 Mbps),填满后,即使发送端优先发送其他流的数据也作用不大,因为 KCP 层还有很大一段缓冲区,其他流的数据一时半会儿也到不了上层的 smux。当然这和我限制极低的速率下载有关系,毕竟是为了模拟这样一种极端情况(几乎不取走数据)。其实这样一想,除非加大 KCP 层连接数量,否则还真是个无解的问题!

不知道我的理解是否正确,请不吝赐教,谢谢!

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

你的rcvwnd太大,因为很可能rcvwnd已经超过线路最大带宽,那么物理上的拥塞一直没有反馈到逻辑的拥塞上,无法触发shaper逻辑。你观测到的阻塞,很可能只是因为线路的阻塞。

注意,这个改动的假设是,滑动窗口满后,smux会均匀发送各个流的数据。如果线路没有拥塞,那么是FIFO的情况。

@xtaci
Copy link
Owner

xtaci commented Sep 10, 2019

// shaper shapes the sending sequence among streams
func (s *Session) shaperLoop() {
	var reqs shaperHeap
	var next writeRequest
	var chWrite chan writeRequest

	for {
		if len(reqs) > 0 {
			chWrite = s.writes
			next = heap.Pop(&reqs).(writeRequest)
		} else {
			chWrite = nil
		}

		select {
		case <-s.die:
			return
		case r := <-s.shaper:
			if chWrite != nil { // next is valid, reshape
				heap.Push(&reqs, next)
			}
			heap.Push(&reqs, r)
		case chWrite <- next:
		}
	}
}

注意这里的逻辑:
chWrite,是发送到kcp-go的数据包,如果 chWrite产生阻塞,新发送数据包必定会不断的进入 s.shaper,产生FQ排队,最终的输出一定是均匀的。
如果触发不了chWrite的阻塞,即:线路带宽很充裕(rcvwnd, sndwnd足够大),那么是不会触发FQ排队的。

@papwin
Copy link
Author

papwin commented Sep 10, 2019

我觉得是排队了,开始下载后,用 iftop 看到几秒钟就从代理目标接收了约 70MB 的数据随后缓慢增长,而 wget 取走数据的速度只有 10KB/s,数据不会消失,一定是填在整条链路的各处缓冲区里了

@papwin
Copy link
Author

papwin commented Sep 10, 2019

增大两端 smuxbuf 到 100MB 以上,极低速率下载 100MB 测试文件时其他流的阻塞现象消失,几乎整个文件积压在上海 VPS(客户端),慢慢往我个人电脑传。

这种情况可以抽象成有一个数据源只管不断的发,尽管接收端速率低下,但起初因为中间节点 buffer 充足,会保持高速发送,等 buffer 陆续满了,阻塞反馈到数据源时,两个 kcptun 端点、v2ray 软件以及涉及到的所有 socket buffer 都已经满了

@xtaci
Copy link
Owner

xtaci commented Sep 20, 2019

如果只管发送,不管读取,那么操作系统的 TCP socket buffer(net.ipv4.tcp_rmem),一样也会塞满,占用内存,这样的链接多了后,OS是分配不出来内存的,也会导致新的链接速度降到很低(rcv_wnd变小)。

可以理解为,操作系统的内存够大,在大多数时候避免了出现HOLB的问题,那么问题等价于提高-smuxbuf的数值,也可以在大多数时候避免出现HOLB问题。

目前唯一缺乏的,是tcp per socket buffer的设置,即per stream buffer的设置。

要实现这个,就需要扩展smux,增加控制指令,告知发送方当前的stream buffer的大小。

@xtaci
Copy link
Owner

xtaci commented Sep 21, 2019

https://github.com/xtaci/smux/tree/v2
可以看下这个分支 smux v2协议升级,可以实现per stream的流控,理论上可以解决这个阻塞问题。

增加协议 frame.go: cmdUPD

@papwin
Copy link
Author

papwin commented Sep 21, 2019

辛苦了,明天编译一下跑跑看。系统 socket buffer 满的问题,如果代理的连接不复用的话,应该还是只阻塞该条代理连接本身吧,不会影响到其他连接。smux 算是(我的使用场景里)唯一存在连接复用的环节了。

@xtaci xtaci added the smux label Sep 22, 2019
@xtaci xtaci pinned this issue Sep 22, 2019
@xtaci
Copy link
Owner

xtaci commented Sep 22, 2019

但注意,虽然smux version 2.0可以缓解HOLB问题,但依然受制于 有限的内存,并行,充分的带宽利用,三者不可兼得 困境。

  1. 当 streambuf = smuxbuf的时候,等价于smux version 1
  2. 当streambuf < smuxbuf时,在限定了stream内存使用的同时,stream带宽也就被限定了。
  3. 同时提高streambuf和smuxbuf,可以通过牺牲内存的来获得最大带宽。

@xtaci
Copy link
Owner

xtaci commented Sep 22, 2019

我放了个pre-release
https://github.com/xtaci/kcptun/releases/tag/v20190922

可以先尝试下这个版本,设置如下:

-smuxver 2
-streambuf 1048576   <- 流内存限定

@papwin
Copy link
Author

papwin commented Sep 22, 2019

做了对比实验,效果非常不错,不存在饿死其他 stream 的问题了

@xtaci
Copy link
Owner

xtaci commented Sep 23, 2019

目前我认为这是唯一正确的办法, 即:实现对发送方Write()函数的阻塞控制,流式的窗口滑动。

@xtaci xtaci added the smux v2 label Sep 23, 2019
@xtaci
Copy link
Owner

xtaci commented Sep 23, 2019

已经放入 https://github.com/xtaci/kcptun/releases/tag/v20190923

@lazy-luo
Copy link

lazy-luo commented Oct 10, 2019

@xtaci 的观点方向是正确的,我之前实现过类似算法,其实主要矛盾在于非阻塞io方式下如何设计良好的cork机制。我采用的办法是(说思路,忽略加锁细节):
1、采用tcp per socket wirte buffer设计,且buffer通过内存池方式管理
2、异步read请求根据掩码选择读或者延迟读(cork方法会设置可用事件(读/写)掩码)
3、write 返回AGAIN后缓存,且设置源头socket读cork,防止快发送/慢接收持续恶化
4、有write缓存的socket,关注WRITABLE事件,可写时继续发送,如果发完release之前设置的cork,释放write-buffer
6、并发write请求时如果有未发送数据,且当前发送数据+待发送缓存>缓存最大值(默认设置256K per-socket)时,当前线程尝试发送,直到缓存空闲大小足以容纳当前发送数据大小

@k79e
Copy link

k79e commented Apr 10, 2021

最近在整个系统上开了udp都能拿下的列队控制 结果无意发现 下载大文件好几个的时候 客户端网速打满 竟然不堵塞
开网页还都可以 爆发力也不错 XD
//光开列队还不够 是基本屁用没有 还得加限速才成

我用的smux1 这个好像不是holb相关的事情?? 到时候我试试fifo就知道了
我那边测试的是tcp单连接都可以造成一大堆丢包............... 额 堵塞控制没啥用的样子
(所以人们感觉隧道卡了可能隧道自己没卡 是服务器整个系统的网络都卡了)

@chinnkarahoi
Copy link

chinnkarahoi commented Jul 22, 2022

这个就是类似http/2队头阻塞(Head-of-line blocking)的问题。kcp协议本身只针对单连接,不支持多路复用。所以在kcp之上实现的多路复用必然会有队头阻塞的问题。从根本上解决只能像quic那样把多路复用移到协议同层上实现。

@xtaci xtaci unpinned this issue Mar 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants