콘텐츠로 이동

(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 — 는 이 두 층의 변종을 구현한다.

세 가지 독립적인 설계 결정이 결과 프로토콜의 모양을 거의 다 정한다는 점이다. 본 문서의 뼈대도 이 세 결정 위에 얹힌다.

  1. 커스텀 바이너리 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 를 와이어 버퍼에 직접 쓴다. 중간 복사를 없애기 위함이다.

  2. 길이 접두 vs 메시지 타입 프레이밍. 길이 접두는 모든 메시지가 본문 길이를 포함하는 고정 크기 헤더로 시작하는 방식이다. 메시지 타입 프레이밍은 모든 메시지가 1바이트 태그로 시작하고 그 태그가 파서를 골라 안에서 길이를 읽는 방식이다. PostgreSQL의 FE/BE 프로토콜은 후자(메시지 타입 바이트가 가장 먼저), MySQL classic은 전자(매 패킷마다 4바 이트 (length, sequence) 접두), CUBRID는 전자(buffer_size 필드를 가진 NET_HEADER 구조체) 를 택한다. 전자가 대칭 수신 에서 유리하다. 수신 루프가 메시지 타입에 따라 분기 하지 않는 한 덩어리 코드다. 하지만 메시지 종류 가 근본적으로 다른 모양일 때 (handshake와 row event는 무 관하다) 분기 비용이 다른 곳으로 옮겨갈 뿐이다. CUBRID는 이를 본문 안에서 변형을 인코딩해 풀어낸다.

  3. 상태 없는 디스패치 테이블 vs 코드 생성 스텁. 어떤 엔진 은 코드 생성 방식을 쓴다. IDL이 호출 하나하나를 기술하면 컴파일러가 클라이언트 스텁과 서버 스켈레톤을 C/C++ 로 뽑 고, 링커가 바이너리에 합친다. 타입 안전성을 얻지만 빌드 단계가 추가되고 클라이언트와 서버 컴파일이 결합된다. CUBRID는 수동 스텁 노선을 택했다. 서버 진입점마다 network_interface_sr.cpp::s<name> 핸들러가 손으로 작성 되어 인자를 풀고, 짝이 되는 network_interface_cl.c::<name> 클라이언트 스텁이 인자를 포장한다. 둘은 관습으로 동기화되며, 연결고리는 NET_SERVER_* 오피코드다. 정적 타이핑을 포기하는 대가로 단일 진입점 — network.h — 을 얻는다 (모든 새 RPC가 이 파일에서 선언 된다). 핸들러 하나만 핫 패치하기도 쉽다.

이 세 답을 명시하고 나면, CUBRID 네트워크 프로토콜의 나머지는 바이너리·길이 접두·수동 스텁이라는 설계 공간의 한 모서리를 선택했을 때 자연스럽게 따라 나오는 결과로 읽힌다.

교과서적 프레이밍 층 아래로 내려오면 모든 주요 클라이언트/서버 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_commandCOM_* 코드를 switch 한다. CUBRID는 이걸 정통 테이블 형태로 들고 있다. network_sr.cstatic 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 ridNET_HEADER 안에 있다. 서버는 모든 응답에 그 값을 쓰고, 클라이언트는 대기 중인 request_queue / data_queue 항목과 맞춰 본다. PostgreSQL은 한 연결 위에 엄격한 요청/응답 순서를 강제해 이 문제를 우회 한다. MySQL은 1바이트 시퀀스 번호를 쓴다. CUBRID의 RID는 연결 단위로 굴러가는 16비트 카운터라서, 한 질의 안에서 동시 콜백들이 충돌하지 않는다는 점이다.

디스패치 루프를 소유하는 워커 풀

섹션 제목: “디스패치 루프를 소유하는 워커 풀”

서버는 디스패치를 I/O 쓰레드 위에서 할 수 없다. 핸들러가 락, 페이지 읽기, 하위 RPC(PL 호출) 같은 곳에서 막힐 수 있기 때문 이다. 표준 모양은 워커 풀이다. I/O 쓰레드가 요청을 읽어 태 스크로 포장해 큐에 넣고, 워커 쓰레드가 그 태스크를 꺼내 핸들 러를 부른다. CUBRID는 두 풀을 쓴다. 연결 I/O를 위한 cubconn::connection::worker (epoll 기반) 와, cubthreadtransaction 워커 풀(핸들러 실행 담당) 이다. 연결 워커가 push_task_into_worker_pool 로 트랜잭션 풀에 핸들러 호출을 밀어 넣는 식이다.

이론적 개념CUBRID 명칭
와이어 프레이머 헤더connection_defs.hNET_HEADER 구조체 (9개 필드, 고정 크기)
길이 접두 본문 길이header.buffer_size (와이어 위에서는 htonl/ntohl)
패킷 종류 태그header.type{COMMAND_TYPE, DATA_TYPE, ABORT_TYPE, CLOSE_TYPE, ERROR_TYPE}
호출별 request idheader.request_id (16비트, css_get_request_id 가 할당)
서버 함수 코드 (RPC 오피코드)header.function_code (16비트) + enum net_server_request
정적 디스패치 테이블network_sr.cstatic 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.cnet_client_request* 계열
호출별 서버 핸들러network_interface_sr.cpps<module>_<verb> (예: slocator_force, sqmgr_execute_query)
연결 수락자master_connector.cppcubconn::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-311NET_CAP_* 매크로
클라이언트/서버 엔디언 검사network.h:337-343 의 inline get_endian_type ()

네트워크 모듈의 이동 부품은 다섯이다. 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

CUBRID 클라이언트/서버 와이어의 모든 패킷은 고정 크기 NET_HEADER 로 시작한다 (9개 필드, htonl 로 인코딩된 big-endian). 구조체는 한 번만 정의되고 양쪽이 공유한다.

// packet_header — connection_defs.h
typedef 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-192
enum 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:1326
void
css_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_mastercub_server 로 핸드오프

섹션 제목: “연결 수락 — cub_master 가 cub_server 로 핸드오프”

CUBRID의 연결 수락 구조는 흔치 않은 두 프로세스 분리다. 공개 TCP 리스닝 소켓은 별도의 cub_master 프로세스가 소유 한다. 데이터베이스 서버 본체(cub_server) 는 공개 포트를 바인드하지 않는다. 클라이언트가 접속하면 cub_master 가 연결을 받아 인사한 뒤 어떤 데이터베이스를 원하는지 판별해서, 해당 cub_server 에 파일 디스크립터를 Unix 도메인 소켓으로 넘긴다.

이 프로토콜의 서버 측은 cubconn::master::connector (master_connector.cpp) 에 산다. 서버 부팅 시 net_server_startcss_init 을 부르고, 그 안에서 master::connector 를 만들어 connect → prepare_handshake → execute 순으로 호출한다.

// connector::run — master_connector.cpp:160
bool 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 루프로 들어간다.

  1. 마스터 측 수신 (handle_master_reception) — cub_master 가 새 클라이언트 연결을, 그 클라이언트의 파일 디스크립터를 실은 Unix 도메인 메시지로 포워딩한다. 서버는 그 fd를 받아 풀에서 새 CSS_CONN_ENTRY 를 떼어내고 connection::worker 풀로 디스패치한다.

  2. 워커 통계 / 셧다운 제어 — 부수적인 제어 메시지가 같은 채널로 흐른다.

새 서버 등록에 대한 마스터 측 응답은 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.ccss_open_server_connection_socket).

