Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Simon says...

Szymon Pobiega o architekturze i inżynierii oprogramowania
Historia pewnej szyny - epilog
Jak część z Was pamięta, jakiś czas temu dosyć aktywnie blogowałem na temat szyny (tu, tu oraz tu). Od tej pory upłynęło już sporo czasu. Chciałoby się powiedzieć: "dziś sam jestem dziadkiem". Ale nie, wertersów Wam nie dam. Opowiem za to o moim dojrzewaniu do komunikacji za pomocą asynchronicznej wymiany komunikatów.

Nieświadomy


Żyłem długo w nieświadomości istnienia komunikacji kolejkowej (asynchronicznej). To znaczy niby coś tam pamiętałem ze szkoły, że jest API JMS i są jego implementacje, ale nie rozważałem komunikacji asynchronicznej jako narzędzie w moim warsztacie deweloperskim. Przecież skoro są webserwisy, kto chciałby się bawić jakimiś kolejkami? Webserwisy rządzą!

Starcie z pierwszym problemem


I tak sobie żyłem szczęśliwie, aż tu pewnego dnia napotkałem problem: nowy projekt, którego celem była integracja dwóch systemów stworzonych w mojej firmie. Projekt trafił do mnie - miałem zaprojektować interakcję między systemami. Logicznie rzecz biorąc sprawa była prosta: w momencie, kiedy proces biznesowy w jednym z systemów zmiana stan na "wykonany", do drugiego systemu zostaje wysłany komunikat o konieczności wykonania pewnej operacji biznesowej.

Prawdziwy problem pojawił się w momencie projektowania, jak wspomniana interakcja ma być wykonana fizycznie. Zmiana stanu procesu w pierwszym systemie przejawia się zapisem odpowiednich danych do relacyjnej bazy danych. Podobnie, wykonanie wspomnianej operacji w drugim systemie, także wiąże się z zapisem danych do bazy. Pierwszym rozwiązaniem, jakie przyszło nam do głowy, była transakcja rozproszona. Ponieważ kontrolujemy środowiska uruchomieniowe obu systemów, wpięcie ich w DTC nie stanowiłoby problemu. Okazało się jednak, że z transakcjami rozproszonymi związane są dużo większe problemy, niż początkowo myśleliśmy. Nie tylko są one wolne, a nawet WOLNE. Do tego (a było to w czasach przed WCF-em) są trudne do implementacji. Znalazłem w sieci jakiś "hack", który pozwalał na dodanie transaction flow do Remoting-u, do webserwisu ASMX pewnie także by się dało. Ale z tym dużo i jakieś takie niepewne.

Wtedy zauważyliśmy, że wykonanie operacji w drugim systemie zawsze się udaje. To znaczy może się nie udać z przyczyn losowych (jak pad bazy danych), ale jeśli odpowiednio długo będziemy ponawiać próby, to mamy gwarancję, że w końcu się uda. To spostrzeżenie naprowadziło nas na następujące rozwiązanie. W pierwszym systemie wraz ze zmianą stanu procesu zapisujemy (w bazie danych, w ramach tej samej transakcji) informację o konieczności zlecenia operacji dla drugiego systemu. Do tego w pierwszym systemie istnieje autonomiczny proces, który okresowo sprawdza, czy są jakieś operacje do zlecenia dla drugiego systemu. Jeśli są, to wyciąga ze swojej bazy parametry wywołania i próbuje się skontaktować z drugim systemem. Jeśli dostanie odpowiedź pozytywną, zamyka zlecenie. W ten sposób każde zlecenie w końcu zostanie zrealizowane.

Rozwiązanie takie wymagało, aby istniał (po stronie pierwszego systemu) rejestr zleceń do wykonania. Skoro jest rejestr to także i GUI, które pozwoli administratorowi monitorować pracę procesu go obsługującego. Na wszelki wypadek dodaliśmy też możliwość ręcznego ponawiania i tym podobne bajery. Generalnie, bardzo dużo pracy i czasu kosztowało nas stworzenie tego rejestru. Po zakończeniu projektu stwierdziłem, że nigdy więcej. Obraziłem się na taki, nietransakcyjny, sposób komunikacji. Przecież musi istnieć coś lepszego, prawda?

Nowa nadzieja


