콘텐츠로 이동

(KO) CUBRID Broker — CAS 프로세스 풀, 연결 라우팅, 클라이언트 측 프론트엔드

목차

관계형 데이터베이스 엔진은 결국 네트워크에 자기를 노출해야 한다. 그런데 그 노출이 시작되는 순간 세 개의 서로 다른 힘이 설계를 잡아당기기 시작한다는 점이다. 엔진 쪽은 오래 살고 비싸게 만들어 둔 인프로세스 상태 — 파서, XASL 캐시, 세션별 prepared statement 등록표, 인증된 트랜잭션 디스크립터 — 를 TCP가 끊길 때마다 같이 무너뜨리고 싶어 하지 않는다. 드라이 버 쪽은 가장 단순한 모델만 쓰고 싶다. 소켓을 열고, SQL을 보내고, 행을 읽고, 소켓을 닫는다는 것이다. 운영자 쪽은, 드라이버 측에서 몇 개의 풀이 동시에 두드리든 상관없이, 호스 트가 실제로 먹여 살릴 수 있는 무거운 컨텍스트의 수에 수평적 인 상한을 두고 싶다는 것이다. Database Internals (Petrov, 13장 Database Architectures) 는 이 긴장의 결과를 한 클라이 언트당 전용 워커 컨텍스트를 두는 dedicated 모델과, 더 작 은 풀 위로 클라이언트들을 다중화하는 shared 모델의 선택 으로 정리한다. 그리고 그 사이에 connection broker 가 끼 어 들어, 값싼 TCP 소켓 다수를 비싼 엔진 핸들 소수로 변환한 다는 그림을 그린다.

세 가지 서로 독립적인 설계 결정이 모든 구체적 broker 구현 의 골격을 정한다. 본 문서의 나머지 구조는 이 세 결정 위에 앉는다.

  1. 프로세스 모델 — 쓰레드인가, 프로세스인가, 혼합인가. broker는 풀에 묶인 워커들을 한 주소 공간 안의 쓰레드로 돌릴 수도 있고 (컨텍스트 스위치 비용은 낮지만 워커 하나 가 죽으면 풀 전체가 같이 죽고, 막힌 워커가 공유 malloc 아레나를 굶길 수 있다), 독립 프로세스로 돌릴 수도 있고 (핸드오프마다 fork/IPC 비용이 추가로 들지만 워커 하나 가 자체 fault domain이고 exit 가 다른 어떤 것도 오염 시키지 않는다), 또는 혼합으로 돌릴 수도 있다 (예컨대 accept된 소켓당 I/O 쓰레드 하나가 프로세스 풀로 분기시 키는 식). CUBRID는 순수 프로세스 모델을 골랐다. cub_broker 부모가 부팅 시점에 고정 크기의 cub_cas 자식 배열을 fork 해 두고, 명시적으로 재활용되지 않는 한 broker의 수명만큼 그대로 살아 있다. 부팅 시 한 번의 fork+execle 와 클라이언트 한 명당 한 번의 SCM_RIGHTS 소켓 패스라는 비용을 내고, 그 대가로 깨끗한 fault 격리 서사, OS 수준의 자원 상한 (appl_server_max_num), 그리 고 한 CAS에 gdb 를 붙여도 다른 CAS들은 멈추지 않는다 는 운영성을 산다.

  2. 풀링 단위 — 세션 풀링인가, 트랜잭션 풀링인가. 세션 풀링 에서는 클라이언트가 TCP 세션 전체 동안 (드라이버 connect() 부터 close() 까지) 자기 전용 워 커 컨텍스트를 받아 가지고, 워커는 클라이언트가 끊긴 뒤 에야 다시 쓸 수 있게 된다. 트랜잭션 풀링 에서는 워 커 컨텍스트가 열린 트랜잭션의 수명만큼만 점유되며, 트랜 잭션 사이에는 클라이언트가 어느 워커에도 매여 있지 않 고 다음 문장을 비어 있는 워커가 가져갈 수 있다. 트랜잭 션 풀링은 워커 재사용률이 훨씬 높지만, 세션에 묶인 모든 상태 — prepared statement, SET 변수, 임시 테이블 — 가 워커가 복원할 수 있는 세션 id 뒤에 살거나, 트랜잭션 마다 다시 만들어져야 한다는 부담을 진다. CUBRID는 세션 풀링 쪽에 가깝게 자리잡는다. 클라이언트를 받아 들인 CAS는 드라이버가 원하는 만큼 그 클라이언트를 붙들고 있고, KEEP_CONNECTION (ON / AUTO) 으로 트랜잭션 사이의 리셋 여부를 옵션으로 둔다. AUTO 가 켜져 있고 클라이언트가 time_to_kill 을 넘겨 idle 상태이면, 드라 이버가 트랜잭션 사이에 있는 동안 broker가 그 CAS를 회수 대상으로 표시할 수 있다. 드라이버가 다시 돌아오면 broker가 새로 배정한다. 세션 id의 연속성은 서버 쪽 세 션 표가 책임진다 (cubrid-server-session.md 참조). broker가 들고 있는 게 아니다.

  3. broker가 와이어를 어디까지 프록시하는가. broker는 SQL 프로토콜의 L7 프록시다 (PINGQC 쿼리 취소, connect 핸드셰이크를 직접 이해하고 그 외는 라우팅한다). 그래서 클라이언트 소켓을 요청 수명 동안 broker에 유지 할지, 아니면 워커에게 넘겨 직접 통신하게 할지를 결정 해야 한다. 넘기는 쪽이 빠르다 (패킷당 memcpy 한 번 덜, broker 병목 없음). 그러나 그러려면 워커마다 별개 리스너 를 두거나 (TCP 포트가 워커 수만큼 — 낭비고 방화벽 운영 이 어렵다), 두 무관한 프로세스 사이에서 열린 파일 디스 크립터를 옮길 수 있어야 한다. POSIX는 후자에 정확히 한 가지 원시 도구만 준다. AF_UNIX SOCK_STREAM 위의 SCM_RIGHTS ancillary 메시지인데, 커널이 recvmsg 동 안 디스크립터를 수신측 프로세스 표에 복제해 준다. 이게 리눅스의 표준 관용구다 (nginx, 일부 모드의 pgbouncer, Postgres pg_dump 의 병렬 인프라가 모두 이걸 쓴다). CUBRID도 이걸 쓴다. broker는 공개 broker 포트에서 TCP 소켓을 accept한 뒤 비어 있는 CAS를 골라 미리 만들어 둔 AF_UNIX 랑데부 채널로 커널 디스 크립터send_fd() 로 CAS에게 보내고, 자기 사본은 닫는다. 그 시점부터 broker와 CAS는 공유 메모리만 거쳐 통신하고, 클라이언트 바이트는 CAS↔드라이버로 곧장 흐른다. broker는 살아 있는 쿼리를 떨어뜨리지 않고도 디버깅하거 나 재시작할 수 있다 (원칙적으로는 Open Questions 참조).

이 세 가지 결정이 명명되고 나면 — 프로세스 풀, 세션-편향 풀링, fd 패싱 핸드오프 — CUBRID broker 코드의 나머지 모양 은 직접적인 결과다.

교과서적 그림 아래로 내려가 보면, 한 번에 두 개 이상의 클라 이언트를 다뤄야 하는 모든 관계형 엔진은 비슷한 모양으로 진 화한다. 정통 RPC 논문에는 이 어휘들이 없다. 추상적 풀과 실 제 소스 사이를 채우는 엔지니어링 어휘다.

broker 없는 맨몸 엔진. 가장 단순한 구성은 broker가 없 다. 클라이언트 TCP 연결마다 엔진 안에서 fork (PostgreSQL의 고전적 모델) 하거나 쓰레드 (MySQL의 고전적 모델) 를 띄운다. PostgreSQL의 postmaster 프로세스는 공개 TCP 포트에서 accept 하고 클라이언트당 backend를 fork 한다. backend는 exec 하지 않고 postmaster의 주소 공간을 그대로 상속받아 SQL 파이프라인을 직접 돈다. MySQL은 클라이언트당 THD 쓰 레드를 띄운다. 둘 다 연결된 드라이버 한 명당 OS 수준 워커 하나를 지불한다. 클라이언트가 수십 명이면 괜찮지만, 거의 대부분 idle 인 JDBC 풀 수천 개로는 확장되지 않는다. 그래 서 connection broker 가 발명됐다.

pgbouncer (PostgreSQL). 별도 프로세스. 순수 세션 풀 링 또는 트랜잭션 풀링. 드라이버는 pgbouncer 포트로 붙 는다. pgbouncer서버 측 연결 을 드라이버당 (세션 모 드), 활성 트랜잭션당 (트랜잭션 모드), 또는 SQL 문장당 (statement 모드, 거의 안 쓰임) 들고 있는다. libevent 기반 단일 프로세스 프록시라서 모든 바이트가 pgbouncer 메모리 를 두 번 거친다 (드라이버에서 읽어 서버에 쓰고, 그 반대로 한 번 더). 강점은 무상태성이다. pgbouncer 는 백엔드 Postgres 워커들을 떨어뜨리지 않고 재시작할 수 있다. 약점 은 핫 패스 위의 프록시 hop이다.

pgpool-II (PostgreSQL). 풀링 위에 복제 인지 부하 분 산과 read/write 분리를 얹는다. 기능 표면이 더 크고, 폭발 반경도 더 크다. 운영자가 더 능력 있는 broker의 가격으로 더 복잡한 broker를 받는다.

Oracle shared servers / MTS. 외부 프로세스 없이 broker가 엔진 안에 있다. 작은 dispatcher 풀이 TCP 소켓 을 보유하고, 더 큰 shared server 풀이 실제 SQL을 실행한 다. 클라이언트는 요청마다 dispatcher 에서 shared server 로 큐잉되는 statement-pool 스타일이다. 기본은 dedicated-server 모드 (세션당 서버 하나) 고, shared server는 옵트인이다. Oracle Connection Manager (CMAN) 는 한 단계 위에 자리 잡아 ACL과 프로토콜 수준 방화벽 기능이 있는 Oracle 인지 TCP 프록시 역할을 한다.

MySQL Router / ProxySQL. MySQL 프로토콜을 이해하는 외 부 프록시다. ProxySQL은 쿼리 재작성과 라우팅 규칙을 더한 다. 모양 자체는 pgbouncer 의 그것에 L7 로직을 얹은 것이다.

CUBRID broker — 프로세스 풀 + fd 핸드오프. CUBRID가 이 설계 공간에서 잡은 위치는 깔끔한 혼합이다. pgbouncer 의 외재성 (별개 프로세스, 별개 포트, ACL, SQL 로그 수집) 과 Oracle MTS 의 엔진 내 풀링 (워커가 일반적인 바이트 프 록시가 아니라 진짜 엔진 프로세스라는 점) 을 함께 가진다. SCM_RIGHTS 핸드오프는 pgbouncer 와 ProxySQL 이 지불 하는 데이터 경로 hop을 없앤다. broker는 control plane에 머무르고, CAS가 data plane을 가진다.

이론적 개념CUBRID 명칭
broker 부모 프로세스cub_broker (broker.c 로 빌드되는 바이너리)
풀 안의 워커 프로세스cub_cas (cas.c + cas_common_main.c 로 빌드되는 바이너리)
broker 단위 설정 레코드T_BROKER_INFO in broker_config.h, broker_config_read 가 채워 넣는다
CAS 단위 런타임 레코드T_APPL_SERVER_INFO in broker_shm.h:290
프로세스 트리 공유 상태T_SHM_BROKER, T_SHM_APPL_SERVER, 옵션으로 T_SHM_PROXY 가 들어 있는 SysV shm 세그먼트
공개 TCP 리스너broker.c::init_env 에서 getenv(PORT_NUMBER_ENV_STR) 에 bind 된 sock_fd
Receiver 쓰레드 (accept 루프)broker.creceiver_thr_f
잡 큐 (priority + 도착시간 기준 max-heap)T_SHM_APPL_SERVER 안의 T_MAX_HEAP_NODE job_queue[JOB_QUEUE_MAX_SIZE+1]
Dispatcher 쓰레드broker.cdispatch_thr_f (비-shard) / shard_dispatch_thr_f (shard)
Idle CAS 선택기broker.cfind_idle_cas
Auto-grow 선택기broker.cfind_add_as_index + broker_add_new_cas
Auto-shrink 선택기broker.cfind_drop_as_index + broker_drop_one_cas_by_time_to_kill
CAS 단위 랑데부 소켓ut_get_as_port_name 이 만들어 주는 경로의 AF_UNIX 소켓 (CAS 인덱스당 하나)
클라이언트 fd 핸드오프broker 측 send_fd (broker_send_fd.c); CAS 측 net_connect_client 안에서 recv_fd (broker_recv_fd.c)
CAS 메인 루프cas_common_main.ccas_main_loop, cas_main/shard_cas_main 에서 호출
CAS 측 요청 디스패치 표cas.cstatic T_SERVER_FUNC server_fn_table[]
CAS↔서버 프로토콜CSS 프로토콜 — cubrid-network-protocol.md 참조 (CAS는 서버의 클라이언트 로 행동)
연결 상태 머신enum t_con_status (CON_STATUS_OUT_TRAN, _IN_TRAN, _CLOSE, _CLOSE_AND_CONNECT)
UTS (워커) 상태 머신broker_shm.hUTS_STATUS_IDLE, _BUSY, _RESTART, _START, _CON_WAIT, _STOP 매크로
접근 제어broker_acl.caccess_control_check_right + cubrid_broker.acl[%name] 섹션
모니터링broker_monitor.c (broker_monitor 관리 도구. shm을 읽기 전용으로 attach)
관리 인터페이스broker_admin_pub.c (cubrid broker {start,stop,on,off,reset,…} 의 구현)
SQL 로그 수집broker_log_top.c, broker_log_replay.c, broker_log_converter.c
Shard / proxy 변형broker.cT_SHM_PROXY + proxy_* 데몬. CAS↔proxy↔broker 한 hop이 추가
ODBC 게이트웨이 변형cas_cgw.c, cas_cgw_execute.c, cas_cgw_odbc.c. broker는 같지만 CAS 바이너리가 다르다

broker에는 여섯 개의 이동 부품이 있다. broker 부모 프로 세스 가 공개 포트에서 listen 한다. CAS 풀 이 실제 SQL 작업을 처리한다. 공유 메모리 세그먼트 가 둘을 락 없이 묶는다. 연결 수명 주기 가 새로 들어온 TCP SYN을 SQL을 받을 준비가 된 CAS 핸들로 만들어 낸다. 접근 제어 + SQL 로그 + 모니터링 이 대역 외 보조 시설로 붙어 있다. SHARD/proxy 변형과 CGW (ODBC 게이트웨이) 변형 이 같은 뼈대를 다른 배포 모양에 맞춰 변형한다. 이 순서로 본다.

flowchart TB
  subgraph host["단일 호스트"]
    direction TB
    BR["cub_broker (parent)<br/>broker.c::main<br/>TCP :broker_port 에서 listen"]
    subgraph THR["broker 쓰레드들"]
      RX["receiver_thr_f<br/>accept() + 프로토콜 prefix sniff"]
      DSP["dispatch_thr_f<br/>job_queue 에서 pop<br/>find_idle_cas → send_fd"]
      MON["cas_monitor_thr_f"]
      PSZ["psize_check_thr_f"]
      HC["hang_check_thr_f"]
      SVM["server_monitor_thr_f"]
    end
    subgraph CASES["fork 된 CAS 자식들 (N = appl_server_max_num)"]
      direction LR
      C0["cub_cas[0]<br/>cas.c::cas_main_loop"]
      C1["cub_cas[1]"]
      Cn["…cub_cas[N-1]"]
    end
    SHM[["SysV shm:<br/>T_SHM_BROKER<br/>T_SHM_APPL_SERVER<br/>(job_queue, as_info[])"]]
    SRV[["cub_server<br/>(별개 프로세스,<br/>다른 포트)"]]
  end
  CL["JDBC / CCI / ODBC<br/>드라이버 프로세스"]

  CL -- "1: TCP connect" --> BR
  BR --- THR
  BR -- "2: SCM_RIGHTS<br/>over AF_UNIX" --> C0
  BR -.-> C1
  BR -.-> Cn
  CL -. "3: 같은 fd로 재사용<br/>상속받은 TCP 위에서 직접 CSS" .-> C0
  C0 -- "4: CSS 프로토콜" --> SRV
  RX <-.-> SHM
  DSP <-.-> SHM
  MON <-.-> SHM
  C0 <-.-> SHM

broker 부모는 TCP listen 소켓 sock_fd 하나와 작은 고정 크기 워커 쓰레드 집합을 가진다. CAS 자식들은 부팅 시점에 한 번만 fork 되며 (broker_admin_pub.c::as_activatebr_activate 에서 호출되어 그 일을 한다), appl_server_max_num 까지 CAS 슬롯당 하나씩 만든다. 그들의 수명은 개별 드라이버 연결과 분리된다. 하나의 CAS는 여러 클라이언트를 순차적으로 처리하며, broker가 명시적으로 요청할 때만 (롤링 재시작, 메 모리 초과, hang) 재시작된다.

broker의 조율은 거의 전부 SysV shm으로 이뤄진다. 자식들이 독립 프로세스라서 부모의 결정 (어느 CAS가 busy 인지, 어느 CAS가 회수 중인지, 각 CAS가 언제 시작됐는지, 몇 건을 처리 했는지) 을 봐야 하기 때문이다. 락 없는 접근은 누가 어떤 필드를 쓰는가 라는 규칙으로 강제된다. 뮤텍스가 아니라.

flowchart LR
  subgraph SHM["SysV shm 세그먼트 (broker_shm_id)"]
    direction TB
    BRINFO["T_SHM_BROKER<br/>&mdash; magic, num_broker, my_ip_addr,<br/>access_control_file, br_info[1]"]
    APPLSHM["T_SHM_APPL_SERVER<br/>&mdash; broker_name, log dirs,<br/>job_queue[JOB_QUEUE_MAX_SIZE+1],<br/>as_info[APPL_SERVER_NUM_LIMIT],<br/>access_info[ACL_MAX_ITEM_COUNT],<br/>unusable_databases[][]"]
    PROXSHM["T_SHM_PROXY (shard 전용)<br/>&mdash; proxy_info[MAX_PROXY_NUM],<br/>shard_user, shard_conn, shard_key"]
  end
  BR["cub_broker<br/>(거의 모든 필드의 writer)"]
  CAS["cub_cas[i]<br/>(자기 슬롯 as_info[i].* 만 쓴다)"]
  ADM["cubrid broker 관리 도구<br/>(broker_admin_pub.c)"]
  MON["broker_monitor<br/>(read-only)"]

  BR -- "writes job_queue, br_info,<br/>service_flag, num_appl_server" --> SHM
  CAS -- "writes uts_status, con_status,<br/>num_request, last_access_time,<br/>자기 슬롯의 session_id" --> SHM
  ADM -- "writes 설정 유래 필드,<br/>num_appl_server 변경" --> SHM
  MON -. "SHM_MODE_MONITOR 로 read-only attach" .- SHM

