TL;DR
我测试了这些情况:
-
用 a 增加一个值transaction
call:
ref.transaction(function(value) {
return (value || 0) + 1;
});
-
用新的增加一个值increment
操作员:
ref.set(admin.database.ServerValue.increment(1));
增量更快这一事实并不令人意外,但是……增量是多少?
Results:
- 通过事务,我每秒可以增加大约 60-70 次的值。
- 随着
increment
操作员,我每秒能够增加大约 200-300 次的值。
我如何进行测试并获得这些数字
我在我的 2016 型号 MacBook pro 上运行了测试,并将上述内容包装在一个简单的 Node.js 脚本中,该脚本使用客户端节点SDK。操作的包装脚本也非常基本:
timer = setInterval(function() {
... the increment or transaction from above ...
}, 100);
setTimeout(function() {
clearInterval(timer);
process.exit(1);
}, 60000)
因此:每秒增加该值 10 次,1 分钟后停止这样做。然后我用这个脚本生成了这个过程的实例:
for instance in {1..10}
do
node increment.js &
done
所以这将运行 10 个并行进程increment
运算符,每个值每秒增加 10 次,总共每秒增加 100 次。然后,我更改实例数,直到“每秒增量”达到峰值。
然后我写了一个小jsbin 上的脚本监听该值,并通过简单的低通移动平均滤波器确定每秒的增量数。我在这里遇到了一些麻烦,所以不确定计算是否完全正确。鉴于我的测试结果,它们已经足够接近了,但如果有人想写一个更好的观察者:请成为我的客人。 :)
测试注意事项:
我不断增加进程数量,直到“每秒增量”似乎达到最大值,但我注意到这与我的笔记本电脑风扇全速运转同时发生。所以很可能我没有找到服务器端操作的真正最大吞吐量,而是我的测试环境和服务器的结合。因此,当您尝试重现此测试时,很有可能(事实上很可能)您可能会得到不同的结果,尽管当然increment
吞吐量应始终显着高于transaction
。无论您得到什么结果:请分享它们。 :)
我使用了客户端 Node.js SDK,因为它最容易工作。使用不同的 SDK 可能会产生稍微不同的结果,尽管我预计主要的 SDK(iOS、Android 和 Web)与我得到的结果非常接近。
两个不同的队友立即问我是否会在单个节点上运行它,或者是否并行增加多个值。并行增加多个值可能会显示是否存在系统范围的吞吐量瓶颈或者是否是特定于节点的(我期望如此)。
正如已经说过的:我的测试工具没什么特别的,但我的 jsbin 观察者代码特别可疑。如果有人想在相同的数据上编写一个更好的观察者,那么值得赞扬。
交易和增量运算符如何在幕后工作
了解之间的性能差异transaction
and increment
了解这些操作的幕后工作原理确实很有帮助。对于 Firebase 实时数据库,“幕后”是指通过 Web Socket 连接在客户端和服务器之间发送的命令和响应。
交易在 Firebase 中使用比较和设置方法。每当我们像上面那样启动事务时,客户端都会猜测节点的当前值。如果之前从未见过该节点,则猜测是null
。它用这个猜测调用我们的事务处理程序,然后我们的代码返回新值。客户端将猜测和新值发送到服务器,服务器执行比较和设置操作:如果猜测正确,则设置新值。如果猜测错误,服务器会拒绝该操作,并将实际的当前值返回给客户端。
在完美的情况下,最初的猜测是正确的,并且该值立即写入服务器上的磁盘(然后发送给所有侦听器)。在流程图中,如下所示:
Client Server
+ +
transaction() | |
| |
null | |
+---<-----+ |
| | |
+--->-----+ |
1 | (null, 1) |
+--------->---------+
| |
+---------<---------+
| (ack, 3) |
| |
v v
但是如果节点在服务器上已经有一个值,它会拒绝写入,发回实际值,然后客户端再次尝试:
Client Server
+ +
transaction() | |
| |
null | |
+---<-----+ |
| | |
+--->-----+ |
1 | |
| (null, 1) |
+--------->---------+
| |
+---------<---------+
| (nack, 2) |
| |
2 | |
+---<-----+ |
| | |
+--->-----+ |
3 | (2, 3) |
+--------->---------+
| |
+---------<---------+
| (ack, 3) |
| |
| |
v v
这还不错,多了一次往返。即使 Firebase 使用悲观锁定,它也需要往返,所以我们不会丢失任何东西。
如果多个客户端同时修改相同的值,就会出现问题。这引入了所谓的节点争用,如下所示:
Client Server Client
+ + +
transaction() | | |
| | | transaction()
null | | |
+---<-----+ | | null
| | | +--->----+
+--->-----+ | | |
1 | | +---<----+
| (null, 1) | | 1
+--------->---------+ (null, 1) |
| |---------<---------+
+---------<---------+ |
| (nack, 2) |--------->---------+
| | (nack, 2) |
2 | | |
+---<-----+ | | 2
| | | |--->----+
+--->-----+ | | |
3 | (2, 3) | |---<----+
+--------->---------+ | 3
| | |
+---------<---------+ |
| (ack, 3) | (2, 3) |
| |---------<---------+
| | |
| |--------->---------+
| | (nack, 3) |
| | | 3
| | |--->----+
| | | |
| | |---<----+
| | | 4
| | (3, 4) |
| |---------<---------+
| | |
| |--------->---------+
| | (ack, 4) |
| | |
v v v
TODO:更新上面的图表,以便服务器上的操作不会重叠。
第二个客户端必须再次重试其操作,因为服务器端值在第一次和第二次尝试之间已被修改。我们向此位置写入的客户端越多,您看到重试的可能性就越大。 Firebase 客户端会自动执行这些重试,但在多次重试后,它将放弃并引发Error: maxretry
申请的例外。
这就是我每秒只能增加计数器约 60-70 次的原因:如果写入次数超过此数量,则节点上的争用过多。
An 增量操作本质上是原子的。您告诉数据库:无论当前值是什么,都将其设为x
更高。这意味着客户端永远不必知道节点的当前值,因此它也不会猜错。它只是告诉服务器要做什么。
使用时,我们的多个客户端流程图如下所示increment
:
Client Server Client
+ + +
increment(1) | | |
| | | increment(1)
| (increment, 1) | |
+--------->---------+ (increment, 1) |
| |---------<---------+
+---------<---------+ |
| (ack, 2) |--------->---------+
| | (ack, 3) |
| | |
v v v
仅最后两个流程图的长度就足以解释原因increment
在这种情况下要快得多:increment
操作就是为此而进行的,因此有线协议更能代表我们想要实现的目标。仅在我的简单测试中,这种简单性就会导致 3 到 4 倍的性能差异,在生产场景中甚至可能更大。
当然,事务仍然有用,因为除了递增/递减之外,还有更多的原子操作。