[转载]数据库全量SQL分析与审计系统性能优化之旅


1 背景 数据库安全一直是美团信息安全团队和数据库团队非常注重的领域,但由于历史原因,对数据库的访问只具备采样审计能力,导致对于一些攻击事件无法快速地发现、定损和优化。安全团队根据历史经验,发现攻击访问数据库基本上都存在着某些特征,经常会使用一些特定SQL,我们希望通过对MySQL访问流量进行全量分析,识别出惯用SQL,在数据库安全性上做到有的放矢。

2 现状及挑战 下图是采样MySQL审计系统的架构图,数据采集端基于pcap抓包方式实现,数据处理端选用美团大数据中心的日志接入方案。所有MySQL实例都部署了用于采集MySQL相关数据的rds-agent、日志收集的log-agent。rds-agent抓取到MySQL访问数据,通过log-agent上报到日志接收端,为了减少延时,上报端与接收端间做了同机房调度优化。日志接收端把数据写入到约定的Kafka中,安全团队通过Storm实时消费Kafka分析出攻击事件,并定期拉数据持久化到Hive中。

我们发现,通常被攻击的都是一些核心MySQL集群。经统计发现,这些集群单机最大QPS的9995线约5万次左右。rds-agent作为MySQL机器上的一个寄生进程,为了宿主稳定性,资源控制也极为重要。为了评估rds-agent在高QPS下的表现,我们用Sysbench对MySQL进行压测,观察在不同QPS下rds-agent抓取的数据丢失率和CPU消耗情况,从下面的压测数据来看结果比较糟糕:

QPS 丢失率 CPU利用率 10368.72 1.03% 307.35% 17172.61 7.23% 599.90% 29005.51 28.75% 662.39% 42697.05 51.73% 622.34% 50833.50 63.95% 601.39% 如何在高QPS下保证较低的丢失率与CPU消耗?已经成为当前系统的一个亟待解决的难题与挑战。

3 分析及优化 下面主要介绍围绕丢失率与CPU消耗这一问题,我们对数据采集端在流程、调度、垃圾回收和协议方面做的分析与改进。

3.1 数据采集端介绍 首先,简要介绍一下数据采集端rds-agent,它是一个MySQL实例上的进程,采用Go语言编写,基于开源的MysqlProbe的Agent改造。通过监听网卡上MySQL端口的流量,分析出客户端的访问时间、来源IP、用户名、SQL、目标数据库和目标IP等审计信息。下面是其架构图,主要分为5大功能模块:

  1. probe

probe意为探针,采用了gopacket作为抓包方案,它是谷歌开源的一个Go抓包库,封装了pcap。probe把抓取到原始的数据链路层帧封装成TCP层的数据包。通过变种的Fowler-Noll-Vo算法哈希源和目的IP port字段,快速实现把数据库连接打散到不同的worker中,该算法保证了同一连接的来包与回包的哈希值一样。

  1. watcher

登录用户名对于审计来说极其重要,客户端往往都是通过长连接访问MySQL,而登录信息仅出现在MySQL通信协议的认证握手阶段,仅通过抓包容易错过。

watcher通过定时执行show processlist获取当前数据库的所有连接数据,通过对比Host字段与当前包的客户端ip port,补偿错过的用户名信息。

  1. worker

不同的worker负责管理不同数据库连接的生命周期,一个worker管理多个连接。通过定期比对worker的当前连接列表与watcher中的连接列表,及时发现过期的连接,关闭并释放相关资源,防止内存泄漏。

  1. connStream

整个数据采集端的核心逻辑,负责根据MySQL协议解析TCP数据包并识别出特定SQL,一个连接对应一个connStream Goroutine。因为SQL中可能包含敏感数据,connStream还负责对SQL进行脱敏,具体的特定SQL识别策略,由于安全方面原因,这里不再进行展开。

  1. sender

负责数据上报逻辑,通过thrift协议将connStream解析出的审计数据上报给log-agent。

3.2 基础性能测试 抓包库gopacket的性能直接决定了系统性能上限,为了探究问题是否出在gopacket上,我们编写了简易的tcp-client和tcp-server,单独对gopacket在数据流向图中涉及到的前三个步骤(如下图所示)进行了性能测试,从下面的测试结果数据上看,性能瓶颈点不在gopacket。

QPS pcap缓冲区 丢失率 CPU利用率 100000 100MB 0% 144.9% 3.3 CPU画像分析 丢失率与CPU消耗二者密不可分,为了探究如此高CPU消耗的原因,我们用Go自带的pprof工具对进程的CPU消耗进行了画像分析,从下面火焰图的调用函数可以归纳出几个大头:SQL脱敏、解包、GC和Goroutine调度。下面主要介绍一下围绕它们做的优化工作。

3.4 脱敏分析及改进 因为SQL中可能包含敏感信息,出于安全考虑,rds-agent会对每一条SQL进行脱敏处理。

脱敏操作使用了pingcap的SQL解析器对SQL进行模板化:即把SQL中的值全部替换成“?”来达到目的,该操作需要解析出SQL的抽象语法树,代价较高。当前只有采样和抓取特定SQL的需求,没有必要在解析阶段对每条SQL进行脱敏。这里在流程上进行了优化,把脱敏下沉到上报模块,只对最终发送出去的样本脱敏。

这个优化取得的效果如下:

对比项 QPS 丢失率 CPU利用率 改进前 50833.50 63.95% 601.39% 改进后 51246.47 31.95% 259.59% 3.5 调度分析及改进 从下面的数据流向图可以看出整个链路比较长,容易出现性能瓶颈点。同时存在众多高频运行的Goroutine(红色部分),由于数量多,Go需要经常在这些Goroutine间进行调度切换,切换对于我们这种CPU密集型的程序来说无疑是一种负担。

针对该问题,我们做了如下优化:

缩短链路:分流、worker、解析SQL等模块合并成一个Goroutine解析器。 降低切换频率:解析器每5ms从网络协议包的队列中取一次,相当于手动触发切换。(5ms也是一个多次测试后的折中数据,太小会消耗更多的CPU,太大会引起数据丢失) 这个优化取得的效果如下:

对比项 QPS 丢失率 CPU利用率 改进前 51246.47 31.95% 259.59% 改进后 51229.54 0% 206.87% 3.6 垃圾回收压力分析及改进 下图为rds-agent抓包30秒,已分配指针对象的火焰图。可以看出已经分配了4千多万个对象,GC压力可想而知。关于GC,我们了解到如下两种优化方案:

池化:Go的标准库中提供了一个sync.Pool对象池,可通过复用对象来减少对象分配,从而降低GC压力。 手动管理内存:通过系统调用mmap直接向OS申请内存,绕过GC,实现内存的手动管理。

但是,方案2容易出现内存泄漏。从稳定性的角度考虑,我们最终选择了方案1来管理高频调用函数里创建的指针对象,这个优化取得的效果如下:

对比项 QPS 丢失率 CPU利用率 改进前 51229.54 0% 206.87% 改进后 51275.11 0% 153.32% 3.7 解包分析及改进 MySQL是基于TCP协议之上的,在功能调试过程中,我们发现了很多空包。从下面的MySQL客户端-服务端数据的交互图可以看出:当客户端发送一条SQL命令,服务端响应结果,由于TCP的消息确认机制,客户端会发送一个空的ack包来确认消息,而且空包在整个流程中的比例较大,它们会穿透到解析环节,在高QPS下对于Goroutine调度和GC来说无疑是一个负担。

下图是MySQL数据包的唯一格式,通过分析,我们观察到以下特点:

一个完整的MySQL数据包长度>=4Byte 客户端新发送命令的sequence id都是为0或者1 而pcap支持设置过滤规则,让我们可以在内核层将空包排除掉,下面是上述特点对应的两条过滤规则:

特点1: ip[2:2] - ((ip[0] & 0x0f) << 2) - ((tcp[12:1] & 0xf0) >> 2) >= 4 特点2: (dst host {localIP} and dst port 3306 and (tcp[(((tcp[12:1] & 0xf0) >> 2) + 3)] <= 0x01)) 这个优化取得的效果如下:

对比项 QPS 丢失率 CPU利用率 改进前 51275.11 0% 153.32% 改进后 51246.02 0% 142.58% 基于上述经验,我们对数据采集端进行功能代码重构,同时还进行一些其它优化。

4 最终成果 下面是优化前后的数据对比,丢失率从最高60%下降到了0%, CPU消耗从最高占用6个核下降到了1个核。


分类:Java
标签: 转载
文章目录