(KO) CUBRID 네트워크 프로토콜 — 연결 수락, NRP 디스패치, 서버 측 요청 핸들러
목차
학술적 배경
섹션 제목: “학술적 배경”관계형 엔진은 네트워크를 두 개의 층으로 나누어 다룬다. 두 층을 뒤섞어 보면 DBMS RPC 코드의 가장 흔한 버그가 그대로 들어선다. 아래쪽 층은 와이어 프레이머(wire framer) 다. 스트림 소켓 위에 길이 접두 헤더를 얹어, 수신 측이 현재 요청에 속하는 바이트 가 어디까지인지를 알 수 있게 만드는 일이다. 위쪽 층은 호출 디스패치(call dispatch) 다. 어떤 서버 함수가 그 본문을 소비하고 응답을 만들지 결정하는 오피코드다. 모델은 Database Internals (Petrov, 6장 B-Tree Variants 와 7장 Replication), 그리고 RPC 프레임워크 교과서적 정리인 Birrell & Nelson의 Implementing Remote Procedure Calls (ACM TOCS 1984) 가 깔끔하게 잡아 준다. 모든 상용 DBMS — PostgreSQL, MySQL, Oracle, CUBRID — 는 이 두 층의 변종을 구현한다.
세 가지 독립적인 설계 결정이 결과 프로토콜의 모양을 거의 다 정한다는 점이다. 본 문서의 뼈대도 이 세 결정 위에 얹힌다.
-
커스텀 바이너리 vs 범용 RPC. DBMS도 클라이언트/서버 채널에 gRPC, Thrift, 심지어 REST/JSON을 쓸 수 있다. 그럼 에도 모든 주요 엔진이 커스텀 바이너리 프로토콜을 택한 이유는 두 가지다. 첫째, 와이어를 흐르는 값 — CUBRID의
DB_VALUE, PostgreSQL의Datum, MySQL의Field— 은 이미 디스크 표현을 갖춘 태그드 유니언이다. 범용 직렬화기를 쓰면 portable 스키마(Protobuf, Thrift IDL)로 한 번 변환했 다가 다시 풀어야 한다는 점이다. 둘째, 처리량이 결정적인 경로는 한 행씩 끌어오는 fetch와 대량 insert이고, 한 마샬 링당 수 나노초의 비용이 행 수만큼 곱해진다. CUBRID의or_pack_value는 힙 매니저가 쓰는 바이트 레이아웃 그대 로DB_VALUE를 와이어 버퍼에 직접 쓴다. 중간 복사를 없애기 위함이다. -
길이 접두 vs 메시지 타입 프레이밍. 길이 접두는 모든 메시지가 본문 길이를 포함하는 고정 크기 헤더로 시작하는 방식이다. 메시지 타입 프레이밍은 모든 메시지가 1바이트 태그로 시작하고 그 태그가 파서를 골라 안에서 길이를 읽는 방식이다. PostgreSQL의 FE/BE 프로토콜은 후자(메시지 타입 바이트가 가장 먼저), MySQL classic은 전자(매 패킷마다 4바 이트
(length, sequence)접두), CUBRID는 전자(buffer_size필드를 가진NET_HEADER구조체) 를 택한다. 전자가 대칭 수신 에서 유리하다. 수신 루프가 메시지 타입에 따라 분기 하지 않는 한 덩어리 코드다. 하지만 메시지 종류 가 근본적으로 다른 모양일 때 (handshake와 row event는 무 관하다) 분기 비용이 다른 곳으로 옮겨갈 뿐이다. CUBRID는 이를 본문 안에서 변형을 인코딩해 풀어낸다. -
상태 없는 디스패치 테이블 vs 코드 생성 스텁. 어떤 엔진 은 코드 생성 방식을 쓴다. IDL이 호출 하나하나를 기술하면 컴파일러가 클라이언트 스텁과 서버 스켈레톤을 C/C++ 로 뽑 고, 링커가 바이너리에 합친다. 타입 안전성을 얻지만 빌드 단계가 추가되고 클라이언트와 서버 컴파일이 결합된다. CUBRID는 수동 스텁 노선을 택했다. 서버 진입점마다
network_interface_sr.cpp::s<name>핸들러가 손으로 작성 되어 인자를 풀고, 짝이 되는network_interface_cl.c::<name>클라이언트 스텁이 인자를 포장한다. 둘은 관습으로 동기화되며, 연결고리는NET_SERVER_*오피코드다. 정적 타이핑을 포기하는 대가로 단일 진입점 —network.h— 을 얻는다 (모든 새 RPC가 이 파일에서 선언 된다). 핸들러 하나만 핫 패치하기도 쉽다.
이 세 답을 명시하고 나면, CUBRID 네트워크 프로토콜의 나머지는 바이너리·길이 접두·수동 스텁이라는 설계 공간의 한 모서리를 선택했을 때 자연스럽게 따라 나오는 결과로 읽힌다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서적 프레이밍 층 아래로 내려오면 모든 주요 클라이언트/서버 DBMS는 똑같은 한 줌의 패턴을 들고 있다. 원래 RPC 논문에 적혀 있는 패턴들이 아니라, 추상 프로토콜과 실제 소스 사이의 빈자리 를 메우는 엔지니어링 어휘다.
모든 RPC 오피코드를 모은 단일 enum
섹션 제목: “모든 RPC 오피코드를 모은 단일 enum”오피코드 공간은 한 enum이다. 서버 진입점 하나당 한 멤버다.
PostgreSQL의 BackendMessageCode (src/include/libpq/protocol.h),
MySQL의 enum_server_command (include/my_command.h), CUBRID의
enum net_server_request (src/communication/network.h) 모두
같은 발상이다. 새 기능이 새 서버 함수를 추가할 때마다 정확히
하나의 오피코드를 끝에 덧붙이고, 그 값은 와이어 호환성 계약의
일부가 된다. 끝에 덧붙이는 것은 하위 호환이며 — 재정렬·삭제는
모든 구버전 클라이언트를 깬다.
오피코드로 인덱싱하는 정적 디스패치 테이블
섹션 제목: “오피코드로 인덱싱하는 정적 디스패치 테이블”오피코드가 읽히면 서버는 함수 포인터 배열로 인덱싱해 핸들러
를 고른다. PostgreSQL의 PostgresMain 의 switch가 가장 가까
운 유사물이다 (테이블이 아니라 손으로 짠 디스패치). MySQL의
do_command 는 COM_* 코드를 switch 한다. CUBRID는 이걸
정통 테이블 형태로 들고 있다. network_sr.c 의
static struct net_request net_Requests[NET_SERVER_REQUEST_END]
가 그 자리이며, 부팅 시 net_server_init() 가 오피코드마다
한 행씩 채운다. 각 행은 함수 포인터 외에 속성 비트마스크
(CHECK_DB_MODIFICATION, CHECK_AUTHORIZATION, IN_TRANSACTION
…) 를 들고 있다. 이 비트마스크는 부수 조건 — “이 RPC는 DBA
권한이 필요하다”, 이 RPC는 트랜잭션이 열려 있다는 뜻이다. 을 핸들러 안에 흩뿌리지 않고 선언적으로 표현하기 위함이다.
클라이언트와 서버가 공유하는 대칭 pack/unpack 헬퍼
섹션 제목: “클라이언트와 서버가 공유하는 대칭 pack/unpack 헬퍼”마샬링 코드는 호출 방향과 무관하다. or_pack_int 는 4바이트
big-endian 정수를 버퍼에 쓰고 새 포인터를 돌려준다.
or_unpack_int 는 하나를 읽고 새 포인터를 돌려준다. 호출자
는 그 포인터를 필드별 호출 시퀀스에 그대로 꿰고, 오프셋을
직접 만지지 않는다. 같은 헤더를 클라이언트와 서버가 함께
포함하므로, 클라이언트 스텁과 짝이 되는 서버 핸들러는 서로
거울상이다. 클라이언트의 or_pack_X 시퀀스가 그대로 서버의
or_unpack_X 시퀀스가 된다. PostgreSQL의 pq_send* /
pq_get*, MySQL의 net_store_* / net_field_length_ll 도
같은 관용구다.
응답 demux를 위한 호출별 request id (RID)
섹션 제목: “응답 demux를 위한 호출별 request id (RID)”한 연결 위에 여러 in-flight 요청이 동시에 있을 수 있다 (질의
실행 도중 서버가 거꾸로 부르는 콜백, 비동기 cancel). 그래서
응답이 요청과 같은 식별자를 들고 와야 매칭이 가능하다.
CUBRID의 unsigned short rid 는 NET_HEADER 안에 있다. 서버는 모든 응답에 그 값을 쓰고, 클라이언트는 대기 중인
request_queue / data_queue 항목과 맞춰 본다. PostgreSQL은
한 연결 위에 엄격한 요청/응답 순서를 강제해 이 문제를 우회
한다. MySQL은 1바이트 시퀀스 번호를 쓴다. CUBRID의 RID는
연결 단위로 굴러가는 16비트 카운터라서, 한 질의 안에서 동시
콜백들이 충돌하지 않는다는 점이다.
디스패치 루프를 소유하는 워커 풀
섹션 제목: “디스패치 루프를 소유하는 워커 풀”서버는 디스패치를 I/O 쓰레드 위에서 할 수 없다. 핸들러가 락,
페이지 읽기, 하위 RPC(PL 호출) 같은 곳에서 막힐 수 있기 때문
이다. 표준 모양은 워커 풀이다. I/O 쓰레드가 요청을 읽어 태
스크로 포장해 큐에 넣고, 워커 쓰레드가 그 태스크를 꺼내 핸들
러를 부른다. CUBRID는 두 풀을 쓴다. 연결 I/O를 위한
cubconn::connection::worker (epoll 기반) 와, cubthread 의
transaction 워커 풀(핸들러 실행 담당) 이다. 연결 워커가
push_task_into_worker_pool 로 트랜잭션 풀에 핸들러 호출을
밀어 넣는 식이다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| 와이어 프레이머 헤더 | connection_defs.h 의 NET_HEADER 구조체 (9개 필드, 고정 크기) |
| 길이 접두 본문 길이 | header.buffer_size (와이어 위에서는 htonl/ntohl) |
| 패킷 종류 태그 | header.type ∈ {COMMAND_TYPE, DATA_TYPE, ABORT_TYPE, CLOSE_TYPE, ERROR_TYPE} |
| 호출별 request id | header.request_id (16비트, css_get_request_id 가 할당) |
| 서버 함수 코드 (RPC 오피코드) | header.function_code (16비트) + enum net_server_request |
| 정적 디스패치 테이블 | network_sr.c 의 static struct net_request net_Requests[NET_SERVER_REQUEST_END] |
| 액션 속성 비트마스크 | enum net_req_act { CHECK_DB_MODIFICATION, CHECK_AUTHORIZATION, SET_DIAGNOSTICS_INFO, IN_TRANSACTION, OUT_TRANSACTION } |
| Pack/unpack 프리미티브 | or_pack_int / or_unpack_int / or_pack_oid / or_pack_value / or_unpack_value |
| 호출별 클라이언트 스텁 | network_cl.c 의 net_client_request* 계열 |
| 호출별 서버 핸들러 | network_interface_sr.cpp 의 s<module>_<verb> (예: slocator_force, sqmgr_execute_query) |
| 연결 수락자 | master_connector.cpp 의 cubconn::master::connector (Unix 도메인 스위치) |
| 연결 워커 풀 | cubconn::connection::pool + cubconn::connection::worker (epoll 기반) |
| 핸들러 실행자 | REGISTER_WORKERPOOL 로 등록되어 css_internal_request_handler 가 디스패치하는 transaction 풀 |
| 초기 핸드셰이크 | NET_SERVER_PING_WITH_HANDSHAKE = 999 (대역 외 오피코드) |
| 능력(capability) 비트 | network.h:304-311 의 NET_CAP_* 매크로 |
| 클라이언트/서버 엔디언 검사 | network.h:337-343 의 inline get_endian_type () |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”네트워크 모듈의 이동 부품은 다섯이다. CSS 프레이밍 (와이어
헤더와 패킷 종류), 연결 수락 (새 클라이언트가 워커를 얻는
방법), 워커 풀 (소켓에서 요청을 떼어 디스패치하는 방법),
NRP 디스패치 (오피코드가 핸들러 호출이 되는 방법), 그리고
packer/unpacker (인자가 와이어를 들고 나는 방법) 다. 이
순서로 본 뒤 마지막에 SELECT 질의 하나를 끝까지 따라간다.
전체 구조
섹션 제목: “전체 구조”flowchart LR
subgraph CLIENT["클라이언트 프로세스<br/>(libcubridcs / CAS / csql)"]
APP["애플리케이션<br/>또는 CAS 워커"]
CL_INTF["network_interface_cl.c<br/>호출별 스텁<br/>(qmgr_execute_query, ...)"]
CL_NET["network_cl.c<br/>net_client_request*"]
CL_CSS["connection_cl.cpp<br/>· connection_support.cpp<br/>(클라이언트 측 CSS 프레이밍)"]
end
subgraph SERVER["서버 프로세스<br/>(cub_server)"]
SR_CONN["connection_sr.c<br/>연결 수명 주기"]
SR_WORKER["connection_worker.cpp<br/>epoll 워커<br/>(cubconn::connection)"]
SR_DISP["network_sr.c<br/>net_Requests[opcode]<br/>디스패치 테이블"]
SR_HANDL["network_interface_sr.cpp<br/>호출별 핸들러<br/>(sqmgr_execute_query, ...)"]
SR_TRAN["transaction 풀<br/>(cubthread 워커)"]
end
subgraph MASTER["cub_master"]
MASTER_LSN["TCP 리스닝 소켓<br/>port = PRM_ID_TCP_PORT_ID"]
end
APP --> CL_INTF --> CL_NET --> CL_CSS
CL_CSS -->|"NET_HEADER + body"| MASTER_LSN
MASTER_LSN -->|"Unix 도메인 핸드오프"| SR_CONN
SR_CONN --> SR_WORKER
SR_WORKER --> SR_DISP
SR_DISP --> SR_TRAN
SR_TRAN --> SR_HANDL
SR_HANDL -->|"응답 NET_HEADER + body"| CL_CSS
CSS 프레이밍 — 와이어 헤더
섹션 제목: “CSS 프레이밍 — 와이어 헤더”CUBRID 클라이언트/서버 와이어의 모든 패킷은 고정 크기
NET_HEADER 로 시작한다 (9개 필드, htonl 로 인코딩된
big-endian). 구조체는 한 번만 정의되고 양쪽이 공유한다.
// packet_header — connection_defs.htypedef struct packet_header NET_HEADER;struct packet_header{ int type; // COMMAND_TYPE | DATA_TYPE | ABORT_TYPE | CLOSE_TYPE | ERROR_TYPE int version; // unused in current code; reserved int host_id; // unused; reserved int transaction_id; // server-assigned tran index for this request int request_id; // per-connection RID for response demux int db_error; // last error code piggy-backed short function_code; // NET_SERVER_* opcode (when type == COMMAND_TYPE) unsigned short flags; // NET_HEADER_FLAG_METHOD_MODE | NET_HEADER_FLAG_INVALIDATE_SNAPSHOT int buffer_size; // length of the body that follows};다섯 가지 type 값은 임의의 값이 아니다. 패킷의 종류 를 인
코딩해 본문을 파싱하지 않고도 수신 디스패처가 라우팅할 수 있
게 한다는 점이다.
// css_packet_type — connection_defs.h:185-192enum css_packet_type{ COMMAND_TYPE = 1, // request from client to server (carries an opcode) DATA_TYPE = 2, // payload data (request args or response data) ABORT_TYPE = 3, // server tells client "your last request was aborted" CLOSE_TYPE = 4, // half-close; this connection is going away ERROR_TYPE = 5 // server-side error, body is a packed error area};한 클라이언트 요청이 끝까지 가려면 여러 패킷 이 필요할 수
있다. 최소 구성은 COMMAND_TYPE 헤더 한 개와 (arg_size > 0
이면) DATA_TYPE 헤더 + 본문이다. 응답 경로는 작은 고정 응답
용 DATA_TYPE 한 개에 가변 페이로드용 DATA_TYPE 들이 더
붙고, 문제가 생기면 ERROR_TYPE 이 따른다. 모든 패킷의 헤더
는 같은 request_id 를 들고 있어 클라이언트가 상관 가능하다.
css_set_net_header() 가 정식 작성자다.
// css_set_net_header — connection_support.cpp:1326voidcss_set_net_header (NET_HEADER *header_p, int type, short function_code, int request_id, int buffer_size, int transaction_id, int invalidate_snapshot, int db_error){ unsigned short flags = 0; header_p->type = htonl (type); header_p->function_code = htons (function_code); header_p->request_id = htonl (request_id); header_p->buffer_size = htonl (buffer_size); header_p->transaction_id = htonl (transaction_id); header_p->db_error = htonl (db_error); if (invalidate_snapshot) flags |= NET_HEADER_FLAG_INVALIDATE_SNAPSHOT;#if defined (CS_MODE) if (tran_is_in_libcas ()) flags |= NET_HEADER_FLAG_METHOD_MODE;#endif header_p->flags = htons (flags);}요청 한 건의 전체 레이아웃은 다음과 같다.
graph LR
subgraph REQ["클라이언트 요청 (두 패킷)"]
direction TB
H1["NET_HEADER<br/>type=COMMAND_TYPE<br/>function_code=NET_SERVER_QM_QUERY_EXECUTE<br/>request_id=R<br/>buffer_size=0"]
H2["NET_HEADER<br/>type=DATA_TYPE<br/>request_id=R<br/>buffer_size=N"]
BODY["packed args<br/>(or_pack_∗ 시퀀스)<br/>N 바이트"]
H1 --> H2 --> BODY
end
subgraph RESP["서버 응답 (한 개 또는 여러 패킷)"]
direction TB
H3["NET_HEADER<br/>type=DATA_TYPE<br/>request_id=R<br/>buffer_size=M"]
REPLY["고정 크기 응답<br/>(or_pack_int x N)<br/>M 바이트"]
H4["NET_HEADER<br/>type=DATA_TYPE<br/>request_id=R<br/>buffer_size=K"]
DATA2["벌크 데이터<br/>(packed list-id, page, plan)<br/>K 바이트"]
H3 --> REPLY --> H4 --> DATA2
end
REQ --> RESP
연결 수락 — cub_master 가 cub_server 로 핸드오프
섹션 제목: “연결 수락 — cub_master 가 cub_server 로 핸드오프”CUBRID의 연결 수락 구조는 흔치 않은 두 프로세스 분리다.
공개 TCP 리스닝 소켓은 별도의 cub_master 프로세스가 소유
한다. 데이터베이스 서버 본체(cub_server) 는 공개 포트를
바인드하지 않는다. 클라이언트가 접속하면 cub_master 가
연결을 받아 인사한 뒤 어떤 데이터베이스를 원하는지 판별해서,
해당 cub_server 에 파일 디스크립터를 Unix 도메인 소켓으로
넘긴다.
이 프로토콜의 서버 측은 cubconn::master::connector
(master_connector.cpp) 에 산다. 서버 부팅 시
net_server_start 가 css_init 을 부르고, 그 안에서
master::connector 를 만들어 connect → prepare_handshake → execute 순으로 호출한다.
// connector::run — master_connector.cpp:160bool connector::run (int port, std::string &server_name) noexcept{ m_master_port = port; m_server_name = server_name; if (!this->connect (port)) // open TCP to cub_master return false; if (!this->prepare_handshake (server_name)) // tell master "I serve <name>" return false; if (!this->execute ()) // run the epoll-based fwd loop return false; return true;}connect() 는 잘 알려진 포트(PRM_ID_TCP_PORT_ID) 로
cub_master 에 TCP 소켓을 연다. prepare_handshake() 는
내가 <db_name> 을 서비스한다 는 서버 측 등록 패킷(데이터
베이스 이름과 서버 PID 포함) 을 보낸다. execute() 는 두
스트림을 다루는 epoll 루프로 들어간다.
-
마스터 측 수신 (
handle_master_reception) —cub_master가 새 클라이언트 연결을, 그 클라이언트의 파일 디스크립터를 실은 Unix 도메인 메시지로 포워딩한다. 서버는 그 fd를 받아 풀에서 새CSS_CONN_ENTRY를 떼어내고connection::worker풀로 디스패치한다. -
워커 통계 / 셧다운 제어 — 부수적인 제어 메시지가 같은 채널로 흐른다.
새 서버 등록에 대한 마스터 측 응답은 enum css_master_response
(connection_defs.h) 한 값으로 인코딩된다.
enum css_master_response{ SERVER_ALREADY_EXISTS = 0, SERVER_REQUEST_ACCEPTED = 1, // legacy Unix-domain handoff DRIVER_NOT_FOUND = 2, SERVER_REQUEST_ACCEPTED_NEW = 3 // Windows/new-style: server opens its own port};리눅스/유닉스에서는 마스터가 항상 SERVER_REQUEST_ACCEPTED +
Unix 도메인 fd 전달을 쓴다. 윈도우에서는 Unix 도메인 소켓이
없으므로 마스터가 SERVER_REQUEST_ACCEPTED_NEW 로 떨어져
서버가 직접 보유할 TCP 포트 번호를 돌려준다
(connection_sr.c 의 css_open_server_connection_socket).
레거시 서버 단독 경로는 connection_sr.c:1066 의
css_connect_to_master_server (master_port_id, server_name, name_length)
다. CUBRID의 옛 in-process 스타일이 쓰던 함수이며, 모던
경로는 master::connector 다.
sequenceDiagram participant CL as 클라이언트 (CAS / csql) participant MA as cub_master participant SR as cub_server (해당 DB) participant WK as connection::worker Note over MA: PRM_ID_TCP_PORT_ID 에 바인드됨 CL->>MA: TCP connect CL->>MA: DATA_REQUEST + db_name (CSS framed) MA->>SR: Unix 도메인 메시지 (fd + db_name) SR->>SR: claim_context() / css_make_conn(fd) SR->>WK: dispatch(conn) → epoll 등록 WK->>CL: ready (NET_SERVER_PING_WITH_HANDSHAKE = 999 응답) CL->>WK: NET_SERVER_BO_REGISTER_CLIENT (정식 RPC 시작) WK-->>CL: reply
NET_SERVER_PING_WITH_HANDSHAKE — 대역 외 오피코드
섹션 제목: “NET_SERVER_PING_WITH_HANDSHAKE — 대역 외 오피코드”새 연결 위의 첫 요청은 특별하다. 오피코드 999
(NET_SERVER_PING_WITH_HANDSHAKE) 다. 일반
NET_SERVER_REQUEST_LIST enum 범위에는 *들지 않는다. 일부
러 999 라는 상수에 박아 두어 버전 변경에도 그 수치가 보존되
도록 한 것이다. network_interface_sr.cpp:563 의
server_ping_with_handshake 핸들러가 다음을 수행한다.
- 클라이언트 릴리스 문자열, 능력 플래그, 비트 플랫폼(32 vs 64), 클라이언트 종류, 호스트 이름을 읽는다.
rel_get_net_compatible(client, server)로 호환성을 검사한다.check_client_capabilities로 능력 비트를 검증한다.css_increment_num_conn(client_type)으로 연결 슬롯을 예 약한다.- 서버 릴리스 문자열, 능력 비트, 서버 호스트 이름,
REL_COMPATIBILITY판정을 응답한다.
이 오피코드가 이후 모든 디스패치의 문지기 이기 때문에 — 그
다음 요청들은 클라이언트와 서버가 버전 호환임을 가정해도 된다.
net_server_request 의 디스패치는 테이블 조회 전에 이 케이
스를 단락 평가한다.
// net_server_request — network_sr.c:791if (request == NET_SERVER_PING_WITH_HANDSHAKE) { status = server_ping_with_handshake (thread_p, rid, buffer, size); goto end; }else if (request == NET_SERVER_SHUTDOWN) { er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, ER_NET_SERVER_SHUTDOWN, 0); status = CSS_UNPLANNED_SHUTDOWN; goto end; }if (request <= NET_SERVER_REQUEST_START || request >= NET_SERVER_REQUEST_END) { er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, ER_NET_UNKNOWN_SERVER_REQ, 0); return_error_to_client (thread_p, rid); goto end; }핸드셰이크에서 인코딩되는 능력 비트(network.h:304-311)는
다음과 같다.
#define NET_CAP_BACKWARD_COMPATIBLE 0x80000000#define NET_CAP_FORWARD_COMPATIBLE 0x40000000#define NET_CAP_INTERRUPT_ENABLED 0x00800000#define NET_CAP_UPDATE_DISABLED 0x00008000#define NET_CAP_REMOTE_DISABLED 0x00000080#define NET_CAP_HA_REPL_DELAY 0x00000008#define NET_CAP_HA_REPLICA 0x00000004#define NET_CAP_HA_IGNORE_REPL_DELAY 0x00000002replica 전용 브로커가 비-replica 서버에 접속하면 핸드셰이크
가 ER_NET_HS_HA_REPLICA_ONLY 로 실패하고, 읽기 전용
클라이언트가 primary 에 접속하면 ER_NET_HS_INCOMPAT_RW_MODE
경고가 뜬다.
워커 풀 — epoll 수신과 transaction 풀 디스패치
섹션 제목: “워커 풀 — epoll 수신과 transaction 풀 디스패치”핸드셰이크가 끝나면 그 연결은 cubconn::connection::worker
인스턴스(connection_worker.hpp 선언) 의 소유가 된다. 이
워커는 SQL 요청 핸들러를 직접 돌리지는 않는다. 책임이
둘로 갈라져 있기 때문이다.
-
연결 워커 (epoll 기반, N개의 연결당 하나씩) 는 소켓에 서 CSS 프레임 패킷을 떼어 와 완전한 요청 본문을 조립하고, transaction 워커 풀이 실행할 태스크 를 큐에 밀어 넣는다. 연결 워커 수는
PRM_ID_CSS_MIN_CONNECTION_WORKER와PRM_ID_CSS_MAX_CONNECTION_WORKER사이에서 변동한다. -
transaction 워커 풀 은 전역으로 등록된다.
// server_support.c:548REGISTER_WORKERPOOL (transaction, []() { return (int) prm_get_integer_value (PRM_ID_TASK_WORKER); });요청 하나는 태스크로 포장되어 이 풀로 들어가며, 태스크가 실행하는 함수는
css_internal_request_handler— 연결 층에 서 디스패치 테이블로 가는 다리다.
연결 워커는 워커마다 multi-producer/single-consumer 형태의
TBB 큐와 쓰레드 간 깨우기를 위한 eventfd 를 한 쌍씩 가진다
(connection_worker.hpp:236-241). 큐 종류가 두 개 — 핫패스
메시지(IMMEDIATE) 와 미루어도 되는 제어 메시지(LAZY) — 인
이유는 새 클라이언트가 몰려 들어와도 진행 중인 SEND_PACKET
이 지연되지 않게 분리하기 위함이다.
연결 워커가 완전한 요청을 조립하면 결국
css_internal_request_handler 로 진입한다.
// css_internal_request_handler — server_support.c:450static intcss_internal_request_handler (THREAD_ENTRY & thread_ref, CSS_CONN_ENTRY & conn_ref){ unsigned short rid; unsigned int eid; int request, rc, size = 0; char *buffer = NULL; int local_tran_index = thread_ref.tran_index; int status = CSS_UNPLANNED_SHUTDOWN;
rc = css_receive_request (&conn_ref, &rid, &request, &size); if (rc == NO_ERRORS) { thread_ref.tran_index = conn_ref.get_tran_index (); pthread_mutex_unlock (&thread_ref.tran_index_lock); if (size) { rc = css_receive_data (&conn_ref, rid, &buffer, &size, -1); if (rc != NO_ERRORS) return status; } conn_ref.db_error = 0; eid = css_return_eid_from_conn (&conn_ref, rid); css_set_thread_info (&thread_ref, conn_ref.client_id, eid, conn_ref.get_tran_index (), request); // 3. Call server_request() function status = css_Server_request_handler (&thread_ref, eid, request, size, buffer); css_set_thread_info (&thread_ref, -1, 0, local_tran_index, -1); } ...}함수 포인터 css_Server_request_handler 는 부팅 시
css_initialize_server_interfaces (net_server_request)
(server_support.c:516) 로 한 번 꽂아 둔다. 이 분리는 의도적
이다. connection/ 코드는 프레이밍과 워커 관리만 책임지고,
communication/ 코드가 디스패치를 전담한다는 점이다. 둘 중
어느 한쪽을 갈아 끼워도 다른 쪽을 건드릴 필요가 없도록.
NRP 디스패치 — net_Requests[] 테이블
섹션 제목: “NRP 디스패치 — net_Requests[] 테이블”디스패치 테이블은 오피코드당 한 행짜리 평탄 배열이다.
// network_sr.c — top of filestatic struct net_request net_Requests[NET_SERVER_REQUEST_END];행 자체(net_request)는 의도적으로 작다
(network_request_def.hpp).
typedef void (*net_server_func) (THREAD_ENTRY *thrd, unsigned int rid, char *request, int reqlen);struct net_request{ int action_attribute; // bitmask of net_req_act net_server_func processing_function; net_request () = default;};서버 진입점은 정확히 한 번, net_server_init() 안에서 등록된
다. 대표 행 몇 개만 보면 다음과 같다.
// net_server_init — network_sr.c:74req_p = &net_Requests[NET_SERVER_PING];req_p->processing_function = server_ping;
req_p = &net_Requests[NET_SERVER_BO_REGISTER_CLIENT];req_p->processing_function = sboot_register_client;
req_p = &net_Requests[NET_SERVER_LC_FORCE];req_p->action_attribute = (CHECK_DB_MODIFICATION | SET_DIAGNOSTICS_INFO | IN_TRANSACTION);req_p->processing_function = slocator_force;
req_p = &net_Requests[NET_SERVER_QM_QUERY_EXECUTE];req_p->action_attribute = (SET_DIAGNOSTICS_INFO | IN_TRANSACTION);req_p->processing_function = sqmgr_execute_query;
req_p = &net_Requests[NET_SERVER_TM_SERVER_COMMIT];req_p->action_attribute = (CHECK_DB_MODIFICATION | SET_DIAGNOSTICS_INFO | OUT_TRANSACTION);req_p->processing_function = stran_server_commit;진짜 디스패처는 net_server_request 다. 대역 외 케이스(핸드
셰이크, 셧다운) 와 범위 검사를 끝낸 뒤, 행을 읽고 부수 조건을
적용한 다음 핸들러를 호출한다.
// net_server_request — network_sr.c:791if (net_Requests[request].action_attribute & CHECK_DB_MODIFICATION) { bool check = true; if (request == NET_SERVER_TM_SERVER_COMMIT) { if (!logtb_has_updated (thread_p)) // commit of a read-only txn doesn't need write check check = false; } if (check) { CHECK_MODIFICATION_NO_RETURN (thread_p, error_code); if (error_code != NO_ERROR) { return_error_to_client (thread_p, rid); css_send_abort_to_client (conn, rid); goto end; } } }if (net_Requests[request].action_attribute & CHECK_AUTHORIZATION) { if (!logtb_am_i_dba_client (thread_p)) { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_AU_DBA_ONLY, 1, ""); return_error_to_client (thread_p, rid); css_send_abort_to_client (conn, rid); goto end; } }if (net_Requests[request].action_attribute & IN_TRANSACTION) conn->in_transaction = true;
// call a request processing functionfunc = net_Requests[request].processing_function;thread_p->push_resource_tracks ();if (conn->invalidate_snapshot != 0) logtb_invalidate_snapshot_data (thread_p);(*func) (thread_p, rid, buffer, size);thread_p->pop_resource_tracks ();pgbuf_unfix_all (thread_p); // defence: don't leak page latches이렇게 action_attribute 한 필드가 직교적인 행동들을 모두
선언한다. 그렇지 않다면 모든 핸들러 안에 같은 검사 코드가
중복되었을 것이다.
| 비트 | 의미 |
|---|---|
CHECK_DB_MODIFICATION | DB가 쓰기를 받아야 한다 (read-only 모드, replica, HA 적용기 정지 시 거부) |
CHECK_AUTHORIZATION | 클라이언트가 DBA/소유자여야 한다. 아니면 ER_AU_DBA_ONLY 로 거부 |
SET_DIAGNOSTICS_INFO | perfmon 타이머(PSTAT_*) 와 trace-log 탭을 호출 둘레에 두른다 |
IN_TRANSACTION | 이 호출은 트랜잭션을 연다는 뜻이다 (conn->in_transaction 셋) |
OUT_TRANSACTION | 호출이 끝나면 in-transaction 플래그를 끈다 (COMMIT / ABORT) |
핸들러 견본 — 세 가지 대표적 모양
섹션 제목: “핸들러 견본 — 세 가지 대표적 모양”모양 1: 작은 요청, 작은 응답. server_ping 이 표준적인
최소 형태다. int 하나 들어오고 int 하나 나간다.
// server_ping — network_interface_sr.cpp:532voidserver_ping (THREAD_ENTRY *thread_p, unsigned int rid, char *request, int reqlen){ OR_ALIGNED_BUF (OR_INT_SIZE) a_reply; char *reply = OR_ALIGNED_BUF_START (a_reply); int client_val, server_val;
or_unpack_int (request, &client_val); server_val = 0; or_pack_int (reply, server_val); css_send_data_to_client (thread_p->conn_entry, rid, reply, OR_INT_SIZE);}모양 2: 가변 요청, 혼합 크기 응답. sqp_get_server_info
는 요청 비트에 따라 크기가 바뀌는 DB_VALUE 페이로드를
돌려준다.
// sqp_get_server_info — network_interface_sr.cpp:7962 (condensed)voidsqp_get_server_info (THREAD_ENTRY *thread_p, unsigned int rid, char *request, int reqlen){ OR_ALIGNED_BUF (OR_INT_SIZE + OR_INT_SIZE) a_reply; char *reply = OR_ALIGNED_BUF_START (a_reply); char *ptr, *buffer = NULL; int buffer_length, server_info_bits, success = NO_ERROR; DB_VALUE dt_dbval, ts_dbval, lt_dbval;
ptr = or_unpack_int (request, &server_info_bits);
buffer_length = 0; if (server_info_bits & SI_SYS_DATETIME) { success = db_sys_date_and_epoch_time (&dt_dbval, &ts_dbval); buffer_length += OR_VALUE_ALIGNED_SIZE (&dt_dbval); buffer_length += OR_VALUE_ALIGNED_SIZE (&ts_dbval); } if (server_info_bits & SI_LOCAL_TRANSACTION_ID) { success = xtran_get_local_transaction_id (thread_p, <_dbval); buffer_length += OR_VALUE_ALIGNED_SIZE (<_dbval); }
buffer = (char *) malloc (buffer_length); ptr = buffer; if (server_info_bits & SI_SYS_DATETIME) { ptr = or_pack_value (ptr, &dt_dbval); ptr = or_pack_value (ptr, &ts_dbval); } if (server_info_bits & SI_LOCAL_TRANSACTION_ID) ptr = or_pack_value (ptr, <_dbval);
ptr = or_pack_int (reply, buffer_length); ptr = or_pack_int (ptr, success); css_send_reply_and_data_to_client (thread_p->conn_entry, rid, reply, OR_ALIGNED_BUF_SIZE (a_reply), buffer, buffer_length, std::move (deleter));}두 단계 송신 — 먼저 들어올 벌크 크기를 알리는 작은 고정 응답 을 보낸 뒤 벌크 데이터 본체를 보내는 가 가변 응답을 다루는 보편 관용구다.
모양 3: 벌크 요청, 다단 응답. slocator_force 는 더티
객체의 copy area 를 클라이언트에서 서버로 보내고, 갱신된
디스크립터(서버가 새 OID를 박았을 수 있다) 를 다시 돌려준다.
// slocator_force — network_interface_sr.cpp:1381 (condensed)voidslocator_force (THREAD_ENTRY *thread_p, unsigned int rid, char *request, int reqlen){ int num_objs, multi_update_flags, packed_desc_size, content_size, num_ignore_error_list; int success, csserror; LC_COPYAREA *copy_area = NULL; char *packed_desc = NULL, *content_ptr = NULL, *new_content_ptr = NULL; char *ptr; int ignore_error_list[-ER_LAST_ERROR];
ptr = or_unpack_int (request, &num_objs); ptr = or_unpack_int (ptr, &multi_update_flags); ptr = or_unpack_int (ptr, &packed_desc_size); ptr = or_unpack_int (ptr, &content_size); ptr = or_unpack_int (ptr, &num_ignore_error_list); for (int i = 0; i < num_ignore_error_list; i++) ptr = or_unpack_int (ptr, &ignore_error_list[i]);
copy_area = locator_recv_allocate_copyarea (num_objs, &content_ptr, content_size); // 1. pull the descriptor block from the client csserror = css_receive_data_from_client (thread_p->conn_entry, rid, &packed_desc, &packed_size); locator_unpack_copy_area_descriptor (num_objs, copy_area, packed_desc, -1); // 2. pull the content block if (content_size > 0) csserror = css_receive_data_from_client (thread_p->conn_entry, rid, &new_content_ptr, &received_size); // 3. run the actual server-side function success = xlocator_force (thread_p, copy_area, num_ignore_error_list, ignore_error_list); // 4. repack the descriptor (server may have written new OIDs into it) locator_pack_copy_area_descriptor (num_objs, copy_area, packed_desc, packed_desc_size); // 5. send the small reply + the updated descriptor as two pieces ptr = or_pack_int (reply, success); ptr = or_pack_int (ptr, packed_desc_size); ptr = or_pack_int (ptr, 0); css_send_reply_and_2_data_to_client (thread_p->conn_entry, rid, reply, OR_ALIGNED_BUF_SIZE (a_reply), packed_desc, packed_desc_size, NULL, 0, std::move (deleter));}css_receive_data_from_client 호출은 같은 연결로 거꾸로 더
당겨오는 인-라인 풀(pull) 이다. 서버가 받은 요청 본문에는
디스크립터·콘텐츠 블롭이 들어있지 않고 크기만 들어있다는
것이다. 벌크는 같은 RID 로 잇따라 오는 DATA_TYPE 패킷에
실려 도착한다.
패커/언패커 — or_pack_* 와 OR_PACK_*
섹션 제목: “패커/언패커 — or_pack_* 와 OR_PACK_*”마샬링 층은 big-endian 바이트 단위 직렬화 위에 놓인 얇은 어
댑터다. or_pack_int 는 버퍼 포인터를 4 만큼 전진시킨다.
// from object_representation.hextern char *or_pack_int (char *ptr, int number);extern char *or_pack_int64 (char *ptr, INT64 number);extern char *or_pack_string (char *ptr, const char *string);extern char *or_pack_oid (char *ptr, const OID *oid);extern char *or_pack_value (char *buf, DB_VALUE *value); // !! the heavyweight one
extern char *or_unpack_int (char *ptr, int *number);extern char *or_unpack_string (char *ptr, char **string);extern char *or_unpack_oid (char *ptr, OID *oid);extern char *or_unpack_value (const char *buf, DB_VALUE *value);CUBRID 스텁 코드 어디에서나 똑같이 쓰는 관용구는 포인터
를 꿰는 방식이다. 한 호출의 반환값이 다음 호출의 입력이
된다. 오프셋 산술도, 손으로 계산한 크기로 memcpy 도 없다.
패커가 둘 다 가린다는 점이다. PostgreSQL 의 StringInfo
와 같은 관용구이지만 (PostgreSQL 은 오프셋을 구조체 안에
들고 다닌다는 차이는 있다), 발상은 동일하다.
DB_VALUE (만능 값 타입 — dbtype_def.h) 의 경우
or_pack_value 는 다음 모양을 쓴다.
+-----------------+--------------------+----------------+| domain header | nullness flag | value bytes || (variable size) | (1 byte, in domain | (depends on || | header, encoded | domain type) || | via or_packed_ | || | domain_size) | |+-----------------+--------------------+----------------+domain header 자체가 가변 길이다. 도메인 타입 태그
(DB_TYPE_INTEGER, DB_TYPE_VARCHAR, …), 확장 도메인 플래그,
collation, precision, scale 등을 비트 패킹으로 묶은 int 라는
뜻이다. 따라서 수신자는 domain header 를 파싱하기 전까지는
값의 바이트 길이를 알 수 없다. 트레이드오프는 분명하다. 기
본형이면 와이어 위에서 매우 작지만(DB_TYPE_INTEGER 는 헤더
포함 5바이트 안팎) 파싱이 상태에 의존 한다.
도우미 매크로 OR_INT_SIZE = 4, OR_OID_SIZE = 8,
OR_VALUE_ALIGNED_SIZE, OR_ALIGNED_BUF (정렬된 스택 버퍼
매크로) 는 모든 스텁에 등장해 호출당 인자/응답 버퍼 크기를
잡는다.
OR_BUF 구조체(object_representation.h:1029) 는 힙·B-트리
패킹에서 주로 쓰는 한 단계 위 추상이다. 버퍼 포인터, 끝
포인터, overflow 플래그를 함께 들고 다닌다. 네트워크 코드
는 보통 raw char * 포인터-쓰레딩을 쓰고, OR_BUF 는
overflow 검사가 더 중요한 스토리지 측 패킹용이다.
클라이언트 스텁 — net_client_request_* 패밀리
섹션 제목: “클라이언트 스텁 — net_client_request_* 패밀리”클라이언트 측은 오피코드 단위로 서버를 거울처럼 마주본다. 요청은 다음 6단계다.
OR_ALIGNED_BUF로 고정 크기 인자 버퍼를 잡고,or_pack_*로 인자를 채워 넣고,- 고정 크기 응답 버퍼를 잡고,
- 적절한
net_client_request_*변종을 부르고, or_unpack_*로 응답을 풀고,- 결과를 호출자에게 맞게 변환한다.
디스패처는 net_client_request_internal (network_cl.c:495)
이다.
// net_client_request_internal — network_cl.c:495 (condensed)static intnet_client_request_internal (int request, char *argbuf, int argsize, char *replybuf, int replysize, char *databuf, int datasize, char *replydata, int replydatasize){ unsigned int rc; int size, error = 0; char *reply = NULL;
if (net_Server_name[0] == '\0') // not connected { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_NET_SERVER_CRASHED, 0); return -1; }
rc = __gv_cvar.css_send_req_to_server (net_Server_host, request, argbuf, argsize, databuf, datasize, replybuf, replysize); if (rc == 0) return set_server_error (__gv_cvar.css_get_errno ());
if (replydata != NULL) __gv_cvar.css_queue_receive_data_buffer (rc, replydata, replydatasize);
error = __gv_cvar.css_receive_data_from_server (rc, &reply, &size); if (error != NO_ERROR) return set_server_error (error); error = COMPARE_SIZE_AND_BUFFER (&replysize, size, &replybuf, reply);
if (replydata != NULL) { error = __gv_cvar.css_receive_data_from_server (rc, &reply, &size); if (error == NO_ERROR) error = COMPARE_SIZE_AND_BUFFER (&replydatasize, size, &replydata, reply); } return error;}여기서 __gv_cvar 라는 간접 호출이 등장한다. 함수 포인터들
의 전역 vtable (css_send_req_to_server, css_receive_data_from_server,
css_queue_receive_data_buffer, …) 이다. 이 vtable 덕분에
같은 클라이언트 스텁 이 CS_MODE(실제 클라이언트/서버, TCP
로 호출이 나간다) 와 SA_MODE(standalone — 클라이언트와 서버
가 한 프로세스로 묶여 호출이 in-process 큐로 단락된다) 양쪽
에서 그대로 동작한다는 점이다. vtable 은 링크 시점에 어느
모드의 connection_cl.cpp(또는 standalone 짝꿍) 가 컴파일
되었는지에 따라 채워진다.
상위 호출 모양 — 벌크 응답이 따라오는 요청, 콜백을 받는
요청, 스트리밍 응답 — 에는 network_cl.c 가 별도 래퍼를
제공한다.
| 함수 | 용도 |
|---|---|
net_client_request_no_reply | 단발 fire-and-forget (예: interrupt) |
net_client_request | 표준 요청/응답 |
net_client_request_with_callback | 처리 도중 서버가 거꾸로 콜백을 보낼 수 있는 호출(질의) |
net_client_request_recv_copyarea | 응답에 LC_COPYAREA 페이로드가 실려 옴 |
net_client_request_method_callback | 서버가 클라이언트 측 메서드를 호출(레거시 stored-procedure 경로) |
net_client_request_with_logwr_context | 복제 로그-라이터 스트리밍 |
net_client_request_recv_stream | 끝이 열린 스트리밍 응답 (예: loaddb 진행 상황) |
서버 측 핸들러 모양마다 짝이 되는 클라이언트 측 래퍼가 정해 져 있다.
클라이언트 스텁 예 — qmgr_execute_query
섹션 제목: “클라이언트 스텁 예 — qmgr_execute_query”같은 RPC를 양쪽에서 동시에 보면 대칭이 가장 잘 드러난다.
서버 측은 위에서 본 sqmgr_execute_query 다. 클라이언트
측은 다음과 같다.
// qmgr_execute_query — network_interface_cl.c:6916 (condensed)QFILE_LIST_ID *qmgr_execute_query (const XASL_ID *xasl_id, QUERY_ID *query_idp, int dbval_cnt, const DB_VALUE *dbvals, QUERY_FLAG flag, ...){ QFILE_LIST_ID *list_id = NULL; int req_error; char *request, *reply, *senddata = NULL; OR_ALIGNED_BUF (OR_XASL_ID_SIZE + OR_INT_SIZE * 5 + ...) a_request; OR_ALIGNED_BUF (OR_INT_SIZE * 7 + OR_PTR_ALIGNED_SIZE + OR_CACHE_TIME_SIZE) a_reply;
request = OR_ALIGNED_BUF_START (a_request); reply = OR_ALIGNED_BUF_START (a_reply);
/* 1. pack DB_VALUE host vars into bulk send buffer */ for (int i = 0; i < dbval_cnt; i++) senddata_size += OR_VALUE_ALIGNED_SIZE (&dbvals[i]); senddata = (char *) malloc (senddata_size); ptr = senddata; for (int i = 0; i < dbval_cnt; i++) ptr = or_pack_db_value (ptr, (DB_VALUE *) &dbvals[i]);
/* 2. pack the small fixed args into the request buffer */ ptr = request; OR_PACK_XASL_ID (ptr, xasl_id); ptr = or_pack_int (ptr, dbval_cnt); ptr = or_pack_int (ptr, senddata_size); ptr = or_pack_int (ptr, flag); OR_PACK_CACHE_TIME (ptr, clt_cache_time); ptr = or_pack_int (ptr, query_timeout);
/* 3. send + receive (callback variant: server may issue method callbacks back to us) */ req_error = net_client_request_with_callback (NET_SERVER_QM_QUERY_EXECUTE, request, request_len, reply, OR_ALIGNED_BUF_SIZE (a_reply), senddata, senddata_size, ...);
/* 4. unpack the reply */ ptr = or_unpack_ptr (reply + OR_INT_SIZE * 4, query_idp); OR_UNPACK_CACHE_TIME (ptr, &local_srv_cache_time); ... return list_id;}2단계의 or_pack_* 시퀀스는 sqmgr_execute_query 안의
or_unpack_* 시퀀스와 바이트 단위로 동일 하다 (같은 순서:
XASL_ID, dbval_cnt, data_size, query_flag, cache_time,
query_timeout). 한쪽이라도 어긋나면 와이어가 깨진다는 뜻
이다.
에러 전파
섹션 제목: “에러 전파”서버 측 에러는 평행한 채널로 흘러간다.
- 핸들러가
er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_*, ...)를 호출해 thread-local 에러 영역에 에러를 기록한다 (er_set은error_manager.c에 있다). - 핸들러가
return_error_to_client (thread_p, rid)를 부 르면,er_get_area_error()로 에러 영역을 직렬화한 뒤ERROR_TYPE패킷으로 (css_send_error) 보낸다. - 클라이언트의
net_client_request_internal이ERROR_TYPE을 읽으면set_server_error()를 부른다. 대부분의enum css_error_code는ER_NET_SERVER_CRASHED로 매핑 되지만, 서버 측 거부에 해당하는 특수 코드 (ER_DB_NO_MODIFICATIONS,ER_AU_DBA_ONLY) 는 원래 에러 를 그대로 보존한다. set_server_error()안의er_set_with_oserror가errno를 함께 박아 두기 때문에 클라이언트는 “서버가 내 소켓을 끊었다 와 서버가 논리 에러를 돌려 주었다” 를 구 분할 수 있다.
abort 경로(데드락 희생자, 질의 인터럽트) 는 다르다. 서버는
css_send_abort_to_client (conn, rid) 로 페이로드 없는
ABORT_TYPE 패킷을 보내고, 클라이언트는 ABORT_TYPE 을 “이
요청은 거부되었다, 다음 에러 패킷을 보고 이유를 확인하라” 는
신호로 인식한다.
종단 추적 — SELECT 한 건의 여정
섹션 제목: “종단 추적 — SELECT 한 건의 여정”sequenceDiagram
participant CL as 클라이언트
participant CSTUB as qmgr_execute_query<br/>(network_interface_cl.c)
participant CNET as net_client_request_with_callback<br/>(network_cl.c)
participant WIRE as TCP / Unix 도메인<br/>NET_HEADER 프레이밍
participant SWORK as connection::worker<br/>(connection_worker.cpp)
participant SDISP as net_server_request<br/>(network_sr.c)
participant SHND as sqmgr_execute_query<br/>(network_interface_sr.cpp)
CL->>CSTUB: qmgr_execute_query(xasl_id, dbvals, ...)
CSTUB->>CSTUB: or_pack_value(senddata, dbvals)<br/>or_pack_int(...)
CSTUB->>CNET: net_client_request_with_callback(<br/> NET_SERVER_QM_QUERY_EXECUTE, req, replybuf, senddata)
CNET->>WIRE: NET_HEADER{type=COMMAND, op=QM_QUERY_EXECUTE, rid=R}
CNET->>WIRE: NET_HEADER{type=DATA, rid=R} + req body
CNET->>WIRE: NET_HEADER{type=DATA, rid=R} + senddata body
WIRE->>SWORK: epoll_wait → readv
SWORK->>SDISP: 태스크 enqueue → cubthread::transaction 워커가 픽업
SDISP->>SDISP: net_Requests[NET_SERVER_QM_QUERY_EXECUTE]<br/>action_attribute = SET_DIAGNOSTICS_INFO | IN_TRANSACTION
SDISP->>SHND: sqmgr_execute_query(thread_p, rid, request, reqlen)
SHND->>SHND: OR_UNPACK_XASL_ID(...)<br/>or_unpack_int(...)<br/>css_receive_data_from_client → host vars
SHND->>SHND: xqmgr_execute_query(...) → list_id
SHND->>WIRE: NET_HEADER{type=DATA, rid=R} + reply (success, size, query_id)
SHND->>WIRE: NET_HEADER{type=DATA, rid=R} + list_id payload
SHND->>WIRE: NET_HEADER{type=DATA, rid=R} + page0 payload
WIRE->>CNET: 패킷 read, rid 매칭, 호출자의 reply/replydata 버퍼로 전달
CNET->>CSTUB: return; reply 안에 query_id, list_id 포인터
CSTUB->>CL: QFILE_LIST_ID *
이 흐름에서 짚어 둘 미묘한 점들이 있다.
- 헤더의
function_code필드가 의미를 갖는 것은COMMAND_TYPE패킷에 한정된다.DATA_TYPE패킷은request_id로 식별하고, 어느 큐에 넣어 두었던 버퍼인지로 행선지를 찾는다는 점이다. - 호스트 변수 (질의 파라미터) 는 요청 본문(XASL_ID, dbval_cnt,
query_flag 등을 담은) 과 별도의
DATA_TYPE패킷에 실려 간다. 둘을 분리해 두면 작은 요청 본문은 고정 크기OR_ALIGNED_BUF를 공용하면서, 벌크 파라미터는 자기 버퍼 로 따로 날 수 있다. - 서버 응답은
DATA_TYPE패킷이 여러 개 나올 수 있다. 작은 응답 한 개, 결과 list_id 한 개, 첫 결과 페이지 한 개 식 으로. 클라이언트의net_client_request_with_callback은 호출의 시그니처로부터 몇 개를 기대해야 하는지를 알고 있다. - 처리 도중 서버가 콜백 요청을 클라이언트로 거꾸로 보낼
수 있다 (메서드 호출, 사용자 입력 프롬프트, 콘솔 출력).
이 콜백들은
QUERY_SERVER_REQUEST값으로 인코딩된다 (connection_defs.h:313):{QUERY_END, METHOD_CALL, ASYNC_OBTAIN_USER_INPUT, GET_NEXT_LOG_PAGES, END_CALLBACK, CONSOLE_OUTPUT}.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”연결 수명 주기 (서버 측)
섹션 제목: “연결 수명 주기 (서버 측)”| 심볼 | 파일 | 역할 |
|---|---|---|
CSS_CONN_ENTRY | connection_defs.h | 연결당 상태 (fd, request_id, status, transaction_id, queue 들) |
css_initialize_conn | connection_sr.c | 풀에서 재사용하기 위해 CSS_CONN_ENTRY 를 리셋 |
css_make_conn | connection_sr.c | CSS_CONN_ENTRY 할당 + 리스트 초기화 |
css_init_conn_list | connection_sr.c | 부팅 시 connection-entry 배열 생성 |
css_shutdown_conn | connection_sr.c | 연결 해제 시 정리 — 리스트 finalise, 버전 문자열 free |
css_connect_to_master_server | connection_sr.c | 레거시 서버→마스터 등록 (Unix 도메인 또는 신형) |
css_set_proc_register | connection_sr.c | 등록 시 보낼 CSS_SERVER_PROC_REGISTER 페이로드 빌드 |
cubconn::master::connector::run | master_connector.cpp | 모던 진입점: connect → handshake → 마스터 포워딩 루프 실행 |
connector::handle_master_reception | master_connector.cpp | cub_master 가 포워딩한 fd 를 받아 워커 풀로 디스패치 |
cubconn::connection::pool | connection_pool.{cpp,hpp} | context 객체 + 워커 free-list; claim_context/retire_context |
cubconn::connection::worker | connection_worker.{cpp,hpp} | 워커당 epoll 루프; CSS 패킷을 읽고 요청 태스크를 enqueue |
worker::handle_command_header_packet | connection_worker.cpp | NET_HEADER 를 읽고 command/data/error/abort/close 로 분류 |
worker::handle_data_packet | connection_worker.cpp | RID 로 매칭, 큐에 걸린 사용자 버퍼로 전달 |
worker::push_task_into_worker_pool | connection_worker.cpp | 조립된 요청을 transaction 워커 풀로 넘김 |
css_internal_request_handler | server_support.c | 다리 — 연결에서 풀어내고 css_Server_request_handler 호출 |
css_initialize_server_interfaces | server_support.c | 부팅 시 request-handler 함수 포인터 설치 |
css_init | server_support.c | 서버 네트워크 main: pool 빌드, transaction 워커 등록, run |
css_pack_server_name | server_support.c | (서버 이름 + DB 버전 + 비트 플랫폼) 을 등록 블롭으로 인코딩 |
NRP 테이블과 디스패치
섹션 제목: “NRP 테이블과 디스패치”| 심볼 | 파일 | 역할 |
|---|---|---|
enum net_server_request | network.h | 오피코드 enum; 서버 진입점당 한 값 |
NET_SERVER_REQUEST_LIST 매크로 | network.h | enum + 이름 테이블을 동시에 펼치는 X-매크로 |
NET_SERVER_PING_WITH_HANDSHAKE = 999 | network.h | 대역 외 오피코드; 버전 변경에도 보존 |
NET_CAP_* 능력 비트 | network.h:304-311 | 핸드셰이크 시 협상; replica/read-only/interrupt 기능 게이트 |
struct net_request | network_request_def.hpp | 디스패치 테이블 한 행: (action_attribute, processing_function) |
enum net_req_act | network_request_def.hpp | 비트마스크: CHECK_DB_MODIFICATION / CHECK_AUTHORIZATION / etc. |
net_Requests[] (static) | network_sr.c | 디스패치 테이블 본체 |
net_server_init | network_sr.c:74 | 모든 오피코드를 net_Requests[opcode] 채우기 |
net_server_request | network_sr.c:791 | 디스패처 본체 — bounds check, 부수 조건, 핸들러 호출 |
net_server_start | network_sr.c:1058 | 서버 main(): er_init → cubthread → boot_restart_server → css_init |
net_server_conn_down | network_sr.c:1040 | 클라이언트 연결이 끊길 때 콜백 — 클라이언트 등록 해제 |
net_server_wakeup_workers | network_sr.c:927 | 셧다운 시 tran index 를 잡고 있는 쓰레드 인터럽트 |
get_net_request_name | network_sr.c | 로그용 역방향 조회 (opcode → string) |
서버 측 핸들러 견본
섹션 제목: “서버 측 핸들러 견본”| 심볼 | 파일 | 역할 |
|---|---|---|
server_ping | network_interface_sr.cpp:532 | 최소 핸들러: int 입력, int 출력 |
server_ping_with_handshake | network_interface_sr.cpp:563 | 초기 핸드셰이크; 비트 플랫폼·능력·버전 검사 |
sboot_register_client | network_interface_sr.cpp:3760 | 핸드셰이크 이후 연결당 등록 |
sqp_get_server_info | network_interface_sr.cpp:7962 | sysdate / local txn id 조회; 다중 DB_VALUE 응답 |
slocator_fetch | network_interface_sr.cpp:671 | OID 하나로 객체 fetch |
slocator_force | network_interface_sr.cpp:1381 | 벌크 DML: copy area 입력, 디스크립터 출력 |
sqmgr_prepare_query | network_interface_sr.cpp:5107 | 질의 prepare — XASL_ID 반환 |
sqmgr_execute_query | network_interface_sr.cpp:5399 | 준비된 질의 실행 — query_id, list_id, page0 반환 |
stran_server_commit | network_interface_sr.cpp (디스패치 표 경유) | 현재 트랜잭션 커밋; OUT_TRANSACTION 마킹 |
return_error_to_client | network_interface_sr.cpp (헬퍼) | er_get_area_error 래핑 후 css_send_error 호출 |
패커/언패커
섹션 제목: “패커/언패커”| 심볼 | 파일 | 역할 |
|---|---|---|
or_pack_int / or_unpack_int | object_representation.h | 4바이트 big-endian int |
or_pack_int64 / or_unpack_int64 | object_representation.h | 8바이트 int |
or_pack_string / or_unpack_string | object_representation.h | 길이 접두 C 문자열 |
or_pack_oid / or_unpack_oid | object_representation.h | 8바이트 (volid, pageid, slotid) 튜플 |
or_pack_value / or_unpack_value | object_representation.h | DB_VALUE (도메인 헤더 + null 플래그 + 값 바이트) |
or_packed_string_length | object_representation.h | 가변 길이 패킹용 사이징 헬퍼 |
OR_VALUE_ALIGNED_SIZE | object_representation.h | 매크로: DB_VALUE 의 정렬 패딩된 바이트 크기 |
OR_ALIGNED_BUF | object_representation.h | 정렬 요건을 만족하는 스택 버퍼 + 시작 포인터 |
OR_PACK_XASL_ID / OR_UNPACK_XASL_ID | object_representation.h | XASL_ID 복합 (sha1 + cache_flag + temp_file_id) |
OR_PACK_CACHE_TIME / OR_UNPACK_CACHE_TIME | object_representation.h | CACHE_TIME 복합 (sec + usec) |
OR_INT_SIZE / OR_OID_SIZE | object_representation.h | 모든 스텁에 등장하는 사이징 상수 |
OR_BUF | object_representation.h:1029 | heap/btree 패킹 코드가 쓰는 상위 버퍼 구조체 |
클라이언트 스텁
섹션 제목: “클라이언트 스텁”| 심볼 | 파일 | 역할 |
|---|---|---|
net_client_init | network_cl.c:3657 | 최초 연결: net_Server_host/name 셋, 핸드셰이크 수행 |
net_client_request_internal | network_cl.c:495 | __gv_cvar vtable 위의 핵심 송수신 |
net_client_request | network_cl.c:587 | 표준 래퍼 |
net_client_request_with_callback | network_cl.c:1153 | 호출 도중 서버가 콜백을 보내는 변종 |
net_client_request_recv_copyarea | network_cl.c:2317 | 응답에 LC_COPYAREA 가 실리는 변종 |
net_client_request_with_logwr_context | network_cl.c:2072 | 로그-라이터 스트리밍 변종 |
client_capabilities | network_cl.c:235 | 로컬 NET_CAP_* 비트마스크 빌드 |
check_server_capabilities | network_cl.c:259 | 핸드셰이크 시 클라이언트/서버 능력 비트 화해 |
set_server_error | network_cl.c | enum css_error_code → ER_NET_* 매핑 후 er_set 으로 전파 |
locator_force | network_interface_cl.c:697 | slocator_force 와 짝 |
qmgr_execute_query | network_interface_cl.c:6916 | sqmgr_execute_query 와 짝 |
locator_fetch | network_interface_cl.c:271 | slocator_fetch 와 짝 |
위치 힌트 (2026-04-30 기준)
섹션 제목: “위치 힌트 (2026-04-30 기준)”| 심볼 | 파일 | 대략 행 |
|---|---|---|
enum net_server_request | src/communication/network.h | 289 |
NET_SERVER_PING_WITH_HANDSHAKE = 999 | src/communication/network.h | 300 |
NET_CAP_BACKWARD_COMPATIBLE | src/communication/network.h | 304 |
get_endian_type | src/communication/network.h | 337 |
struct packet_header (NET_HEADER) | src/connection/connection_defs.h | 382 |
enum css_packet_type | src/connection/connection_defs.h | 185 |
enum css_command_type | src/connection/connection_defs.h | 67 |
struct css_conn_entry | src/connection/connection_defs.h | 437 |
enum net_req_act | src/communication/network_request_def.hpp | 32 |
struct net_request | src/communication/network_request_def.hpp | 43 |
net_server_init | src/communication/network_sr.c | 74 |
net_server_request | src/communication/network_sr.c | 791 |
net_server_start | src/communication/network_sr.c | 1058 |
net_Requests[] (table) | src/communication/network_sr.c | 68 |
server_ping | src/communication/network_interface_sr.cpp | 532 |
server_ping_with_handshake | src/communication/network_interface_sr.cpp | 563 |
slocator_fetch | src/communication/network_interface_sr.cpp | 671 |
slocator_force | src/communication/network_interface_sr.cpp | 1381 |
sboot_register_client | src/communication/network_interface_sr.cpp | 3760 |
sqmgr_prepare_query | src/communication/network_interface_sr.cpp | 5107 |
sqmgr_execute_query | src/communication/network_interface_sr.cpp | 5399 |
sqp_get_server_info | src/communication/network_interface_sr.cpp | 7962 |
css_initialize_conn | src/connection/connection_sr.c | 255 |
css_init_conn_list | src/connection/connection_sr.c | 420 |
css_make_conn | src/connection/connection_sr.c | 577 |
css_connect_to_master_server | src/connection/connection_sr.c | 1066 |
css_read_header | src/connection/connection_sr.c | 1428 |
css_receive_request | src/connection/connection_sr.c | 1470 |
css_receive_data | src/connection/connection_sr.c | 1487 |
css_internal_request_handler | src/connection/server_support.c | 450 |
css_initialize_server_interfaces | src/connection/server_support.c | 516 |
css_init | src/connection/server_support.c | 554 |
css_send_data_to_client | src/connection/server_support.c | 708 |
css_pack_server_name | src/connection/server_support.c | 1417 |
css_set_net_header | src/connection/connection_support.cpp | 1326 |
css_send_request_with_data_buffer | src/connection/connection_support.cpp | 1367 |
css_send_request | src/connection/connection_support.cpp | 1468 |
css_send_data | src/connection/connection_support.cpp | 1526 |
css_send_two_data | src/connection/connection_support.cpp | 1578 |
css_send_error | src/connection/connection_support.cpp | 1652 |
css_net_send | src/connection/connection_support.cpp | 1057 |
css_net_recv | src/connection/connection_support.cpp | 544 |
css_read_remaining_bytes | src/connection/connection_support.cpp | 501 |
cubconn::master::connector::run | src/connection/master_connector.cpp | 160 |
cubconn::connection::worker (class) | src/connection/connection_worker.hpp | 52 |
cubconn::connection::pool (class) | src/connection/connection_pool.hpp | 39 |
net_client_init | src/communication/network_cl.c | 3657 |
net_client_request_internal | src/communication/network_cl.c | 495 |
net_client_request | src/communication/network_cl.c | 587 |
net_client_request_with_callback | src/communication/network_cl.c | 1153 |
client_capabilities | src/communication/network_cl.c | 235 |
check_server_capabilities | src/communication/network_cl.c | 259 |
locator_force | src/communication/network_interface_cl.c | 697 |
qmgr_execute_query | src/communication/network_interface_cl.c | 6916 |
소스 검증 노트
섹션 제목: “소스 검증 노트”cubrid-pl-javasp.md 와의 관계
섹션 제목: “cubrid-pl-javasp.md 와의 관계”PL 패밀리(JavaSP, PL/CSQL) 는 cub_server 와 cub_pl 사이
에 별도 Unix 도메인 소켓을 두고, 별도의 와이어 프로토콜
을 쓴다. 이 문서가 다루는 CSS 프레이밍이 아니다. PL 와이어
는 더 단순하다(세션 id 와 RequestCode 를 담은 Header 하
나에 packed body 가 따라옴). 참여자가 고정되어 있고
(cub_server 하나, cub_pl 하나) 메시지 종류도 닫혀 있기
때문이다 (SP_CODE_INVOKE, SP_CODE_RESULT, SP_CODE_ERROR,
SP_CODE_INTERNAL_JDBC, …). NET_HEADER 도 쓰지 않고
net_Requests[] 를 거치지도 않는다. Java 측 디스패치는
ExecuteThread.run() 안의 switch 다.
두 프로토콜이 만나는 자리는 한 곳이다. JavaSP 가 서버 측
JDBC 드라이버(CUBRIDServerSideConnection) 로 SQL을
던지면, 요청은 원본 서버 워커 쓰레드로 같은
cub_pl-to-cub_server PL 소켓을 타고 돌아간다
(METHOD_CALLBACK_* 코드 경유). 이 문서가 다루는 정규 NRP
가 아니다. 그 콜백 경로의 정사는 PL 문서이고, 본 문서는
SP 호출을 처음 트리거한 원본 클라이언트 요청
(NET_SERVER_PL_CALL, 핸들러 spl_call) 에 대한 정사다.
드리프트와 미해석 영역
섹션 제목: “드리프트와 미해석 영역”-
마스터로의 등록 경로가 물리적으로 두 갈래. 레거시 경로 는
connection_sr.c의css_connect_to_master_server다 (비-Windows 는 TCP + Unix 도메인 핸드오프, Windows 는 서버 가 자기 포트를 직접 여는SERVER_REQUEST_NEW). 모던 경로 는master_connector.cpp의cubconn::master::connector::run으로, 현재css_init흐름에서 활성화된다. 두 코드 모두 현 소스에 컴파일되어 있 다. 레거시 함수는 심볼 테이블에서 참조되지만 활성 경로의css_init이 부르지는 않는다. standalone/non-pool 빌드와 과거 빌드 구성을 위해 유지된 것이다. “리스닝 소켓이 어디 서 바인드되나” 를 추적하는 독자는master_connector.cpp를 먼저 보면 된다는 점이다. -
net_Requests는 컴파일 타임에 크기가 고정. 배열 크기 는 X-매크로에서 파생된 enum 값NET_SERVER_REQUEST_END다. 오피코드를 추가하려면 클라이언트와 서버 양쪽을 모두 다시 컴파일해야 한다. 런타임 등록 메커니즘이 없다. 이는 manual- stub 선택의 의도된 결과지만, hot-deploy 확장이 새 RPC를 도입할 수 없다는 뜻이기도 하다. -
현재
net_server_request는OUT_TRANSACTION을 사실상 쓰지 않는다. 디스패처가 비트를 읽어 핸들러 반환 후conn->in_transaction을 끄긴 하지만,net_server_init을 훑어 보면 실제로 이 비트를 세팅하는 오피코드는NET_SERVER_TM_SERVER_COMMIT,NET_SERVER_TM_SERVER_ABORT와 두엇 정도뿐이다. 대부분의 read-only RPC 는 트랜잭션 전이를 비트마스크로 광고하지 않고, 핸들러 안에서 직접 관리한다. -
엔디언 검사는 단방향.
network.h의get_endian_type ()은 정의되어 있지만 현 소스의 표준 핸드셰이크에서 호출되 지 않는다. 프로토콜은 양 끝이 동일 엔디언이라고 암묵적으로 가정한다 (와이어 포맷은 htonl/ntohl big-endian 이지만, 피어 의 플랫폼 모델은client_bit_platform으로 32/64만 식별 하지 엔디언은 식별하지 않는다는 점이다). 현재 지원 타깃 (Linux x86_64, Windows x86_64) 이 모두 little-endian 이라 실무에서 문제 된 적은 없다. -
__gv_cvar가CS_MODE와SA_MODE의 분기점. 이 문서 가 클라이언트 스텁이 인자를 패킹하고 보내고 응답을 읽는다 라고 적었지만,SA_MODE에서는 같은 호출이 소켓을 거치지 않고 in-process 큐로 단락된다. 디버거에서net_client_request를 추적하는 독자라면__gv_cvar.css_send_req_to_server안 쪽에 브레이크포인트를 걸어 어느 모드인지 확인하면 된다. -
CSS 프레이머의
version과host_id는 예약 후 미사용.css_set_net_header가 0으로 채우고 수신자도 무시한다. 결국 등장하지 못한 미래 프로토콜 버저닝을 위해 남겨 둔 자리다. 현 코드에서는 버전 스큐가 패킷 단위가 아니라server_ping_with_handshake시점에rel_get_net_compatible로만 검증된다.
미해결 질문
섹션 제목: “미해결 질문”-
TLS / 암호화. 현재 소스에는 클라이언트/서버 채널 측 TLS 종단이 없다. 브로커(
cas) 가 브로커-클라이언트 구간에 SSL 을 지원하지만,cas↔cub_server구간은 평문이다. 종단 간 TLS 의 로드맵이 있는가, 그리고 그 자리는css_net_send층인가 그 위인가? -
압축. 큰
LC_COPYAREA페이로드와 질의 결과 페이지가 압축 없이 전송된다. 일부 이웃 DBMS (mysql_compress의 MySQL,pg_compress스트리밍의 PostgreSQL) 는 프레이머 에서 압축한다. 페이지-버퍼 인지 압축을 NRP 경로에 끼우는 조사가 진행되고 있는가? -
오피코드 단위 버저닝.
enum net_server_request끝에 오피코드를 더하는 일은 하위 호환이지만 (옛 클라이언트는 요청하지 않을 뿐), 기존 오피코드의 인자 레이아웃을 바꾸 는 일 은 옛 클라이언트를 조용히 깨뜨린다. 핸드셰이크 시 release-string 호환성 검사는 너무 거칠다. 오피코드 단위 wire-version 레지스트리가 어딘가 있는데 내가 못 찾은 것 인가? -
function_code가 16비트(short). 현재 ~150개 오피 코드 수준이라 한계까지는 멀지만, 이 필드의 크기는src/바깥 모듈(loaddb, CDC, flashback 이 이미 블록을 받아갔다) 이 자기 RPC를 등록하는 미래를 염두에 둔다는 신호다. 오피 코드 namespace 계획이 있는가, 아니면 테이블이 그냥 평탄 하게 계속 자라게 두는가? -
워커 풀 튜너블.
PRM_ID_TASK_WORKER,PRM_ID_CSS_MAX_CONNECTION_WORKER,PRM_ID_CSS_MIN_CONNECTION_WORKER가 합쳐져 동시성을 제한한다. 코드에는 epoll 의 edge-triggered 모드와 TBB 큐 와의 상호작용이 문서화되어 있지 않다. 운영 배포에서는 경험적으로 튜닝하고 있을 가능성이 크다. 용량 산정 가이드 가 한 편 있으면 유용할 것이다. -
css_internal_request_handler의 전역 간접. 핸들러 포 인터는 부팅 시점에css_initialize_server_interfaces로 한 번 꽂힌 뒤 교체 수단이 없다 (hot-patch, 텔레메트리 훅, A/B 트래픽 섀도잉). 의도된 경직성인가, 단일 테넌트 전제의 부산물인가?
src/connection/— CSS 프레이밍, 연결 수명 주기, 워커 풀src/communication/— NRP 테이블, 디스패치, 호출별 핸들러/스텁src/connection/AGENTS.md— 연결 모듈 개요src/communication/AGENTS.md— 프로토콜 모듈 개요references/cubrid/CLAUDE.md— CUBRID 엔진 최상위 구조