규약은 다음과 같다. broker는 잡 큐서비스 플래그 를 쓴다. 각 CAS는 자기 슬롯의 상태 필드 만 쓴다. 관리 도구는 cubrid broker reset 이 떨어졌을 때 설정 관련 필드를 재기 입한다. 각 writer가 만지는 필드 집합이 서로 겹치지 않기 때 문에 뮤텍스는 거의 필요 없다. 존재하는 몇몇은 좁게 한정되 어 있다. broker_shm_mutex (broker 내부 슬롯 회계), run_appl_mutex (fork+execle 직렬화 — 새 CAS의 pid 슬 롯이 경쟁 상태에 빠지지 않게), clt_table_mutex + clt_table_cond (receiver_thr_fdispatch_thr_f 사이 의 producer/consumer 랑데부), 그리고 CAS 단위 POSIX 세마 포어 as_info->con_status_sem (broker↔CAS 가 “이 연결 아 직 살아 있나?” 를 핸드셰이크할 때 사용) 이다.

shm 키는 결정적으로 유도된다 (DEFAULT_SHM_KEY = 0x3f5d1c0a 와 broker 자신의 설정값 appl_server_shm_id 의 합). 자식 프로세스는 환경 변수 (APPL_SERVER_SHM_KEY_STR, AS_ID_ENV_STR) 로 받은 값으로 attach 하고, uw_shm_openmagic 필드 검사가 stale 세그먼트를 거부한다.

클라이언트의 connect() 에서부터 SQL 받을 준비가 된 CAS 핸들까지의 여정이 broker의 가장 중심적인 안무다. 상태 머 신은 다음과 같다.

stateDiagram-v2
  [*] --> SERVICE_OFF: CAS 슬롯 설정됨, 아직 시작 전
  SERVICE_OFF --> SERVICE_ON: broker_add_new_cas \n fork+execle cub_cas

  SERVICE_ON --> UTS_IDLE: 자식 등록 \n service_ready_flag=TRUE

  UTS_IDLE --> UTS_BUSY: dispatch_thr_f 가 슬롯 선택 \n send_fd 핸드오프

  UTS_BUSY --> CON_OUT_TRAN: cas_main_loop accept \n cas_db_connect 성공
  CON_OUT_TRAN --> CON_IN_TRAN: process_request 첫 문장
  CON_IN_TRAN --> CON_OUT_TRAN: end_tran(commit/rollback)
  CON_OUT_TRAN --> CON_CLOSE: KEEP_CON_AUTO + time_to_kill 초과 idle
  CON_OUT_TRAN --> CON_CLOSE_AND_CONNECT: 명시적 con_close + 재연결

  CON_CLOSE --> UTS_IDLE: cas_cleanup_session \n ux_database_shutdown
  UTS_BUSY --> UTS_RESTART: cas_monitor_worker \n fn_status 가 hang_timeout 초과 멈춤
  UTS_RESTART --> SERVICE_ON: restart_appl_server(): kill+fork

  SERVICE_ON --> SERVICE_OFF_ACK: 운영자의 drop 요청
  SERVICE_OFF_ACK --> SERVICE_OFF: stop_appl_server()

상태 벡터가 두 개라는 점이 중요하다. service_flag (운영 자가 보는: ON / OFF / OFF_ACK / UNKNOWN) 와 uts_status (런타임 한정: IDLE / BUSY / RESTART / START / CON_WAIT / STOP) 는 서로 다른 행위자가 쓴다. 둘이 합쳐져서 입장 결정을 끌어낸다. broker의 find_idle_casas_info[0..N-1] 을 훑으면서 service_flag==SERVICE_ON && uts_status==UTS_STATUS_IDLE 인 슬롯을 찾는다.

단계 1: receiver 쓰레드가 TCP 소켓을 accept 하고 sniff

섹션 제목: “단계 1: receiver 쓰레드가 TCP 소켓을 accept 하고 sniff”
// receiver_thr_f — src/broker/broker.c
clt_sock_fd = accept (sock_fd, (struct sockaddr *) &clt_sock_addr,
&clt_sock_addr_len);
if (IS_INVALID_SOCKET (clt_sock_fd)) continue;
// ... condensed: TCP_NODELAY, keepalive, optional reject-while-hung ...
read_len = read_nbytes_from_client (clt_sock_fd, cas_req_header,
SRV_CON_CLIENT_INFO_SIZE);
if (strncmp (cas_req_header, "PING", 4) == 0) { /* health check */ }
if (strncmp (cas_req_header, "ST", 2) == 0) { /* status query */ }
else if (strncmp (cas_req_header, "QC", 2) == 0
|| strncmp (cas_req_header, "CANCEL", 6) == 0
|| strncmp (cas_req_header, "X1", 2) == 0) { /* query cancel */ }
// Otherwise: regular driver handshake.
// Validate magic, SSL flag, cas_client_type, version,
// run uw_acl_check on source IP, push to job_queue.

receiver 쓰레드는 broker 전체에서 클라이언트의 첫 바이트 를 실제로 들여다 보는 유일한 곳이다. 30바이트짜리 cas_req_header (SRV_CON_CLIENT_INFO_SIZE) 는 매직 문자 열 CUBRK00 (또는 SSL용 CUBRKS00), 클라이언트 타입 (CCI/JDBC/ODBC/…), 프로토콜 버전, 함수 플래그 바 이트 (BROKER_RENEWED_ERROR_CODE 같은 capability 비트 들), 그리고 패딩을 담는다. 같은 prefix가 제어 채널 로도 쓰여서, broker가 PING, ST (상태), QC/CANCEL/X1 (쿼리 취소) 를 CAS 하나도 깨우지 않고 인라인으로 답할 수 있게 해 준다. 쿼리 취소는 pid 가 일치하는 CAS 슬롯을 찾 아 SIGUSR1 을 보내는 식으로 동작한다. CAS는 이미 query_cancel 에 그 시그널을 묶어 두었다 (cas_common_main.c::cas_main_init 에서 등록).

정상 클라이언트는 T_MAX_HEAP_NODE 안에 박힌다.

// receiver_thr_f — src/broker/broker.c
new_job.id = job_count;
new_job.clt_sock_fd = clt_sock_fd;
new_job.recv_time = time (NULL);
new_job.priority = 0;
new_job.cas_client_type = cas_client_type;
new_job.port = ntohs (clt_sock_addr.sin_port);
memcpy (new_job.ip_addr, &(clt_sock_addr.sin_addr), 4);
strcpy (new_job.prg_name, cas_client_type_str[(int) cas_client_type]);
new_job.clt_version = client_version;
memcpy (new_job.driver_info, cas_req_header, SRV_CON_CLIENT_INFO_SIZE);
pthread_mutex_lock (&clt_table_mutex);
max_heap_insert (job_queue, job_queue_size, &new_job);
pthread_cond_signal (&clt_table_cond);
pthread_mutex_unlock (&clt_table_mutex);

잡 큐는 (priority, insert_time_inverse) 를 키로 하는 max-heap 이다 (broker_max_heap.c). 오래 기다린 잡은 매 이터레이션마다 max_heap_incr_priority 로 priority가 올라 가서 결국 새로 도착한 잡을 이긴다. 용량은 shm_appl->job_queue_size 이며 broker 단위로 설정한다 (기 본 1024). 힙이 가득 차면 receiver는 드라이버에 CAS_ER_FREE_SERVER 를 돌려 주고 소켓을 닫는다. 무한정 큐잉 대신 backpressure를 건다.

단계 2: dispatcher가 잡을 꺼내고 CAS와 랑데부

섹션 제목: “단계 2: dispatcher가 잡을 꺼내고 CAS와 랑데부”

dispatcher 쓰레드는 같은 힙에서 pop 한다. 힙이 비었으면 컨 디션 변수 위에서 블록한다.

// dispatch_thr_f — src/broker/broker.c
pthread_mutex_lock (&clt_table_mutex);
if (max_heap_delete (job_queue, &cur_job) < 0) {
// ... wait up to 30ms on clt_table_cond ...
}
hold_job = 1;
max_heap_incr_priority (job_queue);
pthread_mutex_unlock (&clt_table_mutex);
retry:
while (1) {
as_index = find_idle_cas ();
if (as_index < 0) {
if (broker_add_new_cas ()) continue; // auto-grow up to max
else SLEEP_MILISEC (0, 30);
} else break;
}
shm_appl->as_info[as_index].num_connect_requests++;
shm_appl->as_info[as_index].clt_version = cur_job.clt_version;
memcpy (shm_appl->as_info[as_index].driver_info, cur_job.driver_info,
SRV_CON_CLIENT_INFO_SIZE);
shm_appl->as_info[as_index].cas_client_type = cur_job.cas_client_type;
memcpy (shm_appl->as_info[as_index].cas_clt_ip, cur_job.ip_addr, 4);
shm_appl->as_info[as_index].cas_clt_port = cur_job.port;
srv_sock_fd = connect_srv (shm_br->br_info[br_index].name, as_index);
// ... handshake on con_status, then ...
ret_val = send_fd (srv_sock_fd, cur_job.clt_sock_fd, ip_addr,
cur_job.driver_info);

이 코드 조각에는 두 가지 중요한 결정이 들어 있다.

  • idle CAS 탐색은 풀 자료구조가 아니라 선형 스캔이다. find_idle_casas_info 를 인덱스 0부터 앞으로 훑으 면서 service_flag == SERVICE_ON && uts_status == UTS_STATUS_IDLE 이고 CAS 단위 keep-alive 술어가 통과하는 첫 슬롯을 돌려 준다. 자유 리스트가 없는 이유는 자연스러운 자유 리스트 가 슬롯 표 자기 자신이고, 경쟁의 상한이 appl_server_max_num 으로 묶여 있기 때문이다. 안쪽 루프 비용이 실재하긴 하지만 미리 지불한 셈이다. appl_server_max_num 은 보통 수십에서 낮은 수백 단위다. 더 큰 풀이라면 자유 리스트가 정당화되겠지만, CUBRID의 배포 모양은 그렇지 않다.

  • CAS 단위 랑데부 소켓. connect_srvAF_UNIX SOCK_STREAM 소켓을 연다. 경로는 ut_get_as_port_name(path, br_name, as_index, …) 이 만들어 준다 (보통 $CUBRID_TMP/ 아래). 각 CAS는 그 경로에서 listen 하고 있고, broker 는 핸드오프마다 새로 제어 연결을 connect 하고, 4바이 트 con_status ping-pong으로 CAS가 살아 있고 받을 의사 가 있는지 확인하고, send_fd 를 부르고, uts_status ack을 읽고, 랑데부 소켓을 닫는다. 이 시점에 클라이언트 의 TCP fd는 CAS의 디스크립터 표 안에 복제된 상태다. 양 쪽 모두 원래의 핸드오프 소켓 사본을 닫고, CAS는 건네 받 은 fd를 세션 동안 자기 client_sock_fd 로 쓴다.

단계 3: CAS가 fd를 받고 cas_main_loop 를 돈다

섹션 제목: “단계 3: CAS가 fd를 받고 cas_main_loop 를 돈다”
// cas_main_loop — src/broker/cas_common_main.c (condensed)
for (;;) {
ssl_client = false;
// ...
br_sock_fd = net_connect_client (srv_sock_fd); // recv_fd from broker
if (IS_INVALID_SOCKET (br_sock_fd)) goto finish_cas;
req_info.client_version = as_info->clt_version;
memcpy (req_info.driver_info, as_info->driver_info,
SRV_CON_CLIENT_INFO_SIZE);
// accept the now-passed fd as our client
if (cas_accept_client (br_sock_fd, &client_sock_fd, &client_ip_addr) < 0)
goto finish_cas;
cas_log_open (broker_name);
// ... read DB connection blob, parse via cas_parse_db_info ...
err_code = cas_handle_db_connection (client_sock_fd, &req_info,
&conn_info, cas_info,
client_ip_addr, ops,
is_new_connection);
if (err_code < 0) { cas_finish_session (...); goto finish_cas; }
// request loop
while (fn_ret == FN_KEEP_CONN) {
fn_ret = ops->process_request (client_sock_fd, &net_buf,
&req_info, srv_sock_fd);
cas_log_error_handler_clear ();
as_info->last_access_time = time (NULL);
if (as_info->con_status == CON_STATUS_OUT_TRAN
&& hm_srv_handle_get_current_count () >= shm_appl->max_prepared_stmt_count)
fn_ret = FN_CLOSE_CONN;
}
ops->cleanup_session ();
finish_cas:
cas_log_close (true);
CLOSE_SOCKET (client_sock_fd);
// and around the loop: maybe restart_is_needed, maybe just go IDLE
}

깨끗한 시각으로 보면 cas_main_loop 는 CAS가 가지고 있는 유일한 루프다. 드라이버 단위 상태 — cub_server 와의 DB 연결, prepared statement 등록표, req_info — 는 안쪽 while (fn_ret == FN_KEEP_CONN) 의 수명 동안 CAS 프로세스의 힙에 살아 있다. 드라이버가 끊으면 cleanup_session 이 열려 있는 트랜잭션을 롤백하고 서버 측 세션을 종료시키거나 (ux_end_session → 와이어 위의 xsession_end_session, cubrid-server-session.md 참조), KEEP_SESS 가 돌아온 경 우 다음 드라이버에게 서버의 세션 표로 세션 id를 넘 긴다. broker의 표가 아니다.

process_requestfunc_codeserver_fn_table[] 을 인덱싱한다 (44개 CAS_FC_* 코드 — END_TRAN, PREPARE, EXECUTE, FETCH, … 중 하나). 각 fn_* 는 드라이버 요청을 하나 또는 여러 개의 xsession_*/xqmgr_*/xtran_* 호출 로 번역해 CSS 프로토콜 위에서 cub_server 에 보내는 CAS 측 stub이다. 그래서 CAS는 (드라이버에 대한) 서버 이면서 동시에 (cub_server 에 대한) 클라이언트 다.

단계 4: SQL 흐름 — 끝에서 끝까지

섹션 제목: “단계 4: SQL 흐름 — 끝에서 끝까지”
sequenceDiagram
  autonumber
  participant Drv as JDBC/CCI 드라이버
  participant Br as cub_broker
  participant Cas as cub_cas[i]
  participant Srv as cub_server

  Drv->>Br: broker_port 로 TCP connect
  Drv->>Br: 30바이트 SRV_CON_CLIENT_INFO 헤더 (CUBRK00 + version)
  Br->>Br: magic, ACL, capacity 검증 → job_queue 로 push
  Br->>Cas: AF_UNIX 랑데부 connect + send_fd(client_sock_fd)
  Cas->>Cas: cas_accept_client, cas_parse_db_info
  Cas->>Srv: NET_SERVER_PING_WITH_HANDSHAKE (CSS) [cub_server boot 경로]
  Cas->>Srv: NET_SERVER_BO_REGISTER_CLIENT (db_user, db_pass, capabilities)
  Srv-->>Cas: SESSION_ID, transaction_id, server caps
  Cas-->>Drv: CAS 연결 응답 (cas_send_connect_reply_to_driver)
  loop 드라이버 요청마다
    Drv->>Cas: MSG_HEADER + func_code + args (CAS 프로토콜)
    Cas->>Srv: NET_SERVER_QM_QUERY_PREPARE / _EXECUTE / etc.
    Srv-->>Cas: CSS 응답
    Cas-->>Drv: net_buf 응답
  end
  Drv-->>Cas: CON_CLOSE 또는 TCP close
  Cas->>Srv: NET_SERVER_BO_UNREGISTER_CLIENT
  Cas->>Cas: ux_end_session, uts_status = IDLE

드라이버 측에서는 CAS 프로토콜, 서버 측에서는 CSS 프로 토콜 이라는 이중 프로토콜 구조가 broker가 사 주는 것이다. 드라이버는 안정적이고, 좁은 버전 관리를 받고, 연결 셋업과 문장 실행에 최적화된 44 opcode 짜리 프로토콜만 알면 된다. CAS는 그 너머에서 수백 opcode 짜리 더 넓은 내부 CSS 프로토 콜을 다룬다 (cubrid-network-protocol.mdenum net_server_request 참조). 드라이버와 서버는 CAS가 양쪽을 정직하게 유지해 주는 한 독립적으로 진화할 수 있다.

find_idle_cas 가 -1을 돌려줄 때마다 dispatcher는 broker_add_new_cas 를 부른다.

// broker_add_new_cas — src/broker/broker.c
cur_appl_server_num = shm_br->br_info[br_index].appl_server_num;
if (cur_appl_server_num >= shm_br->br_info[br_index].appl_server_max_num)
return false;
add_as_index = find_add_as_index ();
if (add_as_index < 0) return false;
pid = run_appl_server (&(shm_appl->as_info[add_as_index]),
br_index, add_as_index);
if (pid <= 0) return false;
pthread_mutex_lock (&broker_shm_mutex);
shm_appl->as_info[add_as_index].pid = pid;
shm_appl->as_info[add_as_index].uts_status = UTS_STATUS_IDLE;
shm_appl->as_info[add_as_index].service_flag = SERVICE_ON;
(shm_br->br_info[br_index].appl_server_num)++;
pthread_mutex_unlock (&broker_shm_mutex);
return true;

거울 짝은 broker_drop_one_cas_by_time_to_kill 이다. 부모 의 main 안 idle 루프가 100ms 마다, auto_add_appl_server == ON 인 경우에 호출한다. 현재 busy CAS 수에서 큐에 대기 중인 잡 수를 빼서 여유분이 양수면, idle, out-of-transaction, 그 리고 마지막 접근이 time_to_kill 보다 오래 전이었던 CAS를 훑어 찾는다. 선택된 CAS는 con_status = CON_CLOSE 를 받고, CAS 자체 루프는 다음 net_read_* 에서 close를 보고 깨끗하 게 종료한다. shrink 경로는 idle 타이머가 만료됐어도 busy 인 CAS를 회수하는 일을 명시적으로 피한다. broker는 CAS 가 자연스럽게 OUT_TRAN 으로 돌아올 때까지 기다린다.

