Завжди зважайте на 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 та зберігайти дані локально (в тому числі в кеші).
Як це працює?
- Коли відбувається оновлення даних (наприклад, створення або зміна замовлення), відповідний сервіс «публікує» подію про зміну в шину подій.
- Інші частини вашої системи, які потребують ці дані, підписуються на відповідні події. Коли подія надходить, вони отримують оновлену інформацію та зберігають її локально.
- Таким чином, коли вам знадобляться ці дані, ви вже не будете виконувати мережевий виклик, а скористаєтесь швидким локальним сховищем.
Звичайно, це ускладнює архітектуру, звичайно, це підійде не для всіх задач. Але памʼятайте про цей прийом, він може досить елегантно вирішити проблему в вашому проекті.
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівЗавжди, окрім тих випадків коли лейтенсі не є важливим якісним атрибутом.
Але давайте спробуємо подивитись, як ми все ж плануємо вирішити проблему лейтенсі (мережеве лейтенсі):
Як казав один знайомий «Є лише 2 способи покращити перформенс — додати кеш та прибрати кеш». Якщо у вас не патерн «часте читання рідкозмінюваних даних», то ви міняєте мережеве лейтенсі на прогрівання/оновлення кешів.
1) Інвалідація — це про інтегреті, а не про лейтенсі. В контесті лейтенсі вас будуть цікавити кешміси. Інвалідація може додати проблем зі збільшенням кешмісів теж.
2) Ви говорите про розподілену систему, але якщо ваш кеш «всередині процесу», то це не масштабується і кеш-асайд не буде працювати при обробці запитів іншим процесом. Якщо у вас віддалений кеш, то він працює по мережі, це інше але все ж мережеве лейтенсі (датагрід може бути краще, але у випадку, коли данні локально).
3) У багатьох випадках у вас ще буде лейтенсі на десереалізацію даних з кеша, бо багато кешів зберігають дані саме серіалізованими по ряду причин.
Знову ж, якщо у вас паб-саб не ін процес, то ви маєте певне мережеве лейтенсі, ще лейтенсі не серіалізацію/десеріалізацію повідомлень і надодачу лейтенсі в середині самого паб-саба.
---
Ось це гарна стратегія по цілому ряду причин.
Але стратегія, а от реалізація має ризики у вигляді надлишкових/непотрібних запитів на читання даних, що знову ж може погіршити загальний лейтенсі системи/цільового запиту.
Згоден по всім пунктам, дякую за якісне доповнення