Redis 网络连接层的过去、现状和展望

转载自“Redis网络连接层的过去、现状和展望”,进行了些许修改。

Redis 网络连接层

Redis 取自 Remote Dictionary Server,顾名思义,Redis 是运行在网络环境之上的。Redis 目前支持 3 种网络连接类型:

  • TCP:默认监听 TCP 6379 端口,接收网络请求,提供服务。
  • Unix Socket:可以用作测试,以及使用 Unix Socket 做配置变更等。
  • TLS:使用 TLS 加密的网络连接,可以防止网络链路上被监听、劫持,更加安全。不过代价就是性能有损失。

Redis 通过这 3 种网络连接类型的支持,满足了绝大多数的用户需求,成为了目前最流行的 KV 存储数据库。

过去

截至 2025-04,Redis 最新的版本是 7.4.2。在 7.2.0 版本之前,使用的是“传统”的网络连接管理方式。在 initServer() 中监听端口,位于 server.c

/* Open the TCP listening socket for the user commands. */
if (server.port != 0 &&
    listenToPort(server.port,&server.ipfd) == C_ERR) {
    /* Note: the following log text is matched by the test suite. */
    serverLog(LL_WARNING, "Failed listening on port %u (TCP), aborting.", server.port);
    exit(1);
}
if (server.tls_port != 0 &&
    listenToPort(server.tls_port,&server.tlsfd) == C_ERR) {
    /* Note: the following log text is matched by the test suite. */
    serverLog(LL_WARNING, "Failed listening on port %u (TLS), aborting.", server.tls_port);
    exit(1);
}

/* Open the listening Unix domain socket. */
if (server.unixsocket != NULL) {
    unlink(server.unixsocket); /* don't care if this fails */
    server.sofd = anetUnixServer(server.neterr,server.unixsocket,
        (mode_t)server.unixsocketperm, server.tcp_backlog);
    if (server.sofd == ANET_ERR) {
        serverLog(LL_WARNING, "Failed opening Unix socket: %s", server.neterr);
        exit(1);
    }
    anetNonBlock(NULL,server.sofd);
    anetCloexec(server.sofd);
}

以及设置监听文件描述符的处理函数,开始提供网络服务:

/* Create an event handler for accepting new connections in TCP and Unix
 * domain sockets. */
if (createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) != C_OK) {
    serverPanic("Unrecoverable error creating TCP socket accept handler.");
}
if (createSocketAcceptHandler(&server.tlsfd, acceptTLSHandler) != C_OK) {
    serverPanic("Unrecoverable error creating TLS socket accept handler.");
}
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
    acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");

从代码中,我们可以清晰地看到这几种连接类型的初始化过程和配置参数等。但是它的代价是:

  • 不能扩展:如果想要支持一个新的连接类型,那么势必要修改代码。事实上,需要修改的代码还包含另外的几处。
  • 宏的引用:因为 TLS 不是操作系统默认支持,需要依赖编译选项控制,那么存在 TLS 是否支持两种情况,就需要使用宏控制。在Redis的代码中存在多处 #ifdef USE_OPENSSL 的使用。
  • 代码的可维护性降低:在 server.c 中,不得不引用、调用 TCP、TLS 和 Unix Socket 相关的代码逻辑。

现状

连接层框架

Redis 7.2.0 正式引入支持了连接层框架(connection layer framework), 它长成这样:

                           uplayer
                              |
                       connection layer
                         /    |     \
                       TCP   Unix   TLS

connection layer 负责抽象连接类型,它要求每种连接类型具有如下的方法:

