Temat: OpenCPU - krótka historia REST owej aplikacji webowej z R...

Niedawno doradzałem w kwestii projektu serwisu internetowego wykonującego pewne obliczenia i generującego raporty. Obliczenia nie były zbytnio złożone, można by je wykonać w dowolnym języku programowania z odpowiednimi bibliotekami, ale w firmie i tak jest już wykorzystywany R. Raporty miały być generowane w formacie MS Office (Excel i Word - do dalszego przetwarzania, PowerPoint - "podsumowanie dla szefa") oraz "log" w PDF (do archiwum).

Padła nieśmiertelna kwestia: "No to jak i w czym to piszemy?". Propozycji było kilka: Java ze Springiem, .NET z ASP.NET MVC, PHP z CodeIgniter albo Kohana. Było Reporting Services, Jasper Reports, a nawet Excel z Sharepointem.

Ponieważ serwis nie był dużych rozmiarów, zaproponowałem: "Napiszcie go R z użyciem HTML i JS. Będzie darmowo, ładnie, RWD (responsywnie) i "niezasobożernie". Zdziwienie było ogromne, "Ale jak to w R? Tak w samym R? A baza danych? A uwierzytelnianie? A generowanie widoków? A.......?".

"Jak to jak? Normalnie" - odpowiedziałem z uśmiechem :)

- Konektory mamy do wszystkich wykorzystywanych przez nas baz danych. Nie mówcie, że wyciągnięcia paru datasetów potrzebujecie ORMa i wzorców repozytorium :)

- Kod R i HTML maksymalnie rozdzielimy na osobne pliki. Nie mamy tutaj oczywiście typowego MVC czy MVP, ale mamy rozdzielenie logiki od widoku, a to już nieźle.

- Generowanie widoków - przecież i tak ma być z założenia statyczne, więc przygotujemy odpowiednie strony HTML dla zapytań i widoków, wykorzystamy do tego jQuery i AJAXa. Aby było responsywnie, wykorzystamy Bootstrap (bo nie macie speca od CSS żeby wam zaprojektował odpowiedni layout "z palca") i jego pakiet "kontrolek" HTMLowych. Mamy tam wszystko co potrzeba. Oczywiście jestem otwarty także na inne frameworki widoku (layout + kontrolki).

- Dokumenty generować będzie eRowy pakiet ReporteRs (pracuje na Javie). Mamy tam wszystko co potrzeba by wygenerować profesjonalnie wyglądające dokumenty, które dodatkowo można otwierać w OpenOffice (OpenXML).

- PDFowy log wygeneruje się "sam" - a to dzięki podejściu reproducible research, wykorzystamy do tego pakiety knitr i pandoc

- uwierzytelnianie - RESTowo. Żadnych "sesji" na serwerze. Wszystko po stronie klienta. Użytkownik podaje login, hasło, serwis zwraca mu token, którym ten się posługuje podczas kolejnych wywołań usług serwisu. Tak naprawdę użytkownik nie musi nawet pamiętać, aby się zalogować, bo jak wykona jakąś akcję nieuwierzytelniony (z pustym tonekem), system sam go przeniesie na stronę logowania.

Wylogowanie? Zamknięcie okna przeglądarki. Zabije to "sesję klienta" i token zostanie utracony

- W ogóle wszystko ma być RESTowo, dzięki czemu nie będziemy się martwić co z "osieroconymi sesjami R na serwerze" (a takie sesje trzymają połączenia do bazy danych, datasety obiekty pomocnicze, załadowane biblioteki) i nie będziemy ich musieli samodzielnie "ubijać" ani martwić się o timeouty - proces R zostanie "ubity" po obsłużeniu zapytania. Każde wywołanie usługi serwisu ma jej dostarczać wszystkiego, czego ona potrzebuje do wykonania zadania, a ona zwróci wyniki. Dzięki temu nie zajedziemy pamięci.

Dla wygody użytkownika, który może zechcieć wrócić do wyników wielokrotnie w ciągu dnia, pójdziemy krok dalej i wprowadzimy niejako "równolegle" pseudosesje, w których będziemy przechowywać wygenerowane wyniki wraz z tokenem. Taka pseudosesja trwać będzie np. 24 godziny i zajmie bardzo mało pamięci, bo raptem tyle, co na wyniki. Pod pojęciem "wyniki" rozumiem nie megabajtowego rozmiaru datasety, tylko finalne tabelki, która mają trafić do dokumentu.

- Wydajność? Tak, tu jest mały szkopuł, bo zamiast mieć jedną sesję R z wczytanymi uprzednio bibliotekami, musimy ją tworzyć za każdym razem. Wczytywanie bibliotek kosztuje. ALE - z drugiej strony nie trwa to długo (raptem sekundę, może 2), a poza tym sama odpowiedź bazy danych na przeciętne zapytanie SQL z waszych raportów trwa dłużej, niż załadowanie procesu R. Użytkownicy nie potrzebują tutaj milisekundowych odpowiedzi, to nie Facebook :]

Jeśli jednak skorzystamy z proponowanego przeze mnie rozwiązania, będziemy mogli skorzystać z opcji "preload", która pozwoli załadować wybrane pakiety do "globalnego cache", co zlikwiduje problem :)

A poza tym jest jeszcze jedna, niezaprzeczalna zaleta podejścia RESTowego. Jeśli R się wysypie (a potrafi się wysypać, podobnie jak Excel, SAS czy dowolny inny program), to nie "zamorduje" wyników innym użytkownikom, którzy może czekali na nie już kilka ładnych minut. Po prostu - padnie proces tego jednego konkretnego użytkownika, reszta będzie sobie pracować dalej. Tego nie da się przecenić.

---

"OK, powiedział jeden z eRowych speców, czyli RApache? Shiny? RServe?"

Nie - pokręciłem głową.

RApache - tak i nie. Sam w sobie jest naprawdę fajny i świetnie
integruje się z Apachem. Co bardzo ważne, wykorzystuje apache2-mpm-prefork, co pozwoli korzystać bezpiecznie i stabilnie z osobnych procesów dla każdego wywołania Niestety, tworzenie aplikacji z użyciem wyłącznie RApache przypomina dawne złe praktyki - mix HTMLa z kodem
R. Do "internetowego reproducible research" nadaje się wspaniale, do prostych aplikacji również, ale do naszych celów - nie. Nie samodzielnie. Użyjemy go jednak "nie wprost", ponieważ jest on wykorzystywany przez rozwiązanie, które podam na końcu.

Shiny - w wersji darmowej uruchamia jedną instancję R per aplikacja. Oznacza to, że bezpiecznie pracować może tylko jeden użytkownik. Jeśli będzie ich więcej - "pogryzą się" wzajemnie. No i nie jest RESTowy. No i nie ma SSLa.

RServe - obsługuje co prawda sesje użytkowników, ale musiałbyś napisać osobnego klienta, np. okienkowego. No i nie jest RESTowy.

Jest tylko jedna opcja: OpenCPU (opis: http://arxiv.org/abs/1406.4806 ) , oparty o wspomniany wcześniej RApache (z apache2-mpm-prefork). Tutaj macie nieco przykładów: https://www.opencpu.org/apps.html

A tutaj piękny przykład separacji widoku od kodu serwisu: https://github.com/opencpu/VisiStat/tree/master/inst/www

Jedna wada: niestety, działa tylko na Linuksie.

"Łe... to mamy stawiać osobną maszynę tylko pod raporty?"
Ależ skąd! Postawimy środowisko robocze na maszynie wirtualnej, a ona sobie chodzić na Windows Server.

Aczkolwiek.... uważam, że TAK, powinniście postawić osobną maszynę do raportów z JEJ WŁASNĄ bazą danych zasilaną z bazy macierzystej za pomocą ciągłej replikacji (albo okresowych snapshotów). Dobrą praktyką jest rozdzielanie serwerów aplikacji, bazy danych i raportowania, ponieważ źle napisane raporty nie "zabiją" wydajnościowo bazy danych głównego oprogramowania ani procesora serwera, który je obsługuje.

...chwila konsternacji...

"Robimy!" :)Ten post został edytowany przez Autora dnia 28.09.15 o godzinie 03:19
Kamil Bęczyński

