海量日志实时收集系统架构设计与go语言实现
日志收集系统应该说是到达一定规模的公司的标配了,一个能满足业务需求、运维成本低、稳定的日志收集系统对于运维的同学和日志使用方的同学都是非常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
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语言实现的更多相关文章
- 基于Flume的美团日志收集系统 架构和设计 改进和优化
3种解决办法 https://tech.meituan.com/mt-log-system-arch.html 基于Flume的美团日志收集系统(一)架构和设计 - https://tech.meit ...
- NET ERP系统架构设计
解析大型.NET ERP系统架构设计 Framework+ Application 设计模式 我对大型系统的理解,从数量上面来讲,源代码超过百万行以上,系统有超过300个以上的功能,从质量上来讲系统应 ...
- 图数据库 Nebula Graph 的数据模型和系统架构设计
Nebula Graph:一个开源的分布式图数据库.作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,而且能够提供极高的 ...
- [转]【转】大型高性能ASP.NET系统架构设计
大型高性能ASP.NET系统架构设计 大型动态应用系统平台主要是针对于大流量.高并发网站建立的底层系统架构.大型网站的运行需要一个可靠.安全.可扩展.易维护的应用系统平台做为支撑,以保证网站应用的平稳 ...
- 万级TPS亿级流水-中台账户系统架构设计
万级TPS亿级流水-中台账户系统架构设计 标签:高并发 万级TPS 亿级流水 账户系统 背景 业务模型 应用层设计 数据层设计 日切对账 背景 我们需要给所有前台业务提供统一的账户系统,用来支撑所有前 ...
- PetShop的系统架构设计
<解剖PetShop>系列 一.PetShop的系统架构设计 http://www.cnblogs.com/wayfarer/archive/2007/03/23/375382.html ...
- petshop4.0 具体解释之中的一个(系统架构设计)
前言:PetShop是一个范例,微软用它来展示.Net企业系统开发的能力.业界有很多.Net与J2EE之争,很多数据是从微软的PetShop和Sun的PetStore而来.这样的争论不可避免带有浓厚的 ...
- 基于Struts2,Spring4,Hibernate4框架的系统架构设计与示例系统实现
笔者在大学中迷迷糊糊地度过了四年的光景,心中有那么一点目标,但总感觉找不到发力的方向. 在四年间,尝试写过代码结构糟糕,没有意义的课程设计,尝试捣鼓过Android开发,尝试探索过软件工程在实际开发中 ...
- Unity3D手游开发日记(2) - 技能系统架构设计
我想把技能做的比较牛逼,所以项目一开始我就在思考,是否需要一个灵活自由的技能系统架构设计,传统的技能设计,做法都是填excel表,技能需要什么,都填表里,很死板,比如有的技能只需要1个特效,有的要10 ...
随机推荐
- 单系统登录机制SSO
一.单系统登录机制 1.http无状态协议 web应用采用browser/server架构,http作为通信协议.http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关 ...
- Python3实战系列之九(获取印度售后数据项目)
项目现状:已经部署在服务器上并正常运行了. 1.服务器上的部署 2.下载到服务器的文件列表 3.转存在到数据库SQL Server中的数据 项目总结:这次项目采用python来实现,刚开始还是有点担忧 ...
- margin和padding的用法与区别--以及bug处理方式
margin和padding的用法: (1)padding (margin) -left:10px; 左内 (外) 边距(2)padding (margin) -right:10px; 右内 (外 ...
- 手机端table表格bug
table表格在手机端有一个小小的bug,就是td有一个右边线,解决办法可已给tr加一个背景色就行,或者table都行,完美解决
- js删除map中元素
js中删除map中元素后,map的长度不变,这时需要我们自己处理 delete vacc[0]; delete vacc[1]; ClearNullArr(vacc); //清除vacc中的null值 ...
- ABP框架系列之二十九:(Hangfire-Integration-延迟集成)
Introduction Hangfire is a compherensive background job manager. You can integrate ASP.NET Boilerpla ...
- Codeforces Round #539 (Div. 2) 异或 + dp
https://codeforces.com/contest/1113/problem/C 题意 一个n个数字的数组a[],求有多少对l,r满足\(sum[l,mid]=sum[mid+1,r]\), ...
- C++ MFC棋牌类小游戏day5
先整理一下之前的内容: 1.画了棋盘,把棋盘的每个点的状态都保存起来. 2.画棋子,分别用tiger类和people类画了棋子,并且保存了棋子的初始状态. 下面开始设计棋子的移动: 1.单机棋子,选中 ...
- Python设计模式运用
1 面向对象 2 创建型模式 3 结构型模式 4 行为型模式
- 17.异常(三)之 e.printStackTrace()介绍
一.关于printStackTrace()方法 public void printStackTrace()方法将此throwable对象的堆栈追踪输出至标准错误输出流,作为System.err的值.输 ...