Articles

Dateideskriptorübertragung über Unix-Domain-Sockets

Update 31.12.2020: Wenn Sie einen neueren Kernel (Linux 5.6+) verwenden, wurde ein Großteil dieser Komplexität durch die Einführung eines neuen Systemaufrufs vermieden pidfd_getfd. Weitere Informationen finden Sie im Beitrag Seamless File Descriptor Transfer Between Processes with pidfd und pidfd_getfd veröffentlicht am 31.12.2020.

Gestern habe ich ein phänomenales Papier darüber gelesen, wie störungsfrei Dienste freigegeben werden, die verschiedene Protokolle sprechen und verschiedene Arten von Anforderungen erfüllen (langlebige TCP / UDP-Sitzungen, Anforderungen mit großen Datenmengen usw.) arbeitet bei Facebook.

Eine der von Facebook verwendeten Techniken ist das, was sie „Socket Takeover“ nennen.

Socket Takeover ermöglicht Zero Downtime-Neustarts für Proxygen, indem eine aktualisierte Instanz parallel hochgefahren wird, die die abhörenden Sockets übernimmt, während die alte Instanz in eine anmutige Entleerungsphase übergeht. Die neue Instanz übernimmt die Verantwortung für die Bereitstellung der neuen Verbindungen und die Reaktion auf Health-Check-Sonden aus dem L4LB Katran. Alte Verbindungen werden von der älteren Instanz bis zum Ende des Zeitraums bedient, wonach ein anderer Mechanismus (z. B. Wiederverwendung der nachgelagerten Verbindung) einsetzt.

Wenn wir eine offene FD vom alten Prozess an den neu gesponnenen übergeben, teilen sich sowohl der weiterleitende als auch der empfangende Prozess denselben Dateitabelleneintrag für den abhörenden Socket und verarbeiten separate akzeptierte Verbindungen, auf denen sie Transaktionen auf Verbindungsebene ausführen. Wir nutzen die folgenden Linux-Kernel-Funktionen, um dies zu erreichen:

CMSG: Eine Funktion in sendmsg() ermöglicht das Senden von Steuerungsnachrichten zwischen lokalen Prozessen (allgemein als Zusatzdaten bezeichnet). Während des Neustarts von L7LB-Prozessen verwenden wir diesen Mechanismus, um den Satz von FDs für alle aktiven Listening-Sockets für jeden VIP (Virtual IP of Service) von der aktiven Instanz an die neu gesponnene Instanz zu senden. Diese Daten werden mit sendmsg und recvmsg über einen UNIX Domain Socket ausgetauscht.

SCM_RIGHTS: Wir setzen diese Option, um offene FDs mit dem Datenteil zu senden, der ein Integer-Array der offenen FDs enthält. Auf der Empfangsseite verhalten sich diese FDs so, als wären sie mit dup(2) erstellt worden.

Ich habe auf Twitter eine Reihe von Antworten von Leuten erhalten, die erstaunt waren, dass dies überhaupt möglich ist. In der Tat, wenn Sie mit einigen der Funktionen von Unix-Domain-Sockets nicht sehr vertraut sind, könnte der oben genannte Absatz aus dem Papier ziemlich unergründlich sein.Das Übertragen von TCP-Sockets über einen Unix-Domain-Socket ist eigentlich eine bewährte Methode, um „Hot-Restarts“ oder „Zero Downtime Restarts“ zu implementieren. Beliebte Proxys wie HAProxy und Envoy verwenden sehr ähnliche Mechanismen, um Verbindungen von einer Instanz des Proxys zu einer anderen zu entleeren, ohne Verbindungen zu löschen. Viele dieser Merkmale sind jedoch nicht sehr bekannt.In diesem Beitrag möchte ich einige der Funktionen von Unix-Domain-Sockets untersuchen, die es zu einem geeigneten Kandidaten für mehrere dieser Anwendungsfälle machen, insbesondere das Übertragen eines Sockets (oder eines beliebigen Dateideskriptors) von einem Prozess zu einem anderen, in dem eine Eltern-Kind-Beziehung nicht unbedingt zwischen den beiden Prozessen besteht.

Es ist allgemein bekannt, dass Unix-Domain-Sockets die Kommunikation zwischen Prozessen auf demselben Hostsystem ermöglichen. Unix-Domain-Sockets werden in vielen gängigen Systemen verwendet: HAProxy, Envoy, AWS Firecracker Virtual Machine Monitor, Kubernetes, Docker und Istio, um nur einige zu nennen.

UDS: Eine kurze Einführung

Wie Netzwerk-Sockets unterstützen Unix-Domain-Sockets sowohl Stream- als auch Datagramm-Socket-Typen. Im Gegensatz zu Netzwerk-Sockets, die eine IP-Adresse und einen Port als Adresse verwenden, hat eine Unix-Domain-Socket-Adresse jedoch die Form eines Pfadnamens. Im Gegensatz zu Netzwerk-Sockets beinhalten E / A über Unix-Domänen-Sockets keine Operationen auf dem zugrunde liegenden Gerät (was Unix-Domänen-Sockets im Vergleich zu Netzwerk-Sockets für die Durchführung von IPC auf demselben Host viel schneller macht).

Durch Binden eines Namens an einen Unix-Domänensocket mit bind(2) wird eine Socket-Datei mit dem Namen pathname im Dateisystem erstellt. Diese Datei unterscheidet sich jedoch von jeder normalen Datei, die Sie möglicherweise erstellen.

Ein einfaches Go-Programm zum Erstellen eines „Echo-Servers“, der auf einem Unix-Domain-Socket lauscht, wäre das folgende:

Wenn Sie dieses Programm erstellen und ausführen, können einige interessante Fakten beobachtet werden.

Socket-Dateien != Normale Dateien

Zunächst wird die Socket-Datei /tmp/uds.sock als Socket markiert. Wenn stat() auf diesen Pfadnamen angewendet wird, wird der Wert S_IFSOCK in der Dateitypkomponente des Felds st_mode der Struktur stat zurückgegeben.

Bei ls –l wird ein UNIX-Domain-Socket mit dem Typ s in der ersten Spalte angezeigt, während ein ls –F ein Gleichheitszeichen (=) an den Socket-Pfadnamen anhängt.

[email protected]:~/uds# ./uds
^C
[email protected]:~/uds# ls -ls /tmp
total 0
0 srwxr-xr-x 1 root root 0 Aug 5 01:45 [email protected]:~/uds# stat /tmp/uds.sock
File: /tmp/uds.sock
Size: 0 Blocks: 0 IO Block: 4096 socket
Device: 71h/113d Inode: 1835567 Links: 1
Access: (0755/srwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2020-08-05 01:45:41.650709000 +0000
Modify: 2020-08-05 01:45:41.650709000 +0000
Change: 2020-08-05 01:45:41.650709000 +0000Birth: [email protected]:~/uds# ls -F /tmp
uds.sock=
[email protected]:~/uds#

Normale Systemaufrufe, die auf Dateien funktionieren, funktionieren nicht auf Socket-Dateien: Dies bedeutet, dass Systemaufrufe wie open(), close(), read() nicht auf Socket-Dateien verwendet werden können. Stattdessen Socket-spezifische Systemaufrufe wie socket()bind()recv()sendmsg()recvmsg() usw. werden verwendet, um mit Unix-Domain-Sockets zu arbeiten.

Eine weitere interessante Tatsache über die Socket-Datei ist, dass sie nicht entfernt wird, wenn der Socket geschlossen wird, sondern durch Aufrufen geschlossen wird:

  • unlink(2) unter macOS
  • remove() oder häufiger unlink(2) unter Linux

Unter Linux wird eine Unix-Domain-Socket-Adresse durch die folgende
-Struktur dargestellt:

struct sockaddr_un {
sa_family_t sun_family; /* Always AF_UNIX */
char sun_path; /* Pathname */
};

Unter macOS lautet die Adressstruktur wie folgt:

struct sockaddr_un {
u_char sun_len;
u_char sun_family;
char sun_path;
};

bind(2) schlägt fehl, wenn versucht wird, an einen vorhandenen Pfad zu binden

Die Option SO_REUSEPORT ermöglicht es mehreren Netzwerk-Sockets auf einem beliebigen Host, sich mit derselben Adresse und demselben Port zu verbinden. Der allererste Socket, der versucht, an den angegebenen Port zu binden, muss die Option SO_REUSEPORT , und jeder nachfolgende Socket kann an denselben Port binden.

Unterstützung für SO_REUSEPORT wurde in Linux 3.9 und höher eingeführt. Unter Linux müssen jedoch alle Sockets, die dieselbe Adress- und Portkombination gemeinsam nutzen möchten, zu Prozessen gehören, die dieselbe effektive UID gemeinsam nutzen.

int fd = socket(domain, socktype, 0);int optval = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
bind(sfd, (struct sockaddr *) &addr, addrlen);

Es ist jedoch nicht möglich, dass zwei Unix-Domain-Sockets an denselben Pfad gebunden werden.

SOCKETPAIR(2)

Die socketpair() Funktion erzeugt zwei Sockets, die dann miteinander verbunden werden. In gewisser Weise ist dies sehr ähnlich zu pipe, außer dass es die bidirektionale Übertragung von Daten unterstützt.

socketpair funktioniert nur mit Unix Domain Sockets. Es werden zwei Dateideskriptoren zurückgegeben, die bereits miteinander verbunden sind (man muss also nicht das Ganze socketbindlistenaccept) , um einen Listening-Socket und einen socketconnect Versuchen Sie, einen Client für den Listening-Socket zu erstellen, bevor Sie mit der Datenübertragung beginnen!).

