开发者学堂课程【PostgreSQL快速入门:17PostgreSQL shared nothing分布式用法讲解】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/16/detail/76
17PostgreSQL shared nothing分布式用法讲解
内容介绍:
一、数据库的异地分区
二、PL/Proxy 应用场景举例
三、PL/Proxy 的使用背景
四、PL/Proxy 的原理
五、PL/Proxy 的安装使用
六、PL/Proxy 的使用
七、小测试
八、PL/Proxy 使用注意事项
上节课学习了 PostgreSQL 的本地分区,本节课学习 PostgreSQL 的异地分区,并以 PL/Proxy 为例学习shared nothing 数据分区的部署,了解其应用场景设计,学习其原理、安装方式,并以实例的方式学习如何配置代理库、编写代理函数以及实体函数。
一、数据库的异地分区
异地分区与分布式数据库在概念上十分相近,是 shared nothing 的数据分区,即在一个节点中存储的数据只是整个数据的一部分,所有的节点存储的数据之和才是一份完整的数据。异地分区的方法很多,如通过中间件来实现,像PL/Proxy、PgBouncer、Pgpool 等支持 shared nothing 数据分区的。
本节课以 PL/Proxy 为例来学习数据库的异地分区,PL/Proxy 是一个比较小的软件,其代理节点本身支持无缝扩展,节点也是支持无缝扩展的插件。
二、PL/Proxy 应用场景举例
1、Shared nothing(run on any|nr|hash)+Replication (语句级复制,run on all)+Load Balance(run on ANY|NR\HASH)
(1)Shared nothing 数据分区
若要使用PL/Proxy的方式来实现 Shared nothing 数据分区,则要依靠 PL/Proxy 中的语法:run on any|nr|hash,其中“nr”是一个数字,若分区数为4,则nr为0、1、2、3,“run on 0”即指定在0号节点运行,“run on 3”即指定在3号节点运行;“hash”是指一个哈希函数,其只要返回一个int类型,如 int2、int4或 int8,若返回一个哈希数字,通过数字再与节点数做比特微运算,若有四个节点,则比特微运算的结果也是0-3之间的数字,运营在指定的节点;run on any 是指在所有的数据节点里随机执行。
(2)replication 模式
若处于 replication 模式,依靠 replication(语句级复制,run on all)。对于一个数据存储量较小的表,要使得所有的节点都有一份相同的拷贝,可以使用 run on all,即在所有的节点都执行,比如可以使 insert 语句或 select 语句在所有的节点都执行。比如,数据已经完成分布,要查询所有的数据,则可以使用 select 语句+run on all,同样对于分节点(在某一个/某几个/任一节点)也适用。
(3)Load Balance
Load Balance 与 Shared nothing 用法相同,依靠 run on ANY|NR|HASH。
如果是某一份数据,如某一个表尤其是数据量较小的表,在所有的节点都相同,则可以使用 run on ANY|NR|HASH 随机或指定选择在节点执行来实现 Load Balance。
以下为其架构示意图:
应用程序连接代理节点,代理节点实际上是数据库代理函数,只是该函数是使用 PL/Proxy 语言编写的,代理节点下方才是真正的数据节点,其中包含实际函数。图上显示分了四个数据节点p0、p1、p2、p3。联系上方应用场景对应的语法,在代理节点中用 PL/Proxy 语言编写了一个函数,函数中包括 run on any,则在调用函数时,就会随机的在四个节点里面选一个节点执行该函数。真正的数据节点中编写的才是实际函数,而实际的函数 plpgsql 可以 plpython或其他语言编写,而代理函数必须使用 plproxy 语言编写,是专门的 plproxy 插件,它支持一种语法。
plproxy 本身可以无限扩展,可以完全独立,比如使用了两个 plproxy,当其中一个 plproxy 不运行时,可以用程序将它连到另外的 plproxy。只要 plproxy 之间他的代理函数定义相同即可,因为代理节点(代理函数)中不存储实际的数据,只存储代理函数。而 plproxy 不承担运算,因此不容易成为瓶颈,当然也不是绝对的,如部分运算需要下面真正的数据汇聚上来之后再来做一些聚合,这种情况下,plproxy 也会承担一定的运算量,如要 count 所有的数据,则需要所有节点的数据 count 之后,再将所有 count 的结果相加。当然即使 plproxy 要承担一定的运算量,这种运算量也很小。
数据层支持2n 个节点,同时也支持无限扩展,如要将4个节点扩展,就只能扩成8个、16个节点等,即该扩展的数值一定是2n。因为若为 run on hash,返回的hash数值要与的节点数作比特微运算,这就限定了扩展节点的数值是种是2n 个。该算法在代码中是确定的,当然也可以修改,如要支持5个节点,而像这种支持的节点数不是2n 的情况,同样可以,只是 run on hash 算法会更复杂,效率更低,而支持节点数为2n 个的情况比特微运算的效率很高,因此,建议使用支持2n个节点的应用场景。
2、Replication(流复制)+Load Balance(读写分离,读 run on any|nr|hash,写 run on 0)
该应用场景同样支持2n 个节点,但其所有的节点的数据是一致的。以下为其架构示意图(展示支持4个节点):
若使用 Replication(流复制),则p0是主节点,s1、s2、s3是该主节点的复制节点。
复制节点可以做读写分离,“读”可请求,可以使用 run on any 或 run on nr 或 run on hash 指定在某个节点执行。对于“写”,可以使用 run on 0写入节点,该节点即 run on nr 中的0号节点,也就是 p0节点。写的数据要写到p0节点,读的数据可以随机指定。这样即可使用 PL/Proxy 实现读写分离的场景。
三、PL/Proxy 的使用背景
1、PL/Proxy 的解决方案是水平扩展,对硬件的要求不高,由于是水平扩展,因此对于单个节点的硬件投入较低,还可以计算其投入产出比。如将4个节点扩展成8个节点,性能可以翻倍,同理,8个节点扩展为16个节点,性能也翻倍。
2、PL/Proxy 可以通过调用 PostgreSQL 函数来支持事务。在 PostgreSQL 中,函数操作具有原子性,因此需要用到事务的操作可以封装到 PostgreSQL 函数当中。PL/Proxy 其实上是通过函数来实现交互的,如使用 plproxy 编写了代理函数,代理函数可以指定运行的节点,而“运行”的过程还是在指定的节点中调用函数。在默认情况下,函数名会与同一 scheme下面同样的函数名称相同。
如有一函数为 function a,那么在调用该函数时,指定 run on 0,实际上会在调用p0节点数据库中 function a(PostgreSQL 语言编写)的同时,将函数的结果返回代理函数,再返回给 app。
总之,所有节点的函数操作都是原子性的,要么将整个函数都提交,要么全部回滚,因此其可以很好地支持节点上面事务操作。
PL/Proxy 路由选择非常灵活,非常容易做到读写的负载均衡。
PL/Proxy 与 PostgreSQL 都是免费的,PL/Proxy 的许可也是弹性的,可以复制使用,可以开源也可以闭源。
PL/Proxy 的耦合度不高。因为P L/Proxy 本身没有处理逻辑,它真正的处理逻辑都写在数据节点里的实际函数中,如果不使用代理节点了,就可以将它破坏直接通过直联数据库调用函数,与使用代理的效果相同。
调用函数的好处:
安全性提高,因为应用连接代理数据库,而非直连数据库,并且 PL/Proxy 语言只有超级用户可以编写,即超级用户写好函数之后,可以将函数的调动权限赋给普通用户,而应用程序通过普通用户再调用函数。
也就是其只有调用函数的权限,而没有编写函数的权限,可以隐藏掉一些实际操作的,如可以隐藏直连数据库的操作,这些具体操作数据库的程序无法获取,即使遭受恶意攻击,攻击者也只能获取已经存在的函数,而无法自行创立函数直连数据库进行操作。
业务逻辑代码放在数据库端,当数据经过处理之后可以直接发布至用户,减少交互,使处理效率更高。但前提是在数据库的处理能力足够的情况下,当数据库的处理能力不足的情况下,可以把数据提取出来在app端进行逻辑处理。
四、PL/Proxy 的原理
PL/Proxy 的原理分三个层次:PL/Proxy 节点(s)、连接池(s)与数据节点(s)。其中连接池(s)可以隐藏。
1、PL/Proxy 节点(s)
实际上就是的 PL/Proxy 层。
负责接收应用程序发取请求,即调用plproxy编写的函数;(2)负责解析提交给数据节点的 SQL。
如使用 PL/Proxy 函数,函数书写方式为:CREATE FUNCTION get_date(IN first_name text, IN last_name text, OUT bdate date, OUT balance numeric(20,10)),其中IN first_name text、 IN last_name text是传入参数, OUT bdate date、OUT balance numeric(20,10)是输出参数。
PL/Proxy 调用函数时,会将其解析成:SELECT bdate::date, balance:: numeric(20,10) FROM public.get_date(S1::text, S2::text)
,即将其转成其中的“OUT”,以上函数中输出值为 date 类型,若该函数写在 public 下面,那么就会在数据节点调用函数,S1与S2指的是两个传入的变量;最终显示 Explicite 的类型转换,指定输出顺序。
即在应用程序调用 PL/Proxy 函数时,PL/Proxy 代理节点会将其解析,在实际的数据节点运行,比如要在 p0节点运行,就将解析结果语句交给 p0节点,p0节点处理数据之后,再返回给 PL/Proxy,进而返回给应用程序。
如果是 run all,则是一个异步的过程,直接把 sql 语句传给所有的数据,而不是在一个节点返回数据之后,再传给另一节点,这是一个并行的过程。当接收到数据节点返回的数据后就直接输出给用户,这也是一种异步的输出。
(3)PL/Proxy 支持旁路模式(CONNECT 模式)或选择数据节点(CLUSTER 模式)。使用旁路模式后,后期不直接交给数据节点函数,而是在 PL/Proxy 函数中写入 SELECT 语句,该 SELECT 语句是真正传递给数据节点的是 SELECT 语句,而不是传入函数的方式,这就是旁路模式。而非航路模式是指在数据节点也必须要有对应的实际操作的函数。
(4)CLUSTER 模式分为两种,一种是它的集群是存储在 SQL/MED 配置的集群信息里,选择数据节点是通过Libpq asyne API 异步 API 发送解析的 SQL 给节点。由于其调用的是异步接口,因此执行run all 是 SQL 语句的发送是多个并行的,待数据节点返回结果后,再发送至应用程序;另一种其配置不在 SQL/MED 中,而是由,而由 PL/Proxy 自行存储配置信息,有其专门的配置表,其中有专门的配置缓存的函数。除了配置信息的不同,其余与第一种模式相同
2、连接池(s)
简单来说,PL/Proxy 层和数据库之间是长连接,两者之间可以加连接池,也可以不加连接池。
在正常情况下,如果应用程序与数据库节点之间是短连接,如应用程序连到 PL/Proxy 调用函数获取函数结果之后就断开了连接,如果下次数据获取再重新连接,反复连接--断开,就会导致 PL/Proxy 与数据库节点之间有断开连接-重新连接的过程。如果应用程序与数据库节点之间是长链接,用户连在连接池上,PL/Proxy 与数据库的连接就不会断开,即只要会话存在,应用程序与数据库连接就不会断掉。且连接时间可以配置,如可以配置最长生存时间是1800秒,那么只要会话存在,这1800秒之内连接都不会断掉,后面如果要进行查询就不需要再重新建立连接。
但是对于短连接,只要断开,PL/Proxy 跟数据库之间的节点也断开。因此如果应用程序与数据库节点之间是短连接,则建议在应用程序和 PL/Proxy 之间加连接池,而不是加在 PL/Proxy层和数据库之间,因为 PL/Proxy 层和数据库之间本身本身就是一种长连接,没必要另加。
PL/Proxy2.x 之后的代理和连接池模块都拆掉了,因此,PL/Proxy 不依赖连接池或连接池的类型。
3、数据节点(s)
数据节点存放实际的数据与实际的函数,实际函数可以使用 plpgsql 语言编写,也可以用其他语言编写,如plpython、pljava 等。数据节点负责接收来自于 plproxy 发起的 SQL 请求,并把执行结果的返回给 plproxy。
各种函数的用法:
plproxy 函数的用法
①旁路模式,即不使用代理函数,直接在代理函数中编写 SQL 语句,并将其直接传递给数据节点,而不经过代理函数翻译。
②集群模式,即使用代理函数,应用程序的传递参数给代理函数,将代理函数解析成 SELECT 的 SQL 语句,再传给数据节点执行。具体的使用方法会在后面的案例中进行实际操作。
plproxy 函数的的语法
目前仅支持以下几种语法:
①CONNECT
CONNECT 'libpq connstr'; | connect_func(...) | argname
不使用种集群配置,直接使用connect连接数据节点,其后使用'libpq connstr'字符串或 connect_func(...)连接函数或argname,如配置 host=...,port=...,use=..., password=...,即指定连接的位置。
②CLUSTER, [RUN ON ALL|ANY|int 2,4,8|NR]
CLUSTER 'cluster name'; | cluster_func(...)
集群模式,指定 cluster 的名字或 cluster 函数(不使用 SQL/MED 存储)。指定完成后,选择 RUN ON,RUN ON NR指运行节点,RUN ON HASH函数返回int类型即可,RUN ON ALL 是在所有节点运行,RUN ON ANY 是在任意节点运行。若该集群种有4个连接,则 RUN ON ALL 会在4个连接里都发起 SQL 请求。
③SELECT(CONNECT+SELECT 旁路模式)
不使用 RUN ON 选择运行的节点,而是直接连接然后进行 SELECT④SPLIT
指当传入的参数是数组时,可以按元素分组拆分,减少 plproxy 和程序端的交互,比如传入的参数是数组[1,2,3,4,5,6],即其对应的是 useID,只要查询数组对应的 useID 传给函数,函数就会进行拆分,即将这6个元素封装成一个数组传给代理函数,代理函数使用 SPLIT 语法将元素分组拆分,分六次传给数据节点,而无需将每个元素对应的useID 参数传到数据节点,每传入一个参数调用一次,即整个过程只需要 plproxy 和程序交互一次。
⑤TARGET
当代理函数和节点函数不同名或 schema 不同时,可以使用 target 来指定。如代理函数是放在 schema A下,但实际的数据数据的函数却存储在 schema B下,在调用函数时,如果不使用 target 语法,则默认调用数据节点 schema A下的函数,直接报错,而使用 target 就可以解决该问题,target 是 a.function_name,在调用时就可指定调用该target 下的 function_name,而非默认的。
⑥run on
tag_run_on_partitions(ProxyFunction *func, FunctionCallInfo fcinfo, int tag, DarunArray **array_parans, int array_row)
(
ProxyCluster *cluster = func->cur_cluster;
int i;
switch (func->run_type)
(
case R_HASH:
tag_hash_partitions(func, fcinfo, tag, array_parans, array_row);
break;
//使用HASH函数得到数字,即运行的节点
case R_ALL:
for (i = 0; i< cluster->part_count; i++)
cluster->part_nap[i]->run_tag =tag;
break;
//其实是for循环,即所有的节点都要运行
case R_EXACT:
i =func->exact_nr;
//指定运行的节点 nr
if (i < 0 ::i >= cluster->part_count)
plproxy_error(func, “part nunber out of range");
cluster->part_map[i]->run_tag=tag;
break;
//此处存在判断语句,当i<0或i ≥集群的节点数时,则系统报错
如果有4个数据节点,输入小于0以及大于或等于4的值,都会
报错,即只有输入0、1、2、3才不会报错。
case R_ANY:
i = randon() & cluster->part_mask;
cluster->part nap[i]->run_tag=tag;
break;
//随机指定运行的节点
default:
plproxy_error(func, "uninitialized run_type");
)
)