在互联网高速发展的过程中,随着分布式、微服务、容器化架构的流行,互联网已全面进入云原生时代。构建系统的方式由最初的单体大应用演变为分布式架构,一台服务器可能仅存几小时甚至几分钟,这种复杂性大大增加了把系统运行状态可视化的难度。

为了应对上述情况,希望能够对系统进行360全方位立体化的监控,于是可观测性概念应运而生,可观测性的提出最早来自于 Google 著名的 SRE 体系和 Apple 工程师 Cindy Sridharan 的博文《Monitoring and Oberservability》也就是说在可观测性等概念出现以前,前辈们将这套理论称之为监控,Baron SchSchwarz 大咖的一个定义:“监控告诉我们系统的哪些部分是工作的。可观测性告诉我们那里为什么不工作了。”

可观察性是一套理念系统,没有对技术的具体要求,可观察性包含在你的需求之中,它是与扩展性,可用性同等重要的非业务性需求,更多偏向于是一种理念,可观测是监控的超集,它包括监控、告警、可视化、分布式追踪与日志分析。监控是通过观测其核心关键指标展现系统的状态,并报告异常情况,而可观测性在此基础上增加了问题定位的能力,通过可视化、分布式追踪和日志分析功能来提供给用户交互式定位问题的能力。

可观测性可以概括为三个方面分别是:Logging日志、Metrics指标、Tracing链路追踪,通过这三类数据来构建完整的可观测性系统,通过多形式、多维度、多视图、多场景来帮助发现系统瓶颈、消除隐患。

第十六篇:稳定性之可观测性_可靠性

Logging日志

Logging日志,日志是系统中比较常见且很基础的功能,日志记录了函数运行中的关键信息,这些信息是离散且具体的,结合错误日志与函数代码可以迅速定位问题。

日志在系统中是非常容易生成的,如果它大量地输出,会占据比较大的存储空间,记录日志的过程中也是会影响整个应用程序的性能,当前多数日志框架,采用异步来输出日志,那么异步的情况也会损耗应用的性能。日志一方面可以帮助我们排查和定位问题,另一方面可利用日志追溯,结构化的特点来实现相关功能的,比如我们最常见的 WAL(Write-Ahead Logging)。WAL 就是在操作之前先进行日志写入,再执行操作;如果没有执行操作,那么在下次启动时就可以通过日志中结构化的,有时间标记的信息恢复操作,其中较为典型的就是 MySQL 中的 Redo log。

Mysql之所以能够利用日志追溯是定义了一套标准化日志格式,那么对于应用系统该如何日志标准化,或者说打印日志有哪些要求呢,见过很多工程师在出现问题是要么后悔当初忘记打印日志,要么是这个日志我应该加个什么参数就好了,要不日志量太大,无从下手,可读性差,还有日志参考的价值不大。那么究竟该如何有效的打印日志呢?

如何有效打印日志

如何有效打印日志这个可以说是一门学问,日志是在工程师编码的过程中打印或记录的日志,而不能说事后需要的时候才增加,当然也会有这种情况,可能是事前打印日志不够完整,无法提供有价值的参考;当我们需要日志时,说故障已产生,可想解决问题效率是怎样的。下面从分别从几个维度,来解读如何有效打印日志

日志编写

占位符

日志编写常见有两种形式,分别是字符串拼接形式、占位符形式,这里提倡使用占位符的这种形式,使用占位符不管是性能还是友好性都要优于字符串拼接这种形式。如下面两个列子

示例一:
func save_user_info(username,address string) {
log.Printf("save_user_info failed username:"+username+",address: "+address)
}

示例二:
func save_user_info(username,address string) {
log.Printf("save_user_info failed username:%s,address: %s ",username,address)
}

在生成较高级别的日志时,低级别的日志不会一直拼接字符串,那么在拼接字符串过程中会占用过多的内存、CPU资源,会导致资源浪费。

通过上述两个示例,可以清晰看到日志想表达的内容,及所需要编写的参数,这样在写日志时,目的也会更加明确,对于工程师而言,也相对更友好些。

日志格式

日志格式常见有两种形式,分别是字符串形式、JSON形式;这两种方式各有各的优势,字符串形式是通过分割符来进行分割成数据,通过数组下标,标识各个含义,简单、存储数据量小;JSON形式更友好些,可以通过自定义的key进行解析,格式化更友好,存储数据较大。这里提倡使用JSON形式,在分布式架构下,多数情况下通过日志平台进行日志统一采集和存储,可视化展示,这样做的好处是更友好,效率也比较高。业界当前比较常用的是通过ELK(Elasticsearch、Logstash、Kibana)来构建日志平台。

字符串形式:

