Это — справочный материал о гейзенбагах. Говорим о том, как они выглядят и какое отношение имеют к мейнфреймам — прародителям облака.


/ фото Lars Zimmermann CC BY

Heisenbug (гейзенбаг или хайзенбаг) — термин, описывающий ошибки, которые меняют свойства во время отладки кода. То есть они исчезают при тестировании и дебаггинге, но проявляются в продакшене.

Название «гейзенбаг» отсылает к принципу неопределенности Гейзенберга из квантовой механики. В общих чертах его можно описать как неожиданное изменение свойств наблюдаемого объекта в результате факта наблюдения.

История


Автором термина «гейзенбаг» считается сотрудник исследовательского центра IBM Брюс Линдсей (Bruce Lindsay). Он внес вклад в развитие реляционных баз данных и занимался разработкой корпоративной СУБД IBM System R.

В 1985 году во время учебы в университете Беркли Брюс и Джим Грей (James Nicholas Gray), американский ученый в области теории вычислительных систем, трудились над ОС CAL-TSS. Она писалась специально для двухпроцессорного мейнфрейма Control Data 6400 [PDF, стр.3], на котором военные обрабатывали большие объемы данных.

Само собой, в процессе разработки возникали баги. Но несколько из них были особенными — как только инженеры пытались их исправить, они исчезали. В то время Линдсей как раз изучал физику и принцип Гейзенберга в частности. Внезапно Линдсея осенило — они с Греем стали свидетелями аналогичного явления: ошибки исчезали, потому что наблюдение влияло на свойства объекта. Отсюда и пошло название «гейзенбаг».

Эту истории Линдсей рассказал в интервью с представителями Ассоциации вычислительной техники (ACM) в 2003 году.

Примеры гейзенбагов


Пользователи в сети и на тематических платформах вроде Stack Overflow поделились несколькими примерами гейзенбагов, с которыми они встречались в своих проектах. Один из резидентов SO пытался вычислить площадь фигуры между двумя кривыми с точностью до трех знаков после запятой. Для отладки алгоритма на C++ он добавил строку:

cout << current << endl;

Но как только он её закомментировал, код перестал работать и зациклился. Программа выглядела следующим образом:

#include <iostream>
#include <cmath>

using namespace std;

double up = 19.0 + (61.0/125.0);
double down = -32.0 - (2.0/3.0);
double rectangle = (up - down) * 8.0;

double f(double x) {
return (pow(x, 4.0)/500.0) - (pow(x, 2.0)/200.0) - 0.012;
}

double g(double x) {
return -(pow(x, 3.0)/30.0) + (x/20.0) + (1.0/6.0);
}

double area_upper(double x, double step) {
return (((up - f(x)) + (up - f(x + step))) * step) / 2.0;
}

double area_lower(double x, double step) {
return (((g(x) - down) + (g(x + step) - down)) * step) / 2.0;
}

double area(double x, double step) {
return area_upper(x, step) + area_lower(x, step);
}

int main() {
double current = 0, last = 0, step = 1.0;

do {
last = current;
step /= 10.0;
current = 0;

for(double x = 2.0; x < 10.0; x += step) current += area(x, step);

current = rectangle - current;
current = round(current * 1000.0) / 1000.0;
//cout << current << endl; //<-- COMMENT BACK IN TO "FIX" BUG
 } while(current != last);

cout << current << endl;
return 0;
}

Суть гейзенбага: когда нет printout, программа выполняет сравнение с высокой точностью в регистрах процессора. При этом точность результата превышает возможности double. Для вывода значения компилятор возвращает результат вычислений в основную память — при этом дробная часть отбрасывается. И последующее сравнение в while приводит к верному результату. Когда строчка закомментирована, неявного усечения дробной части не происходит. По этой причине два значения в while всегда оказываются неравными друг другу. В качестве решения проблемы один из участников обсуждения предложил использовать приближенное сравнение чисел с плавающей запятой.

Еще одной историей про гейзенбаг поделились инженеры, работавшие со средой языка Smalltalk-80 на Unix. Они заметили, что система зависала, если оставить её на какое-то время без дела. Но после перемещения курсора мыши, все вновь работало как обычно.

Проблема была связана с планировщиком Unix, который снижал приоритет задач, которые простаивают. В какой-то момент приоритет понижался настолько, что процессы в Smalltalk не успевали завершаться. Стек задач разрастался и «вешал» программу. Когда пользователь двигал курсор, ОС восстанавливала приоритет и все возвращалось на круги своя.

Другие *баги


Есть еще ряд терминов, которые описывают разного рода ошибки: Борбаг, Мандельбаг, Шрёдинбаг.

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


/ фото OLCF at ORNL CC BY

Шрёдинбаг — ошибка, которая существует и не существует одновременно, пока на нее не посмотрит разработчик. Название ошибка получила в честь известного мысленного эксперимента.

Что касается мандельбага, то это ошибка, из-за которой система ведет себя хаотично и непредсказуемо. Феномен назван в честь физика, математика и создателя фрактальной геометрии Бенуа Мандельброта.

Что в итоге


Примеров гейзенбагов (и других *багов) — множество. Их очень сложно искать, но причины возникновения обычно банальны: неинициализированная переменная, ошибки синхронизации в многопоточной среде или проблемы с алгоритмами удаления «мёртвого» кода. Получается, что для борьбы с подобными ошибками их нужно отсекать еще на этапе проектирования приложения.



Из блога о корпоративном IaaS:

Комментарии (23)


  1. rsashka
    13.04.2019 21:27
    +1

    Спасибо, улыбнуло ;-)


  1. nerdeek
    13.04.2019 23:47

    В маркетинге бывает наоборот — попытка собрать информацию ломает коммерческий процесс. Или она так его меняет, что собранная информация обесценивается, полностью или частично.


  1. darkxanter
    14.04.2019 12:47

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

    bool is_not_equal(double a, double b) {
    	return std::fabs(a - b) > std::numeric_limits<double>::epsilon();
    }
    


    1. DistortNeo
      14.04.2019 13:28

      Можно написать ещё проще:


      //current = round(current * 1000.0) / 1000.0;
      //cout << current << endl; //<-- COMMENT BACK IN TO "FIX" BUG
      } while (fabs(current - last) >= 1e-3);

      А round не нужен. Это источник потенциальных ошибок. Например, если результат будет последовательно меняться: 1.0005 + eps, 1.0005 — eps,…, тогда даже если eps будет очень мало, мы никогда не остановимся.


    1. AngReload
      14.04.2019 17:06

      Как минимум, эпсилон ещё нужно масштабировать на величину наибольшего значения, иначе на больших числах такое сравнение мало поможет.


      std::fabs(a - b) <= std::numeric_limits<T>::epsilon() * std::max(std::abs(a), std::abs(b)) * units_in_the_last_place;


    1. Dmitri-D
      15.04.2019 05:48

      epsilon, обчно величина порядка 1E-7...1E-8 для float, это отклонение МАНТИССЫ числа, а не самого числа.
      для применения epsilon нужно сначала отделить порядок от мантиссы, иначе у вас 1E-30 будет равен 1E-20


  1. Throwable
    14.04.2019 13:09

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


    1. nerdeek
      14.04.2019 13:17

      Сталкивался :-( ужасные впечатления, после 100500 тестов хочется просто проснуться и чтобы такое больше никогда не снилось.


      1. Throwable
        15.04.2019 10:22
        +1

        Ха! Вот только за последние три месяца (специфично для Java):


        1. Пустой catch. Вредитель, писавший бизнес-логику верхнего уровня, замолчал исключение и поставил try с пустым catch. В итоге мы думали, что сбой ниже — в протоколе передачи: данные-то на экране показывались совсем левые, а исключение "типа" не выбрасывалось.
        2. Невозможное поведение на сертификационном вокрбенче. Тестовая группа клиента отправляет нам багрепорты, которые уже давно были пофикшены и на тестах не возникали. Две недели компостировали нам мозги: делаем в коде различные "подпорки" для гипотетических (и невозможных) случаев, добавляем больше логов. В итоге требую доступ к сертификационной машине и вижу — о чудо — определена переменная CLASSPATH и в нее добавлены старые .jar-ы из другой директории...
        3. Пустой-не пустой список. Функция фильтровала значения в списке, но на выход выдавала всегда пустой. Причем на все 100% верная и локально все тесты проходит. А на сервере не выдает. После разделывания кода оказался баг в библиотеке ORM, который выдавал "бажной" IndirectList: https://bugs.eclipse.org/bugs/show_bug.cgi?id=433075
        4. В задеплоенном приложении тупо не работали периодические процессы, а ошибки в логах не было. В результате головной боли обнаружилось, что клиент использовал кривой sftp, который почему-то обрезал концовку файла. А возникавший NoClassDefFoundError не перехватывался catch(Exception e) и поэтому в лог не выводился.


        1. nerdeek
          17.04.2019 08:40

          Пункт 1) это вообще какая-то хитроумная диверсия, по-моему. Пункт 2) — ситуация странным образом довольно типичная, полагаю, на нечто подобное попадались (в том или ином варианте) почти все. Пункты 3) и 4) — ну это просто злой рок, никто не застрахован.


    1. DistortNeo
      14.04.2019 13:29

      Да и ошибки в компиляторах никто не отменял. Уже 2 раза с таким сталкивался.


    1. cyberly
      14.04.2019 13:39

      На PHP сплошь и рядом бывает во всяких нечасто используемых расширениях Особенно, если писать под виндой, а потом разворачивать на Linux-сервере (хотя и с разными версиями Linux случаются нестыковки). Из того что помню — функции для работы с сокетами сильно платформозависимы.


    1. F0iL
      15.04.2019 12:03

      Было такое.
      Код

      std::timed_mutex mutex;
      mutex.lock();
      mutex.try_lock_for(std::chrono::seconds(1));
      почему-то не ждал, как полагается, секунду для разблокировки мьютекса, а сразу говорил что таймаут истёк и блокировку снять не удалось.
      На это была убита почти половина дня, а в итоге выяснилось что это баг в стандартной библиотеке:
      gcc.gnu.org/bugzilla/show_bug.cgi?id=54562

      А если еще вспомнить описание прекрасной POSIX-функции cuserid() из manpages
      Sometimes it does not work at all, because some program messed up the utmp file. Often, it gives only the first 8 characters of the login name.

      Nobody knows precisely what cuserid() does; avoid it in portable programs.


  1. Hommit
    14.04.2019 18:15

    О даа, «повезло» пару раз словить :( Ну, зато теперь я знаю как это называется :)


  1. artmmslv
    14.04.2019 19:23

    Я словил механический гейзенбаг.
    Не запускалась стиральная машинка: вместо стирки гудит и выключается. Через неделю работает. А потом нет. Нет стабильности.
    Оказалось, в нее вода не поступает. Или поступает — как получится. Тройник между холодной водой для раковины и стиральной машинкой был сделан на основе крана (с крутящейся ручкой), и вода поступает в обе системы только в некоторых положениях крана.
    Вот вам костыль в сантехнике.


  1. MacIn
    14.04.2019 20:19

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


    1. force
      15.04.2019 18:02

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


      1. MacIn
        15.04.2019 21:25

        И как нашли, кто нашкодил? Это ж самое сложное. Ну вот, упал у тебя поток Х, а данные его попортил поток Y сто лет назад :( Пока ввели искусственные guard page в тех местах, где была порча.


        1. force
          15.04.2019 22:42

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


          1. MacIn
            16.04.2019 17:48
            +1

            Мы сегодня нашли свою проблему, ура.
            Она была в коде 10 лет, и стала проявляться только теперь.


  1. Whuthering
    15.04.2019 00:12

    Про шрёдинбаг есть ещё другая формулировка:


    Шрёдинбаг[5] (англ. schroedin bug — в честь мысленного эксперимента с котом Шрёдингера) — термин, используемый в программировании для описания программной ошибки, которая никак не проявляет себя, однако внезапно возникает, если кто-то наткнётся на неё в исходном коде… и осозна?ет, что система вообще не могла работать при наличии такой ошибки. После этого программа перестаёт работать вообще до тех пор, пока ошибка не будет исправлена. Хотя это звучит невероятно, некоторые программы содержат в себе такие ошибки.


  1. Bedal
    15.04.2019 15:11

    Ловил типичный гейзенбаг в начале 90-х, на MSC 6.0 (ещё без плюсов, именно С). Short (16-битный) в стеке параметров занимал стандартные 32 бита. При debug сборке параметр укладывался в одну половину слова, а в release — в другую. Читался при этом он в обоих случаях из одной половины, так что в дебаге всё работало правильно, а в релизе — нет. Ох, долго же я это отлавливал…

    О-о-о, было же ещё круче! 5" дискеты. Был 40-дорожечный дисковод, поставили 80-дорожечные. Но у клиентов (за полярным кругом, между прочим), был 40-дорожечный, так что я форматировал под 40 дорожек. И — не работает. То есть там, у них — не работает. У меня — работает. Дискету возят самолётом, программа запускается именно с неё, наличие ложной копии на харде исключено — не было никаких хардов. Привозили, я записывал очередную версию, быстро на следующий самолёт, и — туда. Не работает :-)
    … оказалось, что, поскольку «мой» дисковод был 80-дорожечным, в режиме 40 дорожек он писал только на половину ширины дорожки. И у пользователей считывалась старая версия, записанная, ещё когда у меня стоял 40-дорожечный дисковод. Ну, и не работала с новыми данными, ессно.
    Разрешилось только, когда я, во-первых, слетал туда в командировку, во-вторых, личную дискету пустил в дело (тогда это было и дорого и дефицитно) — всё заработало сразу. И появилась возможность не спеша анализировать.


    1. nerdeek
      15.04.2019 18:51

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