Przyszedł czas na kolejny projekt. Ja, zaopatrzony w przekonanie, że asynchroniczna komunikacja jest jedynym słusznym rozwiązaniem (niezaprzeczalny wpływ poprzedniego TechEd-u i prezentacji Udi-ego), byłem gotowy na kolejne starcie. Tym razem w moim arsenale miałem nową tajną broń: SQL Server Service Broker - infrastruktura kolejkowa wbudowana w bazę danych. Cóż w niej takiego interesującego? Ano fakt, że umożliwia wykonywanie normalnych
operacji bazodanowych oraz operacji na kolejkach w ramach jednej lokalnej transakcji. Czyż to nie fantastyczne, móc zapisując nowy stan procesu biznesowego jednocześnie wysłać komunikat do drugiego systemu? W razie błędu lub awarii serwera obie operacje automatycznie zostaną wycofane. Fantastycznie! Z drugiej strony, system odbierający może odczytać komunikat i w ramach tej samej transakcji zapisać dane biznesowej, czyli wykonać operację. W razie awarii, nie tylko dane nie zostaną zapisane, ale także i komunikat wróci automagicznie do kolejki. Cudownie!

Stałem się fanatycznym zwolennikiem Service Broker. Nie przeszkadzał mi fakt, że SB nie jest wspierane przez praktycznie żadną liczącą się bibliotekę wymiany komunikatów (WCF, NServiceBus, MassTransit). Przecież mogę napisać swoją! No zacząłem. Stad wzięły się trzy notki, do których odwoływałem się we wstępie do tej notki. Co się okazało?

Po fazie prototypowania API własnej szyny komunikatów (do której to zatrudniłem najtęższe mózgi czytelników Zine-a oraz pracowników mojej firmy) przyszedł czas na implementację. Niestety, na to już nie starczyło sił. System, który przyszło mi tworzyć wykorzystywał wczesną, prototypową wersję biblioteki komunikacyjnej dla SB.

Upadek


Nie miałem czasu na implementację mojej szyny. Ani w pracy, ani w ramach "hobby". Skala projektu przerosła mnie jeszcze zanim napisałem pierwszą linijką kodu (na szczęście). Ogromną ilość pracy koncepcyjnej (która zapewne przełożyłaby się na podobno ilość pracy implementacyjnej) włożyłem w opracowanie sposobu na łączenie w jednej transakcji operacji na szynie z operacjami bazodanowymi. Prototypowa biblioteka, z której wciąż nasz system korzystał nie obsługiwała takich podstawowych aspektów asynchronicznej komunikacji, jak obsługa poison messages. Domyślnym zachowaniem SB w wypadku tego typu komunikatów jest automatyczne zablokowanie kolejki po 5 nieudanych próbach odczytu. Co ciekawsze, nie da się tego mechanizmu wyłączyć! Pisanie własnej obsługi pewnie trochę by zajęło. Znalazłem się wiec w sytuacji, kiedy miałem piękną architekturę z wymianą komunikatów, ale nie dysponowałem technologią, aby tę architekturę wprowadzić w życie.

Oświecenie


I wtedy postanowiłem raz jeszcze zwrócić się o pomoc do Udi-ego. To znaczy oczywiście nie osobiście! Przetrawiłem po raz n-ty kilka postów na jego blogu. Zrozumiałem, że same podstawy mojego podejścia do tematu są błędne. Sama idea (zrodzona z trudności wynikłych przy realizacji pierwszego projektu integracyjnego), aby łączyć w jednej transakcji operacje na bazie danych i na kolejkach jest z definicji zła. A nawet ZŁA.

Rozwiązanie


Rozwiązanie problemu transakcji wymaga dwóch zmian. Obie są zmianami biznesowymi (koncepcyjnymi), a nie technicznymi. Pierwsza z nich dotyczy wysyłania komunikatu w momencie zmiany stanu procesu. Zamiast tego wprowadzanym w procesie dodatkowy stan: "gotowy do wysłania" komunikatu. Dawne przejście do stanu "wysłano", teraz ma prowadzić do stanu "gotowy". Obsługa procesu w tym stanie polega na podjęciu próby wysłania komunikatu. Jeśli próba ta się powiedzie, proces zmienia stan na "wysłano". W ten sposób w końcu uda się wysłać komunikat. Efekt uboczny tego rozwiązania jest taki, że możliwe jest, że komunikat zostanie wysłany więcej niż raz. Dzieje się tak wtedy, gdy po udanym wysłaniu komunikatu nastąpi awaria bazy danych, w której przechowywany jest stan procesu. System natrafi na błąd. Po powrocie do normalnego działania proces wciąż będzie w stanie "gotowy" i system spróbuje wysłać komunikat ponownie.

