• 作者:老汪软件技巧
  • 发表时间:2024-10-03 10:01
  • 浏览量:

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

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

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

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

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

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

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

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

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

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

另外借用官方的一句话:

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

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

性能测试

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

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

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

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

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

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

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

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

线程数设置太少了;

测试代码:


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 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 responses = pipeline.syncAndReturnAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

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

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