2 слова о серверах

September 16th, 2011 No comments

Сегодня я хочу поделиться технологией создания масштабируемого сервера на основе linux pthreads и epoll.

Общий принцип программы прост как паровоз: предварительно созданный пул потоков, обрабатывает входящие соединения в порядке живой очереди.

 

Теперь подробнее:

создаём слушающий сокет, причём настоятельно рекомендую использовать опции SO_REUSEADDR и TCP_NODELAY. Да, ещё рекомендую явно указать линжер в нули.

После этого – время создавать потоки. Каждому создаваемому потоку – передаём параметром сокет созданный ранее.

 

pthread_create(&th[t_cnt], &ta,(void*(*)(void *)) server_thread,(void *)&sock)

server_thread – это указатель на функцию потока
sock – это и есть указатель на серверный сокет созданный в начале.

 

Вот как-то так.
Теперь самое главное – функция потока обрабатывающая соединения:

 
void *server_thread(void *fd) {
int *sock = (int *)fd;
int localSock = *sock;

Так преобразовываем поинтер в сокет. Ну, на всякий случай, если кто не знает.

 

struct epoll_event ev, *events;
ep_fd = epoll_create(num_sockets_per_sender);
events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * epoll_room_in_receiver);
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP | EPOLLET;
ev.data.fd = localSock;
epoll_ctl(ep_fd, EPOLL_CTL_ADD,localSock, &ev);

 

Создаём структуру для еполла. Вообще принцип epoll – прост. В структуру записываются сокеты, на которых отслеживаются изменения. Т.е. если в какой-то сокет прошла запись клиентом – ядро (epoll обслуживается ядром Linux) – подаёт сигнал, который вытаскивает программу из ожидания в функции epoll_wait.

Вот как-то так:

while (1) {
int num_fds = epoll_wait(ep_fd, events, num_sockets_per_sender, -1);
.........
};

 

Сиё работает крайне просто, и является абсолютно thread-safe.
Все потоки которые вы создали, заваливаются на epoll_wait, и ждут когда в ep_fd появятся изменения – когда подключится новый клиент, или напишет чего-нибудь кто-то из уже подключенных клиентов.

Ну а дальше – совсем просто. Как только что-то появляется (в количестве в пределах 3 параметра функции epoll_wait) – функция возвращает количество евентов, ну и структуру с сокетами в которых произошли эвенты, и описания происшедших эвентов.

Т.е. например:

if (events.events & (EPOLLHUP | EPOLLERR)) {
fputs("epoll: EPOLLERR", stderr);
close(events.data.fd);
continue;
};

 

В общем-то просто и понятно.
В этом цикле и должна происходить обработка соединений – чтение, запись, закрытие сокетов.

Самое важное в этом процессе – добавлять новые сокеты в структуру epoll.

 

if (events.data.fd == localSock) {
struct sockaddr remote_addr;
socklen_t addr_size = sizeof(remote_addr);
 
int connection = accept(localSock, &remote_addr, &addr_size);
if (connection == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
serr("accept");
}
continue;
};
fcntl(connection, F_SETFL,O_NONBLOCK | fcntl(connection, F_GETFL, 0));
ev.data.fd = connection;
epoll_ctl(ep_fd, EPOLL_CTL_ADD, connection, &ev);
continue;
};

 

Идея проста – если событие произошло на серверном сокете – это означает только одно – появился новый клиент. Т.е. надо принимать коннект и добавлять в структуру epoll для дальнейшего мониторинга.

Ну и двигаясь к концу – пара общих советов, которые вы и так наверняка знаете:
При работе с еполлом – используйте не блокирующиеся сокеты. Это показано выше, но обращу отдельное внимание.

fcntl(connection, F_SETFL,O_NONBLOCK | fcntl(connection, F_GETFL, 0));

При работе с многопоточными приложениями, не надо бояться использовать потоки. В некоторых случаях потоки вообще единственно приемлемое решение.

Мутексы. Мутексы это основа многопоточности. Однако, злоупотребляя мутексами – можно свести на ноль все преимущества многопоточной системы.

Да, два слова о производительности:
Сервер, кусочки кода которого я тут приводил, продемонстрировал примерно вот такие результаты: при 900 обращениях в секунду, полная обработка 1 соединения занимает примерно 27 миллисекунд. Со всей математикой, регекспами, обсчётами, массивами и даже поиском по огромным бинарным деревьям.
Сервер – Dual Quad Core Xeon 2.4Ghz, x64 Debian, 24GB ram.

Из дополнительных параметров компиляции – только -msse3 (только если у вас современный Intel), -O3 (общий набор оптимизации компилятора g++).

Растягивание железного рейда

September 16th, 2011 No comments

Итак,  дано следующее:

  • raid-контроллер LSI, скажем LSI 9260-8i или любой другой контроллер LSI.
  • raid6 на 20 дисков
  • 12 новых HDD
  • среднесуточная нагрузка – 1300 IOPS на чтение, 135 IOPS на запись.

Делаем следующе:

  1. Удостоверяемся, что у нас есть свежий бэкап всего находящегося на рейде, и располагается этот бэкап не на этом рейде.
  2. Убеждаемся, что рейд который мы хотим растянуть находится в оптимальном состоянии:
    MegaCli -LDInfo -LALL -a0 

    В выдаче ищем вот это:
    State               : Optimal

  3. Вставляем в сервер новые HDD.
  4. Получаем список дисков сервера:
    MegaCli64 -PDList -a0
    смотрим на диски в статусе Unconfigured Good, и записываем их Enclosure Device ID и Slot Number.
  5. Запускаем расширение рейда:
    MegaCli -LDRecon -Start -r6 -Add -Physdrv[EnclosureID1:SlotNumber1,EnclosureID2:SlotNumber2,EnclosureID3:SlotNumber3] -LRAID -aController
    с реальными параметрами это будет выглядеть примерно так:
    MegaCli -LDRecon -Start -r6 -Add -Physdrv[21:17,21:18,21:19] -L0 -a0
  6. C момента запуска, категорически не рекомендуется что-либо делать с сервером примерно 1 неделю. При наличии нагрузки на рейде, именно столько займет растягивание рейда и его последующая инициализация.

Все время работы с рейдом, он будет полностью доступен и теоритически ваши пользователи даже не заметят происходящих изменений. Однако:

  • Рейд будет принудительно переведен в режим Direct-IO, кэширования чтения со стороны контроллера не будет.
  • Рейд будет принудительно переведен в режим Write-Through, кэширования записи со стороны контроллера не будет.
  • Средний IOwait поднимется на 5-50 миллисекунд, в зависимости от приоритета реконструкции и инициализиации.

Также следует обратить внимание на еще пару системных нюансов:

  • Если вы хотите изменить приоритет реконструкции или инициализации, сделать это вы должны ДО начала процесса растягивания рейда.
  • В существующих драйверах и прошивках LSI 92хх есть баг: по завершении процесса добавления новых дисков вы никак не заставите систему увидеть новое пространство. Для этого понадобится перезагрузка операционной системы.
Categories: Linux Tags: , ,

Новый блог

September 16th, 2011 2 comments

Первый пост нового блога.

Теперь именно здесь мы будем знакомить пользователей с новостями наших сервисов.

Именно здесь пользователи смогут обсудить эти же новости с нашими разработчиками.

И, именно здесь мы будем по мере появления свободного времени, рассказывать, как устроены наши сервисы “изнутри”.

Categories: Вне категорий Tags: