12/22
2015

ELK Stack Advent Calendar Day 22


网友们多次讨论如何利用 ES 计算用户留存率的问题。这是个比较尴尬的情况,如果多次请求再自己做一下运算,问题很简单。但如果想要一次请求得到最终结果,在没有完整 JOIN 支持的 ES 里又显得比较难以完成。

目前我想到的比较容易达成的做法,是我们在记录用户登录操作日志的时候,把该用户的注册时间也同期输出。也就是说,这个索引的 mapping 是下面这样:

curl -XPUT 'http://127.0.0.1:9200/login-2015.12.23/' -d '{
  "settings" : {
    "number_of_shards" : 1
  },
  "mappings" : {
    "logs" : {
      "properties" : {
        "uid" : { "type" : "string", "index" : "not_analyzed" },
        "register_time" : { "type" : "date", "index" : "not_analyzed" },
        "login_time" : { "type" : "date", "index" : "not_analyzed" }
      }
    }
  }
}'

那么实际记录的日志会类似这样:

{"index":{"_index":"login-2015.12.23","_type":"logs"}}
{"uid":"1","register_time":"2015-12-23T12:00:00Z","login_time":"2015-12-23T12:00:00Z"}
{"index":{"_index":"login-2015.12.23","_type":"logs"}}
{"uid":"2","register_time":"2015-12-23T12:00:00Z","login_time":"2015-12-23T12:00:00Z"}
{"index":{"_index":"login-2015.12.24","_type":"logs"}}
{"uid":"1","register_time":"2015-12-23T12:00:00Z","login_time":"2015-12-24T12:00:00Z"}

这段我虚拟的数据,表示 uid 为 1 的用户,23 号注册并登录,24 号再次登录;uid 为 2 的用户,23 号注册并登录,24 号无登录。

显然以这短短 3 行示例数据,我们口算都知道单日留存率是 50% 了。那么怎么通过一次 ES 请求也算出来呢?下面就要用到 ES 2.0 新增加的 pipeline aggregation 了。

curl -XPOST 'http://127.0.0.1:9200/login-2015.12.23,login-2015.12.24/_search' -d'
{
  "size" : 0,
  "aggs" : {
    "per_day" : {
      "date_histogram" : {
        "field":"register_time",
        "format":"yyyy-MMM-dd",
        "interval":"day"
      },
      "aggs" : {
        "register_count" : {
          "cardinality" : {
            "field" : "uid"
          }
        },
        "today" : {
          "filter" : {
            "range" : {
              "login_time" : {
                "gte" : "now-1d",
                "lt" : "now"
              }
            }
          },
          "aggs" : {
            "login_count" : {
              "cardinality" : {
                "field" : "uid"
              }
            }
          }
        },
        "retention" : {
          "bucket_script" : {
            "buckets_path" : {
              "today_count" : "today>login_count",
              "yesterday_count" : "register_count"
            },
            "script" : {
              "lang" : "expression",
              "inline" : "today_count / yesterday_count"
            }
          }
        }
      }
    }
  }
}'

这个 pipeline aggregation 在使用上有几个要点:

  • pipeline agg 的 parent agg 必须是返回数组的 buckets agg 类型。我这里曾经打算使用 filter agg 直接请求 register_time:["now-2d" TO "now-1d"],结果报错说找不到 buckets_path 的 START_OBJECT。
  • bucket_script agg 同样受 scripting module 的影响。也就是说,官网示例里的 "script":"today_count / yesterday_count" 这种写法,是采用了 groovy 引擎的 inline 模式。在 ES 2.0 的默认设置下,是被禁止运行的!所以,应该按照 scripting module 的统一要求,改写成 file 形式存放到 config/scripts 下;或者改用 Lucene Expression 运行。考虑到 pipeline aggregation 只支持数值运算,这里使用 groovy 价值不大,所以直接指明 lang 参数即可。

最终这次请求的响应如下:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "per_day" : {
      "buckets" : [ {
        "key_as_string" : "2015-Dec-23",
        "key" : 1450828800000,
        "doc_count" : 3,
        "today" : {
          "doc_count" : 1,
          "login_count" : {
            "value" : 1
          }
        },
        "register_count" : {
          "value" : 2
        },
        "retention" : {
          "value" : 0.5
        }
      } ]
    }
  }
}

这个 retention 数据,就是我们要求解的 0.5 了。

12/21
2015

ELK Stack Advent Calendar Day 21


我们都知道Kibana4里,所有的aggregation生成的visualize都可以在请求细节查看里选择Export成raw或者formatted。其中formatted就是CSV文件。

但是Discover页上,除了顶部的date_histogram这个visualize,更重要的是下边的search document table的内容。当我们通过搜索发现异常信息,想要长期保存证据,或者分享给其他没有权限的外部人员的时候,单纯保存search到es,或者分享单条日志的link都不顶用,还是需要能导出成一个文件。

可惜Kibana4没有针对search document table的导出!

国外一家叫MineWhat的公司,最近公开了一个非常细小的创新方案,意图解决这个问题。他们的方式是:避免修改Kibana源码,而通过chrome浏览器插件完成……

点击这个地址安装chrome插件:https://chrome.google.com/webstore/detail/elasticsearch-csv-exporte/kjkjddcjojneaeeppobfolgojhohbpjn/related

然后再访问Kibana的时候,你会发现自己的搜索框最右侧多了一个CSV按钮:

然后点击这个『CSV』按钮,会弹出一片提示:

可以点击选择,把search document table内容保存到本机的复制粘贴板,还是Google Drive网盘。 我们当然选择本机…… 然后打开本地的文本文件,Ctrl+V,就看到编辑器里出现了整个CSV内容。 实测下来,发现有个小问题,粘贴出来的数据里丢掉了空格~不过聊胜于无吧,还是介绍给大家一试。

注意:这个功能只会导出目前页面上已经展示出来的table内容。并不代表其使用了scroll API去ES拉取全部结果集!

12/20
2015

ELK Stack Advent Calendar Day 20


本文作者childe

事情是这样滴, 我们在很多linux机器上部署了logstash采集日志, topic_id用的是 test-%{type}, 但非常不幸的是, 有些机器的某些日志, 没有带上type字段.

因为在topic名字里面不能含有%字符, 所以kafka server的日志里面大量报错. Logstash每发一次数据, kafka就会生成下面一大段错误

[2015-12-23 23:20:47,749] ERROR [KafkaApi-0] error when handling request Name: TopicMetadataRequest; Version: 0; CorrelationId: 48; ClientId: ; Topics: test-%{type} (kafka.server.KafkaApis)
kafka.common.InvalidTopicException: topic name test-%{type} is illegal, contains a character other than ASCII alphanumerics, '.', '_' and '-'
    at kafka.common.Topic$.validate(Topic.scala:42)
    at kafka.admin.AdminUtils$.createOrUpdateTopicPartitionAssignmentPathInZK(AdminUtils.scala:181)
    at kafka.admin.AdminUtils$.createTopic(AdminUtils.scala:172)
    at kafka.server.KafkaApis$$anonfun$19.apply(KafkaApis.scala:520)
    at kafka.server.KafkaApis$$anonfun$19.apply(KafkaApis.scala:503)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
    at scala.collection.immutable.Set$Set1.foreach(Set.scala:74)
    at scala.collection.TraversableLike$class.map(TraversableLike.scala:244)
    at scala.collection.AbstractSet.scala$collection$SetLike$$super$map(Set.scala:47)
    at scala.collection.SetLike$class.map(SetLike.scala:93)
    at scala.collection.AbstractSet.map(Set.scala:47)
    at kafka.server.KafkaApis.getTopicMetadata(KafkaApis.scala:503)
    at kafka.server.KafkaApis.handleTopicMetadataRequest(KafkaApis.scala:542)
    at kafka.server.KafkaApis.handle(KafkaApis.scala:62)
    at kafka.server.KafkaRequestHandler.run(KafkaRequestHandler.scala:59)
    at java.lang.Thread.run(Thread.java:744)

把可用的信息瞬间淹没.

更不幸的是, 错误日志里面并没有客户来源的信息, 根本不知道是哪些机器还有问题.

我想做的, 就是把有问题的logstash机器找出来.

我就先事后诸葛亮一把, 用下面这个命令就可以把配置错误的机器找出来(也可以没有任何结果, 原因后面说)

tcpdump -nn 'dst port 9092 and tcp[37]==3 and tcp[57]==37'

dst port 9092就不说了, 这是kafka的默认端口, 后面的tcp[37]==3 and tcp[57]==37是啥意思呢, 我们慢慢说.

先要说一下: client要生产数据到kafka, 在发送消息之前, 首先得向kafka"询问"这个topic的metadata信息, 包括有几个partiton, 每个parttion在哪个服务器上面等信息, 拿到这些信息之后, 才能把消息发到正确的kafka服务器上.

重点来了! 向kafka"询问"topic的metadata, 其实就是发送一个tcp包过去, 我们需要知道的是这个tcp包的格式. 我已经帮你找到了, 就在这里 https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-TopicMetadataRequest

看完文档之后(半小时或者更长时间过去了), 你就会知道, tcp body(除去tcp head)里面的第6个字节是03, 代表这是一个TopicMetadataRequest请求. topicname里面的%字符出现在tcp body的第26个字节, %的ascii码是37

tcp头一般是20个字符, 所以加上这20个字节, 然后下标从0算起, 就是tcp[20+5]==3 and tcp[20+25]==37, 也就是 tcp[25]==3 and tcp[45]==37.

咦, 为啥和开始写的那个过滤条件不一样呢, 因为tcp头"一般"是20字节, 但是如果其中还包含了tcp选项的话, 就可能比20多了. 反正我这里看到的的tcp头都是32个字节, 所以不能加20, 要加32, 也就是最开始写的 tcp[37]==3 and tcp[57]==37.

最后呢, 再提2点结束.

  1. 终极大杀器, 不过tcp头的长度是多少, 20也好, 32也好, 或者其他也好, 下面这样都能搞定 tcpdump -nn 'dst port 9092 and tcp[(tcp[12]>>2)+5]==3 and tcp[(tcp[12]>>2)+25]==37'

  2. 不要一上来就这么高端, 其实我最开始是这样先确定问题的 tcpdump -vv -nn -X -s 0 dst port 9092 | grep -C 5 "test-"

你问我为啥不把test-%{type}写完整? 不是为了省事, 其实是因为很不幸, test-%{t 到这里的时候, 正好换行了.

12/19
2015

ELK Stack Advent Calendar Day 19


本文作者wood

“该给ES分配多少内存?” “JVM参数如何优化?“ “为何我的Heap占用这么高?” “为何经常有某个field的数据量超出内存限制的异常?“ “为何感觉上没多少数据,也会经常Out Of Memory?”

以上问题,显然没有一个统一的数学公式能够给出答案。 和数据库类似,ES对于内存的消耗,和很多因素相关,诸如数据总量、mapping设置、查询方式、查询频度等等。默认的设置虽开箱即用,但不能适用每一种使用场景。作为ES的开发、运维人员,如果不了解ES对内存使用的一些基本原理,就很难针对特有的应用场景,有效的测试、规划和管理集群,从而踩到各种坑,被各种问题挫败。

要理解ES如何使用内存,先要尊重下面两个基本事实:

  1. ES是JAVA应用
  2. 底层存储引擎是基于Lucene的

看似很普通是吗?但其实没多少人真正理解这意味着什么。

首先,作为一个JAVA应用,就脱离不开JVM和GC。很多人上手ES的时候,对GC一点概念都没有就去网上抄各种JVM“优化”参数,却仍然被heap不够用,内存溢出这样的问题搞得焦头烂额。了解JVM GC的概念和基本工作机制是很有必要的,本文不在此做过多探讨,读者可以自行Google相关资料进行学习。如何知道ES heap是否真的有压力了? 推荐阅读这篇博客:Understanding Memory Pressure Indicator。 即使对于JVM GC机制不够熟悉,头脑里还是需要有这么一个基本概念: 应用层面生成大量长生命周期的对象,是给heap造成压力的主要原因,例如读取一大片数据在内存中进行排序,或者在heap内部建cache缓存大量数据。如果GC释放的空间有限,而应用层面持续大量申请新对象,GC频度就开始上升,同时会消耗掉很多CPU时间。严重时可能恶性循环,导致整个集群停工。因此在使用ES的过程中,要知道哪些设置和操作容易造成以上问题,有针对性的予以规避。

其次,Lucene的倒排索引(Inverted Index)是先在内存里生成,然后定期以段文件(segment file)的形式刷到磁盘的。每个段实际就是一个完整的倒排索引,并且一旦写到磁盘上就不会做修改。 API层面的文档更新和删除实际上是增量写入的一种特殊文档,会保存在新的段里。不变的段文件易于被操作系统cache,热数据几乎等效于内存访问。

基于以上2个基本事实,我们不难理解,为何官方建议的heap size不要超过系统可用内存的一半。heap以外的内存并不会被浪费,操作系统会很开心的利用他们来cache被用读取过的段文件。

Heap分配多少合适?遵从官方建议就没错。 不要超过系统可用内存的一半,并且不要超过32GB。JVM参数呢?对于初级用户来说,并不需要做特别调整,仍然遵从官方的建议,将xms和xmx设置成和heap一样大小,避免动态分配heap size就好了。虽然有针对性的调整JVM参数可以带来些许GC效率的提升,当有一些“坏”用例的时候,这些调整并不会有什么魔法效果帮你减轻heap压力,甚至可能让问题更糟糕。

那么,ES的heap是如何被瓜分掉的? 说几个我知道的内存消耗大户并分别做解读:

  1. segment memory
  2. filter cache
  3. field data cache
  4. bulk queue
  5. indexing buffer
  6. state buffer
  7. 超大搜索聚合结果集的fetch

Segment Memory

Segment不是file吗?segment memory又是什么?前面提到过,一个segment是一个完备的lucene倒排索引,而倒排索引是通过词典 (Term Dictionary)到文档列表(Postings List)的映射关系,快速做查询的。 由于词典的size会很大,全部装载到heap里不现实,因此Lucene为词典做了一层前缀索引(Term Index),这个索引在Lucene4.0以后采用的数据结构是FST (Finite State Transducer)。 这种数据结构占用空间很小,Lucene打开索引的时候将其全量装载到内存中,加快磁盘上词典查询速度的同时减少随机磁盘访问次数。

下面是词典索引和词典主存储之间的一个对应关系图:

Lucene file的完整数据结构参见Apache Lucene - Index File Formats

说了这么多,要传达的一个意思就是,ES的data node存储数据并非只是耗费磁盘空间的,为了加速数据的访问,每个segment都有会一些索引数据驻留在heap里。因此segment越多,瓜分掉的heap也越多,并且这部分heap是无法被GC掉的! 理解这点对于监控和管理集群容量很重要,当一个node的segment memory占用过多的时候,就需要考虑删除、归档数据,或者扩容了。

怎么知道segment memory占用情况呢? CAT API可以给出答案。

  • 查看一个索引所有segment的memory占用情况:

  • 查看一个node上所有segment占用的memory总和:

那么有哪些途径减少data node上的segment memory占用呢? 总结起来有三种方法:

  1. 删除不用的索引
  2. 关闭索引 (文件仍然存在于磁盘,只是释放掉内存)。需要的时候可以重新打开。
  3. 定期对不再更新的索引做optimize (ES2.0以后更改为force merge api)。这Optimze的实质是对segment file强制做合并,可以节省大量的segment memory。

Filter Cache

Filter cache是用来缓存使用过的filter的结果集的,需要注意的是这个缓存也是常驻heap,无法GC的。我的经验是默认的10% heap设置工作得够好了,如果实际使用中heap没什么压力的情况下,才考虑加大这个设置。

Field Data cache

在有大量排序、数据聚合的应用场景,可以说field data cache是性能和稳定性的杀手。 对搜索结果做排序或者聚合操作,需要将倒排索引里的数据进行解析,然后进行一次倒排。 这个过程非常耗费时间,因此ES 2.0以前的版本主要依赖这个cache缓存已经计算过的数据,提升性能。但是由于heap空间有限,当遇到用户对海量数据做计算的时候,就很容易导致heap吃紧,集群频繁GC,根本无法完成计算过程。 ES2.0以后,正式默认启用Doc Values特性(1.x需要手动更改mapping开启),将field data在indexing time构建在磁盘上,经过一系列优化,可以达到比之前采用field data cache机制更好的性能。因此需要限制对field data cache的使用,最好是完全不用,可以极大释放heap压力。 需要注意的是,很多同学已经升级到ES2.0,或者1.0里已经设置mapping启用了doc values,在kibana里仍然会遇到问题。 这里一个陷阱就在于kibana的table panel可以对所有字段排序。 设想如果有一个字段是analyzed过的,而用户去点击对应字段的排序表头是什么后果? 一来排序的结果并不是用户想要的,排序的对象实际是词典; 二来analyzed过的字段无法利用doc values,需要装载到field data cache,数据量很大的情况下可能集群就在忙着GC或者根本出不来结果。

Bulk Queue

一般来说,Bulk queue不会消耗很多的heap,但是见过一些用户为了提高bulk的速度,客户端设置了很大的并发量,并且将bulk Queue设置到不可思议的大,比如好几千。 Bulk Queue是做什么用的?当所有的bulk thread都在忙,无法响应新的bulk request的时候,将request在内存里排列起来,然后慢慢清掉。 这在应对短暂的请求爆发的时候有用,但是如果集群本身索引速度一直跟不上,设置的好几千的queue都满了会是什么状况呢? 取决于一个bulk的数据量大小,乘上queue的大小,heap很有可能就不够用,内存溢出了。一般来说官方默认的thread pool设置已经能很好的工作了,建议不要随意去“调优”相关的设置,很多时候都是适得其反的效果。

Indexing Buffer

Indexing Buffer是用来缓存新数据,当其满了或者refresh/flush interval到了,就会以segment file的形式写入到磁盘。 这个参数的默认值是10% heap size。根据经验,这个默认值也能够很好的工作,应对很大的索引吞吐量。 但有些用户认为这个buffer越大吞吐量越高,因此见过有用户将其设置为40%的。到了极端的情况,写入速度很高的时候,40%都被占用,导致OOM。

Cluster State Buffer

ES被设计成每个node都可以响应用户的api请求,因此每个node的内存里都包含有一份集群状态的拷贝。这个cluster state包含诸如集群有多少个node,多少个index,每个index的mapping是什么?有少shard,每个shard的分配情况等等 (ES有各类stats api获取这类数据)。 在一个规模很大的集群,这个状态信息可能会非常大的,耗用的内存空间就不可忽视了。并且在ES2.0之前的版本,state的更新是由master node做完以后全量散播到其他结点的。 频繁的状态更新都有可能给heap带来压力。 在超大规模集群的情况下,可以考虑分集群并通过tribe node连接做到对用户api的透明,这样可以保证每个集群里的state信息不会膨胀得过大。

超大搜索聚合结果集的fetch

ES是分布式搜索引擎,搜索和聚合计算除了在各个data node并行计算以外,还需要将结果返回给汇总节点进行汇总和排序后再返回。无论是搜索,还是聚合,如果返回结果的size设置过大,都会给heap造成很大的压力,特别是数据汇聚节点。超大的size多数情况下都是用户用例不对,比如本来是想计算cardinality,却用了terms aggregation + size:0这样的方式; 对大结果集做深度分页;一次性拉取全量数据等等。

小结

  1. 倒排词典的索引需要常驻内存,无法GC,需要监控data node上segment memory增长趋势。
  2. 各类缓存,field cache, filter cache, indexing cache, bulk queue等等,要设置合理的大小,并且要应该根据最坏的情况来看heap是否够用,也就是各类缓存全部占满的时候,还有heap空间可以分配给其他任务吗?避免采用clear cache等“自欺欺人”的方式来释放内存。
  3. 避免返回大量结果集的搜索与聚合。缺失需要大量拉取数据可以采用scan & scroll api来实现。
  4. cluster stats驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过tribe node连接。
  5. 想知道heap够不够,必须结合实际应用场景,并对集群的heap使用情况做持续的监控。
12/18
2015

ELK Stack Advent Calendar Day 18


本文作者childe

在logstash内部, input到filter, 以及filter到output, 消息都是通过一个队列来中转.

在我写hangout的第一个版本,也是这么做的,用ArrayBlockingQueue来中转消息, 上游几个线程把消息放在queue中, 下游再几个线程把queue中的消息消费走.

但是, 用下来之后, 发现在queue上面消耗的资源是相当的大,strace查看,非常大量的lock相关的系统调用, 现在的版本已经把queue去掉了. 想必Logstash也会有大量资源用在这一块.

zeromq中的Parallel Pipeline正好适合这个场景,而且文档中说是lock free的, 拿来和queue对比一下看.

在我自己的电脑上测试,2.6 GHz Intel Core i5. 一个主线程生成10,000,000个随机数, 分发给四个线程消费.

用Queue来实现, 需要约37秒, CPU使用率在150%. 用zeromq的ipc来传递消息, 只需要22秒, 期间CPU使用率在250%. 总的CPU使用时间都60秒左右.

不知道java中还有没有更合适的Queue可以用在这个场景中.至少zeromq和ArrayBlockingQueue相比, zeromq可以更快的处理消息, 但代价就是更高的CPU使用率.

12/17
2015

ELK Stack Advent Calendar Day 17


本文作者childe

除了应用在日志系统外, 越来越多的业务数据也接入ES, 利用它天生强大的搜索性能和分布式可扩展, 可以为业务的精确快速灵活的搜索提供极大便利, 我觉得这是未来一个很好的方向.

但是, 对它ES各种各样的搜索方式, 你了解了吗?

我们来看几个"奇怪"的搜索.

奇怪的打分

奇怪的打分1

我们有个数据结构是

{
    "first_name":"string",
    "last_name":"string"
}

插入了几条数据, 有诸葛亮 诸葛明 诸葛暗 诸葛黑, 还有个人名字很奇怪, 叫司马诸葛.

然后我们要搜索诸葛瑾, 虽然索引里面没有一个人叫这个名字, 但搜索出来诸葛亮也不错, 他们名字这么像, 说不定是亲兄弟, 可以顺藤摸瓜, 找到我们需要的信息呢.

{
    "query": {
        "multi_match": {
            "query":       "诸葛瑜",
            "type":        "most_fields",
            "fields":      [ "*_name" ]
        }
    }
}

但实际上呢, 司马诸葛这个人居然稳居搜索榜首位, 他是搞竞价排名了吧? 你知道其中的打分原理吗?

奇怪的打分2

我们有两条数据:

PUT /my_index/my_type/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}
PUT /my_index/my_type/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

要搜索

{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

第二条文档里面明确含有"brown fox"这个词组, 但是它的搜索得分比较低, 你知道为啥吗?

and用在哪

{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "most_fields",
            "operator":    "and",
            "fields":      [ "first_name", "last_name" ]
        }
    }
}

你知道这个and代表什么吗?

是说

A: 姓和名里面都要含有"peter smith",

还是说

B: 姓或者名里面要包含peter以及smith ?

还有, 怎么才能获得另外一个效果呢?

列表中的元素

我们有一条数据如下(按汉语分词)

{
    "时代":"三国",
    "姓名": ["大司马","诸葛亮"]
}

我以词组的方式搜索:

{
    "query": {
        "match_phrase": {
            "姓名": "司马诸葛"
        }
    }
}

能搜索到吗?

上面这些其实都是elasticsearch Definitive Guide里面的几个小例子, 欢迎大家继续去那里寻找答案和其他各种小技巧.

12/16
2015

ELK Stack Advent Calendar Day 16


本文作者childe

看到前一天, Medcl 介绍了Beat, 我想今天我就介绍一下算是同一个领域的, 我们的一个小产品吧, 同样也基于elastic旗下的logstash-forwarder. 我真的不是来打广告的, 就是第一次写, 没经验, 看着前一天的文章, 顺手就想到了.

在日志收集系统中, 从kafkf到ES这条路是没问题了, 但散布在各个服务器上采集日志的agent用logstash实在是太重了, 而且效率也低. 特别是我们有大量的windows服务器, 找一个合适的agent居然不是想象中的容易.

logstash-forwarder对于日志文件的探测和offset记录, deadtime等配置都非常适合我们, 但惟一不支持吐数据到kafak,对我们来说是一个遗憾. 我和oliver做过一点改造之后, 让她支持了这个功能.

目前我们所有iis服务器已经部署了这个应用, 效率高, 占资源小, 可以数据压缩, 支持简单的格式切割, 实乃windows居家必备(我真不是来打广告的). golang客户端, 还能直接发送到kafka, 想想就很贴心~

贴上一段配置瞅瞅先, 启一个进程采集nginx和tomcat日志, 分别吐到kafka的2个topic中.

{
  "files": [
    {
      "paths": [
        "/var/log/nginx/*.log"
      ],
      "Fields":{
        "type":"nginx"
      },
      "DeadTime": "30m"
    },
    {
      "paths": [
        "/var/log/tomcat/*.log",
        "/var/log/tomcat/*/*.log"
      ],
      "Fields":{
        "type":"tomcat"
      },
      "DeadTime": "30m"
    }
  ],
  "kafka": {
    "broker_list": ["10.0.0.1:9092","10.0.0.2:9092"],
    "topic_id": "topic_name_change_it_",
    "compression_codec": "gzip"
  }
}

再简单介绍一下参数吧,

  • DeadTime:30m 是说超过30分钟没有更新, 就不会再继续跟踪这个文件了(退出goroutine)
  • "Fields":{ "type":"tomcat" } , 会在每条日志中增加配置的字段
  • path目前就是用的golang官方库, 好像是还不支持递归多层目录查找, 反正我翻了一下文档, 没有找到.

grok还不支持, 但简单的分割是可以的

"files": [
  {
    "paths": [
      "d:\\target.txt"
    ],
    "FieldNames": ["datetime", "datetime", "s_ip", "cs_method", "cs_uri_stem", "cs_uri_query", "s_port", "time_taken"],
    "Delimiter": "\\s+",
    "QuoteChar": "\""
  }
]

以上配置就是说按空白符把日志切割来, 塞到对应的字段中去. 第一个第二个合在一起, 放在datetime字段中.

其实还是有不少要完善的地方, 比如说没有带上机器的Hostname, 以及日志的路径. 在很多时候, 这些信息还是很有用的, 我们也会继续完善.

现在放在了https://github.com/childe/logstash-forwarder/tree/kafka, 有需要的同学,可以去看下.

12/15
2015

ELK Stack Advent Calendar Day 15


本文作者:Medcl

Advent接力传到我这里了,今天我给大家介绍一下Beats,刚好前几天也有好多人问我它是干嘛的,之前的上海我有分享过Beats的内容,PPT在这里:http://pan.baidu.com/s/1hrtHL0C

事实上Beats是一系列产品的统称,属于ElasticStack里面收集数据的这一层:Data Shipper Layer,包括以下若干Beats:

  • PacketBeat,用来嗅探和分析网络流量,如HTTP、MySQL、Redis等
  • TopBeat,用来收集系统的监控信息,功能如其名,类似*nix下的top命令,只不过所有的信息都会发送给后端的集中存储:Elasticsearch,这样你就可以很方便的监控所有的服务器的运行情况了
  • FileBeat,用来收集数据源是文件的数据,比如常见的系统日志、应用日志、网站日志等等,FIleBeat思路来自Logstash-forwarder,Beats团队加入之后重构改写而成,解决的就是Logstash作为Agent采集时占用太多被收集系统资源的问题,Beats家族都是Golang编写,效率高,占用内存和CPU比较少,非常适合作为agent跑着服务器上

所以Beats其实是一套框架,另外的一个子项目Libbeat,就是所有beats都共用的模块,封装了所有的公共的组件,如配置管理、公共基础类、协议的解析处理、与Elasticsearch的操作等等,你可以很方便基于它实现你自己的beats,这也是Beats的目标,希望将来会出现更多的Beats,做各种各样的事情。

另外PacketBeat比较特殊,它又是网络协议抓包和处理的一个框架,目前支持了常见的一些协议,要扩展未知的协议其实非常简单,PacketBeat作为一个框架,数据抓包和后续的存储已经帮你处理好了,你只需要实现你的协议的解码操作就行了,当然这块也是最难和最业务相关的。

关于PacketBeat我回头再单独写一篇文章来介绍怎样编写一个PacketBeat的协议扩展吧,PacketBeat扩展的其它协议最终还是需要和PacketBeat集成在一起,也就是最终你的代码是要和PacketBeat的代码在一个工程里面的,而其它的Beats使用Libbeat完全是单独的Beat,如Filebeat和TopBeat,完全是独立打包和独立运行,这个也是两大Beats的主要区别。

随便提一下,现在所有的这些Beats已经合并到一个项目里面来方便管理了,golang,you know:https://github.com/elastic/beats

现在社区已经提交了的Beats: https://www.elastic.co/guide/en/beats/libbeat/current/community-beats.html

明后天在Beijing的ArchSummit2015,我将在Elastic展台,欢迎过来骚扰,领取Elastic的各种贴纸,还有限量的印有Elastic的T恤,数量有限哦

今天的Advent就这些吧。

12/14
2015

ELK Stack Advent Calendar Day 14


我们都知道 Elasticsearch 除了普通的 search 接口以外,还有另一个 Percolator 接口,天生用来做实时过滤告警的。但是由于接口比较复杂,在目前的 ELK 体系中不是很容易运用。

而单纯从 Logstash 来做实时过滤报警,规则又不是很灵活。toplog.io 公司开发了一个 logstash-output-percolator 插件,在有一定既定条件的情况下,成功运用上了 Percolator 方案。

这个插件的设计逻辑是:

  • 通过 logstash-filter-checksum 自主生成 ES 文档的 _id
  • 使用上一步生成的 _id 同时发送 logstash-output-elasticsearch 和 logstash-output-percolator
  • Percolator 接口一旦过滤成功,将 _id 发送给 Redis 服务器
  • 其他系统从 Redis 服务器中获取 _id 即可从 ES 里拿到实际数据

Percolator 接口的用法简单说是这样:

  1. 创建接口:
curl -XPUT 'localhost:9200/patterns/.percolator/my-pattern-id' -d '{"query" : {"match" : {"message" : "ERROR"} } }'
  1. 过滤测试:
curl -XGET 'localhost:9200/my-index/my-type/_percolate' -d '{"doc" : {"message" : "ERROR: Service Apache failed to connect to MySQL"} }'

要点就是把文档放在 doc 属性里发送到 _percolate 里。

对应的 Logstash 配置如下:

filter {
    checksum {
        algorithm => "md5"
        keys => ["message"]
    }
}
output {
    elasticsearch {
        host => "localhost"
        cluster => "my-cluster"
        document_id => "%{logstash_checksum}"
        index => "my-index"
    }
    percolator {
        host => "es-balancer"
        redis_host => ["localhost"]
        document_id => "%{logstash_checksum}"
        pattern_index => "patterns"
    }
}

连接上对应的 Redis,就可以看到报警信息了:

$ redis-cli
127.0.0.1:6379> lrange percolator 0 1
1) "{\"matches\":[\"2\"],\"document_id\":\"a5d5c5f69b26ac0597370c9b1e7a8111\"}"

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/13
2015

ELK Stack Advent Calendar Day 13


Geo 定位在 ELK 应用中是非常重要和有用的一个环节。不幸的是:GeoIP 本身在国内的准确度实在堪忧。高春辉近年成立了一个项目,专注收集细化 IP 地址在国内的数据:http://www.ipip.net。数据分为免费版和收费版两种。项目提供了不少客户端,有趣的是,有社区贡献了一个 Logstash 插件:https://github.com/bittopaz/logstash-filter-ipip

用法很简单:

filter {
    ipip {
        source => "clientip"
        target => "ipip"
    }
}

生成的 JSON 数据结构类似下面这样:

{
    "clientip" : "",
    "ipip" : {
        "country" : "",
        "city" : "",
        "carrier" : "",
        "province" : ""
    }
}

不过这个插件只实现了收费版的数据库基础格式。免费版的支持,收费版高级的经纬度、基站位置等,都没有随着更新。事实上,我们可以通过 ipip 官方的 Java 库,实现一个更灵活的 logstash-filter-ipip_java 插件出来,下期见。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/12
2015

ELK Stack Advent Calendar Day 12


很多从 MySQL 转过来的 Elasticsearch 用户总是很习惯的问一个问题:『怎么在 ES 里实现 join 操作?』过去,我们的回答一般都是:通过类似宽表的思路,将数据平铺在一个索引里。不过,最近另一家 Lucene 开发商给出了另一个方案,他们开发了一个 Elasticsearch 插件,实现了 filter 层面的 join,GitHub 项目地址见: https://github.com/sirensolutions/siren-join

不过需要提醒一下的是:filter 层面的意思,就是只相当于是 SQL 里的 exists 操作。所以目前对这个插件也不要抱有太大期望。今天我们来稍微演示一下。

安装和其他 ES 插件一样:

# bin/plugin -i solutions.siren/siren-join/1.0

注意 siren-join v1.0 只支持 ES 1.7 版本,2.0 版本支持据说正在开发中。

我们 bulk 上传这么一段数据:

{"index":{"_index":"index1","_type":"type","_id":"1"}}
{"id":1, "foreign_key":"13"}
{"index":{"_index":"index1","_type":"type","_id":"2"}}
{"id":2}
{"index":{"_index":"index1","_type":"type","_id":"3"}}
{"id":3, "foreign_key": "2"}
{"index":{"_index":"index1","_type":"type","_id":"4"}}
{"id":4, "foreign_key": "14"}
{"index":{"_index":"index1","_type":"type","_id":"5"}}
{"id":5, "foreign_key": "2"}
{"index":{"_index":"index2","_type":"type","_id":"1"}}
{"id":"1", "tag": "aaa"}
{"index":{"_index":"index2","_type":"type","_id":"2"}}
{"id":"2", "tag": "aaa"}
{"index":{"_index":"index2","_type":"type","_id":"3"}}
{"id":"3", "tag": "bbb"}
{"index":{"_index":"index2","_type":"type","_id":"4"}}
{"id":"4", "tag": "ccc"}

注意,siren-join 要求用来 join 的字段必须数据类型一致。所以,当我们要用 index2 的 id 和 index1 的 foreign_key 做 join 的时候,这两个字段就要保持一致,这里为了演示,特意都改成字符串。那么我们发起一个请求如下:

# curl -s -XPOST 'http://localhost:9200/index1/_coordinate_search?pretty' -d '
{
    "query":{
        "filtered":{
            "query":{
                "match_all":{}
            },
            "filter":{
                "filterjoin":{
                    "foreign_key":{
                        "index":"index2",
                        "type":"type",
                        "path":"id",
                        "query":{
                            "terms":{
                                "tag":["aaa"]
                            }
                        }
                    }
                }
            }
        }
    },
    "aggs":{
        "avg":{
            "avg":{
                "field":"id"
            }
        }
    }
}'

意即:从 index2 中搜索 q=tag:aaa 的数据的 id,查找 index1 中对应 foreign_key 的文档的 id 数据平均值。响应结果如下:

{
    "coordinate_search" : {
        "actions" : [ {
            "relations" : {
                "from" : {
                    "indices" : [ ],
                    "types" : [ ],
                    "field" : "id"
                },
                "to" : {
                    "indices" : null,
                    "types" : null,
                    "field" : "foreign_key"
                }
            },
            "size" : 2,
            "size_in_bytes" : 20,
            "is_pruned" : false,
            "cache_hit" : true,
            "took" : 0
        } ]
    },
    "took" : 2,
    "timed_out" : false,
    "_shards" : {
        "total" : 5,
        "successful" : 5,
        "failed" : 0
    },
    "hits" : {
        "total" : 2,
        "max_score" : 1.0,
        "hits" : [ {
            "_index" : "index1",
            "_type" : "type",
            "_id" : "5",
            "_score" : 1.0,
            "_source":{"id":5, "foreign_key": "2"}
        }, {
            "_index" : "index1",
            "_type" : "type",
            "_id" : "3",
            "_score" : 1.0,
            "_source":{"id":3, "foreign_key": "2"}
        } ]
    },
    "aggregations" : {
        "avg" : {
            "value" : 4.0
        }
    }
}

响应告诉我们:从 index2 中搜索到 2 条参与 join 的文档,在 index1 中命中 2 条数据,最后求平均值为 4.0。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/11
2015

ELK Stack Advent Calendar Day 11


ES2.0 开始提供了一个崭新的 pipeline aggregation 特性,但是 Kibana 似乎并没有立刻跟进这方面的意思,相反,Elastic 公司推出了另一个实验室产品:Timelion

timelion 的用法在官博里已经有介绍。尤其是最近两篇如何用 timelion 实现异常告警的文章,更是从 ES 的 pipeline aggregation 细节和场景一路讲到 timelion 具体操作,我这里几乎没有再重新讲一遍 timelion 操作入门的必要了。不过,官方却一直没有列出来 timelion 支持的请求语法的文档,而是在页面上通过点击图标的方式下拉帮助。

timelion

timelion 页面设计上,更接近 Kibana3 而不是 Kibana4。比如 panel 分布是通过设置几行几列的数目来固化的;query 框是唯一的,要修改哪个 panel 的 query,鼠标点选一下 panel,query 就自动切换成这个 panel 的了。

为了方便大家在上手之前了解 timelion 能做到什么,今天特意把 timelion 的请求语法所支持的函数分为几类,罗列如下:

  1. 可视化效果类:
    .bars($width): 用柱状图展示数组
    .lines($width, $fill, $show, $steps): 用折线图展示数组
    .points(): 用散点图展示数组
    .color("#c6c6c6"): 改变颜色
    .hide(): 隐藏该数组
    .label("change from %s"): 标签
    .legend($position, $column): 图例位置
    .yaxis($yaxis_number, $min, $max, $position): 设置 Y 轴属性,.yaxis(2) 表示第二根 Y 轴
  1. 数据运算类:
    .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() 的简写
  1. 数据源设定类:
    .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 功能。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/10
2015

ELK Stack Advent Calendar Day 10


ELK 收集业务日志的来源,除了应用服务器以外,还有很大一部分来自客户端。考虑到客户端网络流量的因素,一般实现上都不会要求实时上报数据,而是攒一批,等到手机连上 WIFI 网络了,再统一发送出来。所以,这类客户端日志一般都有几个特点:

  1. 预先已经记录成 JSON 了;
  2. 日志主体内容是一个巨大无比的数组,数据元素才是实际的单次日志记录;
  3. 一次 POST 会有几 MB 到几十 MB 大小。

在处理这类数据的时候,第一关是别让数据超长直接给丢弃了(说的就是你啊,Rsyslog);第二关就是拆分 JSON 数组,把几十 MB 数据扔 ES 字段里,显然是不利于搜索和统计需求的。今天我们就来说说怎么拆分 JSON 数组。

假设收到的是这么一段日志:

{"uid":123456,"upload_datetime":"2015-12-10 11:38:11","logs":[{"type":"crash","timestamp":"2015-12-10 17:55:00","reason":"****"},{"type":"network_error","timestamp":"2015-12-10 17:56:12","tracert":"****"}]}

首先我们知道可以在读取的时候把 JSON 数据解析成 LogStash::Event 对象:

input {
    tcp {
        codec => json
    }
}

但是怎么把解析出来的 logs 字段拆分成多个 event 呢?这里我们可以用一个已有插件:logstash-filter-split

filter {
    split {
        field => "logs"
    }
    date {
        match => ["timestamp", "yyyy-MM-dd HH:mm:ss"]
        remove_fields => ["logs", "timestamp"]
    }
}

这样,就可以得到两个 event 了:

{"uid":123456,"upload_datetime":"2015-12-10 11:38:11","type":"crash","@timestamp":"2015-12-10T09:55:00Z","reason":"****"}
{"uid":123456,"upload_datetime":"2015-12-10 11:38:11","type":"network_error","@timestamp":"2015-12-10T09:56:12Z","tracert":"****"}

看起来可能跟这个插件的文档描述不太一样。文档上写的是通过 terminator 字符,切割 field 字符串成多个 event。但实际上,field 设置是会自动判断的,如果 field 内容是字符串,就切割字符串成为数组再循环;如果内容已经是数组了,直接循环:

    original_value = event[@field]

    if original_value.is_a?(Array)
        splits = original_value
    elsif original_value.is_a?(String)
        splits = original_value.split(@terminator, -1)
    else
        raise LogStash::ConfigurationError, "Only String and Array types are splittable. field:#{@field} is of type = #{original_value.class}"
    end

    return if splits.length == 1

    splits.each do |value|
        next if value.empty?

        event_split = event.clone
        @logger.debug("Split event", :value => value, :field => @field)
        event_split[(@target || @field)] = value
        filter_matched(event_split)

        yield event_split
    end
    event.cancel

顺带提一句:这里 yield 在 Logstash 1.5.0 之前,实现有问题,生成的新事件,不会继续执行后续 filter,直接进入到 output 阶段。也就是说,如果你用 Logstash 1.4.2 来执行上面那段配置,生成的两个事件会是这样的:

{"@timestamp":"2015-12-10T09:38:13Z","uid":123456,"upload_datetime":"2015-12-10 11:38:11","type":"crash","timestamp":"2015-12-10 17:55:00","reason":"****","logs":[{"type":"crash","timestamp":"2015-12-10 17:55:00","reason":"****"},{"type":"network_error","timestamp":"2015-12-10 17:56:12","tracert":"****"}]}
{"@timestamp":"2015-12-10T09:38:13Z","uid":123456,"upload_datetime":"2015-12-10 11:38:11","type":"network_error","@timestamp":"2015-12-10 17:56:12","tracert":"****","logs":[{"type":"crash","timestamp":"2015-12-10 17:55:00","reason":"****"},{"type":"network_error","timestamp":"2015-12-10 17:56:12","tracert":"****"}]}

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/09
2015

ELK Stack Advent Calendar Day 9


ELK Stack 在入门学习过程中,必然会碰到自己修改定制索引映射(mapping)乃至模板(template)的问题。

这时候,不少比较认真看 Logstash 文档的新用户会通过下面这段配置来制定自己的模板策略:

output {
    elasticsearch {
        host => "127.0.0.1"
        manage_template => true
        template => "/path/to/mytemplate"
        template_name => "myname"
    }
}

然而随后就发现,自己辛辛苦苦修改出来的模板,通过 curl -XGET 'http://127.0.0.1:9200/_template/myname' 看也确实上传成功了,但实际新数据索引创建出来,就是没生效!

这个原因是:Logstash 默认会上传一个名叫 logstash 的模板到 ES 里。如果你在使用上面这个配置之前,曾经运行过 Logstash(一般来说都会),那么 ES 里就已经存在这么一个模板了。你可以 curl -XGET 'http://127.0.0.1:9200/_template/logstash' 验证。

这个时候,ES 里就变成有两个模板,logstash 和 myname,都匹配 logstash-* 索引名,要求设置一定的映射规则了。

ES 会按照一定的规则来尝试自动 merge 多个都匹配上了的模板规则,最终运用到索引上: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html#multiple-templates

其中要点就是:template 是可以设置 order 参数的!而不写这个参数,默认的 order 值就是 0。order 值越大,在 merge 规则的时候优先级越高。

所以,解决这个问题的办法很简单:在你自定义的 template 里,加一行,变成这样:

{
    "template" : "logstash-*",
    "order" : 1,
    "settings" : { ... },
    "mappings" : { ... }
}

当然,其实如果只从 Logstash 配置角度出发,其实更简单的办法是:直接修改原来默认的 logstash 模板,然后模板名称也不要改,就好了:

output {
    elasticsearch {
        host => "127.0.0.1"
        manage_template => true
        template_overwrite => true
    }
}

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/08
2015

ELK Stack Advent Calendar Day 8


Kibana4 上线后,又有同事找过来。还好这次是小问题:『新版的这个仪表盘顶部菜单栏太宽了啊。头顶上监控屏幕空间有限,能不能省省?』

跟 Kibana3 相比,确实宽了点。这时候好几个方案瞬间进入我脑子里:

  1. 浏览器往下拖动一点,不过要确保定期刷新的时候还能回到拖动位置;
  2. ui/public/chrome/chrome.html 里把 navbar 干掉;
  3. 添加一个 bootstrap 效果,navbar 默认隐藏,鼠标挪上去自动浮现。

不过等打开 chrome.html 看了一下,发现 navbar 本身是有相关的隐藏判断的:

<nav
  ng-style="::{ background: chrome.getNavBackground() }"
  ng-class="{ show: chrome.getVisible() }"
  class="hide navbar navbar-inverse navbar-static-top">

这个设置在 ui/public/chrome/api/angular.js 里的 internals.setVisibleDefault(!$location.search().embed);。我们知道 $locatio.search() 是 AngularJS 的标准用法,这里也就是代表 URL 请求参数里是否有 ?embed 选项。

好了,我们试一下,把 http://localhost:5601/app/kibana/#/dashboard/mydash 改成 http://localhost:5601/app/kibana/#/dashboard/mydash?embed,回车,果然,整个菜单栏都消失了!同步消失的还有每个 panel 的编辑按钮。

其实呢,embed 在页面上是有说明的,在 dashboard 的 share 连接里,提供了一个 iframe 分享方式,iframe 里使用的,就是 embed 链接!

注意:Kibana4 部分版本的 share 说明中的 embed 位置生成的有问题,请小心。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/07
2015

ELK Stack Advent Calendar Day 7


用 Logstash 接收 Kafka 里的业务日志再写入 Elasticsearch 已经成为一个常见的选择。但是大多数人随后就会碰到一个问题:logstash-input-kafka 的性能上不去!

这个问题,主要是由于 Logstash 用 JRuby 实现,所以数据从 Kafka 下来到最后流转进 Logstash 里,要经过四五次 Ruby 和 Java 之间的数据结构转换,大大浪费和消耗了 CPU 资源。作为优化,我们可以通过修改默认的 logstash-input-kafka 的 codec 配置为 line,把 Jrjackson 处理流程挪到 logstash-filter-json 里多线程处理,但是也只能提高一倍性能而已。

Logstash 开发组目前也在实现纯 Java 版的 logstash-core-event,但是最终能提高多少,也是未知数。

那么在 Logstash 性能提上去之前,围绕 Kafka 还有什么办法能高效又不失灵活的做到数据处理并写入 Elasticsearch 呢?今天给大家推荐一下携程网开源的 hangout

hangout 采用 YAML 格式配置语法,跟 Elasticsearch 一样,省去了 Logstash 解析 DSL 的复杂度。下面一段配置是 repo 中自带的 example 示例:

inputs:
  - Kafka:
    codec: plain
    encoding: UTF8 # defaut UTF8
    topic: 
      app: 2
    consumer_settings:
      group.id: hangout
      zookeeper.connect: 192.168.1.200:2181
      auto.commit.interval.ms: "1000"
      socket.receive.buffer.bytes: "1048576"
      fetch.message.max.bytes: "1048576"
      num.consumer.fetchers: "4"
  - Kafka:
    codec: json
    topic: 
      web: 1
    consumer_settings:
      group.id: hangout
      zookeeper.connect: 192.168.1.201:2181
      auto.commit.interval.ms: "5000"

filters:
  - Grok:
    match:
      - '^(?<logtime>\S+) (?<user>.+) (-|(?<level>\w+)) %{DATA:msg}$'
    remove_fields: ['message']
  - Add:
    fields:
      test: 'abcd'
    if:
      - '<#if message??>true</#if>'
      - '<#if message?contains("liu")>true<#elseif message?contains("warn")>true</#if>'
  - Date:
    src: logtime
    formats:
      - 'ISO8601'
    remove_fields: ['logtime']
  - Lowercase:
    fields: ['user']
  - Add:
    fields:
      me: 'I am ${user}'
  - Remove:
    fields:
      - logtime
  - Trim:
    fields:
      - user
  - Rename:
    fields:
      me: he
      user: she
  - Gsub:
    fields:
      she: ['c','CCC']
      he: ['(^\w+)|(\w+$)','XXX']
  - Translate:
    source: user
    target: nick
    dictionary_path: /tmp/app.dic
  - KV:
    source: msg
    target: kv
    field_split: ' '
    value_split: '='
    trim: '\t\"'
    trimkey: '\"'
    include_keys: ["a","b","xyz","12"]
    exclude_keys: ["b","c"] # b in excluded
    tag_on_failure: "KVfail"
    remove_fields: ['msg']
  - Convert:
    fields:
      cs_bytes: integer
      time_taken: float
  - URLDecode:
    fields: ["query1","query2"]

outputs:
  - Stdout:
    if:
      - '<#if user=="childe">true</#if>'
  - Elasticsearch:
    cluster: hangoutcluster
    hosts:
      - 192.168.1.200
    index: 'hangout-%{user}-%{+YYYY.MM.dd}'
    index_type: logs # default logs
    bulk_actions: 20000 #default 20000
    bulk_size: 15 # default 15 MB
    flush_interval: 10 # default 10 seconds
    concurrent_requests: 0 # default 0, concurrent_requests设置成大于0的数, 意思着多线程处理, 以我应用的经验,还有是一定OOM风险的,强烈建议设置为0
  - Kafka:
    broker_list: 192.168.1.200:9092
    topic: test2

其 pipeline 设计和 Logstash 不同的是:整个 filter 和 output 流程,都在 Kafka 的 consumer 线程中完成。所以,并发线程数完全是有 Kafka 的 partitions 设置来控制的。

实际运行下来,hangout 比 Logstash 确实在处理能力,尤其是 CPU 资源消耗方面,性价比要高出很多。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/06
2015

ELK Stack Advent Calendar Day 6


Elastic 公司最近推出了 beats 系列,在官方的 packet/top/file{beat} 之外,社区也自发制作了一些比如 docker/nginx/

不过很可惜的是:nginxbeat 只支持两个数据来源:标准的 ngx_http_stub_status_module 和商业版 Nginx Plus 的 ngx_http_status_module

我们都知道,ngx_http_stub_status_module 输出的信息太少,除了进程级别的连接数,啥都没有。那么,在使用开源版本 Nginx 的我们,还有别的办法么?

在官网的第三方模块列表里,发现了一个韩国人写的 nginx-module-vts。这个扩展可以做到 vhost 级别的状态信息输出。(我知道国人还有很多类似的统计扩展,但是没上官网,不便普及,就忽略吧)

但是,不懂 Golang 的话,没法自己动手实现一个 nginx-vts-beat 啊。怎么办?

其实我们可以用 logstash-input-http_poller 实现类似的功能。

首先,我们要给自己的 Nginx 加上 vts 扩展。编译方式这里就不讲了,和所有其他第三方模块一样。配置方式详见 README。我们这里假设是按照核心和非核心接口来统计 URL 的状态:

http {
    vhost_traffic_status_zone;

    map $uri $filter_uri {
        default 'non-core';
        /2/api/timeline core;
        ~^/2/api/unread core;
    }

    server {
        vhost_traffic_status_filter_by_set_key $filter_uri;
        location /status {
            auth_basic "Restricted"; 
            auth_basic_user_file pass_file;
            vhost_traffic_status_display;
            vhost_traffic_status_display_format json;
        }
    }
}

然后我们需要下面一段 Logstash 配置来定期获取这个数据:

input {
  http_poller {
    urls => {
      0 => {
        method => get
        url => "http://localhost:80/status/format/json"
        headers => {
          Accept => "application/json"
        }
        auth => {
          user => "YouKnowIKnow"
          password => "IKnowYouDonotKnow"
        }
      }
      1 => {
        method => get
        url => "http://localhost:80/status/control?cmd=reset&group=*"
        headers => {
          Accept => "application/json"
        }
        auth => {
          user => "YouKnowIKnow"
          password => "IKnowYouDonotKnow"
        }
      }
    }
    request_timeout => 60
    interval => 60
    codec => "json"
  }
}

这样,就可以每 60 秒,获得一次 vts 数据,并重置计数了。

注意,urls 是一个 Hash,所以他的执行顺序是根据 Hash.map 来的,为了确保我们是先获取数据再重置,这里干脆用 0, 1 来作为 Hash 的 key,这样顺序就没问题了。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/05
2015

ELK Stack Advent Calendar Day 5


前几天,我们已经一步步搞定了一个业务日志从 mapping 设计到异常统计追踪上的用法。作为一个工程师,自评 100 分 —— But,领导找上门来说:你这个结构怎么搞的嘛,在 Kibana 上完全没法搜索!让客服和分析师怎么办?

因为 Kibana 上的输入框,默认使用 querystring 语法。这个里面压根没有对 nested object 的相关语法设计。

不过经过仔细查阅,发现原来 Kibana4 的搜索输入框,其实除了 querystring 以外,还支持 JSON 字符串的方式直接定义 query!其具体处理方式就是:把你输入的字符串判断一下是否是 JSON,如果是 JSON,直接替换进 {"query": 这里};如果不是,才生成一个 querystring query 放进 {"query":{"query_string":""}}

那我们来尝试一下把第三天写的那个 nested query 贴进搜索框里。内容是:

{
  "nested" : {
    "path" : "video_time_duration",
    "query" : {
      "match" : {
        "video_time_duration.type" : "1"
      }
    }
  }
}

意外发生了!Kibana4 竟然在页面上弹出一个错误提示,而且搜索栏的放大镜图标也变成不可以点击的灰色样式,敲回车同样没有反应:

当然我很确定我的数据是没问题的。这时候 Kibana4 的另一个特性救了我:它默认会把所有可修改的状态都 rison 序列化了放在 URL 里!于是我尝试直接在浏览器地址栏里输入下面这段 URL:

http://kibana:5601/#/discover?_g=()&_a=(columns:!(_source),index:%5Blogstash-mweibo-%5DYYYY.MM.DD,interval:auto,query:(nested:(path:video_time_duration,query:(term:(video_time_duration.type:1)))),sort:!('@timestamp',desc))

地址栏回车之后,页面刷新,看到搜索结果更新(如上图)!虽然搜索栏依然有报错,但实际上 nested query 生效了,我们在下面 search 里看到的都是成功过滤出来的『有过卡顿的视频播放记录』日志。

感谢 Kibana 如此开放的设计原则!

ps: 目前 nested aggregation 还没法像这样简单的绕过,不过已经有相关 pull request 在 review 中,或许 Kibana4.3/4.4 的时候就会合并了。有兴趣的同学,也可以跟我一样先睹为快哟:https://github.com/elastic/kibana/pull/5411

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/04
2015

ELK Stack Advent Calendar Day 4


昨天我们通过 nested aggregation 计算出来,视频卡顿次数最多的是北京。不过这个结论似乎也没有什么奇怪的,北京的网民本身就多嘛。

Elasticsearch 还有一个有趣的聚合方式,叫 significant_terms。这时候就可以派上用场了!

我们把昨天的 query JSON 中,最后一段 sub agg 改成这样:

    "city_terms" : {
        "significant_terms" : {
            "field" : "geoip.city",
            "size" : "4"
        }
    }

重新运行请求,得到的响应结果是这样的:

"city_terms" : {
  "doc_count" : 2521720,
  "buckets" : [ {
    "key" : "武汉",
    "doc_count" : 85980,
    "score" : 0.1441705001066121,
    "bg_count" : 15347191
    }, {
    "key" : "北京",
    "doc_count" : 142761,
    "score" : 0.11808069152203737,
    "bg_count" : 43176384
    }, {
    "key" : "广州",
    "doc_count" : 104677,
    "score" : 0.10716870365361204,
    "bg_count" : 27274482
    }, {
    "key" : "郑州",
    "doc_count" : 59234,
    "score" : 0.09915501610550795,
    "bg_count" : 10587590
  } ]
}

大家一定发现了:第一名居然变成了武汉

而且每个结果后面,还多出来了 scorebg_count 两个数据。这个 bg_count 是怎么回事呢?

这就是 significant_terms 的作用了。这个 agg 的大概计算步骤是这样:

  1. 计算一个 term 在整个索引中的比例,作为背景计数(background),这里是 15347191 / 2353406423;
  2. 计算一个 term 在 parent agg 中的比例,作为前景计数(foreground),这里是 85980 / 2521720;
  3. 用 fgpercent 除以 bgpercent,得到这个 term 在 parent agg 的条件下比例凸显的可能性。

由于两个作分母的总数其实大家都是相等的,其实比较的就是各 term 的 doc_count / bg_count 了。

当然,实际的 score 不只是这么简单,还有其他综合因素。毕竟也不能给出来本身就没啥关注度的数据嘛。

我们还可以来验证一下『武汉』的 bg_count 是不是这个意思:

curl -XPOST 'http://10.19.0.67:9200/logstash-mweibo-2015.12.02/_count?pretty' -d '{
  "query" : {
    "match" : {
      "geoip.city" : "武汉"
    }
  }
}'

结果如下:

{
  "count" : 15347191,
  "_shards" : {
    "total" : 100,
    "successful" : 100,
    "failed" : 0
  }
}

数值完全对上了。没错,bg_count 就是『武汉』在整个索引里的总数。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/03
2015

ELK Stack Advent Calendar Day 3


话接上回,我们只是解决了写数据的问题,这种格式不太符合常规的数据怎么读,也需要我们相应的做出点改变。

今天以一个实际的例子来讲。我曾经处理过一份数据,记录的是视频播放的卡顿情况。其中有一个数组,每次卡顿就新增一个对象元素。所以设计的 mapping 如下:

         "video_time_duration" : {
           "type": "nested",
           "properties" : {
             "duration" : {
               "type" : "long",
               "doc_values" : true
             },
             "type" : {
               "type" : "long",
               "doc_values" : true
             }
           }
         },

其中 type 只有 0 或 1 两个可能,0 表示播放正常,1 表示卡顿。所以下面我们发一个请求,要求是计算这样的结果:

出现了播放卡顿的用户,单次卡顿时长在10到200ms的,最常见于哪些城市?

下面是我们最终的查询请求 JSON:

