Трохи про сокети, redis і битих яйцях

Працювати в п'ятницю після обіду першого квітня не хочеться — раптом ще техніка утне якусь жарт. Тому вирішив про що-небудь написати.
Не так давно на просторах хабра в одній статті огульно охаяли відразу Unix сокети, mysql, php і redis. Говорити про все в одній статті не будемо, зупинимося на сокетах і трохи на redis.
Отже питання: що швидше Unix — або TCP сокетів?
Питання не варто й виїденого яйця, проте, постійно муссируемый і писати не став би якщо б не опитування в тій самій статті, згідно з яким ледь не половина респондентів вважає, що краще/надійніше/стабільніше використовувати TCP сокетів.
Тим, хто і так обирає AF_UNIX, можна далі не читати.

Почнемо з короткої вичавки з теорії.
Сокет — один з інтерфейсів межпроцессного взаємодії, що дозволяє розробляти клієнт-серверні системи для локального або мережевого використання. Так як ми розглядаємо в порівнянні (з одного боку) Unix сокети, то надалі будемо говорити про IPC в межах однієї машини.
На відміну від іменованих каналів, при використанні сокетів простежується відмінність між клієнтом і сервером. Механізм сокетів дозволяє створювати сервер до якого підключається безліч клієнтів.

Як реалізується взаємодія зі сторони сервера:
— системний виклик socket створює сокет, але цей сокет не може використовуватися спільно з іншими процесами;
— сокет іменується. Для локальних сокетів домену AF_UNIX(AF_LOCAL) адреса буде заданий ім'ям файлу. Мережеві сокети AF_INET іменуються відповідно з їх ip/портом;
— системний виклик listen(int socket, int backlog) формує чергу вхідних підключень. Другий параметр backlog визначає довжину цієї черги;
— ці підключення сервер прийняв за допомогою виклику accept, який створює новий сокет, який відрізняється від іменованого сокета. Цей новий сокет застосовується тільки для взаємодії з даним конкретним клієнтом.

З точки зору клієнта підключення відбувається дещо простіше:
— викликається socket;
— і connect використовуючи в якості адреси іменований сокет серверу.

Зупинимося уважніше на викликint socket(int domain, int type, int protocol) другий параметр якого визначає тип обміну даними використовується з цим сокетом. В нашому порівнянні ми будемо розглядати його можливе значення SOCK_STREAM, що є надійним, впорядкованим двонаправленим потоком байтів. Тобто у розгляді беруть участь сокети виду
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
і
sockfd = socket(AF_INET, SOCK_STREAM, 0);

Структура сокета в домені AF_UNIX проста
struct sockaddr_un {
unsigned char sun_len; /* sockaddr len including null */
sa_family_t sun_family; /* AF_UNIX */
char sun_path[104]; /* path name (gag) */
};

У домені AF_INET дещо складніше:
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};

і на її заповнення ми понесемо додаткові витрати. Зокрема це можуть бути витрати на — ресолвинг (gethostbyname) або з'ясування того з якого боку розбивати яйця (htons).

Також сокети в домені AF_INET, не дивлячись на звернення до localhost «не знають» того, що вони працюють на локальній системі. Тим самим вони не докладають жодних зусиль, щоб обійти механізми мережевого стека для збільшення продуктивності. Таким чином ми оплачуємо» зусилля на перемикання контексту, ACK, TCP управління потоком, маршрутизацію, розбиття великих пакетів і т. п. тобто це «повноцінна TCP робота» незважаючи на те, що ми працюємо на локальному інтерфейсі.

У свою чергу сокети AF_UNIX «усвідомлюють, що вони працюють в межах однієї системи. Вони уникають зусиль на встановлення ip-заголовка, роутинг, розрахунок контрольних сум і т. д. Крім того, раз в домені AF_UNIX файлова система як адресного простору ми отримуємо бонус у вигляді можливості використання прав доступу до файлів і управління доступу до них. Тим самим ми можемо без істотних зусиль обмежувати процесів доступ до сокетам і знову ж таки не несемо витрат на етапи обсепечения безпеки.

Перевіримо теорію на практиці.
Мені лінь писати серверну частину, тому скористаюся тим же redis-server. Його функціонал відмінно для цього підходить і заодно перевіримо справедливими були звинувачення на його адресу. Клієнтські частини набросаєм свої. Будемо виконувати найпростішу команду INCR зі складністю O(1).
Створення сокетів навмисно поміщаємо усередині циклів.
TCP-клієнт:
AF_INET
#include < stdio.h>
#include < stdio.h>
#include <stdlib.h>

#include < sys/types.h>
#include < sys/socket.h>

#include <netdb.h>
#include <netinet/in.h>

#include < string.h>

int main(int argc, char *argv[]) {
int sockfd, portno, n;
struct sockaddr_in serv_addr;
struct hostent *server;

char buffer[256];

if (argc < 4) {
fprintf(stderr,"usage %s hostname port count_req\n", argv[0]);
exit(0);
}

portno = atoi(argv[2]);

int i=0;
int ci = atoi(argv[3]);
for(i; i < ci; i++)
{

sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}

server = gethostbyname(argv[1]);

if (server == NULL) {
fprintf(stderr,"ERROR, no such host\n");
exit(0);
}

bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);
serv_addr.sin_port = htons(portno);

if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR connecting");
exit(1);
}

n = write(sockfd, buffer, strlen(buffer));

if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}

bzero(buffer,256);
n = read(sockfd, buffer, 255);

if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}

printf("%s\n",buffer);
close(sockfd);
}
return 0;
}


UNIX-клієнт:
AF_UNIX
#include < stdio.h>
#include <stdlib.h>

#include < sys/types.h>
#include < sys/socket.h>

#include < sys/un.h>

#include < string.h>

int main(int argc, char *argv[]) {
int sockfd, portno, n;
struct sockaddr_un serv_addr;
struct hostent *server;

char buffer[256];

if (argc < 1) {
fprintf(stderr,"usage %s count_req\n", argv[0]);
exit(0);
}

int i=0;
int ci = atoi(argv[1]);
for(i; i < ci; i++)
{

sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}

bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sun_family = AF_UNIX;
strcpy(serv_addr.sun_path, "/tmp/redis.sock");

if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR connecting");
exit(1);
}

char str[] = "*2\r\n$4\r\nincr\r\n$3\r\nfoo\r\n";
int len = sizeof(str);
bzero(buffer, len);
memcpy ( buffer, str, len );

n = write(sockfd, buffer, strlen(buffer));

if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}

bzero(buffer,256);
n = read(sockfd, buffer, 255);

if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}

printf("%s\n",buffer);
close(sockfd);
}
return 0;
}


Тестуємо з одним клієнтом:
# redis-cli set foo 0 ; time ./redistcp 127.0.0.1 6379 1000000 > /dev/null ; redis-cli get foo
OK
2.108 u 21.991 s 1:13.75 32.6% 9+158k 0+0io 0pf+0w
"1000000"

# redis-cli set foo 0 ; time ./redisunix 1000000 > /dev/null ; redis-cli get foo
OK
0.688u 9.806 s 0:36.90 28.4% 4+151k 0+0io 0pf+0w
"1000000"


І тепер для двадцяти паралелльных клієнтів відправляють 500000 запитів кожен.
для TCP: 6:12.86
# redis-cli info Commandstats
cmdstat_set:calls=1,usec=5,usec_per_call=5.00
cmdstat_incr:calls=10000000,usec=24684314,usec_per_call=2.47

для UNIX: 4:11.23
# redis-cli info Commandstats
cmdstat_set:calls=1,usec=8,usec_per_call=8.00
cmdstat_incr:calls=10000000,usec=22258069,usec_per_call=2.23


Тим самим, в цілому, аргументами на користь TCP сокетів може служити лише мобільність застосування і можливість простого масштабування. Але якщо ж вам потрібна робота в межах однієї машини, то вибір безумовно на користь UNIX-сокетів. Тому вибір між TCP — і UNIX-сокетами — це, в першу чергу, вибір між переносимістю і продуктивністю.

На сим пропоную любити Unix сокети, а питання тупоконечностей залишити жителям Ліліпутією та Блефуску.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.