Surviving highload with Node.js [ukr]

Talk presentation

Is Node.js suitable for highload applications? Yes! But what makes the regular Node.js application suitable for highload that will it survive 100k req/s for a reasonable price? In my talk we will go through potential bottlenecks, and how to find and fight them. What metrics can show us potential problems? How to decide, when and how we should scale our application? Also, I’ll show some interesting cases of debugging issues in real-world highload Node.js apps.

Andrii Shumada
WalkMe
  • R&D Team Lead at WalkMe
  • 14 years in Software Development
  • Engaged in development and deployment of high-load projects
  • A member of the program committee at Fwdays conferences, public speaker
  • He likes concise and beautiful code that does’t need comments
  • Besides programming enjoys riding on a motorcycle
  • Twitter, GitHub, website

Talk transcription

Радий знову бути на цій офлайн сцені, нарешті, після кількох років. Тому я буду радий поділитися з вами своїм досвідом. Трішки про себе. Я вже 14 років в IT, 13 років в Node.js розробці, ще з версії 0.6. Зараз працюю Team Lead команди розробки в WalkMe і займаюся високонавантаженими сервісами. Перед тим, як розглянути, як вирішувати проблему, давайте розберемося з термінами. А що таке Highload? Багато людей може порівнювати свої системи. Там кількість запитів, але я вважаю, що Highload – це система, в якій вже вам не вистачає двох серверів для роботи вашого додатка. Сьогодні не будемо говорити багато про архітектури, постараємося зосередитися тільки на Node.js і згадати просту архітектуру, в якій у нас є лоуд-балансер, на який надходять запити. Запити балансуються між кількістю нод або подів, які маємо, і далі ці запити надходять вже до якогось… третьої застосунків, де є бази даних, черги та інше.

Припустимо, що лоуд-балансер – це вже готове рішення, яке витримує всю цю навантаженість і може її розподілити. Залишимо базу даних для DevOps, хоча в принципі нам не потрібно розбиратися в цьому. Сьогодні фокусуємося на нодах. Чому два сервіси? Чому два плюс – це Highload? Тому що два – це мінімальна кількість серверів, яка необхідна навіть для... додатку без великого навантаження. Тобто, як тільки ви запускаєте ваш додаток у продакшен, вам вже має бути два сервери. Для чого? Як мінімум для того, щоб один сервіс може, не знаю, просто відвалитися, вимкнутися з якихось фізичних причин, незалежних від вас. І з іншого боку, вам потрібно мати можливість оновлювати ваш додаток без даунтайму, щоб він постійно продовжував працювати. Тобто, ви можете оновити, наприклад, одну ноду, а потім іншу ноду. Все, що більше, я вже вважаю Highload. Скільки це запитів? Я думаю, неважливо. Запити у кожного різні. Навантаження, безпосередньо робота сервісу у кожного різне. Коли нам потрібно вже більше двох серверів? Коли ваші клієнти починають скаржитися? Коли технічна підтримка повідомляє, що сайт тормозить, все погано, і щось потрібно робити. Або коли ви за своїми метриками бачите, що щось не так. Тепер, коли ваші клієнти залишилися, ви можете зрозуміти, що з сервером щось не так, і з ним потрібно щось робити. Звичайно, краще цей момент виявити самостійно, а не чекати, доки про це напише технічна підтримка, дізнається ваше керівництво та інше.

Можливо, все ж таки потрібно оптимізувати вашу систему? Ну, коротка відповідь – так, в принципі потрібно. Але можливості оновлення, оптимізації вашої системи, вони не безмежні. Рано чи пізно ви все одно дійдете до обмежень вашого процесора, і ви рано чи пізно досягнете моменту, коли вам просто не вистачатиме процесора для обробки цих запитів. Оптимізувати потрібно, але рано чи пізно ви все одно дійдете до межі. І вам доведеться додавати ще сервери до вашої інфраструктури. Отже, додавання більше серверів до інфраструктури звучить як логічний спосіб для того, щоб забезпечити обробку більшої кількості запитів. Так, але питання – скільки? Скільки серверів вам потрібно? Можна додавати 20 серверів, можна додавати 9, можна додавати 10, питання – скільки? Ну, щоб відповісти на це питання, потрібно повернутися до вимог, які ми ставимо до нашої системи. Тобто, ми всі працюємо в якихось бізнесах, отримуємо зарплату, можливо у вас є власні проекти, але коли ви робите будь-які рішення у вашій інфраструктурі, потрібно розуміти, тримати завжди на увазі, що ви хочете досягти. Це такі базові речі, як аплікація має відповідати 200 статус-кодами, потрібно, щоб було більше 200 відповідей і майже не було 500. Потрібно, щоб було невелике back-end latency, щоб ваш сервіс відповідав вчасно. Бо якщо ви чим довше відповідаєте клієнтам вашим, тим, звісно, більше ви втрачаєте грошей. Ну і, звісно, бізнес завжди звертається до грошей. Пам'ятайте, чим менше ми витратимо на нашу інфраструктуру, тим більше зекономить бізнес і, можливо, поділиться з нами прибутком. Як досягти цього?

