我们开始讲第10课
这一课讲微服务的可观察性
什么叫微服务的可观察性呢
你从巨石架构演进过来以后
它的微服务的数量慢慢变多
以前是一个大服务
所有的它的问题的诊断看日志
看指标都相对比较简单
我们以前8台机器都是所谓的巨石架构
用户量也不是特别大
可能就几台机器
我们以前日志都是打在本机
打在本机以后我印象中就是我们用XShell登上这几台机器
然后批量发送一个tail -f去看
这是我们经历过这样的一个时间
后来觉得这样的话机器节点变多以后看日志非常麻烦
我们就把它统一用/data/logs这种日志盘
我们单独把它挂载在一个NAS的存储上
就是一个网络存储
然后再去网络存储上去tail -f去看
其实我们也经历过一段类似的的时间
随着我们拆分服务以后
服务的种类变多
节点也越来越多
以前都是大机可能48核48G就部署七八台机器
现在的话因为我们都趋向于让服务的故障域变小
就是我4核4G一个容器
我可以启动50个容器
虽然用的机器的总量占的资源可能是差不多的
但是因为我4核4G拆成很多细度的一些容器以后
这样容器数量变多
容器数量变多以后
意味着我可以让它更可能的分散在不同的物理机上
最终从实际结果来说
你以前挂一台影响1/8的流量
那么我现在挂一个容器 那么
这个容器可能在某一台物理机上
我的影响可能是1/50
它的故障域其实就缩小了
这个也是非常重要的一点
这样的话拆分完以后
我的日志是不是看起来就不太像以前能够登到机器上tail -f
或者说统一用NAS存储了
整个日志采集会变得非常复杂 这是第一个
第二个 我们的指标关注的是什么
你要通过指标可以量化你服务的运行情况
包括它的所处的物理机的情况
它依赖的组件的情况
如果我需要跳很多个页面来回去查他依赖的资源的问题
它的主机的监控信息
基于这些指标做的大盘 它不完整
它需要到处切 去找
然后才能定位问题 那么指标大盘
应该是一眼你就能找出微服务的问题
其实是非常关键的
那么最后一个就是指链路追踪
链路追踪其实是你即便在巨石架构里面也是有用的
因为你的调用链路复杂
你的巨石架构里面很多个模块之间相互依赖
你要去排查性能问题的时候
你怎么知道是哪一个模块 它具体为什么变慢了
它其实是不太好去查找到细粒度的模块级代码问题的
那么从微服务的角度讲
我拆分很多服务以后
我要去定位服务内它的瓶颈在哪里
我的请求从BFF到微服务
整个请求扇出是很多的
整个流量也会变的变得到处都是
我要去定位这一次请求所涉及的服务
到底是哪个环节有问题比较慢
或者说为什么它比较慢
是服务类具体某一个组件 哪里慢
都不太好查
所以它也需要链路追踪
把完整的那种Tracing的一些追踪的信息
比方说谁调谁花了多少时间
带了一些什么样的一些标记
比方说他请求的是Redis的GET操作
这些明细信息 我们希望能够都拿到
有了这些信息以后
它就从日志指标和链路追踪这三个层面形成了
对微服本身的可观测性
你就可以通过这三种手段让我的微服务去查问题的时候
或者是看它内部运行的情况的时候
甚至我针对请求级别去诊断问题的时候会变得非常方便
所以这三块是围绕微服务的外围的中间件的一些基础设施
我们现在看一下日志
我们最早用在内部用的话
主要是用的Go的glog
glog其实最早是Google内C++们那边用的比较多的一个日志库
目前glog在Go语言这个版本
目前谷歌应该是不维护了
但是它里面还是有很多想法和思路
对我们产生了很大的影响
它内部的日志级别
主要是包含像Info Warning Error 还有Fatal
Fatal它其实会中断程序的执行
相当于我们调完log.Fatal之后
它整个程序会退出
除了像glog
开源的其实还有像log4go loggo zap
其他的第三方日志库
它们又提供了设置日志级别的可见性
可以使用这个日志库设置一个
我们叫log的level
就某一个级别之上可以可见
一般来说我们总结下来它会包含
像Trace Debug Info Warning Error 还有Critical
怎么又多了好几种级别
针对日志级别应该怎么用怎么打
这个我今天也是要重点去提一下的
以及我们Kratos的日志是怎么去设计的
它背后的一些想法是什么样的
首先我们看一下Warning日志
这个就有意思了
我印象中我们很多Java的服务启动的时候
一片片的那种Warning打出来也不去修
看到这种Warning也不去修
因为Warning在我看来
虽然Warning你经常打
但是打了之后基本上没什么人看 这个很尴尬
最终是没人看这些报警的这些日志的
但是我觉得很负责的同学可能一般会关注一下
我会觉得说Warning的日志
大家觉得它只是个告警
它只是个提示你可能有些问题有些风险存在
但是它还没到报错这一步
听上去这个Warning也不是我导致的
好像是别人的问题别人去修复就好了
跟我有什么关系
我觉得这个其实是很常见的一种想法
从我的视角 就是说作为一个微服务owner
一个负责人
我们应该尽可能的去消除掉警告的级别
要么它最终就是一个Info的输出
要么它最终就是直接报错
打一个log.Error
告诉你报错 然后程序中断
你Warning其实很尴尬 就是你提示它
但是你不会去修正
所以我觉得这个是需要我们内部讨论的机制
最终逐渐让这些Warning的日志消失
比方说你标记某个东西过期直到多少天它是Warning
但是多少天以后报Error然后让程序退出
这可能也是一种强制手段
让我们的Warning逐渐变少
我们如果参考Go语言的设计哲学
所有警告都应该是个错误
那么其他语言的Warning其实目前是可以忽略的
像Java或者ObjectiveC这些其实都可以忽略
除非你在IDE 层面或者在CI/CD流程中
强制把他们就是Warning as Error
就是把它们标记为Error
逼程序员们尽可能让他们去消掉
这个是一种策略
就像我刚刚说打Warning的时候
如果你给个deadline几天以后
它一定会变成一个Error然后你程序会退出
这样可能别人有动力
否则其实是越来越多
他基本上不会去改的
同样的话 如果想要最终消除Warning
应该刚刚说的在某些时间点以后直接把它记录为Error
让代码的作者本人可以重视起来 这是第一个
所以Warning级别
在一些真的你需要它做报警
并且可能需要他去fix掉这个问题
接下来有一天它可能会变成Error的
情况下就要使用Warning 这是第一个
那么第二个叫Fatal
我们刚才提到的glog提供了Fatal级别
但是你在调完Fatal的时候
它记录消息以后
它会直接调用os.Exit(1)退出
它直接调这个退出
这意味着如果有其他的goroutine有defer语句不会被执行
第二 可能你微服务里面有各种内存的buffer需要刷盘
比方说日志在buffer里面需要刷盘
它可能不会被flush
包括像日志的刚才说的 还有第三个
可能之前用了一些临时文件或者目录
在进程要退出结束的时候
可能要把它们移除或者清理
可能也来不及清理
所以我觉得标准库的log.Fatal
还是第三方库的log.Fatal也好
我们如果看到它的定义是一定会调用os.Exit的话
我是不建议用log.Fatal来记录日志的
这会导致我们刚上述说的你的defer不会执行
你的buffer可能有些没有flush
还有你的要处理的一些临时目录
可能没有去处理干净
其实都不好怎么办
如果我不使用log.Fatal
我们应该是向调用者返回错误
就说有人调了你这个库它报错了
我可以直接一层层往上抛
那么如果最终这个错误传到我们的main函数
main函数应该是在它退出之前
做好这些清理工作以后
然后可以把报错信息再打印出来
或者说你这时候再去调log.Fatal是可接受的
因为你的清理的动作已经做完了
那么你是可以做刚刚说的上述的事情的
否则我是不建议使用Fatal
那么第三个就是Error级别
有很多人我们包括第二周讲的时候
我们讲异常处理的时候
有很多人是在错误发生的地方
立马记录日志 大家有印象吗
就是产生的错误立刻打个日志
看上去是个好习惯
因为你一报错误立刻日志肯定是个好习惯
但是因为Go的整个error的设计和处理
使得我们两种姿势
第一种把error的报错抛给调用者
就像我们这个地方
如果你somethingHard报错了
那么if err != nil 通常大家做法是什么
log.Error把这个错误记起来
然后再把它return掉
我的建议是log.Error都不打了
直接return error或者用errors.Wrap把它包装起来
携带一些更详细的信息往上层抛
抛到哪个时候可以打印
前面讲过在HTTP或者gRPC的middleware
在你的请求的一个middleware拦截器里面
把日志统一打印出来
在最顶部的地方去打日志
我觉得这个是很好的一个点
第二个 当我们产生错误的时候
除了我们往上层抛 还有一个是什么
我们应该处理这个错误
什么叫处理错误
举个例子
我们如果是调了一个planA报错了
然后如error != nil
我这个地方可以做什么
降级调planB
这个时候应该打一条这种事务性的描述出来
比方我通过log.Infof打了一下说我目前打不开这个文件
然后我尝试用planB这样的话
相当于是标记出它是一个提示信息了
就最终把error给吞掉了变成一个提示信息
你看我们这个地方error也没有返回的
所以我认为如果你出现了一个错误
你处理掉了它
那么你就不应该再往上层返回error
如果你没处理它
你想往上层抛
那么应该在我们的最顶层去统一打日志
但是在这个case里面
我认为planA如果失败了
产生了一个降级行为
调planB
本质上 planA降级到planB
它属于有损服务
我更倾向说在这个地方使用Warning
不应该是一个Info
为什么是Warning
它是警示性的告诉你
降级不应该常发生
那么在planA里面你失败了
你产生了降级
你应该重视planA为什么会老失败
这个是我的一个观点
还有一个就是Debug
Debug其实大家也用的比较多
尤其是写基础库的时候
或者说我们要调试业务的时候
可能会故意加一些log.Debug去排障
或者说看看它的一些输入输出
用于开发过程中调试软件的时候使用的
所以最终打日志的时候应该关注两个事情
一个是给开发人员在开发调试你的服务的时候
关心的一些数据 一些事情
开发调试我们应该用log.Debug
那么另外一类就是说我们应该写的日志是服务的owner
他可能关心的 甚至说用户可能都会关心的
这些信息我们应该用什么log.Info
原则上来讲我们叫最小化API
我觉得我们日志库最好是只有三种级别
可能就是Info Debug Error
但我觉得在我们Kratos设计的时候
我们其实也把Warning加上
因为Warning我觉得还是有些场景的
比方说某一个函数即将过期
它做了一些打印输出
提示你某一个时间点你要把它换掉
我觉得这个时候用Warning
包括像我们刚才说的降级这种场景
可能使用Warning也是合适的
这两个我觉得是非常重要
一个是面向调试的
一个是面向信息输出的两种级别
log.Info就是一个纯打日志描述性的信息
所以我们日志库设计的时候也不应该有关闭它的选项
因为它就是一个单纯的输出
只有用户被告知他们有用的这种信息你才会去打印
所以如果发生了一个无法处理的错误
我们要逐层一层层往上抛到main函数
我刚才其实漏了一个 除了Debug Info
还有Error也是需要的
因为你Error和Info的话
还是Error我们看到之后会更敏感更紧张一些
它毕竟是个报错
log.Debug它其实是完全不同的
它其实用于开发人员或者是售前工程师去参考的
那么在开发过程中调试语句应该是丰富的
我们看到很多日志库
增加了什么类似像trace
debug2 debug3 debug4这种级别
然后通过我们的SetLogLevel就是通过控制日志级别
来做日志级别过滤
我觉得是一个很不好的设计
假设我有好多层 你是不是要加到debug30呢
我觉得这个应该是不好的
日志应该是支持日志级别的过滤
包括像启用日志和禁用某一个级别的日志
但是我觉得也应该支持更细粒度的Debug的一个控制
那么更细粒度的Debug控制
我们待会看一下谷歌的glog它是怎么做的
考虑到上面的所有的一些思想
我们当时也是对Kratos的日志库重新进行设计
现在看代码看当时是怎么思考的
我们看一下在Kratos log目录下
我们首先是要定一个log的interface
看接口的设计它其实就是一个log方法
然后带上level和key values就这么简单
我们设计的时候并没有把Debug Info Error这些方法全部加上
第一觉得应该最小化接口interface的表面积
第二点尽可能去通用
而不是某种特例的方式
加上一个函数签名 尽可能是通用
比方说Debug Warning Info Error
有些人还有trace debug2345
我是不是要考虑所有开源实现的日志库的方法签名全部加上去
那肯定不合适
我们第一个版本其实连level都没有
后来因为我们考虑到log库要适配很多第三方的一些log库的实现
所以我们还是把level加上了
我们看一下它的基本的用法
其实也非常简单
假设你是一个默认的defaultLogger
然后你可以调用一个With方法
第一个参数就是在哪个logger上
旁路携带一些key value
也就是说这With以后logger每次在它之后打Log这个方法
都会把这些公共字段给带上
所以它是有个With方法
我们打日志的时候就是调了个Log方法
注意Log方法把日志级别和key value给加上
这里级别其实就是定义了我们刚刚说的那几种
Info Error Warning Debug这四种
这个是我们常用的
除了用法以外 我们再看一下DefaultLogger我们刚才也提到了
它是一个标准的stdout的log库
Kratos为什么是这样设计
为什么它是只有一个接口的定义
没有具体的代码
其实也跟在业务包里面使用日志有关系
举个例子 我们的业务包比方说你import一个mylogger
它是一个具体的第三方的实现
比方说你是用log4go
假设在我项目里面定义的这样的一个log的变量
之后我就用log变量去打日志
那么这样一个case的话会导致我们package
跟我们具体的日志库的第三方库实现产生依赖
也就是你foo可能会依赖log4go
它产生一个强依赖
所以做kit库的时候
我们为什么去定义logger的interface
核心就是说延迟需要打日志的类型
与日志的实际类型之间的一个绑定 是要推迟的
就打日志的模块或者是包
你要依赖一个第三方的库产生一个强依赖的话
还不如依赖一个interface
具体logger的interface
和他第三方库的一个适配的话
应该是在我们main函数
或者说在需要的时候进行关系绑定
而不是一开始我强依赖某一个第三方库
这样的话我未来有一天想要替换
一个日志库是搞不定的
使用了kit库以后
假设我们在这个地方是pkg/log
假设我们有这样的一个第三方库
它可能是类似像我们Kratos log库一样的interface
我的T依赖的是什么呢
依赖的实际上是log.Logger
它依赖的是一个interface
实际的绑定可以在main函数里面去做
所以它的扩展性和通用性就会要好的多
再回到我们Kratos里面
看一下我们做了一个第三方的适配
我们主要是使用的fluent
非常简单和好用 fluent是做日志采集的
它的NewLogger实际上是依赖我们Kratos V2的log库的
这个是一个interface
我们现在需要做的是让fluent来适配interface
这个fluent就是第三方库
我在工作中经常用到
希望在编译的时候运行时就可以发现
某一个对象是否实现了某一个接口
所以经常有同学问我
我们看比方说这个地方log类*Logger是谁
是fluent定义的一个Logger的结构体
然后定义完Logger结构体以后
因为我不想初始化具体对象
所以我可以用nil指针把它转成类型
然后再var声明一个变量
这个变量因为我不使用
所以你可以用下划线去忽略掉
然后它的实际类型是log.Logger
意味着这是Kratos log的适配Logger的一个interface
这句话其实就是方便验证我们实现者有没有实现
Kratos的Logger的interface
这个其实工作中也非常常见
那么怎么适配呢
我们刚刚说了
NewLogger返回的是fluent的具体的一个Logger类型
这些初始化的工作我就不看了
我们直接看他Log方法
这个地方还叫Print因为我们还没改过
应该是叫Log方法
这个Log方法如果这个地方带上level的话
就可以在这个里面New完fluent logger以后
在这个地方你已经拿到了level
就做一个switch case位
如果switch它是debug
我就可以调movement的点debug
如果他case位它是一个info
我就可以调fluent.Info
所以这样适配起来就非常简单了
最终你其实可以适配fluent还有别的一些日志
像ELK的也是通过类似的方式来适配的
这样的话就非常的灵活
我们log库除了定义内部的interface以外
它其实有个自带的简单实现
核心其实还是处理一下我们内部的
有一个叫Valuer的对象
相当于是你的代码的行号 你的TraceID
你的调用者是谁
你的时间戳是什么
这些其实是按照某一种方式扩展一个字段的
为什么要去添加一个Valuer的模式
是因为 举个例子
调用代码的caller 就是谁调用的
就是文件的文件名和文件行号
这个caller其实应该是在运行时后确定他是第几层路径的
而不应该是你当时调的时候它处于第几层路径
所以这个caller你会经常要调手动调调不准
但是如果你是在log库内部去使用的
它位置一定是固定的
你就可以使用Valuer在运行时把值实际的值取出来
包括像我们的timestamp
我们刚才讲了
我们现在Kratos log携带一些公共字段
肯定是打日志的时候才把当前时间戳给重新拿到
而不是说我在设定公共字段的时候
把时间戳一开始就算死了 那肯定不行
既然是你需要设定一个时间戳
在运行的时候拿到当前时间
所以我们这个Valuer你可以理解为
在调用的时候会去真正执行Valuer的实际内容
我们再看一下log库
在Log方法里面
我们先判断一下有没有Valuer这种对象
如果有的话 它就会调bindValues
实际上for循环 然后把它的key断言出来
再把它的value获取到
然后这个时候你就可以调v方法
我们再看一下Valuer是什么
它其实是一个函数的指针
你就可以再去触发获取当前时间戳
因为你的内容是你自己去填充的
就是你调什么方法
这个方式也是非常不错的一个点
除此之外
我们最终类型的第三方库和Kratos log的interface在哪绑定
就是用wire在我们的main函数去初始化
相当于是初始化一个第三方库
然后用Kratos的log的interface
就在项目中主要是用这个interface
你不应该传递三方的一个实现类进来了
假设我有一天传到biz data层
还有service层 我可以直接用Logger对象
无论你第三方库切成什么
我实际上一行代码都不需要改
这样的话就是用interface的一个好处了
Kratos log就介绍到这里了
我们再看一下glog
实际刚刚说了
我们需要有控制log level
就是控制日志级别的方法
但是很多日志它都忽略了一个点
支持多少层级的log.Debug
的确我看过就是log.Debug12345678
加个好多个这样的方法签名
但是glog它的做法非常巧妙
它使用了一个叫Verbose的方法名 也叫v开头
这样的话要设置不同级别怎么做
glog.V(2).Infoln
表示的是必须要v值必须比它大
大于等于它才会展现
这种写法和上面这种写法有什么区别
我们当时做压测的时候发现
如果你把debug关掉
那么这个函数它仍然会去执行
它会执行到哪呢
把参数解析压参
然后调用方法的时候发现被过滤掉了
因为你还是会陷入到Infoln这个函数里面去
然后在里面去把为V(2) V(3)这些值给屏蔽掉或者过滤掉
如果符合条件的话就打印出来
它是有一些门槛和成本开销的
glog它还有一种写法
设置log verbose level以后
你就可以提供无数多个012345678910对吧
随便无限多
那么这样的写法还有一个好处
就是说他会先走条件分支的一个判定
这一行代码有可能调 有可能不调
那么它的成本开销就会小得多
我觉得这个是非常重要的一个点
我们除了第三方的日志库的一个使用
以及和Kratos log一个桥接以外
其实还有一个很关心的就是日志选型
我们前面提到你的微服务节点数量变多
你有没有一个比较方便的可以
统一去查看这些日志文件的一个界面或者UI
而不是还需要敲指令登机器 这个可能成本比较高
我认为一个完整的集中式日志采集的一套系统
它应该包含下面几个特点 第一
它是能够统一采集的
支持多种来源的日志数据和格式
第二它支持支持传输
能够稳定的把日志数据传输到中央的系统
比方说可能存储到Elasticsearch
第三是它可以可靠地存储
比如我们刚说的 也是可以使用Elasticsearch
传输指的是其实你怎么传到Elasticsearch
存储指的是你怎么存储这种日志和检索
第四块其实就是分析
我有一个很好看的UI平台
然后基于这个UI平台可以写一些过滤条件
甚至出现仪表盘
最后的话甚至可以基于日志
比方说我调了log.Error打了一些错出来
那么连续发现产生多少个报错
也可以支持一些报警的策略
所以开源界鼎鼎大名其实还是ELK的整个stack
它的整个技术栈 它分别表示
Elasticsearch Logstash和Kibana的缩写 它们都是开源的
所以你去搭一套完整的ELK是比较快的
稍微投入一些运维的人力
了解各个组件的基础的运维
还要和它的一些性能调优
后来又新增了一个Filebeat
它是一个轻量级的日志收集工具 它是个agent
说白了就是你物理机有多少台
你的agent就部署多少台
每台机器都要部署这样的一个agent
方便去做日志的收集和采集
Filebeat它的好处是占用资源少
适合在各个服务器上
直接在那个服务器上本地去采集
收集日志之后再传输到Logstash
官方也比较推荐这个工具
整个ELK的演进是怎么来的
我们先看一下你的应用APPServer
日志打到Logstash的agent
它支持TCP或者是UDP的监听
打过去以后它再上报给Elasticsearch
直接用Logstash agent上报到Elasticsearch
然后我们再使用Kibana去做UI的前端
最终你可以通过浏览器访问到我们日志的前端
那么你用的时候体验就会非常好
你打开浏览器快速就可以看到日志了
也不需要登服务器
也不需要去再去管统一采集的一些问题
Elasticsearch它将数据以分片的形式压缩存储
并且提供了很多API给用户使用
你可以更直观
无论是用Elasticsearch的API自己去做UI
还是直接用Kibana去对接
都是非常方便的
那么因为Logstash它属于一个Server角色
我们看一下APPServer把日志上报给Logstash agent
它像是一个server
接收来自APP端的日志的这种流量
我反而不建议这种部署方式
因为我们知道所有的微服务都应该要考虑去中心化的问题
那么你的Logstash是很有可能会成为瓶颈
可能这个地方是画了两台
但是你的应用业务一多以后
你这两台可能就扛不住
它就算再去扩更多台 本质上还是一个热点的存在
所以Logstash另外一个原因就是因为它
还需要耗费大量的match的操作
比方说格式化日志这种
那么他消耗的CPU也是非常多的
那么不利于去让整套系统scale out出来
后面Elastic公司它又在这个架构上引入了消息队列
我们看以前其实连消息队列没有
相当于Logstash的agent就是一个消息队列
但是如果它一旦出现宕机怎么办
数据会不会丢
甚至说如果我上游Elasticsearch压力很大怎么办
你的数据堆积在哪里
这些可能都需要考虑
在这个架构里面我们引入了消息队列
实际上是在各个节点上的Logstash agent
先将数据日志传递给Kafka
然后再将队列中的消息或者是数据间接地投递
让Logstash消费
它在做过滤做分析以后最终传到Elasticsearch
再使用Kibana将日志和数据呈现给用户
那么引入Kafka其实意味着
即便远端的Logstash server因故障停止运行
那么数据因为也会被持久化在Kafka里面
从而避免数据的丢失
那么更进一步的就是将收集到的Logstash
换成Filebeat 它更灵活
因为Filebeat其实是有ELK后面出来的
它的整个消耗资源更少 扩展性更强
所以这套架构 Logstash这套偏Server这种集中式采集的方式
它就逐渐又变成我们的APP端
开始打本地日志
然后部署一个Filebeat的文件收集和采集
报给Kafka
之后再起一个Logstash的agent
进行数据的过滤和分析后再投到Elasticsearch
我刚才前面也讲了
最终它是引入了Filebeat
Filebeat Logstash Elasticsearch Kibana
组成了一个完整的日志采集的方案
日志系统其实它还是有一些设计目标的
我们当时主要是希望接入方式收敛
因为公司大了
你的各个语言各个模块
各种需要做日志采集的话
一定是需要接收方式统一
否则运维工作会非常大
A部门是使用A的组件 B部门可能又是另外一种姿势
这个其实可能会非常痛苦
所以第一个就是要考虑到你的接入准入应该是统一的
第二个其实是日志格式的规范
第三个就是我的日志解析对日志应该是透明
也就是我的解日志应该不需要我的业务再深度参与进去了
还有系统的高吞吐低延迟
还有其他什么可运维高可用可扩展
这些都是非常重要的
先看一下规范
我们当时的规范其实是JSON作为日志输出的格式
先有time字段就是产生这个事件的行为时间
第二个就是日志等级
就是DEBUG WARNING INFO和ERROR
还有就是应用ID
我们前面介绍过一个微服务应该是a.b.c
类似像DNS这样的一个唯一的名称
用来标示日志来源
还有一个就是叫instance ID 指的是实例ID
用来区分同一组应用中不同的实例
也就是他们不同的hostname分别是什么
像时间戳
instance_id指的是它的实例hostname的ID
然后日志级别 打的什么日志
这些其实都是自己可以去扩展的
当然了现在也可以直接使用OpenTelemetry的规范
它对每一个字段它都进行了约定
最好是用哪个字段的名称来做key value
它其实做了很多规范
大家可以关注一下
从生产到检索可检索指的是生产采集
传输切分和存储检索
我们先对比看一下Logstash
它是监听TCP和UDP的
通过网络上报日志的方式来去做的
对于Filebeat它是直接采集本地生成的日志文件
就是我刚刚说的你可以直接去本地采集
适用于日志无法定制化的数据应用
logagent是我们当时自研的小的采集的agent
它也是一样的物理机部署
同时也支持本地的日志采集
以及本地的UNIX socket监听
这两个都是支持的
我们logagent的第一个版本是基于UNIX socket
或者说基于自己UDP的一个设计
自己去做了这样的一个协议
这样的话语言他都需要各种SDK
所以Go语言一个 Java一个 C++一个其实非常痛苦
agent它可能会需要加载配置
我们当时其实主要还是依赖的本地配置
对于logagent来说
它因为不需要每台机器有什么特别大的变化
基本上都是一样的
所以我们再看一下我们当时的logagent
它其实分为collector和sender两个角色
这边负责从UDP或者是TCP
或者是本地日志文件去捞取这些数据
然后经过一个file cache
也就是类似像配置page cache一样
再把这个数据给发送出去
这个是我们当时的架构
我们再来看一下传输
传输指的是你的日志报到到一个地方去
比方写在本地
如何经过各个环节最终录到Elasticsearch呢
我们早期其实也是基于Flume加Kafka的统一传输平台
也就是我们早期是基于Flume改的
第二个就是说我们LogID最好是要做日志分流的
一个日志ID里面它一定是有低中高优先级的
比方说像日志系统里面Error
因为它是非常敏感非常重要的一个事件
所以Error我们认为是高优先的
所以我们当时是拆了通道的 在Kafka里面拆了不同的Topic
避免相互影响
现在其实我们打日志已经替换成Flink加Kafka
也不需要手动去做类似Logstash做的一些工作
比方说过滤 可能还会有些分析
最终包括序列化
可能序列化成Elasticsearch需要的格式
然后最终再录入进去
所以除了这样一个演进
我们目前使用Flink
其实因为它偏实时计算
我们现在传输的体系基本上都是主要是用Fink为主的
所以它的架构其实变成了logagent的采集
然后通过我们的类似像flume的传输网关
然后再写Kafka
我们Kafka消费早期是logstash去解析的
我们后来因为发现它的效率是比较不好的
就是Logstash效率 它是Elastic官方组件
它是基于jruby开发的
功能虽然强大
资源消耗很高 性能也不太好
它虽然支持各种过滤和match
所谓的裁切还有序列化这些
但是它确实性能很一般
所以我们后来就自己研发了专门的入库的模块
就是写入到Elasticsearch的模块
这个其实我刚才讲的我们现在因为使用Flink
所以我的Kafka消费者也使用了Flink
Flink可以直接写一个collector
可以直接写到Elasticsearch
就是最终我们把自己的bili-index写入Elasticsearch的服务
也把它干掉了
也就是说我们既没有使用自研的方案
也没有使用Elasticsearch官方的组件
因为这一步其实主要就是从Kafka消费日志
解析日志 再写入Elasticsearch
我们就直接用Flink就可以了
如果是未按照日志规范产生的一些日志
可能需要自己再去手动配一些日志的解析规则
这个确实是比较恶心的
存储和检索使用Elasticsearch
我们现在是搭了多套Elasticsearch集群
还是分一下日志级别
比方说日志分级非常重要的
审计类的以及错误类的
我们可能是独立集群
剩下的可能是一套集群
在集群内 因为Elasticsearch 我们可以想象一下
十几天前的日志你基本上搜索应该会搜的比较少
我们认为这种数据是冷数据
最近三天的数据可能是热数据
所以我们在Elasticsearch机型上就是本地有很多大盘
HDD的SATA盘
我们还会放一些SSD用来做最近几天的一个查询加速
每天固定的会进行一些数据的冷热迁移
然后我们的索引也是提前一天去创建的
最终检索其实是基于Kibana
因为Kibana整套UI也是非常炫酷也非常好用
所以我们还是标准的有点像Elasticsearch的整个技术栈
但是我们把其中的一些组件都替换掉了
我们最终一套方案 像APP端是打本地日志
然后被我们的logagent采集走
我建议大家也可以其实用fluent的日志采集也非常好用
它采完以后就写到Kafka
写到Kafka以后再使用Flink
把它入到Elasticsearch就是入库
目前是一个这样的架构
跟以前传统的ELK的做法有一些区别
其实也是因为复用我们内部的基础设施
假设我们现在想自己搭建一套完整的这套体系的话
我建议用标准的ELK
从它的采集的Filebeat agent
或者说你用fluent agent
然后再把它投到Kafka
以后再去使用Logstash做日志的解析
然后最终再搭建一个Elasticsearch集群去做检索
然后再搭建Kibana拿去做查询
这样的话其实就是一个完整的一个日志采集系统了
我们这个过程中其实还踩过一些坑
我们一开始使用的是自定义的协议
就是我们用的UDP的一个协议
公司内有好多种语言
什么C++各种都有
导致说SDK的版本升级
多语言的支持
各个语言的实现的质量它都是有些差异的
所以我们痛定思痛
就觉得每个语言的日志库写本地永远都是很成熟的
所以我们就直接还是把日志写到了本地磁盘
在容器里面也是写本地磁盘
写完以后 需要我的logagent能够采集容器内的一些日志
所以我们logagent也支持容器内的日志采集
这个是非常重要的一步
最终我们是退成了用APP直接打日志的方式
打到本地磁盘上 也不走网络 不走远程
剩下的工作都是交由logagent
它自己去走传输体系去分发这些日志了
这个是很重要的一个点
我们可能还有一些待完善的措施
比方说我们突然发现某一个宿主机
突然打的日志量非常猛
比方io很高 或者说导致我们宿主机的磁盘空间不够
可能我们会限制它的发送速度
我们可能会下发一个指定到一批agent某一个应用
它的日志可能要做采样
有可能因为一些意外的情况突然加了很大量的日志
我的Elasticsearch可能扛不住
可能也有些冲突
所以我们其实在logagent的支持一些LogID
或者说支持一些应用ID 更细粒度的一些管控
这个其实是非常重要的
那么在容器内的采集非常简单的就是overlay2
然后从物理机上查到对应的日志文件
然后把它统一上报上来
我们配置文件大概就长个这个样子
我们当时logagent是否启用
然后哪些路径需要被采到 排掉的一些文件
它是一个JSON还是一个什么
它LogID的是多少
然后它怎么去匹配多行的
非常简单 日志这块就咱们到这里
我非常建议大家直接使用ELK stack
包括现在其实也是新的一套体系出来 像Loki
也是有这样的日志的一个解决方案
Loki也好也有ELK也好
当然就是我们核心还是要看
我是建议大家都是写本地日志的 因为非常简单
然后你再使用Filebeat去采本地日志
然后你的传输卡可能是需要搭建一个的
另外就是消费者我刚才说我们内部是用Flink
但我建议你们可以直接使用Logstash去做日志的解析和裁剪
然后再入库到Elasticsearch
基本上大家分布式搜索全部检索都是用Elasticsearch比较多 开源的
最后就Kibana 你可能需要做一些定制
比方说需要登录账号
可能需要按照应用粒度去做一些权限设定
目前我们是相对比较开放一点
就各个人都可以查看别人的日志
因为有可能你去从BFF视角去诊断日志问题的时候
可能需要关联好几个应用
我觉得是需要注意的
日志是我们侦测的手段
其实包含像指标也好 链路追踪也好
也是非常重要的一块
我认为日志在什么情况下有用
在我们发布以后
你通常会去盯着我们的指标
其实你除了看指标 看有没有性能退化
有没有报错 你最好还是要看一下日志有没有正常的输出
有没有一些异常的输出
如果有异常的输出
它的错误的详细信息
哪个接口你都能找到的话
你去具体修复这个问题的时候也会非常快
包括我们其实出事故的时候
我们第一反应一定是看一下指标和日志
有没有一些意外的一些日志
比方大量报错 或者指标有没有一些明显的性能下降
或者说某一个接口在大量报某种状态码
这两个手段其实是很常见的
日志其实还有一些别的细节
包括像安全的脱敏
这些可能需要和内部的安全同事去做对齐
看一看哪些字段不能打进来
比方有人经常把账号登录成功的账号密码token全部往里面塞
这个其实是有日志的一些泄露风险的
这个是要注意一下的
好日志我们就先讲到这