HAProxy中描述符管理

本文深入解析 HAProxy 中的描述符管理机制,包括数据结构设计、事件检测及处理流程,强调其如何通过优化数据结构和状态管理,实现高效性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在包含网络通讯的程序中,都需要管理描述符。描述符的管理也是服务器程序性能的关键点之一。很多开源软件,比如libevent和HAProxy都有自己的方式。但是他们都有相似的地方,并且目标都是一致的:把性能发挥到极致。因为当前的工作中也使用到了网络通讯,因此希望能从这些开源软件中借鉴学习一些东西,并将他们汇总起来。下面描述一下我对HAProxy的描述符管理的理解和总结。
HAProxy使用统一的接口对描述符进行管理,不同的事件检测接口,提供不同的实现方式。描述符的管理一般包括增删改查,并在此基础上实现事件的触发和处理。
HAProxy的描述符相关代码主要在include/types/fd.h、include/proto/fd.h和src/fd.c中。

数据结构

为了性能的考虑,首先要设计一个合适的数据结构。HAProxy使用一个数组存放描述符,在fd.c中有如下定义:

struct fdtab *fdtab = NULL;     /* array of all the file descriptors */

HAProxy直接使用fd(描述符)的值做索引,能够很快的检索到描述符信息。不管是增删改查,效率都是O(1),但这也是用空间换时间,不过还好,系统在给进程分配描述符的时候都是从0开始的,并且会优先从小的未使用的开始(至少Linux是这样)。
struct fdtab的定义如下:

struct fdtab {
    int (*iocb)(int fd);                 /* IO处理器,返回 FD_WAIT_* */
    void *owner;                         /* the connection or listener associated with this fd, NULL if closed */
    unsigned int  cache;                 /* position+1 in the FD cache. 0=not in cache. */
    unsigned char state;                 /* FD state for read and write directions (2*3 bits) */
    unsigned char ev;                    /* event seen in return of poll() : FD_POLL_* */
    unsigned char new:1;                 /* 1 if this fd has just been created */
    unsigned char updated:1;             /* 1 if this fd is already in the update list */
    unsigned char linger_risk:1;         /* 1 if we must kill lingering before closing */
    unsigned char cloned:1;              /* 1 if a cloned socket, requires EPOLL_CTL_DEL on close */
};

HAProxy中的poller

poller的定义

HAProxy的描述符数据结构设计的很好,实现了最快的增删改查,但是描述符的最重要的功能是检测新事件的发生。在众多操作系统中,都提供一些异步检测机制,比如poll、epoll等,HAProxy统称之为poller:

struct poller {
    void   *private;                                     /* any private data for the poller */
    void REGPRM1   (*clo)(const int fd);                 /* mark <fd> as closed */
        void REGPRM2   (*poll)(struct poller *p, int exp);   /* the poller itself */
    int  REGPRM1   (*init)(struct poller *p);            /* poller initialization */
    void REGPRM1   (*term)(struct poller *p);            /* termination of this poller */
    int  REGPRM1   (*test)(struct poller *p);            /* pre-init check of the poller */
    int  REGPRM1   (*fork)(struct poller *p);            /* post-fork re-opening */
    const char   *name;                                  /* poller name */
    int    pref;                                         /* try pollers with higher preference first */
};

pref:默认为0,意思是这个poller不可用。init函数失败的时候要设置为0,term函数也要设置为0.一个普通的未优化的select() poller要设置为100。
pref类似于一个poller的权重,值越大,权重越大。在使用时,优先选用权重较大的。haproxy支持的有epoll, kqueue, poll和select,他们的pref分别是300,300,300和150。
private:init函数初始化时创建的,在term中销毁
clo:poller标记fd被关闭的

HAProxy使用一个数组记录所有的poller:

struct poller pollers[MAX_POLLERS];
int nbpollers = 0;  // 记录当前poller的个数

poller的初始化

这些poller的初始化很奇特,都是在main函数之前做的,比如poll类型的poller。

__attribute__((constructor))
static void _do_register(void)
{
    struct poller *p;

    if (nbpollers >= MAX_POLLERS)
        return;
    p = &pollers[nbpollers++];  // 在这里加到全局数组中,nbpoller记录个数

    p->name = "poll";
    p->pref = 200;
    p->private = NULL;

    p->clo  = __fd_clo;
    p->test = _do_test;
    p->init = _do_init;
    p->term = _do_term;
    p->poll = _do_poll;
}

根据代码中的介绍,__attribute__((constructor))的含义是这是一个在main函数前调用的构造函数,是一种GCC(版本2.95或以后)特性。当然,这种函数中不能使用写一些未初始化的数据。
找一个poller的例子来观察描述符管理者的行为,比如poll类型的poller。
首先看看这么多poller都是怎么初始化出来的,并假设最后选取了poll类型的poller。
在fd.c中,有poller的初始化函数:

int init_pollers()
{
    int p;
    struct poller *bp;
    // fd_cache和fd_updt也是管理描述符用的数据结构,慢慢说。
    if ((fd_cache = (uint32_t *)calloc(1, sizeof(uint32_t) * global.maxsock)) == NULL)
        goto fail_cache;

    if ((fd_updt = (uint32_t *)calloc(1, sizeof(uint32_t) * global.maxsock)) == NULL)
        goto fail_updt;

    // 就是用一个循环,按照poller的pref值作为权重,选取最好的poller
    // 然后复制到当前的一个全局变量cur_poller中,以后所有的执行,
    // 都是调用cur_poller,不明白为啥不把写成指针类型。
    do {
        bp = NULL;
        // 找到一个pref最大的poller
        // pollers是在编译时就添加的,比如在ev_poll.c中
        for (p = 0; p < nbpollers; p++)
            if (!bp || (pollers[p].pref > bp->pref))
                bp = &pollers[p];

        if (!bp || bp->pref == 0)
            break;

        if (bp->init(bp)) {
            memcpy(&cur_poller, bp, sizeof(*bp));
            return 1;
        }
    } while (!bp || bp->pref == 0);
    return 0;

 fail_updt:
    free(fd_cache);
 fail_cache:
    return 0;
}

选取出了最好的poller,接下来就可以看下poller是怎么工作的(假设大家已经知道了poll的工作方式,如果不知道可以参考http://blog.chinaunix.net/uid-23722297-id-1624846.html)。

1. poll类型poller的初始化

static int _do_init(struct poller *p)
{
    \_\_label__ fail_swevt, fail_srevt, fail_pe;
    int fd_evts_bytes;

    p->private = NULL;
    // global.maxsock在haproxy.c中初始化为10
    fd_evts_bytes = (global.maxsock + sizeof(**fd_evts) - 1) / sizeof(**fd_evts) * sizeof(**fd_evts);

    // 这个就是存放每次需要检测的描述符和事件类型的数组
    poll_events = calloc(1, sizeof(struct pollfd) * global.maxsock);

    if (poll_events == NULL)
        goto fail_pe;

    // fd_evts[DIR_RD]和fd_evts[DIR_WR]是两个数组,分别存放对应的描述符
    // 是否有读/写事件,也是以描述符作为索引 
    if ((fd_evts[DIR_RD] = calloc(1, fd_evts_bytes)) == NULL)
        goto fail_srevt;

    if ((fd_evts[DIR_WR] = calloc(1, fd_evts_bytes)) == NULL)
        goto fail_swevt;

    return 1;

 fail_swevt:
    free(fd_evts[DIR_RD]);
 fail_srevt:
    free(poll_events);
 fail_pe:
    p->pref = 0;
    return 0;
}

其中fd_evts[DIR_RD]和fd_evts[DIR_WR]的定义如下:
static unsigned int *fd_evts[2]; // 一个用来读,一个用来写
0是读(宏定义DIR_RD),1是写(宏定义DIR_WR)fd_evts[DIR_RD]和fd_evts[DIR_WR]是两个数组,分别存放对应的描述符是否有读/写事件,也是以描述符作为索引,不过是bit位,就是一个字节8个数据。

2. 异步事件检测: poll操作

这里面会牵涉到一些陌生的东西,描述的会比较混乱,不过总体还是很简单的,如果耐心一点,花两三分钟,也是能够理解的。

001 static void _do_poll(struct poller *p, int exp)
002 {
003     int status;
004     int fd, nbfd;
005     int wait_time;
006     int updt_idx, en, eo;
007     int fds, count;
008     int sr, sw;
009     unsigned rn, wn; /* read new, write new */
010 
011     // 从更新列表中查找是否有需要重新poll检测的描述字
012     for (updt_idx = 0; updt_idx < fd_nbupdt; updt_idx++) {
013         fd = fd_updt[updt_idx]; // 更新列表一样是将fd作为索引
014         fdtab[fd].updated = 0;
015         fdtab[fd].new = 0;
016 
017         if (!fdtab[fd].owner)
018             continue;
019 
020         eo = fdtab[fd].state;
021         en = fd_compute_new_polled_status(eo); // 判断是否需要执行poll操作
022 
023         if ((eo ^ en) & FD_EV_POLLED_RW) {  // 判断需要检测读写事件
024             fdtab[fd].state = en;
025 
026             // 根据具体的值修改数据,取消读、增加读或者取消写、增加写
027             if ((eo & ~en) & FD_EV_POLLED_R)
028                 hap_fd_clr(fd, fd_evts[DIR_RD]);
029             else if ((en & ~eo) & FD_EV_POLLED_R)
030                 hap_fd_set(fd, fd_evts[DIR_RD]);
031 
032             if ((eo & ~en) & FD_EV_POLLED_W)
033                 hap_fd_clr(fd, fd_evts[DIR_WR]);
034             else if ((en & ~eo) & FD_EV_POLLED_W)
035                 hap_fd_set(fd, fd_evts[DIR_WR]);
036         }
037     }
038     fd_nbupdt = 0;
039 
040     // 将描述符复制到待检测的struct pollfd 数组中
041     nbfd = 0;
042     for (fds = 0; (fds * 8*sizeof(**fd_evts)) < maxfd; fds++) {
043         rn = fd_evts[DIR_RD][fds];
044         wn = fd_evts[DIR_WR][fds];
045       
046         // 一个字节表示8个数据,一次性可以跳过8个
047         if (!(rn|wn))
048             continue;
049 
050         for (count = 0, fd = fds * 8*sizeof(**fd_evts); count < 8*sizeof(**fd_evts) && fd < maxfd; count++, fd++) {
051             sr = (rn >> count) & 1;
052             sw = (wn >> count) & 1;
053             if ((sr|sw)) {
054                 poll_events[nbfd].fd = fd;
055                 poll_events[nbfd].events = (sr ? POLLIN : 0) | (sw ? POLLOUT : 0);
056                 nbfd++;
057             }
058         }         
059     }
060       
061     // 这些是无关的先不关心
062     if (fd_cache_num || run_queue || signal_queue_len)
063         wait_time = 0;
064     else if (!exp)
065         wait_time = MAX_DELAY_MS;
066     else if (tick_is_expired(exp, now_ms))
067         wait_time = 0;
068     else {
069         wait_time = TICKS_TO_MS(tick_remain(now_ms, exp)) + 1;
070         if (wait_time > MAX_DELAY_MS)
071             wait_time = MAX_DELAY_MS;
072     }
073 
074     // 开始做poll操作
075     gettimeofday(&before_poll, NULL);
076     status = poll(poll_events, nbfd, wait_time);
077     tv_update_date(wait_time, status);
078     measure_idle();
079 
080     // 根据检测的结果更新缓存
081     for (count = 0; status > 0 && count < nbfd; count++) {
082         int e = poll_events[count].revents;
083         fd = poll_events[count].fd;
084       
085         if (!(e & ( POLLOUT | POLLIN | POLLERR | POLLHUP )))
086             continue;
087 
088         /* ok, we found one active fd */
089         status--;
090 
091         if (!fdtab[fd].owner)
092             continue;
093 
094         fdtab[fd].ev &= FD_POLL_STICKY; // 清掉除了错误标记外的标记
095         // 看似多次一举,其实这是HAProxy根据GCC的特性做的一个优化,
096         // 在GCC中常数的比较可以直接在编译期间优化,当然如果这些常数是相等的话
097         // HAProxy不放过一个优化的细节
098         if (POLLIN == FD_POLL_IN && POLLOUT == FD_POLL_OUT &&  
099             POLLERR == FD_POLL_ERR && POLLHUP == FD_POLL_HUP) {
100             fdtab[fd].ev |= e & (POLLIN|POLLOUT|POLLERR|POLLHUP);
101         }
102         else {
103             fdtab[fd].ev |=
104                 ((e & POLLIN ) ? FD_POLL_IN  : 0) |
105                 ((e & POLLOUT) ? FD_POLL_OUT : 0) |
106                 ((e & POLLERR) ? FD_POLL_ERR : 0) |
107                 ((e & POLLHUP) ? FD_POLL_HUP : 0);
108         }
109 
110         // 检测描述符的读写事件,更新fd_cache
111         if (fdtab[fd].ev & (FD_POLL_IN | FD_POLL_HUP | FD_POLL_ERR))
112             fd_may_recv(fd);
113 
114         if (fdtab[fd].ev & (FD_POLL_OUT | FD_POLL_ERR))
115             fd_may_send(fd);
116     }
117 
118 }

这个函数有点长,不过这还不是HAProxy中算长的函数,HAProxy中有的函数长达千行!函数虽然长,逻辑还是很清晰的,虽然里面有些结构的含义不清楚,但是很容易猜到。
函数中最主要的步骤可以分为下面步:
1. 从更新列表中查找是否有需要使用poll做异步事件检测的描述符;
2. 将待检测的描述符复制到struct pollfd数组中并调用poll系统接口;
3. 根据系统返回结果,更新描述符关联的数据fd_cache。
这个函数中涉及到了描述符管理时用到的两个数据:fd_updt和fd_cache。这也是两个数组,但不是以描述符为索引的数组了。里面每个元素就是一个描述符,分别存储”需要做事件检测处理的描述符(即需要做poll检测)”和”当前有哪些描述符有事件需要处理(即当前有数据可读或可写,或者连接关闭、新连接等)”。其中struct fdtab使用一个字段cache记录一个描述符在fd_cache中的索引位置。同时对应的有两个整数,表示两个数组的大小:fd_nbupdt和fd_cache_num。
了解了这两个数据,在看struct fdtab.state成员。从表面含义上看,它指一个描述符关联的状态,对,就是一个状态。HAProxy对此做了详细的说明(HAProxy这点做得特别好,代码的注释写的非常详细)。但是为了容易简单,可以先将这个状态理解为fdtab用来记录fd当前的读写状态,比如是否可读,是否可写。
理解了这三个数据,再回过头来读一遍代码就很容易理解了。代码第23行,根据原先的状态判断它之后的状态(这个也许不容易明白他是怎么运作的,后面会有更详细的解释,包含了HAProxy对状态的描述),判断是否需要做poll操作,包括读和写,并更新fd_evts中对应的标记位。
检测完更新的描述符,根据刚计算出来的需要检测的事件状态,开始准备做poll操作的数据,struct pollfd数组poll_events。接着就是调用poll(中间忽略了一部分)。poll系统调用会根据描述符关联的信息,判断是否有变更,比如有数据可读,连接关闭,或者收到新连接等,将这些有事件发生的描述符和发生的事件类型(读recv或写send)更新到fd_cache中,函数112和115行。
下面插入一些碎片。

插曲

fdtab.state

一个描述符的事件状态,就是fdtab.state,包含了全部类型。状态字段这样构成的:低4字节表示读(R),高4字节表示写(W),描述如下:

            | 7 |  6 |  5 |  4 |  3|  2 |  1 |  0 |
            | 0 | PW | RW | AW | 0 | PR | RR | AR |

                  A* = active     *R = read
                  P* = polled     *W = write
                  R* = ready

当描述符可用时标记为”active”;
在polling(需要异步事件检测)时标记为”polled”;
当最近一次唤醒时没有遇到EAGAIN的错误时标记为”ready”(不管polling是否改变,这只是上次EAGAIN的缓存)。
(可能这些不好理解,但是看到后面代码执行的动作后,就很好理解了。)
基于这3个标记,对每种类型(读和写)就可以有8中可能的状态:

  +---+---+---+----------+---------------------------------------------+
  | P | R | A | State    | Description                                 |
  +---+---+---+----------+---------------------------------------------+
  | 0 | 0 | 0 | DISABLED | No activity desired, not ready.             |
  | 0 | 0 | 1 | MUSTPOLL | Activity desired via polling.               |
  | 0 | 1 | 0 | STOPPED  | End of activity without polling.            |
  | 0 | 1 | 1 | ACTIVE   | Activity desired without polling.           |
  | 1 | 0 | 0 | ABORT    | Aborted poll(). Not frequently seen.        |
  | 1 | 0 | 1 | POLLED   | FD is being polled.                         |
  | 1 | 1 | 0 | PAUSED   | FD was paused while ready (eg: buffer full) |
  | 1 | 1 | 1 | READY    | FD was marked ready by poll()               |
  +---+---+---+----------+---------------------------------------------+

为这些状态转换提供的接口也很简单:

fd_want_*() : set flag A
fd_stop_*() : clear flag A
fd_cant_*() : clear flag R (when facing EAGAIN)
fd_may_*() : set flag R (upon return from poll())
sync() : if (A) { if (!R) P := 1 } else { P := 0 }

状态PAUSED,ABORT和MUSTPULL是水平触发的poller转换的(关于水平触发和边缘触发,可以搜索,对于poll机制,就是水平触发,epoll通过设置,也可以支持水平触发),并在poller开始时通过sync接口调整。对于事件触发的poller,只有MUSTPOLL状态才会转换,ABORT会直接变成PAUSED。ACTIVE状态是唯一稳定的状态,它的P != A。
READY是一个特殊的状态,可能被poller或cache(有一个fd_cache)激活。一些多层的协议(比如SSL)会需要用到这个状态,因为它们的连接活动不是100%与FD的活动状态关联的(FD的活动状态只是传输层的)。如果开启/关闭一个描述符比较容易,一些poller可能选择使用ACTIVE来实现这个状态。对于一个分配的cache来说,READY和ACTIVE是两个状态。
状态转换图如下。polling开启的状态只有4个。

         (POLLED=0)          (POLLED=1)

         +----------+  sync  +-------+
         | DISABLED | <----- | ABORT |         (READY=0, ACTIVE=0)
         +----------+        +-------+
        clr |  ^           set |  ^
            |  |               |  |
            v  | set           v  | clr
         +----------+  sync  +--------+
         | MUSTPOLL | -----> | POLLED |        (READY=0, ACTIVE=1)
         +----------+        +--------+
               ^          poll |  ^
               |               |  |
               | EAGAIN        v  | EAGAIN
          +--------+         +-------+
          | ACTIVE |         | READY |         (READY=1, ACTIVE=1)
          +--------+         +-------+
        clr |  ^           set |  ^
            |  |               |  |
            v  | set           v  | clr
         +---------+   sync  +--------+
         | STOPPED | <------ | PAUSED |        (READY=1, ACTIVE=0)
         +---------+         +--------+

因为本人英语功底实在有限,翻译成中文可能会有很多错误,原文在src/fd.c的开头注释的地方。

处理fd_cache中的事件

void fd_process_cached_events()
{
    int fd, entry, e;

    for (entry = 0; entry < fd_cache_num; ) {
        fd = fd_cache[entry];
        e = fdtab[fd].state;

        fdtab[fd].ev &= FD_POLL_STICKY;

        // 根据fdtab的状态分析需要处理的事件
        if ((e & (FD_EV_READY_R | FD_EV_ACTIVE_R)) == (FD_EV_READY_R | FD_EV_ACTIVE_R))
            fdtab[fd].ev |= FD_POLL_IN;

        if ((e & (FD_EV_READY_W | FD_EV_ACTIVE_W)) == (FD_EV_READY_W | FD_EV_ACTIVE_W))
            fdtab[fd].ev |= FD_POLL_OUT;

        if (fdtab[fd].iocb && fdtab[fd].owner && fdtab[fd].ev)
            fdtab[fd].iocb(fd); // 调用回调函数
        else
            fd_release_cache_entry(fd);

        // fd_cache的操作机制是删除之后,用最后一个补充上
        if (entry < fd_cache_num && fd_cache[entry] != fd)
            continue;
        entry++;
    }
}

总结

  1. HAProxy利用了描述符都是尽量使用小数字的特性,直接用一个以描述符作为索引的数组,这样操作的效率最高;
  2. 使用”状态”记录和判断当前描述符应该执行什么操作,或者触发什么事件,比如读、写、poll等;
  3. 直接将需要做异步事件检测的描述符放到数组中(fd_updt),poller根据这里的数据更新描述符数据;
  4. poller将检测后需要进一步处理的描述符放到fd_cache数组中,并与fdtab关联。

HAProxy的这些处理,是事件管理的基础,也是程序效率的关键点之一。这也许不适合一些应用场景,但是也有值得借鉴和学习的地方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值