Хабрахабр

Как я эволюцию админов в программистов измерял

Недавно мой знакомый Karl (имя изменено) проходил собеседование на должность DevOps и обратился ко мне с просьбой проверить его решение. Я почитал условие задачи и решил, что из нее бы вышел неплохой тест, поэтому немного расширил задачу и написал свою реализацию, а заодно попросил коллегу Alex подумать о своей реализации. Когда все три варианта были готовы, я сделал еще две сравнительные версии на C# и сел писать эту статью. Задача довольно проста, а соискатели находятся на неких ступенях эволюции из админов в программисты, которые я и хотел оценить.

Кому интересны грязные детали, необъективные тесты и субъективные оценки — прошу под кат.

Задача

По условию у нас есть текстовые логи с загрузкой CPU серверов и необходимо делать из них некие выборки.

Полный текст задания

Представьте себе систему мониторинга, в которой 1000+ серверов по несколько CPU записывают каждую минуту лог нагрузки в отдельный файл на выделенный сервер.

В итоге для 1000 серверов по 2 CPU за один день получается каталог с 1000 логами в текстовом виде по 2880 записей в таком формате:

168. 1414689783 192. 10 0 87
1414689783 192. 1. 1. 168. 11 1 93

Поля в файле означают следующее:
timestamp IP cpu_id usage

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

Нужно поддерживать следующие команды для запроса:

Команда QUERY — сводная статистика по серверу за диапазон времени
Синтаксис: IP cpu_id time_start time_end 1.

*Время задается в виде YYYY-MM-DD HH:MM

Пример:

168. >QUERY 192. 10 1 2014-10-31 00:00 2014-10-31 00:05
(2014-10-31 00:00, 90%), (2014-10-31 00:01, 89%), (2014-10-31 00:02, 87%), (2014-10-31 00:03, 94%), (2014-10-31 00:04, 88%) 1.

Команда LOAD — средняя загрузка выбранного процессора для выбранного сервера
Синтаксис: IP cpu_id time_start time_end 2.

Пример:

168. >LOAD 192. 10 1 2014-10-31 00:00 2014-10-31 00:05
88% 1.

Команда STAT — статистика всех процессоров для выбранного сервера
Синтаксис: IP time_start time_end 3.

Пример:

168. >STAT 192. 10 2014-10-31 00:00 2014-10-31 00:05
0: 23%
1: 88% 1.

Разрешается использовать любые языки программирования, сторонние утилиты.

S. P. Это не обязательно и программа может быть разбита на раздельные части для загрузки и для выполнения запросов. В изначальном задании подразумевалось, что это интерактивная программа, которая принимает команды с консоли после загрузки. допускается вариант с несколькими скриптами init.sh, query.sh, load.sh и т.д.
Т.е.

В целом, задача весьма прозрачна и в ней напрашивается использование БД, поэтому не удивительно, что все три варианта используют SQLite. Вспомогательные варианты на C# я уже сделал для сравнения скорости и они работают иначе.

Оценка

В готовых решениях я оценивал два фактора с соотношением 40/60%: скорость и качество кода. Методика оценки факторов приведена чуть ниже, но оба фактора никак не относятся к общему вопросу и не показывают степерь «админства» или «программерства», поэтому отдельно я кроме сухих баллов скорости/качества, вывел отдельно субъективную шкалу «админское решение», «программерское», «универсальное». Это ни в каком виде не соревнование и не сравнение скорости разных языков, а скорее оценка подходов к программированию.

Оценка скорости

