Konteneryzacja, czyli jak cenny jest czas
Branża telekomunikacyjna wydaje się odstawać od mainstreamowych technologii programistycznych, wytwarzając jednocześnie “technologie telekomunikacji przyszłości”. Przykładem może być tutaj wirtualizacja, która od wielu lat obecna wśród gigantów IT takich jak Google czy Amazon, zaczęła pojawiać się dopiero u operatorów telekomunikacyjnych od niedawna. Dlaczego?
Ma to głębokie uzasadnienie powiązane z czasem, a dokładniej z wysoką dostępnością usług, czyli tzw. carrier-grade systems o dostępności rzędu 99.999% (ok. 5 minut przerwy w ciągu roku!). Nie bierze się to znikąd. Wszystkie usterki z każdą sekundą nieobsłużonego ruchu w godzinach szczytu, mogą powodować gigantyczne straty dla operatora komórkowego. Dlatego technologie wykorzystywane przez producentów z tej branży, muszą być przede wszystkim uznawane za stabilne.
Migracja z maszyn wirtualnych w labie 5G w kierunku kontenerów może oznaczać zatem, że rozwiązanie to nabrało cech dojrzałości dla branży telefonii mobilnej. Co więcej, klienci jakimi są operatorzy sektora telco, już od pewnego czasu używają w swoich serwerowniach popularnych środowisk kontenerowych jak Kubernetes. Stąd też wymuszenie zmiany w architekturze 5/6G – aby podążyć z duchem czasu. Nie jest to oczywiście jedyny powód użycia kontenerów.
Omawiany problem awarii wiąże się często z szybkością przywrócenia funkcjonalności przez restart systemu. I tutaj pojawia się pierwsza wyższość kontenerów nad maszynami wirtualnymi. Czas uruchomienia maszyny wirtualnej, a więc systemu operacyjnego wraz z aplikacjami, może być nawet o rząd wielkości dłuższy od takiej samej aplikacji opartej o kontenery. Z prostego powodu, brak dodatkowej warstwy abstrakcji hypervisora i przede wszystkim konieczności startu systemu operacyjnego. Problem rośnie jeszcze bardziej, gdy zdamy sobie sprawę, że w standardzie 5G, tzw. Network Services (przykład NS to gNB), realizowane jest przez szereg maszyn wirtualnych nazywanych VNF (ang. Virtual Network Function).
Nawet zakładając równoległe uruchamianie instancji VNF, start usługi może trwać wyjątkowo długo z uwagi na interakcje pomiędzy samymi funkcjami, a także wewnątrz infrastruktury wirtualizacyjnej samego środowiska chmury. Przesiadka na kontenery ma zatem rozwiązać problem czasu startu systemu.
Kolejnym istotnym czynnikiem jest czas potrzebny na cykl życia kodu. Użycie kontenerów, przede wszystkim ułatwia testowanie oprogramowania przez deweloperów. Dzięki małemu zużyciu zasobów w porównaniu do maszyn wirtualnych, kontenery mogą posłużyć nawet do testów integracyjnych na maszynie deweloperskiej. Nie wspominając o testowaniu zmian kodu na żywo, pisząc jednocześnie w swoim ulubionym IDE – po prostu bajka.
W niniejszym artykule posłużyłem się pythonowym frameworkiem Nameko do tworzenia microservices. Benchmark składał się z dwóch usług (service oraz http) oraz serwera RabbitMQ jako implementacji protokołu AMQP służącego do komunikacji pomiędzy usługami.
Idea testu jest w miarę prosta – klient za pomocą RESTowego API, odpytuje się usługi http, która korzysta z metody usługi service, aby otrzymać wynik.
Przykładowe użycie testowanego REST API
# curl -s -m 1 http://localhost:8000/greeting/world {‘greeting’: ‘Hello, world!’}
Komunikacja RPC pomiędzy usługami http oraz service odbywa się za pomocą AMQP (RabbitMQ). Poniżej listingi kodu opisanych usług.
http.py
import json import eventlet from eventlet.timeout import Timeout eventlet.monkey_patch() from nameko.rpc import RpcProxy from nameko.web.handlers import http class HttpService: name = "http_service" greeting_rpc = RpcProxy('greeting_service') @http('GET', '/greeting/<value>') def greeting(self, request, value): greeting = None with Timeout(0.5, False): greeting = self.greeting_rpc.hello(value) if greeting: return json.dumps({'greeting': greeting}) return 504, json.dumps({'error': 'Timeout'})
service.py
import eventlet eventlet.monkey_patch() from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): return "Hello, {}!".format(name)
Czas zmierzony pomiędzy wystartowaniem usług a poprawną odpowiedzią na zapytanie za pomocą REST API usługi http, został zamieszczony w tabelce poniżej. Pomiary zostały dokonane na tym samym serwerze wielokrotnie dla zmniejszenia błędu pomiaru.
W poszczególnych kolumnach KVM oznaczających użyty hypervisor widać wpływ zwiększenia ilości pamięci oraz procesorów dla serwera z RabbitMQ, ale tylko względem pierwszej opcji. Dalsze zwiększanie zasobów nie miało sensu, jak można zauważyć w ostatniej kolumnie. Zgodnie z oczekiwaniami dodawanie zasobów dla prostych serwisów napisanych w Pythonie nie zmieniało uzyskanych wyników. Zabieg wykonania dodatkowych pomiarów dla KVM jest celowy, aby wyeliminować fakt braku limitów użytych rdzeni procesora i pamięci dla porównywanych kontenerów. Jak widać oczywistym leaderem jest środowisko Dockera, z prawie dwukrotnie lepszym czasem uruchomienia aplikacji. Co istotne, podobne limity zasobów nie miały wpływu na wyniki kontenerów, stąd tylko jedna kolumna dotycząca Dockera.
W powyższym przykładzie dotyczącym porównania czasu uruchomienia serwisu, stworzenie kontenera wraz z serwisem jest zdecydowanie mniej kłopotliwe. Architektura, którą udostępnia środowisko Docker, z założenia jest przygotowana do wsparcia dewelopera w procesie tworzenia paczki z aplikacją w postaci kontenera. W przypadku maszyny wirtualnej całą “zabawę” trzeba wykonać praktycznie wewnątrz systemu gościa, który może nie być optymalnym miejscem do tego (kopiowanie danych z zewnątrz, brak ulubionych edytorów tekstu w danym systemie, etc.). Często trzeba dorzucić własne mechanizmy uruchamiające aplikację (w moim przypadku użyłem supervisord).
Notabene, aby wyniki były porównywalne, stworzyłem do testów maszynę wirtualną opartą o dystrybucję Alpine, tą samą, która użyta była w testowanych kontenerach, co też wymagało pogłębienia wiedzy o tej dystrybucji.
Wydłużenie czasu startu maszyny
Co jest równie istotne, to czas tuningu maszyny wirtualnej jest zdecydowanie dłuższy niż środowiska Docker, w którym standardowo proces otrzymuje praktycznie pełne zasoby systemu tzn. pamięć i procesor. A brak odpowiedniego tuningu może spowodować kilku lub kilkunastokrotnie wydłużenie czasu startu maszyny. Oprócz wyników z tabeli przykładem może być zaobserwowane przeze mnie oczekiwanie przy starcie systemu VM na “wypełnienie” generatora liczb pseudolosowych odpowiednią entropią. Zatrzymywało start systemu na kilka dobrych minut. W moim przypadku pomogło zainstalowanie demona rngd.
Istotny jest też wpływ na architekturę systemu. Lekkie opakowanie aplikacji, jakim są kontenery, sprzyja dążeniu w kierunku microservices. Jak każde rozwiązanie, nie jest to panaceum na całe zło związane z utrzymaniem kodu złożonych systemów, ale daje istotne możliwości na zmniejszenie złożoności. Tym samym cykl związany z poprawą kodu czy wprowadzeniem nowej zmiany ulega kolejnemu skróceniu.
Kolejnym tematem jest czas instalacji i powiązana z nim “waga” opakowania kodu aplikacji. W przypadku kontenerów jest ona zdecydowanie mniejsza poprzez zastosowanie warstw. Innymi słowy, gdy kontenery współdzielą warstwy, to konieczny będzie jedynie transfer różnych warstw. Zwykle jest to kod samej aplikacji, który jest “lekki” w porównaniu do zastosowanych bibliotek, interpreterów, etc. Dodatkowo brak koniecznych dodatkowych aplikacji/usług koniecznych do startu systemu w maszynie wirtualnej, przekłada się na szybszą instalację nowszej wersji oprogramowania opartej o kontenery. Może to być niebagatelna różnica, gdy weźmie się pod uwagę, że operatorzy komórkowi posiadają zwykle po kilkadziesiąt tysięcy stacji bazowych, a liczba ta ma tendencję wzrostową.
W przypadku rozwiązań telekomunikacyjnych bardzo istotną sprawą jest synchronizacja czasu pomiędzy elementami systemu. I tutaj kontenery pokazują kolejną swoją zaletę. Otóż zegary maszyn wirtualnych wykazują wredną cechę – niezależność od zegara hosta. Powoduje to konieczność synchronizacji czasu np. za pomocą klienta NTP na każdej maszynie wirtualnej w chmurze (a to kolejna aplikacja do startu w VNF…). W przypadku kontenerów nie ma tego problemu, gdyż używany jest zegar hosta.
Co gorsza, w zależności od miejsca w systemie telefonii komórkowej dokładność synchronizacji w standardzie NTP może być zdecydowanie (mam na myśli parę rzędów wielkości) niewystarczająca. Z odsieczą przychodzi protokół PTP, który bazuje na warstwie Ethernetowej. Tu ponownie pełna wirtualizacja stanowi problem w postaci braku bezpośredniego dostępu do sprzętu (karty sieciowej) i opóźnienia wprowadzane przez hypervisora, które są porównywalne do precyzji, jaką chcemy osiągnąć. Można problem próbować usunąć za pomocą modułu kernela PTP KVM, uzyskiwana dokładność niestety może być dalej niewystarczająca.
Wady kontenerów
Jakie zatem są wady kontenerów? Szczerze trudno mi takie wypisać tutaj. Bezpieczeństwo? W dobie wykrytych błędów w procesorach takich jak Meltdown czy Spectre, maszyny wirtualne wcale nie wydają się o wiele bezpieczniejsze niż parawirtualizacja Dockera. Poza tym szybki przegląd na nvd.nist.gov wskaże błędy bezpieczeństwa we wszystkich popularnych hypervisorach:
Dostęp do sieciowych zasobów dyskowych? Faktycznie OpenStack wydaje się mieć tę funkcjonalność wbudowaną w swoją strukturę w stosunku do Dockera, ale właśnie… Można uruchamiać przecież środowisko kontenerów w OpenStack’u (za pomocą Magnum na maszynach wirtualnych lub fizycznych, tzw. bare metal) i dostać te możliwości niejako “gratis”.
Uważny czytelnik zauważy, że coś tutaj nie gra. Jak to? Kontenery na VM’kach? Gdzie tutaj zysk? Wtedy jeszcze bardziej uważny czytelnik, pomoże mu, przypominając o zysku czasowym w uruchamianiu wielu “lekkich” kontenerów, w porównaniu do takiej samej liczby maszyn wirtualnych. Nie mówiąc już o większym apetycie na zasoby każdej takiej wirtualnej maszyny.
Podsumowując, głównym powodem zmian w otaczającym nas świecie jest po raz kolejny czas. W tym konkretnym przypadku jest to czas na kontenery.
Zdjęcie główne artykułu pochodzi z unsplash.com.