aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/106-tpcc-conditions-first.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/research/106-tpcc-conditions-first.md')
-rw-r--r--docs/research/106-tpcc-conditions-first.md177
1 files changed, 175 insertions, 2 deletions
diff --git a/docs/research/106-tpcc-conditions-first.md b/docs/research/106-tpcc-conditions-first.md
index a0eae9d..919bcb4 100644
--- a/docs/research/106-tpcc-conditions-first.md
+++ b/docs/research/106-tpcc-conditions-first.md
@@ -2,8 +2,8 @@
2026-09-30. [외부 리뷰 004](../outside-feedback/feedback-004.md)의 넷째 권고에서 시작한다. [092](092-tpcc.md)의 비교는
창고 하나(W=1)였고, SQLite는 트랜잭션마다 커밋했다. 리뷰는 SD 카드에서의 약 90배가 대부분 그룹 커밋과 로그 쓰기 구조에서
-온다고 읽었다. 그래서 SQLite도 묶어 커밋하게 하고, 창고를 넷으로 늘려 다시 잰다. 아래는 측정 행렬을 돌리기 전에 정했다
-(하니스를 확인하려고 5초짜리 시험을 몇 번 돌린 뒤다).
+온다고 읽었다. 그래서 SQLite도 묶어 커밋하게 하고, 창고를 넷으로 늘려 다시 잰다. 아래 착수 기준은 조율과 측정 행렬을
+돌리기 전에 정해 커밋했다(`7f71c85`, 하니스를 확인하려고 5초짜리 시험을 몇 번 돌린 뒤다). 결과를 본 뒤에 고치지 않았다.
착수 기준:
@@ -26,6 +26,26 @@
`mmap_size` 1 GiB, 연결마다 캐시 64 MiB, `temp_store=MEMORY`(SAVEPOINT의 하위 저널을 메모리에)를 견준다. SD 카드에서는
페이지 크기 4,096 B와 1,024 B를 견준다. 묶어 커밋하는 실행의 처리량이 높은 쪽을 쓴다.
+## 쉬운 말로
+
+- **SD 카드에서의 차이는 90배에서 4–11배로 줄었지만, 2배를 훨씬 넘게 남았다.**
+ - 092의 SQLite는 기본 설정으로 트랜잭션마다 커밋했다. 이번에는 SQLite도 Pure처럼 기다리는 거래를 묶어 한 번에 커밋하고,
+ 카드에 맞게 조율했다(페이지 1 KiB, 체크포인트는 WAL 40 MB마다). SQLite가 스스로 10배 넘게 빨라졌다.
+ - 그래도 Pure가 같은 터미널 수에서 4.2–10.8배다(W=1 64터미널: 4,980건 대 739건).
+ - 남은 차이는 거래당 장치에 쓰는 바이트에서 온다. SQLite는 행 하나를 바꿔도 그 행이 든 페이지를 통째로 쓴다(거래당 7.6–15
+ KB). Pure는 바뀐 필드와 더한 행만 기록한다(0.7–1.3 KB). 8–18배다.
+- **RAM에서는 Pure가 3.2–3.5배다.** 조율한 SQLite의 최고(10,357건)에 대해 Pure는 36,567건(W=1)이다. RAM에서는 아낄 동기화가
+ 없어서, 묶어 커밋해도 SQLite가 빨라지지 않는다. Pure는 HTTP와 JSON까지 하면서도 거래당 CPU가 SQLite의 절반이다.
+- **창고가 넷이어도 맞았다.** 같은 거래 2만 건 뒤 세 쪽(Pure, SQLite, 묶어 커밋하는 SQLite)의 모든 표가 같았다. 원격 재고와
+ 원격 결제가 들어간다. 모든 실행 뒤 일관성 조건이 성립했고, 실패한 거래는 없었다.
+- **RAM에서 Pure의 한계는 소유자 스레드 하나다.** W=4에서 터미널을 16에서 64로 늘려도 처리량이 늘지 않았고, 소유자가 코어의
+ 94%를 썼다. 서버의 세 코어 가운데 1.74개만 썼다. 소유자의 바쁜 시간 가운데 약 3분의 1은 스냅숏이 공유한 노드를 복사하고
+ 놓는 데 간다. SD 카드에서는 소유자가 코어의 7–15%만 쓴다(카드가 한계다).
+- **메모리는 기준 안이다.** Pure는 모든 상태를 메모리에 든다. 상주가 상태 바이트의 1.6–2.2배였다(기준 2.5배). 같은 실행에서
+ SQLite는 최고 상주가 190 MiB 아래였고, 데이터는 파일(최대 124 MiB)에 있다. 메모리를 더 쓰는 것이 Pure의 값이다.
+- 아직 모르는 것: 레코드를 기록하는 다른 저장소(RocksDB 같은 LSM)와는 견주지 않았다. SD 카드에서의 이점은 레코드 로그가
+ 페이지 WAL보다 적게 쓰는 데서 오므로, 그런 저장소와의 차이는 더 작을 것이다.
+
## 방법
[`benchmarks/tpcc/run.py`](../../benchmarks/tpcc/run.py)와 [`sqlite-tpcc.c`](../../benchmarks/tpcc/sqlite-tpcc.c)에 SQLite의
@@ -42,3 +62,156 @@
- **행렬.** RAM(`/dev/shm`)과 SD 카드(ext4), 각각 W=1과 W=4. Pure 16·64터미널, SQLite 1·4스레드, 묶어 커밋하는 SQLite
16·64터미널. RAM은 20초, SD 카드는 30초씩이다. SD 카드는 상태에 따라 흔들리므로 세 번 돌려 범위를 적는다.
- 서버와 SQLite는 코어 0–2, Pure의 부하 생성기(HTTP/1.1, JSON)는 코어 3이다. SQLite는 프로세스 안에서 돈다.
+- 실행마다 앞에서 `sync`로 쌓인 쓰기를 비운다(적재와 복사가 남긴 것이 먼저 도는 쪽의 몫이 되지 않게). Pure는 제 실행이
+ 끝나면 멈춘다. 그 체크포인트가 SQLite의 실행 동안 장치를 쓰지 않게 하려는 것이다.
+- 같은 거래 2만 건의 확인은 W마다 RAM에서 한 번 했다. 장치와 상관없는 확인이고, SD 카드에서 한 터미널로 거래마다 동기화하며
+ 돌리면 SQLite 쪽만 몇 분이 걸린다. SD 카드의 실행도 끝날 때마다 clause 3.3.2를 확인했다.
+
+## SQLite 설정 고르기
+
+W=1에서 SQLite만 돌렸다([원시 결과](../../research/experiments/022-tpcc-grouped/), `tune-`로 시작하는 파일).
+
+**RAM, 20초, 초당 거래.**
+
+| SQLite | 1스레드 | 4스레드 | 묶음 16 | 묶음 64 |
+|---|---:|---:|---:|---:|
+| 기본값 | 9,390 | 9,083 | 7,872 | 8,555 |
+| mmap 1 GiB, 캐시 64 MiB, `temp_store=MEMORY` | 9,886 | 9,470 | 9,680 | 10,230 |
+| 여기에 체크포인트를 WAL 40 MB마다 | 10,361 | 10,272 | 9,890 | 10,211 |
+
+기본값의 묶음은 데이터베이스가 RAM에 있는데도 거래당 7.6 KB(16터미널)와 1.9 KB(64터미널)를 장치에 썼다. SAVEPOINT의
+하위 저널이 64 KiB를 넘으면 임시 파일로 넘치는데, 그 파일이 SD 카드의 `/var/tmp`에 생긴다. `temp_store=MEMORY`가 이것을
+막는다.
+
+**SD 카드, 30초, 초당 거래(거래당 장치에 쓴 바이트).**
+
+| SQLite | 1스레드 | 묶음 16 | 묶음 64 |
+|---|---:|---:|---:|
+| 페이지 4,096 B | 72 (60.7 KB) | 162 (41.8 KB) | 139 (29.3 KB) |
+| 페이지 1,024 B | 132 (31.9 KB) | 203 (22.5 KB) | 263 (17.9 KB) |
+| 1,024 B, 체크포인트를 WAL 40 MB마다 | 87 (40.9 KB) | 694 (10.7 KB) | 594 (7.7 KB) |
+
+- 기준대로 SD 카드에서는 페이지를 1,024 B로 한다. SQLite는 행 하나를 바꿔도 그 행이 든 페이지를 통째로 WAL에 쓰므로,
+ 페이지가 작을수록 덜 쓴다.
+- **체크포인트 간격은 기준에 없던 조율이다.** SQLite에 유리한 쪽이라 더했다. 기본값은 1,000페이지마다다. 1 KiB 페이지라면
+ WAL 1 MB마다 체크포인트가 데이터베이스 파일의 페이지를 되쓴다. 40 MB마다로 늘리면 한 페이지를 여러 번 바꿔도 한 번만
+ 되쓴다. 묶음 16의 처리량이 3.4배가 됐다. RAM에서도 1스레드의 New-Order p99가 1.2 ms에서 0.2 ms가 됐다(체크포인트가 그
+ 꼬리였다). 그래서 두 장치 모두 이 간격을 쓴다([`run.py`](../../benchmarks/tpcc/run.py)의 `tuned`).
+- 1스레드는 이 간격에서 오히려 느려졌다(132 → 87건). 원인은 재지 않았다. WAL이 자라는 동안에는 커밋마다 파일 크기까지
+ 동기화해야 하는데(ext4 저널), 그 몫일 수 있다. 묶음은 그 비용을 거래들이 나눠 진다. 판정은 묶음으로 한다.
+
+## 결과
+
+원시 결과는 [022-tpcc-grouped](../../research/experiments/022-tpcc-grouped/)에 있다. 명령은 그 안의 `runs.sh`이고, 아래 표는
+`tables.py`가 만든다.
+
+**정합성.** W=1과 W=4 모두, 같은 거래 2만 건 뒤 Pure와 SQLite, SQLite와 묶어 커밋하는 SQLite의 아홉 표가 행마다 같았다
+(되돌린 New-Order도 W=1 90건, W=4 93건으로 같다). 실패한 거래는 없었다. 조율까지 시간 잰 실행 70번 모두, 끝난 뒤 clause
+3.3.2.1–4가 성립했고 실패한 거래가 없었다.
+
+**RAM(tmpfs), 20초.** 칸마다 W=1 / W=4다.
+
+| | 초당 거래 | New-Order p99 (ms) | 거래당 CPU (µs) | 소유자의 코어 몫 |
+|---|---:|---:|---:|---:|
+| Pure, 16터미널 | 35,672 / 32,765 | 1.1 / 1.0 | 45.6 / 48.1 | 0.83 / 0.86 |
+| Pure, 64터미널 | 36,567 / 32,255 | 4.4 / 3.6 | 48.9 / 53.9 | 0.90 / 0.94 |
+| SQLite, 1스레드 | 10,357 / 10,272 | 0.2 / 0.2 | 96.5 / 97.1 | |
+| SQLite, 4스레드 | 10,219 / 9,780 | 0.3 / 0.3 | 98.4 / 103.9 | |
+| SQLite 묶음, 16터미널 | 9,843 / 8,828 | 4.4 / 8.2 | 134.0 / 147.2 | |
+| SQLite 묶음, 64터미널 | 10,271 / 8,876 | 12.3 / 17.4 | 131.2 / 147.3 | |
+
+- Pure는 조율한 SQLite의 최고보다 3.5배(W=1), 3.2배(W=4)다. 같은 터미널 수의 묶음 SQLite보다는 3.6–3.7배이고, New-Order
+ p99는 2.8–8.2배 짧다.
+- 거래당 CPU는 Pure가 45.6–53.9 µs(서버 프로세스 전체, HTTP와 JSON 포함), SQLite가 96.5–147.3 µs(프로세스 안, HTTP 없음)다.
+- RAM에서는 묶어 커밋해도 SQLite가 빨라지지 않는다. 동기화가 거의 공짜라 아낄 것이 없고, 작성자에게 넘기고 SAVEPOINT를 두는
+ 값(거래당 35–43 µs)만 든다.
+- W=4에서 Pure는 W=1보다 8–12%, 묶음 SQLite는 10–14% 느리다.
+
+**SD 카드(ext4), 30초, 세 판의 중앙값(범위).** SQLite의 페이지는 1 KiB다.
+
+| | 초당 거래, W=1 | 초당 거래, W=4 | New-Order p99 (ms) | 거래당 장치 바이트 |
+|---|---:|---:|---:|---:|
+| Pure, 16터미널 | 2,138 (2,136–2,672) | 2,247 (1,893–2,314) | 59 / 59 | 1,324 / 1,245 |
+| Pure, 64터미널 | 4,980 (4,864–6,083) | 4,590 (4,324–5,540) | 78 / 94 | 680 / 682 |
+| SQLite, 1스레드 | 110 (96–124) | 84 (80–89) | 86 / 115 | 38,982 / 40,111 |
+| SQLite, 4스레드 | 267 (152–293) | 161 (159–222) | 78 / 90 | 18,110 / 25,886 |
+| SQLite 묶음, 16터미널 | 505 (485–520) | 335 (333–546) | 475 / 590 | 10,871 / 15,001 |
+| SQLite 묶음, 64터미널 | 739 (578–853) | 426 (302–612) | 1,245 / 1,770 | 7,647 / 12,067 |
+
+(p99와 바이트는 W=1 / W=4다.)
+
+- 같은 터미널 수에서 Pure는 묶음 SQLite의 4.2배(W=1, 16터미널), 6.7배(W=1, 64), 6.7배(W=4, 16), 10.8배(W=4, 64)다.
+ New-Order p99는 8–19배 짧다.
+- **거래당 장치에 쓴 바이트가 차이를 설명한다.** 묶음 SQLite는 7.6–15.0 KB, Pure는 0.68–1.32 KB를 썼다(8–18배).
+ - SQLite의 WAL은 바뀐 행이 든 페이지를 통째로 쓰고, 체크포인트가 그 페이지를 데이터베이스 파일에 한 번 더 쓴다.
+ - 창고가 넷이면 거래들이 더 흩어진 페이지를 건드린다. 한 커밋 안에서 겹치는 페이지가 줄어, SQLite는 거래당 W=1의
+ 1.4–1.6배를 쓴다.
+ - Pure는 바뀐 필드(수 하나에 약 7 B)와 더한 행(주문 줄 약 24 B)을 기록하므로 W와 거의 상관없다.
+- 장치가 받은 양으로 보면, 묶음 SQLite(64터미널)는 초당 5.1–5.7 MB를 썼다. 이 카드의 지속 쓰기(초당 3–8 MB,
+ [092](092-tpcc.md)) 안쪽이라, 바이트가 한계로 보인다. Pure는 초당 3.1–3.4 MB를 쓰면서 거래는 6.7–10.8배를 넣었다.
+- Pure의 SD 처리량은 W=4가 W=1의 105%(16터미널), 92%(64터미널)다. 체크포인트는 W=1에서 두 실행을 합쳐 8–12번, W=4에서
+ 3–5번 돌았다. 목록이 크면 그 이미지를 다시 쓰는 기준도 크다.
+
+**메모리.** Pure는 모든 상태를 메모리에 든다. 같은 행을 적재한 직후, Pure의 상주는 38 MiB(W=1)와 104 MiB(W=4)이고 SQLite의
+파일은 13.8 MiB와 52.8 MiB다. SQLite는 필요한 페이지만 읽어 든다.
+
+| | Pure 상주 | 상태 바이트 | 배수 | Pure 저장소 | SQLite 최고 상주 | SQLite 파일(직접 / 묶음) |
+|---|---:|---:|---:|---:|---:|---:|
+| RAM, W=1 | 835 MiB | 375 MiB | 2.23 | 434 MiB | 41–124 MiB | 84 / 82 MiB |
+| RAM, W=4 | 846 MiB | 402 MiB | 2.10 | 411 MiB | 60–188 MiB | 124 / 118 MiB |
+| SD, W=1 | 150–179 MiB | 73–85 MiB | 2.06–2.09 | 70–87 MiB | 17–89 MiB | 15–16 / 19–21 MiB |
+| SD, W=4 | 209–239 MiB | 130–142 MiB | 1.61–1.68 | 104–128 MiB | 23–136 MiB | 54–55 / 56–58 MiB |
+
+Pure의 값은 Pure의 실행이 모두 끝난 뒤다. 상태는 거의 모두 실행이 더한 주문 줄이다(RAM W=1에서 144만 건의 거래로 22 MiB에서
+375 MiB). SQLite의 최고 상주는 실행마다의 범위다(SD는 세 판의 중앙값).
+
+**소유자가 하는 일(RAM, W=1, 64터미널).** Pure만 90초 돌리며 gdb로 소유자 스레드의 스택을 80번 떴다
+([`owner-samples.py`](../../research/experiments/022-tpcc-grouped/owner-samples.py)). 붙을 때마다 프로세스가 멈추므로 그동안의
+처리량은 33,376건이었다. 표본마다 스택에 든 함수로 나눴다.
+
+| 소유자가 하던 일 | 표본 |
+|---|---:|
+| 항목 찾기(목록의 B+트리를 내려간다, `pure_list_seek`) | 24 (30%) |
+| 요청 기다리기 | 19 (24%) |
+| 스냅숏이 공유한 노드를 고치기 전에 복사하기(`own`, `new_node`, `retire`) | 15 (19%) |
+| 스냅숏 게시와 놓은 노드 해제(`publish`) | 5 (6%) |
+| 해석기(`run`, `dispatch`) | 5 (6%) |
+| 항목 바꾸기, 항목 읽기, 답 넘기기 | 각 3 (4%) |
+| 저장(`pure_app_save`)과 그 밖 | 3 (4%) |
+
+- 바쁜 시간(표본 61개)의 39%가 찾기, 33%가 스냅숏에 딸린 복사와 해제다. 저장은 1%다.
+- pure-serve는 저장마다 스냅숏을 게시한다. 읽기가 방금 답한 쓰기를 보게 하려는 것이다. 게시한 뒤 처음 고치는 노드는 그
+ 스냅숏과 공유되어 있어 복사한다. RAM에서는 저장이 초당 약 1,000번이라, 무작위로 흩어진 항목(재고, 고객)을 고칠 때마다
+ 거의 매번 그 잎을 복사한다.
+
+## 판정
+
+1. **SD 카드에서의 주장: 2배 넘게 남았다.** 같은 터미널 수에서 4.2–10.8배다. 기준대로 차이가 어디서 오는지 적었다. 거래당
+ 장치에 쓴 바이트가 8–18배 다르다.
+ - 092의 90배 가운데, 묶어 커밋하기와 조율이 SQLite를 10–15배 빠르게 했다(50건에서 505–739건). 남은 4–11배가 레코드를
+ 기록하는 로그의 몫이다.
+ - Pure는 묶어 커밋하는 구조를 앱 코드 없이 준다. SQLite에서는 작성자 스레드와 SAVEPOINT를 손으로 짰다.
+ - README와 CHECKPOINT의 SD 비교를 이 비교로 바꿨다. 90배는 기본 설정으로 거래마다 커밋하는 SQLite에 대한 값이었다.
+2. **소유자 하나: 병목이다.** W=4 RAM, 64터미널에서 소유자가 코어의 0.94를 썼고, 처리량은 16터미널보다 1.6% 적었다. 서버는
+ 코어 셋 가운데 1.74개만 썼다. 기준대로 키로 나눈 소유자를 다음 설계로 올린다. [084](084-parallel-syncs.md)는 "앱의 적용이
+ CPU 하나를 넘을 때 소유자를 나누되 동기화는 장치마다 한 줄로 모은다"고 했다. TPC-C는 RAM에서 그 조건에 닿았다.
+ - 설계는 표본이 보인 것과 함께 본다. 소유자 시간의 3분의 1이 스냅숏 때문에 드는 복사와 해제다. 이것을 줄이면 키로 나눌 수
+ 없는 앱(한 키에 모이는 상태)까지 모두 빨라진다.
+ - 나누기는 창고를 넘는 거래를 두 소유자가 함께 해야 하는 문제를 새로 만든다. New-Order의 약 10%가 다른 창고의 재고를
+ 쓰고, Payment의 15%가 다른 창고의 고객에게 간다.
+ - SD 카드에서는 소유자가 코어의 7–15%만 쓴다. 이 장치에서 쓰기를 늘리는 길은 여전히 거래당 바이트와 동기화다(084 그대로).
+3. **체크포인트: 다음 일이 아니다.** SD 카드에서 W=4가 W=1의 92–105%다(기준은 절반 아래).
+4. **메모리: 기준 안이다.** Pure의 상주가 상태 바이트의 1.61–2.23배다(기준 2.5배).
+
+## 한계와 다음
+
+- W=4까지다. RAM에서 저장소와 데이터베이스까지 메모리에 들어야 해서다. SD 카드의 실행은 30초다. W=4에서 Pure의 체크포인트는
+ 두 실행을 합쳐 3–5번이라, 더 긴 실행에서 체크포인트가 어떻게 겹치는지는 재지 않았다.
+- SQLite는 프로세스 안에서 돌고 HTTP도 JSON도 없다. Pure의 수에는 그것이 들어 있다. CPU 비교는 SQLite에 유리하다.
+- 규격의 생각 시간, 창고당 터미널 10개, 텍스트 필드는 없다(092와 같다). 공식 tpmC가 아니다.
+- 1스레드 SQLite가 큰 WAL에서 느려진 까닭은 재지 않았다.
+- 소유자 표본은 80개다. 한 칸이 ±5%포인트쯤 흔들린다. gdb가 붙을 때마다 프로세스가 멈춘다.
+- 레코드를 기록하는 저장소(LSM 같은)와는 견주지 않았다.
+
+다음은 **소유자의 시간**이다. 설계 노트에서 두 길을 함께 다룬다. 스냅숏이 공유한 잎을 고칠 때 덜 복사하는 길(모든 앱에
+듣는다)과, 키로 나눈 소유자가 한 저장소의 그룹 커밋을 함께 쓰는 길(코어를 더 쓴다)이다. 덜 복사하는 쪽을 먼저 잰다.