Zum Hauptinhalt springen

Chaos Engineering in Kubernetes implementieren: Funktionsanalyse von Chaos Mesh und Entwicklung der Steuerungsebene

· 19 Minuten Lesezeit
Mayo Cream
Kubernetes Member, CNCF Security TAG Member, OSS Contributor
Inoffizielle Beta-Übersetzung

Diese Seite wurde von PageTurner AI übersetzt (Beta). Nicht offiziell vom Projekt unterstützt. Fehler gefunden? Problem melden →

Chaos Engineering in Kubernetes implementieren
Chaos Engineering in Kubernetes implementieren

Chaos Mesh ist eine Open-Source, Cloud-native Chaos-Engineering-Plattform, die auf Kubernetes Custom Resource Definitions (CRDs) basiert. Chaos Mesh kann verschiedene Fehlertypen simulieren und bietet umfangreiche Möglichkeiten zur Orchestrierung von Fehlerszenarien. Sie können Chaos Mesh nutzen, um gezielt verschiedene Störungen in Entwicklungs-, Test- und Produktionsumgebungen nachzubilden und potenzielle Systemschwachstellen zu identifizieren.

In diesem Artikel untersuche ich die praktische Anwendung von Chaos Engineering in Kubernetes-Clustern, analysiere wichtige Funktionen von Chaos Mesh anhand des Quellcodes und zeige anhand von Codebeispielen, wie Sie die Steuerungsebene von Chaos Mesh entwickeln können.

Falls Sie mit Chaos Mesh noch nicht vertraut sind, lesen Sie bitte die Chaos Mesh-Dokumentation, um die Grundlagen der Architektur zu verstehen.

Den Testcode für diesen Artikel finden Sie im GitHub-Repository mayocream/chaos-mesh-controlpanel-demo.

Wie Chaos Mesh Chaos erzeugt

Chaos Mesh ist das Schweizer Taschenmesser für Chaos Engineering in Kubernetes. Dieser Abschnitt erklärt die Funktionsweise.

Privilegierter Modus

Chaos Mesh nutzt privilegierte Container in Kubernetes, um Fehler zu erzeugen. Der Pod von Chaos Daemon läuft als DaemonSet und fügt über den Security Context des Pods zusätzliche Capabilities zur Container-Laufzeitumgebung hinzu.

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

Die Linux-Capabilities erlauben Containern, auf die /dev/fuse-Schnittstelle (Filesystem in Userspace) zuzugreifen. FUSE ist die Linux-Benutzerbereichs-Dateisystemschnittstelle, die nicht-privilegierten Benutzern ermöglicht, eigene Dateisysteme zu erstellen, ohne Kernel-Code zu modifizieren.

Gemäß Pull Request #1109 auf GitHub nutzt das DaemonSet-Programm cgo, um die Linux-Funktion makedev aufzurufen und eine FUSE-Schnittstelle zu erstellen.

// #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))
}
}

In Pull Request #1453 aktiviert Chaos Daemon standardmäßig den privilegierten Modus, indem es privileged: true im SecurityContext des Containers setzt.

Pods terminieren

PodKill, PodFailure und ContainerKill gehören zur Kategorie PodChaos. PodKill beendet zufällig einen Pod, indem es den API-Server zur Ausführung des Kill-Befehls aufruft.

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
}

Der Parameter GracePeriodSeconds ermöglicht Kubernetes das erzwungene Beenden eines Pods. Um einen Pod sofort zu löschen, verwenden Sie beispielsweise den Befehl kubectl delete pod --grace-period=0 --force.

PodFailure modifiziert die Pod-Ressource, indem es das Container-Image durch ein fehlerhaftes ersetzt. Dabei werden nur die image-Felder von containers und initContainers geändert, da die meisten Pod-Metadaten unveränderlich sind. Details finden Sie unter Pod-Aktualisierung und -Ersetzung.

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
}

