MGR是有别于传统Master-Slave主从复制模式的一种新型的复制方式,其通过分布式一致性协议Paxos在Group中原子广播事务产生的Binlog并在远端节点进行回放的方式来达到数据复制的目的。但MGR的复制模式还是异步的,一个节点产生的事务数据,被Group中的大多数(majority)节点所接受(accept)即能在本地节点顺利提交并返回给客户端。此时,在远程(异地)节点,这个事务可能还在该节点的事务认证队列里面等待进行冲突检测,事务是按照通过Paxos的先后顺序进行认证的,通过认证后再写入Relaylog,最后按顺序进行并行回放,因此,一个事务在MGR实例的不同节点执行时间没有必然的关联性。
基于这样的事实,我们假设3节点的Group中各个节点的性能是不一样的,A节点性能最好,每秒能处理100个事务,B性能次之,能处理80个事务,C性能最次,仅能处理50个事务。如果A节点作为Primary节点,那么每秒就可能有100个事务通过Paxos发送到B和C,由于B、C没有足够的能力处理这些事务,就会导致未处理的事务数据在B、C的认证队列(内存中的队列)和回放队列(Relaylog文件)中堆积,这就出现了数据延迟。
数据延迟有多种危害,比如在正常情况下,用户从B、C节点无法读到较新(uptodate)的数据,在故障场景下,如A节点crash后,B节点成为新的Primary节点,那么其无法马上提供(正常)的读写服务。所以,一个可用的MGR实例,应该是数据延迟可控的集群,MGR的flow-control(流控)机制就是发挥这样的作用,他确保MGR各个节点的数据延迟尽可能小。
MGR基于配额(Quota)进行流控,也就是说,一个Group在一个周期内,只能执行固定数量的事务,这个固定数量就是配额。下面我们通过几个问题来说明流控原理。
流控的配额怎么确定呢?通过上面的分析,我们应该采用C节点的事务处理能力作为MGR的流控总配额,再根据该Group中有写操作的节点个数来均分这个配额。比如在单主模式下,Primary节点可分配到50的配额。在多主模式下,如果B和C存在写操作,则他们俩分别可以分配到25的配额。这就是MGR流控的基本原理。
流控的对象是谁?MGR的流控机制是控制本节点的负载,而不是其他节点的负载,也就是作用对象是自己当前的节点,这是跟Galera的流控不一样的地方。具体的对象是在本节点上执行的写事务(write transactions),只读事务(readonly transaction)并不在流控的范围内。但由于系统的资源是有限的,一个节点上只读事务多了,势必也会影响本节点的写事务处理能力,进而也就会降低流控的总配额,因此,只读事务能够间接影响流控。
流控的数据来源及更新?MGR作为一个整体,各个节点会每秒发送自己的状态信息给其他节点,这些信息中就包含了流控所需的信息。节点会缓存接收到的各节点状态信息,并使用最新的状态信息覆盖历史状态信息。通过计算某个节点前后2次状态信息的差值就可以知道节点的事务执行能力。
如何让流控起作用?我们知道MGR是以plugin的方式动态加载到mysql server上的,所以,流控起作用的地方不可能在mysql server,只能可能在plugin内部,但应该尽可能靠近mysql server。现实中,MGR在一个事务进入plugin但还未进行全局原子广播时判断是否需要对该事务采取进行流控。
在MGR流控机制上,MySQL官方对5.7和8.0版本采取了不同的处理方式。相比5.7,8.0版本的流控机制粒度更细,可调性更好,能够适配更多的使用场景,但8.0版本还未GA,稳定性存疑。InnoSQL 5.7.20-v2版本backport了8.0版本的流控机制,可以为用户提供更好的MGR流控体验。InnoSQL 5.7.20版本提供的流控参数如下:

其中红框内为原生5.7版本支持的3个流控参数,其他7个参数为InnoSQL版本增强的参数,以上这些流控参数都是可以在线调整的。各个参数的官方说明可参考下面的文档:https://dev.mysql.com/doc/refman/8.0/en/group-replication-options.html
但文档中对新增的各参数的说明似乎有些语焉不详,光凭这个很难把握这个参数的具体含义,进而指导进行MGR流控调节。这也是要写这篇文章的目的之一,在此,先对各个参数做下简要说明,到代码实现小节再介绍其实现。

该参数用于在线开启和关闭流控,默认为QUOTA,表示开启流控,设置为DISABLED表示关闭,从该参数也可以看出MGR目前支持基于QUOTA的流控,但也不排除后续增加新的流控机制。只有该参数开启了,剩余9个参数才有可能发挥作用。