레거시 서버 단독 경로는 connection_sr.c:1066css_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:563server_ping_with_handshake 핸들러가 다음을 수행한다.

  1. 클라이언트 릴리스 문자열, 능력 플래그, 비트 플랫폼(32 vs 64), 클라이언트 종류, 호스트 이름을 읽는다.
  2. rel_get_net_compatible(client, server) 로 호환성을 검사한다.
  3. check_client_capabilities 로 능력 비트를 검증한다.
  4. css_increment_num_conn(client_type) 으로 연결 슬롯을 예 약한다.
  5. 서버 릴리스 문자열, 능력 비트, 서버 호스트 이름, REL_COMPATIBILITY 판정을 응답한다.

이 오피코드가 이후 모든 디스패치의 문지기 이기 때문에 — 그 다음 요청들은 클라이언트와 서버가 버전 호환임을 가정해도 된다. net_server_request 의 디스패치는 테이블 조회 전에 이 케이 스를 단락 평가한다.

// net_server_request — network_sr.c:791
if (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 0x00000002

replica 전용 브로커가 비-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_WORKERPRM_ID_CSS_MAX_CONNECTION_WORKER 사이에서 변동한다.

  • transaction 워커 풀 은 전역으로 등록된다.

    // server_support.c:548
    REGISTER_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:450
static int
css_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 file
static 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:74
req_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:791
if (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 function
func = 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_MODIFICATIONDB가 쓰기를 받아야 한다 (read-only 모드, replica, HA 적용기 정지 시 거부)
CHECK_AUTHORIZATION클라이언트가 DBA/소유자여야 한다. 아니면 ER_AU_DBA_ONLY 로 거부
SET_DIAGNOSTICS_INFOperfmon 타이머(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:532
void
server_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)
void
sqp_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, &lt_dbval);
buffer_length += OR_VALUE_ALIGNED_SIZE (&lt_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, &lt_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)
void
slocator_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.h
extern 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단계다.

  1. OR_ALIGNED_BUF 로 고정 크기 인자 버퍼를 잡고,
  2. or_pack_* 로 인자를 채워 넣고,
  3. 고정 크기 응답 버퍼를 잡고,
  4. 적절한 net_client_request_* 변종을 부르고,
  5. or_unpack_* 로 응답을 풀고,
  6. 결과를 호출자에게 맞게 변환한다.

디스패처는 net_client_request_internal (network_cl.c:495) 이다.

// net_client_request_internal — network_cl.c:495 (condensed)
static int
net_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). 한쪽이라도 어긋나면 와이어가 깨진다는 뜻 이다.

