在 Kubernetes 實施混沌工程—— Chaos Mesh® 原理分析與控制面開發

語言: CN / TW / HK

Chaos Mesh® 是由 TiDB 背後的 PingCAP 公司開發,執行在 Kubernetes 上的混沌工程(Chaos Engineering)系統。簡而言之,Chaos Mesh® 通過執行在 K8s 叢集中的“特權”容器,依據 CRD 資源中的測試場景,在叢集中製造渾沌(模擬故障)[1]。

本文探索混沌工程在 Kubernetes 叢集上的實踐,基於原始碼分析瞭解 Chaos Mesh® 的工作原理,以程式碼示例闡述如何開發 Chaos Mesh® 的控制平面。如果你缺乏基礎知識,要想對 Chaos Mesh® 的架構有巨集觀上的認識,請參閱文末尾註中的連結。

本文試驗程式碼位於 mayocream/chaos-mesh-controlpanel-demo 倉庫。

如何製造混沌

Chaos Mesh® 是在 Kubernetes 上實施混沌工程的利器,那它是如何工作的呢?

特權模式

上面提到 Chaos Mesh® 執行 Kubernetes 特權容器來製造故障。Daemon Set 方式執行的 Pod 授權了容器執行時的權能字(Capabilities)。 apiVersion:apps/v1kind:DaemonSetspec: 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}}

這些 Linux 權能字用於授予容器特權,以建立和訪問 /dev/fuse FUSE 管道[2](FUSE 是 Linux 使用者空間檔案系統介面,它使無特權的使用者能夠無需編輯核心程式碼而建立自己的檔案系統)。 參閱 #1109 Pull Request,Daemon Set 程式使用 CGO 呼叫 Linux makedev 函式建立 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 onefunc 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)) }}

同時在 #1103 PR 中,Chaos Daemon 預設啟用特權模式,即容器的 securityContext 中設定 privileged: true。

殺死 Pod

PodKill、PodFailure、ContainerKill 都歸屬於 PodChaos 類別下,PodKill 是隨機殺死 Pod。 PodKill 的具體實現其實是通過呼叫 API Server 傳送 Kill 命令。 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} GracePeriodSeconds 引數適用於 K8s 強制終止 Pod。例如在需要快速刪除 Pod 時,我們使用 kubectl delete pod --grace-period=0 --force 命令。 PodFailure 是通過 Patch Pod 物件資源,用錯誤的映象替換 Pod 中的映象。Chaos 只修改了 containers 和 initContainers 的 image 欄位,這也是因為 Pod 大部分欄位是無法更改的,詳情可以參閱 Pod 更新與替換。 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} 預設用於引發故障的容器映象是 gcr.io/google-containers/pause:latest, 如果在國內環境使用,大概率會水土不服,可以將 gcr.io 替換為 registry.aliyuncs.com。 ContainerKill 不同於 PodKill 和 PodFailure,後兩個都是通過 K8s API Server 控制 Pod 生命週期,而 ContainerKill 是通過執行在叢集 Node 上的 Chaos Daemon 程式操作完成。具體來說,ContainerKill 通過 Chaos Controller Manager 執行客戶端向 Chaos Daemon 發起 grpc 呼叫。 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} 向 Chaos Daemon 傳送命令時會依據 Pod 資訊建立對應的客戶端,例如要控制某個 Node 上的 Pod,會獲取該 Pod 所在 Node 的 ClusterIP,以建立客戶端。如果 TLS 證書配置存在,Controller Manager 會為客戶端新增 TLS 證書。 Chaos Daemon 在啟動時如果有 TLS 證書,會附加證書以啟用 grpcs。TLS 校驗配置 RequireAndVerifyClientCert 表示啟用雙向 TLS 認證(mTLS)。 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 提供了以下 grpc 呼叫介面: // 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)}

網路故障

從最初的 #41 PR 中,可以清晰地瞭解到,Chaos Mesh® 的網路錯誤注入是通過呼叫 pbClient.SetNetem 方法,將引數封裝成請求,交給 Node 上的 Chaos Daemon 處理的。 (注:這是 2019 年初期的程式碼,隨著專案發展,程式碼中的函式已分散到不同的檔案中) 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} 同時在 pkg/chaosdaemon 包中,我們能看到 Chaos Daemon 處理請求的方法。 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 namespacefunc 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} 最終使用 vishvananda/netlink 庫操作 Linux 網路介面來完成工作。 這裡能夠知道,NetworkChaos 混沌型別,操作了Linux 宿主機網路來製造混沌,包含 iptables、ipset 等工具。 在 Chaos Daemon 的 Dockerfile 中,可以看到其依賴的 Linux 工具鏈: 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/*

壓力測試

StressChaos 型別的混沌也是由 Chaos Daemon 實施的,Controller Manager 計算好規則後就將任務下發到具體的 Daemon 上。拼裝的引數如下,這些引數會組合成命令執行的引數,附加到 stress-ng 命令後執行[3]。 // Normalize the stressors to comply with stress-ngfunc (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} Chaos Daemon 服務端處理函式中呼叫 Go 官方包 os/exec 執行命令,再大段貼程式碼就沒有意思了,具體可以閱讀 pkg/chaosdaemon/stress_server_linux.go 檔案。同名檔案還有以 darwin 結尾的,推測是為了在 macOS 上開發除錯方便。 程式碼中使用 shirou/gopsutil 包獲取 PID 程序狀態,並讀取了 stdout、stderr 等標準輸出,這種處理模式我在 hashicorp/go-plugin 見過,go-plugin 在這方面做得更加優秀。我的另一篇文章 Dkron 原始碼分析中提到了它[4]。

IO 注入

開篇就提到了 Chaos Mesh® 使用了特權容器,用於掛載宿主機上的 FUSE 裝置 /dev/fuse。 看到這裡,我鐵定以為 Chaos Mesh® 是使用 Mutating 准入控制器進行 Sidecar 容器的注入,掛載 FUSE 裝置,或修改 Pod Volumes Mount 等配置,然後它的實現方式與直觀上不同。 仔細看了 #826 PR,這個 PR 引入了新的 IOChaos 的實現,避免使用 Sidecar 注入的方式,而採用 Chaos Daemon 直接通過 runc 容器底層命令操作 Linux 名稱空間,執行用 Rust 開發的 chaos-mesh/toda FUSE 程式(使用 JSON-RPC 2.0 協議通訊)進行容器 IO 混沌注入。 關注新的 IOChaos 實現,它不會修改 Pod 資源,IOChaos 混沌實驗定義被建立時,針對選擇器(selector 欄位)篩選出的每一個 Pod,對應的一個 PodIoChaos 資源會被建立,PodIoChaos 的屬主引用(Owner Reference)為該 Pod。PodIoChaos 同時會被新增上一組 Finalizers,用於在被刪除前釋放 PodIoChaos 資源。 // Apply implements the reconciler.InnerReconciler.Applyfunc (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} 在 PodIoChaos 資源的控制器中,Controller Manager 會將資源封裝成引數,呼叫 Chaos Daemon 介面進行實際處理。 // Apply flushes io configuration on podfunc (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} 在 Chaos Daemon 中處理 IOChaos 的程式碼檔案 pkg/chaosdaemon/iochaos_server.go 中,容器需要被注入一個 FUSE 程式,通過 #2305 Issue,可以瞭解到執行了 /usr/local/bin/nsexec -l -p proc/119186/ns/pid -m proc/119186/ns/mnt -- usr/local/bin/toda --path tmp --verbose info 命令,以在特定的 Linux 名稱空間(Namespace)下執行 toda 程式,即與 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) } ... // 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 } ...} 下面這段程式碼最終構建了執行的命令,而這些命令正是 runc 底層的 Namespace 隔離實現[5]: // GetNsPath returns corresponding namespace pathfunc GetNsPath(pid uint32, typ NsType) string { return fmt.Sprintf("%s/%d/ns/%s", DefaultProcPrefix, pid, string(typ))}// SetNS sets the namespace of the processfunc (b *ProcessBuilder) SetNS(pid uint32, typ NsType) *ProcessBuilder { return b.SetNSOpt([]nsOption{{ Typ: typ, Path: GetNsPath(pid, typ), }})}// Build builds the processfunc (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 } ...}

控制平面

Chaos Mesh® 是一個開源的混沌工程系統,以 Apache 2.0 協議開源,經過以上分析知道它的能力豐富,並且它的生態良好,維護團隊圍繞混沌系統研發了使用者態檔案系統(FUSE)chaos-mesh/toda 、CoreDNS 混沌外掛 chaos-mesh/k8s_dns_chaos、基於 BPF 的核心錯誤注入 chaos-mesh/bpfki 等。 下述如果我想要建立一個面向終端使用者的混沌工程平臺,在服務端實現的程式碼應該是怎樣的。示例僅為一種實踐,並不代表最佳實踐,如果想看 Real World 平臺的開發實踐的話,可以參考 Chaos Mesh® 官方的 Dashboard,內部使用了 uber-go/fx 依賴注入框架和 controller runtime 的 manager 模式。

職能劃分

