Інженерний блог — Prexy: надшвидкі правила пошуку та заміни

Ласкаво просимо до нашого першого інженерного блогу. Можливо, він буде дещо технічнішим, ніж ви звикли бачити в інших блогах, але ми зробили все можливе, щоб він був зрозумілим для всіх. У цій статті ми поговоримо про Prexy — нове програмне забезпечення в рамках Clonable , яке використовується для застосування правил заміни.

Історія питання

Як користувач Clonable, ви, можливо, вже знайомі з функцією правил заміни. Ви можете використовувати ці правила пошуку та заміни, щоб замінити фрагменти тексту або коду на самостійно обраний варіант. Наприклад, ви можете використовувати це для заміни ключа API або ідентифікатора аналітики, щоб мати змогу проводити різний аналіз для вашого оригінального сайту та його перекладених клонів. Внутрішньо ці самі правила також використовуються для заміни вашого оригінального доменного імені на доменне ім’я вашого клону та для низки інших цілей.

З часів найпершої версії Clonable ця функція ніколи не оновлювалася, що саме по собі не дивно, адже вона чудово виконувала свою роботу. Проте вдосконалення були можливі як у плані продуктивності, так і зручності використання. Час від часу ми в Clonable беремо частину нашого продукту, якою давно не займалися, щоб почати її вдосконалювати. Наприклад, минулого року ми повністю переробили всю нашу інфраструктуру даних, щоб зробити її швидшою, масштабованішою та відмовостійкішою, а до цього ми також повністю переробили функцію перекладу URL -адрес. Цього кварталу настала черга правил заміни.

Попередня ситуація та обмеження

Правила заміни в Clonable застосовуються лише на самому останньому етапі, безпосередньо перед відправкою відповіді клієнту. Тому в першій версії Clonable ми вирішили реалізувати це в рамках веб-сервера NGINX за допомогою — модуля, створеного OpenResty. Цей модуль використовує як потоковий механізм обробки регулярних виразів, також створений OpenResty. Потокова обробка в цьому випадку є важливою, оскільки ми не хочемо завантажувати кожну відповідь повністю в пам'ять. Якби ми це робили, велика кількість великих відповідей могла б призвести до збою або аварійного завершення роботи NGINX через недостатню кількість доступної пам'яті. Другою важливою особливістю sregex є те, що він може обробляти кілька рядків паралельно. Це пов’язано з тим, що кожен клон уже має певні стандартні правила, а також, іноді, деякі правила, додані спеціально для цього клону. Якби ці правила не можна було обробляти паралельно, нам все одно довелося б буферизувати весь відгук, оскільки нам потрібно було б мати можливість перевірити його на відповідність ще раз після першого правила для другого правила.

Однак минулого року ми дедалі частіше стикалися з дивними проблемами з продуктивністю. Ці проблеми часто були короткочасними, але в крайніх випадках могли збільшувати час відгуку сторінки на кілька секунд. Після одного такого сплеску сторінка часто знову завантажувалася нормально, що ускладнювало відтворення проблеми. Щоб детальніше це відладити, ми додали заголовок Clonable-Timings до всіх відповідей, що дозволило нам приблизно визначити, який саме етап процесу перекладу забирав так багато часу. Це показало, що майже у всіх випадках сервер-джерело відповідав швидко, і сам переклад також відбувався досить плавно. Однак між завершенням перекладу та повним виконанням запиту існувала затримка, і це дало нам підказку, що це може бути пов’язано з правилами заміни.

Модель паралелізму NGINX

Однак це ще не дало нам відповіді на питання, чому ці затримки виникали лише спорадично і чому це відбувалося, коли сервери були завантажені навіть не наполовину. Щоб краще зрозуміти це явище, нам потрібно трохи глибше розглянути, як NGINX обробляє робочі навантаження.

NGINX має архітектуру, що складається з одного головного процесу та декількох робочих процесів. З’єднання розподіляються між робочими процесами для одночасної обробки декількох запитів. Однак така конфігурація має й недолік: коли робочий процес зайнятий обробкою одного запиту, інші запити, які також призначені цьому ж робочому процесу, змушені чекати. Це може призвести до того, що один ресурсоємний запит спричинить затримки для багатьох інших запитів, навіть якщо інші робочі процеси не мають роботи. Цей ефект добре видно на зображенні нижче. Під час створення цього зображення виконувався навантажувальний тест із 10 різними з’єднаннями. На зображенні чітко видно, що 3 робочі процеси зайняті, а четвертий майже нічого не робить.

Час для Prexy

Оскільки вузьке місце було чітко видно, ми розробили план його усунення. Цей проєкт отримав назву Prexy — поєднання слів RegEx та Proxy. До кінцевого результату висувалося 3 основні вимоги:

  1. Він має стати повноцінною заміною поточної конфігурації. Правила заміни слід застосовувати таким самим чином, щоб уникнути порушень у роботі існуючих конфігурацій.

  2. Правила заміни мають передаватися потоково, щоб рішення не використовувало занадто багато пам'яті.

  3. Нове рішення має бути швидшим за поточне.

Спочатку нам потрібно було знайти потужне багатопотокове середовище виконання, здатне ефективно обробляти запити. Розглянувши кілька варіантів, ми зупинилися на середовищі виконання для мови програмування Rust. Rust відомий тим, що дозволяє писати швидкі програми без ризиків для безпеки, властивих іншим низькорівневим мовам, таким як C++. Середовище виконання Tokyo — це гнучке асинхронне середовище, створене для мережевих додатків. Однією з найважливіших для нас особливостей є те, що вона підтримує механізм work-stealing. Це означає, що, на відміну від NGINX, запит не прив’язаний до одного робочого процесу, а робочий процес, який не має роботи, може work-steal в іншого робочого процесу. Таким чином, ресурси наших серверів можна використовувати ефективніше.

Перший прототип

Вибравши базові технології, ми вирішили створити початковий прототип, щоб приблизно оцінити, наскільки цей проєкт підвищить швидкість роботи та чи варто взагалі його реалізовувати. Як першу реалізацію двигуна регулярних виразів (частини, яка фактично обробляє та застосовує правила), ми використали стандартну бібліотеку регулярних виразів (або crate, як їх називають у термінології Rust). Цей двигун, як і sregex, не використовує зворотного простежування, що означає менший ризик виникнення проблеми ReDoS. У разі ReDoS регулярний вираз стикається з такими вхідними даними, що час, необхідний для їх оцінки, зростає експоненціально. Це може мати катастрофічні наслідки для продуктивності і стало причиною Cloudflare. Тому для нас важливо, щоб движок, який ми використовуємо, був беззворотним.

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

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

Результати перших тестів були очевидними: Prexy обробляв один запит приблизно у 22 рази швидше (див. зображення нижче). Це дало проекту остаточне зелене світло, адже за такої значної різниці залишався достатній запас для компенсації можливого зниження продуктивності, яке могло б виникнути під час доопрацювання функціоналу.

Після початкового тестування ми впровадили низку очевидних оптимізацій, таких як кешування скомпільованих регулярних виразів. Ми також реалізували більш ефективний спосіб повторного використання з’єднань із сервером-джерелом. Завдяки цьому ми досягли прискорення роботи приблизно на 20%. Потім ми також протестували обидва рішення в умовах пікового навантаження, надсилаючи на сервер якомога більше запитів. І знову різниця була чітко помітною: Prexy продемонстрував пропускну здатність, приблизно у 25 разів вищу, ніж старе рішення.

Потоковий механізм обробки регулярних виразів

Однією з вимог цього проєкту було те, щоб використовуваний механізм регулярних виразів підтримував потокову обробку. Прототип функції regex create цього не робить, тому в цій області знадобилася додаткова робота. Однак модуль regex виявився дуже добре оптимізованим, тому ми вирішили взяти цю реалізацію за основу для нашого механізму потокової обробки. Під час потокової обробки даних через механізм регулярних виразів важливо пам’ятати про кілька речей. По-перше, ви не знаєте, які дані ще мають надійти. Тому потрібно починати працювати з частковими збігами: збігами, які ще не завершені, але вже відповідають 1 або більше символів. Знайшовши повний збіг, потрібно перевірити, чи немає часткових збігів, що перекриваються, оскільки вони також можуть виявитися збігами (а в разі перекриття довший збіг має пріоритет над коротшим). Отже, ви можете розпочати обробку збігу лише тоді, коли всі поточні часткові збіги виявляться неповними.

Подальша оптимізація

Як і очікувалося, переробка механізму регулярних виразів негативно вплинула на продуктивність Prexy. Проте порівняно зі старим рішенням у нас залишався значний запас продуктивності, і завдяки додатковим оптимізаціям ми навіть перевершили показники, які були до переробки механізму регулярних виразів. Однією з застосованих нами оптимізацій було запам’ятовування того, чи слід застосовувати правило до конкретного файлу. Статичні ресурси, такі як CSS- та JS-файли, майже ніколи не змінюються, тому немає сенсу виконувати правило щоразу, коли воно ніколи не спрацьовує для цього конкретного файлу. Альтернативою цьому було кешування результатів заміни, але недоліком цієї стратегії є те, що для зберігання всіх файлів потрібно багато пам’яті. Запам’ятовуючи, які правила застосовувати, нам потрібно лише кілька байтів на файл, оскільки інформація зберігається у вигляді бітової мапи.

Крім того, ми повною мірою використовуємо оптимізації в механізмі регулярних виразів, наприклад, не зберігаючи групи захоплення, коли вони не використовуються в заміні. В результаті механізму регулярних виразів потрібно запам'ятовувати лише початок і кінець усього збігу, що економить ресурси. Ми також вимкнули іменовані групи захоплення, оскільки це було досить складним у потоковому варіанті, а оскільки старе рішення теж цього не підтримувало, у цьому не було особливої потреби. Усі ці оптимізації в підсумку забезпечили таку продуктивність:

Кінцевий результат

Після ретельного тестування Prexy, щоб переконатися, що його поведінка дійсно збігається зі старим рішенням, ми почали поступово впроваджувати його. Протягом приблизно тижня ми активували Prexy для кожного клону. Під час моніторингу часто було легко помітити момент активації. Зниження часу відгуку приблизно на 50% було досить поширеним явищем. Незначні відмінності, близько 10%, також спостерігалися у інших клієнтів, які мали дуже мало правил заміни. Швидкість роботи нашого власного веб-сайту зросла приблизно на 30 % (~50 мс).

Після впровадження Prexy у нашій загальній інфраструктурі ми спостерігаємо такі зміни.

  • Споживання оперативної пам'яті зменшено на 33%

  • Використання процесора зменшилося на 20%

  • Відповіді на 10–50 % швидше

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


Дякуємо, що читаєте цей інженерний блог. Повідомте нас, якщо хочете частіше отримувати подібні технічні матеріали про наш продукт. Цей проєкт вас зацікавив? Тоді завітайте на нашу сторінку з вакансіями :)

Почніть користуватися Clonable безкоштовно вже сьогодні

Дізнайтеся, як централізовано керувати міжнародними веб-сайтами з єдиної платформи.

Ми використовуємо файли cookie для покращення вашого досвіду, аналізу відвідуваності веб-сайту та вдосконалення наших послуг.

Ознайомтеся з нашою політикою щодо файлів cookie