Comments 70
Хочу дополнить по тому когда стоит применять подход — если логика вашего приложения зависит от большого количества точек возможного отказа + получить логи намного проще чем воспользоваться удаленной отладкой/собрать краш дамп.
В качестве альтернативы 2 минусу «в продакшене много логов» — можно применить подход «ключ/опция для включения трейс-логов». Если проблема воспроизводится, но только в среде клиента — просим его запустить с определенной опцией.
4 минус вообще не понял — речь о ревью?
Спасибо за мнение.
В C# тоже дефайны есть.
Но для C# есть и другой вариант: писать Log.Trace?.Text(...)
Если объект Log.Trace == null
, то выражение не будет вычисляться, тем самым сохраняя драгоценное cputime.
А как дефайны решают эту проблему?
для дебага — не делаешь.
Проблему захламлённости кода это не решает, код как был захламлённым, так и остался.
Фолдить так, чтобы от кода оставалась только одна строчка и можно было щёлкнуть на плюсик рядом с ней, чтобы раскрыть весь блок?
мне нравится, когда просто полоска со значком.
вот например так в jetbrains продуктах:
blog.jetbrains.com/idea/2012/03/custom-code-folding-regions-in-intellij-idea-111
www.jetbrains.com/help/idea/code-folding.html
В целом давно ждал подобную статью, сам довольно много размышлял над подходами к отладке — и выбрал для себя именно отладку логированием.
Запись ненужных данных это не только место на диске, но и время CPU, и время iowait.
И я предложил:
Это лечится логированием по флагу,
В ответ на что и было дано то сравнение с изменением исходников, которое мне и показалось некорректным. Что перекомпиляция, что изменение env — все не то, о чем я говорил изначально, это ложная дихотомия. Указанием на это и является мое «при чем тут».
В ответ на что и было дано то сравнение с изменением исходников, которое мне и показалось некорректным. Что перекомпиляция, что изменение env — все не то, о чем я говорил изначально
Это не я писал про изменения исходников, но…
На самом деле, флаги, изменяющие поведения продакшена, по хорошему всегда опасны. Даже всего-то включение логирования может привести к замедлению работы (из-за большого оферхеда на запись логов), переполнению памяти (из-за создаваемых отладочных объектов), другую работу многопоточности и т.п. В чем-то изменение исходников предпочительнее, передеплой, по крайне мере, достаточно заметная процедура и требует прохождения тестов и проверок.
Представьте — один программист включил логирование для поиска мелкой баги, а второй программист (может совсем в другой команде) получил супер критичную багу о том что продакшен стал работать в десять раз медленее. И пока одни в мыле пытаются понять, что же с продакшеном случилось, другие не торопясь разбирают логи.
Конечно, может быть ситуация когда критическая бага, которая воспросизводится только на проде и такое логирование меньшее из зол, но это лечится хорошим кодом, правильной отладкой и архитектурой. В конце концов, тестовой копией продакшена, куда копируются все запросы.
Объясните, пожалуйста, как вы с помощью одного SIGTERM перезапустите процесс без сброса открытых соединений?
И, да, сильно с докером коррелирует. Явной причинно-следственной связи нет, но докер чаще всего используется в реализациях подхода. И этот подход не предполагает перезапуска процесса на той же физической или виртуальной машине. Может даже в другом датацентре она запустится в среде не предполагающей перкидывание соединений с хоста на хост.
Но мы уже ушли от темы. Я вам ответил как я в своей среде реализую включение вывода в лог нужной информации, без перезапуска демона. Вы работаете в другой среде, у вас другой механизм.
Навскидку, просто в лоб — есть watchdog, который с некоей периодичностью проверяет наличие файла с определенным именем и при обнаружении оного взводит глобальную логическую переменную.
Почему-то нет в минусах уменьшения производительности на продакшене.
Грамотно сделанные логи практически не влияют на производительность. Для Java — см. log4j2 в асинхронном режиме. Для .Net — NLog (тоже в асинхронном режиме). Что важно — часть используют для той же Java slf4/log4 (не log4j2, это важно), которые просто не умеют в асинхронность. А значит каждый вызов log.info приведет к записи на диск.
Более того, если мы делаем Web сервис, то легко можно организовать систему, когда логи копятся в памяти, пока работает request. И сбрасываются на диск после него. Тут одновременно и буфферизация и асинхронность. Однако важно — это для .Net/Java, по остальным платформам не подскажу.
Грамотно сделанные логи практически не влияют на производительность.
В некоторых случаях влияют, если их ставить не подумав. У меня как попало логирование в цикл где перебирались миллиарды и триллионы записей (данные были очень близки к BigData, но все-таки за ночь успевали обработаться на одном сервере), деградация производительности оказалось раза в 3, просто никакая асинхроность не поместит такую массу данных в памяти (мы данные специально брали в архиве, на лету разархивировали, обрабатывали и снова архивировали — это давала значительные прирост в скорости за счет записи и чтения на диск, а логи, естественно, писались без архивирования).
В общем, при реально высокопроизводительном приложении ( не при обычных web servic'ах) — нужно быть очень внимательным с логированием, оно, кстати, и место на диске забивает влет, если не подумать.
используют для той же Java slf4/log4 (не log4j2, это важно), которые просто не умеют в асинхронность.
Насчёт log4j согласен, но чего ради slf4j обвиняется в неумении?
slf4f — это просто фасад, набор интерфейсов, за которыми может скрываться какая угодно их имплементация. Например, авторский logback умеет быть асинхронным.
Не раз делал отладку в приложениях с несколькими потоками, например при взломе денег/здоровья/оружия в играх. Расставляем брекпойнты, в пределах одной-двух функций шагаем по F10, потом запускаем по F9 до следующего брекпойнта. Хардварные брекпойнты на запись тоже полезная штука.
Можете описать какой-то конкретный кейс?
Например, попытка понять, почему при обработке изображения в 1 поток всё хорошо, а в при использовании parallel_for на изображении иногда появляется мусор. Точнее, почему — ответ очевидный (второй поток начал использовать ещё не вычисленные значения), а нужно понять, что сделать, чтобы этого избежать, не потеряв при этом в производительности.
Или другой пример: отладка протокола общения с какой-нибудь железкой, которая шлёт пинг-пакеты, на которые клиент должен сразу же отвечать. Если начать отлаживать по шагам, то возникнут большие задержки, и железка решит, что вы отвалились по таймауту.
Расставляем брекпойнты, в пределах одной-двух функций шагаем по F10, потом запускаем по F9 до следующего брекпойнта.
Дело в том, что пока вы шагаете по функции, остальные потоки тоже работают.
И если вы работаете с общими данными, то возможно всякое.
А как вы этот поток найдете?Примерно вот так:

Как узнаете какое из множества мест, в которых мутируют данные вам нужно?А как вы узнаете, в какое из множества мест логирование добавить? Если точное место возникновения ошибки неизвестно, можно поставить на все места. Туда же, куда мы бы добавили логирование.
Но вообще я говорил о случае, когда ошибка стабильно воспроизводится. То есть можно сказать, что в этом месте ошибки еще нет, а в этом уже есть. Ставим несколько точек в разных местах, между ними запускаем, чтобы работали все потоки. Если есть возможность поставить брекпойнт на изменение данных, то еще лучше, остановится сразу в нужном потоке.
к моменту ошибки данные УЖЕ изменены, и бряк ставить просто некудаИзвините, эту часть я не понял. Код никуда не девается, ставить надо заранее.
Примерно вот так
Сдается мне этот способ настолько трудозатратен, что всерьез его рассматривать банально не стоит :)
А как вы узнаете, в какое из множества мест логирование добавить?
В том и дело, что логгирование добавляется при написании кода, а не после возникновения ошибки. То есть я не буду узнавать «в какое из множества мест добавить лог» — я его просто добавлю во множество мест, и получу вполне ясную, хоть и весьма объемную картину происходящего.
Но вообще я говорил о случае, когда ошибка стабильно воспроизводится. То есть можно сказать, что в этом месте ошибки еще нет, а в этом уже есть. Ставим несколько точек в разных местах, между ними запускаем, чтобы работали все потоки. Если есть возможность поставить брекпойнт на изменение данных, то еще лучше, остановится сразу в нужном потоке.
Не поможет в случае, когда данные сломаны в момент T, а в виде ошибки проявляются в момент T+сколько-то итераций. Придется идти вверх по коду, в поисках места, где данные были поломаны, а таких мест может быть много. Стабильность воспроизведения ошибки тут мало что дает.
Извините, эту часть я не понял. Код никуда не девается, ставить надо заранее.
Так куда заранее? Данные пришли, скажем по сети, в виде websocket-сообщения, были преобразованы, разложены в разные структуры данных, которые прошли многопоточный конвеер обработки и преобразования, протиснулись сквозь разные очереди и преобразователи, и на выходе вы обнаруживаете, что там, где ожидается не ноль — у вас ноль. При чем не всегда, а в одном случае на миллион входных сообщений. Имея лог вы возьмете id поломанного сообщения, и сможете отследить весь процесс преобразования в обратную сторону, и найдете место, где возник этот ноль. В этом месте может быть даже ошибки не будет — ошибка будет еще раньше. И вы продолжите идти по логу вверх… Как в таком виде будет выглядеть разбор бряками? Поставил бряк с условием «если ноль», запустился, дождался останова, пошел смотреть, что на входе в метод, пошел выше, поставил бряк с условием… смыть, повторить. Или не так?
Сдается мне этот способ настолько трудозатратен, что всерьез его рассматривать банально не стоит
Я там специально активировал окно со списком потоков. Что там трудозатратно? Посмотреть список, переключиться двойным кликом? В Visual Studio такое же окно есть, в других отладчиках думаю тоже.
То есть я не буду узнавать «в какое из множества мест добавить лог» — я его просто добавлю во множество мест, и получу вполне ясную, хоть и весьма объемную картину происходящего.
С брекпойнтами абсолютно аналогично. Ставим везде где надо, проверяем значения переменных.
Не поможет в случае, когда данные сломаны в момент T, а в виде ошибки проявляются в момент T+сколько-то итераций. Придется идти вверх по коду, в поисках места, где данные были поломаны, а таких мест может быть много. Стабильность воспроизведения ошибки тут мало что дает.
То, что вы описали, это и есть стандартная работа с отладчиком. Он можно сказать и предназначен, чтобы делать такие вещи проще. Стабильность воспроизведения наоборот важна, она дает возможность перезапустить выполненение, поставив точки в другие места, если вдруг пропустил момент появления ошибки.
И первая точка ставится на момент (T минус сколько-то итераций). Он прикидывается исходя из логики. Если не угадали, ставим еще раньше, перезапускаем. И нет, на это уходит меньше времени, чем читать кучу логов.
При чем не всегда, а в одном случае на миллион входных сообщений.
Вот именно поэтому стабильность воспроизведения важна. Важно знать, что вот это сообщение или группа сообщений дают ошибку.
Имея лог вы возьмете id поломанного сообщения, и сможете отследить весь процесс преобразования в обратную сторону, и найдете место, где возник этот ноль.
И точно так же можно взять id поломанного сообщения, отследить логику его обработки в отладчике, и найти место, где возникает этот ноль.
Как в таком виде будет выглядеть разбор бряками?
Ставим бряк в процедуру приема сообщения, в процедуру возврата результата, в процедуры, которые работают с этим полем структуры данных.
Запускаем, отправляем сообщение, которое обрабатывается неправильно, останавливается на приеме сообщения. Смотрим, на входе пришло 100.
Запускаем выполнение дальше, останавливается на процедуре разложения в структуры. Проверяем, раскладывается правильно.
Запускаем, останавливается на выходе из конвейера. Проверяем, обработано правильно.
Запускаем, останавливается на выходе из преобразователя. Проверяем, вот он ноль появился.
Проверяем код преобразователя. Ага, в коде написано $x -= $x;
вместо $x -= $y;
.
Или если не нашли, расставляем бряки в преобразователе, перезапускаем, повторяем.
При этом можно смотреть не только то, что залогировано, а вообще весь контекст — переменные, конфиги, родительские объекты, где-то даже методы вызывать.
И это если код не менять. А если менять, то можно раскидать по коду проверки if ($obj->field == 0) { ... }
и поставить бряки в тело условия. Тогда сразу можно найти, где появляется ноль. Похоже на логирование, только логгер в зависимости пробрасывать не надо, логировать все не надо, тестировать этот код не надо, и на продакшн он не влияет, потому что коммитить его не надо.
Всё бы ничего, но:
Анализируем отдельные части (придумываем для них невалидные ситуации, граничные случаи)
Бывают случаи, когда ты уже не можешь представить, что могло пойти не так, у тебя кончилась вся фантазия, ты знаешь что результат не верный, но не можешь найти причину. Вариант "придумывать" уже не катит, нужно ставить дебаги, следить за потоками данных.
Но все же не выкидываем Дебаг. И то и то полезно.
Но в целом, именно хорошее логгирование позволит понять почему именно в проде через неделю-две что то падает. Мультипоточность, утечки памяти, и прочие вещи связанные с CAP теоремой ловятся часто только через логи. НО. Часто возникают ситуации, когда надо идти и смотреть в ключевых точках что происходит, и от этого никуда не деться.
Мир не черно белый. Нет одного правильного подхода. Серединный путь, как говорил еще Будда про ИТ разработку.
И все свойства меняются, порядка 200 изменений(связных) за пару минут в рамках одной бизнес-операции. И в редких редких случаях одно-два свойства оказываются не в том состоянии.
Ещё раз, изменение свойств — связное. Т.е. при изменении свойства А может быть изменено свойство Б и иногда В. Логгировать изменения абсолютно всех свойств никто не будет, более того, некоторые значения нельзя логгировать по требованиям ИБ.
И тут, кроме отладки, вариантов почти не остается. Есть ещё второй способ — обратное разворачивание всей этой операции. Ну т.е. идти от обратного. Свойство В оказалось таким-то, какие предпосылки могли быть, а как мы могли придти к ним и так далее.
При этом, как и все хорошие практики, предложенный подход имеет некоторые недостатки (точнее, это не недостатки, а следствие состояния индустрии):
Такие практики ведут к тому, что эффект от них проявляется в средне- и долгосрочной перспективах.
Если постараться — эффект можно получить даже в ближней перспективе.
Но руководству фичи нужны здесь и сейчас, даже если потом все сломается и/или будет глючить, и время и цена сопровождения фич растянутся на годы.
Отсюда следуют и другие моменты.
Например: «Коллегам приходится смотреть не только на сам алгоритм, но и на трассировку его выполнения».
Т.е., коллегам для работы с вашей фичей нужно будет соблюдать определенную методологию.
Будут ли они ее соблюдать, тем более в ситуации, когда это от вас не зависит (например, опять же, когда от них будут требовать быстрых выдачи фич на гора и закрытия тасок)?
Ну и если уж заговорили о методологии, то, если соблюдать определенные методологии (т.е., если в принципе их получится соблюдать на проекте) на самом раннем этапе — этапе проектирования и написания кода — условно, тот же SOLID (до кучи можно о многом поговорить — и о понимании сложностей O(N), иммутабельности и прочем), то необходимость в отладке (дебаге) и логгировании драматически сокращается.
Хотя конечно, это не отменяет отладку и логгирование как таковые.
1. Написать тесты,
2. Добавить логи,
3. Включить отладку,
Во-вторых, судя по всему отладку просто используют неправильно, отладка нужна для того, чтобы написал тесты, написал код к этим тестам — проверил работоспособность, не работает — отладил. Бессмысленно добавлять тонны логов, которые после того как код будет написан — не понадобятся.
Отладка зло только если пытатесь отлаживать сразу все приложение, когда вы отлаживаете один unit тест — она. позволяет писать код в разы быстрее, чем поиск ошибок через логирование.
В-третих, логирование увеличивает кол-ва кода в приложении и «загрязняет» бизнес код, что идет в разрез сразу многим принципам чистого кода, не пишите того что вам не нужно и т.д. Логирование должно быть только там где оно потенциально нужно и важно, иначе у вас логирования ненужного поведения будет больше чем собственно кода. По хорошему, вместо логирования стоит использовать всякие AOP и т.п. техники, чтобы не забивать собственно код приложения.
1. Логгирование типовыми/хорошими библиотеками есть настраиваемое (см комменты, но не код в статье)
2. Выкидывать дополнительный способ отладки — зачем? Давайте лучше отрежем руку.
3. Классика DbC и подобные техники. Рекомендую почитать возможности D
P.S. Асинхронное логгирование в многопоточке — то еще гадание на кофе
В реальном продакшне производительность может страдать, поэтому флаг/опция на уровень логгирования обязателен.
Еще у всяких фреймворков типа NLog есть оверлоады на лямбды применимые даже к вашему примеру:
_log.Trace($«Privilege: {privilege} not added to User: {user}»);
user тут объект, т.е. ожидаю что в нём перекрыт ToString который может быть еще довольно большой строкой. Также довольно удобно просто писать что-нибудь типа JsonConvert.Serialize(user), чтобы в логах почитать можно было. Поэтому лучше писать это так:
_log.Trace(() => $«Privilege: {privilege} not added to User: {user}»);
Если совсем все плохо в продакшене и требования ИБ это допускают — подключается через специальный адаптер Amplitude с ее возможностями user path отслеживать.
Конечно, если кто направит на путь истинный, буду благодарен
Просто залогируйте состояние нужных вам объектов при определённых условиях на определённой итерации цикла.
Ага, потом на чтение и поиск нужной строчки среди записей о success-варианте уйдет больше времени, чем весь цикл вручную с F10 пробежать.
_log.Trace($"Remote role: {roleInOtherService}")
Как логирование поможет узнать причину, почему roleInOtherService = null
?
Почему-то априори предполагается, что на отладку уходит больше времени. Наоборот, не раз замечал, если с var_dump() за 1-2 раза не получилось найти причину, лучше сразу отладчик включить.
Не надо противопоставлять логи и отладчик. Они дополняют друг друга. Логи помогают установить факт наличия ошибки в логике выполнения, а пошаговая отладка помогает найти ее причину. Анализируя логи, мы тоже делаем отладку, только мысленно. Если наше представление о коде неправильное или недостаточное, то такая мысленная отладка может не дать результата.
Логирование как способ отлаживать код