Рассказывает Никита Салников-Тарновски, работник Plumbr
Недавно я натолкнулся на такой термин, как «старение ПО». Изначально я подумал, что это всего лишь какое-то очередное ничего не значащее определение (а убедился я в этом после прочтения статьи на Википедии), но когда я поглубже вник в эту концепцию, то она показалась мне весьма здравой. Так что я подумал, что стоит поделиться своими мыслями и знаниями по этой теме с вами.
Начнем с того, что Википедия говорит о «старении ПО»:
Старение ПО — процесс продолжительной деградации производительности и/или внезапных зависаний/падений программного обеспечения в связи с истощением ресурсов ОС, фрагментации и накапливанием ошибок.
Это определение чертовски скучно. Но, думаю, все вы сталкивались со следующей ситуацией: вы только что поставили Windows и довольны ее производительностью. Постепенно ее работа становится все более медленной, и чтобы решить проблему, необходима перезагрузка. Проходит год, и даже перезагрузка системы уже не помогает, и приходится переустанавливать ОС заново.
Перезагрузка/переустановка Windows — отличный пример, знакомый многим. Дэвид Парнас однажды сказал:
Программы, как и люди, стареют. Мы не можем предотвратить старение, но мы можем понять его причины, ограничить его эффекты, временно устранить ущерб, который оно нанесло, и приготовиться ко дню, когда программа станет нежизнеспособной. С первого релиза вашего ПО необходимо заниматься поддержкой и улучшением состояния продукта.
Парнас также упоминал о том, что legacy ПО более уязвимо к старению. Вне зависимости от своего объема, ваш код, скорее всего, подвержен проблеме старения ПО, вызванного разными причинами:
- утечки памяти;
- блокирующее поведение;
- слишком много открытых файлов;
- раздувание памяти/файла подкачки;
- повреждение данных;
- фрагментация хранилища данных;
- накопление ошибки округления.
Так как список выглядит немного сухо, я попробую проиллюстрировать его с помощью Java.
Утечки памяти
На данный момент работа с утечками памяти — главный источник заработка нашей компании. Мы работаем с тысячами приложений, и 50% из них содержат по меньшей мере одну утечку. Приведенный ниже пример иллюстрирует такой случай.
Программа читает одно число и возводит его в квадрат, а затем добавляет получившийся результат в примитивную реализацию кэша. Но так как значения никогда не читаются из кэша, можно сказать, что память используется зря, и, таким образом, возникает утечка памяти. Если мы запустим этот код и позволим пользователям работать с ним достаточно долго, то кэш со временем будет занимать все больше памяти, но при этом никак использоваться. Такая ситуация является хорошим примером старения — программа может работать дни перед тем, как пользователи начнут испытывать неудобства.
public class Calc {
Map cache = new HashMap();
public int square(int i) {
int result = i * i;
cache.put(i, result);
return result;
}
public static void main(String[] args) throws Exception {
Calc calc = new Calc();
while (true)
System.out.println("Enter a number between 1 and 100");
int i = readUserInput();
System.out.println("Answer " + calc.square(i));
}
}
}
Блокирующее поведение
Все мы были в ситуации, когда программа отлично работала годами, а потом что-то происходило, и потоки начинали простаивать и страдать от нехватки ресурсов.
Следующий пример служит отличной иллюстрацией. Нижеприведенный код может работать отлично месяцами до тех пор, пока кто-то не запустит одновременно операции transfer(a, b)
и transfer(b, a)
. В таком случае возникнет взаимная блокировка.
class Account {
double balance;
int id;
void withdraw(double amount){
balance -= amount;
}
void deposit(double amount){
balance += amount;
}
void transfer(Account from, Account to, double amount){
sync(from);
sync(to);
from.withdraw(amount);
to.deposit(amount);
release(to);
release(from);
}
}
Незакрытые файлы
Уверен, что многие из вас встречали такой код, проклиная коллегу, который забывает закрывать ресурсы после загрузки из них всего нужного. Такой код может работать месяцами, но в конце концов неизбежно выдаст java.io.IOException: Too many open files
, что, опять же, является отличным примером причин старения ПО.
Properties p = new Properties();
try {
p.load(new FileInputStream(“my.properties”));
} catch (Exception ex) {}
finally {
// Не хочу закрывать поток, не буду закрывать поток
}
Раздувание памяти/файла подкачки
Современные ОС зачастую перемещают долго неиспользуемые участки операционной памяти во вторичное хранилище, и вы можете натолкнуться на проблемы, когда физической памяти уже будет не хватать, и система начнет перемещать ее неиспользуемые куски в файл подкачки или swap-раздел, освобождая ОЗУ для более нужных операций. Ситуация становится еще хуже при использовании сборщика мусора — при полной сборке мусора сборщик должен обойти каждый доступный объект в графе объектов, чтобы найти мусор. В процессе работы он затронет каждую страницу в куче приложения, тем самым вызывая постоянный обмен страницами между оперативной памятью и файлом подкачки.
К счастью, на современных JVM последствия такой работы незначительны по нескольким причинам:
- большинство объектов никогда не выходят из молодого поколения, которое почти гарантированно всегда находится в оперативной памяти;
- к объектам, перешедшее в старое поколение, постоянно обращаются, а значит, им обеспечено место в оперативной памяти.
Так что, может, вам и удалось избежать таких проблем, но я видел, как сборка мусора занимала десятки секунд из-за постоянного обмена страницами. И опять у нас ситуация, когда приложение с лениво загружаемым кэшем становится невозможно использовать после нескольких месяцев работы.
Согласны ли вы с тем, что ПО имеет тенденцию стареть так же, как и люди, учитывая рассмотренные выше примеры? Лично я думаю, что да.
Перевод статьи «Why is your software aging?»