Kamil Bęczyński R, SAS, analizy

Temat: OpenCPU - krótka historia REST owej aplikacji webowej z R...

Cześć, nie jestem specem, ostatnio postawiono R na serwerze na moje żądanie, gdyż się osobiście na integracji nie znam. Zastanawiam, się czy dobrze zrozumiałem cały mechanizm. Najpierw jest html + uwierzytelnianie, następnie wywoływany jest R uruchomiony na OpenCPU - ? W jaki sposób komendy w R są wywoływane ? czy jest to uruchomienie w trybie wsadowym, czy może interaktywna sesja z użytkownikiem również jest możliwa ?

Temat: OpenCPU - krótka historia REST owej aplikacji webowej z R...

Działa to tak, że OpenCPU wystawia po HTTP funkcje znalezione w pakietach R, pozwala je wykonać, a następnie zwraca ścieżki do tymczasowych obiektów wyników (wynik, wyjście konsoli, wyjscie konsoli błędów itd).

Ponieważ OpenCPU działa bezstanowo (nie ma tam czegoś takiego jak "sesja użytkownika"), za każdym razem musisz dostarczyć do funkcji wszystkiego, czego ona potrzebuje by wykonać zadanie (tzw. "unit of work").
W jaki sposób komendy w R są wywoływane ?

Przez wywołanie za pomocą protokołu HTTP (z odpowiednim rodzajem żądania - GET lub POST) odpowiednich adresów na serwerze odwołujących się do pakietów i funkcji. Parametry przekazuje się POSTem.

http://adres.serwera/ocpu/library/nazwa_pakietu/R/nazwa_funkcji


W celu dostarczenia Twoich własnych funkcji, musisz utworzyć standardowy pakiet (bibliotekę) R. Idealnie nadaje się do tego RStudio. Dla Linuksa tworzymy pakiety źródłowe, nie binarne. Po wszystkim wrzucamy pakiet tar.gz na serwer i instalujemy go lokalnie jak każdy inny:

install.packages("/home/user/pakiet.tar.gz", type="source", repos=NULL)


Od tej pory możemy wywoływać jego funkcje.

Jak je wywołasz zależy od tego co chcesz zrobić - czy masz gotowego klienta webserwisu, czy np. budujesz stronę www. Wtedy strona ta jest częścią pakietu (katalog ../inst/www) i w jej obrębie wywołujesz odpowiednie funkcje.

Aby użyć tego na stronie www, wykorzystuje się dostarczoną bibliotekę napisaną w JS, która pozwala na asynchroniczne wywołanie funkcji R i zwraca bądź sam wynik, bądź tymczasową sesję z w/w zawartością. Wyniki obliczeń można pobrać do wyświetlenia jako string lub dalszego przetwarzania w formacie JSON.

GET (zwraca zawartość funkcji; opisuje ją)

Obrazek


POST (uruchamia funkcję)

Obrazek


Obrazek


Obrazek


minimalistyczny przykład wywołania funkcji z poziomu JavaScript:

Obrazek


Pytasz o uwierzytelnianie - otóż OpenCPU wystawia "wszystko dla wszystkich".

Jeśli chcesz uzyskać efekt uwierzytelniania, musisz je zaimplementować sam. Przy czym odpada jakiekolwiek "logowanie na stałe" trzymające sesję użytkownika, ponieważ sesja R będzie "ubita" natychmiast po zakończeniu wykonywania przez nią zadania, a wszystkie dane - utracone. Chyba, że będziesz trzymał samodzielnie sesje w jakiejś bazie danych - ale wtedy zalety "bezstanowości" przepadają bezpowrotnie.

