从这一章开始,我们就正式从单机应用转向了分布式应用的旅程!
ps: 其实DDIA这书我1月份已经看完了,只不过那会儿实在没有心力去翻译。前段时间太太太忙了,对几千万日活的系统做了技术栈迁移、跨洲数据中心无缝平移。后续也会写文章来分享我们是怎么做的,欢迎持续关注。
副本机制的意思是,在多台通过网络互连的机器上保存同一份数据的多份拷贝。我们为什么需要副本:
常见的一种副本模式:leader-based replication, 工作原理如下图所示:
1.副本中的一个被指定为leader,客户端的写请求必须发给leader处理
2.别的副本被称作followers, 当leader把新数据写到本地存储时,它也会把数据变更以log的形式发给它所有的followers。 每个follower从leader那里拿到log之后,把变更应用到本地存储。
3.客户端的读请求可以由leader或者followers来处理。
这种模式有很多系统在用,比如PostgreSQL, MySQL, Oracle Data Guard, MongoDB, RethinkDB, Espresso ,Kafka, RabbitMQ。
同步复制: leader接受到写请求后,需要等待follower的确认。好处是提高了duration。
异步: 不需要等待。好处是提高了响应速度。 目前被广泛应用。
在下图中,follower1的复制是同步的,follower2的复制是异步的。
半同步的配置: 一部分同步,一部分异步。
如果follower挂了或是与leader间的网络连接中断,那么可以在恢复之后,向主库请求从中断点开始之后所有的变更即可。
故障切换: 把一个follower提升为新的leader,重新配置客户端,将它们的写操作发送给新的leader,其他follower开始拉取来自新leader的变更。
故障切换可以手动也可以自动完成。自动的步骤一般如下:
故障切换是一件非常麻烦的事情:
比如对于mysql而言, 用delete之类的语句。
但现在在默认情况下,如果语句中存在任何不确定性(比如调用函数NOW()、自增列、有副作用(比如触发器和存储过程)),MySQL会切换到基于行的复制(见下文)。
非常底层,WAL会记录哪些磁盘块中的哪些字节发生了更改。
以行为粒度记录变更:
比如Canal之类的。
如果客户端从异步复制的follower那里读取,它可能会拿到已经过时的数据。如果停止向leader写入并等待足够时间,follower最终会追上leader,这叫做最终一致性。
复制延迟导致的一些问题和解决方法:
如果用户上传了一些数据,但是读请求打在了还没同步变更的那个follower上,这个用户大概率会骂一句傻逼。
这种情况我们就不能用最终一致性,可以用读写一致性,read-after-write consistency / read-your-writes consistency。
具体怎么做呢:
从异步复制的follower读数据会碰到另一个问题是:moving backward in time,也就是时光倒流问题。。
比如用户看别人的主页,第一次从一个延迟小的从库,第二次读一个延迟大的从库,会发现刚刚刷到的动态又消失了。
解决这种问题的方法是单调读: 单调读比强一致性弱,单比最终一致性强。 单调读保证如果一个用户顺序进行多次读,那么后续的读取不会读到比前面的读取更老的数据。
实现方法:保证每个用户总是读同一个副本,比如根据userid来做hash(但是如果那个副本挂了,做了重新路由之后,这种保证又被打破了)。
在分区/分片数据库中,还有一个特殊问题: 如果某个分区的复制速度比另一个慢,那么同时读取这俩分区的用户可能得到顺序错乱的数据, 如下图:
解决这个问题的常见思路是: 有因果关系的写入都写到同一个分区中。
(ps:比如在kafka中,如果要保证两条消息的消费顺序,那么就要保证它们写入了同一个partition )
前面我们讨论的都是单一leader的复制架构,这也是比较常见的做法。除此之外,还有一些别的方案,比如多leader、无leader。
如果是单数据中心,没有必要用多leader,平白无故提高复杂度。
但是如果是多数据中心,我们就可以用这种方案:
好处:
缺点:
会有写入冲突,下文会介绍。
如下图,当多个用户对同一个数据做修改时,如果用异步复制,可能会导致:数据都写到了本地leader,但是在复制时发生了冲突。
上图是异步检测。
同步冲突检测: 等到写入被复制到所有的副本后才返回成功
特定用户的写入、或者同一份数据的写入都写到同一个数据中心
不管怎么样,数据库最后必须处于一致状态,即所有副本必须再副本复制完成后处于同一个值。
方法:
现在基本不用了。 AWS内部的Dynamo系统在用。(注意并不是DynamoDB, DynamoDB是单一Leader架构)。
上文已经介绍了
如果操作B依赖操作A,那么A happens-before B。 如果A和B都没有happens-before对方,那么可以说他们是并发的。
我们可以用下面的方法来判断两个操作的关系:
这个算法的工作原理是: