Статьи

Jstack с наддувом: как отлаживать серверы на скорости 100 миль в час

Брейк данс

Руководство по использованию jstack для отладки живых рабочих серверов Java

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

По своей сути jstack — это очень простой инструмент, который показывает вам следы стека всех потоков Java, работающих в целевой JVM. Просто укажите на процесс JVM через pid и получите распечатку всех трасс стека потоков в этот момент времени. Это позволяет вам ответить на давний вопрос «что делает этот сервер?» И приблизить вас на один шаг, чтобы понять, почему он на самом деле это делает. Самым большим преимуществом jstack является то, что он легок — он не добавляет никаких накладных расходов на производительность JVM и не изменяет состояние выполнения (в отличие от отладчика или профилировщика).

Поскольку ничто не идеально , jstack имеет два существенных недостатка. Во-первых, jstack не предоставляет вам никакого другого состояния переменной, кроме стека вызовов, что означает, что, хотя вы, возможно, просматриваете стек, вы не будете иметь представления о том, в каком состоянии он находится там. Хорошим примером будет рассмотрение зависшей JVM, где jstack покажет вам, что большое количество потоков выполняет запросы к БД или ожидает получения соединения.

Это, вероятно, означает, что выполнение некоторых запросов занимает слишком много времени, в результате чего другие потоки либо ожидают соединения, либо отклоняются. Это то место, где вы действительно хотели бы знать, какой запрос выполняется (или каковы его параметры), что вызывает замедление и когда он начался. Это, конечно, только один пример из множества сценариев, в которых некоторые потоки блокируются и снижают пропускную способность вашего приложения. Но, к сожалению, с jstack, так как вы не получаете никакого переменного состояния — вы не можете точно сказать, какой поток виноват. Или ты можешь?

Вторым недостатком jstack является то, что он не всегда включен. Это означает, что вы должны быть там, когда возникает проблема — что в производстве может быть редким событием. Это еще более верно в эластичных средах, где виртуальные машины постоянно перезагружаются.

Здесь есть хорошая часть — давайте рассмотрим два метода, которые могут помочь нам преодолеть эти два недостатка, и сделать хороший инструмент действительно великолепным.

Создание данных потока с сохранением состояния

Первый вопрос: как добавить состояние в распечатку jstack? Ответ простой и мощный — имена потоков. Хотя многие ошибочно считают имя потока неизменным или определяемым ОС свойством, на самом деле это изменчивая и невероятно важная черта, присущая каждому потоку. Это также тот, который попадает в ваш поток jstack, и в этом заключается ключ.

Практическое применение заключается в том, что, подобно регистрации, вы должны контролировать имя потока, как только оно вводится в ваш код через точку входа, такую ​​как сервлет, субъект или планировщик. В этот момент вы захотите установить его имя в значимом значении, которое поможет вам понять контекст выполнения и соответствующие параметры, которые помогут вам изолировать транзакцию и ее содержимое.

Скорее всего, это будет включать —

  1. Назначение потока (например, обработка сообщения, ответ на запрос пользователя и т. Д.).
  2. Идентификатор транзакции, который позволит вам идентифицировать этот конкретный поток данных на разных машинах и в разных частях приложения.
  3. Значения параметров, такие как параметры сервлета или идентификатор удаляемого сообщения.
  4. Время, за которое вы получили контроль над потоком. Этот последний элемент крайне важен для вас, чтобы точно знать, какие потоки в вашем коде застряли, когда вы используете jstack для их наблюдения.
1
Thread.currentThread().setName(Context + TID + Params + current Time,..);

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

«Pool-1-thread-1 ″ # 17 prio = 5 os_prio = 31 tid = 0x00007f9d620c9800 nid = 0x6d03 в Object.wait () [0x000000013ebcc000]

Сравните это со следующей распечаткой темы:

Поток обработки очереди, MessageID: AB5CAD, тип: AnalyzeGraph, очередь: ACTIVE_PROD, Transaction_ID: 5678956, время начала: 08.10.2014 18:34 ″

# 17 prio = 5 os_prio = 31 tid = 0x00007f9d620c9800 nid = 0x6d03 в Object.wait () [0x000000013ebcc000]

То, что вы видите здесь, является гораздо более полным объяснением того, что на самом деле делает эта тема. Вы можете легко увидеть сообщения об исключении из очереди AWS, какое сообщение он анализирует, его тип, идентификатор и идентификатор транзакции. И последнее, но далеко не менее важное — когда поток начал работать над ним. Это может помочь вам очень быстро сосредоточиться на тех потоках, которые застряли, и увидеть состояние, в котором они находятся. С этого момента оптимизация и локальное воспроизведение становится намного более легкой задачей.

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

