понедельник, 14 февраля 2022 г.

[prog.c++] Мои пять копеек про тестовое задание от Network Optix (по мотивам срачей на Хабре и RSDN)

Сперва была статья на Хабре "Мои собеседования '2021 (C++ developer)", потом один кусочек из нее процитировали на RSDN:

Домашнее задание, написать эффективный TCP-сервер с определенными требованиями. Код должен быть покрыт юнит-тестами. Раньше TCP-сервера писать не приходилось, потратил три дня почти full-time, отослал результат. Ответили что стилистически код понравился, но сервер недостаточно эффективен, в частности имеются лишние копирования данных. Оценил что на исправление замечаний может уйти еще N часов. Забил.

после чего образовался небольшой срач, в который угораздило вляпаться и мне. В какой-то момент туда пришел и автор хабровской статьи, показал написанные им варианты решения тестового задания (номер раз, номер два), после чего озвучил и условие самого тестового задания:

Клиентское приложение устанавливает TCP-соединение и передает строки, разделенные символом перевода строки “\n”.
Сервер должен считать их хеш-суммы (тип суммы — на выбор кандидата) и отправлять их обратно в HEX-виде, также завершая каждую сумму символом перевода строки.
Клиентские запросы должны обрабатываться параллельно, в случае достаточного количества параллельных соединений должны быть загружены все ядра CPU.
Сервер должен работать эффективно — не потреблять лишней памяти и отправлять хэш-суммы по мере их готовности.
Входные строки могут быть неограниченной длины.
Для реализации сетевой части сервера можно использовать любую удобную вам библиотеку из числа стандартных пакетов репозитория Ubuntu 16. Сервер также должен собираться и работать на Ubuntu 16.
На модули приложения должны быть написаны unit-тесты.

Сейчас не буду вдаваться в то, уместно ли в 2021-2022 давать тестовые задания соискателям. Как и не буду говорить про впечатления от RSDN-овского срача. Лучше я этому посвящу отдельный пост.

В этом же посте хочется поговорить именно про само это тестовое задание. Потому что оно мне понравилось. И потому что я слегка прифигел от уровня "решений" автора статьи с Хабра. Так что кому интересны мои пять копеек, то милости прошу под кат.


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

Потому, что оно простое. Понятно, что нужно сделать. И, если у человека есть опыт в работе с сетью, то ему будет сразу понятно и как делать.

Потому, что оно оставляет моменты, которые можно обсудить.

Вот, например, формулировка

"Клиентские запросы должны обрабатываться параллельно, в случае достаточного количества параллельных соединений должны быть загружены все ядра CPU."

может трактоваться по-разному. Например, если у нас больше одного параллельного запроса, то их обработка раскидывается по разным ядрам CPU (т.е. параллельных соединений достаточно для загрузки нескольких ядер).

Или же должно быть некоторое количество параллельных соединений, которые должны обслуживаться строго на одном ядре. Скажем, пока у нас от 1 до 1000 соединений, то мы их обрабатываем на одном ядре. Под следующую тысячу уже задействуем второе ядро. И т.д.

Я сильно сомневаюсь, что подобные вариации в трактовках изначально задумывались авторами тестового задания. Но сам факт обращения за уточнением по этому поводу уже можно было бы однозначно занести в плюс соискателю. А соискатель такими вопросами мог бы и потроллить работодателя, чтобы у того не возникло ощущение, что тестовое задание отлично сформулировано :)

Еще одним достоинством этого задания является то, что в нем можно идти от самой простой реализации ко все более и более сложным, учитывающим разные нюансы.

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

Затем можно начать добавлять надежности.

Скажем, мы можем оказаться в ситуации, когда клиент нам свои данные присылает, а вот наши хэши читать не торопится. И исходящие данные с нашей стороны начинают скапливаться. Что не есть хорошо. Такую ситуацию нужно диагностировать и, скажем, приостанавливать чтение до тех пор, пока не будут записаны ранее сформированные хэши.

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

Для начала можно было бы защититься от "медленных" клиентов. Которые пишут по байту в минуту. А то и еще меньше (типа сделают новое подключение, но ничего в него не пишут).

Такая защита будет необходима если мы сами приостанавливаем чтение входящих из-за медленной отправки исходящих. Ведь если мы перестали читать, а наши исходящие никуда не уходят, то рано или поздно с этим нужно будет что-то делать. Скажем, рвать соединение, если скорость приема входящих данных за последние 30 секунд составила 0 байт в секунду.

А потом можно было бы сделать и более-менее равномерное чтение данных от клиентов. Чтобы не оказаться в ситуации, когда у нас 100 соединений, но 90% времени мы читаем данные только из трех из них, а остальные обслуживаются по остаточному принципу.

В общем, здесь есть где развернуться...

Но при этом нужно учитывать еще одно хорошее качество этого тестового задания: требование о покрытии тестами.

По моим ощущениям, трудозатраты на написание тестов к решению этой задачи будут, минимум, в 1.5 раза выше, чем написание самого решения. Причем, чем больше нюансов мы учитываем в решении, тем больше тестов нам нужно будет сделать. Что приведет к тому, что для простой реализации объем/сложность тестов будет в 1.5 больше, а для сложной, надежной реализации, уже раза в 3-4 больше.

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

В какой-то момент нужно будет остановиться, отослать полученное решение и описать, что еще следовало бы сделать, если бы были время/ресурсы на продолжение банкета.

И по этому моменту также многое можно понять о кандидате: увлекся ли он только решением, забив на тестирование, или же, напротив, предпочел тестировать, а не наполнять решение функциональностью. Или же сумел выдержать золотую середину, да еще и не стал вкладывать в тестовое задание слишком много, но сумел обозначить куда и как следовало бы двигаться.

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


Теперь несколько слов о грустном. А именно о впечатлении, которое на меня произвели решения этой задачи от автора статьи на Хабре.

Но нет, лучше я это отдельным постом опубликую. Stay tuned...

Продолжение номер раз. И номер два.

2 комментария:

Yury Schkatula комментирует...

В таких случаях всегда вспоминаю nGinx как один из примеров софта, активно работающего с сетью и выступающего в ряде случаев и как сервер, и как клиент, поэтому интересующимся будет интересно погуглить "nginx architecture explained". Там много вроде бы неочевидных, но весьма логичных вещей для условий mass-service с заранее непредсказуемыми характеристиками.

Например
Inside NGINX: How We Designed for Performance & Scale
https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/

Ну и вспоминая сколько лет понадобилось nGinx для оттачивания этих подходов, ожидать всех их учтёнными кандидатом в тестовом задании - безусловно наивно. Равно как и в обратную сторону: рассчитывать что собеседующий сходу поймёт "зачем и почему" тоже не надо (к вопросу смог ли бы Игорь Сысоев пройти такой собес успешно)

eao197 комментирует...

@Yury Schkatula

Для человека, который собственоручно сделал хотя бы пару серверов (даже очень простеньких), в этом тестовом нет ничего сложного. Поскольку понятно, что от него качества nginx или envoy не требуется. Зато есть о чем поговорить с соискателем.

Ну а если человек с сетью никогда не сталкивался, но все равно пытается пройти по вакансии, то здесь да, здесь будет непросто.