0x0 需求

  消费Kafka的日志并写入ElasticSearch供查询

0x1 依赖库

golang版Kafka客户端 https://github.com/Shopify/sarama

golang版ElasticSearch客户端  https://github.com/elastic/go-elasticsearch

0x2 实现

总共分3部分

1、Kafka消费者

// LogJson json格式
type LogJson struct {
Tag string `json:"tag"`
Level string `json:"level"`
File string `json:"file"`
Time time.Time `json:"@timestamp"`
Message string `json:"message"`
} type taskProcessor interface {
AddTask(key string, val []byte)
} // MyConsumer 可关闭的带任务处理器的消费者
type MyConsumer struct {
processor taskProcessor
ctx context.Context
} // NewMyConsumer 构造
func NewMyConsumer(p taskProcessor, ctx context.Context) *MyConsumer {
c := &MyConsumer{
processor: p,
ctx: ctx,
} return c
} // Setup 启动
func (consumer *MyConsumer) Setup(s sarama.ConsumerGroupSession) error {
log.Printf("[main] consumer.Setup memberID=[%s]", s.MemberID())
return nil
} // Cleanup 当退出时
func (consumer *MyConsumer) Cleanup(s sarama.ConsumerGroupSession) error {
log.Printf("[main] consumer.Cleanup memberID=[%s]", s.MemberID())
return nil
} // ConsumeClaim 消费日志
func (consumer *MyConsumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { for {
select {
case message, ok := <-claim.Messages():
if !ok {
return nil
}
js := &LogJson{}
if err := json.Unmarshal(message.Value, js); nil != err {
fmt.Fprintf(os.Stderr, "[MyConsumer] ConsumeClaim json.Unmarshal err=[%s] topic=[%s] key=[%s] val=[%s]\n", err.Error(), message.Topic, message.Key, string(message.Value))
} else {
index := fmt.Sprintf("%s-%s", message.Topic, js.Time.Format("2006.01.02"))
consumer.processor.AddTask(index, message.Value)
session.MarkMessage(message, "")
} case <-consumer.ctx.Done():
return nil
}
} return nil
}

2、插入ElasticSearch的Worker

package elastic_worker

import (
"context"
"encoding/json"
"fmt"
"log"
"runtime"
"sync"
"time" "github.com/olivere/elastic"
) // Config 配置
type Config struct {
MaxMessage int `xml:"max_msg"` // 最大缓冲
WorkerNum int `xml:"worker_number"` // 线程个数
BatchSize int `xml:"batch_size"` // 每个批次最大条数
TickTime int `xml:"tick_millisecond"` // 处理频率
} type task struct {
key string
val []byte
} // Worker 消息处理器
type Worker struct {
msgQ chan *task
client *elastic.Client
wg sync.WaitGroup
config *Config
} // NewWorker 构造
func NewWorker(client *elastic.Client, cfg *Config) *Worker {
w := &Worker{
client: client,
config: cfg,
msgQ: make(chan *task, cfg.MaxMessage),
} return w
} // Run 开工
func (w *Worker) Run(ctx context.Context) { // 线程数
thread := w.config.WorkerNum
if thread <= {
thread = runtime.NumCPU()
} // ticker
tickTime := time.Duration(w.config.TickTime) * time.Millisecond
if tickTime <= {
tickTime = time.Duration() * time.Millisecond
} // 启动
for i := ; i < thread; i++ {
w.wg.Add()
time.Sleep(tickTime / time.Duration(thread))
go func(idx int) { // 构造一个service,server可以反复使用
service := w.client.Bulk()
service.Refresh("wait_for")
defer service.Reset() log.Printf("[elastic_worker] worker[%d] start", idx)
defer w.wg.Done() // ticker
ticker := time.NewTicker(tickTime)
defer ticker.Stop() LOOP:
for {
select {
case <-ctx.Done():
log.Printf("[elastic_worker] worker[%d] is quiting", idx)
// 要把通道里的全部执行完才能退出
for {
if num := w.process(service); num > {
log.Printf("[elastic_worker] worker[%d] process batch [%d] when quiting", idx, num)
} else {
break LOOP
}
time.Sleep(tickTime)
} case <-ticker.C:
if num := w.process(service); num > {
log.Printf("[elastic_worker] worker[%d] process batch [%d] ", idx, num)
}
}
} log.Printf("[elastic_worker] worker[%d] stop", idx)
}(i)
}
} // AddTask 添加任务,goroutine safe
func (w *Worker) AddTask(key string, val []byte) {
t := &task{
key: key,
val: val,
}
w.msgQ <- t
} // process 处理任务
func (w *Worker) process(service *elastic.BulkService) int {
//service.Reset() // 每个批次最多w.config.BatchSize个
LOOP:
for i := ; i < w.config.BatchSize; i++ {
// 有任务就加到这个批次,没任务就退出
select {
case m := <-w.msgQ:
req := elastic.NewBulkIndexRequest().Index(m.key).Type("doc").Doc(json.RawMessage(m.val))
service.Add(req)
default:
break LOOP
}
} total := service.NumberOfActions()
if total > {
if resp, err := service.Do(context.Background()); nil != err {
panic(err)
} else {
if resp.Errors {
for _, v := range resp.Failed() {
fmt.Println("service.Do failed", v)
}
panic("resp.Errors")
}
}
} return total
} // Close 关闭 需要外面的context关闭,和等待msgQ任务被执行完毕
func (w *Worker) Close() {
w.wg.Wait()
if n := len(w.msgQ); n > {
log.Printf("[elastic_worker] worker Close remain msg[%d]", n)
}
}

3、main.go

package main

import (
"context"
"encoding/xml"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/signal"
"runtime"
"strings"
"syscall"
"time" "consumer"
"elastic_worker" "github.com/Shopify/sarama"
"github.com/olivere/elastic"
) // Consumer Consumer配置
type ConsumerConfig struct {
Topic []string `xml:"topic"`
Broker string `xml:"broker"`
Partition int32 `xml:"partition"`
Replication int16 `xml:"replication"`
Group string `xml:"group"`
Version string `xml:"version"`
} // Config 配置
type Config struct {
Consumer ConsumerConfig `xml:"consumer"`
ElasticURL string `xml:"elastic_url"`
Filters []string `xml:"filter"`
Worker elastic_worker.Config `xml:"elastic_worker"`
} var (
configFile = "" // 配置路径
initTopic = false
listTopic = false
delTopic = ""
cfg = &Config{}
web = ""
) func init() {
flag.StringVar(&configFile, "config", "cfg.xml", "config file ")
flag.BoolVar(&initTopic, "init", initTopic, "create topic")
flag.BoolVar(&listTopic, "list", listTopic, "list topic")
flag.StringVar(&delTopic, "del", delTopic, "delete topic")
} var (
elasticClient *elastic.Client
) func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) defer time.Sleep(time.Second) // 获取host名字
hostName, err := os.Hostname()
if nil != err {
hostName = "[beats]"
} // 加载配置
if contents, err := ioutil.ReadFile(configFile); err != nil {
panic(err)
} else {
if err = xml.Unmarshal(contents, cfg); err != nil {
panic(err)
}
} // sarama的logger
sarama.Logger = log.New(os.Stdout, fmt.Sprintf("[%s]", hostName), log.LstdFlags) // 指定kafka版本,一定要支持kafka集群
version, err := sarama.ParseKafkaVersion(cfg.Consumer.Version)
if err != nil {
panic(err)
}
config := sarama.NewConfig()
config.Version = version
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.ClientID = hostName // 工具
if tool(cfg, config) {
return
} else {
initTopic = true
tool(cfg, config)
} // 启动elastic客户端
urls := strings.Split(cfg.ElasticURL, ",")
if cli, err := elastic.NewClient(elastic.SetURL(urls...)); err != nil {
panic(err)
} else {
elasticClient = cli
// ping检查
if ret, _, err := elasticClient.Ping(urls[]).Do(context.Background()); nil != err { panic(err)
} else {
log.Printf("elasticClient.Ping %+v", ret)
} defer elasticClient.Stop()
} // ctx
ctx, cancel := context.WithCancel(context.Background()) // Worker
worker := elastic_worker.NewWorker(elasticClient, &cfg.Worker)
worker.Run(ctx)
defer worker.Close() // kafka consumer client
kafkaClient, err := sarama.NewConsumerGroup(strings.Split(cfg.Consumer.Broker, ","), cfg.Consumer.Group, config)
if err != nil {
panic(err)
} consumer := consumer.NewMyConsumer(worker, ctx)
go func() {
for {
select {
case <-ctx.Done():
return
default:
err := kafkaClient.Consume(ctx, cfg.Consumer.Topic, consumer)
if err != nil {
log.Printf("[main] client.Consume error=[%s]", err.Error())
time.Sleep(time.Second)
}
}
}
}() // os signal
sigterm := make(chan os.Signal, )
signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM) //time.Sleep(time.Second * 4)
sig := <-sigterm
log.Printf("[main] os sig=[%v]", sig) cancel()
log.Printf("[main] cancel")
if err := kafkaClient.Close(); nil != err {
log.Printf("[main] kafkaClient close error=[%s]", err.Error())
} log.Printf("[main] beats quit")
} func tool(cfg *Config, config *sarama.Config) bool {
if initTopic || listTopic || len(delTopic) > {
ca, err := sarama.NewClusterAdmin(strings.Split(cfg.Consumer.Broker, ","), config)
if nil != err {
panic(err)
} if len(delTopic) > { // 删除Topic
if err := ca.DeleteTopic(delTopic); nil != err {
panic(err)
}
log.Printf("delete ok topic=[%s]\n", delTopic)
} else if initTopic { // 初始化Topic
if detail, err := ca.ListTopics(); nil != err {
panic(err)
} else {
for _, v := range cfg.Consumer.Topic {
if d, ok := detail[v]; ok {
if cfg.Consumer.Partition > d.NumPartitions {
if err := ca.CreatePartitions(v, cfg.Consumer.Partition, nil, false); nil != err {
panic(err)
}
log.Println("alter topic ok", v, cfg.Consumer.Partition)
} } else {
if err := ca.CreateTopic(v, &sarama.TopicDetail{NumPartitions: cfg.Consumer.Partition, ReplicationFactor: cfg.Consumer.Replication}, false); nil != err {
panic(err)
}
log.Println("create topic ok", v)
}
}
}
} // 显示Topic列表
if detail, err := ca.ListTopics(); nil != err {
log.Println("ListTopics error", err)
} else {
for k := range detail {
log.Printf("[%s] %+v", k, detail[k])
}
} if err := ca.Close(); nil != err {
panic(err)
} return true
}
return false
}

