Un modèle MVC en javascript/ajax

il est possible d’imaginer un pseudo modèle MVC avec les technologies ajax/javascript/python.

Le modèle est présenter avec le schema suivant

_images/mvc_html.png

le principe est:

  • le naviguateur ne demande que des fichier statique au serveur html (img,js,html, css). Ce dernier représente la vue
  • le javascript permet sur le naviguateur de gérer la page, les évenements, les données. Ce dernier représente le modèle
  • le serveur bottle represente le contrôleur: il permet la mise à jours des données et échanges XML avec le modèle via l’utilisation d’AJAX (exemple de fonction: liste des articles, commande, ...)

l’intérêt principal de ce odèle est que le serveur httpd ne fournit en majorité que des pages statisque ce qui permet à l’application d’avoir de très bonne performance.

Les échanges entre le modèle et le contrôleur ne sont que des échanges de données ... pas de format. Il est possible alors de modifier l’ensemble du site et des intercation naviguateur/utilisateur en modifiant les css et le html.

Il y a une vraie sépration entre le contenu et le contenant

Au niveau des performances, l’utilisation d’Ajax change complément la répartition des charges. Dans une application web traditionnelle, le serveur est chargé de la plupart des traitements. Dans une application web Ajax, le navigateur client est de plus en plus sollicité pour effectuer des contrôles, notamment lors de la saisie des formulaires et pour afficher le nouveau contenu en provenance du serveur. Le nombre de requêtes total d’une application Ajax augmente, mais la quantité totale de données transmises a tendance à fortement diminué.

Les performances en termes de rapidités sont donc améliorées par l’utilisation de la technologie Ajax. En revanche, il n’est pas rare que la première page soit plus longue à charger car elle doit prendre en compte un plus grand nombre de librairies JavaScript nécessaires au bon fonctionnement de l’application, mais c’est sans comparaison avec un rechargement répété des pages. Les performances varient aussi considérablement en fonction des techniques utilisés pour permettre au serveur d’envoyer les informations au client: certains préfèrent envoyer de multiples requêtes brèves pour avoi des nouvelles du serveur alors que d’autres applications envoient une requête qui reste longtemps en attente côté serveur. Ces deux techniques ont leurs avantages et leurs inconvénient et le choix de l’une ou l’autre dépend par conséquent des besoins de l’application.

Même si l’utilisation d’Ajax permet d’améliorer les performances et surtout la simplicité d’utilisation des applications et des sites web, il faut garder à l’esprit lors du développement que tous les postes ne disposent parfois pas de la puissance nécessaire pour exécuter rapidement le code Javascript. C’est d’ailleurs pour cette raison que les navigateurs web ont tout miser ces dernières années sur la rapidité d’exécution de leurs moteurs Javascript.

la relation python / sgdb

Un des points crucial dans cette architecture est le relation entre le contrôleur et la base de donnée

En mode wsgi il est possible d’imaginer plusieurs solutions qui possèdent chacune des avantages et des défauts

dans nos exemples la structure sera la suivante

C:
 |_ www
    |_ test1
        |_ index.html
        |_ wsgi-scripts
           |_ myapp.wsgi
           |_ pypyodbc.py

Chaque exemple à pour but de fournir le même fichier xml.

On se connecte à une base postgresql en utilisant le module pypyodbc

En effetlors de nos tests nous avons rencontré des soucis avec le module pyodbc.

Nous avons l’erreur suivante

#[Sat May 25 18:41:45 2013] [error] [client 127.0.0.1]   File "C:/www/test1/wsgi-scripts/myapp.wsgi", line 16, in <module>
#[Sat May 25 18:41:45 2013] [error] [client 127.0.0.1]     import odbc
#[Sat May 25 18:41:45 2013] [error] [client 127.0.0.1]   File "C:\\Python\\Python27\\lib\\site-packages\\odbc-3.0.5-py2.7-win32.egg\\odbc.py", line 11, in <module>
#[Sat May 25 18:41:45 2013] [error] [client 127.0.0.1]   File "C:\\Python\\Python27\\lib\\site-packages\\odbc-3.0.5-py2.7-win32.egg\\odbc.py", line 10, in __bootstrap__
#[Sat May 25 18:41:45 2013] [error] [client 127.0.0.1] ImportError: DLL load failed: Le module sp\xe9cifi\xe9 est introuvable.

A priori il s’agit d’un soucis de dll de .net

Pour regler probleme voir http://code.google.com/p/odbc/issues/detail?id=214 et http://stackoverflow.com/questions/10193445/cannot-import-odbc-via-mod-wsgi-under-apache-on-windows

une connexion par demande

# Change working directory so relative paths (and template lookup) work again
import os, sys
os.chdir(os.path.dirname(__file__))
sys.path.append(os.path.dirname(__file__)) #permet l'utilisation de pypyodbc

import bottle
from bottle import route, run, template, static_file, request, response

import time
from xml.dom.minidom import getDOMImplementation

import Queue

import pypyodbc as odbc

