編寫健壯且高性能的網絡服務須要付出大量的努力。提升服務性能的方式有不少種,好比優化應用層的代碼,更進一步,還能夠看看垃圾回收器,操做系統,網絡傳輸,以及部署咱們服務的硬件是否有優化空間。node
TCP/IP協議棧中的一些算法會影響到服務性能。本文將簡單介紹其中的Nagle算法,與Nagle算法相關的socket選項TCP_NODELAY
,以及在Go語言中如何使用它。git
大部分平臺上的TCP實現都提供了socket選項,用於控制鏈接生命週期,流量控制等算法。github
其中一個會對網絡傳輸性能形成影響的算法是Nagle算法,它在Linux,macOS,Windows平臺默認都是打開的。golang
Nagle算法的作法是:將要發送的小包合併,並延緩發送。延緩後的發送策略是,收到前一個發送出去的包的ACK確認包,或者必定時間後,收集了足夠數量的小數據包。算法
Nagle算法的目的是減小發送小包的數量,從而減少帶寬,並提升網絡吞吐量,付出的代價是有時會增長服務的延時。(譯者yoko注:補充解釋一下爲何減小小包的數量能夠減少帶寬。由於每一個TCP包,除了包體中包含的應用層數據外,外層還要套上TCP包頭和IP包頭。因爲應用層要發送的業務數據量是固定的,因此包數量越多,包頭佔用的帶寬也越多)shell
引入的延時一般在毫秒級別,可是對於延遲敏感的服務來講,減小一些毫秒數的延遲也是值得的。緩存
Nagle算法所對應的TCP socket選項是TCP_NODELAY
。開啓TCP_NODELAY
能夠禁用Nagle算法。禁用Nagle算法後,數據將盡量快的被髮送出去。bash
另外,咱們也能夠在應用層對數據進行緩存合併發送來達到Nagle算法的目的(譯者yoko注:在Go語言中即便用bufio.Writer
。我的認爲,使用bufio.Writer
還有一個好處,就是減小了調用write系統調用的次數,可是相應的,增長了數據拷貝的開銷)。網絡
在Go語言中,TCP_NODELAY
默認是開啓的,而且標準庫提供了net.SetNodelay(bool)
方法來控制它。併發
咱們經過一個小實驗來觀察TCP_NODELAY
打開和關閉時底層TCP包的變化。
代碼邏輯十分簡單,client端連續調用5次conn.Write
函數向server端發送相同的字符串GOPHER
。
服務端代碼(server.go):
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func main() {
port := ":" + "8000"
// 建立監聽
l, err := net.Listen("tcp", port)
if err != nil {
log.Fatal(err)
}
defer l.Close()
for {
// 接收新的鏈接
c, err := l.Accept()
if err != nil {
log.Println(err)
return
}
// 處理新的鏈接
go handleConnection(c)
}
}
func handleConnection(c net.Conn) {
fmt.Printf("Serving %s\n", c.RemoteAddr().String())
for {
// 讀取數據
netData, err := bufio.NewReader(c).ReadString('\n')
if err != nil {
log.Println(err)
return
}
cdata := strings.TrimSpace(netData)
if cdata == "GOPHER" {
c.Write([]byte("GopherAcademy Advent 2019!"))
}
if cdata == "EXIT" {
break
}
}
c.Close()
}
複製代碼
客戶端代碼(client.go):
package main
import (
"fmt"
"log"
"net"
)
func main() {
target := "localhost:8000"
raddr, err := net.ResolveTCPAddr("tcp", target)
if err != nil {
log.Fatal(err)
}
// 和服務端創建鏈接
conn, err := net.DialTCP("tcp", nil, raddr)
if err != nil {
log.Fatal(err)
}
// conn.SetNoDelay(false) // 若是打開這行代碼,則禁用TCP_NODELAY,打開Nagle算法
fmt.Println("Sending Gophers down the pipe...")
for i := 0; i < 5; i++ {
// 發送數據
_, err = conn.Write([]byte("GOPHER\n"))
if err != nil {
log.Fatal(err)
}
}
}
複製代碼
爲了觀察TCP包,首先開啓抓包程序tcpdump。爲了簡單,兩個程序都在本機運行。我環境的內網環路網卡爲lo0
,不一樣機器上可能不一樣:
$sudo tcpdump -X -i lo0 'port 8000'
複製代碼
而後,再打開兩個終端窗口,先執行服務端程序,再執行客戶端程序:
$go run server.go
複製代碼
$go run client.go
複製代碼
觀察抓包結果,咱們會發現每次調用write函數發送"GOPHER",對應的都是一個獨立的TCP包被髮送。總共有5個TCP包。如下是抓包結果,爲了簡單,我只貼出兩個包:
....
14:03:11.057782 IP localhost.58030 > localhost.irdmi: Flags [P.], seq 15:22, ack 1, win 6379, options [nop,nop,TS val 744132314 ecr 744132314], length 7
0x0000: 4500 003b 0000 4000 4006 0000 7f00 0001 E..;..@.@.......
0x0010: 7f00 0001 e2ae 1f40 80c5 9759 6171 9822 .......@...Yaq." 0x0020: 8018 18eb fe2f 0000 0101 080a 2c5a 8eda ...../......,Z.. 0x0030: 2c5a 8eda 474f 5048 4552 0a ,Z..GOPHER. 14:03:11.057787 IP localhost.58030 > localhost.irdmi: Flags [P.], seq 22:29, ack 1, win 6379, options [nop,nop,TS val 744132314 ecr 744132314], length 7 0x0000: 4500 003b 0000 4000 4006 0000 7f00 0001 E..;..@.@....... 0x0010: 7f00 0001 e2ae 1f40 80c5 9760 6171 9822 .......@...`aq."
0x0020: 8018 18eb fe2f 0000 0101 080a 2c5a 8eda ...../......,Z..
0x0030: 2c5a 8eda 474f 5048 4552 0a ,Z..GOPHER.
...
複製代碼
若是咱們打開客戶端中被註釋掉的conn.SetNoDelay(false)
這行代碼,也即禁用掉TCP_NODELAY
,開啓Nagle算法,再次抓包,結果以下:
14:27:20.120673 IP localhost.64086 > localhost.irdmi: Flags [P.], seq 8:36, ack 1, win 6379, options [nop,nop,TS val 745574362 ecr 745574362], length 28
0x0000: 4500 0050 0000 4000 4006 0000 7f00 0001 E..P..@.@.......
0x0010: 7f00 0001 fa56 1f40 07c9 d46f a115 3444 .....V.@...o..4D
0x0020: 8018 18eb fe44 0000 0101 080a 2c70 8fda .....D......,p..
0x0030: 2c70 8fda 474f 5048 4552 0a47 4f50 4845 ,p..GOPHER.GOPHE
0x0040: 520a 474f 5048 4552 0a47 4f50 4845 520a R.GOPHER.GOPHER.
複製代碼
能夠看到,有四個"GOPHER"被合併到了一個TCP包中。
TCP_NODELAY
並非萬能的,有好處有壞處,須要根據實際業務場景決定打開仍是關閉。可是,在使用具體語言編寫網絡服務時,咱們須要知道它是否被默認開啓。
還有其餘一些相似的socket選項,好比TCP_QUICKACK
和TCP_CORK
等。可是因爲有些socket選項是平臺相關的,所以Go沒有提供和TCP_NODELAY
相同的方式來控制這些socket選項。咱們能夠經過一些平臺相關的包來實現這一點。好比說,在類unix系統下,咱們可使用golang.org/x/sys/unix
包中的SetsockoptInt
方法。
舉例:
err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_QUICKACK, 1)
if err != nil {
return os.NewSyscallError("setsockopt", err)
}
複製代碼
最後,若是你想學習更多和Nagle算法相關的知識,能夠看看這篇英文博客
英文原文連接:Control packet flow with TCP_NODELAY in Go
原文連接: pengrl.com/p/20191217/
原文出處: yoko blog (pengrl.com)
原文做者: yoko (github.com/q191201771)
版權聲明: 本文歡迎任何形式轉載,轉載時完整保留本聲明信息(包含原文連接、原文出處、原文做者、版權聲明)便可。本文後續全部修改都會第一時間在原始地址更新。
本篇文章由一文多發平臺ArtiPub自動發佈