Articles

file Descriptor Transfer over Unix Domain Sockets

actualizare 12/31/2020: dacă sunteți pe un nucleu mai nou (Linux 5.6+), o mare parte din această complexitate a fost evitată prin introducerea unui nou apel de sistempidfd_getfd. Vă rugăm să consultați postarea transfer Descriptor de Fișiere fără sudură între procese cu pidfd și pidfd_getfd publicat pe 12/31/2020 pentru mai multe detalii.

ieri, am citit o lucrare fenomenală despre modul în care eliberarea fără întreruperi a serviciilor care vorbesc diferite protocoale și servesc diferite tipuri de solicitări (sesiuni TCP / UDP de lungă durată, cereri care implică bucăți uriașe de date etc.) lucrează la Facebook.

una dintre tehnicile folosite de Facebook este ceea ce ei numesc „preluare Socket”.

Socket Takeover permite repornirea timpului de nefuncționare Zero pentru Proxygen prin rotirea unei instanțe actualizate în paralel care preia prizele de ascultare, în timp ce vechea instanță intră în faza de scurgere grațioasă. Noua instanță își asumă responsabilitatea de a servi noile conexiuni și de a răspunde la sondele de verificare a sănătății de la L4lb Katran. Conexiunile vechi sunt deservite de instanța mai veche până la sfârșitul perioadei de scurgere, după care începe un alt mecanism (de exemplu,reutilizarea conexiunii în aval).

pe măsură ce trecem un FD deschis de la vechiul proces la cel nou Filat, atât procesul de trecere, cât și cel de primire împărtășesc aceeași intrare de tabel de fișiere pentru soclul de ascultare și gestionează conexiuni acceptate separate pe care servesc tranzacții la nivel de conexiune. Folosim următoarele caracteristici ale kernel-ului Linux pentru a realiza acest lucru:

CMSG: o caracteristică din sendmsg() permite trimiterea de mesaje de control între procesele locale (denumite în mod obișnuit date auxiliare). În timpul repornirii proceselor L7LB, folosim acest mecanism pentru a trimite setul de FDS pentru toate prizele active de ascultare pentru fiecare VIP (IP Virtual de serviciu) de la instanța activă la instanța nou rotită. Aceste date sunt schimbate folosindsendmsg șirecvmsg pe un soclu de domeniu UNIX.

SCM_RIGHTS: setăm această opțiune pentru a trimite FDS deschise cu porțiunea de date care conține o matrice întreagă a FDS deschise. Pe partea de primire, aceste FD se comportă ca și cum ar fi fost create cu dup(2).

am primit o serie de replici pe Twitter de la oameni care își exprimă uimirea că acest lucru este chiar posibil. Într-adevăr, dacă nu sunteți foarte familiarizat cu unele dintre caracteristicile soclurilor de domeniu Unix, paragraful menționat mai sus din hârtie ar putea fi destul de inscrutabil.

transferul soclurilor TCP pe un soclu de domeniu Unix este, de fapt, o metodă încercată și testată pentru a implementa „reporniri la cald” sau „reporniri de nefuncționare zero”. Proxy-urile populare precum HAProxy și Envoy folosesc mecanisme foarte similare pentru a scurge conexiunile de la o instanță a proxy-ului la alta, fără a renunța la nicio conexiune. Cu toate acestea, multe dintre aceste caracteristici nu sunt foarte cunoscute.

în această postare, vreau să explorez câteva dintre caracteristicile soclurilor de domeniu Unix care îl fac un candidat potrivit pentru mai multe dintre aceste cazuri de utilizare, în special transferul unui socket (sau orice descriptor de fișier, de altfel) de la un proces la altul în care o relație părinte-copil nu există neapărat între cele două procese.

este cunoscut faptul că soclurile de domeniu Unix permit comunicarea între procesele din același sistem gazdă. Soclurile de domeniu Unix sunt utilizate în multe sisteme populare: HAProxy, Envoy, AWS ‘ s Firecracker virtual Machine monitor, Kubernetes, Docker și Istio pentru a numi câteva.

