Repository: fatedier/frp (language: go)

Issue:
tcpmux 会大幅降低链路速度
### Bug Description

**仅创建一个tcp内网穿透连接**

修改选项 `tcp_mux = true`

开启tcp_mux 测速得 10-15 Mbps
关闭tcp_mux 测速得 100 Mbps


### frpc Version

0.42.0

### frps Version

0.42.0

### System Architecture

linux/amd64

### Configurations

[common]
server_addr = *****
server_port = 3000
token = *****

heartbeat_interval = 3
heartbeat_timeout = 10

tcp_mux = true
pool_count = 1

### Logs

_No response_

### Steps to reproduce

_No response_

### Affected area

- [ ] Docs
- [ ] Installation
- [X] Performance and Scalability
- [ ] Security
- [ ] User Experience
- [ ] Test and Release
- [ ] Developer Infrastructure
- [ ] Client Plugin
- [ ] Server Plugin
- [ ] Extensions
- [ ] Others


Discussion / hints:
测速方向 frpc客户端(源)->frps服务端(下载)
https://github.com/hashicorp/yamux/issues/24
kcptun 2016年的时候使用yamux也出现了同样的问题
https://github.com/xtaci/kcptun/issues/126
我已经自行将frp中的 `yamux` 替换为 https://github.com/xtaci/smux

速度已经达到正常水平
![20220618021135](https://user-images.githubusercontent.com/3510035/174354732-4cc960a6-896a-4f9a-95fe-b662e404c0c5.png)

在我这里问题解决了,如有觉得必要官方可以也进行替换. (本issues可以随时关闭)
目前不会替换，稳定性比速度更重要，v2 中可能会倾向于使用 QUIC。
Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.
> 我已经自行将frp中的 `yamux` 替换为 https://github.com/xtaci/smux
> 
> 速度已经达到正常水平 ![20220618021135](https://user-images.githubusercontent.com/3510035/174354732-4cc960a6-896a-4f9a-95fe-b662e404c0c5.png)
> 
> 在我这里问题解决了,如有觉得必要官方可以也进行替换. (本issues可以随时关闭)

经过分析，发现了yamux和smux在速度上存在的区别的可能原因。smux的第一个版本协议里，只有一整个session的接收缓冲区，所有的stream共用，这导致了smux可以减少很多在yamux上使用的缓冲区通信，因为每次的缓冲区通信需要一个TTL时间，**所以降低了速度，但是重点来了，这个设计会有一个严重的问题，当某个stream有问题，占用满了整个session缓冲区时，会影响同一个session下的所有stream，所以后来smux开发了v2版本添加了和yamux一样的stream缓冲区控制逻辑，但是如果用户没有特别指定使用v2版本，默认是使用v1版本。所以想问下当时您测试的环境使用的是V2还是V1版本**？

那么接下来说说我自己在**排除**这个TTL影响的情况下测试（因为做包含TTL的测试比较麻烦，让我偷懒一下 : ) )
我本地直接复制了yamux的bechmark测试代码（smux没有benchmark代码），因为yamux和smux的对外接口很像，所以基本没有问题，yamux的测试代码是直接在本地内存拷贝模拟的网络IO，所以基本没有延迟，这个测试主要是测试代码的CPU和内存消耗以及操作速度，直接说结论，都是yamux胜出。全部都使用默认配置。

欢迎大家一起讨论

### yamux
<img width="1313" alt="image" src="https://user-images.githubusercontent.com/1856880/186386382-b5486610-8df6-4ff0-827b-5589aa3db23b.png">

### smux（v1）
<img width="1334" alt="image" src="https://user-images.githubusercontent.com/1856880/186386857-9c175680-c195-44c9-a456-4ca39b5a7e2d.png">

### smux（v2）
<img width="1334" alt="image" src="https://user-images.githubusercontent.com/1856880/186387365-bb3be0a3-fb92-4032-a23a-4211f8f8672e.png">

使用的是smux v1
> 但是重点来了，这个设计会有一个严重的问题，当某个stream有问题，占用满了整个session缓冲区时，会影响同一个session下的所有stream
你所说的问题我确实也发现了，但是吞吐量对我来说更为重要

所谓的smux v2最后更新时间为2019年，而v1是2021年。

我刚才简单测试了一下smux v2也存在吞吐量低的问题