min_appl_server 하한도 같은 함수가 강제한다.

// broker_drop_one_cas_by_time_to_kill — src/broker/broker.c
if (cur_appl_server_num <= shm_br->br_info[br_index].appl_server_min_num
|| wait_job_cnt > 0)
return false;

그래서 CAS 풀은 minmax 사이에서 진동한다. max 는 강한 backpressure 상한 역할을 하고, min 은 따뜻한 풀 보장 역할을 한다.

broker는 자체 ACL 계층이 있다. 서버 측 인증과는 별 개다. 별도의 cubrid_broker.acl 파일 (경로는 T_SHM_BROKERaccess_control_file) 이 broker 단위 섹션을 담는다.

[%query_editor]
demodb:dba:dba_ips.txt
demodb:public:public_ips.txt
[%broker1]
*:*:all_ips.txt

각 항목은 (dbname, dbuser, ip-list-file) 삼중을 broker 섹션에 묶는다. broker_acl.c::access_control_read_config_file 이 부팅 시점과 cubrid broker acl reload 때마다 파일을 파 싱해 결과를 shm_appl->access_info[] 에 밀어 넣는다. 각 CAS 는 자체 사본을 캐시하고, broker가 reload 할 때마다 증가하는 CHN 바뀜 (shm_as_p->acl_chn) 을 보고 갱신한다.

// access_control_check_right — src/broker/broker_acl.c
if (access_info_changed != shm_as_p->acl_chn) {
uw_sem_wait (&shm_as_p->acl_sem);
memcpy (access_info, shm_as_p->access_info, sizeof (access_info));
num_access_info = shm_as_p->num_access_info;
access_info_changed = shm_as_p->acl_chn;
uw_sem_post (&shm_as_p->acl_sem);
}
return access_control_check_right_internal (shm_as_p, dbname,
dbuser, address);

ACL 검사 자체는 한 연결당 두 번 돈다. 한 번은 broker 계층 의 receiver_thr_f 안에서 v3_acl (broker 단위의 거친 IP 필터. shm_br->br_info[br_index].acl_file 에서 uw_acl_make 가 만든다) 를 싸게 돈다. 또 한 번은 드라이버가 DB 자 격 증명을 제공한 뒤 CAS 계층의 access_control_check_right 안에서 (dbname, dbuser, ip) 삼중을 정밀하게 돈다. 두 단계 검사라는 의미는, broker 전역 블랙리스트에 IP가 잡 힌 적대적 클라이언트는 어떤 CAS도 깨지 않은 채 거부된다는 것이다. broker 계층에서는 허용되지만 특정 (dbname, dbuser) 에는 허용되지 않은 클라이언트는 인증 후 깔끔한 에러를 받는 다.

내부 IP 매칭은 ip_info->address_list[i*5] 의 prefix 길이 형태로 인코딩된 CIDR 비슷한 와일드카드를 지원한다 (선두 바이트가 유효 octet 수 0..4. 0은 어떤 IP든 일치).

각 CAS는 자기 SQL 로그를 log/broker/sql_log/<broker>_<as>.sql.log 에 쓴다 (sql_log_max_size 가 다스리고 CAS가 직접 cas_log.c 에서 회전한다). 이 파일들은 크고, 프로세스마다 하나씩이다. 배포 환경에서 보통 세 가지 파생 뷰를 원한다.

  • broker_log_top (broker_log_top.c) — slow-query 리 포트. 한 개 이상의 SQL 로그 파일을 스캔해 SQL 시그니처로 묶고, 경과 시간/실행 횟수/페치 행 수 기준 top 쿼리를 출 력한다. thr_main 으로 멀티쓰레드 처리한다.
  • broker_log_replay (broker_log_replay.c) — 기록된 SQL을 대상 데이터베이스에 다시 실행한다. capacity 테스트 용도, 그리고 운영 trace를 staging 클러스터에 재생할 때 유 용하다.
  • broker_log_converter (broker_log_converter.c) — SQL 로그를 다른 형태로 (CSV, 시간 정렬 스트림 등) 재포맷 한다.

이 도구들은 로그 파일을 직접 연다. broker를 거치지 않는다. broker의 일은 로그가 존재 하게 하는 것까지다. 분석은 오프 라인 작업이다.

broker_monitor.c 는 사용자 노출 admin 도구 broker_monitor 다 (cubrid broker status). broker의 shm을 SHM_MODE_MONITOR (커널 SHM_RDONLY) 로 열어, CAS 단위 상태 표를 curses 풍 화면으로 그린다. shm이 RO이므로 모니터가 broker 상태를 실 수로 망칠 수가 없다. 그 대가로 경합 상황에서 멀티바이트 필 드의 찢어진 읽기를 볼 가능성은 있는데, UI 용도로는 받아들 일 만하다.

broker 단위로 합산된 카운터들 (num_requests_received, num_transactions_processed, num_queries_processed, num_long_queries, …) 은 각 CAS가 자기 T_APPL_SERVER_INFO 슬롯에 쓰고 모니터가 as_info[] 를 합산한다.

broker_admin_pub.c쓰기 쪽이다. 운영자 명령 (start, stop, on, off, restart, reset, acl, reload, add, drop) 을 같은 shm을 만지고 broker에 신호를 보내는 방식으로 구현한다. 대부분의 동작은 결국 개별 as_info[] 슬 롯의 service_flag 를 뒤집는 것으로 환원되며, broker의 모 니터 쓰레드들이 그 변화를 관찰한다.

broker의 hang_check_thr_fmonitor_hang_flag 가 켜져 있을 때만 돈다. 그 모델은 보수적이다. CAS가 긴 서버 호출에 들어갈 때 갱신하는 heartbeat (claimed_alive_time) 와 last_access_time 을 표본 추출한다. 너무 많은 CAS가 monitor_hang_interval 안에 자기 claim을 진행시키지 않은 경우 broker는 reject_client_flag 를 켠다. 그러면 receiver_thr_f 가 새 TCP accept 를 거부하기 시작한다 (accept 루프 안에서 CLOSE_SOCKET (clt_sock_fd); continue; 로 단락한다). 신호는 의도적으로 거칠다. broker는 살아 있 는 임의의 CAS를 죽이는 것보다 부하를 흘려 보내는 쪽 을 선호한다. hang 이벤트 이후 운영자 개입이 기대된다.

br_info_p->shard_flag == ON 이면 broker 와 CAS 사이에 한 계층이 더 끼어든다. T_SHM_PROXY 로 설정되는 작은 고정 크기 cub_proxy 풀이다. 디스패치 경로는 다음과 같이 바뀐 다.

flowchart LR
  CL["JDBC 드라이버"] --> BR["cub_broker"]
  BR -- "send_fd" --> PX1["cub_proxy[0]"]
  BR -. "send_fd" .-> PX2["cub_proxy[1..M-1]"]
  PX1 --> CASGRP["shard 0 의 cub_cas[*]"]
  PX1 --> CASGRP2["shard 1 의 cub_cas[*]"]

broker는 shard를 직접 알지 못한다. broker_find_available_proxy 로 임의의 proxy를 골라 클라이언트 fd를 그 proxy에 보낸다. proxy는 SQL 힌트 (/*+ SHARD_KEY('foo') */) 를 파싱하고, T_SHM_PROXYshard_key_function_name 가 가리키는 라이 브러리로 shard id를 계산해 올바른 CAS 풀로 라우팅한다. shard 모드의 CAS는 fork 시점에 특정 shard_id 에 묶이고 (as_info->shard_cas_id, as_info->proxy_id), broker 핸드 오프를 기다리지 않고 자기가 proxy 쪽으로 접속한다 (cas_network.cnet_connect_proxy).

// shard_cas_main — src/broker/cas.c
proxy_sock_fd = net_connect_proxy ();
if (IS_INVALID_SOCKET (proxy_sock_fd)) {
SLEEP_SEC (1); goto conn_proxy_retry;
}
error = cas_register_to_proxy (proxy_sock_fd);

이 제어 흐름의 역전 (CAS가 proxy로 connect) 덕분에 proxy는 shard별로 사용 가능한 CAS의 오래 사는 정렬된 리스트를 들고 있을 수 있고, fd 패싱 round-trip을 추가하지 않고 들어오는 문장을 hint 기반으로 그들에게 디스패치할 수 있다. 한 번의 핸드오프 비용이 한 CAS 풀을 공유하는 많은 클라이언트에 분 할 상환된다.

FOR_ODBC_GATEWAY 빌드는 같은 broker 뼈대를 재사용하면서 다른 CAS를 묶은 별개 바이너리를 낸다.

  • cas_cgw.ccas_main_loop 에 다른 CAS_MAIN_OPS 벡 터 (다른 db_connect, 다른 process_request) 로 등록되 는 alternate CAS main.
  • cas_cgw_execute.c — 같은 CAS_FC_* opcode를 CUBRID 자체의 dbi.h 가 아니라 ODBC 호출로 구현한다.
  • cas_cgw_odbc.c — 실제 ODBC wrapper 계층.
  • cas_cgw_function.c — CGW 서버 함수 표 (cas.c::server_fn_table[] 과 모양은 같지만 CGW 구현을 가리킨다).

broker 입장에서는 CGW CAS와 일반 CAS가 서로 교체 가능하다. 같은 shm 슬롯 레이아웃, 같은 핸드오프 프로토콜, 같은 SQL 로그 파일. 디스크상의 유일한 차이는 broker config의 appl_server_name (CAS 대신 CAS_CGW) 이다. 그래서 단 일 CUBRID broker 인스턴스가 여러 데이터베이스 의 프론트 엔드 역할을 할 수 있는데, 그중 일부는 진짜 cub_server 인 스턴스이고 일부는 ODBC로 닿는 원격 SQL Server / Oracle / MySQL 엔드포인트일 수 있다. 모두 동일한 클라이언트 측 CCI/JDBC 의미론으로 보인다는 점이다. 그게 이 게이트웨이 제 품이 노리는 정확한 사례다.

failover는 broker의 일이 *아니다. broker는 단일 호스트 다. CAS의 database_hostdb_connection_file (후보 호 스트 목록) 은 dbi 의 connect 경로로 그대로 전달되고, 주 호스트가 닿지 않으면 CAS 안의 드라이버 가 후보 목록을 순 회한다 (shm의 connect_order 가 sequential 인지 random 인 지를 정하고, replica_only_flagaccess_mode 는 HA 역 할 기준으로 후보를 더 좁힌다). CUBRID가 하는 HA 재라우팅 자체는 server 측 heartbeat / master 프로세스에서 일어난다 (cubrid-heartbeat.md 참조). broker의 유일한 기여는 server_monitor_thr_f 인데, 등록된 각 db_host 의 상태를 master에 주기적으로 묻고 그 결과로 unusable_databases[] 를 뒤집어, 죽은 replica로 향하는 향후 드라이버 connect를 단락시킨다.

아래는 서브시스템별로 묶은 읽기 순서다. 절 끝의 position hints 표는 각 심볼을 (file, line) 문서가 마지막으로 updated: 되었을 시점 기준 으로 짝지어 놓은 것이다.

  • 프로세스 부트스트랩 — main (argv 파싱, shm 읽기, 시그널 설치), broker_init_shm (broker 자체 shm attach), init_env (PORT_NUMBER_ENV_STR 위에서 TCP socket+bind+listen), init_proxy_env (shard 전용).
  • 쓰레드 fan-out — main 안의 THREAD_BEGIN(receiver_thr_f, …) 와 형제들. 쓰레드들: receiver_thr_f, dispatch_thr_f, shard_dispatch_thr_f, psize_check_thr_f, cas_monitor_thr_f, hang_check_thr_f, proxy_monitor_thr_f, proxy_listener_thr_f, server_monitor_thr_f.
  • 잡 큐 — T_MAX_HEAP_NODE (in broker_max_heap.h), max_heap_insert, max_heap_delete, max_heap_incr_priority (broker_max_heap.c 에 정의).
  • CAS 수명 주기 — broker_add_new_cas, broker_drop_one_cas_by_time_to_kill, find_idle_cas, find_drop_as_index, find_add_as_index, run_appl_server (실제 fork+execle), stop_appl_server, restart_appl_server.
  • 핸드오프 — connect_srv (broker 측, 특정 CAS에 AF_UNIX 를 연다), send_fd (broker_send_fd.c 에 위치).
  • 헬스 & 모니터링 — cas_monitor_worker, psize_check_worker, proxy_check_worker, proxy_monitor_worker, connect_to_master_for_server_monitor, get_server_state_from_master, insert_db_server_check_list.
  • 인라인 사전 디스패치 프로토콜 — receiver 쓰레드의 PING/ST/QC/CANCEL/X1 처리.

CAS 프로세스 (cas.c + cas_common_main.c)

섹션 제목: “CAS 프로세스 (cas.c + cas_common_main.c)”
  • CAS 부트스트랩 — cas_init, cas_init_shm (같은 broker shm을 APPL_SERVER_SHM_KEY_STR 환경 변수로 attach 하고, as_infoAS_ID_ENV_STR 로 인덱싱).
  • 두 개의 메인 루프 — cas_main (비-shard ops 벡터로 cas_main_loop 호출) 과 shard_cas_main (다른 루프, proxy 주도).
  • 공통 루프 — cas_main_loop (cas_common_main.c), cas_main_init, cas_accept_client, cas_parse_db_info, cas_handle_db_connection, cas_finish_session.
  • 요청별 디스패치 — process_request (CAS 측. msg 헤더 를 읽고, args를 디코드하고, server_fn_table[func_code-1] 을 인덱싱). 함수 표 항목들: fn_end_tran, fn_prepare, fn_execute, fn_fetch, fn_schema_info, fn_oid_get, fn_collection, fn_savepoint, fn_lob_*, fn_check_cas, fn_get_row_count, fn_get_last_insert_id, fn_set_cas_change_mode, 등 (enum t_cas_func_code 에 총 44개).
  • DB 연결 — cas_db_connect, cas_post_db_connect, set_db_connection_info, clear_db_connection_info, need_database_reconnect, set_db_parameter.
  • 세션 id 표면 — cas_make_session_for_driver, cas_set_session_id, cas_send_connect_reply_to_driver.
  • 시그널 처리 — cas_sig_handler, query_cancel, set_hang_check_time, unset_hang_check_time, check_server_alive.
  • 네트워크 헬퍼 — net_connect_client (broker로부터 recv_fd), net_connect_proxy (shard CAS가 proxy로 connect), net_read_header_keep_con_on, net_read_int_keep_con_auto, net_read_process (shard 변형).

공유 메모리 (broker_shm.c + broker_shm.h)

섹션 제목: “공유 메모리 (broker_shm.c + broker_shm.h)”
  • 최상위 구조체 — T_SHM_BROKER, T_SHM_APPL_SERVER, T_SHM_PROXY, T_BROKER_INFO, T_APPL_SERVER_INFO, T_PROXY_INFO, T_SHARD_INFO, T_SHARD_CONN_INFO, T_SHARD_USER, T_SHARD_KEY, T_SHARD_KEY_RANGE, T_CLIENT_INFO, T_DB_SERVER.
  • 상태 enum과 매크로 — enum t_con_status, T_BROKER_SERVICE_STATUS, UTS_STATUS_*, CON_STATUS_LOCK_*, CON_STATUS_LOCK, CON_STATUS_UNLOCK, MAGIC_NUMBER, SHM_APPL_SERVER/SHM_BROKER/SHM_PROXY, T_SHM_MODE.
  • attach/detach — uw_shm_open, uw_shm_create, uw_shm_destroy, uw_shm_detach. uw_shm_openmagic == MAGIC_NUMBER 를 검사해 stale 세그먼트를 거부 한다.
  • 초기화 — broker_shm_initialize_shm_broker, broker_shm_initialize_shm_as, broker_shm_set_as_info, shard_shm_set_shard_conn_info.
  • 동기화 원시도구 — uw_sem_init, uw_sem_wait, uw_sem_post, uw_sem_destroy (POSIX wrapper. Windows 버전은 named-semaphore 기반).

설정 (broker_config.c + broker_config.h)

섹션 제목: “설정 (broker_config.c + broker_config.h)”
  • 최상위 진입점 — broker_config_read (cubrid_broker.conf 를 읽어 T_BROKER_INFO[] 를 채운다), broker_config_dump, dir_repath, conf_get_value_table_n.
  • 표 — tbl_appl_server (CAS vs CAS_CGW), tbl_on_off, tbl_allow_deny, tbl_sql_log_mode, tbl_keep_connection, tbl_access_mode, tbl_connect_order, tbl_proxy_log_mode.
  • 섹션 이름 — SECTION_NAME 은 일반 빌드에서 broker, FOR_ODBC_GATEWAY 일 때 gateway 다.

접근 제어 (broker_acl.c + broker_access_list.c)

섹션 제목: “접근 제어 (broker_acl.c + broker_access_list.c)”
  • 셋업 — access_control_set_shm (broker init에서 한 번 호출), access_control_read_config_file (cubrid_broker.acl 을 파싱해 shm_appl->access_info[] 로 옮긴다).
  • 런타임 검사 — access_control_check_right (CAS가 사 용하는 진입점), access_control_check_right_internal, access_control_check_ip, record_ip_access_time.
  • 포맷 검사 — is_invalid_acl_entry ([%broker] 헤더와 db:user:files 줄 검증).
  • IP 파일 파싱 — access_control_read_ip_info (octet 단 위로, * 와일드카드는 prefix 길이로 인코딩).
  • broker_access_list.creceiver_thr_f 에서 쓰이는 broker 단위 거친 IP 필터 (uw_acl_make, uw_acl_check).

관리 (broker_admin_pub.c + broker_admin.c + broker_admin_so.c)

섹션 제목: “관리 (broker_admin_pub.c + broker_admin.c + broker_admin_so.c)”
  • 공개 명령 표면 — admin_start_cmd, admin_stop_cmd, admin_on_cmd, admin_off_cmd, admin_reset_cmd, admin_info_cmd, admin_drop_cmd, admin_add_cmd, admin_acl_load_cmd, admin_acl_status_cmd, admin_acl_reload_cmd, admin_get_broker_status_from_shm, admin_get_proxy_conf, admin_get_shard_conf, etc.
  • CAS 단위 수명 주기 헬퍼 — as_activate, as_inactivate, proxy_activate, proxy_inactivate, proxy_activate_internal, make_env, free_env.
  • broker_admin.c — 위 함수들을 호출해 주는 얇은 cubrid 유틸 진입점. broker_admin_so.c 는 같은 로직을 cubridmanager 용 공유 객체로 노출한 버전이다.
  • broker_log_top.cmain, log_top_query, log_top, log_execute, read_multi_line_sql, read_bind_value, read_execute_end_msg, search_offset, organize_query_string, thr_main. SQL로 묶어 집계하는 멀티쓰레드 스캐너.
  • broker_log_top_tran.c — 같은 모양이지만 트랜잭션 단위로 묶는다.
  • broker_log_replay.creplay_main, replay_one_log_file (SQL 재실행).
  • broker_log_converter.c — 포맷 변환 유틸리티.
  • broker_log_util.c + broker_log_sql_list.c + broker_log_time.c — 위 모두가 공유하는 SQL 로그 줄 포맷 파서.
  • 진입점 — main, get_args, print_usage.
  • 표시 — appl_monitor, brief_monitor, metadata_monitor, client_monitor, unusable_databases_monitor, print_appl_header, print_monitor_header, print_monitor_items, appl_info_display, set_monitor_items, print_value, time_format, time2str, ip2str.
  • curses or not — 함수 포인터 tgoto_func_t, tgetent_func_t, tgetstr_func_t, tputs_func_t, tgetnum_func_t. 도구는 curses 모드 또는 plain stdout 모드 로 돈다. 후자가 대부분의 CI에서 쓰이는 모드다.
  • cas_cgw.c — alternate CAS main. 같은 cas_main_loop, 다른 CAS_MAIN_OPS 벡터.
  • cas_cgw_execute.c — opcode 핸들러들이 dbi.h 대신 cas_cgw_function.c 의 ODBC wrapper들을 부른다.
  • cas_cgw_odbc.c — 직접 ODBC API 표면 (SQLAllocHandle, SQLConnect, SQLPrepare, …).
  • broker_send_fd.c / broker_recv_fd.csend_fd / recv_fd SCM_RIGHTS 헬퍼 (Linux/macOS). Windows에서는 broker가 WSADuplicateSocket 으로 인프로세스 포트 핸드 오프를 한다.
  • broker_proxy_conn.c — shard 모드에서 쓰는 proxy 연 결 표 (broker_init_proxy_conn, broker_find_available_proxy, broker_delete_proxy_conn_by_fd, broker_destroy_proxy_conn).
  • broker_changer.c — 런타임에 broker 설정을 바꾸는 cubrid broker_changer 유틸.
  • broker_tester.ccubrid broker_tester 헬스 체크 CLI.
  • broker_process_size.cpsize_check_thr_f 와 oversize 자동 재시작을 끌고 가는 RSS 조회 (getsize).
  • broker_filename.c — SQL 로그, ACL 파일, AF_UNIX 소 켓 (ut_get_as_port_name) 의 경로 규약.
  • broker_max_heap.cjob_queue 가 쓰는 일반 max-heap.
  • broker_list.c — Windows shm 추적에서 쓰이는 작은 연 결 리스트 헬퍼.
  • broker_wsa_init.c — Windows WSA 부트스트랩.
심볼파일대략적 line
main (broker)src/broker/broker.c461
receiver_thr_fsrc/broker/broker.c784
dispatch_thr_fsrc/broker/broker.c1156
shard_dispatch_thr_fsrc/broker/broker.c1080
cas_monitor_thr_fsrc/broker/broker.c1912
cas_monitor_workersrc/broker/broker.c1797
broker_add_new_cassrc/broker/broker.c346
broker_drop_one_cas_by_time_to_killsrc/broker/broker.c392
find_idle_cassrc/broker/broker.c2700
find_drop_as_indexsrc/broker/broker.c2786
find_add_as_indexsrc/broker/broker.c2847
run_appl_serversrc/broker/broker.c1505
stop_appl_serversrc/broker/broker.c1619
restart_appl_serversrc/broker/broker.c1646
connect_srvsrc/broker/broker.c1726
init_envsrc/broker/broker.c1370
cleanupsrc/broker/broker.c715
shard_broker_processsrc/broker/broker.c653
main (cas)src/broker/cas.c209
cas_mainsrc/broker/cas.c262
shard_cas_mainsrc/broker/cas.c470
cas_initsrc/broker/cas.c735
cas_init_shmsrc/broker/cas.c764
process_requestsrc/broker/cas.c836
cas_register_to_proxysrc/broker/cas.c1468
net_read_processsrc/broker/cas.c1338
cas_db_connectsrc/broker/cas.c375
cas_post_db_connectsrc/broker/cas.c410
cas_make_session_for_driversrc/broker/cas.c280
cas_set_session_idsrc/broker/cas.c296
cas_send_connect_reply_to_driversrc/broker/cas.c321
cas_main_loopsrc/broker/cas_common_main.c79
cas_main_initsrc/broker/cas_common_main.c425
cas_accept_clientsrc/broker/cas_common_main.c981
cas_parse_db_infosrc/broker/cas_common_main.c1033
cas_handle_db_connectionsrc/broker/cas_common_main.c1119
cas_finish_sessionsrc/broker/cas_common_main.c1204
cas_sig_handlersrc/broker/cas_common_main.c522
query_cancelsrc/broker/cas_common_main.c781
restart_is_neededsrc/broker/cas_common_main.c813
set_hang_check_timesrc/broker/cas_common_main.c716
check_server_alivesrc/broker/cas_common_main.c741
net_read_header_keep_con_onsrc/broker/cas_common_main.c882
net_read_int_keep_con_autosrc/broker/cas_common_main.c1226
T_SHM_BROKERsrc/broker/broker_shm.h654
T_SHM_APPL_SERVERsrc/broker/broker_shm.h558
T_APPL_SERVER_INFOsrc/broker/broker_shm.h291
T_PROXY_INFOsrc/broker/broker_shm.h458
T_SHM_PROXYsrc/broker/broker_shm.h525
enum t_con_statussrc/broker/broker_shm.h187
uw_shm_open (POSIX)src/broker/broker_shm.c142
uw_shm_create (POSIX)src/broker/broker_shm.c337
broker_shm_initialize_shm_brokersrc/broker/broker_shm.c419
broker_shm_initialize_shm_assrc/broker/broker_shm.c481
access_control_set_shmsrc/broker/broker_acl.c62
access_control_read_config_filesrc/broker/broker_acl.c122
access_control_check_rightsrc/broker/broker_acl.c469
access_control_check_right_internalsrc/broker/broker_acl.c495
access_control_check_ipsrc/broker/broker_acl.c554
is_invalid_acl_entrysrc/broker/broker_acl.c326
access_control_read_ip_infosrc/broker/broker_acl.c370
log_top_querysrc/broker/broker_log_top.c347
log_topsrc/broker/broker_log_top.c471
log_executesrc/broker/broker_log_top.c717
thr_main (log_top)src/broker/broker_log_top.c448
appl_monitorsrc/broker/broker_monitor.c1316 (대략. 큰 파일)
brief_monitorsrc/broker/broker_monitor.c1395 (대략)
print_appl_headersrc/broker/broker_monitor.c768 (대략)
as_activatesrc/broker/broker_admin_pub.c1846 (대략)
as_inactivatesrc/broker/broker_admin_pub.c1995 (대략)
proxy_activatesrc/broker/broker_admin_pub.c2400 (대략)
broker_config_readsrc/broker/broker_config.c1100 (대략)

대략 표시가 붙은 줄 번호는 read 윈도우보다 큰 파일이라서 정확히 측정하지 못한 항목들이다. 심볼 이름 자체가 정식 앵커 다.

Broker SHM 와 server 측 session 표 (cubrid-server-session.md)

섹션 제목: “Broker SHM 와 server 측 session 표 (cubrid-server-session.md)”

broker는 자기 만의 이 클라이언트가 누구냐 라는 개념을 T_APPL_SERVER_INFO 안에 가지고 있다 (cas_clt_ip, cas_clt_port, clt_version, driver_info, cas_client_type). 서버 는 또 다른 개념을 들고 있는데, SESSION_ID 로 키잉되는 SESSION_STATE 가 그것이다 (cubrid-server-session.md 참조). CAS는 이 둘 사이의 다리 다. 첫 connect 때 cub_server 로부터 받은 SESSION_IDas_info->session_id 에 캐시해 둔다. 드라이버가 같은 세션 id 로 재접속하면 (KEEP_CON_AUTO + 드라이버 재시도), CAS는 그 id를 xsession_check_session 에 다시 넣어 prepared statement 캐시를 유지한다. 결정적으로, broker 자체는 세 션을 추적하지 않는다 — 슬롯을 추적할 뿐이다. CAS 간 세 션 마이그레이션은 지원되지 않는다 (세션 id가 그 세션을 연 CAS에 sticky 인 이유는 prepared statement 캐시가 shm이 아 니라 CAS의 힙에 살기 때문이다).

Broker CSS vs. 드라이버 CAS 프로토콜 (cubrid-network-protocol.md)

