xtaci / smux

A Stream Multiplexing Library for golang with least memory usage(TDMA)
MIT License
1.31k stars 196 forks source link

add token into stream #19

Closed cs8425 closed 5 years ago

cs8425 commented 7 years ago

18

解決一個慢的stream餓死其他stream的問題 保留原本的token來控制總buffer的使用量 除非夠慢又夠多的stream才會造成餓死的問題 (stream的MaxBuffer * N >= session的MaxBuffer)

config的預設值目前我是抓比較高, 可以看情況調整 每個Session最多16MB (MaxReceiveBuffer) 每個Stream最多16kB (MaxStreamBuffer) Stream buffer小於4kB的時候允許對面寫入新資料 (MinStreamBuffer) 預設值能允許1024個慢速stream才會造成餓死 假設不被惡意攻擊(對面無視cmdFUL控制封包繼續傳) 此情況下記憶體使用量 ~= 16MB

xtaci commented 7 years ago

你这样做最大的传输速度被限制在 16KB/RTT了

cs8425 commented 7 years ago

實測過並不會限制在16KB/RTT 設成16KB, 4MB, ping 12ms, 一樣跑到94Mbps(我這邊的頻寬上限100Mbps) 設成4KB, 4MB, 下降為70Mbps 只有stream read的速度<對面傳的速度才會受制於stream的buffer

xtaci commented 7 years ago

你这个ping值太小,如果ping值一大,比如200ms,你速度就下来了

cs8425 commented 7 years ago

我去租一台ping 200ms的主機試試@@ 晚點再回報 不知道有沒有更好的模擬方法?

cs8425 commented 7 years ago

scaleway的巴黎主機(VC1S)實測 ping 300~350ms的情況下差異並沒有很大 kcptun為master直接clone下來編譯的 只有smux有差異 是否有別的參數/測試建議? ps.如果有需要主機可以直接開帳號給你測試

iperf server端皆為: iperf -s -p 9999 -f M -N socks5 server已確認過不是瓶頸

改過的:

$ iperf -c 127.0.0.1 -p 5002 -f M -N -w 100M -t 60 -i 5
------------------------------------------------------------
Client connecting to 127.0.0.1, TCP port 5002
TCP window size: 0.41 MByte (WARNING: requested  100 MByte)
------------------------------------------------------------
[  3] local 127.0.0.1 port 51926 connected with 127.0.0.1 port 5002
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0- 5.0 sec  46.9 MBytes  9.38 MBytes/sec
[  3]  5.0-10.0 sec  40.1 MBytes  8.03 MBytes/sec
[  3] 10.0-15.0 sec  39.5 MBytes  7.90 MBytes/sec
[  3] 15.0-20.0 sec  40.1 MBytes  8.03 MBytes/sec
[  3] 20.0-25.0 sec  40.1 MBytes  8.03 MBytes/sec
[  3] 25.0-30.0 sec  40.8 MBytes  8.15 MBytes/sec
[  3] 30.0-35.0 sec  37.4 MBytes  7.47 MBytes/sec
[  3] 35.0-40.0 sec  42.1 MBytes  8.43 MBytes/sec
[  3] 40.0-45.0 sec  41.6 MBytes  8.32 MBytes/sec
[  3] 45.0-50.0 sec  41.0 MBytes  8.20 MBytes/sec
[  3] 50.0-55.0 sec  40.6 MBytes  8.12 MBytes/sec
[  3] 55.0-60.0 sec  40.9 MBytes  8.18 MBytes/sec
[  3]  0.0-60.1 sec   491 MBytes  8.18 MBytes/sec

http://beta.speedtest.net

原本的:

$ iperf -c 127.0.0.1 -p 5002 -f M -N -w 100M -t 60 -i 5
------------------------------------------------------------
Client connecting to 127.0.0.1, TCP port 5002
TCP window size: 0.41 MByte (WARNING: requested  100 MByte)
------------------------------------------------------------
[  3] local 127.0.0.1 port 51940 connected with 127.0.0.1 port 5002
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0- 5.0 sec  45.8 MBytes  9.15 MBytes/sec
[  3]  5.0-10.0 sec  40.5 MBytes  8.10 MBytes/sec
[  3] 10.0-15.0 sec  40.5 MBytes  8.10 MBytes/sec
[  3] 15.0-20.0 sec  40.6 MBytes  8.12 MBytes/sec
[  3] 20.0-25.0 sec  40.1 MBytes  8.03 MBytes/sec
[  3] 25.0-30.0 sec  40.4 MBytes  8.07 MBytes/sec
[  3] 30.0-35.0 sec  40.9 MBytes  8.18 MBytes/sec
[  3] 35.0-40.0 sec  40.1 MBytes  8.03 MBytes/sec
[  3] 40.0-45.0 sec  40.2 MBytes  8.05 MBytes/sec
[  3] 45.0-50.0 sec  40.9 MBytes  8.18 MBytes/sec
[  3] 50.0-55.0 sec  41.0 MBytes  8.20 MBytes/sec
[  3] 55.0-60.0 sec  40.6 MBytes  8.12 MBytes/sec
[  3]  0.0-60.0 sec   492 MBytes  8.19 MBytes/sec

http://beta.speedtest.net

client設定

2017/04/25 17:45:30 main.go:283: version: SELFBUILD
2017/04/25 17:45:30 main.go:319: listening on: 127.0.0.1:5002
2017/04/25 17:45:30 main.go:320: encryption: salsa20
2017/04/25 17:45:30 main.go:321: nodelay parameters: 0 30 2 1
2017/04/25 17:45:30 main.go:322: remote address: 212.47.246.125:5001
2017/04/25 17:45:30 main.go:323: sndwnd: 128 rcvwnd: 512
2017/04/25 17:45:30 main.go:324: compression: true
2017/04/25 17:45:30 main.go:325: mtu: 1350
2017/04/25 17:45:30 main.go:326: datashard: 1 parityshard: 0
2017/04/25 17:45:30 main.go:327: acknodelay: true
2017/04/25 17:45:30 main.go:328: dscp: 0
2017/04/25 17:45:30 main.go:329: sockbuf: 16777216
2017/04/25 17:45:30 main.go:330: keepalive: 10
2017/04/25 17:45:30 main.go:331: conn: 1
2017/04/25 17:45:30 main.go:332: autoexpire: 0
2017/04/25 17:45:30 main.go:333: scavengettl: 0
2017/04/25 17:45:30 main.go:334: snmplog: 
2017/04/25 17:45:30 main.go:335: snmpperiod: 60

server設定:

2017/04/25 09:45:29 main.go:297: version: SELFBUILD
2017/04/25 09:45:29 main.go:330: listening on: [::]:5001
2017/04/25 09:45:29 main.go:331: target: 127.0.0.1:9999
2017/04/25 09:45:29 main.go:332: encryption: salsa20
2017/04/25 09:45:29 main.go:333: nodelay parameters: 0 50 0 0
2017/04/25 09:45:29 main.go:334: sndwnd: 1024 rcvwnd: 1024
2017/04/25 09:45:29 main.go:335: compression: true
2017/04/25 09:45:29 main.go:336: mtu: 1350
2017/04/25 09:45:29 main.go:337: datashard: 1 parityshard: 0
2017/04/25 09:45:29 main.go:338: acknodelay: true
2017/04/25 09:45:29 main.go:339: dscp: 0
2017/04/25 09:45:29 main.go:340: sockbuf: 16777216
2017/04/25 09:45:29 main.go:341: keepalive: 10
2017/04/25 09:45:29 main.go:342: snmplog: 
2017/04/25 09:45:29 main.go:343: snmpperiod: 60
2017/04/25 09:45:29 main.go:344: pprof: false
xtaci commented 7 years ago

看了下,session.token你没有去掉, 总阀门没有关啊。 只是在当前的基础上,还增加了限速。

xtaci commented 7 years ago

我明白你的意思了,你是想通过接收方发送控制信息,告诉发送方停止推流。

发送方必须要执行cmdFUL,cmdEMP进行阻塞。

这种方法的主要的问题就是可能造成限速, 最终 cmdFUL, cmdEMP会成为驱动下一次数据发送的时钟信号,这个时钟信号的间隔就是RTT。

你上面的测试是一个快速的consumer,可能并没有触发到cmdFUL/EMP。也就和原来一样的结果。

