Powrót do strony głównej

Logowanie asynchroniczne C++: rzeczywiste ograniczenia

Logowanie asynchroniczne w C++ redystrybuuje obciążenie, ale nie eliminuje kopiowania danych i bottleneck'ów sinków. Analiza pipeline'u, problemów deferred formatting i zachowania przy przeciążeniu. Benchmarki spdlog, logme, Quill.

Dlaczego async logi w C++ zawodzą: analiza bottleneck'ów
Advertisement 728x90

Ograniczenia asynchronicznego logowania w C++: analiza wąskich gardeł

Asynchroniczne logowanie w C++ często uznawane jest za uniwersalne rozwiązanie do przyspieszania aplikacji. W rzeczywistości jedynie przesuwa obciążenie, nie eliminując podstawowych ograniczeń. Wydajność zależy nie tylko od kolejek, ale także od kosztów kopiowania danych, szybkości sinków (celów zapisu) oraz zachowania systemu przy przeciążeniu. Poprawnie skonfigurowane logowanie synchroniczne czasem osiąga wyższy throughput niż jego odpowiednik asynchroniczny.

Struktura asynchronicznego potoku logowania

Asynchroniczne logowanie składa się z dwóch etapów: przechwytywania i formatowania argumentów oraz asynchronicznego zapisu.

Przechwytywanie argumentów

  • Kopiowanie ciągów z string_view lub char*.
  • Serializacja złożonych obiektów.
  • Weryfikacja czasu życia (lifetime) dla odłożonego formatowania.

Bezpieczne odtworzenie danych w późniejszym czasie możliwe jest wyłącznie po ich skopiowaniu. W przeciwnym razie formatowanie odbywa się synchronicznie.

Google AdInline article slot

Asynchroniczny zapis

  • Producenty umieszczają wpisy w kolejce.
  • Jeden wątek backendu zajmuje się formatowaniem i zapisem do sinków (pliku, konsoli).
  • Operacje flush i fsync decydują o rzeczywistej prędkości zapisu.

Schemat:

Synchronicznie: caller → format → write
Asynchronicznie: caller → capture → enqueue → backend → format → write

Dodatkowe narzuty obejmują synchronizację i zużycie pamięci.

Problemy z odłożonym formatowaniem

Odłożone formatowanie (deferred formatting) zachowuje argumenty do przetwarzania w wątku backendu. Jednak:

Google AdInline article slot
  • string_view sv = s; LOG_INFO("{}", sv); s.clear(); — dane stają się nieprawidłowe.
  • To samo dotyczy char* na stosie, widoków (views) kontenerów oraz obiektów zawierających wskaźniki.

Rozwiązania:

  • Kopiowanie: string_viewstd::string. Bezpieczne, ale zużywa więcej CPU i pamięci niż synchroniczne formatowanie.
  • Formatowanie synchroniczne: Zwiększa opóźnienie (latency) wywołującego wątku.
  • Niestandardowa serializacja: Wymaga implementacji kodeków, kopii danych lub gwarancji czasu życia.

Przeciążenie kolejek i jego konsekwencje

Asynchroniczne logowanie nie przyspiesza zapisu do sinka. Jeśli generowane jest 1 mln komunikatów na sekundę, a sink obsługuje tylko 100 tys. na sekundę, bufor rośnie bezustannie.

Jeden wątek backendu stanowi sztywne wąskie gardło w scenariuszach wielu producentów.

Google AdInline article slot

Gdy liczba producentów przewyższa możliwości konsumenta:

  • Rosnąca kolejka prowadzi do wycieku pamięci.
  • Blokowanie producentów — utrata korzyści asynchroniczności.
  • Odrzucanie komunikatów — utrata danych.
  • Nadpisywanie starszych wpisów — utrata historii.

Wolny sink (np. konsola: 200 µs vs plik: 10 µs) spowalnia cały potok.

Porównanie z podejściem synchronicznym

Synchroniczne formatowanie + asynchroniczny zapis są prostsze:

  • Brak problemów z czasem życia i wiszącymi referencjami (dangling references).
  • Przewidywalność: „co zalogowano — to się pojawi”.
  • Mniej błędów związanych z wyścigami (race conditions) i uszkodzeniem danych.

Asynchroniczne logowanie ma sens przy:

  • Nagłych skokach natężenia logowania (burst’ach).
  • Wielowątkowości z prostymi argumentami.

Nie ma sensu przy:

  • Stałej przeciążonej pracy.
  • Złożonych obiektach.
  • Wolnych sinkach.

Benchmarki popularnych loggerów

Testy przeprowadzone na logbench pokazują:

| Logger | Tryb synchroniczny | Tryb asynchroniczny |

|--------|---------------------|----------------------|

| spdlog | Wysoki throughput | + narzuty |

| logme | Wysoki throughput | + narzuty |

| Quill | Niedostępny | Blokada/odrzucanie przy przeciążeniu |

Synchroniczne wersje spdlog i logme są często szybsze od ich odpowiedników asynchronicznych ze względu na brak kolejek i operacji synchronizacji. Quill (pełnie asynchroniczny) wypełnia bufory sekwencyjnie: backend → frontend → blokada/odrzucanie.

Kluczowe wnioski

  • Asynchroniczne logowanie zmniejsza opóźnienia p99 w gorącym ścieżce, ale nie zwiększa całkowitego przepływu (throughput).
  • Kopiowanie danych dla odłożonego formatowania niweluje potencjalne korzyści.
  • Jeden wątek backendu to absolutny limit skalowalności.
  • Kolejki maskują, ale nie rozwiązują problemu przeciążenia.
  • Wybór modelu zależy od charakteru obciążenia: nagłe skoki (burst) vs stabilny przepływ (steady-state).

— Editorial Team

Advertisement 728x90

Czytaj dalej