CNX_STR='DSN=TESTPG;UID=postgres;PWD=postgres'
SQL='select count(*) from test'

def generate_response(value):
    impl = getDOMImplementation()
    newdoc = impl.createDocument(None,"sample",None)
    top_element = newdoc.documentElement
    att = newdoc.createElement('msg')
    txt = newdoc.createTextNode(str(value))
    att.appendChild(txt)
    top_element.appendChild(att)
    return newdoc.toxml('iso-8859-15')

@route('/')
def index():
    return static_file('index.html', root='C:/www/test1')

@route('/onebyquery')
def onebyquery():
    a = odbc.connect(CNX_STR)
    cursor = a.cursor()
    cursor.execute(SQL)
    row = cursor.fetchone()
    a.commit()
    response.headers['Content-Type'] = 'text/xml'
    return generate_response(row[0])

application = bottle.default_app()

avantage: chaque demande possède une connexion manager (commit et roolback possible) défaut: le temps de connexion qui augmente le temps de réponse. Il est possible de saturer le SGBD de demande

un benchmark

C:\Program Files\Apache Software Foundation\Apache2.2\bin>ab.exe -t 30 -c 5 http://test1.fr/myapp/onebyquery
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking test1.fr (be patient)
Finished 1088 requests


Server Software:        Apache/2.2.22
Server Hostname:        test1.fr
Server Port:            80

Document Path:          /myapp/onebyquery
Document Length:        73 bytes

Concurrency Level:      5
Time taken for tests:   30.009 seconds
Complete requests:      1088
Failed requests:        999
   (Connect: 0, Receive: 0, Length: 999, Exceptions: 0)
