日志收集系统应该说是到达一定规模的公司的标配了,一个能满足业务需求、运维成本低、稳定的日志收集系统对于运维的同学和日志使用方的同学都是非常nice的。然而这时理想中的日志收集系统,现实往往不是这样的...本篇的主要内容是:首先吐槽一下公司以前的日志收集和上传;介绍新的实时日志收集系统架构;用go语言实现。澄清一下,并不是用go语言实现全部,比如用到卡夫卡肯定不能重写一个kafka吧...

logagent所有代码已上传到github:https://github.com/zingp/logagent

1 老系统吐槽

我司以前的日志收集系统概述如下:

日志收集的频率有每小时收集一次、每5分钟收集一次、实时收集三种。大部分情况是每小时收集上传一次。
(1)每5分钟上传一次和每小时上传一次的情况是这样的:
每台机器上都需要部署一个日志收集agengt,部署一个日志上传agent,每台机器都需要挂载hadoop集群的客户端。
日志收集agent负责切割日志,上传agent整点的时候启动利用hadoop客户端,将切割好的前1小时或前5分钟日志打包上传到hadoop集群。
(2)实时传输的情况是这样的
每台机器上部署另一个agent,该agent实时收集日志传输到kafka。

看到这里你可能都看不下去了,这么复杂臃肿费劲的日志收集系统是怎么设计出来的?额...先辩解一下,这套系统有4年以上的历史了,当时的解决方案确实有限。辩解完之后还是得吐槽一下系统存在的问题:
(1)首先部署在每台机器上的agent没有做统一的配置入口,需要根据不同业务到不同机器上配置,运维成本太大;十台机器也就罢了,问题是现在有几万台机器,几千个服务。
(2)最无语的是针对不同的hadoop集群,需要挂载多个hadoop客户端,也就是存在一台机器上部署几个hadoop客户端的情况。运维成本太大...
(3)没做限流,整点的时候传输压力变大。某些机器有很多日志,一到整点压力就上来了。无图无真相,我们来看下:

CPU:看绿色的线条

负载:

网卡:

这组机器比较典型(这就是前文说的有多个hadoop客户端的情况),截图是凌晨至上午的时间段,还未到真正的高峰期。不过总体上可看出整点的压力是明显比非正点高很多的,已经到了不能忍的地步。

(4)省略n条吐槽...

2 新系统架构

首先日志收集大可不必在客户端分为1小时、5分钟、实时这几种频率,只需要实时一种就能满足前面三种需求。

其次可以砍掉在机器上挂载hadoop客户端,放在其他地方做日志上传hadoop流程。

第三,做统一的配置管理系统,提供友好的web界面,用户只需要在web界面上配置一组service需要收集的日志,便可通知该组service下的所有机器上的日志收集agent。

第四,流量削峰。应该说实时收集可以避免旧系统整点负载过大情况,但依旧应该做限流功能,防止高峰期agent过度消耗资源影响业务。

第五,日志补传...

实际上公司有的部门在用flume做日志收集,但觉得太重。经过一段时间调研和结合自身业务特点,利用开源软件在适当做些开发会比较好。go应该擅长做这个事,而且方便运维。好了,附上架构图。

将用go实现logagent,Web,transfer这个三个部分。

logagent主要负责按照配置实时收集日志发送到kafka,此外还需watch etcd中的配置,如改变,需要热更新。

web部分主要用于更新etcd中的配置,etcd已提供接口,我们只需要集成到资源管理系统或CMDB系统的管理界面中去即可。

transfer 做的是消费kafka队列中的日志,发送到es/hadoop/storm中去。

3 实现logagent

3.1 配置设计

首先思考下logagent的配置文件内容:

etcd_addr = 10.134.123.183:2379         # etcd 地址
etcd_timeout = 5 # 连接etcd超时时间
etcd_watch_key = /logagent/%s/logconfig # etcd key 格式 kafka_addr = 10.134.123.183:9092 # 卡夫卡地址 thread_num = 4 # 线程数
log = ./log/logagent.log # agent的日志文件
level = debug # 日志级别 # 监听哪些日志,日志限流大小,发送到卡夫卡的哪个topic 这个部分可以放到etcd中去。 

如上所说,监听哪些日志,日志限流大小,发送到卡夫卡的哪个topic 这个部分可以放到etcd中去。etcd中存储的value格式设计如下:

