프레이밍 포맷

프레이밍 포맷 (Framing formats)

프레이밍 포맷은 쿼리의 서로 다른 응답 부분(데이터 청크, totals, extremes, 진행 패킷, 프로파일 이벤트, 서버 로그)을 단일 스트림으로 다중화해요. 이를 통해 HTTP 프로토콜에서 풍부한 데이터 교환이 가능해요.

출처: 문서

본문

프레이밍 포맷은 쿼리의 서로 다른 응답 부분을 단일 스트림에서 다중화해요. 데이터 청크, totals와 extremes, 진행 패킷, 프로파일 이벤트(메트릭), 서버 로그 — 네이티브 프로토콜이 지원하는 모든 것. 이렇게 해서 HTTP 프로토콜에서 풍부한 데이터 교환이 가능해요.

프레이밍 포맷은 출력 포맷과 독립적이에요. 어떤 출력 포맷이든 생성하는 바이트를 캡슐화하는데, 그 청크들을 분리하고 잠재적으로 인코딩해요. 모든 data, totals, extremes 패킷의 페이로드를 이어 붙이면, 프레이밍 없이 출력 포맷이 생성했을 것과 정확히 같아요. 보조 패킷(진행, 로그, 프로파일 이벤트, 예외)은 JSON으로 표현돼요.

프레이밍은 출력 포맷을 더 표현력 있게 만들 수도 있어요 — 이것이 위 규칙의 유일한 의도적 예외예요. JSONCompactEachRow 계열 포맷은 평문 출력에서 totals와 extremes를 버려요. 그 행들이 일반 데이터 행과 구분되지 않기 때문이에요. 프레이밍 포맷 아래에서는 패킷 종류가 그것들을 구분해 주므로, 이 포맷들은 totals와 extremes 행을 (보통의 행 구문으로) totalsextremes 패킷으로 내보내요. 이 포맷들의 경우 data 패킷의 페이로드만 이어 붙이면 프레이밍 없이 출력 포맷이 생성했을 것과 정확히 같으며, totalsextremes 패킷은 프레이밍되지 않은 출력에는 없는 추가 행을 담고 있어요. 그래서 그런 스트림에서 프레이밍되지 않은 출력을 재구성하는 클라이언트는 data 페이로드만 이어 붙여야 해요.

프레이밍 포맷은 쿼리 레벨 설정인 framing_output_format으로 선택돼요. 현재는 HTTP 프로토콜에 적용되며 다른 인터페이스에서는 무시돼요.

send_logs_level 설정이 설정되어 있으면 서버 로그가 패킷으로 포함돼요. send_profile_events 설정이 활성화되어 있으면(기본값) 프로파일 이벤트가 포함돼요. 진행 및 프로파일 이벤트 패킷은 interactive_delay 마이크로초마다 최대 한 번 전송돼요.

성공적인 스트림은 네이티브 프로토콜의 마지막 진행 패킷처럼, 최종 카운터(result_rows, result_bytes, memory_usage)를 담은 마지막 progress 패킷으로 끝나요. 이 카운터는 쿼리가 끝난 후에만 알 수 있으므로, 그 이전의 어떤 progress 패킷도 그것들을 담지 않아요. 마지막 progress 패킷은 쿼리 종료 로깅이 내보낸 후행 logprofile_events 패킷(예: "peak memory usage" 로그 항목) 다음에 쓰여지므로, 정말로 스트림의 마지막 패킷이 돼요. 실패 시에는 exception 패킷이 대신 마지막 패킷이 되고, 최종 카운터 progress 패킷은 전혀 쓰이지 않아요 — 그것은 스트림의 성공 종결자예요 — 쿼리 자체가 끝난 후에 실패가 발생했고 최종 카운터를 이미 알고 있더라도 마찬가지예요(예: 쿼리 로그를 작성하는 중 실패).

이 스트림의 꼬리가 system.query_logQueryFinish 항목이 기록된 후에 쓰이기 때문에, 쿼리의 네트워크 전송 프로파일 이벤트(NetworkSendBytes, NetworkSendElapsedMicroseconds)는 후행 패킷 전송과 응답 종료를 포함하지 않아요. 응답이 버퍼링될 때(http_response_buffer_size 또는 wait_end_of_query) 쿼리가 끝난 후에만 전송되는 버퍼링된 응답 본문 전송도 포함하지 않아요. 이는 네이티브 프로토콜이 후행 로그와 프로파일 이벤트를 쿼리 로그 항목 뒤에 보내는 것과 일치해요.

