为什么我们不发布 Redis 与其他数据库的对比基准测试
Redis 的速度可能是吸引新用户的一个卖点,因此顺应比较式“广告”的潮流,在 Redis.io 上放几份对比似乎是合乎逻辑的。然而这样做存在两个问题。一个是目标问题:我并不想说服开发者去采用 Redis,我们只是尽力提供一个合适的产品,如果人们能用它完成工作,我们就很开心,我的营销愿望到此为止。还有更重要的一点:以公平的方式比较不同的系统几乎总是不可能的。
当你比较两个数据库时,要得到公平的数据,它们需要在*很多方面*保持一致:数据模型、确切的持久化保证、数据复制安全性、分区期间的可用性等等:一个系统得分低于另一个系统,往往是因为它为了提供那些不那么“引人注目”但依然非常重要的特性而牺牲了速度。此外,除非不同的数据库系统使用完全相同的协议,否则测试套件本身也是一个复杂的问题:仅客户端库的差异就可能导致巨大的差别。
然而也有人持不同意见,认为无论如何比较不同数据库系统的速度都是个好主意。例如,昨天这里发布了一份 Redis 与 AerospikeDB 的基准测试:http://lynnlangit.com/2015/01/28/lessons-learned-benchmarking-nosql-on-the-aws-cloud-aerospikedb-and-redis/。
我将用这份基准测试来说明基准测试是多么容易产生误导。在该基准测试中,出于某种奇怪的原因使用了巨大的 EC2 实例,这些实例配备了 244 GB 内存(!)。那是 R3.8xlarge 实例。在我的测试中,我将使用更贴近现实的 m3.medium 实例。
使用这种庞然大物般的实例,Redis 在单节点情况下得分是每秒可提供 12.8 万次操作。我的 EC2 实例要受限得多,从另一台 EC2 实例使用 Redis benchmark 进行测试,在不使用 pipelining(流水线)的情况下,使用同样 100 字节的数据大小,我得到的是 3.2 万次操作/秒,所以在单进程情况下,我的实例大约慢了 4 倍。
让我们通过 Redis INFO 命令来看看在此基准测试期间系统是如何使用 CPU 的:
# CPU
used_cpu_sys:181.78
used_cpu_user:205.05
used_cpu_sys_children:0.12
used_cpu_user_children:0.87
127.0.0.1:6379> info cpu……10 秒测试后……
# CPU
used_cpu_sys:184.52
used_cpu_user:206.42
used_cpu_sys_children:0.12
used_cpu_user_children:0.87Redis 花费了约 3 秒的系统时间,而在用户态仅花费了约 1.5 秒。这里发生的情况是,对于每个请求,大部分工作是执行 read() 和 write() 调用。此外,由于每个客户端都是一问一答的工作负载,每个客户端的每次请求我们都要付出一次完整的 RTT。
现在让我们看看如果改用 pipelining 会发生什么,这是一个为 Redis 用户所熟知并被广泛利用的特性,因为它是最大化服务器利用率的唯一方式,而且在应用中通常有不少地方可以在同一时间执行多个操作。
当流水线长度为 32 个操作时,数据发生了巨大变化。我的这个小型实例使用单核就能达到 25 万次操作/秒,这相当于上述基准测试中使用 32 个(每个都更快的)核心所取得的*顶级*成绩的 25%。
让我们看看 CPU 时间:
# CPU
used_cpu_sys:189.16
used_cpu_user:216.46
used_cpu_sys_children:0.12
used_cpu_user_children:0.87
127.0.0.1:6379> info cpu……10 秒测试后……
# CPU
used_cpu_sys:190.60
used_cpu_user:224.92
used_cpu_sys_children:0.12
used_cpu_user_children:0.87这一次我们真正地在使用数据库引擎和 CPU 来处理查询,而不是把大量时间浪费在上下文切换上。我们使用了约 1.5 秒的系统时间,以及 8.46 秒的 Redis 进程自身时间。
在流水线中使用更小的数值会得到介于两者之间的结果。流水线长度为 4 时 = 10 万次操作/秒(换算到基准测试中使用的大型实例上大约相当于约 40 万次操作/秒),流水线长度为 8 时 = 18 万次操作/秒,依此类推。
所以,基本上以这种方式对 Redis 和 AerospikeDB 进行基准测试会得到惊人相似的结果并非巧合。或多或少,你测试的不是数据库,而是网络协议栈和内核。如果数据库能够仅通过一次 read 和一次 write 系统调用来处理查询,而没有其他巨大的开销,就会得到这样的结果,而在这里,处理实际查询的时间很短,因为我们讨论的是能完全放入内存的数据(顺便提一下,在 Redis 中 10M 个 100k 的键只会占用那些实例所分配内存的一小部分)。
然而关于这一点还有更多要说的。我们能执行哪些操作呢?仅用 GET/SET 来测试 Redis,就像通过检查雨天清洁后视镜的能力来测试一辆法拉利。
Redis 架构的一个基本特点是,截然不同的操作具有相似的开销,那么我们那个向排行榜发布已完成游戏分数的庞大 Facebook 游戏又会怎样呢?
同一个单进程在执行如下查询时可以做到 11 万次操作/秒:ZADD myscores <random-int> <random-int>。
但是让我们要求更高一点,如果用 HyperLogLog(基数估计算法)来估算基数,同时用两个 redis-benchmark 进程向同一个 HyperLogLog 中添加新元素并读取当前的估算值,会怎么样呢?集合大小同样是 1000 万。在这次测试中,我启动了一个执行 PFADD 并添加集合中随机元素的基准测试,以及另一个同时在同一个 HyperLogLog 上执行 PFCOUNT 的基准测试。两者同时都达到了 25 万次操作/秒,单个 Redis 进程总计每秒 50 万次操作。
在 Redis 中执行复杂操作类似于 pipelining。你希望每次 read/write 能*做更多*的事情,否则你的性能就会被 I/O 所主导。
好了,这里有几点有用的总结。1)GET/SET 基准测试不是比较不同数据库系统的好方法。2)更好的性能比较方式是按用例来比较。也就是说,对于一个给定的具体用例,使用不同的数据模型、模式、查询、策略,用两种不同的数据库系统处理相同应用的相同流量,各自需要多少实例?3)要用大多数人实际会使用的实例类型进行测试,巨大的实例类型可能会掩盖某些数据库系统的低效,而且无论如何也不是大多数人会使用的类型。
我们将继续为 Redis 优化速度,并将继续避免发布比较性的基准测试。
[感谢 Amazon AWS 为我提供 EC2 的免费访问]
随机一篇博客