JavaRush /Java блог /Random UA /Посібник з мікросервісів Java. Частина 3: загальні питанн...

Посібник з мікросервісів Java. Частина 3: загальні питання

Стаття з групи Random UA
Переклад та адаптація Java Microservices: A Practical Guide . Попередні частини гайду: Давайте розглянемо властиві Java проблеми мікросервісів, починаючи з абстрактних речей і закінчуючи конкретними бібліотеками. Посібник з мікросервісів Java.  Частина 3: загальні питання - 1

Як зробити мікросервіс Java стійким?

Нагадаємо, що при створенні мікросервісів ви по суті змінюєте дзвінки методів JVM на синхронні дзвінки HTTP або асинхронний обмін повідомленнями. В той час, як виконання виклику методу в основному гарантовано (за винятком несподіваного завершення роботи JVM), мережний виклик за замовчуванням ненадійний. Він може працювати, але може і не працювати з різних причин: перевантажена мережа, запровадабо нове правило брандмауера і таке інше. Щоб побачити, яке це має значення, погляньмо на приклад BillingService.

Паттерни стійкості HTTP/REST

Допустимо, клієнти можуть купити електронні книги на сайті вашої компанії. Для цього ви тільки-но впровадабо мікросервіс білінгу, який може викликати ваш інтернет-магазин для створення фактичних рахунків у форматі PDF. Зараз ми зробимо цей виклик синхронно, через HTTP (хоча розумніше викликати цю службу асинхронно, оскільки генерація PDF не обов'язково має бути миттєвою з точки зору користувача. Ми використовуємо цей приклад у наступному розділі і подивимося на відмінності).
@Service
class BillingService {

    @Autowired
    private HttpClient client;

     public void bill(User user, Plan plan) {
        Invoice invoice = createInvoice(user, plan);
        httpClient.send(invoiceRequest(user.getEmail(), invoice), responseHandler());
        // ...
    }
}
Якщо узагальнити, ось три можливі результати цього HTTP-дзвінка.
  • ОК: дзвінок пройшов, рахунок успішно створено.
  • ЗАТРИМКА: дзвінок пройшов, але знадобилося занадто багато часу для цього.
  • ПОМИЛКА. Виклик не відбувся, можливо, ви надіслали несумісний запит або система не працювала.
Від будь-якої програми очікують на обробку помилкових ситуацій, а не лише успішних. Те саме стосується і мікросервісів. Навіть якщо вам потрібно докласти додаткових зусиль для забезпечення сумісності всіх розгорнутих версій API, як тільки почнете з розгортань і випусків окремих мікросервісів. Посібник з мікросервісів Java.  Частина 3: загальні питання - 2Цікавий випадок, який варто звернути увагу, — випадок затримки. Наприклад, мікросервісний жорсткий диск респондента переповнений і замість 50 мс для відповіді потрібно 10 секунд. Ще цікавішим стає тоді, коли ви відчуваєте певне навантаження, так що нечуйність вашого BillingService починає каскадно проходити через вашу систему. Як наочний приклад уявіть кухню, що запускає "блок" всіх офіціантів ресторану. Цей розділ, очевидно, не може дати вичерпний огляд теми стійкості мікросервісів, але служить нагадуванням для розробників про те, що насправді це те, що потрібно вирішувати, а не ігнорувати до першого випуску (що, за досвідом, відбувається частіше, ніж слід). Популярною бібліотекою, яка допомагає вам думати про затримки та стійкість до відмов, є Hystrix від Netflix.

Messaging Resilience Patterns.

Розгляньмо асинхронну комунікацію. Наша програма BillingService тепер може виглядати приблизно так, за умови, що ми використовуємо Spring та RabbitMQ для обміну повідомленнями. Щоб створити рахунок, ми відправляємо повідомлення нашому брокеру повідомлень RabbitMQ, де є кілька працівників, які очікують нових повідомлень. Ці працівники створюють рахунки у форматі PDF та надсилають їх відповідним користувачам.
@Service
class BillingService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

     public void bill(User user, Plan plan) {
        Invoice invoice = createInvoice(user, plan);
        // преобразует счет, например, в json и использует его як тело повідомлення
        rabbitTemplate.convertAndSend(exchange, routingkey, invoice);
        // ...
    }
}
Тепер потенційні помилки виглядають трохи інакше, тому що ви не отримуєте негайних відповідей OK або ERROR, як це було з синхронним HTTP-з'єднанням. Натомість у нас може бути три потенційні варіанти неправильного розвитку подій, які можуть викликати такі питання:
  1. Чи було моє повідомлення доставлено та використано працівником? Чи це втрачено? (Користувач не отримує рахунок-фактуру).
  2. Моє повідомлення було доставлене лише один раз? Чи доставлено більше одного разу та обробляється лише один раз? (Користувач отримає кілька рахунків).
  3. Конфігурація: Від «Чи я використовував правильні ключі маршрутизації/імена для обміну» до «Чи правильно налаштований і підтримується мій брокер повідомлень або переповнені його черги?» (Користувач не отримує рахунок-фактуру).