Das standardmäßig verwendete Fehler-Image ist gcr.io/google-containers/pause:latest.

PodKill und PodFailure steuern den Pod-Lebenszyklus über den Kubernetes-API-Server. Aber ContainerKill macht dies über den Chaos Daemon auf dem Cluster-Knoten. ContainerKill verwendet den Chaos Controller Manager, um über einen Client gRPC-Aufrufe an Chaos Daemon zu initiieren.

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
}

Wenn der Chaos Controller Manager Befehle an den Chaos Daemon sendet, erstellt er basierend auf den Pod-Informationen einen entsprechenden Client. Um beispielsweise einen Pod auf einem Knoten zu steuern, erstellt er einen Client, indem er die ClusterIP des Knotens abruft, auf dem sich der Pod befindet. Falls eine TLS-Zertifikatkonfiguration (Transport Layer Security) vorhanden ist, fügt der Controller Manager dem Client das TLS-Zertifikat hinzu.

Wenn der Chaos Daemon startet und über ein TLS-Zertifikat verfügt, bindet er dieses ein, um gRPCS zu aktivieren. Die TLS-Konfigurationsoption RequireAndVerifyClientCert gibt an, ob gegenseitige TLS-Authentifizierung (mTLS) aktiviert werden soll.

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 bietet folgende gRPC-Schnittstellen für Aufrufe:

// 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)
}

Netzwerkfehler-Injektion

Aus Pull Request #41 geht hervor, dass Chaos Mesh Netzwerkfehler folgendermaßen injiziert: Es ruft pbClient.SetNetem auf, um Parameter in eine Anfrage zu kapseln und diese an den Chaos Daemon auf dem Knoten zur Verarbeitung zu senden.

Der Code zur Netzwerkfehler-Injektion ist unten im Stand von 2019 dargestellt. Im Laufe der Projektentwicklung wurden die Funktionen auf mehrere Dateien verteilt.

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
}

Im Paket pkg/chaosdaemon können wir sehen, wie Chaos Daemon Anfragen verarbeitet.

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
}

Abschließend erledigt die Bibliothek vishvananda/netlink den Job durch Operationen an der Linux-Netzwerkschnittstelle.

Ab diesem Punkt manipuliert NetworkChaos das Linux-Hostnetzwerk, um Chaos zu erzeugen. Dabei kommen Tools wie iptables und ipset zum Einsatz.

Im Dockerfile von Chaos Daemon ist die Linux-Toolchain zu sehen, von der es abhängt:

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/*

Stresstest

Chaos Daemon implementiert ebenfalls StressChaos. Nachdem der Controller Manager die Regeln berechnet hat, sendet er die Aufgabe an den spezifischen Daemon. Die zusammengesetzten Parameter sind unten dargestellt. Sie werden zu Befehlsausführungsparametern kombiniert und an den stress-ng-Befehl angehängt.

// 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
}

Die Server-Seite von Chaos Daemon verarbeitet den Ausführungsbefehl der Funktion durch Aufruf des offiziellen Go-Pakets os/exec. Details finden sich in der Datei pkg/chaosdaemon/stress_server_linux.go. Es existiert auch eine gleichnamige Datei mit der Endung darwin. *_darwin-Dateien verhindern mögliche Fehler beim Betrieb auf macOS.

Der Code verwendet das Paket shirou/gopsutil, um den PID-Prozessstatus abzufragen, und liest die Standardausgaben stdout und stderr. Dieses Verarbeitungsmuster ist auch in hashicorp/go-plugin zu finden, wobei go-plugin dies besser umsetzt.

I/O-Fehler-Injektion

Pull Request #826 führt eine neue Implementierung von IOChaos ohne Sidecar-Injektion ein. Dabei verwendet Chaos Daemon direkt die Low-Level-Befehle der runc-Container, um den Linux-Namespace zu manipulieren, und führt das in Rust entwickelte chaos-mesh/toda-FUSE-Programm aus, um Container-I/O-Chaos zu injizieren. Für die Kommunikation zwischen toda und der Steuerungsebene kommt das JSON-RPC 2.0-Protokoll zum Einsatz.

Die neue IOChaos-Implementierung modifiziert keine Pod-Ressourcen. Wenn Sie ein IOChaos-Experiment definieren, wird für jeden durch das Selector-Feld gefilterten Pod eine entsprechende PodIOChaos-Ressource erstellt. Die Owner Reference von PodIoChaos verweist auf den Pod. Gleichzeitig wird eine Reihe von Finalizern zu PodIoChaos hinzugefügt, um die Ressourcen freizugeben, bevor PodIoChaos gelöscht wird.

// 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
}

Im Controller der PodIoChaos-Ressource kapselt der Controller Manager die Ressource in Parameter und ruft die Chaos Daemon-Schnittstelle auf, um diese Parameter zu verarbeiten.

// 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
}

Die Datei pkg/chaosdaemon/iochaos_server.go verarbeitet IOChaos. In dieser Datei muss ein FUSE-Programm in den Container injiziert werden. Wie in Issue #2305 auf GitHub diskutiert, wird der Befehl /usr/local/bin/nsexec -l- p /proc/119186/ns/pid -m /proc/119186/ns/mnt - /usr/local/bin/toda --path /tmp --verbose info ausgeführt, um das Toda-Programm im selben Namespace wie der Pod zu starten.

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
}
...
}

Das folgende Codebeispiel konstruiert die auszuführenden Befehle. Diese Befehle repräsentieren die zugrundeliegende Namespace-Isolationsimplementierung von 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
}
...
}

Steuerungsebene

Chaos Mesh ist ein Open-Source-Chaos-Engineering-System unter der Apache-2.0-Lizenz. Wie bereits erörtert, bietet es umfangreiche Funktionen und ein gutes Ökosystem. Das Entwicklungsteam hat basierend auf dem Chaos-System das FUSE-basierte chaos-mesh/toda, das CoreDNS-Chaos-Plugin chaos-mesh/k8s_dns_chaos und die Berkeley Packet Filter (BPF)-basierte Kernel-Fehlerinjektion chaos-mesh/bpfki entwickelt.

Nun beschreibe ich den serverseitigen Code, der für den Aufbau einer anwenderoffenen Chaos-Engineering-Plattform erforderlich ist. Diese Implementierung dient lediglich als Beispiel – nicht unbedingt als bestmögliche Referenz. Für Entwicklungspraktiken in realen Plattformen können Sie Chaos Mesh's Dashboard konsultieren. Es verwendet das uber-go/fx-Dependency-Injection-Framework und den Manager-Modus der Controller-Runtime.

Wichtige Funktionen von Chaos Mesh

Wie im folgenden Chaos Mesh-Workflow dargestellt, müssen wir einen Server implementieren, der YAML an die Kubernetes-API sendet. Der Chaos Controller Manager übernimmt komplexe Regelvalidierungen und die Regelweitergabe an Chaos Daemon. Wenn Sie Chaos Mesh mit Ihrer eigenen Plattform integrieren möchten, müssen Sie lediglich den Prozess zur Erstellung von CRD-Ressourcen anbinden.

Chaos Mesh&#39;s grundlegender Workflow
Chaos Mesh&#39;s grundlegender Workflow

Werfen wir einen Blick auf das Beispiel auf der Chaos Mesh-Website:

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 bietet APIs für alle CRDs. Wir verwenden die von Kubernetes API Machinery SIG entwickelte controller-runtime, um die Interaktion mit der Kubernetes-API zu vereinfachen.

Chaos einspielen

Angenommen, wir möchten eine PodKill-Ressource durch Programmaufruf erstellen. Nachdem die Ressource an den Kubernetes-API-Server gesendet wurde, durchläuft sie Chaos Controller Managers validierenden Admission Controller zur Datenprüfung. Wenn wir ein Chaos-Experiment erstellen und der Admission Controller die Eingabedaten nicht validieren kann, wird ein Fehler an den Client zurückgegeben. Spezifische Parameter können Sie unter Experimente mit YAML-Konfigurationsdateien erstellen nachlesen.

NewClient erstellt einen Kubernetes-API-Client. Sie können sich an diesem Beispiel orientieren:

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
}

Die Log-Ausgabe des laufenden Programms lautet:

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

Verwenden Sie kubectl, um den Status der PodKill-Ressource zu prüfen:

$ 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

Die Steuerungsebene muss auch Chaos-Ressourcen abfragen und abrufen können, damit Plattformnutzer den Implementierungsstatus aller Chaos-Experimente einsehen und verwalten können. Dazu können wir die REST-API aufrufen, um Get- oder List-Anfragen zu senden. In der Praxis müssen wir jedoch auf Details achten. In unserem Unternehmen haben wir festgestellt, dass bei jeder Abfrage des vollständigen Ressourcendatensatzes durch den Controller die Last des Kubernetes-API-Servers ansteigt.

Ich empfehle Ihnen das Tutorial How to use the controller-runtime client (auf Japanisch). Wenn Sie kein Japanisch verstehen, können Sie dennoch viel aus dem Tutorial lernen, indem Sie den Quellcode lesen. Es behandelt viele Details. Beispielsweise liest der Controller Runtime standardmäßig Kubeconfig, Flags, Umgebungsvariablen und das Servicekonto automatisch aus mehreren Quellen, einschließlich der im Pod bereitgestellten Zugangsdaten. Pull request #21 für armosec/kubescape nutzt diese Funktion. Das Tutorial behandelt auch gängige Operationen wie Paginierung, Aktualisierung und Überschreiben von Objekten. Ich kenne keine englischen Tutorials, die so detailliert sind.

Hier Beispiele für Get- und List-Anfragen:

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
}

Dieses Beispiel nutzt den Manager. Dieser Modus verhindert, dass der Cache-Mechanismus wiederholt große Datenmengen abruft. Die folgende Abbildung zeigt den Workflow:

  1. Pod abrufen.

  2. Vollständige Daten der List-Anfrage erstmals abholen.

  3. Cache aktualisieren, wenn sich Watch-Daten ändern.

List request
List request

Chaos orchestrieren

Die Container Runtime Interface (CRI) bietet starke Isolationsfähigkeiten, die den stabilen Betrieb von Containern unterstützen. Für komplexere und skalierbare Szenarien ist jedoch eine Container-Orchestrierung erforderlich. Chaos Mesh bietet auch Schedule und Workflow-Funktionen. Basierend auf festgelegten Cron-Zeiten kann Schedule regelmäßig und in Intervallen Fehler auslösen. Workflow kann wie Argo Workflows mehrere Fehlertests planen.

Chaos Controller Manager übernimmt den Großteil der Arbeit für uns. Die Steuerungsebene verwaltet hauptsächlich diese YAML-Ressourcen. Sie müssen nur überlegen, welche Funktionen Sie Endnutzern bereitstellen möchten.

Plattformfunktionen

Die folgende Abbildung zeigt das Chaos Mesh Dashboard. Wir müssen überlegen, welche Funktionen die Plattform Endnutzern bieten soll.

Chaos Mesh Dashboard
Chaos Mesh Dashboard

Aus dem Dashboard ergeben sich möglicherweise diese Funktionen:

  • Chaos-Injektion

  • Pod-Absturz

  • Netzwerkausfälle

  • Lasttests

  • I/O-Fehler

  • Ereignisverfolgung

  • Verbundene Alarmierung

  • Zeitgesteuerte Telemetrie

Wenn Sie sich für Chaos Mesh interessieren und es verbessern möchten, treten Sie dem Slack-Kanal bei (#project-chaos-mesh) oder senden Sie Pull Requests oder Issues an das GitHub-Repository.