Pull to refresh

Книга «Software Dynamics: оптимизация производительности программного обеспечения»

Reading time11 min
Views2.7K
image Привет, Хаброжители!

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

За плечами автора несколько десятков лет практической работы и обучения профессиональных разработчиков. Он знакомит читателя с принципами и техниками, которые применимы в любой среде, начиная со встраиваемых устройств и заканчивая дата-центрами, и подкрепляет их примерами на основе подключенных по Ethernet систем x86 и ARM под управлением Linux. Опираясь на полученную с помощью KUtrace информацию, читатели смогут использовать конкретные решения, а не просто перебирать техники, такие как отключение кэша или ядер.
Структура книги
Материал книги разделен на четыре части, одна из которых посвящена созданию нетребовательного к ресурсам инструмента наблюдения KUTrace.
  • В части I (главы 1–7) описано, как проводить тщательные измерения четырех фундаментальных компьютерных ресурсов: процессора, памяти, диска/SSD и сети.
  • В части II (главы 8–13) представлены типичные инструменты наблюдения: логирование, информационные панели, подсчет/профилирование/выборка и трассировка.
  • В части III (главы 14–19) описаны проектирование и построение нетребовательного инструмента Linux для трассировки, записывающего действия каждого ядра процессора каждую наносекунду, а также программ постобработки для создания динамических HTML-страниц, отображающих полученные временны́е линии и взаимодействия.
  • В части IV (главы 20–30) содержатся практические примеры анализа помех, лежащих в основе необычных задержек при слишком длительном выполнении, медленном выполнении инструкций, ожидании процессора, памяти, диска, сети, программных блокировок, очередей и таймеров.
Оперируя этими идеями, вы сможете превратить эту картинку, на которой показана неожиданная задержка:

image

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

image

Используя те же идеи, можно превратить пример задержки программы в следующую картинку, на которой удаленный демон авторизации ssh пробуждает gedit на CPU 1:

image

(В части III вы узнаете, как создать эту картинку для вашего ПО.)

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

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

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

Слишком много кода


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

Обзор


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

image

Если опираться на схему из главы 20, то текущая глава будет посвящена случаю, когда выполнение происходит, но при этом выполняется лишний код. Чтобы отличать этот случай от ситуаций, когда выполнение происходит, но слишком медленно, о чем пойдет речь в следующей главе, мы используем IPC. Для выявления лишних путей кода мы обратимся к подробностям, предоставляемым выборками счетчика программ.

Программа


Программа mystery21 представляет сервер транзакций. В исходном коде вы увидите, что для каждого запроса RPC она выполняет примерно следующее:

if (SomeComplexBusinessLogic()) {
    // Тестируемый кейс
    if (OtherBusinessLogic()) {
       DoProcessRpc(data);
    } else {
       DecryptingRpc(data);
    }
} else {
...
}

Загадка


Не так давно после некоторых программных обновлений производительность кода этого сервера начала периодически варьироваться, зачастую превышая целевые ограничения по времени. Мы анализируем ее через клиент, отправляющий 200 идентичных RPC. Ожидается, что первый RPC будет отличаться от остальных, поскольку обращается к данным на диске, но мы предполагаем, что остальные 199 будут использовать те же данные, кэшированные в основной памяти. Тем не менее вместо этого мы наблюдаем 30-кратную разницу в задержке транзакций. Почему?

При выполнении этих идентичных транзакций логирование RPC (см. главу 8) загадочным образом показывает значительную вариативность в общем прошедшем времени на сервере, что можно видеть на рис. 21.1. На нем 200 RPC упорядочены вертикально по прошедшему времени, аналогично рис. 9.4 (см. выше), но в этом случае повернуты на 90 градусов. До этого здесь имела место некоторая вариативность, но только в двукратных пределах. Простые счетчики, такие как команда top, показывают, что прошедшее время программы практически равно процессорному времени в режиме пользователя. Значит, в этом процессе вряд ли участвует задержка диска или сети. Рассматриваемая программа почти на 100 % привязана к процессору в коде режима пользователя.

На выполнение большинства из этих 200 RPC уходит около 0,5 мс, некоторые занимают 1,5–2,5 мс, а несколько — аж 15 мс. Исходя из предыдущего поведения и начальных оценок времени выполнения, мы ожидаем, что транзакции будут занимать около 1 мс. Нам «известно», что DoProcessRpc и DecryptingRpc занимают аналогичное время обработки для каждого запроса. Таким образом, оказывается трудно объяснить широкую вариативность во времени отклика. Вместо того чтобы гадать, мы обращаемся к инструментам, которые позволят пронаблюдать поведение этой программы в течение всех 200 транзакций. В текущей главе мы сосредоточимся на трех группах RPC с рис. 21.1: группе из ~145 коротких RPC длительностью 0,5 мс, ~50 RPC длительностью 1,5 мс и четырех очень медленных транзакциях.