该参数表示多久时间进行一次流控统计,单位为秒,也就是说每隔多少秒节点发送自己的状态信息给其他节点,每个节点收集各节点发送的流控信息,进行统计并计算新一个流控周期的各节点配额。默认和最小的流控周期均为一秒,最小流控周期可设置为60秒,即一分钟。显然,流控的周期越短,流控的精确度越高,性能也更加平滑,虽会增加节点间的网络流量,就目前来看,影响很小。还有一个因素需要考虑的是,在某些业务场景下如果事务提交速度很慢,超过了流控统计周期,则流控可能无法正常发挥作用。此时应该把流控周期调节为大于事务提交速度。


上面2个参数分别从不同维度来设置触发流控的阈值。前者表示本节点的事务认证队列中积累了多少个待认证的事务才触发节点流控,后者表示事务回放队列积累了多少待认证的事务才触发流控。当其中一个队列的事务数超过了所设的阈值,即触发流控。
需要注意的是,该阈值跟节点的配额并不相同。我们举水库蓄水为例,所设置的阈值就相当于水库进行泄洪时的水位,超过了该水位就开始泄洪,对应我们这里所说的流控,只不过水库是通过增加流出的水立方数,而MGR是通过限制进入plugin的事务数的方式进行。而配额则是单位周期内能够处理的事务数,对应到水库上就是单位周期内从水库流出的水体积。如果单位周期内增加的等待认证事务数超过了节点的事务认证处理能力,则必然导致队列中等待认证的事务数增加,久而久之就会超过所设置的流控阈值。同理,单位周期内增加的等待回放事务数超过了节点的事务回放能力,也会导致等待回放的事务数超过所设置的阈值。
该值大小的影响需要分场景考虑:
如果MGR实例所承受的负载比较稳定,且超过MGR实例的处理能力,则流控机制一直起作用,此时等待认证或等待回放的队列事务个数在所设置的阈值上下小幅波动,波动的幅度跟流控的作用周期相关。这是流控机制的一种使用场景;
如果实例所承受的负载变动很大,低峰时远低于实例处理能力,高峰期时超出实例处理能力。这种情况下,等待认证或回放的事务数也会周期性得剧烈波动,低峰时降为0,高峰时超过所设置的阈值。那么此时阈值越大,表示流控机制开始起作用的时机越延后。如果各个节点的事务处理能力相同,则大家都积累了足够多的等待处理的事务后触发流控。如果处理能力不同,则仅处理能力最差的节点积累最多的事务。反映到用户层面就是该节点相比其他节点有最大的数据延迟。阈值越大,则延迟也越大。

该参数表示为节点分配配额百分比。可设置范围为0~100,如果设置为0,表示采用默认的方式,总配额由所有的写操作节点进行均分。如果大于0,则本节点分配到的配额为总配额的百分比就通过该参数进行设置。所以,该参数可用于为多主模式的MGR实例的各有写操作的节点分配配额。如果单主模式下设置该参数,可能会引起性能损失。若要在MGR实例中配置该参数,一般而言,各个节点都需要配置一个百分比,所有节点的百分比总和为100%,但MGR代码层面并没有做对应的限制,具体的百分比设置由用户自己控制,不受总和100%所约束。需要注意的是,该节点仅在实例中存在多个写操作节点时才会发挥作用,单主模式下,该参数无效。

按照前述的方法计算出的MGR实例配额,能够确保实例以最大的能力提供对外服务。在正常运行时,这个并没有什么问题,只是会因为3和4两个参数而在认证和回放队列出现数量可控的事务堆积。但若出现Group中某个节点crash重新加入,其加入后必然会跟存活的节点产生较大的数据延迟,若此时外部负载仍然是实例所能承受的最大值,则新加入的节点就无法通过Catch Up追上其他节点。基于这些的考虑,实际对外提供的配额并不是Group最大能力,而是做了部分保留,具体的保留百分比就是通过该参数确定的。默认预留了10%的处理能力,用于有数据延迟的节点追上其他节点。
下面介绍3个对基于配额的流控机制进行优化的参数,可以让节点不再完全限制于计算所得的配额,而是由用户自己来指定配额是多少。首先是用于全局控制的参数,如下:

先指出官方文档的一处错误,该参数在代码中是group_replication_flow_control_max_quota,没有commit。该参数以另一种方式设置实例的总配额。如果所设置的参数非零且小于流控机制计算所得的总配额,则该参数作为调整后的总配额起作用,不受group_replication_flow_control_hold_percent参数约束。否则,该参数失效。


这两个参数均用于设置一个节点最小能够拥有的配额。但使用场景不同,优先级也不同。若前者非零,则后者无效。前者定义了在任何情况下,该节点所拥有的最小配额。而后者仅定义在Group中有其他节点处于故障恢复中时,本节点最小拥有的配额,用于避免新节点加入Group后引起已有节点的性能出现太大的下降。
最后介绍的这个参数作在流控机制退出时起作用,用于确保流控退出后,MGR实例的性能得到稳步的提升而不是突然变大进而又触发新一轮的流控。

流控退出时,也会计算下个流控周期的配额,默认的策略是每个周期增加50%的配额,一直增加到外部业务的实际最大压力,或者到触发新一轮流控的阈值。如果该参数未设置(为零),则在流控退出时,MGR会允许实例上的实际负载全都进入plugin,极有可能又触发流控导致性能急剧下降。想必过山车似的性能表现不是业务想要看到的。
上一小节我们简要介绍了用于设置和调节流控的10个参数,本节重点介绍流控在MGR代码中是如何实现的。MGR流控在代码中是较为独立的一部分,绝大部分代码都在http://pipeline_stats.cc和http://certifier.cc中。
各节点的状态消息(包含了流控相关信息)通过http://certifier.cc中的Certifier_broadcast_thread线程发布,该线程的功能包括发送节点的状态信息(用于进行流控计算),节点的gtid_executed信息等,如下图所示:

在此我们仅关注流控相关的部分。Certifier_broadcast_thread的处理函数dispatcher每秒调用一次Flow_control_module::flow_control_step(Pipeline_stats_member_collector* member),该函数判断距上次发送状态消息是否已经group_replication_flow_control_period秒,若满足,则调用Pipeline_stats_member_collector::send_stats_member_message(Flow_control_mode mode)发送本节点新的状态信息,send_stats_member_message仅在节点状态为ONLINE或RECOVERING时才会发送。该消息类型为Plugin_gcs_message::CT_PIPELINE_STATS_MEMBER_MESSAGE,消息内容如下所示:

该消息即为系统表performance_schema.replication_group_member_stats的数据来源。
状态消息的处理函数为Plugin_gcs_events_handler::handle_stats_message(const Gcs_message& message),调用逻辑如下:

Flow_control_module模块的函数handle_stats_data(const uchar *data,uint64 len,const std::string& member_id)用于处理每个节点发送的状态消息,该函数将消息缓存到本节点的Flow_control_module_info对象中,进一步调用Pipeline_member_stats::update_member_stats()更新本节点保存的各节点流控状态,包括上个流控周期认证的事务数m_delta_transactions_certified,认证的本地事务数m_delta_transactions_local和回放的事务数m_delta_transactions_applied,调用Pipeline_member_stats::is_flow_control_needed()根据m_transactions_waiting_certification和m_transactions_waiting_apply判断某个节点是否触发了流控,若是则递增m_holds_in_period。
前面所述的6个参数会进一步用于在flow_control_step函数中进行流控配额计算,我们看看该函数的具体处理逻辑。首先本节点流控已启用(flow_control_mode_var为QUOTA),且有节点已达到了触发流控的阈值(m_holds_in_period>0)。只有满足这两个条件才会为该节点计算流控配额,否则流控配额为零或按照所设置的group_replication_flow_control_release_percent增加配额,即表示不受流控限制。若两个条件都满足,则根据本节点记录的各个节点与流控相关的信息来计算性能最差的节点的处理能力,设为quota_lowest_member,扣除group_replication_flow_control_hold_percent,若写操作节点数大于1需再除于有写的节点数(m_delta_transactions_local>0的节点个数),进一步扣除上个流控周期超额使用的配额量(详见下文do_wait函数逻辑),结果即为本流控周期的本节点配额quota_final。若用户设置了group_replication_flow_control_member_quota_percent且写操作节点数大于1,则按照百分比计算本节点的配额。按照这样的判断逻辑group_replication_flow_control_member_quota_percent在单主模式下是不起作用的。
MGR在流控机制还加入了最低配额保障的能力,若用户未设置group_replication_flow_control_min_quota和group_replication_flow_control_min_recovery_quota,则默认会以group_replication_flow_control_applier_threshold和group_replication_flow_control_certifier_threshold参数较小值的5%作为最小配额quota_least,也就是如果计算而得的quota_lowest_member小于quota_least,则以quota_least取代quota_lowest_member。若设置了group_replication_flow_control_min_quota,无论是否设置group_replication_flow_control_min_recovery_quota,group_replication_flow_control_min_quota取代quota_least角色。若设置了group_replication_flow_control_min_recovery_quota且未设置group_replication_flow_control_min_quota,只有当Group中有节点处于故障恢复(num_non_recovering_members=0)时group_replication_flow_control_min_recovery_quota才会取代quota_least的角色。
除了最低保障,还有最高约束,通过group_replication_flow_control_max_quota参数来调节。如果quota_lowest_member大于该参数确定的值,则该参数值覆盖quota_lowest_member。
通过flow_control_step()函数的计算,得到本流控周期的两个值,分别是m_quota_size和m_quota_used,m_quota_used表示已使用的配额,初始值为零。在mysql server执行完成准备提交的事务调用group_replication_trans_before_commit(Trans_param *param)进入MGR plugin处理时,在将该事务信息通过paxos协议进行原子广播前,plugin先调用applier_module->get_flow_control_module()->do_wait()进行流控相关处理,包括增加配额使用量m_quota_used,判断m_quota_used是否超过了m_quota_size,若超出,则do_wait()会先等待1秒再广播事务信息。这就是MGR流控的作用原理。
个人认为,目前MGR流控机制中group_replication_flow_control_min_recovery_quota参数设置和作用方式不怎么合理。
首先,若用户未看过代码,很容易忽略其会被group_replication_flow_control_min_quota覆盖这样的前提。

