在这一章中,将向您介绍一个全新的技术,成为BDR。双向复制(BDR),在PostgreSQL的世界里,它绝对是一颗冉冉升起的新星。在不久的将来,许多新的东西将会被看到,并且人们可以期待一个蓬勃发展的项目。
本章将是关于如下这些主题:
?理解 BDR 复制概念
?安装 BDR
?设置一个简单的集群
?修改集群和故障转移
?了解 BDR 的性能
在挖掘所有的技术细节之前,理解 BDR 方面的基本技术是非常重要的。
理解 BDR 复制概念
过去,在9.0被引进之前,人们不得不使用Slony来复制数据。如Slony这样的解决方案的核心问题是,需要一个更新的触发器,它实际上将数据写入两次。基于触发器的解决方案管理起来很困难,不能处理DDLs,并且一般操作起来有点棘手。
BDR已经被创造来终结基于触发器的解决方案,并把PostgreSQL转变为一个更强壮,更具扩展性,并且方式更简单的管理解决方案。基于触发器的复制确实是一件过时的事情,并且不应该在现代的基础设施中被看到。你可以打赌BDR—它是一个长期的,安全的解决方案。
理解最终一致性
在本书前面的一部分,对CAP理解进行了讨论。这是重要的一个组成部分,当一个新的数据库技术被评价是,它应该时刻被牢记在心。对于BDR,这个系统是具有最终一致性的。这是什么意思呢?维基百科(http://en.wikipedia.org/wiki/Eventual_consistency)提供了如下定义:
最终一致性是分布式计算使用的一个一致性模型,被用来实现非正式地保证高可用,如果对于给定的数据没有更新,最终对给定数据的所有访问将返回最后更新的值。
这个定义其实是如此的好并且简单,以至于把它放在这里很有意义。最终一致性的思想是在所有的节点上数据并不会立即相同,但是过一段时间,它实际上会相同的(如果没有发生什么事情)。最终一致性也意味着默认情况下,数据将被异步复制,因此,不是所有的节点什么时候都看到相同的数据。你必须期待看到略有不同的数据,这取决于您连接到的主机。
[BDR也支持同步复制。然而,它不像经典的两阶段提交事务那样安全。]
处理冲突
考虑到一致性模型,一个重要的话题出现了:冲突了怎么办?一般情况下,使用最终一致性的所有系统需要需要某种冲突解决。这用于适用于BDR。
BDR的妙处在于冲突管理是非常有弹性的。默认情况下,BDR提供了一个明确的冲突管理算法,它定义了最后的更新总是赢。
然而,更多的是,在BDR中,你自己写冲突解决方案算法也是可能的。一个服务器端的存储过程可以用来定义发生冲突是必须做的事情。这个机制给用户提供了最大的灵活性,并帮助用户实现更加复杂的目标。
BDR的另外一个优势是冲突修改可以被记录在一个表中。换句话说,如果发生冲突,逆转系统中正在发生的工程仍然是可能的。数据不会被默默地遗忘,但会为以后的调查被保存。
当谈到冲突,BDR的使用必须要牢记;BDR被设计成一个(地理地)分布式数据库系统,它允许您处理大量的数据。从一致性的观点来看,有一件事情必须要牢记:一个在纽约的人和一个在罗马的人在它们的节点上同时更改同一行的可能性有多大?如果这是在表中情况下的冲突,BDR真的不是合适的解决方案。然而,如果冲突几乎不发生(这对99%的应用程序的情况下来说),BDR是真正需要选择的方案。请记住,只要同一行同时被许多人改变,或者如果违反了一个主键,就会发生冲突。如果两个人更改完全相互独立的两行,冲突时不会发生的。因此,对于有些场合来说,最终一致性是一个合理的选择。
分布序列
序列是潜在的冲突来源。想象一下,数以百计的用户同时往一个相同的表中添加数据。任何自动增长列都会立即变成 冲突的真正来源,因为实例倾向于重复赋值相同或者相似的数字。
因此,BDR提供了分布式序列。每个节点不是被赋为一个值而一系列值,然后,这些值可以被使用,直到下一个系列值被分配。分布式序列的可用性大大减少了潜在的冲突的数量,并帮助您的系统比其它方式更顺利地运行。
处理DDLs
数据结构绝不是固定不变的。一旦在一段时间内,结构更改就会发生。可能一张表要被添加数据,或者一列必须被删除,等等。
BDR可以很好地处理这些操作。大多数DDLs只是被复制,因为它们只是被发送到所有节点并执行。然而,也有被禁止的命令。这里有两个比较突出的:
ALTER TABLE ... ALTER COLUMN ... USING();
ALTER TABLE ... ADD COLUMN ... DEFAULT;
请记住,冲突很可能是并发的,所以没有这些命令并非太多的问题。在大多数情况下,这些限制并不重要。然而,它们必须被牢记。
在许多情况下,有一个为那些命令变通的方法,例如,设置明确的值,和其它的方法。
BDR的使用场景
对BDR来说,既有好的使用场景也有不好的使用场景。当然,这条规则适用于每一个软件。然而,数据库系统有点不同,在作出决定之前仔细思考是必要的。
好的BDR使用场景
一般来说,如果一个特定的数据集只在一个节点上修改,BDR工作的最好。这大大减少了冲突的可能性,并帮助您享受一个平稳的过程。
在一个节点上修改实际意味着什么呢?我们假设有三个地点:维也纳,柏林,伦敦。与工作在柏林的德国人而言,在维也纳工作的修改奥地利的数据的可能性更大。德国操作员通常修改德国客户的数据而不是奥地利或者英国人的数据。
奥地利操作员极有可能更改奥地利的数据。每个操作员在每个国家都应该看到公司所有的数据。然而,更可能的是,数据在哪里被创建,就在哪里被改变。从商业的角度来看,这基本上是不可能的,两个人同一时间在不同的国家更改相同的数据—冲突时不可能的。
除此之外,如果工作负载主要有写操作组成,而不是UPDATE 或者DELETE 操作。插入式不太可能引起冲突的,因此,对BDR来说是一个好的工作负载。
坏的BDR 使用场景
然而,也有一些一般来说,对BDR是没有益处的工作负载,如果一致性是您的首要目标,而且是您的应用程序的关键,那么,使用BDR当然不是正确的选择。由于产品的异步特性,冲突和一致性可以相互抵消。
对BDR来说,另外一个不好的场景是,如果所有的节点需要在同一时间看到完全相同的数据,BDR是很困难的。
BDR可以有效地写数据。然而,它不能无限制地扩展写操作,因为在这一点,所有的写操作仍然结束于每台服务器。请记住,只有通过实际拆分数据,才能扩展写操作。所以,如果您正在找一个可以较好地扩展写操作的方案,PL/Proxy可能是一个更好的选择。
逻辑解码的戏法
BDR背后的主要概念是逻辑解码。正如本书已经提到的,逻辑解码已经被发明来剖析事务日志流和把二进制日志转换到一个更可读的格式,如SQL。和二进制流相比,逻辑流的优势是复制可以更容易地在版本边界发生。
另外一个重要的优点是不需要同步物理XLOG位置。正如本书在前面章节所展示的,XLOG地址是非常重要的,要使事情工作,不可以更改。因此,基于XLOG的复制总是单master和多slave复制。根本没有办法把两个二进制XLOG流统一到一个改变的流。逻辑解码以一个优雅的方式解决了那个问题,因为留下了整个XLOG同步的问题。通过SQL格式复制真实物理的更改,获得了很多的灵活性并提供了对为未来的改进的新的操作。
整个XLOG解码事情基本上在幕后进行;终端用户不会注意到它。
安装BDR
安装BDR很容易。该软件作为一个源程序包是可用的,并且它可以直接使用二进制包来部署。当然,从源代码来安装也可以的。然而,随着越来越多的更改转移到PostgreSQL内核,这一过程可能会改变。因此,我决定跳过源代码安装。
安装二进制包
在前面的章节中,您已经学习了如何使用预编译的二进制软件包在Linux系统上安装BDR。选择的显示安装工作是如果进行的Linux发行版是CentOS 7(要找到关于其它包的信息,请检查http://bdr-project.org/docs/stable/installation.html)
安装过程本身是简单的、首先安装repo:
yum install http://packages.2ndquadrant.com/postgresql-bdr94-2ndquadrant/ yum-repo-rpms/postgresql-bdr94-2ndquadrant-redhat-1.0-2.noarch.rpm
接下来的步骤中,可以部署BDR:
yum install postgresql-bdr94-bdr
一旦在所有的节点上都安装了DBR,系统就准备好运行了。
[请记住,BDR仍处于相当早期的发展状态,因此,随后的过程可能可能会随时间而改变。]
设置一个简单的集群
一旦安装完成,是时间开始并实际地设置一个简单的集群了。在这个场景,将创建一个由三个节点组成的集群。
请注意,为了让初学者更容易使用,所有的数据节点都将安装在相同的物理服务器上。
安排存储
管理员要做的第一件事情是为PostgreSQL创建一些空间。在这个简单的例子中,只创建三个目录:
[root@localhost ~]# mkdir /data
[root@localhost ~]# mkdir /data/node1 /data/node2 /data/node3
确保这些目录属于postgres(如果使用postgres用户运行PostgreSQL,这通常是一个好主意):
[root@localhost ~]# cd /data/
[root@localhost data]# chown postgres.postgres node*
一旦创建了这些目录,就准备好了一个成功的设置所需要的一切:
[root@localhost data]# ls -l
total 0
drwxr-xr-x 2 postgres postgres 6 Apr 11 05:51 node1
drwxr-xr-x 2 postgres postgres 6 Apr 11 05:51 node2
drwxr-xr-x 2 postgres postgres 6 Apr 11 05:51 node3
创建数据库实例
为我们的实验创建了一些空间之后,对交叉检查我们的系统路径是有意义的。确保正确的PostgreSQL版本在您的路径中。一些用户报告了在安装过程中因为意外地,一些操作系统提供的PostgreSQL版本在那个路径中中的问题。因此,只需检查路径并做相应的设置是有意义的,如果需要:
export PATH=/usr/pgsql-9.4/bin:$PATH
然后,可以创建三个数据库实例。initdb命令可以用在任何通常的情况:
[postgres@localhost ~]$ initdb -D /data/node1/ -A trust
[postgres@localhost ~]$ initdb -D /data/node2/ -A trust
[postgres@localhost ~]$ initdb -D /data/node3/ -A trust
为了使安装过程更简单,trust将被用作验证方法。当然,用户身份验证是可能的,但它不是这章主题的核心,所以最好尽可能地简化这部分。
既然已经创建了三个数据库实例,可以调整postgresql.conf了。以下参数是需要的:
shared_preload_libraries = 'bdr'
wal_level = 'logical'
track_commit_timestamp = on
max_connections = 100
max_wal_senders = 10
max_replication_slots = 10
max_worker_processes = 10
要做的第一件事是吧BDR模块加载到PostgreSQL中。它包含了复制的重要基础设施。下一步,必须启用逻辑解码。它将是整个基础设施的支柱。
要让BDR工作,就必须把track_commit_timestamp 打开。在标准PostgreSQL9.4中,这个设置是不存在的。它将最有可能和BDR一起出现在未来的PostgreSQL版本中。知道了提交的时间戳对BDR的内部冲突解决算法(最后胜利)是必不可少的。
然后,max_wal_senders必须和 replication slots一起设置。流复制也需要这些设置,也不应是一个大的惊喜。
最后,有一个max_worker_processes。只要PostgreSQL被启动,BDR就在后台发起一些客户端工作进程。这些工作进程是基于标准后台工作API的,并且在复制过程中被处理数据传输所需要。确保有足够可用的进程是必不可少的。
最后,还有一些冲突相关的设置可以被使用:
# Handling conflicts
#bdr.default_apply_delay=2000 # milliseconds
#bdr.log_conflicts_to_table=on
既然postgresql.conf已经配置好了,是时候把注意力集中在pg_hba.conf上了。在最简单的情况下,简单的复制规则必须被创建:
local replication postgres trust
host replication postgres 127.0.0.1/32 trust
host replication postgres ::1/128 trust
请注意,在一个真实的,高效的设置中,一个明智的管理者会配置一个特殊的复制用户并设置一个 密码,或者使用其它认证方法。为了简单起见,这个过程已经被排除在这里了。
使数据库开始工作就像普通PostgreSQL一样:
pg_ctl -D /data/node1/ start
pg_ctl -D /data/node2/ start
pg_ctl -D /data/node3/ start
对于我们的测试,需要一个数据库一个实例:
createdb test -p 5432
createdb test -p 5433
createdb test -p 5434
加载模块并启动集群
到目前为止,非常好!为了确保BDR可以做它的工作,它必须被加载到数据库中。需要两个扩展,即btree_gist和bdr:
[postgres@localhost node1]$ psql test -p 5432
test=# CREATE EXTENSION btree_gist;
CREATE EXTENSION
test=# CREATE EXTENSION bdr;
CREATE EXTENSION
这些扩展必须被加载到之前创建的三个数据库中。仅仅将它们加载到一个组件是不够的。把它们加载到所有的数据库中是至关重要的。
最后,我们的数据库节点必须都被加入到一个BDR组中。到目前为止,只有三个独立的数据库实例,其中正好包含谢谢模块。在下一步中,这些节点将彼此连接。
首先要做的是创建一个BDR组:
test=# SELECT bdr.bdr_group_create(
local_node_name := 'node1',
node_external_dsn := 'port=5432 dbname=test'
);
bdr_group_create
------------------
(1 row)
基本上,需要两个参数:本地名称和一个从远程主机连接到节点的数据库连接。要定义local_node_name,最简单的做法是,给节点一个简单名字。
要检查节点是否为BDR做好了准备,调用下面的函数。如果答案和下面的一样,就意味着配置没有问题:
test=# SELECT bdr.bdr_node_join_wait_for_ready();
bdr_node_join_wait_for_ready
------------------------------
(1 row)
现在是将其它节点添加到复制系统的时候了:
test=# SELECT bdr.bdr_group_join(
local_node_name := 'node2',
node_external_dsn := 'port=5433 dbname=test',
join_using_dsn := 'port=5432 dbname=test'
);
bdr_group_join
----------------
(1 row)
再次,一个NULL值是一个很好的标志。首先,第二个节点被添加到了BDR。然后,第三个节点一个可以加入:
test=# SELECT bdr.bdr_group_join(
local_node_name := 'node3',
node_external_dsn := 'port=5434 dbname=test',
join_using_dsn := 'port=5432 dbname=test'
);
bdr_group_join
----------------
(1 row)
一旦所有的节点都被添加了,管理员可以检查是否所有的节点都准备好了:
[postgres@localhost node2]$ psql test -p 5433
test=# SELECT bdr.bdr_node_join_wait_for_ready();
bdr_node_join_wait_for_ready
------------------------------
(1 row)
[postgres@localhost node2]$ psql test -p 5434
test# SELECT bdr.bdr_node_join_wait_for_ready();
bdr_node_join_wait_for_ready
------------------------------
(1 row)
如果两个查询都返回NULL,就意味着系统运行良好。
检查您的设置
这个简单过程之后,BDR启动并运行了。为了检查是否所有的工作都如预期的那样,检查相关复制进程是有意义的:
[postgres@localhost ~]$ ps ax | grep bdr
31296 ? Ss 0:00 postgres: bgworker: bdr supervisor
31396 ? Ss 0:00 postgres: bgworker: bdr db: test
31533 ? Ss 0:00 postgres: bgworker: bdr supervisor
31545 ? Ss 0:00 postgres: bgworker: bdr supervisor
31553 ? Ss 0:00 postgres: bgworker: bdr db: test
31593 ? Ss 0:00 postgres: bgworker: bdr db: test
31610 ? Ss 0:00 postgres: bgworker: bdr (6136360420896274864,1,16385,)->bdr (6136360353631754624,1,
...
31616 ? Ss 0:00 postgres: bgworker: bdr (6136360353631754624,1,16385,)->bdr (6136360420896274864,1,
正如你所看到的,每个实例都会有至少三个BDR进程。如果这些进程都在,这通常是一个好的标志,并且复制应该像预期的那样工作。
一个简单的测试可以揭示系统是否工作:
test=# CREATE TABLE t_test (id int, t timestamp DEFAULT now() );
CREATE TABLE
表创建之后,该结构应该看起来像这样:
test=# \d t_test
Table "public.t_test"
Column | Type | Modifiers
--------+-----------------------------+---------------
id | integer |
t | timestamp without time zone | default now()
Triggers:
truncate_trigger AFTER TRUNCATE ON t_test FOR EACH STATEMENT EXECUTE PROCEDURE bdr.queue_truncate()
这张表看起来像预期的那样。只有一个例外:一个TRUNCATE触发器被自动创建。请记住,replication slots能够流传输INSERT, UPDATE, 以及 DELETE 语句。DDLs和TRUNCATE现在是行级别信息,因此,这些语句还不再流中。触发器被需要来捕获TRUNCATE并把它复制成纯文本。不要尝试更改或者删除触发器。
要测试复制,一个简单的INSERT语句就可以工作:
test=# INSERT INTO t_test VALUES (1);
INSERT 0 1
test=# TABLE t_test;
id | t
----+----------------------------
1 | 2015-04-11 08:48:46.637675
(1 row)
在这个例子中,该值已经被添加到监听5432的实例。一个快速的检查显示,数据已经很好地被复制到监听5433和5434的实例中:
[postgres@localhost ~]$ psql test -p 5434
test=# TABLE t_test;
id | t
----+----------------------------
1 | 2015-04-11 08:48:46.637675
(1 row)
处理冲突
正如本章前面所属,当与BDR一起工作时,冲突是一件重要的事情。请记住,BDR被设计成一个分布式的系统,所以,当冲突不太可能时使用它才有意义。然而,了解冲突事件中发生了什么事情是很重要的。
要显示发生了什么,这里有一个简单的表:
test=# CREATE TABLE t_counter (id int PRIMARY KEY);
CREATE TABLE
然后,添加一行:
test=# INSERT INTO t_counter VALUES (1);
INSERT 0 1
要运行测试,一个简单的SQL查询是必要的。在这个例子中,使用了10000条UPDATE语句:
[postgres@localhost ~]$ head -n 3 /tmp/script.sql
UPDATE t_counter SET id = id + 1;
UPDATE t_counter SET id = id + 1;
UPDATE t_counter SET id = id + 1;
现在这个脚本被执行三次,一个节点上执行一次:
[postgres@localhost ~]$ cat run.sh
#!/bin/sh
psql test -p 5432 < /tmp/script.sql > /dev/null &
psql test -p 5433 < /tmp/script.sql > /dev/null &
psql test -p 5434 < /tmp/script.sql > /dev/null &
由于同一行一遍又一遍地冲击,冲突的数量预计将猛增。
[请注意,这不是BDR最初建立的的目的。它只是一个演示,以显示冲突事件中发生了什么。]
一旦这三个脚本完成了,就可以检查出冲突方面发生了什么:
test=# \x
Expanded display is on.
test=# TABLE bdr.bdr_conflict_history LIMIT 1;
-[ RECORD 1 ]------------+------------------------------
conflict_id | 1
local_node_sysid | 6136360318181427544
local_conflict_xid | 0
local_conflict_lsn | 0/19AAE00
local_conflict_time | 2015-04-11 09:01:23.367467+02
object_schema | public
object_name | t_counter
remote_node_sysid | 6136360353631754624
remote_txid | 1974
remote_commit_time | 2015-04-11 09:01:21.364068+02
remote_commit_lsn | 0/1986900
conflict_type | update_delete
conflict_resolution | skip_change
local_tuple |
remote_tuple | {"id":2}
local_tuple_xmin |
local_tuple_origin_sysid |
error_message |
error_sqlstate |
error_querystring |
error_cursorpos |
error_detail |
error_hint |
error_context |
error_columnname |
error_typename |
error_constraintname |
error_filename |
error_lineno |
error_funcname |
BDR提供了一个简单并且非常容易的方式来读取包含所有冲突行的表。在顶部的LSN,事务ID以及更多冲突相关信息的显示。在这个例子中,BDR已经做了一个skip_change解决方案。记得每个更改的行都会命中相同的行,因为我们是异步多主。这对BDR来说是非常讨厌的。在这个例子中
UPDATE语句确实被跳过了;理解这一点是非常重要的。BDR可以跳过冲突或者并发事件在您的集群中的更改。
理解集合
到目前为止,已经使用了整个集群。每个人都能够复制数据到其他人。在许多情况下,这是不需要的。BDR在这方面比较有弹性。
单向复制
BDR不仅能够进行双向复制,也可以进行双向复制。在某些情况下,这是非常方便的。考虑一个系统只提供读服务。一个简单的单向slave可能是您所需要的。
BDR提供了一个简单的函数来注册一个节点作为一个单向的slave:
bdr.bdr_subscribe(local_node_name,
subscribe_to_dsn,
node_local_dsn,
apply_delay integer DEFAULT NULL,
replication_sets text[] DEFAULT ARRAY['default'],
synchronize bdr_sync_type DEFAULT 'full')
当然,也有可能从单向复制中删除一个节点:
bdr.bdr_unsubscribe(local_node_name)
安装过程非常简单,适合BDR的基本设计原则。
处理数据表
BDR的妙处在于不需要复制整个实例到一个集群。复制可以是非常细粒度的,并且管理员可以决定什么数据复制到哪里。两个函数可以用于表复制集合:
bdr.table_set_replication_sets(p_relation regclass, p_sets text[])
这设置了一个表的复制集合。最初的分配将被覆盖。
如果您想要知道一个表属于哪个复制集合,可以调用下面的函数:
bdr.table_get_replication_sets(relation regclass) text[]
我们会在部分复制区域随着BDR的发展看到更多的功能。它将允许您根据需要灵活地调度数据。
控制复制
由于维护的原因,可能保持和一次又一次地恢复复制是有必要的。只要考虑一个主要软件更新。它可能会对您的数据结构做一些讨厌的事情。您绝对不想有错误的东西被复制到您的系统。因此,它可以方便地停止复制并重新启动它,一旦它被证明是正常的。
两个函数可以用于这个工作:
SELECT bdr.bdr_apply_pause()
要再次重新启动,可以使用下面的函数:
SELECT bdr.bdr_apply_resume()
连接到远程节点(或者节点)被保持,但是不能从它们那里读取数据。暂停请求的效果不是长久的,所以,如果PostgreSQL被重新启动或者postmaster在后台故障之后重新恢复,重放将重新恢复。中止个人后台使用pg_terminate_backend将不会引起重放来恢复,或者将重装postmaster,而不需要完全重新启动。没有选择从唯一一个对等节点来暂停一个重放
总结
BDR在PostgreSQL的复制世界中是一个后起之秀。目前,它仍然处于开发中,我们可以在不远的将来期待更多(也许在您手中拿着这本书的时候)。
BDR是一个异步多主并且允许人们运行地理分布式数据库。记住复制冲突率很低的时候BDR是特别有用的是非常重要的。