
⏰ 🔥 Un proxy TCP pour simuler des conditions réseau et système pour les tests de chaos et de résilience.
Toxiproxy est un framework pour simuler des conditions réseau. Il est conçu spécifiquement pour les environnements de test, d'intégration continue (CI) et de développement, prenant en charge l'altération déterministe des connexions, mais aussi le chaos aléatoire et la personnalisation. Toxiproxy est l'outil dont vous avez besoin pour prouver par des tests que votre application n'a pas de points de défaillance uniques. Nous l'utilisons avec succès dans tous les environnements de développement et de test chez Shopify depuis octobre 2014. Voir notre [article de blog][blog] sur la résilience pour plus d'informations.
L'utilisation de Toxiproxy se compose de deux parties : un proxy TCP écrit en Go (ce que contient ce dépôt) et un client qui communique avec le proxy via HTTP. Vous configurez votre application pour que toutes les connexions de test passent par Toxiproxy et pouvez ensuite manipuler leur état via HTTP. Voir Utilisation ci-dessous pour savoir comment configurer votre projet.
Par exemple, pour ajouter 1000ms de latence à la réponse de MySQL depuis le client Ruby :```ruby Toxiproxy[:mysql_master].downstream(:latency, latency: 1000).apply do Shop.first # this takes at least 1s end
Pour faire tomber toutes les instances Redis:```ruby
Toxiproxy[/redis/].down do
Shop.first # this will throw an exception
end
While the examples in this README are currently in Ruby, there's nothing stopping you from creating a client in any other language (see Clients).
Les solutions existantes que nous avons trouvées ne fournissaient pas le type d'API dynamique dont nous avions besoin pour
les tests d'intégration et unitaires. Les outils Linux comme nc et autres ne sont pas
multi-plateformes et nécessitent les droits root, ce qui les rend problématiques dans les tests,
le développement et les environnements d'intégration continue.
Prenons un exemple avec une application Rails. Notez que Toxiproxy n'est en aucun cas lié à Ruby, ce n'était que notre premier cas d'usage. Vous pouvez voir l'exemple complet sur sirupsen/toxiproxy-rails-example. Pour commencer immédiatement, rendez-vous à la section Utilisation.
Pour notre blog populaire, pour une raison ou une autre, nous stockons les tags de nos articles dans
Redis et les articles eux-mêmes dans MySQL. Nous pourrions avoir une classe Post qui
inclut des méthodes pour manipuler les tags dans un Redis set :```ruby
class Post < ActiveRecord::Base
def tags TagRedis.smembers(tag_key) end
def add_tag(tag) TagRedis.sadd(tag_key, tag) end
def remove_tag(tag) TagRedis.srem(tag_key, tag) end
def tag_key "post:tags:#{self.id}" end end
Nous avons décidé qu'il est acceptable de lever une erreur lors de l'écriture dans le magasin de données de tags
(ajout/suppression). Cependant, si le magasin de données de tags est indisponible, nous devrions
pouvoir voir le post sans tags. Nous pourrions simplement intercepter l'erreur
`Redis::CannotConnectError` autour de l'appel Redis `SMEMBERS` dans la méthode
`tags`. Utilisons Toxiproxy pour tester cela.
Comme nous avons déjà installé Toxiproxy et qu'il tourne sur notre machine, nous pouvons
passer à l'étape 2. C'est là que nous devons nous assurer que Toxiproxy a un mappage pour
les tags Redis. Dans `config/boot.rb` (avant toute connexion) nous ajoutons:```ruby
require 'toxiproxy'
Toxiproxy.populate([
{
name: "toxiproxy_test_redis_tags",
listen: "127.0.0.1:22222",
upstream: "127.0.0.1:6379"
}
])
Ensuite, dans config/environments/test.rb, nous définissons TagRedis comme un client Redis
qui se connecte à Redis via Toxiproxy en ajoutant cette ligne :```ruby
TagRedis = Redis.new(port: 22222)
Tous les appels dans l'environnement de test passent désormais par Toxiproxy. Cela signifie que nous pouvons
ajouter un test unitaire où nous simulons une panne :```ruby
test "should return empty array when tag redis is down when listing tags" do
@post.add_tag "mammals"
# Take down all Redises in Toxiproxy
Toxiproxy[/redis/].down do
assert_equal [], @post.tags
end
end
Le test échoue avec Redis::CannotConnectError. Parfait ! Toxiproxy a arrêté
Redis avec succès pendant la durée de la fermeture. Corrigeons la méthode tags
pour qu'elle soit résiliente :```ruby
def tags
TagRedis.smembers(tag_key)
rescue Redis::CannotConnectError
[]
end
Les tests passent ! Nous avons désormais un test unitaire qui prouve que la récupération des tags lorsque Redis est indisponible renvoie un tableau vide, au lieu de lever une exception. Pour une couverture complète, vous devriez également écrire un test d'intégration qui couvre la récupération de la page entière de l'article de blog lorsque Redis est indisponible.
L'application d'exemple complète se trouve à
[sirupsen/toxiproxy-rails-example](https://github.com/sirupsen/toxiproxy-rails-example).
## Utilisation
La configuration d'un projet pour utiliser Toxiproxy comprend trois étapes :
1. Installer Toxiproxy
2. Peupler Toxiproxy
3. Utiliser Toxiproxy
### 1. Installer Toxiproxy
**Linux**
Consultez [`Releases`](https://github.com/Shopify/toxiproxy/releases) pour les derniers binaires et paquets système pour votre architecture.
**Ubuntu**```bash
$ wget -O toxiproxy-2.1.4.deb https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy_2.1.4_amd64.deb
$ sudo dpkg -i toxiproxy-2.1.4.deb
$ sudo service toxiproxy start
OS X
Avec Homebrew:```bash $ brew tap shopify/shopify $ brew install toxiproxy
Ou avec [MacPorts](https://www.macports.org/):```bash
$ port install toxiproxy
Windows
Toxiproxy pour Windows est disponible en téléchargement sur https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-windows-amd64.exe
Docker
Toxiproxy est disponible sur Github container registry.
Les anciennes versions <= 2.1.4 sont disponibles sur sur Docker Hub.```bash
$ docker pull ghcr.io/shopify/toxiproxy
$ docker run --rm -it ghcr.io/shopify/toxiproxy
Si vous utilisez Toxiproxy depuis l'hôte plutôt que depuis d'autres conteneurs, activez le mode réseau de l'hôte avec `--net=host`.```shell
$ docker run --rm --entrypoint="/toxiproxy-cli" -it ghcr.io/shopify/toxiproxy list
Si vous avez Go installé, vous pouvez compiler Toxiproxy à partir des sources à l'aide du fichier make :```bash $ make build $ ./toxiproxy-server
#### Mise à niveau depuis Toxiproxy 1.x
Dans Toxiproxy 2.0, plusieurs modifications ont été apportées à l'API qui la rendent incompatible avec la version 1.x. Pour utiliser la version 2.x du serveur Toxiproxy, vous devrez vous assurer que votre bibliothèque cliente prend en charge la même version. Vous pouvez vérifier la version de Toxiproxy que vous exécutez en consultant le point de terminaison `/version`.
Consultez la documentation de votre bibliothèque cliente pour connaître les modifications spécifiques à la bibliothèque. Les modifications détaillées du serveur Toxiproxy se trouvent dans [CHANGELOG.md](https://github.com/shopify/toxiproxy/blob/HEAD/CHANGELOG.md).
### 2. Remplir Toxiproxy
Au démarrage de votre application, vous devez vous assurer que Toxiproxy sait quels points de terminaison relayer et vers où. Les principaux paramètres sont : le nom, l'adresse sur laquelle Toxiproxy doit **écouter** et l'adresse du serveur en amont.
Certaines bibliothèques clientes disposent d'helpers pour cette tâche, ce qui revient essentiellement à s'assurer que chaque proxy d'une liste est créé. Exemple tiré du client Ruby :```ruby
# Make sure `shopify_test_redis_master` and `shopify_test_mysql_master` are
# present in Toxiproxy
Toxiproxy.populate([
{
name: "shopify_test_redis_master",
listen: "127.0.0.1:22220",
upstream: "127.0.0.1:6379"
},
{
name: "shopify_test_mysql_master",
listen: "127.0.0.1:24220",
upstream: "127.0.0.1:3306"
}
])
Ce code doit s'exécuter le plus tôt possible au démarrage, avant que tout code n'établisse une connexion via Toxiproxy. Veuillez consulter la documentation de votre bibliothèque client pour les helpers de population.
Vous pouvez également utiliser la CLI pour créer des proxys, par exemple :```bash toxiproxy-cli create -l localhost:26379 -u localhost:6379 shopify_test_redis_master
Nous recommandons une dénomination telle que celle-ci : `<app>_<env>_<data store>_<shard>`.
Cela permet d'éviter tout conflit entre les applications utilisant le même
Toxiproxy.
Pour une grande application, nous recommandons de stocker les configurations Toxiproxy dans un
fichier de configuration séparé. Nous utilisons `config/toxiproxy.json`. Ce fichier peut être
transmis au serveur à l'aide de l'option `-config`, ou chargé par l'application
pour être utilisé avec la fonction `populate`.
Un exemple de `config/toxiproxy.json` :```json
[
{
"name": "web_dev_frontend_1",
"listen": "[::]:https://raw.githubusercontent.com/shopify/toxiproxy/HEAD/18080%22,
"upstream": "webapp.domain:8080",
"enabled": true
},
{
"name": "web_dev_mysql_1",
"listen": "[::]:13306",
"upstream": "database.domain:3306",
"enabled": true
}
]
Utilisez des ports en dehors de la plage de ports éphémères pour éviter les conflits de ports aléatoires.
Par défaut, elle va de 32,768 à 61,000 sur Linux, voir
/proc/sys/net/ipv4/ip_local_port_range.
Pour utiliser Toxiproxy, vous devez maintenant configurer votre application pour qu'elle se connecte via Toxiproxy. En poursuivant avec notre exemple de l'étape deux, nous pouvons configurer notre client Redis pour se connecter via Toxiproxy :```ruby
redis = Redis.new(port: 6380)
redis = Redis.new(port: 22220)
Maintenant, vous pouvez le perturber via l'API Toxiproxy. En Ruby :```ruby
redis = Redis.new(port: 22220)
Toxiproxy[:shopify_test_redis_master].downstream(:latency, latency: 1000).apply do
redis.get("test") # will take 1s
end
Ou via la CLI :```bash toxiproxy-cli toxic add -t latency -a latency=1000 shopify_test_redis_master
Veuillez consulter votre bibliothèque cliente respective pour l'utilisation.
### 4. Journalisation
Les niveaux de journalisation suivants sont disponibles : panic, fatal, error, warn ou warning, info, debug et trace.
Le niveau peut être modifié via la variable d'environnement `LOG_LEVEL`.
### Toxics
Les Toxics manipulent le canal entre le client et l'amont (upstream). Ils peuvent être ajoutés
et supprimés des proxys via l'[API HTTP](#http-api). Chaque toxic possède ses propres paramètres
pour modifier la façon dont il affecte les liens du proxy.
Pour la documentation sur l'implémentation de toxics personnalisés, voir [CREATING_TOXICS.md](https://github.com/shopify/toxiproxy/blob/HEAD/CREATING_TOXICS.md)
#### latency
Ajoute un délai à toutes les données transitant par le proxy. Le délai est égal à `latency` +/- `jitter`.
Attributs :
- `latency` : temps en millisecondes
- `jitter` : temps en millisecondes
#### down
Mettre un service hors ligne n'est pas techniquement un toxic dans l'implémentation de
Toxiproxy. Cela se fait en envoyant une requête `POST` vers `/proxies/{proxy}` et en définissant
le champ `enabled` sur `false`.
#### bandwidth
Limite une connexion à un nombre maximal de kilo-octets par seconde.
Attributs :
- `rate` : débit en Ko/s
#### slow_close
Retarde la fermeture de la socket TCP jusqu'à ce que `delay` se soit écoulé.
Attributs :
- `delay` : temps en millisecondes
#### timeout
Empêche toutes les données de passer et ferme la connexion après `timeout`. Si
`timeout` est 0, la connexion ne se ferme pas et les données sont supprimées jusqu'à ce que le
toxic soit retiré.
Attributs :
- `timeout` : temps en millisecondes
#### reset_peer
Simule une réinitialisation TCP (Connection reset by peer) sur les connexions en fermant l'entrée du stub
immédiatement ou après un `timeout`.
Attributs :
- `timeout` : temps en millisecondes
#### slicer
Découpe les données TCP en petits morceaux, en ajoutant éventuellement un délai entre chaque
« paquet » découpé.
Attributs :
- `average_size` : taille en octets d'un paquet moyen
- `size_variation` : variation en octets d'un paquet moyen (doit être inférieure à average_size)
- `delay` : temps en microsecondes pour retarder chaque paquet
#### limit_data
Ferme la connexion lorsque les données transmises dépassent la limite.
- `bytes` : nombre d'octets à transmettre avant la fermeture de la connexion
#### packet_loss
Supprime aléatoirement des morceaux de données transitant par le proxy, simulant
des conditions réseau instables de type Wi-Fi, mobile ou satellite.
Attributs :
- `loss_rate` : probabilité [0.0-1.0] qu'un morceau soit supprimé (par défaut 0.0)
- `correlation` : probabilité de suppression supplémentaire lorsque le morceau précédent a été supprimé, modélisant les pertes en rafales (par défaut 0.0)
### HTTP API
Toutes les communications entre le client et le démon Toxiproxy passent par l'
interface HTTP, décrite ici.
Toxiproxy écoute le trafic HTTP sur le port **8474**.
#### Champs d'un proxy :
- `name` : nom du proxy (chaîne de caractères)
- `listen` : adresse d'écoute (chaîne de caractères)
- `upstream` : adresse amont du proxy (chaîne de caractères)
- `enabled` : true/false (vaut true par défaut à la création)
Pour changer le nom d'un proxy, il doit être supprimé puis recréé.
Modifier les champs `listen` ou `upstream` redémarre le proxy et interrompt toutes les connexions actives.
Si `listen` est spécifié avec un port 0, toxiproxy choisira un port éphémère. Le champ `listen`
de la réponse sera mis à jour avec le port réel.
Si vous passez `enabled` à `false`, le proxy sera arrêté. Vous pouvez le repasser
à `true` pour le réactiver.
#### Champs d'un toxic :
- `name` : nom du toxic (chaîne, par défaut `<type>_<stream>`)
- `type` : type du toxic (chaîne)
- `stream` : direction du lien à affecter (par défaut `downstream`)
- `toxicity` : probabilité que le toxic soit appliqué à un lien (par défaut 1.0, soit 100 %)
- `attributes` : une carte (map) d'attributs spécifiques au toxic
Voir [Toxics](#toxics) pour les attributs spécifiques aux toxics.
La direction `stream` doit être soit `upstream`, soit `downstream`. `upstream` applique
le toxic sur la connexion `client -> server`, tandis que `downstream` applique le toxic
sur la connexion `server -> client`. Cela peut être utilisé pour modifier séparément les requêtes et les réponses.
#### Endpoints
Tous les endpoints sont en JSON.
- **GET /proxies** - Liste les proxys existants et leurs toxics
- **POST /proxies** - Crée un nouveau proxy
- **POST /populate** - Crée ou remplace une liste de proxys
- **GET /proxies/{proxy}** - Affiche le proxy avec tous ses toxics actifs
- **POST /proxies/{proxy}** - Met à jour les champs d'un proxy
- **DELETE /proxies/{proxy}** - Supprime un proxy existant
- **GET /proxies/{proxy}/toxics** - Liste les toxics actifs
- **POST /proxies/{proxy}/toxics** - Crée un nouveau toxic
- **GET /proxies/{proxy}/toxics/{toxic}** - Récupère les champs d'un toxic actif
- **POST /proxies/{proxy}/toxics/{toxic}** - Met à jour un toxic actif
- **DELETE /proxies/{proxy}/toxics/{toxic}** - Supprime un toxic actif
- **POST /reset** - Active tous les proxys et supprime tous les toxics actifs
- **GET /version** - Renvoie le numéro de version du serveur
- **GET /metrics** - Renvoie des métriques compatibles Prometheus
#### Peuplement des proxys
Les proxys peuvent être ajoutés et configurés en masse à l'aide de l'endpoint `/populate`. Pour cela,
on transmet un tableau json de proxys à toxiproxy. Si un proxy portant le même nom existe déjà,
il sera comparé au nouveau proxy et remplacé si l'adresse `upstream` et `listen` ne correspondent pas.
Un appel `/populate` peut par exemple être inclus au démarrage de l'application pour garantir que tous les proxys requis
existent. Il est sûr d'effectuer cet appel plusieurs fois, car les proxys restent inchangés tant que leurs
champs sont cohérents avec les nouvelles données.
### Exemple CLI```bash
$ toxiproxy-cli create -l localhost:26379 -u localhost:6379 redis
Created new proxy redis
$ toxiproxy-cli list
Listen Upstream Name Enabled Toxics
======================================================================
127.0.0.1:26379 localhost:6379 redis true None
Hint: inspect toxics with `toxiproxy-client inspect <proxyName>`
Veuillez fournir le contenu Markdown à traduire.```bash $ redis-cli -p 26379 127.0.0.1:26379> SET omg pandas OK 127.0.0.1:26379> GET omg "pandas"
No input content was provided for translation. Please supply the chunk text to be translated.```bash
$ toxiproxy-cli toxic add -t latency -a latency=1000 redis
Added downstream latency toxic 'latency_downstream' on proxy 'redis'
Veuillez fournir le contenu Markdown à traduire.```bash $ redis-cli -p 26379 127.0.0.1:26379> GET omg "pandas" (1.00s) 127.0.0.1:26379> DEL omg (integer) 1 (1.00s)
The input content is missing. Please provide the Markdown content for chunk 47 so I can translate it into French.```bash
$ toxiproxy-cli toxic remove -n latency_downstream redis
Removed toxic 'latency_downstream' on proxy 'redis'
Aucun contenu Markdown fourni.```bash $ redis-cli -p 26379 127.0.0.1:26379> GET omg (nil)
Veuillez fournir le contenu Markdown à traduire.```bash
$ toxiproxy-cli delete redis
Deleted proxy redis
Veuillez fournir le contenu Markdown à traduire.```bash $ redis-cli -p 26379 Could not connect to Redis at 127.0.0.1:26379: Connection refused
### Metrics
Toxiproxy expose des métriques compatibles Prometheus via son API HTTP sur /metrics.
Voir [METRICS.md](https://github.com/shopify/toxiproxy/blob/HEAD/METRICS.md) pour les descriptions complètes.
### Questions fréquemment posées
**À quelle vitesse va Toxiproxy ?** La vitesse de Toxiproxy dépend en grande partie de votre matériel,
mais vous pouvez vous attendre à une latence de *< 100 µs* lorsqu'aucun toxique n'est activé. En exécutant
avec `GOMAXPROCS=4` sur un Macbook Pro, nous avons obtenu un débit de *~1000 Mo/s*, et jusqu'à
*2400 Mo/s* sur un ordinateur de bureau plus haut de gamme. En gros, vous pouvez vous attendre à ce que Toxiproxy déplace
les données au moins aussi vite que l'application que vous testez.
**Toxiproxy peut-il effectuer des tests aléatoires ?** De nombreux toxiques disponibles peuvent être configurés
pour inclure de l'aléatoire, comme `jitter` dans le toxique `latency`. Il existe également un
paramètre global `toxicity` qui spécifie le pourcentage de connexions qu'un toxique
affectera. C'est particulièrement utile pour des toxiques comme `timeout`, qui permettrait
à X% des connexions d'expirer.
**Je ne vois pas mes actions Toxiproxy reflétées pour MySQL**. MySQL préférera
le socket de domaine Unix local pour certains clients, quel que soit le port que vous lui transmettez
si l'hôte est défini sur `localhost`. Configurez votre serveur MySQL pour ne pas créer de
socket et utilisez `127.0.0.1` comme hôte. N'oubliez pas de supprimer l'ancien socket
après avoir redémarré le serveur.
**Toxiproxy provoque des échecs de connexion intermittents**. Utilisez des ports en dehors de la
plage de ports éphémères pour éviter des conflits de ports aléatoires. Elle va de `32,768` à `61,000` sur
Linux par défaut, voir `/proc/sys/net/ipv4/ip_local_port_range`.
**Devrais-je exécuter un Toxiproxy pour chaque application ?** Non, nous recommandons d'utiliser le
même Toxiproxy pour toutes les applications. Pour distinguer les services, nous
recommandons de nommer vos proxys selon le schéma : `<app>_<env>_<data store>_<shard>`.
Par exemple, `shopify_test_redis_master` ou `shopify_development_mysql_1`.
### Développement
* `make`. Construire un binaire de développement toxiproxy pour la plateforme actuelle.
* `make all`. Construire les binaires et paquets Toxiproxy pour toutes les plateformes. Nécessite
que Go soit compilé avec la compilation croisée activée sur Linux et Darwin (amd64)
ainsi que [`goreleaser`](https://goreleaser.com/) dans votre `$PATH` pour
construire les binaires du paquet Linux.
* `make test`. Exécuter les tests Toxiproxy.
### Publication
Voir [RELEASE.md](https://github.com/shopify/toxiproxy/blob/HEAD/RELEASE.md)
[blog]: https://shopify.engineering/building-and-testing-resilient-ruby-on-rails-applications