Завжди зважайте на Latency при мережевих викликах в розподілених системах

Думаю, що не треба наголошувати, що request до зовнішньої мережі буде працювати значно повільніше (і менш стабільно) ніж звернення до локальної памʼяті або локальної мережі. Різниця в часі щонайменше в 7 разів. Саме тому архітектура вашого застосунку, ваш код, має це враховувати.

Чітко і явно відокремлюйте виклики через зовнішню мережу.

Ми постійно використовуємо DI контейнер для зручності, тому клієнтський код десь в application layer, може виглядати достатньо звично:

<?php
// client code after DI container wiring
$order = new Order($id, $userId, $total);
$this->orderRepository->save($order);

Проте, всередині може бути досить велика різниця:

<?php
final readonly class OrderRepository
{
    public function __construct(
        private PostgresConnection $connection
    )
    {
    }
    public function save(Order $order)
    {
        $this->connection->execute('INSERT INTO orders (id, user_id, total) VALUES (?, ?, ?)', [
            $order->id(),
            $order->userId(),
            $order->total()
        ]);
    }
}

Зовсім інша реалізація

<?php
final readonly class OrderRepository
{
    public function __construct(
        private OrderApiClient $client
    )
    {
    }
    public function save(Order $order)
    {
        $this->client->createOrder($order);
    }
}

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

🔥 Введіть неймінг, котрий явно буде кричати про себе, говорити що виклик зовнішній:

OrderProvider, OrderGateway, OrderIntegration, etc.

Будь яка конвенція, котра буде команді довподоби, але вона має чітко відрізнятись від:

OrderRepository, OrderStorage, etc.

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

Намагайтесь отримати одразу всі необхідні дані

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

<?php
employees = $this->employeeProvider->getList($limit, $offset);
foreach ($employees as $employee) {
    $department = $this->departmentProvider->getById(
      $employee->getDepartmentId(),
    );
    // do something with department
}

Проти

<?php
$employees = $this->employeeProvider->getList($limit, $offset);
$departments = $this->departmentProvider->getAll();
// prepare a map of department id to department
foreach ($employees as $employee) {
    $department = $departments[$employee->getDepartmentId()];
    // do something with department
}

Кешування ваш друг, але будьте з ним обережні

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

❗️Обовʼязково приділіть час для узгодження стратегії інвалідації кеша.

Використовуючи паттерн cache-aside (Cпочатку дивимось в кеш, якщо дані є — повертаємо response. Якщо даних немає в кеші, то отримуємо із визначеного джерела, зберігаємо в кеш і потім повертаємо response), памʼятайте, що ваш кеш не повинен стати додатковою точкою відмови, наприклад якщо ваш redis впав по якійсь причині.

cache-aside pattern example

<?php 
$employees = $this->cache->get($cacheKey);
if (null === $employees) {
    $employees = $this->employeeProvider->getList($limit, $offset);
    $this->cache->set($cacheKey, $employees);
}
return $employees;

Інвертуйте потік даних

Замість того, щоб кожного разу опитувати інші сервіси, ви можете використовувати патерн Publisher-Subscriber та зберігайти дані локально (в тому числі в кеші).

Як це працює?

  1. Коли відбувається оновлення даних (наприклад, створення або зміна замовлення), відповідний сервіс «публікує» подію про зміну в шину подій.
  2. Інші частини вашої системи, які потребують ці дані, підписуються на відповідні події. Коли подія надходить, вони отримують оновлену інформацію та зберігають її локально.
  3. Таким чином, коли вам знадобляться ці дані, ви вже не будете виконувати мережевий виклик, а скористаєтесь швидким локальним сховищем.

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

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному0
LinkedIn
Ctrl + Enter
Ctrl + Enter
Завжди зважайте на Latency при мережевих викликах в розподілених системах

Завжди, окрім тих випадків коли лейтенсі не є важливим якісним атрибутом.
Але давайте спробуємо подивитись, як ми все ж плануємо вирішити проблему лейтенсі (мережеве лейтенсі):

Намагайтесь отримати одразу всі необхідні дані

Як казав один знайомий «Є лише 2 способи покращити перформенс — додати кеш та прибрати кеш». Якщо у вас не патерн «часте читання рідкозмінюваних даних», то ви міняєте мережеве лейтенсі на прогрівання/оновлення кешів.

Кешування — ваш друг, але будьте з ним обережні
...Він одночасно зменшить час на виклик, обробку даних і навантаження на мережу.
❗️Обовʼязково приділіть час для узгодження стратегії інвалідації кеша.
Використовуючи паттерн cache-aside

1) Інвалідація — це про інтегреті, а не про лейтенсі. В контесті лейтенсі вас будуть цікавити кешміси. Інвалідація може додати проблем зі збільшенням кешмісів теж.
2) Ви говорите про розподілену систему, але якщо ваш кеш «всередині процесу», то це не масштабується і кеш-асайд не буде працювати при обробці запитів іншим процесом. Якщо у вас віддалений кеш, то він працює по мережі, це інше але все ж мережеве лейтенсі (датагрід може бути краще, але у випадку, коли данні локально).
3) У багатьох випадках у вас ще буде лейтенсі на десереалізацію даних з кеша, бо багато кешів зберігають дані саме серіалізованими по ряду причин.

Замість того, щоб кожного разу опитувати інші сервіси, ви можете використовувати патерн Publisher-Subscriber

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

---

Чітко і явно відокремлюйте виклики через зовнішню мережу

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

Згоден по всім пунктам, дякую за якісне доповнення

Підписатись на коментарі