콘텐츠로 이동

C++ messaging bench

이 문서는 규격의 server-driven 모델(§10)을 C++에서 어떻게 구현했는지와 실행 방법을 적는다. 비교 대상·패턴·단위·판정 규칙은 규격이 소유하고, 이 문서는 C++ 고유의 값만 담는다.

1. 비교 대상

구현 request send/command 사용 모듈
grpc-cpp PrepareAsyncEcho + CompletionQueue (unary) PrepareAsyncCommand, Empty reply 시스템 libgrpc++
zlink-cpp raw ROUTER request(peer).message(...).async() command 전용 raw ROUTER send(peer).message(...).async() packaged zlink::cpp
zlink-framework-cpp route_client_t::request_to_channel(...).async<BenchPayload>() route_client_t::send_to_channel(...).async() 저장소 zlink::framework, zlink::framework_codec_protobuf

모든 행은 같은 BenchPayload protobuf DTO와 body 앞의 29-byte header(규격 §6)를 쓴다. raw는 envelope와 protobuf body 두 part를 보내며 request와 command endpoint를 분리한다. framework 행은 RouteMesh용 route_client_t를 쓴다(channel_client_t는 ClientServer 채널용이다).

2. 실행 방법

runner는 빌드하지 않는다. 먼저 빌드하고, 측정은 항상 perf 티켓 큐로 낸다.

# 빌드 — local package root(기본 .artifacts/wsl)에 install/zlink-cpp/0.17.6 과
# install/zlink-core/0.17.5 (릴리스 Core prefix로의 symlink여도 됨)가 있어야 한다.
cmake -S framework/bench/grpc/cpp -B framework/bench/grpc/cpp/build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build framework/bench/grpc/cpp/build --parallel 2

# 전체 matrix 1 run — 항상 perf 티켓 큐로
bash scripts/perf/perf-ticket.sh submit -p 1 -o <owner> -d "cpp with-grpc run 1/3" -- \
  bash framework/bench/grpc/cpp/run_local.sh cpp-s3-run1

# 한 셀
bash framework/bench/grpc/cpp/run_local.sh smoke --implementations zlink-framework-cpp \
  --patterns request-window --payload-sizes 1024 --duration-seconds 2
입력 기본값 의미
첫 인자 cpp-router-1 run label(결과 디렉터리 이름)
BUILD_DIR cpp/build 사전에 빌드한 실행 파일 위치
--implementations / IMPLEMENTATIONS 세 구현 구현 목록
--patterns / PATTERNS 네 패턴 패턴 목록
--payload-sizes / PAYLOAD_SIZES 1024,4096 body 크기(다른 값은 거부)
--duration-seconds / DURATION_SECONDS 5 active 초
--warmup / WARMUP_SECONDS 5 warmup 초
--warmup-segments / WARMUP_SEGMENTS 10 warmup throughput 관측 구간 수
REQUEST_WINDOW, SEND_CONCURRENCY 100, 8 다른 값은 거부
REQUEST_TIMEOUT_MS / DRAIN_BOUND_MS 30000 / 30000 request timeout / drain·settle 상한
COMMAND_SETTLE_MS 200 수신·완료 count 안정 확인 구간
OUTPUT_DIR / --output-dir log/cpp/<stamp>/<label> 결과 디렉터리
RUN_STAMP 현재 시각 run 묶음 ID
LOAD_GATE 2.0 측정 시작 load average 기준

3. 프로세스 구성

source A A 포트(trigger/stats) target B B 포트
grpc-cpp bench_cpp_client — async unary stub 5280/5281 bench_cpp_grpc_server — synchronous ServerBuilder gRPC 5282, stats 5283
zlink-cpp bench_cpp_client — ROUTER + coroutine/poller 5285/5286 bench_cpp_zlink_server — request·command ROUTER 분리 request 5287, command 5288, stats 5289
zlink-framework-cpp bench_cpp_clientroute_client_t 5292/5293 bench_cpp_framework_server — RouteMesh typed echo/count handler RouteMesh 5294, stats 5295

