Як компресія та кеш Redis покращили продуктивність мого додатку

pic

Вступ
Чи коли-небудь ви чекали, поки веб-сторінка завантажиться кілька секунд занадто довго, і вирішили її покинути? Ви не одні — дослідження показують, що 40% користувачів залишають сайт, якщо він завантажується більше ніж 3 секунди. Повільно завантажувані додатки можуть втратити користувачів і доходи, але ми знайшли рішення.

У цьому пості я розповім, як ми використовували Gzip для стиснення даних і Redis як кеш в пам'яті для значного покращення продуктивності нашого додатка. Обидві ці технології широко використовуються, перевірені часом і легко інтегруються в більшість сучасних веб-стеків.

1. Визначення проблеми

Ми почали помічати певний шаблон: під час пікових навантажень наш додаток працював дуже повільно. Метрики з AWS CloudWatch показали, що використання процесора в базі даних досягало 90%, а часи відповіді різко зростали.

2. Впровадження стиснення

Чому це корисно?
Стиснення зменшує розмір даних, що передаються між клієнтом і сервером.
Для впровадження стиснення ми використовували Gzip у нашому додатку на Node.js. Ось приклад middleware для Express, щоб увімкнути стиснення:

 // Middleware для увімкнення стиснення в Express  
 app.use(  
   compression({  
     level: zlib.Z_DEFAULT_COMPRESSION,  
     filter: (req, res) => {  
       if (req.headers['x-no-compression']) {  
         // не стискати відповіді з цим заголовком, щоб дозволити стрімінг  
         return false;  
       }  
       // стандартна функція фільтра  
       return compression.filter(req, res);  
     },  
   }),  
 );

Ми помітили зменшення розміру відповіді на 90% під час пікових навантажень, що призвело до значного прискорення часу завантаження — користувачі помітили різницю вже через кілька днів.

pic

3. Впровадження кешу Redis

Redis — це не лише кеш, а й універсальне, високо-продуктивне сховище даних, здатне обробляти публікації/підписки (pub/sub), таблиці лідерів та інше. Однак для нас його здатність до кешування стала справжнім проривом.

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

Приклад впровадження RedisClient у Node.js з NestJS:

 import { FactoryProvider } from '@nestjs/common';  
 import { Redis } from 'ioredis';  

 export const redisClientFactory: FactoryProvider = {  
   provide: 'RedisClient',  

   useFactory: () => {  
     const redisInstance = new Redis({  
       host: 'your-redis-host',  
       port: 6379,  
       password: 'your-redis-password',  
       tls: {},  
     });  

     redisInstance.on('error', (e) => {  
       redisInstance.disconnect();  
       throw new Error(`Redis connection failed: ${e}`);  
     });  

     return redisInstance;  
   },  
   inject: [],  
 };

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

Наша конфігурація Redis-кластера

Для ефективного впровадження Redis ми використали Terraform для розгортання і налаштування Redis-кластера на AWS ElastiCache. Ось ключові елементи нашої конфігурації:

Висока доступність
Ми налаштували два кеш-ноди (num_cache_clusters = 2), щоб забезпечити резервування та підвищити стійкість до збоїв.
Ця конфігурація забезпечує автоматичний перехід на інший вузол, якщо один з них стане недоступним.

Продуктивність і масштабованість
Ми обрали тип екземпляра cache.m6g.xlarge, оптимізований для продуктивності та економії. Цей тип екземпляра є частиною інстансів на основі Graviton2 від AWS, які забезпечують відмінну продуктивність для навантажень Redis.

Безпека даних
Шифрування при зберіганні та при передачі: Ми увімкнули параметри at_rest_encryption_enabled і transit_encryption_enabled, щоб забезпечити шифрування даних як під час зберігання, так і при передачі.

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

Обслуговування та резервне копіювання
Час для обслуговування: Планове обслуговування (maintenance_window) в години, коли навантаження на систему мінімальне, знижує вплив на користувачів.
Резервні копії за допомогою знімків: Ми увімкнули щоденні знімки (snapshot_window і snapshot_retention_limit = 7), щоб зберігати резервні копії протягом тижня, що забезпечує швидке відновлення у разі збою.

Користувацькі параметри
Ми налаштували конфігурацію Redis, збільшивши розмір буферу реплікації (repl-backlog-size = 16384), щоб покращити продуктивність реплікації під час високого трафіку.

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

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

Використання ioredis
Для взаємодії з Redis ми вибрали ioredis, потужну бібліотеку Node.js для роботи з Redis. Ми обрали її завдяки її багатофункціональності, включаючи підтримку кластерів Redis, Sentinel, конвеєризації та Pub/Sub. Її гнучкість і продуктивність роблять її ідеальним вибором для продуктивних середовищ.

Ось приклад того, як ми використовували клієнт ioredis, який ми налаштували раніше, для взаємодії з нашим Redis кластером:

 import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';  
 import { Redis } from 'ioredis';  

 @Injectable()  
 export class RedisRepository implements OnModuleDestroy {  
   constructor(@Inject('RedisClient') private readonly redisClient: Redis) {}  

   onModuleDestroy(): void {  
     this.redisClient.disconnect();  
   }  

   async get(key: string): Promise {  
     if (this.redisClient.status !== 'ready') return null;  
     const keys = await this.getKeys(key);  
     return this.redisClient.get(keys[0]);  
   }  
   async getKeys(key: string): Promise {  
     if (this.redisClient.status !== 'ready') return null;  
     return this.redisClient.keys(key);  
   }  

   async set(key: string, value: string): Promise {  
     if (this.redisClient.status !== 'ready') return null;  
     await this.redisClient.set(key, value);  
   }  

   async delete(key: string[]): Promise {  
     if (this.redisClient.status !== 'ready') return null;  
     await this.redisClient.del(key);  
   }  

   async setWithExpiry(key: string, value: string, expiry: number): Promise {  
     if (this.redisClient.status !== 'ready') return null;  
     await this.redisClient.set(key, value, 'EX', expiry);  
   }  
 }

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

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

