perf(client): Optimize client performance and introduce a data frame processing worker pool

- Add runtime performance optimization configurations to main.go, including setting GOMAXPROCS, adjusting GC frequency, and memory limits.

- Implement a worker pool-based data frame processing mechanism in connector.go to improve processing capabilities under high concurrency.

- Adjust frame writer configuration to improve batch write efficiency and enable adaptive refresh strategy.

- Add callback handling support for write errors to enhance connection stability.

refactor(server): Introduce an adaptive buffer pool to optimize memory usage

- Add adaptive_buffer_pool.go to implement large and small buffer reuse, reducing memory allocation overhead.

- Apply buffer pool management for large/medium temporary buffers in proxy handlers and TCP connections.

- Change the HTTP response writer to a cached bufio.Writer to improve I/O performance.

- Optimize HTTP request reading logic and response sending process.

build(docker): Update mount paths and remove unused named volumes

- Modify the data directory mount method in docker-compose.release.yml. ./data:/app/data

- Remove the unnecessary drip-data named volume definition

test(script): Add performance testing and profiling scripts

- Add profile-test.sh script for automating stress testing and performance data collection

- Supports collecting pprof data such as CPU, stack traces, and coroutines and generating analysis reports
This commit is contained in:
Gouryella
2025-12-08 12:24:42 +08:00
parent 9e8b3b001d
commit 7283180e6a
11 changed files with 724 additions and 90 deletions

View File

