Aplikacje DIRT 
w NodeJS

Plan

  1. Definicja DIRT
  2. Relatywność czasu w aplikacjach
  3. Hard real-time / Soft real-time
  4. Node.js z lotu ptaka
  5. Asynchroniczność jest wszędzie
  6. Asynchroniczność bez kaskad callbacków
  7. JavaScript wysokiej wydajności
  8. Pytania/dyskusja

DIRT

vs

standardowe podejście

CRUD

  1. Create
  2. Read
  3. Update
  4. Delete

ACID

  1. Atomicity 
  2. Consistency 
  3. Isolation 
  4. Durability 

Standardowe podejście

Teoria CAP (Brewera)

Każdy rozproszony system może zagwarantować tylko dwa z trzech następujących punktów:

 

  1. Spójność (Consistency)
  2. Dostępność (Availability)
  3. Odporność na podział (Partition tolerance)

BASE

(Basically Available, Soft state, Eventual consistency)

  1. Zasadnicza dostępność
  2. Stan systemu może się zmieniać w czasie nawet bez ingerencji z zewnątrz
  3. Opóźniona spójność
(t \to \infty, \Delta \to 0)
(t,Δ0)

DIRT

Data Intensive Real Time

 

  1. Czas jest krytyczny
  2. Stare dane są bezwartościowe
  3. Nowe dane muszą być przetwarzane tak szybko jak to tylko możliwe

Przykłady aplikacji typu DIRT

  1. Serwer IM
  2. Twitter
  3. Streaming danych finansowych
  4. Telefonia
  5. Agregatory danych (monitoring)
  6. Awionika

Hard real-time

Firm real-time

Soft real-time

Funkcja zysku

Funkcja zysku

Funkcja zysku

Funkcja zysku

Hard real-time

Firm real-time

Soft real-time

Relatywność czasu w aplikacjach

Jeżeli dostęp do cache L1 zajmuje 1 sekundę to:


Bład przewidywania instrukcji warunkowych: 0:00:10


Odczyt z cache L2 : 0:00:14


Mutex lock/unlock : 0:00:50


Odczyt z pamięci RAM : 0:03:20


Kompresja ZIP 1kb danych : 1:40:00


Wysłanie 1kb przez sieć 1 Gbps : 5:33:20


Odczytanie losowe 4kb danych z SSD: 3 dni, 11:20:00
 

Jeżeli dostęp do cache L1 zajmuje 1 sekundę to:


Odczytanie sekwencyjne 1MB danych z dysku: 5 dni, 18:53:20


PING w obrębie jednej sieci : 11 dni, 13:46:40


Odczytanie sekwencyjnie 1MB danych z SSD : 23 dni, 3:33:20


Disk seek : 231 dni, 11:33:20


Odczytanie sekwencyjnie 1MB danych z dysku : 462 dni, 23:06:40


Wysłanie pakietu Kalifornia->Holadia->Kalifornia :
9 lat, 187 dni, 5:20:00

Proporcje z którymi trzeba się oswoić

Proporcje

  L2 RAM Dysk Sieć
L1 5 83 13.666.666 80.000.000
L2   18 2.928.571 17.142.857
RAM     164.000 960.000
Dysk       6

Charakterystyka typowej aplikacji webowej

  • Małe obciążenie CPU
  • Dużo operacji I/O
  • Dużo czekania (np na bazę danych)

Histogram

Node.js z lotu ptaka

Dojrzałość

Co jest pod maską?

  • Silnik V8 od Google
  • Pojedynczy proces
  • Libuv
  • Cały kod z założenia jest pisany tak, aby nie blokował I/O
  • Możliwość wspóldzielenia kodu pomiędzy serwerem a klientem

Hello World 0

console.log("Hello World");

Hello World I

var fs = require("fs");

fs.readFile("example_log.txt", function (err, logData) {
  
  if (err) {
    throw err;
  }
  
  var text = logData.toString();
  console.log(text);
});

