libevent之丟失header問題

 

本文爲原創,轉載請註明:http://www.cnblogs.com/gistao/

 

背景

分享一個hhvm使用http server方式來處理請求的問題及對應的patch。hhvm3+版本支持fastcgi模式,而以前的版本都只能用http serve模式來響應請求,因爲hhvm的http server支持的功能比較弱,如今大部分使用場景都選擇fastcgi了,而此次要說的問題僅存在於使用了http server模式的全部版本hhvm。html

HHVM問題

咱們發現線上的hhvm會有丟失http header的極小機率問題,在單臺日請求量近千萬狀況下只有2個左右的發生機率,而且將出問題的url在線下復現,也沒有復現,瞬間沒有一點點線索了。沒有辦法,用笨辦法:看代碼+排除法,沒有辦法的辦法。但事實證實每每笨辦法也是最終的辦法。nginx

問題相關

簡單的說,咱們的服務架構是nginx做爲反向代理服務器,來請求hhvm。Facebook的工程師基於libevent1.4.14b,增長了設置backlog功能和修復了一些內存泄漏問題,http的header的解析工做仍是在libevent裏獨立完成的,截取http.c部分代碼以下git

enum message_read_status
evhttp_parse_headers(struct evhttp_request *req, struct evbuffer* buffer) 
{
        char *line;
        enum message_read_status status = MORE_DATA_EXPECTED;

        struct evkeyvalq* headers = req->input_headers;
        while ((line = evbuffer_readline(buffer))
               != NULL) { 
                char *skey, *svalue;

                if (*line == '\0') { /* Last header - Done */
                        status = ALL_DATA_READ;
                        free(line);
                        break;  
                }       

                /* Check if this is a continuation line */
                if (*line == ' ' || *line == '\t') { 
                        if (evhttp_append_to_last_header(headers, line) == -1)
                                goto error;
                        free(line);
                        continue;
                }       

                /* Processing of header lines */
                svalue = line; 
                skey = strsep(&svalue, ":");
                if (svalue == NULL)
                        goto error;

                svalue += strspn(svalue, " ");

                if (evhttp_add_header(headers, skey, svalue) == -1)
                        goto error;

                free(line);
        }       

        return (status);

 error:
        free(line);
        return (DATA_CORRUPTED);
}

能看出問題嗎,因爲定位問題過程比較曲折,這裏不羅嗦了。github

Libevent問題

bug出如今buffer.c裏的readline函數,以下服務器

char *
evbuffer_readline(struct evbuffer *buffer)
{
        u_char *data = EVBUFFER_DATA(buffer);
        size_t len = EVBUFFER_LENGTH(buffer);
        char *line;
        unsigned int i;

        for (i = 0; i < len; i++) {
                if (data[i] == '\r' || data[i] == '\n')
                        break;  
        }       

        if (i == len) 
                return (NULL); 

        if ((line = malloc(i + 1)) == NULL) { 
                fprintf(stderr, "%s: out of memory\n", __func__);
                return (NULL); 
        }       

        memcpy(line, data, i);
        line[i] = '\0'; 

        /*      
         * Some protocols terminate a line with '\r\n', so check for
         * that, too.
         */
        if ( i < len - 1 ) { 
                char fch = data[i], sch = data[i+1];

                /* Drain one more character if needed */
                if ( (sch == '\r' || sch == '\n') && sch != fch )
                        i += 1; 
        }       

        evbuffer_drain(buffer, i + 1); 

        return (line);
}

此函數的功能就是截取一行出來,而後循環解析完全部header。rfc2616規定行分隔符是\r\n,而libevent認定的換行符(eof)倒是很是的寬鬆,好比\r就行。若是收包時剛好將\r和\n分離在兩個包裏,那麼後邊這個包解析時第一個字節就是\n,libevent解析時會認爲這是一行(其實仍是以前的行),而這行並無內容,即內容是\0,這會形成evhttp_parse_headers函數直接認爲header已經所有解析完畢了,也就是說丟失了header架構

多說一句libevent的2x版本,相比以前的版本代碼變化很大,單就解析header來講,已經提供了三種級別的換行符認定標準,不過默認仍是最鬆散級別。app

Libevent patch

因爲咱們這裏的環境都是可控的,不會存在那些亂七八糟換行符,因此就按照強約束\r\n(LRCF)來解析,針對libevent1.4.14b的patch以下socket

--- ./buffer.c  2010-06-20 21:06:04.000000000 +0800
+++ ./buffer.c  2014-04-21 14:22:25.783883798 +0800
@@ -211,45 +211,35 @@
 char *
 evbuffer_readline(struct evbuffer *buffer)
 {
-       u_char *data = EVBUFFER_DATA(buffer);
-       size_t len = EVBUFFER_LENGTH(buffer);
-       char *line;
-       unsigned int i;
-
-       for (i = 0; i < len; i++) {
-               if (data[i] == '\r' || data[i] == '\n')
-                       break;
-       }
-
-       if (i == len)
-               return (NULL);
-
-       if ((line = malloc(i + 1)) == NULL) {
-               fprintf(stderr, "%s: out of memory\n", __func__);
-               return (NULL);
-       }
+    u_char *data = EVBUFFER_DATA(buffer);
+    size_t len = EVBUFFER_LENGTH(buffer);
+    char *line;
+    unsigned int i;
+    
+    for (i = 0; i < len; i++) {
+        if (data[i] == '\r') {
+            if (i + 1 < len && data[i+1] == '\n') {
+                break;
+            }
+        }
+    }
+
+    if (i == len)
+        return (NULL);
+
+    if ((line = malloc(i + 1)) == NULL) {
+        fprintf(stderr, "%s: out of memory\n", __func__);
+        return (NULL);
+    }

-       memcpy(line, data, i);
-       line[i] = '\0';
+    memcpy(line, data, i);
+    line[i] = '\0';

-       /*
-        * Some protocols terminate a line with '\r\n', so check for
-        * that, too.
-        */
-       if ( i < len - 1 ) {
-               char fch = data[i], sch = data[i+1];
+    evbuffer_drain(buffer, i + 2);

-               /* Drain one more character if needed */
-               if ( (sch == '\r' || sch == '\n') && sch != fch )
-                       i += 1;
-       }
-
-       evbuffer_drain(buffer, i + 1);
-
-       return (line);
+    return (line);
 }

-
 char *
 evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out,
                enum evbuffer_eol_style eol_style)
                                                   
相關文章
相關標籤/搜索