前言
在 2019年第五届 Gopher China 大会上,小米科技基础服务高级研发工程师徐成选做了题为《用 Go 构建高性能数据库中间件》的技术演讲,详细介绍了小米开源的数据库中间件 Gaea 的整体架构、内部模块和一些具体实践。以下为演讲实录。
No.0
自我介绍
大家下午好,很荣幸能有这个机会跟大家分享一个用 Go 开发的数据库中间件项目。在之前先做一下自我介绍,我是2015 年年初开始用 Go,最早是用 Python 和 C 比较多,接触 Go 后就立刻喜欢上了这门语言。后来一直用 Go 做了一些项目,包括微服务、数据库和缓存中间件等,还有一些偏业务的基础服务,比如说库存中间层、ID生成器之类的。
No.1
Go in XiaoMi
首先简单介绍下 Go 在小米的一个使用情况,虽然部门分散,缺乏具体的数字,但是可以看到覆盖面还是非常广的。
小米 2014 年引入 Go,最早解决商城日志收集问题。后面感觉表现挺不错,开始大力推广,像现在商城、云平台、金融、IoT 都是用 Go 都是比较多的。用 Go 做的项目有中间件、微服务(比如说我们商城有微服务框架是 koala,这个2016年投产了,目前有几百个微服务),还有更多的业务系统包括订单、活动、运营、API 接入层等都是用 Go 开发的。
中间件很多,NewSQL 发展迅速,为什么还需要 Gaea 这样一款中间件?个人感觉,作为一个单机时代的解决方案,DB proxy 还是有一定的存在价值,再就是也跟我们内部一个现状有关系。
第一,我们内部其实是在用 MyCAT,但是了解很少,使用过程中出问题得不到及时解决,比如说连接过多、连接超时、load 过高和内存溢出等。
第二,这个配置比较麻烦,我们维护了 MyCAT 的一个内部版本,配置有一些写在表里,还有一些写在其他地方,这个很痛苦,易出错、难管理。
第三,还有一些 haproxy、kingshard 等充当了代理角色,不够统一。
基于以上三个原因,并且我们还要做 DB 服务化,所以诞生了 Gaea 这一整套系统。
- Gaea 特性
Gaea 现在已经上线,已经有的特性也跟大家简单介绍一下。首先分布发表,兼容了 MyCAT、kingshard 两种方案。Prepared statements支持分库分表。再就是读写分离,多个从实例负载均衡。还有多租户,针对某一个业务系统特别重要可以单独部署机群,整个运维比较灵活。再就是像 SQL 追踪,慢 SQL 指纹,使用方可以通过 Gaea 平台查找自己的 SQL 信息。还有配置热加载、连接池。我们使用 TiDB 的 Parser 作为 SQL 解析器这也是目前 Go 中最完备、优秀的 SQL 解析器。
- Gaea 架构
这里是Gaea一个大致架构情况。分四个模块:
第一是应用层,各种MySQL Client。
第二是代理层,主要是在线服务处理。
第三是存储层,这一层主要是部署、启停、还有插件系统,DBA的很多插件通过agent来执行。
侧边是管理模块,这个地方多出一个中控,这个中控是Web和Proxy之间进行交互的管控层。我们还打通了微服务koala和gaea,应用方可以通过koala直接连接gaea proxy,无需lvs进行转发。代理层是我本次分享的一个重点,上面是会话管理,下面是连接池,中间是解析、计算、路由、聚合等。整个架构大概如下:
这个项目去年11月份上线,两套机群,16个业务,有两个是分库分表业务。收益上: 综合性能比之前提升了25%,其实好多场景还要高一些。并发优秀,之前被连接打爆的情况没有再次发生。平台化方案,方便DBA分配、配置业务租户。最后打点数据统一汇总到Prometheus,方便在线监控。以上是Gaea目前线上的的情况。
- Why Go?
为什么选择Go?
第一,并发友好。one connection per goroutine,区别多线程像java、C++还要自己处理callback。
第二,开发效率非常高。
第三,工具比较丰富,Go内建工具以及第三方工具。
最后是 Go 目前在db这块的优秀项目非常多,团队本身经验也非常丰富。
No.2
实现Gaea过程中有关的几个技术点
1. 配置热加载
第一件事情就是动静分离,把动态配置和静态配置区别对待。静态配置比如端口、etcd的配置信息、Log位置信息等。动态配置是我们关注的重点,比如说各个分库分表的规则,后端实例信息、读写分离开关等。
方案一,这也是我们在微服务框架里的使用方式。首先构建一个atomic.value,配置加载和构建可以根据框架相关的逻辑提前准备好,最后做一个Store,存到配置面,这是reload阶段。这个配置怎么使用呢?通过atomic.load获取后进行断言,也就是自定义的config,这种方式也可以根据需要包装成多实例的两阶段提交方案。
方案二,是Gaea应用到的,滚动数组方案。首先定义切换标识和两份数据;然后,prepare阶段通过深拷贝当前配置并将要变更的配置准备好;最后,在commit阶段,把上一个准备好的一份配置切换过来,通过cow实现无锁配置热加载。
- 资源抽象
大家可以看到,每个namespace的配置有几十项,几十项的配置都需要有对应的管理接口,很难去编排接口顺序,具体接口开发上也很复杂、繁琐、易出错。我们的方案是把资源直接抽象为新建和关闭,对于关闭,可以做到立刻关闭和延迟关闭。而延迟关闭就是配置热加载阶段需要用到,目的是让既有消息得到安全的处理。这种方案其实是比较省接口,你也不需要在某一个地方考虑接口的顺序之类,简单可依赖。
- 连接池设计
接下来是跟大家分享连接池的设计实现。我理想中的连接池是这样。第一能够在一定范围内做到自动调整容量;第二获取的连接是活的;第三具备一定的超时获取机制,连接池连接不够用,不能一直等着,这样会把代理层打垮,需要fail fast的机制。
自动调整,自动调整包含两个方向,增加和减少。这里的具体实现是基于vitess的resource库封装的连接池。当前连接池可以设置的最大值为MAX,初始连接wrapper为cap。新增连接通过获取时延迟新建连接对象,关闭是通过一个定时器,定时去根据连接上的时间戳判断当前连接空闲时间,超过一定的时间就进行回收,理论上连接数可以回收到0。
保活,大家最常见就是Ping方案,针对多租户下每一个分片的每一个主从实例都需要进行ping,资源消耗可能会非常可观,所以我们采用了另一种方案,简述为获取连接-失败-新建连接-复用连接对象的策略。区别于普通连接池获取连接池失败后、报错、然后重新尝试获取、并通过旁路ping剔除的方案。我们的方案的好处就是,你不需要太多的ping,你的连接对象永远有效,同时你很容易验证在什么位置进行连接创建、回收等操作。这个方案的缺点是: 在网络环境比较差或者是你的MySQL wait timeout比较多会增加重试次数,所以建议idleTimeout小于MySQL wait_timeout值。
超时获取,通过Context实现。我大致总结了一个Context的使用范式。在最底层的goroutine一定有一个单独发送请求、接受应答并通过Select去判断resp、Context的状态。
Context本质是什么?Context是实现Context接口的非导出struct。每一个Context会存储Parent Context或者Chidren的Context。对于取消的情况也有两种方式,一种通过after function或者通过单独的Goroutine执行。最后是value context只有Parent Context,所以自顶向下传值。
会话管理。一般是用SetReadDeadine、SetWriteDeadling两个函数。但是存在一定的问题,一是分散设置,二是每一次读写都需要设置是一种高频操作,会带来一定的性能问题,所以考虑优化。
方案一,在某一个应用里,设置一个SetReadDeadine、SetWriteDeadling做定时设置,不是每次请求都设置,可能每五分钟设置一次,需要定义一些标志位,标志位来的时候设置一次。这个可以缓解性能问题,但也有一些缺点,一是可能有误差,原来状态立刻可以知道,现在需要等几分钟,取决于设置,当然还是有分散管理的问题。
方案二,Gaea使用第二种方案,就是时间片轮转的方式。当我建立一个连接的时候,首先会把会话加入时间轮,给定回调函数,到期执行关闭操作。当客户端主动做Close操作,可以把对应的对象移除。这种管理方案非常清晰,也是一种集中式管理方式,同时 CPU 消耗非常低。
- 内存踩坑优化
分享一个我们之前线上遇到的一个问题。现在go gc已经非常强大,基本不会遇到什么问题。但是我们遇到一个内存一直增长的情况,这个让人很发慌,你会怀疑是不是程序有泄露,我们启动是30兆,跑着一段时间一直是20GB,并且一直会维持在这个内存,我们考虑这是什么问题导致的呢?
我们在梳理我们的请求、处理、应答的过程中发现有很多的append操作。
我们的优化方案是将之前的append操作,包括数据包协议头、返回字段、字段内容等都改为Write操作。
因为上述操作采用了池化技术,并且使用的位置比较多,带来的一个问题就是你需要控制对象生命周期的位置比较多,很容易出现对象未回收到池子内的bug,所以我们将整理流程做了一个改造,将资源回收进行集中,同时,对于能在一个函数完成申请、回收的情况,我们尽量在一个函数内完成。
当然在这个过程中我们也修复掉了几个可能有内存泄露的点,整体改造之后,效果还是比较明显的,内存基本稳定的3GB,未再出现窜高到20GB的情况。
No.4
Impressive runtime
go runtime非常方便,我们的使用方式如下:
第一,将pprof包裹到admin http服务内,并增加鉴权机制,然后中间加一层shellproxy,它可以调用服务里面的一些接口或者指令,把相应数据拿到传给一个WEB;
第二,把一些数据通过打点统计到prometheus,然后通过grafana进行展示;
第三,通过go-torch进行火焰图绘制。
尤其第一种方案,给我们在线排查问题带来了非常大的助力,比如进行全链路压测或者线上有问题时,可以立马做一次pprof,保留现场,观察现场。
No.5
Go toolchains
这里其实是一个流水的页面,介绍一下我们用到了哪一些 Go 工具。版本控制目前用 glide,感觉非常不错。另外 gofmt、golint 还有 goimports 等,我们把一些工具集成到 gitlab ci 和 git hooks 对于提高代码和工程质量都非常有帮助。
No.5
Tests
对于一个基础服务项目,监控先行、注重单测。我们通过不断拆分有状态和无状态模块,在不影响系统模块划分情况下,努力提升单测覆盖面。我们也通过 gitlab ci 进行单测覆盖统计,并针对每一次 commit,都会进行一次 unit tests 验证,保证功能符合预期。
基础服务很少有 QA 进行直接的跟踪测试,所以我们通过 docker,构建了一个简版的集成测试套件,其中包含了 MySQL 官方测试语句、分库分表 python 脚本等。每当发一个新的版本的时候,我们就通过这个工程进行集成测试,投入产出比非常理想。
Q&A
提问:我想请问一下 3GB 的内存大概什么情况下产生的?这个量级是不是有优化的空间?
徐成选:3GB 是目前线上跑到的一个数字,并没有其他理论计算。优化空间还是有的,比如减少中间 ResultSet 结果的构建,做到 session、backend 复用统一 ResultSet,再就是根据是否需要做聚合,减少一部分冗余数据的存储等。
提问:具体的情况是在多少个连接情况下,mysql qps是多少?
徐成选:qps几千,连接几百个左右。因为现在接入的量确实不多。但是这个随着量的增长,这块内存不会增长。这里我们一是通过优化协议层减少一部分内存,二是修复掉了几个可疑的泄漏点,在上述ppt中也有讲到。
有疑问加站长微信联系(非本文作者)