Hinweis perev. : Der Autor des Artikels - Erkan Erol, ein Ingenieur von SAP - teilt seine Studie über die Funktionsmechanismen des kubectl exec
Teams mit, die jedem bekannt ist, der mit Kubernetes arbeitet. Er begleitet den gesamten Algorithmus mit Auflistungen des Kubernetes-Quellcodes (und verwandter Projekte), mit denen Sie das Thema so tief wie nötig verstehen können.
Eines Freitags kam ein Kollege auf mich zu und fragte, wie man einen Befehl in pod mit
client-go ausführt. Ich konnte ihm nicht antworten und merkte plötzlich, dass ich nichts über den Arbeitsmechanismus von
kubectl exec
. Ja, ich hatte bestimmte Ideen zu seinem Gerät, war mir jedoch nicht 100% sicher, ob sie korrekt sind, und entschied mich daher, dieses Problem anzugehen. Nachdem ich Blogs, Dokumentation und Quellcode studiert habe, habe ich viele neue Dinge gelernt und in diesem Artikel möchte ich meine Entdeckungen und mein Verständnis teilen. Wenn etwas nicht stimmt, kontaktieren Sie mich bitte auf
Twitter .
Vorbereitung
Um einen Cluster auf einem MacBook zu erstellen, habe ich
ecomm-integration-ballerina / kubernetes-cluster geklont . Dann korrigierte er die IP-Adressen der Knoten in der Kubelet-Konfiguration, da die Standardeinstellungen die
kubectl exec
nicht zuließen. Mehr über den Hauptgrund dafür können Sie hier lesen.
- Jedes Auto = mein MacBook
- Master-IP = 192.168.205.10
- Worker-Host-IP = 192.168.205.11
- API-Server-Port = 6443
Komponenten