На мой взгляд это наводит на мысль, что тест должен выполняться на порядок или два быстрее, чтобы на адекватном оборудовании всегда оставаться в нужных рамках. По условию запрос должен выполняться меньше секунды, но не приведено ни оборудование, ни количество ядер, ни вообще архитектура тестовой станции. Значит, идеальное решение должно не показывать зависимости от объема данных и не потреблять лимитированные ресурсы в неограниченном количестве. Заодно это должно натолкнуть на идею масштабируемости — в задаче приводится пример на 2880000 записей за один день, но в реальных условиях их может быть заметно больше (больше серверов и ядер), а диапазон выборки может включать не дни, а месяцы и годы. ~420GiB данных. В этом случае бесконтрольное использование памяти (in-memory tables или хранение в массивах в памяти) это минус, а не плюс, потому как выборка за год на 10000 компьютеров по 8 процессоров это навскидку 42 048 000 000 записей, минимум по 10 байт каждая, т.е. К сожалению, проверить такие объемы мне не удалось из-за ограничений доступной техники.

Для проверки скорости использовалась unix команда time (значение user), а для интерактивных решений — внутренние таймеры в программе.

Оценка качества

Нет особого смысла в коде, который работает только в строго заданных рамках и никак не может выйти за их пределы, обработать больше данных, быть изменен для других ситуаций и т.д. Под качеством я в основном понимаю универсальность — универсальность использования, поддержки, доработки. В первую очередь тут оценивалась обработка входящих параметров: нестандартные ситуации, выборки, дающие 0 результатов, не валидные запросы. Например, код на x86 Assembler мог бы быть очень быстрым, но совершенно не гибким и простейшее изменение вроде перехода на IPV6 адреса могло бы стать для него очень болезненным. Во-вторую — язык программирования, стиль кода, количество и качество комментариев.

Субъективная оценка

Лично я разделяю их по такому принципу: администратор работает с инструментами, а программист создает их. Сложно сказать, какой именно параметр определяет, насколько эволюционировал администратор до программиста. Хороший админ знает скорость работы базы данных, понимает что такое горизонтальное и вертикальное масштабирование, каждый день пользуется индексами. Разница примерно как между профессиональным гонщиком и автомехаником — механик зачастую неплохо водит и знает автомобиль досконально, но гонщик чувствует все заложенные в машину свойства и даже больше. Программист может написать свою БД, использовать вложенные деревья для них, может конвертировать все данные в собственный формат и оставить их на диске, хитрым образом расположенные для быстрого доступа.

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

Программы

Всего у меня было 5 программ — три участвуют в сравнении, две написаны уже позже, только для проверки некоторых идей и сделаны на C#. Для удобства я программы буду называть именами их авторов.

7
Зависимости: нет
Интерактивная: да
БД: SQLite, in-memory table Karl
Код на github
Язык: Pyhon 2.

7
Зависимости: progress, readline
Интерактивная: да
БД: SQLite, in-memory table Alex
Код на github
Язык: Pyhon 2.

Nomad1
Код на github
Язык: Bash
Зависимости: нет
Интерактивная: нет
БД: SQLite
Особенность: внешний .db файл для работы

Nomad2
Код на github
Язык: C#
Зависимости: mono
Интерактивная: да
БД: нет
Особенность: Хэш-таблица по IP адресам

Nomad3
Код на github
Язык: C#
Зависимости: mono
Интерактивная: нет
БД: нет
Особенность: специально подготовленные данные

Тестирование

Для тестирования был написан генератор логов, сначала на bash, потом на C++. Было создано три тестовых набора:

  • data_norm — 1000 логов по 2 CPU, один день (~80Mb логов)
  • data_wide — 1000 логов по 2 CPU, один месяц (~2.3Gb логов)
  • data_huge — 10000 логов по 4 CPU, 5 дней (~10Gb логов)

Запросы формировались по принципу:

  • valid — запрос в диапазоне допустимых значений
  • wide — запрос шире, чем допустимые значения (захватывает начало или конец диапазона)
  • invalid — запрос для отсутствующих данных

Все тесты выполнялись по 4 раза, первое значение откидывалось, остальные усреднялись (чтобы исключить время JIT компиляции, прогрева кеша, загрузки из свопа). Тестирование проводилось на рабочем компьютере под Mac OS 10.13.2 с процессором i7 2.2 GHz, 8GB RAM, SSD диском.

