Ecosystème Python pour le web : quelle architecture, quel framework pour quelle problématique

Ecosystème Python pour le web : quelle architecture, quel framework pour quelle problématique

Ce talk examine plusieurs solutions pour construire des sites et applications web à l'aide du langage python, en fonction des problématiques à résoudre.

Après un tour d'horizon et une catégorisation macro des différentes librairies et frameworks populaires, nous examinerons les principales fonctions offertes par ceux-ci. Une fois le framework choisi, plusieurs solutions sont envisageables pour traiter les requêtes entrantes. Nous aborderons les différentes mises en oeuvre de la concurrence en fonction du type de performances attendues (I/O, CPU...) avec un accent sur la gestion évènementielle popularisée dans le monde Javascript par node.js.

Le talk se conclura par une revue d'exemples d'architectures utilisables pour des besoins typiques du web d'aujourd'hui : Single Page Application, Real time, IoT, microservices...

9a53fbe79231c78c893f258af967096e?s=128

Yann Gravrand

March 25, 2016
Tweet

Transcript

  1. OFUOH OFUOH ECOSYSTÈME PYTHON POUR LE WEB Yann Gravrand BreizhCamp

    2016 - Rennes Quelle architecture, quel framework pour quelle problématique
  2. ›  Net-ng - Rennes 20 pers Applications métier en Python

    Forte orientation web Framework web open source à base de composants : www.nagare.org Présentation
  3. @ygravrand  

  4. @ygravrand  

  5. ›  Autour du développement web en Python Les grands types

    de librairies et frameworks Différentes façons de traiter les requêtes Des exemples de la vraie vie pour vous inspirer Ce talk
  6. ›  POC ›  Prod ›  100k connexions en qq jours

    …?? Une anecdote
  7. « FULL STACK » FRAMEWORKS « MICRO » FRAMEWORKS TOUR

    D’HORIZON
  8. ›  Fournissent des solutions à la plupart des problèmes classiques

    d’une application web : Authentification / sécurité Gestion des URLs Persistance / ORM Génération HTML : moteur de templating, composants… Interaction avec le navigateur : JS, CSS… ›  Exemples Django, Pyramid, Zope, Nagare, CubicWeb… « Full stack » frameworks
  9. ›  Django License BSD depuis 2005 Le plus connu Grosse

    communauté (DSF, DjangoCon…) 18200 ★, 1100 contributeurs Github Projets autour : DjangoCMS… •  Modèle MVC (MTV) •  Langages de templates •  Validation de formulaires •  Persistance via ORM •  Administration avec IHM CRUD automatiques •  … ›  Remarques Peut ressembler à Ruby on Rails (en plus explicite), Play… Beaucoup d’ « applications » (extensions) Pas de support particulier d’AJAX « Full stack » frameworks : exemples
  10. ›  Zope Version 2 : 1998 Version 3 : 2004

    •  ZODB : Base objet •  Langages de templates : DTML, ZPT •  CMS : Plone, CPS jusqu’à 2007 ›  Pyramid 2008 •  Racines Zope •  Modèle « MVC » (RV) •  Langages de templates : Chameleon, Jinja2, … •  Persistance pluggable (ZODB, relationnel…) « Full stack » frameworks : exemples
  11. ›  Nagare 2009 •  Approche composants •  Ecriture des vues

    en Python : pas de langage de templating nécessaire •  Persistance via ORM •  Abstractions de navigation par callbacks •  Sérialisation de l’état complet de l’application •  Ajax : par changement de renderer « Full stack » frameworks : exemples
  12. ›  N’adressent volontairement que des parties essentielles : Gestion des

    URLs API accès aux éléments de la requête / réponse Intégration d’un moteur de templating pour la génération HTML ›  Simples dans l’approche ›  Pour des use cases simples… ›  … Attention à l’évolutivité de l’application « Micro » frameworks
  13. ›  Flask Créé par A. Ronacher 18700 ★, 288 contributeurs

    Github Moteur de templating (Jinja2) Mécanisme d’extensions pour le reste (DB, Auth, Forms…) Facilement RESTful Bonne doc ›  Bottle 3550 ★, 130 contributeurs Github Moteur de templating builtin et support de plusieurs formats ›  Web.py, … Plus très actif… « Micro » frameworks : exemples
  14. « Micro » frameworks : exemples # Encoding: utf-8 from

    flask import Flask, render_template app = Flask(__name__) @app.route('/status') def status(): return 'I\'m Flask, up and running!' @app.route('/', defaults={'name': 'Flask'}) @app.route('/hello/<name>') def index(name): return render_template('hello.html', name=name) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) <!doctype html> <title>Hello from Flask</title> {% if name %} <h1>Hello {{ name }}!</h1> {% else %} <h1>Hello World!</h1> {% endif %} templates/   hello.html   app.py  
  15. « Micro » frameworks : exemples from bottle import route,

    run, template @route('/hello/<name>') def index(name): return template('<b>Hello {{name}}</b>!', name=name) run(host='localhost', port=8080)
  16. OULÀ, PAS TROP VITE MULTI THREADING MULTI PROCESSING EVÉNEMENTIEL TRAITER

    LES REQUÊTES
  17. ›  Bloquant i.e : on attend que la requête soit

    traitée dans son intégralité avant d’en accepter une autre Pas trop vite : une requête à la fois Req.  1   Req.  2   Req.  3   Temps  
  18. ›  Grosse capacité en cuisine ›  Un(e) seul(e) hôte(sse) de

    caisse ›  File de clients ›  On ne passe pas au client suivant tant que le premier n’est pas servi ›  L’hôte(sse) bloque… Le fast food : ex 1 Temps   Client  1   Client  2   Client  3                
  19. h<p://contac.tumblr.com/post/114816953764  

  20. github.com/ygravrand/events/breizhcamp2016/ex1_one_at_a_time Ex 1 : flask dev # Encoding: utf-8 import

    time from flask import Flask app = Flask(__name__) def kitchen_work(): time.sleep(5) @app.route('/') def fast_food_host(): print 'Order sent to the kitchen, waiting...' kitchen_work() print 'Burger received from kitchen!' return 'Here\'s your order.' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)
  21. ./fast_food.sh ./clients.sh (ab -n 3 -c 3) 3 req *

    5 sec = 15 sec Ex 1 : flask dev * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) Order sent to the kitchen, waiting... 192.168.99.1 - - [18/Mar/2016 15:32:28] "GET / HTTP/1.0" 200 - Burger received from kitchen! Order sent to the kitchen, waiting... Burger received from kitchen! Order sent to the kitchen, waiting... 192.168.99.1 - - [18/Mar/2016 15:32:33] "GET / HTTP/1.0" 200 - Burger received from kitchen! 192.168.99.1 - - [18/Mar/2016 15:32:38] "GET / HTTP/1.0" 200 - Concurrency Level: 3 Time taken for tests: 15.036 seconds
  22. ›  Réflexe possible : utiliser les threads (Java qqu’un ?)

    Traiter plusieurs requêtes en // Req.  1   Req.  2   Req.  3   Temps   Thread  1   Thread  2   Thread  3  
  23. ›  Threads : plus légers que les processus ›  Threads

    Python : Ce sont bien des threads OS (scheduling en interaction avec l’OS) Mais 1 seul thread permis à un instant t par l’interpréteur Python (pas de concurrence) … détails ci-après Multi threading
  24. ›  1) You do not talk about the GIL › 

    2) You DO NOT talk about the GIL ›  3) One fight at a time (on the processor) The rules of Python.
  25. ›  Un nom : GIL (Global Interpreter Lock) voir https://wiki.python.org/moin/GlobalInterpreterLock

    ›  GVL en Ruby ›  Pas dans toutes les implémentations Python Cpython, PyPy Pas Jython, IronPython ›  Pourquoi ? Pour protéger les structures de données internes qui ne sont pas thread safe, pour simplifier les problèmes de concurrence, les interactions avec les extensions C, etc Multi threading / GIL
  26. ›  Parallélisme Pas d’exécution parallèle des threads Sur plusieurs cœurs,

    exécution des threads sérialisée Et ce, même si l’OS « a envie » d’exécuter plusieurs threads simultanément Multi threading Temps   Thread  1   Thread  2   Thread  3  
  27. ›  CPU Bound Temps d’exécution limité par le CPU Application

    effectuant majoritairement des calculs Ex : traitement d’images en mémoire Dans notre fast food : la plonge, peler des patates ›  I/O Bound Temps d’exécution limité par les E/S Application effectuant beaucoup d’E/S Ex : interfaces avec bases de données, systèmes de fichiers, autres systèmes externes Dans notre fast food : les hôte(sse)s Définitions
  28. ›  CPU Bound ! Multi threading pas recommandé en Python

    ! Multi process ? Cf. plus tard ›  I/O Bound !  Multi threading : GIL pas forcément trop impactant Modèle plutôt léger !  D’autres alternatives si énormément de requêtes / s cf. plus tard Implications
  29. ›  Un processus parent crée plusieurs processus enfants et les

    coordonne ›  Par ex. avec le module multiprocessing ›  Pool de processus Multi processing
  30. ›  Effet minimal du GIL : X GIL ›  Utilise

    tous les CPUs / cœurs ›  « Safe » : pas de corruption liée à un partage de mémoire ›  Encourage de bonnes pratiques pour la scalabilité (stateless, passage de messages) différents processus ≈ différentes machines ›  Passage de messages entre processus : coûteux Multi processing : pour la concurrence
  31. ›  Un processus gère une requête ›  Processus : lourds

    à créer, limités en nb ›  Pool de processus en pre-fork ›  Ex : Gunicorn mode synchrone Multi processing : pour le web Req.  1   Req.  2   Req.  3   Temps   Process  1   Process  2   Process  3   Req.  4  
  32. ›  Grosse capacité en cuisine ›  Plusieurs hôte(sse)s de caisses

    ›  Chaque hôte(sse) gère chaque client séquentiellement, et passe du temps à attendre Le fast food : ex 2 Temps   Hôte(sse)  1   Hôte(sse)  2   Hôte(sse)  3   Client  1   Client  2   Client  3                 File  de  clients  
  33. Hôte(sse)  1   Hôte(sse)  2   Hôte(sse)  3   File

     de  clients  
  34. breizhcamp2016/ex2_multi_process ./fast_food.sh ./clients.sh (ab -n 3 -c 3) Ex 2

    : flask gunicorn [2016-03-18 15:43:22 +0000] [1] [INFO] Starting gunicorn 19.4.5 [2016-03-18 15:43:22 +0000] [1] [INFO] Listening at: http:// 0.0.0.0:8000 (1) [2016-03-18 15:43:22 +0000] [1] [INFO] Using worker: sync [2016-03-18 15:43:22 +0000] [11] [INFO] Booting worker with pid: 11 [2016-03-18 15:43:23 +0000] [12] [INFO] Booting worker with pid: 12 [2016-03-18 15:43:23 +0000] [13] [INFO] Booting worker with pid: 13 [2016-03-18 15:43:23 +0000] [14] [INFO] Booting worker with pid: 14 Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Burger received from kitchen! Burger received from kitchen! Burger received from kitchen! Concurrency Level: 3 Time taken for tests: 5.021 seconds
  35. ›  Si bon dimensionnement / cuisine, optimise le temps en

    cuisine et le temps d’attente des clients ›  Traitements CPU « raisonnables » possibles (encaissement, …) ›  Attention à ne pas peler des patates ! Pas désastreux mais ça bloque une file Ex 2 : conclusions
  36. ./sports_fan_bus.sh (ab -n 50 -c 50) 50 supporters rentrent en

    même temps 50 req / 4 workers * 5 sec = 62.5 sec Ex 2 : limité au nombre d’hôte(sse)s… Concurrency Level: 50 Time taken for tests: 65.260 seconds
  37. ›  Si j’ai à gérer plusieurs dizaines de milliers de

    clients ? ›  Et que ma cuisine a cette capacité ? http://www.kegel.com/c10k.html 1Ok problem
  38. ›  Autres termes : asynchrone, non bloquant, … ›  Un

    élément central (Reactor / Event loop) traite une file d’évènements ›  Dès qu’un traitement nécessite des I/O, on passe à autre chose ›  Vous êtes vous-même une boucle événementielle Evénementiel : principes
  39. ›  Capacité énorme en cuisine ›  Un(e) seul(e) hôte(sse) de

    caisse ›  Passe au client suivant dès qu’une ligne (burger, frites…) nécessite de l’attente ›  Les lignes préviennent l’hôte(sse)… ›  Qui n’est pas bloquée(e) Le fast food : ex 3
  40. Le fast food : ex 3 Temps   Cde  1

      finie   Cde  1   Cde  2   finie   Cde  2   Hôte(sse)  
  41. ›  Explicitement Callbacks (mécanisme de rappel) Coroutines explicites ›  Implicitement

    Green threads Comment rendre la main ?
  42. Ex 3 : callbacks avec twisted breizhcamp2016/ex3_callbacks # Encoding: utf-8

    from twisted.web import ... def kitchen_work(get_result): return deferLater(reactor, 5, get_result) class FastFoodResource(resource.Resource): def on_burger_ready(self, request): print 'Burger received from kitchen!’ request.setHeader("content-type", "text/plain") request.write('Here\'s your order.') request.finish() def render_GET(self, request): print 'Order sent to the kitchen, waiting...' deferred = kitchen_work(lambda: request) deferred.addCallback(self.on_burger_ready) return server.NOT_DONE_YET if __name__ == "__main__": reactor.listenTCP(5000, server.Site(FastFoodResource())) reactor.run()
  43. ./sports_fan_bus.sh (ab -n 50 -c 50) 50 req * 5

    sec = 5 sec Ex 3 : twisted Concurrency Level: 50 Time taken for tests: 5.120 seconds Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... [...] Burger received from kitchen! Burger received from kitchen! Burger received from kitchen! Burger received from kitchen!
  44. breizhcamp2016/ex3_aiohttp Ex 3 : aiohttp / asyncio, PY3.4 # Encoding:

    utf-8 import asyncio from aiohttp import web @asyncio.coroutine def kitchen_work(): yield from asyncio.sleep(5) @asyncio.coroutine def fast_food_host(request): print('Order sent to the kitchen, waiting...', flush=True) yield from kitchen_work() print('Burger received from kitchen!', flush=True) return web.Response(body='Here\'s your order.'.encode('utf-8')) app = web.Application() app.router.add_route('GET', '/', fast_food_host) if __name__ == '__main__': web.run_app(app, host='0.0.0.0', port=8000)
  45. ./sports_fan_bus.sh (ab -n 50 -c 50) 50 req * 5

    sec = 5 sec Ex 3 : aiohttp / asyncio, PY3.4 Concurrency Level: 50 Time taken for tests: 5.267 seconds Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... Order sent to the kitchen, waiting... [...] Burger received from kitchen! Burger received from kitchen! Burger received from kitchen! Burger received from kitchen!
  46. breizhcamp2016/ex3_tornado breizhcamp2016/ex3_asyncio_py35 Ex 3 : autres implémentations

  47. ›  Nécessite d’écrire toute l’application en utilisant ce style :

    Callbacks yield / yield from / async await Style « non bloquant » ›  Autre façon de penser la concurrence Plus nécessairement besoin de mutex, lock, … Autres paradigmes Ex 3 : conclusions / implémentation
  48. ›  Intéressant si travail d’hôte(sse) est surtout de l’orchestration › 

    La limite est en cuisine : I/O Bound ›  CPU réduit au minimum : encaissement rapide ;-) Ex 3 : conclusions
  49. ›  Green threads coopératifs ›  ≠ threads OS Ex 4

    : gevent
  50. ›  J’écris mon application comme dans l’exemple 1 ! › 

    C’est « magique » ?... Ex 4 : Gunicorn + Flask + gevent
  51. breizhcamp2016/ex4_gevent Ex 4 : Gunicorn + Flask + gevent #

    Encoding: utf-8 import time from flask import Flask app = Flask(__name__) def kitchen_work(): time.sleep(5) @app.route('/') def fast_food_host(): print 'Order sent to the kitchen, waiting...' kitchen_work() print 'Burger received from kitchen!' return 'Here\'s your order.' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) Concurrency Level: 50 Time taken for tests: 5.266 seconds
  52. ›  Fonctions monkey-patchées pour utiliser leurs versions non bloquantes :

    time.sleep ! gevent.sleep sockets... ›  Gunicorn appelle monkey.patch_all de gevent Ex 4 : Gunicorn + Flask + gevent
  53. ›  Simplicité (en surface...) ›  Pas pur python, nécessite C

    & ASM ›  Sur 1 process : toujours pas d’utilisation des CPU/cœurs multiples Ex 4 : conclusions
  54. ›  Norme pour connecter votre serveur web frontal à votre

    application web WSGI : Web Server Gateway Interface Serveur  web   Connecteur  /   serveur  WSGI   Votre   applicaTon   AJP   FastCGI   …   API   programmaTque   X  process   X  process  
  55. ›  Norme pour connecter votre serveur web frontal à votre

    application web WSGI : Web Server Gateway Interface Apache   Nginx…   Flup   Gunicorn   uWSGI…   Django   Flask   Pyramid…   AJP   FastCGI   …   API   programmaTque   X  process   X  process  
  56. ›  API : appel de fonction ›  Interface synchrone –

    bloquante ›  Multi thread, multi process : OK ›  Serveurs événementiels à base de callback, yield (ex 3) : KO ›  Green threads (ex 4) : OK WSGI : Web Server Gateway Interface def application(env, start_response): start_response('200 OK', [('Content-Type','text/html')]) return [b"Hello World"]
  57. DES PROPOSITIONS D’ARCHITECTURE, DE TECHNOS, FRAMEWORKS CAS D’USAGE

  58. ›  Framework Full Stack Intranet / CMS Votre   applicaTon

      DB   Cache   Files   d’a<ente   Client  web   Full  Stack  Framework   MulT  process  /   mulT  thread  
  59. ›  Javascript + API REST SPA (Single Page Application) Appli

      Javascript   Appli   Python   API   REST   Client  web   Micro  Framework   MulT  process  /   mulT  thread  
  60. Micro services (attention buzzword) Micro  service  mail   API  

    REST   Micro  Framework   MulT  process  /  mulT  thread   Appli   principale   File   d’a<ente   Micro  service  persistance   API   REST   Micro  Framework   MulT  process  /  mulT  thread   DB  
  61. ›  Bcp de petites connexions très fréquentes IoT (Internet of

    Things) API   REST   Micro  Framework   Appli  Python   EvénemenTel   Traitements   File   Pool  de  process   Traitements  
  62. ›  Connexions à maintenir ouvertes Real time Appli   Javascript

      Client  web   Websockets   /home…   Bus   Appli  Python  principale   Full  Stack  Framework   MulT  process  /  mulT  thread   Appli  RealTme   EvénemenTel   Tornado,  Autobahn,  …  
  63. ›  Gevent, multi process ›  Psycogreen / Psycopg2 ›  2500

    req/s (pas juste du 200 OK) sur 1 machine (OK bien puissante) ›  Python c’est lent ;-) Et notre anecdote…
  64. ›  Docs. Autobahn : bonnes explications http://autobahn.ws/python/asynchronous- programming.html ›  Docs

    Tornado, Twisted, Gevent ›  GIL Dave Beazley - Understanding the Python GIL : https://www.youtube.com/watch?v=Obt-vMVdM8s Ressources
  65. ›  Questions ? @ygravrand github.com/ygravrand/events Merci de votre attention Gif

     credits:  gify