Как часто вы ловите себя на том, что смотрите на свой экран и ждете, пока ваш сценарий или программа выдаст фактические данные, которые вы, наконец, сможете использовать и выполнить свою задачу. Некоторые из этих моментов могут быть действительно полезными, и вы можете потратить их на то, чтобы наполнить чашку кофе или чая (что угодно), но на самом деле все ненавидят ждать. Мы находим гораздо более забавные способы откладывать дела на потом, чем наблюдать за индикатором выполнения.

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

Работая в качестве SRE, мы должны поддерживать нашу основную линейку продуктов, но часто мы создаем множество дополнительных инструментов и утилит, которые облегчают нашу жизнь, чтобы получить дополнительную информацию или избавиться от тяжелого труда. Несмотря на то, что эти инструменты не являются для нас основным направлением, а в некоторых случаях они собираются из спешки и «быстрой и грязной» необходимости, мы начинаем полагаться на них и хотим, чтобы они работали как можно лучше, не тратя слишком много. усилия по развитию.

Постановка задачи

У нас был скрипт, которому требовалось 15–20 секунд для получения данных для наших конечных точек. Ничего критичного, но можно ли его уменьшить до 3-5 сек? Что-то менее раздражающее ожидание? Можно ли это сделать даже мгновенно? Все хорошие вопросы, но насколько это сложно? Все возможно, но нужно ли нам «продать свою почку», чтобы попасть туда?

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

Это оказалось отличным примером, позволяющим увидеть разницу между распространенными языками программирования для автоматизации бэкэнда в реализации параллельного выполнения. Наша команда в основном ежедневно использует Go, Python и немного PowerShell. Поскольку в настоящее время Go является нашим основным выбором из соображений производительности, я решил преобразовать сценарий PowerShell, ранее разработанный в Go, и посмотреть, какой производительности я могу добиться, не жертвуя слишком много времени и усилий.

Решение

Исходные числа: выполните 66 веб-вызовов GET к конечным точкам HTTPS и создайте результаты в удобочитаемой таблице и/или выводе JSON по мере необходимости. Простой, но требующий много времени для последовательного выполнения, тем более, что числа со временем растут. Доведите его до 1000 конечных точек, и вы сможете выпить больше одной чашки кофе… Если вы не ставите своей целью замедлить работу, это не сработает.

Последовательное выполнение PowerShell занимало примерно 15–20 секунд, чтобы получить результаты. Первоначальная конверсия Go с тем же последовательным выполнением была впечатляюще лучше приземлиться за однозначные секунды (4,428 с), что на 300% больше, чем у ворот. Можем ли мы сделать лучше, чем это? Следующим шагом было запустить это параллельно, и Go предлагает свой механизм многопоточности с легковесными горутинами:

var wg sync.WaitGroup
wg.Add(locationsCount)
ch := make(chan Result, locationsCount)
for _, location := range locations {
  location := location
  go func() {
    defer wg.Done()
    processLocation(location, ch)
  }()
}
wg.Wait()
close(ch)

Довольно просто преобразовать цикл for для асинхронного выполнения с подпрограммами go для каждой итерации. Весь код смотрите здесь.

Какие числа мы получили сейчас? С горутинами мы получаем дополнительное улучшение производительности на 150%, что приводит к общему времени выполнения 1,757 секунды. Уже неплохо, это огромное улучшение по сравнению с 15-20 секундами изначально. Можем ли мы сделать его еще лучше? ДА!

Go — это компилируемый язык, поэтому мы можем «собирать» двоичные файлы, а не компилировать их «на лету», что также требует дополнительного времени. При запуске предварительно скомпилированного двоичного файла мы получаем 0,542 секунды. Полсекунды в данном случае для человека мгновенны, и мы достигли своей цели.

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

Сравнение производительности

Go 1.15.15

См. финальное сравнение производительности Golang, измеренной в секундах:

go run main.go 0.61s user 1.06s system 37% cpu 4.428 total
go run main.go 0.67s user 1.31s system 112% cpu 1.757 total
./aro-rp-versions 0.18s user 0.10s system 53% cpu 0.542 total

Общий прирост производительности по сравнению с нашим эталоном PowerShell для последовательного выполнения почти в 30 раз.

Питон 3.9.7

Python предлагает два основных способа асинхронного выполнения с многопроцессорностью и многопоточностью. В этом конкретном сценарии многопоточность имеет наибольший смысл, когда сетевой ввод-вывод преобладает над производительностью ЦП, которая больше подходит для многопроцессорной обработки.

Цифры подтверждают одно и то же:

python3 aro_rp_versions_serial.py 1.77s user 0.24s system 13% cpu 15.157 total
python3 aro_rp_versions_mp.py 9.00s user 4.72s system 538% cpu 2.549 total
python3 aro_rp_versions_mt.py 1.39s user 0.55s system 145% cpu 1.325 total

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

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

Обратите внимание, что выбор правильного варианта здесь имеет значение в зависимости от вашей задачи. При использовании многопроцессорности возникают дополнительные накладные расходы, и он совершенно не подходит для этой задачи.

PowerShell 7.1.4

Первоначально описываемый как самый медленный, PowerShell в последовательном исполнении сравним с Python.

pwsh aro_rp_versions_serial.ps1 2.80s user 0.57s system 21% cpu 15.628 total
pwsh aro_rp_versions_runspace_pool.ps1 2.29s user 0.63s system 144% cpu 2.025 total

PowerShell не сильно отстает от Python и близок к результату Goroutines, компилируемому на лету, в этом случае до бинарных результатов.

Вывод

  • Голанг

Как и ожидалось, имеет самую высокую производительность из этих трех и выполняет свою работу. Самым впечатляющим результатом для меня является сравнение последовательного выполнения, поскольку Golang был на 300% быстрее, чем конкуренты, без каких-либо махинаций с параллельным выполнением.

  • Питон

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

  • PowerShell

Пулы пространства выполнения заимствованы из функциональности .Net в PowerShell и менее просты в реализации по сравнению с Python. Есть и другие способы параллельного выполнения в PowerShell, у которых есть свои плюсы и минусы. Этот пример должен был сравнить наиболее производительный способ сделать это в PowerShell, и он дает довольно хороший результат почти 2 секунды.

использованная литература

Примеры кода для Python и PowerShell

Пример Голанга