# 111 — 한 호스트의 여러 앱: 앱마다 프로세스와 견주어 2026-10-01. [017](../design/017-apps-on-one-host.md)의 1단계를 잰다. 기준은 구현하기 전에 017에 적어 커밋했다(`0b5fa1d`). 비교 상대는 앱마다 프로세스 하나다. - 격리: 앱 A를 64연결의 쓰기로 포화시키는 동안 앱 B(한 연결)의 p99가 두 프로세스일 때의 1.5배를 넘지 않는다(실행 스레드 둘). - 비용: 작은 앱 하나를 더하는 자기만의 메모리가 유휴 프로세스(864 KiB)의 4분의 1 미만이고, 앱 100개까지 스레드가 그대로다. - 처리량: A와 B의 처리량 합이 두 프로세스보다 10% 넘게 낮지 않다. - 앱 하나: TPC-C(RAM, W=1, 64터미널)와 은행 질의를 전(`0b5fa1d`)과 후 번갈아 여섯 쌍 재서 중앙값이 3% 넘게 느려지지 않고, 은행 질의의 명령 수([110](110-counting-instead-of-timing.md)의 `count.py`)가 1% 넘게 늘지 않는다. ## 쉬운 말로 - **통과했다.** A가 몰릴 때 B의 p99는 두 프로세스의 0.5배다. 앱 하나에 120 KiB가 든다(프로세스 하나의 PSS 1,311 KiB의 9%). 둘 다 바쁠 때 처리량과 p99는 두 프로세스와 같다. 앱 하나의 호스트는 전과 같다. - **처음 구현은 처리량을 넘지 못했다.** 짧게 쟀을 때 두 앱이 84k/s, 두 프로세스가 117k/s였다. 실행 스레드가 차례마다 앱을 줄 끝에 다시 세우며 다른 실행 스레드를 깨웠다. 바쁜 앱이 차례마다 코어를 옮겨 다닌 것이다. 다른 앱이 기다리지 않으면 같은 실행 스레드가 잇게 하자 같아졌다(`d95740e`). - **그다음은 꼬리였다.** 두 앱이 다 바쁘면 p99가 늘 4.1 ms였다(두 프로세스 1.5 ms). 한 앱의 연결이 연결 스레드들에 흩어져서, 넘기고 돌려받는 일이 모두-대-모두였다. 요청당 CPU가 3분의 1 더 들고 자발적 문맥 전환이 다섯 배였다. 코어가 붐비니 깨어난 스레드가 커널의 한 틱(HZ=250, 4 ms)을 기다렸다. 앱마다 집 연결 스레드를 두고 연결을 첫 요청에서 그리로 옮기자 1.4 ms가 됐다 (`41502b9`). 프로세스의 1:1 모양을 한 프로세스 안에서 되찾은 셈이다. - **실행량 예산이 표의 줄마다 일을 더하고 있었다.** 앱 하나의 명령 수 기준(1%)을 넘었다(1만 줄 표 +3.0%). 표본은 호스트가 칸마다 실패의 까닭을 묻는 데를 가리켰다. 예산이 없으면 세지 않고, 까닭은 실패했을 때만 쓰게 하자 +0.94%가 됐다(`aef14c0`). - **시간은 여기서도 흔들렸다.** 은행 질의의 시간이 후에 +3–6%로 보인 네 쌍이 있었지만 명령 수는 오히려 줄어 있었다. 110에서 본 배치의 흔들림이다. 판정은 깨끗한 여섯 쌍과 명령 수로 했다. ## 결과 원시 결과와 명령은 [026-apps-on-one-host](../../research/experiments/026-apps-on-one-host/)에 있다(`runs.sh` 단계별, `isolation.py`, `memory.py`; 부하 도구 `benchmarks/http/load`는 경로 앞에 앱의 이름을 붙이는 인자를 받는다). **격리와 처리량**(`isolation.py`, 점수판 앱 둘, RAM, 서버는 코어 0–2, 부하는 코어 3, 15초씩 다섯 라운드, 방식의 순서를 번갈았다. 라운드 사이의 차이는 1–2%였다). 프로세스 둘은 스레드 넷(연결 하나와 소유자 하나씩), 호스트는 스레드 넷(연결 둘, 실행 둘), host1은 셋(연결 하나, 실행 둘)이다. | 중앙값 | 프로세스 둘 | 호스트 | host1 | |---|---:|---:|---:| | B 혼자, p99 | 108 µs | 112 µs | 112 µs | | A가 몰릴 때 B의 p99 | 336 µs | **168 µs (0.50배)** | 544 µs (1.62배) | | A가 몰릴 때 A의 처리량 | 111.1k/s | 104.5k/s | 136.1k/s | | 둘 다 바쁠 때 처리량 합 | 146.2k/s | **146.1k/s (−0.04%)** | 139.5k/s (−4.6%) | | 둘 다 바쁠 때 p99 | 1,408 µs | 1,408 µs | 1,088 µs | host1은 연결 스레드가 하나라 둘 다 바쁠 때 꼬리가 가장 짧지만, A가 몰리면 B가 같은 스레드를 기다린다. 기준은 스레드 수가 같은 호스트로 판정했다. 집 스레드를 두기 전(`d95740e`, `2026-10-01-isolation-d95740e-…`)에는 같은 표가 이랬다. A가 몰릴 때 B의 p99는 232 µs였고, 둘 다 바쁠 때 처리량 합은 145.6k/s, p99는 4,096 µs였다. 그때 두 앱이 둘 다 바쁜 6초 동안 스레드마다 잰 것(한 번 잰 것으로, 파일로 남기지 않았다): | 6초 동안 | 프로세스 둘 | 호스트(집 스레드 전) | |---|---:|---:| | 연결 스레드의 CPU, 자발적 전환 | 3.0 s, 3.3만 (각각) | 4.0 s, 18만 (각각) | | 소유자·실행 스레드의 CPU, 자발적 전환 | 0.82 s, 5.8만 | 1.37 s, 10.5만 | 모으기(저장 뒤에 잠깐 기다리기)를 끈 빌드도 p99가 4,096 µs 그대로여서 원인에서 뺐다. **비용**(`memory.py`, 점수판 앱 N개를 한 호스트에, 앱마다 한 번 쓰고 한 번 읽은 뒤의 `smaps_rollup`, 연결 스레드 하나, 실행 스레드 둘). | | 자기만의 더티 | RSS | PSS | 스레드 | |---|---:|---:|---:|---:| | 프로세스 하나(`--module`) | 1,296 KiB | 2,752 KiB | 1,311 KiB | 2 | | 호스트, 앱 1 | 1,328 KiB | 2,784 KiB | 1,343 KiB | 3 | | 호스트, 앱 10 | 2,384 KiB | 3,840 KiB | 2,399 KiB | 3 | | 호스트, 앱 100 | 13,232 KiB | 14,688 KiB | 13,247 KiB | 3 | 앱 하나에 (13,232 − 1,328) / 99 = 120 KiB다. 기준(216 KiB)의 절반 남짓이다. 프로세스 하나의 자기만의 더티가 087의 864 KiB보다 큰 것은 재부팅 뒤 공유 라이브러리를 함께 매핑한 프로세스가 적어서다(PSS는 같다). **앱 하나**(`runs.sh pairs`, 전은 `0b5fa1d`, 후는 `aef14c0`). | 쌍 | TPC-C 초당 거래(전 → 후) | 거래당 CPU(µs) | 소유자의 코어 몫 | |---|---:|---:|---:| | 1 | 46,682 → 45,754 (−2.0%) | 41.0 → 41.8 | 0.92 → 0.91 | | 2 | 45,195 → 45,762 (+1.3%) | 42.3 → 42.0 | 0.90 → 0.91 | | 3 | 44,967 → 44,905 (−0.1%) | 42.5 → 42.5 | 0.90 → 0.90 | | 4 | 44,086 → 45,181 (+2.5%) | 42.7 → 42.4 | 0.89 → 0.90 | | 5 | 42,992 → 40,751 (−5.2%) | 42.9 → 44.1 | 0.88 → 0.85 (후가 방해받음) | | 6 | 43,066 → 37,280 (−13.4%) | 43.0 → 45.3 | 0.87 → 0.81 (후가 방해받음) | 중앙값 44,527 → 45,043(+1.2%), 거래당 CPU 42.6 → 42.45 µs. 은행 질의의 중앙값은 지점별 합계 +1.7%, 흑자 계좌 수 −1.8%, 조인 −2.6%, 순위 −0.4%다. 017의 커밋마다 짧게 잰 TPC-C(10초, 두 라운드, `2026-10-01-tpcc-by-commit-…`)도 모두 ±3% 안이었다. | 명령 수(한 번 물을 때) | 전 | 후 | |---|---:|---:| | `per_branch` | 27.07M | 26.77M (−1.1%) | | `in_credit` | 3.49M | 3.42M (−1.9%) | | `large` | 1.438M | 1.445M (+0.44%) | | `ranks` | 24.21M | 24.06M (−0.6%) | | `field`(1만 줄, 열 하나) | 8.695M | 8.776M (+0.94%) | | `found`(1만 줄, 키로 찾기) | 15.64M | 15.69M (+0.35%) | `aef14c0` 전(`41502b9`)에는 `field` +3.0%, `large` +1.5%, `found` +1.4%였다(`41502b9/`). 그때의 쌍 넷과, 시간 한도로 끊긴 첫 시도(`interrupted/`)도 남겼다. ## 판정 - **017의 1단계는 들어간다.** 네 기준을 모두 채웠다. 앱 여러 개를 한 프로세스에서 Pure의 차례로 돌린다. - 같은 프로세스 안의 격리는 검증기와 예산과 집 스레드가 만든다. 리눅스 프로세스만큼의 보호(호스트 자신의 결함)는 아니다. ## 다음 - **앱당 120 KiB의 내역.** 모듈(18.7 KB), 소유자의 앱과 연결 스레드마다의 리더(앱 하나에 6 KB씩), 저장소의 매핑, 스냅숏이 있다. 잠든 앱의 리더를 처음 쓸 때 만들면 더 줄 것이다. - **연결 스레드의 수.** 앱이 연결 스레드보다 많으면 한 스레드에 앱 여럿이 산다. 한 앱이 몰리면 같은 집의 앱이 기다린다 (host1의 1.6배가 그 모습이다). 집을 정하는 규칙(지금은 순서)을 부하에 맞출 수 있다. - 017의 2단계(앱 사이와 노드 사이)와 3단계(앱의 수명).