11.128.48.10 - - [05/Aug/2021:14:57:01 +0800] "GET /general/subjectShowSort HTTP/1.1" 200 46 "-" "Go-http-client/1.1"

JSON形式:

{"x_devid":"-","x_logdate":"2021-08-05T14:45:37+08:00", "x_level":"info", "x_source":"log.go:34:30510000002129","x_server_ip":"127.0.0.1", "x_module": "[dbdao]","x_department": "","x_version": "0.1","x_timestamp":1628145937,"x_date":"2021/08/05 14:45:37 CST","x_tag":"[dao]","x_msg":"","x_duration":0}

日志编写时,尽可能一行记录且保持完整性,如果多行记录日志,友好性和可读性比较差。在这个过程中产生的日志比较多,不便于查看。

日志精简

日志精简是减少无用垃圾信息输出,输出的信息必然是有价值的,只有这样日志才是有效的。如下面两个列子

示例一:

func save_user_info(username string) {
var arr = [...]int{1, 2, 3, 4, 5}
for _, value := range arr {
log.Printf("save_user_info value : %s ",value)
}
}
上述示例,循环打印日志,对于打印这种日志信息,对于我们来看不出来什么端倪,无法提供可参考的价值。

对于关键步骤节点,只打印对我们需要的信息,在打印日志时,要思考这个日志是否能够为其提供参考,如不能那么其就意义。可以将示例一修改为:

func save_user_info(username string) {
var arr = [...]int{1, 2, 3, 4, 5}
log.Printf("save_user_info username : %s , arrlen: %s ",username,len(arr))
}

示例二:

将接口返回结果,全部打印到日志中,那么显然这种打印的数据是没有任何意义的,其实并不关心结果内容是什么,可能只关心接口状态码,如Http Code码及业务自身定义的业务Code码,根据这两者来判断接口状态。

日志级别

日志级别是用来控制日志的输出形式,在线上环境,不建议使用debug模式,一旦开启debug模式,那么会有大量的日志数据,可能会对性能有一定的影响,故建议开启info或者及以上。如果开启info以上,那么出现问题该如何用debug模式来排查和定位问题呢,可以通过一种影子的方式,说白就是通过请求头进行设置,来判断该请求是否开启debug,注意是只针对该请求路径,在golang语言中,可以通过context进行上下传递。对于这种方式需要在设计日志组件时,将这种方式考虑到设计中。

日志可读性

日志可读性通常是将日志分类按照级别或者类型进行分类,及日志特殊标识,日志分类是避免将所有日志打印到同一文件,可以将日志分为request、error、info、debug日志,这样对日志查询效率高,减少信息干扰,不同类型的日志,数据格式也不同,便于格式化及统一管理。日志特殊标识是能够通过特殊的标识快速寻找出想要的日志数据,避免大海捞针,下面来看个例子:

示例一:

func save_user_info(username string) {
if !valid_username(username){
log.Printf("valid failed")

return
}
}

这样的日志毫无意义,只告诉验证失败。
示例二:

func save_user_info(username string) {
if !valid_username(username){
log.Printf("valid_username failed")

return
}
}

相对上述列子而言,告知我们valid_username验证失败,但是并清楚那个用户名称验证失败

示例三:
func save_user_info(username string) {
if !valid_username(username){
log.Printf("valid_username failed username:%s",username)
}
}

通过上述日志可以看出那个是那个用户名称,什么校验失败,这样的日志才有效的。

日志中的特殊标识可以userId、requestId等;userId这种方式很常见,这里不过多说明,requestId是请求的唯一标识,这个唯一标识贯穿整个链路,通过这个requestId能够清晰的看到这个请求经过的所有日志,一般情况会配合链路追踪系统来使用,可跨应用,跨中间,在分布式系统中实现全链路追踪。

日志注意事项

行号慎用

在实际应用场景中,在日志中打印行号,可以快速帮助我们定位代码片段,有助于提升问题定位的效率,特别是异常情况下,日志在获取行号的过程中是比较损耗性能,通常是通过获取线程堆栈的信息来实现。在 log4j 的官方文档中有这样一段话:“使用同步方式进行获取位置信息会慢 1.3 到 5 倍,如果是使用异步日志,因为会涉及跨线程获取位置信息,会慢 30 到 100 倍。所以,对于是否要在日志中显示行号这行为要慎重。

避免副作用

在实际场景中,在打印日志的过程,避免出现符作用,说白了是别因为打印日志而影响应用程序的可靠性,日志本身是帮助我们定位问题的。例如打印日志时,对象是空的,继续获取该对象的数据,那么可能会导致空指针的问题。

定期Review

