Почему ваши программы «стареют»?

Рассказывает Никита Салников-Тарновски, работник Plumbr


Недавно я натолкнулся на такой термин, как «старение ПО». Изначально я подумал, что это всего лишь какое-то очередное ничего не значащее определение (а убедился я в этом после прочтения статьи на Википедии), но когда я поглубже вник в эту концепцию, то она показалась мне весьма здравой. Так что я подумал, что стоит поделиться своими мыслями и знаниями по этой теме с вами.

Начнем с того, что Википедия говорит о «старении ПО»:

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

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

Перезагрузка/переустановка Windows — отличный пример, знакомый многим. Дэвид Парнас однажды сказал:

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

Парнас также упоминал о том, что legacy ПО более уязвимо к старению. Вне зависимости от своего объема, ваш код, скорее всего, подвержен проблеме старения ПО, вызванного разными причинами:

  • утечки памяти;
  • блокирующее поведение;
  • слишком много открытых файлов;
  • раздувание памяти/файла подкачки;
  • повреждение данных;
  • фрагментация хранилища данных;
  • накопление ошибки округления.

Так как список выглядит немного сухо, я попробую проиллюстрировать его с помощью Java.

Утечки памяти

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

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

Блокирующее поведение

Все мы были в ситуации, когда программа отлично работала годами, а потом что-то происходило, и потоки начинали простаивать и страдать от нехватки ресурсов.

Следующий пример служит отличной иллюстрацией. Нижеприведенный код может работать отлично месяцами до тех пор, пока кто-то не запустит одновременно операции transfer(a, b) и transfer(b, a). В таком случае возникнет взаимная блокировка.

Незакрытые файлы

Уверен, что многие из вас встречали такой код, проклиная коллегу, который забывает закрывать ресурсы после загрузки из них всего нужного. Такой код может работать месяцами, но в конце концов неизбежно выдаст java.io.IOException: Too many open files, что, опять же, является отличным примером причин старения ПО.

Раздувание памяти/файла подкачки

Современные ОС зачастую перемещают долго неиспользуемые участки операционной памяти во вторичное хранилище, и вы можете натолкнуться на проблемы, когда физической памяти уже будет не хватать, и система начнет перемещать ее неиспользуемые куски в файл подкачки или swap-раздел, освобождая ОЗУ для более нужных операций. Ситуация становится еще хуже при использовании сборщика мусора — при полной сборке мусора сборщик должен обойти каждый доступный объект в графе объектов, чтобы найти мусор. В процессе работы он затронет каждую страницу в куче приложения, тем самым вызывая постоянный обмен страницами между оперативной памятью и файлом подкачки.

К счастью, на современных JVM последствия такой работы незначительны по нескольким причинам:

  • большинство объектов никогда не выходят из молодого поколения, которое почти гарантированно всегда находится в оперативной памяти;
  • к объектам, перешедшее в старое поколение, постоянно обращаются, а значит, им обеспечено место в оперативной памяти.

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

Согласны ли вы с тем, что ПО имеет тенденцию стареть так же, как и люди, учитывая рассмотренные выше примеры? Лично я думаю, что да.

Перевод статьи «Why is your software aging?»