Les serveurs web asynchrones

Comprendre les serveurs web asynchrones tels que Node.js ou Tornado : cas d'utilisation, contraintes et styles de programmation.

Presenter Notes

CPU-bound

Calculs dans le process web : fonctions de crypto, encodage de données, etc. --> l'asynchrone ne sert à rien

Pour aller plus vite, optimiser les algorithmes, rajouter des processus, etc.

Presenter Notes

I/O bound

Requêtes base de données, services REST externes, fork de processus externes. --> l'asynchrone peut être utile

Presenter Notes

Nombreux clients connectés

WebSockets --> l'asynchrone est vite indispensable

Presenter Notes

L'asynchrone peut-il nous aider ?

Nos processus passent leur temps à attendre ? Plus assez d'espace (mémoire) pour en ajouter d'autres ? Les passer en asynchrone peut aider.

Nos processus travaillent ? Les passer en asynchrone ne va pas aider... On peut par contre envisager d'externaliser ce travail pour les passer en asynchrone.

Presenter Notes

Hello Tornado

import tornado.ioloop
import tornado.web


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")


application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    print("Serve http://127.0.0.1:8888/")
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

10 000 requêtes par lots de 100 :

$ ab -n 10000 -c 100 http://127.0.0.1:8888/
Time taken for tests:   6.706 seconds
Complete requests:      10000
Requests per second:    1491.25 [#/sec] (mean)
Time per request:       67.058 [ms] (mean)

Presenter Notes

Tornado avec SQL bloquant

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        cur = self.application.db.cursor()
        cur.execute("SELECT 42, pg_sleep(0.300);")
        result = cur.fetchone()
        self.write("Result: %s" % result[0])

20 requêtes par lots de 10 :

$ ab -n 20 -c 10 http://127.0.0.1:8888/ 
Time taken for tests:   6.201 seconds
Complete requests:      20
Requests per second:    3.23 [#/sec] (mean)
Time per request:       3100.513 [ms] (mean)

Presenter Notes

Node.js / io.js

var http = require('http');
var pg = require('pg');
var conString = "postgres://al:al@localhost/al";
var slowQuery = 'SELECT 42 as number, pg_sleep(0.300);';

var server = http.createServer(function(req, res) {
  pg.connect(conString, function(err, client, done) {
    client.query(slowQuery, [], function(err, result) {
      done();
      res.writeHead(200, {'content-type': 'text/plain'});
      res.end("Result: " + result.rows[0].number);
    });
  });
})

console.log("Serve http://127.0.0.1:3001/")
server.listen(3001)

20 requêtes par lots de 10 :

$ ab -n 20 -c 10 http://127.0.0.1:3001/
Time taken for tests:   0.678 seconds
Complete requests:      20
Requests per second:    29.49 [#/sec] (mean)
Time per request:       339.116 [ms] (mean)

Presenter Notes

Tornado avec SQL asynchrone

from tornado import web, ioloop
import momoko

class MainHandler(tornado.web.RequestHandler):

    @tornado.web.asynchronous
    def get(self):
        self.application.db.execute(
            'SELECT 42, pg_sleep(0.300)', callback=self._done)

    def _done(self, cursor, error):
        result = cursor.fetchone()
        self.write("Result: %s" % result[0])
        self.finish()

if __name__ == "__main__":
    application = tornado.web.Application([(r"/", MainHandler)])
    application.db = momoko.Pool(dsn='dbname=al user=al', size=10)
    application.listen(8888)
    ioloop.IOLoop.instance().start()

20 requêtes par lots de 10 :

$ ab -n 20 -c 10 http://127.0.0.1:8888/
Time taken for tests:   0.622 seconds
Complete requests:      20
Requests per second:    32.18 [#/sec] (mean)
Time per request:       310.756 [ms] (mean)

Presenter Notes

Structure d'une IO loop

Version extrêmement simplifiée de l'IOLoop de Tornado :

def start(self):
    while True:
        # Appelle la fonction de polling de la plateforme
        # (epoll sous Linux, kqueue sous BSD). Celle-ci 
        # renvoie les événements survenus sur les
        # descripteurs de fichiers (sockets, etc.) surveillés
        event_pairs = self._impl.poll(poll_timeout)
        self._events.update(event_pairs)
        while self._events:
            # Appelle les handler d'événements enregistrés
            fd, events = self._events.popitem()
            fd_obj, handler_func = self._handlers[fd]
            handler_func(fd_obj, events)

Enregistrement des handlers :

def add_handler(self, fd, handler, events):
    fd, obj = self.split_fd(fd)
    self._handlers[fd] = (obj, stack_context.wrap(handler))
    self._impl.register(fd, events | self.ERROR)

L'IO loop de Node.js est fournie par la bibliothèque libuv.

Presenter Notes

Ajout d'une requête HTTP

var server = http.createServer(function(req, res) {
  pg.connect(conString, function(err, client, done) {
    client.query(slowQuery, [], function(err, result) {
      var dbValue = result.rows[0].number;
      done();
      http.get("http://127.0.0.1:8000/", function(response) {
        response.on("data", function(chunk) {
          /* Le service met 300 ms à répondre
             et renvoie {"value": 19} */
          var responseObj = JSON.parse(chunk),
            value = dbValue - responseObj.value;
          res.writeHead(200, {'content-type': 'text/plain'});
          res.end("Result: " + value);
        });
      });
    });
  });
});

20 requêtes par lots de 10 :

$ ab -c 10 -n 20 http://127.0.0.1:3001/
Time taken for tests:   1.253 seconds
Complete requests:      20
Requests per second:    15.96 [#/sec] (mean)
Time per request:       626.434 [ms] (mean)

Presenter Notes

Callbacks en Python

class MainHandler(tornado.web.RequestHandler):

    @tornado.web.asynchronous
    def get(self):

        def handle_db(cursor, error):
            db_value = cursor.fetchone()[0]

            def handle_http(response):
                json_data = json.loads(response.body.decode())
                result = db_value - json_data['value']
                self.write("Result: %s" % result)
                self.finish()

            http_client = tornado.httpclient.AsyncHTTPClient()
            http_client.fetch('http://127.0.0.1:8000/', handle_http)

        self.application.db.execute(
            'SELECT 42, pg_sleep(0.300)', callback=handle_db)

20 requêtes par lots de 10 :

$ ab -c 10 -n 20 http://127.0.0.1:8888/
Time taken for tests:   1.260 seconds
Complete requests:      20
Requests per second:    15.88 [#/sec] (mean)
Time per request:       629.834 [ms] (mean)

Presenter Notes

Coroutines en Python

class MainHandler(tornado.web.RequestHandler):

    @tornado.gen.coroutine
    def get(self):
        cursor = yield momoko.Op(self.application.db.execute,
                                 'SELECT 42, pg_sleep(0.300)')
        db_value = cursor.fetchone()[0]
        http_client = tornado.httpclient.AsyncHTTPClient()
        response = yield http_client.fetch('http://127.0.0.1:8000/')
        json_data = json.loads(response.body.decode())
        result = db_value - json_data['value']
        self.write("Result: %s" % result)
        self.finish()

20 requêtes par lots de 10 :

$ ab -c 10 -n 20 http://127.0.0.1:8888/
Time taken for tests:   1.281 seconds
Complete requests:      20
Requests per second:    15.61 [#/sec] (mean)
Time per request:       640.527 [ms] (mean)

Presenter Notes

Parallélisme en Python

class MainHandler(tornado.web.RequestHandler):

    @tornado.gen.coroutine
    def get(self):
        http_client = tornado.httpclient.AsyncHTTPClient()
        # Lancement des requêtes en parallèle
        cursor, response = yield [
            momoko.Op(self.application.db.execute,
                      'SELECT 42, pg_sleep(0.300)'),
            http_client.fetch('http://127.0.0.1:8000/'),
        ]
        db_value = cursor.fetchone()[0]
        json_data = json.loads(response.body.decode())
        result = db_value - json_data['value']
        self.write("Result: %s" % result)
        self.finish()

20 requêtes par lots de 10 :

$ ab -c 10 -n 20 http://127.0.0.1:8888/
Time taken for tests:   0.663 seconds
Complete requests:      20
Requests per second:    30.15 [#/sec] (mean)
Time per request:       331.638 [ms] (mean)

Presenter Notes

Parallélisme en JavaScript

var koa = require('koa');
var koaPg = require('koa-pg');
var request = require('co-request');
var app = koa();
app.use(koaPg('postgres://al:al@localhost/al'))

app.use(function *(){
  var sql = 'SELECT 42 as number, pg_sleep(0.300)';
  /* Lancement des requêtes en parallèle */
  var results = yield [
    this.pg.db.client.query_(sql),
    request('http://127.0.0.1:8000')
  ];
  var dbValue = results[0].rows[0].number;
  var responseObj = JSON.parse(results[1].body);
  this.body = "Result: " + (dbValue - responseObj.value);
});

app.listen(3000);

20 requêtes par lots de 10 :

$ ab -c 10 -n 20 http://127.0.0.1:3000/
Time taken for tests:   0.685 seconds
Complete requests:      20
Requests per second:    29.22 [#/sec] (mean)
Time per request:       342.268 [ms] (mean)

Presenter Notes

En conclusion

Quand utiliser de l'asynchrone ?

  1. le traitement des requêtes est ralenti par l'accès à des ressources externes : base de données, service externe, connexion persistante avec le navigateur, etc.
  2. pas assez de mémoire pour simplement rajouter des processus ou des threads

Comment coder en asynchrone ?

  • utiliser une plateforme ou une bibliothèque faite pour ça
  • callbacks (ou variantes comme les promises)
  • generators

Presenter Notes