네이티브 프로토콜
네이티브 프로토콜 (Native Protocol)
네이티브 프로토콜은 ClickHouse 클라이언트와 서버가 TCP 위에서 말하는 이진, 연결 지향 프로토콜이에요. SQL 쿼리, 결과 데이터, INSERT 페이로드, 실행 텔레메트리, 오류 신호를 나르지요. 명령줄 클라이언트와 C++, 그리고 대부분의 타사 네이티브 드라이버 뒤에 있는 프로토콜이에요.
출처: 문서
본문
네이티브 프로토콜은 ClickHouse 클라이언트와 서버가 TCP 위에서 말하는 이진, 연결 지향 프로토콜이에요. SQL 쿼리, 결과 데이터, INSERT 페이로드, 실행 텔레메트리, 오류 신호를 나르지요. 명령줄 클라이언트와 C++, 그리고 대부분의 타사 네이티브 드라이버 뒤에 있는 프로토콜이에요.
이 페이지는 프로토콜 자체를 다뤄요: 패킷 프레이밍, 연결 상태 머신, 버전 협상, 그리고 모든 비-Block 메시지의 본문. Data-계열 패킷 내부의 바이트(Block, 그 컬럼, 타입별 인코딩)는 별개의 관심사이며 Native 포맷 명세에 문서화돼 있어요.
동반 명세 (Companion specification)
이 페이지는 한 쌍의 절반이며 동반 Native 포맷 명세와 함께 게시돼요. 두 명세는 작업을 깔끔하게 나눠요: 이 페이지가 패킷과 전송 계층을 맡고, Native 포맷 명세가
Data-계열 패킷 내부의 바이트를 맡아요.
몇몇 속성은 전반에 걸쳐 유지돼요. 프로토콜은 이진이고 위치적이에요: BlockInfo 내부를 제외하면 필드 태그가 없으므로, 하나라도 잘못 배치된 바이트는 이어지는 모든 것을 비동기화시켜요. 상태 유지이며, 각 TCP 연결은 한 번에 하나의 쿼리를 처리해요 — 멀티플렉싱이 없어요. 고정 너비 정수는 리틀 엔디언이에요.
개요 (Overview)
| 속성 (Property) | 값 (Value) |
|---|---|
| 전송 (Transport) | TCP |
| 프로토콜 버전 (Protocol version) | 협상됨, 최대값 DBMS_TCP_PROTOCOL_VERSION |
| 데이터 형식 (Data format) | 항상 Native |
| 메시지 지향 (Message-oriented) | 예 — VarUInt 패킷 타입 접두사 |
| 상태 (Stateful) | 예 |
와이어의 모든 메시지는 VarUInt 패킷 타입 코드로 시작하고, 그 코드와 협상된 프로토콜 버전에 따라 형태가 달라지는 본문이 뒤따라요.
연결은 세 단계를 거쳐요 — 일회성 핸드셰이크, 그 다음 임의 개수의 Ping 또는 Query 교환, 마지막에 종료:
- 네이티브 TCP 프로토콜은 SQL의 어떤
FORMAT절과 무관하게 항상 Native 포맷으로 표 형식 데이터를 나르지요.RowBinary,CSV,JSON등으로의 재포맷은 클라이언트가 Native 블록을 디코딩한 뒤 하는 몫이에요. (HTTP 인터페이스는FORMAT절을 존중하는 다른 코드 경로예요. HTTP는 여기서 범위 밖이에요.)
보안 (Security)
전송 보안 (TLS)
TLS는 프로토콜 아래, 전송 계층에 살아요. 활성화되면 전체 TCP 스트림이 암호화되고, 프로토콜 메시지는 TLS 사용 여부와 상관없이 바이트 단위로 동일해요.
인증 (Authentication)
인증은 핸드셰이크 중에 ClientHello 메시지에서 일어나요. user와 password 필드는 평문 문자열로 이동하므로, 전송 중 자격 증명을 보호하는 것은 전송 수준 암호화(TLS)예요.
빈 user 필드는 서버가 설정된 기본 세션 사용자로 해석해요: default_session_user 서버 설정(기본값 default), protocols 섹션에서 리스너별로 덮어쓸 수 있어요. 서버가 빈 기본 세션 사용자로 구성되어 있거나, 서버가 26.8보다 오래되었으면, 빈 user 필드는 예외로 거부돼요. clickhouse-client는 빈 사용자 이름을 결코 보내지 않는다는 점에 주의하세요 — 클라이언트 쪽에서 default로 대체해요.
SSH 챌린지-응답 인증은 프로토콜 버전 54466부터 사용할 수 있어요 — 아래 참조.
서버 간 비밀 (Inter-server secret)
분산 쿼리 실행에서 서버들은 공유 비밀을 안다는 것을 증명해 서로를 인증해요 — 비밀을 와이어에 올리지 않고요. 각 Query는 솔트, nonce, 구성된 비밀, 쿼리에 대해 계산된 32바이트 SHA-256 auth_hash를 Query 필드 4에 나르며, 받는 서버가 다시 계산해 비교해요. 이것은 INTERSERVER_SECRET 기능(v54441)에 의해 게이팅돼요. 외부 클라이언트는 여기에 항상 빈 문자열을 보내요. 서버 간 인증 참조.
버전 지정과 기능 게이트 (Versioning and feature gates)
버전 협상 (Version negotiation)
클라이언트와 서버 모두 핸드셰이크 중에 자신의 최대 지원 프로토콜 버전을 선언해요. 협상된 버전은 둘 중 작은 것이에요:
negotiated_version = min(client_version, server_version)
그 이후의 모든 메시지는 협상된 버전을 사용해 어떤 필드가 와이어에 존재하는지 결정해요.
기능 게이트 (Feature gates)
기능은 그것을 도입한 프로토콜 버전으로 식별되며, 협상된 버전이 그 숫자 이상이면 활성이에요.
주의
기능이 활성일 때 그 필드는 와이어에 반드시 존재해야 해요. 프로토콜은 엄격히 위치적이므로, 기능-게이팅된 필드를 생략하면 이어지는 모든 필드의 바이트 스트림을 손상시켜요.
기능 표 (Feature table)
| 기능 (Feature) | 버전 (Version) | 영향 (Affects) | 와이어 영향 (Wire impact) |
|---|---|---|---|
| BLOCK_INFO | all | Block | 모든 Block에 BlockInfo 접두사(is_overflows, bucket_number)를 추가. |
| CLIENT_INFO | 54032 | Query | Query 본문에 ClientInfo 블록을 추가. |
| TIMEZONE | 54058 | ServerHello | ServerHello에 timezone 필드를 추가. |
| QUOTA_KEY_IN_CLIENT_INFO | 54060 | ClientInfo | ClientInfo에 quota_key 필드를 추가. |
| DISPLAY_NAME | 54372 | ServerHello | ServerHello에 display_name 필드를 추가. |
| VERSION_PATCH | 54401 | ServerHello, ClientInfo | 둘 다에 version_patch 필드를 추가. |
| SERVER_LOGS | 54406 | Log | send_logs_level이 설정되면 서버가 Log 패킷을 내보냄. |
| COLUMN_DEFAULTS_METADATA | 54410 | TableColumns | 서버가 INSERT/입력 스키마 블록 전에 컬럼 기본값 메타데이터와 함께 TableColumns 패킷(타입 11)을 보낼 수 있음. 협상된 버전 ≥ 54410 그리고 input_format_defaults_for_omitted_fields가 활성화일 때만 전송. 이 버전 아래에서는 패킷이 결코 전송되지 않으므로 클라이언트는 기다리면 안 됨. |
| WRITE_CLIENT_INFO | 54420 | Progress | Progress에 wrote_rows와 wrote_bytes를 추가. (이름에도 불구하고 ClientInfo 블록을 게이팅하지 않아요 — 그것은 CLIENT_INFO(v54032)예요.) |
| SETTINGS_SERIALIZED_AS_STRINGS | 54429 | Query (설정 인코딩) | 항상 존재하는 설정 목록의 인코딩 방식을 바꿈; 설정이 전송되는지 여부를 게이팅하지 않아요. v54429+는 각 설정을 (name, flags, value-as-string)으로 작성. 오래된 피어는 플래그 없이 (name, type-specific-binary-value) 작성. Setting 참조. |
| INTERSERVER_SECRET | 54441 | Query | Query에 서버 간 auth_hash 필드 추가 — 클러스터 비밀이 아닌, 클러스터 비밀에 대한 소금친 SHA-256. 외부 클라이언트는 빈 문자열 전송. 서버 간 인증 참조. |
| OPEN_TELEMETRY | 54442 | ClientInfo | ClientInfo에 OpenTelemetry 트레이스 컨텍스트를 추가. |
| DISTRIBUTED_DEPTH | 54448 | ClientInfo | ClientInfo에 distributed_depth 필드를 추가. |
| INITIAL_QUERY_START_TIME | 54449 | ClientInfo | initial_time 필드(Int64, 고정 너비)를 추가. |
| PROFILE_EVENTS | 54451 | ProfileEvents | 쿼리 실행 중 서버가 ProfileEvents 패킷을 내보냄. |
| PARALLEL_REPLICAS | 54453 | ClientInfo | ClientInfo에 병렬-복제 조정 필드를 추가. |
| CUSTOM_SERIALIZATION | 54454 | Block | Block에서 has_custom_serialization 바이트 + kind_stack 필드를 사용 가능하게 함. |
| PARALLEL_REPLICAS_PROTOCOL_VERSION | 54455 | ClientInfo | 병렬-복제 프로토콜 버전 필드를 추가. |
| ADDENDUM | 54458 | 핸드셰이크 | 핸드셰이크에 Addendum 메시지를 추가. |
| PARAMETERS | 54459 | Query | Query 본문에 쿼리 매개변수 목록을 추가. |
| SERVER_QUERY_TIME_IN_PROGRESS | 54460 | Progress | Progress에 elapsed_ns 필드를 추가. |
| PASSWORD_COMPLEXITY_RULES | 54461 | ServerHello | ServerHello에 password 복잡도 규칙 목록을 추가. |
| INTERSERVER_SECRET_V2 | 54462 | ServerHello | ServerHello에 nonce 필드를 추가. |
| TOTAL_BYTES_IN_PROGRESS | 54463 | Progress | Progress에 total_bytes 필드를 추가. |
| TIMEZONE_UPDATES | 54464 | TimezoneUpdate | TimezoneUpdate 패킷을 도입. |
| SSH_AUTHENTICATION | 54466 | 핸드셰이크 | SSH 챌린지-응답 인증을 도입. |
| AGGREGATE_FUNCTIONS_VERSIONING | 54468 | Block | AggregateFunction 상태에 버전 지정을 도입. |
| ROWS_BEFORE_AGGREGATION | 54469 | ProfileInfo | ProfileInfo에 applied_aggregation과 rows_before_aggregation을 추가. |
| CHUNKED_PROTOCOL | 54470 | 모든 패킷 | 청크 프레이밍을 도입. |
| VERSIONED_PARALLEL_REPLICAS_PROTOCOL | 54471 | 핸드셰이크 | 병렬-복제 조정 프로토콜의 버전을 도입. |
| INTERSERVER_EXTERNALLY_GRANTED_ROLES | 54472 | Query | 외부에서 부여된 역할 목록을 추가. |
| V2_DYNAMIC_AND_JSON_SERIALIZATION | 54473 | Block | Dynamic/JSON V2 직렬화를 도입. |
| SERVER_SETTINGS | 54474 | ServerHello | 서버 설정 브로드캐스트를 추가. |
| QUERY_PLAN_SERIALIZATION | 54477 | QueryPlan | 쿼리 플랜 직렬화를 도입. |
| PARALLEL_BLOCK_MARSHALLING | 54478 | Block | 병렬 블록 마샬링 / ColumnBLOB 경로를 도입. |
| VERSIONED_CLUSTER_FUNCTION_PROTOCOL | 54479 | ServerHello | *Cluster 테이블 함수 프로토콜 버전을 추가. |
| COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS | 54481 | Log/ProfileEvents/TableColumns | 이 패킷의 압축된 본문을 도입. |
| PROGRESS_IN_ASYNC_INSERT | 54484 | Progress | 비동기 INSERT 완료 시 추가 Progress 패킷을 도입. |
| INTERSERVER_CURRENT_ROLES | 54488 | Query | 서버 간 인증에 현재 역할 목록을 추가. |
| HTTP_HANDLER_IN_CLIENT_INFO | 54490 | ClientInfo | ClientInfo에 HTTP 핸들러 이름/요청 URL 필드를 추가. |
| LOW_CARDINALITY_SERIALIZATION | 54493 | Block | LowCardinality 키 직렬화 버전을 도입. |
패킷 봉투 (Packet envelope)
와이어의 모든 메시지는 양방향 모두 같은 외부 구조를 공유해요:
[VarUInt: packet_type_code] 항상 VarUInt로 인코딩
[message body] packet_type_code에 따라 형식이 달라짐
전체 패킷-타입 표는 패킷 타입 참조에 있어요.
패킷 타입은 고정 너비 바이트가 아니라 VarUInt예요. 128 미만의 값에 대해 VarUInt는 같은 단일 바이트를 만들지만, 미래의 패킷 타입이 128 이상에 도달해도 호환되도록 구현은 VarUInt 인코딩을 사용해야 해요.
메시지 참조는 각 패킷의 본문만 문서화해요 — 패킷 타입 코드 다음의 바이트. 필드 번호는 첫 본문 필드에서 1부터 시작해요.
청크 프레이밍 (Chunked framing, v54470+)
CHUNKED_PROTOCOL 기능이 협상되면(핸드셰이크 참조), 와이어의 모든 패킷이 청크 프레이밍으로 감싸져요. 감싸기는 방향별이에요: 클라이언트→서버와 서버→클라이언트는 따로 협상되고 서로 다른 모드(청크 vs 비프레이밍)가 될 수 있어요.
패킷당 와이어 레이아웃:
... 하나 이상의 청크; 그 페이로드를 이어 붙인 것이 전체 패킷
[u32 LE = 0] 패킷의 끝을 표시하는 제로 크기 종결자
청크당 와이어 레이아웃:
[u32 LE: chunk_size] chunk_size는 [1, UINT32_MAX]
[chunk_size bytes] 패킷 바이트 (아래 참고 참조)
패킷 타입 VarUInt는 청크 스트림 안에 있어요: 그것은 패킷 페이로드의 첫 번째 바이트(첫 청크의 첫 번째 바이트)이지, 프레이밍보다 앞서 보내지는 별도 바이트가 아니에요. 각 패킷의 청크 페이로드는 패킷 봉투의 전체 [VarUInt packet_type_code][message body]예요. 패킷 타입을 청크 스트림 밖에 두는 클라이언트는 피어가 그 타입 바이트를 u32 청크 크기의 첫 번째 바이트로 읽게 해서 연결을 비동기화시켜요.
단일 패킷은 작성자 버퍼가 패킷 중간에 채워지면 여러 청크로 나뉠 수 있어요. 분할은 패킷 타입의 VarUInt 내부를 포함해 어디에든 떨어질 수 있어요. 리더는 청크 페이로드를 연결하고 trailing 4바이트 제로를 투명한 패킷 경계로 취급해요 — 소비하지만 패킷 본문을 읽는 쪽에는 표면화하지 않아요.
본문 없는 패킷도 여전히 감싸져요: Ping이나 Pong 같은 단일 바이트 패킷은 청킹이 협상되면 [u32 size = 1][0x04][u32 0]이 돼요. 이 페이지의 다른 곳에 있는 "와이어의 단일 바이트" 설명은 청킹 전 형태예요.
협상. ServerHello와 Addendum 각각은 방향당 하나씩인 두 개의 String 필드를 나르며, 값은 {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}에서 나와요:
chunked/notchunked는 엄격해요: 그 쪽이 정확히 그 모드를 요구해요._optional변형은 유연해요: 상대가 고르는 모드를 받아들여요.
각 방향의 합의 값은 쌍으로 계산돼요:
| 서버 선호 (Server pref) | 클라이언트 선호 (Client pref) | 합의 (Agreed) |
|---|---|---|
| *_optional | anything | CLIENT를 따름 (starts_with("chunked")) |
| anything | *_optional | SERVER를 따름 |
| chunked strict | chunked strict | chunked |
| notchunked strict | notchunked strict | notchunked |
| strict | strict, 불일치 | 프로토콜 오류 — 연결을 반드시 끊어야 함 |
클라이언트 쪽에서는 클라이언트의 SEND 선호가 서버의 RECV 선호와 협상되고, 그 반대도 마찬가지예요.
타이밍. 협상 문자열은 비프레이밍 와이어로 이동해요: ClientHello → ServerHello(서버 선호) → Addendum(클라이언트의 협상된 값). 프레이밍 전환은 Addendum이 플러시된 후에 보내진 모든 바이트에 적용돼요. Addendum 자체, ClientHello, ServerHello는 항상 비프레이밍이에요.
연결 수명주기 (Connection lifecycle)
어느 순간이든 연결은 정확히 네 상태 중 하나에 있어요: HANDSHAKE, READY, READING_RESPONSE, 또는 terminated. 프로토콜이 멀티플렉싱하지 않으므로, 클라이언트가 이전 응답을 비우기 전에 새 요청을 보내면 와이어에 바이트를 인터리브해 스트림을 손상시켜요.
상태 (States)
행복한 경로는 바로 아래로 달려요 — HANDSHAKE → READY → READING_RESPONSE → READY — Ping/Pong 자체-루프와 모든 실패 가장자리가 단일 Terminated 싱크로 흘러들어가요.
| 상태 (State) | 설명 (Description) |
|---|---|
| HANDSHAKE | TCP 연결이 열린 후의 초기 상태. 핸드셰이크 메시지만 유효. 성공 시 READY로 전환, 실패 시 종료. |
| READY | 유휴. 클라이언트는 Ping, Query, 또는 종료를 보낼 수 있음. 연결은 무기한 READY에 머무를 수 있음(idle_connection_timeout 적용, 연결 제한 참조). 여기서 Data(2) 또는 Scalar(7) 패킷은 프로토콜 위반: 서버가 패킷 본문을 디코딩하지 않고, Exception 패킷을 먼저 보내지도 않고 연결을 종료. 본문이 읽히지 않은 채 남으므로 클라이언트는 정돈된 종료보다 TCP reset을 볼 수 있음. |
| READING_RESPONSE | 클라이언트가 Query를 보낼 때 진입. 클라이언트는 READY로 돌아가기 전에 서버의 응답 스트림을 완전히 비워야 함. 여기 허용된 유일한 클라이언트→서버 패킷은 Cancel(이 페이지에서 명세되지 않음). |
| Terminated | 더 이상 사용할 수 없음. 클라이언트는 새 TCP 연결을 열고 핸드셰이크를 다시 시작해야 함. |
핸드셰이크 단계 (Handshake phase)
인증하고 프로토콜 버전을 협상해요. 연결당 정확히 한 번, 다른 무엇보다 먼저 일어나요.
TCP 연결이 막 열렸고 메시지가 교환되지 않았어요. 흐름:
- 클라이언트가 최대 지원 프로토콜 버전과 함께 ClientHello를 보내요.
- 클라이언트가 응답을 읽고 패킷 타입으로 분기해요:
- Hello(0): ServerHello를 디코딩.
negotiated_version = min(client_ver, server_ver)를 계산. 단계 3으로 진행. - Exception(2): Exception을 디코딩. 오류로 반환하고 연결을 종료.
- 그 외: 프로토콜 위반. 연결을 종료.
- Hello(0): ServerHello를 디코딩.
negotiated_version ≥ 54458(ADDENDUM 기능)이면 클라이언트가 Addendum을 보내요. 이 결정은 클라이언트의 선언 버전이 아니라 협상된 버전에 기반해요.- 성공 시 연결은 READY로 이동하고, 어떤 오류든 종료해요.
Ping 단계
TCP keepalive와 독립적인 애플리케이션 수준 활성 검사예요. 성공적인 Ping/Pong 왕복은 TCP 연결이 양방향 모두 살아 있고 서버가 응답함을 확인해요. Ping은 무상태이며 어떤 쿼리와도 상관되지 않으므로, 여러 순차 Ping은 독립적이에요.
READY에서 시작해 흐름은:
- 클라이언트가 Ping을 보내요.
- 클라이언트가 응답을 읽어요:
- Pong(4): 활성 확인. READY로 복귀.
- Exception(2): Exception을 디코딩해 오류로 반환.
- 그 외: 프로토콜 위반.
Query 단계
클라이언트가 SQL 문을 제출하면 서버가 결과 블록과 실행 텔레메트리를 스트리밍해요. 응답은 정확히 하나의 EndOfStream 또는 Exception으로 끝나는 패킷 시퀀스예요.
READY에서 시작해 흐름은:
- 클라이언트가 고유한
query_id(보통 UUID)와 함께 Query를 보내요. - 클라이언트가 외부 테이블을 보낸 다음 빈 Data 마커를 보내요. 빈 Data 패킷은
table_name = "",num_columns = 0,num_rows = 0이에요. 서버는 이 마커를 받기 전에는 쿼리 실행을 시작하지 않아요. 외부 테이블은 같은 table_name으로 여러 Data 패킷에 나뉠 수 있어요. 그중 첫 번째가 테이블의 스키마를 묶고, 그 이름의 이후 모든 패킷은 같은 순서로 같은 컬럼 이름과 타입을 나르지 않으면 서버가INCORRECT_DATA로 쿼리를 거부해요(Data 참조). - 클라이언트가 READING_RESPONSE로 이동하고 쓰기 버퍼를 플러시해요.
- 클라이언트가 응답 패킷을 타입별로 분기하며 루프로 읽어요:
- Data(1): 블록을 디코딩. 첫 Data는 스키마 헤더, 이후는 결과 블록(누적). 빈 블록은 경계 마커.
num_rows == 0은 쿼리 끝이 아니에요. - Progress(3): 실행 메트릭. 각 패킷은 이전 이후의 증분 — 로컬에서 누적.
- EndOfStream(5): 쿼리 완료. 루프를 나와 READY로 복귀.
- ProfileInfo(6): 실행 후 프로파일링 데이터.
- Totals(7): 집계 총계 블록(Data와 같은 와이어 포맷).
- Extremes(8): 최소/최대 값 블록(Data와 같은 와이어 포맷).
- Log(10): 서버 로그 줄.
- TableColumns(11): 컬럼 기본값 메타데이터.
- ProfileEvents(14): 성능 카운터.
- Exception(2): 디코딩해 오류로 반환. 루프를 나와 READY로 복귀.
- 그 외: Query 단계 중 예상하지 못한 것. 연결을 종료.
- Data(1): 블록을 디코딩. 첫 Data는 스키마 헤더, 이후는 결과 블록(누적). 빈 블록은 경계 마커.
- EndOfStream 또는 처리된 Exception에서 연결은 READY로 복귀해요. 프로토콜 위반이나 I/O 오류는 종료해요.
주의
num_rows == 0 경우는 새 구현을 걸려 넘어지게 해요. 0-행 블록은 경계 마커 또는 스키마 헤더이지, end-of-stream 신호가 아니에요. 응답을 끝내는 것은 EndOfStream 또는 Exception뿐이에요.
INSERT 단계
INSERT 단계는 두 개의 추가 교환이 있는 Query 단계예요. 클라이언트가 INSERT 문을 제출하면, 서버는 대상 테이블을 설명하는 스키마 블록으로 응답하고, 클라이언트는 행을 담은 Data 패킷을 스트리밍한 다음 빈 Data 마커를 보내고, 서버는 EndOfStream 또는 Exception으로 마무리해요.
READY에서 시작해, SQL은 INSERT INTO <name> [<columns>] VALUES 형태의 INSERT예요 — 인라인 VALUES (...) 리터럴이 없는데, 행 데이터가 Data 패킷을 통해 흐르기 때문이에요. 흐름:
- 클라이언트가 본문을 INSERT SQL로 설정한 Query를 보내요.
- 클라이언트가 외부 테이블을 보내요(INSERT에선 드묾). Query 단계와 달리 여기서는 빈 Data 마커를 보내지 않아요. INSERT Query 패킷은 대기 데이터와 함께 보내지므로, 빈 종료-데이터 블록은 5단계로 연기돼요. 스키마 블록 전에 보내면 서버가 그것을 행 스트림의 끝으로 읽어 행 없는 INSERT를 끝내고, 첫 실제 행 패킷을 어긋난 최상위 패킷으로 파싱해요.
- 클라이언트는 스키마 Data 패킷 — 행 0개지만 완전한 컬럼 구조(이름과 타입)를 가진 Block — 을 읽을 때까지 메타데이터 패킷(TableColumns, Progress, ProfileInfo, Log, ProfileEvents)을 비워요. 스키마 블록이 계약이에요: 클라이언트가 다음에 보내는 행들이 이 컬럼 형태와 일치해야 해요.
- 클라이언트가 데이터 블록을 보내요. 각 블록에 대해
VarUInt(ClientPacket::Data = 2), 그 다음 빈 외부 테이블 이름을 위한String(""), 그 다음 Block을 작성해요. 컬럼 타입은 스키마 블록의 컬럼과 위치적으로 정렬돼야 해요. - 클라이언트가 입력 끝 종결자 — 빈 Block(컬럼 0, 행 0)을 가진 Data 패킷 — 을 보내요.
- 클라이언트가 EndOfStream(성공) 또는 Exception(실패)까지 응답 스트림을 비워요.
비동기 INSERT (v54484+). 쿼리가 async_insert = 1을 나르면 서버가 행을 큐에 넣고 배치의 일부로 플러시해요. 협상된 버전 ≥ 54484(PROGRESS_IN_ASYNC_INSERT)에서 플러시가 완료되면 서버가 추가 Progress 패킷을 내보내고, 바로 이어 그 insert의 ProfileEvents, 그 다음 EndOfStream을 내보내요. 54484 미만에서는 서버가 그 trailing Progress를 건너뛰어요. 그 패킷은 보통 Progress예요. 서버가 쓰기 개수를 접기 전에 쿼리 파이프라인을 재설정하므로, 그 증분은 실제로 경과 시간만을 나르고, 작성된 행과 바이트 통계는 동반하는 ProfileEvents를 통해 클라이언트에 도달해요. 6단계에서 이미 인터리브된 Progress를 비우는 클라이언트는 패킷 하나를 더 받아들이기만 하면 돼요.
연결은 EndOfStream 또는 처리된 Exception에서 READY로 복귀해요. 프로토콜 위반과 I/O 오류는 종료해요.
메시지 참조 (Message reference)
필드는 와이어 순서로 나열돼요. Type 열은 다음을 사용해요:
- VarUInt — 가변 길이 부호 없는 정수(VarUInt 참조).
- String — VarUInt 접두사 바이트(String 참조).
- UInt8, Int32 등 — 고정 너비 리틀 엔디언 정수.
- Bool — 단일 바이트, 0x00 또는 0x01.
Role 열은 각 필드를 누가 사용하는지 말해요:
- client** — 외부 클라이언트가 설정.
- inter-server** — 서버 간 통신에서만 의미. 외부 클라이언트는 기본값을 작성.
- universal** — 둘 다 사용.
이 표들은 각 패킷의 본문만, 패킷 타입 코드 뒤만 문서화해요.
ClientHello (패킷 타입 0)
Client → Server. TCP 연결이 열린 후의 첫 메시지.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 설명 (Description) |
|---|---|---|---|---|
| 1 | client_name | String | universal | 클라이언트 식별자 (예: "clickhouse-client") |
| 2 | version_major | VarUInt | universal | 클라이언트 주요 버전 |
| 3 | version_minor | VarUInt | universal | 클라이언트 부 버전 |
| 4 | protocol_version | VarUInt | universal | 클라이언트의 최대 지원 프로토콜 버전 |
| 5 | database | String | universal | 기본 데이터베이스 이름 |
| 6 | user | String | universal | 인증용 사용자 이름. 빈 값 = 서버의 기본 세션 사용자(default_session_user 서버 설정; 26.8보다 오래된 서버는 거부). 인증 참조. |
| 7 | password | String | universal | 비밀번호 (평문) |
ServerHello (패킷 타입 0)
Server → Client. 성공적인 인증의 ClientHello에 대한 응답.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 조건 (Condition) | 설명 (Description) |
|---|---|---|---|---|---|
| 1 | server_name | String | universal | always | 서버 식별자 |
| 2 | version_major | VarUInt | universal | always | 서버 주요 버전 |
| 3 | version_minor | VarUInt | universal | always | 서버 부 버전 |
| 4 | protocol_version | VarUInt | universal | always | 서버의 프로토콜 버전 |
| 4a | parallel_replicas_protocol_version | VarUInt | universal | VERSIONED_PARALLEL_REPLICAS_PROTOCOL (v54471) | 서버의 병렬-복제 조정 프로토콜 버전. 와이어 위치: protocol_version** 바로 뒤, timezone 앞. 현재: 8 |
| 5 | timezone | String | universal | TIMEZONE (v54058) | 서버 시간대 (예: "UTC") |
| 6 | display_name | String | universal | DISPLAY_NAME (v54372) | 사람이 읽는 서버 이름 |
| 7 | version_patch | VarUInt | universal | VERSION_PATCH (v54401) | 서버 패치 버전 |
| 8 | proto_send_chunked_srv | String | universal | CHUNKED_PROTOCOL (v54470) | 서버의 선호 나가는 방향 청킹. "chunked", "notchunked", "chunked_optional", "notchunked_optional" 중 하나. 청크 프레이밍 참조. 와이어에서 password_complexity_rules보다 앞에 앉지만 버전 게이트는 더 높아요. |
| 9 | proto_recv_chunked_srv | String | universal | CHUNKED_PROTOCOL (v54470) | 서버의 선호 들어오는 방향 청킹. 필드 8과 같은 값 집합. |
| 10 | password_complexity_rules | Rule[] | universal | PASSWORD_COMPLEXITY_RULES (v54461) | 서버의 비밀번호 정책. VarUInt 개수 뒤에 count × Rule. 아래 참조. |
| 11 | nonce | UInt64 | inter-server | INTERSERVER_SECRET_V2 (v54462) | 8바이트 LE 랜덤 nonce. 서버의 서버 간 쿼리 서명 체계가 사용. 외부 클라이언트는 (스트림을 정렬하기 위해) 디코딩하고 (값은) 무시해야 해요. |
| 12 | server_settings | Setting[] | universal | SERVER_SETTINGS (v54474) | 서버의 비기본 설정 브로드캐스트. 형식: 0개 이상의 (String key, VarUInt flags, String value) 트리플, 빈 키로 종결. Query 패킷의 설정 목록과 같음. |
| 13 | query_plan_serialization_version | VarUInt | universal | QUERY_PLAN_SERIALIZATION (v54477) | 서버의 지원 쿼리-플랜 직렬화 버전. 외부 클라이언트는 디코딩 후 무시. |
| 14 | cluster_function_protocol_version | VarUInt | universal | VERSIONED_CLUSTER_FUNCTION_PROTOCOL (v54479) | 서버의 *Cluster 테이블 함수 프로토콜 버전. 외부 클라이언트는 디코딩 후 무시. |
Rule — password_complexity_rules의 요소:
| # | 필드 (Field) | 타입 (Type) | 설명 (Description) |
|---|---|---|---|
| 1 | pattern | String | 준수 비밀번호가 일치해야 하는 정규식 패턴. |
| 2 | message | String | 비밀번호가 이 규칙에 실패할 때 표시되는 사람이 읽는 설명. |
목록은 서버 운영자의 비밀번호 정책 구성을 반영하며 순전히 조언적이에요 — 서버는 핸드셰이크 중에 이 규칙을 강제하지 않아요. 비밀번호 변경/설정 기능을 노출하는 클라이언트는 규칙을 사용해 비준수 비밀번호를 서버에 왕복시키기 전에 오류를 표시할 수 있어요.
주의
적대적이거나 잘못 구성된 서버에 대한 자원 사용을 제한하려면 디코딩된 개수를 256개 항목으로, 각 pattern과 message String을 4096바이트로 상한을 정하세요. 개수 0(뒤따르는 쌍 없음)은 비밀번호 정책이 구성되지 않은 서버의 일반적인 경우예요.
Addendum (패킷 타입 없음)
Client → Server, ADDENDUM(v54458)로 게이팅. 핸드셰이크 교환이 완료된 직후에 보내져요. 별개의 패킷 타입이 아니에요 — 필드들이 패킷 타입 바이트 접두사 없이 와이어에 원시로 가요.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 조건 (Condition) | 설명 (Description) |
|---|---|---|---|---|---|
| 1 | quota_key | String | universal | always | 서버 측 키드 할당량을 위한 리소스 할당량 키. 키드 할당량을 쓰지 않는 클라이언트는 빈 문자열을 보내요. |
| 2 | proto_send_chunked | String | universal | CHUNKED_PROTOCOL (v54470) | 클라이언트의 협상된 나가는 방향 청킹: "chunked" 또는 "notchunked". ServerHello의 proto_recv_chunked_srv에 대해 계산. |
| 3 | proto_recv_chunked | String | universal | CHUNKED_PROTOCOL (v54470) | 클라이언트의 협상된 들어오는 방향 청킹. proto_send_chunked_srv에 대해 계산. |
| 4 | parallel_replicas_protocol_version | VarUInt | universal | VERSIONED_PARALLEL_REPLICAS_PROTOCOL (v54471) | 클라이언트의 지원 병렬-복제 조정 프로토콜 버전. 분산 쿼리에 참여하지 않는 외부 클라이언트도 서버의 호환성 검사가 성공하도록 유효한 버전(현재 8)을 보내야 해요. |
청크 프레이밍 전환은 이 Addendum이 플러시된 후에 적용돼요 — Addendum 자체는 비프레이밍이에요.
Ping (패킷 타입 4)
Client → Server. 본문 없음 — 패킷은 청크 프레이밍 전에는 단일 바이트 0x04예요. 청킹이 협상되면 그 바이트가 청크의 1바이트 페이로드가 돼요(청크 프레이밍 참조).
Pong (패킷 타입 4)
Server → Client. 본문 없음 — 패킷은 청크 프레이밍 전에는 단일 바이트 0x04예요. 청킹이 협상되면 그 바이트가 청크의 1바이트 페이로드가 돼요(청크 프레이밍 참조).
Exception (패킷 타입 2)
Server → Client. 서버가 어떤 단계 중 오류를 만날 때 보내져요.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 설명 (Description) |
|---|---|---|---|---|
| 1 | code | Int32 | universal | 오류 코드 |
| 2 | name | String | universal | 예외 클래스 (예: "DB::Exception") |
| 3 | message | String | universal | 사람이 읽는 오류 메시지 |
| 4 | stack_trace | String | universal | 서버 측 스택 트레이스 |
| 5 | has_nested (obsolete) | Bool | universal | 폐기된 호환 바이트. 서버에 의해 항상 false로 작성. |
Query (패킷 타입 1)
Client → Server.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 조건 (Condition) | 설명 (Description) |
|---|---|---|---|---|---|
| 1 | query_id | String | universal | always | 고유 쿼리 식별자 (UUID) |
| 2 | client_info | ClientInfo | universal | CLIENT_INFO (v54032) | ClientInfo 참조 |
| 3 | settings | Setting[] | universal | always | Setting 참조. 항상 존재** (빈 키로 종결). 설정별 인코딩_ 만 버전 게이팅돼요 — Setting의 인코딩 참고 참조. 클라이언트는 협상된 버전이 54429 미만일 때 이 필드를 생략하면 안 돼요. |
| 3a | external_roles | String | universal | INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472) | 외부에서 부여된 역할 이름의 직렬화된 목록. 빈 목록 = String 봉투로 감싼 바이트 0x00 (VarUInt 0).(와이어에서 [VarUInt 1][0x00]). 외부 클라이언트는 항상 빈 값 전송. |
| 4 | auth_hash | String | inter-server | INTERSERVER_SECRET (v54441) | 서버 간 인증 해시 — 원시 클러스터 비밀이 아님. 아래 서버 간 인증 참조. 외부 클라이언트(그리고 어떤 InitialQuery든)는 빈 문자열 전송. |
| 5 | stage | VarUInt | universal | always | 쿼리 처리 단계. 0 = FetchColumns, 1 = WithMergeableState, 2 = Complete, 3 = WithMergeableStateAfterAggregation, 4 = WithMergeableStateAfterAggregationAndLimit, 7 = QueryPlan. 값 3/4는 분산 쿼리에 나타나고, 7은 직렬화된 쿼리 플랜을 동반. 외부 클라이언트는 보통 2 전송. |
| 6 | compression | VarUInt | universal | always | 0 = 비활성, 1 = 활성 |
| 7 | query_body | String | universal | always | SQL 텍스트 |
| 8 | parameters | Parameter[] | client | PARAMETERS (v54459) | Parameter 참조. 빈 키로 종결. |
ClientInfo (Query에 내장)
Client → Server, Query 본문(필드 2)에 내장. CLIENT_INFO(v54032)로 게이팅. (ClientInfo 내부의 일부 필드는 아래 필드별로 나온 대로 더 늦은 버전으로 게이팅돼요.)
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 조건 (Condition) | 설명 (Description) |
|---|---|---|---|---|---|
| 1 | query_kind | UInt8 | universal | always | 0 = NoQuery, 1 = InitialQuery, 2 = SecondaryQuery. 외부 클라이언트는 1 전송. |
| 2 | initial_user | String | universal | always | 쿼리를 시작한 사용자 |
| 3 | initial_query_id | String | universal | always | 원래 쿼리 ID |
| 4 | initial_address | String | universal | always | 발신 클라이언트 소켓 주소. 서버는 이 값을 결코 해석하지 않아요(호스트네임이나 서비스-이름 조회 없음). SECONDARY_QUERY(값이 유지되고 사용되는 곳, 예: system.query_log와 서버 간 인증)의 경우 허용 문법은 IPv4 a.b.c.d:port 또는 대괄호 IPv6 [addr]:port이며, 호스트는 IP 리터럴이고 포트는 0..65535의 십진수. 다른 형태(예: localhost:9000, host:http, :9000, 또는 /tmp/ch.sock 같은 UNIX 소켓 경로)는 INCORRECT_DATA로 거부. INITIAL_QUERY의 경우 서버가 이 필드를 실제 피어 주소로 덮어쓰므로 어떤 값이든 받아들여요(평범한 ip:port가 아닌 값은 기본 0.0.0.0:0으로 대체). 외부 클라이언트는 자신의 ip:port를 보내야 해요. |
| 5 | initial_time | Int64 | client | INITIAL_QUERY_START_TIME (v54449) | 쿼리 시작 시간(마이크로초). VarUInt가 아닌 고정 너비 8바이트 |
| 6 | query_interface | UInt8 | universal | always | 1 = TCP, 2 = HTTP |
| 7 | os_user | String | client | 인터페이스 = TCP일 때 | OS 사용자 이름 |
| 8 | client_hostname | String | client | 인터페이스 = TCP일 때 | 클라이언트 머신 호스트네임 |
| 9 | client_name | String | client | 인터페이스 = TCP일 때 | 클라이언트 애플리케이션 이름 |
| 10 | version_major | VarUInt | universal | 인터페이스 = TCP일 때 | 클라이언트 주요 버전 |
| 11 | version_minor | VarUInt | universal | 인터페이스 = TCP일 때 | 클라이언트 부 버전 |
| 12 | protocol_version | VarUInt | universal | 인터페이스 = TCP일 때 | 발신 클라이언트 자신의 TCP 프로토콜 버전(DBMS_TCP_PROTOCOL_VERSION), 협상된 버전이 아님. 피어 개정판은 어떤 필드가 존재하는지만 결정하고, 이 값은 시작자의 컴파일된 버전이므로 더 새로운 클라이언트가 더 오래된 서버와 말할 때 협상된/서버 개정판보다 클 수 있어요. |
| 13 | quota_key | String | universal | QUOTA_KEY_IN_CLIENT_INFO (v54060) | 서버 측 키드 할당량을 위한 리소스 할당량 키. 키드 할당량을 쓰지 않는 클라이언트는 빈 문자열 전송. |
| 14 | distributed_depth | UInt8 | universal | DISTRIBUTED_DEPTH (v54448) | 분산 쿼리에서의 깊이. 외부 클라이언트는 0 전송. |
| 15 | version_patch | VarUInt | universal | 인터페이스 = TCP일 때, VERSION_PATCH (v54401) | 클라이언트 패치 버전 |
| 16 | OpenTelemetry | (아래 참조) | universal | OPEN_TELEMETRY (v54442) | 트레이스 컨텍스트. 아래 인코딩 참조. |
인터페이스 의존 레이아웃 (필드 7–12)
위 필드 7–12는 TCP 분기예요. query_interface(필드 6)가 TCP가 아니면 이 필드들은 다른 와이어 레이아웃으로 _대체_돼요 — 단순히 선택적 생략이 아니므로, 디코더는 필드 6에서 분기해야 해요.
query_interface = 2(HTTP): 서버-전달 HTTP 요청 정보가 대신 작성돼요 —http_method(UInt8),http_user_agent(String), 그 다음forwarded_for(String, X_FORWARDED_FOR_IN_CLIENT_INFO v54443으로 게이팅),http_referer(String, REFERER_IN_CLIENT_INFO v54447으로 게이팅), 마지막으로http_handler_name(String)과http_request_url(String)(둘 다 HTTP_HANDLER_IN_CLIENT_INFO v54490으로 게이팅, 그 순서). os_user/client_hostname/client_name/version_*/protocol_version 필드는 존재하지 않아요. 마지막 두 필드는 일치하는 SQL 정의 HTTP 핸들러 이름과 요청 URL을 나르므로 원격 샤드까지 살아남아요(핸들러가 아닌 HTTP 쿼리는 빈 값).- 그 외 인터페이스: TCP 필드(7–12)도 HTTP 필드도 작성되지 않고, 스트림이 quota_key로 직접 계속돼요.
이 분기 후 레이아웃이 다시 합쳐져요: quota_key(필드 13)와 distributed_depth(필드 14)가 모든 인터페이스에서 뒤따르고, version_patch(필드 15)는 TCP에서만 작성돼요.
이 분기는 주로 서버 간 트래픽에 중요해요. 시작 서버가 원래 HTTP로 도착한 쿼리를 전달하는 곳이거든요. 항상 TCP 필드를 읽는 디코더는 그런 패킷을 잘못 읽어
http_method나http_user_agent를 quota_key로 취급해요.
OpenTelemetry 인코딩 (필드 16):
[UInt8: has_trace] 0 = 뒤따르는 트레이스 데이터 없음, 1 = 트레이스 데이터 있음
If has_trace == 1:
[16 bytes: trace_id] 8바이트마다 바이트-스왑
[8 bytes: span_id] 바이트-스왑
[String: trace_state] W3C trace state
[UInt8: trace_flags] W3C trace flags
Inter-server 인증 (Inter-server authentication)
Query 필드 4(auth_hash)는 와이어의 공유 클러스터 비밀이 아니에요. 원시 비밀을 보내면 인증이 실패할 뿐만 아니라 그것을 새어보내요. 대신 서버 간 클라이언트 역할을 하는 서버는 소금친 SHA-256 해시로 비밀을 안다는 것을 증명해요:
- 서버 간 모드 진입. 연결하는 서버가 ClientHello 안에서 신호를 보내요:
user필드가 서버 간 마커이고 password가 비어요. 그런 다음 user/password 필드 직후 같은 ClientHello 패킷의 일부로 두 개의 추가 문자열 — 클러스터 이름과 갓 생성된 32바이트 솔트(encodeSHA256of a random value) — 을 추가해요. 서버는 ServerHello를 보내기 전에 이 두 문자열을 읽으므로, 클라이언트는 그것들을 먼저 써야 해요. ServerHello를 먼저 기다리면 서버가 그것들을 읽느라 막혀 있으므로 교착 상태가 돼요. - nonce 얻기. ServerHello는 INTERSERVER_SECRET_V2(v54462)가 협상되면 8바이트 UInt64 nonce를 나르지요.
- 해시 계산. 모든 비-InitialQuery Query 패킷에 대해 클라이언트는 필드 4에
encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)— 32바이트 다이제스트 — 를 작성해요. (nonce는 그 십진 문자열 형태이며, v54462 이상이 협상될 때만 존재. external_roles는 INTERSERVER_EXTERNALLY_GRANTED_ROLES(v54472)가 협상될 때만 추가. current_roles는 직렬화된 역할-이름 목록 —[VarUInt count][String]*count, 존재 바이트 없음 — 이며 INTERSERVER_CURRENT_ROLES(v54488)가 협상되고 목록이 존재할 때만 추가.) InitialQuery이거나, 클러스터 비밀이 구성되지 않았으면 클라이언트는 대신 빈 문자열을 작성해요. - 검증. 서버는 32바이트 상한으로 필드 4를 읽고 자신의 클러스터 비밀 사본으로 같은 연결을 다시 계산해요. 다이제스트가 다르면 연결이 거부돼요.
외부(비서버 간) 클라이언트는 이 모드에 결코 들어가지 않고 항상 빈 auth_hash를 보내요.
Setting
Query 본문의 설정 목록에 인라인으로 인코딩돼요 (Query 패킷, 필드 3). 목록은 협상된 버전과 무관하게 항상 존재하고, 빈 키를 가진 Setting — 플래그나 값이 뒤따르지 않는 단일 VarUInt 0 — 으로 종결돼요. 설정별 인코딩만 협상된 버전에 의존하며, SETTINGS_SERIALIZED_AS_STRINGS(v54429)로 게이팅돼요.
v54429+ (STRINGS_WITH_FLAGS)** — 각 설정은 여기 보이는 트리플:
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 설명 (Description) |
|---|---|---|---|---|
| 1 | key | String | universal | 설정 이름. 빈 값 = 목록 끝. |
| 2 | flags | VarUInt | universal | 메타데이터 비트 플래그; 아래 참조. |
| 3 | value | String | universal | 문자열로 된 설정 값 |
필드 2와 3은 key가 빈 값일 때 없어요.
Pre-54429 (BINARY)** — 각 설정은 [String key][타입별 이진 값]: flags 필드는 작성되지 않고, 값은 십진/텍스트 문자열이 아니라 설정의 네이티브 이진 형태(예: 고정 너비 정수 또는 길이 접두사 문자열)로 인코딩돼요. 목록은 여전히 빈 키로 종결돼요. 54429 미만의 협상된 버전을 목표로 하는 클라이언트는 위 트리플이 아니라 이 이진 형태를 읽고 써야 해요. (사용자 정의 설정은 예외예요: 두 인코딩 모두에서 항상 플래그와 문자열 값을 나르지요.)
flags 필드는 다음을 담아요:
- 0x01 — Important(중요): 설정이 쿼리 결과에 영향을 주며 오래된 피어가 조용히 무시하면 안 돼요.
- 0x02 — Custom(사용자 정의): 사용자 정의 설정.
- 0x0c — 독립 플래그가 아니라 2비트 티어(tier) 필드: 0x00 = Production, 0x04 = Obsolete, 0x08 = Experimental, 0x0c = Beta. 2비트 전체(
flags & 0x0c)를 읽으세요 — 단순한flags & 0x04검사는 Beta(0x0c)를 Obsolete로 잘못 분류해요. - 0x80 — HotReload(재시작 없이 설정 리로드; flags 열거에 정의, 주로 조정 설정에서 나타남).
Parameter
쿼리 매개변수, SELECT {x:UInt64} 같은 매개변수화 쿼리용. Custom 플래그(0x02)가 설정된 Setting과 동일하게 인코딩되고, 같은 방식으로 빈 키로 종결돼요.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 설명 (Description) |
|---|---|---|---|---|
| 1 | key | String | client | 매개변수 이름. 빈 값 = 목록 끝. |
| 2 | flags | VarUInt | client | 항상 0x02 (Custom) |
| 3 | value | String | client | 문자열로 된 매개변수 값. 아래 인용 참고 참조. |
주의
매개변수 값은 값의 SQL 표현이지, 원시 리터럴이 아니에요. String 타입 매개변수는 이미 단일 인용된 채 전달돼야 해요(예:
{name:String}의 값은 Alice가 아니라 'Alice'). 그렇지 않으면 서버의 값 파서가 거부해요.
Data (패킷 타입 1 서버→클라이언트, 패킷 타입 2 클라이언트→서버)
양방향. 결과 블록, INSERT 데이터, 외부 테이블, 종료-데이터 마커를 나르지요.
와이어 포맷은 대칭이에요 — 양방향 모두 Block 앞에 table_name 접두사를 포함해요. 패킷 타입 바이트만 다를 뿐이에요.
[VarUInt: packet_type] 1 (서버→클라이언트) 또는 2 (클라이언트→서버)
[String: table_name] 외부 테이블 이름; 대부분의 경우 빈 값
[Block] Block 레이아웃은 Native 포맷 명세 참조
| 필드 (Field) | 타입 (Type) | 역할 (Role) | 설명 (Description) |
|---|---|---|---|
| table_name | String | universal | 외부 테이블 이름. 빈 값("")이 일반적인 경우 — 메인 테이블, 쿼리 결과, INSERT 행 스트림용. 빈 table_name만으로는 종료-데이터 마커가 아니에요(보통 INSERT 행 패킷도 ""를 나르지요). 비어 있지 않은 이름은 한 쿼리의 클라이언트 Data 패킷에서 반복될 수 있어요: 주어진 이름의 첫 패킷이 외부 테이블을 만들고 스키마를 묶고(컬럼은 있지만 num_rows = 0인 패킷도 포함), 그 이름의 각 이후 패킷은 같은 순서로 같은 컬럼 이름과 타입을 선언해야 해요 — 서버는 블록 헤더를 묶인 스키마와 비교하고 불일치 시 Exception(INCORRECT_DATA)으로 응답해요. |
| Block body | — | — | Block & column structure 참조. |
종료-데이터 마커는 Block이 빈 — 컬럼 0, 행 0 — 패킷이에요, table_name과 무관하게. 서버는 클라이언트 Data 패킷을 디코딩된 블록이 빈 경우만(block.empty()) 종결자로 취급해요. table_name = ""이고 비어 있지 않은 블록을 가진 패킷은 보통 행 패킷이지 종결자가 아니에요. 따라서 INSERT 행 스트림은 비어 있지 않은 Data 블록 시퀀스 뒤에 그것을 끝내는 빈 Data 블록 하나예요.
블록 변형과 그 의미는 Block variants 아래 문서화돼 있어요.
Progress (패킷 타입 3)
Server → Client. 쿼리 실행 중 주기적으로 보내져요. 모든 필드는 VarUInt이고, 각 패킷은 누적 총계가 아니라 이전 Progress 패킷 이후의 증분을 나르지요. 보내기 전에 서버는 카운터를 읽고 원자적으로 0으로 재설정하며, elapsed_ns를 마지막 전송 이후의 시간 델타로 계산해요. 따라서 클라이언트는 실행 총계를 얻기 위해 후속 패킷을 로컬에서 반드시 누적해야 해요 — 한 패킷을 절댓값으로 취급하면 패킷이 두 개 이상 도착했을 때 진행 표시가 뒤로 점프하거나 과소 계산돼요.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 조건 (Condition) | 설명 (Description) |
|---|---|---|---|---|---|
| 1 | rows | VarUInt | universal | always | 이전 패킷 이후 읽은 행(실행 총계에 더할 값) |
| 2 | bytes | VarUInt | universal | always | 이전 패킷 이후 읽은 바이트(실행 총계에 더할 값) |
| 3 | total_rows | VarUInt | universal | always | 읽을 추정 총 행에 대한 증분; 누적 (주어진 패킷에서 0일 수 있음) |
| 4 | total_bytes | VarUInt | universal | TOTAL_BYTES_IN_PROGRESS (v54463) | 읽을 추정 총 바이트에 대한 증분; 누적. 와이어에서 total_rows와 wrote_rows 사이에 앉음. |
| 5 | wrote_rows | VarUInt | universal | WRITE_CLIENT_INFO (v54420) | 이전 패킷 이후 작성된 행(INSERT용); 누적 |
| 6 | wrote_bytes | VarUInt | universal | WRITE_CLIENT_INFO (v54420) | 이전 패킷 이후 작성된 바이트(INSERT용); 누적 |
| 7 | elapsed_ns | VarUInt | universal | SERVER_QUERY_TIME_IN_PROGRESS (v54460) | 이전 패킷 이후 경과한 나노초(델타, 총 쿼리 시간이 아님); 누적 |
ProfileInfo (패킷 타입 6)
Server → Client. 쿼리당 한 번, 실행 끝 근처에 보내져요.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 조건 (Condition) | 설명 (Description) |
|---|---|---|---|---|---|
| 1 | rows | VarUInt | universal | always | 처리된 총 행 |
| 2 | blocks | VarUInt | universal | always | 처리된 총 블록 |
| 3 | bytes | VarUInt | universal | always | 처리된 총 바이트 |
| 4 | applied_limit | Bool | universal | always | LIMIT 절이 적용되었는지 여부 |
| 5 | rows_before_limit | VarUInt | universal | always | LIMIT 전 행 수 |
| 6 | obsolete_ | Bool | universal | always | 폐기된 호환 바이트. 서버는 여기에 항상 true를 쓰고 클라이언트는 읽을 때 버려요. "rows_before_limit 이 계산됐음" 플래그가 아니에요. 의미 있는 제한 상태는 필드 4(applied_limit)와 필드 5입니다. 읽고 무시하세요. |
| 7 | applied_aggregation | Bool | universal | ROWS_BEFORE_AGGREGATION (v54469) | GROUP BY가 적용되었는지 여부 |
| 8 | rows_before_aggregation | VarUInt | universal | ROWS_BEFORE_AGGREGATION (v54469) | 집계 전 행 수 |
Totals (패킷 타입 7)
Server → Client. WITH TOTALS가 있는 쿼리에 보내져요. 와이어 포맷은 Data와 동일해요: 항상 빈 table_name 문자열 뒤 Block. 패킷 타입 바이트만 다를 뿐이에요.
[VarUInt: 7] packet type
[String: table_name] always empty
[Block] Native 포맷 명세 참조
Extremes (패킷 타입 8)
Server → Client. extremes 설정이 활성화되면 보내져요. 와이어 포맷은 Data와 동일해요. 블록은 정확히 2행이에요: 행 0이 각 컬럼의 최소값, 행 1이 최대값.
[VarUInt: 8] packet type
[String: table_name] always empty
[Block] num_rows = 2
Log (패킷 타입 10)
Server → Client. 쿼리가 활성 로그 큐를 가질 때 보내져요(send_logs_level 설정; 로그 스트리밍 참조).
Data와 같은 봉투와 본문 포맷. 블록은 고정 num_columns = 8과 미리 정의된 스키마를 가져요. 각 로그 줄이 8개 컬럼 전체에 걸친 행 하나이고, 단일 Log 패킷은 많은 행을 담을 수 있어요.
[VarUInt: 10] packet type
[String: table_name] always empty
[Block] num_columns = 8, num_rows = 로그 줄 수
8개 컬럼, 정확히 이 순서:
| # | 이름 (Name) | 타입 (Type) | 설명 (Description) |
|---|---|---|---|
| 1 | event_time | DateTime | 이벤트 타임스탬프 (epoch 이후 초) |
| 2 | event_time_microseconds | UInt32 | 마이크로초 성분 |
| 3 | host_name | String | 로그를 내보내는 서버 호스트네임 |
| 4 | query_id | String | 로그가 속한 쿼리 ID |
| 5 | thread_id | UInt64 | OS 스레드 ID |
| 6 | priority | Int8 | 로그 수준 (Poco 우선순위: 1 = Fatal, … 8 = Trace, 9 = Test) |
| 7 | source | String | 로거 이름 |
| 8 | text | String | 로그 메시지 텍스트 |
ProfileEvents (패킷 타입 14)
Server → Client. 쿼리별 성능 카운터를 나르지요.
Data와 같은 봉투와 본문 포맷. 블록은 고정 num_columns = 6과 미리 정의된 스키마를 가져요. 각 이벤트는 행 하나예요.
[VarUInt: 14] packet type
[String: table_name] always empty
[Block] num_columns = 6, num_rows = 이벤트 수
6개 컬럼:
| # | 이름 (Name) | 타입 (Type) | 설명 (Description) |
|---|---|---|---|
| 1 | host_name | String | 서버 호스트네임 |
| 2 | current_time | DateTime | 이벤트 타임스탬프 |
| 3 | thread_id | UInt64 | 스레드 ID |
| 4 | type | Enum8 | 이벤트 타입: 1 = Increment(카운터), 2 = Gauge. 밑바탕 저장은 부호 있는 바이트 하나. |
| 5 | name | String | 이벤트 이름 (예: "Query", "NetworkReceiveBytes") |
| 6 | value | Int64 | 카운터 값 또는 gauge 판독 |
주의
value 컬럼의 요소 타입은 패킷 간 고정되지 않아요 — 오래된 서버는 UInt64, 새 서버는 Int64를 내보내요. 하나의 너비를 가정하지 말고 블록 헤더에서 컬럼의 타입 문자열을 읽으세요.
TableColumns (패킷 타입 11)
Server → Client, COLUMN_DEFAULTS_METADATA(v54410)로 게이팅. 서버는 INSERT 스키마 블록 전에 컬럼 기본값 메타데이터를 나르기 위해 보내는데, 협상된 버전이 ≥ 54410 이고 input_format_defaults_for_omitted_fields 설정이 활성화일 때만. 54410 미만에서는 패킷이 결코 전송되지 않으므로 오래된 클라이언트는 그것을 기다리면 안 돼요 — 스키마 Data 블록이 직접 와요. v54410+ 클라이언트는 어느 순서든 준비해야 해요: 선택적 TableColumns, 그 다음 스키마 블록.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 설명 (Description) |
|---|---|---|---|---|
| 1 | external_table | String | universal | 외부 테이블 이름. 빈 값 = 메인 테이블. |
| 2 | columns_description | String | universal | 텍스트 컬럼 정의, 예: "id Int32, name String DEFAULT ''". 자유 형식 텍스트 — 문자열로 파싱. |
v54481+의 압축된 본문
협상된 버전 ≥ 54481(COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS)에서 서버는 두 필드를 같은 선택적 압축 출력 경로로 작성해요. 따라서 쿼리에 compression = true가 있으면 전체 TableColumns 본문(external_table + columns_description)이 압축 프레임 안에 있고, 클라이언트는 matching 압축 해제 스트림을 통해 읽어요. 쿼리에 압축이 없으면 본문이 위 표가 보여주는 대로 압축되지 않은 채 와이어에 있어요. 이것은 INSERT 스키마 응답에 중요해요: Log와 ProfileEvents에는 압축 처리를 바꾸지만 TableColumns에는 바꾸지 않는 클라이언트는 쿼리 압축이 활성화될 때 응답을 잘못 읽어요.
TimezoneUpdate (패킷 타입 17)
Server → Client, TIMEZONE_UPDATES(v54464)로 게이팅. 정확히 한 곳에서 보내져요: 입력 테이블 함수의 초기화자(클라이언트에서 행을 스트리밍하는 INSERT INTO <t> SELECT ... FROM input('') 형태의 쿼리). 서버가 입력-스키마 Data 블록(INSERT 단계 참조)을 보낸 직후, 쿼리 컨텍스트의 현재 session_timezone을 나르는 TimezoneUpdate를 내보내서 클라이언트가 곧 보낼 행을 같은 시간대로 파싱하게 해요. 서버는 임의의 중-쿼리 SET session_timezone 변경이나, 이후 결과 블록을 어떻게 포맷할지 알려주는 용도로는 이 패킷을 내보내지 않아요.
| # | 필드 (Field) | 타입 (Type) | 역할 (Role) | 설명 (Description) |
|---|---|---|---|---|
| 1 | timezone | String | universal | 새 세션 기본 시간대 (예: "UTC", "Europe/Berlin"). |
패킷은 입력-스키마 블록 직후, 클라이언트가 행 블록을 보내기 시작하기 전에 한 번 도착해요. TimezoneUpdate를 무시하는 디코더는 와이어를 정렬하기 위해 trailing String을 반드시 소비해야 해요.
SSH 챌린지-응답 인증 (패킷 타입 11, 12, 18)
SSH_AUTHENTICATION(v54466)으로 게이팅되고, 선택적이기만 해요. 연결은 ClientHello가 user = " SSH KEY AUTHENTICATION " + <user>(선행·후행 공백 포함)와 password = ""를 보낼 때 SSH 흐름에 들어가요. 서버는 접두사를 읽고, 벗겨 실제 사용자를 복구하고, 챌린지-응답으로 전환해요.
| 패킷 (Packet) | 코드 (Code) | 방향 (Direction) | 본문 (Body) |
|---|---|---|---|
| SSHChallengeRequest | 11 | Client → Server | (본문 없음) |
| SSHChallenge | 18 | Server → Client | String 챌린지 — 랜덤 바이트; 아래에서 서명되는 문자열의 한 성분 |
| SSHChallengeResponse | 12 | Client → Server | String 서명 — 아래 정의된 연결에 대한 SSH 서명, 원시 챌린지가 아님 |
흐름은 비밀번호 인증을 대신해 실행되고, 챌린지-응답 교환은 ServerHello 전에 일어나요 — 서버는 인증이 성공할 때까지 Hello 응답을 연기해요:
- 클라이언트가 SSH 마커 접두사와 빈 password로 ClientHello를 보내요.
- 클라이언트가 SSHChallengeRequest(패킷 11)를 보내요. 서버가 아직 ServerHello를 보내지 않았어요 — 먼저 인증을 처리하고 이 패킷을 기다리며 막혀 있어요.
- 서버가 랜덤 바이트를 나르는 SSHChallenge(패킷 18)로 응답해요.
- 클라이언트가 서명할 문자열을 만들고 그것에 서명해요(원시 챌린지가 아니라) — 그런 다음 SSHChallengeResponse(패킷 12)로 서명을 보내요. 서명된 메시지는 구분자 없이 정확히 이 순서의 네 부분을 바이트별로 연결한 것이에요:
to_sign = decimal(protocol_version) + default_database + user + challenge
| 부분 (Part) | 출처 (Source) |
|---|---|
| decimal(protocol_version) | 클라이언트의 프로토콜 버전을 십진 ASCII 문자열**로 (예: "54466") — VarUInt나 고정 너비 정수가 아닌 버전 번호의 문자열. 서버는 ClientHello에서 받은 것과 같은 프로토콜 버전을 사용해 검증. |
| default_database | ClientHello의 database 필드 (없으면 빈 문자열). |
| user | " SSH KEY AUTHENTICATION "** 마커 접두사를 벗긴 실제 사용자 이름 — 서버가 접두사를 벗긴 뒤 복구하는 것과 같은 이름. |
| challenge | SSHChallenge 패킷의 원시 챌린지 바이트. |
- 서버는 사용자의 등록 공개 키에 대해 서명을 검증하고, 같은
decimal(protocol_version) + default_database + user + challenge문자열을 재구성해요. 성공 시 ServerHello — 비밀번호 흐름과 같은 응답 — 를 보내고 핸드셰이크가 정상적으로(Addendum 등) 계속돼요. 실패 시 Exception을 반환하고 연결을 종료해요. 원시 챌린지 바이트에만 서명하는 클라이언트는 인증에 실패해요.
주의
이것은 ServerHello가 ClientHello를 즉시 뒤따르는 비밀번호 핸드셰이크의 반대예요. SSH 인증 아래에서는 서명이 검증된 후까지 ServerHello가 보류되므로, SSH 챌린지-응답이 어떤 ServerHello도 보이기 전에 핸드셰이크에 인터리브돼요.
SSH 인증을 쓰지 않는 외부 클라이언트는 패킷 11, 12, 18을 결코 보지 못해요 — 사용자가 사용자 이름 접두사를 통해 명시적으로 선택하지 않는 한 와이어에서 벗어나 있어요.
MergeTreeAllRangesAnnouncementResponse (패킷 타입 14)
Client → Server, 서버 간 전용. parallel_replicas_protocol_version ≥ 8로 게이팅(VERSIONED_PARALLEL_REPLICAS_PROTOCOL 참조). 외부 클라이언트는 이 패킷을 결코 보내지 않아요.
협상된 병렬-복제 버전이 ≥ 8이면, 팔로워의 [MergeTreeAllRangesAnnouncement(패킷 타입 15, 서버→클라이언트 방향)에 대한 시작자(initiator)의 요청/응답 주기가 바뀌어요:
- 팔로워가 읽기 파이프라인을 열고 MergeTreeAllRangesAnnouncement를 시작자에게 보내요.
- 오직 announcement의 mode가 비-Default일 때만(WithOrder = 1 또는 ReverseOrder = 2, 둘 다 순서 병렬 읽기에 사용) 시작자가 MergeTreeAllRangesAnnouncementResponse로 응답해요. mode = Default = 0이면 시작자는 조용히 있고 팔로워는 기다리지 않아요 — Default 모드는 각 MergeTreeReadTaskRequest로 범위를 나눠주고 사전 parts 목록이 필요 없어요.
- 팔로워는 첫 MergeTreeReadTaskRequest(서버 패킷 16 — 팔로워→시작자로 보내지고, 시작자는 client 패킷 10 MergeTreeReadTaskResponse로 응답)를 발행하기 전에 (기대될 때) 응답을 블로킹하고, 반환된 parts 목록을 사용해 소스 구성을 정확히 자체
#split_i스트림이 소유하는 parts로 필터링해요.
버전 8 미만에서는 announcement가 모드와 무관하게 fire-and-forget이고, 팔로워는 모든 로컬 알려진 part에 대해 소스를 구성해요(레거시 동작).
본문 (Body)
| # | 필드 (Field) | 타입 (Type) | 설명 (Description) |
|---|---|---|---|
| 1 | version | Int64 (리틀 엔디언) | 보내는 쪽의 병렬-복제 프로토콜 버전. 수신자의 TCP 개정판이 ≥ DBMS_MIN_REVISION_WITH_VERSIONED_PARALLEL_REPLICAS_PROTOCOL(54471)이면 DBMS_PARALLEL_REPLICAS_PROTOCOL_VERSION(현재 8)과 같음. 그렇지 않으면 DBMS_MIN_SUPPORTED_PARALLEL_REPLICAS_PROTOCOL_VERSION(3)으로 폴백. 수신자는 DBMS_MIN_SUPPORTED_PARALLEL_REPLICAS_PROTOCOL_VERSION 미만의 어떤 값도 거부. |
| 2 | parts | RangesInDataPartsDescription | 조정자가 announcement의 스트림에 등록한 part의 권위 있는 집합. 빈 목록은 스트림이 조정자에 존재하지 않음을 뜻함(예: 팔로워가 시작자가 만든 것보다 더 많은 split을 과도하게 알림). 그 스트림의 팔로워 풀은 즉시 완료로 표시. |
| 3 | stream_id | String | 이 응답이 답하는 announcement의 stream_id를 반향(split 토폴로지가 작용 중이면 테이블 이름 + #split_i 접미사). |
RangesInDataPartsDescription 본문
| # | 필드 (Field) | 타입 (Type) | 설명 (Description) |
|---|---|---|---|
| 1 | count | VarUInt | 뒤따르는 part 기술자 수. 디코더는 100'000'000'000 이상의 값을 잘못된 것으로 거부. |
| 2 | parts | RangesInDataPartDescription, count 번 반복 | 기술자들, 조정자의 등록 순서대로. |
RangesInDataPartDescription 본문
| # | 필드 (Field) | 타입 (Type) | 게이트 (Gate) | 설명 (Description) |
|---|---|---|---|---|
| 1 | info | MergeTreePartInfo | universal | part 정체성 (파티션, 블록 범위, 레벨, mutation). |
| 2 | ranges | MarkRanges | universal | info 안에서 이 스트림이 제공할 수 있는 mark 범위. 빈 목록은 part가 등록됐지만 현재 할당된 작업이 없음을 뜻함. |
| 3 | rows | VarUInt | universal | ranges가 덮는 총 행. |
| 4 | projection_name | String | DBMS_PARALLEL_REPLICAS_MIN_VERSION_WITH_PROJECTION (PR v5) | 주 part 행에 대해서는 빈 값; 그 외엔 projection의 이름. |
| 5 | min_marks_per_task | VarUInt | DBMS_PARALLEL_REPLICAS_MIN_VERSION_WITH_MIN_MARKS_PER_TASK (PR v6) | 팔로워 풀이 이 part에 대해 단일 읽기 작업으로 묶어야 하는 mark의 하한. |
MergeTreePartInfo 본문
| # | 필드 (Field) | 타입 (Type) | 설명 (Description) |
|---|---|---|---|
| 1 | version | Int64 (리틀 엔디언) | 항상 DBMS_MERGE_TREE_PART_INFO_VERSION (1). 디코더는 다른 어떤 값도 거부. |
| 2 | partition_id | String | 파티션 식별자 (예: 비파티션 테이블의 "all", 또는 파티션-키 튜플 표현의 문자열화된 값). |
| 3 | min_block | Int64 (리틀 엔디언) | part의 블록 범위에서 첫 블록 번호. |
| 4 | max_block | Int64 (리틀 엔디언) | part의 블록 범위에서 마지막 블록 번호(포함). |
| 5 | level | UInt32 (리틀 엔디언) | 병합 레벨. |
| 6 | mutation | Int64 (리틀 엔디언) | 이 part를 만든 mutation 버전 (변이되지 않으면 0). |
| 7 | use_legacy_max_level | Bool (텍스트) | 단일 ASCII 바이트('1' 또는 '0')로 인코딩 — part-이름 형식의 역사적 호환 플래그. |
MarkRanges 본문
| # | 필드 (Field) | 타입 (Type) | 설명 (Description) |
|---|---|---|---|
| 1 | size | UInt64 (리틀 엔디언) | 뒤따르는 mark-범위 쌍 수. 주의: 리틀 엔디언 고정 너비, VarUInt가 아님. |
| 2 | ranges | size 번 반복의 (UInt64 begin, UInt64 end), 각각 리틀 엔디언 | 반개방 [begin, end) mark 구간. |
패킷 타입 참조 (Packet type reference)
Client → Server
| 코드 (Code) | 이름 (Name) | 본문 형식 (Body format) | 설명 (Description) |
|---|---|---|---|
| 0 | Hello | ClientHello | 핸드셰이크 시작 |
| 1 | Query | Query | 쿼리 실행 요청 |
| 2 | Data | Data | 데이터 블록 (INSERT 데이터, 외부 테이블, 종료-데이터 마커) |
| 3 | Cancel | (본문 없음) | 실행 중인 쿼리 취소 |
| 4 | Ping | Ping | 활성 검사 |
| 5 | TablesStatusRequest | 명세되지 않음 | 테이블 상태 검사 |
| 6 | KeepAlive | 명세되지 않음 | 연결 keepalive |
| 7 | Scalar | 명세되지 않음 | 스칼라 데이터 블록 |
| 8 | IgnoredPartUUIDs | 명세되지 않음 | 쿼리에서 제외할 parts |
| 9 | ReadTaskResponse | 명세되지 않음 | S3 클러스터 읽기 응답 |
| 10 | MergeTreeReadTaskResponse | 명세되지 않음 | 병렬 읽기 작업 응답 |
| 11 | SSHChallengeRequest | SSH 인증 | SSH 인증 챌린지 요청 |
| 12 | SSHChallengeResponse | SSH 인증 | SSH 인증 챌린지 응답 |
| 13 | QueryPlan | QueryPlan | 쿼리 플랜 |
| 14 | MergeTreeAllRangesAnnouncementResponse | MergeTreeAllRangesAnnouncementResponse | 팔로워의 MergeTreeAllRangesAnnouncement에 대한 시작자의 응답 (parallel_replicas_protocol_version ≥ 8로 게이팅 — VERSIONED_PARALLEL_REPLICAS_PROTOCOL 참조). 서버 간 전용 — 외부 클라이언트는 결코 보내지 않음. |
Server → Client
| 코드 (Code) | 이름 (Name) | 본문 형식 (Body format) | 설명 (Description) |
|---|---|---|---|
| 0 | Hello | ServerHello | 핸드셰이크 응답 |
| 1 | Data | Data | 결과 데이터 블록 |
| 2 | Exception | Exception | 오류 |
| 3 | Progress | Progress | 쿼리 실행 진행 |
| 4 | Pong | Pong | 활성 응답 |
| 5 | EndOfStream | (본문 없음) | 쿼리 완료 |
| 6 | ProfileInfo | ProfileInfo | 실행 후 프로파일링 데이터 |
| 7 | Totals | Totals | GROUP BY WITH TOTALS 행 |
| 8 | Extremes | Extremes | 최소/최대 값 (2행 블록) |
| 9 | TablesStatusResponse | 명세되지 않음 | 테이블 상태 응답 |
| 10 | Log | Log | 쿼리 실행 로그 줄 |
| 11 | TableColumns | TableColumns | 기본값용 컬럼 설명 |
| 12 | PartUUIDs | 명세되지 않음 | 고유 part ID |
| 13 | ReadTaskRequest | 명세되지 않음 | 클러스터 읽기 작업 요청 |
| 14 | ProfileEvents | ProfileEvents | 성능 카운터 |
| 15 | MergeTreeAllRangesAnnouncement | 명세되지 않음 | 병렬 읽기 초기화 |
| 16 | MergeTreeReadTaskRequest | 명세되지 않음 | 병렬 읽기 작업 할당 |
| 17 | TimezoneUpdate | TimezoneUpdate | 서버 시간대 갱신 |
| 18 | SSHChallenge | SSH 인증 | SSH 인증 챌린지 |
QueryPlan
QueryPlan은 서버 간 전용 클라이언트 패킷이에요. stage가 QueryPlan(7)인 Query를 뒤따르고 직렬화된 플랜 스트림 하나를 담아요.
| # | 필드 (Field) | 타입 (Type) | 게이트 (Gate) | 설명 (Description) |
|---|---|---|---|---|
| 1 | serialization_version | VarUInt | universal | 쿼리-플랜 직렬화 버전, ServerHello에서 광고된 수신자의 query_plan_serialization_version보다 크지 않아야 함. |
| 2 | max_threads | VarUInt | 직렬화 버전 ≥ 10 | 플랜 수준 스레드 제한. 0 = 플랜 특유 제한 없음. |
| 3 | concurrency_control | Bool | 직렬화 버전 ≥ 10 | 플랜이 동시성 제어에 참여하는지 여부. |
| 4 | plan | 버전 지정 쿼리-플랜 스트림 | universal | 직렬화된 플랜 트리, 단계 설정, 헤더, 준비된 집합. 내부 레이아웃은 쿼리-플랜 직렬화 버전이 소유. |
버전 10 전에는 필드 2와 3이 없어요. 비기본 실행 제한 중 하나라도 갖는 시작자는 그 오래된 레이아웃을 보내면 안 돼요: 대신 원래 SQL 쿼리를 실어 보내므로 오래된 수신자가 쿼리 설정에서 제한을 재구성해요.
구성 (Configuration)
이 섹션은 네이티브 프로토콜 연결을 형태 짓는 조정 가능 항목들을 다뤄요:
- 전송 계층 설정 — TCP 소켓 옵션과 시간초과. TCP 연결 자체가 어떻게 행동하는지에 영향을 줘요.
- 애플리케이션 계층 설정 — Query 패킷의 설정 목록에 나르는 쿼리별 조정 항목. 서버가 와이어로 무엇을 보내는지 또는 어떻게 프레이밍되는지에 영향을 줘요.
- 범위 밖 설정 — 종종 프로토콜 설정으로 혼동되지만 실제로 SQL 실행이나 저장을 제어하는 설정.
아래 기본값은 최근 서버 릴리스를 반영해요. 버전과 배포에 따라 다를 수 있어요.
전송 계층 설정
소켓 옵션
| 옵션 (Option) | 기본값 (Default) | 쪽 (Side) | 설명 (Description) |
|---|---|---|---|
| TCP_NODELAY | on | both | Nagle 알고리즘 비활성. 작은 패킷이 즉시 전송. |
| SO_KEEPALIVE | on (클라이언트), OS 기본 (서버) | 비대칭 | 커널 수준 TCP keepalive 프로브. 클라이언트는 tcp_keep_alive_timeout > 0일 때 명시적으로 활성화. 서버는 OS 기본을 상속. |
| SO_RCVBUF / SO_SNDBUF | OS 기본 | — | 소켓 버퍼 크기. 프로토콜이 조정하지 않음. |
시간초과
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 쪽 (Side) | 설명 (Description) |
|---|---|---|---|---|
| connect_timeout | 10 | 초 | client | 초기 TCP 연결을 맺기 위한 시간초과. |
| handshake_timeout_ms | 10000 | 밀리초 | client | 핸드셰이크 중 ServerHello를 받기 위한 시간초과. |
| send_timeout | 300 | 초 | both | 이 구간 내에 어떤 바이트도 쓸 수 없으면 연결이 던져요. |
| receive_timeout | 300 | 초 | both | 이 구간 내에 어떤 바이트도 읽을 수 없으면 연결이 던져요. |
| tcp_keep_alive_timeout | 290 | 초 | client | OS가 첫 TCP keepalive 프로브를 보내기 전의 유휴 시간. |
| receive_data_timeout_ms | 2000 | 밀리초 | client | 복제본에서 첫 Data 패킷을 받기 위한 시간초과. |
| connect_timeout_with_failover_ms | 1000 | 밀리초 | client | 복제본을 반복할 때 시도별 연결 시간초과. |
| connect_timeout_with_failover_secure_ms | 1000 | 밀리초 | client | TLS 위에서 복제본을 반복할 때 시도별 연결 시간초과. |
| hedged_connection_timeout_ms | 50 | 밀리초 | client | 헤지드 요청의 시도별 연결 시간초과. |
| poll_interval | 10 | 초 | server | 서버의 유휴-연결 및 종료 검사 루프의 세분성. |
시간초과는 이렇게 중첩돼요:
tcp_keep_alive_timeout (290s)
< receive_timeout (300s)
< idle_connection_timeout (3600s)
< tcp_close_connection_after_queries_seconds (0 = 기본 무제한)
OS keepalive가 먼저 발화하고 커널 수준에서 죽은 피어를 조용히 감지할 수 있어요. 애플리케이션 수신 시간초과가 다음 방어선이에요. 유휴 시간초과는 오래 사용하지 않은 연결을 정리하는 마지막 수단이에요.
연결 제한
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 쪽 (Side) | 설명 (Description) |
|---|---|---|---|---|
| max_connections | 4096 | 개수 | server | 최대 동시 TCP 연결. |
| idle_connection_timeout | 3600 | 초 | server | 유휴 연결이 열려 있을 수 있는 최대 시간. |
| tcp_close_connection_after_queries_num | 0 (무제한) | 개수 | server | 강제 종료 전에 연결당 최대 쿼리 수. |
| tcp_close_connection_after_queries_seconds | 0 (무제한) | 초 | server | 활동과 무관한 최대 총 연결 수명. |
정기적으로 쿼리를 내는 연결은 무기한 살 수 있어요. 유휴 연결만 한 시간 후에 정리되고, 기본 최대 수명은 없어요.
애플리케이션 계층 설정
이 설정들은 쿼리별로 Query 패킷의 설정 목록에서 이동해요. 서버가 와이어로 보내는 것을, 또는 어떻게 프레이밍되는지를 바꿔요.
압축
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 설명 (Description) |
|---|---|---|---|
| network_compression_method | "ZSTD" | 문자열 | Query 패킷의 compression 플래그가 설정됐을 때 사용되는 압축 코덱. 값: "LZ4", "LZ4HC", "ZSTD", "NONE". |
| network_zstd_compression_level | 3 | 1–15 | network_compression_method == "ZSTD"일 때 ZSTD 레벨. |
Query 패킷(필드 6)의 compression 플래그가 압축을 켜고 끄고, 이 설정들은 켜졌을 때 어떤 코덱을 쓸지 선택해요.
로그 스트리밍
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 설명 (Description) |
|---|---|---|---|
| send_logs_level | "fatal" | 문자열 | 최소 로그 수준. 값: "none", "fatal", "error", "warning", "information", "debug", "trace", "test". |
| send_logs_source_regexp | "" | 문자열 | 로거 소스에 대한 정규식 필터. 빈 값 = 모든 소스 통과. |
send_logs_level을 "none"이 아닌 다른 값으로 설정하면 서버가 쿼리 실행 중 Log 패킷을 내보내요.
진행 보고
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 설명 (Description) |
|---|---|---|---|
| interactive_delay | 100000 | 마이크로초 | 연속 Progress 패킷 사이의 목표 최소 간격. |
이것은 target minimum이지 엄격한 최대가 아니에요: 쿼리가 작업을 충분히 빨리 만들지 못하면 서버가 Progress 패킷을 덜 자주 보낼 수 있어요.
결과 봉투
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 설명 (Description) |
|---|---|---|---|
| extremes | false | bool | true일 때, 서버가 컬럼별 최소/최대 값을 가진 Extremes 패킷을 보내요. |
| max_result_rows | 0 (무제한) | 개수 | 전송된 행 상한. 동작은 result_overflow_mode가 제어. |
| max_result_bytes | 0 (무제한) | 압축되지 않은 바이트 | 압축되지 않은 바이트 양 상한. 동작은 result_overflow_mode가 제어. |
| result_overflow_mode | "throw" | 문자열 | "throw"는 스트림을 Exception으로 끝냄; "break"는 부분 결과 다음에 EndOfStream을 보냄. |
비동기 INSERT
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 설명 (Description) |
|---|---|---|---|
| async_insert | true | bool | true일 때, INSERT 데이터가 서버 측에서 큐에 넣어지고 배치됨. |
| wait_for_async_insert | true | bool | (async_insert 켜짐으로) true일 때, 서버는 큐에 넣어진 데이터가 플러시될 때까지 응답을 붙잡음. |
| wait_for_async_insert_timeout | 120 | 초 | 서버가 플러시를 기다렸다가 반환하는 최대 시간. |
분산 트레이싱
| 설정 (Setting) | 기본값 (Default) | 단위 (Unit) | 설명 (Description) |
|---|---|---|---|
| opentelemetry_start_trace_probability | 0.0 | 0–1 확률 | 응답 텔레메트리에 OpenTelemetry 컨텍스트를 붙일 서버 측 확률. |
범위 밖 설정
이 설정들은 가끔 프로토콜 수준 설정으로 오인되지만, 와이어 동작이 아니라 SQL 실행, 저장, 또는 CPU 사용을 제어해요. 프로토콜 구현이 특별히 처리할 필요가 없어요.
max_threads— 쿼리 실행 내 병렬성.max_memory_usage— 쿼리별 메모리 상한.max_block_size,preferred_block_size_bytes— 쿼리 처리 중 내부 블록 크기 조정; 와이어 블록은 이것들과 독립적이에요.compile_expressions— JIT 컴파일; CPU 전용.async_insert_max_data_size— 서버 측 큐 버퍼.input_format_native_*/output_format_native_*패밀리를 제외한 모든input_format_*및output_format_*설정 — 비네이티브 것들은 다른 포맷(예: HTTP 위)을 선택하거나 조정하고 네이티브 프로토콜의 Data 블록을 바꾸지 않아요.
*_native_* 설정은 예외예요: 네이티브 TCP Data 블록 내부의 바이트를 바꾸므로 프로토콜 구현이 그것을 고려해야 해요. output_format_native_encode_types_in_binary_format은 컬럼 타입 필드를 텍스트 문자열에서 이진 타입 인코딩으로 바꾸고, output_format_native_write_json_as_string은 JSON 컬럼을 String으로 내보내며, output_format_native_use_flattened_dynamic_and_json_serialization은 FLATTENED Dynamic/JSON 레이아웃을 선택해요. 이것들은 블록 본문에 영향을 주지 패킷 봉투에 영향을 주지 않으므로 Native 포맷 명세에 명세돼요.
용어집 (Glossary)
- Cancel — 실행 중인 쿼리를 중단하는 클라이언트 시작 패킷(타입 3). 이 페이지에서 상세히 명세되지 않음.
- 클라이언트 데이터 끝 마커 (End-of-client-data marker) — 클라이언트가 입력 스트림을 닫기 위해 보내는 빈 Data 패킷(0 컬럼, 0 행). 위치는 쿼리 종류에 따라 달라요:
- 일반 쿼리(SELECT** 등): "더 이상 외부 데이터 없음"을 알리기 위해 Query 패킷과 외부 테이블 Data 패킷 뒤에 보냄. 서버는 그때 실행을 시작해요.
- INSERT: 클라이언트는 사전-스키마 마커를 보내지 않아요. 서버가 스키마 블록을 먼저 보내고, 클라이언트가 행 Data 블록을 스트리밍하고, 그런 다음에만 행 스트림을 종결하는 빈 Data 패킷을 보내요. 스키마 블록 전에 빈 마커를 보내면 즉시 행-끝으로 읽혀 데이터를 잃어요.
- 기능 (Feature) — 특정 프로토콜 버전에서 도입된 와이어-포맷 변경. 협상된 버전이 기능의 버전 이상일 때 활성. 버전 지정과 기능 게이트 참조.
- 서버 간 (Inter-server) — 서버 간 분산 쿼리에서만 의미 있는 필드에 대한 역할 라벨. 외부 클라이언트는 기본값(보통 빈 문자열, 0, 또는 false)을 작성.
- 협상된 버전 (Negotiated version) — min(client_version, server_version), 핸드셰이크 중 계산. 연결 수명 동안 어떤 기능이 활성인지 결정.
- 패킷 (Packet) — 와이어 메시지: VarUInt 패킷 타입 코드 뒤에 타입에 따라 형식이 달라지는 본문. 패킷 봉투 참조.
- 패킷 타입 코드 (Packet type code) — 패킷의 선두 VarUInt로서 그 형식을 식별. 값 0–18이 현재 할당됨. 패킷 타입 참조 참조.
- 응답 스트림 (Response stream) — 서버가 쿼리 중 내보내는 패킷 시퀀스. 길이가 열려 있고, 정확히 하나의 EndOfStream(성공) 또는 Exception(실패)으로 종결. Query 단계 참조.
- 스키마 블록 (Schema block) — 서버가 INSERT 단계 중 클라이언트가 데이터를 보내기 전에 예상 컬럼 형태를 알리기 위해 보내는 헤더 블록(컬럼은 있지만 0 행인 Block).
- 설정 목록 (Settings list) — Query 본문의 (key, flags, value) 튜플 시퀀스, 빈 키로 종결. 쿼리별 애플리케이션 계층 구성을 나름. Setting 참조.
- 단계 (Stage) — Query 패킷(필드 5)의 VarUInt 필드로서 서버가 쿼리를 얼마나 멀리 실행할지 제어. 외부 클라이언트는 보통 2(Complete)를 보냄. 분산 쿼리와 직렬화된 쿼리 플랜은 더 높은 값을 사용. 전체 와이어 값 집합은 Query 필드 5 참조.
- Terminator — 스트림을 끝내는 패킷. Query 응답은 EndOfStream(성공) 또는 Exception(실패)으로 끝남. 클라이언트의 입력 스트림은 빈 Data 마커로 끝남.