Агент Java в дії

pic

Java Agent in action

Я шукав випадок використання для реалізації Java агенту. Оскільки останнім часом я трохи "ржавий", я вирішив ще раз звернутися до агентів New Relic і спробувати наслідувати їх.

Отже, я намагаюся збирати час виконання кожного методу разом з використанням пам'яті цим методом в логах.

Ці логи далі використовуються для створення панелі моніторингу, і на основі результатів ми можемо оптимізувати додатки.

Основи для Java агентів: https://medium.com/@vinodatwal/java-agent-powerful-tool-d24975ccfadd

Випадок використання: Відстеження часу виконання та використання пам'яті

Основною метою мого Java агента було відстежувати час виконання методів і записувати їх використання пам'яті під час виконання. Хоча метрики пам'яті можуть бути не зовсім точними, вони дають корисне наближення для розуміння використання ресурсів.

Ось як працює процес:

  1. Агент інструментує методи програми під час виконання.
  2. Для кожного методу записує:
  • Час виконання
  • Використання пам'яті на початку та в кінці методу

Ці логи потім використовуються для побудови панелі моніторингу, яка візуалізує метрики продуктивності. Аналізуючи ці дані, стає простіше виявити вузькі місця та оптимізувати додаток для кращої ефективності.

JavaAssist для генерації байт-коду

Для цієї реалізації я використав бібліотеку JavaAssist для маніпуляцій з байт-кодом. Основна перевага використання JavaAssist — це його читабельність, що робить код простішим для розуміння та підтримки.

Хоча є й інші бібліотеки, такі як ASM, я вибрав JavaAssist, оскільки ASM часто жертвує читабельністю на користь гнучкості та продуктивності — і давайте будемо чесними, моєму мозку просто важко дається складність ASM! 😄

JavaAssist поєднує простоту та функціональність, що робить його чудовим вибором для експериментів з генерацією байт-коду та Java агентами.


 org.javassist  
 javassist  
 3.29.2-GA  

Давайте розважимось!

Реалізація, яку я вивчав, була спроектована для запису метрик продуктивності додатку. Хоча це надзвичайно корисно, є одна проблема: інструментування всіх методів в додатку може значно знизити продуктивність під час виконання.

Отже, щоб зберегти ефективність і практичність, я додав один нюанс: замість відстеження кожного методу, Java агент приймає конкретні пакети як аргументи. Це дозволяє моніторити лише ті частини додатку, які є найбільш важливими.

Розумне відстеження: Орієнтуємось на те, що важливо 😎

Наприклад, якщо ви усуваєте проблеми з продуктивністю бази даних, ви можете передати пакет бібліотеки JDBC як аргумент Java агенту.
З цим підходом ви можете відстежувати:

  • Час виконання запитів
  • Метрики продуктивності підключення

Це цілеспрямоване інструментування не тільки знижує накладні витрати, але й дозволяє зосередитися на зборі інформації там, де вона найбільше потрібна.

Уявіть, що ви поєднуєте це з налаштованими панелями моніторингу — раптом у вас є легкий, але потужний інструмент моніторингу продуктивності, який відповідає вашим конкретним потребам.

Реалізація 🧑‍💻 :

Статичне завантаження агента

Передача агента під час запуску програми

 // ======================= основний виконавець ===========================  
 public static void premain(String agentArgs, Instrumentation inst) {  
 inst.addTransformer(new PerformanceTransformer(agentArgs));  
 }  

// ===================== трансформер ================================  

 public class PerformanceTransformer implements ClassFileTransformer {  
 private final List trackedPackages;  

 @Override  
 public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,  
 ProtectionDomain protectionDomain, byte[] classfileBuffer)  
 throws IllegalClassFormatException {  

 // Перетворити ім'я класу у формат Java  
 String javaClassName = className.replace('/', '.');  

 // Пропустити системні класи та інші пакети, які ви не хочете інструментувати  
 if (javaClassName.startsWith("java.") ||  
 javaClassName.startsWith("javax.") ||  
 javaClassName.startsWith("sun.")) {  
 return null;  
 }  
 if (!shouldTrackPackage(javaClassName)) {  
 return null;  
 }  

 try {  
 ClassPool cp = ClassPool.getDefault();  
 ByteArrayInputStream bis = new ByteArrayInputStream(classfileBuffer);  
 CtClass cc = cp.makeClass(bis);  

 // Отримати всі методи класу  
 CtMethod[] methods = cc.getDeclaredMethods();  

 for (CtMethod method : methods) {  
 // Пропустити рідні та абстрактні методи  
 if (method.isEmpty()) continue;  

 method.addLocalVariable("startTime", CtClass.longType);  
 method.addLocalVariable("endTime", CtClass.longType);  
 method.addLocalVariable("memoryBefore", CtClass.longType);  
 method.addLocalVariable("memoryAfter", CtClass.longType);  

 // Вставити код для відстеження часу і пам'яті на початку методу  
 method.insertBefore(  
 "startTime = System.nanoTime();" +  
 "memoryBefore = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();"  
 );  

 // Вставити код для відстеження часу і пам'яті в кінці методу  
 method.insertAfter(  
 "endTime = System.nanoTime();" +  
 "memoryAfter = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();" +  
 "System.out.println(\"" + javaClassName + "." + method.getName() +  
 " - Час виконання: \" + (endTime - startTime) + " +  
 "\" нс, Використано пам'яті: \" + (memoryAfter - memoryBefore) + " +  
 "\" байт\");"  
 );  
 }  

 return cc.toBytecode();  

 } catch (Exception e) {  
 e.printStackTrace();  
 return null;  
 }  
 }  

 public PerformanceTransformer(String packages) {  
 this.trackedPackages = packages != null ?  
 Arrays.asList(packages.split(",")) :  
 new ArrayList<>();  
 }  

 private boolean shouldTrackPackage(String className) {  
 return trackedPackages.isEmpty() ||  
 trackedPackages.stream().anyMatch(pkg ->  
 className.startsWith(pkg.trim()));  
 }  
}
  • Запуск програми
java -javaagent:Agent.jar=org.va,org.report-mgmt -jar Service-1.0-SNAPSHOT.jar
  • Подивимося на логи
$ java -javaagent:Agent.jar=org.va -jar Service-1.0-SNAPSHOT.jar  

org.va.model.EmployeeDao.createTableIfNotExists - Час виконання: 22218792 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.getId - Час виконання: 2750 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.getName - Час виконання: 583 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.getDepartment - Час виконання: 375 нс, Використано пам'яті: 0 байт  
org.va.model.EmployeeDao.createEmployee - Час виконання: 5272125 нс, Використано пам'яті: 428352 байт  
org.va.model.Employee.getId - Час виконання: 500 нс, Використано пам'яті: 0 байт  

org.va.model.Employee.setId - Час виконання: 1333 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.setName - Час виконання: 500 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.setDepartment - Час виконання: 416 нс, Використано пам'яті: 0 байт  
org.va.model.EmployeeDao.getEmployee - Час виконання: 14640583 нс, Використано пам'яті: 1470816 байт  
org.va.model.Employee.toString - Час виконання: 1824250 нс, Використано пам'яті: 0 байт  
Employee{id='ID-1', name='Vinod Atwal', department='ENG'}  
org.va.model.Employee.toString - Час виконання: 4250 нс, Використано пам'яті: 0 байт  
Employee{id='ID-1', name='Vinod Atwal', department='ENG'}  
org.va.runner.RepositoryScenariosRunner.run - Час виконання: 353206833 нс, Використано пам'яті: 3272040 байт  
org.va.Main.main - Час виконання: 356449667 нс, Використано пам'яті: 3272040 байт  

Динамічне завантаження агента

Окрема програма для підключення до поточної працюючої програми з PID 32327

public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {  
 URL agentUrl = DynamicAgentTest.class.getClassLoader()  
 .getResource("Agent.jar");  
 VirtualMachine vm = VirtualMachine.attach("32327");  
 try {  
 vm.loadAgent( agentUrl.getPath(),"org.va");  
 } catch (AgentInitializationException e) {  
 System.err.println("Ініціалізація агента не вдалася: " + e.getMessage());  
 e.printStackTrace();  
 }  
 vm.detach();  
 }
  • Подивимося на логи
Поточний PID: 32327  
WARNING: Динамічно завантажено Java агент (/Users/vinodatwal/Documents/projects/AnnotationMagic/Service/target/classes/Agent.jar)  
WARNING: Якщо використовується інструмент обслуговування, будь ласка, запустіть з -XX:+EnableDynamicAgentLoading, щоб приховати це попередження  
WARNING: Якщо інструмент обслуговування не використовується, запустіть з -Djdk.instrument.traceUsage для додаткової інформації  
WARNING: Динамічне завантаження агентів буде заборонено за замовчуванням у майбутніх релізах  

