Journal Le taptempo du web

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
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  . É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)
    
    

Suivre le flux des commentaires

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