aboutsummaryrefslogtreecommitdiffstats
path: root/docs/design/017-apps-on-one-host.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/017-apps-on-one-host.md')
-rw-r--r--docs/design/017-apps-on-one-host.md51
1 files changed, 41 insertions, 10 deletions
diff --git a/docs/design/017-apps-on-one-host.md b/docs/design/017-apps-on-one-host.md
index c9ee199..7167e2e 100644
--- a/docs/design/017-apps-on-one-host.md
+++ b/docs/design/017-apps-on-one-host.md
@@ -1,6 +1,6 @@
# 017 — 한 호스트의 여러 앱: 차례, 예산, 실패 경계
-작성일: 2026-10-01. 상태: 설계. 1단계의 착수 기준을 적었다.
+작성일: 2026-10-01. 상태: 1단계 구현, 검사로 고정하고 쟀다([111](../research/111-apps-on-one-host.md)). 2·3단계는 설계.
[외부 리뷰 004](../outside-feedback/feedback-004.md)의 "구현 방향에 대한 평가" 셋째에서 시작한다. 작은 OS의 운영 모델(
[007](007-execution-domains.md))을 Pure가 실제로 갖게 하는 단계다.
@@ -47,26 +47,36 @@ counter apps/count.pmod stores/counter
`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`의 이름)다. 시작할 때 알리는 줄에 앱마다 상태와 까닭이 있다.
+**집 스레드.** 앱마다 연결 스레드 하나가 집이다(`--apps`의 순서를 스레드 수로 나눈 나머지). 연결은 첫 요청에서 그 앱의 집으로
+한 번 옮겨진다(머리를 읽었고 티켓은 아직 없을 때: 집 스레드가 서술자와 읽은 바이트를 받아 요청을 다시 읽는다). 그래서 한 앱의
+요청은 한 스레드에서 소유자를 만난다. 프로세스 하나에 앱 하나일 때와 같다. 연결 스레드의 한 라운드가 그 앱의 요청을 한 번에
+넘기고, 한 차례의 답은 한 스레드로 돌아간다.
+
**차례.** 연결 스레드는 요청을 그 앱의 대기열에 넘기고, 그 앱이 차례 줄에 없으면 줄 끝에 세운다. 실행 스레드는 줄 앞의 앱을
-가져와 한 차례를 돈다. 때가 된 타이머, 받은 답, 기다리는 요청을 한 번에 적용하고 한 번 저장하고 답을 돌려준다. 일이 남았으면
-그 앱을 줄 끝에 다시 세운다.
+가져와 한 차례를 돈다. 때가 된 타이머, 받은 답, 기다리는 요청을 한 번에 적용하고 한 번 저장하고 답을 돌려준다. 일이 남았고
+다른 앱이 기다리지 않으면 같은 실행 스레드에서 다음 차례를 잇는다(코어를 옮기지 않고, 다른 스레드를 깨우지 않는다). 다른
+앱이 기다리면 줄 끝으로 간다.
- 한 차례가 받는 요청: 줄에 다른 앱이 없으면 기다리는 것을 모두, 있으면 256개까지(연결 스레드의 한 라운드와 같다).
- 저장 뒤의 모으기(답을 받은 클라이언트의 다음 요청을 잠깐 기다려 한 저장에 묶는 것, 지금의 소유자가 한다)는 줄에 다른 앱이
없을 때만 한다. 기다리는 앱이 있으면 실행 스레드는 그 앱으로 간다.
- 타이머는 앱마다 다음 때를 가진다. 실행 스레드는 줄이 비면 가장 이른 때까지 잔다.
-**실행량 예산(`work=`).** 이벤트 하나, 그리고 보기·질의 하나의 계산마다 센다. 단위는 명령이다.
+**실행량 예산(`work=`).** 이벤트 하나, 그리고 보기·셀·걸러진 표의 필터 하나의 계산마다 센다. 단위는 명령이고, 쓴 그대로
+센다.
-- 핸들러, 보기, 열, 필터, 키의 블록이 한 번 돌면 그 블록의 길이를 센다.
-- 걷기(EACH)와 집계(FOLD), 걸러진 표의 필터는 도는 항목마다 블록 길이를 센다. 잎 단위로 돌아도(레인) 같다. 범위로 좁혀
- 일부만 돌면 도는 만큼이다. 모양 있는 집계(그룹·순위, 091)는 패스를 만들 때 항목 수 × 블록 길이를 한 번, 줄마다 키 블록을
- 센다. 실제로 도는 일을 결정적으로 센다. 명령마다 세지 않으므로 해석기의 안쪽 루프에 비용이 들지 않는다.
+- 핸들러와 보기와 열의 블록이 돌기 시작하면 그 블록의 길이를 센다.
+- 걷기(EACH)와 집계(FOLD), 걸러진 표의 필터는 시작할 때 블록 길이 × 그 목록(조각을 받으면 그 조각)의 항목 수를 한 번에
+ 센다. 계획이 무엇을 돌든 같다. 잎 단위(레인)든, 범위로 좁히든, 모양 있는 집계(그룹·순위, 091)의 패스를 다시 쓰든 쓴
+ 그대로다. 패스는 만들어 둔 것을 다음 계산이 다시 쓰므로, 실제로 도는 일을 세면 같은 요청이 그때그때 다른 양이 된다.
+- 넘을 일은 시작하기 전에 거절된다. 명령마다 세지 않으므로 해석기의 안쪽 루프에 비용이 들지 않는다. 예산이 없으면 세지도
+ 않는다.
- 넘으면 그 사건은 새 실패 `work`로 실패하고, 실패한 사건처럼 아무것도 바뀌지 않는다. HTTP로는 422 `work`다. 상태가 같으면
다시 해도 같으므로 "잠시 뒤"(503)가 아니다. 앱이 일을 나눠야 한다(조각, 범위).
- 텍스트를 잇고 고치는 일은 바이트에 비례하고 명령으로 세지 않는다. 그 크기는 메모리 예산이 묶는다.
@@ -82,7 +92,8 @@ counter apps/count.pmod stores/counter
- 시작: 모듈을 읽지 못하거나 검증에 실패하거나 저장소를 열지 못한 앱(저장된 상태를 읽지 못하는 것도)은 멈춘 앱이 된다.
나머지는 시작한다.
-- 실행 중: 저장이 불확실해진 앱(지금의 `uncertain`)은 그 뒤의 사건을 거절한다. 그 앱만이다.
+- 실행 중: 저장이 불확실해진 앱(지금의 `uncertain`, 앱마다의 깃발이다)은 그 뒤의 사건을 거절한다. 그 앱만이다. 다른
+ 프로세스가 한 앱의 저장소를 쥐면 그 앱만 저장하지 못한다(503 `store_busy`, 적용한 것 없음).
- 끝: 앱마다 센 것(사건, 차례, 거절한 것과 까닭, 저장)을 멈출 때 알린다.
## 단계
@@ -119,6 +130,26 @@ counter apps/count.pmod stores/counter
- 앱 하나: TPC-C(RAM, W=1, 64터미널)와 은행 질의를 전과 후 번갈아 여섯 쌍 재서 중앙값이 3% 넘게 느려지지 않고, 은행 질의의
명령 수(110의 `count.py`)가 1% 넘게 늘지 않는다.
+### 1단계에서 알게 된 것
+
+- **앱이 차례마다 실행 스레드를 옮겨 다니면 비싸다.** 처음 구현은 차례가 끝날 때마다 앱을 줄 끝에 세우고 다른 실행 스레드를
+ 깨웠다. 바쁜 두 앱이 두 프로세스보다 28% 덜 썼다(짧게 잰 한 번). 다른 앱이 기다리지 않으면 같은 실행 스레드가 잇는다.
+- **연결 스레드가 앱을 섞으면 프로세스보다 비싸다.** 한 앱의 연결이 연결 스레드들에 흩어지면 넘기고 돌려받는 일이
+ 모두-대-모두가 되어 요청당 CPU가 3분의 1 더 들고, 둘 다 바쁠 때 p99가 커널의 한 틱(4 ms)이 됐다. 집 스레드가 그것을 없앴다.
+ 설계에 없던 것이다.
+- **실행량은 쓴 그대로 센다.** 처음 설계는 실제로 도는 일(패스, 범위)을 세려 했다. 패스는 만들어 둔 것을 다음 계산이 다시
+ 쓰므로 같은 요청이 그때그때 다른 양이 된다. 계획과 상관없는 쓴 그대로를 세고, 넘을 일은 시작할 때 거절한다. 레인 대조
+ 3,600건이 계획 있는 앱과 없는 앱의 셈이 같은지 본다.
+- **세는 것도 비용이다.** 예산이 없어도 세고 칸마다 까닭을 물었더니 표의 줄마다 26 명령이 늘었다(앱 하나의 1% 기준을 넘었다).
+ 예산이 없으면 세지 않고, 까닭은 실패했을 때만 쓴다.
+- **저장의 경계는 저장소의 정체로 가른다.** 쓰기 차례를 경로 문자열로 가르면 한 디렉터리를 두 경로로 열 때 두 스레드가 함께
+ 쓴다(파일 잠금은 프로세스의 것이다). 장치와 inode로 가른다.
+
+측정과 판정은 [111](../research/111-apps-on-one-host.md)에 있다. 네 기준을 모두 채웠다(격리 0.50배, 앱당 120 KiB와 스레드 그대로,
+처리량 −0.04%, 앱 하나 +1.2%와 명령 +0.94% 이하). 커밋: Pure `cac499d`(저장소마다의 차례), `38fde9d`(실행량 예산), `d8fcd53`(여러
+앱과 실행 스레드), `d95740e`(바쁜 앱은 제 실행 스레드에), `eb9a3a5`(부하 도구), `41502b9`(집 스레드), `aef14c0`(예산이 없으면
+세지 않는다).
+
## 하지 않는 것
- 이벤트를 중간에 멈추고 다른 앱에 넘기는 선점. 이벤트는 원자적이고 끝까지 돈다. 그 대신 실행량 예산이 한 이벤트의 길이를,