Slide 1

Slide 1 text

Divide et impera Christian Steiner / pixelio.de

Slide 2

Slide 2 text

Basti • Sebastian Springer • aus München • arbeite bei MaibornWolff GmbH • https://github.com/sspringer82 • @basti_springer • JavaScript Entwickler

Slide 3

Slide 3 text

Agenda HTTP Microservice mit Express • Routing • Struktur • Datenbank • Logging • Tests • Container Commandbased Microservices mit Seneca • Patterns • Plugins • Queueing Rainer Sturm / pixelio.de

Slide 4

Slide 4 text

Microservices Small Scaleable Resilient Independent Testable Focused

Slide 5

Slide 5 text

JavaScript ist sehr weit verbreitet. Es gibt sehr viele OSS Module Node.js wurde fürs Web gebaut Es werden alle Protokolle, Systeme und Datenbanken unterstützt Node ist relativ schlank

Slide 6

Slide 6 text

Beispiel Ein Microservice, der Artikel für einen 
 Webshop ausliefern soll. M. Hermsdorf / pixelio.de

Slide 7

Slide 7 text

Webshop Client API Articles Payment Users DB DB DB Auth Logger

Slide 8

Slide 8 text

Artikelliste Router Controller Model Datenbank Logger

Slide 9

Slide 9 text

Pakete S. Hofschlaeger / pixelio.de

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Express.js Express ist ein leichtgewichtiges (< 2MB) Web Application Framework für Node.js. Express unterstützt HTTP und HTTPS in Version 1 und 2. Express kümmert sich primär um das Routing in einer Applikation Außerdem verfügt es über eine Plugin-Schnittstelle, die Middleware genannt wird.

Slide 12

Slide 12 text

Installation yarn init yarn add express

Slide 13

Slide 13 text

Aufbau eines Microservices . ├── db │ ├── config.js │ └── db.sqlite3 ├── package.json ├── spec └── src ├── config.js ├── controller.js ├── index.js ├── logger.js ├── model.js └── router.js

Slide 14

Slide 14 text

Einstieg index.js

Slide 15

Slide 15 text

Einstieg const express = require('express'); const router = require('./router'); const app = express(); router(app); app.listen(8080, () => { console.log('Service is listening to http://localhost: 8080'); });

Slide 16

Slide 16 text

Einstieg Hier werden alle globalen Pakete eingebunden und die Applikation konfiguriert. Alle weiteren Funktionalitäten werden in separate Dateien ausgelagert.

Slide 17

Slide 17 text

Router router.js

Slide 18

Slide 18 text

Router const controller = require('./controller'); module.exports = (app) => { app.get('/article', controller.getAll); app.get('/article/:id', controller.getOne); app.post('/article', controller.create); app.put('/article', controller.update); app.delete('/article', controller.remove); }

Slide 19

Slide 19 text

Router Der Router definiert alle für den Microservice verfügbaren Routen. Eine Route besteht aus einer HTTP-Methode und einem Pfad. Die Flexibilität einer Route kann durch Variablen erhöht werden. Um den Router übersichtlich zu halten, werden die Callback- Funktionen für die einzelnen Routen ausgelagert.

Slide 20

Slide 20 text

Controller controller.js

Slide 21

Slide 21 text

Controller const model = require('./model'); module.exports = { getAll(req, res) { res.json( model.getAll() ); }, getOne(req, res) {}, create(req, res) {}, update(req, res) {}, remove(req, res) {} }

Slide 22

Slide 22 text

Controller Der Controller ist dafür zuständig Informationen aus dem Request zu extrahieren und die Informationen an das Model zu übergeben. Auf die Variablen im Pfad der Route wird über req.params. zugegriffen Werden Informationen im Request-Body übermittelt, bindet man die body-parser-Middleware ein und greift dann auf req.body zu.

Slide 23

Slide 23 text

Model model.js

Slide 24

Slide 24 text

Model Das Model steuert den Datenbankzugriff. Außerdem beinhaltet es die Businesslogik des Microservices. Im Idealfall verfügt ein Microservice über eine eigene Datenbank. Dies steigert die Unabhängigkeit des Services.

Slide 25

Slide 25 text

Model Node.js unterstützt alle gebräuchlichen Datenbanken wie beispielsweise OracleDB, MySQL, Redis, MongoDB. Für den Datenbankzugriff wird zunächst der Datenbanktreiber installiert. yarn add sqlite3 Zur einfacheren Handhabung der Operationen existieren verschiedene ORMs bzw. ODMs yarn add orm

Slide 26

Slide 26 text

ORM Um den Aufwand der Implementierung von CRUD- Operationen auf einer Datenbank zu reduzieren, können ORMs beziehungsweise ODMs verwendet werden. Das ORM übernimmt eine Sicherheits-Funktion, indem es für das korrekte Escaping der Abfragen sorgt.

Slide 27

Slide 27 text

ORM const express = require('express'); const orm = require('orm'); const router = require('./router'); const dbConfig = require('./db/config.js'); const {dbPath} = require('./config.js'); const app = express(); app.use(orm.express(dbPath, dbConfig)); router(app); app.listen(8080, () => { console.log('Service is listening to http://localhost: 8080'); });

Slide 28

Slide 28 text

ORM module.exports = { define(db, models, next) { models.articles = db.define('articles', { id: Number, title: String, price: Number }); next(); } }

Slide 29

Slide 29 text

Model Das Model übernimmt neben der Kapselung der Datenbank- Kommunikation weitere Aufgaben wie die Validierung der Eingaben und verschiedene Berechnungen. Die meisten Operationen erfolgen asynchron. Das Model kann hier für eine saubere API sorgen, indem statt Callback- Funktionen asynchrone Funktionen verwendet werden.

Slide 30

Slide 30 text

async getAll(req, res) { try { const articles = await model.getAll(req); res.json(articles); } catch (e) { res.status(500).send(e); } } getAll(req) { return new Promise((resolve, reject) => { req.models.articles.all((err, results) => { if (err) { reject(err); } else { resolve(results); } }); }); } controller model

Slide 31

Slide 31 text

Logging Tim Reckmann / pixelio.de

Slide 32

Slide 32 text

Logging Im Microservice können verschiedene Fehler auftreten. Neben dem Abfangen und der Behandlung solcher Probleme, sollte das Auftreten solcher Situationen in einem Log festgehalten werden. Der Logger sollte die Informationen nicht lokal vorhalten, sondern die Möglichkeit bieten die Informationen an einen zentralen Logging Server zu senden.

Slide 33

Slide 33 text

Logging Für remote-Logging kann beispielsweise log4js verwendet werden. Die Bibliothek beinhaltet bereits einige Appender für verschiedene Log-Targets wie zum Beispiel Dateien oder einen Logstash-Server. Der Logstash-Appender kann für das Remote-Logging verwerndet werden.

Slide 34

Slide 34 text

Logging const log4js = require('log4js'); log4js.configure({ appenders: [{ "host": "127.0.0.1", "port": 10001, "type": "logstashUDP", "logType": "database", "layout": { "type": "pattern", "pattern": "%m" }, "category": "database" }], categories: { default: { appenders: ['database'], level: 'error' } } }); module.exports = log4js;

Slide 35

Slide 35 text

Logging const log4js = require('./logger'); module.exports = { getAll(req) { return new Promise((resolve, reject) => { req.models.articles.all((err, results) => { if (err) { reject(err); log4js.getLogger('database').error(err); } else { resolve(results); } }); }); } }

Slide 36

Slide 36 text

Tests Dieter Schütz / pixelio.de

Slide 37

Slide 37 text

Tests Microservices müssen automatisiert getestet werden. Dies geschieht auf zwei Ebenen: Unittests für einzelne Units of Code und Schnittstellen-Tests für das Big Picture. Für Unittests kommt Jasmine zum Einsatz und die Schnittstellen können zum Beispiel mit Frisby.js getestet werden.

Slide 38

Slide 38 text

Unittests yarn add jasmine node_modules/.bin/jasmine init

Slide 39

Slide 39 text

Unittests const model = require('../model'); describe('model', () => { it('should handle a database error correctly', () => { const req = { models: { articles: { all: (cb) => {cb('error', null);} } } } model.getAll(req).catch((e) => { expect(e).toBe('error'); }); }) });

Slide 40

Slide 40 text

Unittests Die Tests werden mit dem Kommando ./node_modules/.bin/ jasmine ausgeführt. Die Tests können entweder im Verzeichnis “spec” abgelegt werden oder bei den jeweiligen Source-Dateien die sie testen.

Slide 41

Slide 41 text

Mockery In Node.js geschieht das Abhängigkeits-Management vorwiegend über das Modulsystem. Mockery ist eine Bibliothek mit deren Hilfe man diese Abhängigkeiten für Testzwecke austauschen kann. yarn add -D mockery

Slide 42

Slide 42 text

Mockery const mockery = require('mockery'); beforeEach(() => { mockery.enable(); const fsMock = { stat: function (path, cb) {...} }; mockery.registerMock('fs', fsMock); }); afterEach(() => { mockery.disable(); });

Slide 43

Slide 43 text

