Jak zaprojektować bardziej złożoną aplikację w RoR?

Temat troszeczkę bardziej złożony, więc potrzebuję kilkunastu zdań aby go opisać. Chciałbym się dowiedzieć co RoR ma do zaoferowania przy bardziej złożonym projekcie. Być może istnieją już jakieś gotowe rozwiązania i dobrze działają.

Dobrym przykładem będzie tutaj aplikacja sklepu internetowego, ponieważ w sposób widoczny spełnia ona kilka założeń:

  1. Każdy sklep musi być oddzielnym wydzielonym środowiskiem pod względem dostępu do zasobów sprzętowych.
  2. Każdy sklep powinien mieć możliwe najnowszą wersję ale z różnych przyczyn musi być też możliwość pozostawienia danego sklepu na starszej wersji.
  3. System sklepu musi być zintegrowany także z innymi systemami jak np. system magazynowy, księgowy, generowania druczków itd.
  4. Aktualizacje wersji systemu muszą przebiegać szybko i sprawnie.

Możliwe są 2 podejścia do zaprojektowania takiego systemu:

  1. Cały system upakowany w 1 serwer, całkowicie niezależne.
  2. Główna część systemu z bazą danych oddzielnie dla każdego sklepu. Natomiast poszczególne część jak np. obsługa e-maili, integracja druczków z kurierem na oddzielnych serwerach.

*serwer - wirtualny serwer / oddzielny serwer / konto na hostingu

A teraz clue: Czy RoR posiada jakieś gotowe rozwiązania dla systemów, które są pokrojone na zależne części i komunikują się pomiędzy sobą np. przez jakieś API.
Tj. np. chciałbym uzyskać następującą konfigurację:

  1. system sklepu jest niezależny jeśli chodzi o wydajność sprzętową (baza danych + obsługa ruchu w sklepie)
  2. System integracji druczków dla kuriera stanowi oddzielny serwer, z którym system sklepu się łączy i wymieniają pomiędzy sobą informację
  3. System integracji z systemem księgowo-magazynowym stanowi oddzielny serwer i system sklepu łączy się z nim poprzez API i wymieniają pomiędzy sobą informację
  4. System obsługujący e-maile współpracuje zarówno z systemem sklepu jak i wszystkimi pozostałymi modułami

Co RoR może zaoferować w tej kwestii?

Jeśli ktoś ma doświadczenia w pracy w RoR i może porównać aplikacje, które stanowiły zupełnie niezależne byty oraz aplikacje, które były rozproszone tak jak tutaj opisałem również będę wdzięczny za takie porównanie. Mam tutaj przede wszystkim na myśli rozwój tych aplikacji w czasie.

Pytanie długie, ale odpowiedź krótka:

Jak zwykle trzeba odróżnić “czy się da” od “czy warto”.

Odpalanie jednej aplikacji dla wielu różnych klientów w różnych, odseparowanych środowiskach - do tego ze zmianami per klient - to wielki PITA. Możesz użyć rails engines jak sugeruje apohllo ale to będzie i tak boleć.

Lepszym rozwiązaniem jest stworzenie jednej wersji i odpalanie jej jako SaaS. Zawierasz funkcjonalności dla wszystkich klientów w jednej aplikacji, co najwyżej włączając ją lub wyłączając per klient. W ten sposób masz jeden kod, jeden zestaw testów i jedno środowisko uruchomieniowe.

Nie musisz się obawiać, że duże użycie przez jednego klienta będzie wpływać na pozostałych. Wystarczy, że dobrze zbudujesz swoją architekturę. W skrócie: jeden wspólny frontend, dużo serwerów aplikacyjnych, jeden lub dwa serwery bazodanowe. Taki zestaw pociągnie Ci dowolną aplikację - skalujesz się tylko liczbą serwerów aplikacyjnych.