- kubectl exec-Prozess : Wenn wir "kubectl exec ..." ausführen, startet der Prozess. Sie können dies auf jedem Computer mit Zugriff auf den K8s-API-Server tun. Hinweis trans .: Weiterhin verwendet der Autor in Konsolenlisten den Kommentar "any machine", was bedeutet, dass nachfolgende Befehle auf solchen Computern mit Zugriff auf Kubernetes ausgeführt werden können.
- API-Server : Eine Komponente auf dem Master, die den Zugriff auf die Kubernetes-API ermöglicht. Dies ist das Frontend für die Steuerebene in Kubernetes.
- kubelet : Ein Agent, der auf jedem Knoten im Cluster ausgeführt wird. Es bietet Container in pod'e.
- Container-Laufzeit ( Container-Laufzeit ): Software, die für den Betrieb von Containern verantwortlich ist. Beispiele: Docker, CRI-O, Containerd ...
- Kernel : Betriebssystemkernel auf dem Arbeitsknoten; verantwortlich für das Prozessmanagement.
- Zielcontainer : Ein Container, der Teil eines Pods ist und auf einem der Arbeitsknoten ausgeführt wird.
Was ich entdeckt habe
1. Client-seitige Aktivität
Erstellen Sie einen Pod im
default
Namespace:
// any machine $ kubectl run exec-test-nginx --image=nginx
Dann führen wir den Befehl exec aus und warten 5000 Sekunden auf weitere Beobachtungen:
// any machine $ kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
Der Kubectl-Prozess wird angezeigt (in unserem Fall mit pid = 8507):
// any machine $ ps -ef |grep kubectl 501 8507 8409 0 7:19PM ttys000 0:00.13 kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
Wenn wir die Netzwerkaktivität des Prozesses überprüfen, stellen wir fest, dass er Verbindungen zum API-Server hat (192.168.205.10.6443):
// any machine $ netstat -atnv |grep 8507 tcp4 0 0 192.168.205.1.51673 192.168.205.10.6443 ESTABLISHED 131072 131768 8507 0 0x0102 0x00000020 tcp4 0 0 192.168.205.1.51672 192.168.205.10.6443 ESTABLISHED 131072 131768 8507 0 0x0102 0x00000028
Schauen wir uns den Code an. Kubectl erstellt eine POST-Anforderung mit der Exec-Subressource und sendet eine REST-Anforderung:
req := restClient.Post(). Resource("pods"). Name(pod.Name). Namespace(pod.Namespace). SubResource("exec") req.VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: p.Command, Stdin: p.Stdin, Stdout: p.Out != nil, Stderr: p.ErrOut != nil, TTY: t.Raw, }, scheme.ParameterCodec) return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
( kubectl / pkg / cmd / exec / exec.go )
2. Aktivität auf der Seite des Masterknotens
Wir können die Anfrage auch auf der Seite des API-Servers beobachten:
handler.go:143] kube-apiserver: POST "/api/v1/namespaces/default/pods/exec-test-nginx-6558988d5-fgxgg/exec" satisfied by gorestful with webservice /api/v1 upgradeaware.go:261] Connecting to backend proxy (intercepting redirects) https://192.168.205.11:10250/exec/default/exec-test-nginx-6558988d5-fgxgg/exec-test-nginx?command=sh&input=1&output=1&tty=1 Headers: map[Connection:[Upgrade] Content-Length:[0] Upgrade:[SPDY/3.1] User-Agent:[kubectl/v1.12.10 (darwin/amd64) kubernetes/e3c1340] X-Forwarded-For:[192.168.205.1] X-Stream-Protocol-Version:[v4.channel.k8s.io v3.channel.k8s.io v2.channel.k8s.io channel.k8s.io]]
Beachten Sie, dass die HTTP-Anforderung eine Protokolländerungsanforderung enthält. Mit SPDY können Sie einzelne stdin / stdout / stderr / spdy-error-Streams über eine einzige TCP-Verbindung multiplexen.Der API-Server empfängt die Anforderung und konvertiert sie in
PodExecOptions
:
( pkg / apis / core / types.go )Um die erforderlichen Aktionen ausführen zu können, muss der API-Server wissen, an welchen Pod er sich wenden muss:
( pkg / registry / core / pod / strategie.go )Endpunktdaten werden natürlich aus Hostinformationen entnommen:
nodeName := types.NodeName(pod.Spec.NodeName) if len(nodeName) == 0 {
( pkg / registry / core / pod / strategie.go )Hurra! Kubelet verfügt jetzt über einen Port (
node.Status.DaemonEndpoints.KubeletEndpoint.Port
), mit dem der API-Server eine Verbindung herstellen kann:
( pkg / kubelet / client / kubelet_client.go )Aus der Dokumentation von Master-Node-Kommunikation> Master zu Cluster> Apiserver zu Kubelet :
Diese Verbindungen werden auf dem HTTPS-Endpunkt von kubelet geschlossen. Standardmäßig überprüft Apiserver das Kubelet-Zertifikat nicht, wodurch die Verbindung für „Intermediary Attacks“ (MITMs) anfällig und für die Arbeit in nicht vertrauenswürdigen und / oder öffentlichen Netzwerken unsicher ist.
Jetzt kennt der API-Server den Endpunkt und stellt eine Verbindung her:
( pkg / registry / core / pod / rest / subresources.go )Mal sehen, was auf dem Masterknoten passiert.
Zuerst ermitteln wir die IP des Arbeitsknotens. In unserem Fall ist dies 192.168.205.11:
// any machine $ kubectl get nodes k8s-node-1 -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME k8s-node-1 Ready <none> 9h v1.15.3 192.168.205.11 <none> Ubuntu 16.04.6 LTS 4.4.0-159-generic docker://17.3.3
Installieren Sie dann den Kubelet-Port (in unserem Fall 10250):
// any machine $ kubectl get nodes k8s-node-1 -o jsonpath='{.status.daemonEndpoints.kubeletEndpoint}' map[Port:10250]
Jetzt ist es Zeit, das Netzwerk zu überprüfen. Gibt es eine Verbindung zum Arbeitsknoten (192.168.205.11)? Es ist da! Wenn Sie den
exec
Prozess
exec
, verschwindet er, sodass ich weiß, dass die Verbindung vom API-Server als Ergebnis des ausgeführten Exec-Befehls hergestellt wurde.
// master node $ netstat -atn |grep 192.168.205.11 tcp 0 0 192.168.205.10:37870 192.168.205.11:10250 ESTABLISHED …

Die Verbindung zwischen kubectl und dem API-Server ist noch offen. Darüber hinaus besteht eine weitere Verbindung zwischen API-Server und Kubelet.
3. Aktivität auf dem Arbeitsknoten
Stellen Sie nun eine Verbindung zum Worker-Knoten her und sehen Sie, was darauf passiert.
Zunächst sehen wir, dass auch die Verbindung damit hergestellt wird (zweite Zeile); 192.168.205.10 ist die IP des Masterknotens:
// worker node $ netstat -atn |grep 10250 tcp6 0 0 :::10250 :::* LISTEN tcp6 0 0 192.168.205.11:10250 192.168.205.10:37870 ESTABLISHED
Was ist mit unserem
sleep
? Hurra, sie ist auch anwesend!
// worker node $ ps -afx ... 31463 ? Sl 0:00 \_ docker-containerd-shim 7d974065bbb3107074ce31c51f5ef40aea8dcd535ae11a7b8f2dd180b8ed583a /var/run/docker/libcontainerd/7d974065bbb3107074ce31c51 31478 pts/0 Ss 0:00 \_ sh 31485 pts/0 S+ 0:00 \_ sleep 5000 …
Aber warte: Wie hat Kubelet das aufgedreht? In kubelet gibt es einen Daemon, der den Zugriff auf die API über den Port für API-Server-Anforderungen ermöglicht:
( pkg / kubelet / server / stream / server.go )Kubelet berechnet den Antwortendpunkt für Ausführungsanforderungen:
func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { if err := validateExecRequest(req); err != nil { return nil, err } token, err := s.cache.Insert(req) if err != nil { return nil, err } return &runtimeapi.ExecResponse{ Url: s.buildURL("exec", token), }, nil }
( pkg / kubelet / server / stream / server.go )Nicht verwirren. Es wird nicht das Ergebnis des Befehls zurückgegeben, sondern der Endpunkt für die Kommunikation:
type ExecResponse struct {
( cri-api / pkg / apis / runtime / v1alpha2 / api.pb.go )Kubelet implementiert die
RuntimeServiceClient
Schnittstelle, die Teil der Container Runtime-Schnittstelle ist
(wir haben zum Beispiel hier mehr darüber geschrieben - ca. Übersetzung) :
Lange Auflistung von Cri-API zu Kubernetes / Kubernetes Es wird nur gRPC verwendet, um eine Methode über die Container-Laufzeitschnittstelle aufzurufen:
type runtimeServiceClient struct { cc *grpc.ClientConn }
( cri-api / pkg / apis / runtime / v1alpha2 / api.pb.go ) func (c *runtimeServiceClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) { out := new(ExecResponse) err := c.cc.Invoke(ctx, "/runtime.v1alpha2.RuntimeService/Exec", in, out, opts...) if err != nil { return nil, err } return out, nil }
( cri-api / pkg / apis / runtime / v1alpha2 / api.pb.go )Die Container Runtime ist für die Implementierung des
RuntimeServiceServer
:
Lange Auflistung von Cri-API zu Kubernetes / Kubernetes 
Wenn ja, sollten wir eine Verbindung zwischen dem Kubelet und der Container-Laufzeit sehen, oder? Lass es uns überprüfen.
Führen Sie diesen Befehl vor und nach dem Befehl exec aus und überprüfen Sie die Unterschiede. In meinem Fall ist der Unterschied folgender:
// worker node $ ss -a -p |grep kubelet ... u_str ESTAB 0 0 * 157937 * 157387 users:(("kubelet",pid=5714,fd=33)) ...
Hmmm ... Eine neue Verbindung über Unix-Sockets zwischen Kubelet (pid = 5714) und etwas Unbekanntem. Was könnte es sein? Das stimmt, das ist Docker (pid = 1186)!
// worker node $ ss -a -p |grep 157387 ... u_str ESTAB 0 0 * 157937 * 157387 users:(("kubelet",pid=5714,fd=33)) u_str ESTAB 0 0 /var/run/docker.sock 157387 * 157937 users:(("dockerd",pid=1186,fd=14)) ...
Wie Sie sich erinnern, ist dies ein Docker-Daemon-Prozess (pid = 1186), der unseren Befehl ausführt:
// worker node $ ps -afx ... 1186 ? Ssl 0:55 /usr/bin/dockerd -H fd:// 17784 ? Sl 0:00 \_ docker-containerd-shim 53a0a08547b2f95986402d7f3b3e78702516244df049ba6c5aa012e81264aa3c /var/run/docker/libcontainerd/53a0a08547b2f95986402d7f3 17801 pts/2 Ss 0:00 \_ sh 17827 pts/2 S+ 0:00 \_ sleep 5000 ...
4. Aktivität in der Container-Laufzeit
Lassen Sie uns den Quellcode von CRI-O untersuchen, um zu verstehen, was passiert. In Docker ist die Logik ähnlich.
Es gibt einen Server, der für die Implementierung des
RuntimeServiceServer
:
( cri-o / server / server.go )
( cri-o / erver / container_exec.go )Am Ende der Kette führt die Container-Laufzeit einen Befehl auf dem Arbeitsknoten aus:
( cri-o / internal / oci / runtime_oci.go )
Schließlich führt der Kernel die folgenden Befehle aus:

Erinnerungen
- API Server kann auch eine Verbindung zu kubelet herstellen.
- Die folgenden Verbindungen bleiben bis zum Ende der interaktiven Exec-Sitzung erhalten:
- zwischen kubectl und api-server;
- zwischen API-Server und Kubectl;
- zwischen Kubelet und Container Laufzeit.
- Kubectl oder API-Server können auf Produktionsknoten nichts ausführen. Kubelet kann starten, aber für diese Aktionen interagiert es auch mit der Container-Laufzeit.
Ressourcen
PS vom Übersetzer
Lesen Sie auch in unserem Blog: