Škálovatelná rasová evidence prakticky

Posted 23. 07. 2017 / By Petr Soukup / Cloud

Narazil jsem teď na zajímavý web vyfot-imigranta.cz - když na ulici potkáte někoho, kdo je tmavší než vy, tak ho vyfotíte a spolu s adresou nahrajete na web. Jenže to funguje jen dokud jich je málo a co budete dělat, až přijde ta slibovaná invaze? Ukážeme si, jak takové nahrávání fotek vyřešit škálovatelně.

Předpokládejme tedy, že máme tisíce fotek a potřebujeme je nahrávat do online služby. Fotky můžou vznikat v různě velkých dávkách a služba může být také přetížená, takže si s tím musíme také poradit. Také se může stát, že budeme potřebovat nahrávat desetitisíce fotek, ale nechceme investovat do nějakých serverů - cloud FTW.

Disclaimer: Ehm... je vám doufám jasný, že to nemyslím vážně... Ale pro ukázku AWS je to hezký příklad.

Struktura aplikace

Aplikaci rozdělíme na dvě části - zpracování vstupních fotek a upload fotek na web. Skript v AWS Lambdě zpracuje fotky a uloží je do SQS fronty. Odtud si je vyzvedne druhá Lambda a nahraje je na web. Tyto dvě části jsou na sobě nezávislé a každá může pracovat svým vlastním tempem.

AWS SQS je primitivní služba na fronty, která vám vyřeší spoustu problémů. Z jedné strany do ní můžete sypat data (například JSON) a jiným skriptem si je můžete vyzvedávat. Narozdíl třeba od databáze u SQS nemusíte řešit škálování (to řeší AWS), platíte pouze za requesty ($0.4 za milion requestů) a nemusíte vůbec řešit zámky atd. Bez práce tak máte rovnou vyřešeno, že data může zpracovávat více workerů paralelně, aniž by tím vznikl problém.

Vstupní data - download fotek

Za současné situace těžko splašíme dost fotek migrantů, aby na to mělo smysl psát nějaký skript. Prozatím si tak pomůžeme fotky vakoveverek z Flickeru. Proč zrovna vakoveverek? Protože na to už mám hotové rozšíření do Chrome a tak ten kousek kódu zrecykluju :)

const request = require('request');
const AWS = require('aws-sdk');
const sqs = new AWS.SQS({apiVersion: '2012-11-05'});

exports.handler = function (event, context, callback) {
    request({
        url: 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=0fa629c6c4xxxxxa5a7427b708208a03&' +
        'text=%22sugar%20glider%22&' +
        'sort=relavance&' +
        'min_taken_date=' + (Math.floor(Date.now() / 1000) - 60 * 60 * 24) + '&' +
        'per_page=200&' +
        'content_type=1&' +
        'format=json',
        method: 'GET'
    }, function (error, response) {
        if (error || response.statusCode < 200 || response.statusCode > 299) {
            return console.log(error);
        }
        var photos = JSON.parse(this.response).photos.photo;
        if (photos.length === 0) {
            return callback();
        }

        var batch = photos.map(function (photo) {
            return {
                Id: photo.id,
                MessageBody: 'https://farm' + photo.farm + '.staticflickr.com/' + photo.server + '/' + photo.id + '_' + photo.secret + '_z.jpg'
            };
        });

        sqs.sendMessageBatch({
            QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/000000/racist_photos',
            ReceiptHandle: message.ReceiptHandle
        }, callback);
    });
};

Tento jednoduchý skript si přes API Flickeru stáhne vakoveverky nahrané za posledních 24 hodin, vygeneruje URL pro jednotlivé fotky a každou pošle jako samostatnou zprávu do AWS SQS.

V AWS založíme SQS frontu a také Lambda funkci se skriptem výše. Lambdě také rovnou nastavíme Cloudwatch Event, který ji bude spouštět jednou denně. Kdybychom to chtěli chytřejší, tak by také šlo například napojit Flickr webhook, který Lambda funkci spustí - nebyla by potřeba žádná změna ve skriptu a jen bychom místo časovaného Eventu naklikali AWS API Gateway

Upload fotek

Fotky z SQS fronty lze zpracovávat různě. Je možné například nastavit CloudWatch event, který spustí Lambda funkci vždy po přidání nové fotky do SQS. Pro zjednodušení ale použijeme zase jen časované spouštění.

Na web je potřeba se přihlásit před nahráním fotky. Protože se nám ale nechce ukládat do aplikace heslo, tak místo toho pro každou fotku rovnou založíme nový účet.