Osobna sprawa to rozbicie na kilka mniejszych aplikacji rozmawiających ze sobą przez API. API musisz stworzyć samemu, najlepiej tak jak pisze apohllo wykorzystując REST. Pamiętaj, żeby rozbijać według funkcjonalności aplikacji a nie funkcjonalności stosu. W szczególności złym rozwiązaniem jest stworzenie API do dostępu do bazy danych dla wszystkich aplikacji.

Kiedy rozbijesz aplikacje według funkcjonalności i połączysz je API to możesz całość skalować umieszczając każdy z serwisów we własnym skalowalnym klastrze.

W Ragnarsonie odpalamy podobne systemy na Shelly Cloud obsługując ruch na poziomie 4 tys req/s. (gwoli wyjaśnienia - jestem właścicielem zarówno Ragnarsona jak i Shelly Cloud)

@Bragi to tylko pogratulować przedsiębiorstw :slight_smile:

Dokładnie tak to jakoś widziałem. Czyli, żeby np. na heroku ( lub czymś innym, np. tym czego Ty jesteś właścicielem) stworzyć moduły funkcjonalne jak właśnie generowanie druczków itd. a to czego nie da się wynieść poza system sklepu umieścić właśnie na koncie danego klienta (też w tym samym środowisku czyli heroku). Nie wiedziałem tylko czy RoR to jakoś wspiera.

Piszesz, żeby bazę danych mieć na dedykowanym do tego serwerze. Nie lepiej aby bazę danych każdy klient miał swoją? Dzięki temu będzie łatwiej zarządzać jego potrzebami sprzętowymi i jakieś większe obciążenie nie wpłynie na pozostałe aplikacje klientów. Czy coś pominąłem?

To samo tyczy się małych aplikacji, wszystkie według Ciebie powinny korzystać z tego samego serwera bazy danych? Zakładamy cały czas, że pracujemy w chmurze, więc środowisko jest spójne nawet jeśli każda aplikacja ma swoją bazę na oddzielnym koncie.

Odnośnie skalowalności rozumiem, że sugerujesz aby wyrzucić wszystko do małych aplikacji, a jako fronted zrobić tylko coś co z nimi rozmawia, a samo w sobie nic nie robi?

Z Twojej wypowiedzi rozumiem, że jest to lepsze rozwiązanie niż postawienie frontendu z bazą danych oraz podstawową funkcjonalnością aby móc skalować również same konta klientów?

Co do skalowania baz danych: jedna z naszych aplikacji obsługuje ruch do 250 równoczesnych połączeń przy 25 nowych sesjach na sekundę (mówię tu tylko o takich połączeniach, które dotykają już aplikacji, nie licząc ruchu na assety). Ten ruch jest obsługiwany przez jeden serwer baz danych. Przekłada się to na 400 zapytań SQL na sekundę. Serwer baz danych działa bez zadyszki i nadal ma spory zapas mocy. A mówimy o 1,5M zapytań do aplikacji dziennie.

Zanim nasz zespół stworzył zintegrowane rozwiązanie według architektury opisanej powyżej aplikacja miała osobne wersje językowe odpalone na osobnych maszynach. Duże problem z ich zarządzaniem, różnymi wersjami kodu i nierównym wykorzystaniem zasobów sprawiły, że zwrócili się do nas o pomoc.

Teraz mają jeden aktywny serwer na bazę danych SQL, 15 serwerów aplikacyjnych oraz kilka pomocniczych serwerów na memcached. A taka architektura pozwoliła im zwiększyć ruch kilkunastokrotnie w ciągu roku i nadal skaluje się liniowo.

Rozbicie kodu na osobne środowiska/klastry z obawy o wydajność bazy danych to przedwczesna optymalizacja.

Druga sprawa do zrobienie wielu małych aplikacji: każdą z takich aplikacji możesz skalować oddzielnie. Na raz masz do obięcia mniejszy kod, łatwiej rozwijać i testować takie mniejsze aplikacje.

