Як писати менше коду для MR, або Навіщо світові ще одну мову запитів? Історія Yandex Query Language

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



Дуже скоро ми зрозуміли, що тут міг би здорово допомогти загальний високорівнева мова запитів, який би надавав централізований доступ до вже наявних систем, а також визволив від необхідності заново реалізовувати типові абстракції на низькорівневих примітивах, прийнятих у цих системах. Так почалася розробка Yandex Query Language (YQL) — універсального декларативної мови запитів до систем зберігання та обробки даних. (Відразу скажу, що ми знаємо, що це вже не перша штука в світі, яка називається YQL, але ми вирішили, що це справі не заважає, і залишили назву).

Напередодні нашій встречи, яка буде присвячена інфраструктурі Яндекса, ми вирішили розповісти про YQL читачам Хабрахабра.

Архітектура
Ми, звичайно, могли б подивитися в бік популярних у світі open source-екосистем, таких як Hadoop або Spark. Але всерйоз вони навіть не розглядалися. Справа в тому, що потрібна підтримка поширених в Яндексі сховищ даних та обчислювальних систем. Багато в чому із-за цього YQL був спроектований і реалізований розширюваним на будь-якому з рівнів. Всі рівні ми по черзі розберемо нижче.



На діаграмі запити переміщуються зверху вниз, але обговорювати охоплюються елементи ми будемо в зворотному порядку, знизу вгору, щоб розповідь вийшов більш зв'язним. Для початку пару слів про підтримуються на даний момент бэкендах або, як ми їх називаємо, провайдерів даних:

  • Так склалося, що в Яндексі вже більше п'яти років розробляються дві реалізації парадигми MapReduce — YaMR і YT, про яку можна детальніше почитати недавньому пості. Технічно вони не мають майже нічого спільного ні один з одним, ні з Hadoop. Оскільки розробка систем такого класу — досить дороге задоволення, рік тому було прийнято рішення провести «MapReduce-тендер». YT переміг, і зараз користувачі YaMR закінчують на нього переходити. Розробка YQL розпочалася майже одночасно зі стартом тендеру, тому одним з основних вимог стала підтримка і YT, і YaMR, яку потрібно було реалізувати для полегшення життя користувачів в перехідний період.
  • Про RTMR (Real Time MapReduce) теж колись був окремий пост. Його підтримка зараз перебуває на ранній стадії розробки. По-перше, цей проект по інтеграції дозволить новим користувачам впроваджувати RTMR без спецпідготовки. По-друге, вони зможуть правильно аналізувати як потік свіжих даних, так і архів, зібраний за тривалий період і знаходиться в розподіленої файлової системи YT.
  • В Яндексі систем зберігання даних OLTP-паттерном використання ще більше, ніж заснованих на парадигмі MapReduce. В якості пілотного проекту з інтеграції з YQL серед них був обраний KiKiMR. Багато в чому такий вибір був зроблений тому, що потреба в дружньому інтерфейсі KiKiMR сформувалася в один час з активним ростом популярності YQL. Ще одна причина була в наявності у команди KiKiMR ресурсів на цей проект. Детальний розповідь про KiKiMR тут не вмістити, але якщо коротко, це розподілене надійне strict consistent-сховище даних, у тому числі розподілений між дата-центрами. Воно може використовуватися в інсталяціях, які складаються і з декількох машин, і з тисяч вузлів. Відмінною особливістю сховища KiKiMR є вбудована можливість ефективно виконувати операції транзакционно c рівнем ізоляції serializable як над окремими об'єктами (single-row transactions), так і над групами розподілених об'єктів сховища (cross-row/cross-table transactions).
  • Цей список містить лише те, що вже реалізовано, або знаходиться в роботі. У планах — розширювати асортимент підтримуваних YQL систем і далі. Наприклад, дуже логічним розвитком подій стане підтримка ClickHouse, яка зараз кілька відкладена лише із-за обмеженості ресурсів і відсутності гострої необхідності.


Ядро
Технічно YQL, хоч він і складається з відносно ізольованих компонентів і бібліотек, внутрішнім користувачам надається в першу чергу як сервіс. Це дозволяє виглядати з їх точки зору як «служба " одного вікна» і мінімізувати трудовитрати на організаційні питання видачі доступів або налаштувань фаєрвола для кожного з бэкендов. Крім того, обидві реалізації класичного MapReduce в Яндексі вимагають наявності синхронно очікує завершення транзакції клієнтського процесу, а сервіс YQL бере це на себе і дозволяє користувачам працювати в режимі «запустив і забув прийшов за результатами пізніше». Але якщо порівняти модель надання сервісу з поширенням у вигляді бібліотеки, мінуси теж знайдуться. Наприклад, слід набагато більш обережно ставитися до несумісним змін і релізам — інакше можна в самий невідповідний момент зламати користувальницькі процеси.

Основною точкою входу в сервіс YQL є HTTP REST API, який реалізований як Java-додаток на Netty і не тільки займається запуском вступників запитів на обчислення, але і має широкий спектр допоміжних обов'язків:
  • Кілька варіантів аутентифікації.
  • Перегляд списку доступних кластерів з бэкендами, списків таблиць і схем, навігація по них.
  • Репозиторій збережених користувачами запитів, а також історія усіх запусків (історично живе в MongoDB, але можливо в майбутньому це зміниться).
  • Повідомлення про завершених запитах:
    • Поруч з REST API доступний WebSocket endpoint, з допомогою якого користувацькі інтерфейси (про них поговоримо трохи нижче) здатні показувати спливаючі повідомлення в реальному часі;
    • Інтеграція з внутрішніми сервісами для відправки листів, смс повідомлень в Jabber;

    • Сповіщення через бота в Telegram.
Використання Java дозволило досить швидко реалізувати всю цю бізнес-логіку завдяки наявності готових асинхронних клієнтів для всіх потрібних систем. Оскільки надто суворих вимог щодо latency поки немає, то проблем із збіркою сміття було мало, а після переходу на G1 вони практично зникли. Ще, крім згаданого вище, для синхронізації між вузлами використовується ZooKeeper, в тому числі в паттерні publisher-subscriber при відправці повідомлень.

Саме виконання запитів на обчислення оркестрируется окремими процесами на С++ під назвою yqlworker. Вони можуть бути запущені як на тих же машинах, що і REST API, так і віддалено. Справа в тому, що між ними йде спілкування по мережі за допомогою розробленого та широко поширеного в Яндексі протоколу MessageBus. Під кожен запит за допомогою системного виклику fork (без exec) створюється копія yqlworker. Така схема дозволяє досягти достатньої ізоляції між запитами різних користувачів і при цьому — завдяки механізму copy-on-write — не витратити час на ініціалізацію.

Як видно з діаграми з високорівневої архітектурою, Yandex Query Language має два подання:
  • Основний синтаксис базується на SQL і призначений для написання людьми.
  • Синтаксис s-expressions, в свою чергу, більш зручний для кодогенерации.
Із запиту, незалежно від обраного синтаксису, створюється граф обчислень (Expression Graph), який логічно описує необхідну обробку даних з використанням примітивів, популярних у функціональному програмуванні. До таких примітивів відносяться: λ-функції, відображення (Map і FlatMap), фільтрація (Filter), згортка (Fold), сортування (Sort), застосувати (Apply) і багато інших. SQL-синтаксису лексер і парсер, засновані на ANTLR v3 будують Abstract Syntax Tree, за яким потім будується граф обчислень. Для синтаксису s-expression парсер практично тривіальний, оскільки граматика вкрай проста, а програми і так оперують цими абстракціями.

