DevOps: відправляємо метрики і спимо спокійно



Раптово, вночі лунає дзвінок і ми дізнаємося, що наш додаток не працює. Є 2 години на його реанімацію…


Так, ми справно відправляли логи в Elasticsearch, надихнувшись ідеями статті «Публікація логів в Elasticsearch — життя без регулярних виразів і без logstash». Але от для розслідування справжньої причини «падіння» додатка нам явно не вистачає даних з jmx бинов jvm, jmx пулу з'єднання з базою даних про кількість відкритих нашим процесом файлових дескрипторів і т. п. Як же ми так про них забули!? Пізно пити Боржомі…

Ми звичайно можемо брати дані про процесі nagios, zabbix, collectd або пропрієтарних систем моніторингу, але було б зручніше мати всі ці дані відразу в Elasticsearch. Це дозволило б візуалізувати kibana і отримувати сповіщення на основі єдиного джерела подій з безлічі процесів на різних вузлах мережі. Elasticsearch дозволяє індексувати, проводити повнотекстовий пошук та горизонтально масштабувати сховище даних за рахунок додавання нових процесів пошукового сервера.

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

Поділюся з вами рецептом більше спати вночі:
  • Готуємо Elasticsearch і kibana
  • Відправляємо метрики з jvm в Elasticsearch
  • А як же логи програми?
  • Як бути з уже запущеними процесами jvm і причому тут groovy?


Готуємо Elasticsearch і kibana
Почнемо з того, куди ми будемо збирати метрики. Запускаємо Elasticsearch(ES) сервер. Наприклад, Elasticsearch у найпростішій конфігурації , яка відразу встановлює шаблон за замовчуванням для всіх нових індексів, щоб було можна було шукати в kibana використовуючи готовий Logstash Dashboard. Запускається mvn package. Вузли серверів знаходять один одного за допомогою multicast UDP пакетів і об'єднуються в кластер за збігається clusterName. Ясно, що відсутність unicast адрес спрощує життя в розробці, але в промисловій експлуатації так налаштовувати кластер не варто!
Для цього прикладу можна запустити Elasticsearch з допомогою groovy скрипта...
java -jar groovy-grape-aether-2.4.5.1.jar elasticsearch-server.groovy
Скрипт elasticsearch-server.groovy:
@Grab(group='org.elasticsearch', module='elasticsearch', version='1.1.1')
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder;

import java.io.InputStream;
import java.net.URL;
import java.util.concurrent.TimeUnit;

int durationInSeconds = args.length == 0 ? 3600 : Integer.parseInt(this.args[0]);

String template;
InputStream templateStream = new URL("https://raw.githubusercontent.com/logstash-plugins/logstash-output-elasticsearch/master/lib/logstash/outputs/elasticsearch/elasticsearch-template.json").openStream()
try{
template = new String(sun.misc.IOUtils.readFully(templateStream, -1, true));
} finally{
templateStream.close()
}

Node elasticsearchServer = NodeBuilder.nodeBuilder().settings(ImmutableSettings.settingsBuilder().put("http.cors.enabled","true")).clusterName("elasticsearchServer").data(true).build();
Node node = elasticsearchServer.start();
node.client().admin().indices().preparePutTemplate("logstash").setSource(template).get();
System.out.println("ES STARTED");

Thread.sleep(TimeUnit.SECONDS.toMillis(durationInSeconds));


Сховище для метрик у нас є. Тепер підключимося до нього з допомогою веб-застосунку Kibana та нічого не побачимо, так як ще ніхто не посилає туди метрики. Дивитися статус кластера Elasticsearch і підключилися клієнтів можна за допомогою elasticsearch-HQ, але варіантів консолей адміністрування багато і вибір буде на ваш смак.

Пару слів про конфігурацію kibana для цього прикладу. У файлі kibana-3.1.3/config.js потрібно замінити властивість: elasticsearch: «127.0.0.1:9200». Після чого веб інтерфейс зможе підключитися до elasticsearch сервера, запущеного на тому ж вузлі мережі скриптом elasticsearch-server.groovy або командою mvn package.



Відправляємо метрики з jvm в Elasticsearch
Всю магію будемо робити за допомогою AspectJ-Scripting агента. Агент був доопрацьований так, щоб його можна було легко впроваджувати в процес за допомогою AttachAPI і щоб він отримував конфігурацію через параметр агента. Тепер агент можливо використовувати з конфігурацією без аспектів і можливо описувати періодично виконувані завдання.

AspectJ-Scripting дозволить нам без перекомпіляції і перепакування програми, з допомогою jar файлу з агентом aspectj-scripting-1.3-agent.jar, файлу конфігурації в файловій системі (або на веб сервері) та одного додаткового параметра -javaagent при старті JVM, відправляти метрики зі всіх процесів додатка в Elasticsearch.

У конфігурації java агента будемо використовувати com.github.igor-suhorukov:jvm-metrics:1.2 «під капотом» цієї бібліотеки працюють:
  • Jolokia для отримання jmx метрик з підключенням або до локального mbean сервера або по JSR160(RMI)
  • jvmstat performance counters — метрики доступні, якщо ми працюємо під OpenJDK/Oracle JVM
  • SIGAR для отримання показників від ОС і io.kamon:sigar-loader для спрощення завантаження його native бібліотеки.


Отже, все що робить потрібну нам роботу по збору і відправки метрик, знаходиться у файлі log.metrics.xml
<?xml version="1.0" encoding="UTF-8" standalone="так"?>
<configuration>
<globalContext>
<artifacts>
<artifact>com.github.igor-suhorukov:jvm-metrics:1.2</artifact>
<classRefs>
<variable>SigarCollect</variable><className>org.github.suhorukov.SigarCollect</className>
</classRefs>
<classRefs>
<variable>JmxCollect</variable><className>org.github.suhorukov.JmxCollect</className>
</classRefs>
</artifacts>
<artifacts>
<artifact>org.elasticsearch:elasticsearch:1.1.1</artifact>
<classRefs>
<variable>NodeBuilder</variable><className>org.elasticsearch.node.NodeBuilder</className>
</classRefs>
</artifacts>
<init>
<expression>
SigarCollect sigar = new SigarCollect();
JmxCollect jmxCollect = new JmxCollect();

reportHost = java.net.InetAddress.getLocalHost().getHostName();
pid = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().split("@")[0];

Thread.currentThread().setContextClassLoader(NodeBuilder.class.getClassLoader());
client = NodeBuilder.nodeBuilder().clusterName("elasticsearchServer").data(false).client(true).build().start().client();

additionalRecords = new java.util.HashMap();
additionalRecords.put("Host", reportHost); additionalRecords.put("Pid", pid);
</expression>
</init>
<timerTasks>
<delay>500</delay>
<period>2000</period>
<jobExpression>
import java.text.SimpleDateFormat;
import java.util.TimeZone;

logstashFormat = new SimpleDateFormat("yyyy.MM.dd");
logstashFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
timestamp = new java.util.Date();
index = "logstash-" + logstashFormat.format(timestamp);
jmxInfo = jmxCollect.getJsonJmxInfo("java.lang:type=Memory", timestamp, additionalRecords);
nativeInfo = sigar.getJsonProcessInfo(additionalRecords);
client.index(client.prepareIndex(index, "logs").setSource(jmxInfo).request()).actionGet();
client.index(client.prepareIndex(index, "logs").setSource(nativeInfo).request()).actionGet();
</jobExpression>
</timerTasks>
</globalContext>
</configuration>


У цій конфігурації створюємо екземпляри класів SigarCollect і JmxCollect, які завантажуються з бібліотеки com.github.igor-suhorukov:jvm-metrics:1.2 під час старту агента.

SigarCollect — дозволяє збирати дані про процес і операційній системі, багатьох з яких немає в jxm, і перетворює метрики в формат json. JmxCollect — дозволяє запитувати дані з jmx віртуальної машини і додатки, також перетворюючи їх в json документ. Інформацію з JmxCollect ми обмежили фільтром «java.lang:type=Memory», але вам точно знадобиться більше інформації з jxm бинов.

У additionalRecords додаємо reportHost — ім'я хоста, на якому запущено програму і ідентифікатор процесу pid

У client зберігаємо клієнт для elasticsearch, який приєднується до серверів з ім'ям Elasticsearch кластера «elasticsearchServer», який ми будемо використовувати для відправки метрик.

Наше завдання по відправці метрик буде виконуватися кожні 2 секунди і писати метрики в індекс з відповідною датою, наприклад logstash-2015.11.25. У цій задачі метрики з JmxCollect і SigarCollect повертаються як json об'єкти у форматі, який вміє візуалізувати kibana. Метрики з допомогою клієнта Elasticsearch відправляються в кластер і індексуються.

Як видем, вийшла конфігурація цілком знайомому розробникам java-подібний синтаксис мови MVEL.

В скрипт старту нашого додатка додаємо -javaagent:/?PATH?/aspectj-scripting-1.3-agent.jar=config:file:/?PATH?/log.metrics.xml
Запускаємо всі процеси додатки на всіх вузлах і дивимося як метрики течуть рікою в кластер Elasticsearch



А як же логи програми?
Ми вже відправляємо метрики в Elasticsearch, але там не вистачає іншої інформації від додатка. При використанні підходу з статті «Публікація логів в Elasticsearch — життя без регулярних виразів і без logstash» парсинг файлів журналів буде не потрібен. При зміні формату логування або появі нових повідомлень не потрібно підтримувати великий набір регулярок. У цьому підході пропоную перехоплювати дзвінки методів error, warn, info, debug, trace логера і відправляти дані відразу в elasticsearch.