Datenübertragung über UDS

Nachdem wir nun festgestellt haben, dass ein Unix-Domain-Socket die Kommunikation zwischen zwei Prozessen auf demselben Host ermöglicht, ist es an der Zeit zu untersuchen, welche Art von Daten über einen Unix-Domain-Socket übertragen werden können.

Da ein Unix-Domain-Socket in vielerlei Hinsicht Netzwerk-Sockets ähnelt, können alle Daten, die normalerweise über einen Netzwerk-Socket gesendet werden, über einen Unix-Domain-Socket gesendet werden.

Darüber hinaus erlauben die speziellen Systemaufrufe sendmsg und recvmsgdas Senden einer speziellen Nachricht über den Unix-Domänensocket. Diese Nachricht wird speziell vom Kernel verarbeitet, der es ermöglicht, offene Dateibeschreibungen vom Absender an den Empfänger zu übergeben.

Dateideskriptoren vs Dateibeschreibung

Beachten Sie, dass ich die Dateibeschreibung und nicht den Dateideskriptor erwähnt habe. Der Unterschied zwischen den beiden ist subtil und wird oft nicht gut verstanden.

Ein Dateideskriptor ist eigentlich nur ein Zeiger pro Prozess auf eine zugrunde liegende Kernel-Datenstruktur, die (verwirrenderweise) die Dateibeschreibung genannt wird. Der Kernel verwaltet eine Tabelle aller offenen Dateibeschreibungen, die open file table genannt wird. Wenn zwei Prozesse (A und B) versuchen, dieselbe Datei zu öffnen, verfügen die beiden Prozesse möglicherweise über eigene Dateideskriptoren, die auf dieselbe Dateibeschreibung in der Tabelle geöffnete Datei verweisen.

Also „Senden eines Dateideskriptors“ von ein Unix-Domain-Socket zu einem anderen mit sendmsg() bedeutet wirklich nur, einen Verweis auf die Dateibeschreibung zu senden. Wenn Prozess A den Dateideskriptor 0 (fd0) an Prozess B sendet, könnte der Dateideskriptor sehr wohl durch die Nummer 3 (fd3) in Prozess B referenziert werden.

Der sendende Prozess ruft sendmsg auf, um den Deskriptor über den Unix-Domänensocket zu senden. Der empfangende Prozess ruft recvmsg auf, um den Deskriptor auf dem Unix-Domänensocket zu empfangen.

