Letscode.hu

… minden ami fejlesztés

Az elmúlt pár cikkben igyekeztem meglovagolni én is azt a hype-ot, ami a microservice architektúrával kapcsolatos, de eddig eléggé elméleti megközelítése volt a dolgoknak, így gondoltam most kicsit váltok és egy saját kis példát dobok össze, hogy szemléltessem a dolgot.

Amire a cikkben lévő példához szükség lesz:

  • docker (erről itt olvashattok)
  • némi kakaó a gépben, amin csináljátok, mert pár szolgáltatást feldobunk rá (nem, most nem AWS példa lesz, de igény szerint majd csinálok azt is), név szerint egy Eureka fog futni, 3×3 darab egyszerűbb Node alkalmazás, 3 key-value store, meg 3 MySQL (sima, nem cluster).
  • Node és npm.
  • Egy shell, ahol fel tudod tolni a szükséges initial SQL szerkezetet és dummy tartalmat (vagy myadmin, ha az jobban tetszik 🙂 )
  • Ha lusta vagy gépelni, akkor példakód letöltéséhez git 🙂

    microservices-aggregator-1024x528
    Forrás: http://blog.arungupta.me/microservice-design-patterns/

Akkor az első amire szükségünk lesz, hogy összetartsuk a fenti rendszert, az az Eureka lesz, aminek a telepítésével és hasonlókkal most minimálisan fogunk foglalkozni, mert Dockerben fogjuk azt futtatni, amivel a beröffentése leegyszerűsödik egy szimpla

docker run -d --name eureka-server -p 32784:8080 netflixoss/eureka:1.1.142

-ra.

Akinek nem lenne tiszta mi is ez az Eureka: Ez egy ún. service registry, ahova a különböző service-eink beregisztrálnak, aztán meghatározott időközönként küldenek életjelet magukról. Az eureka lesz az, amit aztán kérdezgetnek a service-ek, ha valamelyik másik service hol- és mibenlétéről akarnak infót. Így elkerülhető az, hogy a service-einkbe beleégessük a többiek elérését, csupán a nevükre van szükség, ami alapján az eureka-tól ki tudja annak elérését kérni.

A fenti parancs lehúzza a dockerhubról az eureka image-ét, elindítja azt eureka-server néven, mégpedig daemonként a háttérben és a 8080-as belső portra a docker0 interface 32784-es portja fog mutatni. Kell neki egy kis idő mire észhez tér, de kb. 1 perc múlva már le tudjuk tesztelni. Ha nem tudjuk, hogy hol is találhato a docker0 interface, akkor az ifconfig-al elő tudjuk halászni. Ha megvan, akkor nézzük meg, hogy működik-e a dolog. Indítsunk egy GET kérést az /eureka/v2/apps endpointra. A válaszban a következőt kellene kapjuk:

<applications>
 <versions__delta>1</versions__delta>
 <apps__hashcode></apps__hashcode>
</applications>

Üres, lévén még semmi sincs felregisztrálva oda.

Akkor jöjjenek a MySQL-ek:

docker run -d -p 12805:3306 --name accounts_mysql -e MYSQL_ROOT_PASSWORD=password mysql:latest
docker run -d -p 12806:3306 --name products_mysql -e MYSQL_ROOT_PASSWORD=password mysql:latest
docker run -d -p 12807:3306 --name orders_mysql -e MYSQL_ROOT_PASSWORD=password mysql:latest

A fenti három konténerben futnak majd elkülönítve a három külön service-t kiszolgáló MySQL-ek. Alapból expose-olják a 3306-os portot, amivel docker style össze is linkelhetnénk őket a service-ekkel, de most ennyire nem mennék bele. A lényeg, hogy ezek is a 12805-12807-es portokon elérhetőek lesznek a docker0 interface-en.

Töltsük is fel dummy adatokkal őket! A szükséges SQL dumpok megtalálhatóak a repo-ban.

cat accounts.sql | mysql --host={DOCKER0_IF} --port=12805 --password=password
cat products.sql | mysql --host={DOCKER0_IF} --port=12806 --password=password
cat orders.sql | mysql --host={DOCKER0_IF} --port=12807 --password=password

Akkor most jöjjön a cache réteg:

docker run -d -p 10564:6379 --name accounts-redis redis
docker run -d -p 10565:6379 --name products-redis redis
docker run -d -p 10566:6379 --name orders-redis redis

Persze azt mindig tartsuk észben, hogy ezek nem kötelezően egyazon gépen helyezkednek el, most csak a példa kedvéért van így