@@ -43,6 +43,10 @@ type Connector struct {
handlerWg sync.WaitGroup // Tracks active data frame handlers
closed bool
closedMu sync.RWMutex
// Worker pool for handling data frames
dataFrameQueue chan *protocol.Frame
workerCount int
}
// ConnectorConfig holds connector configuration
@@ -71,16 +75,21 @@ func NewConnector(cfg *ConnectorConfig, logger *zap.Logger) *Connector {
localHost = "127.0.0.1"
}
numCPU := pool.NumCPU()
workerCount := max(numCPU+numCPU/2, 4)
return &Connector{
serverAddr: cfg.ServerAddr,
tlsConfig: tlsConfig,
token: cfg.Token,
tunnelType: cfg.TunnelType,
localHost: localHost,
localPort: cfg.LocalPort,
subdomain: cfg.Subdomain,
logger: logger,
stopCh: make(chan struct{}),
serverAddr: cfg.ServerAddr,
tlsConfig: tlsConfig,
token: cfg.Token,
tunnelType: cfg.TunnelType,
localHost: localHost,
localPort: cfg.LocalPort,
subdomain: cfg.Subdomain,
logger: logger,
stopCh: make(chan struct{}),
dataFrameQueue: make(chan *protocol.Frame, workerCount*100),
workerCount: workerCount,
}
}
@@ -135,6 +144,11 @@ func (c *Connector) Connect() error {
c.frameWriter.EnableHeartbeat(constants.HeartbeatInterval, c.createHeartbeatFrame)
for i := 0; i < c.workerCount; i++ {
c.handlerWg.Add(1)
go c.dataFrameWorker(i)
}
go c.frameHandler.WarmupConnectionPool(3)
go c.handleFrames()
@@ -200,6 +214,29 @@ func (c *Connector) register() error {
return nil
}
func (c *Connector) dataFrameWorker(workerID int) {
defer c.handlerWg.Done()
for {
select {
case frame, ok := <-c.dataFrameQueue:
if !ok {
return
}
if err := c.frameHandler.HandleDataFrame(frame); err != nil {
c.logger.Error("Failed to handle data frame",
zap.Int("worker_id", workerID),
zap.Error(err))
}
frame.Release()
case <-c.stopCh:
return
}
}
}
// handleFrames handles incoming frames from server
func (c *Connector) handleFrames() {
defer c.Close()
@@ -246,14 +283,15 @@ func (c *Connector) handleFrames() {
frame.Release()
case protocol.FrameTypeData:
c.handlerWg.Add(1)
go func(f *protocol.Frame) {
defer c.handlerWg.Done()
defer f.Release()
if err := c.frameHandler.HandleDataFrame(f); err != nil {
c.logger.Error("Failed to handle data frame", zap.Error(err))
}
}(frame)
select {
case c.dataFrameQueue <- frame:
case <-c.stopCh:
frame.Release()
return
default:
c.logger.Warn("Data frame queue full, dropping frame")
frame.Release()
}
case protocol.FrameTypeClose:
frame.Release()
@@ -280,7 +318,6 @@ func (c *Connector) handleFrames() {
}
}
// createHeartbeatFrame creates a heartbeat frame to be sent by the write loop.
func (c *Connector) createHeartbeatFrame() *protocol.Frame {
c.closedMu.RLock()
if c.closed {
@@ -293,7 +330,6 @@ func (c *Connector) createHeartbeatFrame() *protocol.Frame {
c.heartbeatSentAt = time.Now()
c.heartbeatMu.Unlock()
c.logger.Debug("Heartbeat sent")
return protocol.NewFrame(protocol.FrameTypeHeartbeat, nil)
}
@@ -306,7 +342,6 @@ func (c *Connector) SendFrame(frame *protocol.Frame) error {
return c.frameWriter.WriteFrame(frame)
}
// Close closes the connection
func (c *Connector) Close() error {
c.once.Do(func() {
c.closedMu.Lock()
@@ -314,9 +349,8 @@ func (c *Connector) Close() error {
c.closedMu.Unlock()
close(c.stopCh)
close(c.dataFrameQueue)
// Wait for active handlers with timeout
c.logger.Debug("Waiting for active handlers to complete")
done := make(chan struct{})
go func() {
c.handlerWg.Wait()
@@ -325,7 +359,6 @@ func (c *Connector) Close() error {
select {
case <-done:
c.logger.Debug("All handlers completed")
case <-time.After(3 * time.Second):
c.logger.Warn("Force closing: some handlers are still active")
}

View File

@@ -89,19 +89,21 @@ func NewFrameHandler(conn net.Conn, frameWriter *protocol.FrameWriter, localHost
httpClient: &http.Client{
// No overall timeout - streaming responses can take arbitrary time
Transport: &http.Transport{
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 500,
MaxConnsPerHost: 0,
IdleConnTimeout: 180 * time.Second,
DisableCompression: true,
DisableKeepAlives: false,
TLSHandshakeTimeout: 10 * time.Second,
MaxIdleConns: 2000, // Increased from 1000 for better connection reuse
MaxIdleConnsPerHost: 1000, // Increased from 500 for high concurrency
MaxConnsPerHost: 0, // Unlimited connections per host
IdleConnTimeout: 180 * time.Second, // Keep connections alive for reuse
DisableCompression: true, // Disable compression for better CPU efficiency
DisableKeepAlives: false, // Enable keep-alive for connection reuse
TLSHandshakeTimeout: 5 * time.Second, // Reduced from 10s for faster failure detection
TLSClientConfig: tlsConfig,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 15 * time.Second, // Reduced from 30s for faster timeout
ExpectContinueTimeout: 500 * time.Millisecond, // Reduced from 1s for better responsiveness
WriteBufferSize: 32 * 1024, // 32KB write buffer
ReadBufferSize: 32 * 1024, // 32KB read buffer
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
Timeout: 3 * time.Second, // Reduced from 5s for faster connection attempts
KeepAlive: 30 * time.Second, // Keep TCP keepalive
}).DialContext,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -516,7 +518,12 @@ func (h *FrameHandler) adaptiveHTTPResponse(streamID, requestID string, resp *ht
break
}
if readErr != nil {
if errors.Is(readErr, context.Canceled) || errors.Is(readErr, context.DeadlineExceeded) || errors.Is(readErr, http.ErrBodyReadAfterClose) || errors.Is(readErr, net.ErrClosed) {
// Check for expected errors that indicate connection/body closure
if errors.Is(readErr, context.Canceled) ||
errors.Is(readErr, context.DeadlineExceeded) ||
errors.Is(readErr, http.ErrBodyReadAfterClose) ||
errors.Is(readErr, net.ErrClosed) ||
strings.Contains(readErr.Error(), "read on closed response body") {
return nil
}
return fmt.Errorf("read response body: %w", readErr)
@@ -652,7 +659,12 @@ func (h *FrameHandler) streamHTTPResponse(streamID, requestID string, resp *http
break
}
if readErr != nil {
if errors.Is(readErr, context.Canceled) || errors.Is(readErr, context.DeadlineExceeded) || errors.Is(readErr, http.ErrBodyReadAfterClose) || errors.Is(readErr, net.ErrClosed) {
// Check for expected errors that indicate connection/body closure
if errors.Is(readErr, context.Canceled) ||
errors.Is(readErr, context.DeadlineExceeded) ||
errors.Is(readErr, http.ErrBodyReadAfterClose) ||
errors.Is(readErr, net.ErrClosed) ||
strings.Contains(readErr.Error(), "read on closed response body") {
return nil
}
return fmt.Errorf("read response body: %w", readErr)