Halo Tech

Global Distributed Database中pipeline的应用

    在Global Distributed Database(简称GDD)中的数据同步回放存在多种方式。在上一篇文章中已有简要介绍。简单回顾一下:

GDD设计和实现了三种同步机制:

1. 批量apply模式

    GDD的apply流水线积攒批量的SQL,组成一个较大的SQL组,发给数据库作为一个整体的事务进行apply操作。用户可以自由调整批量的时间属性和批次属性,以测试在用户生产环境的最佳配置。

2.pipeline apply模式

    pipeline apply模式采用libpq的异步执行模式,我们在GDD端维护PLAN列表,对一批需要同步的数据以PBE的格式打包数据包写入libpq, 统一进行返回结果集处理。这种模式下减少网络传输量, 减小网络IO等待,以提升apply的效率。

3.快速同步模式

    如果apply速度依旧无法追赶生产速度导致有严重积压,那么可以开启快速同步模式,这种模式会通过特殊的分发机制,将前后有关联的事务分发到同一个流水线,将无关联的事务尽量均衡的分发到不同的流水线,通过并行处理可以达到倍数级的apply效率提升。

    但是快速同步模式只关心在执行顺序上两个事务是否有关联,不关心在业务逻辑上两个事务是否有关联,所以快速同步模式会打乱事务执行的顺序,可能会有业务视图不一致的现象。可以根据业务数据一致性的要求选择性的开启此模式。

    在上述三种机制中,pipeline apply模式在平衡性能和复杂度方面表现突出,下文将对其进行深入剖析,下面来详细介绍pipeline的相关内容

一、什么是pipeline?

    在haloDB的libpq接口驱动中提供了管道模式,即pipeline。管道模式允许接口函数发送请求而无需读取先前发送请求的结果。 利用管道模式的优点,客户端将减少对服务器等待。Pipeline进行apply使用的是PBE方式,进而减少服务器的执行消耗(因为需缓存计划,也将占用部分内存),进而提升服务器的吞吐效率。

Pipeline的机制简介

(1) 基本概念

    Pipeline 模式的核心思想是命令批处理与异步执行。在传统模式下,客户端必须等待每个命令完成并返回结果后才能发送下一个命令,而在Pipeline模式下,客户端可以连续发送多个命令,然后再逐一处理结果。在连续发送多个命令后需要发送同步点信息(在消息报文中为’s’),这个同步点用作隐式事务的定界符和错误恢复点。

    在pipeline模式中处理查询结果时,应用重复调用PQgetResult并处理每个结果,直到PQgetResult返回空。可以再次使用PQgetResult检索管道中下一个查询的结果,并且循环重复。应用像通常一样处理单个语句结果。 当管道中所有查询的结果都返回时,PQgetResult返回一个结果,其包含状态值PGRES_PIPELINE_SYNC,表示这一批的请求处理完成。

    如果请求中出现了异常 ,在第一个错误和所有后续结果结果中会是PGRES_PIPELINE_ABORTED,直到下一个PGRES_PIPELINE_SYNC到来。

关于事务:

    在不显式指定事务的时候(即隐式事务)时:

- 如果pipeline中某个操作失败,已执行的操作会被回滚

- 排队等待执行的操作会被完全跳过

- 整个pipeline会进入中止状态,每个剩余的操作都会返回 PGRES_PIPELINE_ABORTED 结果

- 客户端需要使用 PQgetResult() 处理所有结果以完成错误恢复

    显示事务会带来业务逻辑的复杂性,如何时执行同步点,区分事务等额外考虑因素,所以在GDD的实践中使用pipeline的隐式事务。

(2) 主要函数

Pipeline模式提供了四个核心函数:

- PQenterPipelineMode:将连接切换到Pipeline模式;

- PQpipelineSync:在 Pipeline中插入同步点分隔命令组;

- PQpipelineStatus:查询当前连接的Pipeline模式状态;

- PQexitPipelineMode:退出Pipeline模式;

(3) 工作流程

在默认情况下的工作流程如下

客户端                          务

   |                                             |

   | ---- 发送查询1 -------------->|

   | <--- 返回查询1的结果 --------|

   | ---- 发送查询2 -------------->|

   | <--- 返回查询2的结果 -------- |

   | ---- 发送查询3 -------------->|

   | <--- 返回查询3的结果 --------|

   |                                             |

在pipeline模式中的工作流程如下

客户端(处于pipeline模式)      

   |                                                 |

   | ---- 发送查询1 -------------->    |

   | ---- 发送查询2 -------------->    |

   | ---- 发送查询3 -------------->    |

   |----- PQpipelineSync  --------->|

   | <--- 返回查询1的结果 --------    |

   | <--- 返回查询2的结果 --------    |

   | <--- 返回查询3的结果 --------    |

   |                                                 |

    在pipeline模式中甚至可以连续提交多同步点而最后在取结果集,当然这种方式会使处理结果集变得复杂,应用可能难以控制其行为(尤其是在判断和处理异常方面);示例图如下

Image

    在pipeline中处理多批次的结果集时如出现异常则需要考虑如下问题:异常结果集所在的批次,如何处理异常,以及需要考虑后续的正确事务与异常事务的关系。基于此GDD在使用pipeline时不使用多批次提交的方式,便于处理异常场景。

在GDD中使用pipeline的优缺点简述如下

优点:

    1.大幅度减少网络开销;

    2.提升apply速度效率

缺点:

    1.编码实现繁琐(如状态机管理、错误处理逻辑复杂等);

    2.导致业务库内存使用开销略有增加

    3.结果集处理复杂化

二、Pipeline的简单使用流程

1.进入pipeline模式

PQenterPipelineMode

2.发送prepare请求

PQprepare

3.循环发送prepared请求

PQsendQueryPrepared

4.发送同步点

PQpipelineSync

5.循环获取结果集

PQgetResult直到PGRES_PIPELINE_SYNC

三、Pipeline的性能验证对比

    通过编写测试函数,验证pipeline与批量apply的性能对比。在插入100W条数据,1000条sql提交一次的场景下,pipeline模式同步批量apply提升巨大。

    在测试环境中,经过验证GDD的pipeline模式的速度同比批量apply可提升80%以上。

Image

四、在GDD中围绕pipeline的其他设计 

1. Raft日志到请求

    由于GDD支持多种Apply方式,其Raft日志被设计为通用格式,无法直接用于pipeline模式。从raft日志到pipeline的应用需要做转换处理,也需要判断执行请求是否缓存过。这里通过hashtable 做检索,检查对象是否做过缓存;也做了数据参数化的转换处理等操作。

2.资源的控制

    因为pipeline模式使用PBE方式,服务端需通过缓存语句的计划才能带来性能提升(占用了部分内存),那么GDD对prepare的请求数量也做了限制,避免服务器占用大量的内存资源。这里GDD使用了LRU算法,当缓存的请求数量达到配置阈值时,GDD会通过LRU淘汰相应数量的缓存请求。

3.异步的可靠性

    在pipeline的使用中,大量的函数请求都是异步的,可能因为网络问题或其他问题,导致函数请求失败。这里如果出现异常则根据原因可能执行连接的重建,GDD中的缓存清理等资源管理操作;

4.结果集的处理

    在处理获取结果集的时候,如出现错误,则根据缓存的提交内容做反序列化操作,可得知异常事务的具体请求是什么,进而可根据情况做相应的处理。