Оптимізація веб-сервісу підказок для поштових адрес та ПІБ

У даній статті я хотів би поділитися досвідом розробки веб-сервісу на C++. На мій погляд, це досить цікава тема, оскільки використання C++ для веб-розробки — річ рідкісна і часто викликає в ІТ-колах подив. В Інтернеті можна знайти багато аргументів не на користь даного підходу. Використання вказівників, витоку пам'яті, сегфолты, відсутність підтримки веб-стандартів «з коробки» — ось неповний перелік того, з чим нам довелося ознайомитися, перш ніж прийняти рішення про вибір даної технології.

Розробка, про яку йде мова в цій статті була виконана в 2015 році, однак, передумови до неї з'явилися значно раніше. Все почалося з того, що в 2008-му році у нас виникла ідея розробити веб-сервіс по стандартизації і виправлення користувальницьких контактних даних, таких як поштові адреси і номери телефонів. Веб-сервіс повинен був отримувати допомогою REST API контактні дані, які вказав якийсь користувач в довільному текстовому вигляді, і приводити ці дані в порядок. По суті, сервіс повинен був вирішувати задачу розпізнавання користувача контактних даних у довільній текстовому рядку. Додатково в ході такої обробки сервіс повинен був виправляти помилки в адресах, відновлювати пропущені компоненти адреси, а також приводити оброблені дані до структурованого вигляду. Сервіс розроблявся для потреб бізнес-користувачів, для яких коректність клієнтських контактних даних є критичним чинником. В першу чергу це інтернет-магазини, служби доставки, а також CRM і MDM системи великих організацій.

В обчислювальному плані поставлена задача виявилась досить важкою, оскільки обробці підлягають неструктуровані текстові дані. Тому вся обробка була реалізована на C++, тоді як прикладна бізнес-логіка була написана на Perl і оформлена у вигляді FastCGI-сервера.

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

Обробка в реальному часі
Формування підказок в режимі реального часу увазі, що сервіс отримує від користувача новий HTTP-запит всякий раз, коли той вводить черговий символ поштової адреси або ПІБ в процесі заповнення певної форми з контактними даними. В рамках запиту сервіс отримує рядок, введену користувачем до теперішнього моменту, аналізує її та формує декілька найбільш ймовірних варіантів її доповнення. Користувач бачить отримані від сервісу підказки або вибирає підходящий варіант, або продовжує enter. У реальності це має виглядати приблизно наступним чином.



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

