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 :
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 :
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.