这是五个小时与一个字符的战斗html
是的,做为一个程序员,你每每发现,有的时候你花费了数小时,数天,甚至数星期来查找问题,但最终可能只花费了数秒,改动了数行,甚至几个字符就解决了问题。此次给你们分享一个困扰了我好久,我花了五个小时才查找出问题缘由,最终只添加了一个字符解决了的问题。nginx
咱们的业务系统比较复杂,但最终提供给用户的访问接口比较单一,都是使用 Nginx 来作一个代理转发,而这个代理转发,每每须要匹配不少种不一样类型的 URL 转给不一样的服务。这就使得咱们的 Nginx 配置文件变得很复杂,粗略估计了下,咱们有近20个 upstream,有近60个 location 匹配。这些配置按照模块分布在不一样的文件中,虽然复杂,可是仍然在咱们的努力下运行的良好。直到有一天,有位同事给我反映说偶尔有些 URL 会出现 404 的问题。一开始没太在乎,由于他也说不许是哪种 URL 才遇到这个问题。程序员
后来,慢慢的查找,找到了一些规律,一开始只知道是 tomcat 那边返回 404了,想到 Nginx 都代理给了 tomcat,一开始就怀疑是程序的问题,不会想到是 Nginx。正则表达式
我开始查找代码的问题,我在本地的开发环境,尝试了好久,我使用 8080 端口访问,不论如何都是正确的结果,但是生产环境就是不行。而后我就听信了某坑友同事的理论,重启解决 95% 的问题,重装解决 100%的问题,我尝试重启了 tomcat 和 Nginx,依然不行,而后是重装,你猜结果如何????? ------想啥呢?固然也是不行!tomcat
后来就开始怀疑是生产环境和开发环境的差别,去服务器上访问 8080 端口,仍然是能够的。但是一通过 Nginx 代理,就不行。这个时候才开始怀疑是 Nginx 出了什么问题。服务器
Nginx 怎么会出问题呢,业务系统中 URL 模式 /helloworld/* ,这样的 URL 咱们都是统一处理的。怎么会出现一些行,一些不行呢。问题表现为 A URL (/helloworld/nn/hello/world)没问题,而 B URL(/helloworld/ii/hello/world) 有问题。app
因此到目前为止,基本能够确定是 Nginx 的 location 上出了一些问题。curl
因篇幅有限,为了直面本次问题的核心,我再也不贴出完整的 Nginx 配置,我简化这次问题的模型。请看以下 Nginx 配置,这是咱们以前的会致使问题的错误配置模型。测试
worker_processes 1; error_log logs/error.log; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; log_format main '$remote_addr - $request_time - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log logs/access.log main; sendfile on; keepalive_timeout 65; gzip on; server { listen 80; server_name localhost; location / { root html; index index.html index.htm; } location = /helloworld { return 602; } location /helloworld { return 603; } ## 生产环境中以下两个 location 在另一个文件中,经过 include 包含进来 location /ii { return 604; } location ~ /ii/[^\/]+/[^\/]+ { return 605; } ## location ~ ^/helloworld/(scripts|styles|images).* { return 606; } } }
注意,这里有几点须要说明一下,生产环境的 Nginx 服务器配置文件比这里要复杂不少,并且是按模块分布在不一样的文件中的。这里简化模型后,使用 Http 响应状态码 60x 来区分到底被哪一个 location 匹配到了。
我针对当时的状况,作了大量尝试,最终的简化版本以下:url
上面这些尝试支持读者自行试验,Nginx 配置文件是完整可用的,我本地 Nginx 的版本是1.6.2
问题就在这里:我这里是过后,把这些匹配 location 标记成了不一样的响应码,才方便查找问题。当发现这个不符合预期后,我仍是难以理解,为什么我一个以 /helloworld 开头的 URL 会被匹配到 605 这个以 /ii 开头的 location 里面来。在当时的生产环境中,以 /ii 的配置统一放在另一个文件中,这里是很难直观的察觉出来这个 /ii 跟访问的 URL 里面的 /ii 的关系。
我不得不从新编译了 Nginx ,加上了调试参数,修改配置项,看调试日志了。
这里再也不讲如何给 Nginx 加调试的编译参数,可自行查看相关文档。修改配置项很简单,只须要在
error_log logs/error.log;
后面加上 debug 就能够了。
打出详细调试日志后,访问
http://localhost/helloworld/ii/hello/world
我获得了这样的一段日志(省略掉了先后无用的日志,只保留有意义的一段):
2015/02/02 15:38:48 [debug] 5801#0: *60 http request line: "GET /helloworld/ii/hello/world HTTP/1.1" 2015/02/02 15:38:48 [debug] 5801#0: *60 http uri: "/helloworld/ii/hello/world" 2015/02/02 15:38:48 [debug] 5801#0: *60 http args: "" 2015/02/02 15:38:48 [debug] 5801#0: *60 http exten: "" 2015/02/02 15:38:48 [debug] 5801#0: *60 http process request header line 2015/02/02 15:38:48 [debug] 5801#0: *60 http header: "User-Agent: curl/7.37.1" 2015/02/02 15:38:48 [debug] 5801#0: *60 http header: "Host: localhost" 2015/02/02 15:38:48 [debug] 5801#0: *60 http header: "Accept: */*" 2015/02/02 15:38:48 [debug] 5801#0: *60 http header done 2015/02/02 15:38:48 [debug] 5801#0: *60 event timer del: 4: 1422862788055 2015/02/02 15:38:48 [debug] 5801#0: *60 rewrite phase: 0 2015/02/02 15:38:48 [debug] 5801#0: *60 test location: "/" 2015/02/02 15:38:48 [debug] 5801#0: *60 test location: "ii" 2015/02/02 15:38:48 [debug] 5801#0: *60 test location: "helloworld" 2015/02/02 15:38:48 [debug] 5801#0: *60 test location: ~ "/ii/[^\/]+/[^\/]+" 2015/02/02 15:38:48 [debug] 5801#0: *60 using configuration "/ii/[^\/]+/[^\/]+" 2015/02/02 15:38:48 [debug] 5801#0: *60 http cl:-1 max:1048576 2015/02/02 15:38:48 [debug] 5801#0: *60 rewrite phase: 2 2015/02/02 15:38:48 [debug] 5801#0: *60 http finalize request: 605, "/helloworld/ii/hello/world?" a:1, c:1 2015/02/02 15:38:48 [debug] 5801#0: *60 http special response: 605, "/helloworld/ii/hello/world?" 2015/02/02 15:38:48 [debug] 5801#0: *60 http set discard body 2015/02/02 15:38:48 [debug] 5801#0: *60 posix_memalign: 00007FC3BB816000:4096 @16 2015/02/02 15:38:48 [debug] 5801#0: *60 HTTP/1.1 605 Server: nginx/1.6.2 Date: Mon, 02 Feb 2015 07:38:48 GMT Content-Length: 0 Connection: keep-alive
能够看到,Nginx 测试了几回 location 匹配,最终选择了
~ "/ii/[^\/]+/[^\/]+"
这个做为最终的匹配项。到这里问题就彻底展示出来了,咱们原本的意思,是要以 /ii 开头,后面有两个或者更多的 / 分割的 URL 模型才匹配,可是这里的正则表达式匹配写的不够精准,致使了匹配错误。正则表达式没有限制必须从开头匹配,因此才会匹配到 /helloworld/ii/hello/world 这样的 URL 。
解决办法就是在这个正则表达式前面加上 ^ 来强制 URL 必须以 /ii 开头才能匹配.
由
/ii/[^\/]+/[^\/]+
变成
^/ii/[^\/]+/[^\/]+
至此,这个坑被填上了,消耗的是五个小时和一个字符。
相信不少人在写 Nginx 的location 的时候都会 location ~ /xxx 或者 location /iii 这样简单了事,可是我想说的是能尽可能精确就尽可能精确,不然出现问题的时候,很是难以查找。
有关 Nginx 的 location 匹配规则,能够查看: http://nginx.org/en/docs/http/ngx_http_core_module.html
这个问题看似简单,却也隐含了很多问题,值得咱们深思。
本文做者: 王振威
文章出自: Coding 官方技术博客 如需转载,请注明做者与出处,谢谢