From: Willy Tarreau Date: Sat, 2 Jan 2010 21:47:18 +0000 (+0100) Subject: [MAJOR] http: add support for option http-server-close X-Git-Tag: v1.4-dev5~22 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=b608feb82ae08270fa97f435f065942188e7d177;p=thirdparty%2Fhaproxy.git [MAJOR] http: add support for option http-server-close This option enables HTTP keep-alive on the client side and close mode on the server side. This offers the best latency on the slow client side, and still saves as many resources as possible on the server side by actively closing connections. Pipelining is supported on both requests and responses, though there is currently no reason to get pipelined responses. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 5a31e855ee..7e7cc5dcc2 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -758,6 +758,8 @@ monitor-uri X X X - [no] option forceclose X X X X option forwardfor X X X X option httpchk X - X X +[no] option http-server- + close X X X X [no] option httpclose X X X X option httplog X X X X [no] option http_proxy X X X X @@ -2510,6 +2512,38 @@ option httpchk "check", "port" and "interval" server options. +option http-server-close +no option http-server-close + Enable or disable HTTP connection closing on the server side + May be used in sections : defaults | frontend | listen | backend + yes | yes | yes | yes + Arguments : none + + This mode enables HTTP connection-close mode on the server side while keeping + the ability to support HTTP keep-alive and pipelining on the client side. + This provides the lowest latency on the client side (slow network) and the + fastest session reuse on the server side to save server resources, similarly + to "option forceclose". It also permits non-keepalive capable servers to be + served in keep-alive mode to the clients if they conform to the requirements + of RFC2616. + + At the moment, logs will not indicate whether requests came from the same + session or not. The accept date reported in the logs corresponds to the end + of the previous request, and the request time corresponds to the time spent + waiting for a new request. The keep-alive request time is still bound to the + timeout defined by "timeout http-request". + + This option may be set both in a frontend and in a backend. It is enabled if + at least one of the frontend or backend holding a connection has it enabled. + It is worth noting that "option forceclose" has precedence over "httpclose", + which itself has precedence over "option http-server-close". + + If this option has been enabled in a "defaults" section, it can be disabled + in a specific instance by prepending the "no" keyword before it. + + See also : "option forceclose" and "option httpclose" + + option httpclose no option httpclose Enable or disable passive HTTP connection closing @@ -2540,7 +2574,7 @@ no option httpclose If this option has been enabled in a "defaults" section, it can be disabled in a specific instance by prepending the "no" keyword before it. - See also : "option forceclose" + See also : "option forceclose" and "option http-server-close" option httplog [ clf ] diff --git a/include/types/proto_http.h b/include/types/proto_http.h index 8a1293988b..c9ec7e3492 100644 --- a/include/types/proto_http.h +++ b/include/types/proto_http.h @@ -56,7 +56,9 @@ #define TX_SVDENY 0x00000004 /* a server header matches a deny regex */ #define TX_SVALLOW 0x00000008 /* a server header matches an allow regex */ #define TX_CLTARPIT 0x00000010 /* the session is tarpitted (anti-dos) */ -/* unused: 0x00000020 */ + +/* used only for keep-alive purposes, to indicate we're on a second transaction */ +#define TX_NOT_FIRST 0x00000020 /* the transaction is not the first one */ /* transaction flags dedicated to cookies : bits values 0x40, 0x80 (0-3 shift 6) */ #define TX_CK_NONE 0x00000000 /* this session had no cookie */ diff --git a/include/types/proxy.h b/include/types/proxy.h index 92517ca2ec..54190ee8e5 100644 --- a/include/types/proxy.h +++ b/include/types/proxy.h @@ -100,12 +100,11 @@ #define PR_O_TPXY_CLI 0x06000000 /* bind to the client's IP+port when connect()ing */ #define PR_O_TPXY_MASK 0x06000000 /* bind to a non-local address when connect()ing */ -/* unused : tcpsplice 0x08000000 */ +#define PR_O_SERVER_CLO 0x08000000 /* option http-server-close */ #define PR_O_CONTSTATS 0x10000000 /* continous counters */ #define PR_O_HTTP_PROXY 0x20000000 /* Enable session to use HTTP proxy operations */ #define PR_O_DISABLE404 0x40000000 /* Disable a server on a 404 response to a health-check */ #define PR_O_ORGTO 0x80000000 /* insert x-original-to with destination address */ -/* unused: 0x80000000 - now used by PR_O_ORGTO */ /* bits for proxy->options2 */ #define PR_O2_SPLIC_REQ 0x00000001 /* transfer requests using linux kernel's splice() */ diff --git a/src/cfgparse.c b/src/cfgparse.c index 7b344c8108..fdaa6062b9 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -115,6 +115,7 @@ static const struct cfg_opt cfg_opts[] = { "http_proxy", PR_O_HTTP_PROXY, PR_CAP_FE | PR_CAP_BE, 0 }, { "httpclose", PR_O_HTTP_CLOSE, PR_CAP_FE | PR_CAP_BE, 0 }, { "keepalive", PR_O_KEEPALIVE, PR_CAP_NONE, 0 }, + { "http-server-close", PR_O_SERVER_CLO, PR_CAP_FE | PR_CAP_BE, 0 }, { "logasap", PR_O_LOGASAP, PR_CAP_FE, 0 }, { "nolinger", PR_O_TCP_NOLING, PR_CAP_FE | PR_CAP_BE, 0 }, { "persist", PR_O_PERSIST, PR_CAP_BE, 0 }, diff --git a/src/proto_http.c b/src/proto_http.c index 5eec4b6c2d..fd266730f2 100644 --- a/src/proto_http.c +++ b/src/proto_http.c @@ -2176,6 +2176,9 @@ int http_wait_for_request(struct session *s, struct buffer *req, int an_bit) /* 2: have we encountered a read error ? */ else if (req->flags & BF_READ_ERROR) { + if (txn->flags & TX_NOT_FIRST) + goto failed_keep_alive; + /* we cannot return any message on error */ if (msg->err_pos >= 0) http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe); @@ -2195,6 +2198,9 @@ int http_wait_for_request(struct session *s, struct buffer *req, int an_bit) /* 3: has the read timeout expired ? */ else if (req->flags & BF_READ_TIMEOUT || tick_is_expired(req->analyse_exp, now_ms)) { + if (txn->flags & TX_NOT_FIRST) + goto failed_keep_alive; + /* read timeout : give up with an error message. */ if (msg->err_pos >= 0) http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe); @@ -2216,6 +2222,9 @@ int http_wait_for_request(struct session *s, struct buffer *req, int an_bit) /* 4: have we encountered a close ? */ else if (req->flags & BF_SHUTR) { + if (txn->flags & TX_NOT_FIRST) + goto failed_keep_alive; + if (msg->err_pos >= 0) http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe); txn->status = 400; @@ -2243,6 +2252,19 @@ int http_wait_for_request(struct session *s, struct buffer *req, int an_bit) /* we're not ready yet */ return 0; + + failed_keep_alive: + /* Here we process low-level errors for keep-alive requests. In + * short, if the request is not the first one and it experiences + * a timeout, read error or shutdown, we just silently close so + * that the client can try again. + */ + txn->status = 0; + msg->msg_state = HTTP_MSG_RQBEFORE; + req->analysers = 0; + s->logs.logwait = 0; + stream_int_cond_close(req->prod, NULL); + return 0; } /* OK now we have a complete HTTP request with indexed headers. Let's @@ -2562,11 +2584,12 @@ int http_process_req_common(struct session *s, struct buffer *req, int an_bit, s */ if ((txn->meth != HTTP_METH_CONNECT) && - ((s->fe->options|s->be->options) & (PR_O_KEEPALIVE|PR_O_HTTP_CLOSE|PR_O_FORCE_CLO))) { + ((s->fe->options|s->be->options) & (PR_O_KEEPALIVE|PR_O_SERVER_CLO|PR_O_HTTP_CLOSE|PR_O_FORCE_CLO))) { int tmp = TX_CON_WANT_TUN; if ((s->fe->options|s->be->options) & PR_O_KEEPALIVE) tmp = TX_CON_WANT_KAL; - /* FIXME: for now, we don't support server-close mode */ + if ((s->fe->options|s->be->options) & PR_O_SERVER_CLO) + tmp = TX_CON_WANT_SCL; if ((s->fe->options|s->be->options) & (PR_O_HTTP_CLOSE|PR_O_FORCE_CLO)) tmp = TX_CON_WANT_CLO; @@ -3340,7 +3363,12 @@ int http_request_forward_body(struct session *s, struct buffer *req, int an_bit) * to reset the transaction here. */ - if ((s->fe->options | s->be->options) & PR_O_FORCE_CLO) { + if ((txn->flags & TX_CON_WANT_MSK) == TX_CON_WANT_SCL) { + /* initiate a connection close to the server */ + req->cons->flags |= SI_FL_NOLINGER; + buffer_shutw_now(req); + } + else if ((s->fe->options | s->be->options) & PR_O_FORCE_CLO) { /* Option forceclose is set, let's enforce it now * that the transfer is complete. We can safely speed * up the close because we know the server has received @@ -3372,6 +3400,111 @@ int http_request_forward_body(struct session *s, struct buffer *req, int an_bit) } else if (msg->msg_state == HTTP_MSG_CLOSED) { req->flags &= ~BF_DONT_READ; + + if ((txn->flags & TX_CON_WANT_MSK) == TX_CON_WANT_SCL) { + + /* FIXME : this part is 1) awful, 2) tricky, 3) duplicated + * ... but it works. + * We need a better way to force a connection close without + * any risk of propagation to the other side. We need a more + * portable way of releasing a backend's and a server's + * connections. We need a safer way to reinitialize buffer + * flags. We also need a more accurate method for computing + * per-request data. + */ + s->req->cons->flags |= SI_FL_NOLINGER; + s->req->cons->shutr(s->req->cons); + s->req->cons->shutw(s->req->cons); + + if (s->flags & SN_BE_ASSIGNED) + s->be->beconn--; + + s->logs.t_close = tv_ms_elapsed(&s->logs.tv_accept, &now); + session_process_counters(s); + + if (s->txn.status) { + int n; + + n = s->txn.status / 100; + if (n < 1 || n > 5) + n = 0; + + if (s->fe->mode == PR_MODE_HTTP) + s->fe->counters.p.http.rsp[n]++; + + if ((s->flags & SN_BE_ASSIGNED) && (s->fe != s->be) && + (s->be->mode == PR_MODE_HTTP)) + s->be->counters.p.http.rsp[n]++; + } + + /* don't count other requests' data */ + s->logs.bytes_in -= s->req->l - s->req->send_max; + s->logs.bytes_out -= s->rep->l - s->rep->send_max; + + /* let's do a final log if we need it */ + if (s->logs.logwait && + !(s->flags & SN_MONITOR) && + (!(s->fe->options & PR_O_NULLNOLOG) || s->req->total)) { + s->do_log(s); + } + + s->logs.accept_date = date; /* user-visible date for logging */ + s->logs.tv_accept = now; /* corrected date for internal use */ + tv_zero(&s->logs.tv_request); + s->logs.t_queue = -1; + s->logs.t_connect = -1; + s->logs.t_data = -1; + s->logs.t_close = 0; + s->logs.prx_queue_size = 0; /* we get the number of pending conns before us */ + s->logs.srv_queue_size = 0; /* we will get this number soon */ + + s->logs.bytes_in = s->req->total = s->req->l - s->req->send_max; + s->logs.bytes_out = s->rep->total = s->rep->l - s->rep->send_max; + + if (s->pend_pos) + pendconn_free(s->pend_pos); + + if (s->srv) { + if (s->flags & SN_CURR_SESS) { + s->flags &= ~SN_CURR_SESS; + s->srv->cur_sess--; + } + if (may_dequeue_tasks(s->srv, s->be)) + process_srv_queue(s->srv); + } + + if (unlikely(s->srv_conn)) + sess_change_server(s, NULL); + s->srv = NULL; + + s->req->cons->state = s->req->cons->prev_state = SI_ST_INI; + s->req->cons->fd = -1; /* just to help with debugging */ + s->req->cons->err_type = SI_ET_NONE; + s->req->cons->err_loc = NULL; + s->req->cons->exp = TICK_ETERNITY; + s->req->cons->flags = SI_FL_NONE; + s->req->flags &= ~(BF_SHUTW|BF_SHUTW_NOW|BF_AUTO_CONNECT|BF_WRITE_ERROR|BF_STREAMER|BF_STREAMER_FAST|BF_AUTO_CLOSE); + s->rep->flags &= ~(BF_SHUTR|BF_SHUTR_NOW|BF_READ_ATTACHED|BF_READ_ERROR|BF_READ_NOEXP|BF_STREAMER|BF_STREAMER_FAST|BF_AUTO_CLOSE|BF_WRITE_PARTIAL); + s->flags &= ~(SN_DIRECT|SN_ASSIGNED|SN_ADDR_SET|SN_BE_ASSIGNED); + s->flags &= ~(SN_CURR_SESS|SN_REDIRECTABLE); + s->txn.meth = 0; + http_reset_txn(s); + txn->flags |= TX_NOT_FIRST; + if (s->be->options2 & PR_O2_INDEPSTR) + s->req->cons->flags |= SI_FL_INDEP_STR; + + /* make ->lr point to the first non-forwarded byte */ + s->req->lr = s->req->w + s->req->send_max; + if (s->req->lr >= s->req->data + s->req->size) + s->req->lr -= s->req->size; + s->rep->lr = s->rep->w + s->rep->send_max; + if (s->rep->lr >= s->rep->data + s->rep->size) + s->rep->lr -= s->req->size; + + s->req->analysers |= s->fe->fe_req_ana; + s->rep->analysers = 0; + } + /* FIXME: we're still forced to do that here */ s->rep->flags &= ~BF_DONT_READ; break; @@ -3828,6 +3961,7 @@ int http_process_res_common(struct session *t, struct buffer *rep, int an_bit, s int cur_idx; int conn_ka = 0, conn_cl = 0; int must_close = 0; + int must_del_close = 0, must_keep = 0; DPRINTF(stderr,"[%u] %s: session=%p b=%p, exp(r,w)=%u,%u bf=%08x bl=%d analysers=%02x\n", now_ms, __FUNCTION__, @@ -3903,7 +4037,9 @@ int http_process_res_common(struct session *t, struct buffer *rep, int an_bit, s * handled. We also explicitly state that we will close in * case of an ambiguous response having no content-length. */ - if (may_close || !(txn->flags & TX_RES_XFER_LEN)) + if ((may_close && + (may_keep || ((txn->flags & TX_CON_WANT_MSK) != TX_CON_WANT_SCL))) || + !(txn->flags & TX_RES_XFER_LEN)) txn->flags = (txn->flags & ~TX_CON_WANT_MSK) | TX_CON_WANT_CLO; /* Now we must adjust the response header : @@ -3915,6 +4051,12 @@ int http_process_res_common(struct session *t, struct buffer *rep, int an_bit, s */ if (may_keep && (txn->flags & TX_CON_WANT_MSK) == TX_CON_WANT_CLO) must_close = 1; + else if (((txn->flags & TX_CON_WANT_MSK) == TX_CON_WANT_SCL) && + may_close && (txn->flags & TX_RES_XFER_LEN)) { + must_del_close = 1; + if (!(txn->flags & TX_REQ_VER_11)) + must_keep = 1; + } txn->flags |= TX_CON_HDR_PARS; } @@ -3923,7 +4065,7 @@ int http_process_res_common(struct session *t, struct buffer *rep, int an_bit, s * returns a connection status that is not compatible with * the client's or with the config. */ - if ((txn->status >= 200) && must_close && (conn_cl|conn_ka)) { + if ((txn->status >= 200) && (must_del_close|must_close) && (conn_cl|conn_ka)) { char *cur_ptr, *cur_end, *cur_next; int cur_idx, old_idx, delta, val; int must_delete; @@ -3931,6 +4073,10 @@ int http_process_res_common(struct session *t, struct buffer *rep, int an_bit, s /* we just have to remove the headers if both sides are 1.0 */ must_delete = !(txn->flags & TX_REQ_VER_11) && !(txn->flags & TX_RES_VER_11); + + /* same if we want to re-enable keep-alive on 1.1 */ + must_delete |= must_del_close; + cur_next = rep->data + txn->rsp.som + hdr_idx_first_pos(&txn->hdr_idx); for (old_idx = 0; (cur_idx = txn->hdr_idx.v[old_idx].next); old_idx = cur_idx) { @@ -3959,6 +4105,7 @@ int http_process_res_common(struct session *t, struct buffer *rep, int an_bit, s txn->hdr_idx.used--; cur_hdr->len = 0; must_close = 0; + must_del_close = 0; } else { if (cur_end - cur_ptr - val != 5 || strncasecmp(cur_ptr + val, "close", 5) != 0) { @@ -4147,6 +4294,12 @@ int http_process_res_common(struct session *t, struct buffer *rep, int an_bit, s goto return_bad_resp; must_close = 0; } + else if (must_keep && !(txn->flags & TX_REQ_VER_11)) { + if (unlikely(http_header_add_tail2(rep, &txn->rsp, &txn->hdr_idx, + "Connection: keep-alive", 22) < 0)) + goto return_bad_resp; + must_keep = 0; + } if (txn->flags & TX_RES_XFER_LEN) rep->analysers |= AN_RES_HTTP_XFER_BODY; @@ -4197,6 +4350,12 @@ int http_response_forward_body(struct session *s, struct buffer *res, int an_bit if (unlikely(msg->msg_state < HTTP_MSG_BODY)) return 0; + /* note: in server-close mode, we don't want to automatically close the + * output when the input is closed. + */ + if ((txn->flags & TX_CON_WANT_MSK) == TX_CON_WANT_SCL) + buffer_dont_close(res); + if (msg->msg_state < HTTP_MSG_CHUNK_SIZE) { /* we have msg->col and msg->sov which both point to the first * byte of message body. msg->som still points to the beginning @@ -4302,6 +4461,11 @@ int http_response_forward_body(struct session *s, struct buffer *res, int an_bit /* option forceclose is set, let's enforce it now that the transfer is complete. */ buffer_abort(res); } + else if ((txn->flags & TX_CON_WANT_MSK) == TX_CON_WANT_SCL) { + /* server close is handled entirely on the req analyser */ + s->req->cons->flags |= SI_FL_NOLINGER; + buffer_shutw_now(s->req); + } if (res->flags & (BF_SHUTW|BF_SHUTW_NOW)) { if (res->flags & BF_OUT_EMPTY)