Rozbijałem monolit na mikroserwisy metodą Strangler Fig. Oto czego nie mówią o tym poradniki
- backend
- mikroserwisy
- spring boot
Rozkład monolitu na niezależne usługi - dokładnie ten proces opisuje wpis.
Miałem gotowy monolit. Nazwałem go ordered-backend - system e-commerce wzorowany na Allegro, z płatnościami przez Stripe, koszykiem, adresami dostawy i pełną obserwowalnością (Prometheus, Grafana, Jaeger). Działał. Testy przechodziły, load testy w Gatlingu dawały dobre liczby. A mimo to postanowiłem go rozbić na sześć niezależnych serwisów metodą Strangler Fig.
Powód był prosty: monolit dobrze pokazuje, że umiesz napisać działającą aplikację. Mikroserwisy pokazują, że rozumiesz, co się dzieje, kiedy ta aplikacja przestaje mieścić się w jednej głowie.
Podział usług to dziesięć procent roboty
Wydzielenie granic - order, product, user, engagement - zajęło mi jedno popołudnie z kartką papieru. Prawdziwa robota zaczęła się później:
- Duplikacja logiki. Każdy serwis potrzebował tych samych klas: uwierzytelniania po JWT, obsługi wyjątków, odpowiedzi stronicowanych. Wydzieliłem to do wspólnej biblioteki
ordered-commons, ale to samo w sobie rodzi pytanie: gdzie jest granica między "wspólnym kodem" a "ukrytym sprzężeniem między serwisami, które miały być niezależne"? - Discovery i routing. Serwisy muszą się nawzajem znaleźć. Postawiłem Eurekę do service discovery i API Gateway jako jeden punkt wejścia - nagle doszła mi warstwa infrastruktury, której w monolicie po prostu nie było.
- Spójność danych bez transakcji rozproszonych. To był największy problem. W monolicie zdarzenie "zamówienie dostarczone" i aktualizacja stanu magazynowego żyły w jednej transakcji bazodanowej. Po podziale to fizycznie niemożliwe. Rozwiązaniem był transactional outbox - zapisujesz zdarzenie do tabeli w tej samej transakcji co zmianę danych, a osobny proces wysyła je do Kafki. Brzmi prosto, dopóki nie zapomnisz dodać
@EnableSchedulingdo serwisu i przez dobry czas zastanawiasz się, czemu poller w ogóle się nie odpala.
Rzeczy, które faktycznie mnie zaskoczyły
Obserwowalność w monolicie to jeden dashboard. W systemie rozproszonym musisz umieć powiedzieć, który z sześciu serwisów jest winny, kiedy request trwa 800ms zamiast 80ms - i to bez logowania się po kolei do każdego z nich. Rozpiąłem trace'y przez Jaegera i dopiero wtedy mikroserwisy zaczęły mieć sens jako coś więcej niż "ten sam kod, tylko podzielony na foldery".
Drugą niespodzianką była konfiguracja. Zamiast plików application.yml w każdym repo, scentralizowałem to przez Spring Cloud Config Server. To wygodne, dopóki nie uderzysz w typową pułapkę: konfiguracja testowa (application.yml w src/test/resources) całkowicie nadpisuje główną konfigurację zamiast się z nią scalać. Testy integracyjne próbowały łączyć się z serwerem configu, którego nie było, i wywalały się w losowych momentach. Fix: -Dspring.cloud.config.enabled=false w konfiguracji Surefire/Failsafe.
Czy było warto
Load testy w Gatlingu (te same scenariusze co w monolicie, identyczne profile obciążenia) dały wynik 100% sukcesu przy p95 na poziomie ok. 200ms dla katalogu produktów. To nie jest efekt magii mikroserwisów - to efekt tego, że w trakcie podziału musiałem zrozumieć każdy fragment systemu na tyle dobrze, żeby przenieść go bez utraty spójności danych.
Gdybyś pytał, czy zaczynałbym od razu od mikroserwisów przy nowym projekcie - nie. Zacząłbym od monolitu, dobrze rozdzielonego na moduły domenowe, i rozbijał dopiero wtedy, kiedy podział przestałby być ćwiczeniem akademickim, a stał się odpowiedzią na realny problem: inny cykl skalowania dla katalogu produktów niż dla płatności, inny zespół odpowiedzialny za inną domenę. Podział dla samego podziału to najdroższy sposób na nauczenie się Kafki.
Pracujesz nad podobnym problemem? Napisz do mnie.