如何在Golang TCP服务器中减少CPU使用率?

3
我尝试实现一个 Golang TCP 服务器,发现并发性对我来说已经足够了,但 CPU 使用率太高(在一台24核 Linux 机器上并发量为15W+ / s,但 CPU 使用率约为800%)。同时,具有类似并发性的 C++ TCP 服务器(使用 libevent)仅使用约200%的 CPU。

以下代码是 Golang 的演示:

func main() {
    listen, err := net.Listen("tcp", "0.0.0.0:17379")
    if err != nil {
        fmt.Errorf(err.Error())
    }
    go acceptClient(listen)
    var channel2 = make(chan bool)
    <-channel2
}

func acceptClient(listen net.Listener) {
    for {
        sock, err := listen.Accept()
        if err != nil {
            fmt.Errorf(err.Error())
        }
        tcp := sock.(*net.TCPConn)
        tcp.SetNoDelay(true)
        var channel = make(chan bool, 10)
        go read(channel, sock.(*net.TCPConn))
        go write(channel, sock.(*net.TCPConn))
    }
}

func read(channel chan bool, sock *net.TCPConn) {
    count := 0
    for {
        var buf = make([]byte, 1024)
        n, err := sock.Read(buf)
        if err != nil {
            close(channel)
            sock.CloseRead()
            return
        }
        count += n
        x := count / 58
        count = count % 58
        for i := 0; i < x; i++ {
            channel <- true
        }
   }
}

func write(channel chan bool, sock *net.TCPConn) {
    buf := []byte("+OK\r\n")
    defer func() {
        sock.CloseWrite()
        recover()
    }()
    for {
        _, ok := <-channel
        if !ok {
            return
        }
        _, writeError := sock.Write(buf)
        if writeError != nil {
            return
        }
    }
}

我通过多个客户端使用redis-benchmark测试了这个TCP服务器:

redis-benchmark -h 10.100.45.2  -p 17379 -n 1000 -q script load "redis.call('set','aaa','aaa')"

我也通过pprof分析了我的golang代码,结果显示CPU在系统调用上花费了大量时间: 查看图片描述


1
这个问题可能更适合在代码审查中讨论。一些想法:您使用的通道比需要的多得多,并且您错过了对套接字类型断言的检查。 - Passer By
我对此无能为力(我只能建议添加一个睡眠...),但很感兴趣。您介意在ML https://groups.google.com/forum/#!forum/golang-nuts上发布吗? - user4466350
大多数时间在系统调用io上花费的时间是你所期望的。你正在产生很多垃圾,也许是GC占用了额外的时间。请注意,在Go程序中,总CPU使用率预计会更高,因为Go程序可能只是做更多的事情,但你应该能够获得相当接近的吞吐量。 - JimB
Go语言工具链包含强大的分析器。 - Adrian
@PasserBy 是的,我为每个套接字连接创建了一个通道,这样做是为了提高套接字连接的并发性。这个演示仅用于测试golang套接字性能,在这种情况下,我没有进行套接字类型断言。你认为太多的通道会导致高CPU使用率吗? - E.SHEN
显示剩余7条评论
2个回答

1
我认为在这种情况下,使用通道并行化读写操作不会提供更好的性能。您应该尝试减少内存分配和系统调用(写入函数可能会执行大量系统调用)。
您可以尝试这个版本吗?
package main

import (
    "bytes"
    "fmt"
    "net"
)

func main() {
    listen, err := net.Listen("tcp", "0.0.0.0:17379")
    if err != nil {
        fmt.Errorf(err.Error())
    }
    acceptClient(listen)
}

func acceptClient(listen net.Listener) {
    for {
        sock, err := listen.Accept()
        if err != nil {
            fmt.Errorf(err.Error())
        }
        tcp := sock.(*net.TCPConn)
        tcp.SetNoDelay(true)
        go handleConn(tcp) // less go routine creation but no concurrent read/write on the same conn
    }
}

var respPattern = []byte("+OK\r\n")

// just one goroutine per conn
func handleConn(sock *net.TCPConn) {
    count := 0
    buf := make([]byte, 4098) // Do not create a new buffer each time & increase the buff size
    defer sock.Close()

    for {
        n, err := sock.Read(buf)
        if err != nil {
            return
        }
        count += n
        x := count / 58
        count = count % 58
        resp := bytes.Repeat(respPattern, x) // can be optimize
        _, writeError := sock.Write(resp) // do less syscall
        if writeError != nil {
            return
        }
    }
}

0
也许在主购买循环中添加一个睡眠... time.Sleep(10 * time.Millisecond)
func acceptClient(listen net.Listener) {
    for {
        sock, err := listen.Accept()
        if err != nil {
            fmt.Errorf(err.Error())
        }
        tcp := sock.(*net.TCPConn)
        tcp.SetNoDelay(true)
        var channel = make(chan bool, 10)
        go read(channel, sock.(*net.TCPConn))
        go write(channel, sock.(*net.TCPConn))
        time.Sleep(10 * time.Millisecond)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接