trigger·stats·phase 규칙은 common/bench_stats_server.hpp(Framework public HTTP hosting) 한 곳이 소유한다. trigger client는 runner의 curl이다. 셀 순서는 규격 §10.4이고 셀마다 새 A/B process 쌍을 쓴다. A는 HTTP listener 두 개가 실제로 응답한 뒤 outbound transport를 시작한다(이 머신의 ip_local_port_range=1024 65535에서 outbound socket이 고정 HTTP 포트를 선점하는 문제를 막기 위해서다). framework A는 HTTP용 app과 channel용 app을 한 process에 두고 공통 lifecycle이 순차로 시작한다. Object role은 A/B 모두 None, 고정 RID와 manual peer 연결이다. runner는 시작 전과 셀 종료 뒤 5280-5299의 LISTEN 소켓을 확인한다.

패턴 streams.count streams.inFlightPerStream 구현
request-serial 1 1 application thread 하나의 순차 submit/completion
request-window 1 100 gRPC async call / raw coroutine / framework task의 미완료 집합 100
request-backpressure 1 없음 제출마다 completion pump에 기회를 주며 상한 없이 제출
send-saturation 8 1 gRPC는 stream당 stub, raw는 coroutine slot, framework는 task slot

세 driver 모두 submit/completion 관측은 application thread 하나에서 한다. framework task의 완료는 public await_ready() polling으로 관측하며 그 CPU는 submit_thread_cores에 포함한다. transport completion을 직접 drain하거나 두 번째 poller를 두지 않는다.

4. 언어별로 다르게 둔 값

항목
warmup 5초, 10구간. trigger 원본의 warmup은 ms 단위(5000)
gRPC source channel 하나, logical stream당 unary stub 하나, application thread의 CompletionQueue
gRPC target synchronous ServerBuilder 기본값, insecure loopback
compiler GNU C++ 13.3.0, C++20, Release -O3
gRPC / protobuf 시스템 1.51.1 / 3.21.12
ZLink binding / Core local package 0.17.6 / release 0.17.5
framework 저장소 소스(public CMake target; 측정한 커밋을 원본에 기록)
source 포화 지표 submit_thread_cores(CLOCK_THREAD_CPUTIME_ID); 상한 1
latency 표본 상한 A 200,000개, B 2,000,000개

5. 결과 위치

framework/bench/grpc/log/cpp/<stamp>/<label>/
├── runner.log · report.txt · load-gates.txt
└── <implementation>-<pattern>-<payload>/
    ├── results.json            # with-grpc-cell-v1: role·trigger·streams·target_stats
    ├── source.log / target.log
    ├── warmup-target-stats.json
    └── target-stats.json       # { "snapshot": <B stats 응답> }

두 stats 파일은 진단 snapshot이라 snapshot container로 감싼다(root에 role=target을 두면 집계기가 독립 셀로 오인한다). results.jsoncompleted_at_close·server_received_at_close는 A가 active 경계에서 본 값이고, completedtarget_stats.received는 settle 뒤 최종값이다. 표의 send KMSG/s는 규격대로 settle 뒤 B의 active-header 수신 수 / durationMs이며, drain이 active의 10%를 넘는 행은 drain ms와 소비율을 함께 읽는다(결정 기록 FB-051).

python3 framework/bench/grpc/tools/bench_aggregate.py --lang cpp --judgement-pattern request-window \
  --runs-glob 'framework/bench/grpc/log/cpp/<run-1>/*' \
  --runs-glob 'framework/bench/grpc/log/cpp/<run-2>/*' \
  --runs-glob 'framework/bench/grpc/log/cpp/<run-3>/*' \
  --runs-glob 'framework/bench/grpc/log/c/<c-run>*' --format full

6. 알려진 제약

  • zlink-framework-cpp는 request-serial에서 raw의 약 7%, window에서 요청당 ~1 ms의 직렬화가 보인다(결정 기록 FB-052). 벤치 결함이 아니라 framework C++ runtime의 특성으로 다루며 3-run 값으로 판정한다.
  • zlink-framework-cpp send-saturation: warmup flood 뒤 RouteMesh send target이 사라져 active send가 전부 실패한다(결정 기록 FB-054, FB-012 계열). 결함 수정 전에는 그 행을 오류 셀로만 기록하고 게재하지 않는다.
  • framework HTTP host는 listener bind 실패를 start 결과로 돌려주지 않는다(FB-053). runner는 HTTP 응답 readiness로 listener 시작을 확인한다.
  • HTTP host의 여러 listen 포트에 같은 route가 노출된다. trigger URL은 규격의 trigger 포트만 기록하고 runner도 그 URL만 쓴다.
  • raw 비교는 ROUTER↔ROUTER만 허용한다. request-backpressure는 application in-flight 상한이 없으므로 도달 깊이가 결과다.