Як бути з уже запущеними процесами jvm і причому тут groovy?
Скрипт на Groovy дозволить нам впровадити агент навіть у вже запущене в jvm додаток, вказавши лише ідентифікатор процесу і шлях до конфігурації. І навіть tools.jar з jvm не потрібен для впровадження агента в працюючий процес з допомогою AttachAPI. Це стало можливо завдяки бібліотеці com.github.igor-suhorukov:attach-vm:1.0, заснованої на класах з JMockit/OpenJDK.

Для запуску цього сценарію нам знадобиться спеціально приготовлений груви groovy-grape-aether-2.4.5.1.jar. Про можливості якого я нещодавно розповідав.
java -jar groovy-grape-aether-2.4.5.1.jar attachjvm.groovy JVM_PID config:file:?PATH?/log.metrics.xml
Скрипт attachjvm.groovy викачує aspectj-scripting:jar:agent з maven репозитарію і з допомогою AttachAPI підключається до jvm з номером процесу JVM_PID, завантажуючи в цю jvm aspectj-scripting агент з конфігурацією log.metrics.xml
@Grab(group='com.github.igor-suhorukov', module='attach-vm', version='1.0')
import com.github.igorsuhorukov.jvmattachapi.VirtualMachineUtils;
import com.sun.tools.attach.VirtualMachine;
import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader;

def aspectJScriptingFile = MavenClassLoader.forMavenCoordinates("com.github.igor-suhorukov:aspectj-scripting:jar:agent:1.3").getURLs().getAt(0).getFile()
println aspectJScriptingFile

def processId = this.args.getAt(0) //CliBuilder
def configPath = this.args.getAt(1)

VirtualMachine virtualMachine = VirtualMachineUtils.connectToVirtualMachine(processId)
try {
virtualMachine.loadAgent(aspectJScriptingFile, configPath)
} finally {
virtualMachine.detach()
}

Якщо ж буде потрібно діагностувати систему або змінити рівень логування, вбудовуємо кросплатформенный ssh server в java додаток. Який ми можемо впровадити у вже запущений java процес:
java -jar groovy-grape-aether-2.4.5.1.jar attachjvm.groovy JVM_PID config:file:?PATH?/CRaSHub_ssh.xml
З наступною конфігурацією CRaSHub_ssh.xml...
<?xml version="1.0" encoding="UTF-8" standalone="так"?>
<configuration>
<globalContext>
<artifacts>
<artifact>org.crashub:crash.connectors.ssh:1.3.1</artifact>
<classRefs>
<variable>Bootstrap</variable>
<className>org.crsh.standalone.Bootstrap</className>
</classRefs>
<classRefs>
<variable>Builder</variable>
<className>org.crsh.vfs.FS$Builder</className>
</classRefs>
<classRefs>
<variable>ClassPathMountFactory</variable>
<className>org.crsh.vfs.spi.url.ClassPathMountFactory</className>
</classRefs>
<classRefs>
<variable>FileMountFactory</variable>
<className>org.crsh.vfs.spi.file.FileMountFactory</className>
</classRefs>
</artifacts>
<init>
<expression>
import java.util.concurrent.TimeUnit;

otherCmd = new FileMountFactory(new java.io.File System.getProperty("user.dir")));
classLoader = otherCmd.getClass().getClassLoader();
classpathDriver = new ClassPathMountFactory(classLoader);
cmdFS = new Builder().register("classpath", classpathDriver).register("file", otherCmd).mount("classpath:/crash/commands/").build();
confFS = new Builder().register("classpath", classpathDriver).mount("classpath:/crash/").build();
bootstrap = new Bootstrap(classLoader, confFS, cmdFS);

config = new java.util.Properties();
config.put("crash.ssh.port", "2000");
config.put("crash.ssh.auth_timeout", "300000");
config.put("crash.ssh.idle_timeout", "300000");
config.put("crash.auth", "simple");
config.put("crash.auth.simple.username", "admin");
config.put("crash.auth.simple.password", "admin");

bootstrap.setConfig(config);
bootstrap.bootstrap();

Thread.sleep(TimeUnit.MINUTES.toMillis(30));

bootstrap.shutdown();
</expression>
</init>
</globalContext>
</configuration>




Висновки
Щоб спати спокійно, треба приділяти не меншу увагу завдань, пов'язаних з експлуатацією програми.

Тепер у нас є працюючий кластер Elasticsearch, дані з індексів якого ми можемо аналізувати в kibana. У Elasticsearch з додатків приходить інформація з метриками з JMX бинов, метрик процесу jvm і операційної системи. Сюди ж ми вже вміємо писати логи додатків. Налаштовуємо нотифікації і у випадку оповіщення швидко з'ясовуємо причину неполадок в нашому додатку та оперативно усуваємо неполадки. Більш того вбудовуємо кросплатформенный ssh server в java додаток і ми вже можемо діагностувати навіть працююче додаток без перезапуску. У тому числі перемикати в ньому рівень логування з допомогою CRaSH.

Бажаю вашому додатком стабільної роботи, а вам швидкого пошуку причин несправностей, якщо вони все ж виникли!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.