You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Go语言GRPC双向流性能优化求助:同机时延需降至10-50微秒

Optimizing gRPC Bidirectional Stream Latency in Go

Hey there, let's tackle that latency issue you're seeing—local gRPC absolutely should hit sub-100µs (and even single-digit µs in ideal cases) instead of the 400-800µs you're measuring. Below are the most impactful optimizations tailored to your code and use case:


1. Switch to Unix Domain Sockets (UDS) Instead of TCP Loopback

TCP loopback adds unnecessary kernel overhead for local communication. Using Unix sockets cuts this overhead dramatically, often reducing latency by 50% or more.

Client Modification:

import "strings"

// Replace TCP dial with UDS
conn, err := grpc.Dial("unix:///tmp/grpc_highperf.sock", 
    grpc.WithInsecure(),
    grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
        return net.Dial("unix", strings.TrimPrefix(addr, "unix://"))
    }),
)

Server Modification:

// Listen on a Unix socket instead of TCP
lis, err := net.Listen("unix", "/tmp/grpc_highperf.sock")

2. Tune gRPC Connection & Stream Parameters

Your current gRPC setup uses default settings optimized for general networks—not low-latency local communication. Add these options to both client and server:

Client Dial Options:

conn, err := grpc.Dial(/* your address */,
    grpc.WithInsecure(),
    grpc.WithNoProxy(), // Skip proxy detection for local connections
    grpc.WithInitialWindowSize(1<<20), // 1MB initial window (avoids early flow control)
    grpc.WithInitialConnWindowSize(1<<20),
    grpc.WithWriteBufferSize(1<<20), // Larger buffers reduce syscall overhead
    grpc.WithReadBufferSize(1<<20),
    grpc.WithDisableRetry(), // Disable retries for local traffic (no need)
    grpc.WithBlock(), // Ensure connection is fully established before proceeding
    // For TCP (if you don't use UDS), enable TCP_NODELAY:
    grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
        conn, err := net.Dial("tcp", addr)
        if err != nil {
            return nil, err
        }
        if tcpConn, ok := conn.(*net.TCPConn); ok {
            tcpConn.SetNoDelay(true) // Disable Nagle's algorithm
        }
        return conn, nil
    }),
)

Server Creation Options:

s := grpc.NewServer(
    grpc.InitialWindowSize(1<<20),
    grpc.InitialConnWindowSize(1<<20),
    grpc.MaxConcurrentStreams(1000), // Adjust based on your workload
    grpc.WithTcpNoDelay(true), // Disable Nagle's algorithm for TCP connections
)

3. Optimize Application Code for Low Latency

Your current code has several bottlenecks that add unnecessary latency:

a. Remove Synchronous Logging from Critical Paths

The log.Printf calls in your send/receive loops are blocking I/O operations—they’re likely responsible for a huge chunk of your measured latency. Comment them out entirely during latency testing, or use an asynchronous logging library if you need to retain logs.

b. Reuse Protobuf Messages (Avoid Allocations)

Creating a new pb.Request in every loop iteration triggers frequent garbage collection (GC), which adds jitter. Reuse a single message instance instead:

// Client side: Create once outside the loop
req := &pb.Request{}
for i := 1; i <= msgCount; i++ {
    req.Num = int32(i) // Just update the field
    if err := stream.Send(req); err != nil {
        log.Fatalf("can not send %v", err)
    }
}

c. Simplify Server Loop Logic

Your server’s select statement checking ctx.Done() is redundant—srv.Recv() already respects the context and returns an error when it’s canceled. Remove it to reduce goroutine scheduling overhead:

func (s server) Max(srv pb.Math_MaxServer) error {
    log.Println("start new server")
    var max int32
    i := 0
    fromMsg := 0

    for {
        req, err := srv.Recv()
        if err == io.EOF {
            log.Println("exit")
            return nil
        }
        if err != nil {
            if err == context.Canceled {
                return nil
            }
            log.Printf("receive error %v", err)
            continue
        }
        // ... rest of your logic
    }
}

4. Runtime Environment Tuning

Set GOMAXPROCS to Match CPU Cores

Ensure the Go runtime uses all available CPU cores to avoid scheduling bottlenecks:

import "runtime"

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    // ... rest of your code
}

Use High-Performance Protobuf Libraries

The official google.golang.org/protobuf library is robust but not the fastest. For maximum speed, switch to github.com/gogo/protobuf (a faster alternative with code generation optimizations). Regenerate your proto files with gogo’s protoc plugin to get serialized/deserialized speedups.


5. Validate Latency Accurately

Make sure your latency measurement isn’t skewed by external factors:

  • Run tests on a quiet machine (no other heavy processes)
  • Measure latency using high-precision timers (time.Now().UnixNano() is okay, but consider runtime.ReadMemStats for GC tracking)
  • Average latency over thousands of messages to account for initial connection setup overhead

After applying these changes, you should see latency drop well into your target 10-50µs range. Start with UDS and logging removal—those will give you the biggest wins immediately.

内容的提问来源于stack exchange,提问作者tolgatanriverdi

火山引擎 最新活动