Write errors:           0
Non-2xx responses:      999
Total transferred:      984198 bytes
HTML transferred:       756746 bytes
Requests per second:    36.26 [#/sec] (mean)
Time per request:       137.908 [ms] (mean)
Time per request:       27.582 [ms] (mean, across all concurrent requests)
Transfer rate:          32.03 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        1    3   0.8      3       7
Processing:    74  135  97.8    112    1421
Waiting:       72  132  97.4    109    1413
Total:         76  137  97.7    114    1422

Percentage of the requests served within a certain time (ms)
  50%    114
  66%    131
  75%    154
  80%    171
  90%    197
  95%    224
  98%    260
  99%    309
 100%   1422 (longest request)

Nous avons un grand nombre de requête sans réponse

cela est dut à l’erreur suivante (trouvé dans le error.log d’apache)

[Sun May 26 20:32:03 2013] [error] [client 127.0.0.1] DatabaseError: (u'53300', u'[53300] FATAL: d\\xe9sol\\xe9, trop de clients sont d\\xe9j\\xe0 connect\\xe9s')

nous saturons la base en nombre de connexion

une connexion par serveur

# Change working directory so relative paths (and template lookup) work again
import os, sys
os.chdir(os.path.dirname(__file__))
sys.path.append(os.path.dirname(__file__)) #permet l'utilisation de pypyodbc

import bottle
from bottle import route, run, template, static_file, request, response

import time
from xml.dom.minidom import getDOMImplementation

import Queue

import pypyodbc as odbc

CNX_STR='DSN=TESTPG;UID=postgres;PWD=postgres'
SQL='select count(*) from test'

CNX = odbc.connect(CNX_STR)

def generate_response(value):
    impl = getDOMImplementation()
    newdoc = impl.createDocument(None,"sample",None)
    top_element = newdoc.documentElement
    att = newdoc.createElement('msg')
    txt = newdoc.createTextNode(str(value))
    att.appendChild(txt)
    top_element.appendChild(att)
    return newdoc.toxml('iso-8859-15')

@route('/')
def index():
    return static_file('index.html', root='C:/www/test1')

@route('/onebyserver')
def onebyserver():
    cursor = CNX.cursor()
    cursor.execute(SQL)
    row = cursor.fetchone()
    CNX.commit()
    response.headers['Content-Type'] = 'text/xml'
    return generate_response(row[0])

application = bottle.default_app()

défaut: chaque demande ne possède pas une connexion manager (pas de commit ou de roolback possible).Il est possible d’avoir des ralentissements si plusieurs cursors sur la même connexion. si un cursor génère un database error on perd toutes les connexions

avantage: le temps de réponse optimal car pas de reconnexion. Il n’est pas possible de saturer le SGBD de demande.

un benchmark

C:\Program Files\Apache Software Foundation\Apache2.2\bin>ab.exe -t 30 -c 5 http://test1.fr/myapp/onebyserver
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking test1.fr (be patient)
Finished 3455 requests


Server Software:        Apache/2.2.22
Server Hostname:        test1.fr
Server Port:            80

Document Path:          /myapp/onebyserver
Document Length:        73 bytes

Concurrency Level:      5
Time taken for tests:   30.004 seconds
Complete requests:      3455
Failed requests:        0
Write errors:           0
Total transferred:      860295 bytes
HTML transferred:       252215 bytes
Requests per second:    115.15 [#/sec] (mean)
Time per request:       43.421 [ms] (mean)
Time per request:       8.684 [ms] (mean, across all concurrent requests)
Transfer rate:          28.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        1    3   0.9      3       6
Processing:    19   40   9.1     39      90
Waiting:        9   36   9.7     35      86
Total:         20   43   9.1     42      93

Percentage of the requests served within a certain time (ms)
  50%     42
  66%     46
  75%     49
  80%     51
  90%     56
  95%     61
  98%     63
  99%     66
 100%     93 (longest request)

utilisation d’un pool de connexion

Pour réaliser le pool nous utilisons le module Queue

il est possible d’améloirer le fonctionnement du pool.

# Change working directory so relative paths (and template lookup) work again
import os, sys
os.chdir(os.path.dirname(__file__))
sys.path.append(os.path.dirname(__file__)) #permet l'utilisation de pypyodbc

import bottle
from bottle import route, run, template, static_file, request, response

import time
from xml.dom.minidom import getDOMImplementation

import Queue

import pypyodbc as odbc

CNX_STR='DSN=TESTPG;UID=postgres;PWD=postgres'
SQL='select count(*) from test'
SIZE_POOL=10

class PoolConnect(Queue.Queue):

    def put(self, item, block=True, timeout=None):
        item.commit()
        Queue.Queue.put(self, item, block, timeout)

POOL = PoolConnect(maxsize=SIZE_POOL)
for i in range(0,SIZE_POOL):
    POOL.put(odbc.connect(CNX_STR))


def generate_response(value):
    impl = getDOMImplementation()
    newdoc = impl.createDocument(None,"sample",None)
    top_element = newdoc.documentElement
    att = newdoc.createElement('msg')
    txt = newdoc.createTextNode(str(value))
    att.appendChild(txt)
    top_element.appendChild(att)
    return newdoc.toxml('iso-8859-15')

@route('/')
def index():
    return static_file('index.html', root='C:/www/test1')

@route('/bypool')
def bypool():
    a = POOL.get()
    cursor = a.cursor()
    cursor.execute(SQL)
    row = cursor.fetchone()
    POOL.put(a)
    response.headers['Content-Type'] = 'text/xml'
    return generate_response(row[0])

application = bottle.default_app()

défaut: il est possible que certaines demandes de connexion soit longue si la queue est vide

avantage: on gère nos connexions (en nombre et en commit). On ne peut saturer le serveur.

un benchmark pour un pool de 10

C:\Program Files\Apache Software Foundation\Apache2.2\bin>ab.exe -t 30 -c 5 http://test1.fr/myapp/bypool
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking test1.fr (be patient)
Finished 4210 requests


Server Software:        Apache/2.2.22
Server Hostname:        test1.fr
Server Port:            80

Document Path:          /myapp/bypool
Document Length:        73 bytes

Concurrency Level:      5
Time taken for tests:   30.009 seconds
Complete requests:      4210
Failed requests:        0
Write errors:           0
Total transferred:      1048290 bytes
HTML transferred:       307330 bytes
Requests per second:    140.29 [#/sec] (mean)
Time per request:       35.640 [ms] (mean)
Time per request:       7.128 [ms] (mean, across all concurrent requests)
Transfer rate:          34.11 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        1    4   1.1      4      11
Processing:    13   31   4.8     30     104
Waiting:        7   23   7.1     23     102
Total:         17   35   4.8     34     107

Percentage of the requests served within a certain time (ms)
  50%     34
  66%     35
  75%     36
  80%     37
  90%     40
  95%     42
  98%     47
  99%     53
 100%    107 (longest request)

un benchmark pour un pool de 20

C:\Program Files\Apache Software Foundation\Apache2.2\bin>ab.exe -t 30 -c 5 http://test1.fr/myapp/bypool
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking test1.fr (be patient)
Finished 4279 requests


Server Software:        Apache/2.2.22
Server Hostname:        test1.fr
Server Port:            80

Document Path:          /myapp/bypool
Document Length:        73 bytes

Concurrency Level:      5
Time taken for tests:   30.009 seconds
Complete requests:      4279
Failed requests:        0
Write errors:           0
Total transferred:      1065720 bytes
HTML transferred:       312440 bytes
Requests per second:    142.59 [#/sec] (mean)
Time per request:       35.065 [ms] (mean)
Time per request:       7.013 [ms] (mean, across all concurrent requests)
Transfer rate:          34.68 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        1    4   1.0      4      13
Processing:    17   31   4.5     29      99
Waiting:        7   23   6.7     22      90
Total:         19   35   4.5     33     102

Percentage of the requests served within a certain time (ms)
  50%     33
  66%     34
  75%     35
  80%     35
  90%     40
  95%     43
  98%     46
  99%     49
 100%    102 (longest request)

comparaison

5 conn simul. temps moyens (ms)
par demande par serveur par pool de 10 par pool de 20 27.582 43.421 35.640 35.065

A priori le temps de traitement sans erreur est meilleur avec un pool de 20

Mais avec une base locale et une requête simple les écarts restent négligeable