Запись этой информации в имя потока также помогает при традиционной регистрации. Несмотря на то, что большинство каркасов ведения журналов предоставляют контекст, основанный на потоках, который можно добавить в журнал, вы должны убедиться, что настроили его правильно. Использование имени потока также может гарантировать, что в журнале будут все необходимые данные.

Примечание. Некоторые люди могут сказать, что имена потоков не должны изменяться или изменяться. Я очень мало верю в это, исходя из моего личного опыта многолетнего опыта и опыта многих коллег.

Сделать jstack всегда включенным

Вторая проблема, с которой мы сталкиваемся при использовании jstack, заключается в том, что, подобно отладчику, это инструмент, с которым вам нужно вручную работать в тот момент, когда возникает проблема, чтобы зафиксировать поврежденное состояние. Однако есть более активный способ использования jstack для автоматической генерации распечаток, когда сервер зависает или падает ниже или выше определенного порога. Ключ заключается в том, чтобы программно вызывать jstack так же, как если бы вы выполняли определенные условия приложения, как если бы вы выполняли какие-либо функции регистрации изнутри JVM.
Здесь есть две ключевые проблемы: когда и как вы это делаете.

Как активировать jstack программно?

Поскольку jstack — это простой процесс ОС, вызывать его довольно просто. Все, что вам нужно сделать, это активировать процесс jstack и направить его на себя. Кикер здесь, как получить pid для вашего процесса из JVM. На самом деле нет стандартного Java API для этого (по крайней мере, до Java 9 ). Вот небольшой фрагмент, который выполняет работу (хотя и не является частью документированного API):

01
02
03
04
05
06
07
08
09
10
11
String mxName = ManagementFactory.getRuntimeMXBean().getName();
 
int index = mxName.indexOf(PID_SEPERATOR);
 
String result;
 
if (index != -1) {
    result = mxName.substring(0, index);
} else {
    throw new IllegalStateException("Could not acquire pid using " + mxName);
}

Еще одна небольшая проблема — направить вывод jstack в ваш журнал. Это также довольно легко настроить, используя гоблеры потока вывода. Посмотрите здесь пример того, как направлять выходные данные, распечатанные процессом, который вы вызываете, в свой файл журнала или поток вывода.

Хотя можно получить трассировку стека внутренних потоков, используя getAllStackTraces , я предпочитаю делать это, выполняя jstack по ряду причин. Во-первых, я бы хотел, чтобы это происходило внешне с работающим приложением (даже если JVM участвует в предоставлении информации), чтобы убедиться, что я не влияю на стабильность приложения, делая интроспективные вызовы. Другая причина заключается в том, что jstack является более мощным с точки зрения его возможностей, таких как отображение собственных кадров и состояния блокировки, чего нет в JVM.

Когда вы активируете jstack?

Второе решение, которое вам нужно принять, — это какие условия вы хотите, чтобы JVM регистрировала jstack. Это, вероятно, будет сделано после периода прогрева, когда сервер падает ниже или выше определенного порога обработки (т.е. обработки запроса или сообщения). Вы также можете убедиться, что вы проводите достаточно времени между каждой активацией; просто чтобы убедиться, что вы не затопите свои журналы при низкой или высокой нагрузке.

Шаблон, который вы использовали бы здесь, загружает сторожевой поток изнутри JVM, который может периодически просматривать состояние пропускной способности приложения (например, количество сообщений, обработанных за последние две минуты) и решать, будет ли «снимок экрана» было бы полезно состояние потока, в этом случае он активировал бы jstack и записал бы его в файл.

Задайте имя этого потока, чтобы оно содержало целевое и фактическое состояние пропускной способности, поэтому, когда вы делаете автоматический снимок jstack, вы можете точно понять, почему поток сторожевого таймера решил это сделать. Так как это будет происходить только каждые несколько минут, это не повлияет на производительность системы, особенно по сравнению с качеством предоставляемых данных.

Ниже приведен фрагмент, показывающий этот шаблон в действии. StartScheduleTask загружает сторожевой поток, чтобы периодически проверять значение пропускной способности, которое увеличивается с использованием параллельного сумматора Java 8 при обработке сообщения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void startScheduleTask() {
 
    scheduler.scheduleAtFixedRate(new Runnable() {
        public void run() {
 
            checkThroughput();
 
        }
    }, APP_WARMUP, POLLING_CYCLE, TimeUnit.SECONDS);
}
 
private void checkThroughput()
{
    int throughput = adder.intValue(); //the adder in inc’d when a message is processed
 
    if (throughput < MIN_THROUGHPUT) {
        Thread.currentThread().setName("Throughput jstack thread: " + throughput);
        System.err.println("Minimal throughput failed: exexuting jstack");
        executeJstack(); //see the code on github to see how this is done
    }
 
    adder.reset();
}
  • Полный исходный код для превентивного вызова jstack из вашего кода можно найти здесь .