Kibana中文指南


0 1 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2 1.2.1 1.2.1.1 1.2.1.2 1.2.1.3 1.2.1.4 1.2.1.5 1.2.2 1.2.2.1 1.2.2.2 1.2.2.3 1.2.3 1.2.3.1 1.2.3.2 1.2.3.3 1.2.3.4 1.2.3.5 1.2.3.6 1.2.3.7 1.2.3.8 1.2.3.9 1.2.3.10 目錄 前言 Logstash 入门示例 下载安装 hello world 配置语法 plugin的安装 长期运行 插件配置 input配置 collectd file stdin syslog tcp codec配置 json multiline netflow filter配置 date grok geoip json kv metrics mutate ruby split elapsed ELKstack 中文指南 2 1.2.4 1.2.4.1 1.2.4.2 1.2.4.3 1.2.4.4 1.2.4.5 1.2.4.6 1.2.4.7 1.2.4.8 1.2.4.9 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.4 1.4.1 1.4.2 1.4.2.1 1.4.2.2 1.5 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6 1.6.1 output配置 elasticsearch email exec file nagios statsd stdout tcp hdfs 场景示例 nginx访问日志 nginx错误日志 postfix日志 ossec日志 windows系统日志 Java日志 MySQL慢查询日志 性能与测试 generator方式 监控方案 logstash-input-heartbeat方式 jmx启动参数方式 扩展方案 通过redis传输 通过kafka传输 AIX 平台上的logstash-forwarder-java rsyslog nxlog heka fluent Message::Passing 源码解析 pipeline ELKstack 中文指南 3 1.6.2 1.7 1.7.1 2 2.1 2.1.1 2.1.2 2.2 2.2.1 2.2.2 2.3 2.4 2.5 3 3.1 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.2 3.2.1 3.2.2 3.2.3 3.2.4 3.3 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.4 plugins 插件开发 utmp插件示例 Beats 官方工具 filebeat packetbeat网络流量分析 社区工具 dockerbeat nginxbeat libbeat源码解析 beat开发示例 packetbeat协议开发示例 ElasticSearch 架构原理 segment、buffer和translog对实时性的影响 segment merge对写入性能的影响 routing和replica的读写过程 shard的allocate控制 自动发现的配置 接口使用示例 增删改查操作 搜索请求 script reindex 性能优化 bulk提交 gateway配置 集群状态维护 缓存 fielddata curator工具 扩展和测试方案 ELKstack 中文指南 4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.11.1 3.12 3.12.1 3.12.2 3.13 3.13.1 3.13.1.1 3.13.1.2 3.13.1.3 3.13.1.4 3.13.1.5 3.13.2 3.13.3 3.13.4 3.13.5 3.14 3.14.1 3.14.2 3.14.3 3.14.4 3.14.5 4 4.1 4.2 4.2.1 4.2.2 4.2.3 多集群互联 别名的应用 映射与模板的定制 puppet-elasticsearch模块的使用 计划内停机升级的操作流程 镜像备份 Hadoop 集成 spark streaming交互 权限管理 Shield Search-Guard 监控方案 监控相关接口 集群健康状态 节点状态 索引状态 等待执行的任务 cat 接口的命令行使用 日志记录 实时bigdesk方案 官方marvel方案 zabbix trapper方案 ES在运维监控领域的其他玩法 percolator api和watcher报警 ElastAlert 时序数据库 Grafana Etsy的Kale异常检测 Kibana k3和k4的对比 k3 kibana3入门 config.js配置 dashboard的保存和载入 ELKstack 中文指南 5 4.2.4 4.2.4.1 4.2.4.2 4.2.5 4.2.5.1 4.2.5.2 4.2.5.3 4.2.5.4 4.2.5.5 4.2.5.6 4.2.5.7 4.2.5.8 4.2.5.9 4.2.5.10 4.2.5.11 4.2.5.12 4.2.5.13 4.2.6 4.2.6.1 4.2.6.2 4.2.6.3 4.2.7 4.2.7.1 4.2.7.2 4.2.7.3 4.2.8 4.2.8.1 4.2.8.2 4.2.8.3 4.2.8.4 4.2.8.5 4.2.9 4.2.10 布局 query和filtering row和panel 各panel功能 histogram table map bettermap terms column stats query trend text sparklines hits goal 自定义dashboard功能 schema简介 template用法 scripted用法 认证授权 用nginx实现基础的认证 用nodejs实现基于CAS的认证 用perl实现认证和用户授权 k3源码解析 源码目录结构 入口和模块依赖 控制器和服务 面板指令 面板实现 用facet接口开发一个range panel 用agg接口开发一个percentile panel ELKstack 中文指南 6 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.4.1 4.3.4.2 4.3.4.3 4.3.4.4 4.3.4.5 4.3.4.6 4.3.4.7 4.3.4.8 4.3.5 4.3.6 4.3.7 4.3.7.1 4.3.7.2 4.3.7.3 4.3.7.4 4.3.7.5 4.3.8 4.3.8.1 4.3.8.2 4.3.8.3 4.3.8.4 4.3.8.5 4.3.8.6 4.3.9 4.3.9.1 4.3.9.2 4.3.9.3 4.4 5 k4 安装、配置和运行 生产环境部署 discover功能 各visualize功能 area table line markdown metric pie tile map vertical bar dashboard功能 setting功能 常用sub agg示例 函数堆栈链分析 分图统计 TopN的时序趋势图 响应时间的百分占比趋势图 响应时间的概率分布在不同时段的相似度对比 k4源码解析 .kibana索引的数据结构 主页入口 discover解析 visualize解析 dashboard解析 setting解析 k4 插件 timelion 介绍 可视化开发示例 后端开发示例 Kibana报表 竞品对比 ELKstack 中文指南 7 6 7 8 推荐阅读 合作名单 捐赠名单 ELKstack 中文指南 8 前言 ELK Stack 是 Elasticsearch、Logstash、Kibana 三个开源软件的组合。在实时数据检索和分 析场合,三者通常是配合共用,而且又都先后归于 Elastic.co 公司名下,故有此简称。 ELK Stack 在最近两年迅速崛起,成为机器数据分析,或者说实时日志处理领域,开源界的 第一选择。和传统的日志处理方案相比,ELK Stack 具有如下几个优点: 处理方式灵活。Elasticsearch 是实时全文索引,不需要像 storm 那样预先编程才能使 用; 配置简易上手。Elasticsearch 全部采用 JSON 接口,Logstash 是 Ruby DSL 设计,都是 目前业界最通用的配置语法设计; 检索性能高效。虽然每次查询都是实时计算,但是优秀的设计和实现基本可以达到全天 数据查询的秒级响应; 集群线性扩展。不管是 Elasticsearch 集群还是 Logstash 集群都是可以线性扩展的; 前端操作炫丽。Kibana 界面上,只需要点击鼠标,就可以完成搜索、聚合功能,生成炫 丽的仪表板。 当然,ELK Stack 也并不是实时数据分析界的灵丹妙药。在不恰当的场景,反而会事倍功 半。我自 2014 年初开 QQ 群交流 ELK Stack,发现网友们对 ELK Stack 的原理概念,常有 误解误用;对实现的效果,又多有不能理解或者过多期望而失望之处。更令我惊奇的是,网 友们广泛分布在传统企业和互联网公司、开发和运维领域、Linux 和 Windows 平台,大家对 非专精领域的知识,一般都缺乏了解,这也成为使用 ELK Stack 时的一个障碍。 为此,写一本 ELK Stack 技术指南,帮助大家厘清技术细节,分享一些实战案例,成为我近 半年一大心愿。本书大体完工之后,幸得机械工业出版社华章公司青眼,以《ELK Stack 权 威指南》之名重修完善并出版,有意收藏者欢迎购买。 我本人于 ELK Stack,虽然接触较早,但本身专于 web 和 app 应用数据方面,动笔以来,得 到诸多朋友的帮助,详细贡献名单见合作名单。此外,还要特别感谢曾勇(medcl)同学,完成 ES 在国内的启蒙式分享,并主办 ES 中国用户大会;吴晓刚(wood)同学,积极帮助新用户 们,并最早分享了携程的 ELK Stack 日亿级规模的实例。 欢迎加入 ELK Stack 交流 QQ 群:315428175。 欢迎捐赠,作者支付宝账号:rao.chenlin@gmail.com ELKstack 中文指南 9前言 Version 由于 ELK Stack 处于高速迭代状态,特标明本书知识主要针对的具体版本号: Logstash-2.2 Elasticsearch-2.x Kibana-4.4 beats-1.0.1 TODO 限于个人经验、时间和场景,有部分 ELK Stack 社区比较常见的用法介绍未完成,期待各位 同好出手。罗列如下: es-hadoop 用例 beats 开发 grafana 介绍 codec/netflow 的详解 filter/elapsed 的用例 K4的oauth2权限插件 juttle的ES后端支持 ELKstack 中文指南 10前言 第一部分 Logstash Logstash is a tool for managing events and logs. You can use it to collect logs, parse them, and store them for later use (like, for searching). -- http://logstash.net Logstash 项目诞生于 2009 年 8 月 2 日。其作者是世界著名的运维工程师乔丹西塞 (JordanSissel),乔丹西塞当时是著名虚拟主机托管商 DreamHost 的员工,还发布过非常棒 的软件打包工具 fpm,并主办着一年一度的 sysadmin advent calendar(advent calendar 文化 源自基督教氛围浓厚的 Perl 社区,在每年圣诞来临的 12 月举办,从 12 月 1 日起至 12 月 24 日止,每天发布一篇小短文介绍主题相关技术)。 小贴士:Logstash 动手很早,对比一下,scribed 诞生于 2008 年,flume 诞生于 2010 年, Graylog2 诞生于 2010 年,Fluentd 诞生于 2011 年。 scribed 在 2011 年进入半死不活的状态,大大激发了其他各种开源日志收集处理框架的蓬勃 发展,Logstash 也从 2011 年开始进入 commit 密集期并延续至今。 作为一个系出名门的产品,Logstash 的身影多次出现在 Sysadmin Weekly 上,它和它的小伙 伴们 Elasticsearch、Kibana 直接成为了和商业产品 Splunk 做比较的开源项目(乔丹西塞曾经 在博客上承认设计想法来自 AWS 平台上最大的第三方日志服务商 Loggly,而 Loggly 两位创 始人都曾是 Splunk 员工)。 2013 年,Logstash 被 Elasticsearch 公司收购,ELK stack 正式成为官方用语(虽然还没正式 命名)。Elasticsearch 本身 也是近两年最受关注的大数据项目之一,三次融资已经超过一亿美 元。在 Elasticsearch 开发人员的共同努力下,Logstash 的发布机制,插件架构也愈发科学和 合理。 社区文化 日志收集处理框架这么多,像 scribe 是 facebook 出品,flume 是 apache 基金会项目,都算 声名赫赫。但 logstash 因乔丹西塞的个人>性格,形成了一套独特的社区文化。每一个在 google groups 的 logstash-users 组里问答的人都会看到这么一句话: Remember: if a new user has a bad time, it's a bug in logstash. 所以,logstash 是一个开放的,极其互助和友好的大家庭。有任何问题,尽管在 github issue,Google groups,Freenode#logstash channel 上发问就好! ELKstack 中文指南 11Logstash 基础知识 什么是 Logstash?为什么要用 Logstash?怎么用 Logstash? 本章正是来回答这个问题,或许不完整,但是足够讲述一些基础概念。跟着我们安装章节一 步步来,你就可以成功的运行起来自己的第一个 logstash 了。 我可能不会立刻来展示 logstash 配置细节或者运用场景。我认为基础原理和语法的介绍应该 更加重要,这些知识未来对你的帮助绝对更大! 所以,认真阅读他们吧! ELKstack 中文指南 12入门示例 安装 下载 Logstash 从 1.5 版本开始,将核心代码和插件代码完全剥离,并重构了插件架构逻辑,所有 插件都以标准的 Ruby Gem 包形式发布。不过,为了方便大家从 1.4 版本过度,目前,官方 依然发布打包有所有官方维护的插件在内的软件包。只是不再发布类似原先 1.4 时代的 logstash-contrib.tar.gz 这样的软件包了。依然要使用社区插件的读者,请阅读稍后插件安 装章节。 下载官方软件包的方式有以下几种: 压缩包方式 wget https://download.elastic.co/logstash/logstash/logstash-1.5.1.tar.gz Debian 平台 wget https://download.elastic.co/logstash/logstash/packages/debian/logstash_1.5.1-1_all.deb Redhat 平台 wget https://download.elastic.co/logstash/logstash/packages/centos/logstash-1.5.1-1.noarch.rpm 安装 上面这些包,你可能更偏向使用 rpm , dpkg 等软件包管理工具来安装 Logstash,开发者在 软件包里预定义了一些依赖。比如, logstash-1.5.1-1.narch 就依赖于 jre 包。 另外,软件包里还包含有一些很有用的脚本程序,比如 /etc/init.d/logstash 。 如果你必须得在一些很老的操作系统上运行 Logstash,那你只能用源代码包部署了,记住要 自己提前安装好 Java: yum install java-1.8.0-openjdk export JAVA_HOME=/usr/java tar zxvf logstash-1.5.1.tar.gz ELKstack 中文指南 13下载安装 最佳实践 但是真正的建议是:如果可以,请用 Elasticsearch 官方仓库来直接安装 Logstash! Debian 平台 wget -O - http://packages.elasticsearch.org/GPG-KEY-elasticsearch | apt-key add - cat >> /etc/apt/sources.list < /etc/yum.repos.d/logstash.repo <rubydebug}}' 然后你会发现终端在等待你的输入。没问题,敲入 Hello World,回车,然后看看会返回什么 结果! 结果 { "message" => "Hello World", "@version" => "1", "@timestamp" => "2014-08-07T10:30:59.937Z", "host" => "raochenlindeMacBook-Air.local", } 没错!你搞定了!这就是全部你要做的。 解释 每位系统管理员都肯定写过很多类似这样的命令: cat randdata | awk '{print $2}' | sort | uniq -c | tee sortdata 。这个管道符 | 可以算是 Linux 世界最伟大的发明之一(另一个是“一 切皆文件”)。 Logstash 就像管道符一样! 你输入(就像命令行的 cat )数据,然后处理过滤(就像 awk 或者 uniq 之类)数据,最后输 出(就像 tee )到其他地方。 当然实际上,Logstash 是用不同的线程来实现这些的。如果你运行 top 命令然后按下 H 键,你就可以看到下面这样的输出: ELKstack 中文指南 15hello world PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 21401 root 16 0 1249m 303m 10m S 18.6 0.2 866:25.46 |worker 21467 root 15 0 1249m 303m 10m S 3.7 0.2 129:25.59 >elasticsearch. 21468 root 15 0 1249m 303m 10m S 3.7 0.2 128:53.39 >elasticsearch. 21400 root 15 0 1249m 303m 10m S 2.7 0.2 108:35.80 output 21470 root 15 0 1249m 303m 10m S 1.0 0.2 56:24.24 >elasticsearch. 小贴士:logstash 很温馨的给每个线程都取了名字,输入的叫xx,过滤的叫|xx 数据在线程之间以 事件 的形式流传。不要叫行,因为 logstash 可以处理多行事件。 Logstash 会给事件添加一些额外信息。最重要的就是 @timestamp,用来标记事件的发生时 间。因为这个字段涉及到 Logstash 的内部流转,所以必须是一个 joda 对象,如果你尝试自 己给一个字符串字段重命名为 @timestamp 的话,Logstash 会直接报错。所以,请使用 filters/date 插件 来管理这个特殊字段。 此外,大多数时候,还可以见到另外几个: 1. host 标记事件发生在哪里。 2. type 标记事件的唯一类型。 3. tags 标记事件的某方面属性。这是一个数组,一个事件可以有多个标签。 你可以随意给事件添加字段或者从事件里删除字段。事实上事件就是一个 Ruby 对象,或者更 简单的理解为就是一个哈希也行。 小贴士:每个 logstash 过滤插件,都会有四个方法叫 add_tag , remove_tag , add_field 和 remove_field 。它们在插件过滤匹配成功时生效。 推荐阅读 《the life of an event》官网文档 《life of a logstash event》Elastic{ON} 上的演讲 ELKstack 中文指南 16hello world 配置语法 Logstash 社区通常习惯用 shipper,broker 和 indexer 来描述数据流中不同进程各自的角色。 如下图: 不过我见过很多运用场景里都没有用 logstash 作为 shipper,或者说没有用 elasticsearch 作 为数据存储也就是说也没有 indexer。所以,我们其实不需要这些概念。只需要学好怎么使用 和配置 logstash 进程,然后把它运用到你的日志管理架构中最合适它的位置就够了。 语法 Logstash 设计了自己的 DSL —— 有点像 Puppet 的 DSL,或许因为都是用 Ruby 语言写的吧 —— 包括有区域,注释,数据类型(布尔值,字符串,数值,数组,哈希),条件判断,字段 引用等。 区段(section) Logstash 用 {} 来定义区域。区域内可以包括插件区域定义,你可以在一个区域内定义多个 插件。插件区域内则可以定义键值对设置。示例如下: input { stdin {} syslog {} } ELKstack 中文指南 17配置语法 数据类型 Logstash 支持少量的数据值类型: bool debug => true string host => "hostname" number port => 514 array match => ["datetime", "UNIX", "ISO8601"] hash options => { key1 => "value1", key2 => "value2" } 注意:如果你用的版本低于 1.2.0,哈希的语法跟数组是一样的,像下面这样写: match => [ "field1", "pattern1", "field2", "pattern2" ] 字段引用(field reference) 字段是 Logstash::Event 对象的属性。我们之前提过事件就像一个哈希一样,所以你可以想 象字段就像一个键值对。 小贴士:我们叫它字段,因为 Elasticsearch 里是这么叫的。 如果你想在 Logstash 配置中使用字段的值,只需要把字段的名字写在中括号 [] 里就行 了,这就叫字段引用。 ELKstack 中文指南 18配置语法 对于 嵌套字段(也就是多维哈希表,或者叫哈希的哈希),每层的字段名都写在 [] 里就可以 了。比如,你可以从 geoip 里这样获取 longitude 值(是的,这是个笨办法,实际上有单独的 字段专门存这个数据的): [geoip][location][0] 小贴士:logstash 的数组也支持倒序下标,即 [geoip][location][-1] 可以获取数组最后一个 元素的值。 Logstash 还支持变量内插,在字符串里使用字段引用的方法是这样: "the longitude is %{[geoip][location][0]}" 条件判断(condition) Logstash从 1.3.0 版开始支持条件判断和表达式。 表达式支持下面这些操作符: == (等于), != (不等于), < (小于), > (大于), <= (小于等于), >= (大于等于) =~ (匹配正则), !~ (不匹配正则) in (包含), not in (不包含) and (与), or (或), nand(非与), xor(非或) () (复合表达式), !() (对复合表达式结果取反) 通常来说,你都会在表达式里用到字段引用。为了尽量展示全面各种表达式,下面虚拟一个 示例: if "_grokparsefailure" not in [tags] { } else if [status] !~ /^2\d\d/ or ( [url] == "/noc.gif" nand [geoip][city] != "beijing" ) { } else { } 命令行参数 Logstash 提供了一个 shell 脚本叫 logstash 方便快速运行。它支持一下参数: -e 意即执行。我们在 "Hello World" 的时候已经用过这个参数了。事实上你可以不写任何具体配 置,直接运行 bin/logstash -e '' 达到相同效果。这个参数的默认值是下面这样: ELKstack 中文指南 19配置语法 input { stdin { } } output { stdout { } } --config 或 -f 意即文件。真实运用中,我们会写很长的配置,甚至可能超过 shell 所能支持的 1024 个字符 长度。所以我们必把配置固化到文件里,然后通过 bin/logstash -f agent.conf 这样的形式 来运行。 此外,logstash 还提供一个方便我们规划和书写配置的小功能。你可以直接用 bin/logstash -f /etc/logstash.d/ 来运行。logstash 会自动读取 /etc/logstash.d/ 目录下所有 *.conf 的文本文件,然后在自己内存里拼接成一个完整的大配置文件,再去执行。 注意: logstash 列出目录下所有文件时,是字母排序的。而 logstash 配置段的 filter 和 output 都是 顺序执行,所以顺序非常重要。采用多文件管理的用户,推荐采用数字编号方式命名配置文 件,同时在配置中,严谨采用 if 判断限定不同日志的动作。 --configtest 或 -t 意即测试。用来测试 Logstash 读取到的配置文件语法是否能正常解析。Logstash 配置语法 是用 grammar.treetop 定义的。尤其是使用了上一条提到的读取目录方式的读者,尤其要提前 测试。 --log 或 -l 意即日志。Logstash 默认输出日志到标准错误。生产环境下你可以通过 bin/logstash -l logs/logstash.log 命令来统一存储日志。 --pipeline-workers 或 -w 运行 filter 和 output 的 pipeline 线程数量。默认是 CPU 核数。 --pipeline-batch-size 或 -b 每个 Logstash pipeline 线程,在执行具体的 filter 和 output 函数之前,最多能累积的日志条 数。默认是 125 条。越大性能越好,同样也会消耗越多的 JVM 内存。 --pipeline-batch-delay 或 -u 每个 Logstash pipeline 线程,在打包批量日志的时候,最多等待几毫秒。默认是 5 ms。 --pluginpath 或 -P ELKstack 中文指南 20配置语法 可以写自己的插件,然后用 bin/logstash --pluginpath /path/to/own/plugins 加载它们。 --verbose 输出一定的调试日志。 --debug 输出更多的调试日志。 ELKstack 中文指南 21配置语法 plugin的安装 从 logstash 1.5.0 版本开始,logstash 将所有的插件都独立拆分成 gem 包。这样,每个插件 都可以独立更新,不用等待 logstash 自身做整体更新的时候才能使用了。 为了达到这个目标,logstash 配置了专门的 plugins 管理命令。 plugin 用法说明 Usage: bin/plugin [OPTIONS] SUBCOMMAND [ARG] ... Parameters: SUBCOMMAND subcommand [ARG] ... subcommand arguments Subcommands: install Install a plugin uninstall Uninstall a plugin update Install a plugin list List all installed plugins Options: -h, --help print help 示例 首先,你可以通过 bin/plugin list 查看本机现在有多少插件可用。(其实就在 vendor/bundle/jruby/1.9/gems/ 目录下) 然后,假如你看到 https://github.com/logstash-plugins/ 下新发布了一个 logstash-output- webhdfs 模块(当然目前还没有)。打算试试,就只需要运行: bin/plugin install logstash-output-webhdfs 就可以了。 同样,假如是升级,只需要运行: bin/plugin update logstash-input-tcp ELKstack 中文指南 22plugin的安装 即可。 本地插件安装 bin/plugin 不单可以通过 rubygems 平台安装插件,还可以读取本地路径的 gem 文件。这 对自定义插件或者无外接网络的环境都非常有效: bin/plugin install /path/to/logstash-filter-crash.gem 执行成功以后。你会发现,logstash-1.5.0 目录下的 Gemfile 文件最后会多出一段内容: gem "logstash-filter-crash", "1.1.0", :path => "vendor/local_gems/d354312c/logstash-filter-mweibocrash-1.1.0" 同时 Gemfile.jruby-1.9.lock 文件开头也会多出一段内容: PATH remote: vendor/local_gems/d354312c/logstash-filter-crash-1.1.0 specs: logstash-filter-crash (1.1.0) logstash-core (>= 1.4.0, < 2.0.0) ELKstack 中文指南 23plugin的安装 长期运行 完成上一节的初次运行后,你肯定会发现一点:一旦你按下 Ctrl+C,停下标准输入输出, logstash 进程也就随之停止了。作为一个肯定要长期运行的程序,应该怎么处理呢? 本章节问题对于一个运维来说应该属于基础知识,鉴于 ELK 用户很多其实不是运维,添加这 段内容。 办法有很多种,下面介绍四种最常用的办法: 标准的 service 方式 采用 RPM、DEB 发行包安装的读者,推荐采用这种方式。发行包内,都自带有 sysV 或者 systemd 风格的启动程序/配置,你只需要直接使用即可。 以 RPM 为例, /etc/init.d/logstash 脚本中,会加载 /etc/init.d/functions 库文件,利用 其中的 daemon 函数,将 logstash 进程作为后台程序运行。 所以,你只需把自己写好的配置文件,统一放在 /etc/logstash/ 目录下(注意目录下所有配 置文件都应该是 .conf 结尾,且不能有其他文本文件存在。因为 logstash agent 启动的时候 是读取全文件夹的),然后运行 service logstash start 命令即可。 最基础的 nohup 方式 这是最简单的方式,也是 linux 新手们很容易搞混淆的一个经典问题: command command > /dev/null command > /dev/null 2>&1 command & command > /dev/null & command > /dev/null 2>&1 & command &> /dev/null nohup command &> /dev/null 请回答以上命令的异同…… 具体不一一解释了。直接说答案,想要维持一个长期后台运行的 logstash,你需要同时在命 令前面加 nohup ,后面加 & 。 ELKstack 中文指南 24长期运行 更优雅的 SCREEN 方式 screen 算是 linux 运维一个中高级技巧。通过 screen 命令创建的环境下运行的终端命令,其 父进程不是 sshd 登录会话,而是 screen 。这样就可以即避免用户退出进程消失的问题,又 随时能重新接管回终端继续操作。 创建独立的 screen 命令如下: screen -dmS elkscreen_1 接管连入创建的 elkscreen_1 命令如下: screen -r elkscreen_1 然后你可以看到一个一模一样的终端,运行 logstash 之后,不要按 Ctrl+C,而是按 Ctrl+A+D 键,断开环境。想重新接管,依然 screen -r elkscreen_1 即可。 如果创建了多个 screen,查看列表命令如下: screen -list 最推荐的 daemontools 方式 不管是 nohup 还是 screen,都不是可以很方便管理的方式,在运维管理一个 ELK 集群的时 候,必须寻找一种尽可能简洁的办法。所以,对于需要长期后台运行的大量程序(注意大量, 如果就一个进程,还是学习一下怎么写 init 脚本吧),推荐大家使用一款 daemontools 工具。 daemontools 是一个软件名称,不过配置略复杂。所以这里我其实是用其名称来指代整个同 类产品,包括但不限于 python 实现的 supervisord,perl 实现的 ubic,ruby 实现的 god 等。 以 supervisord 为例,因为这个出来的比较早,可以直接通过 EPEL 仓库安装。 yum -y install supervisord --enablerepo=epel 在 /etc/supervisord.conf 配置文件里添加内容,定义你要启动的程序: ELKstack 中文指南 25长期运行 [program:elkpro_1] environment=LS_HEAP_SIZE=5000m directory=/opt/logstash command=/opt/logstash/bin/logstash -f /etc/logstash/pro1.conf -w 10 -l /var/log/logstash/pro1.log [program:elkpro_2] environment=LS_HEAP_SIZE=5000m directory=/opt/logstash command=/opt/logstash/bin/logstash -f /etc/logstash/pro2.conf -w 10 -l /var/log/logstash/pro2.log 然后启动 service supervisord start 即可。 logstash 会以 supervisord 子进程的身份运行,你还可以使用 supervisorctl 命令,单独控 制一系列 logstash 子进程中某一个进程的启停操作: supervisorctl stop elkpro_2 ELKstack 中文指南 26长期运行 输入插件(Input) 在 "Hello World" 示例中,我们已经见到并介绍了 logstash 的运行流程和配置的基础语法。从 这章开始,我们就要逐一介绍 logstash 流程中比较常用的一些插件,并在介绍中针对其主要 适用的场景,推荐的配置,作一些说明。 限于篇幅,接下来内容中,配置示例不一定能贴完整。请记住一个原则:Logstash 配置一定 要有一个 input 和一个 output。在演示过程中,如果没有写明 input,默认就会使用 "hello world" 里我们已经演示过的 input/stdin ,同理,没有写明的 output 就是 output/stdout。 以上请读者自明。 ELKstack 中文指南 27input配置 collectd简述 本节作者:crazw collectd 是一个守护(daemon)进程,用来收集系统性能和提供各种存储方式来存储不同值的 机制。它会在系统运行和存储信息时周期性的统计系统的相关统计信息。利用这些信息有助 于查找当前系统性能瓶颈(如作为性能分析 performance analysis )和预测系统未来的 load(如能力部署 capacity planning )等 下面简单介绍一下: collectd的部署以及与logstash对接的相关配置实例 collectd的安装 解决依赖 rpm -ivh "http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm" yum -y install libcurl libcurl-devel rrdtool rrdtool-devel perl-rrdtool rrdtool-prel libgcrypt-devel gcc make gcc-c++ liboping liboping-devel perl-CPAN net-snmp net-snmp-devel 源码安装collectd wget http://collectd.org/files/collectd-5.4.1.tar.gz tar zxvf collectd-5.4.1.tar.gz cd collectd-5.4.1 ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --libdir=/usr/lib --mandir=/usr/share/man --enable-all-plugins make && make install 安装启动脚本 cp contrib/redhat/init.d-collectd /etc/init.d/collectd chmod +x /etc/init.d/collectd 启动collectd service collectd start ELKstack 中文指南 28input配置 collectd的配置 以下配置可以实现对服务器基本的CPU、内存、网卡流量、磁盘 IO 以及磁盘空间占用情况的 监控: Hostname "host.example.com" LoadPlugin interface LoadPlugin cpu LoadPlugin memory LoadPlugin network LoadPlugin df LoadPlugin disk Interface "eth0" IgnoreSelected false ## logstash 的 IP 地址和 collectd 的数据接收端口号 logstash的配置 以下配置实现通过 logstash 监听 25826 端口,接收从 collectd 发送过来的各项检测数据: 示例一: input { collectd { port => 25826 ## 端口号与发送端对应 type => collectd } 示例二:(推荐) udp { port => 25826 buffer_size => 1452 workers => 3 # Default is 2 queue_size => 30000 # Default is 2000 codec => collectd { } type => "collectd" } ELKstack 中文指南 29input配置 运行结果 下面是简单的一个输出结果: { "_index": "logstash-2014.12.11", "_type": "collectd", "_id": "dS6vVz4aRtK5xS86kwjZnw", "_score": null, "_source": { "host": "host.example.com", "@timestamp": "2014-12-11T06:28:52.118Z", "plugin": "interface", "plugin_instance": "eth0", "collectd_type": "if_packets", "rx": 19147144, "tx": 3608629, "@version": "1", "type": "collectd", "tags": [ "_grokparsefailure" ] }, "sort": [ 1418279332118 ] } 参考资料 collectd支持收集的数据类型: http://git.verplant.org/? p=collectd.git;a=blob;hb=master;f=README collectd收集各数据类型的配置参考资料: http://collectd.org/documentation/manpages/collectd.conf.5.shtml collectd简单配置文件示例: https://gist.github.com/untergeek/ab85cb86a9bf39f1fc6d ELKstack 中文指南 30input配置 读取文件(File) 分析网站访问日志应该是一个运维工程师最常见的工作了。所以我们先学习一下怎么用 logstash 来处理日志文件。 Logstash 使用一个名叫 FileWatch 的 Ruby Gem 库来监听文件变化。这个库支持 glob 展开 文件路径,而且会记录一个叫 .sincedb 的数据库文件来跟踪被监听的日志文件的当前读取位 置。所以,不要担心 logstash 会漏过你的数据。 sincedb 文件中记录了每个被监听的文件的 inode, major number, minor number 和 pos。 配置示例 input { file { path => ["/var/log/*.log", "/var/log/message"] type => "system" start_position => "beginning" } } 解释 有一些比较有用的配置项,可以用来指定 FileWatch 库的行为: discover_interval logstash 每隔多久去检查一次被监听的 path 下是否有新文件。默认值是 15 秒。 exclude 不想被监听的文件可以排除出去,这里跟 path 一样支持 glob 展开。 close_older 一个已经监听中的文件,如果超过这个值的时间内没有更新内容,就关闭监听它的文件句 柄。默认是 3600 秒,即一小时。 ignore_older 在每次检查文件列表的时候,如果一个文件的最后修改时间超过这个值,就忽略这个文件。 默认是 86400 秒,即一天。 ELKstack 中文指南 31input配置 sincedb_path 如果你不想用默认的 $HOME/.sincedb (Windows 平台上在 C:\Windows\System32\config\systemprofile\.sincedb ),可以通过这个配置定义 sincedb 文件 到其他位置。 sincedb_write_interval logstash 每隔多久写一次 sincedb 文件,默认是 15 秒。 stat_interval logstash 每隔多久检查一次被监听文件状态(是否有更新),默认是 1 秒。 start_position logstash 从什么位置开始读取文件数据,默认是结束位置,也就是说 logstash 进程会以类似 tail -F 的形式运行。如果你是要导入原有数据,把这个设定改成 "beginning",logstash 进 程就从头开始读取,有点类似 cat ,但是读到最后一行不会终止,而是继续变成 tail -F 。 注意 1. 通常你要导入原有数据进 Elasticsearch 的话,你还需要 filter/date 插件来修改默认 的"@timestamp" 字段值。稍后会学习这方面的知识。 2. FileWatch 只支持文件的绝对路径,而且会不自动递归目录。所以有需要的话,请用数组 方式都写明具体哪些文件。 3. LogStash::Inputs::File 只是在进程运行的注册阶段初始化一个 FileWatch 对象。所以它不 能支持类似 fluentd 那样的 path => "/path/to/%{+yyyy/MM/dd/hh}.log" 写法。达到相同 目的,你只能写成 path => "/path/to/*/*/*/*.log" 。FileWatch 模块提供了一个稍微简 单一点的写法: /path/to/**/*.log ,用 ** 来缩写表示递归全部子目录。 4. start_position 仅在该文件从未被监听过的时候起作用。如果 sincedb 文件中已经有这 个文件的 inode 记录了,那么 logstash 依然会从记录过的 pos 开始读取数据。所以重复 测试的时候每回需要删除 sincedb 文件(官方博客上提供了另一个巧妙的思路:将 sincedb_path 定义为 /dev/null ,则每次重启自动从头开始读)。 5. 因为 windows 平台上没有 inode 的概念,Logstash 某些版本在 windows 平台上监听文 件不是很靠谱。windows 平台上,推荐考虑使用 nxlog 作为收集端,参阅本书稍后章 节。 ELKstack 中文指南 32input配置 标准输入(Stdin) 我们已经见过好几个示例使用 stdin 了。这也应该是 logstash 里最简单和基础的插件了。 所以,在这段中,我们可以学到一些未来每个插件都会有的一些方法。 配置示例 input { stdin { add_field => {"key" => "value"} codec => "plain" tags => ["add"] type => "std" } } 运行结果 用上面的新 stdin 设置重新运行一次最开始的 hello world 示例。我建议大家把整段配置都 写入一个文本文件,然后运行命令: bin/logstash -f stdin.conf 。输入 "hello world" 并回车 后,你会在终端看到如下输出: { "message" => "hello world", "@version" => "1", "@timestamp" => "2014-08-08T06:48:47.789Z", "type" => "std", "tags" => [ [0] "add" ], "key" => "value", "host" => "raochenlindeMacBook-Air.local" } 解释 ELKstack 中文指南 33input配置 type 和 tags 是 logstash 事件中两个特殊的字段。通常来说我们会在输入区段中通过 type 来 标记事件类型 —— 我们肯定是提前能知道这个事件属于什么类型的。而 tags 则是在数据处 理过程中,由具体的插件来添加或者删除的。 最常见的用法是像下面这样: input { stdin { type => "web" } } filter { if [type] == "web" { grok { match => ["message", %{COMBINEDAPACHELOG}] } } } output { if "_grokparsefailure" in [tags] { nagios_nsca { nagios_status => "1" } } else { elasticsearch { } } } 看起来蛮复杂的,对吧? 继续学习,你也可以写出来的。 ELKstack 中文指南 34input配置 读取 Syslog 数据 syslog 可能是运维领域最流行的数据传输协议了。当你想从设备上收集系统日志的时候, syslog 应该会是你的第一选择。尤其是网络设备,比如思科 —— syslog 几乎是唯一可行的办 法。 我们这里不解释如何配置你的 syslog.conf , rsyslog.conf 或者 syslog-ng.conf 来发送数 据,而只讲如何把 logstash 配置成一个 syslog 服务器来接收数据。 有关 rsyslog 的用法,稍后的类型项目一节中,会有更详细的介绍。 配置示例 input { syslog { port => "514" } } 运行结果 作为最简单的测试,我们先暂停一下本机的 syslogd (或 rsyslogd )进程,然后启动 logstash 进程(这样就不会有端口冲突问题)。现在,本机的 syslog 就会默认发送到 logstash 里了。我们可以用自带的 logger 命令行工具发送一条 "Hello World"信息到 syslog 里(即 logstash 里)。看到的 logstash 输出像下面这样: { "message" => "Hello World", "@version" => "1", "@timestamp" => "2014-08-08T09:01:15.911Z", "host" => "127.0.0.1", "priority" => 31, "timestamp" => "Aug 8 17:01:15", "logsource" => "raochenlindeMacBook-Air.local", "program" => "com.apple.metadata.mdflagwriter", "pid" => "381", "severity" => 7, "facility" => 3, "facility_label" => "system", "severity_label" => "Debug" } ELKstack 中文指南 35input配置 解释 Logstash 是用 UDPSocket , TCPServer 和 LogStash::Filters::Grok 来实现 LogStash::Inputs::Syslog 的。所以你其实可以直接用 logstash 配置实现一样的效果: input { tcp { port => "8514" } } filter { grok { match => ["message", "%{SYSLOGLINE}" ] } syslog_pri { } } 最佳实践 建议在使用 LogStash::Inputs::Syslog 的时候走 TCP 协议来传输数据。 因为具体实现中,UDP 监听器只用了一个线程,而 TCP 监听器会在接收每个连接的时候都启 动新的线程来处理后续步骤。 如果你已经在使用 UDP 监听器收集日志,用下行命令检查你的 UDP 接收队列大小: # netstat -plnu | awk 'NR==1 || $4~/:514$/{print $2}' Recv-Q 228096 228096 是 UDP 接收队列的默认最大大小,这时候 linux 内核开始丢弃数据包了! 强烈建议使用 LogStash::Inputs::TCP 和 LogStash::Filters::Grok 配合实现同样的 syslog 功 能! 虽然 LogStash::Inputs::Syslog 在使用 TCPServer 的时候可以采用多线程处理数据的接收, 但是在同一个客户端数据的处理中,其 grok 和 date 是一直在该线程中完成的,这会导致总 体上的处理性能几何级的下降 —— 经过测试,TCPServer 每秒可以接收 50000 条数据,而 在同一线程中启用 grok 后每秒只能处理 5000 条,再加上 date 只能达到 500 条! 才将这两步拆分到 filters 阶段后,logstash 支持对该阶段插件单独设置多线程运行,大大提 高了总体处理性能。在相同环境下, logstash -f tcp.conf -w 20 的测试中,总体处理性能 可以达到每秒 30000 条数据! ELKstack 中文指南 36input配置 注:测试采用 logstash 作者提供的 yes "<44>May 19 18:30:17 snack jls: foo bar 32" | nc localhost 3000 命令。出处 见:https://github.com/jordansissel/experiments/blob/master/ruby/jruby-netty/syslog- server/Makefile 小贴士 如果你实在没法切换到 TCP 协议,你可以自己写程序,或者使用其他基于异步 IO 框架(比如 libev )的项目。下面是一个简单的异步 IO 实现 UDP 监听数据输入 Elasticsearch 的示例: https://gist.github.com/chenryn/7c922ac424324ee0d695 ELKstack 中文指南 37input配置 读取网络数据(TCP) 未来你可能会用 Redis 服务器或者其他的消息队列系统来作为 logstash broker 的角色。不过 Logstash 其实也有自己的 TCP/UDP 插件,在临时任务的时候,也算能用,尤其是测试环 境。 小贴士:虽然 LogStash::Inputs::TCP 用 Ruby 的 Socket 和 OpenSSL 库实现了高级的 SSL 功能,但 Logstash 本身只能在 SizedQueue 中缓存 20 个事件。这就是我们建议在生产环境 中换用其他消息队列的原因。 配置示例 input { tcp { port => 8888 mode => "server" ssl_enable => false } } 常见场景 目前来看, LogStash::Inputs::TCP 最常见的用法就是配合 nc 命令导入旧数据。在启动 logstash 进程后,在另一个终端运行如下命令即可导入数据: # nc 127.0.0.1 8888 < olddata 这种做法比用 LogStash::Inputs::File 好,因为当 nc 命令结束,我们就知道数据导入完毕 了。而用 input/file 方式,logstash 进程还会一直等待新数据输入被监听的文件,不能直接看 出是否任务完成了。 ELKstack 中文指南 38input配置 编码插件(Codec) Codec 是 logstash 从 1.3.0 版开始新引入的概念(Codec 来自 Coder/decoder 两个单词的首字 母缩写)。 在此之前,logstash 只支持纯文本形式输入,然后以过滤器处理它。但现在,我们可以在输 入 期处理不同类型的数据,这全是因为有了 codec 设置。 所以,这里需要纠正之前的一个概念。Logstash 不只是一个 input | filter | output 的数据 流,而是一个 input | decode | filter | encode | output 的数据流!codec 就是用来 decode、encode 事件的。 codec 的引入,使得 logstash 可以更好更方便的与其他有自定义数据格式的运维产品共存, 比如 graphite、fluent、netflow、collectd,以及使用 msgpack、json、edn 等通用数据格式 的其他产品等。 事实上,我们在第一个 "hello world" 用例中就已经用过 codec 了 —— rubydebug 就是一种 codec!虽然它一般只会用在 stdout 插件中,作为配置测试或者调试的工具。 小贴士:这个五段式的流程说明源自 Perl 版的 Logstash (后来改名叫 Message::Passing 模 块)的设计。本书最后会对该模块稍作介绍。 ELKstack 中文指南 39codec配置 采用 JSON 编码 在早期的版本中,有一种降低 logstash 过滤器的 CPU 负载消耗的做法盛行于社区(在当时的 cookbook 上有专门的一节介绍):直接输入预定义好的 JSON 数据,这样就可以省略掉 filter/grok 配置! 这个建议依然有效,不过在当前版本中需要稍微做一点配置变动 —— 因为现在有专门的 codec 设置。 配置示例 社区常见的示例都是用的 Apache 的 customlog。不过我觉得 Nginx 是一个比 Apache 更常用 的新型 web 服务器,所以我这里会用 nginx.conf 做示例: logformat json '{"@timestamp":"$time_iso8601",' '"@version":"1",' '"host":"$server_addr",' '"client":"$remote_addr",' '"size":$body_bytes_sent,' '"responsetime":$request_time,' '"domain":"$host",' '"url":"$uri",' '"status":"$status"}'; access_log /var/log/nginx/access.log_json json; 注意:在 $request_time 和 $body_bytes_sent 变量两头没有双引号 " ,这两个数据在 JSON 里应该是数值类型! 重启 nginx 应用,然后修改你的 input/file 区段配置成下面这样: input { file { path => "/var/log/nginx/access.log_json"" codec => "json" } } 运行结果 ELKstack 中文指南 40codec配置 下面访问一下你 nginx 发布的 web 页面,然后你会看到 logstash 进程输出类似下面这样的内 容: { "@timestamp" => "2014-03-21T18:52:25.000+08:00", "@version" => "1", "host" => "raochenlindeMacBook-Air.local", "client" => "123.125.74.53", "size" => 8096, "responsetime" => 0.04, "domain" => "www.domain.com", "url" => "/path/to/file.suffix", "status" => "200" } 小贴士 对于一个 web 服务器的访问日志,看起来已经可以很好的工作了。不过如果 Nginx 是作为一 个代理服务器运行的话,访问日志里有些变量,比如说 $upstream_response_time ,可能不会 一直是数字,它也可能是一个 "-" 字符串!这会直接导致 logstash 对输入数据验证报异 常。 有两个办法解决这个问题: 1. 用 sed 在输入之前先替换 - 成 0 。 运行 logstash 进程时不再读取文件而是标准输入,这样命令就成了下面这个样子: tail -F /var/log/nginx/proxy_access.log_json \ | sed 's/upstreamtime":-/upstreamtime":0/' \ | /usr/local/logstash/bin/logstash -f /usr/local/logstash/etc/proxylog.conf 1. 日志格式中统一记录为字符串格式(即都带上双引号 " ),然后再在 logstash 中用 filter/mutate 插件来变更应该是数值类型的字符字段的值类型。 有关 LogStash::Filters::Mutate 的内容,本书稍后会有介绍。 ELKstack 中文指南 41codec配置 合并多行数据(Multiline) 有些时候,应用程序调试日志会包含非常丰富的内容,为一个事件打印出很多行内容。这种 日志通常都很难通过命令行解析的方式做分析。 而 logstash 正为此准备好了 codec/multiline 插件! 小贴士:multiline 插件也可以用于其他类似的堆栈式信息,比如 linux 的内核日志。 配置示例 input { stdin { codec => multiline { pattern => "^\[" negate => true what => "previous" } } } 运行结果 运行 logstash 进程,然后在等待输入的终端中输入如下几行数据: [Aug/08/08 14:54:03] hello world [Aug/08/09 14:54:04] hello logstash hello best practice hello raochenlin [Aug/08/10 14:54:05] the end 你会发现 logstash 输出下面这样的返回: ELKstack 中文指南 42codec配置 { "@timestamp" => "2014-08-09T13:32:03.368Z", "message" => "[Aug/08/08 14:54:03] hello world\n", "@version" => "1", "host" => "raochenlindeMacBook-Air.local" } { "@timestamp" => "2014-08-09T13:32:24.359Z", "message" => "[Aug/08/09 14:54:04] hello logstash\n\n hello best practice\n\n hello raochenlin\n" "@version" => "1", "tags" => [ [0] "multiline" ], "host" => "raochenlindeMacBook-Air.local" } 你看,后面这个事件,在 "message" 字段里存储了三行数据! 小贴士:你可能注意到输出的事件中都没有最后的"the end"字符串。这是因为你最后输入的 回车符 \n 并不匹配设定的 ^\[ 正则表达式,logstash 还得等下一行数据直到匹配成功后 才会输出这个事件。 解释 其实这个插件的原理很简单,就是把当前行的数据添加到前面一行后面,,直到新进的当前 行匹配 ^\[ 正则为止。 这个正则还可以用 grok 表达式,稍后你就会学习这方面的内容。 Log4J 的另一种方案 说到应用程序日志,log4j 肯定是第一个被大家想到的。使用 codec/multiline 也确实是一个 办法。 不过,如果你本身就是开发人员,或者可以推动程序修改变更的话,logstash 还提供了另一 种处理 log4j 的方式:input/log4j。与 codec/multiline 不同,这个插件是直接调用了 org.apache.log4j.spi.LoggingEvent 处理 TCP 端口接收的数据。稍后章节会详细讲述 log4j 的用法。 推荐阅读 https://github.com/logstash-plugins/logstash-patterns-core/blob/master/patterns/java ELKstack 中文指南 43codec配置 ELKstack 中文指南 44codec配置 netflow input { udp { port => 9995 codec => netflow { definitions => "/home/administrator/logstash-1.4.2/lib/logstash/codecs/netflow/netflow.yaml" versions => [5] } } } output { stdout { codec => rubydebug } if ( [host] =~ "10\.1\.1[12]\.1" ) { elasticsearch { index => "logstash_netflow5-%{+YYYY.MM.dd}" host => "localhost" } } else { elasticsearch { index => "logstash-%{+YYYY.MM.dd}" host => "localhost" } } } ELKstack 中文指南 45codec配置 curl -XPUT localhost:9200/_template/logstash_netflow5 -d '{ "template" : "logstash_netflow5-*", "settings": { "index.refresh_interval": "5s" }, "mappings" : { "_default_" : { "_all" : {"enabled" : false}, "properties" : { "@version": { "index": "analyzed", "type": "integer" }, "@timestamp": { "index": "analyzed", "type": "date" }, "netflow": { "dynamic": true, "type": "object", "properties": { "version": { "index": "analyzed", "type": "integer" }, "flow_seq_num": { "index": "not_analyzed", "type": "long" }, "engine_type": { "index": "not_analyzed", "type": "integer" }, "engine_id": { "index": "not_analyzed", "type": "integer" }, "sampling_algorithm": { "index": "not_analyzed", "type": "integer" }, "sampling_interval": { "index": "not_analyzed", "type": "integer" }, "flow_records": { "index": "not_analyzed", "type": "integer" }, "ipv4_src_addr": { "index": "analyzed", "type": "ip" }, "ipv4_dst_addr": { "index": "analyzed", "type": "ip" }, "ipv4_next_hop": { "index": "analyzed", "type": "ip" }, "input_snmp": { "index": "not_analyzed", "type": "long" }, "output_snmp": { "index": "not_analyzed", "type": "long" }, "in_pkts": { "index": "analyzed", "type": "long" }, "in_bytes": { "index": "analyzed", "type": "long" }, "first_switched": { "index": "not_analyzed", "type": "date" }, "last_switched": { "index": "not_analyzed", "type": "date" }, "l4_src_port": { "index": "analyzed", "type": "long" }, "l4_dst_port": { "index": "analyzed", "type": "long" }, "tcp_flags": { "index": "analyzed", "type": "integer" }, "protocol": { "index": "analyzed", "type": "integer" }, "src_tos": { "index": "analyzed", "type": "integer" }, "src_as": { "index": "analyzed", "type": "integer" }, "dst_as": { "index": "analyzed", "type": "integer" }, "src_mask": { "index": "analyzed", "type": "integer" }, "dst_mask": { "index": "analyzed", "type": "integer" } } } } } } }' ELKstack 中文指南 46codec配置 过滤器插件(Filter) 丰富的过滤器插件的存在是 logstash 威力如此强大的重要因素。名为过滤器,其实提供的不 单单是过滤的功能。在本章我们就会重点介绍几个插件,它们扩展了进入过滤器的原始数 据,进行复杂的逻辑处理,甚至可以无中生有的添加新的 logstash 事件到后续的流程中去! ELKstack 中文指南 47filter配置 时间处理(Date) 之前章节已经提过,filters/date 插件可以用来转换你的日志记录中的时间字符串,变成 LogStash::Timestamp 对象,然后转存到 @timestamp 字段里。 注意:因为在稍后的 outputs/elasticsearch 中常用的 %{+YYYY.MM.dd} 这种写法必须读取 @timestamp 数据,所以一定不要直接删掉这个字段保留自己的字段,而是应该用 filters/date 转换后删除自己的字段! 这在导入旧数据的时候固然非常有用,而在实时数据处理的时候同样有效,因为一般情况下 数据流程中我们都会有缓冲区,导致最终的实际处理时间跟事件产生时间略有偏差。 小贴士:个人强烈建议打开 Nginx 的 access_log 配置项的 buffer 参数,对极限响应性能有极 大提升! 配置示例 filters/date 插件支持五种时间格式: ISO8601 类似 "2011-04-19T03:44:01.103Z" 这样的格式。具体Z后面可以有 "08:00"也可以没 有,".103"这个也可以没有。常用场景里来说,Nginx 的 log_format 配置里就可以使用 $time_iso8601 变量来记录请求时间成这种格式。 UNIX UNIX 时间戳格式,记录的是从 1970 年起始至今的总秒数。Squid 的默认日志格式中就使用 了这种格式。 UNIX_MS 这个时间戳则是从 1970 年起始至今的总毫秒数。据我所知,JavaScript 里经常使用这个时间 格式。 TAI64N TAI64N 格式比较少见,是这个样子的: @4000000052f88ea32489532c 。我目前只知道常见应用 中, qmail 会用这个格式。 ELKstack 中文指南 48filter配置 Joda-Time 库 Logstash 内部使用了 Java 的 Joda 时间库来作时间处理。所以我们可以使用 Joda 库所支持 的时间格式来作具体定义。Joda 时间格式定义见下表: 时间格式 Symbol Meaning Presentation Examples G era text AD C century of era (>=0) number 20 Y year of era (>=0) year 1996 x weekyear year 1996 w week of weekyear number 27 e day of week number 2 E day of week text Tuesday; Tue y year year 1996 D day of year number 189 M month of year month July; Jul; 07 d day of month number 10 a halfday of day text PM K hour of halfday (0~11) number 0 h clockhour of halfday (1~12) number 12 H hour of day (0~23) number 0 k clockhour of day (1~24) number 24 m minute of hour number 30 s second of minute number 55 S fraction of second number 978 z time zone text Pacific Standard Time; PST Z time zone offset/id zone -0800; -08:00; America/Los_Angeles ' escape for text delimiter '' single quote literal ' http://joda-time.sourceforge.net/apidocs/org/joda/time/format/DateTimeFormat.html ELKstack 中文指南 49filter配置 下面我们写一个 Joda 时间格式的配置作为示例: filter { grok { match => ["message", "%{HTTPDATE:logdate}"] } date { match => ["logdate", "dd/MMM/yyyy:HH:mm:ss Z"] } } 注意:时区偏移量只需要用一个字母 Z 即可。 时区问题的解释 很多中国用户经常提一个问题:为什么 @timestamp 比我们早了 8 个小时?怎么修改成北京 时间? 其实,Elasticsearch 内部,对时间类型字段,是统一采用 UTC 时间,存成 long 长整形数据 的!对日志统一采用 UTC 时间存储,是国际安全/运维界的一个通识——欧美公司的服务器普 遍广泛分布在多个时区里——不像中国,地域横跨五个时区却只用北京时间。 对于页面查看,ELK 的解决方案是在 Kibana 上,读取浏览器的当前时区,然后在页面上转换 时间内容的显示。 所以,建议大家接受这种设定。否则,即便你用 .getLocalTime 修改,也还要面临在 Kibana 上反过去修改,以及 Elasticsearch 原有的 ["now-1h" TO "now"] 这种方便的搜索语句无法正 常使用的尴尬。 以上,请读者自行斟酌。 ELKstack 中文指南 50filter配置 Grok 正则捕获 Grok 是 Logstash 最重要的插件。你可以在 grok 里预定义好命名正则表达式,在稍后(grok参 数或者其他正则表达式里)引用它。 正则表达式语法 运维工程师多多少少都会一点正则。你可以在 grok 里写标准的正则,像下面这样: \s+(?\d+(?:\.\d+)?)\s+ 小贴士:这个正则表达式写法对于 Perl 或者 Ruby 程序员应该很熟悉了,Python 程序员可能 更习惯写 (?Ppattern) ,没办法,适应一下吧。 现在给我们的配置文件添加第一个过滤器区段配置。配置要添加在输入和输出区段之间 (logstash 执行区段的时候并不依赖于次序,不过为了自己看得方便,还是按次序书写吧): input {stdin{}} filter { grok { match => { "message" => "\s+(?\d+(?:\.\d+)?)\s+" } } } output {stdout{}} 运行 logstash 进程然后输入 "begin 123.456 end",你会看到类似下面这样的输出: { "message" => "begin 123.456 end", "@version" => "1", "@timestamp" => "2014-08-09T11:55:38.186Z", "host" => "raochenlindeMacBook-Air.local", "request_time" => "123.456" } 漂亮!不过数据类型好像不太满意……request_time 应该是数值而不是字符串。 我们已经提过稍后会学习用 LogStash::Filters::Mutate 来转换字段值类型,不过在 grok 里,其实有自己的魔法来实现这个功能! ELKstack 中文指南 51filter配置 Grok 表达式语法 Grok 支持把预定义的 grok 表达式 写入到文件中,官方提供的预定义 grok 表达式 见:https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns。 注意:在新版本的logstash里面,pattern目录已经为空,最后一个commit提示core patterns将会由logstash-patterns-core gem来提供,该目录可供用户存放自定义patterns 下面是从官方文件中摘抄的最简单但是足够说明用法的示例: USERNAME [a-zA-Z0-9._-]+ USER %{USERNAME} 第一行,用普通的正则表达式来定义一个 grok 表达式;第二行,通过打印赋值格式,用前面 定义好的 grok 表达式来定义另一个 grok 表达式。 grok 表达式的打印复制格式的完整语法是下面这样的: %{PATTERN_NAME:capture_name:data_type} 小贴士:data_type 目前只支持两个值: int 和 float 。 所以我们可以改进我们的配置成下面这样: filter { grok { match => { "message" => "%{WORD} %{NUMBER:request_time:float} %{WORD}" } } } 重新运行进程然后可以得到如下结果: { "message" => "begin 123.456 end", "@version" => "1", "@timestamp" => "2014-08-09T12:23:36.634Z", "host" => "raochenlindeMacBook-Air.local", "request_time" => 123.456 } 这次 request_time 变成数值类型了。 ELKstack 中文指南 52filter配置 最佳实践 实际运用中,我们需要处理各种各样的日志文件,如果你都是在配置文件里各自写一行自己 的表达式,就完全不可管理了。所以,我们建议是把所有的 grok 表达式统一写入到一个地 方。然后用 filter/grok 的 patterns_dir 选项来指明。 如果你把 "message" 里所有的信息都 grok 到不同的字段了,数据实质上就相当于是重复存储 了两份。所以你可以用 remove_field 参数来删除掉 message 字段,或者用 overwrite 参 数来重写默认的 message 字段,只保留最重要的部分。 重写参数的示例如下: filter { grok { patterns_dir => "/path/to/your/own/patterns" match => { "message" => "%{SYSLOGBASE} %{DATA:message}" } overwrite => ["message"] } } 小贴士 多行匹配 在和 codec/multiline 搭配使用的时候,需要注意一个问题,grok 正则和普通正则一样,默认 是不支持匹配回车换行的。就像你需要 =~ //m 一样也需要单独指定,具体写法是在表达式 开始位置加 (?m) 标记。如下所示: match => { "message" => "(?m)\s+(?\d+(?:\.\d+)?)\s+" } 多项选择 有时候我们会碰上一个日志有多种可能格式的情况。这时候要写成单一正则就比较困难,或 者全用 | 隔开又比较丑陋。这时候,logstash 的语法提供给我们一个有趣的解决方式。 文档中,都说明 logstash/filters/grok 插件的 match 参数应该接受的是一个 Hash 值。但是因 为早期的 logstash 语法中 Hash 值也是用 [] 这种方式书写的,所以其实现在传递 Array 值 给 match 参数也完全没问题。所以,我们这里其实可以传递多个正则来匹配同一个字段: ELKstack 中文指南 53filter配置 match => [ "message", "(?\d+(?:\.\d+)?)", "message", "%{SYSLOGBASE} %{DATA:message}", "message", "(?m)%{WORD}" ] logstash 会按照这个定义次序依次尝试匹配,到匹配成功为止。虽说效果跟用 | 分割写个大 大的正则是一样的,但是可阅读性好了很多。 最后也是最关键的,我强烈建议每个人都要使用 Grok Debugger 来调试自己的 grok 表达 式。 ELKstack 中文指南 54filter配置 GeoIP 地址查询归类 GeoIP 是最常见的免费 IP 地址归类查询库,同时也有收费版可以采购。GeoIP 库可以根据 IP 地址提供对应的地域信息,包括国别,省市,经纬度等,对于可视化地图和区域统计非常有 用。 配置示例 filter { geoip { source => "message" } } 运行结果 { "message" => "183.60.92.253", "@version" => "1", "@timestamp" => "2014-08-07T10:32:55.610Z", "host" => "raochenlindeMacBook-Air.local", "geoip" => { "ip" => "183.60.92.253", "country_code2" => "CN", "country_code3" => "CHN", "country_name" => "China", "continent_code" => "AS", "region_name" => "30", "city_name" => "Guangzhou", "latitude" => 23.11670000000001, "longitude" => 113.25, "timezone" => "Asia/Chongqing", "real_region_name" => "Guangdong", "location" => [ [0] 113.25, [1] 23.11670000000001 ] } } ELKstack 中文指南 55filter配置 配置说明 GeoIP 库数据较多,如果你不需要这么多内容,可以通过 fields 选项指定自己所需要的。 下例为全部可选内容: filter { geoip { fields => ["city_name", "continent_code", "country_code2", "country_code3", "country_name", "dma_code", "ip", "latitude", "longitude", "postal_code", "region_name", "timezone"] } } 需要注意的是: geoip.location 是 logstash 通过 latitude 和 longitude 额外生成的数 据。所以,如果你是想要经纬度又不想重复数据的话,应该像下面这样做: filter { geoip { fields => ["city_name", "country_code2", "country_name", "latitude", "longitude", "region_name"] remove_field => ["[geoip][latitude]", "[geoip][longitude]"] } } ``` 小贴士 geoip 插件的 "source" 字段可以是任一处理后的字段,比如 "client_ip",但是字段内容却需要 小心!geoip 库内只存有公共网络上的 IP 信息,查询不到结果的,会直接返回 null,而 logstash 的 geoip 插件对 null 结果的处理是:不生成对应的 geoip.字段。 所以读者在测试时,如果使用了诸如 127.0.0.1, 172.16.0.1, 182.168.0.1, 10.0.0.1 等内网地 址,会发现没有对应输出! ELKstack 中文指南 56filter配置 JSON 编解码 在上一章,已经讲过在 codec 中使用 JSON 编码。但是,有些日志可能是一种复合的数据结 构,其中只是一部分记录是 JSON 格式的。这时候,我们依然需要在 filter 阶段,单独启用 JSON 解码插件。 配置示例 filter { json { source => "message" target => "jsoncontent" } } 运行结果 { "@version": "1", "@timestamp": "2014-11-18T08:11:33.000Z", "host": "web121.mweibo.tc.sinanode.com", "message": "{\"uid\":3081609001,\"type\":\"signal\"}", "jsoncontent": { "uid": 3081609001, "type": "signal" } } 小贴士 如果不打算使用多层结构的话,删掉 target 配置即可。新的结果如下: ELKstack 中文指南 57filter配置 { "@version": "1", "@timestamp": "2014-11-18T08:11:33.000Z", "host": "web121.mweibo.tc.sinanode.com", "message": "{\"uid\":3081609001,\"type\":\"signal\"}", "uid": 3081609001, "type": "signal" } ELKstack 中文指南 58filter配置 Key-Value 切分 在很多情况下,日志内容本身都是一个类似于 key-value 的格式,但是格式具体的样式却是多 种多样的。logstash 提供 filters/kv 插件,帮助处理不同样式的 key-value 日志,变成实际 的 LogStash::Event 数据。 配置示例 filter { ruby { init => "@kname = ['method','uri','verb']" code => "event.append(Hash[@kname.zip(event['request'].split(' '))])" } if [uri] { ruby { init => "@kname = ['url_path','url_args']" code => "event.append(Hash[@kname.zip(event['uri'].split('?'))])" } kv { prefix => "url_" source => "url_args" field_split => "&" remove_field => [ "url_args", "uri", "request" ] } } } 解释 Nginx 访问日志中的 $request ,通过这段配置,可以详细切分成 method , url_path , verb , url_a , url_b ... 进一步的,如果 url_args 中有过多字段,可能导致 Elasticsearch 集群因为频繁 update mapping 或者消耗太多内存在 cluster state 上而宕机。所以,更优的选择,是只保留明确有 用的 url_args 内容,其他部分舍去。 ELKstack 中文指南 59filter配置 kv { prefix => "url_" source => "url_args" field_split => "&" include_keys => [ "uid", "cip" ] remove_field => [ "url_args", "uri", "request" ] } 上例即表示,除了 url_uid 和 url_cip 两个字段以外,其他的 url_* 都不保留。 ELKstack 中文指南 60filter配置 数值统计(Metrics) filters/metrics 插件是使用 Ruby 的 Metriks 模块来实现在内存里实时的计数和采样分析。该模 块支持两个类型的数值分析:meter 和 timer。下面分别举例说明: Meter 示例(速率阈值检测) web 访问日志的异常状态码频率是运维人员会非常关心的一个数据。通常我们的做法,是通 过 logstash 或者其他日志分析脚本,把计数发送到 rrdtool 或者 graphite 里面。然后再通过 check_graphite 脚本之类的东西来检查异常并报警。 事实上这个事情可以直接在 logstash 内部就完成。比如如果最近一分钟 504 请求的个数超过 100 个就报警: filter { metrics { meter => "error.%{status}" add_tag => "metric" ignore_older_than => 10 } if "metric" in [tags] { ruby { code => "event.cancel if event['error.504.rate_1m'] * 60 < 100" } } } output { if "metric" in [tags] { exec { command => "echo \"Out of threshold: %{error.504.rate_1m}\"" } } } 这里需要注意 *60 的含义。 metriks 模块生成的 rate_1m/5m/15m 意思是:最近 1,5,15 分钟的每秒速率! Timer 示例(box and whisker 异常检测) ELKstack 中文指南 61filter配置 官版的 filters/metrics 插件只适用于 metric 事件的检查。由插件生成的新事件内部不存有来自 input 区段的实际数据信息。所以,要完成我们的百分比分布箱体检测,需要首先对代码稍微 做几行变动,即在 metric 的 timer 事件里加一个属性,存储最近一个实际事件的数 值:https://github.com/chenryn/logstash/commit/bc7bf34caf551d8a149605cf28e7c5d33fae7 458 然后我们就可以用如下配置来探测异常数据了: filter { metrics { timer => {"rt" => "%{request_time}"} percentiles => [25, 75] add_tag => "percentile" } if "percentile" in [tags] { ruby { code => "l=event['rt.p75']-event['rt.p25'];event['rt.low']=event['rt.p25']-l;event['rt.high']=event['rt.p75']+l" } } } output { if "percentile" in [tags] and ([rt.last] > [rt.high] or [rt.last] < [rt.low]) { exec { command => "echo \"Anomaly: %{rt.last}\"" } } } 小贴士:有关 box and shisker plot 内容和重要性,参见《数据之魅》一书。 ELKstack 中文指南 62filter配置 数据修改(Mutate) filters/mutate 插件是 Logstash 另一个重要插件。它提供了丰富的基础类型数据处理能力。包 括类型转换,字符串处理和字段处理等。 类型转换 类型转换是 filters/mutate 插件最初诞生时的唯一功能。其应用场景在之前 Codec/JSON 小节 已经提到。 可以设置的转换类型包括:"integer","float" 和 "string"。示例如下: filter { mutate { convert => ["request_time", "float"] } } 注意:mutate 除了转换简单的字符值,还支持对数组类型的字段进行转换,即将 ["1","2"] 转换成 [1,2] 。但不支持对哈希类型的字段做类似处理。有这方面需求的可以采用稍后讲述 的 filters/ruby 插件完成。 字符串处理 gsub 仅对字符串类型字段有效 gsub => ["urlparams", "[\\?#]", "_"] split filter { mutate { split => ["message", "|"] } } 随意输入一串以 | 分割的字符,比如 "123|321|adfd|dfjld*=123",可以看到如下输出: ELKstack 中文指南 63filter配置 { "message" => [ [0] "123", [1] "321", [2] "adfd", [3] "dfjld*=123" ], "@version" => "1", "@timestamp" => "2014-08-20T15:58:23.120Z", "host" => "raochenlindeMacBook-Air.local" } join 仅对数组类型字段有效 我们在之前已经用 split 割切的基础再 join 回去。配置改成: filter { mutate { split => ["message", "|"] } mutate { join => ["message", ","] } } filter 区段之内,是顺序执行的。所以我们最后看到的输出结果是: { "message" => "123,321,adfd,dfjld*=123", "@version" => "1", "@timestamp" => "2014-08-20T16:01:33.972Z", "host" => "raochenlindeMacBook-Air.local" } merge 合并两个数组或者哈希字段。依然在之前 split 的基础上继续: ELKstack 中文指南 64filter配置 filter { mutate { split => ["message", "|"] } mutate { merge => ["message", "message"] } } 我们会看到输出: { "message" => [ [0] "123", [1] "321", [2] "adfd", [3] "dfjld*=123", [4] "123", [5] "321", [6] "adfd", [7] "dfjld*=123" ], "@version" => "1", "@timestamp" => "2014-08-20T16:05:53.711Z", "host" => "raochenlindeMacBook-Air.local" } 如果 src 字段是字符串,会自动先转换成一个单元素的数组再合并。把上一示例中的来源字段 改成 "host": filter { mutate { split => ["message", "|"] } mutate { merge => ["message", "host"] } } 结果变成: ELKstack 中文指南 65filter配置 { "message" => [ [0] "123", [1] "321", [2] "adfd", [3] "dfjld*=123", [4] "raochenlindeMacBook-Air.local" ], "@version" => "1", "@timestamp" => "2014-08-20T16:07:53.533Z", "host" => [ [0] "raochenlindeMacBook-Air.local" ] } 看,目的字段 "message" 确实多了一个元素,但是来源字段 "host" 本身也由字符串类型变成 数组类型了! 下面你猜,如果来源位置写的不是字段名而是直接一个字符串,会产生什么奇特的效果呢? strip lowercase uppercase 字段处理 rename 重命名某个字段,如果目的字段已经存在,会被覆盖掉: filter { mutate { rename => ["syslog_host", "host"] } } update 更新某个字段的内容。如果字段不存在,不会新建。 replace 作用和 update 类似,但是当字段不存在的时候,它会起到 add_field 参数一样的效果,自 动添加新的字段。 ELKstack 中文指南 66filter配置 执行次序 需要注意的是,filter/mutate 内部是有执行次序的。其次序如下: rename(event) if @rename update(event) if @update replace(event) if @replace convert(event) if @convert gsub(event) if @gsub uppercase(event) if @uppercase lowercase(event) if @lowercase strip(event) if @strip remove(event) if @remove split(event) if @split join(event) if @join merge(event) if @merge filter_matched(event) 而 filter_matched 这个 filters/base.rb 里继承的方法也是有次序的。 @add_field.each do |field, value| end @remove_field.each do |field| end @add_tag.each do |tag| end @remove_tag.each do |tag| end ELKstack 中文指南 67filter配置 随心所欲的 Ruby 处理 如果你稍微懂那么一点点 Ruby 语法的话,filters/ruby 插件将会是一个非常有用的工具。 比如你需要稍微修改一下 LogStash::Event 对象,但是又不打算为此写一个完整的插件,用 filters/ruby 插件绝对感觉良好。 配置示例 filter { ruby { init => "@kname = ['client','servername','url','status','time','size','upstream','upstreamstatus','upstreamtime','referer','xff','useragent']" code => "event.append(Hash[@kname.zip(event['message'].split('|'))])" } } 官网示例是一个比较有趣但是没啥大用的做法 —— 随机取消 90% 的事件。 所以上面我们给出了一个有用而且强大的实例。 解释 通常我们都是用 filters/grok 插件来捕获字段的,但是正则耗费大量的 CPU 资源,很容易成为 Logstash 进程的瓶颈。 而实际上,很多流经 Logstash 的数据都是有自己预定义的特殊分隔符的,我们可以很简单的 直接切割成多个字段。 filters/mutate 插件里的 "split" 选项只能切成数组,后续很不方便使用和识别。而在 filters/ruby 里,我们可以通过 "init" 参数预定义好由每个新字段的名字组成的数组,然后在 "code" 参数 指定的 Ruby 语句里通过两个数组的 zip 操作生成一个哈希并添加进数组里。短短一行 Ruby 代码,可以减少 50% 以上的 CPU 使用率。 filters/ruby 插件用途远不止这一点,下一节你还会继续见到它的身影。 更多实例 2014 年 09 年 23 日新增 ELKstack 中文指南 68filter配置 filter{ date { match => ["datetime" , "UNIX"] } ruby { code => "event.cancel if 5 * 24 * 3600 < (event['@timestamp']-::Time.now).abs" } } 在实际运用中,我们几乎肯定会碰到出乎意料的输入数据。这都有可能导致 Elasticsearch 集 群出现问题。 当数据格式发生变化,比如 UNIX 时间格式变成 UNIX_MS 时间格式,会导致 logstash 疯狂 创建新索引,集群崩溃。 或者误输入过老的数据时,因为一般我们会 close 几天之前的索引以节省内存,必要时再打 开。而直接尝试把数据写入被关闭的索引会导致内存问题。 这时候我们就需要提前校验数据的合法性。上面配置,就是用于过滤掉时间范围与当前时间 差距太大的非法数据的。 ELKstack 中文指南 69filter配置 split 拆分事件 上一章我们通过 multiline 插件将多行数据合并进一个事件里,那么反过来,也可以把一行数 据,拆分成多个事件。这就是 split 插件。 配置示例 filter { split { field => "message" terminator => "#" } } 运行结果 这个测试中,我们在 intputs/stdin 的终端中输入一行数据:"test1#test2",结果看到输出两个 事件: { "@version": "1", "@timestamp": "2014-11-18T08:11:33.000Z", "host": "web121.mweibo.tc.sinanode.com", "message": "test1" } { "@version": "1", "@timestamp": "2014-11-18T08:11:33.000Z", "host": "web121.mweibo.tc.sinanode.com", "message": "test2" } 重要提示 split 插件中使用的是 yield 功能,其结果是 split 出来的新事件,会直接结束其在 filter 阶段的 历程,也就是说写在 split 后面的其他 filter 插件都不起作用,进入到 output 阶段。所以,一 定要保证 split 配置写在全部 filter 配置的最后。 使用了类似功能的还有 clone 插件。 ELKstack 中文指南 70filter配置 注:从 logstash-1.5.0beta1 版本以后修复该问题。 ELKstack 中文指南 71filter配置 elapsed filter { grok { match => ["message", "%{TIMESTAMP_ISO8601} START id: (?.*)"] add_tag => [ "taskStarted" ] } grok { match => ["message", "%{TIMESTAMP_ISO8601} END id: (?.*)"] add_tag => [ "taskTerminated"] } elapsed { start_tag => "taskStarted" end_tag => "taskTerminated" unique_id_field => "task_id" } } ELKstack 中文指南 72filter配置 保存进 Elasticsearch Logstash 可以试用不同的协议实现完成将数据写入 Elasticsearch 的工作。在不同时期,也有 不同的插件实现方式。本节以最新版为准,即主要介绍 HTTP 方式。同时也附带一些原有的 node 和 transport 方式的介绍。 配置示例 output { elasticsearch { hosts => ["192.168.0.2:9200"] index => "logstash-%{type}-%{+YYYY.MM.dd}" document_type => "%{type}" workers => 1 flush_size => 20000 idle_flush_time => 10 template_overwrite => true } } 解释 批量发送 flush_size 和 idle_flush_time 共同控制 Logstash 向 Elasticsearch 发送批量数据的行 为。以上面示例来说:Logstash 会努力攒到 20000 条数据一次性发送出去,但是如果 10 秒 钟内也没攒够 20000 条,Logstash 还是会以当前攒到的数据量发一次。 默认情况下, flush_size 是 500 条, idle_flush_time 是 1 秒。这也是很多人改大了 flush_size 也没能提高写入 ES 性能的原因——Logstash 还是 1 秒钟发送一次。 索引名 写入的 ES 索引的名称,这里可以使用变量。为了更贴合日志场景,Logstash 提供了 % {+YYYY.MM.dd} 这种写法。在语法解析的时候,看到以 + 号开头的,就会自动认为后面是时间 格式,尝试用时间格式来解析后续字符串。所以,之前处理过程中不要给自定义字段取个加 号开头的名字…… ELKstack 中文指南 73filter配置 此外,注意索引名中不能有大写字母,否则 ES 在日志中会报 InvalidIndexNameException, 但是 Logstash 不会报错,这个错误比较隐晦,也容易掉进这个坑中。 Java 协议 1.4.0 版本之前,有 logstash-output-elasticsearch , logstash-output-elasticsearch_http , logstash-output-elasticsearch_river 三个插件。 1.4.0 到 2.0 版本之间,配合 Elasticsearch 废弃 river 方法,只剩下 logstash-output- elasticsearch 一个插件,同时实现了 node、transport、http 三种协议。 2.0 版本开始,为了兼容性和调试方便, logstash-output-elasticsearch 改为只支持 http 协 议。想继续使用 node 或者 transport 协议的用户,需要单独安装 logstash-output- elasticsearch_java 插件。 一个小集群里,使用 node 协议最方便了。Logstash 以 elasticsearch 的 client 节点身份(即不 存数据不参加选举)运行。如果你运行下面这行命令,你就可以看到自己的 logstash 进程名, 对应的 node.role 值是 c: # curl 127.0.0.1:9200/_cat/nodes?v host ip heap.percent ram.percent load node.role master name local 192.168.0.102 7 c - logstash-local-1036-2012 local 192.168.0.2 7 d * Sunstreak Logstash-1.5 以后,也不再分发一个内嵌的 elasticsearch 服务器。如果你想变更 node 协议 下的这些配置,在 $PWD/elasticsearch.yml 文件里写自定义配置即可,logstash 会尝试自动 加载这个文件。 对于拥有很多索引的大集群,你可以用 transport 协议。logstash 进程会转发所有数据到你指 定的某台主机上。这种协议跟上面的 node 协议是不同的。node 协议下的进程是可以接收到 整个 Elasticsearch 集群状态信息的,当进程收到一个事件时,它就知道这个事件应该存在集 群内哪个机器的分片里,所以它就会直接连接该机器发送这条数据。而 transport 协议下的进 程不会保存这个信息,在集群状态更新(节点变化,索引变化都会发送全量更新)时,就不会对 所有的 logstash 进程也发送这种信息。更多 Elasticsearch 集群状态的细节,参 阅http://www.elasticsearch.org/guide。 小贴士 Logstash 1.4.2 在 transport 和 http 协议的情况下是固定连接指定 host 发送数据。从 1.5.0 开始,host 可以设置数组,它会从节点列表中选取不同的节点发送数据,达到 Round-Robin 负载均衡的效果。 Kibana4 强制要求 ES 全集群所有 node 版本在 1.4 以上,Kibana4.2 要求 ES 2.0 以上。 所以采用 node 方式发送数据的 logstash-1.4(携带的 Elasticsearch.jar 库是 1.1.1 版本) ELKstack 中文指南 74filter配置 会导致 Kibana4 无法运行,采用 Kibana4 的读者务必改用 http 方式。 开发者在 IRC freenode#logstash 频道里表示:"高于 1.0 版本的 Elasticsearch 应该都能 跟最新版 logstash 的 node 协议一起正常工作"。此信息仅供参考,请认真测试后再上 线。 经常有同学问,为什么 Logstash 在有多个 conf 文件的情况下,进入 ES 的数据会重 复,几个 conf 数据就会重复几次。其实问题原因在之前启动参数章节有提过,output 段 顺序执行,没有对日志 type 进行判断的各插件配置都会全部执行一次。在 output 段对 type 进行判断的语法如下所示: output { if [type] == "nginxaccess" { elasticsearch { } } } 老版本的性能问题 Logstash 1.4.2 在 http 协议下默认使用作者自己的 ftw 库,随同分发的是 0.0.39 版。该版本 有内存泄露问题,长期运行下输出性能越来越差! 解决办法: 1. 对性能要求不高的,可以在启动 logstash 进程时,配置环境变量 ENV["BULK"] ,强制采 用 elasticsearch 官方 Ruby 库。命令如下: export BULK="esruby" 2. 对性能要求高的,可以尝试采用 logstash-1.5.0RC2 。新版的 outputs/elasticsearch 放弃 了 ftw 库,改用了一个 JRuby 平台专有的 Manticore 库。根据测试,性能跟 ftw 比相当 接近。 3. 对性能要求极高的,可以手动更新 ftw 库版本,目前最新版是 0.0.42 版,据称内存问题 在 0.0.40 版即解决。 模板 Elasticsearch 支持给索引预定义设置和 mapping(前提是你用的 elasticsearch 版本支持这个 API,不过估计应该都支持)。Logstash 自带有一个优化好的模板,内容如下: { "template" : "logstash-*", "settings" : { "index.refresh_interval" : "5s" }, "mappings" : { ELKstack 中文指南 75filter配置 "_default_" : { "_all" : {"enabled" : true, "omit_norms" : true}, "dynamic_templates" : [ { "message_field" : { "match" : "message", "match_mapping_type" : "string", "mapping" : { "type" : "string", "index" : "analyzed", "omit_norms" : true, "fielddata" : { "format" : "disabled" } } } }, { "string_fields" : { "match" : "*", "match_mapping_type" : "string", "mapping" : { "type" : "string", "index" : "analyzed", "omit_norms" : true, "fielddata" : { "format" : "disabled" }, "fields" : { "raw" : {"type": "string", "index" : "not_analyzed", "doc_values" : true, "ignore_above" } } } }, { "float_fields" : { "match" : "*", "match_mapping_type" : "float", "mapping" : { "type" : "float", "doc_values" : true } } }, { "double_fields" : { "match" : "*", "match_mapping_type" : "double", "mapping" : { "type" : "double", "doc_values" : true } } }, { "byte_fields" : { "match" : "*", "match_mapping_type" : "byte", "mapping" : { "type" : "byte", "doc_values" : true } } }, { "short_fields" : { "match" : "*", "match_mapping_type" : "short", "mapping" : { "type" : "short", "doc_values" : true } } }, { "integer_fields" : { "match" : "*", "match_mapping_type" : "integer", "mapping" : { "type" : "integer", "doc_values" : true } } ELKstack 中文指南 76filter配置 }, { "long_fields" : { "match" : "*", "match_mapping_type" : "long", "mapping" : { "type" : "long", "doc_values" : true } } }, { "date_fields" : { "match" : "*", "match_mapping_type" : "date", "mapping" : { "type" : "date", "doc_values" : true } } }, { "geo_point_fields" : { "match" : "*", "match_mapping_type" : "geo_point", "mapping" : { "type" : "geo_point", "doc_values" : true } } } ], "properties" : { "@timestamp": { "type": "date", "doc_values" : true }, "@version": { "type": "string", "index": "not_analyzed", "doc_values" : true }, "geoip" : { "type" : "object", "dynamic": true, "properties" : { "ip": { "type": "ip", "doc_values" : true }, "location" : { "type" : "geo_point", "doc_values" : true }, "latitude" : { "type" : "float", "doc_values" : true }, "longitude" : { "type" : "float", "doc_values" : true } } } } } } } 这其中的关键设置包括: template for index-pattern 只有匹配 logstash-* 的索引才会应用这个模板。有时候我们会变更 Logstash 的默认索引名 称,记住你也得通过 PUT 方法上传可以匹配你自定义索引名的模板。当然,我更建议的做法 是,把你自定义的名字放在 "logstash-" 后面,变成 index => "logstash-custom-% {+yyyy.MM.dd}" 这样。 refresh_interval for indexing ELKstack 中文指南 77filter配置 Elasticsearch 是一个近实时搜索引擎。它实际上是每 1 秒钟刷新一次数据。对于日志分析应 用,我们用不着这么实时,所以 logstash 自带的模板修改成了 5 秒钟。你还可以根据需要继 续放大这个刷新间隔以提高数据写入性能。 multi-field with not_analyzed Elasticsearch 会自动使用自己的默认分词器(空格,点,斜线等分割)来分析字段。分词器对于 搜索和评分是非常重要的,但是大大降低了索引写入和聚合请求的性能。所以 logstash 模板 定义了一种叫"多字段"(multi-field)类型的字段。这种类型会自动添加一个 ".raw" 结尾的字段, 并给这个字段设置为不启用分词器。简单说,你想获取 url 字段的聚合结果的时候,不要直接 用 "url" ,而是用 "url.raw" 作为字段名。 geo_point Elasticsearch 支持 geo_point 类型, geo distance 聚合等等。比如说,你可以请求某个 geo_point 点方圆 10 千米内数据点的总数。在 Kibana 的 tilemap 类型面板里,就会用到这个 类型的数据。 doc_values doc_values 是 Elasticsearch 1.0 版本引入的新特性。启用该特性的字段,索引写入的时候会 在磁盘上构建 fielddata。而过去,fielddata 是固定只能使用内存的。在请求范围加大的时 候,很容易触发 OOM 或者 circuit breaker 报错: ElasticsearchException[org.elasticsearch.common.breaker.CircuitBreakingException: Data too large, data for field [@timestamp] would be larger than limit of [639015321/609.4mb]] doc_values 只能给不分词(对于字符串字段就是设置了 "index":"not_analyzed" ,数值和时间 字段默认就没有分词) 的字段配置生效。 doc_values 虽然用的是磁盘,但是系统本身也有自带 VFS 的 cache 效果并不会太差。据官 方测试,经过 1.4 的优化后,只比使用内存的 fielddata 慢 15% 。所以,在数据量较大的情况 下,强烈建议开启该配置。 Elasticsearch 2.0 以后, doc_values 变成默认设置。这部分可以不再单独指定了。 fielddata 和 doc_values 对应的,则是 fielddata。在 Elasticsearch 2.x 全面启用 doc_values 后, Logstash 的默认 template 更干脆的加上了对 fielddata 的 {"format":"disabled"} 。当你还对 分词字段发起聚合和排序请求的时候,直接提示无法构建 fielddata 了! 其他模板配置建议 order ELKstack 中文指南 78filter配置 如果你有自己单独定制 template 的想法,很好。这时候有几种选择: 1. 在 logstash/outputs/elasticsearch 配置中开启 manage_template => false 选项,然后一 切自己动手; 2. 在 logstash/outputs/elasticsearch 配置中开启 template => "/path/to/your/tmpl.json" 选项,让 logstash 来发送你自己写的 template 文件; 3. 避免变更 logstash 里的配置,而是另外发送一个 template ,利用 elasticsearch 的 templates order 功能。 这个 order 功能,就是 elasticsearch 在创建一个索引的时候,如果发现这个索引同时匹配上 了多个 template ,那么就会先应用 order 数值小的 template 设置,然后再应用一遍 order 数 值高的作为覆盖,最终达到一个 merge 的效果。 比如,对上面这个模板已经很满意,只想修改一下 refresh_interval ,那么只需要新写一 个: { "order" : 1, "template" : "logstash-*", "settings" : { "index.refresh_interval" : "20s" } } 然后运行 curl -XPUT http://localhost:9200/_template/template_newid -d '@/path/to/your/tmpl.json' 即可。 logstash 默认的模板, order 是 0,id 是 logstash,通过 logstash/outputs/elasticsearch 的配 置选项 template_name 修改。你的新模板就不要跟这个名字冲突了。 ELKstack 中文指南 79filter配置 发送邮件(Email) 配置示例 output { email { to => "admin@website.com,root@website.com" cc => "other@website.com" via => "smtp" subject => "Warning: %{title}" options => { smtpIporHost => "localhost", port => 25, domain => 'localhost.localdomain', userName => nil, password => nil, authenticationType => nil, # (plain, login and cram_md5) starttls => true } htmlbody => "" body => "" attachments => ["/path/to/filename"] } } 解释 outputs/email 插件支持 SMTP 协议和 sendmail 两种方式,通过 via 参数设置。SMTP 方式 有较多的 options 参数可配置。sendmail 只能利用本机上的 sendmail 服务来完成 —— 文档 上描述了 Mail 库支持的 sendmail 配置参数,但实际代码中没有相关处理,不要被迷惑 了。。。 ELKstack 中文指南 80filter配置 调用命令执行(Exec) outputs/exec 插件的运用也非常简单,如下所示,将 logstash 切割成的内容作为参数传递给 命令。这样,在每个事件到达该插件的时候,都会触发这个命令的执行。 output { exec { command => "sendsms.pl \"%{message}\" -t %{user}" } } 需要注意的是。这种方式是每次都重新开始执行一次命令并退出。本身是比较慢速的处理方 式(程序加载,网络建联等都有一定的时间消耗)。最好只用于少量的信息处理场景,比如不适 用 nagios 的其他报警方式。示例就是通过短信发送消息。 ELKstack 中文指南 81filter配置 保存成文件(File) 通过日志收集系统将分散在数百台服务器上的数据集中存储在某中心服务器上,这是运维最 原始的需求。早年的 scribed ,甚至直接就把输出的语法命名为 。Logstash 当然也 能做到这点。 和 LogStash::Inputs::File 不同, LogStash::Outputs::File 里可以使用 sprintf format 格式来 自动定义输出到带日期命名的路径。 配置示例 output { file { path => "/path/to/%{+yyyy}/%{+MM}/%{+dd}/%{host}.log.gz" message_format => "%{message}" gzip => true } } 解释 使用 output/file 插件首先需要注意的就是 message_format 参数。插件默认是输出整个 event 的 JSON 形式数据的。这可能跟大多数情况下使用者的期望不符。大家可能只是希望按照日 志的原始格式保存就好了。所以需要定义为 %{message} ,当然,前提是在之前的 filter 插件 中,你没有使用 remove_field 或者 update 等参数删除或修改 %{message} 字段的内容。 另一个非常有用的参数是 gzip。gzip 格式是一个非常奇特而友好的格式。其格式包括有: 10字节的头,包含幻数、版本号以及时间戳 可选的扩展头,如原文件名 文件体,包括DEFLATE压缩的数据 8字节的尾注,包括CRC-32校验和以及未压缩的原始数据长度 这样 gzip 就可以一段一段的识别出来数据 —— 反过来说,也就是可以一段一段压缩了添加 在后面! 这对于我们流式添加数据简直太棒了! ELKstack 中文指南 82filter配置 小贴士:你或许见过网络流传的 parallel 命令行工具并发处理数据的神奇文档,但在自己用的 时候总见不到效果。实际上就是因为:文档中处理的 gzip 文件,可以分开处理然后再合并 的。 注意 1. 按照 Logstash 标准,其实应该可以把数据格式的定义改在 codec 插件中完成,但是 logstash-output-file 插件内部实现中跳过了 @codec.decode 这步,所以 codec 设置无法 生效! 2. 按照 Logstash 标准,配置参数的值可以使用 event sprintf 格式。但是 logstash-output- file 插件对 event.sprintf(@path) 的结果,还附加了一步 inside_file_root? 校验(个人 猜测是为了防止越权到其他路径),这个 file_root 是通过直接对 path 参数分割 / 符 号得到的。如果在 sprintf 格式中带有 / 符号,那么被切分后的结果就无法正确解析 了。 所以,如下所示配置,虽然看起来是正确的,实际效果却不对,正确写法应该是本节之前的 配置示例那样。 output { file { path => "/path/to/%{+yyyy/MM/dd}/%{host}.log.gz" codec => line { format => "%{message}" } } } ELKstack 中文指南 83filter配置 报警到 Nagios Logstash 中有两个 output 插件是 nagios 有关的。outputs/nagios 插件发送数据给本机的 nagios.cmd 管道命令文件,outputs/nagios_nsca 插件则是 调用 send_nsca 命令以 NSCA 协议格式把数据发送给 nagios 服务器(远端或者本地皆可)。 Nagios.Cmd nagios.cmd 是 nagios 服务器的核心组件。nagios 事件处理和内外交互都是通过这个管道文 件来完成的。 使用 CMD 方式,需要自己保证发送的 Logstash 事件符合 nagios 事件的格式。即必须在 filter 阶段预先准备好 nagios_host 和 nagios_service 字段;此外,如果在 filter 阶段也准备 好 nagios_annotation 和 nagios_level 字段,这里也会自动转换成 nagios 事件信息。 filter { if [message] =~ /err/ { mutate { add_tag => "nagios" rename => ["host", "nagios_host"] replace => ["nagios_service", "logstash_check_%{type}"] } } } output { if "nagios" in [tags] { nagios { } } } 如果不打算在 filter 阶段提供 nagios_level ,那么也可以在该插件中通过参数配置。 所谓 nagios_level ,即我们通过 nagios plugin 检查数据时的返回值。其取值范围和含义如 下: "0",代表 "OK",服务正常; "1",代表 "WARNNING",服务警告,一般 nagios plugin 命令中使用 -w 参数设置该阈 值; "2",代表 "CRITICAL",服务危急,一般 nagios plugin 命令中使用 -c 参数设置该阈 值; "3",代表 "UNKNOWN",未知状态,一般会在 timeout 等情况下出现。 ELKstack 中文指南 84filter配置 默认情况下,该插件会以 "CRITICAL" 等级发送报警给 Nagios 服务器。 nagios.cmd 文件的具体位置,可以使用 command_file 参数设置。默认位置是 "/var/lib/nagios3/rw/nagios.cmd"。 关于和 nagios.cmd 交互的具体协议说明,有兴趣的读者请阅读 Using external commands in Nagios 一文,这是《Learning Nagios 3.0》书中内容节选。 NSCA NSCA 是一种标准的 nagios 分布式扩展协议。分布在各机器上的 send_nsca 进程主动将监 控数据推送给远端 nagios 服务器的 NSCA 进程。 当 Logstash 跟 nagios 服务器没有在同一个主机上运行的时候,就只能通过 NSCA 方式来发 送报警了 —— 当然也必须在 Logstash 服务器上安装 send_nsca 命令。 nagios 事件所需要的几个属性在上一段中已经有过描述。不过在使用这个插件的时候,不要 求提前准备好,而是可以在该插件内部定义参数: output { nagios_nsca { nagios_host => "%{host}" nagios_service => "logstash_check_%{type}" nagios_status => "2" message_format => "%{@timestamp}: %{message}" host => "nagiosserver.domain.com" } } 这里请注意, host 和 nagios_host 两个参数,分别是用来设置 nagios 服务器的地址,和报 警信息中有问题的服务器地址。 关于 NSCA 原理,架构和配置说明,还不了解的读者请阅读官方网站 Using NSClient++ from nagios with NSCA 一节。 推荐阅读 除了 nagios 以外,logstash 同样可以发送信息给其他常见监控系统。方式和 nagios 大同小 异: outputs/ganglia 插件通过 UDP 协议,发送 gmetric 型数据给本机/远端的 gmond 或者 gmetad outputs/zabbix 插件调用本机的 zabbix_sender 命令发送 ELKstack 中文指南 85filter配置 输出到 Statsd 基础知识 Statsd 最早是 2008 年 Flickr 公司用 Perl 写的针对 Graphite、datadog 等监控数据后端存储 开发的前端网络应用,2011 年 Etsy 公司用 node.js 重构。用于接收(默认 UDP)、写入、读取 和聚合时间序列数据,包括即时值和累积值等。 Graphite 是用 Python 模仿 RRDtools 写的时间序列数据库套件。包括三个部分: carbon: 是一个Twisted守护进程,监听处理数据; whisper: 存储时间序列的数据库; webapp: 一个用 Django 框架实现的网页应用。 Graphite 安装简介 通过如下几步安装 Graphite: 1. 安装 cairo 和 pycairo 库 # yum -y install cairo pycairo 1. pip 安装 # yum install python-devel python-pip # pip install django django-tagging carbon whisper graphite-web uwsgi 1. 配置 Graphite # cd /opt/graphite/webapp/graphite # cp local_settings.py.example local_settings.py # python manage.py syncdb 修改 local_settings.py 中的 DATABASE 为设置的 db 信息。 1. 启动 cabon ELKstack 中文指南 86filter配置 # cd /opt/graphite/conf/ # cp carbon.conf.example carbon.conf # cp storage-schemas.conf.example storage-schemas.conf # cd /opt/graphite/ # ./bin/carbon-cache.py start statsd 安装简介 1. Graphite 地址设置 # cd /opt/ # git clone git://github.com/etsy/statsd.git # cd /opt/statsd # cp exampleConfig.js Config.js 根据 Graphite 服务器地址,修改Config.js 中的配置如下: { graphitePort: 2003, graphiteHost: "10.10.10.124", port: 8125, backends: [ "./backends/graphite" ] } 1. uwsgi 配置 cd /opt/graphite/webapp/graphite cat > wsgi_graphite.xml < 0.0.0.0:8630 2 2 100 /opt/graphite/webapp/graphite .. wsgi graphite.pid true true true /var/log/uwsgi_graphite.log EOF cp /opt/graphite/conf/graphite.wsgi /opt/graphite/webapp/graphite/wsgi.py 1. nginx 的 uwsgi 配置 ELKstack 中文指南 87filter配置 cat > /usr/local/nginx/conf/conf.d/graphite.conf <stats->test->logstash->num的表,statsd 里面多了numStats等数据。 配置示例 output { statsd { host => "statsdserver.domain.com" namespace => "logstash" sender => "%{host}" increment => ["httpd.response.%{status}"] } } 解释 ELKstack 中文指南 88filter配置 Graphite 以树状结构存储监控数据,所以 statsd 也是如此。所以发送给 statsd 的数据的 key 也一定得是 "first.second.tree.four" 这样的形式。而在 logstash-output-statsd 插件中,就会以 三个配置参数来拼接成这种形式: namespace.sender.metric 其中 namespace 和 sender 都是直接设置的,而 metric 又分为好几个不同的参数可以分别设 置。statsd 支持的 metric 类型如下: metric 类型 increment 增量,一个计量周期内,某个数字接收了多少次,比如 nginx 的 status 状态 码。 示例语法: increment => ["nginx.status.%{status}"] decrement 语法同 increment。 count 对数字的计数,比如,每秒接收一个数字,一个计量周期内,所有数字的和,比如 nginx 的 body_bytes_sent。 示例语法: count => {"nginx.bytes" => "%{bytes}"} gauge 语法同 count。 set 语法同 count。 timing 时间范围内,某种数字的最大值,最小值,平均值,比如 nginx 的响应时间 request_time。 语法同 count。 关于这些 metric 类型的详细说明,请阅读 statsd 文 档:https://github.com/etsy/statsd/blob/master/docs/metric_types.md。 推荐阅读 Etsy 发布 nodejs 版本 statsd 的博客:Measure Anything, Measure Everything Flickr 发布 statsd 的博客:Counting & Timing ELKstack 中文指南 89filter配置 标准输出(Stdout) 和之前 inputs/stdin 插件一样,outputs/stdout 插件也是最基础和简单的输出插件。同样在这 里简单介绍一下,作为输出插件的一个共性了解。 配置示例 output { stdout { codec => rubydebug workers => 2 } } 解释 输出插件统一具有一个参数是 workers 。Logstash 为输出做了多线程的准备。 其次是 codec 设置。codec 的作用在之前已经讲过。可能除了 codecs/multiline ,其他 codec 插件本身并没有太多的设置项。所以一般省略掉后面的配置区段。换句话说。上面配 置示例的完全写法应该是: output { stdout { codec => rubydebug { } workers => 2 } } 单就 outputs/stdout 插件来说,其最重要和常见的用途就是调试。所以在不太有效的时候,加 上命令行参数 -vv 运行,查看更多详细调试信息。 ELKstack 中文指南 90filter配置 发送网络数据(TCP) 虽然之前我们已经提到过不建议直接使用 LogStash::Inputs::TCP 和 LogStash::Outputs::TCP 做转发工作,不过在实际交流中,发现确实有不少朋友觉得这种简单配置足够使用,因而不 愿意多加一层消息队列的。所以,还是把 Logstash 如何直接发送 TCP 数据也稍微提点一 下。 配置示例 output { tcp { host => "192.168.0.2" port => 8888 codec => json_lines } } 配置说明 在收集端采用 tcp 方式发送给远端的 tcp 端口。这里需要注意的是,默认的 codec 选项是 json。而远端的 LogStash::Inputs::TCP 的默认 codec 选项却是 line !所以不指定各自的 codec ,对接肯定是失败的。 另外,由于IO BUFFER 的原因,即使是两端共同约定为 json 依然无法正常运行,接收端会 认为一行数据没结束,一直等待直至自己 OutOfMemory ! 所以,正确的做法是,发送端指定 codec 为 json_lines ,这样每条数据后面会加上一个回 车,接收端指定 codec 为 json_lines 或者 json 均可,这样才能正常处理。包括在收集端已 经切割好的字段,也可以直接带入收集端使用了。 ELKstack 中文指南 91filter配置 HDFS https://github.com/dstore-dbap/logstash-webhdfs This plugin based on WebHDFS api of Hadoop, it just POST data to WebHDFS port. So, it's a native Ruby code. output { hadoop_webhdfs { workers => 2 server => "your.nameno.de:14000" user => "flume" path => "/user/flume/logstash/dt=%{+Y}-%{+M}-%{+d}/logstash-%{+H}.log" flush_size => 500 compress => "snappy" idle_flush_time => 10 retry_interval => 0.5 } } https://github.com/avishai-ish-shalom/logstash-hdfs This plugin based on HDFS api of Hadoop, it import java classes like org.apache.hadoop.fs.FileSystem etc. Configuration output { hdfs { path => "/path/to/output_file.log" enable_append => true } } Howto run CLASSPATH=$(find /path/to/hadoop -name '*.jar' | tr '\n' ':'):/etc/hadoop/conf:/path/to/logstash-1.1.7-monolithic.jar java logstash.runner agent -f conf/hdfs-output.conf -p /path/to/cloned/logstash-hdfs ELKstack 中文指南 92filter配置 场景示例 前面虽然介绍了几十个常用的 Logstash 插件的常见配置项。但是过多的选择下,如何组合使 用这些插件,依然是一部分用户的幸福难题。本节,列举一些最常见的日志场景,演示一下 针对性的组件搭配。希望能给读者带来一点启发。 ELKstack 中文指南 93场景示例 Nginx 访问日志 grok Logstash 默认自带了 apache 标准日志的 grok 正则: COMMONAPACHELOG %{IPORHOST:clientip} %{USER:ident} %{NOTSPACE:auth} \[%{HTTPDATE:timestamp}\] "(?:%{WORD:verb} %{NOTSPACE:request}(?: HTTP/%{NUMBER:httpversion})?|%{DATA:rawrequest})" %{NUMBER:response} (?:%{NUMBER:bytes}|-) COMBINEDAPACHELOG %{COMMONAPACHELOG} %{QS:referrer} %{QS:agent} 对于 nginx 标准日志格式,可以发现只是最后多了一个 $http_x_forwarded_for 变量。所以 nginx 标准日志的 grok 正则定义是: MAINNGINXLOG %{COMBINEDAPACHELOG} %{QS:x_forwarded_for} 自定义的日志格式,可以照此修改。 split nginx 日志因为部分变量中内含空格,所以很多时候只能使用 %{QS} 正则来做分割,性能和 细度都不太好。如果能自定义一个比较少见的字符作为分隔符,那么处理起来就简单多了。 假设定义的日志格式如下: log_format main "$http_x_forwarded_for | $time_local | $request | $status | $body_bytes_sent | " "$request_body | $content_length | $http_referer | $http_user_agent | $nuid | " "$http_cookie | $remote_addr | $hostname | $upstream_addr | $upstream_response_time | $request_time"; 实际日志如下: ELKstack 中文指南 94nginx访问日志 117.136.9.248 | 08/Apr/2015:16:00:01 +0800 | POST /notice/newmessage? sign=cba4f614e05db285850cadc696fcdad0&token=JAGQ92Mjs3-- gik_b_DsPIQHcyMKYGpD&did=4b749736ac70f12df700b18cd6d051d5&osn=android& osv=4.0.4&appv=3.0.1&net=460-02- 2g&longitude=120.393006&latitude=36.178329&ch=360&lp=1&ver=1&ts=14284799981 51&im=869736012353958&sw=0&sh=0&la=zh-CN&lm=weixin&dt=vivoS11t HTTP/1.1 | 200 | 132 | abcd-sign- v1://dd03c57f8cb6fcef919ab5df66f2903f:d51asq5yslwnyz5t/{\x22type\x22:4,\x22uid\x22 :7567306} | 89 | - | abcd/3.0.1, Android/4.0.4, vivo S11t | nuid=0C0A0A0A01E02455EA7CF47E02FD072C1428480001.157 | - | 10.10.10.13 | bnx02.abcdprivate.com | 10.10.10.22:9999 | 0.022 | 0.022 59.50.44.53 | 08/Apr/2015:16:00:01 +0800 | POST /feed/pubList? appv=3.0.3&did=89da72550de488328e2aba5d97850e9f&dt=iPhone6%2C2&im=B48C 21F3-487E-4071-9742- DC6D61710888&la=cn&latitude=0.000000&lm=weixin&longitude=0.000000&lp=-1.000 000&net=0-0- wifi&osn=iOS&osv=8.1.3&sh=568.000000&sw=320.000000&token=7NobA7asg3Jb6n9 o4ETdPXyNNiHwMs4J&ts=1428480001275 HTTP/1.1 | 200 | 983 | abcd-sign- v1://b398870a0b25b29aae65cd553addc43d:72214ee85d7cca22/{\x22nextkey\x22:\x22 \x22,\x22uid\x22:\x2213062545\x22,\x22token\x22:\x227NobA7asg3Jb6n9o4ETdPXyN NiHwMs4J\x22} | 139 | - | Shopping/3.0.3 (iPhone; iOS 8.1.3; Scale/2.00) | nuid=0C0A0A0A81DF2455017D548502E48E2E1428480001.154 | nuid=CgoKDFUk34GFVH0BLo7kAg== | 10.10.10.11 | bnx02.abcdprivate.com | 10.10.10.35:9999 | 0.025 | 0.026 然后还可以针对 request 做更细致的切分。比如 URL 参数部分。很明显,URL 参数中的字 段,顺序是乱的,第一行,问号之后的第一个字段是 sign,第二行问号之后的第一个字段是 appv,所以需要将字段进行切分,取出每个字段对应的值。官方自带 grok 满足不了要求。最 终采用的 logstash 配置如下: ELKstack 中文指南 95nginx访问日志 filter { ruby { init => "@kname = ['http_x_forwarded_for','time_local','request','status','body_bytes_sent','request_body','content_length','http_referer','http_user_agent','nuid','http_cookie','remote_addr','hostname','upstream_addr','upstream_response_time','request_time']" code => "event.append(Hash[@kname.zip(event['message'].split(' | '))])" } if [request] { ruby { init => "@kname = ['method','uri','verb']" code => "event.append(Hash[@kname.zip(event['request'].split(' '))])" } if [uri] { ruby { init => "@kname = ['url_path','url_args']" code => "event.append(Hash[@kname.zip(event['request'].split('?'))])" } kv { prefix => "url_" source => "url_args" field_split => "& " remove_field => [ "url_args","uri","request" ] } } } mutate { convert => [ "body_bytes_sent" , "integer", "content_length", "integer", "upstream_response_time", "float", "request_time", "float" ] } date { match => [ "time_local", "dd/MMM/yyyy:hh:mm:ss Z" ] locale => "en" } } 最终结果: ELKstack 中文指南 96nginx访问日志 { "message" => "1.43.3.188 | 08/Apr/2015:16:00:01 +0800 | POST /search/suggest?appv=3.0.3&did=dfd5629d705d400795f698055806f01d&dt=iPhone7%2C2&im=AC926907-27AA-4A10-9916-C5DC75F29399&la=cn&latitude=-33.903867&lm=sina&longitude=151.208137&lp=-1.000000&net=0-0-wifi&osn=iOS&osv=8.1.3&sh=667.000000&sw=375.000000&token=_ovaPz6Ue68ybBuhXustPbG-xf1WbsPO&ts=1428480001567 HTTP/1.1 | 200 | 353 | abcd-sign-v1://a24b478486d3bb92ed89a901541b60a5:b23e9d2c14fe6755/{\\x22key\\x22:\\x22last\\x22,\\x22offset\\x22:\\x220\\x22,\\x22token\\x22:\\x22_ovaPz6Ue68ybBuhXustPbG-xf1WbsPO\\x22,\\x22limit\\x22:\\x2220\\x22} | 148 | - | abcdShopping/3.0.3 (iPhone; iOS 8.1.3; Scale/2.00) | nuid=0B0A0A0A9A64AF54F97634640230944E1428480001.113 | nuid=CgoKC1SvZJpkNHb5TpQwAg== | 10.10.10.11 | bnx02.abcdprivate.com | 10.10.10.26:9999 | 0.070 | 0.071", "@version" => "1", "@timestamp" => "2015-04-08T08:00:01.000Z", "type" => "nginxapiaccess", "host" => "blog05.abcdprivate.com", "path" => "/home/nginx/logs/api.access.log", "http_x_forwarded_for" => "1.43.3.188", "time_local" => " 08/Apr/2015:16:00:01 +0800", "status" => "200", "body_bytes_sent" => 353, "request_body" => "abcd-sign-v1://a24b478486d3bb92ed89a901541b60a5:b23e9d2c14fe6755/{\\x22key\\x22:\\x22last\\x22,\\x22offset\\x22:\\x220\\x22,\\x22token\\x22:\\x22_ovaPz6Ue68ybBuhXustPbG-xf1WbsPO\\x22,\\x22limit\\x22:\\x2220\\x22}", "content_length" => 148, "http_referer" => "-", "http_user_agent" => "abcdShopping/3.0.3 (iPhone; iOS 8.1.3; Scale/2.00)", "nuid" => "nuid=0B0A0A0A9A64AF54F97634640230944E1428480001.113", "http_cookie" => "nuid=CgoKC1SvZJpkNHb5TpQwAg==", "remote_addr" => "10.10.10.11", "hostname" => "bnx02.abcdprivate.com", "upstream_addr" => "10.10.10.26:9999", "upstream_response_time" => 0.070, "request_time" => 0.071, "method" => "POST", "verb" => "HTTP/1.1", "url_path" => "/search/suggest", "url_appv" => "3.0.3", "url_did" => "dfd5629d705d400795f698055806f01d", "url_dt" => "iPhone7%2C2", "url_im" => "AC926907-27AA-4A10-9916-C5DC75F29399", "url_la" => "cn", "url_latitude" => "-33.903867", "url_lm" => "sina", "url_longitude" => "151.208137", "url_lp" => "-1.000000", "url_net" => "0-0-wifi", "url_osn" => "iOS", "url_osv" => "8.1.3", "url_sh" => "667.000000", "url_sw" => "375.000000", "url_token" => "_ovaPz6Ue68ybBuhXustPbG-xf1WbsPO", "url_ts" => "1428480001567" } 如果 url 参数过多,可以不使用 kv 切割,或者预先定义成 nested object 后,改成数组形式: ELKstack 中文指南 97nginx访问日志 if [uri] { ruby { init => "@kname = ['url_path','url_args']" code => "event.append(Hash[@kname.zip(event['request'].split('?'))])" } if [url_args] { ruby { init => "@kname = ['key','value']" code => "event['nested_args'] = event['url_args'].split('&').collect {|i| Hash[@kname.zip(i.split('='))]}" remove_field => [ "url_args","uri","request" ] } } } 采用 nested object 的优化原理和 nested object 的使用方式,请阅读稍后 Elasticsearch 调优 章节。 json format 自定义分隔符虽好,但是配置写起来毕竟复杂很多。其实对 logstash 来说,nginx 日志还有 另一种更简便的处理方式。就是自定义日志格式时,通过手工拼写,直接输出成 JSON 格 式: log_format json '{"@timestamp":"$time_iso8601",' '"host":"$server_addr",' '"clientip":"$remote_addr",' '"size":$body_bytes_sent,' '"responsetime":$request_time,' '"upstreamtime":"$upstream_response_time",' '"upstreamhost":"$upstream_addr",' '"http_host":"$host",' '"url":"$uri",' '"xff":"$http_x_forwarded_for",' '"referer":"$http_referer",' '"agent":"$http_user_agent",' '"status":"$status"}'; 然后采用下面的 logstash 配置即可: ELKstack 中文指南 98nginx访问日志 input { file { path => "/var/log/nginx/access.log" codec => json } } filter { mutate { split => [ "upstreamtime", "," ] } mutate { convert => [ "upstreamtime", "float" ] } } 这里采用多个 mutate 插件,因为 upstreamtime 可能有多个数值,所以先切割成数组以后, 再分别转换成浮点型数值。而在 mutate 中,convert 函数执行优先级高于 split 函数,所以只 能分开两步写。mutate 内各函数优先级顺序,之前插件介绍章节有详细说明,读者可以返回 去加强阅读。 syslog Nginx 从 1.7 版开始,加入了 syslog 支持。Tengine 则更早。这样我们可以通过 syslog 直接 发送日志出来。Nginx 上的配置如下: access_log syslog:server=unix:/data0/rsyslog/nginx.sock locallog; 或者直接发送给远程 logstash 机器: access_log syslog:server=192.168.0.2:5140,facility=local6,tag=nginx-access,severity=info logstashlog; 默认情况下,Nginx 将使用 local7.info 等级, nginx 为标签,发送数据。注意,采用 syslog 发送日志的时候,无法配置 buffer=16k 选项。 ELKstack 中文指南 99nginx访问日志 Nginx 错误日志 Nginx 错误日志是运维人员最常见但又极其容易忽略的日志类型之一。Nginx 错误日志即没有 统一明确的分隔符,也没有特别方便的正则模式,但通过 logstash 不同插件的组合,还是可 以轻松做到数据处理。 值得注意的是,Nginx 错误日志中,有一类数据是接收过大请求体时的报错,默认信息会把请 求体的具体字节数记录下来。每次请求的字节数基本都是在变化的,这意味着常用的 topN 等 聚合函数对该字段都没有明显效果。所以,对此需要做一下特殊处理。 最后形成的 logstash 配置如下所示: filter { grok { match => { "message" => "(?\d\d\d\d/\d\d/\d\d \d\d:\d\d:\d\d) \[(?\w+)\] \S+: \*\d+ (?[^,]+), (?.*)$" } } mutate { rename => [ "host", "fromhost" ] gsub => [ "errmsg", "too large body: \d+ bytes", "too large body" ] } if [errinfo] { ruby { code => "event.append(Hash[event['errinfo'].split(', ').map{|l| l.split(': ')}])" } } grok { match => { "request" => '"%{WORD:verb} %{URIPATH:urlpath}(?:\?%{NGX_URIPARAM:urlparam})?(?: HTTP/%{NUMBER:httpversion})"' } patterns_dir => "/etc/logstash/patterns" remove_field => [ "message", "errinfo", "request" ] } } 经过这段 logstash 配置的 Nginx 错误日志生成的事件如下所示: ELKstack 中文指南 100nginx错误日志 { "@version": "1", "@timestamp": "2015-07-02T01:26:40.000Z", "type": "nginx-error", "errtype": "error", "errmsg": "client intended to send too large body", "fromhost": "web033.mweibo.yf.sinanode.com", "client": "36.16.7.17", "server": "api.v5.weibo.cn", "host": "\"api.weibo.cn\"", "verb": "POST", "urlpath": "/2/client/addlog_batch", "urlparam": "gsid=_2A254UNaSDeTxGeRI7FMX9CrEyj2IHXVZRG1arDV6PUJbrdANLROskWp9bXakjUZM5792FW9A5S9EU4jxqQ..&wm=3333_2001&i=0c6f156&b=1&from=1053093010&c=iphone&v_p=21&skin=default&v_f=1&s=8f14e573&lang=zh_CN&ua=iPhone7,1__weibo__5.3.0__iphone__os8.3", "httpversion": "1.1" } ELKstack 中文指南 101nginx错误日志 postfix 日志 postfix 是 Linux 平台上最常用的邮件服务器软件。邮件服务的运维复杂度一向较高,在此提 供一个针对 postfix 日志的解析处理方案。方案出自:https://github.com/whyscream/postfix- grok-patterns。 因为 postfix 默认通过 syslog 方式输出日志,所以可以选择通过 rsyslog 直接转发给 logstash,也可以由 logstash 读取 rsyslog 记录的文件。下列配置中省略了对 syslog 协议解 析的部分。 filter { # grok log lines by program name (listed alpabetically) if [program] =~ /^postfix.*\/anvil$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_ANVIL}" ] tag_on_failure => [ "_grok_postfix_anvil_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/bounce$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_BOUNCE}" ] tag_on_failure => [ "_grok_postfix_bounce_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/cleanup$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_CLEANUP}" ] tag_on_failure => [ "_grok_postfix_cleanup_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/dnsblog$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_DNSBLOG}" ] tag_on_failure => [ "_grok_postfix_dnsblog_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/local$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_LOCAL}" ] tag_on_failure => [ "_grok_postfix_local_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/master$/ { ELKstack 中文指南 102postfix日志 grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_MASTER}" ] tag_on_failure => [ "_grok_postfix_master_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/pickup$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_PICKUP}" ] tag_on_failure => [ "_grok_postfix_pickup_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/pipe$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_PIPE}" ] tag_on_failure => [ "_grok_postfix_pipe_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/postdrop$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_POSTDROP}" ] tag_on_failure => [ "_grok_postfix_postdrop_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/postscreen$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_POSTSCREEN}" ] tag_on_failure => [ "_grok_postfix_postscreen_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/qmgr$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_QMGR}" ] tag_on_failure => [ "_grok_postfix_qmgr_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/scache$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_SCACHE}" ] tag_on_failure => [ "_grok_postfix_scache_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/sendmail$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_SENDMAIL}" ] tag_on_failure => [ "_grok_postfix_sendmail_nomatch" ] ELKstack 中文指南 103postfix日志 add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/smtp$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_SMTP}" ] tag_on_failure => [ "_grok_postfix_smtp_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/lmtp$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_LMTP}" ] tag_on_failure => [ "_grok_postfix_lmtp_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/smtpd$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_SMTPD}" ] tag_on_failure => [ "_grok_postfix_smtpd_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/tlsmgr$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_TLSMGR}" ] tag_on_failure => [ "_grok_postfix_tlsmgr_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/tlsproxy$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_TLSPROXY}" ] tag_on_failure => [ "_grok_postfix_tlsproxy_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/trivial-rewrite$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_TRIVIAL_REWRITE}" ] tag_on_failure => [ "_grok_postfix_trivial_rewrite_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } else if [program] =~ /^postfix.*\/discard$/ { grok { patterns_dir => "/etc/logstash/patterns.d" match => [ "message", "%{POSTFIX_DISCARD}" ] tag_on_failure => [ "_grok_postfix_discard_nomatch" ] add_tag => [ "_grok_postfix_success" ] } } ELKstack 中文指南 104postfix日志 # process key-value data is it exists if [postfix_keyvalue_data] { kv { source => "postfix_keyvalue_data" trim => "<>," prefix => "postfix_" remove_field => [ "postfix_keyvalue_data" ] } # some post processing of key-value data if [postfix_client] { grok { patterns_dir => "/etc/logstash/patterns.d" match => ["postfix_client", "%{POSTFIX_CLIENT_INFO}"] tag_on_failure => [ "_grok_kv_postfix_client_nomatch" ] remove_field => [ "postfix_client" ] } } if [postfix_relay] { grok { patterns_dir => "/etc/logstash/patterns.d" match => ["postfix_relay", "%{POSTFIX_RELAY_INFO}"] tag_on_failure => [ "_grok_kv_postfix_relay_nomatch" ] remove_field => [ "postfix_relay" ] } } if [postfix_delays] { grok { patterns_dir => "/etc/logstash/patterns.d" match => ["postfix_delays", "%{POSTFIX_DELAYS}"] tag_on_failure => [ "_grok_kv_postfix_delays_nomatch" ] remove_field => [ "postfix_delays" ] } } } # Do some data type conversions mutate { convert => [ # list of integer fields "postfix_anvil_cache_size", "integer", "postfix_anvil_conn_count", "integer", "postfix_anvil_conn_rate", "integer", "postfix_client_port", "integer", "postfix_nrcpt", "integer", "postfix_postscreen_cache_dropped", "integer", "postfix_postscreen_cache_retained", "integer", "postfix_postscreen_dnsbl_rank", "integer", "postfix_relay_port", "integer", "postfix_server_port", "integer", "postfix_size", "integer", "postfix_status_code", "integer", "postfix_termination_signal", "integer", ELKstack 中文指南 105postfix日志 "postfix_uid", "integer", # list of float fields "postfix_delay", "float", "postfix_delay_before_qmgr", "float", "postfix_delay_conn_setup", "float", "postfix_delay_in_qmgr", "float", "postfix_delay_transmission", "float", "postfix_postscreen_violation_time", "float" ] } } 配置中使用了一系列自定义 grok 正则,全部内容如下所示。保存成 /etc/logstash/patterns.d/ 下一个文本文件即可。 # common postfix patterns POSTFIX_QUEUEID ([0-9A-F]{6,}|[0-9a-zA-Z]{15,}|NOQUEUE) POSTFIX_CLIENT_INFO %{HOST:postfix_client_hostname}?\[%{IP:postfix_client_ip}\](:%{INT:postfix_client_port})? POSTFIX_RELAY_INFO %{HOST:postfix_relay_hostname}?\[(%{IP:postfix_relay_ip}|%{DATA:postfix_relay_service})\](:%{INT:postfix_relay_port})?|%{WORD:postfix_relay_service} POSTFIX_SMTP_STAGE (CONNECT|HELO|EHLO|STARTTLS|AUTH|MAIL|RCPT|DATA|RSET|UNKNOWN|END-OF-MESSAGE|VRFY|\.) POSTFIX_ACTION (reject|defer|accept|header-redirect) POSTFIX_STATUS_CODE \d{3} POSTFIX_STATUS_CODE_ENHANCED \d\.\d\.\d POSTFIX_DNSBL_MESSAGE Service unavailable; .* \[%{GREEDYDATA:postfix_status_data}\] %{GREEDYDATA:postfix_status_message}; POSTFIX_PS_ACCESS_ACTION (DISCONNECT|BLACKLISTED|WHITELISTED|WHITELIST VETO|PASS NEW|PASS OLD) POSTFIX_PS_VIOLATION (BARE NEWLINE|COMMAND (TIME|COUNT|LENGTH) LIMIT|COMMAND PIPELINING|DNSBL|HANGUP|NON-SMTP COMMAND|PREGREET) POSTFIX_TIME_UNIT %{NUMBER}[smhd] POSTFIX_KEYVALUE %{POSTFIX_QUEUEID:postfix_queueid}: %{GREEDYDATA:postfix_keyvalue_data} POSTFIX_WARNING (warning|fatal): %{GREEDYDATA:postfix_warning} POSTFIX_TLSCONN (Anonymous|Trusted|Untrusted|Verified) TLS connection established (to %{POSTFIX_RELAY_INFO}|from %{POSTFIX_CLIENT_INFO}): %{DATA:postfix_tls_version} with cipher %{DATA:postfix_tls_cipher} \(%{DATA:postfix_tls_cipher_size} bits\) POSTFIX_DELAYS %{NUMBER:postfix_delay_before_qmgr}/%{NUMBER:postfix_delay_in_qmgr}/%{NUMBER:postfix_delay_conn_setup}/%{NUMBER:postfix_delay_transmission} POSTFIX_LOSTCONN (lost connection|timeout|Connection timed out) POSTFIX_PROXY_MESSAGE (%{POSTFIX_STATUS_CODE:postfix_proxy_status_code} )?(%{POSTFIX_STATUS_CODE_ENHANCED:postfix_proxy_status_code_enhanced})?.* # helper patterns GREEDYDATA_NO_COLON [^:]* # smtpd patterns POSTFIX_SMTPD_CONNECT connect from %{POSTFIX_CLIENT_INFO} POSTFIX_SMTPD_DISCONNECT disconnect from %{POSTFIX_CLIENT_INFO} POSTFIX_SMTPD_LOSTCONN (%{POSTFIX_LOSTCONN:postfix_smtpd_lostconn_data} after %{POSTFIX_SMTP_STAGE:postfix_smtp_stage}( \(%{INT} bytes\))? from %{POSTFIX_CLIENT_INFO}|%{GREEDYDATA:postfix_action} from %{POSTFIX_CLIENT_INFO}: %{POSTFIX_LOSTCONN:postfix_smtpd_lostconn_data}) POSTFIX_SMTPD_NOQUEUE NOQUEUE: %{POSTFIX_ACTION:postfix_action}: %{POSTFIX_SMTP_STAGE:postfix_smtp_stage} from %{POSTFIX_CLIENT_INFO}: %{POSTFIX_STATUS_CODE:postfix_status_code} %{POSTFIX_STATUS_CODE_ENHANCED:postfix_status_code_enhanced}( <%{DATA:postfix_status_data}>:)? (%{POSTFIX_DNSBL_MESSAGE}|%{GREEDYDATA:postfix_status_message};) %{GREEDYDATA:postfix_keyvalue_data} POSTFIX_SMTPD_PIPELINING improper command pipelining after %{POSTFIX_SMTP_STAGE:postfix_smtp_stage} from %{POSTFIX_CLIENT_INFO}: POSTFIX_SMTPD_PROXY proxy-%{POSTFIX_ACTION:postfix_proxy_result}: (%{POSTFIX_SMTP_STAGE:postfix_proxy_smtp_stage}): %{POSTFIX_PROXY_MESSAGE:postfix_proxy_message}; %{GREEDYDATA:postfix_keyvalue_data} # cleanup patterns POSTFIX_CLEANUP_MILTER %{POSTFIX_QUEUEID:postfix_queueid}: milter-%{POSTFIX_ACTION:postfix_milter_result}: %{GREEDYDATA:postfix_milter_message}; %{GREEDYDATA_NO_COLON:postfix_keyvalue_data}(: %{GREEDYDATA:postfix_milter_data})? # qmgr patterns POSTFIX_QMGR_REMOVED %{POSTFIX_QUEUEID:postfix_queueid}: removed ELKstack 中文指南 106postfix日志 POSTFIX_QMGR_ACTIVE %{POSTFIX_QUEUEID:postfix_queueid}: %{GREEDYDATA:postfix_keyvalue_data} \(queue active\) # pipe patterns POSTFIX_PIPE_DELIVERED %{POSTFIX_QUEUEID:postfix_queueid}: %{GREEDYDATA:postfix_keyvalue_data} \(delivered via %{WORD:postfix_pipe_service} service\) POSTFIX_PIPE_FORWARD %{POSTFIX_QUEUEID:postfix_queueid}: %{GREEDYDATA:postfix_keyvalue_data} \(mail forwarding loop for %{GREEDYDATA:postfix_to}\) # postscreen patterns POSTFIX_PS_CONNECT CONNECT from %{POSTFIX_CLIENT_INFO} to \[%{IP:postfix_server_ip}\]:%{INT:postfix_server_port} POSTFIX_PS_ACCESS %{POSTFIX_PS_ACCESS_ACTION:postfix_postscreen_access} %{POSTFIX_CLIENT_INFO} POSTFIX_PS_NOQUEUE %{POSTFIX_SMTPD_NOQUEUE} POSTFIX_PS_TOOBUSY NOQUEUE: reject: CONNECT from %{POSTFIX_CLIENT_INFO}: %{GREEDYDATA:postfix_postscreen_toobusy_data} POSTFIX_PS_DNSBL %{POSTFIX_PS_VIOLATION:postfix_postscreen_violation} rank %{INT:postfix_postscreen_dnsbl_rank} for %{POSTFIX_CLIENT_INFO} POSTFIX_PS_CACHE cache %{DATA} full cleanup: retained=%{NUMBER:postfix_postscreen_cache_retained} dropped=%{NUMBER:postfix_postscreen_cache_dropped} entries POSTFIX_PS_VIOLATIONS %{POSTFIX_PS_VIOLATION:postfix_postscreen_violation}( %{INT})?( after %{NUMBER:postfix_postscreen_violation_time})? from %{POSTFIX_CLIENT_INFO}( after %{POSTFIX_SMTP_STAGE:postfix_smtp_stage})? # dnsblog patterns POSTFIX_DNSBLOG_LISTING addr %{IP:postfix_client_ip} listed by domain %{HOST:postfix_dnsbl_domain} as %{IP:postfix_dnsbl_result} # tlsproxy patterns POSTFIX_TLSPROXY_CONN (DIS)?CONNECT( from)? %{POSTFIX_CLIENT_INFO} # anvil patterns POSTFIX_ANVIL_CONN_RATE statistics: max connection rate %{NUMBER:postfix_anvil_conn_rate}/%{POSTFIX_TIME_UNIT:postfix_anvil_conn_period} for \(%{DATA:postfix_service}:%{IP:postfix_client_ip}\) at %{SYSLOGTIMESTAMP:postfix_anvil_timestamp} POSTFIX_ANVIL_CONN_CACHE statistics: max cache size %{NUMBER:postfix_anvil_cache_size} at %{SYSLOGTIMESTAMP:postfix_anvil_timestamp} POSTFIX_ANVIL_CONN_COUNT statistics: max connection count %{NUMBER:postfix_anvil_conn_count} for \(%{DATA:postfix_service}:%{IP:postfix_client_ip}\) at %{SYSLOGTIMESTAMP:postfix_anvil_timestamp} # smtp patterns POSTFIX_SMTP_DELIVERY %{POSTFIX_KEYVALUE} status=%{WORD:postfix_status}( \(%{GREEDYDATA:postfix_smtp_response}\))? POSTFIX_SMTP_CONNERR connect to %{POSTFIX_RELAY_INFO}: (Connection timed out|No route to host|Connection refused) POSTFIX_SMTP_LOSTCONN %{POSTFIX_QUEUEID:postfix_queueid}: %{POSTFIX_LOSTCONN} with %{POSTFIX_RELAY_INFO} # master patterns POSTFIX_MASTER_START (daemon started|reload) -- version %{DATA:postfix_version}, configuration %{PATH:postfix_config_path} POSTFIX_MASTER_EXIT terminating on signal %{INT:postfix_termination_signal} # bounce patterns POSTFIX_BOUNCE_NOTIFICATION %{POSTFIX_QUEUEID:postfix_queueid}: sender (non-delivery|delivery status|delay) notification: %{POSTFIX_QUEUEID:postfix_bounce_queueid} # scache patterns POSTFIX_SCACHE_LOOKUPS statistics: (address|domain) lookup hits=%{INT:postfix_scache_hits} miss=%{INT:postfix_scache_miss} success=%{INT:postfix_scache_success} POSTFIX_SCACHE_SIMULTANEOUS statistics: max simultaneous domains=%{INT:postfix_scache_domains} addresses=%{INT:postfix_scache_addresses} connection=%{INT:postfix_scache_connection} POSTFIX_SCACHE_TIMESTAMP statistics: start interval %{SYSLOGTIMESTAMP:postfix_scache_timestamp} # aggregate all patterns POSTFIX_SMTPD %{POSTFIX_SMTPD_CONNECT}|%{POSTFIX_SMTPD_DISCONNECT}|%{POSTFIX_SMTPD_LOSTCONN}|%{POSTFIX_SMTPD_NOQUEUE}|%{POSTFIX_SMTPD_PIPELINING}|%{POSTFIX_TLSCONN}|%{POSTFIX_WARNING}|%{POSTFIX_SMTPD_PROXY}|%{POSTFIX_KEYVALUE} POSTFIX_CLEANUP %{POSTFIX_CLEANUP_MILTER}|%{POSTFIX_WARNING}|%{POSTFIX_KEYVALUE} POSTFIX_QMGR %{POSTFIX_QMGR_REMOVED}|%{POSTFIX_QMGR_ACTIVE}|%{POSTFIX_WARNING} POSTFIX_PIPE %{POSTFIX_PIPE_DELIVERED}|%{POSTFIX_PIPE_FORWARD} POSTFIX_POSTSCREEN %{POSTFIX_PS_CONNECT}|%{POSTFIX_PS_ACCESS}|%{POSTFIX_PS_NOQUEUE}|%{POSTFIX_PS_TOOBUSY}|%{POSTFIX_PS_CACHE}|%{POSTFIX_PS_DNSBL}|%{POSTFIX_PS_VIOLATIONS}|%{POSTFIX_WARNING} POSTFIX_DNSBLOG %{POSTFIX_DNSBLOG_LISTING} POSTFIX_ANVIL %{POSTFIX_ANVIL_CONN_RATE}|%{POSTFIX_ANVIL_CONN_CACHE}|%{POSTFIX_ANVIL_CONN_COUNT} POSTFIX_SMTP %{POSTFIX_SMTP_DELIVERY}|%{POSTFIX_SMTP_CONNERR}|%{POSTFIX_SMTP_LOSTCONN}|%{POSTFIX_TLSCONN}|%{POSTFIX_WARNING} POSTFIX_DISCARD %{POSTFIX_KEYVALUE} status=%{WORD:postfix_status} ELKstack 中文指南 107postfix日志 POSTFIX_LMTP %{POSTFIX_SMTP} POSTFIX_PICKUP %{POSTFIX_KEYVALUE} POSTFIX_TLSPROXY %{POSTFIX_TLSPROXY_CONN} POSTFIX_MASTER %{POSTFIX_MASTER_START}|%{POSTFIX_MASTER_EXIT} POSTFIX_BOUNCE %{POSTFIX_BOUNCE_NOTIFICATION} POSTFIX_SENDMAIL %{POSTFIX_WARNING} POSTFIX_POSTDROP %{POSTFIX_WARNING} POSTFIX_SCACHE %{POSTFIX_SCACHE_LOOKUPS}|%{POSTFIX_SCACHE_SIMULTANEOUS}|%{POSTFIX_SCACHE_TIMESTAMP} POSTFIX_TRIVIAL_REWRITE %{POSTFIX_WARNING} POSTFIX_TLSMGR %{POSTFIX_WARNING} POSTFIX_LOCAL %{POSTFIX_KEYVALUE} ELKstack 中文指南 108postfix日志 ossec 本节作者:林鹏 配置OSSEC SYSLOG 输出 (所有agent) 1. 编辑ossec.conf 文件(默认为/var/ossec/etc/ossec.conf) 2. 在ossec.conf中添加下列内容(10.0.0.1 为 接收syslog 的服务器) 10.0.0.1 9000 default 1. 开启OSSEC允许syslog输出功能 /var/ossec/bin/ossec-control enable client-syslog 1. 重启 OSSEC服务 /var/ossec/bin/ossec-control start 配置LOGSTASH 1. 在logstash 中 配置文件中增加(或新建)如下内容:(假设10.0.0.1 为ES服务器,假设文件 名为logstash-ossec.conf ) ELKstack 中文指南 109ossec日志 input { udp { port => 9000 type => "syslog" } } filter { if [type] == "syslog" { grok { match => { "message" => "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_host} %{DATA:syslog_program}: Alert Level: %{BASE10NUM:Alert_Level}; Rule: %{BASE10NUM:Rule} - %{GREEDYDATA:Description}; Location: %{GREEDYDATA:Details}" } add_field => [ "ossec_server", "%{host}" ] } mutate { remove_field => [ "syslog_hostname", "syslog_message", "syslog_pid", "message", "@version", "type", "host" ] } } } output { elasticsearch_http { host => "10.0.0.1" } } 推荐 Kibana dashboard 社区已经有人根据 ossec 的常见需求,制作有 dashboard 可以直接从 Kibana3 页面加载使 用。 ELKstack 中文指南 110ossec日志 dashboard 的 JSON 文件 见:https://github.com/magenx/Logstash/raw/master/kibana/kibana_dashboard.json 加载方式,请阅读本书稍后 Kibana 章节相关内容。 ELKstack 中文指南 111ossec日志 Windows Event Log 前面说过如何在 windows 上利用 nxlog 传输日志数据。事实上,对于 windows 本身,也有类 似 syslog 的设计,叫 eventlog。本节介绍如何处理 windows eventlog。 采集端配置 logstash 配置 input { eventlog { #logfile => ["Application", "Security", "System"] logfile => ["Security"] type => "winevent" tags => [ "caen" ] } } nxlog 配置 ELKstack 中文指南 112windows系统日志 ## This is a sample configuration file. See the nxlog reference manual about the ## configuration options. It should be installed locally and is also available ## online at http://nxlog.org/nxlog-docs/en/nxlog-reference-manual.html ## Please set the ROOT to the folder your nxlog was installed into, ## otherwise it will not start. #define ROOT C:\Program Files\nxlog define ROOT C:\Program Files (x86)\nxlog Moduledir %ROOT%\modules CacheDir %ROOT%\data Pidfile %ROOT%\data\nxlog.pid SpoolDir %ROOT%\data LogFile %ROOT%\data\nxlog.log Module xm_json Module im_msvistalog # For windows 2003 and earlier use the following: # Module im_mseventlog Exec to_json(); Module om_tcp Host 10.66.66.66 Port 5140 Path in => out Logstash 解析配置 input { tcp { codec => "json" port => 5140 tags => ["windows","nxlog"] type => "nxlog-json" } } # end input filter { ELKstack 中文指南 113windows系统日志 if [type] == "nxlog-json" { date { match => ["[EventTime]", "YYYY-MM-dd HH:mm:ss"] timezone => "Europe/London" } mutate { rename => [ "AccountName", "user" ] rename => [ "AccountType", "[eventlog][account_type]" ] rename => [ "ActivityId", "[eventlog][activity_id]" ] rename => [ "Address", "ip6" ] rename => [ "ApplicationPath", "[eventlog][application_path]" ] rename => [ "AuthenticationPackageName", "[eventlog][authentication_package_name]" ] rename => [ "Category", "[eventlog][category]" ] rename => [ "Channel", "[eventlog][channel]" ] rename => [ "Domain", "domain" ] rename => [ "EventID", "[eventlog][event_id]" ] rename => [ "EventType", "[eventlog][event_type]" ] rename => [ "File", "[eventlog][file_path]" ] rename => [ "Guid", "[eventlog][guid]" ] rename => [ "Hostname", "hostname" ] rename => [ "Interface", "[eventlog][interface]" ] rename => [ "InterfaceGuid", "[eventlog][interface_guid]" ] rename => [ "InterfaceName", "[eventlog][interface_name]" ] rename => [ "IpAddress", "ip" ] rename => [ "IpPort", "port" ] rename => [ "Key", "[eventlog][key]" ] rename => [ "LogonGuid", "[eventlog][logon_guid]" ] rename => [ "Message", "message" ] rename => [ "ModifyingUser", "[eventlog][modifying_user]" ] rename => [ "NewProfile", "[eventlog][new_profile]" ] rename => [ "OldProfile", "[eventlog][old_profile]" ] rename => [ "Port", "port" ] rename => [ "PrivilegeList", "[eventlog][privilege_list]" ] rename => [ "ProcessID", "pid" ] rename => [ "ProcessName", "[eventlog][process_name]" ] rename => [ "ProviderGuid", "[eventlog][provider_guid]" ] rename => [ "ReasonCode", "[eventlog][reason_code]" ] rename => [ "RecordNumber", "[eventlog][record_number]" ] rename => [ "ScenarioId", "[eventlog][scenario_id]" ] rename => [ "Severity", "level" ] rename => [ "SeverityValue", "[eventlog][severity_code]" ] rename => [ "SourceModuleName", "nxlog_input" ] rename => [ "SourceName", "[eventlog][program]" ] rename => [ "SubjectDomainName", "[eventlog][subject_domain_name]" ] rename => [ "SubjectLogonId", "[eventlog][subject_logonid]" ] rename => [ "SubjectUserName", "[eventlog][subject_user_name]" ] rename => [ "SubjectUserSid", "[eventlog][subject_user_sid]" ] rename => [ "System", "[eventlog][system]" ] rename => [ "TargetDomainName", "[eventlog][target_domain_name]" ] rename => [ "TargetLogonId", "[eventlog][target_logonid]" ] rename => [ "TargetUserName", "[eventlog][target_user_name]" ] rename => [ "TargetUserSid", "[eventlog][target_user_sid]" ] rename => [ "ThreadID", "thread" ] ELKstack 中文指南 114windows系统日志 } mutate { remove_field => [ "CurrentOrNextState", "Description", "EventReceivedTime", "EventTime", "EventTimeWritten", "IPVersion", "KeyLength", "Keywords", "LmPackageName", "LogonProcessName", "LogonType", "Name", "Opcode", "OpcodeValue", "PolicyProcessingMode", "Protocol", "ProtocolType", "SourceModuleType", "State", "Task", "TransmittedServices", "Type", "UserID", "Version" ] } } } ELKstack 中文指南 115windows系统日志 Java 日志 之前在 codec 章节,曾经提到过,对 Java 日志,除了使用 multiline 做多行日志合并以外, 还可以直接通过 log4j 写入到 logstash 里。本节就讲述如何在 Java 应用环境做到这点。 Log4J 首先,需要配置 Java 应用的 Log4J 设置,启动一个内置的 SocketAppender 。修改应用的 log4j.xml 配置文件,添加如下配置段: 然后把这个新定义的 appender 对象加入到 root logger 里,可以跟其他已有 logger 共存: 如果是 log4j.properties 配置文件,则对应配置如下: log4j.rootLogger=DEBUG, logstash ###SocketAppender### log4j.appender.logstash=org.apache.log4j.net.SocketAppender log4j.appender.logstash.Port=4560 log4j.appender.logstash.RemoteHost=logstash_hostname log4j.appender.logstash.ReconnectionDelay=60000 log4j.appender.logstash.LocationInfo=true Log4J 会持续尝试连接你配置的 logstash_hostname 这个地址,建立连接后,即开始发送日 志数据。 Logstash ELKstack 中文指南 116Java日志 Java 应用端的配置完成以后,开始设置 Logstash 的接收端。配置如下所示。其中 4560 端口 是 Log4J SocketAppender 的默认对端端口。 input { log4j { type => "log4j-json" port => 4560 } } 异常堆栈测试验证 运行起来 logstash 后,编写如下一个简单 log4j 程序: import org.apache.log4j.Logger; public class HelloExample{ final static Logger logger = Logger.getLogger(HelloExample.class); public static void main(String[] args) { HelloExample obj = new HelloExample(); try{ obj.divide(); }catch(ArithmeticException ex){ logger.error("Sorry, something wrong!", ex); } } private void divide(){ int i = 10 /0; } } 编译运行: # javac -cp ./logstash-1.5.0.rc2/vendor/bundle/jruby/1.9/gems/logstash-input-log4j-0.1.3-java/lib/log4j/log4j/1.2.17/log4j-1.2.17.jar HelloExample.java # java -cp .:./logstash-1.5.0.rc2/vendor/bundle/jruby/1.9/gems/logstash-input-log4j-0.1.3-java/lib/log4j/log4j/1.2.17/log4j-1.2.17.jar HelloExample 即可在 logstash 的终端输出看到如下事件记录: ELKstack 中文指南 117Java日志 { "message" => "Sorry, something wrong!", "@version" => "1", "@timestamp" => "2015-07-02T13:24:45.727Z", "type" => "log4j-json", "host" => "127.0.0.1:52420", "path" => "HelloExample", "priority" => "ERROR", "logger_name" => "HelloExample", "thread" => "main", "class" => "HelloExample", "file" => "HelloExample.java:9", "method" => "main", "stack_trace" => "java.lang.ArithmeticException: / by zero\n\tat HelloExample.divide(HelloExample.java:13)\n\tat HelloExample.main(HelloExample.java:7)" } 可以看到,异常堆栈直接就记录在单行内了。 JSON Event layout 如果无法采用 socketappender ,必须使用文件方式的,其实 Log4J 有一个 layout 特性,用 来控制日志输出的格式。和 Nginx 日志自己拼接 JSON 输出类似,也可以通过 layout 功能, 记录成 JSON 格式。推荐使用:https://github.com/logstash/log4j-jsonevent-layout ELKstack 中文指南 118Java日志 MySQL慢查询日志 MySQL 有多种日志可以记录,常见的有 error log、slow log、general log、bin log 等。其中 slow log 作为性能监控和优化的入手点,最为首要。本节即讨论如何用 logstash 处理 slow log。至于 general log,格式处理基本类似,不过由于 general 量级比 slow 大得多,推荐采 用 packetbeat 协议解析的方式更高效的完成这项工作。相关内容阅读本书稍后章节。 MySQL slow log 的 logstash 处理配置示例如下: input { file { type => "mysql-slow" path => "/var/log/mysql/mysql-slow.log" codec => multiline { pattern => "^# User@Host:" negate => true what => "previous" } } } filter { # drop sleep events grok { match => { "message" => "SELECT SLEEP" } add_tag => [ "sleep_drop" ] tag_on_failure => [] # prevent default _grokparsefailure tag on real records } if "sleep_drop" in [tags] { drop {} } grok { match => [ "message", "(?m)^# User@Host: %{USER:user}\[[^\]]+\] @ (?:(?\S*) )?\[(?:%{IP:clientip})?\]\s*# Query_time: %{NUMBER:query_time:float}\s+Lock_time: %{NUMBER:lock_time:float}\s+Rows_sent: %{NUMBER:rows_sent:int}\s+Rows_examined: %{NUMBER:rows_examined:int}\s*(?:use %{DATA:database};\s*)?SET timestamp=%{NUMBER:timestamp};\s*(?(?\w+)\s+.*)\n# Time:.*$" ] } date { match => [ "timestamp", "UNIX" ] remove_field => [ "timestamp" ] } } 运行该配置,logstash 即可将多行的 MySQL slow log 处理成如下事件: ELKstack 中文指南 119MySQL慢查询日志 { "@timestamp" => "2014-03-04T19:59:06.000Z", "message" => "# User@Host: logstash[logstash] @ localhost [127.0.0.1]\n# Query_time: 5.310431 Lock_time: 0.029219 Rows_sent: 1 Rows_examined: 24575727\nSET timestamp=1393963146;\nselect count(*) from node join variable order by rand();\n# Time: 140304 19:59:14", "@version" => "1", "tags" => [ [0] "multiline" ], "type" => "mysql-slow", "host" => "raochenlindeMacBook-Air.local", "path" => "/var/log/mysql/mysql-slow.log", "user" => "logstash", "clienthost" => "localhost", "clientip" => "127.0.0.1", "query_time" => 5.310431, "lock_time" => 0.029219, "rows_sent" => 1, "rows_examined" => 24575727, "query" => "select count(*) from node join variable order by rand();", "action" => "select" } ELKstack 中文指南 120MySQL慢查询日志 性能与测试 任何软件都需要掌握其性能瓶颈,以及线上运行时的性能状态。Logstash 也不例外。 长久以来,Logstash 在这方面一直处于比较黑盒的状态。因为其内部队列使用的是标准的 stud 库,并非自己实现,在 Logstash 本身源代码里是找不出来什么问题的。我们只能按照其 pipeline 原理,总结出来一些模拟检测的手段。本节主要介绍这方面的内容。 Logstash 官方已经将性能监控问题,列入 roadmap,或许在未来的 1.6 或者 2.0 版本中,会 有质的改变。 ELKstack 中文指南 121性能与测试 生成测试数据(Generator) 实际运行的时候这个插件是派不上用途的,但这个插件依然是非常重要的插件之一。因为每 一个使用 ELK stack 的运维人员都应该清楚一个道理:数据是支持操作的唯一真理(否则你 也用不着 ELK)。所以在上线之前,你一定会需要在自己的实际环境中,测试 Logstash 和 Elasticsearch 的性能状况。这时候,这个用来生成测试数据的插件就有用了! 配置示例 input { generator { count => 10000000 message => '{"key1":"value1","key2":[1,2],"key3":{"subkey1":"subvalue1"}}' codec => json } } 插件的默认生成数据,message 内容是 "hello world"。你可以根据自己的实际需要这里来写 其他内容。 使用方式 做测试有两种主要方式: 配合 LogStash::Outputs::Null inputs/generator 是无中生有,output/null 则是锯嘴葫芦。事件流转到这里直接就略过,什么 操作都不做。相当于只测试 Logstash 的 pipe 和 filter 效率。测试过程非常简单: $ time ./bin/logstash -f generator_null.conf real 3m0.864s user 3m39.031s sys 0m51.621s 使用 pv 命令配合 LogStash::Outputs::Stdout 和 LogStash::Codecs::Dots 上面的这种方式虽然想法挺好,不过有个小漏洞:logstash 是在 JVM 上运行的,有一个明显 的启动时间,运行也有一段事件的预热后才算稳定运行。所以,要想更真实的反应 logstash 在长期运行时候的效率,还有另一种方法: ELKstack 中文指南 122generator方式 output { stdout { codec => dots } } LogStash::Codecs::Dots 也是一个另类的 codec 插件,他的作用是:把每个 event 都变成一 个点( . )。这样,在输出的时候,就变成了一个一个的 . 在屏幕上。显然这也是一个为了测 试而存在的插件。 下面就要介绍 pv 命令了。这个命令的作用,就是作实时的标准输入、标准输出监控。我们这 里就用它来监控标准输出: $ ./bin/logstash -f generator_dots.conf | pv -abt > /dev/null 2.2MiB 0:03:00 [12.5kiB/s] 可以很明显的看到在前几秒中,速度是 0 B/s,因为 JVM 还没启动起来呢。开始运行的时 候,速度依然不快。慢慢增长到比较稳定的状态,这时候的才是你需要的数据。 这里单位是 B/s,但是因为一个 event 就输出一个 . ,也就是 1B。所以 12.5kiB/s 就相当于 是 12.5k event/s。 注:如果你在 CentOS 上通过 yum 安装的 pv 命令,版本较低,可能还不支持 -a 参数。单纯 靠 -bt 参数看起来还是有点累的。 如果你要测试的是 input 插件的效率,方法也是类似的。此外,如果不想使用额外而且可能低 版本的 pv 命令,通过 logstash-filter-metric 插件也可以做到类似的效果,官方博客中对此有 详细阐述,建议大家阅读。 额外的话 既然单独花这么一节来说测试,这里想额外谈谈一个很常见的话题: ELK 的性能怎么样? 其实这压根就是一个不正确的提问。ELK 并不是一个软件而是一个并不耦合的套件。所以, 我们需要分拆开讨论这三个软件的性能如何?怎么优化? LogStash 的性能,是最让新人迷惑的地方。因为 LogStash 本身并不维护队列,所以整 个日志流转中任意环节的问题,都可能看起来像是 LogStash 的问题。这里,需要熟练使 用本节说的测试方法,针对自己的每一段配置,都确定其性能。另一方面,就是本书之 前提到过的,LogStash 给自己的线程都设置了单独的线程名称,你可以在 top -H 结果 中查看具体线程的负载情况。 ELKstack 中文指南 123generator方式 Elasticsearch 的性能。这里最需要强调的是:Elasticsearch 是一个分布式系统。从来没 有分布式系统要跟人比较单机处理能力的说法。所以,更需要关注的是:在确定的单机 处理能力的前提下,性能是否能做到线性扩展。当然,这不意味着说提高处理能力只能 靠加机器了——有效利用 mapping API 是非常重要的。不过暂时就在这里讲述了。 Kibana 的性能。通常来说,Kibana 只是一个单页 Web 应用,只需要 nginx 发布静态文 件即可,没什么性能问题。页面加载缓慢,基本上是因为 Elasticsearch 的请求响应时间 本身不够快导致的。不过一定要细究的话,也能找出点 Kibana 本身性能相关的话题:因 为 Kibana3 默认是连接固定的一个 ES 节点的 IP 端口的,所以这里会涉及一个浏览器的 同一 IP 并发连接数的限制。其次,就是 Kibana 用的 AngularJS 使用了 Promise.then 的 方式来处理 HTTP 请求响应。这是异步的。 ELKstack 中文指南 124generator方式 心跳检测 缺少内部队列状态的监控办法一直是 logstash 最为人诟病的一点。从 logstash-1.5.1 版开 始,新发布了一个 logstash-input-heartbeat 插件,实现了一个最基本的队列堵塞状态监控。 配置示例如下: input { heartbeat { message => "epoch" interval => 60 type => "heartbeat" add_field => { "zbxkey" => "logstash.heartbeat", "zbxhost" => "logstash_hostname" } } tcp { port => 5160 } } output { if [type] == "heartbeat" { file { path => "/data1/logstash-log/local6-5160-%{+YYYY.MM.dd}.log" } zabbix { zabbix_host => "zbxhost" zabbix_key => "zbxkey" zabbix_server_host => "zabbix.example.com" zabbix_value => "clock" } } else { elasticsearch { } } } 示例中,同时将数据输出到本地文件和 zabbix server。注意,logstash-output-zabbix 并不是 标准插件,需要额外安装: bin/plugin install logstash-output-zabbix 文件中记录的就是 heartbeat 事件的内容,示例如下: ELKstack 中文指南 125generator方式 {"clock":1435191129,"host":"logtes004.mweibo.bx.sinanode.com","@version":"1","@timestamp":"2015-06-25T00:12:09.042Z","type":"heartbeat","zbxkey":"logstash.heartbeat","zbxhost":"logstash_hostname"} 可以通过文件最后的 clock 和 @timestamp 内容,对比当前时间,来判断 logstash 内部队列 是否有堵塞。 ELKstack 中文指南 126generator方式 JMX 监控方式 Logstash 是一个运行在 JVM 上的软件,也就意味着 JMX 这种对 JVM 的通用监控方式对 Logstash 也是一样有效果的。要给 Logstash 启用 JMX,需要修改 ./bin/logstash.lib.sh 中 $JAVA_OPTS 变量的定义,或者在运行时设置 LS_JAVA_OPTS 环境变量。 在 ./bin/logstash.lib.sh 第 34 行 JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true" 下, 添加如下几行: JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote" JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=9010" JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.local.only=false" JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false" JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false" 重启 logstash 服务,JMX 配置即可生效。 有 JMX 以后,我们可以通过 jconsole 界面查看,也可以通过 zabbix 等监控系统做长期监 控。甚至 logstash 自己也有插件 logstash-input-jmx 来读取远程 JMX 数据。 zabbix 监控 zabbix 里提供了专门针对 JMX 的监控项。详 见:https://www.zabbix.com/documentation/2.2/manual/config/items/itemtypes/jmx_monitorin g 注意,zabbix-server 本身并不直接对 JMX 发起请求,而是单独有一个 Java Gateway 作为中 间代理层角色。zabbix-server 的 java poller 连接 zabbix-java-gateway,由 zabbix-java- gateway 去获取远程 JMX 信息。所以,在 zabbix-web 配置之前,需要先配置 zabbix server 相关进程和设置: # yum install zabbix-java-gateway # cat >> /etc/zabbix/zabbix-server.conf < BLPOP channel => SUBSCRIBE pattern_channel => PSUBSCRIBE 注意到了么?这里面没有 GET 命令! Redis 服务器通常都是用作 NoSQL 数据库,不过 logstash 只是用来做消息队列。所以不要担 心 logstash 里的 Redis 会撑爆你的内存和磁盘。 配置示例 input { redis { data_type => "pattern_channel" key => "logstash-*" host => "192.168.0.2" port => 6379 threads => 5 } } 使用方式 基本方法 首先确认你设置的 host 服务器上已经运行了 redis-server 服务,然后打开终端运行 logstash 进程等待输入数据,然后打开另一个终端,输入 redis-cli 命令(先安装好 redis 软件包),在 交互式提示符后面输入 PUBLISH logstash-demochan "hello world" : ELKstack 中文指南 130通过redis传输 # redis-cli 127.0.0.1:6379> PUBLISH logstash-demochan "hello world" 你会在第一个终端里看到 logstash 进程输出类似下面这样的内容: { "message" => "hello world", "@version" => "1", "@timestamp" => "2014-08-08T16:26:29.399Z" } 注意:这个事件里没有 host 字段!(或许这算是 bug……) 输入 JSON 数据 如果你想通过 redis 的频道给 logstash 事件添加更多字段,直接向频道发布 JSON 字符串就 可以了。 LogStash::Inputs::Redis 会直接把 JSON 转换成事件。 继续在第二个终端的交互式提示符下输入如下内容: 127.0.0.1:6379> PUBLISH logstash-chan '{"message":"hello world","@version":"1","@timestamp":"2014-08-08T16:34:21.865Z","host":"raochenlindeMacBook-Air.local","key1":"value1"}' 你会看到第一个终端里的 logstash 进程随即也返回新的内容,如下所示: { "message" => "hello world", "@version" => "1", "@timestamp" => "2014-08-09T00:34:21.865+08:00", "host" => "raochenlindeMacBook-Air.local", "key1" => "value1" } 看,新的字段出现了!现在,你可以要求开发工程师直接向你的 redis 频道发送信息好了,一 切自动搞定。 小贴士 这里我们建议的是使用 pattern_channel 作为输入插件的 data_type 设置值。因为实际使用 中,你的 redis 频道可能有很多不同的 keys,一般命名成 logstash-chan-%{type} 这样的形 式。这时候 pattern_channel 类型就可以帮助你一次订阅全部 logstash 相关频道! 扩展方式 ELKstack 中文指南 131通过redis传输 如上段"小贴士"提到的,之前两个使用场景采用了同样的配置,即数据类型为频道发布订阅方 式。这种方式在需要扩展 logstash 成多节点集群的时候,会出现一个问题:通过频道发布的 一条信息,会被所有订阅了该频道的 logstash 进程同时接收到,然后输出重复内容! 你可以尝试再做一次上面的实验,这次在两个终端同时启动 logstash -f redis-input.conf 进程,结果会是两个终端都输出消息。 这种时候,就需要用 list 类型。在这种类型下,数据输入到 redis 服务器上暂存,logstash 则 连上 redis 服务器取走 ( BLPOP 命令,所以只要 logstash 不堵塞,redis 服务器上也不会有数 据堆积占用空间)数据。 配置示例 input { redis { batch_count => 1 data_type => "list" key => "logstash-list" host => "192.168.0.2" port => 6379 threads => 5 } } 使用方式 这次我们同时在两个终端运行 logstash -f redis-input-list.conf 进程。然后在第三个终端 里启动 redis-cli 命令交互: $ redis-cli 127.0.0.1:6379> RPUSH logstash-list "hello world" (integer) 1 这时候你可以看到,只有一个终端输出了结果。 连续 RPUSH 几次,可以看到两个终端近乎各自输出一半条目。 小贴士 RPUSH 支持 batch 方式,修改 logstash 配置中的 batch_count 值,作为示例这里只改到 2,实际运用中可以更大(事实上 LogStash::Outputs::Redis 对应这点的 batch_event 配置默 认值就是 50)。 重启 logstash 进程后,redis-cli 命令中改成如下发送: ELKstack 中文指南 132通过redis传输 127.0.0.1:6379> RPUSH logstash-list "hello world" "hello world" "hello world" "hello world" "hello world" "hello world" (integer) 3 可以看到,两个终端也各自输出一部分结果。而你只用了一次 RPUSH 命令。 输出到 Redis 配置示例 input { stdin {} } output { redis { data_type => "channel" key => "logstash-chan-%{+yyyy.MM.dd}" } } 使用方式 我们还是继续先用 redis-cli 命令行来演示 outputs/redis 插件的实质。 基础方式 运行 logstash 进程,然后另一个终端启动 redis-cli 命令。输入订阅指定频道的 Redis 命令 ("SUBSCRIBE logstash-chan-2014.08.08") 后,首先会看到一个订阅成功的返回信息。如下 所示: # redis-cli 127.0.0.1:6379> SUBSCRIBE logstash-chan-2014.08.08 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "logstash-chan-2014.08.08" 3) (integer) 1 好,在运行 logstash 的终端里输入 "hello world" 字符串。切换回 redis-cli 的终端,你发现已 经自动输出了一条信息: 1) "message" 2) "logstash-chan-2014.08.08" 3) "{\"message\":\"hello world\",\"@version\":\"1\",\"@timestamp\":\"2014-08-08T16:34:21.865Z\",\"host\":\"raochenlindeMacBook-Air.local\"}" ELKstack 中文指南 133通过redis传输 broker 方式 上面那条信息看起来是不是非常眼熟?这一串字符其实就是我们在前面"读取 Redis 中的数 据"小节中使用的那段数据。 看,这样就把 outputs/redis 和 inputs/redis 串联起来了吧! 事实上,这就是我们使用 redis 服务器作为 logstassh 架构中 broker 角色的原理。 让我们把这两节中不同配置的 logstash 进程分别在两个终端运行起来,这次不再要运行 redis-cli 命令了。在配有 outputs/redis 这端输入 "hello world",配有 "inputs/redis" 的终端 上,就自动输出数据了! 告警用途 我们还可以用其他程序来订阅 redis 频道,程序里就可以随意写其他逻辑了。你可以看看 output/juggernaut 插件的原理。这个 Juggernaut 就是基于 redis 服务器和 socket.io 框架构建 的。利用它,logstash 可以直接向 webkit 等支持 socket.io 的浏览器推送告警信息。 扩展方式 和 LogStash::Inputs::Redis 一样,这里也有设置成 list 的方式。使用 RPUSH 命令发送给 redis 服务器,效果和之前展示的完全一致。包括可以调整的参数 batch_event ,也在之前章 节中讲过。这里不再重复举例。 ELKstack 中文指南 134通过redis传输 通过kafka传输 本节作者:jingbli Kafka 是一个高吞吐量的分布式发布订阅日志服务,具有高可用、高性能、分布式、高扩展、 持久性等特性。目前已经在各大公司中广泛使用。和之前采用 Redis 做轻量级消息队列不 同,Kafka 利用磁盘作队列,所以也就无所谓消息缓冲时的磁盘问题。此外,如果公司内部已 有 Kafka 服务在运行,logstash 也可以快速接入,免去重复建设的麻烦。 如果打算新建 Kafka 系统的,请参考 Kafka 官方入门文 档:http://kafka.apache.org/documentation.html#quickstart kafka 基本概念 以下仅对相关基本概念说明,更多概念见官方文档: Topic 主题,声明一个主题,producer指定该主题发布消息,订阅该主题的consumer对 该主题进行消费 Partition 每个主题可以分为多个分区,每个分区对应磁盘上一个目录,分区可以分布在 不同broker上,producer在发布消息时,可以通过指定partition key映射到对应分区,然 后向该分区发布消息,在无partition key情况下,随机选取分区,一段时间内触发一次(比 如10分钟),这样就保证了同一个producer向同一partition发布的消息是顺序的。 消费者 消费时,可以指定partition进行消费,也可以使用high-level-consumer api,自动进行负载 均衡,并将partition分给consumer,一个partition只能被一个consumer进行消费。 Consumer 消费者,可以多实例部署,可以批量拉取,有两类API可供选择:一个 simpleConsumer,暴露所有的操作给用户,可以提交offset、fetch offset、指定partition fetch message;另外一个high-level-consumer(ZookeeperConsumerConnector),帮助 用户做基于partition自动分配的负载均衡,定期提交offset,建立消费队列等。 simpleConsumer相当于手动挡,high-level-consumer相当于自动挡。 simpleConsumer:无需像high-level-consumer那样向zk注册brokerid、owner,甚至不需 要提交offset到zk,可以将offset提交到任意地方比如(mysql,本地文件等)。 high-level-consumer:一个进程中可以启多个消费线程,一个消费线程即是一个 consumer,假设A进程里有2个线程(consumerid分别为1,2),B进程有2个线程 (consumerid分别为1,2),topic1的partition有5个,那么partition分配是这样的: ELKstack 中文指南 135通过kafka传输 partition1 ---> A进程consumerid1 partition2 ---> A进程consumerid1 partition3 ---> A进程consumerid2 partition4 ---> B进程consumer1 partition5 ---> B进程consumer2 Group High-level-consumer可以声明group,每个group可以有多个consumer,每group 各自管理各自的消费offset,各个不同group之间互不关联影响。 由于目前版本消费的offset、owner、group都是consumer自己通过zk管理,所以group对 于broker和producer并不关心,一些监控工具需要通过group来监控,simpleComsumer 无需声明group。 小提示 以上概念是 logstash 的 kafka 插件的必要参数,请理解阅读,对后续使用 kafka 插件有重要 作用。logstash-kafka-input 插件使用的是 High-level-consumer API 。 插件安装 logstash-1.4 安装 如果你使用的还是 1.4 版本,需要自己单独安装 logstash-kafka 插件。插件地址 见:https://github.com/joekiller/logstash-kafka。 插件本身内容非常简单,其主要依赖同一作者写的 jruby-kafka 模块。需要注意的是:该模块 仅支持 Kafka-0.8 版本。如果是使用 0.7 版本 kafka 的,将无法直接使 jruby-kafka 该模块和 logstash-kafka 插件。 安装按照官方文档完全自动化的安装。或是可以通过以下方式手动自己安装插件,不过重点 注意的是 kafka 的版本,上面已经指出了。 1. 下载 logstash 并解压重命名为 ./logstash-1.4.0 文件目录。 2. 下载 kafka 相关组件,以下示例选的为 kafka_2.8.0-0.8.1.1-src,并解压重命名为 ./kafka_2.8.0-0.8.1.1 。 3. 从 releases 页下载 logstash-kafka v0.4.2 版,并解压重命名为 ./logstash-kafka- 0.4.2 。 4. 从 ./kafka_2.8.0-0.8.1.1/libs 目录下复制所有的 jar 文件拷贝到 ./logstash- 1.4.0/vendor/jar/kafka_2.8.0-0.8.1.1/libs 下,其中你需要创建 kafka_2.8.0- 0.8.1.1/libs 相关文件夹及目录。 5. 分别复制 ./logstash-kafka-0.4.2/logstash 里的 inputs 和 outputs 下的 kafka.rb , 拷贝到对应的 ./logstash-1.4.0/lib/logstash 里的 inputs 和 outputs 对应目录下。 ELKstack 中文指南 136通过kafka传输 6. 切换到 ./logstash-1.4.0 目录下,现在需要运行 logstash-kafka 的 gembag.rb 脚本去 安装 jruby-kafka 库,执行以下命令: GEM_HOME=vendor/bundle/jruby/1.9 GEM_PATH= java -jar vendor/jar/jruby-complete-1.7.11.jar --1.9 ../logstash-kafka-0.4.2/gembag.rb ../logstash-kafka-0.4.2/logstash-kafka.gemspec 。 7. 现在可以使用 logstash-kafka 插件运行 logstash 了。 logstash-1.5 安装 logstash 从 1.5 版本开始才集成了 Kafka 支持。1.5 版本开始所有插件的目录和命名都发生了 改变,插件发布地址见:https://github.com/logstash-plugins。 安装和更新插件都可以使用官 方提供的方式: $bin/plugin install OR $bin/plugin update 小贴士 对于插件的安装和更新,默认走的Gem源为 https://rubygems.org,对于咱们国内网络来说是出 奇的慢或是根本无法访问(爬梯子除外),在安装或是更新插件是,可以尝试修改目录下 Gemfile 文件中的 source 为淘宝源https://ruby.taobao.org,这样会使你的安装或是更新顺 畅很多。 插件配置 Input 配置示例 以下配置可以实现对 kafka 读取端(consumer)的基本使用。 消费端更多详细的配置请查看 http://kafka.apache.org/documentation.html#consumerconfigs kafka 官方文档的消费者部分配置文档。 input { kafka { zk_connect => "localhost:2181" group_id => "logstash" topic_id => "test" codec => plain reset_beginning => false # boolean (optional), default: false consumer_threads => 5 # number (optional), default: 1 decorate_events => true # boolean (optional), default: false } } ELKstack 中文指南 137通过kafka传输 Input 解释 作为 Consumer 端,插件使用的是 High-level-consumer API ,请结合上述 kafka 基本概念进 行设置: group_id 消费者分组,可以通过组 ID 去指定,不同的组之间消费是相互不受影响的,相互隔离。 topic_id 指定消费话题,也是必填项目,指定消费某个 topic ,这个其实就是订阅某个主题,然后去 消费。 reset_beginning logstash 启动后从什么位置开始读取数据,默认是结束位置,也就是说 logstash 进程会以从 上次读取结束时的偏移量开始继续读取,如果之前没有消费过,那么就开始从头读取.如果你 是要导入原有数据,把这个设定改成 "true", logstash 进程就从头开始读取.有点类似 cat ,但是读到最后一行不会终止,而是变成 tail -F ,继续监听相应数据。 decorate_events 在输出消息的时候会输出自身的信息包括:消费消息的大小, topic 来源以及 consumer 的 group 信息。 rebalance_max_retries 当有新的 consumer(logstash) 加入到同一 group 时,将会 reblance ,此后将会有 partitions 的消费端迁移到新的 consumer 上,如果一个 consumer 获得了某个 partition 的消费权限,那么它将会向 zookeeper 注册, Partition Owner registry 节点 信息,但是有可能此时旧的 consumer 尚没有释放此节点,此值用于控制,注册节点的重试 次数。 consumer_timeout_ms 指定时间内没有消息到达就抛出异常,一般不需要改。 以上是相对重要参数的使用示例,更多参数可以选项可以跟据 https://github.com/joekiller/logstash-kafka/blob/master/README.md 查看 input 默认参数。 注意 1.想要使用多个 logstash 端协同消费同一个 topic 的话,那么需要把两个或是多个 logstash 消费端配置成相同的 group_id 和 topic_id , 但是前提是要把相应的 topic 分多个 partitions (区),多个消费者消费是无法保证消息的消费顺序性的。 ELKstack 中文指南 138通过kafka传输 这里解释下,为什么要分多个 partitions(区), kafka 的消息模型是对 topic 分区以达到 分布式效果。每个 topic 下的不同的 partitions (区)只能有一个 Owner 去消费。所以 只有多个分区后才能启动多个消费者,对应不同的区去消费。其中协调消费部分是由 server 端协调而成。不必使用者考虑太多。只是消息的消费则是无序的。 总结:保证消息的顺序,那就用一个 partition。 kafka 的每个 partition 只能同时被同一个 group 中的一个 consumer 消费。 Output 配置 以下配置可以实现对 kafka 写入端 (producer) 的基本使用。 生产端更多详细的配置请查看 http://kafka.apache.org/documentation.html#producerconfigs kafka 官方文档的生产者部分配置文档。 output { kafka { broker_list => "localhost:9092" topic_id => "test" compression_codec => "snappy" # string (optional), one of ["none", "gzip", "snappy"], default: "none" } } Output 解释 作为 Producer 端使用,以下仅为重要概念解释,请结合上述 kafka 基本概念进行设置: compression_codec 消息的压缩模式,默认是 none,可以有 gzip 和 snappy (暂时还未测试开启压缩与不开启的 性能,数据传输大小等对比)。 compressed_topics 可以针对特定的 topic 进行压缩,设置这个参数为 topic ,表示此 topic 进行压缩。 request_required_acks 消息的确认模式: 可以设置为 0: 生产者不等待 broker 的回应,只管发送.会有最低能的延迟和最差的保证 性(在服务器失败后会导致信息丢失) 可以设置为 1: 生产者会收到 leader 的回应在 leader 写入之后.(在当前 leader 服务器为复制前失败可能会导致信息丢失) 可以设置为 -1: 生产 者会收到 leader 的回应在全部拷贝完成之后。 partitioner_class ELKstack 中文指南 139通过kafka传输 分区的策略,默认是 hash 取模 send_buffer_bytes socket 的缓存大小设置,其实就是缓冲区的大小 消息模式相关 serializer_class 消息体的系列化处理类,转化为字节流进行传输,请注意 encoder 必须和下面的 key_serializer_class 使用相同的类型。 key_serializer_class 默认的是与 serializer_class 相同 producer_type 生产者的类型 async 异步执行消息的发送 sync 同步执行消息的发送 queue_buffering_max_ms 异步模式下,那么就会在设置的时间缓存消息,并一次性发送 queue_buffering_max_messages 异步的模式下,最长等待的消息数 queue_enqueue_timeout_ms 异步模式下,进入队列的等待时间,若是设置为0,那么要么进入队列,要么直接抛弃 batch_num_messages 异步模式下,每次发送的最大消息数,前提是触发了 queue_buffering_max_messages 或是 queue_enqueue_timeout_ms 的限制 以上是相对重要参数的使用示例,更多参数可以选项可以跟据 https://github.com/joekiller/logstash-kafka/blob/master/README.md 查看 output 默认参数。 小贴士 logstash-kafka 插件输入和输出默认 codec 为 json 格式。在输入和输出的时候注意下编码格 式。消息传递过程中 logstash 默认会为消息编码内加入相应的时间戳和 hostname 等信息。 如果不想要以上信息(一般做消息转发的情况下),可以使用以下配置,例如: ELKstack 中文指南 140通过kafka传输 output { kafka { codec => plain { format => "%{message}" } } } 作为 Consumer 从kafka中读数据,如果为非 json 格式的话需要进行相关解码,例如: input { kafka { zk_connect => "xxx:xxx" group_id => "test" topic_id => "test-topic" codec => "line" ........... } } 性能 队列监控 其实 logstash 的 kafka 插件性能并不是很突出,可以通过使用以下命令查看队列积压消费情 况: $/bin/kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --group test 队列积压严重,性能跟不上的情况下,结合实际服务器资源,可以适当增加 topic 的 partition 多实例化 Consumer 进行消费处理消息。 input-kafka 的 JSON 序列化性能 此外,跟 logstash-input-syslog 改在 filter 阶段 grok 的优化手段类似,也可以将 logstash- input-kafka 的默认 JSON 序列化操作从 codec 阶段后移到 filter 阶段。如下: ELKstack 中文指南 141通过kafka传输 input { kafka { codec => plain } } filter { json { source => "message" } } 然后通过 bin/logstash -w $num_cpus 运行,利用多核计算优势,可以获得大约一倍左右的性 能提升。 其他方案 https://github.com/reachkrishnaraj/kafka-elasticsearch-standalone-consumer https://github.com/childe/hangout ELKstack 中文指南 142通过kafka传输 AIX 平台上的 Logstash-Forwarder-Java 本节作者:鹏程 在AIX环境下(IBM Power小型机的一种操作系统),你无法使用logstash(因为IBM JDK没有 实现相关方法),也无法使用logstash-forwarder,github上有个Logstash-forwarder再实现的 项目就是解决这个问题的:https://github.com/didfet/logstash-forwarder-java 配置和Logstash-forwarder基本一致,但是注意有一个坑是需要关注的,作者也在他的github 上提到了,就是: the ssl ca parameter points to a java keystore containing the root certificate of the server, not a PEM file 不熟悉证书相关体系的读者可能不太清楚这个意思,换句话说,如果你还按照logstash- forwarder的配置方法配置shipper端,那么你将会得到一个诡异的java.io.IOException: Invalid keystore format 异常。 首先介绍下背景知识,摘录一段知乎上的讲解: @刘长元 from http://www.zhihu.com/question/29620953 ELKstack 中文指南 143AIX 平台上的logstash-forwarder-java 把SSL系统比喻为工商局系统。 首先有SSL就有CA,certificate authority。证书局,用 于制作、认证证书的第三方机构,我们假设营业执照非常难制作,就像身份证一样,需 要有制证公司来提供,并且提供技术帮助工商局验证执照的真伪。然后CA是可以有多个 的,也就是可以有多个制证公司,但工商局就只有一个,它来说那个制证公司是可信 的,那些是假的,需要打击。在SSL的世界中,微软、Google和Mozilla扮演了一部分这 个角色。也就是说,IE、Chrome、Firefox中内置有一些CA,经过这些CA颁发,验证过 的证书都是可以信的,否则就会提示你不安全。 这也是为什么前几天Chrome决定屏蔽 CNNIC的CA时,CNNIC那么遗憾了。 也因为内置的CA是相对固定的,所以当你决定要 新建网站时,就需要购买这些内置CA颁发的证书来让用户看到你的域名前面是绿色的, 而不是红色。而这个最大的卖证书的公司就是VeriSign如果你听说过的话,当然它被卖给 了Symantec,这家伙不只出Ghost,还是个卖证书的公司。 要开店的老板去申请营业执照的时候是需要交他的身份证的,然后办出来的营业执照上 也会有他的照片和名字。身份证相当于私钥,营业执照就是证书,Ceritficate,.cer文 件。 然后关于私钥和公钥如何解释我没想好,而它们在数据加密层面,数据的流向是这样 的。 消息-->[公钥]-->加密后的信息-->[私钥]-->消息 公钥是可以随便扔给谁的,他把消息加了密传给我。对了,可以这样理解,我有一个箱 子,一把锁和一把钥匙,我把箱子和开着的锁给别人,他写了信放箱子里,锁上,然后 传递回我手上的途中谁都是打不开箱子的,只有我可以用原来的钥匙打开,这就是 SSL,公钥,私钥传递加密消息的方式。这里的密钥就是key文件。 于是我们就有了.cer和.key文件。接下来说keystore 不同的语言、工具序列SSL相关文件的格式和扩展名是不一样的。 其中Java系喜欢用 keystore, truststore来干活,你看它的名字,Store,仓库,它里面存放着key和信任的 CA,key和CA可以有多个。 这里的truststore就像你自己电脑的证书管理器一样,如果你 打开Chrome的设置,找到HTTP SSL,就可以看到里面有很多CA,truststore就是干这 个活儿的,它也里面也是存一个或多个CA让Tomcat或Java程序来调用。 而keystore就是 用来存密钥文件的,可以存放多个。 然后是PEM,它是由RFC1421至1424定义的一种数据格式。其实前面的.cert和.key文件 都是PEM格式的,只不过在有些系统中(比如Windows)会根据扩展名不同而做不同的 事。所以当你看到.pem文件时,它里面的内容可能是certificate也可能是key,也可能两 个都有,要看具体情况。可以通过openssl查看。 看到这儿你就应该懂了,按照logstash-forwarder-java的作者设计,此时你的shipper端ssl ca 这个域配置的应该是keystore,而不是PEM,因此需要从你生成的crt中创建出 keystore(jks)文件,方法为: ELKstack 中文指南 144AIX 平台上的logstash-forwarder-java keytool -importcert -trustcacerts -file logstash-forwarder.crt -alias ca -keystore keystore.jks 一个示例的shipper.conf为: { "network": { "servers": [ "192.168.1.1:5043"], "ssl ca": "/mnt/disk12/logger/logstash/config/keystore.jks" }, "files": [ { "paths": [ "/mnt/disk12/logger/logstash/config/2.txt" ], "fields": { "type": "sadb" } } ] } 注意:server可以配置多个,这样agent如果一个logstash连接不上可以连接另外的。 其余配置信息,请参考logstash-forwarder,它完全兼容。主要包括下面几个可用配置项: network.servers: 用来指定远端(即 logstash indexer 角色)服务器的 IP 地址和端口。这里 可以写数组,但是 logstash-forwarder 只会随机选一台作为对端发送数据,一直到对端故 障,才会重选其他服务器。 network.ssl*: 网络交互中使用的 SSL 证书路径。 files.*.paths: 读取的文件路径。 logstash-forwarder 只支持两种输入,一种就是示例中用 的文件方式,和 logstash 一样也支持 glob 路径,即 "/var/log/*.log" 这样的写法;一 种是标准输入,写法为 "paths": [ "-" ] files.*.fields: 给每条日志添加的固定字段,相当于 logstash 里的 add_field 参数。 配置好以后启动它即可: nohup java -jar logstash-forwarder-java-0.2.3-SNAPSHOT.jar -quiet -config logforwarder.conf > logforwarder.log & -quiet 参数可以大大减少不必要的日志量,如果遇到错误请打开-debug和-trace选项,得到相 关信息后查证,未果时请转向该项目github的issue区,作者很热心 测试通过环境: AIX版本 6100-04-06-1034 Java版本 ELKstack 中文指南 145AIX 平台上的logstash-forwarder-java java version "1.6.0" Java(TM) SE Runtime Environment (build pap6460sr14- 20130705_01(SR14)) IBM J9 VM (build 2.4, JRE 1.6.0 IBM J9 2.4 AIX ppc64-64 jvmap6460sr14-20130704_155156 (JIT enabled, AOT enabled) J9VM - 20130704_155156 JIT - r9_20130517_38390 GC - GA24_Java6_SR14_20130704_1138_B155156) JCL - 20130618_01 ELKstack 中文指南 146AIX 平台上的logstash-forwarder-java Rsyslog Rsyslog 是 RHEL6 开始的默认系统 syslog 应用软件(当然,RHEL 自带的版本较低,实际官 方稳定版本已经到 v8 了)。官网地址:http://www.rsyslog.com 目前 Rsyslog 本身也支持多种输入输出方式,内部逻辑判断和模板处理。 常用模块介绍 不同模块插件在 rsyslog 流程中发挥作用的原理,可以阅 读:http://www.rsyslog.com/doc/master/configuration/modules/workflow.html 流程中可以使用 mmnormalize 组件来完成数据的切分(相当于 logstash 的 filters/grok 功能)。 rsyslog 从 v7 版本开始带有 omelasticsearch 插件可以直接写入数据到 elasticsearch 集群, 配合 mmnormalize 的使用示例见: http://puppetlabs.com/blog/use-rsyslog-and-elasticsearch- powerful-log-aggregation ELKstack 中文指南 147rsyslog 而 normalize 语法说明见: http://www.liblognorm.com/files/manual/index.html? sampledatabase.htm 类似的还有 mmfields 和 mmjsonparse 组件。注意,mmjsonparse 要求被解析的 MSG 必须 以 @CEE: 开头,解析之后的字符串为 JSON。使用示例 见:http://blog.sematext.com/2013/05/28/structured-logging-with-rsyslog-and-elasticsearch/ 此外,rsyslog 从 v6 版本开始,设计了一套 rainerscript 作为配置中的 DSL。利用 rainerscript 中的函数,也可以做到一些数据解析和逻辑判断: tolower cstr cnum wrap replace field re_extract re_match contains if-else foreach lookup set/reset/unset 详细说明见:http://www.rsyslog.com/doc/v8-stable/rainerscript/functions.html rsyslog 与 logstash 合作 虽然 Rsyslog 很早就支持直接输出数据给 elasticsearch,但如果你使用的是 v8.4 以下的版 本,我们这里并不推荐这种方式。因为normalize 语法还是比较简单,只支持时间,字符串, 数字,ip 地址等几种。在复杂条件下远比不上完整的正则引擎。 那么,怎么使用 rsyslog 作为日志收集和传输组件,来配合 logstash 工作呢? 如果只是简单的 syslog 数据,直接单个 logstash 运行即可,配置方式见本书 2.4 章节。 如果你运行着一个高负荷运行的 rsyslog 系统,每秒传输的数据远大过单个 logstash 能处理 的能力,你可以运行多个 logstash 在多个端口,然后让 rsyslog 做轮训转发(事实上,单个 omfwd 本身的转发能力也有限,所以推荐这种做法): ELKstack 中文指南 148rsyslog Ruleset( name="forwardRuleSet" ) { Action ( type="mmsequence" mode="instance" from="0" to="4" var="$.seq" ) if $.seq == "0" then { action (type="omfwd" Target="127.0.0.1" Port="5140" Protocol="tcp" queue.size="150000" queue.dequeuebatchsize="2000" ) } if $.seq == "1" then { action (type="omfwd" Target="127.0.0.1" Port="5141" Protocol="tcp" queue.size="150000" queue.dequeuebatchsize="2000" ) } if $.seq == "2" then { action (type="omfwd" Target="127.0.0.1" Port="5142" Protocol="tcp" queue.size="150000" queue.dequeuebatchsize="2000" ) } if $.seq == "3" then { action (type="omfwd" Target="127.0.0.1" Port="5143" Protocol="tcp" queue.size="150000" queue.dequeuebatchsize="2000" ) } } 如果 rsyslog 仅是作为 shipper 角色运行,环境中有单独的消息队列可用,rsyslog 也有对应 的 omkafka, omredis, omzmq 插件可用。 rsyslog v8 版的 mmexternal 模块 如果你使用的是 v8.4 及以上版本的 rsyslog,其中有一个新加入的 mmexternal 模块。该模块 是在 v7 的 omprog 模块基础上发展出来的,可以让你使用任意脚本,接收标准输入,自行处 理以后再输出回来,而 rsyslog 接收到这个输出再进行下一步处理,这就解决了前面提到的 “normalize 语法太简单”的问题! 下面是使用 rsyslog 的 mmexternal 和 omelasticsearch 完成 Nginx 访问日志直接解析存储的 配置。 rsyslog 配置如下: ELKstack 中文指南 149rsyslog module(load="imuxsock" SysSock.RateLimit.Interval="0") module(load="mmexternal") module(load="omelasticsearch") template(name="logstash-index" type="list") { constant(value="logstash-") property(name="timereported" dateFormat="rfc3339" position.from="1" position.to="4") constant(value=".") property(name="timereported" dateFormat="rfc3339" position.from="6" position.to="7") constant(value=".") property(name="timereported" dateFormat="rfc3339" position.from="9" position.to="10") } template( name="nginx-log" type="string" string="%msg%\n" ) if ( $syslogfacility-text == 'local6' and $programname startswith 'wb-www-access-' and not ($msg contains '/2/remind/unread_count' or $msg contains '/2/remind/group_unread') ) then { action( type="mmexternal" binary="/usr/local/bin/rsyslog-nginx-elasticsearch.py" interface.input="fulljson" forcesingleinstance="on" ) action( type="omelasticsearch" template="nginx-log" server="eshost.example.com" bulkmode="on" dynSearchIndex="on" searchIndex="logstash-index" searchType="nginxaccess" queue.type="linkedlist" queue.size="50000" queue.dequeuebatchsize="5000" queue.dequeueslowdown="100000" ) stop } 其中调用的 python 脚本示例如下(注意只是做示例,脚本中的 split 功能其实可以用 rsyslog 的 mmfields 插件完成): #! /usr/bin/python import sys import json import datetime def nginxLog(data): hostname = data['hostname'] logline = data['msg'] time_local, http_x_up_calling_line_id, request, http_user_agent, staTus, remote_addr, http_x_log_uid, http_referer, request_time, body_bytes_sent, http_x_forwarded_proto, http_x_forwarded_for, request_uid, http_host, http_cookie, upstream_response_time = logline.split( try: upstream_response_time = float(upstream_response_time) except: upstream_response_time = None method, uri, verb = request.split(' ') arg = {} try: ELKstack 中文指南 150rsyslog url_path, url_args = uri.split('?') for args in url_args.split('&'): k, v = args.split('=') arg[k] = v except: url_path = uri # Why %z do not implement? ret = { "@timestamp": datetime.datetime.strptime(time_local, ' [%d/%b/%Y:%H:%M:%S +0800]' "host": hostname, "method": method.lstrip('"'), "url_path": url_path, "url_args": arg, "verb": verb.rstrip('"'), "http_x_up_calling_line_id": http_x_up_calling_line_id, "http_user_agent": http_user_agent, "status": int(staTus), "remote_addr": remote_addr.strip('[]'), "http_x_log_uid": http_x_log_uid, "http_referer": http_referer, "request_time": float(request_time), "body_bytes_sent": int(body_bytes_sent), "http_x_forwarded_proto": http_x_forwarded_proto, "http_x_forwarded_for": http_x_forwarded_for, "request_uid": request_uid, "http_host": http_host, "http_cookie": http_cookie, "upstream_response_time": upstream_response_time } return ret def onInit(): """ Do everything that is needed to initialize processing """ def onReceive(msg): data = json.loads(msg) ret = nginxLog(data) print json.dumps({'msg': ret}) def onExit(): """ Do everything that is needed to finish processing. This is being called immediately before exiting. """ # most often, nothing to do here onInit() keepRunning = 1 while keepRunning == 1: msg = sys.stdin.readline() if msg: msg = msg[:len(msg)-1] onReceive(msg) ELKstack 中文指南 151rsyslog sys.stdout.flush() else: keepRunning = 0 onExit() sys.stdout.flush() 注意输出的时候,顶层的 key 是不能变的,msg 还得叫 msg,如果是 hostname 还得叫 hostname ,等等。否则,rsyslog 会当做处理无效,直接传递原有数据内容给下一步。 慎用提示 mmexternal 是基于 direct mode 的,所以如果你发送的数据量较大时,rsyslog 并不会像 linkedlist mode 那样缓冲在磁盘队列上,而是持续 fork 出新的 mmexternal 程序,几千个进 程后,你的服务器就挂了!!所以,务必开启 forcesingleinstance 选项。 rsyslog 的 mmgrok 模块 Rsyslog 8.15.0 开始,附带了 mmgrok 模块,系笔者贡献。利用该模块,可以将 Logstash 的 Grok 规则,运用在 Rsyslog 中。欢迎有兴趣的读者试用。 ELKstack 中文指南 152rsyslog nxlog 本节作者:松涛 nxlog 是用 C 语言写的一个跨平台日志收集处理软件。其内部支持使用 Perl 正则和语法来进 行数据结构化和逻辑判断操作。不过,其最常用的场景。是在 windows 服务器上,作为 logstash 的替代品运行。 nxlog 的 windows 安装文件下载 url 见: http://nxlog.org/system/files/products/files/1/nxlog- ce-2.8.1248.msi 配置 Nxlog默认配置文件位置在: C:\Program Files (x86)\nxlog 。 配置文件中有3个关键设置,分别是:input(日志输入端)、output(日志输出端)、 route(绑定某输入到具体某输出)。。 例子 假设我们有两台服务器,收集其中的 windows 事务日志: logstash服务器ip地址:192.168.1.100 windows测试服务器ip地址:192.168.1.101 收集流程: 1. nxlog 使用模块 im_file 收集日志文件,开启位置记录功能 2. nxlog 使用模块tcp输出日志 3. logstash 使用input/tcp ,收集日志,输出至es Logstash配置文件 ELKstack 中文指南 153nxlog input { tcp { port => 514 } } output{ elasticsearch { host => "127.0.0.1" port => "9200" protocol => "http" } } Nxlog配置文件 Module im_file File "C:\\test\\\*.log" SavePos TRUE Module om_tcp Host 192.168.1.100 Port 514 Path testfile => out 配置文件修改完毕后,重启服务即可: ELKstack 中文指南 154nxlog ELKstack 中文指南 155nxlog Heka heka 是 Mozilla 公司仿造 logstash 设计,用 Golang 重写的一个开源项目。同样采用了input - > decoder -> filter -> encoder -> output 的流程概念。其特点在于,在中间的 decoder/filter/encoder 部分,设计了 sandbox 概念,可以采用内嵌 lua 脚本做这一部分的工 作,降低了全程使用静态 Golang 编写的难度。此外,其 filter 阶段还提供了一些监控和统计 报警功能。 官网地址见:http://hekad.readthedocs.org/ 下面是同样的处理逻辑,通过 syslog 接收 nginx 访问日志,解析并存储进 Elasticsearch, heka 配置文件如下: ELKstack 中文指南 156heka [hekad] maxprocs = 48 [TcpInput] address = ":514" parser_type = "token" decoder = "shipped-nginx-decoder" [shipped-nginx-decoder] type = "MultiDecoder" subs = ['RsyslogDecoder', 'nginx-access-decoder'] cascade_strategy = "all" log_sub_errors = true [RsyslogDecoder] type = "SandboxDecoder" filename = "lua_decoders/rsyslog.lua" [RsyslogDecoder.config] type = "nginx.access" template = '<%pri%>%TIMESTAMP% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n' tz = "Asia/Shanghai" [nginx-access-decoder] type = "SandboxDecoder" filename = "lua_decoders/nginx_access.lua" [nginx-access-decoder.config] type = "combined" user_agent_transform = true log_format = '[$time_local]`$http_x_up_calling_line_id`"$request"`"$http_user_agent"`$staTus`[$remote_addr]`$http_x_log_uid`"$http_referer"`$request_time`$body_bytes_sent`$http_x_forwarded_proto`$http_x_forwarded_for`$request_uid`$http_host`$http_cookie`$upstream_response_time' [ESLogstashV0Encoder] es_index_from_timestamp = true fields = ["Timestamp", "Payload", "Hostname", "Fields"] type_name = "%{Type}" [ElasticSearchOutput] message_matcher = "Type == 'nginx.access'" server = "http://eshost.example.com:9200" encoder = "ESLogstashV0Encoder" flush_interval = 50 flush_count = 5000 heka 目前仿造的还是旧版本的 logstash schema 设计,所有切分字段都存储在 @fields 下。 经测试,其处理性能跟开启了多线程 filters 的 logstash 进程类似,都在每秒 30000 条。 ELKstack 中文指南 157heka fluentd Fluentd 是另一个 Ruby 语言编写的日志收集系统。和 Logstash 不同的是,Fluentd 是基于 MRI 实现的,并不是利用多线程,而是利用事件驱动。 Fluentd 的开发和使用者,大多集中在日本。 配置示例 Fluentd 受 Scribe 影响颇深,包括节点间传输采用磁盘 buffer 来保证数据不丢失等的设计, 也包括配置语法。下面是一段配置示例: type tail read_from_head true path /var/lib/docker/containers/*/*-json.log pos_file /var/log/fluentd-docker.pos time_format %Y-%m-%dT%H:%M:%S tag docker.* format json # Using filter to add container IDs to each event type record_transformer container_id ${tag_parts[5]} type copy # for debug (see /var/log/td-agent.log) type stdout type elasticsearch logstash_format true host "#{ENV['ES_PORT_9200_TCP_ADDR']}" # dynamically configured to use Docker's link feature port 9200 flush_interval 5s ELKstack 中文指南 158fluent 注意,虽然示例中演示的是 tail 方式。Fluentd 对应用日志,并不推荐如此读取。FLuentd 为 各种编程语言提供了客户端库,应用可以直接加载日志库发送日志。下面是一个 PHP 应用的 示例: post("fluentd.test.follow", array("from"=>"userA", "to"=>"userB")); Fluentd 使用如下配置接收即可: type unix path /var/run/td-agent/td-agent.sock type forward send_timeout 60s recover_wait 10s heartbeat_interval 1s phi_threshold 16 hard_timeout 60s name myserver1 host 192.168.1.3 port 24224 weight 60 name myserver2 host 192.168.1.4 port 24224 weight 60 type file path /var/log/fluent/forward-failed fluentd 插件 作为用动态语言编写的软件,fluentd 也拥有大量插件。每个插件都以 RubyGem 形式独立存 在。事实上,logstash 在这方面就是学习 fluentd 的。安装方式如下: ELKstack 中文指南 159fluent /usr/sbin/td-agent-gem install fluent-plugin-elasticsearch fluent-plugin-grok_parser fluentd 插件列表,见:http://www.fluentd.org/plugins。 ELKstack 中文指南 160fluent Message::Passing Message::Passing 是一个仿造 Logstash 写的 Perl5 项目。项目早期甚至就直接照原样也叫 "Logstash" 的名字。 但实际上,Message::Passing 内部原理设计还是有所偏差的。这个项目整个基于 AnyEvent 事件驱动开发框架(即著名的 libev 库)完成,也要求所有插件不要采取阻塞式编程。所以,虽 然项目开发不太活跃,插件接口不甚完善,但是性能方面却非常棒。这也是我从多个 Perl 日 志处理框架中选择介绍这个的原因。 Message::Passing 有比较全的 input 和 output 插件,这意味着它可以通过多种协议跟 logstash 混跑,不过 filter 插件比较缺乏。对等于 grok 的插件叫 Message::Passing::Filter::Regexp ( 我写的,嘿嘿)。下面是一个完整的配置示例: ELKstack 中文指南 161Message::Passing use Message::Passing::DSL; run_message_server message_chain { output stdout => ( class => 'STDOUT', ); output elasticsearch => ( class => 'ElasticSearch', elasticsearch_servers => ['127.0.0.1:9200'], ); encoder("encoder", class => 'JSON', output_to => 'stdout', output_to => 'es', ); filter regexp => ( class => 'Regexp', format => ':nginxaccesslog', capture => [qw( ts status remotehost url oh responsetime upstreamtime bytes )] output_to => 'encoder', ); filter logstash => ( class => 'ToLogstash', output_to => 'regexp', ); decoder decoder => ( class => 'JSON', output_to => 'logstash', ); input file => ( class => 'FileTail', output_to => 'decoder', ); }; ELKstack 中文指南 162Message::Passing 源码解析 Logstash 和过去很多日志收集系统比,优势就在于其源码是用 Ruby 写的,所以插件开发相 当容易。现在已经有两百多个插件可供选择。但是,随之而来的问题就是:大多数框架都用 Java 写,毕竟做大规模系统 Java 有天生优势。而另一个新生代 fluentd 则是标准的 Ruby 产 品(即 Matz's Ruby Interpreter)。logstash 为什么选用 JRuby 来实现,似乎有点两头不讨好 啊? 乔丹西塞曾经多次著文聊过这个问题。为了避凑字数的嫌,这里罗列他的 gist 地址: Time sucks 一文是关于 Time 对象的性能测试,最快的生成方法是 sprintf 方法,MRI 性能为 82600 call/sec,JRuby1.6.7 为 131000 call/sec,而 JRuby1.7.0 为 215000 call/sec。 Comparing egexp patterns speeds 一文是关于正则表达式的性能测试,使用的正则统一 为 (?-mix:('(?:[^\\']+|(?:\\.)+)*')) ,结果 MRI1.9.2 为 530000 matches/sec,而 JRuby1.6.5 为 690000 matches/sec。 Logstash performance under ruby一文是关于 logstash 本身数据流转性能的测试,使用 inputs/generator 插件生成数据,outputs/stdout 到 pv 工具记点统计。结果 MRI1.9.3 为 4000 events/sec,而 JRuby1.7.0 为 25000 events/sec。 可能你已经运行着 logstash 并发现自己的线上数据远超过这个测试——这是因为乔丹西塞在 2013年之前,一直是业余时间开发 logstash,而且从未用在自己线上过。所以当时的很多测 试是在他自己电脑上完成的。 在 logstash 得到大家强烈关注后,作者发表了《logstash needs full time love》,表明了这点 并求一份可以让自己全职开发 logstash 的工作,同时列出了1.1.0 版本以后的 roadmap。 (不过事实证明当时作者列出来的这些需求其实不紧急,因为大多数,或者说除了 kibana 以 外,至今依然没有==!) 时间轴继续向前推,到 2011 年,你会发现 logstash 原先其实也是用 MRI1.8.7 写的!在 grok 模块从 C 扩展改写成 FFI 扩展后,才正式改用 JRuby。 切换语言的当时,乔丹西塞发表了《logstash, why jruby?》大家可以一读。 事实上,时至今日,多种 Ruby 实现的痕迹(到处都有 RUBY_ENGINE 变量判断)依然遍布 logstash 代码各处,作者也力图保证尽可能多的代码能在 MRI 上运行。 作为简单的提示,在和插件无关的核心代码中,只有 LogStash::Event 里生成 @timestamp 字 段时用了 Java 的 joda 库为 JRuby 仅有的。稍微修改成 Ruby 自带的 Time 库,即可在 MRI 上运行起来。而主要插件中,也只有 filters/date 和 outputs/elasticsearch 是 Java 相关的。 ELKstack 中文指南 163源码解析 另一个温馨预警,Logstash 被 Elastic.co 收购以后,又有另一种讨论中的发展方向,就是把 logstash 的 core 部分代码,尽量的 JVM 通用化,未来,可以用 JRuby,Jython、Scala, Groovy,Clojure 和 Java 等任意 JVM 平台语言写 Logstash 插件。 这就是开源软件的多样性未来,让我们拭目以待吧~ ELKstack 中文指南 164源码解析 pipeline 在一开始,就介绍过,Logstash 对日志的处理,从 input 到 output,就像在 Linux 命令行上 的管道操作一样。事实上,在 Logstash 中,对此有一个专门的名词,叫 Pipeline。 Pipeline 的代码加载路径如下: bin/logstash -> lib/logstash/runner.rb -> lib/logstash/agent.rb -> lib/logstash/pipeline.rb Logstash 2.2 版对 pipeline 做了大幅重构,最新版的 pipeline.rb ,可以归纳成下面这么一 段缩略版的代码: ELKstack 中文指南 165pipeline @config = grammar.parse(configstr) code = @config.compile eval(code) @input_queue = LogStash::Util::WrappedSynchronousQueue.new LogStash::Util.set_thread_name("[#{pipeline_id}]-pipeline-manager") @inputs.each do |input| input.register @input_threads << Thread.new do LogStash::Util::set_thread_name("[#{pipeline_id}]<#{input.class.config_name}") plugin.run(@input_queue) end end @outputs.each {|o| o.register } @filters.each {|f| f.register } @settings[:pipeline_workers].times do |t| @worker_threads << Thread.new do LogStash::Util.set_thread_name("[#{pipeline_id}]>worker#{t}") while true input_batch = [] batch_size.times do |t| event = (t == 0) ? @input_queue.take : @input_queue.poll(batch_delay) input_batch << event end input_batch.reduce([]) do |acc,e| filtered = filter_func(e) filtered.each {|fe| acc << fe unless fe.cancelled?} acc end .reduce(Hash.new { |h, k| h[k] = [] }) do |acc, event| outputs_for_event = output_func(event) || [] outputs_for_event.each { |output| acc[output] << event } acc end .each { |output, events| output.multi_receive(events) } end end end 整个缩略版,可以了解到一个关键信息,对我们理解 Logstash 原理是最有用 的: @input_queue 是一个固定大小为 0 的多线程同步队列。filter 和 output 插件,则在相同 的 pipeline_worker 线程中运行,该线程每次批量获取数据,也批量传递给 filter 和 output 插件。 由于 input 到 filter 之间有唯一的队列,任意一个 filter 或者 output 发生堵塞,都会一直堵塞到 最前端的接收。这也是 logstash-input-heartbeat 的理论基础。 ELKstack 中文指南 166pipeline 注:2.2 版这种改造,导致 logstash-output-elasticsearch 的 ESClient 数量比过去大幅增加, 对写入 Elasticsearch 的性能是不利的。目前官方已经意识到这个问题,正在实现一个多线程 共享的 ESClient 对象。在此之前,建议大家谨慎使用。 ELKstack 中文指南 167pipeline plugin input 中的 codec 上一节大家可能注意到了,整个 pipeline 非常简单,无非就是一个多线程的线程间数据读 写。但是,之前介绍的 codec 在哪里?我们可以看到 filter 阶段的 pop 操作,但是 Event 又 是怎么进 @input_to_filter 的呢?这两个问题,并不在 pipeline 中完成,而是 plugin 中。 Logstash 从 1.5 开始,把各个 plugin 拆分成了单独的 gem,主代码里只留下了几个 base.rb 类。所以,要了解详细情况,我们需要阅读一个实际跑数据的插件,比如 vendor/bundle/jruby/1.9/gems/logstash-input-file-0.1.6/lib/logstash/inputs/file.rb 。 可以看到其中最关键的读取数据部分代码如下: hostname = Socket.gethostname @tail.subscribe do |path, line| @logger.debug? && @logger.debug("Received line", :path => path, :text => line) @codec.decode(line) do |event| decorate(event) event["host"] = hostname if !event.include?("host") event["path"] = path queue << event end end 这里两个关键函数: @codec.decode(line) 和 decorate(event) 。 @codec 在 base.rb 中默认为 plain,那么我们就继续看 vendor/bundle/jruby/1.9/gems/logstash-codec-plain-0.1.5/lib/logstash/codecs/plain.rb 的相 关部分: def register @converter = LogStash::Util::Charset.new(@charset) @converter.logger = @logger end public def decode(data) yield LogStash::Event.new("message" => @converter.convert(data)) end # def decode ELKstack 中文指南 168plugins 超简短。就是在这个 @codec.decode(line) 里,生成了 LogStash::Event 对象。那么,我们 通过 output { codec => rubydebug } 看到的除了 message 字段以外的那些数据,又是怎么 来的呢? 继续看 lib/logstash/event.rb 的内容: CHAR_PLUS = "+" TIMESTAMP = "@timestamp" VERSION = "@version" VERSION_ONE = "1" TIMESTAMP_FAILURE_TAG = "_timestampparsefailure" TIMESTAMP_FAILURE_FIELD = "_@timestamp" public def initialize(data = {}) @logger = Cabin::Channel.get(LogStash) @cancelled = false @data = data @accessors = LogStash::Util::Accessors.new(data) @data[VERSION] ||= VERSION_ONE @data[TIMESTAMP] = init_timestamp(@data[TIMESTAMP]) 现在就清楚了,这个特殊的 @timestamp 是在 event 对象初始化的时候加上的,其实现为: private def init_timestamp(o) begin timestamp = o ? LogStash::Timestamp.coerce(o) : LogStash::Timestamp.now 再看 lib/logstash/timestamp.rb 中的实现: class Timestamp def initialize(time = Time.new) @time = time.utc end def self.now Timestamp.new(::Time.now) end 这就是我们看到 Logstash 生成的事件总是 UTC 时区时间的原因。 至于如果一开始就传入了 @timestamp 数据的处理,则是这样: ELKstack 中文指南 169plugins JODA_ISO8601_PARSER = org.joda.time.format.ISODateTimeFormat.dateTimeParser UTC = org.joda.time.DateTimeZone.forID("UTC") def self.parse_iso8601(t) millis = JODA_ISO8601_PARSER.parseMillis(t) LogStash::Timestamp.at(millis / 1000, (millis % 1000) * 1000) def self.coerce(time) case time when String LogStash::Timestamp.parse_iso8601(time) 同样会利用 joda 库做一次解析,还是转换成 UTC 时区。 output 中的 worker 我们知道,logstash 中,命令行的 -w 参数是设置 filter 插件的线程数的,而在 output 插件 配置中,很多都另外有一个 workers 选项。这是从 lib/logstash/ouput/base.rb 中继承来 的。其在 pipeline 中,是通过这行 @outputs.each(&:worker_setup) 调用的。 worker_setup 函数实现如下: define_singleton_method(:handle, method(:handle_worker)) @worker_queue = SizedQueue.new(20) @worker_plugins = @workers.times.map { self.class.new(params.merge("workers" => 1, "codec" => @codec.clone)) } @worker_plugins.map.with_index do |plugin, i| Thread.new(original_params, @worker_queue) do |params, queue| LogStash::Util::set_thread_name(">#{self.class.config_name}.#{i}") plugin.register while true event = queue.pop plugin.handle(event) end end end 这个单例方法 handle_worker 的实现是: def handle_worker(event) @worker_queue.push(event) end 注意第一行,这里,对可以运行多线程,且确实配置了多线程的 output 插件,logstash 是给 每个多线程插件单独准备一个依然固定大小 20 的线程安全数组。然后,pipeline 从 @filter_to_output 拿到的数据,再推进单个插件的 @worker_queue 里,由多线程来获取。 ELKstack 中文指南 170plugins ELKstack 中文指南 171plugins 自己写一个插件 前面已经提过在运行 logstash-1.4.2 的时候,可以通过 --pluginpath 参数来加载自己写的插 件。那么,插件又该怎么写呢? 插件格式 一个标准的 logstash 输入插件格式如下: require 'logstash/namespace' require 'logstash/inputs/base' class LogStash::Inputs::MyPlugin < LogStash::Inputs::Base config_name 'myplugin' milestone 1 config :myoption_key, :validate => :string, :default => 'myoption_value' public def register end public def run(queue) end end 其中大多数语句在过滤器和输出阶段是共有的。 config_name 用来定义该插件写在 logstash 配置文件里的名字; milestone 标记该插件的开发里程碑,一般为1,2,3,如果不再维护的,标记为 0; config 可以定义很多个,即该插件在 logstash 配置文件中的可配置参数。logstash 很温 馨的提供了验证方法,确保接收的数据是你期望的数据类型; register logstash 在启动的时候运行的函数,一些需要常驻内存的数据,可以在这一步先 完成。比如对象初始化,filters/ruby 插件中的 init 语句等。 小贴士 milestone 级别在 3 以下的,logstash 默认为不足够稳定,会在启动阶段,读取到该插件的时 候,输出类似下面这样的一行提示信息,日志级别是 warn。这不代表运行出错!只是提示如 果用户碰到 bug,欢迎提供线索。 {:timestamp=>"2015-02-06T10:37:26.312000+0800", :message=>"Using milestone 2 input plugin 'file'. This plugin should be stable, but if you see strange behavior, please let us know! For more information on plugin milestones, see http://logstash.net/docs/1.4.2-modified/plugin-milestones", :level=>:warn} ELKstack 中文指南 172插件开发 插件的关键方法 输入插件独有的是 run 方法。在 run 方法中,必须实现一个长期运行的程序(最简单的就是 loop 指令)。然后在每次收到数据并处理成 event 之后,一定要调用 queue << event 语 句。一个输入流程就算是完成了。 而如果是过滤器插件,对应修改成: require 'logstash/filters/base' class LogStash::Filters::MyPlugin < LogStash::Filters::Base public def filter(event) end end 输出插件则是: require 'logstash/outputs/base' class LogStash::Outputs::MyPlugin < LogStash::Outputs::Base public def receive(event) end end 另外,为了在终止进程的时候不遗失数据,建议都实现如下这个方法,只要实现了,logstash 在 shutdown 的时候就会自动调用: public def teardown end 推荐阅读 Extending logstash Plugin Milestones 插件打包 Logstash 从 1.5.0-GA 版开始,对插件规范做了重大变更。放弃了 milestone 定义,去除了 - -pluginpath 命令行参数。统一改成 bin/plugin 管理的 rubygem 包插件。那么,我们自己 写的 Logstash 插件,也同样需要适应这个新规则,写完 Ruby 代码,还要打包成 gem 才能 使用。 ELKstack 中文指南 173插件开发 为了我们更方便的完成工作,logstash 针对 4 种插件形态提供了 4 个示例库,可以按照自己 所需克隆使用。比如要写一个 logstash-filter-mything 插件: git clone https://github.com/logstash-plugins/logstash-filter-example cd logstash-filter-example mv logstash-filter-example.gemspec logstash-filter-mything.gemspec mv lib/logstash/filters/example.rb lib/logstash/filters/mything.rb mv spec/filters/example_spec.rb spec/filters/mything_spec.rb 然后把代码写在 lib/logstash/filters/mything.rb 里即可。 代码部分完成。然后就是定义 gem 打包需要的额外文件和库依赖了。 目录中有两个文件, Gemfile 和 logstash-filter-mything.gemspec 。 Gemfile 文件就是标准格式,用来运行 bundler install 时下载 rubygems 包的。默认情况 下,最基础的内容是: source 'https://rubygems.org' # 国内建议改成 'http://ruby.taobao.org' gemspec gem "logstash", :github => "elastic/logstash", :branch => "1.5" gemspec 文件则是用来定义软件包本身规范,不单限于 rubygems。示例如下: Gem::Specification.new do |s| s.name = 'logstash-filter-mything' s.version = '1.1.0' s.licenses = ['Apache License (2.0)'] s.summary = "This mything filter is just for ELK Stack Guide example" s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program" s.authors = ["Chenryn"] s.email = 'chenlin7@staff.sina.com.cn' s.homepage = "http://kibana.logstash.es" s.require_paths = ["lib"] s.files = `find . -type f ! -wholename '*.svn*'`.split($\) s.test_files = s.files.grep(%r{^(test|spec|features)/}) s.metadata = { "logstash_plugin" => "true", "logstash_group" => "filter" } s.requirements << "jar 'org.elasticsearch:elasticsearch', '1.4.0'" s.add_runtime_dependency "jar-dependencies" s.add_runtime_dependency "logstash-core", '>= 1.4.0', '< 2.0.0' s.add_development_dependency 'logstash-devutils' end 其中: ELKstack 中文指南 174插件开发 s.version 就是 milestone 的替代品,0.1.x 相当于是 milestone 0;0.9.x 相当于是 milestone 2;1.x.x 相当于是 milestone 3。 s.file 默认写法是 git ls-files ,因为默认是 git 库,如果你本身采用了 svn,或者 cvs 库,都不要紧,只要命令列出的是你需要打包进去的文件即可。 s.metadata 是 logstash 的 plugin 命令在 install 的时候会提前 verify 的特殊信息,一定 要保留。 s.add_runtime_dependency 是定义插件依赖库的指令。如果有 jar 包依赖,则额外再加 s.requirements 。 好了,全部完毕。下面打包: gem build logstash-filter-mything.gemspec 运行完就会生成一个 logstash-filter-mything-1.1.0.gem 软件包,可以安装使用了。 ELKstack 中文指南 175插件开发 读取二进制文件(utmp) 本节作者:NERO 我们知道Linux系统有些日志不是以可读文本形式存在文件中,而是以二进制内容存放的。比 如utmp等。 Files插件在读取二进制文件和文本文件的区别在于没办法一行行处理内容,内容也不是直观 可见的。但是在其他处理逻辑是保持一致的,比如多文件支持、断点续传、内容监控等等, 区别在于bytes的截断和bytes的转换。 在Files的基础上,新增了个插件utmp,插件实现了对 二进制文件的读取和解析,实现类似于文本文件一行行输出内容,以供Filter做进一步分析。 utmp新增了两个配置项:struct_size和struct_format。 struct_size代表结构体的大小,number类型;struct_format为结构体的内存分布格式,string 类型。 配置实例 input { utmp { path => "/var/run/utmp" type => "syslog" start_position => "beginning" struct_size => 384 struct_format => "s2Ia32a4a32a256s2iI2I4a20" } } *小贴士:我们可以通过man utmp了解utmp的结构体声明。通过结构体声明我们能知道结构 体的内存分布情况。我们看 struct_format => "s2Ia32a4a32a256s2iI2I4a20"中的第一个s,s 代表short,后面的数字2代表short元素的个数,那么意思就是结构体的第一个和第二个元素 是short类型,各占连个2个字节。 以此类推。s2Ia32a4a32a256s2iI2I4a20总共代表384个字 节,与struct_size配置保持一致。"s2Ia32a4a32a256s2iI2I4a20"本质上是对应于ruby中的 unpack函数的参数。其中s、T、a、i等等含义参见ruby的unpack函数说明。 *需要注意的是,你必须了解当前操作系统的字节序是大端模式还是小端模式,对于数字类型 的结构体元素很有必要,比如对于i和I的选择。还有一个就是,结构体内存对齐的问题。实际 上utmp结构体第一个元素2字节,第二个元素是4字节。编译器在编译的时候会在第一个2字节 后插入2字节来补齐4字节,知道这点尤为重要,所以其实第二个short是无意义的,只是为了 内存对其才有补的。 ELKstack 中文指南 176utmp插件示例 输出 经过utmp插件的event,message字段形如"field1|field2|field3",以"|"分隔每个结构体元素。 可用Filter插件进一步去做解析。utmp本身只负责将二级制内容转换成可读的文本,不对文件 编码格式负责。 安装 插件地址:https://github.com/txyafx/logstash-input-utmp git clone https://github.com/txyafx/logstash-input-utmp cd logstash-input-utmp rm -rf .git vim Gemfile 修改插件绝对路径 git init git add . gem clean gem build logstash-input-utmp.gemspec 用logstash安装本地utmp插件 ELKstack 中文指南 177utmp插件示例 Beats 平台 Beats 平台是 Elastic.co 从 packetbeat 发展出来的数据收集器系统。beat 收集器可以直接写 入 Elasticsearch,也可以传输给 Logstash。其中抽象出来的 libbeat,提供了统一的数据发送 方法,输入配置解析,日志记录框架等功能。 也就是说,所有的 beat 工具,在配置上,除了 input 以外,在 output、shipper、logging、 run-options 上的配置规则都是完全一致的。 output 目前 beat 可以发送数据给 Elasticsearch、Logstash、File 和 Console 四个目的地址。 Elasticsearch beats 发送到 Elasticsearch 也是走 HTTP 接口。示例配置段如下: output: elasticsearch: hosts: ["http://localhost:9200", "https://onesslip:9200/path", "anotherip"] username: "user" password: "pwd" index: "topbeat" bulk_max_size: 20000 flush_interval: 5 tls: certificate_authorities: ["/etc/pki/root/ca.pem"] certificate: "/etc/pki/client/cert.pem" certificatekey: "/etc/pki/client/cert.key" hosts 中可以通过 URL 的不同形式,来表示 HTTP 还是 HTTPS,是否有添加代理层的 URL 路径等情况。 index 表示写入 Elasticsearch 时索引的前缀,比如示例即表示索引名为 topbeat- yyyy.MM.dd Logstash beat 写入 Logstash 时,会配合 Logstash-1.5 后新增的 metadata 特性。将 beat 名和 type 名 记录在 metadata 里。所以对应的 Logstash 配置应该是这样: ELKstack 中文指南 178Beats input { beats { port => 5044 } } output { elasticsearch { hosts => ["http://localhost:9200"] index => "%{[@metadata][beat]}-%{+YYYY.MM.dd}" document_type => "%{[@metadata][type]}" } } beat 示例配置段如下: output: logstash: hosts: ["localhost:5044", "localhost:5045"] worker: 2 loadbalance: true index: topbeat 这里 worker 的含义,是 beat 连到每个 host 的线程数。在 loadbalance 开启的情况下,意 味着有 4 个worker 轮训发送数据。 File output: file: path: "/tmp/topbeat" filename: topbeat rotate_every_kb: 1000 number_of_files: 7 Console output: console: pretty: true shipper shipper 部分是一些和网络拓扑相关的配置,就目前来说,大多数是 packetbeat 独有的。 ELKstack 中文指南 179Beats shipper: name: "my-shipper" tags: ["my-service", "hardware", "test"] ignore_outgoing: true refresh_topology_freq: 10 topology_expire: 15 geoip: paths: - "/usr/share/GeoIP/GeoLiteCity.dat" logging logging: level: warning to_files: true to_syslog: false files: path: /var/log/mybeat name: mybeat.log keepfiles: 7 run options runoptions: uid=501 gid=501 ELKstack 中文指南 180Beats filebeat filebeat 是基于原先 logstash-forwarder 的源码改造出来的。换句话说:filebeat 就是新版的 logstash-forwarder,也会是 ELK Stack 在 shipper 端的第一选择。 安装部署 deb: curl -L -O https://download.elastic.co/beats/filebeat/filebeat_1.0.1_amd64.deb sudo dpkg -i filebeat_1.0.1_amd64.deb rpm: curl -L -O https://download.elastic.co/beats/filebeat/filebeat-1.0.1-x86_64.rpm sudo rpm -vi filebeat-1.0.1-x86_64.rpm mac: curl -L -O https://download.elastic.co/beats/filebeat/filebeat-1.0.1-darwin.tgz tar xzvf filebeat-1.0.1-darwin.tgz ELKstack 中文指南 181filebeat win: 下载 https://download.elastic.co/beats/filebeat/filebeat-1.0.1-windows.zip 解压到 C:\Program Files 重命名 filebeat-1.0.1-windows 目录为 Filebeat 右键点击 PowerSHell 图标,选择『以管理员身份运行』 运行下列命令,将 Filebeat 安装成 windows 服务: PS > cd 'C:\Program Files\Filebeat' PS C:\Program Files\Filebeat> .\install-service-filebeat.ps1 注意 可能需要额外授予执行权限。命令为: PowerShell.exe -ExecutionPolicy RemoteSigned -File .\install-service-filebeat.ps1 . filebeat 配置 所有的 beats 组件在 output 方面的配置都是一致的,之前章节已经介绍过。这里只介绍 filebeat 在 input 段的配置,如下: ELKstack 中文指南 182filebeat filebeat: spool_size: 1024 # 最大可以攒够 1024 条数据一起发送出去 idle_timeout: "5s" # 否则每 5 秒钟也得发送一次 registry_file: ".filebeat" # 文件读取位置记录文件,会放在当前工作目录下。所以如果你换一个工作目录执行 filebeat 会导致重复传输! config_dir: "path/to/configs/contains/many/yaml" # 如果配置过长,可以通过目录加载方式拆分配置 prospectors: # 有相同配置参数的可以归类为一个 prospector - fields: ownfield: "mac" # 类似 logstash 的 add_fields paths: - /var/log/system.log # 指明读取文件的位置 - /var/log/wifi.log - document_type: "apache" # 定义写入 ES 时的 _type 值 ignore_older: "24h" # 超过 24 小时没更新内容的文件不再监听。在windows上另外有一个配置叫force_close_files,只要文件名一变化立刻关闭文件句柄,保证文件可以被删除,缺陷是可能会有日志还没读完 scan_frequency: "10s" # 每 10 秒钟扫描一次目录,更新通配符匹配上的文件列表 tail_files: false # 是否从文件末尾开始读取 harvester_buffer_size: 16384 # 实际读取文件时,每次读取 16384 字节 backoff: "1s" # 每 1 秒检测一次文件是否有新的一行内容需要读取 paths: - "/var/log/apache/*" # 可以使用通配符 - input_type: "stdin" # 除了 "log",还有 "stdin" output: ... 我们已完成了配置,当 sudo service filebeat start 之后,你就可以在 kibana 上看到你的 日志了。 字段 Filebeat 发送的日志,会包含以下字段: beat.hostname beat 运行的主机名 beat.name shipper 配置段设置的 name ,如果没设置,等于 beat.hostname @timestamp 读取到该行内容的时间 type 通过 document_type 设定的内容 input_type 来自 "log" 还是 "stdin" source 具体的文件名全路径 offset 该行日志的起始偏移量 message 日志内容 fields 添加的其他固定字段都存在这个对象里面 ELKstack 中文指南 183filebeat packetbeat 目前 packetbeat 支持的网络协议有:HTTP,MySQL,PostgreSQL,Redis,Thrift,DNS, MongoDB,Memcache。 对于很多 ELK Stack 新手来说,面对的很可能就是几种常用数据流,而书写 logstash 正则是 一个耗时耗力的重复劳动,文件落地本身又是多余操作,packetbeat 的运行方式,无疑是对 新手入门极大的帮助。 安装部署 packetbeat 同样有已经编译完成的软件包可以直接安装使用。需要注意的是,packetbeat 支 持不同的抓包方式,也就有不同的依赖。比如最通用的 pcap,就要求安装有 libpcap 包,pf_ring 就要求有 pfring 包,而在 windows 平台上,则需要下载安装 WinPcap 软件。 # yum install libpcap # rpm -ivh http://www.nmon.net/packages/rpm6/x86_64/PF_RING/pfring-6.1.1-83.x86_64.rpm # rpm -ivh https://download.elasticsearch.org/beats/packetbeat/packetbeat-1.0.0~Beta1-x86_64.rpm packetbeat 还附带了一个定制的 Elasticsearch 模板,要在正式使用前导入 ES 中。 # curl -XPUT 'http://localhost:9200/_template/packetbeat' -d@/etc/packetbeat/packetbeat.template.json 配置示例 通过 RPM 安装的 packetbeat 配置文件位于 /etc/packetbeat/packetbeat.yml 。其示例如下: interfaces: device: any type: af_packet snaplen: 65536 # 保证大过 MTU 即可,所以公网抓包的话可以改成 1514 buffer_size_mb: 30 # 该参数仅在 af_packet 时有效,表示在 linux 内核空间和用户空间之间开启多大共享内存,可以减低 CPU 消耗 with_vlans: false # 在生成抓包语句的时候,是否带上 vlan 标示。示例:"port 80 or port 3306 or (vlan and (port 80 or port 3306))" protocols: dns: ports: [53] send_request: false # 通用配置:是否将原始的 request 字段内容都发给 ES send_response: false # 通用配置:是否将原始的 response 字段内容都发给 ES transaction_timeout: "10s" # 通用配置:超过 10 秒的不再等待响应,直接认定为一个事务,发送给 ES ELKstack 中文指南 184packetbeat网络流量分析 include_additionals: false # DNS 专属配置:是否带上 dns.additionals 字段 include_authorities: false # DNS 专属配置:是否带上 dns.authority 字段 http: ports: [80, 8080] # 抓包的监听端口 hide_keywords: ["password","passwd"] # 对 GET 方法的请求参数,POST 方法的顶层参数里的指定关键信息脱敏。注意:如果你又开启了 send_request,request 字段里的并不会脱敏" redact_authorization: true # 对 header 中的 Authorization 和 Proxy-Authorization 内容做模糊处理。道理和上一条类似 send_headers: ["User-Agent"] # 只在 headers 对象里记录指定的 header 字段内容 send_all_headers: true # 在 headers 对象里记录所有 header 字段内容 include_body_for: ["text/html"] # 在开启 send_response 的前提下,只记录某些类别的响应体内容 split_cookie: true # 将 headers 对象里的 Cookie 或 Set-Cookie 字段内容再继续做 KV 切割 real_ip_header: "X-Forwarded-For" # 将 headers 对象里的指定字段内容设为 client_location 和 real_ip 字段 memcache: ports: [11211] parseunknown: false maxvalues: 0 maxbytespervalue: 100 udptransactiontimeout: 10000 mysql: ports: [3306] max_rows: 10 # 发送给 ES 的 SQL 内容最多为 10 行 max_row_length: 1024 # 发送给 ES 的 SQL 内容每行最长为 1024 字节 thrift: transport_type: socket # Thrift 传输类型,"socket" 或 "framed" protocol_type: binary idl_files: ["shared.thrift"] # 一般来说默认内置的 IDL 文件已经足够了 string_max_size: 200 # 参数或返回值中字符串的最大长度,超长的都截断掉 collection_max_size: 15 # Thrift 的 list, map, set, structure 结构中的元素个数上限,超过的元素丢弃 capture_reply: true # 为了节省资源,可以设置 false,只解析响应中的方法名称,其余信息丢弃 obfuscate_strings: true # 把所有方法参数、返回码、异常结构中的字符串都用 * 代替 drop_after_n_struct_fields: 500 # 在 packetbeat 结束整个事务之前,一个结构体最多能存多少个字段。该配置主要考虑节约内存 monogodb: max_docs: 10 # 设置 send_response 前提下,response 字段最多保存多少个文档。超长的都截断掉。不限则设为 0 max_doc_length: 5000 # response 字段里每个文档的最大字节数。超长截断。不限则设为 0 procs: enabled: true monitored: - process: mysqld cmdline_grep: mysqld - process: app # 设置发送给 ES 的数据中进程名字段的内容 cmdline_grep: gunicorn # 从 /proc//cmdline 中过滤实际进程名的字符串 output: ... 在 windows 上,网卡设备名称会比较长。所以 packetbeat 单独提供了一个参数: packetbeat -device ,返回整个可用网卡设备列表数组,你可以直接写数组下标来代表这个设备。比 如: device: 0 。 字段 ELKstack 中文指南 185packetbeat网络流量分析 @timestamp 事件时间 type 事件类型,比如 HTTP, MySQL, Redis, RUM 等 count 事件代表的实际事务数。也就是采样率的倒数 direction 事务流向。比如 "in" 或者 "out" status 事务状态。不同协议取值方式可能不一致 method 事务方法。对 HTTP 是 GET/POST/PUT 等,对 SQL 是 SELECT/UPDATE/INSERT 等 resource 事务关联的逻辑资源。对 HTTP 是 URL 路径的第一个层级,对 SQL 是表名 path 事务关联的路径。对 HTTP 是 URL 路径,对 SQL 是表名,对 KV 存储是 key query 人类可读的请求字符串。对 HTTP 是 GET /resource/path?key=value,对 SQL 是 SELECT id FROM table WHERE name=test params 请求参数。对 HTTP 就是 GET 或者 POST 的请求参数,对 Thrift 是 request 里 的参数 notes packetbeat 解析数据出错时的日志记录 其余根据采用协议不同,各自有自己的字段。 dashboard 效果 针对 packetbeat 自动识别的不同协议,packetbeat 还自带了几个预定义好的 Kibana dashboard 方便使用和查看。包括: Packetbeat Statistics: 针对 HTTP 和标准流量事件的性能统计仪表盘 Packetbeat Search: 用来搜索关键字的仪表盘 MySQL Performance: MySQL 性能分析仪表盘 PgSQL Performance: PgSQL 性能分析仪表盘 预定义仪表盘的导入方式如下: # git clone https://github.com/elastic/packetbeat-dashboards # cd packetbeat-dashboards # ./load.sh http://192.168.0.2:9200 ELKstack 中文指南 186packetbeat网络流量分析 效果如下: ELKstack 中文指南 187packetbeat网络流量分析 Kibana3 topology 其实在 Kibana4 推出之前,packetbeat 曾经自己 fork 了一个 Kibana3 的分支,并在此基础上 二次开发了一个专门用来展示网络拓扑结构的面板,叫 force panel。该特性至今依然只能运 行在 Kibana3 上。所以,需要网络拓扑展现的用户,还得继续使用 Kibana3。部署方式如 下: curl -L -O https://github.com/packetbeat/kibana/releases/download/v3.1.2-pb/kibana-3.1.2-packetbeat.tar.gz tar xzvf kibana-3.1.2-packetbeat.tar.gz curl -L -O https://download.elasticsearch.org/beats/packetbeat/packetbeat-dashboards-k3-1.0.0~Beta1.tar.gz tar xzvf packetbeat-dashboards-k3-1.0.0~Beta1.tar.gz cd packetbeat-dashboards-k3-1.0.0~Beta1/ ./load.sh 192.169.0.2 force panel 示例如下图。注意,force panel 用到的数据,其实质是对各来源 IP 分别请求目的 IP,对 ES 的计算量要求较大,并不适合在高流量高负载的条件下使用。 ELKstack 中文指南 188packetbeat网络流量分析 小贴士 pfring 抓包模式的原厂,ntop 公司,也有类似 packetbeat 的计划。ntopng/nProbe 除了储存 到 SQLite 以外,也开始支持存储到 Elasticsearch 中。不过它们推荐采用的 dashboard,是 Kibana3 的另一个 fork 分支,叫 Qbana。 ELKstack 中文指南 189packetbeat网络流量分析 有兴趣的读者可以参考 ntop 官方文档:http://www.ntop.org/ntopng/exploring-your-traffic- using-ntopng-with-elasticsearchkibana/ ELKstack 中文指南 190packetbeat网络流量分析 ELKstack 中文指南 191dockerbeat ELKstack 中文指南 192nginxbeat ELKstack 中文指南 193libbeat源码解析 This guide walks you through the steps for creating a new Elastic Beat. The Beats are a collection of lightweight daemons that collect operational data from your servers and ship it to Elasticsearch or Logstash. The common parts for all Beats are placed in the libbeat library, which contains packages for sending data to Elasticsearch and Logstash, for configuration file handling, for signal handling, for logging, and more. By using this common framework, you can ensure that all Beats behave consistently and that they are easy to package and run with common tools. Get Ready All Beats are written in Go, so having Go installed and knowing the basics are prerequisites for understanding this guide. But don’t worry if you aren’t a Go expert. Go is a relatively new language, and very few people are experts in it. In fact, several people learned Go by contributing to Packetbeat and libbeat, including the original Packetbeat authors. After you have installed Go and set up the GOPATH environment variable to point to your preferred workspace location, a simple way of getting the source code for Topbeat and libbeat and compiling them at the same time is to do: go get github.com/elastic/Topbeat In this guide, we use working examples from the Topbeat source code to demonstrate how to implement a Beat. Topbeat is similar to the top command line tool, but instead of printing the statistics to the screen, Topbeat periodically ships them to Elasticsearch for storage. You can use Topbeat as an example implementation for creating a new Beat. Just copy the source files and modify them as necessary for your Beat. Overview At the high level, a simple Beat like Topbeat has two main components: a component that collects the actual data, and a publisher that sends the data to the specified output, such as Elasticsearch or Logstash. The publisher is already implemented in libbeat, so you typically only have to worry about the logic specific to your Beat (the code that creates the event and sends it to the publisher). Libbeat also offers common services like configuration management, logging, daemonizing, and Windows service handling, and in the future, will offer data processing modules, such as filtering or sampling. ELKstack 中文指南 194beat开发示例 The event that you create is a JSON-like object (Golang type map[string]interface{} ) that contains the collected data to send to the publisher. At a minimum, the event object must contain a @timestamp field and a type field. Beyond that, events can contain any additional fields, and they can be created as often as necessary. The following example shows an event object in Topbeat: { count":1, "proc.cpu":{ "user":20, "percent":0.983284169124877, "system":0, "total":20, "start_time":"20:20" }, "proc.mem":{ "size":333772, "rss":6316, "share":2996 }, "proc.name":"topbeat", "proc.pid":13954, "proc.ppid":10027, "proc.state":"sleeping", "shipper":"vagrant-ubuntu-trusty-64", "@timestamp":"2015-08-06T20:20:34.089Z", "type":"proc" } Now that you have the big picture, let’s dig into the code. ELKstack 中文指南 195beat开发示例 Developing the Beat-Specific Code The Beat-specific code should implement the Beater interface defined in libbeat. type Beater interface { Config(*Beat) error Setup(*Beat) error Run(*Beat) error Cleanup(*Beat) error Stop() } This means your Beat should implement the following methods: Config Deals with the configuration file and optionally with custom CLI flags Setup Contains logic that executes before the main loop, usually for initialization Run Contains the main application loop that captures data and sends it to the publisher Cleanup Contains logic that executes after the main loop finishes (or is interrupted) Stop Contains logic that is called when the Beat is signaled to stop The Beat parameter received by most of these methods contains data about the Beat, such as the name, version, and common configuration options. Note: To be consistent with other Beats, you should append beat to your Beat name. Let’s go through each of the methods in the Beater interface and look at a sample implementation. Config Method The Config method deals with the configuration file and optionally with custom CLI flags. ELKstack 中文指南 196beat开发示例 The recommended way of handling the configuration is to create a ConfigSettings type that matches the structure of the expected configuration file. Here is an example configuration section for Topbeat: input: period: 1 procs: [".*"] And here are the corresponding Go structures, which are defined in config.go: type TopConfig struct { Period *int64 Procs *[]string } type ConfigSettings struct { Input TopConfig } Pointers are used to distinguish between when the setting is completely missing from the configuration file and when it has a value that matches the type’s default value. With these structures defined, the Config method looks like this: func (tb *Topbeat) Config(b *beat.Beat) error { err := cfgfile.Read(&tb.TbConfig, "") if err != nil { logp.Err("Error reading configuration file: %v", err) return err } if tb.TbConfig.Input.Period != nil { tb.period = time.Duration(*tb.TbConfig.Input.Period) * time.Second } else { tb.period = 1 * time.Second } [...] return nil } ELKstack 中文指南 197beat开发示例 Setup method The Setup method enables you to execute logic before the main loop, usually for initialization. In the Topbeat implementation, this method only assigns the Beat object to the Topbeat object, so it doesn’t have to be passed to all sub-functions. func (tb *Topbeat) Setup(b *beat.Beat) error { tb.Beat = b return nil } Run Method The Run method should contain your main application loop. For Topbeat it looks like this: func (t *Topbeat) Run(b *beat.Beat) error { t.isAlive = true t.initProcStats() var err error for t.isAlive { time.Sleep(t.period) err = t.exportSystemStats() if err != nil { logp.Err("Error reading system stats: %v", err) } [...] } return err } Inside the loop, Topbeat sleeps for a configurable period of time and then captures the required data and sends it to the publisher via the events publisher client. The publisher client is available as part of the Beat object through the Beat.Events variable. ELKstack 中文指南 198beat开发示例 The actual sending is done inside the exportSystemStats() method: func (t *Topbeat) exportSystemStats() error { load_stat, err := GetSystemLoad() if err != nil { logp.Warn("Getting load statistics: %v", err) return err } [...] event := common.MapStr{ "@timestamp": common.Time(time.Now()), "type": "system", "load": load_stat, "cpu": cpu_stat, "mem": mem_stat, "swap": swap_stat, } t.Beat.Events.PublishEvent(event) return nil } Cleanup Method The Cleanup method is executed after the main loop finishes (or is interrupted) and gives you the opportunity to release any resources you might use. For Topbeat, the method is completely empty: func (tb *Topbeat) Cleanup(b *beat.Beat) error { return nil } Stop Method The Stop method is called when the Beat is signaled to stop, for example through the SIGTERM signal on Unix systems or the service control interface on Windows. For Topbeat, this method simply sets isAlive to false, which breaks the main loop. ELKstack 中文指南 199beat开发示例 func (t *Topbeat) Stop() { t.isAlive = false } The main Function If you follow the Topbeat model and put your Beat-specific code in its own type that implements the Beater interface, the code from your main package is very simple: func main() { tb := &Topbeat{} b := beat.NewBeat(Name, Version, tb) b.CommandLineSetup() b.LoadConfig() tb.Config(b) b.Run() } ELKstack 中文指南 200beat开发示例 Elasticsearch 来源于作者 Shay Banon 的第一个开源项目 Compass 库,而这个 Java 库最初 的目的只是为了给 Shay 当时正在学厨师的妻子做一个菜谱的搜索引擎。2010 年, Elasticsearch 正式发布。至今已经成为 GitHub 上最流行的 Java 项目,不过 Shay 承诺给妻 子的菜谱搜索依然没有面世…… 2015 年初,Elasticsearch 公司召开了第一次全球用户大会 Elastic{ON}15。诸多 IT 巨头纷纷 赞助,参会,演讲。会后,Elasticsearch 公司宣布改名 Elastic,公司官网也变成 http://elastic.co/。这意味着 Elasticsearch 的发展方向,不再限于搜索业务,也就是说,ELK Stack 等机器数据和 IT 服务领域成为官方更加注意的方向。随后几个月,专注监控报警的 Watcher 发布 beta 版,社区有名的网络抓包工具 Packetbeat 也在前几天被 Elastic 公司收 购。 ELKstack 中文指南 201ElasticSearch 架构原理 本书作为 ELK Stack 指南,关注于 Elasticsearch 在日志和数据分析场景的应用,并不打算对 底层的 Lucene 原理或者 Java 编程做详细的介绍,但是 Elasticsearch 层面上的一些架构设 计,对我们做性能调优,故障处理,具有非常重要的影响。 所以,作为 ES 部分的起始章节,先从数据流向和分布的层面,介绍一下 ES 的工作原理,以 及相关的可控项。各位读者可以跳过这节先行阅读后面的运维操作部分,但作为性能调优的 基础知识,依然建议大家抽时间返回来了解。 ELKstack 中文指南 202架构原理 segment、buffer和translog对实时性的影响 既然介绍数据流向,首先第一步就是:写入的数据是如何变成 ES 里可以被检索和聚合的索引 内容的? 以单文件的静态层面看,每个全文索引都是一个词元的倒排索引,具体涉及到全文索引的通 用知识,这里不单独介绍,有兴趣的读者可以阅读《Lucene in Action》等书籍详细了解。 动态更新的 Lucene 索引 以在线动态服务的层面看,要做到实时更新条件下数据的可用和可靠,就需要在倒排索引的 基础上,再做一系列更高级的处理。 其实总结一下 Lucene 的处理办法,很简单,就是一句话:新收到的数据写到新的索引文件 里。 Lucene 把每次生成的倒排索引,叫做一个段(segment)。然后另外使用一个 commit 文件,记 录索引内所有的 segment。而生成 segment 的数据来源,则是内存中的 buffer。也就是说, 动态更新过程如下: 1. 当前索引有 3 个 segment 可用。索引状态如图 2-1; 图 2-1 ELKstack 中文指南 203segment、buffer和translog对实时性的影响 2. 新接收的数据进入内存 buffer。索引状态如图 2-2; 图 2-2 3. 内存 buffer 刷到磁盘,生成一个新的 segment,commit 文件同步更新。索引状态如图 2- 3。 图 2-3 ELKstack 中文指南 204segment、buffer和translog对实时性的影响 利用磁盘缓存实现的准实时检索 既然涉及到磁盘,那么一个不可避免的问题就来了:磁盘太慢了!对我们要求实时性很高的 服务来说,这种处理还不够。所以,在第 3 步的处理中,还有一个中间状态: 1. 内存 buffer 生成一个新的 segment,刷到文件系统缓存中,Lucene 即可检索这个新 segment。索引状态如图 2-4。 图 2-4 2. 文件系统缓存真正同步到磁盘上,commit 文件更新。达到图 2-3 中的状态。 这一步刷到文件系统缓存的步骤,在 ES 中,是默认设置为 1 秒间隔的,对于大多数应用来 说,几乎就相当于是实时可搜索了。ES 也提供了单独的 /_refresh 接口,用户如果对 1 秒 间隔还不满意的,可以主动调用该接口来保证搜索可见。 不过对于 ELK Stack 的日志场景来说,恰恰相反,我们并不需要如此高的实时性,而是需要 更快的写入性能。所以,一般来说,我们反而会通过 /_settings 接口或者定制 template 的 方式,加大 refresh_interval 参数: # curl -XPOST http://127.0.0.1:9200/logstash-2015.06.21/_settings -d' { "refresh_interval": "10s" } ' 如果是导入历史数据的场合,那甚至可以先完全关闭掉: ELKstack 中文指南 205segment、buffer和translog对实时性的影响 # curl -XPUT http://127.0.0.1:9200/logstash-2015.05.01 -d' { "settings" : { "refresh_interval": "-1" } }' 在导入完成以后,修改回来或者手动调用一次即可: # curl -XPOST http://127.0.0.1:9200/logstash-2015.05.01/_refresh translog 提供的磁盘同步控制 既然 refresh 只是写到文件系统缓存,那么第 4 步写到实际磁盘又是有什么来控制的?如果这 期间发生主机错误、硬件故障等异常情况,数据会不会丢失? 这里,其实有另一个机制来控制。ES 在把数据写入到内存 buffer 的同时,其实还另外记录了 一个 translog 日志。也就是说,第 2 步并不是图 2-2 的状态,而是像图 2-5 这样: 图 2-5 在第 3 和第 4 步,refresh 发生的时候,translog 日志文件依然保持原样,如图 2-6: ELKstack 中文指南 206segment、buffer和translog对实时性的影响 图 2-6 也就是说,如果在这期间发生异常,ES 会从 commit 位置开始,恢复整个 translog 文件中的 记录,保证数据一致性。 等到真正把 segment 刷到磁盘,且 commit 文件进行更新的时候, translog 文件才清空。这 一步,叫做 flush。同样,ES 也提供了 /_flush 接口。 对于 flush 操作,ES 默认设置为:每 30 分钟主动进行一次 flush,或者当 translog 文件大小 大于 512MB (老版本是 200MB)时,主动进行一次 flush。这两个行为,可以分别通过 index.translog.flush_threshold_period 和 index.translog.flush_threshold_size 参数修 改。 如果对这两种控制方式都不满意,ES 还可以通过 index.translog.flush_threshold_ops 参 数,控制每收到多少条数据后 flush 一次。 translog 的一致性 索引数据的一致性通过 translog 保证。那么 translog 文件自己呢? 默认情况下,ES 每 5 秒会强制刷新 translog 日志到磁盘上。所以,如果数据没有副本,然后 又发生故障,确实有可能丢失 5 秒数据。如果你很在意这个情况,首先记得开副本,其次, 调小 index.gateway.local.sync 设置,然后重启 ES 服务。 ES 分布式索引 ELKstack 中文指南 207segment、buffer和translog对实时性的影响 大家可能注意到了,前面一段内容,一直写的是"Lucene 索引"。这个区别在于,ES 为了完成 分布式系统,对一些名词概念作了变动。索引成为了整个集群级别的命名,而在单个主机上 的Lucene 索引,则被命名为分片(shard)。至于数据是怎么识别到自己应该在哪个分片,请阅 读稍后有关 routing 的章节。 ELKstack 中文指南 208segment、buffer和translog对实时性的影响 segment merge对写入性能的影响 通过上节内容,我们知道了数据怎么进入 ES 并且如何才能让数据更快的被检索使用。其中用 一句话概括了 Lucene 的设计思路就是"开新文件"。从另一个方面看,开新文件也会给服务器 带来负载压力。因为默认每 1 秒,都会有一个新文件产生,每个文件都需要有文件句柄,内 存,CPU 使用等各种资源。一天有 86400 秒,设想一下,每次请求要扫描一遍 86400 个文 件,这个响应性能绝对好不了! 为了解决这个问题,ES 会不断在后台运行任务,主动将这些零散的 segment 做数据归并, 尽量让索引内只保有少量的,每个都比较大的,segment 文件。这个过程是有独立的线程来 进行的,并不影响新 segment 的产生。归并过程中,索引状态如图 2-7,尚未完成的较大的 segment 是被排除在检索可见范围之外的: 图 2-7 当归并完成,较大的这个 segment 刷到磁盘后,commit 文件做出相应变更,删除之前几个小 segment,改成新的大 segment。等检索请求都从小 segment 转到大 segment 上以后,删除 没用的小 segment。这时候,索引里 segment 数量就下降了,状态如图 2-8 所示: ELKstack 中文指南 209segment merge对写入性能的影响 图 2-8 归并线程配置 segment 归并的过程,需要先读取 segment,归并计算,再写一遍 segment,最后还要保证 刷到磁盘。可以说,这是一个非常消耗磁盘 IO 和 CPU 的任务。所以,ES 提供了对归并线程 的限速机制,确保这个任务不会过分影响到其他任务。 默认情况下,归并线程的限速配置 indices.store.throttle.max_bytes_per_sec 是 20MB。对 于写入量较大,磁盘转速较高,甚至使用 SSD 盘的服务器来说,这个限速是明显过低的。对 于 ELK Stack 应用,建议可以适当调大到 100MB或者更高。 # curl -XPUT http://127.0.0.1:9200/_cluster/settings -d' { "persistent" : { "indices.store.throttle.max_bytes_per_sec" : "100mb" } }' 归并线程的数目,ES 也是有所控制的。默认数目的计算公式是: Math.min(3, Runtime.getRuntime().availableProcessors() / 2) 。即服务器 CPU 核数的一半大于 3 时,启 动 3 个归并线程;否则启动跟 CPU 核数的一半相等的线程数。相信一般做 ELK Stack 的服 务器 CPU 合数都会在 6 个以上。所以一般来说就是 3 个归并线程。如果你确定自己磁盘性能 跟不上,可以降低 index.merge.scheduler.max_thread_count 配置,免得 IO 情况更加恶化。 归并策略 ELKstack 中文指南 210segment merge对写入性能的影响 归并线程是按照一定的运行策略来挑选 segment 进行归并的。主要有以下几条: index.merge.policy.floor_segment 默认 2MB,小于这个大小的 segment,优先被归并。 index.merge.policy.max_merge_at_once 默认一次最多归并 10 个 segment index.merge.policy.max_merge_at_once_explicit 默认 optimize 时一次最多归并 30 个 segment。 index.merge.policy.max_merged_segment 默认 5 GB,大于这个大小的 segment,不用 参与归并。optimize 除外。 根据这段策略,其实我们也可以从另一个角度考虑如何减少 segment 归并的消耗以及提高响 应的办法:加大 flush 间隔,尽量让每次新生成的 segment 本身大小就比较大。 optimize 接口 既然默认的最大 segment 大小是 5GB。那么一个比较庞大的数据索引,就必然会有为数不少 的 segment 永远存在,这对文件句柄,内存等资源都是极大的浪费。但是由于归并任务太消 耗资源,所以一般不太选择加大 index.merge.policy.max_merged_segment 配置,而是在负载 较低的时间段,通过 optimize 接口,强制归并 segment。 # curl -XPOST http://127.0.0.1:9200/logstash-2015-06.10/_optimize?max_num_segments=1 由于 optimize 线程对资源的消耗比普通的归并线程大得多,所以,绝对不建议对还在写入数 据的热索引执行这个操作。这个问题对于 ELK Stack 来说非常好办,一般索引都是按天分割 的。更合适的任务定义方式,请阅读本书稍后的 curator 章节。 ELKstack 中文指南 211segment merge对写入性能的影响 routing和replica的读写过程 之前两节,完整介绍了在单个 Lucene 索引,即 ES 分片内的数据写入流程。现在彻底回到 ES 的分布式层面上来,当一个 ES 节点收到一条数据的写入请求时,它是如何确认这个数据 应该存储在哪个节点的哪个分片上的? 路由计算 作为一个没有额外依赖的简单的分布式方案,ES 在这个问题上同样选择了一个非常简洁的处 理方式,对任一条数据计算其对应分片的方式如下: shard = hash(routing) % number_of_primary_shards 每个数据都有一个 routing 参数,默认情况下,就使用其 _id 值。将其 _id 值计算哈希 后,对索引的主分片数取余,就是数据实际应该存储到的分片 ID。 由于取余这个计算,完全依赖于分母,所以导致 ES 索引有一个限制,索引的主分片数,不可 以随意修改。因为一旦主分片数不一样,所以数据的存储位置计算结果都会发生改变,索引 数据就完全不可读了。 副本一致性 作为分布式系统,数据副本可算是一个标配。ES 数据写入流程,自然也涉及到副本。在有副 本配置的情况下,数据从发向 ES 节点,到接到 ES 节点响应返回,流向如下(附图 2-9): 1. 客户端请求发送给 Node 1 节点,注意图中 Node 1 是 Master 节点,实际完全可以不 是。 2. Node 1 用数据的 _id 取余计算得到应该讲数据存储到 shard 0 上。通过 cluster state 信息发现 shard 0 的主分片已经分配到了 Node 3 上。Node 1 转发请求数据给 Node 3。 3. Node 3 完成请求数据的索引过程,存入主分片 0。然后并行转发数据给分配有 shard 0 的副本分片的 Node 1 和 Node 2。当收到任一节点汇报副本分片数据写入成功,Node 3 即返回给初始的接收节点 Node 1,宣布数据写入成功。Node 1 返回成功响应给客户 端。 ELKstack 中文指南 212routing和replica的读写过程 图 2-9 这个过程中,有几个参数可以用来控制或变更其行为: replication 通过在客户端发送请求的 URL 中加上 ?replication=async ,可以控制 Node 3 在完成本机主分片写入后,就返回给 Node 1 宣布写入成功。这个参数看似可以提高 ES 接收数据写入的性能,但事实上,由于 ES 的副本数据写入也是要经过完成索引过程 的,一旦由于发送过多数据,主机负载偏高导致某块数据写入有异常,可能整个主机的 CPU 都会飙高,导致分配到这台主机上其他主分片的数据都无法高性能完成,最终反而 拖累了整体的写入性能。 从 ES 1.6 版本开始,该参数已经被标记为废弃,2.0 版预计将 正式删除该参数。 consistency 上面示例中,2 个副本分片只要有 1 个成功,就可以返回给客户端了。这点 也是有配置项的。其默认值的计算来源如下: int( (primary + number_of_replicas) / 2 ) + 1 根据需要,也可以将参数设置为 one,表示仅写完主分片就返回,等同于 async;还可以设置 为 all,表示等所有副本分片都写完才能返回。 timeout 如果集群出现异常,有些分片当前不可用,ES 默认会等待 1 分钟看分片能否恢 复。可以使用 ?timeout=30s 参数来缩短这个等待时间。 副本配置和分片配置不一样,是可以随时调整的。有些较大的索引,甚至可以在做 optimize 前,先把副本全部取消掉,等 optimize 完后,再重新开启副本,节约单个 segment 的重复归 并消耗。 # curl -XPUT http://127.0.0.1:9200/logstash-mweibo-2015.05.02/_settings -d '{ "index": { "number_of_replicas" : 0 } }' ELKstack 中文指南 213routing和replica的读写过程 shard 的 allocate 控制 某个 shard 分配在哪个节点上,一般来说,是由 ES 自动决定的。以下几种情况会触发分配 动作: 1. 新索引生成 2. 索引的删除 3. 新增副本分片 4. 节点增减引发的数据均衡 ES 提供了一系列参数详细控制这部分逻辑: cluster.routing.allocation.enable 该参数用来控制允许分配哪种分片。默认是 all 。可选 项还包括 primaries 和 new_primaries 。 none 则彻底拒绝分片。该参数的作用,本书 稍后集群升级章节会有说明。 cluster.routing.allocation.allow_rebalance 该参数用来控制什么时候允许数据均衡。默认 是 indices_all_active ,即要求所有分片都正常启动成功以后,才可以进行数据均衡操 作,否则的话,在集群重启阶段,会浪费太多流量了。 cluster.routing.allocation.cluster_concurrent_rebalance 该参数用来控制集群内同时运行 的数据均衡任务个数。默认是 2 个。如果有节点增减,且集群负载压力不高的时候,可 以适当加大。 cluster.routing.allocation.node_initial_primaries_recoveries 该参数用来控制节点重启 时,允许同时恢复几个主分片。默认是 4 个。如果节点是多磁盘,且 IO 压力不大,可以 适当加大。 cluster.routing.allocation.node_concurrent_recoveries 该参数用来控制节点除了主分片 重启恢复以外其他情况下,允许同时运行的数据恢复任务。默认是 2 个。所以,节点重 启时,可以看到主分片迅速恢复完成,副本分片的恢复却很慢。除了副本分片本身数据 要通过网络复制以外,并发线程本身也减少了一半。当然,这种设置也是有道理的—— 主分片一定是本地恢复,副本分片却需要走网络,带宽是有限的。从 ES 1.6 开始,冷索 引的副本分片可以本地恢复,这个参数也就是可以适当加大了。 indices.recovery.concurrent_streams 该参数用来控制节点从网络复制恢复副本分片时的 数据流个数。默认是 3 个。可以配合上一条配置一起加大。 indices.recovery.max_bytes_per_sec 该参数用来控制节点恢复时的速率。默认是 40MB。显然是比较小的,建议加大。 此外,ES 还有一些其他的分片分配控制策略。比如以 tag 和 rack_id 作为区分等。一般 来说,ELK Stack 场景中使用不多。运维人员可能比较常见的策略有两种: 1. 磁盘限额 为了保护节点数据安全,ES 会定时( cluster.info.update.interval ,默认 30 秒)检查一下各节点的数据目录磁盘使用情况。在达到 ELKstack 中文指南 214shard的allocate控制 cluster.routing.allocation.disk.watermark.low (默认 85%)的时候,新索引分片就不会 再分配到这个节点上了。在达到 cluster.routing.allocation.disk.watermark.high (默认 90%)的时候,就会触发该节点现存分片的数据均衡,把数据挪到其他节点上去。这两个 值不但可以写百分比,还可以写具体的字节数。有些公司可能出于成本考虑,对磁盘使 用率有一定的要求,需要适当抬高这个配置: # curl -XPUT localhost:9200/_cluster/settings -d '{ "transient" : { "cluster.routing.allocation.disk.watermark.low" : "85%", "cluster.routing.allocation.disk.watermark.high" : "10gb", "cluster.info.update.interval" : "1m" } }' 1. 热索引分片不均 默认情况下,ES 集群的数据均衡策略是以各节点的分片总数 (indices_all_active)作为基准的。这对于搜索服务来说无疑是均衡搜索压力提高性能的好 办法。但是对于 ELK Stack 场景,一般压力集中在新索引的数据写入方面。正常运行的 时候,也没有问题。但是当集群扩容时,新加入集群的节点,分片总数远远低于其他节 点。这时候如果有新索引创建,ES 的默认策略会导致新索引的所有主分片几乎全分配在 这台新节点上。整个集群的写入压力,压在一个节点上,结果很可能是这个节点直接被 压死,集群出现异常。 所以,对于 ELK Stack 场景,强烈建议大家预先计算好索引的分 片数后,配置好单节点分片的限额。比如,一个 5 节点的集群,索引主分片 10 个,副本 1 份。则平均下来每个节点应该有 4 个分片,那么就配置: # curl -s -XPUT http://127.0.0.1:9200/logstash-2015.05.08/_settings -d '{ "index": { "routing.allocation.total_shards_per_node" : "5" } }' 注意,这里配置的是 5 而不是 4。因为我们需要预防有机器故障,分片发生迁移的情况。如 果写的是 4,那么分片迁移会失败。 reroute 接口 上面说的各种配置,都是从策略层面,控制分片分配的选择。在必要的时候,还可以通过 ES 的 reroute 接口,手动完成对分片的分配选择的控制。 reroute 接口支持三种指令:allocate,move 和 cancel。常用的一般是 allocate 和 move: allocate 指令 因为负载过高等原因,有时候个别分片可能长期处于 UNASSIGNED 状态,我们就可以手动 分配分片到指定节点上。默认情况下只允许手动分配副本分片,所以如果是主分片故障,需 要单独加一个 allow_primary 选项: ELKstack 中文指南 215shard的allocate控制 # curl -XPOST 127.0.0.1:9200/_cluster/reroute -d '{ "commands" : [ { "allocate" : { "index" : "logstash-2015.05.27", "shard" : 61, "node" : "10.19.0.77", "allow_primary" : true } } ] }' 注意,如果是历史数据的话,请提前确认一下哪个节点上保留有这个分片的实际目录,且目 录大小最大。然后手动分配到这个节点上。以此减少数据丢失。 move 指令 因为负载过高,磁盘利用率过高,服务器下线,更换磁盘等原因,可以会需要从节点上移走 部分分片: curl -XPOST 127.0.0.1:9200/_cluster/reroute -d '{ "commands" : [ { "move" : { "index" : "logstash-2015.05.22", "shard" : 0, "from_node" : "10.19.0.81", "to_node" : "10.19.0.104" } } ] }' 节点下线 集群中个别节点出现故障预警等情况,需要下线,也是 Elasticsearch 运维工作中常见的情 况。如果已经稳定运行过一段时间的集群,每个节点上都会保存有数量不少的分片。这种时 候通过 reroute 接口手动转移,就显得太过麻烦了。这个时候,有另一种方式: curl -XPOST 127.0.0.1:9200/_cluster/settings -d '{ "transient" :{ "cluster.routing.allocation.exclude._ip" : "10.0.0.1" } }' Elasticsearch 集群就会自动把这个 IP 上的所有分片,都自动转移到其他节点上。等到转移完 成,这个空节点就可以毫无影响的下线了。 ELKstack 中文指南 216shard的allocate控制 和 _ip 类似的参数还有 _host , _name 等。此外,这类参数不单是 cluster 级别,也可以是 index 级别。下一小节就是 index 级别的用例。 冷热数据的读写分离 Elasticsearch 集群一个比较突出的问题是: 用户做一次大的查询的时候, 非常大量的读 IO 以 及聚合计算导致机器 Load 升高, CPU 使用率上升, 会影响阻塞到新数据的写入, 这个过程甚至 会持续几分钟。所以,可能需要仿照 MySQL 集群一样,做读写分离。 实施方案 1. N 台机器做热数据的存储, 上面只放当天的数据。这 N 台热数据节点上面的 elasticsearc.yml 中配置 node.tag: hot 2. 之前的数据放在另外的 M 台机器上。这 M 台冷数据节点中配置 node.tag: stale 3. 模板中控制对新建索引添加 hot 标签: { "order" : 0, "template" : "*", "settings" : { "index.routing.allocation.require.tag" : "hot" } } 4. 每天计划任务更新索引的配置, 将 tag 更改为 stale, 索引会自动迁移到 M 台冷数据节点 # curl -XPUT http://127.0.0.1:9200/indexname/_settings -d' { "index": { "routing": { "allocation": { "require": { "tag": "stale" } } } } }' 这样,写操作集中在 N 台热数据节点上,大范围的读操作集中在 M 台冷数据节点上。避免了 堵塞影响。 该方案运用的,是 Elasticsearch 中的 allocation filter 功能,详细说明 见:https://www.elastic.co/guide/en/elasticsearch/reference/master/shard-allocation- filtering.html ELKstack 中文指南 217shard的allocate控制 ELKstack 中文指南 218shard的allocate控制 集群自动发现 ES 是一个 P2P 类型(使用 gossip 协议)的分布式系统,除了集群状态管理以外,其他所有的 请求都可以发送到集群内任意一台节点上,这个节点可以自己找到需要转发给哪些节点,并 且直接跟这些节点通信。 所以,从网络架构及服务配置上来说,构建集群所需要的配置极其简单。在无阻碍的网络 下,所有配置了相同 cluster.name 的节点都自动归属到一个集群中。 multicast 方式 只配置 cluster.name 的集群,其实就是采用了默认的自动发现协议,即组播(multicast)方 式。节点会在本机所有网卡接口上,使用组播地址 224.2.2.4 ,以 54328 端口建立组播组发 送 clustername 信息。 但是,并不是所有的路由交换设备都支持并且开启了组播信息传输!甚至可以说,默认情况 下,都是不开启组播信息传输的。 所以在没有网络工程师帮助的情况下,ES 以默认组播方式,只有在同一个交换机下的节点, 能自动发现,跨交换机的节点,是无法收到组播信息的。 此外,由于节点是以所有网卡接口发送组播信息,而操作系统内核层面对组播信息来源的验 证中,却对网卡接口地址有一步校验,有可能发生内核层面的信息丢弃,导致多网卡的节点 也无法正常使用组播方式。 Elasticsearch 2.0 开始,为安全考虑,默认不分发 multicast 功能。依然希望使用 multicast 自 动发现的用户,需要单独安装: bin/plugin install discovery-multicast unicast 方式 除了组播方式,ES 还支持单播(unicast)方式。配置里提供几台节点的地址,ES 将其视作 gossip router 角色,借以完成集群的发现。由于这只是 ES 内一个很小的功能,所以 gossip router 角色并不需要单独配置,每个 ES 节点都可以担任。所以,采用单播方式的集群,各节 点都配置相同的几个节点列表作为 router 即可。 此外,考虑到节点有时候因为高负载,慢 GC 等原因可能会有偶尔没及时响应 ping 包的可 能,一般建议稍微加大 Fault Detection 的超时时间。 ELKstack 中文指南 219自动发现的配置 discovery.zen.minimum_master_nodes: 3 discovery.zen.ping.timeout: 100s discovery.zen.fd.ping_timeout: 100s discovery.zen.ping.multicast.enabled: false discovery.zen.ping.unicast.hosts: ["10.19.0.97","10.19.0.98","10.19.0.99","10.19.0.100"] ELKstack 中文指南 220自动发现的配置 增删改查 增删改查是数据库的基础操作方法。ES 虽然不是数据库,但是很多场合下,都被人们当做一 个文档型 NoSQL 数据库在使用,原因自然是因为在接口和分布式架构层面的相似性。虽然在 ELK Stack 场景下,数据的写入和查询,分别由 Logstash 和 Kibana 代劳,作为测试、调研 和排错时的基本功,还是需要了解一下 ES 的增删改查用法的。 数据写入 ES 的一大特点,就是全 RESTful 接口处理 JSON 请求。所以,数据写入非常简单: # curl -XPOST http://127.0.0.1:9200/logstash-2015.06.21/testlog -d '{ "date" : "1434966686000", "user" : "chenlin7", "mesg" : "first message into Elasticsearch" }' 命令返回响应结果为: {"_index":"logstash-2015.06.21","_type":"testlog","_id":"AU4ew3h2nBE6n0qcyVJK","_version":1,"created":true} 数据获取 可以看到,在数据写入的时候,会返回该数据的 _id 。这就是后续用来获取数据的关键: # curl -XGET http://127.0.0.1:9200/logstash-2015.06.21/testlog/AU4ew3h2nBE6n0qcyVJK 命令返回响应结果为: {"_index":"logstash-2015.06.21","_type":"testlog","_id":"AU4ew3h2nBE6n0qcyVJK","_version":1,"found":true,"_source":{ "date" : "1434966686000", "user" : "chenlin7", "mesg" : "first message into Elasticsearch" }} 这个 _source 里的内容,正是之前写入的数据。 ELKstack 中文指南 221增删改查操作 如果觉得这个返回看起来有点太过麻烦,可以使用 curl -XGET http://127.0.0.1:9200/logstash-2015.06.21/testlog/AU4ew3h2nBE6n0qcyVJK/_source 来指明只获 取源数据部分。 更进一步的,如果你只想看数据中的一部分字段内容,可以使用 curl -XGET http://127.0.0.1:9200/logstash-2015.06.21/testlog/AU4ew3h2nBE6n0qcyVJK?fields=user,mesg 来 指明获取字段,结果如下: {"_index":"logstash-2015.06.21","_type":"testlog","_id":"AU4ew3h2nBE6n0qcyVJK","_version":1,"found":true,"fields":{"user":["chenlin7"],"mesg":["first message into Elasticsearch"]}} 数据删除 要删除数据,修改发送的 HTTP 请求方法为 DELETE 即可: # curl -XDELETE http://127.0.0.1:9200/logstash-2015.06.21/testlog/AU4ew3h2nBE6n0qcyVJK 删除不单针对单条数据,还可以删除整个整个索引。甚至可以用通配符。 # curl -XDELETE http://127.0.0.1:9200/logstash-2015.06.0* 在 Elasticsearch 2.x 之前,可以通过查询语句删除,也可以删除某个 _type 内的数据。现在 都已经不再内置支持,改为 Delete by Query 插件。因为这种方式本身对性能影响较大! 数据更新 已经写过的数据,同样还是可以修改的。有两种办法,一种是全量提交,即指明 _id 再发送 一次写入请求。 # curl -XPOST http://127.0.0.1:9200/logstash-2015.06.21/testlog/AU4ew3h2nBE6n0qcyVJK -d '{ "date" : "1434966686000", "user" : "chenlin7", "mesg" " "first message into Elasticsearch but version 2" }' 另一种是局部更新,使用 /_update 接口: ELKstack 中文指南 222增删改查操作 # curl -XPOST 'http://127.0.0.1:9200/logstash-2015.06.21/testlog/AU4ew3h2nBE6n0qcyVJK/_update' -d '{ "doc" : { "user" : "someone" } }' 或者 # curl -XPOST 'http://127.0.0.1:9200/logstash-2015.06.21/testlog/AU4ew3h2nBE6n0qcyVJK/_update' -d '{ "script" : "ctx._source.user = \"someone\"" }' ELKstack 中文指南 223增删改查操作 搜索请求 上节介绍的,都是针对单条数据的操作。在 ES 环境中,更多的是搜索和聚合请求。在之前章 节中,我们也介绍过数据获取和数据搜索的一点区别:刚写入的数据,可以通过 translog 立 刻获取;但是却要等到 refresh 成为一个 segment 后,才能被搜索到。本节就介绍一下 ES 的搜索语法。 全文搜索 ES 对搜索请求,有简易语法和完整语法两种方式。简易语法作为以后在 Kibana 上最常用的 方式,一定是需要学会的。而在命令行里,我们可以通过最简单的方式来做到。还是上节输 入的数据: # curl -XGET http://127.0.0.1:9200/logstash-2015.06.21/testlog/_search?q=first 可以看到返回结果: {"took":240,"timed_out":false,"_shards":{"total":27,"successful":27,"failed":0},"hits":{"total":1,"max_score":0.11506981,"hits":[{"_index":"logstash-2015.06.21","_type":"testlog","_id":"AU4ew3h2nBE6n0qcyVJK","_score":0.11506981,"_source":{ "date" : "1434966686000", "user" : "chenlin7", "mesg" : "first message into Elasticsearch" }}]}} 还可以用下面语句搜索,结果是一样的。 # curl -XGET http://127.0.0.1:9200/logstash-2015.06.21/testlog/_search?q=user:"chenlin7" querystring 语法 上例中, ?q= 后面写的,就是 querystring 语法。鉴于这部分内容会在 Kibana 上经常使用, 这里详细解析一下语法: 全文检索:直接写搜索的单词,如上例中的 first ; 单字段的全文检索:在搜索单词之前加上字段名和冒号,比如如果知道单词 first 肯定出 现在 mesg 字段,可以写作 mesg:first ; 单字段的精确检索:在搜索单词前后加双引号,比如 user:"chenlin7" ; 多个检索条件的组合:可以使用 NOT , AND 和 OR 来组合检索,注意必须是大写。比 ELKstack 中文指南 224搜索请求 如 user:("chenlin7" OR "chenlin") AND NOT mesg:first ; 字段是否存在: _exists_:user 表示要求 user 字段存在, _missing_:user 表示要求 user 字段不存在; 通配符:用 ? 表示单字母, * 表示任意个字母。比如 fir?t mess* ; 正则:需要比通配符更复杂一点的表达式,可以使用正则。比如 mesg:/mes{2}ages?/ 。 注意 ES 中正则性能很差,而且支持的功能也不是特别强大,尽量不要使用。ES 支持的 正则语法见:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl- regexp-query.html#regexp-syntax; 近似搜索:用 ~ 表示搜索单词可能有一两个字母写的不对,请 ES 按照相似度返回结 果。比如 frist~ ; 范围搜索:对数值和时间,ES 都可以使用范围搜索,比如: rtt:>300 , date:["now-6h" TO "now"} 等。其中, [] 表示端点数值包含在范围内, {} 表示端点数值不包含在范围 内; 完整语法 ES 支持各种类型的检索请求,除了可以用 querystring 语法表达的以外,还有很多其他类 型,具体列表和示例可参 见:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-queries.html。 作为最简单和常用的示例,这里展示一下 term query 的写法,相当于 querystring 语法中的 user:"chenlin7" : # curl -XGET http://127.0.0.1:9200/_search -d ' { "query": { "term": { "user": "chenlin7" } } }' 聚合请求 在检索范围确定之后,ES 还支持对结果集做聚合查询,返回更直接的聚合统计结果。在 ES 1.0 版本之前,这个接口叫 Facet,1.0 版本之后,这个接口改为 Aggregation。 Kibana 分别在 v3 中使用 Facet,v4 中使用 Aggregation。不过总的来说,Aggregation 是 Facet 接口的强化升级版本,我们直接了解 Aggregation 即可。本书后续章节也会介绍如何在 Kibana 的 v3 版本中使用 aggregation 接口做二次开发。 堆叠聚合示例 ELKstack 中文指南 225搜索请求 在 Elasticsearch 1.x 系列中,aggregation 分为 bucket 和 metric 两种,分别用作词元划分和 数值计算。而其中的 bucket aggregation,还支持在自身结果集的基础上,叠加新的 aggregation。这就是 aggregation 比 facet 最领先的地方。比如实现一个时序百分比统计,在 facet 接口就无法直接完成,而在 aggregation 接口就很简单了: # curl -XPOST 'http://127.0.0.1:9200/logstash-2015.06.22/_search?size=0&pretty' -d'{ "aggs" : { "percentile_over_time" : { "date_histogram" : { "field" : "@timestamp", "interval" : "1h" }, "aggs" : { "percentile_one_time" : { "percentiles" : { "field" : "requesttime" } } } } } }' 得到结果如下: { "took" : 151595, "timed_out" : false, "_shards" : { "total" : 81, "successful" : 81, "failed" : 0 }, "hits" : { "total" : 3307142043, "max_score" : 1.0, "hits" : [ ] }, "aggregations" : { "percentile_over_time" : { "buckets" : [ { "key_as_string" : "22/Jun/2015:22:00:00 +0000", "key" : 1435010400000, "doc_count" : 459273981, "percentile_one_time" : { "values" : { "1.0" : 0.004, "5.0" : 0.006, "25.0" : 0.023, "50.0" : 0.035, ELKstack 中文指南 226搜索请求 "75.0" : 0.08774675719725569, "95.0" : 0.25732934416125663, "99.0" : 0.7508899754871812 } } }, { "key_as_string" : "23/Jun/2015:00:00:00 +0000", "key" : 1435017600000, "doc_count" : 768620219, "percentile_one_time" : { "values" : { "1.0" : 0.004, "5.0" : 0.007000000000000001, "25.0" : 0.025, "50.0" : 0.03987809503972864, "75.0" : 0.10297843567746187, "95.0" : 0.30047269327062875, "99.0" : 1.015495933753329 } } }, { "key_as_string" : "23/Jun/2015:02:00:00 +0000", "key" : 1435024800000, "doc_count" : 849467060, "percentile_one_time" : { "values" : { "1.0" : 0.004, "5.0" : 0.008, "25.0" : 0.027000000000000003, "50.0" : 0.0439999899006102, "75.0" : 0.1160416197625958, "95.0" : 0.3383140614483838, "99.0" : 1.0275839684542212 } } } ] } } } 管道聚合示例 在 Elasticsearch 2.x 中,新增了 pipeline aggregation 类型。可以在已有 aggregation 返回的 数组数据之后,再对这组数值做一次运算。最常见的,就是对时序数据求移动平均值。比如 对响应时间做周期为 7,移动窗口为 30,alpha, beta, gamma 参数均为 0.5 的 holt-winters 季 节性预测 2 个未来值的请求如下: ELKstack 中文指南 227搜索请求 { "aggs" : { "my_date_histo" : { "date_histogram" : { "field" : "@timestamp", "interval" : "1h" }, "aggs" : { "avgtime" : { "avg" : { "field" : "requesttime" } }, "the_movavg" : { "moving_avg" : { "buckets_path" : "avgtime", "window" : 30, "model" : "holt_winters", "predict" : 2, "settings" : { "type" : "mult", "alpha" : 0.5, "beta" : 0.5, "gamma" : 0.5, "period" : 7, "pad" : true } } } } } } } 响应如下: ELKstack 中文指南 228搜索请求 { "took" : 12, "timed_out" : false, "_shards" : { "total" : 10, "successful" : 10, "failed" : 0 }, "hits" : { "total" : 111331, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "my_date_histo" : { "buckets" : [ { "key_as_string" : "2015-12-24T02:00:00.000Z", "key" : 1450922400000, "doc_count" : 1462, "avgtime" : { "value" : 508.25649794801643 } }, { ... }, { "key_as_string" : "2015-12-24T17:00:00.000Z", "key" : 1450976400000, "doc_count" : 1664, "avgtime" : { "value" : 504.7067307692308 }, "the_movavg" : { "value" : 500.9766851760192 } }, { ... }, { "key_as_string" : "2015-12-25T09:00:00.000Z", "key" : 1451034000000, "doc_count" : 0, "the_movavg" : { "value" : 493.9519632950849, "value_as_string" : "1970-01-01T00:00:00.493Z" } } ] } } 可以看到,在第一个移动窗口还没满足之前,是没有移动平均值的;而在实际数据已经结束 以后,虽然没有平均值了,但是预测的移动平均值却还有数。 ELKstack 中文指南 229搜索请求 buckets_path 语法 由于 aggregation 是有堆叠层级关系的,所以 pipeline aggregation 在引用 metric aggregation 时也就会涉及到层级的问题。在上例中, the_movavg 和 avgtime 是同一层级,所以 buckets_path 直接写 avgtime 即可。但是如果我们把 the_movavg 上提一层,跟 my_date_histo 同级,这个 buckets_path 怎么写才行呢? "buckets_path" : "my_date_histo>avgtime" 如果用的是返回的数值有多个值的聚合,比如 percentiles 或者 extended_stats ,则是: "buckets_path" : "percentile_over_time>percentile_one_time.95" ES 目前能支持的聚合请求列表,参 见:https://www.elastic.co/guide/en/elasticsearch/reference/current/search- aggregations.html。 See Also Holt Winters 预测算法,见:https://en.wikipedia.org/wiki/Holt-Winters。其在运维领域最著名 的运用是 RRDtool 中的 HWPREDICT。 ELKstack 中文指南 230搜索请求 script Elasticsearch 中,可以使用自定义脚本扩展功能。包括评分、过滤函数和聚合字段等方面。 作为 ELK Stack 场景,我们只介绍在聚合字段方面使用 script 的方式。 动态提交 最简单易用的方式,就是在正常的请求体中,把 field 换成 script 提交。比如一个标准的 terms agg 改成 script 方式,写法如下: # curl 127.0.0.1:9200/logstash-2015.06.29/_search -d '{ "aggs" : { "clientip_top10" : { "terms" : { "script" : "doc['clientip'].value" } } } }' 在 script 中,有两种方式引用数据: doc['clientip'].value 和 _source.clientip 。其区别在 于: doc[].value 读取 fielddata 内的数据, _source.obj.attr 读取 _source 的 JSON 内 容。这也意味着,前者必须读取的是最终的词元字段数据,而后者可以返回任意的数据结 构。 注意:因为读取的是 fielddata,所以如果有分词的话, doc[].value 读取到的是分词后的数 据。所以请按需使用 doc['clientip.raw'].value 写法。 固定文件 ES 在 1.4.0 之前,默认脚本引擎是使用 mvel 语言,随后改成 groovy,但是从 1.4.3 开始, 因为安全漏洞,关掉了动态提交功能。只能使用固定文件方式运行。 为了和动态提交的语法有区别,调用固定文件的写法如下: ELKstack 中文指南 231script # curl 127.0.0.1:9200/logstash-2015.06.29/_search -d '{ "aggs" : { "clientip_subnet_top10" : { "terms" : { "script_file" : "getvalue", "lang" : "groovy", "params" : { "fieldname": "clientip.raw", "pattern": "^((?:\d{1,3}\.?){3})\.\d{1,3}$" } } } } }' 上例要求在 ES 集群的所有数据节点上,都保存有一个 /etc/elasticsearch/scripts/getvalue.groovy 文件,并且该脚本文件可以接收 fieldname 和 pattern 两个变量。试举例如下: #!/usr/bin/env groovy matcher = ( doc[fieldname].value =~ /${pattern}/ ) if (matcher.matches()) { matcher[0][1] } 注意:ES 进程默认每分钟扫描一次 /etc/elasticsearch/scripts/ 目录,并尝试加载该目录 下所有文件作为 script。所以,不要在该目录内做文件编辑等工作,不要分发 .svn 等目录到 生成环境,这些临时或者隐藏文件都会被 ES 进程加载然后报错。 其他语言 ES 支持通过插件方式,扩展脚本语言的支持,目前默认自带的语言包括: lucene expression groovy mustache 而 github 上目前已有以下语言插件支持,基本覆盖了所有 JVM 上的可用语言: https://github.com/elastic/elasticsearch-lang-mvel https://github.com/elastic/elasticsearch-lang-javascript https://github.com/elastic/elasticsearch-lang-python https://github.com/hiredman/elasticsearch-lang-clojure https://github.com/felipehummel/elasticsearch-lang-scala ELKstack 中文指南 232script https://github.com/fcheung/elasticsearch-jruby ELKstack 中文指南 233script reindex Elasticsearch 本身不提供对索引的 rename,mapping 的 alter 等操作。所以,如果有需要对 全索引数据进行导出,或者修改某个已有字段的 mapping 设置等情况下,我们只能通过 scroll API 导出全部数据,然后重新做一次索引写入。这个过程,叫做 reindex。 既然没有直接的方式,那么自然只能使用其他工具了。这里介绍两个常用的方法,自己写程 序和用 logstash。 Perl 客户端 Elastic 官方提供各种语言的客户端库,其中,Perl 库提供了对 reindex 比较方便的写法和示 例。通过 cpanm Search::Elasticsearch 命令安装库完毕后,使用以下程序即可: use Search::Elasticsearch; my $es = Search::Elasticsearch->new( nodes => ['192.168.0.2:9200'] ); my $bulk = $es->bulk_helper( index => 'new_index', ); $bulk->reindex( source => { index => 'old_index', size => 500, # default search_type => 'scan' # default } ); Logstash 做 reindex 在最新版的 Logstash 中,对 logstash-input-elasticsearch 插件做了一定的修改,使得通过 logstash 完成 reindex 成为可能。 reindex 操作的 logstash 配置如下: ELKstack 中文指南 234reindex input { elasticsearch { hosts => [ "192.168.0.2" ] index => "old_index" size => 500 scroll => "5m" docinfo => true } } output { elasticsearch { hosts => [ "192.168.0.3" ] index => "%{[@metadata][_index]}" document_type => "%{[@metadata][_type]}" document_id => "%{[@metadata][_id]}" } } 如果你做 reindex 的源索引并不是 logstash 记录的内容,也就是没有 @timestamp , @version 这两个 logstash 字段,那么可以在上面配置中添加一段 filter 配置,确保前后索引字段完全一 致: filter { mutate { remove_field => [ "@timestamp", "@version" ] } } ELKstack 中文指南 235reindex 批量提交 在 CRUD 章节,我们已经知道 ES 的数据写入是如何操作的了。喜欢自己动手的读者可能已 经迫不及待的自己写了程序开始往 ES 里写数据做测试。这时候大家会发现:程序的运行速度 非常一般,即使 ES 服务运行在本机,一秒钟大概也就能写入几百条数据。 这种速度显然不是 ES 的极限。事实上,每条数据经过一次完整的 HTTP POST 请求和 ES indexing 是一种极大的性能浪费,为此,ES 设计了批量提交方式。在数据读取方面,叫 mget 接口,在数据变更方面,叫 bulk 接口。mget 一般常用于搜索时 ES 节点之间批量获取 中间结果集,对于 ELK Stack 用户,更常见到的是 bulk 接口。 bulk 接口采用一种比较简朴的数据积累格式,示例如下: # curl -XPOST http://127.0.0.1:9200/_bulk -d' { "create" : { "_index" : "test", "_type" : "type1" } } { "field1" : "value1" } { "delete" : { "_index" : "test", "_type" : "type1" } } { "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } } { "field1" : "value2" } { "update" : {"_id" : "1", "_type" : "type1", "_index" : "test"} } { "doc" : {"field2" : "value2"} } ' 格式是,每条 JSON 数据的上面,加一行描述性的元 JSON,指明下一行数据的操作类型, 归属索引信息等。 采用这种格式,而不是一般的 JSON 数组格式,是因为接收到 bulk 请求的 ES 节点,就可以 不需要做完整的 JSON 数组解析处理,直接按行处理简短的元 JSON,就可以确定下一行数 据 JSON 转发给哪个数据节点了。这样,一个固定内存大小的 network buffer 空间,就可以 反复使用,又节省了大量 JVM 的 GC。 事实上,产品级的 logstash、rsyslog、spark 都是默认采用 bulk 接口进行数据写入的。对于 打算自己写程序的读者,建议采用 Perl 的 Search::Elasticsearch::Bulk 或者 Python 的 elasticsearch.helpers.* 库。 bulk size 在配置 bulk 数据的时候,一般需要注意的就是请求体大小(bulk size)。 这里有一点细节上的矛盾,我们知道,HTTP 请求,是可以通过 HTTP 状态码 100 Continue 来持续发送数据的。但对于 ES 节点接收 HTTP 请求体的 Content-Length 来说,是按照整个 大小来计算的。所以,首先,要确保 bulk 数据不要超过 http.max_content_length 设置。 ELKstack 中文指南 236bulk提交 那么,是不是尽量让 bulk size 接近这个数值呢?当然不是。 依然是请求体的问题,因为请求体需要全部加载到内存,而 JVM Heap 一共就那么多(按 31GB 算),过大的请求体,会挤占其他线程池的空间,反而导致写入性能的下降。 再考虑网卡流量,磁盘转速的问题,所以一般来说,建议 bulk 请求体的大小,在 15MB 左 右,通过实际测试继续向上探索最合适的设置。 注意:这里说的 15MB 是请求体的字节数,而不是程序里里设置的 bulk size。bulk size 一般 指数据的条目数。不要忘了,bulk 请求体中,每条数据还会额外带上一行元 JSON。 以 logstash 默认的 bulk_size => 5000 为例,假设单条数据平均大小 200B ,一次 bulk 请求 体的大小就是 1.5MB。那么我们可以尝试 bulk_size => 50000 ;而如何单条数据平均大小是 20KB,一次 bulk 大小就是 100MB,显然超标了,需要尝试下调至 bulk_size => 500 。 UDP ES 其实还提供了一个连 HTTP header 解析步骤都能省略的 bulk 方法,叫 UDP bulk,即开 启 UDP 9700 端口,直接 nc 发送 bulk 数据内容写入。 由于 UDP 的不可靠性,ES 计划从 2.0 版本开始废弃该功能,确实需要高性能写入又不担心 数据缺失问题的读者,可以参考 ES 官方文档使用该功能。 ELKstack 中文指南 237bulk提交 gateway gateway 是 ES 设计用来长期存储索引数据的接口。一般来说,大家都是用本地磁盘来存储索 引数据,即 gateway.type 为 local 。 数据恢复中,有很多策略调整我们已经在之前分片控制小节讲过。除开分片级别的控制以 外,gateway 级别也还有一些可优化的地方: gateway.recover_after_nodes 该参数控制集群在达到多少个节点的规模后,才开始数据 恢复任务。这样可以避免集群自动发现的初期,分片不全的问题。 gateway.recover_after_time 该参数控制集群在达到上条配置设置的节点规模后,再等待 多久才开始数据恢复任务。 gateway.expected_nodes 该参数设置集群的预期节点总数。在达到这个总数后,即认为 集群节点已经完全加载,即可开始数据恢复,不用再等待上条设置的时间。 注意:gateway 中说的节点,仅包括主节点和数据节点,纯粹的 client 节点是不算在内的。如 果你有更明确的选择,也可以按需求写: gateway.recover_after_data_nodes gateway.recover_after_master_nodes gateway.expected_data_nodes gateway.expected_master_nodes 共享存储上的影子副本 虽然 ES 对 gateway 使用 NFS,iscsi 等共享存储的方式极力反对,但是对于较大量级的索引 的副本数据,ES 从 1.5 版本开始,还是提供了一种节约成本又不特别影响性能的方式:影子 副本(shadow replica)。 首先,需要在集群各节点的 elasticsearch.yml 中开启选项: node.enable_custom_paths: true 同时,确保各节点使用相同的路径挂载了共享存储,且目录权限为 Elasticsearch 进程用户可 读可写。 然后,创建索引: ELKstack 中文指南 238gateway配置 # curl -XPUT 'http://127.0.0.1:9200/my_index' -d ' { "index" : { "number_of_shards" : 1, "number_of_replicas" : 4, "data_path": "/var/data/my_index", "shadow_replicas": true } }' 针对 shadow replicas ,ES 节点不会做实际的索引操作,而是单纯的每次 flush 时,把 segment 内容 fsync 到共享存储磁盘上。然后 refresh 让其他节点能够搜索该 segment 内 容。所以,shadow replicas 里是没有 translog 的,对于还没有 refresh 的数据,如果 GET 获 取请求传到 shadow replicas 上,是查询不到的,请求会自动变成 ?preference=_primary 模 式,只从主分片上获取数据。同理,在 cluster state 还没定期更新过来之前,节点上的索引 映射可能也还保持着自己主分片数据的样式,不会因为 shadow replica 里数据样式的变动发 生变动,搜索请求也有可能失败。 综上,shadow replicas 只是一个在某些特定环境下有用的方式。在资源允许的情况下,还是 应该使用 local gateway。而另外采用 snapshot 接口来完成数据长期备份到 HDFS 或其他共 享存储的需要。 ELKstack 中文指南 239gateway配置 集群状态维护 我们都知道,ES 中的 master 跟一般 MySQL、Hadoop 的 master 是不一样的。它即不是写 入流量的唯一入口,也不是所有数据的元信息的存放地点。所以,一般来说,ES 的 master 节点负载很轻,集群性能是可以近似认为随着 data 节点的扩展线性提升的。 但是,上面这句话并不是完全正确的。 ES 中有一件事情是只有 master 节点能管理的,这就是集群状态(cluster state)。 集群状态中包括以下信息: 集群层面的设置 集群内有哪些节点 各索引的设置,映射,分析器和别名等 索引内各分片所在的节点位置 这些信息在集群的任意节点上都存放着,你也可以通过 /_cluster/state 接口直接读取到其 内容。注意这最后一项信息,之前我们已经讲过 ES 怎么通过简单地取余知道一条数据放在哪 个分片里,加上现在集群状态里又记载了分片在哪个节点上,那么,整个集群里,任意节点 都可以知道一条数据在哪个节点上存储了。所以,数据读写才可以发送给集群里任意节点。 至于修改,则只能由 master 节点完成!显然,集群状态里大部分内容是极少变动的,唯独有 一样除外——索引的映射。因为 ES 的 schema-less 特性,我们可以任意写入 JSON 数据, 所以索引中随时可能增加新的字段。这个时候,负责容纳这条数据的主分片所在的节点,会 暂停写入操作,将字段的映射结果传递给 master 节点;master 节点合并这段修改到集群状 态里,发送新版本的集群状态到集群的所有节点上。然后写入操作才会继续。一般来说,这 个操作是在一二十毫秒内就可以完成,影响也不大。 但是也有一些情况会是例外。 批量新索引创建 在较大规模的 ELK Stack 应用场景中,这是比较常见的一个情况。因为 ELK Stack 建议采用 日期时间作为索引的划分方式,所以定时(一般是每天),会统一产生一批新的索引。而前面已 经讲过,ES 的集群状态每次更新都是阻塞式的发布到全部节点上以后,节点才能继续后续处 理。 这就意味着,如果在集群负载较高的时候,批量新建新索引,可能会有一个显著的阻塞时 间,无法写入任何数据。要等到全部节点同步完成集群状态以后,数据写入才能恢复。 ELKstack 中文指南 240集群状态维护 不巧的是,中国使用的是北京时间,UTC +0800。也就是说,默认的 ELK Stack 新建索引时 间是在早上 8 点。这个时间点一般日志写入量已经上涨到一定水平了(当然,晚上 0 点的量其 实也不低)。 对此,可以通过定时任务,每天在最低谷的早上三四点,提前通过 POST mapping 的方式, 创建好之后几天的索引。就可以避免这个问题了。 过多字段持续更新 这是另一种常见的滥用。在使用 ELK Stack 处理访问日志时,为了查询更方便,可能会采用 logstash-filter-kv 插件,将访问日志中的每个 URL 参数,都切分成单独的字段。比如一个 "/index.do?uid=1234567890&action=payload" 的 URL 会被转换成如下 JSON: "urlpath" : "/index.do", "urlargs" : { "uid" : "1234567890", "action" : "payload", ... } 但是,因为集群状态是存在所有节点的内存里的,一旦 URL 参数过多,ES 节点的内存就被 大量用于存储字段映射内容。这是一个极大的浪费。如果碰上 URL 参数的键内容本身一直在 变动,直接撑爆 ES 内存都是有可能的! 以上是真实发生的事件,开发人员莫名的选择将一个 UUID 结果作为 key 放在 URL 参数里。 直接导致 ES 集群 master 节点全部 OOM。 如果你在 ES 日志中一直看到有新的 updating mapping [logstash-2015.06.01] 字样出现的 话,请郑重考虑一下自己是不是用的上如此细分的字段列表吧。 好,三秒钟过去,如果你确定一定以及肯定还要这么做,下面是一个变通的解决办法。 nested object 用 nested object 来存放 URL 参数的方法稍微复杂,但还可以接受。单从 JSON 数据层面 看,新方式的数据结构如下: "urlargs": [ { "key": "uid", "value": "1234567890" }, { "key": "action", "value": "payload" }, ... ] ELKstack 中文指南 241集群状态维护 没错,看起来就是一个数组。但是 JSON 数组在 ES 里是有两种处理方式的。 如果直接写入数组,ES 在实际索引过程中,会把所有内容都平铺开,变成 Arrays of Inner Objects。整条数据实际类似这样的结构: { "urlpath" : ["/index.do"], "urlargs.key" : ["uid", "action", ...], "urlargs.value" : ["1234567890", "payload", ...] 这种方式最大的问题是,当你采用 urlargs.key:"uid" AND urlargs.value:"0987654321" 语句 意图搜索一个 uid=0987654321 的请求时,实际是整个 URL 参数中任意一处 value 为 0987654321 的,都会命中。 要想达到正确搜索的目的,需要在写入数据之前,指定 urlargs 字段的映射类型为 nested object。命令如下: curl -XPOST http://127.0.0.1:9200/logstash-2015.06.01/_mapping -d '{ "accesslog" : { "properties" : { "urlargs" : { "type" : "nested", "properties" : { "key" : { "type" : "string", "index" : "not_analyzed", "doc_values" : true }, "value" : { "type" : "string", "index" : "not_analyzed", "doc_values" : true } } } } } }' 这样,数据实际是类似这样的结构: { "urlpath" : ["/index.do"], }, { "urlargs.key" : ["uid"], "urlargs.value" : ["1234567890"], }, { "urlargs.key" : ["action"], "urlargs.value" : ["payload"], } ELKstack 中文指南 242集群状态维护 当然,nested object 节省字段映射的优势对应的是它在使用的复杂。Query 和 Aggs 都必须 使用专门的 nested query 和 nested aggs 才能正确读取到它。 nested query 语法如下: curl -XPOST http://127.0.0.1:9200/logstash-2015.06.01/accesslog/_search -d ' { "query": { "bool": { "must": [ { "match": { "urlpath" : "/index.do" }}, { "nested": { "path": "urlargs", "query": { "bool": { "must": [ { "match": { "urlargs.key": "uid" }}, { "match": { "urlargs.value": "1234567890" }} ] }}}} ] }}}' nested aggs 语法如下: ELKstack 中文指南 243集群状态维护 curl -XPOST http://127.0.0.1:9200/logstash-2015.06.01/accesslog/_search -d ' { "aggs": { "topnuid": { "nested": { "path": "urlargs" }, "aggs": { "uid": { "filter": { "term": { "urlargs.key": "uid", } }, "aggs": { "topn": { "terms": { "field": "urlargs.value" } } } } } } } }' ELKstack 中文指南 244集群状态维护 缓存 ES 内针对不同阶段,设计有不同的缓存。以此提升数据检索时的响应性能。主要包括节点层 面的 filter cache 和索引层面的 query cache。下面分别讲述。 filter cache ES 的 query DSL 分为 query 和 filter 两种,很多检索语法,是同时存在 query 和 filter 里的。 比如最常用的 term、prefix、range 等。那么,怎么选择是使用 query 还是 filter 呢? 首先,要明白 query 跟 filter 的区别: query 是要相关性评分的,filter 不要; query 结果无法缓存,filter 可以。 所以,选择也就出来了: 全文搜索、评分排序,使用 query; 是非过滤,精确匹配,使用 filter。 上面做对比时,说 filter 能用缓存 query 不能,其实是不精确的。默认情况下,并不是所有的 filter 都能用缓存。常用的比如 term、terms、prefix、range、bool 等 filter,其过滤结果明 确,也容易设置缓存,ES 就对这几个默认开启了 filter cache 功能。而更复杂的一些比如 geo、script 等 filter,从 fielddata 数据到过滤结果还需要进行一系列计算的,ES 默认是不开 启 filter cache 的。而像 and、not、or 这几个关系型 filter,也是不开启的。 如果想要强制开启这些默认没有的 filter cache,需要在请求的 JSON 中带上 "cache": true 参数。 检索的时候想尽量使用 filter cache,但是 ES 接口有要求必须传递 query 参数,这时候,有 一个特殊的 query,叫 filtered query。其作用是在 query 中使用 filter,这样,我们就可以实 现如下这样的请求: # curl -XGET http://127.0.0.1:9200/_search -d ' { "query": { "filtered": { "filter": { "range": { "@timestamp": { "gte": "now - 1d / d" }} } } } }' ELKstack 中文指南 245缓存 事实上,Kibana3 中,就大量使用了这种 filtered query 语法。 需要注意的是,filter cache 是节点层面的缓存设置,每个节点上所有数据在响应请求时,是 共用一个缓存空间的。当空间用满,按照 LRU 策略淘汰掉最冷的数据。 可以用 indices.cache.filter.size 配置来设置这个缓存空间的大小,默认是 JVM 堆的 10%,也可以设置一个绝对值。 shard query cache ES 还有另一个索引层面的缓存,叫 shard query cache。之前章节中说过,ES 集群的任意节 点都可以接受请求,它会自动转发给数据所在的各个节点,等待各节点把各自的结果返回 后,完成数据的汇聚处理再返回给客户端。 这里可以把这个过程再细化一下。ES 对请求的处理过程,是有不同类型的,默认的叫 query_then_fetch。在这种情况下,各数据节点处理检索请求后,返回的,是只包含文档 id 和相关性分值的结果,这一段处理,叫做 query 阶段;汇聚到这份结果后,按照分值排序, 得到一个全集群最终需要的文档 id,再向对应节点发送一次文档获取请求,拿到文档内容, 这一段处理,叫做 fetch 阶段。两段都结束后才返回响应。在稍后的 ES 日志记录章节,我们 可以看到 ES 对这两个阶段,甚至都有分别的慢查询记录。 此外,还有 DFS_query_then_fetch 类型,提高小数据量时的精确度;query_and_fetch 类 型,在有明确 routing 时可以省略一个数据来回;count 类型,再不关心文档内容只需要计数 时省略 fetch 阶段,这是 ELK Stack 聚合统计场景最常用的类型;scan 类型,批量获取数据 省略 query 阶段,在 reindex 时就是使用这种类型。 回到 query cache,这里这个 query,就是处理过程中 query 阶段的意思。各个节点上的数据 分片,会在处理完 query 阶段时,将得到的本分片有关该请求的计数值,缓存起来。 根据上面的请求类型介绍,显然,只有当 ?search_type=count 的时候,这个 query cache 才 能起到作用。 不过,query cache 默认并不开启。因为 query cache 要起作用,还有几个先决条件: 1. 分片数据不再变动,也就是对当天的索引是无效的(如果 refresh_interval 很大,那么 在这个间隔内倒也算有效); 2. 使用了 "now" 语法的请求无法被缓存,因为这个是要即时计算的; 3. 缓存的键是请求的整个 JSON 字符串,整个字符串发生任何字节变动,缓存都无效。 以 ELK Stack 场景来说,Kibana 里几乎所有的请求,都是有 @timestamp 作为过滤条件的, 而且大多数是以最近 N 小时/分钟这样的选项,也就是说,页面每次刷新,发出的请求 JSON 里的时间过滤部分都是在变动的。query cache 在处理 Kibana 发出的请求时,完全无用。 ELKstack 中文指南 246缓存 所以,虽然官网宣称 query cache 对日志场景非常有用,但是对使用 Kibana 的一般用户来 说,完全没法体会到这个优势。 如果是自己写程序做历史统计分析和展示的,想办法固化时间过滤条件或者干脆去掉这个条 件,那么用上这个特性还是不错的。要对某个索引开启 query cache 的话,利用 index settings 接口动态调整即可: # curl -XPUT http://127.0.0.1:9200/logstash-2015.06.23/_settings -d' { "index.cache.query.enable": true }' 此外,还可以针对具体某个请求单独开启: # curl 'http://127.0.0.1:9200/logstash-2015.06.23/_search?search_type=count&query_cache=true' -d' { "aggs": { "popular_colors": { "terms": { "field": "colors" } } } }' 和 filter cache 一样,query cache 的大小也是以节点级别控制的,配置项名为 indices.cache.query.size ,其默认值为 1% 。 ELKstack 中文指南 247缓存 字段数据 Elasticsearch 2.x 已经默认所有 not_analyzed 字段自动启用 doc_values !本节不再重 要,仅作为知识共享保留。 字段数据(fielddata),在 Lucene 中又叫 uninverted index。我们都知道,搜索引擎会使用倒排 索引(inverted index)来映射单词到文档的 ID 号。而同时,为了提供对文档内容的聚合, Lucene 还可以在运行时将每个字段的单词以字典序排成另一个 uninverted index,可以大大 加速计算性能。 作为一个加速性能的方式,fielddata 当然是被全部加载在内存的时候最为有效。这也是 ES 默认的运行设置。但是,内存是有限的,所以 ES 同时也需要提供对 fielddata 内存的限额方 式: indices.fielddata.cache.size 节点用于 fielddata 的最大内存,如果 fielddata 达到该阈 值,就会把旧数据交换出去。该参数可以设置百分比或者绝对值。默认设置是不限制, 所以强烈建议设置该值,比如 10% 。 indices.fielddata.cache.expire 进入 fielddata 内存中的数据多久自动过期。注意,因为 ES 的 fielddata 本身是一种数据结构,而不是简单的缓存,所以过期删除 fielddata 是一 个非常消耗资源的操作。ES 官方在文档中特意说明,这个参数绝对绝对不要设置! Circuit Breaker Elasticsearch 在 total,fielddata,request 三个层面上都设计有 circuit breaker 以保护进程不 至于发生 OOM 事件。在 fielddata 层面,其设置为: indices.breaker.fielddata.limit 默认是 JVM 堆内存大小的 60%。注意,为了让设置正常发 挥作用,如果之前设置过 indices.fielddata.cache.size 的,一定要确保 indices.breaker.fielddata.limit 的值大于 indices.fielddata.cache.size 的值。否则 的话,fielddata 大小一到 limit 阈值就报错,就永远道不了 size 阈值,无法触发对旧数据 的交换任务了。 doc values 但是相比较集群庞大的数据量,内存本身是远远不够的。为了解决这个问题,ES 引入了另一 个特性,可以对精确索引的字段,指定 fielddata 的存储方式。这个配置项叫: doc_values 。 所谓 doc_values ,其实就是在 ES 将数据写入索引的时候,提前生成好 fielddata 内容,并记 录到磁盘上。因为 fielddata 数据是顺序读写的,所以即使在磁盘上,通过文件系统层的缓 存,也可以获得相当不错的性能。 ELKstack 中文指南 248fielddata 注意:因为 doc_values 是在数据写入时即生成内容,所以,它只能应用在精准索引的字段 上,因为索引进程没法知道后续会有什么分词器生成的结果。所以,字段设置应该是这样: "myfieldname": { "type": "string", "index": "not_analyzed", "doc_values": true } ELKstack 中文指南 249fielddata curator 如果经过之前章节的一系列优化之后,数据确实超过了集群能承载的能力,除了拆分集群以 外,最后就只剩下一个办法了:清除废旧索引。 为了更加方便的做清除数据,合并 segment,备份恢复等管理任务,Elasticsearch 在提供相 关 API 的同时,另外准备了一个命令行工具,叫 curator 。curator 是 Python 程序,可以直接 通过 pypi 库安装: pip install elasticsearch-curator 注意,是 elasticsearch-curator 不是 curator。PyPi 原先就有另一个项目叫这个名字 参数介绍 和 ELK Stack 里其他组件一样,curator 也是被 Elastic.co 收购的原开源社区周边。收编之后 同样进行了一次重构,命令行参数从单字母风格改成了长单词风格。新版本的 curator 命令可 用参数如下: Usage: curator [OPTIONS] COMMAND [ARGS]... Options 包括: --host TEXT Elasticsearch host. --url_prefix TEXT Elasticsearch http url prefix. --port INTEGER Elasticsearch port. --use_ssl Connect to Elasticsearch through SSL. --http_auth TEXT Use Basic Authentication ex: user:pass --timeout INTEGER Connection timeout in seconds. --master-only Only operate on elected master node. --dry-run Do not perform any changes. --debug Debug mode --loglevel TEXT Log level --logfile TEXT log file --logformat TEXT Log output format [default|logstash]. --version Show the version and exit. --help Show this message and exit. Commands 包括: alias Index Aliasing allocation Index Allocation bloom Disable bloom filter cache close Close indices delete Delete indices or snapshots open Open indices optimize Optimize Indices replicas Replica Count Per-shard show Show indices or snapshots snapshot Take snapshots of indices (Backup) 针对具体的 Command,还可以继续使用 --help 查看该子命令的帮助。比如查看 close 子命 令的帮助,输入 curator close --help ,结果如下: ELKstack 中文指南 250curator工具 Usage: curator close [OPTIONS] COMMAND [ARGS]... Close indices Options: --help Show this message and exit. Commands: indices Index selection. 常用示例 在使用 1.4.0 以上版本的 Elasticsearch 前提下,curator 曾经主要的一个子命令 bloom 已经 不再需要使用。所以,目前最常用的三个子命令,分别是 close , delete 和 optimize ,示 例如下: curator --timeout 36000 --host 10.0.0.100 delete indices --older-than 5 --time-unit days --timestring '%Y.%m.%d' --prefix logstash-mweibo-nginx- curator --timeout 36000 --host 10.0.0.100 delete indices --older-than 10 --time-unit days --timestring '%Y.%m.%d' --prefix logstash-mweibo-client- --exclude 'logstash-mweibo-client-2015.05.11' curator --timeout 36000 --host 10.0.0.100 delete indices --older-than 30 --time-unit days --timestring '%Y.%m.%d' --regex '^logstash-mweibo-\d+' curator --timeout 36000 --host 10.0.0.100 close indices --older-than 7 --time-unit days --timestring '%Y.%m.%d' --prefix logstash- curator --timeout 36000 --host 10.0.0.100 optimize --max_num_segments 1 indices --older-than 1 --newer-than 7 --time-unit days --timestring '%Y.%m.%d' --prefix logstash- 这一顿任务,结果是: logstash-mweibo-nginx-yyyy.mm.dd 索引保存最近 5 天,logstash-mweibo-client- yyyy.mm.dd 保存最近 10 天,logstash-mweibo-yyyy.mm.dd 索引保存最近 30 天;且所有七 天前的 logstash-* 索引都暂时关闭不用;最后对所有非当日日志做 segment 合并优化。 ELKstack 中文指南 251curator工具 扩展和测试方案 在体验完 Elasticsearch 便捷的操作后,下一步一定会碰到的问题是:数据写入变慢了,机器 变卡了,是需要做优化呢?还是需要扩容设备了?如果做扩容,索引的分片和副本设置多少 才合适?如果做优化,某个参数能造成什么样的影响? 而 ES 集群性能,受服务器硬件、数据结构和长度、请求接口复杂度等各种环节影响颇大。这 些问题,都需要有一个标准的测试流程给出答案。 由于 ES 是近乎线性扩展的分布式系统,所以对上述需求我们都可以总结成同一个测试模式: 1. 使用和线上集群相同硬件配置的服务器搭建一个单节点集群。 2. 使用和线上集群相同的映射创建一个 0 副本,1 分片的测试索引。 3. 使用和线上集群相同的数据写入进行压测。 4. 观察写入性能,或者运行查询请求观察搜索聚合性能。 5. 持续压测数小时,使用监控系统记录 eps、requesttime、fielddata cache、GC count 等 关键数据。 测试完成后,根据监控系统数据,确定单分片的性能拐点,或者适合自己预期值的临界点。 这个数据,就是一个基准数据。之后的扩容计划,都可以以这个基准单位进行。 需要注意的是,测试是以分片为单位的,在实际使用中,因为主分片和副本分片都是在各自 节点做 indexing 和 merge 操作,需要消耗同样的写入性能。所以,实际集群的容量预估中, 要考虑副本数的影响。也就是说,假如你在基准测试中得到单机写入性能在 10000 eps,那 么开启一个副本后所能达到的 eps 就只有 5000 了。还想写入 10000 eps 的话,就需要加一 倍机器。 另外,测试中我们使用的配置都尽量贴合当前现状。事实上,很多配置可能其实并不合理。 在确定基准线并开始扩容之前,还是要认真调节配置,审核请求使用的接口是否最优,然后 反复测试。然后取一个最终的基准值。 审核请求,更是一个长期的过程,就像 DBA 永远需要关注慢查询一样。ES 的慢查询请求处 理,请阅读稍后性能日志一节。 ELKstack 中文指南 252扩展和测试方案 多集群连接 当你的 ES 集群发展到一定规模,单集群不足以应对庞大的在线索引量级,或者由于业务隔离 需求,都有可能划分成多个集群。这时候,另一个问题就出来了:可能其中有一部分数据, 被分割在两个集群里,但是还是需要一起使用的。如果是自己写程序,当然可以初始化两个 对象,分别连接两个集群,得到结果集后再自行合并。但是如果用 ELK Stack 的,Kibana 可 不支持同时连接两个集群地址,这时候,就要用到 ES 中一个特殊的角色:tribe 节点。 tribe 节点只需要提供集群自动发现方面的配置,连接上多个集群后,对外提供只读功 能。 elasticsearch.yml 配置示例如下: tribe: 1002: cluster.name: es1002 discovery.zen.ping.timeout: 100s discovery.zen.ping.multicast.enabled: false discovery.zen.ping.unicast.hosts: ["10.19.0.22","10.19.0.24",10.19.0.21"] 1003: cluster.name: es1003 discovery.zen.ping.timeout: 100s discovery.zen.ping.multicast.enabled: false discovery.zen.ping.unicast.hosts: ["10.19.0.97","10.19.0.98","10.19.0.99","10.19.0.100"] blocks: write: true metadata: true on_conflict: prefer_1003 注意这里的 on_conflict 设置,当多个集群内,索引名称有冲突的时候,tribe 节点默认会把 请求轮询转发到各个集群上,这显然是不可以的。所以可以设置一个优先级,在索引名冲突 的时候,偏向于转发给某一个集群。 以 tribe 配置启动的 Elasticsearch 服务,其日志输入如下: [2015-06-18 18:05:51,983][INFO ][node ] [Manslaughter] version[1.5.1], pid[12846], build[5e38401/2015-04-09T13:41:35Z] [2015-06-18 18:05:51,984][INFO ][node ] [Manslaughter] initializing ... [2015-06-18 18:05:51,990][INFO ][plugins ] [Manslaughter] loaded [], sites [] [2015-06-18 18:05:54,891][INFO ][node ] [Manslaughter/1003] version[1.5.1], pid[12846], build[5e38401/2015-04-09T13:41:35Z] [2015-06-18 18:05:54,891][INFO ][node ] [Manslaughter/1003] initializing ... [2015-06-18 18:05:54,891][INFO ][plugins ] [Manslaughter/1003] loaded [], sites [] [2015-06-18 18:05:55,654][INFO ][node ] [Manslaughter/1003] initialized [2015-06-18 18:05:55,655][INFO ][node ] [Manslaughter/1002] version[1.5.1], pid[12846], build[5e38401/2015-04-09T13:41:35Z] [2015-06-18 18:05:55,655][INFO ][node ] [Manslaughter/1002] initializing ... [2015-06-18 18:05:55,656][INFO ][plugins ] [Manslaughter/1002] loaded [], sites [] [2015-06-18 18:05:56,275][INFO ][node ] [Manslaughter/1002] initialized ELKstack 中文指南 253多集群互联 [2015-06-18 18:05:56,285][INFO ][node ] [Manslaughter] initialized [2015-06-18 18:05:56,286][INFO ][node ] [Manslaughter] starting ... [2015-06-18 18:05:56,486][INFO ][transport ] [Manslaughter] bound_address {inet[/0:0:0:0:0:0:0:0:9301]}, publish_address {inet[/10.19.0.100:9301]} [2015-06-18 18:05:56,499][INFO ][discovery ] [Manslaughter] elasticsearch/Oewo-L2fR3y2xsgpsoI4Og [2015-06-18 18:05:56,499][WARN ][discovery ] [Manslaughter] waited for 0s and no initial state was set by the discovery [2015-06-18 18:05:56,529][INFO ][http ] [Manslaughter] bound_address {inet[/0:0:0:0:0:0:0:0:9201]}, publish_address {inet[/10.19.0.100:9201]} [2015-06-18 18:05:56,530][INFO ][node ] [Manslaughter/1003] starting ... [2015-06-18 18:05:56,603][INFO ][transport ] [Manslaughter/1003] bound_address {inet[/0:0:0:0:0:0:0:0:9302]}, publish_address {inet[/10.19.0.100:9302]} [2015-06-18 18:05:56,609][INFO ][discovery ] [Manslaughter/1003] es1003/m1-cDaFTSoqqyC2iiQhECA [2015-06-18 18:06:26,610][WARN ][discovery ] [Manslaughter/1003] waited for 30s and no initial state was set by the discovery [2015-06-18 18:06:26,610][INFO ][node ] [Manslaughter/1003] started [2015-06-18 18:06:26,611][INFO ][node ] [Manslaughter/1002] starting ... [2015-06-18 18:06:26,674][INFO ][transport ] [Manslaughter/1002] bound_address {inet[/0:0:0:0:0:0:0:0:9303]}, publish_address {inet[/10.19.0.100:9303]} [2015-06-18 18:06:26,676][INFO ][discovery ] [Manslaughter/1002] es1002/4FPiRPh7TFyBk-BaPc_TLg [2015-06-18 18:06:56,676][WARN ][discovery ] [Manslaughter/1002] waited for 30s and no initial state was set by the discovery [2015-06-18 18:06:56,677][INFO ][node ] [Manslaughter/1002] started [2015-06-18 18:06:56,677][INFO ][node ] [Manslaughter] started [2015-06-18 18:07:37,266][INFO ][cluster.service ] [Manslaughter/1003] detected_master [10.19.0.97][jnA-rt2fS_22Mz9nYl5Ueg][localhost.localdomain][inet[/10.19.0.97:9300]]{max_local_storage_nodes=1, data=false, master=true}, added {[10.19.0.73][_S8ylz1OTv6Nyp1YoMRNGQ][esnode073.mweibo.bx.sinanode.com][inet[/10.19.0.73:9300]]{max_local_storage_nodes=1, master=false},}, reason: zen-disco-receive(from master [[10.19.0.97][jnA-rt2fS_22Mz9nYl5Ueg][localhost.localdomain][inet[/10.19.0.97:9300]]{max_local_storage_nodes=1, data=false, master=true}]) [2015-06-18 18:07:37,382][INFO ][tribe ] [Manslaughter] [1003] adding node [[10.19.0.73][_S8ylz1OTv6Nyp1YoMRNGQ][esnode073.mweibo.bx.sinanode.com][inet[/10.19.0.73:9300]]{max_local_storage_nodes=1, tribe.name=1003, master=false}] [2015-06-18 18:07:37,391][INFO ][tribe ] [Manslaughter] [1003] adding node [[Manslaughter/1003][m1-cDaFTSoqqyC2iiQhECA][localhost.localdomain][inet[/10.19.0.100:9302]]{data=false, tribe.name=1003, client=true}] [2015-06-18 18:07:37,393][INFO ][tribe ] [Manslaughter] [1003] adding node [[10.19.0.97][_mIrWKzZTYifp1xshngBew][esnode054.mweibo.bx.sinanode.com][inet[/10.19.0.54:9300]]{max_local_storage_nodes=1, tribe.name=1003, master=false}] [2015-06-18 18:07:37,393][INFO ][tribe ] [Manslaughter] [1003] adding index [logstash-mweibo-vip-2015.06.15] [2015-06-18 18:07:37,394][INFO ][tribe ] [Manslaughter] [1003] adding index [logstash-php-2015.06.08] [2015-06-18 18:07:37,394][INFO ][tribe ] [Manslaughter] [1003] adding index [logstash-mweibo-vip-2015.06.16] [2015-06-18 18:07:37,395][INFO ][tribe ] [Manslaughter] [1003] adding index [.kibana] [2015-06-18 18:07:37,398][INFO ][tribe ] [Manslaughter] [1003] adding index [logstash-php-2015.06.14] [2015-06-18 18:07:37,403][INFO ][tribe ] [Manslaughter] [1003] adding index [logstash-mweibo-vip-2015.06.10] [2015-06-18 18:07:37,403][INFO ][tribe ] [Manslaughter] [1003] adding index [kibana-int] [2015-06-18 18:07:37,404][INFO ][tribe ] [Manslaughter] [1003] adding index [logstash-mweibo-2015.06.13] [2015-06-18 18:07:37,411][INFO ][cluster.service ] [Manslaughter] added {[10.19.0.73][_S8ylz1OTv6Nyp1YoMRNGQ][esnode073.mweibo.bx.sinanode.com][inet[/10.19.0.73:9300]]{max_local_storage_nodes=1, tribe.name=1003, master=false},[10.19.0.97][jnA-rt2fS_22Mz9nYl5Ueg][localhost.localdomain][inet[/10.19.0.97:9300]]{max_local_storage_nodes=1, tribe.name=1003, data=false, master=true},}, reason: cluster event from 1003, zen-disco-receive(from master [[10.19.0.97][jnA-rt2fS_22Mz9nYl5Ueg][localhost.localdomain][inet[/10.19.0.97:9300]]{max_local_storage_nodes=1, data=false, master=true}]) [2015-06-18 18:08:07,316][INFO ][cluster.service ] [Manslaughter/1002] detected_master [10.19.0.22][6qyQh9EURUyO7RBC_dXDow][localhost.localdomain][inet[/10.19.0.22:9300]]{max_local_storage_nodes=1, master=true}, added {[10.19.0.93][qAklY08iSsSfIf2vvu6Iyw][localhost.localdomain][inet[/10.19.0.93:9300]]{max_local_storage_nodes=1, master=false}]) [2015-06-18 18:08:07,350][INFO ][indices.breaker ] [Manslaughter/1002] Updating settings parent: [PARENT,type=PARENT,limit=259489792/247.4mb,overhead=1.0], fielddata: [FIELDDATA,type=MEMORY,limit=155693875/148.4mb,overhead=1.03], request: [REQUEST,type=MEMORY,limit=103795916/98.9mb,overhead=1.0] [2015-06-18 18:08:07,353][INFO ][tribe ] [Manslaughter] [1002] adding node [[10.19.0.93][qAklY08iSsSfIf2vvu6Iyw][localhost.localdomain][inet[/10.19.0.93:9300]]{max_local_storage_nodes=1, tribe.name=1002, master=false}] [2015-06-18 18:08:07,357][INFO ][tribe ] [Manslaughter] [1002] adding node [[Manslaughter/1002][4FPiRPh7TFyBk-BaPc_TLg][localhost.localdomain][inet[/10.19.0.100:9303]]{data=false, tribe.name=1002, client=true}] [2015-06-18 18:08:07,358][INFO ][tribe ] [Manslaughter] [1002] adding node [[10.19.0.22][tkrBsbnLTry0zzZEdbQR0A][localhost.localdomain][inet[/10.19.0.27:9300]]{max_local_storage_nodes=1, tribe.name=1002, master=false}] [2015-06-18 18:08:07,358][INFO ][tribe ] [Manslaughter] [1002] adding index [test.yingju1-mweibo_client_downstream_success-2015.06.07] [2015-06-18 18:08:07,363][INFO ][tribe ] [Manslaughter] [1002] adding index [logstash-mweibo_client_downstream_error-2015.06.02] [2015-06-18 18:08:07,366][INFO ][tribe ] [Manslaughter] [1002] adding index [.kibana_5601] [2015-06-18 18:08:07,377][INFO ][cluster.service ] [Manslaughter] added {[10.19.0.22][6qyQh9EURUyO7RBC_dXDow][localhost.localdomain][inet[/10.19.0.22:9300]]{max_local_storage_nodes=1, tribe.name=1002, master=false},[10.19.0.93][l7nkk-H7S6GvMzWwGe0_CA][localhost.localdomain][inet[/10.19.0.93:9300]]{max_local_storage_nodes=1, tribe.name=1002, master=false},}, reason: cluster event from 1002, zen-disco-receive(from master [[10.19.0.22][6qyQh9EURUyO7RBC_dXDow][localhost.localdomain][inet[/10.19.0.22:9300]]{max_local_storage_nodes=1, master=true}]) [2015-06-18 18:08:13,208][DEBUG][discovery.zen.publish ] [Manslaughter/1003] received cluster state version 782404 [2015-06-18 18:08:21,803][DEBUG][discovery.zen.publish ] [Manslaughter/1003] received cluster state version 782405 [2015-06-18 18:08:33,229][DEBUG][discovery.zen.publish ] [Manslaughter/1003] received cluster state version 782406 日志中可以明显看到,节点是如何分别连接上两个集群的。 最后,我们可以使用标准的 RESTful 接口来验证一下: ELKstack 中文指南 254多集群互联 # curl 10.19.0.100:9201/_cat/indices?v health status index pri rep docs.count docs.deleted store.size pri.store.size green open test.yingju1-mweibo_client_downstream_success-2015.06.07 20 1 40692459 0 154.1gb 77gb green open weibo-client-video-2015.06.19 5 1 0 0 970b 575b green open dpool-pc-weibo-2015.06.19 20 1 0 0 3.7kb 2.2kb green open logstash-video-2015.06.16 27 0 149015413 0 13.4gb 13.4gb 不同集群的索引,都可以通过 tribe node 访问到了。 ELKstack 中文指南 255多集群互联 alias的几点应用 本节作者:childe 索引更改名字时, 无缝过渡 情景1 用Logstash采集当前的所有nginx日志, 放入ES, 索引名叫nginx-YYYY.MM.DD. 后来又增加了apache日志, 希望能放在同一个索引里面,统一叫web-YYYY.MM.DD. 我们只要把Logstash配置更改一下,然后重启, 数据就会写入新的索引名字下. 但是同一天的索 引就会被分成了2个, kibana上面就不好配置了. 如此实现 1. 今天是2015.07.28. 我们为nginx-2015.07.28建一个alias叫做web-2015.07.28, 之前的所 有nginx日志也如此照做. 2. kibana中把dashboard配置的索引名改成web-YYYY.MM.DD 3. 将logstash里面的elasticsearch的配置改成web-YYYY.MM.DD, 重启. 4. 无缝切换实现. 情景2 用Logstash采集当前的所有nginx日志, 放入ES, 索引名叫nginx-YYYY.MM.DD. 某天(2015.07.28)希望能够按月建立索引, 索引名改成nginx-YYYY.MM. [注意] 像情景1中那样新建一个叫nginx-2015.07的alias, 并指向本月的其他的索引是不行的. 因为一个alias指向了多个索引, 写这个alias的时候, ES不可能知道写入哪个真正的索引. 如此实现 1. 新建索引nginx-2015.07, 以及他的alias: nginx-2015.07.29, nginx-2015.07.30 ... 等. 新建索引nginx-2015.08, 以及他的alias: nginx-2015.08.01, nginx-2015.08.02 ... 等. 2. 等到第二天, 将logstash配置更改为nginx-YYYY.MM, 重启. ELKstack 中文指南 256别名的应用 3. 如果索引只保留10天(一般来说, 不可能永久保存), 在10天之后的某天, 将kibana配置更改 为[nginx-]YYYY.MM 缺点 第二步, 第三步需要记得手工操作, 或者写一个crontab定时任务. 另外一种思路 1. 新建一个叫nginx-2015.07的alias, 并指向本月的其他的索引. 这个时候不能马上更改logstash配置写入nginx-2015.07. 因为一个alias指向了多个索引, 写这个alias的时候, ES不可能知道写入哪个真正的索引. 2. 新建索引nginx-2015.08, 以及他的alias: nginx-2015.08.01, nginx-2015.08.02 ... 等. 3. 把kibana配置改为[nginx-]YYYY.MM. 4. 到2015.08.01这天, 或者之后的某天, 更改logstash配置, 把elasticsearch的配置更改为 nginx-YYYY.MM. 重启. 缺点 1. 7月份的索引还是按天建立. 2. 第四步需要记得手工操作. 情景2的操作有些麻烦, 这些都是为了"无缝"这个大前提. 我们不希望用户(kibana的使用者)感 觉到任何变化, 更不能让使用者感受到数据有缺失. 如果用户愿意妥协, 那ELK的管理者就可以马上简单粗暴的把数据写到nginx-2015.07, 并把 kibana配置改过去. 这样一来, 用户可能就不能方便的看到之前的数据了. 按域名配置kibana的dashboard nginx日志中有个字段是domain, 各个业务部门在Kibana中需要看且只看自己域名下的日志. 可以在logstash中按域名切分索引,但ES的metadata会成倍的增长,带来极大的负担. 也可以放在nginx-YYYY.MM.DD一个索引中, 在kibana加一个filter过滤. 但这个关键的filter和其 它所有filter排在一起,是很容易很不小心清掉的. 我们可以在template中配置, 对每一个域名做一个alias. ELKstack 中文指南 257别名的应用 "aliases" : { "{index}-www.corp.com" : { "filter" : { "term" : { "domain" : "www.corp.com" } } }, "{index}-abc.corp.com" : { "filter" : { "term" : { "domain" : "abc.corp.com" } } } } 当新的索引nginx-2015.07.28生成的时候, 会有nginx-2015.07.28-www.corp.com和nginx- 2015.07.28-abc.corp.com等alias指向nginx-2015.07.28. 在kibana配置dashboard的时候, 就可以直接用alias做配置了. 在实际使用中, 可能还需要一个crontab定时的查询是否有新的域名加入, 自动对新域名做当天 的alias, 并把它加入template. ELKstack 中文指南 258别名的应用 映射与模板的定制 Elasticsearch 是一个 schema-less 的系统,但 schema-less 并不代表 no schema,而是 ES 会尽量根据 JSON 源数据的基础类型猜测你想要的字段类型映射。如果你对这种动态生成的 映射关系不满意,或者想要使用一些更高级的映射设置,那么就需要使用自定义映射。 创建和更新映射 正如上面所说,ES 可以随时根据数据中的新字段来创建新的映射关系。所以,我们也可以自 己在还没有正式数据写入之前,先创建一个基础的映射。等后续数据有其他字段时,ES 也一 样会自动处理。 映射的的创建方式如下: # curl -XPUT http://127.0.0.1:9200/logstash-2015.06.20/_mapping -d ' { "mappings": { "syslog" : { "properties" : { "@timestamp" : { "type" : "date" }, "message" : { "type" : "string" }, "pid" : { "type" : "long" } } } } }' 注意:对于已存在的映射,ES 的自动处理仅限于新字段出现。已经生成的字段映射,是不可 变更的。如果确实需要,请参阅之前的 reindex 接口小节,采用重新导入数据的方式完成。 而如果是新增一个字段映射的更新,那还是可以通过 /_mapping 接口直接完成的: ELKstack 中文指南 259映射与模板的定制 # curl -XPUT http://127.0.0.1:9200/logstash-2015.06.21/_mapping/syslog -d ' { "properties" : { "syslogtag" : { "type" : "string", "index": "not_analyzed" } } }' 没错,这里只需要单独写这个新字段的内容就够了。ES 会自动合并进去。 删除映射 删除数据并不代表会删除数据的映射。比如: # curl -XDELETE http://127.0.0.1:9200/logstash-2015.06.21/syslog 删除了索引下 syslog 的全部数据,但是 syslog 的映射还在。删除映射(同时也就删掉了数据) 的命令是: # curl -XDELETE http://127.0.0.1:9200/logstash-2015.06.21/_mapping/syslog 当然,如果删除整个索引,那映射也是同时被清除的。 核心类型 mapping 中主要就是针对字段设置类型以及类型相关参数。那么,我们首先来了解一下 Elasticsearch 支持的核心类型: 1. JSON 基础类型 2. 字符串: string 3. 数字: byte, short, integer, long, float, double 4. 时间: date 5. 布尔值: true, false 6. 数组: array 7. 对象: object 8. ES 独有类型 ELKstack 中文指南 260映射与模板的定制 9. 多重: multi 10. 经纬度: geo_point 11. 网络地址: ip 12. 堆叠对象: nested object 13. 二进制: binary 14. 附件: attachment 前面提到,ES 是根据收到的 JSON 数据里的类型来猜测的。所以,一个内容为 "123" 的数 据,猜测出来的类型应该是 string 而不是 long。除非这个字段已经有了确定为 long 的映射关 系,那么 ES 会尝试做一次转换。如果转换失败,这条数据写入就会报错。 注意: ES 的映射虽然有 index 和 type 两层关系,但是实际索引时是以 index 为基础的。如果同一 个 index 下不同 type 的字段出现 mapping 不一致的情况,虽然数据依然可以成功写入并生成 各自的 mapping,但实际 fielddata 中的索引结果却依然是以 index 内第一个 mapping 类型来 生成的。这种情况下可能会有比较奇怪的事情发生。比如看似 double 的数据实际存储成 long,导致数值比较的搜索结果异常。 从 Kibana4 开始,会在 Object Setting 页对该情况做出冲突预警;并预计在 ES 2.0 版本正式 拒绝这种冲突数据写入。 查看已有数据的映射 学习索引映射最直接的方式,就是查看已有数据索引的映射。我们用 logstash 写入 ES 的数 据,都会根据 logstash 自带的 template,生成一个很有学习意义的映射: ELKstack 中文指南 261映射与模板的定制 # curl -XGET http://127.0.0.1:9200/logstash-2015.06.16/_mapping/tweet { "gb": { "mappings": { "tweet": { "properties": { "date": { "type": "date", "format": "dateOptionalTime" }, "name": { "type": "string" }, "tweet": { "type": "string" }, "user_id": { "type": "long" } } } } } } 自定义字段映射 大家可以通过上面一个现存的映射发现其实所有的字段都有好几个属性,这些都是我们可以 自己定义修改的。除了已经看到的这些基本内容外,ES 还支持其他一些可能会比较常用的映 射属性: 全文索引还是精确索引 自定义分词器 自定义日期格式 精确索引 字段都有几个基本的映射选项,类型(type)和索引方式(index)。以字符串类型为例,index 有 三个可设置项: analyzed 默认选项,以标准的全文索引方式,分析字符串,完成索引。 not_analyzed 精确索引,不对字符串做分析,直接索引字段内数据的精确内容。 no 不索引该字段。 对于日志应用来说,很多字段都是不需要在 Elasticsearch 里做解析这步的,所以,我们可以 设置: ELKstack 中文指南 262映射与模板的定制 "myfieldname": { "type": "string", "index": "not_analyzed" } 时间格式 稍微见过 ELK Stack 示例的人,都对其中 @timestamp 字段的特殊格式有深刻的印象。这个 时间格式在 Nginx 中叫 $time_iso8601 ,在 Rsyslog 中叫 date-rfc3339 ,在 ES 中叫 dateOptionalTime 。但事实上,ES 完全可以接收其他时间格式作为时间字段的内容。对于 ES 来说,时间字段内容实际都是转换成 long 类型作为内部存储的。所以,接收段的时间格 式,可以任意配置: "@timestamp" : { "type" : "date" "index" : "not_analyzed", "doc_values" : true, "format" : "dd/MMM/YYYY:HH:mm:ss Z", } 而 ES 默认的时间字段格式,除了 dateOptionalTime 以外,还有一种,就是 UNIX_MS ,毫秒 级的 UNIX 时间戳。因为这个数值 ES 可以直接好不修改的存成内部实际的 long 数值。 多重索引 多重索引是 logstash 用户最习惯的一个映射,因为这是 logstash 默认设置开启的配置: "title": { "type": "string", "fields": { "raw": { "type": "string", "index": "not_analyzed" } } } 其作用是,在 title 字段数据写入的时候,ES 会自动生成两个字段,分别是 title 和 title.raw 。这样,在可能同时需要分词与不分词结果的环境下,就可以很灵活的使用不同 的索引字段了。比如,查看标题中最常用的单词,应该使用 title 字段;查看阅读数最多的 文章标题,应该使用 title.raw 字段。 注意:raw 这个名字你可以自己随意取。比如说,如果你绝大多数时候用的是精确索引,那么 你完全可以为了方便反过来定义: ELKstack 中文指南 263映射与模板的定制 "title": { "type": "string", "index": "not_analyzed", "fields": { "alz": { "type": "string" } } } 特殊字段 上面介绍的,都是对普通数据字段的一些常用设置。而实际上,ES 默认还有一些特殊字段, 在默默的发挥着作用。这些字段,统一以 _ 下划线开头。在之前 CRUD 章节中,我们就已 经看到一些,比如 _index , _type , _id 。默认不开启的还有 _ttl , _timestamp , _size , _parent 等。这里需要单独介绍两个,对我们索引和检索的 效果和性能,都有较大影响的: _all _all 里存储了各字段的数据内容。其作用是,在检索的时候,如果无法或者未指明具体搜 索哪个字段的数据,那么 ES 默认就会是从 _all 里去查找。 对于日志场景,如果你的日志划分出来的字段比较少且数目固定。那么,完全可以关闭掉 _all 功能,节省这部分 IO 和 CPU。 "_all" : { "enabled" : false } _source _source 里存储了该条记录的 JSON 源数据内容。这部分内容只是按照 ES 接收到的内容原 样存储下来,并不经过索引过程。对于 ES 的请求过程来说,它不参与 Query 阶段,而只用 于 Fetch 阶段。我们在 GET 或者 /_search 时看到的数据内容,都是从 _source 里获取到 的。 所以,虽然 _source 也重复了一遍索引中的数据,一般我们并不建议关闭这个功能。因为一 旦关闭,你搜索的结果除了一个 _id ,啥都看不到。对于日志场景,意义不是很大。 当然,也有少数场景是可以关闭 _source 的: 1. 把 ES 作为时间序列数据库使用,只要聚合统计结果,不要源数据内容。 2. 把 ES 作为纯检索工具使用, _id 对应的内容在 HDFS 上另外存储,搜索后使用所得 ELKstack 中文指南 264映射与模板的定制 _id 去 HDFS 上读取内容。 动态模板映射 不想使用默认识别的结果,单独设置一个字段的映射的方法,上面已经介绍完毕。那么,如 果你有一类相似的数据字段,想要统一设置其映射,就可以用到下一项功能:动态模板映射 ( dynamic_templates )。 "_default_" : { "dynamic_templates" : [ { "message_field" : { "mapping" : { "index" : "analyzed", "omit_norms" : true, "store" : false, "type" : "string" }, "match" : "*msg", "match_mapping_type" : "string" } }, { "string_fields" : { "mapping" : { "index" : "not_analyzed", "ignore_above" : 256, "store" : false, "doc_values" : true, "type" : "string" }, "match" : "*", "match_mapping_type" : "string" } } ], "properties" : { } } 这样,只要字符串类型字段名以 msg 结尾的,都会经过全文索引,其他字符串字段则进行精 确索引。同理,还可以继续书写其他类型( long , float , date 等)的 match_mapping_type 和 match 。 索引模板 对每个希望自定义映射的索引,都要定时提前通过发送 PUT 请求的方式创建索引的话,未免 太过麻烦。ES 对此设计了索引模板功能。我们可以针对同一类索引,定制相同的模板。 ELKstack 中文指南 265映射与模板的定制 模板中的内容包括两大类,setting(设置)和 mapping(映射)。setting 部分,多为在 elasticsearch.yml 中可以设置全局配置的部分,而 mapping 部分,则是这节之前介绍的内 容。 如下为定义所有以 te 开头的索引的模板: # curl -XPUT http://localhost:9200/_template/template_1 -d ' { "template" : "te*", "settings" : { "number_of_shards" : 1 }, "mappings" : { "type1" : { "_source" : { "enabled" : false } } } }' 同时,索引模板是有序合并的。如果我们在同一类索引里,又想单独修改某一小类索引的一 两处单独设置,可以再累加一层模板: # curl -XPUT http://localhost:9200/_template/template_2 -d ' { "order" : 1, "template" : "tete*", "settings" : { "number_of_shards" : 2 }, "mappings" : { "type1" : { "_all" : { "enabled" : false } } } }' 默认的 order 是 0,那么新创建的 order 为 1 的 template_2 在合并时优先级大于 template_1。最终,对 tete*/type1 的索引模板效果相当于: ELKstack 中文指南 266映射与模板的定制 { "settings" : { "number_of_shards" : 2 }, "mappings" : { "type1" : { "_source" : { "enabled" : false }, "_all" : { "enabled" : false } } } } ELKstack 中文指南 267映射与模板的定制 Puppet 自动部署 Elasticsearch Elasticsearch 作为一个 Java 应用,本身的部署已经非常简单了。不过作为生产环境,还是有 必要采用一些更标准化的方式进行集群的管理。Elasticsearch 官方提供并推荐使用 Puppet 方式部署和管理。其 Puppet 模块源码地址见: https://github.com/elastic/puppet-elasticsearch 安装方法 和其他标准 Puppet Module 一样,puppet-elasticsearch 也可以通过 Puppet Forge 直接安 装: # puppet module install elasticsearch-elasticsearch 配置示例 安装好 Puppet 模块后,就可以使用了。模块提供几种 Puppet 资源,主要用法如下: class { 'elasticsearch': version => '1.5.2', config => { 'cluster.name' => 'es1003' }, java_install => true, } elasticsearch::instance { $fqdn: config => { 'node.name' => $fqdn }, init_defaults => { 'ES_USER' => 'elasticsearch', 'ES_HEAP_SIZE' => $memorysize > 64 ? '31g' : $memorysize / 2 }, datadir => [ '/data1/elasticsearch' ], } elasticsearch::template { 'templatename': host => $::ipaddress, port => 9200, content => '{"template":"*","settings":{"number_of_replicas":0}}' } 示例中展示了三种资源: class: 配置具体安装的 Elasticsearch 软件版本,全集群公用的一些基础配置项。注意, puppet-elasticsearch 模块默认并不负责 Java 的安装,它只是调用操作系统对应的 Yum,Apt 工具,而 elasticsearch.rpm 或者 elasticsearch.deb 本身没有定义其他依赖(因 ELKstack 中文指南 268puppet-elasticsearch模块的使用 为 java 版本太多了,定义起来不方便)。所以,如果依然要走 puppet-elasticsearch 配置 来安装 Java 的话,需要额外开启 java_install 选项。该选项会使用另一个 Puppet Module —— puppetlabs-java 来安装,默认安装的是 jdk。如果你要节省空间,可以再加 一行 java_package 来明确指定软件全名。 instance: 配置具体单个进程实例的配置。其中 config 和 init_defaults 选项在 class 和 instance 资源中都可以定义,实际运行时,会自动做一次合并,当然,instance 里的 配置优先级高于 class 中的配置。此外,最重要的定义是数据目录的位置。有多快磁盘 的,可以在这里定义一个数组。 template: 模板是 Elasticsearch 创建索引映射和设置时的预定义方式。一般可以通过在 config/templates/ 目录下放置 JSON 文件,或者通过 RESTful API 上传配置两种方式 管理。而这里,单独提供了 template 资源,通过 puppet 来管理模板。 content 选项中 直接填入模板内容,或者使用 file 选项读取文件均可。 事实上,模块还提供了 plugin 和 script 资源管理这两方面的内容。考虑在 ELK 中,二者用的 不是很多,本节就不单独介绍了。想了解的读者可以参考官方文档。 ELKstack 中文指南 269puppet-elasticsearch模块的使用 集群版本升级 Elasticsearch 作为一个新兴项目,版本更新非常快。而且每次版本更新都或多或少带有一些 重要的性能优化、稳定性提升等特性。可以说,ES 集群的版本升级,是目前 ES 运维必然要 做的一项工作。 按照 ES 官方设计,有 restart upgrade 和 rolling upgrade 两种可选的升级方式。对于 1.0 版 本以上的用户,推荐采用 rolling upgreade 方式。 但是,对于主要负载是数据写入的 ELK Stack 场景来说,却并不是这样! rolling upgrade 的步骤大致如下: 1. 暂停分片分配; 2. 单节点下线升级重启; 3. 开启分片分配; 4. 等待集群状态变绿后继续上述步骤。 实际运行中,步骤 2 的 ES 单节点从 restart 到加入集群,大概要 100s 左右的时间。也就是 说,这 100s 内,该节点上的所有分片都是 unassigned 状态。而按照 Elasticsearch 的设计, 数据写入需要至少达到 replica/2+1 个分片完成才能算完成。也就意味着你所有索引都必须 至少有 1 个以上副本分片开启。 但事实上,很多日志场景,由于写入性能上的要求要高于数据可靠性的要求,大家普遍减小 了副本数量,甚至直接关掉副本复制。这样一来,整个 rolling upgrade 期间,数据写入就会 受到严重影响,完全丧失了 rolling 的必要性。 其次,步骤 3 中的 ES 分片均衡过程中,由于 ES 的副本分片数据都需要从主分片走网络复制 重新传输一次,而由于重启,新升级的节点上的分片肯定全是副本分片(除非压根没副本)。在 数据量较大的情况下,这个步骤耗时可能是几十分钟甚至以小时计。而且并发和限速上稍微 不注意,可能导致分片均衡的带宽直接占满网卡,正常写入也还是受到影响。 所以,对于写入压力较大,数据可靠性要求偏低的实时日志场景,依然建议大家进行主动停 机式的 restart upgrade。 restart upgrade 的步骤如下: 1. 首先适当加大集群的数据恢复和分片均衡并发度以及磁盘限速: ELKstack 中文指南 270计划内停机升级的操作流程 # curl -XPUT http://127.0.0.1:9200/_cluster/settings -d '{ "persistent" : { "cluster" : { "routing" : { "allocation" : { "disable_allocation" : "false", "cluster_concurrent_rebalance" : "5", "node_concurrent_recoveries" : "5", "enable" : "all" } } }, "indices" : { "recovery" : { "concurrent_streams" : "30", "max_bytes_per_sec" : "2gb" } } }, "transient" : { "cluster" : { "routing" : { "allocation" : { "enable" : "all" } } } } }' 1. 暂停分片分配: # curl -XPUT http://127.0.0.1:9200/_cluster/settings -d '{ "transient" : { "cluster.routing.allocation.enable" : "none" } }' 1. 通过配置管理工具下发新版本软件包。 2. 公告周知后,停止数据写入进程(即 logstash indexer 等) 3. 如果使用 Elasticsearch 1.6 版本以上,可以手动运行一次 synced flush,同步副本分片 的 commit id,缩小恢复时的网络传输带宽: # curl -XPOST http://127.0.0.1:9200/_flush/synced 1. 全集群统一停止进程,更新软件包,重新启动。 2. 等待各节点都加入到集群以后,恢复分片分配: ELKstack 中文指南 271计划内停机升级的操作流程 # curl -XPUT http://127.0.0.1:9200/_cluster/settings -d '{ "transient" : { "cluster.routing.allocation.enable" : "all" } }' 由于同时启停,主分片几乎可以同时本地恢复,整个集群从 red 变成 yellow 只需要 2 分钟左 右。而后的副本分片,如果有 synced flush,同样本地恢复,否则网络恢复总耗时,视数据大 小而定,会明显大于单节点恢复的耗时。 1. 如果有 synced flush,建议等待集群变成 green 状态后,恢复写入;否则在集群变成 yellow 状态之后,即可着手开始恢复数据写入进程。 ELKstack 中文指南 272计划内停机升级的操作流程 镜像备份 本节作者:李宏旭 大多数公司在使用 Elasticsearch 之前,都已经维护有一套 Hadoop 系统。因此,在实时数据 慢慢变得冷却,不再被经常使用的时候,一个需求自然而然的就出现了:怎么把 Elasticsearch 索引数据快速转移到 HDFS 上,以解决 Elasticsearch 上的磁盘空间;而在我 们需要的时候,又可以较快的从 HDFS 上把索引恢复回来继续使用呢? Elasticsearch 为此提供了 snapshot 接口。通过这个接口,我们可以快速导入导出索引镜像到 本地磁盘,网络磁盘,当然也包括 HDFS。 HDFS 插件安装配置 下载repository-hdfs插件,通过标准的 elasticsearch plugin 安装命令安装: bin/plugin install elasticsearch/elasticsearch-repository-hdfs/2.2.0 然后在 elasticsearch.yml 中增加以下配置: # repository 配置 hdfs: uri:"hdfs://:"(默认port为8020) #Hadoop file-system URI path:"some/path" #path with the file-system where data is stored/loaded conf.hdfs_config:"/hadoop/hadoop-2.5.2/etc/hadoop/hdfs-site.xml" conf.hadoop_config:"/hadoop/hadoop-2.5.2/etc/hadoop/core-site.xml" load_defaults:"true" #whether to load the default Hadoop configuration (default) or not compress:"false" # optional - whether to compress the metadata or not (default) chunk_size:"10mb" # optional - chunk size (disabled by default) # 禁用 jsm security.manager.enabled: false 默认情况下,Elasticsearch 为了安全考虑会在运行 JVM 的时候执行 JSM。出于 Hadoop 和 HDFS 客户端权限问题,所以需要禁用 JSM。将 elasticsearch.yml 中的 security.manager.enabled 设置为 false 。 ELKstack 中文指南 273镜像备份 将插件安装好,配置修改完毕后,需要重启 Elasticsearch 服务。没有重启节点插件可能会执 行失败。 注意:Elasticsearch 集群的每个节点都要执行以上步骤! Hadoop 配置 本节内容基于Hadoop版本:2.5.2,假定其配置文件目录:hadoop-2.5.2/etc/Hadoop。注 意,安装hadoop集群需要建立主机互信,互信方法请自行查询,很简单。 相关配置文件如下: mapred-site.xml.template 默认没有 mapred-site.xml 文件,复制 mapred-site.xml.template 一份,并把名字改为 mapred-site.xml,需要修改 3 处的 IP 为本机地址: mapreduce.framework.name yarn mapreduce.jobtracker.http.address XX.XX.XX.XX:50030 mapreduce.jobhistory.address XX.XX.XX.XX:10020 mapreduce.jobhistory.webapp.address XX.XX.XX.XX:19888 yarn-site.xml 需要修改5处的IP为本机地址: ELKstack 中文指南 274镜像备份 yarn.nodemanager.aux-services mapreduce_shuffle yarn.resourcemanager.address XX.XX.XX.XX:8032 yarn.resourcemanager.scheduler.address XX.XX.XX.XX:8030 yarn.resourcemanager.resource-tracker.address XX.XX.XX.XX:8031 yarn.resourcemanager.admin.address XX.XX.XX.XX:8033 yarn.resourcemanager.webapp.address XX.XX.XX.XX:8088 hadoop-env.sh 修改 jdk 路径和 jvm 内存配置,内存使用根据情况配置。 export JAVA_HOME=/usr/java/jdk1.7.0_79 export HADOOP_PORTMAP_OPTS="-Xmx512m $HADOOP_PORTMAP_OPTS" export HADOOP_CLIENT_OPTS="-Xmx512m $HADOOP_CLIENT_OPTS" core-site.xml 临时目录及 hdfs 机器 IP 端口指定: hadoop.tmp.dir /soft/hadoop-2.5.2/tmp Abase for other temporary directories. fs.defaultFS hdfs:// XX.XX.XX.XX:9000 io.file.buffer.size 4096 slaves 配置集群 IP 地址,集群有几个 IP 都要配置进去: ELKstack 中文指南 275镜像备份 192.168.0.2 192.168.0.3 192.168.0.4 hdfs-site.xml namenode 和 datenode 数据存放路径及别名: dfs.namenode.name.dir /data01/hadoop/name dfs.datanode.data.dir /data01/hadoop/data dfs.replication 1 启动 Hadoop 格式化完成后也可使用 sbin/start-all.sh 启动,但有可能出现异常,建议按照顺序分开启 动。 1. 首先需要格式化存储: bin/Hadoop namenode –format 2. 启动start-dfs.sh sbin/start-dfs.sh 3. 启动start-yarn.sh sbin/start-yarn.sh 备份导出 创建快照仓库 ELKstack 中文指南 276镜像备份 curl -XPUT 'localhost:9200/_snapshot/backup' -d '{ "type":"hdfs", "settings":{ "path":"/test/repo", "uri":"hdfs://:" } }' 在这步可能会报错。通常是因为 hadoop 配置问题,更改好配置需要重新格式化文件系统: 在 hadoop 目录下执行 bin/hadoop namenode -format 索引快照 执行索引快照命令,可写入crontab,定时执行 curl -XPUT 'http://localhost:9200/_snapshot/backup/snapshot_1' -d '{"indices":"indices_01,indices_02"}' 备份恢复 curl -XPOST "localhost:9200/_snapshot/backup/snapshot_1/_restore" 备份删除 curl -XDELETE "localhost:9200/_snapshot/backup/snapshot_1" 查看仓库信息 curl -XGET 'http://localhost:9200/_snapshot/backup?pretty' ELKstack 中文指南 277镜像备份 spark streaming 交互 Apache Spark 是一个高性能集群计算框架,其中 Spark Streaming 作为实时批处理组件,因 为其简单易上手的特性深受喜爱。在 es-hadoop 2.1.0 版本之后,也新增了对 Spark 的支 持,使得结合 ES 和 Spark 成为可能。 目前最新版本的 es-hadoop 是 2.1.0-Beta4。安装如下: wget http://d3kbcqa49mib13.cloudfront.net/spark-1.0.2-bin-cdh4.tgz wget http://download.elasticsearch.org/hadoop/elasticsearch-hadoop-2.1.0.Beta4.zip 然后通过 ADD_JARS=../elasticsearch-hadoop-2.1.0.Beta4/dist/elasticsearch-spark_2.10- 2.1.0.Beta4.jar 环境变量,把对应的 jar 包加入 Spark 的 jar 环境中。 下面是一段使用 spark streaming 接收 kafka 消息队列,然后写入 ES 的配置: ELKstack 中文指南 278spark streaming交互 import org.apache.spark._ import org.apache.spark.streaming.kafka.KafkaUtils import org.apache.spark.streaming._ import org.apache.spark.streaming.StreamingContext._ import org.apache.spark.SparkContext import org.apache.spark.SparkContext._ import org.apache.spark.SparkConf import org.apache.spark.sql._ import org.elasticsearch.spark.sql._ import org.apache.spark.storage.StorageLevel import org.apache.spark.Logging import org.apache.log4j.{Level, Logger} object Elastic { def main(args: Array[String]) { val numThreads = 1 val zookeeperQuorum = "localhost:2181" val groupId = "test" val topic = Array("test").map((_, numThreads)).toMap val elasticResource = "apps/blog" val sc = new SparkConf() .setMaster("local[*]") .setAppName("Elastic Search Indexer App") sc.set("es.index.auto.create", "true") val ssc = new StreamingContext(sc, Seconds(10)) ssc.checkpoint("checkpoint") val logs = KafkaUtils.createStream(ssc, zookeeperQuorum, groupId, topic, StorageLevel.MEMORY_AND_DISK_SER) .map(_._2) logs.foreachRDD { rdd => val sc = rdd.context val sqlContext = new SQLContext(sc) val log = sqlContext.jsonRDD(rdd) log.saveToEs(elasticResource) } ssc.start() ssc.awaitTermination() } } 注意,代码中使用了 spark SQL 提供的 jsonRDD() 方法,如果在对应的 kafka topic 里的数 据,本身并不是已经处理好了的 JSON 数据的话,这里还需要自己写一写额外的处理函数, 利用 cast class 来规范数据。 ELKstack 中文指南 279spark streaming交互 ELKstack 中文指南 280spark streaming交互 Shield 权限管理 本节作者:cameluo Shield 是 Elastic 公司官方发布的权限管理产品。其主要特性包括: 提供集群节点身份验证和集群数据访问身份验证 提供基于身份角色的细粒度资源和行为访问控制,细到索引级别的读写控制 提供节点间数据传输通道加密保护输出传输安全 提供审计功能 以插件的形式发布 License管理策略 Shield 是一款商业产品,不过提供 30 天免费试用,使用期间是全功能的。过期后 Shield 将 会在降级模式下工作,此模式下对 cluster health、cluster stats 以及 index stats 等接口的访 问将被阻止无法使用。 Shield架构 用户认证: Shield 通过定义一套用户集合来认证用户,采用抽象的域方式定义用户集合,支持本地用户 (esusers 域)和 LDAP 用户(含 AD)。 Shield 提供工具 ./bin/shield/esusers 用于创建和管理本地用户。 集成 LDAP 认证支持映射 LDAP 安全组到 Shield 角色,LDAP 安全组与 Shield 角色可以 是多对多的关系。 Shield 支持定义多个认证域,采用order字段进行优先级排序。如一个本地域 esusers, order=1,加一个 LDAP 域,order=2。如果用户不再本地用户域中则在 LDAP 域中查找验 证。 ./config/shield/roles.yml 文件中定义角色和角色的所拥有的权限。 ./config/shield/group_to_role_mapping.yml 文件中定义 LDAP 组到角色映射关系。 节点认证与通道加密: ELKstack 中文指南 281Shield 使用SSL/TLS证书进行相互认证和通讯加密。加密是可选配置。如果不使用,shield 节点之间 可以进行简单的密码验证(明文传输)。 授权: Shield 采用 RBAC 授权模型,数据模型包含如下元素: 受保护资源(Secured Resource):控制用户访问的客体,包括 cluster、index/alias 等 等。 权能(Priviliege):用户可以对受保护资源执行的一种或多种操作,如 read, write 等。 许可(Permissions):对被保护的资源拥有的一个或多个权能,如 read on the "products" index。 角色(Role):命名的一组许可。 用户(Users):用户实体,可以被赋予 0 个或多种角色,授权他们对被保护的资源执行各 种权能。 审计: 增加认证尝试、授权失败等安全相关事件和活动日志。 安装 1. 安装 License 和 Shield 插件 bin/plugin -i elasticsearch/license/latest bin/plugin -i elasticsearch/shield/latest 注意:初次运行 Shield 需要重新启动 ES 集群。后续更新 License(license.json 为 License 文 件)就可以在线运行: # curl -XPUT -u admin 'http://127.0.0.1:9200/_licenses' -d @license.json 1. 创建本地管理员: ./bin/shield/esusers useradd esadmin -r admin 配置 ELKstack 中文指南 282Shield 这里使用简单的配置先完成基本验证:使用纯本地用户认证或者使用本地认证 + 基本的 ldap 认证。 ES 配置 在elasticsearch.yml中增加如下配置: hostname_verification: false #shield.ssl.keystore.path: /app/elasticsearch/node01.jks #shield.ssl.keystore.password: xxxxxx shield: authc: realms: default: type: esusers order: 1 ldaprealm: type: ldap order: 2 url: "ldap://ldap.example.com:389" bind_dn: "uid=ldapuser, ou=users, o=services, dc=example, dc=com" bind_password: changeme user_search: base_dn: "dc=example,dc=com" attribute: uid group_search: base_dn: "dc=example,dc=com" files: role_mapping: "/app/elasticsearch/shield/group_to_role_mapping.yml" unmapped_groups_as_roles: false 角色配置 根据默认配置文件增减角色和访问控制权限。角色配置文件可以在线修改,保存后立即生 效: ([INFO ][shield.authz.store ] [Winky Man] updated roles (roles file [/opt/elasticsearch/config/shield/roles.yml] changed)) 注意:如果需要集成kibana认证,用户角色也需要有访问'.kibana'索引的访问权限和 cluster:monitor/nodes/info的访问权限,具体参照kibana4角色中的定义,否则用户通过kibana 认证后仍然无法访问到数据索引 用户组与角色映射配置 ELKstack 中文指南 283Shield 根据默认配置文件增减用户、用户组与角色配置中定义角色的映射关系,可以灵活实现各种 需求。LDAP组仅支持安全组,不支持动态组。这个配置文件可以在线修改,保存后立即生 效: ([INFO ][shield.authc.ldap.support] [Vishanti] role mappings file [/opt/elasticsearch/config/shield/group_to_role_mapping.yml] changed for realm [ldap/ldaprealm]. updating mappings...) 测试 curl -u username http://127.0.0.1:9200/ ELKstack 中文指南 284Shield Searchguard的部署与权限策略配置 本节作者:elain 背景 当前es正在被各大互联网公司大量的使用,但目前安全方面还没有一个很成熟的方案,大部 门都没有做安全认证或基于自身场景自己开发,没有一个好的开源方案 es官方推出了shield 认证,试用了一番,很是方便,功能强大,文档也较全面,但最大的问题是收费的,我相信 中国很多公司都不愿去花钱使用,所以随后在github 中找到了search-guard项目,接下来我们 一起来了解并部署此项目到我们的ES环境中。 注:目前此项目只支持到1.6以下的es,1.7 还未支持,所以,我们在ES1.6下来部署此项目 软件版本: ES 1.6.0 kibana 4.0.2 CentOS 6.3 功能特性: 基于用户与角色的权限控制 支持SSL/TLS方式安全认证 支持LDAP认证 支持最新的kibana4 更多特性见官网介绍 官网地址:http://floragunn.com/searchguard 目标: 实现用户访问es中日志需要登陆授权,不同用户访问不同索引,不授权的索引无法查看,分 组控制不同rd查看各自业务的日志, ELKstack 中文指南 285Search-Guard 部署 download maven: axel -n 10 http://mirror.bit.edu.cn/apache/maven/maven-3/3.3.3/binaries/apache-maven-3.3.3-bin.tar.gz tar zxvf apache-maven-3.3.3-bin.tar.gz cd apache-maven-3.3.3/ #git search-guard and build git clone -b es1.6 https://github.com/floragunncom/search-guard.git cd search-guard ;/home/work/app/maven/bin/mvn package -DskipTests #把编译好的包放到一个下载地址(方便于es集群使用,单台测试可不使用这种方案): http://www.elain.org/dl/search-guard-16-0.6-SNAPSHOT.zip 在es上以插件方式安装编译好的包 cd /home/work/app/elasticsearch/plugins/ ./bin/plugin -u http://www.elain.org/dl/search-guard-16-0.6-SNAPSHOT.zip -i search-guard elasticsearch.yml 添加 ELKstack 中文指南 286Search-Guard #################search-guard################### searchguard.enabled: true searchguard.key_path: /home/work/app/elasticsearch/keys searchguard.auditlog.enabled: true searchguard.allow_all_from_loopback: true #本地调试可打开,建议在线上关闭 searchguard.check_for_root: false searchguard.http.enable_sessions: true #配置认证方式 searchguard.authentication.authentication_backend.impl: com.floragunn.searchguard.authentication.backend.simple.SettingsBasedAuthenticationBackend searchguard.authentication.authorizer.impl: com.floragunn.searchguard.authorization.simple.SettingsBasedAuthorizator searchguard.authentication.http_authenticator.impl: com.floragunn.searchguard.authentication.http.basic.HTTPBasicAuthenticator #配置用户名和密码 searchguard.authentication.settingsdb.user.admin: admin searchguard.authentication.settingsdb.user.user1: 123 searchguard.authentication.settingsdb.user.user2: 123 #配置用户角色 searchguard.authentication.authorization.settingsdb.roles.admin: ["root"] searchguard.authentication.authorization.settingsdb.roles.user1: ["user1"] searchguard.authentication.authorization.settingsdb.roles.user2: ["user2"] #配置角色权限(只读) searchguard.actionrequestfilter.names: ["readonly","deny"] searchguard.actionrequestfilter.readonly.allowed_actions: ["indices:data/read/*", "indices:admin/exists","indices:admin/mappings/*","indices:admin/validate/query"] searchguard.actionrequestfilter.readonly.forbidden_actions: ["indices:data/write/*"] #配置角色权限(禁止访问) searchguard.actionrequestfilter.deny.allowed_actions: [] searchguard.actionrequestfilter.deny.forbidden_actions: ["indices:data/write/*"] #################search-guard################### logging.yml 添加 logger.com.floragunn: DEBUG #开启debug,方便调试 创建key echo 'be226fd1e6ddc74b' >/home/work/app/elasticsearch/keys/searchguard.key 重启elasticsearch 配置权限策略如下 : ELKstack 中文指南 287Search-Guard curl -XPUT 'http://localhost:9200/searchguard/ac/ac?pretty' -d ' {"acl": [ { "__Comment__": "Default is to execute all filters", "filters_bypass": [], "filters_execute": ["actionrequestfilter.deny"] }, //默认禁止访问 { "__Comment__": "This means that every requestor (regardless of the requestors hostname and username) which has the root role can do anything", "roles": [ "root" ], "filters_bypass": ["*"], "filters_execute": [] }, // root角色完全权限 { "__Comment__": "This means that for the user spock on index popstuff only the actionrequestfilter.readonly will be executed, no other", "users": ["user1"], "indices": ["index1-*","index2-*",".kibana"], "filters_bypass": ["actionrequestfilter.deny"], "filters_execute": ["actionrequestfilter.readonly"] }, //user1 用户只能访问index1-*,index2-* 索引,且只有只读权限 { "__Comment__": "This means that for the user spock on index popstuff only the actionrequestfilter.readonly will be executed, no other", "users": ["user2"], "indices": ["index3-*",".kibana"], "filters_bypass": ["actionrequestfilter.deny"], "filters_execute": ["actionrequestfilter.readonly"] } //user2 用户只能访问index3-* 索引,且只有只读权限 ]}} 查看策略 curl -XGET -uadmin:admin http://localhost:9200/searchguard/ac/ac 注:以上是我自己使用的策略,方便于不同用户访问不同索引,不授权的索引无法查看,分 组控制不同rd查看各自业务的日志 更多策略见:https://github.com/floragunncom/search- guard/blob/es1.6/searchguard_config_example_1.yml 更多配置与功能见:https://github.com/floragunncom/search- guard/blob/es1.6/searchguard_config_template.yml ELKstack 中文指南 288Search-Guard 监控方案 Elasticsearch 作为一个分布式系统,监控自然是重中之重。Elasticsearch 本身提供了非常完 善的,由浅及深的各种性能数据接口。和数据读写检索接口一样,采用 RESTful 风格。我们 可以直接使用 curl 来获取数据,编写监控程序,也可以使用一些现成的监控方案。通常这些 方案也是通过接口读取数据,解析 JSON,渲染界面。 本章会先介绍一些常用的监控接口,以及其响应数据的含义。然后再介绍几种常用的开源和 商业 Elasticsearch 监控产品。 ELKstack 中文指南 289监控方案 集群健康状态监控 说到 Elasticsearch 集群监控,首先我们肯定是需要一个从总体意义上的概要。不管是多大规 模的集群,告诉我正常还是不正常?没错,集群健康状态接口就是用来回答这个问题的,而 且这个接口的信息已经出于意料的丰富了。 命令示例 # curl -XGET 127.0.0.1:9200/_cluster/health?pretty { "cluster_name" : "es1003", "status" : "green", "timed_out" : false, "number_of_nodes" : 38, "number_of_data_nodes" : 27, "active_primary_shards" : 1332, "active_shards" : 2381, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "number_of_pending_tasks" : 0 } 状态信息 输出里最重要的就是 status 这行。很多开源的 ES 监控脚本,其实就是拿这行数据做报警判 断。status 有三个可能的值: green 绿灯,所有分片都正确运行,集群非常健康。 yellow 黄灯,所有主分片都正确运行,但是有副本分片缺失。这种情况意味着 ES 当前 还是正常运行的,但是有一定风险。注意,在 Kibana4 的 server 端启动逻辑中,即使是 黄灯状态,Kibana 4 也会拒绝启动,死循环等待集群状态变成绿灯后才能继续运行。 red 红灯,有主分片缺失。这部分数据完全不可用。而考虑到 ES 在写入端是简单的取余 算法,轮到这个分片上的数据也会持续写入报错。 对 Nagios 熟悉的读者,可以直接将这个红黄绿灯对应上 Nagios 体系中的 Critical, Warning,OK 。 其他数据解释 ELKstack 中文指南 290监控方案 number_of_nodes 集群内的总节点数。 number_of_data_nodes 集群内的总数据节点数。 active_primary_shards 集群内所有索引的主分片总数。 active_shards 集群内所有索引的分片总数。 relocating_shards 正在迁移中的分片数。 initializing_shards 正在初始化的分片数。 unassigned_shards 未分配到具体节点上的分片数。 显然,后面三项在正常情况下,一般都应该是 0。但是如果真的出来了长期非 0 的情况,怎 么才能知道这些长期 unassign 或者 initialize 的分片影响的是哪个索引呢?本书随后还有有更 多接口获取相关信息。不过在集群健康这层,本身就可以得到更详细一点的内容了。 level 请求参数 接口请求的时候,可以附加一个 level 参数,指定输出信息以 indices 还是 shards 级别显示。 当然,一般来说,indices 级别就够了。 ELKstack 中文指南 291监控方案 # curl -XGET http://127.0.0.1:9200/_cluster/health?level=indices { "cluster_name": "es1003", "status": "red", "timed_out": false, "number_of_nodes": 38, "number_of_data_nodes": 27, "active_primary_shards": 1332, "active_shards": 2380, "relocating_shards": 0, "initializing_shards": 0, "unassigned_shards": 1 "indices": { "logstash-2015.05.31": { "status": "green", "number_of_shards": 81, "number_of_replicas": 0, "active_primary_shards": 81, "active_shards": 81, "relocating_shards": 0, "initializing_shards": 0, "unassigned_shards": 0 }, "logstash-2015.05.30": { "status": "red", "number_of_shards": 81, "number_of_replicas": 0, "active_primary_shards": 80, "active_shards": 80, "relocating_shards": 0, "initializing_shards": 0, "unassigned_shards": 1 }, ... } } 这就看到了,是 logstash-2015.05.30 索引里,有一个分片一直未能成功分配,导致集群状态 异常的。 不过,一般来说,集群健康接口,还是只用来简单监控一下集群状态是否正常。一旦收到异 常报警,具体确定 unassign shard 的情况,更推荐使用 kopf 工具在页面查看。 ELKstack 中文指南 292监控方案 节点状态监控接口 集群状态是从最上层高度来评估你的集群概况,而节点状态则更底层一些,会返回给你集群 里每个节点的统计信息。这个接口的信息极为丰富,从硬件到数据到线程,应有尽有。本节 会以单节点为例,分段介绍各部分数据的含义。 首先,通过如下命令获取节点状态: # curl -XGET 127.0.0.1:9200/_nodes/stats 节点概要 返回数据的第一部分是节点概要,主要就是节点的主机名,网卡地址和监听端口等。这部分 内容除了极少数时候(一个主机上运行了多个 ES 节点)一般没有太大用途。 { "cluster_name": "elasticsearch_zach", "nodes": { "UNr6ZMf5Qk-YCPA_L18BOQ": { "timestamp": 1408474151742, "name": "Zach", "transport_address": "inet[zacharys-air/192.168.1.131:9300]", "host": "zacharys-air", "ip": [ "inet[zacharys-air/192.168.1.131:9300]", "NONE" ], ... 索引信息 这部分内容会列出该节点上存储的所有索引数据的状态统计。 1. 首先是概要: ELKstack 中文指南 293监控方案 "indices": { "docs": { "count": 6163666, "deleted": 0 }, "store": { "size_in_bytes": 2301398179, "throttle_time_in_millis": 122850 }, docs.count 是节点上存储的数据条目总数;store.size_in_bytes 是节点上存储的数据占用磁 盘的实际大小。而 store.throttle_time_in_millis 则是 ES 进程在做 segment merge 时出现磁 盘限速的时长。如果你在 ES 的日志里经常会看到限速声明,那么这里的数值也会偏大。 1. 写入性能: "indexing": { "index_total": 803441, "index_time_in_millis": 367654, "index_current": 99, "delete_total": 0, "delete_time_in_millis": 0, "delete_current": 0 }, indexing.index_total 是一个递增累计数,表示节点完成的数据写入总次数。至于后面又删除 了多少,额外记录在 indexing.delete_total 里。 1. 读取性能: "get": { "total": 6, "time_in_millis": 2, "exists_total": 5, "exists_time_in_millis": 2, "missing_total": 1, "missing_time_in_millis": 0, "current": 0 }, get 这里显示的是直接使用 _id 读取数据的状态。 1. 搜索性能: ELKstack 中文指南 294监控方案 "search": { "open_contexts": 0, "query_total": 123, "query_time_in_millis": 531, "query_current": 0, "fetch_total": 3, "fetch_time_in_millis": 55, "fetch_current": 0 }, search.open_contexts 表示当前正在进行的搜索,而 search.query_total 表示节点启动以来完 成过的总搜索数,search.query_time_in_millis 表示完成上述搜索数花费时间的总和。显 然, query_time_in_millis/query_total 越大,说明搜索性能越差,可以通过 ES 的 slowlog,获取具体的搜索语句,做出针对性的优化。 search.fetch_total 等指标含义类似。因为 ES 的搜索默认是 query-then-fetch 式的,所以 fetch 一般是少而快的。如果计算出来 search.fetch_time_in_millis > search.query_time_in_millis ,说明有人采用了较大的 size 参数做分页查询,通过 slowlog 抓 到具体的语句,相机优化成 scan 式的搜索。 1. 段合并性能: "merges": { "current": 0, "current_docs": 0, "current_size_in_bytes": 0, "total": 1128, "total_time_in_millis": 21338523, "total_docs": 7241313, "total_size_in_bytes": 5724869463 }, merges 数据分为两部分,current 开头的是当前正在发生的段合并行为统计;total 开头的是 历史总计数。一般来说,作为 ELK Stack 应用,都是以数据写入压力为主的,merges 相关数 据会比较突出。 1. 过滤器缓存: "filter_cache": { "memory_size_in_bytes": 48, "evictions": 0 }, ELKstack 中文指南 295监控方案 filter_cache.memory_size_in_bytes 表示过滤器缓存使用的内存,filter_cache.evictions 表示 因内存满被回收的缓存大小,这个数如果较大,说明你的过滤器缓存大小不足,或者过滤器 本身不太适合缓存。比如在 ELK Stack 场景中常用的时间过滤器,如果使用 @timestamp: ["now-1d" TO "now"] 这种表达式的话,需要每次计算 now 值,就没法长期缓存。事实上, Kibana 中通过 timepicker 生成的 filtered 请求里,对 @timestamp 部分就并不是直接使用 "now" ,而是在浏览器上计算成毫秒数值,再发送给 ES 的。 请注意,过滤器缓存是建立在 segment 基础上的,在当天新日志的索引中,存在大量的或多 或少的 segments。一个已经 5GB 大小的 segment,和一个刚刚 2MB 大小的 segment,发 生一次 filter_cache.evictions 对搜索性能的影响区别是巨大的。但是节点状态中本身这个计数 并不能反应这点区别。所以,尽力减少这个数值,但如果搜索本身感觉不慢,那么有几个也 无所谓。 1. id 缓存: "id_cache": { "memory_size_in_bytes": 0 }, id_cache 是 parent/child mappings 使用的内存。不过在 ELK Stack 场景中,一般不会用到这 个特性,所以此处数据应该一直是 0。 1. fielddata: "fielddata": { "memory_size_in_bytes": 0, "evictions": 0 }, 此处显示 fielddata 使用的内存大小。fielddata 用来做聚合,排序等工作。 注意:fielddata.evictions 应该永远是 0。一旦发现这个数据大于 0,请立刻检查自己的内存配 置,fielddata 限制以及请求语句。 1. segments: "segments": { "count": 319, "memory_in_bytes": 65812120 }, segments.count 表示节点上所有索引的 segment 数目的总和。一般来说,一个索引通常会有 50-150 个 segments。再多就对写入性能有较大影响了(可能 merge 速度跟不上新 segment 出现的速度)。所以,请根据节点上的索引数据正确评估节点 segment 的情况。 ELKstack 中文指南 296监控方案 segments.memory_in_bytes 表示 segments 本身底层数据结构所使用的内存大小。像索引的 倒排表,词典,bloom filter(ES1.4以后已经默认关闭) 等,都是要在内存里的。所以过多的 segments 会导致这个数值迅速变大。 操作系统和进程信息 操作系统信息主要包括 CPU,Loadavg,Memory 和 Swap 利用率,文件句柄等。这些内容 都是常见的监控项,本书不再赘述。 进程,即 JVM 信息,主要在于 GC 相关数据。 GC 对不了解 JVM 的 GC 的读者,这里先介绍一下 GC(垃圾收集)以及 GC 对 Elasticsearch 的影 响。 Java is a garbage-collected language, which means that the programmer does not manually manage memory allocation and deallocation. The programmer simply writes code, and the Java Virtual Machine (JVM) manages the process of allocating memory as needed, and then later cleaning up that memory when no longer needed. Java 是一个自动垃圾收集的编程语 言,启动 JVM 虚拟机的时候,会分配到固定大小的内存块,这个块叫做 heap(堆)。JVM 会 把 heap 分成两个组: Young 新实例化的对象所分配的空间。这个空间一般来说只有 100MB 到 500MB 大小。 Young 空间又分为两个 survivor(幸存)空间。当 Young 空间满,就会发生一次 young gc,还存活的对象,就被移入幸存空间里,已失效的对象则被移除。 Old 老对象存储的空间。这些对象应该是长期存活而且在较长一段时间内不会变化的内 容。这个空间会大很多,在 ES 来说,一节点上可能就有 30GB 内存是这个空间。前面 提到的 young gc 中,如果某个对象连续多次幸存下来,就会被移进 Old 空间内。而等到 Old 空间满,就会发生一次 old gc,把失效对象移除。 听起来很美好的样子,但是这些都是有代价的!在 GC 发生的时候,JVM 需要暂停程序运 行,以便自己追踪对象图收集全部失效对象。在这期间,其他一切都不会继续运行。请求没 有响应,ping 没有应答,分片不会分配…… 当然,young gc 一般来说执行极快,没太大影响。但是 old 空间那么大,稍慢一点的 gc 就意 味着程序几秒乃至十几秒的不可用,这太危险了。 JVM 本身对 gc 算法一直在努力优化,Elasticsearch 也尽量复用内部对象,复用网络缓冲, 然后还提供像 Doc Values 这样的特性。但不管怎么说,gc 性能总是我们需要密切关注的数 据,因为它是集群稳定性最大的影响因子。 ELKstack 中文指南 297监控方案 如果你的 ES 集群监控里发现经常有很耗时的 GC,说明集群负载很重,内存不足。严重情况 下,这些 GC 导致节点无法正确响应集群之间的 ping ,可能就直接从集群里退出了。然后数 据分片也随之在集群中重新迁移,引发更大的网络和磁盘 IO,正常的写入和搜索也会受到影 响。 在节点状态数据中,以下部分就是 JVM 相关的数据: "jvm": { "timestamp": 1408556438203, "uptime_in_millis": 14457, "mem": { "heap_used_in_bytes": 457252160, "heap_used_percent": 44, "heap_committed_in_bytes": 1038876672, "heap_max_in_bytes": 1038876672, "non_heap_used_in_bytes": 38680680, "non_heap_committed_in_bytes": 38993920, }, 首先可以看到的就是 heap 的情况。其中这个 heap_committed_in_bytes 指的是实际被进程使 用的内存,以 JVM 的特性,这个值应该等于 heap_max_in_bytes 。 heap_used_percent 则是 一个更直观的阈值数据。当这个数据大于 75% 的时候,ES 就要开始 GC。也就是说,如果 你的节点这个数据长期在 75% 以上,说明你的节点内存不足,GC 可能会很慢了。更进一 步,如果到 85% 或者 95% 了,估计节点一次 GC 能耗时 10s 以上,甚至可能会发生 OOM 了。 继续看下一段: ELKstack 中文指南 298监控方案 "pools": { "young": { "used_in_bytes": 138467752, "max_in_bytes": 279183360, "peak_used_in_bytes": 279183360, "peak_max_in_bytes": 279183360 }, "survivor": { "used_in_bytes": 34865152, "max_in_bytes": 34865152, "peak_used_in_bytes": 34865152, "peak_max_in_bytes": 34865152 }, "old": { "used_in_bytes": 283919256, "max_in_bytes": 724828160, "peak_used_in_bytes": 283919256, "peak_max_in_bytes": 724828160 } } }, 这段里面列出了 young, survivor, 和 old GC 区域的情况,不过一般来说用途不大。再看下一 段: "gc": { "collectors": { "young": { "collection_count": 13, "collection_time_in_millis": 923 }, "old": { "collection_count": 0, "collection_time_in_millis": 0 } } } 这里显示的 young 和 old gc 的计数和耗时。young gc 的 count 一般比较大,这是正常情况。 old gc 的 count 应该就保持在比较小的状态,包括耗时的 collection_time_in_millis 也应该 很小。注意这两个计数都是累计的,所以对于一个长期运行的系统,不能拿这个数值直接做 报警的判断,应该是取两次节点数据的差值。有了差值之后,再来看耗时的问题,一般来 说,一次 young gc 的耗时应该在 1-2 ms,old gc 在 100 ms 的水平。如果这个耗时有量级上 的差距,建议打开 slow-GC 日志,具体研究原因。 线程池信息 ELKstack 中文指南 299监控方案 Elasticsearch 内部是保持着几个线程池的,不同的工作由不同的线程池负责。一般来说,每 个池子的工作线程数跟你的 CPU 核数一样。之前有传言中的优化配置是加大这方面的配置 项,其实没有什么实际帮助 —— 能干活的 CPU 就那么些个数。所以这段状态数据目的不是 用作 ES 配置调优,而是作为性能监控,方便优化你的读写请求。 ES 在 index、bulk、search、get、merge 等各种操作都有专门的线程池,大家的统计数据格 式都是类似的: "index": { "threads": 1, "queue": 0, "active": 0, "rejected": 0, "largest": 1, "completed": 1 } 这些数据中,最重要的是 rejected 数据。当线程中中所有的工作线程都在忙,即 active == threads,后续的请求就会暂时放到排队的队列里,即 queue > 0。但是每个线程池的 queue 也是有大小限制的,默认是 100。如果后续请求超过这个大小,意味着 ES 真的接受不过来这 个请求了,就会把后续请求 reject 掉。 Bulk Rejections 如果你确实注意到了上面数据中的 rejected,很可能就是你在发送 bulk 写入的时候碰到 HTTP 状态码 429 的响应报错了。事实上,集群的承载能力是有上限的。如果你集群每秒就 能写入 10000 条数据,以其浪费内存多放几条数据在排队,还不如直接拒绝掉。至少可以让 你知道到瓶颈了。 另外有一点可以指出的是,因为 bulk queue 里的数据是维护在内存中,所以节点发生意外死 机的时候,是会丢失的。 如果你碰到 bulk rejected,可以尝试以下步骤: 1. 暂停所有的写入进程。 2. 从 bulk 响应中过滤出来 rejected 的那部分。因为 bulk index 中可能大部分已经成功了。 3. 重发一次失败的请求。 4. 恢复写入进程,或者重新来一次上述步骤。 大家可能看出来了,没错,对 rejected 其实压根没什么特殊的操作,重试一次而已。 当然,如果这个 rejected 是持续存在并增长的,那重试也无济于事。你可能需要考虑自己集 群是否足以支撑当前的写入速度要求。 ELKstack 中文指南 300监控方案 如果确实没问题,那么可能是因为客户端并发太多,超过集群的 bulk threads 总数了。尝试 减少自己的写入进程个数,改成加大每次 bulk 请求的 size。 文件系统和网络 数据继续往下走,是文件系统和网络的数据。文件系统方面,不管是剩余空间还是 IO 数据, 都推荐大家还是通过更传统的系统层监控手段来做。而网络数据方面,主要有两部分内容: "transport": { "server_open": 13, "rx_count": 11696, "rx_size_in_bytes": 1525774, "tx_count": 10282, "tx_size_in_bytes": 1440101928 }, "http": { "current_open": 4, "total_opened": 23 }, 我们知道 ES 同时运行着 transport 和 http ,默认分别是 9300 和 9200 端口。由于 ES 使用 了一些 transport 连接来维护节点内部关系,所以 transport.server_open 正常情况下一直会 有一定大小。而 http.current_open 则是实际连接上来的 HTTP 客户端的数量,考虑到 HTTP 建联的消耗,强烈建议大家使用 keep-alive 长连接的客户端。 Circuit Breaker 继续往下,是 fielddata circuit breaker 的数据: "fielddata_breaker": { "maximum_size_in_bytes": 623326003, "maximum_size": "594.4mb", "estimated_size_in_bytes": 0, "estimated_size": "0b", "overhead": 1.03, "tripped": 0 } fielddata_breaker.maximum_size 是一个请求能使用的内存的最大 值。 fielddata_breaker.tripped 记录的是触发 circuit breaker 的次数。如果这个数值太高, 或者持续增长,说明目前 ES 收到的请求亟待优化,或者单纯的,加机器,加内存。 ELKstack 中文指南 301监控方案 hot_threads 状态 除了 stats 信息以外, /_nodes/ 下还有另一个监控接口: # curl -XGET 'http://127.0.0.1:9200/_nodes/_local/hot_threads?interval=60s' 该接口会返回在 interval 时长内,该节点消耗资源最多的前三个线程的堆栈情况。这对于 性能调优初期,采集现状数据,极为有用。 默认的采样间隔是 500ms,一般来说,这个时间范围是不太够的,建议至少 60s 以上。 默认的,资源消耗是按照 CPU 来衡量,还可以用 ?type=wait 或者 ?type=block 来查看在 等待和堵塞状态的当前线程排名。 ELKstack 中文指南 302监控方案 索引状态监控接口 索引状态监控接口的输出信息和节点状态监控接口非常类似。一般情况下,这个接口单独监 控起来的意义并不大。 不过在 ES 最新的 1.6 版,新加入了对索引分片级别的 commit id 功能。 回忆一下之前原理章节的内容,commit 是在分片内部,对每个 segment 做的。而数据在主分 片和副本分片上,是由各自节点自行做 segment merge 操作,所以副本分片和主分片的 segment 的 commit id 是不一致的。这导致 ES 副本恢复时,跟主分片比对 commit id,基本 上每个 segment 都不一样,所以才需要从主分片完整重传一份数据。 新加入分片级别的 commit id 后,副本恢复时,先比对跟主分片的分片级 commit id,如果一 致,直接本地恢复副本分片内容即可。 查看分片级别 commit id 的命令如下: # curl 'http://127.0.0.1:9200/logstash-mweibo-2015.06.15/_stats/commit?level=shards&pretty' ... "indices" : { "logstash-2015.06.15" : { "primaries" : { }, "total" : { }, "shards" : { "0" : [ { "routing" : { "state" : "STARTED", "primary" : true, "node" : "AqaYWFQJRIK0ZydvVgASEw", "relocating_node" : null }, "commit" : { "generation" : 726, "user_data" : { "translog_id" : "1434297603053", "sync_id" : "AU4LEh6wnBE6n0qcEXs5" }, "num_docs" : 36792652 } } ], ... 注意:为了节约频繁变更的资源消耗,ES 并不会实时更新分片级 commit id。只有连续 5 分 钟没有新数据写入的索引,才会触发给索引各分片更新 commit id 的操作。如果你查看的是一 个还在新写入数据的索引,看到的内容应该是下面这样: ELKstack 中文指南 303监控方案 "commit" : { "generation" : 590, "user_data" : { "translog_id" : "1434038402801" }, "num_docs" : 29051938 } ELKstack 中文指南 304监控方案 等待执行的任务列表 首先需要解释的是,这个接口返回的列表,只是对于集群的 master 节点来说的等待执行。数 据节点本身的写入和查询线程,如果因为较慢导致排队,是不在这个列表里的。 之前章节已经讲过,master 节点负责处理的任务其实很少,只有集群状态的数据维护。所以 绝大多数情况下,这个任务列表应该都是空的。 # curl -XGET http://127.0.0.1:9200/_cluster/pending_tasks { "tasks": [] } 但是如果你碰上集群有异常,比如频繁有索引映射更新,数据恢复啊,分片分配或者初始化 的时候反复出错啊这种时候,就会看到一些任务列表了: { "tasks" : [ { "insert_order": 767003, "priority": "URGENT", "source": "create-index [logstash- 2015.06.01], cause [api]", "time_in_queue_millis": 86, "time_in_queue": "86ms" }, { "insert_order" : 767004, "priority" : "HIGH", "source" : "shard-failed ([logstash-2015.05.31] [50], node[F3EGSRWGSvWGFlcZ6K9pRA], [P], s[INITIALIZING]), reason [shard failure [failed recovery][IndexShardGatewayRecoveryException[[logstash-2015.05.31][50] failed to fetch index version after copying it over]; nested: CorruptIndexException[[logstash- 2015.05.31][50] Preexisting corrupted index [corrupted_nC9t_ramRHqsbQqZO78KVg] caused by: CorruptIndexException[did not read all bytes from file: read 269 vs size 307 (resource: BufferedChecksumIndexInput(NIOFSIndexInput(path=\"/data1/elasticsearch/data/es1003/no des/0/indices/logstash- 2015.05.31/50/index/_16c.si\")))]\norg.apache.lucene.index.CorruptIndexException: did not read all bytes from file: read 269 vs size 307 (resource: BufferedChecksumIndexInput(NIOFSIndexInput(path=\"/data1/elasticsearch/data/es1003/no des/0/indices/logstash-2015.05.31/50/index/_16c.si\")))\n\tat org.apache.lucene.codecs.CodecUtil.checkFooter(CodecUtil.java:216)\n\tat org.apache.lucene.codecs.lucene46.Lucene46SegmentInfoReader.read(Lucene46SegmentI nfoReader.java:73)\n\tat org.apache.lucene.index.SegmentInfos.read(SegmentInfos.java:359)\n\tat org.apache.lucene.index.SegmentInfos$1.doBody(SegmentInfos.java:462)\n\tat org.apache.lucene.index.SegmentInfos$FindSegmentsFile.run(SegmentInfos.java:923)\n\tat org.apache.lucene.index.SegmentInfos$FindSegmentsFile.run(SegmentInfos.java:769)\n\tat org.apache.lucene.index.SegmentInfos.read(SegmentInfos.java:458)\n\tat org.elasticsearch.common.lucene.Lucene.readSegmentInfos(Lucene.java:89)\n\tat ELKstack 中文指南 305监控方案 org.elasticsearch.index.store.Store.readSegmentsInfo(Store.java:158)\n\tat org.elasticsearch.index.store.Store.readLastCommittedSegmentsInfo(Store.java:148)\n\tat org.elasticsearch.index.engine.InternalEngine.flush(InternalEngine.java:675)\n\tat org.elasticsearch.index.engine.InternalEngine.flush(InternalEngine.java:593)\n\tat org.elasticsearch.index.shard.IndexShard.flush(IndexShard.java:675)\n\tat org.elasticsearch.index.translog.TranslogService$TranslogBasedFlush$1.run(TranslogServi ce.java:203)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\n\tat java.lang.Thread.run(Thread.java:745)\n]; ]]", "executing" : true, "time_in_queue_millis" : 2813, "time_in_queue" : "2.8s" }, { "insert_order" : 767005, "priority" : "HIGH", "source" : "refresh-mapping [logstash-2015.06.03][[curl_debuginfo]]", "executing" : false, "time_in_queue_millis" : 2787, "time_in_queue" : "2.7s" }, { "insert_order" : 767006, "priority" : "HIGH", "source" : "refresh-mapping [logstash-2015.05.29][[curl_debuginfo]]", "executing" : false, "time_in_queue_millis" : 448, "time_in_queue" : "448ms" } ] } ``` 可以看到列表中的任务都有各自的优先级,URGENT 优先于 HIGH。然后是任务在队列中的 排队时间,任务的具体内容等。 在上例中,由于磁盘文件损坏,一个分片中某个 segment 的实际内容和长度对不上,导致分 片数据恢复无法正常完成,堵塞了后续的索引映射更新操作。这个错误一般来说不太常见, 也只能是关闭索引,或者放弃这部分数据。更常见的可能,是集群存储长期数据导致索引映 射数据确实大到了 master 节点内存不足以快速处理的地步。 这时候,根据实际情况,可以有以下几种选择: 索引就是特别多:给 master 加内存。 索引里字段太多:改用 nested object 方式节省字段数量。 索引多到内存就是不够了:把一部分数据拆出来另一个集群。 ELKstack 中文指南 306监控方案 cat 接口的命令行使用 之前介绍的各种接口数据,其响应数据都是 JSON 格式,更适用于程序处理。对于我们日常 运维,在 Linux 命令行终端环境来说,简单的分行和分列表格才是更方便的样式。为此, Elasticsearch 提供了 cat 接口。 cat 接口可以读取各种监控数据,可用接口列表如下: /_cat/allocation /_cat/shards /_cat/shards/{index} /_cat/master /_cat/nodes /_cat/indices /_cat/indices/{index} /_cat/segments /_cat/segments/{index} /_cat/count /_cat/count/{index} /_cat/recovery /_cat/recovery/{index} /_cat/health /_cat/pending_tasks /_cat/aliases /_cat/aliases/{alias} /_cat/thread_pool /_cat/plugins /_cat/fielddata /_cat/fielddata/{fields} 集群状态 还是以最基础的集群状态为例,采用 cat 接口查询集群状态的命令如下: # curl -XGET http://127.0.0.1:9200/_cat/health 1434300299 00:44:59 es1003 red 39 27 2589 1505 4 1 0 0 如果单看这行输出,或许不熟悉的用户会有些茫然。可以通过添加 ?v 参数,输出表头: ELKstack 中文指南 307监控方案 # curl -XGET http://127.0.0.1:9200/_cat/health?v epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks 1434300334 00:45:34 es1003 green 39 27 2590 1506 5 0 0 0 节点状态 # curl -XGET http://127.0.0.1:9200/_cat/nodes?v host ip heap.percent ram.percent load node.role master name esnode109 10.19.0.109 62 69 6.37 d - 10.19.0.109 esnode096 10.19.0.96 63 69 0.29 - - 10.19.0.96 esnode100 10.19.0.100 56 79 0.07 - m 10.19.0.100 跟集群状态不一样的是,节点状态数据太多,cat 接口不方便在一行表格中放下所有数据。所 以默认的返回,只是最基本的内存和负载数据。具体想看某方面的数据,也是通过请求参数 的方式额外指明。比如想看 heap 百分比和最大值: # curl -XGET 'http://127.0.0.1:9200/_cat/nodes?v&h=ip,port,heapPercent,heapMax' ip port heapPercent heapMax 192.168.1.131 9300 66 25gb h 请求参数可用的值,可以通过 ?help 请求参数来查询: # curl -XGET http://127.0.0.1:9200/_cat/nodes?help id | id,nodeId | unique node id host | h | host name ip | i | ip address port | po | bound transport port heap.percent | hp,heapPercent | used heap ratio heap.max | hm,heapMax | max configured heap ram.percent | rp,ramPercent | used machine memory ratio ram.max | rm,ramMax | total machine memory load | l | most recent load avg node.role | r,role,dc,nodeRole | d:data node, c:client node ... 中间第二列就是对应的请求参数的值及其缩写。也就是说上面示例还可以写成: # curl -XGET http://127.0.0.1:9200/_cat/nodes?v&h=i,po,hp,hm 索引状态 ELKstack 中文指南 308监控方案 查询索引列表和存储的数据状态是也是 cat 接口最常用的功能之一。为了方便阅读,默认输出 时会把数据大小以更可读的方式自动换算成合适的单位,比如 3.2tb 这样。 如果你打算通过 shell 管道做后续处理,那么可以加上 ?bytes 参数,指明统一采用字节数输 出,这样保证在同一个级别上排序: # curl -XGET http://127.0.0.1:9200/_cat/indices?bytes=b | sort -rnk8 green open logstash-mweibo-2015.06.12 26 1 754641614 0 2534955821580 1256680767317 green open logstash-mweibo-2015.06.14 27 1 855516794 0 2419569433696 1222355996673 分片状态 # curl -XGET http://127.0.0.1:9200/_cat/shards?v index shard prirep state docs store ip node logstash-mweibo-h5view-2015.06.10 20 p STARTED 4690968 679.2mb 127.0.0.1 10.19.0.108 logstash-mweibo-h5view-2015.06.10 20 r STARTED 4690968 679.4mb 127.0.0.1 10.19.0.39 logstash-mweibo-h5view-2015.06.10 2 p STARTED 4725961 684.3mb 127.0.0.1 10.19.0.53 logstash-mweibo-h5view-2015.06.10 2 r STARTED 4725961 684.3mb 127.0.0.1 10.19.0.102 同样,可以用 ?help 查询其他可用数据细节。比如每个分片的 segment.count: # curl -XGET 'http://127.0.0.1:9200/_cat/shards/logstash-mweibo-nginx-2015.06.14?v\&h=n,iic,sc' n iic sc 10.19.0.72 0 42 10.19.0.41 0 36 10.19.0.104 0 32 10.19.0.102 0 40 恢复状态 在出现集群状态波动时,通过这个接口查看数据迁移和恢复速度也是一个非常有用的功能。 不过默认输出是把集群历史上所有发生的 recovery 记录都返回出来,所以一般会加上 ? active_only 参数,仅列出当前还在运行的恢复状态: # curl -XGET 'http://127.0.0.1:9200/_cat/recovery?active_only&v&h=i,s,shost,thost,fp,bp,tr,trp,trt' i s shost thost fp bp tr trp trt logstash-mweibo-2015.06.12 10 esnode041 esnode080 87.6% 35.3% 0 100.0% 0 logstash-mweibo-2015.06.13 10 esnode108 esnode080 95.5% 88.3% 0 100.0% 0 logstash-mweibo-2015.06.14 17 esnode102 esnode080 96.3% 92.5% 0 0.0% 118758 ELKstack 中文指南 309监控方案 线程池状态 curl -s -XGET http://127.0.0.1:9200/_cat/thread_pool?v host ip bulk.active bulk.queue bulk.rejected index.active index.queue index.rejected search.active search.queue search.rejected esnode073 127.0.0.1 1 0 20669 0 0 50 4 0 0 包括 merge,optimize,flush,refresh 等其他线程池状态,也可以通过 ?h 参数指明获取。 ELKstack 中文指南 310监控方案 日志记录 Elasticsearch 作为一个服务,本身也会记录很多日志信息。默认情况下,日志都放在 $ES_HOME/logs/ 目录里。 日志级别可以通过 elasticsearch.yml 配置设置,也可以通过 /_cluster/settings 接口动态 调整。比如说,如果你的节点一直无法正确的加入集群,你可以将集群自动发现方面的日志 级别修改成 DEBUG,来关注这方面的问题: # curl -XPUT http://127.0.0.1:9200/_cluster/settings -d' { "transient" : { "logger.discovery" : "DEBUG" } }' 性能日志 除了进程状态的日志输出,ES 还支持跟性能相关的日志输出。针对数据写入,检索,读取三 个阶段,都可以设置具体的慢查询阈值,以及不同的输出等级。 此外,慢查询日志是针对索引级别的设置。除了在 elasticsearch.yml 中设置(注意,默认是 全注释不开启的状态)以及通过 /_cluster/settings 接口配置一组集群各索引共用的参数以 外,还可以针对每个索引设置不同的参数。 比如说,我们可以先设置集群共同的参数: # curl -XPUT http://127.0.0.1:9200/_cluster/settings -d' { "transient" : { "logger.index.search.slowlog" : "DEBUG", "logger.index.indexing.slowlog" : "WARN", "index.search.slowlog.threshold.query.debug" : "10s", "index.search.slowlog.threshold.fetch.debug": "500ms", "index.indexing.slowlog.threshold.index.warn": "5s" } }' 然后针对某个比较大的索引,调高设置: ELKstack 中文指南 311日志记录 # curl -XPUT http://127.0.0.1:9200/logstash-wwwlog-2015.06.21/_settings -d' { "index.search.slowlog.threshold.query.warn" : "10s", "index.search.slowlog.threshold.fetch.debug": "500ms", "index.indexing.slowlog.threshold.index.info": "10s" } ELKstack 中文指南 312日志记录 bigdesk 要想最快的了解 ES 各节点的性能细节,推荐使用 bigdesk 插件,其 GitHub 地址 见:https://github.com/lukas-vlcek/bigdesk bigdesk 通过浏览器直连 ES 节点,发起 RESTful 请求,并渲染结果成图。所以其安装部署极 其简单: # git clone https://github.com/lukas-vlcek/bigdesk # cd bigdesk # python -mSimpleHTTPServer Serving HTTP on 0.0.0.0 port 8000 ... 浏览器打开 http://localhost:8000 即可看到 bigdesk 页面。在 endpoint 输入框内填写要连 接的 ES 节点地址,选择 refresh 间隔和 keep 时长,点击 connect,完成。 注意:设置 refresh 间隔请考虑 ELK Stack 使用的 template 里实际的 refresh_interval 是多 少。否则你可能看到波动太大的数据,不足以说明情况。 点选某个节点后,就可以看到该节点性能的实时走势。一般重点关注 JVM 性能和索引性能。 有关 JVM 部分截图如下: ELKstack 中文指南 313实时bigdesk方案 有关数据读写性能部分截图如下: ELKstack 中文指南 314实时bigdesk方案 Elasticsearch 2.x 上的部署方法 bigdesk 的源码开发在 2015 年夏天就已经停止,所以默认是无法支持 Elasticsearch 2.x 的监 控的。不过总体来说,Elasticsearch 性能监控接口没有什么变化,我们只需要稍微修改 bigdesk,使之符合 Elasticsearch 2.x 的插件规范即可继续使用。 Elasticsearch 2.x 的插件目录规范,可以参考 marvel-agent,kopf 等。 ELKstack 中文指南 315实时bigdesk方案 mkdir -p $ES_HOME/plugins/bigdesk cd $ES_HOME/plugins/bigdesk git clone https://github.com/lukas-vlcek/bigdesk _site sed -i '142s/==/>=/' _site/js/store/BigdeskStore.js cat >plugin-descriptor.properties<2 AND program:kernel" } } }' # curl -XPUT http://127.0.0.1:9200/syslog/.percolator/disk -d '{ "query" : { "query_string" : { "default_field" : "message", "default_operator" : "OR", "query" : "scsi sata hdd sda AND severity:>2 AND program:kernel" } } }' 然后,将标准的数据写入请求稍微做一点改动: # curl -XPOST http://127.0.0.1:9200/syslog/msg/_percolate -d '{ "doc" : { "timestamp" : "Jul 17 03:57:23", "host" : "localhost", "program" : "kernel", "facility" : 0, "severity" : 3, "message" : "swapper/0: page allocation failure: order:4, mode:0x4020" } }' 得到如下结果: { ..., "total": 1, "matches": [ { "_index": "syslog", "_id": "memory" } ] } 从结果可以看出来,这条 syslog 日志匹配上了 memory 异常。接下来就可以发送给报警系统 了。 ELKstack 中文指南 326percolator api和watcher报警 如果是 syslog 索引中已经有的数据,也可以重新过一遍 Percolator 接口: # curl -XPOST http://127.0.0.1:9200/syslog/msg/existsid/_percoloate 利用更复杂的 query DSL 做 Percolator 请求的示例,推荐阅读官网这篇 geo 定位的文 章:https://www.elastic.co/blog/using-percolator-geo-tagging Watcher 产品 Watcher 也是 Elastic.co 公司的商业产品,和 Shield,Marvel 一样插件式安装即可: bin/plugin -i elasticsearch/license/latest bin/plugin -i elasticsearch/watcher/latest Watcher 使用方面,也提供标准的 RESTful 接口,示例如下: # curl -XPUT http://127.0.0.1:9200/_watcher/watch/error_status -d' { "trigger": { "schedule" : { "interval" : "5m" } }, "input" : { "search" : { "request" : { "indices" : [ "", "" ], "body" : { "query" : { "filtered" : { "query" : { "match" : { "status" : "error" }}, "filter" : { "range" : { "@timestamp" : { "from" : "now-5m" }}} } } } } } }, "condition" : { "compare" : { "ctx.payload.hits.total" : { "gt" : 0 }} }, "transform" : { "search" : { "request" : { "indices" : [ "", "" ], "body" : { "query" : { "filtered" : { "query" : { "match" : { "status" : "error" }}, ELKstack 中文指南 327percolator api和watcher报警 "filter" : { "range" : { "@timestamp" : { "from" : "now-5m" }}} } }, "aggs" : { "topn" : { "terms" : { "field" : "userid" } } } } } } }, "actions" : { "email_admin" : { "throttle_period" : "15m", "email" : { "to" : "admin@domain", "subject" : "Found {{ctx.payload.hits.total}} Error Events at {{ctx.trigger.triggered_time}}", "priority" : "high", "body" : "Top10 users:\n{{#ctx.payload.aggregations.topn.buckets}}\t{{key}} {{doc_count}}\n{{/ctx.payload.aggregations.topn.buckets}}" } } } }' 上面这行命令,意即: 1. 每 5 分钟,向最近两天的 logstash-yyyy.MM.dd 索引发起一次条件为最近五分钟,status 字段内容为 error 的查询请求; 2. 对查询结果做 hits 总数大于 0 的判断; 3. 如果为真,再请求一次上述条件下,userid 字段的 Top 10 数据集作为后续处理的来源; 4. 如果最近 15 分钟内未发送过报警,则向 admin@domain 邮箱发送一个标题为 "Found N erroneous events at yyyy-MM-ddTHH:mm:ssZ",内容为 "Top10 users" 列表的报警邮 件。 整个请求体顺序执行。目前 trigger 只支持 scheduler 方式,input 支持 search 和 http 方式, actions 支持 email,logging,webhook 方式,transform 是可选项,而且可以设置在 actions 里,不同 actions 做不同的 payload 转换。 condition, transform 和 actions 中,默认使用 Watcher 增强版的 xmustache 模板语言(示例 中的数组循环就是一例)。也可以使用固化的脚本文件,比如有 threshold_hits.groovy 的 话,可以执行: ELKstack 中文指南 328percolator api和watcher报警 "condition" : { "script" : { "file" : "threshold_hits", "params" : { "threshold" : 0 } } } Watcher 中可用的 ctx 变量包括: ctx.watch_id ctx.execution_time ctx.trigger.triggered_time ctx.trigger.scheduled_time ctx.metadata.* ctx.payload.* 完整的 Watcher 插件内部执行流程如下图。相信有编程能力的读者都可以用 crontab/at 配合 curl,email 工具仿造出来类似功能的 shell 脚本。 ELKstack 中文指南 329percolator api和watcher报警 注意: 在 search 中,对 indices 内容可以写完整的索引名比如 syslog ,也可以写通配符比如 logstash-* ,也可以写时序索引动态定义方式如 。而这个动态定义, Watcher 是支持根据时区来确定的,这个需要在 elasticsearch.yml 里配置一行: watcher.input.search.dynamic_indices.time_zone: '+08:00' ELKstack 中文指南 330percolator api和watcher报警 ElastAlert ElastAlert 是 Yelp 公司开源的一套用 Python2.6 写的报警框架。属于后来 Elastic.co 公司出品 的 Watcher 同类产品。官网地址见:http://elastalert.readthedocs.org/。 安装 比官网文档说的步骤稍微复杂一点,因为其中 mock 模块安装时依赖的 setuptools 要求版本 在 0.17 以上,CentOS6 默认的不够,需要通过 yum 命令升级,当前可以升级到的是 0.18 版。 # yum install python-setuptools # git clone https://github.com/Yelp/elastalert.git # cd elastalert # python setup.py install # cp config.yaml.example config.yaml 安装完成后会自带三个命令: elastalert-create-index ElastAlert 会把执行记录存放到一个 ES 索引中,该命令就是用来 创建这个索引的,默认情况下,索引名叫 elastalert_status 。其中有 4 个 _type,都有 自己的 @timestamp 字段,所以同样也可以用 kibana 来查看这个索引的日志记录情况。 elastalert-rule-from-kibana 从 Kibana3 已保存的仪表盘中读取 Filtering 设置,帮助生成 config.yaml 里的配置。不过注意,它只会读取 filtering,不包括 queries。 elastalert-test-rule 测试自定义配置中的 rule 设置。 最后,运行命令: # python -m elastalert.elastalert --config ./config.yaml 或者单独执行 rules_folder 里的某个 rule: # python -m elastalert.elastalert --config ./config.yaml --rule ./examele_rules/one_rule.yaml 配置结构 ELKstack 中文指南 331ElastAlert 和 Watcher 类似(或者说也只有这种方式),ElastAlert 配置结构也分几个部分,但是它有自己 的命名。 query 部分 除了有关 ES 服务器的配置以外,主要包括: run_every 配置,用来设置定时向 ES 发请求,默认 5 分钟。 buffer_time 配置,用来设置请求里时间字段的范围,默认 45 分钟。 rules_folder 配置,用来加载下一阶段的 rule 设置,默认是 example_rules 。 timestamp_field 配置,设置 buffer_time 时针对哪个字段,默认是 @timestamp 。 timestamp_type 配置,设置 timestamp_field 的时间类型,ElastAlert 内部也需要转换 成时间对象,默认是 ISO8601 ,也可以是 UNIX 。 rule 部分 rule 设置各自独立以文件方式存储在 rules_folder 设置的目录里。其中可以定义下面这些参 数: name 配置,每个 rule 需要有自己独立的 name,一旦重复,进程将无法启动。 type 配置,选择某一种数据验证方式。 index 配置,从某类索引里读取数据,目前只支持通配符。 filter 配置,设置向 ES 请求的过滤条件。 timeframe 配置,累积触发报警的时长。 alert 配置,设置触发报警时执行哪些报警手段。 不同的 type 还有自己独特的配置选项。目前 ElastAlert 有以下几种自带 ruletype: any : 只要有匹配就报警; blacklist : compare_key 字段的内容匹配上 blacklist 数组里任意内容; whitelist : compare_key 字段的内容一个都没能匹配上 whitelist 数组里内容; change : 在相同 query_key 条件下, compare_key 字段的内容,在 timeframe 范围内 发送变化; frequency : 在相同 query_key 条件下, timeframe 范围内有 num_events 个被过滤出 来的异常; spike : 在相同 query_key 条件下,前后两个 timeframe 范围内数据量相差比例超过 spike_height 。其中可以通过 spike_type 设置具体涨跌方向是 up , down , both 。还 可以通过 threshold_ref 设置要求上一个周期数据量的下限, threshold_cur 设置要求 当前周期数据量的下限,如果数据量不到下限,也不触发; flatline : timeframe 范围内,数据量小于 threshold 阈值; new_term : fields 字段新出现之前 terms_window_size (默认 30 天) 范围内最多的 terms_size (默认 50) 个结果以外的数据; ELKstack 中文指南 332ElastAlert cardinality : 在相同 query_key 条件下, timeframe 范围内 cardinality_field 的值 超过 max_cardinality 或者低于 min_cardinality 。 alert 部分 alert 配置是一个数组,目前支持 command, email,jira,opsgenie,sns,hipchat,slack 等方式。 command command 最灵活也最简单。默认会采用 %(fieldname)s 格式: command: ["/bin/send_alert", "--username", "%(username)s", "--time", "%(key_as_string)s"] 如果要用的比较多,可以开启 pipe_match_json 参数,会把整个过滤到的内容,以一整个 JSON 字符串的方式管道输入指定脚本。 email email 方式采用 SMTP 协议,所以有一系列 smtp_* 配置,然后加上 email 参数提供收件人 地址数组。 特殊的是,email 和 jira 两种方式,ElastAlert 提供了一些内容格式化模板: 比如可以这样控制邮件标题: alert_subject: "Issue {0} occurred at {1}" alert_subject_args: - issue.name - "@timestamp" 而默认的邮件内容模板是: body = rule_name [alert_text] ruletype_text {top_counts} {field_values} 这些内容同样可以通过 alert_text (及对应 alert_text_args )等来灵活修改。 此外,alert 还有一系列控制报警风暴的选项,从属于 rule: aggregation :设置一个时长,则该时长内所有报警最终合在一起发一次; realert :设置一个时长,则该时长内,相同 query_key 的报警只发一个; ELKstack 中文指南 333ElastAlert exponential_realert :设置一个时长,必须大于 realert 设置。则在 realert 到 exponential_realert 之间,每次报警后,realert 自动翻倍。 enhancements 部分 match_enhancements 配置,设置一个数组,在报警内容发送到 alert 之前修改具体数据。 ElastAlert 默认不提供具体的 enhancements 实现,需要自己扩展。 不过,作为通用方式,ElastAlert 提供几个便捷选项,把 Kibana 地址加入报警: generate_kibana_link : 自动生成一个 Kibana3 的临时仪表盘附在报警内容上。 use_kibana_dashboard : 采用现成的 Kibana3 仪表盘附在报警内容上。 use_kibana4_dashboard : 采用现成的 Kibana4 仪表盘附在报警内容上。 扩展 rule 创建一个自己的 rule,是以 Python 模块的形式存在的,所以首先创建目录: # mkdir rule_modules # cd rule_modules # touch __init__.py example_rule.py example_rule.py 的内容如下: ELKstack 中文指南 334ElastAlert import dateutil.parser from elastalert.util import ts_to_dt from elastalert.ruletypes import RuleType class AwesomeNewRule(RuleType): # 用来指定本 rule 对应的配置文件中必要的参数项 required_options = set(['time_start', 'time_end', 'usernames']) # 每次运行获取的数据以时间排序数据传递给 add_data 函数 def add_data(self, data): for document in data: # 配置文件中的设置可以通过 self.rule[] 获取 if document['username'] in self.rule['usernames']: login_time = document['@timestamp'].time() time_start = dateutil.parser.parse(self.rule['time_start']).time() time_end = dateutil.parser.parse(self.rule['time_end']).time() if login_time > time_start and login_time < time_end: # 最终过滤结果,使用 self.add_match 添加 self.add_match(document) # alert_text 中使用的文本 def get_match_str(self, match): return "%s logged in between %s and %s" % (match['username'], self.rule['time_start'], self.rule['time_end']) def garbage_collect(self, timestamp): pass 配置中,指定 type: rule_modules.example_rule.AwesomeRule time_start: "20:00" time_end: "24:00" usernames: - "admin" - "userXYZ" - "foobaz" 即可使用。 alerter alerter 也是以 Python 模块的形式存在的,所以还是要创建目录(如果之前二次开发 rule 已经 创建过可以跳过): # mkdir rule_modules # cd rule_modules # touch __init__.py example_alert.py ELKstack 中文指南 335ElastAlert example_alert.py 的内容如下: from elastalert.alerts import Alerter, basic_match_string class AwesomeNewAlerter(Alerter): required_options = set(['output_file_path']) def alert(self, matches): for match in matches: with open(self.rule['output_file_path'], "a") as output_file: # basic_match_string 函数用来转换异常数据成默认格式的字符串 match_string = basic_match_string(self.rule, match) output_file.write(match_string) # 报警发出后,ElastAlert 会调用该函数的结果写入 ES 索引的 alert_info 字段内 def get_info(self): return {'type': 'Awesome Alerter', 'output_file': self.rule['output_file_path']} 配置中,指定 alert: "rule_modules.example_alert.AwesomeNewAlerter" output_file_path: "/tmp/alerts.log" 即可使用。 enhancement enhancement 也是以 Python 模块的形式存在的,所以还是要创建目录(如果之前二次开发 rule 或 alert 已经创建过可以跳过): # mkdir rule_modules # cd rule_modules # touch __init__.py example_enhancement.py example_enhancement.py 的内容如下: from elastalert.enhancements import BaseEnhancement class MyEnhancement(BaseEnhancement): def process(self, match): if 'domain' in match: url = "http://who.is/whois/%s" % (match['domain']) match['domain_whois_link'] = url 在需要的 rule 配置文件中添加如下内容即可启用: ELKstack 中文指南 336ElastAlert match_enhancements: - "rule_modules.example_enhancement.MyEnhancement" 因为 match_enhancements 是个数组,也就是说,如果数组有多个 enhancement,会依次执 行,完全完成后,才传递给 alert。 ELKstack 中文指南 337ElastAlert 时序数据 之前已经介绍过,ES 默认存储数据时,是有索引数据、_all 全文索引数据、_source JSON 字符串三份的。其中,索引数据由于倒排索引的结构,压缩比非常高。因此,在某些特定环 境和需求下,可以只保留索引数据,以极小的容量代价,换取 ES 灵活的数据结构和聚合统计 功能。 在监控系统中,对监控项和监控数据的设计一般是这样: metric_path value timestamp (Graphite 设计) { "host": "Host name 1", "key": "item_key", "value": "33", "clock": 1381482894 } (Zabbix 设计) 这些设计有个共同点,数据是二维平面的。以最简单的访问请求状态监控为例,一次请求, 可能转换出来的 metric_path 或者说 key 就有:{city,isp,host,upstream}.{urlpath...}. {status,rt,ut,size,speed} 这么多种。假设 urlpath 有 1000 个,就是 20000 个组合。意味着 需要发送 20000 条数据,做 20000 次存储。 而在 ES 里,这就是实实在在 1000 条日志。而且在多条日志的时候,因为词元的相对固定, 压缩比还会更高。所以,使用 ES 来做时序监控数据的存储和查询,是完全可行的办法。 对时序数据,关键就是定义缩减数据重复。template 示例如下: { "order" : 2, "template" : "logstash-monit-*", "settings" : { }, "mappings" : { "_default_" : { "_source" : { "enabled" : false }, "_all" : { "enabled" : false } } }, "aliases" : { } } 如果有些字段,是完全不用 Query ,只参加 Aggregation 的,还可以设置: ELKstack 中文指南 338时序数据库 "properties" : { "sid" : { "index" : "no", "doc_values" : true, "type" : "string" } }, 关于 Elasticsearch 用作 rrd 用途,与 MongoDB 等其他工具的性能测试与对比,可以阅读腾 讯工程师写的系列文章:http://segmentfault.com/a/1190000002690600 ELKstack 中文指南 339时序数据库 Kale 系统 Kale 系统是 Etsy 公司开源的一个监控分析系统。Kale 分为两个部分:skyline 和 oculus。 skyline 负责对时序数据进行概率分布校验,对校验失败率超过阈值的时序数据发报警; oculus 负责给被报警的时序,找出趋势相似的其他时序作为关联性参考。 看到“相似”两个字,你一定想到了。没错,oculus 组件,就是利用了 Elasticsearch 的相似度 打分。 oculus 中,为 Elasticsearch 的 org.elasticsearch.script.ExecutableScript 扩展了 DTW 和 Euclidian 两种 NativeScript。可以在界面上选择用其中某一种算法来做相似度打分: 然后相似度最高的几个时序图就依次排列出来了。 Euclidian 即欧几里得距离,是时序相似度计算里最基础的方式。 ELKstack 中文指南 340Etsy的Kale异常检测 DWT 即动态时间规整(Dynamic Time Warping),也是时序相似度计算的常用方式,它和欧几 里得距离的差别在于,欧几里得距离要求比对的时序数据是一一对应的,而动态时间规整计 算的时序数据并不要求长度相等。在运维监控来说,也就是延后一定时间发生的相近趋势也 可以以很高的打分项排名靠前。 不过,oculus 插件仅更新到支持 Elasticsearch-0.90.3 版本为止。Etsy 性能优化团队在 Oreilly 2015 大会上透露,他们内部已经根据 Kale 的经验教训,重新开发了 Kale 2.0 版。会 在年内开源放出来。大家一起期待吧! 参考阅读 http://codeascraft.com/2013/06/11/introducing-kale/ ELKstack 中文指南 341Etsy的Kale异常检测 简介 Logstash 早期曾经自带了一个特别简单的 logstash-web 用来查看 ES 中的数据。其功能太过 简单,于是 Rashid Khan 用 PHP 写了一个更好用的 web,取名叫 Kibana。这个 PHP 版本 的 Kibana 发布时间是 2011 年 12 月 11 日。 Kibana 迅速流行起来,不久的 2012 年 8 月 19 日,Rashid Khan 用 Ruby 重写了 Kibana, 也被叫做 Kibana2。因为 Logstash 也是用 Ruby 写的,这样 Kibana 就可以替代原先那个简 陋的 logstash-web 页面了。 目前我们看到的 angularjs 版本 kibana 其实原名叫 elasticsearch-dashboard,但跟 Kibana2 作者是同一个人,换句话说,kibana 比 logstash 还早就进了 elasticsearch 名下。这个项目改 名 Kibana 是在 2014 年 2 月,也被叫做 Kibana3。全新的设计一下子风靡 DevOps 界。随后 其他社区纷纷借鉴,Graphite 目前最流行的 Grafana 界面就是由此而来,至今代码中还留存 有十余处 kbn 字样。 2014 年 4 月,Kibana3 停止开发,ES 公司集中人力开始 Kibana4 的重构,在 2015 年初发 布了使用 JRuby 做后端的 beta 版后,于 3 月正式推出使用 node.js 做后端的正式版。由于设 计思路上的差别,一些 K3 适宜的场景并不在 K4 考虑范围内,所以,至今 K3 和 K4 并存使 用。本书也会分别讲解两者。 注释 本章配置参数内容部分译自Elasticsearch 官方指南 Kibana 部分,其中 v3 的 panel 部分额外 添加了截图注释。然后在使用的基础上,添加了原创的源码解析,场景示例,二次开发入门 等内容。 ELKstack 中文指南 342Kibana K3 和 K4 对比 Kibana 4 正式版 2015 年初发布,至今已近半年。但是本书依然将 Kibana 3 和 4 两个版本分 别作阐述,并推荐大家同时了解两个系统。因为二者分别基于不同接口,不同目的,采取了 不同的页面设计和逻辑。在不同场景下,各有优势。这里稍作解释,免受凑字骗钱之讥。 Kibana 3 的设计思路和功能 本书一开始就提到,Kibana3 在设计之初,有另一个名字,叫 elasticsearch dashboard。 事实上,整个 Kibana3 就是一个围绕着 dashboard 构建的单页应用。 所以,在页面逻辑上,Kibana3 异常简洁。大量的代码和逻辑,都下放到 panel 层次上。每 个 panel 要独立完成自己的可视化设计、数据请求,数据处理,数据渲染。panel 和 panel 之 间,则几乎毫无关联。简单一点看,整个页面就像是一堆 iframe 一样。 而 panel 的设计,则是以使用者角度来考虑的。Kibana3 尽量提供能让运维人员一步到位的 使用策略。即,使用者只需要了解 panel 的配置页面能填什么参数,得到什么可视化结果。 最明显的例子,就是 trend panel。trend panel 背后,其实是针对今天和昨天,分别发起两次 请求,然后再拿两次请求的结果,做一次除法,计算涨跌幅。这个除法计算,是在浏览器端 完成的。 类似在浏览器端完成的,还有 histogram panel 的 hits,second 等的计算。 此外,Kibana3 还有一个非常有用的功能,setting 中的 index pattern,是可以输入多个的, 比如 accesslog-[YYYY.MM.DD],syslog-[YYYY.MM.DD] ,这样就可以在同一个面板上,看到来自 不同索引的数据的情况。 Kibana 4 的设计思路和功能 从新特性来说,Kibana4 全面支持 Aggregation 接口,还有更多的可视化选择,可以任意拖动 自动对齐的挂件框架,保存在 URL 可以跨页面保持的检索条件,以及对页面请求的内部排队 机制。 从页面设计来说,Kibana4 参考了 Splunk 的产品形态,将功能拆分成了搜索,可视化和仪表 盘三个标签页。可视化和搜索,是一一绑定的,无法跨多个 index pattern 做搜索,勿论可视 化了。而且可视化标签页中,用 d3.js 实现的可视化构建器,与请求 ES 数据的聚合选择器, 又是各自独立的插件。 ELKstack 中文指南 343k3和k4的对比 也就是说,Kibana4 在使用 Aggregation 接口提供更复杂功能和更高性能的同时,彻底改变了 用户的使用形式。用户必须明确了解 ES 各个 aggs 接口的意义,请求和响应体的数据情况; 还要想清楚可视化的展现形式,充分理解数据字段的作用。然后才能实现想要的结果。毫无 疑问,这是有学习成本的。 至于像 Kibana3 那种在浏览器端计算的功能,Kibana4 中则完全没有。ES 2.0 将会提供一种 pipeline aggregation 特性,目前猜测或许 Kibana4 会在这个 ES 新特性的基础上来实现类似 功能。 在界面美观方面。Kibana4 至今未提供类似 Kibana3 中的 Query 设置功能,包括 Query 别名 和颜色选择器这两个常用功能都没有。直接导致目前 Kibana4 的图例几乎毫无作用。 在 filter 方面,Kibana4 用 filter agg 替代了 Kibana3 使用的 facet_filter。页面表现形式上, Kibana3 是在页面顶部添加 Query 输入框,全局生效;Kibana4 是在 Visualize 页添加 aggs,单个面板生效。依然需要多查询条件对比的用户,需要一个个面板创建,非常麻烦。 ELKstack 中文指南 344k3和k4的对比 简介 Kibana3 是一个使用 Apache 开源协议,基于浏览器的 Elasticsearch 分析和搜索仪表板。 Kibana3 非常容易安装和使用。整个项目都是用 HTML 和 Javascript 写的,所以 Kibana3 不 需要任何服务器端组件,一个纯文本发布服务器就够了。Kibana 和 Elasticsearch 一样,力争 成为极易上手,但同样灵活而强大的软件。 ELKstack 中文指南 345k3 10 分钟入门 Kibana 对实时数据分析来说是特别适合的工具。本节内容首先让你快速入门,了解 Kibana 所能做的大部分事情。如果你还没下载 Kibana,点击右侧链接:下载 Kibana。我们建议你在 开始本教程之前,先部署好一个干净的 elasticsearch 进程。 到本节结束,你就会: 导入一些数据 尝试简单的仪表板 搜索你的数据 配置 Kibana 只显示你的新索引而不是全部索引 我们假设你已经: 在自己电脑上安装好了 Elasticsearch 在自己电脑上搭建好了网站服务器,并把 Kibana 发行包解压到了发布目录里 对 UNIX 命令行有一点了解,使用过 curl 导入数据 我们将使用莎士比亚全集作为我们的示例数据。要更好的使用 Kibana,你需要为自己的新索 引应用一个映射集(mapping)。我们用下面这个映射集创建"莎士比亚全集"索引。实际数据的 字段比这要多,但是我们只需要指定下面这些字段的映射就可以了。注意到我们设置了对 speaker 和 play_name 不分析。原因会在稍后讲明。 在终端运行下面命令: curl -XPUT http://localhost:9200/shakespeare -d ' { "mappings" : { "_default_" : { "properties" : { "speaker" : {"type": "string", "index" : "not_analyzed" }, "play_name" : {"type": "string", "index" : "not_analyzed" }, "line_id" : { "type" : "integer" }, "speech_number" : { "type" : "integer" } } } } } '; ELKstack 中文指南 346kibana3入门 很棒,我们这就创建好了索引。现在需要做的时导入数据。莎士比亚全集的内容我们已经整 理成了 elasticsearch 批量 导入所需要的格式,你可以通过shakeseare.json下载。 用如下命令导入数据到你本地的 elasticsearch 进程中。这可能需要一点时间,莎士比亚可是 著作等身的大文豪! curl -XPUT localhost:9200/_bulk --data-binary @shakespeare.json 访问 Kibana 界面 现在你数据在手,可以干点什么了。打开浏览器,访问已经发布了 Kibana 的本地服务器。 如果你解压路径无误(译者注:使用 github 源码的读者记住发布目录应该是 kibana/src/ 里 面),你已经就可以看到上面这个可爱的欢迎页面。点击 Sample Dashboard 链接 ELKstack 中文指南 347kibana3入门 好了,现在显示的就是你的 sample dashboard!如果你是用新的 elasticsearch 进程开始本教 程的,你会看到一个百分比占比很重的饼图。这里显示的是你的索引中,文档类型的情况。 如你所见,99% 都是 lines,只有少量的 acts 和scenes。 再下面,你会看到一长段 JSON 格式的莎士比亚诗文。 第一次搜索 Kibana 允许使用者采用 Lucene Query String 语法搜索 Elasticsearch 中的数据。请求可以在 页面顶部的请求输入框中书写。 在请求框中输入如下内容。然后查看表格中的前几行内容。 friends, romans, countrymen ELKstack 中文指南 348kibana3入门 关于搜索请求的语法,请阅读稍后 Query 和 Filtering 小节。 配置另一个索引 目前 Kibana 指向的是 Elasticsearch 一个特殊的索引叫 _all 。 _all 可以理解为全部索引 的大集合。目前你只有一个索引, shakespeare ,但未来你会有更多其他方面的索引,你肯 定不希望 Kibana 在你只想搜《麦克白》里心爱的句子的时候还要搜索全部内容。 配置索引,点击右上角的配置按钮: 在这里,你可以设置你的索引为 shakespeare ,这样 Kibana 就只会搜索 shakespeare 索引 的内容了。 下一步 ELKstack 中文指南 349kibana3入门 恭喜你,你已经学会了安装和配置 Kibana,算是正式下水了!下一步,打开我们的视频和其 他教程学习更高级的技能吧。现在,你可以尝试在一个空白仪表板上添加自己的面板。这方 面的内容,请阅读稍后 row 和 panel 小节。 译注 在 Elasticsearch 发布 1.4 版后,使用 kibana3 访问 ES1.4 集群,会显示如下错误: 这是因为 ES1.4 增强了权限管理。你需要在 ES 配置文件 elasticsearch.yml 中添加下列配置 并重启服务后才能正常访问: http.cors.enabled: true http.cors.allow-origin: "*" 记住 kibana3 页面也要刷新缓存才行。 此外,如果你可以很明确自己 kibana 以外没有其他 http 访问,可以把 kibana 的网址写在 http.cors.allow-origin 参数的值中。比如: http.cors.allow-origin: "/https?:\/\/kbndomain/" ELKstack 中文指南 350kibana3入门 配置 config.js 是 Kibana 核心配置的地方。文件里包括的参数都是必须在初次运行 kibana 之前提 前设置好的。在你把 Kibana 和 ES 从自己个人电脑搬上生产环境的时候,一定会需要修改这 里的配置。下面介绍几个最常见的修改项: 参数 elasticsearch 你 elasticsearch 服务器的 URL 访问地址。你应该不会像写个 http://localhost:9200 在这, 哪怕你的 Kibana 和 Elasticsearch 是在同一台服务器上。默认的时候这里会尝试访问你部署 kibana 的服务器上的 ES 服务,你可能需要设置为你 elasticsearch 服务器的主机名。 注意:如果你要传递参数给 http 客户端,这里也可以设置成对象形式,如下: +elasticsearch: {server: "http://localhost:9200", withCredentials: true}+ default_route 没有指明加载哪个仪表板的时候,默认加载页路径的设置参数。你可以设置为文件,脚本或 者保存的仪表板。比如,你有一个保存成 "WebLogs" 的仪表板在 elasticsearch 里,那么你就 可以设置成: default_route: /dashboard/elasticsearch/WebLogs, kibana_index 用来保存 Kibana 相关对象,比如仪表板,的 Elasticsearch 索引名称。默认为 kibana-int 。 panel_name 可用的面板模块数组。面板只有在仪表板中有定义时才会被加载,这个数组只是用在 "add panel" 界面里做下拉菜单。 ELKstack 中文指南 351config.js配置 保存和加载 你已经构建了一个漂亮的仪表板!现在你打算分享给团队,或者开启自动刷新后挂在一个大 屏幕上?Kibana 可以把仪表板设计持久化到 Elasticsearch 里,然后在需要的时候通过加载菜 单或者 URL 地址调用出来。 保存你漂亮的仪表板 保存你的界面非常简单,打开保存下拉菜单,取个名字,然后点击保存图表即可。现在你的 仪表板就保存在一个叫做 kibana-int 的 Elasticsearch 索引里了。 调用你的仪表板 ELKstack 中文指南 352dashboard的保存和载入 要搜索已保存的仪表板列表,点击右上角的加载图标。在这里你可以加载,分享和删除仪表 板。 分享仪表板 已保存的仪表板可以通过你浏览器地址栏里的 URL 分享出去。每个持久化到 Elasticsearch 里的仪表板都有一个对应的 URL,像下面这样: http://your_host/index.html#/dashboard/elasticsearch/MYDASHBOARD 这个示例中 MYDASHBOARD 就是你在保存的时候给仪表板取得名字。 你还可以分享一个即时的仪表板链接,点击 Kibana 右上角的分享图标,会生成一个临时 URL。 默认情况下,临时 URL 保存 30 天。 保存成静态仪表板 ELKstack 中文指南 353dashboard的保存和载入 仪表板可以保存到你的服务器磁盘上成为 .json 文件。把文件放到 app/dashboards 目录, 然后通过下面地址访问 http://your_host/index.html#/dashboard/file/MYDASHBOARD.json MYDASHBOARD.json 就是磁盘上文件的名字。注意路径中得 /#dashboard/file/ 看起来跟之前 访问保存在 Elasticsearch 里的仪表板很类似,不过这里访问的是文件而不是 elasticsearch。 导出的仪表板纲要的详细信息,阅读稍后 scheme 简介小节。 下一步 你现在知道怎么保存,加载和访问仪表板了。你可能想知道怎么通过 URL 传递参数来访问, 这样可以在其他应用中直接链接过来。请阅读稍后 scripted 用法 和 template 用法 两节。 ELKstack 中文指南 354dashboard的保存和载入 布局 之前说过,Kibana3 是一个单页应用。所以其页面布局设计是固定的。顶部栏作为全局设 置,接下来是全局 Query 栏,全局 Filtering 栏,以及具体可视化图表的部分。 本节就介绍这两部分的操作方式和用法。 ELKstack 中文指南 355布局 请求和过滤 图啊,表啊,地图啊,Kibana 有好多种图表,我们怎么控制显示在这些图表上的数据呢?这 就是请求和过滤起作用的地方。 Kibana 是基于 Elasticsearch 的,所以支持强大的 Lucene Query String 语法,同样还能用上 Elasticsearch 的过滤器能力。 我们的仪表板 我们的仪表板像下面这样,可以搜索莎士比亚文集的内容。如果你喜欢本章截图的这种仪表 板样式,你可以下载导出的仪表板纲要(dashboard schema) 请求 在搜索栏输入下面这个非常简单的请求 to be or not to be ELKstack 中文指南 356布局 你会注意到,表格里第一条就是你期望的《哈姆雷特》。不过下一行却是《第十二夜》的安 德鲁爵士,这里可没有"to be",也没有"not to be"。事实上,这里匹配上的是 to OR be OR or OR not OR to OR be 。 我们需要这么搜索(译者注:即加双引号)来匹配整个短语: "to be or not to be" 或者指明在某个特定的字段里搜索: line_id:86169 我们可以用 AND/OR 来组合复杂的搜索,注意这两个单词必须大写: food AND love 还有括号: ("played upon" OR "every man") AND stage 数值类型的数据可以直接搜索范围: line_id:[30000 TO 80000] AND havoc 最后,当然是搜索所有: * 多个请求 有些场景,你可能想要比对两个不同请求的结果。Kibana 可以通过 OR 的方式把多个请求连 接起来,然后分别进行可视化处理。 添加请求 点击请求输入框右侧的 + 号,即可添加一个新的请求框。 ELKstack 中文指南 357布局 点击完成后你应该看到的是这样子 在左边,绿色输入框,输入 "to be" 然后右边,黄色输入框,输入 "not to be" 。这就会搜 索每个包含有 "to be" 或者 "not to be" 内容的文档,然后显示在我们的 hits 饼图上。我 们可以看到原先一个大大的绿色圆形变成下面这样: 移除请求 要移除一个请求,移动鼠标到这个请求输入框上,然后会出现一个 x 小图标,点击小图标即 可: ELKstack 中文指南 358布局 颜色和图例 Kibana 会自动给你的请求分配一个可用的颜色,不过你也可以手动设置颜色。点击请求框左 侧的彩色圆点,就可以弹出请求设置下拉框。这里面可以修改请求的颜色,或者设置为这个 请求设置一个新的图例文字: 过滤 很多 Kibana 图表都是交互式的,可以用来过滤你的数据视图。比如,点击你图表上的第一个 条带,你会看到一些变动。整个图变成了一个大大的绿色条带。这是因为点击的时候,就添 加了一个过滤规则,要求匹配 play_name 字段里的单词。 ELKstack 中文指南 359布局 你要问了“在哪里过滤了”? 答案就藏在过滤(FILTERING)标签上出现的白色小星星里。点击这个标签,你会发现在 filtering 面板里已经添加了一个过滤规则。在 filtering 面板里,可以添加,编辑,固定,删除 任意过滤规则。很多面板都支持添加过滤规则,包括表格(table),直方图(histogram),地图 (map)等等。 过滤规则也可以自己点击 + 号手动添加。 ELKstack 中文指南 360布局 行和面板 Kibana 仪表板的可视化图标部分是由行和面板组成的。这些都可以随意的添加,删除和重 组。 这节我们会介绍: 加载一个空白仪表板 添加,隐藏行,以及修改行高 添加面板和修改面板宽度 删除面板和行 我们假设你已经: 在自己电脑上安装好了 Elasticsearch 在自己电脑上搭建好了网站服务器,并把 Kibana 发行包解压到了发布目录里 按照之前 kibana 入门小节的内容创建好了存有莎士比亚文集的索引 加载一个空白仪表板 从主屏里选择第三项,就会加载一个空白仪表板(Blank Dashboard)。默认情况下,空白仪表 板会搜索 Elasticsearch 的 _all 索引,也就是你的全部索引。要指定搜索某个索引的方法, 已在 kibana 入门小节中介绍。 ELKstack 中文指南 361布局 添加一行 你的新空白仪表板上只有展开的请求和过滤区域,页面顶栏上有个时间过滤选择器,除此以 外什么都没有。在右下方,点击添加行(ADD A ROW)按钮,添加你的第一行。 给你的行取个名字,然后点击创建(Create Row)按钮。你会看到你的新行出现在左侧的行列 表里。点击保存(Save) 行的控制 现在你有了一行,你会注意到仪表板上多了点新元素。主要是左侧多出来的三个小小的不同 颜色的长方形。移动鼠标到它们上面 ELKstack 中文指南 362布局 哈哈!看到了吧,这三个按钮是让你做这三件事情的: 折叠行(蓝色) 配置行(橘色) 添加面板(绿色) 添加面板 现在我们专注在行控制力的绿色按钮上,试试点击它。你也可以点击空白行内的灰色按钮 (Add panel to empty row),不过它是灰色的啊,有啥意思…… 让我们来添加一个 terms 面板。terms 面板可以让我们用上 Elasticsearch 的 terms facet 功 能,查找一个字段内最经常出现的几个值。 你可以看到,terms 面板有一系列可配置选选,不过我们现在先只管第一段里德通用配置好 了: ELKstack 中文指南 363布局 1. Title: 面板的名称 2. Span: 面板的宽度。Kibana 仪表板等分成 12 个 spans 面板最大就是到 12 个 spans 宽。但是行可以容纳超过 12 个 spans 的总宽度,因为它会自动把新的面板放到下面显 示。现在我们先设置为 4。 3. Editable: 面板是否在之后可以继续被编辑。现在先略过。 4. Inspectable: 面板是否允许用户查看所用的请求内容。现在先略过。 5. 点击 Save 添加你的新 terms 面板到你的仪表板 译者注:面板宽度也可以在仪表板内直接拖拽修改,将鼠标移动至面板左(右)侧边线处,鼠标 会变成相应的箭头,按住左键拖拽成满意宽度松开即可 太棒了!你现在有一个面板了!你可能意识到这个数据跟 kibana 入门中的饼图数据一样。 shakespeare 数据集集中在 lines,还有少量的 acts 和 scenes。 折叠和展开行 ELKstack 中文指南 364布局 蓝色按钮可以折叠你的行。被折叠行里的面板不会刷新数据,也就不要求 Elasticsearch 资 源。所以折叠行可以用于那些你不需要经常看的数据。有需要的时候点击蓝色按钮展开就可 以了。 顶部的请求和过滤区域也可以被折叠。点击彩色标签就可以折叠和展开。 编辑行 通过行编辑器,可以给行重命名,改行高等其他配置。点击橙色按钮打开行编辑器。 这个对话框还允许你修改面板的排序和大小,以及删除面板。 ELKstack 中文指南 365布局 移动和删除面板 面板可以在本行,甚至其他行之间任意拖拽。按住面板右上角的十字架形状小图标然后拖动 即可。 点击面板右上角的 remove 小图标就可以从仪表板上移除它。前面说到从行编辑器上也可以做 到统一效果。 ELKstack 中文指南 366布局 移动和删除行 行可以在仪表板配置页中重新排序和删。点击屏幕右上角的配置按钮,选择行(Rows)标签切 换到行配置层。看到这里你一定会记起来我们在添加第一个行时候的屏幕。 左侧的箭头用来修改仪表板上行的次序。X 用来删除行。 下一步 在你关闭浏览器之前,你可能打算保存这个新仪表板。请按照之前 dashboard 的保存和载入 小节的介绍操作。 ELKstack 中文指南 367布局 面板 Kibana 仪表板由面板( panels )块组成。面板在行中可以起到很多作用,不过大多数是用来给 一个或者多个请求的数据集结果做可视化。剩下一些面板则用来展示数据集或者用来为使用 者提供插入指令的地方。 面板可以很容易的通过 Kibana 网页界面配置。为了了解更高级的用法,比如模板化或者脚本 化仪表板,这章开始介绍面板属性。你可以发现一些通过网页界面看不到的设置。 每个面板类型都有自己的属性,不过有这么几个是大家共有的。 span 一个从 1-12 的数字,描述面板宽度。 editable 是否在面板上显示编辑按钮。 type 本对象包含的面板类型。每个具体的面板类型又要求添加新的属性,具体列表说明稍后详 述。 译作者注 官方文档中只介绍了各面板的参数,而且针对的是要使用模板化,脚本化仪表板的高级用 户,其中有些参数甚至在网页界面上根本不可见。 为了方便初学者,我在每个面板的参数介绍之后,自行添加界面操作和效果方面的说明。 另外,本书 Kibana3 介绍基于我的 https://github.com/chenryn/kibana.git 分支,其中有十多 处增强功能式的扩展。读者如果发现操作介绍中内容在自己界面上找不到的,可以尝试替换 我的版本。 ELKstack 中文指南 368各panel功能 histogram 状态:稳定 histogram 面板用以显示时间序列图。它包括好几种模式和变种,用以显示时间的计数,平均 数,最大值,最小值,以及数值字段的和,计数器字段的导数。 参数 轴(axis)参数 mode 用于 Y 轴的值。除了 count 以外,其他 mode 设置都要求定义 value_field 参数。可选 值为:count, mean, max, min, total。我的 fork 中新增了一个可选值为 uniq。 time_field X 轴字段。必须是在 Elasticsearch 中定义为时间类型的字段。 value_field 如果 mode 设置为 mean, max, min 或者 total,Y 轴字段。必须是数值型。 x-axis 是否显示 X 轴。 y-axis 是否显示 Y 轴。 scale 以该因子规划 Y 轴 y_format Y 轴数值格式,可选:none, bytes, short 注释 注释对象 可以指定一个请求的结果作为标记显示在图上。比如说,标记某时刻部署代码了。 annotate.enable ELKstack 中文指南 369各panel功能 是否显示注释(即标记) annotate.query 标记使用的 Lucene query_string 语法请求 annotate.size 最多显示多少标记 annotate.field 显示哪个字段 annotate.sort 数组排序,格式为 [field,order]。比如 [‘@timestamp’,‘desc’] ,这是一个内部参数。 auto_int 是否自动调整间隔 resolution 如果 auto_int 设为真,shoot for this many bars. interval 如果 auto_int 设为假,用这个值做间隔 intervals 在 View 选择器里可见的间隔数组。比如 [‘auto’,‘1s’,‘5m’,‘3h’],这是绘图参数。 lines 显示折线图 fill 折线图的区域填充因子,从 1 到 10。 linewidth 折线的宽度,单位为像素 points 在图上显示数据点 pointradius 数据点的大小,单位为像素 ELKstack 中文指南 370各panel功能 bars 显示条带图 stack 堆叠多个序列 spyable 显示审核图标 zoomlinks 显示 ‘Zoom Out’ 链接 options 显示快捷的 view 选项区域 legend 显示图例 show_query 如果没设别名(alias),是否显示请求 interactive 允许点击拖拽进行放大 legend_counts 在图例上显示计数 timezone 是否调整成浏览器时区。可选值为:browser, utc percentage 把 Y 轴数据显示成百分比样式。仅对多个请求时有效。 zerofill 提高折线图准确度,稍微消耗一点性能。 derivative 在 X 轴上显示该点数据在前一个点数据上变动的数值。 提示框(tooltip)对象 ELKstack 中文指南 371各panel功能 tooltip.value_type 控制 tooltip 在堆叠图上怎么显示,可选值:独立(individual)还是累计(cumulative) tooltip.query_as_alias 如果没设别名(alias),是否显示请求 网格(grid)对象 Y 轴的最大值和最小值 grid.min Y 轴的最小值 grid.max Y 轴的最大值 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected queries.ids 如果设为 selected 模式,具体被选的请求编号。 界面配置说明 添加面板的方式在之前的"行和面板"一节中已经有过讲解。在 "Add panel"对话框选择类型为 "histogram" 后,你会看到一系列可配置的选项: ELKstack 中文指南 372各panel功能 选项分为四类,可以在添加之后,通过点击面板右上角的配置"Configure"小图标弹出浮层继 续修改。 通用(General)配置 主要用来修改面板的标题和宽度 面板(Panel)配置 ELKstack 中文指南 373各panel功能 设置面板向 Elasticsearch 发出何种请求,以及请求中需要使用的变量。 在 histogram 面板中,经常用的 Chart value (即参数部分描述的 mode ) 有: count 最常见场景就是统计请求数。这种时候只需要提供一个在 Elasticsearch 中是时间 类型的字段(即参数部分描述的 time_field )即可。一般来说,都是 @timestamp , 所以不用修改了。这也是默认的 Logstash 仪表板的基础面板的样式 mean 最常见场景就是统计平均时间。这时候配置浮层会变成下面这个样子: 这里就需要提供一个在 Elasticsearch 中是数值类型的字段(即参数部分描述的 value_field )作为计算平均值的数据集来源了。以 nginx 访问日志为例,这里就填 "request_time"。 ELKstack 中文指南 374各panel功能 如果你在 Logstash 中使用的是 %{NUMBER:request_time} ,那么实际类型还是字符串 (请记住,正则捕获是 String 类的方法,也只能生成 String 结果),必须写成 % {NUMBER:request_time:float} 强制转换才行。否则,你会看到如下报错信息: total 最常见场景就是统计带宽。配置界面和 mean 是一样的。同样要求填写数值类型的 字段名,比如 "bytes_sent"。 带宽在习惯上会换算成每秒数据,但是通过修改 interval 的方式来求每秒数据, 对 elasticsearch 性能是一个很大的负担,绘制出来的图形也太过密集影响美观。所 以 Kibana 提供了另一种方式:保持 interval ,勾选 seconds 。Kibana 会自动将 每个数值除以间隔秒数得到每秒数据。( count 也可以这样,用来计算 qps 等数据) ELKstack 中文指南 375各panel功能 另一个有用的功能,假如你的日志量实在太大,被迫采用抽样日志的方式,可以在 Kibana 上填写 Scale 。比如百分之一的抽样日志, Scale 框就写 100,带宽数据 就会在展示的时候自动翻 100 倍显示出来。 uniq ELKstack 中文指南 376各panel功能 ES 从 1.1 版本开始通过 HyperLogLog++ 算法支持去重统计聚合。在用 Aggregation API 重写了 histogram panel 后,也可以支持了。 常用场景比如:UV 统计。效果如下: 风格(Style)配置 ELKstack 中文指南 377各panel功能 设置获取的数据如何展现。其中小部分(即条带(Bars)、折线(Lines)、散点(Points))可以直 接在面板左上角的 "View" 下拉菜单里直接勾选。 对于带宽数据,可以切换 Y Format 为 bytes。则 Y 轴数据可以自动换算成 MB,GB 的 形式,比较方便 此外,还可以在 Grid 区域定义 Y 轴的起始点和终点的具体值。这可以用来在 Y 轴上放大 部分区域,观察细微变动;或者忽略某些异常值。 ELKstack 中文指南 378各panel功能 如果面板关联了多个请求,可以勾选以堆叠( Stack )方式展示(最常见的堆叠展示的监控 数据就是 CPU 监控)。 堆叠的另一种形式是百分比。在勾选了 Stack 的前提下勾选 Percent 。 ELKstack 中文指南 379各panel功能 效果如下。注意:百分比是 A / (A + B) 的值,而不是 A / B。 关联请求(Queries)配置 ELKstack 中文指南 380各panel功能 默认的 Queries 方式是 all 。可以使用 selected 方式,在右侧选择具体的请求框(可 多选)。被选中的会出现边框加粗放大效果。 多请求的默认效果如下。而堆叠和百分比效果,在之前已经谈过。可以对比上下两图的 Y 轴刻度: ELKstack 中文指南 381各panel功能 table 状态:稳定 表格面板里是一个可排序的分页文档。你可以定义需要排列哪些字段,并且还提供了一些交 互功能,比如执行 terms 聚合查询。 参数 size 每页显示多少条 pages 展示多少页 offset 当前页的页码 sort 定义表格排序次序的数组,示例如右:[‘@timestamp’,‘desc’] overflow css 的 overflow 属性。‘min-height’ (expand) 或 ‘auto’ (scroll) fields 表格显示的字段数组 highlight 高亮显示的字段数组 sortable 设为假关掉排序功能 header 设为假隐藏表格列名 paging ELKstack 中文指南 382各panel功能 设为假隐藏表格翻页键 field_list 设为假隐藏字段列表。使用者依然可以展开它,不过默认会隐藏起来 all_fields 设为真显示映射表内的所有字段,而不是表格当前使用到的字段 trimFactor 裁剪因子(trim factor),是参考表格中的列数来决定裁剪字段长度。比如说,设置裁剪因 子为 100,表格中有 5 列,那么每列数据就会被裁剪为 20 个字符。完整的数据依然可以 在展开这个事件后查看到。 localTime 设为真调整 timeField 的数据遵循浏览器的本地时区。 timeField 如果 localTime 设为真,该字段将会被调整为浏览器本地时区。 spyable 设为假,不显示审查(inspect)按钮。 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected queries.ids 如果设为 selected 模式,具体被选的请求编号。 界面配置说明 table 和 histogram 面板,是 kibana 默认的 logstash 仪表板里唯二使用的面板。可以说是最 重要和常用的组件。 虽然重要,table 面板的可配置项却不多。主要是 panel 和 paging 两部分: ELKstack 中文指南 383各panel功能 panel panel 设置可以分成几类,其中比较重要和有用的是: 时间字段 Time Field 设置的作用,和 histogram 面板中类似,主要是帮助 Kibana 使用者自动转换 elasticsearch 中的 UTC 时间成本地时间。 裁剪因子 和 Splunk 不同,Kibana 在显示事件字段的时候,侧重于单行显示。详情内容通过点击具体 某行向下展开的方式参看。每个字段在屏幕中的可用宽度,就会通过裁剪因子来计算。计算 方式见官方参数说明部分。 字段列表 table 面板左侧,是字段列表多选区域。字段分为 _all 和 current 两种。_all 是 Kibana 通过 elasticsearch 的 _mapping API 直接获取的索引内所有存在过的字段;current 则仅显示 table 匹配范围内的数据用到的字段。 勾选字段列表中某个字段,该字段就加入 table 面板右侧的表格中成为一列。 ELKstack 中文指南 384各panel功能 字段列表中,可以点击具体字段,查看 table 匹配范围内该字段数据的统计和排行数据的小面 板。 小面板上虽然只显示一个很小范围内(即size pages,默认是 500)的数据统计,但是点击小面 板底部的 *TERMS 下拉菜单选项,生成的 term panel 浮层数据却都是基于整个搜索结果的。 这部分的内容介绍。请阅读 term panel 章节。 ELKstack 中文指南 385各panel功能 排序 设置中可以设置一个默认的排序字段。在 logstash 仪表板默认的 event table 中,设置的是时 间字段 @timestamp 。不过这个设置,指的是面板加载的时候,使用该字段排序,实际你可 以在表头任意字段名上单击,以该字段的值来临时排序。排序字段会在表头本列字段名后, 出现一个小三角图标,三角箭头朝上代表升序,反之降序: 高亮 elasticsearch 作为一种搜索引擎,很贴心的提供了高亮功能。Kibana 中也同样支持解析 ES 返回的 HTML 高亮文本。只需要在 panel 标签页右侧添加 Highlighted field ,在搜索框里 填入的关键词,如果出现在被指定为 Highlighted field 的字段里,这个词在 table 里就会高亮 显示(前提是该字段已经在字段列表中勾选)。 小贴士:高亮仅在 table 状态有效,点击展开后的事件详情中是不会高亮的。 paging ELKstack 中文指南 386各panel功能 考虑到同时展示太多内容,一来对 elasticsearch 压力较大,二来影响页面展示效果和渲染性 能。 注意: paging 其实是一次请求下来设定大小的全部数据,然后在浏览器上分页显示,而不是 调用 scroll API 来逐步显示。所以,千万不要设置太大! ELKstack 中文指南 387各panel功能 map 状态:稳定 map 面板把 2 个字母的国家或地区代码转成地图上的阴影区域。目前可用的地图包括世界地 图,美国地图和欧洲地图。 参数 map 显示哪个地图:world, usa, europe colors 用来涂抹地图阴影的颜色数组。一旦设定好这 2 个颜色,阴影就会使用介于这 2 者之间 的颜色。示例 [‘#A0E2E2’, ‘#265656’] size 阴影区域的最大数量 exclude 排除的区域数组。示例 [‘US’,‘BR’,‘IN’] spyable 设为假,不显示审查(inspect)按钮。 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected queries.ids 如果设为 selected 模式,具体被选的请求编号。 ELKstack 中文指南 388各panel功能 界面配置说明 考虑到都是中国读者,本节准备采用中国地图进行讲解,中国地图代码,,基于本人 https://github.com/chenryn/kibana.git 仓库,除标准的 map 面板参数外,还提供了 terms_stats 功能,也会一并讲述。 map 面板,最重要的配置即输入字段,对于不同的地图,应该配置不同的 Field : world 对于世界地图,其所支持的格式为由 2 个字母构成的国家名称缩写,比 如: US , CN , JP 等。如果你使用了 LogStash::Filters::GeoIP 插件,那么默认生成 的 geoip.country_code2 字段正好符合条件。 cn 对于中国地图,其所支持的格式则是由 2 个数字构成的省份编码,比如: 01 (即安 徽), 30 (即广东), 04 (即江苏)等。如果你使用了 LogStash::Filters::GeoIP 插件,那 么默认生成的 geoip.region_name 字段正好符合条件。 ELKstack 中文指南 389各panel功能 如果你使用了我的仓库代码,或者自行合并了该功能,你的 map 面板配置界面会稍有变动成 下面样子 如果选择 terms_stats 模式,就会和 histogram 面板一样出现需要填写 value_field 的位 置。同样必须使用在 Elasticsearch 中是数值类型的字段,然后显示的地图上,就不再是个 数而是具体的均值,最大值等数据了。 ELKstack 中文指南 390各panel功能 ELKstack 中文指南 391各panel功能 bettermap 状态:实验性 Bettermap 之所以叫 bettermap 是因为还没有更好(better)的名字。Bettermap 使用地理坐标 来在地图上创建标记集群,然后根据集群的密度,用橘色,黄色和绿色作为区分。 要查看细节,点击标记集群。地图会放大,原有集群分裂成更小的集群。一直小到没法成为 集群的时候,单个标记就会显现出来。悬停在标记上可以查看 tooltip 设置的值。 注意: bettermap 需要从互联网上下载它的地图面板文件。 参数 field 包含了地理坐标的字段,要求是 geojson 格式。GeoJSON 是一个数组,内容为 [longitude,latitude] 。这可能跟大多数实现([latitude, longitude])是反过来的。 size 用来绘制地图的数据集大小。默认是 1000。注意:table panel 默认是展示最近 500 条, 跟这里的大小不一致,可能引起误解。 spyable 设为假,不显示审查(inspect)按钮。 tooltip 悬停在标记上时显示哪个字段。 provider 选择哪家地图提供商。 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected ELKstack 中文指南 392各panel功能 queries.ids 如果设为 selected 模式,具体被选的请求编号。 界面配置说明 bettermap 面板是为了解决 map 面板地图种类太少且不方便大批量添加各国地图文件的问题 开发的。它采用了 leaflet 库,其 L.tileLayer 加载的 OpenStreetMap 地图文件都是在使用 的时候单独请求下载,所以在初次使用的时候会需要一点时间才能正确显示。 在 bettermap 的配置界面,我提供了 provider ,可以选择各种地图提供商。比如中文用户可 以选择 GaoDe(高德地图): 注:最近,高德地图的 API 出现问题,该 provider 已经无法使用。 其他 leaflet 库有丰富的插件资源。比如 热力图 ELKstack 中文指南 393各panel功能 注:热力图插件最终在 Kibana4.1 中,作为 tile map 的新 option 加入了。 小贴士:其实 Kibana 官方效果的标记集群也是插件实现的,叫markercluster ELKstack 中文指南 394各panel功能 terms 状态:稳定 基于 Elasticsearch 的 terms facet 接口数据展现表格,条带图,或者饼图。 参数 field 用于计算 facet 的字段名称。 script 用于提交 facet 的 scriptField 脚本字符串。系我的 fork 中新增的功能,仅在 fmode 为 script 时生效。 exclude 要从结果数据中排除掉的 terms missing 设为假,就可以不显示数据集内有多少结果没有你指定的字段。 other 设为假,就可以不显示聚合结果在你的 size 属性设定范围以外的总计数值。 size 显示多少个 terms order terms 模式可以设置:count, term, reverse_count 或者 reverse_term;terms_stats 模式 可以设置:term, reverse_term, count, reverse_count, total, reverse_total, min, reverse_min, max, reverse_max, mean 或者 reverse_mean donut 在饼图(pie)模式,在饼中画个圈,变成甜甜圈样式。 tilt 在饼图(pie)模式,倾斜饼变成椭圆形。 ELKstack 中文指南 395各panel功能 lables 在饼图(pie)模式,在饼图分片上绘制标签。 arrangement 在条带(bar)或者饼图(pie)模式,图例的摆放方向。可以设置:水平(horizontal)或者垂直 (vertical)。 chart 可以设置:table, bar 或者 pie counter_pos 图例相对于图的位置,可以设置:上(above),下(below),或者不显示(none)。 spyable 设为假,不显示审查(inspect)按钮。 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected queries.ids 如果设为 selected 模式,具体被选的请求编号。 tmode Facet 模式:terms 或者 terms_stats。 fmode Field 模式:normal 或者 script。我的 fork 中新增参数,normal 行为和原版一致,选择 script 时,scriptField 参数生效。 tstat Terms_stats facet stats 字段。 valuefield Terms_stats facet value 字段。 ELKstack 中文指南 396各panel功能 界面配置说明 terms 面板是针对单项数据做聚合统计的面板。可配置项比较简单: 主要分为两部分,数据模式和显示风格。 数据模式 terms 面板能够使用两种数据模式(也是 Kibana 大多数面板所使用的): terms terms 即普通的分类计数(类比为 group by 语法)。填写具体字段名即可。此外,排序( order by )和结果数( limit )也可以定义,具体选项介绍见本页前半部分。 terms_stats terms_stats 在 terms 的基础上,获取另一个数值类型字段的统计值作为显示内容。可选的统 计值有:count, total_count, min, max, total, mean。最常用的就是 mean。 显示风格 terms 面板可以使用多个风格来显示数据。 bar ELKstack 中文指南 397各panel功能 pie 这时候可能就会觉得这个 "other value" 太大了,又不关心它。那么可以在配置里去掉 other 的勾选。图形会变成这样: ELKstack 中文指南 398各panel功能 如果勾选 "donut",则可以看到圈圈饼效果: table ELKstack 中文指南 399各panel功能 如果你是个对数字敏感的人,或者主要数据差距不大,通过 bar 或者 pie 方式不是很明显,那 么看表格最好了: 注意这个表格只有单列数据,使用配置里定义的排序,不像 table 面板。 如果你需要同时看多种统计数据,则应该使用 stats 面板。 script field 在 fmode 选择 script 的时候,可以填写 script 脚本字符串获取脚本化字段结果做聚合。 在 scriptField 输入框中输入 doc['path.raw'].value 的时候,效果完全等价于直接在 Field 输入框中输入 path.raw 因为 script 和 analyzer 的次序关系,务必使用带有 "not_analyzed" 属性的字段。否则一条 数据中只会有一个分词结果参与后续聚合运算。 支持的 script 语法,请参阅 ES 官方文 档:http://www.elasticsearch.org/guide/en/elasticsearch/reference/3.0/modules- scripting.html#_document_fields ELKstack 中文指南 400各panel功能 需要注意的是,出于安全考虑,ES 1.4 以下 大多建议关闭动态脚本运行的支持;在 1.4 新增 了沙箱运行并设置为默认。所以,建议在 ES 1.4 的前提下使用该特性。 ELKstack 中文指南 401各panel功能 column 状态:稳定 这是一个伪面板。目的是让你在一列中添加多个其他面板。虽然 column 面板状态是稳定,它 的限制还是很多的,比如不能拖拽内部的小面板。未来的版本里,column 面板可能被删除。 参数 panel 面板对象构成的数组 界面配置说明 column 面板是为了在高度较大的 row 中放入多个小 panel 准备的一个容器。其本身配置界面 和 row 类似只有一个 panel 列表: 在 column 中的具体的面板本身的设定,需要点击面板自带的配置按钮来配置: ELKstack 中文指南 402各panel功能 ELKstack 中文指南 403各panel功能 stats 状态: Beta 基于 Elasticsearch 的 statistical Facet 接口实现的统计聚合展示面板。 参数 format 返回值的格式。默认是 number,可选值还有:money,bytes,float。 style 主数字的显示大小,默认为 24pt。 mode 用来做主数字显示的聚合值,默认是 count,可选值为:count(计数),min(最小值), max(最大值),mean(平均值),total(总数),variance(方差),std_deviation(标准差), sum_of_squares(平方和)。 show 统计表格中具体展示的哪些列。默认为全部展示,可选列名即 mode 中的可选值。 spyable 设为假,不显示审查(inspect)按钮。 请求 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected queries.ids 如果设为 selected 模式,具体被选的请求编号。 ELKstack 中文指南 404各panel功能 query query 面板和 filter 面板都是特殊类型的面板,在 dashboard 上有且仅有一个。不能删除不能 添加。 query 和 filter 的普通样式和基本操作,在之前 Query 和 Filtering 章节已经讲述过。这里,额 外讲一下一些高阶功能。 请求类型 query 搜索框支持三种请求类型: lucene 这也是默认的类型,使用要点就是请求语法。语法说明在之前 Elasticsearch 一章已经讲过。 regex topN topN 是一个方便大家进行多项对比搜索的功能。其配置界面如下: ELKstack 中文指南 405各panel功能 注意 topN 后颜色选择器的小圆点变成了齿轮状! 其运行实质,是先根据你填写的 field 和 size,发起一次 termsFacet 查询,获取 topN 的 term 结果;然后拿着这个列表,逐一发起附加了 term 条件的其他请求(比如绑定在 histogram 面板 就是 date_histogram 请求,stats 面板就是 termStats 请求),也就获得了 topN 结果。 小贴士:如果 ES 响应较慢的时候,你甚至可以很明显的看到 histogram 面板上的多条曲线是 一条一条出来数据绘制的。 别名 query 还可以设置别名(alias)。默认没有别名的时候,各 panel 上显示对应 query 时,会使用 具体的 query 语句。在查询比较复杂的时候,不便观看。而设置别名后,pinned queries, panel 图例等处,都会只显示设置好的别名,而不再显示复杂的查询语句,这样一目了然。 ELKstack 中文指南 406各panel功能 trends 状态: Beta 以证券报价器风格展示请求随着时间移动的情况。比如说:当前时间是 1:10pm,你的时间选 择器设置的是 "Last 10m",而本面板的 "Time Ago" 参数设置的是 "1h",那么面板会显示的是 请求结果从 12:00-12:10pm 以来变化了多少。 参数 ago 描述需要对比请求的时期的时间数值型字符串。 arrangement ‘horizontal’ 或 ‘vertical’ spyable 设为假,不显示审查(inspect)按钮。 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected queries.ids 如果设为 selected 模式,具体被选的请求编号。 界面配置说明 trends 面板用来对比实时数据与过去某天的同期数据的量的变化。配置很简单,就是设置具 体某天前: ELKstack 中文指南 407各panel功能 效果如下: ELKstack 中文指南 408各panel功能 文本(text) 状态:稳定 文本面板用来显示静态文本内容,支持 markdown,简单的 html 和纯文本格式。 参数 mode ‘html’, ‘markdown’ 或者 ‘text’ content 面板内容,用 mode 参数指定的标记语言书写 ELKstack 中文指南 409各panel功能 sparklines 状态: 试验性 sparklines 面板显示微型时间图。目的不是显示一个确切的数值,而是以紧凑的方式显示时间 序列的形态。 参数 mode 用作 Y 轴的数值模式。除 count 以外,都需要定义 value_field 字段。可选值 有:count, mean, max, min, total. time_field X 轴字段。必须是 Elasticsearch 中的时间类型字段。 value_field 如果 mode 设置为 mean, max, min 或者 total,Y 轴字段。必须是数值类型字 段。 interval 如果有现成的时间过滤器,Sparkline 会自动计算间隔。如果没有,就用这个间 隔。默认是 5 分钟。 spyable 显示 inspect 图标。 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected queries.ids 如果设为 selected 模式,具体被选的请求编号。 界面配置说明 sparklines 面板其实就是 histogram 面板的缩略图模式。在配置上,只能选择 Chart value 模 式,填写 Time Field 或者 Value Field 字段。上文描述中的 interval 在配置页面上是看不到 的。 ELKstack 中文指南 410各panel功能 我们可以对比一下对同一个 topN 请求绘制的 sparklines 和 histogram 面板的效果: sparklines histogram 注:topN 请求的配置和说明,见 query 面板 ELKstack 中文指南 411各panel功能 hits 状态: 稳定 hits 面板显示仪表板上每个请求的 hits 数,具体的显示格式可以通过 "chart" 属性配置指定。 参数 arrangement 在条带(bar)或者饼图(pie)模式,图例的摆放方向。可以设置:水平(horizontal)或者垂直 (vertical)。 chart 可以设置:none, bar 或者 pie counter_pos 图例相对于图的位置,可以设置:上(above),下(below) donut 在饼图(pie)模式,在饼中画个圈,变成甜甜圈样式。 tilt 在饼图(pie)模式,倾斜饼变成椭圆形。 lables 在饼图(pie)模式,在饼图分片上绘制标签。 spyable 设为假,不显示审查(inspect)图标。 请求(queries) 请求对象 这个对象描述本面板使用的请求。 queries.mode 在可用请求中应该用哪些?可设选项有: all, pinned, unpinned, selected ELKstack 中文指南 412各panel功能 queries.ids 如果设为 selected 模式,具体被选的请求编号。 ELKstack 中文指南 413各panel功能 goal 状态: 稳定 goal 面板在一个饼图上显示到达指定目标的进度。 参数 donut 在饼图(pie)模式,在饼中画个圈,变成甜甜圈样式。 tilt 在饼图(pie)模式,倾斜饼变成椭圆形。 legend 图例的位置,上、下或者无。 lables 在饼图(pie)模式,在饼图分片上绘制标签。 spyable 设为假,不显示审查(inspect)图标。 请求(queries) 请求对象 query.goal goal 模式的指定目标 ELKstack 中文指南 414各panel功能 仪表板纲要 Kibana 仪表板可以很容易的在浏览器中创建出来,而且绝大多数情况下,浏览器已经足够支 持你创建一个很有用很丰富的节目了。不过,当你真的需要一点小修改的时候,Kibana 也可 以让你直接编辑仪表板的纲要。 注意:本节内容只针对高级用户。JSON 语法非常严格,多一个逗号,少一个大括号,都会 导致你的仪表板无法加载。 我们会用上面这个仪表板作为示例。你可以导出任意的仪表板纲要,点击右上角的保存按 钮,指向高级(Advanced)菜单,然后点击导出纲要(Export Schema)。示例使用的纲要文件可 以在这里下载: schema.json 因为仪表板是由特别长的 JSON 文档组成的,我们只能分成一段段的内容,分别介绍每段的 作用和目的。 和所有的 JSON 文档一样,都是以一个大括号开始的。 { 服务(services) "services": { 服务(Services)是被多个面板使用的持久化对象。目前仪表板对象附加有 2 种服务对象,不指 明的话,就会自动填充成请求(query)和过滤(filter)服务了。 ELKstack 中文指南 415各panel功能 Query Filter query "query": { "list": { "0": { "query": "play_name:\"Romeo and Juliet\"", "alias": "", "color": "#7EB26D", "id": 0, "pin": false, "type": "lucene", "enable": true } }, "ids": [ 0 ] }, 请求服务主要是由仪表板顶部的请求栏控制的。有两个属性: List: 一个以数字为键的对象。每个值描述一个请求对象。请求对象的键命名一目了然, 就是描述请求输入框的外观和行为的。 Ids: 一个由 ID 组成的数组。每个 ID 都对应前面 list 对象的键。 ids 数组用来保证显示时 list 的排序问题。 filter "filter": { "list": { "0": { "type": "querystring", "query": "speaker:ROMEO", "mandate": "must", "active": true, "alias": "", "id": 0 } }, "ids": [ 0 ] } }, ELKstack 中文指南 416各panel功能 过滤的行为和请求很像,不过过滤不能在面板级别选择,而是对全仪表板生效。过滤对象和 请求对象一样有 list 和 ids 两个属性,各属性的行为和请求对象也一样。 垂幕(pulldown) "pulldowns": [ 垂幕是一种特殊的面板。或者说,是一个特殊的可以用来放面板的地方。在垂幕里的面板就 跟在行里的一样,区别就是不能设置 span 宽度。垂幕里的面板永远都是全屏宽度。此外,垂 幕里的面板也不可以吧被使用者移动或编辑。所以垂幕特别适合放置输入框。垂幕的属性是 一个由面板对象构成的数组。关于特定的面板,请阅读 Kibana Panels { "type": "query", "collapse": false, "notice": false, "enable": true, "query": "*", "pinned": true, "history": [ "play_name:\"Romeo and Juliet\"", "playname:\"Romeo and Juliet\"", "romeo" ], "remember": 10 }, { "type": "filtering", "collapse": false, "notice": true, "enable": true } ], 垂幕面板有 2 个普通行面板没有的选项: Collapse: 设置为真假值,代表着面板被折叠还是展开。 Notice: 面板设置这个值,控制在垂幕的标签主题上出现一个小星星。用来通知使用者, 这个面板里发生变动了。 导航(nav) ELKstack 中文指南 417各panel功能 nav 属性里也有一个面板列表,只是这些面板是被用来填充在页首导航栏里德。目前唯一支 持导航的面板是时间选择器(timepicker) "nav": [ { "type": "timepicker", "collapse": false, "notice": false, "enable": true, "status": "Stable", "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ], "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "timefield": "@timestamp" } ], loader loader 属性描述了仪表板顶部的保存和加载按钮的行为。 ELKstack 中文指南 418各panel功能 "loader": { "save_gist": false, "save_elasticsearch": true, "save_local": true, "save_default": true, "save_temp": true, "save_temp_ttl_enable": true, "save_temp_ttl": "30d", "load_gist": false, "load_elasticsearch": true, "load_elasticsearch_size": 20, "load_local": false, "hide": false }, 行数组 rows 就是通常放置面板的地方。也是唯一可以通过浏览器页面添加的位置。 "rows": [ { "title": "Charts", "height": "150px", "editable": true, "collapse": false, "collapsable": true, 行对象包含了一个面板列表,以及一些行的具体参数,如下所示: title: 行的标题 height: 行的高度,单位是像素,记作 px editable: 真假值代表面板是否可被编辑 collapse: 真假值代表行是否被折叠 collapsable: 真价值代表使用者是否可以折叠行 面板数组 行的 panels 数组属性包括有一个以自己出现次序排序的面板对象的列表。各特定面板本身 的属性列表和说明,阅读 Kibana Panels "panels": [ { "error": false, "span": 8, "editable": true, "type": "terms", ELKstack 中文指南 419各panel功能 "loadingEditor": false, "field": "speech_number", "exclude": [], "missing": false, "other": false, "size": 10, "order": "count", "style": { "font-size": "10pt" }, "donut": false, "tilt": false, "labels": true, "arrangement": "horizontal", "chart": "bar", "counter_pos": "above", "spyable": true, "queries": { "mode": "all", "ids": [ 0 ] }, "tmode": "terms", "tstat": "total", "valuefield": "", "title": "Longest Speeches" }, { "error": false, "span": 4, "editable": true, "type": "goal", "loadingEditor": false, "donut": true, "tilt": false, "legend": "none", "labels": true, "spyable": true, "query": { "goal": 111397 }, "queries": { "mode": "all", "ids": [ 0 ] }, "title": "Percentage of Total" } ] } ], ELKstack 中文指南 420各panel功能 索引设置 索引属性包括了 Kibana 交互的 Elasticsearch 索引的信息。 "index": { "interval": "none", "default": "_all", "pattern": "[logstash-]YYYY.MM.DD", "warm_fields": false }, interval: none, hour, day, week, month。这个属性描述了索引所遵循的时间间隔模式。 default: 如果 interval 被设置为 none,或者后面的 failover 设置为 true 而且没有 索引能匹配上正则模式的话,搜索这里设置的索引。 pattern: 如果 interval 被设置成除了 none 以外的其他值,就需要解析这里设置的模 式,启用时间过滤规则,来确定请求哪些索引。 warm_fields: 是否需要解析映射表来确定字段列表。 其余 下面四个也是顶层的仪表板配置项 "failover": false, "editable": true, "style": "dark", "refresh": false } failover: 真假值,确定在没有匹配上索引模板的时候是否使用 index.default 。 editable: 真假值,确定是否在仪表板上显示配置按钮。 style: "亮色(light)" 或者 "暗色(dark)" refresh: 可以设置为 "false" 或者其他 elasticsearch 支持的时间表达式(比如 10s, 1m, 1h),用来描述多久触发一次面板的数据更新。 导入纲要 默认是不能导入纲要的。不过在仪表板配置屏的控制(Controls)标签里可以开启这个功能,启 用 "Local file" 选项即可。然后通过仪表板右上角加载图标的高级设置,选择导入文件,就可 以导入纲要了。 ELKstack 中文指南 421各panel功能 ELKstack 中文指南 422各panel功能 模板和脚本 Kibana 支持通过模板或者更高级的脚本来动态的创建仪表板。你先创建一个基础的仪表板, 然后通过参数来改变它,比如通过 URL 插入一个新的请求或者过滤规则。 模板和脚本都必须存储在磁盘上,目前不支持存储在 Elasticsearch 里。同时它们也必须是通 过编辑或创建纲要生成的。所以我们强烈建议阅读 The Kibana Schema Explained 仪表板目录 仪表板存储在 Kibana 安装目录里的 app/dashboards 子目录里。你会注意到这里面有两种文 件: .json 文件和 .js 文件。 模板化仪表板(.json) .json 文件就是模板化的仪表板。模板示例可以在 logstash.json 仪表板的请求和过滤对象 里找到。模板使用 handlebars 语法,可以让你在 json 里插入 javascript 语句。URL 参数存在 ARGS 对象中。下面是 logstash.json (on github) 里请求和过滤服务的代码片段: "0": { "query": "{{ARGS.query || '*'}}", "alias": "", "color": "#7EB26D", "id": 0, "pin": false } [...] "0": { "type": "time", "field": "@timestamp", "from": "now-{{ARGS.from || '24h'}}", "to": "now", "mandate": "must", "active": true, "alias": "", "id": 0 } 这允许我们在 URL 里设置两个参数, query 和 from 。如果没设置,默认值就是 || 后面 的内容。比如说,下面的 URL 就会搜索过去 7 天内 status:200 的数据: ELKstack 中文指南 423各panel功能 注意:千万注意 url #/dashboard/file/logstash.json 里的 file 字样 http://yourserver/index.html#/dashboard/file/logstash.json?query=status:200&from=7d ELKstack 中文指南 424各panel功能 脚本化仪表板(.js) 脚本化仪表板比模板化仪表板更加强大。当然,功能强大随之而来的就是构建起来也更复杂。 脚本化仪表板的目的就是构建并返回一个描述了完整的仪表板纲要的 javascript 对 象。 app/dashboards/logstash.js (on github) 就是一个有着详细注释的脚本化仪表板示例。这 个文件的最终结果和 logstash.json 一致,但提供了更强大的功能,比如我们可以以逗号分 割多个请求: 注意:千万注意 URL #/dashboard/script/logstash.js 里的 script 字样。这让 kibana 解析 对应的文件为 javascript 脚本。 http://yourserver/index.html#/dashboard/script/logstash.js?query=status:403,status:404&from=7d 这会创建 2 个请求对象, status:403 和 status:404 并分别绘图。事实上这个仪表板还能接 收另一个参数 split ,用于指定用什么字符串切分。 http://yourserver/index.html#/dashboard/script/logstash.js?query=status:403!status:404&from=7d&split=! 我们可以看到 logstash.js (on github) 里是这么做的: // In this dashboard we let users pass queries as comma separated list to the query parameter. // Or they can specify a split character using the split aparameter // If query is defined, split it into a list of query objects // NOTE: ids must be integers, hence the parseInt()s if(!_.isUndefined(ARGS.query)) { queries = _.object(_.map(ARGS.query.split(ARGS.split||','), function(v,k) { return [k,{ query: v, id: parseInt(k,10), alias: v }]; })); } else { // No queries passed? Initialize a single query to match everything queries = { 0: { query: '*', id: 0, } }; } ELKstack 中文指南 425各panel功能 该仪表板可用参数比上面讲述的还要多,全部参数都在 logstash.js (on github) 文件里开始 的注释中有讲解。 ELKstack 中文指南 426各panel功能 nginx 代理和简单权限验证 Kibana3 作为一个纯静态文件式的单页应用,可以运行在任意主机上,却要求所有用户的浏 览器,都可以直连 ELasticsearch 集群。这对网络和数据安全都是极为不利的。所以,一般在 生产环境的 ELK Stack,都是采取 HTTP 代理层的方式来做一层防护。最简单的办法,就是 使用 Nginx 代理配置。 ES 官方也提供了一个推荐配 置:https://github.com/elastic/kibana/blob/3.0/sample/nginx.conf server { listen *:80; server_name kibana.myhost.org; access_log /var/log/nginx/kibana.myhost.org.access.log; location / { root /usr/share/kibana3; index index.html index.htm; } location ~ ^/_aliases$ { proxy_pass http://127.0.0.1:9200; proxy_read_timeout 90; } location ~ ^/.*/_aliases$ { proxy_pass http://127.0.0.1:9200; proxy_read_timeout 90; } location ~ ^/_nodes$ { proxy_pass http://127.0.0.1:9200; proxy_read_timeout 90; } location ~ ^/.*/_search$ { proxy_pass http://127.0.0.1:9200; proxy_read_timeout 90; } location ~ ^/.*/_mapping { proxy_pass http://127.0.0.1:9200; proxy_read_timeout 90; } # Password protected end points location ~ ^/kibana-int/dashboard/.*$ { proxy_pass http://127.0.0.1:9200; proxy_read_timeout 90; limit_except GET { proxy_pass http://127.0.0.1:9200; ELKstack 中文指南 427各panel功能 auth_basic "Restricted"; auth_basic_user_file /etc/nginx/conf.d/kibana.myhost.org.htpasswd; } } location ~ ^/kibana-int/temp.*$ { proxy_pass http://127.0.0.1:9200; proxy_read_timeout 90; limit_except GET { proxy_pass http://127.0.0.1:9200; auth_basic "Restricted"; auth_basic_user_file /etc/nginx/conf.d/kibana.myhost.org.htpasswd; } } } 由此也可以看到,Kibana3 实际往 ES 发起的请求,都有哪些。 然后,在同时运行了 Nginx 和 Elasticsearch 服务(建议为 client 角色)的这台服务器上,配置 elasticsearch.yml 为: network.bind_host: 127.0.0.1 network.publish_host: 127.0.0.1 network.host: 127.0.0.1 其他 Elasticsearch 节点统一关闭 HTTP 服务,配置 elasticsearch.yml : http.enabled: false 这样,所有人都只能从 Nginx 服务来查询 ES 服务了。从 Nginx 日志中,我们还可以审核查 询请求的语法性能和合理性。 注意:改配置前提是 Logstash 采用 node/transport 协议写入数据,如果使用 http 协议的,请 采用 iptables 防护,或新增 /_bulk 等写入接口的代理配置。 ELKstack 中文指南 428各panel功能 配置Kibana的CAS验证 本节作者:childe 我们公司用的是 CAS 单点登陆, 用如下工具将kibana集成到此单点登陆系统 准备工具 nginx: 仅仅是为了记录日志, 不用也行 nodejs: 为了跑 kibana-authentication-proxy kibana: https://github.com/elasticsearch/kibana kibana-authentication-proxy: https://github.com/fangli/kibana-authentication-proxy 配置 1. nginx 配置 8080 端口, 反向代理到 es 的 9200 2. git clone kibana-authentication-proxy 3. git clone kibana 4. 将 kibana 软链接到 kibana-authentication-proxy 目录下 5. 配置 kibana-authentication-proxy/config.js 可能有如下参数需要调整: es_host #这里是nginx地址 es_port #nginx的8080 listen_port #node的监听端口, 80 listen_host #node的绑定IP, 可以0.0.0.0 cas_server_url #CAS地址 6. 安装 kibana-authentication-proxy 的依赖, npm install express , 等 7. 运行 node kibana-authentication-proxy/app.js 原理 app.js 里面 app.get('/config.js', kibana3configjs) ; 返回了一个新的 config.js, 不是用 的 kibana/config.js, 在这个配置里面,调用 ES 数据的 URL 前面加了一个 __es 的前缀 在 app.js 入口这里, 有两个关键的中间层(我也不知道叫什么)被注册: 一个是 ELKstack 中文指南 429各panel功能 configureCas , 一个是 configureESProxy 一个请求来的时候, 会到 configureCas 判断是不是已经登陆到 CAS, 没有的话就转到 cas 登陆页面 configureESProxy 在 lib/es-proxy.js 里, 会把 __es 打头的请求(其实就是请求 es 数据的 请求)转发到真正的 es 接口那里(我们这里是 nginx) 请求路径 node(80) <=> nginx(8080) <=> es(9200) kibana-authentication-proxy 本身没有记录日志的代码, 而且转发 es 请求用的流式的(看起来), 并不能记录详细的 request body. 所以我们就用 nginx 又代理一层做日志了.. ELKstack 中文指南 430各panel功能 Auth WebUI in Mojolicious 社区已经有用 nodejs 或者 rubyonrails 写的 kibana-auth 方案了。不过我这两种语言都不太擅 长,只会写一点点 Perl5 代码,所以我选择用 Mojolicious web 开发框架来实现我自己的 kibana 认证鉴权。 整套方案的代码以 kbnauth 子目录形式存在于我的 kibana 仓库中,如果你不想用这套认证 方案,照旧使用 src 子目录即可。事实上, kbnauth/public/ 目录下的静态文件我都是通过 软连接方式指到 src/ 下的。 特性 全局透明代理 和 nodejs 实现的那套方案不同,我这里并没有使用 __es/ 这样附加的路径。所有发往 Elasticsearch 的请求都是通过这个方案来控制。除了使用 config.js.ep 模版来定制 elasticsearch 地址设置以外,方案还会伪造 /_nodes 请求的响应体,伪造的响应体中 永远只有运行着认证方案的这台服务器的 IP 地址。 这么做的原因是我的 kibana 升级了 elasticjs 版本,新版本默认会通过这个 API 获取 nodes 列表,然后浏览器直接轮询多个 IP 获取响应。 注意:Mojolicious 有一个环境变量叫 max_message_size ,默认是 10MB,即只允许代 理响应大小在 10MB 以内的数据。我在 script/kbnauth 启动脚本中把它修改成了 0,即 不限制。如果你有这方面的需求,可以修改成任意你想要的阈值。 使用 kibana-auth elasticsearch 索引做鉴权 因为所有的请求都会发往代理服务器(即运行着认证鉴权方案的服务器),每个用户都可以 有自己的仪表板空间(没错,这招是从 kibana-proxy 项目学来的,每个用户使用单独的 kibana-int-$username 索引保存自己的仪表板设置)。而本方案还提供另一个高级功能: 还可以通过另一个新的索引 kibana-auth 来指定每个用户所能访问的 Elasticsearch 集群 地址和索引列表。 给用户 "sri" 添加鉴权信息的命令如下: $ curl -XPOST http://127.0.0.1:9200/kibana-auth/indices/sri -d '{ "prefix":["logstash-sri","logstash-ops"], "server":"192.168.0.2:9200" }' ELKstack 中文指南 431各panel功能 这就意味着用户 "sri" 能访问的,是存在 "192.168.0.2:9200" 上的 "logstash-sri- YYYY.MM.dd" 或者 "logstash-ops-YYYY.MM.dd" 索引。 小贴士:所以你在 kbn_auth.conf 里配置的 eshost/esport,其实并不意味着 kibana 数 据的来源,而是认证方案用来请求 kibana-auth 信息的地址! 使用 Authen::Simple 框架做认证 Authen::Simple 是一个很棒的认证框架,支持非常多的认证方法。比如:LDAP, DBI, SSH, Kerberos, PAM, SMB, NIS, PAM, ActiveDirectory 等。 默认使用的是 Passwd 方法。也就是用 htpasswd 命令行在本地生成一个 .htpasswd 文 件存用户名密码。 如果要使用其他方法,比如用 LDAP 认证,只需要配置 kbn_auth.conf 文件就行了: authen => { LDAP => { host => 'ad.company.com', binddn => 'proxyuser@company.com', bindpw => 'secret', basedn => 'cn=users,dc=company,dc=com', filter => '(&(objectClass=organizationalPerson)(objectClass=user)(sAMAccountName=%s))' }, } 可以同时使用多种认证方式,但请确保每种都是有效可用的。某一个认证服务器连接超 时也会影响到其他认证方式超时。 安装 该方案代码只有两个依赖:Mojolicious 框架和 Authen::Simple 框架。我们可以通过 cpanm 部署: curl http://xrl.us/cpanm -o /usr/local/bin/cpanm chmod +x /usr/local/bin/cpanm cpanm Mojolicious Authen::Simple::Passwd 如果你需要使用其他认证方法,每个方法都需要另外单独安装。比如使用 LDAP 部署,就再 运行一行: cpanm Authen::Simple::LDAP 就可以了。 小贴士:如果你是在一个新 RHEL 系统上初次运行代码,你可能会发现有报错说找不到 Digest::SHA 模块。这个模块其实是 Perl 核心模块,但是 RedHat 公司把所有的 Perl 核心模 块单独打包成了 perl-core.rpm ,所以你得先运行一下 yum install -y perl-core 才行。我 ELKstack 中文指南 432各panel功能 讨厌 RedHat! 运行 cd kbnauth # 开发环境监听 3000 端口,使用单进程的 morbo 服务器调试 morbo script/kbnauth # 生产环境监听 80 端口,使用高性能的 hypnotoad 服务器, 具体端口在 kbn_auth.conf 中定义 hypnotoad script/kbnauth 现在,打开浏览器,就可以通过默认的用户名/密码:"sri/secr3t" 登录进去了。(sri 是 Mojolicious 框架的作者,感谢他为 Perl5 社区提供这么高效的 web 开发框架) 注意:这时候你虽然认证通过进去了 kibana 页面,但是还没有赋权。按照上面提到的 kibana-auth 命令操作,才算全部完成。 ELKstack 中文指南 433各panel功能 源码剖析与二次开发 Kibana 3 作为 ELK Stack 风靡世界的最大推动力,其与优美的界面配套的简洁的代码同样功 不可没。事实上,graphite 社区就通过移植 kibana 3 代码框架的方式,启动了 grafana 项 目。至今你还能在 grafana 源码找到二十多处 "kbn" 字样。 巧合的是,在 Kibana 重构 v4 版的同时,grafana 的 v2 版也到了 Alpha 阶段,从目前的预览 效果看,主体 dashboard 沿用了 Kibana 3 的风格,不过添加了额外的菜单栏,供用户权限设 置等使用 —— 这意味着 grafana 2 跟 kibana 4 一样需要一个单独的 server 端。 笔者并非专业的前端工程师,对 angularjs 也处于一本入门指南都没看过的水准。所以本节内 容,只会抽取一些个人经验中会有涉及到的地方提出一些"私货"。欢迎方家指正。 ELKstack 中文指南 434k3源码解析 源码目录结构 下面是 kibana 源码的全部文件的 tree 图: . ├── app │ ├── app.js │ ├── components │ │ ├── extend-jquery.js │ │ ├── kbn.js │ │ ├── lodash.extended.js │ │ ├── require.config.js │ │ └── settings.js │ ├── controllers │ │ ├── all.js │ │ ├── dash.js │ │ ├── dashLoader.js │ │ ├── pulldown.js │ │ └── row.js │ ├── dashboards │ │ ├── blank.json │ │ ├── default.json │ │ ├── guided.json │ │ ├── logstash.js │ │ ├── logstash.json │ │ ├── noted.json │ │ ├── panel.js │ │ └── test.json │ ├── directives │ │ ├── addPanel.js │ │ ├── all.js │ │ ├── arrayJoin.js │ │ ├── configModal.js │ │ ├── confirmClick.js │ │ ├── dashUpload.js │ │ ├── esVersion.js │ │ ├── kibanaPanel.js │ │ ├── kibanaSimplePanel.js │ │ ├── ngBlur.js │ │ ├── ngModelOnBlur.js │ │ ├── resizable.js │ │ └── tip.js │ ├── factories │ │ └── store.js │ ├── filters │ │ └── all.js │ ├── panels │ │ ├── bettermap │ │ │ ├── editor.html ELKstack 中文指南 435k3源码解析 │ │ │ ├── leaflet │ │ │ │ ├── images │ │ │ │ │ ├── layers-2x.png │ │ │ │ │ ├── layers.png │ │ │ │ │ ├── marker-icon-2x.png │ │ │ │ │ ├── marker-icon.png │ │ │ │ │ └── marker-shadow.png │ │ │ │ ├── leaflet-src.js │ │ │ │ ├── leaflet.css │ │ │ │ ├── leaflet.ie.css │ │ │ │ ├── leaflet.js │ │ │ │ ├── plugins.css │ │ │ │ ├── plugins.js │ │ │ │ └── providers.js │ │ │ ├── module.css │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── column │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ ├── module.js │ │ │ └── panelgeneral.html │ │ ├── dashcontrol │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── derivequeries │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── fields │ │ │ ├── editor.html │ │ │ ├── micropanel.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── filtering │ │ │ ├── editor.html │ │ │ ├── meta.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── force │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── goal │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── histogram │ │ │ ├── editor.html │ │ │ ├── interval.js │ │ │ ├── module.html │ │ │ ├── module.js ELKstack 中文指南 436k3源码解析 │ │ │ ├── queriesEditor.html │ │ │ ├── styleEditor.html │ │ │ └── timeSeries.js │ │ ├── hits │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── map │ │ │ ├── editor.html │ │ │ ├── lib │ │ │ │ ├── jquery.jvectormap.min.js │ │ │ │ ├── map.cn.js │ │ │ │ ├── map.europe.js │ │ │ │ ├── map.usa.js │ │ │ │ └── map.world.js │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── multifieldhistogram │ │ │ ├── editor.html │ │ │ ├── interval.js │ │ │ ├── markersEditor.html │ │ │ ├── meta.html │ │ │ ├── module.html │ │ │ ├── module.js │ │ │ ├── styleEditor.html │ │ │ └── timeSeries.js │ │ ├── percentiles │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── query │ │ │ ├── editor.html │ │ │ ├── editors │ │ │ │ ├── lucene.html │ │ │ │ ├── regex.html │ │ │ │ └── topN.html │ │ │ ├── help │ │ │ │ ├── lucene.html │ │ │ │ ├── regex.html │ │ │ │ └── topN.html │ │ │ ├── helpModal.html │ │ │ ├── meta.html │ │ │ ├── module.html │ │ │ ├── module.js │ │ │ └── query.css │ │ ├── ranges │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── sparklines │ │ │ ├── editor.html │ │ │ ├── interval.js │ │ │ ├── module.html ELKstack 中文指南 437k3源码解析 │ │ │ ├── module.js │ │ │ └── timeSeries.js │ │ ├── statisticstrend │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── stats │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── table │ │ │ ├── editor.html │ │ │ ├── export.html │ │ │ ├── micropanel.html │ │ │ ├── modal.html │ │ │ ├── module.html │ │ │ ├── module.js │ │ │ └── pagination.html │ │ ├── terms │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── text │ │ │ ├── editor.html │ │ │ ├── lib │ │ │ │ └── showdown.js │ │ │ ├── module.html │ │ │ └── module.js │ │ ├── timepicker │ │ │ ├── custom.html │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ ├── module.js │ │ │ └── refreshctrl.html │ │ ├── trends │ │ │ ├── editor.html │ │ │ ├── module.html │ │ │ └── module.js │ │ └── valuehistogram │ │ ├── editor.html │ │ ├── module.html │ │ ├── module.js │ │ ├── queriesEditor.html │ │ └── styleEditor.html │ ├── partials │ │ ├── connectionFailed.html │ │ ├── dashLoader.html │ │ ├── dashLoaderShare.html │ │ ├── dashboard.html │ │ ├── dasheditor.html │ │ ├── inspector.html │ │ ├── load.html │ │ ├── modal.html ELKstack 中文指南 438k3源码解析 │ │ ├── paneladd.html │ │ ├── paneleditor.html │ │ ├── panelgeneral.html │ │ ├── querySelect.html │ │ └── roweditor.html │ └── services │ ├── alertSrv.js │ ├── all.js │ ├── dashboard.js │ ├── esVersion.js │ ├── fields.js │ ├── filterSrv.js │ ├── kbnIndex.js │ ├── monitor.js │ ├── panelMove.js │ ├── querySrv.js │ └── timer.js ├── config.js ├── css │ ├── angular-multi-select.css │ ├── animate.min.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.dark.min.css │ ├── bootstrap.light.min.css │ ├── font-awesome.min.css │ ├── jquery-ui.css │ ├── jquery.multiselect.css │ ├── normalize.min.css │ └── timepicker.css ├── favicon.ico ├── font │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── img │ ├── annotation-icon.png │ ├── cubes.png │ ├── glyphicons-halflings-white.png │ ├── glyphicons-halflings.png │ ├── kibana.png │ ├── light.png │ ├── load.gif │ ├── load_big.gif │ ├── small.png │ └── ui-icons_222222_256x240.png ├── index.html └── vendor ├── LICENSE.json ├── angular │ ├── angular-animate.js │ ├── angular-cookies.js ELKstack 中文指南 439k3源码解析 │ ├── angular-dragdrop.js │ ├── angular-loader.js │ ├── angular-resource.js │ ├── angular-route.js │ ├── angular-sanitize.js │ ├── angular-scenario.js │ ├── angular-strap.js │ ├── angular.js │ ├── bindonce.js │ ├── datepicker.js │ └── timepicker.js ├── blob.js ├── bootstrap │ ├── bootstrap.js │ └── less │ ├── accordion.less │ ├── alerts.less │ ├── bak │ │ ├── bootswatch.dark.less │ │ └── variables.dark.less │ ├── bootstrap.dark.less │ ├── bootstrap.less │ ├── bootstrap.light.less │ ├── bootswatch.dark.less │ ├── bootswatch.light.less │ ├── breadcrumbs.less │ ├── button-groups.less │ ├── buttons.less │ ├── carousel.less │ ├── close.less │ ├── code.less │ ├── component-animations.less │ ├── dropdowns.less │ ├── forms.less │ ├── grid.less │ ├── hero-unit.less │ ├── labels-badges.less │ ├── layouts.less │ ├── media.less │ ├── mixins.less │ ├── modals.less │ ├── navbar.less │ ├── navs.less │ ├── overrides.less │ ├── pager.less │ ├── pagination.less │ ├── popovers.less │ ├── progress-bars.less │ ├── reset.less │ ├── responsive-1200px-min.less │ ├── responsive-767px-max.less │ ├── responsive-768px-979px.less │ ├── responsive-navbar.less ELKstack 中文指南 440k3源码解析 │ ├── responsive-utilities.less │ ├── responsive.less │ ├── scaffolding.less │ ├── sprites.less │ ├── tables.less │ ├── tests │ │ ├── buttons.html │ │ ├── css-tests.css │ │ ├── css-tests.html │ │ ├── forms-responsive.html │ │ ├── forms.html │ │ ├── navbar-fixed-top.html │ │ ├── navbar-static-top.html │ │ └── navbar.html │ ├── thumbnails.less │ ├── tooltip.less │ ├── type.less │ ├── utilities.less │ ├── variables.dark.less │ ├── variables.less │ ├── variables.light.less │ └── wells.less ├── chromath.js ├── elasticjs │ ├── elastic-angular-client.js │ └── elastic.js ├── elasticsearch.angular.js ├── filesaver.js ├── jquery │ ├── jquery-1.8.0.js │ ├── jquery-ui-1.10.3.js │ ├── jquery.flot.byte.js │ ├── jquery.flot.events.js │ ├── jquery.flot.js │ ├── jquery.flot.pie.js │ ├── jquery.flot.selection.js │ ├── jquery.flot.stack.js │ ├── jquery.flot.stackpercent.js │ ├── jquery.flot.threshold.js │ ├── jquery.flot.time.js │ ├── jquery.multiselect.filter.js │ └── jquery.multiselect.js ├── jsonpath.js ├── lodash.js ├── modernizr-2.6.1.js ├── moment.js ├── numeral.js ├── require │ ├── css-build.js │ ├── css.js │ ├── require.js │ ├── text.js │ └── tmpl.js ELKstack 中文指南 441k3源码解析 ├── simple_statistics.js ├── timezone.js └── underscore.string.js 一目了然,我们可以归纳出下面几类主要文件: 入口:index.html 模块库:vendor/ 程序入口:app/app.js 组件配置:app/components/ 仪表板控制:app/controllers/ 挂件页面:app/partials/ 服务:app/services/ 指令:app/directives/ 图表:app/panels/ ELKstack 中文指南 442k3源码解析 入口和模块依赖 这一部分是网页项目的基础。从 index.html 里就可以学到 angularjs 最基础的常用模板语法 了。出现的指令有: ng-repeat , ng-controller , ng-include , ng-view , ng-slow , ng- click , ng-href ,以及变量绑定的语法: {{ dashboard.current.** }} 。 index.html 中,需要注意 js 的加载次序,先 require.js ,然后再 require.config.js ,最后 app 。整个 kibana 项目都是通过 requrie 方式加载的。而具体的模块,和模块的依赖关系, 则定义在 require.config.js 里。这些全部加载完成后,才是启动 app 模块,也就是项目本 身的代码。 require.config.js 中,主要分成两部分配置,一个是 paths ,一个是 shim 。paths 用来指定 依赖模块的导出名称和模块 js 文件的具体路径。而 shim 用来指定依赖模块之间的依赖关 系。比方说:绘制图表的 js,kibana3 里用的是 jquery.flot 库。这个就首先依赖于 jquery 库。 (通俗的说,就是原先普通的 HTML 写法里,要先加载 jquery.js 再加载 jquery.flot.js) 在整个 paths 中,需要单独提一下的是 elasticjs:'../vendor/elasticjs/elastic-angular- client' 。这是串联 elastic.js 和 angular.js 的文件。这里面实际是定义了一个 angular.module 的 factory,名叫 ejsResource 。后续我们在 kibana 3 里用到的跟 Elasticsearch 交互的所有 方法,都在这个 ejsResource 里了。 factory 是 angular 的一个单例对象,创建之后会持续到你关闭浏览器。Kibana 3 就是通过这 种方式来控制你所有的图表是从同一个 Elasticsearch 获取的数据 app.js 中,定义了整个应用的 routes,加载了 controller, directives 和 filters 里的全部内容。 就是在这里,加载了主页面 app/partials/dashboard.html 。当然,这个页面其实没啥看头, 因为里面就是提供 pulldown 和 row 的 div,然后绑定到对应的 controller 上。 ELKstack 中文指南 443k3源码解析 controller 和 service controller 里没太多可讲的。kibana 3 里,pulldown 其实跟 row 差别不大,看这简单的几行代 码里,最关键的就是几个注入: define(['angular','app','lodash'], function (angular, app, _) { 'use strict'; angular.module('kibana.controllers').controller('RowCtrl', function($scope, $rootScope, $timeout,ejsResource, querySrv) { var _d = { title: "Row", height: "150px", collapse: false, collapsable: true, editable: true, panels: [], notice: false }; _.defaults($scope.row,_d); $scope.init = function() { $scope.querySrv = querySrv; $scope.reset_panel(); }; $scope.init(); } ); }); 这里面,注入了 $scope , ejsResource 和 querySrv 。 $scope 是控制器作用域内的模型数 据对象,这是 angular 提供的一个特殊变量。 ejsResource 是一个 factory ,前面已经讲 过。 querySrv 是一个 service,下面说一下。 service 跟 factory 的概念非常类似,一般来说,可能 factory 偏向用来共享一个类,而 service 用来共享一组函数功能。 kibana 3 里,比较有用和常用的 services 包括: dashboard dashboard.js 里提供了关于 Kibana 3 仪表板的读写操作。其中主要的几个是提供了三种读取 仪表板布局纲要的方式,也就是读取文件,读取存在 .kibana-int 索引里的数据,读取 js 脚 本。下面是读取 js 脚本的相关函数: ELKstack 中文指南 444k3源码解析 this.script_load = function(file) { return $http({ url: "app/dashboards/"+file.replace(/\.(?!js)/,"/"), method: "GET", transformResponse: function(response) { /*jshint -W054 */ var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response); return _f($routeParams,kbn,_,moment); } }).then(function(result) { if(!result) { return false; } self.dash_load(dash_defaults(result.data)); return true; },function() { alertSrv.set('Error', "Could not load scripts/"+file+". Please make sure it exists and returns a valid dashboard" , 'error'); return false; }); }; 可以看到,最关键的就是那个 new Function 。知道这步传了哪些函数进去,也就知道你的 js 脚本里都可以调用哪些内容了~ 最后调用的 dash_load 方法也需要提一下。这个方法的最后,有几行这样的代码: self.availablePanels = _.difference(config.panel_names, _.pluck(_.union(self.current.nav,self.current.pulldowns),'type')); self.availablePanels = _.difference(self.availablePanels,config.hidden_panels); 从最外层的 config.js 里读取了 panel_names 数组,然后取出了 nav 和 pulldown 用过的 panel,剩下就是我们能在 row 里添加的 panel 类型了。 querySrv querySrv.js 里定义了跟 query 框相关的函数和属性。主要有几个值得注意的。 一个是 color 列表; 一个是 queryTypes ,尤其是里么的 topN ,可以看到 topN 方式其实就是先请求了一次 termsFacet,然后把结果 map 成一组普通的 query。 一个是 ids 和 idsByMode 。之后图表的绑定具体 query 的时候,就是通过这个函数来 选择的。 ELKstack 中文指南 445k3源码解析 filterSrv filterSrv.js 跟 querySrv 相似。特殊的是两个函数。 一个是 toEjsObjs 。根据不同的 filter 类型调用不同的 ejs 方法。 一个是 timeRange 。因为在 histogram panel 上拖拽,会生成好多个 range 过滤器,都 是时间。这个方法会选择最后一个类型为 time 的 filter,作为实际要用的 filter。这样保证 请求 ES 的是最后一次拖拽选定的时间段。 fields fields.js 里最重要的作用就是通过 mapping 接口获取索引的字段列表,存在 fields.list 里。这个数组后来在每个 panel 的编辑页里,都以 bs-typeahead="fields.list" 的形式作为 文本输入时的自动补全提示。在 table panel 里,则是左侧栏的显示来源。 esVersion esVersion.js 里提供了对 ES 版本号的对比函数。之所以专门提供这么个 service,一来是因 为不同版本的 ES 接口有变化,比如我自己开发的 percentile panel 里,就用 esVersion 判断 了两次版本。因为 percentile 接口是 1.0 版之后才有,而从 1.3 版以后返回数据的结构又发生 了一次变动。二来 ES 的版本号格式比较复杂,又有点又有字母。 ELKstack 中文指南 446k3源码解析 panel 相关指令 添加 panel 前面在讲 app/services/dashboard.js 的时候,已经说到能添加的 panel 列表是怎么获取的。 那么panel 是怎么加上的呢? 同样是之前讲过的 app/partials/dashaboard.html 里,加载了 partials/roweditor.html 页 面。这里有一段:
Note: This row is full, new panels will wrap to a new line. You should add another row.
这个 add-panel 指令,是有 app/directives/addPanel.js 提供的。方法如下: $scope.$watch('panel.type', function() { var _type = $scope.panel.type; $scope.reset_panel(_type); if(!_.isUndefined($scope.panel.type)) { $scope.panel.loadingEditor = true; $scope.require(['panels/'+$scope.panel.type.replace(".","/") +'/module'], function () { var template = '
'; elem.html($compile(angular.element(template))($scope)); $scope.panel.loadingEditor = false; }); } }); 可以看到,其实就是 require 了对应的 panels/xxx/module.js ,然后动态生成一个 div,绑定 到对应的 controller 上。 展示 panel 还是在 app/partials/dashaboard.html 里,用到了另一个指令 kibana-panel : ELKstack 中文指南 447k3源码解析
当然,这里面还有 resizable 指令也是自己实现的,不过一般我们用不着关心这个的代码实 现。 下面看 app/directives/kibanaPanel.js 里的实现。 这个里面大多数逻辑跟 addPanel.js 是一样的,都是为了实现一个指令嘛。对于我们来说,关 注点在前面那一大段 HTML 字符串,也就是变量 panelHeader 。这个就是我们看到的实际效 果中,kibana 3 每个 panel 顶部那个小图标工具栏。仔细阅读一下,可以发现除了每个 panel 都一致的那些 span 以外,还有一段是: '' + ''+ '' 也就是说,每个 panel 可以在自己的 panelMeta.modals 数组里,定义不同的小图标,弹出不 同的对话浮层。我个人给 table panel 二次开发加入的 exportAsCsv 功能,图标就是在这里加 入的。 ELKstack 中文指南 448k3源码解析 panel 内部实现 终于说到最后了。大家进入到 app/panels/ 下,每个目录都是一种 panel。原因前一节已经 分析过了,因为 addPanel.js 里就是直接这样拼接的。入口都是固定的:module.js。 下面以 stats panel 为例。(因为我最开始就是抄的 stats 做的 percentile,只有表格没有图 形,最简单) 每个目录下都会有至少一下三个文件: module.js module.js 就是一个 controller。跟前面讲过的 controller 写法其实是一致的。在 $scope 对象 上,有几个属性是 panel 实现时一般都会有的: $scope.panelMeta : 这个前面说到过,其中的 modals 用来定义 panelHeader。 $scope.panel : 用来定义 panel 的属性。一般实现上,会有一个 default 值预定义好。你 会发现这个 $scope.panel 其实就是仪表板纲要里面说的每个 panel 的可设置值! 然后一般 $scope.init() 都是这样的: $scope.init = function () { $scope.ready = false; $scope.$on('refresh', function () { $scope.get_data(); }); $scope.get_data(); }; 也就是每次有刷新操作,就执行 get_data() 方法。这个方法就是获取 ES 数据,然后渲染效 果的入口。 $scope.get_data = function () { if(dashboard.indices.length === 0) { return; } $scope.panelMeta.loading = true; var request, results, boolQuery, queries; request = $scope.ejs.Request(); ELKstack 中文指南 449k3源码解析 $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); queries = querySrv.getQueryObjs($scope.panel.queries.ids); boolQuery = $scope.ejs.BoolQuery(); _.each(queries,function(q) { boolQuery = boolQuery.should(querySrv.toEjsObj(q)); }); request = request .facet($scope.ejs.StatisticalFacet('stats') .field($scope.panel.field) .facetFilter($scope.ejs.QueryFilter( $scope.ejs.FilteredQuery( boolQuery, filterSrv.getBoolFilter(filterSrv.ids()) )))).size(0); _.each(queries, function (q) { var alias = q.alias || q.query; var query = $scope.ejs.BoolQuery(); query.should(querySrv.toEjsObj(q)); request.facet($scope.ejs.StatisticalFacet('stats_'+alias) .field($scope.panel.field) .facetFilter($scope.ejs.QueryFilter( $scope.ejs.FilteredQuery( query, filterSrv.getBoolFilter(filterSrv.ids()) ) )) ); }); $scope.inspector = request.toJSON(); results = $scope.ejs.doSearch(dashboard.indices, request); results.then(function(results) { $scope.panelMeta.loading = false; var value = results.facets.stats[$scope.panel.mode]; var rows = queries.map(function (q) { var alias = q.alias || q.query; var obj = _.clone(q); obj.label = alias; obj.Label = alias.toLowerCase(); //sort field obj.value = results.facets['stats_'+alias]; obj.Value = results.facets['stats_'+alias]; //sort field return obj; }); $scope.data = { value: value, ELKstack 中文指南 450k3源码解析 rows: rows }; $scope.$emit('render'); }); }; stats panel 的这段函数几乎就跟基础示例一样了。 1. 生成 Request 对象。 2. 获取关联的 query 对象。 3. 获取当前页的 filter 对象。 4. 调用选定的 facets 方法,传入参数。 5. 如果有多个 query,逐一构建 facets。 6. request 完成。生成一个 JSON 内容供 inspector 查看。 7. 发送请求,等待异步回调。 8. 回调处理数据成绑定在模板上的 $scope.data 。 9. 渲染页面。 注:stats/module.js 后面还有一个 filter,terms/module.js 后面还有一个 directive,这些都是 为了实际页面效果加的功能,跟 kibana 本身的 filter,directive 本质上是一样的。就不单独讲 述了。 module.html module.html 就是 panel 的具体页面内容。没有太多可说的。大概框架是:
Term {{ panel.tmode == 'terms_stats' ? panel.tstat : 'Count' }} Action
{{term.label}} {{term.data[0][1]}}
主要就是绑定要 controller 和 init 函数。对于示例的 stats,里面的 data 就是 module.js 最 后生成的 $scope.data 。 editor.html ELKstack 中文指南 451k3源码解析 editor.html 是 panel 参数的编辑页面主要内容,参数编辑还有一些共同的标签页,是在 kibana 的 app/partials/ 里,就不讲了。 editor.html 里,主要就是提供对 $scope.panel 里那些参数的修改保存操作。当然实际上并不 是所有参数都暴露出来了。这也是 kibana 3 用户指南里,官方说采用仪表板纲要,比通过页 面修改更灵活细腻的原因。 editor.html 里需要注意的是,为了每次变更都能实时生效,所有的输入框都注册到了刷新事 件。所以一般是这样子: 这个 set_refresh 函数是在 module.js 里定义的: $scope.set_refresh = function (state) { $scope.refresh = state; }; 总结 kibana 3 源码的主体分析,就是这样了。怎么样,看完以后,大家有没有信心也做些二次开 发,甚至跟 grafana 一样,替换掉 esResource,换上一个你自己的后端数据源呢? ELKstack 中文指南 452k3源码解析 range facet 面板开发 查看响应时间在不同区间内占比的是非常常见的一个监控和 SLA 需求。Elasticsearch 对此有 直接的接口支持。考虑到 kibana3 内大多数还是用 facet 接口,这里也沿 用:http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets- range-facet.html。 range facet 本身的使用非常简单,就像官网示例那样,直接 curl 命令就可以完成调试: curl -XPOST http://localhost:9200/logstash-2014.08.18/_search?pretty=1 -d '{ "query" : { "match_all" : {} }, "facets" : { "range1" : { "range" : { "field" : "resp_ms", "ranges" : [ { "to" : 100 }, { "from" : 101, "to" : 500 }, { "from" : 500 } ] } } } }' 不过在 kibana 里,我们就不要再自己拼 JSON 发请求了。elastic.js 关于 range facet 的文档 见:http://docs.fullscale.co/elasticjs/ejs.RangeFacet.html 因为 range facet 本身比较简单,所以 RangeFacet 对象支持的方法也比较少。一个 addRange 方法添加 ranges 数组,一个 field 方法添加 field 名称即可。 代码实现难点 面板代码的主要层次和方法,在上节中已经讲过。在二次开发中,完全可以复制一个类似的 现有 panel ,然后开始编辑。比如本节预备开发一个以饼图展示区间统计的面板,即可复制 terms 面板代码: # cp -r src/app/panels/terms src/app/panels/ranges # sed -i 's/terms/ranges/g' src/app/panels/ranges/* ELKstack 中文指南 453用facet接口开发一个range panel terms 面板中,设计有 fmode 和 tmode ,分别控制 terms 和 term_stats 时的参数情况,而 ranges 面板中并不需要,所以去除这部分属性,在 module.js 中,有关 tmode 的内容更加 简单。 构建请求 if($scope.panel.tmode === 'ranges') { rangefacet = $scope.ejs.RangeFacet('ranges'); // AddRange _.each($scope.panel.values, function(v) { rangefacet.addRange(v.from, v.to); }); request = request .facet(rangefacet .field($scope.field) .facetFilter($scope.ejs.QueryFilter( $scope.ejs.FilteredQuery( boolQuery, filterSrv.getBoolFilter(filterSrv.ids()) )))).size(0); } 组织结果 function build_results() { var k = 0; scope.data = []; _.each(scope.results.facets.ranges.ranges, function(v) { var slice; if(scope.panel.tmode === 'ranges') { slice = { label : [v.from,v.to], data : [[k,v.count]], actions: true}; } scope.data.push(slice); k = k + 1; }); scope.data.push({label:'Missing field', data:[[k,scope.results.facets.ranges.missing]],meta:"missing",color:'#aaa',opacity:0}); if(scope.panel.tmode === 'ranges') { scope.data.push({label:'Other values', data:[[k+1,scope.results.facets.ranges.other]],meta:"other",color:'#444'}); } } 区间范围配置编辑 ELKstack 中文指南 454用facet接口开发一个range panel 这个新 panel 的实现,更复杂的地方在配置编辑上如何让 range 范围值支持自定义添加和填 写。对此,设计有一个 $scope.panel.values 数组,对应每个区间: values 用于计算 facet 的数值范围数组。数组每个元素包括: from range 范围的起始点 to range 范围的结束点 editor.html 里 Field 栏由普通的文本输入框改成表格输入:
From To Delete
这里使用了一个 add_new_value 函数,需要在 module.js 中定义: ELKstack 中文指南 455用facet接口开发一个range panel $scope.defaultValue = { 'from': 0, 'to' : 100 }; $scope.add_new_value = function(panel) { panel.values.push(angular.copy($scope.defaultValue)); }; 面板单击生成的 filtering 条件 另一个需要注意的地方是饼图出来以后,单击饼图区域,自动生成的 filterSrv 内容。一般 的面板这里都是 terms 类型的 filterSrv ,传递的是面板的 label 值。而我们这里 label 值 显然不是 ES 有效的 terms 语法,还好 filterSrv 有 range 类型(histogram 面板的 time 类型的 filterSrv 是在 daterange 基础上实现的),所以稍微修改就可以了。 $scope.build_search = function(range,negate) { if(_.isUndefined(range.meta)) { filterSrv.set({type:'range',field:$scope.field,from:range.label[0],to:range.label[1], mandate:(negate ? 'mustNot':'must')}); } else if(range.meta === 'missing') { filterSrv.set({type:'exists',field:$scope.field, mandate:(negate ? 'must':'mustNot')}); } else { return; } }; 面板效果 最终效果如下: ELKstack 中文指南 456用facet接口开发一个range panel 属性编辑界面效果如下: ranges panel 完整代码,见 https://github.com/chenryn/kibana/src/app/panels/ranges。 ELKstack 中文指南 457用facet接口开发一个range panel percentile agg 面板开发 Kibana3.1 中有一个面板是 stats 类型,返回对应请求的某指定数值字段的数学统计值,包括 最大值、最小值、平均值、方差和标准差。这个 stats 图表是利用 Elasticsearch 的 facets 功 能来实现的。而在 Elasticsearch 1.0 版本以后,新出现了一个更细致的功能叫 aggregation, 按照官方文档所说,会慢慢的彻底替代掉 facets。具体到 1.1 版本的时候, aggregation 里多 了一项 percentile,可以具体返回某指定数值字段的区间分布情况。这对日志分析可是大有帮 助。对这项功能,Elasticsearch 官方也很得意的专门在博客上写了一篇报道:Averages can be misleading: try a percentile。 percentile agg 请求示例如下: # curl -XPOST http://127.0.0.1:9200/logstash-2014.07.11/_search -d '{ "aggs" : { "request_time_percentiles" : { "percentiles" : { "field" : "request_time", "percents" : [50,75,90,99] } } } }' 本节就讲解如何基于 Elasticsearch 的 percentile Aggregation 接口实现统计聚合展示面板。 其他 Aggregation 也可以类比。 和上一节实现 facet 接口面板相比,页面布局和主体逻辑基本一致。其难点在以下几方面: Kibana3 使用了社区流行的 elastic.js 第三方库作为基础依赖库。而 kibana/src/vendor/elasticjs/elastic.js 文件开头写着版本号是 v1.1.1 ,但是其实它 是 2013-08-14 发布的。而具体加上 aggregation 支持的时间是 2014-03-16 ,但是版本 号依然是 v1.1.1 !所以在官网文档看到 1.1.1 版的 aggs 语法,其实在 Kibana 里都用 不了。 elastic.js 新版在支持 aggs 接口的同时,对本身的底层依赖也做了大幅度改动,和 ES 的 实际交互已经改用官方的 elasticsearch.js 库,elastic.js 本身相当于只是做一个封装。 但是 elasticsearch.js 本身目录结构复杂,加入到 require.config.js 里也不是那么容 易。 所以,这里有两种解决办法。 1. 直接使用 angularjs 框架的 $.http 对象,手拼 JSON 请求体。把 kibana 当做 curl 来处 理得了。 ELKstack 中文指南 458用agg接口开发一个percentile panel request = { 'stats': { 'filter': JSON.parse($scope.ejs.QueryFilter( $scope.ejs.FilteredQuery( boolQuery, filterSrv.getBoolFilter(filterSrv.ids()) ) ).toString(), true), 'aggs': { 'stats': { 'percentiles': { 'field': $scope.panel.field, 'percents': $scope.modes } } } } }; $.each(queries, function (i, q) { var query = $scope.ejs.BoolQuery(); query.should(querySrv.toEjsObj(q)); var qname = 'stats_'+i; var aggsquery = {}; aggsquery[qname] = { 'percentiles': { 'field': $scope.panel.field, 'percents': $scope.modes } }; request[qname] = { 'filter': JSON.parse($scope.ejs.QueryFilter( $scope.ejs.FilteredQuery( query, filterSrv.getBoolFilter(filterSrv.ids()) ) ).toString(), true), 'aggs': aggsquery }; }); $scope.inspector = angular.toJson({aggs:request},true); results = $http({ url: config.elasticsearch + '/' + dashboard.indices + '/_search?size=0', method: "POST", data: { aggs: request } }); 1. 统一升级整个 kibana3 的基础依赖库版本,然后采用 agg 接口方法。 ELKstack 中文指南 459用agg接口开发一个percentile panel request = $scope.ejs.Request(); $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); queries = querySrv.getQueryObjs($scope.panel.queries.ids); boolQuery = $scope.ejs.BoolQuery(); _.each(queries,function(q) { boolQuery = boolQuery.should(querySrv.toEjsObj(q)); }); var percents = _.keys($scope.panel.show); request = request .aggregation( $scope.ejs.FilterAggregation('stats') .filter($scope.ejs.QueryFilter( $scope.ejs.FilteredQuery( boolQuery, filterSrv.getBoolFilter(filterSrv.ids()) ) )) .aggregation($scope.ejs.PercentilesAggregation('stats') .field($scope.panel.field) .percents(percents) .compression($scope.panel.compression) ) ).size(0); 页面参数上,根据 percentile 的请求参数需要,主要需要提供一个 $scope.panel.modes 数组 即可。不赘述。 代码实现要点 1.1 和 1.3 版本的返回结果集层次变动 percentile Aggregation 是 Elasticsearch 从 1.1.0 开始新加入的实验性功能,而且在 1.3.0 之 后其返回的数据结构发生了变动。所以代码中对 ES 的版本要做判断和兼容性处理。 Kibana3 提供了一个 service 叫 esVersion ,所以我们可以直接这样: ELKstack 中文指南 460用agg接口开发一个percentile panel module.controller('percentiles', function ($scope, querySrv, dashboard, filterSrv, $http, esVersion) { ... results.then(function(results) { $scope.panelMeta.loading = false; esVersion.gte('1.3.0').then(function(is) { if (is) { var value = results.aggregations.stats['stats']['values'][$scope.panel.mode+'.0']; ... } else { esVersion.gte('1.1.0').then(function(is) { if (is) { var value = results.aggregations.stats['stats'][$scope.panel.mode+'.0']; ... } }); } }); }); 浮点数排序 percentile Aggregation 返回的数据中,强制保留了百分数的小数点后一位,这导致在 js 处理 中会把小数点当做是属性调用的操作符,Kibana 提供的表头点击自动排序表格数据功能也就 失效了。所以需要替换掉 sort_field 里的小数点。 module.js 中: ELKstack 中文指南 461用agg接口开发一个percentile panel var rows = queries.map(function (q, i) { var alias = q.alias || q.query; var obj = _.clone(q); obj.label = alias; obj.Label = alias.toLowerCase(); //sort field obj.value = results.aggregations['stats_'+i]['stats_'+i]['values']; obj.Value = results.aggregations['stats_'+i]['stats_'+i]['values']; //sort field var _V = {} for ( var k in obj.Value ) { var v = obj.Value[k]; k = k.replace('.',''); _V[k] = v; } obj.Value = _V; return obj; }); $scope.data = { value: value, rows: rows }; $scope.$emit('render'); module.html 中: {{panel.label_name}} {{stat}}% query alias 的中文支持 目前 Kibana 里都是以 alias 形式来区分每一个子请求的,具体内容是 var alias = q.alias || q.query; ,即在页面上搜索框里写的查询语句或者是搜索框左侧色彩设置菜单里的 Legend value 。 比如我的场景下, q.query 是 "xff:10.5.16.*", q.alias 是"教育网访问"。那么最后发送的请 求里这条过滤项的 facets_name 就叫 "stats_教育网访问"。 ELKstack 中文指南 462用agg接口开发一个percentile panel 同样的写法迁移到 aggregation 上就完全不可解析了。服务器会返回一条报错 说: aggregation_name 只能是字母、数字、 _ 或者 - 四种。 这里比较怪的是抓包看到 facets 其实也报错说请求内容解析失败,但是居然同时也返回了结 果,只能猜测目前是处在一种兼容状态? 于是这里稍微修改了一下逻辑,把 queries 数组的 _.each 改用 $.each 来做,这样回调函 数里不单返回数组元素,还返回数组下标,下标是一定为数字的,就可以以数组下标作为 aggregation_name 了。后面处理结果的 queries.map 同样以下标来获取即可。 $.each(queries, function (i, q) { var query = $scope.ejs.BoolQuery(); query.should(querySrv.toEjsObj(q)); var qname = 'stats_'+i; request.aggregation( $scope.ejs.FilterAggregation(qname) .filter($scope.ejs.QueryFilter( $scope.ejs.FilteredQuery( query, filterSrv.getBoolFilter(filterSrv.ids()) ) )) .aggregation($scope.ejs.PercentilesAggregation(qname) .field($scope.panel.field) .percents(percents) .compression($scope.panel.compression) ) ); }); $scope.inspector = request.toJSON(); results = $scope.ejs.doSearch(dashboard.indices, request); 面板效果 最终的 percentile 面板界面与 stats 面板界面类似。 ELKstack 中文指南 463用agg接口开发一个percentile panel percentile panel 完整版代码, 见:https://github.com/chenryn/kibana/src/app/panels/percentiles。 ELKstack 中文指南 464用agg接口开发一个percentile panel Kibana4 Kibana4 是 Elastic.co 一次崭新的重构产品。在操作界面上,有一定程度的对 Splunk 的模 仿。为了更直观的体现 Kibana4 跟 Kibana3 的不同,先让我们看看 Kibana4 要怎么用来探索 和展示数据。 配置索引模式后,默认打开 Kibana4 会出现在一个叫做 Discover 的标签页,在这个类似 Kibana3 的 logstash dashboard 的页面上,我们可以提交搜索请求,过滤结果,然后检查返 回的文档里的数据。如下图示: 默认情况下,Discover 页会显示匹配搜索条件的前 500 个文档,页面下拉到底部,会自动加 载后续数据。你可以修改时间过滤器,拖拽直方图下钻数据,查看部分文档的细节。Discover 页上如何探索数据,详细说明见 Discover 功能。 Visualization 标签页用来为你的搜索结果构造可视化。每个可视化都是跟一个搜索关联着的。 比如,我们可以基于前面那个搜索创建一个展示每周流量的直方图。Y 轴显示流量。X 轴显示 时间。而添加一个子聚合,我们还可以看到每小时排名前三的结果。 ELKstack 中文指南 465k4 还可以保存并分析可视化结果,然后合并到 Dashboard 标签页上以便对比分析。比如说,我 们可以创建一个展示多个可视化数据的仪表板: 更多关于创建和分享可视化和仪表板的内容,请阅读 Visualize 和 Dashboard 章节。 ELKstack 中文指南 466k4 安排、配置和运行 Kibana4 安装方式依然简单,你可以在几分钟内安装好 Kibana 然后开始探索你的 Elasticsearch 索引。只需要预备: Elasticsearch 1.4.4 或者更新的版本 一个现代浏览器 - 支持的浏览器列表. 有关你的 Elasticsearch 集群的信息: 你想要连接 Elasticsearch 实例的 URL 你想搜索哪些 Elasticsearch 索引 如果你的 Elasticsearch 是被 Shield 保护着的,阅读生产环境部署章节相关内容学习额 外的安装说明。 安装并启动 kibana 要安装启动 Kibana: 1. 下载对应平台的 Kibana 4 二进制包 2. 解压 .zip 或 tar.gz 压缩文件 3. 在安装目录里运行: bin/kibana (Linux/MacOSX) 或 bin\kibana.bat (Windows) 完毕!Kibana 现在运行在 5601 端口了。 让 kibana 连接到 elasticsearch 在开始用 Kibana 之前,你需要告诉它你打算探索哪个 Elasticsearch 索引。第一次访问 Kibana 的时候,你会被要求定义一个 index pattern 用来匹配一个或者多个索引名。好了。这 就是你需要做的全部工作。以后你还可以随时从 Settings 标签页添加更多的 index pattern。 默认情况下,Kibana 会连接运行在 localhost 的 Elasticsearch。要连接其他 Elasticsearch 实例,修改 kibana.yml 里的 Elasticsearch URL,然后重启 Kibana。如 何在生产环境下使用 Kibana,阅读生产环境部署章节。 要从 Kibana 访问的 Elasticsearch 索引的配置方法: 1. 从浏览器访问 Kibana 界面。也就是说访问比如 localhost:5601 或者 http://YOURDOMAIN.com:5601 。 2. 制定一个可以匹配一个或者多个 Elasticsearch 索引的 index pattern 。默认情况下, Kibana 认为你要访问的是通过 Logstash 导入 Elasticsearch 的数据。这时候你可以用默 ELKstack 中文指南 467安装、配置和运行 认的 logstash-* 作为你的 index pattern。通配符(*) 匹配索引名中零到多个字符。如果 你的 Elasticsearch 索引有其他命名约定,输入合适的 pattern。pattern 也开始是最简单 的单个索引的名字。 3. 选择一个包含了时间戳的索引字段,可以用来做基于时间的处理。Kibana 会读取索引的 映射,然后列出所有包含了时间戳的字段(译者注:实际是字段类型为 date 的字段,而不 是“看起来像时间戳”的字段)。如果你的索引没有基于时间的数据,关闭 Index contains time-based events 参数。 4. 如果一个新索引是定期生成,而且索引名中带有时间戳,选择 Use event times to create index names 选项,然后再选择 Index pattern interval 。这可以提高搜索性能, Kibana 会至搜索你指定的时间范围内的索引。在你用 Logstash 输出数据给 Elasticsearch 的情况下尤其有效。 5. 点击 Create 添加 index pattern。第一个被添加的 pattern 会自动被设置为默认值。如果 你有多个 index pattern 的时候,你可以在 Settings > Indices 里设置具体哪个是默认 值。 好了。Kibana 现在连接上你的 Elasticsearch 数据了。Kibana 会显示匹配上的索引里的字段 名的只读列表。 开始探索你的数据! 你可以开始下钻你的数据了: 在 Discover 页搜索和浏览你的数据。 在 Visualize 页转换数据成图表。 在 Dashboard 页创建定制自己的仪表板。 ELKstack 中文指南 468安装、配置和运行 生产环境部署 Kibana4 是是一个完整的 web 应用。使用时,你需要做的只是打开浏览器,然后输入你运行 Kibana 的机器地址然后加上端口号。比如说: localhost:5601 或者 http://YOURDOMAIN.com:5601 。 但是当你准备在生产环境使用 Kibana4 的时候,比起在本机运行,就需要多考虑一些问题: 在哪运行 kibana 是否需要加密 Kibana 出入的流量 是否需要控制访问数据的权限 Nginx 代理配置 因为 Kibana4 不再是 Kibana3 那种纯静态文件的单页应用,所以其服务器端是需要消耗计算 资源的。因此,如果用户较多,Kibana4 确实有可能需要进行多点部署,这时候,就需要用 Nginx 做一层代理了。 和 Kibana3 相比,Kibana4 的 Nginx 代理配置倒是简单许多,因为所有流量都是统一配置 的。下面是一段包含入口流量加密、简单权限控制的 Kibana4 代理配置: ELKstack 中文指南 469生产环境部署 upstream kibana4 { server 127.0.0.1:5601 fail_timeout=0; } server { listen *:80; server_name kibana_server; access_log /var/log/nginx/kibana.srv-log-dev.log; error_log /var/log/nginx/kibana.srv-log-dev.error.log; ssl on; ssl_certificate /etc/nginx/ssl/all.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location / { root /var/www/kibana; index index.html index.htm; } location ~ ^/kibana4/.* { proxy_pass http://kibana4; rewrite ^/kibana4/(.*) /$1 break; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; auth_basic "Restricted"; auth_basic_user_file /etc/nginx/conf.d/kibana.myhost.org.htpasswd; } } 如果用户够多,当然你可以单独跑一个 kibana4 集群,然后在 upstream 配置段中添加多个 代理地址做负载均衡。 配置 Kibana 和 shield 一起工作 Nginx 只能加密和管理浏览器到服务器端的请求,而 Kibana4 到 ELasticsearch 集群的请求, 就需要由 Elasticsearch 方面来完成了。如果你在用 Shield 做 Elasticsearch 用户认证,你需 要给 Kibana 提供用户凭证,这样它才能访问 .kibana 索引。Kibana 用户需要由权限访问 .kibana 索引里以下操作: ELKstack 中文指南 470生产环境部署 '.kibana': - indices:admin/create - indices:admin/exists - indices:admin/mapping/put - indices:admin/mappings/fields/get - indices:admin/refresh - indices:admin/validate/query - indices:data/read/get - indices:data/read/mget - indices:data/read/search - indices:data/write/delete - indices:data/write/index - indices:data/write/update - indices:admin/create 更多配置 Shield 的内容,请阅读官网的 Shield with Kibana 4。 要配置 Kibana 的凭证,设置 kibana.yml 里的 kibana_elasticsearch_username 和 kibana_elasticsearch_password 选项即可: # If your Elasticsearch is protected with basic auth: kibana_elasticsearch_username: kibana4 kibana_elasticsearch_password: kibana4 开启 ssl Kibana 同时支持对客户端请求以及 Kibana 服务器发往 Elasticsearch 的请求做 SSL 加密。 要加密浏览器(或者在 Nginx 代理情况下,Nginx 服务器)到 Kibana 服务器之间的通信,配置 kibana.yml 里的 ssl_key_file 和 ssl_cert_file 参数: # SSL for outgoing requests from the Kibana Server (PEM formatted) ssl_key_file: /path/to/your/server.key ssl_cert_file: /path/to/your/server.crt 如果你在用 Shield 或者其他提供 HTTPS 的代理服务器保护 Elasticsearch,你可以配置 Kibana 通过 HTTPS 方式访问 Elasticsearch,这样 Kibana 服务器和 Elasticsearch 之间的通 信也是加密的。 要做到这点,你需要在 kibana.yml 里配置 Elasticsearch 的 URL 时指明是 HTTPS 协议: elasticsearch: "https://.com:9200" ELKstack 中文指南 471生产环境部署 如果你给 Elasticsearch 用的是自己签名的证书,请在 kibana.yml 里设定 ca 参数指明 PEM 文件位置,这也意味着开启了 verify_ssl 参数: # If you need to provide a CA certificate for your Elasticsarech instance, put # the path of the pem file here. ca: /path/to/your/ca/cacert.pem 控制访问权限 你可以用 Elasticsearch Shield 来控制用户通过 Kibana 可以访问到的 Elasticsearch 数据。 Shield 提供了索引级别的访问控制。如果一个用户没被许可运行这个请求,那么它在 Kibana 可视化界面上只能看到一个空白。 要配置 Kibana 使用 Shield,你要位 Kibana 创建一个或者多个 Shield 角色(role),以 kibana4 作为开头的默认角色。更详细的做法,请阅读 Using Shield with Kibana 4。 ELKstack 中文指南 472生产环境部署 discover 功能 Discover 标签页用于交互式探索你的数据。你可以访问到匹配得上你选择的索引模式的每个 索引的每条记录。你可以提交搜索请求,过滤搜索结果,然后查看文档数据。你还可以看到 匹配搜索请求的文档总数,获取字段值的统计情况。如果索引模式配置了时间字段,文档的 时序分布情况会在页面顶部以柱状图的形式展示出来。 设置时间过滤器 时间过滤器(Time Filter)限制搜索结果在一个特定的时间周期内。如果你的索引包含的是时序 诗句,而且你为所选的索引模式配置了时间字段,那么就就可以设置时间过滤器。 默认的时间过滤器设置为最近 15 分钟。你可以用页面顶部的时间选择器(Time Picker)来修改 时间过滤器,或者选择一个特定的时间间隔,或者直方图的时间范围。 要用时间选择器来修改时间过滤器: 1. 点击菜单栏右上角显示的 Time Filter 打开时间选择器。 2. 快速过滤,直接选择一个短链接即可。 3. 要指定相对时间过滤,点击 Relative 然后输入一个相对的开始时间。可以是任意数字的 秒、分、小时、天、月甚至年之前。 4. 要指定绝对时间过滤,点击 Absolute 然后在 From 框内输入开始日期,To 框内输入结束 日期。 5. 点击时间选择器底部的箭头隐藏选择器。 要从住房图上设置时间过滤器,有以下几种方式: ELKstack 中文指南 473discover功能 想要放大那个时间间隔,点击对应的柱体。 单击并拖拽一个时间区域。注意需要等到光标变成加号,才意味着这是一个有效的起始 点。 你可以用浏览器的后退键来回退你的操作。 搜索数据 在 Discover 页提交一个搜索,你就可以搜索匹配当前索引模式的索引数据了。你可以直接输 入简单的请求字符串,也就是用 Lucene query syntax,也可以用完整的基于 JSON 的 Elasticsearch Query DSL。 当你提交搜索的时候,直方图,文档表格,字段列表,都会自动反映成搜索的结果。hits(匹配 的文档)总数会在直方图的右上角显示。文档表格显示前 500 个匹配文档。默认的,文档倒序 排列,最新的文档最先显示。你可以通过点击时间列的头部来反转排序。事实上,所有建了 索引的字段,都可以用来排序,稍后会详细说明。 要搜索你的数据: 1. 在搜索框内输入请求字符串: 简单的文本搜索,直接输入文本字符串。比如,如果你在搜索网站服务器日志,你 可以输入 safari 来搜索各字段中的 safari 单词。 要搜索特定字段中的值,则在值前加上字段名。比如,你可以输入 status:200 来 限制搜索结果都是在 status 字段里有 200 内容。 要搜索一个值的范围,你可以用范围查询语法, [START_VALUE TO END_VALUE] 。比 如,要查找 4xx 的状态码,你可以输入 status:[400 TO 499] 。 要指定更复杂的搜索标准,你可以用布尔操作符 AND , OR , 和 NOT 。比如,要查 找 4xx 的状态码,还是 php 或 html 结尾的数据,你可以输入 status:[400 TO 499] AND (extension:php OR extension:html) 。 这些例子都用了 Lucene query syntax。你也可以提交 Elasticsearch Query DSL 式的请 求。更多示例,参见之前 Elasticsearch 章节。 1. 点击回车键,或者点击 Search 按钮提交你的搜索请求。 开始一个新的搜索 要清除当前搜索或开始一个新搜索,点击 Discover 工具栏的 New Search 按钮。 ELKstack 中文指南 474discover功能 保存搜索 你可以在 Discover 页加载已保存的搜索,也可以用作 visualizations 的基础。保存一个搜 索,意味着同时保存下了搜索请求字符串和当前选择的索引模式。 要保存当前搜索: 1. 点击 Discover 工具栏的 Save Search 按钮 。 2. 输入一个名称,点击 Save 。 加载一个已存搜索 要加载一个已保存的搜索: 1. 点击 Discover 工具栏的 Load Search 按钮 。 2. 选择你要加载的搜索。 如果已保存的搜索关联到跟你当前选择的索引模式不一样的其他索引上,加载这个搜索也会 切换当前的已选索引模式。 改变你搜索的索引 当你提交一个搜索请求,匹配当前的已选索引模式的索引都会被搜索。当前模式模式会显示 在搜索栏下方。要改变搜索的索引,需要选择另外的模式模式。 要选择另外的索引模式: 1. 点击 Discover 工具栏的 Settings 按钮 。 2. 从索引模式列表中选取你打算采用的模式。 关于索引模式的更多细节,请阅读稍后 Setting 功能小节。 自动刷新页面 亦可以配置一个刷新间隔来自动刷新 Discover 页面的最新索引数据。这回定期重新提交一次 搜索请求。 设置刷新间隔后,会显示在菜单栏时间过滤器的左边。 要设置刷新间隔: ELKstack 中文指南 475discover功能 1. 点击菜单栏右上角的 Time Filter 。 2. 点击 Refresh Interval 标签。 3. 从列表中选择一个刷新间隔。 要想自动刷新数据,点击 Auto-refresh 按钮然后选择一个自动刷新间 隔: 开启自动刷新后,Kibana 的顶部栏会出现一个暂停按钮和自动刷新的间隔: 。 点击 Pause 按钮可以暂停自动刷新。 按字段过滤 你可以过滤搜索结果,只显示在某字段中包含了特定值的文档。也可以创建反向过滤器,排 除掉包含特定字段值的文档。 你可以从字段列表或者文档表格里添加过滤器。当你添加好一个过滤器后,它会显示在搜索 请求下方的过滤栏里。从过滤栏里你可以编辑或者关闭一个过滤器,转换过滤器(从正向改成 反向,反之亦然),切换过滤器开关,或者完全移除掉它。 要从字段列表添加过滤器: 1. 点击你想要过滤的字段名。会显示这个字段的前 5 名数据。每个数据的右侧,有两个小 按钮 —— 一个用来添加常规(正向)过滤器,一个用来添加反向过滤器。 2. 要添加正向过滤器,点击 Positive Filter 按钮 。这个会过滤掉在本字段不包含这 个数据的文档。 3. 要添加反向过滤器,点击 Negative Filter 按钮 。这个会过滤掉在本字段包含这个 数据的文档。 要从文档表格添加过滤器: 1. 点击表格第一列(通常都是时间)文档内容左侧的 Expand 按钮 展开文档表格中的文 档。每个字段名的右侧,有两个小按钮 —— 一个用来添加常规(正向)过滤器,一个用来 添加反向过滤器。 2. 要添加正向过滤器,点击 Positive Filter 按钮 。这个会过滤掉在本字段不包含这 ELKstack 中文指南 476discover功能 个数据的文档。 3. 要添加反向过滤器,点击 Negative Filter 按钮 。这个会过滤掉在本字段包含这个 数据的文档。 过滤器(Filter)的协同工作方式 在 Kibana 的任意页面创建过滤器后,就会在搜索输入框的下方,出现一个绿色椭圆形的过滤 条件: 鼠标移动到过滤条件上,会显示下面几个图标: 过滤器开关 点击这个图标,可以在不移除过滤器的情况下关闭过滤条件。再次点击 则重新打开。被禁用的过滤器是条纹状的灰色,要求包含(相当于 Kibana3 里的 must)的 过滤条件显示为绿色,要求排除(相当于 Kibana3 里的 mustNot)的过滤条件显示为红色。 过滤器图钉 点击这个图标钉住过滤器。被钉住的过滤器,可以横贯 Kibana 各个标签 生效。比如在 Visualize 标签页钉住一个过滤器,然后切换到 Discover 或者 Dashboard 标签页,过滤器依然还在。注意:如果你钉住了过滤器,然后发现检索结果为空,注意 查看当前标签页的索引模式是不是跟过滤器匹配。 过滤器反转 点击这个图标反转过滤器。默认情况下,过滤器都是包含型,显示为 绿色,只有匹配过滤条件的结果才会显示。反转成排除型过滤器后,显示的是不匹配过 滤器的检索项,显示为红色。 移除过滤器 点击这个图标删除过滤器。 自定义过滤器 点击这个图标会打开一个文本编辑框。编辑框内可以修改 JSON 形式 的过滤器内容,并起一个 alias 别名: ELKstack 中文指南 477discover功能 JSON 中可以灵活应用 bool query 组合各种 should 、 must 、 must_not 条件。一个用 should 表达的 OR 关系过滤如下: { "bool": { "should": [ { "term": { "geoip.country_name.raw": "Canada" } }, { "term": { "geoip.country_name.raw": "China" } } ] } } 想要对当前页所有过滤器统一执行上面的某个操作,点击 Global Filter Actions 按钮,然后再执行操作即可。 ELKstack 中文指南 478discover功能 查看文档数据 当你提交一个搜索请求,最近的 500 个搜索结果会显示在文档表格里。你可以在 Advanced Settings 里通过 discover:sampleSize 属性配置表格里具体的文档数量。默认的,表格会显 示当前选择的索引模式中定义的时间字段内容(转换成本地时区)以及 _source 文档。你可以 从字段列表添加字段到文档表格。还可以用表格里包含的任意已建索引的字段来排序列出的 文档。 要查看一个文档的字段数据,点击表格第一列(通常都是时间)文档内容左侧的 Expand 按钮 。Kibana 从 Elasticsearch 读取数据然后在表格中显示文档字段。这个表格每行是一个字 段的名字、过滤器按钮和字段的值。 1. 要查看原始 JSON 文档(格式美化过的),点击 JSON 标签。 2. 要在单独的页面上查看文档内容,点击链接。你可以添加书签或者分享这个链接,以直 接访问这条特定文档。 3. 收回文档细节,点击 Collapse 按钮 。 4. To toggle a particular field’s column in the Documents table, click the Toggle column in table button. 文档列表排序 你可以用任意已建索引的字段排序文档表格中的数据。如果当前索引模式配置了时间字段, 默认会使用该字段倒序排列文档。 要改变排序方式: 点击想要用来排序的字段名。能用来排序的字段在字段名右侧都有一个排序按钮。再次 ELKstack 中文指南 479discover功能 点击字段名,就会反向调整排序方式。 给文档表格添加字段列 By default, the Documents table shows the localized version of the time field specified in the selected index pattern and the document _source . You can add fields to the table from the Fields list. 默认的,文档表格会显示当前选择的索引模式中定义的时间字段内容(转换成本地 时区)以及 _source 文档。你可以从字段列表添加字段到文档表格。 要添加字段列到文档表格: 1. 移动鼠标到字段列表的字段上,点击它的 add 按钮 。 2. 重复操作直到你添加完所有你想移除的字段。 添加的字段会替换掉文档表格里的 _source 列。同时还会显示在字段列表顶部的 Selected Fields 区域里。 要重排表格中的字段列,移动鼠标到你要移动的列顶部,点击移动过按钮。 从文档表格删除字段列 要从文档表格删除字段列: 1. 移动鼠标到字段列表的 Selected Fields 区域里你想要移除的字段上,然后点击它的 remove 按钮 。 2. 重复操作直到你移除完所有你想移除的字段。 查看字段数据统计 从字段列表,你可以看到文档表格里有多少数据包含了这个字段,排名前 5 的值是什么,以 及包含各个值的文档的占比。 要查看字段数据统计: 点击字段列表里一个字段的名字。这个字段可以在字段列表的任意位置 —— 已选字段 (Selected Fields),常用字段(Popular Fields),或其他字段。 ELKstack 中文指南 480discover功能 要基于这个字段创建可视化,点击字段统计下方的 Visualize 按钮。 ELKstack 中文指南 481discover功能 各 Visualize 功能 Visualize 标签页用来设计可视化。你可以保存可视化,以后再用,或者加载合并到 dashboard 里。一个可视化可以基于以下几种数据源类型: 一个新的交互式搜索 一个已保存的搜索 一个已保存的可视化 可视化是基于 Elasticsearch 1.0 引入的聚合(aggregation) 特性。 创建一个新可视化 要开始一个 New Visualization 向导,点击页面左上角的 Visualize 标签。如果你已经在创建 一个可视化了。你可以在搜索栏的右侧工具栏里点击 New Visualization 按钮 向导会引导 你继续以下几步: 第 1 步: 选择可视化类型 New Visualization 向导起始页如下: 你也可以加载一个你之前创建好保存下来的可视化。已存可视化选择器包括一个文本框用来 过滤可视化名称,以及一个指向 对象编辑器(Object Editor) 的链接,可以通过 Settings > Edit Saved Objects 来管理已存的可视化。 如果你的新可视化是一个 Markdown 挂件,选择这个类型会带你到一个文本内容框,你可以 在框内输入打算显示在挂件里的文本。其他的可视化类型,选择后都会转到数据源选择。 第 2 步: 选择数据源 你可以选择新建或者读取一个已保存的搜索,作为你可视化的数据源。搜索是和一个或者一 系列索引相关联的。如果你选择了在一个配置了多个索引的系统上开始你的新搜索,从可视 化编辑器的下拉菜单里选择一个索引模式。 当你从一个已保存的搜索开始创建并保存好了可视化,这个搜索就绑定在这个可视化上。如 果你修改了搜索,对应的可视化也会自动更新。 第 3 步: 可视化编辑器 ELKstack 中文指南 482各visualize功能 The visualization editor enables you to configure and edit visualizations. The visualization editor has the following main elements: 可视化编辑器用来配置编辑可视化。它有下面几个主 要元素: 1. 工具栏(Toolbar) 2. 聚合构建器(Aggregation Builder) 3. 预览画布(Preview Canvas) 工具栏 工具栏上有一个用户交互式数据搜索的搜索框,用来保存和加载可视化。因为可视化是基于 保存好的搜索,搜索栏会变成灰色。要编辑搜索,双击搜索框,用编辑后的版本替换已保存 搜索。 搜索框右侧的工具栏有一系列按钮,用于创建新可视化,保存当前可视化,加载一个已有可 视化,分享或内嵌可视化,和刷新当前可视化的数据。 聚合构建器 用页面左侧的聚合构建器配置你的可视化要用的 metric 和 bucket 聚合。桶(Buckets) 的效果 类似于 SQL GROUP BY 语句。想更详细的了解聚合,阅读 Elasticsearch aggregations reference。 在条带图或者折线图可视化里,用 metrics 做 Y 轴,然后 buckets 做 X 轴,条带颜色,以及 行/列的区分。在饼图里,metrics 用来做分片的大小,buckets 做分片的数量。 为你的可视化 Y 轴选一个 metric 聚合,包括 count, average, sum, min, max, or cardinality (unique count). 为你的可视化 X 轴,条带颜色,以及行/列的区分选一个 bucket 聚合,常见 的有 date histogram, range, terms, filters, 和 significant terms。 你可以设置 buckets 执行的顺序。在 Elasticsearch 里,第一个聚合决定了后续聚合的数据 集。下面例子演示一个网页访问量前五名的文件后缀名统计的时间条带图。 ELKstack 中文指南 483各visualize功能 要看所有相同后缀名的,设置顺序如下: 1. Color : 后缀名的 Terms 聚合 2. X-Axis : @timestamp 的时间条带图 Elasticsearch 收集记录,算出前 5 名后缀名,然后为每个后缀名创建一个时间条带图。 要看每个小时的前 5 名后缀名情况,设置顺序如下: 1. X-Axis : @timestamp 的时间条带图( 1 小时间隔) 2. Color : 后缀名的 Terms 聚合 这次,Elasticsearch 会从所有记录里创建一个时间条带图,然后在每个桶内,分组(本例中就 是一个小时的间隔)计算出前 5 名的后缀名。 记住,每个后续的桶,都是从前一个的桶里分割数据。 要在预览画布(preview canvas)上渲染可视化,点击聚合构建器底部的 Apply 按钮。 预览画布(canvas) 预览 canvas 上显示你定义在聚合构建器里的可视化的预览效果。要刷新可视化预览,点击工 具栏里的 Refresh 按钮 。 ELKstack 中文指南 484各visualize功能 区块图 这个图的 Y 轴是数值维度。该维度有以下聚合可用: Count count 聚合返回选中索引模式中元素的原始计数。 Average 这个聚合返回一个数值字段的 average 。从下拉菜单选择一个字段。 Sum sum 聚合返回一个数值字段的总和。从下拉菜单选择一个字段。 Min min 聚合返回一个数值字段的最小值。从下拉菜单选择一个字段。 Max max 聚合返回一个数值字段的最大值。从下拉菜单选择一个字段。 Unique Count cardinality 聚合返回一个字段的去重数据值。从下拉菜单选择一个字段。 Standard Deviation extended stats 聚合返回一个数值字段数据的标准差。从下拉菜单选 择一个字段。 Percentile percentile 聚合返回一个数值字段中值的百分比分布。从下拉菜单选择一个字 段,然后在 Percentiles 框内指定范围。点击 X 移除一个百分比框,点击 +Add 添加一 个百分比框。 Percentile Rank percentile ranks 聚合返回一个数值字段中你指定值的百分位排名。从下 拉菜单选择一个字段,然后在 Values 框内指定一到多个百分位排名值。点击 X 移除一 个百分比框,点击 +Add 添加一个数值框。 你可以点击 + Add Aggregation 按键添加一个聚合。 buckets 聚合指明从你的数据集中将要检索什么信息。 图形的 X 轴是buckets 维度。你可以为 X 轴定义 buckets,同样还可以为图片上的分片区域, 或者分割的图片定义 buckets。 该图形的 X 轴支持以下聚合。点击每个聚合的链接查看该聚合的 Elasticsearch 官方文档。 Date Histogram date histogram 基于数值字段创建,由时间组织起来。你可以指定时间 片的间隔,单位包括秒,分,小时,天,星期,月,年。 Histogram 标准 histogram 基于数值字段创建。为这个字段指定一个整数间隔。勾选 Show empty buckets 让直方图中包含空的间隔。 Range 通过 range 聚合。你可以为一个数值字段指定一系列区间。点击 Add Range 添 加一对区间端点。点击红色 (x) 符号移除一个区间。 Date Range date range 聚合计算你指定的时间区间内的值。你可以使用 date math 表达 式指定区间。点击 Add Range 添加新的区间端点。点击红色 (/) 符号移除区间。 IPv4 Range IPv4 range 聚合用来指定 IPv4 地址的区间。点击 Add Range 添加新的区 间端点。点击红色 (/) 符号移除区间。 Terms terms 聚合允许你指定展示一个字段的首尾几个元素,排序方式可以是计数或者其 他自定义的metric。 Filters 你可以为数据指定一组 filters。你可以用 query string,也可以用 JSON 格式来指 ELKstack 中文指南 485各visualize功能 定过滤器,就像在 Discover 页的搜索栏里一样。点击 Add Filter 添加下一个过滤器。 Significant Terms 展示实验性的 significant terms 聚合的结果。 一旦你定义好了一个 X 轴聚合。你可以继续定义子聚合来完善可视化效果。点击 + Add Sub Aggregation 添加子聚合,然后选择 Split Area 或者 Split Chart,然后从类型菜单中选择一 个子聚合。 当一个图形中定义了多个聚合,你可以使用聚合类型右侧的上下箭头来改变聚合的优先级。 比如,一个事件计数的日期图,可以按照时序显示,你也可以提升事件聚合的优先级,首先 显示最活跃的几天。时序图用来显示事件随着时间变化的趋势,而按照活跃时间排序则可以 揭示你数据中的部分异常值。 你可以点击 Advanced 链接显示更多有关聚合的自定义参数: Exclude Pattern 指定一个从结果集中排除掉的模式。 Exclude Pattern Flags 排除模式的 Java flags 标准集。 Include Pattern 指定一个从结果集中要包含的模式。 Include Pattern Flags 包含模式的 Java flags 标准集。 JSON Input 一个用来添加 JSON 格式属性的文本框,内容会合并进聚合的定义中,格式 如下例: { "script" : "doc['grade'].value * 1.2" } 注意 Elasticsearch 1.4.3 及以后版本,这个函数需要你开启 dynamic Groovy scripting。 这些参数是否可用,依赖于你选择的聚合函数。 选择 view options 更改表格中如下方面: Chart Mode 当你为图形定义了多个 Y 轴时,你可以用该下拉菜单定义聚合如何显示在图 形上: stacked 聚合结果依次叠加在顶部。 overlap 聚合结果重叠的地方采用半透明效果。 wiggle 聚合结果显示成 streamgraph 效果。 percentage 显示每个聚合在总数中的百分值。 silhouette 显示每个聚合距离中间线的方差。 多选框可以用来控制以下行为: Smooth Lines 勾选该项平滑数据点之间的折线成曲线。 Set Y-Axis Extents 勾选该项,然后在 y-max 和 y-min 框里输入数值限定 Y 轴为指定数 值。 ELKstack 中文指南 486各visualize功能 Scale Y-Axis to Data Bounds 默认的 Y 轴长度为 0 到数据集的最大值。勾选该项改变 Y 轴的最大和最小值为数据集的返回值。 Show Tooltip 勾选该项显示工具栏。 Show Legend 勾选该项在图形右侧显示图例。 ELKstack 中文指南 487各visualize功能 数据表格 Count count 聚合返回选中索引模式中元素的原始计数。 Average 这个聚合返回一个数值字段的 average 。从下拉菜单选择一个字段。 Sum sum 聚合返回一个数值字段的总和。从下拉菜单选择一个字段。 Min min 聚合返回一个数值字段的最小值。从下拉菜单选择一个字段。 Max max 聚合返回一个数值字段的最大值。从下拉菜单选择一个字段。 Unique Count cardinality 聚合返回一个字段的去重数据值。从下拉菜单选择一个字段。 Standard Deviation extended stats 聚合返回一个数值字段数据的标准差。从下拉菜单选 择一个字段。 Percentile percentile 聚合返回一个数值字段中值的百分比分布。从下拉菜单选择一个字 段,然后在 Percentiles 框内指定范围。点击 X 移除一个百分比框,点击 + Add Percent 添加一个百分比框。 Percentile Rank percentile ranks 聚合返回一个数值字段中你指定值的百分位排名。从下 拉菜单选择一个字段,然后在 Values 框内指定一到多个百分位排名值。点击 X 移除一 个百分比框,点击 +Add 添加一个数值框。 你可以点击 + Add Aggregation 按键添加一个聚合。 数据表格的每行,叫做 buckets。你可以定义 buckets 来切割表格成行,或者切割表格成另一 个表格。 每个 bucket 类型都支持以下聚合: Date Histogram date histogram 基于数值字段创建,由时间组织起来。你可以指定时间 片的间隔,单位包括秒,分,小时,天,星期,月,年。 Histogram 标准 histogram 基于数值字段创建。为这个字段指定一个整数间隔。勾选 Show empty buckets 让直方图中包含空的间隔。 Range 通过 range 聚合。你可以为一个数值字段指定一系列区间。点击 Add Range 添 加一堆区间端点。点击红色 (x) 符号移除一个区间。 Date Range date range 聚合计算你指定的时间区间内的值。你可以使用 date math 表达 式指定区间。点击 Add Range 添加新的区间端点。点击红色 (/) 符号移除区间。 IPv4 Range IPv4 range 聚合用来指定 IPv4 地址的区间。点击 Add Range 添加新的区 间端点。点击红色 (/) 符号移除区间。 Terms terms 聚合允许你指定展示一个字段的首尾几个元素,排序方式可以是计数或者其 他自定义的metric。 Filters 你可以为数据指定一组 filters。你可以用 query string,也可以用 JSON 格式来指 定过滤器,就像在 Discover 页的搜索栏里一样。点击 Add Filter 添加下一个过滤器。 Significant Terms 展示实验性的 significant terms 聚合的结果。 Geohash geohash 聚合显示基于地理坐标的点。 ELKstack 中文指南 488各visualize功能 一旦你定义好了一个 X 轴聚合。你可以继续定义子聚合来完善可视化效果。点击 + Add Sub Aggregation 添加子聚合,然后选择 Split Area 或者 Split Chart,然后从类型菜单中选择一 个子聚合。 当一个图形中定义了多个聚合,你可以使用聚合类型右侧的上下箭头来改变聚合的优先级。 你可以点击 Advanced 链接显示更多有关聚合的自定义参数: Exclude Pattern 指定一个从结果集中排除掉的模式。 Exclude Pattern Flags 排除模式的 Java flags 标准集。 Include Pattern 指定一个从结果集中要包含的模式。 Include Pattern Flags 包含模式的 Java flags 标准集。 JSON Input 一个用来添加 JSON 格式属性的文本框,内容会合并进聚合的定义中,格式 如下例: { "script" : "doc['grade'].value * 1.2" } 注意 Elasticsearch 1.4.3 及以后版本,这个函数需要你开启 dynamic Groovy scripting。 这些参数是否可用,依赖于你选择的聚合函数。 选择 view options 更改表格中如下方面: Per Page 这个输入框控制表格的翻页。默认值是每页 10 行。 多选框用来控制以下行为: Show metrics for every bucket/level 勾选此项用以显示每个 bucket 聚合的中间结果。 Show partial rows 勾选此项显示没有数据的行。 注意 开启这些行为可能带来性能上的显著影响。 ELKstack 中文指南 489各visualize功能 Lines Charts 这个图的 Y 轴是数值维度。该维度有以下聚合可用: Count count 聚合返回选中索引模式中元素的原始计数。 Average 这个聚合返回一个数值字段的 average 。从下拉菜单选择一个字段。 Sum sum 聚合返回一个数值字段的总和。从下拉菜单选择一个字段。 Min min 聚合返回一个数值字段的最小值。从下拉菜单选择一个字段。 Max max 聚合返回一个数值字段的最大值。从下拉菜单选择一个字段。 Unique Count cardinality 聚合返回一个字段的去重数据值。从下拉菜单选择一个字段。 Standard Deviation extended stats 聚合返回一个数值字段数据的标准差。从下拉菜单选 择一个字段。 Percentile percentile 聚合返回一个数值字段中值的百分比分布。从下拉菜单选择一个字 段,然后在 Percentiles 框内指定范围。点击 X 移除一个百分比框,点击 + Add Percent 添加一个百分比框。 Percentile Rank percentile ranks 聚合返回一个数值字段中你指定值的百分位排名。从下 拉菜单选择一个字段,然后在 Values 框内指定一到多个百分位排名值。点击 X 移除一 个百分比框,点击 +Add 添加一个数值框。 你可以点击 + Add Aggregation 按键添加一个聚合。 buckets 聚合指明从你的数据集中将要检索什么信息。 在你选定一个 buckets 聚合之前,先指定你是要切割单个图的分片,还是切割成多个图形。 多图切分必须在其他任何聚合之前要运行。如果你切分图形,你可以选择切分结果是展示成 行还是列的形式,点击 Rows | Columns 选择器即可。 图形的 X 轴是buckets 维度。你可以为 X 轴定义 buckets,同样还可以为图片上的分片区域, 或者分割的图片定义 buckets。 该图形的 X 轴支持以下聚合。点击每个聚合的链接查看该聚合的 Elasticsearch 官方文档。 Date Histogram date histogram 基于数值字段创建,由时间组织起来。你可以指定时间 片的间隔,单位包括秒,分,小时,天,星期,月,年。 Histogram 标准 histogram 基于数值字段创建。为这个字段指定一个整数间隔。勾选 Show empty buckets 让直方图中包含空的间隔。 Range 通过 range 聚合。你可以为一个数值字段指定一系列区间。点击 Add Range 添 加一堆区间端点。点击红色 (x) 符号移除一个区间。 Date Range date range 聚合计算你指定的时间区间内的值。你可以使用 date math 表达 式指定区间。点击 Add Range 添加新的区间端点。点击红色 (/) 符号移除区间。 IPv4 Range IPv4 range 聚合用来指定 IPv4 地址的区间。点击 Add Range 添加新的区 ELKstack 中文指南 490各visualize功能 间端点。点击红色 (/) 符号移除区间。 Terms terms 聚合允许你指定展示一个字段的首尾几个元素,排序方式可以是计数或者其 他自定义的metric。 Filters 你可以为数据指定一组 filters。你可以用 query string,也可以用 JSON 格式来指 定过滤器,就像在 Discover 页的搜索栏里一样。点击 Add Filter 添加下一个过滤器。 Significant Terms 展示实验性的 significant terms 聚合的结果。 一旦你定义好了一个 X 轴聚合。你可以继续定义子聚合来完善可视化效果。点击 + Add Sub Aggregation 添加子聚合,然后选择 Split Area 或者 Split Chart,然后从类型菜单中选择一 个子聚合。 当一个图形中定义了多个聚合,你可以使用聚合类型右侧的上下箭头来改变聚合的优先级。 你可以点击 Advanced 链接显示更多有关聚合的自定义参数: Exclude Pattern 指定一个从结果集中排除掉的模式。 Exclude Pattern Flags 排除模式的 Java flags 标准集。 Include Pattern 指定一个从结果集中要包含的模式。 Include Pattern Flags 包含模式的 Java flags 标准集。 JSON Input 一个用来添加 JSON 格式属性的文本框,内容会合并进聚合的定义中,格式 如下例: { "script" : "doc['grade'].value * 1.2" } 注意 Elasticsearch 1.4.3 及以后版本,这个函数需要你开启 dynamic Groovy scripting。 这些参数是否可用,依赖于你选择的聚合函数。 多选框可以用来控制以下行为: Y 轴比例 可以给图形的 Y 轴选择 linear, log 或 square root 三种比例。你可以给指数变 化的数据采用 log 函数比例显示,比如复利图表;也可以用平方根(square root)比例显示 数值变化差异极大的数据集。这种可变性本身也算变量的一种的数据,又叫异方差数 据。比如,身高和体重的数据集,在较矮的区间变化是很小的,但是在较高另一个区 间,数据集就是异方差式的。 Smooth Lines 勾选该项,将图中的数据点用平滑曲线连接。注意:平滑曲线在高峰低谷 处给人的印象与实际值有较大偏差。 Show Connecting Lines 勾选该项,将图中的数据点用折线连接。 Show Circles 勾选该项,将图中的数据点绘制成一个小圆圈。 Current time marker 对时序数据,勾选该项可以在当前时刻位置标记一条红线。 Set Y-Axis Extents 勾选该项,然后在 y-max 和 y-min 框里输入数值限定 Y 轴为指定数 ELKstack 中文指南 491各visualize功能 值。 Show Tooltip 勾选该项显示工具栏。 Show Legend 勾选该项在图形右侧显示图例。 Scale Y-Axis to Data Bounds 默认的 Y 轴长度为 0 到数据集的最大值。勾选该项改变 Y 轴的最大和最小值为数据集的返回值。 更新选项后,点击绿色 Apply changes 按钮更新你的可视化界面,或者灰色 Discard changes 按钮保持原状。 气泡图(Bubble Charts) 通过以下步骤,可以转换折线图成气泡图: 1. 为 Y 轴点击 Add Metrics 然后选择 Dot Size。 2. 从下拉框里选择一个 metric 聚合函数。 3. 在 Options 标签里,去掉 Show Connecting Lines 的勾选。 4. 点击 Apply changes 按钮。 ELKstack 中文指南 492各visualize功能 Markdown 挂件 Markdown 挂件是一个存放 GitHub 风格 Markdown 内容的文本框。Kibana 会渲染你输入的 文本,然后在仪表盘上显示渲染结果。你可以点击 Help 连接跳转到 help page 查看 GitHub 风格 Markdown 的说明。点击 Apply 在预览框查看渲染效果,或者 Discard 回退成上一个版 本的内容。 ELKstack 中文指南 493各visualize功能 Metric metric 可视化为你选择的聚合显示一个单独的数字: Count count 聚合返回选中索引模式中元素的原始计数。 Average 这个聚合返回一个数值字段的 average 。从下拉菜单选择一个字段。 Sum sum 聚合返回一个数值字段的总和。从下拉菜单选择一个字段。 Min min 聚合返回一个数值字段的最小值。从下拉菜单选择一个字段。 Max max 聚合返回一个数值字段的最大值。从下拉菜单选择一个字段。 Unique Count cardinality 聚合返回一个字段的去重数据值。从下拉菜单选择一个字段。 Standard Deviation extended stats 聚合返回一个数值字段数据的标准差。从下拉菜单选 择一个字段。 Percentile percentile 聚合返回一个数值字段中值的百分比分布。从下拉菜单选择一个字 段,然后在 Percentiles 框内指定范围。点击 X 移除一个百分比框,点击 + Add Percent 添加一个百分比框。 Percentile Rank percentile ranks 聚合返回一个数值字段中你指定值的百分位排名。从下 拉菜单选择一个字段,然后在 Values 框内指定一到多个百分位排名值。点击 X 移除一 个百分比框,点击 +Add 添加一个数值框。 你可以点击 + Add Aggregation 按键添加一个聚合。你可以点击 Advanced 链接显示更多有 关聚合的自定义参数: Exclude Pattern 指定一个从结果集中排除掉的模式。 Exclude Pattern Flags 排除模式的 Java flags 标准集。 Include Pattern 指定一个从结果集中要包含的模式。 Include Pattern Flags 包含模式的 Java flags 标准集。 JSON Input 一个用来添加 JSON 格式属性的文本框,内容会合并进聚合的定义中,格式 如下例: { "script" : "doc['grade'].value * 1.2" } 注意 Elasticsearch 1.4.3 及以后版本,这个函数需要你开启 dynamic Groovy scripting。 这些参数是否可用,依赖于你选择的聚合函数。 点击 view options 修改显示 metric 的字体大小。 ELKstack 中文指南 494各visualize功能 饼图 饼图的分片大小通过 metrics 聚合定义。这个维度可以支持以下聚合: Count count 聚合返回选中索引模式中元素的原始计数。 Sum sum 聚合返回一个数值字段的总和。从下拉菜单选择一个字段。 Unique Count cardinality 聚合返回一个字段的去重数据值。从下拉菜单选择一个字段。 buckets 聚合指明从你的数据集中将要检索什么信息。 在你选定一个 buckets 聚合之前,先指定你是要切割单个图的分片,还是切割成多个图形。 多图切分必须在其他任何聚合之前要运行。如果你切分图形,你可以选择切分结果是展示成 行还是列的形式,点击 Rows | Columns 选择器即可。 你可以为你的饼图指定以下任意 bucket 聚合: Date Histogram date histogram 基于数值字段创建,由时间组织起来。你可以指定时间 片的间隔,单位包括秒,分,小时,天,星期,月,年。 Histogram 标准 histogram 基于数值字段创建。为这个字段指定一个整数间隔。勾选 Show empty buckets 让直方图中包含空的间隔。 Range 通过 range 聚合。你可以为一个数值字段指定一系列区间。点击 Add Range 添 加一堆区间端点。点击红色 (x) 符号移除一个区间。 Date Range date range 聚合计算你指定的时间区间内的值。你可以使用 date math 表达 式指定区间。点击 Add Range 添加新的区间端点。点击红色 (/) 符号移除区间。 IPv4 Range IPv4 range 聚合用来指定 IPv4 地址的区间。点击 Add Range 添加新的区 间端点。点击红色 (/) 符号移除区间。 Terms terms 聚合允许你指定展示一个字段的首尾几个元素,排序方式可以是计数或者其 他自定义的metric。 Filters 你可以为数据指定一组 filters。你可以用 query string,也可以用 JSON 格式来指 定过滤器,就像在 Discover 页的搜索栏里一样。点击 Add Filter 添加下一个过滤器。 Significant Terms 展示实验性的 significant terms 聚合的结果。 一旦你定义好了一个 X 轴聚合。你可以继续定义子聚合来完善可视化效果。点击 + Add Sub Aggregation 添加子聚合,然后选择 Split Area 或者 Split Chart,然后从类型菜单中选择一 个子聚合。 当一个图形中定义了多个聚合,你可以使用聚合类型右侧的上下箭头来改变聚合的优先级。 你可以点击 Advanced 链接显示更多有关聚合的自定义参数: Exclude Pattern 指定一个从结果集中排除掉的模式。 Exclude Pattern Flags 排除模式的 Java flags 标准集。 ELKstack 中文指南 495各visualize功能 Include Pattern 指定一个从结果集中要包含的模式。 Include Pattern Flags 包含模式的 Java flags 标准集。 JSON Input 一个用来添加 JSON 格式属性的文本框,内容会合并进聚合的定义中,格式 如下例: { "script" : "doc['grade'].value * 1.2" } 注意 Elasticsearch 1.4.3 及以后版本,这个函数需要你开启 dynamic Groovy scripting。 这些参数是否可用,依赖于你选择的聚合函数。 选择 view options 更改表格中如下方面: Donut Display the chart as a sliced ring instead of a sliced pie. Show Tooltip 勾选该项显示工具栏。 Show Legend 勾选该项在图形右侧显示图例。 ELKstack 中文指南 496各visualize功能 瓦片地图 瓦片地图显示一个由圆圈覆盖着的地理区域。这些圆圈则是由你指定的 buckets 控制的。 瓦片地图的默认 metrics 聚合是 Count 聚合。你可以选择下列聚合中的任意一个作为 metrics 聚合: Count count 聚合返回选中索引模式中元素的原始计数。 Average 这个聚合返回一个数值字段的 average 。从下拉菜单选择一个字段。 Sum sum 聚合返回一个数值字段的总和。从下拉菜单选择一个字段。 Min min 聚合返回一个数值字段的最小值。从下拉菜单选择一个字段。 Max max 聚合返回一个数值字段的最大值。从下拉菜单选择一个字段。 Unique Count cardinality 聚合返回一个字段的去重数据值。从下拉菜单选择一个字段。 buckets 聚合指明从你的数据集中将要检索什么信息。 在你选择 buckets 聚合前,指定你是打算分割图形,还是在单个图形上显示 buckets 为 Geo Coordinates。多图切割的聚合必须在最先运行。 瓦片地图使用 Geohash 聚合作为他们的初始化聚合。从下拉菜单中选择一个坐标字 段。Precision 滑动条设置圆圈在地图上显示的颗粒度大小。阅读 geohash grid 聚合的文 档,了解每个精度级别的区域细节。Kibana 4.1 目前支持的最大 geohash 长度为 7。 注意 更高的精度意味着同时消耗了浏览器和 ES 集群更多的内存。 一旦你定义好了一个 X 轴聚合。你可以继续定义子聚合来完善可视化效果。点击 + Add Sub Aggregation 添加子聚合,然后选择 Split Area 或者 Split Chart,然后从类型菜单中选择一 个子聚合。 Date Histogram date histogram 基于数值字段创建,由时间组织起来。你可以指定时间 片的间隔,单位包括秒,分,小时,天,星期,月,年。 Histogram 标准 histogram 基于数值字段创建。为这个字段指定一个整数间隔。勾选 Show empty buckets 让直方图中包含空的间隔。 Range 通过 range 聚合。你可以为一个数值字段指定一系列区间。点击 Add Range 添 加一堆区间端点。点击红色 (x) 符号移除一个区间。 Date Range date range 聚合计算你指定的时间区间内的值。你可以使用 date math 表达 式指定区间。点击 Add Range 添加新的区间端点。点击红色 (/) 符号移除区间。 IPv4 Range IPv4 range 聚合用来指定 IPv4 地址的区间。点击 Add Range 添加新的区 间端点。点击红色 (/) 符号移除区间。 Terms terms 聚合允许你指定展示一个字段的首尾几个元素,排序方式可以是计数或者其 ELKstack 中文指南 497各visualize功能 他自定义的metric。 Filters 你可以为数据指定一组 filters。你可以用 query string,也可以用 JSON 格式来指 定过滤器,就像在 Discover 页的搜索栏里一样。点击 Add Filter 添加下一个过滤器。 Significant Terms 展示实验性的 significant terms 聚合的结果。 Geohash The geohash aggregation displays points based on the geohash coordinates. 你可以点击 Advanced 链接显示更多有关聚合的自定义参数: Exclude Pattern 指定一个从结果集中排除掉的模式。 Exclude Pattern Flags 排除模式的 Java flags 标准集。 Include Pattern 指定一个从结果集中要包含的模式。 Include Pattern Flags 包含模式的 Java flags 标准集。 JSON Input 一个用来添加 JSON 格式属性的文本框,内容会合并进聚合的定义中,格式 如下例: { "script" : "doc['grade'].value * 1.2" } 注意 Elasticsearch 1.4.3 及以后版本,这个函数需要你开启 dynamic Groovy scripting。 这些参数是否可用,依赖于你选择的聚合函数。 选择 Options 改变表格的如下方面: Map type 从下拉框选择下面一个选项。 Shaded Circle Markers 根据 metric 聚合的值显示不同的颜色。 Scaled Circle Markers 根据 metric 聚合的值显示不同的大小。 Shaded Geohash Grid 用矩形替换默认的圆形显示 geohash,根据 metric 聚合的值显示 不同的颜色。 Heatmap 热力图可以模糊化圆标而且层叠显示颜色。热力图本身还有如下选项可用: Radius: 设置单个热力图点的大小。 Blur: 设置热力图点的模糊度。 Maximum zoom: Kibana 的 Tilemap 支持 18 级缩放。该选项设置热力图最大强度下 的最高缩放级别。 Minimum opacity: 设置数据点的不透明截止位置。 Show Tooltip: 勾选该项,让鼠标放在数据点上时显示该点的数据。 Desaturate map tiles ELKstack 中文指南 498各visualize功能 淡化地图颜色,凸显标记的清晰度。 WMS compliant map server 勾选该项,可以配置使用符合 Web Map Service (WMS) 标准的其他第三方地图服务。需要指 定一下参数: WMS url: WMS 地图服务的 URL; WMS layers: 用于可视化的图层列表,逗号分隔。每个地图服务商都会提供自己的图 层。 WMS version: 该服务商采用的 WMS 版本。 WMS format: 该服务商使用的图片格式。通常来说是 image/png 或 image/jpeg。 WMS attribution: 可选项。用户自定义字符串,用来显示在图表右下角的属性说明。 WMS styles: 逗号分隔的风格列表。每个地图服务商都会提供自己的风格选项。 更新选项后,点击绿色 Apply changes 按钮更新你的可视化界面,或者灰色 Discard changes 按钮保持原状。 Navigating the Map 可视化地图就绪后,你可以通过一下方式探索地图: 在地图任意位置点击并按住鼠标后,拖动即可转移地图中心。按住鼠标左键拖拽绘制方 框则可以放大选定区域。 点击 Zoom In/Out 按钮手动修改缩放级别。 点击 Fit Data Bounds 按钮让地图自动聚焦到至少有一个数据点的地区。 点击 Latitude/Longitude Filter 按钮,然后在地图上拖拽绘制一个方框,自动生成 这个框范围的经纬度过滤器。 ELKstack 中文指南 499各visualize功能 竖条图 这个图的 Y 轴是数值维度。该维度有以下聚合可用: Count count 聚合返回选中索引模式中元素的原始计数。 Average 这个聚合返回一个数值字段的 average 。从下拉菜单选择一个字段。 Sum sum 聚合返回一个数值字段的总和。从下拉菜单选择一个字段。 Min min 聚合返回一个数值字段的最小值。从下拉菜单选择一个字段。 Max max 聚合返回一个数值字段的最大值。从下拉菜单选择一个字段。 Unique Count cardinality 聚合返回一个字段的去重数据值。从下拉菜单选择一个字段。 Standard Deviation extended stats 聚合返回一个数值字段数据的标准差。从下拉菜单选 择一个字段。 Percentile percentile 聚合返回一个数值字段中值的百分比分布。从下拉菜单选择一个字 段,然后在 Percentiles 框内指定范围。点击 X 移除一个百分比框,点击 + Add Percent 添加一个百分比框。 你可以点击 + Add Aggregation 按键添加一个聚合。 buckets 聚合指明从你的数据集中将要检索什么信息。 在你选定一个 buckets 聚合之前,先指定你是要切割单个图的分片,还是切割成多个图形。 多图切分必须在其他任何聚合之前要运行。如果你切分图形,你可以选择切分结果是展示成 行还是列的形式,点击 Rows | Columns 选择器即可。 图形的 X 轴是buckets 维度。你可以为 X 轴定义 buckets,同样还可以为图片上的分片区域, 或者分割的图片定义 buckets。 该图形的 X 轴支持以下聚合。点击每个聚合的链接查看该聚合的 Elasticsearch 官方文档。 Date Histogram date histogram 基于数值字段创建,由时间组织起来。你可以指定时间 片的间隔,单位包括秒,分,小时,天,星期,月,年。 Histogram 标准 histogram 基于数值字段创建。为这个字段指定一个整数间隔。勾选 Show empty buckets 让直方图中包含空的间隔。 Range 通过 range 聚合。你可以为一个数值字段指定一系列区间。点击 Add Range 添 加一堆区间端点。点击红色 (x) 符号移除一个区间。 Terms terms 聚合允许你指定展示一个字段的首尾几个元素,排序方式可以是计数或者其 他自定义的metric。 Filters 你可以为数据指定一组 filters。你可以用 query string,也可以用 JSON 格式来指 定过滤器,就像在 Discover 页的搜索栏里一样。点击 Add Filter 添加下一个过滤器。 Significant Terms 展示实验性的 significant terms 聚合的结果。 ELKstack 中文指南 500各visualize功能 一旦你定义好了一个 X 轴聚合。你可以继续定义子聚合来完善可视化效果。点击 + Add Sub Aggregation 添加子聚合,然后选择 Split Area 或者 Split Chart,然后从类型菜单中选择一 个子聚合。 当一个图形中定义了多个聚合,你可以使用聚合类型右侧的上下箭头来改变聚合的优先级。 你可以点击 Advanced 链接显示更多有关聚合的自定义参数: Exclude Pattern 指定一个从结果集中排除掉的模式。 Exclude Pattern Flags 排除模式的 Java flags 标准集。 Include Pattern 指定一个从结果集中要包含的模式。 Include Pattern Flags 包含模式的 Java flags 标准集。 JSON Input 一个用来添加 JSON 格式属性的文本框,内容会合并进聚合的定义中,格式 如下例: { "script" : "doc['grade'].value * 1.2" } 注意 Elasticsearch 1.4.3 及以后版本,这个函数需要你开启 dynamic Groovy scripting。 这些参数是否可用,依赖于你选择的聚合函数。 选择 view options 更改表格中如下方面: Bar Mode 当你为自己的图形定义了多个 Y 轴聚合时,你可以用这个选项决定聚合显示的 方式: stacked 依次堆叠聚合效果。 percentage 每个聚合显示为总和的百分比。 grouped 用最低优先级的子聚合的结果做水平分组。 多选框可以用来控制以下行为: Show Tooltip 勾选该项显示工具栏。 Show Legend 勾选该项在图形右侧显示图例。 Scale Y-Axis to Data Bounds 默认的 Y 轴长度为 0 到数据集的最大值。勾选该项改变 Y 轴的最大和最小值为数据集的返回值。 ELKstack 中文指南 501各visualize功能 一个 Kibana dashboard 能让你自由排列一组已保存的可视化。然后你可以保存这个仪表板, 用来分享或者重载。 简单的仪表板像这样。 开始 要用仪表板,你需要至少有一个已保存的 visualization。 创建一个新的仪表板 你第一次点击 Dashboard 标签的时候,Kibana 会显示一个空白的仪表板 通过添加可视化的方式来构建你的仪表板。默认情况下,Kibana 仪表板使用明亮风格。如果 你想切换成黑色风格点击数 Settings 按钮,然后勾选 Use dark theme。 ELKstack 中文指南 502dashboard功能 自动刷新页面 你可以设置页面自动刷新的间隔以查看最新索引进来的数据。这个设置会定期提交搜索请 求。 设置刷新间隔后,它会出现在菜单栏的时间过滤器左侧。 要设置刷新间隔: 1. 点击菜单栏右上角的 Time Filter 2. 点击 Refresh Interval 标签 3. 从列表中选择一个刷新间隔 要自动刷新数据,点击 Auto-Refresh 按钮选择自动刷新间隔: ELKstack 中文指南 503dashboard功能 开启自动刷新后,Kibana 顶部菜单栏会显示一个暂停按钮和自动刷新间隔: 。 点击这个暂停按钮可以暂停自动刷新。 添加可视化到仪表板上 要添加可视化到仪表板上,点击工具栏面板上的 Add Visualization 按钮。从列表中选择 一个已保存的可视化。你可以在 Visualization Filter 里输入字符串来过滤想要找的可视化。 由你选择的这个可视化会出现在你仪表板上的一个容器(container)里。 如果你觉得容器的高度和宽度不合适,可以调整容器大小。 保存仪表板 要保存仪表板,点击工具栏面板上的 Save Dashboard 按钮,在 Save As 栏输入仪表板的名 字,然后点击 Save 按钮。 加载已保存仪表板 点击 Load Saved Dashboard 按钮显示已存在的仪表板列表。已保存仪表板选择器包括了一 个文本栏可以通过仪表板的名字做过滤,还有一个链接到 Object Editor 而已管理你的已保存 仪表板。你也可以直接点击 Settings > Edit Saved Objects 来访问 Object Editor。 分享仪表板 你可以分享仪表板给其他用户。可以直接分享 Kibana 的仪表板链接,也可以嵌入到你的网页 里。 用户必须有 Kibana 的访问权限才能看到嵌入的仪表板。 点击 Share 按钮显示 HTML 代码,就可以嵌入仪表板到其他网页里。还带有一个指向仪表板 的链接。点击复制按钮 可以复制代码,或者链接到你的黏贴板。 嵌入仪表板 要嵌入仪表板,从 Share 页里复制出嵌入代码,然后粘贴进你外部网页应用内即可。 定制仪表板元素 仪表板里的可视化都存在可以调整大小的容器里。接下来会讨论一下容器。 ELKstack 中文指南 504dashboard功能 移动容器 点击并按住容器的顶部,就可以拖动容器到仪表板任意位置。其他容器会在必要的时候自动 移动,给你在拖动的这个容器空出位置。松开鼠标,容器就会固定在当前停留位置。 改变容器大小 移动光标到容器的右下角,等光标变成指向拐角的方向,点击并按住鼠标,拖动改变容器的 大小。松开鼠标,容器就会固定成当前大小。 删除容器 点击容器右上角的 x 图标删除容器。从仪表板删除容器,并不会同时删除掉容器里用到的已 存可视化。 查看详细信息 要显示可视化背后的原始数据,点击容器地步的条带。可视化会被有关原始数据详细信息的 几个标签替换掉。如下所示: ELKstack 中文指南 505dashboard功能 表格(Table)。底层数据的分页展示。你可以通过点击每列顶部的方式给该列数据排序。 ELKstack 中文指南 506dashboard功能 请求(Request)。发送到服务器的原始请求,以 JSON 格式展示。 ELKstack 中文指南 507dashboard功能 响应(Response)。从服务器返回的原始响应,以 JSON 格式展示。 ELKstack 中文指南 508dashboard功能 统计值(Statistics)。和请求响应相关的一些统计值,以数据网格的方式展示。数据报告,请求 时间,响应时间,返回的记录条目数,匹配请求的索引模式(index pattern)。 修改可视化 点击容器右上角的 Edit 按钮 在 Visualize 页打开可视化编辑。 ELKstack 中文指南 509dashboard功能 要使用 Kibana,你就得告诉它你想要探索的 Elasticsearch 索引是那些,这就要配置一个或者 更多的索引模式。此外,你还可以: 创建脚本化字段,这个字段可以实时从你的数据中计算出来。你可以浏览这种字段,并 且在它基础上做可视化,但是不能搜索这种字段。 设置高级选项,比如表格里显示多少行,常用字段显示多少个。修改高级选项的时候要 千万小心,因为一个设置很可能跟另一个设置是不兼容的。 为生产环境配置 Kibana。 创建一个连接到 Elasticsearch 的索引模式 一个索引模式定义了一个或者多个你打算探索的 Elasticsearch 索引。Kibana 会查找匹配指定 模式的索引名。模式中的通配符()匹配零到多个字符。比如,模式 `myindex- 匹配所有名字 以 myindex- 开头的索引,比如 myindex-1 和 myindex-2`。 如果你用了事件时间来创建索引名(比如说,如果你是用 Logstash 往 Elasticsearch 里写数 据),索引模式里也可以匹配一个日期格式。在这种情况下,模式的静态文本部分必须用中括 号包含起来,日期格式能用的字符,参见表 1 "日期格式码"。 比如, [logstash-]YYYY.MM.DD 匹配所有名字以 logstash- 为前缀,后面跟上 YYYY.MM.DD 格式时间戳的索引,比如 logstash-2015.01.31 和 logstash-2015-02-01 。 索引模式也可以简单的设置为一个单独的索引名字。 要创建一个连接到 Elasticsearch 的索引模式: 1. 切换到 Settings > Indices 标签页。 2. 指定一个能匹配你的 Elasticsearch 索引名的索引模式。默认的,Kibana 会假设你是要处 理 Logstash 导入的数据。 当你在顶层标签页之间切换的时候,Kibana 会记住你之前停留的位置。比如,如果你在 Settings 标签页查看了一个索引模式,然后切换到 Discover 标签,再切换回 Settings 标 签,Kibana 还会显示上次你查看的索引模式。要看到创建模式的表单,需要从索引模式 列表里点击 Add 按钮。 1. 如果你索引有时间戳字段打算用来做基于事件的对比,勾选 Index contains time-based events 然后选择包含了时间戳的索引字段。Kibana 会读取索引映射,列出所有包含了时 间戳的字段供选择。 2. 如果新索引是周期性生成,名字里有时间戳的,勾选 Use event times to create index names 和 Index pattern interval 选项。这会让 Kibana 只搜索哪些包含了你指定的时 间范围内的数据的索引。当你使用 Logstash 往 Elasticsearch 写数据的时候非常有用。 3. 点击 Create 添加索引模式。 4. 要设置新模式作为你查看 Discover 页是的默认模式,点击 favorite 按钮。 ELKstack 中文指南 510setting功能 表 1. 日期格式码 格式 描述 M Month - cardinal: 1 2 3 … 12 Mo Month - ordinal: 1st 2nd 3rd … 12th MM Month - two digit: 01 02 03 … 12 MMM Month - abbreviation: Jan Feb Mar … Dec MMMM Month - full: January February March … December Q Quarter: 1 2 3 4 D Day of Month - cardinal: 1 2 3 … 31 Do Day of Month - ordinal: 1st 2nd 3rd … 31st DD Day of Month - two digit: 01 02 03 … 31 DDD Day of Year - cardinal: 1 2 3 … 365 DDDo Day of Year - ordinal: 1st 2nd 3rd … 365th DDDD Day of Year - three digit: 001 002 … 364 365 d Day of Week - cardinal: 0 1 3 … 6 do Day of Week - ordinal: 0th 1st 2nd … 6th dd Day of Week - 2-letter abbreviation: Su Mo Tu … Sa ddd Day of Week - 3-letter abbreviation: Sun Mon Tue … Sat dddd Day of Week - full: Sunday Monday Tuesday … Saturday e Day of Week (locale): 0 1 2 … 6 E Day of Week (ISO): 1 2 3 … 7 w Week of Year - cardinal (locale): 1 2 3 … 53 wo Week of Year - ordinal (locale): 1st 2nd 3rd … 53rd ww Week of Year - 2-digit (locale): 01 02 03 … 53 W Week of Year - cardinal (ISO): 1 2 3 … 53 Wo Week of Year - ordinal (ISO): 1st 2nd 3rd … 53rd WW Week of Year - two-digit (ISO): 01 02 03 … 53 YY Year - two digit: 70 71 72 … 30 YYYY Year - four digit: 1970 1971 1972 … 2030 gg Week Year - two digit (locale): 70 71 72 … 30 gggg Week Year - four digit (locale): 1970 1971 1972 … 2030 ELKstack 中文指南 511setting功能 GG Week Year - two digit (ISO): 70 71 72 … 30 GGGG Week Year - four digit (ISO): 1970 1971 1972 … 2030 A AM/PM: AM PM a am/pm: am pm H Hour: 0 1 2 … 23 HH Hour - two digit: 00 01 02 … 23 h Hour - 12-hour clock: 1 2 3 … 12 hh Hour - 12-hour clock, 2 digit: 01 02 03 … 12 m Minute: 0 1 2 … 59 mm Minute - two-digit: 00 01 02 … 59 s Second: 0 1 2 … 59 ss Second - two-digit: 00 01 02 … 59 S Fractional Second - 10ths: 0 1 2 … 9 SS Fractional Second - 100ths: 0 1 … 98 99 SSS Fractional Seconds - 1000ths: 0 1 … 998 999 Z Timezone - zero UTC offset (hh:mm format): -07:00 -06:00 -05:00 .. +07:00 ZZ Timezone - zero UTC offset (hhmm format): -0700 -0600 -0500 … +0700 X Unix Timestamp: 1360013296 x Unix Millisecond Timestamp: 1360013296123 设置默认索引模式 默认索引模式会在你查看 Discover 标签的时候自动加载。Kibana 会在 Settings > Indices 标签页的索引模式列表里,给默认模式左边显示一个星号。你创建的第一个模式会自动被设 置为默认模式。 要设置一个另外的模式为默认索引模式: 1. 进入 Settings > Indices 标签页。 2. 在索引模式列表里选择你打算设置为默认值的模式。 3. 点击模式的 Favorite 标签。 你也可以在 Advanced > Settings 里设置默认索引模式。 重加载索引的字段列表 ELKstack 中文指南 512setting功能 当你添加了一个索引映射,Kibana 自动扫描匹配模式的索引以显示索引字段。你可以重加载 索引字段列表,以显示新添加的字段。 重加载索引字段列表,也会重设 Kibana 的常用字段计数器。这个计数器是跟踪你在 Kibana 里常用字段,然后来排序字段列表的。 要重加载索引的字段列表: 1. 进入 Settings > Indices 标签页。 2. 在索引模式列表里选择一个索引模式。 3. 点击模式的 Reload 按钮。 删除一个索引模式 要删除一个索引模式: 1. 进入 Settings > Indices 标签页。 2. 在索引模式列表里选择你打算删除的模式。 3. 点击模式的 Delete 按钮。 4. 确认你是想要删除这个索引模式。 创建一个脚本化字段 脚本化字段从你的 Elasticsearch 索引数据中即时计算得来。在 Discover 标签页,脚本化字 段数据会作为文档数据的一部分显示,而且你还可以在可视化里使用脚本化字段。(脚本化字 段的值是在请求的时候计算的,所以它们没有被索引,不能搜索到) 即时计算脚本化字段非常消耗资源,会直接影响到 Kibana 的性能。而且记住, Elasticsearch 里没有内置对脚本化字段的验证功能。如果你的脚本有 bug,你会在查看 动态生成的数据时看到 exception。 脚本化字段使用 Lucene 表达式语法。更多细节,请阅读 Lucene Expressions Scripts。 你可以在表达式里引用任意单个数值类型字段,比如: doc['field_name'].value 要创建一个脚本化字段: 1. 进入 Settings > Indices 2. 选择你打算添加脚本化字段的索引模式。 3. 进入模式的 Scripted Fields 标签。 ELKstack 中文指南 513setting功能 4. 点击 Add Scripted Field 。 5. 输入脚本化字段的名字。 6. 输入用来即时计算数据的表达式。 7. 点击 Save Scripted Field . 有关 Elasticsearch 的脚本化字段的更多细节,阅读 Scripting。 更新一个脚本化字段 要更新一个脚本化字段: 1. 进入 Settings > Indices 。 2. 点击你要更新的脚本化字段的 Edit 按钮。 3. 完成变更后点击 Save Scripted Field 升级。 注意 Elasticsearch 里没有内置对脚本化字段的验证功能。如果你的脚本有 bug,你会在 查看动态生成的数据时看到 exception。 删除一个脚本化字段 要删除一个脚本化字段: 1. 进入 Settings > Indices 。 2. 点击你要删除的脚本化字段的 Delete 按钮。 3. 确认你确实想删除它。 设置高级参数 高级参数页允许你直接编辑那些控制着 Kibana 应用行为的设置。比如,你可以修改显示日期 的格式,修改默认的索引模式,设置十进制数值的显示精度。 修改高级参数可能带来意想不到的后果。如果你不确定自己在做什么,最好离开这个设 置页面。 要设置高级参数: 1. 进入 Settings > Advanced 。 2. 点击你要修改的选项的 Edit 按钮。 3. 给这个选项输入一个新的值。 4. 点击 Save 按钮。 ELKstack 中文指南 514setting功能 管理已保存的搜索,可视化和仪表板 你可以从 Settings > Objects 查看,编辑,和删除已保存的搜索,可视化和仪表板。 查看一个已保存的对象会显示在 Discover, Visualize 或 Dashboard 页里已选择的项目。要 查看一个已保存对象: 1. 进入 Settings > Objects 。 2. 选择你想查看的对象。 3. 点击 View 按钮。 编辑一个已保存对象让你可以直接修改对象定义。你可以修改对象的名字,添加一段说明, 以及修改定义这个对象的属性的 JSON。 如果你尝试访问一个对象,而它关联的索引已经被删除了,Kibana 会显示这个对象的编辑 (Edit Object)页。你可以: 重建索引这样就可以继续用这个对象。 删除对象,然后用另一个索引重建对象。 在对象的 kibanaSavedObjectMeta.searchSourceJSON 里修改引用的索引名,指向一个还存 在的索引模式。这个在你的索引被重命名了的情况下非常有用。 对象属性没有验证机制。提交一个无效的变更会导致对象不可用。通常来说,你还是应 该用 Discover, Visualize 或 Dashboard 页面来创建新对象而不是直接编辑已存在的对 象。 要编辑一个已保存的对象: 1. 进入 Settings > Objects 。 2. 选择你想编辑的对象。 3. 点击 Edit 按钮。 4. 修改对象定义。 5. 点击 Save Object 按钮。 要删除一个已保存的对象: 1. 进入 Settings > Objects 。 2. 选择你想删除的对象。 3. 点击 Delete 按钮。 4. 确认你确实想删除这个对象。 设置 kibana 服务器属性 ELKstack 中文指南 515setting功能 Kibana 服务器在启动的时候会从 kibana.yml 文件读取属性设置。默认设置是运行在 localhost:5601 。要变更主机或端口,或者连接远端主机上的 Elasticsearch,你都需要更新 你的 kibana.yml 文件。你还可以开启 SSL 或者设置其他一系列选项: 表 2. Kibana 服务器属性 属性 描述 port Kibana 服务器运行的端口。默认: port: 5601 。 host Kibana 服务器监听的地址。默认: host: "0.0.0.0" 。 elasticsearch_url 你想请求的索引存在哪个 Elasticsearch 实例上。默 认: elasticsearch_url: "http://localhost:9200" 。 elasticsearch_preserve_host 默认的,浏览器请求中的主机名即作为 Kibana 发送给 Elasticsearch 时请求的主机名。如果你设置这个参数为 false , Kibana 会改用 elasticsearch_url 里的主机 名。你应该不用担心这个设置 —— 直接用默认即可。默 认: elasticsearch_preserve_host: true 。 kibana_index 保存搜索,可视化,仪表板信息的索引的名字。默 认: kibana_index: .kibana 。 default_app_id 进入 Kibana 是默认显示的页面。可以为 discover , visualize , dashboard 或 settings 。默 认: default_app_id: "discover" 。 request_timeout 等待 Kibana 后端或 Elasticsearch 的响应的超时时间, 单位毫秒。默认: request_timeout: 500000 。 shard_timeout Elasticsearch 等待分片响应的超时时间。设置为 0 表示 关闭超时控制。默认: shard_timeout: 0 。 verify_ssl 定义是否验证 Elasticsearch SSL 证书。设置为 false 关 闭 SSL 认证。默认: verify_ssl: true 。 ca 你的 Elasticsearch 实例的 CA 证书的路径。如果你是自 己签的证书,必须指定这个参数,证书才能被认证。否 则,你需要关闭 verify_ssl 。默认:none。 ssl_key_file Kibana 服务器的密钥文件路径。设置用来加密浏览器和 Kibana 之间的通信。默认:none。 ssl_cert_file Kibana 服务器的证书文件路径。设置用来加密浏览器和 Kibana 之间的通信。默认:none。 pid_file 你想用来存进程 ID 文件的位置。如果没有指定,PID 文 件存在 /var/run/kibana.pid 。默认:none。 ELKstack 中文指南 516setting功能 常见 sub aggs 示例 本章开始,就提到 K4 和 K3 的区别,在 K4 中,即便介绍完了全部 visualize 的配置项,也不 代表用户能立刻上手配置出来和 K3 一样的面板。所以本节,会以几个具体的日志分析需求为 例,演示在 K4 中,利用 Elasticsearch 1.0 以后提供的 Aggregation 特性,能够做到哪些有用 的可视化效果。 ELKstack 中文指南 517常用sub agg示例 函数调用链堆栈 本书之前已经介绍过 logstash 如何利用 multiline 或者 log4j 插件解析函数堆栈。那么,对函 数堆栈,我们除了对底层函数做基础的 topN 排序,还能深入发掘出来什么信息呢? 下图是一个 PHP 慢函数堆栈的可视化统计: 该图利用了 Kibana4 的 sub aggs 特性。按照分层次的函数堆栈,逐层做 terms agg。得到一 个类似火焰图效果的千层饼效果。 和火焰图不同的是,千层饼并不能自动深入到函数堆栈的全部层次,需要自己手动指定聚合 到第几层。考虑到重复操作在页面上不是很方便。可以利用 Kibana4 的 url 特性,直接修改地 址生成效果。上图的 url 如下: http://k4domain:5601/#/visualize/edit/php-slow-stack-pie?_g=()&_a=(filters:!(),linked:!t,query:(query_string:(query:'*')),vis:(aggs:!((id:'1',params:(),schema:metric,type:count),(id:'2',params:(field:slow.1,order:desc,orderBy:'1',size:10),schema:segment,type:terms),(id:'3',params:(field:slow.2,order:desc,orderBy:'1',size:10),schema:segment,type:terms),(id:'4',params:(field:slow.3,order:desc,orderBy:'1',size:10),schema:segment,type:terms),(id:'5',params:(field:slow.4,order:desc,orderBy:'1',size:10),schema:segment,type:terms)),listeners:(),params:(addLegend:!f,addTooltip:!t,defaultYExtents:!f,isDonut:!t,shareYAxis:!t,spyPerPage:10),type:pie)) 可以看到,如果打算增减堆栈的聚合层次,对应增减一段 (id:'5',params: (field:slow.4,order:desc,orderBy:'1',size:10), 就可以了。 作为固定可视化分析模式的另一种分享办法,还可以导出该 visualize object 在 .kibana 索引 中的 JSON 记录。这样其他人只需要原样再导入到自己的 .kibana 索引即可: ELKstack 中文指南 518常用sub agg示例 # curl 127.0.0.1:9200/.kibana/visualization/php-slow-stack-pie/_source {"title":"php-slow-stack-pie","visState":"{\"aggs\":[{\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"id\":\"2\",\"params\":{\"field\":\"slow.1\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":10},\"schema\":\"segment\",\"type\":\"terms\"},{\"id\":\"3\",\"params\":{\"field\":\"slow.2\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":10},\"schema\":\"segment\",\"type\":\"terms\"},{\"id\":\"4\",\"params\":{\"field\":\"slow.3\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":10},\"schema\":\"segment\",\"type\":\"terms\"},{\"id\":\"5\",\"params\":{\"field\":\"slow.4\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":10},\"schema\":\"segment\",\"type\":\"terms\"}],\"listeners\":{},\"params\":{\"addLegend\":false,\"addTooltip\":true,\"defaultYExtents\":false,\"isDonut\":true,\"shareYAxis\":true,\"spyPerPage\":10},\"type\":\"pie\"}","description":"","savedSearchId":"php-fpm-slowlog","version":1,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[]}"}} 上面记录中可以看到,这个 visualize 还关联了一个 savedSearch,那么同样,再从 .kibana 索引里把这个内容也导出: # curl 127.0.0.1:9200/.kibana/search/php-fpm-slowlog/_source {"title":"php-fpm-slowlog","description":"","hits":0,"columns":["_source"],"sort":["@timestamp","desc"],"version":1,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\n \"index\": \"[logstash-mweibo-]YYYY.MM.DD\",\n \"highlight\": {\n \"pre_tags\": [\n \"@kibana-highlighted-field@\"\n ],\n \"post_tags\": [\n \"@/kibana-highlighted-field@\"\n ],\n \"fields\": {\n \"*\": {}\n }\n },\n \"filter\": [\n {\n \"meta\": {\n \"index\": \"[logstash-mweibo-]YYYY.MM.DD\",\n \"negate\": false,\n \"key\": \"_type\",\n \"value\": \"php-fpm-slow\",\n \"disabled\": false\n },\n \"query\": {\n \"match\": {\n \"_type\": {\n \"query\": \"php-fpm-slow\",\n \"type\": \"phrase\"\n }\n }\n }\n }\n ],\n \"query\": {\n \"query_string\": {\n \"query\": \"*\",\n \"analyze_wildcard\": true\n }\n }\n}"}} 这个内容看起来有点怪怪的,其实把 searchSourceJSON 字符串复制出来,在终端下贴到 echo -ne 命令后面,回车即可看到其实是这样: ELKstack 中文指南 519常用sub agg示例 { "index": "[logstash-mweibo-]YYYY.MM.DD", "highlight": { "pre_tags": [ "@kibana-highlighted-field@" ], "post_tags": [ "@/kibana-highlighted-field@" ], "fields": { "*": {} } }, "filter": [ { "meta": { "index": "[logstash-mweibo-]YYYY.MM.DD", "negate": false, "key": "_type", "value": "php-fpm-slow", "disabled": false }, "query": { "match": { "_type": { "query": "php-fpm-slow", "type": "phrase" } } } } ], "query": { "query_string": { "query": "*", "analyze_wildcard": true } } } ELKstack 中文指南 520常用sub agg示例 分图统计 上一节我们展示了 sub aggs 在饼图上的效果。不过这多层 agg,其实用的是同一类数据。如 果在聚合中,要加上一些完全不同纬度的数据,还是在单一的图片上继续累加就不是很直观 了。比如说,还是上一节用到的 PHP 慢函数堆栈。我们可以根据机房做一下拆分。由于代码 部署等主动变更都是有灰度部署的,一旦发现某机房有异常,就可以及时处理了。 同样还是新建 sub aggs,但是在开始,选择 split chart 而不是 split slice。 从 URL 里可以看到,分图的 aggs 是 schema:split ,而饼图分片的 aggs 是 schema:segment : http://k4domain:5601/#/visualize/edit/php-slow-stack-pie?_g=(refreshInterval:(display:Off,pause:!f,section:0,value:0),time:(from:now-12h,mode:quick,to:now))&_a=(filters:!(),linked:!t,query:(query_string:(query:'*')),vis:(aggs:!((id:'1',params:(),schema:metric,type:count),(id:'6',params:(field:idc,order:desc,orderBy:'1',row:!f,size:5),schema:split,type:terms),(id:'2',params:(field:slow.1,order:desc,orderBy:'1',size:10),schema:segment,type:terms),(id:'3',params:(field:slow.2,order:desc,orderBy:'1',size:10),schema:segment,type:terms),(id:'4',params:(field:slow.3,order:desc,orderBy:'1',size:10),schema:segment,type:terms),(id:'5',params:(field:slow.4,order:desc,orderBy:'1',size:10),schema:segment,type:terms)),listeners:(),params:(addLegend:!f,addTooltip:!t,defaultYExtents:!f,isDonut:!t,shareYAxis:!t,spyPerPage:10),type:pie)) ELKstack 中文指南 521常用sub agg示例 TopN 的时序趋势图 TopN 的时序趋势图是将 ELK Stack 用于监控场景最常用的手段。乃至在 Kibana3 时代,开 发者都通过在 Query 框上额外定义 TopN 输入的方式提供了这个特性。不过在 Kibana4 中, 因为 sub aggs 的依次分桶原理,TopN 时序趋势图又有了新的特点。 Kibana3 中,请求实质是先单独发起一次 termFacet 请求得到 topN,然后再发起带有 facetFilter 的 dateHistogramFacet ,分别请求每个 term 的时序。那么同样的原理,迁移到 Kibana4 中,就是先利用一次 termAgg 分桶后,再每个桶内做 dateHistogramAgg 了。对应 的 Kibana4 地址为: http://k4domain:5601/#/visualize/edit/php-fpm-slowlog-histogram?_g=(refreshInterval:(display:Off,pause:!f,section:0,value:0),time:(from:now-12h,mode:quick,to:now))&_a=(filters:!(),linked:!t,query:(query_string:(query:'*')),vis:(aggs:!((id:'1',params:(),schema:metric,type:count),(id:'3',params:(field:host,order:desc,orderBy:'1',size:3),schema:group,type:terms),(id:'2',params:(customInterval:'2h',extended_bounds:(),field:'@timestamp',interval:auto,min_doc_count:1),schema:segment,type:date_histogram)),listeners:(),params:(addLegend:!t,addTimeMarker:!f,addTooltip:!t,defaultYExtents:!t,interpolate:linear,mode:stacked,scale:linear,setYExtents:!f,shareYAxis:!t,smoothLines:!f,times:!(),yAxis:()),type:area)) 效果如下: 可以看到图上就是 3 根线,分别代表 top3 的 host 的时序。 一般来说,这样都是够用的。不过如果经常有 host 变动的时候,在这么大的一个时间范围 内,求一次总的 topN,可能就淹没了一些瞬间的变动了。所以,在 Kibana4 上,我们可以把 sub aggs 的顺序颠倒一下。先按 dateHistogramAgg 分桶,再在每个时间桶内,做 termAgg。对应的 Kibana4 地址为: http://k4domain:5601/#/visualize/edit/php-fpm-slowlog-histogram?_g=(refreshInterval:(display:Off,pause:!f,section:0,value:0),time:(from:now-12h,mode:quick,to:now))&_a=(filters:!(),linked:!t,query:(query_string:(query:'*')),vis:(aggs:!((id:'1',params:(),schema:metric,type:count),(id:'2',params:(customInterval:'2h',extended_bounds:(),field:'@timestamp',interval:auto,min_doc_count:1),schema:segment,type:date_histogram),(id:'3',params:(field:host,order:desc,orderBy:'1',size:3),schema:group,type:terms)),listeners:(),params:(addLegend:!t,addTimeMarker:!f,addTooltip:!t,defaultYExtents:!t,interpolate:linear,mode:stacked,scale:linear,setYExtents:!f,shareYAxis:!t,smoothLines:!f,times:!(),yAxis:()),type:area)) ELKstack 中文指南 522常用sub agg示例 可以对比一下前一条 url,其中就是把 id 为 2 和 3 的两段做了对调。而最终效果如下: 差距多么明显! ELKstack 中文指南 523常用sub agg示例 响应时间的百分占比趋势图 时序图除了上节展示的最基本的计数以外,还可以在 Y 轴上使用其他数值统计结果。最常见 的,比如访问日志的平均响应时间。但是平均值在数学统计中,是一个非常不可信的数据。 稍微几个远离置信区间的数值就可以严重影响到平均值。所以,在评价数值的总体分布情况 时,更推荐采用四分位数。也就是 25%,50%,75%。在可视化方面,一般采用箱体图方 式。 Kibana4 没有箱体图的可视化方式。不过采用线图,我们一样可以做到类似的效果。 在上一章的时序数据基础上,改变 Y 轴数据源,选择 Percentile 方式,然后输入具体的四分 位。运行渲染即可。 对比新的 URL,可以发现变化的就是 id 为 1 的片段。变成了 (id:'1',params: (field:connect_ms,percents:!(50,75,95,99)),schema:metric,type:percentiles) : http://k4domain:5601/#/visualize/create?type=area&savedSearchId=curldebug&_g=()&_a=(filters:!(),linked:!t,query:(query_string:(query:'*')),vis:(aggs:!((id:'1',params:(field:connect_ms,percents:!(50,75,95,99)),schema:metric,type:percentiles),(id:'2',params:(customInterval:'2h',extended_bounds:(),field:'@timestamp',interval:auto,min_doc_count:1),schema:segment,type:date_histogram)),listeners:(),params:(addLegend:!f,addTimeMarker:!f,addTooltip:!t,defaultYExtents:!f,interpolate:linear,mode:stacked,scale:linear,setYExtents:!f,shareYAxis:!t,smoothLines:!t,times:!(),yAxis:()),type:area)) 实践表明,在访问日志数据上,平均数一般相近于 75% 的四分位数。所以,为了更细化性能 情况,我们可以改用诸如 90%,95% 这样的百分位。 此外,从 Kibana4.1 开始,新加入了 Percentile_rank 聚合支持。可以在 Y 轴数据源里选择这 种聚合,输入具体的响应时间,比如 2s。则可视化数据变成 2s 内完成的响应数占总数的百 分比的趋势图。 ELKstack 中文指南 524常用sub agg示例 响应时间的概率分布在不同时段的相似度对比 前面已经用百分位的时序,展示如何更准确的监控时序数据的波动。那么,还能不能更进一 步呢?在制定 SLA 的时候,制定报警阈值的时候,怎么才能确定当前服务的拐点?除了压测 以外,我们还可以拿线上服务的实际数据计算一下概率分布。Kibana4 对此提供了直接的支 持,我们可以以数值而非时间作为 X 轴数据。 那么进一步,我们怎么区分不同产品在同一时间,或者相同产品在不同时间,性能上有无渐 变到质变的可能?这里,我们可以采用 grouped 方式,来排列 filter aggs 的结果: 我们可以看出来,虽然两个时间段,响应时间是有一定差距的,但是是整体性的抬升,没有 明显的异变。 当然,如果觉得目测不靠谱的,可以把两组数值拿下来,通过 PDL、scipy、matlab、R 等工 具做具体的差异显著性检测。这就属于后续的二次开发了。 filter 中,可以写任意的 query string 语法。不限于本例中的时间段: http://k4domain:5601/#/visualize/create?type=histogram&indexPattern=%5Blogstash-mweibo-nginx-%5DYYYY.MM.DD&_g=()&_a=(filters:!(),linked:!f,query:(query_string:(analyze_wildcard:!t,query:'request_time:%3C50')),vis:(aggs:!((id:'1',params:(),schema:metric,type:count),(id:'3',params:(filters:!((input:(query:(query_string:(analyze_wildcard:!t,query:'@timestamp:%5B%22now-7m%22%20TO%20%22now%22%5D')))),(input:(query:(query_string:(analyze_wildcard:!t,query:'@timestamp:%5B%22now-15m%22%20TO%20%22now-7m%22%7D')))))),schema:group,type:filters),(id:'2',params:(extended_bounds:(),field:request_time,interval:1),schema:segment,type:histogram)),listeners:(),params:(addLegend:!t,addTimeMarker:!f,addTooltip:!t,defaultYExtents:!f,mode:grouped,scale:linear,setYExtents:!f,shareYAxis:!t,spyPerPage:10,times:!(),yAxis:()),type:histogram)) ELKstack 中文指南 525常用sub agg示例 源码剖析 Kibana 4 采用 angular.js + node.js 框架编写。其中 node.js 主要提供两部分功能,给 Elasticsearch 做搜索请求转发代理,以及 auth、ssl、setting 等操作的服务器后端。 本章节假设你已经对 angular 有一定程度了解 —— 至少是阅读并理解了 kibana 3 源码剖析章 节内容的程度。所以不会再解释其中 angular 的 route,controller,directive,service, factory 等概念。 如果打算迁移 kibana 3 的 CAS 验证功能到 kibana 4,那么可以稍微了解一下 index.js , app.js , lib/auth.js 里的 htpasswd 简单实现,相信可以很快修改成功。本章主要还是集 中在前端 kibana 页面功能的实现上。 在 Elastic{ON} 大会上,也有专门针对 Kibana 4 源码和二次开发入门的演讲。请参 阅:https://speakerdeck.com/elastic/the-contributors-guide-to-the-kibana-galaxy 另外可以看专业的前端工程师怎么看Kibana4的代码 的:http://www.debuggerstepthrough.com/2015/04/reviewing-kibana-4s-client-side- code.html。 ELKstack 中文指南 526k4源码解析 kibana_index 结构 包括有以下 type: config _id 为 kibana4 的 version。内容主要是 defaultIndex,设置默认的 index_pattern. search _id 为 discover 上保存的搜索名称。内容主要是 title,column,sort 和 kibanaSavedObjectMeta。kibanaSavedObjectMeta 内是一个 searchSourceJSON,保存搜 索 json 的字符串。 visualization _id 为 visualize 上保存的可视化名称。内容包括 title,savedSearchId, kibanaSavedObjectMeta 和 visState。其中 visState 里保存了 聚合 json 的字符串。如果绑定 了已保存的搜索,那么把其在 search 类型里的 _id 存在 savedSearchId 字段里,如果是从新 搜索开始的,那么把搜索 json 的字符串直接存在自己的 kibanaSavedObjectMeta 的 searchSourceJSON 里。 dashboard _id 为 dashboard 上保存的仪表盘名称。内容包括 title, panelsJSON 和 kibanaSavedObjectMeta。其中 panelsJSON 是一个数组,每个元素是一个 panel 的属性定 义。定义包括有: type: 具体加载的 app 类型,就默认来说,肯定就是 search 或者 visualization 之一。 id: 具体加载的 app 的保存 id。也就是上面说过的,它们在各自类型下的 _id 内容。 size_x: panel 的 X 轴长度。Kibana 4 采用 gridster 库做挂件的动态划分,默认为 3。 size_y: panel 的 Y 轴长度。默认为 2。 col: panel 的左边侧起始位置。Kibana 4 指定 col 最大为 12。每行第一个 panel 的 col 就 是 1,假如它的 size_x 是 4,那么第二个 panel 的 col 就是 5。 row: panel 位于第几行。gridster 默认的 row 最大为 15。 ELKstack 中文指南 527k4源码解析 index-pattern _id 为 setting 中设置的 index pattern。内容主要是匹配该模式的所有索引的全部字段与字段 映射。如果是基于时间的索引模式,还会有主时间字段 timeFieldName 和时间间隔 intervalName 两个字段。 field 数组中,每个元素是一个字段的情况,包括字段的 type, name, indexed, analyzed, doc_values, count, scripted 这些状态。 如果 scripted 为 true,那么这个元素就是通过 kibana4 页面添加的脚本化字段,那么这条字 段记录还会额外多几个内容: script: 记录实际 script 语句。 lang: 在 Elasticsearch 的 datanode 上采用什么 lang-plugin 运行。默认是 expression。 即 ES 1.4.4 开始默认启用的 Lucene expression。在目前的 kibana4 页面上,不提供对 这个的修改,所以统一都是这个值。 type: 因为 Lucene expression 目前只支持对数值型字段做操作,所以目前 kibana4 页面 上也不提供对这个的修改,直接默认为 “number"。 对确认要使用其他 lang-plugin 的,目前来说,可以自行修改 kibana_index 里的 index- pattern 类型中的数据,修改成 ”lang”: “groovy”,”type”:”string” 即可。页面上是可以 通用的。 小贴士 在本书之前介绍 packetbeat 时提到的自带 dashboard 导入脚本,其实就是通过 curl 命令上 传这些 JSON 到 kibana_index 索引里。 ELKstack 中文指南 528k4源码解析 主页入口 kibana 4 主页入口,分析方法跟 kibana 3 一样,看 index.html 和 require.config.js 即可。由 此可以看到,首先进入的,应该是 index.js。主要分为两步。 第一步,route 设置 define(function (require) { var angular = require('angular'); var _ = require('lodash'); var $ = require('jquery'); var modules = require('modules'); var routes = require('routes'); var configFile = JSON.parse(require('text!config')); var kibana = modules.get('kibana', [ 'elasticsearch', 'pasvaz.bindonce', 'ngRoute', 'ngClipboard' ]); kibana .constant('kbnVersion', window.KIBANA_VERSION) .constant('minimumElasticsearchVersion', '1.6.0') .constant('sessionId', Date.now()) .config(routes.config); routes .otherwise({ redirectTo: '/' + configFile.default_app_id }); index.js 根据 configFile 设置默认 routes,设置要求的 Elasticsearch 版本; 设置 routes 的具体操作在加载的 utils/routes/index.js 文件里,其中会调用 utils/routes/_setup.js ,在未设置 default index pattern 的时候跳转 URL 到 "/settings/indices" 页面。 ELKstack 中文指南 529k4源码解析 handleKnownError: function (err) { if (err instanceof NoDefaultIndexPattern || err instanceof NoDefinedIndexPatterns) { kbnUrl.change('/settings/indices'); } else { return Promise.reject(err); } } 此外, utils/routes/index.js 还会加载 components/setup/setup.js 完成整个环境的检查和 启动(在 Kibana4.0 时,是在第二步 kibana plugin 加载的,4.1 开始往前挪到这里)。setup 过 程包括: var checkForEs = Private(require('components/setup/steps/check_for_es')); var checkEsVersion = Private(require('components/setup/steps/check_es_version')); var checkForKibanaIndex = Private(require('components/setup/steps/check_for_kibana_index')); var createKibanaIndex = Private(require('components/setup/steps/create_kibana_index')); return _.once(function () { return checkForEs() .then(checkEsVersion) .then(checkForKibanaIndex) .then(function (exists) { if (!exists) return createKibanaIndex(); }) }); 也就是依次调用 components/setup/steps/ 下的 check_for_es , check_es_version , check_for_kibana_index ,如果没有 kibana index,再调用一个 create_kibana_index 。完 成。 第二步,kibana 插件加载 kibana.load = _.onceWithCb(function (cb) { var firstLoad = [ 'plugins/kibana/index' ]; var thenLoad = _.difference(configFile.plugins, firstLoad); require(firstLoad, function loadApps() { require(thenLoad, cb); }); }); 执行 kibana.load() 函数,先加载 plugins/kibana/index.js ,然后加载 configFile 里定义的 其他 plugins。 ELKstack 中文指南 530k4源码解析 plugins/kibana/index.js 里又有一系列操作: 1. 加载 components/courier/courier.js 和 components/config/config.js 两个 angular.service; 2. 加载 plugins/kibana/_init , plugins/kibana/_apps , plugins/kibana/_timepicker 。 components/config/config.js 主要是从 kibana index 里的 "config" type 中读取 "kbnVersion" id 的数据。这个 "kbnVersion" 就是之前 index.js 里的加载的第一个常量,在 grunt build 编译 时会自动生成。 plugins/kibana/_init 里监听 application.load 事件,触发 courier.start() 函数。 plugins/kibana/_timepicker 提供时间选择器页面。 courier 概述 components/courier/courier.js 中定义了 Courier 类。Courier 是一个非常重要的东西,可 以简单理解为 kibana 跟 ES 之间的一个 object mapper。简要的说,包括一下功能: function Courier() { var self = this; var DocSource = Private(require('components/courier/data_source/doc_source')); var SearchSource = Private(require('components/courier/data_source/search_source')); var searchStrategy = Private(require('components/courier/fetch/strategy/search')); var requestQueue = Private(require('components/courier/_request_queue')); var fetch = Private(require('components/courier/fetch/fetch')); var docLooper = self.docLooper = Private(require('components/courier/looper/doc')); var searchLooper = self.searchLooper = Private(require('components/courier/looper/search')); self.setRootSearchSource = Private(require('components/courier/data_source/_root_search_source')).set; self.SavedObject = Private(require('components/courier/saved_object/saved_object')); self.indexPatterns = indexPatterns; self.redirectWhenMissing = Private(require('components/courier/_redirect_when_missing')); self.DocSource = DocSource; self.SearchSource = SearchSource; self.start = function () { searchLooper.start(); docLooper.start(); return this; }; self.fetch = function () { fetch.fetchQueued(searchStrategy).then(function () { searchLooper.restart(); }); }; self.started = function () { return searchLooper.started(); }; self.stop = function () { ELKstack 中文指南 531k4源码解析 searchLooper.stop(); return this; }; self.createSource = function (type) { switch (type) { case 'doc': return new DocSource(); case 'search': return new SearchSource(); } }; self.close = function () { searchLooper.stop(); docLooper.stop(); _.invoke(requestQueue, 'abort'); if (requestQueue.length) { throw new Error('Aborting all pending requests failed.'); } }; 从类的方法中可以看出,其实主要就是五个属性的控制: DocSource 和 SearchSource:继承自 components/courier/data_source/_abstract.js , 调用 components/courier/data_source/data_source/_doc_send_to_es.js 完成跟 ES 数据的 交互,用来做 savedObject 和 index_pattern 的读写: es[method](params) .then(function (resp) { if (resp.status === 409) throw new errors.VersionConflict(resp); doc._storeVersion(resp._version); doc.id(resp._id); var docFetchProm; if (method !== 'index') { docFetchProm = doc.fetch(); } else { // we already know what the response will be docFetchProm = Promise.resolve({ _id: resp._id, _index: params.index, _source: body, _type: params.type, _version: doc._getVersion(), found: true }); } ELKstack 中文指南 532k4源码解析 这个 es 在是调用了 services/es.js 里定义的 service,里面内容超级简单,就是加载官方的 elasticsearch.js 库,然后初始化一个最简的 esFactory 客户端,包括超时都设成了 0,把这个 控制交给 server 端。 define(function (require) { require('elasticsearch'); var _ = require('lodash'); var es; // share the client amoungst all apps require('modules') .get('kibana', ['elasticsearch', 'kibana/config']) .service('es', function (esFactory, configFile, $q) { if (es) return es; es = esFactory({ host: configFile.elasticsearch, log: 'info', requestTimeout: 0, apiVersion: '1.4' }); return es; }); }); searchLooper 和 docLooper:分别给 Looper.start 方法传递 searchStrategy 和 docStrategy,对应 ES 的 /_msearch 和 /_mget 请求。searchLooper 的实现如下: var fetch = Private(require('components/courier/fetch/fetch')); var searchStrategy = Private(require('components/courier/fetch/strategy/search')); var requestQueue = Private(require('components/courier/_request_queue')); var Looper = Private(require('components/courier/looper/_looper')); var searchLooper = new Looper(null, function () { $rootScope.$broadcast('courier:searchRefresh'); return fetch.these( requestQueue.getInactive(searchStrategy) ); }); 这里的关键方法是 fetch.these() ,出自 components/courier/fetch/_fetch_these.js ,其中 调用的 components/courier/fetch/_call_client.js 有如下一段代码: ELKstack 中文指南 533k4源码解析 Promise.map(executable, function (req) { return Promise.try(req.getFetchParams, void 0, req) .then(function (fetchParams) { return (req.fetchParams = fetchParams); }); }) .then(function (reqsFetchParams) { return strategy.reqsFetchParamsToBody(reqsFetchParams); }) .then(function (body) { return (esPromise = es[strategy.clientMethod]({ timeout: configFile.shard_timeout, ignore_unavailable: true, preference: sessionId, body: body })); }) .then(function (clientResp) { return strategy.getResponses(clientResp); }) .then(respond) 在这段代码中,我们可以看到 strategy.reqsFetchParamsToBody() , strategy.getResponses() 和 strategy.clientMethod ,正是之前 searchLooper 和 docLooper 传递的对象属性。而最终 发送请求,同样用的是前面解释过的 es 这个 service。 此外,Courier 还提供了自动刷新的控制功能: self.fetchInterval = function (ms) { searchLooper.ms(ms); return this; }; $rootScope.$watchCollection('timefilter.refreshInterval', function () { var refreshValue = _.get($rootScope, 'timefilter.refreshInterval.value'); var refreshPause = _.get($rootScope, 'timefilter.refreshInterval.pause'); if (_.isNumber(refreshValue) && !refreshPause) { self.fetchInterval(refreshValue); } else { self.fetchInterval(0); } }); 路径记忆功能的实现 plugins/kibana/_apps.js 中,我们可以看到路径记忆功能是怎么实现的: ELKstack 中文指南 534k4源码解析 function appKey(app) { return 'lastPath:' + app.id; } function assignPaths(app) { app.rootPath = '/' + app.id; app.lastPath = sessionStorage.get(appKey(app)) || app.rootPath; return app.lastPath; } function getShow(app) { app.show = app.order >= 0 ? true : false; } function setLastPath(app, path) { app.lastPath = path; return sessionStorage.set(appKey(app), path); } $scope.apps = Private(require('registry/apps')); // initialize each apps lastPath (fetch it from storage) $scope.apps.forEach(assignPaths); $scope.apps.forEach(getShow); function onRouteChange() { var route = $location.path().split(/\//); $scope.apps.forEach(function (app) { if (app.active = app.id === route[1]) { $rootScope.activeApp = app; } }); if (!$rootScope.activeApp || $scope.appEmbedded) return; setLastPath($rootScope.activeApp, globalState.removeFromUrl($location.url())); } $rootScope.$on('$routeChangeSuccess', onRouteChange); $rootScope.$on('$routeUpdate', onRouteChange); 这里使用的 sessionStorage 是 HTML5 自带的新特性,这样,每次标签页切换的时候,都可 以把 $location.url 保存下来。至于整个 Kibana 页面上标签页的初始状态,则通过 registry/apps.js 获取。 插件的加载 那么,各标签页插件是怎么进到 registry/apps 里的呢? 之前我们已经说过,index.js 一开始加载完 kibana 以后,会挨个 require(configFile.plugins) 。这个 configFile.plugins 按说就应该是列出来各个标签页了, 但实际上,K4 的配置文件 server/config/kibana.yml 里并没有一个参数叫 plugins 。所 以,还得看看 server 端的实现了。 ELKstack 中文指南 535k4源码解析 和阅读 Kibana 主页入口一样,找到 server 端的主入口 server/index.js : var requirePlugins = require('./lib/plugins/require_plugins'); var extendHapi = require('./lib/extend_hapi'); function Kibana(settings, plugins) { plugins = plugins || []; this.server = new Hapi.Server(); extendHapi(this.server); var config = this.server.config(); if (settings) config.set(settings); this.plugins = []; var externalPluginsFolder = config.get('kibana.externalPluginsFolder'); if (externalPluginsFolder) { this.plugins = _([externalPluginsFolder]) .flatten() .map(requirePlugins) .flatten() .value(); } this.plugins = this.plugins.concat(plugins); 根据这段代码,我们知道,K4 的 server 端,统一也有插件机制,由 server/lib/plugins/require_plugins.js 加载内置 server 插件,然后再加上外部 server 插件 目录。requirePlugins 中查找内置插件的方法如下: globPath = globPath || join(__dirname, '..', '..', 'plugins', '*', 'index.js'); return glob.sync(globPath).map(function (file) { var module = require(file); var regex = new RegExp('([^/]+)/index.js'); var matches = file.match(regex); if (!module.name && matches) { module.name = matches[1]; } 也就是说,加载 server/plugins/ 下所有子目录的 index.js。目前 K4 自带的有:config, elasticsearch, static, status。分别用来返回 config 数据,代理 ES 请求,处理纯静态文件请 求,显示 server 端各插件状态。 我们具体看这个 server/plugins/config/index.js 的内容: ELKstack 中文指南 536k4源码解析 var listPlugins = require('../../lib/plugins/list_plugins'); module.exports = new kibana.Plugin({ init: function (server, options) { server.route({ method: 'GET', path: '/config', handler: function (request, reply) { var config = server.config(); reply({ kibana_index: config.get('kibana.index'), default_app_id: config.get('kibana.defaultAppId'), shard_timeout: config.get('elasticsearch.shardTimeout'), plugins: listPlugins(server) }); } }); } }); 很明显,一个标准的 node.js 的 route,用来响应对 "/config" 这个地址的 GET 请求,返回一 个哈希 JSON,其中就有 plugins。没错,我们前面说的 configFiles.plugins 就是从这里获得 的。 下面看这个 listPlugins 的实现。 var config = server.config(); var bundled_plugins = plugins(config.get('kibana.bundledPluginsFolder')); var external_plugins = _(server.plugins).map(function (plugin, name) { return plugin.self && plugin.self.publicPlugins || []; }).flatten().value(); 这个 "bundledPluginsFolder" 也不是我们 kibana.yml 里存在的参数设置。所以,还得看上面 这行 server.config() 了。回到最早先的 server/index.js 里,其实在 require_plugins 后 面,还有一个 extend_hapi。Hapi 是 nodejs 的一个可扩展框架。我们看到 index.js 中,正是 在 extendHapi(this.server) 后第一次获取了 server.config() 。 extendHapi 只有两行: server.decorate('server', 'config', require('./config')); server.decorate('server', 'loadKibanaPlugins', require('./plugins/load_kibana_plugins')); 这个 server/lib/config/index.js 主要加载同目录下的:config.js 用来实现 Config 类, schema.js 用来实现具体的 Joi 对象。 ELKstack 中文指南 537k4源码解析 然后我们看这个 server/lib/config/schema.js ,就会发现各种配置属性全在这里了~和插件路 径相关的几行如下: var publicFolder = path.resolve(__dirname, '..', '..', 'public'); if (!checkPath(publicFolder)) publicFolder = path.resolve(__dirname, '..', '..', '..', 'kibana'); var bundledPluginsFolder = path.resolve(publicFolder, 'plugins'); module.exports = Joi.object({ kibana: Joi.object({ bundledPluginsFolder: Joi.string().default(bundledPluginsFolder), 这里有两个路径,因为一个是源码位置,一个是 grunt build 编译后的位置。就我们阅读源码 来说,这个 __dirname/../../../kibana/plugins 就是我们最终找到的地方了,我们从 server 端源码里找了一圈,终于回到 kibana 前端页面的源码目录中,这就是 kibana/plugins 目 录,自动加载其下所有子目录为内置插件。包括: dashboard discover doc kbn_vislib_vis_types kibana markdown_vis metric_vis settings table_vis vis_debug_spy visualize 除 kibana 以外,随意进一个,(还记得 index.js 里是把 kibana 去除掉了吧),比如 visualize, 看 visualize/index.js 的内容,最底下有这么一段: var apps = require('registry/apps'); apps.register(function VisualizeAppModule() { return { id: 'visualize', name: 'Visualize', order: 1 }; }); 其他目录也都一样。 ELKstack 中文指南 538k4源码解析 这个 registry/apps.js 主要是加载 registry/_registry.js ,把注册的 app 存入 utils/indexed_array/index 的 IndexedArray 对象。对象主要有几个值:id, name, order。前 面说到的路径记忆功能要用的两个方法,assignPaths 里就是用 app.id 设置 lastPath,而 getShow 里就是用 order 来判断是否展示在页面上。 下一章,我们开始介绍官方提供的几个 apps。 ELKstack 中文指南 539k4源码解析 搜索页 plugins/discover/index.js 中主要就是注册自己的 id, name, order 到上节最后说的 registry.apps 里。此外就是加载本 app 目录内的其他文件。依次说明如下: plugins/discover/saved_searches/saved_searc hes.js 定义 savedSearches 这个 angular service,用来操作 kibana_index 索引里 search 这个 类型下的数据; 加载了 saved_searches/_saved_searches.js 提供的 savedSearch 这个 angular factory, 这里定义了一个搜索 (search) 在 kibana_index 里的数据结构,包括 title, description, hits, column, sort, version 等字段(这部分内容,可以直接通过读取 Elasticsearch 中的索 引内容看到,比阅读代码更直接,本章最后即专门介绍 kibana_index 中的数据结构), 看的是不是有点眼熟?没错,这个 savedSearch 就是继承了上一节我们介绍的那个 courier 的 savedObject: module.factory('SavedSearch', function (courier) { _.class(SavedSearch).inherits(courier.SavedObject); function SavedSearch(id) { ... 还加载并注册了 plugins/settings/saved_object_registry.js ,表示可以在 settings 里修 改这里的 savedSearches 对象。 plugins/discover/directives/timechart.js 加载 components/vislib/index.js 。 提供 discoverTimechart 这个 angular directive,监听 "data" 并调用 vislib.Chart 对象 绘图。 vislib 是整个 Kibana4 可视化的实现部分,下一节会更详细的讲述。 plugins/discover/components/field_chooser/fie ld_chooser.js ELKstack 中文指南 540k4源码解析 提供 discFieldChooser 这个 angular directive,其中监听 "fields" 并调用 fieldCalculator 计算常用字段排行, $scope.$watchMulti([ '[]fieldCounts', '[]columns', '[]hits' ], function (cur, prev) { var newHits = cur[2] !== prev[2]; var fields = $scope.fields; var columns = $scope.columns || []; var fieldCounts = $scope.fieldCounts; if (!fields || newHits) { $scope.fields = fields = getFields(); } _.chain(fields) .each(function (field) { field.displayOrder = _.indexOf(columns, field.name) + 1; field.display = !!field.displayOrder; field.rowCount = fieldCounts[field.name]; }) .sortBy(function (field) { return (field.count || 0) * -1; }) .groupBy(function (field) { if (field.display) return 'selected'; return field.count > 0 ? 'popular' : 'unpopular'; }) .tap(function (groups) { groups.selected = _.sortBy(groups.selected || [], 'displayOrder'); groups.popular = groups.popular || []; groups.unpopular = groups.unpopular || []; var extras = groups.popular.splice(config.get('fields:popularLimit')); groups.unpopular = extras.concat(groups.unpopular); }) .each(function (group, name) { $scope[name + 'Fields'] = _.sortBy(group, name === 'selected' ? 'display' : 'name'); }) .commit(); $scope.fieldTypes = _.union([undefined], _.pluck(fields, 'type')); }); 监听 "data" 并调用 $scope.details() 方法, ELKstack 中文指南 541k4源码解析 提供 $scope.runAgg() 方法。方法中,根据字段的类型不同,分别可能使用 date_histogram / geohash_grid / terms 聚合函数,创建可视化模型,然后带着当前页这些设 定——前面说过,各 app 之间通过 globalState 共享状态,也就是 URL 中的 ?_a=(...) 。各 app 会通过 rison.decode($location.search()._a) 和 rison.encode($location.search()._a) 设置和读取——跳转到 "/visualize/create" 页面,相当于是这三个常用聚合的快速可视化操 作。 默认的 create 页的 rison 如下: $location.path('/visualize/create').search({ indexPattern: $scope.state.index, type: type, _a: rison.encode({ filters: $scope.state.filters || [], query: $scope.state.query || undefined, vis: { type: type, aggs: [ agg, {schema: 'metric', type: 'count', 'id': '2'} ] } }) }); 之前章节的 url 示例中,读者如果注意的话,会发现 id 是从 2 开始的,原因即在此。 加载 plugins/discover/components/field_chooser/lib/field_calculator.js ,提供 fieldCalculator.getFieldValueCounts() 方法,在 $scope.details() 中读取被点击的字 段值的情况。 加载 plugins/discover/components/field_chooser/discover_field.js ,提供 discoverField 这个 angular directive,用于弹出浮层展示零时的 visualize(调用上一条提供的 $scope.details() 方法),同时给被点击的字段加常用度;加载 plugins/discover/components/field_chooser/lib/detail_views/string.html 网页,用于浮 层效果。网页中对 indexed 或 scripted 类型的字段,可以调用前面提到的 runAgg() 方 法。 ELKstack 中文指南 542k4源码解析 var detailsHtml = require('text!plugins/discover/components/field_chooser/lib/detail_views/string.html'); $scope.toggleDetails = function (field, recompute) { if (_.isUndefined(field.details) || recompute) { // This is inherited from fieldChooser $scope.details(field, recompute); detailScope.$destroy(); detailScope = $scope.$new(); detailScope.warnings = getWarnings(field); detailsElem = $(detailsHtml); $compile(detailsElem)(detailScope); $elem.append(detailsElem); } else { delete field.details; detailsElem.remove(); } }; 加载并渲染 plugins/discover/components/field_chooser/field_chooser.html 网页。网页 中使用了上一条提供的 discover-field。 plugins/discover/controllers/discover.js 加载了诸多 js,主要做了: 为 "/discover/:id" 提供 route 并加载 plugins/discover/index.html 网页。 提供 discover 这个 angular controller。 加载 components/vis/vis.js 并在 setupVisualization 函数中绘制 histogram 图。 ELKstack 中文指南 543k4源码解析 var visStateAggs = [ { type: 'count', schema: 'metric' }, { type: 'date_histogram', schema: 'segment', params: { field: $scope.opts.timefield, interval: $state.interval, min_doc_count: 0 } } ]; $scope.vis = new Vis($scope.indexPattern, { type: 'histogram', params: { addLegend: false, addTimeMarker: true }, listeners: { click: function (e) { timefilter.time.from = moment(e.point.x); timefilter.time.to = moment(e.point.x + e.data.ordered.interval); timefilter.time.mode = 'absolute'; }, brush: brushEvent }, aggs: visStateAggs }); $scope.searchSource.aggs(function () { $scope.vis.requesting(); return $scope.vis.aggs.toDsl(); }); ELKstack 中文指南 544k4源码解析 visualize app index.js 中,首要当然是注册自己。此外,还加载两部分功 能: plugins/visualize/editor/editor.js 和 plugins/visualize/wizard/wizard.js 。然后定义 了一个 route,默认跳转 /visualize 到 /visualize/step/1 。 editor editor.js 中也定义了两个 route,分别是 /visualize/create 和 /visualize/edit/:id 。然后 还定义了一个controller,叫 VisEditor ,对应的 HTML 是 plugins/visualize/editor/editor.html ,其中用到两个 directive,分别是 visualize 和 vis-editor-sidebar 。 其中 create 是先加载 registry/vis_types ,并检查 $route.current.params.type 是否存在, 然后调用 savedVisualizations.get($route.current.params) 方法;而 edit 是直接调用 savedVisualizations.get($route.current.params.id) 。 vis_types 实际注册了 vis_types 的地方包括: plugins/table_vis/index.js plugins/metric_vis/index.js plugins/markdown_vis/index.js plugins/kbn_vislib_vis_types/index.js 前三个是表单,最后一个是可视化图。内容如下: define(function (require) { var visTypes = require('registry/vis_types'); visTypes.register(require('plugins/kbn_vislib_vis_types/histogram')); visTypes.register(require('plugins/kbn_vislib_vis_types/line')); visTypes.register(require('plugins/kbn_vislib_vis_types/pie')); visTypes.register(require('plugins/kbn_vislib_vis_types/area')); visTypes.register(require('plugins/kbn_vislib_vis_types/tileMap')); }); 以 histogram 为例解释一下 visTypes。下面的实现较长,我们拆成三部分: 第一部分,加载并生成VislibVisType对象: ELKstack 中文指南 545k4源码解析 define(function (require) { return function HistogramVisType(Private) { var VislibVisType = Private(require('components/vislib_vis_type/VislibVisType')); var Schemas = Private(require('components/vis/Schemas')); return new VislibVisType({ name: 'histogram', title: 'Vertical bar chart', icon: 'fa-bar-chart', description: 'The goto chart for oh-so-many needs. Great for time and non-time data. Stacked or grouped, ' + 'exact numbers or percentages. If you are not sure which chart your need, you could do worse than to start here.', 第二部分,histogram 可视化所接受的参数默认值以及对应的参数编辑页面: params: { defaults: { shareYAxis: true, addTooltip: true, addLegend: true, scale: 'linear', mode: 'stacked', times: [], addTimeMarker: false, defaultYExtents: false, setYExtents: false, yAxis: {} }, scales: ['linear', 'log', 'square root'], modes: ['stacked', 'percentage', 'grouped'], editor: require('text!plugins/kbn_vislib_vis_types/editors/histogram.html') }, 第三部分,histogram 可视化能接受的 Schema。一般来说,metric 数值聚合肯定是 Y 轴; bucket 聚合肯定是 X 轴;而在此基础上,Kibana4 还可以让 bucket 有不同效果,也就是 Schema 里的 segment(默认), group 和 split。根据效果不同,这里是各有增减的,比如饼图 就不会有 group。 ELKstack 中文指南 546k4源码解析 schemas: new Schemas([ { group: 'metrics', name: 'metric', title: 'Y-Axis', min: 1, aggFilter: '!std_dev', defaults: [ { schema: 'metric', type: 'count' } ] }, { group: 'buckets', name: 'segment', title: 'X-Axis', min: 0, max: 1, aggFilter: '!geohash_grid' }, { group: 'buckets', name: 'group', title: 'Split Bars', min: 0, max: 1, aggFilter: '!geohash_grid' }, { group: 'buckets', name: 'split', title: 'Split Chart', min: 0, max: 1, aggFilter: '!geohash_grid' } ]) }); }; }); 这里使用的 VislibVisType 类,继承自 components/vis/VisType.js , VisType.js 内容如下: ELKstack 中文指南 547k4源码解析 define(function (require) { return function VisTypeFactory(Private) { var VisTypeSchemas = Private(require('components/vis/Schemas')); function VisType(opts) { opts = opts || {}; this.name = opts.name; this.title = opts.title; this.responseConverter = opts.responseConverter; this.hierarchicalData = opts.hierarchicalData || false; this.icon = opts.icon; this.description = opts.description; this.schemas = opts.schemas || new VisTypeSchemas(); this.params = opts.params || {}; this.requiresSearch = opts.requiresSearch == null ? true : opts.requiresSearch; // Default to true unless otherwise specified } return VisType; }; }); 基本跟上面 histogram 的示例一致,注意这里面的 responseConverter 和 hierarchicalData, 是给不同的 visType 做相应数据转换的。在实际的 VislibVisType 中,就有下面一段: if (this.responseConverter == null) { this.responseConverter = pointSeries; } 可见默认情况下,Kibana4 是尝试把聚合结果转换成点线图数组的。 VislibVisType 中另一部分,则是扩展了一个自己的方法 createRenderbot,用来生成 VislibRenderbot 对象。这个类的实现在 components/vislib_vis_type/VislibRenderbot.js ,其 中最关键的几行是: var buildChartData = Private(require('components/vislib_vis_type/buildChartData')); ... self.vislibVis = new vislib.Vis(self.$el[0], self.vislibParams); ... VislibRenderbot.prototype.buildChartData = buildChartData; VislibRenderbot.prototype.render = function (esResponse) { this.chartData = this.buildChartData(esResponse); this.vislibVis.render(this.chartData); }; 也就是说,分为两部分,buildChartData 方法和 vislib.Vis 对象。 ELKstack 中文指南 548k4源码解析 先来看 buildChartData 的实现: var aggResponse = Private(require('components/agg_response/index')); return function (esResponse) { var vis = this.vis; if (vis.isHierarchical()) { return aggResponse.hierarchical(vis, esResponse); } var converted = convertTableGroup(vis, tableGroup); return converted; }; function convertTable(vis, table) { return vis.type.responseConverter(vis, table); } 又看到 responseConverter 和 hierarchical 两个熟悉的字眼了,不过这回是另一个对象的方 法,那么我们继续跟踪下去,看看这个 aggResponse 类是怎么回事: define(function (require) { return function NormalizeChartDataFactory(Private) { return { hierarchical: Private(require('components/agg_response/hierarchical/build_hierarchical_data')), pointSeries: Private(require('components/agg_response/point_series/point_series')), tabify: Private(require('components/agg_response/tabify/tabify')), geoJson: Private(require('components/agg_response/geo_json/geo_json')) }; }; }); 然后我们看 vislib.Vis 对象,定义在 components/vislib/vis.js 里。同时我们注意到,定义 vislib 这个服务的 components/vislib/index.js 里,还定义了一个服务,叫 d3,没错,我们 离真正的绘图越来越近了。 vis.js 中加载了 components/vislib/lib/handler/handler_types 和 components/vislib/visualizations/vis_types : var handlerTypes = Private(require('components/vislib/lib/handler/handler_types')); var chartTypes = Private(require('components/vislib/visualizations/vis_types')); chartTypes 用来定义图: ELKstack 中文指南 549k4源码解析 function Vis($el, config) { if (!(this instanceof Vis)) { return new Vis($el, config); } Vis.Super.apply(this, arguments); this.el = $el.get ? $el.get(0) : $el; this.ChartClass = chartTypes[config.type]; this._attr = _.defaults({}, config || {}, {}); } handlerTypes 用来绘制图: Vis.prototype.render = function (data) { var chartType = this._attr.type; this.data = data; this.handler = handlerTypes[chartType](this) || handlerTypes.column(this); this._runOnHandler('render'); }; Vis.prototype._runOnHandler = function (method) { this.handler[method](); }; components/vislib/lib/handler/handler_types 中,根据不同的 vis_types,分别返回不同的处 理对象,主要出自 components/vislib/lib/handler/types/point_series , components/vislib/lib/handler/types/pie 和 components/vislib/lib/handler/types/tile_map 。比如 histogram 就是 pointSeries.column 。 可以看到 point_series.js 中,对 column 是加上了 zeroFill:true, expandLastBucket:true 两 个参数调用 create() 方法。而 create() 方法里的 new Handler() 传递的,显然就是给 d3.js 的绘图参数。而 Handler 具体初始化和渲染过程,则在被加载的 components/vislib/lib/handler/handler.js 中。 Handler.prototype.render 中如下一段: ELKstack 中文指南 550k4源码解析 d3.select(this.el) .selectAll('.chart') .each(function (chartData) { var chart = new self.ChartClass(self, this, chartData); var enabledEvents; if (chart.events.dispatch) { enabledEvents = self.vis.eventTypes.enabled; d3.rebind(chart, chart.events.dispatch, 'on'); if (enabledEvents.length) { enabledEvents.forEach(function (event) { self.enable(event, chart); }); } } charts.push(chart); chart.render(); }); }; 这里面的 ChartClass() 就是在 vislib.js 中加载了的 components/vislib/visualizations/vis_types 。它会根据不同的 vis_types,分别返回不同的 可视化对象,包括: components/vislib/visualizations/column_chart , components/vislib/visualizations/pie_chart , components/vislib/visualizations/line_chart , components/vislib/visualizations/area_chart 和 components/vislib/visualizations/tile_map 。 这些对象都有同一个基类: components/vislib/visualizations/_chart ,其中有这么一段: Chart.prototype.render = function () { var selection = d3.select(this.chartEl); selection.selectAll('*').remove(); selection.call(this.draw()); }; 也就是说,各个可视化对象,只需要用 d3.js 或者其他绘图库,完成自己的 draw() 函数,就 可以了! draw 函数的实现一般格式是下面这样: ELKstack 中文指南 551k4源码解析 LineChart.prototype.draw = function () { var self = this; var $elem = $(this.chartEl); var margin = this._attr.margin; var elWidth = this._attr.width = $elem.width(); var elHeight = this._attr.height = $elem.height(); var width; var height; var div; var svg; return function (selection) { selection.each(function (data) { var el = this; div = d3.select(el); width = elWidth - margin.left - margin.right; height = elHeight - margin.top - margin.bottom; svg = div.append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); // 处理 data 到 svg 上 ... return svg; }); }; }; 当然,为了代码逻辑,有些比较复杂的绘制,还是会继续拆分成其他文件的。比如之前已经 在 v3/bettermap 章节介绍过的 leaflet 地图,在 v4 中,就是在 components/vislib/visualizations/tile_map 里加载的 components/vislib/visualizations/_map.js 完成。还想继续使用高德地图的读者,可以修改 该文件中 tileLayer 变量的参数定义(https://otile{s}- s.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg)即可。 从数据到 d3 渲染,要经过的主要流程就是这样。如果打算自己亲手扩展一个新的可视化方案 的读者,可以具体参考我实现的 sankey 图:https://github.com/chenryn/kibana4/commit/4e0bcbeb4c8fd94807c3a0b1df2ac6f56634f 9a5 savedVisualizations 这个类在 plugins/visualize/saved_visualizations/saved_visualizations.js 里定义。其中分 三步,加载 plugins/visualize/saved_visualizations/_saved_vis ,注册到 plugins/settings/saved_object_registry ,以及定义一个 angular service 叫 savedVisualizations 。 ELKstack 中文指南 552k4源码解析 plugins/visualize/saved_visualizations/_saved_vis 里是定义一个 angular factory 叫 SavedVis 。这个类继承自 courier.SavedObject,主要有 _getLinkedSavedSearch 方法调用 savedSearches 获取在 discover 中保存的 search 对象,以及 visState 属性。该属性保存了 visualize 定义的 JSON 数据。 savedVisualizations 里主要就是初始化 SavedVis 对象,以及提供了一个 find 搜索方法。整 个实现和上一节讲的 savedSearches 基本一样,就不再讲了。 Visualize 这个 directive 在 components/visualize/visualize.js 中定义。而我们可以上拉看到的请求、 响应、表格、性能数据,则使用的是 components/visualize/spy/spy.js 中定义的另一个 directive visualizeSpy 。 visualize.html 上定义了一个普通的 div,其 class 为 visualize-chart,在 visualize.js 中,通 过 getter('.visualize-chart') 方法获取 div 元素: function getter(selector) { return function () { var $sel = $el.find(selector); if ($sel.size()) return $sel; }; } var getVisEl = getter('.visualize-chart'); 然后创建一个 renderbot: $scope.$watch('vis', prereq(function (vis, oldVis) { var $visEl = getVisEl(); if (!$visEl) return; if (!attr.editableVis) { $scope.editableVis = vis; } if (oldVis) $scope.renderbot = null; if (vis) $scope.renderbot = vis.type.createRenderbot(vis, $visEl); })); 最后在 searchSource 对象变化,即有新的搜索响应返回时,完成渲染: ELKstack 中文指南 553k4源码解析 $scope.$watch('searchSource', prereq(function (searchSource) { if (!searchSource || attr.esResp) return; searchSource.onResults().then(function onResults(resp) { if ($scope.searchSource !== searchSource) return; $scope.esResp = resp; return searchSource.onResults().then(onResults); }).catch(notify.fatal); searchSource.onError(notify.error).catch(notify.fatal); })); $scope.$watch('esResp', prereq(function (resp, prevResp) { if (!resp) return; $scope.renderbot.render(resp); })); VisEditorSidebar 这个 directive 在 plugins/visualize/editor/sidebar.js 中定义。对应的 HTML 是 plugins/visualize/editor/sidebar.html ,其中又用到两个 directive,分别是 vis-editor- agg-group 和 vis-editor-vis-options 。它们分别有 sidebar.js 加载的 plugins/visualize/editor/agg_group 和 plugins/visualize/editor/vis_options 提供。然后 继续 HTML -> directive 下去,基本上 plugins/visualize/editor/ 目录下那堆 agg*.js 和 agg*.html 都是做这个用的。 其中比较有意思的,应该算是 agg_add.js 。我们都知道,K4 最大的特点就是可以层叠子聚 合,这个操作就是在这里完成的: .directive('visEditorAggAdd', function (Private) { var AggConfig = Private(require('components/vis/AggConfig')); return { restrict: 'E', template: require('text!plugins/visualize/editor/agg_add.html'), controllerAs: 'add', controller: function ($scope) { var self = this; self.submit = function (schema) { var aggConfig = new AggConfig($scope.vis, { schema: schema }); aggConfig.brandNew = true; $scope.vis.aggs.push(aggConfig); }; } }; }); ELKstack 中文指南 554k4源码解析 另一个比较重要的是 plugins/visualize/editor/agg_params.js 。其中加载了 components/agg_types/index.js ,又监听了 "agg.type" 变量,也就是实现了选择不同的 agg_types 时,提供不同的 agg_params 选项。比方说,选择 date_histogram,字段就只能 是 @timestamp 这种 date 类型的字段。 components/agg_types/index.js 中定义了所有可选 agg_types 的类。其中 metrics 包括: count, avg, sum, min, max, std_deviation, cardinality, percentiles, percentile_rank,具体实现 分别存在 components/agg_types/metrics/ 目录下的同名.js文件里;buckets 包括: date_histogram, histogram, range, date_range, ip_range, terms, filters, significant_terms, geo_hash,具体实现分别存在 components/agg_types/buckets/ 目录下的同名.js文件里。 这些类定义中,都有比较类似的格式,其中 params 数组的第一个元素,都是类似这样: { name: 'field', filterFieldTypes: 'string' } terms.js 里还多了一行 scriptable: true ,而且 filterFieldTypes 是数组。 { name: 'field', scriptable: true, filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string'] } 这个 filterFieldTypes 在 components/vis/_agg_config.js 中,通过 fieldTypeFilter(this.vis.indexPattern.fields, fieldParam.filterFieldTypes); 得到可选字段 列表。fieldTypeFilter 的具体实现在 filters/filed_type.js 中。 wizard wizard.js 中提供两个 route 和对应的 controller。分别是 /visualize/step/1 对应 VisualizeWizardStep1 , /visualize/step/2 对应 VisualizeWizardStep2 。这两个的最终结 果,都是跳转到 /visualize/create?type=* 下。 ELKstack 中文指南 555k4源码解析 dashboard app plugins/dashboard/index.js 结构跟 visualize 类似,注册到 registry;设置两个调用 savedDashboards.get() 方法的 routes,提供一个叫 dashboard-app 的 directive。 savedDashboards 由 plugins/dashboard/services/saved_dashboard.js 提供,调用 es.search 获取数据,生成 savedDashboard 对象,这个对象同样也是继承 savedObject,主要内容是 panelsJSON 数组字段。实现如下: module.factory('SavedDashboard', function (courier) { _.class(SavedDashboard).inherits(courier.SavedObject); function SavedDashboard(id) { courier.SavedObject.call(this, { type: SavedDashboard.type, mapping: SavedDashboard.mapping, searchSource: SavedDashboard.searchsource, id: id, defaults: { title: 'New Dashboard', hits: 0, description: '', panelsJSON: '[]', version: 1, timeRestore: false, timeTo: undefined, timeFrom: undefined }, clearSavedIndexPattern: true }); } 可以注意到,这个 panelsJSON 是一个字符串,这跟之前 kibana_index 提到的是一致的。 dashboard-app 中,最重要的功能,是监听搜索框和过滤条件的变更,我们可以看到 init 函数 中有下面这段: ELKstack 中文指南 556k4源码解析 function updateQueryOnRootSource() { var filters = queryFilter.getFilters(); if ($state.query) { dash.searchSource.set('filter', _.union(filters, [{ query: $state.query }])); } else { dash.searchSource.set('filter', filters); } } $scope.$listen(queryFilter, 'update', function () { updateQueryOnRootSource(); $state.save(); }); 在 index.html 里,实际承载面板的,是下面这行: 这也是一个 angular directive,通过加载 plugins/dashboard/directives/grid.js 引入的。其 中添加面板相关的代码有两部分: $scope.$watchCollection('state.panels', function (panels) { var currentPanels = gridster.$widgets.toArray().map(function (el) { return getPanelFor(el); }); var added = _.difference(panels, currentPanels); if (added.length) added.forEach(addPanel); }); 这段用来监听 $state.panels 数组,一旦有新增面板,调用 addPanel 函数。同理也有删除 面板的,这里就不重复贴了。 而 addPanel 函数的实现大致如下: var addPanel = function (panel) { _.defaults(panel, { size_x: 3, size_y: 2 }); panel.$scope = $scope.$new(); panel.$scope.panel = panel; panel.$el = $compile('
  • ')(panel.$scope); gridster.add_widget(panel.$el, panel.size_x, panel.size_y, panel.col, panel.row); }; ELKstack 中文指南 557k4源码解析 这里即验证了之前 kibana_inex 小节中讲的 gridster 默认大小,又引入了一个新的 directive, 叫 dashboard-panel。 dashboard-panel 在 plugins/dashboard/components/panel/panel.js 中实现,其中使用了 plugins/dashboard/components/panel/panel.html 页面。页面最后是这么一段: 这里使用的 savedObj 对象,来自 plugins/dashboard/components/panel/lib/load_panel.js 获 取的 savedSearch 或者 savedVisualization。获得的对象,以 savedVisualization 为例: define(function (require) { return function visualizationLoader(savedVisualizations, Private) { // Inject services here return function (panel, $scope) { return savedVisualizations.get(panel.id) .then(function (savedVis) { savedVis.vis.listeners.click = filterBarClickHandler($scope.state); savedVis.vis.listeners.brush = brushEvent; return { savedObj: savedVis, panel: panel, editUrl: savedVisualizations.urlFor(panel.id) }; }); }; }; }); 而 visualize 和 doc-table 两个 directive。这两个正是之前在 visualize 和 discover 插件解析里 提到过的,在 components/ 底下实现。 ELKstack 中文指南 558k4源码解析 settings plugins/settings/index.js 结构跟 visualize 类似,注册到 registry;设置默认跳转到 /settings/indices 的 route,提供一个 kbnSettingsApp 的directive。其中关联到 plugins/settings/sections/index.js 内注册的 indices, advanced, objects, about 四个区块。 因为结构基本类似,这里只介绍一个比较有趣的地方。 indices 中的 scripted_field,原本的设 计中,是利用 groovy sandbox 来支持的。但是就在 kibana 4 正式版要发布的几天前, groovy sandbox 出安全漏洞,Elasticsearch 紧急取消掉了 groovy 的默认开启设置。同 理, plugins/settings/sections/indices/scripted_fields/index.js 里也改成了 "expression" 引擎。 但是,如果私有集群在防火墙内部,依然可以开启 groovy sandbox 的,其实还是可以继续使 用的。在稍后的章节中,我们会介绍如何直接修改 kibana_index 完成。而这里,我们则深入 理解相关代码,介绍为什么不用修改 kibana 4 源码,就能继续使用。 scripted field 的修改页面,在 plugins/settings/sections/indices/_scripted_fields.js 。这里 面的 $scope.columns 数组,包括 name, script, type, popularity, controls 五列,也就是我们 在页面上看到的内容。看起来似乎没有定义选用的引擎。 那往上一层,看 scripted field 相关入口 plugins/settings/sections/indices/scripted_fields/index.js 里,在 $scope.submit() 方法 里,我们可以看到下面一段代码: var field = _.defaults($scope.scriptedField, { type: 'number', lang: 'expression' }); try { if (createMode) { $scope.indexPattern.addScriptedField(field.name, field.script, field.type, field.lang); } else { $scope.indexPattern.save(); } 没错,添加 scripted field 到 index pattern 的过程就是这里!我们可以看到,这里是有 field.lang 设定的。只不过其默认值是 expression 而已。 现在就去看 plugins/settings/sections/indices/scripted_fields/index.html 里关于 $scope.scriptedField 是怎么处理的。 ELKstack 中文指南 559k4源码解析
    没错。HTML 里只提供了 name 和 script 两个值的输入框!也就是说,kibana 4 只是不提供 让你输入 groovy 到 field.lang 的文本框而已。 所以,如果你有随时定义 scripted field 的需求,又嫌弃每次 curl 直接修改 kibana_index 太麻 烦还可能出错,那么你只需要稍微修改几处 kibana4 代码就够了: 1. plugins/settings/sections/indices/scripted_fields/index.html 里提供对 scriptedField.lang 和 scriptedField.type 的输入框; 2. plugins/settings/sections/indices/_scripted_fields.js 里给 $scope.columns 数组和 addRow 方法多加上 field.lang 字段的展示。 ELKstack 中文指南 560k4源码解析 Kibana4 插件 Kibana 从 4.2 以后,引入了完善的插件化机制。目前分为 app,vistype,fieldformatter、 spymode 等多种插件类型。原先意义上的 Kibana 现在已经变成了 Kibana 插件框架下的一个 默认 app 类型插件。 本节用以讲述 Kibana4 插件的安装使用和定制开发。 部署命令 安装 Kibana 插件有两种方式: 1. 通过 Elastic.co 公司的下载地址: bin/kibana plugin --install // version 是可选项。这种方式目前适用于官方插件,比如: bin/kibana plugin -i elasticsearch/marvel/latest 1. 通过 GitHub 仓库地址: bin/kibana plugin --install // 这种方式目前适用于非官方,或官方还未发布的,寄存在 GitHub 上的插件代码,比如: bin/kibana plugin -i elastic/timelion bin/kibana plugin -i snuids/jVectorMapKibanaCountry bin/kibana plugin -i sirensolutions/kibi_wordcloud_vis/0.1.0 bin/kibana plugin -i sirensolutions/kibi_radar_vis/0.1.0 1. 通过 zip 压缩包: 支持本地和远程 HTTP 下载两种,比如: bin/kibana plugin -i heatmap -u https://github.com/stormpython/heatmap/archive/master.zip ELKstack 中文指南 561k4 插件 查看与切换 插件安装完成后,可以在 Kibana 页面上通过 app switcher 界面切换。界面如下: 默认插件 除了 Kibana 本身以外,其实还有一些其他默认插件,这些插件本身在 app switcher 页面上是 隐藏的,但是可以通过 url 直接访问到,或者通过修改插件的 index.js 配置项让它显示出 来。 这些隐藏的默认插件中,最有可能被用到的,是 statusPage 插件。 我们可以通过 http://localhost:5601/status 地址访问这个插件的页面: ELKstack 中文指南 562k4 插件 页面会显示 Kibana 的运行状态。包括 nodejs 的内存使用、负载、响应性能,以及各插件的 加载情况。 ELKstack 中文指南 563k4 插件 timelion 介绍 ES2.0 开始提供了一个崭新的 pipeline aggregation 特性,但是 Kibana 似乎并没有立刻跟进 这方面的意思,相反,Elastic 公司推出了另一个实验室产品:]Timelion。 timelion 的用法在官博里已经有介绍。尤其是最近两篇如何用 timelion 实现异常告警的文章, 更是从 ES 的 pipeline aggregation 细节和场景一路讲到 timelion 具体操作,我这里几乎没有 再重新讲一遍 timelion 操作入门的必要了。不过,官方却一直没有列出来 timelion 支持的请 求语法的文档,而是在页面上通过点击图标的方式下拉帮助。 timelion 页面设计上,更接近 Kibana3 而不是 Kibana4。比如 panel 分布是通过设置几行几列 的数目来固化的;query 框是唯一的,要修改哪个 panel 的 query,鼠标点选一下 panel, query 就自动切换成这个 panel 的了。 为了方便大家在上手之前了解 timelion 能做到什么,今天特意把 timelion 的请求语法所支持 的函数分为几类,罗列如下: 可视化效果类 .bars($width) : 用柱状图展示数组 .lines($width, $fill, $show, $steps) : 用折线图展示数组 .points() : 用散点图展示数组 .color("#c6c6c6") : 改变颜色 .hide() : 隐藏该数组 ELKstack 中文指南 564k4 插件 .label("change from %s") : 标签 .legend($position, $column) : 图例位置 .yaxis($yaxis_number, $min, $max, $position) : 设置 Y 轴属性,.yaxis(2) 表示第二根 Y 轴 数据运算类 .abs() : 对整个数组元素求绝对值 .precision($number) : 浮点数精度 .testcast($count, $alpha, $beta, $gamma) : holt-winters 预测 .cusum($base) : 数组元素之和,再加上 $base .derivative() : 对数组求导数 .divide($divisor) : 数组元素除法 .multiply($multiplier) : 数组元素乘法 .subtract($term) : 数组元素减法 .sum($term) : 数组元素加法 .add() : 同 .sum() .plus() : 同 .sum() .first() : 返回第一个元素 .movingaverage($window) : 用指定的窗口大小计算移动平均值 .mvavg() : .movingaverage() 的简写 .movingstd($window) : 用指定的窗口大小计算移动标准差 .mvstd() : .movingstd() 的简写 数据源设定类 .elasticsearch() : 从 ES 读取数据 .es(q="querystring", metric="cardinality:uid", index="logstash-*", offset="-1d") : .elasticsearch() 的简写 .graphite(metric="path.to.*.data", offset="-1d") : 从 graphite 读取数据 .quandl() : 从 quandl.com 读取 quandl 码 .worldbank_indicators() : 从 worldbank.org 读取国家数据 .wbi() : .worldbank_indicators() 的简写 .worldbank() : 从 worldbank.org 读取数据 .wb() : .worldbanck() 的简写 以上所有函数,都在 series_functions 目录下实现,每个 js 文件实现一个 TimelionFunction 功能。 ELKstack 中文指南 565k4 插件 Kibana4 可视化插件开发 上一节,我们看到了一个完整的 Kibana4 插件的官方用例。一般来说,我们不太会需要自己 从头到尾写一个 angular app 出来。最常见的情况,应该是在 Kibana 功能的基础上做一定的 二次开发和扩展。其中,可视化效果应该是重中之重。本节,以一个红绿灯效果,演示如何 开发一个 Kibana4 可视化插件。 插件目录生成 Kibana 开发组提供了一个简单的工具,辅助我们生成一个 Kibana4 插件的目录结构: npm install -g yo npm install -g generator-kibana-plugin mkdir traffic_light_vis cd traffic_light_vis yo kibana-plugin 但是这个是针对完整的 app 扩展的,很多目录对于可视化插件来说,并没有用。所以,我们 可以自己手动创建目录: ELKstack 中文指南 566k4 插件 mkdir -p traffic_light_vis/public cd traffic_light_vis touch index.js package.json cd public touch traffic_light_vis.html traffic_light_vis.js traffic_light_vis.less traffic_light_vis_controller.js traffic_light_vis_params.html 其中 index.js 内容如下: 'use strict'; module.exports = function (kibana) { return new kibana.Plugin({ uiExports: { visTypes: ['plugins/traffic_light_vis/traffic_light_vis'] } }); }; package.json 内容如下: { "name": "traffic_light_vis", "version": "0.1.0", "description": "An awesome Kibana plugin for red/yellow/green status visualize" } 这两个基础文件格式都比较固定,除了改个名就基本 OK 了。 然后我们看最关键的可视化对象定义 public/traffic_light_vis.js 内容: ELKstack 中文指南 567k4 插件 define(function (require) { // 加载样式表 require('plugins/traffic_light_vis/traffic_light_vis.less'); // 加载控制器程序 require('plugins/traffic_light_vis/traffic_light_vis_controller'); // 注册到 vis_types require('ui/registry/vis_types').register(TrafficLightVisProvider); function TrafficLightVisProvider(Private) { // TemplateVisType 基类,适用于基础的 metric 和数据表格式的可视化定义。实际上,Kibana4 的 metric_vis 和 table_vis 就继承自这个, // Kibana4 还有另一个基类叫 VisLibVisType,其他使用 D3.js 做可视化的,继承这个。 var TemplateVisType = Private(require('ui/template_vis_type/TemplateVisType')); var Schemas = Private(require('ui/Vis/Schemas')); // 模板化的 visType 对象定义,用来配置和展示 return new TemplateVisType({ name: 'traffic_light', // 显示在 visualize 选择列表里的名称,描述,小图标 title: 'Traffic Light', description: 'Great for one-glance status readings, the traffic light visualization expresses in green / yellow / red the position of a single value in relation to low and high thresholds.' icon: 'fa-car', // 可视化效果模板页面 template: require('plugins/traffic_light_vis/traffic_light_vis.html'), params: { defaults: { fontSize: 60 }, // 编辑参数的页面 editor: require('plugins/traffic_light_vis/traffic_light_vis_params.html') }, // 在编辑页面上可以选择的 aggregation 类型。 schemas: new Schemas([ { group: 'metrics', name: 'metric', title: 'Metric', min: 1, defaults: [ { type: 'count', schema: 'metric' } ] } ]) }); } // export the provider so that the visType can be required with Private() return TrafficLightVisProvider; }); ELKstack 中文指南 568k4 插件 然后就是可视化效果的模板页面了, traffic_light_vis.html 毫无疑问也是一个 angular 风 格的:
    {{metric.label}}
    这里可以看到: 1. 把 div 绑定到了 TrafficLightVisController 控制器上,这个也是之前在 js 里已经加载过 的。 2. 通过 ng-repeat 循环展示不同的 metric,也就是说模板渲染的时候,收到的是一个 metrics 数组。这个来源当然是在控制器里。 3. 然后具体的数据判断,即什么灯亮什么灯灭,通过了 vis.params.* 的运算判断。这些变 量当然是在编辑页面里设置的。 所以下一步看编辑页面 traffic_light_vis_params.html :
    ELKstack 中文指南 569k4 插件 内容很简单,就是通过 ng-model 设置绑定变量,跟之前 HTML 里的联动。 最后一步,看控制器 traffic_light_vis_controller.js : define(function (require) { var module = require('ui/modules').get('kibana/traffic_light_vis', ['kibana']); module.controller('TrafficLightVisController', function ($scope, Private) { var tabifyAggResponse = Private(require('ui/agg_response/tabify/tabify')); var metrics = $scope.metrics = []; $scope.processTableGroups = function (tableGroups) { tableGroups.tables.forEach(function (table) { table.columns.forEach(function (column, i) { metrics.push({ label: column.title, value: table.rows[0][i] }); }); }); }; $scope.$watch('esResponse', function (resp) { if (resp) { metrics.length = 0; $scope.processTableGroups(tabifyAggResponse($scope.vis, resp)); } }); }); }); 要点在: 1. $scope.$watch('esResponse', function(resp){}) 监听整个页面的请求响应,在有新数据 过来的时候更新页面效果; 2. agg_response/tabify/tabify 把响应结果转换成二维表格形式。 最后加上一段样式表,这里就不贴了,见:https://github.com/logzio/kibana- visualizations/blob/master/traffic_light_vis/traffic_light_vis.less。 本节介绍的示例,出自 logz.io 官方博客和对应的 github 开源项目。logz.io 是基于 Kibana4.1 写的插件。我这里修正成了基于最新 Kibana4.3 的实现。 ELKstack 中文指南 570k4 插件 Kibana4 服务器端插件开发 上一节介绍了如何给 Kibana4 开发浏览器端的可视化插件。Kibana4 跟 Kibana3 比,最大的 一个变化是有了独立的 node.js 服务器端。那么同样的,也就有了服务器端的 Kibana4 插 件。最明显的一个场景:我们可以在 node.js 里跑定时器做 Elasticsearch 的告警逻辑了! 本节示例一个最基础的 Kibana4 告警插件开发。只演示基础的定时器和 Kibana4 插件规范, 实际运用中,肯定还涉及历史记录,告警项配置更新等。请读者不要直接 copy-paste。 首先,我们尽量沿袭 Elastic 官方的 watcher 产品的告警配置设计。也新建一个索引,里面是 具体的配置内容: # curl -XPUT http://127.0.0.1:9200/watcher/watch/error_status -d' { "trigger": { "schedule" : { "interval" : "60" } }, "input" : { "search" : { "request" : { "indices" : [ "", "" ], "body" : { "query" : { "filtered" : { "query" : { "match" : { "host" : "MacBook-Pro" } }, "filter" : { "range" : { "@timestamp" : { "from" : "now-5m" } } } } } } } } }, "condition" : { "script" : { "script" : "payload.hits.total > 0" } }, "transform" : { "search" : { "request" : { "indices" : [ "", "" ], "body" : { "query" : { "filtered" : { "query" : { "match" : { "host" : "MacBook-Pro" } }, "filter" : { "range" : { "@timestamp" : { "from" : "now-5m" } } } } }, ELKstack 中文指南 571k4 插件 "aggs" : { "topn" : { "terms" : { "field" : "path.raw" } } } } } } }, "actions" : { "email_admin" : { "throttle_period" : "15m", "email" : { "to" : "admin@domain", "subject" : "Found {{payload.hits.total}} Error Events", "priority" : "high", "body" : "Top10 paths:\n{{#payload.aggregations.topn.buckets}}\t{{key}} {{doc_count}}\n{{/payload.aggregations.topn.buckets}}" } } } }' 我们可以看到,跟原版的相比,只改动了很小的一些地方: 1. 为了简便, interval 固定写数值,没带 s/m/d/H 之类的单位; 2. condition 里直接使用了 JavaScript,这点也是 ES 2.x 的 mapping 要求跟 watcher 本 身有冲突的一个地方:watcher的 "ctx.payload.hits.total" : { "gt" : 0 } 这种写法, 如果是普通索引,会因为字段名里带 . 直接写入失败的; 3. 因为是在 Kibana 里面运行,所以从 ES 拿到的只有 payload(也就是查询响应),所以把 里面的 ctx. 都删掉了。 好,然后创建插件: cd kibana-4.3.0-darwin-x64/src/plugins mkdir alert 在自定义插件目录底下创建 package.json 描述: { "name": "alert", "version": "0.0.1" } 以及最终的 index.js 代码: ELKstack 中文指南 572k4 插件 'use strict'; module.exports = function (kibana) { var later = require('later'); var _ = require('lodash'); var mustache = require('mustache'); return new kibana.Plugin({ init: function init(server) { var client = server.plugins.elasticsearch.client; var sched = later.parse.text('every 10 minute'); later.setInterval(doalert, sched); function doalert() { getCount().then(function(resp){ getWatcher(resp.count).then(function(resp){ _.each(resp.hits.hits, function(hit){ var watch = hit._source; var every = watch.trigger.schedule.interval; var watchSched = later.parse.recur().every(every).second(); var wt = later.setInterval(watching, watchSched); function watching() { var request = watch.input.search.request; var condition = watch.condition.script.script; var transform = watch.transform.search.request; var actions = watch.actions; client.search(request).then(function(payload){ var ret = eval(condition); if (ret) { client.search(transform).then(function(payload) { _.each(_.values(actions), function(action){ if(_.has(action, 'email')) { var subject = mustache.render(action.email.subject, {"payload":payload}); var body = mustache.render(action.email.body, {"payload":payload}); console.log(subject, body); } }); }); } }); } }); }); }); } function getCount() { return client.count({ index:'watcher', type:"watch" }); } function getWatcher(count) { return client.search({ index:'watcher', type:"watch", ELKstack 中文指南 573k4 插件 size:count }); } } }); }; 其中用到了两个 npm 模块,later 模块用来实现定时器和 crontab 文本解析,mustache 模块 用来渲染邮件内容模板,这也是 watcher 本身采用的渲染模块。 需要安装一下: npm install later npm install mustache 然后运行 ./bin/kibana ,就可以看到终端上除了原有的内容以外,还会定期输出 alert 的 email 内容了。 要点解释 这个极简示例中,主要有两段: 1. 注册为插件 module.exports = function (kibana) { return new kibana.Plugin({ init: function init(server) { 注意上一节的可视化插件,这块是: module.exports = function (kibana) { return new kibana.Plugin({ uiExports: { visTypes: [ 1. 引用 ES client init: function init(server) { var client = server.plugins.elasticsearch.client; ELKstack 中文指南 574k4 插件 这里通过调用 server.plugins 来直接引用 Kibana 里其他插件里的对象。这样,alert 插件就 可以跟其他功能共用同一个 ES client,免去单独配置自己的 ES 设置项和新开网络连接的资 源消耗。 ELKstack 中文指南 575k4 插件 Kibana 截图报表 ELK Stack 本身作为一个实时数据检索聚合的系统,在定期报表方面,是有一定劣势的。因 为基本上不可能把源数据长期保存在 Elasticsearch 集群中。即便保存了,为了一些已经成形 的数据,再全面查询一次过久的冷数据,也是有额外消耗的。那么,对这种报表数据的需 求,如何处理?其实很简单,把整个 Kibana 页面截图下来即可。 FireFox 有插件用来截全网页图。不过如果作为定期的工作,这么搞还是比较麻烦的,需要脚 本化下来。这时候就可以用上 phantomjs 软件了。phantomjs 是一个基于 webkit 引擎做的 js 脚本库。可以通过 js 程序操作 webkit 浏览器引擎,实现各种浏览器功能。 phantomjs 在 Linux 平台上没有二进制分发包,所以必须源代码编译: # yum -y install gcc gcc-c++ make flex bison gperf ruby \ openssl-devel freetype-devel fontconfig-devel libicu-devel sqlite-devel \ libpng-devel libjpeg-devel # git clone git://github.com/ariya/phantomjs.git # cd phantomjs # git checkout 2.0 # ./build.sh 想要给 kibana 页面截图,几行代码就够了。 capture-kibana.js 示例如下: var page = require('webpage').create(); var address = 'http://kibana.example.com/#/dashboard/elasticsearch/h5_view'; var output = 'kibana.png'; page.viewportSize = { width: 1366, height: 600 }; page.open(address, function (status) { if (status !== 'success') { console.log('Unable to load the address!'); phantom.exit(); } else { window.setTimeout(function () { page.render(output); phantom.exit(); }, 30000); } }); 然后运行 phantomjs capture-kibana.js 命令,就能得到截图生成的 kibana.png 图片了。 这里两个要点: 1. 要设置 viewportSize 里的宽度,否则效果会变成单个 panel 依次往下排列。 2. 要设置 setTimeout ,否则在获取完 index.html 后就直接返回了,只能看到一个大白板。 ELKstack 中文指南 576Kibana报表 用 phantomjs 截取 angularjs 这类单页 MVC 框架应用时一定要设置这个。 ELKstack 中文指南 577Kibana报表 ELK Stack 与 Hadoop 体系的区别 Kibana 因其丰富的图表类型和漂亮的前端界面,被很多人理解成一个统计工具。而我个人认 为,ELK 这一套体系,不应该和 Hadoop 体系同质化。定期的离线报表,不是 Elasticsearch 专长所在(多花费分词、打分这些步骤在高负载压力环境上太奢侈了),也不应该由 Kibana 来 完成(每次刷新都是重新计算)。Kibana 的使用场景,应该集中在两方面: 实时监控 通过 histogram 面板,配合不同条件的多个 queries 可以对一个事件走很多个维度组合出 不同的时间序列走势。时间序列数据是最常见的监控报警了。 问题分析 通过 Kibana 的交互式界面可以很快的将异常时间或者事件范围缩小到秒级别或者个位 数。期望一个完美的系统可以给你自动找到问题原因并且解决是不现实的,能够让你三 两下就从 TB 级的数据里看到关键数据以便做出判断就很棒了。这时候,一些非 histogram 的其他面板还可能会体现出你意想不到的价值。全局状态下看似很普通的结 果,可能在你锁定某个范围的时候发生剧烈的反方向的变化,这时候你就能从这个维度 去重点排查。而表格面板则最直观的显示出你最关心的字段,加上排序等功能。入库前 字段切分好,对于排错分析真的至关重要。 Splunk 场景参考 关于 elk 的用途,我想还可以参照其对应的商业产品 splunk 的场景: 使用 Splunk 的意义在于使信息收集和处理智能化。而其操作智能化表现在: 1. 搜索,通过下钻数据排查问题,通过分析根本原因来解决问题; 2. 实时可见性,可以将对系统的检测和警报结合在一起,便> 于跟踪 SLA 和性能问 题; 3. 历史分析,可以从中找出趋势和历史模式,行为基线和阈值,生成一致性报告。 -- Peter Zadrozny, Raghu Kodali 著/唐宏,陈健译《Splunk大数据分析》 ELKstack 中文指南 578竞品对比 推荐阅读 Elasticsearch 权威指南 精通 Elasticsearch The Logstash Book ELK Stack Advent ELKstack 中文指南 579推荐阅读 致谢 感谢 crazw 完成 collectd 插件介绍章节。 感谢 松涛 完成 nxlog 场景介绍章节。 感谢 LeiTu 完成 logstash-forwarder 介绍章节。 感谢 jingbli 完成 kafka 插件介绍章节。 感谢 林鹏 完成 ossec 场景介绍章节。 感谢 cameluo 完成 shield 介绍章节。 感谢 tuxknight 指出 hdfs 章节笔误。 感谢 lemontree 完成 marvel 介绍章节。 感谢 NERO 完成 utmp 插件开发介绍章节。 感谢 childe 完成 kibana-auth-CAS 介绍, elasticsearch 读写分离和别名等章节。 感谢 gnuhpc 完善 elasticsearch 插件 index 配置细节,logstash-fowarder-java 部署细 节。 感谢 东南 指出两处代码示例笔误。 感谢 陆一鸣 指出 openjdk 的 rpm 名错误。 感谢 elain 完成 search-guard 章节。 感谢 李宏旭 完成 hdfs 快照备份 章节。 ELKstack 中文指南 580合作名单 捐赠者名单 感谢以下童鞋的捐助,让我有动力持续下来并且注册了 http://kibana.logstash.es 这么个有意 思的域名用来发布本书: donor value 颜* 50.00 赵*远 10.00 韩*光 22.00 刘*卫 10.00 185**6322 5.00 彭*源 6.11 余* 100.00 李*灿 20.00 孙* 101.00 彭*根 50.00 史*春 10.00 张* 18.76 陈*林 66.00 吴*维 10.00 周* 10.00 叶* 50.00 张*贝 51.00 刘* 10.00 肖*宇 25.60 周* 100.00 高*达 10.00 庞* 30.00 林*光 10.00 张* 20.00 刘* 10.00 ELKstack 中文指南 581捐赠名单 周*文 131.21 俞* 24.80 宋* 4.01 吴*洪 15.00 王* 28.00 金* 110.00 段* 6.66 叶*荣 55.00 闫*苗 100.00 张*梁 20.00 董* 5.50 庄*宇 5.00 李*平 1000.0 wiki奇 10.00 没谱的曲 10.00 陈* 20.00 弓长小宝 147.52 彦达 20.00 剑 20.00 志超 100.00 焕良 10.24 allen 30.00 紧-^ ^ 10.00 正翔 20.00 戒小贤 100.00 纳音妙德 29.99 王先生 5.00 睿 16.60 勇勇 30.00 HugoYang 100.00 春林 30.00 承斌 10.00 ELKstack 中文指南 582捐赠名单 彦峰 30.00 冥炎 3.00 毓 5.00 小蜜蜂 20.00 锐 50.00 岛思族 38.00 奕庆 20.00 公孙 5.00 NoBody™ 6.66 王*勇 100.00 moonwolf 18.88 JoeZhu 70.00 云雾中的月光 10.00 Sandy 20.00 沈宽 20.00 萧田国 40.00 吕建飞 50.00 likuku 10.00 胖子黄 10.00 肖力 10.00 brad 10.00 ming 10.00 ATOM 10.00 TonyWu 10.00 言岳 10.00 海盗旗 20.00 丁天密 20.00 Aroline 50.00 李学明 20.00 余弦 50.00 卫向 10.00 ELKstack 中文指南 583捐赠名单 shuanggui 10.00 Jacob.Yu 10.00 陈洋 10.00 吴晓刚 10.00 肖平Jacky 10.00 马亮 10.00 袁乐天 10.00 魏振华 100.00 刘宇 50.00 吕召刚 20.00 Bin 10.00 华仔 20.00 山野白丁 20.00 陈自欣 10.00 我叫于小炳 20.00 军呐 13.00 吴顺超 50.00 共计:4169.54 元,域名注册已用 275.50 元(3 年),QQ群升级已用 388.00 元(1 年)。 ELKstack 中文指南 584捐赠名单
    还剩583页未读

    继续阅读

    下载pdf到电脑,查找使用更方便

    pdf的实际排版效果,会与网站的显示效果略有不同!!

    需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

    下载pdf

    pdf贡献者

    licanli

    贡献于2016-04-26

    下载需要 10 金币 [金币充值 ]
    亲,您也可以通过 分享原创pdf 来获得金币奖励!