Ukryty koszt granic mikroserwisów: pięcioletnia retrospekcja
W 2021 roku narysowaliśmy 47 pudełek na tablicy. Liczba pochodziła z instytutu badań z dupy: nikt niczego nie mierzył, po prostu wydało nam się, że 47 to mniej więcej tyle ile trzeba na rozmiar domeny. W 2024 roku 22 z tych pudełek były z powrotem wewnątrz innych pudełek. To nie jest historia o tym, że mikroserwisy są złe. To historia o tym, że granica między dwoma serwisami jest najdroższą rzeczą w systemie do zmiany, i że tego jeszcze nie wiedzieliśmy.
Jedyne pytanie, które teraz zadaję przed narysowaniem linii: "jaka jest najmniejsza zmiana, która musi przekraczać tę granicę, i jak często będziemy ją robić?" Jeśli odpowiedź brzmi "co sprint, w lockstepie", tej linii nie powinno być. Nie ma architektury wystarczająco sprytnej, żeby tę linię uczynić tanią.
Co myśleliśmy, że kupujemy
Mieliśmy monolit. Deployował się wolno, testował wolno, własność jednego zespołu, obawiano się go wszędzie. Pitch dla mikroserwisów brzmiał: niezależność, zespoły mogą deployować swoje serwisy we własnym rytmie, własnym językiem, własną bazą danych. Overhead koordynacji monolitu miał zniknąć.
Przez pierwsze 18 miesięcy działało. Zespoły poruszały się szybciej. Serwisy deployowały bez koordynacji. Burden on-calla się rozłożył. Promień incydentu się zmniejszył.
Potem zaczęliśmy budować featury.
Framework kosztu granicy
Każda granica między serwisami ma koszt. Nie jest stały, skaluje się wraz z tym, jak często trzeba zmieniać obie strony jednocześnie.
# framework kosztu granicy, na odwrocie serwetki
cost(granica) =
f_change * cost_per_change
+ f_failure * blast_radius
- autonomy_gained
# jeśli dominuje pierwszy składnik, granica jest w złym miejscu.
# jeśli dominuje drugi — granica jest OK, inwestuj w izolację awarii.
# jeśli dominuje trzeci — shippuj.
f_change to częstotliwość skoordynowanych zmian przez granicę. cost_per_change to overhead: wersjonowanie, contract testy, koordynacja deployów, osobne kolejki PR. f_failure to jak często awaria jednego serwisu dotyka drugiego. blast_radius to skala tej awarii. autonomy_gained to faktyczna niezależność na poziomie zespołu, którą granica umożliwia.
W naszym przypadku 22 z 47 serwisów miało f_change > 1 per sprint i autonomy_gained ≈ 0, bo należały do tego samego zespołu. Granica dodawała overhead bez żadnej korzyści. Z dupy driven development w czystej postaci: architektura wyglądała świetnie na slajdzie konferencyjnym i kosztowała nas spotkanie koordynacyjne deployów każdego czwartku.
12, które przetrwało niezmienione
Serwisy, które zachowały niezależną wartość po pięciu latach, miały jedną wspólną właściwość: należały do zespołów z naprawdę różnym rytmem deploymentu.
Serwis płatności deployuje się codziennie. Serwis modelu wykrywania fraudów deployuje się co sześć tygodni (gdy nowy model jest zwalidowany). Granica między nimi jest wartościowa, bo pozwala płatnościom shippować bez czekania na walidację modelu fraudów.
Serwis notyfikacji deployuje się gdy zmieniają się szablony emaili. Serwis profilu klienta deployuje się z każdym sprintem produktowym. Różne rytmy, różne tryby awarii. Granica zasługuje na swój koszt.
Scalanie z powrotem
Proces ponownego łączenia był zaskakująco tani w większości przypadków. Serwisy z ciasno sprzężonymi bazami danych wymagały jednej operacji ALTER TABLE ... INHERIT plus zmiany aplikacji. Serwisy z osobnymi bazami wymagały migracji danych, ale dla tych, które powinny były być scalone, modele danych były prawie identyczne.
Drogie były fuzje, gdzie każdy serwis przez lata nazbierał własny dialekt modelu domeny. Cztery lata dywergencji spakowane w jedną migrację. Zaplanowaliśmy je na Q1 2025.
Co zrobiłbym inaczej
Zacznij od modułów, nie serwisów. Dobrze ustrukturyzowany monolit z wewnętrznymi granicami modułów jest tańszy do ekstrakcji niż do scalenia. Jeśli moduł udowodni, że potrzebuje niezależnego deploymentu, ekstrahuj go. Koszt ekstrakcji jest jednorazowy. Koszt przedwczesnej ekstrakcji to każdy featura przez pięć lat.
Zmierz f_change przed narysowaniem linii. W 2021 roku nie mieliśmy narzędzi do tego. Dziś istnieją narzędzia do analizy couplingu zmian między repozytoriami, które powiedzą ci z historii gita, które pliki zmieniają się razem. Używaj ich przed code review architektury, nie po, bo do momentu architecture review wszyscy już zakochali się w swoim pudełku na tablicy.
Najpierw przejmij warstwę platformy. Pierwsze sześć miesięcy mikroserwisów powinno być poświęcone na wspólny system buildów, wspólną obserwowalność, wspólny CI/CD. My poświęciliśmy je na logikę biznesową. Dług techniczny z tej decyzji kosztował nas więcej niż błędne granice serwisów.