这个问题不必过于纠结，我自己现在自用的版本已经满足我的需求了。可能对于绝大多数人来说**稳定**比**速度**更为重要
> 使用的是smux v1
> 
> > 但是重点来了，这个设计会有一个严重的问题，当某个stream有问题，占用满了整个session缓冲区时，会影响同一个session下的所有stream
> 
> 你所说的问题我确实也发现了，但是吞吐量对我来说更为重要
> 
> 
> 
> 所谓的smux v2最后更新时间为2019年，而v1是2021年。
> 
> 
> 
> 我刚才简单测试了一下smux v2也存在吞吐量低的问题
> 
> 
> 
> 这个问题不必过于纠结，我自己现在自用的版本已经满足我的需求了。可能对于绝大多数人来说**稳定**比**速度**更为重要

我也只是最近在研究这两个库，看到了相关的讨论顺便回复一下
> 目前不会替换，稳定性比速度更重要，v2 中可能会倾向于使用 QUIC。

使用QUIC的日子可能不会那么快到来，我有个建议，是不是可以给在启用tcpmux的时候，可以设置yamux的stream**最大缓冲大小**。yamux默认的最大缓冲大小为256KB，这个缓冲比较保守。
这样当每个stream实际可用速度大于 **256KB/RTT** 的时候就至少受一个TTL时间的缓冲等待影响。

假设当前client和server的RTT为50ms，当速度要大于 256KB*(1000ms/50ms) = 5MB/s = 40Mb/s 时，就要等待一个RTT时间来确认缓冲区，这个传输时间的影响最大是多等待一个RTT，速度会降到 20Mb/s左右。那么在这种RTT稳定为50ms的情况下，即使CPU运算再快，带宽再大，速度也不可能提升，而相应的提高这个最大缓存带入公式可以得到相应的速度提升（这个速度是指每个stream的最大速度）。

这个缓冲区设置可以针对实际的网络使用情况进行优化。例如，如果网络传输为常规的网页访问，小的请求头，比较大的下行，那么只需要提高Client这一端的缓存，反之也是如此。当这个缓存提高到10MB这个RTT影响会比较小了，甚至当网络传输不大于5MB的时候不会受到这个RTT的影响。

再来说这个缓冲调大后带来的问题，假如Client端的缓存调整为10MB，只要Client端读取没有出现阻塞，一个stream所占用的内存也并不会大到10MB。所以我觉得对处于安全网络环境的frp用户可以根据自己的需要调整这个大小，优化自己的网络。给用户一个选择的机会，也是一个用空间换时间的做法
@aaftio 可以考虑，是否可以有相应的测试数据来说明其影响？
> @aaftio 可以考虑，是否可以有相应的测试数据来说明其影响？

可以，但是大带宽环境我只有局域网环境，并且由于延迟只有5ms左右，应该只有很大的数据包传输才能看到效果，等我试试
> > @aaftio 可以考虑，是否可以有相应的测试数据来说明其影响？
> 
> 可以，但是大带宽环境我只有局域网环境，并且由于延迟只有5ms左右，应该只有很大的数据包传输才能看到效果，等我试试

linux 下可以使用 `tc` 模拟丢包和延迟，可以直接在单机环境测试。
> smux

方便把替换成smux之后的代码开源一下吗？
通过调大yamux.Config.MaxStreamWindowSize（默认大小为256*1024，不能低于此值）可以缓解限速问题。如果带宽比较大的情况下，使用yamux做多路复用，假设多路复用客户端和服务端的RTT `t` ms，则单个stream的带宽约为：
`((1000/t) * (MaxStreamWindowSize/2) * 8)/(1024*1024) `Mbps
> > > @aaftio 可以考虑，是否可以有相应的测试数据来说明其影响？
> > 
> > 
> > 可以，但是大带宽环境我只有局域网环境，并且由于延迟只有5ms左右，应该只有很大的数据包传输才能看到效果，等我试试
> 
> linux 下可以使用 `tc` 模拟丢包和延迟，可以直接在单机环境测试。

近段工作生活变化很大，没有花时间在这上面了，所以鸽了，只能等有时间了再折腾这块
> 通过调大yamux.Config.MaxStreamWindowSize（默认大小为256*1024，不能低于此值）可以缓解限速问题。如果带宽比较大的情况下，使用yamux做多路复用，假设多路复用客户端和服务端的RTT `t` ms，则单个stream的带宽约为： `((1000/t) * (MaxStreamWindowSize/2) * 8)/(1024*1024) `Mbps

