aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/115-apps-that-change.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/research/115-apps-that-change.md')
-rw-r--r--docs/research/115-apps-that-change.md115
1 files changed, 115 insertions, 0 deletions
diff --git a/docs/research/115-apps-that-change.md b/docs/research/115-apps-that-change.md
new file mode 100644
index 0000000..a76f9dd
--- /dev/null
+++ b/docs/research/115-apps-that-change.md
@@ -0,0 +1,115 @@
+# 115 — 달리는 호스트의 앱을 더하고, 멈추고, 바꾸기
+
+2026-10-04. [017](../design/017-apps-on-one-host.md)의 3단계를 잰다. 기준은 구현하기 전에 017에 적어 커밋했다(`e4c7f7e`).
+
+- 바꿀 결정: 달리는 호스트의 앱을 SIGHUP과 구성 파일로 더하고, 멈추고, 바꿀지. 채우지 못하면 앱을 바꿀 때 호스트를 다시 띄운다.
+- 통과 조건(RAM의 Pi 5):
+ - 바꾸는 앱이 답하지 못하는 시간이 호스트를 다시 띄울 때보다 짧다(항목 1,000개와 100만 개).
+ - 한 앱을 거듭 바꾸는 동안 다른 앱(한 연결)의 p99가 바꾸지 않을 때의 1.5배를 넘지 않는다.
+ - 앱 하나의 호스트는 그대로다(TPC-C와 은행 질의의 중앙값이 3% 넘게 느려지지 않고, 명령 수가 1% 넘게 늘지 않는다).
+ - 바꿀 때마다 남는 메모리를 밝힌다.
+
+## 쉬운 말로
+
+- **통과했다.** 호스트가 SIGHUP에 `--apps` 파일을 다시 읽고, 줄이 없어진 앱은 멈추고, 새 줄의 앱은 시작하고, 바뀐 줄의 앱은 그
+ 자리에서 바꾼다. 항목 100만 개의 앱이 모듈을 바꾸는 동안 답하지 못한 시간은 1.1 ms다(호스트를 다시 띄우면 150 ms). 그동안
+ 다른 앱의 p99는 그대로다(0.97배). 앱 하나의 호스트도 그대로다.
+- **처음 구현은 큰 앱에서 기준을 넘지 못했다.** 앱을 멈추고 저장소에서 다시 여는 것으로 바꿨다. 100만 항목의 앱은 150.6 ms를
+ 답하지 못했다. 호스트를 다시 띄우는 것(149.7 ms)과 같았다. 시간을 나눠 보니 거의 전부(142 ms)가 저장소를 다시 읽는
+ 데(`pure_app_start`) 들었다. 호스트를 다시 띄울 때도 같은 일을 하니 짧아질 수 없었다.
+- **고친 것은 다시 읽지 않는 것이다.** 저장소가 그대로인 앱은 받은 일을 다 적용하고 저장한 뒤, 메모리의 상태를 그대로 두고
+ 모듈만 바꾼다(코어의 `pure_app_replace`: 상태와 outbox의 보냄을 이름으로 옮긴다). 출구만 새 모듈로 다시 만든다. 그러자 크기와
+ 상관없이 1 ms 남짓이 됐다. 기록의 모양이 바뀌는 모듈(필드가 하나 늘어난 기록)은 목록을 새 모양으로 다시 만들고 이미지를 다시
+ 써야 해서 172 ms가 들었다. 호스트를 다시 띄우면 다시 읽기에 그 일까지 더해 332 ms다.
+- **모듈을 바꿀 때마다 19 KB가 남는다.** 바뀐 모듈을 놓지 않는다. 그 모듈로 해석된 요청과 스냅숏이 아직 그것을 가리킬 수 있기
+ 때문이다. 예산이나 배선만 바뀌면 남는 것이 없다.
+
+## 결과
+
+원시 결과와 명령은 [030-app-lifecycle](../../research/experiments/030-app-lifecycle/)에 있다(`runs.sh`의 단계 `build`, `window`,
+`isolation`, `pairs`, `counts`, `memory`; `window.py`, `isolation.py`, `memory.py`, `pairs.py`; 앱 `items.aup`과 그 다음 판
+`items-next.aup`(보기 하나 더), `items-wide.aup`(기록에 필드 하나 더)).
+
+**답하지 못하는 시간**(`window.py`). 호스트(`--apps`, 저장소는 RAM, 코어 0–2)에 앱 하나가 항목을 든다(사건 하나가 목록을 두 배로
+늘려 1,024개나 1,048,576개). 클라이언트(코어 3)가 한 연결로 쓰기를 쉬지 않고 보내고, 답을 받은 두 쓰기 사이의 가장 긴 틈을 잰다.
+바꾸기는 줄이 다른 모듈 파일을 가리키게 하고 SIGHUP을 보내는 것이다(두 모듈을 번갈아). 다시 띄우기는 같은 변경을 전처럼 하는
+것이다(줄을 바꾸고 SIGTERM, 다시 시작). 1,024개는 바꾸기 20번과 다시 띄우기 10번, 1,048,576개는 10번과 5번의 중앙값이다.
+
+| 앱, 바뀌는 것 | 멈추고 다시 열기(`12878cf`) | 그 자리에서(`7daf406`) | 호스트를 다시 띄우기 |
+|---|---:|---:|---:|
+| 1,024개, 보기 하나 더 | 1.52 ms | **1.06 ms** | 2.42 ms |
+| 1,024개, 기록에 필드 하나 더 | 1.58 ms | **1.31 ms** | 2.51–2.64 ms |
+| 1,048,576개, 보기 하나 더 | 150.42 ms | **1.12 ms** | 149.63–149.94 ms |
+| 1,048,576개, 기록에 필드 하나 더 | 337.33 ms | **171.54 ms** | 329.85–332.42 ms |
+
+다시 띄우기의 범위는 두 빌드에서 잰 값이다. 처음 잰 것(`f5e9db5`)은 다시 띄울 때 모듈을 바꾸지 않았다(1,024개 1.59 대 3.05 ms,
+100만 개 150.64 대 149.73 ms). 비교가 같은 변경이 되도록 고쳐 다시 쟀다.
+
+멈추고 다시 열 때 100만 항목의 시간을 나누면, 멈추기 7–8 ms(상태를 놓기), 시작 142 ms(저장소를 다시 읽기)다. 호스트를 다시
+띄울 때의 시작은 170 ms였다. 그 자리에서 바꾸면 이 둘이 없다. 기록의 모양이 바뀌면 목록 100만 항목을 새 모양으로 다시 만들고
+이미지를 쓰는 데 약 170 ms가 든다. 다시 띄울 때도 같은 일을 다시 읽기에 더해서 한다.
+
+**다른 앱의 꼬리**(`isolation.py`). 같은 호스트(연결 스레드 둘, 실행 스레드 둘)에 100만 항목의 앱 A와 점수판 B. B에 한 연결의
+점수를 10초 보내는 동안 A를 그대로 두거나 바꾼다. 바꾸는 쪽은 보기 하나 더인 모듈로 0.2초마다(판마다 50번), 기록에 필드 하나
+더인 모듈로 0.4초마다(판마다 18번, 한 번에 약 170 ms의 일)다. 다섯 라운드, 순서를 번갈아.
+
+| B의 p99 중앙값 | 그대로 | 바꾸는 동안 | 비 |
+|---|---:|---:|---:|
+| 보기 하나 더, 0.2초마다 | 120 µs | 116 µs | 0.97 |
+| 기록에 필드 하나 더, 0.4초마다 | 120 µs | 116 µs | 0.97 |
+
+B는 5xx를 하나도 받지 않았다. 바꾸는 일은 파일을 읽는 스레드(`pure-apps`)에서 하고, 실행 스레드는 그 앱의 차례만 쉰다.
+
+**앱 하나의 호스트**(`runs.sh pairs`, 3단계 전 `e4c7f7e`와 후 `7daf406`, TPC-C W=1 64터미널 20초와 은행 질의, 여섯 쌍 번갈아,
+부하 평균 0.9–1.5).
+
+| | 전 | 후 |
+|---|---:|---:|
+| TPC-C 초당 거래 중앙값 | 42,900 | 43,220 (+0.7%) |
+| 거래당 CPU | 44.4 µs | 44.4 µs |
+| `per_branch`, `in_credit`, `large`, `ranks` | 5.31, 0.85, 0.38, 6.81 ms | +0.1%, −2.4%, 0.0%, −0.7% |
+
+쌍마다 TPC-C는 −3.8%에서 +6.2%로 흔들렸다(소유자의 코어 몫 0.87–0.91). 명령 수(025의 `count.py`, 전과 후를 두 번씩):
+
+| 한 번 물을 때 | 전 | 후 |
+|---|---:|---:|
+| `per_branch` | 26.980M | 26.827M (−0.57%) |
+| `in_credit` | 3.471M | 3.437M (−0.99%) |
+| `large` | 1.445M | 1.445M (+0.01%) |
+| `ranks` | 24.170M | 24.087M (−0.35%) |
+| `none`, `field`, `found` | 0.607M, 8.776M, 15.691M | +0.00%, +0.00%, +0.02% |
+
+요청이 해석한 모듈을 들고 다니고 소유자와 보기가 그것을 견주는 일(원자적 읽기와 포인터 비교)은 보이지 않는다.
+
+**남는 메모리**(`memory.py`). 항목 1,000개의 앱을 100번 바꿔 데운 뒤, 1,000번 더 바꾼 앞뒤의 자기만의 더티 메모리.
+
+| 바꾸는 것 | 앞 → 뒤 | 한 번에 |
+|---|---:|---:|
+| 모듈(두 파일을 번갈아, 바꿀 때마다 하나를 읽는다) | 3,120 → 21,952 KiB | 19,284 B |
+| 큐 예산만(모듈은 그대로) | 1,200 → 1,200 KiB | 0 B |
+
+한 번에 남는 것은 바뀐 모듈 하나(구조체 18,760 B와 모듈의 바이트)다.
+
+**검사**(`tests/check.py`의 `server_apps_lifecycle`). 한 호스트에서 앱을 더하고(바로 답한다), 지우고(404 `no_app`, 다른 프로세스가 그
+저장소를 연다), 바꾼다. 받는 앱 둘(같은 호스트의 앱이 안쪽 출구로, 다른 노드가 링크로 보내는)과 보내는 앱 둘을 보내기 한가운데에서
+바꾼다(바꿀 때 보낸 수가 0과 600 사이임을 단언한다). 받는 앱의 새 모듈은 입력의 번호가 다르고 보냄을 이름으로 받는다. 그동안
+목록 보기를 쉬지 않고 읽는다. 600건씩이 모두 한 번씩 순서대로 도착하고, 200을 받은 쓰기는 한 번씩 적용되고 503 `stopped`를 받은
+쓰기는 적용되지 않으며, 보기는 200이나 503 `stopped`만 답하고, 그대로인 앱은 하나도 실패하지 않는다. 저장소를 옮긴 앱은 새 저장소로
+다시 시작하고, 저장소를 만들 수 없던 앱은 고친 뒤의 SIGHUP에 시작하고, 틀린 줄이 있는 파일은 아무것도 바꾸지 않고, `kill -9` 뒤에는
+파일 그대로 시작한다. 혼자 10번, CPU 부하 아래 5번, ASan으로 빌드한 `pure-serve`로 6번 지났다.
+
+이 검사가 처음 구현에서 찾은 것: 보내는 앱이 멈추는 동안 받는 앱의 차례가 그 출구를 놓은 뒤에 읽는 자리(`local_done`), 앱이
+시작하기 전에 다른 앱의 출구가 그 소유자의 락을 쓰는 것(락을 자리와 함께 만든다).
+
+## 판정
+
+- **017의 3단계(수명)는 들어간다.** 네 조건을 모두 채웠다. 달리는 호스트의 앱은 SIGHUP과 파일로 더하고, 멈추고, 바꾼다.
+- 바꾸기는 그 자리에서 한다. 저장소가 바뀐 앱, 저장이 불확실해진 앱, 달리지 않던 앱만 저장소에서 다시 연다.
+- 파일을 읽는 스레드(`pure-apps`)가 여러 앱의 호스트마다 하나 늘었다. 앱 100개의 호스트는 스레드 넷이다(111에서 셋).
+
+## 다음
+
+- **바뀐 모듈을 놓기.** 지금은 바꿀 때마다 19 KB가 남는다(하루 한 번 바꾸면 한 해 7 MB). 놓으려면 그 모듈로 해석된 요청이
+ 끝났는지(본문을 받는 중인 요청은 `convert`에서 모듈을 다시 읽는다)와 연결 스레드의 리더가 다른 모듈로 옮겼는지를 알아야 한다.
+- **3b: 경로의 인증.** 앱의 경로를 누가 부를 수 있는지. 그 뒤에 HTTP로 앱을 더하고 바꾸는 길.
+- 기록의 모양이 바뀌는 큰 목록의 교체(100만 항목 172 ms)는 목록을 새 모양으로 다시 쓰는 값이다. 그동안 그 앱만 쉰다.