cs8425 commented 7 years ago

session.token沒去掉是為了限制記憶體的總使用量 限速是針對個別的stream 讀取慢的stream可以通知對面對應的stream不要再傳資料 畢竟我們的最上層是TCP 如果不通知的話 有這種可能: A端的ID 1拼命傳資料(ex: 上傳測試) B端對應的stream無法達到那麼快的速度 於是A端的ID 1會傳到整個session的token用完(TCP還能寫入就會不斷丟) 造成其他stream一起卡住...

那我這個設計只是加入一個通知的功能 當B端對應的stream無法達到那麼快的速度 會卡住A端的寫入 這麼一來session的token就不會被用完 也就不會造成其他stream一起卡住的問題

是的 理論上是能解決原本issue 至於這個"限速"的後遺症倒是還好 畢竟就是因為太慢才會觸發這個機制

ok 先吃個飯 晚點測試其他的方法 一邊跑youtube一邊跑測速行嗎? 有沒有好一點的測試方法可以建議呢?

xtaci commented 7 years ago

问题的核心在于,在什么条件下去通知发送方,阈值什么是合理的,16KB是拍脑袋?

为什么不是在 stream buffer 达到整体半窗(MaxReceiveBuffer/2)的时候.

cs8425 commented 7 years ago

好的 那我先解釋一下MaxStreamBufferMinStreamBuffer的關係 一個有FIFO buffer的高速系統 一般設計上會用總大小的20%當(快要)空 80%當(快要)滿的時候傳送訊號 這樣不會浪費buffer也不會造成overflow (這類的設計在MCU之類的硬體系統很常見, 有些會選30:70、25:75、10:90之類的) 16KB => 80% 4KB => 20% 當然更進階一點的就是設計一個簡單的估測系統(跟RTT、讀寫速度有關) 預估什麼時候送cmdFUL不會造成overflow也不會剩下太多空間 什麼時候送cmdEMP可以在buffer真的被讀到空之前收到新資料

至於MaxStreamBuffer為何選16KB 我是隨手抓沒錯啦 不過還是有點道理的 假設一個(外部無瓶頸)高寫高讀的情況:

  1. 讀取端的速度會被侷限於read的頻率&一次讀取的量
  2. Buffer至少要保留write完到下次read的資料量才不會影響寫入速度

然而read/write的頻率是有上限的 我沒搞錯的話 在linux kernel跟Internal kernel timer frequency有關 是1000Hz (編譯kernel的時候能調整), 超過就要丟到kernel的buffer/queue 16KB * 1000Hz = 128Mbps ~= 16MB/s 我想應該沒有多少人的頻寬可以支撐N條速度超過128Mbps的stream吧...? 希望我這樣解釋夠清楚@@

xtaci commented 7 years ago
  1. 也就是说16KB是经验值, 但是我说的一半也是有道理的, 在cmdFUL到达发送方的半个RTT时间内,剩下的来自发送方的数据流在这个时间刚好把buffer填满。

  2. 这样貌似解决的问题是慢速receiver导致卡住的问题,但没有解决快速sender+receiver抢占带宽的问题。 也就是流之间的QoS balance。这个问题存在根本的复杂性。

cs8425 commented 7 years ago

為何我不用選MaxReceiveBuffer / 2 因為用這個數值的話 通知對面暫停的時機太晚 有可能 1.速度不夠, 根本用不到那麼大的buffer, 從頭到尾幾乎都沒觸發 2.只要少量的stream觸發這個機制, 很容易造成整個session卡住

正常來說stream buffer使用量會是一個夠小但是變化頻率高的數值 (buffer只保留write完到下次read的資料量, 大大你可以trace一下session.token跟stream.token的數值變化) 太大的stream buffer(相對於MaxReceiveBuffer)效果反而不好

是解決少數慢速receiver造成卡住的問題沒錯 如果要stream之間的QoS問題 可以在這個基礎上加個map計算&紀錄每個stream的速度 大概這幾個速度要紀錄: 下載方向的讀寫、上傳方向的寫、上傳方向對面的讀(這個比較不準, 靠cmdFULcmdEMP回授而已, 可能需要在格式加入精確數值的欄位) 速度超標的就通知對面暫停寫入 自己這邊也可以暫停自己的寫入 詳細的頻寬分配方法/條件就要再思考思考了 可以考慮丟個handler function給上層應用來搞XD

另外我發現我命名好像不是那麼好 stream.markFUL()換成stream.pauseWrite() stream.markEMP()換成stream.resumeWrite() 這樣應該會比較好理解

xtaci commented 7 years ago

修改名字是可以的,另外,这个改动不兼容原来的代码,会造成session.go:273的退出,需要一个开关。

另外 CI 目前还过不了,你需要测试下:

  - go test -coverprofile=coverage.txt -covermode=atomic -bench .
cs8425 commented 7 years ago

在config裡面加入一個bool來開關行嗎? 預設要關閉還是開啟? 順便問一下, protocol version通常是什麼時候才會增加/改變?

test的部份我覺得蠻奇怪的 在我電腦上測試是會過的 CI不知道為啥沒過

$ go test -coverprofile=coverage.txt -covermode=atomic -bench .
test -coverprofile=coverage.txt -covermode=atomic -bench .
BenchmarkAcceptClose-2         50000         58236 ns/op
BenchmarkConnSmux-2              100      11094876 ns/op      11.81 MB/s
BenchmarkConnTCP-2             10000        109450 ns/op    1197.54 MB/s
PASS
coverage: 90.5% of statements
ok      _/home/cs8425/code/smux 11.486s
xtaci commented 7 years ago

预设应该同之前的版本保持一致,用法无需改动。

codecov-io commented 7 years ago

Codecov Report

Merging #19 into master will increase coverage by 1.04%. The diff coverage is 94.89%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master     #19      +/-   ##
=========================================
+ Coverage   89.06%   90.1%   +1.04%     
=========================================
  Files           4       4              
  Lines         384     475      +91     
=========================================
+ Hits          342     428      +86     
- Misses         37      40       +3     
- Partials        5       7       +2
Impacted Files Coverage Δ
frame.go 100% <ø> (ø) :arrow_up:
session.go 89.49% <100%> (+1.23%) :arrow_up:
mux.go 94.28% <77.77%> (-5.72%) :arrow_down:
stream.go 89.42% <95.38%> (+2.52%) :arrow_up:

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 2de5471...a72c8d6. Read the comment docs.

xtaci commented 7 years ago

带stream.token

2017-04-26 2 02 26

原始

2017-04-26 2 04 03

实测对于youtube播放,有明显的速度下降,应该就是stream流控所致。

youtube的视频流会应该会通过阻塞读取(暂停读取)来控制TCP推流速度。但加上stream.token的流控,确实又加了一层 cmdFUL/EMP的时钟,或者说类似于TCP_CORK的效果。

cs8425 commented 7 years ago

算是trade off吧 剛連線的時候的確無法迅速提高throughput 有個改法是剛連線的stream buffer給比較大 過了幾秒再調回正常(要幾秒可能要思考一下) 代價是一次開多個連線的時候記憶體使用量會瞬間飆高 等速度穩定之後才會降回去 雖然個人認為youtube播放只要跟的上不卡就行... 大大你覺得呢? 剛開的連線給先給比較大的stream buffer這個設計是否有必要?

xtaci commented 7 years ago

我还是觉得,这个问题有根本的困难,内存总用量和流量平衡不可兼得。 可以继续探讨。

cs8425 commented 7 years ago

列一下幾個已知條件/現象:

那我試試看改個這種東西出來: 1.剛連線的stream buffer設為MaxReceiveBuffer/2 (或是除其他數, 儘量大但是不要卡住session) 2.觀察stream buffer使用量, 當前用量開始變小(只在0跟上次最大用量之間跳), 判定為進入穩定 3.進入穩定之後stream buffer重設成某個較小的數值(ex: 16KB) 理論上應該有用....晚點試試... RTT部份還在想要怎估測發送時機比較好

cs8425 commented 7 years ago

@xtaci 大大你再試試這個版本 加入了RTT測試(收到cmdNOP回傳cmdACK) 簡單估算cmdFULcmdEMP發送時機跟stream buffer應該給多少 用自己嚕的TCP over TCP跑youtube測試, 跟 c6b175f 比, 初始爬升快了不少, 最終速度高了大約25% 但是其他iperf之類的速度測試結果差不多就是了

