日志库
介绍
无论是软件开发的调试阶段还是软件上线之后的运行阶段,日志一直都是非常重要的一个环节,我们也应该养成在程序中记录日志的好习惯。
Go语言内置的log
包实现了简单的日志服务。本文介绍了标准库log
的基本使用。
使用Logger
log包定义了Logger类型,该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger,可以通过调用函数Print系列
(Print|Printf|Println)、Fatal系列
(Fatal|Fatalf|Fatalln)、和Panic系列
(Panic|Panicf|Panicln)来使用,比自行创建一个logger对象更容易使用。
例如,我们可以像下面的代码一样直接通过log
包来调用上面提到的方法,默认它们会将日志信息打印到终端界面
package main
import (
"log"
)
func main() {
log.Println("这是一条很普通的日志。")
v := "很普通的"
log.Printf("这是一条%s日志。\n", v)
log.Fatalln("这是一条会触发fatal的日志。")
log.Panicln("这是一条会触发panic的日志。")
}Copy to clipboardErrorCopied
编译并执行上面的代码会得到如下输出:
2017/06/19 14:04:17 这是一条很普通的日志。
2017/06/19 14:04:17 这是一条很普通的日志。
2017/06/19 14:04:17 这是一条会触发fatal的日志Copy to clipboardErrorCopied
logger会打印每条日志信息的日期、时间,默认输出到系统的标准错误。Fatal系列函数会在写入日志信息后调用os.Exit(1)。Panic系列函数会在写入日志信息后panic。
日志输出到文件中
我们正常的日志文件,是存储在文件中的,因此我们可以使用以下的方式,将日志存储在文件中
func main() {
fileObj, err := os.OpenFile("./xx.log", os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("open file failed, err : %v \n", err)
return
}
// 设置log的输出路径
log.SetOutput(fileObj)
for {
log.Println("这是一条测试日志")
time.Sleep(time.Second * 3)
}
}Copy to clipboardErrorCopied
日志库的简单实现
- 支持往不同的地方输出日志
- 日志分级别
- debug
- Trace
- info
- warning
- Error
- Fatal:严重错误
- 日志要支持开关控制,比如说开发的时候什么级别都能输出,但是上线之后只有INFO级别往下才能输出
- 日志要有时间、行号、文件名、日志级别、日志信息
- 日志文件要切割
package main
import (
"errors"
"fmt"
"path"
"runtime"
"strings"
"time"
)
// 往终端写日志相关内容
type LogLevel uint16
// 定义日志级别
const(
UNKNOWN LogLevel = iota // 0
DEBUG
TRACE
INFO
WARNING
ERROR
FATAL
)
// Logger日志结构体
type Logger struct {
Level LogLevel
}
func parseLogLevel(s string) (LogLevel, error) {
s = strings.ToLower(s)
switch s {
case "debug":
return DEBUG, nil
case "trace":
return TRACE, nil
case "info":
return INFO, nil
case "warning":
return WARNING, nil
case "error":
return ERROR, nil
case "fatal":
return FATAL, nil
default:
err := errors.New("无效的日志级别")
return UNKNOWN, err
}
}
func getLogLevelStr(logLevel LogLevel) (string) {
switch logLevel {
case DEBUG:
return "debug"
case TRACE:
return "trace"
case INFO:
return "info"
case WARNING:
return "warning"
case ERROR:
return "error"
case FATAL:
return "fatal"
default:
return "unknown"
}
}
// 获取函数名、文件名、行号
// skip表示隔了几层
func getInfo(skip int)(funcName string, fileName string, lineNo int) {
// pc:函数信息
// file:文件
// line:行号,也就是当前行号
pc, file, line, ok := runtime.Caller(skip)
if !ok {
fmt.Printf("runtime.Caller() failed, err:%v \n")
return
}
funName := runtime.FuncForPC(pc).Name()
return funName, path.Base(file), line
}
// Logger构造方法
func NewLog(levelStr string) Logger {
level, err := parseLogLevel(levelStr)
if err != nil {
panic(err)
}
// 构造了一个Logger对象
return Logger{
Level: level,
}
}
// 判断啥级别的日志可以输出是否输出
func (l Logger) enable(logLevel LogLevel) bool {
return logLevel >= l.Level
}
func printLog(lv LogLevel, msg string) {
now := time.Now().Format("2006-01-02 15:04:05")
// 拿到第二层的函数名
funcName, filePath, lineNo := getInfo(3)
fmt.Printf("[%s] [%s] [%s:%s:%d] %s \n", now, getLogLevelStr(lv),filePath, funcName, lineNo, msg)
}
func (l Logger) Debug(msg string) {
if l.enable(DEBUG) {
printLog(DEBUG, msg)
}
}
func (l Logger) TRACE(msg string) {
if l.enable(TRACE) {
printLog(TRACE, msg)
}
}
func (l Logger) Info(msg string) {
if l.enable(INFO) {
printLog(INFO, msg)
}
}
func (l Logger) Warning(msg string) {
if l.enable(WARNING) {
printLog(WARNING, msg)
}
}
func (l Logger) Error(msg string) {
if l.enable(ERROR) {
printLog(ERROR, msg)
}
}
func (l Logger) Fatal(msg string) {
if l.enable(FATAL) {
printLog(FATAL, msg)
}
}
func main() {
log := NewLog("ERROR")
for {
log.Debug("这是一条DEBUG日志")
log.Info("这是一条INFO日志")
log.Warning("这是一条WARNING日志")
log.Error("这是一条ERROR日志")
log.Fatal("这是一条FATAL日志")
fmt.Println("----------------")
time.Sleep(time.Second)
}
}
日志收集项目架构设计及Kafka介绍
项目背景
每个业务系统都有日志,当系统出现问题时,需要通过日志信息来定位和解决问题。当系统机器比较少时,登陆到服务器上查看即可满足当系统机器规模巨大,登陆到机器上查看几乎不现实(分布式的系统,一个系统部署在十几台机器上)
解决方案
把机器上的日志实时收集,统一存储到中心系统。再对这些日志建立索引,通过搜索即可快速找到对应的日志记录。通过提供一个界面友好的web页面实现日志展示与检索。
面临的问题
实时日志量非常大,每天处理几十亿条。日志准实时收集,延迟控制在分钟级别。系统的架构设计能够支持水平扩展。
业界方案
ELK
- AppServer:跑业务的服务器
- Logstash Agent:
- Elastic Search Cluster
- Kibana Server:数据可视化
- Browser:浏览器
ELK方案的问题
- 运维成本高,每增加一个日志收集项,都需要手动修改配置
- 使用etcd来管理被收集的日志项
- 监控缺失,无法精准获取logstash的状态
- 无法做到定制化开发与维护
日志收集系统架构设计
架构设计
通过etcd做一个配置中心的概念,它是用go写的,是可以用来替代Zookeeper的
LogAgent收集日志,然后将其发送到Kafka中,Kafka既可以作为消息队列,也可以做到消息的存储组件
然后Log transfer就将Kafka中的日志记录取出来,进行处理,然后写入到ElasticSearch中,然后将对应的日志
最后通过Kibana进行可视化展示,SysAgent是用来采集系统的日志信息(或者使用 普罗米修斯)
组件介绍
- LogAgent:日志收集客户端,用来收集服务器上的日志
- Kafka:高吞吐量的分布式队列(Linkin开发,Apache顶级开源项目)
- ElasticSearch:开源的搜索引擎,提供基于HTTP RESTful的web接口
- Kibana:开源的ES数据分析和可视化工具
- Hadoop:分布式计算框架,能够对大量数据进行分布式处理的平台
- Storm:一个免费并开源的分布式实时计算框架
将学到的技能
- 服务端agent开发
- 后端服务组件开发
- Kafka和Zookeeper的使用
- ES和Kibana使用
- etcd的使用(配置中心,配置共享)
消息队列的通信模型
点对点模式 queue
消息生产者发送到queue中,然后消息消费者从queue中取出并消费信息,一条消息被消费以后,queue中就没有了,不存在重复消费的问题
发布/订阅 topic
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有的订阅者消费(类似于关注了微信公众号的人都能收到推送的文章)。
补充:发布订阅模式下,当发布者消息量很大时,显然单个订阅者的处理能力是不足的。实际上现实场景中多个订阅者节点组成一个订阅组负载均衡消费topic消息即分组订阅,这样订阅者很容易实现消费能力线扩展。可以看成是一个topic下有多个Queue,每个Queue是点对点的方式,Queue之间是发布订阅方式。
Kafka
Apache Kafka由著名职业社交公司Linkedin开发,最初是被设计用来解决LinkedIn公司内部海量日志传输问题,Kafka使用Scala语言编写,于2011年开源并进入Apache孵化器,2012年10月正式毕业,现在为Apache顶级项目
介绍
Kafka是一个分布式数据流平台,可以运行在单台服务器上,也可以在多台服务器上部署形成集群。它提供了发布和订阅功能,使用这可以发送数据到Kafka中,也可以从Kafka中读取数据(以便进行后续处理)。Kafka具有高吞吐、低延迟、高容错等特点。
Kafka的架构图
- Producer:Producer即生产者,消息的产生者,是消息的入口。
- kafka cluster:kafka集群,一台或多台服务器组成
- Broker:Broker是指部署了Kafka实例的服务器节点。每个服务器上有一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的编号,如图中的broker-0、broker-1等…
- Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。实际应用中通常是一个业务线建一个topic。
- Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的文件夹!
- Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。
- Consumer:消费者,即消息的消费方,是消息的出口。
工作流程
我们看上面的架构图中,produce就是生产者,是数据的入口。Producer在写入数据的时候会把数据写入到Leader中,不会直接将数据写入follower!那leader怎么找呢?写入流程又是怎么样的呢?我们看下图
- 生产者从Kafka集群获取分区leader信息
- 生产者将消息发送给leader
- leader将消息写入本地磁盘
- follower从leader拉取消息数据
- follower将消息写入本地磁盘后向leader发送ACK
- leader收到所有的follower的ACK之后向生产者发送ACK
选择partition的原则
那在kafka中,如果某低opic有多个partition,producer又怎么知道该将数据发往哪个partition呢? kafka中有几个原则:
- partition在写入的时候可以指定需要写入的partition,如果有指定,则写入对应的partition。
- 如果没有指定partition,但是设置了数据的key,则会根据key的值hash出一个partition。
- 如果既没指定partition,又没有设置key,则会采用轮询方式,即每次取一小段时间的数据写入某个partition,下一小段的时间写入下一个partition。
ACK应答机制
producer在向kafka写入消息的时候,可以设置参数来确定是否确认kafka接收到数据,这个参数可设置的值为0、1、all。
- 0:代表producer往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效I率最高。
- 1:代表producer往集群发送数据只要leader应答就可以发送下一条,只确保leader发送成功。
- all:代表producer往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条,确保leader发送成功和所有的副本都完成备份。安全性最高,但是效率最低。
最后要注意的是,如果往不存在的topic写数据,kafka会自动创建topic,partition和replication的数量默认配置都是1。
Topic和数据日志
topic是同一类别的消息记录(record)的集合。在kafka中,一个主题通常有多个订阅者。对于每个主题、Kafka集群维护了一个分区数据日志文件结构如下:
每个partition都是一个有序并且不可变的消息记录集合。当新的数据写入时,就被追加到partition的末尾。在每个partition中,每条消息都会被分配一个顺序的唯一标识,这个标识被称为offset,即偏移量。注意,kafka只保证在他同一个partition内部消息是有序的,在不同partition之间,并不能保证消息有序。
Kafka可以配置一个保留期限,用来标识日志会在Kafka集群中保留多长时间。Kafka集群会保留在保留期限内所有发布的消息,不管这些消息是否被消费过,比如保留期限设置为两天,那么数据被发布到Kafka集群的两天以内,所有的这些数据都可以被消费,当超过两天,这些数据将会被清空。以便为后续的数据腾出空间,由于Kafka会将数据进行持久化存储(即写入到硬盘上),所以保留的数据大小可以设置为一个比较大的值。
Partition结构
Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件三个文件,其中.log文件就是实际存储message的地方,而.index和.timeindex文件为索引文件,用于检索消息。
消费数据
多个消费者实例可以组成一个消费者组,并用一个标签来标识这个消费者组。一个消费者组中的不同消费者实例可以运行在不同的进程甚至不同的服务器上。
如果所有的消费者实例都在同一个消费者组中,那么消息记录会被很好的均衡的发送到每个消费者实例。
如果所有的消费者实例都在不同的消费者组,那么每一条消息记录会被广播到每一个消费者实例。
上面是kafka集群,下面就是消费者组
举个例子,如上图所示一个两个节点的Kafka集群上拥有一个四个partition(PO-P3)的topic。有两个消费者组都在消费这个topic中的数据,消费者组A有两个消费者实例,消费者组B有四个消费者实例。
从图中我们可以看到,在同一个消费者组中,每个消费者实例可以消费多个分区,但是每个分区最多只能被消费者组中的一个实例消费。也就是说,如果有一个4个分区的主题,那么消费者组中最多只能有4个消费者实例去消费,多出来的都不会被分配到分区。其实这也很好理解,如果允许两个消费者实例同时消费同一个分区,那么就无法记录这个分区被这个消费者组消费的offset了。如果在消费者组中动态的上线或下线消费者,那么Kafka集群会自动调整分区与消费者实例间的对应关系。
使用场景
上面介绍了Kafka的一些基本概念和原理,那么Kafka可以做什么呢?目前主流使用场景基本如下:
消息队列(MQ)
在系统架构设计中,经常会使用消息队列(Message Queue)——MQ。MQ是一种跨进程的通信机制,用于上下游的消息传递,使用MQ可以使上下游解耦,消息发送上游只需要依赖MQ,逻辑上和物理上都不需要依赖其他下游服务。MQ的常见使用场景如流量削峰、数据驱动的任务依赖等等。在MQ领域,除了Kafka外还有传统的消息队列如ActiveMQ和RabbitMQ等。
追踪网站活动
Kafka最出就是被设计用来进行网站活动(比如PV、UV、搜索记录等)的追踪。可以将不同的活动放入不同的主题,供后续的实时计算、实时监控等程序使用,也可以将数据导入到数据仓库中进行后续的离线处理和生成报表等。
Metrics
Kafka 经常被用来传输监控数据。主要用来聚合分布式应用程序的统计数据,将数据集中后进行统一的分析和展示等。
日志聚合
很多人使用Kafka作为日志聚合的解决方案。日志聚合通常指将不同服务器上的日志收集起来并放入一个日志中心,比如一台文件服务器或者HDFS中的一个目录,供后续进行分析处理。相比于Flume和Scribe等日志聚合工具,Kafka具有更出色的性能。
Kafka安装和启动
下载
下载地址:https://www.apache.org/dyn/closer.cgi?path=/kafka/2.6.0/kafka_2.12-2.6.0.tgz
安装
将下载好的压缩包解压到本地,注意Kafka需要先安装JDK环境
修改配置
我们首先找到Kafka的配置文件 server.properties
然后修改日志存放位置
############################# Log Basics #############################
# A comma separated list of directories under which to store log files
log.dirs=D:/tmp/kafka-logsCopy to clipboardErrorCopied
启动
修改完成后,我们即可启动Kafka了,kafka默认端口号是9092
然后到 bin\windows目录下,输入下面的脚本
.\kafka-server-start.bat ..\..\config\server.propertiesCopy to clipboardErrorCopied
Kafka启动成功
Zookeeper启动
注意,我们安装的Kafka里面,就包含了所需的Zookeeper配置文件,因此我们只需要修改配置即可
找到Zookeeper.properties配置文件,修改数据目录
修改完成后,我们到 bin\windows目录下
启动脚本
.\zookeeper-server-start.bat ..\..\config\zookeeper.propertiesCopy to clipboardErrorCopied
启动完成后,将占用 2181端口号
LogAgent的工作流程
- 读日志
- 往kafka中写日志
首先我们需要下载tailf,使用下面的命令
go get github.com/hpcloud/tailCopy to clipboardErrorCopied
然后编写测试样例
package main
import (
"fmt"
"github.com/hpcloud/tail"
"time"
)
// tailf的用法
func main() {
// 需要记录的日志文件
fileName := "./my.log"
//
config := tail.Config{
ReOpen: true, // 重新打开,日志文件到了一定大小,就会分裂
Follow: true, // 是否跟随
Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个位置开始读
MustExist: false, // 是否必须存在,如果不存在是否报错
Poll: true, //
}
tails, err := tail.TailFile(fileName, config)
if err != nil {
fmt.Println("tail file failed, err:", err)
return
}
var(
line *tail.Line
ok bool
)
for {
// 从tails中一行一行的读取
line, ok = <- tails.Lines
if !ok {
fmt.Println("tail file close reopen, filename:%s\n", tails.Filename)
time.Sleep(time.Second)
continue
}
fmt.Println("line", line.Text)
}
}
Copy to clipboardErrorCopied
但是我们在启动的时候,又出错了
cannot find module providing package gopkg.in/fsnotify.v1Copy to clipboardErrorCopied
我们定位到源码目录,因为 opkg.in/fsnotify.v1的包改名了,所以我们需要修改两个文件,inotify.go 和 inotify_tracker.go
将里面出错的文件,替换成下面的这个文件即可
log agent开发
下载安装
go get github.com/Shopify/saramaCopy to clipboardErrorCopied
sarama V1.20之后的版本加入了zstd压缩算法,需要用到cgo,在Windows平台编译时会提示类似如下错误:
exec: "gcc":executable file not found in %PATH%Copy to clipboardErrorCopied
所以在Windows平台请使用v1.19版本的sarama,因此我们需要使用下面命令创建一个go.mod文件
go mod init kafkaDemoCopy to clipboardErrorCopied
然后在文件里面配置一下版本号 v1.19.0
go 1.14
require (
github.com/Shopify/sarama v1.19.0
)Copy to clipboardErrorCopied
然后在项目的目录下,下载依赖
go mod downloadCopy to clipboardErrorCopied
下载完成后,我们就可以写测试代码了
package main
import (
"fmt"
"github.com/Shopify/sarama"
)
// 基于sarama第三方库开发的Kafka client
func main() {
config := sarama.NewConfig()
// tailf包使用,发送完数据需要 leader 和 follow都确定
config.Producer.RequiredAcks = sarama.WaitForAll
// 新选出一个partition
config.Producer.Partitioner = sarama.NewRandomPartitioner
// 成功交付的消息将在 success channel返回
config.Producer.Return.Successes = true
msg := &sarama.ProducerMessage{}
msg.Topic = "web_log"
msg.Value = sarama.StringEncoder("this is a test log")
// 连接kafka,可以连接一个集群
client, err := sarama.NewSyncProducer([]string{"127.0.0.1:9092"}, config)
if err != nil {
fmt.Println("producer closed, err: ", err)
}
fmt.Println("连接kafka成功!")
// 定义延迟关闭
defer client.Close()
// 发送消息
pid, offset, err := client.SendMessage(msg)
if err != nil {
fmt.Println("send msg failed, err:", err)
return
}
fmt.Printf("pid:%v offset:%v \n", pid, offset)
}Copy to clipboardErrorCopied
LogAgent编码
首先我们需要创建一个 kafka.go 用来初始化kafka和发送消息到kafka
package kafka
import (
"fmt"
"github.com/Shopify/sarama"
)
// 专门往kafka写日志的模块
var (
// 声明一个全局连接kafka的生产者
client sarama.SyncProducer
)
// 初始化client
func Init()(err error) {
config := sarama.NewConfig()
// tailf包使用,发送完数据需要 leader 和 follow都确定
config.Producer.RequiredAcks = sarama.WaitForAll
// 新选出一个partition
config.Producer.Partitioner = sarama.NewRandomPartitioner
// 成功交付的消息将在 success channel返回
config.Producer.Return.Successes = true
msg := &sarama.ProducerMessage{}
msg.Topic = "web_log"
msg.Value = sarama.StringEncoder("this is a test log")
// 连接kafka,可以连接一个集群
client, err = sarama.NewSyncProducer([]string{"127.0.0.1:9092"}, config)
if err != nil {
fmt.Println("producer closed, err: ", err)
}
fmt.Println("Kafka初始化成功")
return err
}
// 发送消息到Kafka
func SendToKafka(topic, data string) {
msg := &sarama.ProducerMessage{}
msg.Topic = topic
msg.Value = sarama.StringEncoder(data)
// 发送到kafka
pid, offset, err := client.SendMessage(msg)
if err != nil {
fmt.Println("send msg failed, err:", err)
return
}
fmt.Println("发送消息:", data)
fmt.Printf("发送成功~ pid:%v offset:%v \n", pid, offset)
}Copy to clipboardErrorCopied
然后我们在创建一个 taillog.go文件,用来记录日志
package taillog
import (
"fmt"
"github.com/hpcloud/tail"
)
// 定义全局对象
var (
// 声明一个全局连接kafka的生产者
tailObj *tail.Tail
)
// 专门从日志文件收集日志的模块
func Init(fileName string)(err error ){
// 定义配置文件
config := tail.Config{
ReOpen: true, // 重新打开,日志文件到了一定大小,就会分裂
Follow: true, // 是否跟随
Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个位置开始读
MustExist: false, // 是否必须存在,如果不存在是否报错
Poll: true, //
}
tailObj, err = tail.TailFile(fileName, config)
if err != nil {
fmt.Println("tail file failed, err:", err)
return
}
return err
}
// 读取日志,返回一个只读的chan
func ReadChan() <-chan *tail.Line {
return tailObj.Lines
}Copy to clipboardErrorCopied
最后我们创建main.go作为启动类
package main
import (
"LogDemo/kafka"
"LogDemo/taillog"
"fmt"
"time"
)
// logAgent入口程序
func run() {
// 1.读取日志
for {
select {
case line := <-taillog.ReadChan():
{
// 2.发送到kafka
kafka.SendToKafka("web_log", line.Text)
}
default:
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 1. 初始化kafka连接
err := kafka.Init()
if err != nil {
fmt.Printf("init Kafka failed, err:%v \n", err)
return
}
// 2. 打开日志文件,准备收集
err = taillog.Init("./my.log")
if err != nil {
fmt.Printf("Init taillog failed, err: %v \n", err)
return
}
// 3.执行业务逻辑
run()
}Copy to clipboardErrorCopied
最后我们启动main.go,然后往文件里面插入内容,然后就会将日志记录发送到kafka中
最后通过下面的脚本,来进行kafka的消息的消费
.\kafka-console-consumer.bat --bootstrap-server=127.0.0.1:9092 --topic=web_log --from-beginningCopy to clipboardErrorCopied
然后就开始消费kafka中的消息
存在的问题
上述的代码还存在硬编码的问题,我们将通过配置文件将一些信息配置出来,这样能够提高代码的扩展性,这里我们用的是ini文件,创建一个 config.ini文件,填入配置信息
[kafka]
address=127.0.0.1:9092
topic=web_log
[taillog]
path:=./my.logCopy to clipboardErrorCopied
引入依赖
我们需要使用go-ini的依赖,来对我们的配置文件进行解析 ,go-ini官网
首先下载依赖
go get gopkg.in/ini.v1Copy to clipboardErrorCopied
然后通过下面的方式进行读取
// 0. 加载配置文件
cfg, err := ini.Load("./conf/config.ini")
if err != nil {
fmt.Printf("Fail to read file: %v", err)
os.Exit(1)
}
// 典型读取操作,默认分区可以使用空字符串表示
fmt.Println("kafka address:", cfg.Section("kafka").Key("address").String())
fmt.Println("kafka topic:", cfg.Section("kafka").Key("topic").String())
fmt.Println("taillog path:", cfg.Section("taillog").Key("path").String())Copy to clipboardErrorCopied
优化后的main.go如下所示
package main
import (
"LogDemo/kafka"
"LogDemo/taillog"
"fmt"
"gopkg.in/ini.v1"
"os"
"time"
)
// logAgent入口程序
func run() {
// 1.读取日志
for {
select {
case line := <-taillog.ReadChan():
{
// 2.发送到kafka
kafka.SendToKafka("web_log", line.Text)
}
default:
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 0. 加载配置文件
cfg, err := ini.Load("./conf/config.ini")
if err != nil {
fmt.Printf("Fail to read file: %v", err)
os.Exit(1)
}
// 典型读取操作,默认分区可以使用空字符串表示
fmt.Println("kafka address:", cfg.Section("kafka").Key("address").String())
fmt.Println("kafka topic:", cfg.Section("kafka").Key("topic").String())
fmt.Println("taillog path:", cfg.Section("taillog").Key("path").String())
// 1. 初始化kafka连接
address := []string{cfg.Section("kafka").Key("address").String()}
topic := cfg.Section("taillog").Key("path").String()
err = kafka.Init(address, topic)
if err != nil {
fmt.Printf("init Kafka failed, err:%v \n", err)
return
}
// 2. 打开日志文件,准备收集
err = taillog.Init(cfg.Section("taillog").Key("path").String())
if err != nil {
fmt.Printf("Init taillog failed, err: %v \n", err)
return
}
// 3.执行业务逻辑
run()
}Copy to clipboardErrorCopied
最终版本
上述的方式,还是存在一些问题,就是配置信息不能传递,只能在main方法里面,那么我们可以在定义一个结构体,conf.go
package conf
type AppConf struct {
KafkaConf `ini:"kafka"`
TaillogConf `ini:"taillog"`
}
type KafkaConf struct {
Address string `ini:"address"`
Topic string `ini:"topic"`
}
type TaillogConf struct {
FileName string `ini:"filename"`
}Copy to clipboardErrorCopied
然后原来的main.go改为
package main
import (
"LogDemo/conf"
"LogDemo/kafka"
"LogDemo/taillog"
"fmt"
"gopkg.in/ini.v1"
"time"
)
var (
cfg = new(conf.AppConf)
)
// logAgent入口程序
func run() {
// 1.读取日志
for {
select {
case line := <-taillog.ReadChan():
{
// 2.发送到kafka
kafka.SendToKafka(cfg.Topic, line.Text)
}
default:
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 0. 加载配置文件
// 方式2
err := ini.MapTo(&cfg, "./conf/config.ini")
if err != nil {
fmt.Printf("load ini failed, err: %v \n", err)
return
}
fmt.Println("读取到的配置信息", cfg)
// 1. 初始化kafka连接
address := []string{cfg.Address}
topic := cfg.Topic
err = kafka.Init(address, topic)
if err != nil {
fmt.Printf("init Kafka failed, err:%v \n", err)
return
}
// 2. 打开日志文件,准备收集
err = taillog.Init(cfg.FileName)
if err != nil {
fmt.Printf("Init taillog failed, err: %v \n", err)
return
}
// 3.执行业务逻辑
run()
}
etcd介绍
前言
etcd:目前比较火的开源库,docker和k8s都是使用的它
目标:使用etcd优化日志收集项目
介绍
etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现,类似项目有Zookeeper和consul
etcd具有以下特点
-
完全复制:集群中的每个节点都可以使用完整的存档
-
高可用性:Etcd可用于避免硬件的单点故障或网络问题(选择出另外的leader)
-
一致性:每次读取都会返回跨多主机的最新写入
-
简单:包括一个定义良好、面向用户的API(gRPC)
-
安全:实现了带有可选的客户端证书身份验证的自动化TLS
-
快速:每秒10000次写入的基准速度
-
可靠:使用
Raft
算法实现了强一致性,高可用的服务存储目录
- Raft协议:选举、日志复制机制、异常处理(脑裂)、Zookeeper的zad协议的区别
etcd应用场景
服务发现
服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听udp或tcp端口,并且通过名字就可以查找和连接。
配置中心
将一些配置信息放到etcd上进行集中管理。这类场景的使用方式通常是这样的:应用启动的时候,主动从etcd获取一次配置信息,同时,在etcd节点上注册一个Watcher并等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,以此达到获取最新配置信息的目的。
分布式锁
因为etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致性的,所以很容易实现分布式锁。锁服务有两种使用方式,一种是保持独占,二是控制时序。
- 保持独占即所有获取锁的用户最终只有一个可以得到。etcd为此提供了一套实现分布式锁原子操作CAS(CompareAndSwap)的API。通过设置preExist值,可以保证在多个节点同时去创建某个目录时,只有一个成功,而创建成功的用户就可以认为是获得了锁。
- 控制时序,即所有想要获得锁的用户都会被安排执行,但是获得锁的顺序也是全局唯一的,同时决定了执行顺序。etcd为此也提供了一套API(自动创建有序键),对一个目录建值时指定为POST动作,这样etcd会自动在目录下生成一个当前最大的键值。此时这些键的值就是客户端的时序,而这些键中的存储的值可以代表客户端的编号。
上图就是三个同时来竞争锁,最后只有一个获取到了锁
为什么使用etcd而不用Zookeeper?
Zookeeper存在的问题
etcd 实现的这些功能,ZooKeeper都能实现。那么为什么要用etcd 而非直接使用ZooKeeper呢?相较之下,ZooKeeper有如下缺点:
- 复杂:ZooKeeper的部署维护复杂,管理员需要掌握一系列的知识和技能;而Paxos 强一致性算法也是素来以复杂难懂而闻名于世;另外,ZooKeeper的使用也比较复杂,需要安装客户端,官方只提供了Java和C两种语言的接口。
- Java编写:这里不是对Java有偏见,而是Java本身就偏向于重型应用,它会引入大量的依赖。而运维人员则普遍希望保持强一致、高可用的机器集群尽可能简单,维护起来也不易出错。
- 发展缓慢:Apache 基金会项目特有的"Apache Way”在开源界饱受争议,其中一大原因就是由于基金会庞大的结构以及松散的管理导致项目发展缓慢。
etcd的优势
而etcd作为一个后起之秀,其优点也很明显
简单:使用Go语言编写部署简单;使用HTTP作为接口使用简单;使用Raft 算法保证强一致性让用户易于理解。
数据持久化:etcd 默认数据一更新就进行持久化。
安全:etcd 支持SSL客户端安全认证。
最后,etcd作为一个年轻的项目,真正告诉迭代和开发中,这既是一个优点,也是一个缺点。优点是它的未来具有无限的可能性,缺点是无法得到大项目长时间使用的检验。然而,目前CoreOS、Kubernetes 和CloudFoundry等知名项目均在生产环境中使用了etcd,所以总的来说,etcd值得你去尝试。
etcd架构
从etcd的架构图中我们可以看到,etcd主要分为四个部分
- HTTP Server:用于处理用户发送的API请求以及其他etcd节点的同步与心跳信息请求
- Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现
- Raft:Raft强一致性算法的具体实现,是etcd的核心
- WAL:Write Ahead Log(预写式日志),是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd就通过WAL进行持久化存储。WAL中,所有的数据提交前都会实现记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry表示存储的具体日志内容。
etcd集群
etcd作为一个高可用键值存储系统,天生就是为集群化而设计的,由于Raft算法在做决策时需要多数节点投票,所以etcd一般部署集群推荐奇数个节点,推荐的数量为3、5或者7个节点构成一个集群。
搭建一个3节点集群示例
在每个etcd节点指定集群成员,为了区分不同的集群最好同时配置一个独一无二的token。
下面是提前定义好的集群信息,其中n1、n2和n3表示3个不同的etcd节点。
在n1这台机器上执行以下命令来启动etcd:
在n2这台机器上执行以下命令启动etcd:
在n3这台机器上执行以下命令启动etcd:
etcd官网提供了一个公网访问的etcd存储地址,你可以通过如下命令得到etcd服务的目录,并把它作为-discovery参数使用
etcd部署
下载
找到对应的 Github官网,到相应的releases,找到windows平台的压缩包进行下载
解压完成后的目录
启动
双击etcd.exe就是启动了etcd。其它平台解压之后在bin目录下找etcd可执行文件。
默认会在2379端口监听客户端通信,在2380端口监听节点间的通信。
etcdctl.ext可以理解为一个客户端或者本机etcd的控制端
连接
默认的etcdctrl使用的是v2版本的命令,我们需要设置环境变量ETCDCTL_API=3来使用v3版本的API,而默认的也就是环境变量为ETCDCTL_API=2是使用v2版本的API
修改环境变量指定使用API的版本
SET_ETCDCTL_API=3Copy to clipboardErrorCopied
简单使用
put:设置
.\etcdctl.exe --endpoints=http://127.0.0.1:2379 put baodelu "dsb"Copy to clipboardErrorCopied
显示设置成功~
get:获取
.\etcdctl.exe --endpoints=http://127.0.0.1:2379 get baodeluCopy to clipboardErrorCopied
del:删除
.\etcdctl.exe --endpoints=http://127.0.0.1:2379 del baodeluCopy to clipboardErrorCopied
Go操作etcd
安装依赖
go get go.etcd.io/etcd/clientv3Copy to clipboardErrorCopied
put和get操作
put命令用来设置键值对数据,get命令用来根据key获取值
package main
import (
"context"
"fmt"
"go.etcd.io/etcd/clientv3"
"time"
)
func main() {
cli, err := clientv3.New(clientv3.Config {
Endpoints: []string{"127.0.0.1:2379"}, // etcd的节点,可以传入多个
DialTimeout: 5*time.Second, // 连接超时时间
})
if err != nil {
fmt.Printf("connect to etcd failed, err: %v \n", err)
return
}
fmt.Println("connect to etcd success")
// 延迟关闭
defer cli.Close()
// put操作 设置1秒超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
_, err = cli.Put(ctx, "moxi", "lalala")
cancel()
if err != nil {
fmt.Printf("put to etcd failed, err:%v \n", err)
return
}
// get操作,设置1秒超时
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
resp, err := cli.Get(ctx, "q1mi")
cancel()
if err != nil {
fmt.Printf("get from etcd failed, err:%v \n", err)
return
}
fmt.Println(resp)
}Copy to clipboardErrorCopied
错误实例
在我们运行代码的时候,突然出错了,undefined: resolver.BuildOption
经过排查,是因为 google.golang.org/grpc 1.26后的版本不支持clientv3的
所以我们只能将其改成1.26版本的就可以了,具体操作需要在go.mod上加上以下代码
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0Copy to clipboardErrorCopied
watch
使用watch可以做服务的热更新
import (
"context"
"fmt"
"go.etcd.io/etcd/clientv3"
"time"
)
// etcd 的watch操作
func main() {
cli, err := clientv3.New(clientv3.Config {
Endpoints: []string{"127.0.0.1:2379"}, // etcd的节点,可以传入多个
DialTimeout: 5*time.Second, // 连接超时时间
})
if err != nil {
fmt.Printf("connect to etcd failed, err: %v \n", err)
return
}
fmt.Println("connect to etcd success")
defer cli.Close()
// watch
// 派一个哨兵,一直监视着 moxi 这个key的变化(新增,修改,删除),返回一个只读的chan
ch := cli.Watch(context.Background(), "moxi")
// 从通道中尝试获取值(监视的信息)
for wresp := range ch{
for _, evt := range wresp.Events{
fmt.Printf("Type:%v key:%v value:%v \n", evt.Type, evt.Kv.Key, evt.Kv.Value)
}
}
}Copy to clipboardErrorCopied
然后我们往etcd中插入数据的时候
我们的代码就会监听到数据的变化
使用etcd优化日志项目
logagent根据etcd的配置创建多个tailtask
见代码部分 18_LogAgent
etcd底层如何实现watch给客户端发通知(websocket)
logagent根据IP拉取配置
ES介绍和使用
今日内容
Logtransfer
从Kafka里面把日志取出来,写入ES,使用Kibana做可视化展示
系统监控
psutil:采集系统信息的,写入到influxDB,使用 Grafana做展示
promethenus监控:采集性能指标数据,保存起来,使用grafana做展示
ElasticSearch
Elasticsearch(ES)是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎。Elasticsearch还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可被搜索,ES能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。通常作为具有复杂搜索场景情况下的核心发动机。
Kibana
图形化展示
ElasticSearch安装
去官网下载 ElasticSearch ,下载完成后,到bin目录,双击启动
启动完成后,访问下面的URL
http://127.0.0.1:9200/Copy to clipboardErrorCopied
即可看到ElasticSearch的信息
ES API
以下示例使用curl
演示。
查看健康状态
curl -X GET 127.0.0.1:9200/_cat/health?vCopy to clipboardErrorCopied
输出:
epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1564726309 06:11:49 elasticsearch yellow 1 1 3 3 0 0 1 0 - 75.0%Copy to clipboardErrorCopied
查询当前es集群中所有的indices
curl -X GET 127.0.0.1:9200/_cat/indices?vCopy to clipboardErrorCopied
输出:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .kibana_task_manager LUo-IxjDQdWeAbR-SYuYvQ 1 0 2 0 45.5kb 45.5kb
green open .kibana_1 PLvyZV1bRDWex05xkOrNNg 1 0 4 1 23.9kb 23.9kb
yellow open user o42mIpDeSgSWZ6eARWUfKw 1 1 0 0 283b 283bCopy to clipboardErrorCopied
创建索引
curl -X PUT 127.0.0.1:9200/wwwCopy to clipboardErrorCopied
输出:
{"acknowledged":true,"shards_acknowledged":true,"index":"www"}Copy to clipboardErrorCopied
删除索引
curl -X DELETE 127.0.0.1:9200/wwwCopy to clipboardErrorCopied
输出:
{"acknowledged":true}Copy to clipboardErrorCopied
插入记录
curl -H "ContentType:application/json" -X POST 127.0.0.1:9200/user/person -d '
{
"name": "dsb",
"age": 9000,
"married": true
}'Copy to clipboardErrorCopied
输出:
{
"_index": "user",
"_type": "person",
"_id": "MLcwUWwBvEa8j5UrLZj4",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}Copy to clipboardErrorCopied
也可以使用PUT方法,但是需要传入id
curl -H "ContentType:application/json" -X PUT 127.0.0.1:9200/user/person/4 -d '
{
"name": "sb",
"age": 9,
"married": false
}'Copy to clipboardErrorCopied
检索
Elasticsearch的检索语法比较特别,使用GET方法携带JSON格式的查询条件。
全检索:
curl -X GET 127.0.0.1:9200/user/person/_searchCopy to clipboardErrorCopied
按条件检索:
curl -H "ContentType:application/json" -X PUT 127.0.0.1:9200/user/person/4 -d '
{
"query":{
"match": {"name": "sb"}
}
}'Copy to clipboardErrorCopied
ElasticSearch默认一次最多返回10条结果,可以像下面的示例通过size字段来设置返回结果的数目。
curl -H "ContentType:application/json" -X PUT 127.0.0.1:9200/user/person/4 -d '
{
"query":{
"match": {"name": "sb"},
"size": 2
}
}'Copy to clipboardErrorCopied
Go操作Elasticsearch
elastic client
我们使用第三方库https://github.com/olivere/elastic来连接ES并进行操作。
注意下载与你的ES相同版本的client,例如我们这里使用的ES是7.2.1的版本,那么我们下载的client也要与之对应为github.com/olivere/elastic/v7
。
# 初始化mod
go mod init es
# 下载依赖
go get github.com/olivere/elastic/v7Copy to clipboardErrorCopied
使用go.mod
来管理依赖:
require (
github.com/olivere/elastic/v7 v7.0.4
)Copy to clipboardErrorCopied
简单示例:
package main
import (
"context"
"fmt"
"github.com/olivere/elastic/v7"
)
// Elasticsearch demo
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Married bool `json:"married"`
}
func main() {
client, err := elastic.NewClient(elastic.SetURL("http://192.168.1.7:9200"))
if err != nil {
// Handle error
panic(err)
}
fmt.Println("connect to es success")
p1 := Person{Name: "rion", Age: 22, Married: false}
put1, err := client.Index().
Index("user").
BodyJson(p1).
Do(context.Background())
if err != nil {
// Handle error
panic(err)
}
fmt.Printf("Indexed user %s to index %s, type %s\n", put1.Id, put1.Index, put1.Type)
}Copy to clipboardErrorCopied
更多使用详见文档:https://godoc.org/github.com/olivere/elastic
Kibana介绍和使用
注意
Kibana和ElasticSearch配合使用的时候,需要确保两者的版本号一直,不然可能出现无法正常使用的问题
下载
官网下载Kibana,下载完成后,解压缩文件,修改对应的配置
找到 config/kibana.yml,然后修改下面的配置信息,因为kibana
# Specifies locale to be used for all localizable strings, dates and number formats.
i18n.locale: "zh-CN"Copy to clipboardErrorCopied
启动
修改配置完成后,找到 bin/kibana.bat,双击启动即可
启动完成后,访问下面的URL进入到管理页面
http://127.0.0.1:5601Copy to clipboardErrorCopied
Prometheus和Grafana介绍
系统监控
gopsutil:做系统监控信息的采集,写入influxDb,使用grafana作展示
prometheus:采集性能指标数据,使用grafana作展示
Prometheus
普罗米修斯:专用于服务监控,主动去拉取数据
- Jobs/Exporters:任务,监控项
- Serveice Discovery:服务发现
- Short-lived jobs:短期存活的任务
下载普罗米修斯
去Github的官网下载
下载完成后解压即可
双击 prometheus.exe启动,然后访问下面的地址
http://localhost:9090/graphCopy to clipboardErrorCopied
进入到prometheus的图形化页面
使用
就能够看到我们的图形化信息了
通过上图我们发现,使用 prometheus的图形化界面好像不太美观,所以就引出了下面的 grafana
grafana
grafana是采用go语言编写的,非常美观的图形化展示,我们找到官网下载,选择window环境
解压后的目录,如下所示
我们进入bin目录,找到 grafana-server.exe 然后启动 【首次启动比较慢,需要建立数据库】,启动成功后,访问下面的地址
http://127.0.0.1:3000Copy to clipboardErrorCopied
即可进入到grafana的图形化页面了
然后输入admin admin 登录即可
然后选择普罗米修斯
然后输入url保存
然后导入我们的普罗米修斯的仪表盘
然后到Home目录下,选择 new board
选择刚刚的import 的 仪表信息
这样就生成了我们的仪表信息了
或者可以选择另外一个样式
结语
如果我们要监控其它的一些服务,比如redis、mysql、Memcache等等,需要自己到官网下载对应的包
https://prometheus.io/download/