使用Go TCP客户端服务器实现高吞吐量

时间:2018-08-11 10:52:48

标签: linux go tcp

我将开发一个简单的TCP客户端和服务器,并希望实现高吞吐量(300000请求/秒),而使用服务器硬件上的Cpp或C TCP客户端和服务器很容易达到这一目标。我的意思是一台具有48核和64G内存的服务器。

在我的测试平台上,客户端和服务器都具有10G网络接口卡,并且我在服务器端具有接收侧扩展功能,并在客户端启用了发送数据包导向功能。

我将客户端配置为每秒发送1万个请求。我只是从bash脚本中运行Go go run client.go的多个实例以增加吞吐量。但是,通过这种方式,Go将在操作系统上创建许多线程,而大量线程会导致较高的上下文切换成本,而我无法实现这种吞吐量。我怀疑从命令行运行的Go实例的数量。下面的代码是该方法中客户端的代码片段:

func Main(cmd_rate_int int, cmd_port string) {

   //runtime.GOMAXPROCS(2) // set maximum number of processes to be used by this applications

   //var rate float64 = float64(rate_int)

   rate := float64(cmd_rate_int)

   port = cmd_port

   conn, err := net.Dial("tcp", port)
   if err != nil {
       fmt.Println("ERROR", err)
       os.Exit(1)
   }

   var my_random_number float64 = nextTime(rate) * 1000000
   var my_random_int int = int(my_random_number)
   var int_message int64 = time.Now().UnixNano()
   byte_message := make([]byte, 8)

   go func(conn net.Conn) {
       buf := make([]byte, 8)

       for true {
           _, err = io.ReadFull(conn, buf)
           now := time.Now().UnixNano()

           if err != nil {
               return
           }

           last := int64(binary.LittleEndian.Uint64(buf))
           fmt.Println((now - last) / 1000)
       }
       return

   }(conn)

   for true {
       my_random_number = nextTime(rate) * 1000000
       my_random_int = int(my_random_number)
       time.Sleep(time.Microsecond * time.Duration(my_random_int))
       int_message = time.Now().UnixNano()
       binary.LittleEndian.PutUint64(byte_message, uint64(int_message))
       conn.Write(byte_message)
   }
}

因此,我尝试通过在go client()中调用main来运行我的所有Go线程,因此我不会在Linux命令行中运行多个实例。我认为这可能是一个更好的主意。从根本上说,这确实是一个更好的主意,并且在操作系统中线程数量不会增加到700个左右。但是吞吐量仍然很低,并且似乎没有利用基础硬件的所有功能。实际上,您可能想看看我在第二种方法中运行的代码:

func main() {

   //runtime.GOMAXPROCS(2) // set maximum number of processes to be used by this applications
   args := os.Args[1:]
   rate_int, _ := strconv.Atoi(args[0])
   client_size, _ := strconv.Atoi(args[1])
   port := args[2]

   i := 0
   for i <= client_size {
       go client.Main(rate_int, port)
       i = i + 1
   }

   for true {

   }
}

我想知道达到高吞吐量的最佳实践是什么?我一直听说Go是轻量级的,高性能的,可以与C / Cpp pthread相提并论。但是,我认为在性能方面,C / Cpp仍然远远优于Go。在这个问题上我可能做错了什么,所以如果有人可以帮助实现Go的高吞吐量,我会很高兴。

2 个答案:

答案 0 :(得分:1)

修改,这是一个非常糟糕的答案。检查mh-cbon注释的原因。


我不完全了解您的尝试方式,但是如果我想控制Go的速率,通常会做2个嵌套的for循环:

for ;; time.Sleep(time.Second) {
  go func (){
    for i:=0; i<rate; i++ {
      go func (){
        // Do whatever
      }()
    }
  }()
}

我在每个循环中启动一个goroutine来:

  • 在外循环上,以确保每次迭代之间只有1秒
  • 在内部循环上,以确保我可以启动我想要的所有请求

将其放在像您这样的问题上,看起来就像:

package main

import (
        "net"
        "os"
        "time"
)

const (
        rate    = 100000
        address = "localhost:8090"
)

func main() {
        conn, err := net.Dial("tcp", address)
        if err != nil {
                os.Stderr.Write([]byte(err.Error() + "\n"))
                os.Exit(1)
        }

        for ; err == nil; time.Sleep(time.Second) {
                go func() {
                        for i := 0; i < rate; i++ {
                                go func(conn net.Conn) {
                                        if _, err := conn.Write([]byte("01234567")); err != nil {
                                                os.Stderr.Write([]byte("\nConnection closed: " + err.Error() + "\n"))
                                        }
                                }(conn)
                        }
                }()
        }
}

要验证这是否确实在发送目标请求速率,可以使用如下所示的测试TCP侦听器:

package main

import (
        "fmt"
        "net"
        "os"
        "time"
)

const (
        address = ":8090"
        payloadSize = 8
)
func main() {
        count := 0
        b := make([]byte, payloadSize)
        l, err := net.Listen("tcp", address)
        if err != nil {
                fmt.Fprintf(os.Stdout, "\nCan't listen to address %v: %v\n", address, err)
                return
        }


   defer l.Close()
    go func() {
            for ; ; time.Sleep(time.Second) {
                    fmt.Fprintf(os.Stdout, "\rRate: %v/s       ", count)
                    count = 0
            }
    }()
    for {
            conn, err := l.Accept()
            if err != nil {
                    fmt.Fprintf(os.Stderr, "\nFailed to accept connection: %v\n", err)
            }
            for {
                    _, err := conn.Read(b)
                    if err != nil {
                            fmt.Fprintf(os.Stderr, "\nConnection closed: %v\n", err)
                            break
                    }
                    count = count + 1
            }
    }

}

我发现了一些问题,因为无法同时写入错误inconsistent fdMutex到连接中。这是由于fdMutex不支持超过0xfffff个并发写入。为缓解此问题,请确保您没有超过并发写入的次数。在我的系统中,速度为> 100k / s。这不是您期望的300k / s,但是我的系统没有为此做好准备。

答案 1 :(得分:1)

这是操作码的快速重做。 由于原始源代码正在运行,因此它没有提供解决方案,但是它说明了存储桶令牌的用法以及其他一些小技巧。

它确实将类似的默认值用作op源代码。

它说明您不需要两个文件/程序即可提供客户端和服务器。

它演示了标志包的用法。

它显示了如何使用时间正确解析unix nano时间戳。Unix(x,y)

它显示了如何利用io.Copy来在同一网络上写所读内容。而不是手工编写。

仍然,这不适用于生产交付。

package main

import (
    "encoding/binary"
    "flag"
    "fmt"
    "io"
    "log"
    "math"
    "math/rand"
    "net"
    "os"
    "sync/atomic"
    "time"

    "github.com/juju/ratelimit"
)

var total_rcv int64

func main() {

    var cmd_rate_int float64
    var cmd_port string
    var client_size int

    flag.Float64Var(&cmd_rate_int, "rate", 400000, "change rate of message reading")
    flag.StringVar(&cmd_port, "port", ":9090", "port to listen")
    flag.IntVar(&client_size, "size", 20, "number of clients")

    flag.Parse()

    t := flag.Arg(0)

    if t == "server" {
        server(cmd_port)

    } else if t == "client" {
        for i := 0; i < client_size; i++ {
            go client(cmd_rate_int, cmd_port)
        }
        // <-make(chan bool) // infinite wait.
        <-time.After(time.Second * 2)
        fmt.Println("total exchanged", total_rcv)

    } else if t == "client_ratelimit" {
        bucket := ratelimit.NewBucketWithQuantum(time.Second, int64(cmd_rate_int), int64(cmd_rate_int))
        for i := 0; i < client_size; i++ {
            go clientRateLimite(bucket, cmd_port)
        }
        // <-make(chan bool) // infinite wait.
        <-time.After(time.Second * 3)
        fmt.Println("total exchanged", total_rcv)
    }
}

func server(cmd_port string) {
    ln, err := net.Listen("tcp", cmd_port)
    if err != nil {
        panic(err)
    }

    for {
        conn, err := ln.Accept()
        if err != nil {
            panic(err)
        }
        go io.Copy(conn, conn)
    }
}

func client(cmd_rate_int float64, cmd_port string) {

    conn, err := net.Dial("tcp", cmd_port)
    if err != nil {
        log.Println("ERROR", err)
        os.Exit(1)
    }
    defer conn.Close()

    go func(conn net.Conn) {
        buf := make([]byte, 8)
        for {
            _, err := io.ReadFull(conn, buf)
            if err != nil {
                break
            }
            // int_message := int64(binary.LittleEndian.Uint64(buf))
            // t2 := time.Unix(0, int_message)
            // fmt.Println("ROUDNTRIP", time.Now().Sub(t2))
            atomic.AddInt64(&total_rcv, 1)
        }
        return
    }(conn)

    byte_message := make([]byte, 8)
    for {
        wait := time.Microsecond * time.Duration(nextTime(cmd_rate_int))
        if wait > 0 {
            time.Sleep(wait)
            fmt.Println("WAIT", wait)
        }
        int_message := time.Now().UnixNano()
        binary.LittleEndian.PutUint64(byte_message, uint64(int_message))
        _, err := conn.Write(byte_message)
        if err != nil {
            log.Println("ERROR", err)
            return
        }
    }
}

func clientRateLimite(bucket *ratelimit.Bucket, cmd_port string) {

    conn, err := net.Dial("tcp", cmd_port)
    if err != nil {
        log.Println("ERROR", err)
        os.Exit(1)
    }
    defer conn.Close()

    go func(conn net.Conn) {
        buf := make([]byte, 8)
        for {
            _, err := io.ReadFull(conn, buf)
            if err != nil {
                break
            }
            // int_message := int64(binary.LittleEndian.Uint64(buf))
            // t2 := time.Unix(0, int_message)
            // fmt.Println("ROUDNTRIP", time.Now().Sub(t2))
            atomic.AddInt64(&total_rcv, 1)
        }
        return
    }(conn)

    byte_message := make([]byte, 8)
    for {
        bucket.Wait(1)
        int_message := time.Now().UnixNano()
        binary.LittleEndian.PutUint64(byte_message, uint64(int_message))
        _, err := conn.Write(byte_message)
        if err != nil {
            log.Println("ERROR", err)
            return
        }
    }
}

func nextTime(rate float64) float64 {
    return -1 * math.Log(1.0-rand.Float64()) / rate
}