To jak je połączysz to osobna sprawa. Każda aplikacja powinna reprezentować osobną funkcjonalność dla szczególnej grupy użytkowników - np. panel administracyjny, system sprzedaży, system magazynowy. Mogą się wymieniać danymi przez API i powiadamiać się nawzajem o zmianach. Każda z takich aplikacji może mieć własną bazę danych - pozwala to na rozbicie danych (sharding) funkcjonalny i daje duże możliwości optymalizacji.

Według mnie warto rozbijać aplikacje na podstawie funkcji dla użytkownika a nie funkcji w systemie. Utworzenie dużej ilości małych aplikacji i jednej osobnej na frontend to reprezentacja drugiego podejścia - według funkcji. Lepiej osobne aplikacje, z osobnymi frontendami, z których każda zajmuje się małym wycinkiem funkcjonalności.

Jeśli całość chcesz połączyć za pomocą jednej aplikacji to niech to będzie aplikacja single sign-on.

Każda z małych aplikacji ma pełne prawo przechowywać swoje dane o danym użytkowniku - tylko te, których na prawdę potrzebuje. Aplikacja, która dla potrzeb administracyjnych lub statystycznych zbiera informacje ze wszystkich mniejszych aplikacji może je duplikować lub wyciągać świeże na podstawie API.

Skomplikowane systemy nie są łatwe w tworzeniu. Możesz wybrać dowolną drogę i na pewno dasz sobie radę o ile masz dość samozaparcia i liczysz się z częstymi zmianami w systemie. Gdybym ja miał wybierać architekturę dla systemu od początku poszedłbym w SaaS i dużo mniejszych aplikacji - i tak w końcu ląduję w takiej architekturze :slight_smile:

“Gdybym ja miał wybierać architekturę dla systemu od początku poszedłbym w SaaS i dużo mniejszych aplikacji - i tak w końcu ląduję w takiej architekturze”

Jeśli dobrze rozumiem dla przykładu sklepu internetowego sugerujesz zrobienie oddzielnego panelu dla magazynu, obsługi klienta itd. zamiast łączyć to w jeden panel?

Wszystkie łączą się z tym samym serwerem bazy danych ale każda aplikacja ma swoją bazę danych z danymi z redundancją?

Dziękuję za podzielenie się doświadczeniem, brzmi konkretnie :slight_smile:

Twój podział sklepu internetowego wygląda dobrze.

Backoffice - czyli zarządzanie sklepem to jedna aplikacja. Może być prosta - nie będzie miała dużo użytkowników. Dane przez nią przygotowane (wygląd sklepu) są przesyłane do frontu - czyli aplikacji wyświetlającej sklep.

Front ma dużo użytkowników więc trzeba go będzie optymalizować pod kontem szybkości wyświetlania. Mała, prosta aplikacja, która tylko wyświetla dostarczone dane będzie łatwa w utrzymaniu i szybka.

Panel magazynowy z kolei musi być zoptymalizowany tak aby był jak najprostszy i jak najszybszy w obsłudze. Dane o dostępności może pchać od razu do Frontu.

Aplikacje mogą dzielić się infrastrukturą - np. jednym serwerem baz. Warto jednak, żeby dane były przekazywane przez API a nie przez dzielenie się na poziomie serwera. Wtedy gdy serwer będzie wymagał rozbicia na osobne - np. wyłączenie bazy dla Frontu na osobny komputer - będziesz gotowy.

Śledzę ten wątek i szczególnie zaciekawiło mnie jedno zdanie

mógł byś rozwinąć myśl i dodać jakieś linki żeby zgłębić wiedzę ?

Nasuwa mi się takie pytanie piszę taki hobbysycznie aplikację do wymiany kruszców rozumiem* że powinienem mieć jedną aplikację do składania ofert, drugą do parowania i dokonywania transakcji oraz trzecią do administracji całym systemem.
*rozumiem = najbardziej optymalna forma.

@Bragi czy mam się zainteresować czymś jeszcze oprócz REST dla RoR?

Dobra firma która to napisze ;]

A tak na poważnie to https://github.com/spastorino/rails-api, ja bym w ogole wybral sinatre, albo goliath + Grape ale to już moje osobiste preferencje.