쿼리가 자신의 SETTINGS 절을 통해서만 활성화하는 것(프레이밍 포맷, send_logs_level, send_profile_events)은 쿼리가 파싱될 때까지 알 수 없으므로, 해당 로그와 프로파일 이벤트는 쿼리 실행 시점부터만 캡처돼요. 파싱, 계획, 분석 단계의 로그와 프로파일 이벤트는 설정이 세션이나 URL에서 오는 경우에만 캡처돼요. 특히, 파이프라인 실행 전인 분석 단계에서 실패하는 쿼리(예: 알 수 없는 테이블 참조)가 send_logs_level을 자기 SETTINGS 절에서만 활성화하면, 분석 단계 로그가 아닌 exception 패킷만 전달해요. 그것들을 캡처하려면 세션이나 URL에 send_logs_level을 설정하세요.

같은 늦은 발견 주의 사항이 send_logs_source_regexp에도 적용돼요. 로그 큐는 각 항목이 캡처되는 시점에 소스를 기준으로 항목을 필터링하므로, 쿼리 자신의 SETTINGS 절에만 설정된 정규식은 쿼리 실행 시점부터 효과가 있어요. 파싱, 계획, 분석 단계의 log 패킷은 설정의 세션 또는 URL 값으로 필터링되어(그곳에 설정되지 않았으면 필터링되지 않아서) 쿼리 레벨 정규식과 일치하지 않는 소스를 포함할 수 있어요. 반대로 더 좁은 세션이나 URL 정규식으로 버려진 항목은 사라졌으며 더 넓은 쿼리 레벨 정규식으로 복구되지 않아요. 전체 쿼리 수명 주기를 필터링하려면 세션이나 URL에 send_logs_source_regexp를 설정하세요.

쿼리 실행 중 예외가 발생하면 그것은 http_write_exception_in_output_format 설정과 무관하게 exception 패킷(스트림의 마지막 패킷)으로 전송되므로, 클라이언트는 항상 응답을 패킷 스트림으로 파싱할 수 있어요. 예외가 기록되면 출력 포맷은 더 이상 페이로드 바이트를 내놓지 않아요. 출력을 아무것도 생성하기 전에 실패하는 쿼리는 data 패킷을 전혀 전달하지 않고(포맷의 빈 문서 스켈레톤조차도), 스트림 중간에 실패하는 쿼리는 결합 페이로드를 실패 지점에서 잘린 채로 남기고 포맷의 접미사 없이 두어요 — 실패한 쿼리의 페이로드는 완전한 문서처럼 보여서는 안 돼요.

이에 대한 예외가 하나 있어요. 패킷 쓰기 자체가 도중에 실패하면(예: 패킷의 일부 바이트가 이미 클라이언트에 도달한 후 연결이 끊김) 프레이밍은 안전 실패(fail closed)로 끝나고 마지막 exception 패킷 없이 스트림이 종료돼요. 절반 쓰인 패킷을 다시 시도하지 않아요. 다시 내보내면 잘린 바이트 뒤에 중복이 추가되어 스트림을 손상시키기 때문이에요. 그 상황에서 클라이언트는 잘 구성된 종료 패킷이 아니라 잘린 응답과 중단된 HTTP 연결을 관찰해요. 같은 규칙이 응답 스트림 자체를 닫는 중(버퍼링된 결과 플러시, HTTP 압축 종료, 소켓 닫기)의 실패에도 적용돼요. 그 시점엔 성공 스트림의 일부 또는 전체가 이미 전선에 있으므로, 아무것도 — exception 패킷도 일반 HTTP 오류 블록도 — 추가되지 않고, 클라이언트는 잘린 응답과 중단된 연결을 관찰해요. 예외 전달 자체가 실패할 때도 적용돼요. 패킷 스트림의 어떤 부분이 생성된 후(이미 전송되었거나 여전히 서버 측 응답 버퍼 http_response_buffer_size에 있든) 마지막 exception 패킷 쓰기가 실패하면(예: 후행 로그를 비우는 중) 스트림은 역시 아무것도 추가하지 않고 종료되므로, 일반 HTTP 오류 본문이 부분 패킷 스트림에 섞이지 않아요. 보조 log, profile_events, exception 패킷의 문자열 필드를 쓰는 중 실패는 반쯤 쓰인 패킷으로도 간주되며, 그런 문자열의 마지막 바이트를 쓰는 실패도 포함해요. 그러면 스트림은 그 잘린 패킷으로 끝나고 종결자를 전혀 가지지 않아요 — exception 패킷도 최종 카운터 progress 패킷도 없어서, 종결자를 요구하는 클라이언트는 쿼리 자체가 성공했더라도 실패를 감지해요.

