全量SQL(所有访问数据库的SQL)可以有效地帮助安全进行数据库审计,帮助业务快速排查性能问题。一般可通过开启genlog日志或者启动MySQL审计插件方式来进行获取,而美团选用了一种非侵入式的旁路抓包方案,使用Go语言实现。无论采用哪种方案,都需要重点关注它对数据库的性能损耗。
1 背景
2 现状及挑战
3 分析及优化
3.1 数据采集端介绍
3.2 基础性能测试
3.3 CPU画像分析
3.4 脱敏分析及改进
3.5 调度分析及改进
3.6 垃圾回收压力分析及改进
3.7 解包分析及改进
4 终成果
5 未来规划
本文作者
招聘信息
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端到端丢失率指标的缺失,我们也将陆续进行补齐。
本文作者
粟含,来自于美团基础研发平台/基础技术部/数据库技术中心。