`[
{
"service":"test_service",
"log_path": "/search/nginx/logs/ping-android.shouji.sogou.com_access_log", "topic": "nginx_log",
"send_rate": 1000
},
{
"service":"srv.android.shouji.sogou.com",
"log_path": "/search/nginx/logs/srv.android.shouji.sogou.com_access_log","topic": "nginx_log",
"send_rate": 2000
}
]` - "service":"服务名称",
- "log_path": "应该监听的日志文件",
- "topic": "kfk topic",
- "send_rate": "日志条数限制"  

其实可以将更多的配置放入etcd中,根据自身业务情况可自行定义,本次就做如此设计,接下来可以写解析配置文件的代码了。

config.go

package main

import (
"fmt"
"github.com/astaxie/beego/config"
) type AppConfig struct {
EtcdAddr string
EtcdTimeOut int
EtcdWatchKey string KafkaAddr string ThreadNum int
LogFile string
LogLevel string
} var appConf = &AppConfig{} func initConfig(file string) (err error) {
conf, err := config.NewConfig("ini", file)
if err != nil {
fmt.Println("new config failed, err:", err)
return
}
appConf.EtcdAddr = conf.String("etcd_addr")
appConf.EtcdTimeOut = conf.DefaultInt("etcd_timeout", 5)
appConf.EtcdWatchKey = conf.String("etcd_watch_key") appConf.KafkaAddr = conf.String("kafka_addr") appConf.ThreadNum = conf.DefaultInt("thread_num", 4)
appConf.LogFile = conf.String("log")
appConf.LogLevel = conf.String("level")
return
} 

代码主要定义了一个AppConf结构体,然后读取配置文件,存放到结构体中。

此外,还有部分配置在etcd中,需要做两件事,第一次启动程序时将配置从etcd拉取下来;然后启动一个协程去watch etcd中的配置是否更改,如果更改需要拉取并更新到内存中。代码如下:

etcd.go:

package main

import (
"context"
"fmt"
"sync"
"time" "github.com/astaxie/beego/logs"
client "github.com/coreos/etcd/clientv3"
) var (
confChan = make(chan string, 10)
cli *client.Client
waitGroup sync.WaitGroup
) func initEtcd(addr []string, keyFormat string, timeout time.Duration) (err error) {
// init a global var cli and can not close
cli, err = client.New(client.Config{
Endpoints: addr,
DialTimeout: timeout,
})
if err != nil {
fmt.Println("connect etcd error:", err)
return
}
logs.Debug("init etcd success")
// defer cli.Close() //can not close var etcdKeys []string
ips, err := getLocalIP()
if err != nil {
fmt.Println("get local ip error:", err)
return
}
for _, ip := range ips {
key := fmt.Sprintf(keyFormat, ip)
etcdKeys = append(etcdKeys, key)
} // first, pull conf from etcd
for _, key := range etcdKeys {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
resp, err := cli.Get(ctx, key)
cancel()
if err != nil {
fmt.Println("get etcd key failed, error:", err)
continue
} for _, ev := range resp.Kvs {
// return result is not string
confChan <- string(ev.Value)
fmt.Printf("etcd key = %s , etcd value = %s", ev.Key, ev.Value)
}
} waitGroup.Add(1)
// second, start a goroutine to watch etcd
go etcdWatch(etcdKeys)
return
} // watch etcd
func etcdWatch(keys []string) {
defer waitGroup.Done() var watchChans []client.WatchChan
for _, key := range keys {
rch := cli.Watch(context.Background(), key)
watchChans = append(watchChans, rch)
} for {
for _, watchC := range watchChans {
select {
case wresp := <-watchC:
for _, ev := range wresp.Events {
confChan <- string(ev.Kv.Value)
logs.Debug("etcd key = %s , etcd value = %s", ev.Kv.Key, ev.Kv.Value)
}
default:
}
}
time.Sleep(time.Second)
}
} //GetEtcdConfChan is func get etcd conf add to chan
func GetEtcdConfChan() chan string {
return confChan
}  

其中,有一个比较个性化的设计,就是一台主机对应的etcd 中的key我们设置成/logagent/本机ip/logconfig的格式,因此还需要一个获取本机IP的功能,注意一台机器可能存在多个IP。

ip.go:

package main

