GraphQL запыт, які выкаранаў буфер базы дадзеных
Адзін вкладаны документ GraphQL згорэў у базе дадзеных працоўнай сістэмы. Чаму не выканаліся ліміты колькасці запытоў, тайма-ауты HTTP і DataLoader — і чатыры меры, якія нарэшце яго стабілізавалі.
Адзін запыт GraphQL POST выключыў API на майже гадзину — і гэта не было злым намерам
Проблемы пачаліся сярод вечара ў выходны, чырвоўгадзінкай.
Працэсор Postgres працаваў на пачатковай моцы. У базе не засталося вольных з’ўязкаў. Кліенты сталкнуліся з працягванням часу адпаведзення з боку шлюза. Публічны API быў абэцька недоступны ў вечар па недзелю, які зазвычай ўсё ж такі ўскладнены для данага продукту.
Об’ём трафіку быў звычны — нават трохи меншы, чым зазвычай, таму раптовае падвышэння было малая можлівасць.
З середы нічога не выклікалася, таму паказвалася мала можлівасць проблемы з розгортанням.
Прыблізна чверць гадзіны розследаванняя выявілі прычыну, якая на першы погляд здалася абсурдной.
Толькі адзін HTTP-званак. Адзін POST /graphql ўжо праз прыблізна паўтора мінуты виканаў роботу з базай дадзеных і яшчэ працаваў. Штырый адзін такі запуск вже зайняў больш часу базы дадзеных, чым усе паказванні ў пачатковыя калькі гадзіны.
У наступнай часты наводзіцца структура дакумента, прычыны, чаму існуючыя механізмы контролю яго не зазначылі, а таксама чатыры межы, якія былі выявлены за калькі дзён пазней. У заканчэнне паказваны найгоршы варыянт: як мало зусіль патрэбна было агульнаму працоўніку, каб скорастыць атаку на тую ж самую слабасць.
Запыт
Дакумент, хоць і скорачаны, быў структурна правільны і нагадваў:
query {
organizations {
members {
user {
organizations {
members {
user {
organizations {
members {
user { id, email }
}
}
}
}
}
}
}
}
}
Структура складалася з семі роўней, якія стваралі цыкл: організацыі маюць членства, членства вяду да адзінакоў, а адзінакі зноў належаць да організацый.
Этыя грані ўсё ж рэальныя і двунаправленыя, таму ўзьмець іх у такім выглядзе — правильна падходжэння. Распалювачы працавалі нормальна. Кожны окремы запыт SQL працаваў добра і швядка.
Панэроз выйшаў з-за комбінатарнага росту.
Тыповыя абліканты знаходзяцца працоўна ў трох організацыях. У тыповых організацыях є адносна пяцьдесят чалавек. Расширэнне гэтага лесу дае такія показнікі:
- глыбіна 1 → ~3 організацыі
- глыбіна 2 → ~150 членства
- глыбіна 3 → ~150 корыстувачаў
- глыбіна 4 → ~450 організацыяў
- глыбіна 5 → ~22,5 тыс. членства
- глыбіна 6 → ~22,5 тыс. корыстувачаў
- глыбіна 7 → ~67,5 тыс. організацыяў
У самым ніжнем роўні є майже сяродзесят тысяч елементаў, кожны з якіх спрыяе дадатковым запытам пра членства. Расширэнне яшчэ трывало, калі аператары зупінілі работу.
Короткі текстовы дакумент. Няма працоўных слабэсцей з аутаналізам. Няма страк, якія можна было б увести. Нічага, што могла б зазначыць сканер. Схема проста следавала графу, які ўпубліковала.
Адыяктычнае, а не ворагоўское
Парадыгма мае значэнне для урока.
Звонак быў адправлены через атрыбутаваную сесію адна з працавальнікаў. Інжынер практычна працаваў у Apollo Studio, выявляючы патрэбныя данні для інтерфейса, адкрываючы вялізныя поле, каб пазнакоміцца з існуючымі дадзеннямі.
Яны нажалі на «Выконаць», побачылі, што інтерфейс застаўся нерухомым, звалілі віну на сеть і закрылі вікно браузера.
Закрыць вікно не спыняе процесы на серверах. Сокет зник, але выконанне продавалася; база дадзеных продавалася обрабоцваць дзесяткі тысяч элементаў, пры чым ніхто не чакаў на адпаведзь.
Яны дазналіся пра перыв у роботе толькі з паведамленням пра інцыдэт з панедзелкі. Нічога аграсыўнага не выйшло: яны вжывалі інструменты, якія ўступіла компанія, і працавалі за схемай, якая таксама была наданая компаніяй.
Чаму існуючыя захоўнікі не спрацавалі
Контрольныя механізмы існавалі. Але ні адны з яных не падходзілі для такога типу абякання — і самэ гэта ёсць сутніста проблемы.
Ліміты запыткаў на аднаго IP. Ліміты вылічваюцца па колькасці HTTP-запыткаў за хвіліну. Адны запытк = адны запытак. Система лімітацыі праверна яго прыняла.
Тэрміны выканання HTTP-запыткаў. За 30 секундав прыключылася тайма-аут; кліент побачыў код 504. SQL-запыткі на бэкендзе продаваліся, таму што закрыць сокет не значыць завершыць роботу, якая ведзеца на ўсіх серверах. Кліентам сказалі неправду; сервер продаваў ресурсы.
DataLoader. Команды часта вважаюць групаванне запыткаў заходам безпекі.
За адну мілісэкунду DataLoader з’едынае дублікатныя запыткі ў адну і справжняя спосабам усуне проблему N+1. На глыбокім роўні дадзенняяў тысячы запыткаў з’едынаюцца ў калькі запыткаў типу WHERE id IN (...).
Складванне болейш чым двадцяці тысяч ID ў адну заявку не робіць гэтыя рядкі вольнымі. Колькасць запытоў зменшаецца; кардинальнасць — ні; глыбокія рэвэры все ўсё існуюць. Групаванне — это спосаб падвышэння эфектыўнасці, а не максимальная межа. Эфектыўнасць часта плуталі з такой межай.
Уваходжэнне і ідэнтыфікацыя. Апёлюючы корыстувальнік быў уваходзілі. Ідэнтыфікацыя адпавядае на пытанне хто, а не насколькі це дорога.
Разрэшенні па полям. Кожна выбраная поль была дазволена для гэтага корыстувальніка. Автарызацыя прыйшлася на успех. Канфлікт вынікаў з великай колькасці законных запытоў, а не з забранымі дадзеннямі.
Як жа потворна выглядае тая ж самая праслойка пад атакай
Пасля вяснавання паўчасці дзён было пасвячана моделюванню ворагоўскага выкарыстання той самай праслойкі. Саме гэта моделювання стало прычыной існавання гэтага матэрыялу.
Алясы дазволяюць аднаму дакументу павтарыць поль з разнымі параметрамі:
mutation {
a1: login(email: "target@company.com", password: "000001") { token }
a2: login(email: "target@company.com", password: "000002") { token }
a3: login(email: "target@company.com", password: "000003") { token }
# ... two thousand more
}
Яшчэ адна HTTP-запрос. Частота запыткаў контролюецца, як і ў першым случае. Лічыльнік блакавання ад прыходжэння, який актываўся пасля пяці невялікіх неудач, знаходзіўся ў самам рэшары і правернуто післяў лічбу тыяў тысяч спроб.
Такім чынам, атака з множлівых спроб введэння пароляў была зупінена аднойчы — лічыльнік якраз знаходзіўся там, дзе фактычна ведуцыяся роботы.
Больш заўсёды застаўся неконтроліруемым. Будзь-які дорогі рэшар могаў быць выкарыстоўваны сотні разоў у адным запытку, пры чым ліміты частоты запыткаў не дзейнамалі: пошук, адчыт заданняў, вызовы трэціх сторон. Гады регулювання частоты запыткаў у стылі REST сталкнуліся з API, які не параднаецца REST.
У прыменні таксама застаўся увімкнуты механізм самапрацэўкі. Кожны могаў заглянуць у цэлыя графы типоў — з усімі ўз’ёмамі — і стварыць дакументы з максимальнымі виткамі без неабходнасці спадзявання.
Ніхто такога не рабіў. Удача — гэта не спосаб кантролю.
Чатыры межы, якія былі даданы пазней
Каля калькі дзён роботы інжынераў былі даданы наступныя межы, распараджаныя па ўплыве.
1. Абсалютная глыбіна
Першы і найпросты спосаб: адмова ў обработцы дакументаў, якія знаходзяцца на глыбі большай за заданы ліміт.
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [depthLimit(7)]
});
Была адбывана дзеянна з рэальным трафікам кліентаў. Глыбокія запыты, якія маглі быць обработаны, зупініліся на пяці роўнях. Ліміт быў паднесены да семи — гэта даў можлівасць для росту, а таксама дазволіла адмовіць у обработцы паталагічных запытоў пры ўжыванні рэзалвераў.
Правілы верыфікацыі пераглядаюць аналізаваны дакумент пры пачатку выконання, таму адмова ў обработцы здзейснюецца практычна без каштоўкаў.
2. Аналіз каштоўкаў запыту
Толькі глыбіна недастатня. Нават паверхневы дакумент, які запрашае дзесяць тысяч элементаў спісу, залішаецца вельмі вялікім па об’ёму.
Система расчытку каштоўкаў прыдзеляе вагі полям, множыць іх на колькасць элементаў у спісе і адмовляе ў обработцы запыту, які перакроўляе заданы бюджет.
const server = new ApolloServer({
schema,
plugins: [
createComplexityPlugin({
maximumComplexity: 1000,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 })
]
})
]
});
Паў’язка элементаў ёсць простая; выбір вагі — гэта складная частка. Скаляры коштуюць аднае вагі. Спісы коштуюць першы раз стоімасцю кожнага з ўсіх іх элементаў. Рэзалверы, якіе запрашаюць дапамогу трэціх сторон, атрымуюць ручна заданыя вагі, напрыклад п’ятдесят.
Двух дзён на налагоджэнне на адказах з рэальных логаў дастало ціфры прыблізна правильныя. Прыблізна правильнае было дастатнім.
3. Ліміты аліяў і колькасці вузлаў
Установіце ліміты аліяў на адну операцыю і загальная колькасць вузлаў AST.
Пяцідзесят аліяў плюс ліміт вузлаў падходзілі для рэальных умоў; жанчыны-кліенты не падходзілі да такіх значэнняў. Викорыстанне тыяч аліяў прыводзіць да памылак пераверкі, а не да масовых проблем з рашэнням.
Пакетаваныя захоўнікі дапамагаюць. GraphQL Armor аб’еднвае кантроль глыбіны, костаў, аліяў, дырэктываў і можнаць тыпавага аналізу. Командам, якія працуют з нуля, следуець установіць і налашаваць гэты пакет прычым перад тым, як ствараць кожную частку занова.
4. Таймаут для запытоў у базе дадзенаў
Пяршы захоўнік — і найшыльнейшы спосаб выгрышу:
ALTER ROLE api_user SET statement_timeout = '10s';
Заявкі пры выкарыстанні ролі аплікацыі згінуць за дзесяць секунд. Не HTTP-сокет, а сам SQL. Ляч гэта толькі зменіла бы часы перыву з ~94 секунд роботы БД да дзесяці, без якога-лібо досвяду ў GraphQL.
Функцыя інтроспекціі ў працоўным режыме была выключана за дапамогою настройкаў той жа тыдзень — гігіена з самага пачатку, якая была прыменена нечыста.
Урокі для ранейшай версіі таго жа калектыва
Тры нагадванні.
Контролюйце косты, а не толькі кантэнт запытаў. Пільгавы пісак пра колькасць HTTP-запытаў ёсць прывычкай REST. GraphQL можа схаваць будзь-якую роботу ў аднам POST. Якщо пісак бачыць толькі запыты, то няма справжньага пісака. Косты пісака.
Тэрміны павінны зупініць роботу. Таймаут HTTP у трыдзесят секунд здаваўся захоцкім і толькі сховваў пашкоджэння ад корыстувачаў, пакуль бэкенды продавялі роботу. Установіце таймауты там, дзе ведаецца робота — для Postgres, statement_timeout у ролі.
DataLoader не абсалюе розмер. Функцыя батчавання усувае эфект N+1 і робіць болей дашавымі запиты па кожнай выйзд-прыходзе. Але гэта не робіць іх малымі. Канэктыўнасць і верхнія межы — гэта разныя проблемы; трэба адразу рашыць обе.
Спіс пераканальнай перапаловкі для практычнага выкарыстоўвання GraphQL
Штоўна пераканайцеся ў гэтым. Большасць пунктав трэба выконваць за калькі хвілін.
- Інтроспекцыя выключана ў режыме практычнага выкарыстоўвання? Як не так, то весь шыма будзе доступны для всіх.
- Установлена верхня межа глыбі? Замерьце глыбокія запиты; установіце значэнне трохкі вышэй за іх.
- Установлен бюджет на витраты? Толькі глыба не ўрахоўвае дужа большыя спісы.
- Установлена верхня межа для аляск? Часта не ўрахоўваецца; гэта запобегае неконтрольаваным запитам.
- Ролі базы дадзеных
statement_timeoutналаштаваны? Час выконання запіту; гэта значэнне стосуецца всей такой клясы.
Этая каманда пачала з аднаго з шасця варыянтаў. Яны зараз выкорыстоваюць усе шасць; чатыры з іх прыбылі за адзин дзень.
Правіла
Зберагце гэта раззлічэнне:
Канцэнтры REST працуюць за прадзеяннем дизайна. GraphQL дазваляе кліентам самім кераваць працэю. Як толькі сервер не встановіць явны ліміт, прызначаны для данага запыту, гэты ліміт не перамешчаецца — ён проста выдалываецца.
Ранейшыя методы захавы прыпускалі, што серверы вядуць, насколькі дорогі можа быць запыт. GraphQL перадае гэты адпаведны контроль таму, хто пішаў дакумент — гэта ўзнемагучы факт і адна з паўстаноўчых прычын для ўжывання GraphQL. Адпаведальнасць неабходна вярнуць у код; фрамворк гэтага не зрабіць.
Практычна годзіна перыяду пачалася, калі аднаго з колег у студыі нажаў кнопку «Застартаваць». Гэта дружелюбная версія. У агрэсыўной версіи былі патрэбны толькі адказнае кантакты і короткая думка; гэта ніколі не выйшла, таму што ніхто не прабаваў.
Спачатку пераканайцеся ў настройках самапрацэвыявлення. Це займае калькі секунд, і багатыя команды вядома ведаюць адказ.