aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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% 넘게 늘지 않는다.
+
+## 하지 않는 것
+
+- 이벤트를 중간에 멈추고 다른 앱에 넘기는 선점. 이벤트는 원자적이고 끝까지 돈다. 그 대신 실행량 예산이 한 이벤트의 길이를,
+ 차례의 요청 수가 한 차례의 길이를 묶는다.
+- 실행 스레드에 앱을 고정하거나 앱마다 무게를 주는 스케줄링. 차례 줄 하나로 시작하고, 측정이 요구할 때 더한다.
+- 앱들의 저장을 한 저장소, 한 동기화로 묶는 것. 동기화가 비싼 기기에서 이득일 수 있지만, 한 앱의 망가진 저장소가 모두를
+ 멈추게 된다. 실패 경계를 먼저 두고, 묶음은 따로 잰다.