org.va.model.EmployeeDao.createTableIfNotExists - Час виконання: 55558625 нс, Використано пам'яті: 427352 байт  
org.va.model.Employee.getId - Час виконання: 3625 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.getName - Час виконання: 625 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.getDepartment - Час виконання: 500 нс, Використано пам'яті: 0 байт  
org.va.model.EmployeeDao.createEmployee - Час виконання: 12134917 нс, Використано пам'яті: 432200 байт  
org.va.model.Employee.getId - Час виконання: 458 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.setId - Час виконання: 1125 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.setName - Час виконання: 500 нс, Використано пам'яті: 0 байт  
org.va.model.Employee.setDepartment - Час виконання: 458 нс, Використано пам'яті: 0 байт  
org.va.model.EmployeeDao.getEmployee - Час виконання: 17510541 нс, Використано пам'яті: 1454176 байт  
org.va.model.Employee.toString - Час виконання: 1836083 нс, Використано пам'яті: 210776 байт  
Employee{id='ID-1', name='Vinod Atwal', department='ENG'}  
org.va.model.Employee.toString - Час виконання: 4500 нс, Використано пам'яті: 0 байт  
Employee{id='ID-1', name='Vinod Atwal', department='ENG'}  
org.va.runner.RepositoryScenariosRunner.run - Час виконання: 442516458 нс, Використано пам'яті: 2458120 байт  

Процес завершено з кодом виходу 0  

Посилання на репозиторій: https://github.com/VinodAtwal/MetricsAgent

Висновки

У цьому дослідженні ми розглянули підхід до реалізації Java агента, виявивши, як його можна використовувати як під час запуску, так і динамічно під час виконання.
Натхненний інструментами, такими як New Relic (який, можливо, ми колись захочемо наслідувати 😅), цей досвід показав величезний потенціал Java агентів для маніпуляцій з кодом під час виконання.

З цією реалізацією ми додали ще один потужний інструмент до нашого набору інструментів розробника. Можливості безмежні:

  • Управління функціональністю за допомогою feature flagging для динамічного вмикання та вимикання функцій
  • Створення проксі для динамічного перехоплення методів
  • Моніторинг продуктивності для збору корисних метрик
  • І багато інших креативних випадків використання, що чекають на відкриття!

Цей експеримент став захоплюючим зануренням у потужність маніпуляцій з байт-кодом та інструментацію під час виконання. Тепер відкрито двері для більш складних додатків і глибших досліджень у майбутньому.

Що далі в моєму дослідженні?

Поглиблюючись у захоплюючий світ генерації коду, я виявив різноманітні підходи, від технік під час виконання до технік на етапі компіляції. Кожне нове відкриття вело мене до ще одного потужного інструменту в Java, що демонструє її величезні можливості для вирішення складних і цікавих завдань.

Але що ж далі?

Я планую перенаправити своє дослідження на JMX (Java Management Extensions) інтерфейси. Ці інтерфейси пропонують захоплюючі випадки використання для моніторингу та керування додатками. За допомогою JMX можна отримати цінні відомості про продуктивність додатку, включаючи активність потоків, використання пам'яті та споживання CPU, все це через використання MXBeans. Це може бути надзвичайно корисним для виявлення та усунення таких проблем, як дедлоки, створення дампів потоків, аналіз поведінки збору сміття (GC) та багато іншого.

Потенціал JMX для надання реального часу видимості стану додатка робить його важливим інструментом для будь-якого розробника або команди операцій.

Перерва в серії

Зараз я можу поставити цю серію досліджень на паузу, щоб дослідити інші захоплюючі теми, які я запланував. Однак я готовий повернутися до цієї подорожі і зануритися ще глибше в ці складні інструменти Java в майбутньому.

Якщо у вас є які-небудь пропозиції, думки або теми, які ви хотіли б, щоб я дослідив, не соромтеся поділитися своїми інтересами. Ваш внесок може стати поштовхом до наступної великої ідеї!

Перекладено з: Java Agent in Action

Leave a Reply

Your email address will not be published. Required fields are marked *