← 목록
기타 2026-07-09 10KB 읽기 10분

게이트웨이 LuaJIT급 최적화 — 정량 분석·계획·실행

2026-07-09. 요청: 게이트웨이를 LuaJIT보다 빠르게 최적화할 수준 분석·정량계획·실행. 방법: 4방향 병렬 분석(핫패스 프로파일·VM 인터프리터·네이티브패스·정밀벤치, 그림자 실측) + opus 종합. 라이브 무접촉.

현재 실측 (그림자, 라이브 무접촉·PID불변)

지표
단일워커 프록시46.6 RPS · 21.4ms/req (ab, 301라우트)
배선 이전93 RPS · 10.7ms/req
백엔드(파이썬) 자체8409 RPS · 0.18ms/req
nginx 격리(worker 1)19,139 RPS(13B)
c1 ≈ c20 지연 동일, c50 타임아웃완전 단일스레드 직렬화(CPU 100%)

병목 분해 (실측 A/B)

  1. 경로매칭() O(n) 전량스캔 = 9.2ms(43%) — pathRoutes 쓰는 서비스 5/272뿐인데 매 요청 전체 라우트맵(301쌍) 조기종료 없이 순회. 이 한 줄 고치면 46.6→81.9 RPS(+76%) 실측. ← P1 초기 버그, 최근 CORS배선 아님.
  2. CORS(147)+Cache(266)+Auth 정책맵 = ~1.3ms(6%) — no-op 조기반환이라 저비용(최근 배선은 병목 아님).
  3. 나머지 ~10.8ms = 커넥션풀 없는 요청별 신규 소켓연결 + poll/recv 왕복 + 단일 accept 직렬화(I/O 구조).
  4. VM 디스패치 자체 = ~0.23ms(1%) — 병목 아님(원시 51M ops/sec/코어, 18ms에 91만 op 여유).
  5. 정적서빙 wc/test-f 2 fork = 9.3ms(정적경로 32%).

LuaJIT 정직 판정 (2질문 분리)

(A) 인터프리터가 코어당 LuaJIT을 원시속도로 이기나 → 불가능(측정 확정). crownyc는 switch+실함수콜(bl exec_opcode, computed-goto 아님) + 27바이트 큐브값(push/pop마다 27B memcpy) + 24트릿 직렬 캐리 ALU. 동형 산술루프 실측: crownyc 4.59s vs LuaJIT -joff 0.16s(29배), vs LuaJIT JIT 0.015s(362배). 소규모 패치로 못 메우는 구조적 격차. "빠르게 최적화 가능"은 과장.

(B) 게이트웨이가 LuaJIT 프록시급(nginx/OpenResty 수천~수만 RPS/코어) RPS에 도달하나 → 조건부 가능. 단 인터프리터를 빠르게 해서가 아니라, VM이 요청비용의 1%뿐이므로 I/O 데이터플레인을 C 네이티브로 옮겨서(nginx/OpenResty와 동일 구조: 제어평면=한선씨 라우팅판정, 데이터평면=C I/O). VM은 라우팅만 맡기면 이론 ~4,000 RPS/코어, ×워커=수만 RPS 집합.

정량 향상계획 (ROI 순, 3단 천장)

천장1 — 코드레벨(한선씨+저위험C): 46.6→~85-90 RPS/코어(~2x)

  1. 경로매칭 pathRoutes 소배열 분리 → 46.6→81.9 RPS(+76%) 실측 [실행중]
  2. CORS/Cache 도메인 해시인덱싱(맵생성/맵꺼내) → 확장성 보험
  3. _컨텐츠길이 요청당 2회 중복 제거
  4. 정적 wc fork 제거(버퍼파일읽기/네이티브 stat) → 정적 35→~52 RPS [실행중]
  5. exec_opcode 죽은 128KB case340 콜드패스 분리(스택/캐시)
천장2 — 멀티워커(REUSEPORT): per-core 동일, ×코어 → 8코어 ~650 RPS 집합
  1. 워커 N개 병렬 기동. ※일부 벤치서 macOS REUSEPORT 미분산 관찰(단, #8 실측선 2워커 동시성 작동) → 라이브 재검증 필요.
천장3 — I/O 데이터플레인 C 네이티브(고위험·장기): per-worker 수천 RPS → 수만 집합 = LuaJIT 프록시급
  1. 업스트림 커넥션 풀링(connect 제거)
  2. 네이티브 프록시 opcode(splice/sendfile zero-copy + kqueue 이벤트루프). ※macOS는 소켓splice 없음(Linux 전용), 파일→소켓 sendfile만.
  3. computed-goto 디스패치(컴퓨트 워크로드용, 게이트웨이엔 1% 기여 — 후순위).

실행 (이번 세션)

  • Step 1(경로매칭)+Step 4(wc fork) 구현·그림자검증·라이브 컷오버.
  • 나머지는 계획으로 추적(천장2 멀티워커=최대 총처리량 레버, 천장3=LuaJIT급 유일경로지만 헌법상 한선씨 우선과 충돌하는 C 데이터플레인이라 별도 승인 필요).

실행 결과 (2026-07-09 라이브 배선 완료)

Step 1(경로매칭 소배열 분리) 구현·검증·컷오버 완료.

  • 구현: 설정.한선에 모듈전역 _경로소배열 추가. 설정로드()가 pathRoutes 복합키("도메인|접두")만 이 소배열에 적재(요청루프 밖 1회). 경로매칭()이 전체 라우트맵(301쌍) 대신 소배열(~5원소)만 스캔. 호출부(게이트웨이통합.한선:624) 시그니처 불변.
  • 그림자 실측(고속백엔드로 게이트웨이 비용 격리): 46.6 → 102.1 RPS (+119%, 2배 초과), 실패 0. 분석 예측(81.9)보다 상회.
  • 경로매칭 정확성 검증(host백엔드 vs pathRoute백엔드 구분 테스트): host→HOST · /special→PATH · /special/x→PATH(접두매칭) · /other→HOST(폴백) — 4/4 정확. 기능 완전 동일.
  • 라이브 컷오버: /tmp/gwlive.toau 교체(백업 gwlive.bak-preopt.toau) + launchctl kickstart -k org.crowny.gateway. 워커 2개 정상 기동, 8443 stunnel 유지.
  • 라이브 헬스: crowny.org·docs·bank·gateway.crowny.org 전부 200, /dashboard 401(게이트 정상), book.crowny.org 200, HTTPS(8443) 200. 실패 0.
  • 롤백 안전망: /tmp/gwlive.bak-preopt.toau 보존.

Step 4 (정적 wc fork 제거) — 구현·검증·라이브 완료

  • 구현: 게이트웨이SPA정적코어.한선파일바이트수()wc -c 체계 fork(~6ms) 대신 네이티브 파일바이트크기(opcode297, stat 기반 fork0) 호출. crownyc 기존 내장이라 opcode 신설·재빌드 불필요.
  • 정합 검증: gateway.crowny.org 랜딩 Content-Length=32348 = 실제 index.html 바이트 = 다운로드 바이트(3중 일치). 존재X=0 반환이라 존재판정 겸용.
  • 정량: 정적경로 50.1 → 65.7 RPS (+31%) (그림자 A/B). 라이브 정적요청당 fork 1개 제거.
  • 라이브: crowny/docs/bank/gateway/security 전부 200, 정적 Content-Length 정합, HTTPS 200. 컴파일 산출 toau 오히려 축소(fork 명령문자열 제거).

천장2 (멀티워커 REUSEPORT) — 라이브 진단 완료: macOS 미분산 확정

중요 발견: CROWNY_TCP_REUSEPORT=1로 crownyc 워커 2개가 *:8080에 동시 LISTEN하지만, 부하 하 실측 PID 78294=77~79% CPU(전량 처리) · PID 80165=0.0%(완전 유휴). c=1(67) → c=10(62) 스케일 없음, c=30↑ 연결리셋 실패.

  • 원인: macOS/Darwin SO_REUSEPORT는 Linux와 달리 커넥션을 워커 소켓에 로드밸런싱하지 않는다(마지막/특정 바인드가 전량 수신). 이 맥에서 2번째 워커는 죽은 무게, 실효 용량 = 단일워커(~65 정적 / ~102 프록시 RPS).
  • 함의: 천장2(×코어 총처리량)는 Linux(VPS·P2P 노드)에서만 실현 — Linux SO_REUSEPORT는 커널이 분산. 이 게이트웨이의 최종 배치 타깃이 Linux P2P 노드라 프로덕션에선 유효.
  • 맥에서 멀티코어가 필요하면: accept-mutex 또는 SCM_RIGHTS fd-passing 수용자 설계 필요(천장3 인접, 별도 작업). 현 트래픽 수준엔 단일워커로 충분(사전 분석 결론과 일치)이라 즉시 필요성 낮음.
  • REUSEPORT 설정은 유지(Linux 노드용 정본). 맥 로컬 = 사실상 단일워커임을 명시.

천장3-B (순수 한선씨 업스트림 커넥션풀) — 시도 후 기각: 아키텍처 충돌 (부정 결과)

C 데이터플레인(천장3-A)에 앞서 헌법 원칙("먼저 한선씨로")대로 순수 한선씨 워커별 커넥션풀을 구현·검증했다. 결론은 기각이며, 그 이유가 중요한 발견이다.

  • 구현: GW_POOL=1 게이트, 모듈전역 _풀키/_풀소켓, keep-alive 요청재작성, 소켓폴링(0) 헬스체크, CL 정확종료일 때만 반납. 정합성 통과(본문 무결·CL 정확·5연속 정상).
  • 작동 실패 근본원인 (디버그 확정): 게이트웨이 accept 루프는 매 요청 후 메모리복원(_힙마커) + 문자열되감기(_문마커)로 요청 중 할당을 전부 되감는다 — 이것이 요청당 핸들 무한누적(480k 고갈 exit43 → 하루 793회 재기동)을 막는 핵심 안정성 장치. 풀 키/소켓은 요청 중(마커 이후) 할당돼 매 요청 소멸 → 매번 빈 풀 → 재사용 0(백엔드 CONNS=요청수 실측). 격리 테스트로 모듈전역 배열 지속·문자열키 비교는 정상 확인 → 원인은 오직 되감기.
  • 우회 가능성 검증: 마커 이전 사전할당 배열 슬롯의 제자리 정수기록은 되감기 생존(777 확인). 이론상 라우트맵에서 키를 startup에 사전적재하면 가능하나, 소켓 자체가 요청 중 생성돼 되감기 대상일 위험 + 복잡·취약.
  • 결정적 판단 = 페이오프 미미: 모든 업스트림이 loopback(127.0.0.1) → connect 비용 sub-ms. 실측 풀 ON/OFF 차이 +1.9%(주 비용은 connect가 아니라 poll/recv 왕복·단일스레드 직렬화). 원격 업스트림이면 RTT 절감으로 컸겠지만 loopback엔 무의미.
  • 조치: 풀 변경 7건 전부 되돌림(소스 바이트동일 1554588B 복원 확인, 라이브 무변경). loopback + 요청별 되감기 VM에서 커넥션풀은 잘못된 도구로 기록.

천장3-A (I/O 데이터플레인 C 네이티브) — 유일한 LuaJIT급 경로 (미착수, 아키텍처 결정 필요)

천장3-B 기각으로 남는 유일경로. C 이벤트루프(kqueue/epoll) + 커넥션풀 + splice를 VM 되감기 사이클 밖에서 자체 메모리 관리하는 데이터플레인으로 구현. 헌법 준수형(B)은 crownyc.c에 네이티브 프록시 opcode 추가(파일소켓전송 opcode856 확장선) — VM은 라우팅 판정만, C가 I/O. macOS 멀티코어 분산(천장2)도 fd-passing 수용자로 함께 해결. crownyc 정본 재빌드(200+서비스 영향)라 백업→스모크→자동복원 필수. 큰 아키텍처 커밋 — 착수 전 판단 필요.

한 줄 결론

인터프리터로 LuaJIT를 코어당 이기는 건 불가능(29~362배 열세). 그러나 게이트웨이 성능은 인터프리터 속도가 아니라 (a)경로매칭 O(n)버그·(b)단일스레드·(c)커넥션풀 부재가 결정한다. (a)만 고쳐 즉시 +76%, I/O를 C로 내리면 LuaJIT 프록시급 RPS 도달 가능.