0x3 配置文件

<?xml version="1.0" encoding="utf-8"?>
<config> <consumer>
<!-- Kafka cluster -->
<broker>127.0.0.1:</broker> <!-- topic 可以配多个-->
<topic>top1</topic>
<topic>top2</topic> <!-- Kafka 分组 -->
<group>test-group</group> <!-- Kafka 版本 -->
<version>2.2.</version> <!-- partition 个数,开consumer个数不能超过这个 -->
<partition></partition> <!-- 副本因子 -->
<replication></replication>
</consumer> <elastic_url>http://127.0.0.1:9200</elastic_url> <elastic_worker>
<!-- 最大缓冲 这个小点可以防止崩溃导致丢失太多-->
<max_msg></max_msg> <!-- 线程个数 -->
<worker_number></worker_number> <!-- 每个批次最大数量 -->
<batch_size></batch_size> <!-- 处理频率(毫秒) -->
<tick_millisecond></tick_millisecond>
</elastic_worker> </config>

0x4 注意

1、如果你的ElasticSearch集群的配置足够高,你可以修改配置文件里的<worker_number>1</worker_number>给Worker开多协程,否则还是单协程性能更高一些。

2、可以适当调整<batch_size>1024</batch_size>每个批次的数量来提升写入性能。

3、如果报这个错误  EsRejectedExcutionException,说明ES性能扛不住了,需要提升配置,降低写入量。