Мысли вслух по работе программ

DISCLAIMER: «Прогрев кэша» приводит к тому, что данные подтягиваются в кэш и память и мы измеряем не реальное время отклика, которое было бы у оператора программы, а сферическое в вакууме время отдачи данных из кеша и обработки их в sqlite/python/C#. Это не научно, не профессионально и бесполезно для чего-либо еще кроме этой статьи. Не делайте так в реальной жизни!

В случае программы Nomad1 вывод может занимать сотни миллисекунд из-за очень медленного форматирования в Bash, в то время как запрос выполняется за миллисекунды. К сожалению, тесты по запросу QUERY не очень показательны у половины программ, потому как вывод на экран зачастую в разы дольше самого запроса. В моем понимании «время выполнения команды» это время между вводом команды и получением результата, поэтому к этой программе пришлось применить штрафы, описанные ниже. В программе Karl вообще допущена ошибка измерения: считается время выполнения внутреннего запроса для QUERY без вывода на экран.

7, с использованием SQLite, в интерактивном режиме (сначала загружаются данные, потом принимаются команды). Примечательно, что Karl и Alex не сговариваясь написали программы на python 2. Программа Nomad1 написана на чистом bash как набор CLI скриптов и тоже использует SQLite.

В случае с Nomad3 условно принимается, что имя файла это IP адрес и при поиске программа просто считывает файл в память и дальше работает перебором. Программы Nomad2 и Nomad3 интересны общим подходом: в случае с Nomad2 все данные грузятся в память в хэш-таблицу с ключом по IP. Кроме всего прочего, они написаны на C#, который представлен на Unix в виде mono и имеет кучу особенностей. Оба теста актуальны только для сравнения скорости и не участвуют в оценке качества. Net все работает еще быстрее.
Например, результаты mono32 и mono64 разнятся в разы для того же кода, а на Windows и .

Результаты по скорости

Сами команды запросов я спрячу под кат, чтобы не засорять топик. В таблицах результат записано по три строки на ячейку, это скорость выполнения команд QUERY, LOAD, STAT в секундах.

Запросы

data_norm/valid:
QUERY 10.0.2.23 1 2014-10-31 09:00 2014-10-31 12:00
LOAD 10.0.2.254 0 2014-10-31 13:10 2014-10-31 20:38
STAT 10.0.1.1 2014-10-31 04:21 2014-10-31 08:51

0. data_norm/wide:
QUERY 10. 11 0 2014-10-01 09:00 2014-10-31 07:21
LOAD 10. 1. 2. 0. 0. 254 1 2014-10-31 15:55 2014-11-04 10:00
STAT 10. 100 2014-10-31 14:21 2015-01-01 01:01 1.

0. data_norm/invalid
QUERY 10. 23 1 2015-10-31 09:00 2015-10-31 12:00
LOAD 10. 2. 2. 0. 0. 254 0 2015-10-31 13:10 2015-10-31 20:38
STAT 10. 1 2015-10-31 04:21 2015-10-31 08:51 1.

0. data_wide/valid:
QUERY 10. 33 0 2014-10-30 09:00 2014-10-31 02:00
LOAD 10. 2. 0. 0. 0. 125 1 2014-10-02 14:04 2014-10-04 20:38
STAT 10. 10 2014-10-07 00:00 2014-10-17 23:59 1.

0. data_wide/wide:
QUERY 10. 11 1 2014-07-30 09:00 2014-10-01 07:21
LOAD 10. 1. 0. 0. 0. 137 0 2014-10-20 04:12 2015-02-01 00:00
STAT 10. 3 2014-10-20 04:12 2015-02-01 00:00 3.

0. data_wide/invalid
QUERY 10. 123 1 2015-10-31 09:00 2015-10-31 12:00
LOAD 10. 0. 0. 0. 0. 154 0 2015-10-31 13:10 2015-10-31 20:38
STAT 10. 1 2015-10-31 04:21 2015-10-31 08:51 0.

