代码分析
调试过程中你会对代码结构渐渐的有清晰的认识,现在我们讲一下这中间的全局变量和函数
现在我们依旧是分析 v1.0.0 版本
全局变量
-
proxy
变量
该变量是一个结构指针struct proxy *
,实际上是作为链表的形式存在,保存了所有代理的结构
在readcfgfile
函数读配置时赋值
同时task
成员也是非常重要的,它保存着当前 proxy 的所有请求和响应信息,相当于 session 的功能struct task { struct task *next, *prev; /* chaining ... */ struct task *rqnext; /* chaining in run queue ... */ int state; /* task state : IDLE or RUNNING */ struct timeval expire; /* next expiration time for this task, use only for fast sorting */ /* application specific below */ struct timeval crexpire; /* expiration date for a client read */ struct timeval cwexpire; /* expiration date for a client write */ struct timeval srexpire; /* expiration date for a server read */ struct timeval swexpire; /* expiration date for a server write */ struct timeval cnexpire; /* expiration date for a connect */ char res_cr, res_cw, res_sr, res_sw;/* results of some events */ struct proxy *proxy; /* the proxy this socket belongs to */ int cli_fd; /* the client side fd */ int srv_fd; /* the server side fd */ int cli_state; /* state of the client side */ int srv_state; /* state of the server side */ int conn_retries; /* number of connect retries left */ int conn_redisp; /* allow reconnection to dispatch in case of errors */ struct buffer *req; /* request buffer */ struct buffer *rep; /* response buffer */ struct sockaddr_in cli_addr; /* the client address */ struct sockaddr_in srv_addr; /* the address to connect to */ char cookie_val[SERVERID_LEN+1]; /* the cookie value, if present */ }; struct proxy { int listen_fd; /* the listen socket */ int state; /* proxy state */ struct sockaddr_in listen_addr; /* the address we listen to */ struct sockaddr_in dispatch_addr; /* the default address to connect to */ struct server *srv; /* known servers */ char *cookie_name; /* name of the cookie to look for */ int clitimeout; /* client I/O timeout (in milliseconds) */ int srvtimeout; /* server I/O timeout (in milliseconds) */ int contimeout; /* connect timeout (in milliseconds) */ char *id; /* proxy id */ int nbconn; /* # of active sessions */ int maxconn; /* max # of active sessions */ int conn_retries; /* number of connect retries left */ int conn_redisp; /* allow to reconnect to dispatch in case of errors */ int mode; /* mode = PR_MODE_TCP or PR_MODE_HTTP */ struct task task; /* active sessions (bi-dir chaining) */ struct task *rq; /* sessions in the run queue (unidir chaining) */ struct proxy *next; struct sockaddr_in logsrv1, logsrv2; /* 2 syslog servers */ char logfac1, logfac2; /* log facility for both servers. -1 = disabled */ struct timeval stop_time; /* date to stop listening, when stopping != 0 */ int nb_cliexp, nb_srvexp; struct hdr_exp cli_exp[MAX_REGEXP]; /* regular expressions for client headers */ struct hdr_exp srv_exp[MAX_REGEXP]; /* regular expressions for server headers */ int grace; /* grace time after stop request */ }; struct proxy *proxy = NULL; /* list of all existing proxies */
-
fdtab
变量
该变量同样是一个结构指针struct fdtab *
,作为一个数组存储着所有 fd 关联的信息/* info about one given fd */ struct fdtab { int (*read)(int fd); /* read function */ int (*write)(int fd); /* write function */ struct task *owner; /* the session (or proxy) associated with this fd */ int state; /* the state of this fd */ }; struct fdtab *fdtab = NULL; /* array of all the file descriptors */
fd_set *
类型变量
ReadEvent
和WriteEvent
用于select
函数,因为执行完后会修改里面的值,所以作为临时存储 fd 的空间
而StaticReadEvent
和StaticWriteEvent
则是存储所有原始的 fd 信息
函数
-
init
函数
这个函数主要干这么些事:- 命令行参数的读取
- 配置文件的分析
- 预分配一些空间给一些关键变量并初始化这些变量
- 执行过程中若失败就退出程序
我们可以看到初期的代码还是很简陋的,处理命令行参数的基本全是手写的 if else 代码,对后期维护会带来一定挑战。同时读配置文件的函数
readcfgfile
里面也存在大量硬编码的逻辑,没有把配置语义化规则化。未来规则复杂后,恐怕不能再使用大量的条件判断去实现了,否则维护过程会很难。另外第
2968
行到第2983
行是用来切换到 daemon 模式的,我觉得可以独立出一个函数会比较干净 -
dump
函数
dump
函数捕获了SIGQUIT
信号,打印了一些当前的状态变量void dump(int sig) { struct proxy *p; for (p = proxy; p; p = p->next) { struct task *t, *tnext; tnext = ((struct task *)LIST_HEAD(p->task))->next; while ((t = tnext) != LIST_HEAD(p->task)) { /* we haven't looped ? */ tnext = t->next; fprintf(stderr,"[dump] wq: task %p, still %ld ms, " "cli=%d, srv=%d, cr=%d, cw=%d, sr=%d, sw=%d, " "req=%d, rep=%d, clifd=%d\n", t, tv_remain(&now, &t->expire), t->cli_state, t->srv_state, FD_ISSET(t->cli_fd, StaticReadEvent), FD_ISSET(t->cli_fd, StaticWriteEvent), FD_ISSET(t->srv_fd, StaticReadEvent), FD_ISSET(t->srv_fd, StaticWriteEvent), t->req->l, t->rep?t->rep->l:0, t->cli_fd ); } } }
-
sig_soft_stop
函数
sig_soft_stop
函数捕获了SIGUSR1
信号void sig_soft_stop(int sig) { soft_stop(); signal(sig, SIG_IGN); }
static void soft_stop(void) { struct proxy *p; stopping = 1; p = proxy; while (p) { if (p->state != PR_STDISABLED) tv_delayfrom(&p->stop_time, &now, p->grace); p = p->next; } }
大致的意思就是使用比较优雅的方式关闭连接
在停之前设置标志位stopping
然后设置每个节点要停止的时间点,等待主线程把所有相关连接全部关闭
可是整个进程并没有终止,还是需要发送停止信号才会关闭 -
start_proxies
函数
遍历proxy
链表,并启动所有的未禁用的代理,成功返回 0,否则退出程序
在对 socket 绑定和监听前经过了以下设置- 无阻塞(
O_NONBLOCK
) - TCP 无延时(
TCP_NODELAY
) - 可重用地址(
SO_REUSEADDR
)
最后设置
fdtab
- 无阻塞(
-
select_loop
函数
我们看到这个版本的 HAProxy 因为是在 2000~2001年开发的,所以这时候还没有 epoll,使用 select 遍历的技术是比较普遍的
在select
前会调用maintain_proxies
函数来更新代理的状态
该函数逻辑很简单- 若设置了
stopping
,则停止所有端口监听 - 若监听总连接数没达到连接数最大限制,则把空闲状态(
PR_STIDLE
)的代理变为运行中(PR_STRUN
) - 若达到了最大连接数限制,则把所有运行中的代理设置为空闲(停止接收新连接)
接着执行
select
函数, 对所有激活的 fd 进行读写
最后把可以执行的task
放入执行队列run queue
批量处理这些task
,这里会涉及到 3 个函数process_cli
,process_srv
,process_task
说明:这里的 client 是指请求方,server 是后端响应方
- 若设置了
事件相关函数
event_accept
: 监听到新的连接时触发
event_cli_read
: 请求数据到达 HAProxy 时触发
event_cli_write
: 把数据发回请求方时触发
connect_server
: 连接后端服务器
event_srv_read
: 后端服务响应到达 HAProxy 时触发
event_srv_write
: 把数据发送到后端服务时触发
总结
我们看到 1.0.0 版本的 HAProxy 还是相当简陋的,连 acl 也还没开发,好处是可以很容易的读懂,为后面复杂的版本打下基础
后面我们将对版本迭代中的关键代码变更进行讲解