Руководство по решению проблем вашего проекта проще

Часто при отладке нам нужно выйти за пределы удобных объятий IDE, чтобы воспроизвести или отследить проблему. В этой серии я хотел бы рассказать о некоторых инструментах, которые могут оказаться полезными для этих случаев. Я постараюсь ограничиться инструментами, которые на 100% являются инструментами отладки, а не теми, которые полезны для тестирования разработки.

Например, такие инструменты, как curl или jq, очень полезны. Вы можете и должны использовать их при отладке. Но вы, вероятно, использовали их при создании и тестировании функции, поэтому вы уже знакомы с ними и должны иметь некоторое представление о том, что они делают. Я хочу сосредоточиться на инструментах, которые вы чаще всего используете при отладке. В этом смысле инструменты вроде SDKMan и т.п. здесь также не имеют смысла.

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

Инструменты, которые я расскажу в этой серии, охватывают следующие категории:

  • Инструменты системного мониторинга — как в случае со strace и DTrace, о которых мы сегодня поговорим
  • Сетевые мониторы — также входят в приведенную выше группу, но требуют отдельной категории.
  • Мониторинг VM/Runtime — например, инструменты, которые позволяют нам проверять JVM и т. д.
  • Профилировщики и мониторы памяти

В этой первой статье я хотел бы обсудить двух чемпионов в супертяжелом весе: DTrace и strace. Если вы разработчик Java или пользователь Windows, скорее всего, вы никогда не слышали об этих инструментах. Вы могли непреднамеренно использовать один из них, так как на их основе было создано так много инструментов, но, вероятно, это не так.

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

DTrace

Еще в 2004 году я впервые услышал о DTrace, работая в Sun Microsystems. Это стало модным в коридорах, поскольку это была инновация, которую продвигала Sun Microsystems. Позже DTrace был перенесен в MacOS X (он возник на Solaris). Сегодня есть порты и на Windows, и на Linux.

DTrace — это мощный низкоуровневый фреймворк для динамической трассировки. Но это просто еще одна превосходная степень, и если вы никогда не использовали такой инструмент и не имеете опыта системного программирования, вы можете чувствовать себя немного сбитым с толку: какого черта он делает?

Он позволяет «видеть» все. Хотите знать, какие файлы были открыты процессом?

OK.

Хотите знать, кто вызывает API ядра и передает вызывающей стороне трассировку стека?

OK.

Хотите знать, почему процесс умирает?

OK.

Хотите знать, сколько процессорного времени тратится на операцию?

OK.

Вы можете подумать, что DTrace — это один из тех инструментов, который полностью уничтожит ваш процессор… но вот убийственная особенность: он достаточно быстр, чтобы работать в производственной среде с минимальным влиянием на производительность или вообще без него!

Это было революционно, когда оно было запущено почти два десятилетия назад, и оно остается актуальным и по сей день!

Запуск DTrace

Прежде чем мы начнем, слово предупреждения. Сохраните ваши данные!

Этот инструмент может легко привести к сбою вашего компьютера, и для его включения требуется отключить важные средства безопасности в MacOS. Это рискованная системная служба «низкого уровня», и с ней следует обращаться соответствующим образом.

На Mac DTrace конфликтует с «Защитой целостности системы», которая представляет собой функцию безопасности, которая блокирует некоторые взаимодействия между процессами (среди прочего). В обычных условиях это было бы здорово. Но если вы хотите запустить DTrace, это будет проблемой.

Решение загружается в режим восстановления на компьютерах Intel Mac; это означает удержание клавиш Command-R при загрузке. На ARM Mac просто нажмите и удерживайте кнопку питания.

Затем в терминале режима восстановления введите команду: csrutil disable.

Затем после перезагрузки DTrace должен работать как положено.

Основное использование

Как упоминалось ранее, DTrace — очень мощный инструмент. Об этом написаны целые книги. У него есть собственный язык программирования, основанный на синтаксисе C, который вы можете использовать для создания сложной логики. Например, следующая команда будет регистрировать некоторую информацию из заданных обратных вызовов:

sudo dtrace -qn 'syscall::write:entry, syscall::sendto:entry /pid == $target/ { printf("(%d) %s %s", pid, probefunc, copyinstr(arg1)); }' -p [PID]

Фрагмент кода, переданный команде DTrace, прослушивает обратный вызов sendto для идентификатора целевого процесса. Затем выводит информацию на консоль, например: (pid) text