Akkor mostmár a data source réteg rendben, jöhetnek az alkalmazások, amik ezt használják!

Node.js-t fogunk használni az egyszerűség kedvéért, amihez ugye szükség lesz egy express-re, egy eureka, mysql, redis kliensre és indulhat is a menet!

Package.json tartalma:

{
  "name": "hello-eureka",
  "description": "Eureka test app",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "3.12.0",
    "eureka-js-client" : "2.4.0",
    "md5" : "2.*",
    "redis" : "2.6.0-1",
    "mysql" : "2.10.*"
  }
}

Ezzel már le is tudjuk húzni azt a pár függőséget, ami nekünk kell egy-egy service működéséhez. Az aggregátort és a load balancert majd külön tárgyaljuk. Az egyszerűség kedvéért, most egy igen egyszerű node app-ot rakunk össze, ami parancssori paraméter alapján dönti el, hogy a 3 sql/redis duóból melyikből is fog kiszolgálni, valamint itt lesz a load-balancer és a view aggregator is, de értelemszerűen ennyire nem egyszerű a helyzet a való életben 🙂

Service.js:

// a service típusa
var serviceType = process.argv.pop();

var port = 5000;

var express = require('express');
// eureka kliensünk
var eureka = require("eureka-js-client").Eureka;
// csak az egyedi hostname miatt
var md5 = require("md5");

// redis
var redis = require("redis");
var mysql = require("mysql");

// config
var config = require("./config").config[serviceType];
// eureka configja
var euConfig = require("./config").config.eureka;


var redisClient = redis.createClient({
    "host" : config.redis.host,
    "port" : config.redis.port
});

var connection = mysql.createConnection({
    port    : config.mysql.port,
    host     : config.mysql.host,
    user     : config.mysql.username,
    password : config.mysql.password,
    database : config.mysql.database
});

connection.connect(function(err) {
    if (err) {
        console.error('error connecting: ' + err.stack);
        return;
    }
    console.log('connected as id ' + connection.threadId);
});

var app = express();

var euClient = new eureka({
    // application instance information
    instance: {
        app: config.serviceName,
        hostName: md5(Date.now()),
        ipAddr: process.env.DOCKER_HOST,
        port: process.env.DOCKER_PORT,
        vipAddress: 'jq.test.something.com',
        dataCenterInfo: {
            name: 'MyOwn'
        }
    },
    eureka: {
        // eureka server host / port
        host: euConfig.host,
        port: euConfig.port
    }
});
// beregisztrálunk az eurekára és küldjük az életjeleket
euClient.start();

// ne vegyünk példát a szerkezetről, az anonymous function-öket kerüljük máskor
app.get('/', function (req, res) {
    // meglessük a cache-t
    redisClient.get("accounts", function(err, reply) {
        if (err != null || reply == null) {
    // nem visszük túlzásba a dolgokat, csak a példa kedvéért
            connection.query('SELECT * FROM ' + config.mysql.table, function(err, rows) {
                if (err) {
                    console.error('error connecting: ' + err.stack);
                    res.end();
                }
                console.log("sql-ből");
                res.end(JSON.stringify(rows));
                redisClient.set("accounts", JSON.stringify(rows));
            });
            return;
        }
        console.log("redis-ből");
        res.end(reply);
    });
});

app.listen(port);

module.exports = app;

Na most a fenti alkalmazásunkat akkor elemezgessük csak végig! Behúzzuk a szükséges függőségeket és konfigurációt, aztán felregisztrálunk az eurekára. Itt környezeti változókat fogunk majd használni, mert habár az app a konténeren belül az 5000-es porton csücsül, kívülről másik porton és címen fogjuk elérni. Ezeket a változókat majd a docker-el fogjuk átadni. Sajnos még nem találtam megoldást, hogy a docker átadná-e a dolgokat a konténernek, amivel megkönnyíthetné az életemet, de ha valaki tud ilyenről, az szóljon!

Nos ha beérkezik egy kérés a figyelt portra, akkor a hozzánk rendelt redis-ből megpróbáljuk előhalászni az adatokat, ha nem sikerült, akkor pedig szimplán lekérjük az összeset SQL-ből és elmentjük a cache-be, későbbi használatra. Pretty simple.

Önmagában nem is a service lesz itt a lényeg, szóval ezt próbáltam a lehető legegyszerűbbre hagyni.

A használt konfig (config.js):

var docker0 = "172.17.42.1"; // mivel minden a docker0-on csücsül, így egyszerűbb

exports.config = {
    eureka : {
        host : docker0,
        port : 32784
    },
    aggregator : {
        serviceName : "aggregatorService"
    },
    accounts : {
        serviceName : "accountService",
        mysql : {
            host : docker0,
            port : 12805,
            table : "accounts",
            database : "account",
            "username" : "root",
            "password" : "password"
        },
        redis : {
            host : docker0,
            port : 10564
        }
    },
    orders : {
        serviceName : "orderService",
        mysql : {
            host : docker0,
            port : 12807,
            table : "orders",
            database : "order",
            "username" : "root",
            "password" : "password"
        },
        redis : {
            host : docker0,
            port : 10565
        }
    },
    products : {
        serviceName : "productService",
        mysql : {
            host : docker0,
            port : 12806,
            table : "products",
            database : "product",
            "username" : "root",
            "password" : "password"
        },
        redis : {
            host : docker0,
            port : 10566
        }
    }
};

Szépen feldarabolva az egyes service-ekhez tartozó konfigurációk. Alapból mindenhova így adnánk meg az elérést, de most a service registry miatt nem kell annyira belemennünk.

Most, hogy a service-ek megvannak, nem árt beletennünk őket egy-egy konténerbe. Ehhez szükségünk lesz egy Dockerfile-ra:

FROM node:latest
MAINTAINER fejlesztes@letscode.hu
# set default workdir
WORKDIR /usr/src
# Add package.json to allow for caching
COPY package.json /usr/src/package.json
# Install app dependencies
RUN npm install
# Bundle app source and config
COPY config.js /usr/src/
COPY service.js /usr/src/
COPY load-balancer.js /usr/src/
COPY aggregator.js /usr/src/

# user to non-privileged user

USER nobody
# Expose the application port and run application
EXPOSE 5000
# Itt van az initial command
CMD ["node","service.js", "products"]

A fenti fájlt majd szükséges lesz módosítanunk az egyes buildek előtt, mert a service.js-nek átadott paraméter változni fog majd, de most írjuk be a következő parancsot:

docker build -t product-service .

Majd írjuk át a CMD sor utolsó paraméterét orders-re és futtassuk újra:

docker build -t order-service .

Ezután a paramétert írjuk át accounts-ra és újra:

docker build -t account-service .

Most, hogy a három kis kiszolgáló kész van, jöjjön az, ami egybevarázsolja a dolgokat, a view aggregátor. Ez is egy eléggé fapados cucc lesz, csak szemléltetni az aggregator.js:

var express = require('express');
// eureka kliensünk
var eureka = require("eureka-js-client").Eureka;
// csak az egyedi hostname miatt
var md5 = require("md5");
var config = require("./config").config.aggregator;

// eureka configja
var euConfig = require("./config").config.eureka;

var http = require("http");

var app = express();

var hostName = md5(Date.now());
var euClient = new eureka({
    // application instance information
    instance: {
        app: config.serviceName,
        hostName: hostName,
        ipAddr: process.env.DOCKER_HOST,
        port: process.env.DOCKER_PORT,
        vipAddress: 'jq.test.something.com',
        dataCenterInfo: {
            name: 'MyOwn'
        }
    },
    eureka: {
        // eureka server host / port
        host: euConfig.host,
        port: euConfig.port
    }
});

euClient.start();

// a kezdőindex
var i = 0;
// ez fog kidobni nekünk egy instance-t a service-ből
function getWorkingInstance(name) {
    var instances = euClient.getInstancesByAppId(name);
    var ret = [];

    if (instances) {
        // előszűrűnk, hogy csak a működőek legyenek benne
        instances.forEach(function(instance) {
            if (instance.status !== "UP") {
                return;
            }
            ret.push({
                // csak a host és a port érdekel minket
                "host" : instance.ipAddr,
                "port" : instance.port.$
            });
        });
    }
    i = (i >= ret.length -1) ? 0 : (i + 1);
    return ret[i];
}

function getPromiseWithData(hostPortConfig, fieldName) {
    return new Promise(function(resolve, reject) {
        var request = http.get({
            "host":  hostPortConfig.host,
            "port": hostPortConfig.port,
            "path": "/"
        }, function(response) {
            var data = "";
            response.on("data", function(chunk) {
                data += chunk;
            });
            response.on("end", function() {
                var response = {
                    "key" : fieldName,
                    "data" :JSON.parse(data)
                };
                resolve(response);
            });
        });
        request.on("error", function(err) {
            reject(err);
        });

        request.end();
    });
}

app.get("/", function(req, res) {
    var productService = getWorkingInstance("productService");
    var orderService = getWorkingInstance("orderService");
    var accountService = getWorkingInstance("accountService");

    var product = getPromiseWithData(productService, "products");
    var account = getPromiseWithData(accountService, "accounts");
    var order = getPromiseWithData(orderService, "orders");

    Promise.all([product, account, order]).then(function(values) {
        var responseObj = {};
        values.forEach(function(item) {
            responseObj.hostId = hostName;
            responseObj[item.key] = item.data;
        });
        res.end(JSON.stringify(responseObj));
    }, function(err) {
        console.log(err);
        res.end(JSON.stringify({
            "error" : "Sorry, we cant fulfill your request!"
        }));
    });
});

app.listen(5000);

module.exports = app;

Szintén az 5000-es porton ül belül. Feliratkozik az Eurekára, mint a többiek. Ha beérkezik egy lekérdezés, akkor meghívja az Eureka-t és kikéri a három service példányait. Azoknak egy-egy lekérést indít és a végén ezeket bevárva visszaad egy választ. Ha valamelyik hibára fut, akkor hibaüzenettel tér vissza (igen, egy fokkal szofisztikáltabb megoldás lenne, ha csak annak az egy service-nek az adatai hiányoznának a válaszból, tudom).

Ezután a paramétert írjuk át az utolsó sort így és újra:

CMD ["node","aggregator.js"]
docker build -t aggregator-service .

És a legvégén a load-balancer.js tartalma:

var http = require('http');
// eureka kliensünk
var eureka = require("eureka-js-client").Eureka;
// csak az egyedi hostname miatt
var md5 = require("md5");
// eureka configja
var euConfig = require("./config").config.eureka;

var euClient = new eureka({
    // application instance information
    instance: {
        app: "balancer",
        hostName: md5(Date.now()),
        ipAddr: '127.0.0.1',
        port: 5000,
        vipAddress: 'jq.test.something.com',
        dataCenterInfo: {
            name: 'MyOwn'
        }
    },
    eureka: {
        // eureka server host / port
        host: euConfig.host,
        port: euConfig.port
    }
});

euClient.start();

// a kezdőindex
var i = 0;
// ez fog kidobni nekünk egy instance-t a service-ből
function getWorkingInstance(name) {
    var instances = euClient.getInstancesByAppId(name);
    var ret = [];

    if (instances) {
        // előszűrűnk, hogy csak a működőek legyenek benne
        instances.forEach(function(instance) {
            if (instance.status !== "UP") {
                return;
            }
            ret.push({
                // csak a host és a port érdekel minket
                "host" : instance.ipAddr,
                "port" : instance.port.$
            });
        });
    }
    i = (i >= ret.length -1 ) ? 0 : (i + 1);
    return ret[i];
}

http.createServer(function (req, res) {
    var instance = getWorkingInstance("aggregatorService");
    var request = http.get({
        "host" : instance.host,
        "port" : instance.port
    },function(response) {
        response.pipe(res);
    });
    request.on("error", function() {
        res.end(JSON.stringify({
            "error" : "Unable to serve your request!"
        }));
    });

    request.end();
}).listen(5000);

és a hozzá tartozó buildfile módosítás:

CMD ["node","load-balancer.js"]
docker build -t balancer-service .

A fenti parancsok beleégetik az aktuális parancsok kimenetét egy image-be és azt megtag-elik a -t paraméterrel. Bizonyára megfigyeltétek a DOCKER_HOST és DOCKER_PORT környezeti változót. Ezt majd a run parancs során kell átadnunk a konténernek, ami alapján helyes elérési úttal regisztrál be az Eurekába.

Update: Erre a célra szolgál a start.sh a projekt gyökerében, hogy ne kelljen egyesével beírkálni a dolgokat.

Na de most nézzük, hogy mi is történik mikor beröffentjük azt?

Elindul 3 product, 3 order és 3 account service. Most az adatok mibenlétére ne térjünk ki, csak random belepakoltam ezt-azt a táblákba. Ezen felül van 2 view aggregator, ami a fenti 3 service közül hívogatja azokat, amik épp futnak. Aztán a hármas válaszát összesítve tér vissza. Valamint lesz még egy balancer, ami a két aggregátor közül választja ki az épp futót. Azért Node.JS van itt használva mert a két aggregátor is változó címen lehet, így Eureka-ból kéri le azokat is. Ez persze megoldható lenne Nginx dinamikus rekonfigurálásával, de az megint egy másik sztori 🙂

Viszont az az érzésem, hogy aki nem sűrűn foglalkozott ilyesmivel az kezdi elveszteni a fonalat, így jöjjön egy ábra az egészről:

servicek

Látható, hogy a rendszer pár része fix címen van, míg egy része dinamikus. Ez utóbbiak miatt van szükség az Eurekára igazából. Akkor most, hogy mindenki lehúzta a repóból a ccucot, nézzük mi is történik, ha az SQL-ek beoktrojálása után elindítjuk azt a bizonyos start.sh-t!

Lebuildelődnek a docker image-ek, aztán pedig el is indulnak a példányok. Ezután ha mindent jól csináltunk és nem dobott valami hibát a rendszer, akkor ha megnyitjuk a böngészőnkben a localhost:13344-et, akkor kihányja elénk a view aggregate tartalmát, vagyis működik a dolog!

Na de mi alapján is dolgozik mindez?

Ha rálesünk a http://172.17.42.1:32784/eureka/v2/apps/aggregatorservice -ra, akkor láthatjuk, hogy szépen itt van XML-ben, minden info az aggregatorservice-ről. Az Eureka REST API-ja sok infót tud nekünk szolgáltatni és a kliensek is ezt használják értelemszerűen. Látható hogy hány példány van, milyen IP-n, porton, milyen státusszal, de aki többet szeretne erről megtudni, annak itt a komplett doksi.

<application>
 <name>AGGREGATORSERVICE</name>
 <instance>
 <hostName>dd2a1d15f95e7b9c6026b5a70fcd07d3</hostName>
 <app>AGGREGATORSERVICE</app>
 <ipAddr>172.17.42.1</ipAddr>
 <status>UP</status>
 <overriddenstatus>UNKNOWN</overriddenstatus>
 <port enabled="true">13343</port>
 <securePort enabled="false">7002</securePort>
 <countryId>1</countryId>
 <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
 <name>MyOwn</name>
 
 </dataCenterInfo>
 <leaseInfo>
 <renewalIntervalInSecs>30</renewalIntervalInSecs>
 <durationInSecs>90</durationInSecs>
 <registrationTimestamp>1460892987340</registrationTimestamp>
 <lastRenewalTimestamp>1460893497976</lastRenewalTimestamp>
 <evictionTimestamp>0</evictionTimestamp>
 <serviceUpTimestamp>1460892987238</serviceUpTimestamp>
 
 </leaseInfo>
 <metadata class="java.util.Collections$EmptyMap"/>
 <vipAddress>jq.test.something.com</vipAddress>
 <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
 <lastUpdatedTimestamp>1460892987340</lastUpdatedTimestamp>
 <lastDirtyTimestamp>1460892987237</lastDirtyTimestamp>
 <actionType>ADDED</actionType>
 
 </instance>
 <instance>
 <hostName>d1e74f559e3a1766bab29d011fc3614e</hostName>
 <app>AGGREGATORSERVICE</app>
 <ipAddr>172.17.42.1</ipAddr>
 <status>UP</status>
 <overriddenstatus>UNKNOWN</overriddenstatus>
 <port enabled="true">13342</port>
 <securePort enabled="false">7002</securePort>
 <countryId>1</countryId>
 <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
 <name>MyOwn</name>
 
 </dataCenterInfo>
 <leaseInfo>
 <renewalIntervalInSecs>30</renewalIntervalInSecs>
 <durationInSecs>90</durationInSecs>
 <registrationTimestamp>1460892986340</registrationTimestamp>
 <lastRenewalTimestamp>1460893496975</lastRenewalTimestamp>
 <evictionTimestamp>0</evictionTimestamp>
 <serviceUpTimestamp>1460892986312</serviceUpTimestamp>
 
 </leaseInfo>
 <metadata class="java.util.Collections$EmptyMap"/>
 <vipAddress>jq.test.something.com</vipAddress>
 <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
 <lastUpdatedTimestamp>1460892986340</lastUpdatedTimestamp>
 <lastDirtyTimestamp>1460892986311</lastDirtyTimestamp>
 <actionType>ADDED</actionType>
 
 </instance>
</application>

Persze az Eureka önmagában még nem jelent semmit, mert jön az, ami az ilyen rendszerek lényege.. Mi van akkor, ha lehalnak a service-eink?:)  Na de erről majd legközelebb!

A példában szereplő fájlok megtalálhatóak githubon.

Copyright Letscode.hu 2014-2020 © Minden jog fenntartva. | Newsphere by AF themes.