Детальний опис кожного окремого патерну стійкості асинхронного мікросервісу виходить за межі даного посібника. Проте тут є вказівники в правильному напрямку. Тим більше, що вони залежатимуть від технології обміну повідомленнями. Приклади:
  • Якщо ви використовуєте реалізації JMS, наприклад ActiveMQ, ви можете обміняти швидкість на гарантії двофазних (XA) коммітів (two-phase (XA) commits).
  • Якщо ви використовуєте RabbitMQ, для початку прочитайте цей посібник, а потім добре обміркуйте підтвердження, відмовостійкість і надійність повідомлень в цілому.
  • Можливо, хтось добре розуміється на конфігуруванні серверів Active або RabbitMQ, особливо у поєднанні з кластеризацією та Docker (хто-небудь?;))

Який фрейморк буде найкращим рішенням для мікросервісів Java?

З одного боку, можна встановити дуже популярний варіант, такий як Spring Boot . Він дозволяє легко створювати файли .jar, що поставляється з вбудованим веб-сервером, таким як Tomcat або Jetty, і який можна запустити швидко і будь-де. Ідеально підходить для створення програм мікросервісу. Нещодавно з'явилася пара спеціалізованих мікросервісних фреймворків Kubernetes або GraalVM , частково натхнених реактивним програмуванням. Ось ще кілька цікавих претендентів: Quarkus , Micronaut , Vert.x , Helidon. Зрештою, вам доведеться вибирати самостійно, але ми можемо дати вам кілька рекомендацій, можливо, не цілком стандартних: За винятком Spring Boot, всі платформи мікросервісів зазвичай позиціонуються як неймовірно швидкі, з майже миттєвим запуском, малим об'ємом пам'яті, що використовується, можливістю масштабування до нескінченності. У маркетингових матеріалах зазвичай фігурують вражаючі графіки, що представляють платформу у вигідному світлі поруч із “бегемотом” Spring Boot чи друг з одним. Це за ідеєю щадить нерви розробників, що підтримують легаси-проекти, які часом завантажуються по кілька хвабон. Або розробникам, які працюють у хмарі, які хочуть запустити стільки мікроконтейнерів, скільки їм зараз потрібно протягом 50 мс. Посібник з мікросервісів Java.  Частина 3: загальні питання – 3Проблема, однак, полягає в тому, що такий (штучний) час старту «голого заліза» та час повторного розгортання навряд чи впливають на загальний успіх проекту. Принаймні впливають набагато менше, ніж сильна фреймворкова інфраструктура, сильна документація, спільнота і сильні навички розробника. Так що краще дивитися на це так: Якщо досі:
  • Ви дозволяєте своїм ORM працювати у режимі нестримної генерації сотень запитів для простих робочих процесів.
  • Вам потрібні нескінченні гігабайти для запуску моноліту помірної складності.
  • У вас так багато коду і складність настільки висока (зараз ми говоримо не про потенційно повільні стартери, такі як Hibernate), що вашому додатку потрібно кілька хвабон для завантаження.
Якщо справа саме так, то додавання додаткових мікосервісних проблем (відмовостійкість, обмін повідомленнями, DevOps, інфраструктура) набагато більше вплине на ваш проект, ніж завантаження порожнього Hello, world. А для гарячих повторних розгортань під час розробки вам можуть стати в нагоді такі рішення, як JRebel або DCEVM . Не полінуємося знову процитувати Саймона Брауна : " якщо люди не можуть створювати (швидкі та ефективні) моноліти, їм буде важко створювати (швидкі та ефективні) мікросервіси незалежно від структури ". Тож вибирайте фрейморки з розумом.

Які бібліотеки найкраще підходять для синхронних викликів Java REST?

На низькорівневому технічному боці ви, ймовірно, отримаєте одну з наступних клієнтських бібліотек HTTP: Власний HttpClient Java (починаючи з Java 11), HttpClient Apache або OkHttp . Зверніть увагу, що тут я говорю «ймовірно», тому що є й інші варіанти, починаючи зі старих добрих клієнтів JAX-RS до сучасних клієнтів WebSocket . У будь-якому випадку, існує тенденція до генерації HTTP-клієнта, з відходом від самостійної метушні з HTTP-викликами. Для цього вам потрібно поглянути на проект OpenFeign та його документацію як відправну точку для подальшого читання.

Які брокери найкращі для асинхронного обміну повідомленнями Java?

Швидше за все, ви зіткнетеся з популярними ActiveMQ (Classic або Artemis) , RabbitMQ або Kafka .
  • ActiveMQ та RabbitMQ є традиційними, повноцінними брокерами повідомлень. Вони передбачають взаємодію "розумного брокера" і "дурненьких користувачів".
  • Історично ActiveMQ мав перевагу простого вбудовування (для тестування), яке можна пом'якшити за допомогою налаштувань RabbitMQ/Docker/TestContainer.
  • Kafka не можна назвати традиційним "розумним" брокером. Навпаки, це «дурне» сховище повідомлень (файл журналу), для обробки якого потрібні розумні споживачі.
Щоб краще зрозуміти, коли використовувати RabbitMQ (або інші традиційні брокери повідомлень в цілому) або Kafka, погляньте на цю посаду в Pivotal(англійською мовою) як відправна точка. Загалом, коли вибираєте брокер обміну повідомленнями, намагайтеся ігнорувати штучні причини продуктивності. Був час, коли команди та інтернет-спільноти постійно сперечалися про те, наскільки швидким був RabbitMQ та наскільки повільним ActiveMQ. Тепер ті ж аргументи наводяться щодо RabbitMQ, мовляв, він повільно працює з 20-30 тисячами повідомлень за секунду. У Kafka фіксується 100 тисяч повідомлень за секунду. Відверто кажучи, такі порівняння подібні до порівняння теплого з м'яким. Крім того, в обох випадках значення пропускної спроможності можуть бути на нижньому або середньому рівні, скажімо, Alibaba Group. Однак ви навряд чи стикалися з проектами такого масштабу (мільйони повідомлень за хвабону) насправді. Вони безперечно існують, і у них були б проблеми. На відміну від решти 99% "звичайних" бізнес-проектів Java. Так що не звертайте уваги на моду та хайп. Вибирайте з розумом.

Які бібліотеки я можу використати для тестування мікросервісів?

Це залежить від вашого стека. Якщо у вас розгорнута екосистема Spring, буде розумно використовувати спеціальні інструменти цього фреймворку . Якщо JavaEE - щось на зразок Arquillian . Можливо, варто подивитися на Docker і справді хорошу бібліотеку Testcontainers , яка допомагає, зокрема, легко та швидко налаштувати базу даних Oracle для локальних тестів розробки чи інтеграції. Для мок-тестів цілих HTTP-серверів, зверніть увагу на Wiremock . Для тестування асинхронного обміну повідомленнями спробуйте впровадити ActiveMQ або RabbitMQ, а потім написати тести за допомогою Awaitility DSL . Крім цього, застосовуються всі ваші звичні інструменти - Junit , TestNGдля AssertJ і Mockito . Зверніть увагу, що це далеко не повний перелік. Якщо раптом ви не знайшли тут ваш улюблений інструмент, опублікуйте його в розділі коментарів.