modb_20211027_53fe590e-36bd-11ec-964e-38f9d3cd240d.png 這裡標題雖是控制平面,但檢視上述 Chaos Mesh® 工作流程圖,其實我們需要做的只是實現一個將 YAML 下發到 Kubernetes API 的伺服器,複雜的規則校驗、規則下發到 Chaos Daemon 的行為是由 Chaos Controller Manager 完成的。想要結合自己的平臺使用,只需要對接 CRD 資源建立的過程就足夠了。 我們來看一下 PingCAP 官方給出的示例: 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® 已經提供了所有的 CRD 資源定義對應的 API,我們使用 Kubernetes API Machinery SIG 開發的 controller-runtime 來簡化與 Kubernetes API 的互動。

實施混沌

例如我們想通過程式呼叫的方式,建立一個 PodKill 資源,該資源被髮送到 Kubernetes API Server 後,會經由 Chaos Controller Manager 的 Validating 准入控制器,進行資料校驗,若資料格式驗證失敗,會在建立時返回錯誤。具體引數可以查閱官方文件使用 YAML 配置檔案建立實驗。 NewClient 建立了一個 K8s API Client,可以參考客戶端建立示例。 package mainimport ( "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} 執行程式的日誌輸出為: 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 通過 kubectl 檢視 PodKill 資源的狀態: $ k describe podchaos.chaos-mesh.org -n dev podkillvjn77Name: podkillvjn77Namespace: devLabels: <none>Annotations: <none>API Version: chaos-mesh.org/v1alpha1Kind: PodChaosMetadata: 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-04918d89fd0bSpec: Action: pod-kill Grace Period: 0 Mode: one Selector: Label Selectors: app: nginx Namespaces: devStatus: 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: RunEvents: 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 控制面還自然而然要有查詢和獲取 Chaos 資源的功能,方便平臺使用者檢視到所有的混沌試驗的實施狀態,對其進行管理。當然,這裡無非是呼叫 REST API 傳送 Get/List 請求,但在實踐上細節需要留意,敝司就發生過 Controller 每次請求全量的資源資料,造成 K8s API Server 的負載增高。 這裡十分推薦閱讀 クライアントの使い方,這篇 controller runtime 使用教程,提到了很細節的地方。例如 controller runtime 預設會從多個位置讀取 kubeconfig,flag、環境變數、再是自動掛載在 Pod 中的 Service Account,armosec/kubescape #21 PR 也是利用了該特性。這篇教程還包括瞭如何分頁、更新、覆蓋物件等常用的操作,我目前還沒有看到有哪篇中文、英文教程有這麼詳細。 Get/List 請求示例: ``` package controlpanelimport ( "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 } ``` 示例中使用了 manager,該模式下會啟用 cache 機制,避免重複獲取大量資料。

modb_20211027_540c8678-36bd-11ec-964e-38f9d3cd240d.png * 獲取 Pod

  • 初次獲取全量資料(List)

  • Watch 資料變化時更新快取

混沌編排

就如同 CRI 容器執行時提供了強大的底層隔離能力,能夠支撐容器的穩定執行,而想要更大規模、更復雜的場景就需要容器編排一樣,Chaos Mesh® 提供了 Schedule 和 Workflow 功能。Schedule 能夠根據設定的 Cron 時間定時、間隔地觸發故障,Workflow 能像 Argo Workflow 一樣編排多個故障試驗。

當然,Chaos Controller Manager 替我們做了大部分工作,控制面所需要的還是管理這些 YAML 資源,唯一需要考慮的是要給使用者提供怎樣的功能。

平臺功能

modb_20211027_541d2bea-36bd-11ec-964e-38f9d3cd240d.png 參考 Chaos Mesh® Dashboard,我們需要考慮平臺該提供哪些功能給終端使用者。

可能的平臺功能點:

  • 混沌注入

  • Pod 崩潰

  • 網路故障

  • 負載測試

  • IO 故障

  • 事件跟蹤

  • 關聯告警

  • 時序遙測

參閱資料 本文既是為公司引入新技術進行試探,同時也是自我學習的記錄,此前接觸的學習材料及撰寫本文時查閱的資料,較為優秀的部分列在這裡,以便快速查閱。

  • controller-runtime原始碼分析

  • つくって學ぶKubebuilder(日文教程)

  • Kubebuilder Book / 中文版

  • kube-controller-manager原始碼分析(三)之 Informer機制

  • kubebuilder2.0學習筆記——進階使用

  • client-go和golang原始碼中的技巧

  • Chaos Mesh - 讓應用跟混沌在 Kubernetes 上共舞

  • 自制檔案系統 —— 02 開發者的福音,FUSE 檔案系統

  • 系統壓力測試工具-stress-ng

  • Dkron 原始碼分析

  • RunC 原始碼通讀指南之 NameSpace