Azure funkcije, ili — Kako kupiti njivu preko SMS-a?

Tekst Branka Kokanovića o isprobavanju mogućnosti Azure funkcija i automatizaciji dobijanja obaveštenja za dva različita slučaja.

Branko Kokanović - 19. April, 2017.

Imao sam dva skroz dijametralno suprotna use case-a gde sam želeo da dobijem brza obaveštenja (po mogućstvu SMS) o određenim oglasima. Na primer, hteo sam da kupim njivu, ali sam shvatio da mi je okej 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 veoma jeftin telefon (koji se obično proda za 2-3 časa od objavljivanja).

Rešio sam da oba problema rešim na isti način, koristeći što je više moguće iz Azure stacka. Ne, možda se nismo razumeli, stvarno sam overarchitectovao š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), su Microsoftov 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:

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 uploadujem fajl FTP-om, a čak i Kudu radi sa adrese https://<function_app_name>.scm.azurewebsites.net).

Sve u svemu, rešenje je bilo prilično očigledno – skrejpuj sajt unutar Azure Functionsa, 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 bi trebalo 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

Okej, sada kad znamo gde je state, kako Azure funkciji 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 nekom 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>. Ovako 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 Microsoftovo 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 Busa, proveri da li zadata ponuda postoji već u Table Storage-u, a ima dva outputa – 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 overarchitectovani Frankenštajn:

arch-1

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 + 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:

Ako vas zanima neki detalj o ovome što sam pričao, ceo source code je dostupan na GitHubu.

Evo i za kraj kako to izgleda na portalu:

Capture


Tekst je u originalu objavljen na blog.kokanovic.org, a prilagođen je i objavljen na Startitu uz dozvolu autora.