По-перше, обов'язково потрібно моніторити вашу систему, включаючи показники, такі як використання процесора. Для оптимальної роботи визначено, що краще тримати процесор у діапазоні 40-60% середнього навантаження. Також важливо контролювати використання пам'яті, яке має залишатися менше 50%. Потрібно розуміти, як змінюється кількість запитів протягом дня та слідкувати за метриками, такими як кількість активних запитів та обробників. Це допомагає налаштувати систему автоматичного масштабування для ефективного використання ресурсів та зменшення витрат.

Припустимо, що графік трафіку досить прогнозований і змінюється від 8 до 10, збільшуючись в 3-4 рази. У такому випадку можна налаштувати автоскейлінг, щоб автоматично збільшувати кількість серверів залежно від навантаження на CPU. Це дозволяє заощадити гроші, використовуючи лише необхідну кількість ресурсів у потрібний момент. Проте, в деяких випадках автоматичне масштабування не є ефективним, особливо при спайкових навантаженнях, коли важко передбачити зростання запитів. У таких випадках слід ретельно аналізувати роботу системи та шукати шляхи оптимізації коду, використання кешування та інші методи для зменшення навантаження на сервер.

Також важливо слідкувати за метриками Node.js, такими як Event Loop Lag, щоб вчасно виявляти проблеми з продуктивністю та швидко реагувати на них. Пам'ятайте, що ефективне управління інфраструктурою допомагає зекономити гроші та забезпечити стабільну роботу вашого додатку. І якщо один запит може займати, скажімо, 100 мс, це ще не є критичним, але якщо таких запитів у вас багато, то ваш додаток може перестати відповідати на інші запити. Це може призвести до помилок зі сторони клієнта. Як можна боротися з цим? Одним із способів є додавання setImmediate у ваші цикли, щоб дати можливість іншим запитам прорватися в стек вашого додатка. Також рекомендується використовувати асинхронні операції для роботи з файлами та іншими системними викликами, оскільки це допомагає уникнути блокування event loop і покращує продуктивність вашого додатка. Для виявлення проблем з event loop lag можна використовувати метрики від бібліотеки PromClient, яка збирає нативні метрики Node.js. Ці метрики можна збирати через Prometheus та відображати в графані, що дозволяє вам візуалізувати стан вашого додатка та вчасно реагувати на проблеми з продуктивністю. Щодо стандартних метрик в PromClient, можна отримати інформацію про garbage collector, відкриті запити та обробники. Рекомендується також використовувати зовнішні метрики системи для отримання інформації про back-end latency, status тощо.

Таким чином, моніторинг різних метрик дозволяє вам вчасно виявляти проблеми та підтримувати стабільну роботу вашого додатка. Отже, можна використовувати фільтри для аналізу конкретних параметрів і досліджувати проблеми саме з їх урахуванням. Наприклад, шляхом аналізу статистики загальної кількості активних обробників можна отримати інформацію про різні типи обробників у сервісі. Це допомагає зрозуміти, які типи запитів переважають і які операції можуть бути оптимізовані. Наприклад, ми можемо виявити, що деякі запити не є зашифрованими і йдуть через лоадбалансер. Це може бути нормально, якщо ці запити приходять від довірених джерел і можуть бути оброблені без шифрування. У такому випадку переважна більшість зовнішніх з'єднань відбуваються через TLS сокети, що дозволяє забезпечити безпеку та ефективність комунікації з іншими сервісами.

Щодо оптимізації додатка, можна використовувати бачування запитів, що дозволяє збирати дані у пам'яті перед їхнім записом у базу даних або інші системи. Це може допомогти зменшити кількість запитів і покращити продуктивність, особливо при великих об'ємах даних. Також важливо враховувати, що деякі системи можуть мати обмеження на кількість запитів, тому важливо мінімізувати їхню кількість. Узагальнюючи, оптимізація додатка полягає у використанні ефективних стратегій збору і обробки даних, а також у вдосконаленні процесів комунікації з іншими сервісами. Такий підхід допомагає зробити додаток ефективнішим і забезпечити його стабільну роботу.

Давайте обговоримо логи. Що може піти не так з логами? Якщо ми просто робимо, там, стандартно пишемо код. Якщо залогувати помилку. При Highload у нас все може піти не так. Тобто, якщо у нас, наприклад, лягає якась база даних, якийсь сторонній сервіс, це насправді в Highload це буде виглядати приблизно отак. Тобто, що ви кожну секунду, допустимо, у нас є 100 тисяч запитів в секунду, просто кругле число. Тобто, уявіть собі, що кожну секунду 100 тисяч запитів у вас виконується код команди logero. В цілому в Highload треба розуміти, що будь-яка команда привелика на навантаженні, навіть мінімальна, навіть безобідна команда, вона буде виконуватися дуже-дуже багато разів за секунду. І при кожній такій команді, особливо, якщо вона використовує якісь сторонні сервіси, треба завжди ставити собі питання. Допустимо, ми використовуємо там Datadog, скільки ми заплатимо Datadogу за таку кількість логів? Оце скільки там? Третина мільярда за годину. Скільки це буде коштувати? Це буде коштувати великих грошей. Це може створити, насправді, паразитичне навантаження до мережі. Це може загубити ваш процесор кінець кінців. Тож, ви ж пам'ятаєте, при нормальній роботі аплікухи ми працювали, ми стараємося там тримати 30, хай до 40-50% процесу. Якщо в нас почне спамити помилка на кожен запит, то навантаження може зрости до такої кількості, щоб покласти весь сервіс.

Власне, це сталося зі мною колись. Перший раз деплоїв один сервіс, і в мене була задача додавати логи. Я додаю логи, запускаю деплої аплікуху і падає. Падає чого? Через велику кількість логів. А як я можу знати, яка була кількість логів, якщо вони до цього не писалися, якщо я не міг до цього помірити? Тобто, при першому запиті. Тому, навіть, якщо ви не впевнені, не знаю, як цей кусок кода відпрацює на великій нагрузці, можна, не знаю, старатися поступово його викатувати для якогось малого процента запитів, моніторити вашу систему, розуміти, який наслідок вона вносить в вашу інфраструктуру, де у вас появляється високі місця, і поступово далі вже, скажімо так, викатувати вашу фічу якусь вже на більшу кількість запитів. Ну, або якщо у вас вже є досвід, ви знаєте, що там в цьому місці рано чи пізно будуть помилки. Цим треба буде щось робити.

Окей, добре. Що можна робити з банальними логами? Як тоді обійти оцю обробку помилок, до якої ми так всі звикли? Що може бути простіше? Насправді, можна логувати не всі помилки, а можна логувати тільки якусь частину з них. Якщо вам прилітає 100 тисяч помилок, ви з цими помилками, вам не потрібно їх всі аналізувати, читати. Ви їх не обробите, ви їх не прочитаєте в собі в логах, ви не зможете зробити якісь висновки, ви потратите кучу грошей і ресурсів, ну, банально для того, щоб, не знаю, ви все одно їх не прочитаєте в таку кількість. Тому достатньо взяти якийсь семпл, якусь частину цих помилок, наприклад, там, одна тисяча, наприклад, або одна сота, і логувати тільки якусь конкретну, якусь малу кількість помилок. Вам буде цього достатньо. Якщо у вас щось лягло, в принципі, і це сталося, і ви знаєте, що, наприклад, база даних лягла, і ця помилка буде вискакувати тільки тоді, коли, наприклад, ліг якийсь сервіс. Вам достатньо тільки якусь частину цих логів, щоб зрозуміти, що щось сталося не так, і не покласти систему ще більше, і зберегти роботу інших, наприклад, ендпойнтів, вашого додатку, де, в принципі, щоб логи взагалі дійшли в якусь іншу лог-систему, яку ви використовуєте. Інший варіант, як можна боротися з великою кількістю цієї інформації, для мені, для дебага, це все переводити в метрики.

