Внезапно, ночью раздается звонок и мы узнаем что наше приложение не работает. Есть 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 адресов упрощает жизнь в разработке, но в промышленной эксплуатации так настраивать кластер не стоит!
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="yes"?> <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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <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.
Желаю вашему приложению стабильной работы, а вам быстрого поиска причин неполадок, если они все же возникли!
ссылка на оригинал статьи http://habrahabr.ru/post/269793/
Добавить комментарий