Эта статья представлена в Руководстве DZone по экосистеме Java . Получите бесплатную копию для более проницательных статей, отраслевой статистики и многого другого.
Сегодня, как никогда, скорость играет большую роль в жизненном цикле разработки программного обеспечения. Мы видим команды разработчиков, которые хотят быстрее внедрять код в производственную среду с возрастающей сложностью, и это усиливает уязвимость, которую необходимо устранить.
Те несколько часов после нового развертывания задали тон его успеху. Время от времени что-то идет не так, как бы ни были строги ваши тесты. Когда ваш код запущен в производство и соответствует реальной архитектуре и масштабам вашей системы, а реальные данные проходят через приложение, все может довольно быстро уйти на юг. Для того, чтобы быть устойчивым и быть в курсе событий, необходимо реализовать стратегию, которая позволяет:
- Определите, когда происходит ошибка
- Оцените серьезность ошибки, чтобы расставить приоритеты
- Вытянуть состояние, вызвавшее ошибку
- Проследите и решите основную причину
- Развернуть исправление
В этой статье мы рассмотрим некоторые из наиболее полезных методов, которые позволят вам собрать критический по времени ответ и «вооружить» свое приложение.
Дело о распределенной регистрации
Поскольку производственные среды распределены по нескольким узлам и кластерам, для транзакции, которая запускается на одном компьютере или службе, легко вызвать ошибку в другом месте. Когда возникает исключение, необходимо иметь возможность проследить этот тип распределенной транзакции, и журналы часто являются первым местом для поиска подсказок.
Вот почему для каждой распечатанной строки журнала мы должны иметь возможность извлечь полный контекст, чтобы точно понять, что там произошло. Некоторые данные могут поступать из самого регистратора и местоположения, в котором создается журнал; другие данные должны быть извлечены в момент события. Хороший способ отследить такие ошибки до их источника — генерировать UUID в точке входа приложения каждого потока.
Полезной, но недостаточно используемой функцией здесь является использование имен потоков, чтобы обеспечить окно для этого драгоценного контекста, прямо перед тем, как стек разрушится и данные будут потеряны. Вы можете отформатировать имя вашей темы в что-то вроде:
Thread.currentThread().setName(prettyFormat(threadName, getUUID(), message.getMsgType(), message.getMsgID(), getCurrentTime()));
Таким образом, вместо анонимного имени, такого как «pool-1-thread-17», ваше приложение теперь создает интеллектуальные трассировки стека, которые начинаются следующим образом: «threadName: pool-1-thread-17, UUID: AB5CAD, MsgType: AnalyzeGraph, MsgID: 5678956 , 30.08.2015 17:37 ”
Это хорошо работает при обработке перехваченных исключений, но что, если существует необработанное исключение? Хорошей практикой является установка обработчика неперехваченных исключений по умолчанию как для покрытия этого, так и для извлечения любых полезных данных, которые вам нужны. Помимо имен потоков, дополнительные места, которые мы можем использовать для хранения подсказок о том, что произошло, — это TLS (Thread Local Storage) и MDC (Mapped Diagnostic Context, предоставляемый вашей средой журналирования). Все остальные данные теряются при разрушении кадров стека.
Опираясь на инструментальный пояс JVM
Некоторые более сложные ошибки, такие как взаимоблокировки или узкие места с высокой производительностью, требуют другого подхода. Возьмем , к примеру, jstack : мощный инструмент, который поставляется вместе с JDK. Большинство из вас, вероятно, уже знакомы с этим в некотором роде. По сути, jstack позволяет вам подключиться к запущенному процессу и вывести все потоки, которые в нем в данный момент выполняются. Он напечатает трассировку стека каждого потока; фреймы — либо Java, либо нативные; замки они держат; и все виды других метаданных. Он также может анализировать дампы кучи или дампы процессов, которые уже завершены. Это давний и очень полезный инструментарий.
The problem here is that jstack is mostly used in retrospect. The condition you’re looking to debug has already happened, and now you’re left searching through the debris. The server isn’t responding, the throughput is dropping, database queries are taking forever: a typical output would be a few threads stuck on some nasty database query, with no clue of how we got there. A nice hack that would allow you to get the jstack output where it matters most is to activate it automatically when things start tumbling down. For example, you can set a certain throughput threshold and get jstack to run at the moment it drops [1].
Combined with using smart thread names, we can now know exactly which messages caused us to get stuck, and we can retrace our steps back, reproduce the error, isolate it, and solve it.
Using Java Agents to Skip Logging Altogether
The next step in this process is gaining visibility into your application during runtime. Logs are inspected in retrospect and only include the information that you’ve decided to put there in advance. We’ve seen how we can enrich them with stateful data, but we also need a way to access the exact variable values that caused each error to get down to the real root cause. Java agents give us the ability to get to the data we need straight from the source without writing to disk and using huge log files, so we can extract only the data we’ll actually be using.
One interesting approach is using BTrace, an open-source Java agent that hooks up to a JVM and opens up a scripting language that lets you query it during runtime. For instance, you can get access to things like ClassLoaders and their subclasses, and load up jstack whenever some troubled new class is instantiated. It’s a useful tool for investigating specific issues and requires you to write scripts for each case you want to cover.
You could also write your own custom Java agent, just like BTrace. One way this helped our team at Takipi was when a certain class was instantiating millions of new objects for some reason. We wrote an agent that hooks up to the constructor of that object. Anytime the object was allocated an instance, the agent would extract its stack trace. Later we analyzed the results and understood where the load was coming from. These kind of problems really pique our team’s interest. On our day-to-day we’re building a production grade agent that knows how to extract the variable values that cause each exception or logged error, all across the stack trace, and across different machines.
Testing in Production: Not for the Faint Hearted
Jokes aside, testing in production is a serious practice that many companies are taking part in. They don’t cancel the testing phase completely, but they understand that staging environments are not enough to mimic full blown distributed environments, no matter how much time and effort you put into setting them up. The only real testing takes place in production, with real data flowing through the system and unexpected use cases being thrown at it.
There are several approaches you can adopt for performing controlled production testing, depending on what kind of functionality is it that you’re trying to test. One option is duplicating and routing live traffic both through the current system and through the new version of the component that you’re testing. This way you can see how the new component behaves and compare it directly to the current version without risking the delivery of wrong results back to the user if, for example, it’s some data crunching task.
Another option is segmenting your traffic and releasing new features gradually. One way to do this is to use a canary server, which is a single node in your system updated with the new version you’d like to roll out (just like a canary in the coal mine). Sometimes it’s also helpful to add more fine-grained monitoring and logging to the canary server. Another option is to add more abstraction on top of the canary setup, implementing and making use of gradual rollouts with feature switches, or A/B testing small changes in your application to see how they impact performance.
Final Thoughts
Debugging Java applications in production requires a creative and forward-thinking mindset. Unless we prepare our applications and environment in advance, there will not be much insight to recover after getting hit by errors and performance issues.
[1] https://github.com/takipi/jstack/
For more insights on microservices, JVM languages, and more trends in Java, get your free copy of the DZone Guide to the Java Ecosystem!