暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

He3Proxy读一致性方案设计

原创 汤包出ོ来了 2023-11-06
203

He3Proxy最终支持三种一致性级别:最终一致性、会话一致性、强一致性,本文将阐述详细方案设计。

1. 读写分离架构下面临的一致性问题

读写分离架构的收益很明显,但也会带来一些问题,核心的问题就是数据读一致性问题。

造成不一致的原因主要是主从复制时的数据延迟,数据库中设置主从数据为强同步是可以避免一致性问题,但是如果你有5个从、10个从时会怎样?相信性能的下降会使你用到崩溃,因此业务一般不会使用同步复制方式而采用异步复制;异步带来了效率的提升,假设A请求修改了数据t1,紧接着B请求查询t1数据,查询结果会是什么?

返回什么

返回结果处于不确定状态,当B请求负载到主节点或者已经完成数据同步的从节点上时结果为最新数据,当负载到未完成数据同步的从节点时结果为旧数据;这种不确定性造成读数据产生不一致,这种情况也称为数据的最终一致性,即在未来的某个时间点数据能够达到一致性;但这种方式会对一致性敏感的业务造成影响,如何解决这类问题呢?

2.会话一致性

我们先将问题范围缩小到一个会话(session)内即一个连接中处理。核心需要解决的问题是:如何在做负载之前得知哪些节点已经是最新数据了?

数据库中有个名词叫日志序列编号(LSN)是事务日志里面每条记录的编号,我们可以利用它来判断节点是否具有最新数据,过程也容易理解:

(1)更新操作执行后获取该操作对应的LSN;

(2)获取从节点日志回放的最新LSN;

(3)负载时判断哪些节点的LSN >= 当前session的最大LSN,满足条件的节点即可作为请求处理的节点。

session一致性执行逻辑

方案非常简单,核心点在于如何获取每个节点最新的LSN呢?参照 PolarDB[1] 的解法,可以有两种方式:

(1)查询通道返回,即每次查询操作结果中返回LSN信息;

(2)节点定期上报最新的LSN点位;

当然也有其他方式获取比如建立长链接通道、消息中间件、服务发现机制等,考虑架构复杂性、性能损耗、实际收益等因素个人认为 PolarDB 的处理方式比较得当;LSN数据不需要落地,维护在session内存即可,链接断开后数据随之销毁。

看到这里你可能会问 并发大的情况下岂不是主库压力会非常大?

传统数据库中从机回放速度相对较慢,大并发下因从机未完成回放,请求只能落到主节点,确实会造成主节点压力过大,需要结合实际业务去权衡方案。

在云原生数据库架构下,得益于数据的物理复制,速度极快,可能在主节点数据返回时,数据复制已经开始,也就是说下次查询请求到来时,极可能读节点已经可以提供最新数据的查询,所以在这种架构体系下,主节点压力并不会太大,读节点可以分摊查询请求。

进一步细化,我们可以将LSN信息的维护细化到表粒度,这样可以提高负载的精确性,比如更新t2返回的LSN为88,紧接着查询的是t1数据,它的最大LSN才到77,因此可以选择LSN >= 77 的节点进行负载,而不是>=88。

He3Proxy中我们选择复用PG的系统表 pg_stat_replication 的数据维护LSN和节点的关系,连接建立时先查询pg_stat_replication中的数据,维护基础的node与LSN关系,并通过后续连接查询结果中携带的LSN信息更新关系。接着讨论下如何在返回结果中携带LSN信息呢?

LSN维护方式

He3Proxy通过改造PG协议,新增LSN信息返回的新消息,需要计算层He3PG配合同步修改,因计算层消息协议的变动会影响客户端的连接使用,我们通过新增环境变量的方式区分连接He3PG的是He3Proxy还是其他客户端,只有He3Proxy连接时才返回LSN信息,避免用户使用其他客户端连接时引起报错,具体消息格式设计参考PG协议中的一般消息体格式,具体如下:

LSN返回的消息格式

