From 83286a0722be240690c134d9c6c75b2da6e94e8a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 1 Oct 2026 09:18:23 +0100 Subject: Say what one sending loop showed (017 2b, research 113) The loop alone slowed four links to half: woken by every record queued, it chased the owner a record at a time. Pushing once a turn made links 1.6 times as fast, with threads or the loop alike; the loop's own cost is within the noise. Twenty outputs now add two threads, not 21. --- docs/design/017-apps-on-one-host.md | 28 +++++++-- docs/research/113-one-sending-loop.md | 107 ++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 docs/research/113-one-sending-loop.md (limited to 'docs') diff --git a/docs/design/017-apps-on-one-host.md b/docs/design/017-apps-on-one-host.md index aaaef51..3ffdc0f 100644 --- a/docs/design/017-apps-on-one-host.md +++ b/docs/design/017-apps-on-one-host.md @@ -1,7 +1,7 @@ # 017 — 한 호스트의 여러 앱: 차례, 예산, 실패 경계 -작성일: 2026-10-01. 상태: 1·2단계 구현, 검사로 고정하고 쟀다([111](../research/111-apps-on-one-host.md), -[112](../research/112-sending-between-apps.md)). 2b는 착수 기준을 적었다. 3단계는 설계. +작성일: 2026-10-01. 상태: 1·2단계와 2b 구현, 검사로 고정하고 쟀다([111](../research/111-apps-on-one-host.md), +[112](../research/112-sending-between-apps.md), [113](../research/113-one-sending-loop.md)). 3단계는 설계. [외부 리뷰 004](../outside-feedback/feedback-004.md)의 "구현 방향에 대한 평가" 셋째에서 시작한다. 작은 OS의 운영 모델( [007](007-execution-domains.md))을 Pure가 실제로 갖게 하는 단계다. @@ -175,12 +175,17 @@ counter apps/count.pmod stores/counter **2b: 보내는 루프 하나.** 보냄마다의 스레드(전달선 하나, 링크 하나에 하나씩)를 호스트의 보내는 스레드 하나로 모은다. 출력을 가진 앱은 보냄마다 스레드 하나를 더해 왔다. 앱마다 스레드를 두지 않는다는 원칙에 어긋나는 마지막 자리다. -- 루프 하나(`pure-send`)가 epoll 하나에 링크의 UDP 소켓과 전달선의 TCP 소켓, 깨움 하나를 둔다. 기다리는 시간은 링크들의 다음 때와 - 전달선들의 쉼 가운데 가장 이른 것이다. 출구에 보냄이 쌓이면 루프를 깨운다(자고 있을 때 한 번). +- 루프 하나(`pure-send`, `link/sending.c`)가 epoll 하나에 링크의 UDP 소켓과 전달선의 TCP 소켓, 깨움 하나를 둔다. 보내는 쪽은 + 루프에 들어오며 자기 바퀴(round)와 기다릴 소켓을 준다. 루프는 원하는 것만 돈다. 소켓이 준비됐거나, 밀렸거나, 원한 때가 온 + 것이다. 기다리는 시간은 그 때들 가운데 가장 이른 것이다. +- 소유자는 출구에 보냄을 쌓고, 차례의 것을 다 쌓은 뒤 출구마다 한 번 민다(`pure_link_push`, `pure_wire_push`). 미는 것이 루프를 + 깨운다(자고 있을 때 한 번). 쌓기마다 깨우면 루프가 기록을 하나씩 따라잡으며 작은 패킷을 낸다(아래). - 링크(2b-1): 지금 스레드가 한 바퀴에 하던 일을 루프의 한 바퀴에서 링크마다 한다. 답 받기, 시각, 보내기다. 받는 쪽을 다시 찾는 이름 찾기(DNS)는 그 자리에서 기다린다. 프로브 셋이 답을 못 받았을 때뿐이라 드물지만, 그동안 다른 보냄도 멈춘다. 이것을 밝힌다. -- 전달선(2b-2): 비블로킹 HTTP/1.1 클라이언트의 상태기다(연결, TLS, 요청 쓰기, 답 읽기, 쉼). TLS는 crypto의 함수 표에 비블로킹 - 단계를 더한다. 이름 찾기는 연결할 때 기다린다(링크와 같다). +- 전달선(2b-2): 비블로킹 HTTP/1.1 클라이언트의 상태기다(연결, TLS, 요청 쓰기, 답 읽기, 쉼). 바퀴는 소켓이 받는 데까지 가고, + 소켓이나 마감(아무것도 움직이지 않은 10초)을 기다린다. 답은 오는 대로 읽는다. 머리를 다 받고, 크기가 적힌 몸은 세어 내려가고, + 조각난(chunked) 몸은 자랄 때마다 처음부터 다시 읽는다(드물고 작다). TLS는 crypto의 함수 표(판 6)에 핸드셰이크의 걸음을 + 더했다. 이름 찾기는 연결할 때 기다린다(링크와 같다). **2b의 착수 기준.** @@ -190,6 +195,17 @@ counter apps/count.pmod stores/counter - 통과 조건(RAM의 Pi 5, 전과 후 번갈아): 112의 링크 측정(두 앱, 루프백 링크 하나)과 같은 방식의 전달선 측정(HTTP로 받는 앱)에서 처리량의 중앙값이 5% 넘게 낮지 않다. 링크 넷이 함께 보낼 때 처리량의 합이 10% 넘게 낮지 않다. +### 2b에서 알게 된 것 + +- **깨움의 단위가 문제였다.** 루프만 바꾼 첫 구현(`32f96b3`)은 링크 넷을 초당 58만 건에서 27만 건으로 늦췄다. 쌓기마다 깨우니 + 루프가 소유자의 쌓기를 하나씩 따라잡고 잠들기를 되풀이했다. 루프가 기다린 번이 17.7만, 링크의 바퀴가 70만, 패킷이 9.2만이었다. + 차례마다 한 번 밀고 원하는 링크만 돌게 하자(`6d3c3c2`) 링크 넷이 80만 건이 됐다. 같은 밀기를 링크마다 스레드인 전 구현에 + 붙여도 80만 건이다. 빨라진 것은 밀기 덕이고, 루프 자체의 값은 이 측정의 흔들림(1–3%) 안이다. +- **결과.** 링크와 전달선 스무 개를 가진 호스트의 스레드가 24개에서 5개다(출력이 없으면 3개). 전달선 하나 +0.7%, 넷 −2.3%(보냄 + 하나의 CPU는 같다), 링크 하나 +63%, 넷 +67%. 검사가 모두 초록이다. 커밋: Pure `32f96b3`(링크의 루프), `6d3c3c2`(밀기), + `b6bcd4c`(루프를 따로), `eb3812a`(전달선의 상태기), `56eb228`(측정). +- **남은 값.** 이름 찾기(DNS)가 루프를 멈추고, 전달선 넷의 요청이 차례로 쓰이고 읽힌다. 둘 다 측정에 보일 때 다룬다. + ## 2단계의 착수 기준 - **바꿀 결정:** 같은 호스트의 앱 사이를 링크(소켓, 봉인)가 아니라 안쪽 출구로 건넬지다. 아래를 채우지 못하면 같은 호스트의 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단계(앱의 수명). -- cgit v1.2.3