const request = require('request').defaults({jar: true});
const AWS = require('aws-sdk');
const sqs = new AWS.SQS({apiVersion: '2012-11-05'});
var queueUrl = 'https://sqs.eu-west-1.amazonaws.com/000000/racist_photos'
exports.handler = function (event, context, callback) {
    sqs.receiveMessage({
        MaxNumberOfMessages: 1,
        QueueUrl: queueUrl,
        VisibilityTimeout: 60, // jak dlouho držet zámek
        WaitTimeSeconds: 0
    }, function (err, sqsResponse) {
        if (err) return callback(err);
        if (!sqsResponse.Messages || sqsResponse.Messages.length === 0) {
            return callback();
        }
        var url = sqsResponse.Messages[0];

        // stáhnout fotku
        request.get({url: url}, function (error, response) {
            if (error || response.statusCode < 200 || response.statusCode > 299) {
                return console.log(error);
            }
            var photo = response.body;

            // vytvořit nový účet
            request.post({
                url: 'http://vyfot-imigranta.cz/register',
                formData: {
                    name: 'test',
                    email: context.awsRequestId + '@example.org',
                    password: 'test-test-test',
                    password_confirmation: 'test-test-test'
                }
            }, function (error) {
                if (error) return callback(error);

                // upload fotky
                request.post({
                    url: 'http://vyfot-imigranta.cz/upload',
                    formData: {
                        location: 'Praha',
                        photo: {value: photo, options: {filename: 'photo.jpg'}}
                    }
                }, function (error) {
                    if (error) return callback(error);
                    sqs.deleteMessage({
                        QueueUrl: queueUrl,
                        ReceiptHandle: sqsResponse.Messages[0]
                    }, callback);
                });

            });

            // hotovo, můžeme smazat zprávu z fronty
            sqs.sendMessageBatch({
                QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/000000/racist_photos',
                ReceiptHandle: message.ReceiptHandle.ReceiptHandle
            }, callback);
        });
    });
};

Na skriptu jsou zajímavé dvě věci - VisibilityTimeout a mazání zprávy z fronty až na konci.

Atribut VisibilityTimeout zajistí, že skript bude mít konkrétní URL na minutu pro sebe a nebude ji stahovat jiná instance skriptu. Pokud by ale za minutu fotku nezpracoval (například dojde k chybě), tak se vrátí zpátky do fronty. Z té si ji vyzvedne zase znovu skript a zkusí zprávu zpracovat znovu. Tohle opakované zpracování není potřeba ve skriptu nijak řešit a už to tak rovnou funguje by design.

Takhle by se ale mohl točit do někonečna, takže je SQS možné nastavit, že po určitém počtu pokusů se zpráva smaže nebo se přesune do další fronty. Je tak možné snadno zařídit, že bude několik front, kdy první se zpracovává hned jak zpráva přijde, v druhé se nechá hodinu uležet, ve třetí se nechá celý den uležet a ve čtvrté proběhne jen přesun zprávy do nějakého logu. S minimálním úsilím tak jde vyřešit i výpadek cílového webu.

A to je vše - je zaveverkováno

Dva krátké skriptíky, chvilka klikání v konzoli AWS a máme hotovou škálovatelnou aplikaci. Základ pro podobné aplikace je rozdělit problém na menší samostatné části. V tomto příkladu jsou na sobě vstupní a výstupní části nezávislé. O frontu se starat nemusíme, protože ta škáluje sama. A pokud by například výstupní část nestíhala provoz, tak můžeme upravit jen tu, aniž bychom se museli ohlížet na zbytek aplikace.

Samozřejmě tady hodně zjendodušuju, ale stačí když si budete pamatovat, že AWS je lego. Není to o složitém programování, ale spojení správných kostiček za sebe. Tenhle příklad byl jen takový náhled a chystám se, že bych o tom pořádně popovídal na příští Poslední sobotě.


Potřebujete s AWS poradit? Nabízím konzultace! Posadíme se, popíšete mi svůj projekt a já vám zkusím vysvětlit, jakou cestou se můžete vydat. AWS je složený z desítek služeb, které je potřeba šikovně pospojovat za sebe, abyste cloud skutečně využili.

Tags: aws, cloud


O blogu
Blog o provozování eshopů a technologickém zázemí.
Aktuálně řeším hlavně cloud, bezpečnost a optimalizaci rychlosti.

Rozjíždím službu pro propojení eshopů s dodavateli.