diff options
| author | eunhhu <[email protected]> | 2026-09-30 03:19:45 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-30 03:19:45 +0100 |
| commit | 41cb1cca55c67dd2aac2afa2f96bdc09a5d50b78 (patch) | |
| tree | e2929b52580a1d52544178c18e2f5cfaea50f993 /docs | |
| parent | 385754c2d00ba3f4c74a22f55d4756e101f215b1 (diff) | |
| download | pure-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')
| -rw-r--r-- | docs/design/012-link-protocol.md | 4 | ||||
| -rw-r--r-- | docs/design/013-memory-budget.md | 95 | ||||
| -rw-r--r-- | docs/research/103-review-004-defects.md | 13 | ||||
| -rw-r--r-- | docs/research/104-where-memory-goes.md | 98 |
4 files changed, 210 insertions, 0 deletions
diff --git a/docs/design/012-link-protocol.md b/docs/design/012-link-protocol.md index f2e9820..99d7a91 100644 --- a/docs/design/012-link-protocol.md +++ b/docs/design/012-link-protocol.md @@ -346,3 +346,7 @@ LOST는 그 패킷의 번호를 담아 에폭 0의 키로 봉인한다. 답 run� 갔다. 기록의 상한을 16 MiB로 맞췄다. - 받는 쪽은 피어마다 run 자리를 둘만 두었다. 한 노드의 출력 셋 이상이 같은 노드로 가면 run들이 서로 자리를 밀어내어, 답이 다른 run으로 갔다. 살아 있는 run이 더 오면 자리를 늘린다. +- 앱의 메모리 예산([013](013-memory-budget.md))을 더하며 하나를 더 찾았다. 앱이 가득 차 전달을 거절하면, 받는 쪽은 같은 + 기록을 곧바로 다시 넘겨 한 코어를 태웠고(2초에 거절 27,809번), 보내는 쪽은 받은 것이 1초째 저장되지 않자 창 전체를 매초 + 다시 보냈다(시뮬레이션에서 5초 동안 21,640건). 가득 참 깃발(2)과 다시 넘기는 간격을 더했다. 같은 조건에서 거절은 7번, + 다시 보낸 기록은 0건이다. 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배다 | diff --git a/docs/research/103-review-004-defects.md b/docs/research/103-review-004-defects.md index 3273ab3..e897c41 100644 --- a/docs/research/103-review-004-defects.md +++ b/docs/research/103-review-004-defects.md @@ -42,6 +42,19 @@ 이어받은 뒤 필드를 재배치한 모듈로 바꾸는 경우만 여전히 타입까지만 본다. - **시계 검사의 방법.** 이 기기에서 시스템 시계를 바꿀 권한은 없고, 바꿔서도 안 된다. 그래서 `CLOCK_REALTIME`만 한 시간 뒤로 돌리는 LD_PRELOAD 라이브러리를 썼다. Vega의 검사도 Pure가 만든 이 라이브러리를 쓴다(`make check`가 넘긴다). +- **뒤에 찾은 것(2026-09-30): 시계의 고침이 모자랐다.** + - `run_timers`는 마감을 monotonic clock으로 셌지만, 그 마감까지 소유자가 기다리는 `pthread_cond_timedwait`는 여전히 wall + clock의 시각까지 기다렸다. HTTP 전달의 다시 보내기 간격과, 저장소의 group commit이 함께 커밋할 스레드를 기다리는 2 ms도 + 같았다. + - 이 라이브러리는 프로세스가 읽는 시각만 옮긴다. 그래서 되돌린 시각으로 센 마감은 커널에게 이미 지난 시각이고, 기다림이 + 곧바로 끝나 되풀이된다. 검사는 틱이 이어지는 것만 보았고, 그동안 소유자가 한 코어의 64 %를 쓰는 것은 보지 못했다. + 실제로 시계를 뒤로 돌리면 반대로 기다림이 한 시간 늘어난다. 타이머가 멈추고, 전달이 한 시간 쉬며, 혼자 커밋하는 요청이 + 한 시간 걸린다. + - 고쳤다. 세 기다림 모두 monotonic clock으로 센다(`core/wait.h`). 라이브러리가 시각을 앞으로도 옮길 수 있게 했고(`CLOCK_SHIFT_S`), + check.py `server_clocks`가 한 시간 뒤로, 또 앞으로 옮긴다. + - 고치기 전: 뒤로 옮기면 CPU 159 %, 전달 시도 1.2초에 50,886번. 앞으로 옮기면 틱 1, 전달 시도 1번, 세 요청이 함께 커밋한 + 뒤 혼자 온 커밋이 끝나지 않았다(스무 번째 안에). + - 고친 뒤: 두 방향 모두 틱 59, CPU 0 %, 전달 시도 4번, 혼자 온 커밋 2.6 ms. ## 리뷰의 다른 지적 diff --git a/docs/research/104-where-memory-goes.md b/docs/research/104-where-memory-goes.md new file mode 100644 index 0000000..03749c7 --- /dev/null +++ b/docs/research/104-where-memory-goes.md @@ -0,0 +1,98 @@ +# 104 — 메모리가 어디로 가는가: 느린 수신자와 오래 붙든 스냅숏 + +2026-09-30. [외부 리뷰 004](../outside-feedback/feedback-004.md)의 둘째 권고에서 시작한다. 앱 전체의 메모리 예산을 정하기 +전에, 느린 수신자와 오래 열린 화면 스냅숏이 있을 때 노드의 메모리가 어디로 가는지 잰다. + +착수 기준: + +- **바꿀 결정.** 앱 전체 메모리 예산([013](../design/013-memory-budget.md))이 무엇을 세고, 넘칠 때 무엇을 할지. +- **최소 확인.** 두 조건에서 상주 메모리가 무엇에 비례해 느는지 잰다. 한계가 없는 증가가 있으면, 예산보다 먼저 고친다. +- **중단 기준.** 고친 것이 스냅숏의 읽기(연결 스레드, 표면, 체크포인트)를 하나라도 틀리게 하면 되돌린다. + +## 쉬운 말로 + +- 알아낸 것: + - **스냅숏 하나를 오래 붙들면 메모리가 끝없이 늘었다.** + - 표면 하나가 항목 2만 개의 스냅숏을 붙든 채 글을 고치게 했다. 노드가 30초 만에 6.4 MiB에서 678 MiB가 됐다(고칠 때마다 + 3.5 KB). + - 스냅숏들이 옛것에서 새것으로 사슬을 이루어, 하나를 붙들면 그 뒤의 모든 판을 붙들었기 때문이다. + - 고쳤다. 이제 붙든 스냅숏은 그때의 상태 한 벌만 쥔다. 같은 경우가 6.5 MiB에서 10.4 MiB로 늘고 멈춘다. + - **받는 쪽이 늦으면 보낸 것에 비례해 는다.** 이것은 한계가 아니라 설계다. 보낸 것은 전달될 때까지 앱의 상태이기 때문이다. + - 보내지 못한 한 건마다 outbox에 221 B가 든다(100 B 글의 주문). + - 링크(`pure://`)로 보내면 링크가 따로 사본을 들어 399 B, HTTP로 보내면 534 B다. + - 그래서 예산은 "상태의 상한"과 "넘칠 때 거절"로 정해야 한다. 전송의 사본은 줄일 수 있는 상수배다([013](../design/013-memory-budget.md)). + - **예산을 주면 멈춘다.** 013의 1단계(`--memory`) 뒤, 받는 쪽 없이 보내는 노드에 상태 4 MB를 주었다. 4초 남짓에 차고, + 그 뒤로 보내는 요청은 503 `memory`를 받으며 상주 메모리가 9.6–9.8 MiB에 머문다. 예산이 없으면 30초 동안 45.9 MiB까지 는다. +- 아직 모르는 것: 체크포인트가 쓰는 동안의 메모리는 따로 재지 않았다. + +## 방법 + +[`research/experiments/021-memory/run.py`](../../research/experiments/021-memory/run.py)가 경우마다 `pure-serve`를 띄운다. +클라이언트 넷이 연결을 유지한 채 쉬지 않고 보내고, 0.5초마다 노드의 상주 메모리(`/proc/PID/status`의 VmRSS)와 보낸 수를 +적는다. 끝나면 저장소 크기와 노드의 종료 통계를 남긴다. + +| 경우 | 앱 | 조건 | +|---|---|---| +| outbox | 주문마다 출력으로 보내는 앱 | 출력에 연결이 없다. 보낸 것은 모두 outbox에 남는다 | +| link-down | 같은 앱 | 출력을 아무도 듣지 않는 포트로 링크했다(`pure://`) | +| wire-down | 같은 앱 | 출력을 아무도 듣지 않는 포트로 HTTP 전달한다 | +| unheld | 항목 2만 개(각 100 B 글)를 두고 무작위 항목의 글을 고치는 앱 | 스냅숏을 붙드는 쪽이 없다 | +| held | 같은 앱 | 표면 하나([hold-surface.c](../../research/experiments/021-memory/hold-surface.c))가 항목이 다 든 스냅숏을 끝까지 붙든다 | + +## 결과 + +30초씩, 이 기기(Raspberry Pi 5)에서 잰 값이다(`research/experiments/021-memory/2026-09-30-memory-linux-aarch64.jsonl`, 고치기 +전의 held는 `2026-09-30-held-before-linux-aarch64.jsonl`: `de2860a`의 pure-serve를 `PURE_SERVE`로 주었다). + +| 경우 | 보낸 수 | 상주 메모리 | 한 건마다 | 저장소 | +|---|---:|---|---:|---:| +| outbox | 202,763 | 3.0 → 45.7 MiB | 221 B | 34.7 MB | +| link-down | 198,874 | 8.5 → 84.1 MiB | 399 B | 34.7 MB | +| wire-down | 198,269 | 3.0 → 104.0 MiB | 534 B | 34.7 MB | +| unheld | 206,179 | 6.5 → 6.8 MiB | 1.9 B | 19.3 MB | +| held, 고치기 전(`de2860a`) | 203,791 | 6.4 → 677.8 MiB | 3,455 B | | +| held | 205,354 | 6.5 → 10.4 MiB | 19.9 B | 19.3 MB | + +"한 건마다"는 늘어난 상주 메모리를 보낸 수로 나눈 것이다. + +### 스냅숏의 사슬 + +- 앱의 스냅숏은 옛것이 다음 것의 참조를 쥐는 사슬이었다. 목록이 스냅숏을 뜬 뒤에 놓는 노드와 텍스트는, 그때 가장 새 스냅숏과 + 함께 풀렸다. 그래서 스냅숏이 풀리는 순서가 옛것부터로 정해졌다. +- 그런데 스냅숏 하나를 오래 붙들면 그 뒤의 스냅숏이 모두 붙들렸고, 그동안 놓인 노드와 텍스트가 모두 남았다. 표면, 느린 읽기, + 체크포인트가 스냅숏 하나를 오래 쥐면 노드의 메모리가 쓰기에 비례해 끝없이 늘었다. +- 옛 스냅숏이 읽을 수 있는 것은 자기가 떠진 때 이미 있던 노드뿐이다. 그래서 이렇게 바꿨다(`eebdeea`). + - 스냅숏에 번호(epoch)를 매긴다. 목록의 사본도 그 번호를 받고, 그 뒤에 만든 노드와 텍스트는 그 번호를 적는다. + - 붙들린 스냅숏들은 함께 쓰는 등록부 아래 옛것부터 이어진다. + - 스냅숏이 풀릴 때, 그것과 함께 가던 것 가운데 아직 붙들린 바로 앞 스냅숏이 읽을 수 있는 것(그 번호보다 먼저 만든 것)만 + 그 스냅숏에 넘기고, 나머지는 곧바로 풀어 준다. + - 이음은 등록부의 스핀락 아래서 옮기고, 해제는 그 밖에서 한다. +- 검사: test-module의 `snapshot_held`가 스냅숏 하나를 붙든 채 사건 4만 번과 스냅숏 4만 개를 지나간다. 뒤쪽 2만 번 동안 남은 + 할당은 0 B다. 옛 방식처럼 붙들게 하면 이 검사가 실패한다. 스냅숏 여덟 개를 무작위 순서로 풀며 내용을 대조하는 기존 검사와 + 두 스레드의 읽기 검사도 AddressSanitizer 아래에서 통과한다. ThreadSanitizer는 이 커널(가상 주소 47비트)에서 돌지 않는다. + +### 느린 수신자 + +- 보낸 것은 받는 쪽이 저장했다고 할 때까지 앱의 상태다. 그래서 받는 쪽이 꺼져 있으면 outbox는 보낸 만큼 는다. 막을 수 없고, + 막아서도 안 된다. 정할 것은 상한과 넘칠 때의 행동이다. +- outbox 한 건은 221 B였다. 주문의 글 100 B, 출력 이름과 번호, 목록 노드와 할당의 머리다. +- 전송은 outbox를 한 번 더 복사해 든다. 링크의 보내는 쪽은 자기 ring에(한 건에 178 B 더), HTTP 전달은 자기 큐에(313 B 더) + 둔다. 둘 다 저장소에 이미 있는 것의 사본이다. + +### 예산을 준 뒤 + +013의 1단계를 넣은 뒤(`883d64c`), 같은 앱을 같은 부하로 30초씩 다시 쟀다 +(`research/experiments/021-memory/2026-09-30-budget-linux-aarch64.jsonl`). + +| 경우 | 받아들임 | 거절(503 `memory`) | 상주 메모리 | 상태 바이트 | +|---|---:|---:|---|---:| +| outbox, 예산 없음 | 203,928 | 0 | 3.0 → 45.9 MiB, 계속 는다 | 27,734,208 | +| budget(`--memory 4000000`) | 29,411 | 177,258 | 3.0 → 9.6 MiB, 6초부터 9.6–9.8 MiB | 3,999,896 | + +- 예산은 4초 남짓에 찼다. 그 뒤로 보내는 요청은 곧바로 거절되어, 같은 시간에 받아들임과 거절을 합친 수가 예산 없는 경우와 + 비슷하다. +- 늘어난 상주 메모리는 상태 바이트의 1.72배(예산), 1.62배(예산 없음)다. 보낸 것 한 건이 상태 136 B, 상주 221 B다. 013의 + 중단 기준(3배)보다 낮다. +- 세는 비용: 가장 작은 사건(수 하나를 더함, 저장소 없음)이 상태 1개인 앱에서 125 → 136 ns(+8 %), 32개인 앱에서 321 → + 349 ns(+9 %)다. 처음에는 사건마다 모든 상태를 두 번 걸어 125 → 146 ns(+17 %), 328 → 454 ns(+38 %)였다. 목록과 텍스트만 + 비트로 골라 걷게 했다(`883d64c`). 예산이 없으면 비용이 없다. HTTP로 받는 쓰기는 한 건에 수십 µs라 처리량에 보이지 않는다. |