如之前的交流，是想以测试结果来验证理论，如果方便有时间你可以帮忙发个测试结果么。
> 

![image](https://user-images.githubusercontent.com/45646343/236771702-46274a15-56bb-4ed6-9e0e-8a443e667539.png)
这个是我使用yamux做代理测试的结果（自己写的一个简单的多路复用代理，没有用frp）。从实验结果来看，通过调大yamux.Config.MaxStreamWindowSize可以缓解带宽限制，这个限制只针对_单个stream_。比如带宽的物理限制是100Mbps，使用了多路复用后单个stream被限制到10Mbps，则开启两个stream时，两个stream都能达到10Mbps。
> > 
> 
> ![image](https://user-images.githubusercontent.com/45646343/236771702-46274a15-56bb-4ed6-9e0e-8a443e667539.png) 这个是我使用yamux做代理测试的结果（自己写的一个简单的多路复用代理，没有用frp）。从实验结果来看，通过调大yamux.Config.MaxStreamWindowSize可以缓解带宽限制，这个限制只针对_单个stream_。比如带宽的物理限制是100Mbps，使用了多路复用后单个stream被限制到10Mbps，则开启两个stream时，两个stream都能达到10Mbps。

谢谢你的数据。 我刚刚才看到现在已经支持 QUIC 了！这个似乎已经不那么急了 。

@fatedier 这个测试虽然没有通过改变 RTT 来验证是 RTT 对传输速度上限的影响，但是验证了在相同的 RTT 下，不同的最大缓冲区对传输上限的影响。当然在提供这个参数设置的时候，为了更好去解释这个参数，还需要提供 Client 端的设置值和 Server 端的设置值对不同传输场景的影响。如果还是觉得添加这个配置的条件不足，再等等我有时间提供再详细的测试数据。


@Triangleowl @aaftio 非常感谢你们提供的数据和对这方面的研究。

我自己对于 yamux 的各个参数细节可能没有精力深入，所以相对会比较谨慎。如果确实能解决问题，提供参数配置也没问题。

Fix patch:
diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go
index 4ba8d50c870..e6c76203ebf 100644
--- a/client/proxy/xtcp.go
+++ b/client/proxy/xtcp.go
@@ -137,7 +137,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s
 
 	fmuxCfg := fmux.DefaultConfig()
 	fmuxCfg.KeepAliveInterval = 10 * time.Second
-	fmuxCfg.MaxStreamWindowSize = 2 * 1024 * 1024
+	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 	fmuxCfg.LogOutput = io.Discard
 	session, err := fmux.Server(remote, fmuxCfg)
 	if err != nil {
diff --git a/client/service.go b/client/service.go
index 0849484c53f..df85c05f4b1 100644
--- a/client/service.go
+++ b/client/service.go
@@ -396,6 +396,7 @@ func (cm *ConnectionManager) OpenConnection() error {
 	fmuxCfg := fmux.DefaultConfig()
 	fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.TCPMuxKeepaliveInterval) * time.Second
 	fmuxCfg.LogOutput = io.Discard
+	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 	session, err := fmux.Client(conn, fmuxCfg)
 	if err != nil {
 		return err
diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go
index 73f307896e5..8dcd9363e67 100644
--- a/client/visitor/xtcp.go
+++ b/client/visitor/xtcp.go
@@ -351,7 +351,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
 
 	fmuxCfg := fmux.DefaultConfig()
 	fmuxCfg.KeepAliveInterval = 10 * time.Second
-	fmuxCfg.MaxStreamWindowSize = 2 * 1024 * 1024
+	fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 	fmuxCfg.LogOutput = io.Discard
 	session, err := fmux.Client(remote, fmuxCfg)
 	if err != nil {
diff --git a/server/service.go b/server/service.go
index e67ee73d3f5..3c4f5a81e5b 100644
--- a/server/service.go
+++ b/server/service.go
@@ -461,6 +461,7 @@ func (svr *Service) HandleListener(l net.Listener) {
 				fmuxCfg := fmux.DefaultConfig()
 				fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
 				fmuxCfg.LogOutput = io.Discard
+				fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
 				session, err := fmux.Server(frpConn, fmuxCfg)
 				if err != nil {
 					log.Warn("Failed to create mux connection: %v", err)


Test patch: