阅读视图
多机多盘 minio 集群不同纠删码配置在 IPoIB 下的性能测试
MinIO 多节点多盘部署与运维
分布式 | 如何解决重复消费 | 小记
𠄗𠳵
𡎱𡢉𭇥𬷿𤸳𪯰𧂿(𨝶𤒪𥎉𧟭𬓃𬓃𨥋𤜾𥶝𬱆、𥎉𭵰𣝡𭝉𣏅𤜾𥶝𬱆、𫎉𧟭𭖙𭝉𧟭)𣲐,𨴧𬘭𠀐𥠔𪱌𥟽、𮜅𦦕𣤵、𤟴𣢚𦦕𧼠𭅢𢅜𦻰𫉙𬚑,𭇥𬷿𦦕𨕱𪳒𭔾𮃇𦌅𭅺𪶴。
𬚑𠫎,𣿽𨘍𦤳𢽮𩎄𭶽𪶴𪌎𡐽𤸘𠜂𫿲𡎱“𤗘”,𤀿𡎱“𡸘”,𤄨𡎱𭇥𡎭𮍀幂𭅢𭀁𠶈𮈋。
𬂪幂𭅢𭀁,𪳒𧆋𢽮𣍴𨟕𣇓𢅜𦦕𨕱𣍴𨟕𤶙𤷍𮡈𣿦𡒉𪶴𤋑𭭝𪳒𢽮𫹉𪶴。𨏍𢋄𠴃𪇅𬻡𢽮𦉋𩏄𧜖𪶴𤒪𧹘𬱆𦒚𤒪𥓖𠼜𮜛𮈋𣿽:
幂𭅢𣤸𢫋:
1
UPDATE order_table SET counter = 10 WHERE id = 1;
𭔾𨿇𬰭𧙹𢅜𩣡,
counter𤒪𪶴𣝥𬍘𡗀𭩔𪳒𤒪𡆐𦢏。𫻒幂𭅢𣤸𢫋:
1
UPDATE order_table SET counter = counter + 1 WHERE id = 1;
𩆿𬰭𧙹𭩔𩊼𩓴𬂭𬲮𮡟。𨝶𮡟𭇥𬷿𦦕𨕱𣔵𫹉庰𣖴𨹤𢵗𬰭𧙹𢅜,
counter𤒪𪶴𣝥𮎩𩊼𠱾𫃑𮜑𩐥𢅜。
𣿽𨘍𦦕𨕱𭇥𡎭𪶴𪅎𣦋,𮎩𪳒𬻗𦤳𦻰𫻒幂𭅢𪶴𠽐𣢚𣤸𢫋𭒟𩴖𫽡幂𭅢𣤸𢫋。𩫵𠰒𪶴𣿽𨘍𤸘𠜂𤔭𡊧𨯶𪹠𦻰。
分布式计算框架 Ray
MPI 通信原语及 Python 编程使用
关于多写入点数据库集群的一些想法
在分布式数据库系统领域, 多主(多写入点, Leader-less)是一个非常诱人的特性, 因为客户端可以随机请求任何一个节点. 这种可随机选择访问点(写入点)的特性, 使得系统的高可用唾手可得, 因为当客户端发现某个节点出故障时, 更换另一个节点重试就可以了, 只要系统没有完全宕机, 几次重试之后一定成功, 也就可以达到百分之百高可用.
传统的 Basic Paxos 常常被误认为是 Leader-less 的, 也即多主, 但 Basic Paxos 只能用于确定一个实例的共识, 真正落地还需要结合日志复制状态机, 如果复制组(多节点)不指定 Leader 的话, 那么就会出现争取同一个位置的日志的情况, 也就是在尝试达成这个位置的日志的共识时出现活锁. 这种多节点争取同一个位置的情况, 在实践上将导致系统不可用, 因为, 通常自称采用 Paxos 的多副本数据库系统, 依然要显式指定 Leader, 并不是真正 Leader-less 的.
借鉴 vector 时钟和多复制组的思路(如 Multi Raft Group), 是可以避免多主冲突的, 因为, 不同的节点作为各自组的 Leader, 分别写入不同的日志序列, 也就完全没有争取同一个位置的日志而导致冲突了.
以一个3节点的集群来举例, 通过预先配置强制指定各为其主, 结构为:
| 主 | 从 | 从 | |
|---|---|---|---|
| G1 | A | B | C |
| G2 | B | A | C |
| G3 | C | B | A |
有复制组 G1, 成员节点是 {A, B, C}, 其中节点 A 是主. 类似的, 复制组 G2 和 G3 的主是 B, C. 当客户端请求不同节点时, 将日志写入不同复制组的日志序列, 因此不会产生冲突.
某一时刻, 所有节点的状态是:
| G1 | G2 | G3 | |
|---|---|---|---|
| A | 6 | 1 | 2 |
| B | 6 | 1 | 2 |
| C | 6 | 1 | 2 |
这个状态表明, 复制组 G1 累计有 6 条日志, 复制组 G2 是 1 条, 复制组 G3 是 2 条.
到目前为此, 似乎一切顺利, 写入没有冲突, 还能通过复制组形成多副本. 但是, 现实不是这样的, 一个只能写入的数据库几乎没有任何用处, 数据库必须支持读取, 所以, 日志复制状态机架构必须有状态机, 也即这 3 条日志序列必须在所有节点 Apply 到各节点的状态机实例中.
最简单的方法是在各个节点上把 3 条日志序列合并成(Merge)一条日志序列, 通过某种算法保证所有节点上的合并结果一定是相同的, 例如按时间戳排序, 这样才能保证状态机一致(相同).
但是, 节点不能只依赖自己本地的 3 条日志序列合并, 在每一次合并时, 它需要获取其它复制组的最新信息, 判断自己本地的日志序列是不是足够新. 自己作为 Leader 的那一条序列, 当然自己能确定, 但其它两条不能, 需要询问其它节点, 不能定死询问 Leader, 因为需要容忍某个复制组的 Leader 宕机的情况.
所以, 针对其它复制组, 采取 Read Index 逻辑, 可以判断是否最新. 并且在其它组 Leader 宕机的情况下, 将对应的日志序列从其它 Follower 那里补齐.
日志序列中可能包含非幂等的指令, 通过加入时间戳之后, 非幂等的指令可以变成幂等的.(需要文章论证). 以一个 key 为例, 状态机中的 key-value 带有初始化时间戳和最新更新时间戳 {reset_time, modify_time}. 当收到一条 incr 指令时, 指令中带有时间戳, 与 key-value 中的 meta 信息比对之后, 就能知道 Apply 算法.
if key.reset_time < op.time {
value += 1
} else {
// 忽略在初始化之前的指令
}
例如, 在 A 节点上针对 key 执行了 set 操作, 便会复位其 reset_time, 之后, 再收到其它节点的更早的 incr 指令时, 这个 incr 指令就不能修改状态机了, 因为这个指令发生在复位之前.
各个节点的系统时钟不同步没关系, 因为一致性与系统时钟没有必然联系(需要文章论证). 只要确保所有节点最终的结果是一致的(相同)的就行. 不要让 A 节点先执行 set 再执行 incr, 而 B 节点先执行 incr 再执行 set 这种情况出现. 在这个例子中, 从上帝视角知道 incr 在 set 之前, 所以, A 节点先执行 set, 然后遇到 incr 指令时会忽略.
上面的时间戳比较要求不同的节点产生的时间戳不能相同, 可以把节点ID当作时间戳的最低位, 避免两个时间戳相同.
对于 incr 操作, 初始化比较简单, 但是, 对于 pop 这种非幂等性的操作, 并不是初始化, 很难解决. 本质上, 是一种回滚操作, 能实现, 但成本太大, 要讨论起来真是够多的.
不同的节点收到不是自己负责的日志序列的延迟不一定, 但是, 我们又不能等确保全部日志序列都收到之后再 Apply, 虽然等全部日志序列都到齐之后再 Apply 可以不需要回滚操作, 但等待行为不能容忍宕机(CAP 理论).
总结起来, 就是没有银弹. vector 时钟看起来很美好, 但日志序列之间的相互依赖问题, 要么通过停止等待来解决, 要么通过回滚操作来解决, 否则无法保证多副本一致(相同). 但是, 某些操作的回滚成本太大.
注: 有一种技术叫 CRDT, 思想类似, 但是上面讨论的问题依然存在.
考虑过这个问题:
C 宕机, 但是它上面有一条日志还没有复制到其它节点.
A 和 B 各自合并日志序列之后 Apply 了, 两者的状态机一致, 没问题.
这时, C 恢复, 那条日志得以复制出去, 这时, A 和 B 需要回滚, 然后重新合并, 再 Apply.
如果 C 在恢复之后, 给那条日志赋予新的时间戳, 那么也会有一种场景(C 宕机前日志已经复制出去了, 但它还没知道结果), 同样需要回滚, 因为别的节点可能已经 Apply 过那条日志了.
Related posts:
ShardingSphere-JDBC介绍
ShardingSphere-JDBC是一款可以将JDBC操作进行封装,然后实现数据分片、分布式事务、读写分离、高可用、数据加密和数据脱敏等功能的模块。它的原理是实现JDBC的接口,随后将收到的JDBC操作进行改写和处理,再将操作命中到真正的数据库之上。因为它实现了JDBC接口,因此现有的Java项目都可以100%兼容使用,只需要依赖ShardingSphere-JDBC并提供相关的配置即可。
JDBC数据分片的简单使用
我们看一个简单的JDBC数据分片的例子,首先我们需要添加相关的maven依赖
1 | <dependency> |
如上添加了shardingsphere-jdbc和mysql的依赖,shardingsphere-jdbc是项目的核心依赖,而mysql则是jdbc操作需要用到的依赖。添加了maven依赖之后我们可以先创建相关的数据库和表,创建数据库和表的sql如下
1 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; |
我们会创建6个数据库,分别为ds_0到ds_5,并且会在每个数据库里面创建一个名叫t_order的表。
为了使用shardingsphere-jdbc,我们需要创建相应的jdbc连接和配置,因为shardingsphere-jdbc实现了jdbc的接口,所以我们可以像使用普通的jdbc一样使用shardingsphere-jdbc。创建shardingsphere-jdbc连接的代码如下
1 | Class.forName("org.apache.shardingsphere.driver.ShardingSphereDriver"); |
如上我们创建了一个shardingsphere-jdbc的连接,可以看到就是一个创建JDBC的过程。其中使用的SPI类是org.apache.shardingsphere.driver.ShardingSphereDriver,而具体的jdbcUrl则是一个文件地址shardingsphere-config.yaml,shardingsphere-jdbc的配置就保存在这个文件中。根据shardingsphere-jdbc的官方文档,其配置包含五大类:
- JDBC逻辑数据库名称
- 运行模式配置
- 数据源集合配置
- 规则集合配置
- 属性配置
shardingsphere-jdbc的配置支持Java代码和yaml文件,这里我们只介绍yaml文件,下面是一个简单的例子shardingsphere-config.yaml
1 | dataSources: |
如上配置了6个数据源分别是数据库ds_0到ds_5,props设置了打印sql语句,rules包含了表、分片算法和主键生成算法的配置。表设置中创建了一个逻辑表t_order,对应的真正数据库表是ds_0.t_order到ds_5.t_order,数据库的使用策略是通过id进行分片,分片算法是testInline,表的id字段的生成算法为snowflake。分片算法中定义了算法testInline,它使用INLINE内置方式来对id取模并和ds_进行拼接,构成数据库名。字段生成算法中定义了类型为SNOWFLAKE的字段生成算法。
有了如上配置之后,我们就可以使用shardingsphere-jdbc了。以一个数据插入操作为例,在引入了maven依赖、创建了相关的数据库和表、定义了相关的shardingsphere-jdbc配置之后,我们就可以使用上面创建的conn字段实现数据插入了。
1 | String sql = "INSERT INTO t_order (`user_id`, `order_id`) VALUES (?, ?)"; |
如上代码会创建一条数据并且随机根据snowflake算法生成一个id字段,并根据id字段的取模结果将数据保存到真正的数据库中去。更多的增删改查操作可在如下代码中看到:https://github.com/RitterHou/test-shardingsphere/tree/basic/src/main/java/com/nosuchfield/shardingsphere/data
SpringBoot集成MyBatis使用shardingsphere-jdbc
根据官方issue,目前shardingsphere-jdbc已经不再使用spring-boot-starter,而是直接使用jdbc实现相关功能。这种方式可以完美兼容JDBC的相关接口,因此可以简化很多已有项目的使用。
在SpringBoot中使用ShardingSphere需要设置如下的pom配置,在这里我们使用MyBatis作为ORM框架。
1 | <?xml version="1.0" encoding="UTF-8"?> |
SpringBoot的application.yml配置如下,这里配置的数据源驱动为ShardingSphereDriver,而url就是我们配置ShardingSphere属性的地方。除此之外,我们还配置了mybatis的SQL语句所对应xml文件的路径信息。
1 | spring: |
接着我们配置ShardingSphere的配置信息config.yaml,这里的配置和上面简单使用的配置差不多,不再赘述了
1 | dataSources: |
接着我们定义一个订单模型Order,订单包含了一些属性信息
1 | public class Order { |
我们根据这个模型可以定义个MyBatis的Mapper,它包含了插入、查询的操作
1 | @Mapper |
其中getAllOrders方法通过注解实现了SQL的定义,而另外两个方法的SQL则在xml文件中进行实现
1 | <?xml version="1.0" encoding="UTF-8"?> |
构建了如上的ShardingSphere和MyBatis的配置之后,我们可以创建相关的数据库和表
1 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; |
有了上面的数据库和表之后,我们就可以测试ShardingSphere的数据插入和查询了
1 | @Slf4j |
读写分离和数据脱敏
上面我们测试了ShardingSphere的数据分片功能,下面我们了解一下它的读写分离和数据脱敏。我们先在ds_0、ds_1和ds_2数据库中创建表t_user
1 | CREATE TABLE `t_user` ( |
之后我们在ShardingSphere的rules属性下添加如下配置
1 | - !READWRITE_SPLITTING |
配置包含了写库ds_0和读库ds_1、ds_2的配置,读库的负载均衡策略为随机(这里需要先设置ds_1和ds_2自动同步ds_0的数据,详细过程可查看文章MySQL实现双服务器主从同步)。数据脱敏策略为对t_user的id字段进行md5脱敏,对phone字段保留前3位和后4位,剩下的部分用*替换。创建好了表和配置之后,我们设置User的model
1 | public class User { |
以及mapper
1 | @Mapper |
之后我们测试上面的操作
1 | @Slf4j |
我们先插入数据,随后到从库中查询数据,得到结果如下
ShardingSphere-SQL:73 Logic SQL: SELECT * FROM t_userShardingSphere-SQL:73 Actual SQL: ds_1 ::: SELECT * FROM t_usercom.nosuchfield.shardingsphere.UserMapperTest:30 [User(id=0a113ef6b61820daa5611c870ed8d5ee, name=小明, phone=138****8888, address=江苏省南京市)]可以看到数据插入到了主库中,随后从从库ds_1中查询出了相关的数据,并且对id和phone字段的数据进行了脱敏操作,id字段被转化为了MD5的结果,而phone的中间4位被星号替代了。
数据加密
数据加密可以保证我们存到数据库中的数据都是经过加密的,和数据脱敏刚好反过来。首先我们创建表t_member
1 | CREATE TABLE `t_member` ( |
随后我们配置ShardingSphere的数据加密配置
1 | - !ENCRYPT |
我们将表t_member的name字段使用name_encryptor的加密方式进行加密,加密之后的字段名仍然叫做name,name_encryptor的配置在encryptors中可以看到,使用了AES加密算法并设置key为123abc。类似的,password的加密方式为MD5,在计算MD5的时候加盐nosuchfield。
随后我们创建model
1 | public class Member { |
和mapper
1 | @Mapper |
并测试写入和读取
1 | @Slf4j |
在插入了数据{"name": "张三", "password": "123456"}之后,可以到数据库中查看插入的数据如下
PS C:\Program Files\MySQL\MySQL Server 8.0\bin> ./mysql -u root -pmysql> select * from t_member;+--------------------------+----------------------------------+| name | password |+--------------------------+----------------------------------+| Fod6ouOanqNvHlTdBsx1Lw== | 47514eed77109a04ce4c9f9931d0c5ec |+--------------------------+----------------------------------+1 row in set (0.00 sec)可以看到name和password在存储到数据库的时候都加密了。随后我们执行测试代码中的查询逻辑,可以看到结果如下,name又通过AES算法解密成功,而password因为使用的是MD5算法就无法解密了
com.nosuchfield.shardingsphere.MemberMapperTest:27 [Member(name=张三, password=47514eed77109a04ce4c9f9931d0c5ec)]本节使用到的代码:https://github.com/RitterHou/test-shardingsphere