import (
"fmt"
"net"
) // var a slice for ip addr
var ipArray []string func getLocalIP() (ips []string, err error) {
ifaces, err := net.Interfaces()
if err != nil {
fmt.Println("get ip interfaces error:", err)
return
} for _, i := range ifaces {
addrs, errRet := i.Addrs()
if errRet != nil {
continue
} for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
if ip.IsGlobalUnicast() {
ips = append(ips, ip.String())
}
}
}
}
return
}

3.2 初始化kafka

初始化kafka很简单,就是创建kafka实例,提供发送日志功能。只不过发送是并发的。

package main

import (
"fmt"
"github.com/Shopify/sarama"
"github.com/astaxie/beego/logs"
) var kafkaSend = &KafkaSend{} type Message struct {
line string
topic string
} type KafkaSend struct {
client sarama.SyncProducer
lineChan chan *Message
} func initKafka(kafkaAddr string, threadNum int) (err error) {
kafkaSend, err = NewKafkaSend(kafkaAddr, threadNum)
return
} // NewKafkaSend is
func NewKafkaSend(kafkaAddr string, threadNum int) (kafka *KafkaSend, err error) {
kafka = &KafkaSend{
lineChan: make(chan *Message, 10000),
} config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // wait kafka ack
config.Producer.Partitioner = sarama.NewRandomPartitioner // random partition
config.Producer.Return.Successes = true client, err := sarama.NewSyncProducer([]string{kafkaAddr}, config)
if err != nil {
logs.Error("init kafka client err: %v", err)
return
}
kafka.client = client for i := 0; i < threadNum; i++ {
fmt.Println("start to send kfk")
waitGroup.Add(1)
go kafka.sendMsgToKfk()
}
return
} func (k *KafkaSend) sendMsgToKfk() {
defer waitGroup.Done() for v := range k.lineChan {
msg := &sarama.ProducerMessage{}
msg.Topic = v.topic
msg.Value = sarama.StringEncoder(v.line) _, _, err := k.client.SendMessage(msg)
if err != nil {
logs.Error("send massage to kafka error: %v", err)
return
}
}
} func (k *KafkaSend) addMessage(line string, topic string) (err error) {
k.lineChan <- &Message{line: line, topic: topic}
return
}

3.3 实时读取日志,发送到kafka

用到第三方包:"github.com/hpcloud/tail"。将每个监听的日志,都抽象成一个对象。

package main

import (
"encoding/json"
"fmt"
"strings"
"sync" "github.com/astaxie/beego/logs"
"github.com/hpcloud/tail"
) // TailObj is TailMgr's instance
type TailObj struct {
tail *tail.Tail
offset int64
logConf LogConfig
secLimit *SecondLimit
exitChan chan bool
} var tailMgr *TailMgr //TailMgr to manage tailObj
type TailMgr struct {
tailObjMap map[string]*TailObj
lock sync.Mutex
} // NewTailMgr init TailMgr obj
func NewTailMgr() *TailMgr {
return &TailMgr{
tailObjMap: make(map[string]*TailObj, 16),
}
} //AddLogFile to Add tail obj
func (t *TailMgr) AddLogFile(conf LogConfig) (err error) {
t.lock.Lock()
defer t.lock.Unlock() _, ok := t.tailObjMap[conf.LogPath]
if ok {
err = fmt.Errorf("duplicate filename:%s", conf.LogPath)
return
} tail, err := tail.TailFile(conf.LogPath, tail.Config{
ReOpen: true,
Follow: true,
Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // read to tail
MustExist: false, //file does not exist, it does not return an error
Poll: true,
})
if err != nil {
fmt.Println("tail file err:", err)
return
} tailObj := &TailObj{
tail: tail,
offset: 0,
logConf: conf,
secLimit: NewSecondLimit(int32(conf.SendRate)),
exitChan: make(chan bool, 1),
}
t.tailObjMap[conf.LogPath] = tailObj waitGroup.Add(1)
go tailObj.readLog()
return
} func (t *TailMgr) reloadConfig(logConfArr []LogConfig) (err error) {
for _, conf := range logConfArr {
tailObj, ok := t.tailObjMap[conf.LogPath]
if !ok {
err = t.AddLogFile(conf)
if err != nil {
logs.Error("add log file failed:%v", err)
continue
}
continue
}
tailObj.logConf = conf
tailObj.secLimit.limit = int32(conf.SendRate)
t.tailObjMap[conf.LogPath] = tailObj
} for key, tailObj := range t.tailObjMap {
var found = false
for _, newValue := range logConfArr {
if key == newValue.LogPath {
found = true
break
}
}
if found == false {
logs.Warn("log path :%s is remove", key)
tailObj.exitChan <- true
delete(t.tailObjMap, key)
}
}
return
} // Process hava two func get new log conf and reload conf
func (t *TailMgr) Process() {
for conf := range GetEtcdConfChan() {
logs.Debug("log conf: %v", conf) var logConfArr []LogConfig
err := json.Unmarshal([]byte(conf), &logConfArr)
if err != nil {
logs.Error("unmarshal failed, err: %v conf :%s", err, conf)
continue
} err = t.reloadConfig(logConfArr)
if err != nil {
logs.Error("reload config from etcd failed: %v", err)
continue
}
logs.Debug("reload config from etcd success")
}
} func (t *TailObj) readLog() { for line := range t.tail.Lines {
if line.Err != nil {
logs.Error("read line error:%v ", line.Err)
continue
} lineStr := strings.TrimSpace(line.Text)
if len(lineStr) == 0 || lineStr[0] == '\n' {
continue
} kafkaSend.addMessage(line.Text, t.logConf.Topic)
t.secLimit.Add(1)
t.secLimit.Wait() select {
case <-t.exitChan:
logs.Warn("tail obj is exited: config:", t.logConf)
return
default:
}
}
waitGroup.Done()
} func runServer() {
tailMgr = NewTailMgr()
tailMgr.Process()
waitGroup.Wait()
} 

