콘텐츠로 이동

[KO] CUBRID 스레드 매니저 NG — 고동시성을 위한 Connection/Worker 풀 재설계 (CBRD-26177)

목차

이 문서는 cubrid-thread-worker-pool.md 의 차세대 짝 이다. 형제 문서가 다루는 것은 legacy 구조 — accept된 connection 마다 polling 스레드 한 개, 그 뒤에 max_clients 크기의 cubthread::worker_pool, 모든 dispatch가 풀 단위 mutex를 거치는 모양이다. 본 문서가 따라 가는 재설계는 CBRD-26177 EPIC 으로 guava 릴리스에 들어 갔는데, 앞단을 작은 경계의 epoll 구동 connection worker 집합으로 교체하고, 그 사이에 coordinator 를 두어 connection 분배·재배치· 자동 스케일링을 맡기며, 한 tick의 I/O 양을 send/recv 예산 으로 묶어 두고, context 할당은 worker별 freelist로 돌게 만들어 hot path에서 new/delete 호출을 없앤 것이 골자다. 뒤단의 task worker pool은 그대로 남되, 두 개의 새 파라미터 (task_group, task_worker) 로 크기를 다시 잡아 고동시성에서의 경합을 피한다.

데이터베이스 서버의 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초 타이머로 모아서 합산한다.

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 뿐이다.

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 이다.

이 재설계는 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에서 경합 상태였다.

flowchart LR
  subgraph "앞단 (legacy) — N == 활성 클라이언트 수"
    p1["polling 스레드 1<br/>poll(fd1)"]
    p2["polling 스레드 2<br/>poll(fd2)"]
    pN["polling 스레드 N<br/>poll(fdN)"]
  end
  subgraph "뒤단 (legacy) — task worker (size = max_clients)"
    direction TB
    M["per-core mutex<br/>(공유 dispatch)"]
    W1["worker 1"]
    W2["worker 2"]
    WK["worker K"]
  end
  p1 --> M
  p2 --> M
  pN --> M
  M --> W1
  M --> W2
  M --> WK

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 로 크기가 정해진다.

flowchart LR
  subgraph "앞단 (TO-BE) — 경계 있는 epoll worker"
    cw1["connection_worker 0<br/>epoll_wait()"]
    cw2["connection_worker 1<br/>epoll_wait()"]
    cwM["connection_worker M-1<br/>epoll_wait()"]
  end
  subgraph "뒤단 — task worker (task_group × task_worker)"
    direction TB
    G0["group 0<br/>workers"]
    G1["group 1<br/>workers"]
    GG["group g-1<br/>workers"]
  end
  client1 -.fd.- cw1
  client2 -.fd.- cw1
  client3 -.fd.- cw2
  clientK -.fd.- cwM
  cw1 -- "css_push_server_task(idx)" --> G0
  cw2 --> G1
  cwM --> GG

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에서 나오고 들어간다.

flowchart LR
  C["coordinator<br/>(고정, core 0)"]
  subgraph "Connection worker (current = 4..max)"
    cw0["worker 0"]
    cw1["worker 1"]
    cwN["worker N-1"]
  end
  FL["pool::freelist<br/>(context 캐시)"]
  TP["task worker pool<br/>(task_group × task_worker)"]
  CTRL["controller socket<br/>(/tmp/cub_server_∗_coordinator.sock)"]

  C -- "NEW_CLIENT / HANDOFF / HIBERNATE / AWAKEN" --> cw0
  C -- "..." --> cw1
  C -- "..." --> cwN
  cw0 -- "STATISTICS / RETURN_TO_POOL / HANDOFF_REPLY" --> C
  cw0 -- "claim_context / retire_context" --> FL
  cw1 --> FL
  cwN --> FL
  cw0 -- "css_push_server_task" --> TP
  cw1 --> TP
  cwN --> TP
  CTRL -.SHOW_STATS / SCALE_UP / SCALE_DOWN / CLIENT_MOVE.- C

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.

생성자가 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--;
}

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

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

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] 보정

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_grouptask_worker

섹션 제목: “Task worker 재정비 — task_group 과 task_worker”

뒤단 풀은 여전히 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 의 동기다 (소스 검증 노트 참고).

심볼은 서브시스템별로 묶었다. 식별 가능한 경우 각 심볼에 구동 티켓 (CBRD-*) 을 붙여 둔다.

  • 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)
  • cubthread::worker_pool (thread_worker_pool.hpp) — 추상 인터페이스는 그대로.
  • cubthread::worker_pool::core — 이제 task_group 으로 크기 결정.
  • 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 (옛 thread_core_count 의 새 이름).
  • PRM_ID_TASK_WORKER.
  • PRM_ID_CSS_MAX_CONNECTION_WORKER / PRM_ID_CSS_MIN_CONNECTION_WORKER.
  • PRM_ID_CSS_AUTO_SCALING_WINDOW_SIZE.
  • PRM_ID_CSS_RECV_BUDGET_PER_CONNECTION / PRM_ID_CSS_SEND_BUDGET_PER_CONNECTION.

이 개정 시점의 위치 힌트 (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

형제 문서 — 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 을 통한 집계 경로가 이미 엮여 있다.

CBRD-26662 로의 전환 — Logical-Wait-Aware Concurrency Control. 이 재설계는 고동시성에서의 고처리율 을 가져왔지만, CBRD-26636 이 짚은 후속 약점도 드러냈다 — task_worker 가 처리율 위주로 공격적으로 작게 (4–6 × cores) 잡힐 때, 몇몇 worker가 lock 위에 서 대기하면 백엔드 전체가 막힐 수 있다. CBRD-26662 가 slot 추상화를 들여온다. worker는 slot을 들고 있어야 active 이며, logical wait (lock 또는 condition variable) 에 들어간 worker가 slot을 반환하면 그 slot은 worker에게 풀린다 — 단, high_concurrency (런타임 튜너블) 를 상한으로 한다. 계획은 task_group / task_worker 를 폐기하고 high_concurrency 로 대체하는 것이다. 그 작업은 진행 중이고, 지금은 task_workertask_group 을 정식 knob으로 본다.

  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가 그 테스트 갭을 닫을 것이다.

  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 경로는 있는가?

  • 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)
  • 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) 의미론은 필수 사전 독서다.