DB performance at scale
Opendoor JD ๋ช ์ ์๊ฑด: โrelational databases + reason about performance at scale.โ ๊ฐ ์ฌ๋ค๋ฆฌ = ์ฆ์ โ ์ โ EXPLAIN์ผ๋ก ๋ณด๊ธฐ โ boring ํด๋ฒ โ at-scale ํด๋ฒ โ ํธ๋ ์ด๋์คํ. ์ง์ง SQL/EXPLAIN๊ณผ ํจ๊ป, ๋จ๊ณ๋ง๋ค ์์ด ํ ์ค.
๐ ์์ผ๋ก ์ง์ : ๋ ํฌ์
infra/dblab โ Opendoor ๋ชจ์ ์คํค๋ง(3M offers, ์ธ๋ฑ์ค ์ผ๋ถ๋ฌ ๋น ์ง)๋ฅผ Docker Postgres๋ก ๊น๊ณ EXERCISES.md์ 8๊ฐ ํ๋ จ(EXPLAINโ์ธ๋ฑ์คโ์ฌ์ธก์ , ๋ฝ ๊ฒฝํฉ 2์ธ์
, pgbench ์ํ ๋ถํ)์ ๋
ธํธ๋ถ์์ ๋๋ ค๋ผ. ์ค์ธก: ๋ณตํฉ ์ธ๋ฑ์ค๋ก 46msโ0.3ms.- EXPLAIN + indexes โ why is this query slow?6 steps10๋ง ๊ฑด์ผ ๋ 30ms์๋ ์ฟผ๋ฆฌ๊ฐ 2,000๋ง ๊ฑด์ด ๋๋ 4์ด๊ฐ ๊ฑธ๋ฆฐ๋ค. ์ถ์ธกํ์ง ๋ง๊ณ EXPLAIN์ผ๋ก ์ง์ ๋ณด๊ณ , ์ธ๋ฑ์ค๋ก ๊ณ ์น๊ณ , ๊ทธ ๋๊ฐ๊น์ง ๋งํ ์ ์๊ฒ ํ ๊ณ๋จ์ฉ ์ฌ๋ผ๊ฐ๋ค.
- N+1 queries โ the hidden loop of round-trips6 steps์ง 100์ฑ ๋ชฉ๋ก ํ์ด์ง๊ฐ 600ms ๊ฑธ๋ฆฝ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ฟผ๋ฆฌ ํ๋ํ๋๋ 0.4ms๋ก ๋น ๋ฆ ๋๋ค. ๋ฒ์ธ์ ์ฝ๋ ์ ์จ์ ๋ฐ๋ณต๋ฌธ โ ๋ชฉ๋ก ์ฟผ๋ฆฌ 1๋ฒ ๋ค์ ์ง๋ง๋ค 1๋ฒ์ฉ, ์ด 1 + 100 = 101๋ฒ DB๋ฅผ ์๋ณตํ๋ N+1 ๋ฌธ์ ์ ๋๋ค. ์ฆ์ โ ์์ธ โ ๋์ผ๋ก ํ์ธ โ ๊ธฐ๋ณธ ํด๊ฒฐ โ ๋๊ท๋ชจ ํด๊ฒฐ โ ํธ๋ ์ด๋์คํ ์์๋ก ์ฌ๋ผ๊ฐ๋๋ค.
- Lock contention โ writers blocking writers6 steps์ฐ๊ธฐ๊ฐ ์ฐ๊ธฐ๋ฅผ ๋ง๋ ๋ฌธ์ ์ ๋๋ค. 100๋ช ์ด ๊ฐ์ row๋ฅผ ๋์์ UPDATEํ๋ฉด Postgres๋ ํ ๋ช ์ฉ ์ค์ ์ธ์๋๋ค. ์ฒซ ํธ๋์ญ์ ์ด COMMITํ ๋๊น์ง ๋๋จธ์ง 99๋ช ์ ๊ทธ๋ฅ ๊ธฐ๋ค๋ฆฝ๋๋ค. CPU๋ ํ๊ฐํ๋ฐ ์๋ต๋ง ๋๋ ค์ง๋, "๋ฐ์ ๊ฒ ์๋๋ผ ๊ธฐ๋ค๋ฆฌ๋" ๋ฌธ์ ์ ๋๋ค. ์ฆ์ โ ์์ธ โ ๋ณด๋ ๋ฒ โ ๊ธฐ๋ณธ ์์ โ ์ค์ผ์ผ ์์ โ ํธ๋ ์ด๋์คํ ์์๋ก ์ฌ๋ผ๊ฐ๋๋ค.
- Pagination at scale โ OFFSET dies, keyset lives6 steps๋งค๋ฌผ(listings) ํ ์ด๋ธ 1,000๋ง ํ. ๊ฐ์ ๋ชฉ๋ก ์ฟผ๋ฆฌ์ธ๋ฐ 1ํ์ด์ง๋ 5ms, 5,000ํ์ด์ง๋ 2์ด๊ฐ ๊ฑธ๋ฆฐ๋ค. ์ฆ์ โ ์์ธ โ ๋์ผ๋ก ํ์ธ(EXPLAIN) โ ๋น์ฅ ๋ง๋ ๋ฒ โ ์ค์ผ์ผ์์ ์ฌ๋ ๋ฒ(keyset) โ ๊ทธ ๋๊ฐ, ์ด ์์๋ก ์ฌ๋ผ๊ฐ๋ค.
- Hot rows + partitioning โ when one row/table melts7 steps์ฟผ๋ฆฌ ์์ฒด๋ ๋ฉ์ฉกํ๋ฐ DB๊ฐ ๋๋ ค์ง ๋๊ฐ ์๋ค. ๋ฒ์ธ์ ๋ณดํต ๋ ์ค ํ๋๋ค: ๋ชจ๋๊ฐ ๊ฐ์ ํ ํ๋๋ฅผ ๋๋๋ฆฌ๊ฑฐ๋(hot row), ํ ์ด๋ธ ํ๋๊ฐ ๋๋ฌด ์ปค์ก๊ฑฐ๋. ์ด๋น 3,000๋ฒ UPDATE ๋ง๋ ์กฐํ์ ํ์์ ์์ํด์ 5์ต ํ ์ด๋ฒคํธ ํ ์ด๋ธ๊น์ง โ ์ฆ์ โ ์์ธ โ ๋์ผ๋ก ํ์ธ โ ์ง๋ฃจํ ํด๊ฒฐ โ ์ค์ผ์ผ ํด๊ฒฐ โ ๋น์ฉ ์์๋ก ์ฌ๋ผ๊ฐ๋ค.
- Connection pooling โ why the DB ran out of connections6 steps์์์ผ ์์นจ ๋ฐฐํฌ ์งํ, ๋ชจ๋ API๊ฐ 500์ ๋ฑ๊ธฐ ์์ํ๋ค. ๋ก๊ทธ์๋ "sorry, too many clients already" ํ ์ค. Postgres ๊ธฐ๋ณธ ์ฐ๊ฒฐ ํ๋๋ 100๊ฐ์ธ๋ฐ, ์ฑ ์๋ฒ 10๋๊ฐ ๊ฐ์ 20๊ฐ์ฉ ์ฐ๊ฒฐ์ ์ด๋ ค๊ณ ํ๋ค. ์ด ์ฌ๋ค๋ฆฌ๋ ๊ทธ ์ฌ๊ณ ๋ฅผ ์ซ์ ๊ทธ๋๋ก ๋ฐ๋ผ๊ฐ๋ค: ์ฆ์ โ ์์ธ โ ๋์ผ๋ก ํ์ธ โ ๋น์ฅ ๊ณ ์น๊ธฐ โ ๊ท๋ชจ์์ ๊ณ ์น๊ธฐ โ ๊ทธ ๋๊ฐ.
- Serialization failures (40001) + retry6 stepsSERIALIZABLE(๋๋ REPEATABLE READ)์์ ๋์์ ๋๋ ๋ ํธ๋์ญ์ ์ด ์ง๋ ฌํ ์์๋ฅผ ๊นฐ ๊ฒ ๊ฐ์ผ๋ฉด, Postgres๋ ๋ ์ค ํ๋๋ฅผ SQLSTATE 40001(serialization_failure)๋ก ๊ทธ๋ฅ ์ฃฝ์ธ๋ค โ ๋ฐ๋๋ฝ์ 40P01. ์ด๊ฑด ๋ฒ๊ทธ๊ฐ ์๋๋ผ DB๊ฐ ์ ํฉ์ฑ์ ์งํค๋ ์ ์ ๋์์ด๋ค. ํต์ฌ์ ์ฑ์ด 40001์ ์ก์์ ํธ๋์ญ์ ์ ํต์งธ๋ก ์ฌ์๋ํด์ผ ํ๋ค๋ ๊ฒ, ๊ทธ๋ฆฌ๊ณ ์ฌ์๋ํด๋ ์์ ํ๋๋ก ํธ๋์ญ์ ์ด ๋ฉฑ๋ฑ(idempotent)ํด์ผ ํ๋ค๋ ๊ฒ์ด๋ค. ์ฆ์ โ ์์ธ(MVCC write-skew) โ ๋์ผ๋ก ํ์ธ โ ์ฌ์๋ ๋ฃจํ โ ๋ฉฑ๋ฑ์ฑ โ SERIALIZABLE vs ๋๊ด์ ๋ฒ์ ํธ๋ ์ด๋์คํ ์์๋ก ์ฌ๋ผ๊ฐ๋ค.
- VACUUM, autovacuum, dead tuples, bloat, VACUUM FULL vs pg_repack7 steps์๋ฌด๋ DELETE๋ฅผ ๋ง์ด ์ ํ๋๋ฐ ํ ์ด๋ธยท์ธ๋ฑ์ค๊ฐ ๊ณ์ ์ปค์ง๊ณ , ์ธ๋ฑ์ค๊ฐ ๋ฉ์ฉกํ๋ฐ๋ ์ฟผ๋ฆฌ๊ฐ ์ ์ ๋๋ ค์ง๋ค. ์์ธ์ Postgres์ MVCC๋ค โ UPDATE/DELETE๋ ํ์ ๊ทธ ์๋ฆฌ์์ ์ ๊ณ ์น๊ณ ์ ๋ฒ์ ์ ์ฃฝ์ ํํ(dead tuple)๋ก ๋จ๊ธด๋ค. ์ฃฝ์ ํํ์ด ์ ์น์์ง๋ฉด ๋์คํฌ๊ฐ ๋ถํ๊ณ (bloat), VACUUM์ด ๋ฐ๋ผ์ก์ง ๋ชปํ๋ฉด ํธ๋์ญ์ ID ์์ง(wraparound)๊น์ง ๊ฐ๋ค. ์ฆ์ โ ์์ธ(MVCC) โ ๋์ผ๋ก ํ์ธ โ autovacuum ํ๋(์ง๋ฃจํ ์ ๋ต) โ ์ค์ผ์ผ ์ฒ๋ฐฉ(ํํฐ์ ๋/HOT) โ bloat ํ์(VACUUM FULL vs pg_repack) ์์๋ก ์ฌ๋ผ๊ฐ๋ค.
- Index types beyond B-tree: BRIN, GIN, partial, expression indexes7 stepsB-tree๋ ๊ธฐ๋ณธ๊ฐ์ผ ๋ฟ, ๋ง๋ฅ์ด ์๋๋ค. ์๊ณ์ด์ฒ๋ผ ์์ฐ ์ ๋ ฌ๋ ๊ฑฐ๋ ํ ์ด๋ธ์ BRIN์ด B-tree๋ณด๋ค 1000๋ฐฐ ์๊ณ , JSONBยท๋ฐฐ์ดยท์ ๋ฌธ๊ฒ์์ GIN, 'ํ์ฑ ํ๋ง'์ด๋ '์๋ฌธ์ ์ด๋ฉ์ผ'์ฒ๋ผ ์กฐ๊ฑดยทํํ์ ์์ ๊ฑฐ๋ ์ฟผ๋ฆฌ์ partial/expression ์ธ๋ฑ์ค๊ฐ ๋ง๋ ๋๊ตฌ๋ค. ํต์ฌ์ '์ฟผ๋ฆฌ๊ฐ ๋ฌด์์ ๋ฌป๋๊ฐ'์ ์ธ๋ฑ์ค ์ข ๋ฅ๋ฅผ ๋ง์ถ๋ ๊ฒ โ ์ฆ์ โ ์ B-tree๋ก ๋ถ์กฑํ๊ฐ โ EXPLAIN์ผ๋ก ํ์ธ โ ๋ง๋ ์ธ๋ฑ์ค ์ฒ๋ฐฉ โ ์ค์ผ์ผ ์ฒ๋ฐฉ โ ๊ทธ ๋๊ฐ ์์๋ก ์ฌ๋ผ๊ฐ๋ค.