Приклад ендпоінта:

@Get(':id')  
 async findOne(@Param('id') id: string): Promise {  
 const cachedResponse = await this.redisRepository.get(id);  
 if (!cachedResponse) {  
 const entity = await this.entityService.findOneEntity(id);  
 if (!entity) {  
 throw new NotFoundException(`Entity with id ${id} not found`);  
 }  
 await this.redisRepository.set(id, JSON.stringify(entity));  
 return entity;  
 }  
 return JSON.parse(entity);  
 }

З цими змінами ми зменшили середній час відповіді з 388 мс до 188 мс та значно знизили навантаження на нашу основну базу даних.

4. Остаточні результати

Результати впровадження Redis Cache та Gzip Compression були значними, трансформувавши як технічну продуктивність нашого додатка, так і загальний користувацький досвід.

Вплив на часи відповіді

  • Для складних запитів, що включають численні об'єднання чи агрегацію даних, Redis мав величезний вплив. Запити, що раніше виконувались за 388 мс, зменшились до 188 мс — покращення на 52%.
  • Навіть для простіших запитів Redis зменшив критичні мілісекунди, покращуючи загальну чутливість додатка.
  • Завдяки кешованим відповідям безпосередньо з пам'яті, ми уникали повторних запитів до бази даних, зменшуючи латентність та забезпечуючи більш стабільну продуктивність при високих навантаженнях.

Вплив на навантаження на базу даних

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

Вплив на пропускну здатність і передачу даних

  • Завдяки зменшенню розміру відповіді за допомогою Gzip на 90%, вимоги до пропускної здатності були значно знижені. Це дозволило нам обслужити більше користувачів одночасно без необхідності масштабувати серверні ресурси.

Вплив на користувацький досвід

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

Бізнес переваги

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

pic

pic

5. Уроки, які ми засвоїли

Впровадження компресії та кешу Redis дало нам цінні уроки щодо оптимізації продуктивності додатків. Ось основні висновки:

a. Компресія: “Мала зміна з великими результатами
Низькі витрати, великий ефект: Додавання компресії Gzip вимагало мінімальних зусиль з розробки, але результати були миттєвими та трансформуючими. Зменшення розміру відповіді на 90% не тільки прискорило завантаження сторінок, а й зменшило використання пропускної здатності, що дозволило ефективніше обслуговувати більше користувачів.
Сумісність важлива: Вибір широко підтримуваного алгоритму компресії, як Gzip, забезпечив безшовну інтеграцію з більшістю браузерів і клієнтів, уникнувши потенційних проблем із сумісністю.

b. Redis: “Продуктивність і простота
Кешування є важливим для масштабованості: Redis допоміг нам зняти навантаження з бази даних, значно зменшивши використання ЦП і покращивши часи відповіді, особливо для складних запитів.
Спрощення архітектури: Впровадивши кешування, ми спростили спосіб доступу до даних та їх надання. Дані, що часто використовуються, більше не потребували дорогих запитів до бази даних, що спростило логіку бекенду нашого додатка.

Універсальний інструмент: Redis — це не просто кеш, він має такі можливості, як Pub/Sub та скрипти Lua, які ми наразі вивчаємо для вирішення інших проблем у нашій системі.

c. Виклики та рішення
Термін придатності кешу: Управління інвалідацією кешу було одним з найбільш складних аспектів. Використовуючи шаблон cache-aside, ми забезпечили консистентність даних, зберігаючи переваги кешування щодо продуктивності.
Тонке налаштування конфігурацій: Налаштування параметрів Redis, таких як розмір реплікаційного буферу, вимагало методів проб і помилок, але зрештою це покращило продуктивність реплікації під час високого трафіку.

d. Важливість моніторингу: “Вимірюйте, а потім оптимізуйте
Метрики, такі як використання ЦП бази даних, часи відповіді та залучення користувачів, допомогли нам виявити вузькі місця продуктивності та оцінити вплив наших змін. Інструменти, як AWS CloudWatch та New Relic, були безцінні для відстеження цих покращень.

e. Орієнтованість на користувачів
Сприйнята продуктивність — це ключ. Користувачам не важливі технічні деталі — вони цінують швидкість і надійність. Сфокусувавшись на часах відповіді та зменшенні латентності, ми створили кращий досвід для них.

6. Висновок

Завдяки комбінації компресії та кешу Redis ми оптимізували наш додаток, покращили користувацький досвід та знизили операційні витрати.
Якщо ваш додаток стикається з проблемами продуктивності, подумайте про впровадження цих технік.
Які методи ви використовуєте для покращення продуктивності своїх додатків?
Напишіть у коментарях, які техніки працювали для вас, або якщо у вас є питання щодо впровадження цих стратегій.

Перекладено з: How Compression and Redis Cache Improved My App’s Performance

Leave a Reply

Your email address will not be published. Required fields are marked *