Journal Le taptempo du web

Posté par (site web personnel) . Licence CC By‐SA.
11
15
juin
2022

Un journal précédent vient de lancer un taptempo du web. Le but est d'évaluer des technos quand il s'agit de faire un backend qui ne fait qu'envoyer une redirection 302 (cf le journal en dessous pour les détails).

La vitesse est mesuré par ab en local, wrk a aussi été proposé. L'empreinte mémoire est mesuré avec "ps aux" pour avoir le VSZ et le RSS.

On a pour l'instant 9k requêtes/s et 8 Mo en Java, 23k requêtes/s et 350Ko en Rust, 19k requêtes/s en Haskell. Bien sûr il faudrait lancer ces tests sur une même machine pour être fairplay.

Journal d'origine : https://linuxfr.org/users/spacefox/journaux/java-presque-9-000-requetes-par-seconde-avec-8-mo-de-ram

  • # En Python (Flask)

    Posté par (Mastodon) . Évalué à 4.

    Bon, histoire de jouer j'ai lancé sur mon ordi portable du boulot le script suivant en Python. Fait en 2mn chrono, sans aucune recherche d'optimiser quoi que ce soit. Le CPU est un Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz .

    from flask import Flask, redirect
    app = Flask(__name__)
    @app.route("/")
    def index():
     return redirect("http://www.example.com", code=302)

    Voici la ligne ps aux :

    xxx 164633 111 0.1 728040 27716 pts/2 Sl+ 11:29 0:56 /home/xxx/src/python/redir/.venv/bin/python /home/xxx/src/python/redir/.venv/bin/flask run
    

    Et le résultat de ab :

    $ ab -n 100000 -c 10 http://localhost:5000/
    This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    Benchmarking localhost (be patient)
    Completed 10000 requests
    Completed 20000 requests
    Completed 30000 requests
    Completed 40000 requests
    Completed 50000 requests
    Completed 60000 requests
    Completed 70000 requests
    Completed 80000 requests
    Completed 90000 requests
    Completed 100000 requests
    Finished 100000 requests
    Server Software: Werkzeug/2.1.2
    Server Hostname: localhost
    Server Port: 5000
    Document Path: /
    Document Length: 231 bytes
    Concurrency Level: 10
    Time taken for tests: 70.681 seconds
    Complete requests: 100000
    Failed requests: 0
    Non-2xx responses: 100000
    Total transferred: 44200000 bytes
    HTML transferred: 23100000 bytes
    Requests per second: 1414.81 [#/sec] (mean)
    Time per request: 7.068 [ms] (mean)
    Time per request: 0.707 [ms] (mean, across all concurrent requests)
    Transfer rate: 610.69 [Kbytes/sec] received
    Connection Times (ms)
     min mean[+/-sd] median max
    Connect: 0 0 0.0 0 2
    Processing: 2 7 1.9 7 52
    Waiting: 1 7 1.8 6 52
    Total: 2 7 1.9 7 52
    Percentage of the requests served within a certain time (ms)
     50% 7
     66% 7
     75% 7
     80% 8
     90% 9
     95% 10
     98% 11
     99% 14
     100% 52 (longest request)
    

    Je vous laisse interpréter tout ça, j'ai pas trop suivi l'ensemble des débats du journal original :)

    En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

    • [^] # Re: En Python (Flask)

      Posté par (Mastodon) . Évalué à 6.

      Alors pour faire suite à mon commentaire originel que vous trouverez avec le code python en Bottle ici :
      https://linuxfr.org/users/spacefox/journaux/java-presque-9-000-requetes-par-seconde-avec-8-mo-de-ram#comment-1893297

      Rappel des épisodes précédents : le benchmark avec ab donne 3400 req/s en fournissant les images dans la réponse, et seulement 2800 req/s avec la redirection 302, avec 21Mo de RAM utilisée.

      En utilisant le serveur gunicorn (simplement en mettant server="gunicorn", workers=5 dans l'appel à bottle.run()) on monte à 14000 req/s, la machine ayant 4 cœurs, augmenter le nombre de workers au delà de 5 ne fait absolument rien gagner. Mais là on est à 30Mo de RAM pour le master, et 21Mo de RAM pour chacun des 5 workers. Rapide mais on le paye !

      J'ai tenté de mettre l'application bottle derrière apache et le mod_wsgi (processes=5 threads=2), on est à 12000 req/s, les processus mod_wsgi font 23Mo de RAM chacun, on est quasiment à la vitesse et aux ressources de gunicorn mais avec le reste des possibilités d'apache, si on en a besoin.

      Et puis j'ai poussé un petit peu, tester les différents serveurs disponibles comme backend de bottle, il y en a plein.
      Et je suis tombé sur bjoern, qui utilise la libev, bibliothèque C de gestion d'évènements, très - très - rapide.
      On monte à 17000 req/s, en monothread et 20Mo de RAM.
      C'est assez bluffant par rapport à toutes les autres solutions, surtout par rapport aux 3400 du serveur par défaut !

      Mais bon, en fait, pourquoi pas lancer l'application bjoern 5 fois et utiliser un load-balancer devant ?
      lighttpd fait ça très bien et on est à 13000 req/s, honorable, mais sans intérêt.
      Alors on va utiliser un vrai load-balancer : haproxy !
      Et donc 5 backend appli standalone avec bjoern, un haproxy en round-robin devant, on passe à 19500 req/s !
      On a le même score avec 4 backend, la machine a 4 cœurs, fait tourner haproxy et aussi l'outil de benchmark, on sature sans surcharger le nombre de backend.
      Si on descends à 2 backend, on monte à 19700 req/s, à 3 backend on est à 19800 req/s.
      On s'y attends un peu : 5 processus qui bossent (3 backends bottle/bjoern, haproxy, et ab pour les tests) sur 4 cœurs c'est en général ce qui optimise le plus, mais c'est assez marginal.
      Bien sûr, là on est à 3*20Mo+8Mo = 68Mo.

      En bref haproxy ça déchire, on le savait déjà, on le constate encore, 8Mo de RSS, c'est tout mini.
      Et le serveur WSGI bjoern est très impressionnant dans l'écosystème WSGI.
      Et on descends pas en dessous de 20Mo de RAM pour un processus Python.
      Et le Bottle.redirect() est plus lent, avec 3*bjoern+haproxy on est à 15000 req/s, c'est naze.

      Conclusion, fournir le fichier directement et ne pas faire de redirection 302 (de toute façon, derrière, côté navigateur, on fait deux requêtes, c'est atroce), utiliser bjoern en server WSGI, et si on vaut load-balancer entre plusieurs serveurs, dégainer haproxy.

      • Yth.

      Pour info le /etc/haproxy/haproxy.cfg utilisé est le suivant (les timeout servent pas dans notre cas, mais haproxy geint si on les met pas) :

      defaults http
       mode http
       timeout client 1m
       timeout server 1m
       timeout connect 10s
       timeout http-keep-alive 2m
       timeout queue 15s
      frontend img
       bind :8001 name clear
       default_backend bimg
      backend bimg
       balance roundrobin
       server Server00 127.0.0.1:8080 check
       server Server01 127.0.0.1:8081 check
       server Server02 127.0.0.1:8082 check
       #server Server03 127.0.0.1:8083 check

      Et le code final bottle, avec exactement deux dépendances python : bottle et bjoern

      #!/usr/bin/python3
      from bottle import route, run, response
      import glob
      import random
      import sys
      def readfile(path):
       try:
       with open(path, 'rb') as f:
       a = f.read()
       return (a, len(a), f"image/{path[-3:]}")
       except Exception:
       raise
       return None
      files = [
       x
       for ext in ['jpg', 'png']
       for path in glob.glob(f'*.{ext}')
       for x in [readfile(path)]
       if x
      ]
      @route('/')
      def img():
       img, size, ctyp = random.choice(files)
       response.set_header('Content-Type', ctyp)
       response.add_header('Content-Length', size)
       return img
      try:
       port = int(sys.argv[1])
      except Exception:
       port = 8080
      run(host='localhost', port=port, server='bjoern')
  • # nginx / openresty

    Posté par . Évalué à 3. Dernière modification le 15 juin 2022 à 13:31.

    Avec nginx / openresty (il faut openresty pour pouvoir générer la valeur aléatoire avec la commande set_random)

    worker_processes auto;
    worker_rlimit_nofile 100000;
    error_log logs/error.log crit;
    events {
     use epoll;
     multi_accept on;
     worker_connections 4000;
    }
    http {
     access_log /dev/null;
     sendfile on;
     tcp_nopush on;
     tcp_nodelay on;
     gzip off;
     server {
     listen 6666;
     error_page 302 @30x;
     location @30x {
     default_type "";
     return 300;
     }
     location / {
     set_random $rid 1 21;
     rewrite ^/*$ https://avatar.spacefox.fr/Renard-${rid}.png redirect;
     }
     }
    }

    Commande: openresty -p "$PWD" -c ./nginx.conf

    ps aux:

    USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
    user 114966 0.0 0.0 11500 1320 ? Ss 12:35 0:00 nginx: master process
    user 114967 0.0 0.0 13164 4068 ? S 12:35 0:00 nginx: worker process
    user 114968 0.0 0.0 13164 4068 ? S 12:35 0:00 nginx: worker process
    user 114969 0.0 0.0 13164 4068 ? S 12:35 0:00 nginx: worker process
    user 114970 0.0 0.0 13164 4068 ? S 12:35 0:00 nginx: worker process

    Résultat, sur un i7-4900MQ 4 cœurs 2.8GHz

    $ ab -q -n 100000 -c 10 http://localhost:6666/ 
    This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    Benchmarking localhost (be patient).....done
    Server Software: openresty/1.21.4.1
    Server Hostname: localhost
    Server Port: 6666
    Document Path: /
    Document Length: 0 bytes
    Concurrency Level: 10
    Time taken for tests: 3.214 seconds
    Complete requests: 100000
    Failed requests: 0
    Non-2xx responses: 100000
    Total transferred: 18856993 bytes
    HTML transferred: 0 bytes
    Requests per second: 31116.39 [#/sec] (mean)
    Time per request: 0.321 [ms] (mean)
    Time per request: 0.032 [ms] (mean, across all concurrent requests)
    Transfer rate: 5730.09 [Kbytes/sec] received
    Connection Times (ms)
     min mean[+/-sd] median max
    Connect: 0 0 0.0 0 2
    Processing: 0 0 0.1 0 4
    Waiting: 0 0 0.1 0 4
    Total: 0 0 0.1 0 4
    Percentage of the requests served within a certain time (ms)
     50% 0
     66% 0
     75% 0
     80% 0
     90% 0
     95% 0
     98% 0
     99% 0
     100% 4 (longest request)

    À noter que le nombre de requètes/s dépend de l'ordinateur utilisé. Et aussi que "ab" est mono-threadé et est donc limité en performances (htstress -n 200000 -c 10 -t 8 retourne 85595 requètes/s)

  • # re- python server.http

    Posté par . Évalué à 3. Dernière modification le 15 juin 2022 à 16:32.

    Je reprends l’implémentation en python http.server que j'avais donne, mais cette fois-ci elle renvoit bien un 302 au lieu de servir un fichier, ce qui la rend éligible a ce petit jeu:

    import http.server # Our http server handler for http requests 
    import os 
    import socketserver # Establish the TCP Socket connections 
    PORT = 9000 
    print(os.getpid()) 
    class MyHttpRequestHandler(http.server.BaseHTTPRequestHandler): 
     def do_GET(self): 
     return self.send_response(302, "Hello") 
    Handler = MyHttpRequestHandler 
    with socketserver.TCPServer(("", PORT), Handler) as httpd: 
     print("Http Server Serving at port", PORT) 
     httpd.serve_forever() 
    

    Ca me donne 8.4k requests/s pour 7.8 MB de ram.

    • [^] # Re: re- python server.http

      Posté par . Évalué à 3.

      En relisant, le self.path = 'index.html' ne sert a rien. On va peut etre gratter 0.1 MB de ram en l'enlevant :-)

      • [^] # Re: re- python server.http

        Posté par (Mastodon) . Évalué à 3.

        Tu veux que je le vire ? Si qqu'un les compile tous ça va lui faciliter la tâche.

        En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

  • # OCaml (avec cohttp)

    Posté par (site web personnel) . Évalué à 2.

    Et voici une version en OCaml, j’ai utilisé cohttp qui est une librairie bas-niveau pour construire des requêtes client/serveur.

    open Cohttp
    open Cohttp_lwt_unix
    let server imgCount =
     let callback _conn req body =
     ignore body;
     let headers =
     Header.add (Request.headers req) "Location"
     (String.concat ""
     [
     "https://avatar.spacefox.fr/Renard-";
     string_of_int @@ (1 + Random.int imgCount);
     ".png";
     ])
     in
     Server.respond ~headers ~status:`Temporary_redirect ~body:`Empty ()
     in
     Server.create ~mode:(`TCP (`Port 8000)) (Server.make ~callback ())
    let () = Lwt_main.run (server 5)

    La sortie ps :

    $ ps aux | grep tempo
    sebasti+ 23319 0.8 0.4 119996 16464 pts/2 Sl+ 13:49 0:03 _build/default/tempo.exe

    et le résultat du bench :

    $ ab -n 100000 -c 10 http://localhost:8000/
    This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    Benchmarking localhost (be patient)
    ...
    Finished 100000 requests
    Server Software: 
    Server Hostname: localhost
    Server Port: 8000
    Document Path: /
    Document Length: 0 bytes
    Concurrency Level: 10
    Time taken for tests: 3.350 seconds
    Complete requests: 100000
    Failed requests: 0
    Non-2xx responses: 100000
    Total transferred: 18600000 bytes
    HTML transferred: 0 bytes
    Requests per second: 29851.08 [#/sec] (mean)
    Time per request: 0.335 [ms] (mean)
    Time per request: 0.033 [ms] (mean, across all concurrent requests)
    Transfer rate: 5422.17 [Kbytes/sec] received
    Connection Times (ms)
     min mean[+/-sd] median max
    Connect: 0 0 0.0 0 1
    Processing: 0 0 0.5 0 51
    Waiting: 0 0 0.5 0 51
    Total: 0 0 0.5 0 51
    Percentage of the requests served within a certain time (ms)
     50% 0
     66% 0
     75% 0
     80% 0
     90% 1
     95% 1
     98% 1
     99% 2
     100% 51 (longest request)
    
  • # Julia

    Posté par (site web personnel, Mastodon) . Évalué à 2.

    #!/usr/local/bin/julia
    using HTTP
    using HTTP.Messages
    using Random
    using Logging
    using Sockets
    LogLevel(Logging.Warn)
    if length(ARGS)<2
     println("Missing arguments : Usage 'random_url_server.jl port nbImages'")
     exit();
    end
    port=parse(Int64,ARGS[1])
    nbImg=parse(Int64,ARGS[2])+1
    HTTP.listen(Sockets.localhost,port) do http::HTTP.Stream
     if uri(http.message)=="/"
     HTTP.setstatus(http, 307) # Temporary Redirect
     HTTP.setheader(http, "Location" => string("https://avatar.spacefox.fr/Renard-",rand(1:nbImg),".png"))
     HTTP.startwrite(http)
     end
    end

    C'est un langage réputé performant. Par contre il se "compile" au premier appel alors j'ai fais un premier appel avant le test.

    ab -n 100000 -c 10 http://localhost:3000/
    This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    Benchmarking localhost (be patient)
    Completed 10000 requests
    Completed 20000 requests
    Completed 30000 requests
    Completed 40000 requests
    Completed 50000 requests
    Completed 60000 requests
    Completed 70000 requests
    Completed 80000 requests
    Completed 90000 requests
    Completed 100000 requests
    Finished 100000 requests
    Server Software: 
    Server Hostname: localhost
    Server Port: 3000
    Document Path: /
    Document Length: 0 bytes
    Concurrency Level: 10
    Time taken for tests: 16.392 seconds
    Complete requests: 100000
    Failed requests: 0
    Non-2xx responses: 100000
    Total transferred: 8657325 bytes
    HTML transferred: 0 bytes
    Requests per second: 6100.53 [#/sec] (mean)
    Time per request: 1.639 [ms] (mean)
    Time per request: 0.164 [ms] (mean, across all concurrent requests)
    Transfer rate: 515.76 [Kbytes/sec] received
    Connection Times (ms)
     min mean[+/-sd] median max
    Connect: 0 0 0.1 0 4
    Processing: 0 2 1.1 1 76
    Waiting: 0 1 1.1 1 76
    Total: 1 2 1.1 1 76
    Percentage of the requests served within a certain time (ms)
     50% 1
     66% 2
     75% 2
     80% 2
     90% 2
     95% 2
     98% 3
     99% 4
     100% 76 (longest request)
    

    Sous licence Creative common. Lisez, copiez, modifiez faites en ce que vous voulez.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.