Ja nie mam czasu się za bardzo rozpisywać, ale pracowałem (i właściwie teraz pracuję) nad systemami złożonymi z wielu małych aplikacji i trzeba pamiętać, że takie podejście rozwiązuje problem monolitycznych aplikacji, ale wprowadza całą gamę nowych problemów. Kilka słów o sednie problemu na wikipedii: http://en.wikipedia.org/wiki/Fallacies_of_Distributed_Computing (część z tych punktów ma małe znaczenie w omawianym przypadku, ale pokazują błędny sposób myślenia).

Musisz też pamiętać, że budowanie rozproszonych aplikacji będzie miało znacznie większy koszt na początku.

Nie chcę zniechęcać do tego pomysłu, bo sam jestem zwolennikiem takich rozwiązań, ale często kiedy mówi się o takim podejściu, to ludzie zapominają o problemach jakie to ze sobą niesie.

Szkoda, że nie mam czasu na rant dotyczący grape :wink:

Co do sinatry, to bardzo ją lubię, ale jednak sinatra bez żadnych dodatków robi tylko część tego co Railsy jeżeli chodzi o obsługę HTTP, więc trzeba rozważyć na ile się zna ten temat i samemu chce robić to co railsy już mają rozwiązane. wycats kiedyś napisał tekst o tym jakie są plusy używania railsów do API, ale nie mogę tego znaleźć, jak ktoś ma linka, to poproszę o wklejenie.

No grape dodałem z racji konieczność bo Goliath od wersji 1.0 nie ma routingu już i trzeba to gdzieś rozwiązać. Można na wyższym poziomie jak nginx czy haproxy

Szkoda, że nie mam czasu na rant dotyczący grape ;)[/quote]
No to weź to napisz - chętnie poczytam, bo jak na mój ogląd Grape wygląda interesująco, ale chętnie poznałbym negatywną opinię na jego temat.

@Bragi czy mógłbyś rozwinąć w kilku zdaniach:

Szkoda, że nie mam czasu na rant dotyczący grape ;)[/quote]
No to weź to napisz - chętnie poczytam, bo jak na mój ogląd Grape wygląda interesująco, ale chętnie poznałbym negatywną opinię na jego temat.[/quote]
Właściwie to muszę doprecyzować. Źle zrozumiałem gotara i po tym dopisku o goliath rozumiem już o co chodzi. Szczerze mówiąc nie bardzo interesowałem się goliathem do tej pory, więc wydawało mi się, że chodzi o goliath + rails lub sinatra i na to jeszcze grape. Jeżeli grape ma być jedyną aplikacją na goliacie, to już trochę bardziej sensowne, aczkolwiek też nie do końca podoba mi się ta idea (pomijam już fakt, że goliath miał przez długi czas skopaną obsługę HTTP, chyba przy Connection: close, pewnie to już rozwiązali, ale wtedy ludzie się na to rzucili BO WEBSCALE I ASYNC!!111 nie patrząc na nic innego).

Co do samego grape’a, to gem sam w sobie nie jest tragiczny. Największy problem jaki z nim mam, to fakt, że jeżeli używa się go z railsami albo sinatrą, to dodaje jakiegoś bezsensownego DSLa zupełnie oderwanego od reszty aplikacji. Sam gem nie wnosi prawie nic co by mi się przydało przy robieniu API, a te rzeczy, których domyślnie nie ma (np. obsługa version) można bardzo prosto załatwić dodając gemy, które rozszerzają railsy zamiast budować cały oddzielny framework. Jak się używa grape’a to dostajesz kilka nowych metod, a reszta to reimplementacja tego co jest w railsach. Łącznie ze wszystkimi tego konsekwencjami, typu np. jest tam jakieś rescue_from, ale czy działa tak samo jak w railsach? Być może, a może nie. Dodatkowo, jeżeli aplikacja jest w railsach, to team zna już railsy. No i teraz zamiast po prostu wykorzystać to co wszyscy znają, dodaje się zupełnie nowy DSL, który w README wygląda bardzo fajnie, ale jak będzie trzeba obsłużyć jakieś edgecase’y, to trzeba będzie się uczyć jak to ustrojstwo można rozszerzyć (no i tutaj uwaga uwaga, jako, że to jest DSL i nie wiadomo jak w środku napisany, to nie wiadomo czy są rzeczy typu before_filter, albo czy tak jak w kontrolerach można po prostu zrobić include jakiegoś modułu i rozszerzyć jakieś metody). Być może da się podmontować jakiś middleware, ale tego nie wiem, bo w README nie ma o tym ani słowa.

Jedyna rzecz, której na pewno bym używał, to metoda errors, ale do tego z reguły dorzucam halt: https://github.com/strobecorp/strobe-rails-ext/blob/master/lib/strobe/action_controller/haltable.rb - łatwiej jest dodać 20 linijek niż cały nowy framework.

Ciekawa opinia - ważne jest to zastrzeżenie, że jeśli używa się Railsów, to Grape’a nie ma sensu używać. I wydaje mi się, że faktycznie w tym scenariuszu “same” Railsy dadzą radę.

Ale jeśli chcemy mieć dużo lżejszą aplikację, która nie potrzebuje wszystkich dobrodziejstw Railsowych, ale potrzebujemy jakiejś fasady do API, to użycie Grape’a jest lepsze np. niż użycie Sinatry, właśnie ze względu na ten DSL. Osobną kwestią jest oczywiście dojrzałość tej biblioteki - to pewnie wyjdzie w praniu, ale patrząc na liczbę forków to chyba nie jest tak źle.

W przypadku lekkiego API bym też bym użył Sinatry :slight_smile: Ale tutaj rzeczywiście jest trochę większy sens używnia grape’a :wink:

… to wyłączamy wszystkie komponenty Rails oprócz tych których naprawdę potrzebujemy. Właśnie otake railsy walczyli autorzy 3.0.
https://gist.github.com/1942658

A co do meritum wątku, to bardzo polecam notkę o odchudzaniu klas Activerecord:

… to wyłączamy wszystkie komponenty Rails oprócz tych których naprawdę potrzebujemy. Właśnie otake railsy walczyli autorzy 3.0.
https://gist.github.com/1942658[/quote]
Pozwolę się nie zgodzić - powiem szczerze, że w Railsach wkurza mnie routing. W sumie doszedłem do wniosku, że separowanie routera od kontrolera, to był bardzo zły pomysł. Nawet żeby zorientować się w tym, jakie są parametry muszę zajrzeć do innego pliku, niż ten, w którym one są dostępne. To jest idiotyczne. Podejście Sinatry/Grape’a jest tutaj dużo lepsze.

To samo się tyczy AcitveRecord - uważam, że podejście DataMappera, gdzie wprost definiujesz dostępne pola też pozwala znacznie szybciej zorientować się co jest dostępne. Summa summarum powstają narzędzia do anotowania klas modelu, ale nie musiałyby powstawać gdyby pola były wprost zdefiniowane.

A co do linku do “jednoplikowych” Railsów - wyobraź sobie, że zaczynam ludzi uczyć Rubiego i pokazuję taki kod - mówiąc, to sobie wpiszcie żeby dostać aplikację “Hello World”. Może nie jest to hello world w JSP, ale imho niedaleko temu do Javy. Zamiast tego mogę w Sinatrze zrobić:

[code=ruby]require ‘rubygems’
require ‘sinatra’
require ‘sinatra/reloader’ if development?

get ‘/’ do
erb :index
end

post ‘/’ do
@message = "Witaj " + params[:name]
erb :result
end[/code]
i wytłumaczyć każdą linijkę. Bez porównania.

To rozmawiamy o uczeniu ludzi Railsów czy o specjalizowanych rozwiązaniach? Bo co najmniej jeden z nas bezpardonowo offtopuje/derailuje/robi irrelewantne argumenty.