Databases 11 min read

Redis 6 Multithreaded I/O Implementation and Performance Evaluation

The article details Redis 6’s new multithreaded I/O feature—motivated by network‑I/O bottlenecks, implemented with lock‑free pending‑read queues that offload reads, writes, and protocol parsing to worker threads while keeping command execution single‑threaded—and demonstrates through a simple benchmark that using four I/O threads roughly doubles GET/SET throughput compared with Redis 5.

Meitu Technology
Meitu Technology
Meitu Technology
Redis 6 Multithreaded I/O Implementation and Performance Evaluation

After watching the Redis author Salvatore's talk at RedisConf 2019, I was excited to see the multithreaded I/O feature introduced in Redis 6, which claims to double performance. This article explains the motivation, design, and code implementation of Redis's multithreaded I/O, and presents a simple benchmark.

For the single‑threaded Redis, the main performance bottleneck is network I/O. Optimization can be approached in two ways:

Improve network I/O performance, e.g., replace the kernel network stack with DPDK.

Utilize multiple cores with multithreading, similar to Memcached.

Multithreaded I/O was finally added to Redis 6 after many community requests. Unlike Memcached, Redis only uses threads for network read/write and protocol parsing; command execution remains single‑threaded to avoid the complexity of synchronizing keys, Lua scripts, transactions, etc.

Code Implementation

The multithreaded I/O read (request) and write (response) share the same flow; the example below shows the read path. Only core logic is shown.

When a client request arrives, the main thread places the connection into a global pending‑read queue:

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    /* Check if we want to read from the client later when exiting from
     * the event loop. This is the case if threaded I/O is enabled. */
    if (postponeClientRead(c)) return;
    ...
}

The function postponeClientRead decides whether to defer the read to an I/O thread:

int postponeClientRead(client *c) {
    if (io_threads_active &&
        server.io_threads_do_reads &&
        !(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ))) {
        c->flags |= CLIENT_PENDING_READ;
        listAddNodeHead(server.clients_pending_read, c);
        return 1;
    } else {
        return 0;
    }
}

If the function returns 1, readQueryFromClient returns immediately, and the main thread later distributes the pending connections to I/O threads using a round‑robin scheme:

int handleClientsWithPendingReadsUsingThreads(void) {
    ...
    listRewind(server.clients_pending_read, &li);
    int item_id = 0;
    while ((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        int target_id = item_id % server.io_threads_num;
        listAddNodeTail(io_threads_list[target_id], c);
        item_id++;
    }
    ...
    while (1) {
        unsigned long pending = 0;
        for (int j = 0; j < server.io_threads_num; j++)
            pending += io_threads_pending[j];
        if (pending == 0) break;
    }
}

Each I/O thread runs IOThreadMain and processes its assigned connections either for reads or writes based on the global io_threads_op flag:

void *IOThreadMain(void *myid) {
    while (1) {
        listRewind(io_threads_list[id], &li);
        while ((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            if (io_threads_op == IO_THREADS_OP_WRITE) {
                writeToClient(c->fd, c, 0);
            } else if (io_threads_op == IO_THREADS_OP_READ) {
                readQueryFromClient(NULL, c->fd, c, 0);
            } else {
                serverPanic("io_threads_op value is unknown");
            }
        }
        listEmpty(io_threads_list[id]);
        io_threads_pending[id] = 0;
    }
}

The design is lock‑free because the main thread spins until all I/O threads finish their work, avoiding data races. Note that the I/O threads have no sleep mechanism; when idle they consume CPU, so Redis disables the threads when the pending queue is small.

Performance Comparison

Benchmark environment:

Redis Server: Alibaba Cloud Ubuntu 18.04, 8 CPU 2.5 GHz, 8 GB RAM (ecs.ic5.2xlarge)
Redis Benchmark Client: Alibaba Cloud Ubuntu 18.04, 8 CPU 2.5 GHz, 8 GB RAM (ecs.ic5.2xlarge)

The multithreaded I/O code lives on the unstable branch, while the single‑threaded baseline is Redis 5.0.5. The following configuration enables four I/O threads and lets them handle reads:

io-threads 4               # enable 4 I/O threads
io-threads-do-reads yes   # also parse requests in I/O threads

Benchmark command (GET/SET, 1 M operations, 256 concurrent clients, 4 client threads):

redis-benchmark -h 192.168.0.49 -a foobared -t set,get -n 1000000 -r 100000000 --threads 4 -d ${datasize} -c 256

Results show that GET/SET throughput roughly doubles when using four I/O threads compared with the single‑threaded version. The numbers are meant for a quick validation of the multithreaded I/O benefit; they are not exhaustive latency or scalability studies.

Conclusion

Redis 6.0 (expected end of 2019) will bring significant improvements in performance, protocol handling, and access control. The author, Salvatore, continues to work on Redis and its cluster proxy, which should further broaden Redis’s adoption in China.

PerformanceDatabaseRedisCbenchmarkMultithreaded I/O
Meitu Technology
Written by

Meitu Technology

Curating Meitu's technical expertise, valuable case studies, and innovation insights. We deliver quality technical content to foster knowledge sharing between Meitu's tech team and outstanding developers worldwide.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.