В начале была проблема...
На работе поддерживаю и разрабатываю приложение под Линукс, которое раздает клиентам некий контент. В целом работа каждого клиента выглядит обычно — подключился по TCP/IP и обменивается пакетами с текстовой информацией пока дисконнект не разлучит их. Сервер с своей стороны на каждого клиента создает тред и цикле вызывает select. Пока клиентом было немного — всё работало достаточно стабильно и красиво. Но клиентов становилось больше и было замечено, что количество активных клиентов на сервер застрягло в районе 500-520. При этом нагрузки на сервере особой не было. Увеличение количества серверов давало немного времени, но ведь это не может быть бесконечным. Паралельно была замечена другая проблема — в логе часто начала появляться ошибка "Bad file description" (он же EBADF) и клиента отключало, а они жаловались. Причины подобной ошибки не непонятны. А это уже плохо.
Изучаем документацию
Исходя с документации, такая ошибка будет возникать, если дескриптор на момент обращения к нему (recv, send, select и так дальше) является невалидным. Например его кто то закрыл или благодаря неаккуратности программиста произошёл выход за пределы массива и в переменную, которая хранит дескриптор, был записан мусор. Но если почитать документацию ещё внимательней, то там есть интересная вещь — оказывается, select не может корректно работать с дескрипторами, чей номер больше 1024 (на самом деле это FD_SETSIZE, но в большинстве современных Linux он равен 1024), но многие мне доказывали, что select не может корректно работать, если количество дескрипторов, которые он "селектит" больше 1024. В манах этот факт либо умалчивается, либо написан очень расплывчато (а может виной мой не очень сильный английский...). По крайней мере, в man'ах debian, ubuntu и Fedora. В Suse есть заметка " An fd_set is a fixed size buffer. Executing FD_CLR or FD_SET with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior." То есть, не важно, сколько у вашего приложения открытых дескрипторов, важен номер. И после это всё стало на свои места — у меня на каждого клиента выделяется два дескриптора — один на сокет для клиента, другой для внутренних нужд. Эти два дескриптора в цикле и обслуживал select.
Опыты на гране UB
Паралельно выявил ещё одну интересную штуку. Если номер дескриптора больше чем количество разрешённых открытых файлов (это ulimit -n, обычно он равен также 1024). то select "зависает" на нем. (проверено на debian). Если в диапазоне FD_SETSIZE и ulimit -n, то стабильно дает Bad file description. Вполне возможно, что на других платформах поведение будет другим. Но на то оно и UB, что бы быть странным и не предсказуемым.
Идем в наступление на проблему
Были предложены следующие варианты выхода с сложившейся ситуации:
- "подменить" FD_SETSIZE в приложении, указав число побольше. Обещают, что под FreeBSD это работает на ура. Но под Linux только добавляет глюков. Приложение начинает работать нестабильно, как только номер дескриптора добирается до 1024.
- перекомпилировать ядро, указав нужное значение константы FD_SETSIZE. Уж слишком кардинальная мера, была оставленна на последок.
- использовать poll вместо select.
Хороший poll
На то, что бы переписать приложение под использования poll вместо select, ушло около 4 часов.
Теперь код выглядит так.
struct pollfd fds[2];
int timeout_msecs = 100;
int ret;
//....
while (!m_terminated) {
fds[0].fd = m_fd1;
fds[0].events = POLLIN | POLLERR | POLLHUP;
fds[1].fd = m_fd2;
fds[1].events = POLLIN | POLLERR | POLLHUP;
ret = poll(fds, 2, timeout_msecs);
if (ret < 0 || errno == EAGAIN)
continue;
if (ret < 0) break;
if (ret > 0) {
//читаем+пишем в сокеты
}
}
Итого
Несколько недель наблюдений показало, что ошибка перестала возникать, стабильность улучшилась на порядок. Более того, количество клиентов на сервере поднялось почти в два раза. А в пиках достигало и трикратного прироста.
Начальство довольно, а я могу дальше продолжать доделывать мелкие баги…
Надеюсь заметка поможет и другим.