文章目录

  • 一、架构
  • 二、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 &;