aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/113-one-sending-loop.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/research/113-one-sending-loop.md')
-rw-r--r--docs/research/113-one-sending-loop.md107
1 files changed, 107 insertions, 0 deletions
diff --git a/docs/research/113-one-sending-loop.md b/docs/research/113-one-sending-loop.md
new file mode 100644
index 0000000..1dd4ded
--- /dev/null
+++ b/docs/research/113-one-sending-loop.md
@@ -0,0 +1,107 @@
+# 113 — 보내는 루프 하나: 링크와 전달선을 스레드 하나에
+
+2026-10-01. [017](../design/017-apps-on-one-host.md)의 2b를 잰다. 기준은 구현하기 전에 017에 적어 커밋했다(`3791ab8`).
+
+- 바꿀 결정: 보냄(링크 하나, 전달선 하나)마다의 스레드를 프로세스의 보내는 루프 하나로 모을지.
+- 최소 확인: 링크·전달·답·봉인·`server_apps_links`의 검사가 모두 초록. 링크 열과 전달선 열로 보내는 호스트의 스레드가 출력이
+ 없는 호스트보다 둘(보내는 루프, 링크를 받는 스레드)만 많다(전에는 스물하나).
+- 통과 조건(RAM의 Pi 5, 전과 후 번갈아): 링크 하나와 전달선 하나의 처리량 중앙값이 5% 넘게 낮지 않다. 링크 넷이 함께 보낼 때
+ 처리량의 합이 10% 넘게 낮지 않다.
+
+## 쉬운 말로
+
+- **통과했다.** 링크와 전달선 스무 개를 가진 호스트의 스레드가 24개에서 5개가 됐다(출력이 없으면 3개). 전달선은 전과 같다
+ (하나 +0.7%, 넷 −2.3%, 보냄 하나의 CPU는 같다). 링크는 빨라졌다(하나 +63%, 넷 +67%).
+- **루프만 바꾼 첫 구현은 느렸다.** 링크 넷이 초당 58만 건에서 27만 건이 됐다. 루프가 자는 동안 쌓인 기록마다 깨어났기 때문이다.
+ 소유자는 기록을 하나씩 쌓고, 루프는 그것을 따라잡고 잠들기를 되풀이했다. 깰 때마다 모든 링크를 돌아, 링크마다 패킷이 작아졌다
+ (패킷 하나에 기록 20건에서 12건). 패킷이 많으니 받았다는 답도, 깨움도, 소유자에게 알림도 많았다.
+- **고친 것은 루프가 아니라 경계였다.** 소유자가 차례의 보냄을 다 쌓은 뒤 출구마다 한 번 민다(push). 루프는 원하는 링크만 돈다
+ (답이 왔거나, 밀렸거나, 때가 됐다). 그러자 링크 넷이 80만 건이 됐다. 같은 밀기를 링크마다 스레드인 전 구현에 붙여도 80만
+ 건이다. 빨라진 것은 밀기 덕이고, 루프 자체의 값은 이 측정의 흔들림(1–3%) 안이다(링크 하나 −2.8%, 넷 +0.2%). 스레드
+ 셋이 줄었다.
+- 전달선은 블로킹 HTTP 클라이언트에서 비블로킹 상태기가 됐다(연결, TLS 핸드셰이크, 쓰기, 답 읽기). 그 과정에서 TLS의 함수
+ 표가 핸드셰이크를 한 걸음씩 하게 됐다.
+
+## 결과
+
+원시 결과와 명령은 [028-one-sending-loop](../../research/experiments/028-one-sending-loop/)에 있다(`runs.sh`의 단계 `build`,
+`links`, `wires`, `threads`; `sends.py`, `threads.py`, 앱 `src.aup`, `dst.aup`, `many.aup`; 링크마다 스레드에 밀기만 붙인
+`threads-push.patch`).
+
+**방법.** 한 호스트(`--apps`, 연결 스레드 하나, 실행 스레드 둘, RAM, 코어 0–2)에 앱 `src`가 항목 1,000개를 들고, `burst` 사건
+하나가 그것을 걸으며 출력 하나(`out`)에, 또는 넷(`o1`–`o4`)에 하나씩 보낸다. 링크는 호스트가 자기 `--link-port`로 봉인해
+보내고, 전달선은 자기 HTTP 포트로 보낸다. 받는 앱(`d1`–`d4`)마다 출력 하나다. 한 판은 `burst` 1,000번(보냄 100만 건, 넷이면
+400만 건)을 차례로 보내고, 모든 받는 앱이 다 저장할 때까지의 시간과 호스트의 CPU를 잰다. 받는 앱의 합이 모든 보냄이 한 번씩
+왔는지 본다. 다섯 라운드, 빌드의 순서를 라운드마다 돌렸다. 부하 평균은 첫 판 0.5–1.0, 그 뒤로는 측정 자신이 만든 2.0–2.8이다.
+
+**링크**(`2026-10-01-links-…`). 빌드 넷: 링크마다 스레드(`3791ab8`), 거기에 밀기(`threads-push.patch`), 루프만(`32f96b3`), 루프와
+밀기(`6d3c3c2`). 중앙값:
+
+| | 스레드 | 스레드와 밀기 | 루프 | 루프와 밀기 |
+|---|---:|---:|---:|---:|
+| 링크 하나(건/초, µs/건) | 596,795, 2.96 | 923,011, 1.91 | 525,093, 3.29 | 896,986, 1.92 |
+| 링크 넷 | 506,173, 3.58 | 798,600, 2.26 | 267,735, 6.84 | 800,265, 2.24 |
+| 링크 넷의 패킷 | 169,233 | 64,239 | 426,383 | 64,064 |
+| 링크 넷의 스레드 | 8 | 8 | 5 | 5 |
+| 전달선 하나(코드는 그대로) | 788,620, 2.25 | 772,398, 2.29 | 781,221, 2.25 | 794,427, 2.24 |
+
+루프만의 링크 넷은 다섯 판이 212k–387k로 흔들렸다. 깨움이 기록을 따라 생기니 두 스레드의 엇갈림에 따라 달라진다.
+
+**루프만일 때 무슨 일이 있었나**(한 번 잰 것으로, 파일로 남기지 않았다. 링크 넷, `burst` 200번, 보냄 80만 건). 루프에 셈을
+넣은 빌드로 쟀다.
+
+| | 루프만 | 루프와 밀기(모든 링크를 돌던 때) |
+|---|---:|---:|
+| 루프가 기다린 번 | 176,547 | 7,502 |
+| 그중 기다리지 않은 번(쌓인 것이 있어서) | 83,562 | 52 |
+| 깨움으로 깬 번 | 75,767 | 647 |
+| 링크의 바퀴 | 706,175 | 29,995 |
+| 소유자에게 알림 | 17,426 | 3,583 |
+| 패킷 | 91,866 | 15,270 |
+| 건/초 | 257,202 | 651,438 |
+
+스레드마다 나눠 보면(같은 조건) 링크마다 스레드일 때 링크 스레드들이 합해 user 0.17 s, sys 0.29 s, 자발적 전환 2.7만 번이었다.
+루프만일 때 루프 하나가 user 0.68 s, sys 1.15 s, 9만 번이었다. 소유자의 자발적 전환은 3.8만 번에서 16.3만 번이 됐다.
+
+**전달선**(`2026-10-01-wires-…`). 빌드 셋: 스레드(`3791ab8`), 링크만 루프와 밀기(`6d3c3c2`), 전달선도 루프(`eb3812a`). 중앙값:
+
+| | 스레드 | 링크만 루프 | 전달선도 루프 |
+|---|---:|---:|---:|
+| 전달선 하나(건/초, µs/건) | 773,787, 2.30 | 781,585, 2.27 | 778,944, 2.25 (+0.7%) |
+| 전달선 넷 | 629,470, 2.75 | 625,265, 2.78 | 615,065, 2.74 (−2.3%) |
+| 전달선 넷의 스레드 | 7 | 7 | 4 |
+| 링크 하나 | 581,019, 3.00 | 921,686, 1.90 | 948,267, 1.87 (+63%) |
+| 링크 넷 | 484,872, 3.72 | 805,687, 2.21 | 811,087, 2.21 (+67%) |
+
+전달선 넷의 −2.3%는 보냄 하나의 CPU가 같은 채로 생겼다. 넷의 요청을 스레드 넷이 따로 쓰고 읽던 것을 루프 하나가 차례로 하니,
+겹치던 몫이 줄었다. 기준(10%) 안이다.
+
+**스레드**(`2026-10-01-threads-…`, `threads.py`). 앱 `many`가 링크 열(호스트의 링크 포트로 `d1`–`d10`에)과 전달선 열(호스트의
+HTTP 포트로 `d11`–`d20`에, 봉인해서)에 하나씩 보내고, 모두 받은 뒤 센다.
+
+| | 출력 없음 | 링크 열, 전달선 열 |
+|---|---:|---:|
+| 스레드(`3791ab8`) | 3 | 24 |
+| 루프(`eb3812a`) | 3 | 5 |
+
+늘어난 둘은 보내는 루프(`pure-send`)와 링크를 받는 스레드(`pure-listen`)다.
+
+**검사.** `make check`가 모두 초록이다. 전달선의 검사(웹훅의 HTTPS와 믿지 못할 인증서, 답, 메모리, 창, 이름으로 받기,
+업그레이드, 봉인, 시계를 옮긴 재시도)는 블로킹 클라이언트를 상태기로 바꾼 뒤 첫 실행에서 모두 지났다.
+
+## 판정
+
+- **017의 2b는 들어간다.** 프로세스의 링크와 전달선은 보내는 루프 하나(`link/sending.c`)가 보낸다. 앱과 출력을 더해도 스레드는
+ 늘지 않는다.
+- 밀기(소유자가 차례의 보냄을 다 쌓고 한 번 깨우기)는 루프와 상관없이 링크를 1.6배로 만들었다. 깨움의 단위가 기록이 아니라
+ 차례여야 한다. 루프는 그것을 드러냈을 뿐이다.
+
+## 다음
+
+- **전달선 넷의 −2.3%.** 넷의 요청이 차례로 쓰이고 읽힌다. 루프가 하나라는 값이다. 바쁜 호스트에서 이것이 커지면 루프를 둘로
+ 나누는 것(출구를 루프에 나눠 배정)을 잰다.
+- **이름 찾기(DNS)가 루프를 멈춘다.** 링크가 받는 쪽을 다시 찾을 때와 전달선이 연결할 때 `getaddrinfo`가 그 자리에서 기다린다.
+ 드물지만, 그동안 모든 보냄이 멈춘다. 이름을 찾는 일을 루프 밖으로 빼는 것은 이 문제가 측정에 보일 때 한다.
+- **출구 셋의 모양.** 호스트의 출구(전달선, 링크, 안쪽 출구)가 같은 일(쌓기, 밀기, 쌓은 번호, 저장된 번호, 쥔 바이트)을 세
+ 갈래로 한다(`serve/serve.c`의 `feed_outlets`). 하나의 출구 꼴로 줄일 수 있다.
+- 017의 3단계(앱의 수명).