Hello World II

var http = require("http");

var server = http.createServer(function (request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello World\n");
});

server.listen(8000);

console.log("Server running at http://127.0.0.1:8000/");

Hello World III

var http = require("http");
var mysql = require("mysql"); 
	 
var connection = mysql.createConnection({ 
	user: "root", 
	password: "", 
	database: "db_name"
}); 

http.createServer(function (request, response) { 
	request.on("end", function () { 
		connection.query("SELECT * FROM your_table;", function (error, rows, fields) { 
			response.writeHead(200, { "Content-Type": "application/json" }); 
			response.end(JSON.stringify(rows)); 
		}); 
	}); 
}).listen(8080);

Wydajność I

var i, a, b, c, max;

max = 1000000000;

var time = process.hrtime();

for (i = 0; i < max; i++) {
    a = 1234 + 5678 + i;
    b = 1234 * 5678 + i;
    c = 1234 / 2 + i;
}

var diff = process.hrtime(time);

console.log((diff[0] * 1e9 + diff[1]) / 1e6);
<?php

$a = $b = $c = $i = null;
$max = 1000000000;
 
$start = microtime(true);
 
for ($i = 0; $i < $max; $i++) {
    $a = 1234 + 5678 + $i;
    $b = 1234 * 5678 + $i;
    $c = 1234 / 2 + $i;
}

$end = microtime(true);

var_dump(($end - $start) * 1000);

Czy takie testy są sensowne?

PHP 5.6.6 (VC11 x64)

Node v0.12.0 (x64)

Wydajność I

Czy takie porównania mają sens?

Liczba iteracji

 

100

10,000

1,000,000

10,000,000

1,000,000,000

Node.js

 

0.008 ms

0.06 ms

1.5 ms

10 ms

943 ms

PHP

 

0.03 ms

1.44 ms

141 ms

1,429 ms

142,414 ms

Wydajność II

function fibonacci(n) {
    if (n < 2)
        return 1;
    else
        return fibonacci(n-2) + fibonacci(n-1);
}

var time = process.hrtime();

console.log(fibonacci(40));

var diff = process.hrtime(time);

console.log((diff[0] * 1e9 + diff[1]) / 1e6);
<?php

function fibonacci($n) {
   if ($n < 2) {
      return 1;
   } else {
      return fibonacci($n - 2) + fibonacci($n - 1);
   }
}

$start = microtime(true);

echo fibonacci(40);

$end = microtime(true);

var_dump(($end - $start) * 1000);

Czy takie testy są sensowne?

PHP 5.6.6 (VC11 x64)

Node v0.12.0 (x64)

Wydajność II

Czy takie porównania mają sens?

Wartość n

 

10

30

40

45

Node.js

 

14 ms

26 ms

1,527 ms

16,782  ms

PHP

 

0.27 ms

473 ms

57,902  ms

640,945 ms

PHP / Node.js

 

x 0.019

x 18,2

x 37,9

x 38,2

Asynchroniczność jest wszędzie

Klasyczny kod

<?php

$result = query($sql)
doSomething($result);

echo "Next command";
"use strict";

query(sql, function(result) {
    doSomething(result); 
});

console.log("Next command");

Siła tkwi w tym, że w obrębie JS to jest naturalne

<?php

$result = query($sql)
doSomething($result);
// waste of time
echo "Next command";
"use strict";

query(sql, function(result) {
    doSomething(result); 
});

console.log("Next command");

Porównajmy z:

Gdzie szukamy zysku?

  • O ile moglibyśmy zmniejszyć czas odpowiedzi na pojedyncze zapytanie zrównoleglając ten kod, który odpytuje zewnętrzne systemy?
  • Wciąż obecne podejście mikrooptymalizacji pomimo używania wysokopoziomowych języków
  • Kupujemy 100 cykli procesora równocześnie marnując 10 mln obok

Anatomia żądania

Obsłużenie zapytania

Odpytanie bazy danych

