Golang 中的TCP_NODELAY控制数据包及流量

编写 Web 服务需要大量的努力和思考,才能使它们变得健壮和高性能。为了提高我们服务的性能,有广泛的领域需要研究。我们可以从改进代码本身开始,如果我们进入优化的兔子洞,我们也可能开始研究垃圾回收器,操作系统,网络级别以及托管我们服务的硬件。

这篇博客文章将触及一些网络算法的表面,当我们试图提高Web服务的性能时,这些算法可能会派上用场。我们将介绍套接字选项,看看如何在没有任何外部软件包的情况下在Go中控制它。TCP_NODELAY

一些理论

大多数平台上的TCP实现都提供算法和套接字选项来决定数据包流,连接寿命等等。影响网络性能并在Linux,macOS和Windows上默认启用的算法是Nagle的算法。Nagle的算法合并小数据包并延迟其传递,直到从先前发送的数据包返回ACK或在一定时间后累积足够数量的小数据包。此过程通常需要毫秒,但是,对于延迟敏感型服务或严格的延迟服务级别目标 (SLO),减少几毫秒可能是值得的。

此处非常有用的跨平台 TCP 套接字选项是 。启用后,它实际上会禁用Nagle的算法。它不是合并小包,而是尽快将它们发送到管道。一般来说,Nagle算法的目标是减少发送的数据包数量,以节省带宽和增加吞吐量,有时还需要权衡增加服务延迟。另一方面,可能会降低小型写入的吞吐量,但有一些方法可以通过在应用程序端使用缓冲区来缓解这种情况。TCP_NODELAYTCP_NODELAY

在 Go 中,默认情况下处于启用状态,但标准库提供了通过网络禁用行为的功能。SetNoDelay 方法。TCP_NODELAY

一个小实验

为了观察数据包级别发生的情况,并查看数据包到达的差异,我们将使用一个用Go编写的小型TCP客户端/服务器。通常,我们在不同地区都有相互连接的服务,但为了实验,我们将在本地机器上进行实验。完整的源代码也可以在Github上找到。

服务器代码 (server.go):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "strings"
)

func main() {
    port := ":" + "8000"

    // Create a listening socket.
    l, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatal(err)
    }
    defer l.Close()

    for {
        // Accept new connections.
        c, err := l.Accept()
        if err != nil {
            log.Println(err)
            return
        }

        // Process newly accepted connection.
        go handleConnection(c)
    }
}
func handleConnection(c net.Conn) {
    fmt.Printf("Serving %s\n", c.RemoteAddr().String())

    for {
        // Read what has been sent from the client.
        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()
}

The client code (client.go):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    target := "localhost:8000"

    raddr, err := net.ResolveTCPAddr("tcp", target)
    if err != nil {
        log.Fatal(err)
    }

    // Establish a connection with the server.
    conn, err := net.DialTCP("tcp", nil, raddr)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Sending Gophers down the pipe...")

    for i := 0; i < 5; i++ {
        // Send the word "GOPHER" to the open connection.
        _, err = conn.Write([]byte("GOPHER\n"))
        if err != nil {
            log.Fatal(err)
        }
    }
}

To observe the behavior change, first execute . You might have to change the network interface to match your own machine:tcpdump

1
sudo tcpdump -X  -i lo0 'port 8000'

Then, execute the server (server.go) and the client (client.go).

1
go run server.go

In another terminal window execute:

1
go run client.go

Initially, if we look closer at the payload, we’ll notice that each write () of the word “GOPHER” is transmitted as a separate packet. Five in total. For brevity, I just posted only a couple of packets.Write()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
....
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..;[email protected]@.......
        0x0010:  7f00 0001 e2ae 1f40 80c5 9759 6171 9822  [email protected]"
        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..;[email protected]@.......
        0x0010:  7f00 0001 e2ae 1f40 80c5 9760 6171 9822  [email protected]`aq."
        0x0020:  8018 18eb fe2f 0000 0101 080a 2c5a 8eda  ...../......,Z..
        0x0030:  2c5a 8eda 474f 5048 4552 0a              ,Z..GOPHER.

...

If we disable via the method now, the code of the client looks like the following:TCP_NODELAYSetNoDelay

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    target := "localhost:8000"

    raddr, err := net.ResolveTCPAddr("tcp", target)
    if err != nil {
        log.Fatal(err)
    }

    // Establish a connection with the server.
    conn, err := net.DialTCP("tcp", nil, raddr)
    if err != nil {
        log.Fatal(err)
    }

    conn.SetNoDelay(false) // Disable TCP_NODELAY; Nagle's Algorithm takes action.

    fmt.Println("Sending Gophers down the pipe...")

    for i := 1; i <= 5; i++ {
        // Send the word "GOPHER" to the open connection.
        _, err = conn.Write([]byte("GOPHER\n"))
        if err != nil {
            log.Fatal(err)
        }
    }
}

在禁用的情况下再次运行客户端(),Nagle的算法正在采取行动,我们得到以下结果:go run client.goTCP_NODELAY

1
2
3
4
5
6
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  [email protected]@.......
        0x0010:  7f00 0001 fa56 1f40 07c9 d46f a115 3444  [email protected]
        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"

结论

总而言之,这不是灵丹妙药,在决定禁用它或保持启用之前需要进行实验。但是,知道它是否默认在我们最喜欢的编程语言中启用总是很好的。在启用 Nagle 算法的情况下,服务可能表现得更好()。该选项可用于发送方和接收方。没有限制。在我们的示例中,我们在客户端进行了试验。这完全取决于工作负载以及我们对客户端和服务器的访问权限。TCP_NODELAYSetNoDelay(false)TCP_NODELAY

还有其他一套接字选项,例如 和 要试验。其中一些可能是特定于平台的。因此,Go 没有提供一种控制这些选项的方法,但方法与 相同。但是,我们可以通过特定于平台的软件包来做到这一点。例如,要在 *nix 系统中启用套接字选项,我们可以使用 golang.org/x/sys/unix 包和 SetsockoptInt 方法。TCP_QUICKACKTCP_CORKTCP_NODELAY

例:

1
2
3
4
err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_QUICKACK, 1)
if err != nil {
  return os.NewSyscallError("setsockopt", err)
}

如果你想了解Nagle的算法,TCP_NODELAY和类似的算法,我强烈建议阅读这篇博客文章


转自:使用 Go | 中的TCP_NODELAY控制数据包流歌斐学院博客





Comments

Popular posts from this blog

Python Receiving and parse JSON Data via UDP protocol

ubus lua client method and event registration code demo/example