З Модулё на кольцо хэшаў: расшырэнне флоту кешаў Node.js без працяжных перашкод
Дазвольце дазнаць, чаму метод шардавання hash-mod-N спрычыная злам базаў дадзеных, калі змінююцца вузлы, як пораўняваюцца методы хэшавання rendezvous, jump і ring, а таксама як стварыць збалансаваны кольцавы структуру з вагамі ў Node.js.
Кластэр кэша, які працав без проблем месяцями, можа зупніць роботу базы дадзеных за кальканы хвілі пасля стандартных змян – напрыклад, падаўшы ўжо аднае вузло. Прычыной зазвычай ёст толькі адна лінія коду кліента, якая выбірае сервер за формулай hash(key) % N. У гэтым кярыі точна адказваецца, чаму гэтая лінія не працюе, практычна паруравнююцца чатыры серьзныя альтернатывы, ствараецца кільцо консистэнтных хешаў высокай якосці для прыменення ў Node.js, а таксама пераказваюцься операцыйныя падступы, ад якіх сам алгоритм не захавае.
Фармат перываноў
Уявіце сабе здаровы кластэр з чатырыма вузламі кэша, якія даходзяць до 94% рэткі спраўнае адпаведання запытаў. Навантажэнне падвайваецца наперадзе сезоннага піку, таму інжынер дадае пяцый вузл. Цэе двухлінейная змена конфігурацыі, якая адбываецца астаточна аператыўна ў середзіне рабочага дня. Прыбліжна за падвеўсю хвілі база дадзеных заповнюецца на 100% CPU, і сайт стае недоступным.
Ніхто не падаў у недбаласць. Кліент проста перадаваў ключы так, як завжды:
const node = nodes[hash(key) % nodes.length];
Этот выраз дужа равнамерна распадзеў ключы, таму ён выглядае правільным. Проблема крыўця ў тым, што він робіць як толькі змянюецца nodes.length, і самэ гэта є началом раштату ціх наставленняў. У раштате рассматрываюцца чатыры окремыя проблемы, аналізуюцься рэальныя альтернатывы, а пасля ствараецца і пераканальваецца працэс у Node.js.
Чатыры проблемы, якія хаваюцца за адной ідеяй
Consistent hashing часта представляюць як адну хітрасць. На практыцы ён рашае чатыры разныя проблемы, і рэалізацыя, якая адпрацоўвае толькі першую з іх, все равно будзе неэфектываў у практычным выкарыстоўванні чераз трэція та чатыртая проблемы.
Проблема 1: змяна N перамішвае практычна всі ключы
За дапамогою hash(key) % N змяна N не перамішвае каля канечнай колькасці ключоў — яна перамішвае практычна ўсія з іх.
Дапамагае працаваць з цыфрамі. Ключ з хэшам 1,000,003 супараджуецца з вузлам 3 пад % 4, і якраз таксама супараджуецца з вузлам 3 пад % 5. Ключ з хэшам 1,000,004 супараджуецца з вузлам 0 пад % 4 і з вузлам 4 пад % 5. этыя два супараджэння не маюць адносіна адзін да другога, таму ключ застаецца там, дзе быў, толькі ад случая, з верыгатываннем працыяга 1 да N.
Пры анаізе мільйона ключоў патэрн выглядае чыста: пераход з 8 на 9 вузлаў перасунуе 88,93% ключоў, а ў кластэры з ста вузламі даданне аднаго вузла зробляе недзійсным прыбліжна 99% кэша.
Паглядзіце, у какім напрамку развиваецца гэты трэнд. Чым больш расте ваша система, тым болей разрушальным становіцца кожны крок масштабавання. Цэта проблема чакае, пакуль бізнес мае успех.
Кожны рэлакаліраваны ключ — это непаспэль, кожная непаспэль — запыт да базе дадзеных, і ўсе яны прыходзяць за калькі секунд. База дадзеных, прадзеяна для 6% запытов да чытання, якія зазвычай непаспэльны, раптам прыме майже ўсія з іх.
Проблема 2: тая ж пераранжаванне, непланавана
Прынеймна першая проблема выйшвае, калі вы вяршыце рашэнне пра расширэння. Другая — гэта той ж самы прыбутак, вызваны нештатам, у найменш падходягучы момент.
У вузла выкаанчоўваецца памяць, хост завершае сваю роботу, або раздзел сеті скрывае аднаго вузла ад паловы часткі сеті. Колькасць вузлаў зменшваецца з 8 да 7, і кожны кліент, дзейнуючы самастоятельна, перенаслаўа прыблізна 87% своих ключоў на засталыя вузлы.
Сяпэр ситуацыя ўжо крытая: 12,5% магчымасцей кэша ўжо недаступныя, база дадзеных сталкаецца з 87% колькасцю неудачных запыткаў, а семь выжылых узлаў прымуцца за трафік загубленага узла адночас з падпілненням своих рэзерваў майже кожным ключам. Часты наследак — занепад другога узла, што вымагае новага повнага перераспадзелу, а гэта зноў прыводзіць да занепаду трэцяга.
Метод модулярнага маршрутызавання ператварая прычыну занепаду адного узла на супаўзвязаны проблему ў всім кластэре, якая сама сабе падтрымлівае. Гэты каскад, а не холадны кэш, ёсць практычная апасцярожнасць.
Проблема 3: просты кольцавы патэрн сяродзіцца ў вельмі неравнаважаны стан
Вочывае рашэння для проблемы 1 — разместіць узлы і ключы ў аднай числовай прасторэ і даць кожны ключ таму узлу, які знаходзіцца пасля яго ў напрамку гадзінніка. Гэта ёсць сутнасць методу консистэнтнага хэшавання, і ён дзеясно усунея масовы перераспадзел.
Якщо яго ўпэўненна реалізаваць, яно паслаба балансуе навантажэнне. Узлы знаходзяцца там, дзе падаюць іх хэшы, таму прасторы межы ўзламі є абыяковымі, а абыяковыя прасторы рэдка калі бываюць равнымі. Калі кожны хэш размешчае по чатыро узлов, адна з мераванняў паказала, што адзін узел контролюе 45% ключоў, а іншы — толькі 13%; разлік становіць 3,4 раза, пры чым няма жадных багоў.
Неравнавага таксама застойна. Яна вытыкаецца з самых назв узлоў, таму той жа узел, напрыклад cache-04, застаецца актыўным, пакуль яго не перэназначаюць, а кожны, хто расследуе, бачыць код, які працуе абсалютна так, як напісаны.
У большых масштабах ситуацыя пагіршваецца. Калі на 8 узлоў є по аднаму точкі кольца, найбольш завантажаны узел мае 434% свайго справядлівага часткі, а найменш завантажаны — 3,7%. Эфектывна гэта адзін перавантажаны сервер і семь бездзейных.
Проблема 4: кожны кліент должен пагадзіцца
Найменш зрозумелая проблема стосуецца автантыя. Чыгульна вещ прыкладзецца да вузла па ключу, і яна должна даваць той самы адказ на кожным прасільнікам.
Адна з аптавакаў — служба-координатор, якая зберагае карточку автантыя. У такім случае або кожны пошук выклікае два перавозы дадзеных па сетцы, або кліенты кэшуюць гэтую карточку, і тады патрэбна мера для ўневажлівання яе. Якщо два кліенты на момент часу будуць выкорыстоўваць разныя версіі карточкі, адны можа запісаць user:42 у вузел A, а другі — прачытаць яго з вузла B. Нічога не пропадае, але гэта, можна сказаць, ўсёга горш: тады існуе два можлівых значэння.
Тое, чаго вы хачаце, — это карта, якая ўособлівае сабой чыстую функцыю ключа і чыннага списка членства. Ніякога коордынатора, ніякой службы пошуку, ніякога спакульнага стану: кожны кліент выконвае тыя ж арыфметычныя операцыі і доходзіць да таго ж рэзультата. Консистэнтны хэшынг дае самэ гэта, і самэ таму ён перамагае табелю пошуку, незважаючы на большую гнучкасць табелі.
Конкрэтны сценарый і варыянты
Ёжчэ конкрэтная парадынг, розглядзіце гэтыя системы.
Система. API для інтернет-торгавлі зберагае даны сэсій і прафіляў у пуле вузлаў Redis. Максымальная навантажэння становіць або 40 000 запыткаў за секунду для 25 мільйонах ключоў, пры чым ступень успеху запыткаў — 94%. База дадзенаў застаецца працоспроможной толькі завялік з-за 6% запыткаў, якія не маюць успеху. (Ёсць інфармацыя пра самыя патэрны кэшавання ў „Асновы кэшавання ў Redis“.)
Тэрэтарыі:
- Здольнасць сістэмы павышацца з 8 да 12 вузлаў перад праймом, не спрычынюючы ашаленага навантажэння
- Здольнасць прыжываць пасля выключэння вузла з мерым, толькі дапускаемым радыясам уражэння
- Равнамернае распадзелэнне навантажэння, калі жадны вузел не перакрочвае прыбліжна 120% сваей часткі
- Утриманне кожнага координатора пахоце ад шляху чытання
- Урахоўванне разнага апарату: у дзеярых вузлаў 64 ГБ, у іншых — 16 ГБ, і яны не должны несці адно і тое ж навантажэння
Некалькі алгорытмоў можа выпанаваць частку або ўсе этыя вакалідаты. Яны разлічаюцца за значамымі параметрамі, і неправильны выбар мае вялікія наследкі.
Варыянт А: модулярны хэшаванне
hash(key) % N дае ідеальны баланс, выкарыстоўвае адну інструкцыю і не выкарыстоўвае памяці.
Ён цалкам не выпанавае вакалідаты 1 і 2. Ён заслуговае на упамінак, таму што гэта тое, што всі пачынаюць пісаць першым, і ён працуе ідеальна пакуль не перестане.
Выбірайце яго тады, калі N сапраўды ніколі не змянюецца, напрыклад, падзеўленне заводу з работамі між фіксаваным колікствам працоўнікаў або раздзелэнне дадзеных унутранічна ў адны процэс.
Варыянт Б: коордынатор і табліца пошуку
Зберагаюце явныя звязкі между дыапазонамі ключоў і вузламі ў сховішчы, такім как etcd або ZooKeeper. Сістэмы на кшталт Vitess і HBase працуюць приблізна так.
Прыцэплівасць ёсць рэальным: полная кантроль. Вы можете пераместіць адзін «гарэтый шард», паштоўна перакансаліраваць адзін дыяпазон за раз, стараючыся стежыць за показнікамі, або прыкрепіць конкрэтнага корыстувальца да адзінаковага апарату. Жадны падход, базаваны на хэшах, не можа запропанаваць нічога з гэтаго, і пасля певнага масштабу вам гэта будзе неабходна.
Цена таксама рэальная: неабходна праця сістэмы кансенсусу, неабходна борьба з тым, каб усі кэшаваныя копіі карты заставаліся актуальнымі, а таксама неабходна залежнасць ад шляху чытання.
Выбірайце гэты падход тады, калі вы перамешчаеце дырацыйнальныя даны, а не тымчасовыя елементы кэша, і вам трэба кантроліраваць процес міграцыі, а не дазволяць усему перамешчацца заодно.
Варыянт C: хэшаванне на асабліванні (HRW)
Хэшаванне з найвышым вялічынам случайнага коефіціента практыкуе апраноўкі кожнага ключа пры кожным вузле і выбірае паводзярнага:
function rendezvous(key, nodes) {
let best = null, bestScore = -1;
for (const node of nodes) {
const score = mix(hash(key), hash(node));
if (score > bestScore) { bestScore = score; best = node; }
}
return best;
}
Это ўсі алгорытм. Не існуе кольца, няма віртуальных вузлаў, няма сортаванай структуры, і няма чаго перабудаваць, калі зменшыцца колькасць учаснікаў.
Па двух найважлівейшых показніках ён таксама перавырашвае кольцевы алгорытм. Пераход з 8 на 9 вузлаў спрычыніў перамешчання 11,09% ключоў, пры тоеорэтычным мінімуме 11,11%, а баланс быў практычна ідеальны без жадных налаштаванняў.
Недзеяючы момент — цэе O(N) выконання за кожны пошук, таму што кожны ключ хэшуецца па кожным вузлу, і гэтыя затраты быстра растуць ў меру зростання колькасці вузлаў.
Выберыце яго, калі у вас менейш за адносна 30 вузлаў. Багато команд выкарыстоўваюць адносна восьмі вузлаў кэша, і для ўсіх яны будуць кращыя за алгорытм rendezvous hashing: яго лёгкае напісаць і розумець, а ён краща падтрымлівае баланс. Кольцевы алгорытм є болей вядомым, але не завжды і кращым.
Варыянт D: jump consistent hash
Гэты алгорытм, выданый Google у 2014 году, запісваецца прыблізна ў дзесять рэчэй, не выкарыстоўвае памяці, майже ідеальна збалансаваны і перамешчае мінімальную колькасць ключоў.
Яго мерыя — структурныя. Ён прысвойвае ключ номеру контейнера ў дыапазоне [0, N) і не мае уявлення пра ідэнтычнасць вузла. Контейнеры можна дадаць толькі ў канец дыапазона або выдаліць з яго; няма можлівасці вынуць вузел пад номерам 3 з середзіны, захаваўшы стабільнасць усёго іншага.
Выбірайце яго тады, калі контейнеры можна заменяць адзін на другі, і змінюецца толькі ўжо ў якіх іх колькасць, напрыклад, пад час разбівання набора данных між працоўнікамі з можлівасцю змены ўжо ў якіх іх колькасць. Ён не падходзіць, калі дадаюцца або выключаюцца конкрэтныя серверы з іменамі, што самэ так і ведаюць вузлы кеша.
Варыянт E: кільцо хэша з віртуальнымі вузламі
Это класычны дзеянне. Узлы і ключы дзеліць адным кругавым прасторам адресаў, і ключ належыць першаму узлу, які знаходзится за годзіннай стрэлкой ад яго.
Выберыце гэты варіант, калі флот большы гэткі, што лінейныя затраты на пошук пунктав пры сустрэчах становяцца значнымі, і вам таксама патрэбны узлы з вагамі і можлівасць адключэння будзь-каго з узлоў.
Выбіранне варыянта для данай ситуацыі
Пры перакладзе з 8 на 9 узлоў сярод мільйона ключоў, альтернатывы, кроме методу модуля, падходзяць майже да тэорычнага мінімуму, а баланс кольца сильна залежыць ад таго, скількі вярчынных узлоў атрымляе кожны сервер. Для гэтай системы е-камерцыі, з разным апаратам, узламі з іменамі, якія можуць зламацца, і флотам, які мае перавышыць 30 узлоў, кола ёсць правым выборам. Рэшта гіду паказвае, як правільна яго стварыць.
Як працюе кало
Занякшчыце арэй і застатакі. Уявіцеся калёсу, нумараваны з 0 да 2³² − 1, які заканчваецца ў верхней часты.
Весь алгорытм складаецца з двух правіла:
- Зробіце хэш кожнага назвы вузла на гэтым калёсе, так што
cache-01будзе розташоўвацца там, дзе паказае яго хэш. - Зробіце хэш кожнага ключа на тым жа калёсе, а потым перайдзіце за гадоўнік. Першы вузел, да якога дасягнёте, ёсць власнікам ключа.
Ключовая ідея закладзена ў тым, што вузлы і ключы дзеляць адно прастор адрес. Усё інша выводзіцца з гэтага, укладаючы ў сябе і прычыну таго, чаму дадаць новы вузел не ўскладнена.
Чаму змяны у членства застаюцца локальнымі
Як толькі на калёсе ставяцца новы вузел, ён падае між двума існуючымі. Ён перэймае пад свой контроль толькі дзвяжку межы сабою і сваім суседам у напрамку проты гадоўніка.
Ключы, які знаходзяцца за межам гэтага відрогу, не падаюць пад ўплыв і пераходзяць да свога паканальнага власніка так сама, як і раней. Новы учасник у сярэднем захопляе 1/(N+1) дзялу кола, таму частка ключоў змінюецца. Пры зменшэнні колькасці вузлаў з 8 на 9 гэта практычна станавіла 11,06%, пры чым мінімум складае 11,11%, тады як пры викорыстанні методу модуля для той самай змены і набора ключоў — 88,93%.
Выдаленне вузлаў працюе у зворачным напрамку: відрог узла, які выходзіць, пераходзіць да його наступніка па часовай стрэлцы. Пры зменшэнні колькасці вузлаў з 8 на 7 пераходзіць 12,60% ключоў, што ўздымаецца блізка да тэорычных 12,50%. Уплыв залишаецца мяркавым і пераканальным, а шасць іншых вузлаў не падаюць пад ўплыв.
Віртуальныя вузлы выправляюць дзіэнтарнасць
Вярнемся да задачы 3: чатыры вузлы на чатыро аднойчайных пазіцый ствараюць дужа неравнамірныя відрогі.
Рашэнь нейкак не простае: не размешчайце кожны вузел толькі адназначы. Размешчайце яго 160 раз пад 160 імен, які былі выведзены, напрыклад cache-01#0 і cache-01#1. Кожна з такіх імен упадае ў разны месца, таму кожны фізычны вузел мае 160 маленькіх, розсейваных лукоў замест аднаго вялікага, і закон большых чисоў выравнаважвае ситуацыю.
Пры аналізе мільйона ключоў на 8 вузелах баланс стабільна павышаецца з ростам колькасці реплікаў. 160 — это стандартная значэнне, таму што самэлькі тут крывая павышэння стае прагнучай, але 500 даўае яшчэ значна лепыя рэзультаты. Точка кольца патрабуе працоўна 12 байт (4 байта на пазіцыю плюс 8 байт на аднародную ссылку на власніка), таму восьць вузелаў з 500 реплікамі займаюць менейш чым 50KB. Якщо для вас баланс важнейшы за гэты объём памяці, выберыце большую колькасць; гэты расчытак мало якія команды беруцься выконваць.
Вяртуальныя вузлы таксама робяць наданню ваг практычна безкоштовным. Вузел з удвоеным запам’ятовуванням атрыбутаў отрымае удвоеныя балы, а значыць і працэздатнасць, якая заўсёды прыбліжна удвоюецца. Пры вагах 4:4:1:1 співвяшчанні склалася як 40,6%, 41,1%, 9,4% і 9,0%, тады як ідеальныя показнікі — 40/40/10/10.
Рэалізацыя кольца ў Node.js
Рэалізацыя складаецца з чатырох крокаў: гэшаванне імен вяртуальных вузлаў, ўпорядкуванне іх, а таксама викорыстанне бінарнага пошуку для знаходжэння власніка ключа. Клас нижчэй зберагае членства ў Map, перабудоввае упорядкованыя масівы, калі змінюецца членства, і надае метод get(key) для абшуку.
export class ConsistentHashRing {
#positions = new Uint32Array(0); // sorted ring positions
#owners = []; // owners[i] owns #positions[i]
#nodes = new Map(); // id -> { weight, points }
constructor({ replicas = 160, hash = defaultHash } = {}) {
if (replicas < 1) throw new RangeError("replicas must be >= 1");
this.replicas = replicas;
this.hash = hash;
}
addNode(id, weight = 1) {
if (typeof id !== "string" || id.length === 0)
throw new TypeError("node id must be a non-empty string");
if (weight <= 0) throw new RangeError("weight must be > 0");
if (this.#nodes.has(id)) return this;
this.#nodes.set(id, {
weight,
points: Math.max(1, Math.round(this.replicas * weight)),
});
this.#rebuild();
return this;
}
removeNode(id) {
if (this.#nodes.delete(id)) this.#rebuild();
return this;
}
#rebuild() {
const pairs = [];
for (const [id, { points }] of this.#nodes) {
for (let i = 0; i < points; i++)
pairs.push([this.hash(`${id}#${i}`), id]);
}
pairs.sort((a, b) => a[0] - b[0]);
this.#positions = Uint32Array.from(pairs, (p) => p[0]);
this.#owners = pairs.map((p) => p[1]);
}
/** Index of the first ring point >= h, wrapping to 0. */
#successor(h) {
const pos = this.#positions;
let lo = 0,
hi = pos.length;
while (lo < hi) {
const mid = (lo + hi) >>> 1;
if (pos[mid] < h) lo = mid + 1;
else hi = mid;
}
return lo === pos.length ? 0 : lo;
}
get(key) {
if (this.#positions.length === 0) return null;
return this.#owners[this.#successor(this.hash(key))];
}
}
Тры выборы дизайну заслуговуюць на увагу.
Паралельныя масівы заместо масіва об’ектаў. Зберагчы пазіцыі ў Uint32Array можна выконваць бінарны пошук у компактнай, суседзячай памяці, якая застаецца ў кэшы CPU. З 8 вузламі і 160 реплікамі існуе 1 280 точак, прыблізна 5 КБ, а вычысленне пазіцыі трывае адпаведна 11 порэванаў.
Эфект «звороту» у lo === pos.length ? 0 : lo. Ключ, які пасля хэшавання пераходзіць за межы апошней точкі, належыць першаму вузлу на коле. Ігнораванне гэтага умовы — найчастэйшая памялка ў самастраўніх кольцах: рэзультат є правільным для практычна всіх ключоў, але незрозумела неправільным для калькі, якія знаходзяцца блізка да верхней межы дыапазона.
Перабудова толькі пасля змены прыналежнасці, а не пасля вычыслення. Змены прыналежнасці трапляюцца рэдка, а вычыслення відбываюцца дзесяткі тыяч разоў у секунду, таму сортаванне 1 280 ентрытаў час а час не карыстаецца значнымі зусіллямі.
Реплікацыя, тое самае падбор наступных власнікаў ключа, — это дваржыба па гадзіннікавай стрэлцы, якая збірае разныя фізычныя вузлы. Слова "разныя" маюць значэнне, таму што суседзяючыя точкі на кольцы ў большай частцы належаюць да таго ж сервера:
getReplicas(key, count = 1) {
const n = this.#positions.length;
if (n === 0) return [];
const wanted = Math.min(count, this.#nodes.size);
const out = [];
const start = this.#successor(this.hash(key));
for (let step = 0; step < n && out.length < wanted; step++) {
const owner = this.#owners[(start + step) % n];
if (!out.includes(owner)) out.push(owner);
}
return out;
}
Заўважыце, што значэнне wanted обмежваецца колькасцю фізычных вузлоў, таму запит на больш лік реплікаў, чым сервераў, не можа трываць безканечна, і дваржыба ў будзь-якім случае зупініцца пасля адной цэлай петлі.
Актыўны выбор функцыі хэша
Багато навучальных матэрыялаў прыхоўдзяюць ціх частак, а саме ў яных крыўдзяцца найболей павучальныя рэзультаты всіх вядомаў.
Большасць рэалізацый кольца ў стандартным режыме викорыстоўваюць MD5. Ён працуе, але ўповільнюе роботу: кольцо, якое яго викорыстоўвае, досягала толькі 423 000 запытоў за секунду, а аналіз показаў, што майже весь час выканання прыходзіцца на обробку дадзеных па методу MD5.
Замена яго на FNV-1a, шырокае некрыптаграфічнае хэш-функцыя, падняла працэздатнасць прыбліжна у 15 разоў. Аднак баланс згас: стандартная дывергэнція навантажэння на кожны вузел паднялася з 10,4% да 30,7%.
Аналіз вычысленых пазіцый каля калькольных вяртуальных вузелаў паказвае прычыну:
cache-01#0 → 4037809751
cache-01#1 → 4021032132
cache-01#2 → 4071364989
cache-01#3 → 4054587370
cache-01#4 → 3970699275
Усе яны знаходзяцца ў вузкай дыяпазоне адносна 4,0 мільярда. FNV-1a асобліва чутліва да эфекту аваланча, тое значыць, падобныя вхідныя даны даюць падобныя выходныя рэзультаты. Назвы вяртуальных вузелаў разлічаюцца толькі суфіксам, таму замест таго, каб 160 пунктаў распаўзліся по колу, кожны вузел складвае іх у адну ўздымленую групу. Оптымаўванне за магчымасцю працы таямніча вярнула проблему 3.
Рашэння — працаваць з выходам FNV через фіналізатор для сумешання бітоў, які ёсць пасляпэльным крокам у MurmurHash3:
function fnv1a(str) {
let h = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
}
// Scrambles the bits so near-identical inputs land far apart.
function fmix32(h) {
h ^= h >>> 16;
h = Math.imul(h, 0x85ebca6b);
h ^= h >>> 13;
h = Math.imul(h, 0xc2b2ae35);
h ^= h >>> 16;
return h >>> 0;
}
export const defaultHash = (str) => fmix32(fnv1a(str));
fmix32 адмініструе перекіды, операции XOR і множэння так, каб змяна будзь-каго вхіднага біта распростраўлялася на всі выходныя біты. Math.imul адрабатвае справжнее множэнне целых 32-бітных чисоў, а >>> 0 перакладае рэзультат знадвор'я ў беззнаковае 32-бітнае число, якое падходзіць для Uint32Array. За рахунак дзесятка дадатковых аперацый сумарны хеш быў балансаваны лепей, чым у MD5, пры тым яго выконанне было приблізна у 13 разоў шырэе.
Шырэйшы урок паслужыць не толькі для хэшавання: калі вы заменяеце які-небудзь компонент на шырэйшы, памерце тую характэрыстыку, якую вы не оптымізавалі. FNV-1a — гэта абсолютна прыемны хеш; ён проста не падходзіць для гэтай задачы, і ніч у його апісанні не паведамляе пра гэта.
Ператворэнне кільца ў кліента кешу
Адзін кольцо сама па сабе не ўтварае кліента кэшу. Код для прыменнага выкарыстоўвання павінен справляцца з неудачамі, і кольцо праставляе простую стратэгію: перайсць да наступнага вузла за часовым направленнем.
Указаны вышэй кэпсулкаваючы код бере карточку з іменамі кліентаў, створывае з яёх кольцо і пад час кожнага вызову get пробуюе першыя failoverDepth власнікаў па порядку. Вузел, які выклікае проблемы, пазначаецца на час downtimeMs і адключаецца з кольца, пасля чаго знову дадаецца ў яго склад, калі заканчываецца тэрмін його пазначкі.
export class ShardedCache {
#ring; #clients; #down = new Map();
constructor(clients, { replicas = 160, failoverDepth = 2, downtimeMs = 10_000 } = {}) {
this.#clients = new Map(Object.entries(clients));
this.#ring = new ConsistentHashRing({ replicas });
for (const id of this.#clients.keys()) this.#ring.addNode(id);
this.failoverDepth = failoverDepth;
this.downtimeMs = downtimeMs;
}
#markDown(id) {
this.#down.set(id, Date.now() + this.downtimeMs);
this.#ring.removeNode(id);
}
#reviveExpired() {
const now = Date.now();
for (const [id, until] of this.#down) {
if (now >= until) { this.#down.delete(id); this.#ring.addNode(id); }
}
}
async get(key) {
this.#reviveExpired();
for (const id of this.#ring.getReplicas(key, this.failoverDepth)) {
try {
return { value: await this.#clients.get(id).get(key), node: id };
} catch {
this.#markDown(id);
}
}
return { value: null, node: null, allDown: true };
}
}
Адрабатка гэтага коду на чотырох сімуляваных вузлах, якія хранилі 10 000 ключоў, а пасля чаго быў выключаны адны з іх, дала наступныя рэзультаты:
Keys per node: cache-01 2213 | cache-02 2462 | cache-03 2923 | cache-04 2402
Killing cache-02...
served from cache: 7538
cache misses: 2462
hard failures: 0
healthy nodes: cache-01, cache-03, cache-04
24.6% of traffic became a miss.
Гэты показнік — це ваш план ўрабоцкай спроможнасці. У флоте з чатырох вузлаў адна нештача прыводзіць да таго, што ўсё дадаткова 24,6% запытань на чытанне падае на базу данных, тады як у флоте з восьмі вузлаў гэта практычна складае 12,5%. Якщо база данных не можа вытрымаць 1/N вашага навантажэння на чытанне, якое прыходзіць адразу, то справжняя проблема — у ўрабоцкай спроможнасці базы данных, а не ў кэшаванні, і метод цяпловага хешування ператварыў гэтую проблему з фатальнай на видную. Таксама зазначыце, што не было жадных серьёзных нештач: запытанні на ключы зламанага вузла пераводзіліся на наступны вузел і стаўалі звычнымі невдалымі запытаннямі.
Проблемы практычнага выкарыстоўвання, якія алгорытм не пакрывае
Некалькі проблем застаюцца пахацянэ гэтага алгорытму і будуць заважаць вам у будзь-якім случае.
Неравенасць версій кольца
Кільце будзе эфектыва толькі тады, калі кожны кліент вырахоўвае той самы адпаведны рэзультат. Паступова вводзіце змяны ў склад членства: прыблізна на калькі хвілін палова флоту бачыць 8 вузлаў, а другая палова — 9. Две групы не пагадваюцца ўжо ў стосунку да прыблізна 11% ключаў. Для кэшу гэта значыць короткі спад у шчыльнасці прабаў; для всіх систем, якія прыманяюць змены дадзеных, гэта означае рознакі ў дадзеных. Версіявайце набор членства, распрасцюйце яго через адны канал і фіксуйце версію ў сваіх показніках, каб вы моглі бачыць асэтненне, а не толькі здагадваліцца пра яго.
Занадто апошні выключэнне вузлаў
Не трэба адчыняць вузел пасля аднаго тайма-аўтамату. Вузел, які постаянна выходзіць і знову прыўязываецца, спрычыняе шторм змін, таму што кожная пераходна процедура перасунуе 1/N клучоў. Перш чым адчыняць вузел, трэба спачатку пабачыць калькольныя неудачы ў паводзе часу, а потым аберагвальна яго знову прыняць. У прыкладзе вышэй викорыстоўваецца фіксаваная штрафна пэнальтія у дзесять секунд; код для працы ў рэальных умовах павінен викорыстоўваць экспанэнцыйны падход да адкладання спроб і пераканальную перапрацоўку стану, прычым прыняць вузел знову.
Гарячыя клучы не ўзвешаны
Кансістэнтны хэшынг равнамерна распрадзеяе клучы, ал нічога не кажае пра запыткі. Калі адзін продукт раптам становіцца надзвычайна популярным, яго запис знаходзіцца на аднам сервере, і гэты сервер перагреваецца, нават якщо система кансістэнтнага хэшынга выконвае свою роботу. Існуюць два спосабы рашэння: маленькі кэш у самай працы пад роўнем кансістэнтнага хэшынга для самых популярных клучоў, або варіянт обмежаных навантажэнняў кансістэнтнага хэшынга з даследжэнняў Google, який лімітуе колькасць дадзеных, якія можа прыняць кожны вузел, і адправляе застаток у напрамку гадоўніка.
Перэзначэнне — гэта не міграцыя
Усе вышесказанае прыме, што пазбітка клячкі коштае толькі працяў памяці кэшу. Якщо кольца перадае стойкія даны, то фраза «11% клячак перасунутых» значыць, што 11% даных неабходна фізычна скопіюваць на новы вузел, прычаму ў іх можна будзе чытаць, зазвычай чытання або запіс ведуцца ў оба месцы пакуль копіюванне не завершыцца. Кольцо визначае што трэба перасунуць; сама яго не перасувае. Самэ гэтая прычына, чаму такія системы, як Vitess, павязаны з координатарам: ім неабходна кераваць міграцыяй, а не толькі яе вылічваць.
Колькасць реплік фактычна ўстойчыва
Змена значэння replicas з 160 на 500 перасувае кожную позыцыю кольца і пераранжоўвае майже всі клячкі, што ёсць настолькі ж разламным, як і змена модуля. Спрыяйце гэту рашынку як одноразовай дыяжанне па проекцыі, якая прымеўчваецца да таго, калі ўжо няма жывых даных, а як не ведаеце — выберыце большое значэнне.
Калі кольцо — не падходячы інструмент
Частка інжынярскага суджэння — гэта здатнасць распазнаваць, калі вражаючая рашэнка ёсць неправильная.
- Менейша колькасць узлоў — адносна 30: выкорыстаць метод хэшавання на асабліванні. Ён трэбуе менш коду, краща балансуе і не выклікае патрэбы на налаштаванне колькасці реплікаў. Џедыная перавага такога падходу — шуканне за часам
O(log N), якое ў такых мераках є акадэмічным. - Пераменныя контейнеры, у якіх зменшуецца толькі калькуляцыя: выкорыстаць метод хэшавання з перескокамі; для яго патрабуецца толькі дзесятак ліній коду і жаданая колькасць памяці.
- Данные, якія трэба стабільна кераваць: выкорыстаць координатора. Толькі явны карточны апіс дазволяе перасунуць адзін шард, стежачы за яго наследкамі, чаго не можа зрабіць метод хэшавання.
- K, які насправды ніколі не зменшуецца:
% Nбудзе падходзячым. Не трэба ствараць складную сістэму для змены, якая ніколи не адбудзецца.
Кільцо здобывае свое месца, калі вузлы маюць імены, ўсеяныя, множыстыя і спраблівыя да збою. Гэта па-майш прыгожа апісвае флот кэшаў, і самэ гэта прычына таго, чаму столькі распрацаваных кэшаў будуецца на аднам такім флоте.
Ключовыя выводы
- Основная ідея простая: размістаць ключы і серверы ў аднам прасторе адресаў, так што змена члена флоту парадзейвае толькі суседню зону, а не весь флот.
- Модульна маршрутазацыя ператварае кожны прыбутак у велічыну і кожны збой вузла ў майже цэлычны спалош кэша, пры чым збиткі растуць разам з размерам кластэра.
- Хэшаванне на основе зустрочы часта ёсць кращым выборам для малых флотаў; выбірайце кільцо, калі важныя ўсё: размер, вага і дэтальнае адключэння вузлаў.
- Без віртуальных вузлаў аднам серверу можа давацца колькісна большая частка, чым яго справядлівае падзел. Выберыце колькасць рэплікаў з самага пачатку, бо ўзмены пазней пераранжуюць усё.
1/N запытаванняя падае на базу дадзеных. Размір базы дадзеных трэба выбіраць з учыткам гэтага, а таксама з урахоўваннем змян у складзе версій, аберагаючы падчас выключэння вузлаў, і окрема карміцца з проблемамі, связанымі з «гарячымі» клучамі.Сам рынг складаецца толькі з калькоў дзесяткаў лінак коду. Інжынерныя рашэння, якія забезпечваюць яго надзеямасць у практычным выкарыстоўванні, — це все, што апынулася ўсякоўкола яго.
Спадневаная літэратура
- Стварэнне надзеямасных систем фонавых заданняў з BullMQ і Redis — Дазвольце вам дазнацца, як проектаваць стойкія пайплайны фонавых заданняў у Node.js з выкарыстоўваннем BullMQ і Redis, уключаючы механізмы перапрыбутку, канкурэнцыю, ідемпотентнасць і манітарынг.
- Проектаванне бэкендаў для чату у рэальным часе: камнеры, зберагачванне дадзеных і масштабаванне — Дазвольце вам дазнацца, як спроекаваць бэкенд для чату у рэальным часе з выкарыстоўванням Socket.IO, PostgreSQL і Redis, уключаючы камнеры, порядак зберагачвання паведамленняў, статус прысутнасці і масштабаванне на калькі сервераў.
- Ад працуючага скрыпта да сервісу для практычнага выкарыстоўвання: што выклікае патрэбы Node.js — Дазвольце вам дазнацца, як цыкл змаганняў, блакуючыя задачы, таймауты, канкурэнція, можлівасць абзору і доступ да базы дадзеных паўтараюць форму бэкенда Node.js, який можа функцыонаваць у працэсе виробніцтва.
- Усередзіне мнагаадавальнай службы паведамленняў: аберанцы, запасныя варыянты, доверлівасць — Практычны аналіз дыяжурнага падходу да стварэння надзеямогучай службы паведамленняў: зберагчык атрыбутаў, запасныя варыянты ад адавальнікаў, прабныя запускі BullMQ і викорыстоўванне DLQ, справядлівасць даступу для розных корыстнікаў і перакананне ў падписях webhook.
- Забавянне нечыстых кэшаў Redis у Node.js: анулюванне дадзенняў без чытання застарэлых версій — Пораўнанне методаў TTL, анулювання дадзенняў праз зміны, моделі pub/sub і анулювання дадзенняў па версіях ключоў у Redis у Node.js, аналіз проблем, якія гэтыя методы ствараюць, і стварэнне кэшовага пакету з функцыяй автаматычнага адключэння у разе нештапланых ситуацый.
- Cache Stampedes у Node.js: Чаму кэш Redis можа перавантажыць вашу базу дадзеных — Дазвольце дазнацца, чаму простая наладка кэша з использованнем Redis синхронізуе навантажэння на вашу базу дадзеных, калі заканчываецца тэрмін дзейснасці важлівага ключа, і як блакіткі ўправы та фонавая апдэйтаванне запобегаюць гэтам.