根据MGR fine flow-control worklog中的描述,group_replication_flow_control_min_recovery_quota应该具备更高的优先级。是否是个实现上的bug需进一步确认。

其次,该参数的引入是为了解决Group中存在故障恢复的节点导致流控,引起正常的在线节点性能下降,那么为什么故障恢复节点会引发流控呢,在之前的文章中已提到故障恢复分为多个阶段,其中包括了全局恢复和缓存事务回放,全局恢复时,本视图的事务被缓存在等待认证队列中到缓存事务回放节点才开始认证并回放,所以如果全局恢复时间较长,就会导致堆积的事务超过group_replication_flow_control_certifier_threshold阈值,这是引发Group其他节点进入流控状态的原因。但目前group_replication_flow_control_min_recovery_quota起作用的判断条件比较晦涩难懂。代码如下:

红框中的判断条件应该是重复的。。从第一个判断条件可以看出,如果本节点设置了等待回放队列的阈值group_replication_flow_control_applier_threshold,且消息中携带的等待回放的事务超过了本阈值,上个周期有事务被回放,则表示该节点不是处于故障恢复状态的节点。从代码逻辑上看,凡是因为等待回放队列的事务超阈值导致的流控,都会引起num_non_recovering_members增加,那么仅在因为等待认证的事务超过阈值情况下,group_replication_flow_control_min_recovery_quota才能发挥作用,即可认为仅在节点进行全局恢复时起作用,在本地缓存事务回放时,由于积累的事务被快速地经过认证,应该会导致等待回放的事务数超过阈值,此时就触发了num_non_recovering_members增加的条件,从而引起group_replication_flow_control_min_recovery_quota失效。
当然,上述疑问只是个人看代码的一点想法,或许是错误的。相信随着了解的不断深入可能会有新的理解。
group_replication_flow_control_mode用于开启和关闭流控,是总开关。group_replication_flow_control_period确定了流控的监控周期。group_replication_flow_control_applier_threshold和group_replication_flow_control_certifier_threshold用于设置流控起作用的阈值。group_replication_flow_control_hold_percent和group_replication_flow_control_release_percent分别用于在流控作用时预留部分配额给延迟节点进行Catch Up,在流控退出时让性能平滑得增加。对于一般场景,仅需调节group_replication_flow_control_applier_threshold和group_replication_flow_control_certifier_threshold这2个参数。除上面所述的之外的参数,主要用于定制化场景,包括自定义最大group_replication_flow_control_max_quota和最小group_replication_flow_control_min_quota的流控配额。各节点自助分配流控配额group_replication_flow_control_member_quota_percent。定义故障恢复时的最小流控配额group_replication_flow_control_min_recovery_quota等。
在group_replication_trans_before_commit()中更新配额使用量并根据需要进行流控(推迟事务信息的原子广播时间);在Flow_control_module::flow_control_step()中发送最新的节点状态信息(用于下一周期的配额更新),并根据是否开启了流控和设置了流控标志位来确定本周期的流控配额;在Flow_control_module::handle_stats_data()中处理接收到的节点状态信息,缓存在内存中并计算两次状态信息的差值(节点的事务处理能力)并根据状态设置触发流控的标志位。
MGR提供了强大的流控能力,用户可以结合业务需求配置不同的流控参数达成想要的目标。但流控参数设置是复杂的,无法凭空确定某个参数值,还需要结合业务和MGR实例的具体情况来进行调整和适配。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69923634/viewspace-3145638/ ,如需转载,请注明出处,否则将追究法律责任。