全量SQL(所有访问数据库的SQL)可以有效地帮助安全进行数据库审计,帮助业务快速排查性能问题。一般可通过开启genlog日志或者启动MySQL审计插件方式来进行获取,而美团选用了一种非侵入式的旁路抓包方案,使用Go语言实现。无论采用哪种方案,都需要重点关注它对数据库的性能损耗。
本文介绍了美团基础研发平台抓包方案在数据库审计实践中遇到的性能问题以及优化实践,希望能对大家有所帮助或启发。
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消耗?已经成为当前系统的一个亟待解决的难题与挑战。
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中,该算法保证了同一连接的来包与回包的哈希值一样。
2. watcher
登录用户名对于审计来说极其重要,客户端往往都是通过长连接访问MySQL,而登录信息仅出现在MySQL通信协议的认证握手阶段,仅通过抓包容易错过。
watcher通过定时执行show processlist获取当前数据库的所有连接数据,通过对比Host字段与当前包的客户端ip port,补偿错过的用户名信息。
3. worker
不同的worker负责管理不同数据库连接的生命周期,一个worker管理多个连接。通过定期比对worker的当前连接列表与watcher中的连接列表,及时发现过期的连接,关闭并释放相关资源,防止内存泄漏。
4. connStream
整个数据采集端的核心逻辑,负责根据MySQL协议解析TCP数据包并识别出特定SQL,一个连接对应一个connStream Goroutine。因为SQL中可能包含敏感数据,connStream还负责对SQL进行脱敏,具体的特定SQL识别策略,由于安全方面原因,这里不再进行展开。
5. sender
负责数据上报逻辑,通过thrift协议将connStream解析出的审计数据上报给log-agent。
3.2 基础性能测试
抓包库gopacket的性能直接决定了系统性能上限,为了探究问题是否出在gopacket上,我们编写了简易的tcp-client和tcp-server,单独对gopacket在数据流向图中涉及到的前三个步骤(如下图所示)进行了性能测试,从下面的测试结果数据上看,性能瓶颈点不在gopacket。
3.3 CPU画像分析
丢失率与CPU消耗二者密不可分,为了探究如此高CPU消耗的原因,我们用Go自带的pprof工具对进程的CPU消耗进行了画像分析,从下面火焰图的调用函数可以归纳出几个大头:SQL脱敏、解包、GC和Goroutine调度。下面主要介绍一下围绕它们做的优化工作。
3.4 脱敏分析及改进
因为SQL中可能包含敏感信息,出于安全考虑,rds-agent会对每一条SQL进行脱敏处理。
脱敏操作使用了pingcap的SQL解析器对SQL进行模板化:即把SQL中的值全部替换成“?”来达到目的,该操作需要解析出SQL的抽象语法树,代价较高。当前只有采样和抓取特定SQL的需求,没有必要在解析阶段对每条SQL进行脱敏。这里在流程上进行了优化,把脱敏下沉到上报模块,只对最终发送出去的样本脱敏。
这个优化取得的效果如下:
3.5 调度分析及改进
从下面的数据流向图可以看出整个链路比较长,容易出现性能瓶颈点。同时存在众多高频运行的Goroutine(红色部分),由于数量多,Go需要经常在这些Goroutine间进行调度切换,切换对于我们这种CPU密集型的程序来说无疑是一种负担。
针对该问题,我们做了如下优化:
- 缩短链路:分流、worker、解析SQL等模块合并成一个Goroutine解析器。
- 降低切换频率:解析器每5ms从网络协议包的队列中取一次,相当于手动触发切换。(5ms也是一个多次测试后的折中数据,太小会消耗更多的CPU,太大会引起数据丢失)
这个优化取得的效果如下:
3.6 垃圾回收压力分析及改进
下图为rds-agent抓包30秒,已分配指针对象的火焰图。可以看出已经分配了4千多万个对象,GC压力可想而知。关于GC,我们了解到如下两种优化方案:
- 池化:Go的标准库中提供了一个sync.Pool对象池,可通过复用对象来减少对象分配,从而降低GC压力。
- 手动管理内存:通过系统调用mmap直接向OS申请内存,绕过GC,实现内存的手动管理。
但是,方案2容易出现内存泄漏。从稳定性的角度考虑,我们最终选择了方案1来管理高频调用函数里创建的指针对象,这个优化取得的效果如下:
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))
这个优化取得的效果如下:
基于上述经验,我们对数据采集端进行功能代码重构,同时还进行一些其它优化。
4 最终成果
下面是优化前后的数据对比,丢失率从最高60%下降到了0%, CPU消耗从最高占用6个核下降到了1个核。
为了探究抓包功能对MySQL性能损耗,我们用Sysbench做了一个性能对比测试。从下面的结果数据可以看出功能对MySQL的TPS、QPS和响应时间99线指标最高大约有6%的损耗。
5 未来规划
虽然我们对抓包方案进行了各种优化,但对于一些延迟敏感的业务来说性能损耗还是偏大,而且该方案对一些特殊场景支持较差:如TCP协议层发生丢包、重传、乱序时,MySQL协议层使用压缩、传输大SQL时。而业界普遍采用了直接改造MySQL内核的方式来输出全量SQL,同时也支持输出更多的指标数据。
目前,数据库内核团队也完成了该方案开发,正在线上灰度替换抓包方案中。另外,对于线上全量SQL端到端丢失率指标的缺失,我们也将陆续进行补齐。
本文作者
粟含,来自于美团基础研发平台/基础技术部/数据库技术中心。