[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_connection
과 send_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초 타이머로 모아서 합산한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”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_manager 는 one-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_worker … max_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 + CBRD-26177)
섹션 제목: “동기 (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에 있게 하는 주요 병목 지점이 된다.”
여기서 도출된 목표가 다음 다섯이다.
- connection-당 polling을 작은 경계의 epoll 구동 connection
worker 집합으로 교체 — 과도한
poll()호출 제거 (CBRD-26177 수락 기준 1). - throughput을 동시성을 단조 로 만들기 — 클라이언트가 더 늘어났다고 처리율이 줄어들지 않게 (수락 기준 2).
- 각 worker 안에 admission 형태의 backpressure 추가 (CBRD-26392) — 굵은 connection 한 개가 동료들을 굶기지 못하게.
- 부하를 본 배치와 동적 크기 조정 추가 (CBRD-26406, CBRD-26407, CBRD-26424) — 엔진이 idle과 saturated 사이에서 스스로 튜닝 되도록.
- 모니터링 hot path에서 atomic 떼어 내기 (CBRD-26191).
CBRD-26177 은 이 모든 후속 티켓의 모양을 결정짓고 본 문서 전체 를 떠받치는 강한 directive 도 함께 내놓았다.
“connection worker는 상당히 동시성이 높은 hot-path이므로 perfmon 계열의 모니터링 코드를 추가해서는 안된다. 심각한 성능 저하를 일으킬 수 있다.”
소스를 읽을 때 가장 무겁게 머리에 새겨야 할 제약이 이것이다.
worker tick 위에 글로벌 atomic 카운터 같은 것이나
perfmon_inc_stat() 류 호출이 들어가면 그 자체가 regression
이다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”이 재설계는 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 (CBRD-26212)
섹션 제목: “Connection worker (CBRD-26212)”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.cppm_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 를 차례로 부른다. initialize 는 os::resources::cpu::setaffinity (m_core)
로 스레드를 자기 코어에 고정시키고, cubthread::entry 를 잡고,
스레드 이름을 connections 로 설정한다. 이 이름이 task pool
까지 새는 것을 CBRD-26617 가 잡았다.
메인 loop는 교과서적 reactor 모양이다.
// worker::run — src/connection/connection_worker.cppwhile (!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.cppvoid 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 풀 (CBRD-26255)
섹션 제목: “Connection 풀 (CBRD-26255)”풀 (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.hppstruct 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.cppcontext *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::initialize 는 pool::initialize_topology 를 거쳐
요청된 max_connection_workers 를 os::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_workers
가 m_watcher->active == 1 (coordinator만 남은 상태) 까지
css_get_shutdown_timeout() 만큼 기다린 다음, pool::finalize_coordinator
가 active == 0 까지 다시 기다린다. 그 상태에 못 닿으면
try_to_lock_resource 의 10초짜리 try-lock loop 뒤에 _exit(0)
가 부수듯 호출된다 — 일부러의 hard exit이다. 대안이 “다른 어
떤 것도 정리해 줄 수 없는 상태를 들고 있는 스레드를 영원히
기다리기” 인 까닭이다.
Send/recv 예산 (CBRD-26392)
섹션 제목: “Send/recv 예산 (CBRD-26392)”예산 메커니즘이 이 설계에서 가장 미묘한 자리다. 이게 없으면
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.cppio_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.cppstatus = 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_context 의 prepared 플래그가 그 뒤로 미루
는 가드다. 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::BudgetExhausted 가 result::Pending 과 다른 enum 값
이라는 점은 짚어 둘 만하다 — Pending 은 “지금 커널이 더 줄
바이트가 없다” (다음 epoll edge까지 자연스럽게 back-off) 인 반
면, BudgetExhausted 는 더 줄 게 있는데 자발적으로 양보한다
(이번 loop나 다음 loop에 반드시 다시 와야 한다) 는 뜻이다.
자동 스케일링 (CBRD-26406)
섹션 제목: “자동 스케일링 (CBRD-26406)”CBRD-26406 가 connection 재배치와 worker 수 스케일링의 메커니
즘 을 깐다. 정책 자체는 CBRD-26424 (점수 기반 선택, 아래
참조) 에 산다. 메커니즘은 모양만 보면 단순하다 — worker가 1초
타이머로 통계를 보고하고, coordinator의 5초짜리 REBALANCING
타이머가 worker별 점수를 비교해 가장 무거운 worker가 자기
connection 하나를 가장 가벼운 쪽으로 hand-off 하게 시키며,
coordinator의 60초짜리 SCALING 타이머가 자동 스케일링 상태
머신을 굴린다.
scaling_status enum은 두 상태뿐이다.
STABLE— 현재 카운트가 충분히 좋다, 측정 중 아님.TRIAL—count개의 후보 크기를 sweep 하며 throughput 점수를 기록한 뒤 최댓값을 고른다.
SCALING tick마다 다음을 한다.
// coordinator::statistics_scaling — src/connection/coordinator.cppif (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이 같은 방향으로 흘러가지
않도록), count 를 auto_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_finish 는
handle_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.cppm_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.hpp 의
timer_latency enum 참고).
제어 소켓이 다음 관리 명령들을 노출한다.
SHOW_STATS— worker별 EWMA throughput과 큐 깊이를 (statistics_print) stdout에 찍는다.SCALE_UP/SCALE_DOWN— 자동 스케일링 상태 머신의 한 step을 강제한다.CLIENT_MOVE— id로 지정한 connection 한 개를 workerfrom에서 workerto로 수동 이동시킨다.
이건 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.cppstd::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.cppm_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.hppacc = 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% 미만으로 떨어진다.
Atomic 없는 통계 (CBRD-26191)
섹션 제목: “Atomic 없는 통계 (CBRD-26191)”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.cppmessage.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 위에서 측정해 두었다.
| 워크로드 | 적용 전 | 적용 후 | 향상 |
|---|---|---|---|
| workloada | 58 464.28 | 60 646.59 | +3.7% |
| workloadb | 70 009.99 | 72 976.31 | +4.2% |
| update | 44 158.66 | 45 128.96 | +2.2% |
| mix | 9 440.82 | 10 115.33 | +7.1% |
connection 측 메트릭 설계는 같은 템플릿을 새 layer에 그대로 가져왔다.
TCP keepalive 튜너블
섹션 제목: “TCP keepalive 튜너블”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
섹션 제목: “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_group
≤ task_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 × cores 가
task_worker = max_clients 보다 일관되게 우월함을 보였다 —
단, task_worker < max_clients 일 때 많은 worker가 긴 lock
위에서 대기하면 deadlock 위험이 있다. 그 위험이 CBRD-26662 의
동기다 (소스 검증 노트 참고).
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼은 서브시스템별로 묶었다. 식별 가능한 경우 각 심볼에 구동 티켓 (CBRD-*) 을 붙여 둔다.
epoll wrapper (CBRD-26212)
섹션 제목: “epoll wrapper (CBRD-26212)”cubsocket::epoll(class,src/base/epoll.hpp) —epoll_create1/epoll_ctl/epoll_wait위의 RAII wrapper. 생성자가EPOLL_CLOEXEC인스턴스를 열고, 소멸자가 닫는다.cubsocket::epoll::wait—epoll_wait위의 thin shim.cubsocket::epoll::add_descriptor—EPOLL_CTL_ADD,void *ptr페이로드 옵션 (context 포인터를events[i].data.ptr로 함께 흘려보내는 데 쓴다).cubsocket::epoll::modify_descriptor—EPOLL_CTL_MOD, transmitter가 pending 데이터를 큐잉할 때EPOLLOUT추가/제거 용도.cubsocket::epoll::remove_descriptor—EPOLL_CTL_DEL.cubsocket::nonblocking(parent class,nonblocking.hpp) — receiver/transmitter/worker 호출들이 모두 돌려주는resultenum (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 모드 전환 경로.
connection::pool (CBRD-26255 / 26407)
섹션 제목: “connection::pool (CBRD-26255 / 26407)”cubconn::connection::pool::freelist— 단일 연결 context 캐시 노드.pool::initialize/pool::finalize— 최상위 bring-up / tear-down. 실행파일 wire-up이 부른다.pool::initialize_topology—os::resources::cpu::effective ()와 (가능한 경우)os::resources::net::map_nic_to_index ()조회.pool::initialize_freelist—max_connections * 1.1만큼 freelist 노드 사전 할당.pool::initialize_workers—max_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_shutdown—m_stop을 true로.coordinator::transfer_connection—m_migrating으로 보호. 소스 worker로HANDOFF_CLIENT전송.coordinator::scale_up— 다음 workerAWAKEN,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_extremes—m_statistics[0..m_current_worker)를 선형으로 훑어(min_index, max_index)반환.coordinator::statistics_update_score—EVAL_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_watcher—mutex + cv + int active. 정렬된 shutdown 용.cubconn::message_blocker— single-shotmutex + 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)
task worker pool 변경
섹션 제목: “task worker pool 변경”cubthread::worker_pool(thread_worker_pool.hpp) — 추상 인터페이스는 그대로.cubthread::worker_pool::core— 이제task_group으로 크기 결정.cubthread::worker_pool::execute/execute_on_core—css_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가 소비.
system parameter
섹션 제목: “system parameter”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.hpp | 42 |
cubsocket::epoll::epoll | src/base/epoll.cpp | 37 |
cubsocket::epoll::wait | src/base/epoll.cpp | 54 |
cubsocket::epoll::add_descriptor | src/base/epoll.cpp | 59 |
cubsocket::epoll::modify_descriptor | src/base/epoll.cpp | 80 |
cubsocket::epoll::remove_descriptor | src/base/epoll.cpp | 101 |
cubconn::connection::worker (class) | src/connection/connection_worker.hpp | 52 |
worker::message_type (enum) | src/connection/connection_worker.hpp | 106 |
worker::worker | src/connection/connection_worker.cpp | 75 |
worker::attach | src/connection/connection_worker.cpp | 2107 |
worker::initialize | src/connection/connection_worker.cpp | 1943 |
worker::finalize | src/connection/connection_worker.cpp | 1975 |
worker::run | src/connection/connection_worker.cpp | 2007 |
worker::enqueue | src/connection/connection_worker.cpp | 160 |
worker::notify | src/connection/connection_worker.cpp | 182 |
worker::enqueue_and_notify | src/connection/connection_worker.cpp | 218 |
worker::push_task_into_worker_pool | src/connection/connection_worker.cpp | 288 |
worker::purge_stale_contexts | src/connection/connection_worker.cpp | 294 |
worker::handle_connection_close | src/connection/connection_worker.cpp | 386 |
worker::statistics_metrics_to_coordinator | src/connection/connection_worker.cpp | 562 |
worker::hibernate_check | src/connection/connection_worker.cpp | 584 |
worker::ha_close_all_connections | src/connection/connection_worker.cpp | 606 |
worker::handle_message_queue_new_client | src/connection/connection_worker.cpp | 1016 |
worker::handle_message_queue_handoff_client | src/connection/connection_worker.cpp | 1079 |
worker::handle_message_queue_takeover_client | src/connection/connection_worker.cpp | 1160 |
worker::handle_message_queue_shutdown_client | src/connection/connection_worker.cpp | 1227 |
worker::handle_message_queue | src/connection/connection_worker.cpp | 1356 |
worker::handle_reception | src/connection/connection_worker.cpp | 1694 |
worker::handle_transmission | src/connection/connection_worker.cpp | 1782 |
worker::handle_exhausted_add_context | src/connection/connection_worker.cpp | 1837 |
worker::handle_exhausted | src/connection/connection_worker.cpp | 1854 |
cubconn::connection::pool (class) | src/connection/connection_pool.hpp | 39 |
pool::freelist | src/connection/connection_pool.hpp | 42 |
pool::initialize | src/connection/connection_pool.cpp | 62 |
pool::finalize | src/connection/connection_pool.cpp | 89 |
pool::dispatch | src/connection/connection_pool.cpp | 109 |
pool::claim_context | src/connection/connection_pool.cpp | 140 |
pool::retire_context | src/connection/connection_pool.cpp | 160 |
pool::initialize_freelist | src/connection/connection_pool.cpp | 213 |
pool::initialize_topology | src/connection/connection_pool.cpp | 249 |
pool::initialize_workers | src/connection/connection_pool.cpp | 269 |
pool::finalize_workers | src/connection/connection_pool.cpp | 314 |
pool::initialize_coordinator | src/connection/connection_pool.cpp | 353 |
pool::start_coordinator | src/connection/connection_pool.cpp | 376 |
cubconn::connection::coordinator (class) | src/connection/coordinator.hpp | 41 |
coordinator::coordinator | src/connection/coordinator.cpp | 57 |
coordinator::initialize | src/connection/coordinator.cpp | 1192 |
coordinator::run | src/connection/coordinator.cpp | 1240 |
coordinator::transfer_connection | src/connection/coordinator.cpp | 237 |
coordinator::scale_up | src/connection/coordinator.cpp | 281 |
coordinator::scale_down | src/connection/coordinator.cpp | 348 |
coordinator::scale_down_finish | src/connection/coordinator.cpp | 317 |
coordinator::scale_trial | src/connection/coordinator.cpp | 378 |
coordinator::scale_selection | src/connection/coordinator.cpp | 415 |
coordinator::statistics_find_score_extremes | src/connection/coordinator.cpp | 460 |
coordinator::statistics_update_score | src/connection/coordinator.cpp | 482 |
coordinator::statistics_update_connection | src/connection/coordinator.cpp | 502 |
coordinator::statistics_update_task | src/connection/coordinator.cpp | 545 |
coordinator::statistics_rebalancing | src/connection/coordinator.cpp | 586 |
coordinator::statistics_scaling | src/connection/coordinator.cpp | 629 |
coordinator::handle_message_queue_new_client | src/connection/coordinator.cpp | 934 |
coordinator::handle_message_queue_return_to_pool | src/connection/coordinator.cpp | 970 |
coordinator::handle_message_queue_handoff_reply | src/connection/coordinator.cpp | 992 |
coordinator::handle_message_queue_statistics | src/connection/coordinator.cpp | 1032 |
coordinator::handle_controller_request | src/connection/coordinator.cpp | 1110 |
cubconn::connection::context | src/connection/connection_context.hpp | 141 |
cubconn::statistics::metrics | src/connection/connection_statistics.hpp | 111 |
cubconn::connection::controller (template) | src/connection/controller.hpp | 43 |
cubthread::worker_pool | src/thread/thread_worker_pool.hpp | 54 |
cubthread::worker_pool_task_capper | src/thread/thread_worker_pool_taskcap.hpp | 30 |
css_push_server_task | src/connection/server_support.c | 2354 |
css_get_task_stats | src/connection/server_support.c | 2647 |
REGISTER_CONNECTION (macro) | src/thread/thread_manager.hpp | 496 |
PRM_ID_TCP_KEEPALIVE_IDLE (param row) | src/base/system_parameter.c | 5161 |
PRM_ID_TASK_WORKER (param row) | src/base/system_parameter.c | 5197 |
PRM_ID_CSS_MAX_CONNECTION_WORKER (param row) | src/base/system_parameter.c | 5209 |
PRM_ID_CSS_AUTO_SCALING_WINDOW_SIZE (param row) | src/base/system_parameter.c | 5243 |
PRM_ID_CSS_RECV_BUDGET_PER_CONNECTION (param row) | src/base/system_parameter.c | 5259 |
PRM_ID_CSS_SEND_BUDGET_PER_CONNECTION (param row) | src/base/system_parameter.c | 5271 |
소스 검증 노트
섹션 제목: “소스 검증 노트”형제 문서 — cubrid-thread-worker-pool.md. legacy 문서가
다루는 것은 (a) css_master_thread accept loop, (b) accept된
connection 마다 polling 스레드 한 개, (c) cubthread::worker_pool
과 core::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_entry
의 session_p 필드는 task 측이 읽는다 (server_support.c 의
css_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 계열의 모니터링 코드를 추가해서는 안된다. 심각한 성능 저하를 일으킬 수 있다.”
코드를 읽거나 편집할 때의 실무 함의는 다음과 같다.
worker::run,worker::handle_reception,worker::handle_transmission,worker::handle_packet, 메시지 큐 핸들러들, 또는 그것들이 부르는 어떤 함수에도perfmon_inc_stat이나 그 어떤 글로벌 atomic 증가를 넣지 말 것.- worker 위의
statistics::metrics<>인스턴스 (privateuint64_t[]) 에는 메트릭을 추가해도 된다. coordinator가 이 미 그것들을 합산한다. - controller 소켓 (
SHOW_STATS) 이 지원되는 read-out 경로다.statistics_print가 그 렌더러다. - 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_worker 와
task_group 을 정식 knob으로 본다.
미해결 질문
섹션 제목: “미해결 질문”-
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 플래그가 그 갭을 메울 것이다. -
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 레이트의 구체적 상한은 측정되어 있지 않다. -
점수 함수 가중치.
EVAL_WORKER (25, 3.5, …) + (500, 1, …)와EVAL_CONTEXT (50, 1000, …) + (10, 1, …)매크로는 튜닝된 상수다. CBRD-26424 가 이 점이 경험적임을 인정했다. 민감도 surface는 어디에 있는가? 런타임 튜너블 가중치 셋이auto_scaling_window_size를 대체할 수 있는가 — 운영자가 점수를 latency 쪽이나 throughput 쪽으로 편향시키게 해서? -
CBRD-26421 의 검증 갭. 티켓이 명시했듯이, connection-worker 재배치와 동적 스케일링은 자동화 테스트에 잡혀 있지 않다 — connection 풀의 내부 상태가 어떤 사용자 가시 인터페이스 로도 드러나지 않기 때문 이다. controller 소켓은 디버깅용 이다. 읽기 전용
SHOW STATSSQL 또는 DBA-RPC view가 그 테스트 갭을 닫을 것이다. -
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 중? 오늘은 첫 번째다. -
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.hppsrc/connection/connection_context.hppsrc/connection/connection_statistics.hppsrc/connection/connection_support.{cpp,hpp}src/connection/server_support.c—css_push_server_task(line 2354),css_get_task_stats(line 2647)src/connection/tcp.c—setsockopt 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 capsrc/thread/thread_manager.hpp—REGISTER_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와 rowsrc/executables/server.c—cubconn::connection::pool connections;(line 557)
JIRA 티켓
섹션 제목: “JIRA 티켓”- EPIC: http://jira.cubrid.org/browse/CBRD-26177
- Survey: http://jira.cubrid.org/browse/CBRD-26152
- POC: http://jira.cubrid.org/browse/CBRD-26212
- Pool 재설계: http://jira.cubrid.org/browse/CBRD-26255
- Send/recv 예산: http://jira.cubrid.org/browse/CBRD-26392
- 재배치 + 자동 스케일링: http://jira.cubrid.org/browse/CBRD-26406
- Coordinator + freelist: http://jira.cubrid.org/browse/CBRD-26407
newnull-guard: http://jira.cubrid.org/browse/CBRD-26412- 검증 케이스 (보류): http://jira.cubrid.org/browse/CBRD-26421
- 점수 공식: http://jira.cubrid.org/browse/CBRD-26424
- HA 버그: http://jira.cubrid.org/browse/CBRD-26523
- Synonym-enum coredump: http://jira.cubrid.org/browse/CBRD-26544
- Parallel-query CPU 회귀: http://jira.cubrid.org/browse/CBRD-26586
- 스레드 이름 상속: http://jira.cubrid.org/browse/CBRD-26617
- Worker 개수 sweep: http://jira.cubrid.org/browse/CBRD-26636
- Atomic 없는 모니터링: http://jira.cubrid.org/browse/CBRD-26191
- Logical-Wait-Aware (후속 EPIC): http://jira.cubrid.org/browse/CBRD-26662
- 매뉴얼 갱신: http://jira.cubrid.org/browse/CUBRIDMAN-333
교재 참고
섹션 제목: “교재 참고”- 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) 의미론은 필수 사전 독서다.