Ten moduł pokazał kilka narzędzi: cache w pamięci procesu i w Redisie, indeksy w bazie, kompresję odpowiedzi, ograniczanie liczby żądań. Projekt jest miejscem, w którym trzeba zdecydować, którego użyć - a to zupełnie inna umiejętność niż znajomość składni.
Zbudujesz API zarządzania legionem, które radzi sobie z ruchem większym, niż wytrzymałaby wersja naiwna, i potrafi pokazać, dlaczego sobie radzi.
Optymalizacja bez pomiaru to zgadywanie. Brzmi banalnie, a jest najczęściej łamaną regułą w tej dziedzinie: dodaje się cache tam, gdzie akurat przyszło do głowy, i odnotowuje poprawę, której nikt nie zmierzył.
Porządek pracy jest zawsze ten sam:
Dlatego projekt zaczyna się od pomiaru, nie od cache'a.
Pierwszą rzeczą, jaką wystawisz, jest podgląd stanu procesu:
1@Controller('metrics')
2export class MetricsController {
3 @Get()
4 getMetrics() {
5 const memory = process.memoryUsage();
6
7 return {
8 heapUsed: memory.heapUsed,
9 heapTotal: memory.heapTotal,
10 rss: memory.rss,
11 uptime: process.uptime(),
12 };
13 }
14}process.memoryUsage() zwraca kilka liczb i warto wiedzieć, czym się różnią. heapUsed to pamięć faktycznie zajęta przez obiekty twojej aplikacji - ta rośnie, gdy trzymasz w cache'u za dużo. heapTotal to pamięć, którą silnik V8 zarezerwował na stertę; jest zawsze większa i zmienia się skokowo. rss (Resident Set Size) to całość pamięci procesu widziana przez system operacyjny - sterta, stos, kod i bufory razem.Najważniejsza jest różnica między
heapUsed a rss. Gdy rośnie sam heapUsed, gromadzisz obiekty - zwykle w jakimś cache'u bez limitu. Gdy rośnie rss, a heapUsed stoi, wyciek jest poza stertą: bufory, otwarte połączenia, biblioteki natywne.
podaje, ile sekund działa proces. Sam w sobie mało mówi, ale w zestawieniu z resztą owszem: zużycie pamięci rosnące liniowo z czasem działania to wyciek, a nie obciążenie.process.uptime()
Cache w pamięci procesu, który poznałeś wcześniej, ma jedną wadę: każda instancja aplikacji ma własny. Przy dwóch instancjach ten sam odczyt trafia do bazy dwa razy, a unieważnienie w jednej nie dociera do drugiej. Redis to naprawia:
1CacheModule.register({
2 store: redisStore,
3 host: 'localhost',
4 port: 6379,
5 ttl: 300 })Kolejność zapisu jest stała:
otwiera konfigurację, CacheModule.register({
podmienia magazyn z pamięci procesu na Redisa, store: redisStore,
i host: 'localhost',
wskazują, gdzie ten Redis stoi, a port: 6379,
ustala domyślny czas życia wpisu i domyka.ttl: 300 })
Kluczowy jest
store. Bez niego CacheModule działa dalej, tylko w pamięci procesu - i to jest podstępne, bo lokalnie wszystko wygląda dobrze, a problem pojawia się dopiero po uruchomieniu drugiej instancji na produkcji.Zwróć uwagę, że
host i port stoją tu bezpośrednio w obiekcie konfiguracji. W nowszych wersjach cache-manager zagnieżdża się je w osobnym polu połączenia - sprawdź wersję w swoim package.json, zanim skopiujesz cudzy przykład.Cache przyspiesza powtarzalne odczyty. Zapytania, których nie da się zbuforować, trzeba naprawić u źródła:
1@Entity('legionaries')
2@Index(['cohortId', 'isActive'])
3export class Legionary {
4 @Column()
5 cohortId: number;
6
7 @Column({ default: true })
8 isActive: boolean;
9}
10
11// w repozytorium
12findActiveByCohort(cohortId: number) {
13 return this.repo.find({
14 where: { cohortId, isActive: true },
15 select: ['id', 'name', 'rank'],
16 take: 50,
17 });
18}Trzy poprawki naraz, każda o innym działaniu. Indeks złożony na kolumnach, po których naprawdę filtrujesz, zamienia przegląd całej tabeli w skok do konkretnych wierszy.
pobiera tylko potrzebne kolumny - bez niego ściągasz z bazy pola, których nikt nie użyje, wraz z całą ich objętością. select
ogranicza liczbę wierszy; endpoint bez limitu działa świetnie do dnia, w którym ktoś ma ich sto tysięcy.take
Zanim dodasz indeks, sprawdź plan zapytania przez
EXPLAIN. Indeks na kolumnie, po której nikt nie filtruje, nie przyspiesza odczytu, a spowalnia każdy zapis.Na koniec dwie rzeczy działające na brzegu aplikacji:
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.use(compression());
5
6 await app.listen(3000);
7}compression() pakuje odpowiedzi gzipem - przy listach w formacie JSON to zwykle kilkukrotne zmniejszenie transferu, kosztem odrobiny procesora. Rate limiting z ThrottlerModule działa z drugiej strony: nie przyspiesza obsługi, tylko ogranicza liczbę żądań, które w ogóle przyjmujesz.Te dwa mechanizmy warto rozumieć jako parę przeciwieństw. Kompresja i cache sprawiają, że jedno żądanie kosztuje mniej. Ograniczanie liczby żądań sprawia, że żądań jest mniej. Gdy system się dławi, zawsze masz do wyboru te dwie drogi - i zwykle potrzebujesz obu.
Projekt jest gotowy, gdy zawiera:
/metrics z heapUsed, heapTotal, rss i uptime.CacheModule.register ze store: redisStore, z unieważnianiem przy zapisie.select i take w zapytaniach listujących.Piąty punkt jest najważniejszy i najczęściej pomijany. Bez niego oddajesz zbiór technik, a nie optymalizację - bo optymalizacja to różnica między dwoma pomiarami, @name, a nie lista zastosowanych narzędzi.
Prześlij link do repozytorium, gdy skończysz.