Как инженеры, мы проводим много времени за отладкой проблем, однако этому редко учат, как самостоятельному навыку. Некоторые ошибки настолько сложны, что их решение может показаться практически невозможным, особенно для разработчиков без опыта. Нет худшего чувства, чем застрять на сложной проблеме, не зная, как действовать дальше. Конечно, самое правильное, что нужно сделать, если вы застряли — это попросить помощи: у своей команды, у других инженеров в вашей организации или в вашем кругу общения, у случайных незнакомцев в интернете. Как случайный незнакомец в интернете, этот пост — моя попытка помочь вам выбраться из тупика, если вы оказались в подобной ситуации.
Пост написан в виде упорядоченного списка, но не каждая проблема требует выполнения всех шагов. Иногда правильное решение приходит в голову само собой на шаге 1 или, что еще лучше, на шаге 0! В других случаях вы можете пропустить несколько шагов или выполнить их в другом порядке. Но в целом, порядок, приведенный здесь, это основа, на которую я постепенно перешел со времени моей первой работы над модулем обработки ресурсов для контроллера базовой станции GSM в компании Lucent Technologies в 1997 году. За прошедшие годы я работал во многих различных средах: системное программирование, базы данных, настольные приложения, веб-приложения, backend и frontend. Эти шаги обобщены и применимы ко всем этим средам, они не являются специфическими для какого-то конкретного языка или парадигмы.
0. Ваше психическое состояние
Самые сложные проблемы часто возникают в моменты наибольшего напряжения. Что-то сломалось на продакшене и платящие клиенты жалуются на это. Возможно, они требуют возврата денег. Ваш начальник хочет знать, когда вы это пофиксите, а вы еще даже не знаете, в чем дело.
Если все это происходит, вы, вероятно, находитесь в состоянии стресса, а стресс заставит вас решать проблему медленнее, а не быстрее. Поэтому, прежде чем перейти к очевидному шагу 1, нам нужно сначала позаботиться о шаге 0. Убедитесь, что вы находитесь в хорошем расположении духа. Постарайтесь расслабиться, будьте спокойны. Ваш продакшен может не работать в течение часа, но это лучше, чем если бы он простоял много часов из-за того, что вы поспешно предприняли неправильные действия.
Не менее важно быть уверенным и оптимистичным в своих взглядах. Программирование — это не волшебство, системы следуют правилам, даже если эти правила загадочны и неизвестны нам. У каждой проблемы есть рациональная причина и решение, которые вы найдете со временем. Поэтому будьте настойчивы, не сдавайтесь.
Наконец, будьте честны с собой в отношении проблемы. Не обманывайте себя в том, что вы знаете, что что-то является правдой, если это всего лишь предположение. Проверяйте эти предположения, потому что они часто удивляют вас. Это нормально, что вы не всегда понимаете все части проблемы, если только вы признаете те части, которые вам пока непонятны. Держите их в уме, но отделяйте и возвращайтесь к ним позже. Разделяй и властвуй.
1. Воспроизведите проблему
Воспроизведение проблемы кажется настолько очевидным первым шагом, что почти не заслуживает упоминания. Это должен быть первый шаг каждого, но я часто удивлялся в разговоре с инженерами, когда спрашивал, воспроизвели ли они проблему самостоятельно.
Недостаточно работать на основе чужого описания ошибки или того, что вы думаете о проблеме. Помните, что вам нужно проверять свои предположения, и нет более важного предположения, чем то, существует ли проблема в том виде, в котором она описана, или какие шаги нужно предпринять, чтобы ее решить. Сначала докажите, что вы правильно их понимаете.
На моей второй работе в компании Transoft я работал над текстовым редактором и получил отчет об ошибке от команды контроля качества о “бесконечном цикле” при нажатии правой кнопки мыши для вызова контекстного меню. Я не мог воспроизвести это, поэтому попросил их показать мне. Оказалось, что “бесконечный цикл” возникал, когда они щелкали правой кнопкой мыши в другой области экрана и ожидали, что это приведет к закрытию меню. Но программа работала как положено, закрывая первоначальное меню и открывая новое в новом месте щелчка. Таким образом, “ошибка” на самом деле была просто ошибкой в ожиданиях.
2. Воспроизведите ее снова
Отлично, вы воспроизвели проблему. Но действительно ли это так или это было просто совпадение? Ошибки иногда могут быть продуктом многих взаимосвязанных факторов, и если у вас есть только одна точка отсчета, вы не можете быть уверены, что понимаете основную причину (причины).
Повторное воспроизведение может исключить возможность того, что в первый раз вы допустили глупые ошибки, и повысить уверенность в том, что вы на правильном пути. Уверенность, если она подкреплена честностью, — ваш лучший друг в этом процессе. Но уверенность — это нежный цветок, и вы должны защищать его любой ценой. Не позволяйте ничему растоптать вашу уверенность.
Если вы знаете, как воспроизвести проблему, знаете ли вы также, как ее не воспроизводить? Иными словами, знаете ли вы, какие переменные играют важную роль в определении возникновения проблемы?
Экспериментируйте с этими переменными, изменяйте их и доказывайте их значимость. Это может привести к сокращению шагов по воспроизведению, что абсолютно необходимо сделать на данном этапе. Недостаточно того, что вы можете надежно воспроизвести проблему, вы хотите изолировать ее до наименьшего количества шагов или наименьшего количества данных.
Теперь вы находитесь на том этапе, когда можно посмотреть на код и попытаться понять, что не так, потому что теперь вы действительно понимаете суть проблемы.
Примените свои знания о действующих переменных к системе, которая находится перед вами. Какой код управляет этими переменными? Как они взаимодействуют? Если есть код, который вы не понимаете, попробуйте найти человека или команду, которые над ним работали. Они смогут сократить ваш путь к просветлению и, возможно, даже уже сталкивались с проблемами, подобными вашей.
Иногда код поступает из непрозрачных сторонних источников. Если у вас нет доступа к этим источникам, у вас все равно есть пути для расследования. Прочитайте справку по API или другую документацию, поищите в базе данных ошибок, если таковая имеется. Есть ли соответствующие вопросы на Stackoverflow или в других местах?
В начале 2000-х годов я работал над прикладным фреймворком, который был бинарником для Internet Explorer 6. Это означало использование ряда API IE и Windows, которые имели скудную документацию. Всякий раз, когда реальность не совпадала с нашими ожиданиям от этих API, мы прибегали к поиску ответа в Usenet или на других онлайн-форумах. Чаще всего, когда мы в конце концов находили правильный ответ, его размещал таинственный гений с именем “Игорь Тандетник”. Прошло немного времени, прежде чем мы начали по умолчанию добавлять ко всем нашим поисковым запросам “Игорь Тандетник”. В качестве ускорителя отладки это полностью оправдало себя.
5. Наблюдайте за состоянием
После рассуждений о коде в его статической форме, посмотрите на динамическое состояние памяти в момент возникновения проблемы (до, во время и после).
Как вы это сделаете, зависит от вас. В начале своей карьеры я предпочитал использовать отладчик, но в наши дни я чаще всего просто вывожу значения на консоль. Отладчик — это здорово, но для определенных классов проблем (например, параллелизм, события пользовательского интерфейса) они представляют собой наблюдение как взаимодействие; попадание в точку останова может само по себе изменить условия кода, который вы пытаетесь отладить. Ведение журнала может быть более надежным отладчиком в таких условиях. И наоборот, чтение логов быстро надоедает, если ваш проект медленно компилируется. Выбирайте то, что лучше всего подходит для конкретных условий.
Логи также помогут вам на этом этапе, не забывайте обращаться к ним. В идеале ваши логи структурированы и доступны для поиска, поэтому вы можете легко устранить шум, используя соответствующие условия запроса. Если вы не знакомы с инфраструктурой ведения производственных журналов, найдите кого-нибудь, кто знаком с ней, и попросите их ввести вас в курс дела.
Какой бы метод вы ни использовали, есть два типа состояний, которые вас интересуют: пути, по которым следует код, и сохраненные значения. Обязательно посмотрите и то, и другое.
6. Запишите то, что вы (как вам кажется) знаете
Записывание информации на бумаге или в электронном виде может быть удивительно эффективным методом анализа. Он работает на два фронта: заставляет вас активно обдумывать то, о чем вы пишете, а затем служит в качестве памятки при просмотре информации в ваших заметках.
Постарайтесь не поддаваться искушению преждевременно найти решение в этих заметках. Если преждевременная оптимизация — корень всех зол (или, по крайней мере, большинства из них), то преждевременная разработка решений — корень всех неправильно диагностированных ошибок (или, по крайней мере, большинства из них). Сосредоточение внимания только на тех вещах, которые, по вашим наблюдениям, являются безусловно верными, поможет держать под контролем ваши предположения и предубеждения.
Заставляйте себя делать записи, как только вы начинаете исследовать проблему, даже если кажется, что она может быть тривиальной. В худшем случае вы сможете выбросить их, если они не пригодятся. Также может быть полезно записывать их в публичном месте, чтобы другие люди могли воспользоваться тем, что вы узнали, и, возможно, внести свои предложения по проблеме, над которой вы работаете. Прозрачность — это суперсила.
Всякий раз, когда я отлаживаю продакшн-инциденты или просто выполняю рутинное обслуживание прод-инфраструктуры, я начинаю новую тему в Slack в нашем канале #devops и веду там оперативные записи. По крайней мере, эти темы служат публичной записью всего, что я делал или наблюдал, с привязкой к временной метке. В дальнейшем, инженеры могут найти их с помощью поиска и вернуться к ним, если подобные сценарии возникнут снова. Но не раз они также служили толчком для полезного обсуждения того, над чем я работаю. Благодаря этим тредам мы быстрее устраняли проблемы.
7. Исключите некоторые вещи
Иногда полезно удалить куски кода, чтобы доказать, что они не связаны (или нет). Это можно сделать по двум направлениям: по времени и по функциям.
Временной подход означает использование контроля исходного кода для постепенного определения набора изменений, в который была внесена ошибка. Если вы используете git, то git bisect существует именно для этой цели. Это отличное оружие в вашем арсенале, и вам следует ознакомиться с ним, если вы еще не знакомы с ним.
Feature-based означает посмотреть на код и физически удалить его части самостоятельно. Удалите его, закомментируйте, используйте условную компиляцию, что угодно. Это проверка ваших предположений. Убедитесь, что вы делаете маленькие шаги, следуя этому подходу. Слишком легко изменить множество вещей одним махом и потом не знать, какая из них отвечает за наблюдаемые эффекты.
Если вы слишком долго концентрируетесь на одной и той же проблеме, ваш мозг “затуманивается”, и вы становитесь менее эффективным. В этот момент лучше всего пойти погулять, но часто бывает трудно понять, когда для этого настало время. Старайтесь сознательно анализировать свою работу всякий раз, когда вы отходите от рабочего места. Будьте честны в своей оценке.
Мне повезло, что у меня есть собака Майло, которая заставляет меня прекращать работу через регулярные промежутки времени, чтобы мы могли поиграть или погулять. Эти прогулки иногда являются самой продуктивной частью моего дня, количество случаев, когда во время прогулки приходит свежая идея, просто поразительно. (Самое главное, чтобы вам никто не мешал думать, когда вы гуляете :)) Если это не полное решение, то это может быть какая-то его часть или теория, которая продвигает меня на шаг ближе.
Дело в том, что ваш мозг не перестает работать над проблемой только потому, что вы перестали активно думать о ней. Он все еще там, работает в фоновом режиме. Дайте ему немного передышки, чтобы сделать свое дело.
Хотя переписывание целых систем редко бывает хорошей идеей, переписывание небольших фрагментов функциональности может быть эффективным способом выявить факторы, которые часто могут быть скрыты от глаз. Иногда вы можете смотреть на код целую вечность, и он выглядит прекрасно, но как только вы попытаетесь переделать его на свой лад, вы столкнетесь с костылями, на которые пришлось пойти автору. Эти костыли — отличный источник моментов “ага!” для отладки.
Важно отметить, что вы не стремитесь заменить код, который вы переписываете. План состоит в том, чтобы отбросить ваш код после того, как он выполнит свою работу, которая заключается исключительно в том, чтобы помочь вам понять. Иногда вам может повезти, и вы обнаружите, что исправление вашей ошибки скрывается в коде “замены”, но лучше не ставить перед собой такую цель, так как это может отвлечь вас от реальной задачи.
10. Напишите неудачный тест
Если и есть одно замечание, которое бросают мне чаще других, как в качестве комплимента, так и в качестве критики, так это то, что я пишу много тестов (для некоторых людей слишком много). Но есть один вид тестов, в котором я абсолютно не иду на компромисс, и это регрессионные тесты. Они подобны технологическому антидолгу, сложным процентам, которые выплачивают все большие суммы по мере накопления их в вашем проекте.
Каждый раз, когда вы исправляете баг, вы должны добавлять как минимум один новый тест в ваш набор регрессионных тестов. То, что в программных проектах идет не так один раз, часто идет не так и во второй раз. Молния бьет дважды. Самый простой способ справиться с этим — писать регрессионные тесты по ходу работы. А самый простой способ убедиться в том, что ваши регрессионные тесты действительно работают, — это написать сначала неудачный тестовый пример, прежде чем приступить к его исправлению.
Написание подобных тестов также является хорошим способом убедить любых специалистов, не склонных к тестированию, внести свой вклад в тестируемый код. Им будет гораздо труднее отказаться по причине нехватки времени или усилий, если вы попросите всего один тест в их PR. Дюйм за дюймом вы сможете подтолкнуть их в направлении лучших привычек.
В конце концов, вы поймете проблему настолько хорошо, что один или несколько способов ее решения откроются вам. Если вы в глубине души знаете, в чем заключается единственное верное решение, то можете смело использовать его, и никаких проблем. Но в других случаях дальнейший путь будет менее ясен, и в таких случаях вам следует проявлять инициативу, собирая больше мнений. Не думайте о неуверенности как о признаке слабости; напротив, ваша готовность обсуждать ее — признак силы. И все эти обсуждения будут направлены на устранение будущих ошибок, делая вас более сильными для решения проблем, которые ждут вас впереди.