Articles

Transfer deskryptorów plików przez gniazda domeny Unix

aktualizacja 12/31/2020: Jeśli korzystasz z nowszego jądra (Linux 5.6+), większość tej złożoności została wyeliminowana dzięki wprowadzeniu nowego wywołania systemowegopidfd_getfd. Więcej informacji można znaleźć w zakładce przesyłanie deskryptorów Pliku bez szwu między procesami z pidfd I pidfd_getfd opublikowanej w dniu 31.12.2020.

wczoraj czytałem fenomenalny artykuł o tym, jak zakłócać wolne uwalnianie usług, które mówią różnymi protokołami i obsługują różne typy żądań (długie sesje TCP / UDP, żądania obejmujące ogromne ilości danych itp.) pracuje na Facebooku.

jedną z technik używanych przez Facebooka jest tzw. „Socket Takeover”.

Socket Takeover umożliwia zerowe przestoje restartu dla Proxygen poprzez równoległe uruchomienie zaktualizowanej instancji, która przejmuje gniazda nasłuchujące, podczas gdy stara instancja przechodzi w fazę wdzięcznego opróżniania. Nowa instancja przejmuje odpowiedzialność za obsługę nowych połączeń i reagowanie na sondy kontroli zdrowia z L4lb Katran. Stare połączenia są obsługiwane przez starszą instancję do końca okresu opróżniania, po czym uruchamia się inny mechanizm (np. ponowne użycie połączenia podrzędnego).

gdy przekazujemy otwarty plik FD ze starego procesu do nowo uruchomionego, zarówno proces przekazujący, jak i odbierający współdzielą ten sam wpis w tabeli plików dla gniazda nasłuchującego i obsługują oddzielne akceptowane połączenia, na których obsługują transakcje na poziomie połączenia. Aby to osiągnąć, wykorzystujemy następujące funkcje jądra Linuksa:

CMSG: funkcja w sendmsg() umożliwia wysyłanie wiadomości sterujących między lokalnymi procesami (powszechnie określane jako dane pomocnicze). Podczas restartu procesów L7LB używamy tego mechanizmu do wysyłania zestawu FDs dla wszystkich aktywnych serwerów nasłuchujących dla każdego VIP (wirtualnego adresu IP usługi) z aktywnej instancji do nowo uruchomionej instancji. Dane te są wymieniane za pomocą sendmsg I recvmsg przez gniazdo domeny uniksowej.

SCM_RIGHTS: ustawiamy tę opcję, aby wysyłać otwarte FD z częścią Danych zawierającą tablicę liczb całkowitych otwartych FD. Po stronie odbiorczej FD zachowują się tak, jakby zostały utworzone za pomocą dup(2).

otrzymałem kilka odpowiedzi na Twitterze od ludzi wyrażających zdumienie, że jest to w ogóle możliwe. Rzeczywiście, jeśli nie jesteś zbyt zaznajomiony z niektórymi funkcjami gniazd domen uniksowych, wspomniany akapit z artykułu może być dość tajemniczy.

przenoszenie gniazd TCP przez gniazdo domeny Unixowej jest w rzeczywistości sprawdzoną metodą implementacji „hot restarts” lub „zero downtime restarts”. Popularne Serwery Proxy, takie jak HAProxy i Envoy, używają bardzo podobnych mechanizmów do drenowania połączeń z jednej instancji serwera proxy do drugiej bez upuszczania żadnych połączeń. Jednak wiele z tych cech nie jest zbyt szeroko znanych.

w tym poście chcę zbadać niektóre cechy gniazd domeny uniksowej, które sprawiają, że jest to odpowiedni kandydat do kilku z tych przypadków użycia, zwłaszcza przenoszenia gniazda (lub dowolnego deskryptora pliku) z jednego procesu do drugiego, gdzie relacja rodzic-dziecko niekoniecznie istnieje między tymi dwoma procesami.

powszechnie wiadomo, że gniazda domeny uniksowej umożliwiają komunikację między procesami na tym samym systemie hosta. Gniazda domen Unix są używane w wielu popularnych systemach: HAProxy, Envoy, Firecracker virtual machine monitor AWS, Kubernetes, Docker i Istio, aby wymienić tylko kilka.

UDS: Krótki podkład

