HAProxy几个重要的结构体
上一篇文章《HAProxy的event_accept函数源码分析》(以下简称上文)理顺了HAProxy接收客户端连接的主要流程,遗留下相关的基础设施和数据结构没有深入分析,这一篇先介绍几个重要的结构体。
另外要说明的是,本系列文章对HAProxy的分析都基于目前的稳定版本HAProxy 1.4,而目前的开发版本HAProxy 1.5和1.4相比,重构力度比较大,不少函数名称和结构体都发生变化,但是关键流程还是基本一致的,请各位读者留意。
1. session
上文讲的其实就是HAProxy怎样在客户端和服务端之间建立一个完整的链路,相关信息都保存到一个session结构体中。原本的session结构体有80行,可以说非常冗长。和上文思路一样,本着不追求旁枝末节,以较小的代价描述HAProxy原理的原则,我把它进行简化,去掉了HTTP处理相关的成员和一些实现额外功能的成员,只把必不可少的成员列出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct session {
struct list list; /* position in global sessions list */
struct task *task; /* the task associated with this session */
int conn_retries; /* number of connect retries left */
int flags; /* some flags describing the session */
struct buffer *req; /* request buffer */
struct buffer *rep; /* response buffer */
struct stream_interface si[2]; /* client and server stream interfaces */
struct sockaddr_storage cli_addr; /* the client address */
struct sockaddr_in srv_addr; /* the address to connect to */
struct server *srv; /* the server the session will be running or has been running on */
struct server *prev_srv; /* the server the was running on, after a redispatch, otherwise NULL */
}; |
其中,task、req、rep和si在上文已经有所提及,下面也会有进一步解释;conn_retries、flags、cli_addr和srv_addr甚至不用看注释就明白其意义;srv和prev_srv也很明显,一个指向将要执行或正在执行的后端服务器结构体,一个指向上一次曾经执行的后端服务器结构体。
list也是一个结构体,包含n和p两个指向struct list类型的指针,详见mini-clist.h。它在这里的作用是把各个session结构体串起来,形成 一个双向链表,如下图所示。这是包括HAProxy和Linux内核广泛使用的一种数据结构。我认为用在这里倒不是很必要,不过仍然是一种值得学习的技巧。
值得注意的是,这种链表是嵌入到各个结构体中使用,p和n两个指针成员各自指向前、后节点节点的list成员,而非前、后节点本身。
2. task
task结构体的主要成员如下:
1 2 3 4 5 6 7 8 | struct task {
struct eb32_node wq; /* ebtree node used to hold the task in the wait queue */
struct eb32_node rq; /* ebtree node used to hold the task in the run queue */
int state; /* task state : bit field of TASK_* */
int expire; /* next expiration date for this task, in ticks */
struct task * (*process)(struct task *t); /* the function which processes the task */
void *context; /* the task's context */
}; |
其中,state和expire都是顾名思义;process是函数指针,根据task的类型会执行不同的处理函数,以后的文章再叙述;context指向与task关联的结构体,在event_accept函数中,context指向的就是session。
重点讲一下wq和rq,它们是两个使用32位键的ebtree节点(以下简称ebnode),分别代表该task与等待队列和运行队列的关系。在第一篇文章《HAProxy的独门武器:ebtree》中,因为没有具体例子印证,有一个关于ebnode的特点没有交代:一个ebnode在ebtree中,当且仅当该ebnode的leaf_p不为空。这一点可以从ebnode的插入、删除操作中推导得出。
因此,判断一个task是否在运行队列中就是:
1 2 3 4 | static inline int task_in_rq(struct task *t)
{
return t->rq.node.leaf_p != NULL;
} |
同样,判断一个task是否在等待队列中就是:
1 2 3 4 | static inline int task_in_wq(struct task *t)
{
return t->wq.node.leaf_p != NULL;
} |
从运行队列中删除一个task:
1 2 3 4 5 | static inline struct task *__task_unlink_rq(struct task *t)
{
eb32_delete(&t->rq);
return t;
} |
从等待队列中删除一个task,这里有个优化,last_timer指向最后一个入队列的task(稍后再介绍):
1 2 3 4 5 6 7 | static inline struct task *__task_unlink_wq(struct task *t)
{
eb32_delete(&t->wq);
if (last_timer == &t->wq)
last_timer = NULL;
return t;
} |
添加task到运行队列,也就是唤醒该task,上文的event_accept函数,最终也是会执行到这里:
1 2 3 4 5 6 7 8 | extern struct task *__task_wakeup(struct task *t)
{
t->rq.key = ++rqueue_ticks;
// clear state flags at the same time
t->state &= ~TASK_WOKEN_ANY;
eb32_insert(&rqueue, &t->rq);
return t;
} |
添加task到等待队列,也就是让该task排队,乍看上去有点复杂,其实核心部分就只有两三句,关于last_timer的都是一些优化工作,使得HAProxy更快地找到插入的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | extern void __task_queue(struct task *task)
{
if (likely(task_in_wq(task)))
__task_unlink_wq(task);
/* the task is not in the queue now */
task->wq.key = task->expire;
if (likely(last_timer &&
last_timer->node.bit < 0 &&
last_timer->key == task->wq.key &&
last_timer->node.node_p)) {
eb_insert_dup(&last_timer->node, &task->wq.node);
if (task->wq.node.bit < last_timer->node.bit)
last_timer = &task->wq;
return;
}
eb32_insert(&timers, &task->wq);
/* Make sure we don't assign the last_timer to a node-less entry */
if (task->wq.node.node_p && (!last_timer || (task->wq.node.bit < last_timer->node.bit)))
last_timer = &task->wq;
return;
} |
由此可以看出,ebtree函数库在使用上,只负责ebnode的组织,并不负责对ebnode的分配和释放,否则wq和rq就是两个ebnode类型的指针,而不是两个ebnode。这样做是有道理的,利用以上ebnode的特点,一个task可以先后多次挂载到相应的以ebtree为具体设施的队列中,减少了内存分配和释放次数。
3. stream interface
stream interface结构体的主要成员如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | struct stream_interface {
unsigned int state; /* SI_ST* */
unsigned int prev_state; /* SI_ST*, copy of previous state */
void *owner; /* generally a (struct task*) */
int fd; /* file descriptor for a stream driver when known */
unsigned int flags;
unsigned int exp; /* wake up time for connect, queue, turn-around, ... */
void (*update)(struct stream_interface *); /* I/O update function */
void (*shutr)(struct stream_interface *); /* shutr function */
void (*shutw)(struct stream_interface *); /* shutw function */
void (*chk_rcv)(struct stream_interface *); /* chk_rcv function */
void (*chk_snd)(struct stream_interface *); /* chk_snd function */
int (*connect)(struct stream_interface *, struct proxy *, struct server *,
struct sockaddr *, struct sockaddr *); /* connect function if any */
void (*iohandler)(struct stream_interface *); /* internal I/O handler when embedded */
struct buffer *ib, *ob; /* input and output buffers */
unsigned int err_type; /* first error detected, one of SI_ET_* */
void *err_loc; /* commonly the server, NULL when SI_ET_NONE */
void *private; /* may be used by any function above */
unsigned int st0, st1; /* may be used by any function above */
}; |
大部分成员可以从注释中了解其用途,值得注意的是几个函数指针。大致上,它们可以指向两组函数:默认一组是以stream_sock_开头,用于在socket与缓冲区之间传输数据,转发处理业务流;另一组是以stream_int_开头,负责拦截、统计和响应客户端的监控请求,因为HAProxy的业务请求与监控请求是发送到同一个端口,通过URI来区分的,而监控请求显然是不能转发到后端服务器的,所以必须在获得各计数器数据后返回客户端。
以下简述一下stream_sock_开头的那一组函数的作用。这一组函数在上文的event_accept函数中初始化。
update指向的函数(stream_sock_data_finish)用于更新stream interface的fd的读写状态和相关标志位。
shutr和shutw指向的函数(stream_sock_shutr和stream_sock_shutw)分别用于关闭stream interface上的读和写。
chk_rcv指向的函数(stream_sock_chk_rcv)由消费者调用,用于通知生产者:缓冲区可能有空间了。
chk_snd指向的函数(stream_sock_chk_snd)由生产者调用,用于通知消费者:缓冲区可能有数据了。
当si是客户端stream interface时,connect为空,因为客户端连接显然已经建立。
当si是服务端stream interface时,connect指向HAProxy与服务端建立连接的那个函数(tcpv4_connect_server),而它会在backend.c的connect_server执行时被调用。
iohandler总是为空。
stream_int_开头的那一组函数在stream_interface.c的stream_int_register_handler函数中初始化,而该函数只有在监控分支才会执行。最明显区别是这一组会设置iohandler,而且调用时会重新唤醒所属的task,调用后马上返回,不再向下处理,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct task *process_session(struct task *t)
{
...
/* Call the second stream interface's I/O handler if it's embedded.
* Note that this one may wake the task up again.
*/
if (s->req->cons->iohandler) {
s->req->cons->iohandler(s->req->cons);
if (task_in_rq(t)) {
/* If we woke up, we don't want to requeue the
* task to the wait queue, but rather requeue
* it into the runqueue ASAP.
*/
t->expire = TICK_ETERNITY;
return t;
}
}
...
} |
4. buffer
完整的buffer结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct buffer {
unsigned int flags; /* BF_* */
int rex; /* expiration date for a read, in ticks */
int wex; /* expiration date for a write or connect, in ticks */
int rto; /* read timeout, in ticks */
int wto; /* write timeout, in ticks */
int cto; /* connect timeout, in ticks */
unsigned int l; /* data length */
char *r, *w, *lr; /* read ptr, write ptr, last read */
unsigned int size; /* buffer size in bytes */
unsigned int send_max; /* number of bytes the sender can consume om this buffer, <= l */
unsigned int to_forward; /* number of bytes to forward after send_max without a wake-up */
unsigned int analysers; /* bit field indicating what to do on the buffer */
int analyse_exp; /* expiration date for current analysers (if set) */
void (*hijacker)(struct session *, struct buffer *); /* alternative content producer */
unsigned char xfer_large; /* number of consecutive large xfers */
unsigned char xfer_small; /* number of consecutive small xfers */
unsigned long long total; /* total data read */
struct stream_interface *prod; /* producer attached to this buffer */
struct stream_interface *cons; /* consumer attached to this buffer */
struct pipe *pipe; /* non-NULL only when data present */
char data[0]; /* <size> bytes */
}; |
可以看到,HAProxy的buffer结构体也由许多成员组成,常规buffer该有的一个都不少,例如size、data、l、r、w、lr等等,除此之外,还有具有HAProxy特有的生产者、消费者stream interface指针(prod和cons),还有众多表示读写过期时间、超时时间的成员,还有指示具体请求响应解析过程的标志位(analysers),还有回调函数hijacker(不过看起来没有使用),还有表示该buffer最多能向其消费者发送多少字节的send_max,还有在HTTP chunked格式转发时才有意义的to_forward,还有连续满负荷传输数据和连续低负荷传输数据的计数器(xfer_large和xfer_small),以触发判断是否使用splice系统调用。面对把这么多功能揽于一身的buffer,看你晕不晕!
下面只挑一些具有通用借鉴意义的成员提及一下。
最后的那个成员data[0]是一个“零长度数组”,不懂的google一下就知道了,很简单,不多说。
5. pipe
buffer的倒数第二个成员pipe指针,指向的是这么一个链表结构:
1 2 3 4 5 6 | struct pipe {
int data; /* number of bytes present in the pipe */
int prod; /* FD the producer must write to ; -1 if none */
int cons; /* FD the consumer must read from ; -1 if none */
struct pipe *next;
}; |
因为splice系统调用要求输入和输出至少必须有一个描述符是管道符,所以,HAProxy准备了一对管道符(prod和cons)。当使用splice读数据时,HAProxy从源socket描述符(对于请求,就是连接客户端与HAProxy的socket描述符,对于响应,则是连接服务端与HAProxy的socket 描述符;下面的目的socket描述符刚好相反)splice到管道符prod;当使用splice写数据时,HAProxy从管道符cons splice到目的socket描述符。详见stream_socket.c的stream_sock_splice_in函数和stream_sock_write_loop函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static int stream_sock_splice_in(struct buffer *b, struct stream_interface *si)
{
...
ret = splice(fd, NULL, b->pipe->prod, NULL, max,
SPLICE_F_MOVE|SPLICE_F_NONBLOCK);
...
}
static int stream_sock_write_loop(struct stream_interface *si, struct buffer *b)
{
...
ret = splice(b->pipe->cons, NULL, si->fd, NULL, b->pipe->data,
SPLICE_F_MOVE|SPLICE_F_NONBLOCK);
...
} |
这样一来,每个buffer就要创建一对管道符,从上文和上面的分析可知,每个session就有两个buffer,而每个session的生命周期是从客户端连接建立到客户端连接释放(这里之前没有提到,以后可能会讲一下),所以,如果并发量很大,这是一个巨大的消耗。为此,HAProxy用管道池来解决这一问题,就像内存池的使用那样,需要有一个next指针把pipe结构体串起来。具体流程是,读取数据之前,调用get_pipe函数获取一对管道符,全部数据写完之后(由data指示数据是否写完),调用put_pipe函数进行释放,这样管道只会在异步等待写的时候被占用,大大减少使用量,从而减少系统开销。详见上面两个函数的代码。
6. 小结
五个我认为比较重要的结构体就介绍到这里。当然,HAProxy还有许多结构体,例如proxy、server、listener等等,不过,这些结构体,要么比较容易看懂,要么网上已经有比较齐全的资料,要么可以陆续在后面的文章中单独说明。而session、task、stream interface、buffer和pipe这五个结构体,连同第一篇介绍的ebtree,向我们展现了HAProxy作为一个高性能代理服务器的底层数据组织和一些重要的处理细节。
建议继续学习:
- 使用HAProxy对MySQL进行负载均衡和状态监控 (阅读:6259)
- C语言结构体里的成员数组和指针 (阅读:5716)
- 结构体初始化的方法 (阅读:2886)
- mysql innodb 文件相关的三个重要结构体 (阅读:2707)
- c、cpp中使用匿名结构体、类定义数组 (阅读:2625)
- HAProxy的event_accept函数源码分析 (阅读:2308)
- 根据成员地址获取结构体变量 (阅读:2253)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:若羽 来源: UC技术博客
- 标签: HAProxy 结构体
- 发布时间:2013-07-29 23:14:57
-
[927] WordPress插件开发 -- 在插件使用 -
[133] 解决 nginx 反向代理网页首尾出现神秘字 -
[52] 如何保证一个程序在单台服务器上只有唯一实例( -
[52] 整理了一份招PHP高级工程师的面试题 -
[50] 全站换域名时利用nginx和javascri -
[50] 海量小文件存储 -
[50] 用 Jquery 模拟 select -
[49] CloudSMS:免费匿名的云短信 -
[48] Innodb分表太多或者表分区太多,会导致内 -
[47] jQuery性能优化指南

