Elasticsearch学习(一)集群red状态的处理

今天刚刚搭建好公司的日志收集系统,晚上的时候根据Kafka的生产和消费速度情况适当的调节了一下Logstash和ES的配置,做了一些配置的优化。但是没过多久居然出现了集群red状态的情况。突然感觉好慌张,通过head插件看了一下当前集群的问题,发现有两个索引的shared是无法分配的。

1
2
3
4
5
# 查看集群的索引情况
curl -XGET 'http://localhost:9200/_cluster/health?level=indices&pretty'
# 查看集群的分片情况
curl -XGET 'http://localhost:9200/_cluster/health?level=shards&pretty'

根据ES的集群API查看一下索引和分片的健康情况
以前遇到这种情况通常手动做reroute操作就可以分配了,但是这次的情况有些特殊。

1
2
3
4
5
6
7
8
9
10
curl -XPOST 'localhost:9200/_cluster/reroute?pretty' -d '{
"commands" : [ {
"allocate" : {
"index" : "node_access_logs_index_2016.12.21",
"shard" : 0,
"node" : "node-1",
"allow_primary" : true
}
}]
}'

尝试了reroute操作后,报错如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"state" : "INITIALIZING",
"primary" : true,
"node" : "MsryV3-fTOCdgolwpxd0_w",
"relocating_node" : null,
"shard" : 0,
"index" : "node_track_logs_index_2016.12.21",
"version" : 3,
"allocation_id" : {
"id" : "lOUerCpGQTWcX0grHWLr0Q"
},
"unassigned_info" : {
"reason" : "INDEX_CREATED",
"at" : "2016-12-22T13:03:08.686Z",
"details" : "force allocation from previous reason ALLOCATION_FAILED, failed to create shard, failure ElasticsearchException[failed to create shard]; nested: ElasticsearchParseException[Failed to parse setting [index.refresh_interval] with value [10] as a time value: unit is missing or unrecognized]; "
}
}

通过上面的错误信息,我们能够看出来其实这个问题是我马虎造成的,配置我们的Logstash创建索引的Template模板参数错误没有带时间单位,正确的配置应该是”index.refresh_interval”: “10s”,我之前的配置少了单位秒,所以ES重新分片的时候会报错[Failed to parse setting [index.refresh_interval] with value [10] as a time value: unit is missing or unrecognized]

