(KO) CUBRID Broker — CAS 프로세스 풀, 연결 라우팅, 클라이언트 측 프론트엔드
목차
학술적 배경
섹션 제목: “학술적 배경”관계형 데이터베이스 엔진은 결국 네트워크에 자기를 노출해야 한다. 그런데 그 노출이 시작되는 순간 세 개의 서로 다른 힘이 설계를 잡아당기기 시작한다는 점이다. 엔진 쪽은 오래 살고 비싸게 만들어 둔 인프로세스 상태 — 파서, XASL 캐시, 세션별 prepared statement 등록표, 인증된 트랜잭션 디스크립터 — 를 TCP가 끊길 때마다 같이 무너뜨리고 싶어 하지 않는다. 드라이 버 쪽은 가장 단순한 모델만 쓰고 싶다. 소켓을 열고, SQL을 보내고, 행을 읽고, 소켓을 닫는다는 것이다. 운영자 쪽은, 드라이버 측에서 몇 개의 풀이 동시에 두드리든 상관없이, 호스 트가 실제로 먹여 살릴 수 있는 무거운 컨텍스트의 수에 수평적 인 상한을 두고 싶다는 것이다. Database Internals (Petrov, 13장 Database Architectures) 는 이 긴장의 결과를 한 클라이 언트당 전용 워커 컨텍스트를 두는 dedicated 모델과, 더 작 은 풀 위로 클라이언트들을 다중화하는 shared 모델의 선택 으로 정리한다. 그리고 그 사이에 connection broker 가 끼 어 들어, 값싼 TCP 소켓 다수를 비싼 엔진 핸들 소수로 변환한 다는 그림을 그린다.
세 가지 서로 독립적인 설계 결정이 모든 구체적 broker 구현 의 골격을 정한다. 본 문서의 나머지 구조는 이 세 결정 위에 앉는다.
-
프로세스 모델 — 쓰레드인가, 프로세스인가, 혼합인가. 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들은 멈추지 않는다 는 운영성을 산다. -
풀링 단위 — 세션 풀링인가, 트랜잭션 풀링인가. 세션 풀링 에서는 클라이언트가 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가 들고 있는 게 아니다. -
broker가 와이어를 어디까지 프록시하는가. broker는 SQL 프로토콜의 L7 프록시다 (
PING과QC쿼리 취소, connect 핸드셰이크를 직접 이해하고 그 외는 라우팅한다). 그래서 클라이언트 소켓을 요청 수명 동안 broker에 유지 할지, 아니면 워커에게 넘겨 직접 통신하게 할지를 결정 해야 한다. 넘기는 쪽이 빠르다 (패킷당 memcpy 한 번 덜, broker 병목 없음). 그러나 그러려면 워커마다 별개 리스너 를 두거나 (TCP 포트가 워커 수만큼 — 낭비고 방화벽 운영 이 어렵다), 두 무관한 프로세스 사이에서 열린 파일 디스 크립터를 옮길 수 있어야 한다. POSIX는 후자에 정확히 한 가지 원시 도구만 준다.AF_UNIXSOCK_STREAM위의SCM_RIGHTSancillary 메시지인데, 커널이recvmsg동 안 디스크립터를 수신측 프로세스 표에 복제해 준다. 이게 리눅스의 표준 관용구다 (nginx, 일부 모드의pgbouncer, Postgrespg_dump의 병렬 인프라가 모두 이걸 쓴다). CUBRID도 이걸 쓴다. broker는 공개 broker 포트에서 TCP 소켓을 accept한 뒤 비어 있는 CAS를 골라 미리 만들어 둔AF_UNIX랑데부 채널로 커널 디스 크립터 를send_fd()로 CAS에게 보내고, 자기 사본은 닫는다. 그 시점부터 broker와 CAS는 공유 메모리만 거쳐 통신하고, 클라이언트 바이트는 CAS↔드라이버로 곧장 흐른다. broker는 살아 있는 쿼리를 떨어뜨리지 않고도 디버깅하거 나 재시작할 수 있다 (원칙적으로는 Open Questions 참조).
이 세 가지 결정이 명명되고 나면 — 프로세스 풀, 세션-편향 풀링, fd 패싱 핸드오프 — CUBRID broker 코드의 나머지 모양 은 직접적인 결과다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서적 그림 아래로 내려가 보면, 한 번에 두 개 이상의 클라 이언트를 다뤄야 하는 모든 관계형 엔진은 비슷한 모양으로 진 화한다. 정통 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 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | 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.c 의 receiver_thr_f |
| 잡 큐 (priority + 도착시간 기준 max-heap) | T_SHM_APPL_SERVER 안의 T_MAX_HEAP_NODE job_queue[JOB_QUEUE_MAX_SIZE+1] |
| Dispatcher 쓰레드 | broker.c 의 dispatch_thr_f (비-shard) / shard_dispatch_thr_f (shard) |
| Idle CAS 선택기 | broker.c 의 find_idle_cas |
| Auto-grow 선택기 | broker.c 의 find_add_as_index + broker_add_new_cas |
| Auto-shrink 선택기 | broker.c 의 find_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.c 의 cas_main_loop, cas_main/shard_cas_main 에서 호출 |
| CAS 측 요청 디스패치 표 | cas.c 의 static 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.h 의 UTS_STATUS_IDLE, _BUSY, _RESTART, _START, _CON_WAIT, _STOP 매크로 |
| 접근 제어 | broker_acl.c 의 access_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.c 의 T_SHM_PROXY + proxy_* 데몬. CAS↔proxy↔broker 한 hop이 추가 |
| ODBC 게이트웨이 변형 | cas_cgw.c, cas_cgw_execute.c, cas_cgw_odbc.c. broker는 같지만 CAS 바이너리가 다르다 |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”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_activate 가
br_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/>— magic, num_broker, my_ip_addr,<br/>access_control_file, br_info[1]"]
APPLSHM["T_SHM_APPL_SERVER<br/>— 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/>— 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_f 와 dispatch_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_open
의 magic 필드 검사가 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_cas 는 as_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.cclt_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.cnew_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.cpthread_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_cas는as_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_srv는AF_UNIXSOCK_STREAM소켓을 연다. 경로는ut_get_as_port_name(path, br_name, as_index, …)이 만들어 준다 (보통$CUBRID_TMP/아래). 각 CAS는 그 경로에서listen하고 있고, broker 는 핸드오프마다 새로 제어 연결을connect하고, 4바이 트con_statusping-pong으로 CAS가 살아 있고 받을 의사 가 있는지 확인하고,send_fd를 부르고,uts_statusack을 읽고, 랑데부 소켓을 닫는다. 이 시점에 클라이언트 의 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_request 는 func_code 로 server_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.md 의 enum net_server_request
참조). 드라이버와 서버는 CAS가 양쪽을 정직하게 유지해 주는
한 독립적으로 진화할 수 있다.
Auto-grow와 auto-shrink
섹션 제목: “Auto-grow와 auto-shrink”find_idle_cas 가 -1을 돌려줄 때마다 dispatcher는
broker_add_new_cas 를 부른다.
// broker_add_new_cas — src/broker/broker.ccur_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.cif (cur_appl_server_num <= shm_br->br_info[br_index].appl_server_min_num || wait_job_cnt > 0) return false;그래서 CAS 풀은 min 과 max 사이에서 진동한다. max 는
강한 backpressure 상한 역할을 하고, min 은 따뜻한 풀 보장
역할을 한다.
접근 제어 (ACL)
섹션 제목: “접근 제어 (ACL)”broker는 자체 ACL 계층이 있다. 서버 측 인증과는 별
개다. 별도의 cubrid_broker.acl 파일 (경로는 T_SHM_BROKER
의 access_control_file) 이 broker 단위 섹션을 담는다.
[%query_editor]demodb:dba:dba_ips.txtdemodb: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.cif (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든 일치).
SQL 로그 수집
섹션 제목: “SQL 로그 수집”각 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의 모
니터 쓰레드들이 그 변화를 관찰한다.
Hang 감지
섹션 제목: “Hang 감지”broker의 hang_check_thr_f 는 monitor_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 이벤트 이후 운영자 개입이 기대된다.
SHARD / proxy 변형
섹션 제목: “SHARD / proxy 변형”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_PROXY 의 shard_key_function_name 가 가리키는 라이
브러리로 shard id를 계산해 올바른 CAS 풀로 라우팅한다.
shard 모드의 CAS는 fork 시점에 특정 shard_id 에 묶이고
(as_info->shard_cas_id, as_info->proxy_id), broker 핸드
오프를 기다리지 않고 자기가 proxy 쪽으로 접속한다
(cas_network.c 의 net_connect_proxy).
// shard_cas_main — src/broker/cas.cproxy_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 풀을 공유하는 많은 클라이언트에 분 할 상환된다.
CGW (ODBC 게이트웨이) 변형
섹션 제목: “CGW (ODBC 게이트웨이) 변형”FOR_ODBC_GATEWAY 빌드는 같은 broker 뼈대를 재사용하면서
다른 CAS를 묶은 별개 바이너리를 낸다.
cas_cgw.c—cas_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
섹션 제목: “Failover”failover는 broker의 일이 *아니다. broker는 단일 호스트
다. CAS의 database_host 와 db_connection_file (후보 호
스트 목록) 은 dbi 의 connect 경로로 그대로 전달되고, 주
호스트가 닿지 않으면 CAS 안의 드라이버 가 후보 목록을 순
회한다 (shm의 connect_order 가 sequential 인지 random 인
지를 정하고, replica_only_flag 와 access_mode 는 HA 역
할 기준으로 후보를 더 좁힌다). CUBRID가 하는 HA 재라우팅
자체는 server 측 heartbeat / master 프로세스에서 일어난다
(cubrid-heartbeat.md 참조). broker의 유일한 기여는
server_monitor_thr_f 인데, 등록된 각 db_host 의 상태를
master에 주기적으로 묻고 그 결과로 unusable_databases[]
를 뒤집어, 죽은 replica로 향하는 향후 드라이버 connect를
단락시킨다.
Source Walkthrough
섹션 제목: “Source Walkthrough”아래는 서브시스템별로 묶은 읽기 순서다. 절 끝의 position
hints 표는 각 심볼을 (file, line) 문서가 마지막으로
updated: 되었을 시점 기준 으로 짝지어 놓은 것이다.
Broker 프로세스 (broker.c)
섹션 제목: “Broker 프로세스 (broker.c)”- 프로세스 부트스트랩 —
main(argv 파싱, shm 읽기, 시그널 설치),broker_init_shm(broker 자체 shm attach),init_env(PORT_NUMBER_ENV_STR위에서 TCPsocket+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(inbroker_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_info를AS_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_open이magic == 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(CASvsCAS_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.c—receiver_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용 공유 객체로 노출한 버전이다.
SQL 로깅 (broker_log_*.c)
섹션 제목: “SQL 로깅 (broker_log_*.c)”broker_log_top.c—main,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.c—replay_main,replay_one_log_file(SQL 재실행).broker_log_converter.c— 포맷 변환 유틸리티.broker_log_util.c+broker_log_sql_list.c+broker_log_time.c— 위 모두가 공유하는 SQL 로그 줄 포맷 파서.
모니터링 (broker_monitor.c)
섹션 제목: “모니터링 (broker_monitor.c)”- 진입점 —
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에서 쓰이는 모드다.
CGW (cas_cgw*.c)
섹션 제목: “CGW (cas_cgw*.c)”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.c—send_fd/recv_fdSCM_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.c—cubrid broker_tester헬스 체크 CLI.broker_process_size.c—psize_check_thr_f와 oversize 자동 재시작을 끌고 가는 RSS 조회 (getsize).broker_filename.c— SQL 로그, ACL 파일, AF_UNIX 소 켓 (ut_get_as_port_name) 의 경로 규약.broker_max_heap.c—job_queue가 쓰는 일반 max-heap.broker_list.c— Windows shm 추적에서 쓰이는 작은 연 결 리스트 헬퍼.broker_wsa_init.c— Windows WSA 부트스트랩.
Position hints (updated: 기준)
섹션 제목: “Position hints (updated: 기준)”| 심볼 | 파일 | 대략적 line |
|---|---|---|
main (broker) | src/broker/broker.c | 461 |
receiver_thr_f | src/broker/broker.c | 784 |
dispatch_thr_f | src/broker/broker.c | 1156 |
shard_dispatch_thr_f | src/broker/broker.c | 1080 |
cas_monitor_thr_f | src/broker/broker.c | 1912 |
cas_monitor_worker | src/broker/broker.c | 1797 |
broker_add_new_cas | src/broker/broker.c | 346 |
broker_drop_one_cas_by_time_to_kill | src/broker/broker.c | 392 |
find_idle_cas | src/broker/broker.c | 2700 |
find_drop_as_index | src/broker/broker.c | 2786 |
find_add_as_index | src/broker/broker.c | 2847 |
run_appl_server | src/broker/broker.c | 1505 |
stop_appl_server | src/broker/broker.c | 1619 |
restart_appl_server | src/broker/broker.c | 1646 |
connect_srv | src/broker/broker.c | 1726 |
init_env | src/broker/broker.c | 1370 |
cleanup | src/broker/broker.c | 715 |
shard_broker_process | src/broker/broker.c | 653 |
main (cas) | src/broker/cas.c | 209 |
cas_main | src/broker/cas.c | 262 |
shard_cas_main | src/broker/cas.c | 470 |
cas_init | src/broker/cas.c | 735 |
cas_init_shm | src/broker/cas.c | 764 |
process_request | src/broker/cas.c | 836 |
cas_register_to_proxy | src/broker/cas.c | 1468 |
net_read_process | src/broker/cas.c | 1338 |
cas_db_connect | src/broker/cas.c | 375 |
cas_post_db_connect | src/broker/cas.c | 410 |
cas_make_session_for_driver | src/broker/cas.c | 280 |
cas_set_session_id | src/broker/cas.c | 296 |
cas_send_connect_reply_to_driver | src/broker/cas.c | 321 |
cas_main_loop | src/broker/cas_common_main.c | 79 |
cas_main_init | src/broker/cas_common_main.c | 425 |
cas_accept_client | src/broker/cas_common_main.c | 981 |
cas_parse_db_info | src/broker/cas_common_main.c | 1033 |
cas_handle_db_connection | src/broker/cas_common_main.c | 1119 |
cas_finish_session | src/broker/cas_common_main.c | 1204 |
cas_sig_handler | src/broker/cas_common_main.c | 522 |
query_cancel | src/broker/cas_common_main.c | 781 |
restart_is_needed | src/broker/cas_common_main.c | 813 |
set_hang_check_time | src/broker/cas_common_main.c | 716 |
check_server_alive | src/broker/cas_common_main.c | 741 |
net_read_header_keep_con_on | src/broker/cas_common_main.c | 882 |
net_read_int_keep_con_auto | src/broker/cas_common_main.c | 1226 |
T_SHM_BROKER | src/broker/broker_shm.h | 654 |
T_SHM_APPL_SERVER | src/broker/broker_shm.h | 558 |
T_APPL_SERVER_INFO | src/broker/broker_shm.h | 291 |
T_PROXY_INFO | src/broker/broker_shm.h | 458 |
T_SHM_PROXY | src/broker/broker_shm.h | 525 |
enum t_con_status | src/broker/broker_shm.h | 187 |
uw_shm_open (POSIX) | src/broker/broker_shm.c | 142 |
uw_shm_create (POSIX) | src/broker/broker_shm.c | 337 |
broker_shm_initialize_shm_broker | src/broker/broker_shm.c | 419 |
broker_shm_initialize_shm_as | src/broker/broker_shm.c | 481 |
access_control_set_shm | src/broker/broker_acl.c | 62 |
access_control_read_config_file | src/broker/broker_acl.c | 122 |
access_control_check_right | src/broker/broker_acl.c | 469 |
access_control_check_right_internal | src/broker/broker_acl.c | 495 |
access_control_check_ip | src/broker/broker_acl.c | 554 |
is_invalid_acl_entry | src/broker/broker_acl.c | 326 |
access_control_read_ip_info | src/broker/broker_acl.c | 370 |
log_top_query | src/broker/broker_log_top.c | 347 |
log_top | src/broker/broker_log_top.c | 471 |
log_execute | src/broker/broker_log_top.c | 717 |
thr_main (log_top) | src/broker/broker_log_top.c | 448 |
appl_monitor | src/broker/broker_monitor.c | 1316 (대략. 큰 파일) |
brief_monitor | src/broker/broker_monitor.c | 1395 (대략) |
print_appl_header | src/broker/broker_monitor.c | 768 (대략) |
as_activate | src/broker/broker_admin_pub.c | 1846 (대략) |
as_inactivate | src/broker/broker_admin_pub.c | 1995 (대략) |
proxy_activate | src/broker/broker_admin_pub.c | 2400 (대략) |
broker_config_read | src/broker/broker_config.c | 1100 (대략) |
대략 표시가 붙은 줄 번호는 read 윈도우보다 큰 파일이라서 정확히 측정하지 못한 항목들이다. 심볼 이름 자체가 정식 앵커 다.
Cross-check Notes
섹션 제목: “Cross-check Notes”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_ID 를
as_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 프로토콜은
CAS 가 cub_server 에 말할 때 쓰는 것이다. 드라이버 가
broker/CAS 에 말할 때 쓰는 30바이트 SRV_CON_CLIENT_INFO
헤더 + MSG_HEADER 프레이밍은 별개이고 더 좁은 프로토콜
(CAS 프로토콜, 버전은 CAS_PROTO_VER_*) 이다. broker의
receiver_thr_f 만이 그 드라이버 헤더를 파싱하고, 그 뒤는
모두 CAS 내부다.
특히 network.h 의 enum 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_f 는 connect_to_master_for_server_monitor
와 get_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는 그 결과를 관찰만 하고, 죽었
다고 알려진 호스트로 새 세션이 가지 않게 막는다.
드리프트 / open
섹션 제목: “드리프트 / open”- 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 동작을 게이트한다. 오픈소스 빌드에서는 사용 되지 않는다.
Open Questions
섹션 제목: “Open Questions”- 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_request는func_code당 엄격히 request-then-response 이다. 서버 측에서 푸시되는 cursor 진행이나 비동기 결과 셋 청크 전달을 위한 opcode가 없다. CAS 프로토콜은 Postgres 스타일SUSPENDEDportal 모델보다 앞선다. 가능한 진화 경로는 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. 배치) 를 노출할 수 있다.
Sources
섹션 제목: “Sources”- 코드 (
/data/hgryoo/references/cubrid아래 경로):src/broker/broker.csrc/broker/cas.csrc/broker/cas_common_main.c+cas_common_main.hsrc/broker/cas_common_execute.c+cas_common_function.csrc/broker/cas_common_vars.csrc/broker/cas_network.c+cas_network.hsrc/broker/broker_shm.c+broker_shm.hsrc/broker/broker_config.c+broker_config.hsrc/broker/broker_acl.c+broker_acl.hsrc/broker/broker_access_list.c+broker_access_list.hsrc/broker/broker_admin_pub.c+broker_admin_pub.hsrc/broker/broker_admin.c+broker_admin_so.csrc/broker/broker_log_top.c+broker_log_top_tran.csrc/broker/broker_log_replay.c+broker_log_converter.csrc/broker/broker_log_util.c+broker_log_sql_list.csrc/broker/broker_monitor.csrc/broker/broker_proxy_conn.csrc/broker/broker_send_fd.c+broker_recv_fd.csrc/broker/broker_max_heap.c+broker_list.csrc/broker/broker_process_size.c+broker_process_info.csrc/broker/broker_filename.c+broker_util.csrc/broker/cas_cgw.c+cas_cgw_execute.c+cas_cgw_function.c+cas_cgw_odbc.csrc/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계층의 비교 대상이다.