aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/111-apps-on-one-host.md
blob: 3086a232157266e037098558904a2cedd58bcea3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 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단계(앱의 수명).