typedef struct ConnectionType {
    /* connection type */
    const char *(*get_type)(struct connection *conn);
    /* connection type initialize & finalize & configure */
    void (*init)(void); /* auto-call during register */
    void (*cleanup)(void);
    int (*configure)(void *priv, int reconfigure);

    /* ae & accept & listen & error & address handler */
    void (*ae_handler)(struct aeEventLoop *el, int fd, void *clientData, int mask);
    aeFileProc *accept_handler;
    int (*addr)(connection *conn, char *ip, size_t ip_len, int *port, int remote);
    int (*listen)(connListener *listener);

    /* create/close connection */
    connection* (*conn_create)(void);
    connection* (*conn_create_accepted)(int fd, void *priv);
    void (*close)(struct connection *conn);
    /* connect & accept */
    int (*connect)(struct connection *conn, const char *addr, int port, const char *source_addr, ConnectionCallbackFunc connect_handler);
    int (*blocking_connect)(struct connection *conn, const char *addr, int port, long long timeout);
    int (*accept)(struct connection *conn, ConnectionCallbackFunc accept_handler);
    /* IO */
    int (*write)(struct connection *conn, const void *data, size_t data_len);
    int (*writev)(struct connection *conn, const struct iovec *iov, int iovcnt);
    int (*read)(struct connection *conn, void *buf, size_t buf_len);
    int (*set_write_handler)(struct connection *conn, ConnectionCallbackFunc handler, int barrier);
    int (*set_read_handler)(struct connection *conn, ConnectionCallbackFunc handler);
    const char *(*get_last_error)(struct connection *conn);
    ssize_t (*sync_write)(struct connection *conn, char *ptr, ssize_t size, long long timeout);
    ssize_t (*sync_read)(struct connection *conn, char *ptr, ssize_t size, long long timeout);
    ssize_t (*sync_readline)(struct connection *conn, char *ptr, ssize_t size, long long timeout);

    /* pending data */
    int (*has_pending_data)(void);
    int (*process_pending_data)(void);
    /* TLS specified methods */
    sds (*get_peer_cert)(struct connection *conn);
} ConnectionType;

上层(uplayer)通过 connection layer 访问 Redis 的各个连接类型,则可以忽略连接类型的具体实现,仅仅需要调用各个方法即可。

同时,connection layer 还负责管理各个连接类型,例如一个新连接类型在使用之前,需要调用 connTypeRegister() 向 Redis 进行注册,位于 connection.c

int connTypeRegister(ConnectionType *ct) {
    const char *typename = ct->get_type(NULL);
    ConnectionType *tmpct;
    int type;

    /* find an empty slot to store the new connection type */
    for (type = 0; type < CONN_TYPE_MAX; type++) {
        tmpct = connTypes[type];
        if (!tmpct)
            break;
        /* ignore case, we really don't care "tls"/"TLS" */
        if (!strcasecmp(typename, tmpct->get_type(NULL))) {
            serverLog(LL_WARNING, "Connection types %s already registered", typename);
            return C_ERR;
        }
    }

    serverLog(LL_VERBOSE, "Connection type %s registered", typename);
    connTypes[type] = ct;

    if (ct->init) {
        ct->init();
    }

    return C_OK;
}

基于此,在 server.c 监听各个连接类型则变成:

/* create all the configured listener, and add handler to start to accept */
int listen_fds = 0; 
for (int j = 0; j < CONN_TYPE_MAX; j++) {
    listener = &server.listeners[j];
    if (listener->ct == NULL)
        continue;

    if (connListen(listener) == C_ERR) {
        serverLog(LL_WARNING, "Failed listening on port %u (%s), aborting.", listener->port, listener->ct->get_type(NULL));
        exit(1);
    }    

    if (createSocketAcceptHandler(listener, connAcceptHandler(listener->ct)) != C_OK)
        serverPanic("Unrecoverable error creating %s listener accept handler.", listener->ct->get_type(NULL));

   listen_fds += listener->count;
}       

连接类型

目前 Redis 支持 3 中连接类型:

  • TCP:对应的 ConnectionType 实现是 CT_Socket,位于 socket.c
  • TLS:对应的 ConnectionType 实现是 CT_TLS,位于 tls.c
  • Unix Socket:对应的 ConnectionType 实现是 CT_Unix,位于 unix.c

