Limitowanie OPS

20 kwietnia 2013

Spędziłem ostatnio trochę czasu nad tematem limitowania operacji wykonywanych w określonym przedziale czasu. Sama idea zapoczątkowana została potrzebą szybkiej implementacji ograniczenia szybkości łącza, czyli typowe limitowanie transferu na potrzeby aplikacji, aby jeden program nie zajmował wszystkich dostępnych zasobów sieciowych. Nie będzie to typowe zagadnienie klasyfikowania, kolejkowania operacji lub kształtowania ruchu. Będzie to przedstawienie bardzo prostej metody, dosyć często wykorzystywanej w różnych aplikacjach.

Idea

Działanie jest bardzo proste, poniekąd podobne do typowych metod kontrolowania ruchu sieci, bazujący na odrzucaniu lub opóźnianiu pakietów. W istocie operacje zostają sztucznie opóźniane i wydłużane, aby w danej jednostce czasu nie przekroczyć wykonania określonej ich ilości.

Dla ułatwienia oprzyjmy się o sieciowy przykład. Funkcją $F$ możemy opisać przepływ danych w sieci, gdzie jej wartość od czasu - $F(t)$ - określać będzie tempo ruchu w sieci w danej chwili $t$. Ilość danych wysłanych w określonym przedziale czasu możemy przedstawić za pomocą $\int_x^y F(t) dt$, co odpowiada transmisji danych od chwili $x$ do chwili $y$. Na przykład funkcja $F_1(t)=D \delta(t-a) dt$, gdzie $\delta(t)$ jest deltą Diraca, odpowiada wysłaniu $D$ danych w chwili $a$, więc wynika z tego, że $\int_x^y F_1(t) = D$, gdy $x \le a \le y$.

Dochodząc do wniosku, chcąc ograniczyć szybkość przesyłania danych do wartości nie przekraczającej $R$ w przedziale czasu $W$, gdzie $W >= y-x$, zwanym oknem, należy spełnić następujący warunek:

$$
\int_x^y F(t) dt <= R(W)
$$

Mówiąc ludzkim językiem, w czasie W możemy wysłać maksymalnie R danych i powinniśmy tak kontrolować i zarządzać wysyłaniem danych, aby ten warunek zawsze pozostał spełniony.

Porzućmy dalej wzory i opiszmy w skrócie działanie, jakie chcemy osiągnąć i rozważmy możliwe przypadki.

Jak wiemy, sumarycznie w czasie określanym przez okno W nie możemy wysłać większej ilości danych, niż R. A zatem, każde kolejne żądanie wysłania danych, powyżej ustalonego limitu, należy opóźnić o czas pozostały do zamknięcia bieżącego okna (czyli do czasu otwarcia kolejnego). Oczywiście dotyczy to tylko sytuacji, kiedy żądanie te napłynie w tym samym oknie, w którym wykorzystano już limit transferu. W innym przypadku, przetwarzamy żądanie.

Musimy pamiętać również, że żądania mogą napływać z różną częstotliwością oraz różną ilością danych do wysłania. Gdy ilość danych w jednym zadaniu przekracza limit, należy przetworzyć tyle danych ile jeszcze można wysłać w bieżącym oknie, a następnie wstrzymać się do czasu nowego okna i tak dalej, aż całe żądanie zostanie przetworzone.

Wszystko to opisane wyżej, można byłoby w prosty i ładny sposób przedstawić na wykresie, ale niestety takowego nie będzie.

Powyższy opis oparto na przykładzie ograniczania przepustowości wysyłania danych w sieci. W istocie, sposób ten nadaje się do limitowania dowolnych operacji, więc docelowo, algorytm operuje na żetonach (tokens). Każdy żeton reprezentuje pozwolenie na wykonanie określonej ilości operacji przez funkcję w danym kwancie czasu, aby założenia limitowania zostały spełnione. Sposób, w jaki żetony interpretowane są przez funkcję, zależy od jej implementacji.

Implementacja