Як увімкнути логування для всіх мікросервісів Java?

Логування у випадку з мікросервісами – цікава та досить складна тема. Замість одного файлу лога, з яким ви можете працювати за допомогою команд less або grep, тепер у вас є n файлів логування, і бажано, щоб вони не були занадто розрізнені. Добре розписані особливості екосистеми логування у цій статті (англійською мовою). Обов'язково прочитайте його і зверніть увагу на розділ Централізоване ведення лога з точки зору мікросервісів. На практиці ви зіткнетеся з різними підходами: Системний адміністратор пише певні сценарії, які збирають та об'єднують файли логів з різних серверів в один файл логів та поміщають їх на FTP-сервери для завантаження. Запуск комбінацій cat/grep/unig/sort у паралельних SSH-сесіях. Саме так чинить Amazon AWS, про що ви можете повідомити свого менеджера. Використовуйте такий інструмент, як Graylog або ELK Stack (Elasticsearch, Logstash, Kibana)

Як мої мікросервіси знаходять одне одного?

Досі ми припускали, що наші мікросервіси знають один про одного, знають відповідний IPS. Поговоримо про статичне налаштування. Отже, наш банківський моноліт [ip = 192.168.200.1] знає, що йому потрібно поговорити з ризик-сервером [ip = 192.168.200.2], який захардкожен у файлі properties. Однак ви можете зробити все динамічнішим:
  • Використовуйте хмарний сервер конфігурації, з якого всі мікросервіси виймають конфігурації замість розгортання файлів application.properties на своїх мікросервісах.
  • Оскільки екземпляри ваших служб можуть динамічно змінювати своє розташування, варто придивитися до служб, які знають, де живуть ваші служби, які IP і як їх маршрутизувати.
  • Тепер, коли все динамічно, з'являються нові проблеми, такі як автоматичне обрання лідера: хто є майстром, який працює над певними завданнями, щоб, наприклад, не обробити їх двічі? Хто заміняє лідера, коли він зазнає невдачі? За яким принципом відбувається заміна?
Загалом, це те, що називається мікросервісним оркеструванням і воно є ще однією бездонною темою. Такі бібліотеки, як Eureka або Zookeeper , намагаються "вирішити" ці проблеми, показуючи які служби доступні. З іншого боку, вони приносять додаткову складність. Запитайте будь-кого, хто колись встановлював ZooKeeper.

Як організувати авторизацію та аутентифікацію за допомогою мікросервісів Java?

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

Як переконатись, що всі мої оточення виглядають однаково?

Те, що правильне для розгортань без мікросервісу, також вірно і для розгортань з ним. Спробуйте комбінацію Docker/Testcontainers, а також Scripting/Ansible.

Не питання: коротко про YAML

Давайте ненадовго відійдемо від бібліотек та пов'язаних із ними питань і коротко розглянемо Yaml. Цей формат файлу використовується де-факто як формат для «запису конфігурації у вигляді коду». Використовують його і прості інструменти, на зразок Ansible і гіганти на кшталт Kubernetes. Щоб випробувати біль від відступів в YAML, спробуйте написати простий Ansible-файл і подивіться, скільки вам доведеться редагувати файл, перш ніж він запрацює як треба. І це незважаючи на підтримку формату всіма великими IDE! Після цього повертайтеся, щоб дочитати цей посібник.
Yaml:
  - is:
    - so
    - great

А як щодо розподілених транзакцій? Тестування продуктивності? Інші теми?

Може, колись у наступних редакціях керівництва. А поки що все. Залишайтеся з нами!

Концептуальні проблеми мікросервісів

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

Невідповідність Frontend та Backend