动态加载的连接类型

目前只有 TLS 支持模块化,动态加载。

TCP 和 Unix Sokcet 都是静态链接到 Redis 中的。它们的连接类型在 main() -> connTypeInitialize() 中注册。

如果 TLS 也是静态链接到 Redis 的,那么它也是在 connTypeInitialize() 中注册的。

如果 TLS 也是动态链接到 Redis 的,那么他在模块入口函数 RedisModule_OnLoad 中注册。

在过去的版本中,需要在 Redis 编译时决定是否支持 TLS。得益于新的连接层框架,Redis 支持:

  • 不支持 TLS
  • 静态支持 TLSmake BUILD_TLS=yes,代价是 redis-server 始终需要链接 libssllibcrypto,尽管可能不运行。
  • 动态支持 TLSmake BUILD_TLS=module 即可把 TLS 支持编译成为 redis-tls.so。如果希望使用 TLS,通过 redis-server --loadmodule src/redis-tls.so 即可动态加载 TLS,达到了“运行时加载”的效果。

同时,在代码结构上,也带来了一定的收益:几乎移除掉 #ifdef USE_OPENSSL,仅在 tls.c 中使用,同时重载 ConnectionType

static ConnectionType CT_TLS = {
    /* connection type */
    .get_type = connTLSGetType,
    /* connection type initialize & finalize & configure */
    .init = tlsInit,
    .cleanup = tlsCleanup,
    .configure = tlsConfigure,
    /* ae & accept & listen & error & address handler */
    .ae_handler = tlsEventHandler,
    .accept_handler = tlsAcceptHandler,
    .addr = connTLSAddr,
    .listen = connTLSListen,
    /* create/close connection */
    .conn_create = connCreateTLS,
    .conn_create_accepted = connCreateAcceptedTLS,
    .close = connTLSClose,
    /* connect & accept */
    .connect = connTLSConnect,
    .blocking_connect = connTLSBlockingConnect,
    .accept = connTLSAccept,
    /* IO */
    .read = connTLSRead,
    .write = connTLSWrite,
    .writev = connTLSWritev,
    .set_write_handler = connTLSSetWriteHandler,
    .set_read_handler = connTLSSetReadHandler,
    .get_last_error = connTLSGetLastError,
    .sync_write = connTLSSyncWrite,
    .sync_read = connTLSSyncRead,
    .sync_readline = connTLSSyncReadLine,

    /* pending data */
    .has_pending_data = tlsHasPendingData,
    .process_pending_data = tlsProcessPendingData,

    /* TLS specified methods */
    .get_peer_cert = connTLSGetPeerCert,
};

每一个 Redis 模块都必须实现 RedisModule_OnLoad,在加载 Redis 模块后会自动调用 RedisModule_OnLoad,Redis TLS 模块加载后会在该函数中调用 connTypeRegister 向 Redis 注册新的连接类型。TLS 模块对 RedisModule_OnLoad 的实现位于 tls.c

int RedisModule_OnLoad(void *ctx, RedisModuleString **argv, int argc) {
    UNUSED(argv);
    UNUSED(argc);

    /* Connection modules must be part of the same build as redis. */
    if (strcmp(REDIS_BUILD_ID_RAW, redisBuildIdRaw())) {
        serverLog(LL_NOTICE, "Connection type %s was not built together with the redis-server used.", CONN_TYPE_TLS);
        return REDISMODULE_ERR;
    }

    if (RedisModule_Init(ctx,"tls",1,REDISMODULE_APIVER_1) == REDISMODULE_ERR)
        return REDISMODULE_ERR;
    /* Connection modules is available only bootup. */
    if ((RedisModule_GetContextFlags(ctx) & REDISMODULE_CTX_FLAGS_SERVER_STARTUP) == 0) {
        serverLog(LL_NOTICE, "Connection type %s can be loaded only during bootup", CONN_TYPE_TLS);
        return REDISMODULE_ERR;
    }

    RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_HANDLE_REPL_ASYNC_LOAD);

    if(connTypeRegister(&CT_TLS) != C_OK)
        return REDISMODULE_ERR;

    return REDISMODULE_OK;
}

通过 Redis Module 机制,以及连接层的抽象和框架扩展能力,让 Redis 的连接类型支持更加易用、可扩展。

重写的 Unix Socket 连接类型

尽管 Unix Socket 和 TCP 是完全不同的连接类型,但是二者具有很大的相似性:基于一个 FD(文件描述符)即可操作;支持 read、write、writev 等 IO 操作。于是 Redis 在代码中谨慎地判断 TCP/ Unix Socket,最大程度上复用了 TCP 的函数。

基于新的连接类型框框架,把 Unix Socket 支持从 TCP 中剥离出来,让代码拥有更好的维护性,参考 unix.c

/* ==========================================================================
 * unix.c - unix socket connection implementation
 * --------------------------------------------------------------------------
 */
#include "server.h"
#include "connection.h"

static ConnectionType CT_Unix;

static const char *connUnixGetType(connection *conn) {
    UNUSED(conn);


    return CONN_TYPE_UNIX;
}

static void connUnixEventHandler(struct aeEventLoop *el, int fd, void *clientData, int mask) {
    connectionTypeTcp()->ae_handler(el, fd, clientData, mask);
}

static int connUnixAddr(connection *conn, char *ip, size_t ip_len, int *port, int remote) {
    return connectionTypeTcp()->addr(conn, ip, ip_len, port, remote);
}

static int connUnixListen(connListener *listener) {
    int fd;
    mode_t *perm = (mode_t *)listener->priv;

    if (listener->bindaddr_count == 0)
        return C_OK;

    /* currently listener->bindaddr_count is always 1, we still use a loop here in case Redis supports multi Unix socket in the future */
    for (int j = 0; j < listener->bindaddr_count; j++) {
        char *addr = listener->bindaddr[j];

        unlink(addr); /* don't care if this fails */
        fd = anetUnixServer(server.neterr, addr, *perm, server.tcp_backlog);
        if (fd == ANET_ERR) {
            serverLog(LL_WARNING, "Failed opening Unix socket: %s", server.neterr);
            exit(1);
        }
        anetNonBlock(NULL, fd);
        anetCloexec(fd);
        listener->fd[listener->count++] = fd;
    }

    return C_OK;
}

static connection *connCreateUnix(void) {
    connection *conn = zcalloc(sizeof(connection));
    conn->type = &CT_Unix;
    conn->fd = -1;

    return conn;
}

static connection *connCreateAcceptedUnix(int fd, void *priv) {
    UNUSED(priv);
    connection *conn = connCreateUnix();
    conn->fd = fd;
    conn->state = CONN_STATE_ACCEPTING;
    return conn;
}

static void connUnixAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cfd, max = MAX_ACCEPTS_PER_CALL;
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);

    while(max--) {
        cfd = anetUnixAccept(server.neterr, fd);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        serverLog(LL_VERBOSE,"Accepted connection to %s", server.unixsocket);
        acceptCommonHandler(connCreateAcceptedUnix(cfd, NULL),CLIENT_UNIX_SOCKET,NULL);
    }
}

static void connUnixClose(connection *conn) {
    connectionTypeTcp()->close(conn);
}

static int connUnixAccept(connection *conn, ConnectionCallbackFunc accept_handler) {
    return connectionTypeTcp()->accept(conn, accept_handler);
}

static int connUnixWrite(connection *conn, const void *data, size_t data_len) {
    return connectionTypeTcp()->write(conn, data, data_len);
}

static int connUnixWritev(connection *conn, const struct iovec *iov, int iovcnt) {
    return connectionTypeTcp()->writev(conn, iov, iovcnt);
}

static int connUnixRead(connection *conn, void *buf, size_t buf_len) {
    return connectionTypeTcp()->read(conn, buf, buf_len);
}

static int connUnixSetWriteHandler(connection *conn, ConnectionCallbackFunc func, int barrier) {
    return connectionTypeTcp()->set_write_handler(conn, func, barrier);
}

static int connUnixSetReadHandler(connection *conn, ConnectionCallbackFunc func) {
    return connectionTypeTcp()->set_read_handler(conn, func);
}

static const char *connUnixGetLastError(connection *conn) {
    return strerror(conn->last_errno);
}

static ssize_t connUnixSyncWrite(connection *conn, char *ptr, ssize_t size, long long timeout) {
    return syncWrite(conn->fd, ptr, size, timeout);
}

static ssize_t connUnixSyncRead(connection *conn, char *ptr, ssize_t size, long long timeout) {
    return syncRead(conn->fd, ptr, size, timeout);
}

static ssize_t connUnixSyncReadLine(connection *conn, char *ptr, ssize_t size, long long timeout) {
    return syncReadLine(conn->fd, ptr, size, timeout);
}

static ConnectionType CT_Unix = {
    /* connection type */
    .get_type = connUnixGetType,

    /* connection type initialize & finalize & configure */
    .init = NULL,
    .cleanup = NULL,
    .configure = NULL,

    /* ae & accept & listen & error & address handler */
    .ae_handler = connUnixEventHandler,
    .accept_handler = connUnixAcceptHandler,
    .addr = connUnixAddr,
    .listen = connUnixListen,

    /* create/close connection */
    .conn_create = connCreateUnix,
    .conn_create_accepted = connCreateAcceptedUnix,
    .close = connUnixClose,
    /* connect & accept */
    .connect = NULL,
    .blocking_connect = NULL,
    .accept = connUnixAccept,

    /* IO */
    .write = connUnixWrite,
    .writev = connUnixWritev,
    .read = connUnixRead,
    .set_write_handler = connUnixSetWriteHandler,
    .set_read_handler = connUnixSetReadHandler,
    .get_last_error = connUnixGetLastError,
    .sync_write = connUnixSyncWrite,
    .sync_read = connUnixSyncRead,
    .sync_readline = connUnixSyncReadLine,

    /* pending data */
    .has_pending_data = NULL,
    .process_pending_data = NULL,
};

int RedisRegisterConnectionTypeUnix()
{
    return connTypeRegister(&CT_Unix);
}

由于 Unix Socket 实现较为简单,且复用了大量的 TCP 连接代码,unix.c 中仅使用了少量的代码实现,从中依然可以窥探一个连接类型具有的基本属性:

  • 重载 ConnectionType 连接类型。
  • 连接类型变量和重载函数为 static 类型,对外不做任何暴露。
  • 向连接层注册。
  • 事实上,也可以把 Unix Socket 支持编译成为一个动态链接库,以 loadmodule 的方式动态加载。Redis 的 Maintainer Oran 认为 Unix Socket 是一个基础的连接类型,不需要额外的链接和宏控制,因此始终使用静态编译支持。

展望

得益于 Redis 连接层框架和 Module 机制,向 Redis 中增加一个新的连接类型变得更加容易。RDMA 是一种高性能的网络技术,iWARP 和 RoCE v2 也让数据中心的以太网络支持了 RDMA,近年来也变得更加流行。因此,是不是可以让 Redis 跑在 RDMA 上呢?在测试中,在 1KB 的 KV 情况下,Redis Over RDMA 技术让 Redis 单核性能达到~450K QPS,大约是相同环境下的 TCP 性能的 2.5 倍(~180K QPS)。目前 Redis Over RDMA 的 Pull Request 正在社区接受 Review,也欢迎提建议、捉 BUG。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值