file opened
Z dziennika architekta: duże pliki, Swarm i NFS
Z dziennika architekta: duże pliki, Swarm i NFS
Tym razem, ze względu na przygotowania do prowadzenia szkolenia z Grafany, chciałem pobrać obraz Windows Server 2022 ze swojego systemu. Nie potrzebuję samego Windows Servera do szkolenia, ale uznałem, że moja robocza maszyna z Windowsem nie musi być 10-tką czy 11-tką (11-tki nie lubię jak nie wiem co), a mam pod ręką obraz Windows Server 2022. No, to czemu by nie? Do podpięcia się po jakimś VPN, skompilowania czegoś czy przetestowania paru skryptów się nada.
Tylko że tu pojawił się problem, bo „pod ręką” oznaczało, że jest na moim serwerze, w Pika Cloud. I o ile malutkie pliki (kB czy MB) nie powodowały żadnego problemu, o tyle próba pobrania już 1,5 GB, wiedziałem, że stanowi problem. Natomiast obraz Windows Server 2022 ma 5,2 GB. Co mogło pójść nie tak?
A mianowicie — co było nie tak?
Do tej pory system dosłownie przekazywał stream bezpośrednio z klienta S3. Oczywiście, nie dokładnie tamten stream, a kopię. Innymi słowy — pobierając cokolwiek, i tak szło to około 2x do pamięci: najpierw MinioClient musiał to zbuforować, a potem jeszcze mój klient.
Przy małych i średnich plikach to nie problem, przy większych zaczynamy odczuwać, co jest nie tak: zużycie RAM leci do góry, I/O się poci. Szczególnie jeśli setup dysków jest mieszany (SSD, HDD) i wszystko nadal leci na PCI-e 3.0.
Ostatecznie jednak dało się coś tak pobrać i miało to swoje niebywałe zalety: direct linki streamowalne z range processingiem. Jednak, jak widać gołym okiem, to rozwiązanie nie było przeznaczone do jakkolwiek zoptymalizowanego pobierania plików.
Skąd nagły refactor i jak to ma działać
Od dłuższego czasu planowałem zmianę wskazanego wyżej flow wraz z tym, że planowany będzie dodatkowy komponent systemu działający jako player. Infrastrukturalnie będzie to oparte o ffmpeg i MediaMTX. Z tego też powodu i tak potrzeba będzie buforować ponownie ten plik, aby pozwolić MediaMTX czy innemu bliźniaczemu rozwiązaniu na playback.
Stąd powstał w mojej głowie pomysł: no to dawaj, zrobimy workera działającego w tle, buforującego plik w wyznaczonym folderze. Tymczasowo jest to zwykły /tmp wewnątrz kontenera, ale docelowo chcę wyznaczyć prywatny katalog, na ścisłych uprawnieniach, też widoczny jako TempPath dla .NET, a także zacieśnić politykę TTL buforowania takiego pliku na dysku dla tych, które są zaszyfrowane w trakcie przechowywania.
Działanie zostało zmienione tak, że nadal system operuje na streamach, ale MinioClient automatycznie zapisuje stream na dysk (opcja .WithFile()), a następnie biorę tę tymczasową ścieżkę, otwierany jest stream do tego pliku i to dopiero jest zwracane do warstwy wyżej. Ze względu na to, że już sam MinioClient zapisuje do pliku, nie tworzymy też żadnego duplikatu.
System wpierw wykonuje buforowanie i plik pobrany z S3 (jego ścieżkę) zwraca dla metod warstwy wyżej, które użytkują ją, by czy to zwrócić go jako stream, czy jako surowy tekst.
Wszystkie powiązane funkcjonalności przeszły na ten flow, także system notatek. System jest wyposażony w konfigurowalny TTL przechowywania zbuforowanego pliku, a w planie jest implementacja mechanizmu mapowania plików buforowanych, aby nie duplikować ich, jeśli dany plik jeszcze jest w buforze.
Mechanizm share linków również został przerzucony i poprawiony pod względem UX — teraz link kieruje na domenę front-endu, nie na API, a po załadowaniu się UI systemu otwiera się panel pobierania i rozpoczyna się proces buforowania, a następnie pobierania pliku.
Aktualnie pobieranie dzieje się w tle i widoczne jest wyłącznie jako proces, który nie ma paska postępu (zostanie to skorygowane w oparciu o stopień pobrania pliku na dysk komputera klienckiego).
Odpowiadając natomiast na pytanie z tytułu, tj. skąd ten pomysł, odpowiedź jest prosta: bo mogłem. A ponieważ i tak planowałem to zrobić i miałem wcześniej draft tego rozwiązania, a z poziomu infrastruktury i tak planowałem dorzucać dysk pod dokładnie ten use case, uznałem, że nie ma na co czekać.
Warto już teraz przenieść system na to, wykorzystywać I/O i powierzchnię dysków bardziej, a proces na tyle, na ile się da, wypchnąć w tło, aby było to przyjemniejsze dla użytkownika.
Tutaj pewnie zadajesz sobie pytanie: no dobra, a co z plikiem tymczasowym, który jest bazą dla zwróconych danych do użytkownika? Dobre pytanie.
W momencie uruchamiania się workera buforującego plik planowany jest również worker, który odpala się po co najmniej 30 minutach, a w moim wypadku po 240 minutach, i czyści powiązany z danym buforowaniem plik.
Dodatkowo, w razie gdyby worker buforujący złapał wyjątek, również próbuje usunąć plik, zanim skończy swoją pracę. To samo się wstępnie tyczy dowolnego błędu przy operacjach kryptograficznych, tj. jeśli dojdzie do błędu w trakcie odszyfrowywania, dostępu do kluczy czy czegoś podobnego — system usuwa plik natychmiastowo.
Jest jeszcze kilka kwestii do skorygowania, jak wspomniałem w trakcie, jednak można uznać, że MVP tej funkcjonalności zostało wdrożone.
Wszystko jest cudownie, ale…
Pobieranie pliku na dysk ma jeden mankament. Dysk jest fizyczny, więc plik jest „przywiązany” do hosta, który obsługuje żądanie. O ile używam Apache jako proxy, które ma włączony balancer i dodaną stickiness sesji, o tyle Swarm już nie respektuje tego mechanizmu zgodnie z 12 Factor App manifest. Aplikacja powinna być bezstanowa i niezależna od podległego hosta.
Jednym z lepszych podejść byłoby po prostu route’owanie na twardo, ze stickiness dodanym przez Apache, na usługę, która jest tylko do buforowania i pobierania pliku. Można by wtedy całkiem pominąć Swarm lub postawić ją w trybie host.
Na ten moment jednak nie zdecydowałem się na separację całej logiki do odrębnego serwisu. Wolałem pozostawić to w ramach Pika Core, który jednocześnie nie jest dostosowany do uruchamiania tylko w tym trybie, więc zużywałby nadmiernie zasoby.
Niekoniecznie najszybszym z perspektywy czasu oczekiwania, ale najszybszym z perspektywy „dowiezienia” rozwiązaniem było wystawienie /tmp (które i tak używane jest tylko wewnątrz kontenera przez moją apkę) na katalogi na hoście, które mają dość ścisłe uprawnienia, i spięcie ich połączeniem NFS.
Oczywiście firewall został skonfigurowany tak, by NFS był dostępny tylko dla drugiego serwera, a nie dla pozostałych hostów w sieci. Docelowo trzeba będzie nadać uprawnienia tylko konkretnej grupie, tak by tylko moja apka i serwer NFS mogły tam zapisywać, a sam NFS zostanie zapewne zastąpiony sshfs.
Jest to stadium pośrednie stworzone pod MVP tej funkcjonalności. Jeśli zaś chodzi o wydajność, oczywiście ta nie powala (jak napisałem wyżej), ale nie jest i tak źle, a po dołożeniu w miarę szybkich dysków tylko pod taki bufor plików sama sieć LAN jest stosunkowo szybka jak na moje potrzeby. Natomiast na to, jaki ktoś ma Internet i jak po drodze jest to route’owane przez sieć, nie mam i tak już wpływu.
No, a czy pobrałem ten plik?
Ostatecznie się udało. Trwało to przy aktualnym Internecie około 30–40 minut, czyli w sumie tyle, ile byśmy oczekiwali, mając po WiFi 100 Mbps dzielone między kilka osób. Pobrałem jednak ten obraz, więc mogę na spokojnie postawić sobie roboczą maszynę z Windows Server 2022.