Za procesami typu Worker (w systemie rummager) stoi prosta idea – sprawdź, zapisz wynik, sprawdź następny, itd… Na stronie rummager/worker umieściłem ogólny opis tych procesów. Prosty nie znaczy jednak nieciekawy :) Jest tutaj kilka interesujących kwestii do przemyślenia.
Pierwsza rzecz to sposób pracy samego procesu Worker – czy nie mógłby być procesem samodzielnym, a nie uruchamianym w wątku przez Worker_Managera? Oczywiście tak, problem w tym, że uruchamiany „oddzielnie”, każdy Worker będzie alokował w systemie pamięć na interpreter Pythona, co oznacza około 100 MB (przy aktualnie importowanych przez proces bibliotekach) – czyli dwa Workery będą wymagały 200 MB pamięci Ram do działania. To trochę dużo szczególnie jeżeli korzystamy z jakiś platform PaaS (jak np.: OpenShift), gdzie wielkość pamięci Ram, przy planie wolnym od opłat, dostępnej dla programu to około 256 MB. Na takiej platformie, przy wykorzystaniu podejścia każdy-Worker-to-osobny-proces będziemy mogli uruchomić maksymalnie dwa takie procesy. Wykorzystanie wątkowania daje tutaj dużo lepsze wyniki. Proces główny – w tym przypadku Worker_Manager - pochłania ciągle 100 MB, jednak każdy z uruchamianych wątków (procesy Worker) to już dodatkowo tylko około 10 MB – czyli dysponując 256 MB Ram możemy takich wątków uruchomić około 15! To znaczny wzrost :) W rzeczywistości zajętość pamięci jest trochę niższa – zaokrągliłem dla wygody – i pozwala na uruchomienie około 4 czy 5 oddzielnych Workerów bądź około 50 "wątkowanych" przy 256 MB Ram (czy nawet więcej, aktualnie jednak nie testowałem większej ilości ze względu na ograniczone możliwości przyjmowania danych przez DB, a sama maksymalna liczba możliwych do uruchomienie wątków, do tej chwili, jakoś nie specjalnie mnie interesowała – czyli to jeszcze jest do sprawdzenia :)) – to duża różnica i dlatego opcja z wątkowaniem procesów Worker wydaje się dużo lepszym rozwiązaniem niż uruchamianie każdego z nich jako oddzielny proces.
Teraz łyżka dziegciu. Wątkowanie w pythonie przy wykorzystaniu „standardowego” interpretera - CPython - nie ma „dobrej prasy” – wszystkiemu winien GIL – Global Interpreter Lock – mutex, którego zadaniem jest ograniczyć dostęp do interpretera tylko dla jednego wątku w danej chwili, inne wątki muszą po prostu czekać na swoją kolej. To wygląda dość „groźnie” i wydaje się mocno ograniczać korzyści wynikające z wątkowania procesów. W przypadku projektu Rummager i wykorzystywanych w nim wątków Workerów wygląda jednak, że ograniczenie to nie ma specjalnego znaczenia. Na razie brak dokładnych statystyk, będę się temu zagadnieniu jeszcze przyglądał, ale nie obserwuję żadnych negatywnych skutków w rodzaju zmniejszenia wydajności wątków w zależności od ich uruchomionej ilości. To może wynikać zresztą z pewnej specyfiki pracy wątków typu Worker_SMTP (aktualnie jedyny typ Workera) – proces Worker_SMTP nawiązuje kontakt z serwerem i, w przypadku braku natychmiastowej odpowiedzi, odczekuje około 6 sekund do „timeoutowania” połączenia – te 6 sekund może być wystarczającą luką dla wykonania przeliczeń wymaganych przez inne wątki – jednak to tylko moje gdybania. Tak jak napisałem postaram się temu jeszcze przyjrzeć. Dodam tylko jeszcze, iż GIL występuje tylko w CPythonie, korzystając z JPythona (interpretera wykorzystującego JVM – wirtualną maszynę Javy), czy IronPythona (tutaj wykorzystana została platforma .NET) nie musimy się nim przejmować.
Być może mechanizmem alternatywnym w stosunku do wątków byłoby wykorzystanie asynchroniczności – to temat do ewentualnego zbadania, na razie, mimo iż teoretycznie potrafię sobie wyobrazić jak taka asynchroniczna wersja Worker_Managera mogłaby działać (a przynajmniej tak mi się wydaje), to jednak brakuje mi praktyki w implementacji takich rozwiązań. Wygląda ciekawie i na pewno muszę spróbować i sprawdzić w wolnym czasie takie rozwiązanie w praktyce!
Asynchroniczność to jednak temat z mniejszym priorytetem niż protokół komunikacyjny Worker - Server. Okazuje się bowiem, że aktualna komunikacja (SOAP) jest niesamowicie transfero-żerna – przy działających 10-ciu Workerach miesięczny transfer wykazywany przez usługodawcę hostującego część serwerową aplikacji to około 50 GB! (Część "serwerowa" nie została jeszcze opublikowana, ale mam nadzieję uczynić to już niedługo). Taki transfer to koszty - w większości usługodawcy ograniczają miesięczny transfer, przynajmniej dla najtańszych pakietów hostingowych – i trzeba coś z tym szybko zrobić. Duża transfero-żerność SOAPa jest jednym z jego znanych minusów i alternatywa wydaje się oczywista – JSON. Tak więc przygotowanie Modelu operującego formatem JSON przy komunikacji z serwerem to aktualnie priorytet.
Myślę, że po rozwiązaniu problemu z transferem (implementacji formatu JSON) przyjdzie czas na kolejne typy Workerów – aktualnie wyszukiwane są serwery SMTP, trzeba rozpocząć sprawdzanie które z nich są źle skonfigurowane (open relay) i być może dodać wyszukiwanie innych usług – możliwości jest wiele, nie czynię jednak żadnych konkretnych planów – to tylko zabawa, samo rozwiązywanie bieżących problemów (transfer i sposoby komunikacji, optymalizacja wykorzystania pamięci, wielowątkowość/asynchroniczność) to już sporo nauki i wyzwań, choć oczywiście opracowanie kompletnego rozwiązania jest bardzo kuszące – oby tylko starczyło czasu i sił! :)