Невідповідність Frontend та Backend — дуже поширена проблема багатьох мікросервісних проектів. Що вона означає? Лише те, що в старих добрих монолітах, розробники веб-інтерфейсу мали одне конкретне джерело для отримання даних. У мікросервісних проектах у розробників веб-інтерфейсу зненацька з'являються n джерел для отримання даних. Уявіть, що ви створюєте проект мікросервісів IoT (інтернет речей) на Java. Скажімо, управляєте геодезичними машинами, промисловими печами по всій Європі. І ці печі регулярно відправляють вам оновлення із зазначенням їх температури тощо. Рано чи пізно ви, можливо, захочете знайти печі в інтерфейсі користувача адміністратора, можливо, за допомогою мікросервісів «пошуку печі». Залежно від того, наскільки строго ваші бекенд-колеги застосовуютьпредметно-орієнтоване проектування чи закони мікросервісів, мікросервіс “знайти піч” може повертати лише ідентифікатори печей, а чи не інші дані, такі як тип, модель чи місцезнаходження. Для цього фронтенд-розробникам потрібно буде виконати один або n додаткових викликів (залежно від реалізації пейджингу) у мікросервісі «отримати дані про печі» з ідентифікаторами, які вони отримали від першого мікросервісу. Посібник з мікросервісів Java.  Частина 3: загальні питання – 4І хоча це лише простий приклад, нехай і взятий із реального (!) проекту, навіть він демонструє наступну проблему: супермаркети стали надзвичайно популярними. А все тому, що з ними вам не потрібно йти в 10 різних місць, щоб купити овочі, лимонад, заморожену піцу та туалетний папір. Натомість ви йдете в одне місце. Це простіше і швидше. Те саме стосується розробників інтерфейсів та мікросервісів.

Очікування керівництва

У менеджменту складається помилкове враження, що тепер потрібно наймати нескінченну кількість розробників у (всеосяжний) проект, оскільки розробники тепер можуть працювати абсолютно незалежно один від одного, кожен на своєму мікросервісі. Наприкінці потрібна лише невелика робота з інтеграції (незадовго до запуску). Посібник з мікросервісів Java.  Частина 3: загальні питання – 5Насправді такий підхід є вкрай проблематичним. У наступних параграфах ми намагатимемося пояснити, чому.

"Менші шматочки" не одно "кращі шматочки"

Буде великою помилкою вважати, що розділений на 20 частин код обов'язково буде якісніше одного цільного шматка. Навіть якщо взяти якість з суто технічної точки зору: наші окремі служби, як і раніше, можуть виконувати 400 запитів Hibernate для вибору користувача з бази даних, проходячи по шарах коду, що не підтримується. Вкотре повертаємося до цитати Саймона Брауна: якщо не вдасться побудувати моноліти належним чином, буде складно створити належні мікросервіси. Найчастіше про стійкість до відмови в мікросервісних проектах вкрай несвоєчасно. Настільки, що часом страшно дивитися, як мікросервіси працюють у реальних проектах. Причина цього полягає в тому, що Java-розробники не завжди готові вивчати стійкість до відмов, мережі та інші суміжні теми на належному рівні. Самі "шматочки" - менше, а ось "технічних частин" - більше. Уявіть, що вашій мікросервісній команді пропонується написати технічний мікросервіс для входу в систему бази даних приблизно такою:
@Controller
class LoginController {
    // ...
    @PostMapping("/login")
    public boolean login(String username, String password) {
        User user = userDao.findByUserName(username);
        if (user == null) {
            // обработка варианта с несуществующим пользователем
            return false;
        }
        if (!user.getPassword().equals(hashed(password))) {
            // обработка неверного пароля
            return false;
        }
        // 'Ю-ху, залогинабось!';
        // установите cookies, делайте, что угодно
        return true;
    }
}
Тепер ваша команда може вирішити (і, можливо, навіть переконати людей бізнесу), мовляв, це все надто просто і нудно, краще замість служби входу в систему написати дійсно корисний мікросервіс UserStateChanged (зміна стану користувача) без будь-яких реальних та відчутних бізнес- вимог. А оскільки до Java зараз деякі люди ставляться як до динозавра, напишемо наш мікросервіс UserStateChanged на модному Erlang. І давайте спробуємо де-небудь використовувати червоно-чорні дерева, тому що Стів Єгге написав, що ви повинні знати їх зсередини, щоб подати заявку в Google. З погляду інтеграції, обслуговування та загального проекту це так само погано, як написання шарів спагетті-коду всередині одного моноліту. Штучний та пересічний приклад? Так і є. Проте подібне може бути і насправді.