섹션 제목: “Broker CSS vs. 드라이버 CAS 프로토콜 (cubrid-network-protocol.md)”

cubrid-network-protocol.md 에서 다루는 CSS 프로토콜은 CAScub_server 에 말할 때 쓰는 것이다. 드라이버 가 broker/CAS 에 말할 때 쓰는 30바이트 SRV_CON_CLIENT_INFO 헤더 + MSG_HEADER 프레이밍은 별개이고 더 좁은 프로토콜 (CAS 프로토콜, 버전은 CAS_PROTO_VER_*) 이다. broker의 receiver_thr_f 만이 그 드라이버 헤더를 파싱하고, 그 뒤는 모두 CAS 내부다.

특히 network.henum net_server_request 는 드라이버- broker 경계에서 보이지 않는다. 보이는 것은 44개 CAS_FC_* opcode뿐이다. 드라이버는 새 CSS opcode 때문에 코드를 고칠 필요가 없어야 한다. 새 CAS opcode가 추가될 때만 그렇다. 그래서 CAS 프로토콜은 CSS 프로토콜보다 훨씬 천천히 움직 인다.

Broker vs. heartbeat (cubrid-heartbeat.md)

섹션 제목: “Broker vs. heartbeat (cubrid-heartbeat.md)”

server_monitor_thr_fconnect_to_master_for_server_monitorget_server_state_from_master 를 부른다. 이 둘은 cubrid-heartbeat.md 에 설명된 master/heartbeat 프로토콜을 거친다. broker는 읽기 전용 heartbeat 소비자다. 등록된 db_name@db_host 마다 master에게 그 서버가 REGISTERED_AND_ACTIVE 인지 _STANDBY 인지 DEAD 인지를 물어, 자기 unusable_databases[] 리스트를 업데이트한다. failover 자체 (어느 cub_server 가 active 가 되는가) 는 heartbeat의 결정이다. broker는 그 결과를 관찰만 하고, 죽었 다고 알려진 호스트로 새 세션이 가지 않게 막는다.

  • broker의 T_SHM_BROKER::magic 필드는 uw_shm_open 이 검 사하지만, 단조 증가하는 버전 필드는 없다. 그래서 broker 와 다른 commit 으로 빌드된 CAS라도 magic만 일치하면 깨끗 이 attach 할 수 있다. 실제로는 CUBRID가 broker와 CAS를 같이 패키징해서 출시하므로 이 일치는 패키징 계층의 보장 이지 런타임 검사된 보장은 아니다.
  • WIN_FW 는 레거시 Windows 인프로세스 포킹 모델이다 (service_thr_f, process_cas_request, read_from_cas_client). 오래된 빌드 플래그로 게이트되 어 표준 Linux 빌드에서는 작동하지 않는다. 살아 있는 경 로는 POSIX fd 패싱 변형이다.
  • _EDU_ 빌드 (#ifdef _EDU_) 는 EDU_KEY 상수를 출하해 하루 trial 동작을 게이트한다. 오픈소스 빌드에서는 사용 되지 않는다.
  • TLS 종단 위치. broker는 broker 섹션마다 use_SSL 을 존중한다. 켜져 있으면 IS_SSL_CLIENT(req_info.driver_info)CAS 안의 cas_init_ssl 로 라우팅하고, TLS 핸드셰이 크는 broker가 이미 send_fd 를 한 이후에 일어난다. 즉 broker 는 평문을 절대 보지 못한다. 보안적으로는 좋지 만, 그 대가로 broker가 SQL 자체에 기반한 L7 라우팅 결정 을 내리지 못한다는 뜻이기도 하다. broker에서 TLS를 종단 시키고 CAS에 다시 암호화해 주는 모드, 또는 proxy에서의 L7 라우팅에 대한 수요가 있는가?
  • HTTP/REST 게이트웨이. src/broker/ 아래의 어떤 소스 도 HTTP를 다루지 않는다. CUBRID를 REST로 소비하고 싶은 클라이언트는 오늘 CCI/JDBC + 외부 게이트웨이를 거쳐야 한 다. CGW는 ODBC 전용이다.
  • 비동기 쿼리 지원. process_requestfunc_code 당 엄격히 request-then-response 이다. 서버 측에서 푸시되는 cursor 진행이나 비동기 결과 셋 청크 전달을 위한 opcode가 없다. CAS 프로토콜은 Postgres 스타일 SUSPENDED portal 모델보다 앞선다. 가능한 진화 경로는 streaming 용 새 CAS_FC_* opcode 추가지만, 드라이버 변경이 필요하다 (같 은 request id 위에서 fetch 가 reentrant 해야 한다).
  • CAS 간 세션 마이그레이션. Cross-check Notes 에서 지적했듯이 prepared statement 캐시는 CAS의 힙에 산다. 캐시를 shm으로 옮겨, 드라이버 재접속이 같은 세션 id를 들고 있는 다른 CAS에 다시 붙을 수 있게 만들 수 있을 까? 그렇게 되면 broker가 드라이버에게 들키지 않으면서 CAS를 우아하게 재활용할 수 있게 된다. 비용은 캐시를 shm 안전하게 만드는 것이고, 이 일은 XASL_NODE 의 역직렬화 포인터들과 어색하게 상호작용한다.
  • 온라인 shard 재균형. T_SHM_PROXY::shm_shard_conn 의 shard 레이아웃은 proxy 시작 시점에 한 번 로드되고 라이 브 reshape 용으로 설계되어 있지 않다. cubrid broker shard reload 가 있긴 한데 메타데이터를 다시 읽을 뿐이 다. 트래픽이 흐르는 동안 키 범위를 한 shard에서 다른 shard로 옮기는 일은 broker가 오늘 지원하지 않는다 (그리 고 그러려면 엔진 쪽에서 행을 먼저 옮겨 둬야 한다).
  • 잡 큐 priority 의미론. max_heap_incr_priority 는 pop 할 때마다 호출되어서 오래된 잡이 결국 새 잡을 이기 지만, 그 bump가 일률적이다. broker별로 사용자가 튜닝 할 수 있는 priority 클래스는 없다. 미래 개정에서 [%broker] 설정 섹션에 priority 클래스 (예: 인터랙티브 vs. 배치) 를 노출할 수 있다.
  • 코드 (/data/hgryoo/references/cubrid 아래 경로):
    • src/broker/broker.c
    • src/broker/cas.c
    • src/broker/cas_common_main.c + cas_common_main.h
    • src/broker/cas_common_execute.c + cas_common_function.c
    • src/broker/cas_common_vars.c
    • src/broker/cas_network.c + cas_network.h
    • src/broker/broker_shm.c + broker_shm.h
    • src/broker/broker_config.c + broker_config.h
    • src/broker/broker_acl.c + broker_acl.h
    • src/broker/broker_access_list.c + broker_access_list.h
    • src/broker/broker_admin_pub.c + broker_admin_pub.h
    • src/broker/broker_admin.c + broker_admin_so.c
    • src/broker/broker_log_top.c + broker_log_top_tran.c
    • src/broker/broker_log_replay.c + broker_log_converter.c
    • src/broker/broker_log_util.c + broker_log_sql_list.c
    • src/broker/broker_monitor.c
    • src/broker/broker_proxy_conn.c
    • src/broker/broker_send_fd.c + broker_recv_fd.c
    • src/broker/broker_max_heap.c + broker_list.c
    • src/broker/broker_process_size.c + broker_process_info.c
    • src/broker/broker_filename.c + broker_util.c
    • src/broker/cas_cgw.c + cas_cgw_execute.c + cas_cgw_function.c + cas_cgw_odbc.c
    • src/communication/network.h (CAS 상위 호출이 참조하는 CSS 프로토콜 opcode enum)
    • src/connection/connection_defs.h (connect_to_master_for_server_monitor 가 쓰는 CSS_CONN_ENTRY)
  • 본 저장소 안 상호 참조:
    • cubrid-network-protocol.md (CAS가 cub_server 에 닿 을 때 쓰는 CSS 프로토콜)
    • cubrid-server-session.md (as_info->session_id 가 인 덱싱하는 서버 측 세션)
    • cubrid-heartbeat.md (server_monitor_thr_f 가 조회하 는 master/HA 계층)
  • 이론적 참고:
    • Petrov, Database Internals, 13장 Database Architectures 연결 풀링, shared 대 dedicated 워커 컨텍스트.
    • Birrell & Nelson, Implementing Remote Procedure Calls, ACM TOCS 1984 — broker→CAS→server 파이프라인이 구체화 하는 framing/dispatch 분리.
    • pgbouncer 문서 (트랜잭션 풀링 vs. 세션 풀링 의미론) CUBRID broker에 가장 가까운 비교 대상이지만, fd 핸드 오프 대신 바이트 프록시를 한다는 차이가 있다.
    • pgpool-II 문서 (풀링 위에 부하 분산 + failover).
    • Oracle Shared Server (구 MTS) 문서 — 엔진 안에 dispatcher
      • shared server 풀을 둔다는 점에서 broker + CAS의 가장 가까운 아키텍처적 사촌 격이다. 단, CUBRID의 워커는 SGA를 공 유하는 쓰레드가 아니라 외부 프로세스다.
    • Oracle Connection Manager (CMAN) 문서 — ACL/방화벽이 있 는 엔진 외부 프록시. v3_acl / access_control 계층의 비교 대상이다.