Если вам кажется, что это слишком много и слишком сложно для начала… вы правы на 100%. Это мощный инструмент, когда он вам нужен. Но для большей части нашего повседневного использования он слишком мощный. Мы хотим знать немного базовых вещей, а этого слишком много!

Простое использование

Как назло, у нас есть простое решение:

man -k dtrace

Это распечатывает список инструментов, которые стоит прочитать, чтобы понять, насколько обширна эта вещь. Вот несколько интересных строк вывода этой команды:

bitesize.d(1m)           - analyse disk I/O size by process. Uses DTrace
dapptrace(1m)            - trace user and library function usage. Uses DTrace
errinfo(1m)              - print errno for syscall fails. Uses DTrace
iotop(1m)                - display top disk I/O events by process. Uses DTrace
plockstat(1)             - front-end to DTrace to print statistics about POSIX mutexes and read/write locks

Стоит потратить время на просмотр этого списка, чтобы понять, что вы действительно можете здесь сделать.

Примеры

Вы сталкиваетесь с проблемами записи на диск с повышенными правами, которые приводят к снижению производительности вашего приложения… но виновато ли это ваше приложение или какое-то другое приложение?

Просто беги:

sudo rwbypid.d

Он распечатает чтение/запись на диск:

PID CMD                       DIR    COUNT
  2957 wordexp-helper              W        1
  2959 wc                          W        1
  2961 grep                        W        1
... snipped for clarity ...
   637 firefox                     R     6937
   637 firefox                     W    15325
   343 sentineld                   W   100287

Программное обеспечение безопасности действительно снижает производительность…

Вы также можете использовать bitesize.d, чтобы получить более конкретные результаты о количестве записанных/распределенных байтов.

Хотя это довольно высокий уровень. Что, если вам нужны подробности: имя файла, имя процесса и т. д.?

sudo iosnoop -a

Распечатывает вывод, который включает почти все, что вам нужно:

STRTIME              DEVICE  MAJ MIN   UID   PID D      BLOCK     SIZE                     PATHNAME ARGS
2022 Jun 30 12:16:56 ??        1  17   501  1111 W  150777072     4096 ??/idb/3166453069wcaw.sqlite-wal firefox\0
2022 Jun 30 12:16:56 ??        1  17   501   661 W  150777175   487424  ??/index-dir/the-real-index Slack Helper\0
2022 Jun 30 12:16:57 ??        1  17   499   342 W  150777294     4096 ??/persistent/.dat.nosync0156.ztvXap sentineld\0

Я вижу идентификатор процесса и сколько байт он записал в конкретный файл!

Допустим, ваша программа охватывает процессы, и вы хотите увидеть, что происходит. Например, я запускаю сборку исходного кода на построенном мной сервере:

sudo errinfo

Позволяет мне обнаружить ошибку, возвращаемую системными вызовами, и команду, которая первоначально вызвала это:

EXEC          SYSCALL  ERR  DESC
    WindowServer workq_kernreturn   -2 
    WindowServer workq_kernreturn   -2 
   SentinelAgent workq_kernreturn   -2 
   SentinelAgent workq_kernreturn   -2 
          Signal           Helper    0 
          Google           Chrome    0 
           Brave          Browser    0 
          Google           Chrome    0

Это только вершина айсберга. Я предлагаю ознакомиться с этим старым учебником по DTrace от Oracle или книгой. Отказ от ответственности: я не читал книгу.

стрейс

Забавно, но в данном случае в 90-х годах в Sun Microsystems также появился инструмент strace. В этом нет ничего удивительного, поскольку список технологий, разработанных Sun Microsystems, просто ошеломляет.

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

Strace включен благодаря функции ядра, известной как ptrace. Поскольку ptrace уже есть в Linux, нам не нужно добавлять дополнительный код или модули ядра. Как правило, DTrace требует более глубокой поддержки ядра, чтобы обойти проблемы с лицензированием в Linux, он находится в отдельном загружаемом модуле, но это все еще создает некоторые проблемы.

Работа со strace похожа на печать записи журнала каждый раз, когда мы делаем вызов ядра. Это создает очень подробный журнал для каждой выполняемой вами команды. В результате вы можете следить за тем, что на самом деле происходит под капотом запущенного процесса.

Беговая дорожка

В настоящее время strace широко используется в Linux, это мой любимый инструмент диагностики системы на этой платформе. С ним очень удобно работать, так как мы можем запускать его без особых привилегий. Обратите внимание, что, в отличие от DTrace, вы должны держать strace отдельно от рабочих сред (если только код не отделен). Это влечет за собой значительные накладные расходы на производительность и может привести к выходу из строя производственной системы.

Самое простое использование strace — это просто передача ему командной строки:

strace java -classpath. PrimeMain

Вывод strace для этого довольно длинный. Пройдемся по нескольким строчкам:

execve("/home/ec2-user/jdk1.8.0_45/bin/java", ["java", "-classpath.", "PrimeMain"], 0x7fffd689ec20 /* 23 vars */) = 0
brk(NULL)                               = 0xb85000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0294272000
readlink("/proc/self/exe", "/home/ec2-user/jdk1.8.0_45/bin/j"..., 4096) = 35
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/tls/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/tls/x86_64", 0x7fff37af09a0) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/tls/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/tls", 0x7fff37af09a0) = -1 ENOENT (No such file or directory)

Каждая из этих строк является системным вызовом Linux. Мы можем погуглить каждый из них, чтобы понять, что происходит. Вот простой пример:

open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/tls/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

Java пытается загрузить библиотеку pthread из каталога tls, используя системный вызов open для загрузки файла. Код выхода системного вызова — -1, что означает, что файла нет. При нормальных обстоятельствах мы должны получить значение дескриптора файла из этого API.

Глядя в каталог, кажется, что каталог tls отсутствует. Я предполагаю, что это из-за отсутствия установки JCE. Это, вероятно, нормально, но в некоторых случаях может быть интересно.

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

strace -e open java -classpath . PrimeMain

Будут показаны только открытые системные вызовы:

open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/tls/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/tls/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/tls/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/tls/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/libjli.so", O_RDONLY|O_CLOEXEC) = 3
open("/home/ec2-user/jdk1.8.0_45/bin/../lib/amd64/jli/libdl.so.2", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

Есть много системных вызовов, которые вы можете изучить и использовать для отслеживания многих действий, таких как: подключение, запись и т. д. Это только верхушка айсберга того, что вы можете делать с помощью strace. Джулия Эванс написала одни из самых исчерпывающих и занимательных постов о strace. Если вы хотите узнать об этом больше, лучшего места, наверное, не найти (также ознакомьтесь с другими ее работами. Удивительные ресурсы!).

Стрейс и Java

Как вы видели ранее, strace отлично работает с JVM. Поскольку strace предшествует Java и является очень низкоуровневым инструментом, он не знает о JVM. JVM работает так же, как и большинство других платформ, и вызывает системные вызовы, которые можно использовать для отладки ее поведения. Однако из-за уникального подхода к некоторым проблемам некоторые аспекты могут быть не столь очевидны при использовании strace.

Хорошим примером являются ассигнования. Системные инструменты используют malloc, который сопоставляется с логикой распределения ядра, но Java идет другим путем. Он управляет собственной памятью для повышения эффективности и упрощения логики сборки мусора. В результате некоторые аспекты выделения памяти будут скрыты от вывода strace. Это может быть замаскированным благословением, поскольку результат иногда может быть ошеломляющим.

На момент написания этой статьи многопоточность хорошо работала со strace. Но это может измениться в будущем, поскольку проект Loom может изменить однозначное сопоставление между потоками Java и системными потоками. Это может затруднить определение вывода strace в многопоточных приложениях.

Окончательно

Существует алфавитный набор утилит «*trace» в различных формах, которые постоянно заимствуют идеи друг у друга. Угнаться за всем этим шумом — серьезная задача. Есть слишком много отличных инструментов, которые нужно охватить, но я хотел бы обсудить btrace в одной из следующих статей. Он очень похож на DTrace, но также очень специфичен для JVM, поэтому, вероятно, стоит отдельного поста.

Инструменты, о которых я говорил сегодня, используют разные подходы к решению схожих проблем: как понять, что «на самом деле» делает бинарное приложение? Исследователи безопасности и хакеры используют эти инструменты, чтобы понять вашу программу. Им не нужен код и дизассемблирование, чтобы увидеть, что вы на самом деле делаете.

Вы также можете использовать эти инструменты, чтобы понять влияние ваших действий. Мы часто вызываем API и на этом все заканчивается. Но дьявол кроется в деталях, а эти детали могут иметь тяжелые последствия. Как Java-разработчик, я редко задумываюсь о доставке сигналов, управлении процессами или других низкоуровневых компонентах. Но я трачу время на изучение этих вещей, поскольку они в конечном итоге влияют на стабильность и производительность моего приложения.