Далі для отримання необхідного результату запит проходить через кілька стадій, при необхідності повертаючись до вже пройдених:
  • Типізація. YQL — принципово суворо типізований мову. Доказів на користь цього було багато, починаючи від коренів в SQL, де мається на увазі схематизація, і закінчуючи більш широким простором для прискорення — наприклад, за рахунок генерації нативного коду на льоту. Крім простих типів даних, підтримується кілька видів контейнерів (Optional, List, Dict, Tuple і Struct) і спеціальних типів, наприклад непрозорий покажчик (Resource).
  • Оптимізація. На цій стадії відбуваються не тільки еквівалентні перетворення, покликані скоротити час виконання. Крім них виконується приведення плану дій до виду, який бекенд здатний виконати. Зокрема, логічні операції, які бекенд може нативно виконати, замінюються на фізичні. Таким чином, в YQL є свій фреймворк для оптимізаторів, які можна умовно розділити на три категорії:
    1. загальні правила логічних оптимізацій;
    2. загальні правила, специфічні для конкретних бэкендов;
    3. оптимізації, вибирають ту чи іншу стратегію виконання в runtimе (до них ми ще повернемося).
  • Виконання. Якщо після оптимізації не залишилося помилок, граф набуває вигляду, здійсненний з використанням API бекенду. Велику частину часу yqlworker саме це і робить. Залишилися в графі обчислень логічні операції виконуються з допомогою вузькоспеціалізованого інтерпретатора, по можливості — на обчислювальних потужностях бэкендов.
На будь-якій із стадій життя запиту, він може бути серіалізовать назад в синтаксисі s-expressions, що вкрай зручно для діагностики і розуміння того, що відбувається.

Інтерфейси
Як згадувалося у вступі, однією з ключових вимог до YQL було зручність використання. Тому публічним інтерфейсам приділяється особлива увага та вони вкрай активно розвиваються.

Консольний клієнт




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

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

Цікавий технічний нюанс: консольний клієнт реалізований на Python, але поширюється як статично слинкованное нативне додаток без залежностей з вбудованим інтерпретатором, який компілюється під Linux, OS X і Windows. Крім того, він вміє автоматично самостійно оновлюватися — приблизно як сучасні браузери. Все це було досить просто організувати завдяки внутрішній інфраструктурі Яндекса для складання коду і підготовки релізів.

Python-бібліотека




Python є другим за поширеністю мовою програмування в Яндексі після C++, тому клієнтська бібліотека YQL реалізована саме на ньому. Насправді вона спочатку розроблялася як частина консольного клієнта, а потім була виділена в незалежний продукт, щоб з'явилася можливість використовувати її в інших Python-оточеннях, не винаходячи аналогічний код заново.

Наприклад, багато аналітиків люблять працювати в середовищі Jupyter, для якої на основі даної клієнтської бібліотеки був створений так званий %yql magic:



Разом з консольним клієнтом поставляються дві спеціальні підпрограми, які запускають преднастроенный Jupyter або IPython з вже доступною клієнтської бібліотекою. Саме онді показано вище.

Веб-інтерфейс




Основний інструмент вивчення мови YQL, розробки запитів та аналітики ми залишили на закуску. Завдяки відсутності технічних обмежень консолі, в веб-інтерфейсі всі функції YQL доступні в більш наочній формі і завжди знаходяться під рукою. Частина можливостей інтерфейсу показана на прикладах інших екранів:

  • Автодоповнення і перегляд схеми таблиць



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



    При збереженні запиту під ім'ям вони потрапляють в міні-аналог репозиторію коду з можливістю перегляду історії і повернення до попередніх версій.
  • План виконання запиту



    Тут показана найбільш проста і універсальна реалізація JOIN в термінах MapReduce.
… і не тільки
Всі ручки в самому REST API аннотируются за кодом і на основі цих анотацій з допомогою Swagger автоматично генерується детальна онлайн-документація. З неї можна спробувати позадавать запити без єдиної рядки коду. Це дозволяє легко використати YQL, навіть якщо перераховані вище готові варіанти з якихось причин не підійшли. Наприклад — якщо ви любите Perl.