日志是随着业务变化的,而不是一成不变的,对于新的业务功能,那么日志也需要定期观察和维护的,验证打印的日志是否理想状态,避免腐蚀。

日志归档

日志归档是避免所有的日志都打同一个日志文件中,那么查询效率大大降低,上述说到对于日志归档要日志类型进行划分,那么按照类型分类之后,日志还可能比较大,需要对日志量进行合理评估,按照天或则小时进行拆分;还有一种形式按照日志大小,自动切割。

标准化

日志对于每个工程师而言,其重要性不言而喻,但是对于一个团队,那么日志标准化就变得尤其重要,如果团队日志是非标准化,不便于统一管理是其一,对问题排查和定位效率也非常低。通常日志会封装日志SDK组件,提供通用日志打印方式,好处是日志格式、规范均统一,包括影子动态打印debug模式,都可以SDK中实现,还可以生成RequestId,便于问题的排查和定位,也可提升效率,日志是基础,也是基石,要能够充分认识日志的重要性。

Metrics 指标

指标(Metrics)是在时间间隔内测量的数据的数字表示,可用于随时间确定服务或者组件的行为,一般情况下指标包括名称、值、标签、时间戳,还可利用数据建模和预测的力量来获取系统在当前或未来一段时间内的行为。例如聚合、汇总、相关性,通过这些特征指标更来反馈服务的整体健康度。

第十六篇:稳定性之可观测性_稳定性_02

无论是操作系统还是服务,都可以提炼出指标,然后通过指标来度量,通常情况下指标就代表系统的运行状态,用于诊断系统的健康,对于指标的监控要求实时的,大多数是通过拉的模式,很少也有推的模式,原因是这样做的好处是服务不受监控负载的影响,以下是基础层和中间件、应用层的监控参考:

  • 基础层:监控主机和底层资源,比如:CPU、内存、网络吞吐、硬盘 I/O、硬盘使用等。网络通信是互联网中最重要的基石之一,如果两台主机之间出现如网络延迟时间大、丢包率高这样的网络问题,会导致业务受阻。
  • 中间件:缓存 例如:命中率、读QPS、写QPS等;在例如MQ、消息挤压、消息生产速率、消费速率等等。
  • 应用层:HTTP 访问的吞吐量、响应时间、返回码、错误率。

度量指标这类主要采用时序数据进行存储,它是以事件发生事件及当前数据值的角度来记录的监控信息,是可以聚合运算,一般度量类型有Gauges(度量)、Counters(计数器)、Histograms(直方图)、Meters(TPS计算器)、Timers(计时器)来观测指标数据和指标数据的趋势。指标(Metrics)一般包括名称、指标值、标签、时间戳五大类,基于时序数据库的指标比较适合做监控告警,原因是查询效率比较高,操作相对也比较简单。

指标和日志相比最大的优势在于实时性,数据包小;标传输和存储具有恒定的开销,存储方式也不相同,指标的成本不会随着用户的流量或者其他可能导致数据上升的活动而同步增加。而日志的成本随着用户流量的增加而增加,如磁盘的利用率,处理复杂性,可视化速度。

例如,可以收集服务的正常运行时间、响应时间、每秒的请求数以及服务的处理能力及内存指标,当系统超过指定阈值时,可以通过使用指标来触发报警;再例如,假设你要监控HTTP服务中的每秒请求数,如果突然流量激增,并想知道系统的中发生了什么,指标可以更深入的可见性和洞察力,便于了解服务的峰值,或许峰值可能由于不正确的服务配置、恶意行为,其他的服务引起的问题等,除了提供可见性指纹,还可以通过这些信息来检测和确定问题的严重性。

当前业界比较流行的是Prometheus结合Grafana统一监控告警平台,而Prometheus采用的也是拉的模式。

Tracing 链路追踪

Tracing 链路追踪,尽管在日志和指标可能足以了解单个系统的行为和性能,但它们还是不能提供并满足分布式系统中请求的整个生命周期相关信息,如果需要了解或者查看整个生命周期的信息,可能就需要Tracing链路追踪,尤其是微服务架构或者容器化的服务,通过分析跟踪数据,可以对整体系统健康状态、查明瓶颈,更快地识别和解决问题,并优先考虑优化和改进。

第十六篇:稳定性之可观测性_稳定性_03

通过上图可以看到一个请求,经过哪些服务,分别耗时情况,那么我在进行链路优化时,也依据可寻。

通过链路追踪还有以下作用:

  • 性能分析:通过聚合链路的数据,能够清楚哪些服务代码效率低下或者有问题,方便问题定位和耗时分析。
  • 拓扑图:通过对链路信息的聚合分析,能够根据数据绘制服务之间拓扑图,能够更直观地了解整个系统的构成。
  • 依赖关系:通过链路的聚合分析,可以了解到服务之间的依赖关系,从而快速感知操作之间的重要等级。这块对熔断降级相结合,可以更好低对核心关键链路的保护。

设计思路

链路追踪整体设计很复杂,其中包括数据采集、上报、分析、查询等多个模块,在这里简单介绍下两个模型,分别是Span Mode(跨度模型)和Context Mode(上下文模型)。

Span Mode (跨度模型) 有下几部分组成:

  • Trace ID,代表整个跟踪。
  • Span ID,代表当前span。
  • 一个operation name,描述此span执行的操作。
  • start timestamp(开始时间戳)。
  • finish timestamp(完成时间戳)。
  • 当前Span的Service和Service Instance名称。
  • 一组零个或多个键值对组成的的span tag。
  • 一组零个或多个span日志,每个span日志本身就是与时间戳配对的key:value映射。
  • 引用零个或多个因果相关的span。参考包括parent span id和trace id。

Context Mode(上下文模型):用于将客户端信息传播到原始RPC调用所携带的服务器端,通常在header(例如HTTP header或MQ header)中传播。

当前业界比较流行Skywalking、Zipkin、Pinpoint三款开源软件,各有各自的优势,不论是在扩展性和社区活跃性,Skywalking好像略胜一筹。

三者关联关系

第十六篇:稳定性之可观测性_可用性_04

通过上图其实已经很清晰看到Logging日志、Metrics指标、Tracing链路追踪者三者各自的关系了,

链路追踪+统计指标(Request-scoped metrics)请求级别的指标统计,在链路追踪的基础上,与相关的统计数据结合,从而得知数据与数据、应用与应用之间的关系,应用与应用之间的关系。

链路追踪+日志(Request-scoped events)请求级别的事件,这是链路中一个比较常见的组合模式。日志本身是每一条单独存在的,将链路追踪收集到的信息集成在日志中,可以让日志之间具备关联性,使其具有除了事件维度以外的另一个新的维度,上下文信息。

日志+统计指标(Aggregatable events)聚合级别的事件,这是在日志中的比较常见的组合。通过解析这部分具有统计指标的信息,我们可以获取相关的指标数据。

三者结合(Request-scoped,aggregatable events)三者结合可以理解为请求级别+聚合级别的事件,由此就形成了一个丰富的、全局的观测体系。

统计指标是数值的形式,同时又可以压缩,所以它所需的存储量是最小的;日志的输出量最大,但相对的,它也有比较全的内容记录;链路追踪则正好处于二者之间,它不会像日志一样大量地输出,也不像统计指标一样节能。

可观测性与监控区别

首先可观测性不等于监控,监控是有点类似于机器人,代替人工长期观测系统的行为和输出,从而帮助我们发现问题。可观测性是通过Metrics来制定系统核心关键指标,来反应系统的稳定性,一旦指标不达预期,继而结合Tracing、Logging来对核心关键指标的底层调用链进行抽丝剥茧,寻找出核心关键指标变化的本质原因。

关注点不同

监控更多关注是具体指标的变化,结合规则、报警,更多关注系统的失败因素,关注的是表现,比较单一,零散。而可观测性关注的应用本身的状态,是对系统的自我审视,而且是从内到外,更多是白盒监控,站在宏观的角度去聚合分析各种指标,还包括分布式系统的调用链的运行状态,便于关键指标不达预期时,能够清楚的知道本质原因,关注的是点和线,将两者相互结合。有点类似于下五子棋,单看每个点不成势,只有将每个点连成线,这样才能够形成势,但是呢也离不开某个点,因为每个点的都很重要。

目标不同

监控可以告诉应用在什么时间点,发生了什么问题,但是对于为什么发生却不能给更多的参考信息。而可观测性是为了告诉我们为什么发生了问题。如果你的系统具备可观测性,那么可以能直观的观测到系统的整体运行状态,上下游依赖关系,各个中间件的耗时情况等,在出现故障时,能够快速帮助我们定位和修复问题。

监控与可观测性是相辅相成的,监控是可观测性的一项重要基础设施,如果没有监控,就等于缺少了眼睛。那么只有监控是往往是不够的,对于出现故障时,首先想到是故障的本质原因是什么,只有清楚故障的原因,才能有效的解决。

小结

可观测性是通过日志记录离散事件,事后分析出程序的行为;链路追踪主要帮助我们对调用链进行分析,能看到某个请求的生命周期,如调用链中哪些什么服务,哪些耗时久;度量是对某一个指标通过聚合统计,通过对该数据可视化,能够帮助监控和告警,另外可以用于趋势分析和未来预测。