aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-01 01:47:23 +0100
committereunhhu <[email protected]>2026-10-01 01:47:23 +0100
commit3be6f4d31cdb3d6d4c65e0387215c96f0d7345f8 (patch)
treee55c59dfa2fd788f5ec631266c5c4f781b3db328
parent2168466cc9302ce95a5f3381f6c663ca4c180a28 (diff)
downloadpure-3be6f4d31cdb3d6d4c65e0387215c96f0d7345f8.tar.gz
pure-3be6f4d31cdb3d6d4c65e0387215c96f0d7345f8.zip
Say how one host will run many apps, and what it must show (017)
pure-serve runs one app a process: a module, a store, its own threads, port and key, scheduled by Linux. Within one process nothing bounds an event's run (it grows with its lists), the requests waiting for the owner, or one store's sync, which holds every other store's commit since the tree store has one writer's turn per process. 017 makes an app an execution domain (007): a host of many apps (--apps FILE, routes /APP/...) runs them by turns on a fixed number of worker threads, none per app; budgets per app in deterministic units (memory in stored bytes as before, work in instructions counted per block and per item visited, queue in requests: 422 work, 503 busy); writer turns per store; and an app that cannot start or whose save became uncertain stops alone. Stage 1 is all that; links, sends between apps and an app's lifetime come after. Before building: it goes in if an app B's p99 under app A's flood is within 1.5 times what two processes give, an app costs under a quarter of an idle process's own memory with no thread of its own, the pair's throughput is within 10% of two processes', and a one-app host keeps its speed (3% in time, 1% in instructions).
-rw-r--r--docs/design/017-apps-on-one-host.md128
1 files changed, 128 insertions, 0 deletions
diff --git a/docs/design/017-apps-on-one-host.md b/docs/design/017-apps-on-one-host.md
new file mode 100644
index 0000000..c9ee199
--- /dev/null
+++ b/docs/design/017-apps-on-one-host.md
@@ -0,0 +1,128 @@
+# 017 — 한 호스트의 여러 앱: 차례, 예산, 실패 경계
+
+작성일: 2026-10-01. 상태: 설계. 1단계의 착수 기준을 적었다.
+[외부 리뷰 004](../outside-feedback/feedback-004.md)의 "구현 방향에 대한 평가" 셋째에서 시작한다. 작은 OS의 운영 모델(
+[007](007-execution-domains.md))을 Pure가 실제로 갖게 하는 단계다.
+
+## 문제
+
+- **앱 하나가 프로세스 하나다.** `pure-serve`는 모듈 하나와 저장소 하나를 품는다. 앱 열 개는 프로세스 열 개다. 앱마다
+ 연결 스레드, 소유자 스레드, 체크포인트를 쓰는 스레드, 포트, 키를 따로 가진다. 유휴 프로세스 하나가 자기만의 메모리 864 KiB를
+ 쓴다([087](../research/087-small-reads-and-idle-memory.md)). 무엇을 언제 돌리고 누구에게 얼마를 주는지를 Pure가 아니라
+ 리눅스가 정한다. 007은 이것을 출발점으로 삼지 않는다고 정했다.
+- **한 프로세스 안에서도 경계가 없다.**
+ - 시간: 이벤트 하나의 실행량은 정적으로 묶여 있지만 목록 크기에 비례한다(블록 길이 × 항목 수, 목록은 1,600만 항목까지).
+ 한 이벤트가 소유자를 몇 초씩 잡을 수 있다. 보기와 질의도 연결 스레드를 그만큼 잡는다.
+ - 대기열: 소유자에게 넘긴 요청은 한없이 쌓인다(배열을 늘리고, 메모리가 다하면 멈춘다).
+ - 저장: 트리 저장소의 쓰기 차례가 프로세스에 하나다(`store/store-tree.c`의 `writers`). 서로 다른 저장소의 커밋도 동기화까지
+ 서로 기다린다. 한 저장소의 동기화가 오래 걸리면(SD 카드에서는 몇 초도 걸린다) 그동안 다른 저장소는 아무것도 저장하지
+ 못한다.
+
+## 원칙
+
+- **앱은 실행 영역이다**(007). 모듈, 상태와 그 저장소, 입력과 출력, 예산을 가진다. 상태의 소유자는 하나라서 한 앱은 한 번에
+ 한 스레드에서만 돈다.
+- **앱마다 스레드를 두지 않는다**(007). 정해진 수의 실행 스레드가 일이 있는 앱을 차례로 돈다. 앱을 더해도 스레드는 늘지 않는다.
+- **한도는 결정적인 단위로 센다.** 메모리는 저장 형식의 바이트([013](013-memory-budget.md)), 실행량은 명령 수, 대기열은 요청
+ 수다. 같은 앱과 같은 입력이면 어느 기기에서나 같은 사건이 같은 이유로 거절된다. 벽시계로 세면 빠른 기기에서 되던 사건이
+ 느린 기기에서 실패한다.
+- **실패는 그 앱에서 멈춘다.** 모듈을 읽지 못하거나 저장소를 열지 못한 앱은 그 앱만 시작하지 않는다. 저장이 불확실해진 앱은
+ 그 앱만 멈춘다. 다른 앱의 이벤트와 저장은 계속된다.
+- **보호의 범위를 부풀리지 않는다.** 같은 프로세스 안에서 앱을 가르는 것은 검증기(검증된 모듈은 자기 상태 밖을 읽거나 쓰지
+ 못한다, 008)와 예산이다. 호스트 자신의 결함(C 코드의 버그, 할당기의 고갈)은 모든 앱의 운명이다. 이것을 리눅스 프로세스만큼의
+ 격리라고 말하지 않는다.
+
+## 모양
+
+**호스트 구성.** `pure-serve --apps FILE`. 줄마다 앱 하나다(`#`부터 줄 끝은 주석).
+
+```text
+# 이름 모듈 저장소 예산
+board apps/board.pmod stores/board memory=4194304 work=20000000 queue=1024
+counter apps/count.pmod stores/counter
+```
+
+- 이름은 `[a-z0-9_-]`의 1–32자다. 경로의 첫 마디가 된다. 두 앱이 같은 저장소를 가리키면 시작하지 않는다.
+- 예산(모두 없으면 한도 없음, 지금처럼): `memory=` 상태 바이트(013의 `--memory`), `work=` 이벤트나 보기 하나의 실행량,
+ `queue=` 차례를 기다리는 요청 수.
+- 지금의 `--module FILE --store DIR`는 이름 없는 앱 하나다. 경로와 옵션이 그대로다(`/views/NAME`, `--memory`, `--send` …).
+- `--workers N`: 실행 스레드의 수. 앱 하나면 1이다(지금의 소유자 스레드와 같다). 여러 앱이면 상태를 가진 앱 수와 CPU 수 가운데
+ 작은 것이다. 상태 없이 문서만 가진 앱은 지금처럼 연결 스레드가 돌린다.
+
+**경로.** 여러 앱이면 `/앱/views/NAME`, `/앱/inputs/NAME`, `/앱/queries/NAME`, `/앱/objects/KEY`다. 모르는 앱은 404 `no_app`,
+멈춘 앱은 503 `stopped`(지금 `PURE_CLOSED`의 이름)다. 시작할 때 알리는 줄에 앱마다 상태와 까닭이 있다.
+
+**차례.** 연결 스레드는 요청을 그 앱의 대기열에 넘기고, 그 앱이 차례 줄에 없으면 줄 끝에 세운다. 실행 스레드는 줄 앞의 앱을
+가져와 한 차례를 돈다. 때가 된 타이머, 받은 답, 기다리는 요청을 한 번에 적용하고 한 번 저장하고 답을 돌려준다. 일이 남았으면
+그 앱을 줄 끝에 다시 세운다.
+
+- 한 차례가 받는 요청: 줄에 다른 앱이 없으면 기다리는 것을 모두, 있으면 256개까지(연결 스레드의 한 라운드와 같다).
+- 저장 뒤의 모으기(답을 받은 클라이언트의 다음 요청을 잠깐 기다려 한 저장에 묶는 것, 지금의 소유자가 한다)는 줄에 다른 앱이
+ 없을 때만 한다. 기다리는 앱이 있으면 실행 스레드는 그 앱으로 간다.
+- 타이머는 앱마다 다음 때를 가진다. 실행 스레드는 줄이 비면 가장 이른 때까지 잔다.
+
+**실행량 예산(`work=`).** 이벤트 하나, 그리고 보기·질의 하나의 계산마다 센다. 단위는 명령이다.
+
+- 핸들러, 보기, 열, 필터, 키의 블록이 한 번 돌면 그 블록의 길이를 센다.
+- 걷기(EACH)와 집계(FOLD), 걸러진 표의 필터는 도는 항목마다 블록 길이를 센다. 잎 단위로 돌아도(레인) 같다. 범위로 좁혀
+ 일부만 돌면 도는 만큼이다. 모양 있는 집계(그룹·순위, 091)는 패스를 만들 때 항목 수 × 블록 길이를 한 번, 줄마다 키 블록을
+ 센다. 실제로 도는 일을 결정적으로 센다. 명령마다 세지 않으므로 해석기의 안쪽 루프에 비용이 들지 않는다.
+- 넘으면 그 사건은 새 실패 `work`로 실패하고, 실패한 사건처럼 아무것도 바뀌지 않는다. HTTP로는 422 `work`다. 상태가 같으면
+ 다시 해도 같으므로 "잠시 뒤"(503)가 아니다. 앱이 일을 나눠야 한다(조각, 범위).
+- 텍스트를 잇고 고치는 일은 바이트에 비례하고 명령으로 세지 않는다. 그 크기는 메모리 예산이 묶는다.
+
+**대기열 예산(`queue=`).** 앱의 대기열에 기다리는 요청이 한도면 새 요청은 503 `busy`다. 다른 앱의 요청은 받는다. 연결
+스레드의 payload 예산(`--budget`)은 그대로 호스트 전체의 것이다.
+
+**저장의 경계.** 트리 저장소의 쓰기 차례를 저장소마다 둔다. 같은 저장소의 스레드끼리만 차례를 기다린다. 묶음 커밋(그룹)도
+저장소마다다. 기기가 동기화를 한 줄로 세우는 것([084](../research/084-parallel-syncs.md))은 그대로지만, 한 저장소의 멈춘
+동기화가 다른 저장소의 커밋을 잡지 않는다.
+
+**실패 경계.**
+
+- 시작: 모듈을 읽지 못하거나 검증에 실패하거나 저장소를 열지 못한 앱(저장된 상태를 읽지 못하는 것도)은 멈춘 앱이 된다.
+ 나머지는 시작한다.
+- 실행 중: 저장이 불확실해진 앱(지금의 `uncertain`)은 그 뒤의 사건을 거절한다. 그 앱만이다.
+- 끝: 앱마다 센 것(사건, 차례, 거절한 것과 까닭, 저장)을 멈출 때 알린다.
+
+## 단계
+
+1. **한 호스트, 여러 앱.** 위의 모양 전부: 구성, 경로, 차례와 실행 스레드, 실행량·대기열 예산, 저장소마다의 쓰기 차례, 실패
+ 경계. 여러 앱의 구성에서 보냄(`--send`), 봉인, 답, 링크 포트, 표면은 아직 받지 않는다(앱 하나에서는 지금처럼 된다).
+2. **앱 사이와 노드 사이.** 앱마다의 보냄과 답. 링크 포트 하나가 모든 앱의 입력을 받는다(스트림의 input 이름 `앱/입력`, 012의
+ 형식은 그대로). 같은 호스트의 앱끼리는 소켓과 봉인 없이 건넨다(007: 내부 연결은 준비된 관계를 쓴다). 보냄마다의 스레드를
+ 하나의 보내는 루프로 모은다.
+3. **앱의 수명.** 달리는 호스트에 앱을 더하고, 멈추고, 모듈을 바꾼다. 불확실해진 앱을 저장소에서 다시 연다. 앱 경로의 인증과
+ 함께다(CHECKPOINT의 남은 것).
+
+## 1단계의 착수 기준
+
+구현하기 전에 정해 커밋한다.
+
+- **바꿀 결정:** 앱 여러 개를 한 프로세스에서 Pure의 차례로 돌릴지, 앱마다 프로세스를 띄워 리눅스에 맡길지다. 아래를 채우지
+ 못하면 한 프로세스 모양을 다시 본다.
+- **최소 확인:**
+ - 저장소: 한 저장소의 커밋이 동기화 중에 멈춰 있는 동안 다른 저장소가 커밋한다. 같은 저장소의 두 스레드는 여전히 차례다
+ (`store/test-tree-store.c`).
+ - 실행량(test-module): 걷기, 집계(항목 단위·잎 단위·모양 있는 패스), 걸러진 표, 보기가 위의 규칙대로 센다. 넘으면 `work`로
+ 실패하고 아무것도 바뀌지 않는다. 한 사건은 계획(잎 단위인지 항목 단위인지)과 상관없이 같은 양을 센다.
+ - 호스트(`tests/check.py`의 `server_apps`): 앱 셋의 경로와 격리(같은 이름의 상태, 같은 이름의 보기), 재시작 뒤의 상태. 모듈을
+ 읽지 못하는 앱과 저장소를 열지 못하는 앱은 그 앱만 멈춘다. 실행 중 저장이 불확실해진 앱도 그 앱만 멈춘다. 대기열을 넘긴
+ 앱은 503 `busy`이고 다른 앱은 계속 답한다. 실행량을 넘긴 사건은 422 `work`이고 바뀐 것이 없다.
+ - 앱 하나의 호스트는 지금과 같다. 모든 검사가 초록이다.
+- **통과 조건:** RAM 저장소의 Pi 5에서 잰다. 비교 상대는 앱마다 프로세스 하나다.
+ - 격리: 앱 A를 64연결의 쓰기로 포화시키는 동안 앱 B(한 연결, 쓰기)의 지연 p99가, 같은 둘을 두 프로세스로 띄웠을 때의 1.5배를
+ 넘지 않는다(실행 스레드 둘).
+ - 비용: 작은 앱 하나를 더할 때 늘어나는 자기만의 메모리(더티)가 유휴 프로세스 하나(864 KiB)의 4분의 1 미만이다. 앱을
+ 100개까지 더해도 스레드 수는 그대로다.
+ - 처리량: A와 B의 처리량 합이 두 프로세스일 때보다 10% 넘게 낮지 않다.
+ - 앱 하나: TPC-C(RAM, W=1, 64터미널)와 은행 질의를 전과 후 번갈아 여섯 쌍 재서 중앙값이 3% 넘게 느려지지 않고, 은행 질의의
+ 명령 수(110의 `count.py`)가 1% 넘게 늘지 않는다.
+
+## 하지 않는 것
+
+- 이벤트를 중간에 멈추고 다른 앱에 넘기는 선점. 이벤트는 원자적이고 끝까지 돈다. 그 대신 실행량 예산이 한 이벤트의 길이를,
+ 차례의 요청 수가 한 차례의 길이를 묶는다.
+- 실행 스레드에 앱을 고정하거나 앱마다 무게를 주는 스케줄링. 차례 줄 하나로 시작하고, 측정이 요구할 때 더한다.
+- 앱들의 저장을 한 저장소, 한 동기화로 묶는 것. 동기화가 비싼 기기에서 이득일 수 있지만, 한 앱의 망가진 저장소가 모두를
+ 멈추게 된다. 실패 경계를 먼저 두고, 묶음은 따로 잰다.