Performances, Golang à la rescousse

DéveloppementBlog24 juil. 2017

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 :

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, 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