Co zrobić z tymi zduplikowanymi komunikatami? Na ratunek przychodzi nam kolejna koncepcja biznesowa: idempotentność. Jest to właściwość komunikatu oznaczająca, że wysłanie tej samej wiadomości więcej niż raz nie spowoduje żadnych dodatkowych konsekwencji. Czyli w praktyce: że system odbierający zignoruje zduplikowane wiadomości.

Epilog


Jestem właśnie w trakcie eksperymentalnego przełączania mojego najnowszego systemu (integrującego się z 3 innymi systemami) z rozwiązania SB na tandem NServiceBus + MSMQ. Komunikaty, które są wymieniane między systemami już wcześniej (dobra decyzja architektoniczna:)) zaopatrzone były w unikalne identyfikatora pozwalające na odrzucanie duplikatów. Teraz tylko dodaje kod, który wykonuje rzeczywiste sprawdzenie zduplikowania. Do tej pory (ze względu na transakcyjność SB i zapisu do bazy) nie było takiej konieczności. Do tej pory spędziłem nad tym eksperymentem jakieś 8 godzin. Dziś udało mi się nawet przesłać pierwszy komunikat! Mam nadzieję, że za jakieś dwa tygodnie (pracuje nad tym w ramach firmowego Programu Innowacji, jeden dzień w tygodniu) będę mógł ogłosić pełny sukces.

Trzymajcie kciuki!

Referencje

Udi Dahan, Spectacular Scalability with Smart Service Contracts
Udi Dahan, Build Scalable Systems That Handle Failure Without Losing Data
MSDN, Optimizing Performance in a Microsoft Message Queue Server Environment

Opublikowane 4 czerwca 2009 17:07 przez simon

Filed under: ,

Komentarze:

# Simon says... : Historia pewnej szyny - epilog @ 4 czerwca 2009 20:56

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

dotnetomaniak.pl

# re: Historia pewnej szyny - epilog @ 4 czerwca 2009 23:34

heh, mam podobne przemyślenia i doświadczenia. Jako, że w niedługim czasie czeka mnie podobna praca z integracją systemów, będę wdzięczny za kolejne posty w tym temacie :)

A jak już zacznę w to brnąć nie omieszkam podzielić się i własnym doświadczeniem  :)

dario-g

# re: Historia pewnej szyny - epilog @ 7 czerwca 2009 09:49

Czekaj czekaj, ale dlaczego nie chcesz łączyć w jednej transakcji operacji na bazie danych i na kolejkach komunikatów? Udi coś takiego powiedział? Ja bym się nie zgodził:

Po pierwsze, NServiceBus podczas obsługi komunikatów przychodzących robi wszystko w ramach transakcji rozproszonej. Czyli jeśli to będzie operacja na bazie danych a na dodatek wyślesz sobie jakiś komunikat to zrobisz to w tej samej transakcji.

Po drugie jak dla mnie właśnie to jest fajne że robiąc transakcję rozproszoną obejmujesz bazę danych i kolejki komunikatów. Oznacza to ze jak transakcja się nie uda z dowolnego powodu to zadne komunikaty nie będą wysłane. Bez transakcji musiał być coś z tymi komunikatami zrobić po stronie odbiorcy - a nie wiem czy dałbyś radę ustalić czy są one wynikiem transakcji udanej czy nieudanej.

rafalg

# re: Historia pewnej szyny - epilog @ 7 czerwca 2009 10:46

Hej:)

Tutaj: http://www.infoq.com/articles/scale-with-service-contracts jest jeden z artykułów Udiego z serii "budowaliśmy system obsługi zamówień". Inny, opublikowany w MSDNMag jest tutaj: http://msdn.microsoft.com/en-us/magazine/cc663023.aspx.

Dlaczego łączenie w jednej transakcji zapisu danych i odczytu z kolejki jest złe? Ano dlatego, że jest bardzo zależne od technologii. W tym momencie (bez używania transakcji rozproszonych) możesz to zrobić za pomocą Service Broker-a, co przypina Twój system bardzo mocno do infrastruktury M$: zarówno do silnika bazy danych jak i kolejek.

