专栏名称: 狗厂
目录
相关文章推荐
OFweek维科网  ·  突发!又一光伏企业实控人被立案调查! ·  4 天前  
OFweek维科网  ·  荣耀暴涨45%!小米再超苹果 ·  4 天前  
半导体行业联盟  ·  上海卓远,12英寸晶圆项目,开工 ·  3 天前  
半导体行业联盟  ·  AI + 智能硬件-深圳大会! ·  3 天前  
半导体行业联盟  ·  中国“半导体封测”大会!聚焦无锡! ·  4 天前  
51好读  ›  专栏  ›  狗厂

科普一种可以将PG变成通用SQL引擎的技术

狗厂  · 掘金  ·  · 2018-06-14 10:28

正文

FDW(Foreign Data Wrapper)是PostgreSQL(下文简称PG)中一项非常有意思的技术,通过它可以将PG变成一个通用的SQL引擎,使得用户可以通过SQL访问存储在PG之外的数据。本文将介绍一下PG的FDW,并探讨一下用GO语言来实现一个第三方数据源的FDW的经验与实践。

一、FDW的前世今生

1、起源——SQL/MED

随着一个企业/组织的IT体系的日益增大,往往会不可避免地在多个应用之间需要进行数据共享与交换。

通常我们希望这些应用的DAC(Data Access Code)能够简单直观一些。然而不幸的是,即使是在同一个企业或组织的内部,不同应用所涉及到的数据往往会存储在不同的数据源中——可能是不同厂商的DBMS产品,也可能是根本不支持SQL的异构数据存储中。因此,往往随着应用规模的增大,多个应用之间数据访问关系就会变得像下图一样杂乱无章:

随着社会的信息化推进,上述问题逐渐成为业界的共性课题。因此数据库业界在2001年对于该课题给出了一个积极的响应,这就是SQL/MED扩展标准(该标准的第一个版本为 ISO/IEC 9075-9:2001,目前最新版本为 ISO/IEC 9075-9:2016)。

SQL/MED标准旨在建立一个解决此类课题的技术规范:即应用程序可以通过统一且标准的方式(SQL)去访问存储在不同数据源中的数据,且数据源本身对应用透明。在理想情况下,SQL/MED标准希望达成的效果如下图所示:

需要注意的是SQL/MED标准对于解决上述问题实际上定义了两套技术规范,一个是Foreign Data Wrapper,另一个则是datalink 类型(它与PG中的dblink以及Oracle中的Database Link不是一回事,尽管其目的有相似之处)。

dblink: https://www.postgresql.org/docs/10/static/dblink.html Database Link: https://docs.oracle.com/cd/B28359_01/server.111/b28310/ds_concepts002.htm

本文的剩余部分将主要围绕FDW来展开说明。

2、PG中FDW的现状

PG对于FDW的支持早在10年前就已经开始了。

社区从2009年推出8.4版本中首先就已经提供了对FDW的创建语句以及Foreign Server的创建语句的语法支持(但未实现实际的功能)。到了2011年社区推出9.1版本时正式对外公开了支持FDW功能的内部接口,从而让扩展的编写者可以利用这些接口为不同的数据源编写FDW的实现。并且在接下来的7年内,社区每一次PG的版本升级都会带来FDW的功能提升——尽管这些功能增强不一定总是体现在语法层面。

比如,在最初的版本中,FDW仅仅只能将远端数据源的数据原封不动地拉至PG中;但到了最近的两三个版本中,借助FDW已经可以实现将更多地运算(如JOIN,聚合等)下推至远端数据源,并能够对远端数据源的数据进行更新。

注: 前提是远端的数据源(特别是异构数据源)本身要能够具备这些被下推的能力(JOIN、 聚合、数据更新等等)

截止到目前为止,全世界已有成百上千种数据源有了相应的FDW实现,从传统的文件系统到各种新型的Nosql数据库,甚至还包括互联网上的Web Service。在PG社区官方的wiki页中罗列了一部分较常见的数据源的FDW。

PG社区官方wiki页: https://wiki.postgresql.org/wiki/Foreign_data_wrappers

另外值得一说的是,尽管基于SQL/MED标准的FDW技术的初衷是为了统一异构数据源的访问方式。但是,随着这些年PG的FDW内置功能(core functionality)支持将越来越多的运算下推到远端执行,同时还有一个获得社区官方支持的用于访问远端PG服务器的FDW扩展postgres_fdw也变得越来越强大。

postgres_fdw: https://www.postgresql.org/docs/10/static/postgres-fdw.html

PG社区核心团队的大佬 Bruce Momjian在2016年给社区写了一封邮件提议了一个基于FDW技术的分布式水平扩展方案(即通称的所谓“Sharding”方案),与此同时,老爷子还撰写了一份PPT专门阐述这个想法。基于“对现有PG代码改动最小”这一原则,目前社区已经基本上认可了基于FDW的Sharding方案作为PG源生的分布式实现方案。

邮件链接: https://www.postgresql.org/message-id/20160223164335.GA11285%40momjian.us PPT链接: http://momjian.us/main/writings/pgsql/sharding.pdf PG源生的分布式实现方案: https://wiki.postgresql.org/wiki/Built-in_Sharding

事实上,基于FDW的Sharding方案在实现和落地方面,日本技术者显然走得很远。比如,实现FDW核心功能的patch最早就是由日本的花田茂提出的。

参考链接: https://www.postgresql.org/message-id/20101125163436.96F6.6989961C%40metrosystems.co.jp

2017年,NTT DATA分享了他们在基于PG 9.6做的Sharding方案——简而言之就是PG的表继承 + postgres_fdw。其概念和架构分别如下所示:

概念图

架构图

FDW技术应用到了Sharding上,这恐怕也是SQL/MED标准制定者最初没有设想到的吧。但既然社区已经选取FDW这条技术路线作为PG的内置Sharding方案,那么在可预见的未来,社区必然会继续完善FDW的核心功能并积极地增强postgres_fdw扩展的功能。

二、揭秘FDW

1、FDW的通用用法

使用FDW的核心就在于使用外部表(FOREIGN TABLE)。尽管面向不同数据源的FDW实现各有不同,但是受益于SQL/MED定义的标准,创建不同数据源的外部表的方法都是一样的,分别需要在PG端依次创建以下几个数据库对象:

  • 向PG安装某个数据源的FDW扩展;
  • 使用CREATE FOREIGN DATA WRAPPER语句创建该数据源的FDW对象;
  • 使用CREATE SERVER语句创建该数据源的服务器对象;
  • 使用CREATE USER MAPPING语句创建外部数据源用户与PG用户的映射关系(这一步是可选的。比如外部数据源根本没有权限控制时,也就无需创建USER MAPPING了);
  • 使用CREATE FOREIGN TABLE语句创建外部表。

之后就可以使用 SELECT 语句按照访问普通表的方式访问外部表;如果该数据源支持写操作且它的 FDW 也已实现支持写操作的相关接口,则也可以使用 INSERT,UPDATE 或 DELETE 语句去更新外部表。

上述过程中,运用FDW创建的数据库对象可以简要地用下图来概述:

2、FDW关联的数据库对象

上一小节介绍了FDW的通用用法,这里将简单说明一下该用法中提及的几个数据库对象的作用:

FOREIGN DATA WRAPPER对象 对应的DDL语法: https://www.postgresql.org/docs/10/static/sql-createforeigndatawrapper.html

是一个纯粹的抽象概念,创建该对象的实质是向PG注册了某个数据源的FDW所实现的两个自定义函数——该FDW所实现的所有接口的注册函数(在CREATE FOREIGN DATA WRAPPER语句中称为HANDLER)以及该FDW的所支持的选项验证函数(在CREATE FOREIGN DATA WRAPPER语句中称为VALIDATOR)。

该对象被创建后,语句中制定的HANDLER与VALIDATOR会被添加至系统表pg_proc(保存所有自定义函数的元数据)中,且两者的OID以及该FOREIGN DATA WRAPPER对象的名称与OID一同被保存至系统表pg_foreign_data_wrapper中。

pg_proc: https://www.postgresql.org/docs/10/static/catalog-pg-proc.html pg_foreign_data_wrapper: https://www.postgresql.org/docs/10/static/catalog-pg-foreign-data-wrapper.html

通常FOREIGN DATA WRAPPER对象的创建过程是直接包含在了安装FDW扩展的CREATE EXTENSION语句中,从而在安装时被自动执行,无需数据库用户在使用中单独执行。

需要补充说明的是,HANDLER的作用是将该FDW实现的一系列fdw回调函数的地址打包返回给PG,从而使PG之后访问外部表时可以调用这些访问外部数据的函数。而所谓的fdw回调函数则是指PG手册所提及的下述接口的实现:

  • GetForeignRelSize
  • GetForeignPaths
  • GetForeignPlan
  • BeginForeignScan
  • IterateForeignScan
  • EndForeignScan
  • 等等......

参考链接: https://www.postgresql.org/docs/10/static/fdw-callbacks.html

关于这些回调函数的作用,会在后文介绍,此处暂略。

FOREIGN SERVER对象 对应的DDL语法: https://www.postgresql.org/docs/10/static/sql-createserver.html

表示的是外部数据源的数据库对象,比如可以在CREATE SERVER时通过选项指定数据库所在服务器的IP地址等信息。FOREIGN SERVER对象被创建后,相关的元数据被保存在系统表pg_foreign_server中。

pg_foreign_server: https://www.postgresql.org/docs/10/static/catalog-pg-foreign-server.html

FOREIGN TABLE对象 对应的DDL语法: https://www.postgresql.org/docs/10/static/sql-createforeigntable.html

将外部数据源的数据组织为表的形式,这样的表就被称作为外部表,它可能对应的是外部异构RDBMS的一张表,也有可能是文件系统上的某一个文件、一个建立在企业网中的微服务,或是一个互联网上的Web API。具体如何对应,取决于这个数据源的FDW实现。

当外部表对象被创建后,它与PG中的普通表一样,元数据都会被保存在系统表pg_class中,只是它的relkind字段会以“f”进行标识;同时,该表在也会在系统表pg_foreign_table被保存一条记录,它存储了该表在pg_class的OID与该表所属的FOREIGN SERVER的OID的对应关系。

pg_class: https://www.postgresql.org/docs/10/static/catalog-pg-class.html pg_foreign_table: https://www.postgresql.org/docs/10/static/catalog-pg-foreign-table.html

从9.5开始,PG提供了一个新的语法IMPORT FOREIGN SCHEMA支持用户批量导入外部数据源的外部表,以省却一个一个CREATE FOREIGN TABLE的繁琐。当然,前提是该数据源的FDW实现中需要实现IMPORT FOREIGN SCHEMA所对应的回调函数。

有了上述外部对象相关的元数据支持,当一个查询试图访问外部表以获取外部数据源的数据时,FDW在整个查询的执行过程中就可以发挥下述作用:

而在此图中,FDW得以介入整个执行过程的奥秘就在于回调函数。

3、FDW回调函数与外部表查询

如上文所说,一个FDW实现的核心就是实现一组回调函数。有了这些回调函数的帮助,在查询外部表对象的执行过程中就可以将运行逻辑切换至自定义的扩展代码中,进而遵照PG的内部机制实现对外部数据源的访问。

截止到PG 10.0,PG提供的FDW回调函数接口已有20余个。FDW的实现者需要根据外部数据源自身的能力(比如是否支持写操作,以及是否支持在外部数据源端执行JOIN操作等等)对这些接口有选择性地予以实现。

这些接口中,最核心的接口有7个。无论外部数据源自身能力如何,这7个接口是实现通过外部表对象访问该数据源的必须接口。它们的接口定义如下:

typedef void (*GetForeignRelSize_function) (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);

typedef void (*GetForeignPaths_function) (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);

typedef ForeignScan *(*GetForeignPlan_function) (PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid, ForeignPath *best_path, List *tlist, List *scan_clauses, Plan *outer_plan);

typedef void (*BeginForeignScan_function) (ForeignScanState *node, int eflags);

typedef TupleTableSlot *(*IterateForeignScan_function) (ForeignScanState *node);

typedef void (*ReScanForeignScan_function) (ForeignScanState *node);

typedef void (*EndForeignScan_function) (ForeignScanState *node);

我们知道, 一条查询语句在PG中会经历三个大的阶段:
  • Parser: 包含对SQL的语法解析,语义校验,查询重写;
  • Optimizer: 生成查询计划;
  • Executor: 按照经典的火山模型执行查询计划的算子并向上“吐”数据。

火山模型: http://dbms-arch.wikia.com/wiki/Volcano_Model

上述这七个回调函数主要在Optimizer和Executor阶段进行“介入”。如下所示:

需要注意的是,上图仅仅是显示这些回调函数被调用的时序顺序。图中的箭头并不意味着两个回调之间存在相互调用关系。事实上这些回调函数都是由PG的Optimizer和Executor进行调用。

这七个回调函数详细的调用时机以及作用总结如下:

4、FDW回调函数间的数据传递

如上文所述,回调函数本身是由PG来调用的,各回调函数之间并不会产生彼此的互相调用。因此就产生了一个衍生的问题——如果在FDW的实现想要在这些回调函数之间传递数据怎么办?PG在设计回调函数的接口时也充分考虑到了这一点:

Optimizer阶段

在Optimizer阶段执行的三个回调函数都会传入一个RelOptInfo结构体作为输入参数,该结构体专门有一个字段可供FDW使用:

typedef struct RelOptInfo {     ...(上略)...     void       *fdw_private;     ...(下略)... } RelOptInfo;

因此如果FDW的实现需要在Optimizer阶段的回掉函数间传递数据时,只需要自行申请内存存储临时数据后将指针挂在上述字段即可。

Executor阶段

在Optimizer阶段执行的回调函数都会传入ForeignScanState结构体。在PG中,各种Scan算子都会有一个对应的State结构体,用于存放算子相关的状态数据。考虑到不同数据源的FDW实现可能会需要带一些自定义数据,因此ForeignScanState结构体也专门有一个字段可供FDW使用:

typedef struct ForeignScanState {     ScanState   ss;                 List       *fdw_recheck_quals;      struct FdwRoutine *fdwroutine;     void       *fdw_state;    /* 存放各FDW实现的私有状态数据 */ } ForeignScanState;

与RelOptInfo的fdw_private字段类似,FDW的实现只需申请内存存储临时数据后将指针挂在fdw_state字段即可。

从Optimizer到Executor

如果FDW的实现中想把一些数据从Optimizer阶段带到Executor阶段,会比上面的两个途径略显复杂。







请到「今天看啥」查看全文