0. data_huge/valid:
QUERY 10. 33 0 2014-10-30 09:00 2014-10-31 02:00
LOAD 10. 2. 0. 0. 0. 125 1 2014-10-28 14:04 2014-10-30 20:38
STAT 10. 10 2014-10-28 00:00 2014-10-30 23:59 1.

0. data_huge/wide:
QUERY 10. 72 0 2014-10-31 09:00 2015-11-03 12:11
LOAD 10. 5. 0. 0. 0. 137 0 2014-10-20 04:12 2015-02-01 00:00
STAT 10. 3 2014-10-20 04:12 2015-02-01 00:00 3.

0. data_huge/invalid
QUERY 10. 11 1 2014-07-30 09:00 2014-10-01 07:21
LOAD 10. 1. 0. 0. 0. 154 0 2015-10-31 13:10 2015-10-31 20:38
STAT 10. 1 2015-10-31 04:21 2015-10-31 08:51
0.

Было сделано 135 тестов (по 27 на каждую программу), их скорость выполнения приведена в таблице:

Test

Karl

Alex

Nomad1

Nomad2

Nomad3

data_norm/valid

0.008800
0.000440
0.000420

0.215300
0.211700
0.217800

0.256200
0.007300
0.008300

0.002160
0.000130
0.000140

0.050200
0.050300
0.052600

data_norm/wide

0.002640
0.000330
0.000630

0.218000
0.212000
0.215000

0.716000
0.008000
0.008600

0.005000
0.000150
0.000320

0.050200
0.005200
0.005500

data_norm/invalid

0.000063
0.000073
0.000065

0.214200
0.209100
0.206300

0.007600
0.008300
0.008100

0.000008
0.000026
0.000034

0.048000
0.053000
0.050000

data_wide/valid

0.007300
0.005500
0.002300

6.237600
6.146500
6.151000

1.446000
0.036000
0.069000

0.017186
0.001099
0.005665

0.167000
0.088000
0.126000

data_wide/wide

0.006800
0.002100
0.024200

6.176600
6.157900
6.326100

0.570000
0.039000
0.070000

0.008363
0.005818
0.005592

0.071000
0.160000
0.159000

data_wide/invalid

0.000085
0.000110
0.000150

6.288100
6.152100
6.130400

0.044000
0.040000
0.062000

0.000013
0.000040
0.000013

0.155000
0.156000
0.164000

data_huge/valid

0.009107
0.007655
0.012858

155.9738
146.5377
140.1752

1.401000
0.013300
0.026000

0.036806
0.003798
0.003751

0.069000
0.066000
0.072000

data_huge/wide

0.009418
0.013718
0.014266

157.1896
148.5435
147.9525

1.078000
0.011700
0.026000

0.018393
0.000805
0.003329

0.072000
0.081000
0.077000

data_huge/invalid

0.000070
0.000095
0.000081

144.7307
158.0090
165.6820

0.012000
0.013000
0.023000

0.000012
0.000031
0.000013

0.054000
0.071000
0.081000

Результат по скорости я оценивал математически: для каждого запроса и набора данных считался порядок (десятичный логарифм от времени в микросекундах) и затем он выступал делителем для порядка самого быстрого решения. Таким образом, самое быстрое решение получало коэффициент 1.0, на порядок более медленное 0.5 и т.д. Результат по каждой программе усредняется и умножается на 40.

$R = 40 \cdot \frac^{n} \frac{\log_{10}T_{best}}{\log_{10} T_{i} + M_{i}}}{n}$

она не считала время работы всей команды QUERY, а только внутреннего SQL запроса. Для программы Karl, к сожалению, пришлось ввести уменьшающий коэффициент, т.к. Я добавил один порядок (M) ко всем не-пустым результатам QUERY, что уменьшило балл Karl примерно на 2 балла суммарно.