서버 측 에러는 평행한 채널로 흘러간다.

  1. 핸들러가 er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_*, ...) 를 호출해 thread-local 에러 영역에 에러를 기록한다 (er_seterror_manager.c 에 있다).
  2. 핸들러가 return_error_to_client (thread_p, rid) 를 부 르면, er_get_area_error() 로 에러 영역을 직렬화한 뒤 ERROR_TYPE 패킷으로 (css_send_error) 보낸다.
  3. 클라이언트의 net_client_request_internalERROR_TYPE 을 읽으면 set_server_error() 를 부른다. 대부분의 enum css_error_codeER_NET_SERVER_CRASHED 로 매핑 되지만, 서버 측 거부에 해당하는 특수 코드 (ER_DB_NO_MODIFICATIONS, ER_AU_DBA_ONLY) 는 원래 에러 를 그대로 보존한다.
  4. set_server_error() 안의 er_set_with_oserrorerrno 를 함께 박아 두기 때문에 클라이언트는 “서버가 내 소켓을 끊었다 와 서버가 논리 에러를 돌려 주었다” 를 구 분할 수 있다.

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_ENTRYconnection_defs.h연결당 상태 (fd, request_id, status, transaction_id, queue 들)
css_initialize_connconnection_sr.c풀에서 재사용하기 위해 CSS_CONN_ENTRY 를 리셋
css_make_connconnection_sr.cCSS_CONN_ENTRY 할당 + 리스트 초기화
css_init_conn_listconnection_sr.c부팅 시 connection-entry 배열 생성
css_shutdown_connconnection_sr.c연결 해제 시 정리 — 리스트 finalise, 버전 문자열 free
css_connect_to_master_serverconnection_sr.c레거시 서버→마스터 등록 (Unix 도메인 또는 신형)
css_set_proc_registerconnection_sr.c등록 시 보낼 CSS_SERVER_PROC_REGISTER 페이로드 빌드
cubconn::master::connector::runmaster_connector.cpp모던 진입점: connect → handshake → 마스터 포워딩 루프 실행
connector::handle_master_receptionmaster_connector.cppcub_master 가 포워딩한 fd 를 받아 워커 풀로 디스패치
cubconn::connection::poolconnection_pool.{cpp,hpp}context 객체 + 워커 free-list; claim_context/retire_context
cubconn::connection::workerconnection_worker.{cpp,hpp}워커당 epoll 루프; CSS 패킷을 읽고 요청 태스크를 enqueue
worker::handle_command_header_packetconnection_worker.cppNET_HEADER 를 읽고 command/data/error/abort/close 로 분류
worker::handle_data_packetconnection_worker.cppRID 로 매칭, 큐에 걸린 사용자 버퍼로 전달
worker::push_task_into_worker_poolconnection_worker.cpp조립된 요청을 transaction 워커 풀로 넘김
css_internal_request_handlerserver_support.c다리 — 연결에서 풀어내고 css_Server_request_handler 호출
css_initialize_server_interfacesserver_support.c부팅 시 request-handler 함수 포인터 설치
css_initserver_support.c서버 네트워크 main: pool 빌드, transaction 워커 등록, run
css_pack_server_nameserver_support.c(서버 이름 + DB 버전 + 비트 플랫폼) 을 등록 블롭으로 인코딩
심볼파일역할
enum net_server_requestnetwork.h오피코드 enum; 서버 진입점당 한 값
NET_SERVER_REQUEST_LIST 매크로network.henum + 이름 테이블을 동시에 펼치는 X-매크로
NET_SERVER_PING_WITH_HANDSHAKE = 999network.h대역 외 오피코드; 버전 변경에도 보존
NET_CAP_* 능력 비트network.h:304-311핸드셰이크 시 협상; replica/read-only/interrupt 기능 게이트
struct net_requestnetwork_request_def.hpp디스패치 테이블 한 행: (action_attribute, processing_function)
enum net_req_actnetwork_request_def.hpp비트마스크: CHECK_DB_MODIFICATION / CHECK_AUTHORIZATION / etc.
net_Requests[] (static)network_sr.c디스패치 테이블 본체
net_server_initnetwork_sr.c:74모든 오피코드를 net_Requests[opcode] 채우기
net_server_requestnetwork_sr.c:791디스패처 본체 — bounds check, 부수 조건, 핸들러 호출
net_server_startnetwork_sr.c:1058서버 main(): er_init → cubthread → boot_restart_server → css_init
net_server_conn_downnetwork_sr.c:1040클라이언트 연결이 끊길 때 콜백 — 클라이언트 등록 해제
net_server_wakeup_workersnetwork_sr.c:927셧다운 시 tran index 를 잡고 있는 쓰레드 인터럽트
get_net_request_namenetwork_sr.c로그용 역방향 조회 (opcode → string)
심볼파일역할
server_pingnetwork_interface_sr.cpp:532최소 핸들러: int 입력, int 출력
server_ping_with_handshakenetwork_interface_sr.cpp:563초기 핸드셰이크; 비트 플랫폼·능력·버전 검사
sboot_register_clientnetwork_interface_sr.cpp:3760핸드셰이크 이후 연결당 등록
sqp_get_server_infonetwork_interface_sr.cpp:7962sysdate / local txn id 조회; 다중 DB_VALUE 응답
slocator_fetchnetwork_interface_sr.cpp:671OID 하나로 객체 fetch
slocator_forcenetwork_interface_sr.cpp:1381벌크 DML: copy area 입력, 디스크립터 출력
sqmgr_prepare_querynetwork_interface_sr.cpp:5107질의 prepare — XASL_ID 반환
sqmgr_execute_querynetwork_interface_sr.cpp:5399준비된 질의 실행 — query_id, list_id, page0 반환
stran_server_commitnetwork_interface_sr.cpp (디스패치 표 경유)현재 트랜잭션 커밋; OUT_TRANSACTION 마킹
return_error_to_clientnetwork_interface_sr.cpp (헬퍼)er_get_area_error 래핑 후 css_send_error 호출
심볼파일역할
or_pack_int / or_unpack_intobject_representation.h4바이트 big-endian int
or_pack_int64 / or_unpack_int64object_representation.h8바이트 int
or_pack_string / or_unpack_stringobject_representation.h길이 접두 C 문자열
or_pack_oid / or_unpack_oidobject_representation.h8바이트 (volid, pageid, slotid) 튜플
or_pack_value / or_unpack_valueobject_representation.hDB_VALUE (도메인 헤더 + null 플래그 + 값 바이트)
or_packed_string_lengthobject_representation.h가변 길이 패킹용 사이징 헬퍼
OR_VALUE_ALIGNED_SIZEobject_representation.h매크로: DB_VALUE 의 정렬 패딩된 바이트 크기
OR_ALIGNED_BUFobject_representation.h정렬 요건을 만족하는 스택 버퍼 + 시작 포인터
OR_PACK_XASL_ID / OR_UNPACK_XASL_IDobject_representation.hXASL_ID 복합 (sha1 + cache_flag + temp_file_id)
OR_PACK_CACHE_TIME / OR_UNPACK_CACHE_TIMEobject_representation.hCACHE_TIME 복합 (sec + usec)
OR_INT_SIZE / OR_OID_SIZEobject_representation.h모든 스텁에 등장하는 사이징 상수
OR_BUFobject_representation.h:1029heap/btree 패킹 코드가 쓰는 상위 버퍼 구조체
심볼파일역할
net_client_initnetwork_cl.c:3657최초 연결: net_Server_host/name 셋, 핸드셰이크 수행
net_client_request_internalnetwork_cl.c:495__gv_cvar vtable 위의 핵심 송수신
net_client_requestnetwork_cl.c:587표준 래퍼
net_client_request_with_callbacknetwork_cl.c:1153호출 도중 서버가 콜백을 보내는 변종
net_client_request_recv_copyareanetwork_cl.c:2317응답에 LC_COPYAREA 가 실리는 변종
net_client_request_with_logwr_contextnetwork_cl.c:2072로그-라이터 스트리밍 변종
client_capabilitiesnetwork_cl.c:235로컬 NET_CAP_* 비트마스크 빌드
check_server_capabilitiesnetwork_cl.c:259핸드셰이크 시 클라이언트/서버 능력 비트 화해
set_server_errornetwork_cl.cenum css_error_codeER_NET_* 매핑 후 er_set 으로 전파
locator_forcenetwork_interface_cl.c:697slocator_force 와 짝
qmgr_execute_querynetwork_interface_cl.c:6916sqmgr_execute_query 와 짝
locator_fetchnetwork_interface_cl.c:271slocator_fetch 와 짝
심볼파일대략 행
enum net_server_requestsrc/communication/network.h289
NET_SERVER_PING_WITH_HANDSHAKE = 999src/communication/network.h300
NET_CAP_BACKWARD_COMPATIBLEsrc/communication/network.h304
get_endian_typesrc/communication/network.h337
struct packet_header (NET_HEADER)src/connection/connection_defs.h382
enum css_packet_typesrc/connection/connection_defs.h185
enum css_command_typesrc/connection/connection_defs.h67
struct css_conn_entrysrc/connection/connection_defs.h437
enum net_req_actsrc/communication/network_request_def.hpp32
struct net_requestsrc/communication/network_request_def.hpp43
net_server_initsrc/communication/network_sr.c74
net_server_requestsrc/communication/network_sr.c791
net_server_startsrc/communication/network_sr.c1058
net_Requests[] (table)src/communication/network_sr.c68
server_pingsrc/communication/network_interface_sr.cpp532
server_ping_with_handshakesrc/communication/network_interface_sr.cpp563
slocator_fetchsrc/communication/network_interface_sr.cpp671
slocator_forcesrc/communication/network_interface_sr.cpp1381
sboot_register_clientsrc/communication/network_interface_sr.cpp3760
sqmgr_prepare_querysrc/communication/network_interface_sr.cpp5107
sqmgr_execute_querysrc/communication/network_interface_sr.cpp5399
sqp_get_server_infosrc/communication/network_interface_sr.cpp7962
css_initialize_connsrc/connection/connection_sr.c255
css_init_conn_listsrc/connection/connection_sr.c420
css_make_connsrc/connection/connection_sr.c577
css_connect_to_master_serversrc/connection/connection_sr.c1066
css_read_headersrc/connection/connection_sr.c1428
css_receive_requestsrc/connection/connection_sr.c1470
css_receive_datasrc/connection/connection_sr.c1487
css_internal_request_handlersrc/connection/server_support.c450
css_initialize_server_interfacessrc/connection/server_support.c516
css_initsrc/connection/server_support.c554
css_send_data_to_clientsrc/connection/server_support.c708
css_pack_server_namesrc/connection/server_support.c1417
css_set_net_headersrc/connection/connection_support.cpp1326
css_send_request_with_data_buffersrc/connection/connection_support.cpp1367
css_send_requestsrc/connection/connection_support.cpp1468
css_send_datasrc/connection/connection_support.cpp1526
css_send_two_datasrc/connection/connection_support.cpp1578
css_send_errorsrc/connection/connection_support.cpp1652
css_net_sendsrc/connection/connection_support.cpp1057
css_net_recvsrc/connection/connection_support.cpp544
css_read_remaining_bytessrc/connection/connection_support.cpp501
cubconn::master::connector::runsrc/connection/master_connector.cpp160
cubconn::connection::worker (class)src/connection/connection_worker.hpp52
cubconn::connection::pool (class)src/connection/connection_pool.hpp39
net_client_initsrc/communication/network_cl.c3657
net_client_request_internalsrc/communication/network_cl.c495
net_client_requestsrc/communication/network_cl.c587
net_client_request_with_callbacksrc/communication/network_cl.c1153
client_capabilitiessrc/communication/network_cl.c235
check_server_capabilitiessrc/communication/network_cl.c259
locator_forcesrc/communication/network_interface_cl.c697
qmgr_execute_querysrc/communication/network_interface_cl.c6916

PL 패밀리(JavaSP, PL/CSQL) 는 cub_servercub_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.ccss_connect_to_master_server 다 (비-Windows 는 TCP + Unix 도메인 핸드오프, Windows 는 서버 가 자기 포트를 직접 여는 SERVER_REQUEST_NEW). 모던 경로 는 master_connector.cppcubconn::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_requestOUT_TRANSACTION 을 사실상 쓰지 않는다. 디스패처가 비트를 읽어 핸들러 반환 후 conn->in_transaction 을 끄긴 하지만, net_server_init 을 훑어 보면 실제로 이 비트를 세팅하는 오피코드는 NET_SERVER_TM_SERVER_COMMIT, NET_SERVER_TM_SERVER_ABORT 와 두엇 정도뿐이다. 대부분의 read-only RPC 는 트랜잭션 전이를 비트마스크로 광고하지 않고, 핸들러 안에서 직접 관리한다.

  • 엔디언 검사는 단방향. network.hget_endian_type () 은 정의되어 있지만 현 소스의 표준 핸드셰이크에서 호출되 지 않는다. 프로토콜은 양 끝이 동일 엔디언이라고 암묵적으로 가정한다 (와이어 포맷은 htonl/ntohl big-endian 이지만, 피어 의 플랫폼 모델은 client_bit_platform 으로 32/64만 식별 하지 엔디언은 식별하지 않는다는 점이다). 현재 지원 타깃 (Linux x86_64, Windows x86_64) 이 모두 little-endian 이라 실무에서 문제 된 적은 없다.

  • __gv_cvarCS_MODESA_MODE 의 분기점. 이 문서 가 클라이언트 스텁이 인자를 패킹하고 보내고 응답을 읽는다 라고 적었지만, SA_MODE 에서는 같은 호출이 소켓을 거치지 않고 in-process 큐로 단락된다. 디버거에서 net_client_request 를 추적하는 독자라면 __gv_cvar.css_send_req_to_server 안 쪽에 브레이크포인트를 걸어 어느 모드인지 확인하면 된다.

  • CSS 프레이머의 versionhost_id 는 예약 후 미사용. css_set_net_header 가 0으로 채우고 수신자도 무시한다. 결국 등장하지 못한 미래 프로토콜 버저닝을 위해 남겨 둔 자리다. 현 코드에서는 버전 스큐가 패킷 단위가 아니라 server_ping_with_handshake 시점에 rel_get_net_compatible 로만 검증된다.

  1. TLS / 암호화. 현재 소스에는 클라이언트/서버 채널 측 TLS 종단이 없다. 브로커(cas) 가 브로커-클라이언트 구간에 SSL 을 지원하지만, cascub_server 구간은 평문이다. 종단 간 TLS 의 로드맵이 있는가, 그리고 그 자리는 css_net_send 층인가 그 위인가?

  2. 압축. 큰 LC_COPYAREA 페이로드와 질의 결과 페이지가 압축 없이 전송된다. 일부 이웃 DBMS (mysql_compress 의 MySQL, pg_compress 스트리밍의 PostgreSQL) 는 프레이머 에서 압축한다. 페이지-버퍼 인지 압축을 NRP 경로에 끼우는 조사가 진행되고 있는가?

  3. 오피코드 단위 버저닝. enum net_server_request 끝에 오피코드를 더하는 일은 하위 호환이지만 (옛 클라이언트는 요청하지 않을 뿐), 기존 오피코드의 인자 레이아웃을 바꾸 는 일 은 옛 클라이언트를 조용히 깨뜨린다. 핸드셰이크 시 release-string 호환성 검사는 너무 거칠다. 오피코드 단위 wire-version 레지스트리가 어딘가 있는데 내가 못 찾은 것 인가?

  4. function_code 가 16비트(short). 현재 ~150개 오피 코드 수준이라 한계까지는 멀지만, 이 필드의 크기는 src/ 바깥 모듈(loaddb, CDC, flashback 이 이미 블록을 받아갔다) 이 자기 RPC를 등록하는 미래를 염두에 둔다는 신호다. 오피 코드 namespace 계획이 있는가, 아니면 테이블이 그냥 평탄 하게 계속 자라게 두는가?

  5. 워커 풀 튜너블. PRM_ID_TASK_WORKER, PRM_ID_CSS_MAX_CONNECTION_WORKER, PRM_ID_CSS_MIN_CONNECTION_WORKER 가 합쳐져 동시성을 제한한다. 코드에는 epoll 의 edge-triggered 모드와 TBB 큐 와의 상호작용이 문서화되어 있지 않다. 운영 배포에서는 경험적으로 튜닝하고 있을 가능성이 크다. 용량 산정 가이드 가 한 편 있으면 유용할 것이다.

  6. 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 엔진 최상위 구조