新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「碼農(nóng)桃花源」,作者峰云就她了。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼農(nóng)桃花源公眾號(hào)。

前段時(shí)間(已經(jīng)是 2 年前了??)優(yōu)化了 golang udp client 和 server 的性能問(wèn)題,我在這里簡(jiǎn)單描述下 udp 服務(wù)的優(yōu)化過(guò)程。
當(dāng)然,udp 性能本就很高,就算不優(yōu)化,也輕易可以到幾十萬(wàn)的 qps,但我們想更好的優(yōu)化 go udp server 和 client。
UDP 存在粘包半包問(wèn)題?
我們知道應(yīng)用程序之間的網(wǎng)絡(luò)傳輸會(huì)存在粘包半包的問(wèn)題。該問(wèn)題的由來(lái)我這里就不描述了,大家去搜吧。使用 tcp 會(huì)存在該問(wèn)題,而 udp 是不存在該問(wèn)題的。
為啥? tcp 是無(wú)邊界的,tcp 是基于流傳輸?shù)模瑃cp 報(bào)頭沒(méi)有長(zhǎng)度這個(gè)變量,而 udp 是有邊界的,基于消息的,是可以解決粘包問(wèn)題的。udp 協(xié)議里有 16 位來(lái)描述包的大小,16 位決定他的數(shù)字最大數(shù)字是 65536,除去 udp 頭和 ip 頭的大小,最大的包差不多是 65507 byte。
但根據(jù)我們的測(cè)試,udp 并沒(méi)有完美的解決應(yīng)用層粘包半包的問(wèn)題。如果你的 go udp server 的讀緩沖是 1024,那么 client 發(fā)送的數(shù)據(jù)不能超過(guò) server read buf 定義的 1024 byte,不然還是要處理半包了。如果發(fā)送的數(shù)據(jù)小于 1024 byte,倒是不會(huì)出現(xiàn)粘包的問(wèn)題。
- // xiaorui.cc
- buf := make([]byte, 1024)
- for {
- n, _ := ServerConn.Read(buf[0:])
- if string(buf[0:n]) != s {
- panic(...)
- ...
在 Linux下 借助 strace 發(fā)現(xiàn) syscall read fd 的時(shí)候,最大只獲取 1024 個(gè)字節(jié)。這個(gè) 1024 就是上面配置的讀緩沖大小。
- // xiaorui.cc
- [pid 25939] futex(0x56db90, FUTEX_WAKE, 1) = 1
- [pid 25939] read(3, "Introduction... 隱藏... overview of IPython'", 1024) = 1024
- [pid 25939] epoll_ctl(4, EPOLL_CTL_DEL, 3, {0, {u32=0, u64=0}}) = 0
- [pid 25939] close(3
- [pid 25940] <... restart_syscall resumed> ) = 0
- [pid 25939] <... close resumed> ) = 0
- [pid 25940] clock_gettime(CLOCK_MONOTONIC, {19280781, 509925143}) = 0
- [pid 25939] pselect6(0, NULL, NULL, NULL, {0, 1000000}, 0
- [pid 25940] pselect6(0, NULL, NULL, NULL, {0, 20000}, 0) = 0 (Timeout)
- [pid 25940] clock_gettime(CLOCK_MONOTONIC, {19280781, 510266460}) = 0
- [pid 25940] futex(0x56db90, FUTEX_WAIT, 0, {60, 0}
下面是 golang 里 socket fd read 的源碼,可以看到你傳入多大的 byte 數(shù)組,他就 syscall read 多大的數(shù)據(jù)。
- // xiaorui.cc
- func read(fd int, p []byte) (n int, err error) {
- var _p0 unsafe.Pointer
- if len(p) > 0 {
- _p0 = unsafe.Pointer(&p[0])
- } else {
- _p0 = unsafe.Pointer(&_zero)
- }
- r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p)))
- n = int(r0)
- if e1 != 0 {
- err = errnoErr(e1)
- }
- return
- }
http2 為毛比 http1 的協(xié)議解析更快,是因?yàn)?http2 實(shí)現(xiàn)了 header 的 hpack 編碼協(xié)議。thrift 為啥比 grpc 快?單單對(duì)比協(xié)議結(jié)構(gòu)體來(lái)說(shuō),thrift 和 protobuf 的性能半斤八兩,但對(duì)比網(wǎng)絡(luò)應(yīng)用層協(xié)議來(lái)說(shuō),thrift 要更快。因?yàn)間rpc是在 http2 上跑的,grpc server 不僅要解析 http2 header,還要解析 http2 body,這個(gè) body 就是 protobuf 數(shù)據(jù)。
所以說(shuō),高效的應(yīng)用層協(xié)議也是高性能服務(wù)的重要的一個(gè)標(biāo)準(zhǔn)。我們先前使用的是自定義的 TLV 編碼,t 是類型,l 是 length,v 是數(shù)據(jù)。一般解決網(wǎng)絡(luò)協(xié)議上的數(shù)據(jù)完整性差不多是這個(gè)思路。當(dāng)然,我也是這么搞得。
如何優(yōu)化 udp 應(yīng)用協(xié)議上的開銷?
上面已經(jīng)說(shuō)了,udp 在合理的 size 情況下是不需要依賴應(yīng)用層協(xié)議解析包問(wèn)題。那么我們只需要在 client 端控制 send 包的大小,server 端控制接收大小,就可以節(jié)省應(yīng)用層協(xié)議帶來(lái)的性能高效。??別小看應(yīng)用層協(xié)議的 cpu 消耗!
解決 golang udp 的鎖競(jìng)爭(zhēng)問(wèn)題
在 udp 壓力測(cè)試的時(shí)候,發(fā)現(xiàn) client 和 server 都跑不滿 cpu 的情況。開始以為是 golang udp server 的問(wèn)題,去掉所有相關(guān)的業(yè)務(wù)邏輯,只是單純的做 atomic 計(jì)數(shù),還是跑不滿 cpu。通過(guò) go tool pprof 的函數(shù)調(diào)用圖以及火焰圖,看不出問(wèn)題所在。嘗試使用 iperf 進(jìn)行 udp 壓測(cè),golang udp server 的壓力直接干到了滿負(fù)載??梢哉f(shuō)是壓力源不足。
那么 udp 性能上不去的問(wèn)題看似明顯了,應(yīng)該是 golang udp client 的問(wèn)題了。我嘗試在 go udp client 里增加了多協(xié)程寫入,10 個(gè) goroutine,100 個(gè) goroutine,500 個(gè) goroutine,都沒(méi)有好的明顯的提升效果,而且性能抖動(dòng)很明顯。??
進(jìn)一步排查問(wèn)題,通過(guò) lsof 分析 client 進(jìn)程的描述符列表,client 連接 udp server 只有一個(gè)連接。也就是說(shuō),500 個(gè)協(xié)程共用一個(gè)連接。接著使用 strace 做 syscall 系統(tǒng)調(diào)用統(tǒng)計(jì),發(fā)現(xiàn) futex 和 pselect6 系統(tǒng)調(diào)用特別多,這一看就是存在過(guò)大的鎖競(jìng)爭(zhēng)。翻看 golang net 源代碼,果然發(fā)現(xiàn) golang 在往 socket fd 寫入時(shí),存在寫鎖競(jìng)爭(zhēng)。
TODO 圖片
- // xiaorui.cc
- // Write implements io.Writer.
- func (fd *FD) Write(p []byte) (int, error) {
- if err := fd.writeLock(); err != nil {
- return 0, err
- }
- defer fd.writeUnlock()
- if err := fd.pd.prepareWrite(fd.isFile); err != nil {
- return 0, err
- }
- }
怎么優(yōu)化鎖競(jìng)爭(zhēng)?
實(shí)例化多個(gè) udp 連接到一個(gè)數(shù)組池子里,在客戶端代碼里隨機(jī)使用 udp 連接。這樣就能減少鎖的競(jìng)爭(zhēng)了。
總結(jié)
udp 性能調(diào)優(yōu)的過(guò)程就是這樣子了。簡(jiǎn)單說(shuō)就兩個(gè)點(diǎn):一個(gè)是消除應(yīng)用層協(xié)議帶來(lái)的性能消耗,再一個(gè)是 golang socket 寫鎖帶來(lái)的競(jìng)爭(zhēng)。
當(dāng)我們一些性能問(wèn)題時(shí),多使用 perf、strace 功能,再配合 golang pprof 分析火焰圖來(lái)分析問(wèn)題。實(shí)在不行,直接干 golang 源碼。
文章標(biāo)題:GoUdp的高性能優(yōu)化
地址分享:http://www.dlmjj.cn/article/cojeihc.html


咨詢
建站咨詢