Przetworzenie wyniku

Przygotowanie odpowiedzi

Wysłanie

odpowiedzi

Jak to skalować?

Wielowątkowość

Wątek 1

Wątek 2

Wątek 3

Wątek 4

Musimy brać pod uwagę narzut ze zmianą kontekstu oraz zwiększony poziom skomplikowania kodu jaki to generuje

Wiele procesów równolegle

Proces 1

Proces 2

Proces 3

Proces 4

Musimy brać pod uwagę wysoki koszt pamięci 

Pętla zdarzeń

Proces 1

System plików

Sieć

Baza danych

Inne

Pula wątków

Kolejka zdarzeń

Pętla zdarzeń

Asynchroniczność bez kaskad callbacków

Jak wygląda kod w js?

fs.readdir(source, function(err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function(filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function(err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function(width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

Rozwiązanie 1 - async

async.map(['file1','file2','file3'], fs.stat, function(err, results){
    // results is now an array of stats for each file
});

async.filter(['file1','file2','file3'], fs.exists, function(results){
    // results now equals an array of the existing files
});

async.parallel([
    function(){ ... },
    function(){ ... }
], callback);

async.series([
    function(){ ... },
    function(){ ... }
]);

Rozwiązanie 2 - Q

var Q = require("q");
var FS = require("fs");

function getFile(name) {
    var deferred = Q.defer();
    
    FS.readFile(name, "utf-8", function (error, text) {
        if (error) {
            deferred.reject(new Error(error));
        } else {
            deferred.resolve(text);
        }
    });
    
    return deferred.promise;
}

var file = getFile("foo.txt");

file.then(function(data) {      
    console.log('File has been read:', data);
})
.fail(function(err) {
    console.error('Error received:', err);
})

Rozwiązanie 2 - Q - lepiej

var FS = require("fs");
var Q = require("q");

Q.nfcall(FS.readFile, "foo.txt", "utf-8")
.then(function(data) {      
    console.log('File has been read:', data);
})
.fail(function(err) {
    console.error('Error received:', err);
})
.done();

Rozwiązanie 2 
Q - możliwości

Q.all([getFromDisk(), getFromCloud()]).spread(function (diskVal, cloudVal) {
    assert(diskVal === cloudVal);
}).done();

promise.timeout(10000).then(
  function (result) {
    // will be called if the promise resolves normally
    console.log(result);
  },
  function (err) {
    // will be called if the promise is rejected, or the 10 second timeout occurs
    console.log(err);
  }
);

Rozwiązanie 3 
EventEmitter

var EventEmitter = require("events").EventEmitter;
var door = new EventEmitter();

door.on("knockknock", function(guest) {
    console.log(guest);
});

setInterval(function() {
    door.emit("knockknock", "Boss");
}, 1000);

JavaScript wysokiej wydajności

TypedArrays

Benchmark.prototype.setup = function() {
  var canvas = document.getElementById("canvas");
  var canvasWidth  = canvas.width;
  var canvasHeight = canvas.height;
  var ctx = canvas.getContext("2d");
  var imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);

  var data = imageData.data;
  var buf = new ArrayBuffer(imageData.data.length);
  var buf8 = new Uint8ClampedArray(buf); //View for buf
  var data32 = new Uint32Array(buf); //View for buf
};

Benchmark.prototype.teardown = function() {
  ctx.putImageData(imageData, 0, 0);
};

Cluster mode

var cluster = require("cluster");
var http = require("http");
var numCPUs = require("os").cpus().length;

if (cluster.isMaster) {
  // Fork workers.
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on("exit", function(worker, code, signal) {
    console.log("worker " + worker.process.pid + " died");
  });
} else {
  // Workers can share any TCP connection
  // In this case its a HTTP server
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}

asm.js

function M(stdlib, foreign, heap) {
    "use asm";

    var i32 = new stdlib.Int32Array(heap);
    var f64 = new stdlib.Float64Array(heap);

    var imul = stdlib.Math.imul;
    var truncate = foreign.truncate;

    function foo() {
      var a = 0;  // a is signed
      var b = 0.0; // b is double
      a = i32[0]|0;
      b = +f64[0];
      return imul(a, truncate(b))|0;
    }

    return { foo: foo };
}

var module = M(window, {
  truncate: function (x) {
    return x | 0;
  }
}, new ArrayBuffer(4 * 1024));

asm.js - C++

#include <stdio.h>
 
int fib(int x) {
  if (x < 2) {
    return 1;
  } else {
    return fib(x - 1) + fib(x - 2);
  }
}
 
int main() {
  int result = fib(45);
 
  printf("Fib: %d\n", result);
  return 1;
}

(Visual Studio 2013 z opcją /O2) - 5,288 ms

asm.js - konwersja

function __Z3fibi($x) {
 $x = $x|0;
 var $0 = 0, $1 = 0, $2 = 0, $3 = 0, $4 = 0, $5 = 0, $accumulator$tr$lcssa = 0, 
$accumulator$tr1 = 0, $x$tr2 = 0, label = 0, sp = 0;
 sp = STACKTOP;
 $0 = ($x|0)<(2);
 if ($0) {
  $accumulator$tr$lcssa = 1;
 } else {
  $accumulator$tr1 = 1;$x$tr2 = $x;
  while(1) {
   $1 = (($x$tr2) + -1)|0;
   $2 = (__Z3fibi($1)|0);
   $3 = (($x$tr2) + -2)|0;
   $4 = (($2) + ($accumulator$tr1))|0;
   $5 = ($3|0)<(2);
   if ($5) {
    $accumulator$tr$lcssa = $4; break;
   } else {
    $accumulator$tr1 = $4;$x$tr2 = $3;
   }
  }
 }
 STACKTOP = sp;return ($accumulator$tr$lcssa|0);
}

16,050 ms

asm.js - ręczny kod

function asmCode(global, env, buffer) {
    "use asm";
	
    function fib(x) {
      x = x|0;
      if ((x >>> 0) < 2) return 1;
      return ((fib((x-2)|0)|0) + (fib((x-1)|0)|0))|0;
    }

    return fib;
}

var fibModuleAsm = asmCode(window, {}, new ArrayBuffer(4 * 1024));
console.log(fibModuleAsm( 45 ));

8,695 ms

(C++ /O2) 5,288 ms

vs

(js) 17,831 ms

vs

(asm.js - automat) 16,050 ms

vs

(asm.js - ręczny kod) 8,695 ms 

SIMD.js

var a = SIMD.float32x4(1.0, 2.0, 3.0, 4.0);
var b = SIMD.float32x4(5.0, 6.0, 7.0, 8.0);
var c = SIMD.float32x4.add(a,b);

function simdAverage(src, len) {

  var sumx4 = SIMD.float32x4.splat(0.0);
  var srcx4 = new Float32x4Array(src.buffer);

  for (var i = 0, n = len/4; i < n; ++i) {
    sumx4 = SIMD.float32x4.add(sumx4, srcx4.getAt(i));
  }

  return (sumx4.x + sumx4.y + sumx4.z + sumx4.w) / len;
}

Websockets

var server = http.createServer(function(request, response){});
var connections = [];

server.listen(8080, function(){});

var wsServer = new WebSocketServer({
    httpServer: server,
});

wsServer.on("request", function(request) {
    var connection = request.accept(null, request.origin);
    connections.push(connection);

    connection.send("Hello World!");
    
    connection.on("message", function(message) {
         parseMessage(message);
    });

    connection.on("close", function(connection) {
        connections.forEach(function(connection) {
            connection.send("Client disconnected");
        });
    });
});

GPU? Da się.

Baza wiedzy

Pytania/dyskusja

Dziękuję za uwagę!

DIRTy Application in NodeJs

By Artur Trzesiok

DIRTy Application in NodeJs

  • 997