此处设计了一个限流功能,逻辑大概如下:设置阈值A,如阈值为1000条,如果这秒钟已经发送1000条,那么这一秒剩下的时间就sleep。limit.go代码如下:

package main

import (
"sync/atomic"
"time" "github.com/astaxie/beego/logs"
)
// SecondLimit to limit num in one second
type SecondLimit struct {
unixSecond int64
curCount int32
limit int32
} // NewSecondLimit to init a SecondLimit obj
func NewSecondLimit(limit int32) *SecondLimit {
secLimit := &SecondLimit{
unixSecond: time.Now().Unix(),
curCount: 0,
limit: limit,
} return secLimit
} // Add is func to
func (s *SecondLimit) Add(count int) {
sec := time.Now().Unix()
if sec == s.unixSecond {
atomic.AddInt32(&s.curCount, int32(count))
return
} atomic.StoreInt64(&s.unixSecond, sec)
atomic.StoreInt32(&s.curCount, int32(count))
} // Wait to limit num
func (s *SecondLimit) Wait() bool {
for {
sec := time.Now().Unix()
if (sec == atomic.LoadInt64(&s.unixSecond)) && s.curCount >= s.limit {
time.Sleep(time.Millisecond)
logs.Debug("limit is runing, limit: %d s.curCount:%d", s.limit, s.curCount)
continue
} if sec != atomic.LoadInt64(&s.unixSecond) {
atomic.StoreInt64(&s.unixSecond, sec)
atomic.StoreInt32(&s.curCount, 0)
}
logs.Debug("limit is exited")
return false
}
}

此外,写日志的代码非主要代码,这里就不介绍了。所有代码均上传到github上,如有兴趣可前去clone,地址已经在文章开头处给出。

transfer将在下一篇文章中介绍。文中涉及kafka,etcd等搭建,可参考官网搭建单机版用于测试。

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=18j46a3goe9gk

 

  

 

 

