流量管理的过程主要有三个步骤:采集和发送监控数据、监控数据结构化存储、使用监控数据进行报警和管理控制,这三个流程由客户端、数据存储和管理端交互完成。
客户端:通过pom依赖集成在所有服务方应用,需配置过滤器和启动Bean。
数据存储:目前使用了Redis集群,3分片每分片1GB内存,目前存储100+服务所有流量两天的监控信息,使用了40%空间。
管理端:独立部署的应用,用于展示图表等监控信息、发送报警通知、配置降级限流和授权等功能的参数。
1 采集和发送监控数据
一、采集监控数据
监控系统为了采集监控数据,需要对每一次请求都进行记录,如果这一过程发生异常或占用较多资源将影响正常服务运行,因此采集客户端需要足够的轻量和高效,并隔离异常以确保不影响原流程。主流 RPC 框架(JSF、dubbo、rest 等)中都提供了 Filter 过滤器的扩展能力,我们通过在自定义的服务端过滤器中对请求进行记录和控制。
每一次请求必须的监控关键信息是服务信息(接口名、方法名)、来源应用(将来源 IP 自动转换为应用名,后面会解释如何转换)、时间戳,即监控每一个服务方法的每一个来源的每秒调用量。根据时间戳精度,如果记录每秒钟的调用量则每个服务每个调用来源一天需要 86400 个记录,显然这是一个比较庞大的记录量,一番权衡之后,目前记录每个服务每个调用来源 4 秒的请求量,除以 4 得到该时间范围的平均 TPS。
如何采集上述信息呢?我们在每个应用实例中创建一个静态的并发 Map,将每一个服务者和来源作为 key,对应值为该服务的一个原子计数器,每当调用时发生时进行计数器自增操作。同时在应用启动时会创建一个监控主线程,它通过定时器每 4 秒执行一次计划任务,这个任务需要一些技巧严格维持在每分钟的 0 秒、4 秒、8 秒……56 秒这样的时间戳执行,任务会将当前时间戳和 Map 中所有计时器统计结果发送到异步队列,同时清零所有原子计数器。由于这个过程没有耗时性任务,这样能保证精准完成统计信息并且不影响原服务流程。
二、发送监控数据
接下来如何发送采集的监控数据呢?为了尽量减少资源占用和减少复杂性,我们没有选择本地日志记录发送的方式,而是直接在消费队列的异步线程中将 4 秒一次的统计信息发送到 Redis 集群。针对每一个服务-调用来源-时间戳作为 key,调用量作为 value 在 redis 中进行存储,因此每次发送实际是一个个 INCRBY 操作,所有应用实例操作后就能得到整个集群准确的流量信息。
实际应用中这种方案会造成大量的 Redis 并发写入,从整个 Redis 集群监控中可以看到 4 秒一次的非常高的 TPS 波峰,一个简单的思路是将异步线程发送随机延后,这可以起到一定削峰效果,但是依然有很大优化空间,大量的 Redis 调用可以通过 pipeline 机制进行加速。使用 pipeline 进行批量操作不仅可以节约每次单独调用时的 RTT(网络来回延迟),还能减少 Redis 内部处理时的 IO 系统调用。经优化提高了 10 倍左右的性能。为了使用批量发送,需要将队列任务收集并积累为一批任务发送,我们通过队列的 poll 和超时检测,将未发生出队超时的(一批)任务批量一次发送。
2 监控数据结构化存储和读取
从轻量和效率等因素考虑,我们直接使用内部申请的 Redis 集群作为存储。
第一种办法:前面我们提到的key-value存储结构比较简单,它通过系统名-接口名-方法名-调用方应用名-时间戳这样的key,设置value为计数量,这是最简单但是最浪费存储空间的设计方式,因为key会非常长,优点是逻辑简单直接。
第二种办法:针对它的直接优化,将所有接口名、方法名等都转换为数字序列id,如0-0-0-0这样,这样key精简到非常简短,但同时增加复杂性,实际上空间占用还是很大。最好的办法是利用hash这类的数据结构进行存储,如key中包括系统名-接口名-方法名-调用方、filed中存储简短的时间戳、对应的value中存储计数量,这种方式能极大的减少存储空间。经测试按同样规则生产的100个1天范围的采集数据。
第三种办法:hash结构存储比第一种简单方式(带超时信息)减少了一半以上的存储空间。
在读取和生成监控图表的过程,经常会面对非常大时间范围,如果此时还使用秒级数据将导致查询结果非常多,数据会超过图表的像素容量,此时最好在存储时进行汇总,即通过后台任务将前一分钟秒级监控数据汇总为分钟级,较大范围查询时直接查询分钟级数据,类似的如果查询较高级别的查询则通过查询所有下一级数据进行汇总展示,同时读取批量数据也可以同 pipeline 进行批量读取加速。
3 报警和管理控制
**
**
一、监控管理端提供多维度页面进行来源和 TPS 等信息展示,可以迅速了解当前服务的流量来源和分布,此外可以针对每一个接口方法来源进行 TPS 报警和控制,通过预先配置的调用来源预估调用量,当超过阈值时进行报警通知。并可采取以下三种管理措施:
降级:根据配置开关,当触发熔断条件时进行自动或手动操作降级,降级后采集端的过滤器会直接对新请求新返回失败,在不影响其他调用方的情况下对服务端进行保护。
限流:与降级类似的方案是限流,由于分布式系统中进行总量限流会依赖中心节点进行令牌桶这类计算,同步调用对原接口影响较大,非严格的情况下并不是一个很通用的选择,这里我们使用了一个简单方案,即在每个服务节点进行单位时间固定量限制,即4秒内超过限制量的请求会被阻挡,虽然不是平滑限流,而且设置量也需要根据机器数进行调整,但该方案对正常的流量影响最小。
流量许可:除了对异常流量进行降级和限流外,还可以通过白名单机制对部分接口进行流量许可,没有预先加入白名单的视为非授权请求。
以上这些降级、限流以及白名单的配置存储在 Redis 中,客户端本地缓存在 map 中并每分钟刷新,在过滤器中通过简单规则匹配实现管理逻辑,不强依赖存储和管理端。
二、管理端还承担了各类定时任务,如定期每分钟汇总秒级数据到分钟级数据,将所有 IP 转换为应用名,各类系统控制开关和配置项等。
这里有个前文忽略的一点,在监控过滤器中的取到的调用方 IP,它是如何转换成应用名称呢?京东当前有多个应用部署平台,其中通过 J-ONE 发布的应用在调用时会携带应用名参数,但是其他部署平台就只能获得 IP 信息,我们通过访问统一的运维平台,主动查询 IP 地址到应用名的映射关系,进行存储并定期每天刷新,过滤器本地也保持有该映射的缓存并定期刷新,在减少影响和实时性之间作了权衡。由于生产环境不允许混合部署(一个实例多个应用),因此不会产生识别冲突,更多可能的延迟主要在应用缩容扩容时,可以通过缩容扩容主动刷新缓存的方案解决。
4 总结
受限于条件和目标,上述流量管理方案有不足可优化之处,如:
范围限制在了服务端(provider);
调用方客户端(consumer)也有更多扩展思路,例如限流或降级的开关应该设置在客户端,当降级时甚至不需要再发起远程调用请求;
超过几千台规模的应用实例的情况下可能会产生较高的Redis写入压力,可以通过增加Redis集群分片数量解决;
也可考虑其他同步和存储方案,如Kafka异步传输和HBase集群存储等。
但作为一个快速实现的监控管理系统,可以看出很多通用监控的思路和要点,同时加强了对微服务流量方面的治理,希望能对大家有所帮助,欢迎后续和作者一起探讨沟通。
评论