Biorąc pod uwagę możliwość operowania na różnych typach i funkcjach, implementacja oparta jest na szablonach. Pozwala to w pełni "przełożyć" ideę działania w uogólniony algorytm, który w prosty sposób za pomocą klas cech i wytycznych, może zostać wykorzystany do limitowania dowolnych operacji w określonej jednostce czasu.

Cała zawartość znajduje się w przestrzeni nazw lops. Sercem limitera jest klasa Limiter:

template<
	typename Token,									// token type
	typename Time,									// time type
	ProcessType Process = ProcessAll,				// process algorithm
	typename TokenTraits = TokenTraits<Token>,		// token traits - is_eof/is_error
	typename TimePolicy = NativeTimePolicy<Time>	// time policy - time/sleep
>
class Limiter { ... };

Dostarcza ona get/set-tery do wartości rate, określającej ograniczenie operacji przypadających na 1 sekundę oraz operator wywołania, który zastępuje wywołanie limitowanej funkcji.

Operator ten przyjmuje obiekt spełniający koncept callable, czyli cokolwiek co da się wywołać i zachowuje się podobnie do funkcji, może to być funkcja, funktor, lambda, o określonym prototypie:

Token func(Token token);

Funkcją ta przyjmuje wartość żetonów jaka może wykorzystać, zwraca wartość wykorzystanych żetonów.

Parametr Token określa typ używany do reprezentacji żetonów. Może to być dowolny typ, ale musi zachowywać się jak typ liczbowy - posiadać operatory matematyczne.

Klasa cech żetonów TokenTraits, dostarcza informacji o sposobie interpretacji wartości żetonów zwracanych przez funkcję użytkownika. W jakiś sposób musimy wiedzieć, interpretować jej wyniki, szczególnie w dwóch istotnych sytuacjach - końca przetwarzania danych i wystąpienia błędu (w sytuacjach, gdy funkcja nie rzuca wyjątkami). Jej definicja jest prosta:

template<typename T>
struct TokenTraits {
 
	static inline bool is_eof(T token);
	static inline bool is_error(T token);
 
};

Parametr typu Time, określa typ używany do przechowywania czasu, wraz z TimePolicy dostarcza pełnych informacji niezbędnych do kontrolowania czasem. TimePolicy implementuje dwa niezbędne do działania limitera funkcjonalności - pobieranie upływu czasu i wstrzymanie działania bieżącego wątku na określony czas. Jej koncept przedstawia się następująco:

template<typename T>
struct TimePolicy {
 
	static void sleep(T usec);
	static T time();
 
};

Znacznik czasu zwracany przez funkcję time nie musi w żaden sposób odnosić się do realnego czasu, wymagane jest jedynie to, aby na podstawie kolejnych pobrań wartości można było wyliczyć upływ czasu, korzystając ze wzoru $d_t= t_1-t_0$.

Standardowo biblioteka zawiera definicje NativeTimePolicy, wykorzystujący w implementacji systemowe funkcje i elementy, specjalizowany do 64-bitowego typu przechowywującego czas. Bezproblemowo powinna działać pod systemami Microsoftu i posixowymi.

Pozostał jeszcze argument typu enum definiujący w czasie kompilacji używany algorytm przy wywołaniu funkcji - ProcessType. Obecnie są dwie implementacje, a raczej dwa sposoby wywoływania funkcji użytkownika przez limiter, zależnie od sposobu używania oryginalnej funkcji: ProcessOne i ProcessAll.

Przypuśćmy, że użytkownik wykorzystuje funkcje write do zapisywania danych do pliku, albo wysyłania w sieć. Jej prototyp jest typowy, jak na takie zastosowanie:

int write(const char* buffer, int length);

ProcessOne ogranicza się do jednokrotnego wywołania funkcji użytkownika przez limiter, oczywiście z odpowiednią do bieżącej sytuacji wartością żetonów. Jej najczęstsze zastosowanie sprowadza się do zastąpienia kodu użytkownika, który sam zapewnia wysłanie wszystkich danych. Dla przykładowej funkcji write, kod ten przedstawiałby się następująco:

while (length) {
 
	int ret = write(buffer, length);
	if (ret == 0)
		break;
 
	buffer += ret;
	length -= ret;
}

Limitowanie zapisywania danych w powyższym kodzie, ograniczałoby się do podmiany wywołania funkcji write na obiekt limitera:

typedef limiter::Limiter<uint32_t, uint64_t> WriteLimiter;
WriteLimiter writer;
 
...
 
while (length) {
 
	int ret = writer(
		[&buffer](int token) { return write(buffer, token); },
		length
	);
	if (ret == 0)
		break;
 
	buffer += ret;
	length -= ret;
}

ProcessAll w przeciwieństwie do ProcessOne, tak długo będzie wywoływał funkcje użytkownika, dopóki nie przetworzy wszystkich danych jakie zlecił użytkownik, lub nie zwróci wartości interpretowanej jako eof.

Sposób ten idealnie nadaje się do kodu użytkownika, w którym następuje jedno, pojedyncze wywołanie funkcji write:

int ret = write(buffer, length);

wystarczy zamienić na obiekt limitera z odpowiednią funkcją callback:

typedef limiter::Limiter<uint32_t, uint64_t> WriteLimiter;
WriteLimiter writer;
 
...
 
int ret = writer(
	[&buffer](int token) -> int {
		int ret = write(buffer, token);
		buffer += ret;
		return ret;
	},
	length
);

Jeśli zerkniemy do definicji funkcji process dla algorytmu ProcessAll, zobaczymy, że znajduje się w niej podobny mechanizm, z pętlą zapewniającą przetworzenie wszystkich danych, podobnie jak to miało miejsce w kodzie użytkownika przedstawionego nieco wyżej (dla ProcessOne).

Oczywiście, w miejsce ProcessOne, możemy bez problemu zastosować ProcessAll, ale wtedy niepotrzebnie kompilator wygeneruje kod, w którym pętla zawarta w pętli de facto będą robiły to samo. Niepotrzebne, może bez sensowne dublowanie, ale będzie zadziała to poprawnie. Niemniej warto dobierać odpowiedni sposób przetwarzania dla danej sytuacji.

W istocie wybór typu paradoksalnie jest odwrotnością tego co robi kod użytkownika - gdy przetwarza wszystko w pętli, wybieramy ProcessOne, gdy wywołuje tylko raz funkcje użytkownika, wybieramy ProcessAll.

W implementacji wykorzystano popularny idom int-to-type i static call dispatch, do wyboru w czasie kompilacji odpowiedniej funkcji process na podstawie wartości ProcessType.

Całym zrządzaniem i kontrolowaniem czasem, żetonami i wywoływaniem funkcji użytkownika zajmuje się kod zawarty w funkcji do_process:

template<typename T>
Token do_process(T func, Token token) {
 
	// process window stuff
	Time now = TimePolicy::time();
	Time inv = now - m_start;
 
	if (inv >= m_interval) {
		// in new window
		m_start = now - (inv % m_interval);
		m_used = 0;
 
	} else if (m_used >= m_value) {
		// sleep rest of window time
		TimePolicy::sleep(m_interval - inv);
		m_start = TimePolicy::time();
		m_used = 0;
	}
 
	// calculate token chunk for current window
	Token chunk = m_value - m_used;
	chunk = std::min(chunk, token);
 
	// call user function
	Token ret = func(chunk);
	if ( !TokenTraits::is_error(ret) )
		m_used += ret;
 
	return ret;
}

Kod ten jest implementacją przedstawionej wyżej idei ograniczania i limitowania operacji. Cały kod można podzielić na trzy części, fragment odpowiedzialny za sterowane oknem i ewentualne wstrzymywanie działania, kod wyliczający wartość żetonów, jaka została przypisana do bieżącego wywołania i kod odpowiedzialny za sterowanie funkcją użytkownika.

Eksperymenty