Полную версию таблицы с результатами можно увидеть тут.

Результаты:
Karl: 31/40 (33 без штрафа)
Alex: 15/40
Nomad1: 22/40
Nomad2: 39/40
Nomad3: 21/40

Результаты по качеству

Тесты скорости выявили разные интересные ошибки и подводные камни. Я прячу их под спойлер на случай, если вам взбредет в голову написать свою программу и вы уверены, что наверняка не допустите чужих багов. Программы Nomad2 и Nomad3 тут не разбираются и не оцениваются.

Ошибки и недочеты

1. Время в UTC. И Alex и Nomad1 забыли об этом и их результаты смещены на 2 часа из-за нахождения в зоне GMT +2.

Индексы. 2. Karl создал индекс из всех полей — IP + Timestamp + CPU. Alex забыл об индексах вообще. Это не критично, если размер базы более-менее адекватный, но для вариантов _wide и _huge это привело к огромным потерям памяти при минимуме выигрыша в скорости. Это оправдано только в очень редких случаях поиска по конкретному Timestamp, но по условию задачи мы всегда делаем выборку по IP + CPU и диапазону Timestamp. Программа Karl на данных _huge постоянно вылетала с ошибкой «Killed: 9» из-за переполнения памяти и свопа.

Включение границ диапазона в расчеты. 3. Nomad1 об этом забыл и его выборка из-за каких-то особенностей преобразования timestamp в bash иногда не включает нижнюю границу (в github есть исправленный результат, но в тесты он не вошел).

Использование :memory: таблицы с неизвестным объемом данных.
Это архитектурная ошибка и ее допустили Karl и Alex — они сделали in-memory table, не спрашивая себя о последствиях и объемах. 4. В реальных условиях такие бы программы не работали или работали с проблемами. В итоге их программы очень зависимы от объема данных и доступной памяти, что уже видно в тесте data_huge. Идеальный вариант должен оценивать объем считываемых данных и выбирать тип базы.

Проверка входящих данных и ошибок. 5. В случае invalid запроса LOAD у Alex вылетает ошибка деления на ноль, у Karl пишется No data, а у Nomad1 вообще нет ни одного Exception и вывод ошибки SQLite в запросе STAT будет перемолот через разбиение строки по знаку |. Тут налажали все — запросы в базу не проверяются на валидность даты, адреса, SQL Injection и т.д. 00. Ни одна программа не воспринимает IP адрес вида 010. 003. 020. для тестов пришлось сделать 540+ выполнений команд, у меня не хватило здоровья собрать и разобрать их примеры. Вылеты от неверных запросов были у всех, но т.к.

Округление результатов для LOAD и STAT. 6. Alex привел число к INT, отбросив дробную часть целиком.
Karl ничего не округлял и вывел число с десятичной точкой, что не смертельно, но не соответствует условию задачи.

Все три программы написаны на современных и читабельных языках программирования (VBScript и Brainfuck не замечено). Код на bash чуть менее читабелен, чем версии на Python, но заметно меньше по объему. Код Alex использует сторонние библиотеки readline и progress, написан свой класс для Auto-complete по Tab, есть отдельные функции для хелпов, работы с датой, поддержка перезагрузки данных, обработка ошибок, однако база не закрывается при выходе. Код Karl использует класс для наследования от Cmd, обработку исключений, закрывает БД при выходе, ловит Ctrl-C. К сожалению, комментариев нет ни у кого (с парой не существенных исключений).
Интересный и более программистский подход использует Alex — он для всех трех команд делает одинаковый запрос, а затем в коде считает данные для STAT/LOAD, не пользуясь AVG и GROUP BY. Это существенно снижает объем кода, а скорость выполнения в целом получается такая же, как если переложить эту задачу на БД.

С учетом описанных особенностей и пары дополнительных факторов по качеству я оценил программы так:

Karl: 35/60
Alex: 40/60
Nomad1: 30/60

Выводы

Сумма баллов:
Karl: 66/100
Alex: 55/100
Nomad: 52/100

Что интересно, как только я сообщил Alex про невысокую скорость, он сказал, что в 82й строке можно добавить индексы, он это планировал и продумал, но решил оставить «на потом». По баллам и скорости всех обошло решение Karl, потому как решение Alex не конкурентно по скорости из-за отсутствия индексов. К сожалению, это было уже было после приема программ и заморозки кода, поэтому такое изменение внести было нельзя.

Не удивительно, что работа с хеш-таблицей оказалась быстрее БД, пусть и с большими потерями памяти. Программы Nomad2 и Nomad3 набрали по скорости 39/40 и 21/40 балла соответственно. Работа напрямую с файловой системой оказалась не особо быстрой, но надо понимать, что у такого варианта почти отсутствует время инициализации, у него минимальная нагрузка на память и по большому счету он может использоваться с любыми объемами заранее подготовленных данных.

Все варианты с :memory: таблицей или хэш-таблицами не смогут работать при объемах 10Гб и выше, в то время как решение с БД в файле не намного медленнее и масштабируется гораздо лучше. Вариант Karl за счет «широкого» индекса потреблял больше всех памяти и падал уже при размере данных в 6Гб. К сожалению, вывод данных через bash поставил крест на скорости этой программы.

Работа в виде интерактивных приложений дает существенный прирост к скорости — у программ Nomad1 и Nomad3 явно видно, что даже на пустых запросах около 10 мс для bash и 50 мс для C# уходит только на запуск.

Субъективная оценка

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

Это несомненный плюс, но и явно показывает, что все три варианта далеки от чистого программирования. Все три участника использовали SQLite и не стали городить свой велосипед. Чуть-чуть ближе к программерскому решению подход Alex к использованию единого запроса, а потом расчетах LOAD/STAT в коде. Они решают свою задачу, при чем, достаточно быстро, но без попыток создать собственную In-Memory Database с быстрой индексацией (как в варианте Nomad2) или выборками без предварительной загрузки (как в варианте Nomad3). Авторы в целом не стали задумываться о хранении данных и по большому счету просто сделали перенос текстовых файлов в бинарный формат SQLite. Так же я не увидел в коде других «спутников программиста», таких как логи, комментарии, собственные структуры для данных (адрес в IP4 ведь это 32-битное число, а CPU и LOAD — однобайтовые переменные!).

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

Самое «админское» решение:

Nomad1 — это и команда .import, и передача данных в консольный клиент sqlite вместо коннектора/курсора
2. 1. Alex Karl — работа с индексами, SQL запросы для всех операций, GROUP BY, ORDER BY
3.

Самое «программерское» решение (перк «он создал новый инструмент»):

Alex — хорошая структура, работа с массивом данных при выборке, сторонние библиотеки
2. 1. Nomad1 Karl — код c исключениями, очистка данных
3.

Самое «универсальное» решение:

Nomad1 — команды дописываются отдельными файлами-запросами к готовой БД по аналогии с имеющимися; программа не зависит от объема данных и памяти.
2. 1. Karl Alex — единый запрос выдает массив данных, дальше код их обрабатывает; в шапке файла есть код для работы с файловой БД
3.

Все коды программ, включая генератор, доступны на GitHub

Там же есть команды, которые используются для генерации наборов данных.
Если у кого-то есть желание проверить себя на аналогичном тесте — милости прошу.

S.: По итогам тестов Nomad1 — программист с 20-летним стажем — получил меньше баллов в данной задаче, чем DevOps и Junior developer. P. С другой стороны, он еще и автор статьи и было бы, кхм, не корректно присуживать себе более высокие баллы 🙂

P. P. Производительность автора как писателя — однозначно неудовлетворительная. S.: Написание статьи и выполнение замеров потребовало три рабочих дня, это больше, чем все участники вместе взятые потратили на написание и отладку кода.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»