podobnie jak gniazda sieciowe, gniazda domeny Unix obsługują zarówno typy gniazd strumieniowych, jak i datagramowych. Jednak w przeciwieństwie do gniazd sieciowych, które przyjmują adres IP i port jako adres, adres gniazda domeny Unix ma postać nazwy ścieżki. W przeciwieństwie do gniazd sieciowych, wejścia / Wyjścia pomiędzy gniazdami domeny uniksowej nie wymagają operacji na urządzeniu bazowym (co sprawia, że gniazda domeny uniksowej są znacznie szybsze w porównaniu z gniazdami sieciowymi do wykonywania IPC na tym samym hoście).

powiązanie nazwy gniazda domeny uniksowej z bind(2) tworzy plik gniazda o nazwie pathname w systemie plików. Jednak ten plik różni się od zwykłego pliku, który możesz utworzyć.

prosty program go do tworzenia” serwera echo ” nasłuchującego na gnieździe domeny Unix byłby następujący:

jeśli zbudujesz i uruchomisz ten program, można zaobserwować kilka interesujących faktów.

Socket Files !=Normalne pliki

najpierw plik gniazda /tmp/uds.sock jest oznaczony jako gniazdo. Gdy do tej ścieżki zostanie zastosowanastat(), zwraca ona wartość S_IFSOCK w składniku typu pliku w polu st_mode struktury stat.

gdy na liście znajduje sięls –l, Gniazdo domeny UNIX jest wyświetlane z typems w pierwszej kolumnie, podczas gdyls –F dodaje znak równości (=) do ścieżki gniazda.

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

normalne wywołania systemowe działające na plikach nie działają na plikach gniazd: oznacza to, że wywołania systemowe takie jak open(), close(), read() nie mogą być używane na plikach gniazd. Zamiast tego, specyficzne dla gniazda wywołania systemowe, takie jak socket()bind()recv()sendmsg()recvmsg() itp. są używane do pracy z gniazdami domeny Unix.

inną ciekawostką dotyczącą pliku gniazda jest to, że nie jest on usuwany, gdy gniazdo jest zamknięte, ale jest zamykane przez wywołanie:

  • unlink(2) w systemie MacOS
  • remove() lub częściej unlink(2) w systemie Linux

w systemie Linux adres gniazda domeny Unix jest reprezentowany przez następującą strukturę
:

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

w systemie macOS struktura adresu jest następująca:

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

bind(2) nie powiedzie się podczas próby powiązania z istniejącą ścieżką

opcja SO_REUSEPORT pozwala wielu gniazdom sieciowym na dowolnym hoście połączyć się z ten sam adres i port. Pierwsze gniazdo, które spróbuje połączyć się z podanym portem, musi ustawić opcję SO_REUSEPORT, a każde kolejne gniazdo może połączyć się z tym samym portem.

Wsparcie dla SO_REUSEPORT zostało wprowadzone w Linuksie 3.9 i nowszym. Jednak w Linuksie wszystkie gniazda, które chcą mieć ten sam adres i kombinację portów, muszą należeć do procesów, które mają ten sam efektywny UID.

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

jednak nie jest możliwe, aby Dwa gniazda domeny uniksowej były powiązane z tą samą ścieżką.

SOCKETPAIR(2)

funkcja socketpair() tworzy dwa gniazda, które są następnie połączone ze sobą. W pewnym sensie jest to bardzo podobne do pipe, z tym że obsługuje dwukierunkowy transfer danych.

socketpair działa tylko z gniazdami domeny Unix. Zwraca dwa deskryptory plików, które są już ze sobą połączone (więc nie trzeba wykonywać całego socketbindlistenaccept tańczyć, aby skonfigurować Gniazdo nasłuchujące i

socket

connecttańcz, aby utworzyć klienta do gniazda nasłuchującego przed rozpoczęciem przesyłania danych!).

Transfer Danych przez UDS

teraz, gdy ustaliliśmy, że gniazdo domeny uniksowej umożliwia komunikację między dwoma procesami na tym samym hoście, nadszedł czas, aby zbadać, jakiego rodzaju dane mogą być przesyłane przez gniazdo domeny uniksowej.

ponieważ gniazdo domeny uniksowej jest pod wieloma względami podobne do gniazd sieciowych, wszelkie dane, które można zazwyczaj wysyłać przez gniazdo sieciowe, mogą być wysyłane przez gniazdo domeny uniksowej.

ponadto specjalne wywołania systemowesendmsg Irecvmsg umożliwiają wysyłanie specjalnej wiadomości przez gniazdo domeny Unix. Ta wiadomość jest obsługiwana specjalnie przez jądro, które umożliwia przekazywanie otwartych opisów plików od nadawcy do odbiorcy.

deskryptory plików vs Opis pliku

zauważ, że wspomniałem opis pliku, a nie deskryptor pliku. Różnica między nimi jest subtelna i często nie jest dobrze zrozumiana.

deskryptor pliku jest tak naprawdę tylko wskaźnikiem na proces do podstawowej struktury danych jądra zwanej (myląco) opisem pliku. Jądro utrzymuje tabelę wszystkich otwartych opisów plików zwaną tabelą otwartych plików. Jeśli dwa procesy (a i B) spróbują otworzyć ten sam plik, mogą one mieć własne oddzielne deskryptory plików, które wskazują ten sam opis pliku w tabeli otwartych plików.

więc „wysłanie deskryptora pliku” z jednego gniazda domeny unixowej do drugiego za pomocąsendmsg()tak naprawdę oznacza wysłanie odniesienia do opisu pliku. Jeśli proces a miał wysłać deskryptor pliku 0 (fd0) do procesu B, to deskryptor pliku może być odwołany przez liczbę 3 (fd3) w procesie B. będą one jednak odnosić się do tego samego opisu pliku.

proces wysyłający wywołuje sendmsg, aby wysłać deskryptor przez gniazdo domeny Unix. Proces odbierający wywołuje recvmsg, aby odebrać deskryptor na gnieździe domeny Unix.

nawet jeśli proces wysyłający zamknie swój deskryptor pliku odwołujący się do opisu pliku przekazywanego przez sendmsg przed wywołaniem procesu odbierającego recvmsg, opis pliku pozostaje otwarty dla procesu odbierającego. Wysłanie deskryptora zwiększa liczbę referencji opisu o jeden. Jądro usuwa opisy plików ze swojej otwartej tabeli plików tylko wtedy, gdy liczba referencji spadnie do 0.

sendmsg i recvmsg

podpis dla wywołania funkcjisendmsg w Linuksie jest następujący:

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

odpowiednikiemsendmsg jestrecvmsg:

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

specjalna „wiadomość”, którą można przesłać za pomocąsendmsg Gniazdo domeny UNIX jest określone przezmsghdr. Proces, który chce wysłać opis pliku do innego procesu, tworzymsghdr strukturę zawierającą opis do przekazania.

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 członek struktury msghdr, która ma długość msg_controllen, wskazuje na bufor wiadomości o postaci:

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

w POSIX bufor struktur struct cmsghdr z dołączonymi danymi nazywa się danymi pomocniczymi. W Linuksie maksymalny rozmiar bufora na gniazdo można ustawić modyfikując /proc/sys/net/core/optmem_max.

dodatkowy Transfer danych

chociaż istnieje mnóstwo gotchas z takim transferem danych, jeśli jest poprawnie używany, Może to być dość potężny mechanizm do osiągnięcia wielu celów.

w Linuksie istnieją trzy typy „danych pomocniczych”, które mogą być współdzielone między dwoma gniazdami domeny Unix:

  • SCM_RIGHTS
  • SCM_CREDENTIALS
  • SCM_SECURITY

wszystkie trzy formy danych pomocniczych powinny być dostępne tylko przy użyciu makr opisanych poniżej i nigdy bezpośrednio.

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

chociaż nigdy nie miałem potrzeby korzystania z dwóch ostatnich,SCM_RIGHTS jest tym, co mam nadzieję zbadać więcej w tym poście.

SCM_RIGHTS

SCM_RIGHTSumożliwia procesowi wysyłanie lub odbieranie zestawu otwartych deskryptorów plików z innego procesu za pomocą sendmsg.

komponent cmsg_data struktury cmsghdr może zawierać tablicę deskryptorów plików, które proces chce wysłać do innego.

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

proces odbioru wykorzystujerecvmsg do odbioru danych.

książka interfejs programowania Linuksa ma dobry przewodnik programowy na temat korzystania z sendmsgI recvmsg.

SCM_RIGHTS Gotchas

jak wspomniano, istnieje wiele gotchas podczas próby przekazania danych pomocniczych przez gniazda domeny Unix.

trzeba wysłać kilka „prawdziwych” danych wraz z Komunikatem pomocniczym

w Linuksie, co najmniej jeden bajt „prawdziwych danych” jest wymagany do pomyślnego wysłania danych pomocniczych przez gniazdo strumienia domeny Unix.

jednak podczas wysyłania danych pomocniczych przez gniazdo datagramowe domeny Unix w Linuksie nie jest konieczne wysyłanie żadnych towarzyszących mu rzeczywistych danych. To powiedziawszy, aplikacje przenośne powinny również zawierać co najmniej jeden bajt rzeczywistych danych podczas wysyłania danych pomocniczych przez gniazdo datagramowe.

deskryptory plików mogą zostać usunięte

jeśli buforcmsg_data używany do odbierania danych pomocniczych zawierających deskryptory plików jest zbyt mały (lub jest nieobecny), wtedy dane pomocnicze są obcinane (lub odrzucane), a nadmiarowe deskryptory plików są automatycznie zamykane w procesie odbioru.

Jeśli liczba deskryptorów plików odebranych w danych pomocniczych spowoduje, że proces przekroczy limit zasobów RLIMIT_NOFILE, nadmiar deskryptorów plików zostanie automatycznie zamknięty w procesie otrzymującym. 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. Podobnie, może przyjmować wiele wywołańrecvmsg, aby wykorzystać dane wysłane przez pojedyncze wywołaniesendmsg. Ma to poważne i zaskakujące konsekwencje, z których niektóre zostały tutaj zgłoszone.

ograniczenie liczby opisów plików

stała jądra SCM_MAX_FD ( 253 (lub 255 w jądrach przed 2.6.38)) określa ograniczenie liczby deskryptorów plików w tablicy.

próba wysłania tablicy większej niż ten limit powoduje, że sendmsg nie powiedzie się z błędem EINVAL.

kiedy warto przesyłać deskryptory plików?

bardzo konkretny przypadek użycia w świecie rzeczywistym, w którym jest to używane, to przeładowanie proxy bez przestojów.

każdy, kto kiedykolwiek miał do pracy z HAProxy może zaświadczyć, że „zero przestojów Config reloads” nie było tak naprawdę rzeczą przez długi czas. Często do tego celu wykorzystywano mnóstwo hacków w stylu Rube Goldberga.

pod koniec 2017 roku HAProxy 1.8 dostarczono z obsługą bezstratnych przeładowań osiągniętych przez przeniesienie deskryptorów plików gniazda nasłuchującego ze starego procesu HAProxy do nowego. Envoy używa podobnego mechanizmu dla hot restartów, gdzie deskryptory plików są przekazywane przez gniazdo domeny uniksowej.

pod koniec 2018 r.Cloudflare blogował o wykorzystaniu przesyłania deskryptorów plików z Nginx na serwer proxy Go TLS 1.3.

artykuł o tym, jak Facebook osiąga zero przestojów, który skłonił mnie do napisania tego całego postu na blogu, wykorzystuje sztuczkę selfsame CMSG + SCM_RIGHTS, aby przekazać deskryptory plików na żywo z procesu opróżniania do nowo wydanego procesu.

wniosek

przesyłanie deskryptorów plików przez gniazdo domeny uniksowej może okazać się bardzo wydajne, jeśli zostanie użyte poprawnie. Mam nadzieję, że ten post dał ci nieco lepsze zrozumienie gniazd domen uniksowych i funkcji, które umożliwia.

  1. https://www.man7.org/linux/man-pages/man7/unix.7.html
  2. https://blog.cloudflare.com/know-your-scm_rights/
  3. LWN.net ma ciekawy artykuł na temat tworzenia cykli podczas przekazywania opisów plików przez gniazdo domeny Unix i implikacje dla wspaniałego nowego API jądra io_uring. https://lwn.net/Articles/779472/
  4. interfejs programowania Linuksahttps://learning.oreilly.com/library/view/the-linux-programming/9781593272203/
  5. programowanie sieci UNIX: interfejs sieciowy gniazd https://learning.oreilly.com/library/view/the-sockets-networking/0131411551/