Percolator论文阅读

Percolator论文阅读

需求与背景

文章提出了在谷歌搜索引擎所依赖的网页文件系统中,假如我们运算一个类似pagerank的算法来统计页面信息,当网页不断更新时,尽管增量并不多,由于网页系统中原先的数据不变性得不到保证以及网页之间的链接的存在,会造成大量的重复计算。

谷歌的索引系统存储了几十PB的数据,每天在数千台机器上处理数十亿的更新。在面对这样的业务需求中,传统数据库或者现有的分布式计算系统并不能满足这些任务的存储或吞吐量要求。例如MapReduce和其他批处理系统是为了大型批处理任务的效率而量身定制的,并不适合单独的处理小的更新。而传统数据库虽然能保证数据的一致性,但却做不到很好的延展性。

文章提出并构建了 Percolator,一个用于增量处理大型数据集更新的系统,并部署它来创建谷歌网络搜索索引。通过用基于增量处理的Percolator的索引系统取代基于批处理的索引系统,能同时保证数据系统的性能要求和数据一致性。


Percolator为用户提供对多PB资源库的随机访问。随机访问允许我们单独地处理文档,避免了MapReduce需要的存储库的全局扫描。为了实现高吞吐量,许多机器上的许多线程需要同时转换存储库,因此Percolator提供ACID兼容事务以使程序员更容易推断存储库的状态;我们目前实现了快照隔离的语义。

除了对并发性进行推理外,增量系统的程序员还需要跟踪增量计算的状态。为了帮助他们完成这项任务,Percolator 提供了observers:当用户指定的列发生变化时,系统调用的代码段。Percolator应用程序是由一系列观察员构成的;每个observers通过写入表格来完成一项任务并为“下游”观察员创造更多的工作。外部进程通过将初始数据写入表中来触发链中的第一个observers。

Percolator专门为增量处理而构建,并不打算替代大多数数据处理任务的现有解决方案。如果计算结果不能被分解成小的更新(例如,对文件进行排序),那么由MapReduce来处理会更好。另外,计算应该有很强的一致性要求,否则Bigtable就足够了。最后,在计算某个维度应该会非常大(总数据大小,转换所需的CPU等);小数据量的计算可以使用 DBMSs 来处理。

design

Percolator为大规模执行增量处理提供了两个主要抽象:通过随机访问库和observers的ACID事务以及一个增量数据的观察者,这是组织增量计算的一种方式。

一个 Percolator 系统由三个二进制文件组成,它们在集群中的每台机器上运行:一个 Percolator worker,一个 Bigtable tablet 服务器和一个 GFS 块服务器。所有的 observers 都被链接到 Percolator Worker 中,它扫描Bigtable的变化列,并在Worker进程中以函数调用的方式调用相应的 observers。observers 通过向 Bigtable tablet 服务器发送读取/写入RPC来执行事务,Bigtable tablet服务器将读/写RPC发送给 GFS 块服务器。该系统还依赖于两个小服务:时间戳oracle和轻量级锁服务。时间戳oracle提供严格增加的时间戳:正确操作快照隔离协议所需的属性。Workers 使用轻量级锁定服务来更有效地搜索脏数据通知。

图1

Percolator的设计受到两个前提的影响:一是必须运行在大规模数据上,二是并不要求非常低的延迟。例如,不严格的延迟要求让我们采取一种懒惰的方法来清除在失败的机器上运行的事务遗留的锁。这种懒惰的、简单易行的方法可能会使事务提交延迟几十秒。在运行OLTP 任务的 DBMS 中,这种延迟是不可接受的,但在构建 Web 索引的增量处理系统中是可以容忍的。Percolator 的事务管理缺乏一个全局控制器:尤其是它缺少一个全局死锁检测器。这增加了冲突事务的延迟,但允许系统扩展到数千台机器。

事务

Percolator 使用 ACID 快照隔离语义提供跨行跨表事务。Percolator的用户可使用必要的语言(当前是C++)编写它们的事务代码,然后加上对 Percolator API的调用。图2 表现了一段简化的基于内容hash的文档聚类分析程序。这个例子中,Commit 返回 false 说明事务存在冲突(在这种情况下,因为两个具有相同内容哈希的URL被同时处理),所以需要回退重试。调用 Get 和 Commit 是阻塞式的,可以通过在一个线程池里同时运行很多事务来增强并行。

图2

尽管不利用强事务的优势也可能做到数据增量处理,但事务使得用户能更方便的推导出系统状态,避免将难以发现的错误带到长期使用的存储库中。比如,在一个事务型的web索引系统中,开发者能保证一个原始文档的内容hash值永远和索引复制表中的值保持一致。而没有事务,一个不合时的冲击可能造成永久的不一致问题。事务也让构建最新、一致的索引表更简单。注意我们说的事务指的是跨行事务,而不是Bigtable提供的单行事务。

Percolator使用Bigtable中的时间戳维度,对每个数据项都存储多版本,以实现快照隔离。在一个事务中,按照某个时间戳读取出来的某个版本的数据就是一个隔离的快照,然后再用一个较迟的时间戳写入新的数据。快照隔离可以有效的解决“写-写”冲突:如果事务A和B并行运行,往某个cell执行写操作,大部分情况下都能正常提交。与可序列化隔离级别相比,快照隔离的主要优势在于更高效的读取。因为任何时间戳都代表一个一致的快照,读取一个单元只需要在给定的时间戳上执行一个Bigtable查询;获取锁是不必要的。图3说明了快照隔离下事务之间的关系。

图3

由于Percolator是作为访问Bigtable的客户端库而构建的,而不是控制对存储的访问,因此它在实现分布式事务方面面临着与传统PDBMS不同的挑战。传统PDBMS为了实现分布式事务,可以集成基于磁盘访问管理的锁机制:PDBMS中每个节点都会间接访问磁盘上的数据,控制磁盘访问的锁机制就可以控制生杀大权,拒绝那些违反锁要求的访问请求。

相比之下,Percolator中的任何节点都可以发出请求,直接修改Bigtable中的状态:没有太好的办法来拦截并分配锁。所以,Percolator一定要明确的维护锁。锁必须持久化以防机器故障;如果一个锁在两阶段提交之间消失,系统可能错误的提交两个会冲突的事务。锁服务一定要高吞吐量,因为几千台机器将会并行的请求锁。锁服务应该也是低延迟的;每个Get 操作都需要申请“读取锁”,我们倾向于最小化延迟。锁服务器需要冗余备份(以防异常故障)、分布式和负载均衡(以解决负载),并需要持久化存储。Bigtable作为存储介质,可以满足所有我们的需求,所以Percolator将锁和数据存储在同一行,用特殊的内存列,访问某行数据时Percolator将在一个Bigtable行事务中对同行的锁执行读取和修改。

流程

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在 Commit 的第一阶段(预写,prewrite ),我们尝试锁住所有被写的 cell。(为了处理客户端失败的情况,我们指派一个任意锁为primary ;后续会讨论此机制)事务在每个被写的cell上读取元数据来检查冲突。有两种冲突场景:

  • 如果事务在它的开始时间戳之后看见另一个写记录,它会取消(32行),这是”写-写”冲突,也就是快照隔离机制所重点保护的情况。
  • 如果事务在任意时间戳看见另一个锁,它也取消(34行)。如果看到的锁在我们的开始时间戳之前,可能提交的事务已经提交了却因为某种原因推迟了锁的释放,但是这种情况可能性不大,保险起见所以取消。如果没有冲突,我们将锁和数据写到各自cell的开始时间戳下(36-38行)。
1 class Transaction {
2 	struct Write { Row row; Column col; string value; };
3 	vector<Write> writes ;
4 	int start ts ;
5
6 	Transaction() : start ts (oracle.GetTimestamp()) {}
7 	void Set(Write w) { writes .push back(w); }
8 	bool Get(Row row, Column c, string* value) {  //读事务流程
9 		while (true) {
10 			bigtable::Txn T = bigtable::StartRowTransaction(row);
11 			// Check for locks that signal concurrent writes.
12 			if (T.Read(row, c+"lock", [0, start ts ])) {  // 在开始前已经有事务加锁,暂时不能读取
13 				// There is a pending lock; try to clean it and wait
14 				BackoffAndMaybeCleanupLock(row, c);
15 					continue;
16 			}
17
18 			// Find the latest write below our start timestamp.
19 			latest write = T.Read(row, c+"write", [0, start ts ]);   //寻找最后一次提交成功的时间戳
20 			if (!latest write.found()) return false; // no data    //没有write信息,此行是空行,不存在
21 			int data ts = latest write.start timestamp();
22 			*value = T.Read(row, c+"data", [data ts, data ts]);  //读取特定时间范围内的数据
23 			return true;
24 		}
25 	}
26 	// Prewrite tries to lock cell w, returning false in case of conflict.
27 	bool Prewrite(Write w, Write primary) {  //写事务流程
28 		Column c = w.col;
29 		bigtable::Txn T = bigtable::StartRowTransaction(w.row);
30
31 		// Abort on writes after our start timestamp . . .
32 		if (T.Read(w.row, c+"write", [start ts ,])) return false;  //开始后有事务成功提交,rollback
33 		// . . . or locks at any timestamp.
34 		if (T.Read(w.row, c+"lock", [0,])) return false;  //有锁,rollback
35
36 		T.Write(w.row, c+"data", start ts , w.value);  //先写数据
37 		T.Write(w.row, c+"lock", start ts ,   //再加锁
38 		{primary.row, primary.col}); // The primary’s location.
39 		return T.Commit();  //进入commit阶段
40 	}
41 	bool Commit() {
42 		Write primary = writes [0];
43 		vector<Write> secondaries(writes .begin()+1, writes .end());
44 		if (!Prewrite(primary, primary)) return false;
45 		for (Write w : secondaries)
46 			if (!Prewrite(w, primary)) return false;
47
48 			int commit ts = oracle .GetTimestamp();
49
50 			// Commit primary first.
51 		Write p = primary;
52 		bigtable::Txn T = bigtable::StartRowTransaction(p.row);
53 		if (!T.Read(p.row, p.col+"lock", [start ts , start ts ]))
54 			return false; // aborted while working
55 		T.Write(p.row, p.col+"write", commit ts,   //写入提交信息
56		start ts ); // Pointer to data written at start ts .
57		T.Erase(p.row, p.col+"lock", commit ts);  //成功提交,修改锁信息
58 		if (!T.Commit()) return false; // commit point
59
60 		// Second phase: write out write records for secondary cells.
61 		for (Write w : secondaries) {
62 			bigtable::Write(w.row, w.col+"write", commit ts, start ts );
63 			bigtable::Erase(w.row, w.col+"lock", commit ts);
64 		}
65 		return true;
66 	}
67 } // class Transaction

如果没有cell发生冲突,事务可以提交并执行到第二阶段。在第二阶段的开始,客户端从 oracle 获取提交时间戳(48行)。然后,在每个cell(从 primary 开始),客户端释放它的锁,替换锁为一个写记录以让其他读事务知晓。读过程中看到写记录就可以确定它所在时间戳下的新数据已经完成了提交,并可以用它的时间戳作为指针找到提交的真实数据。一旦 primary 的写记录可见了(58行),其他读事务就会知晓新数据已写入,所以事务必须提交。

一个 Get 操作第一步是在时间戳范围 [0,开始时间戳)内检查有没有锁,这个范围是在此次事务快照所有可见的时间戳(12行)。如果看到一个锁,表示另一个事务在并发的写这个cell,所以读事务必须等待直到此锁释放。如果没有锁出现,Get 操作在时间戳范围内读取最近的写记录(19行)然后返回它的时间戳对应的数据项(22行)。

crash处理

由于客户端随时可能故障,导致了事务处理的复杂度(Bigtable 可保证 tablet 服务器故障不影响系统,因为 Bigtable 确保写锁持久存在)。如果一个客户端在一个事务被提交时发生故障,锁将被遗弃。Percolator 必须清理这些锁,否则他们将导致将来的事务被非预期的挂起。Percolator 用一个懒惰的途径来实现清理:当一个事务A遭遇一个被事务B遗弃的锁,A可以确定B遭遇故障,并清除它的锁。

然而希望事务 A 很准确的判断出事务 B 失败是十分困难的;可能发生这样的情况,A准备清理B的事务,而事实上B并未故障还在尝试提交事务,我们必须想办法避免。Percolator 在每个事务中会对任意的提交或者清理操作指定一个 cell 作为同步点。这个cell的锁被称之为 primary锁 。A和B在哪个锁是 primary 上达成一致( primary锁的位置被写入所有 cell 的锁中)。执行一个清理或提交操作都需要修改 primary锁 ;这个修改操作会在一个 Bigtable 行事务之下执行,所以只有一个操作可以成功。特别的,在B提交之前,它必须检查它依然拥有primary锁,提交时会将它替换为一个写记录。在A删除B的锁之前,A也必须检查 primary锁 来保证B没有提交;如果 primary锁 依然存在它就能安全的删除B的锁。在这种情况下,关于一个事务的多行数据的冲突就被简化为对单一数据行的冲突检查。

如果一个客户端在第二阶段提交时崩溃,一个事务将错过提交点(它已经写过至少一个写记录),而且出现未解决的锁。我们必须对这种事务执行roll-forward。当其他事务遭遇了这个因为故障而被遗弃的锁时,它可以通过检查 primary锁 来区分这两种情况:如果 primary锁 已被替换为一个写记录,写入此锁的事务则必须提交,此锁必须被 roll forward(继续执行);否则它应该被回滚(因为我们总是先提交primary,所以如果primary没有提交我们能肯定回滚是安全的)。执行roll forward时,执行清理的事务也是将搁浅的锁替换为一个写记录。

primary锁上的清理操作是同步的,所以清理活跃客户端持有的锁是安全的;然而,由于回滚会迫使事务中止,因此会产生性能损失。所以,一个事务将不会轻易清理一个锁除非猜测这个锁属于一个僵死的 worker。Percolator使用简单的机制来确定另一个事务的活跃度。运行中的worker会写一个 token 到 Chubby 锁服务来指示他们属于本系统,token 会被其他 worker 视为一个代表活跃度的信号(当处理退出时 token 会被自动删除)。为了防止 worker 假死,附加的写入一个 wall time 到锁中;一个锁的 wall time 果太老,即使 token 有效也会被清理。有些操作运行很长时间才会提交,针对这种情况,在整个提交过程中 worker 会周期的更新 wall time。

时间戳

时间戳oracle是一个用严格的单调增序给外界分配时间戳的服务器。因为每个事务都需要调用oracle两次,这个服务必须有很好的可伸缩性。oracle会定期分配出一个时间戳范围,通过将范围中的最大值写入稳定的存储;范围确定后,oracle能在内存中原子递增来快速分配时间戳,查询时也不涉及磁盘I/O。如果oracle重启,将以稳定存储中的上次范围的最大值作为开始值。为了节省RPC消耗(会增加事务延迟)Percolator的worker会维持一个长连接RPC到oracle,低频率的、批量的获取时间戳。随着oracle负载的增加,worker可通过增加每次批处理返回的量来缓解。批处理有效的增强了时间戳oracle的可伸缩性而不影响其功能。我们oracle中单台机器每秒向外分配接近两百万的时间戳。

这里的缓存应该只是说时间戳服务器的,在长请求的批处理地获取时间戳应该也不会预取吧

事务协议使用严格增加的时间戳来保证 Get 在事务的开始时间戳之前返回所有已提交的写写操作。举个例子,如果一个事务R在时间戳TR执行读操作,事务W在时间戳TR执行提交,并且TW < TR;因为TW < TR所以oracle肯定是在TR之前或相同的批处理中给出TW;因此事务W的时间戳TW是在事务R的时间戳TR之前。我们知道R在收到TR之前不能执行读取操作,而W在它的提交时间戳TW之前必定完成了锁的写入;因此,上面的推理保证了W在R做任何读之前就写入了它所有的锁;事务R Get 要么看到已经完全提交的写记录,要么看到锁,在看到锁时R将阻塞直到锁被释放所以在任何情况下,W的写对R的 Get 都是可见的。

通知

在传统数据库中,事务可以让用户改变 table,同时维护了不变量,但是用户同时还需要一个方法来触发和运行事务。在 Percolator,用户编写的代码( observers )将因表的变化而触发,然后我们将所有 observers 放入一个可执行文件(也就是上面提到的 Percolator Worker),它会伴随每一个 tablet 服务器运行。每个 observers 向 Percolator 注册一个 function 和表的列字段,当数据被写到这些列时 Percolator 会调用此 function。

Percolator 应用的结构就是一系列的 observers;每个 observer 完成一个任务然后对相应 table 执行写操作,从而触发“下游”的observer 任务。在我们的索引系统中,一个 MapReduce 通过运行事务加载器将抓取的文档加载到 Percolator 中,事务加载器触发文档处理器事务为文档建立索引(解析、提取链接等),文档处理器事务触发更多后续的事务比如聚类分析。最后聚类分析反过来触发事务将改变的文档聚类数据导出到在线服务系统。

