分享

小米 Go 开发实践——用 Go 构建高性能数据库中间件

 侯培彬 2019-10-16

前言

在 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 这一整套系统。

1. Gaea 特性

Gaea 现在已经上线,已经有的特性也跟大家简单介绍一下。首先分布发表,兼容了 MyCAT、kingshard 两种方案。Prepared statements支持分库分表。再就是读写分离,多个从实例负载均衡。还有多租户,针对某一个业务系统特别重要可以单独部署机群,整个运维比较灵活。再就是像 SQL 追踪,慢 SQL 指纹,使用方可以通过 Gaea 平台查找自己的 SQL 信息。还有配置热加载、连接池。我们使用 TiDB 的 Parser 作为 SQL 解析器这也是目前 Go 中最完备、优秀的 SQL 解析器。

2. Gaea 架构

这里是Gaea一个大致架构情况。分四个模块:

第一是应用层,各种MySQL Client。

第二是代理层,主要是在线服务处理。

第三是存储层,这一层主要是部署、启停、还有插件系统,DBA的很多插件通过agent来执行。

侧边是管理模块,这个地方多出一个中控,这个中控是Web和Proxy之间进行交互的管控层。我们还打通了微服务koala和gaea,应用方可以通过koala直接连接gaea proxy,无需lvs进行转发。代理层是我本次分享的一个重点,上面是会话管理,下面是连接池,中间是解析、计算、路由、聚合等。整个架构大概如下:

这个项目去年11月份上线,两套机群,16个业务,有两个是分库分表业务。收益上: 综合性能比之前提升了25%,其实好多场景还要高一些。并发优秀,之前被连接打爆的情况没有再次发生。平台化方案,方便DBA分配、配置业务租户。最后打点数据统一汇总到Prometheus,方便在线监控。以上是Gaea目前线上的的情况。

3. 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实现无锁配置热加载。

2. 资源抽象

大家可以看到,每个namespace的配置有几十项,几十项的配置都需要有对应的管理接口,很难去编排接口顺序,具体接口开发上也很复杂、繁琐、易出错。我们的方案是把资源直接抽象为新建和关闭,对于关闭,可以做到立刻关闭和延迟关闭。而延迟关闭就是配置热加载阶段需要用到,目的是让既有消息得到安全的处理。这种方案其实是比较省接口,你也不需要在某一个地方考虑接口的顺序之类,简单可依赖。

3. 连接池设计

接下来是跟大家分享连接池的设计实现。我理想中的连接池是这样。第一能够在一定范围内做到自动调整容量;第二获取的连接是活的;第三具备一定的超时获取机制,连接池连接不够用,不能一直等着,这样会把代理层打垮,需要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 消耗非常低。

4. 内存踩坑优化

分享一个我们之前线上遇到的一个问题。现在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中也有讲到。

重磅活动预告

Gopher Meetup 广州站即将开启。来自小鹏汽车、腾讯、早安科技、PingCAP的大咖讲师带来 Go 开发领域的一线实践经验分享,尽在10月26日,小鹏汽车总部销售展厅!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多