[client-v2] httpcomponents.httpclient.connect.time does not include the TCP connect or the TLS handshake
維護者通常 1 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 56/100
- Issue 類型
- 缺陷
- 描述清晰度
- 描述清楚
- 活躍度
- 活躍
- 技術堆疊
- apache, java
- 領域
- backend, observability
研究方向
Read client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java around MeteredManagedHttpClientConnectionFactory.createConnection, then trace the connection manager's connect() path and the suggested operator or manager hooks. Check MetricsTest.testConnectionTime in client-v2/src/test/java/com/clickhouse/client/metrics/MetricsTest.java. Done means new connections record DNS, TCP and TLS setup time, reused connections add no sample, and the test verifies the behavior.
由索引模型根據 Issue 內容生成。
描述
Description
The Micrometer gauge httpcomponents.httpclient.connect.time is documented as "Running average connection establishment time" (client.mdx#L1081). It does not measure connection establishment. It measures only the construction of the Java connection object. The DNS lookup, the TCP connect and the TLS handshake occur after the measured call. For a new connection the gauge shows 0-2 ms, also when the connect takes hundreds of milliseconds.
MeteredManagedHttpClientConnectionFactory.createConnection(Socket) times super.createConnection(socket). Apache HttpClient 5 calls this method with socket == null. It does this when it leases a pool entry that has no connection (PoolingHttpClientConnectionManager.java:443 in httpclient5 5.6.4), or at the start of connect() (:535). After that, connectionOperator.connect(...) (:547) resolves the host, opens the socket and does the TLS upgrade (DefaultHttpClientConnectionOperator.connect). The factory does not wrap that call. httpclient5 5.2.1 was the version when the metric was added (605d50687). It has the same call order, so the metric has never included the connect. This is not a regression.
Related facts:
- The only test of the value,
MetricsTest.testConnectionTime, is disabled with the comment "Disabled because we can't assume the time is greater than 0" (MetricsTest.java#L124-L126). - The factory records whole milliseconds (
System.currentTimeMillis()), andgetTime()returns an integer-millisecond average. Thus a sub-millisecond value shows as0.
The same factory has a different defect in #3253: that issue is about how long the recorded values are kept, and this issue is about what the value measures.
Found by automated analysis of the client while working on #3253. Verified against a live server, not by inspection only.
Steps to reproduce
- Put a local TLS proxy in front of ClickHouse. The proxy waits 300 ms before it does the TLS handshake, so each new connection needs 300 ms or more to become usable.
- Build a
ClientwithregisterClientMetrics(new SimpleMeterRegistry(), "repro"), endpointhttps://127.0.0.1:<proxy port>, andsetConnectionTTL(1, ChronoUnit.MILLIS), so that each request opens a new connection. - Run
SELECT 1several times. Read the gauge after each request. Compare with a run that reuses one connection (no TTL). - Optional: set a jdb breakpoint on
HttpAPIClientHelper$MeteredManagedHttpClientConnectionFactory.createConnection(java.net.Socket). Then examinesocketand the stack.
Error Log or Exception StackTrace
No exception. In the output below, proxy accepts is the number of TCP connections that the proxy accepted during the request. factory recorded is the content of the factory queue before the gauge read.
=== TLS, 300 ms handshake delay, connection_ttl = 1 ms (new connection per request)
req 1: request wall time=864 ms, proxy accepts=1, factory recorded=[70] ms, gauge connect.time=70.0
req 2: request wall time=396 ms, proxy accepts=1, factory recorded=[1] ms, gauge connect.time=1.0
req 3: request wall time=347 ms, proxy accepts=1, factory recorded=[2] ms, gauge connect.time=2.0
req 4: request wall time=423 ms, proxy accepts=1, factory recorded=[1] ms, gauge connect.time=1.0
req 5: request wall time=409 ms, proxy accepts=1, factory recorded=[1] ms, gauge connect.time=1.0
=== TLS, 300 ms handshake delay, default config (connection reused)
req 1: request wall time=441 ms, proxy accepts=1, factory recorded=[3] ms, gauge connect.time=3.0
req 2: request wall time=12 ms, proxy accepts=0, factory recorded=[] ms, gauge connect.time=0.0
req 3: request wall time=16 ms, proxy accepts=0, factory recorded=[] ms, gauge connect.time=0.0
req 4: request wall time=53 ms, proxy accepts=0, factory recorded=[] ms, gauge connect.time=0.0
req 5: request wall time=11 ms, proxy accepts=0, factory recorded=[] ms, gauge connect.time=0.0
=== plain HTTP direct, connection_ttl = 1 ms (new connection per request)
req 1: request wall time=18 ms, proxy accepts=0, factory recorded=[0] ms, gauge connect.time=0.0
req 2: request wall time=7 ms, proxy accepts=0, factory recorded=[0] ms, gauge connect.time=0.0
req 3: request wall time=9 ms, proxy accepts=0, factory recorded=[1] ms, gauge connect.time=1.0
A new connection adds 330-410 ms to the request (347-423 ms, against 11-16 ms on a reused connection), but the gauge shows 1-2 ms. The 70 ms on the first request is class loading on first use.
jdb at the breakpoint, first request:
main[1] print socket
socket = null
main[1] print ConnectTimeRepro.accepted
ConnectTimeRepro.accepted = "0"
main[1] where
[1] com.clickhouse.client.api.internal.HttpAPIClientHelper$MeteredManagedHttpClientConnectionFactory.createConnection (HttpAPIClientHelper.java:1,303)
[2] com.clickhouse.client.api.internal.HttpAPIClientHelper$MeteredManagedHttpClientConnectionFactory.createConnection (HttpAPIClientHelper.java:1,293)
[3] org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager$3.get (PoolingHttpClientConnectionManager.java:443)
[4] org.apache.hc.client5.http.impl.classic.InternalExecRuntime.acquireEndpoint (InternalExecRuntime.java:111)
[5] org.apache.hc.client5.http.impl.classic.ConnectExec.execute (ConnectExec.java:127)
...
[20] com.clickhouse.client.api.internal.HttpAPIClientHelper.doExecuteRequest (HttpAPIClientHelper.java:821)
The factory runs during the pool lease (acquireEndpoint). At that time there is no socket and the proxy has not accepted a TCP connection.
Expected Behaviour
For a new connection, httpcomponents.httpclient.connect.time includes the time to establish it: DNS lookup, TCP connect and TLS handshake, as the docs say. In the reproduction above, it shows approximately 300 ms or more for each new TLS connection, not 1-2 ms.
Root cause (main 0946034a2):
- HttpAPIClientHelper.java#L1302-L1310:
createConnection(Socket)times onlysuper.createConnection(socket). HttpClient calls it withnullbefore the connect. - httpclient5 5.6.4
PoolingHttpClientConnectionManager:createConnection(null)at lines 443 (lease) and 535 (connect()), thenconnectionOperator.connect(...)at line 547, which does the DNS lookup,socket.connectand the TLS upgrade. - MicrometerLoader.java#L55-L60: the gauge (description "The running average connection creation time.").
Suggested fix:
-
Measure the connect step, not the connection factory. Two possible ways:
- Override
connect(ConnectionEndpoint, TimeValue, HttpContext)in aPoolingHttpClientConnectionManagersubclass, and timesuper.connect(...). - Wrap the
HttpClientConnectionOperator, and time itsconnect(...).PoolingHttpClientConnectionManagerBuilder.createConnectionOperator(...)isprotected.
Both ways include the DNS lookup, the TCP connect and the TLS handshake for direct routes.
- Override
-
Measure with
System.nanoTime()and keep sub-millisecond precision, or use a MicrometerTimerinstead of an integer-millisecond gauge. -
Contrast case that must not change: a request on a reused pooled connection adds no sample.
-
#3253 changes the same class. It can be useful to fix both together.
-
Re-enable
testConnectionTime, or add a test with a delayed endpoint. Also make the descriptions agree ("establishment" in the docs, "creation" inMicrometerLoader).
Code Example
// Classpath: client-v2 + dependencies + micrometer-core. /tmp/proxy.p12: self-signed key, SAN ip:127.0.0.1.
// Proxy: wait 300 ms, then do the TLS handshake, then forward bytes to ClickHouse HTTP.
SSLServerSocket ss = (SSLServerSocket) serverCtx.getServerSocketFactory()
.createServerSocket(0, 50, InetAddress.getLoopbackAddress());
// for each accepted SSLSocket s (own thread):
// Thread.sleep(300); s.startHandshake(); Socket up = new Socket("clickhouse", 8123); pipe(s <-> up);
SimpleMeterRegistry reg = new SimpleMeterRegistry();
try (Client client = new Client.Builder()
.addEndpoint("https://127.0.0.1:" + ss.getLocalPort())
.setUsername("default").setPassword("...")
.setSSLContext(trustAllContext).setSSLMode(SSLMode.TRUST)
.setConnectionTTL(1, ChronoUnit.MILLIS) // new connection for each request
.registerClientMetrics(reg, "repro")
.build()) {
Gauge gauge = reg.find("httpcomponents.httpclient.connect.time").gauge();
for (int i = 1; i <= 5; i++) {
long t0 = System.nanoTime();
try (QueryResponse r = client.query("SELECT 1").get(30, TimeUnit.SECONDS)) { }
long ms = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t0);
Thread.sleep(5); // let the TTL expire
System.out.printf("req %d: request wall time=%d ms, gauge connect.time=%.1f%n", i, ms, gauge.value());
}
}
Configuration
Client Configuration
new Client.Builder()
.addEndpoint("https://127.0.0.1:<proxy port>")
.setSSLContext(trustAllContext).setSSLMode(SSLMode.TRUST)
.setConnectionTTL(1, ChronoUnit.MILLIS)
.registerClientMetrics(new SimpleMeterRegistry(), "repro")
.build();
Environment
- Cloud
- Client version: client-v2
0.12.0-rc1-SNAPSHOT(main0946034a2), httpclient5 5.6.4, micrometer-core 1.17.1 - Language version: OpenJDK 17.0.20.1 (Temurin)
- OS: Linux (Ubuntu 22.04 container)
ClickHouse Server
- ClickHouse Server version: 26.9.1.1629
- ClickHouse Server non-default settings, if any: none that apply
CREATE TABLEstatements for tables involved: none (SELECT 1)- Sample data for all these tables: n/a
- 主要語言
- Java
- 星號
- 1.6k
- 分支
- 649
- 平均合併
- 1 天 20 小時
- 30 天內合併 PR
- 39
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
ClickHouse/clickhouse-java 的其他 Issue
-
難度 2/5 1-3 小時 新手友好度 76/100
ClickHouse/clickhouse-java#3349 ·
維護者通常 1 天內回覆
-
bug jdbc-v2
難度 2/5 1-3 小時 新手友好度 85/100
ClickHouse/clickhouse-java#3347 ·
維護者通常 1 天內回覆
-
bug jdbc-v2
難度 2/5 1-3 小時 新手友好度 82/100
ClickHouse/clickhouse-java#3326 ·
維護者通常 1 天內回覆
-
[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum members可能已有人在做 @jaya232 於 6 天前認領。 未關閉
難度 2/5 1-3 小時 新手友好度 78/100
ClickHouse/clickhouse-java#3304 · 1 則留言 ·
維護者通常 1 天內回覆
-
bug jdbc-v2
難度 2/5 1-3 小時 新手友好度 70/100
ClickHouse/clickhouse-java#3214 ·
維護者通常 1 天內回覆
查看 ClickHouse/clickhouse-java 的全部 Issue
相似的 Issue
-
bug
難度 2/5 1-3 小時 新手友好度 78/100
dekobon/big-code-analysis#1913 ·
維護者通常 1 天內回覆
-
bug
難度 2/5 1-3 小時 新手友好度 78/100
維護者通常 1 天內回覆
-
bug:unconfirmed
難度 2/5 1-3 小時 新手友好度 68/100
UniversalMediaServer/UniversalMediaServer#6473 ·
維護者通常 1 天內回覆
-
難度 1/5 1 小時以內 新手友好度 82/100
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 70/100
維護者通常 1 天內回覆