Posts tagged NServiceBus
Jak zwątpiłem w transakcje
Jun 17th
Transakcje to fajna sprawa. Polubiłem je od pierwszego użycia. Zostałem oczarowany przez ich magiczną właściwość — zwalniają z myślenia o spójności danych. Cool, przecież nie lubię myśleć. Jeszcze bardziej byłem oczarowany, gdy odkryłem transakcje rozproszone. To dopiero jazda. Mogę coś “zapdejtować” na tej bazie, na tej drugiej bazie i jeszcze wrzucić komunikat do kolejki MSMQ i wszystko wykona się transakcyjnie — w całości lub wcale.
Od dłuższego czasu zaczynam jednak wątpić w transakcje, szczególnie te rozproszone. Zbyt często ja sam lub ktoś ze znajomych wpada przez nie w pułapkę bez wyjścia. Zamiast używać najodpowiedniejszej technologii, musimy wtedy wziąć taką, która współdziała z naszym sposobem zarządzania transakcjami.
Najczęstszym problemem, na który natrafiałem jest odbieranie i wysyłanie komunikatów do kolejek z jednoczesnym zapisem danych w bazie. Pierwsza rzecz, która przychodzi w takim wypadku do głowy, to transakcja, która obejmuje zarówno infrastrukturę kolejkową, jak i RDBMS. Pierwsza nie oznacza, niestety, najlepsza.
Nieodpowiednia technologia
Wymaganie transakcyjności w komunikacji z RDBMS i kolejkami pchnęło mnie kilka lat temu do wykorzystania SQL Server Service Broker jako kolejki komunikatów. Unikalną cechą Service Broker’a jest fakt, że żyjąc wewnątrz silnika bazodanowego, jest w naturalny sposób zintegrowany z mechanizmami transakcyjnymi SQL Server. Nie ma potrzeby stosowania (wolnych) transakcji rozproszonych, aby wysłać komunikat i zrobić przysłowiowy “apdejt”.
Niestety poza tą jedną zaletą, Service Broker ma bardzo wiele wad, z których największą jest brak dobrego gotowego API w C#, o modelu usługowym (np. WCF) nie wspominając. Jest bardzo trudny w użyciu i utrzymywaniu. Definitywnie był to zły wybór technologiczny, podyktowany jedynie chęcią zastosowania mechanizmu transakcji.
Niezgodność API
W każdym nietrywialnym systemie wykorzystuje się wiele zewnętrznych bibliotek. Bardzo rzadko pochodzą one od jednego dostawcy. Zdecydowanie częściej jest to mieszanka rozwiązań open source oraz COTS. Problem pojawia się, gdy dwie z bibliotek mają współpracować w ramach jednej transakcji. Weźmy jako przykład NHibernate i NServiceBus. Ten pierwszy posiada własną abstrakcję reprezentującą transakcje, podczas gdy pod spodem korzysta z transakcji ADO.NET. Ten drugi wykorzystuje transakcje System.Transactions do dostępu do MSMQ.
W przypadku użycia obu technologii w jednej transakcji, pojawia się problem, jak sprawić, aby każda z bibliotek mogła wykorzystywać swoje API odwołując się do tej samej fizycznej transakcji. W wypadku wspomnianej pary, jedynym rozwiązaniem jest pozwolić NHibernate używać zewnętrznych transakcji System.Transactions. Jest to jednak zamiana jednego problemu na inny. Do niedawna bowiem pojawiał się w NServiceBus wyciek pamięci, ponieważ zachowanie NHibernate w wypadku współpracy z System.Transactions jest bardzo słabo udokumentowane i łatwo o błąd wynikający z niezrozumienia.
Błedne implementacje transakcyjności
Jonathan Oliver opisał swoje testy kompatybilności różnych silników bazodanowych z transakcjami rozproszonymi na swoim blogu. Wnioski nie są optymistyczne: jedynie sterowniki do SQL Server i Oracle w pełni i bez problemów je obsługują.
Z drugiej strony nie dziwie się, że mniej płatne lub darmowe rozwiązania nie wspierają rozproszonych transakcji. Zdecydowana większość systemów radzi sobie bez nich, więc zysk (w sensie pieniędzy z licencji lub satysfakcji użytkowników) z implementacji wsparcia dla nich byłby znikomy.
Proste rozwiązanie
Rozwiązanie jest oczywiście proste. Wymaga jednak nieco innego podejścia do projektowania komunikacji. Wystarczy zadbać o to, aby każdy komunikat był
- albo idempotentny (wielokrotne przetworzenie takiego komunikatu daje taki sam efekt, jak przetworzenie jednokrotne),
- albo jednoznacznie identyfikowalny (unikalne ID).
Drugi przypadek można sprowadzić do pierwszego dodając rejestr przetworzonych komunikatów zawierający ich unikalne ID i przed obsłużeniem komunikatu sprawdzać, czy aby nie został przetworzony wcześniej.
Po spełnieniu któregoś z powyższych warunków zyskujemy możliwość rozłącznego zarządzania transakcją związaną z obieraniem komunikatu (MSMQ, ServiceBroker) oraz transakcją bazodanową. Ta pierwsza powinna być zatwierdzana dopiero po zatwierdzeniu tej drugiej. Powoduje to, że mamy pewność, iż każdy komunikat zostanie przetworzony co najmniej raz. Z drugiej strony idempotentność gwarantuje nam, że skutki wielokrotnego przetworzenia będą takie, jak jednokrotnego. Ostatecznie więc uzyskujemy semantykę dokładnie raz — taką samą jak przy zastosowaniu transakcji rozproszonych.
Praktyka
Dokładnie taki mechanizm zastosowałem w swoim ostatnim systemie. Pozwolił mi on na użycie klienta Service Broker (użycie tej kolejki było narzucone z góry) w połączeniu z NHibernate bez konieczności integracji obu technologii. Ponieważ transakcje były rozdzielone, klient kolejek nie musiał wiedzieć nic o dostępie do danych i vice versa. Prawdopodobnie zaoszczędziło mi to kilka dni pracy przy implementacji, testach i poprawianiu bugów w warstwie integracyjnej. Nauczyłem się także, że najlepszą strategią integracji technologii jest unikanie integracji technologii, kiedy to tylko możliwe.
Czym jest NServiceBus?
Feb 1st
Matt Burton, jeden z deweloperów NServiceBus, zainspirował mnie wczoraj do napisania tej notki zadając pytanie, czy można określić NSB mianem application server. Matt miał problem prowadząc swoje szkolenie z NSB, ponieważ stwierdzenie, że NServiceBus to ESB nie trafiało do słuchaczy.
Nie wiem, jak Wam, ale mnie ESB kojarzy się od razu z wielkim i ciężkim kawałem infrastruktury, który jest centralnym punktem architektury SOA danej organizacji. Pisałem już nieco o tym tutaj, porównując wzorce Message Broker i Message Bus. NSB jest dokładnym przeciwieństwem ESB, jednak w praktyce służy do tego samego. To trochę mylące, więc spróbujmy znaleźć jakąś lepszą analogię.
Niestety stwierdzenie, że NServiceBus to szyna komunikatów (bez odwoływania się do magicznego skrótu ESB) zdaje się nie trafiać do ludzi. Dlaczego? Pewnie dlatego, że w coś takiego jak szyna ciężko sobie wyobrazić w oderwaniu od kontekstu konkretnego wdrożenia. Jeśli widzimy serwerownię z 10 serwerami, na każdym z nich inny system i wszystkie te systemy wykorzystują do komunikacji NSB — to tak, wtedy możemy sobie wyobrazić NServiceBus jako szynę. Ale patrząc w kontekście pojedynczego systemu? Czym jest NSB dla mojego systemu?
I tu dochodzimy do tego, co zaproponował Matt, czyli application server. Serwer aplikacyjny jest platformą, na której działa aplikacja. Musi ona być, oczywiście, napisana w pewien specyficzny sposób — zgodnie z założeniami twórców serwera. W zamian za dochowanie wierności tym założeniom, serwer aplikacyjny oferuje wiele udogodnień, takich jak:
- wymuszenie spójności architektonicznej (poprzez nałożenie niezbędnych ograniczeń)
- gotowy do użycia mechanizm komunikacji, pozwalający udostępnić funkcje systemu na zewnątrz
- gotowe rozwiązania dla problemów związanych z aspektami niefunkcjonalnymi systemu: wydajnością, bezpieczeństwem itp.
Powyższa definicja jest oczywiście moją osobistą. Zgodnie z nią, serwerami aplikacyjnymi są np. (niespodzianka!) dowolny serwer aplikacyjny J2EE, tandem IIS/WCF oraz właśnie NSB.
Serwer J2EE służy do budowania aplikacji będącej kolekcją tzw. beanów. Na zewnątrz mogą być udostępniane session beans (komunikacja synchroniczna) oraz message-driven beans (komunikacja asynchroniczna – JMS).
IIS/WCF pozwala budować systemy jako kolekcję usług Web Service udostępnianych na zewnątrz dowolnym kanałem (WAS).
NServiceBus natomiast zakłada, że aplikacja jest zbiorem tzw. message handlerów — obiektów obsługujących rozmaite komunikaty. Obiekty te są bardzo podobne do message-driven beans, zapewne dlatego, że służą do tego samego celu — obsługi komunikatów pochodzących z kolejki.
To oczywiście bardzo duże uproszczenie. Każda z wymienionych trzech technologii posiada dużo więcej dodatkowych możliwości. Nie o możliwości tu jednak chodzi, ale o charakter, o model, którego dany serwer aplikacyjny używa do reprezentacji hostowanej aplikacji. Dlaczego model jest tak ważny? Spróbujcie hostować za pomocą WCF coś, co nie jest usługą Web Service, np. proces uruchamiany co 5 minut. Coś takiego nie ma reprezentacji w modelu WCF, trzeba więc zrobić jakieś obejścia tu i tam. Podobnie w NServiceBus, gdzie tego typu zadania można zrealizować wysyłając komunikat do tzw. timer service, ale rozwiązaniu takiemu daleko do elegancji. W takim wypadku być może powinniśmy użyć Quartz.NET, jako serwera aplikacji? No i mamy zadanie dla architekta…:-)
NHibernate, NServiceBus i transakcje
Jan 13th
Dziś chciałbym podzielić się z Wami moimi refleksjami na temat sposobu zarządzania transakcjami w NHibernate, ze szczególnym uwzględnieniem nietrywialnego przypadku, kiedy w ramach jednej transakcji wykorzystujemy zarówno NHibernate, jak i NServiceBus. Posłużę się w tym celu kodem DDDSample.Net.
Aby wprowadzić Was w klimat, przypomnę jak wygląda architektura DDDSample. Począwszy od najwyższego poziomu, występują tam następujące warstwy:
- WebUI (ASP.NET MVC) – prezentacja danych, interfejs
- Application – stanowi fasadę dla modelu domeny udostępniając interfejs poszczególnych akcji/komend
- Domain – logika biznesowa żyje tutaj
- Domain.Persistence.NHibernate – mapowania NHibernate oraz implementacje repozytoriów
Która warstwa odpowiada zatem za transakcje? Oczywiście Application. Dlaczego? Ponieważ jednostką izolacji są akcje/komendy — każda taka jednostka wykonywana jest w ramach osobnej transakcji.
Ponieważ transakcyjność postrzegam (zwykle) jako jedno z wymagań niefunkcjonalnych, implementuje ją za pomocą aspektów. Korzystam przy tym z możliwości mojego ulubionego kontenera Unity, jednak to samo można zrobić za pomocą niemal dowolnego innego kontenera. Moja warstwa Application składa się par (interfejs, klasa), przy czym interfejs zdefiniowany jest nie po to, aby umożliwić zmianę implementacji, ale tylko i wyłącznie po to, aby umożliwić AOP (tak, wiem, że można to samo osiągnąć za pomocą metod wirtualnych)
public interface IBookingService
{
TrackingId BookNewCargo(UnLocode origin, UnLocode destination, DateTime arrivalDeadline);
//...
public class BookingService : IBookingService
{
public TrackingId BookNewCargo(UnLocode originUnLocode, UnLocode destinationUnLocode, DateTime arrivalDeadline)
{
Location origin = _locationRepository.Find(originUnLocode);
Location destination = _locationRepository.Find(destinationUnLocode);
//...
container.Configure<Interception>()</pre>
//Ustaw sposób przechwytywania
.SetInterceptorFor<IBookingService>(new InterfaceInterceptor())
.SetInterceptorFor<IHandlingEventService>(new InterfaceInterceptor())
//Dodaj nową "politykę"
.AddPolicy("Transactions")
//Wykorzystującą aspekt obsługi transakcji
.AddCallHandler<TransactionCallHandler>()
//I podłącz do wszystkich interfejsów z assembly
.AddMatchingRule(new AssemblyMatchingRule("DDDSample.Application"));</div>
<div>
Który wykorzystując API sesji kontekstowych tworzy nową transakcję, wywołuje właściwą metodę, po czym, jeśli nie wystąpił żaden wyjątek, zatwierdza transakcję. W przypadku wyjątku transakcja pozostaje niezatwierdzona, a wyjątek (nienaruszony) przelatuje do warstw wyższych.
public class TransactionCallHandler : ICallHandler
{
private readonly ISessionFactory _sessionFactory;
public TransactionCallHandler(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
using (ITransaction tx = _sessionFactory.GetCurrentSession().BeginTransaction())
{
IMethodReturn result = getNext()(input, getNext);
if (result.Exception == null)
{
tx.Commit();
}
return result;
}
}
public int Order { get; set;}
}
Bardzo lubię ten kawałek kodu. Niestety nie sprawdza się on w moim ulubionym scenariuszu: NHibernate + NServiceBus. W takim przypadku potrzebuje transakcji rozproszonej System.Transactions. Swego czasu Ayende pisał o tym, że NHibernate “automagicznie” współdziała z System.Transactions. Ostatnio jednak pojawił się w NHibernate koncept ITransactionFactory, którego zadaniem jest (chyba?) poprawa jakości tego współdziałania. Domyślna implementacja fabryki, AdoNetWithDistrubtedTransactionFactory, jak sama nazwa sugeruje wspiera transakcje rozproszone. Niestety nie udało mi się sprawić, aby działała w najprostszym scenariuszu integracji z NServiceBus. Zrezygnowałem więc z tego ficzera i powróciłem do “starego dobrego” AdoNetTransactionFactory (który teraz trzeba sobie skonfigurować samemu). Niezbędna była jednak modyfikacja mojego TransactionCallHandler:
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
IMethodReturn result;
SqlConnection sqlConnection = _sessionFactory.GetCurrentSession().Connection as SqlConnection;
if (sqlConnection == null)
{
throw new NotSupportedException("Only SqlConnection is supported.");
}
using (TransactionScope tx = new TransactionScope())
{
sqlConnection.EnlistTransaction(Transaction.Current);
result = getNext()(input, getNext);
if (result.Exception == null)
{
_sessionFactory.GetCurrentSession().Flush();
tx.Complete();
}
}
sqlConnection.EnlistTransaction(null);
return result;
}
Zamiast korzystać z API transakcji NHibernate, korzystam bezpośrednio z System.Transactions. Negatywnym skutkiem tego podejścia jest ograniczenie wspieranych baz do SQLServera 2005/2008. Niestety ADO.NET nie umożliwia niezależnego od sterownika bazy danych wpinania połączeń w transakcje rozproszone.
Pierwsze wywołanie EnlistTransaction wpina aktualne połączenie, na którym działa sesja NHibernate do transakcji rozproszonej. Drugie wywołanie (to z null-em) odłącza połączenie od transakcji. Jeśli transakcja ta nie została wcześniej zatwierdzona (tx.Complete()), zmiany są wycofywane na poziomie bazy danych.
Jest jeszcze jeden szkopuł. NHibernate domyślnie zwalnia połączenia związane z sesją najwcześniej, jak może. Jest to zachowanie optymalne z punktu widzenia wydajności, jednak w przypadku takiego zarządzania transakcjami, niepoprawne. Nie chcemy przecież, aby nasze połączenie, które podłączyliśmy do rozproszonej transakcji, zostało zamknięte. Aby temu zapobiec, musimy do konfiguracji NHibernate dodać następujący wpis:
<property name=“connection.release_mode“>on_close</property>
Na koniec jeszcze jedna niemiła informacja: powyższy sposób zarządzania transakcjami jest niekompatybilny z cache 2-go poziomu NHibernate. Co to oznacza? Otóż modyfikacje dokonane na danych w ramach tak zrealizowanych transakcji nie zostaną uwzględnione w cache 2-go poziomu. Nie sprawia to jednak problemu, jeśli nie modyfikujemy danych cache-owanych. Można więc próbować obejścia, polegającego na stosowaniu obu pokazanych wersji TransactionCallHandler w zależności od tego, czy transakcje rozproszone są wymagane.
Taka strategia powinna sprawdzać się dobrze, ponieważ wymaganie transakcji rozproszonych jest związane z odbieraniem / wysyłaniem komunikatów NServiceBus, a tego rodzaju akcje nie powinny modyfikować danych słownikowych (które są zwykle cache-owane).
NServiceBus w kontekście zwykłej aplikacji – notka sponsorowana
Nov 21st
Jak już napisałem, NServiceBus pozwala budować szyny informacyjne. Jest jednak (w przeciwieństwie do ciężkich rozwiązań ESB) zorientowany na wykorzystanie w pojedynczym systemie. Czym jest system, to już każdy może sobie odpowiedzieć. Może to być jedna mała aplikacja, może być także cały zestaw aplikacji CRM, ERP i innych stanowiących kompleksowy system informatyczny obsługujący np. internetowy sklep z warzywami.
Nie zrozumcie mnie źle, NServiceBus może być użyty do komunikacji z innym systemem nie wykorzystującym NSB, jednak jest to przypadek szczególny. Zdecydowanie lepiej biblioteka ta sprawdza się jako wewnętrzny mechanizm komunikacji dla luźno powiązanego systemu. W takim kontekście NSB może nam zaoferować możliwość osiągnięcia:
- wysokiej przepustowości
- wysokiej dostępności
- lepszego modelowania procesów biznesowych
Jak to możliwe? Zaraz opowiem. Dla niecierpliwych tylko krótka informacja: wszystko to dzięki asynchronicznej komunikacji.
Wysoka przepustowość
- blokowanie zasobów bazodanowych jednocześnie w 3 bazach danych przez transakcje rozproszoną
- blokowanie wątków oczekujących na odpowiedź Web Service z usług B oraz C
Mając do dyspozycji narzędzia oferowane przez NServiceBus możemy tak zaprojektować system, że moduły A, B i C przesyłają sobie komunikaty zlecające zadania asynchronicznie. Jeśli moduł A do przetworzenia komunikatu żądania potrzebuje informacji X z modułu B, a moduł B produkuje informację X jako wynik swojego przetwarzania, to zwykły schemat request-response:
można zastąpić następującym:
Dzięki temu nikt na nikogo nie czeka. Zarówno zadania (strzałki czerwone), jak i stan (strzałki niebieskie) płyną od modułu do modułu. Jest to analogia taśmy produkcyjnej, podczas gdy rozwiązanie synchroniczne bardziej przypomina pracę rzemieślnika posiadającego dwóch pomocników.
Wysoka dostępność
A teraz przypadek z NSB. Ponieważ informacje X i Y są przesyłane asynchronicznie do modułów ich potrzebujących i są tam przechowywane lokalnie, brak dostępności modułu C oznacza, że dane będą przez jakiś czas nieco nieaktualne. Taki stan rzeczy jest prawdopodobnie całkowicie akceptowalny. Kiedy tylko moduł C zostanie uruchomiony ponownie, przetworzy zaległe żądania i opublikuje nowe wartości danej Y. Klient niczego nie zauważy.
Lepsze modelowanie procesów biznesowych
Po co nam tutaj NServiceBus? Otóż zamiast wyszukiwać obiekty w określonym “stanie” można, po pomyślnym wykonaniu zadania publikować na szynie NSB zdarzenie zadanie X wykonane pomyślnie. Co z tego, że jedynym subskrybentem zdarzenia będzie ten sam system, który je publikuje? W niczym to nie przeszkadza, a za to sprawia, że kod nareszcie przypomina biznes, który wspiera i automatyzuje.
NServiceBus – Message Broker vs Message Bus
Nov 20th
Message Broker (znany także jako hub-and-spoke) zakłada istnienie jednago centralnego punktu, przez który przechodzą wszystkie komunikaty w danym systemie. Broker zajmuje się konwersją formatów oraz innymi kwestiami niefunkcjonalnymi (np. bezpieczeństwo). Oczywiście stwierdzenie “jeden centralny punkt” dotyczy architektury logicznej. Fizycznie Broker może być zrealizowany za pomocą klastra maszyn w celu zapewniania zwiększonej wydajności i/lub niezawodności.
Wzorzec Message Bus opiera się na braku wspomnianego centralnego punktu. Każdy system (usługa) biorąca udział w komunikacji stanowi węzeł abstrakcyjnej “szyny informacyjnej”. Skoro brak jest centralnej koordynacji, to czym Message Bus różni się od tak znienawidzonego spaghetti? Już spieszę z odpowiedzią — wspólnym mechanizmem komunikacji. Każdy węzeł szyny komunikuje się zegodnie ze wspólnym, uzgodnionym protokołem. Dzięki temu dodanie kolejnego systemu do układanki nie powoduje lawinowego wzrostu skomplikowania systemu.
Dlaczego o tym piszę? Otóż dlatego, że NServiceBus jest frameworkiem pozwalającym budować systemy (lub systemy systemów;-) zgdonie z wzorcem Message Bus. Często wyobrażamy sobie szynę informacji (nazywaną czasem ESB), jako ogromny kawał infrastruktury, który kupujemy za ciężkie pieniądze od IBM czy Oracle. Takie ESB stoi sobie na dedykowanych maszynach w dedykowanej serwerowni… Tylko czy w tym dzikim pędzie za kolejnymi ficzerami i trzyliterowymi skrótami nie zapominamy o samej idei Message Bus? Brak centralnych punktów, tak? Hmm…
NServiceBus pozwala zrealizować ten jeden, konkretny system, nad którym właśnie pracujesz tak, aby komunikacja odbywała się w topologii Message Bus. W NServiceBus każdy niezależny proces systemu to węzeł Szyny. Wszystko, czego potrzeba, aby wpiąć się w tę szynę, to dołączyć referencję do NSB i wystartować swoją instancję IBus.
Na powyższym obrazku klocki ze strzałkami reprezentują kolejki MSMQ. To one (i tylko one) budują szynę. To, czego obrazek nie oddaje to fakt, że kolejki te zlokalizowane są na tych samych fizycznych maszynach, na których systemy je wykorzystujące. Obrazki starają się oddać ważność elementów składowych rozwiązania w obu koncepcjach. Według klasycznej, ESB jest najważniejszym elementem architektury. Według tej, jaką można zrealizować za pomocą NServiceBus, najważniejsze są systemy biznesowe.





