structure & meta
This commit is contained in:
parent
da6d838193
commit
b25ce65735
284 changed files with 461 additions and 1000 deletions
172
posts/2017/2017-07-24-golang-a-la-rescousse.md
Executable file
172
posts/2017/2017-07-24-golang-a-la-rescousse.md
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
<!-- title: Performances, Golang à la rescousse -->
|
||||
<!-- categories: Développement Blog -->
|
||||
<!-- tag: planet -->
|
||||
|
||||
Dans l'[article précédent](/2017/performance-python-web) j'ai optimisé le
|
||||
[système de gestion des commentaires
|
||||
Stacosys](https://github.com/kianby/stacosys)<!-- more --> en :
|
||||
|
||||
- remplaçant le serveur HTTP de [Flask](http://flask.pocoo.org) par [Sanic](http://sanic.readthedocs.io), 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](https://fr.wikipedia.org/wiki/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](https://golang.org) 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](http://spf13.com/post/is-go-object-oriented) 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 :
|
||||
|
||||
``` golang
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConfigType represents config info
|
||||
type ConfigType struct {
|
||||
HostPort string
|
||||
Stacosys string
|
||||
CorsOrigin string
|
||||
}
|
||||
|
||||
var config ConfigType
|
||||
var countCache = cache.New(5*time.Minute, 10*time.Minute)
|
||||
|
||||
func die(format string, v ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf(format, v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func commentsCount(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// only GET method is supported
|
||||
if r.Method != "GET" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// set header
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Header().Add("Access-Control-Allow-Origin", config.CorsOrigin)
|
||||
|
||||
// get cached value
|
||||
cachedBody, found := countCache.Get(r.URL.String())
|
||||
if found {
|
||||
//fmt.Printf("return cached value")
|
||||
w.Write([]byte(cachedBody.(string)))
|
||||
return
|
||||
}
|
||||
|
||||
// relay request to stacosys
|
||||
response, err := http.Get(config.Stacosys + r.URL.String())
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// cache body and return response
|
||||
countCache.Set(r.URL.String(), string(body), cache.DefaultExpiration)
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
func main() {
|
||||
pathname := flag.String("config", "", "config pathname")
|
||||
flag.Parse()
|
||||
if *pathname == "" {
|
||||
die("%s --config <pathname>", os.Args[0])
|
||||
}
|
||||
// read config File
|
||||
file, e := ioutil.ReadFile(*pathname)
|
||||
if e != nil {
|
||||
die("File error: %v", e)
|
||||
}
|
||||
json.Unmarshal(file, &config)
|
||||
fmt.Printf("config: %s\n", string(file))
|
||||
|
||||
http.HandleFunc("/comments/count", commentsCount)
|
||||
http.ListenAndServe(config.HostPort, nil)
|
||||
}
|
||||
```
|
||||
|
||||
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](https://discordapp.com/), 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*.
|
||||
Loading…
Add table
Add a link
Reference in a new issue