Менше шматочки - менше розуміння

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

Комунікації та обслуговування

Рівень комунікації та обслуговування залежить від розміру компанії. Проте, загальна залежність очевидна: що більше, то проблематичніше.
  • Хто працює на мікросервісі №47?
  • Вони щойно розгорнули нову несумісну версію мікросервісу? Де це було задокументовано?
  • З ким мені потрібно поговорити, щоб запитити нову функцію?
  • Хто підтримуватиме той мікросервіс на Erlang, після того, як єдиний хто знав цю мову покинув компанію?
  • Усі наші мікросервісні команди працюють не тільки різними мовами програмування, але й у різних часових поясах! Як ми це правильно скоординуємо?
Посібник з мікросервісів Java.  Частина 3: загальні питання - 6Головна думка полягає в тому, що як і у випадку з DevOps, повноцінний підхід до мікросервісів у великій, можливо навіть міжнародній компанії, пов'язаний з купою додаткових комунікаційних проблем. І компанія має серйозно до цього підготуватись.

Висновки

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

Мікросервіси чи моноліт?

Використання Java-мікросервісів завжди і скрізь це одна крайність. Інший виявляється щось на кшталт сотень старих добрих модулів Maven у моноліті. Ваше завдання знайти правильний баланс. Особливо це стосується нових проектів. Тут вам ніщо не завадить дотримуватись більш консервативного, “монолітного” підходу та створювати меншу кількість хороших модулів Maven, замість того, щоб починати з двадцяти мікросервісів, готових до роботи у хмарах.

Мікросервіси генерують додаткову складність

Майте на увазі, що чим більше у вас мікросервісів і чим менше у вас дійсно потужних DevOps'ів (ні, запуск пари-трійки сценаріїв Ansible або розгортання на Heroku не вважається!), тим більше проблем у вас виникне пізніше у роботі. Навіть просто прочитати до кінця розділ цього посібника, присвячений загальним питанням про мікросервіси Java - досить стомлююче заняття. Добре подумайте про реалізацію рішень для всіх цих інфраструктурних завдань, і ви раптово зрозумієте, що все це більше не пов'язане з бізнес-програмуванням (за що вам платять), а скоріше з фіксацією більшої кількості технологій на більшій кількості технологій. Шива Прасад Редді відмінно резюмував у своєму блозі : "Ви не уявляєте собі, як це жахливо, коли команда 70% часу бореться з цією сучасною інфраструктурою і лише 30% часу залишається на реальну бізнес-логіку" Шива Прасад Редді

Чи варто створювати мікросервіси Java?

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

Сценарій

Уявіть, що у вас є Java-моноліт, що працює один на самому маленькому виділеному сервері Hetzner . Те саме стосується і вашого сервера баз даних, він також працює на аналогічній машині Hetzner . І давайте також припустимо, що ваш Java-моноліт може обробляти робочі процеси, скажімо, реєстрацію користувачів, і ви створюєте не сотні запитів до бази даних на робочий процес, а розумнішу кількість (<10).

Питання

Скільки з'єднань з базою даних має відкрити моноліт Java (пул з'єднань) на вашому сервері баз даних? Чому так? Як ви вважаєте, скільки активних користувачів одночасно може (приблизно) масштабувати ваш моноліт?

Відповідь

Залишіть свою відповідь на ці запитання у розділі коментарів. Я з нетерпінням чекаю на всі відповіді. Посібник з мікросервісів Java.  Частина 3: загальні питання – 8Тепер наважуйтесь. Якщо ви дочитали до кінця, ми дуже вам вдячні!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