image

Изучение и анализ


Упрощенная статистика выборки счетчика программ для этой программы без простоя (табл. 21.1) показывает, что она в четыре раза больше времени проводит в процедуре DoProcessRpc по сравнению с DecryptingRpc, а также много времени затрачивает на memcpy. Это не то, чего мы ожидали. В частности, эта статистика не объясняет измерения, отображенные в журнале сервера.

image

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

Выполнение KUtrace для mystery21 показывает, что программа много времени тратит на выполнение кода пользователя, очень редко переключаясь между режимами ядра и пользователя. Это согласуется с измерениями счетчика, показывающими, что время выполнения программы практически на 100 % состоит из процессорного времени, затраченного в режиме пользователя. Данные трассировки мало что говорят нам о различиях между быстрыми и медленными транзакциями, упорядоченными по номеру ядра процессора. Но маркеры начала/завершения RPC позволяют упорядочить эти данные по ID RPC, как показано на рис. 21.2.

image

Ага! Теперь мы видим, что, согласуясь с данными журнала RPC, большинство отдельных транзакций оказываются быстрыми, некоторые выполняются дольше, а две, находящиеся примерно в точках 200 и 340, очень медленные. Полный трейс подтверждает, что здесь не происходит почти ничего, кроме выполнения на одном ядре процессора в режиме пользователя с небольшими разрывами на ожидание очередного запроса от клиента. Значит, можно точно исключить из подозреваемых ожидание процессора, диска, системных вызовов и т.д. Но нам еще неизвестно, почему некоторые транзакции оказываются медленнее других.

Рассматривая сначала несколько первых RPC, как показано на рис. 21.3, мы видим, что они выполняются на ядре 0 в верхней части и те же самые временны́е отрезки упорядочены по RPC ID внизу. Здесь имеется четыре быстрых и два медленных. В каждом RPC прошедшее время на 100 % привязано к процессору, значит, можно исключить вероятность того, что более длительные транзакции чего-то ожидают. На трех других ядрах ничего не выполняется, за исключением коротких прерываний таймера и некоторых коротких сетевых прерываний на ядре 3, значит, никаких очевидных помех для выполнения нет. Одна выборка счетчика программ без простоя, полученная в момент 985 (указано стрелкой), находится в memcpy, что согласуется со статистикой процессора на рис. 21.3.

image

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

При отсутствии каких-либо очевидных источников помех мы ожидаем, что все транзакции будут выполнять инструкции примерно с одной скоростью. Если включить отображение количества инструкций на такт, как показано на рис. 21.4, то мы действительно увидим почти одинаковую скорость выполнения по всем шести транзакциям, поэтому можно уверенно заключить, что медленные транзакции выполняют примерно в 2,5–3 раза больше инструкций. Теперь осталось только понять, откуда берутся эти лишние инструкции.

Статистики выборки счетчика программ, сгенерированные KUtrace, встроены в полный трейс переходов между режимами ядра/пользователя вместе с маркерами начала/конца RPC ID. Если включить отображение выборок счетчика программ, то мы увидим рассеянные выборки без простоя во многих, но не во всех транзакциях. На рис. 21.5 представлено несколько десятков начальных транзакций. Здесь выборки счетчика программ были отображены обратно в имена функций (см. главу 19), часть из которых показана.

image

В этих транзакциях выборки разрежены, поэтому их недостаточно для построения уверенных выводов о любой отдельной транзакции, для коротких — точно. Однако если мы соберем выборки по многим нормальным транзакциям и отдельно по группам медленных, то начнем видеть достоверные различия. Бежевые выборки относятся к Checksum, а синие — к memcpy.

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

На рис. 21.6 показано шесть бакетов для 200 рассматриваемых RPC. Пять RPC находятся в первом бакете [250–500 мкс). Далее мы видим среднее поведение по всем бакетам и всем 200 RPC, сопровождаемое бакетом [1–2 мкс) и т.д. Мы аккумулировали выборки счетчика программ по всем транзакциям в каждом бакете, упорядочив их в каждой линии от наиболее до наименее частых. Поскольку продолжительности выборок счетчика программ все являются кратными интервалу прерывания таймера 4 мс, они могут в общей сложности примерно равняться сумме точных продолжительностей выполнения. Наши более быстрые транзакции находятся в бакетах, относящихся к 250 и 500 мкс, а замедленные примерно в три раза попали в бакеты 1 и 2 мс. Два крайне медленных RPC находятся в бакете [8–16 мс). В большинстве бакетов практически все выборки относятся к Checksum и memcpy, но в последнем бо́льшая их часть относится к DecryptingChecksum.

image