UDS: un Primer scurt

la fel ca prizele de rețea, prizele de domeniu Unix acceptă atât tipurile de prize de flux, cât și cele de date. Cu toate acestea, spre deosebire de soclurile de rețea care iau o adresă IP și un port ca adresă, o adresă de soclu de domeniu Unix ia forma unui nume de cale. Spre deosebire de prizele de rețea, I/O în prizele de domeniu Unix nu implică operații pe dispozitivul de bază (ceea ce face ca prizele de domeniu Unix să fie mult mai rapide în comparație cu prizele de rețea pentru efectuarea IPC pe aceeași gazdă).

legarea unui nume la un soclu de domeniu Unix cubind(2) creează un fișier socket numit nume de cale în sistemul de fișiere. Cu toate acestea, acest fișier este diferit de orice fișier normal pe care l-ați putea crea.

Un program simplu Go pentru a crea un” server echo ” de ascultare pe un soclu de domeniu Unix ar fi următoarele:

dacă construiți și rulați acest program, pot fi observate câteva fapte interesante.

fișiere Socket != Fișiere normale

În primul rând, fișierul socket/tmp/uds.sock este marcat ca soclu. Când stat() este aplicat acestui nume de cale, returnează valoarea S_IFSOCK în componenta de tip fișier a st_mode câmpul stat structura.

când este listat culs –l, un soclu de domeniu UNIX este afișat cu tipuls în prima coloană, în timp ce unls –F adaugă un semn egal (=) la numele căii socket.

[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#

apelurile normale de sistem care funcționează pe fișiere nu funcționează pe fișiere socket: aceasta înseamnă că apelurile de sistem precumopen(), close(), read() nu pot fi utilizate pe fișiere socket. În schimb, apeluri de sistem specifice socket ca socket()bind()recv()sendmsg()recvmsg() etc. sunt folosite pentru a lucra cu prize de domeniu Unix.

Un alt fapt interesant despre fișierul socket este că acesta este eliminat nu atunci când soclul este închis, ci mai degrabă este închis prin apelare:

  • unlink(2) pe MacOS
  • remove() sau mai frecvent, unlink(2) pe Linux

pe Linux, o adresă de soclu de domeniu Unix este reprezentată de următoarea structură:

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

pe MacOS, structura adresei este următoarea:

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

Bind(2) va eșua atunci când încercați să legați la o cale existentă

SO_REUSEPORTopțiunea Permite mai multor prize de rețea de pe orice gazdă dată să se conecteze la aceeași adresă și portul. Primul soclu care încearcă să se lege de portul dat trebuie să seteze opțiunea SO_REUSEPORT și orice soclu ulterior se poate lega la același port.

suport pentruSO_REUSEPORT a fost introdus în Linux 3.9 și mai sus. Cu toate acestea, pe Linux, toate socketurile care doresc să partajeze aceeași adresă și combinație de porturi trebuie să aparțină proceselor care partajează același UID eficient.

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

cu toate acestea, nu este posibil ca două prize de domeniu Unix să se lege de aceeași cale.

SOCKETPAIR(2)

funcțiasocketpair() creează două prize care sunt apoi conectate împreună. Într-un fel, acest lucru este foarte similar cu pipe, cu excepția faptului că acceptă transferul bidirecțional de date.

socketpair funcționează numai cu prize de domeniu Unix. Returnează doi descriptori de fișiere care sunt deja conectați unul la celălalt (deci nu trebuie să faceți întregul socketbindlistenaccept dans pentru a configura o priză de ascultare și o

socket

connectdans pentru a crea un client la priza de ascultare înainte de a începe pentru a transfera date!).

transfer de date prin UDS

acum că am stabilit că un soclu de domeniu Unix permite comunicarea între două procese pe aceeași gazdă, este timpul să explorăm ce fel de date pot fi transferate printr-un soclu de domeniu Unix.

deoarece un soclu de domeniu Unix este similar cu soclurile de rețea în multe privințe, orice date pe care le-ar putea trimite de obicei printr-un soclu de rețea pot fi trimise printr-un soclu de domeniu Unix.

în plus, apelurile speciale de sistemsendmsg șirecvmsg permit trimiterea unui mesaj special în soclul domeniului Unix. Acest mesaj este gestionat special de kernel, care permite transmiterea descrierilor de fișiere deschise de la expeditor la receptor.

descriptorii de fișiere vs descrierea fișierului

rețineți că am menționat descrierea fișierului și nu descriptorul fișierului. Diferența dintre cele două este subtilă și nu este adesea bine înțeleasă.

un descriptor de fișier este într-adevăr doar un pointer per proces către o structură de date kernel subiacentă numită (confuz) descrierea fișierului. Nucleul menține un tabel cu toate descrierile de fișiere deschise numite tabel de fișiere deschise. Dacă două procese (A și B) încearcă să deschidă același fișier, cele două procese ar putea avea propriile descriptori de fișiere separate, care indică aceeași descriere a fișierului în tabelul open file.

deci, „trimiterea unui descriptor de fișiere” de la un soclu de domeniu Unix la altul cu sendmsg() înseamnă doar trimiterea unei referințe la descrierea fișierului. Dacă procesul A ar trimite descriptorul fișierului 0 (fd0) la procesul B, descriptorul fișierului ar putea fi foarte bine referit de numărul 3 (fd3) în procesul B. Cu toate acestea, se vor referi la aceeași descriere a fișierului.

procesul de trimitere solicităsendmsg pentru a trimite descriptorul în soclul domeniului Unix. Procesul de primire solicită recvmsg pentru a primi descriptorul pe soclul domeniului Unix.

chiar dacă procesul de trimitere își închide Descriptorul de fișier care face referire la descrierea fișierului care este transmisă prinsendmsg înainte ca procesul de primire să apelezerecvmsg, descrierea fișierului rămâne deschisă pentru procesul de primire. Trimiterea unui descriptor incrementează numărul de referință al descrierii cu unul. Nucleul elimină descrierile fișierelor din tabelul de fișiere deschise numai dacă numărul de referință scade la 0.

sendmsg și recvmsg

semnătura pentrusendmsg apel funcție pe Linux este următoarea:

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

omologulsendmsg esterecvmsg:

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

„mesajul” special pe care îl puteți transfera cusendmsg peste un soclu de domeniu UNIX este specificat demsghdr. Procesul care dorește să trimită descrierea fișierului la un alt proces creează o structurămsghdr care conține descrierea care trebuie transmisă.

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

msg_control membru al msghdr structura, care are lungimea msg_controllen, indică un tampon de mesaje de forma:

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

în POSIX, un tampon de structuri cmsghdr Struct cu date anexate se numește date auxiliare. Pe Linux, dimensiunea maximă a tamponului permisă pe soclu poate fi setată modificând /proc/sys/net/core/optmem_max.

transfer de date auxiliare

deși există o mulțime de Gotcha cu un astfel de transfer de date, atunci când este utilizat corect, poate fi un mecanism destul de puternic pentru a atinge o serie de obiective.

pe Linux, există trei astfel de tipuri de „date auxiliare” care pot fi partajate între două prize de domeniu Unix:

  • SCM_RIGHTS
  • SCM_CREDENTIALS
  • SCM_SECURITY

toate cele trei forme de date auxiliare ar trebui accesate numai folosind macrocomenzile descrise mai jos și niciodată direct.

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);

deși nu am avut niciodată nevoie să folosesc ultimele două,SCM_RIGHTS este ceea ce sper să explorez mai mult în acest post.

SCM_RIGHTS

SCM_RIGHTS permite unui proces să trimită sau să primească un set de descriptori de fișiere deschise dintr-un alt proces folosindsendmsg.

componenta cmsg_data a structurii cmsghdr poate conține o serie de descriptori de fișiere pe care un proces dorește să-i trimită altuia.

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

procesul de primire utilizeazărecvmsg pentru a primi datele.

cartea interfața de programare Linux are un ghid programatic bun cu privire la modul de utilizare asendmsg șirecvmsg.

SCM_RIGHTS Gotchas

după cum sa menționat, există un număr de gotchas atunci când încearcă să treacă date auxiliare peste prize de domeniu Unix.

trebuie să trimiteți câteva date „reale” împreună cu mesajul auxiliar

pe Linux, cel puțin un octet de „date reale” este necesar pentru a trimite cu succes date auxiliare printr-un soclu de flux de domeniu Unix.

cu toate acestea, atunci când trimiteți date auxiliare printr-un soclu de datagramă de domeniu Unix pe Linux, nu este necesar să trimiteți date reale însoțitoare. Acestea fiind spuse, aplicațiile portabile ar trebui să includă, de asemenea, cel puțin un octet de date reale atunci când trimiteți date auxiliare printr-un soclu de datagramă.

descriptorii de fișiere pot fi abandonați

dacă tamponulcmsg_data utilizat pentru a primi datele auxiliare care conțin descriptorii de fișiere este prea mic (sau este absent), atunci datele auxiliare sunt trunchiate (sau aruncate) și descriptorii de fișiere în exces sunt închise automat în procesul de primire.

dacă numărul de descriptori de fișiere primiți în datele auxiliare determină ca procesul să depășească limita de resurse RLIMIT_NOFILE, descriptorii de fișiere în exces sunt închise automat în procesul de primire. 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. De asemenea, poate dura mai multe apeluri recvmsg pentru a consuma datele trimise printr-un singur apel sendmsg. Acest lucru are implicații serioase și surprinzătoare, dintre care unele au fost raportate aici.

limita numărului de descrieri de fișiere

Constanta nucleului SCM_MAX_FD ( 253 (sau 255 în nucleele înainte de 2.6.38)) definește o limită a numărului de descriptori de fișiere din matrice.

încercarea de a trimite o matrice mai mare decât această limită determinăsendmsg să eșueze cu eroarea einval.

când este util să transferați descriptori de fișiere?

un caz foarte concret de utilizare din lumea reală în care este utilizat este zero reîncărcări proxy de nefuncționare.

oricine a avut vreodată de a lucra cu HAProxy poate atesta că „zero downtime Config reloads” nu a fost într-adevăr un lucru pentru o lungă perioadă de timp. Adesea, o multitudine de hack-uri Rube Goldberg-esque au fost folosite pentru a realiza acest lucru.

la sfârșitul anului 2017, HAProxy 1.8 livrat cu suport pentru reîncărcări hitless realizate prin transferul descriptorilor de fișiere socket de ascultare de la vechiul proces HAProxy la cel nou. Envoy folosește un mecanism similar pentru repornirile la cald în care descriptorii de fișiere sunt trecuți peste un soclu de domeniu Unix.

la sfârșitul anului 2018, Cloudflare a scris pe blog despre utilizarea transferului descriptorilor de fișiere de la nginx la un proxy Go TLS 1.3.

lucrarea despre modul în care Facebook realizează zero lansări de nefuncționare care m-au determinat să scriu întreaga postare pe blog folosește trucul selfsame CMSG + SCM_RIGHTS pentru a trece descriptorii de fișiere live de la procesul de drenare la procesul recent lansat.

concluzie

transferul descriptorilor de fișiere pe un soclu de domeniu Unix se poate dovedi a fi foarte puternic dacă este utilizat corect. Sper că această postare v-a oferit o înțelegere puțin mai bună a soclurilor de domeniu Unix și a caracteristicilor pe care le permite.

  1. https://www.man7.org/linux/man-pages/man7/unix.7.html
  2. https://blog.cloudflare.com/know-your-scm_rights/
  3. LWN.net are un articol interesant despre crearea de cicluri atunci când trece descrieri de fișiere pe un soclu de domeniu Unix și implicații pentru fabulos nou API kernel io_uring. https://lwn.net/Articles/779472/
  4. interfața de programare Linuxhttps://learning.oreilly.com/library/view/the-linux-programming/9781593272203/
  5. programare de rețea UNIX: Sockets Networking APIhttps://learning.oreilly.com/library/view/the-sockets-networking/0131411551/