Badamy silnik VoIP Mediastreamer2. Część 12

Materiał artykułu pochodzi z mojego kanału Zen.

Badamy silnik VoIP Mediastreamer2. Część 12

W poprzedniej artykuł, obiecałem rozważyć kwestię oceny obciążenia na tickerze oraz sposoby radzenia sobie z nadmiernym obciążeniem obliczeniowym w mediastreamerze. Jednak postanowiłem, że logiczniej będzie poruszyć kwestie debugowania filtrów kraftowych związane z przesyłaniem danych, a później omówić kwestie optymalizacji wydajności.

Debugowanie filtrów kraftowych

Po tym, jak w poprzednim artykule omawialiśmy mechanizm przesyłania danych w mediastreamerze, logiczne będzie omówić kryjące się w nim niebezpieczeństwa. Jedną z cech zasady „data flow” jest to, że alokacja pamięci z sterty odbywa się w filtrach znajdujących się u źródła przepływu danych, a zwolnienie pamięci z powrotem do sterty odbywa się w filtrach umiejscowionych na końcu ścieżki przepływu. Ponadto, tworzenie nowych danych i ich usuwanie może mieć miejsce gdzieś w punktach pośrednich. Generalnie rzecz biorąc, zwolnienia pamięci dokonuje nie ten filtr, który utworzył blok danych.

Z perspektywy transparentnego monitorowania pamięci, rozsądne byłoby, aby filtr, otrzymując blok wejściowy, po przetworzeniu od razu go niszczył, zwalniając pamięć, a na wyjściu udostępniałby nowo utworzony blok z danymi wyjściowymi. W takim przypadku wyciek pamięci w filtrze byłby łatwy do prześledzenia — jeśli analizator wykrył wyciek w filtrze, oznaczałoby to, że następujący po nim filtr nie niszczy odpowiednio przychodzących bloków i błąd leży w nim. Jednak z perspektywy utrzymania wysokiej wydajności, takie podejście do pracy z blokami danych nie jest produktywne — prowadzi do dużej liczby operacji alokacji/zwolnienia pamięci pod bloki danych bez jakiejkolwiek użytecznej wydajności.

Z tego powodu filtry mediastreamera, aby nie spowalniać przetwarzania danych, przy kopiowaniu wiadomości wykorzystują funkcje tworzące lekkie kopie (opowiadaliśmy o nich w poprzednim artykule). Te funkcje jedynie tworzą nową instancję nagłówka wiadomości, "przyczepiając" do niego blok danych z kopiowanej "starej" wiadomości. W wyniku tego, do jednego bloku danych przypisane są dwa nagłówki, a licznik referencji w bloku danych jest inkrementowany. Jednak z perspektywy, będzie to wyglądać jak dwa osobne wiadomości. Może być więcej wiadomości z takim "uspójnionym" blokiem danych, na przykład filtr MS_TEE generuje od razu dziesiątki takich lekkich kopii, rozdzielając je po swoich wyjściach. Przy prawidłowym działaniu wszystkich filtrów w łańcuchu, na końcu ta liczba referencji powinna osiągnąć zero, co spowoduje wywołanie funkcji zwalniania pamięci: ms_free(). Jeśli nie nastąpi wywołanie, oznacza to, że ten blok pamięci nie powróci do sterty, tzn. "wycieknie". Ceną za korzystanie z lekkich kopii jest utrata możliwości łatwego ustalenia (jak to byłoby w przypadku użycia zwykłych kopii), w którym filtrze grafu występuje wyciek pamięci.

Ponieważ odpowiedzialność za znajdowanie wycieków pamięci w "rdzennych" filtrach spoczywa na twórcach mediastreamera, prawdopodobnie nie będziesz musiał ich debugować. Jednak w przypadku twojego własnego filtra — jesteś kowalem swojego szczęścia i od twojej staranności zależy, ile czasu spędzisz na poszukiwaniu wycieków w swoim kodzie. Aby skrócić czas twojego błądzenia z debugowaniem, musimy omówić techniki lokalizacji wycieków podczas tworzenia filtrów. Może się również zdarzyć tak, że wyciek ujawni się dopiero podczas używania filtra w rzeczywistym systemie, gdzie liczba "podejrzanych" może się okazać ogromna, a czas na debugowanie ograniczony.

Jak objawia się wyciek pamięci?

Logicznie można założyć, że w wyjściu programu top będzie pokazywany rosnący procent pamięci zajmowanej przez twoją aplikację.

Zewnętrzne objawy będą polegać na tym, że w pewnym momencie system zacznie wolno reagować na ruch myszy, wolno odświeżać ekran. Może także wzrosnąć dziennik systemowy, zajmując miejsce na twardym dysku. W tym czasie twoja aplikacja zacznie zachowywać się dziwnie, nie będzie reagować na polecenia, nie będzie mogła otworzyć pliku itp.

Aby wykryć fakt wystąpienia wycieku, wykorzystamy analizator pamięci (dalej analizator). Może być to Valgrind (dobry artykuł na temat) lub zintegrowany w kompilatorze gcc MemorySanitizer lub coś innego. Jeśli analizator pokaże, że wyciek występuje w jednym z filtrów grafu, oznacza to, że nadszedł czas, aby zastosować jedną z metod opisanych poniżej.

Metoda trzech sosen

Jak już wspomniano, w przypadku wycieku pamięci analizator wskaże na filtr, który zażądał przydzielenia pamięci z heapu. Ale nie wskaże na filtr, który "zapomniał" jej zwrócić, który tak naprawdę jest winny. Tak więc analizator może tylko potwierdzić nasze obawy, ale nie wskazać ich źródła.

Aby ustalić lokalizację „złego” filtra w grafie, można skrócić graf do minimalnej liczby węzłów, przy której analizator jeszcze wykrywa wyciek i w pozostałych trzech sosnach zlokalizować problematyczny filtr.

Jednak może się zdarzyć, że skracając liczbę filtrów w grafie, zaburzysz normalny przebieg interakcji filtrów z innymi elementami twojego systemu i wyciek przestanie się objawiać. W takim przypadku trzeba będzie pracować z pełnowymiarowym grafem i zastosować podejście opisane poniżej.

Metoda przesuwającego izolatora

Dla uproszczenia przedstawimy graf składający się z jednego łańcucha filtrów. Jest on przedstawiony na rysunku.

Badamy silnik VoIP Mediastreamer2. Część 12

Zwykły graf, w którym obok gotowych filtrów mediastreamera zastosowane zostały cztery filtrów krawieckich F1…F4, czterech różnych typów, które stworzyliście dawno temu i ich poprawność nie budzi wątpliwości. Niemniej jednak załóżmy, że w kilku z nich występuje wyciek pamięci. Uruchamiając nasz program nadzoru analizy, z jego raportu dowiadujemy się, że pewien filtr poprosił o pewną ilość pamięci i nie zwrócił jej do sterty N-ty raz. Łatwo można się domyślić, że będzie odniesienie do wewnętrznych funkcji filtra typu MS_VOID_SOURCE. Jego zadaniem jest pobierać pamięć z sterty. Zwracać ją powinny inne filtry. Tzn. odkryjemy fakt wycieku.

Aby określić, na którym odcinku linii produkcyjnej doszło do bezczynności prowadzącej do wycieku pamięci, proponuje się wprowadzenie dodatkowego filtra, który po prostu przenosi wiadomości z wejścia na wyjście, ale przy tym tworzy nieskompresowaną, w normalnej "ciężkiej" kopii wiadomości wejściowej, następnie całkowicie usuwając wiadomość, która wpłynęła na wejście. Będziemy nazywać taki filtr izolatorem. Zakładamy, że ponieważ filtr jest prosty, więc wyciek w nim jest wykluczony. I jeszcze jedna pozytywna cecha — jeśli dodamy go w jakimkolwiek miejscu naszego grafu, to nie wpłynie to na działanie schematu. Będziemy przedstawiać filtr-izolator w postaci okręgu z podwójną krawędzią.

Włączamy izolator tuż po filtrze voidsourse:
Badamy silnik VoIP Mediastreamer2. Część 12

Ponownie uruchamiamy program z analizatorem i widzimy, że tym razem, analizator obarcza winą izolator. To on teraz tworzy bloki danych, które następnie znikają z nieznanego, niedbałego filtra (lub filtrów). Następnym krokiem przesuwamy izolator w prawo o jeden filtr i ponownie uruchamiamy analizę. Tak, krok po kroku przesuwając izolator w prawo, uzyskamy sytuację, w której w kolejnym raporcie analizatora liczba "wyciekłych" bloków pamięci zmniejszy się. To oznacza, że na tym etapie izolator znalazł się w łańcuchu tuż po problematycznym filtrze. Jeśli "zły" filtr był tylko jeden, to wyciek całkowicie zniknie. W ten sposób zlokalizowaliśmy problematyczny filtr (lub jeden z kilku). "Naprawiając" filtr, możemy kontynuować przesuwanie izolatora w prawo po łańcuchu aż do pełnego zwycięstwa nad wyciekami pamięci.

Implementacja filtra-izolatora

Wdrożenie izolatora wygląda jak zwykły filtr. Plik nagłówkowy:

/* Файл iso_filter.h  Описание изолирующего фильтра. */

#ifndef iso_filter_h
#define iso_filter_h

/* Задаем идентификатор фильтра. */
#include <mediastreamer2/msfilter.h>

#define MY_ISO_FILTER_ID 1024

extern MSFilterDesc iso_filter_desc;

#endif

Sam filtr:

/* Файл iso_filter.c  Описание изолирующего фильтра. */

#include "iso_filter.h"

    static void
iso_init (MSFilter * f)
{
}
    static void
iso_uninit (MSFilter * f)
{
}

    static void
iso_process (MSFilter * f)
{
    mblk_t *im;

    while ((im = ms_queue_get (f->inputs[0])) != NULL)
    {
        ms_queue_put (f->outputs[0], copymsg (im));
        freemsg (im);
    }
}

static MSFilterMethod iso_methods[] = {
    {0, NULL}
};

MSFilterDesc iso_filter_desc = {
    MY_ISO_FILTER_ID,
    "iso_filter",
    "A filter that reads from input and copy to its output.",
    MS_FILTER_OTHER,
    NULL,
    1,
    1,
    iso_init,
    NULL,
    iso_process,
    NULL,
    iso_uninit,
    iso_methods
};

MS_FILTER_DESC_EXPORT (iso_desc)

Metoda podmiany funkcji zarządzania pamięcią

Dla bardziej szczegółowych badań, w mediastreamerze przewidziano możliwość podmiany funkcji dostępu do pamięci na własne, które oprócz podstawowej pracy będą rejestrować "Kto, gdzie i po co". Podmienia się trzy funkcje. Oto jak to się robi:

OrtpMemoryFunctions reserv;
OrtpMemoryFunctions my;

reserv.malloc_fun = ortp_malloc;
reserv.realloc_fun = ortp_realloc;
reserv.free_fun = ortp_free;

my.malloc_fun = &my_malloc;
my.realloc_fun = &my_realloc;
my.free_fun = &my_free;

ortp_set_memory_functions(&my);

Taka możliwość przydaje się w sytuacjach, gdy analizer spowalnia działanie filtrów na tyle, że zakłóca pracę systemu, w który jest wbudowany nasz schemat. W takiej sytuacji trzeba zrezygnować z analizatora i używać podmiany funkcji zarządzania pamięcią.

Rozpatrzyliśmy algorytm działania dla prostego grafu, który nie zawiera rozgałęzień. Ale to podejście można zastosować również w innych przypadkach, oczywiście z większą złożonością, ale idea pozostanie ta sama.

W następnym artykule omówimy kwestię oceny obciążenia na tickerze i sposoby radzenia sobie z nadmiernym obciążeniem obliczeniowym w mediastreamerze.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster