关于KeyDB 和 Redis 的性能测试与分析

据传言,KeyDB的时延比redis小,吞吐量比redis高,那么我就想测测keyDB;

首先对于KeyDB,有几点其实我没太弄清

  1. KeyDB的并发控制方案: 对比了很多资料,我推测的模式是这样的:KeyDB整体使用MVCC机制,在多线程并发写时,使用key-level-lock给每个k-v结构加锁;而这个key-value-lock就在redisObject结构之中,MVCC机制的核心trx_id,在这里应该是用时间戳代替了,也可以在redisObject中;

    由于网上有很多说不全面的局部资料,导致我也只能部分听取后自己尝试梳理出来;当然更合理的办法是直接看源码,但是这种架构层面的逻辑从源码里感觉会很抽象

  2. redis我们都知道是单线程执行指令,单线程仍然快的原因之一是,限制速度的瓶颈在于内存大小和网络带宽 ------ 八股文是这样说的;按照这种思路,KeyDB想要基于redis提速应当首先改善内存布局或者优化网络IO,而不是通过引入多线程就能够有效降低时延。

    关于这个想法其实第一句话就不太能站得住脚,内存大小和网络带宽本身确实会影响redis的响应时间,但是就时延方面来说 影响应该是很有限的

    内存占用太多可能导致fork时主线程被占用,也可能由于key太多导致在全局散列表上查询变慢,还可能由于回收的k-v太多导致一直在以某个速度回收...但是影响都不足以称为瓶颈,最主要很少会让redis内存占用太高吧,据说为了防止写时复制产生的备份太多,部署时一般会预留50%左右的空余内存。

    所以在正常情况下的指令,其时延和CPU、内存、带宽都没关系

    这一点在这位大哥的压测说明中也体现了

    从一次压测看redis的瓶颈问题 - 知乎 (zhihu.com)

    另外借用官方的一句话: 翻译:从访问数据结构和生成回复的角度来看,处理每个命令的成本非常低廉,但从执行套接字 I/O 的角度来看,成本却非常高。这涉及调用 sendrecv 系统调用,这意味着需要从用户态切换到内核态。这样的上下文切换带来了巨大的速度损失

    所以影响时延的关键在于:是否使用了pipelining,来减少redis的系统调用。


性能测试

使用单台设备进行性能测试有好多坑,都会导致最终的结论和想调查的内容无关,在此简单记录一些犯过的错误

  1. 刚开始用python脚本写多线程测试,结果python有一套GLI机制,想实现并发似乎需要多进程,有点难搞

    深入理解Python中的GIL(全局解释器锁)。 - 知乎 (zhihu.com)

  2. 之后在Java中用16个线程分别循环六千多次,每次循环调用jedis的send方法,但是切记16个线程需要有各自的连接,并且建立连接的工作是提前做好的,不计入计时范围

  3. 后面怀疑调用jedis.send()是同步阻塞的,有没有可能整个测试的时间瓶颈在send后等待响应?所以后续使用redisson的异步调用方案;但是无论是同步还是异步,时间都差不多,原因在于:即使是转化成异步,在一条redis连接上也会被同步 ------ redis的RESP协议本身实现很简单,客户端和服务器的通信也是交互式的,一条收到返回确认后再发一条,如果是异步的话还需要实现滑动窗口,通信协议里也要带id,但实际上RESP中都没有

    【高阶篇】3.1 Redis协议(RESP )详解_redis resp-CSDN博客

    但是在使用异步请求时,观察到这样一个现象:所有请求全部发出去只用了0.3s(验证了之前一个连接上的请求会被串行),所有请求都收到响应的时间是3s(和同步一致),所以推测的结论是这样的:由于通信协议限制,一条连接上只能一个一个发,这才是本次测试真正的瓶颈;如果有一种方法能模拟出多台设备 - 多个连接 - 并发请求,这才是真正的压测;

  4. 后续使用redis的pipelining,在一条连接的一次请求中发起二百个指令;这两百个指令到达redis后,被一个IO线程读取,就和其他连接的指令一起被主线程执行了;所以感觉可以模拟出并发的场景(存在干扰项:使用pipelining减少了context-switched的次数,而这个是时延的主要瓶颈)

测试代码:

JAVA 复制代码
package KeyDB_Redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class PipeLineTest {
    private static final String REDIS_HOST = "192.168.56.10";
    private static final int REDIS_PORT = 6379;
    private static final int NUM_THREADS = 16;
    private static final int NUM_ITEMS = 1000000;
    static List<Jedis> jedisPool = new ArrayList<>();

    static {
        for (int i = 0; i < NUM_THREADS; i++) {
            jedisPool.add(new Jedis(REDIS_HOST, REDIS_PORT));
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
        Instant startTime = Instant.now();

        int itemsPerThread = NUM_ITEMS / NUM_THREADS;

        for (int i = 0; i < NUM_THREADS; i++) {
            int startIndex = i * itemsPerThread;
            int endIndex = startIndex + itemsPerThread;
            int finalI = i;
            executor.submit(() -> insertData(startIndex, endIndex, finalI));
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
            // Wait for all threads to finish
        }

        Instant endTime = Instant.now();
        System.out.println("插入完毕 " + Duration.between(startTime, endTime).toMillis() + " 毫秒");
    }

    private static void insertData(int start, int end, int index) {
        Jedis jedis = jedisPool.get(index);
        for (int i = start; i < end; i+=200) {
            Pipeline pipeline = jedis.pipelined();
            try {
                for (int ii = 0; ii < 200; ii++) {
                    pipeline.get("testKey");
                    // pipeline.set("test" + i * 200 + ii,i + "+val");


                }
                List<Object> responses = pipeline.syncAndReturnAll();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

测试结果: 很尴尬的是:redis是1135ms,keyDB是1554ms;其他指令的测试也是keyDB稍慢

二者在同样有1000000条k-v时,内存占用分别是:

  • Keydb

  • redis

    内存占用keyDB更大我是可以理解的,毕竟有MVCC的多个版本共存,而且在数据结构上也很难比redis更优化,内存分配上也没有变,所以一定会更大;

    但是KeyDB的时延比redis更长,这个我觉得问题还是在我测试方法上,但一时也难以发现;希望看到的大佬可以指点一下

    完整测试代码在github中:github.com/13038032626...

相关推荐
蓝染-惣右介15 分钟前
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
java·设计模式
KELLENSHAW39 分钟前
MySQL45讲 第三十七讲 什么时候会使用内部临时表?——阅读总结
数据库·mysql
秋恬意1 小时前
IBatis和MyBatis在细节上的不同有哪些
java·mybatis
齐 飞1 小时前
BeanFactory和FactoryBean
java·sprint
大霞上仙2 小时前
lxml 解析xml\html
java·服务器·网络
Xiaoweidumpb2 小时前
tomcat temp临时文件不清空,占用硬盘,jdk字体内存泄漏
java·tomcat
小刘鸭!2 小时前
Hbase的特点、特性
大数据·数据库·hbase
AI人H哥会Java2 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱2 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django