Druga sprawa: NServiceBus wykorzystuje System.Transactions, ale _nie_ wykorzystuje transakcji rozproszonych. Z tego co wyczytałem i przetestowałem wynika, że obsługa MSQM w System.Transaction zrealizowana jest w ten sposób, że jeśli w danym Scope bieże udział tylko MSMQ, to transakcja jest fizycznie transakcją lokalną MSMQ. Sprawdzałem to wyłączając DTC u siebie na maszynie - działało.

Tutaj: http://msdn.microsoft.com/en-us/library/ms811054.aspx można znaleźć porównanie wydajności MSMQ z transakcjami lokalnymi i DTC. Różnica jest ooogromna.

No i ostatnie pytanie: po stronie wysyłającego najpierw zapisuje dane do bazy (ustawiam stan na "przed wysłaniem"), a potem osobnym procesem próbuje do skutku wysłać komunikat do kolejki. Po udanym wysłaniu zmieniań stan na "wysłano". Jeśli niechcąco wyśle komunikat do kolejki więcej niż raz, to nic, bo komunikaty są idempotentne: po stronie odbiorcy, na podstawie pewnego identyfikatora, następuje sprawdzenie, czy ten komunikat nie został już przetworzony.

Jeśli nie, następuje zapis do bazy po stronie odbiorcy i dopiero _potem_ oznaczenie transakcji odczytu z kolejki jako wykonanej. Jeśli ta ostatnia operacja się nie powiedzie to nic, dostaniemy ten komunikat jeszcze raz.

simon

# re: Historia pewnej szyny - epilog @ 7 czerwca 2009 20:31

Napisałeś że NServiceBus wykorzystuje system.Transactions - i tak jest. Działanie TransactionScope jest takie że jeśli w ramach transakcji używasz tylko jednego 'providera' - np tylko MSMQ, to rzeczywiście jest ona lokalna i obejmuje tylko MSMQ. Ale jeśli w obsłudze komunikatu będziesz chciał zrobić coś w bazie danych to przy rozpoczęciu transakcji w bazie danych zostanie utworzona transakcja rozproszona obejmująca MSMQ oraz bazę danych. A taka sytuacja ma miejsce w większości przypadków więc nawet nieświadomie można wdepnąć w transakcję rozproszoną.

A co do Twojego rozwiązania, to myślę że właśnie zaimplementowałeś własną kolejkę komunikatów w bazie danych. Tzn jeden proces coś tam wstawia do bazy a drugi odczytuje. To jeszcze potrzebny Ci ten MSMQ? Może wystarczy jak popracujesz nad tą kolejką w tabeli bazy danych?

rafalg

# re: Historia pewnej szyny - epilog @ 8 czerwca 2009 07:16

Ja na to patrze jak na zwykły workflow, a nie kolejkę, chociaż rzeczywiście, można też patrzeć na to kolejkowo. Kwestia tego, jak zaimplementowany jest Twój proces biznesowy.

W moich ostatnich dwóch aplikacjach nie korzystam z frameworku workflow (WF?), ale mam model "potoków i filtrów": zdefiniowane kilka procesów biznesowych (zaimplementowanych jako niezależne wątki), które zbierają obiekty w pewnym stanie, przetwarzają, a następnie zapisują w innym. Coś takiego, owszem, jest systemem kolejek, ale ze względu na np. GUI łatwiej się to symuluje na bazie danych.

Stąd w moim przypadku naturalne jest dołożenie kolejnego procesu, który będzie odpowiedzialny za wysyłanie komunikatów. W "klasycznym" rozwiązaniu workflow mielibyśmy dodatkowe Activity.

Jeśli chodzi o transakcje rozproszone, to osobiście miałem tyle jazd z poprawną konfiguracją DTC, że nie sądzę, żeby ktoś przez przypadek załapał się w transakcję rozproszoną. Z drugiej strony uważam, że decyzja M$, aby SqlConnection _domyślnie_ samo włączało się w transakcję rozproszoną jest błędna. Teraz mam już nawyk doklejania do każdego connection string-a ";Enlist=false".

simon

Komentarze anonimowe wyłączone