开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第23天,点击查看活动详情
本文先看MongoDB的系统架构,而后考虑如何使用因果一致性,
本文章节部分内容选自: 论文地址:dl.acm.org/doi/10.1145… 请注意,MongoDB的因果一致性无法解决下面这个问题,User1通过不同的Proxy进入到服务去读写MongoDB,但是session2无法排序session1,去实现RYW,在不改User1访问策略情况下,因为MongoDB Client SDK的令牌若无法通过Proxy透传到MongoDB,则无法实现RYW。此时你需要额外的组件,比如晚点我会写的Facebook FlightTracker.可以client无入侵地实现RYW。
系统模型
MongoDB的部署可以是一个分片集群、副本集或独立的。单机是一个存储节点,代表一个数据存储的单一实例。一个MongoDB部署可能由以下部分组成
- 存储节点也被称为mongod,通常是复制集的一部分,因此可能是primary节点、secondary节点或其他一些跟随者模式(即回滚、恢复、初始同步)。我们将描述一个分片的集群部署,其中每个分片是一个由存储节点组成的复制集。
- 客户端是通过驱动与一个或多个MongoDB部署进行通信的应用程序进程。
- 路由器也被称为mongos,它不保存数据,只路由客户端命令。集群节点是MongoDB部署中的任何路由器或存储节点。
MongoDB部署可能包含多个数据分片。每个分片代表了一个水平分区的数据集。一个配置分片也是为了保存描述数据分布和数据库配置的元数据,因为数据在MongoDB中可以在分片之间弹性迁移。每个分片可以是一个复制集,包含一个主节点和一个或多个辅助节点。在任何时候,只能有一个主节点接受客户端对一组数据的写入,通过共识选举选择。然而,在网络分区期间,有可能暂时有多个节点充当主节点(尽管只有一个是真正的主节点),在这种情况下,以前的主节点还没有意识到它已经从大多数人那里被分区了。只有通过共识选出的新主节点的写入内容才会被持久化。主节点接收所有客户端的写操作,并在其操作日志中记录对其数据集的所有其他变化。辅助节点复制主节点的操作日志,并将操作应用于他们的数据集,从而使他们的数据集反映主节点的数据集。如果主节点不可用,一个符合条件的二级节点将调用一个选举来选举新的主节点。
每个写请求都包含或暗含了一个配置项:write concern[32]。write concern指定了一个写入何时可以被确认给客户端。一个可能的write concern值是 "majority"。majority write保证只有在数据被包括主节点在内的大多数投票节点持续保存之后,写才会被确认。majority write的写入是持久的,即使在分区的情况下也不会丢失。
尽管客户端不能向二级成员写入数据,但客户端可以从二级成员中读取数据。客户端可以指定一个read concern[31],以控制读取数据的一致性和隔离属性。其中,读取关注级别 "majority "允许客户端只查询多数人承诺的数据。即使在系统发生故障的情况下,返回的文件也是持久的。
图1 MongoDB 架构
复制集中的每个存储节点都是一个状态机,应用操作日志的变化。操作日志条目在主节点上创建,并在此后的某个时间复制到辅助节点上。虽然辅助节点最终会有最新的数据,但其状态可能会落后于主节点。操作日志中的条目是由一个基于时间戳的结构来排序的,可以用来确定事件发生的顺序。
时间戳是基于master的walltime。MongoDB不提供部署中的节点之间的时钟同步的执行。节点使用操作系统提供的时钟同步,这在很多情况下是基于流行的NTP协议的[28]。节点walltime的准确性受制于互联网连接延迟、为同步而选择的时间服务器的阶层以及与时间服务器的距离。
关于因果一致性实现的算法、协议和数据结构的详细描述可以在附录中找到。
6 使用因果一致性
在引入因果一致性支持之前,MongoDB驱动的应用程序已经存在了好几年。在因果一致性之前,用户应用程序通常是使用副本集中的主节点进行写入和读取,以增加观察数据一致性的可能性。这种方法在没有可能导致分裂脑的选举中起作用,因为MongoDB完全对操作日志中的所有写进行排序。偶尔的非因果一致的读取和命令可以被大多数应用所容忍,因为选举是罕见的:我们对Atlas[29]--我们的管理云MongoDB的统计数据提供 - 显示每个副本集每50天的1次选举(包括维护)。
然而,从primary节点读取只影响了应用的可扩展性,特别是对于那些主要是读或地理分布的工作负载。虽然地理分布的写可以用区域分片来解决[34],但本地读需要因果一致性,以便与写保持因果一致性。为了解决那些不能容忍数据偶尔呆滞的环境中的可扩展性和性能要求,用户正在实施应用端逻辑,以确保从第二方读取的数据不陈旧(stale)。
在本节中,我们将概述其中一些应用程序如何使用MongoDB的功能,以及添加因果一致性如何让它们改善和扩展功能和服务。MongoDB在客户端会话中启用因果一致性。每个启用了因果一致性的会话都会跟踪签名的clusterTime--最高的已知逻辑时间和opearationTime--最后一次操作的因果快照的逻辑时间。operationTime必须不大于clusterTime。MongoDB提供了一个API来在会话之间传递这些值,以使客户能够在多个会话,甚至是客户之间扩展因果一致性的操作链。因为因果一致性为用户提供了所有操作都是因果的保证,它允许从二级节点进行因果一致的读取,这在某些场景下可能是有用的,包括地理复制的低延迟读取。
最终可以这样使用因果一致性特性:请注意,我们的目标是类似session一致性的需求:如下代码是实现的关键:session2的 clustertime 和 operation time设置为 session1的,至此,我们完成了session2的因果一致性读
auto cluster_time_1 = session_1.cluster_time();
auto operation_time_1 = session_1.operation_time();
session_2.advance_cluster_time(std::move(cluster_time_1));
session_2.advance_operation_time(std::move(operation_time_1));
int main() {
using namespace mongocxx;
instance inst{};
client client{mongocxx::uri{"mongodb://localhost/?replicaSet=replset"}};
write_concern wc_majority{};
wc_majority.majority(std::chrono::milliseconds(1000));
read_concern rc_majority{};
rc_majority.acknowledge_level(read_concern::level::k_majority);
// 为操作1创建session1,请注意,设置causal consistency为true
options::client_session session_opts;
session_opts.causal_consistency(true);
auto session_1 = client.start_session(std::move(session_opts));
auto time_point = std::chrono::system_clock::now();
bsoncxx::types::b_date current_date(time_point);
auto items = client["test"]["items"];
items.write_concern(wc_majority);
items.read_concern(rc_majority);
// 在session1上更新
auto none = bsoncxx::types::b_null{};
auto update_filter = document{} << "sku"
<< "111"
<< "end" << none << finalize;
auto update_op = document{} << "$set" << open_document << "end" << current_date
<< close_document << finalize;
items.update_one(session_1, std::move(update_filter), std::move(update_op), {});
//insert ,注意, 这里重载了<<
auto insert_doc = document{} << "sku"
<< "nuts-111"
<< "name"
<< "Pecans"
<< "start" << current_date << finalize;
items.insert_one(session_1, std::move(insert_doc));
//session2 当做session1的写后读。
options::client_session session_opts_2;
session_opts_2.causal_consistency(true);
auto session_2 = client.start_session(std::move(session_opts_2));
// session2的 clustertime 和 operation time设置为 session1的
// 至此,我们完成了session2的因果一致性读
auto cluster_time_1 = session_1.cluster_time();
auto operation_time_1 = session_1.operation_time();
session_2.advance_cluster_time(std::move(cluster_time_1));
session_2.advance_operation_time(std::move(operation_time_1));
read_preference rp_secondary;
rp_secondary.mode(read_preference::read_mode::k_secondary);
items = client["test"]["items"];
items.read_preference(rp_secondary);
items.write_concern(wc_majority);
items.read_concern(rc_majority);
auto find_query = document{} << "end" << none << finalize;
auto cursor = items.find(session_2, std::move(find_query));
for (auto&& doc : cursor) {
std::cout << bsoncxx::to_json(doc) << std::endl;
}
}