HAProxy Research Notes--HTTP Request Processing-2-Analysis

  • Author: Godbach 
  • Blog:  http://godbach.blog.chinaunix.net  


  • ----

This article continues to analyze the processing after receiving client data in 1.5-dev17.

For the process of receiving the request data sent by the client in haproxy-1.5-dev17, see the document:  HTTP Request Processing-1-Receive

1. The processing flow of the haproxy main loop

For the main loop processing flow, see the document  Introduction to the main loop

The parsing of the request data is performed in the main loop process_runnable_tasks().

2. Execute the tasks in the run queue

 HTTP request processing -1-  After analyzing the establishment of the session in the reception, the task of the session will be put into the runqueue, and the task will appear in the next round of traversal of the tasks that can be run, and will be executed. The second is to immediately call conn_fd_handler to receive the data sent by the client.

After the data receiving process ends (note that this does not mean that a complete client request has been received, because the client's data may not be read temporarily to exit the receiving process), haproxy schedules the execution of the next round of loops, calling process_runnable_tasks() to process all runqueue tasks in:

void process_runnable_tasks(int *next)
{
    ...
	eb = eb32_lookup_ge(&rqueue, rqueue_ticks - TIMER_LOOK_BACK);
	while (max_processed--) {
	    ...
		t = eb32_entry(eb, struct task, rq);
		eb = eb32_next(eb);
		__task_unlink_rq(t);

		t->state |= TASK_RUNNING;
		/* This is an optimisation to help the processor's branch
		 * predictor take this most common call.
		 */
		t->calls++;
		if (likely(t->process == process_session))
			t = process_session(t);
		else
			t = t->process(t);
		...
    }
}

In most cases, the task's proecss points to the process_session() function. This function is responsible for parsing the received data, selecting the backend server, changing the session state, and so on.

3. Session processing: process_session()

The implementation of the process_session() function is described below. The code of this function is relatively huge, with more than a thousand lines. Here, only the logic of HTTP request processing is introduced, and the logic of code block is introduced.

The logic code for handling HTTP requests is concentrated at label resync_request.

struct task *process_session(struct task *t)
{
    ...
 resync_request:
	/* Analyse request */
	if (((s->req->flags & ~rqf_last) & CF_MASK_ANALYSER) ||
	    ((s->req->flags ^ rqf_last) & CF_MASK_STATIC) ||
	    s->si[0].state != rq_prod_last ||
	    s->si[1].state != rq_cons_last) {
		unsigned int flags = s->req->flags;

		if (s->req->prod->state >= SI_ST_EST) {
			ana_list = ana_back = s->req->analysers;
			while (ana_list && max_loops--) {
			    /*这段代码中逐一的列举出了所有的 analysers 对应的处理函数
                 *这里不一一列出,等待下文具体分析
                 */
                 ...
		    }
        }
		rq_prod_last = s->si[0].state;
		rq_cons_last = s->si[1].state;
		s->req->flags &= ~CF_WAKE_ONCE;
		rqf_last = s->req->flags;

		if ((s->req->flags ^ flags) & CF_MASK_STATIC)
			goto resync_request;
    }

首先要判断 s->req->prod->state 的状态是否已经完成建连,根据之前的初始化动作, se->req->prod 指向 s->si[0],即标识与 client 端连接的相关信息。正确建连成功之 后,会更改 si 的状态的,具体代码在 session_complete() 中:

    s->si[0].state     = s->si[0].prev_state = SI_ST_EST;
    ...
	s->req->prod = &s->si[0];
	s->req->cons = &s->si[1];

只有 frontend 连接建立成功,才具备处理 client 发送请求数据的基础。上一篇文章中 已经接收到了 client 发送的数据。这里就是需要根据 s->req->analysers 的值,确定 while 循环中哪些函数处理当前的数据。

补充介绍一下 s->req->analysers 的赋值。 同样是在 session_complete 中初始化的

	/* activate default analysers enabled for this listener */
	s->req->analysers = l->analysers;

可见,其直接使用 session 所在的 listener 的 analyser。 listener 中该数值的初始化 是在 check_config_validity() 中完成的:

			listener->analysers |= curproxy->fe_req_ana;

而归根结蒂还是来源于 listener 所在的 proxy 上的 fe_req_ana, proxy 上的 fe_req_ana 的初始化同样是在 check_config_validity(),且是在给 listener->analysers 赋值之前

		if (curproxy->cap & PR_CAP_FE) {
			if (!curproxy->accept)
				curproxy->accept = frontend_accept;

			if (curproxy->tcp_req.inspect_delay ||
			    !LIST_ISEMPTY(&curproxy->tcp_req.inspect_rules))
				curproxy->fe_req_ana |= AN_REQ_INSPECT_FE;

			if (curproxy->mode == PR_MODE_HTTP) {
				curproxy->fe_req_ana |= AN_REQ_WAIT_HTTP | AN_REQ_HTTP_PROCESS_FE;
				curproxy->fe_rsp_ana |= AN_RES_WAIT_HTTP | AN_RES_HTTP_PROCESS_FE;
			}

			/* both TCP and HTTP must check switching rules */
			curproxy->fe_req_ana |= AN_REQ_SWITCHING_RULES;
		}

从上面代码可以看出,一个 HTTP 模式的 proxy,至少有三个标记位会被置位: AN_REQ_WAIT_HTTP, AN_REQ_HTTP_PROCESS_FE, AN_REQ_SWITCHING_RULES。也就是说, s->req->analysers 由以上三个标记置位。那么随后处理 HTTP REQ 的循环中,就要经过 这三个标记位对应的 analyser 的处理。

接着回到 resync_request 标签下的那个 while 循环,就是逐个判断 analysers 的设置, 并调用对应的函数处理。需要启用那些 analysers,是和 haproxy 的配置相对应的。本文 使用最简单的配置,下面仅列出配置所用到的几个处理函数:

			while (ana_list && max_loops--) {
				/* Warning! ensure that analysers are always placed in ascending order! */

				if (ana_list & AN_REQ_INSPECT_FE) {
					if (!tcp_inspect_request(s, s->req, AN_REQ_INSPECT_FE))
						break;
					UPDATE_ANALYSERS(s->req->analysers, ana_list, ana_back, AN_REQ_INSPECT_FE);
				}
			
				if (ana_list & AN_REQ_WAIT_HTTP) {
					if (!http_wait_for_request(s, s->req, AN_REQ_WAIT_HTTP))
						break;
					UPDATE_ANALYSERS(s->req->analysers, ana_list, ana_back, AN_REQ_WAIT_HTTP);
				}

				if (ana_list & AN_REQ_HTTP_PROCESS_FE) {
					if (!http_process_req_common(s, s->req, AN_REQ_HTTP_PROCESS_FE, s->fe))
						break;
					UPDATE_ANALYSERS(s->req->analysers, ana_list, ana_back, AN_REQ_HTTP_PROCESS_FE);
				}

				if (ana_list & AN_REQ_SWITCHING_RULES) {
					if (!process_switching_rules(s, s->req, AN_REQ_SWITCHING_RULES))
						break;
					UPDATE_ANALYSERS(s->req->analysers, ana_list, ana_back, AN_REQ_SWITCHING_RULES);
				}
				...
            }

analysers 的处理也是有顺序的。其中处理请求的第一个函数是 tcp_inspect_request()。 该函数主要是在于如果配置了这里先介绍 http_wait_for_request() 函数的实现。 顾名思义,该函数主要是配置中启用 inspect_rules 时,会调用到该函数。否则的话, 处理 HTTP Req 的第一个函数就是 http_wait_for_request().

顾名思义,http_wait_for_request() 该函数分析所解析的 HTTP Requset 不一定是一个 完整的请求。上篇文章分析读取 client 请求数据的实现中,已经提到,只要不能从 socket 读到更多的数据,就会结束数据的接收。一个请求完全完全有可能因为一些异常原因,或者 请求长度本身就比较大而被拆分到不同的 IP 报文中,一次 read 系统调用可能只读取到其 中的一部分内容。因此,该函数会同时分析已经接收到的数据,并确认是否已经接收到了 完整的 HTTP 请求。只有接收到了完整的 HTTP 请求,该函数处理完,才会交给下一个 analyser 处理,否则只能结束请求的处理,等待接收跟多的数据,解析出一个完成的 HTTP 请求才行。

4. 解析接收到的 http 请求数据: http_wait_for_request()

以下是 http_wait_for_request() 的简要分析:

1. 调用 http_msg_analyzer,解析 s->req->buf 中新读取到的数据。该函数会按照 HTTP 协议, 解析 HTTP request 和 response 的头部数据,并记录到数据结构 struct http_msg 中。

2. 如果开启了 debug,并且已经完整的解析了 header,则 header 内容打印出来

3. 尚未读取到完整的 request 的处理,分作以下几种情形处理:

	if (unlikely(msg->msg_state < HTTP_MSG_BODY)) {
		/*
		 * First, let's catch bad requests.
		 */

  • 解析到 header 内容中有不符合 HTTP 协议的情形 HTTP_MSG_ERROR,应答 400 bad request 处理
  • req->buf 满了,甚至加入 maxrewrite 的空间仍然不够用,应答 400 bad request
  • 读取错误 CF_READ_ERROR 发生,比如 client 发送 RST 断开连接, 应答 400 bad request
  • 读取超时,client 超时未发送完整的请求,应答 408 Request Timeout
  • client 主动关闭,发送 FIN 包,实际上是所谓的 half-close,同样应答 400 bad request
  • 如果以上情况都不满足,则意味着还可以继续尝试读取新数据,设置一下超时
    		/* just set the request timeout once at the beginning of the request */
    		if (!tick_isset(req->analyse_exp)) {
    			if ((msg->msg_state == HTTP_MSG_RQBEFORE) &&
    			    (txn->flags & TX_WAIT_NEXT_RQ) &&
    			    tick_isset(s->be->timeout.httpka))
    				req->analyse_exp = tick_add(now_ms, s->be->timeout.httpka);
    			else
    				req->analyse_exp = tick_add_ifset(now_ms, s->be->timeout.httpreq);
    		}
    
    

根据以上代码,在等待 http request 期间,有两种 timeout 可以设置: 当是http 连接 Keep-Alive 时,并且处理完了头一个请求之后,等待第二个请求期间,设置 httpka 的超 时,超过设定时间不发送新的请求,将会超时;否则,将设置 http 的 request timeout。

因此,在不启用 http ka timeout 时,http request 同时承担起 http ka timeout 的 功能。在有 http ka timeout 时,这两者各自作用的时间段没有重叠。

满足该环节的请求都终止处理,不再继续了。

4. 处理完整的 http request

这里处理的都是已经解析到完整 http request header 的情况,并且所有 header 都被 索引化了,便于快速查找。根据已经得到的 header 的信息,设置 session 和 txn 的 相关成员,相当于汇总一下 header 的摘要信息,便于随后处理之用。流程如下:

  • 更新 session 和 proxy 的统计计数
  • 删除 http ka timeout 的超时处理。可能在上一个请求处理完之后,设置了 http ka 的 timeout,因为这里已经得到完整的请求,因此需要停止该 timeout 的处理逻辑
  • 确认 METHOD,并设置 session 的标记位 s->flags |= SN_REDIRECTABLE,只有 GET 和 HEAD 请求可以被重定向
  • 检测 URI 是否是配置的要做 monitor 的 URI,是的话,则执行对应 ACL,并设置应答
  • 检测如果开启 log 功能的话,要给 txn->uri 分配内存,用于记录 URI
  • 检测 HTTP version
    • 将 0.9 版本的升级为 1.0
    • 1.1 及其以上的版本都当做 1.1 处理
  • 初始化用于标识 Connection header 的标记位
  • 如果启用了 capture header 配置,调用 capture_headers() 记录下对应的 header
  • 处理 Transfer-Encoding/Content-Length 等 header
  • 最后一步,清理 req->analysers 的标记位 AN_REQ_WAIT_HTTP,因为本函数已经成功处理完毕,可以进行下一个 analyser 的处理了。

至此,http_wait_for_request() 的处理已经结束。

5. 其他对 HTTP 请求的处理逻辑

按照我们前面分析的,随后应该还有两个 analyser 要处理,简单介绍一下:

  • AN_REQ_HTTP_PROCESS_FE 对应的 http_process_req_common()
    • 对 frontend 中 req 配置的常见处理,比如 block ACLs, filter, reqadd 等
    • 设置 Connection mode, 主要是 haproxy 到 server 采用什么连接方式,tunnel 或者 按照 transcation 处理的短连接
  • AN_REQ_SWITCHING_RULES 对应的 process_switching_rules()
    • 如果配置了选择 backend 的 rules,比如用 use_backend,则查询规则为 session 分配一个 backend
    • 处理 persist_rules,一旦设置了 force-persist, 则不管 server 是否 down,都要保证 session 分配给 persistence 中记录的 server。

以上两个函数,不再具体分析。待以后需要时再完善。

至此,client 端 http 请求已经完成解析和相关设置,并且给 session 指定了将来选择 server 所属的 backend。

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325603628&siteId=291194637