aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/120-pushing-views.md
blob: 53339b834f03db742cdf61d35cefc6c70903fb16 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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`으로 묶는 것을 잴 수 있다).