海量日志实时收集系统架构设计与go语言实现的更多相关文章

  1. 基于Flume的美团日志收集系统 架构和设计 改进和优化

    3种解决办法 https://tech.meituan.com/mt-log-system-arch.html 基于Flume的美团日志收集系统(一)架构和设计 - https://tech.meit ...

  2. NET ERP系统架构设计

    解析大型.NET ERP系统架构设计 Framework+ Application 设计模式 我对大型系统的理解,从数量上面来讲,源代码超过百万行以上,系统有超过300个以上的功能,从质量上来讲系统应 ...

  3. 图数据库 Nebula Graph 的数据模型和系统架构设计

    Nebula Graph:一个开源的分布式图数据库.作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,而且能够提供极高的 ...

  4. [转]【转】大型高性能ASP.NET系统架构设计

    大型高性能ASP.NET系统架构设计 大型动态应用系统平台主要是针对于大流量.高并发网站建立的底层系统架构.大型网站的运行需要一个可靠.安全.可扩展.易维护的应用系统平台做为支撑,以保证网站应用的平稳 ...

  5. 万级TPS亿级流水-中台账户系统架构设计

    万级TPS亿级流水-中台账户系统架构设计 标签:高并发 万级TPS 亿级流水 账户系统 背景 业务模型 应用层设计 数据层设计 日切对账 背景 我们需要给所有前台业务提供统一的账户系统,用来支撑所有前 ...

  6. PetShop的系统架构设计

    <解剖PetShop>系列 一.PetShop的系统架构设计 http://www.cnblogs.com/wayfarer/archive/2007/03/23/375382.html ...

  7. petshop4.0 具体解释之中的一个(系统架构设计)

    前言:PetShop是一个范例,微软用它来展示.Net企业系统开发的能力.业界有很多.Net与J2EE之争,很多数据是从微软的PetShop和Sun的PetStore而来.这样的争论不可避免带有浓厚的 ...

  8. 基于Struts2,Spring4,Hibernate4框架的系统架构设计与示例系统实现

    笔者在大学中迷迷糊糊地度过了四年的光景,心中有那么一点目标,但总感觉找不到发力的方向. 在四年间,尝试写过代码结构糟糕,没有意义的课程设计,尝试捣鼓过Android开发,尝试探索过软件工程在实际开发中 ...

  9. Unity3D手游开发日记(2) - 技能系统架构设计

    我想把技能做的比较牛逼,所以项目一开始我就在思考,是否需要一个灵活自由的技能系统架构设计,传统的技能设计,做法都是填excel表,技能需要什么,都填表里,很死板,比如有的技能只需要1个特效,有的要10 ...

随机推荐

  1. 初始Spring mvc

    转自:http://elf8848.iteye.com/blog/875830很棒的一篇博客,想了解SpringMvc的入门选手可以去看看. 一,核心类与接口: DispatcherServlet - ...

  2. Step by Step Guide on Yanhua ACDP Clear BMW EGS ISN

    Yanhua Mini ACDP authorize new function on BMW EGS ISN clearing.So here UOBDII want to share this st ...

  3. Python:每日一题002

    题目: 企业发放的奖金根据利润提成.利润(I)低于或等于10万元时,奖金可提10%:利润高于10万元,低于20万元时,低于10万元的部分按10%提成,高于10万元的部分,可提成7.5%:20万到40万 ...

  4. C语言编程常见技巧(问题???)

    本文章根据<算法竞赛入门经典(第二版)>一书整理... 第一章 程序设计入门 printf 语句控制输出小数位数或总长度 printf("%.3f\n",8.0/5.0 ...

  5. Testng用例失败重新运行

    Testng用例失败重新运行   在ui测试用例的运行过程中,发现有很多不确定的因素会导致用例失败,比如网络原因,比如屏幕滑动失败等.想到需要让测试用例,在失败后重新运行来提高测试成功率. 在gith ...

  6. 桌面应用开发之WPF页面导航

    先看效果图 Get Start   为了项目解耦,使用mvvmlight框架.MVVM设计模式请自行了解. 1 新建项目   新建一个MvvmLight(WPF)项目,删除其中无关文件夹:Design ...

  7. Beta冲刺 (5/7)

    Part.1 开篇 队名:彳艮彳亍团队 组长博客:戳我进入 作业博客:班级博客本次作业的链接 Part.2 成员汇报 组员1(组长)柯奇豪 过去两天完成了哪些任务 共享编辑文章的后端数据处理 部分代码 ...

  8. JavaScript ~~ECMAScript

    一.JavaScript 简介 HTML:从语义的角度,描述页面结构 CSS:从审美的角度,描述样式(美化页面) JavaScript:从交互的角度,描述行为(提升用户体验) 2.JavaScript ...

  9. Kaldi单音素模型 训练部分

    在Kaldi中,单音素GMM的训练用的是Viterbi training,而不是Baum-Welch training.因此就不是用HMM Baum-Welch那几个公式去更新参数,也就不用计算前向概 ...

  10. 安卓Task和Back Stack

    概述 一个Activity允许用户完成一些操作,甚至,Android中设计Activity为组件的形式,这样,多个Activity--甚至是其它App的Activity可以一起完成一项任务. Task ...