[譯] Go語言使用TCP_NODELAY控制發包流量

編寫健壯且高性能的網絡服務須要付出大量的努力。提升服務性能的方式有不少種,好比優化應用層的代碼,更進一步,還能夠看看垃圾回收器,操做系統,網絡傳輸,以及部署咱們服務的硬件是否有優化空間。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_QUICKACKTCP_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自動發佈

相關文章
相關標籤/搜索