Imao sam dva skroz dijametralno suprotna use case-a gde sam želeo da dobijem brza obaveštenja (po mogućstvu SMS) o “ponudama” (oglasima). Npr. hteo sam da kupim njivu, ali sam provalio da mi je OK da čekam da se njiva pojavi na doboš preko banke. Drugi use case je da sam hteo da kupim mobilni telefon. U prvom slučaju, nisam želeo da obilazim sajt banke svaki dan za nešto što se pojavi svaka 3 meseca, a u drugom sam želeo jako brza obaveštenja kad se u ponudi pojavi jako jeftin telefon (koji se obično proda za 2-3h od objavljivanja).
Rešio sam da oba problema rešim na isti način, koristeći što više iz Azure stack-a što je moguće. Ne, možda se nismo razumeli, stvarno sam overarchitect-ovao što je više moguće, koristeći što novije i što hipsterskije tehnologije.
<cinizam>
da se ne lažemo, sve ove tehnologije su stare koliko i računarstvo, samo su implementacije drugačije…
</cinizam>
Ovo nije bilo uzaludno iz dva razloga: prvo, naučio sam dosta novih stvari, a i ovakvo rešenje bi imalo smisla za neke veće sisteme, gde skaliranje stvarno može biti problem (ali ne, definitivno ne i za kupovinu njive).
Jedini constraint koji sam imao je da želim da isprobam Azure Functions – serverless ekvivalent AWS-ovim Lambdama. Međutim, bilo je tu par problema, pa evo da prođemo sve natenane.
Šta su Azure funkcije
(preskočite ovaj pasus ako ste upoznati sa Azure funkcijama, ovo je samo uvod)
Azure Functions (ili Azure funkcije, po naški), su Microsoft-ov odgovor na AWS Lambde. Način da se implementira stateless arhitektura što, u zavisnosti od vaše biznis logike, može dosta da uštedi para (plaćate samo izvršavanje funkcija, ali ne i ceo VM koji zvrji prazan ostatak vremena). Slanje SMS-a i skrejpovanje sajtova je baš dobar primer. Ako želite više da znate o Azure funkcijama, krenite odavde.
<cinizam>
Opet moram da se umešam. Nema ništa inherentno stateless u stateless arhitekturi, samo vi ne morate da brinete o tome. Sviđa mi se kako je to sročeno ovde: Serverless = “someone else is responsible for these servers going down”
</cinizam>
Ukratko objašnjenje je da vi pišete samo jednu funkciju (u jeziku u kom hoćete, dosta ih je podržano), i definišete šta je:
- trigger (kada se funkcija startuje, može biti npr. tajmer, može biti kad stigne nešto na message queue…)
- input (šta vam je ulaz u funkciju, može biti npr. blob, Document DB dokument…)
- output (šta je izlaz iz funkcije; može biti sve što i input, ali i razne druge stvari, kao što su HTTP request, slanje mail-a, ili ono što ćemo mi ovde iskoristiti – slanje SMS-a preko Twilio platforme
Za sva ova tri gorepomenuta (trigger, input i output) niste ograničeni na samo jedan, već ih može biti i više od jednog (npr. output je u našem slučaju i Table Storage i Twilio SMS, videćete kasnije), i svi su definisani kao argumenti u toj vašoj funkciji koju pišete.
Ima tu još par gremlina na koje sam naletao, ali ih i dosta brzo rešavao uz pomoć dokumentacije i SO-a, tipa kako da dodaš nove nuget pakete, kako da funkcije dele zajednički kod, a pošto Azure funkcije koriste web apps ispod, sve ostala pitanja su dobila automatski odgovor (kao kako da postavim environment variable, kako da upload-ujem fajl FTP-om, a čak i Kudu radi sa adrese https://<function_app_name>.scm.azurewebsites.net
).
Overall, rešenje je bilo prilično očigledno – skrejpuj sajt unutar Azure Functions-a, sačuvaj to negde i pošalji SMS. Al’ ne lezi vraže…
Gde čuvati state sistema u stateless arhitekturi
Prvi problem ovde je bio gde čuvati state sistema, tj. gde čuvati već obrađene njive, odnosno telefone. Od ponuđenih opcija, Azure Functions je nudio Document DB i Table Storage, ali ne i npr. SQL. Hteo sam nešto jeftino i lightweight, pa sam se odlučio za Azure Table Storage (Document DB mi je bio preskup za ovu namenu, mada mislim da bi on bio bolji izbor kad bi ovo trebalo da skalira).
Jedna zanimljiva stvar na koju treba paziti i koja može da vas ujede je da Azure Functions može pokrenuti vašu funkciju više puta konkurentno. Mislite o tome ako vam treba atomičnost. Prost primer je da se dve funkcije pokrenu paralelno, u funkcijama se skrejpuju iste ponude, onda se provere u obe da li postoje u Table Storage-u, i pošto ne postoje, da se pošalje SMS dva puta iz obe. Pazite se!
Kako merge-ovati ponude
OK, sada kad znamo gde je state, kako Azure Function-u da kažemo da ne želimo da ubacimo novi red ako postojeći već postoji. Ispostavlja se da tako nešto ne postoji (što ima logike pošto Azure Functions radi samo sa nekim ulaznom i izlaznom komponentom, ne sa hipotetičkim ulazom koji zadovoljava neki query). Tu sam morao da zasučem rukave i da napravim upit koji će proveriti da li postoji već takva ponuda u Azure Table Storage-u. Da se razumemo, nije ovo teško, nego nije bilo u duhu Azure funkcija. Što se tiče primarnog ključa u Table Storage-u, on podržava dva odvojena entiteta – PartitionKey i RowKey (pretpostavljam da su značenja jasna iz imena). Nekako mi je bilo logično da za PartitionKey postavim sajt sa koga skidam ponudu, tj. tip ponude (njive, telefoni, avioni, kamioni…), a da RowKey dobijem od sajta i da on bude specifičan za datu ponudu.
Ima ovde još jedna bitna stvar vredna pomena. Azure funkcije rade po principu da, ukoliko ste naveli neki output (Table Storage, u našem slučaju), on nije opciona stvar, već Azure očekuje da prosledite novi red i tačka. U ovom slučaju, mi želimo red samo ako ponuda ne postoji već u tabeli. Srećom, Azure funkcije podržavaju ovo, i to tako što output funkcije nije objekat T
koji se upisuje u tabelu, već ICollector<T>
. Ovakav napravljena kolekcija dozvoljava da krajnji izlaz bude i 0 redova, ali i više od jednog reda!
Decoupling različitih tipova ponuda
Osnovna ideja je da možemo da imamo različite tipove ponuda (njive, telefoni) koje prolaze kroz sistem. Malo bi glupo bilo da sva skrejpovanja svih sajtova budu u istoj Azure funkciji. Takođe, period skrejpovanja za mobilne telefone (npr. 15 minuta) nije isti kao i za njive (max. jednom dnevno). Naravno, kad god je ovakav decoupling u pitanju, uvek je odgovor naš dobar drugar message queue. Azure Functions podržava taj scenario (queue može da bude trigger), a Microsoft-ovo rešenje se zove Azure Service Bus. Naravno, format ponuda koje ćemo trpati u queue će biti JSON (zato što je XML so 1990s). Sada je iz aviona jasno rešenje – napraviti N različitih Azure funkcija, gde će svaka da skrejpuje po jedan tip sajtova sa ponudama, trigger će im biti tajmer, a output će im biti Azure Service Bus. JSON koji pumpaju u Service Bus može da bude kakav god dictionary, ali mora da ima ključeve “partition” i “id” u njemu. Sa druge strane queue-a je funkcija kojoj je trigger Service Bus, a kojoj je zadatak da čita ponudu iz Service Bus-a, proveri da li zadata ponuda postoji već u Table Storage-u, a ima dva output-a – jedan je opet Table Storage (opcioni, i ima ga samo ako se pojavi nova ponuda koja treba da se upiše), a drugi je, takođe opcioni Twilio SMS servis.
Krajnje rešenje
Posle svih ovih problema, evo i šematski prikaz kako izgleda ovaj moj overarchitect-ovani Frankenštajn:
Jeste ružno, ali i treba da bude ružno. Inače, cela ova zezancija košta oko 1$ mesečno (plus 1$ za Twilio SMS servis), što je mnogo manje nego da sam uzeo VM na koji bih npr. potrpao neki Python plus Mongo. Nisam ekspert, a nisam ni probao AWS Lambdu da bih dao bolji pregled i imao bolju referentnu tačku, ali evo stvari koje treba da se unaprede, po meni:
- Podrška za SQL (ima nas matorih koji bi ipak koristili SQL) i druga NoSQL rešenja
- Podrška za lokalno testiranje (to danas nije moguće)
- Twilio SMS servis nije radio, morao sam “ručno” da ga dodajem (ovaj OOB je imao problema) – loš prvi experience za Micorosft i Azure.
Ako vas zanima neki detalj o ovome što sam pričao, ceo source code je dostupan na GitHub-u. Ako vas ne zanima nijedan detalj i sve vam je jasno, možete uvek otići na ovaj link da vidite moj broj telefona i da mi me pozovete i zahvalite na kristalno jasnom opisu Azure funkcija:)
Evo i za kraj kako to izgleda na portalu: