文章目录
- 一、架构
- 二、input 读取kafka配置
- 2.1、简单配置及说明
- 2.2 protobuf数据反序列化
- 2.2.1 protocol buffer简介
- 2.2.2 安装插件
- 2.2.3 ReleaseRecordES.proto文件
- 2.2.4 protoc命令编译proto文件为ruby文件
- 2.2.5 input.kafka配置protobuf反序列化
- 三、filter对数据源进行过滤
- 3.1 elasticsearch plugin
- 3.2 data plugin
- 3.3 ruby plugin
- 四、output
- 4.1 stdout plugin
- 4.2 elasticsearch plugin
- 4.2.1 自动template模板
- 五、集群
- 六、启动命令
一、架构
本项目是通过Logstash从kafka读取release相关升级数据到ElasticSearch(下称es)中,并支持升级数据需要按照taskId的updateTime最新时间对es更新。由于从kafka中读取的数据是经过protobuf序列化的,logstash也需要对数据进行反序列化转换成json。Logstash实现的功能拓扑图如下所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-egKF8j2A-1591927503626)(https://i.imgur.com/f93WbB3.png)]
二、input 读取kafka配置
该模块负责从kafka集群中读取已经被protobuf序列化的数据,并进行反序列化操作。
input{
kafka {
bootstrap_servers => ["192.168.144.34:9092,192.168.144.35:9092,192.168.144.36:9092"]
group_id => "logstash-release-upgrade-test"
consumer_threads => 5
decorate_events => false
topics => ["release_upgrade_ES_test"]
type => "release-upgrade-es"
auto_offset_reset => "latest"
key_deserializer_class => "org.apache.kafka.common.serialization.ByteArrayDeserializer"
value_deserializer_class => "org.apache.kafka.common.serialization.ByteArrayDeserializer"
codec => protobuf{
class_name => ["ReleaseRecords.ReleaseRecordOrigin"]
include_path => ['/opt/logstash-6.5.4/config/fates/ReleaseRecordES_pb.rb']
protobuf_version => 3
}
}
}
2.1、简单配置及说明
- bootstrap_servers
- 值类型为string
- 默认值为"localhost:9092"
- 用于建立到集群的初始连接的Kafka实例的url列表,这个列表应该是host1:port1,host2:port2的形式,这些url仅用于初始连接,以发现完整的集群成员(可能会动态更改),因此这个列表不需要包含完整的服务器集(不过,如果一个服务器宕机,你可能需要多个服务器)。
- group_id
- 值类型为string
- 默认值为"logstash"
- 此消费者所属的组的标识符,消费者组是由多个处理器组成的单个逻辑订阅服务器,主题中的消息将分发给具有相同group_id的所有Logstash实例。
- consumer_threads
- 值类型为number
- 默认值为1
- 理想情况下,为了达到完美的平衡,你应该拥有与分区数量一样多的线程,线程多于分区意味着有些线程将处于空闲状态。
- decorate_events
- 值类型为boolean
- 默认值为false
- 可向事件添加Kafka元数据,比如主题、消息大小的选项,这将向logstash事件中添加一个名为kafka的字段,其中包含以下属性:topic:此消息关联的主题、consumer_group:这个事件中用来读取的消费者组、partition:此消息关联的分区、offset:此消息关联的分区的偏移量、key:包含消息key的ByteBuffer。
- topics
- topics
- 值类型为array
- 默认值为[“logstash”]
- 要订阅的主题列表,默认为[“logstash”]。
- enable_auto_commit
- 值类型为string
- 默认值为"true"
- 如果是true,消费者定期向Kafka提交已经返回的消息的偏移量,当进程失败时,将使用这个提交的偏移量作为消费开始的位置。
- type
- 值类型为string
- 这个设置没有默认值
- 向该输入处理的所有事件添加type字段,类型主要用于过滤器激活,该type作为事件本身的一部分存储,因此你也可以使用该类型在Kibana中搜索它。如果你试图在已经拥有一个type的事件上设置一个type(例如,当你将事件从发送者发送到索引器时),那么新的输入将不会覆盖现有的type,发送方的type集在其生命周期中始终与该事件保持一致,甚至在发送到另一个Logstash服务器时也是如此。
- 其它配置请参考
- 英文文档:https://www.elastic.co/guide/en/logstash/current/plugins-inputs-kafka.html#plugins-inputs-kafka-bootstrap_servers
- 中文文档:
2.2 protobuf数据反序列化
2.2.1 protocol buffer简介
相比无模式的JSON格式,Protobufs是一种有模式、高效的数据序列化格式。我们在Kafaka中传输的数据,很多都在使用Protocol Buffers进行编码。它的优势就在于:首先,编码后的数据size明显要比其他的编码方式要小。以JSON编码举例,消息体中不仅仅包含实际数据,还有对应的Key值及很多的中括号。对于文档结构基本不变的数据,传输中包含这些附加信息,是一种资源的浪费。当发送端和接收端对交互的文档结构达成一致后,传输过程还携带这部分结构信息就显得多余。在整个日志处理过程中,该部分消耗的资源是可以被节省下来。其次,消费者所处理的数据,数据格式都是约定好的,完全不会像JSON一样,莫名奇妙多出一个字段。同时,给数据字段的理解产生误解。
Logstash不支持Protobufs编解码。目前,它支持纯文本、JSON格式和其他别的消息格式。
Protobufs编解码需要手动配置,其配置如下。
2.2.2 安装插件
- kafka中使用protobuf版本为3.x,所以本地需要安装的protobuf编译器也是3.x的
- To build protobuf from source, the following tools are needed:
- autoconf
- automake
- libtool
- make
- g++
- unzip
sudo yum install autoconf automake libtool curl make g++ unzip
- 安装protobuf
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
./autogen.sh
./configure --prefix=/usr/local/protobuf
make
make check
make install
- 配置环境变量
- vim /etc/profile
export PATH=$PATH:/usr/local/protobuf/bin/
export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
保存执行,source /etc/profile。同时在~/.profile中添加上面两行代码,否则会出现登录用户找不到protoc命令。
- 配置动态链接库
vim /etc/ld.so.conf,在文件中添加/usr/local/protobuf/lib(注意: 在新行处添加),然后执行命令: ldconfig
2.2.3 ReleaseRecordES.proto文件
- 在/opt/logstash-6.5.4/config/fates/目录下新建ReleaseRecordES.proto
syntax = "proto3";
// Compile: protoc --ruby_out=. ReleaseRecordES.proto
package ReleaseRecords;
message ReleaseRecordOrigin {
string taskId = 1;
string device = 2;
string appName = 3;
string mediaAppName = 4;
string version = 5;
string oldVersion = 6;
string mediaVersion = 7;
int32 strategyId = 8;
int32 sp = 9;
int32 softBit = 10;
int32 upgradeMode = 11;
int32 strategyType = 12;
int32 status = 13;
string country = 14;
string province = 15;
string city = 16;
string os = 17;
string ip = 18;
string systemBit = 19;
int32 upgradeType = 20;
int32 situation = 21;
string time = 22;
int32 patchId = 23;
}
2.2.4 protoc命令编译proto文件为ruby文件
- 只能把proto文件编译成logstash识别的ruby文件才能解码消息
- 编译命令:
protoc --ruby_out=. ReleaseRecordES.proto
----------------------------------------------
#ls
ReleaseRecordES.proto
ReleaseRecordES_pb.rb
- 编译后的文件ReleaseRecordES_pb.rb
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: ReleaseRecordES.proto
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_message "ReleaseRecords.ReleaseRecordOrigin" do
optional :taskId, :string, 1
optional :device, :string, 2
optional :appName, :string, 3
optional :mediaAppName, :string, 4
optional :version, :string, 5
optional :oldVersion, :string, 6
optional :mediaVersion, :string, 7
optional :strategyId, :int32, 8
optional :sp, :int32, 9
optional :softBit, :int32, 10
optional :upgradeMode, :int32, 11
optional :strategyType, :int32, 12
optional :status, :int32, 13
optional :country, :string, 14
optional :province, :string, 15
optional :city, :string, 16
optional :os, :string, 17
optional :ip, :string, 18
optional :systemBit, :string, 19
optional :upgradeType, :int32, 20
optional :situation, :int32, 21
optional :time, :string, 22
optional :patchId, :int32, 23
end
end
module ReleaseRecords
ReleaseRecordOrigin = Google::Protobuf::DescriptorPool.generated_pool.lookup("ReleaseRecords.ReleaseRecordOrigin").msgclass
end
2.2.5 input.kafka配置protobuf反序列化
- 在input.kafka中添加如下信息
- class_name => [“ReleaseRecords.ReleaseRecordOrigin”]
- 包名+类名
- include_path => [’/opt/logstash-6.5.4/config/fates/ReleaseRecordES_pb.rb’]
- ReleaseRecordES_pb.rb文件所在路径
- protobuf_version => 3
- protobuf编译器版本为3.x
key_deserializer_class => "org.apache.kafka.common.serialization.ByteArrayDeserializer"
value_deserializer_class => "org.apache.kafka.common.serialization.ByteArrayDeserializer"
codec => protobuf{
class_name => ["ReleaseRecords.ReleaseRecordOrigin"]
include_path => ['/opt/logstash-6.5.4/config/fates/ReleaseRecordES_pb.rb']
protobuf_version => 3
}
三、filter对数据源进行过滤
获取升级日志是通过taskId进行更新:
首先,input读取kafka中的一条升级日志记录;
其次,filter的elasticsearch插件负责通过日志的主键taskId到es中查询该条记录;
最后,判断查询到的es记录是否存在,如果不存在就把kafka传来的日志记录插入到es中;如果存在,判断kafka中的updateTime是否新于es查询到的升级日志updateTime时间,新于
则把kafka数据插入到es中,老于或者时间一样则阻止插入到es中。
下面是filter各个插件在上述功能的实现:
3.1 elasticsearch plugin
elasticsearch{
hosts => ["192.168.144.23:19200","192.168.144.34:19200","192.168.144.35:19200","192.168.144.36:19200","192.168.162.137:19200"]
index => "release_upgrade_es_*"
query => "routing:%{appName} and _id:%{taskId}"
fields => {
"updateTime" => "oldUpdateTime"
}
}
- index => “release_upgrade_es_*”
- 由于index是按照月份进行分库,所以检索的时候需要匹配所有按月份分库
- query => “routing:%{appName} and _id:%{taskId}”
- 通过routing:产品名称和_id:taskId获取唯一的升级记录信息
- 把从es中获取的updateTime字段写入kafka中升级记录对象中,并以oldUpdateTime重新命名
- oldUpdateTime供ruby插件进行时间判断来用
3.2 data plugin
该插件重要把updateTime时间(ISO8601格式)写入@timestamp中,在本项目中@timestamp没有起到任何作用,该步骤也可以省略
date {
match => ["[updateTime]", "ISO8601"]
target => "[@timestamp]"
}
- filters/date 插件支持五种时间格式:
- ISO8601
- 类似 “2011-04-19T03:44:01.103Z” 这样的格
- UNIX
- UNIX 时间戳格式,记录的是从 1970 年起始至今的总秒数
- UNIX_MS
- 这个时间戳则是从 1970 年起始至今的总毫秒数
- TAI64N
- TAI64N 格式比较少见,是这个样子的:@4000000052f88ea32489532c
- Joda-Time 库
- Logstash 内部使用了 Java 的 Joda 时间库来作时间处理
- 详细说明请参考如下文章:
3.3 ruby plugin
判断查询到的kafka记录中的oldUpdateTime是否存在:
如果不存在就把kafka传来的日志记录插入到es中;
如果存在,判断kafka中的updateTime是否新于es查询到的升级日志updateTime时间:
新于则把kafka数据插入到es中;
老于或者时间一样则阻止插入到es中最后把的kafka记录中的oldUpdateTime字段移除
ruby {
code => "
if event.get('oldUpdateTime') != nil
then
duration_hrs = (Time.parse(event.get('updateTime')).to_f*1000 - Time.parse(event.get('oldUpdateTime')).to_f*1000) / 3600
if duration_hrs <= 0
then
event.cancel
end
end
"
remove_field => ["oldUpdateTime"]
}
四、output
该模块主要负责把kafka中通过filter过滤的数据存到es中
output {
stdout{codec=>rubydebug}
if [type] == "release-upgrade-es" {
elasticsearch{
hosts => ["192.168.144.23:19200","192.168.144.34:19200","192.168.144.35:19200","192.168.144.36:19200","192.168.162.137:19200"]
index => "release_upgrade_es_%{+YYYY.MM}"
action => "update"
doc_as_upsert => "true"
document_id => "%{taskId}"
routing => "%{appName}"
document_type => "doc"
manage_template => true
template => "/opt/logstash-6.5.4/config/fates/templateRecord.json"
template_name => "release_upgrade_record"
template_overwrite => true
}
}
}
4.1 stdout plugin
测试时在终端界面上显示向es中写入的数据,此模块可以在正式环境中删除
对索引按照年月进行分库
按照产品名称设置路由,所以只要属于某一个产品下的升级记录都会存到指定的分片中
4.2 elasticsearch plugin
- 主要配置
- hosts
- 设置远程实例的主机,如果es是集群可以按数组方式写入
- index => “release_upgrade_es_%{+YYYY.MM}”
- 对索引按照年月进行分库
- action
- index:将logstash.时间索引到一个文档
- delete:根据id删除一个document(这个动作需要一个id)
- create:建立一个索引document,如果id存在 动作失败.
- update:根据id更新一个document,有一种特殊情况可以upsert–如果document不是已经存在的情况更新document 。参见upsert选项。
- routing
- 默认情况下,索引数据的分片规则,是下面的公式:
shard_num = hash(_routing) % num_primary_shards
- 此时我们按照产品名称设置路由,所以只要属于某一个产品下的升级记录都会存到指定的分片中
shard_num = hash(_routing) % num_primary_shards
- document_id
- 为索引提供document id ,对重写elasticsearch中相同id词目很有用
- document_type
- 事件要被写入的document type,一般要将相似事件写入同一type,可用%{}引用事件type,默认type=log
- manage_template=>true
- 一个默认的es mapping 模板将启用(除非设置为false 用自己的template)
- template
- 有效的filepath,设置自己的template文件路径,不设置就用已有的
- template_name 在es内部模板的名字,可以任意命名
4.2.1 自动template模板
定义模板根据数据情况:
1.某一个产品的升级记录值在一个分片中,并且每个分片只有一个备份;
2.对kafka中已经进行protobuf反序列化的对象中string类型字段转换为keyword类型(该类型不会进行分词),其它类型按照默认类型即可;
3.对该模板下所有index起一个别名:release-upgrade-info;
4.如果kafka中已经进行protobuf反序列化的对象中string类型字段的名称中有message会转换为text类型(会产生分词,建立倒排索引,比较占空间),该message_field设置可以删除,暂时保留;
5.index.refresh_interval的值是5s(默认值是5s),这迫使Elasticsearch集群每5秒创建一个新的 segment(可以理解为Lucene的索引文件)。增加这个值,例如30s,可以允许更大的segment写入,减后以后的segment合并压力;
6. geoip 获取指定ip字段的地理信息,由于升级记录已经有ip地理信息,该映射暂时没用,后续可以删除该影响。
- 模板文件放在指定路径目录下:
• template => “/opt/logstash-6.5.4/config/fates/templateRecord.json”
- 模板内容
{
"template" : "release_upgrade_record_*",
"version" : 60001,
"settings" : {
"index": {
"number_of_shards": 1,
"number_of_replicas": 1,
"refresh_interval" : "5s"
}
},
"mappings" : {
"doc" : {
"_source": {"enabled": true},
"_all": {"enabled": false},
"dynamic_templates" : [ {
"message_field" : {
"path_match" : "message",
"match_mapping_type" : "string",
"mapping" : {
"type" : "text",
"norms" : false
}
}
}, {
"string_fields" : {
"match" : "*",
"match_mapping_type" : "string",
"mapping" : {
"type" : "keyword"
}
}
} ],
"properties" : {
"@timestamp": { "type": "date"},
"@version": { "type": "keyword"},
"geoip" : {
"dynamic": true,
"properties" : {
"ip": { "type": "ip" },
"location" : { "type" : "geo_point" },
"latitude" : { "type" : "half_float" },
"longitude" : { "type" : "half_float" }
}
}
}
}
},
"aliases": {
"release-upgrade-info": {}
}
}
五、集群
根据上诉方法编写好logstash配置文件后,用n台logstash服务器运行即可。由于这个集群使用的是同一个groupid ,并不会出现logstash重复消费kafka集群的问题。
六、启动命令
cd /opt/logstash-6.5.4
bin/logstash -f config/fates/kafkaToES.conf
- 利用nohup扔到后台运行
nohup /opt/logstash-6.5.4/bin/logstash -f /opt/logstash-6.5.4/config/config/fates/kafkaToES.conf >/dev/null &;