cs8425 commented 7 years ago

重編了kcptun 其他測試參數跟上次一樣 不知道為啥今天比較快@@

重點在throughput變化的部份 原本的smux初期會衝到40Mbps左右, 然後再降回25Mbps左右 c6b175f 是一路慢慢爬到20Mbps左右測試就結束了 31ccec0 衝到25Mbps左右就維持在那個位置 上傳部份可能頻寬不夠大看不太出來

目前版本 31ccec0: kcptun-smux-31ccec0

c6b175f: kcptun-smux-c6b175f

原本的: kcptun-smux-org

xtaci commented 7 years ago

降速是无论如何也不能接受的,影响的用户量太大了。

cs8425 commented 7 years ago

31ccec0 並沒有降速啊 是直接爬到頻寬上限就穩定了 而原本的會超過頻寬上限之後才回歸正常 超過的部份就是靠記憶體再撐 所以 31ccec0 如果要達到一樣的效果 把buffer限制開大一點即可

xtaci commented 7 years ago

我先观察一下

xtaci commented 7 years ago

未来两周会有点忙,后面再回复,暂时没有时间验证这个,告知一下。

cs8425 commented 7 years ago

要不要另外編譯一版kcptun 開個issue特別著名一下是測試版 交給大家決定要不要當白老鼠&回報結果? 如果不行也沒關係 就等大大有空摟

xtaci commented 7 years ago

不急,慢慢来,先思考一下。

xtaci commented 7 years ago

抱歉晚回复,前几周工作上的事有点忙。

xtaci commented 7 years ago

我始终觉得:

  1. 最大吞吐量
  2. stream之间流量平衡(stream饿死)
  3. 内存总量一定

这三者最多同时满足两者。

cs8425 commented 7 years ago

大致上正確 第3點不是總量一定 應該說 RAM要小於 "最大吞吐量所需的RAM" x "stream數量" 而且3者之間的分配vs效果 愈接近極限 會愈沒有效果 假設最大吞吐量要 32kB / stream 95%的最大吞吐量可能只要 16kB / stream 如果RAM的使用量真的無法妥協 我會偏好降個幾%的最大吞吐量 來換(幾乎)所有stream不會卡住 畢竟...youtube點下去播了幾秒發現這不是想要的 點其他影片連結就整個卡住其實還蠻惱人的@@

jannson commented 5 years ago

@cs8425 有空更新一个最新版本么?感觉你说得有道理,我想测试测试。

cs8425 commented 5 years ago

@jannson 我直接merge了xtaci的v1.1.1到我的master 7f8c639 你可以先試試看效果 改的有點亂 要提交PR回這邊的話還要多花點功夫...

jannson commented 5 years ago

非常感谢,一回家就开始测试了,手动 merge 了一波。直观感觉效果挺好的~~~ 具体参数还没提供。 看了代码,可惜就是无法无缝兼容旧版本!

对了,go test -race 测试不过。

cs8425 commented 5 years ago

感謝提醒 已修正 除了基於net.Pipe()TestParallel2()加上-race還是不會過 把goroutine dump出來看還是找不到原因 全部都在select狀態...

找到問題點了 好像是net.Pipe()有buffer大小的限制 兩邊同時sendLoop()寫到一半剛好buffer滿了 而對面的recvLoop()又正在傳cmdACK cmdFUL cmdEMP不能清空buffer就會卡住

cs8425 commented 5 years ago

懶的慢慢修 我從xtaci的master merge這邊重新提交一份PR好了

jannson commented 5 years ago

感謝提醒 已修正 除了基於net.Pipe()TestParallel2()加上-race還是不會過 把goroutine dump出來看還是找不到原因 全部都在select狀態...

找到問題點了 好像是net.Pipe()有buffer大小的限制 兩邊同時sendLoop()寫到一半剛好buffer滿了 而對面的recvLoop()又正在傳cmdACK cmdFUL cmdEMP不能清空buffer就會卡住

那天晚上为了这个问题我测试到半夜 1 点,现在修复这个问题了?就是 TestParallel2 测试不过,其它改了是没问题的。