Той самий prom-client дозволяє вам збирати ваші власні метрики, і ви можете їх також відображати в Prometheus, в Grafana, що допоможе вам при аналізі ваших проблем, все, що з ними станеться. Деколи достатньо просто знати назву помилки, щоб ви зрозуміли, ага, з базою щось не так, допустимо, якийсь спайк з базою даних, окей, ви зрозуміли, що в цей момент часу були проблеми з базою даних. Або, наприклад, з'явився якийсь клієнт, який почав просто спамити невалідними даними, і, не знаю, можливо, треба якось там з клієнтом поговорити, щоб він там перестав ці дані слати, і не створював лишнього навантаження. Можливо, це якась внутрішня команда ваша це зробила. Деколи достатньо знати текст помилки, наприклад, і скільки відбувалося цих помилок протягом часу. Звісно, якщо логувати всі помилки в якомусь там Datadog, Lux, IOC, Ibane, також можна зробити аналітику по цих логах, але, знову ж таки, зберігання всіх цих помилок і їх аналіз уже там, це дорожче, ніж зробити прегрегацію цих помилок на вашому сервісі спочатку і віддавати уже на сторонні ресурси, такі як Prometheus, уже агреговані метрики. Коли і де стала ця подія. Як це виглядає приблизно в коді, знову ж таки, використовуємо наш улюблений промклайнд, заводимо якийсь там каунтер, який ми будемо потім викликати і кажемо, збільшити, з коду це виглядає так, за треки помилку, з таким-то ім'ям, відповідно, промклайнд в себе в оперативній пам'яті просто буде збільшувати цей каунтер. От, і в результаті ці дані потім попадуть в Prometheus через зв'язок між Prometheusом і вашим додатком. Це окрема тема, в принципі, не будемо зараз її чіпати. От, і часто цього достатньо. Але ідеально насправді використовувати обидві ці техніки для того, щоб мати повну картину. Тобто, якщо у вас приходить помилка, насправді краще мати повну статистику всіх помилок, що в нас було така-то кількість помилок протягом такого-то часу. І безпосередньо влоги уже слати тільки частину їх, щоб ви зрозуміли, наприклад, де саме було джерело там цієї помилки. От, і так це дозволить вам отримати повну картину. Ну, і звісно, що це от число, це цього семпла, при якому ми робимо логи, це все індивідуально. В когось 500 запитів на сервері, в когось тисяча, в когось 10 тисяч, в когось 100 тисяч, в когось мільйон запитів в секунду. І кожен сам обирає це число в залежності від вашого навантаження.

Ну, і на завершення давайте поговоримо, просумуємо всі поради мої за сьогодні. З чого я почав? Перш за все, це горизонтальне масштабування. Це найбільш ефективний спосіб обробити більше запитів. Якщо у вас аплікуха не справляється, найшвидше, що ви можете зробити, це насправді докинути вашу інфраструктуру більше серверів. Оптимізація коду, вона завжди вимагає часу, вона приносить менше, вона приносить менше результату, на це треба час, і, скажімо так, в критичних ситуаціях краще спробувати все-таки масштабуватися. А потім уже розбиратися, чому саме це сталося. Можливо, у вас просто вирісла кількість користувачів, і вам треба мати більше серверів. Можливо, ви налаштували. А в Autoscaler, якщо у вас прогнозований трафік, тоді він автоматично буде піднімати необхідну кількість серверів, яка потрібна. Треба використовувати якомога менше серверів в вашій інфраструктурі, щоб економити гроші. Тобто будь-який Highload можна збити на не Highload, якщо просто докинути дофіга серверів. У вас було два сервери, ви вкинули лише 100, і у вас з кожного додатка виходить дуже мала нагрузка. І все. І в принципі, ну, з цим по ідеї можна так і жити деякий час. Але знову ж таки, до вас потім прийде бізнес і попросить якось оптимізувати ці витрати.

Обов'язково треба все моніторити. Це стосується знаходження балансу між кількістю ресурсів і якістю роботи вашої інфраструктури. Будьте готові до того, що бізнес буде постійно вимагати у вас аргументацію щодо рішень, таких як заскейлінг вашого додатку або збільшення витрат на інфраструктуру. Вам потрібно відслідковувати помилки та пульс вашого сервісу, тому моніторинг є обов'язковим. Важкі задачі можна вигружати в офлайн-воркери. В Node.js існує функціонал так званих "Worker Threads", які дозволяють виконувати роботу на сусідньому ядрі процесора. Проте, деякі завдання, які потребують серіалізації даних для передачі між процесами, можуть бути не ефективними для виконання у Worker Threads. Якщо у вас є різношерстні задачі, можливо, краще рознести їх на різні сервіси для полегшення масштабування і оптимізації роботи системи в цілому. Важливо уникати блокуючих операцій та використовувати batch-запити при роботі з базами даних та чергами, щоб зменшити кількість запитів і підвищити пропускну здатність системи.Все, дякую, час для запитань.

Sign in
Or by mail
Sign in
Or by mail
Register with email
Register with email
Forgot password?