{
  "size" : 0,
  "query" : {
    "nested" : {
      "path" : "video_time_duration",
      "query" : {
        "match" : {
          "video_time_duration.type" : "1"
        }
      }
    }
  },
  "aggs" : {
    "video" : {
      "nested" : {
        "path" : "video_time_duration"
      },
      "aggs" : {
        "filter_type" : {
          "filter" : {
            "term" : {
              "video_time_duration.type" : "1"
            }
          },
          "aggs" : {
            "duration_ranges" : {
              "range" : {
                "field" : "video_time_duration.duration",
                "ranges" : [
                  { "from" : 10, "to" : 200 }
                ]
              },
              "aggs" : {
                "city" : {
                  "reverse_nested": {},
                  "aggs" : {
                    "city_terms" : {
                      "terms" : {
                        "field" : "geoip.city"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

很明显的可以看到对 nested object 里存的数据,不管是做 query 还是 agg,都需要显式的加上 "nested": {"path" : "video_time_duration" 的声明。这样,才能保证我们取到的 duration 数值是对应 type 为卡顿的,而不是流畅播放的。

大家可能注意到,我同时在 query 和 aggFilter 中重复了一场 term 过滤。其中这次 nested query 是不必要的,除了作为语法展示以外,也有一个减少 hits 数的作用。但是和一般的请求不同的是,这里不可以去掉 nested agg 里的 term filter,因为 nested query 只是拿到『有过卡顿』的数据 id。不加 filter,聚合 duration 的时候,会把卡过但也流畅过的那部分都计算在内。

另一个要点:当我们过滤好 nested 数据的时候,要取顶层其他字段的内容,在 sub agg 里是无法直接获取的,需要额外使用一次 reverse_nested 来跳出这个 nested path,才可以恢复正常的 agg 路径。

最终得到的响应如下:

{
  "took" : 4672,
  "timed_out" : false,
  "_shards" : {
    "total" : 100,
    "successful" : 100,
    "failed" : 0
  },
  "hits" : {
    "total" : 9560309,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "video" : {
      "doc_count" : 33713503,
      "filter_type" : {
        "doc_count" : 25441559,
        "duration_ranges" : {
          "buckets" : [ {
            "key" : "10.0-200.0",
            "from" : 10.0,
            "from_as_string" : "10.0",
            "to" : 200.0,
            "to_as_string" : "200.0",
            "doc_count" : 2521720,
            "city" : {
              "doc_count" : 2521720,
              "city_terms" : {
                "doc_count_error_upper_bound" : 0,
                "sum_other_doc_count" : 2267886,
                "buckets" : [ {
                    "key" : "北京",
                    "doc_count" : 142761
                  }, {
                    "key" : "广州",
                    "doc_count" : 104677
                  }
                ]
              }
            }
          } ]
        }
      }
    }
  }
}

响应数据中,我们可以直接看这些 hits 和 doc_count 数据。他们表示:

  • 一共命中了『有过卡顿』的视频播放次数:9560309;
  • 其中记录下来的播放间隔 33713503 次;
  • 里面有 25441559 次是卡顿(减一下即 8271944 次是流畅咯);
  • 里面卡顿时长在 10-200 ms 的是 2521720 次;
  • 这些卡顿出现最多的在北京,发生了 142761 次。

数据蛮有意思吧。ES 能告诉你的还不止这点。更有趣的,明天见。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/02
2015

ELK Stack Advent Calendar Day 2


Elasticsearch 中有些高级特性,可能不太常用,但是在恰当场景下,又非常有效果。今天,我们来说说 nested object

我们都知道,Elasticsearch 宣传中是 schemaless 的。但实际使用中,并不是完全的随意。比如过多的 kv 切割,会导致 mapping 大小暴涨,对集群稳定性是个不小的挑战。

以 urlparams 为例,下面这段 urlparams 直接通过 logstash-filter-kv 切割得到的结果,需要在 mapping 中占用 4 个字段的定义。

"urlparams" : {
    "uid" : "1234567890",
    "action" : "payload",
    "t" : "1449053032000",
    "pageid" : "v6"
  }

如果哪个开发一时想不开,把 urlparams 写成 uid:123456789&action=payload&1449053032000=t&pageid=v6,那基本上整个 ES 集群就会被过于频繁的 mapping 更新搞挂了。

这时候,我们修改一下 mapping 定义:

{
  "accesslog" : {
    "properties" : {
      "urlparams" : {
        "type" : "nested",
        "properties" : {
            "key" : { "type" : "string", "index" : "not_analyzed", "doc_values" : true },
            "value" : { "type" : "string", "index" : "not_analyzed", "doc_values" : true }
        }
      }
    }
  } 
}

同时在 Logstash 的 filter 配置中添加一段:

if [urlargs] {
                ruby {
                    init => "@kname = ['key','value']"
                    code => "event['urlparams'] = event['urlargs'].split('&').collect {|i| Hash[@kname.zip(i.split('='))]}"
                    remove_field => [ "urlargs","uri","request" ]
                }
            }

生成的 JSON 数据变成这个样子:

"urlargs": [
    { "key": "uid", "value": "1234567890" },
    { "key": "action", "value": "payload" },
    { "key": "1449053032000", "value": "t" },
    { "key": "pageid", "value": "v6" }
  ]

这样,再错乱的 urlparams,也不会发生 mapping 变更,导致集群故障了!

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

12/01
2015

ELK Stack Advent Calendar Day 1


Advent Calendar 是各大技术社区每年 12 月大多会举办的一个系列活动。原意是圣诞节前夕的小礼品,延伸为每天一篇技术小分享的意思。最常见的包括 Perl Advent、sysadmin advent、web advent、performance advent 等。个人从 2009 年开始每年都看,从2013 年开始偶尔会参加其他社区的 advent 写作。今年考虑自己在 ELK Stack 上专注较多,在历次技术大会和最终出版的《ELK Stack权威指南》之外,又有一些新的发现和收获,干脆尝试一把自己一个人的 advent,也算是对 ELK 小知识的一种查漏补缺。

今天是 12 月 1 日,第一天,开天辟地,让我们也从最简单而又容易被忽略的一个小技巧开始吧!

每个上手 ELK 的新用户,肯定都需要测试一下读取文件输出到终端这步。在 Logstash 中,也就是配置这样一段:

input {
    file {
        path => ["/data/test.log"]
    }
}
output {
    stdout {
        codec => rubydebug
    }
}

不过很多新人的测试随后就卡在第二步了:当你修改一下配置,准备添加一段 filter 配置再重复运行 logstash 命令时,发现终端一直停滞没有输出。

这是因为:Logstash 会记录自己读取文件内容的偏移量到一个隐藏文件里,默认情况下,下次启动,他会从这个偏移量继续往后读,避免重复读取数据。

这个隐藏文件,叫做 $HOME/.sincedb_****。过去很多文档,在解释了这个原理后,都会告诉大家解决办法:每次重新运行 logstash 命令之前,删除掉家目录下的 sincedb 隐藏文件。

但是这种办法很笨,不是么?

今天告诉大家一个更方便的办法,改用下面这段 Logstash 配置:

input {
    file {
        path => ["/data/test.log"]
        start_position => "beginning"
        sincedb_path => "/dev/null"
    }
}
output {
    stdout {
        codec => rubydebug
    }
}

要点就在这行 sincedb_path => "/dev/null" 了!该参数用来指定 sincedb 文件名,但是如果我们设置为 /dev/null 这个 Linux 系统上特殊的空洞文件,那么 logstash 每次重启进程的时候,尝试读取 sincedb 内容,都只会读到空白内容,也就会理解成之前没有过运行记录,自然就从初始位置开始读取了!

好了,第一天就是这样。更多内容,敬请期待。

想了解更全面的 ELK Stack 知识和细节,欢迎购买我的《ELK Stack权威指南》,也欢迎加 QQ 群:315428175 哟。

11/23
2014

利用动态仪表板实现kibana单图表导出功能


昨天和朋友聊天,说监控报表的话题,他们认为 kibana 的仪表板形式,还是偏重技术人员做监控的 screen 思路,对 erp 之类的报表不是很友好。要想跟其他系统结合,或者说嵌入到其他系统中,就必须得有单个图表的导出,或者 URL 引用方式。当时我直觉上的反应,就是这个没问题,可以通过 javascript 动态仪表板这个高级功能完成。回来试了一下,比我想的稍微复杂一点点,还是可以很轻松完成的。

读过仪表板纲要一文,或者自己看过源代码中 src/app/dashboards/logstash.json 文件的人,应该都知道 kibana 中有些在页面配置界面里看不到的隐藏配置选项。其中很符合我们这次需求的,就有 editable, collapsable 等。所以,首先第一步,我们可以在自己的 panel.js(直接从 logstash.js 复制过来) 中,把这些关掉:

dashboard.rows = [
  {
    editable: false,         //不显示每行的编辑按钮
    collapsable: false,      //不显示每行的折叠按钮
    title: "Events",
    height: "400px",
    panels = [{
      editable: false,       //不显示面板的编辑按钮
      title: 'events over time',
      type: 'histogram',
      time_field: ARGS.timefield||"@timestamp",
      auto_int: true,
      span: 12
    }]
  }
];
dashboard.editable = false;     //不显示仪表板的编辑按钮
dashboard.panel_hints = false;  //不显示面板的添加按钮

然后要解决面板上方的 query 框和 filtering 框。这个同样在纲要介绍里说了,这两个特殊的面板是放在垂幕(pulldows)里的。所以,直接关掉垂幕就好了:

dashboard.pulldowns = [];

然后再往上是顶部栏。顶部栏里有时间选择器,这个跟垂幕一样是可以关掉的:

dashboard.nav = [];

好了,javascript 里可以关掉的,都已经关了。

但是运行起来,发现顶部栏里虽然是没有时间选择器和配置编辑按钮了,本身这个黑色条带和 logo 图什么的,却依然存在!这时候我想起来有时候 config.js 没写对,/_nodes 获取失败的时候,打开的页面就是背景色外加这个顶条 —— 也就是说,这部分代码是写在 index.html 里的,不受 app/dashboards/panel.js 控制。

所以这里就得去修改一下 index.html 了。不过为了保持兼容性,我这里没有直接删除顶部栏的代码,而是用了 angularjs 中很常用的 ng-show 指令:

<div ng-cloak class="navbar navbar-static-top" ng-show="dashboard.current.nav.length">

因为之前关闭时间选择器的时候,已经把这个 nav 数组定义为空了,所以只要判断一下数组长度即可。

效果如下:

single panel

因为 dashboard.services 的定义没有做修改,所以这个其实照样支持你用鼠标拉动选择时间范围,支持你在 URL 后面加上 ?query=status:404&from=1h 这样的参数,效果都是对的。只不过不会再让你看到这些文字显示在页面上了。

如果要求再高一点,其实完全可以在 ARGS 里处理更复杂的参数,比如直接 ?type=terms&field=host&value_field=requesttime 就生成 dashboard.rows[0].panels[0] 里的对应参数,达到自动控制图表类型和效果的目的。

11/19
2014

在 kibana 里实现去重计数


如何在 elk 里统计或者展示去重计数,是一个持续很久的需求了。几乎每个月都会有新手提问题说:“我怎么在 kibana 里统计网站 UV 啊?”可惜这个问题的回答总是:做不到……

其实 Elasticsearch 从 1.1.0 版本开始已经可以做到去重统计了。但是 kibana3 本身是在 0.90 版本基础上实现的,所以也就没办法了。

今天抽出时间,把 histogram 面板的代码重写了一遍,用 aggregations 接口替换了 facets 接口。改造完成后,再加上去重就很容易了。

aggregations 接口最大的特点是层级关系。不过也不是可以完全随便嵌套的,原先 date_histogram facets 里的 global 参数,被拆分成了 global aggregation,但是这个 global aggregation 就强制要求必须用在顶层。所以最后 request 相关代码就变成了这个样子:

var aggr = $scope.ejs.DateHistogramAggregation(q.id);
if($scope.panel.mode === 'count') {
  aggr = aggr.field($scope.panel.time_field);
} else if($scope.panel.mode === 'uniq') {
  aggr = aggr.field($scope.panel.time_field).agg($scope.ejs.CardinalityAggregation(q.id).field($scope.panel.value_field));
} else {
  aggr = aggr.field($scope.panel.time_field).agg($scope.ejs.StatsAggregation(q.id).field($scope.panel.value_field));
}
request = request.agg(
  $scope.ejs.GlobalAggregation(q.id).agg(
    $scope.ejs.FilterAggregation(q.id).filter($scope.ejs.QueryFilter(query)).agg(
      aggr.interval(_interval)
    )
  )
).size($scope.panel.annotate.enable ? $scope.panel.annotate.size : 0);

完整的代码已经提交到 github,见 https://github.com/chenryn/kibana-authorization/commit/6cb4d28a6c610d28680fffdb81c9f6c83cfaf488

11/18
2014

Kibana 4 beta 2 发布


原文地址见:http://www.elasticsearch.org/blog/kibana-4-beta-2-get-now/

哈哈哈哈哈哈哈哈哈!来啦!Kibana 4 Beta 2 现在正式雪地 360° 裸跪求调戏,包括你家喵星人都行,只要你给反馈。(译者注:ES 的发版日志越来越活泼,我也翻译的更中文化点好了)

如果你已经等不及要开动,从这里下载 Kibana 4 Beta 2,否则继续阅读下面的亮点。

除了很多小的修复和改进,这个版本里还有一些非常值得一看的新东西:

地图支持

地图回来啦,而且比过去更强大了!新的瓦片式地图可视化用上了 Elasticsearch 强大的 geohash_grid 来显示地理数据,比如可视化展示相对响应时间:

map

可视化选项

在 Beta 1 里,柱状图是固定成堆叠式的。在 Kibana 4 Beta 2 里,我们添加了选项让你修改可视化展示数据的方式。比如,分组柱状图:

grouped bars

或者百分比式柱状图:

Percent bars

区域图

Beta 2 里区域图也回来了,包括堆叠式和非堆叠式:

area

高级参数

我们目标是支持尽可能多的 Elasticsearch 特性,不过有时候我们确实还没覆盖到某个聚合选项,而你偏偏现在就要用它。这种情况下,我们引入了 JSON 输入,让你可以定义附加的聚合参数到发送的请求里。比如,你可能想在一个 terms 聚合里传递一个 shard_size,或者在一个基数聚合里调大 precision_threshold。在下面示例中,我们传了一个小脚本作为高级参数,计算 bytes 字段的 _value 的对数值,然后用它作为 X 轴:

scripts

数据表格

有时候你想要个动态图,有时候可能只想要数值就够了。数据表格可视化达成你这个愿望:

data table

喂!我的仪表盘哪去了?

Kibana 内部使用的索引从 kibana-int 改名叫 .kibana 了。我们建议你从老索引里把文档(比如:仪表盘,设置,可视化等)都挪到新索引来。不过,你还是可以在 kibana.yml 里直接定义 kibanaIndex: "kibana-int" 的。

我们现在在做什么?

可以从 roadmap 上看到我们离 Kibana 4 正式版还有多远。另外,我们永远欢迎你在 GitHub 的反馈、bug 报告、补丁等等。

10/18
2014

在终端命令行上调试 grok 表达式


解决 grokdubugger 网站翻墙才能用的问题

用 logstash 的人都知道在 http://grokdebug.herokuapp.com 上面调试 grok 正则表达式。现在问题来了:翻墙技术哪家强? 页面中用到了来自 google 域名的 js 文件,所以访问经常性失败。所以,在终端上通过命令行方式快速调试成了必需品。

其实在 logstash 还在 1.1 的年代的时候,官方 wiki 上是有一批专门教大家怎么通过 irb 交互式测试 grok 表达式的。但不知道为什么后来 wiki 这页没了…… 好在代码本身不复杂,稍微写几行脚本,就可以达到目的了:

#!/usr/bin/env ruby
require 'rubygems'
gem 'jls-grok', '=0.11.0'
require 'grok-pure'
require 'optparse'
require 'ap'

options = {}
ARGV.push('-h') if ARGV.size === 0
OptionParser.new do |opts|
  opts.banner = 'Run grokdebug at your terminal.'
  options[:dirs] = %w(patterns)
  options[:named] = false
  opts.on('-d DIR1,DIR2', '--dirs DIR1,DIR2', Array, 'Set grok patterns directories. Default: "./patterns"') do |value|
    options[:dirs] = value
  end
  opts.on('-m MESSAGE', '--msg MESSAGE', 'Your raw message to be matched') do |value|
    options[:message] = value
  end
  opts.on('-p PATTERN', '--pattern PATTERN', 'Your grok pattern to be compiled') do |value|
    options[:pattern] = value
  end
  opts.on('-n', '--named', 'Named captures only') do
    options[:named] = true
  end
end.parse!

grok = Grok.new
options[:dirs].each do |dir|
  if File.directory?(dir)
    dir = File.join(dir, "*")
  end
  Dir.glob(dir).each do |file|
    grok.add_patterns_from_file(file)
  end
end
grok.compile(options[:pattern], options[:named])
ap grok.match(options[:message]).captures()

测试一下:

    $ sudo gem install jls-grok awesome_print
    $ ruby grokdebug.rb
    Run grokdebug at your terminal.
        -d, --dirs DIR1,DIR2             Set grok patterns directories. Default: "./patterns"
        -m, --msg MESSAGE                Your raw message to be matched
        -p, --pattern PATTERN            Your grok pattern to be compiled
        -n, --named                      Named captures only
    $ ruby grokdebug.rb -m 'abc123' -p '%{NUMBER:test}'
    {
             "test" => [
            [0] "123"
        ],
        "BASE10NUM" => [
            [0] "123"
        ]
    }
    $ ruby grokdebug.rb -m 'abc123' -p '%{NUMBER:test:float}' -n
    {
        "test" => [
            [0] 123.0
        ]
    }

没错,我这比 grokdebug 网站还多了类型转换的功能。它用的 jls-grok 是 0.10.10 版,而我用的是最新的 0.11.0 版。

10/18
2014

Rsyslog 性能数据 impstats 直接写入 Elasticsearch


Rsyslog 的性能数据,可以通过自带的 impstats 插件输出。但是在用的比较复杂的场景下,每次输出都会有好几十个 action 的各种状态,肉眼观察变得比较困难,这时候,我们可以直接输出给 Elasticsearch ,然后利用 Kibana 做快速搜索和分析。

Rsyslog 官方提供了直接输出给 Elasticsearch 的插件:omelasticsearch。配置如下:

    module(load="omelasticsearch")
    module(load="impstats" interval="120" severity="6" log.syslog="on" format="json" resetCounters="on")
    template(name="logstash-index" type="list") {
        constant(value="logstash-rsyslog-")
        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="plain-syslog" type="list") {
        constant(value="{")
        constant(value="\"@timestamp\":\"") property(name="timereported" dateFormat="rfc3339")
        constant(value="\",\"host\":\"")    property(name="hostname")
        constant(value="\",")   property(name="msg" position.from="2")
    }
    if ( $syslogfacility-text == 'syslog' ) then {
        action( type="omelasticsearch"
                template="plain-syslog"
                server="10.13.57.35"
                searchIndex="logstash-index"
                searchType="impstats"
                bulkmode="on"
                dynSearchIndex="on"
        )
        stop
    }

这里用到一个小窍门。impstats 只是 message 部分内容是 JSON 格式,那么如果合在总的内容里,可能就得跟老版的 logstash 事件格式一样,专门放在 @fields: 里面去了。但是,利用 position.from 参数,把 message 部分的开头 { 给删掉,就把整个内容都提升到顶层了,变成了新版 logstash 的事件格式了!

Rsyslog 的 template 语法多变,实现这个同样的目的,在 mmjsonparse 或者 mmnormalize 的配合下,就可以有不同写法:

normalize 是 rsyslog 作者自己写的一个日志格式分析工具,搞怪的是它的文本格式示例文件后缀名叫 .rb,不是 Ruby,而是 RuleBase……

RuleBase 支持的语法不多:

  • date-rfc3164: date as specified in rfc3164 (example: %date:date-rfc3164%)
  • date-rfc5424: date as specified in rfc5424 (example: %date:date-rfc5424%)
  • ipv4: IP adress (example: %ip:ipv4%)
  • number: sequence of numbers (example: %port:number%)
  • word: everything until the next blank (example: %host:word%)
  • char-to: the field will be defined by the sign in the additional information (example: %tag:char-to:\x3a%: (x3a means ":" in the additional information))
  • quoted-string: If a quoted string is present, a property can be filled with the whole string (example: %quote:quoted-string%)
  • date-iso: date in ISO format (example: %date:date-iso%)
  • time-24hr: detects time in 24hr format (example: %time:time-24hr%)
  • time-12hr: detects time in 12hr format (example: %time:time-12hr%)
  • iptables: parses IP tables messages and fills properties accordingly(example: %tables:iptables%)

从这个格式设计也可以看出,主要还是用来分析系统日志比较多。

目前来看,使用 Rsyslog 做整套日志处理系统的话,在数据结构化这步,还是用 mmexternal 插件来完成比较合适。

mmexternal 模块类似 squid 的 url_rewrite_program ,都是支持用任意语言写的脚本,死循环接收 STDIN(可以配置传输 line 还是 json 格式),处理完成后(JSON 格式)输出给 STDOUT 即可。官方示例见:https://github.com/rsyslog/rsyslog/blob/master/plugins/external/messagemod/anon_cc_nbrs/anon_cc_nbrs.py。性能如何,有待测试了。

10/18
2014

LogStash::Inputs::Syslog 性能测试与优化


最近因为项目需要,必须想办法提高 logstash indexer 接收 rsyslog 转发数据的性能。首先,就是要了解 logstash 到底能收多快?

之前用 libev 库写过类似功能的程序,所以一开始也是打算找个能在 JRuby 上运行的 netty 封装。找到了 foxbat 库,不过最后发现效果跟官方的标准 socket 实现差不多。(这部分另篇讲述)

后来又发现另一个库:jruby-netty,注意到这个作者就是 logstash 作者 jordansissel!

当然,最终并不是用上这个项目的代码来改写 logstash,而是从这里面学到了如何方便的进行 syslog server 性能压测。测试方式:

yes "<44>May 19 18:30:17 snack jls: foo bar 32" | nc localhost 3000

或者

loggen -r 500000 -iS -s 120 -I 50  localhost 3000

loggen 是 syslog-ng 带的工具,还得另外安装。而上面第一行的方式,这个 yes 用的真是绝妙!

就用这个测试方法,最终发现单机上 LogStash::Inputs::Syslog 的每秒处理能力只有 700 条:

    input {
        syslog {
            port => 3000
        }
    }
    output {
        stdout {
            codec => dots
        }
    }

logstash 配置文件见上。然后测试启动命令如下:

./bin/logstash -f syslog.conf | pv -abt > /dev/null

注意,centos 上的 pv 命令可能还没有 -a 参数。

为了逐一排除性能瓶颈。我依次注释掉了 lib/logstash/inputs/syslog.rb@date_filters.filter(event)@grok_filters.filter(event) 两段,并重新运行上次的测试。结果发现:

  • TCPServer 接收的性能是每秒 50k 条
  • TCPServer 接收并完成 grok filter 的性能是每秒 5k 条
  • TCPServer 接收并完成 grok 和 date filter 的性能是每秒 700 条

性能成几何级的下降!

而另外通过 input { generator { count => 3000000 } } 测试可以发现,logstash 本身空数据流转的性能也不过就是每秒钟几万条。所以,优化点就在后面的 filter 上。

注:空数据流转的测试采用 inputs/generator 插件

LogStash::Inputs::Syslog 中,TCPServer 对每个 client 单独开一个 Thread,但是这个 Thread 内要顺序完成 @codec.decode@grok_filter.filter@date_filter.filter 三大步骤后,才算完成。而我们都知道:Logstash 配置中 filter 阶段的插件是可以多线程完成的。所以,解决办法就来了:

    input {
        tcp {
            port => 3000
        }
    }
    filter {
        grok {
            overwrite => "message"
            match => ["message", "<\d+>%{SYSLOGLINE}"]
        }
        date {
            locale => "en"
            match => ["timestamp", "MMM dd HH:mm:ss", "MMM  d HH:mm:ss"]
        }
    }
    output {
        stdout {
            codec => dots
        }
    }

然后重新测试,发现性能提高到了每秒 4.5k。再用下面命令运行测试:

  ./bin/logstash -f syslog.conf -w 20 | pv -bt > /dev/null

发现性能提高到了每秒 30 k 条!

此外,还陆续完成了另外一些测试。

比如:

  • outputs/elasticsearch 的 protocol 使用 node 还是 http 的问题。测试在单台环境下,node 只有 5k 的 indexing 速度,而 http 有7k。
  • 在 inputs/file 的前提下,outputs/stdout{dots} 比 outputs/elasticsearch{http} 处理速度快一倍,即有 15k。
  • 下载了 heka 的二进制包,通过下面配置测试其接受 syslog 输入,并以 logstash 的 schema 输出到文件的性能。结果是每秒 30k,跟之前优化后的 logstash 基本一致。
[hekad]
maxprocs = 48

[TcpInput]
address = ":5140"
parser_type = "token"
decoder = "RsyslogDecoder"

[RsyslogDecoder]
type = "SandboxDecoder"
filename = "lua_decoders/rsyslog.lua"

[RsyslogDecoder.config]
type = "mweibo"
template = '<%pri%>%TIMESTAMP% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n'
tz = "Asia/Shanghai"

[ESLogstashV0Encoder]
es_index_from_timestamp = true
fields = ["Timestamp", "Payload", "Hostname", "Fields"]
type_name = "%{Type}"

# [ElasticSearchOutput]
# message_matcher = "Type == 'nginx.access'"
# server = "http://10.13.57.35:9200"
# encoder = "ESLogstashV0Encoder"
# flush_interval = 50
# flush_count = 5000

[counter_output]
type = "FileOutput"
path = "/tmp/debug.log"
message_matcher = "TRUE"
encoder = "ESLogstashV0Encoder"

heka 文档称 maxprocs 设置为 cpu 数的两倍。不过实际测试中,不配置跟配置总共也就差一倍的性能。

10/10
2014

从源代码运行 Kibana 4


目前kibana4官方也用 jar 包方式发布,不便于调试和二次开发。本文提供源码部署方式

Kibana 4 发布了,出人意料的是提供的居然是一个 jar 包的运行方式。好在有源码可看,根据源码可以分析得知,v4 版其实是一个 angularjs 写的 kibana 配上一个 sinatra 写的 proxyserver。这么一来,我们也就知道怎么来从源代码运行 Kibana 4,而不是用 Java 启动了。

# 安装 nodejs 和 npm 命令,仅用于下载依赖包,实际运行不需要
port install nodejs npm
# 下载 kibana 4 源码
git clone https://github.com/elasticsearch/kibana.git kibana4
cd kibana4/
# 安装 bower 工具
npm install -g bower
# 读取目录中的 bower.json,
# 依此下载所有 js 依赖库到其中定义的路径
# src/kibana/bower_components 下
bower install
cd src/server
# 安装 bundler 工具
gem install bundler
# 读取目录中的 Gemfile,
# 依此安装所有的 RubyGem 依赖库
bundle install
# 安装 lessc 工具
npm install -g less
# kibana 4 源码中在导入 lesshat 的时候都没写具体路径,所以要切换到对应目录下执行
cd ../src/kibana/bower_components/lesshat/build
# 编译 kibana 内的 *.less 文件为 *.css 文件
for i in `find ../../.. -name [a-z]*.less|grep -v bower_components`;do
    ../../../../../node_modules/.bin/lessc $i ${i/.less/.css/}
done
# 进入代理服务器目录
cd ../../../../server/
# 启动 sinatra 服务器
./bin/initialize

这样就可以通过 "localhost:5601" 访问了。

此外,Elasticsearch 集群的地址,在 src/server/config/kibana.yml 中配置。注意里面的 kibana-int 建议大家使用的时候改个名儿,不然万一跟你原先 kibana3 的混合在一起了就不好了。

最后,如果你的集群版本低于 1.4.0.BETA1,也不要着急,其实目前代码并没有用上什么这个版本的特性,所以可以通过修改 src/kibana/index.js 改变这个版本检测:

--- a/src/kibana/index.js
+++ b/src/kibana/index.js
@@ -33,7 +33,7 @@ define(function (require) {
     // Use this for cache busting partials
     .constant('cacheBust', window.KIBANA_COMMIT_SHA)
     // The minimum Elasticsearch version required to run Kibana
-    .constant('minimumElasticsearchVersion', '1.4.0.Beta1')
+    .constant('minimumElasticsearchVersion', '1.1.0')
     // When we need to identify the current session of the app, ef shard preference
     .constant('sessionId', Date.now())
     // attach the route manager's known routes

Kibana 4 的界面,改成了 Query -> Visual -> Dashboard 三个解耦层次。而且不再是固定的提供某种某种 panel,改成自己选择、拼接甚至书写 Aggr 聚合函数的方式来灵活的生成图表。可以说,对使用者的 ES 知识,要求更高了。

后续如何发展,大家一起关注吧。

10/10
2014

Elasticsearch 1.4.0 beta 1 发版日志


原文见:http://www.elasticsearch.org/blog/elasticsearch-1-4-0-beta-released/

今天,我们很高兴公告基于 Lucene 4.10.1Elasticsearch 1.4.0.Beta1 发布。你可以从这里下载并阅读完整的变更列表:Elasticsearch 1.4.0.Beta1

1.4.0 版的主题就是弹性:让 Elasticsearch 比过去更稳定更可靠。当所有东西都按照它应该的样子运行的时候,就很容易变得可靠了。但是不在意料中的事情发生时,复杂的部分就来了:节点内存溢出,它们的性能被慢垃圾回收或者超重的 I/O 拖累,网络连接失败,或者数据传输不规律。

这次 beta 版主要在三方面力图改善弹性:

分布式系统是复杂的。我们已经有一个广泛的测试套件,可以创建随机场景,模拟我们自己都没想过的条件。但是依然会有无限多在此范围之外的情况。1.4.0.Beta1 里已经包含了我们目前能做到的各种优化努力。真心期望大家在实际运用中测试这些变更,然后告诉我们你碰到的问题

内存管理

这次发版包括了一系列变更来提升内存管理,并由此提升节点稳定性:

doc values

fielddata 是最主要的内存大户。为了让聚合、排序以及脚本访问字段值时更快速,我们会加载字段值到内存,并保留在内存中。内存的堆空间非常宝贵,所以内存里的数据需要使用复杂的压缩算法和微优化来完成每次计算。正常情况下这样会工作的很好,直到你的数据大小超过了堆空间大小。这个问题看起来可以通过添加更多节点的方式解决。不过通常来说,堆空间问题总是会在 CPU 和 I/O 之前先到达瓶颈。

现有版本已经添加了 doc values 支持。本质上,doc values 提供了和内存中 fielddata 一样的功能,不过他们在写入索引的时候就直接落到了磁盘上。而好处就是:他们消耗很少的堆空间。Doc values 在读取的时候也不是从内存,而是从磁盘上读取。虽然访问磁盘很慢,但是 doc values 可以利用内核的文件系统缓存。文件系统缓存可不像 JVM 的堆,不会有 32GB 的限制。所以把 fielddata 从堆转移到文件系统缓存里,你只用消耗更小的堆空间,也意味着更快的垃圾回收,以及更稳定的节点

在本次发版之前,doc values 明显慢于在内存里的 fielddata 。而这次我们显著提升了性能,几乎达到了和在内存里一样快的效果。

用doc values 替换内存 fielddata,你只需要向下面这样构建新字段就行:

PUT /my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "timestamp": {
          "type":       "date",
          "doc_values": true
        }
      }
    }
  }
}

有了这个映射表,要用这个字段数据都会自动从磁盘加载 doc values 而不是进到内存里。注意:目前 doc values 还不能在经过分词器的 string 字段上使用。

request circuit breaker

fielddata 断路器之前已经被加入,用作限制 fielddata 可用的最大内存,这是导致 OOM 的最大恶因。而限制,我们把这个机制扩展到请求界别,用来限制每次请求可用的最大内存。

bloom filters

Bloom filters 在写入索引时提供了重要的性能优化 -- 用以检查是否有已存在的文档 id ,在通过 id 访问文档时,用来探测哪个 segment 包含这个文档。不过当然的,这也有代价,就是内存消耗。目前的改进是移除了对 bloom filters 的依赖。目前 Elasticsearch 只在写入索引(仅是真实用例上的经验,没有我们的测试用例证明)的时候构建它,但默认不再加载进内存。如果一切顺利的话,未来的版本里我们会彻底移除它。

集群稳定性

提高集群稳定性最大的工作就是提高节点稳定性。如果节点稳定且响应及时,就极大的减少了集群不稳定的可能。换句话说,我们活在一个不完美的世界 -- 事情总是往意料之外发展,而集群就需要能无损的从这些情况中恢复回来。

我们在 improve_zen 分支上花了几个月的时间来提高 Elasticsearch 从失败中恢复的能力。首先,我们添加测试用例来复原复杂的网络故障。然后为每个测试用例添加补丁。肯定还有很多需要做的,不过目前来说,用户们已经碰到过的绝大多数问题我们已经解决了,包括issue #2488 -- "minimum_master_nodes 在交叉脑裂时不起作用"。

我们非常认真的对待集群的弹性问题。希望你能明白 Elasticsearch 能为你做什么,也能明白它的弱点在哪。考虑到这点,我们创建了弹性状态文档。这个文档记录了我们以及我们的用户碰到过各种弹性方面的问题,有些可能已经修复,有些可能还没有。请认真阅读这篇文档,采取适当的措施来保护你的数据。

数据损坏探测

从网络恢复过来的分片的 checksum 帮助我们发现过一个压缩库的 bug,这是 1.3.2 版本的时候发生的事情。从那天起,我们给 Elasticsearch 添加了越来越多的 checksum 认证。

  • 在合并时,segment 中的所有文件都有自己的 checksum 验证(#7360).
  • 重新开所有索引的时候,segment 里的小文件完整的验证,大文件则做轻量级的分段验证(LUCENE-5842).
  • 从 transaction 日志重放事件的时候,每个事件都有自己的 checksum 验证(#6554).
  • During shard recovery, or when restoring from a snapshot, Elasticsearch needs to compare a local file with a remote copy to ensure that they are identical. Using just the file length and checksum proved to be insufficient. Instead, we now check the identity of all the files in the segment (#7159).

其他亮点

你可以在 Elasticsearch 1.4.0.Beta1 changelog 里读到这个版本的所有特性,功能和修复。不过还是有些小改动值得单独提一下的:

groovy 代替了 mvel

Groovy 现在成为了新的默认脚本语言。之前的 MVEL 太老了,而且它不能运行在沙箱里也带来了安全隐患。Groovy 是沙箱化的(这意味着可以放心的开启)(译者注:还记得1.2版本时候的所谓安全漏洞吧),而且 Groovy 有个很好的管理团队,运行速度也很快!更多信息见博客关于脚本的内容

默认关闭 cors

默认配置下的 Elasticsearch 很容易遭受跨站攻击。所以我们默认关闭掉 CORS。Elasticsearch 里的 site 插件会照常工作,但是外部站点不再被允许访问远程集群,除非你再次打开 CORS。我们还添加了更多的CORS 配置项让你可以控制哪些站点可以被允许访问。更多信息请看我们的安全页

请求缓存(query cache)

一个新的实验性分片层次的请求缓存可以让在静态索引上的聚合请求瞬间返回响应。想想你有一个仪表板展示你的网站每天的 PV 数。这个书在过去的索引上不可能再变化了,但是聚合请求在每次页面刷新的时候都需要重新计算。有了新的请求缓存,聚合结果就可以直接从缓存中返回,除非分片中的数据发生了变化。你不用担心会从缓存中得到过期的结果 -- 它永远都会跟没缓存一样。

新的聚合函数

我们添加了三个新的聚合函数:

filters

这是 `filter` 聚合的扩展。允许你定义多个桶(bucket),每个桶里有不同的过滤器。

children

相当于 `nested` 的父子聚合,`children` 可以针对属于某个父文档的子文档做聚合。

scripted_metric

给你完全掌控数据数值运算的能力。提供了在初始化、文档收集、分片层次合并,以及全局归并阶段的钩子。

获取 /index 的接口

之前,你可以分别为一个索引获取他的别名,映射表,配置等等。而get-index 接口 现在让你可以一次获取一个或者多个索引的全部信息。这在你需要创建一个跟已有索引很类似或者几乎一样的新索引的时候,相当有用。

索引写入和更新

在文档写入和更新方面也有一些改进:

  • 我们现在用 Flake IDs 自动生成文档的 ID。在查找主键的时候,能提供更好的性能。
  • 如果设置 detect_nooptrue,一个不做任何实际变动的更新操作现在消耗更小了。打开这个参数,就只有变更了 _source 字段内容的更新请求才能写入新版本文档。
  • 更新操作可以完全由脚本控制。之前,脚本只能在字段已经存在的时候运行,否则会插入一个 upsert 文档。现在 scripted_upsert 参数允许你在脚本中直接处理文档创建工作。

function score

非常有用的 function_score 请求现在支持权重参数,用来优化每个指定函数的相关性影响。这样你可以把更多权重给新近的而不是热点的,给价格而不是位置。此外,random_score函数不再被 segment 合并影响,增强了排序一致性。

试一试

下载 Elasticsearch 1.4.0.Beta1,尝试一下,然后在 Twitter 上@elasticsearch) 说出你的想法。你也可以在 GitHub issues 页上报告问题。

10/07
2014

Kibana 4 beta 1 发版日志


原文地址见:http://www.elasticsearch.org/blog/kibana-4-beta-1-released/

今天,我们自豪高兴满意控制不住地兴奋过头欣喜若狂相当高兴得给大家分享一下 Kibana 项目的未来,以及 Kibana 4 的第一个 beta 版本。

我现在就要!快给我!

这里下载,然后看 README.md 里新的而且更简单的安装流程。当然,你最好还是读一下本文剩下的内容,有很多超棒的秘诀呢!

欢迎来到 kibana 4

我们正走在 Kibana 4 的漫漫长路上:可以预见还会有好几个 beta 版本,每个都有新的特性,可视化和改善。我们梳理了各种反馈、邮件列表、IRC 以及 Github 的 issue ,把特性加入到这个 beta1 版本中,真是罪孽深重。我们已经在为 beta2 版本努力工作,在此,很高兴分享一下我们的 roadmap,查看 Github 上打有 "Roadmap" 标签的 issue。你们的反馈是我们永远做正确的事的保证。

反馈之外,我们回头想了想人们是怎么看数据的,更进一步,人们是怎么解决真实问题的。我们发现一个问题总是能引出另一些问题,而这些问题又能引出更多其他问题。如果你参加了 Monitotama,或者其他 Elasticsearch 见面会,你可能已经看到过 Kibana 4 概念性的原型演示。它可以让你创建更复杂的图标,Kibana 4 从 PoC 出发,扩展出一大堆新特性,让你编写问题,得到解答,然后解决之前从来没这么解决过的问题。

这种组合方式在 Kibana 4 中体现为聚合、搜索、可视化和仪表板融合在一起的方式。为了简化组成,我们把 Kibana 4 分成 3 个不同的界面,虽然一起工作,但是每个负责解决不同的一部分问题。

熟悉的界面

如果你是 Kibana 老用户,你会发现主页上 Discover 标签页的样子很熟悉。

Discover 功能跟原先的带有一个文档表格和事件时间轴的搜索界面很像。在搜索框里输入,敲回车,然后让 Kibana 去挖掘你的 Elasticsearch 索引。说到索引,有一个快速下拉菜单让你在搜索的时候灵活的在多个索引之间切换。要切换回上一个索引,点击浏览器的回退按钮即可。不喜欢新的搜索关键词?同样点击回退按钮就能返回原来的搜索词了。当然,搜索框的历史中也存着过去的记录。

说道搜索,你既可以输入 Lucene Query String 语法,也可以用上一个经常被要求的特性,Elasticsearch JSON 搜索 到搜索框里。我们知道 JSON 格式可能比较难输对,所以不管你输入的是 Lucene Query String 还是 JSON,我们都会在发送给 Elasticsearch 之前替你验证一遍语法。不管你在 Kibana 4 的任何位置输入请求,这点都是生效的。

这样搜索也可以保存下来留待后用。重要的是:搜索不在绑定在仪表板上,他们可以在 Discover 页上再次调用,也可以运用在可能稍后才添加到仪表板上的可视化页里。因为,不管你在仪表板的哪一屏,搜索一直都会通过 URL 传递,所以链接到搜索非常简单。

画图的在这里

Kibana 4 的 Visualize 标签是之前说的概念原型里最高潮的地方。Kibana 4 把 Elasticsearch 的 nested 聚合函数的威力带到鼠标点击上。比如我想知道哪些国家访问我的网站,什么时候访问的,他们是否登录认证了?通过一个 canvas 上的单一请求,我就可以问出上面这些问题,然后看到结果是怎么相互联系的:

Kibana 3 的时候,时间只能在 histogram 面板上显示,而 terms 只能在柱状图上显示。Kibana 4 可以利用多个 Elasticsearch 聚合函数。这包括 bucket 和 metric 聚合函数,其中有备受期待的基数(又叫唯一计数)聚合函数,更多支持还在实现中。我们不得不创建了一个全新的可视化框架来处理复杂的聚合函数。目前有三种支持的类型:柱状图,线状图和光圈图。同样,更多支持还在实现中。未来每个 Kibana 4 的 beta 版本都值得你期待。

光圈图类似多层次的饼图。理论上它可以有无限的环:

柱状图现在还不单单可以做时间。这里我们展示根据文件后缀名分解文件大小范围。

现在你可能已经注意到每个可视化页底部的灰色小条。点击它,就可以看到图背后的源数据,然后,在大众要求下,提供了导出到CSV 以便后续分析的功能。你还可以看到 Elasticsearch 请求和响应的内容,以及请求的处理耗时。

Visualization 既可以互动式搜索创建,让你在建图的时候修改请求,也可以关联到一个之前通过 Discover 标签创建保存的请求上。这样你可以关联一个请求到多个可视化页,如果需要更新一个搜索参数,只需要更新单独一个请求就行了。比如,假设你有多个图表,是用下面语句搜索图片内容的:

png OR jpg

保存成 "Images"。然后你打算支持动态 GIF 格式,你只需要更新 "Images" 的内容然后保存即可。所有关联了 "Images" 请求的图都会自动应用变更。

]

给我看更多的图!

当然,你依然可以创建令人惊叹的仪表板,而且它们现在更方便创建和管理了。过去那堆凌乱的配置框一去不复返了。添加进仪表板的每个面板都可以在 Visualize 标签页理创建、保存,并且重复利用。就像保存了的搜索可以在多个 visualizations 里使用一样,保存了的 visualization 也可以在多个仪表板里使用。你需要更新一个 visualization 的话,只需要在一个地方修改好,每个仪表板里的都会应用变更。

更进一步,虽然请求和可视化是绑定到一个选定的索引的,仪表板却不用。一个仪表板可以有从不同索引来的可视化。这意味着,你可以从你的用户索引关联到网站流量索引,从销售数据关联到市场研究再关联到气象站日志。这些都可以在同一屏上!

更多

一篇博客里完全不够说完全部内容,所以去下载安装然后亲自试试 HERE 吧。如果你来自 Kibana 3,我们收集了一个小小的 FAQ 解释:HERE。还是老话,我们需要你的反馈,构建 Kibana 4 的每一天,我们都用得着这些反馈,而我们也会继续让 Kibana 变得更好,更快,更简单。

10/07
2014

Kibana 3 升级到 4 的常见问答


原文见https://github.com/elasticsearch/kibana/blob/master/K3_FAQ.md

问:我在 Kibana 3 里最想要的某某特性有了么? 答:就会有了!我们已经以 ticket 形式发布了目前的 roadmap。查看 GitHub 上的 beta 里程碑,看看有没有你想要的特性。

问:仪表板模式是否兼容? 答:不好意思,不兼容了。要创建我们想要的新特性,还是用原先的模式是不可能的。Aggregation 跟 Facet 请求从根本上工作方式就不一样,新的仪表板不再绑定成行和列的样式,而且搜索框,可视化和仪表板的关系过于复杂,我们不得不重新设计一遍,来保证它的灵活可用。

问:怎么做多项搜索? 答:"filters" Aggregation 可以运行你输入多项搜索条件然后完成可视化。甚至你可以在这里面自己写 JSON。

问:模板化/脚本化仪表板还在么? 答:看看 URL 吧。每个应用的状态都记录在那里面,包括所有的过滤器,搜索和列。现在构建脚本化仪表板比过去简单多了。URL 是采用 RISON 编码的。

译者注:

RISON 是一个跟 JSON 很类似,还节省不少长度的东西。其官网见:http://mjtemplate.org/examples/rison.html。但是我访问看似乎已经挂了,更多一点的说明可以看https://github.com/Nanonid/rison

09/24
2014

在 logstash 里使用其他 RubyGems 模块


在开发和使用一些 logstash 自定义插件的时候,几乎不可避免会导入其他 RubyGems 模块 —— 因为都用不上模块的小型处理,直接写在 filters/ruby 插件配置里就够了 —— 这时候,运行 logstash 命令可能会发现一个问题:这个 gem 模块一直是 "no found" 状态。

这其实是因为我们一般是通过 java 命令来运行的 logstash,这时候它回去寻找的 Gem 路径跟我们预计中的是不一致的。

要查看 logstash 运行时实际的 Gem 查找路径,首先要通过 ps aux 命令确定 ruby 的实际运行方式:

$ ps uax|grep logstash
raochenlin      27268  38.0  4.3  3268156 181344 s003  S+    7:10PM   0:22.36 /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java -Xmx500m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -Djava.awt.headless=true -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -jar /Downloads/logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar -I/Users/raochenlin/Downloads/logstash-1.4.2/lib /Users/raochenlin/Downloads/logstash-1.4.2/lib/logstash/runner.rb agent -f test.conf

看,实际的运行方式应该是:java -jar logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar -Ilogstash-1.4.2/lib logstash-1.4.2/lib/logstash/runner.rb 这样。

那么我们查看 gem 路径的命令也就知道怎么写了:

java -jar logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar `which gem` env

你会看到这样的输出:

RubyGems Environment: - RUBYGEMS VERSION: 2.1.9 - RUBY VERSION: 1.9.3 (2014-02-24 patchlevel 392) [java] - INSTALLATION DIRECTORY: file:/Downloads/logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar!/META-INF/jruby.home/lib/ruby/gems/shared - RUBY EXECUTABLE: java -jar /Downloads/logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar - EXECUTABLE DIRECTORY: file:/Downloads/logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar!/META-INF/jruby.home/bin - SPEC CACHE DIRECTORY: /.gem/specs - RUBYGEMS PLATFORMS: - ruby - universal-java-1.7 - GEM PATHS: - file:/Downloads/logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar!/META-INF/jruby.home/lib/ruby/gems/shared - /.gem/jruby/1.9 - GEM CONFIGURATION: - :update_sources => true - :verbose => true - :backtrace => false - :bulk_threshold => 1000 - "install" => "--no-rdoc --no-ri --env-shebang" - "update" => "--no-rdoc --no-ri --env-shebang" - :sources => ["http://ruby.taobao.org/"] - REMOTE SOURCES: - http://ruby.taobao.org/ - SHELL PATH: - /usr/bin - /bin - /usr/sbin - /sbin - /usr/local/bin

看到其中的 GEM PATHS 部分,是一个以 file: 开头的路径!也就是说,要求所有的 gem 包都打包在这个 jruby-complete-1.7.11.jar 里面才认。

所以我们需要把额外的 gem 包,也加入这个 jar 里:

jar uf jruby-completa-1.7.11.jar META-INF/jruby.home/lib/ruby/1.9/CUSTOM_RUBY_GEM_LIB

注:加入 jar 是用的相对路径,所以前面这串目录要提前创建然后复制文件进去。

当然,其实还有另一个办法。

让我们返回去再看一次 logstash 的进程,在 jar 后面,还有一个 -I 参数!所以,其实我们还可以把文件安装在 logstash-1.4.2/lib 目录下去。

最后,你可能会问:那 --pluginpath 参数指定的位置可不可以呢?

答案是:也可以。

这个参数指定的位置在 logstash-1.4.2/lib/logstash/agent.rb 中,被加入了 $LOAD_PATH 中:

  def configure_plugin_path(paths)
    paths.each do |path|
      if !Dir.exists?(path)
        warn(I18n.t("logstash.agent.configuration.plugin_path_missing",
                    :path => path))
      end
      plugin_glob = File.join(path, "logstash", "{inputs,codecs,filters,outputs}", "*.rb")
      if Dir.glob(plugin_glob).empty?
        @logger.warn(I18n.t("logstash.agent.configuration.no_plugins_found",
                    :path => path, :plugin_glob => plugin_glob))
      end
      @logger.debug("Adding plugin path", :path => path)
      $LOAD_PATH.unshift(path)
    end
  end

$LOAD_PATH 是 Ruby 的一个特殊变量,类似于 Perl 的 @INC 或者 Java 的 class_path 。在这个数组里的路径下的文件,都可以被 require 导入。

可以运行如下命令查看:

$ java -jar logstash-1.4.2/vendor/jar/jruby-complete-1.7.11.jar -e 'p $LOAD_PATH'
["file:/Users/raochenlin/Downloads/logstash-1.4.2/vendor/jar/rar/jruby-complete-1.7.11.jar!/META-INF/jruby.home/lib/ruby/1.9/site_ruby", "file:/Users/raochenlin/Downloads/logstash-1.4.2/vendor/jar/rar/jruby-complete-1.7.11.jar!/META-INF/jruby.home/lib/ruby/shared", "file:/Users/raochenlin/Downloads/logstash-1.4.2/vendor/jar/rar/jruby-complete-1.7.11.jar!/META-INF/jruby.home/lib/ruby/1.9"]

这三种方式,你喜欢哪种呢?

09/23
2014

Kibana 认证鉴权方案


用 Perl5 实现的完整方案

Kibana 作为一个纯 JS 项目,一直都没有提供完整的权限控制方面的功能。只是附带了一个 nginx.conf 做基本的 Basic Auth。社区另外有在 nodejs 上实现的方案,则使用了 CAS 方式做认证。

不过我对这两种方案都不太满意。

  1. 认证方式太单一,适应性不强;
  2. 权限隔离不明确,只是通过修改 kibana-intkiban-int-user 来区分不同用户的 dashboard,并不能限制用户对 ES 索引的访问。

加上 nodejs 我也不熟,最终在多番考虑后,决定抽一个晚上自己写一版。

最终代码见 https://github.com/chenryn/kibana

原理和实现

  1. 全站代理和虚拟响应

    这里不单通过 config.js 限定了 kibana 默认连接的 Elasticsearch 服务器地址和端口,还拦截伪造了 /_nodes 请求的 JSON 响应体。伪造的响应中也只包含自己这个带认证的 web 服务器地址和端口。

    这么做是因为我的 kibana 版本使用的 elasticjs 库比官方新增了 sniff 功能,默认会自动轮训所有 nodes 发送请求。

  2. 新增 kibana-auth 鉴权索引

    在通常的 kibana-int-user 区分 dashboard 基础上,我新增加 kibana-auth 索引,专门记录每个用户可以访问的 ES 集群地址和索引前缀。请求会固定代理到指定的 ES 集群上,并且确认是被允许访问的索引。

    这样,多个用户通过一个 kibana auth 服务器网址,可以访问多个不同的 ES 集群后端。而同一个 ES 集群后端的索引,也不用担心被其他人访问到。

  3. Authen::Simple 认证框架

    这是 Perl 一个认证框架,支持十多种不同的认证方式。项目里默认采用最简单的 htpasswd 文件记录方式,实际我线上是使用了 LDAP 方式,都没问题。

部署

方案采用了 Mojolicious 框架开发,代码少不说,最关键的是 Mojolicious 无额外的 CPAN 模块依赖,这对于不了解 Perl 但是又有 Kibana 权限控制需求的人来说,大大减少了部署方面的麻烦。

curl http://xrl.us/cpanm -o /usr/local/bin/cpanm
chmod +x /usr/local/bin/cpanm
cpanm Mojolicious Authen::Simple::Passwd

三行命令,就可以完成整个项目的安装需求了。然后运行目录下的:

hypnotoad script/kbnauth

就可以通过 80 端口访问这个带有权限控制的 kibana 了。

权限赋值

因为 kibana-auth 结构很简单,kibana 一般又都是内部使用,所以暂时还没做权限控制的管理页面。直接通过命令行方式即可赋权:

curl  -XPOST http://127.0.0.1:9200/kibana-auth/indices/sri -d '{
  "prefix":["logstash-sri","logstash-ops"],
  "server":"192.168.0.2:9200"
}'

这样,sri 用户,就只能访问 192.168.0.2 集群上的 logstash-sri 或 logstash-ops 开头的日期型索引(即后面可以-YYYY, -YYYY.MM, -YYYY.MM.dd 三种格式)了。

下一步

考虑到新方案下各用户都有自己的 kibana-int-user 索引,已经用着官方 kibana 的用户大批量的 dashboard 有迁移成本,找个时间可能做一个迁移脚本辅助这个事情。

开发完成后,得到了 @高伟 童鞋的主动尝试和各种 bug 反馈支持,在此表示感谢~也希望我这个方案能帮到更多 kibana 用户。

注:我的 kibana 仓库除了新增的这个 kbnauth 代理认证鉴权功能外,本身在 kibana 分析统计功能上也有一些改进,这方面已经得到多个小伙伴的试用和好评,自认在官方 Kibana v4 版本出来之前,应该会是最好用的版本。欢迎大家下载使用!

新增功能包括:

  1. 仿 stats 的百分比统计面板(利用 PercentileAggr 接口)
  2. 仿 terms 的区间比面板(利用 RangeFacets 接口)
  3. 给 bettermap 增强的高德地图支持(利用 leaflet provider 扩展)
  4. 给 map 增强的中国地图支持(利用 jvectormap 文件)
  5. 给 map 增强的 term_stats 数据显示(利用 TermStatsFacets 接口)
  6. 给 query 增强的请求生成器(利用 getMapping/getFieldMapping 接口和 jQuery.multiSelect 扩展)
  7. 仿 terms 的 statisticstrend 面板(利用 TermStatsFacets 接口)
  8. 仿 histogram 增强的 multifieldhistogram 面板(可以给不同query定制不同的panel setting,比如设置某个抽样数据 * 1000 倍和另一个全量数据做对比)
  9. 仿 histogram 的 valuehistogram 面板(去除了 histogram 面板的 X 轴时间类型数据限制,可以用于做数据概率分布分析)
  10. 给 histogram 增强的 threshold 变色功能(利用了 jquery.flot.threshold 扩展)
  11. 单个面板自己的刷新按钮(避免调试的时候全页面刷新的麻烦)

效果截图同样在 README 里贴出。欢迎试用和反馈!

09/04
2014

用 Spark 处理数据导入 Elasticsearch


探索用 spark 而不是 logstash 输入 ES 的方式

Logstash 说了这么多。其实运用 Kibana 和 Elasticsearch 不一定需要 logstash,其他各种工具导入的数据都可以。今天就演示一个特别的~用 Spark 来处理导入数据。

首先分别下载 spark 和 elasticsearch-hadoop 的软件包。注意 elasticsearch-hadoop 从最新的 2.1 版开始才带有 spark 支持,所以要下新版:

wget http://d3kbcqa49mib13.cloudfront.net/spark-1.0.2-bin-cdh4.tgz
wget http://download.elasticsearch.org/hadoop/elasticsearch-hadoop-2.1.0.Beta1.zip

分别解压开后,运行 spark 交互命令行 ADD_JARS=../elasticsearch-hadoop-2.1.0.Beta1/dist/elasticsearch-spark_2.10-2.1.0.Beta1.jar ./bin/spark-shell 就可以逐行输入 scala 语句测试了。

注意 elasticsearch 不支持 1.6 版本的 java,所以在 MacBook 上还设置了一下 JAVA_HOME="/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home" 启用自己从 Oracle 下载安装的 1.7 版本的 Java。

基础示例

首先来个最简单的测试,可以展示写入 ES 的用法:

import org.apache.spark.SparkConf
import org.elasticsearch.spark._

// 更多 ES 设置,见
val conf = new SparkConf()
conf.set("es.index.auto.create", "true")
conf.set("es.nodes", "127.0.0.1")

// 在spark-shell下默认已建立
// import org.apache.spark.SparkContext    
// import org.apache.spark.SparkContext._
// val sc = new SparkContext(conf)

val numbers = Map("one" -> 1, "two" -> 2, "three" -> 3)
val airports = Map("OTP" -> "Otopeni", "SFO" -> "San Fran")

sc.makeRDD(Seq(numbers, airports)).saveToEs("spark/docs")

这就 OK 了。尝试访问一下:

$ curl '127.0.0.1:9200/spark/docs/_search?q=*'

返回结果如下:

{"took":66,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":2,"max_score":1.0,"hits":[{"_index":"spark","_type":"docs","_id":"BwNJi8l2TmSRTp42GhDmww","_score":1.0, "_source" : {"one":1,"two":2,"three":3}},{"_index":"spark","_type":"docs","_id":"7f7ar-9kSb6WEiLS8ROUCg","_score":1.0, "_source" : {"OTP":"Otopeni","SFO":"San Fran"}}]}}

文件处理

下一步,我们看如何读取文件和截取字段。scala 也提供了正则和捕获的方法:

var text = sc.textFile("/var/log/system.log")
var Pattern = """(\w{3}\s+\d{1,2} \d{2}:\d{2}:\d{2}) (\S+) (\S+)\[(\d+)\]: (.+)""".r
var entries = text.map {
    case Pattern(timestamp, host, program, pid, message) => Map("timestamp" -> timestamp, "host" -> host, "program" -> program, "pid" -> pid, "message" -> message)
    case (line) => Map("message" -> line)
}
entries.saveToEs("spark/docs")

这里示例写了两个 case ,因为 Mac 上的 "system.log" 不知道用的什么 syslog 协议,有些在 [pid] 后面居然还有一个 (***) 才是 :。正好就可以用这个来示例如果匹配失败的情况如何处理。不加这个默认 case 的话,匹配失败的就直接报错不会存进 entries 对象了。

注意:.textFile 不是 scala 标准的读取文件函数,而是 sparkContext 对象的方法,返回的是 RDD 对象(包括后面的 .map 返回的也是新的 RDD 对象)。所以后面就不用再 .makeRDD 了。

网络数据

Spark 还有 Spark streaming 子项目,用于从其他网络协议读取数据,比如 flume,kafka,zeromq 等。官网上有一个配合 nc -l 命令的示例程序。

import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("localhost", 9999)
...
ssc.start()
ssc.awaitTermination()

有时间我会继续尝试 Spark 其他功能。

08/29
2014

山寨一个 Splunk 的 source 上下文查看功能


介绍 ES 的 _id 设计及 logstash 中自定义 id 的方法

跟很多朋友在聊 elk stack 的时候,都会不知不觉的开始跟 Splunk 做对比。最常见的两个抱怨就是:Splunk 的搜索构建语法 比 Kibana 方便,以及 Splunk 搜索出来的消息可以通过点击 Source 按钮查看其原始日志中的前后几条日志。

splunk source context

平心而论,这个上下文查找的功能确实在排错过程中非常有用。但是在 elk 里却不那么容易实现,原因是:

elasticsearch 是一个分布式项目,其索引的 _id 默认使用的是 UUID 方式生成的随机字符串,你没法根据 UUID 来判断数据的先后。

LogStash::Outputs::Elasticsearch 提供了让你指定 _id 内容的选项,但是在集群环境下,你很难自己搞定一个全局自增 ID。

相反,虽然我不知道 splunk 的数据存储的内部实现,但是就他昂贵的报价来说,基本只见过单机案例。就单机而言,自增 id 太轻松了

所以,从原理上来说,就很难实现一个通用的 elk 版上下文查看功能。

不过我们缩小一下使用场景,却未必不能自己山寨一个对自己可用的办法来。

假设我们一个最常见的场景,就是从各 web 服务器上收集不同日志到中心。那么这时候,通过 %{host}%{path} 的 "AND" 过滤,我们就可以把范围缩小到一个单一的文件内容里。所以,我们只需要能够搞定这个文件的自增 id 就够了!

logstash.conf 示例

input {
    file {
        path => ["/var/log/*.log"]
    }
}
filter {
    ruby {
        init => '@incr={}'
        code => "key = event['host']+event['path']
                 if @incr.has_key?(key)
                     @incr[key] += 1
                 else
                     @incr[key] = 1 
                 end
                 event['lineno'] = @incr[key]"
    }
}
output {
    elasticsearch {
    }
}

上下文查询 curl 示例

使用上面的配置运行起来 logstash 之后,假设我们现在搜到一条 syslog 日志,其 lineno 是 20,那么查看它的前后 5 条记录的 curl 命令就是:

curl -XPOST 'http://localhost:9200/logstash-2014.08.29/_search?pretty=1' -d '
{
  "query":{
    "range":{
      "lineno": {
        "gt":15,
        "lte":25
      }
    }   
  },  
  "filter":{
    "term":{
      "host.raw":"raochenlindeMacBook-Air.local",
      "path.raw":"/var/log/system.log"
    }
  },
  "sort":[{"lineno":"asc"}],
  "fields":["message"],
  "size":10
}'

得到的结果是:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 10,
    "max_score" : null,
    "hits" : [ {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "ILkv4oZOQRGXkH5nxjPT6Q",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:34:44 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391727104]: Service [sproxy] accepted connection from 127.0.0.1:52673" ]
      },
      "sort" : [ 16 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "frRzVZUDQr-dkRog9LEypQ",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:34:44 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391727104]: s_connect: connected 50.116.12.155:65080" ]
      },
      "sort" : [ 17 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "fQ50VrbuSfy6AmhNOaHpFg",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:34:44 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391727104]: Service [sproxy] connected remote server from 192.168.0.102:52674" ]
      },
      "sort" : [ 18 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "Bpza8x6gSQi3OFRfAz3vPA",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:35:23 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391882752]: Service [sproxy] accepted connection from 127.0.0.1:52710" ]
      },
      "sort" : [ 19 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "I7SQ4o-aSr--em1WXO0y0A",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:35:24 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391882752]: s_connect: connected 50.116.12.155:65080" ]
      },
      "sort" : [ 20 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "POLq7XA_QVe6E5f9cP9V-w",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:35:24 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391882752]: Service [sproxy] connected remote server from 192.168.0.102:52711" ]
      },
      "sort" : [ 21 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "sXCLVr7URu-2uKhcOP3wjA",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:35:35 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391882752]: Connection closed: 0 byte(s) sent to SSL, 0 byte(s) sent to socket" ]
      },
      "sort" : [ 22 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "3wxxElNuS7OgyvjSm8CQfg",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:36:25 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391571456]: Connection closed: 2825 byte(s) sent to SSL, 2407 byte(s) sent to socket" ]
      },
      "sort" : [ 23 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "xdsiB1cmRpagWiMxtAjMzQ",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:36:52 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391493632]: Connection closed: 1109 byte(s) sent to SSL, 583 byte(s) sent to socket" ]
      },
      "sort" : [ 24 ]
    }, {
      "_index" : "logstash-2014.08.29",
      "_type" : "logs",
      "_id" : "mLScPMbwTzSPMz9WqOPXlw",
      "_score" : null,
      "fields" : {
        "message" : [ "Aug 29 23:36:52 raochenlindeMacBook-Air.local stunnel[304]: LOG5[4391571456]: Service [sproxy] accepted connection from 127.0.0.1:52719" ]
      },
      "sort" : [ 25 ]
    } ]
  }
}

没错,这就是我们想要的结果了!

注释

这里两个要点:

  • 自增 id 为啥不用行号,因为 LogStash::Inputs::File 实现是通过 File.seekFile.sysread(16394) 完成的,这种时候 File.lineno 永远都是 0。获取真的行号很困难。
  • 自增 id 为什么不指定成 _id 而是另外存字段,因为 _id 是特殊字段,要求在一个 _index/_type 里是唯一的。我们对 logstash 的使用一般情况下都是多个 host 内容存在同一个 _index/_type 下,会发生重复的(重复写入 _id 相同的数据等同于 update 操作)。

延伸

数据如何通过 kibana 展示,则是另外一个层面的内容。有时间可能我会也做一下。

非 input/file 方式的其他场景,只要你能通过 event 中其他字段确定出来源唯一,都可以采用这个方式做。

08/18
2014

用 ES 的 RangeFacets 接口实现一个查看区间占比的 Kibana 面板


以 range 为例演示如何定制自己的 kibana panel

公司用 kibana 的同事提出一个需求,希望查看响应时间在不同区间内占比的饼图。第一想法是用 1.3.0 新加的 percentile rank aggregation 接口。不过仔细想想,其实并不合适 —— 这个接口目的是计算固定的 [0 TO $value] 的比例。不同的区间反而还得自己做减法来计算。稍微查了一下,更适合的做法是专门的 range aggregation。考虑到 kibana 内大多数还是用 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 发请求了 —— 虽然之前我实现 percentile panel 的时候就是这么做的 —— 前两天合并了 github 上一个 commit 后,现在可以用高版本的 elastic.js 了,所以我也把原来用原生 $http.post 方法写的 percentile panel 用 elastic.js 对象重写了。

elastic.js 关于 range facet 的文档见:http://docs.fullscale.co/elasticjs/ejs.RangeFacet.html

因为 range facet 本身比较简单,所以 RangeFacet 对象支持的方法也比较少。一个 addRange 方法添加 ranges 数组,一个 field 方法添加 field 名称,就没了。

所以这个新 panel 的实现,更复杂的地方在如何让 range 范围值支持自定义填写。这一部分借鉴了同样是前两天合并的 github 上另一个第三方面板 multifieldhistogram 的写法。

另一个需要注意的地方是饼图出来以后,单击饼图区域,自动生成的 filterSrv 内容。一般的面板这里都是 terms 类型的 filterSrv,传递的是面板的 label 值。而我们这里 label 值显然不是 ES 有效的 terms 语法,还好 filterSrvrange 类型(histogram 面板的 time 类型的 filterSrv 是在 daterange 基础上实现的),所以稍微修改就可以了。

最终效果如下:

面板的属性界面如下:

代码已经上传到我个人 fork 的 kibana 项目里:https://github.com/chenryn/kibana.git

我这个 kibana 里已经综合了 8 个第三方面板或重要修改。在官方年底推出 4.0 版本之间,自觉还是值得推荐给大家用的。具体修改说明和效果图见 README。

07/28
2014

Kibana 动态仪表板的使用


如何通过 url 参数动态变更仪表盘内容

半年前,Kibana3.4 版刚出来的时候,曾经在官方博客上描述了一个新功能,当时我的翻译见:【翻译】Kibana3 里程碑 4

今天我实际使用了一下这个新功能,感觉还是蛮有用的,单独拿出来记录一下用法和一些没在之前文章里提到的细节。

使用方法

使用方法其实在官方描述里已经比较清楚了。就是在原本的 http://127.0.0.1:9292/#/dashboard/file/logstash.json 地址后面,再加上请求参数 ?query=*** 即可。

注意事项

看起来好像太过简单,不过用起来其实还是有点注意事项的:

  • Kibana 目前不支持对保存在 Elasticsearch 中的 dashboard 做这个事情。

所以一定得保存成 yourname.json 文件放入 app/dashboards/ 目录里才行。

  • 静态的 JSON 文件其实是利用模板技术。

所以直接导出得到的 JSON 文件还不能直接起作用。需要稍微做一点修改。

你可以打开默认可用的 logstash.json 文件,看看有什么奇特的地方,没错,就是下面这样:

"query": "{<span>{</span>ARGS.query || '*'}}"

而你自己保存下来的 JSON,这里都会是具体的数据。所以,要让自己的 JSON 布局也支持动态仪表板的话,按照这个写法也都加上 ARGS.query 就好了!

从 logstash.json 里还可以看到,除了 ?query= 以外,其实还支持 from= 参数,默认是 24h

  • query 参数的特殊字符问题。

比如我之前在搜索框里输入的 querystring 是这样的:type:mweibo_action AND urlpath:"/2/statuses/friends_timeline.json"

那么实际用的时候,如果写成这样一个 url:http://127.0.0.1:9292/#/dashboard/file/logstash.json?query=type:mweibo_action AND urlpath:"/2/statuses/friends_timeline.json",实际是不对的。我一度怀疑是不是 urlpath 里的 / 导致的问题,后来发现,其实是 " 在进 JSON 文件模板变量替换的时候给当做只是字符串赋值引号的作用,就不再作为字符串本身传递给 Elasticsearch 作为请求内容本身了。所以需要用 \" 做转义。

(这里一定要有 " 的原因是,ES 的 querystring 里,field:/regex/ 是正则匹配搜索的语法,刚好 url 也是以 / 开头的)

所以可用的 url 应该是:http://127.0.0.1:9292/#/dashboard/file/logstash.json?query=type:mweibo_action AND urlpath:\"/2/statuses/friends_timeline.json\"

经过 url_encode 之后就变成了:http://127.0.0.1:9292/#/dashboard/file/logstash.json?query=type:mweibo_action%20AND%20urlpath:%5C%22%2F2%2Fstatuses%2Ffriends_timeline.json%5C%22

这样就可以了!

  • 用 JSON 的局限。

动态仪表板其实有两种用法,这里只用到了 file/logstash.json 静态文件方式,这种方式只支持一个 query 条件,也没有太多的附加参数支持。而 script/logstash.js 方式,支持多个 query 条件,以及 index、pattern、interval、timefield 等更多的参数选项。

当然,研究一下 angularjs 的用法,给 JSON 文件里也加上 ARGS.querysplit 方法,也不算太难。

06/11
2014

用 LEK 组合处理 Nginx 访问日志


介绍 logstash 中用 ruby 插件替代 grok 插件的性能优化,以及自行发送数据到 ES 的办法

Tengine 支持通过 syslog 方式发送日志(现在 Nginx 官方也支持了),所以可以通过 syslog 发送访问日志到 logstash 平台上,这种做法相对来说对线上服务器影响最小。最近折腾这件事情,一路碰到几个难点,把解决和优化思路记录一下。

少用 Grok

感谢群里 @wood 童鞋提供的信息,Grok 在高压力情况下确实比较容易率先成为瓶颈。所以在日志格式可控的情况下,最好可以想办法跳过使用 Grok 的环节。在早先的 cookbook 里,就有通过自定义 LogFormat 成 JSON 样式的做法。我前年博客上也写过 nginx 上如此做的示例:http://chenlinux.com/2012/09/21/json-event-for-logstash/index.html

不过这次并没有采用这种方式,而是定义日志格式成下面的样子,因为这种分割线方式对 Hive 平台同样是友好的。

log_format syslog '$remote_addr|$host|$request_uri|$status|$request_time|$body_bytes_sent|'
                  '$upstream_addr|$upstream_status|$upstream_response_time|'
                  '$http_referrer|$http_add_x_forwarded_for|$http_user_agent';
access_log syslog:user:info:10.4.16.68:29125:tengine syslog ratio=0.1;

那么不用 Grok 怎么做呢?这里有一个很炫酷的写法。下面是 logstash 配置里 filter 段的实例:

    filter {
        ruby {
            remove_field => ['@version', 'priority', 'timestamp', 'logsource', 'severity', 'severity_label', 'facility', 'facility_label', 'pid','message']
            init => "@kname = ['client','servername','url','status','time','size','upstream','upstreamstatus','upstreamtime','referer','xff','useragent']"
            code => "event.append(Hash[@kname.zip(event['message'].split('|'))])"
        }
        mutate {
            convert => ["size", "integer", "time", "float", "upstreamtime", "float"]
        }
        geoip {
            source => "client"
            fields => ["country_name", "region_name", "city_name", "real_region_name", "latitude", "longitude"]
            remove_field => [ "[geoip][longitude]", "[geoip][latitude]" ]
        }
    }

而要达到跟这段 ruby+mutate 效果一致的 grok ,写法是这样的:

    filter {
        grok {
            match => ["message", "%{IPORHOST:client}\|%{HOST:servername}\|%{URIPATHPARAM:url}\|%{NUMBER:status}\|(?:%{NUMBER:time:int}|-)\|(?:%{NUMBER:size}|-)\|(?:%{HOSTPORT:upstream}|-)\|(?:%{NUMBER:upstreamstatus}|-)\|(?:%{NUMBER:upstreamtime:int}|-)\|(?:%{URI:referer}|-)\|%{GREEDYDATA:xff}\|%{GREEDYDATA:useragent}"]
            remove_field => ['@version', 'priority', 'timestamp', 'logsource', 'severity', 'severity_label', 'facility', 'facility_label', 'pid','message']
        }
    }

syslog 瓶颈

运行起来以后,通过 Kibana 看到的全网 tengine 带宽只有 60 MBps左右,这个结果跟通过 NgxAccounting 统计输出的结果差距太大了。明显是有问题。

首先怀疑不会是 nginx.conf 通过 Puppet 下发重启的时候有问题吧?实际当然没有。

这时候运行 netstat -pln | grep 29125 命令,发现 Recv-Q 已经达到了 228096,并且一致维持在这个数没有变化。

由于之前对 ES 写入速度没太大信心,所以这时候的反应就是去查看 ES 服务器的状态,结果其实服务器 idle% 在 80% 以上,各种空闲,Kibana 上搜索反应也非常快。通过 top 命令看具体的线程情况,logstash 的 output/elasticsearch worker 本身占用资源就很少。包括后来实际也尝试了加大 output 的 workers 数量,加大 bin/logstash -w 的 filter worker 数量,其实都没用。

那么只能是 input/syslog 就没能收进来了。

之前写 filter 的时候,开过 -vv 模式,所以注意到过 input/syslog 里是利用 Logstash::Filter::Grok 来判定切割 syslog 内容的。按照前一节的说法,那确实可能是在收 syslog 的时候性能跟不上啊?

于是去翻了一下 Logstash::Input::Syslog 的代码,主体逻辑很简单,就是 Thread.new { UDPSocket.new } 这样。也就是说是一个单线程监听 UDP 端口!

然后我又下载了同为 Ruby 写的日志收集框架 fluentd 的 syslog 插件看看源代码,fluent-plugin-syslog 里,用的是 Cool.io 库作 UDP 异步处理。好吧,其实在此之前我只知道 EventMachine 库。。。不过由于 Logstash 是 JRuby 平台,又不清楚其 event 代码(以前基本只是看各种 plugin 的代码就够了),担心这么把 em 加上去会不会不太好。所以在摸清 logstash 代码之前,先用自己最熟悉的手段,搞定这个问题:

用 Perl 的高性能 EV 库解决

前年我同样提到过 Perl 也有仿照 Logstash 写的框架叫 Message::Passing,这个框架就是用 AnyEvent 和 Moo 写的,性能绝对没问题。不过各种插件和文档比较潦草,要想兼容现在 logstash 1.4 的 schema 比较费劲。所以,最后我选择了自己根据 tengine 日志的情况单独写一个脚本,结果如下:

80 行左右的代码,从 input 到 output 都是 anyevent 驱动。( Search::Elasticsearch::Async 默认是基于 AnyEvent::HTTP 的,不过用 Promises 模块做了封装,所以写起来好像看不太出来~)

最终到 elasticsearch 里的数据结构跟 logstash 一模一样,之前配置好的 Kibana 样式完全不需要变动。而实际运行起来以后,Recv-Q 虽然不是一直保持在 0,但是偶然累积的队列也肯定会在几秒钟内被读取处理完毕。完全达到了效果。Kibana 上,带宽图回复到了跟 NgxAccounting 统计结果一样的 300 MBps 。成功!

05/17
2014

给 Kibana 实现百分比统计图表


介绍如何给 Kibana 添加自定义 panel

kibana 图表类型中有个 stats 类型,返回对应请求的某指定数值字段的数学统计值,包括最大值、最小值、平均值、方差和标准差(当前通过 logstash-1.4.1 分发的 kibana 版本还只支持单列显示,前天,即 5 月 15 日刚更新了 Kibana 3.1 版,支持多列同时显示)。这个 stats 图表是利用 Elasticsearch 的 facets 功能来实现的。而在 Elasticsearch 1.0 版本以后,新出现了一个更细致的功能叫 aggregation,按照官方文档所说,会慢慢的彻底替代掉 facets。具体到 1.1 版本的时候, aggregation 里多了一项 percentile,可以具体返回某指定数值字段的区间分布情况。这对日志分析可是大有帮助。对这项功能,Elasticsearch 官方也很得意的专门在博客上写了一篇报道:Averages can be misleading: try a percentile

周五晚上下班前,我突然决定试试给 Kibana 加上 percentile 图表类型。因为群里正好携程的同学说到他们仿造 trend 类型做了 stat_trend 图表,我想 percentile 从数据结构到展示方法跟 stats 都很像,应该难度不大,正好作为学习 angularjs 的入手点好了。

花了半天多的时间,基本搞定这件事情,中间几度碰到难题,这里记录一下:

kibana 3.1 中的 elasticjs 版本

这是一个非常非常坑爹的地方,kibana/src/vendor/elasticjs/elastic.js 文件开头写着版本号是 v1.1.1,但是其实它是大半年前(2013-08-14)的。而实际它加上 aggregation 支持的时间是今年的 3 月 16 号,最近版本是 3 月 21 号发布的 ——但是版本号依然是 v1.1.1!!

我在昨天晚上花了一个多小时慢慢看完了 elasticjs 官网上 v1.1.1 的接口说明,结果其实在 kibana3.1 自带的 elasticjs 上完全不可用。

elasticjs 新版用法

随后我替换成了最新的 elasticjs 文件,结果依然不可用,仔细看过文档后发现,新的 elasticjs 只专心处理请求的 DSL,把客户端初始化、配置、收发等事情都交给了 Elasticsearch 官方发布的 elasticsearch.js 来完成。原先版本自带的 elastic-angular-client.js 压根就没用了。

变动大成这样了,居然还不改版本号!?!?

elasticsearch.js 的多层目录

下载了 elasticsearch.js 源码后,发现目录里有一个 elasticsearch.angular.client.js 文件,于是我很开心的想,官方考虑的还是很周全的嘛!然后花了一阵功夫在 kibana/src/app/app.js、kibana/src/app/components/require.config.js 等各处添加上了这个 elasticsearch 模块。结果依然不可用。

原来整个 elasticsearch.js 把功能模块化拆分到了很多个不同的多层次的目录里,然后相互之间广泛采用类似 require('../lib/util/') 这样的语句进行加载。

但是:Kibana 采用的是 requirejs 和 angularjs 合作的模式,整个 js 库的加载过程完全在 kibana/src/app/components/require.config.js 一个文件里定义,你可以看到这个文件里就写了很多 jquery 的子项目文件,但是这些文件都是平铺在 kibana/src/vendor/jquery/ 这个目录里的。

所以,即便在 require.config.js 里写了 elasticsearh 也没用,文件里的 require 语句依然是报错的。而且再往下的压根没法继续添加到 require.config.js 里了,因为太复杂了,肯定得修改 elasticsearch.js 源码的各个文件。

总的来说,就是 elasticsearch.js 不适合跟 requirejs 一起工作。


至此,简单更新 js 库然后调用现成接口的计划完全破产。

感谢 Elasticsearch 本身就是一个 RESTful 接口,所以还剩下一个不太漂亮但是确实好用的办法,那就是自己组装请求数据,直接通过 angularjs 内置的 $http 收发。

aggregation_name 的限制

angularjs 的 $http.post 使用跟 jquery 的 $.post 非常类似,所以写起来难度不大,确定这个思路之后唯一碰到的问题却是 Elasticsearch 本身的新限制。

目前 Kibana 里都是以 alias 形式来区分每一个子请求的,具体内容是 var alias = q.alias || q.query;,即在页面上搜索框里写的查询语句或者是搜索框左侧色彩设置菜单里的 Legend value

比如我的场景下,q.query 是 "xff:10.5.16.*",q.alias 是"教育网访问"。那么最后发送的请求里这条过滤项的 facets_name 就叫 "stats_教育网访问"。

同样的写法迁移到 aggregation 上就完全不可解析了。服务器会返回一条报错说:aggregation_name 只能是字母、数字、_ 或者 - 四种。

(这里比较怪的是抓包看到 facets 其实也报错说请求内容解析失败,但是居然同时也返回了结果,只能猜测目前是处在一种兼容状态?)

于是这里稍微修改了一下逻辑,把 queries 数组的 _.each 改用 $.each 来做,这样回调函数里不单返回数组元素,还返回数组下标,下标是一定为数字的,就可以以数组下标作为 aggregation_name 了。后面处理结果的 queries.map 同样以下标来获取即可。

目前效果图如下:

我的改动已经上传到 github 上,欢迎大家一起改进。

目前的问题有两个:图表里的列排序功能不可用,还没找到原因;percents 值还没在 editor.html 里提供自定义办法。


2014.05.26 更新: percents 值已经可以自定义


2014.06.06 更新: 排序功能可用。原因是 elasticsearch 不管你提交的 percents 带不带小数点,返回值里都会保留小数点后一位,而在 sortBy 里头,这个小数点就会被理解成 javascript 里获取数据结构键值的意思。所以收到响应后,用 parseInt 函数干掉小数点就可以了。

03/07
2014

如何搜索 Elasticsearch 中存储的动态请求 URL


ES 中的正则匹配搜索语法

当我们用 logstash 处理 WEB 服务器访问日志的时候,肯定就涉及到一个后期查询的问题。

可能一般我们在 Kibana 上更多的是针对响应时间做数值统计,针对来源IP、域名或者客户端情况做分组统计。但是如果碰到这么个问题的时候呢——过滤所有动态请求的响应时间

这时候你可能会发现一个问题:我们肯定都是用 URL 里带有问号? 来作为过滤条件。但是实际是 Kibana 里一条数据都过滤不出来。

于是我开测试库模拟了一下:

# 插入两条数据
curl http://localhost:9200/test/log/1 -d '{"url":"http://locahost/index.html"}'
curl http://localhost:9200/test/log/2 -d '{"url":"http://locahost/index.php?key=value"}'
# 搜索显示全部数据
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":".*"}}}}'
# 搜索返回请求格式解析失败
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":"\?.*"}}}}'
# 搜索返回空数据
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":".*\\?.*"}}}}'

后来发现问题出在分词上面。

# 删除之前的测试数据和索引
curl -XDELETE http://localhost:9200/test/log
# 预定义索引类型的映射,url字段在索引的时候不分词
curl http://localhost:9200/test/log/_mapping -d '{"log":{"properties":{"url":{"index":"not_analyzed","type":"string"}}}}'
# 还是插入两条数据
curl http://localhost:9200/test/log/1 -d '{"url":"http://locahost/index.html"}'
curl http://localhost:9200/test/log/2 -d '{"url":"http://locahost/index.php?key=value"}'
# 同样的搜索请求,返回了一条结果(index.php?这条)
curl http://localhost:9200/test/log/_search?pretty=1 -d '{"query":{"regexp":{"url":{"value":".*\\?.*"}}}}'

上面这个搜索还可以简写成 Query DSL 的样式:

curl 'http://localhost:9200/test/log/_search?q=url:/.*\\?.*/&pretty=1'

而在 Logstash 比较新的 1.3.3 版本之后,有自带的 template 定义,会对每个 fields 采用 multi-fields 特性,也就是除了默认分词的 URL 字段以外,还有一个 URL.raw 字段都是不分词的。所以只要过滤这个字段就可以了。

(注意,ES1.0版的multi-fields的template写法完全不一样了,所以要用这个特性的童鞋还是谨慎测试logstash和es的版本配套)

Medcl 大神提示我:不指定 mapping 的情况下,ES 默认采用 unigram 分词。也就是切成尽可能小的单词。

02/19
2014

用 logstash 统计 Nginx 的 http_accounting 模块输出


演示 logstash 中 syslog、kv 两个插件,if 条件判断的用法

http_accounting 是 Nginx 的一个第三方模块,会每隔5分钟自动统计 Nginx 所服务的流量,然后发送给 syslog。

流量以 accounting_id 为标签来统计,这个标签可以设置在 server {} 级别,也可以设置在 location /urlpath {} 级别,非常灵活。 统计的内容包括响应字节数,各种状态码的响应个数。

公司原先是有一套基于 rrd 的系统,来收集处理这些 syslog 数据并作出预警判断、异常报警。不过今天不讨论这个,而是试图用最简单的方式,快速的搞定类似的中心平台。

这里当然是 logstash 的最佳用武之地。

logstash.conf 示例如下:

    input {
        syslog {
            port => 29124
        }
    }
    filter {
        grok {
            match => [ "message", "^%{SYSLOGTIMESTAMP:timestamp}\|\| pid:\d+\|from:\d{10}\|to:\d{10}\|accounting_id:%{WORD:accounting}\|requests:%{NUMBER:req:int}\|bytes_out:%{NUMBER:size:int}\|(?:200:%{NUMBER:count.200:int}\|?)?(?:206:%{NUMBER:count.206:int}\|?)?(?:301:%{NUMBER:count.301:int}\|?)?(?:302:%{NUMBER:count.302:int}\|?)?(?:304:%{NUMBER:count.304:int}\|?)?(?:400:%{NUMBER:count.400:int}\|?)?(?:401:%{NUMBER:count.401:int}\|?)?(?:403:%{NUMBER:count.403:int}\|?)?(?:404:%{NUMBER:count.404:int}\|?)?(?:499:%{NUMBER:count.499:int}\|?)?(?:500:%{NUMBER:count.500:int}\|?)?(?:502:%{NUMBER:count.502:int}\|?)?(?:503:%{NUMBER:count.503:int}\|?)?"
        }
        date {
            match => [ "timestamp", "MMM dd HH:mm:ss", "MMM  d HH:mm:ss" ]
        }
    }
    output {
        elasticsearch {
            embedded => true
        }
    }

然后运行 java -jar logstash-1.3.3-flatjar.jar agent -f logstash.conf 即可完成收集入库! 再运行 java -jar logstash-1.3.3-flatjar.jar web 即可在9292端口访问到 Kibana 界面。

然后我们开始配置界面成自己需要的样子:

  1. Top-N 的流量图

点击 Query 搜索栏左边的有色圆点,弹出搜索栏配置框,默认是 lucene 搜索方式,改成 topN 搜索方式。然后填入分析字段为 accounting。

点击 Event Over Time 柱状图右上角第二个的 Configure 小图标,弹出图表配置框:

  • Panel 选项卡中修改 Chart valuecounttotalValue Field 设置为 size,勾选 Seconds 项,转换 size 的累加值成每秒带宽(不然 interval 变化会导致累加值变化)
  • Style 选项卡中修改 Chart OptionsBars 勾选项为 LinesY Format 为 bytes;
  • Queries 选项卡中修改 Charted Queriesselected,然后点中右侧列出的请求中所需要的那项(当前只有一个,就是*)。

保存退出配置框,即可看到该图表开始自动更新。

  1. 50x 错误的技术图

点击 Query 搜索栏右边的 + 号,添加新的 Query 搜索栏,然后在新搜索栏里输入需要搜索的内容,比如 count.500

鼠标移动到流量图最左侧,会移出 Panel 快捷选项,点击最底下的 + 号选项添加新的 Panel:

  • 选择 Panel 类型为 histogram
  • 选择 Queries 类型为 selected,然后点中右侧列出的请求中所需要的那项(现在出现两个了,选中我们刚添加的 count.500)。

保存退出,即可看到新多出来一行,左侧三分之一(默认是span4,添加的时候可以选择)的位置有了一个柱状图。

重复这个步骤,添加 502/503 的柱状图。

  1. 仪表盘设置存档

页面右上角选择 Save 小图标保存即可。之后再上界面后,就可以点击右上角的 Load 小图标自动加载。

上面这个 grok 写的很难看,不过似乎也没有更好的办法~下一步会研究在这个基础上合并 skyline 预警。


2014 年 5 月 10 日更新:

logstash/docs 上发现一个 filter 叫 kv,很适合这个场景,可以大大简化 grok 工作,新的 filter 配置如下:

    filter {
        grok {
            match => [ "message", "^%{SYSLOGTIMESTAMP:timestamp}\|\| pid:\d+\|from:\d{10}\|to:\d{10}\|accounting_id:%{WORD:accounting}\|requests:%{NUMBER:req:int}\|bytes_out:%{NUMBER:size:int}\|%{DATA:status}"
        }
        kv {
            target => "code"
            source => "status"
            field_split => "|"
            value_split => ":"
        }
        ruby {
            code => "n={};event['code'].each_pair{|x,y|n[x]=y.to_i};event['code']=n"
        }
    }

不晓得为什么 filter/mutate 不提供转换 Hash 的功能,所以只能把这行写在 filter/ruby 里面。kv 截出来的 value 默认都是字符串类型。


2014 年 5 月 28 日更新:

发现默认的 LVS 检查导致的 400 会记录到默认的 accounting 组("default")里,虽然不占带宽,却占不少请求数。这类日志可以在 logstash层面就干掉:

    filter {
        grok {
            match => [ "message", "^%{SYSLOGTIMESTAMP:timestamp}\|\| pid:\d+\|from:\d{10}\|to:\d{10}\|accounting_id:%{WORD:accounting}\|requests:%{NUMBER:req:int}\|bytes_out:%{NUMBER:size:int}\|%{DATA:status}"
        }
        if [accounting] == 'default' {
            drop { }
        } else {
            kv {
                target => "code"
                source => "status"
                field_split => "|"
                value_split => ":"
            }
            ruby {
                code => "n={};event['code'].each_pair{|x,y|n[x]=y.to_i};event['code']=n"
            }
        }
    }

另外说明一下,ngx_http_accounting_module 中设定 http_accounting_id 这步是预先处理的,所以只能写固定字符串,不能用 $host 之类的 nginx.conf 变量。

02/08
2014

Kibana 发生什么事了?


本文是 Elasticsearch 官方博客 2014 年 1 月 27 日《what’s cooking in kibana》的翻译,原文地址见:http://www.elasticsearch.org/blog/whats-cooking-kibana/

Elasticsearch 1.0 即将发布, Kibana 团队也准备发布自己的新版。除了一些常见的 bug 修复和小调整,下一个版本中还有一些超棒的特性:

面板组

面板现在可以组织成组的形式,组内可以容纳你乐意加入的任意多的面板。每行的删减都很干净,隐藏面板也不会消耗任何资源。

图表标记

变更部署,用户登录以及其他危险性事件导致的流量、内存消耗或者平均负载的变动,图表标记让你可以输入自定义的查询来将这些重要事件标记到时间轴图表上。

即时过滤器

创建你自己的请求过滤器然后保存下来以备后用。过滤器将和仪表盘一起保存,而且可以在对比你定义的数据子集的时候菜单式展开或收缩。

top-n 查询

单击某个查询旁边的带色的点,就可以设置这个查询的颜色。新版的top-N 查询会找出一个字段 最流行的结果,然后用他们来完成新的查询。

stats 面板

Stats 面板最后都将把搜索归总成一个单独的有意义的数值。

terms_stats 模式

按国家统计流量?每个用户的收入?每页的内存使用?terms面板的terms_stat模式正是你想要的。

01/15
2014

Kibana3 里程碑 4


本文来自Elasticsearch官方博客,2013年11月5日的文章Kibana 3: mileston 4,作为kibana3 Milestone 4重要的使用说明

Kibana 3: Milestone 4 已经发布,带来了一系列性能、易用性和可视化上的提升。让我们来看看这些重大改变。如果你还在Milestone 3上,先看看之前这篇博客里的新特性介绍。

一个全新的界面

Kibana 面板改造成了一个标签更突出,按键和链接更易用,风格全新的样子。改造结果提高了可用度,因为有了更高效的空间利用设计,来支持更大的数据密度和更一致的UI。

Kibana的新界面

一致性查询和过滤布局

为了改善UI,查询和过滤面板现在有自己的可折叠、下拉的区域,具体位置在导航栏的下方。以后不再需要你自己摆放这些基本面板的布局了,它们默认会包含在每一个仪表盘里。和很多Kibana的特性一样,你也可以在仪表盘配置对话框里禁用这个一致性布局。

100%全新的时间范围选择器

如果你熟悉Kibana这两年来的历史,你可能知道曾经存在过好几个时间选择器方案。新的时间选择器经过了完全的重写,不仅占用空间比原来的小,也更容易使用。把这个重要组件移出主仪表盘后,Kibana 现在有更多空间专注于重要数据和图表。还有,新的过滤格式实现了Elasticsearch的时间运算,所以不用每次重新选择一个时间范围来移动你的时间窗口了,每个搜索都能自动更新这个窗口。

全新的时间选择器

可过滤的字段列表

利用表格的"即输即过滤"特性,可以简单而快速的找到字段。

可过滤的字段列表

即时(ad-hoc) facets

然后,当你找到了这些字段,就可以利用即时 facets 快速分析他们。只需要点击一个字段然后选择可视化即可查看到前10个匹配该字段的term。

研究起来也更加简单了

不需要添加面板,饼图可以直接悬浮出现!

动态的仪表盘和url参数

Kibana 3: Milestone 4现在可以通过URL参数获取输入!这个备受期待的特性体现为两个方式:模板化的仪表盘和脚本化的仪表盘。Kibana 3: Milestone 4附带两个可以和Logstash完美配合的示例,在此基础上你可以构建自己的仪表盘。模板化仪表盘的创建非常简单,导出当前仪表盘结构成文件,编辑文件然后保存添加进你的 app/dashboards 目录既可以了。比如,从 logstash.json 里摘录下面一段:

  "0": {
    "query": "",
    "alias": "",
    "color": "#7EB26D",
    "id": 0,
    "pin": false
  }

模板化仪表盘用"handlebar 语法"添加动态区段到基于JSON的仪表盘结构里。比如这里我们就用一个表达式替换掉了查询键的内容:使用URL里的请求参数,如果不存在,使用'*'。 现在我们可以用下面这条URL访问这个仪表盘了:

http://kibana.example.com/index.html#/dashboard/file/logstash.json?query=extension:zip

更灵活的脚本化仪表盘

脚本化仪表盘在处理URL参数的时候更加强大,它能运用上Javascript的全部威力构建一个完整的仪表盘对象。同样用 app/dashboards 里的 logstash.js 举例。因为脚本化仪表盘完全就是javascript,我们可以执行复杂的操作,比如切割URL参数。如下URL中,我们搜索最近2天内的HTML, CSS 或者 PHP,然后在表格里显示 request, responseuser agent注意URL本身路径从 file变成了script

http://localhost:8000/index.html#/dashboard/script/logstash.js?query=html,css,php&from=2d&fields=request,response,agent

立刻下载

Milestone 4对作者和使用者都是一个飞跃。它功能更强大,当然使用也更简单。Kibana 继续集成在 Logstash 里,最新发布的 Logstash 1.2.2 中就带有。Kibana现在也可以直接用elasticsearch.org官网下载,地址见:http://www.elasticsearch.org/overview/kibana/installation/

01/14
2014

2013 年 9 月的 kibana 周报


本文来自Elasticsearch官方博客,2013年9月19日的文章this week in kibana,作为kibana3 Milestone 3重要的使用说明

直方图零填充

直方图面板经过了一番改造,实现了正确的零填充。也就是说,当一个间隔内查询收到0个结果的时候,就显示为0,而不是绘制一条斜线连接到下一个点。零填充也意味着堆叠式直方图从顶端到底部的次序将保持不变。

此外,堆叠提示栏现在允许你在累积和个人模式之间自由选择。

数组字段的微分析

数组字段现在可以在微分析面板上单独或者分组处理。比如,如果我有一个tags数组,我即可以看到前10个最常见的tags,也可以看到前10个最常见的tags组合。

_source 作为默认的表字段

如果你没有给你的表选择任何字段,Kibana现在默认会给你显示 _source 里的 json 数据,直到你选择了具体的字段。

可配置的字段截取

注意到下面截图中 _source 字段末尾的"..."了吗?表格字段能被一个可以配置的"因子"截断。所谓因子就是,表格的列数除以它,得到一个字段的最大长度,然后各字段会被很好的截断成刚好符合这个长度。比如,如果我的截断因子是300,而表格有3列,那么每个字段会被截断成最大100个字符,然后后面跟上'...'。当然,字段的完整内容还是可以在细节扩展视图里看到的。

关于细节视图

你可能已经知道单击表格某行后可以看到包含这个事件的字段的表格。现在你可以选择你希望如何观察这个事件的细节了,包括有语法高亮的JSON以及原始的未高亮的JSON。

更轻,更快,更小,更好

Kibana有了一个全新的构建系统!新的系统允许我们构建一个优化的,小巧的,漂亮的新Kibana。当你升级的时候它还可以自动清除原来的缓存,定期构建的Kibana发布在 http://download.elasticsearch.org/kibana/kibana/kibana-latest.zip ,zip包可以直接解压到你的web服务器里。

如果愿意,你也可以从 Github repository 开始运行。不用复制整个项目,只需要上传 src/ 目录到服务器就可以了。不过我们强烈建议使用构建好的版本,因为这样性能好很多。

01/14
2014

kibana发生什么变化了?


本文来自Elasticsearch官方博客,2013年8月21日的文章kibana: what’s cooking,作为kibana3重要的使用说明

还没有升级Kibana么?那你可错过了一个好技术!Kibana 发生了翻天覆地的变化,新面板只是这个故事中的一部分。整个系统都被重构,给表盘提供统一的颜色和图例方案选择。接口也经过了标准化,很多函数都修改成提供更简单,快速和功能更强大的方式。让我们进一步看看现在的样子。

Terms 面板;全局色彩;别名和查询;过滤器。

新的查询输入

新的查询面板替代了原来的“字符串查询”面板作为你输入查询的方式。每个面板都有自己独立的请求输入。你也还可以为特殊的面板定制请求,不过你要先在这里输入他们,包括可以有别名和颜色设置,然后再在面板编辑器里选取。在没有被激活修改的时候, 查询也可以被固定在一个可折叠的区域。

分配查询到具体面板

分配查询到具体面板非常非常简单。面板编辑器里就可以直接打开或关闭查询,哪怕这个查询已经更新或者过滤掉,它的别名是保持全局一致性的。你还会注意到配置窗口被分割成了选项卡形式,已提供更清晰的配置界面。

自定义颜色和别名

当你给一个查询分配某个颜色的时候,它会立刻反映到所有的面板上。通常用于做图例值的别名也一样。这样,我们可以很简单的通过在一个逻辑组里分配颜色变化,调节整个仪表盘和数据的意义。

你好,terms!

引入了一个新的terms面板,可以使用3种不同的格式展示顶层字段数据:饼图、柱状图和表格。而且都可以点击进入新的过滤器面板。

过滤器面板?

刚刚提到过滤器面板,对吧?没错,过滤器!过滤器允许你深入分解数据集而不用你去修改查询本身。然后,过滤器也可以被删除、隐藏和编辑。过滤器有三种模式:

  • must: 记录必须匹配这个过滤器;
  • mustNot: 记录必须不能匹配这个过滤器;
  • either: 记录必须匹配这些过滤器中的一个。

字段列表和微面板

字段面板集成在表格面板里。字段列表现在会通过访问Elasticsearch的/_mappingAPI来自动填充。注意你可能需要更新自己的代理服务器配置来适应这个变更。为了节约空间,这个字段列表现在也是可折叠的,而新的图形也添加到了微面板。

嗨,那配色方案呢?!

对,你在我解释之前已经发现这个变化了!Kibana现在允许你在黑白两个配色方案之间切换以刚好的匹配你自己的环境和偏好。

汇报完毕!当然kibana一直在更新,注意继续关注这里,给我们的github项目加星,然后上推特fo @rashidkpc@elasticsearch

10/09
2013

用 elasticsearch 和 logstash 为数十亿次客户搜索提供服务


原文地址:http://www.elasticsearch.org/blog/using-elasticsearch-and-logstash-to-serve-billions-of-searchable-events-for-customers/

今天非常高兴的欢迎我们的第一个外来博主,Rackspace软件开发工程师,目前为Mailgun工作的 Ralph Meijer。我们在 Monitorama EU 会面后,Ralph 提出可以给我们写一篇 Mailgun 里如何使用 Elasticsearch 的文章。他本人也早就活跃在 Elasticsearch 社区,经常参加我们在荷兰的聚会了。

Mailgun 收发大量电子邮件,我们跟踪和存储每封邮件发生的每个事件。每个月会新增数十亿事件,我们都要展示给我们的客户,方便他们很容易的分析数据,也就是全文搜索。下文是我们利用Elasticsearch和Logstash技术完成这个需求的技术细节(很高兴刚写完这篇文章就听说《Logstash加入Elasticsearch》了)。

事件

在 Mailgun 里,event可能是如下几种:进来一条信息,可能被接收可能被拒绝;出去一条信息,可能被投递可能被拒绝(垃圾信息或者反弹);信息是直接打开还是通过链接点击打开;收件人要求退订。所有这些事件,都有一些元信息可以帮助我们客户找出他们的信息什么时候,为什么,发生了什么。这个元信息包括:信息的发送者,收件人地址,信息id,SMTP错误码,链接URL,geo地理位置等等。

每个事件都是由一个时间戳和一系列字段构成的。一个典型的事件就是一个关联数组,或者叫字典、哈希表。

事件访问设计

假设我们已经有了各种事件,现在需要一个办法来给客户使用。在Mailgun的控制面板里,有一个日志标签,可以以时间倒序展示事件日志,并且还可以通过域名和级别来过滤日志,示例如下:

在这个示例里,这个事件的级别是"warn",因为SMTP错误码说明这是一个临时性问题,我们稍后会重试投递。这里有两个字段,一个时间戳,一个还没格式化的非结构化文本信息。为了醒目,这里我们会根据级别的不同给事件上不同的底色。

在这个网页之后,我还有一个接收日志的API,一个设置触发报警的hook页面。后面的报警完全是结构化了的带有很多元数据字段的JSON文档。比如,SMTP错误码有自己的字段,收件人地址和邮件标题等也都有。

不幸的是,原有的日志API非常有限。他只能返回邮件投递时间和控制面板里展示的非结构化的文本内容。没办法获取或者搜索多个字段(像报警页面里那样),更不要说全文搜索了。简单说,就是控制面板缺乏全文搜索。

用elasticsearch存储和响应请求

要给控制面板提供API和访问,我们需要一个新的后端来弥补前面提到的短板,包括下面几个新需求:

  • 允许大多数属性的过滤。
  • 允许全文搜索。
  • 支持存储至少30天数据,可以有限度的轮滚。
  • 添加节点即可轻松扩展。
  • 节点失效无影响。

而Elasticsearch,是一个可以“准”实时入库、实时请求的搜索引擎。它基于Apache Lucene,由存储索引的节点组成一个分布式高可用的集群。单个节点离线,集群会自动把索引(的分片)均衡到剩余节点上。你可以配置具体每个索引有多少分片,以及这些分片该有多少副本。如果一个主分片离线,就从副本中选一个出来提升为主分片。

Elasticsearch 是面向文档的,某种层度上可以说也是无模式的。这意味着你可以传递任意JSON文档然后就可以索引成字段。对我们的事件来说完全符合要求。

Elasticsearch 同样还有一个非常强大的请求/过滤接口,可以对特定字段搜索,也可以做全文搜索。

事件存入elasticsearch

有很多工具或者服务可以用来记录事件。我们最终选择了 Logstash,一个搜集、分析、管理和传输日志的工具。

在内部,通过webhooks推送来的event同时在我们系统的其他部分也有使用,目前我们是用Redis来完成这个功能。Logstash有一个Redis输入插件来从Redis列表里接收日志事件。通过几个小过滤器后,事件通过一个输出插件输出。最常用的输出插件就是 Elasticsearch 插件。

利用 Elasticsearch 丰富的 API 最好的办法就是使用 Kibana,这个工具的口号是“让海量日志有意义”。目前最新的 Kibana 3 是一个纯粹的 JavaScript 客户端版,随后也会成为 Logstash 的默认界面。和之前的版本不同的是,它不在依赖于一个类Logstash模式,而是可以用于任意Elasticsearch索引。

认证

到这步,我们已经解决了事件集中的问题,也有了丰富的API来深入解析日志。但是我们不想把所有日志都公开给每个人,所以我们需要一个认证,目前Elasticsearch 和 Kibana 都没提供认证功能,所以寄希望于 Elasticsearch API 是不可能的了。

我们选择了构建双层代理。一层代理用来做认证和流量限速,一层用来转义我们的事件 API 成 Elasticsearch 请求。前面这层代理我们已经以 Apache 2.0 开原协议发布在Github上,叫 vulcan 。我们还把我们原来的那套日志 API 也转移到了 Elasticsearch 系统上。

索引设计

有很多种方法来确定你如何组织自己的索引,基于文档的数目(每个时间段内),以及查询模式。

Logstash 默认每天创建一个新索引,包括当天收到的全部时间。你可以通过配置修改这个时间,或者采用其他属性来区分索引,比如每个用户一个,或者用事件类型等等。

我们这里每秒有1500个时间,而且我们希望每个账户的轮转时间段都是可配置的。可选项有:

  • 一个大索引。
  • 每天一个索引。
  • 每个用户账户一个索引。

当然,如果需要的话,这些都可以在未来进一步切分,比如根据事件类型。

管理轮滚的一个办法是在 Elasticsearch 中给每个文档设定 TTLs 。到了时间 Elasticsearch 就会批量删除过期文档。这种做法使得定制每个账户的轮转时间变得很简单,但是也带来了更多的 IO 操作。

另一个轻量级的办法是直接删除整个索引。这也是 Logstash 默认以天创建索引的原因。过了这天你直接通过 crontab 任务删除索引即可。

不过后面这个办法就没法定制轮转了。我们有很多用户账户,给每个用户每天保持一个索引是不切实际的。当然,给所有用户每天存一个索引又意味着我们要把所有数据都存磁盘上。如果一个账户是保持两天数据的轮转,那么在缓存中的数据就是有限的。在查询多天的垃圾邮件时,处理性能也就受限了。所以,我们需要保留更多的日志以供Kibana访问。

映射

为了定义文档(中的字段)如何压缩、索引和存储在索引里,Elasticsearch 有一个叫做 mapping 的概念。所以为每个字段它都定义了类型,定义了如何分析和标记字段的值以便索引和查询,定义了值是否需要存储,以及其他各种设置。默认的情况,mapping是动态的,也就是说 Elasticsearch 会从它获得的第一个值来尝试猜测字段的类型,然后正式应用这个设置到索引。

如果你的数据来源单一,这样就很好了。但实际可能来源很复杂,或者日志类型根本就不一样,比如我们这,同一个名字的字段的数据类型可能都不一样。 Elasticsearch 会拒绝索引一个类型不匹配的文档,所以我们需要自定义 mapping 。

通过我们的 Events API ,我给日志事件的类型定义了一个映射。不是所有的事件都有所有这些字段,不过相同名字的字段肯定是一致的。

分析器

默认情况下,字段的 mapping 中就带有 标准分析器。简单的说,就是字符串会被转成小写,然后分割成一个一个单词。然后这些标记化的单词再写入银锁,并指向具体的字段。

有些情况,你可能想要些别的东西来完成不同的效果。比如说账户 ID,电子邮件地址或者网页链接 URL之类的,默认标记器会以斜线分割,而不考虑把整个域名作为一个单独的标记。当你通过 facet 统计域名字段的时候,你得到的会是域名中一段一段标签的细分结果。

要解决这个问题,可以设置索引属性,给对应字段设置成 not_analyzed。这样在插入索引的时候,这个字段不再经过映射或者标记器。比如对 domain.name 字段应用这个设置后,每个域名都会完整的作为同一个标签统计 facet 了。

如果你还想在这个字段内通过部分内容查找,你可以使用 multi-field type。这个类型可以映射相同的值到不同的核心类型或者属性,然后在不同名称下使用。我们对 IP 地址就使用了这个技术。默认的字段(比如叫sending-ip)的类型就是 ip,而另一个非默认字段(比如叫 sending-ip.untouched)则配置成 not_analyzed 而且类型为字符串。这样,默认字段可以做 IP 地址专有的范围查询,而 .untouched 字段则可以做 facet 查询。

除此以外,绝大多数字段我们都没用分析器和标记器。不过我们正在考虑未来可以结合上面的多字段类型技巧,应用 pattern capture tokenfilter 到某些字段(比如电子邮件地址)上。

监控

要知道你的集群怎么样,你就必须要监控它。 Elasticsearch 有非常棒的 API 来获取 cluster statenode statistics。我们可以用 Graphite 来存储这些指标并且做出综合表盘,下面就是其中一个面板:

为了收集这些数据并且传输到 Graphite,我创建了 Vör,已经在 Mochi Media 下用 MIT/X11 协议开源了。另外一个保证 Redis 列表大小的收集器也在开发中。

除此以外,我们还统计很多东西,比如邮件的收发、点击数,API调用和耗时等等,这些是通过 StatsD 收集的,同样也添加到我们的 Graphite 表盘。

这绝对是好办法来观察发生了什么。Graphite 有一系列函数可以用来在绘图前作处理,也可以直接返回JSON文档。比如,我们可以很容易的创建一个图片展示 API 请求的数量与服务器负载或者索引速度的联系。

当前状况

我们的一些数据:

  • 每天大概4kw 到 6kw 个日志事件。
  • 30天轮转一次日志。
  • 30个索引。
  • 每个索引5个分片。
  • 每个分片一个1副本。
  • 每个索引占 2 * 50 到 80 GB空间(因为有副本所以乘2)。

为此,我们启动了一共 9 台 Rackspace 云主机,具体配置是这样的:

  • 6x 30GB RAM, 8 vCPUs, 1TB disk: Elasticsearch 数据节点。
  • 2x 8GB RAM, 4 vCPUs: Elasticsearch 代理节点, Logstash, Graphite 和 StatsD。
  • 2x 4GB RAM, 2 core: Elasticsearch 代理节点, Vulcan 和 API 服务器

大多数主机最终会迁移到专属的平台上,同时保留有扩展新云主机的能力。

Elasticsearch 数据节点都配置了 16GB 内存给 JVM heap。其余都是标准配置。此外还设置了 fieldcache 最大大小为 heap 的 40%,以保证集群不会在 facet 和 sort 内容很多的字段时挂掉。我们同时也增加了一点 cluster wide settings 来加速数据恢复和重均衡。另外,相对于我们存储的文档数量来说,indices.recovery.max_bytes_per_sec 的默认设置实在太低了。

总结

我们非常高兴用 Elasticsearch 来保存我们的事件,也得到了试用新 API 和新控制面板中新日志页面的客户们非常积极的反馈。任意字段的可搜索对日志挖掘绝对是一种显著的改善,而 Elasticsearch 正提供了这种高效无痛的改进。当然,Logstash,Elasticsearch 和 Kibana 这整条工具链也非常适合内部应用日志处理。

如果你想了解更多详情或者对我们的 API 有什么疑问,尽管留言。也可以在 Mailgun 博客上阅读更多关于事件 API 的细节

开心处理日志,开心发送邮件!

07/11
2013

根据事件统计值报警


利用了 logstash 的 metrics 和 ruby 两个 filters 插件,这也是玩转 logstash 非常重要的两个插件

之前已经用很多博文说过了 logstash 如何配合 elasticsearch 以及 kibana 来做日子分析和实时搜索。其实 logstash 上百个插件还有很多其他的玩法,绝不是局限在日志搜索统计方面的。今天就展示另一个做法。根据日志中的异常值出现频率报警。

在 logstash 的官网上,针对这个问题采用的办法是讲异常值计数 output 到 statsd 中,然后可以用通过观测 graphite 图形变化来判断异常。(或者配合 nagios 的 check_graphite 插件?) 官网说明见:http://logstash.net/docs/1.1.13/tutorials/metrics-from-logs

如果不想一直盯着页面看的话,可以利用另外几个插件来实现类似的做法,比如我要监控访问日志,如果其中 504 状态码每分钟超过 100 次,就报警出来。logstash 配置如下:

2014 年 08 月 20 日注:上面说法有误,rate_1m 的含义是:最近 1 分钟内的每秒速率!

    input {
        stdin {
            type => "apache"
        }
    }
    filter {
        grok {
            pattern => "\[%{HTTPDATE:ts}\] %{NUMBER:status} %{IPORHOST:remotehost} %{URIHOST} %{WORD} %{URIPATHPARAM:url} HTTP/%{NUMBER} %{URIHOST:oh} %{NUMBER:responsetime:float} %{NUMBER:upstreamtime:float} (?:%{NUMBER:bytes:float}|-)"
            type => "apache"
        }
        metrics {
            type => "apache"
            meter => "error.%{status}"
            add_tag => "metric"
            ignore_older_than => 10
        }
        ruby {
            tags => "metric"
#            code => "event.cancel if event['@fields']['error.504.rate_1m'] < 100"
#           2014/08/20: 每秒速率,所以要乘以60s。另,新版本没有了@fields,都存在顶级field里。
            code => "event.cancel if event['error.504.rate_1m']*60 < 100"
        }
    }
    output {
        exec {
            tags => "metric"
            command => "sendsms.pl -m '%{error\.504\.rate_1m}'"
        }
    }

其中关键在两个 filter。 metrics 插件可以每5秒(前天刚更新了源码,这个值可以自己指定了)更新一次统计值,支持 metertimer 两种,timer 除了 countrate_1|5|15m 外,还可以统计 min|max|stddev|meanp1|5|10|90|95|99 等详细数据。

ruby 插件则是直接 eval 写在 code 配置里的代码。

需要注意的是: output 里使用的时候,需要用 \ 转义 .。否则配置解析后会认为变量不存在。这是目前官网文档上写的有问题的地方。我已經跟作者提过,或许过些天会修改。

值得一提的是:metrics 插件的输出是一个全新的 event,而不会去改变原先 grok 生成的 event。

12/22
2012

用 Amcharts 和 ElasticSearch 做日志分析


自己实现后来 kibana v3 版中的 map panel 和 term panel 功能

之前有一篇从 ElasticSearch 官网摘下来的博客《【翻译】用ElasticSearch和Protovis实现数据可视化》。不过一来 Protovis 已经过时,二来 不管是 Protovis 的进化品 D3 还是 Highchart 什么的,我觉得在多图方面都还不如 amcharts 好用。所以在最后依然选择了老牌的 amcharts 完成。

展示品的大概背景还是 webserver 日志,嗯,这个需求应该是最有代表性的了。我们需要对webserver的性能有所了解。之前有一篇文章《Tatsumaki框架的小demo一个》,讲的是通过 terms_stats 获取固定时段内请求时间的平均值。其实这个demo是可以参照官网博客修改成纯js应用的。因为 Tatsumaki 在这里除了处理 HTTP 请求参数,什么都没干。而且这个demo目的是展示 perl 框架的处理,所以amchart方面直接就写死了各种变量。

但是还有一种需求,比如你需要的是针对某个情况超过某个百分比的分时走势统计。这时候必须多次请求 ES 来做运算,再让 js 做,不是说不行,但是多一倍数据在网络中传输,就不如在服务器端封装 API 了 —— 其实是我 js 太烂这种事情,我会告诉你们么。。。

先上两张效果图,其实这个布局我是从 facetgrapher 项目偷来的,但这个项目只适合比较不同 index 之间同时间段的数据,我建议作者修改,作者说"我自己js也是半吊子水平"。。。

分地区错误情况统计

实时分运营商错误比例统计

2013 年 2 月 21 日更新:利用 bullet 大小来表示 hasErr 的程度

查询的 ES 库情况如下:

    $ curl "http://10.4.16.68:9200/demo-photo/log/_mapping?pretty=1"
    {
      "log" : {
        "properties" : {
          "brower" : {
            "type" : "string"
          },
          "date" : {
            "type" : "date",
            "format" : "dateOptionalTime"
          },
          "fromArea" : {
            "type" : "string",
            "index" : "not_analyzed"
          },
          "hasErr" : {
            "type" : "string"
          },
          "requestUrl" : {
            "type" : "string",
            "index" : "not_analyzed"
          },
          "timeCost" : {
            "type" : "long"
          },
          "userId" : {
            "type" : "string"
          },
          "xnforword" : {
            "type" : "string"
          }
        }
      }
    }
    $ curl "http://10.4.16.68:9200/demo-photo/log/_search?pretty=1&size=1" -d '{"query":{"match_all":{}}}'
    {
      "took" : 14,
      "timed_out" : false,
      "_shards" : {
        "total" : 10,
        "successful" : 10,
        "failed" : 0
      },
      "hits" : {
        "total" : 2330679,
        "max_score" : 1.0,
        "hits" : [ {
          "_index" : "demo-photo",
          "_type" : "log",
          "_id" : "iSI5xic7Qg2p9Sqk5yp-pQ",
          "_score" : 1.0, "_source" : {"hasErr":"false","date":"2012-12-06T15:04:21,983","userId":"123456789","requestUrl":"http://photo.demo.domain.com/path/to/your/app/test.jpg","brower":"chrome17.0.963.84","timeCost":750,"xnforword":["192.168.1.123","10.10.10.10"],"fromArea":"CN-UNI-OTHER"}
        } ]
      }
    }

然后后台是我惯用的 Dancer 框架:

    package AnalysisDemo;
    use Dancer ':syntax';
    use Dancer::Plugin::Ajax;
    use ElasticSearch;
    use POSIX qw(strftime);
    no  warnings;
    
    my $elsearch         = ElasticSearch->new( { %{ config->{plugins}->{ElasticSearch} } } );
    my $index_prefix     = 'demo-';
    my $type             = 'log';
    # 这里是对ip库的归类。数据是需要提前导入ES的,这可以是logstash发挥作用
    my $default_provider = {
        yidong    => [qw(CN-CRN CN-CMN)],
        jiaoyu    => [qw(CN-CER CN-CST)],
        dianxin   => [qw(CN-CHN)],
        liantong  => [qw(CN-UNI CN-CNC)],
        guangdian => [qw(CN-SCN)],
        haiwai => [qw(OS)],
    };
    
    get '/' => sub {
        # 通过 state API 获取 ES 集群现有的所有index列表
        # 因为是一个域名一个index,这样就有了前段页面上的域名下拉选择框
        my $indices = $elsearch->cluster_state->{routing_table}->{indices};
        template 'demo/chart',
          {
            providers => [ sort keys %$default_provider ],
            datasources =>
              [ grep { /^$index_prefix/ && s/$index_prefix// } keys %$indices ],
            inputfrom => strftime("%F\T%T", localtime(time()-864000)),
            inputto => strftime("%F\T%T", localtime()),
          };
    };
    
    # 这里把 api 拆成服务商和区域两个,没啥特殊原因,因为是分两回写的,汗
    # 其实可以看到最开始的请求参数类似,最后json的field名字都一样
    ajax '/api/provider' => sub {
        my $param = from_json(request->body);
        my $index = $index_prefix . $param->{'datasource'};
        my $from  = $param->{'from'} || 'now-10d';
        my $to    = $param->{'to'} || 'now';
        my $providers = $param->{'provider'};
        my ( $pct, $chartData );
        for my $provider ( sort @{$providers} ) {
            my $provider_pct;
            # 这里是比较麻烦的一点,因为一个区域在ip库里可能标记成多个,比如铁通和移动,现在都是移动
            for my $area ( @{ $default_provider->{$provider} } ) {
                my $res = pct_count( $index, $area, $from, $to );
                for my $time ( sort keys %{$res} ) {
                    $provider_pct->{$time}->{count} += $res->{$time}->{count};
                    $provider_pct->{$time}->{error} += $res->{$time}->{error};
                    $provider_pct->{$time}->{slow}  += $res->{$time}->{slow};
                }
            }
            # 这里因为可能没有错误,所以前面关闭了常用的 warnings 警告
            for my $time ( sort keys %{$provider_pct} ) {
                my $right_pct = 100;
                $right_pct =
                  100 -
                  $provider_pct->{$time}->{slow} / $provider_pct->{$time}->{count}
                  * 100;
                $pct->{$time}->{$provider} = sprintf "%.2f", $right_pct;
                $pct->{$time}->{"${provider}Err"} = sprintf "%.2f",
                  $provider_pct->{$time}->{error} / $provider_pct->{$time}->{count}
                  * 100;
                $pct->{$time}->{"${provider}Size"} = sprintf "%.0f",
                  $pct->{$time}->{"${provider}Err"};
            }
        };
    
        for my $time ( sort keys %$pct ) {
            my $data->{date} = $time;
            for my $provider ( @$providers ) {
                $data->{$provider} = $pct->{$time}->{$provider} || 100;
                $data->{"${provider}Err"} = $pct->{$time}->{"${provider}Err"} || 0;
                # 百分比太低,所以翻 5 倍来作为 bullet 的大小
                $data->{"${provider}Size"} =
                  $pct->{$time}->{"${provider}Size"} * 5 || 0;
            };
            push @$chartData, $data;
        };
    
        my $res = {
            type => "line",
            categoryField => "date",
            graphList => $providers,
            chartData => $chartData,
        };
    
        return to_json($res);
    };
    
    ajax '/api/area' => sub {
        my $param = from_json(request->body);
        my $index = $index_prefix . $param->{'datasource'};
        my $limit = $param->{'limit'} || 50;
        my $from  = $param->{'from'} || 'now-10d';
        my $to    = $param->{'to'} || 'now';
        # 这是后来写的,尽可能把 sub 拆分了,所以 ajax 这里就很简略
        # 当然因为不考虑多运营商的问题,本身也容易一些
        my $res = pct_terms( $index, $limit, $from, $to );
        return to_json($res);
    };
    
    sub pct_terms {
        my ( $index, $limit, $from, $to ) = @_;
        my $area_all_count = area_terms( $index, 0,    $limit, $from, $to );
        my $area_err_count = area_terms( $index, 2000, $limit, $from, $to );
        my ( $error, $chartData );
        for ( @{$area_err_count} ) {
            $error->{ $_->{term} } = $_->{count};
        }
        for ( @{$area_all_count} ) {
            push @$chartData, {
                area  => $_->{term},
                error => $error->{ $_->{term} } || 0,
                right => $_->{count} - $error->{ $_->{term} },
            };
        }
        my $res = {
            type => "column",
            categoryField => "area",
            graphList => [qw(right error)],
            chartData => $chartData,
        };
        return $res;
    }
    
    sub pct_count {
        my ( $index, $area, $from, $to ) = @_;
        my $level = $area eq 'OS' ? 3000 : 2000;
        my $all_count  = histo_count( $index, 0,      $area, $from, $to );
        my $slow_count = histo_count( $index, $level,   $area, $from, $to );
        my $err_count  = histo_count( $index, 'hasErr', $area, $from, $to );
        my $res;
        for ( @{$slow_count} ) {
            $res->{ $_->{time} }->{slow} = $_->{count};
        }
        for ( @{$err_count} ) {
            $res->{ $_->{time} }->{error} = $_->{count};
        }
        for ( @{$all_count} ) {
            $res->{ $_->{time} }->{count} = $_->{count};
        }
        return $res;
    }
    
    # 下面开始的两个才是真正发 ES 请求的地方
    sub area_terms {
        my ( $index, $level, $limit, $from, $to ) = @_;
        my $data = $elsearch->search(
            index  => $index,
            type   => $type,
            size   => 0,
            facets => {
                area => {
                    facet_filter => {
                        and => [
                            {
                                range => {
                                    date => {
                                        from => $from,
                                        to   => $to
                                    },
                                },
                            },
                            {
                                numeric_range =>
                                  { timeCost => { gte => $level, }, },
                            },
                        ],
                    },
                    # 使用最简单的 terms facets API,因为只用计数就好了
                    terms => {
                        field => "fromArea",
                        size  => $limit,
                    }
                }
            }
        );
        return $data->{facets}->{area}->{terms};
    }
    
    sub histo_count {
        my ( $index, $level, $area, $from, $to ) = @_;
        # 根据 level 参数判断使用 hasErr 还是 timeCost 列数据
        my $level_ref =
          $level eq 'hasErr'
          ? { term => { hasErr => 'true' } }
          : { numeric_range => { timeCost => { gt => $level } } };
        my $facets = {
            pct => {
                facet_filter => {
                    # 这里条件比较多,所以要用 bool API,不能用 and 了
                    bool => {
                        # must 可以提供多个条件作为 AND 数组
                        # 此外还有 must_not 作为 AND NOT 数组
                        # should 作为 OR 数组
                        must => [
                            {
                                range => {
                                    date => {
                                        from => $from,
                                        to   => $to
                                    },
                                },
                            },
                            { prefix => { fromArea => $area } },
                            $level_ref,
                        ],
                    },
                },
                # 这里是需要针对专门的时间列做汇总,所以用 date_histogram 了,具体说明之前有博客
                date_histogram => {
                    field    => "date",
                    interval => "1h",
                }
            }
        };
        my $data = $elsearch->search(
            index  => $index,
            type   => $type,
            facets => $facets,
            size   => 0,
        );
        return $data->{facets}->{pct}->{entries};
    }

其实把里面请求的hash拆开来一个个定义,然后根据情况组合,但是不方便察看作为 demo 的整体情况。

然后看template里怎么写。这里虽然有两个效果图,但是只有一个template哟:

<link rel="stylesheet" href="[% $request.uri_base %]/amcharts/style.css" type="text/css">
<script src="[% $request.uri_base %]/amcharts/amcharts.js" type="text/javascript"></script>
<script type="text/javascript">
  var chart;

  function createAmChart(data) {
    // 清空原有图形
    $("#chartdiv").empty();
    // 如果是时间轴线图,需要把date字符转成Date对象
    if ( data.categoryField == "date" ) {
      for ( var j = 0; j < data.chartData.length; j++ ) {
        data.chartData[j].date = new Date(Number(data.chartData[j].date));
      }
    }

    chart = new AmCharts.AmSerialChart();
    // 拖动条等图片的路径
    chart.pathToImages = "/amcharts/images/";
    chart.dataProvider = data.chartData;
    chart.categoryField = data.categoryField;
    // 如果是柱状图,可以显示 3D 效果
    if ( data.type == 'column' ) {
//      chart.rotate = true;
      chart.depth3D = 20;
      chart.angle = 30;
    }
    var categoryAxis = chart.categoryAxis;
    categoryAxis.fillAlpha = 1;
    categoryAxis.fillColor = "#FAFAFA";
    categoryAxis.axisAlpha = 0;
    categoryAxis.gridPosition = "start";
    // 时间轴需要解析Date对象
    if ( data.categoryField == "date" ) {
      categoryAxis.parseDates = true;
      categoryAxis.minPeriod = "hh";
    }

    var valueAxis = new AmCharts.ValueAxis();
    valueAxis.dashLength = 5;
    valueAxis.axisAlpha = 0;
    // 指定柱状图为叠加模式,这里有多种模式可以看文档
    if ( data.type == 'column' ) {
      valueAxis.stackType = "regular";
    }
    chart.addValueAxis(valueAxis);

    // 这里有个有趣的事情,如果不把graph当数组直接循环,效果也没问题
    // 我只能猜测是 addGraph 后数据其实已经缓存到 chart 了
    var graph = [];
    var colors = ['#FF6600', '#FCD202', '#B0DE09', '#0D8ECF', '#2A0CD0', '#CD0D74', '#CC0000', '#00CC00', '#0000CC', '#DDDDDD', '#999999', '#333333', '#990000'];
    for ( var i = 0; i < data.graphList.length; i++ ) {
      graph[i] = new AmCharts.AmGraph();
      graph[i].title = data.graphList[i];
      graph[i].valueField = data.graphList[i];
      graph[i].type = data.type;
      if ( data.type == 'column' ) {
        graph[i].lineAlpha = 0;
        graph[i].fillAlphas = 1;
      } else {
        graph[i].valueField = data.graphList[i];
        graph[i].descriptionField = data.graphList[i] + "Err";
        graph[i].bulletSizeField = data.graphList[i] + "Size";
        graph[i].bullet = "round";
        // 设定为空心圆圈
        graph[i].bulletColor = "#ffffff";
        graph[i].bulletBorderAlpha = 1;
        // amchart 本来有默认颜色,不过前面因为修改了圆内的颜色,所以其他颜色无法继承默认设定了
        graph[i].bulletBorderColor =  colors[i];
        graph[i].lineColor =  colors[i];
        graph[i].lineAlpha = 1;
        graph[i].lineThickness = 1;
        graph[i].balloonText = "[[value]]% / hasErr:[[description]]%";
      }
      chart.addGraph(graph[i]);
    }

    // 加图例,这样可以在图上随时勾选察看具体某个数据,也方便某数据异常的时候影响察看其他
    var legend = new AmCharts.AmLegend();
    legend.position = "right";
    legend.horizontalGap = 10;
    legend.switchType = "v";
    chart.addLegend(legend);

    // 加拖拉轴,这样可以拖动察看细节,这个功能很赞
    var scrollbar = new AmCharts.ChartScrollbar();
    scrollbar.graph = graph[0];
    scrollbar.graphType = "line";
    scrollbar.height = 30;
    chart.addChartScrollbar(scrollbar);

    var cursor = new AmCharts.ChartCursor();
    chart.addChartCursor(cursor);

    chart.write("chartdiv");
  };

  function drawChart() {
    var provider = [];
    $("#provider :selected").each(function(){
       provider.push( $(this).val() );
    });
    var datasource = $("#datasource :selected").val();
    var apitype = $(":radio:checked").val();
    var from = $("#from").val();
    var to = $("#to").val();
    $.ajax({
      processData: false,
      url: "[% $request.uri_base %]/demo/api/" + apitype,
      data: JSON.stringify({"provider":provider, "datasource":datasource, "from":from, "to":to}),
      type: "POST",
      dataType: "json",
      success : createAmChart
    });
  };

  function showselect() {
    $("#providers").show();
  };
  function hideselect() {
    $("#providers").hide();
  };
</script>

      <div class="well">
        <div class="span8">
          <input type="text" class="input-medium" id="from" name="from" value="[% $inputfrom %]">
          <input type="text" class="input-medium" id="to" name="to" value="[% $inputto %]">
          <select class="input-medium" id="datasource">
%% for $datasources -> $datasource {
            <option value="[% $datasource %]">[% $datasource %]</option>
%% }
          </select>
        </div>
        <div class="span2">
          <label class="radio">
            <input type="radio" name="querytype" value="provider" onclick="showselect()">服务商趋势
          </label>
          <label class="radio">
            <input type="radio" name="querytype" value="area" checked onclick="hideselect()">分地区统计
          </label>
        </div>
        
        <button type="submit" class="btn btn-primary" onclick="drawChart()">查询</button>
        
        <div id ="providers" class="controls hide">
          <select class="input-medium" id="provider" multiple="mulitiple">
%% for $providers -> $provider {
            <option value="[% $provider %]" selected>[% $provider %]</option>
%% }
          </select>
        </div>
      </div><!--/well-->
      <div id="chartdiv" style="width: 100%; height: 400px;">
      </div>
12/11
2012

不小心踩进 ElasticSearch.pm 模块的坑里


ElasticSearch.pm 模块在初始化对象时,会直接 delete 传参的键值对。再次提醒,目前推荐使用的是官方的 Search::Elasticsearch 模块

在今天以前,我一直认为perl的ElasticSearch.pm是除了原生java库以外封装最好的。不过今天踩进一个硕大的坑里,多亏 dancer-user 邮件列表里外国友人的帮助,才算爬了出来……

事情是这样的

用 dancer 搭建的一个 webserver 用来提供 api 给前端图表页面。dancer 收到 ajax 请求后组装成 json 发给 ElasticSearch。因为要算百分比,无法在单次请求内完成,不然的话直接从页面上发给 ES 服务器了。

这个 webserver 是之前已经创建过的。而且作用类似,也就是说,之前已经存在一个 DancerApp/lib/DancerApp/First.pm 里使用了 ElasticSearch 模块。相关代码如下:

    use Dancer ':syntax';
    use ElasticSearch;
    my $elsearch = ElasticSearch->new( config->{ElasticSearch} );

然后给新项目创建 DancerApp/lib/DancerApp/Second.pm 同样使用 ElasticSearch 模块,代码原样复制。然后在 DancerApp/lib/DancerApp.pm 里先后加载:

    use Dancer ':syntax';
    use FindBin qw($Bin);
    use lib "$Bin/../lib";
    use DancerApp::First;
    use DancerApp::Second;

启动应用后访问页面。怪事出现了: First 应用正常,Second 应用报错说 ElasticSearch 连接不上

仔细看报错信息,发现Second 里的 $elsearch 连接的不是 config.yml 里设定的 servers,而是模块默认的 127.0.0.1:9200

更换DancerApp/lib/DancerApp.pm 里的加载次序,就变成了 Second 正常,First 失败

试图使用下面的代码检查 config ,发现 config 里其他的设置都没问题,唯独和 ElasticSearch 相关的设定发生了变化:

    use Data::Dumper;
    get '/config' => sub { return Dumper config };

结果中 config->{ElasticSearch} 只剩下 trace_calls: 0 一条设定, serverstransportno_refreshmax_requests 都消失了!

真相只有一个

ElasticSearch 模块在初始化的时候,会把参数传递给 ElasticSearch::Transport 模块做具体的操作(包括之前我很欣赏的自动选择节点服务器)。而就在这里,问题出现了:

参数一直是以引用身份传递的,任何修改都会修改原始数据

    my $servers = delete $params->{servers}
        || '127.0.0.1:' . $transport_class->default_port;

随着 delete 操作,悲剧就此发生了。Dancer 里的全局变量 config->{ElasticSearch} 中的 servers 元素就此消失……

善后事宜

解决办法很容易,在每个模块里初始化 ElasticSearch 实例的适合,传递一个全局 config->{ElasticSearch}副本的引用过去。

    my $elsearch = ElasticSearch->new( { %{ config->{ElasticSearch} } } );

亲爱的 David Precious 童鞋已经把这个问题上报给 ElasticSearch.pm 开发者了。或许之后会由模块内部做副本操作。目前只能自己来了。

issue 地址:https://github.com/clintongormley/ElasticSearch.pm/issues/34

11/22
2012

用 Tatsumaki 框架写 elasticsearch 界面


早期,logstash 自带的 web 和 kibana v1 版本功能都还很简单,只能查看一下 count 走势和最近消息。本文以实际代码,演示 facet 接口的使用方法,并通过页面展示出来

Tatsumaki是Plack作者的一个小框架,亮点是很好的利用了psgi.streaming的接口可以async的完成响应。不过因为缺少周边支持,所以除了几个webchat的example,似乎没看到什么应用。笔者之前纯为练手,却用tatsumaki写了个sync响应的小demo,算是展示一下用tatsuamki做普通web应用的基础步骤吧:

(代码本来是作为一个ElasticSearch数据分析的平台,不过后来发现社区有人开始做纯js的内嵌进ElasticSearch的plugin了,所以撤了repo,这里贴下代码)

  • 所有的psgi/plack应用都一样有自己的app.psgi文件:
our $VERSION = 0.01;
### app.psgi
use Tatsumaki::Error;
use Tatsumaki::Application;
use Tatsumaki::HTTPClient;
use Tatsumaki::Server;
### read config
use File::Basename;
use YAML::Syck;
my $config = LoadFile(dirname(__FILE__) . '/config.yml');
### elasticsearch init
use ElasticSearch;
#这里yml的写法借鉴Dancer::Plugin::ElasticSearch了
my $elsearch = ElasticSearch->new( $config->{'options'} );
### index init
use POSIX qw(strftime);
my $index = join '-', ( (+split( '-', $config->{'index'} ))[0], strftime( (+split( '-', $config->{'index'} ))[1], localtime ) );
my $type = $config->{'type'};
#首页类,调用了模板
package MainHandler;
use parent qw(Tatsumaki::Handler);
sub get {
    my $self = shift;
    $self->render('index.html');
};
#具体的API类
package ListHandler;
use parent qw(Tatsumaki::Handler);
sub get {
#这里自动把urlpath切分好了
    my ( $self, $group, $order, $interval ) = @_;
    return 'Not valid order' unless $order eq 'count' or $order eq 'mean';
    return 'Not valid interval' unless $interval =~ m#\d+(h|m|s)#;
    my ($key_field, $value_field);
    if ( $group eq 'url' ) {
        $key_field = 'url';
        $value_field = 'responsetime';
    } elsif ( $group eq 'ip' ) {
        $key_field = 'oh';
        $value_field = 'upstreamtime';
    } else {
        return 'Not valid group field';
    };

    # get index mapping and sort into array
    my $mapping = $elsearch->mapping(
        index => "$index",
        type  => "$type",
    );
    my @res_map;
    for my $property ( sort keys %{ $mapping->{$type}->{'properties'} } ) {
        if ($property eq '@fields' ) {
            my @fields;
            push @fields, { name => $_, type => $mapping->{$type}->{'properties'}->{$property}->{'properties'}->{$_}->{'type'} }
                for sort keys %{ $mapping->{$type}->{'properties'}->{$property}->{'properties'} };
            push @res_map, \@fields;
        } else {
            push @res_map, { name => $property, type => $mapping->{$type}->{'properties'}->{$property}->{'type'} };
        }
    }

    # get value stat group by key field
    my $data = $elsearch->search(
        index => "$index",
        type  => "$type",
        size  => 0,
        query => {
            "range" => {
                '@timestamp' => {
                    from => "now-$interval",
                    to   => "now"
                },
            },
        },
        facets => {
            "$group" => {
                "terms_stats" => {
                    "value_field" => "$value_field",
                    "key_field"   => "$key_field",
                    "order"       => "$order",
                    "size"        => 20,
                }
            },
        }
    );
    my @res_tbl;
    for ( @{$data->{facets}->{"$group"}->{terms}} ) {
        my $key = $_->{term};
        my $mean = sprintf "%.03f", $_->{mean};
        my $code_count = code_count($key_field, $key, $interval);
        push @res_tbl, {
            key   => $key,
            min   => $_->{min},
            max   => $_->{max},
            mean  => $mean,
            code  => $code_count,
            count => $_->{count},
        };
    };

# render可以接收参数,并且默认把$self带进去,具体key是handler
    $self->render('index.html', { table => \@res_tbl, mapping => \@res_map });
};

sub code_count {
    my ($key_field, $key, $interval) = @_;
    my $result;
    my $data = $elsearch->search(
        index => "$index",
        type  => "$type",
        size  => 0,
        query => {
            range => {
                '@timestamp' => {
                    from => "now-$interval",
                    to   => "now"
                },
            },
        },
        facets => {
            "code" => {
                facet_filter => {
                    term => {
                        $key_field => "$key"
                    }
                },
                terms => {
                    field => "status",
                }
            }
        }
    );
    for ( @{$data->{facets}->{code}->{terms}} ) {
        $result->{$_->{term}} = $_->{count};
    };
    return $result;
};
#画图数据API类,因为响应的是Ajax请求,所以这里开启了async,不过其实没意义了。因为这个ElasticSearch代码不是async格式的。应该改造用ElasticSearch::Transport::AEHTTP才能做到全程async。
package ChartHandler;
use parent qw(Tatsumaki::Handler);
__PACKAGE__->asynchronous(1);
use JSON;
sub post {
    my $self = shift;
    my $api = $self->request->param('api') || 'term';
    my $key = $self->request->param('key') || 'oh';
    my $value = $self->request->param('value');
    my $status = $self->request->param('status') || '200';

    my $field =  $key eq 'oh' ? 'upstreamtime' : 'responsetime';
    my $data = $elsearch->search(
        index => "$index",
        type  => "$type",
        size  => 0,
        query => {
            match_all => { }
        },
        facets => {
            "chart" => {
                facet_filter => {
                    and => [
                        {
                            term => {
                                status => $status,
                            },
                        },
                        {
                            $api => {
                                $key => $value,
                            },
                        },
                    ]
                },
                date_histogram => {
                    value_field => $field,
                    key_field => '@timestamp',
                    interval => "1m"
                }
            },
        },
    );
    my @result;
    for ( @{$data->{'facets'}->{'chart'}->{'entries'}} ) {
        push @result, {
           time => $_->{'time'},
           count => $_->{'count'},
           mean => sprintf "%.3f", $_->{'mean'} * 1000,
        };
    };
    header('Content-Type' => 'application/json');
    to_json(\@result);
};
#主函数
package main;
use File::Basename;
#通过Tatsumaki::Application绑定urlpath到不同的类上。注意下面listhandler那里用的正则捕获。对,上面类里传参就是这么来的。注意最多不超过$9。
my $app = Tatsumaki::Application->new([
    '/' => 'MainHandler',
    '/api/chartdata' => 'ChartHandler',
    '/api/(\w+)/(\w+)/(\w+)' => 'ListHandler',
]);
#指定template和static的路径。类似Dancer里的views和public
$app->template_path(dirname(__FILE__) . '/templates');
$app->static_path(dirname(__FILE__) . '/static');
#psgi app组建完成
return $app->psgi_app;
true;

static里都是bootstrap的东西就不贴了。然后说说template。目前Tatsumaki只支持Text::MicroTemplate::File一种template,当然自己在handler里调用其他的template然后返回字符串也行。不过其实Text::MicroTemplate也蛮强大的。下面上例子:

%# 这就是Text::MicroTemplate强大的地方了,行首加个百分号就可以直接使用perl而不像TT那样尽量搞自己的语法
%# 配合render传来的handler(前面说了是类$self),整个环境全可以任意调用。
% my $mapping = $_[0]->{'mapping'};
% my $table = $_[0]->{'table'};
%# 比如这里其实就是通过handler调用request了。
% my $group = $_[0]->{handler}->args->[0];
% my @codes = qw(200 206 302 304 400 403 404 499 502 503 504);
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Bubble -- a perl webui for logstash & elasticsearch</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.css" >
<link rel="stylesheet" href="/static/fontawesome/css/font-awesome.css" >
<link rel="stylesheet" href="/static/css/style.css" >
<link rel="stylesheet" href="/static/amcharts/style.css" type="text/css">
</head>
<body>
<div class="container">
<div class="container-fluid">
  <div class="row-fluid">
    <div class="span3">
      <div class="well sidebar-nav">
        <ul class="nav nav-list">
          <li class="nav-header">查询</li>
% for my $property ( @{ $mapping } ) {
%     if ( ref( $property ) eq 'ARRAY' ) {
          <li>@fields</li>
%          for ( @{$property} ) {
          <li>  - <%= $_->{'name'} %> <%= $_->{'type'} %></li>
%          }
%     } elsif ( $property->{'type'} ) {
          <li><%= $property->{'name'} %> <%= $property->{'type'} %></li>
%     }
% }
        </ul>
      </div>
    </div>
    <div class="span9">
      <div id="search">
        <div class="control-group">
          <div class="controls docs-input-sizes">
            <select class="input-small inline" id="esapi" name="api">
              <option>匹配方式</option>
              <option>prefix</option>
              <option>term</option>
              <option>text</option>
            </select>
            <select class="input-small inline" id="eskey" name="key">
              <option>查询列</option>
              <option>oh</option>
              <option>url</option>
            </select>
            <input type="text" class="input-big inline" id="esvalue" name="value" placeholder="查询文本" />
            <input type="text" class="input-small inline" id="esstatus" name="status" placeholder="指定状态" />
            <button class="btn btn-primary" onclick="genchart()">查看</button>
          </div>
        </div>
      </div>
      <div id="chartdiv" style="display:none; width: 100%; height: 500px;"></div>
      <div id="estable">
% if ( $table ) {
        <table class="table table-striped table-bordered table-condensed">
          <thead>
            <tr>
              <th><%= $group %></th>
              <th>平均响应时间</th>
              <th>最大响应时间</th>
              <th>下载数</th>
%     for ( @codes ) {
              <th><%= $_ %></th>
%     }
            </tr>
          </thead>
          <tbody>
%     for my $list ( @{ $table } ) {
            <tr>
              <td><%= $list->{'key'} %></td>
              <td><%= $list->{'mean'} %></td>
              <td><%= $list->{'max'} %></td>
              <td><%= $list->{'count'} %></td>
%         for ( @codes ) {
%             if ( $list->{'code'}->{$_} ) {
              <td><%= $list->{'code'}->{$_} %></td>
%             } else {
              <td></td>
%             }
%         }
            </tr>
%     }
          </tbody>
        </table>
% }
      </div>
    </div>
  </div>
</div>
</div>
<script src="/static/javascripts/jquery-1.7.2.min.js"></script>
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
<script src="/static/amcharts/amstock.js" type="text/javascript"></script>
<script type="text/javascript">
  var chart;
  var chartProvider = [];
  function createStockChart() {
    chart = new AmCharts.AmStockChart();
    chart.pathToImages = "/static/amcharts/images/";
    var categoryAxesSettings = new AmCharts.CategoryAxesSettings();
    categoryAxesSettings.parseDates = true;
    categoryAxesSettings.minPeriod = "mm";
    chart.categoryAxesSettings = categoryAxesSettings;
    var dataSet = new AmCharts.DataSet();
    dataSet.fieldMappings = [{
      fromField : "count",
      toField : "count",
    }, {
      fromField : "mean",
      toField : "mean",
    }];
    dataSet.dataProvider = chartProvider;
    dataSet.categoryField = "date";
    chart.dataSets = [dataSet];

    var stockPanel1 = new AmCharts.StockPanel();
    stockPanel1.percentHeight = 70;

    var valueAxis1 = new AmCharts.ValueAxis();
    valueAxis1.position = "left";
    valueAxis1.axisColor = "#999999";
    stockPanel1.addValueAxis(valueAxis1);

    var graph1 = new AmCharts.StockGraph();
    graph1.valueField = "mean";
    graph1.title = "mean(ms)";
    graph1.type = "smoothedLine";
    graph1.lineColor = "#999999";
    graph1.fillAlphas = 0.2;
    graph1.useDataSetColors = false;
    stockPanel1.addStockGraph(graph1);

    var stockLegend1 = new AmCharts.StockLegend();
    stockPanel1.stockLegend = stockLegend1;
    stockPanel1.drawingIconsEnabled = true;

    var stockPanel2 = new AmCharts.StockPanel();
    stockPanel2.percentHeight = 30;
    stockPanel2.marginTop = 1;
    stockPanel2.categoryAxis.dashLength = 5;
    stockPanel2.showCategoryAxis = false;

    valueAxis2 = new AmCharts.ValueAxis();
    valueAxis2.dashLength = 5;
    valueAxis2.gridAlpha = 0;
    valueAxis2.axisThickness = 2;
    stockPanel2.addValueAxis(valueAxis2);

    var graph2 = new AmCharts.StockGraph();
    graph2.valueAxis = valueAxis2;
    graph2.valueField = "count";
    graph2.title = "count";
    graph2.balloonText = "[[value]]%";
    graph2.type = "column";
    graph2.cornerRadiusTop = 4;
    graph2.fillAlphas = 1;
    graph2.lineColor = "#FCD202";
    graph2.useDataSetColors = false;
    stockPanel2.addStockGraph(graph2);

    var stockLegend2 = new AmCharts.StockLegend();
    stockPanel2.stockLegend = stockLegend2;

    chart.panels = [stockPanel1, stockPanel2];

    var sbsettings = new AmCharts.ChartScrollbarSettings();
    sbsettings.graph = graph2;
    sbsettings.graphType = "line";
    sbsettings.height = 30;
    chart.chartScrollbarSettings = sbsettings;

    var cursorSettings = new AmCharts.ChartCursorSettings();
    cursorSettings.valueBalloonsEnabled = true;
    chart.chartCursorSettings = cursorSettings;

    $("#chartdiv").show();
    chart.write("chartdiv");

  };

  function genchart() {
    $.getJSON('/api/chartdata', {
      api : $("#esapi").val(),
      key : $("#eskey").val(),
      status : $("#esstatus").val(),
      value : $("#esvalue").val(),
    }, function(data) {
      for ( var i = 0; i < data.length; i++ ) {
        var date = new Date(data[i].time);
        chartProvider.push({
          date: date,
          count: data[i].count,
          mean: data[i].mean,
        });
      }
      createStockChart();
    });
  };
</script>
</body>
</html>

效果如下:

查询表格并提交最多次的url绘图


2012 年 12 月 30 日附注:

更好的纯 js 版本已经作为独立的 elasticsearch-plugin 项目发布在 github 上。地址:https://github.com/chenryn/elasticsearch-logstash-faceter 。欢迎大家试用!!

11/18
2012

用 ElasticSearch 和 Protovis 实现数据可视化


这是 ES 官方博客第一篇介绍如何用 JS 作 ES 的数据可视化。代码齐全,手把手教你入门

搜索引擎最重要的目的,嗯,不出意料就是搜索。你传给它一个请求,然后它依照相关性返回你一串匹配的结果。我们可以根据自己的内容创造各种请求结构,试验各种不同的分析器,搜索引擎都会努力尝试提供最好的结果。

不过,一个现代的全文搜索引擎可以做的比这个更多。因为它的核心是基于一个为了高效查询匹配文档而高度优化过的数据结构——倒排索引。它也可以为我们的数据完成复杂的聚合运算,在这里我们叫它facets。(不好翻译,后文对这个单词都保留英文)

facets通常的目的是提供给用户某个方面的导航或者搜索。 当你在网上商店搜索“相机”,你可以选择不同的制造商,价格范围或者特定功能来定制条件,这应该就是点一下链接的事情,而不是通过修改一长串查询语法。

一个LinkedIn的导航范例如下图所示:

图片1

Facet搜索为数不多的几个可以把强大的请求能力开放给最终用户的办法之一,详见Moritz Stefaner的试验“Elastic Lists”,或许你会有更多灵感。

但是,除了链接和复选框,其实我们还能做的更多。比如利用这些数据画图,而这就是我们在这篇文章中要讲的。

实时仪表板

在几乎所有的分析、监控和数据挖掘服务中,或早或晚的你都会碰到这样的需求:“我们要一个仪表板!”。因为大家都爱仪表板,可能因为真的有用,可能单纯因为它漂亮~这时候,我们不用写任何OLAP实现,用facets就可以完成一个很漂亮很给力的分析引擎。

下面的截图就是从一个社交媒体监控应用上获取的。这个应用不单用ES来搜索和挖掘数据,还通过交互式仪表板提供数据聚合功能。

图片2

当用户深入数据,添加一个关键字,使用一个自定义查询,所有的图都会实时更新,这就是facet聚合的工作方式。仪表板上不是数据定期计算好的的静态快照,而是一个用于数据探索的真正的交互式工具。

在本文中,我们将会学习到怎样从ES中获取数据,然后怎么创建这些图表。

关系聚合(terms facet)的饼图

第一个图,我们用ES中比较简单的termsfacet来做。这个facet会返回一个字段中最常见的词汇和它的计数值。

首先我们先插入一些数据。

curl -X DELETE "http://localhost:9200/dashboard"
curl -X POST "http://localhost:9200/dashboard/article" -d '
             { "title" : "One",
               "tags"  : ["ruby", "java", "search"]}
'
curl -X POST "http://localhost:9200/dashboard/article" -d '
             { "title" : "Two",
               "tags"  : ["java", "search"] }
'
curl -X POST "http://localhost:9200/dashboard/article" -d '
             { "title" : "Three",
               "tags"  : ["erlang", "search"] }
'
curl -X POST "http://localhost:9200/dashboard/article" -d '
             { "title" : "Four",
               "tags"  : ["search"] }
'
curl -X POST "http://localhost:9200/dashboard/_refresh"

你们都看到了,我们存储了一些文章的标签,每个文章可以多个标签,数据以JSON格式发送,这也是ES的文档格式。

现在,要知道文档的十大标签,我们只需要简单的请求:

curl -X POST "http://localhost:9200/dashboard/_search?pretty=true" -d '
{
    "query" : { "match_all" : {} },

    "facets" : {
        "tags" : { "terms" : {"field" : "tags", "size" : 10} }
    }
}
'

你看到了,我接受所有文档,然后定义一个terms facet叫做“tags”。这个请求会返回如下样子的数据:

{
    "took" : 2,
    // ... snip ...
    "hits" : {
        "total" : 4,
        // ... snip ...
    },
    "facets" : {
        "tags" : {
            "_type" : "terms",
            "missing" : 1,
            "terms" : [
                { "term" : "search", "count" : 4 },
                { "term" : "java",   "count" : 2 },
                { "term" : "ruby",   "count" : 1 },
                { "term" : "erlang", "count" : 1 }
            ]
        }
    }
}

JSON中facets部分是我们关心的,特别是facets.tags.terms数组。它告诉我们有四篇文章打了search标签,两篇java标签,等等…….(当然,我们或许应该给请求添加一个size参数跳过前面的结果)

这种比例类型的数据最合适的可视化方案就是饼图,或者它的变体:油炸圈饼图。最终结果如下(你可能希望看这个可运行的实例):

图片3

我们将使用Protovis一个JavaScript的数据可视化工具集。Protovis是100%开源的,你可以想象它是数据可视化方面的RoR。和其他类似工具形成鲜明对比的是,它没有附带一组图标类型来供你“选择”。而是定义了一组原语和一个灵活的DSL,这样你可以非常简单的创建自定义的可视化。创建饼图就非常简单。

因为ES返回的是JSON数据,我们可以通过Ajax调用加载它。不要忘记你可以clone或者下载实例的全部源代码

首先需要一个HTML文件来容纳图标然后从ES里加载数据:

<!DOCTYPE html>
<html>
<head>
    <title>ElasticSearch Terms Facet Donut Chart</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <!-- Load JS libraries -->
    <script src="jquery-1.5.1.min.js"></script>
    <script src="protovis-r3.2.js"></script>
    <script src="donut.js"></script>
    <script>
        $( function() { load_data(); });

        var load_data = function() {
            $.ajax({   url: 'http://localhost:9200/dashboard/article/_search?pretty=true'
                     , type: 'POST'
                     , data : JSON.stringify({
                           "query" : { "match_all" : {} },

                           "facets" : {
                               "tags" : {
                                   "terms" : {
                                       "field" : "tags",
                                       "size"  : "10"
                                   }
                               }
                           }
                       })
                     , dataType : 'json'
                     , processData: false
                     , success: function(json, statusText, xhr) {
                           return display_chart(json);
                       }
                     , error: function(xhr, message, error) {
                           console.error("Error while loading data from ElasticSearch", message);
                           throw(error);
                       }
            });

            var display_chart = function(json) {
                Donut().data(json.facets.tags.terms).draw();
            };

        };
    </script>
</head>
<body>

  <!-- Placeholder for the chart -->
  <div id="chart"></div>

</body>
</html>

文档加载后,我们通过Ajax收到和之前curl测试中一样的facet。在jQuery的Ajaxcallback里我们通过封装的display_chart()把返回的JSON传给Donut()函数.

Donut()函数及注释如下:

// =====================================================================================================
// A donut chart with Protovis - See http://vis.stanford.edu/protovis/ex/pie.html
// =====================================================================================================
var Donut = function(dom_id) {

    if ('undefined' == typeof dom_id)  {                // Set the default DOM element ID to bind
        dom_id = 'chart';
    }

    var data = function(json) {                         // Set the data for the chart
        this.data = json;
        return this;
    };

    var draw = function() {

        var entries = this.data.sort( function(a, b) {  // Sort the data by term names, so the
            return a.term < b.term ? -1 : 1;            // color scheme for wedges is preserved
        }),                                             // with any order

        values  = pv.map(entries, function(e) {         // Create an array holding just the counts
            return e.count;
        });
        // console.log('Drawing', entries, values);

        var w = 200,                                    // Dimensions and color scheme for the chart
            h = 200,
            colors = pv.Colors.category10().range();

        var vis = new pv.Panel()                        // Create the basis panel
            .width(w)
            .height(h)
            .margin(0, 0, 0, 0);

        vis.add(pv.Wedge)                               // Create the "wedges" of the chart
            .def("active", -1)                          // Auxiliary variable to hold mouse over state
            .data( pv.normalize(values) )               // Pass the normalized data to Protovis
            .left(w/3)                                  // Set-up chart position and dimension
            .top(w/3)
            .outerRadius(w/3)
            .innerRadius(15)                            // Create a "donut hole" in the center
            .angle( function(d) {                       // Compute the "width" of the wedge
                return d * 2 * Math.PI;
             })
            .strokeStyle("#fff")                        // Add white stroke

            .event("mouseover", function() {            // On "mouse over", set the "wedge" as active
                this.active(this.index);
                this.cursor('pointer');
                return this.root.render();
             })

            .event("mouseout",  function() {            // On "mouse out", clear the active state
                this.active(-1);
                return this.root.render();
            })

            .event("mousedown", function(d) {           // On "mouse down", perform action,
                var term = entries[this.index].term;    // such as filtering the results...
                return (alert("Filter the results by '"+term+"'"));
            })


            .anchor("right").add(pv.Dot)                // Add the left part of he "inline" label,
                                                        // displayed inside the donut "hole"

            .visible( function() {                      // The label is visible when its wedge is active
                return this.parent.children[0]
                       .active() == this.index;
            })
            .fillStyle("#222")
            .lineWidth(0)
            .radius(14)

            .anchor("center").add(pv.Bar)               // Add the middle part of the label
            .fillStyle("#222")
            .width(function(d) {                        // Compute width:
                return (d*100).toFixed(1)               // add pixels for percents
                              .toString().length*4 +
                       10 +                             // add pixels for glyphs (%, etc)
                       entries[this.index]              // add pixels for letters (very rough)
                           .term.length*9;
            })
            .height(28)
            .top((w/3)-14)

            .anchor("right").add(pv.Dot)                // Add the right part of the label
            .fillStyle("#222")
            .lineWidth(0)
            .radius(14)


            .parent.children[2].anchor("left")          // Add the text to label
                   .add(pv.Label)
            .left((w/3)-7)
            .text(function(d) {                         // Combine the text for label
                return (d*100).toFixed(1) + "%" +
                       ' ' + entries[this.index].term +
                       ' (' + values[this.index] + ')';
            })
            .textStyle("#fff")

            .root.canvas(dom_id)                        // Bind the chart to DOM element
            .render();                                  // And render it.
    };

    return {                                            // Create the public API
        data   : data,
        draw   : draw
    };

};

现在你们看到了,一个简单的JSON数据转换,我们就可以创建出丰富的有吸引力的关于我们文章标签分布的可视化图标。完整的例子在这里

当你使用完全不同的请求,比如显示某个特定作者的文章,或者特定日期内发表的文章,整个可视化都照样正常工作,代码是可以重用的。

日期直方图(date histogram facets)时间线

Protovis让创建另一种常见的可视化类型也非常容易:时间线。任何类型的数据,只要和特定日期相关的,比如文章发表,事件发生,目标达成,都可以被可视化成时间线。

最终结果就像下面这样(同样可以看运行版):

图片4

好了,让我们往索引里存一些带有发表日期的文章吧:

curl -X DELETE "http://localhost:9200/dashboard"
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "1",  "published" : "2011-01-01" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "2",  "published" : "2011-01-02" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "3",  "published" : "2011-01-02" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "4",  "published" : "2011-01-03" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "5",  "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "6",  "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "7",  "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "8",  "published" : "2011-01-04" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "9",  "published" : "2011-01-10" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "10", "published" : "2011-01-12" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "11", "published" : "2011-01-13" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "12", "published" : "2011-01-14" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "13", "published" : "2011-01-14" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "14", "published" : "2011-01-15" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "15", "published" : "2011-01-20" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "16", "published" : "2011-01-20" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "17", "published" : "2011-01-21" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "18", "published" : "2011-01-22" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "19", "published" : "2011-01-23" }'
curl -X POST "http://localhost:9200/dashboard/article" -d '{ "t" : "20", "published" : "2011-01-24" }'
curl -X POST "http://localhost:9200/dashboard/_refresh"

我们用ES的date histogram facet来获取文章发表的频率。

curl -X POST "http://localhost:9200/dashboard/_search?pretty=true" -d '
{
    "query" : { "match_all" : {} },

    "facets" : {
        "published_on" : {
            "date_histogram" : {
                "field"    : "published",
                "interval" : "day"
            }
        }
    }
}
'

注意我们是怎么设置间隔为天的。这个很容易就可以替换成周,月 ,或者年。

请求会返回像下面这样的JSON:

{
    "took" : 2,
    // ... snip ...
    "hits" : {
        "total" : 4,
        // ... snip ...
    },
    "facets" : {
        "published" : {
            "_type" : "histogram",
            "entries" : [
                { "time" : 1293840000000, "count" : 1 },
                { "time" : 1293926400000, "count" : 2 }
                // ... snip ...
            ]
        }
    }
}

我们要注意的是facets.published.entries数组,和上面的例子一样。同样需要一个HTML页来容纳图标和加载数据。机制既然一样,代码就直接看这里吧。

既然已经有了JSON数据,用protovis创建时间线就很简单了,用一个自定义的area chart即可。

完整带注释的Timeline()函数如下:

// =====================================================================================================
// A timeline chart with Protovis - See http://vis.stanford.edu/protovis/ex/area.html
// =====================================================================================================

var Timeline = function(dom_id) {
    if ('undefined' == typeof dom_id) {                 // Set the default DOM element ID to bind
        dom_id = 'chart';
    }

    var data = function(json) {                         // Set the data for the chart
        this.data = json;
        return this;
    };

    var draw = function() {

        var entries = this.data;                        // Set-up the data
            entries.push({                              // Add the last "blank" entry for proper
              count : entries[entries.length-1].count   // timeline ending
            });
        // console.log('Drawing, ', entries);

        var w = 600,                                    // Set-up dimensions and scales for the chart
            h = 100,
            max = pv.max(entries, function(d) {return d.count;}),
            x = pv.Scale.linear(0, entries.length-1).range(0, w),
            y = pv.Scale.linear(0, max).range(0, h);

        var vis = new pv.Panel()                        // Create the basis panel
            .width(w)
            .height(h)
            .bottom(20)
            .left(20)
            .right(40)
            .top(40);

         vis.add(pv.Label)                              // Add the chart legend at top left
            .top(-20)
            .text(function() {
                 var first = new Date(entries[0].time);
                 var last  = new Date(entries[entries.length-2].time);
                 return "Articles published between " +
                     [ first.getDate(),
                       first.getMonth() + 1,
                       first.getFullYear()
                     ].join("/") +

                     " and " +

                     [ last.getDate(),
                       last.getMonth() + 1,
                       last.getFullYear()
                     ].join("/");
             })
            .textStyle("#B1B1B1")

         vis.add(pv.Rule)                               // Add the X-ticks
            .data(entries)
            .visible(function(d) {return d.time;})
            .left(function() { return x(this.index); })
            .bottom(-15)
            .height(15)
            .strokeStyle("#33A3E1")

            .anchor("right").add(pv.Label)              // Add the tick label (DD/MM)
            .text(function(d) {
                 var date = new Date(d.time);
                 return [
                     date.getDate(),
                     date.getMonth() + 1
                 ].join('/');
             })
            .textStyle("#2C90C8")
            .textMargin("5")

         vis.add(pv.Rule)                               // Add the Y-ticks
            .data(y.ticks(max))                         // Compute tick levels based on the "max" value
            .bottom(y)
            .strokeStyle("#eee")
            .anchor("left").add(pv.Label)
                .text(y.tickFormat)
                .textStyle("#c0c0c0")

        vis.add(pv.Panel)                               // Add container panel for the chart
           .add(pv.Area)                                // Add the area segments for each entry
           .def("active", -1)                           // Auxiliary variable to hold mouse state
           .data(entries)                               // Pass the data to Protovis
           .bottom(0)
           .left(function(d) {return x(this.index);})   // Compute x-axis based on scale
           .height(function(d) {return y(d.count);})    // Compute y-axis based on scale
           .interpolate('cardinal')                     // Make the chart curve smooth
           .segmented(true)                             // Divide into "segments" (for interactivity)
           .fillStyle("#79D0F3")

           .event("mouseover", function() {             // On "mouse over", set segment as active
               this.active(this.index);
               return this.root.render();
           })

           .event("mouseout",  function() {             // On "mouse out", clear the active state
               this.active(-1);
               return this.root.render();
           })

           .event("mousedown", function(d) {            // On "mouse down", perform action,
               var time = entries[this.index].time;     // eg filtering the results...
               return (alert("Timestamp: '"+time+"'"));
           })

           .anchor("top").add(pv.Line)                  // Add thick stroke to the chart
           .lineWidth(3)
           .strokeStyle('#33A3E1')

           .anchor("top").add(pv.Dot)                   // Add the circle "label" displaying
                                                        // the count for this day

           .visible( function() {                       // The label is only visible when
               return this.parent.children[0]           // its segment is active
                          .active() == this.index;
            })
           .left(function(d) { return x(this.index); })
           .bottom(function(d) { return y(d.count); })
           .fillStyle("#33A3E1")
           .lineWidth(0)
           .radius(14)

           .anchor("center").add(pv.Label)             // Add text to the label
           .text(function(d) {return d.count;})
           .textStyle("#E7EFF4")

           .root.canvas(dom_id)                        // Bind the chart to DOM element
           .render();                                  // And render it.
    };

    return {                                            // Create the public API
        data   : data,
        draw   : draw
    };

};

完整示例代码在这里。不过先去下载protovis提供的关于area的原始文档,然后观察当你修改interpolate('cardinal')interpolate('step-after')后发生了什么。对于多个facet,画叠加的区域图,添加交互性,然后完全定制可视化应该都不是什么问题了。

重要的是注意,这个图表完全是根据你传递给ES的请求做出的响应,使得你有可能做到简单立刻的完成某项指标的可视化需求。比如“显示这个作者在这个主题上最近三个月的出版频率”。只需要提交这样的请求就够了:

author:John AND topic:Search AND published:[2011-03-01 TO 2011-05-31]

总结

当你需要为复杂的自定义查询做一个丰富的交互式的数据可视化时,使用ES的facets应该是最容易的办法之一,你只需要传递ES的JSON响应给Protovis这样的工具就好了。

通过模仿本文中的方法和代码,你可以在几小时内给你的数据跑通一个示例。

10/30
2012

Outputs::ElasticsearchHTTP 自动获取随机 node


logstash 1.4 以后已经统一使用 Outputs::Elasticsearch 模块设置不同 protocol 的方式进行写入。不过本文对了解 ES 的 HTTP 协议依然有一定帮助

今天在ES群中和medcl请教了一下index的性能问题。基本上在bulk的基础上,还有几点是可以做的。当然medcl说的是正常的全文索引的场景:

  • 不用http协议,直接走tcp层,维护一个pool发bulk;
  • 多台node的情况下,在bulk前先设置replica为0;bulk完成后再调整replica;
  • 因为es会自动路由,所以index请求可以分散开直接发给多个node。

总之,就是减少集群内部的网络传输。

介于logstash的应用是一直持续往es写数据的,所以replica调整这招用不上,顶多加大refresh时间而已。所以可以动手的地方主要就是第三条了。

正好去翻了一下perl的Elasticsearch.pm的POD。发现原来perl模块本色默认就是这么做的。new的时候定义的server,是用来发送请求获取集群所有alive的nodes。然后会从这个nodes列表里选择(随机)一个创建真正的链接返回。获取nodes的API如下:

curl http://192.168.1.33:9200/_cluster/nodes?pretty=1
{
  "ok" : true,
  "cluster_name" : "logstash",
  "nodes" : {
    "he6ipuA3SDeNmOQYIr-bjg" : {
      "name" : "Afari, Jamal",
      "transport_address" : "inet[/192.168.1.33:9300]",
      "hostname" : "ES-33.domain.com",
      "http_address" : "inet[/192.168.1.33:9200]"
    },
    "WXK68VX0ThmNnozq0uioQw" : {
      "name" : "Harker, Quincy",
      "transport_address" : "inet[/192.168.1.68:9300]",
      "hostname" : "ES-68.domain.com",
      "http_address" : "inet[/192.168.1.68:9200]"
    }
  }
}

这样显然可以在cluster较大的时候分担index的压力(search的时候压力在集群本身的cpu上)。我打算给我的pure-ruby branch里的faraday版的Logstash::Outputs::ElasticsearchHTTP也加上这个功能。


大致简单实现如下:

  def select_rand_host
    require "json"
    begin
      response = @agent.get '/_cluster/nodes'
      nodelist = JSON.parse(response.body)['nodes'].values
      livenode = nodelist[rand(nodelist.length)]["http_address"].split(/\[|\]/)[1]
      @agent = Faraday.new(:url => "http:/#{livenode}") do |faraday|
        faraday.use Faraday::Adapter::EMHttp
      end
    end
  end

然后在def register里和def flushretry前面都加上select_rand_host()就好了。当然比起perl的ElasticSearch::Transport里各种检查各种排除,我这个还是简单多了…… 另,在Ruby1.9里从数组返回随机元素可以直接调用.sample,真赞。不过谁让我都是1.8.7的版本呢……

2012 年 12 月 30 日附注:

之后我在 maillist 里联系了 logstash 的作者。不过作者表示:第一,需要自选 node 功能的,建议使用 output/elasticsearch 因为这个是用的 java 客户端,直接会把自己作为一个 node 加入 ES 的 cluster。第二,ruby 的 http 模块他都不喜欢,所以全部项目里的相关部分他都只用自己写的 ftw 模块 ==!

2013 年 02 月 26 日附注:

最新版的 Logstash::Outputs::ElasticSearchHTTP 已经加入随机选择 node 功能。是其他网友在 ftw 模块基础上添加的。

10/21
2012

ElasticSearch 的几点使用事项


总结 ES 在做日志处理项目时的性能优化措施。尤其是 mapping 定制等

之前已经写过一些ES的使用,也翻译了一篇官网上关于ES存储日志的建议日志。今天稍微总结一下近期以来实践出来的方案。

shard和replica的选择

在测试期,可以单节点上设置成1 shard + 0 replica的方式,这种的indexing速度是最快的(存疑:我至今没搞清楚ES在index的时候集群"应该"是比单node快还是慢)。

我曾经按照"常理"(我想象中的)理解,设定成10 shards + 0 replica,期望能用上并行写双node加倍index,事实上压根没用,而且因为另一台node上有其他负载的原因导致更慢了。

更可怕的是:就在前几天,突然出现一个shard挂了,……毫无办法,整个index全作废了。

所以结论是:无论如何,一定要保证有 > 0 份的replica!! 至于shards,保持默认的5个,或者顶多到20个也就差不多了。在maillist里看到有哥们设了100个,然后苦着脸问性能问题…… 要知道shards的份数是一旦设定不能更改的。

template的使用

刚开始的时候,每次实验都去改/etc/elasticsearch/elasticsearch.yml配置文件。事实上在template里修改settings更方便而且灵活!当然最主要的,还是调节里面的properties设定,合理的控制store和analyze了。

template设定也有多种方法。最简单的就是和存储数据一样POST上去。长期的办法,就是写成json文件放在配置路径里。其中,default配置放在/etc/elasticsearch/下,其他配置放在/etc/elasticsearch/templates/下。举例我现在的一个templates/template-logstash.json内容如下:

{
  "template-logstash" : {
    "template" : "logstash*",
    "settings" : {
      "index.number_of_shards" : 5,
      "number_of_replicas" : 1,
      "index" : {
        "store" : {
          "compress" : {
            "stored" : true,
            "tv": true
          }
        }
      }
    },
    "mappings" : {
      "_default_" : {
        "properties" : {
          "dynamic" : "true",
        },
      },
      "loadbalancer" : {
        "_source" : {
          "compress" : true,
        },
        "_ttl" : {
          "enabled" : true,
          "default" : "10d"
        },
        "_all" : {
          "enabled" : false
        },
        "properties" : {
          "@fields" : {
            "dynamic" : "true",
            "properties" : {
              "client" : {
                "type" : "string",
                "index" : "not_analyzed"
              },
              "domain" : {
                "type" : "string",
                "index" : "not_analyzed"
              },
              "oh" : {
                "type" : "string",
                "index" : "not_analyzed"
              },
              "responsetime" : {
                "type" : "double",
              },
              "size" : {
                "type" : "long",
                "index" : "not_analyzed"
              },
              "status" : {
                "type" : "string",
                "index" : "not_analyzed"
              },
              "upstreamtime" : {
                "type" : "double",
              },
              "url" : {
                "type" : "string",
                "index" : "not_analyzed"
              }
            }
          },
          "@source" : {
            "type" : "string",
            "index" : "not_analyzed"
          },
          "@timestamp" : {
            "type" : "date",
            "format" : "dateOptionalTime"
          },
          "@type" : {
            "type" : "string",
            "index" : "not_analyzed",
            "store" : "no"
          }
        }
      }
    }
  }
}

注意:POST 发送的 json 内容比存储的 json 文件内容要少最外层的名字,因为名字是在 url 里体现的。

mapping简介

上面template中除了index/shard/replica之外的部分,就是mapping了,大家注意到其中的dynamic,默认情况下,index会在第一条数据进入的时候自动分析这条数据的情况,给每个value找到最恰当的type,然后以此为该index的mapping。之后再PUT上来的数据,格式如果不符合mapping的,也能存储成功,但是就无法检索了。

mapping中关于store和compress的部分,之前翻译的《用ElasticSearch存储日志》已经说的比较详细了。这里我的建议是 disable 掉 _all,但是 enable 住 _source!! 经过我的惨痛测试,如果连 _source 也 disable 掉的话,一旦你重启进程,整个 index 里除了 _id_timestamp_score 三个默认字段,啥都丢了……

API简介

ES的API,最基本的就是CRUD操作了,这部分是标准的REST,就不说了。

然后还有三个API比较重要且常用,分别是: bulk/count/search。

  • Bulk顾名思义,把多个单条的记录合并成一个大数组统一提交,这样避免一条条发送的header解析,索引频繁更新,indexing速度大大提高
  • Count根据POST的json,返回命中范围内的总条数。当然没POST时就直接返回该index的总条数了。
  • Search根据POST的json或者GET的args,返回命中范围内的数据。这是最重要的部分了。下面说说常用的search API:

query

一旦使用search,必须至少提供query参数,然后在这个query的基础上进行接下来其他的检索。query参数又分三类:

  • "match_all" : { } 直接请求全部;
  • "term"/"text"/"prefix"/"wildcard" : { "key" : "value" } 根据字符串搜索(严格相等/片断/前缀/匹配符);
  • "range" : { "@timestamp" : { "from" : "now-1d", "to" : "now" } } 根据范围搜索,如果type是时间格式,可以使用内置的now表示当前,然后用-1d/h/m/s来往前推。

filter

上面提到的query的参数,在filter中也都存在。此外,还有比较重要的参数就是连接操作:

  • "or"/"and" : [{"range":{}}, {"prefix":""}] 两个filter的查询,交集或者合集;
  • "bool" : ["must":{},"must_not":{},"should":{}] 上面的and虽然更快,但是只能支持两个,超过两个的,要用 bool 方法;
  • "not"/"limit" : {} 取反和限定执行数。注意这个limit和mysql什么的有点不同:它限定的是在每个shards上执行多少条。如果你有5个shards,其实对整个index是limit了5倍大小的设定值。

另一点比较关键的是:filter结果默认是不缓存的,如果常用,需要指定 "_cache" : true

facets

facets接口可以根据query返回统计数据,最基础的是terms和statistical两种。不过在日志分析的情况下,最常用的是:

  • "histogram" : { "key_field" : "", "value_field" : "", "interval" : "" } 根据时间间隔返回柱状图式的统计数据;
  • "terms_stats" : { "key_field" : "", "value_field" : "" } 根据key的情况返回value的统计数据,类似group by的意思。

这里就涉及到前面mapping里为什么针对每个field都设定type的原因了。因为 histogram 里的 key_field 只能是 dateOptionalTime 格式的,value_field 只能是 string 格式的;而 terms_stats 里的 key_field 只能是 string 格式的,value_field 只能是 numberic 格式的。

而我们都知道,http code那些200/304/400/503神马的,看起来是数字,我们却需要的是他们的count数据,不是算他们的平均数。所以不能由ES动态的认定为long,得指定为string。

analyze简介

对于logstash分析日志,基本没有提到analyze的部分,包括Kibana也是。但是做web日志分析,其实也需要注意analyze。因为ES默认提供并开启了一些analyze。最简单的比如空格分隔表示单词,斜线分割表示url路径,@分割表示email地址等等。文档地址见http://www.elasticsearch.org/guide/reference/index-modules/analysis/。当然ES社区的中国人也有提供中文分词的plugin。通常情况下,analyze工作的很好。嗯,ES比其他全文索引工具在默认情况下都工作的好。

但是当你想算的是今天访问的url排名,或者来访者IP排名的时候,麻烦来了,你苦苦等待N久,最后一看排名是这样的:

jpg  2345678
html 123456
20121021 34567
bbs 9876

对,你的url被ES辛辛苦苦的用 / 和 . 分割了,然后每个单词排序来再返回给你。如果你是在一个数千万条的大型库上运行的话,基本吃个饭回来才能有结果。

事实上,url就是一个整体,所以在mapping中,要定义好在indexing的时候,不要启用analyzer。这样,返回一个你心目中想要的正确的url排名,时间从吃个午饭直接缩减到打个喷嚏了!而如果是访问者ip,时间则是眨下眼就够了!

注意:analyze不是只有indexing的时候能用,在query的时候,也可以单独指定某个analyze来分析记录。analyze其实是和search并列的API,不过目前场景下用不上,就不说了。

有以上API,基本上一个针对logstash的ES数据分析系统后台就足够构建出来了。剩下的就是前端页面的事情,这方面可以参考logstash的Kibana,更广义一些的ES数据可视化可以参考ES的blog: http://www.elasticsearch.cn/blog/2011/05/13/data-visualization-with-elasticsearch-and-protovis.html,笔者的译文见http://chenlinux.com/2012/11/18/data-visualization-with-elasticsearch-and-protovis

性能监控

ES周边的工具有很多。目前我主要用三种方式:

  • es_head: 这个主要提供的是健康状态查询,当然标签页里也提供了简单的form给你提交API请求。es_head现在可以直接通过 elasticsearch/bin/plugin -install mobz/elasticsearch-head 安装,然后浏览器里直接输入 http://$eshost:9200/_plugin/head/ 就可以看到cluster/node/index/shards的状态了。
  • bigdesk: 这个主要提供的是节点的实时状态监控,包括jvm的情况,linux的情况,elasticsearch的情况。排查性能问题的时候很有用,现在也可以通过 elasticsearch/bin/plugin -install lukas-vlcek/bigdesk 直接安装了。然后浏览器里直接输入 http://$eshost:9200/_plugin/bigdesk/ 就可以看到了。注意如果使用的 bulk_index 的话,如果选择的刷新间隔太长,indexing per second数据是不准的。
  • 然后是最基础的办法,通过ES本身的status API获取状态。因为上面都是web工具,如果想要避免上文提到的故障很久才发现的问题,我们需要一个可以提供给nagios使用的办法,这很简单就可以做到。刚巧ES本身也有green/yellow/red等不同的状态。所以很简单完成一个check_es_health.sh如下:
#!/bin/sh
    ES_HOST=$1
    ES_URI="http://${ES_HOST}:9200/_cluster/health"
    RES_JSON=`curl -s ${ES_URI}`
    
    status=`echo ${RES_JSON}|awk -F\" '{print $8}'`
    failed=`echo ${RES_JSON}|awk -F\" '{print $NF}'|sed 's/^:\([0-9]*\)}/\1/'`
    
    if [[ "$status" -eq "green" ]];then
        echo "ES Cluster OK | failed_node=${failed}"
        exit 0
    elif [[ "$status" -eq "yellow" ]];then
        echo "Warning! ES Cluster shards relocating or initializing. | failed_node=${failed}"
        exit 1
    else
        echo "Critical! ES Cluster shards unassigned. | failed_node=${failed}"
        exit 2
    fi

其他插件

ES是一个很活跃的开源项目,所以如果有其他目前ES没有你有觉得有需要的功能,大可以上github搜索一下,或许别人早已经做完相关插件了。

比如我就在上面找到一个plugin叫elasticfacets。加强了ES的 date_histogram 功能,原先只能针对某个 value_field 做攻击,这个plugin可以在这个基础上,把 value_field 加强成又一层facets。项目地址: https://github.com/bleskes/elasticfacets。之前和作者反馈了在ES 0.19.8上的问题,不知道修复没。或许最好还是用0.19.9吧。

邮件列表

ES的邮件列表基本每天都有四五十封邮件,地址是:elasticsearch@googlegroups.com

09/21
2012

json-event 数据格式


json-event 是 logstash 1.3 版本之前的配置方式。之后 logstash 实现了专门的 codecs 插件做相关处理

之前的各种示例中,都没有提到logstash的输入输出格式。看起来就好像logstash比Message::Passing少了decoder/encoder一样。其实logstash也有类似的设定的,这就是format。有三种选择:plain/json/json_event。默认情况下是plain。也就是我们之前的通用做法,传文本给logstash,由logstash转换成json。

logstash社区根据某些应用场景,有相关的cookbook。关于访问日志,有http://cookbook.logstash.net/recipes/apache-json-logs/。这是一个不错的思路!我们可以照葫芦画瓢给nginx也定义一下:

 logformat json '{"@timestamp":"$time_iso8601",'
                '"@source":"$server_addr",'
                '"@fields":{'
                '"client":"$remote_addr",'
                '"size":$body_bytes_sent,'
                '"responsetime":$request_time,'
                '"upstreamtime":$upstream_response_time,'
                '"oh":"$upstream_addr",'
                '"domain":"$host",'
                '"url":"$uri",'
                '"status":"$status"}}';
 access_log /data/nginx/logs/access.json json;

这里需要注意的地方是:因为最后需要插入ES的某些field是有double/float类型。所以麻烦来了:一些端口监控工具的请求,状态码为400的,因为直接断开,所以并没有链接上upstream的服务器,其$upstream_response_time变量不存在,记录在日志里是-,这对于数值型是非法的定义。直接把带有400的日志通过file格式输入给logstash的时候,因为这个非法定义会报错,并把这行日志给丢弃掉。那么我们就无法统计400请求的数据了。

这里需要变通一下,我们知道其实所谓的Input::File就等效于tail -F ${path}${filename}(当然其实不是,模块的实际做法是在~/.sincedb里记录上次读取的位置,然后每${stat_interval}秒检查一次内容更新,每${discover_interval}秒检查一次文件描述符变更。也就是说默认其实是每秒读一次,一次几百上千行,这样效率更高)。所以我们可以自己运行tail命令,然后sed修正upstream_response_time后通过管道传递给logstash的Input::STDIN,效果是一样一样的。 新的logstash/agent.conf如下:

input {
    stdin {
        type => "nginx"
        format => "json_event"
    }
} 
output {
    amqp {
        type => "nginx"
        host => "10.10.10.10"
        key  => "cdn"
        name => "logstash"
        exchange_type => "direct"
    }
}

运行命令如下:

#!/bin/sh
  tail -F /data/nginx/logs/access.json \
| sed 's/upstreamtime":-/upstreamtime":0/' \
| /usr/local/logstash/bin/logstash -f /usr/local/logstash/etc/agent.conf &

这样可以直接省略掉昂贵的Grok操作,同时节约原本的all/message/_source_host等等格式的空间。

09/16
2012

ElasticSearch 的 bulk_index 速度测试


本文原为 Logstash 的 Perl 版本,Message::Passing 模块的测试记录。注意文中程序使用的 ElasticSearch 模块已被废弃。现在统一使用官方的 Search::Elasticsearch 模块

连续尝试了logstash的elasticsearch/elasticsearch_http/elasticsearch_river三个putput模块,发现其index/bulk/river三种插入方式的实际运行效果速度居然没有差异。而使用perl脚本测试,单例下index不到300msg/sec,bulk接近2500msg/sec,几乎翻了10倍。

测试脚本如下:

    #!/usr/bin/perl -w
    use ElasticSearch;
    use JSON;
    use Time::HiRes qw/time/;
    use Data::Dumper;
    #curl -XGET 'http://localhost:9200/logstash-2012.09.14/nginx/_mapping'
    my $line = '{
        "@timestamp" : "2012-09-04T13:38:59.496888Z",
        "@tags" : [], 
        "@fields" : { 
           "reqtime" : [ 
              0.016
           ],  
           "req" : [ 
              "/fmn056/20120812/1645/tiny_r3N9_236f000036ad118d.jpg"
           ],  
           "version" : [ 
              "1.1"
           ],  
           "useragent" : [ 
              "\"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)\""
           ],  
           "port" : [ 
              "80"
           ],  
           "size" : [ 
              2360
           ],  
           "client" : [ 
              "210.56.223.176"
           ],  
           "upstream" : [ 
              "10.9.18.50"
           ],  
           "method" : [ 
              "GET"
           ],  
           "referer" : [ 
              "photo.renren.com",
              "/photo/420723228/photo-6408408309?psource=3&fromVIP=false"
           ],  
           "ZONE" : [ 
              "+0800"
           ],  
           "code" : [ 
              200 
           ],  
           "upstime" : [ 
              0.016
           ]   
        },  
        "@source_path" : "//data/nginx/logs/access.log",
        "@source" : "file://DBLYD5-32.opi.com//data/nginx/logs/access.log",
        "@message" : "[04/Sep/2012:21:38:59 +0800] 200 210.56.223.176 fmn.rrimg.com GET /fmn056/20120812/1645/tiny_r3N9_236f000036ad118d.jpg HTTP/1.1 10.9.18.50:80 0.016 0.016 2360 \"http://photo.renren.com/photo/420723228/photo-6408408309?psource=3&fromVIP=false\" \"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)\" \"-\"",
        "@source_host" : "DBLYD5-32.opi.com",
        "@type" : "nginx"
    }';
    my $hash = from_json($line);
    my $elsearch = ElasticSearch->new(
        servers      => '10.4.16.68:9200',
        transport    => 'httplite',
        max_requests => 10000,
    );
    my $begin = time;
    for ( 1 .. 1000 ) {
        my @data;
        push @data, { index => { data => $hash } } for 1 .. 20;
        $elsearch->bulk(
            index => 'logstash-test',
            type  => 'nginx',
            actions => \@data
        );
    }
    print 1000 * 20 / (time - $begin);

注意到这里bulk的数组是20个元素。实验证明超过20个会报出HTTP::Lite的错误(附带提示:ElasticSearch::Transport::*的HTTPLite啊AEHTTP啊的模块都是要另外安装的)。而且在使用Logstash::Outputs::ElasticSearchHTTP时,flush_size的default值100也是无法使用的,也是改到20后才行。

08/26
2012

用 ElasticSearch 存储日志


这是 ES 官方博客最早介绍其日志处理方面的用途的文章。其中一些调优建议至今有效

介绍

如果你使用elasticsearch来存储你的日志,本文给你提供一些做法和建议。

如果你想从多台主机向elasticsearch汇集日志,你有以下多种选择:

  • Graylog2 安装在一台中心机上,然后它负责往elasticsearch插入日志,而且你可以使用它那个漂亮的搜索界面~
  • Logstash 他有很多特性,包括你能输入什么日志,如何变换过滤,最好输出到哪里。其中就有输出到elasticsearch,包括直接输出和通过RabbitMQ的river方式两种。
  • Apache Flume 这个也可以从海量数据源中获取日志,用"decorators"修改日志,也有各种各样的"sinks"来存储你的输出。和我们相关的是elasticflume sink
  • omelasticsearch Rsyslog的输出模块。你可以在你的应用服务器上通过rsyslog直接输出到elasticsearch,也可以用rsyslog传输到中心服务器上来插入日志。或者,两者结合都行。具体如何设置参见rsyslog Wiki
  • 定制方案。比如,专门写一个脚本从天南海北的某个服务器传输你的日志到elasticsearch。

根据你设定的不同,最佳配置也变化不定。不过总有那么几个有用的指南可以推荐一下:

内存和打开的文件数

如果你的elasticsearch运行在专用服务器上,经验值是分配一半内存给elasticsearch。另一半用于系统缓存,这东西也很重要的。

你可以通过修改ES_HEAP_SIZE环境变量来改变这个设定。在启动elasticsearch之前把这个变量改到你的预期值。另一个选择上球该elasticsearch的ES_JAVA_OPTS变量,这个变量时在启动脚本(elasticsearch.in.sh或elasticsearch.bat)里传递的。你必须找到-Xms和-Xmx参数,他们是分配给进程的最小和最大内存。建议设置成相同大小。嗯,ES_HEAP_SIZE其实就是干的这个作用。

你必须确认文件描述符限制对你的elasticsearch足够大,建议值是32000到64000之间。关于这个限制的设置,另有教程可以参见。

目录数

一个可选的做法是把所有日志存在一个索引里,然后用ttl field来确保就日志被删除掉了。不过当你日志量够大的时候,这可能就是一个问题了,因为用TTL会增加开销,优化这个巨大且唯一的索引需要太长的时间,而且这些操作都是资源密集型的。

建议的办法是基于时间做目录。比如,目录名可以是YYYY-MM-DD的时间格式。时间间隔完全取决于你打算保留多久日志。如果你要保留一周,那一天一个目录就很不错。如果你要保留一年,那一个月一个目录可能更好点。目录不要太多,因为全文搜索的时候开销相应的也会变大。

如果你选择了根据时间存储你的目录,你也可以缩小你的搜索范围到相关的目录上。比如,如果你的大多数搜索都是关于最近的日志的,那么你可以在自己的界面上提供一个"快速搜索"的选项只检索最近的目录。

轮转和优化

移除旧日志在有基于时间的目录后变得异常简单:

$ curl -XDELETE 'http://localhost:9200/old-index-name/'

这个操作的速度非常快,和删除大小差不多的少量文件速度接近。你可以放进crontab里半夜来做。

Optimizing indices是在非高峰时间可以做的一件很不错的事情。因为它可以提高你的搜索速度。尤其是在你是基于时间做目录的情况下,更建议去做了。因为除了当前的目录外,其他都不会再改,你只需要对这些旧目录优化一次就一劳永逸了。

$ curl -XPOST 'http://localhost:9200/old-index-name/_optimize'

分片和复制

通过elasticsearch.yml或者使用REST API,你可以给每个目录配置自己的设定。具体细节参见链接

有趣的是分片和复制的数量。默认情况下,每个目录都被分割成5个分片。如果集群中有一个以上节点存在,每个分片会有一个复制。也就是说每个目录有一共10个分片。当往集群里添加新节点的时候,分片会自动均衡。所以如果你有一个默认目录和11台服务器在集群里的时候,其中一台会不存储任何数据。

每个分片都是一个Lucene索引,所以分片越小,elasticsearch能放进分片新数据越少。如果你把目录分割成更多的分片,插入速度更快。请注意如果你用的是基于时间的目录,你只在当前目录里插入日志,其他旧目录是不会被改变的。

太多的分片带来一定的困难——在空间使用率和搜索时间方面。所以你要找到一个平衡点,你的插入量、搜索频率和使用的硬件条件。

另一方面,复制帮助你的集群在部分节点宕机的时候依然可以运行。复制越多,必须在线运行的节点数就可以越小。复制在搜索的时候也有用——更多的复制带来更快的搜索,同时却增加创建索引的时间。因为对猪分片的修改,需要传递到更多的复制。

映射source和all

Mappings定义了你的文档如何被索引和存储。你可以,比如说,定义每个字段的类型——比如你的syslog里,消息肯定是字符串,严重性可以是整数。怎么定义映射参见链接

映射有着合理的默认值,字段的类型会在新目录的第一条文档插入的时候被自动的检测出来。不过你或许会想自己来调控这点。比如,可能新目录的第一条记录的message字段里只有一个数字,于是被检测为长整型。当接下来99%的日志里肯定都是字符串型的,这样Elasticsearch就没法索引他们,只会记录一个错误日志说字段类型不对。这时候就需要显式的手动映射"message" : {"type" : "string"}。如何注册一个特殊的映射详见链接

当你使用基于时间的目录名时,在配置文件里创建索引模板可能更适合一点。详见链接。除去你的映射,你海可以定义其他目录属性,比如分片数等等。

在映射中,你可以选择压缩文档的_source。这实际上就是整行日志——所以开启压缩可以减小索引大小,而且依赖你的设定,提高性能。经验值是当你被内存大小和磁盘速度限制的时候,压缩源文件可以明显提高速度,相反的,如果受限的是CPU计算能力就不行了。更多关于source字段的细节详见链接

默认情况下,除了给你所有的字段分别创建索引,elasticsearch还会把他们一起放进一个叫all的新字段里做索引。好处是你可以在all里搜索那些你不在乎在哪个字段找到的东西。另一面是在创建索引和增大索引大小的时候会使用额外更多的CPU。所以如果你不用这个特性的话,关掉它。即使你用,最好也考虑一下定义清楚限定哪些字段包含进_all里。详见链接

刷新间隔

在文档被索引后,Elasticsearch某种意义上是近乎实时的。在你搜索查找文档之前,索引必须被刷新。默认情况下,目录是每秒钟自动异步刷新的。

刷新是一个非常昂贵的操作,所以如果你稍微增大一些这个值,你会看到非常明显提高的插入速率。具体增大多少取决于你的用户可以接受到什么程度。

你可以在你的index template里保存期望的刷新间隔值。或者保存在elasticsearch.yml配置文件里,或者通过(REST API)[http://www.elasticsearch.org/guide/reference/api/admin-indices-update-settings.html]升级索引设定。

另一个处理办法是禁用掉自动刷新,办法是设为-1。然后用REST API手动的刷新。当你要一口气插入海量日志的时候非常有效。不过通常情况下,你一般会采用的就是两个办法:在每次bulk插入后刷新或者在每次搜索前刷新。这都会推迟他们自己本身的操作响应。

Thrift

通常时,REST接口是通过HTTP协议的,不过你可以用更快的Thrift替代它。你需要安装transport-thrift plugin同时保证客户端支持这点。比如,如果你用的是pyes Python client,只需要把连接端口从默认支持HTTP的9200改到默认支持Thrift的9500就好了。

异步复制

通常,一个索引操作会在所有分片(包括复制的)都完成对文档的索引后才返回。你可以通过index API设置复制为异步的来让复制操作在后台运行。你可以直接使用这个API,也可以使用现成的客户端(比如pyes或者rsyslog的omelasticsearch),都会支持这个。

用过滤器替代请求

通常,当你搜索日志的时候,你感兴趣的是通过时间序列做排序而不是评分。这种使用场景下评分是很无关紧要的功能。所以用过滤器来查找日志比用请求更适宜。因为过滤器里不会执行评分而且可以被自动缓存。两者的更多细节参见链接

批量索引

建议使用bulk API来创建索引它比你一次给一条日志创建一次索引快多了。

主要要考虑两个事情:

  • 最佳的批量大小。它取决于很多你的设定。如果要说起始值的话,可以参考一下pyes里的默认值,即400。
  • 给批量操作设定时器。如果你添加日志到缓冲,然后等待它的大小触发限制以启动批量插入,千万确定还要有一个超时限制作为大小限制的补充。否则,如果你的日志量不大的话,你可能看到从日志发布到出现在elasticsearch里有一个巨大的延时。
06/13
2012

使用 Redis 队列及自定义 Grok 匹配解析 nginx 日志


logstash 1.2 版开始,改为推荐 Redis 队列作为中转,并沿用至今。本文重点在于展示自定义 Grok 的写法。

之前提到,用RabbitMQ作为消息队列。但是这个东西实在太过高精尖,不懂erlang不会调优的情况下,很容易挂掉——基本上我这里试验结果跑不了半小时日志传输就断了。所以改用简单易行的redis来干这个活。

之前的lib里,有inputs/redis.rb和outputs/redis.rb两个库,不过output有依赖,所以要先gem安装redis库,可以修改Gemfile,取消掉相关行的注释,搜redis即可。

然后修改agent.conf:

input {
  file {
    type => "nginx"
    path => ["/var/log/nginx/access.log" ]
  }
}

output {
  redis {
    host => "MyHome-1.domain.com"
    data_type => "channel"
    key => "nginx"
    type => "nginx"
  }
}

启动方式还是一样。

接着修改server.conf:

input {
  redis {
    host => "MyHome-1.domain.com"
    data_type => "channel"
    type => "nginx"
    key => "nginx"
  }
}

filter {
  grok {
    type => "nginx"
    pattern => "%{NGINXACCESS}"
    patterns_dir => ["/usr/local/logstash/etc/patterns"]
  }
}
output {
  elasticsearch { }
}

然后创建Grok的patterns目录,主要就是github上clone下来的那个咯~在目录下新建一个叫nginx的文件,内容如下:

NGINXURI %{URIPATH}(?:%{URIPARAM})*
NGINXACCESS \[%{HTTPDATE}\] %{NUMBER:code} %{IP:client} %{HOSTNAME} %{WORD:method} %{NGINXURI:req} %{URIPROTO}/%{NUMBER:version} %{IP:upstream}(:%{POSINT:port})? %{NUMBER:upstime} %{NUMBER:reqtime} %{NUMBER:size} "(%{URIPROTO}://%{HOST:referer}%{NGINXURI:referer}|-)" %{QS:useragent} "(%{IP:x_forwarder_for}|-)"

Grok正则的编写,可以参考wiki进行测试。

也可以不写配置文件,直接用--grok-patterns-path参数启动即可。

06/01
2012

用 RabbitMQ 和 Elasticsearch 搭建分布式日志收集存储系统


logstash 1.1 时推荐使用 RabbitMQ,其实 Ruby 的 RabbitMQ 库性能不高,这个方法随后已经被废弃。本文仅作历史架构上的参考。

上上篇讲到怎样用MRI的ruby在客户端收集日志。今天主要注意服务器端,考虑grok、elastic、web这几个功能在JRuby上才好。所以服务器端可以再开一个JRuby的进程。

  • 首先安装RabbitMQ的过程

最简单的办法,采用epel的yum,或者apt安装。其次简单的办法,从rabbitmq上下载bin的tar.gz。
这里需要注意一下,rabbitmq-server启动的时候,默认node启动在rabbit@${hostname}上。而且这个hostname不是fqdn的,是第一个主机名。比方说你的hostname是MyHome-1.mydomain.com.,那node就是rabbit@MyHome-1。这个时候很容易报Connect MyHome-1 timeout。所以/etc/hosts一定要写好。
rabbitmq-server起来之后,可以用rabbitmyctl来具体的创建user啊,vhost啊之类的东西,作为测试,我们就直接使用默认的guest用户和/了。

  • 然后安装elasticsearch的过程

这一步在logstash的docs里讲的很清楚了,就是下载tar.gz,解压然后java运行起来即可:

ES_PACKAGE=elasticsearch-0.18.7.zip
ES_DIR=${ES_PACKAGE%%.zip}
SITE=https://github.com/downloads/elasticsearch/elasticsearch
if [ ! -d "$ES_DIR" ] ; then
  wget --no-check-certificate $SITE/$ES_PACKAGE
  unzip $ES_PACKAGE
fi
  • 部署一个logstash的采集节点

和上篇所述一样,传输一个删减版的Gemfile到采集节点。然后使用bundle安装这些模块:

mkdir -p /usr/local/logstash/etc /usr/local/logstash/bin /usr/local/logstash/lib
scp ${logstashmaster}:/usr/local/logstash/Gemfile /usr/local/logstash/
scp -rf ${logstashmaster}:/usr/local/logstash/lib/* /usr/local/logstash/lib/
scp ${logstashmaster}:/usr/local/logstash/bin/logstash /usr/local/logstash/bin/
gem install bundler
cd /usr/local/logstash/
bundle install

然后编写一个使用rabbitmq的配置文件:

input {
  file {
    type => "syslog"
    path => ["/var/log/syslog.log", "/var/log/messages" ]
  } 
}

output {
  amqp {
    host => "MyHome-1"
    exchange_type => "fanout"
    name => "rawlogs"
  }
}

OK,用ruby /usr/local/logstash/bin/logstash agent -f /usr/local/logstash/etc/agent.conf启动即可。

  • 部署一个logstash的汇聚节点

这一步因为用到的模块大多是JRuby的,所以可以直接使用jar包的方式简单搞定。 编写一个使用rabbitmq和elasticsearch的配置文件:

input {
  amqp {
    type => "syslog"
    host => "MyHome-1"
    exchange => "rawlogs"
    name => "rawlogs_consumer"
  }
}

filter {
  grok {
    type => "syslog"
    pattern => "%{SYSLOG}"
  }
}
output {
  elasticsearch { }

}

这里比较讨厌的还是rabbitmq的部分。假如前面的步骤rabbitmq-server压根启动失败了,这里amqp不会返回报错说连接失败或者连接node超时什么的,而是说你试图连接一个私有的被锁定的队列……

  • 部署一个logstash的展示节点

这个节点就没必要再单开一台了,就用上面的jar包再启动一个web即可:java -jar logstash-1.1.0-monolithic.jar agent -f server.conf -- web --backend 'elasticsearch:///?local'

  • 测试

现在可以打开浏览器访问web查看了。很简单的页面,顶上一个搜索栏,中间一个按时间轴显示的柱状图,下面是具体的日志记录。点具体的某条日志,会有浮框显示该条记录的详细信息(host/date/event/message等)

下一步研究grok正则匹配的编写,然后stated实时绘图,lucene查询语法。

05/31
2012

使用 CRuby1.8 运行 logstash 的客户端程序


让 logstash 能在各大 Ruby 实现上运行是 logstash 开发者一直在追求的目标。文中介绍的是在 logstash 1.1 版本的修改办法。截至目前,logstash 仅存的 JRuby 限制就是生成 @timestamp 时需要调用 joda 库。

在一般情况下,我们实验logstash都是直接用官网上下载的jar包,然后java运行即可。但如果在大规模场景下,这样其实并不是运维的最佳实践:

  1. 并不是所有设备都默认或者很方便的可以安装java;
  2. 默认使用的JRuby执行效率比MRI的版本低一些,为了大规模运维管理,一般部署puppet的时候附加yum/apt获取的是Ruby1.8.7。

花了一点时间了解了一下代码结构,发现这点其实是可以做到的。从github上clone代码,在其中的example、bin和lib目录中都看到大量对应官网文档的input/filter/output的东东。

根据我对logstash的了解,仅保留input的file、syslog和remote_luby,filter里的grok,output里的elasticsearch和rabbitmq。然后看lib/logstash/*的具体模块,只有三个模块提到了必须使用java后台。

于是第一步,修改Gemile,只留下必备的模块。 第二步,通过bundle管理工具加载安装。 第三步,通过命令行方式指定配置变量和参数。 第四步,把所属包打包发送到其他设备测试。

现在保留的Gemfile如下:

source :rubygems

gem "cabin", "0.4.4" # for logging. apache 2 license
gem "bunny" # for amqp support, MIT-style license
gem "uuidtools" # for naming amqp queues, License ???

gem "filewatch", "0.3.3"  # for file tailing, BSD License
gem "jls-grok", "0.10.6" # for grok filter, BSD License
gem "json
gem "mail"

gem "minitest" # License: Ruby

gem "statsd-ruby", "0.3.0" # outputs/statsd, # License: As-Is

group :test do
  gem "mocha"
  gem "shoulda"
end

然后客户端的运行,这里有点小问题,默认要求必须大于Ruby1.9.2的版本才行。但是通读一遍,发现其实只是用到了Ruby1.9.2里一个全局变量RUBY_ENGINE来判断自己是不是JRuby,这个对等判断很容易修改成为RUBY_DESCRIPTION变量的正则匹配判断。之后就OK了。

具体替换代码如下:

# if RUBY_ENGINE == 'JRuby' 
  if RUBY_DESCRIPTION =~ m/^Ruby/

最后把挑好的lib/*.rb和bin/logstash、etc/logstash打包发送到其他设备。运行也没问题。写上不同的server和agent.conf启动起来一看,果然就传输过去了。

目前就到这步,随后随时更新