[Golang] 消费Kafka的日志提交到ElasticSearch的更多相关文章

  1. kafka日志同步至elasticsearch和kibana展示

    kafka日志同步至elasticsearch和kibana展示 一 kafka consumer准备 前面的章节进行了分布式job的自动计算的概念讲解以及实践.上次分布式日志说过日志写进kafka, ...

  2. JavaWeb项目架构之Kafka分布式日志队列

    架构.分布式.日志队列,标题自己都看着唬人,其实就是一个日志收集的功能,只不过中间加了一个Kafka做消息队列罢了. kafka介绍 Kafka是由Apache软件基金会开发的一个开源流处理平台,由S ...

  3. storm消费kafka实现实时计算

    大致架构 * 每个应用实例部署一个日志agent * agent实时将日志发送到kafka * storm实时计算日志 * storm计算结果保存到hbase storm消费kafka 创建实时计算项 ...

  4. Spark streaming消费Kafka的正确姿势

    前言 在游戏项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark streaming从kafka中不 ...

  5. 基于Flume+LOG4J+Kafka的日志采集架构方案

    本文将会介绍如何使用 Flume.log4j.Kafka进行规范的日志采集. Flume 基本概念 Flume是一个完善.强大的日志采集工具,关于它的配置,在网上有很多现成的例子和资料,这里仅做简单说 ...

  6. Spark Streaming消费Kafka Direct方式数据零丢失实现

    使用场景 Spark Streaming实时消费kafka数据的时候,程序停止或者Kafka节点挂掉会导致数据丢失,Spark Streaming也没有设置CheckPoint(据说比较鸡肋,虽然可以 ...

  7. 【转】flume+kafka+zookeeper 日志收集平台的搭建

    from:https://my.oschina.net/jastme/blog/600573 flume+kafka+zookeeper 日志收集平台的搭建 收藏 jastme 发表于 10个月前 阅 ...

  8. ELK+kafka构建日志收集系统

    ELK+kafka构建日志收集系统   原文  http://lx.wxqrcode.com/index.php/post/101.html   背景: 最近线上上了ELK,但是只用了一台Redis在 ...

  9. 消费阿里云日志服务SLS

    此文档只关心消费接入,不关心日志接入,只关心消费如何接入,可直接跳转到[sdk消费接入] SLS简介 日志服务: 日志服务(Log Service,简称 LOG)是针对日志类数据的一站式服务,在阿里巴 ...

随机推荐

  1. Matplotlib同时绘制多张图片

    我现在有一组图片,一共100张图片,每张大小是200*200,即imgs.shape=100*200*200*3 (注意通道数在最后一维). 我需要同时绘制这100张图片,而且每张图片需要写上对应的名 ...

  2. 计算机 KB,MB,GB,TB,PB,EB 计算

    ASCII码:一个英文字母(不分大小写)占一个字节的空间.一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数.换算为十进制,最小值-128,最大值127.如一个ASCII码就是一个字节 ...

  3. php析构函数什么时候调用?

    析构函数何时被调用 析构函数在下边3种情况时被调用: 对象生命周期结束,被销毁时: 主动调用delete :(推荐学习:PHP编程从入门到精通) 对象i是对象o的成员,o的析构函数被调用时,对象i的析 ...

  4. UiAutomatorViewer无法获取手机截图进行元素定位的解决办法

    问题描述 本来想使用UIAutomatorView定位app页面元素的,最开始我使用的是夜神模拟器,打开UIAutomatorView连接模拟器没有问题,但是后来我使用真机时发现无法连接到真机获取真机 ...

  5. redux:基于函数式编程的事件处理和状态维护机制

    redux = monand + pipeline + highorder componet + decouple + middleware redex = store based + event h ...

  6. springboot集成jsp,访问jsp页面下载问题

    1.导入相关依赖     (存在jsp页面下载问题,可能是缺少tomcat-embed-jasper的依赖对jsp的支持) <parent> <groupId>org.spri ...

  7. Connected Component in Undirected Graph

    Description Find connected component in undirected graph. Each node in the graph contains a label an ...

  8. CVE-2017-7494复现 Samba远程代码执行

    Samba是在Linux和Unix系统上实现Smb协议的一个免费软件,由服务器及客户端程序构成,Samba服务对应的TCP端口有139.445等.Smb一般作为文件共享服务器,专门提供Linux与Wi ...

  9. Python I/O编程 --读写文件、StringIO/ BytesIO

    I/O编程 Input/Output  输入/输出 Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水 Input Stream就是数据从外面(磁盘.网络)流进内存,Ou ...

  10. 将两个各有n个元素的有序表归并成一个有序表,其最多的比较次数

    最多的比较次数是当两个有序表的数据刚好是插空顺序的时候,比如:第一个序列是1,3,5,第二个序列是2,4,6,把第二个序列插入到第一个序列中,先把第二个序列中的第一个元素2和第一个序列依次比较,需要比 ...