3. 3
Содержание
•Постановка задачи и описание проблемы
•Пример воспроизводимого тест-кейса
•Анализ «зависания» тестового процесса – почему это происходит и как это исправить
•Почему «зависают» процессы, когда ваш код в этом точно не виноват, и как с этим бороться
4. 4
Постановка проблемы
•Есть сервис, который обрабатывает файлы, используя запуск внешних нативных процессов.
•Проблема: на некоторых файлах дочерний процесс зависает.
6. 6
Как работать с процессом
•Чаще всего, с процессом работают так:
Process process = new ProcessBuilder(cmd)
.environment(envp)
.directory(dir)
.start();
process.waitFor();
if (process.exitValue() == 0) {
// Getting out and error streams of this process
InputStream output = process.getInputStream();
InputStream error = process.getErrorStream();
// Skipped: processing data in process streams
} else {
// Skipped: external process returned non-zero code…
}
•Вместо ProcessBuilder в данном случае можно просто Runtime.getRuntime().exec(cmd, envp, dir).
8. 8
Тестовый дочерний процесс
String[] cmd = {
"/bin/bash",
"-c",
"s=$(printf "%-" + n + "s" '~') ; echo "${s// /'~'}""
};
Process process = new ProcessBuilder(cmd).start();
process.waitFor();
9. 9
Результаты работы процесса
•Процесс отрабатывает для n = 10, 20, 30, 40, 1000, 10000.
•Процесс зависает для n = 100000, 200000, 1000000 и т.д.
10. 10
Что значит «процесс зависает»?
•Утверждение «процесс зависает для n = 100000, 1000000» является сомнительным как минимум по трём причинам:
- Проблема остановки неразрешима (Тьюринг, 1936)
- Процесс просто работает долго? Swapping и/или внутренняя жизнь ОС, внезапно возникшая внешняя нагрузка на сервер и т.д.
- Процесс-таки отработал (и очень быстро), а Java не смогла получить его поток на чтение (in.readLine())?
11. 11
Процесс «зависает»?
•Правильно написать:
«Исходное Java-приложение испытывает проблемы с производительностью для n = 100000, 1000000, а именно – не успевает отработать за k секунд».
12. 12
Процесс в ps
•Это снимок примерно сразу после вызова внешнего процесса:
~$ ps -ef | grep bash
kirill 4158 4148 95 01:08 pts/1 00:00:08 /bin/bash -c s=$(printf "%-100000s" '~') ; echo "${s// /'~'}"
13. 13
Процесс в ps
•Это снимок через некоторое время:
~$ ps -ef | grep bash
kirill 4158 4148 10 01:08 pts/1 00:00:14 /bin/bash -c s=$(printf "%-100000s" '~') ; echo "${s// /'~'}"
14. 14
Процесс в top
•Это снимок примерно сразу после вызова внешнего процесса:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4158 kirill 20 0 6196 2032 1148 R 94.8 0.1 0:12.39 /bin/bash -c s=$(printf "%-100000s" '~') ; echo "${s// /'~'}"
15. 15
Процесс в top
•Это снимок через некоторое время:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4158 kirill 20 0 6196 1872 1172 S 0.0 0.1 0:14.06 /bin/bash -c s=$(printf "%-100000s" '~') ; echo "${s// /'~'}"
16. 16
Процесс в /proc/…/stat
~$ less /proc/4158/stat
4158 (bash) S 4148 4148 …
17. 17
Граничное значение параметра n
•Процесс в состоянии sleeping (не использует CPU), который непонятно что делает.
•Граничное значение n – это n = 65535 (двоичный поиск).
18. 18
Источник проблемы
•Буфер ограниченного размера, через который общаются процессы.
19. 19
Что делать с процессом в Java?
•Надо вычитывать stdout дочернего процесса во время работы.
•Желательно избежать вызова process.waitFor().
20. 20
IOUtils.copy из Apache commons-io
public static long copyLarge(InputStream input, OutputStream output,
byte[] buffer) throws IOException {
long count = 0;
int n = 0;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
21. 21
Копируем байты из stdout
Process process = new ProcessBuilder(cmd).start();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
IOUtils.copy(process.getInputStream(), outputStream);
process.waitFor();
22. 22
Забыли про stderr
String[] cmd = {
"/bin/bash",
"-c",
"s=$(printf "%-" + n + "s" '~') ; echo "${s// /'~'}" >&2" };
Process process = new ProcessBuilder(cmd).start();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
IOUtils.copy(process.getInputStream(), outputStream);
process.waitFor();
27. 27
Использование moveBytes
final byte[] buffer = new byte[1 << 15];
while (process.isAlive()) {
while (Math.max(
moveBytes(process.getInputStream(), outputStream, buffer),
moveBytes(process.getErrorStream(), errorStream, buffer)) > 0) {
}
Thread.sleep(10);
}
// Process has terminated, let's read remain data from process:
while (moveBytes(process.getInputStream(), outputStream, buffer) > 0) {
}
while (moveBytes(process.getErrorStream(), errorStream, buffer) > 0) {
}
28. 28
Чтение через дополнительные потоки
Pair<InputStream, ByteArrayOutputStream>[] pairs = new Pair[]{
Pair.of(process.getInputStream(), new ByteArrayOutputStream()),
Pair.of(process.getErrorStream(), new ByteArrayOutputStream())
};
for (Pair<InputStream, ByteArrayOutputStream> pair : pairs) {
executor.submit(() -> IOUtils.copy(pair.getKey(), pair.getValue()));
}
process.waitFor();
29. 29
Без pipe проблемы нет
•Этот процесс работает нормально:
new ProcessBuilder(cmd).
redirectOutput(ProcessBuilder.Redirect.INHERIT).
redirectError(ProcessBuilder.Redirect.INHERIT).
start();
•Если использовать вместо INHERIT – WRITE/APPEND, то тоже всё хорошо.
30. 30
Пойдём далее
•Пусть проблема с буфером уже решена.
•Любой процесс, получающий и обрабатывающий данные из интернета, может зависнуть с достаточно большой вероятностью.
32. 32
Пример с urllib.open try: usock = urllib.urlopen(c_url) except: out.write('Error while opening ' + c_url)
33. 33
Причины зависания процессов
•Ошибки в программном коде внешних программ и библиотек.
•Ошибки того, кто вызывает процесс.
34. 34
Последствия зависания процессов
•Перестаёт контролироваться Java-процессом.
•Если таких процессов много, растёт длина CPU Run Queue. Увеличиваются затраты памяти, возможен swapping.
35. 35
Пример с ZooKeeper
if (lock.acquire(5, TimeUnit.SECONDS)) {
try {
<hanging or looping process>
} finally {
lock.release();
}
}
36. 36
Что же делать?
•Построить свой алгоритм, который будет определять, завис ли процесс, на основе его состояний (D/R/S/T/Z).
•Снимать процесс по таймауту, из Java.
37. 37
Apache commons-exec
•Библиотека от Apache для работы с процессами.
•Совместима с JDK 1.3.
44. 44
Используем метод destroyForcibly()
try {
// Skipped: process initialization
while (process.isAlive() && (timeout <= 0 || System.currentTimeMillis() < finish)) {
// Skipped: get data from process streams (stdout and stderr)
}
// Skipped: get data from process streams for the last time
if (destroyForcibly(process)) {
throw new TimeoutException("Process " + Arrays.toString(cmd)
+ " has not finished its work and was destroyed"
+ " after " + (System.currentTimeMillis() - start) + " ms");
}
} catch (InterruptedException | IOException | RuntimeException | Error e) {
destroyForcibly(process);
throw e;
}