aboutsummaryrefslogtreecommitdiffstats
path: root/docs/design/013-memory-budget.md
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-30 03:19:45 +0100
committereunhhu <[email protected]>2026-09-30 03:19:45 +0100
commit41cb1cca55c67dd2aac2afa2f96bdc09a5d50b78 (patch)
treee2929b52580a1d52544178c18e2f5cfaea50f993 /docs/design/013-memory-budget.md
parent385754c2d00ba3f4c74a22f55d4756e101f215b1 (diff)
downloadpure-41cb1cca55c67dd2aac2afa2f96bdc09a5d50b78.tar.gz
pure-41cb1cca55c67dd2aac2afa2f96bdc09a5d50b78.zip
Say where a node's memory goes, and the budget it now keeps (104, 013)
104 measures a node's memory under a receiver that is down and under a snapshot held for long (021's run.py: resident memory against what was posted, 30 s a case). A held snapshot grew the node without end (6.4 to 678 MiB, fixed in d1e8f38); a slow receiver grows it by what was sent, 221 B a send, which is state until delivered, so the budget bounds state and refuses past it. 013 is that budget's design: what it counts (state bytes, deterministic), what an event over it does, why an answer takes its send's room, how links and HTTP wait, what it does not count and those bounds, and the checks. With --memory 4000000 the same load fills it in about 4 s and the node stays at 9.6-9.8 MiB (45.9 MiB and growing without). 012 records the hole the budget found in the link (the full flag), and 103 that its clock fix fell short (385754c). CHECKPOINT gains the row.
Diffstat (limited to 'docs/design/013-memory-budget.md')
-rw-r--r--docs/design/013-memory-budget.md95
1 files changed, 95 insertions, 0 deletions
diff --git a/docs/design/013-memory-budget.md b/docs/design/013-memory-budget.md
new file mode 100644
index 0000000..fe8736a
--- /dev/null
+++ b/docs/design/013-memory-budget.md
@@ -0,0 +1,95 @@
+# 013 — 앱 전체 메모리 예산
+
+작성일: 2026-09-30. 상태: 1단계 구현.
+[외부 리뷰 004](../outside-feedback/feedback-004.md)의 둘째 권고에서 시작한다. 측정은 [104](../research/104-where-memory-goes.md)에 있다.
+
+지금의 `--budget`은 요청 payload의 접수만 묶는다. 앱의 목록, outbox, 붙들린 스냅숏, 전송의 사본은 아무것도 묶지 않는다. 작은
+기기에서 오래 도는 노드라면 무엇이 얼마나 쓰는지 정해져 있어야 하고, 넘칠 때 무엇을 하는지도 정해져 있어야 한다.
+
+## 결정
+
+앱마다 메모리 예산이 하나 있다(`pure-serve --memory BYTES`). 지금은 노드 하나가 앱 하나다.
+
+**예산이 세는 것: 상태 바이트.**
+
+- 목록 상태의 항목: 저장 형식의 바이트다(수 8 B, 글 4 B와 그 길이). 목록이 늘 알고 있는 값이다.
+- 텍스트 상태의 길이
+- outbox(보냈지만 받는 쪽이 아직 저장하지 않은 것)와 들은 번호 목록
+
+이 수는 결정적이다. 같은 앱과 같은 입력이면 어느 기기에서나 같다. 상주 메모리는 이것의 약 1.6–1.8배다(104: outbox 한 건이
+상주 221 B에 상태 136 B, 4 MB 예산에서 1.72배). 예산을 상주 메모리로 세면 할당기와 기기에 따라 달라지고, 검사할 수 없다.
+
+**넘칠 때: 상태를 늘리는 사건을 거절한다.**
+
+- 사건이 끝났을 때 상태 바이트가 예산을 넘고 사건 전보다 늘었으면, 그 사건은 fault `memory`로 실패한다. 바꾼 것은 다른 실패한
+ 사건처럼 모두 되돌린다.
+- 상태를 줄이거나 그대로 두는 사건, 보기, 읽기는 예산을 넘은 채로도 된다. 그래야 줄일 수 있다.
+- HTTP로는 503 `memory`다. 요청 payload의 예산(503 `payload_budget`)처럼 "잠시 뒤 다시"의 뜻이다.
+- 다른 노드에서 온 전달이 이렇게 거절되면, 들은 번호로 세지 않는다. 앱이 거절한 메시지(한 번 전달되고 끝남)와 달리, 이것은
+ 일시적이다. 보내는 쪽이 다시 보낸다.
+ - 링크(`pure://`): 받는 쪽이 기록을 쥔 채 100 ms에서 두 배씩 500 ms까지 쉬었다가 다시 넘긴다. 그동안 보내는 쪽에 앱이 가득
+ 찼다고 알린다(STORED의 깃발 2, [012](012-link-protocol.md)). 보내는 쪽은 쥐고 있는 것을 다시 보내지 않는다.
+ - HTTP: 503 `memory`를 받은 보내는 쪽이 100 ms에서 두 배씩 10초까지 쉬었다가 다시 보낸다(전부터의 규칙).
+
+**답은 자기 보내기의 자리를 쓴다.** HTTP로 보낸 것의 답(`--reply`)은 그 보내기를 outbox에서 지우는 일과 한 사건이다
+(`pure_app_answer`).
+
+- 따로 두면 멈출 수 있다. outbox가 예산을 채운 앱에서, 받은 것을 적어 두는 답은 예산을 넘으니 거절된다. 그런데 보내기는 답이
+ 적용된 뒤에야 outbox를 떠난다. 그러면 답도 보내기도 영영 그대로다.
+- 한 사건이면 답은 보내기가 쥐던 자리만큼 늘 수 있다. 그보다 많이 쥐는 답은 예산을 넘으면 기다린다. 잃지 않고, 두 번
+ 적용되지 않으며, 그 뒤의 답도 순서대로 기다린다. 앱이 자리를 비우면(줄이는 사건) 100 ms 안에 들어간다.
+
+**느린 수신자: outbox도 상태다.**
+
+- 받는 쪽이 늦거나 꺼져 있으면 outbox가 예산을 채운다. 그 뒤로는 보내는 사건이 거절된다. 이것이 앱의 클라이언트까지 닿는
+ 역압(backpressure)이다.
+- 받는 쪽이 저장해 outbox가 줄면 다시 받는다.
+- 보낸 것을 버리거나 디스크로만 넘기지 않는다. 보낸 것은 전달될 때까지 앱의 상태이고, 목록은 메모리의 상태이며 저장소는 그
+ 기록이다. 메모리보다 큰 데이터는 문서(키로 여는 모음, 트리 저장소)가 맡는다. 문서는 저장소에 있고 읽기는 빌린 extent라서,
+ 예산에 들지 않는다.
+- 두 노드가 서로에게 보내고 둘 다 가득 차면, 서로 상대가 비우기를 기다린다. 링크의 outbox는 받는 쪽이 저장하면 비므로 요청이
+ 응답을 기다리지는 않는다. 그러나 응답을 받는 사건이 늘기만 하면 두 앱이 함께 멈출 수 있다. 응답을 받는 사건이 요청의
+ 기록을 지우게 쓰면 그 사건은 줄이는 사건이라 늘 된다. 런타임이 대신 풀 수 없는 앱의 설계다.
+
+**세지 않는 것과 그 상한.** 007의 원칙대로 세지 않는 것은 그 상한을 적는다.
+
+| 무엇 | 상한 | 이유 |
+|---|---|---|
+| 붙들린 스냅숏 | 붙든 판마다 상태 한 벌. 붙드는 쪽(연결 스레드, 표면, 체크포인트)이 H이면 최악 (1 + H) × 예산 | 104에서 고쳤다. 전에는 한이 없었다 |
+| 전송의 사본 | 링크 ring과 HTTP 큐가 outbox를 한 번 더 든다(한 건에 0.8배, 1.4배) | 2단계에서 창으로 묶는다 |
+| 요청 payload | `--budget` | 지금의 접수 예산 |
+| 체크포인트 | 이미지를 쓰는 동안 스냅숏 하나 | 붙들린 스냅숏의 한 경우 |
+
+## 단계
+
+1. 상태 바이트를 세고, `--memory`로 예산을 두고, 넘칠 때 사건을 거절한다. 전달의 거절은 들은 번호로 세지 않는다.
+2. 전송의 창: 링크와 HTTP 전달이 outbox 전체가 아니라 창만큼만 받는다. 전달되면 다음을 받는다.
+3. 체크포인트가 쓰는 동안의 메모리를 잰다.
+
+## 검사
+
+- test-module `memory_budget`:
+ - 예산을 넘기는 사건이 거절되고 되돌려진다.
+ - 줄이는 사건은 넘은 채로도 된다.
+ - outbox가 차면 보내는 사건이 거절되고, 덜어 내면 다시 된다.
+ - 전달의 거절은 들은 번호를 올리지 않는다. 자리가 나서 다시 오면 한 번 적용된다.
+ - 보내기보다 많이 쥐는 답은 예산에서 통째로 거절되고(보내기가 outbox에 남는다) 자리가 나면 된다. 적게 쥐는 답은 예산에서도
+ 된다. 다시 시작한 뒤에도 그대로다.
+- test-link `full`: 앱이 5초 동안 가득 찬다. 그동안 기록을 다시 넘긴 것은 12번이고, 자리가 난 뒤 0.5초 안에 받는다. 보내는
+ 쪽이 다시 보낸 기록은 0건이다(깃발 2가 없으면 21,640건).
+- check.py `server_memory`:
+ - `--memory`를 준 노드의 링크를 아무도 듣지 않는 곳으로 둔다. 13번째 뒤의 주문이 503 `memory`를 받고, 보기는 읽힌다. 받는
+ 쪽이 뜨면 outbox가 줄고 다시 200이다.
+ - 받는 쪽을 가진 것보다 작은 예산으로 다시 띄운다. 링크의 전달과 클라이언트의 주문이 거절되고, 비우는 사건은 된다. 비우면
+ 기다리던 주문이 모두 한 번씩 들어온다. 가득 찬 2초 동안 거절은 7번이다(기다림이 없으면 27,809번).
+- check.py `server_answers_memory`: 답이 보내기보다 큰 영수증을 남긴다. 영수증이 예산을 채우면 답이 기다리고 주문이 503
+ `memory`를 받는다. 비우면 기다리던 답이 모두 한 번씩 적용되고, API는 주문마다 한 번씩만 불렸다.
+- 021의 run.py `budget`: 상태 4 MB가 4초 남짓에 차고, 상주 메모리가 9.6–9.8 MiB에 멈춘다(예산 없이 30초에 45.9 MiB).
+
+## 중단 기준
+
+| 조건 | 결정 |
+|---|---|
+| 상태 바이트를 세는 비용이 사건마다 측정 가능하게 든다(작은 쓰기의 처리량이 잡음 밖으로 준다) | 셈을 목록 안으로 옮긴다. 가장 작은 사건에서 +38 %였던 것을 세는 상태만 걷게 해 +9 %로 줄였다. 처리량에는 보이지 않는다(104) |
+| 거절된 전달이 반복해 돌며 CPU를 쓴다 | 전달에 기다림을 둔다(1단계에서 일어났고, 두었다) |
+| 예산 안에서 상주 메모리가 상태 바이트의 3배를 넘는다 | 세는 것을 다시 본다. 잰 값은 1.72배다 |