Для оцінки прийнятного часу відгуку ми провели ряд експериментів з регульованою затримкою. В результаті чого прийшли до висновку, що підказки перестають бути корисними, коли час відгуку починає перевищувати 150 мс. Наша вихідна архітектура сервісу дозволяла залишатися в цих рамках при одночасній роботі 40 користувачів (ці показники отримані для сервера з двома ядрами і 8Гб ОПЕРАТИВНОЇ пам'яті). Для збільшення цього числа необхідно нарощувати кількість процесорів у серверного заліза. А оскільки функції підказок для поштових адрес та ПІБ розроблялися для їх вільного використання всіма охочими, ми розуміли, що процесорів і серверів може знадобитися значно більше. Тому виникло питання про те, чи не можна оптимізувати обробку запитів за рахунок зміни архітектури сервісу.

Вихідна архітектура
Архітектура сервісу, яку потрібно було поліпшити, мала наступний вигляд.



Згідно з цією схемою, клієнтська програма (наприклад, веб-браузер), генерує HTTP-запити, які отримує веб-сервер (у нашому випадку використовується легкий веб-сервер lighttpd). Якщо у запитах маємо справу не зі статикою, то вони транслюються сервера додатків, який з'єднаний з веб-сервером за допомогою FastCGI інтерфейсу (у нашому випадку сервер додатків написаний на Perl). Якщо запити стосуються обробки контактних даних, то вони передаються далі сервера обробки. Для взаємодії із сервером обробки використовуються сокети.

Можна помітити, що якщо в даній схемі замінити сервер обробки на сервер БД, то вийде досить поширена схема, застосована в традиційних веб-додатках, що розробляються з використанням популярних фреймворків для Python або Ruby, а також для PHP під управлінням php_fpm.

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



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

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

Далі запит проходить через сервер додатків. На це витрачається додаткові 20% часу. У нашому випадку ніякої обробки запиту сервером додатків не виконується. Додаток лише виконує парсинг HTTP-запиту передає його далі сервера обробки, отримує від нього відповідь і передає його назад FastCGI інтерфейсу. Фактично 20% часу йде на парсинг запиту і на витрати інтерпретатора, оскільки додаток реалізовано на скриптовом мовою.

Ще 20% часу витрачається на проходження даних через сокетный інтерфейс, який використовується для зв'язку програми з сервером обробки. Цей інтерфейс працює трохи швидше, в порівнянні з FastCGI (20% проти 25%), оскільки відповідний протокол і його реалізація значно простіше. Обробка самого запиту, яка полягає у формуванні підказок для введених користувачем даних, забирає лише 10% від усього часу (у тестах використовувався один із найважчих, з точки зору обробки запитів).

Хотілося б підкреслити, що вся специфіка нашої задачі у проведених експериментах проявляється лише на останній стадії і саме ця стадія, з точки зору продуктивності, викликає найменше запитань. Інші етапи досить стандартні. Так, ми використовуємо подієвий веб-сервер, який просто витягує отриманий запит з одного сокета, асоційованого з прослушиваемым HTTP-портом, і кладе ці дані в FastCGI-сокет. Аналогічно сервер додатків – витягує дані з FastCGI-сокета і передає їх сокета сервера обробки. В самому додатку оптимізувати за великим рахунком нічого.

Гнітюча картина, при якій лише 10% від часу відгуку припадає на корисні дії, змусила нас замислитися про зміну архітектури.

Нова архітектура сервісу
Для усунення витрат у вихідній архітектурі необхідно в ідеалі позбутися програми на інтерпретованому мовою, а також усунути сокетные інтерфейси. При цьому необхідно зберегти можливість масштабування сервісу. Ми розглядали такі варіанти.

Подієвий сервер додатків
В рамках цього варіанту була розглянута можливість реалізувати подієвий сервер додатків, наприклад, на Node.js або Twisted. У такій реалізації кількість сокетных інтерфейсів, через які проходять запити, залишається колишнім, оскільки кожний запит надходить на балансуючий веб-сервер, той передає його одному з примірників сервера додатків, який в свою чергу транслює запит серверу обробки. Сумарний час обробки запиту залишається колишнім. Однак число одночасно оброблюваних запитів збільшується за рахунок асинхронного використання сокетів. Грубо кажучи, поки один запит перебуває в процесі переходу через сокетный інтерфейс, інший запит може проходити через бізнес-логіку програми в рамках того ж екземпляра.

Від даного варіанта реалізації довелося відмовитися, оскільки ми вважаємо невиправданим реалізацію повністю асинхронного додатка лише заради усунення одного вузького місця в старій архітектурі – сокетного інтерфейсу між додатком і сервером обробки. Інші операції вводу-виводу, такі як облік, фіксація користувача статистики, розсилка пошти та взаємодія з іншими сервісами в старому додатку виконувалися * ліниво * в окремих потоках, тому асинхронності не вимагали. Крім того, дана архітектура не дозволяє скоротити час обробки одного запиту, так що користувальницькі додатки, що працюють із сервісом через API, приросту в продуктивності не отримають.

Інтеграція програми та веб-сервера
Тут була розглянута реалізація програми у вигляді Java-сервлета або .Net додатки, який безпосередньо викликається веб-сервером. У цьому випадку вдається позбутися від FastCGI інтерфейсу, а заодно від интерпретируемого мови. Сокетный інтерфейс з сервером обробки зберігається.

На прийняття рішення не на користь даного підходу позначилася прив'язка всього рішення до конкретного веб-сервера, який повинен підтримувати обрану технологію. Наприклад, Tomcat для Java-сервлетів або Microsoft IIS у випадку використання .Net. Нам хотілося зберегти сумісність програми з легковагими серверами lighttpd і nginx.

Інтеграція програми з сервером обробки
В даному випадку прив'язки до конкретного веб-сервера немає, оскільки інтерфейс FastCGI зберігається. Додаток реалізується на C++ і об'єднується з сервером обробки. Таким чином, ми йдемо від використання интерпретируемого мови, а також усуваємо сокетный інтерфейс між додатком і сервером обробки.

До недоліків даного підходу можна віднести відсутність досить популярного і обкатаного на великих проектах фреймворка. З кандидатів ми розглядали CppCMS, TreeFrog і Wt. По частині першого у нас виникли побоювання на рахунок майбутньої підтримки проекту її розробниками, оскільки свіжих оновлень на сайті проекту давно не було. TreeFrog базується на Qt. Цю бібліотеку ми активно використовуємо в офлайнових проектах, однак вважали її надмірною і недостатньо надійною для поставленої задачі. По частині Wt – фреймворк має великий акцент на GUI, тоді як у нашому випадку GUI – річ другорядна. Додатковим фактором при відмові від використання цих фреймворків було бажання мінімізувати ризики, пов'язані з використанням сторонніх бібліотек, без яких у принципі можна обійтися, оскільки в даному випадку мала місце переробка існуючого працюючого сервісу, який не хотілося зламати через недостатньо налагодженої сторонньої бібліотеки.

Разом з тим, сам факт існування таких проектів наштовхнув на думку про те, що розробка веб-додатків на C++ справа не таке вже безнадійне. Тому було вирішено провести дослідження наявних бібліотек, які можна було б використати при розробці веб-додатків на C++.

Наявні бібліотеки
Для взаємодії з веб-сервером додаток повинен реалізовувати один з підтримуваних веб-сервером протоколів HTTP, FastCGI або SCGI. Ми зупинилися на FastCGI та його реалізації у вигляді libfcgi.

Для парсингу HTTP-запитів і формування HTTP-відповіді нам підійшла бібліотека cgicc. Дана бібліотека бере на себе всі турботи по розбору HTTP-заголовків, отримання параметрів запиту, декодуванню тіла отриманого повідомлення, а також по формуванню HTTP-відповіді.

Для парсингу XML-запити, які можуть приходити від користувачів сервісу в рамках REST API, був обраний Xerces.

В C++ немає підтримки юнікоду «з коробки», тому для роботи з текстом було прийнято рішення використовувати стандартні STL-рядка за умови обов'язкового дотримання внутрішнього угоди, що всі рядкові дані завжди повинні бути представлені в UTF-8.

Для взаємодії з зовнішніми сервісами та поштовими серверами було вирішено використовувати libcurl, а для генерації хешів — openssl.

Самописні компоненти
Для генерації html-уявлень нам потрібен був нескладний шаблонизатор. У старій реалізації сервісу для цих цілей використовувався HTML::Template, тому при переході на C++ потрібен був шаблонизатор з подібним синтаксисом і схожими можливостями. Ми спробували попрацювати з CTPP, Clearsilver і Google-ctemplate.

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

У Clearsilver весь інтерфейс реалізований на чистому C і для його використання потрібно писати значну об'єктну обгортку. Ну а Google-ctemplate не покривав всі можливості HTML::Template, які використовувалися в старій версії сервісу. Для його повноцінного використання було б змінювати логіку, що відповідає за формування уявлень. Тому у випадку з шаблонизатором довелося розробити власний велосипед, що і було зроблено.

Розробка власного C++ шаблонизатора забрала близько трьох днів, тоді як на пошук і вивчення готових рішень, зазначених вище, ми витратили вдвічі більше часу. Крім того, свій шаблонизатор дозволив розширити синтаксис HTML::Template, додавши в нього конструкцію «else if», а також оператори порівняння змінних з передбаченими в шаблоні значеннями.

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

Мабуть, ключовим стандартом, який нам довелося реалізувати самостійно, є JSON. На C++ є досить багато його відкритих реалізацій, які ми аналізували, перш ніж створювати ще одну. Основною причиною створення власної реалізації є використання JSON в зв'язці з нестандартним аллокатором пам'яті, який використовувався на сервері обробки для прискорення операцій динамічного виділення і звільнення пам'яті. Даний аллокатор працює в 2-3 рази швидше стандартного на масових операціях виділення/вивільнення блоків невеликого розміру. Оскільки робота з JSON укладається в даний патерн, ми хотіли отримати безкоштовний приріст продуктивності на всіх операціях, пов'язаних з парсингом і побудовою JSON-об'єктів.

Підсумковий результат
Архітектура підсумкового рішення, яке у нас вийшло, відображена на схемі.



В рамках монолітного сервера об'єднана як логіка програми, так і сама обробка контактних даних. Для обробки вхідних запитів на сервері передбачений пул потоків виконання. Всі операції введення-виведення, які необхідно виконати в ході обробки API-запитів, робляться * ліниво*. Для цих цілей на сервері створюється окремий пул потоків, які відповідають за виконання асинхронного введення-виведення. До таких операцій відноситься, наприклад, оновлення статистики користувача, а також списання грошей, у разі використання платних функцій API. В обох випадках потрібно робити запис у БД, виконання якої в основному потоці призвело б до його блокування.

Дана архітектура дозволяє масштабувати сервіс за рахунок запуску додаткових примірників монолітного сервера, у цьому випадку веб-сервер наділяється додатковою роллю балансувальника.

Згідно діаграми, наведеної раніше, при переході на нову архітектуру час відгуку сервісу при обробці одиночного запиту повинно було скоротитися приблизно на 40%. Реальні експерименти показали, що скорочення відбулося на 43%. Це можна пояснити тим, що монолітне рішення стало більш ефективно використовувати оперативну пам'ять.

Ми також провели стрес-тестування для визначення кількості користувачів, яких новий сервіс може обслуговувати при одночасному використанні підказок, забезпечуючи при цьому час відгуку не перевищує 150 мс. В такому режимі сервіс зміг забезпечувати одночасну роботу 120 користувачів. Нагадаю, що для старої реалізації це значення становило 40. В даному випадку триразовий приріст продуктивності пояснюється скороченням загального числа процесів, що беруть участь в обслуговуванні потоку запитів. Раніше запити оброблялися декількома екземплярами додатки (в експериментах кількість примірників варіювалася від 5 до 20), тоді як у новій версії сервісу всі запити обробляються в рамках одного багатопотокового процесу. У той час як кожен примірник працює з власної відокремленої пам'яттю, всі разом вони конкурують за один процесорний кеш, використання якого стає менш ефективним. У випадку одного монолітного процесу такої конкуренції немає.

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

Для поліпшення продуктивності нам довелося об'єднати сервер додатків і сервер обробки даних в єдиний монолітний сервер, реалізований на C++. Таке рішення вдвічі зменшило час відгуку при обробці одиночних запитів, а також збільшило продуктивність сервісу в три рази при масовому використанні.

Крім вирішення основного завдання приємними бонусами до виконаної роботи стало спрощення рефакторінгу, оскільки сувора типізація дозволяє не напружуватися з приводу перейменувань в коді, т. к. проект просто не збереться у разі помилок. Також цей проект стало легше супроводжувати в цілому, оскільки ми маємо єдиний сервер, у якого бізнес-логіка і логіка обробки даних написана на одній мові.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.