分布式架构的观测


在一个分布式应用中,如果出现了某个异常,那我们必然不可能只依靠 awk、grep 等命令来查看日志分析问题,往往分布式架构的一个异常都贯通多个节点,我们需要将多个节点联系起来排查问题。这就引出了分布式架构的可观测性,可观测性越高,排查问题越轻松

学术界一般会将可观测性分解为三个更具体方向进行研究,分别是:事件日志、链路追踪和聚合度量,这三个方向各有侧重,又不是完全独立

日志

日志的职责是记录离散事件,通过这些记录事后分析出程序的行为,譬如曾经调用过什么方法,曾经操作过哪些数据

如何打出优秀的日志是程序员的基本功,如果日志量太大会造成 OOM,如果日志经常打 error 会导致监控报警特别大

只要稍微复杂点的系统,尤其是复杂的分布式系统,往往还要有专门的全局查询和可视化功能。此时,从打印日志到分析查询之间,还隔着收集、缓冲、聚合、加工、索引、存储等若干个步骤。我们简单的梳理一下日志存储与打印的流程,下面所说的流程,就是 ELK 技术栈

日志的输出

日志输入就是我们在代码中使用 log 提供的方法输出,当然 print 等方法也可以,不过不推荐。这块主要是开发的工作,打出优秀的日志可以便于排查问题,我们应该尽可能的避免如下几点:

  • 日志不能太多,也不能太少,不必把上下文的所有消息都打进去,否则会造成 IO 问题。有些不必要的 info 日志,在测试的时候可以打出来用于排查问题,但是项目上线后就需要删掉
  • 避免打印敏感信息。不用专门去提醒,任何程序员肯定都知道不该将密码,银行账号,身份证件这些敏感信息打到日志里
  • 避免引用慢操作。日志中打印的信息应该是上下文中可以直接取到的,如果当前上下文中根本没有这项数据,需要专门调用远程服务或者从数据库获取,又或者通过大量计算才能取到的话,那应该先考虑这项信息放到日志中是不是必要且恰当的

我们应当尽可能做到如下几点:

  • 处理请求时的 TraceID
  • 系统运行过程中的关键事件。日志的职责就是记录事件,进行了哪些操作、发生了与预期不符的情况、运行期间出现未能处理的异常或警告、定期自动执行的任务,等等,都应该在日志中完整记录下来
  • 启动时输出配置信息

收集与缓冲

写日志是在服务节点中进行的,但我们不可能在每个节点都单独建设日志查询功能。这不是资源或工作量的问题,而是分布式系统处理一个请求要跨越多个服务节点,为了能看到跨节点的全部日志,就要有能覆盖整个链路的全局日志系统。这个需求决定了每个节点输出日志到文件后,必须将日志文件统一收集起来集中存储、建立索引,由此便催生了专门的日志收集器

为了处理多个节点的协调,除了今天流行的日志收集器,轻量高效的 Filebeat 以外,还需要有专门用于其他功能的组件,比如用于审计数据的 Auditbeat、用于无服务计算架构的 Functionbeat、用于心跳检测的 Heartbeat 等等

至于缓冲,是指流量大的时候,会有过多的日志打进 DB,导致数据库压力过大。一种最常用的缓解压力的做法是将日志接收者从 Logstash 和 Elasticsearch 转移至抗压能力更强的队列缓存,譬如在 Logstash 之前架设一个 Kafka 或者 Redis 作为缓冲层

加工与聚合

将日志集中收集之后,存入 Elasticsearch 之前,一般还要对它们进行加工转换和聚合处理。这是因为日志是非结构化数据,一行日志中通常会包含多项信息,如果不做处理,那在 Elasticsearch 就只能以全文检索的原始方式去使用日志,既不利于统计对比,也不利于条件过滤

为了加快查询速度,我们必须要建立索引,但是对一串巨大的字符串直接建立索引肯定不是优秀的解法,我们可以根据输出日志的特征、形式,来对字符串做拆分,拆出来的数据就可以建立不同的索引了

这就引出了我们处理加工聚会的工具 Logstash。Logstash 的基本职能是把日志行中的非结构化数据,通过 Grok 表达式语法转换为上面表格那样的结构化数据,然后以 JSON 格式输出到 Elasticsearch 中(这是最普遍的输出形式,Logstash 输出也有很多插件可以具体定制不同的格式)

经过 Logstash 转换,已经结构化的日志,Elasticsearch 便可针对不同的数据项来建立索引,进行条件查询、统计、聚合等操作的了

存储与查询

在经过一系列的数据处理后,我们终于可以把数据放进 DB 了,也就是 ES Elasticsearch

Elasticsearch 只提供了 API 层面的查询能力,它通常搭配同样出自 Elastic.co 公司的 Kibana 一起使用,可以将 Kibana 视为 Elastic Stack 的 GUI 部分

Kibana 尽管只负责图形界面和展示,但它提供的能力远不止让你能在界面上执行 Elasticsearch 的查询那么简单。Kibana 宣传的核心能力是“探索数据并可视化”,即把存储在 Elasticsearch 中的数据被检索、聚合、统计后,定制形成各种图形、表格、指标、统计,以此观察系统的运行状态,找出日志事件中潜藏的规律和隐患