프레이밍 포맷은 결과 스트림을 생성하지 않는 쿼리에도 적용돼요. 성공적인 INSERT, DDL 쿼리, 또는 출력이 없는 다른 어떤 쿼리든. 그런 응답은 data 패킷을 담지 않지만, 응답의 Content-Type을 프레이밍 포맷으로 바꾸고 progress, log, profile_events 패킷을 네이티브 프로토콜과 일치하게 스트리밍해요. 스트림은 최종 카운터(예: INSERT의 경우 쓰인 행 수가 담긴 result_rowsresult_bytes)를 담은 마지막 progress 패킷으로 끝나요. 페이로드가 포맷되지 않으므로 출력 포맷은 그런 쿼리에는 무관하며 프레이밍된 스트림에 영향을 주지 않아요.

사용 가능한 프레이밍 포맷 (Available framing formats)

이름 설명
None 프레이밍 없음: 기본적으로 모든 것이 그대로 동작.
EventStream HTTP server-sent events (text/event-stream).
JSONEachPacketBase64 패킷당 JSON 객체 하나; 포맷된 데이터는 base64로 인코딩.
JSONEachPacketString 패킷당 JSON 객체 하나; 포맷된 데이터는 JSON 문자열에 담김.

None

기본값. 적용 가능한 모든 것(데이터, totals, extremes, 진행)을 출력 포맷으로 투명하게 라우팅하고, 적용 불가능한 것(메트릭, 로그)은 무시해요. 따라서 JSONEachRowWithProgress처럼 진행을 스스로 나타내는 포맷을 포함해 기본적으로 모든 것이 그대로 동작해요.

EventStream

패킷을 HTTP server-sent events로 프레이밍하고 응답의 Content-Typetext/event-stream; charset=UTF-8; payload=base64로 설정해요. 모든 패킷은 패킷 종류로 이름 지어진 이벤트로 전송돼요: data, totals, extremes, progress, log, profile_events, exception. 진행과 그 밖의 보조 패킷은 JSON으로 전송돼요.

Server-sent events는 줄 바꿈(캐리지 리턴 \r 포함)을 필드 구분자로 취급하는 텍스트 프로토콜이므로, 출력 포맷이 생성하는 바이트는 그대로 임베드되지 않아요. 포맷된 데이터 블록은 단일 data: 필드로 base64 인코딩되고, 이는 모든 줄 바꿈이 있는 완전히 포맷된 페이로드로 디코딩돼요. 이것이 Content-Typepayload=base64 파라미터가 말하는 바예요. data, totals, extremes 패킷의 디코딩된 페이로드를 이어 붙이면 어떤 출력 포맷이든(텍스트, 바이너리(Native, RowBinary), 또는 원시 패스스루(RawBLOB, TSVRaw)) 프레이밍 없이 출력 포맷이 생성했을 것과 바이트 단위로 정확히 같아요.

보조 JSON 패킷(progress, log, profile_events, exception)은 절대 인코딩되지 않아요. 줄 바꿈이 없는 JSON의 단일 data: 필드로 쓰여져요.

*WithProgress 출력 포맷(JSONEachRowWithProgress, JSONCompactEachRowWithProgress)은 진행을 자신의 출력의 일부인 대역 내(in-band) 행으로 씁니다. 프레이밍 포맷은 대신 진행을 별도의 progress 패킷으로 전달하므로, 이 출력 포맷들과 호환되지 않으며 그것들을 거부해요 — 프레이밍에는 기본 출력 포맷(예: JSONEachRow)을 사용하거나, *WithProgress 포맷과 함께 None 프레이밍을 사용하세요.

curl "http://localhost:8123/?framing_output_format=EventStream" -d "SELECT number FROM numbers(3) FORMAT JSONEachRow"
event: data
data: eyJudW...In0K

event: profile_events
data: [{"host_name":"localhost","current_time":"2026-07-11 00:00:00","thread_id":"0","type":"increment","name":"SelectedRows","value":"3"},{"host_name":"localhost","current_time":"2026-07-11 00:00:00","thread_id":"0","type":"increment","name":"SelectedBytes","value":"24"}]

event: progress
data: {"read_rows":"3","read_bytes":"24","total_rows_to_read":"3","result_rows":"3","result_bytes":"24","elapsed_ns":"1174415"}

EventStream은 HTTP 프로토콜과 통합되며 적용 불가능할 때 예외를 던져요.

JSONEachPacketBase64 및 JSONEachPacketString

모든 패킷은 별도 줄의 JSON 객체(줄바꿈 구분 JSON, application/x-ndjson)이며 패킷에 대한 정보를 담고 있어요. 출력 포맷이 생성하는 바이트는 data 필드에 담겨요. JSONEachPacketBase64에서는 base64로 인코딩되고(바이너리 출력 포맷에 적합), JSONEachPacketString에서는 JSON 문자열로 담겨요.