Можливості
Настала пора поговорити про те, якого плану задачі можна вирішувати за допомогою Yandex Query Language і які можливості надаються користувачам. Ця частина буде швидше тизисной, щоб не подовжувати і без того довгий піст.

SQL
  • Основний діалект YQL заснований на стандарті SQL:1992 з вкрапленнями з більш нових редакцій. Всі основні конструкції підтримуються, але повна сумісність в тонкощах, які виявилися не дуже затребувані, — ще в розробці. Завдяки цьому багатьом новим користувачам, які раніше працювали з будь-якими базами даних з SQL-інтерфейсом, доводиться вивчати мову далеко не з нуля.
  • На бэкендах, що працюють у парадигмі MapReduce, цільові таблиці (для простоти) створюються автоматично. Запити найчастіше складаються з
    SELECT
    довільного рівня складності і опціонально містять
    INSERT INTO
    .
  • В OLTP-сценарії доступні повноцінні DDL (
    CREATE TABLE
    ) і CRUD (плюс
    UPDATE
    ,
    REPLACE
    ,
    UPSERT
    та
    DELETE
    ).
  • Для багатьох ситуацій, які в стандартному SQL або не підтримуються, або вийшли б занадто громіздкими, в YQL додані різні розширення синтаксису, наприклад:

    • Іменовані вираження



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

    • Робота з типами-контейнерами



      Доступний як синтаксис для отримання елементів по ключу або індексом, так і набір спеціалізованих вбудованих функцій.

    • <b>FLATTEN BY</b>




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

      Звучить трохи заплутано — простіше показати на прикладі. Візьмемо таблицю наступного виду:
      [a, b, c] 1
      [d] 2
      [] 3
      Застосувавши
      FLATTEN BY
      лівій колонці, отримаємо таку таблицю:
      a 1
      b 1
      c 1
      d 2
      Подібне перетворення може бути зручним, коли по комірках з колонки контейнера потрібно порахувати яку-небудь статистику (скажімо, через
      GROUP BY
      ) або коли в осередках — ідентифікатори з іншої таблиці, з якої потрібно зробити
      JOIN
      .

      Найцікавіше у
      FLATTEN BY
      ось що: воно називається по-різному у всіх системах, які вміють так робити. З того, що ми знайшли, немає жодного повтору:
      • ARRAY JOIN
        — ClickHouse,
      • unnest
        — PostgreSQL,
      • $unwind
        — MongoDB,
      • LATERAL VIEW
        — Hive,
      • FLATTEN
        — Google BigQuery,
    • Явні
      PROCESS
      (Map) і
      REDUCE
      (Reduce).




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


User Defined Functions
Не всі види перетворень даних зручно виражати декларативно. Іноді простіше написати цикл або скористатися якою-небудь готової бібліотекою. Для таких ситуацій YQL надає механізм для користувача функцій, вони ж User Defined Functions, вони ж UDF:

  • С++ UDF
    • «З коробки» доступно більше 100 функцій на C++, розділених більш ніж на 15 модулів. Приклади модулів: String, DateTime, Pire, Re2, Protobuf, Json та ін
    • Фізично C++ UDF являють собою динамічно завантажувані бібліотеки.so) з ABI-safe-протоколом виклику і реєстрації функцій.

    • Є можливість написати свою C++ UDF, зібрати її локально (система збирання має готовий набір налаштувань для складання UDF), завантажити стандартним чином в сховищі і відразу ж почати використовувати у запитах, приклавши її по URL.
    • Для простих UDF можна використовувати готові C++-макроси, приховують деталі, а при необхідності доступні гнучкі інтерфейси для різних потреб.
  • Python UDF
    • Коли продуктивність не так важлива, а для вирішення завдання потрібно швидко зробити вставку з імперативною бізнес-логікою, дуже зручно розбавити декларативний запит код на Python. Більшість співробітників Яндекса знають Python, а якщо хтось не знає — на базовому рівні він вивчається за одиниці днів.
    • Скрипт на Python можна написати як inline упереміш з SQL або s-expressions, або прикласти до запиту окремим файлом. Взагалі, механізм доставки файлів до місця обчислень з клієнта або URL універсальний і може використовуватися для всього необхідного, наприклад, для файлів-словників.

    • Оскільки в Python використовується динамічна типізація, а в YQL — статична, то від користувача потрібно оголосити сигнатуру функції на кордоні. Зараз вона описується зовні з допомогою додаткового міні-мови: справа в тому, що на етапі типізації не хочеться запускати інтерпретатор. У майбутньому, можливо, прикрутимо підтримку Python 3 type hints.
    • Технічно підтримка Python у YQL реалізована через C++ UDF з вбудованим інтерпретатором Python і невеликий синтаксичний цукор в SQL-парсере для її виклику.
  • Streaming UDF. Щоб можна було плавно перейти з інших технологій, і для деяких особливих випадків є спосіб запустити довільний скрипт або виконуваний файл в потоковому режимі. В результаті отримаємо UDF, перетворюючу один список рядків в інший.


