Бұлттық сервердегі жад кідірісі: болжам емес, өлшем
Қысқаша (2026-11-01 тексерілді): Осы демалыста Hacker News басты бетіне шыққан жазба 100% жүктемені көрсететін процессор жиі тек RAM-ды күтіп тұрады дейді. Автор көбірек жұмыс істеп, бірақ жадқа сирек жүгініп жылдамдаған GROUP BY сұрауын көрсетеді. Біз сол идеяны осы блогты жазатын бұлттық серверде тексердік: деректер L1-ге сыйса, бір оқу шамамен 2 нс тұрады, ал 256 МБ жұмыс жиынында шамамен 200 нс, яғни жүз есеге жуық көп. Сервер жалдайтындар үшін бұл айырма тапсырыстағы ядро санынан маңыздырақ. Төменде скрипт, біздің сандар және олардың бұлт пен арнайы сервер арасындағы таңдауға әсері.
Дереккөз. "It's the memory latency, stupid", авторы Maor Kern (Pivot / Epsio Labs, 2026-10-28 жарияланған), Hacker News талқылауы, 2026-11-01 жағдайы бойынша 175 ұпай. Олардың сандары жазбада қалай берілсе, солай келтірілді; өз сандарымызды өзіміз өлшедік.
Pivot жазбасы нені өлшеді
Тест бір сұраудан тұрады: ClickBench "hits" кестесі бойынша GROUP BY UserID (шамамен 99 миллион жол, шамамен 17 миллион бірегей пайдаланушы), 128 ГБ DDR5 бар AMD EPYC 9124 bare-metal серверінің бір ядросына бекітілген. 17 миллион кілтке арналған қарапайым хеш-кесте ешбір кэшке сыймайды, сондықтан іздеудің барлығына жуығы RAM-ға барады. Жолдарды алдымен 4096 бөлікке бөліп, әрқайсысына шағын хеш-кесте берсе, жұмыс көбейеді, бірақ әр кесте кэште қалады:
| Көрсеткіш (бір ядро) | Қарапайым хеш-кесте | Radix бөлу |
|---|---|---|
| Уақыт, 1-ші / 2-ші іске қосу | 3.9 с / 3.6 с | 3.3 с / 2.7 с |
| Жадты күтетін слоттар | 50.1% | 23.5% |
| Пайдалы жұмыс слоттары | 16.1% | 32.2% |
| DRAM-нан оқулар | 76.2 млн | 25.7 млн |
Бөлінген нұсқа көбірек операция орындайды (19.4 млрд, 11.4 млрд-қа қарсы), бірақ бәрібір бірінші аяқтайды. Автордың оңтайландырылған нұсқасы бір ядрода 1.85 с, барлық 16 ядрода 0.17 с жұмыс істеді. Сабақ бір дерекқорға ғана қатысты емес: top ішіндегі "CPU 100%" ядроның бос тұрмағанын білдіреді, оның есептеп жатқанын емес.
Өзіңіз өлшеңіз: memlat.js
Кідірісті көрудің классикалық тәсілі - pointer chase: әр ұяшықта келесісінің индексі сақталған, кездейсоқ ретпен, бір кэш-сызық (64 байт) қашықтықта орналасқан массив. Әр оқу алдыңғысына тәуелді, сондықтан процессор деректі алдын ала жүктей де, оқуларды қатар орындай да алмайды, ал бір қадамның уақыты жұмыс жиыны тұрған деңгейдің кідірісіне тең. Блог машинамызда C компиляторы жоқ, сондықтан бұл Bun үшін JavaScript: JIT циклды бірнеше машиналық нұсқаулыққа айналдырады, бұл әр санға аздап қосады. Абсолюттік мәндерді емес, сатыларды салыстырыңыз. memlat.js деп сақтаңыз:
// memlat.js - pointer-chase memory latency by working-set size (bun memlat.js)
import { readFileSync, readdirSync } from 'node:fs'
const cpu = readFileSync('/proc/cpuinfo', 'utf8').match(/model name\s*:\s*(.+)/)?.[1] ?? '?'
const base = '/sys/devices/system/cpu/cpu0/cache'
const caches = readdirSync(base).filter(d => d.startsWith('index')).map(d => {
const r = f => readFileSync(`${base}/${d}/${f}`, 'utf8').trim()
return `L${r('level')}${r('type') === 'Data' ? 'd' : r('type') === 'Instruction' ? 'i' : ''} ${r('size')}`
})
console.log(`CPU: ${cpu}\nCaches: ${caches.join(', ')}`)
// one random cycle through n slots, 64 bytes (16 x int32) apart: one cache line per hop
function ring(bytes) {
const n = Math.max(2, Math.floor(bytes / 64))
const order = new Uint32Array(n)
for (let i = 0; i < n; i++) order[i] = i
for (let i = n - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const t = order[i]; order[i] = order[j]; order[j] = t
}
const a = new Int32Array(n * 16)
for (let i = 0; i < n; i++) a[order[i] * 16] = order[(i + 1) % n] * 16
return a
}
function chase(a, hops) {
let p = 0
for (let i = 0; i < hops; i++) p = a[p]
return p
}
const hops = 20_000_000
console.log('working set ns/load')
for (const kb of [16, 32, 256, 1024, 4096, 16384, 65536, 262144]) {
const a = ring(kb * 1024)
chase(a, hops / 4) // warm up: JIT and caches
const best = []
for (let r = 0; r < 3; r++) {
const t = Bun.nanoseconds()
const sink = chase(a, hops)
best.push((Bun.nanoseconds() - t) / hops)
if (sink < 0) console.log(sink)
}
const label = kb >= 1024 ? `${kb / 1024} MB` : `${kb} KB`
console.log(`${label.padStart(10)} ${Math.min(...best).toFixed(1).padStart(7)}`)
}
bun memlat.js
Нәтиже, DCXV бұлттық серверінде Bun 1.4.4 арқылы 2026-11-01 іске қосылды:
CPU: Intel(R) Xeon(R) CPU E5-2698 v4 @ 2.20GHz
Caches: L2 4096K, L1d 32K, L3 16384K, L1i 32K
working set ns/load
16 KB 2.0
32 KB 2.2
256 KB 12.5
1 MB 24.3
4 MB 122.2
16 MB 138.4
64 MB 164.7
256 MB 205.1
Сол таңғы екінші іске қосу 1 МБ-қа дейін ең көбі 2 нс, одан жоғары ең көбі 12 нс айырмашылық берді:
| Жұмыс жиыны | бір оқуға нс, іске қосу 1 / 2 | Қай жерде тұрады |
|---|---|---|
| 32 КБ | 2.2 / 2.4 | L1 деректер кэші |
| 256 КБ | 12.5 / 10.5 | L2, ішінара L3 |
| 1 МБ | 24.3 / 24.3 | L3 |
| 16 МБ | 138.4 / 137.2 | бет кестелерін аралау, L3 және DRAM |
| 256 МБ | 205.1 / 193.5 | DRAM |
Сандар не айтады
- Қонақ жүйе нақты кэштерді көрмейді. ВМ 4 МБ L2 және 16 МБ L3 көрсетеді; Intel Xeon E5-2698 v4 үшін 50 МБ кэш көрсетеді (Intel ark, 2026-11-01 тексерілді). Гипервизор өз топологиясын ұсынады, ал бұлттық сервердегі
lscpuкремнийді емес, соны оқиды. Жазуларға сенбей, жабдықты өлшеңіз. - Құлдырау кэш толмай тұрып-ақ басталады. 1 МБ мен 4 МБ аралығында кідіріс бес есе өседі, бұл 50 МБ-тан әлдеқайда төмен. 4 КБ беттермен 4 МБ бойынша кездейсоқ аралау 1 024 бетті қозғайды, бұл TLB сыйғызатыннан көп, ал ВМ ішінде TLB жіберіп алуы қонақтың да, хосттың да бет кестелерін аралайды; оның үстіне L3 сокеттің басқа ядроларымен ортақ. Бұл себептерді біз бөліп қарамадық. Практикалық қорытынды бірдей: ВМ-де жылдам жұмыс жиыныңыз бірнеше мегабайт қана деп жоспарлаңыз.
- Көп RAM жылдам RAM дегенді білдірмейді. 1 МБ-тан 256 МБ-қа дейін бір оқудың құны шамамен сегіз есе өседі. Қосымша гигабайттар жылдамдық емес, көлем береді; кідіріске тірелген тапсырманы Pivot бөлуі көрсеткендей кішірек ыстық жиын жылдамдатады.
Сервер жалдағанда бұл нені білдіреді
- Бұлттық серверлер веб-қосымшалардың, API мен шағын дерекқорлардың көпшілігіне жарайды: олардың ыстық деректері кэшке сыяды, және олар RAM-ды күтпей тұрып-ақ желіні не дискіні күтеді. Көлемді өлшенген жад қолданысы бойынша таңдаңыз, біздің бұлттық сервер көлемін таңдау нұсқаулығындағыдай.
- Арнайы серверлер үлкен жұмыс жиыны күні бойы кездейсоқ оқылғанда өзін ақтайды: аналитика, үлкен хеш-біріктірулер, жадтағы кэштер, индекстері RAM-ға енді сыймайтын PostgreSQL сервері. Сіз көршілерсіз бүкіл L3 мен жадтың барлық арнасын аласыз және TLB жіберіп алуларын азайтатын huge pages қоса аласыз. Конфигурацияны арнайы сервер бетінде жинаңыз.
- Кез келген жаңартудан бұрын тестті қолыңыздағы серверде іске қосып, баяу тапсырмаңыз кідіріске тірелетінін тексеріңіз. Ядроларды екі есе көбейту уақытының жартысын күтумен өткізетін бір ағынға көмектеспейді. Жадтың өзі биыл қымбаттады, қараңыз: 2026 жылғы сервер RAM, сондықтан ең арзан шешім көбіне оған азырақ жүгінетін код.
Сатып алмас бұрын тексеріңіз
Прага мен Ковильядағы Tier III дата-орталықтарымыз бұлттық серверлерді және арнайы серверлерді ұсынады. Бұлттық серверді іске қосып, memlat.js-ті өз жүктемеңіздің қасында орындаңыз да, нәтижені sales@dcxv.com мекенжайына жіберіңіз: арнайы сервер сандарды өзгерте ме, соны шешуге көмектесеміз.