知道问题的原因之后,我尝试直接修改线上索引Mapping的setting设置,将”index.refresh_interval”: “10s”的单位加上,发现报错如下,提示是当前索引的primary shared不可用,所以无法修改Mapping。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl -XPOST 'localhost:9200/node_track_logs_index_2016.12.21/_settings?pretty' -d'
{
"index.number_of_replicas": "1",
"index.number_of_shards": "5",
"index.refresh_interval": "10s"
}'
{
"error" : {
"root_cause" : [ {
"type" : "unavailable_shards_exception",
"reason" : "[node_track_logs_index_2016.12.21][0] primary shard is not active Timeout: [1m], request: [index {[node_track_logs_index_2016.12.21]
[_settings][AVkmqvF1jCuNyx9kvqWT], source[\n{\n \"index.number_of_replicas\": \"1\",\n \"index.number_of_shards\": \"5\",\n \"index.refresh_interval\": \"10s\"\n}]}]"
} ],
"type" : "unavailable_shards_exception",
"reason" : "[node_track_logs_index_2016.12.21][0] primary shard is not active Timeout: [1m], request: [index {[node_track_logs_index_2016.12.21][_settings][AVkmqvF1jCuNyx9kvqWT], source[\n{\n \"index.number_of_replicas\": \"1\",\n \"index.number_of_shards\": \"5\",\n \"index.refresh_interval\": \"10s\"\n}]}]"
},
"status" : 503
}

后来仔细想了一下我们这个Template是在Logstash按天创建索引的的时候才会使用的,所以我们unassign的node_track_logs_index_2016.12.21和node_proxy_logs_index_2016.12.21索引其实是正好需要按天创建新索引创建的,所以直接删除掉该索引,重启Logstash按照新的Template创建索引的Mapping就好了。

参考文章:

Logstash学习(六)elasticsearch插件——设置ES的Template

我们使用ElasticSearch时一般需要自己创建ElasticSearch的索引的Mapping,当索引非常多的时候,可能需要配置一个索引模板Template来对类似的索引做统一配置,让索引模板Template中配置匹配索引的规则,来确定该Template会被应用到哪些索引上。

当Logstash在整合ElasticSearch的时候,会有下面三种方式的Template配置:

Template配置方式

1. 使用ElasticSearch默认自带的索引模板

ElasticSearch默认自带了一个名字为”logstash”的模板,默认应用于Logstash写入数据到ElasticSearch使用

  • 优点:最简单,无须任何配置
  • 缺点:无法自定义一些配置,例如:分词方式
2. 在Logstash Indexer端自定义配置索引模板

Logstash的output插件中使用template指定本机器上的一个模板json文件路径,可以在json文件中设置对应的Template模板信息。例如:template => “/tmp/logstash.json”

  • 优点:配置简单
  • 缺点:因为分散在Logstash Indexer机器上,维护起来比较麻烦
3. 在ElasticSearch服务端自定义配置索引模板

由ElasticSearch负责加载模板。这种方式需要在ElasticSearch的集群中的config/templates路径下配置模板json。而且ElasticSearch提供了Restful API接口维护索引模板信息。

  • 优点:维护比较容易,可动态更改,全局生效。
  • 缺点:需要注意模板的命名规则,比较容易通过看Template名字就能够确定模板应用到哪些索引

这里可能使用第三种方式统一管理Template最好,推荐使用第三种方式,但是具体问题具体分析。例如我现在的场景就使用的第二种方式,因为我们的Logstash Indexer和ElasticSearch只有一台服务器,所以在Logstash Indexer端维护Template文件也可以。

模板类型

ElasticSearch的模板类型主要由两种:静态模板和动态模板

静态模板

适合索引字段数据固定的场景,一旦配置完成,不能向里面加入多余的字段,否则会报错

  • 优点:scheam已知,业务场景明确,不容易出现因字段随便映射从而造成元数据撑爆es内存,从而导致es集群全部宕机
  • 缺点:字段数多的情况下配置稍繁琐,针对于每个索引可能需要的模板都不同,很有可能需要配置很多个模板
动态模板

适合字段数不明确,大量字段的配置类型相同的场景,可以按照类型规则动态添加新字段,新加字段不会报错。主要需要配置”dynamic_templates”

  • 优点:可动态添加任意字段,无须改动schema
  • 缺点:无标准schema导致数据不规则,如果添加的字段非常多,有可能造成ES集群宕机

需要注意:模板在设置生效后,仅对ES集群中新建立的索引生效,而对已存在的索引及时索引名满足模板的匹配规则,也不会生效,因此如果需要改变现有索引的Mapping信息,仍需要在正确的Mapping基础上建立新的索引,并将数据从原索引拷贝至新索引,变更新索引别名为原索引这种方式来实现。

模板结构

模板的结构大致分四部分:

第一部分:通用设置,主要是模板匹配索引的过滤规则,影响该模板对哪些索引生效;

第二部分:settings:配置索引的公共参数,比如索引的replicas,以及分片数shards等参数;

第三部分:mappings:最重要的一部分,在这部分中配置每个type下的每个field的相关属性,比如field类型(string,long,date等等),是否分词,是否在内存中缓存等等属性都在这部分配置;

第四部分:aliases:索引别名,索引别名可用在索引数据迁移等用途上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
{
"logstash" : {
"order" : 0,
"template" : "logstash-*",
"settings" : {
"index" : {
"refresh_interval" : "5s"
}
},
"mappings" : {
"_default_" : {
"dynamic_templates" : [ {
"message_field" : {
"mapping" : {
"fielddata" : {
"format" : "disabled"
},
"index" : "analyzed",
"omit_norms" : true,
"type" : "string"
},
"match_mapping_type" : "string",
"match" : "message"
}
}, {
"string_fields" : {
"mapping" : {
"fielddata" : {
"format" : "disabled"
},
"index" : "analyzed",
"omit_norms" : true,
"type" : "string",
"fields" : {
"raw" : {
"ignore_above" : 256,
"index" : "not_analyzed",
"type" : "string"
}
}
},
"match_mapping_type" : "string",
"match" : "*"
}
} ],
"_all" : {
"omit_norms" : true,
"enabled" : true
},
"properties" : {
"@timestamp" : {
"type" : "date"
},
"geoip" : {
"dynamic" : true,
"properties" : {
"ip" : {
"type" : "ip"
},
"latitude" : {
"type" : "float"
},
"location" : {
"type" : "geo_point"
},
"longitude" : {
"type" : "float"
}
}
},
"@version" : {
"index" : "not_analyzed",
"type" : "string"
}
}
}
},
"aliases" : { }
}
}

总结:

定制索引模板,是搜索业务中一项比较重要的步骤,需要注意的地方有很多,比如:

  • 字段数固定吗
  • 字段类型是什么
  • 分不分词
  • 索引不索引
  • 存储不存储
  • 排不排序
  • 是否加权

除了这些还有其他的一些因素,比如,词库的维护改动,搜索架构的变化等等。如果前提没有充分的规划好,后期改变的话,改动其中任何一项,都需要重建索引,这个代价是非常大和耗时的,尤其是在一些数据量大的场景中。

Logstash整合ElasticSearch模板实例

首先我们通过ElasticSearch的Restful API接口查询一下ElasticSearch中一共创建了多少个索引模板,默认情况下ElasticSearch应该会有一个名字为”logstash”的Template,这个Template匹配了所有”logstash-*”的索引,也就是说所有以”logstash-“开头的索引都默认使用了这个”logstash”的Template。这个Template就是一个动态模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
curl -XGET localhost:9200/_template?pretty
{
"logstash" : {
"order" : 0,
"template" : "logstash-*",
"settings" : {
"index" : {
"refresh_interval" : "5s"
}
},
"mappings" : {
"_default_" : {
"dynamic_templates" : [ {
"message_field" : {
"mapping" : {
"fielddata" : {
"format" : "disabled"
},
"index" : "analyzed",
"omit_norms" : true,
"type" : "string"
},
"match_mapping_type" : "string",
"match" : "message"
}
}, {
"string_fields" : {
"mapping" : {
"fielddata" : {
"format" : "disabled"
},
"index" : "analyzed",
"omit_norms" : true,
"type" : "string",
"fields" : {
"raw" : {
"ignore_above" : 256,
"index" : "not_analyzed",
"type" : "string"
}
}
},
"match_mapping_type" : "string",
"match" : "*"
}
} ],
"_all" : {
"omit_norms" : true,
"enabled" : true
},
"properties" : {
"@timestamp" : {
"type" : "date"
},
"geoip" : {
"dynamic" : true,
"properties" : {
"ip" : {
"type" : "ip"
},
"latitude" : {
"type" : "float"
},
"location" : {
"type" : "geo_point"
},
"longitude" : {
"type" : "float"
}
}
},
"@version" : {
"index" : "not_analyzed",
"type" : "string"
}
}
}
},
"aliases" : { }
}
}

我们创建一个自定义Template动态模板,这个模板指定匹配所有以”go_logsindex“开始的索引,并且指定允许添加新字段,匹配所有string类型的新字段会创建一个raw的嵌套字段,这个raw嵌套字段类型也是string,但是是not_analyzed不分词的(主要用于解决一些analyzed的string字段无法做统计,但可以使用这个raw嵌套字段做统计)

go_logs_template.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
{
"template": "go_logs_index_*",
"order":0,
"settings": {
"index.number_of_replicas": "1",
"index.number_of_shards": "5",
"index.refresh_interval" : "10s"
},
"mappings": {
"_default_": {
"_all": {
"enabled": false
},
"dynamic_templates": [
{
"my_template": {
"match_mapping_type": "string",
"mapping": {
"type": "string",
"fields": {
"raw": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
]
},
"go": {
"properties": {
"timestamp": {
"type": "string",
"index": "not_analyzed"
},
"msg": {
"type": "string",
"analyzer": "ik",
"search_analyzer": "ik_smart"
},
"file": {
"type": "string",
"index": "not_analyzed"
},
"line": {
"type": "string",
"index": "not_analyzed"
},
"threadid": {
"type": "string",
"index": "not_analyzed"
},
"info": {
"type": "string",
"index": "not_analyzed"
},
"type": {
"type": "string",
"index": "not_analyzed"
},
"@timestamp": {
"format": "strict_date_optional_time||epoch_millis",
"type": "date"
},
"@version": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}

接下来我们看看Logstash是如何使用Template的。首先我们需要准备好Logstash要使用的Template文件,其实这里我们也可以在ES直接创建好Template,然后都在ES维护Template,就是前面说的推荐的第三种方式,但是对于我们单个Logstash Indexer和ElasticSearch节点,方式二和方式三配置都很简单。

在Logstash的配置文件中添加template相关的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
output {
stdout {
codec => rubydebug
}
elasticsearch {
codec => "json"
hosts => ["hadoop1:9200", "hadoop2:9200", "hadoop3:9200"]
index => "go_logs_index_%{+YYYY.MM.dd}"
document_type => "%{type}"
template => "/usr/local/elasticsearch/template/go_logs_template.json"
template_name => "go_logs_template"
template_overwrite => true
workers => 1
flush_size => 20000
idle_flush_time => 10
}
}
# 配置说明:
# template : 指定template模板文件
# template_name : 指定在ES中创建template的名称,默认是logstash
# template_overwrite : 是否覆盖ES中的template,默认是false

这样我们使用Logstash创建的索引”go_logsindex%{+YYYY.MM.dd}”就会匹配到我们的Template,就会使用Template中的配置,所以使用Template之后就不需要每新创建一个索引就自己手动创建Mapping了,可以直接使用Template为一类的索引创建默认Mapping配置。

参考文章:

Logstash学习(五)Logstash的file插件使用技巧

最近在使用Logstash做Shipper端收集go,node,php多种日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
input {
file {
path => ["/home/yunyu/Downloads/*.log.*", "/home/yunyu/Downloads/lumen.log"]
codec => "plain"
# 多个日志文件的offset信息都会记录到这个sincedb文件中,会记录成多行
sincedb_path => "/data/logstash_sincedb/php/.sincedb_php"
start_position => "beginning"
# 设置是否忽略太旧的日志的
# 如果没设置该属性可能会导致读取不到文件内容,因为我们的日志大部分是好几个月前的,所以这里设置为不忽略
ignore_older => 0
}
}
output {
stdout {
codec => rubydebug
}
kafka {
# 指定Kafka集群地址
bootstrap_servers => "hadoop1:9092,hadoop2:9092,hadoop3:9092"
# 指定Kafka的Topic
topic_id => "php_log"
}
}
# path : 指定要采集的日志文件路径,多个日志文件可以使用通配符,也可以使用数组
# sincedb_path : sincedb文件是用于存储Logstash读取文件的位置,每行表示一个文件,每行有两个数字,第一个表示文件的inode,第二个表示文件读取到的位置(byteoffset),默认为$HOME/.sincedb*,文件名是日志文件路径MD5加密后的结果。sincedb_path只能指定为具体的file文件,不能是path目录。
# sincedb_write_interval : Logstash每隔多久写一次sincedb文件,默认是15秒。
# ignore_older : 在每次检查文件列表的时候,如果一个文件的最后修改时间超过这个值,就忽略这个文件。默认是86400秒,即一天。
# start_position : Logstash从什么位置开始读取文件数据,默认是结束位置,也就是说Logstash进程会以类似tail -F的形式运行。如果你是要导入原有数据,把这个设定改成"beginning",logstash进程就从头开始读取。

这里php日志文件按照类型拆分成多个日志文件,这些日志文件都需要收集,所以需要我们在Logstash配置path使用了通配符来处理读取多个日志文件。这里和之前读取go和node日志不同,go和node的日志通常只有一个日志文件,这里php的日志文件按照类型分别写入到不同的日志文件,那我们指定的sincedb_path又只能指定一个file而不是path,那如何记录多个文件的读取进度呢?我在日志系统收集的过程中特意查看了一下sincedb文件,发现是如果Logstash file path指定了读取多个文件,这样sincedb文件就会存储多行,每行代表一个日志文件的读取进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# cat /data/logstash_sincedb/php/.sincedb_php
1058198 0 51713 976040
1058229 0 51713 151513
1056857 0 51713 970977345
1058215 0 51713 115587
1057737 0 51713 873729227
1058197 0 51713 1108533
1058230 0 51713 595155
1058110 0 51713 1085851
1048702 0 51713 10036
1057753 0 51713 206711384
1057777 0 51713 1607939699
1052414 0 51713 359573
1056858 0 51713 1015625306
1058201 0 51713 341179
1058233 0 51713 449755
1048838 0 51713 90406
1058196 0 51713 234060588
# 这四列的内容分别是:inode, major number, minor number, pos
# 可以参考文章:
# http://unix.stackexchange.com/questions/73988/linux-major-and-minor-device-numbers

参考文章:

Logstash学习(四)Logstash的Grok使用技巧

最近在使用Logstash解析日志的时候遇到了很多问题,需要处理多种特殊情况,下面会列举一下我遇到的特殊情况以及处理方式。

如何检查数组是否为空数组

1
2
3
4
5
if [tags] == [] {
mutate {
remove_field => ["tags"]
}
}

判断json中是否有foo这个属性

1
2
3
4
5
6
7
8
9
10
11
if [foo] {
...
} else {
drop {}
}
if ![foo] {
drop {}
} else {
...
}
1
2
3
4
5
6
7
8
9
ruby {
code => "
if event['column16'] == nil
event['log_type'] = 'type2'
else
event['log_type'] = 'type1'
end
"
}
1
2
3
4
5
if "_jsonparsefailure" in [tags] {
drop {}
} else {
...
}

参考文章:

Kafka学习(四)指定Kafka的data和logs路径

环境说明

  • zookeeper-3.4.8
  • kafka_2.11-0.9.0.0

最近公司在做日志收集系统,主要基于Elastic Stack做的,其中用到了Kafka服务器作缓冲,今天主要说的问题就是因为上了日志收集系统之后,大量的线上日志写入到Kafka,导致Kafka磁盘写满。后来发现是因为当时运维安装Kafka环境的时候将Kafka的data和logs都挂在了系统盘下,而且系统盘容量很小只有20G,所以出现了磁盘不够用的情况。

解决办法也很简单,将Kafka的data和logs分别写到挂在的数据磁盘即可。这里需要简单修改一下Kafka的配置文件。

修改Kafka的data目录

Kafka的data目录是存储Kafka的数据文件的目录,是在${KAFKA_HOME}/config/server.properties中修改

1
log.dirs=/data/kafka_data

注意:log.dirs可以配置多个目录,需要用逗号分隔开

修改Kafka的logs目录

Kafka运行的时候都会通过log4j打印很多日志文件,如:server.log, controller.log, state-change.log等,默认都会将其输出到${KAFKA_HOME}/logs目录下,这样很不利于线上运维,因为经常容易出现写满文件系统(尤其是挂在到系统盘的情况),所以一般运维都会建议将Kafka(或者其他环境)都安装在/usr/local系统盘下(方便clone系统镜像),一般系统盘都比较小,而数据和日志会指定到另一个或多个更大空间的挂在数据盘。

修改Kafka的logs目录是在${KAFKA_HOME}/bin/kafka-run-class.sh中修改

1
2
3
4
5
# Log directory to use
if [ "x$LOG_DIR" = "x" ]; then
# LOG_DIR="$base_dir/logs"
LOG_DIR="/data/kafka_logs"
fi

Kibana学习(四)时区差8小时问题

Kibana连接ES查询数据的时候,会有时差8小时的问题。先来描述一下问题的具体情况,我们先来看看Logstash默认写入到ES的索引数据。timestamp是我们App上报日志的时间戳字段,这个字段是客户端写入日志的时间。@timestamp是使用Logstash写入ES的时候默认自带的时间戳(即Logstash生成ES索引的时间戳)。这里我们的这条日志是2016-10-08由App记录的,由Logstash收集到ES服务器的时间是2016-12-05。但是我们发现@timestamp并不是我们系统的当前时间,而是比我们当前的系统时间小了8小时,这就是我们想要解决的8小时时差问题。

原因是Logstash中默认插入的@timestamp时间字段是按照UTC 00:00标准时区的时间转换成long型的时间戳保存在ES中,而我们系统的时间是中国时区UTC +08:00,由ES查询出来的long型的时间戳再按照UTC +08:00转换成时间就正好相差了8小时。因为@timestamp的long型字段在ES中是不可取回的,所以我们在ES的返回值中是看不到这个long型字段的,只能看到@timestamp根据我们的时区转换后的结果”2016-12-05T11:30:40.911Z”,这个结果正好和我们的系统时间相差了8小时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"_index": "api_logs_index_1",
"_type": "command",
"_id": "AVjOwAt_jL-eD07Ygku1",
"_version": 1,
"_score": 1,
"_source": {
"logs": {
"timestamp": "1475912701768",
"rpid": "65351516503932930",
"name": "rp.hb.ad.click_ad",
"bid": 0,
"uid": 0,
"did": 0,
"duid": 0,
"hb_uid": 0,
"ua": "",
"device_id": "",
"server_timestamp": 1475912715001
},
"level": "info",
"message": "logs",
"timestamp": "2016-10-08T07:45:15.001Z",
"@version": "1",
"@timestamp": "2016-12-05T11:30:40.911Z",
"path": "/home/yunyu/Downloads/track.log",
"host": "hadoop1",
"type": "command"
}
}

上面简单的描述了一下问题。我们需要解决下面两个问题:

  1. 如何让Kibana查询和统计使用的是App写入日志的时间,而不是Logstash写入ES的时间(因为Logstash写入ES时间上会有延迟)
  2. 解决时差相差8小时问题

一般我们App上报日志都会带有一个timestamp时间戳字段,这个字段是客户端写入日志的时间。当Logstash收集App上报日志的时候,会将timestamp字段保存到ES中,Kibana通过该字段当做统计的字段进行各种按日期统计的查询。

这里Kibana要求所有ES的索引必须要有一个时间字段作为统计查询的日期字段使用,如果没有ES的索引没有时间或者日期字段,是无法在Kibana中创建索引的。所以默认Kibana也会给一个默认的时间字段@timestamp,这样当我们在Kibana创建api_logs_index索引的时候,就会出现有两个时间字段,一个是timestamp,一个是@timestamp。

Kibana

在Kibana创建索引时,设置索引使用@timestamp字段,但是需要在Logstash中配置@timestamp的值从timestamp取出来的。Logstash中可以指定@timestamp字段的值是从App上报日志的timestamp字段来的。

1
2
3
4
date {
match => ["timestamp", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]
target => "@timestamp"
}

下面用我们这条日志举例,下面的表格是系统时间和UTC时间戳根据北京时区的转换对照表。这里的系统时间转换程UTC时间戳是带有北京时区的。

系统时间 UTC时间戳 ES返回(显示)的时间 ES存储的时间(long)
2016/10/18 15:45:15.001Z 1475912715001 timestamp : 1475912715001
2016/10/18 7:45:15.001Z 1475883915001 timestamp : 2016/10/18 7:45:15.001Z @timestamp : 1475883915001
2016/10/17 23:45:15.001Z @timestamp : 2016/10/17 23:45:15.001Z

Logstash将timestamp的时间2016/10/18 7:45:15.001Z按照默认的标准时区UTC 00:00将timestamp转换成long类型1475912715001存储到ES,而对于@timestamp字段的值,是将timestamp的时间2016/10/18 7:45:15.001Z按照北京时区UTC +08:00将timestamp转换成long类型1475883915001并且赋值给@timestamp并且存储到ES。(为什么要加上北京时区UTC +08:00的原因不清楚,也尝试过不带有Z时区设置的时间格式,转换成的时间戳结果一样)

1
2
3
4
date {
match => ["timestamp", "yyyy-MM-dd HH:mm:ss"]
target => "@timestamp"
}

ES存储的时间long型到ES返回(显示)时间是按照UTC 00:00标准时间转换的。下面是取回的结果。

  • ES将timestamp的long类型1475912715001按照UTC 00:00标准时间转换回时间2016/10/18 7:45:15.001Z
  • ES将@timestamp的long类型1475883915001按照UTC 00:00标准时间转换回时间2016-10-07T23:45:15.001Z
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"_index": "api_logs_index_1",
"_type": "command",
"_id": "AVjOwAt_jL-eD07Ygku1",
"_version": 1,
"_score": 1,
"_source": {
"logs": {
"timestamp": "1475912701768",
"rpid": "65351516503932930",
"name": "rp.hb.ad.click_ad",
"bid": 0,
"uid": 0,
"did": 0,
"duid": 0,
"hb_uid": 0,
"ua": "",
"device_id": "",
"server_timestamp": 1475912715001
},
"level": "info",
"message": "logs",
"timestamp": "2016-10-08T07:45:15.001Z",
"@version": "1",
"@timestamp": "2016-10-07T23:45:15.001Z",
"path": "/home/yunyu/Downloads/track.log",
"host": "hadoop1",
"type": "command"
}
}

这里可以看到Kibana的查询Request,下面是我选择的时间区间条件

  • 2016/10/8 7:30:0:按照北京时区UTC +08:00转换为1475883000000
  • 2016/10/8 8:0:0:按照北京时区UTC +08:00转换为1475884800000

Kibana Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
{
"size": 500,
"sort": [
{
"@timestamp": {
"order": "desc",
"unmapped_type": "boolean"
}
}
],
"query": {
"filtered": {
"query": {
"query_string": {
"analyze_wildcard": true,
"query": "*"
}
},
"filter": {
"bool": {
"must": [
{
"range": {
"@timestamp": {
"gte": 1475883000000,
"lte": 1475884800000,
"format": "epoch_millis"
}
}
}
],
"must_not": []
}
}
}
},
"highlight": {
"pre_tags": [
"@kibana-highlighted-field@"
],
"post_tags": [
"@/kibana-highlighted-field@"
],
"fields": {
"*": {}
},
"require_field_match": false,
"fragment_size": 2147483647
},
"aggs": {
"2": {
"date_histogram": {
"field": "@timestamp",
"interval": "30s",
"time_zone": "Asia/Shanghai",
"min_doc_count": 0,
"extended_bounds": {
"min": 1475883000000,
"max": 1475884800000
}
}
}
},
"fields": [
"*",
"_source"
],
"script_fields": {},
"fielddata_fields": [
"timestamp",
"@timestamp"
]
}

上面的Request可以看出来我们Kibana Request是将查询时间区间的条件先按照Browser的时区(北京时区UTC +08:00)转换成long型时间戳,然后当成条件查询的@timestamp字段。

这里的fields就是ES存储的long类型数值。

  • timestamp 1475912715001:按照标准时区UTC +00:00转换为2016/10/8 7:45:15
  • @timestamp 1475883915001:按照标准时区UTC +00:00转换为2016/10/7 23:45:15
1
2
3
4
5
6
7
8
"fields": {
"timestamp": [
1475912715001
],
"@timestamp": [
1475883915001
]
}

Kibana Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
{
"took": 7,
"hits": {
"hits": [
{
"_index": "api_logs_index_1",
"_type": "command",
"_id": "AVjPhBGGUk8QUkLwTRVu",
"_score": null,
"_source": {
"logs": {
"timestamp": "1475912701768",
"rpid": "65351516503932930",
"name": "rp.hb.ad.click_ad",
"bid": 0,
"uid": 0,
"did": 0,
"duid": 0,
"hb_uid": 0,
"ua": "",
"device_id": "",
"server_timestamp": 1475912715001
},
"level": "info",
"message": "logs",
"timestamp": "2016-10-08T07:45:15.001Z",
"@version": "1",
"@timestamp": "2016-10-07T23:45:15.001Z",
"path": "/home/yunyu/Downloads/track.log",
"host": "hadoop1",
"type": "command"
},
"fields": {
"timestamp": [
1475912715001
],
"@timestamp": [
1475883915001
]
},
"sort": [
1475883915001
]
}
],
"total": 1,
"max_score": 0
},
"aggregations": {
"2": {
"buckets": [
{
"key_as_string": "2016-10-08T07:30:00.000+08:00",
"key": 1475883000000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:30:30.000+08:00",
"key": 1475883030000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:31:00.000+08:00",
"key": 1475883060000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:31:30.000+08:00",
"key": 1475883090000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:32:00.000+08:00",
"key": 1475883120000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:32:30.000+08:00",
"key": 1475883150000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:33:00.000+08:00",
"key": 1475883180000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:33:30.000+08:00",
"key": 1475883210000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:34:00.000+08:00",
"key": 1475883240000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:34:30.000+08:00",
"key": 1475883270000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:35:00.000+08:00",
"key": 1475883300000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:35:30.000+08:00",
"key": 1475883330000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:36:00.000+08:00",
"key": 1475883360000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:36:30.000+08:00",
"key": 1475883390000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:37:00.000+08:00",
"key": 1475883420000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:37:30.000+08:00",
"key": 1475883450000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:38:00.000+08:00",
"key": 1475883480000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:38:30.000+08:00",
"key": 1475883510000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:39:00.000+08:00",
"key": 1475883540000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:39:30.000+08:00",
"key": 1475883570000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:40:00.000+08:00",
"key": 1475883600000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:40:30.000+08:00",
"key": 1475883630000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:41:00.000+08:00",
"key": 1475883660000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:41:30.000+08:00",
"key": 1475883690000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:42:00.000+08:00",
"key": 1475883720000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:42:30.000+08:00",
"key": 1475883750000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:43:00.000+08:00",
"key": 1475883780000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:43:30.000+08:00",
"key": 1475883810000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:44:00.000+08:00",
"key": 1475883840000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:44:30.000+08:00",
"key": 1475883870000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:45:00.000+08:00",
"key": 1475883900000,
"doc_count": 1
},
{
"key_as_string": "2016-10-08T07:45:30.000+08:00",
"key": 1475883930000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:46:00.000+08:00",
"key": 1475883960000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:46:30.000+08:00",
"key": 1475883990000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:47:00.000+08:00",
"key": 1475884020000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:47:30.000+08:00",
"key": 1475884050000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:48:00.000+08:00",
"key": 1475884080000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:48:30.000+08:00",
"key": 1475884110000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:49:00.000+08:00",
"key": 1475884140000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:49:30.000+08:00",
"key": 1475884170000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:50:00.000+08:00",
"key": 1475884200000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:50:30.000+08:00",
"key": 1475884230000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:51:00.000+08:00",
"key": 1475884260000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:51:30.000+08:00",
"key": 1475884290000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:52:00.000+08:00",
"key": 1475884320000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:52:30.000+08:00",
"key": 1475884350000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:53:00.000+08:00",
"key": 1475884380000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:53:30.000+08:00",
"key": 1475884410000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:54:00.000+08:00",
"key": 1475884440000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:54:30.000+08:00",
"key": 1475884470000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:55:00.000+08:00",
"key": 1475884500000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:55:30.000+08:00",
"key": 1475884530000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:56:00.000+08:00",
"key": 1475884560000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:56:30.000+08:00",
"key": 1475884590000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:57:00.000+08:00",
"key": 1475884620000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:57:30.000+08:00",
"key": 1475884650000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:58:00.000+08:00",
"key": 1475884680000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:58:30.000+08:00",
"key": 1475884710000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:59:00.000+08:00",
"key": 1475884740000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T07:59:30.000+08:00",
"key": 1475884770000,
"doc_count": 0
},
{
"key_as_string": "2016-10-08T08:00:00.000+08:00",
"key": 1475884800000,
"doc_count": 0
}
]
}
}
}

ES返回的结果时间是按照标准时区UTC +00:00转换的。这里Kibana还会根据创建索引所选择的时间戳,再将@timestamp的结果转换成Browser默认的时区(即:UTC +08:00)显示出来,也就是在ES返回的@timestamp时间上再加8小时。

如果timestamp字段没有带有时区设置,我们可以在Logstash中指定locale和timezone属性来设置timestamp的字段的时区,这样就知道timestamp的时间应该按照指定的timezone时区解析成long型时间戳,这样my_timestamp字段在ES中是一个使用+08:00中国时区的long类型时间戳,方便使用ES API直接查询timestamp字段,因为一般人都使用+08:00中国时区将时间转换为long类型的时间戳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
date {
match => ["timestamp", "yyyy-MM-dd HH:mm:ss"]
target => "my_timestamp"
locale => "en"
timezone => "+08:00"
}
date {
# 如果日志中的timestamp时间戳带有时区,而且是UTC标准时区,不是中国时区,我们可以使用指定timezone来转换成long类型的@timestamp
match => ["timestamp", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]
target => "@timestamp"
locale => "en"
timezone => "+00:00"
}

Logstash创建索引所使用的YYYY-MM-DD也是使用的@timestamp字段的时间

参考文章:

Go学习(一)开发环境搭建

我这里是Mac系统,不同的操作系统安装略有不同,请知晓

Go安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 我这里直接使用homebrew安装Go,非常的简单
$ brew install go
# 安装成功后,查看Go的环境
$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH=""
GORACE=""
GOROOT="/usr/local/Cellar/go/1.7.3/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.7.3/libexec/pkg/tool/darwin_amd64"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/0h/jtjrr7g95mv2pt4ts1tgmzyh0000gn/T/go-build273829653=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"

配置环境变量

1
2
3
4
5
GOPATH=/Users/yunyu/workspace_go
GOROOT=/usr/local/Cellar/go/1.7.3/libexec
export GOPATH
export GOROOT

在我们以前熟悉的各种语言中都有这样几个概念:系统路径,官方包路径,第三方包路径,项目路径。

Go中只有两个路径:

  • GOROOT: Go的安装路径,官方包路径根据这个设置自动匹配
  • GOPATH: 工作路径(其实不应该用中文翻译解释,直接说GOPATH更合适)

问题:项目路径和第三方包路径呢? 首先:Go中是没有项目这个概念的,只有包。可执行包只是特殊的一种,类似我们常说的项目GOPATH可以设置多个,不管是可执行包,还是非可执行包,通通都应该在某个$GOPATH/src下。

$GOPATH可以包含多个工作目录,取决于你的个人情况。如果你设置了多个工作目录,那么当你在之后使用 go get(远程包安装命令)时远程包将会被安装在第一个目录下。

以上都可以在Go对于gopath和importpath的help使用说明中查看到

1
2
$ go help gopath
$ go help importpath

Go的目录结构

以上$GOPATH目录约定有三个子目录:

  1. src 存放源代码(比如:.go .c .h .s等)。src 子目录通常包会含多种版本控制的代码仓库(例如Git或Mercurial), 以此来跟踪一个或多个源码包的开发。
  2. pkg 编译后生成的包文件(比如:.a)
  3. bin 编译后生成的可执行文件

编写helloworld.go

helloworld.go
1
2
3
4
5
6
7
8
package main
//引入fmt库
import "fmt"
func main() {
fmt.Println("Hello World!")
}

程序入口点(entry point)和包(package)

Go保持了与C家族语言一致的风格:即目标为可执行程序的Go源码中务必要有一个名为main的函数,该函数即为可执行程序的入口点。除此之外Go还增加了一个约束:作为入口点的main函数必须在名为main的package中。正如上面helloworld.go源文件中的那样,在源码第一行就声明了该文件所归属的package为main。

Go去除了头文件的概念,而借鉴了很多主流语言都采用的package的源码组织方式。package是个逻辑概念,与文件没有一一对应的关系。 如果多个源文件都在开头声明自己属于某个名为foo的包,那这些源文件中的代码在逻辑上都归属于包foo(这些文件最好在同一个目录下,至少目前的Go版本还无法支持不同目录下的源文件归属于同一个包)。

我们看到helloworld.go中import一个名为fmt的包,并利用该包内的Println函数输出”Hello World!”。直觉告诉我们fmt包似乎是一个标准库中的包。没错,fmt包提供了格式化文本输出以及读取格式化输入的相关函数,与C中的printf或scanf等类似。我们通过import语句将fmt包导入我们的源文件后就可以使用该fmt包导出(export)的功能函数了(比如 Printf)。

在C中,我们通过static来标识局部函数还是全局函数。而在Go中,包中的函数是否可以被外部调用,要看该函数名的首母是否为大写。这是一种Go语言固化的约定:首母大写的函数被认为是导出的函数,可以被包之外的代码调用;而小写字母开头的函数则仅能在包内使用。在例子中你也看到了fmt包的Println函数其首母就是大写的。

hello.go
1
2
3
4
5
6
7
package hello
import "fmt"
func Hello() {
fmt.Println("Hello World!")
}
main.go
1
2
3
4
5
6
7
8
9
package main
import (
"hello"
)
func main() {
hello.Hello()
}

用go build编译main.go,结果如下:

1
2
3
4
5
6
7
8
9
10
# 执行run或者build都会报错
$ go run main.go
main.go:4:5: cannot find package "hello" in any of:
/usr/local/Cellar/go/1.7.3/libexec/src/hello (from $GOROOT)
/Users/yunyu/workspace_go/src/hello (from $GOPATH)
$ go build main.go
main.go:4:5: cannot find package "hello" in any of:
/usr/local/Cellar/go/1.7.3/libexec/src/hello (from $GOROOT)
/Users/yunyu/workspace_go/src/hello (from $GOPATH)

编译器居然提示无法找到hello这个package,而hello.go中明明定义了package hello了。这是怎么回事呢?原来go compiler搜索package的方式与我们常规理解的有不同,Go在这方面也有一套约定,这里面涉及到一个重要的环境变量:GOPATH。我们可以使用go help gopath来查看一下有关gopath的manual。

Go compiler的package搜索顺序是这样的,以搜索hello这个package为例:

  • 首先,Go compiler会在GO安装目录(GOROOT,这里是Mac环境安装路径是/usr/local/Cellar/go/1.7.3/libexec,如果是Linux安装目录可能是/usr/local/go)下查找是否有src/pkg/hello相关包源码;如果没有则继续;
  • 如果export GOPATH=PATH1:PAHT2,则Go compiler会依次查找是否存在PATH1/src/hello、PATH2/src/hello;配置在GOPATH中的PATH1和PATH2被称作workplace(类似Java项目中的project);
  • 如果在上述几个位置均无法找到hello这个package,则提示出错。

在本例子中,我设置的GOPATH环境变量有问题,之前把GOPATH理解成了Java中的workspace了,也没有建立类似PATH1/src/hello(workspace_go/src/hello)这样的路径,因此Go compiler显然无法找到hello这个package了。我们来修改一下GOPATH变量并建立相关目录:

修改环境变量如下:

/etc/profile或者~/.bash_profile
1
2
3
export TEST_PATH=/Users/yunyu/workspace_go/TestGo
export DEMO_PATH=/Users/yunyu/workspace_go/GoDemo
export GOPATH=$TEST_PATH:$DEMO_PATH

目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
workspace_go
|
|--- TestGo
| |
| |--- src
| |
| |--- hello
| | |
| | |--- hello.go
| |
| |--- main.go
|
|-- GoDemo

再次执行run或者build都能正常运行

1
2
3
4
5
6
$ go run main.go
Hello World!
$ go build main.go
$ ./main
Hello World!

我们将main.go移入到main目录下,这样目录结构更加合理。目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
workspace_go
|
|--- TestGo
| |
| |--- src
| |
| |--- hello
| | |
| | |--- hello.go
| |
| |--- main
| |
| |--- main.go
|
|-- GoDemo

Go提供了install命令,与build命令相比,install命令在编译源码后还会将可执行文件或库文件安装到约定的目录下。我们以main目录为例:

1
2
$ cd main
$ go install

install命令执行后,我们发现main目录下没有任何变化,原先build时产生的main可执行文件也不见了踪影。别急,前面说过Go install也有一套自己的约定:

  • go install(在src/DIR下)编译出的可执行文件以其所在目录名(DIR)命名
  • go install将可执行文件安装到与src同级别的bin目录下,bin目录由go install自动创建
  • go install将可执行文件依赖的各种package编译后,放在与src同级别的pkg目录下

现在我们来看看bin目录:

1
2
3
4
5
6
7
8
$ ls /Users/yunyu/workspace_go/TestGo
bin
src
pkg
$ cd /Users/yunyu/workspace_go/TestGo
$ ls bin
main

的确出现一个bin目录和一个pkg目录,并且刚刚编译的程序main在bin下面。

hello.go编译后并非可执行程序,在编译main的同时,由于main依赖hello package,因此hello也被关联编译了。这与单独在hello目录下执行install的结果是一样的,只是单独安装的时候我们只创建了pkg目录,没有bin目录(因为bin目录是install main.go产生的),我们试试:

1
2
3
4
5
6
$ cd /Users/yunyu/workspace_go/TestGo/src/hello
$ go install
$ ls /Users/yunyu/workspace_go/TestGo
bin
pkg
src

在我们的workspace(TestGo目录)下出现了一个pkg目录,pkg目录下是一个名为darwin_amd64的子目录(这个目录和操作系统有关系),其下面有一个文件:hello.a。这就是我们install的结果。hello.go被编译为hello.a并安装到pkg/darwin_amd64目录下了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
workspace_go
|
|--- TestGo
| |
| |--- src
| | |
| | |--- hello
| | | |
| | | |--- hello.go
| | |
| | |--- main
| | |
| | |--- main.go
| |
| |--- bin
| | |
| | |--- main
| |
| |
| |--- pkg
| |
| |--- hello.a
|
|-- GoDemo

.a这个后缀名让我们想起了静态共享库,但这里的.a却是Go独有的文件格式,与传统的静态共享库并不兼容。但Go语言的设计者使用这个后缀名似乎是希望这个.a文件也承担起Go语言中”静态共享库”的角色。我们不妨来试试,看看这个hello.a是否可以被Go compiler当作”静态共享库”来对待。我们移除src中的hello目录,然后在main目录下执行go build:

1
2
3
4
$ go build
main.go:4:5: cannot find package "hello" in any of:
/usr/local/Cellar/go/1.7.3/libexec/src/hello (from $GOROOT)
/Users/yunyu/workspace_go/TestGo/src/hello (from $GOPATH)

Go编译器提示无法找到hello这个包,可见目前版本的Go编译器似乎不理pkg下的.a文件。http://code.google.com/p/go/issues/detail?id=2775 这个issue也印证了这一点,不过后续Go版本很可能会支持链接.a文件。毕竟我们在使用第三方package的时候,很可能无法得到其源码,并且在每个项目中都保存一份第三方包的源码也十分不利于项目源码的后期维护。

像脚本一样运行Go源码

Go具有很高的编译效率,这得益于其设计者对该目标的重视以及设计过程中细节方面的把控,当然这不是本文要关注的话题。正是由于go具有极速的编译,我们才可以像使用运行脚本语言那样使用它。

目前Go提供了run命令来直接运行源文件。比如:

1
2
$ go run main.go
Hello World!

go run实际上是一个将编译源码和运行编译后的二进制程序结合在一起的命令。但目前go源文件尚不支持作成Shebang Script,因为Go compiler尚不识别#!符号,下面的源码文件运行起来会出错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#! /usr/local/bin/go run
package main
import (
"hello"
)
func main() {
hello.Hello()
}
$ go run main.go
package main:
main.go:1:1: illegal character U+0023 '#'

不过我们可以可借助一些第三方工具来运行Shebang Go scripts,比如gorun。

go run, go build, go install的区别

1
2
3
go run helloworld.go
go build helloworld.go
go install

go build

通过go build加上要编译的Go源文件名,我们即可得到一个可执行文件,默认情况下这个文件的名字为源文件名字去掉.go后缀。

1
2
3
4
$ go build helloworld.go
$ ls
helloworld
helloworld.go

当然我们也 可以通过-o选项来指定其他名字:

1
2
3
4
$ go build -o newworld helloworld.go
$ ls
newworld
helloworld.go

如果我们在当前目录(src)下直接执行go build命令,后面不带文件名,我们将得到一个与目录名同名的可执行文件:

1
2
3
4
$ go build
$ ls
src
helloworld.go

go install

与build命令相比,install命令在编译源码后还会将可执行文件或库文件安装到约定的目录下。

go install编译出的可执行文件以其所在目录名(DIR)命名
go install将可执行文件安装到与src同级别的bin目录下,bin目录由go install自动创建
go install将可执行文件依赖的各种package编译后,放在与src同级别的pkg目录下。

go run

go run实际上是一个将编译源码和运行编译后的二进制程序结合在一起的命令。

go run helloworld.go = go build helloworld.go + ./helloworld

引用第三方包

若你在包的导入路径中包含了代码仓库的URL,go get就会自动地获取、构建并安装它:

1
$ go get github.com/golang/glog

若指定的包不在工作空间中,go get就会将会将它放到GOPATH指定的第一个工作空间内。(若该包已存在,go get就会跳过远程获取,其行为与go install相同)

在执行完上面的go get命令后,工作空间的目录树看起来应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
workspace_go
|
|--- TestGo
| |
| |--- src
| | |
| | |--- github.com
| | | |
| | | |--- golang
| | | |
| | | |--- glog
| | | |
| | | |--- LICENSE
| | | |
| | | |--- README
| | | |
| | | |--- glog.go
| | | |
| | | |--- glog_file.go
| | | |
| | | |--- glog_test.go
| | |
| | |--- hello
| | | |
| | | |--- hello.go
| | |
| | |--- main
| | |
| | |--- main.go
| |
| |--- bin
| | |
| | |--- main
| |
| |
| |--- pkg
| |
| |--- github.com
| | |
| | |--- golang
| | |
| | |--- glog.a
| |
| |--- hello.a
|
|-- GoDemo

hello.go

1
2
3
4
5
6
7
8
9
10
11
12
package hello
import "fmt"
import "flag"
import "github.com/golang/glog"
func Hello() {
flag.Parse()
fmt.Println("Hello World!")
glog.Info("glog Hello World!")
glog.Flush()
}

注意事项:

  1. 因为是使用时依参数配置,所以需在main()中加上flag.Parse()
  2. 需在结尾加上glog.Flush()
  3. 依级别生成不同的日志文件,但级别高的日志信息会同时在级别低的日志文件中输出
1
2
3
4
5
6
7
8
9
10
11
12
13
$ go run main.go -log_dir=./
Hello World!
$ ll
lrwxr-xr-x 1 yunyu staff 51 12 27 11:44 main.INFO -> main.localhost.yunyu.log.INFO.20161227-114425.19830
-rw-r--r-- 1 yunyu staff 72 12 22 14:39 main.go
-rw-r--r-- 1 yunyu staff 186 12 27 11:44 main.localhost.yunyu.log.INFO.20161227-114425.19830
$ vi main.INFO
Log file created at: 2016/12/27 11:48:08
Running on machine: localhost
Binary: Built with gc go1.7.3 for darwin/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I1227 11:48:08.088596 19951 hello.go:10] glog Hello World!

参考文章:

Shell脚本学习(九)简单的启动脚本

最近部署了日志收集服务,发现好多服务都要自己写启动脚本,刚刚开始学习Shell只能一边学一边写,就拿简单的Kibana服务来写个启动脚本吧。

一般服务的启动脚本,都需要实现下面的几点:

  1. 需要设置服务的内存使用大小(特别是Java服务)
  2. 后台启动(一些服务支持后台启动,例如:ElasticSearch)
  3. 限制启动用户(最好限定启动服务的用户,防止其他用户误操作)
  4. 设置PID文件目录(一些服务可以在配置文件中设置PID文件目录,例如:Kibana)
  5. 设置服务的logs目录(一些服务可以在配置文件中设置logs目录,例如:ElasticSearch)

(下面的Kibana启动脚本可能不是最新的,因为目前一直在更新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/bin/bash
# Kibana Issue : https://github.com/elastic/kibana/issues/5170
export NODE_OPTIONS="${NODE_OPTIONS:=--max-old-space-size=200}"
RETVAL=0
SYS_USER="elk"
SYS_USER_ID="501"
KB_NAME="kibana"
KB_DESC="Kibana 4.5.4"
KB_PID_FOLDER="/var/run/kibana"
KB_PID_FILE="${KB_PID_FOLDER}/${KB_NAME}.pid"
KB_LOG_FOLDER="${KB_HOME}/logs"
# 限制启动用户
if [ `id -u` -ne "${SYS_USER_ID}" ]; then
echo "You need ${SYS_USER} privileges to run this script"
exit 1
fi
# 启动服务
start() {
status
RETVAL=$?
if [ $RETVAL -eq 0 ]; then
echo "${KB_NAME} is already running"
exit $RETVAL
fi
echo "Starting ${KB_DESC} : "
if [ ! -d "${KB_PID_FOLDER}" ] ; then
mkdir -p ${KB_PID_FOLDER}
fi
if [ ! -d "${KB_LOG_FOLDER}" ] ; then
mkdir -p ${KB_LOG_FOLDER}
fi
cd ${KB_HOME}/bin
nohup kibana 1>${KB_LOG_FOLDER}/${KB_NAME}.out 2>${KB_LOG_FOLDER}/${KB_NAME}.err &
sleep 3
PID=`cat "${KB_PID_FILE}"`
echo "${KB_NAME} started. PID:$PID"
return 0
}
# 停止服务
stop() {
echo "Stopping ${KB_DESC} : "
if status ; then
PID=`cat "${KB_PID_FILE}"`
echo "Killing ${KB_NAME} (PID:$PID) with SIGTERM"
kill -TERM $PID >/dev/null 2>&1
sleep 1
if status && sleep 1 ; then
echo "${KB_NAME} stop failed; still running. Will force kill ${KB_NAME}"
kill -KILL $PID >/dev/null 2>&1
sleep 1
if status && sleep 1 ; then
echo "${KB_NAME} stop failed; still running."
else
echo "${KB_NAME} stopped."
rm -f ${KB_PID_FILE}
fi
else
echo "${KB_NAME} stopped."
rm -f ${KB_PID_FILE}
fi
fi
}
# 检查服务状态
status() {
if [ -f "${KB_PID_FILE}" ] ; then
PID=`cat "${KB_PID_FILE}"`
if kill -0 $PID > /dev/null 2> /dev/null ; then
return 0
else
# program is dead but pid file exists
return 2
fi
else
# program is not running
return 3
fi
}
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status
RETVAL=$?
if [ $RETVAL -eq 0 ] ; then
PID=`cat "${KB_PID_FILE}"`
echo "${KB_NAME} is running. PID:$PID"
else
echo "${KB_NAME} is not running"
fi
exit $RETVAL
;;
restart)
stop && start
;;
*)
# Invalid Arguments, print the following message.
echo "Usage: $0 {start|stop|status|restart}" >&2
exit 2
;;
esac

Logstash学习(三)Logstash的Grok表达式

今天遇到个比较奇葩的日志解析问题,我们的日志文件内容是标准的JSON格式的,但是在使用Logstash解析的时候,我们希望也能够保存原始的日志字符串到ES

track.log日志文件

1
2
3
4
5
6
7
8
9
{"logs":[{"timestamp":"1475114816071","rpid":"65351516503932932","name":"birdben.api.call","request":"GET /api/test/settings","status":"succeeded","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475914829286}],"level":"info","message":"logs","timestamp":"2016-10-08T08:20:29.286Z"}
{"logs":[{"timestamp":"1475114827206","rpid":"65351516503932930","name":"birdben.api.call","request":"GET /api/test/settings","status":"succeeded","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475914840425}],"level":"info","message":"logs","timestamp":"2016-10-08T08:20:40.425Z"}
{"logs":[{"timestamp":"1475915077351","rpid":"65351516503932934","name":"birdben.api.call","request":"GET /api/test/settings","status":"succeeded","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475915090579}],"level":"info","message":"logs","timestamp":"2016-10-08T08:24:50.579Z"}
{"logs":[{"timestamp":"1475914816133","rpid":"65351516503932928","name":"birdben.api.call","request":"GET /api/test/settings","status":"succeeded","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475914829332}],"level":"info","message":"logs","timestamp":"2016-10-08T08:20:29.332Z"}
{"logs":[{"timestamp":"1475914827284","rpid":"65351516503932936","name":"birdben.api.call","request":"GET /api/test/settings","status":"succeeded","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475914840498}],"level":"info","message":"logs","timestamp":"2016-10-08T08:20:40.499Z"}
{"logs":[{"timestamp":"1475915077585","rpid":"65351516503932932","name":"birdben.api.call","request":"GET /api/test/settings","status":"succeeded","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475915090789}],"level":"info","message":"logs","timestamp":"2016-10-08T08:24:50.789Z"}
{"logs":[{"timestamp":"1475912701768","rpid":"65351516503932930","name":"birdben.api.call","request":"GET /api/test/settings","status":"succeeded","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475912715001}],"level":"info","message":"logs","timestamp":"2016-10-08T07:45:15.001Z"}
{"logs":[{"timestamp":"1475913832349","rpid":"65351516503932934","name":"birdben.api.call","request":"GET /api/test/settings","errorStatus":200,"errorCode":"0000","errorMsg":"操作成功","status":"failed","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475913845544}],"level":"info","message":"logs","timestamp":"2016-10-08T08:04:05.544Z"}
{"logs":[{"timestamp":"1475915080561","rpid":"65351516503932928","name":"birdben.api.call","request":"GET /api/test/settings","errorStatus":200,"errorCode":"0000","errorMsg":"操作成功","status":"failed","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475915093792}],"level":"info","message":"logs","timestamp":"2016-10-08T08:24:53.792Z"}

我开始的想法是从track.log日志文件中读取日志信息,因为track.log日志是标准的JSON格式的,所以直接将codec设置成json,因为logs是一个内嵌的数组,然后在filter根据logs做split,会把logs的数组拆分出多条日志信息,然后在匹配指定格式的timestamp并生成新的字段@timestamp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
input {
file {
path => ["/home/yunyu/Downloads/track.log"]
type => "api"
codec => "json"
start_position => "beginning"
ignore_older => 0
}
}
filter {
split {
field => "logs"
}
date {
match => ["timestamp", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]
target => "@timestamp"
}
}
output {
stdout {
codec => rubydebug
}
elasticsearch {
codec => "json"
hosts => ["hadoop1:9200", "hadoop2:9200", "hadoop3:9200"]
index => "api_logs_index"
document_type => "%{type}"
workers => 1
flush_size => 20000
idle_flush_time => 10
}
}

上述做法的确能够将track.log日志解析成功并且写入到ES中,但是没有办法获取到原日志信息,对我们来说是无法满足要求的。

为什么没办法获取到原日志信息呢?这里再次梳理下Logstash的处理流程。

原来一直以为是

  • input –> filter –> output

应该补上decode/encode

  • input –> decode –> filter –> encode –> output

decode/encode就是解码器和编码器

  • input的codec是设置decode解码器
    • codec => json:把json字符串转换成json对象
  • ouput的codec是设置encode编码器(大部分output都默认codec是json,例如:ES,Kafka等等)
    • codec => json:把json对象转换成json字符串

分析了一下codec => json的作用,就是直接输入预定义好的JSON数据,这样就可以省略掉filter/grok配置,其实就是可以省略在filter/grok中配置json插件配置了,codec默认值是plain,plain是一个空的解析器,它可以让用户自己指定格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
input {
file {
...
codec => "json"
...
}
}
filter {
}
等价于
input {
file {
...
codec => json {
charset => ["UTF-8"] (optional), default: "UTF-8"
}
...
}
}
filter {
}
等价于
input {
file {
...
# 默认值是plain
codec => "plain"
...
}
}
filter {
grok {
match {
"message" => "%{MATCH_ALL:@message}"
}
}
json {
source => "@message"
}
}

MATCH_ALL是在{LOGSTASH_HOME}/patterns/postfix配置的grok表达式

1
2
# MATCH_ALL就是匹配任意的字符串
MATCH_ALL (.*)

也可以自己指定一个format格式,转换成String输出到指定端(ES,Kafka等等),注意format只对output生效

1
2
3
4
5
6
7
output {
kafka {
codec => plain {
format => "%{message}"
}
}
}

但是,仍然不解为何使用codec => “json”后,filter就获取不到原日志信息

再次尝试,仍然使用codec => “json”,但是在filter中添加match表达式,通过match中的”message”来获取原日志信息,然后把值复制给@message新字段,后续进行split拆分等等其他操作。具体配置文件修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
input {
file {
...
codec => "json"
...
}
}
filter {
grok {
match => {
"message" => "%{MATCH_ALL:@message}"
}
}
split {
field => "logs"
}
date {
match => ["timestamp", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]
target => "@timestamp"
}
}

尝试的结果并不是我们预期的,@message中获取的其实是我们track.log中的message字段的值”logs”。这里有个特殊情况就是我们track.log日志中带有message这个字段,但是为什么这里match表达式却获取到track.log日志中的message字段了呢?原因是我们input设置使用codec解码器为json(也就是将Logstash读取到我们file的原日志信息解析成json对象),match这里接收到的其实就是json对象中的message字段(就是我们track.log日志中的message字段),简单的理解match => {“message” => “%{MATCH_ALL:@message}”}就是通过”message”这个key获取filter接收的数据源(json对象或者原日志字符串)中的value,如果codec设置成json就是读取的json对象中的”message”的属性值,如果codec设置成plain,”message”是获取的原日志的字符串信息匹配grok表达式的值。

所以这里我们为了能够让match的”message”获取到原日志信息,而不是我们解析好的json日志中的message属性,我们把input的codec => “json”改成codec => “plain”,这样就会在input就将原日志解析成json对象了,而是我们在filter自己来处理。具体配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
input {
file {
path => ["/home/yunyu/Downloads/track.log"]
type => "api"
start_position => "beginning"
ignore_older => 0
}
}
filter {
grok {
# MATCH_ALL为了匹配所有的字符串,然后将值复制给新字段@message
match => {
"message" => "%{MATCH_ALL:@message}"
}
}
# json的作用可以将日志字符串中是json字符串的部分解析转换成json对象
# 再将新字段@message转换成json对象
json {
source => "@message"
}
# split拆分转化好的json对象中的logs数组
split {
field => "logs"
}
date {
match => ["timestamp", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]
target => "@timestamp"
}
}
output {
stdout {
codec => rubydebug
}
elasticsearch {
codec => "json"
hosts => ["hadoop1:9200", "hadoop2:9200", "hadoop3:9200"]
index => "api_logs_index"
document_type => "%{type}"
workers => 1
flush_size => 20000
idle_flush_time => 10
}
}

ES中的索引数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"_index": "api_logs_index",
"_type": "api",
"_id": "AViQYVbFlnhRMZdfbXco",
"_version": 1,
"_score": 1,
"_source": {
"message": "logs",
"@version": "1",
"@timestamp": "2016-10-08T15:20:29.286Z",
"path": "/home/yunyu/Downloads/birdben.log",
"host": "hadoop1",
"type": "api",
"@message": "{"logs":[{"timestamp":"1475114816071","rpid":"65351516503932932","name":"birdben.ad.open_hb","bid":0,"uid":0,"did":0,"duid":0,"hb_uid":0,"ua":"","device_id":"","server_timestamp":1475914829286}],"level":"info","message":"logs","timestamp":"2016-10-08T08:20:29.286Z"}",
"logs": {
"timestamp": "1475114816071",
"rpid": "65351516503932932",
"name": "birdben.ad.open_hb",
"bid": 0,
"uid": 0,
"did": 0,
"duid": 0,
"hb_uid": 0,
"ua": "",
"device_id": "",
"server_timestamp": 1475914829286
},
"level": "info",
"timestamp": "2016-10-08T08:20:29.286Z"
}
}

这里在使用split插件的过程中还发现了个Logstash的Bug,Split拆分的过程中如果遇到不是String和Array类型就会直接导致Logstash Crash,这里正确的做法应该类似grok一样,给对应的日志添加一个_jsonparsefailure的Tag,而不是导致Logstash直接崩溃,这个问题已经有人在GitHub上提了一个issue,貌似要等到Logstash 5.x的版本才会被修复。

具体地址:https://github.com/logstash-plugins/logstash-filter-split/issues/17

参考文章:

Kibana学习(三)查看Kibana的Request

如果查看Kibana发送给ES的request请求,请看下面的图示例,哈哈

Kibana_request1

Kibana_request2

这里额外说一下使用curl -I获取Kibana Response的Header问题,Kibana要求Request中的Header必须带有kbn-version这个参数,否则获取到的Response中的Header Status Code就是400,而不是200。这个问题我是使用阿里云的健康检查发现的,因为阿里云要求所有服务的Response Status Code必须是200才认为该服务是健康状态。

阿里云的健康检查使用的命令如下,如果返回非2XX、3XX状态,定义为健康检查异常。

1
echo -e "HEAD /test.html HTTP/1.0\r\n\r\n" |nc -t LAN_IP 80
1
$ curl -I 'http://localhost:5601' -H 'Host: localhost:5601' HTTP/1.1 400 Bad Request kbn-name: kibana kbn-version: 4.5.4 content-type: application/json; charset=utf-8 cache-control: no-cache Date: Thu, 01 Dec 2016 10:54:47 GMT Connection: keep-alive $ curl -I 'http://localhost:5601' -H 'Host: localhost:5601' -H 'kbn-version:4.5.4' HTTP/1.1 200 OK kbn-name: kibana kbn-version: 4.5.4 cache-control: no-cache Date: Thu, 01 Dec 2016 11:01:59 GMT Connection: keep-alive

还有就是阿里云的健康检查异常的情况下,我们检查elasticsearch的运行状态会发现会有很多10和100段的IP地址和我们ES服务器有通信,这是由于负载均衡系统进行健康检查引起的。由于阿里云的健康检查异常,所以就会出现很多tcp连接是TIME_WAIT状态。但是最后我们没有办法在header中添加’kbn-version:4.5.4’参数,所以只好关闭阿里云的健康检查了。

参考文章: