diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/019-pushing-views.md | 2 | ||||
| -rw-r--r-- | docs/research/120-pushing-views.md | 91 |
2 files changed, 92 insertions, 1 deletions
diff --git a/docs/design/019-pushing-views.md b/docs/design/019-pushing-views.md index e94fd61..bf409ad 100644 --- a/docs/design/019-pushing-views.md +++ b/docs/design/019-pushing-views.md @@ -1,6 +1,6 @@ # 019 — 보기를 미는 길 (SSE) -작성일: 2026-10-04. 상태: 설계와 착수 기준. 1단계 구현, 검사로 고정했다. +작성일: 2026-10-04. 상태: 설계와 착수 기준. 1단계 구현, 검사로 고정하고 쟀다([120](../research/120-pushing-views.md)). [외부 리뷰 005](../outside-feedback/feedback-005.md)의 4장 (2) "실시간 클라이언트 푸시(SSE / WebSocket) 미지원"에서 시작한다. ## 문제 diff --git a/docs/research/120-pushing-views.md b/docs/research/120-pushing-views.md new file mode 100644 index 0000000..53339b8 --- /dev/null +++ b/docs/research/120-pushing-views.md @@ -0,0 +1,91 @@ +# 120 — 보기를 미는 길 + +2026-10-04. [019](../design/019-pushing-views.md)를 잰다. 기준은 구현하기 전에 적어 커밋했고(`f2675f0`), 구현하기 전에 한 번 +고쳐 커밋했다(`84483fe`: 신선도가 같은 둘을 견주게, 미는 간격을 두게, 모듈이 바뀌어도 스트림을 잇게). + +## 쉬운 말로 + +- `pure-serve`가 보기의 값을 민다. 브라우저의 `EventSource('/views/total')`가 그대로 붙는다(`Accept: text/event-stream`). 처음에 지금 + 값이 오고, 값이 바뀔 때마다 새 값이 온다. 같은 보기와 인자를 보는 구독자들은 계산 한 번을 나눈다. +- **신선도.** 구독자 1,000이 보는 카운터를 쓰는 쪽이 초당 1번, 10번, 100번 바꿀 때, 쓰기의 답을 받은 때부터 구독자가 그 값을 보기까지 + p99가 4–11 ms다(기준 20 ms). 절반 넘게는 쓰는 쪽이 답을 받기 전에 구독자가 먼저 본다. +- **값.** 대시보드처럼 초당 1번 바뀌는 값을 1,000이 볼 때, 미는 호스트의 CPU는 100 ms마다 폴링할 때의 약 10분의 1이다(두 묶음에서 + 0.093과 0.104, 여섯 라운드 모두 0.101). 기준(10분의 1 이하)의 경계에 있다. 같은 신선도라면(20 ms 폴링) 초당 10번 바뀔 때 13%, 초당 + 100번 바뀔 때 같은 CPU로 더 신선하다(p99 11 ms 대 20 ms). +- **크기.** 구독자 1만에서 호스트의 메모리는 구독자마다 0.29–0.42 KiB 늘었다(기준 1 KiB). 값 하나가 1만에게 다 닿기까지 p99는 + 37–45 ms다(기준 100 ms). +- **지금의 길.** TPC-C는 +0.3%, 은행 질의의 명령 수는 같다(±0.1%). 질의 하나(in_credit, 0.84 ms)의 시간이 처음 여섯 쌍에서 +4.2%, + 다음 여섯 쌍에서 +2.4%, 열두 쌍 모두 +3.0%였다. 명령 수가 같으니 일이 아니라 자리의 차이다. + +## 결과 + +원시 결과와 명령은 [035-push](../../research/experiments/035-push/)에 있다(`runs.sh`, `push.py`, `counter.aup`, 도구는 +`benchmarks/http/watch.c`). + +**방법.** `pure-serve`(연결 스레드 셋, 저장소는 RAM, 코어 0–2)에 카운터 앱(`counter.aup`). `watch`(코어 3)가 클라이언트 N을 붙이고 +(미는 쪽은 붙자마자 구독), 쓰는 연결 하나가 초당 RATE번 카운터를 올린다(10초). 쓰기 k의 답이 온 때부터, 각 클라이언트가 k 이상의 값을 +본 때까지가 그 쓰기의 신선도다(먼저 봤으면 0). 폴링은 클라이언트마다 MS마다 `GET`(keep-alive, 첫 요청은 첫 MS 안의 아무 때). 호스트의 +CPU는 `/proc/PID/stat`의 utime+stime, 메모리는 클라이언트를 붙이기 전과 후의 VmRSS 차이다. 여섯 가지를 번갈아 세 라운드(`fresh`). + +| 구독자 1,000, 중앙값 | 신선도 p99 | 최대 | 호스트 CPU(10초) | +|---|---:|---:|---:| +| 초당 1번, 밀기 | 4.4 ms | 6.4 ms | 0.13 s | +| 초당 1번, 100 ms 폴링 | 98 ms | 98 ms | 1.40 s | +| 초당 10번, 밀기 | 6.1 ms | 7.7 ms | 0.66 s | +| 초당 10번, 20 ms 폴링 | 19 ms | 29 ms | 5.17 s | +| 초당 100번, 밀기 | 10.8 ms | 19 ms | 5.38 s | +| 초당 100번, 20 ms 폴링 | 19 ms | 41 ms | 5.36 s | + +- 처음 세 라운드(`fresh-first-*`)는 구독자의 소켓을 쓸 수 있을 때마다 알리게 둔 채였다. 그러면 보낸 이벤트마다 상대의 확인이 스레드를 + 깨운다. 보낼 것을 쥔 동안만 알리게 고친 뒤(`watch_subscriber`) 다시 쟀다(위). 초당 1번 밀기의 CPU는 둘 다 0.13 s로 같았다. 비율이 + 0.104에서 0.093으로 바뀐 것은 폴링 쪽이 1.25 s에서 1.40 s로 흔들린 것이다. 그래서 이 기준은 경계에 있다고 적는다. +- 미는 값의 대부분은 커널이다: 초당 1번 바뀔 때 쓰기 하나에 약 13 ms, 구독자 하나에 약 13 µs. 루프백의 TCP 보내기 한 번(받는 쪽의 + 처리까지 보내는 스레드가 한다)의 값이다. +- 초당 100번이면 밀기는 초당 10만 번 보내고, 20 ms 폴링은 초당 5만 번 요청을 받아 답한다. CPU는 같고 밀기가 더 신선하다. 미는 + 간격(`--push-every-ms` 10 ms)이 쓰기의 빠르기와 상관없이 미는 일을 묶는다(초당 100번 쓰기에 미는 일 약 300번, 스레드 셋). + +**구독자 1만**(`many`, 세 라운드, 초당 1번). + +| 라운드 | 신선도 p50 | p99 | 최대 | 호스트 CPU(10초) | 구독자마다 메모리 | +|---|---:|---:|---:|---:|---:| +| 1 | 0 | 37 ms | 45 ms | 1.03 s | 0.42 KiB | +| 2 | 0 | 41 ms | 45 ms | 1.10 s | 0.33 KiB | +| 3 | 20 ms | 45 ms | 51 ms | 1.13 s | 0.29 KiB | + +구독자는 요청을 읽던 연결 기록(6,192 바이트)을 돌려주고 소켓과 구독만 남는다. 구독자 1,000의 런에서 구독자마다 1.5–4 KiB로 보인 것은 +붙는 동안 한꺼번에 생긴 연결 기록이 남은 것이다(돌려준 기록은 다음 연결이 다시 쓴다). 1만이면 그 몫이 묽어진다. + +**지금의 길**(전 `84483fe`, 후 `a7de450` 다음의 `watch_subscriber`를 넣은 빌드). TPC-C(RAM, W=1, 64터미널)와 은행 질의를 번갈아 +여섯 쌍, 은행 질의만 여섯 쌍 더. + +| | 전 | 후 | 바뀜 | +|---|---:|---:|---:| +| TPC-C 중앙값 | 42,542 | 42,679 | +0.3% | +| per_branch(12쌍) | | | +0.3% | +| in_credit(6쌍, 12쌍) | 0.835 ms, 0.84 ms | 0.87 ms, 0.865 ms | +4.2%, +3.0% | +| large(12쌍) | | | +1.3% | +| ranks(12쌍) | | | +1.2% | +| 은행 질의의 명령 수 | | | −0.09–+0.02% | + +in_credit의 후는 열두 쌍 가운데 세 번 1.01–1.14 ms였고 나머지는 전과 같은 0.82–0.87 ms다. + +**검사**(`tests/check.py`의 `server_push`). 구독하면 지금 값이 먼저 온다. 50 ms마다 쓰면 값이 하나도 빠짐없이 차례로 오고, 값을 바꾸지 +않는 쓰기에는 오지 않는다. 몰아 쓴 200번에는 이벤트 6개가 왔고 마지막 값이 왔으며 `id`는 늘기만 했다. 인자를 받는 보기, 표의 쪽(100행), +두 줄의 text(`data:` 두 줄), 계산하는 보기(018). 받기 버퍼를 작게 하고 읽지 않는 구독자에게 큰 표를 600번 바꿨을 때 호스트의 RSS는 +0 MiB 늘었고 기다리던 이벤트가 169–176번 바뀌었으며, 다시 읽자 마지막 값이 왔다. 여러 앱의 호스트에서: 토큰 없이 401, `read`로 +구독. 모듈을 바꾸면 같은 스트림에 새 모듈의 값(×10)이 왔다. 토큰 파일에서 그 토큰을 빼면 스트림이 끝났다. 보기가 없는 모듈로 바꾸면 +끝났다. 구독자가 모두 떠나면 호스트의 구독자 수가 0이다. + +## 판정 + +- **019는 들어간다.** 신선도(20 ms), 크기(1 KiB), 퍼뜨리기(100 ms), 지금의 길(TPC-C, 명령 수)은 기준 안이다. +- 대시보드의 값(폴링의 10분의 1 이하)은 경계에 있다(0.093–0.104). 미는 쪽의 값은 대부분 루프백의 TCP 보내기다. 그래도 폴링보다 + 열 배 가깝게 적은 CPU로 스무 배 신선하다. +- in_credit의 시간은 열두 쌍에서 기준의 경계(+3.0%)다. 명령 수가 같아 일의 차이는 아니다. + +## 다음 + +- 하트비트(프록시가 쉬는 스트림을 끊지 않게), 보기가 읽는 상태가 바뀌지 않은 스냅숏은 계산하지 않기, 긴 보기를 연결 스레드 밖에서 + 계산하기(119). 측정이 요구할 때. +- 구독자가 아주 많으면 보내기의 값(구독자마다 약 13 µs)이 남는다. 같은 이벤트를 여러 소켓에 한 번에 보내는 길은 리눅스에 없다 + (`io_uring`으로 묶는 것을 잴 수 있다). |