Schnittstellentests frisby.js ist eine Bibliothek mit der sich REST-Schnittstellen testen lassen. Frisby ist eine Erweiterung des Jasmine- Testframeworks. frisby.js benötigt jasmine-node zur Ausführung der Tests. yarn add -D frisby jasmine-node

Slide 44

Slide 44 text

Schnittstellentests require('jasmine-core'); var frisby = require('frisby'); frisby.create('Get all the articles') .get('http://localhost:8080/article') .expectStatus(200) .expectHeaderContains('content-type', 'application/json') .expectJSON('0', { id: function (val) { expect(val).toBe(1);}, title: 'Mannesmann Schlosserhammer', price: 7 }) .toss();

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

PM2 Node.js ist Single Threaded. Durch das Nonblocking I/O sind Node-Applikationen trotzdem schnell. Um mehrere Kerne nutzen zu können, kann das child_process Modul verwendet werden. Für die lokale Skalierung empfiehlt Express allerdings die Verwendung von PM2. yarn add pm2

Slide 47

Slide 47 text

PM2 pm2 start app.js pm2 start app.js -i 4 pm2 reload all pm2 scale 10 pm2 list pm2 stop pm2 delete

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

Docker Die einzelnen Services laufen in separaten, in sich geschlossenen Containern. Von jedem Service kann eine beliebige Anzahl von Containern gestartet werden. Wird der Funktionsumfang der Applikation erweitert, können zusätzliche Services hinzugefügt werden.

Slide 50

Slide 50 text

Dockerfile FROM node:7.10 # Create app directory RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Install app dependencies COPY package.json /usr/src/app/ RUN yarn install # Bundle app source COPY . /usr/src/app EXPOSE 8080 CMD [ "yarn", "start" ]

Slide 51

Slide 51 text

Docker docker build -t basti/microservice . docker run -p 8080:8080 -d basti/microservice

Slide 52

Slide 52 text

API Gateway Jürgen Reitböck / pixelio.de

Slide 53

Slide 53 text

API Gateway Damit sich jeder Microservice um seine Aufgaben kümmern kann, wird ein zentraler Knoten benötigt, der sich um allgemeine Aufgaben wie beispielsweise Authentifizierung kümmert. Das API Gateway leitet die autorisierten Anfragen an die jeweiligen Services weiter und empfängt ihre Antworten, die dann zum Client weitergeleitet werden.

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

Seneca Seneca verfolgt im Gegensatz zu Express einen anderen Ansatz. Die Services kommunizieren über Nachrichten und werden dadurch unabhängig von der Transportschicht.

Slide 56

Slide 56 text

Installation yarn add seneca

Slide 57

Slide 57 text

Service Definition const seneca = require('seneca')(); seneca.add({role: 'math', cmd: 'sum'}, controller.getAll); Das erste Argument der add-Methode ist das Pattern, das den Service beschreibt. Das Pattern ist frei wählbar. Role und cmd haben sich aber als Best Practice etabliert. Das zweite Argument ist Aktion, also der Service-Handler. Hier kann wie schon bei Express beliebig abstrahiert werden.

Slide 58

Slide 58 text

Service-Handler async getAll(msg, reply) { try { const articles = await model.getAll(req); reply(null, JSON.stringify(articles)); } catch (e) { reply(e); } } Der Service-Handler erhält eine Repräsentation der Anfrage und eine Reply-Funktion. Über das msg-Objekt kann auf die Eigenschaften der Anfrage zugegriffen werden. Die Reply- Funktion wird mit einem Fehlerobjekt und den Nutzdaten aufgerufen.

Slide 59

Slide 59 text

Service Aufruf seneca.act({role: 'article', cmd: 'get'}, (err, result) => { if (err) { return console.error(err); } console.log(result); }); Mit der seneca.act-Methode kann ein Microservice konsumiert werden. Diese Methode akzeptiert eine Nachricht und eine Callback-Funktion. Mit der Nachricht wird der Service aufgelöst. Die Callback-Funktion wird aufgerufen, sobald der Service antwortet. Mit act können auch Routinen innerhalb eines Microservices aufgerufen werden (code reuse)

Slide 60

Slide 60 text

Patterns Helmut / pixelio.de Helmut / pixelio.de

Slide 61

Slide 61 text

Patterns Existieren mehrere Patterns vom gleichen Typ, gewinnt immer das spezifischere. Über solche Patterns lassen sich beispielsweise auch Schnittstellen versionieren.

Slide 62

Slide 62 text

Plugins Klicker / pixelio.de

Slide 63

Slide 63 text

Plugins Ein Plugin ist eine Sammlung von Patterns. Es gibt verschiedene Quellen für Plugins: Von Seneca mitgelieferte, selbst entwickelte oder von Drittanbietern. Plugins vereinfachen das Loggen und Debuggen.

Slide 64

Slide 64 text

Plugins Mit der use-Methode können Patterns in Plugins organisiert werden. Der Name der Funktion wird zum Logging verwendet. Dem Plugin können Optionen übergeben werden, um es weiter zu konfigurieren. Das init-Pattern wird statt eines Konstruktors verwendet. const seneca = require('seneca')(); function articles(options) { this.add({role:'article',cmd:'get'}, controller.getAll); } seneca.use(articles);

Slide 65

Slide 65 text

Plugins const seneca = require('seneca')(); function articles(options) { this.add({role:'article',cmd:'get'}, controller.getAll); this.wrap({role:'article'}, controller.verify); } seneca.use(articles); Mit der Wrap-Methode kann Funktionalität definiert werden, die für mehrere Patterns gilt. Mit this.prior(msg, respond) kann der ursprüngliche Service aufgerufen werden.

Slide 66

Slide 66 text

Client/Server cre8tive / pixelio.de

Slide 67

Slide 67 text

Server function articles(options) { this.add({role:'article',cmd:'get'}, controller.getAll); this.wrap({role:'article'}, controller.verify); } require('seneca')() .use(articles) .listen(8080) Die listen-Methode bindet den Server an TCP-Port 8080. Der Service kann daraufhin per Browser oder mit einem anderen Server abgefragt werden: http://localhost:8080/act?role=article&cmd=get

Slide 68

Slide 68 text

Client require('seneca')() .client(8080) .act({role: 'article', cmd: 'get'}, console.log); Mit der client-Methode kann man eine Applikation mit einem Seneca-Microservice verbinden.

Slide 69

Slide 69 text

Änderung des Transports // client seneca.client({ type: 'tcp', port: 8080 }); // server seneca.listen({ type: 'tcp', port: 8080 }); Die Angabe type: ‘tcp’ verwendet TCP statt HTTP als Übertragungsprotokoll.

Slide 70

Slide 70 text

Integration in Express

Slide 71

Slide 71 text

Integration in Express const SenecaWeb = require('seneca-web'); const Express = require('express'); const router = new Express.Router(); const seneca = require('seneca')(); const app = Express(); const senecaWebConfig = { context: router, adapter: require('seneca-web-adapter-express'), options: { parseBody: false } } app.use( require('body-parser').json() ) .use( router ) .listen(8080); seneca.use(SenecaWeb, senecaWebConfig ) .use('api') .client( { type:'tcp', pin:'role:article' } );

Slide 72

Slide 72 text

Integration in Express Seneca fügt mit dieser Konfiguration die entsprechenden Routen zur Applikation hinzu. Die Seneca Patterns müssen dazu etwas angepasst werden. Alle Routen die mit einem Aufruf von seneca.act('role:web', {routes: routes}) definiert werden, werden zu den Express- Routen hinzugefügt. Über das path-Pattern findet dann ein entsprechendes Matching statt.

Slide 73

Slide 73 text

Kommunikation per Queue S. Hofschlaeger / pixelio.de

Slide 74

Slide 74 text

Queue Über Plugins von Drittanbietern kann statt über Netzwerk auch über eine Message Queue kommuniziert werden. Der Vorteil hierbei ist, dass eine Queue Client und Server weiter entkoppelt und das gesamte System weniger Fehleranfällig macht.

Slide 75

Slide 75 text

Queue yarn add seneca-servicebus-transport

Slide 76

Slide 76 text

Queue require('seneca')() .use('seneca-servicebus-transport') .use(articles) .listen({ type: 'servicebus', connection_string: '...' }); require('seneca')() .use('seneca-servicebus-transport') .client({ type: 'servicebus', connection_string: '...' }) .act({role: 'article', cmd: 'get'}, console.log); Server Client

Slide 77

Slide 77 text

Microservices, the silver bullet? Microservices sind eine sehr gute Lösung für bestimmte Probleme, jedoch nicht für alle. Microservices erhöhen die Komplexität einer Applikation. In Node.js gibt es viele Pakete die sich mit diesem Thema beschäftigen. Etliche sind veraltet oder von schlechter Qualität. Ein genauer Blick auf GitHub und npmjs.com vor der Einbindung lohnt sich.

Slide 78

Slide 78 text

Fragen? Rainer Sturm / pixelio.de

Slide 79

Slide 79 text

KONTAKT Sebastian Springer [email protected] MaibornWolff GmbH Theresienhöhe 13 80339 München @basti_springer https://github.com/sspringer82