Journal KissCache, kiss qui se cache

Posté par  . Licence CC By‑SA.
Étiquettes :
12
31
mar.
2020

Sommaire

présentation

Il y a peu, une dépêche présentait "kisscache", un serveur de cache HTTP(S). Particularité, il ne lance qu'un seul téléchargement concurrent pour une même ressource.

efficacité réseau

Cette fonctionnalité permet, si plusieurs clients se présentent pour télécharger la même ressource, avant sa mise en cache, de ne pas envoyer plusieurs fois la même demande au serveur et donc d'économiser de la bande passante côté client et serveur. Au final, tous les clients seront servis plus vite.

cache HTTPS

De plus, l'outil propose de régler le problème de la mise en cache de ressources proposées en HTTPS. En effet, un serveur de cache agit comme un "man in the middle" et est donc difficilement compatible avec un transport sécurisé dont le but est, entre autres, de ne pas permettre la modification de la réponse du serveur sans que le client ne le détecte. Une solution courante - en particulier en entreprise pour inspecter le flux client-serveur, plus que pour la mise en cache - est d'usurper le certificat serveur, en le replaçant par un certificat issu d'une autorité de confiance interne déployée sur les machines clientes. Encore faut-il effectuer ce déploiement.

KISS ?

L'outil est présenté comme suivant le principe KISS. Il est vrai que le nombre de fonctionnalités est limité et que la quantité de code est faible, environ mille lignes de python. Cependant, les dépendances de fonctionnement sont conséquentes : django pour l'API, postgres comme base de données, celery pour les traitements asynchrones, redis comme bus de données, docker pour l'infrastructure. La ré-écriture d'URL rend, quant à elle, son usage peu simple.

analyse

cache HTTPS

La solution proposée implique la réécriture des URL de toutes les ressources demandées. Elle est donc beaucoup plus intrusive et non standard que l'utilisation d'un proxy classique. Un proxy demande des variables d'environnement, des paramètres de ligne de commande ou une modification de fichier de configuration. Un proxy transparent, lui, demande une configuration réseau locale ou sur le routeur. Concrètement, aucun gestionnaire de paquets logiciels (apt, pip, etc.) ne permet la réécriture d'URL. Et il faut donc disposer d'outils spécialement écrits pour être compatibles avec kisscache.

KISS

Clairement la gestion d'un cache HTTP(S) n'est pas simple. Le protocole HTTP est compliqué, avec énormément de cas de figure, en particulier sur la gestion du cycle de vie des ressources, influencé par de nombreux entêtes. L'outil kisscache ne le gère pas vraiment.
Quelles autres pistes peut-on suivre ?

nginx

Nginx n'est pas un logiciel simple. Mais sa mise en œuvre l'est. Moyennant un fichier de configuration relativement court - moins de vingt lignes - il gère correctement un cache HTTP(S) avec les mêmes fonctionnalités que celles attendues pour kisscache :

    server {
    listen 3128;

    expires               max;
    proxy_cache           on;
    proxy_cache_path      /var/cache/proxy-nginx inactive=1w; # one week
    proxy_cache_key       "$scheme://$host$request_uri";
    proxy_pass            $scheme://$host$request_uri;
    proxy_set_header      Host $http_host;
    proxy_ignore_headers  "Set-Cookie";

    proxy_ignore_client_abort on;
    proxy_cache_lock on;
    proxy_cache_lock_age 10m;
    proxy_cache_valid 200 301 1w; # one week
    proxy_cache_valid any 1m;
    proxy_cache_use_stale error;
}

La directive "proxy_cache_lock" permet de n'avoir qu'un client à la fois en train de télécharger une ressource. La directive "proxy_ignore_client_abort" permet de finir le téléchargement même si le client interrompt la connexion. La directive "inactive=1w" permet de faire le ménage des ressources non consultées depuis un certain temps.

Async python

Si il s'agit d'écrire un serveur en python qui met en cache les URL qui lui sont soumises, une démarche simple est de brancher deux bibliothèques logicielles entre elles : un serveur HTTP et un client HTTP. Et pour plus d'efficacité, d'aller voir du côté des capacités AsyncIO de Python. En collant l'exemple serveur et l'exemple client de la page d'accueil de aiohttp, on arrive rapidement à un prototype fonctionnel. On ajoute entre les deux une gestion des fichiers (cache) et un mécanisme pour empêcher les téléchargements multiples et on obtient les quelques quarante lignes de code suivantes :

import asyncio
from aiohttp import web, ClientSession
from hashlib import md5
import os
from aiofile import AIOFile

D = {}

async def dl_and_cache(url, dirname, basename):
    async with ClientSession() as session:
        async with session.get(url) as response:
            content = await response.read()
    os.makedirs(dirname, exist_ok=True)
    path = dirname+"/"+basename
    async with AIOFile(path, 'wb') as afp:
        await afp.write(content)


async def handle(request):
    url = request.match_info.get('url')
    print(url)
    h = md5(bytes(url,"ascii")).hexdigest()
    dirname = ".cache/"+h[0:2]+"/"+h[2:4]
    basename = h[4:]
    path = dirname+"/"+basename
    if not os.path.exists(path):
        # if downloader is marked, just wait for it
        if url in D:
            while url in D:
                await asyncio.sleep(1)
        # else, mark as downloading and start download
        else:
            D[url] = None
            # use shield to prevent cancellation from client disconnection
            await asyncio.shield(dl_and_cache(url, dirname, basename))
            del D[url]
    return web.FileResponse(path)


if __name__ == '__main__':
    app = web.Application()
    app.add_routes([web.get('/{url}', handle)]) # url must be urlencoded
    web.run_app(app)

Les dépendances sont résolues avec un pip install aiohttp[speedups] aiofile.
À tester avec un wget http://0.0.0.0:8080/$(urlencode 'http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz') par exemple.

Pour atteindre le niveau fonctionnel de l'outil proposé, il faudrait ajouter la gestion du TTL et la suppression des ressources non utilisées depuis un certain temps. Cela devrait pouvoir se faire en restant sous les cent lignes de code.
Ensuite, PyInstaller package le tout dans un binaire auto-porteur - i.e: embarquant l'interpréteur python et les dépendances - de moins de 11Mo.

Merci

Je remercie l'auteur pour sa dépêche. Elle m'a permis de réfléchir à la problématique de mise ne cache de ressource proposées en https et qui me pose moi aussi des problèmes pour des tâches de construction logicielle, de me pencher sur aiohttp qui est une très bonne surprise, de me repencher sur nginx qui est définitivement un outil très puissant, de voir une bonne intégration de django+celery qui est un bon pattern pour la construction d'API d'automatisation d'infrastructure.

  • # miniproxy

    Posté par  . Évalué à 2.

    Bonjour,

    si on le couple à miniproxy, ca fait le taff non ?
    https://github.com/joshdick/miniProxy

    un exemple qui fonctionne : https://www.freenixsecurity.net/phpproxy.php
    regarder le source de la page renvoyée.

  • # Efficacité

    Posté par  . Évalué à 3.

    Et pour plus d'efficacité, d'aller voir du côté des capacités AsyncIO de Python.

    Ça dépend. Utiliser des I/O asynchrones est plus efficace si tu dois multiplexer beaucoup de requêtes, mais s'il s'agit de ressources volumineuses ce sera moins efficace que du synchrone si je ne m'abuse.

    Donc il faut voir pourquoi tu as besoin d'un proxy :

    • un grand nombre de requêtes
    • des ressources volumineuses

    Des 2 il faut choisir ce qui est prioritaire. Après tu peux même faire 2 implémentations et configurer les clients pour choisir le bon proxy.

  • # Retour d'expérience

    Posté par  (site Web personnel) . Évalué à 3.

    Bonjour et merci pour ces retours sur la problématique initiale.

    Quelques remarques en tant qu'auteur de KissCache :

    cache HTTPS

    La solution proposée implique la réécriture des URL de toutes les ressources demandées. Elle est donc beaucoup plus intrusive et non standard que l'utilisation d'un proxy classique.
    […]
    Et il faut donc disposer d'outils spécialement écrits pour être compatibles avec kisscache.

    Effectivement c'est un inconvénient de devoir adapter les outils pour qu'ils utilisent KissCache. C'est cependant très rapide à faire puisqu'il suffit de préfixer les urls. Dans l'outil de CI que nous utilisons (LAVA), le support de KissCache représente moins de 10 lignes de code.
    L'alternative étant de faire du MITM avec la création d'un certificat racine et de le déployer sur tous les clients ce qui me semble très lourd et limite au niveau sécurité.

    KISS

    Clairement la gestion d'un cache HTTP(S) n'est pas simple. Le protocole HTTP est compliqué

    Le nom de KissCache a pour but de bien indiquer que le fonctionnement de KissCache est simple, voir stupide et qu'il ne prends pas en compte toutes les subtilités du protocole.

    nginx

    La directive "proxy_cache_lock" permet de n'avoir qu'un client à la fois en train de télécharger une ressource.

    D'après la documentation les autres clients vont devoir attendre que la ressource soit entièrement téléchargée avant de pouvoir y accéder.
    Si nginx met plusieurs minutes à télécharger la ressource, alors les autres clients vont attendre plusieurs minutes avant de recevoir ne serait-ce que les premier bit de donnée.

    KissCache permet à tous les clients, en simultané, de recevoir le fichier en même temps au fur et à mesure de son téléchargement par KissCache. Il n'y a donc qu'une très faible latence (le temps pour le worker celery de démarrer la tache).

    De plus nginx ne permet toujours pas de mettre en cache les ressource disponible via https.

    Async python

    L'implémentation proposée souffre d'une même problème que nginx : les clients vont attendre le téléchargement complet de la ressource avant de recevoir une réponse. Le cache induit donc une latence importante lors du premier téléchargement.

    Sinon j'aime bien l'idée de se baser sur async. J'y avais pensé mais je voulais aussi avoir une interface web pour voir l'état du cache, ainsi qu'une interface d'admin permettant de supprimer les urls mises en cache en cas de problème.
    Travaillant tous les jours avec Django depuis de nombreuses année, j'ai donc choisi Django.

Suivre le flux des commentaires

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