EV-ClickHouse
view release on metacpan or search on metacpan
- on\_trace => sub { my ($message) = @\_ }
Debug trace callback. Called with internal state-machine messages
(connect, dispatch, disconnect). Useful for diagnosing protocol issues.
- on\_failover => sub { my ($old\_host, $old\_port, $new\_host, $new\_port, $msg) = @\_ }
Multi-host only. Fires after the failover wrapper rotates to the next
host in the `hosts => [...]` list, with the old and new (host, port)
pair plus the triggering error message. Use it for metrics ("which host
am I on?") or to log host transitions. Fires before the user's `on_error`.
**Options:**
- compress => 0 | 1
Enable compression: gzip on HTTP (request and response), LZ4 with CityHash
checksums on the native protocol. Default: `0`. Native compression
requires liblz4 at build time.
- session\_id => $id
HTTP session id for stateful operations (temporary tables, SET, etc.).
Native protocol has stateful sessions intrinsically; this option is HTTP-only.
- connect\_timeout => $seconds
TCP/TLS connection timeout. `0` (default) means no timeout. Floating
point allowed.
- query\_timeout => $seconds
Default per-query timeout applied to every query and insert. The query
callback receives a `timeout` error if exceeded. Override per-call via
the `query_timeout` key in the settings hashref.
- max\_query\_size => $bytes
Client-side guard: croak before sending any query whose SQL text exceeds
this many bytes. `0` (default) disables the check. Useful as a
last-resort defense against accidentally sending unbounded strings.
- max\_recv\_buffer => $bytes
Defensive ceiling on the response. The cap applies to the raw recv
buffer (every protocol), the chunked-decoded body (HTTP), and the
gzip-decompressed body (HTTP), so the same upper bound applies to the
user-visible payload regardless of transport encoding. On overflow the
query callback receives an appropriate error ("recv buffer overflow",
"chunked response too large", or "gzip body exceeds max\_recv\_buffer")
and the connection is torn down so no subsequent query can slip past
the cap on the same socket. `0` (default) keeps the historical
no-cap behaviour (still bounded internally by a hard 128 MB ceiling
on compressed paths). Recommended in production when the schema is
constrained and you want a hard upper bound (e.g.
`128 * 1024 * 1024` for 128 MB).
- http\_basic\_auth => 0 | 1
HTTP only. When set, send credentials as
`Authorization: Basic base64(user:password)` instead of the default
`X-ClickHouse-User` / `X-ClickHouse-Key` header pair. Use this when
the connection passes through an HTTP gateway (nginx, Envoy, ...) that
strips the X-ClickHouse-\* headers but forwards Basic auth verbatim.
Default: `0`.
- auto\_reconnect => 0 | 1
Reconnect automatically on connection loss. Default: `0`. When enabled,
queued (unsent) queries are preserved across reconnects; in-flight queries
receive an error.
The reconnect path covers TCP/TLS connect failures, `connect_timeout`
or `query_timeout` expiry, and any clean server-side EOF (idle or
mid-request). Mid-query I/O errors (ECONNRESET / EPIPE) and a malformed
native ServerHello are **not** retried - they typically indicate a
misconfigured peer or client-side bug that retry would only loop on.
Combine with `reconnect_max_attempts` for an explicit ceiling.
- settings => \\%hash
ClickHouse settings applied to every query and insert. Per-call settings
(see ["query"](#query), ["insert"](#insert)) override these.
settings => { async_insert => 1, max_threads => 4 }
- keepalive => $seconds
Send a keepalive request every N seconds while the connection is idle:
a native CLIENT\_PING on the native protocol or a `GET /ping` on HTTP
(some load balancers / NATs drop idle HTTP connections after a few
seconds; TCP-level keepalive is too coarse). Default: `0` (disabled).
- reconnect\_delay => $seconds
Initial delay for the `auto_reconnect` exponential backoff. Each failed
attempt doubles the delay, capped at `reconnect_max_delay`. Default:
`0` (immediate retry, no backoff).
- reconnect\_max\_delay => $seconds
Backoff ceiling. Default: `0`, meaning no explicit cap; the implementation
still bounds the backoff exponent at 20 doublings, so with
`reconnect_delay = 0.5` the worst case is roughly 6 days. Setting an
explicit ceiling is recommended in production.
- reconnect\_jitter => $fraction
Multiplicative jitter applied to each backoff delay: the actual sleep
is uniformly random in `[delay, delay * (1 + jitter)]`. `0` (default)
disables. Set to `0.1`-`0.5` when many clients reconnect against a
shared cluster - without jitter, every replica restart causes a
synchronised reconnect storm at the same backoff intervals. Jitter is
applied _after_ `reconnect_max_delay` clamping, then re-clamped, so
the ceiling is never exceeded.
- reconnect\_max\_attempts => $N
Cap the total number of reconnect attempts before giving up. Once the
cap is reached, `on_error` fires with the message
Without [EV::cares](https://metacpan.org/pod/EV%3A%3Acares), DNS resolution falls back to blocking
`getaddrinfo`. Install [EV::cares](https://metacpan.org/pod/EV%3A%3Acares) for non-blocking lookup; otherwise
use an IP literal or a local caching resolver (nscd / systemd-resolved).
- `connect_timeout` doesn't fire
It does across TCP connect, TLS handshake, and native ServerHello. If
the timer doesn't fire, the underlying issue is usually a synchronous
DNS stall (see above) which happens before `start_connect` arms the
timer; install [EV::cares](https://metacpan.org/pod/EV%3A%3Acares) to push DNS off the loop.
- Per-query `query_timeout` is ignored
Set it inside the `\%settings` hashref, not as a top-level argument:
`$ch->query($sql, { query_timeout => 5 }, $cb)`.
- Which host am I currently pointed at after failover?
`$ch->current_host` and `$ch->current_port` reflect the
live target after a multi-host rotation. Use `on_failover =>
sub { ... }` to get notified at the moment of each rotation.
- How do I retry only on transient errors?
`EV::ClickHouse->is_retryable_error($code)` returns true for the
common transient codes (timeouts, network errors, replica catch-up,
keeper exceptions, ...). Inspect `$ch->last_error_code` from
inside your query callback and schedule a retry only when the predicate
fires - permanent errors (auth failures, missing tables) won't qualify.
Sample skeleton:
$ch->query($sql, sub {
my ($r, $err) = @_;
if ($err && EV::ClickHouse->is_retryable_error($ch->last_error_code)) {
schedule_retry($sql);
} elsif ($err) { warn "permanent: $err" }
});
- Idempotent insert silently drops some rows
`idempotent => 1` auto-mints
`insert_deduplication_token`; if your producer issues the SAME logical
batch twice (e.g. retry after a transient network blip) only the first
write lands, by design. To force two distinct logical batches through,
either pass an explicit `idempotent => $token` per batch or
omit the option for fresh inserts. See `eg/idempotent_insert.pl`.
- `on_data` vs `iterate` - which should I pick?
`on_data => sub { }` in the per-query settings is the
lowest-overhead streaming path: each native data block is delivered as
soon as the parser has it, no per-row allocation overhead beyond the
batch arrayref. `iterate` is a synchronous-feeling pull wrapper around
the same machinery - useful when the surrounding code is procedural
(ETL scripts, exporters) and a callback shape doesn't fit. Both are
native-only.
- Connection in front of nginx / reverse proxy strips X-ClickHouse-\* headers
Pass `http_basic_auth => 1` to send the credentials as
`Authorization: Basic ...` instead. Most HTTP gateways forward
Authorization verbatim while filtering proprietary headers.
# TUNING
- Native vs HTTP
Native (port 9000) is typically 2-5x faster for insert and select-of-many-rows
because rows ship as binary columns instead of TSV text. Use HTTP only when
the network path requires HTTPS-only or when you need `raw => 1` CSV /
JSONEachRow / Parquet bodies.
- `compress => 1`
Enables LZ4 (native) or gzip (HTTP). LZ4 cost is small and saves ~50-70%
on text-heavy columns. Gzip is heavier; turn on only if you're bandwidth-bound.
- `insert_streamer` batch\_size
Default 10\_000 is a good baseline. Smaller (1k-2k) reduces memory pressure
on the producer; larger (50k-100k) reduces server-side merge cost on
MergeTree. Match to your row width: ~1 MB per batch is a sweet spot.
- `keepalive`
Enable on long-lived idle connections (HTTP behind a load balancer or
NAT, or a native connection that may sit minutes between queries). 15-30s
is typical.
- `reconnect_max_attempts`
Always set in production. Default is unlimited; a permanent failure
(wrong host, wrong port, dead server) will spin `on_error` forever
otherwise.
- `progress_period`
Coalesce on\_progress packets to one fire per N seconds. Big SELECTs can
emit hundreds per second; throttle to 1-5s for monitoring dashboards.
- Pull-iterator vs `on_data`
`on_data` has lower per-block overhead. `iterate` trades that for a
synchronous-feeling API; use it when the surrounding code is procedural.
- `EV::ClickHouse::Pool`
A Pool fans concurrent queries across N independent connections, so a
slow query on one doesn't head-of-line-block the others. Use it for
read-mostly fan-out; do not use it for queries that depend on
session-level state (temporary tables, `set`) since each query may
land on a different connection.
## Performance tuning checklist
- 1. Pick the right protocol
Native (port 9000) beats HTTP (port 8123) for almost all workloads.
HTTP is only required for HTTPS-fronted ingress, the `raw` mode that
returns `RowBinary` / `JSONEachRow` / `Parquet` bodies unparsed, or
( run in 1.451 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )