新一代异步IO框架 io_uring | 得物技术

1.Linux IO 模型分类

相比于kernel bypass 模式需要结合具体的硬件支撑来讲,native IO是日常工作中接触到比较多的一种,其中同步IO在较长一段时间内被广泛使用,通常我们接触到的IO操作主要分为网络IO和存储IO。在大流量高并发的今天,提到网络IO,很容易想到大名鼎鼎的epoll  以及reactor架构。但是epoll并不属于异步IO的范畴。本质上是一个同步非阻塞的架构。关于同步异步,阻塞与非阻塞的概念区别这里做简要概述:

  • 什么是同步

指进程调用接口时需要等待接口处理完数据并相应进程才能继续执行。这里重点是数据处理活逻辑执行完成并返回,如果是异步则不必等待数据完成,亦可以继续执行。同步强调的是逻辑上的次序性;

  • 什么是阻塞

当进程调用一个阻塞的系统函数时,该进程被 置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比 如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况,一种是进程正在被CPU调度,另一种是处于就绪状态随时可能被调度的进程;阻塞强调的是函数调用下进程的状态。

2. Linux常见文件操作方式

2.1   open/close/read/write

基本操作API 如下:

#include <unistd.h>                                                  
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 

//  返回值:成功返回新分配的文件描述符,出错返回-1并设置errno                                                  
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

// 返回值:成功返回0,出错返回-1并设置errno                                                  
int close(int fd); 

// 返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0                                                  
ssize_t read(int fd, void *buf, size_t count);   

// 返回值:成功返回写入的字节数,出错返回-1并设置errno                                                     
ssize_t write(int fd, const void *buf, size_t count); 

在打开文件时可以指定为,只读,只写,读写等权限,以及阻塞或者非阻塞操作等;具体通过open函数的flags 参数指定 。这里以打开一个读写文件为例,同时定义了写文件的方式为追加写,以及使用直接IO模式操作文件,具体什么是直接IO下文会细述。open("/path/to/file", O_RDWR|O_APPEND|O_DIRECT);

flags 可选参数如下:

Flag 参数

含义

O_CREATE

创建文件时,如果文件存在则出错返回

O_EXCL

如果同时指定了O_CREAT,并且文件已存在,则出错返回。

O_TRUC

把文件截断成0

O_RDONLY

只读

O_WRONLY

只写

O_RDWR

读写

O_APPEND

追加

O_NONBLOCK

非阻塞标记

O_SYNC

每次读写都等待物理IO操作完成

O_DIRECT

提供最直接IO支持

通常读写操作的数据首先从用户缓冲区进入内核缓冲区,然后由内核缓冲区完成与IO设备的同步:

2.2   Mmap

// 成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],
// error被设为以下的某个值:
// 1 EACCES:访问出错
// 2 EAGAIN:文件已被锁定,或者太多的内存已被锁定
// 3 EBADF:fd不是有效的文件描述词
// 4 EINVAL:一个或者多个参数无效
// 5 ENFILE:已达到系统对打开文件的限制
// 6 ENODEV:指定文件所在的文件系统不支持内存映射
// 7 ENOMEM:内存不足,或者进程已超出最大内存映射数量
// 8 EPERM:权能不足,操作不允许
// 9 ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
//10 SIGSEGV:试着向只读区写入
//11 SIGBUS:试着访问不属于进程的内存区
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

// 成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;
// 该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;
int munmap( void * addr, size_t len )

// 进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。
// 如果期望内存的数据变化能够立刻反应到磁盘上,可以通过调用msync()实现。
int msync( void *addr, size_t len, int flags )

 Mmap 是一种内存映射方法,通过将文件映射到内存的某个地址空间上,在对该地址空间的读写操作时,会触发相应的缺页异常以及脏页回写操作,从而实现文件数据的读写操作;

2.3   直接IO

直接IO的方式比较简单,直接上文提及的open函数入参中指定 O_DIRECT 即可,相比普通IO操作,略过了内核的缓冲区直接操作下一层的文件文件。该操作比较底层,相比普通的文件读写少了一次数据复制,一般需要结合用户态缓存来使用;下图所示为 DIO 透过 buffer层直接操作磁盘文件系统:

2.4   sendFile

严格来讲,sendfile 并不提供完整的读写能力,仅用于加速读取数据到网络的能力,由于数据不经过用户空间,因此无法对数据进行二次处理,也就是说从磁盘中读出来原封不动的发给网卡,下图展示了sendFile 的工作流程,

  • 数据首先以DMA的方式从磁盘上读取到内核的文件缓冲区,

  • 然后再从文件缓冲区读取到了socket的缓冲区,该过程由CPU负责完成。

  • 接着网卡再以DMA的方式从socket缓冲区 拷贝到自己网卡缓冲区,然后进行发送

Linux 内核2.4 版本以后对 sendFile 进行了进一步优化,提供了带有 scatter/gather的 sendfile 操作,将仅有一次的CPU参与copy 环节去掉,该操作需要网卡硬件的支持。其原理就是在内核空间 Read Buffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中。其本质和虚拟内存的解决方法思路一致,就是内存地址的记录。

2.5   splice 

splice 调用和 sendfile  很相似,应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。splice允许任意两个文件互相连接,而并不只是文件与 socket 进行数据传输。对于从一个文件描述符发送数据到 socket 这种特例来说,简化为使用 sendfile 系统调用,splice 适用范围更广且不需要硬件支持, sendfile  是 splice  的一个子集。

  • 用户进程调用 pipe()陷入内核态;创建匿名单向管道 pipe() 返回,从内核态切换回用户态;

  • 用户进程调用 splice()从用户态陷入内核态,DMA 控制器将数据从硬盘拷贝到内核缓冲区,从管道的写入端"拷贝"进管道,splice() 返回,从内核态切换为用户态;

  • 用户进程再次调用 splice(),从用户态陷入内核态,内核把数据从管道的读取端拷贝到socket缓冲区,DMA 控制器将数据从 socket 缓冲区拷贝到网卡,splice() 返回,上下文从内核态切换回用户态。

3. IO_URING是什么

io_uring 是 Linux 提供的一个异步非阻塞 I/O 接口,他既能支持磁盘IO也能支持网络IO,只是存储IO支持的比较早较为成熟。IO_URING的使用需要较高的linux 内核版本,一般建议5.12 版本以后。下面会分别从存储和网络两个角度来介绍IO_URING 。

3.1   IO_URING 架构

  • 应用程序提交的IO 请求会直接进入submission queue 队列的尾部,内核进程会不断的从SQ 队列的头部消费请求

  • 内核处理完的SQ后会更新CQ  tail 部分 ,应用程序读取到CQ 的head时,会更新CQ的head

  • SQ 中的任务称之为 SQE(entry), CQ中的任务称之为CQE

3.2   系统调用API

// 创建一个 SQ 和一个 CQ,queue size 至少 entries 个元素
// 返回一个文件描述符,随后用于在这个 io_uring 实例上执行操作。
// 参数p 有两个作用:
// 1.作为入参:应用用来配置 io_uring 的一些行为
// 2.作为出参:内核返回的 SQ/CQ 地址信息等也通过它带回来。
int io_uring_setup(u32 entries, struct io_uring_params *p);


// 注册用于异步 I/O 的文件或用户缓冲区(files or user buffers):
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);


// 用于初始化和完成I/O,使用共享的 SQ 和 CQ。 单次调用同时提交新的 I/O 请求和等待 I/O 完成操作
// fd 是 io_uring_setup() 返回的文件描述符;
// to_submit 指定了 SQ 中提交的 I/O 数量;
// 默认模式下如果指定了 min_complete,会等待这个数量的 I/O 事件完成再返回;
// 轮询模式(2种):
//   0:要求内核返回当前以及完成的所有 events,无阻塞;
//   非0:如果有事件完成,内核仍然立即返回;如果没有完成事件,内核会 poll,等待指定的次数完成,或者这个进程的时间片用完。
int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);

3.3   三种工作模式

3.3.1  中断驱动模式:

默认模式。可通过 io_uring_enter() 提交 I/O 请求,然后直接检查 CQ 状态判断是否完成。也可以通过 min_complete 来睡在 enter 方法上,等待完成事件到达  ;

3.3.2  轮询模式:

相比中断驱动方式,这种方式延迟更低, 但是会消耗更多的CPU,应用线程需要不断的调用 enter 函数,然后陷入内核态后持续地 polling,等到一个 min_complete 到达。但是注意的是此时 polling 关注的是完成事件。

3.3.3  内核轮询模式

这种模式中,会创建一个内核线程(kernel thread)来执行 SQ 的轮询工作( 是否有新的SQE提交 )。使用这种模式应用无需切到到内核态 就能触发(issue)I/O 操作。应用线程通过mmap 机制更新SQ 来提交 SQE,以及监控 CQ 的完成状态,应用无需任何系统调用,就能提交和收割 I/O(submit and reap I/Os)。如果内核线程的空闲时间超过了用户的配置值,它会通知应用,然后进入 idle 状态。这种情况下,应用必须调用 io_uring_enter() 来唤醒内核线程。如果 I/O 一直很繁忙,内核线程是不会 sleep 的。

在日常的使用中一般建议选择后两种轮训模式,用户线程轮存在用户态到内核态的切换,相比内核轮询存在一定的性能损耗;io_uring 之所以能达到超高性能的原因主要在以下几个方面:

  1. Mmap 机制减少了 内存复制

  2. 内核轮询模式下,没有用户态和内核态的切换降低了损耗

  3. 基于SQ和CQ 机制下的数据竞争消除,即没有并发竞争损耗

3.4   liburing

io_uring的核心系统调用只有三个,但使用起来较为复杂,开发者在io_uring 之上封装了新的liburing 库,简化使用。


// io_uring 结构体中包含需要使用到的 SQ和CQ ,以及需要关联的文件FD, 和相关的配置参数falgs; 
struct io_uring {
        struct io_uring_sq sq;
        struct io_uring_cq cq;
        unsigned flags;
        int ring_fd;
};

struct io_uring_sq {
        unsigned *khead;
        unsigned *ktail;
        unsigned *kring_mask;
        unsigned *kring_entries;
        unsigned *kflags;
        unsigned *kdropped;
        unsigned *array;
        struct io_uring_sqe *sqes;

        unsigned sqe_head;
        unsigned sqe_tail;

        size_t ring_sz;
        void *ring_ptr;
};

struct io_uring_cq {
        unsigned *khead;
        unsigned *ktail;
        unsigned *kring_mask;
        unsigned *kring_entries;
        unsigned *koverflow;
        struct io_uring_cqe *cqes;

        size_t ring_sz;
        void *ring_ptr;
};

// 用户初始化 io_uring。该方法中包含了内存空间的初始化以及mmap 调用,entries:队列深度 
int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);

// 为了提交IO请求,需要获取里面queue的一个空闲项
struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);

// 非系统调用,准备阶段,和libaio封装的io_prep_writev一样
void io_uring_prep_writev(struct io_uring_sqe *sqe, int fd,const struct iovec *iovecs, unsigned nr_vecs, off_t offset)

// 非系统调用,准备阶段,和libaio封装的io_prep_readv一样
void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd, const struct iovec *iovecs, unsigned nr_vecs, off_t offset)

// 提交sq的entry,不会阻塞等到其完成,内核在其完成后会自动将sqe的偏移信息加入到cq,在提交时需要加锁
int io_uring_submit(struct io_uring *ring);

// 提交sq的entry,阻塞等到其完成,在提交时需要加锁。
int io_uring_submit_and_wait(struct io_uring *ring, unsigned wait_nr);

// 非系统调用 遍历时,可以获取cqe的data
void *io_uring_cqe_get_data(const struct io_uring_cqe *cqe)

// 清理io_uring
void io_uring_queue_exit(struct io_uring *ring);

liburing  github地址 : https://github.com/axboe/liburing

3.5   使用方式

3.5.1  读取文件

  1. 调用 io_uring_queue_init  初始化

  2. 获取一个空 SQE用于提交任务

  3. io_uring_prep_readv  方法填充SQE 任务内容

  4. io_uring_submit 提交SQE

  5. io_uring_wait_cqe 获取已完成的CQE

  6. io_uring_cqe_seen   更新CQ 队列的head ,避免CQE被重复处理

  7. io_uring_queue_exit 退出 io_uring

下面是liburing github 上的example 代码适当精简后的代码:

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "liburing.h"

#define QD        4
int main(int argc, char *argv[]){
        struct io_uring ring;
        int i, fd, ret, pending, done;
        struct io_uring_sqe *sqe;
        struct io_uring_cqe *cqe;
        struct iovec *iovecs;
        struct stat sb;
        ssize_t fsize;
        off_t offset;
        void *buf;

        ret = io_uring_queue_init(QD, &ring, 0);
        if (ret < 0) {
                fprintf(stderr, "queue_init: %s\n", strerror(-ret)); return 1;
        }
        fd = open(argv[1], O_RDONLY | O_DIRECT);
        fsize = 0;
        iovecs = calloc(QD, sizeof(struct iovec));
        for (i = 0; i < QD; i++) {
                if (posix_memalign(&buf, 4096, 4096))
                        return 1;
                iovecs[i].iov_base = buf;
                iovecs[i].iov_len = 4096;
                fsize += 4096;
        }

        offset = 0;
        i = 0;
        do {
                sqe = io_uring_get_sqe(&ring);
                if (!sqe)  break;
                io_uring_prep_readv(sqe, fd, &iovecs[i], 1, offset);
                offset += iovecs[i].iov_len;
                i++;
                if (offset > sb.st_size) break;
        } while (1);

        ret = io_uring_submit(&ring);
        if (ret < 0) {
                fprintf(stderr, "io_uring_submit: %s\n", strerror(-ret)); return 1;
        } else if (ret != i) {
                fprintf(stderr, "io_uring_submit submitted less %d\n", ret); return 1;
        }

        done = 0;
        pending = ret;
        fsize = 0;
        for (i = 0; i < pending; i++) {
                ret = io_uring_wait_cqe(&ring, &cqe);
                if (ret < 0) {
                        fprintf(stderr, "io_uring_wait_cqe: %s\n", strerror(-ret));return 1;
                }
                done++;
                ret = 0;
                if (cqe->res != 4096 && cqe->res + fsize != sb.st_size) {
                        fprintf(stderr, "ret=%d, wanted 4096\n", cqe->res);
                        ret = 1;
                }
                fsize += cqe->res;
                io_uring_cqe_seen(&ring, cqe);
                if (ret) break;
        }
        printf("Submitted=%d, completed=%d, bytes=%lu\n", pending, done, (unsigned long) fsize);
        close(fd);
        io_uring_queue_exit(&ring);
        return 0;
}

3.5.2  网络服务

网络服务这里直接参考 Github 地址:GitHub - frevib/io_uring-echo-server: io_uring echo server

#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <unistd.h>

#include "liburing.h"

#define MAX_CONNECTIONS     4096
#define BACKLOG             512
#define MAX_MESSAGE_LEN     2048
#define BUFFERS_COUNT       MAX_CONNECTIONS

void add_accept(struct io_uring *ring, int fd, struct sockaddr *client_addr, socklen_t *client_len, unsigned flags);
void add_socket_read(struct io_uring *ring, int fd, unsigned gid, size_t size, unsigned flags);
void add_socket_write(struct io_uring *ring, int fd, __u16 bid, size_t size, unsigned flags);
void add_provide_buf(struct io_uring *ring, __u16 bid, unsigned gid);

enum {
    ACCEPT,
    READ,
    WRITE,
    PROV_BUF,
};

typedef struct conn_info {
    __u32 fd;
    __u16 type;
    __u16 bid;
} conn_info;

char bufs[BUFFERS_COUNT][MAX_MESSAGE_LEN] = {0};
int group_id = 1337;

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Please give a port number: ./io_uring_echo_server [port]\n");
        exit(0);
    }

    // some variables we need
    int portno = strtol(argv[1], NULL, 10);
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);

    // setup socket
    int sock_listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    const int val = 1;
    setsockopt(sock_listen_fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(portno);
    serv_addr.sin_addr.s_addr = INADDR_ANY;

    // bind and listen
    if (bind(sock_listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Error binding socket...\n");
        exit(1);
    }
    if (listen(sock_listen_fd, BACKLOG) < 0) {
        perror("Error listening on socket...\n");
        exit(1);
    }
    printf("io_uring echo server listening for connections on port: %d\n", portno);

    // initialize io_uring
    struct io_uring_params params;
    struct io_uring ring;
    memset(&params, 0, sizeof(params));

    if (io_uring_queue_init_params(2048, &ring, &params) < 0) {
        perror("io_uring_init_failed...\n");
        exit(1);
    }

    // check if IORING_FEAT_FAST_POLL is supported
    if (!(params.features & IORING_FEAT_FAST_POLL)) {
        printf("IORING_FEAT_FAST_POLL not available in the kernel, quiting...\n");
        exit(0);
    }

    // check if buffer selection is supported
    struct io_uring_probe *probe;
    probe = io_uring_get_probe_ring(&ring);
    if (!probe || !io_uring_opcode_supported(probe, IORING_OP_PROVIDE_BUFFERS)) {
        printf("Buffer select not supported, skipping...\n");
        exit(0);
    }
    free(probe);

    // register buffers for buffer selection
    struct io_uring_sqe *sqe;
    struct io_uring_cqe *cqe;

    sqe = io_uring_get_sqe(&ring);
    io_uring_prep_provide_buffers(sqe, bufs, MAX_MESSAGE_LEN, BUFFERS_COUNT, group_id, 0);

    io_uring_submit(&ring);
    io_uring_wait_cqe(&ring, &cqe);
    if (cqe->res < 0) {
        printf("cqe->res = %d\n", cqe->res);
        exit(1);
    }
    io_uring_cqe_seen(&ring, cqe);

    // add first accept SQE to monitor for new incoming connections
    add_accept(&ring, sock_listen_fd, (struct sockaddr *)&client_addr, &client_len, 0);

    // start event loop
    while (1) {
        io_uring_submit_and_wait(&ring, 1);
        struct io_uring_cqe *cqe;
        unsigned head;
        unsigned count = 0;

        // go through all CQEs
        io_uring_for_each_cqe(&ring, head, cqe) {
            ++count;
            struct conn_info conn_i;
            memcpy(&conn_i, &cqe->user_data, sizeof(conn_i));

            int type = conn_i.type;
            if (cqe->res == -ENOBUFS) {
                fprintf(stdout, "bufs in automatic buffer selection empty, this should not happen...\n");
                fflush(stdout);
                exit(1);
            } else if (type == PROV_BUF) {
                if (cqe->res < 0) {
                    printf("cqe->res = %d\n", cqe->res);
                    exit(1);
                }
            } else if (type == ACCEPT) {
                int sock_conn_fd = cqe->res;
                // only read when there is no error, >= 0
                if (sock_conn_fd >= 0) {
                    add_socket_read(&ring, sock_conn_fd, group_id, MAX_MESSAGE_LEN, IOSQE_BUFFER_SELECT);
                }

                // new connected client; read data from socket and re-add accept to monitor for new connections
                add_accept(&ring, sock_listen_fd, (struct sockaddr *)&client_addr, &client_len, 0);
            } else if (type == READ) {
                int bytes_read = cqe->res;
                int bid = cqe->flags >> 16;
                if (cqe->res <= 0) {
                    // read failed, re-add the buffer
                    add_provide_buf(&ring, bid, group_id);
                    // connection closed or error
                    close(conn_i.fd);
                } else {
                    // bytes have been read into bufs, now add write to socket sqe
                    add_socket_write(&ring, conn_i.fd, bid, bytes_read, 0);
                }
            } else if (type == WRITE) {
                // write has been completed, first re-add the buffer
                add_provide_buf(&ring, conn_i.bid, group_id);
                // add a new read for the existing connection
                add_socket_read(&ring, conn_i.fd, group_id, MAX_MESSAGE_LEN, IOSQE_BUFFER_SELECT);
            }
        }

        io_uring_cq_advance(&ring, count);
    }
}

void add_accept(struct io_uring *ring, int fd, struct sockaddr *client_addr, socklen_t *client_len, unsigned flags) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_accept(sqe, fd, client_addr, client_len, 0);
    io_uring_sqe_set_flags(sqe, flags);

    conn_info conn_i = {
        .fd = fd,
        .type = ACCEPT,
    };
    memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));
}

void add_socket_read(struct io_uring *ring, int fd, unsigned gid, size_t message_size, unsigned flags) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_recv(sqe, fd, NULL, message_size, 0);
    io_uring_sqe_set_flags(sqe, flags);
    sqe->buf_group = gid;

    conn_info conn_i = {
        .fd = fd,
        .type = READ,
    };
    memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));
}

void add_socket_write(struct io_uring *ring, int fd, __u16 bid, size_t message_size, unsigned flags) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_send(sqe, fd, &bufs[bid], message_size, 0);
    io_uring_sqe_set_flags(sqe, flags);

    conn_info conn_i = {
        .fd = fd,
        .type = WRITE,
        .bid = bid,
    };
    memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));
}

void add_provide_buf(struct io_uring *ring, __u16 bid, unsigned gid) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_provide_buffers(sqe, bufs[bid], MAX_MESSAGE_LEN, 1, gid, bid);

    conn_info conn_i = {
        .fd = 0,
        .type = PROV_BUF,
    };
    memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));
}

4. 性能对比

4.1   存储IO

  • Synchronous I/O、 Libaio和IO_uring 特性对比

  • io_uring和spdk的特性对比

SPDK 全名 Storage Performance Development Kit,是一种存储性能开发套件 。针对于支持nvme协议的SSD设备。是一种高性能的解决方案。

  • io_uring和spdk的性能对比

非polling模式,io_uring相比libaio提升不是很明显;在polling模式下,io_uring能与spdk接近,甚至在queue depth较高时性能更好,性能超越libaio。

在queue depth较低时有约7%的差距,但在queue depth较高时基本接近。

对比结论:

io_uring在非polling模式下,相比libaio,性能提升不是非常显著。

io_uring在polling模式下,性能提升显著,与spdk接近,在队列深度较高时性能更好。

4.2   网络IO

  • Epoll 性能对比

与epoll的性能对比差异还是很大的,参考这篇文章的数据  https://juejin.cn/post/7074212680071905311

测试环境:wsl2,内核版本5.10.60.1,发行版为Debian

硬件:I5-9400,16gDDR4

使用webbench进行简易测试,模拟10500、30500台客户端,持续时间为5s,分别在正常访问和不等待返回两种模式下进行测试,两个客户端均关闭日志记录,epoll开启双ET模式,比较每分钟发送页面数,结果如下:

对比结论:

毋庸置疑,碾压性的结果。

5. 总结

得益于精妙的设计,io_uring的性能基本超越linux 内核以往任何软件层面的IO解决方案,达到了与硬件级解决方案媲美的性能。io_uring 需要较高版本的内核支持,目前还没有大面积普及,但可以预料他是 linux 内核 IO未来的核心发展方向。

相关推荐

从中心调度到自驱协作多 Agent 架构的三次反转|得物技术

本文探讨多Agent系统中的控制权分配问题,提出三大协作架构:Master Agent(中心调度)、任务看板(共享状态)与Advisor(轻量咨询)。通过划清上下文边界与模型边界,强调应以信息相关性而非职业标签划分Agent。三种模式分别将决策权集中于中心、下放至共享状态、再下沉至执行者,形成控制权逐步下沉的演进路径。案例显示,合理设计可提升复杂任务的并行性、弹性与可维护性。

SmartCodeTech的博客 397

Jev 实战:为AI工作流引入轻量决策层 x10倍提速|得物技术

完成这三个场景后,我对适用边界有了更明确的认识:它们都可以先定义答案范围,再根据判断结果驱动下一步操作。接入时,我会优先明确输入、选项、采纳条件和兜底路径,再评估Jev是否适合承担这一环节。结合这些实践,我更容易理解Jev(TypeSafe System One)作为专用结构化决策模型不生成自然语言,而是返回带概率的判断结果。后续接入时,我会继续从脚本中边界明确的判断任务开始,分别验证准确性、耗时和费用,再决定是否扩大使用范围。下面整理了相关能力、限制和接入要点,便于查阅。

SmartCodeTech的博客 342

国产算力·昇腾 910C 大模型推理优化(下):源码修改与算子优化,提速30%|得物技术

本文延续上篇,深入vllm-ascend源码优化:通过多流重叠实现MoE共享专家并行计算,消除冗余事件开销,并解除与FUSED_MC2的互斥限制,实现c=1下TPOT从8.2ms降至8.0ms;优化decode阶段host侧开销,引入状态跟踪跳过冗余拷贝;设计Block Table CPU快路径,规避小batch下NPU kernel启动延迟;针对GDN层融合L2Norm至recurrent kernel,减少算子调用与内存往返。综合优化后推理性能显著提升,形成可复用的昇腾910C大模型推理优化方法论。

SmartCodeTech的博客 227

国产算力·昇腾 910C 大模型推理优化(上):镜像更新与参数调优,提速 40%|得物技术

文中详细给出每一项优化的动机、实现(含具体源码改动路径与关键代码片段)、验证数据与踩坑记录(包括十余项实测无效或负向的尝试),希望为同样在国产算力上做推理优化的团队提供一份可直接复用的工程参考。

SmartCodeTech的博客 214

动态规则的 Agent 实践:用确定性工程驾驭 LLM 的资产智能验收

面对财务验收中规则多变、材料非结构化、核对复杂等痛点,传统规则引擎难以应对。系统采用“规则即配置”思路,由大语言模型(LLM)负责理解合同、扫描件等非结构化材料并执行语义比对,工程系统则保障流程编排与可追溯性。通过异步处理、幂等设计、三类流量隔离及双通道(文本/图像)处理,实现高效稳定运行。结合用户反馈构建自动标准答案,辅以试跑、回测与发布门禁机制,形成闭环质量体系。底层采用确定性工作流架构,确保行为可预期、可测试,真正实现灵活规则的快速迭代与安全上线。

SmartCodeTech的博客 342

发一张图,它居然“跃“出屏幕了?一文读懂得物 3D 空间图片技术实践

3D 空间图片功能的落地,涉及模型生成、内容分发、客户端渲染之间的一系列取舍。本文重点分享如何生产出高质量、小体积的模型,以及如何在手机上流畅运行。

SmartCodeTech的博客 555

别再只卷向量检索了,得物交易搜索如何用“生成式”实现召回范式跃迁?

你搜"af1",它得给你"空军一号"。这一年,得物交易搜索把召回从"做选择题"改成了"写答案"——不再从商品池里"找"最像的,而是让模型自己"生成"你真正想要的。从 LLM 语义索引到 HLLM、语义 ID,搜索是怎么从"检索"走向"生成"的?

SmartCodeTech的博客 593

指标平台:从语义底座到智能消费的实践路径|得物技术

这篇文章想做的,不是在本体化的宏大叙事上再叠加一层理论,而是把我们在真实落地中验证过的路径讲清楚:语义底座怎么从零建立起来、怎么保证质量不失控、怎么支撑 Text2SQL 和 AI Coding 两条消费主线,以及沿着这条路径往前走,下一步该往哪个方向发力。

SmartCodeTech的博客 314

企业级 MultiAgent 落地:Plan 模式与主子 Agent 协作|得物技术

基于 AgentScope Java 的企业级 MultiAgent 平台,为研发、数据分析和知识问答等场景提供统一的模型调用、工具接入、任务编排、流式输出与运行追踪能力。

SmartCodeTech的博客 299

得物小摊 AI Native 演进实录:用 Harness 构建可控 AI 交付

AI Coding 一个人用很顺手,进了团队就漏:指令没人审、经验难沉淀、过程看不见、链路不闭环。作者团队基于 Multica 做了 EP-Harness,把 Agent 当成员管,给它套一层可执行、可观察、可治理的 Harness,并让反馈闭环。成果:可追溯决策、多 Agent 协作,高频异常从每 4 小时 2400 多条降到个位数,对得物技术补上团队级 AI 协作关键一环。

SmartCodeTech的博客 539

企业级 MultiAgent 的记忆系统:短期上下文与四层记忆架构实现|得物技术

在 MultiAgent 平台中,Agent 的"记忆"能力是实现智能对话和个性化服务的核心资产。本文深入剖析 Agent 记忆系统的设计理念与实现细节,重点解析长期记忆架构的写入与加载流程,涵盖 LLM 智能判断、语义检索、Token 预算控制、冲突处理等关键技术,为开发者提供完整的技术实现参考。

SmartCodeTech的博客 502

EP-Harness:从个人 AI Coding 到团队级 Agent 工作流|得物技术

AI Coding 个人用得爽,一到团队就崩。EP-Harness 把 Agent 从聊天工具变成团队协作成员——可管理、可追溯、可闭环。落地后高频异常从 2400+/4h 收敛到个位数。

SmartCodeTech的博客 688

得物知识问答:复合检索 Agent 的系统设计实践

从"搜到什么用什么"到"搜→筛→挖→验"自主决策闭环——我们造了一个有判断力的 RAG Agent,不是套壳。

SmartCodeTech的博客 732

实战从零开始构建一个Coding Agent:Violin |得物技术

理解了 coding agent 的构建原理,也就掌握了理解其他 agent 的一把钥匙。笔者对每天都在使用的 agent 原理很感兴趣,于是决定自己写一个:violin。

SmartCodeTech的博客 926

AI Native 交易核心系统的研发范式|得物技术

如何改造旧研发流程适配 AI 编码?本文提出五道标准化关口治理思路,依托 Claude Code Spec-Driven 插件打通需求澄清、方案设计、TDD 落地、门禁管控与埋点监控,构建带人工确认门控的完整工作流,以五层自底向上架构实现 AI 编码规范化落地。

SmartCodeTech的博客 971

推荐系统体验的数字化突破:得物自动化评测平台的技术实践|AICon 文章整理

「得物推荐 AI Harness 工程化实践系列」的末篇内容,本系列共三篇连载。本篇(末篇)详解得物推荐评测平台的完整技术体系:从主观体验量化到大模型自动化评测流水线,涵盖多维度指标设计、人机协作提示词校验闭环、分布式任务调度与成本精控等核心工程实现,以及单周百万级审计需求下实现 91% 资源成本优化的工业级落地实战细节。

SmartCodeTech的博客 1492

RAG 核心概念与原理:Chunking、Embedding、相似度、HNSW 与多路召回|得物技术

决定 RAG 效果的关键不在大模型,而是文本切块、查询改写、多路召回与精排。合理切块存好知识库内容,向量匹配抓语义、BM25 匹配关键词互补。先优化用户提问,快速召回一批相关文档,再用精排模型精细筛选,整套检索流程配合到位,问答准确度才会明显提升。

SmartCodeTech的博客 1079

从“机械应答“到“服务伙伴“:得物高可控智能客服的 Agent 工程实践|AICon 演讲整理

本次分享围绕以下核心技术展开:客服 Agent 挑战与架构设计(Single-Agent→Multi-Agent→Harness);高可控性数据飞轮(PE 自动化 + DPO 训练);RL 策略训练和人工经验对齐;多轮半双工消息流控制。

SmartCodeTech的博客 743

得物推荐系统诊断 Agent:从 “调接口” 到 “会思考”|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。AICon上海2026_全球人工智能开发与应用大会暨大模型应用生态展_InfoQ技术大会「得物推荐 AI Harness 工程化实践系列」的中篇内容,本系列共三篇连载。本篇(中篇)进一步详解得物推荐复杂业务场景下,AI代码从生成、防护校验到安全上线的完整技术体系、自研AI Harness整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。

SmartCodeTech的博客 1361

得物 OceanBase 落地实践

得物选型OceanBase多模数据库应对业务挑战,通过验证其高可用架构、存储压缩技术及混合负载处理能力。测试显示OceanBase在复杂查询场景性能较MySQL提升130-200倍,在高并发TP/AP混合场景表现优异,存储成本降低40%+。通过DBA内部报表业务试点验证其稳定性后,逐步推广至核心业务场景,优化数据库技术栈,实现性能与成本的双重收益。

SmartCodeTech的博客 1380

AI UITester:AI Native 的 UI 自动化测试新范式|得物技术

ai_uitester 代表了 AI Native 的 UI 自动化测试新范式:这不仅是工具的升级,而是测试范式的转变 ------ 从 "代码驱动" 转向 "视觉驱动",从 "人工调试" 转向 "AI 自愈",从 "三端分离" 转向 "统一抽象"。

SmartCodeTech的博客 773

从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。「得物推荐 AI Harness 工程化实践系列」的开篇内容,本系列共三篇连载。将系统拆解得物推荐复杂业务场景下,AI 代码从生成、防护校验到安全上线的完整技术体系,逐一详解自研 AI Harness 整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。本篇(上篇):将整体介绍团队自研的 AI Harness 体系,包括建设思路、全生命周期防护机制、混合智能体架构及落地效果。

SmartCodeTech的博客 1079

从表单到 Agent:得物社区活动搭建的 AI 实践之路

一场营销活动从策划到上线,运营要在三个系统间跳转 10 + 次、填写 40 + 个字段。我们用 AI 重新设计了这条链路 —— 从 “AI 帮你填表单” 到 “两阶段 Agent + 聚合工作台”。这篇文章记录的不是技术细节,而是这条路上的选择和反思。

SmartCodeTech的博客 740

从埋点需求到规则资产:Hermes Agent 重构得物数仓工作流

埋点和指标需求中,消耗数据承接方的不是生成答案,而是补齐证据链、对齐历史口径、识别下游影响,并在生产写入前留下可追溯的确认记录。本文数据承接方指承接业务需求的数据同学或业务合作伙伴;后文讨论内部研发、元数据、查询和门禁能力在流程中的角色。

SmartCodeTech的博客 655

让 Claude Code 拥有自我进化和记忆系统|得物技术

本文记录了为 Claude Code 构建的一套持久化记忆与自我学习系统的设计思路与实现细节。这套系统让 AI 助手能够跨越会话边界记住用户习惯,并在每次对话结束后自动提炼行为规律和系统信息,通过向量检索快速提取关键信息,下一次对话时主动应用,逐步进化为更懂你的编程伙伴。

SmartCodeTech的博客 993

用 LLM Agent 重构告警排查流程|得物技术

告警排查需频繁切换日志、APM、链路追踪等多个平台,耗时 10~30 分钟,且高度依赖个人经验。我们做了 Troubleshooter——用 LLM Agent 自动完成数据采集、根因分析和处置建议生成。上线后中位数排查耗时从 20 分钟降到 4.4 分钟,覆盖 11 个服务和 10+ 种告警类型。

SmartCodeTech的博客 1002

HorizonVault 技术深潜:如何在 HDD 上做出 100GB/s+ 级大吞吐分布式存储|得物技术

HorizonVault是一款面向Kafka远程存储的高吞吐分布式存储引擎,通过分布式并行写入和状态感知调度解决冷数据存储的三大核心问题:吞吐瓶颈、性能抖动和恢复能力。系统采用分层架构,Meta负责全局资源调度,Broker处理请求路由,Store实现本地磁盘管理,形成完整的写入治理链路。通过将HDD磁盘提升为一等资源,实现顺序追加写入、多盘并行和慢盘隔离,在保证高吞吐的同时控制延迟。存储结构采用DiskStore组织磁盘资源,LogSegment顺序写入数据,IndexSegment定位读取,Change

SmartCodeTech的博客 889

Claude Code Harness 工程:数仓侧落地方案|得物技术

Harness 工程通过职责分层实现研发流水线可靠性提升。Claude 负责需求理解与代码生成等语义层工作;hooks 负责规范检查等确定性执行;Subagents 隔离血缘查询、自测等高 token 操作;Memory 承载跨会话状态持久化。四层协同使数仓 AI 开发从"对话式辅助"转变为"规则驱动的自动化流程"。

SmartCodeTech的博客 1089

BP Claw 破解 AI 编码输入难题 ——FlinkSpec 需求智能化实践|得物技术

本文为 FlinkSpec 系列之开篇,亦是这场工程化变革的序章。BP Claw 所立之处,不过链路之源。FlinkSpec 所图,乃以 AI 之力,将实时数仓从需求落地至验收上线的全程工序,熔铸为一套精密自洽、生生不息的智能工程体系。宏图未竟,后续系列将逐章揭幕,敬请期待!

SmartCodeTech的博客 1034

基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术

Harness(约束)思维的本质是:给 AI 一个已有的实现作为参照,让它照着复刻一份,而不是凭空创造。就像给一个新入职的工程师说 "你照着这个模块的风格,写一个类似的",而不是 "你自由发挥"------ 前者往往能更快产出符合团队规范的代码。

SmartCodeTech的博客 1181
上一篇: 得物 API一站式协作平台的一些思考
下一篇: 如何从1到99做好产品 | 得物技术
得物技术
得物技术 得物技术官方账号 得物技术官方账号
博客等级 码龄6年 2492粉丝 407原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值