Агрегаційні функції
Всередині у агрегаційних функцій використовується загальний framework з підтримкою
DISTINCT
b виконання як на верхньому рівні, так і в
GROUP BY
(в тому числі і з
ROLLUP/CUBE/GROUPING SETS
з стандарту SQL:1999). А відрізняються ці функції лише бізнес-логікою. Ось деякі приклади:
  • Стандартні:
    COUNT
    ,
    SUM
    ,
    MIN
    ,
    MAX
    ,
    AVG
    ,
    STDDEV
    ,
    VARIANCE
    ;
  • Додаткові:
    COUNT_IF
    ,
    SOME
    ,
    LIST
    ,
    MIN_BY/MAX_BY
    ,
    BIT_AND/OR/XOR
    ,
    BOOL_AND/OR
    ;
  • Статистика:
    • MEDIAN
      та
      PERCENTILE
      (за алгоритмом TDigest;
    • HISTOGRAM
      — адаптивні гістограми по числовим значенням, не потребують ніякого знання їх розподілу, (за алгоритмом на основі Streaming Parallel Decision Tree).
  • User Defined Aggregation Functions: для зовсім специфічних завдань можна передати в framework агрегаційних функцій свою бізнес-логіку, створивши кілька викликаються значень з певною сигнатурою з допомогою описаного вище механізму UDF, наприклад на Python.


З міркувань продуктивності, в термінах MapReduce для агрегаційних функцій автоматично створюється Map-side Combiner з об'єднанням проміжних результатів агрегації в Reduce.
DISTINCT
зараз завжди працює точно (без наближених обчислень), тому вимагає додаткового Reduce для розмітки унікальних значень.

JOIN таблиць
Злиття таблиць ключів — одна з найпопулярніших операцій, яка часто потрібна для вирішення завдань, але правильно реалізувати яку в термінах MapReduce — майже ціла наука. Логічно в Yandex Query Language доступні всі стандартні режими плюс декілька додаткових:



Щоб приховати деталі від користувачів, заснованих на MapReduce бэкендов стратегія виконання JOIN вибирається на льоту залежно від необхідного логічного типу і фізичних властивостей беруть участь таблиць (це так звана cost-based optimization):

Стратегія Короткий опис Доступна для логічних типів
Common Join 1-2 Map + Reduce
Map-side Join 1 Map Inner, Left, Left only, Left semi, Cross
Sharded Map-side Join k паралелльных Map (k <= 4 за замовчуванням) Inner, Left semi з унікальною правою, Cross
Reduce Without Sort 1 Reduce, але вимагає заздалегідь правильно відсортованого входу у розробці


Напрямки розвитку
Серед наших найближчих і середньострокових планів по Yandex Query Language:
  • бэкендов в статусі production.
  • Генерація нативного коду і векторизація замість спеціалізованого інтерпретатора.
  • Продовження оптимізації вводу-виводу і вибору стратегій виконання на льоту залежно від фізичних властивостей таблиць.
  • Віконні функції на основі стандарту SQL:2003.
  • Підтримка SQL:1992 в повному обсязі, створення ODBC/JDBC-драйвери з подальшою інтеграцією з популярними ORM та інструментами бізнес-аналітики.
  • Наочна демонстрація прогресу виконання операцій.
  • Розширений асортимент доступних мов програмування для UDF — посматриваем на JavaScript (V8), Lua (LuaJIT) і Python 3.
  • Інтеграція з:
    • розподіленим резервний сервісом запуску завдань за розкладом (a la cron) або настання подій,
    • інструментами візуалізації (внутрішній аналог Яндекс.Статистики).




Підводимо підсумки
  • Як показують цифри (див. врізку), YQL став продуктом, дуже затребуваним серед співробітників Яндекса. Тим не менш, обсяги оброблених з його допомогою даних не так великі. Це обумовлено тим, що історично всі продакшен-процеси працюють на низькорівневих інтерфейсах, що підходять під вимоги їх систем. Тобто їх поступовий переклад YQL тільки починається.
  • Яндекса ми спочатку стикалися з опором наступного виду: працюючи в парадигмі MapReduce довгі роки, багато хто вже настільки до неї звикли, що не хочуть переучуватися. В Аркадії, в основному монолітному репозиторії коду Яндекса, у кожного співробітника є свій куточок. Там історично лежать буквально сотні програм на C++, написаних виключно щоб відфільтрувати який-небудь специфічний лог або просто таблицю в MapReduce під конкретну задачу. Але після набору першої критичної маси задоволених користувачів подібний скептицизм зустрічається все рідше.
  • Повертаючись до питання, «чому не Hive, Spark SQL або будь-якої іншої
    SQL over ***
    : в першу чергу нас цікавила саме підтримка активно використовуються в Яндекс систем. Хотілося спростити міграції проектів, тобто всі компоненти з діаграми на початку посту все одно довелося б розробляти та/або модифікувати. При цьому довелося б підлаштовуватися під підвалини open source-спільноти. Крім того, були труднощі з тим, що Java-розробників в Яндексі приблизно на порядок менше, ніж C++-розробників, а люди з досвідом розробки ядра цих open source-проектів — дефіцит навіть у США. І в результаті абсолютно не факт, що вийшло б краще або швидше. YQL створений з нуля десь за рік командою приблизно з 10 чоловік, більшість з яких брали участь не full time.
  • Сконцентрувавшись виключно на SQL-діалекті, ми б закрили двері перед помітним класом людей, яким зручніше описувати макропреобразования даних і бізнес-логіку однорідним чином на одній мові програмування. В Яндексі вже існувала бібліотека такого класу для Python під назвою Nile: ми замість наявного в ній runtime реалізували (за її публічним API) генерацію і запуск YQL-запитів на s-expressions. Зараз ми знаходимося в процесі доопрацювань для перемикання на нього за замовчуванням. Інші мови програмування, де такий інтерфейс був би затребуваний, в Яндексі набагато менш поширені, але в майбутньому не виключена поява аналогів і для, наприклад, Java.
  • Нам було б дуже цікаво викласти YQL з якимось підмножиною бэкендов в open source — щоб спробувати скласти конкуренцію екосистемам від Apache Software Foundation: Hadoop і Spark. На жаль, найближчим часом цього не станеться через різного роду труднощі: наприклад, відсутності інструментів для часткової публікації Аркадії або численних зав'язок на внутрішню інфраструктуру. Але ми вже потихеньку почали рухатися в цьому напрямку.


Наостанок — ще раз запрошуємо на зустріч в нашому офісі в найближчу суботу, 15 жовтня, де ми детальніше розповімо про різні аспекти інфраструктури в Яндексі.


Джерело: Хабрахабр

0 коментарів

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