diff options
Diffstat (limited to 'docs/research/106-tpcc-conditions-first.md')
| -rw-r--r-- | docs/research/106-tpcc-conditions-first.md | 177 |
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 같은)와는 견주지 않았다. + +다음은 **소유자의 시간**이다. 설계 노트에서 두 길을 함께 다룬다. 스냅숏이 공유한 잎을 고칠 때 덜 복사하는 길(모든 앱에 +듣는다)과, 키로 나눈 소유자가 한 저장소의 그룹 커밋을 함께 쓰는 길(코어를 더 쓴다)이다. 덜 복사하는 쪽을 먼저 잰다. |