通知类似于数据库中的触发器或者事件,但是与数据库触发器不同,它们不能被用于维护数据库不变量。比如某个写操作触发了observer逻辑,写操作和observer将运行在各自的事务中,所以它们产生的写不是原子的。通知的目的是帮助构建一个增量计算,而不是帮助维护数据的完整性。

仅仅是一个增量计算,不能提供数据库的一致性

Percolator 提供一个保证:对一个被观察列的每次改变,至多一个observer的事务被提交。反之则不然:对一个被观察列的多次写入可能导致相应的observer只被调用一次。我们称这个特性为消息重叠,它可以避免不必要的重复计算。例如,对 http://google.com 页面来说,周期性的通知其变化就够了,不需要每当一个新链接指向它时就触发一次。

为了给通知机制提供这些语义,每个被监测列旁边都有一个 acknowledgment 列,供每个观察者使用,它包含最近一次 observer 启动的开始时间戳。被监测列被写入时,Percolator 启动一个事务来处理通知。事务读取被监测列和它对应的 acknowledgment 列。如果被监测列发生写操作的时间戳在 acknowledgment 列的最近时间戳之后,我们就运行观察者逻辑,并设置 acknowledgment 列为新的开始时间戳。否则,说明已经有观察者被运行了,所以我们不重复运行它。请注意,如果 Percolator 不小心为一个特定的通知同时启动了两个事务,它们都会看到脏通知并运行 observer,但其中一个会中止,因为它们会在确认列上发生冲突。我们保证对每个通知至多一个observer 可以提交。

Percolator 提供的是一个在增量计算层面类似数据一致性的事务行为,也就是说下层的 BigTable 依然只有他原本的性质,这里的 observer 间由于有了这种事务的特性,就不会发生计算冲突,计算的冲突会导致过量的数据计算从而拖累整个系统的效率。

为了实现通知机制,Percolator 需要高效找到需要被运行的 observers 的脏cell。这个搜索是复杂的因为通知往往是稀疏的:我们表有万亿的cell,但是可能只会有百万个通知。而且,观察者的代码运行在一大批分布式的跨大量机器的客户端进程上,这意味着脏cell搜索也必须是分布式的。

为了识别脏cell,Percolator 维护一个特殊的 notify Bigtable列,其中包含每个脏cell的条目。当一个事务对被监测cell执行写操作时,它同时设置对应的notify cell。worker对notify列执行一个分布式扫描来找到脏cell。在观察者被触发并且事务提交成功后,我们会删除对应的notify cell。因为notify列只是一个 Bigtable 列,不是个 Percolator 列,它没有事务型属性,只是作为一个暗示,配合acknowledgment 列来帮助扫描器确定是否运行观察者。

为了使扫描高效,Percolator 存储notify列为一个独立的 Bigtable locality group,所以扫描时仅需读取百万个脏cell,而不是万亿行个cell。每个 Percolator的worker 指定几个线程负责扫描。对每个线程,worker 为其分配 table 的一部分作为扫描范围,首先挑选一个随机的 tablet,然后挑选一个随机的key,然后从那个位置开始扫描。因为每个 worker 都在扫描table中的一个随机范围,我们担心两个worker会扫描到同一行、并发的运行observers。虽然由于通知的事务本性,这种行为不会导致数据准确性问题,但这是不高效的。为了避免这样,每个worker在扫描某行之前需要从一个轻量级锁服务中申请锁。这个锁服务只是咨询性质、并不严格,所以不需要持久化,因此非常可伸缩。

这个随机扫描机制还需要一个附加优化:当它第一次被部署时,我们注意到扫描线程会倾向于在表的几个区域聚集在一起,严重影响了扫描的并行效果。这现象通常可以在公交系统中看到,被称为“bus凝结”效应。某一个bus可能因为某种原因导致速度减慢(比如在某个站上车的乘客太多)。因为每个车站的乘客数量会随时间增长,导致它到达后续车站的时间延后,于是越来越慢。同时,在这个慢bus后面的bus的速度则会提高,因为它在每个站装载的乘客数量减少了。最终的现象就是多辆公交会同时到达后续的车站。我们扫描线程行为与此类似:一个线程由于运行observer减慢,而它之后的线程快速的跳过已被处理的脏cell,逐渐与领头的线程聚集在一起,但是却没能超过领头的线程因为线程凝结导致tablet服务器繁忙过载。为了解决这个问题,我们做了一个公交系统不能实现的优化:当一个扫描线程发现了它和其他的线程在扫描相同的行,它在table中重新选择一个随机定位继续扫描。这就好比在公交系统中,公交车(扫描线程)为避免凝结而时空穿梭到一个随机的车站(table中的某个位置)。

最后经验让我们采取非常轻量级、弱事务语义、甚至牺牲了部分一致性的通知机制。我们通过只写到Bigtable的 "notify "列来实现这种弱通知。为了保留Percolator其他部分的事务性语义,我们将这些弱通知限制在一种特殊类型的列上,不能写入,只能通知。 较弱的语义也意味着多个观察者可能会因为一个弱通知而运行并提交(尽管系统会尽量减少这种情况的发生)。这已经成为管理冲突的一个重要特征;如果一个观察者经常在一个热点上发生冲突,那么把它分成两个观察者,由热点上的非事务性通知来连接,往往会有帮助。

讨论

相对于基于MapReduce的系统,Percolator的低效率之一是每个工作单元发送的RPC数量。MapReduce只需对GFS进行一次大规模的读取,就能获得10到100个网页的所有数据,而Percolator则需要执行大约50次单独的Bigtable操作来处理一个文档。

导致RPC太多的其中一个因素发生在commit期间。当写入一个锁时就需要两个Bigtable的RPC:一个为查询冲突锁或写记录,另一个来写入新锁。为减少负载,我们修改了Bigtable的API将两个RPC合并,将读-修改-写放在一个RPC中。按这个方法,我们会尽量将可以打包批处理的RPC调用都合并以减少RPC总数。比如将锁操作延缓几秒钟,使它们尽可能的聚集以被批处理。因为锁是并行获取的,所以每个事务仅仅增加了几秒的延迟;这附加的延迟可以用更强的并行来弥补。批处理增大了事务时窗,导致冲突可能性提高,但是通过有效的事务、通知机制,我们的环境中竞争并不强烈,所以不成问题。

从table读取时我们也利用了批处理:每个读取操作都被延缓,从而有一定几率让相同tablet的读取操作打包成批处理(类似buffer的原理)。这就延迟了每次读取,可能会大大增加交易延迟。然而,最后一项优化可以减轻这种影响:预读取。预读取利用了这样一个事实:在同一行中读取两个或多个值与读取一个值的成本基本相同。在这两种情况下,Bigtable必须从文件系统中读取整个SSTable块并解压。Percolator试图预测,每次读取一个列的时候,在事务的后期,一行中的其他列会被读取。这种预测是基于过去的行为而做出的。预读取与已经读过的项目的缓存相结合,将系统对大表的读取次数减少了10倍。

在之前的Percolator的实现中,所有API调用都会阻塞,然后通过调高每台机器的线程数量来支持高并发、提升CPU利用率。相比异步、事件驱动等方案,这种thread—per-request的同步模型的代码更易编写。异步方案需要花费大量精力维护上下文状态,导致应用开发更加困难。根据我们的实际经验,thread—per-request的同步模型还是可圈可点的,它的应用代码简单,多核机器CPU利用率也不错,同步调用下的堆栈跟踪也很方便调试,所遭遇的资源竞争也没有想象中那么恐怖。不过它的最大缺点是可伸缩性问题,linux内核、Google的各种基础设施在遭遇很高的线程数时往往导致瓶颈。

评估

Percolator性能空间处于MapReduce和DBMS之间。例如,由于 Percolator是一个分布式系统,它在处理固定数量的数据时使用的资源比传统的DBMS要多得多;这是它的可扩展性的代价。与MapReduce相比,Percolator处理数据的延迟要低得多。滞后性要低得多,但同样是以支持随机查询所需的额外资源为代价的。这些都是很难量化的工程权衡,效率损失多少才算多?为了通过购买更多的机器来无休止地增加容量,效率损失多少才算过分?机器就能无休止地增加容量,这样的效率损失有多大?或者如何权衡分层系统所带来的开发时间的减少?分层系统所提供的开发时间与相应的效率下降之间如何权衡?效益的相应下降?

每个系统都有自己最适用的场景,用一个过于普适性的测试环境并不能说明一切,对于Percolator来说,虽然消耗了更多的资源,但是在保证增量计算的效率方面有了一些切实的提升和可能性,也许更多地可能性确实要寄托在新硬件和网络优化上,事务的复杂性远超过我们的想象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值