콘텐츠로 이동

(KO) CUBRID 스레드 매니저 — 고동시성 Connection/Worker 풀과 논리적 대기 인지 동시성 제어 (CBRD-26177 + CBRD-26662)

목차

이 문서는 CUBRID의 legacy 서버 스레딩 — accept된 connection 마다 polling 스레드 한 개에, 그 뒤로 max_clients 크기의 cubthread::worker_pool, 모든 dispatch가 풀 단위 mutex를 거치는 구조 — 를 대체하는 재설계를 다룬다. 그 legacy 기준선은 형제 문서 cubrid-thread-worker-pool.md에 따로 정리돼 있으며, 거기서 “connection마다 스레드” 또는 task_group/task_worker 크기 조절을 말하는 부분은 아래 설계로 대체된다. 작업은 guava 릴리스에 2단계로 들어갔다.

  • Phase 1 — Connection/Worker 풀 재설계 (EPIC CBRD-26177, Resolved). 앞단을 작고 크기가 제한된 epoll 구동 connection worker 집합으로 교체하고, coordinator 를 두어 connection 분배·재배치와 개수의 동적 조절을 맡긴다. 한 tick의 I/O는 send/recv 예산 으로 묶고, context 할당은 worker별 freelist로 돌려 hot path에서 new/delete 호출을 없앤다. 뒤단 task worker pool은 그대로 두되 두 개의 잠정 파라미터(task_group, task_worker)로 크기를 잡는다.

  • Phase 2 — 논리적 대기 인지 동시성 제어 (EPIC CBRD-26662, PR #7323, develop 대상 open). 고정된 task_group/task_worker 크기 조절을, 실제 동시성을 스레드 수가 아니라 core별 slot 으로 묶는 elastic worker pool 로 바꾼다. 논리적 대기(트랜잭션 lock 대기, CSS job 큐 idle, PL 블로킹 호출)에 들어간 worker는 자기 slot을 반납해 다른 worker가 그 자리에서 돌게 하고, 빈 slot은 50ms slot daemon 이 재분배한다. 두 knob은 max_request_concurrency(활성 task 상한)와 max_request_worker (스레드 overcommit 상한)가 되며, 둘 다 런타임에 조정 가능하다.

Phase 1은 앞단 이야기(바이트가 어떻게 task worker에 닿는가)이고, Phase 2는 뒤단 이야기(동시에 몇 개의 task worker가 돌 수 있으며 하나가 막히면 어떻게 되는가)다. 둘은 같은 guava 릴리스 트레인과 같은 “hot path에 perfmon 금지” 원칙(아래)을 공유하므로 이 한 문서가 둘을 함께 담는다.

데이터베이스 서버의 connection 앞단은 수많은 TCP 소켓을 유한한 CPU core 위에 multiplex 해야 한다. 문헌이 압축한 아키텍처는 셋이며, 각각 소켓·스레드·event-loop iteration 사이의 매핑이 다르다.

Thread-per-connection (한 클라이언트당 한 스레드). accept된 소켓마다 전용 커널 스레드 하나가 붙어 직접 read()/write() 를 부른다. event-loop 부기도 demultiplex도 없으니 모델이 단순하다 — Stevens 의 UNIX Network Programming, Vol. 1 (3판, §16.5 “TCP Concurrent Server, One Child per Client”) 이 이를 전형적인 Unix 서버로 그려 두었다. 단, 커널의 스레드 전환 비용이 압도하기 시작 하는 지점까지만 확장된다. C10K를 넘기면 스택의 working set이 L1/L2 캐시를 깨고, idle 스레드가 늘어나는 만큼 스케줄러 runqueue 도 선형으로 길어지며, 스레드들이 공유하는 mutex 하나만 있어도 서버 전체가 직렬화된다. Database Internals (Petrov, 2019, §5.3 Concurrent Execution) 가 결론을 한 줄로 요약한다 — 수만 개의 동시 connection으로 확장하려면 connection 당 스레드 모델은 비현실 적이다.

Reactor (이벤트 구동). 작은 고정 크기의 event-loop 스레드 풀이 각자 multiplexer (select/poll/epoll/kqueue) 위에서 block 하고, ready 상태가 된 소켓을 동기적으로 dispatch한다. 교과서 격 작업은 Pai, Druschel, Zwaenepoel, “Flash: An Efficient and Portable Web Server” (USENIX 1999) 다 — 비대칭 단일 event loop와 non-blocking I/O 만으로 thread형 서버를 따라잡거나 능가 하면서 메모리 비용은 한 자릿수 절약된다는 점을 보였다. 결정적인 기계적 개량은 edge-triggered epoll 이다. EPOLLET 가 켜져 있으면 커널은 readiness 전이가 일어났을 때 정확히 한 번만 알려 주고, 사용자 공간 루프가 EAGAIN 까지 직접 소켓을 빨아 내야 한다. edge-trigger는 wake-up storm을 없애지만, 한 번의 빨아 내기를 어느 정도 선에서 멈출지 정하지 않으면 굵은 connection 하나가 동료들을 굶긴다. 이것이 event-loop worker 안에서의 head-of-line blocking 문제다.

Proactor (비동기 I/O). 커널이 readiness 가 아니라 completion 을 알려 주는 모델이다 — Windows IOCP, Linux io_uring, POSIX AIO. 쓰기 비중이 큰 워크로드에서 개념적으로 는 우월하지만 운용이 무겁고 데이터베이스 앞단의 기본 옵션은 아직 아니다. CUBRID의 재설계는 일부러 reactor + edge trigger를 골랐다. proactor는 이 문서 범위 밖이다.

예산을 통한 admission 제어. Welsh, Culler, Brewer의 SEDA (“SEDA: An Architecture for Well-Conditioned, Scalable Internet Services,” SOSP 2001) 는 앞단을 크기가 제한된 큐로 연결된 stage 시퀀스로 보고 각 stage가 독자적인 admission 정책을 적용하게 했다. 경험적 관찰은 분명하다 — 포화 상태에서 latency 가 무너지는 정도는, 각 stage가 한 tick에 흡수할 일을 cap 해 둘 때 훨씬 완만해진다. CUBRID의 recv_budget_per_connectionsend_budget_per_connection (CBRD-26392) 이 한 epoll tick 위에 SEDA admission gate를 그대로 적용한 것이다 — 1 MB 쯤은 거뜬히 빨아 들이려는 굵은 reader가 16 KB에서 한 번 양보하고 exhausted 리스트에 자기를 등록한 뒤 다음 iteration 에서 worker가 round-robin으로 다시 잡아 주기를 기다리게 된다.

풀 크기 — Little의 법칙. 도착률 λ (요청/초) 와 평균 서비스 시간 S (초) 가 주어졌을 때, 평균 in-flight 요청 수는 L = λ · S 다. worker가 L보다 적으면 큐가 끝없이 자라고, 한참 많으면 context switch와 내부 critical section 위에서 CPU가 낭비된다. Database Internals (§5.3) 는 실제 시스템이 보통 물리 코어 수의 작은 배수를 골라 경험적으로 튜닝한다고 짚는다. S 자체가 워크로드 에 따라 움직이기 때문이라는 뜻이다. CBRD-26424 (점수 기반 배치) 와 CBRD-26636 (Worker 개수 sweep) 이 정확히 이 경험적 루프를 구현한다 — 여러 task_worker 크기에서 throughput을 재고, 그 안에서 local 최댓값을 고르는 식이다.

Atomic 없는 모니터링. 순진한 성능 카운터는 매 이벤트마다 std::atomic<uint64_t>::fetch_add 를 호출한다. 부하가 높아지면 카운터의 cache-line이 코어들 사이에서 ping 한다. worker별 초당 수십만 이벤트 단위가 되면, 그 cache 경합 자체가 카운터가 측정 하려던 병목이 된다. 정착된 회피책은 thread-local 누적 + 게으른 집계 다. worker마다 자기만의 사적 카운터를 올리고, 모니터 reader 가 그것들을 더한다. CBRD-26191 가 이 효과를 YCSB로 보여 줬다 — hot path에서 atomic 명령만 빼는 것만으로 workload-a 가 58 K → 60 K ops, workload-b 가 70 K → 73 K ops 가 되었다. 이번 재설계의 connection worker 통계는 같은 규칙을 따른다 — statistics::metrics<> 는 worker마다 가지는 단순한 uint64_t[] 이고, coordinator가 1초 타이머로 모아서 합산한다.

제한된 동시성 vs. worker 수 — 논리적 대기 문제. Phase 1이 답한 것은 스레드가 몇 개 있어야 하는가 이고, Phase 2가 답하는 것은 다른 질문 — 한 번에 몇 개가 돌 수 있어야 하는가 다. 둘은 같은 수가 아니다. Little’s law (L = λ · S) 는 in-flight 요청 수를 정하지만, 트랜잭션의 service time S는 순수 CPU가 아니다 — row/page lock 대기, condition variable 대기, 다른 서버(PL/Java 저장 프로시저)로의 중첩 호출 같은 논리적 대기 를 포함한다. 논리적 대기에 park된 worker는 코어를 쓰지 않으면서 풀 slot 하나를 차지한다. 여기서 풀 크기를 어떻게 잡느냐가 특히 중요해진다 — CBRD-26636이 잰 대로 lean 풀(≈ 코어의 4–6배)이 critical-section·캐시 경합을 최소화해 throughput을 최대화한다면, lock 대기에 걸린 몇 개의 worker가 풀 전체를 차지해 모든 코어가 idle인데도 서버가 멈춘 듯 보일 수 있다. 이것이 고전적인 스레드 기아 교착 — 동시에 막히는 작업 수보다 worker 스레드가 적을 때 벌어지는 일 — 이다.

admission-control 탈출구: “활성”과 “상주”의 분리. 정착된 해법은 동시성 상한(동시에 실제로 실행 중인 작업 수)과 스레드 수(상주하는 OS 스레드 수)를 분리하는 것이다. MySQL Enterprise Thread Pool이 가장 가까운 선례다 — 각 thread group이 stall 감지 타이머를 돌려, group의 모든 스레드가 막혔는데 작업이 남아 있으면 여분 스레드를 깨우거나 띄운다. SQL Server SOS 스케줄러도 cooperative하게 같은 목적을 이룬다. CUBRID Phase 2는 MySQL 모양을 따르되 토큰을 뒤집는다 — 실행 중인 스레드 를 세는 대신 활성 worker에게 slot 을 쥐여 주고, slot 수를 max_request_concurrency 로 묶으며, worker가 논리적 대기에 들어가면 slot을 반납 하게 해 다음 작업에 넘어가는 것이 새 스레드가 아니라 그 slot이 되도록 한다. 스레드는 max_request_worker까지 overcommit되지만, 어느 순간에도 그중 max_request_concurrency개만 slot을 쥐고 돈다. slot은 곧 “CPU 작업이 있다”와 “무언가 논리적인 것을 기다린다” 사이 경계에 놓인 런타임 조정 가능한 admission gate 다.

connection 앞단의 공통 설계 공간은 C10K 시대 이래 좁아져 왔다. 거의 모든 현대 엔진이 스레드 × event loop 매트릭스의 네 점 중 하나에 자리 잡는다.

PostgreSQL — connection 당 프로세스. postmaster 가 accept 된 connection마다 postgres 백엔드 프로세스를 fork 한다. 이 모델은 격리 강도가 높지만 (한 백엔드가 죽어도 동료들을 죽이지 않고 재시작 가능) connection 당 메모리 비용 (≥10 MB) 이 크다. PostgreSQL 커뮤니티는 core에서 이 모델을 갈아치우자는 제안을 일관되게 거절해 왔고, 대신 PgBouncer 같은 외부 pooler를 고 동시성 워크로드에서 쓰라고 권장한다. CUBRID의 “N개 connection 당 하나의 CPU 고정 event loop” 같은 것은 PostgreSQL 본체에는 없다.

MySQL — 기본은 connection 당 스레드, 옵션으로 thread pool 플러그인. 기본 Connection_handler_managerone-thread- per-connection 으로 동작해, TCP 세션마다 전용 pthread 를 부여한다. Enterprise Thread Pool 플러그인이 이를 thread group (보통 코어 수와 같은 수) + group 당 작은 admission 큐로 대체 한다. 이 플러그인이 존재하는 이유는 정확히 — CBRD-26152 가 잰 것과 같은 워크로드에서 unbounded thread-per-connection 모델 이 수백 개 동시 세션 너머에서 무너지기 때문이다. CUBRID의 재 설계는 같은 이웃 동네로 이동한다 — 개수가 제한된 connection worker, group 기반 task dispatch, 예산을 통한 admission — 다만 플러그인 형태가 아니다.

Oracle — dedicated server vs. shared server (DRCP). 기본은 dedicated-server (세션 당 프로세스) 다. shared-server 모드는 dispatcher 가 listening 소켓을 쥐고 큐로 요청을 넘기는 구조로, 여러 세션을 작은 server process 풀 위에 multiplex 한다. Database Resident Connection Pooling (DRCP) 가 이를 일반화해 여러 애플리케이션 서버가 같은 백엔드 풀을 공유하게 한다. CUBRID의 coordinator는 Oracle dispatcher와 같은 중재 역할을 하지만, worker별 통계가 더 세밀하고 자동 스케일링 규칙이 추가되어 있다.

SQL Server — SOSScheduler (cooperative). SQL Server 의 SOS 스케줄러는 고정된 worker 스레드 (≈ 논리 코어 수) 를 두고, 엔진 안의 well-defined yield point에서 cooperative하게 전환 한다. connection은 자기 스레드를 소유하는 게 아니라 스케줄러에 붙는다. CUBRID의 재설계는 PostgreSQL보다 이쪽에 가깝다 — connection worker가 CPU에 고정되어 있고, 개수가 min/max 범위 안에서 정해지며, 한 loop iteration에 여러 세션을 처리한다.

Legacy CUBRID이 어디 있었나. CBRD-26177 이전의 서버는 connection 당 polling 스레드 (각 css_master_thread 가 띄운 세션이 자기 소켓 위에서 loop) 와 max_clients 크기의 cubthread::worker_pool 을 함께 운영했다 — 자세한 walkthrough 는 cubrid-thread-worker-pool.md 에 있다. max_clients 가 2000으로 잡혀 있으면 풀 saturation 시 엔진은 실제로 ≥4000개 스레드를 쥐고 있었다. 모든 polling 스레드가 매 dispatch마다 worker pool의 per-core mutex를 두고 다툼하니, CBRD-26152 가 잰 YCSB-a 의 결과는 동시성이 올라가는데 throughput이 내려가는 모양이었고, 그 여분의 CPU는 user code가 아니라 mutex idle에서 타고 있었다.

재설계가 자리 잡은 곳. CBRD-26177 이후의 앞단은 (min_connection_workermax_connection_worker, 기본값 4 … cores/2) epoll 구동 cubconn::connection::worker 스레드들의 작은 집합이며, 각자 코어에 고정된다. 뒤단은 여전히 task_group × task_worker (옛 thread_core_count × 옛 worker 수에서 이름을 바꾼 두 파라미터) 로 크기가 정해지는 cubthread::worker_pool 이다. 단 한 개의 cubconn::connection::coordinator 스레드가 (역시 코어 고정) 새 클라이언트 배치, 재배치, 자동 스케일링을 중재한다. hot path (connection worker → task push → task worker pop) 는 이제 공유 mutex를 거치지 않는다. 잠깐 잡는 것은 connection 단위 인 css_conn_entry::cmutex / rmutex 뿐이다.

lock에 막힌 worker를 다루는 법 (Phase 2 비교). 위 네 엔진은 바쁜 worker가 lock 에 막혔을 때의 처리에서도 갈리는데, 이것이 정확히 CBRD-26662가 메우는 지점이다. PostgreSQL과 Oracle dedicated-server는 질문 자체를 피해 간다 — 막힌 백엔드가 곧 프로세스이므로 lock 대기는 공유 풀 slot이 아니라 프로세스 하나를 쓴다. MySQL thread pool은 stall을 감지해 group에 스레드를 추가 한다. SQL Server SOS는 막힌 worker의 스케줄러를 runnable 형제에게 양보 한다. CUBRID Phase 2는 MySQL과 SQL Server 사이에 자리한다 — SQL Server처럼 막힌 worker의 실행 권한 을 다른 작업에 넘기고, MySQL처럼 필요하면 스레드를 (max_request_worker까지) overcommit 한다. 다만 둘을 group-지역 스레드 생성이나 cooperative 스케줄러가 아니라 하나의 artifact — concurrency slot — 으로 표현한다. 결정적 차이는 어디에 연결하느냐다 — lock manager 안이 아니라(그건 손대지 않는다) lock manager·page-buffer latch·CSS job 큐가 공통으로 지나가는 thread_entry.cpp의 일반 스레드 suspension 원시연산에 연결한다. 실제로 slot을 반납하는 것은 뒤단 블로킹을 지배하는 두 사유 — 트랜잭션 lock 대기와 CSS job 큐 idle — 이고, 저장 프로시저/PL 경로는 명시적 반납으로 처리된다. 각 서브시스템에 흩뿌린 수정이 아니라 하나의 좁은 통합 지점인 셈이다.

Phase 1 — 동시성이 오를 때의 CPU idle (CBRD-26152 + CBRD-26177)

섹션 제목: “Phase 1 — 동시성이 오를 때의 CPU idle (CBRD-26152 + CBRD-26177)”

CBRD-26152 — [Survey] 동시성 증가에 따른 CPU idle 증가 원인 조사 — 이 재설계를 불러온 경험적 조사다. 홍예찬 님이 클라이언트/CAS cap을 2000으로 둔 채 YCSB workload-b (read 95%, update 5%) 를 돌리며 스레드 수를 200에서 1000까지 sweep 했다. 티켓에 그대로 적힌 예상 밖의 결과는 다음과 같다.

“스레드의 개수가 200개에서 1000개로 증가하였지만, 오히려 iowait가 아닌 CPU idle이 증가하고 있다.”

병목이 디스크였다면 스레드를 늘렸을 때 iowait 가 올라갔어야 한다. 부하가 늘었는데 idle 이 올라간다는 사실은 내부 동기화 를 가리킨다 — 스레드들이 worker-pool dispatch mutex 앞에 holder 가 놓는 속도보다 더 빨리 도착하고, 커널이 그것들을 park 시키는 바람에 코어가 실제로 idle 한 상태가 된다는 뜻이다.

CBRD-26177 은 구조적인 두 가지 원인을 짚는다.

“각 connection 스레드들이 모두 따로 polling하고 cub_server는 이론 상 max_clients × 2 이상의 thread를 가지게 되므로 자원 및 관리 관점에서 비효율적이다.”

“동시성이 점차 높아질수록 각각이 core의 mutex를 잡고 job을 할당 받으려고 하므로 이 contention은 CPU가 idle에 있게 하는 주요 병목 지점이 된다.”

여기서 이끌어낸 목표가 다음 다섯이다.

  1. connection-당 polling을 작고 크기가 제한된 epoll 구동 connection worker 집합으로 교체 — 과도한 poll() 호출 제거 (CBRD-26177 수락 기준 1).
  2. throughput을 동시성을 단조 로 만들기 — 클라이언트가 더 늘어났다고 처리율이 줄어들지 않게 (수락 기준 2).
  3. 각 worker 안에 admission 형태의 backpressure 추가 (CBRD-26392) — 굵은 connection 한 개가 동료들을 굶기지 못하게.
  4. 부하를 본 배치와 동적 크기 조정 추가 (CBRD-26406, CBRD-26407, CBRD-26424) — 엔진이 idle과 saturated 사이에서 스스로 튜닝 되도록.
  5. 모니터링 hot path에서 atomic 떼어 내기 (CBRD-26191).

CBRD-26177 은 이 모든 후속 티켓의 모양을 결정짓고 이 문서 전체 를 떠받치는 강한 directive 도 함께 내놓았다.

“connection worker는 상당히 동시성이 높은 hot-path이므로 perfmon 계열의 모니터링 코드를 추가해서는 안된다. 심각한 성능 저하를 일으킬 수 있다.”

소스를 읽을 때 가장 무겁게 머리에 새겨야 할 제약이 이것이다. worker tick 위에 글로벌 atomic 카운터 같은 것이나 perfmon_inc_stat() 류 호출이 들어가면 그 자체가 regression 이다.

Phase 2 — lean worker pool을 굶기는 lock 대기 (CBRD-26636 + CBRD-26662)

섹션 제목: “Phase 2 — lean worker pool을 굶기는 lock 대기 (CBRD-26636 + CBRD-26662)”

Phase 1이 앞단 병목을 없애자 뒤단의 두 번째 병목이 드러났다. CBRD-26636 — “[성능 실험] Worker 개수에 따른 성능 추이” — 은 task_worker 개수를 sweep 해, lean 풀(≈ 코어의 4–6배)이 task_worker = max_clients 보다 read-heavy YCSB에서 꾸준히 앞선다는 것을 확인했다. worker를 늘릴수록 critical-section 진입과 자원 경합 이 늘기 때문이다.

“각 Worker들은 내부적으로 잦은 Critical section 진입과 Resource contention을 가지고 있어 너무 많으면 성능 저하를 유발합니다. 반면, 너무 적은 개수로 설정되면 자원을 활용하지 못해 성능 저하를 일으키고, 잠금이나 다른 task 대기로 인해 작업이 밀려, 사용자 입장에서는 DBMS가 멈춘 것처럼 보일 수 있습니다.”

CBRD-26636은 lean 풀의 두 실패 모드를 구체적으로 짚는다.

  1. 무거운 task 하나(긴 트랜잭션 과는 다른, 작업 자체가 무겁고 긴 경우)가 worker를 차지해 실효 풀을 줄인다.
  2. lock 대기에 들어간 worker들이 돌고 있는 worker 수를 줄여 처리량이 “DB가 멈춘 것처럼 보이는” 영역으로 무너진다.

저장 프로시저 경로에는 같은 문제의 세 번째, 구조적 사례가 있다. PL/SP 메서드가 중첩 메서드를 호출하면 호출자 worker는 피호출자가 돌아올 때까지 막힌다. 중첩이 깊어지면 많은 worker가 중첩 호출에 park되어 풀 전체가 hang될 수 있다. legacy CUBRID은 이를 중첩 호출 마다 전용 temp worker 를 띄워 우회했다(CBRD-26683).

CBRD-26662 — “Logical-Wait-Aware Concurrency Control” — 은 이 셋을 하나의 아이디어로 친다. EPIC을 인용하면,

“Task worker가 논리적 대기로 인해 멈추게 되면 새 Task worker를 띄운다. 동시에 수행되는 Transaction (=Active한 Task worker)의 개수는 high_concurrency 이하여야 한다.”

활성 동시성을 lean하고 throughput-최적인 수준으로 유지하되, lock 대기가 돌 수 있는 worker 집합을 절대 줄이지 못하게 한다 — 막힌 worker는 권한을 반납하고, 새 worker나 idle worker가 대기 중인 작업을 집어 들며, SP 중첩용 temp worker 편법은 필요 없어진다(중첩 호출 대기도 이제 다른 논리적 대기처럼 slot을 반납하니까). (EPIC의 작업명 high_concurrency 는 구현에서 파라미터 max_request_concurrency 가 되었다 — 아래 Phase 2 참고.)

작업은 두 단계로 제시한다. Phase 1 (CBRD-26177)앞단 재설계 — 바이트가 소켓에서 task worker까지 어떻게 가는가 — 이며 guava 브랜치에서 완전히 resolved 되었다. Phase 2 (CBRD-26662)뒤단 재설계 — 한 번에 몇 개의 task worker가 돌 수 있고 하나가 논리적 대기에 막히면 어떻게 되는가 — 이며 PR #7323로 구현되어 develop 대상으로 open 되어 있다. Phase 2는 Phase 1의 모든 앞단 메커니즘을 그대로 두고, 고정 task_group/task_worker 뒤단 풀만 elastic·slot 제한 풀로 바꾼다.

CUBRID의 구현: Phase 1 — Connection/Worker 풀 재설계 (CBRD-26177)

섹션 제목: “CUBRID의 구현: Phase 1 — Connection/Worker 풀 재설계 (CBRD-26177)”

상태: Resolved (guava). 앞단 재설계.

Phase 1은 EPIC의 다이어그램 페이지가 그러는 것처럼 세 장의 그림 으로 이해하는 게 가장 깔끔하다 — AS-IS 베이스라인, CBRD-26212/26255 이후의 TO-BE 상태, 그리고 coordinator가 들어간 post-CBRD-26407 상태.

AS-IS (legacy). accept된 클라이언트마다 polling 스레드 하나 가 전담했다. polling 스레드는 매 iteration마다 max_clients 크기의 공유 cubthread::worker_pool 에 task를 push 했고, 그 push가 per-core mutex를 잡았다. polling 스레드가 수백 개에 이르 면 mutex는 모든 dispatch에서 경합 상태였다.

그림 1 — AS-IS 연결 처리 구조

그림 1 — AS-IS 연결 처리 구조. N개의 polling 스레드가 각자 fd를 poll하다가 요청이 오면 공유 per-core mutex를 경합하며 단일 task worker 풀로 디스패치한다. 클라이언트 수가 늘수록 mutex 경합이 모든 dispatch에서 발생한다.

TO-BE (CBRD-26212 + CBRD-26255). 작고 크기가 제한된 connection_worker 스레드 집합이 각각 edge-trigger I/O가 켜진 epoll_wait loop를 여러 클라이언트 소켓 위에 돌린다. 각 connection worker는 CPU 고정이다. 완성된 요청 한 개가 들어오면 connection worker가 백엔드 task 풀로 css_push_server_task 를 부른다. connection worker 수는 min_connection_worker/max_connection_worker 가 결정하고, task 풀은 task_group × task_worker 로 크기가 정해진다.

그림 2 — TO-BE epoll 연결 처리 구조

그림 2 — TO-BE epoll 연결 처리 구조 (CBRD-26212). 개수가 제한된 connection_worker M개가 여러 클라이언트 fd를 epoll로 다중화하고, 완성된 요청을 task_group × task_worker 풀로 디스패치한다. mutex 경합이 사라진다.

Post-CBRD-26407 (coordinator + freelist). core 0에 고정된 단일 coordinator 스레드가 배치 (새 클라이언트 → worker), 재배치 (load가 한쪽으로 쏠리면 worker 사이에서 connection 이동), 자동 스케일링 (worker를 min..max 안에서 hibernate/awaken) 을 모두 책임진다. worker들은 느린 타이머로 통계를 보내고, coordinator는 제어 메시지를 거꾸로 broadcast 한다. 각 worker 안에서는 context가 new/delete 가 아니라 풀 단위 freelist에서 나오고 들어간다.

그림 3 — coordinator + freelist 구조

그림 3 — coordinator + freelist 구조 (CBRD-26407). core 0에 고정된 coordinatorNEW_CLIENT / HANDOFF / HIBERNATE / AWAKEN 메시지로 worker를 제어하고, worker는 pool::freelist에서 context를 빌려 task worker 풀로 작업을 넘긴다. 외부 제어 소켓으로 SHOW_STATS / SCALE_UP / SCALE_DOWN이 가능하다.

connection worker는 connection_worker.{cpp,hpp} 안에 cubconn::connection::worker 로 구현되어 있다. 가지고 있는 자원 은 다음과 같다.

  • 리눅스 epoll 인스턴스 (cubsocket::epoll m_events).
  • 그 epoll에 등록되는 두 개의 fd — 스레드 간 wakeup용 eventfd (m_eventfd) 와 주기 작업 (hibernation 점검, 통계 push, HA close-all) 용 timerfd (m_timerfd).
  • worker별 메시지 큐 두 개 (IMMEDIATE, LAZY) — tbb::concurrent_queue<message> 와 atomic 크기 카운터로 구성된다.
  • worker가 소유 중인 context * 의 유지 중인 집합 (m_context) 과 지연 제거 큐 (m_removed_context).
  • 두 개의 예산 knob (m_recv_budget, m_send_budget) 와 exhausted context 맵 (m_exhausted).
  • coordinator로 자기 보고를 보내기 위한 atomic 없는 statistics::metrics<statistics::worker> m_stats.

그림 4 — connection worker 내부 구성

그림 4 — connection worker 내부 구성. CPU에 고정된 한 스레드가 cubsocket::epoll 리액터를 돌리며, 클라이언트 소켓·스레드 간 도어벨 m_eventfd·주기 작업용 m_timerfd가 모두 여기에 등록된다. ready fd는 handle_reception/handle_transmission을 거쳐 송수신 예산으로 제한되고, 예산을 초과한 fd는 m_exhausted에 담겨 다음 tick에 재처리되며, 완성된 요청은 css_push_server_task로 넘어간다. IMMEDIATE/LAZY 메시지 큐, 유지 중인/지연 제거 context 집합, atomic 없는 통계를 worker가 직접 소유한다.

생성자가 epoll을 엮고, eventfd/timerfd를 등록하고, 세 개의 timer 핸들러를 설치한 뒤 worker 스레드를 띄운다.

// worker::worker — src/connection/connection_worker.cpp
m_recv_budget = static_cast<size_t> (prm_get_integer_value (PRM_ID_CSS_RECV_BUDGET_PER_CONNECTION));
m_send_budget = static_cast<size_t> (prm_get_integer_value (PRM_ID_CSS_SEND_BUDGET_PER_CONNECTION));
m_exhausted.reserve (128);
m_eventfd = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC);
m_timerfd = timerfd_create (CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
// ... eventfd_register both into m_events ...
eventfd_addtimer (timer_type::HIBERNATE, timer_latency::MEDIUM_LATENCY, &worker::hibernate_check);
eventfd_addtimer (timer_type::STATISTICS, timer_latency::MEDIUM_LATENCY, &worker::statistics_metrics_to_coordinator);
eventfd_addtimer (timer_type::HA, timer_latency::HIGH_LATENCY, &worker::ha_close_all_connections);
m_thread = std::thread (&worker::attach, this);

worker::attach 가 스레드 진입점이고 initialize → run → finalize 를 차례로 부른다. initializeos::resources::cpu::setaffinity (m_core) 로 스레드를 자기 코어에 고정시키고, cubthread::entry 를 잡고, 스레드 이름을 connections 로 설정한다. 이 이름이 task pool 까지 새는 것을 CBRD-26617 가 잡았다.

메인 loop는 교과서적 reactor 모양이다.

// worker::run — src/connection/connection_worker.cpp
while (!m_stop)
{
nfds = m_events.wait (events.data (), events.size (),
m_exhausted.empty () ? TIMEOUT_INFINITE : TIMEOUT_NOWAIT);
// ...
for (i = 0; i < nfds; i++)
{
ctx = reinterpret_cast<context *> (events[i].data.ptr);
if ((events[i].events & (EPOLLHUP | EPOLLRDHUP | EPOLLERR)) && ...)
{
this->handle_hangup_or_error (ctx, events[i].events & EPOLLERR);
continue;
}
if (events[i].events & EPOLLIN)
{
if (ctx->m_conn->fd == m_eventfd) { eventfds[0] = true; continue; }
if (ctx->m_conn->fd == m_timerfd) { eventfds[1] = true; continue; }
status = this->handle_reception (ctx, false);
// ...
}
if (events[i].events & EPOLLOUT)
status = this->handle_transmission (ctx, false);
}
if (m_exhausted.size () > 0) handle_exhausted ();
if (eventfds[0] || eventfds[1]) eventfd_handler (eventfds);
}

타임아웃 분기가 흥미롭다 — exhausted context가 남아 있어 다시 돌려야 할 게 있으면 (Send/recv 예산 항을 보라) loop는 TIMEOUT_NOWAIT 로 polling 해서 즉시 그것들을 다시 보고, 그 외에는 epoll_wait 위에서 무한정 block 한다. eventfd는 단 하나 의 스레드 간 doorbell이다 — 외부 producer (coordinator, 다른 connection worker가 hand-off 하는 경우, task worker가 버퍼를 돌려 주는 경우) 가 m_eventfd 에 1을 쓰면 worker가 깨어 in-process 큐를 비운다.

connection::context (connection_context.hpp) 가 worker가 소유하는 클라이언트별 객체다. css_conn_entry *m_conn, worker 인덱스, 64비트 unique id, 수신 상태 머신 (HEADER → DATA → ERROR), receiver와 transmitter, 그리고 inline statistics::metrics<statistics::context> 를 담는다. 헤더와 선택적 데이터로 이루어진 완성된 요청 하나는 worker::handle_reception → handle_packet → handle_header_packet 또는 handle_data_packet 안에서 파싱되며, 백엔드 풀로의 task push 는 데이터가 따라붙지 않는 요청이면 handle_command_header_packet 에서, 데이터가 따라붙는 요청이면 데이터가 도착한 뒤 handle_data_packet 에서 일어난다.

// worker::push_task_into_worker_pool — src/connection/connection_worker.cpp
void worker::push_task_into_worker_pool (context *ctx)
{
/* push new task into worker pool */
css_push_server_task (*ctx->m_conn);
}

이 한 줄이 새 앞단과 legacy 뒷단 사이의 인터페이스 전부다. css_push_server_task (server_support.c 안) 가 connection을 css_server_task 로 감싸서 push_task_on_core (..., conn_ref.idx, conn_ref.in_method) 로 cubthread worker pool에 라우팅한다 — core 해시가 connection index 그대로다. legacy 설계와 정확히 같은 규칙이라, 오래 도는 세션은 같은 백엔드 코어 affinity를 유지 한다.

connection lifecycle (close 경로) 은 worker::handle_connection_close 가 구동한다. ctx->m_conn->cmutex 로 직렬화하고, net_server_active_workers 로 in-flight task worker를 다 비우게 만들고, 백엔드 worker가 아직 살아 있 으면 LAZY 큐에 SHUTDOWN_CLIENT 를 다시 넣어 재시도하며, 성공하면 epoll에서 fd를 빼고 context를 m_removed = true 로 표시해 m_removed_context 에 넣는다. 실제 context 반환은 purge_stale_contexts 가 미뤄 두었다가 한꺼번에 처리한다 — batch된 list 한 묶음을 단 한 번의 RETURN_TO_POOL 메시지로 coordinator에 보내는 것이라, freelist는 close 한 번이 아니라 loop tick 한 번에 한 번만 닿는다.

풀 (connection_pool.{cpp,hpp}cubconn::connection::pool) 이 worker, coordinator, context freelist의 소유자다. 서버 수명 동안 살아 있으며 cub_server 가 단일 인스턴스로 들고 있다.

freelist 자체는 pool::freelist 노드의 단일 연결 스택이다. 각 노드는 실제 context 를 첫 멤버로 임베드하므로 reinterpret_cast<freelist *> (ctx) 만으로 노드를 복원할 수 있다. 이 트릭이 legacy 의 connection 마다 new context 할당 패턴을 대체한다.

// pool::freelist — src/connection/connection_pool.hpp
struct freelist
{
/* THIS MUST BE THE FIRST */
context m_context;
freelist *m_next;
freelist (std::size_t capacity) : m_context (capacity), m_next (nullptr) {}
~freelist () = default;
};
// pool::claim_context / retire_context — src/connection/connection_pool.cpp
context *pool::claim_context ()
{
freelist *head;
assert (m_mutex_holder == std::this_thread::get_id ());
head = m_freelist.m_head;
if (head)
{
m_freelist.m_head = m_freelist.m_head->m_next;
}
else
{
head = new freelist (32 * 1024);
}
m_freelist.m_claim++;
return &head->m_context;
}
void pool::retire_context (context *ctx)
{
freelist *head;
// ...
head = reinterpret_cast<freelist *> (ctx);
head->m_context.reset ();
if (m_freelist.m_claim > m_freelist.m_max)
delete head; /* over-cap: actually free */
else
{
head->m_next = m_freelist.m_head;
m_freelist.m_head = head;
}
m_freelist.m_claim--;
}

그림 5 — context freelist 임베딩과 claim/retire

그림 5 — context freelist. pool::freelist 노드는 context 를 첫 멤버로 임베드하므로 reinterpret_cast<freelist *> (ctx) 만으로 할당 헤더 없이 노드를 복원한다. claim_context 는 스택 head를 pop하고(없으면 new), retire_context 는 reset 후 다시 push한다 — 유지 중인 개수가 cap을 넘으면 그대로 free한다. 둘 다 coordinator 위에서 pool::m_mutex 아래 단일 스레드로 도므로 context별 atomic이 전혀 필요 없다.

freelist는 오직 pool::m_mutex 를 잡고 있는 코드만 만진다. coordinator의 handle_message_queue_new_client (claim_context 호출) 와 handle_message_queue_return_to_pool (retire_context 호출) 둘 다 coordinator 스레드 위에서 돈다. coordinator가 자기 수명 내내 풀 lock을 쥐고 있기 때문이라는 점을 짚어 둔다 — coordinator::initialize → m_parent->lock_resource () 가 그 자리다. 이 결정 덕에 context 할당이 단일 스레드로 동작하면서도 context 별 atomic이 전혀 필요 없게 된다.

pool::initializepool::initialize_topology 를 거쳐 요청된 max_connection_workersos::resources::cpu::effective () 가 보고하는 실제 NUMA 코어 배치 위에 매핑하고, 가능한 환경 에서는 NIC RX/TX IRQ도 os::resources::net::map_nic_to_index (cores) 로 같은 코어들에 직렬화한다. CBRD-26255 가 이 NIC pinning까지 함께 들였는데, 티켓 코멘트에서 보고된 warning: NIC channel configuration failed 류 경고 로그의 출처가 이것이다 — non-fatal 이며, 바이너리에 CAP_NET_ADMIN 이 없거나 가상화 환경에서 실행될 때만 떠오른다.

shutdown 시퀀스는 thread_watcher (단순한 condvar + int active) 로 worker가 빠질 때마다 카운트다운하고, pool::finalize_workersm_watcher->active == 1 (coordinator만 남은 상태) 까지 css_get_shutdown_timeout() 만큼 기다린 다음, pool::finalize_coordinatoractive == 0 까지 다시 기다린다. 그 상태에 못 닿으면 try_to_lock_resource 의 10초짜리 try-lock loop 뒤에 _exit(0) 가 부수듯 호출된다 — 일부러 내는 hard exit이다. 대안이 “다른 어 떤 것도 정리해 줄 수 없는 상태를 들고 있는 스레드를 영원히 기다리기” 인 까닭이다.

예산 메커니즘이 이 설계에서 가장 미묘한 자리다. 이게 없으면 edge-trigger epoll + 끝까지 빨아 들이는 reader는 백로그가 쌓인 한 클라이언트가 자기 worker를 독점하게 두게 된다. EPOLLIN 이 한 번 fire되면 reader는 EAGAIN 까지 빨아 내야 할 의무가 있다 — 만약 peer가 계속 쓰고 있다면, 그 빨아 내기 loop는 영원히 끝나지 않는다. CBRD-26392 가 epoll tick 한 번당 빨아 내 는 양에 cap을 걸었다.

티켓이 그대로 적어 둔 의도다.

“하나의 connection worker는 여러 connection들을 관리한다. 이때 하나의 긴 송수신을 수행하게 되면 다른 송수신들이 계속 blocked되며 response가 지연되게 된다. 이때 한 번에 송수신할 수 있는 양을 제한하여 전체 지연을 안정화한다.”

기본값은 receive 16 KB, send 32 KB (system_parameter.c 참고) 다. 둘 다 0 (제한 없음) 부터 1 GB까지 설정 가능하다.

구현은 receiver::drain / transmitter::fill (둘 다 두 번째 인자가 size_t limit = 0 예산이다) 와 worker::handle_reception / worker::handle_transmission / worker::handle_exhausted_add_context / worker::handle_exhausted (connection_worker.cpp) 에 나뉘어 들어 있다.

// worker::handle_reception — src/connection/connection_worker.cpp
io_status = ctx->m_recv.m_receiver.drain (ctx->m_conn->fd, m_recv_budget);
if (io_status == result::PeerReset || io_status == result::Error) { /* close */ }
assert (io_status == result::Pending || io_status == result::BudgetExhausted);
if (!in_exhausted && io_status == result::BudgetExhausted)
{
handle_exhausted_add_context (ctx, EPOLLIN);
}
// worker::handle_transmission — src/connection/connection_worker.cpp
status = ctx->m_send.m_transmitter.fill (ctx->m_conn->fd, m_send_budget);
// ...
else if (!in_exhausted && status == result::BudgetExhausted)
{
handle_exhausted_add_context (ctx, EPOLLOUT);
}

context가 자기 예산을 다 쓰면 context id를 키로 m_exhausted 에 들어간다. 메인 loop는 exhausted 맵이 비어 있지 않은 것을 보고 epoll_wait 의 timeout을 TIMEOUT_NOWAIT 로 바꾼 다음, 현재 epoll batch를 다 돌린 후에 handle_exhausted 로 그것들을 다시 돌린다. exhausted_contextprepared 플래그가 그 뒤로 미루 는 가드다. context가 처음 추가될 때는 !prepared 로 표시되어 건너뛰며, 두 번째 방문에서야 worker가 다시 빨아 낸다. 덕분에 현재 epoll batch 안의 다른 ready fd들이 모두 서비스된 뒤에야 예산을 초과한 context가 다시 보인다.

한 fd에 대한 흐름 제어 유한 상태 머신은 다음과 같다.

stateDiagram-v2
  [*] --> Idle
  Idle --> Reading : EPOLLIN \n handle_reception
  Reading --> Idle : drain Pending \n EAGAIN
  Reading --> Exhausted : drain BudgetExhausted \n m_exhausted에 추가, EPOLLIN
  Exhausted --> Reading : 다음 loop에서 재방문 \n prepared 플래그
  Idle --> Writing : EPOLLOUT \n handle_transmission
  Writing --> Idle : fill Ok
  Writing --> Exhausted : fill BudgetExhausted \n m_exhausted에 추가, EPOLLOUT
  Reading --> Closing : ClosedConnection or PeerReset
  Writing --> Closing : ClosedConnection or PeerReset
  Closing --> [*] : handle_connection_close

그림 6 — fd별 흐름 제어 상태 머신. EPOLLIN/EPOLLOUT 이벤트로 Reading/Writing 상태에 들어가고, 예산 소진 시 Exhausted로 빠져 다음 epoll 루프에서 재방문된다. 연결 종료는 Closing에서 처리된다.

result::BudgetExhaustedresult::Pending 과 다른 enum 값 이라는 점은 짚어 둘 만하다 — Pending 은 “지금 커널이 더 줄 바이트가 없다” (다음 epoll edge까지 자연스럽게 back-off) 인 반 면, BudgetExhausted 는 더 줄 게 있는데 자발적으로 양보한다 (이번 loop나 다음 loop에 반드시 다시 와야 한다) 는 뜻이다.

CBRD-26406 가 connection 재배치와 worker 수 스케일링의 메커니 즘 을 깐다. 정책 자체는 CBRD-26424 (점수 기반 선택, 아래 참조) 에 산다. 메커니즘은 모양만 보면 단순하다 — worker가 1초 타이머로 통계를 보고하고, coordinator의 5초짜리 REBALANCING 타이머가 worker별 점수를 비교해 가장 무거운 worker가 자기 connection 하나를 가장 가벼운 쪽으로 hand-off 하게 시키며, coordinator의 60초짜리 SCALING 타이머가 자동 스케일링 상태 머신을 굴린다.

scaling_status enum은 두 상태뿐이다.

  • STABLE — 현재 카운트가 충분히 좋다, 측정 중 아님.
  • TRIALcount 개의 후보 크기를 sweep 하며 throughput 점수를 기록한 뒤 최댓값을 고른다.

SCALING tick마다 다음을 한다.

// coordinator::statistics_scaling — src/connection/coordinator.cpp
if (m_scaling_statistics.status == scaling_status::STABLE)
{ this->scale_trial (); return true; }
assert (m_scaling_statistics.status == scaling_status::TRIAL);
bytes_inout = 0;
for (i = 0; i < m_max_worker; i++)
{
bytes_inout += m_statistics[i].m_sum.get (statistics::context::BYTES_IN_TOTAL);
bytes_inout += m_statistics[i].m_sum.get (statistics::context::BYTES_OUT_TOTAL);
}
m_scaling_statistics.history.push_back (
{ m_current_worker,
VAL_TO_SCORE (50, 1000, bytes_inout) + m_task_statistics.completed.first * 2 });
m_scaling_statistics.count--;
if (m_scaling_statistics.count == 0)
{
selected = this->scale_selection (); /* pick max-score scale */
if (selected < m_current_worker) this->scale_down ();
else if (selected > m_current_worker) this->scale_up ();
/* else stable */
}
else
{
if (m_scaling_statistics.direction == scaling_direction::DOWN)
this->scale_down ();
else
this->scale_up ();
}

scale_trial 은 history를 비우고, 직전 trial과 반대 방향으로 trial 방향을 뒤집고 (연이은 trial이 같은 방향으로 흘러가지 않도록), countauto_scaling_window_size 파라미터 값으로 세팅한다 — 이 값이 trial 길이와 민감도 사이의 trade-off를 조절 하는 hyper-parameter다. 기본값 4는 한 trial이 결정 전까지 4개 샘플 (SCALING tick 한 번 = 60초이니 한 샘플 당 60초) 을 모은 다는 뜻이다.

슬라이딩 윈도우 메커니즘:

sequenceDiagram
  participant T as SCALING timer (60s)
  participant C as coordinator
  participant H as history (window_size = 4)

  Note over C: status = STABLE
  T->>C: tick
  C->>C: scale_trial()
  Note over C: direction = DOWN (or UP)<br/>count = 4<br/>status = TRIAL

  loop count = 4
    T->>C: tick
    C->>H: push_back({ current_worker, score })
    C->>C: scale_down() or scale_up()
  end

  T->>C: tick
  C->>H: push_back({ current_worker, score })
  C->>C: selected = scale_selection()
  alt selected != current
    C->>C: scale_down() or scale_up() to reach selected
  end
  Note over C: status = STABLE again

그림 7 — 자동 스케일링 슬라이딩 윈도우 시퀀스. SCALING 타이머(60s)가 scale_trial로 TRIAL 상태를 열고, window_size(기본 4)번 샘플을 쌓은 뒤 scale_selection으로 최적 worker 수를 골라 scale_up 또는 scale_down을 실행한다.

scale_selection 은 최대 점수의 95% 안에 들어오는 샘플 중에서 균등 분포로 하나를 고른다 — 평탄한 local 최댓값에 갇히지 않게 하는 작은 Boltzmann 류 randomization 이다 (CBRD-26424 가 작은 머신에서 관찰한 dual local maxima 코멘트를 참고하라).

scale_up 은 hibernating worker 중 다음 차례를 AWAKEN lazy 메시지로 깨우고 m_current_worker 를 하나 올린다. scale_down 은 두 단계로 반대 일을 한다 — scale_down 자체는 비워질 worker 의 모든 connection을 transfer_connection 으로 이동시키고 coordinator status를 DRAINING 으로 둔 다음, 실제 hibernation 은 scale_down_finish 가 한다. scale_down_finishhandle_message_queue_statistics 에서 비워질 worker가 빈 context list를 보고할 때만 호출된다. 이 두 단계 shutdown이 필요한 이유 는 worker shutdown이 비동기이고, draining 중인 worker가 아직 connection을 서비스하는 동안 statistics_find_score_extremes 가 그것을 다시 타깃 삼지 못하게 막아야 하기 때문이다.

Coordinator + context freelist (CBRD-26407)

섹션 제목: “Coordinator + context freelist (CBRD-26407)”

coordinator (coordinator.{cpp,hpp}cubconn::connection::coordinator) 는 모양만 보면 worker와 같은 형태다 — 고정 스레드, epoll 인스턴스, eventfd + timerfd, single-producer-single-consumer (TBB) 큐 — 단, 세 개의 다른 타이머와 외부 Unix-domain 제어 소켓을 추가로 들고 있다.

// coordinator::coordinator — src/connection/coordinator.cpp
m_controller.open ("/tmp/cub_server_" + std::to_string (getpid ()) + "_coordinator.sock",
SOCK_NONBLOCK | SOCK_CLOEXEC);
m_ctrlfd = m_controller.get_fd ();
m_eventfd = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC);
m_timerfd = timerfd_create (CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
eventfd_register (m_eventfd);
eventfd_register (m_timerfd);
eventfd_register (m_ctrlfd);
eventfd_addtimer (timer_type::STATISTICS, timer_latency::LOW_LATENCY, &coordinator::statistics_update);
eventfd_addtimer (timer_type::REBALANCING, timer_latency::MEDIUM_LATENCY, &coordinator::statistics_rebalancing);
eventfd_addtimer (timer_type::SCALING, timer_latency::HIGH_LATENCY, &coordinator::statistics_scaling);

세 timer latency는 각각 1초, 5초, 60초다 (coordinator.hpptimer_latency enum 참고).

제어 소켓이 다음 관리 명령들을 노출한다.

  • SHOW_STATS — worker별 EWMA throughput과 큐 깊이를 (statistics_print) stdout에 찍는다.
  • SCALE_UP / SCALE_DOWN — 자동 스케일링 상태 머신의 한 step을 강제한다.
  • CLIENT_MOVE — id로 지정한 connection 한 개를 worker from 에서 worker to 로 수동 이동시킨다.

이건 out-of-band 디버깅 인터페이스다. 데이터 경로의 어느 코드 도 이걸 쓰지 않는다. SOCK_DGRAM/SOCK_NONBLOCK 으로 control_recv 구조체를 보내면 한 바이트 (OK/NOK) 응답이 돌아온다. CBRD-26177 의 directive (hot path에 perfmon 금지) 때문에 표준 서버 채널을 통한 SHOW 류의 server-side 등가물은 없다 — controller는 일부러 side door 로 둔 것이지 성능 카운터가 아니다.

coordinator::handle_message_queue_new_client배치 정책이 사는 자리다. 재배치가 쓰는 EWMA 기반 점수 극값 함수와 같은 것을 부른다는 점에 주목하자.

// coordinator::handle_message_queue_new_client — src/connection/coordinator.cpp
std::tie (worker, std::ignore) = statistics_find_score_extremes ();
m_statistics[worker].m_contexts.emplace (id,
std::pair</*EWMA*/, /*prev*/>{ });
m_statistics[worker].m_client_num++;
request.type = connection::worker::message_type::NEW_CLIENT;
request.ctx = m_parent->claim_context ();
request.ctx->m_worker = worker;
request.ctx->m_id = id++;
request.conn = item.conn;
workers[worker]->enqueue (queue_type::IMMEDIATE, std::move (request));
workers[worker]->notify ();
this->statistics_update_score (worker);

— 즉 모든 새 클라이언트가 즉시 현재 점수가 가장 낮은 worker 로 라우팅되고, 그 점수는 그 자리에서 갱신되어 다음 배치를 편향시키게 된다.

context 이동 프로토콜 (재배치와 scale-down이 둘 다 쓴다) 은 coordinator와 두 worker 사이의 4단계 handshake 다.

sequenceDiagram
  participant C as coordinator
  participant Wf as worker[from]
  participant Wt as worker[to]

  C->>C: m_migrating.insert(id)
  C->>Wf: HANDOFF_CLIENT(id, worker_ptr=Wt, worker_index=to)
  Wf->>Wf: ctx 찾기, epoll/m_context에서 제거
  Wf->>Wt: TAKEOVER_CLIENT(ctx)
  Wt->>Wt: ctx를 epoll에 등록 (EPOLLIN | 필요 시 EPOLLOUT)
  Wf->>C: HANDOFF_REPLY(transferred=true, id, from, to)
  C->>C: m_migrating.erase(id)<br/>m_statistics[from/to] 보정

그림 8 — connection 재배치 4단계 핸드셰이크. coordinator가 m_migrating에 id를 등록하고 HANDOFF_CLIENT를 보내면, worker[from]이 context를 제거해 worker[to]로 넘기고, HANDOFF_REPLY로 coordinator의 통계를 보정한다.

m_migrating 이 한 connection을 in-flight 상태로 두 번 타깃 삼지 못하게 막는다. worker가 context가 이미 사라졌음을 발견 하면 (이동과 동시에 클라이언트가 close되었다면) 응답이 transferred=false 로 오고, coordinator가 예측해 둔 통계를 되돌린다. 이 설계가 의지하는 단 하나의 동시성 invariant 는 다 음과 같다 — 한 context는 어느 시점에든 오직 한 connection worker에만 소유되며, 소유권은 명시적 메시지로만 이전된다. context 자체에 lock은 필요 없다 — adapter 필드 갱신을 위해 잠깐 잡는 conn entry의 cmutex 만이 예외다.

context freelist (CBRD-26255 항에서 다룬 그것) 가 이 같은 티켓 에서 마무리되었다. CBRD-26407 설명이 목표를 직설적으로 적어 두었다.

“context는 생성마다 Physical Memory와 Virtual Memory를 할당받고 이를 mapping하므로 이 과정을 생략하도록 한다.”

max_connections * 1.1 만큼의 freelist 노드 (각각 32 KB 용량) 를 미리 워밍해 두면, 런타임 hot path는 mmap/페이지 폴트 시퀀스 가 아니라 포인터 한 번 갈아 끼우는 일이 된다.

점수 기반 connection 배치 (CBRD-26424)

섹션 제목: “점수 기반 connection 배치 (CBRD-26424)”

coordinator의 점수 함수는 세 신호를 worker별 비교 가능한 단일 스칼라로 묶는다.

// coordinator::statistics_update_score — src/connection/coordinator.cpp
m_statistics[worker].m_score =
1 * static_cast<double> (m_statistics[worker].m_client_num) / 1
+ EVAL_WORKER (EWMA(MQ_COMPLETED), EWMA(BLOCKED_RMUTEX))
+ EVAL_CONTEXT (EWMA(BYTES_IN_TOTAL) + EWMA(BYTES_OUT_TOTAL),
EWMA(RECV_BUDGET_HIT) + EWMA(SEND_BUDGET_HIT));

가중치 매크로는 다음과 같다.

#define VAL_TO_SCORE(w, m, s) ((w) * static_cast<double> (s) / (m))
#define EVAL_WORKER(mq, rmutex) (VAL_TO_SCORE (25, 3.5, (mq)) + VAL_TO_SCORE (500, 1, (rmutex)))
#define EVAL_CONTEXT(bytes, bgt) (VAL_TO_SCORE (50, 1000, (bytes)) + VAL_TO_SCORE (10, 1, (bgt)))

구체적으로 가중치가 의미하는 바는 — bytes-of-traffic은 50 × 1/1000 (≈ KB 당 1 unit), rmutex로 막힌 마이크로초는 500 × 1 (≈ μs 당 500 units), MQ completion 은 25 × 1/3.5 (≈ completion 당 7 units) 다. 예산 hit (recv/send 예산 cap을 친 context) 이벤 트는 10이다 — 예산 hit이 잦다는 것은 worker가 admission cap에 거듭 부딪히고 있다는 뜻이고, 부담을 나눠 갈 동료가 더 있으면 도움이 된다는 신호이기 때문이다. CBRD-26424 코멘트가 측정된 throughput 곡선의 dual local maxima를 설명한다 — 작은 머신에서는 NUMA / RX-TX / HT-sibling 상호작용 때문에 worker 수와 throughput 사이가 단조 관계가 아니고, 순진한 hill-climber 는 거기 갇힌다. scale_selection 의 randomized top-5% 선택이 그 탈출구다.

EWMA 집계는 α = 0.06 (EWMA_ALPHA) 을 쓴다.

// coordinator::statistics_EWMA — src/connection/coordinator.hpp
acc = acc * (1 - alpha) + (current - prev) * (alpha / (time_delta * 1e-6));
prev = current;

time_delta * 1e-6 으로 나눠 마이크로초 단위로 정규화하므로 EWMA는 raw delta가 아니라 smooth된 rate (마이크로초당 이벤트 수) 다. α = 0.06 에 1초 샘플링 간격이면 effective half-life가 대략 11 샘플 (≈ 11초) 이고, 약 1분이면 노화된 샘플의 기여가 1% 미만으로 떨어진다.

statistics::metrics<T, VT = uint64_t> 템플릿 (connection_statistics.hpp) 은 고정 크기 VT[STATS_COUNT] 이며 add / sub / get / set / reset 연산을 제공한다. 어디에도 std::atomic 이 없다 — 모든 증가가 평범한 메모리 write 다. 모든 증가가 정확히 한 스레드 (메트릭을 소유한 worker) 에서만 일어나기 때문이다. worker 간 집계는 1초에 한 번, worker가 자기 메트릭 블록을 coordinator::message::statistics 페이로드로 복사할 때만 일어나며, coordinator가 자기 단일 스레드 핸들러 안에서 worker별 EWMA 갱신을 한다.

// worker::statistics_metrics_to_coordinator — src/connection/connection_worker.cpp
message.type = coordinator::message_type::STATISTICS;
message.statistics.cpu_time_ns = get_time_ns (CLOCK_THREAD_CPUTIME_ID);
message.statistics.time_ns = get_time_ns (CLOCK_MONOTONIC);
message.statistics.worker.first = m_index;
message.statistics.worker.second = m_stats; /* copy */
message.statistics.contexts.reserve (m_context.size ());
for (context *ctx : m_context)
message.statistics.contexts.emplace_back (ctx->m_id, ctx->m_stats); /* copy */
m_coordinator->enqueue (std::move (message));

bulk copy가 비싼 일이 아닌 이유는 — m_stats 가 고정 배열 (≈ 88 바이트) 이고, context별 배열은 길어야 수백 entry × 56 바이트 정도이기 때문이다. copy는 worker가 동시에 쓰고 있는 cache line을 가로지르지 않으면서 single-producer-single-consumer 큐를 지나 소유권을 옮긴다. 결정적으로, 이 설계는 CBRD-26177 directive (hot path에 perfmon 금지) 를 지키기 위해 존재한다 — worker는 dispatch loop 안에서 공유 카운터를 증가시키지 않고, lock 위에서 spin 하지 않으며, memory barrier도 실행하지 않는다.

CBRD-26191 가 더 큰 목표 — 서버 전반 모니터링에서 atomic 빼 기 — 를 YCSB 위에서 측정해 두었다.

워크로드적용 전적용 후향상
workloada58 464.2860 646.59+3.7%
workloadb70 009.9972 976.31+4.2%
update44 158.6645 128.96+2.2%
mix9 440.8210 115.33+7.1%

connection 측 메트릭 설계는 같은 템플릿을 새 layer에 그대로 가져왔다.

CBRD-26177 이 약속한 세 개의 새 per-socket keepalive 파라미터 는 — tcp_keepalive_idle (idle 후 N초가 지나면 probe 시작), tcp_keepalive_interval (probe 간 간격), tcp_keepalive_count (연속 실패 = dead) — 다. 기본값은 300초 / 300초 / 3이고, 상한 은 1년치 초로 잡혀 있다. 이미 있던 tcp_keepalive 불리언과 함께 system_parameter.c 에 등록되며, 실제 적용은 socket-setup helper (tcp.c::css_sockopt) 가 맡는다. 이 helper는 tcp_keepalive 가 켜져 있을 때 이미 setsockopt (SOL_SOCKET, SO_KEEPALIVE, ...) 를 부르고 있었고, 새 세 knob가 각각 TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT 로 들어가서 dead-peer 감지를 세밀하게 튜닝하게 해 준다. CUBRIDMAN-333 매뉴얼 갱신이 문서 측 롤아웃을 다룬다.

Task worker 풀 크기 조절 — task_group / task_worker (잠정, Phase 2가 대체)

섹션 제목: “Task worker 풀 크기 조절 — task_group / task_worker (잠정, Phase 2가 대체)”

잠정 메커니즘. 아래 task_group / task_worker 크기 조절은 Phase 1의 뒤단 튜닝 knob이며 CBRD-26177 머지와 함께 실렸다. Phase 2(CBRD-26662)가 이를 은퇴시킨다 — task_workermax_request_worker 로 rename, task_group 은 deprecated no-op 으로 강등, 동시에 도는 트랜잭션 상한은 새 max_request_concurrency 로 옮겨 간다. 이 절은 머지 당시 Phase 1 동작으로 읽고, 무엇이 대체하는지는 아래 Phase 2를 보라.

Phase 1의 뒤단 풀은 thread_worker_pool_impl.{hpp,cpp}cubthread::worker_pool 이다. 그 크기는 두 파라미터로 정해지며, 이 둘이 옛 thread_core_count / thread_worker_count 짝을 대체한다.

  • task_group (옛 thread_core_count) — worker pool 안의 core 수. CUBRID 용어에서 한 core 는 자기 큐를 가지는 하위 풀이며 worker_pool::core 가 소유한다.
  • task_worker — 모든 group 합산 총 worker 스레드 수. 서버 기동 시 기본값은 css_get_max_connections () (즉 사실상 옛 max_clients) 이며, 시스템 코어 수를 넘기면 그 만큼 normalize 되어 깎인다.

자동 튜닝 코드가 task_group ≤ 시스템 코어 수, task_grouptask_worker 로 clamp 한다 (system_parameter.c boot sysprm 튜닝 블록).

/* sysprm_tune_client_parameters — src/base/system_parameter.c */
task_worker_prm = GET_PRM (PRM_ID_TASK_WORKER);
if (PRM_GET_INT (task_worker_prm->value) < 0)
{
/* the value of task worker is default. */
sprintf (newval, "%d", task_worker); /* css_get_max_connections() */
(void) prm_set (task_worker_prm, newval, false);
}
task_group_prm = GET_PRM (PRM_ID_TASK_GROUP);
if (PRM_GET_INT (task_group_prm->value) > system_cpu_count)
{
sprintf (newval, "%d", system_cpu_count);
(void) prm_set (task_group_prm, newval, false);
}
if (PRM_GET_INT (task_group_prm->value) > PRM_GET_INT (task_worker_prm->value))
{
sprintf (newval, "%d", PRM_GET_INT (task_worker_prm->value));
(void) prm_set (task_group_prm, newval, false);
}

의미 변화는 — task_worker 가 이제 총 worker 예산 으로 해석되고, task_group분할 을 담당한다는 것이다. 옛 thread_core_count 는 코어 수 로 느슨하게 쓰였을 뿐 정책적 의도는 흐릿했다. 새 이름이 의도를 또렷이 박아 두며, coordinator의 task-completion EWMA (m_task_statistics.completed) 가 server_support.c 의 css_get_task_stats 로 풀의 누적 카운터 를 읽어 점수에 반영한다.

CBRD-26636 ([성능 실험] Worker 개수에 따른 성능 추이) 이 read-heavy YCSB 워크로드에서 task_worker ≈ 4–6 × corestask_worker = max_clients 보다 일관되게 우월함을 보였다 — 단, task_worker < max_clients 일 때 많은 worker가 긴 lock 위에서 대기하면 deadlock 위험이 있다. 그 위험이 CBRD-26662 의 동기다 (아래 Phase 2 참고).

CUBRID의 구현: Phase 2 — 논리적 대기 인지 동시성 제어 (CBRD-26662)

섹션 제목: “CUBRID의 구현: Phase 2 — 논리적 대기 인지 동시성 제어 (CBRD-26662)”

상태: PR #7323(feature/worker_pool_elastic, 21 커밋, 45 파일, +4054/−801)로 구현, develop 대상 open. 머지 전 테스트케이스 대기 중. 뒤단 재설계.

Phase 2는 고정 task_group × task_worker 요청 풀을, 실제 동시성을 고정 스레드 수가 아니라 slot 풀로 다스리는 elastic worker pool 로 교체한다. 위 이론이 그대로 구현된다 — worker는 돌기 위해 slot을 쥐어야 하고, slot 수는 max_request_concurrency 로 묶이며, 논리적 대기에 들어간 worker는 slot을 반납 해 다른 작업이 그 자리에서 돌게 한다. 스레드는 max_request_worker 까지 overcommit된다.

가장 중요한 구조적 사실 — 그리고 이 변경이 “lock manager 재작성” 보다 훨씬 작은 이유 — 은 lock manager·page-buffer·CSS 큐를 손대지 않는다 는 점이다. 셋은 이미 thread_entry.cpp 의 같은 원시연산 으로 스레드를 suspend 하며, Phase 2는 slot 반납/재획득을 거기에, 기존 suspended_reason 을 키로 끼워 넣는다. 하나의 통합 지점이 논리적 대기를 담당한다.

티켓 문구와 실제 코드가 갈리는 세 가지 이름 정정:

  • EPIC의 high_concurrency 와 서브태스크 CBRD-26688의 max_transaction_concurrency 는 코드에서 max_request_concurrency 가 되었고, worker 상한 max_transaction_workermax_request_worker 가 되었다.
  • CBRD-26688은 “task_group task_worker 제거”라 하지만, 코드에서 실제로 제거된 것은 task_worker(→ max_request_worker 로 rename)뿐이고 task_group 은 deprecated no-op 으로 남는다 (PRM_DEPRECATED, 값 1 고정) — 기존 cubrid.conf 가 깨지지 않게.
  • CBRD-26688의 제약식 max_transaction_worker <= max_transaction_concurrency 는 뒤집혀 있고, 코드는 상식적인 방향 max_request_worker >= max_request_concurrency 를 강제 한다 (worker가 활성 상한 위의 overcommit 한도).

elastic 풀은 thread_worker_pool_elastic.hppcubthread::worker_pool_elastic<Stats> 로, 재정비된 worker_pool_impl<Stats> 의 하위 클래스다. Phase 1 구조(코어마다 core_elastic 하나, 각자 task 큐)를 유지하되 각 core가 concurrency_slot_pool m_slots 를 함께 소유하고, 두 크기 수치가 풀 전역 atomic이 된다.

// worker_pool_elastic / core_elastic — src/thread/thread_worker_pool_elastic.hpp
std::atomic<std::size_t> &m_current_worker; // shared across all cores
std::atomic<std::size_t> &m_max_worker; // global overcommit cap
// global. this is not hard cap. allow to make worker overcommit

worker 카운터가 (참조로 모든 core_elastic 이 공유하는) 전역 이라는 점은 같은 PR에 실린 CBRD-26977 하드닝 변경으로, max_request_worker 를 코어별이 아니라 서버 전체 상한으로 만든다.

dispatch는 slot이 게이트한다. 작업이 들어오거나 slot이 비면 core_elastic::adjust_workers 가 가용 slot이 허락하는 만큼만 큐를 비운다.

// core_elastic::adjust_workers — src/thread/thread_worker_pool_elastic.hpp
while (!m_task_queue.empty () && m_slots.available_slots () > 0)
{
worker_elastic *w = get_or_make_available_worker (); // reuse idle, else spawn
if (w == nullptr) break; // hit m_max_worker
std::unique_ptr<concurrency_slot> slot = m_slots.try_acquire_slot (ulock);
// pop one task, hand it the slot
w->try_execute_task_with_slot (std::move (task), std::move (slot));
}

get_or_make_available_worker 는 idle worker가 있으면 재사용하고, 없으면 reserve_available_worker()(전역 m_max_worker 에 묶인 CAS) 후 allocate_worker()new worker_elastic()start_thread() 한다. 새 worker 스레드가 태어나는 유일한 지점 이며, 대기 작업이 있고 slot이 비었는데 돌릴 idle worker가 없을 때 — 즉 돌던 worker가 논리적 대기에 slot을 내준 순간 — 벌어진다.

worker_elastic 은 task 수행 동안 slot을 스레드 entry로 옮겼다가 끝나면 되돌린다.

// worker_elastic::execute_current_task — src/thread/thread_worker_pool_elastic.hpp
m_context_p->m_slot = std::move (m_slot); // entry now holds the slot
m_wrapped_task->execute (*m_context_p); // run the task
// ... on completion, the slot is returned to its pool ...
m_slot = std::move (m_context_p->m_slot);
concurrency_slot::return_to_pool (std::move (m_slot)); // return_slot drives this on the completion path

worker는 풀이 줄었을 때 스스로 은퇴한다 — worker_elastic::run 은 task마다 get_retire_if_excess(this) 를 불러, m_workers.size () > m_max_concurrency 이고 worker 리스트를 훑는 snapshot reader가 없을 때 worker를 정리한다.

그림 9 — Elastic 요청 풀

그림 9 — Elastic 요청 풀. 각 core_elastic 은 task 큐, concurrency_slot_pool, 가변 worker_elastic 집합을 소유한다. worker는 돌기 위해 slot을 얻어야 하며, slot 수가 활성 동시성을 묶는 한편 스레드는 풀 전역 m_max_worker 까지 overcommit된다. 50 ms slot daemon이 코어 간 slot을 재분배한다.

Concurrency slot과 core별 slot pool (CBRD-26685 / 26689)

섹션 제목: “Concurrency slot과 core별 slot pool (CBRD-26685 / 26689)”

concurrency_slot(concurrency_slot.hpp)은 이동 가능한 토큰이다. 자신을 만든 코어와 지금 쓰는 코어를 기억하고(slot은 코어 간 이동한다), soft-wait 플래그와 타임스탬프를 가진다.

// concurrency_slot — src/thread/concurrency_slot.hpp
class concurrency_slot
{
concurrency_slot_pool *const m_owner_pool; // core that created the slot
concurrency_slot_pool *m_holder_pool; // core currently using it
bool m_wait; // soft-wait flag
std::chrono::steady_clock::time_point m_wait_since;
public:
void reset ();
void start_waiting ();
void stop_waiting ();
bool has_wait_expired (now) const; // threshold: 50 ms
static void return_to_pool (std::unique_ptr<concurrency_slot> slot);
};

core별 풀 concurrency_slot_pool소유 코어의 mutex로 보호된다 (별도 slot lock 없이 코어 lock에 얹혀, slot 획득과 task pop이 한 critical section이 된다).

// concurrency_slot_pool — src/thread/concurrency_slot.hpp
class concurrency_slot_pool : public concurrency_slot_subscriber
{
std::mutex *m_mutex; // the core mutex
std::queue<std::unique_ptr<concurrency_slot>> m_available_slots;
std::list<cubthread::entry *> m_wait_queue; // threads waiting for a slot
std::size_t m_slot_count; // slots this core owns
std::size_t m_target_count; // desired count
bool m_surplus;
std::chrono::steady_clock::time_point m_surplus_since;
static constexpr std::size_t SLOT_SURPLUS_THRESHOLD = 2;
};

slot 관리는 전적으로 core별이며, task가 코어에 고르게 분산된다는 가정(CBRD-26685의 근거) 하에 core별 상한이 전역 상한을 근사한다. 핵심 연산:

  • try_acquire_slot — 논블로킹; m_available_slots 에서 pop 하거나 nullptr.

  • acquire_slot(entry *thread_p)블로킹. slot이 있으면 thread_p->m_slot 에 배정하고, 없으면 thread_pm_wait_queue 에 넣고 resume 상태를 THREAD_CONCURRENCY_SLOT_SUSPENDED 로 두고 slot이 넘어올 때까지 pthread_cond_wait 한다. slot 대기는 lock 대기 안에 중첩될 수 있으므로 원래 resume 상태를 저장/복원해 lock의 깨우기 결과를 덮지 않게 한다.

    // concurrency_slot_pool::acquire_slot — src/thread/concurrency_slot.cpp (condensed)
    if (!m_available_slots.empty ())
    {
    thread_p->m_slot = std::move (m_available_slots.front ());
    m_available_slots.pop ();
    thread_p->m_slot->set_holder_pool (this);
    return;
    }
    m_wait_queue.push_back (thread_p);
    auto saved = thread_p->resume_status; // nested wait guard
    thread_p->resume_status = THREAD_CONCURRENCY_SLOT_SUSPENDED;
    while (thread_p->resume_status == THREAD_CONCURRENCY_SLOT_SUSPENDED)
    pthread_cond_wait (&thread_p->wakeup_cond, &thread_p->th_entry_lock);
    thread_p->resume_status = saved;
  • release_slot(slot) — slot을 reset() 하고, 이 코어가 소유하며 목표를 넘었으면 파괴, 아니면 m_available_slots 에 넣거나, 대기 스레드가 있으면 맨 앞 대기자에게 바로 넘기고 thread_wakeup_already_had_mutex (waiter, THREAD_CONCURRENCY_SLOT_RESUMED) 로 깨운다.

  • return_slot(slot, force) — 완료 경로. 대기 작업/대기자/목표 초과/force 가 있으면 풀로 되돌리고, 아니면 concurrency_slot:: return_to_pool 을 통해 소유 코어 쪽으로 정산한다.

  • get_score() = `-available_slots.size () + wait_queue.size ()

    • 2.0f` — daemon이 정렬하는 수요 순위(높을수록 더 급함).
  • get_runtime_stats(total, target, busy) — statdump 공급; busy = m_slot_count − m_available_slots.size ().

그림 10 — core별 concurrency slot 풀

그림 10 — core별 concurrency slot 풀. m_available_slots 는 비어 있는 slot 토큰을, m_wait_queue 는 slot을 기다리며 THREAD_CONCURRENCY_SLOT_SUSPENDED 로 suspend된 스레드를 담는다. 각 concurrency_slot 은 자신을 만든 코어(m_owner_pool)와 지금 쓰는 코어(m_holder_pool)를 기록해 코어 간 이동하며, get_score() 가 daemon을 위한 코어 수요를 순위 매긴다. 모두 코어 m_mutex 아래서 돈다.

stateDiagram-v2
  [*] --> Available : pool initialize \n max_request_concurrency 만큼
  Available --> Running : try_acquire_slot \n worker가 task 획득
  Running --> Available : task 완료 \n return_slot
  Running --> Available : THREAD_CSS_QUEUE_SUSPENDED \n idle worker 즉시 반납
  Running --> SoftWait : THREAD_LOCK_SUSPENDED \n start_waiting, slot 보유
  SoftWait --> Running : 50 ms 내 lock 획득 \n stop_waiting
  SoftWait --> Reclaimed : daemon이 has_wait_expired 확인 \n steal_from_entries_if_excess
  Reclaimed --> Available : distribute_slots 로 수요 높은 코어에

그림 11 — Slot 수명. slot은 core별 풀에서 Available, worker 수행 중 Running, lock suspension 시 SoftWait에 든다. 50 ms 안에 lock을 얻으면 worker가 slot을 유지하고, 아니면 daemon이 회수해 재분배한다. CSS 큐 idle은 즉시 반납한다. slot을 회수당한 worker는 재개 전에 slot을 다시 얻어야 한다(그림 12).

Slot daemon — soft/hard 대기와 core 간 이관 (CBRD-26686 / 26691)

섹션 제목: “Slot daemon — soft/hard 대기와 core 간 이관 (CBRD-26686 / 26691)”

재분배자는 단일 daemon concurrency_slot_daemon 으로, 50 ms looper에 이름 "concurrency" 로 등록된다.

concurrency_slot.cpp
REGISTER_DAEMON (concurrency_slot_daemon);
looper loop = looper (std::chrono::milliseconds (50));
cubthread::get_manager ()->create_daemon (loop, new concurrency_slot_daemon_task (), "concurrency");

execute 는 등록된 모든 core 풀에 대해 다섯 단계를 돈다(코어는 publisher/subscriber 쌍 concurrency_slot_publisher / concurrency_slot_subscriber 로 daemon에 자신을 등록한다).

// concurrency_slot_daemon_task::execute — src/thread/concurrency_slot.cpp (steps)
1. check_and_propagate_parameters (); // 런타임 파라미터 반영
2. steal_from_entries_if_excess (slots, identifier); // 만료된 lock 대기자에게서 회수
3. steal_from_cores_if_excess (slots, subs); // 코어별 잉여 회수
4. distribute_slots (slots, subs); // 가장 수요 높은 코어에 지급
5. wakeup_workers (identifier, subs); // core_elastic::adjust_workers

soft / hard 대기 구분은 2단계와 suspension 코드에 있다. worker가 lock에 막히면 slot을 즉시 반납하지 않고(soft 대기) 대기 시작 시각만 기록한다. 대기가 50 ms 문턱을 넘겨야 daemon이 slot을 뺏어 hard 대기로 승격한다.

// steal_from_entries_if_excess — via pool->map_running_contexts (func)
if (thread_ref.m_status == cubthread::entry::status::TS_WAIT
&& thread_ref.m_slot != nullptr
&& thread_ref.m_slot->has_wait_expired (now)) // held > 50 ms
{
steal thread_ref.m_slot; // hard wait: reclaim
}

이 2단계 방식은 churn을 막는다 — 빨리 풀리는 lock 대기(흔한 경우)는 daemon을 건드리지도, slot을 내주지도 않으니 짧은 대기는 공짜이고, 진짜 긴 대기만 다른 작업에 용량을 내준다.

3단계가 core 간 slot 이관 을 맡는다. 가용 slot을 SLOT_SURPLUS_THRESHOLD(2)개 이상 2000 ms 넘게 들고 있던 코어가 잉여를 내놓는다.

// concurrency_slot_pool::borrow_surplus_slots — src/thread/concurrency_slot.cpp
if (m_surplus
&& now - m_surplus_since > std::chrono::milliseconds (2000)
&& m_available_slots.size () >= SLOT_SURPLUS_THRESHOLD)
{
hand back the surplus slots; // returned to owners / hungry cores
}

4단계는 풀을 get_score()(수요 내림차순)로 정렬해 회수한 slot을 가장 수요 높은 코어에 라운드로빈으로 준다. 빌린 slot은 작업이 끝나면 slot의 m_owner_pool / m_holder_pool 장부를 통해 소유 코어로 돌아간다.

sequenceDiagram
  participant W as worker (entry)
  participant SP as core slot pool
  participant D as slot daemon (50 ms)
  participant PEER as 수요 높은 코어

  W->>SP: lock에 막힘 → THREAD_LOCK_SUSPENDED
  W->>W: thread_prepare_suspension<br/>start_waiting() — soft 대기, slot 보유
  W->>W: pthread_cond_wait (suspended)
  Note over D: 매 50 ms
  D->>W: has_wait_expired(50 ms)? yes<br/>m_slot 회수 (hard 대기)
  D->>PEER: distribute_slots → release_slot
  PEER->>PEER: 대기자 깨우기 / worker 생성
  Note over W: 이후 lock 획득
  W->>W: thread_prepare_resumption<br/>slot이 회수됨
  W->>SP: acquire_slot (blocking) — slot_waits에 집계
  SP-->>W: slot 지급 → 재개

그림 12 — Lock 대기 slot 인계. lock에 막힌 worker는 최대 50 ms 동안 slot을 유지한다(soft 대기). daemon이 만료를 확인하면 slot을 회수(hard 대기)해 수요 높은 코어에 재분배하고, 그 코어가 worker를 돌리거나 깨운다. 원래 worker는 마침내 lock을 얻으면 재개 전에 slot을 다시 얻어야 하며, 그 재획득 시간은 event_stats.slot_waits 에 집계된다.

논리적 대기 연결 — suspension 계층, lock, SP (CBRD-26687 / 26683)

섹션 제목: “논리적 대기 연결 — suspension 계층, lock, SP (CBRD-26687 / 26683)”

통합 지점은 src/thread/thread_entry.cpp 다. 기존 진입점 thread_suspend_wakeup_and_unlock_entrythread_suspend_timeout_wakeup_and_unlock_entry 가 두 static 헬퍼 thread_prepare_suspension / thread_prepare_resumption 을 거치도록 재정비되었고, slot 로직이 거기 산다. suspend 시 기존 suspended_reason 으로 분기한다.

// thread_prepare_suspension — src/thread/thread_entry.cpp (condensed)
if (thread_p->m_slot != nullptr)
switch (suspended_reason)
{
case THREAD_CSS_QUEUE_SUSPENDED: // worker idling on the job queue
holder = thread_p->m_slot->get_holder_pool ();
slot = std::move (thread_p->m_slot);
thread_p->m_slot = nullptr;
concurrency_slot::return_to_pool (std::move (slot)); // release immediately
break;
case THREAD_LOCK_SUSPENDED: // waiting to acquire a transaction lock
holder = thread_p->m_slot->get_holder_pool ();
thread_p->start_waiting (); // soft wait; daemon may reclaim after 50 ms
break;
}

resumeholder 가 기록돼 있으면(사유가 CSS 큐 또는 lock suspension이라는 assert 포함) worker의 운명은 slot이 살아남았는지에 달렸다.

// thread_prepare_resumption — src/thread/thread_entry.cpp (condensed)
if (timedout || resume_status == THREAD_RESUME_DUE_TO_INTERRUPT
|| resume_status == THREAD_RESUME_DUE_TO_SHUTDOWN)
{
if (thread_p->m_slot) release_slot (std::move (thread_p->m_slot)); // giving up
}
else if (thread_p->m_slot != nullptr)
{
thread_p->stop_waiting (); // wait resolved < 50 ms; slot never stolen
}
else
{
// slot was stolen by the daemon — must re-acquire, blocking
static_cast<concurrency_slot_pool *> (holder)->acquire_slot (thread_p);
// wait time charged to thread_p->event_stats.slot_waits
}

이를 받치는 두 사실. 첫째, thread_entry.hpp 에 새 resume 상태 THREAD_CONCURRENCY_SLOT_SUSPENDED = 25 / THREAD_CONCURRENCY_SLOT_RESUMED = 26 과, std::unique_ptr< cubthread::concurrency_slot> m_slot(“elastic worker pool의 worker 에만 설정”), event_statstruct timeval slot_waits 가 추가 됐다. 둘째, network_sr.cnet_server_wakeup_workers 의 깨우기 가능 switch에 THREAD_CONCURRENCY_SLOT_SUSPENDED 를 더해, slot- suspend된 worker도 다른 것처럼 interrupt될 수 있다.

저장 프로시저 (CBRD-26687 / temp worker 제거 CBRD-26683). SP 경로는 명시적(비-suspension) 논리적 대기다 — PL 메서드가 중첩 메서드를 호출하면 PL/Java 서버로부터의 수신에 막힌다. legacy는 중첩 호출마다 temp worker 를 띄워 풀 park를 피했지만, Phase 2는 그 메커니즘을 통째로 지운다(execute_on_core(task, core_hash, bool is_temp)execute_on_core(task, core_hash), execute_task(task, is_temp)execute_task(task)). 대신 SP 호출이 블로킹 수신 주위에서 slot을 반납하고 뒤에 재획득한다 — thread_manager.hpp 의 명시적 헬퍼 thread_concurrency_slot_release / thread_concurrency_slot_acquire 사용.

// pl_execution_stack_context.hpp — around a blocking PL receive
auto *holder = thread_concurrency_slot_release (m_thread_p); // give up slot now
int error = conn->receive_buffer (b, &interrupt_func, 500); // wait on PL server
thread_concurrency_slot_acquire (m_thread_p, holder); // reacquire to resume

lock 경로와의 차이는 반납 시점 이다 — lock 대기는 soft 대기 (slot 유지, 길어지면 daemon이 회수)인데 대부분의 lock 대기는 짧기 때문이고, 중첩 SP 호출은 애초에 다른 task를 기다린다는 게 확실하니 slot을 즉시 반납한다. 둘 다 같은 concurrency_slot_pool 로 모인다.

파라미터: max_request_concurrency / max_request_worker (CBRD-26688 / 26690 / 26977)

섹션 제목: “파라미터: max_request_concurrency / max_request_worker (CBRD-26688 / 26690 / 26977)”

task_workermax_request_worker 로 rename, task_group 은 no-op으로 deprecated, max_request_concurrency 는 신규다. 두 새 파라미터는 PRM_FOR_SERVER | PRM_USER_CHANGE 로, SET SYSTEM PARAMETERS 로 런타임 변경 가능하다.

파라미터Enum id기본값(server)범위런타임의미
max_request_concurrencyPRM_ID_MAX_REQUEST_CONCURRENCYsystem_core_count() * 3[core*3 … CSS_MAX_CLIENT_COUNT]동시에 도는 트랜잭션 최대 수 (총 slot)
max_request_workerPRM_ID_MAX_REQUEST_WORKERCSS_MAX_CLIENT_COUNT[… CSS_MAX_CLIENT_COUNT]worker 스레드 최대 수 (overcommit 상한)
task_groupPRM_ID_TASK_GROUP1 (deprecated no-op)아니오옛 설정이 로드되도록 남긴 것뿐

× 3 기본값은 CBRD-26636의 경험적 선택이며(소스 주석에 /* adjusted based on CBRD-26636 */), core × 2core × 3 이 throughput 고점을 보였다. 값이 user-set이 아니면 prm_tune_parameters 가 client limit에서 유도한다.

/* prm_tune_parameters — src/base/system_parameter.c (SERVER_MODE, condensed) */
client_limit = std::min (max_clients, CSS_MAX_CLIENT_COUNT);
factor = number_of_right_shifts (client_limit - 1); /* clamped >= 3 */
max_value = std::min (std::min (cpu * factor, cpu * 16),
std::min (client_limit, CSS_MAX_CLIENT_COUNT / 2));
max_request_concurrency = std::max (cpu, max_value);
max_request_worker = std::min (max_request_concurrency * 2
+ PRM_REQUEST_WORKER_ELASTIC_HEADROOM /* 32 */,
CSS_MAX_CLIENT_COUNT);
/* invariant enforced: max_request_worker >= max_request_concurrency */

런타임 변경 (CBRD-26690). 시스템 파라미터는 event-driven이 아니라 polling이므로, slot daemon이 매 tick check_and_propagate_parameters 에서 다시 확인하고, 바뀌었으면 css_set_max_concurrency_and_workersworker_pool_elastic::adjust_runtime_parameter 를 부른 뒤 clamp된 값을 prm_set_integer_value 로 되쓴다.

  • max_request_concurrency 를 올리면 새 총량을 코어에 고르게 나눠(quotient + remainder) 각 코어 slot 풀에 slot을 더 공급 한다. 내리면 잉여 slot은 반납 시 파괴된다(lazy 적용).
  • max_request_worker 를 올리면 전역 overcommit 상한을 높이고, 내리면 새 worker 생성을 막고 완료된 worker를 풀 목표까지 은퇴 시킨다.

cgroup 인지 (CBRD-26977). 같은 PR이 worker 상한을 전역 atomic으로 만들고(위), system_core_count() 가 cgroup v1 을 v2 fallback과 함께 읽어 CPU quota를 반영하게 한다(cgroup.cppquota_v1 / effective_v1quota() 디스패처 추가). 그래서 위 모든 기본값의 씨앗인 “코어 수”가 호스트 물리 코어가 아니라 컨테이너 CPU 한도를 반영한다.

관측성: statdump와 SHOW JOB QUEUES (CBRD-26692)

섹션 제목: “관측성: statdump와 SHOW JOB QUEUES (CBRD-26692)”

CBRD-26177의 “hot path에 perfmon 금지” 규칙이 여전하므로, 동시성은 hot-path 카운터가 아니라 요청 시 계산되는 peek 통계로 관측한다. 새 single-peek PSTAT 6종이 추가되고(perf_monitor.{c,h}), 각 코어의 concurrency_slot_pool::get_runtime_stats 를 합산하는 css_get_thread_runtime_stats 가 채운다.

$ cubrid statdump demodb | grep Num_request
Num_request_concurrency_total = 100 # 존재하는 slot
Num_request_concurrency_target = 100 # 목표 slot 수
Num_request_concurrency_busy = 1 # 현재 쥐고 있는 slot
Num_request_worker_total = 100 # 살아 있는 worker 스레드
Num_request_worker_target = 100 # 목표 worker 수
Num_request_worker_busy = 1 # 현재 task를 도는 worker

같은 6개 값이 SHOW JOB QUEUES 를 재구성한다(스캔 컬럼 수가 show_meta.c / server_support.c 에서 4 → 7로 늘고, 스캔 매퍼 css_wp_core_job_scan_mapper 가 worker-pool 코어 타입에 대한 템플릿이 됨). 스레드별 event_stats.slot_waits 집계와 함께, 이들이 실제 워크로드에 맞춰 max_request_concurrency 를 튜닝하는 운영자용 신호다.

심볼은 서브시스템별로 묶었다. 식별 가능한 경우 각 심볼에 구동 티켓 (CBRD-*) 을 붙여 둔다. 앞쪽 그룹(epoll, connection::*, worker_pool 크기, keepalive)은 Phase 1(CBRD-26177)이고, 뒤쪽 그룹(elastic 풀, concurrency slot, slot daemon, suspension 연동)은 Phase 2(CBRD-26662, PR #7323)다.

그림 13 — 파일·서브시스템 지도

그림 13 — 파일·서브시스템 지도. 연결 리액터는 src/connection/에 있다(Phase 1, CBRD-26177). connection_poolconnection_worker(epoll 스레드)와 coordinator를 소유하고, 연결 하나당 상태는 connection_context·connection_statistics·controller가 나눠 갖는다. server_support.c는 두 층을 잇는 다리 역할을 한다. 즉 css_push_server_task가 리액터에서 완성된 요청을 src/thread/의 백엔드 태스크 풀(thread_worker_pool 인터페이스, thread_worker_pool_impl, thread_manager, thread_entry)로 넘긴다는 뜻이다. Phase 2(CBRD-26662, PR #7323, 초록 점선)는 같은 기본 풀 위에 thread_worker_pool_elasticconcurrency_slot을 얹은 것이다. src/base/epoll은 리액터의 토대가 되고, src/base/system_parameter는 두 층 모두에 파라미터를 공급한다. 배선 파일들은 suspension 연동(network_sr.c, pl_execution_stack_context.hpp)과 관측(show_meta.c)을 맡는다.

  • cubsocket::epoll (class, src/base/epoll.hpp) — epoll_create1/epoll_ctl/epoll_wait 위의 RAII wrapper. 생성자가 EPOLL_CLOEXEC 인스턴스를 열고, 소멸자가 닫는다.
  • cubsocket::epoll::waitepoll_wait 위의 thin shim.
  • cubsocket::epoll::add_descriptorEPOLL_CTL_ADD, void *ptr 페이로드 옵션 (context 포인터를 events[i].data.ptr 로 함께 흘려보내는 데 쓴다).
  • cubsocket::epoll::modify_descriptorEPOLL_CTL_MOD, transmitter가 pending 데이터를 큐잉할 때 EPOLLOUT 추가/제거 용도.
  • cubsocket::epoll::remove_descriptorEPOLL_CTL_DEL.
  • cubsocket::nonblocking (parent class, nonblocking.hpp) — receiver/transmitter/worker 호출들이 모두 돌려주는 result enum (Ok, Pending, BudgetExhausted, PeerReset, Error, ClosedConnection, Skewed, Aborted) 정의.

connection::worker (CBRD-26212 / 26392 / 26406 / 26407 / 26617)

섹션 제목: “connection::worker (CBRD-26212 / 26392 / 26406 / 26407 / 26617)”
  • cubconn::connection::worker — 클래스 정의는 connection_worker.hpp. 멤버는 m_parent (pool), m_coordinator, m_watcher, per-thread 상태 (m_thread, m_core, m_status, m_stop, m_entry), context 집합 (m_context, m_removed_context), epoll (m_events), eventfd/timerfd (m_eventfd, m_timerfd), 타이머 테이블 (m_timer_handler), 우선순위 두 개의 메시지 큐 (m_queue[IMMEDIATE/LAZY], m_queue_size[]), 예산 knob과 exhausted 맵 (m_recv_budget, m_send_budget, m_exhausted), worker 측 메트릭 (m_stats).
  • worker::worker — 생성자. 시스템 파라미터 읽기, 세 개 타이머 설치, 스레드 띄우기.
  • worker::attach — 스레드 진입점. initialize → run → finalize 를 차례로 부른다.
  • worker::initialize — affinity 설정, thread entry 잡기, pthread 이름 connections 설정 (CBRD-26617 가 잡은 이름 누수 자리).
  • worker::run — 메인 reactor loop.
  • worker::finalize — 아직 열려 있는 context 비우기, thread entry 회수, watcher 신호.
  • worker::enqueue / worker::notify / worker::enqueue_and_notify — 외부 스레드 인터페이스.
  • worker::push_task_into_worker_pool — 백엔드 풀 (css_push_server_task) 으로 가는 한 줄 짜리 다리.
  • worker::handle_reception / worker::handle_transmission — fd별 I/O 드라이버. m_recv_budget/m_send_budget 을 지키고 BudgetExhausted 를 뱉는다. (CBRD-26392)
  • worker::handle_exhausted_add_context / worker::handle_exhausted — exhausted fd 재방문 큐. (CBRD-26392)
  • worker::handle_message_queue_new_client — 갓 들어온 context 를 fd에 묶고 EPOLLET|EPOLLIN|EPOLLRDHUP 로 epoll에 등록.
  • worker::handle_message_queue_handoff_client / worker::handle_message_queue_takeover_client — 이동 handshake 의 두 반쪽. (CBRD-26406 / CBRD-26407)
  • worker::handle_message_queue_send_packet / worker::handle_message_queue_release_packet — task worker가 연결로 바이트를 돌려보낼 때 직접 소켓을 쓰지 않고 이 메시지 들을 쓴다. send 시 transmitter가 데이터를 buffering 하면 fd 에 EPOLLOUT 이 추가될 수 있다.
  • worker::handle_message_queue_shutdown_client — 외부에서 온 close-connection 요청. handle_connection_close 호출.
  • worker::handle_message_queue_hibernate / worker::handle_message_queue_awaken — 자동 스케일링 상태 전이.
  • worker::handle_connection_close — 6단계 close 프로토콜. 백엔 드 task worker가 아직 conn을 잡고 있으면 LAZY 큐에 다시 넣어 재시도.
  • worker::statistics_metrics_to_coordinator — MEDIUM tick (기본 1초) 마다 m_stats + context 별 메트릭을 coordinator::message::STATISTICS 로 복사. (CBRD-26191)
  • worker::hibernate_check — MEDIUM tick 마다, status가 HIBERNATING 이고 m_context.empty() 면 타이머 정지.
  • worker::ha_close_all_connections — HIGH tick 마다, css_ha_server_state () == HA_SERVER_STATE_TO_BE_STANDBY 이면 idle connection을 강제로 닫는다 — CBRD-26523 와 맞물리는 HA 모드 전환 경로.
  • cubconn::connection::pool::freelist — 단일 연결 context 캐시 노드.
  • pool::initialize / pool::finalize — 최상위 bring-up / tear-down. 실행파일 wire-up이 부른다.
  • pool::initialize_topologyos::resources::cpu::effective () 와 (가능한 경우) os::resources::net::map_nic_to_index () 조회.
  • pool::initialize_freelistmax_connections * 1.1 만큼 freelist 노드 사전 할당.
  • pool::initialize_workersmax_connection_workers 만큼 고정 worker를 만들고 두 큐에 START 메시지를 넣어 워밍.
  • pool::initialize_coordinator / pool::start_coordinator / pool::finalize_coordinator — coordinator lifecycle.
  • pool::dispatch — handover 수신. master_connector 가 TCP connection이 CUBRID handshake를 마쳤을 때 부른다. coordinator 에 NEW_CLIENT 를 보낸다.
  • pool::claim_context / pool::retire_context — freelist API. 부른 스레드가 m_mutex 를 잡고 있어야 한다.
  • pool::lock_resource / pool::release_resource / pool::try_to_lock_resource — coordinator가 자기 수명 동안 잡고 있는 풀 단위 mutex.

connection::coordinator (CBRD-26406 / 26407 / 26424)

섹션 제목: “connection::coordinator (CBRD-26406 / 26407 / 26424)”
  • cubconn::connection::coordinator — 클래스 정의는 coordinator.hpp. 멤버는 m_parent, m_watcher, controller (Unix-domain 소켓 m_controller, m_ctrlfd), 메시지 큐 (m_queue, m_queue_size), worker count 추적 (m_max_worker, m_min_worker, m_current_worker), 이동 in-flight 집합 (m_migrating), 스케일링 부기 (m_scaling, m_scaling_statistics), worker별 통계 (m_statistics).
  • coordinator::coordinator — controller 소켓 열기, fd들을 epoll에 등록, 세 타이머 설치, 스레드 띄우기.
  • coordinator::run — 메인 reactor loop.
  • coordinator::initialize — core 0 (또는 첫 effective 코어) 에 고정, thread entry 잡기, 이름 coordinator 설정, 풀 lock 영구 점유.
  • coordinator::handle_message_queue_new_client — 배치: 최저 점수 worker 선정, context 할당, NEW_CLIENT 전달. (CBRD-26424)
  • coordinator::handle_message_queue_return_to_pool — worker의 m_removed_context 일괄 반환. context 별 통계 비우기 + pool::retire_context 호출.
  • coordinator::handle_message_queue_handoff_reply — 이동 마무 리. transferred=false 면 통계 되돌리기.
  • coordinator::handle_message_queue_statistics — worker별 통계 도착 처리. statistics_update_connection 으로 EWMA 갱신, 뒤따라 statistics_update_score. 보고하는 worker가 현재 draining_worker 이고 빈 context를 보고하면 scale_down_finish 호출. (CBRD-26424)
  • coordinator::handle_message_queue_shutdownm_stop 을 true로.
  • coordinator::transfer_connectionm_migrating 으로 보호. 소스 worker로 HANDOFF_CLIENT 전송.
  • coordinator::scale_up — 다음 worker AWAKEN, m_current_worker 증가. (CBRD-26406)
  • coordinator::scale_down / coordinator::scale_down_finish — 타깃 worker의 connection들을 비우고, HIBERNATE. (CBRD-26406)
  • coordinator::scale_trial / coordinator::scale_selection / coordinator::statistics_scaling — 자동 스케일링 상태 머신. (CBRD-26406 / CBRD-26424)
  • coordinator::statistics_rebalancing — MEDIUM tick (5초) 마다 점수 극값을 찾아, 격차가 high 점수의 20%를 넘으면 context 하나를 이동시킨다. (CBRD-26424)
  • coordinator::statistics_EWMA — α = 0.06, 마이크로초 정규화. worker / context 메트릭 모두에 쓰인다.
  • coordinator::statistics_find_score_extremesm_statistics[0..m_current_worker) 를 선형으로 훑어 (min_index, max_index) 반환.
  • coordinator::statistics_update_scoreEVAL_WORKER + EVAL_CONTEXT + client_num 공식 적용.
  • coordinator::statistics_print — controller가 부르는 worker 별 점수, EWMA, byte count 콘솔 dump.
  • coordinator::handle_controller / coordinator::handle_controller_request — 네 가지 control 명령 dispatch.

connection::context, controller, statistics

섹션 제목: “connection::context, controller, statistics”
  • cubconn::connection::context — 클라이언트별 상태 (worker index, id, ignore guard, recv 상태 머신, receiver, transmitter, blocker shared_ptr, context 별 메트릭). 32 KB inline send/recv 버퍼.
  • cubconn::connection::context::reset — freelist 재사용용 reset.
  • cubconn::thread_watchermutex + cv + int active. 정렬된 shutdown 용.
  • cubconn::message_blocker — single-shot mutex + cv + bool done. enqueue_and_notify 의 blocking caller용.
  • cubconn::connection::controller<RX,TX> — Unix-domain datagram 소켓 wrapper 템플릿 (controller.hpp).
  • cubconn::statistics::context / cubconn::statistics::worker — 메트릭 키 enum (connection_statistics.hpp).
  • cubconn::statistics::metrics<T,VT> — 고정 크기 카운터 배열. +=, - (metrics<T,double> 반환), * (스케일링), add, sub, get, set, reset, copy_from 지원. atomic 없음. (CBRD-26191)

Phase 1 뒤단 심볼. Phase 2(CBRD-26662)가 요청 경로의 이 풀을 worker_pool_elastic 로 대체한다 — 아래 elastic-worker-pool 그룹 참고.

  • cubthread::worker_pool (thread_worker_pool.hpp) — 추상 인터페이스 (Phase 2가 get_worker_countget_pool_size 로 rename하고 is_temp 인자를 없앰).
  • cubthread::worker_pool::core — Phase 1에서는 task_group 으로 크기 결정. Phase 2에서는 요청 풀이 CPU 코어마다 core 하나.
  • cubthread::worker_pool::execute / execute_on_corecss_push_server_task 가 부르는 진입점.
  • cubthread::worker_pool_task_capper (thread_worker_pool_taskcap.{hpp,cpp}) — HA 데몬용으로 남겨 둔 legacy admission cap wrapper. m_tasks_available = m_max_tasks = worker_pool->get_worker_count ().
  • css_push_server_task (server_support.c) — hot path handoff. static_cast<size_t> (conn_ref.idx) 로 분할하므로 같은 connection은 늘 같은 task pool core 위에 떨어진다.
  • css_get_task_stats (server_support.c) — 풀의 내부 카운터 로부터 stats[3] = { requested, started, completed } 를 채운 다. coordinator::statistics_update_task 가 소비.
  • PRM_ID_TCP_KEEPALIVE_IDLE / PRM_ID_TCP_KEEPALIVE_INTERVAL / PRM_ID_TCP_KEEPALIVE_COUNT — keepalive 튜너블.
  • PRM_ID_TASK_GROUP (Phase 1에서 thread_core_count 의 새 이름; Phase 2에서 값 1 의 no-op으로 deprecatedPRM_DEPRECATED).
  • PRM_ID_TASK_WORKERPhase 2에서 제거, PRM_ID_MAX_REQUEST_WORKER 로 rename (Phase 2 심볼 참고).
  • PRM_ID_CSS_MAX_CONNECTION_WORKER / PRM_ID_CSS_MIN_CONNECTION_WORKER (Phase 2가 서버 기본값을 max (system_core_count () / 2, 1) 로 재조정).
  • PRM_ID_CSS_AUTO_SCALING_WINDOW_SIZE.
  • PRM_ID_CSS_RECV_BUDGET_PER_CONNECTION / PRM_ID_CSS_SEND_BUDGET_PER_CONNECTION.
  • cubthread::worker_pool_elastic<Stats> (thread_worker_pool_elastic.hpp) — worker_pool_impl<Stats> 의 하위 클래스; CPU 코어마다 core_elastic 하나.
  • worker_pool_elastic::adjust_runtime_parameter (max_concurrency, max_worker)max_concurrency 를 코어에 고르게 나눠 각 코어의 slot 풀과 worker 집합에 적용.
  • core_elastic — 코어 단위; task 큐, concurrency_slot_pool m_slots, worker_elastic 리스트를 소유하고 풀 전역 std::atomic<std::size_t> &m_current_worker / &m_max_worker 를 들고 있다.
  • core_elastic::adjust_workers — slot 게이트 dispatch 루프.
  • core_elastic::get_or_make_available_worker / reserve_available_worker (m_max_worker 위 CAS) / allocate_worker — worker 재사용-또는-생성.
  • core_elastic::get_task_and_slot_or_become_available — task+slot 원자적 획득 또는 worker idle 등록.
  • worker_elastic — elastic worker; execute_current_task(task 동안 slot을 entry::m_slot 로 이동), get_new_task, run, get_retire_if_excess.
  • cubthread::stats_t (enum class : bool { on, off }) / cubthread::pool_t (enum class : uint8_t { basic, elastic }) (thread_compat.hpp).
  • worker_pool_type<Stats, Type> 별칭 + thread_create_worker_pool<...> (Args&&...) (thread_manager.hpp) — 옛 worker_pool_type / stats_worker_pool_type typedef와 두 인라인 팩토리를 대체.
  • temp worker 제거(CBRD-26683): execute_on_core (task, core_hash, bool is_temp)execute_on_core (task, core_hash); execute_task (task, bool is_temp)execute_task (task); get_worker_count ()get_pool_size (); push_task_on_coremethod_mode 인자를 잃음.

concurrency slot + core별 slot pool (CBRD-26685 / 26689)

섹션 제목: “concurrency slot + core별 slot pool (CBRD-26685 / 26689)”
  • cubthread::concurrency_slot (concurrency_slot.hpp) — 토큰; m_owner_pool, m_holder_pool, m_wait, m_wait_since; reset, start_waiting, stop_waiting, has_wait_expired (now)(50 ms), return_to_pool.
  • cubthread::concurrency_slot_pool : concurrency_slot_subscriber — 코어 mutex로 보호되는 core별 풀; m_available_slots(queue), m_wait_queue(std::list<entry *>), m_slot_count, m_target_count, m_surplus, SLOT_SURPLUS_THRESHOLD = 2.
  • concurrency_slot_pool::initialize / adjust_concurrency / try_acquire_slot / acquire_slot(블로킹; 중첩 대기 resume 상태 저장/복원) / release_slot / return_slot / borrow_surplus_slots(2000 ms) / needs_slot / get_score / get_runtime_stats / check_surplus_slots / wakeup_workers / has_queued_task.

slot daemon + pub/sub (CBRD-26686 / 26691 / 26690)

섹션 제목: “slot daemon + pub/sub (CBRD-26686 / 26691 / 26690)”
  • cubthread::concurrency_slot_subscriber — core 풀이 activate (identifier) 로 자신을 등록(identifier = 소유 worker_pool *).
  • cubthread::concurrency_slot_publisherm_mutex 아래 m_subscriberstraverse (func).
  • cubthread::concurrency_slot_daemon : concurrency_slot_publisher — 싱글턴; initialize / finalize / get_publisher / create_daemon(50 ms looper, 이름 "concurrency").
  • concurrency_slot_daemon_task : entry_task(.cpp) — execute = check_and_propagate_parameterssteal_from_entries_if_excesssteal_from_cores_if_excessdistribute_slotswakeup_workers; 그리고 has_slot_demand, tune_parameters ([system_core_count(), CSS_MAX_CLIENT_COUNT] clamp, worker ≥ concurrency).
  • thread_prepare_suspension / thread_prepare_resumption (thread_entry.cpp, static) — suspend 시 slot 반납/soft 대기, resume 시 slot 유지/재획득; thread_suspend_wakeup_and_unlock_entrythread_suspend_timeout_wakeup_and_unlock_entry 에서 호출.
  • cubthread::entry::m_slot(std::unique_ptr<concurrency_slot>), entry::start_waiting / stop_waiting, event_stat::slot_waits (thread_entry.hpp).
  • thread_resume_suspend_status::THREAD_CONCURRENCY_SLOT_SUSPENDED (25) / THREAD_CONCURRENCY_SLOT_RESUMED(26); thread_resume_status_to_string 에 라벨 추가.
  • thread_concurrency_slot_release / thread_concurrency_slot_acquire (thread_manager.hpp) — pl_execution_stack_context.hpp 의 SP 블로킹 수신 주위 명시적 반납/획득.
  • net_server_wakeup_workers(network_sr.c) — THREAD_CONCURRENCY_SLOT_SUSPENDED 를 받아들임.

Phase 2 시스템 파라미터·통계·서버 연결 (CBRD-26688 / 26692 / 26977)

섹션 제목: “Phase 2 시스템 파라미터·통계·서버 연결 (CBRD-26688 / 26692 / 26977)”
  • PRM_ID_MAX_REQUEST_CONCURRENCY (max_request_concurrency) — 신규; 기본 system_core_count () * 3; PRM_USER_CHANGE.
  • PRM_ID_MAX_REQUEST_WORKER (max_request_worker) — PRM_ID_TASK_WORKER 에서 rename; PRM_USER_CHANGE.
  • PRM_REQUEST_WORKER_ELASTIC_HEADROOM (= 32); client limit에서 둘을 유도하는 prm_tune_parameters 재작성.
  • PRM_ID_PAGE_LATCH_TIMEOUTPRM_ID_PAGE_LATCH_TIMEOUT_IN_MSECS (rename + ×1000; 이 PR에 부수적).
  • css_set_max_concurrency_and_workers / css_get_thread_runtime_stats(server_support.c) — 런타임 반영
    • peek-stats 접근자; css_Server_request_worker_poolworker_pool_type<stats_t::on, pool_t::elastic> 으로 타입을 다시 지정; css_init 에서 concurrency_slot_daemon::initialize/finalize.
  • PSTAT_REQUEST_CONCURRENCY_TOTAL / _TARGET / _BUSY, PSTAT_REQUEST_WORKER_TOTAL / _TARGET / _BUSY (perf_monitor.{c,h}) — statdump Num_request_concurrency_* / Num_request_worker_*.
  • metadata_of_job_queues(show_meta.c) / css_wp_core_job_scan_mapper(템플릿화) — SHOW JOB QUEUES 7 컬럼.
  • cubthread::system_core_count / cgroup::cpu::quota (cgroup.cpp, resources.cpp) — cgroup v1/v2 CPU-quota 인지 코어 수 (CBRD-26977).

이 개정 시점의 위치 힌트 (2026-04-30)

섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”
심볼파일라인
cubsocket::epoll (class)src/base/epoll.hpp42
cubsocket::epoll::epollsrc/base/epoll.cpp37
cubsocket::epoll::waitsrc/base/epoll.cpp54
cubsocket::epoll::add_descriptorsrc/base/epoll.cpp59
cubsocket::epoll::modify_descriptorsrc/base/epoll.cpp80
cubsocket::epoll::remove_descriptorsrc/base/epoll.cpp101
cubconn::connection::worker (class)src/connection/connection_worker.hpp52
worker::message_type (enum)src/connection/connection_worker.hpp106
worker::workersrc/connection/connection_worker.cpp75
worker::attachsrc/connection/connection_worker.cpp2107
worker::initializesrc/connection/connection_worker.cpp1943
worker::finalizesrc/connection/connection_worker.cpp1975
worker::runsrc/connection/connection_worker.cpp2007
worker::enqueuesrc/connection/connection_worker.cpp160
worker::notifysrc/connection/connection_worker.cpp182
worker::enqueue_and_notifysrc/connection/connection_worker.cpp218
worker::push_task_into_worker_poolsrc/connection/connection_worker.cpp288
worker::purge_stale_contextssrc/connection/connection_worker.cpp294
worker::handle_connection_closesrc/connection/connection_worker.cpp386
worker::statistics_metrics_to_coordinatorsrc/connection/connection_worker.cpp562
worker::hibernate_checksrc/connection/connection_worker.cpp584
worker::ha_close_all_connectionssrc/connection/connection_worker.cpp606
worker::handle_message_queue_new_clientsrc/connection/connection_worker.cpp1016
worker::handle_message_queue_handoff_clientsrc/connection/connection_worker.cpp1079
worker::handle_message_queue_takeover_clientsrc/connection/connection_worker.cpp1160
worker::handle_message_queue_shutdown_clientsrc/connection/connection_worker.cpp1227
worker::handle_message_queuesrc/connection/connection_worker.cpp1356
worker::handle_receptionsrc/connection/connection_worker.cpp1694
worker::handle_transmissionsrc/connection/connection_worker.cpp1782
worker::handle_exhausted_add_contextsrc/connection/connection_worker.cpp1837
worker::handle_exhaustedsrc/connection/connection_worker.cpp1854
cubconn::connection::pool (class)src/connection/connection_pool.hpp39
pool::freelistsrc/connection/connection_pool.hpp42
pool::initializesrc/connection/connection_pool.cpp62
pool::finalizesrc/connection/connection_pool.cpp89
pool::dispatchsrc/connection/connection_pool.cpp109
pool::claim_contextsrc/connection/connection_pool.cpp140
pool::retire_contextsrc/connection/connection_pool.cpp160
pool::initialize_freelistsrc/connection/connection_pool.cpp213
pool::initialize_topologysrc/connection/connection_pool.cpp249
pool::initialize_workerssrc/connection/connection_pool.cpp269
pool::finalize_workerssrc/connection/connection_pool.cpp314
pool::initialize_coordinatorsrc/connection/connection_pool.cpp353
pool::start_coordinatorsrc/connection/connection_pool.cpp376
cubconn::connection::coordinator (class)src/connection/coordinator.hpp41
coordinator::coordinatorsrc/connection/coordinator.cpp57
coordinator::initializesrc/connection/coordinator.cpp1192
coordinator::runsrc/connection/coordinator.cpp1240
coordinator::transfer_connectionsrc/connection/coordinator.cpp237
coordinator::scale_upsrc/connection/coordinator.cpp281
coordinator::scale_downsrc/connection/coordinator.cpp348
coordinator::scale_down_finishsrc/connection/coordinator.cpp317
coordinator::scale_trialsrc/connection/coordinator.cpp378
coordinator::scale_selectionsrc/connection/coordinator.cpp415
coordinator::statistics_find_score_extremessrc/connection/coordinator.cpp460
coordinator::statistics_update_scoresrc/connection/coordinator.cpp482
coordinator::statistics_update_connectionsrc/connection/coordinator.cpp502
coordinator::statistics_update_tasksrc/connection/coordinator.cpp545
coordinator::statistics_rebalancingsrc/connection/coordinator.cpp586
coordinator::statistics_scalingsrc/connection/coordinator.cpp629
coordinator::handle_message_queue_new_clientsrc/connection/coordinator.cpp934
coordinator::handle_message_queue_return_to_poolsrc/connection/coordinator.cpp970
coordinator::handle_message_queue_handoff_replysrc/connection/coordinator.cpp992
coordinator::handle_message_queue_statisticssrc/connection/coordinator.cpp1032
coordinator::handle_controller_requestsrc/connection/coordinator.cpp1110
cubconn::connection::contextsrc/connection/connection_context.hpp141
cubconn::statistics::metricssrc/connection/connection_statistics.hpp111
cubconn::connection::controller (template)src/connection/controller.hpp43
cubthread::worker_poolsrc/thread/thread_worker_pool.hpp54
cubthread::worker_pool_task_cappersrc/thread/thread_worker_pool_taskcap.hpp30
css_push_server_tasksrc/connection/server_support.c2354
css_get_task_statssrc/connection/server_support.c2647
REGISTER_CONNECTION (macro)src/thread/thread_manager.hpp496
PRM_ID_TCP_KEEPALIVE_IDLE (param row)src/base/system_parameter.c5161
PRM_ID_TASK_WORKER (param row)src/base/system_parameter.c5197
PRM_ID_CSS_MAX_CONNECTION_WORKER (param row)src/base/system_parameter.c5209
PRM_ID_CSS_AUTO_SCALING_WINDOW_SIZE (param row)src/base/system_parameter.c5243
PRM_ID_CSS_RECV_BUDGET_PER_CONNECTION (param row)src/base/system_parameter.c5259
PRM_ID_CSS_SEND_BUDGET_PER_CONNECTION (param row)src/base/system_parameter.c5271

Phase 2 위치 힌트 (PR #7323 — 머지 전이라 줄번호 생략). CBRD-26662는 아직 develop 대상 open PR이고 브랜치가 로컬에 체크 아웃되어 있지 않아 절대 줄번호가 안정적이지 않다. 아래 표는 Phase 2 심볼을 파일에만 앵커한다. 머지되면 줄번호로 갱신할 것.

심볼파일
cubthread::worker_pool_elastic / core_elastic / worker_elasticsrc/thread/thread_worker_pool_elastic.hpp
cubthread::concurrency_slot / concurrency_slot_poolsrc/thread/concurrency_slot.hpp
concurrency_slot_daemon / concurrency_slot_daemon_tasksrc/thread/concurrency_slot.cpp
stats_t / pool_tsrc/thread/thread_compat.hpp
worker_pool_type<Stats,Type> / thread_create_worker_pool / thread_concurrency_slot_release / thread_concurrency_slot_acquiresrc/thread/thread_manager.hpp
thread_prepare_suspension / thread_prepare_resumptionsrc/thread/thread_entry.cpp
entry::m_slot / event_stat::slot_waits / THREAD_CONCURRENCY_SLOT_SUSPENDED / _RESUMEDsrc/thread/thread_entry.hpp
css_set_max_concurrency_and_workers / css_get_thread_runtime_statssrc/connection/server_support.c
receive_buffer 주위 PL slot 반납/획득src/sp/pl_execution_stack_context.hpp
PRM_ID_MAX_REQUEST_CONCURRENCY / PRM_ID_MAX_REQUEST_WORKER / PRM_REQUEST_WORKER_ELASTIC_HEADROOMsrc/base/system_parameter.c
PSTAT_REQUEST_CONCURRENCY_* / PSTAT_REQUEST_WORKER_*src/base/perf_monitor.{c,h}
cgroup::cpu::quota (v1/v2)src/base/cgroup.cpp

형제 문서 — cubrid-thread-worker-pool.md. legacy 문서가 다루는 것은 (a) css_master_thread accept loop, (b) accept된 connection 마다 polling 스레드 한 개, (c) cubthread::worker_poolcore::worker 기계, (d) dispatch 지점인 css_push_server_task 다. 이 중 (c) 와 (d) 는 여전히 살아 있고 현재 코드 그대로다. (a) 는 master-thread accept 계층 자체는 바뀌지 않았지만, handover 지점 이 이제는 이 fd에 polling 스레드를 띄우라 가 아니라 pool::dispatch (coordinator 로 NEW_CLIENT 전달) 다. (b) 는 대체 되었다. legacy 문서에서 connection마다 스레드 하나 라고 쓴 부분은 더 이상 정확하지 않다. 도메인이 이동한 심볼을 정리해 두면 다음과 같다.

  • legacy의 polling/recv-loop 로직css_internal_request_handler 가 구동하는 connection 별 스레드들에 흩어져 있었다. 이제는 cubconn::connection::worker::handle_reception 과 그 친구들에 산다.
  • legacy의 connection-close 프로토콜 은 polling 스레드에서 css_close_socket 을 동기로 부르는 형태였다. 이제는 worker::handle_connection_close 가 LAZY-큐 재시도 + 별도 freelist 반환 단계를 끼고 돈다.
  • legacy의 통계 는 worker 풀의 get_stats 로 읽는 worker별 cubperf::stat_value 배열이었다. connection 측에서는 그 카운터들이 더 이상 카운터로 존재하지 않는다 (CBRD-26177 directive). 진단은 coordinator의 controller 소켓 (SHOW_STATS) 으로 한다.
  • legacy의 admission control 은 HA daemon에 한정된 worker_pool_task_capper 였다. NG에서는 모든 connection worker가 tick 당 byte 예산을 강제한다. capper 클래스는 트리에 남아 있지만 connection-worker 경로 위에는 없다.

형제 문서 — cubrid-server-session.md. 서버 세션 상태 조회는 요청 처리 중 task worker (백엔드 풀에 css_push_server_task 가 떨어진 뒤) 안에서 일어난다. connection worker는 세션을 조회하지 않는다 — 네트워크 프로토콜만 파싱한다. css_conn_entrysession_p 필드는 task 측이 읽는다 (server_support.ccss_server_task::execute 참고). legacy 문서와 같고, 재설계가 이 자리를 옮기지는 않는다.

EPIC 안에서 추적된 회귀들.

  • CBRD-26586 — worker timeout 이후 parallel query가 한 CPU만 사용. 홍예찬 님이 확인한 근원은 thread_worker_timeout_seconds 와 affinity 상속의 상호작용이다. connection worker가 (task pool이 스레드를 만료시켰기 때문에) task worker를 새로 만들면 새 pthread 가 connection worker의 CPU affinity를 상속받아, 결국 모든 백엔드 일이 connection worker의 코어에 핀된다. fix — 새로 띄우는 task worker는 affinity를 상속하지 않게 한다. fix 들어가기 전 임시 우회 — thread_worker_timeout_seconds 를 크게 잡아 백엔드 스레드가 재활용되지 않게 한다.
  • CBRD-26617 — task worker 스레드 이름이 connections 를 상속. 같은 메커니즘 (스레드 속성 상속). 코어 덤프에서 헷갈린다. 스레드 이름이 core.<name>... 파일 명 라벨에 쓰이기 때문에 task-worker 크래시가 core.connections.* 로 떨어졌다. fix — task pool이 worker를 띄울 때 이름을 명시적으로 다시 잡는다.
  • CBRD-26544 — schema_type_str synonym enum coredump. develop 에 이미 있던 버그가 새 빌드 위에서 드러났다. CCI의 enum과 그 문자열 배열이 어긋나 있었다. 같은 머지 윈도우 안에서 수정.
  • CBRD-26523 — HA 테스트 케이스 cbrd_21506_02, cbrd_22705_02 실패. 진단된 원인은 기존 HA 타이밍 버그 (tid:0 시스템 커밋에서 logwr/copylog 상호작용) 였다. 재설계가 상태 전이를 빠르게 하면서 노출되었을 뿐 재설계의 회귀는 아니다. 실제 fix는 CBRD-26576 으로 라우팅.

머지 후의 HA shell 테스트 셋. CBRD-26255 코멘트가 별도로 기록한 HA shell 테스트 실패 묶음 (bug_bts_5212, bug_bts_9047, cbrd_22207, cbrd_23854 등) 은 모두 타이밍 변화 가 원인이다. 재설계가 실제로 더 빠르고, 그래서 sleep과 grep 필터가 legacy 속도에 맞춰져 있던 테스트 스크립트가 들통난다. 수정은 일부는 테스트 스크립트의 타이밍 미세 조정, 한 건은 진짜 버그 (-353 Resource temporarily unavailable. ulimit -n 제약 아래에서 발생. FD 한도를 올리고 새 최소값을 문서화하는 쪽으로 fix) 였다.

CBRD-26177 의 perfmon 금지 directive. 미래에 누군가가 가장 깰 가능성이 높은 자리이므로 여기서 다시 적어 둔다.

“connection worker는 매우 동시성이 높은 hot-path이므로 perfmon 계열의 모니터링 코드를 추가해서는 안된다. 심각한 성능 저하를 일으킬 수 있다.”

코드를 읽거나 편집할 때 실무에서 유의할 점은 다음과 같다.

  1. worker::run, worker::handle_reception, worker::handle_transmission, worker::handle_packet, 메시지 큐 핸들러들, 또는 그것들이 부르는 어떤 함수에도 perfmon_inc_stat 이나 그 어떤 글로벌 atomic 증가를 넣지 말 것.
  2. worker 위의 statistics::metrics<> 인스턴스 (private uint64_t[]) 에는 메트릭을 추가해도 된다. coordinator가 이 미 그것들을 합산한다.
  3. controller 소켓 (SHOW_STATS) 이 지원되는 read-out 경로다. statistics_print 가 그 렌더러다.
  4. context 별 카운터는 context::m_stats 에 두고, coordinator 의 statistics_update_connection 을 통한 집계 경로가 이미 엮여 있다.

Phase 1 → Phase 2 상태. Phase 1(CBRD-26177)은 “고동시성에서의 고처리율”을 가져왔지만 CBRD-26636이 잰 후속 약점 — lean 뒤단 풀이 몇몇 worker의 lock 대기에 막힌다는 것 — 을 드러냈다. Phase 2 (CBRD-26662)가 위에 정리한 slot 메커니즘으로 이를 닫으며, PR #7323(feature/worker_pool_elastic)로 구현되어 develop 대상으로 open 되어 있다. 담당자에 따르면 머지 전 테스트케이스만 남았다. 트리를 읽는 사람이 알아둘 점:

  • 뒤단의 정식 knob은 이제 max_request_concurrency(활성 트랜잭션 상한)와 max_request_worker(스레드 overcommit 상한)이며, task_group / task_worker아니다. task_worker 는 rename되었고, task_group 은 옛 cubrid.conf 가 로드되도록 deprecated 1-값 no-op으로만 남는다.
  • 티켓 교차 참조 시 주의할 이름 불일치: EPIC은 high_concurrency, 서브태스크 CBRD-26688은 max_transaction_concurrency / max_transaction_worker 라 하지만 실제 파라미터는 max_request_concurrency / max_request_worker 다. CBRD-26688의 제약 문구(worker <= concurrency)는 코드(worker >= concurrency) 대비 뒤집혀 있다.
  • lock manager·page-buffer·CSS 큐는 손대지 않았다. slot 로직을 거기서 찾지 말 것. 그것은 thread_entry.cpp (thread_prepare_suspension / thread_prepare_resumption) 와 concurrency_slot.{hpp,cpp} 에 있다.
  • PR #7323은 CBRD-26977 하드닝 시리즈(전역 worker 상한, cgroup- v1/v2 인지 코어 수)와, 브랜치에서 함께 개발된 것으로 보이는 몇몇 서로 무관한 변경 — xasl_generation.c 의 집계 value-slot 공유, page_buffer.c 의 fix/unfix 카운터 sharding, connection_* NET_HEADER 리팩터, log_manager.c 의 flashback/CDC 가드 — 도 싣는다. 이 중 어느 것도 concurrency-slot 메커니즘의 일부가 아니다 (일부는 develop 머지로 들어왔을 수도 있다).
  1. Affinity-aware connection 배치. coordinator는 최저 점수 worker를 고른다. connection이 pgxc 류로 stateful (HA 복제, CDC 컨슈머, log-writer slave) 일 때, connection 수명 동안 한 worker에 고정 핀하는 가치가 있는가? 현재 transfer_connection 은 오래된 세션도 재배치한다. opt-out 은 worker::is_wait_required 안에서 cdc_Gl.conn.fd 를 false를 반환하는 자리뿐이다. 1급 시민 affinity-pinned connection 플래그가 그 갭을 메울 것이다.

  2. HA 복제의 connection 모델. connection worker는 HA_SERVER_STATE_TO_BE_STANDBY 를 받으면 비활성 context 들을 강제 close 한다 (ha_close_all_connections). 반대 전이 (standby → master) 에서는 어떻게 되는가? 새 클라이언트들이 대거 재접속하고 coordinator가 burst로 많은 context를 할당해야 한다. freelist는 max_connections * 1.1 만큼 잡혀 있어 burst 를 흡수해야 하지만, coordinator는 handle_message_queue_new_client 위에서 단일 스레드다. coordinator가 감당할 수 있는 새 connection 레이트의 구체적 상한은 측정되어 있지 않다.

  3. 점수 함수 가중치. EVAL_WORKER (25, 3.5, …) + (500, 1, …)EVAL_CONTEXT (50, 1000, …) + (10, 1, …) 매크로는 튜닝된 상수다. CBRD-26424 가 이 점이 경험적임을 인정했다. 민감도 surface는 어디에 있는가? 런타임 튜너블 가중치 셋이 auto_scaling_window_size 를 대체할 수 있는가 — 운영자가 점수를 latency 쪽이나 throughput 쪽으로 편향시키게 해서?

  4. CBRD-26421 의 검증 갭. 티켓이 명시했듯이, connection-worker 재배치와 동적 스케일링은 자동화 테스트에 잡혀 있지 않다 — connection 풀의 내부 상태가 어떤 사용자 가시 인터페이스 로도 드러나지 않기 때문 이다. controller 소켓은 디버깅용 이다. 읽기 전용 SHOW STATS SQL 또는 DBA-RPC view가 그 테스트 갭을 닫을 것이다. Phase 2가 뒤단 쪽은 일부 답한다 — Num_request_concurrency_* / Num_request_worker_* statdump 카운터와 재구성된 SHOW JOB QUEUES(CBRD-26692)가 slot/worker 점유를 드러낸다 — 하지만 connection-worker(앞단) 재배치 상태는 여전히 사용자 가시 view가 없다.

  5. std::nothrow 대 STL 예외 (CBRD-26412). 티켓의 결론은 본질적으로 “STL이 throw 하고 코드베이스가 STL을 쓰니 exhaustive 가드는 불가능하다” 였다. 일부 hot-path 할당 (pool::freelist (32 * 1024), m_context.reserve (256), m_exhausted.reserve (128)) 은 OOM 시 여전히 throw 한다. 운영자가 기대해야 할 실패 의미는 무엇인가 — 서버 크래시, 드롭된 connection, graceful degradation 중? 오늘은 첫 번째다.

  6. Send/recv 예산 기본값. OLTP에는 16 KB / 32 KB가 합리적 이지만 bulk-load와 CDC 스트리밍에는 작을 가능성이 크다. cubrid.conf 편집을 거치지 않는 connection-class 단위 override 경로는 있는가?

  7. core별 slot 균형 가정 (CBRD-26685). slot 풀은 “task가 코어에 고르게 분산된다”는 가정 하에 동시성을 코어 단위 로 관리하므로 코어별 상한이 전역 상한을 근사한다. Phase 1은 task를 conn_ref.idx 해시로 코어에 라우팅하는데, 이는 총합으로는 고르되 순간적으로는 아니다 — 무거운 task 버스트가 한 코어로 해시되면 그 코어 slot이 국지적으로 고갈되고 이웃은 논다. slot daemon의 2000 ms 잉여 이관이 초 단위로 이를 완화하는데, 지연 민감 버스트 에 충분히 빠른가, 아니면 재분배가 타이머가 아니라 수요 기반으로 일어나야 하는가?

  8. hard-wait 문턱과 잉여 타이밍. soft→hard 대기 승격은 고정 50 ms(concurrency_slot::has_wait_expired), core 간 잉여 이관은 2000 ms에 SLOT_SURPLUS_THRESHOLD = 2 로 컴파일타임 상수다. 전형적 lock 대기가 50 ms 바로 위에 있는 워크로드에서는 daemon이 slot을 churn 한다(뺏자마자 대기자가 곧 재획득). 문턱이 관측된 lock-wait 분포에 적응해야 하는가?

  9. overcommit 상한 대 스레드 비용. max_request_workermax_request_concurrency * 2 + 32 가 기본이며 soft·non-hard cap 이다(“allow to make worker overcommit”). park된 worker도 각자 cubthread::entry 와 스택을 소유한다. many-core 장비에서 상한의 메모리/스케줄링 비용은 얼마이고, + 32 여유 (PRM_REQUEST_WORKER_ELASTIC_HEADROOM)가 맞는 상수인가, 코어 수에 비례해야 하는가?

  10. SP “slot 없이 어느 지점까지 수행” 경계 (CBRD-26687). 티켓은 SP 경로에서 수신 자체가 코드 critical section과 무관하므로 worker가 slot 경계 너머 어느 정도 진행한 뒤에야 다시 slot을 쥐면 된다고 적는다. pl_execution_stack_context 에서 그 경계가 정확히 어디 그어지며, SP의 경우 max_request_concurrency 보다 많은 진짜 CPU-active worker를 허용할 수 있는가?

  11. 테스트케이스 상태. CBRD-26662의 서브태스크는 모두 “머지 후 regression test로 검증”으로 표시돼 있고, PR #7323은 테스트 케이스가 남은 채 open이다. regression suite가 들어오기 전까지 동시성 불변식(“활성 트랜잭션 ≤ max_request_concurrency”)은 설계로 보장될 뿐 아직 자동 테스트로는 아니다.

  • src/connection/connection_worker.cpp (≈ 58 KB)
  • src/connection/connection_worker.hpp (≈ 10 KB)
  • src/connection/connection_pool.cpp (≈ 10 KB)
  • src/connection/connection_pool.hpp (≈ 3 KB)
  • src/connection/coordinator.cpp (≈ 35 KB)
  • src/connection/coordinator.hpp (≈ 10 KB)
  • src/connection/controller.hpp
  • src/connection/connection_context.hpp
  • src/connection/connection_statistics.hpp
  • src/connection/connection_support.{cpp,hpp}
  • src/connection/server_support.ccss_push_server_task (line 2354), css_get_task_stats (line 2647)
  • src/connection/tcp.csetsockopt SO_KEEPALIVE (line 203)
  • src/base/epoll.{cpp,hpp}
  • src/thread/thread_worker_pool.hpp — 추상 풀 인터페이스 (line 54)
  • src/thread/thread_worker_pool_impl.{cpp,hpp} — 풀 구현
  • src/thread/thread_worker_pool_taskcap.{cpp,hpp} — legacy admission cap
  • src/thread/thread_manager.hppREGISTER_CONNECTION (line 496)
  • src/base/system_parameter.{c,h}tcp_keepalive_*, task_group, task_worker, min/max_connection_worker, auto_scaling_window_size, recv/send_budget_per_connection 의 param ID와 row
  • src/executables/server.ccubconn::connection::pool connections; (line 557)

Phase 2 (CBRD-26662, PR #7323) — 줄번호는 브랜치 머지 전까지 diff 상대값:

  • src/thread/concurrency_slot.hpp (+235) / concurrency_slot.cpp (+925) — slot, core별 slot 풀, pub/sub, slot daemon.
  • src/thread/thread_worker_pool_elastic.hpp (+831) — worker_pool_elastic / core_elastic / worker_elastic.
  • src/thread/thread_worker_pool_impl.{hpp,cpp} — 재정비된 베이스 풀 (available-worker 리스트, snapshot guard, get_pool_size).
  • src/thread/thread_worker_pool.hpp / thread_compat.hpp / thread_manager.{hpp,cpp} — 인터페이스, stats_t/pool_t, worker_pool_type<> 별칭 + 팩토리, thread_concurrency_slot_*.
  • src/thread/thread_entry.{cpp,hpp}thread_prepare_suspension/_resumption, m_slot, slot_waits, 새 resume 상태.
  • src/communication/network_sr.c — 깨우기 가능 switch.
  • src/sp/pl_execution_stack_context.hpp — SP slot 반납/획득.
  • src/base/system_parameter.{c,h}max_request_concurrency / max_request_worker, task_group deprecation, prm_tune_parameters.
  • src/base/perf_monitor.{c,h}Num_request_* peek 통계.
  • src/base/cgroup.{cpp,hpp} / resources.cpp — cgroup 인지 코어 수 (CBRD-26977).
  • src/parser/show_meta.c + src/connection/server_support.cSHOW JOB QUEUES 7 컬럼; css_set_max_concurrency_and_workers, css_get_thread_runtime_stats.
  • 기본 풀의 새 별칭 마이그레이션: src/query/vacuum.c, src/loaddb/load_worker_manager.cpp, src/storage/btree_load.c, src/transaction/log_page_buffer.c, src/query/parallel/px_worker_manager_global.hpp, src/transaction/log_recovery_redo_parallel.hpp.

Phase 2 (Logical-Wait-Aware Concurrency Control):

  • Silberschatz, Korth, Sudarshan. Database System Concepts, 6판 — 13장 Storage and File Structure (버퍼 기초, 앞단/ 뒤단 구도).
  • Petrov, Alex. Database Internals (O’Reilly, 2019). §5.3 Concurrent Execution — 풀 크기에 대한 직관, C10K 구도.
  • Stevens, W. Richard. UNIX Network Programming, Vol. 1, 3판 — §16.5 “TCP Concurrent Server, One Child per Client” (이번 재설계가 떠나온 모델).
  • Pai, V., P. Druschel, W. Zwaenepoel. Flash: An Efficient and Portable Web Server. USENIX 1999. (이벤트 구동 비대칭 멀티 프로세스 설계 — 이 재설계의 reactor 패턴의 직계 조상).
  • Welsh, M., D. Culler, E. Brewer. SEDA: An Architecture for Well-Conditioned, Scalable Internet Services. SOSP 2001. (크기가 제한된 stage 큐를 통한 admission 제어 — recv_budget_per_connection / send_budget_per_connection 의 지적 토대).
  • Linux 커널 docs — epoll(7), eventfd(2), timerfd_create(2). worker::run 을 손대는 사람에게 EPOLLET (edge-triggered) 의미론은 필수 사전 독서다.
  • Silberschatz, Korth, Sudarshan. Database System Concepts — 18장 Concurrency Control (lock 대기가 트랜잭션 service time의 지배적 비-CPU 성분이라는 구도 — Phase 2 slot admission의 배경).
  • MySQL Reference Manual — “MySQL Enterprise Thread Pool” 및 thread-group stall detection (가장 가까운 선례: 활성 스레드 상한, stall 시 overcommit — Phase 2는 모양을 따르되 slot으로 토큰화).
  • Microsoft SQL Server docs — “Thread and Task Architecture Guide” (SOS 스케줄러, 정해진 지점에서의 cooperative yield — slot 인계와 유사).
  • Linux 커널 docs — cgroups(7) (cpu.max / cpu.cfs_quota_us) — CBRD-26977에서 system_core_count() 가 읽어 기본값이 컨테이너 한도를 따르게 하는 CPU-quota 출처.