微服务监控及问题定位

监控 - 发现故障的征兆

在高并发分布式的场景下,故障经常是突然间就雪崩式爆发。所以必须建立完善的监控体系,尽可能发现故障的征兆。

微服务架构中组件繁多,各个组件所需要监控的指标不同。比如Redis缓存一般监控占用内存值、网络流量,数据库监控连接数、磁盘空间,业务服务监控并发数、响应延迟、错误率等。因此如果做一个大而全的监控系统来监控各个组件是不大现实的,而且扩展性会很差。一般的做法是让各个组件提供报告自己当前状态的接口(metrics接口),这个接口输出的数据格式应该是一致的。然后部署一个指标采集器组件,定时从这些接口获取并保持组件状态,同时提供查询服务。最后还需要一个UI,从指标采集器查询各项指标,绘制监控界面或者根据阈值发出告警。

大部分组件都不需要自己动手开发,网络上有开源组件。小海下载了 RedisExporter 和 MySQLExporter,这两个组件分别提供了Redis缓存和MySQL数据库的指标接口。微服务则根据各个服务的业务逻辑实现自定义的指标接口。然后小明采用Prometheus作为指标采集器,Grafana配置监控界面和邮件告警。这样一套微服务监控系统就搭建起来了:

定位问题 - 链路跟踪

在微服务架构下,一个用户的请求往往涉及多个内部服务调用。为了方便定位问题,需要能够记录每个用户请求时,微服务内部产生了多少服务调用,及其调用关系。这个叫做链路跟踪。

我们用一个 Istio 文档里的链路跟踪例子来看看效果:

从图中可以看到,这是一个用户访问 productpage 页面的请求。在请求过程中,productpage服务顺序调用了details和reviews服务的接口。而reviews服务在响应过程中又调用了ratings的接口。整个链路跟踪的记录是一棵树:

要实现链路跟踪,每次服务调用会在HTTP的HEADERS中记录至少记录四项数据:

traceId: traceId标识一个用户请求的调用链路。具有相同traceId的调用属于同一条链路。
spanId:标识一次服务调用的ID,即链路跟踪的节点ID。
parentId:父节点的spanId。
requestTime & responseTime:请求时间和响应时间。

另外,还需要调用日志收集与存储的组件,以及展示链路调用的UI组件。

以上只是一个极简的说明,关于链路跟踪的理论依据可详见 Google 的 Dapper

了解了理论基础后,小明选用了 Dapper 的一个开源实现 Zipkin。然后手指一抖,写了个HTTP请求的拦截器,在每次HTTP请求时生成这些数据注入到HEADERS,同时异步发送调用日志到Zipkin的日志收集器中。这里额外提一下,HTTP请求的拦截器,可以在微服务的代码中实现,也可以使用一个网络代理组件来实现(不过这样子每个微服务都需要加一层代理)。

链路跟踪只能定位到哪个服务出现问题,不能提供具体的错误信息。查找具体的错误信息的能力则需要由日志分析组件来提供。

分析问题 - 日志分析

日志分析组件应该在微服务兴起之前就被广泛使用了。即使单体应用架构,当访问数变大、或服务器规模增多时,日志文件的大小会膨胀到难以用文本编辑器进行访问,更糟的是它们分散在多台服务器上面。排查一个问题,需要登录到各台服务器去获取日志文件,一个一个地查找(而且打开、查找都很慢)想要的日志信息。

因此,在应用规模变大时,我们需要一个日志的“搜索引擎”。以便于能准确的找到想要的日志。另外,数据源一侧还需要收集日志的组件和展示结果的UI组件:

推荐 ELK日志分析组件。

ELK 是 Elasticsearch、Logstash 和 Kibana 三个组件的缩写。

Elasticsearch:搜索引擎,同时也是日志的存储。

Logstash:日志采集器,它接收日志输入,对日志进行一些预处理,然后输出到Elasticsearch。

Kibana: UI组件,通过Elasticsearch的API查找数据并展示给用户。

最后还有一个小问题是如何将日志发送到 Logstash。一种方案是在日志输出的时候直接调用 Logstash 接口将日志发送过去。这样一来又(咦,为啥要用“又”)要修改代码……于是小明选用了另一种方案:日志仍然输出到文件,每个服务里再部署个Agent扫描日志文件然后输出给Logstash。