두 변형은 data 필드를 다르게 인코딩하므로, EventStream처럼 응답의 Content-Type이 그것들을 구분해 줘요. JSONEachPacketBase64application/x-ndjson; charset=UTF-8; payload=base64를, JSONEachPacketStringapplication/x-ndjson; payload=string을 설정해요. 따라서 클라이언트는 응답 메타데이터만으로 data 필드를 base64 디코딩해야 하는지 알 수 있어요. charset=UTF-8JSONEachPacketBase64만 약속해요. 페이로드 바이트와 무관하게 전체 스트림을 유효한 UTF-8로 만드는 것은 base64 인코딩뿐이기 때문이에요(아래 참조).

JSONEachPacketString은 페이로드 바이트를 JSON 문자열에 넣으므로 유효한 UTF-8 텍스트를 생성하는 출력 포맷을 위한 것이에요. StringFixedString 열은 임의의 바이트를 담을 수 있어서, JSONEachRow, TSV, CSV 같은 텍스트 출력 포맷은 그런 값에 대해 유효하지 않은 UTF-8을 내보낼 수 있어요 — ClickHouse 자신의 JSONEachRow가 기본 output_format_json_validate_utf8 = 0에서 그러는 것처럼 — 그 경우 결과 JSON 문자열과 따라서 전체 NDJSON 스트림은 유효한 UTF-8임이 보장되지 않아요. JSONEachPacketString은 페이로드를 검증하거나 재인코딩하지 않아요. 임의 바이트의 바이트 정확 전송에는 JSONEachPacketBase64를 사용하세요.

확실히 비 UTF-8 바이트를 생성하는 출력 포맷은 JSONEachPacketString이 쿼리 실행 전에 오류로 미리 거부해요. 바이너리 포맷(Native, RowBinary), 원시 패스스루 포맷(RawBLOB, TSVRaw), 쿼리 헤더의 non-UTF-8 열 이름/데이터 타입 이름/Tuple 요소 이름을 출력에 쓰는 포맷, 그리고 설정 주도 리터럴을 직렬화가 그대로 쓰고 유효한 UTF-8이 아닌 구성 — format_csv_delimiter, format_tsv_null_representation / format_csv_null_representation, bool_true_representation / bool_false_representation 설정.

curl "http://localhost:8123/?framing_output_format=JSONEachPacketString" -d "SELECT number FROM numbers(3) FORMAT JSONEachRow"
{"packet":"data","data":"{\"number\":\"0\"}\n{\"number\":\"1\"}\n{\"number\":\"2\"}\n"}
{"packet":"profile_events","profile_events":[{"host_name":"localhost","current_time":"2026-07-11 00:00:00","thread_id":"0","type":"increment","name":"SelectedRows","value":"3"}]}
{"packet":"progress","progress":{"read_rows":"3","read_bytes":"24","total_rows_to_read":"3","result_rows":"3","result_bytes":"24","elapsed_ns":"1265958"}}

JSONEachPacketBase64를 사용하면 같은 data 패킷이 이렇게 보여요:

{"packet":"data","data":"eyJudW...In0K"}

패킷 종류 (Packet kinds)

패킷 내용
data 주요 결과에 대해 출력 포맷이 생성한 바이트(포맷 접두사와 접미사 포함).
totals totals 행(WITH TOTALS)에 대해 출력 포맷이 생성한 바이트.
extremes extremes(extremes 설정)에 대해 출력 포맷이 생성한 바이트.
progress 쿼리 진행을 JSON으로: read_rows, read_bytes, total_rows_to_read, result_rows, result_bytes, elapsed_ns, memory_usage(0 필드는 생략).
log 서버 로그 항목을 JSON으로: event_time, host_name, query_id, thread_id, priority, source, text.
profile_events 프로파일 이벤트 배열을 JSON으로: host_name, current_time, thread_id, type (increment 또는 gauge), name, value.
exception 예외 메시지를 JSON으로.

data, totals, extremes 페이로드와 달리(위의 바이트 정확성 주의 사항 참조), 보조 패킷의 문자열 필드(logquery_id, text, source, profile_eventsname, exception 메시지)는 base64 이스케이프가 없고, 그중 일부(예: 쿼리에서 가져온 query_id)는 임의의 바이트를 담을 수 있어요. 이 필드들은 항상 유효한 UTF-8로 정화되며, 유효하지 않은 시퀀스는 대체 문자(U+FFFD)로 바뀌므로 보조 패킷은 항상 유효한 JSON이에요.

여러 쿼리를 동시에 처리하는 것은 아직 구현되지 않았지만, 설계는 허용해요. 모든 패킷을 여러 쿼리에 걸쳐 쿼리 인덱스에 대한 정보로 확장할 수 있어요.

더 알아보기 (Learn more)