Mise en œuvre de l'ingénierie du chaos dans Kubernetes : Analyse des principes de Chaos Mesh et développement du plan de contrôle
Cette page a été traduite par PageTurner AI (bêta). Non approuvée officiellement par le projet. Vous avez trouvé une erreur ? Signaler un problème →

Chaos Mesh est une plateforme open source d'ingénierie du chaos cloud-native basée sur les définitions de ressources personnalisées (CRD) de Kubernetes. Elle permet de simuler divers types de pannes et dispose d'une grande capacité d'orchestration de scénarios de défaillance. Vous pouvez l'utiliser pour reproduire commodément des anomalies susceptibles de survenir dans les environnements de développement, de test et de production, afin d'identifier des problèmes potentiels dans le système.
Dans cet article, j'explorerai la pratique de l'ingénierie du chaos dans les clusters Kubernetes, analyserai les fonctionnalités clés de Chaos Mesh via son code source, et expliquerai comment développer son plan de contrôle avec des exemples de code.
Si vous n'êtes pas familier avec Chaos Mesh, consultez la documentation Chaos Mesh pour acquérir les bases de son architecture.
Le code de test de cet article est disponible dans le dépôt GitHub mayocream/chaos-mesh-controlpanel-demo.
Comment Chaos Mesh crée le chaos
Chaos Mesh est un couteau suisse pour implémenter l'ingénierie du chaos dans Kubernetes. Cette section explique son fonctionnement.
Mode privilégié
Chaos Mesh utilise des conteneurs privilégiés dans Kubernetes pour générer des défaillances. Le Pod Chaos Daemon s'exécute en tant que DaemonSet et ajoute des capabilités supplémentaires au runtime de conteneur via le contexte de sécurité du Pod.
apiVersion: apps/v1
kind: DaemonSet
spec:
template:
metadata: ...
spec:
containers:
- name: chaos-daemon
securityContext:
{{- if .Values.chaosDaemon.privileged }}
privileged: true
capabilities:
add:
- SYS_PTRACE
{{- else }}
capabilities:
add:
- SYS_PTRACE
- NET_ADMIN
- MKNOD
- SYS_CHROOT
- SYS_ADMIN
- KILL
# CAP_IPC_LOCK is used to lock memory
- IPC_LOCK
{{- end }}
Les capacités Linux accordent aux conteneurs les privilèges nécessaires pour créer et accéder au pipe FUSE (/dev/fuse). FUSE est l'interface système de fichiers en espace utilisateur de Linux, permettant aux utilisateurs non privilégiés de créer leurs propres systèmes de fichiers sans modifier le code du noyau.
Comme indiqué dans la pull request #1109 sur GitHub, le programme DaemonSet utilise cgo pour appeler la fonction Linux makedev afin de créer un pipe FUSE.
// #include <sys/sysmacros.h>
// #include <sys/types.h>
// // makedev is a macro, so a wrapper is needed
// dev_t Makedev(unsigned int maj, unsigned int min) {
// return makedev(maj, min);
// }
// EnsureFuseDev ensures /dev/fuse exists. If not, it will create one
func EnsureFuseDev() {
if _, err := os.Open("/dev/fuse"); os.IsNotExist(err) {
// 10, 229 according to https://www.kernel.org/doc/Documentation/admin-guide/devices.txt
fuse := C.Makedev(10, 229)
syscall.Mknod("/dev/fuse", 0o666|syscall.S_IFCHR, int(fuse))
}
}
Dans la pull request #1453, Chaos Daemon active par défaut le mode privilégié en définissant privileged: true dans le SecurityContext du conteneur.
Destruction de Pods
PodKill, PodFailure et ContainerKill appartiennent à la catégorie PodChaos. PodKill termine aléatoirement un Pod en appelant l'API server pour envoyer la commande de destruction.
import (
"context"
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type Impl struct {
client.Client
}
func (impl *Impl) Apply(ctx context.Context, index int, records []*v1alpha1.Record, obj v1alpha1.InnerObject) (v1alpha1.Phase, error) {
...
err = impl.Get(ctx, namespacedName, &pod)
if err != nil {
// TODO: handle this error
return v1alpha1.NotInjected, err
}
err = impl.Delete(ctx, &pod, &client.DeleteOptions{
GracePeriodSeconds: &podchaos.Spec.GracePeriod, // PeriodSeconds has to be set specifically
})
...
return v1alpha1.Injected, nil
}
Le paramètre GracePeriodSeconds permet à Kubernetes d'interrompre un Pod de force. Par exemple, pour supprimer immédiatement un Pod, utilisez la commande kubectl delete pod --grace-period=0 --force.
PodFailure modifie la ressource Pod pour remplacer l'image par une image incorrecte. Le chaos ne modifie que les champs image des containers et initContainers, car la plupart des métadonnées d'un Pod sont immuables. Pour plus de détails, voir la documentation sur la mise à jour et le remplacement des Pods.
func (impl *Impl) Apply(ctx context.Context, index int, records []*v1alpha1.Record, obj v1alpha1.InnerObject) (v1alpha1.Phase, error) {
...
pod := origin.DeepCopy()
for index := range pod.Spec.Containers {
originImage := pod.Spec.Containers[index].Image
name := pod.Spec.Containers[index].Name
key := annotation.GenKeyForImage(podchaos, name, false)
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
// If the annotation is already existed, we could skip the reconcile for this container
if _, ok := pod.Annotations[key]; ok {
continue
}
pod.Annotations[key] = originImage
pod.Spec.Containers[index].Image = config.ControllerCfg.PodFailurePauseImage
}
for index := range pod.Spec.InitContainers {
originImage := pod.Spec.InitContainers[index].Image
name := pod.Spec.InitContainers[index].Name
key := annotation.GenKeyForImage(podchaos, name, true)
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
// If the annotation is already existed, we could skip the reconcile for this container
if _, ok := pod.Annotations[key]; ok {
continue
}
pod.Annotations[key] = originImage
pod.Spec.InitContainers[index].Image = config.ControllerCfg.PodFailurePauseImage
}
err = impl.Patch(ctx, pod, client.MergeFrom(&origin))
if err != nil {
// TODO: handle this error
return v1alpha1.NotInjected, err
}
return v1alpha1.Injected, nil
}
L'image de conteneur par défaut provoquant des échecs est gcr.io/google-containers/pause:latest.
PodKill et PodFailure contrôlent le cycle de vie des Pods via l'API server de Kubernetes, tandis que ContainerKill agit via Chaos Daemon exécuté sur le nœud du cluster. ContainerKill utilise Chaos Controller Manager pour lancer des appels gRPC à Chaos Daemon.
func (b *ChaosDaemonClientBuilder) Build(ctx context.Context, pod *v1.Pod) (chaosdaemonclient.ChaosDaemonClientInterface, error) {
...
daemonIP, err := b.FindDaemonIP(ctx, pod)
if err != nil {
return nil, err
}
builder := grpcUtils.Builder(daemonIP, config.ControllerCfg.ChaosDaemonPort).WithDefaultTimeout()
if config.ControllerCfg.TLSConfig.ChaosMeshCACert != "" {
builder.TLSFromFile(config.ControllerCfg.TLSConfig.ChaosMeshCACert, config.ControllerCfg.TLSConfig.ChaosDaemonClientCert, config.ControllerCfg.TLSConfig.ChaosDaemonClientKey)
} else {
builder.Insecure()
}
cc, err := builder.Build()
if err != nil {
return nil, err
}
return chaosdaemonclient.New(cc), nil
}
Lorsque Chaos Controller Manager envoie des commandes à Chaos Daemon, il crée un client correspondant en fonction des informations du Pod. Par exemple, pour contrôler un Pod sur un nœud, il crée un client en récupérant le ClusterIP du nœud où le Pod est localisé. Si la configuration du certificat TLS (Transport Layer Security) existe, Controller Manager ajoute le certificat TLS pour le client.
Lorsque Chaos Daemon démarre, s'il possède un certificat TLS, il l'attache pour activer gRPCS. L'option de configuration TLS RequireAndVerifyClientCert indique si l'authentification mutuelle TLS (mTLS) doit être activée.
func newGRPCServer(containerRuntime string, reg prometheus.Registerer, tlsConf tlsConfig) (*grpc.Server, error) {
...
if tlsConf != (tlsConfig{}) {
caCert, err := ioutil.ReadFile(tlsConf.CaCert)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
serverCert, err := tls.LoadX509KeyPair(tlsConf.Cert, tlsConf.Key)
if err != nil {
return nil, err
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
})
grpcOpts = append(grpcOpts, grpc.Creds(creds))
}
s := grpc.NewServer(grpcOpts...)
grpcMetrics.InitializeMetrics(s)
pb.RegisterChaosDaemonServer(s, ds)
reflection.Register(s)
return s, nil
}
Chaos Daemon fournit les interfaces gRPC suivantes à appeler :
// ChaosDaemonClient is the client API for ChaosDaemon service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type ChaosDaemonClient interface {
SetTcs(ctx context.Context, in *TcsRequest, opts ...grpc.CallOption) (*empty.Empty, error)
FlushIPSets(ctx context.Context, in *IPSetsRequest, opts ...grpc.CallOption) (*empty.Empty, error)
SetIptablesChains(ctx context.Context, in *IptablesChainsRequest, opts ...grpc.CallOption) (*empty.Empty, error)
SetTimeOffset(ctx context.Context, in *TimeRequest, opts ...grpc.CallOption) (*empty.Empty, error)
RecoverTimeOffset(ctx context.Context, in *TimeRequest, opts ...grpc.CallOption) (*empty.Empty, error)
ContainerKill(ctx context.Context, in *ContainerRequest, opts ...grpc.CallOption) (*empty.Empty, error)
ContainerGetPid(ctx context.Context, in *ContainerRequest, opts ...grpc.CallOption) (*ContainerResponse, error)
ExecStressors(ctx context.Context, in *ExecStressRequest, opts ...grpc.CallOption) (*ExecStressResponse, error)
CancelStressors(ctx context.Context, in *CancelStressRequest, opts ...grpc.CallOption) (*empty.Empty, error)
ApplyIOChaos(ctx context.Context, in *ApplyIOChaosRequest, opts ...grpc.CallOption) (*ApplyIOChaosResponse, error)
ApplyHttpChaos(ctx context.Context, in *ApplyHttpChaosRequest, opts ...grpc.CallOption) (*ApplyHttpChaosResponse, error)
SetDNSServer(ctx context.Context, in *SetDNSServerRequest, opts ...grpc.CallOption) (*empty.Empty, error)
}
Injection de défaillances réseau
D'après la pull request #41, Chaos Mesh injecte les défaillances réseau ainsi : il appelle pbClient.SetNetem pour encapsuler les paramètres dans une requête et l'envoie au Chaos Daemon du nœud pour traitement.
Le code d'injection de défaillance réseau est présenté ci-dessous tel qu'il apparaissait en 2019. Au fil du développement du projet, les fonctions ont été réparties dans plusieurs fichiers.
func (r *Reconciler) applyPod(ctx context.Context, pod *v1.Pod, networkchaos *v1alpha1.NetworkChaos) error {
...
pbClient := pb.NewChaosDaemonClient(c)
containerId := pod.Status.ContainerStatuses[0].ContainerID
netem, err := spec.ToNetem()
if err != nil {
return err
}
_, err = pbClient.SetNetem(ctx, &pb.NetemRequest{
ContainerId: containerId,
Netem: netem,
})
return err
}
Dans le package pkg/chaosdaemon, nous pouvons observer comment Chaos Daemon traite les requêtes.
func (s *Server) SetNetem(ctx context.Context, in *pb.NetemRequest) (*empty.Empty, error) {
log.Info("Set netem", "Request", in)
pid, err := s.crClient.GetPidFromContainerID(ctx, in.ContainerId)
if err != nil {
return nil, status.Errorf(codes.Internal, "get pid from containerID error: %v", err)
}
if err := Apply(in.Netem, pid); err != nil {
return nil, status.Errorf(codes.Internal, "netem apply error: %v", err)
}
return &empty.Empty{}, nil
}
// Apply applies a netem on eth0 in pid related namespace
func Apply(netem *pb.Netem, pid uint32) error {
log.Info("Apply netem on PID", "pid", pid)
ns, err := netns.GetFromPath(GenNetnsPath(pid))
if err != nil {
log.Error(err, "failed to find network namespace", "pid", pid)
return errors.Trace(err)
}
defer ns.Close()
handle, err := netlink.NewHandleAt(ns)
if err != nil {
log.Error(err, "failed to get handle at network namespace", "network namespace", ns)
return err
}
link, err := handle.LinkByName("eth0") // TODO: check whether interface name is eth0
if err != nil {
log.Error(err, "failed to find eth0 interface")
return errors.Trace(err)
}
netemQdisc := netlink.NewNetem(netlink.QdiscAttrs{
LinkIndex: link.Attrs().Index,
Handle: netlink.MakeHandle(1, 0),
Parent: netlink.HANDLE_ROOT,
}, ToNetlinkNetemAttrs(netem))
if err = handle.QdiscAdd(netemQdisc); err != nil {
if !strings.Contains(err.Error(), "file exists") {
log.Error(err, "failed to add Qdisc")
return errors.Trace(err)
}
}
return nil
}
Enfin, la bibliothèque vishvananda/netlink opère sur l'interface réseau Linux pour accomplir la tâche.
À ce stade, NetworkChaos manipule le réseau hôte Linux pour créer le chaos. Cela inclut des outils comme iptables et ipset.
Dans le Dockerfile de Chaos Daemon, vous pouvez voir la chaîne d'outils Linux dont il dépend :
RUN apt-get update && \
apt-get install -y tzdata iptables ipset stress-ng iproute2 fuse util-linux procps curl && \
rm -rf /var/lib/apt/lists/*
Test de stress
Chaos Daemon implémente également StressChaos. Après que le Controller Manager a calculé les règles, il envoie la tâche au Daemon spécifique. Les paramètres assemblés sont présentés ci-dessous. Ils sont combinés en paramètres d'exécution de commande et ajoutés à la commande stress-ng pour exécution.
// Normalize the stressors to comply with stress-ng
func (in *Stressors) Normalize() (string, error) {
stressors := ""
if in.MemoryStressor != nil && in.MemoryStressor.Workers != 0 {
stressors += fmt.Sprintf(" --vm %d --vm-keep", in.MemoryStressor.Workers)
if len(in.MemoryStressor.Size) != 0 {
if in.MemoryStressor.Size[len(in.MemoryStressor.Size)-1] != '%' {
size, err := units.FromHumanSize(string(in.MemoryStressor.Size))
if err != nil {
return "", err
}
stressors += fmt.Sprintf(" --vm-bytes %d", size)
} else {
stressors += fmt.Sprintf(" --vm-bytes %s",
in.MemoryStressor.Size)
}
}
if in.MemoryStressor.Options != nil {
for _, v := range in.MemoryStressor.Options {
stressors += fmt.Sprintf(" %v ", v)
}
}
}
if in.CPUStressor != nil && in.CPUStressor.Workers != 0 {
stressors += fmt.Sprintf(" --cpu %d", in.CPUStressor.Workers)
if in.CPUStressor.Load != nil {
stressors += fmt.Sprintf(" --cpu-load %d",
*in.CPUStressor.Load)
}
if in.CPUStressor.Options != nil {
for _, v := range in.CPUStressor.Options {
stressors += fmt.Sprintf(" %v ", v)
}
}
}
return stressors, nil
}
Le côté serveur de Chaos Daemon traite la commande d'exécution de la fonction pour appeler le package Go officiel os/exec. Pour plus de détails, consultez le fichier pkg/chaosdaemon/stress_server_linux.go. Un fichier homonyme suffixé par darwin existe également. Les fichiers *_darwin préviennent d'éventuelles erreurs lors de l'exécution du programme sur macOS.
Le code utilise le package shirou/gopsutil pour obtenir le statut du processus PID et lire les sorties standard stdout et stderr. J'ai observé ce mode de traitement dans hashicorp/go-plugin, et go-plugin le réalise plus efficacement.
Injection de défauts d'E/S
La pull request #826 introduit une nouvelle implémentation de IOChaos, sans injection de sidecar. Elle utilise Chaos Daemon pour manipuler directement l'espace de noms Linux via les commandes sous-jacentes du conteneur runc et exécute le programme FUSE chaos-mesh/toda développé en Rust pour injecter des défauts d'E/S dans les conteneurs. Le protocole JSON-RPC 2.0 est utilisé pour la communication entre toda et le plan de contrôle.
La nouvelle implémentation d'IOChaos ne modifie pas les ressources Pod. Lorsque vous définissez une expérience de chaos IOChaos, une ressource PodIOChaos correspondante est créée pour chaque Pod filtré par le champ selector. La référence owner de PodIoChaos pointe vers le Pod. Simultanément, un ensemble de finalizers est ajouté à PodIoChaos pour libérer ses ressources avant sa suppression.
// Apply implements the reconciler.InnerReconciler.Apply
func (r *Reconciler) Apply(ctx context.Context, req ctrl.Request, chaos v1alpha1.InnerObject) error {
iochaos, ok := chaos.(*v1alpha1.IoChaos)
if !ok {
err := errors.New("chaos is not IoChaos")
r.Log.Error(err, "chaos is not IoChaos", "chaos", chaos)
return err
}
source := iochaos.Namespace + "/" + iochaos.Name
m := podiochaosmanager.New(source, r.Log, r.Client)
pods, err := utils.SelectAndFilterPods(ctx, r.Client, r.Reader, &iochaos.Spec)
if err != nil {
r.Log.Error(err, "failed to select and filter pods")
return err
}
r.Log.Info("applying iochaos", "iochaos", iochaos)
for _, pod := range pods {
t := m.WithInit(types.NamespacedName{
Name: pod.Name,
Namespace: pod.Namespace,
})
// TODO: support chaos on multiple volume
t.SetVolumePath(iochaos.Spec.VolumePath)
t.Append(v1alpha1.IoChaosAction{
Type: iochaos.Spec.Action,
Filter: v1alpha1.Filter{
Path: iochaos.Spec.Path,
Percent: iochaos.Spec.Percent,
Methods: iochaos.Spec.Methods,
},
Faults: []v1alpha1.IoFault{
{
Errno: iochaos.Spec.Errno,
Weight: 1,
},
},
Latency: iochaos.Spec.Delay,
AttrOverrideSpec: iochaos.Spec.Attr,
Source: m.Source,
})
key, err := cache.MetaNamespaceKeyFunc(&pod)
if err != nil {
return err
}
iochaos.Finalizers = utils.InsertFinalizer(iochaos.Finalizers, key)
}
r.Log.Info("commiting updates of podiochaos")
err = m.Commit(ctx)
if err != nil {
r.Log.Error(err, "fail to commit")
return err
}
r.Event(iochaos, v1.EventTypeNormal, utils.EventChaosInjected, "")
return nil
}
Dans le contrôleur de la ressource PodIoChaos, le Controller Manager encapsule la ressource en paramètres et appelle l'interface de Chaos Daemon pour traiter ces paramètres.
// Apply flushes io configuration on pod
func (h *Handler) Apply(ctx context.Context, chaos *v1alpha1.PodIoChaos) error {
h.Log.Info("updating io chaos", "pod", chaos.Namespace+"/"+chaos.Name, "spec", chaos.Spec)
...
res, err := pbClient.ApplyIoChaos(ctx, &pb.ApplyIoChaosRequest{
Actions: input,
Volume: chaos.Spec.VolumeMountPath,
ContainerId: containerID,
Instance: chaos.Spec.Pid,
StartTime: chaos.Spec.StartTime,
})
if err != nil {
return err
}
chaos.Spec.Pid = res.Instance
chaos.Spec.StartTime = res.StartTime
chaos.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: pod.APIVersion,
Kind: pod.Kind,
Name: pod.Name,
UID: pod.UID,
},
}
return nil
}
Le fichier pkg/chaosdaemon/iochaos_server.go traite IOChaos. Dans ce fichier, un programme FUSE doit être injecté dans le conteneur. Comme discuté dans l'issue #2305 sur GitHub, la commande /usr/local/bin/nsexec -l- p /proc/119186/ns/pid -m /proc/119186/ns/mnt - /usr/local/bin/toda --path /tmp --verbose info est exécutée pour lancer le programme toda dans le même namespace que le Pod.
func (s *DaemonServer) ApplyIOChaos(ctx context.Context, in *pb.ApplyIOChaosRequest) (*pb.ApplyIOChaosResponse, error) {
...
pid, err := s.crClient.GetPidFromContainerID(ctx, in.ContainerId)
if err != nil {
log.Error(err, "error while getting PID")
return nil, err
}
args := fmt.Sprintf("--path %s --verbose info", in.Volume)
log.Info("executing", "cmd", todaBin+" "+args)
processBuilder := bpm.DefaultProcessBuilder(todaBin, strings.Split(args, " ")...).
EnableLocalMnt().
SetIdentifier(in.ContainerId)
if in.EnterNS {
processBuilder = processBuilder.SetNS(pid, bpm.MountNS).SetNS(pid, bpm.PidNS)
}
...
// Calls JSON RPC
client, err := jrpc.DialIO(ctx, receiver, caller)
if err != nil {
return nil, err
}
cmd := processBuilder.Build()
procState, err := s.backgroundProcessManager.StartProcess(cmd)
if err != nil {
return nil, err
}
...
}
L'exemple de code suivant construit les commandes d'exécution. Ces commandes constituent l'implémentation sous-jacente de l'isolation des namespaces par runc :
// GetNsPath returns corresponding namespace path
func GetNsPath(pid uint32, typ NsType) string {
return fmt.Sprintf("%s/%d/ns/%s", DefaultProcPrefix, pid, string(typ))
}
// SetNS sets the namespace of the process
func (b *ProcessBuilder) SetNS(pid uint32, typ NsType) *ProcessBuilder {
return b.SetNSOpt([]nsOption{{
Typ: typ,
Path: GetNsPath(pid, typ),
}})
}
// Build builds the process
func (b *ProcessBuilder) Build() *ManagedProcess {
args := b.args
cmd := b.cmd
if len(b.nsOptions) > 0 {
args = append([]string{"--", cmd}, args...)
for _, option := range b.nsOptions {
args = append([]string{"-" + nsArgMap[option.Typ], option.Path}, args...)
}
if b.localMnt {
args = append([]string{"-l"}, args...)
}
cmd = nsexecPath
}
...
}
Plan de contrôle
Chaos Mesh est un système d'ingénierie du chaos open source sous licence Apache 2.0. Comme mentionné précédemment, il offre des capacités riches et un bon écosystème. L'équipe de maintenance a développé le système FUSE chaos-mesh/toda, le plugin de chaos CoreDNS chaos-mesh/k8s_dns_chaos et l'injection d'erreurs kernel basée sur Berkeley Packet Filter (BPF) chaos-mesh/bpfki.
Je vais maintenant décrire le code serveur nécessaire pour construire une plateforme d'ingénierie du chaos orientée utilisateur final. Cette implémentation n'est qu'un exemple—pas nécessairement optimal. Pour voir des pratiques de développement sur une plateforme réelle, consultez le Dashboard de Chaos Mesh. Il utilise le framework d'injection de dépendances uber-go/fx et le mode manager du controller runtime.
Fonctionnalités clés de Chaos Mesh
Comme illustré dans le workflow Chaos Mesh ci-dessous, nous devons implémenter un serveur qui envoie du YAML à l'API Kubernetes. Chaos Controller Manager implémente la validation complexe des règles et leur livraison à Chaos Daemon. Pour utiliser Chaos Mesh avec votre propre plateforme, il suffit de s'interfacer avec le processus de création des ressources CRD.

Examinons l'exemple du site web de Chaos Mesh :
import (
"context"
"github.com/pingcap/chaos-mesh/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func main() {
...
delay := &chaosv1alpha1.NetworkChaos{
Spec: chaosv1alpha1.NetworkChaosSpec{...},
}
k8sClient := client.New(conf, client.Options{ Scheme: scheme.Scheme })
k8sClient.Create(context.TODO(), delay)
k8sClient.Delete(context.TODO(), delay)
}
Chaos Mesh fournit des API correspondant à toutes les CRD. Nous utilisons le controller-runtime développé par le SIG API Machinery de Kubernetes pour simplifier l'interaction avec l'API Kubernetes.
Injecter du chaos
Supposons que nous voulions créer une ressource PodKill via un programme. Après envoi à l'API server Kubernetes, elle passe par le validating admission controller de Chaos Controller Manager pour vérification des données. Lors de la création d'une expérience de chaos, si la validation échoue, une erreur est renvoyée au client. Pour les paramètres spécifiques, consultez Créer des expériences avec des fichiers de configuration YAML.
NewClient crée un client d'API Kubernetes. Vous pouvez vous référer à cet exemple :
package main
import (
"context"
"controlpanel"
"log"
"github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func applyPodKill(name, namespace string, labels map[string]string) error {
cli, err := controlpanel.NewClient()
if err != nil {
return errors.Wrap(err, "create client")
}
cr := &v1alpha1.PodChaos{
ObjectMeta: metav1.ObjectMeta{
GenerateName: name,
Namespace: namespace,
},
Spec: v1alpha1.PodChaosSpec{
Action: v1alpha1.PodKillAction,
ContainerSelector: v1alpha1.ContainerSelector{
PodSelector: v1alpha1.PodSelector{
Mode: v1alpha1.OnePodMode,
Selector: v1alpha1.PodSelectorSpec{
Namespaces: []string{namespace},
LabelSelectors: labels,
},
},
},
},
}
if err := cli.Create(context.Background(), cr); err != nil {
return errors.Wrap(err, "create podkill")
}
return nil
}
Le journal de sortie du programme en cours d'exécution est :
I1021 00:51:55.225502 23781 request.go:665] Waited for 1.033116256s due to client-side throttling, not priority and fairness, request: GET:https://***
2021/10/21 00:51:56 apply podkill
Utilisez kubectl pour vérifier l'état de la ressource PodKill :
$ k describe podchaos.chaos-mesh.org -n dev podkillvjn77
Name: podkillvjn77
Namespace: dev
Labels: <none>
Annotations: <none>
API Version: chaos-mesh.org/v1alpha1
Kind: PodChaos
Metadata:
Creation Timestamp: 2021-10-20T16:51:56Z
Finalizers:
chaos-mesh/records
Generate Name: podkill
Generation: 7
Resource Version: 938921488
Self Link: /apis/chaos-mesh.org/v1alpha1/namespaces/dev/podchaos/podkillvjn77
UID: afbb40b3-ade8-48ba-89db-04918d89fd0b
Spec:
Action: pod-kill
Grace Period: 0
Mode: one
Selector:
Label Selectors:
app: nginx
Namespaces:
dev
Status:
Conditions:
Reason:
Status: False
Type: Paused
Reason:
Status: True
Type: Selected
Reason:
Status: True
Type: AllInjected
Reason:
Status: False
Type: AllRecovered
Experiment:
Container Records:
Id: dev/nginx
Phase: Injected
Selector Key: .
Desired Phase: Run
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal FinalizerInited 6m35s finalizer Finalizer has been inited
Normal Updated 6m35s finalizer Successfully update finalizer of resource
Normal Updated 6m35s records Successfully update records of resource
Normal Updated 6m35s desiredphase Successfully update desiredPhase of resource
Normal Applied 6m35s records Successfully apply chaos for dev/nginx
Normal Updated 6m35s records Successfully update records of resource
Le plan de contrôle doit également interroger et acquérir les ressources Chaos, afin que les utilisateurs de la plateforme puissent visualiser l'état d'exécution de toutes les expériences de chaos et les gérer. Pour cela, nous pouvons appeler l'API REST pour envoyer une requête Get ou List. Mais en pratique, il faut prêter attention aux détails. Dans notre entreprise, nous avons constaté qu'à chaque fois que le contrôleur demande la totalité des données de ressources, la charge du serveur d'API Kubernetes augmente.
Je vous recommande de lire le tutoriel sur le client controller-runtime (en japonais). Si vous ne comprenez pas le japonais, vous pouvez tout de même apprendre beaucoup de ce tutoriel en lisant le code source. Il couvre de nombreux détails. Par exemple, par défaut, le controller-runtime lit le kubeconfig, les flags, les variables d'environnement et le compte de service automatiquement monté dans le Pod à partir de plusieurs emplacements. La pull request #21 pour armosec/kubescape utilise cette fonctionnalité. Ce tutoriel inclut également des opérations courantes, comme la pagination, la mise à jour et l'écrasement d'objets. Je n'ai vu aucun tutoriel en anglais aussi détaillé.
Voici des exemples de requêtes Get et List :
package controlpanel
import (
"context"
"github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
"github.com/pkg/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func GetPodChaos(name, namespace string) (*v1alpha1.PodChaos, error) {
cli := mgr.GetClient()
item := new(v1alpha1.PodChaos)
if err := cli.Get(context.Background(), client.ObjectKey{Name: name, Namespace: namespace}, item); err != nil {
return nil, errors.Wrap(err, "get cr")
}
return item, nil
}
func ListPodChaos(namespace string, labels map[string]string) ([]v1alpha1.PodChaos, error) {
cli := mgr.GetClient()
list := new(v1alpha1.PodChaosList)
if err := cli.List(context.Background(), list, client.InNamespace(namespace), client.MatchingLabels(labels)); err != nil {
return nil, err
}
return list.Items, nil
}
Cet exemple utilise le manager. Ce mode évite que le mécanisme de cache ne récupère de manière répétitive de grandes quantités de données. La figure suivante montre le flux de travail :
-
Obtenir le Pod.
-
Obtenir la totalité des données de la requête
Listpour la première fois. -
Mettre à jour le cache lorsque les données surveillées changent.

Orchestrer le chaos
L'interface de runtime de conteneur (CRI) fournit des capacités d'isolation sous-jacentes solides qui peuvent supporter le fonctionnement stable du conteneur. Mais pour des scénarios plus complexes et évolutifs, l'orchestration de conteneurs est nécessaire. Chaos Mesh propose également des fonctionnalités Schedule et Workflow. Basé sur l'heure Cron définie, Schedule peut déclencher des pannes à intervalles réguliers. Workflow peut planifier plusieurs tests de défaillance à la manière d'Argo Workflows.
Chaos Controller Manager effectue la majeure partie du travail pour nous. Le plan de contrôle gère principalement ces ressources YAML. Vous devez simplement considérer les fonctionnalités que vous souhaitez offrir aux utilisateurs finaux.
Fonctionnalités de la plateforme
La figure suivante montre le tableau de bord de Chaos Mesh. Nous devons déterminer quelles fonctionnalités la plateforme doit offrir aux utilisateurs finaux.

D'après le tableau de bord, nous savons que la plateforme pourrait inclure ces fonctionnalités :
-
Injection de chaos
-
Crash de Pod
-
Défaillance réseau
-
Test de charge
-
Défaillance I/O
-
Suivi d'événements
-
Alerte associée
-
Télémétrie temporelle
Si Chaos Mesh vous intéresse et que vous souhaitez contribuer à son amélioration, rejoignez son canal Slack (#project-chaos-mesh) ou soumettez vos pull requests ou issues sur son dépôt GitHub.