Robi się to np. tak: tworzy się okno dialogowe z polami login/hasło. Po naciśnięciu przycisku autoryzacji następuje wywołanie funkcji R z loginem i hasłem jako parametrami, która liczy z tego hash (pakiet digest). Następnie sprawdza, czy taki hash jest już w bazie (użytkownik istnieje, hasło poprawne), a jeśli jest, to go zwraca jako token uwierzytelniający. Każda funkcja mojego serwisu przyjmuje hash jako parametr, bez niego (lub gdy jest błędny) zwraca błąd. W ten sposób masz uwierzytelnioną każdą operację. Oczywiście jest tu pewien narzut czasowy, jednak w moim rozwiązaniu absolutnie nie stanowi on problemu. Wszystko zależy od optymalizacji i likwidacji dodatkowych odwołań do serwera.

Ktoś powie, że przesyłanie za każdym razem tokena jest:
a) niebezpieczne
b) nieoptymalne.

Otóż nie. Wszystko zależy, jak przesyłamy. Jeśli open textem (bez szyfrowania, http), to nie ma różnicy, podamy hasło czy token - dane są widoczne podczas przesyłania. Do tego używa się SSL (https), który jest obsługiwany przez OpenCPU. Wtedy hasło/hash są szyfrowane i nie idą open textem po sieci.

Co do "nieoptymalne" - a dlaczego? Po pierwsze user "loguje" się tylko raz (token jest pamiętany po stronie klienta). Po drugie, jeśli jest to zrobione dobrze, to autentykacja daje narzut mniejszy, niż użytkownik "ogarnia" (klika, odczytuje, wybiera opcje) interfejs :) Tak się obecnie tworzy serwisy usług i wszystko jest OK.

czy jest to uruchomienie w trybie wsadowym, czy
może interaktywna sesja z użytkownikiem również jest możliwa ?

Jeśli chcesz wykonać coś w trybie wsadowym, masz dwie opcje:

- napisać funkcję która po prostu wykona wszystko i zwróci wynik
- napisać funkcję, która pobierze z zewnątrz plik z poleceniami R, wykona je (source, ew. RScript) i zwróci wynik.

Jeśli czujesz się dobrze w pisaniu aplikacji JS, możesz spróbować napisać taki online'owe GUI do pracy w trybie interakcyjnym, gdzie każda komenda będzie odpowiednio przygotowywana i wysyłąna na serwer. Będzie to jednak masakryczna robota.

Temat: OpenCPU - krótka historia REST owej aplikacji webowej z R...

Ogólny przepis na aplikację:

0. obowiązkowo zapoznaj się z przykładowymi aplikacjami na stronie projektu
1. utwórz pakiet R
2. dodaj katalog /inst/www (w nim będą wszelkie HTMLe, JSy, CSSy, obrazki i inne zasoby)
2.a utwórz w nim stronę HTML (np. index.html)
2.b. wypełnij stronę statyczną treścią, a następnie użyj JavaScriptu (np. za pomocą jQuery, angularJS) do wysyłania zapytań na serwer i - po odebraniu wyników - aktualizacji tej treści w celu ich wyświetlenia.
3. pamiętaj, że każda funkcja ma być samowystarczalna, więc musisz jej przekazywać komplet informacji (poczytaj też o "function chaining" na stronie projektu, który ułatwia przekazywanie obiektu tymczasowej sesji między funkcjami wywoływanymi z zewnątrz)
4. jeśli chcesz coś przechowywać w bazie danych, użyj baz serwerowych, jak PostgreSQL, MySQL. Jeśli coś modyfikujesz w bazie, używaj transakcji (pamiętaj, że na aplikacji może pracować wielu użytkowników na raz)
5. zapomnij o pakietach wykorzystujących Javę - nie uda się. Korzystaj z nie-Javowych odpowiedników. Zapomnij o ReporteRs, xlsx (użyj openxlsx) czy JDBC.
6. możesz tworzyć procesy potomne lub korzystać z bibliotek, które to robią (np. rmarkdown::render uruchamia w pewnym momencie pandoc).

Temat: OpenCPU - krótka historia REST owej aplikacji webowej z R...

I kolejna nowość: http://deployr.revolutionanalytics.com/

Nie testowałem jeszcze, ale wygląda mocarnie :)

Temat: OpenCPU - krótka historia REST owej aplikacji webowej z R...

Podczas projektowania aplikacji nieodłącznie pojawia się problem uwierzytelniania użytkowników. Mamy do wyboru dwie opcje: przechowywać dane użytkowników lokalnie we własnej bazie danych albo użyć istniejących, centralnych repozytoriów, np. wszechobecny Active Directory.

R posiada odpowiednią bibliotekę, RCurl, stanowiącą wrapper dla (jak można się domyślać) biblioteki libcurl.

I o ile pod Windows działa to bezproblemowo (musimy tylko umieć parsować wyjście), to pod Linuksem już niekoniecznie. RCurl (oraz system("curl ...")) może nam "zawisnąć na wieki". Nie będę się zagłębiał w szczegóły techniczne, ale warto pamiętać, że CURL + LDAP pod Linuksem może oznaczać kłopoty.

Istnieje jednak alternatywne rozwiązanie - program ldapsearch z pakietu OpenLDAP. Istnieją wersje skompilowane dla Linuksa i Windowsów. Pod Debianem sprawa jest prosta: apt-get install ldap-utils,

Szablon:
userDataRaw <- system('ldapsearch -H ldap://...ip... -u -LLL -b "dc=company,dc=com" -w "password" -x -D "user@domain.com" -s sub "(&(objectCategory=person)(userPrincipalName=*someuser@domain.com*))" userPrincipalName msExchShadowGivenName msExchShadowSn sAMAccountName', intern = T)

> userDataRaw
[1] "dn: CN=Olszewski\\, Adrian,OU=xxxx,DC=xxxx,DC=com" "ufn: Olszewski\\2C Adrian, xxxx, xxxx.com"
[3] "sAMAccountName: xxxx" "userPrincipalName: xxxx@xxxx.com"
[5] "msExchShadowSn: Olszewski" "msExchShadowGivenName: Adrian"
[7] "" "# refldap://ForestDnsZones.xxxxx.com/DC=ForestDnsZones,DC=xxxxx,DC=com"
[9] "" "# refldap://DomainDnsZones.xxxxx.com/DC=DomainDnsZones,DC=xxxxx,DC=com"
[11] "" "# refldap://xxxxxx.com/CN=Configuration,DC=xxxxx,DC=com"
[13] ""


Przykład - zapytanie o użytkownika
ldap_host  <- paste0("-H ldap://", ldapAddr)
ldap_user <- paste0("-D \"", user, "\"")
ldap_pswd <- paste0("-w \"", password, "\"")
ldap_base <- paste0("-b \"dc=company,dc=com\"")
ldap_query <- paste0("-s sub \"", query, "\"")
ldap_attr <- paste(attributes, collapse=" ")
ldap_opts <- "-u -LLL -x"

userDataRaw <- system(paste("ldapsearch", ldap_host, ldap_base, ldap_user, ldap_pswd, ldap_opts, ldap_query, ldap_attr), intern = T)
operStatus <- attr(userDataRaw, "status")
userDataRaw <- userDataRaw[grepl(paste0("^(", paste0(attributes, collapse="|"), ").*"), userDataRaw)] # filter the output to selected attrs only
userDataRaw <- strsplit(userDataRaw, ":") # get key-value pairs
userData <- data.frame(t(sapply(userDataRaw, function(l) str_trim(l[2]))))
names(userData) = sapply(userDataRaw, function(l) str_trim(l[1]))
return(list(status=operStatus , data=userData))


Prosto, łatwo i przyjemnie odpytujemy AD przez LDAP spod Windowsa i Linuksa :)

RCurl niestety, nie pomoże nam tutaj zbytnio, chyba, że pracujemy tylko pod Windows.Ten post został edytowany przez Autora dnia 01.02.16 o godzinie 12:51

Temat: OpenCPU - krótka historia REST owej aplikacji webowej z R...

Po długich cierpieniach...

Tadam! OpenCPU na Raspberry PI. Brakowało mi już tylko tego do pełnego wykorzystania możliwości tego komputerka w roli "czarnej skrzynki" do gromadzenia i transformacji danych, analiz przeczesujących i raportowania.


Obrazek
Ten post został edytowany przez Autora dnia 27.07.16 o godzinie 14:44

Następna dyskusja:

R w tle jako unixowy demon?




Wyślij zaproszenie do