nginx日志解析器优化

过去很长一段时间,我们在用 ngxtop 这个模块里面的log parser对nginx日志进行分析。但是很快发现, 这个解析器的效率不行,正好组里面一位同学学习Go语言,所以用go语言重写了一版,效率提高了接近10倍。

今天我重新回顾一下这个问题,看看是否可以优化,或者说看看python版本的瓶颈在什么地方。那位同学Go 语言版本使用的是 gonx 这个库。我上去看了一下代码,好像和 ngxtop 思路一样,都是根据log_format 生成正则表达式,然后对日志进行匹配和解析。

我对原来日志解析器进行profiling, 发现大部分时间都还是在正则表达式匹配上。如果是这样的话,那么 python版本和Go版本性能应该没有这么大的差异才对。在浏览 gonx 文档的时候突然发现下面这段话

Format is nginx-like, here is example

`$remote_addr [$time_local] "$request"`

It should contain variables in form $name. The regular expression will be
created using this string format representation

`^(?P<remote_addr>[^ ]+) \[(?P<time_local>[^]]+)\] "(?P<request>[^"]+)"$`

在匹配每个variable的时候,正则表达式会根据末尾的字符进行阶段,这应该是很显然的逻辑。比如 对于上面的 [$time_local], 因为这个变量末尾是], 那么正则可以写成 [^]].


回过头来看 ngxtop 生成的正则表达式是有问题的。对于下面这个log_format, ngxtop生成的正则表达式是,可以看到没有使用结束符进行优化

"$remote_addr - - [$time_local] \"$host\" \"$request\" $status $body_bytes_sent \"$http_referer\"
 \"$http_user_agent\" \"$http_x_forwarded_for\" \"$upstream_response_time\" \"$request_time\""

(?P<remote_addr>.*) - - \[(?P<time_local>.*)\] "(?P<host>.*)" "(?P<request>.*)" (?P<status>.*) (?P<body_bytes_sent>.*)
 "(?P<http_referer>.*)" "(?P<http_user_agent>.*)" "(?P<http_x_forwarded_for>.*)" "(?P<upstream_response_time>.*)" "(?P<request_time>.*)"

如果使用这个版本分析 7w 条nginx日志的话花费差不多9s.

➜  workspace python ngx_log_parser.py
py 8.90 secs

我按照gonx的思路重新生成了正则表达式之后,上面log_format生成的正则如下

^(?P<remote_addr>[^ ]*)\ \-\ \-\ \[(?P<time_local>[^]]*)\]\ \"(?P<host>[^"]*)\"\ \"(?P<request>[^"]*)\"\ (?P<status>[^ ]*)\
(?P<body_bytes_sent>[^ ]*)\ \"(?P<http_referer>[^"]*)\"\ \"(?P<http_user_agent>[^"]*)\"\ \"(?P<http_x_forwarded_for>[^"]*)\"
\ \"(?P<upstream_response_time>[^"]*)\"\ \"(?P<request_time>[^"]*)\"$

使用这个版本分析的话,花费时间和Go语言版本差不多,都在1s左右。Great !!!

➜  workspace python ngx_log_parser.py
go 1.20 secs
py 1.12 secs

附带上生成正则表达式的代码 Implementation

def custom_build_pattern(log_format):
    buf = '^'
    var_name = ''
    var_mode = False
    for c in log_format:
        if c == '$':
            var_mode = True
        elif c in string.ascii_letters or c in string.digits or c in '_':
            if var_mode:
                var_name += c
            else:
                buf += '\{}'.format(c)
        else:
            if var_mode:
                buf += '(?P<{}>[^{}]*)\{}'.format(var_name, c, c)
                var_mode = False
                var_name = ''
            else:
                buf += '\{}'.format(c)
    if var_mode:
        buf += '(?P<{}>[^$]*)'.format(var_name)
        var_mode = False
        var_name = ''
    buf += '$'
    return buf