Tymczasem mogę zachęcić do eksperymentów z przedstawiona wyżej implementacją, możliwe, że coś da się zrobić inaczej, lepiej, lub w jakikolwiek inny sposób uprościć, albo rozwinąć...

Pełne źródła implementacji dostępne są na moim githubie, jako gist 5425750, na licencji MIT (jeśli jakaś musi być).

Zastanawiam się jak można wykorzystać przedstawiony mechanizm do wspólnego limitowania określonej puli operacji, tak, aby cała pula objęta była tym samym limitem. Pierwsza myśl jaka przychodzi do głowy to używanie tego samego obiektu limitera dla wybranego zbioru operacji. W aplikacjach wielowątkowych pewnie będzie trzeba "oznaczyć" ciało operatora wywołania jako strefę krytyczną, aby tylko jeden watek w danym czasie mógł wykonywać operacje. Ciekawe czy są jakieś większe problemy w takim zastosowaniu. Zastanawiam się również, czy istnieje jeszcze inne proste wykorzystanie przedstawionej implementacji (może z małymi zmianami) w celu ograniczania puli tych samych operacji, pochodzących z różnych źródeł.

Dodatkowym rozwinięciem mechanizmu limitowania jest wprowadzenie adekwatnej funkcjonalności monitorującej operacje, w celu określania szybkości przetwarzania operacji. Niezależny mechanizm, mógłby być wykorzystywany np. do mierzenia transferu danych w sieci. Pomyślę o tym...

Odwracanie kolejności w SSE/AVX

5 kwietnia 2013

W ostatniej notce, o możliwościach zmiany kolejności uporządkowania bajtów i bibliotece ByteOrder, wspomniałem, że w wolnej chwili postaram pobawić się SSE i spróbować, w prosty i w miarę wydajny sposób, zaimplementować operację bswap znaną z 32/64-bitowych rejestrów na rejestrach 128-bitowych, a nawet 256-bitowych. Poniekąd udało mi się to zrobić ;) SSE2 Druga wersja strumieniowych rozszerzeń [...]

czytaj całość »

ByteOrder – kolejność bajtów

29 marca 2013

Architektury współczesnych mikroprocesorów powszechnie używają dwóch różnych metod i konwencji przechowywania danych w pamięci, zwane „kolejnością bajtów” (byte order). Niektóre komputery umieszczają najbardziej znaczący bajt w słowie jako pierwszy (big-endian), a inne jako ostatni (little-endian). Przez większość czasu, kolejność bajtów może być ignorowana, programista nie musi się martwić o to, jaki format jest używany, ale [...]

czytaj całość »

Wykrywanie hardlinków

13 marca 2013

Hardlinki, inaczej łącza stałe lub dowiązania twarde są alternatywnymi nazwami dla pliku, referencjami wskazującymi istniejący wcześniej obiekt. Niestety nie istnieją żadne metody pozwalające określić, które nazwa lub referencją jest oryginalna, a która dodana później. Wszystkie nazwy/referencje wskazują ten sam blok danych. Dosyć dawno temu starałem się znaleźć prosty sposób na wykrywanie łączy stałych oraz ich [...]

czytaj całość »

Wielowątkowość w Bashu

22 lutego 2013

Jakiś czas temu pracowałem nad kilkoma agentami/serwisami w bashu – prostymi (choć może nie tak bardzo) skryptami shellowymi, przetwarzającymi sukcesywnie dane oraz wykonywujące pewne określone operacje w miarę napływania do nich zadań. Chcąc usprawnić i przyspieszyć processing, eksperymentowałem i szukałem bardzo prostego sposobu na zrównoleglenie kilku procesów, dokonywujących podobne zadania… Bash sam w sobie nie [...]

czytaj całość »

Praca magisterska

3 lutego 2013

Trochę dawno to już było, ale do tej pory nie miałem okazji poruszenia publicznie tego tematu. Tak się składa, że ponad 1,5 roku temu, w czerwcu 2011 roku, na Wojskowej Akademii Technicznej, po ciężkiej 3 tygodniowej pracy redagowania i twórczego tworzenia, obroniłem tytuł magistra. Gdybym nie był na cywilnych, byłbym teraz oficerem – podporucznik magister [...]

czytaj całość »

MPU: klasy cech kontenerów STL

18 stycznia 2013

Kolejna notatka z serii MPU, opisująca kilka ciekawych konstrukcji zawartych w mistycznym MetaPrograming-Unit. Tym razem o klasach cech opisujących typy kontenerów zawartych w STL. Każdy programista C++ zaznajomiony jest z klasami cech (traits) i ich wielkim potencjałem. Dla tych wszystkich, którzy nie do końca łapią temat, dwa zdania wprowadzające. Klasy cech w C++ są specyficznym [...]

czytaj całość »

Thread.js

8 stycznia 2013

Zgodnie z zapowiedziami, kontynuacja tematu z ostatniej notki, w której przedstawiałem sposoby umożliwiające w pewnym stopniu na emulacje środowiska wielowątkowego w JS. Teraz, jak obiecałem, nadszedł czas na przedstawienie mojej implementacji, prostej biblioteki umożliwiającej w bardzo prosty sposób emulować wielowątkowość. Wprowadzenie Wspominałem w poprzedniej notatce, że sam problem zawieszania się i blokowania przeglądarki, przez długo [...]

czytaj całość »

Wielowątkowość w JavaScript

7 stycznia 2013

JavaScript nie posiada wielowątkowości, we wszystkich przeglądarkach (z wyjątkiem Chrome), kod JS wykonywany jest w jednym wątku. Niejeden webdeveloepr „naciął się” na zamrożenie przeglądarki (lub ostrzeżenie w Firefoksie) w czasie wykonywania intensywnego kodu zajmującego zasoby. W takich wypadkach JS blokuje przeglądarkę, podobnie aktualizacje interfejsu użytkownika i zawartość strony, do czasu zakończenia wykonywania bieżącej operacji, co [...]

czytaj całość »

Przyszłość komunikacji sieciowej

23 grudnia 2012

Jednym z impulsów do napisania tej notatki można znaleźć w publikacji jaka pojawiła się na stronie głównej jednego z niegdyś (ponoć) dobrego multikomuniakatora, zbliżonego w swej idei do Mirandy – mowa tutaj o konnekcie. Wraz z końcem świata, czyli kilka dni temu, oficjalnie po kilku latach projekt został zamknięty, a źródła otwarte. Myślę, że jest [...]

czytaj całość »

miniBlog

