1. 日志
1.1 日志级别
waring
没人看警告,也许将来会出问题,但这听起来像是别人的问题。
我们尽可能的消除警告级别,它要么是一条信息性消息,要么是一个错误。我们参考Go语言设计额哲学,所有警告都是错误,其他语言的warning都可以忽略,除非IDE或者在CICD流程中强制他们为eror,然后逼着程序员们尽可能去消除。
同样的,如果想要最终消除 warning可以记录为error,让代码作者重视起来。
Fatal
记录消息后,直接调用os.Exit(1)。这意味着:
- 在其他goroutine defer语句不会被执行
- 各种buffers不会被flush,包括日志的
- 临时文件或者目录不会被移除
不要使用fatal记录日志,而是向调用者返回错误。
Error
也有很多人,在错误发生的地方要立马记录日志,尤其要使用eror级别记录。
- 处理error,就不给上层抛出了。
- 把error抛给调用者,在顶部打印日志。
Debug
log.Debug 它由开发人员或支持工程师控制。在开发过程中,调试语句应该是丰富的,而不必求助于trace或debug2(您知道自己是谁)级别。日志包应该支持细粒度控制,以启用或禁用调试。
log.Info 只需将该行写入日志输出,不应该有关闭它的选顶,因为用户只应该被告知对他们有用的事情。
1.2 格式规范
JSON作为日志的输出格式:
- time:日志产生时间,ISO8601格式;
- level:日志等级,ERROR、WARN、INFO、DEBUG;
- app_id:应用id,用于标示日志来源;
- instance id:实例id,用于区分同一应用不同实例,即hostname;
可以使用otel规范!
1.3 日志系统
一个完整的集中式日志系统,需要包含以下几个主要特点:
- 收集-能够采集多种来源的日志数据
- 传输-能够稳定的把日志数据传输到中央系统
- 存储-如何存储日志数据
- 分析-可以支持UⅡ分析
- 警告-能够提供错误报告,监控机制
例如ELK,loki。
日志从产生到可检索,经历几个阶段: 生产 & 采集 & 传输 & 切分 & 存储 & 检索
采集
logstash:
监听tcp/udp 适用于通过网络上报日志的方式filebeat:
直接采集本地生成的日志文件 适用于日志无法定制化输出的应用传输
基于Flume + Kafka ,或者 Flink+kafka。
切分
从kafka消费日志,解析日志,写入elasticsearch。
存储
elasticsearch多集群架构 日志分级、高可用。
每日固定时间进行热->冷迁移
检索
检索基于 kibana。
2. 链路追踪
2.1 Dapper
参考 Google Dapper 论文实现,为每个请求都生成一个全局唯一的 traceid,端到端透传到上下游所有节点,每一层生成一个 spanid,通过traceid 将不同系统孤立的调用日志和异常信息串联一起,通过 spanid 和 level 表达节点的父子关系。
核心是使用 grpc的 metadata的能力,和 go 的context传递。
调用链
追踪信息
- 追踪信息包含时间戳、事件、方法名(Family+Title)、注释(TAG/Comment)。
- 客户端和服务器上的时间戳来自不同的主机,我们必须考虑到时间偏差,RPC 客户端发送一个请求之后,服务器端才能接收到,对于响应也是一样的(服务器先响应,然后客户端才能接收到这个响应)。这样一来,服务器端的 RPC 就有一个时间戳的一个上限和下限。
植入点
Dapper 可以以对应用开发者近乎零浸入的成本对分布式控制路径进行跟踪,几乎完全依赖于基于少量通用组件库的改造。如下: 当一个线程在处理跟踪控制路径的过程中,Dapper 把这次跟踪的上下文的在 ThreadLocal中进行存储。在 Go 语言中,约定每个方法首参数为 context(上下文)
覆盖通用的中间件&通讯框架、不限于:redis、memcache、rpc、http、database、queue。
2.2 跟踪消耗
处理跟踪消耗
- 正在被监控的系统在生成追踪和收集追踪数据的消耗导致系统性能下降。
- 需要使用一部分资源来存储和分析跟踪数据,是Dapper性能影响中最关键的部分:
- 因为收集和分析可以更容易在紧急情况下被关闭,ID生成耗时、创建Span等;
- 修改agent nice值,以防在一台高负载的服务器上发生cpu竞争;
采样
如果一个显着的操作在系统中出现一次,他就会出现上千次,基于这个事情我们不全量收集数据。
2.3 跟踪采样
固定采样
例如:1/1024
这个简单的方案是对我们的高吞吐量的线上服务来说是非常有用,因为那些感兴趣的事件(在大吞吐量的情况下)仍然很有可能经常出现,并且通常足以被捕捉到。
然而,在较低的采样率和较低的传输负载下可能会导致错过重要事件,而想用较高的采样率就需要能接受的性能损耗。对于这样的系统的解决方案就是覆盖默认的采样率,这需要手动干预的,这种情况是我们试图避免在 Dapper 中出现的。
应对积极采样:
我们理解为单位时间期望采集样本的条目,在高 QPS 下,采样率自然下降,在低 QPS 下,采样率自然增加;比如1s内某个接口采集1条。
二级采样:
问题: 容器节点数量多,即使使用积极采样仍然会导致采样样本非常多,所以需要控制写入中央仓库的数据的总规模,利用所有 span 都来自一个特定的跟踪并分享同一个 traceid 这个事实,虽然这些 span 有可能横跨了数千个主机。
解决方案:
- 对于在收集系统中的每一个 span,我们用hash算法把 traceid 转成一个标量Z ,这里0<=Z<=1,我们选择了运行期采样率,这样就可以优雅的去掉我们无法写入到仓库中的多余数据。
- 我们还可以通过调节收集系统中的二级采样率系数来调整这个运行期采样率,最终我们通过后端存储压力把策略下发给 agent采集系统,实现精准的二级采样。
下游采样:
越被依赖多的服务,网关层使用积极采样以后,对于 downstream 的服务采样率仍然很高。
3. 指标
3.1 四个黄金指标
涉及到 net、cache、db、rpc 等资源类型的基础库
- 延迟(耗时,需要区分正常还是异常)95分位,99分位
- 流量(需要覆盖来源,即:caller)
- 错误(覆盖错误码或者 HTTP Status Code)
- 饱和度(服务容量有多“满”)
3.2 系统层面
- CPU,Memory,IO,Network,TCP/IP状态等,FD(等其他),Kernel:Context Switch
- Runtime:各类 GC、Mem 内部状态等
4. 参考资料
- https://juejin.cn/post/6922811488784613389
- 《极客大学-Go进阶训练营》