Loading…
Designing the new async-native ClickHouse Python client
Joe Spadola
- Source
- Clickhouse
- Published
- Added to Yomu
Summary
The official ClickHouse Python client, clickhouse-connect, originally addressed asynchronous execution by wrapping its synchronous HTTP client in a thread pool executor. However, this executor model experienced thread exhaustion, GIL contention, and severe tail latency variance under heavy concurrent workloads. To resolve these scalability bottlenecks, the developers redesigned the client around a half-sync/half-async pattern utilizing aiohttp for async network I/O. A custom bounded queue bridges streaming socket reads on the event loop with synchronous, CPU-intensive binary parsing running inside background threads. Benchmarks demonstrate that this architecture stabilizes tail latencies, delivering an average P95 latency of 556 milliseconds compared to 869 milliseconds for the legacy executor client.
Context
Wrapping the synchronous clickhouse-connect client in a ThreadPoolExecutor created performance bottlenecks under high concurrency, including thread pool exhaustion, GIL contention, stack memory overhead, and erratic tail latencies. Furthermore, while async users represented 13% of all users, they accounted for 24% of all queries, necessitating a dedicated architecture for high-volume workloads.
Approach / What changed
The team implemented a half-sync/half-async architecture using aiohttp for async network I/O while keeping CPU-bound binary format parsing synchronous. An AsyncSyncQueue bounded at 10 chunks (roughly 10MB of data) acts as a bridge with backpressure, where an async producer reads socket chunks on the event loop and a sync consumer decompresses and parses data in a thread pool executor.
Takeaways
- Async mode accounted for 24% of all clickhouse-connect queries despite representing only 13% of users, indicating heavy adoption by high-volume workloads.
- Bridging an async aiohttp producer with a sync parser via a 10-chunk bounded queue prevents event loop blocking while capping memory usage at around 10MB.
- The async-native architecture reduced average P95 latency to 556ms compared to 869ms for the legacy executor client while significantly eliminating tail latency variance.