aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/design/017-apps-on-one-host.md30
-rw-r--r--docs/research/115-apps-that-change.md115
2 files changed, 139 insertions, 6 deletions
diff --git a/docs/design/017-apps-on-one-host.md b/docs/design/017-apps-on-one-host.md
index f9ff765..3f2ce2b 100644
--- a/docs/design/017-apps-on-one-host.md
+++ b/docs/design/017-apps-on-one-host.md
@@ -1,7 +1,8 @@
# 017 — 한 호스트의 여러 앱: 차례, 예산, 실패 경계
작성일: 2026-10-01. 상태: 1·2단계와 2b 구현, 검사로 고정하고 쟀다([111](../research/111-apps-on-one-host.md),
-[112](../research/112-sending-between-apps.md), [113](../research/113-one-sending-loop.md)). 3단계는 착수 기준을 적었다(2026-10-04).
+[112](../research/112-sending-between-apps.md), [113](../research/113-one-sending-loop.md)). 3단계(수명) 구현, 검사로 고정하고
+쟀다([115](../research/115-apps-that-change.md)). 3b(경로의 인증)는 설계 전.
[외부 리뷰 004](../outside-feedback/feedback-004.md)의 "구현 방향에 대한 평가" 셋째에서 시작한다. 작은 OS의 운영 모델(
[007](007-execution-domains.md))을 Pure가 실제로 갖게 하는 단계다.
@@ -238,17 +239,20 @@ counter apps/count.pmod stores/counter
- 새 줄의 앱은 시작한다. 1단계의 시작과 같다(모듈 검증, 저장소, 배선, 실패하면 그 앱만 멈추고 까닭을 남긴다).
- 없어진 줄의 앱은 멈춘다(아래의 순서). 그 뒤로 그 경로는 404 `no_app`이다.
-- 바뀐 줄의 앱은 멈추고 새 줄로 다시 시작한다. 모듈 파일의 내용, 저장소, 예산, 배선 가운데 하나라도 바뀐 줄이다. 상태는
- 저장소에서 이름으로 이어받는다(지금 다른 모듈로 다시 띄울 때와 같다, `server_upgrade`).
+- 바뀐 줄의 앱은 그 자리에서 바뀐다. 모듈 파일의 내용, 예산, 배선 가운데 하나라도 바뀐 줄이다. 받은 일을 다 적용하고 저장한
+ 뒤, 메모리의 상태를 그대로 두고 모듈을 바꾼다(`pure_app_replace`: 상태와 outbox의 보냄을 이름으로 옮긴다). 저장소는 열린
+ 채이고, 출구만 새 모듈로 다시 만든다. 새 모듈에 상태가 들어가지 않으면 그 앱은 멈추고 까닭을 남긴다.
+- 저장소가 바뀐 줄의 앱, 저장이 불확실해진 앱은 멈추고 저장소에서 다시 연다(상태는 이름으로 이어받는다, `server_upgrade`).
- 그대로인 줄의 앱은 건드리지 않는다. 멈춰 있거나(시작하지 못했거나) 불확실해진 앱만 다시 시작한다. 저장소에서 다시 연다.
-- 다 맞추면 시작할 때와 같은 꼴의 줄 하나를 stdout에 낸다. 앱마다 상태와 까닭, 그리고 이번에 한 일(시작, 멈춤, 다시 시작)이다.
+- 다 맞추면 시작할 때와 같은 꼴의 줄 하나를 stdout에 낸다(`{"reading":N,"apps":[…]}`). 앱마다 상태와 까닭, 그리고 이번에 한
+ 일이다(kept, changed, restarted, started, removed).
- 파일을 읽지 못하거나 틀린 줄이 있으면 아무것도 바꾸지 않고 까닭을 낸다.
HTTP로 앱을 더하는 길을 두지 않는 까닭은 인증이다. 그 길은 경로의 인증(3b)을 먼저 요구한다. 신호는 프로세스의 주인만 보낼 수 있고,
파일은 호스트의 구성을 그대로 남긴다(다시 띄워도 같은 모습이다). 다시 읽기는 호스트의 스레드 하나(`pure-apps`)가 한다. 연결
스레드와 실행 스레드는 기다리지 않는다.
-**멈추는 순서.** 앱 하나를 멈추는 것은 이렇다.
+**멈추는 순서.** 앱 하나를 멈추는 것은 이렇다(그 자리에서 바꿀 때는 1–3까지 하고, 모듈을 바꾼 뒤 이어 간다).
1. 새 일을 받지 않는다. 그 앱의 경로는 503 `stopped`다. 링크와 안쪽 출구의 기록은 받지 않는다(보내는 쪽이 쥐고 다시 보낸다).
2. 소유자가 이미 받은 일을 다 적용하고, 저장하고, 답한다.
@@ -279,11 +283,25 @@ HTTP로 앱을 더하는 길을 두지 않는 까닭은 인증이다. 그 길은
**하지 않는 것.**
-- 끊김 없는 교체(새 모듈을 옆에서 띄우고 넘기기). 멈추고 다시 여는 동안 그 앱은 503을 답한다. 그 시간을 잰다.
+- 끊김 없는 교체(새 모듈을 옆에서 띄우고 넘기기). 바꾸는 동안 그 앱은 503을 답한다. 그 자리에서 바꾸면 1 ms 남짓이다(기록의
+ 모양이 바뀌는 큰 목록은 목록을 다시 쓰는 동안).
- HTTP로 앱을 더하고 바꾸는 길. 경로의 인증(3b) 뒤에 따로 정한다.
**3b: 경로의 인증.** 앱의 경로를 누가 부를 수 있는지다(CHECKPOINT의 남은 것). 3단계의 수명과 따로 설계한다.
+### 3단계에서 알게 된 것
+
+- **멈추고 다시 열면 다시 읽기가 전부였다.** 처음 구현(`12878cf`)은 바뀐 앱을 멈추고 저장소에서 다시 열었다. 항목 100만 개의
+ 앱이 150.6 ms를 답하지 못해 호스트를 다시 띄우는 것(149.7 ms)과 같았다. 그 가운데 142 ms가 저장소를 다시 읽는 일이었다.
+ 저장소가 그대로인 앱을 그 자리에서 바꾸자(`7daf406`) 1.12 ms가 됐다(1,024개 1.06 ms). 기록의 모양이 바뀌는 모듈은 목록을 새
+ 모양으로 다시 쓰느라 171.5 ms다(다시 띄우면 332 ms).
+- **결과.** 한 앱을 거듭 바꾸는 동안 다른 앱의 p99는 0.97배, 앱 하나의 호스트는 TPC-C +0.7%와 명령 수 +0.02% 이하, 모듈을 바꿀
+ 때마다 19 KB가 남는다. 파일을 읽는 스레드가 하나 늘었다(앱 100개에 스레드 넷). 커밋: Pure `14c75d0`(해석한 모듈과 앱의 자리),
+ `1d892e2`(링크 스트림의 입력을 다시 찾기), `12878cf`(SIGHUP과 멈추고 다시 열기), `7daf406`(그 자리에서 바꾸기), `f5e9db5`와
+ 그 뒤(측정).
+- **검사가 찾은 것.** 보내는 앱이 멈추는 동안 받는 앱의 차례가 놓인 출구를 읽던 자리, 시작하기 전의 앱의 락을 다른 앱의 출구가
+ 쓰던 것. 둘 다 바꾸기 한가운데의 보내는 앱과 받는 앱을 함께 바꿔 보는 검사로 드러났다.
+
## 3단계의 착수 기준
- **바꿀 결정:** 달리는 호스트의 앱을 SIGHUP과 구성 파일로 더하고, 멈추고, 바꿀지다. 아래를 채우지 못하면 앱을 바꿀 때 호스트를
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)는 목록을 새 모양으로 다시 쓰는 값이다. 그동안 그 앱만 쉰다.