Был прекрасный майский день. Мой взгляд случайно упал на чат ребят с крайнего сервера. У них майский день был не таким прекрасным: во время перераскладки второстепенного сервиса упал сервис авторизации, связанный с ним постольку-поскольку. Цимес ситуации в том, что падающую часть сервиса авторизации никто не поддерживает, он перешел к нам по наследству и никогда особо не сбоил. Меня увлекло чтение детектива поиска причин, и до определенного момента я был пассивным читателем — пока не увидел фразу нашего админа, наполненную приобретенной сединой его волос: «За час натекает 800+ потоков».
Вот это уже интересно! На Java течь потоками в таком темпе, да чтобы этого годами не замечать — не так уж это и просто, что я и озвучил. А поскольку в данном чате я был единственным Java-разработчиком, то было лишь вопросом времени, пока кто-нибудь не скажет: «Раз такой умный, возьми да поправь». И не важно, что ты клиентщик, и вообще последние три года пишешь под Андроид.
А почему бы и нет?
Шаг 1: берем сорцы для обзорного ознакомления. Грепаем «Thread», «Executor» и… ничего не находим. Зато находим некую библиотеку, в которую уходят все вызовы.
Шаг 2: берем сорцы библиотеки и… их нет. Вот это поворот! Как так случилось? Да очень просто. Проект состоит из 300+ сервисов. У него богатая и сложная история с неожиданными поворотами. И при переносе всех этих чудес, местами без документации, с разными репозиториями, языками и технологиями, чисто технически не за всем можно уследить, тем более что все отлично компилируется, либо лежит в проекте в виде jar-ки.
В целом, для ознакомления сорцы не особо и нужны. Intellij Idea вполне сносно декомпилирует код. Даже при беглом прочтении волосы встали дыбом. Слово «Executor» все еще не встречалось, зато «new Thread» было буквально повсюду. На этом отложим в сторону код. Прямо сейчас искать в нем утечку ничуть не проще, чем иголку в стоге иголок. Возьмем лучше thread dump и посмотрим:
"Thread-782" daemon prio=10 tid=0x00007f7db4654800 nid=0x2286d9 in Object.wait() [0x00007f7b929d6000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000b843f3c8> (a java.util.LinkedList) at java.lang.Object.wait(Object.java:503) at com.aol.saabclient.SAABConnection.AsynchMsgProcessor.run(AsynchMsgProcessor.java:83) - locked <0x00000000b843f3c8> (a java.util.LinkedList) "Thread-781" daemon prio=10 tid=0x00007f7db4651000 nid=0x2286d7 in Object.wait() [0x00007f7de37ee000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000b843f3c8> (a java.util.LinkedList) at java.lang.Object.wait(Object.java:503) at com.aol.saabclient.SAABConnection.AsynchMsgProcessor.run(AsynchMsgProcessor.java:83) - locked <0x00000000b843f3c8> (a java.util.LinkedList) "Thread-780" daemon prio=10 tid=0x00007f7db464f000 nid=0x2286d5 in Object.wait() [0x00007f7de118a000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000b843f3c8> (a java.util.LinkedList) at java.lang.Object.wait(Object.java:503) at com.aol.saabclient.SAABConnection.AsynchMsgProcessor.run(AsynchMsgProcessor.java:83) - locked <0x00000000b843f3c8> (a java.util.LinkedList)
Ну, раз такая пьянка, пойдем прямиком в AsyncMsgProcessor (да-да, SAABConnection — это package). Там мы видим что-то вроде ручной реализации blocking queue вокруг LinkedList. Ясно-понятно, что цикл разбора вечен, и это даже может быть desirable behaviour. Также становится понятно, что AsyncMsgProcessor создается для каждого соединения, но вот очередь общая (static LinkedList messages). Таким образом, раз все AsyncMsgProcessor’ы разгребают одну и ту же очередь, можно просто ограничить их число. Ищем инстанцирование, и находим только одно. Отлично! Осталось поменять прямое инстанцирование на пул и будет нам счастье.
Для этого есть два пути:
- Воткнуть декомпилированный код обратно в компилятор и молиться, чтобы декомпилятор не налажал. Это путь темной стороны, так как ведет к непредсказуемым багам;
- Поправить byte-код одного маленького метода руками. Шансов ошибиться в разы меньше, а значит это путь настоящего джедая.
Правим byte-код
Для разбора и сбора обратно class-файлов нужна более-менее специфичная тулза. Я нашел только вот эту: JBE — Java Bytecode Editor. Она имеет большую проблему с редактированием кода: нужно руками считать все смещения в условных и безусловных переходах, что, в общем-то, так себе перспектива, даже для сравнительно небольшого метода. Опять-таки из-за большого шанса ошибиться любое изменение будет даваться кровью и потом. Среди менее готовых для прямого использования тулзов есть отличная, очень мощная штука — ASM. Но из коробки не имеет возможности сначала вывести в виде текста, затем подредактировать и собрать обратно. Но можно научить.
Хачим вывод
Для вывода текста используется классы Textifier + TraceMethodVisitor. Но из такого вывода довольно проблематично будет собрать все обратно, чтобы байткод не изменился (хотя бы функционально). Поэтому немного хаков:
Textifier textifier = new Textifier(Opcodes.ASM5) { @Override public void visitLabel(Label label) { buf.setLength(0); buf.append('#'); appendLabel(label); buf.append(":\n"); text.add(buf.toString()); } @Override public void visitLineNumber(int line, Label start) { buf.setLength(0); buf.append("// line ").append(line).append('\n'); text.add(buf.toString()); } @Override public void visitMaxs(int maxStack, int maxLocals) { buf.setLength(0); buf.append("// MAXSTACK = ").append(maxStack).append('\n'); text.add(buf.toString()); buf.setLength(0); buf.append("// MAXLOCALS = ").append(maxLocals).append('\n'); text.add(buf.toString()); } @Override public void visitLdcInsn(Object cst) { buf.setLength(0); buf.append(tab2).append("LDC "); if (cst instanceof String) { Printer.appendString(buf, (String) cst); } else if (cst instanceof org.objectweb.asm.Type) { buf.append(((org.objectweb.asm.Type) cst).getDescriptor()).append(".class"); } else if (cst instanceof Long) { buf.append(cst).append('L'); } else if (cst instanceof Float) { buf.append(cst).append('F'); } else if (cst instanceof Double) { buf.append(cst).append('D'); } else if (cst instanceof Integer) { buf.append(cst); } else { throw new IllegalArgumentException("cst " + cst); } buf.append('\n'); text.add(buf.toString()); } @Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { } };
Хачим ввод
С вводом сложнее. В ASM есть класс MethodNode, являющийся визитором. Обычно подразумевается, что MethodNode подсовывается в accept ClassReader, заполняясь из него, возможно, видоизменяясь. Мы же хотим подсунуть в него текст, сгенеренный на прошлом шаге (именно там должны были произойти «видоизменения»). Симитируем поведение Reader:
for (String line : methodCode.getText().split("\n")) { int lastCommentPos = line.lastIndexOf("//"); if (lastCommentPos != -1) { line = line.substring(0, lastCommentPos); } line = line.trim(); if (line.isEmpty()) { continue; } String[] withParams = line.split("\\s+"); String command = withParams[0]; if (command.startsWith("#")) { verify(command.endsWith(":")); String substring = command.substring(1, command.length() - 1); method.visitLabel(getLabel(substring, labels)); } else if (command.equals("TRYCATCHBLOCK")) { verify(withParams.length == 5); Label start = getLabel(withParams[1], labels); Label end = getLabel(withParams[2], labels); Label handler = getLabel(withParams[3], labels); String type = withParams[4]; if (type.equals("null")) { type = null; } method.visitTryCatchBlock(start, end, handler, type); } else { Opcode opcode = OPCODES.get(command); //копипаста из сорцов ASM if (opcode == null) { throw new RuntimeException("Unknown " + command); } else { switch (opcode.type) { case OpcodeGroup.INSN: verify(withParams.length == 1); method.visitInsn(opcode.opcode); break; case OpcodeGroup.INSN_INT: verify(withParams.length == 2); method.visitIntInsn(opcode.opcode, Integer.valueOf(withParams[1])); break; case OpcodeGroup.INSN_VAR: verify(withParams.length == 2); method.visitVarInsn(opcode.opcode, Integer.valueOf(withParams[1])); break; case OpcodeGroup.INSN_TYPE: verify(withParams.length == 2); method.visitTypeInsn(opcode.opcode, withParams[1]); break; case OpcodeGroup.INSN_FIELD: verify(withParams.length == 4); verify(withParams[2].equals(":")); int dotIndex = withParams[1].indexOf('.'); String owner = withParams[1].substring(0, dotIndex); String name = withParams[1].substring(dotIndex + 1); method.visitFieldInsn(opcode.opcode, owner, name, withParams[3]); break; case OpcodeGroup.INSN_METHOD: verify(withParams.length == 3); dotIndex = withParams[1].indexOf('.'); owner = withParams[1].substring(0, dotIndex); name = withParams[1].substring(dotIndex + 1); method.visitMethodInsn(opcode.opcode, owner, name, withParams[2], opcode.opcode == INVOKEINTERFACE); break; case OpcodeGroup.INSN_JUMP: verify(withParams.length == 2); method.visitJumpInsn(opcode.opcode, getLabel(withParams[1], labels)); break; case OpcodeGroup.INSN_LDC: withParams = line.split("\\s+", 2); verify(withParams.length == 2); method.visitLdcInsn(parseLdc(withParams[1])); break; case OpcodeGroup.INSN_IINC: verify(withParams.length == 3); method.visitIincInsn(Integer.valueOf(withParams[1]), Integer.valueOf(withParams[2])); break; case OpcodeGroup.INSN_MULTIANEWARRAY: verify(withParams.length == 3); method.visitMultiANewArrayInsn(withParams[1], Integer.valueOf(withParams[2])); break; default: throw new IllegalArgumentException(); } } } }
Врезку в байткод в итоге мы сделали. Осталось напилить тот самый пул. Код приводить не буду, там еще больше всякого… Но подход очень простой: берем модифицированную библиотеку, кладем ее в зависимости, пишем нужные классы и перекомпилируем. После этого потоки перестали течь, все работает, хэппи энд. На самом деле, все, конечно, было не так — потребовалось 5—7 раскладок в тестовом окружении. То байт-код — не байт-код, то пул — не пул…
А вывод у этой истории простой: невыполнимых задач не бывает, даже потеря исходных кодов — не катастрофа. И если нет нужной тулзы, ее всегда можно соорудить с помощью тех же подручных материалов.
P.S.: Пиво мне так никто и не поставил.
ссылка на оригинал статьи http://habrahabr.ru/post/271829/
Добавить комментарий