apache worker模式下keepalive及内核keepalive

一、apache的worker模式下保活选项

由于http的keepalive是控制apache行为的一个重要的特征,所以大致看了一下这个机制的实现及意义。通常来说,如果进行定性的分析,大家都可以随便信口开河,甚至望文生义也可以胡诌半天,但是这些结论对于真正的应用来说意义极小。就好像你说,我这个结论有90%的正确性,或者说这个程序能够处理90%的情况。那么这个结论的意义在不同的情况下就有不同的价值,例如程序在一个课程设计中,99%的正确率是可以完成要求的,对于银行系统来说,这个肯定是不达标的,对于航天系统中,这个概率更加不可靠。在工程中同样也是如此,即使很小的概率,那么在大量访问面前,它同样会导致该问题出现,并且从数量上看并不少。

apache对于保活的选项在httpd-2.4.2\modules\http\http_core.c中实现,这个名字起得相当霸气外露,但是该文件中并没有太多真正的策略性内容,文件包括注释308行。而在这个简短的文件中,就包含了三个对于保活的处理选项,但是这些处理选项只是对于配置文件中配置项的读取,而真正使用这些配置的代码在这个文件中体现并不是很明显。这三个命令的配置为

static const command_rec http_cmds[] = {

AP_INIT_TAKE1("KeepAliveTimeout", set_keep_alive_timeout, NULL, RSRC_CONF,

"Keep-Alive timeout duration (sec)"),

AP_INIT_TAKE1("MaxKeepAliveRequests", set_keep_alive_max, NULL, RSRC_CONF,

"Maximum number of Keep-Alive requests per connection, "

"or 0 for infinite"),

AP_INIT_FLAG("KeepAlive", set_keep_alive, NULL, RSRC_CONF,

"Whether persistent connections should be On or Off"),

{ NULL }

};

这三个函数的实现中合理就不在摘录了,只是把核心的代码拷贝一下

static const char *set_keep_alive_timeout(cmd_parms *cmd, void *dummy,

const char *arg)

{

cmd->server->keep_alive_timeout = timeout;

}

static const char *set_keep_alive(cmd_parms *cmd, void *dummy,

int arg)

{

……

cmd->server->keep_alive = arg;

return NULL;

}

二、这些值在什么时候使用

1、超时参数

worker_thread--->>>process_socket--->>>ap_process_connection--->>ap_run_process_connection--->>>ap_process_http_connection--->>ap_process_http_sync_connection

{

while ((r = ap_read_request(c)) != NULL) {

……

if (c->keepalive != AP_CONN_KEEPALIVE || c->aborted) 对于设置了保活选项的socket,这里的两个条件都是不满足的,所以此时会继续进行循环,在ap_read_request函数中阻塞。

break;

……

apr_socket_opt_set(csd, APR_INCOMPLETE_READ, 1);

apr_socket_timeout_set(csd, c->base_server->keep_alive_timeout);这里的时间就是在配置文件中配置的超时时间。

}

其中阻塞等待的系统调用为

read_request_line--->>>ap_rgetline--->>>ap_rgetline_core--->>>apr_bucket_read-->>socket_bucket_read-->>>apr_socket_recv

while ((rv == -1) && (errno == EAGAIN || errno == EWOULDBLOCK)

&& (sock->timeout > 0)) {

do_select:

arv = apr_wait_for_io_or_timeout(NULL, sock, 1);

这里就使用了保活的时间设置。从循环的主题来看,如果设置了保活选项,那么当socket处理完一个socket请求之后,会在套接口的read系统调用中尝试最多阻塞timeout时间,如果这段时间内没有新的数据到来,那么读操作超时返回,否则开始下一轮读取和处理。

2、max及使能的使用

max的使用

AP_DECLARE(int) ap_set_keepalive(request_rec *r)

int left = r->server->keep_alive_max - r->connection->keepalives;

……

if(……

&& r->server->keep_alive

&& (r->server->keep_alive_timeout > 0)

&& ((r->server->keep_alive_max == 0)

|| (left > 0))

……

{

……

{

r->connection->keepalive = AP_CONN_KEEPALIVE;

r->connection->keepalives++;

……

}

源代码中对于这两项的注释说明为

/** Are we going to keep the connection alive for another request?

* @see ap_conn_keepalive_e */

ap_conn_keepalive_e keepalive;

/** How many times have we used it? */

int keepalives;

这里想说明的问题是:如果你设置了keepalive选项,那么在这个时间段内,即使客户端没有发送任何数据过来,这个worker线程始终要尝试进行这么长时间的等待,这是一个机械的假设,所以需要根据不同的场合慎重使用。

3、如果没有设置该选项

worker_thread-->>process_socket

if (current_conn) {

current_conn->current_thread = thd;

ap_process_connection(current_conn, sock);

ap_lingering_close(current_conn);

}

可以看到的是,如果没有设置保活选项,此时在ap_process_connection之后客户端的套接口被直接关闭,所以说是一个短连接。

三、内核的keepalive时间

1、内核保活定时器动作

内核同样有一个保活选项,名字和这个也类似,但是它的意义和apache的意义完全不同,而apache的该功能也不依赖内核实现。内核的保活使用定时器时间,它是一个在双方长时间没有交互的情况下主动向对方发送一个探测性报文,以确保对方还存在,当然这个功能在应用层做也是可以的。这里大家可以先想一下这个探测性报文是怎么样的,也就是说这个报文怎么能够做到探测并且仅仅是一个探测报文,而不会对正常的报文传递造成影响。其定时器超时函数处理为linux-2.6.21\net\ipv4\tcp_timer.c

static void tcp_keepalive_timer (unsigned long data)----->>>tcp_write_wakeup--->>>tcp_xmit_probe_skb(sk, 0)

/* This routine sends a packet with an out of date sequence

* number. It assumes the other end will try to ack it.

*

* Question: what should we make while urgent mode?

* 4.4BSD forces sending single byte of data. We cannot send

* out of window data, because we have SND.NXT==SND.MAX...

*

* Current solution: to send TWO zero-length segments in urgent mode:

* one is with SEG.SEQ=SND.UNA to deliver urgent pointer, another is

* out-of-date with SND.UNA-1 to probe window.

*/

static int tcp_xmit_probe_skb(struct sock *sk, int urgent)

^

TCP_SKB_CB(skb)->flags = TCPCB_FLAG_ACK;

TCP_SKB_CB(skb)->sacked = urgent;

^

/* Use a previous sequence. This should cause the other

* end to send an ack. Don't queue or clone SKB, just

* send it.

*/

TCP_SKB_CB(skb)->seq = urgent ? tp->snd_una : tp->snd_una - 1;

TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(skb)->seq;

TCP_SKB_CB(skb)->when = tcp_time_stamp;

return tcp_transmit_skb(sk, skb, 0, GFP_ATOMIC);

这里探测的方法就是向对方发送一过时的报文,例如对方已经确认了第3个报文被收到,此时我们发送一个过时的报文2,此时对方如果收到这个报文,它会检测出这是个过时的报文,所以会对这个报文进行回应,并且在回应中强调自己希望收到的是第4个报文,此时保活定时器就可以有机会清掉保活定时器。

2、对于窗口外报文的处理

linux-2.6.21\net\ipv4\tcp_input.c

int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,

struct tcphdr *th, unsigned len)

……

if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {不在滑动窗口可接收范围之间

/* RFC793, page 37: "In all states except SYN-SENT, all reset

* (RST) segments are validated by checking their SEQ-fields."

* And page 69: "If an incoming segment is not acceptable,

* an acknowledgment should be sent in reply (unless the RST bit

* is set, if so drop the segment and return)".

*/

if (!th->rst)

tcp_send_dupack(sk, skb);回传一个重复的确认报文。

goto discard;

}