Last active
July 16, 2026 09:00
-
-
Save kavirajk/ab6e7e37b34f39ee22d746e7a177cfd3 to your computer and use it in GitHub Desktop.
read.go (customer)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| dle connections: why queries after an idle gap take ~2x longer. | |
| // | |
| // The clickhouse-go pool reuses one TCP connection for sequential queries: | |
| // scenarios A and B below finish with dials=1 no matter how long the gaps | |
| // are. Latency only doubles when the PEER (a load balancer, proxy, NAT box, | |
| // or a server setting) closes connections that sit idle. Then every query | |
| // finds a dead pooled connection and pays a reconnect - TCP handshake + | |
| // protocol hello = 2 extra round trips (scenario C: dials == queries). | |
| // | |
| // The fix is in the environment, not the driver: find what closes idle | |
| // connections between client and server and raise its timeout. | |
| // | |
| // Setup: | |
| // | |
| // CREATE TABLE benchmark_test (id UInt64, value Array(Float64)) ENGINE = MergeTree ORDER BY id; | |
| // | |
| // Run: | |
| // | |
| // go mod init repro && go get github.com/ClickHouse/clickhouse-go/v2 && go run . | |
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "log" | |
| "net" | |
| "sync/atomic" | |
| "time" | |
| "github.com/ClickHouse/clickhouse-go/v2" | |
| "github.com/ClickHouse/clickhouse-go/v2/lib/driver" | |
| ) | |
| const ( | |
| addr = "127.0.0.1:9000" | |
| table = "benchmark_test" | |
| numQueries = 5 | |
| idleGap = 2 * time.Second | |
| ) | |
| // dials counts every TCP connection the driver opens. A healthy pool running | |
| // sequential queries dials exactly once, no matter how many queries run. | |
| var dials atomic.Int64 | |
| func connect(settings clickhouse.Settings) driver.Conn { | |
| conn, err := clickhouse.Open(&clickhouse.Options{ | |
| Addr: []string{addr}, | |
| Settings: settings, | |
| DialContext: func(ctx context.Context, addr string) (net.Conn, error) { | |
| dials.Add(1) | |
| var d net.Dialer | |
| return d.DialContext(ctx, "tcp", addr) | |
| }, | |
| }) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| return conn | |
| } | |
| func runReads(label string, gap time.Duration, settings clickhouse.Settings) { | |
| fmt.Println(label) | |
| conn := connect(settings) | |
| defer conn.Close() | |
| before := dials.Load() | |
| var total time.Duration | |
| for i := 1; i <= numQueries; i++ { | |
| start := time.Now() | |
| var maxID uint64 | |
| if err := conn.QueryRow(context.Background(), "SELECT max(id) FROM "+table).Scan(&maxID); err != nil { | |
| log.Fatal(err) | |
| } | |
| elapsed := time.Since(start) | |
| total += elapsed | |
| fmt.Printf(" query %d: %v\n", i, elapsed) | |
| if gap > 0 && i < numQueries { | |
| time.Sleep(gap) | |
| } | |
| } | |
| fmt.Printf(" => average %v, TCP dials %d\n\n", total/numQueries, dials.Load()-before) | |
| } | |
| func main() { | |
| runReads("A: back-to-back queries (baseline)", 0, nil) | |
| runReads("B: 2s idle gaps, healthy server (gaps alone are harmless)", idleGap, nil) | |
| // C simulates a hostile network path by asking the server to close OUR | |
| // connection after 1s idle. idle_connection_timeout is a user-level | |
| // setting, so no server restart is needed. Watch dials == queries. | |
| runReads("C: 2s idle gaps, peer kills idle connections (the problem)", | |
| idleGap, clickhouse.Settings{"idle_connection_timeout": 1}) | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
NOTE: The problem here is in case (C), every new query "established new connection" instead of using the existing one from the pool.
TCP dials 5.So every query is paying 2 RTT (Round Trip Time) for connection establishment + auth.