`
blueswind8306
  • 浏览: 127251 次
  • 来自: ...
社区版块
存档分类
最新评论

redis cluster百万QPS的挑战

阅读更多
最近在做redis cluster性能测试过程中,发现当集群吞吐量到达一定程度后(4台12core的redis服务器,80wQPS左右),集群整体性能不能线性增长。也就是说,通过加机器不能提升集群的整体吞吐。以下是详细记录了一下这个case的排查并最终解决的过程。

先来看一个压测图:

上图中每一条线代表一个起压端进程的压测QPS(一台起压机上开4个起压端),可以看到随着起压机的增多,每个起压机的QPS却在下滑。而此时redis的CPU、内存、网卡都还没有跑满。另外尝试增多redis服务器,发现增多一倍服务器后,每台服务器上每个redis实例的CPU大幅降低。总的QPS却基本没有变化。

由于起压机是无状态的,只是简单的mock随机请求到redis cluster,并且请求的路由策略会在起压机内部调jedis的过程中做好,直接请求到目标redis实例,并且在起压过程中没有槽迁移导致的ASK/MOVED动作。所以排除了起压机的影响,只能怀疑是redis cluster服务端导致的问题。

一开始怀疑是服务端连接数过多的问题,看了一下,服务端连接数在1w以下,每个客户端与每个redis实例的连接数也就在几十个长连接,远没有到达系统瓶颈。

难道是CPU的问题?但是top看每个核的CPU也就打到60%左右,而且如果是CPU到瓶颈了,通过增加redis服务器应该可以线性伸缩才对,但并没有。

再仔细观察top信息,发现每个核的CPU都有一定的si开销,也就是软中断,中断开销在10%以上:


怀疑是软中断导致抢占了Redis的CPU资源,软中断耗在哪里了可以用以下命令看到:
watch -d -n 1 'cat /proc/softirqs'


发现中断开销主要集中在网卡中断上,也就是eth0-TxRx上面。解决思路是将网卡中断和Redis进程使用的CPU隔离开,这样避免网卡中断抢占CPU拖慢Redis服务。另外,由于Redis是单线程模型,在多核服务器上,可以将多个Redis实例绑定到不同的CPU上,使每个Redis进程独占一个CPU。

先将网卡的中断号绑定到特定CPU上:
# 如果开了irqbalance服务,需要先停止服务,否则后续的绑定将无效:
service irqbalance stop

# 将网卡中断号绑定到CPU0-CPU2上:
echo "1" > /proc/irq/78/smp_affinity
echo "1" > /proc/irq/79/smp_affinity
echo "2" > /proc/irq/80/smp_affinity
echo "2" > /proc/irq/81/smp_affinity
echo "2" > /proc/irq/82/smp_affinity
echo "4" > /proc/irq/83/smp_affinity
echo "4" > /proc/irq/84/smp_affinity
echo "4" > /proc/irq/85/smp_affinity


再将Redis master实例绑定到CPU3-CPU10上。这里没有绑定Redis slave,是因为观察到slave占用的CPU极低,每个slave占用大概一个核的5%以下。当然如果CPU核数够多的话也可以考虑绑slave到其它多余的核。
# 绑定master的pid到CPU3-CPU10上:
taskset -cp 3 [pid1]
taskset -cp 4 [pid2]
taskset -cp 5 [pid3]
...


绑定好后再压,看top会发现,前三个CPU主要耗在si上(网卡中断),后8个CPU耗在us/sy上(redis master服务):


至此,解决了单机CPU的问题,但是集群不能线性增长的问题还是解释不通。因为即使单台redis服务器的软中断导致单机性能受影响,在加Redis机器时总吞吐也不应该上不去呀。所以,开始怀疑redis cluster的通讯总线导致线性增长受阻。

每个redis cluster实例都会开一个集群通讯总线端口,并且redis cluster会用gossip协议,每隔一秒钟将集群一部分节点的信息发送给某个实例的总线端口。利用这种传播方式,可以将集群节点的变更以无中心的方式传播到整个集群。这里关键是总线传播的数据量,数据量的大小取决于PING消息携带的gossip数组的长度和大小,这个长度就是具体要捎带多少个其它节点的信息:
/** cluster.c的clusterSendPing函数 **/

/* Send a PING or PONG packet to the specified node, making sure to add enough
 * gossip informations. */
void clusterSendPing(clusterLink *link, int type) {
    unsigned char *buf;
    clusterMsg *hdr;
    int gossipcount = 0; /* Number of gossip sections added so far. */
    int wanted; /* Number of gossip sections we want to append if possible. */
    int totlen; /* Total packet length. */
    /* freshnodes is the max number of nodes we can hope to append at all:
     * nodes available minus two (ourself and the node we are sending the
     * message to). However practically there may be less valid nodes since
     * nodes in handshake state, disconnected, are not considered. */
    int freshnodes = dictSize(server.cluster->nodes)-2;

    /* How many gossip sections we want to add? 1/10 of the number of nodes
     * and anyway at least 3. Why 1/10?
     *
     * If we have N masters, with N/10 entries, and we consider that in
     * node_timeout we exchange with each other node at least 4 packets
     * (we ping in the worst case in node_timeout/2 time, and we also
     * receive two pings from the host), we have a total of 8 packets
     * in the node_timeout*2 falure reports validity time. So we have
     * that, for a single PFAIL node, we can expect to receive the following
     * number of failure reports (in the specified window of time):
     *
     * PROB * GOSSIP_ENTRIES_PER_PACKET * TOTAL_PACKETS:
     *
     * PROB = probability of being featured in a single gossip entry,
     *        which is 1 / NUM_OF_NODES.
     * ENTRIES = 10.
     * TOTAL_PACKETS = 2 * 4 * NUM_OF_MASTERS.
     *
     * If we assume we have just masters (so num of nodes and num of masters
     * is the same), with 1/10 we always get over the majority, and specifically
     * 80% of the number of nodes, to account for many masters failing at the
     * same time.
     *
     * Since we have non-voting slaves that lower the probability of an entry
     * to feature our node, we set the number of entires per packet as
     * 10% of the total nodes we have. */
    wanted = floor(dictSize(server.cluster->nodes)/10);
    if (wanted < 3) wanted = 3;
    if (wanted > freshnodes) wanted = freshnodes;

    /* Compute the maxium totlen to allocate our buffer. We'll fix the totlen
     * later according to the number of gossip sections we really were able
     * to put inside the packet. */
    totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
    totlen += (sizeof(clusterMsgDataGossip)*wanted);
    /* Note: clusterBuildMessageHdr() expects the buffer to be always at least
     * sizeof(clusterMsg) or more. */
    if (totlen < (int)sizeof(clusterMsg)) totlen = sizeof(clusterMsg);
    buf = zcalloc(totlen);
    hdr = (clusterMsg*) buf;

    /* Populate the header. */
    if (link->node && type == CLUSTERMSG_TYPE_PING)
        link->node->ping_sent = mstime();
    clusterBuildMessageHdr(hdr,type);

    /* Populate the gossip fields */
    int maxiterations = wanted*3;
    while(freshnodes > 0 && gossipcount < wanted && maxiterations--) {
        dictEntry *de = dictGetRandomKey(server.cluster->nodes);
        clusterNode *this = dictGetVal(de);
        clusterMsgDataGossip *gossip;
        int j;

        /* Don't include this node: the whole packet header is about us
         * already, so we just gossip about other nodes. */
        if (this == myself) continue;

        /* Give a bias to FAIL/PFAIL nodes. */
        if (maxiterations > wanted*2 &&
            !(this->flags & (REDIS_NODE_PFAIL|REDIS_NODE_FAIL)))
            continue;

        /* In the gossip section don't include:
         * 1) Nodes in HANDSHAKE state.
         * 3) Nodes with the NOADDR flag set.
         * 4) Disconnected nodes if they don't have configured slots.
         */
        if (this->flags & (REDIS_NODE_HANDSHAKE|REDIS_NODE_NOADDR) ||
            (this->link == NULL && this->numslots == 0))
        {
            freshnodes--; /* Tecnically not correct, but saves CPU. */
            continue;
        }

        /* Check if we already added this node */
        for (j = 0; j < gossipcount; j++) {
            if (memcmp(hdr->data.ping.gossip[j].nodename,this->name,
                    REDIS_CLUSTER_NAMELEN) == 0) break;
        }
        if (j != gossipcount) continue;

        /* Add it */
        freshnodes--;
        gossip = &(hdr->data.ping.gossip[gossipcount]);
        memcpy(gossip->nodename,this->name,REDIS_CLUSTER_NAMELEN);
        gossip->ping_sent = htonl(this->ping_sent);
        gossip->pong_received = htonl(this->pong_received);
        memcpy(gossip->ip,this->ip,sizeof(this->ip));
        gossip->port = htons(this->port);
        gossip->flags = htons(this->flags);
        gossip->notused1 = 0;
        gossip->notused2 = 0;
        gossipcount++;
    }

    /* Ready to send... fix the totlen fiend and queue the message in the
     * output buffer. */
    totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
    totlen += (sizeof(clusterMsgDataGossip)*gossipcount);
    hdr->count = htons(gossipcount);
    hdr->totlen = htonl(totlen);
    clusterSendMessage(link,buf,totlen);
    zfree(buf);
}

以上函数定义了发送PING包的逻辑,重点是wanted变量的计算方式,也就是每次PING包内要携带多少个其他节点信息发送。可以看到,wanted最小3个,最大不超过集群总实例数-2个,一般情况是集群实例数的1/10个

再来看看每个gossip包的大小:
typedef struct {
    /* REDIS_CLUSTER_NAMELEN是常量40 */
    char nodename[REDIS_CLUSTER_NAMELEN];
    uint32_t ping_sent;
    uint32_t pong_received;
    char ip[REDIS_IP_STR_LEN];  /* IP address last time it was seen */
    uint16_t port;              /* port last time it was seen */
    uint16_t flags;             /* node->flags copy */
    uint16_t notused1;          /* Some room for future improvements. */
    uint32_t notused2;
} clusterMsgDataGossip;


看到这里就明白了,总线传播的数据量wanted是一个变量,会随着集群实例数的增多而增多。当集群节点数不多时不会造成影响,但随着节点数变多,每次wanted要携带的节点信息也就随之变多,会导致对网卡的消耗越来越大。我压测时每台Redis服务器8主8从共16个节点,四台服务器共64个节点,每个节点每秒钟PING包都要携带6个gossip包的信息(64/10)。如果扩大到十台,就是每个节点每秒钟PING包要携带16个gossip包的信息(160/10)。

解决了网卡中断问题后,Redis实例的CPU不会再被中断抢走,QPS基本可以线性增长。在6台12coreCPU的Redis服务器上,总QPS可以到达200w/s以上,单机QPS在30w以上。

ps:
1. 推荐一篇有关软中断的文章:http://huoding.com/2013/10/30/296

2. 给一个查询当前网卡中断所在的cpu核的脚本(只在CentOS系统的多队列网卡模式下测试通过):
netarr=(`grep eth /proc/interrupts | awk '{print $1}' |awk -F':' '{print $1}'`) && for netid in ${netarr[*]}; do cat /proc/irq/$netid/smp_affinity_list; done
  • 大小: 96.1 KB
  • 大小: 151.6 KB
  • 大小: 330 KB
分享到:
评论
5 楼 爱蜗牛的蝙蝠 2018-06-11  
blueswind8306 写道
箜eric 写道
请问,使用的压测工具是什么?


用的是jmeter做分布式压测,性能指标收集是通过每个jmeter客户端调用metrics做的,服务端有一套我们自己研发的一个后台系统存储metrics数据并展现



您好,如果用每个jmeter 客户端做压测,请求是否会发生move等操作? 我自己用jedis的benchmark 做了改动,改成集群压测发现,集群性能反而低于单机,代码如下:

public class GetSetClusterBenchmark {
  private static Collection<HostAndPort> hnp = HostAndPortUtil.getClusterServers();
  private static final int TOTAL_OPERATIONS = 600;

  public static void main(String[] args) throws UnknownHostException, IOException {
    Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>(hnp);
    JedisCluster jedis = new JedisCluster(jedisClusterNodes);

   // jedis.auth("foobared");


    long begin = Calendar.getInstance().getTimeInMillis();

    for (int n = 100; n <= TOTAL_OPERATIONS; n++) {
      String key = "foo" + n;
      jedis.set(key, "bar" + n);
      jedis.get(key);
    }

    long elapsed = Calendar.getInstance().getTimeInMillis() - begin;

    System.out.println(((1000 * 2 * TOTAL_OPERATIONS) / elapsed) + " ops");
  }
}

这是什么原因呢
4 楼 blueswind8306 2016-02-05  
箜eric 写道
blueswind8306 写道
箜eric 写道
请问,使用的压测工具是什么?


用的是jmeter做分布式压测,性能指标收集是通过每个jmeter客户端调用metrics做的,服务端有一套我们自己研发的一个后台系统存储metrics数据并展现

在麻烦问一下,你们这个redis cluster集群方案,是redis官方的那个redis cluster集群方案么


是的
3 楼 箜eric 2016-01-20  
blueswind8306 写道
箜eric 写道
请问,使用的压测工具是什么?


用的是jmeter做分布式压测,性能指标收集是通过每个jmeter客户端调用metrics做的,服务端有一套我们自己研发的一个后台系统存储metrics数据并展现

在麻烦问一下,你们这个redis cluster集群方案,是redis官方的那个redis cluster集群方案么
2 楼 blueswind8306 2016-01-14  
箜eric 写道
请问,使用的压测工具是什么?


用的是jmeter做分布式压测,性能指标收集是通过每个jmeter客户端调用metrics做的,服务端有一套我们自己研发的一个后台系统存储metrics数据并展现
1 楼 箜eric 2016-01-10  
请问,使用的压测工具是什么?

相关推荐

    一种高效的Redis Cluster的分布式缓存系统.pdf

    在性能测试方面,本研究使用官方RedisBenchmark工具进行了QPS(每秒查询率)性能测试,并将RedisCluster与另一个流行的分布式缓存系统Codis进行了对比。实验结果表明,在高并发访问数(例如10000以上)的场景下,...

    redis-cluster:在Kubernetes上运行的Redis集群设置

    - **监控与告警**:Kubernetes提供丰富的监控工具,如Prometheus和Grafana,可以集成监控Redis的性能指标,如内存使用、QPS、错误等,及时发现和解决问题。 6. **扩展与缩容** - **动态扩展**:随着业务增长,...

    Redis数据导入导出以及数据迁移的4种方法详解

    这种方法简单但速度较慢,且文件可能较大,开启 AOF 可能导致 QPS 下降,并存在数据丢失风险。 第二种方法是通过自定义脚本(例如 `xttblog_redis_mv.sh`)迁移数据。该脚本通过 `redis-cli` 连接源和目标 Redis ...

    Redis开发与运维视频教程

    此外,监控Redis的性能指标(如QPS、内存使用情况、命令执行延迟)至关重要,这通常通过Redis自带的INFO命令或者第三方监控工具实现。 Redis还支持主从复制,可以实现数据备份和读写分离,提高系统的可用性。在高...

    狂神说Redis笔记.pdf

    Redis以其快速的查询速度著称,官方测试表明其QPS(每秒查询次数)可以达到100,000+。Redis不仅可以用作数据库,还可以作为缓存和消息中间件使用,极大地提高了应用程序的响应速度和处理能力。 【Redis数据类型】 ...

    Redis集群高可用,主从1

    同时,由于数据驻留在内存中,Redis 可以达到每秒数万甚至数十万次的查询速率(QPS),极大地提高了 I/O 效率。 7. **虚拟内存(VM)和持久化**: Redis 支持虚拟内存功能,当内存不足时,可以将部分数据换出到...

    redis面试题及其答案.pdf

    最后,Redis支持集群(Cluster),允许将数据分散存储到多个节点上,从而有效地利用资源,并提高可用性。 Redis支持多种数据类型,包括string、list、set、sorted set和hash。String类型是二进制安全的,可以存储...

    10-Redis高级功能.docx

    Redis 集群(Redis Cluster)应运而生,它能够在多个Redis实例之间分配数据,实现横向扩展。自Redis 3.0版本开始,官方正式支持了集群功能,最小集群配置需要包含至少3个主节点和3个对应的从节点。 ##### 5.2 Redis...

    redis缓存服务器

    6. **集群**:Redis Cluster是Redis的分布式解决方案,它可以将数据分散在多个节点上,提供水平扩展和容错能力。每个节点负责一部分数据,通过槽分区算法来确定数据的分布。 7. **lua脚本**:Redis支持在服务器端...

    百度分布式Redis平台架构介绍.pptx

    同时,由于读写比例高,优化了读取性能,以应对单集群数百万QPS的需求。 2. **可用性挑战**:在主从切换时,社区版Redis的全量同步会导致服务中断。BDRP通过采用同源增量同步策略,显著减少了服务中断时间,降低了...

    redisdesktopmanager_jb51.rar

    6. **Cluster 分布式**:Redis Cluster 是 Redis 的分布式解决方案,可以将数据分布在多个节点上,实现数据分区和故障转移。 7. **命令行客户端**:Redis 默认提供了一个命令行客户端 `redis-cli`,用于执行 Redis ...

    【面试资料】-(机构内训资料)Redis面试专题.zip

    一是其高并发性能,由于数据存储在内存,Redis可以实现百万级别的QPS(Queries Per Second)。二是丰富的数据操作,如原子操作(如INCR、DECR)、范围查询(如ZRANGE、LRANGE)以及事务(Transaction)支持。三是...

    redis可视化界面,最新版支持大数据量展示

    综上所述,这个"redis可视化界面,最新版支持大数据量展示"的工具旨在为开发者和系统管理员提供一个强大且用户友好的平台,以应对日益增长的Redis数据管理挑战。通过利用这种工具,他们可以更高效地管理和维护他们的...

    分布式中使用Redis实现Session共享(下)共4页.pdf.zip

    6. **扩展性考虑**:随着用户量增加,可能需要扩展Redis集群,此时可以选择哨兵(Sentinel)系统进行高可用性配置,或者使用Redis Cluster进行数据分片。 7. **安全性**:确保Redis实例不直接暴露在公网,避免未授权...

    Redis性能问题排查解决手册.rar

    1. 监控工具:利用如RedisInsight、Grafana等可视化工具实时监控Redis的各项指标,如QPS(每秒查询量)、TPS(每秒事务处理量)、内存使用、CPU使用等。 2. 日志分析:定期检查Redis日志,查找异常或错误信息,及时...

    gcache国美高性能缓存_王復兴

    描述中提到的“基于redis的大规模集群解决方案”指出,该缓存系统的核心技术基于Redis数据库,通过集群技术来解决大规模数据处理的挑战。这里不仅强调了Redis在实现高性能缓存中的应用,还突出了集群技术在大规模...

    藏经阁-Redis最佳实践与实战指南-47.pdf

    直连模式中,客户端直接连接Redis Cluster,减少中间环节,提高效率。 7. 迁移与故障迁移 标准版向集群版迁移可能遇到命令不兼容问题,代理模式能缓解这一问题。故障迁移过程中,备节点可能有数据延迟,需谨慎处理...

    Redis面试题.docx

    - Redis 单点 TPS(每秒事务处理数)可达到 8 万,QPS(每秒查询率)可达 10 万。TPS 关注的是事务处理,而 QPS 更关注单一接口的响应速度。 10. **与 Memcached 的对比** - 支持更多数据类型:Redis 不仅支持...

    看完这篇Redis.docx

    Redis 以其单进程单线程模型确保线程安全,采用 IO 多路复用机制提高效率,能够支持每秒 10 万次的读写操作(QPS)。作为 NoSQL 数据库,Redis 非常适合用于数据库、缓存、消息中间件等场景。 Redis 的五大数据类型...

    Redis面试题(含答案)哨兵+复制+事务+集群+持久化等.docx

    6. **集群(Cluster)**:Redis 集群是官方提供的分布式解决方案,可以将数据分布在多台服务器上,以支持更大的数据量和更高的并发。集群通过分片(sharding)技术将数据分散在多个节点,每个节点只负责一部分数据,从而...

Global site tag (gtag.js) - Google Analytics