通过以上方式He3Proxy即可在每次查询结果中取到最新LSN信息,并且每次查询结果集中只返回一次LSN,相比通过表扩展字段、隐藏列等方式维护更加简洁,同时通过startUpMessage阶段的变量信息控制是否返回LSN格式的消息,以兼容非He3Proxy的客户端连接。(个人认为因涉及计算层配合改造,这可能也是大多数开源中间件不做读一致性的原因吧)

这种方案强调了session内的一致性,为什么是session内?放到全局行不行?

我认为主要有以下几点原因:

(1)session内对事务的处理相对容易,能很好的处理可见性问题(事务中不同session数据可见性不同);

(2)session内保证请求有序,且LSN信息维护在session内存中;全局情况下,当高并发查询、更新数据时,维护LSN信息时可能需要锁机制,增加复杂性的同时也会影响性能。

3. 全局一致性

某些场景对一致性要求极高,除了会话内部有逻辑上的因果依赖关系,会话之间也存在依赖关系,例如在使用连接池的场景下,同一个线程的请求有可能通过不同连接发送出去。对数据库来说这些请求属于不同会话,但是业务逻辑上这些请求有前后依赖关系,此时会话一致性便无法保证查询结果的一致性。

如何保证全局一致性呢?

1)请求全部由主节点执行

方案简单,逻辑简单,但也把集群能力限制在了单点,方案基本不可取,适用场景局限于写远大于读的场景;这里不做过多的讨论。

2)proxy等备节点完成回放后,再负载请求

全局一致性

会话一致性的基础上,每次读请求都会到主去获取最新的LSN(系统表查询),然后等备节点回放达到主LSN时,进行负载均衡;因为全局一致性保障所有连接具有相同的读数据,因此He3Proxy连接通道内存中无需维护LSN信息,LSN维护粒度也无需到库表粒度,节点维度即可。并由两个参数控制保证执行效率:

  • 等待node节点同步至master节点的LSN的超时时间
  • 超时处理策略:

发往主节点;
报错返回;

方案优点: 减少主节点压力,适合读多写少场景;

方案缺点: 需要等待主备同步,需要容忍一定的延时时间;

为进一步提升执行效率,HeProxy做了以下几点优化

(1)减少从主获取最新LSN的次数,连续多个读请求时只查询一次 pg_stat_replication

(2)得益于He3DB存算分离、缓存分层设计(后续会有文章解析此部分设计,欢迎关注~),日志回放并不是所有节点都需要执行,并且内存buffer热数据节点与需要日志回放节点重合,因此日志回放速度得到较好保障,不会太慢;

(3)He3DB读节点内存分层设计决定了只读节点不对等,查询请求需要负载到对应有数据缓存的节点,因此He3Proxy不需要等待所有节点都完成回放,只要等待查询中涉及关键表的缓存节点完成回放即可;

读全局一致性执行流程

(4)业务系统中往往个别表需要保证强一致性,大多数表session一致性即可满足要求,因此He3Proxy进行了更细粒度的管理设计,支持按照库表粒度按需设置全局一致性,其余继续保持session一致性

<1> He3Proxy解析执行操作涉及的库表;
<2> 元数据表维护哪些库表需要设置强一致性:
不指定表:所有库表都是session一致性;
指定表:指定表为强一致性,其他表维持session;
ALL:所有库表强一致性;
<3> 强一致性实现逻辑维持不变,涉及元数据中维护的库表同样先获取主最新LSN,待缓存节点的日志回放一致后进行负载均衡;

(5)后续He3DB也会考虑结合高性能硬件技术进一步降低日志同步、存储时间,降低主从数据同步时长。

总之性能和一致性是个矛盾,强一致性的保证势必造成性能的下降,我们能做的是为用户提供更多的选择方式,用户需要业务进行取舍。

4. 小结

本文分析了读写分离架构下如何保证读数据的一致性,分别从session一致性到全局一致性进行分析,希望能给大家带来一些思路。

由于作者能力有限,文中错误不当之处望批评指正,感激不尽!

参考文献:

[1] 一致性级别 - 云原生关系型数据库 PolarDB MySQL引擎 - 阿里云


欢迎加入开源社区,与我们一起共建云原生数据库。

项目地址:He3DB/He3Proxy

欢迎通过slack与我们一起交流~

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论