Как мы себя заново писали, или как потерять исходники и не подать виду

от автора

Был прекрасный майский день. Мой взгляд случайно упал на чат ребят с крайнего сервера. У них майский день был не таким прекрасным: во время перераскладки второстепенного сервиса упал сервис авторизации, связанный с ним постольку-поскольку. Цимес ситуации в том, что падающую часть сервиса авторизации никто не поддерживает, он перешел к нам по наследству и никогда особо не сбоил. Меня увлекло чтение детектива поиска причин, и до определенного момента я был пассивным читателем — пока не увидел фразу нашего админа, наполненную приобретенной сединой его волос: «За час натекает 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’ы разгребают одну и ту же очередь, можно просто ограничить их число. Ищем инстанцирование, и находим только одно. Отлично! Осталось поменять прямое инстанцирование на пул и будет нам счастье.

Для этого есть два пути:

  1. Воткнуть декомпилированный код обратно в компилятор и молиться, чтобы декомпилятор не налажал. Это путь темной стороны, так как ведет к непредсказуемым багам;
  2. Поправить 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *