v3虚拟系统_Go 实现海量日志收集系统(三)

2020.1.16

51Reboot 将在 2020.1.16日 21:00 为您带来分享主题

《大佬教你如何从 ES 初学者到 ES专家》

参与方式:

e3a5ce15399b5fde18819d3fa5159acb.png

来源

https://www.cnblogs.com/zhaof/p/8910761.html
再次整理了一下这个日志收集系统的框,如下图

769a5066737c7c70ca78245e21a4cd30.png

这次要实现的代码的整体逻辑为:

5eef844a17af45581487cf21b6dcd401.png

完整代码地址为: https://github.com/pythonsite/logagentetcd 介绍

高可用的分布式 key-value 存储,可以用于配置共享和服务发现 类似的项目: zookeeper 和 consul 开发语言: go 接口: 提供 restful 的接口,使用简单 实现算法: 基于 raft 算法的强一致性,高可用的服务存储目录 etcd 的应用场景:
  • 服务发现和服务注册
  • 配置中心(我们实现的日志收集客户端需要用到)
  • 分布式锁
  • master 选举
官网对 etcd 的有一个非常简明的介绍:

e2a674247af5a98de7f845996860c9bc.png

etcd 搭建: 下载地址: https://github.com/coreos/etcd/releases/ 根据自己的环境下载对应的版本然后启动起来就可以了 启动之后可以通过如下命令验证一下:
[root@localhost etcd-v3.2.18-linux-amd64]# ./etcdctl set name zhaofanzhaofan[root@localhost etcd-v3.2.18-linux-amd64]# ./etcdctl get namezhaofan[root@localhost etcd-v3.2.18-linux-amd64]#
context 介绍和使用

其实这个东西翻译过来就是上下文管理,那么 context 的作用是做什么,主要有如下两个作用:
  • 控制 goroutine 的超时
  • 保存上下文数据
通过下面一个简单的例子进行理解:
package mainimport (    "fmt"    "time"    "net/http"    "context"    "io/ioutil")type Result struct{    r *http.Response    err error}func process(){    ctx,cancel := context.WithTimeout(context.Background(),2*time.Second)    defer cancel()    tr := &http.Transport{}    client := &http.Client{Transport:tr}    c := make(chan Result,1)    req,err := http.NewRequest("GET","http://www.google.com",nil)    if err != nil{        fmt.Println("http request failed,err:",err)        return    }    // 如果请求成功了会将数据存入到管道中    go func(){        resp,err := client.Do(req)        pack := Result{resp,err}        c     }()    select{    case         tr.CancelRequest(req)        fmt.Println("timeout!")    case res :=         defer res.r.Body.Close()        out,_:= ioutil.ReadAll(res.r.Body)        fmt.Printf("server response:%s",out)    }    return}func main() {    process()}
 写一个通过 context 保存上下文,代码例子如:
package mainimport (    "github.com/Go-zh/net/context"    "fmt")func add(ctx context.Context,a,b int) int {    traceId := ctx.Value("trace_id").(string)    fmt.Printf("trace_id:%v\n",traceId)    return a+b}func calc(ctx context.Context,a, b int) int{    traceId := ctx.Value("trace_id").(string)    fmt.Printf("trace_id:%v\n",traceId)    //再将ctx传入到add中    return add(ctx,a,b)}func main() {    //将ctx传递到calc中    ctx := context.WithValue(context.Background(),"trace_id","123456")    calc(ctx,20,30)}
结合 etcd 和 context 使用

关于通过 go 连接 etcd 的简单例子: (这里有个小问题需要注意就是 etcd 的启动方式,默认启动可能会连接不上,尤其你是在虚拟你安装,所以需要通过如下命令启动: ./etcd --listen-client-urls http://0.0.0.0:2371 --advertise-client-urls http://0.0.0.0:2371 --listen-peer-urls http://0.0.0.0:2381 )
package mainimport (    etcd_client "github.com/coreos/etcd/clientv3"    "time"    "fmt")func main() {    cli, err := etcd_client.New(etcd_client.Config{        Endpoints:[]string{"192.168.0.118:2371"},        DialTimeout:5*time.Second,    })    if err != nil{        fmt.Println("connect failed,err:",err)        return    }    fmt.Println("connect success")    defer cli.Close()}
下面一个例子是通过连接 etcd,存值并取值
package mainimport (    "github.com/coreos/etcd/clientv3"    "time"    "fmt"    "context")func main() {    cli,err := clientv3.New(clientv3.Config{        Endpoints:[]string{"192.168.0.118:2371"},        DialTimeout:5*time.Second,    })    if err != nil{        fmt.Println("connect failed,err:",err)        return    }    fmt.Println("connect succ")    defer cli.Close()    ctx,cancel := context.WithTimeout(context.Background(),time.Second)    _,err = cli.Put(ctx,"logagent/conf/","sample_value")    cancel()    if err != nil{        fmt.Println("put failed,err",err)        return    }    ctx, cancel = context.WithTimeout(context.Background(),time.Second)    resp,err := cli.Get(ctx,"logagent/conf/")    cancel()    if err != nil{        fmt.Println("get failed,err:",err)        return    }    for _,ev := range resp.Kvs{        fmt.Printf("%s:%s\n",ev.Key,ev.Value)    }}
关于 context 官网也有一个例子非常有用,用于控制开启的 goroutine 的退出,代码如下:
package mainimport (    "context"    "fmt")func main() {    // gen generates integers in a separate goroutine and    // sends them to the returned channel.    // The callers of gen need to cancel the context once    // they are done consuming generated integers not to leak    // the internal goroutine started by gen.    gen := func(ctx context.Context) chan int {        dst := make(chan int)        n := 1        go func() {            for {                select {                case                     return // returning not to leak the goroutine                case dst                     n++                }            }        }()        return dst    }    ctx, cancel := context.WithCancel(context.Background())    defer cancel() // cancel when we are finished consuming integers    for n := range gen(ctx) {        fmt.Println(n)        if n == 5 {            break        }    }}
关于官网文档中的 WithDeadline 演示的代码例子:
package mainimport (    "context"    "fmt"    "time")func main() {    d := time.Now().Add(50 * time.Millisecond)    ctx, cancel := context.WithDeadline(context.Background(), d)    // Even though ctx will be expired, it is good practice to call its    // cancelation function in any case. Failure to do so may keep the    // context and its parent alive longer than necessary.    defer cancel()    select {    case After(1 * time.Second):        fmt.Println("overslept")    case Done():        fmt.Println(ctx.Err())    }}
通过上面的代码有了一个基本的使用,那么如果我们通过 etcd 来做配置管理,如果配置更改之后,我们如何通知对应的服务器配置更改,通过下面例子演示:
package mainimport (    "github.com/coreos/etcd/clientv3"    "time"    "fmt"    "context")func main() {    cli,err := clientv3.New(clientv3.Config{        Endpoints:[]string{"192.168.0.118:2371"},        DialTimeout:5*time.Second,    })    if err != nil {        fmt.Println("connect failed,err:",err)        return    }    defer cli.Close()    // 这里会阻塞    rch := cli.Watch(context.Background(),"logagent/conf/")    for wresp := range rch{        for _,ev := range wresp.Events{            fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)        }    }}
实现一个 kafka 的消费者代码的简单例子:
package mainimport (    "github.com/Shopify/sarama"    "strings"    "fmt"    "time")func main() {    consumer,err := sarama.NewConsumer(strings.Split("192.168.0.118:9092",","),nil)    if err != nil{        fmt.Println("failed to start consumer:",err)        return    }    partitionList,err := consumer.Partitions("nginx_log")    if err != nil {        fmt.Println("Failed to get the list of partitions:",err)        return    }    fmt.Println(partitionList)    for partition := range partitionList{        pc,err := consumer.ConsumePartition("nginx_log",int32(partition),sarama.OffsetNewest)        if err != nil {            fmt.Printf("failed to start consumer for partition %d:%s\n",partition,err)            return        }        defer pc.AsyncClose()        go func(partitionConsumer sarama.PartitionConsumer){            for msg := range pc.Messages(){                fmt.Printf("partition:%d Offset:%d Key:%s Value:%s",msg.Partition,msg.Offset,string(msg.Key),string(msg.Value))            }        }(pc)    }    time.Sleep(time.Hour)    consumer.Close()}
但是上面的代码并不是最佳代码,因为我们最后是通过 time.sleep 等待    goroutine  的执行,我们可以更改为通过 sync.WaitGroup 方式实现
package mainimport (    "github.com/Shopify/sarama"    "strings"    "fmt"    "sync")var (    wg sync.WaitGroup)func main() {    consumer,err := sarama.NewConsumer(strings.Split("192.168.0.118:9092",","),nil)    if err != nil{        fmt.Println("failed to start consumer:",err)        return    }    partitionList,err := consumer.Partitions("nginx_log")    if err != nil {        fmt.Println("Failed to get the list of partitions:",err)        return    }    fmt.Println(partitionList)    for partition := range partitionList{        pc,err := consumer.ConsumePartition("nginx_log",int32(partition),sarama.OffsetNewest)        if err != nil {            fmt.Printf("failed to start consumer for partition %d:%s\n",partition,err)            return        }        defer pc.AsyncClose()        go func(partitionConsumer sarama.PartitionConsumer){            wg.Add(1)            for msg := range partitionConsumer.Messages(){                fmt.Printf("partition:%d Offset:%d Key:%s Value:%s",msg.Partition,msg.Offset,string(msg.Key),string(msg.Value))            }            wg.Done()        }(pc)    }    //time.Sleep(time.Hour)    wg.Wait()    consumer.Close()}
将客户端需要收集的日志信息放到 etcd 中

关于 etcd 处理的代码为:
package mainimport (    "github.com/coreos/etcd/clientv3"    "time"    "github.com/astaxie/beego/logs"    "context"    "fmt")var Client *clientv3.Clientvar logConfChan chan string// 初始化etcdfunc initEtcd(addr []string,keyfmt string,timeout time.Duration)(err error){    var keys []string    for _,ip := range ipArrays{        //keyfmt = /logagent/%s/log_config        keys = append(keys,fmt.Sprintf(keyfmt,ip))    }    logConfChan = make(chan string,10)    logs.Debug("etcd watch key:%v timeout:%v", keys, timeout)    Client,err = clientv3.New(clientv3.Config{        Endpoints:addr,        DialTimeout: timeout,    })    if err != nil{        logs.Error("connect failed,err:%v",err)        return    }    logs.Debug("init etcd success")    waitGroup.Add(1)    for _, key := range keys{        ctx,cancel := context.WithTimeout(context.Background(),2*time.Second)        // 从etcd中获取要收集日志的信息        resp,err := Client.Get(ctx,key)        cancel()        if err != nil {            logs.Warn("get key %s failed,err:%v",key,err)            continue        }        for _, ev := range resp.Kvs{            logs.Debug("%q : %q\n",  ev.Key, ev.Value)            logConfChan string(ev.Value)        }    }    go WatchEtcd(keys)    return}func WatchEtcd(keys []string){    // 这里用于检测当需要收集的日志信息更改时及时更新    var watchChans []clientv3.WatchChan    for _,key := range keys{        rch := Client.Watch(context.Background(),key)        watchChans = append(watchChans,rch)    }    for {        for _,watchC := range watchChans{            select{            case wresp :=                 for _,ev:= range wresp.Events{                    logs.Debug("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)                    logConfChan string(ev.Kv.Value)                }            default:            }        }        time.Sleep(time.Second)    }    waitGroup.Done()}func GetLogConf()chan string{    return logConfChan}
同样的这里增加对了限速的处理,毕竟日志收集程序不能影响了当前业务的性能,所以增加了 limit.go 用于限制速度:
package mainimport (    "time"    "sync/atomic"    "github.com/astaxie/beego/logs")type SecondLimit struct {    unixSecond int64    curCount int32    limit int32}func NewSecondLimit(limit int32) *SecondLimit {    secLimit := &SecondLimit{        unixSecond:time.Now().Unix(),        curCount:0,        limit:limit,    }    return secLimit}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))}func (s *SecondLimit) Wait()bool {    for {        sec := time.Now().Unix()        if (sec == atomic.LoadInt64(&s.unixSecond)) && s.curCount == s.limit {            time.Sleep(time.Microsecond)            logs.Debug("limit is running,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    }}
小结

这次基本实现了日志收集的前半段的处理,后面将把日志扔到 es 中,并最终在页面上呈现51Reboot 课程信息最新一期 Golang 课程

开课时间:2020.2.16

1、GO 并发

  • goroutine

  • channel

  • 有缓存 chan 和无缓存 chan,以及通信的一些细节

  • 常见并发模式

  • 锁与条件竞争

  • 实战

 并发 Web 爬虫

2、Beego 实战

  • Go Web 浅析

HTTP 协议 

net/http

  • Gin/Beego 框架对比介绍

  • 请求校验包的使用

  • 中间件 middleware ORM

  • 日志模块

3、堡垒机权限管理系统

  • 架构设计

  • HTTP 协议 加密传输

  • 分布式 Agent/Server 开发

  • Beego 可视化 Web 界面统管

  • 思考:如何对接服务树系统

4、分布式监控

  • 架构设计

  • GRPC 传输

  • Etcd 服务注册发现

  • Influxdb 存储

  • Granfan 展示

运维自动化进阶课程

实战项目一:SQL 自动化上线平台

  • 手动 VS 自动的现状对比

  • Mysql、Inception、SQLAdvisor 讲解

  • 用户权限设计、执行流程梳理、平台登录双因子安全认证

  • 敏感配置加解密实现、人员/数据库配置设计,整体代码实现

实战项目:公有云管理平台
  • 通过 API 对[Aws、阿里云、腾讯云、青云、百度云]管理

  • 公有云账单、比价、资源管理

实战项目:任务管理系统
  • Ansible 简介及常用场景分析

  • Ansible API 二次开发入门

  • 基于 Ansible Playbook API 快速实现任务管理系统

实战项目:工单系统
  • 工作中流程规范的设计思路

  • 典型工单系统的实现原理-状态机

实战项目:代码管理系统及发布平台
  • 基于 Gitlab + Jenkins + DevOps 平台实现 CI/CD 的设计思路

  • 结合运维平台流程规范实现持续集成与交付

  • 结合 ELK 的日志分析平台,实现代码上线运维无人值守

docker+k8s 课程

第一阶段:Docker 基础与进阶

第二阶段:Kubernetes 基础

第三阶段:Pod 与生命周期管理

  • Kubernetes 集群资源管理与调度管理

  • Kubernetes 控制器和常用资源对象

第四阶段:Kubernetes 服务发现

  • 持久化存储

第五阶段:Helm 包管理工具

第六阶段:Kubernetes 集群网络

  • Kubernetes 集群网络常用方案比较及选型建议

  • Flannel 网络组件详解

  • Flannel 网络组件配置及应用

  • Flannel 生产环境应用经验

  • Calico 网络组件详解

  • Calico 网络组件配置及应用

  • ......

第七阶段:Kubernetes 集群监控

  • Prometheus 介绍

  • 部署 Prometheus

  • 监控 Kubernetes 集群及应用

  • NodeExporter 的安装使用

  • Prometheus的自动发现

  • Kubernetes常用资源对象监控

  • Grafana的安装与使用

  • Grafana的插件与监控

  • .......

第八阶段:日志收集

  • 日志收集架构

  • Elasticsearch 集群

  • Kibana 可视化组件

  • Fluentd 采集组件

  • 生产环境采集日志方案详解

第九、十阶段:DevOps

  • 动态 Jenkins Slave

  • Jenkins Pipeline

  • Jenkins Blue Ocean

  • Harbor 详解

  • Gitlab 安装与使用

  • Gitlab CI Runner

  • Gitlab CI 示例

  • Kubernetes 开源管理平台

  • 完整 devops 项目实例

3eee5f0e081b3fe15f7c93eafd085e64.png

关注我们 ,一起成长吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值