Selbst wenn der sendende Prozess seinen Dateideskriptor schließt, der auf die über sendmsg übergebene Dateibeschreibung verweist, bevor der empfangende Prozess recvmsg aufruft, bleibt die Dateibeschreibung für den empfangenden Prozess geöffnet. Das Senden eines Deskriptors erhöht den Referenzzähler der Beschreibung um eins. Der Kernel entfernt Dateibeschreibungen nur dann aus seiner open File-Tabelle, wenn der Referenzzähler auf 0 fällt.

sendmsg und recvmsg

Die Signatur für den sendmsg Funktionsaufruf unter Linux lautet wie folgt:

ssize_t sendmsg(
int socket,
const struct msghdr *message,
int flags
);

Das Gegenstück zu sendmsg ist recvmsg:

ssize_t recvmsg(
int sockfd,
const struct msghdr *msg,
int flags
);

Die spezielle „Nachricht“, die man mit sendmsg über einen Unix-Domain-Socket wird durch die msghdr angegeben. Der Prozess, der die Dateibeschreibung an einen anderen Prozess senden möchte, erstellt eine msghdr Struktur, die die zu übergebende Beschreibung enthält.

struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
int msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
socklen_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};

Das msg_control Mitglied der msghdr Struktur, die die Länge msg_controllen hat, zeigt auf einen Puffer von Nachrichten der Form:

struct cmsghdr {
socklen_t cmsg_len; /* data byte count, including header */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by */
unsigned char cmsg_data;};

In POSIX wird ein Puffer von struct cmsghdr-Strukturen mit angehängten Daten als Zusatzdaten bezeichnet. Unter Linux kann die maximal zulässige Puffergröße pro Socket durch Ändern von /proc/sys/net/core/optmem_max .

Zusätzliche Datenübertragung

Obwohl es bei einer solchen Datenübertragung eine Vielzahl von Fallstricken gibt, kann sie bei richtiger Verwendung ein ziemlich leistungsfähiger Mechanismus sein, um eine Reihe von Zielen zu erreichen.

Unter Linux gibt es drei solche Arten von „Zusatzdaten“, die zwischen zwei Unix-Domain-Sockets geteilt werden können:

  • SCM_RIGHTS
  • SCM_CREDENTIALS
  • SCM_SECURITY

Auf alle drei Arten von Zusatzdaten sollte nur mit den unten beschriebenen Makros und niemals direkt zugegriffen werden.

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
size_t CMSG_ALIGN(size_t length);
size_t CMSG_SPACE(size_t length);
size_t CMSG_LEN(size_t length);
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

Obwohl ich die beiden letzteren noch nie verwenden musste, SCM_RIGHTS hoffe ich, in diesem Beitrag mehr zu erfahren.

SCM_RIGHTS

SCM_RIGHTS ermöglicht es einem Prozess, einen Satz geöffneter Dateideskriptoren von einem anderen Prozess mit sendmsg zu senden oder zu empfangen.

Die cmsg_data-Komponente der cmsghdr-Struktur kann ein Array der Dateideskriptoren enthalten, die ein Prozess an einen anderen senden möchte.

struct cmsghdr {
socklen_t cmsg_len; /* data byte count, including header */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by */
unsigned char cmsg_data;};

Der empfangende Prozess verwendet recvmsg, um die Daten zu empfangen.

Das Buch The Linux Programming Interface enthält eine gute programmatische Anleitung zur Verwendung von sendmsg und recvmsg.

SCM_RIGHTS Fallstricke

Wie bereits erwähnt, gibt es eine Reihe von Fallstricken, wenn versucht wird, zusätzliche Daten über Unix-Domain-Sockets zu übergeben.

Sie müssen einige „echte“ Daten zusammen mit der Hilfsnachricht senden

Unter Linux ist mindestens ein Byte „echter Daten“ erforderlich, um Hilfsdaten erfolgreich über einen Unix-Domänenstrom-Socket zu senden.

Beim Senden von Zusatzdaten über einen Unix-Domänen-Datagramm-Socket unter Linux ist es jedoch nicht erforderlich, begleitende reale Daten zu senden. Portable Anwendungen sollten jedoch auch mindestens ein Byte realer Daten enthalten, wenn Zusatzdaten über einen Datagramm-Socket gesendet werden.

Dateideskriptoren können gelöscht werden

Wenn der Puffer cmsg_data, der zum Empfangen der Zusatzdaten verwendet wird, die die Dateideskriptoren enthalten, zu klein ist (oder fehlt), werden die Zusatzdaten abgeschnitten (oder verworfen) und die überschüssigen Dateideskriptoren werden beim Empfangsprozess automatisch geschlossen.

Wenn die Anzahl der in den Nebendaten empfangenen Dateideskriptoren dazu führt, dass der Prozess sein RLIMIT_NOFILE-Ressourcenlimit überschreitet, werden die überschüssigen Dateideskriptoren im empfangenden Prozess automatisch geschlossen. One cannot split the list over multiple recvmsg calls.

recvmsg quirks

sendmsg and recvmsg act similar to send and recv system calls, in that there isn’t a 1:1 mapping between every send call and every recv call.

A single recvmsg call can read data from multiple sendmsg calls. Ebenso können mehrere recvmsg -Aufrufe erforderlich sein, um die über einen einzelnen sendmsg -Aufruf gesendeten Daten zu verbrauchen. Dies hat schwerwiegende und überraschende Auswirkungen, von denen einige hier berichtet wurden.

Begrenzung der Anzahl der Dateibeschreibungen

Die Kernelkonstante SCM_MAX_FD ( 253 (oder 255 in Kerneln vor 2.6.38)) definiert eine Begrenzung der Anzahl der Dateideskriptoren im Array.

Der Versuch, ein Array zu senden, das größer als dieser Grenzwert ist, führt dazu, dass sendmsg mit dem Fehler EINVAL fehlschlägt.

Wann ist es sinnvoll, Dateideskriptoren zu übertragen?

Ein sehr konkreter Anwendungsfall in der realen Welt, in dem dies verwendet wird, sind Proxy-Reloads ohne Ausfallzeiten.

Jeder, der jemals mit HAProxy arbeiten musste, kann bestätigen, dass „Zero Downtime config reloads“ lange Zeit keine wirkliche Sache war. Oft wurde eine Vielzahl von Rube Goldberg-artigen Hacks verwendet, um dies zu erreichen.

Ende 2017 wurde HAProxy 1.8 mit Unterstützung für Hitless Reloads ausgeliefert, die durch die Übertragung der abhörenden Socket-Dateideskriptoren vom alten HAProxy-Prozess auf den neuen erreicht wurden. Envoy verwendet einen ähnlichen Mechanismus für Hot-Neustarts, bei denen Dateideskriptoren über einen Unix-Domänensocket übergeben werden.

Ende 2018 bloggte Cloudflare über die Verwendung der Übertragung von Dateideskriptoren von nginx auf einen Go TLS 1.3-Proxy.

Das Papier, wie Facebook Zero Downtime Releases erreicht, das mich dazu veranlasst hat, diesen gesamten Blogbeitrag zu schreiben, verwendet den gleichen CMSG + SCM_RIGHTS-Trick, um Live-Dateideskriptoren vom ursprünglichen Prozess an den neu veröffentlichten Prozess zu übergeben.

Fazit

Das Übertragen von Dateideskriptoren über einen Unix-Domain-Socket kann sich bei korrekter Verwendung als sehr leistungsfähig erweisen. Ich hoffe, dieser Beitrag hat Ihnen ein etwas besseres Verständnis der Unix-Domain-Sockets und der damit verbundenen Funktionen vermittelt.

  1. https://www.man7.org/linux/man-pages/man7/unix.7.html
  2. https://blog.cloudflare.com/know-your-scm_rights/
  3. LWN.net hat einen interessanten Artikel über das Erstellen von Zyklen beim Übergeben von Dateibeschreibungen über einen Unix-Domänen-Socket und Auswirkungen auf die fabelhafte neue io_id-Kernel-API. https://lwn.net/Articles/779472/
  4. Die Linux-Programmierschnittstelle https://learning.oreilly.com/library/view/the-linux-programming/9781593272203/
  5. UNIX-Netzwerkprogrammierung: Die Sockets-Netzwerk-API https://learning.oreilly.com/library/view/the-sockets-networking/0131411551/