malcom.pinger.pl
00:04:03 08/01/2013
Taki tu spokoj.... sialala...
23:37:56 16/09/2012
A gdzie C64?... :(
http://www.youtube.com/watch?&v=3qaF9-W2Dvg
14:45:25 15/09/2012
Moze czas znalezc jakies inne miejsce na mini-bloga? A moze dodac cos do samego devbloga... a moze czas na blipa, twittera? Bo cos tutaj zycie zamarlo...
20:00:55 01/09/2012
Powrot w blasku i chwale na MaldevBlogu ;)
11:54:50 02/06/2010
Wreszcie kilka dni odpoczynku w domku ;)
22:15:23 07/03/2010
Powrot do zycia ;)
19:32:00 04/01/2010
first day on a new job ;)
18:33:32 14/12/2009
Popularnosc jezykow programowania
http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html
17:54:43 02/12/2009
Moge powiedziec poki co tylko tyle ze intro i muzyka z #Paradox mi sie wybitnie podoba ;p
01:35:01 02/12/2009
Lin Xu przedstawia nowe usprawnienia optymalizacyjne w VC2k10:
http://blogs.msdn.com/vcblog/archive/2009/12/01/gl-and-pgo.aspx
13:35:46 01/12/2009
Secret #Perl Operators
http://www.catonmat.net/blog/secret-perl-operators/
15:58:58 30/11/2009
Interlocked* Win32 - ciekawe zadanie dla chetnych ;p
http://groups.google.pl/group/pl.comp.lang.c/browse_thread/thread/c4ccf09a972ed50d
11:23:06 26/11/2009
W ostatnich dniach troche moich patchy do wxWidgets sie "przeforsowalo"...
23:00:04 22/11/2009
Troche sie rozczarowalem, mialem nadzieje, ze bedzie kontynuacja watku z ostatniego odcinka Stargate Universe...
22:14:56 21/11/2009
Pwtornie motyw "Polscy chłopcy w językach obcych" w Strefa rokendrola wolna od angola na "trocjce" ;)
14:35:39 17/11/2009
"People often write less readable code because they think it will produce faster code. Unfortunately, in most cases, the code will not be faster."
Krotka prezentacja o tym co potrafia wspolczesne kompilatory (odnosnie optymalizacji):
http://www.linux-kongress.org/2009/slides/compiler_survey_felix_von_leitner.pdf
01:10:43 17/11/2009
The Pirate Movie - swietny film z swietnym soundtrackiem, szczegolnie utwor (Song of) Victory.
Dla odmiany nieco metalowa wersja, cover Mayhayrona
http://www.youtube.com/watch?v=Ud8uAKW3U4o
00:11:46 08/11/2009
Bezstykowe karty Mifare sa bardzo wygodne w uzyciu. Gorzej jak ma sie ich kilka np. w portfelu (legitymacja, karta miejska) i chce sie jednej uzyc bez wyjmowania... co ostatnio doswiadczylem przy bramkach na WAT-cie. Zapewne przy metrze i kasownikach bedzie podobnie ;p
01:14:00 06/11/2009
Dziwny swiat, dziwni ludzie, teraz beda dublowoac wszelkie swoje "smieci" w sieci.
Moze warto zintegrowac rowniez wiekszosc for dyskusyjnych i wysylajac na jedno, wysylac na pol sieci?
23:10:46 20/10/2009
Ostatnio babralem sie z COM i XPCOM przy embedowaniu #IE i #Gecko do prostej aplikacji. Moze w xime oprze API na czyms podobnym... Latwe i przyjemne uzywanie spod C++ i C, czyli zewszad ;)
20:48:44 13/10/2009
Musialem napisac co mysle o nowym #tlen, bo nie potrafilem dluzej sie powstrzymac, przepraszam.
http://ekipa.tlen.pl/forum/index.php?showtopic=11310
10:38:53 12/10/2009
Kolumna "This Week in Boost" na "C++Next" mam nadzieje, ze bedzie ciekawa ;p
http://cpp-next.com/archive/2009/10/introducing-%e2%80%9cthis-week-in-boost%e2%80%9d/
13:53:47 06/10/2009
Polskie brzmienia w lacisnkim rocku - Amaryllis ;)
Mi szczegolnie przypadl do gustu utwor "Magnificat".
12:03:14 06/10/2009
Podazajac kierunkiem ostatniej wolnej strefy - "Polscy chłopcy w językach obcych", dzisiejsze poludnie sponsoruje Percival Schuttenbach ;)
http://www.youtube.com/watch?v=uQvvE5qNxi4
http://www.youtube.com/watch?v=Gb36Qyq8DyY
09:15:37 06/10/2009
Paskudny ten #tlen na #qt, kontrolki takie bleee, emulwoane/malowane nijak maja sie do natywnych.
18:22:56 29/09/2009
s2s @ #tlen jest online [via kaworu]
19:50:34 25/09/2009
Maly bug w wx'ach, a juz chcialem klnac na MS...
13:26:02 23/09/2009
Dla tych, ktorzy wciaz nie wiedza, ze jabber != xmpp ;p
http://forum.jabberpl.org/index.php?showtopic=7380
23:05:32 22/09/2009
Na USiu tez mnie "chca", ale zostane przy WAT, tylko 2x drozej ;p
21:43:24 20/09/2009
Szybka konwersja znakow konca linii win<>unix, bez zaprzegania perla, awka i innych kobyl, via sed:
sed -e 's/\r//' -i plik
sed -e 's/$/\r/ -i plik