Используя окошко поиска в пользовательском интерфейсе вывода KUtrace, мы подтверждаем, что все выборки счетчика программ, относящиеся к DecryptingChecksum, находятся в двух самых медленных RPC. Таким образом, согласно проведенным измерениям, в противоположность «известной» нам информации, скорость DecryptingChecksum сильно отличается от скорости Checksum — фактически она выполняется в 30 раз медленее. Это объясняет два крайне медленных RPC. Для их исправления потребуется переписать процедуру DecryptingChecksum.

А что насчет остальных бакетов? В них выборки счетчика программ схожи и занимают примерно 3/5 времени в Checksum и 2/5 в memcpy. Пока неясно, какой дополнительный код выполняется в более медленной группе. К счастью, мы вручную слегка доработали mystery21, включив метку mark_a, указывающую имя метода RPC, в данном случае chksum, в начале каждой транзакции, и метку mark_b, а именно chk, в каждом вызове процедуры Checksum. Эти метки можно видеть на рис. 21.7, где они наглядно показывают, что в более длительных транзакциях процедура Checksum ошибочно вызывается три раза (chk) вместо одного.

image

Загадка разгадана


Теперь мы видим, что происходит на самом деле. Бакеты с небольшим прошедшим временем содержат все нормальные транзакции. Они выполняются в течение примерно 0,5 мс в DoProcessRpc, но совсем не проводят время в DecryptingRpc. Средние бакеты, относящиеся примерно к 1,5 мс, в точности представляют RPC, которые ошибочно вызывают Checksum три раза. Последний бакет содержит два RPC DecryptingChecksum. В целом реальный код выглядит так:

if (OtherBusinessLogic(x)) {
    // 1 из N, замедленная обработка
    retval = DecryptingChecksum(s, chksumbuf);
} else {
    // Нормальная обработка
    retval = DoProcessRpc (s, chksumbuf);
    if (WrongBusinessLogic(x)) {
        // Около 1 из 5, баг излишней обработки
        retval = DoProcessRpc (s, chksumbuf);
        retval = DoProcessRpc (s, chksumbuf);
    }
}

Глядя теперь на код и его историю изменений, мы быстро понимаем, что WrongBusinessLogic() является скрытым багом, который был внесен более двух лет назад, и его никто не заметил. Незамеченным он оставался потому, что результаты лишней работы отбрасывались (вместо того чтобы вести к общему ошибочному результату) и лишнее время не отображалось в статистике отдельно — оно просто увеличивало среднее время в DoProcessRpc за период из множества транзакций примерно на 40 %. История изменений DecryptingRpc раскрывает изменение в алгоритме две недели назад. До этого выполнение DecryptingRpc и DoProcessRpc по факту занимало примерно равное время, но DecryptingRpc не производил ожидае­мых от него вычислений.

Теперь, когда мы раскрыли баг, удаление WrongBusinessLogic() потребует примерно 10 мин. Доработка DecryptingRpc может казаться сложной, поэтому после некоторого обсуждения вы со своими коллегами решаете смириться с этим замедлением, пока не удастся создать и протестировать на скорость более быструю реализацию.

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

Резюме


Вот все шаги, которые мы предприняли, и использованные нами инструменты:
  • отказ от догадок;
  • оценка результатов команды top;
  • просмотр журналов сервера, упорядоченных по прошедшему времени;
  • просмотр упрощенного профиля выборок PC;
  • выполнение KUtrace и упорядочение по RPC ID вместо номера ядра;
  • просмотр вывода KUtrace, содержащего информацию об IPC;
  • просмотр вывода KUtrace, отображающего отдельные выборки PC;
  • разделение RPC по прошедшему времени, кратному степени 2;
  • анализ отличий между быстрыми, средними и медленными запросами;
  • просмотр вывода KUtrace с вручную добавленными метками;
  • просмотр кода и поиск возможности внесения простых изменений;
  • измерение производительности нового кода после внесения этих изменений.
Об авторе
Ричард Л. Сайтс — написал свою первую компьютерную программу в 1959 году и бо́льшую часть карьеры занимался аппаратным и программным обеспечением, проявляя особый интерес к производительности и взаимодействиям процессоров и ПО. В числе последних работ — создание микрокода VAX, участие в разработке архитектуры DEC Alpha и изобретение счетчиков производительности, которые сегодня встречаются практически во всех процессорах. Занимался написанием нетребовательного микрокода и трассировкой ПО в DEC, Adobe, Google и Tesla. Ричард Сайтс получил степень доктора философии в Стэнфорде в 1974 году. Имеет 66 патентов и является членом Национальной инженерной академии США.

Более подробно с книгой можно ознакомиться на сайте издательства:

» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Software Dynamics
Tags:
Hubs:
Total votes 11: ↑11 and ↓0+11
Comments1

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия