Performances, Golang à la rescousse

Dans l’article précédent j’ai optimisé le système de gestion des commentaires Stacosys en :

  • remplaçant le serveur HTTP de Flask par Sanic, un serveur HTTP Python tirant parti des capacités asynchrones de Python 3.5 et multi-processus (plusieurs workers)
  • ajoutant un cache mémoire à la partie de l’API de Stacosys qui récupère le compteur de commentaires d’un article

J’ai terminé sur une performance bien améliorée :

  • plus de 11000 requêtes traitées en 1 minute
  • un temps de requête moyen de 1,3 seconde
  • une répartition du temps de traitement entre 81 ms et 18 secondes (assez élevé)
  • 171 des requêtes (soit 1,5 %) avec un temps de traitement supérieur à 10 secondes

L’architecture avec Sanic ressemble à ceci :

Architecture Stacosys cache

Pour être complet le serveur HTTPS NginX en frontal de Stacosys est configuré avec 4 workers et il déverse les requêtes sur Sanic configuré avec 2 workers, qui lui seul utilise 30% de la CPU lors du test. L’impact sur la CPU est important et doit être mis en balance avec le gain en performance car d’autres services tournent sur le même serveur.

NginX est un serveur Web très complet et il y a des configurations avancées de mise en cache qui, d’après sa documentation, pourraient s’appliquer à mon scénario : un serveur HTTP en mode Proxy qui renvoie au format JSON les résultats d’une API. Si c’est le cas, cela rendrait caduque la nécessité d’ajouter un cache au niveau du serveur HTTP de Stacosys. J’ai fait quelques essais et je ne suis pas arrivé à un résultat fonctionnel. Si vous avez des retours d’expérience, j’aurais voulu mesurer les performances de cette solution. Logiquement, elle devrait l’emporter sur les autres.

Je cherchais depuis un petit moment une occasion excuse pour écrire un peu de Golang. Un test HTTP (hors contexte) de Golang m’a convaincu que je pourrais m’en servir. Le langage Golang a la particularité d’être compilé, typé, multi-plateforme et il fournit en standard des fonctionalités de haut niveau comme HTTP (client et serveur), de la crypto et de la compression, le support du JSON. Le débat reste ouvert sur le fait que Golang soit un langage orienté objet. En tout cas, il propose un paradigme de programmation simple et une richesse de librairies qui le rendent très intéressant pour du développement généraliste où la performance compte.

J’ai donc restauré Stacosys en situation initiale (retour au serveur HTTP de Flask) et j’ai ajouté un serveur HTTP avec cache en Golang qui sert de proxy à NginX pour récupérer le compteur de commentaires. Les autres appels à l’API de Stacosys sont envoyés directement à Stacosys.

L’architecture devient ainsi :

Architecture Golang HTTP/Cache

Dans cette configuration, j’ai relancé mon fameux test étalon. On éclate tout avec + de 14000 requêtes traitées, un taux d’erreur équivalent mais surtout un temps de réponse moyen divisé par 4 et une charge CPU d’à peine 7%. Le serveur HTTP est mono-processus mais il utilise à fond les capacitéss des goroutines de Golang pour gérer la concurrence de traitement.

Serveur Workers Temps de réponse Requêtes Erreurs
Flask HTTPS 1 104 > 4194 > 32000 4043 326
Sanic HTTPS + cache 4 81 > 1152 > 12408 13558 210
Sanic HTTPS + cache 1 81 > 1367 > 18869 11589 171
Golang HTTPS ? 80 > 341 > 6745 14663 175

Pour les fans de code, voici celui du serveur HTTP avec cache :

 1     package main
 2 
 3     import (
 4       "encoding/json"
 5       "flag"
 6       "fmt"
 7       "github.com/patrickmn/go-cache"
 8       "io/ioutil"
 9       "net/http"
10       "os"
11       "time"
12     )
13 
14     // ConfigType represents config info
15     type ConfigType struct {
16       HostPort   string
17       Stacosys   string
18       CorsOrigin string
19     }
20 
21     var config ConfigType
22     var countCache = cache.New(5*time.Minute, 10*time.Minute)
23 
24     func die(format string, v ...interface{}) {
25       fmt.Fprintln(os.Stderr, fmt.Sprintf(format, v...))
26       os.Exit(1)
27     }
28 
29     func commentsCount(w http.ResponseWriter, r *http.Request) {
30 
31       // only GET method is supported
32       if r.Method != "GET" {
33         http.NotFound(w, r)
34         return
35       }
36 
37       // set header
38       w.Header().Add("Content-Type", "application/json")
39       w.Header().Add("Access-Control-Allow-Origin", config.CorsOrigin)
40 
41       // get cached value
42       cachedBody, found := countCache.Get(r.URL.String())
43       if found {
44         //fmt.Printf("return cached value")
45         w.Write([]byte(cachedBody.(string)))
46         return
47       }
48 
49       // relay request to stacosys
50       response, err := http.Get(config.Stacosys + r.URL.String())
51       if err != nil {
52         http.NotFound(w, r)
53         return
54       }
55       defer response.Body.Close()
56       body, err := ioutil.ReadAll(response.Body)
57       if err != nil {
58         http.NotFound(w, r)
59         return
60       }
61 
62       // cache body and return response
63       countCache.Set(r.URL.String(), string(body), cache.DefaultExpiration)
64       w.Write(body)
65     }
66 
67     func main() {
68       pathname := flag.String("config", "", "config pathname")
69       flag.Parse()
70       if *pathname == "" {
71         die("%s --config <pathname>", os.Args[0])
72       }
73       // read config File
74       file, e := ioutil.ReadFile(*pathname)
75       if e != nil {
76         die("File error: %v", e)
77       }
78       json.Unmarshal(file, &config)
79       fmt.Printf("config: %s\n", string(file))
80 
81       http.HandleFunc("/comments/count", commentsCount)
82       http.ListenAndServe(config.HostPort, nil)
83     }

La démonstration ne vise pas à conclure qu’il faut tout réécrire en Golang car Python est trop lent !

Hier, je lisais un article à propos de Discord, une application concurrente de Teamspeak avec de la VoIP, des gros besoins de concurrence de traitement (5 millions de messages échangés en permanence), du Web et de l’application mobile. Leur solution mixe 4 langages différents : Python, NodeJS, Golang et Elixir (Erlang) ; chacun a son rôle et son champ d’application dédié. Plus on acquiert une culture large de l’informatique et plus on sera capable de choisir le bon langage / paradigme de programmation / framework en fonction de la tâche à accomplir, ce qui rejoint ce dicton anglo-saxon que j’aime bien même s’il est un peu galvaudé : if all you have is a hammer, everything looks like a nail.

22decembre - 2017-07-24 17:48:17

Merci. T’as essayé d’adapter ca sur httpd de OpenBSD ?

Yax - 2017-07-24 19:39:06

Pas avec cette optimisation mais j’avais monté une configuration complète de mes services avec httpd et relayd quand j’ai installé le serveur OpenBSD. Je suis repassé à NginX depuis pour avoir HTTP/2.

J’attends de voir les prochaines évolutions de httpd…

22decembre - 2017-07-24 22:10:22

mince…

Votre commentaire
Le site Web est optionel
Le message peut être rédigé au format Markdown