Kibana 标准查询语法

一般来说,直接加上双引号查询肯定没错,比如:

"hello"

标准的查询需要按照 key:value 形式。key 用于指定查询的字段类型

local_ip:111.111.111.111
traceid:EMqATv-DAooV

数字范围查询和字段查询通过 AND、OR 等结合使用,注意连接符需要大写

"value1" AND "value2"

追踪

微服务时代,追踪就不只局限于调用栈了,一个外部请求需要内部若干服务的联动响应,这时候完整的调用轨迹将跨越多个服务,同时包括服务间的网络传输信息与各个服务内部的调用堆栈信息。追踪的主要目的是排查故障,如分析调用链的哪一部分、哪个方法出现错误或阻塞,接口的输入输出是否符合预期

数据收集

追踪数据一般可以使用以下三种方式来实现

  • 基于日志的追踪:我们将 TrackID 打进日志里,然后随着所有节点的日志归集过程汇聚到一起,再从全局日志信息中反推出完整的调用链拓扑关系。打入日志这个操作完全可以由插件实现,做到对开发透明,但是缺点是由于日志归集不及时或者精度丢失,导致日志出现延迟或缺失记录的话,会进而产生追踪失真
  • 基于服务的追踪:这是目前最主流的追踪方式,服务追踪的实现思路是通过某些手段给目标应用注入追踪探针(Probe),探针在结构上可视为一个寄生在目标服务身上的小型微服务系统,把从目标系统中监控得到的服务调用信息,通过另一次独立的 HTTP 或者 RPC 请求发送给追踪系统
  • 基于边车代理的追踪:我们知道边车模式是对服务加一层代理,通过代理来对外层进行交互。边车代理是服务网格的专属方案,也是最理想的分布式追踪模型。它对应用完全透明,无论是日志还是服务本身都不会有任何变化;它与程序语言无关,无论应用采用什么编程语言实现,只要它还是通过网络来访问服务就可以被追踪到

业务追踪

Dapper 提出了追踪与跨度两个概念。从客户端发起请求抵达系统的边界开始,记录请求流经的每一个服务,直到到向客户端返回响应为止,这整个过程就称为一次追踪(Trace)。由于每次 Trace 都可能会调用数量不定、坐标不定的多个服务,为了能够记录具体调用了哪些服务,以及调用的顺序、开始时点、执行时长等信息,每次开始调用服务前都要先埋入一个调用记录,这个记录称为一个跨度

让我举个例子来说明上面文字的重要性:

现在有一个很长的业务场景:用户下单,其中必定经过很多流程,比如拿产品信息,校验用户信息,生单,回调,通知其他系统,调支付接口,等等等等,如果用户生单失败了,他们很有可能过来问我们发生了什么问题,这时候如果我们的日志打的很杂很乱,不记录关键步骤,这时候查这个问题就会非常麻烦

而如果我们将这些关键节点记录下来(比如拿完商品信息记录一下,生单完毕记录一下),记录到一个专门的业务日志中,比如 core.log,这时候我们查问题就会非常简单了,只需要拿一个可以贯穿整个流程的 ID(比如用户 ID)去这个日志中搜一下,就可以拿到数据了。这个日志甚至可以放权给运营或者用户自己使用

度量(监控和预警)

度量是指对系统中某一类信息的统计聚合。譬如,证券市场的每一只股票都会定期公布财务报表,通过财报上的营收、净利、毛利、资产、负债等等一系列数据来体现过去一个财务周期中公司的经营状况,这便是一种信息聚合。像是任务管理器,就是度量的一种

度量(Metrics)的目的是揭示系统的总体运行状态,因此可以拆分为监控(Monitoring)和预警(Alert),如某些度量指标达到风险阈值时触发事件,以便自动处理或者提醒管理员介入

打完了日志之后,别忘了监控报警。在我们做好核心日志追踪后,可以接着搭建业务追踪告警。这样在项目发布时,如果变更比较多的情况下,可以通过观察告警指标来观察我们的系统是否健康。而告警指标可以通过项目的核心链路来搭建

指标

指标收集部分要解决两个问题:如何定义指标以及如何将这些指标告诉服务端, 如何定义指标这个问题听起来应该是与目标系统密切相关的,必须根据实际情况才能讨论,其实并不绝对,无论目标是何种系统,都是具备一些共性特征。确定目标系统前我们无法决定要收集什么指标,但指标的数据类型(Metrics Types)是可数的,所有通用的度量系统都是面向指标的数据类型来设计的:

  • 计数度量器(Counter):这是最好理解也是最常用的指标形式,计数器就是对有相同量纲、可加减数值的合计量,譬如业务指标像销售额、货物库存量、职工人数等等;技术指标像服务调用次数、网站访问人数等都属于计数器指标;预警指标是监控系统健康性最重要的数据,包含发生异常次数以及时间
  • 吞吐率度量器(Meter):吞吐率度量器顾名思义是用于统计单位时间的吞吐量,即单位时间内某个事件的发生次数。譬如交易系统中常以 TPS 衡量事务吞吐率,即每秒发生了多少笔事务交易
  • 时间度量器(Timer):用来统计一个方法或者一个接口的执行时间,然后我们收集 P98 或者 P95 的信息来进行预警,对接口优化

我们现在定义了如何收集这些数据,但是还没有定义如何使用这些数据。优秀的使用方式同样重要,如果错误的使用指标(比如只要每个计数度量器一分钟记了一次就报警一次),监控会瞎报警,从而让程序员忽视掉真正有用的报警。这个问题被称作报警噪音

数据采集方式

而如何将这些指标告诉服务端这个问题,通常有两种解决方案:拉取式采集推送式采集,所谓 Pull 是指度量系统主动从目标系统中拉取指标,相对地,Push 就是由目标系统主动向度量系统推送指标。不管是拉取还是推送,在进行操作之前,指标数据会记在内存中

存放指标一般不会选择 MySQL 或者 PG 等关系型数据库,而是时序数据库。时序数据库用于存储跟随时间而变化的数据,并且以时间(时间点或者时间区间)来建立索引的数据库

时间序列数据是历史烙印,具有不变性、唯一性、有序性。时序数据库同时具有数据结构简单,数据量大的特点

时序数据通常只是追加,很少删改或者根本不允许删改。针对数据热点只集中在近期数据、多写少读、几乎不删改、数据只顺序追加这些特点,时序数据库被允许做出很激进的存储、访问和保留策略:

  • 以日志结构的合并树代替传统关系型数据库中的 B+Tree 作为存储结构,LSM 适合的应用场景就是写多读少,且几乎不删改的数据
  • 设置激进的数据保留策略,譬如根据过期时间自动删除相关数据以节省存储空间,同时提高查询性能。对于普通数据库来说,数据会存储一段时间后就会被自动删除这种事情是不可想象的
  • 对数据进行再采样以节省空间,譬如最近几天的数据可能需要精确到秒,而查询一个月前的数据时,只需要精确到天,查询一年前的数据时,只要精确到周就够了,这样将数据重新采样汇总就可以极大节省存储空间

系统打点准则

为了更好的度量我们的系统,以下有一些类似最佳实践的经验,先说一下在什么地方打点,最最简单的度量就是帮助我们快速召回系统中的问题,即让系统中的每个 bug 都由我们程序员提前发现,这是非常有用的,用户满意发现的 bug 就不算 bug。因此,系统中每次抛出异常的时候,我们可以在以下位置打点:

1,定时任务 try catch 单独打点,如果可以抛出异常,可以使用切面打点
2,rpc 接口如果不能直接把异常抛出,并且返回值没有统一的格式,也是单独打点。如果可以抛出异常,则使用切面打点
3,MQ 的处理和上面一样
4,如果是 http 接口并且抛出异常的话,可以使用全局异常处理器打点
5,如果 http 接口是正常的返回,但是返回的业务码不是正常的,可以切面打点(需要收集系统中存量的所有返回结构数据,比如什么 CommonQueryResult、CommonResult 等,一个系统中往往有多个统一结果返回,但是每个返回都有 code,我们可以根据 code 来区分是异常还是正常返回)
6,不要在 service 层拦截,会打两次点的

知道了在什么地方打点,我们还需要知道针对什么异常打点,如果每次出现已经预知的异常或者业务异常都给我们发告警,未免太影响休息了,因此我们需要对异常分级,分级的意思是将异常分为 info、error、warn 级别,比如:

  • 用户传入参数没有通过校验、用户无权限、输入的字符串过长等业务异常问题,可以将异常记为 info 级别。这些异常不需要重点关注,但是如果一个小时内出现了十万条这样的点,那说明要么是代码写错了,要么是有人在刷我们接口,无论如何,此时都需要通知我们处理
  • 下游接口调用失败、RPC 调用失败等问题,可以将异常标记为 warn,因为这不是我们系统导致的,确保我们做好了异常降级,然后打点即可。我们可以在看到这些异常后疯狂艾特下游,但是不用过多关注这些 warn
  • 空指针等系统异常都需要标记为 error,但凡是系统异常影响用户体验的问题,都必须是 error,一旦出现就需要报警,快速处理,不让用户感知

举个例子:

	SERVICE_ERROR(0, "系统错误", ExceptionLevelEnum.ERROR),
    BIZ_ERROR(101, "业务错误", ExceptionLevelEnum.WARN),
    LOCK_ALREADY_EXISTS(102, "操作太频繁,请稍后再重试", ExceptionLevelEnum.INFO),

我们拿到枚举值中的 LevelEnum 后,就知道需要打什么级别的日志了

我们在打点的时候所关注的核心数据是什么呢?我们在打点需要打出哪些数据呢:哪里的方法抛出的异常(方法名),异常内容,异常分级级别,要是分不了也没关系,只打印最核心的系统异常(因为代码问题导致流程中断的异常)即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值