背景
最近一直在做公司的应用软件服务架构的升级工作,里面涉及使用mod_proxy替换先前的mod_ajp,因为我们要用jetty7。
同时万恶的jetty 7对ajp协议支持不是很好, 具体可见我的另一篇博文: 纠结的mod_jk与jetty的组合。 在线下测试少量的请求没啥问题,一到线上跑个几分钟就开始抛异常了,查了jetty的mail list,也有人报类似的bug。
所以后续的工作重心还是回到mod_proxy_http上来,今天在调试mod_proxy配置时,出了一些小插曲,记录分享一下给大家,免得大家再走歧路。
配置需求
大致url类型介绍:
目前公司的url基本是按模块进行划分,比如 http://域名/module/xxxx.html, 域名后多了一个module路径,用于区分不通的业务,比如home , admin , product等。
对mod_proxy的使用需求:
- 启用长连接。不能每次proxy转发一下,就新建一个socket,性能吃不消
- 连接池只有一个。尽量公用连接池,目前各module之间的压力分布不均,需要合并所有module的proxy连接池,避免出现多个ProxyPass的worker连接池
Apache mod_proxy模块介绍
主要的资料还是来自于官方文档:http://httpd.apache.org/docs/2.2/mod/mod_proxy.html
涉及的主要配置原语:
- ProxyPass
- ProxyPassMatch 区别于ProxyPass就只是匹配方式是通过regex,其他类似
- ProxyRequests 正向代理开关
- ProxyPassReverse 反向代理,修改返回的http header信息
- ProxyPreserveHost 反向代理,是否修改请求的header信息
其他具体配置含义不多做介绍,大家可以看官方文档。
实施过程
1. 考虑mod_proxy需求1,保持长链接
这个mod_proxy本身就支持worker池的概念,不过需要配置一下几个参数:
名称 | 默认值 | 描述 | 官网文档描述 |
min | 0 | 最小保持的连接数 | Minimum number of connections that will always be open to the backend server. |
max | 1...n |
所谓的硬性最大值 最大可保持的连接数,如果是prefork模式则值为1,如果是走worker模式,值等价于ThreadsPerChild配置
|
Hard Maximum number of connections that will be allowed to the backend server. The default for a Hard Maximum for the number of connections is the number of threads per process in the active MPM. In the Prefork MPM, this is always 1, while with the Worker MPM it is controlled by the ThreadsPerChild .
Apache will never create more than the Hard Maximum connections to the backend server. |
smax | max | 所谓的软性最大值,如果连接数超过smax后,大于smax的链接就会进行ttl空闲连接管理 | Upto the Soft Maximum number of connections will be created on demand. Any connections above smax are subject to a time to live or ttl . |
connectiontimeout | timeout | 创建链接的超时时间,单位毫秒 | Connect timeout in seconds. The number of seconds Apache waits for the creation of a connection to the backend to complete. By adding a postfix of ms the timeout can be also set in milliseconds. |
timeout | ProxyTimeout | 等待后端的应用返回数据的超时时间,不可太长,也不可太短 | Connection timeout in seconds. The number of seconds Apache waits for data sent by / to the backend. |
ttl | - | 空闲连接的管理时间 | Time To Live for the inactive connections above the smax connections in seconds. Apache will close all connections that has not been used inside that time period. |
最后的配置:
1.min=5 smax=16 ttl=600 timeout=20
说明:
- 不指定max,保持和ThreadPerChild一致
- timeout,不用设置太大,保持和原先mod_jk一致,使用20秒。如果是下载服务,可适当调大该值
- ttl空闲连接,可适当设置大一些,tomcat默认的是20秒。切忌: 需要保证 ttl <= 后端应用空闲连接的管理时间,不然会出现半开链接。
2. 共享连接池配置
思路一: 坏人匹配策略
- 先整理出应用中不需要应用进行处理的url列表,比如原先的Url Rewrite,apache下的静态资源文件
- 对不需要处理的url,逐一配置过滤
1.ProxyPass /image/ ! ProxyPass /ok.html !
- 后对应用根目录进行proxy配置(只有这么一个公用连接池)
1.ProxyPass / http://localhost:7001/
mod_proxy进行proxy匹配时,是按照配置文件的顺序进行查找匹配,具体可见官方文档描述,直接贴英文。
1.The configured ProxyPass and ProxyPassMatch rules are checked in the order of configuration. The first rule that matches wins.
2.So usually you should sort conflicting ProxyPass rules starting with the longest URLs first.
3.Otherwise later rules for longer URLS will be hidden by any earlier rule which uses a leading substring of the URL.
4.Note that there is some relation with worker sharing.
5.
6.For the same reasons exclusions must come before the general ProxyPass directives
说明:
- 因为考虑直接放开所有/目录的请求,风险比较大。特别是在应用有变化时,过滤url列表比如有遗漏,可能就会引发系统安全问题,比如一些jmx,web-console的后台url就会被打开。
- 原先应用使用mod_jk,采取的都是配置需要转发的请求列表,所以在列出所有过滤列表时,可能会存在遗漏的,所以最终放弃了该方案
思路二:好人匹配策略
- 先整理所有的好人列表,这个挺好整理,把公司的几个module列一下即可,同时加上一些监控的url
- 通过ProxyPassMatch原语,整理出正则匹配url,只有这里一个公用连接池
1.ProxyPassMatch ^/(home|admin|product|monitor)/(.*)$ http://localhost:7001
- 最后可以加上拦截/目录请求,也可以不做(因为mod_proxy如果找不到匹配的ProxyPass,自然就不会处理该请求了)
1.ProxyPass / !
华丽的分割线
小插曲:在实施ProxyPassMatch配置时,真正的被apache官方文档误导了一把,万恶的主啊。花费了我近一天的时间排查问题。希望大家可以引起重视
官方文档针对ProxyPassMatch有这么一个例子:
大体意思是也很好理解,说ProxyPassMatch可以通过$1,$2提取正则匹配的内容,并且添加在worker url之后,组成最终的目标url。
所以参照这官方的例子,我最初的配置就为:
1.ProxyPassMatch ^(home|admin|product|monitor)/(.*)$ http://localhost:7001/$1/$2
通过apache ab进行系统压力测试时,发现一个很严重的问题
1../ab -k -c 15 -n 50000 http://10.20.156.49:2100/member/signin.htm
2.
3.-k 指定保持keepalive
4.-c 并发数
5.-n 请求数
再通过netstat -nat命令看了下tcp状态,发现近2000左右的TIME_WAIT,在停止ab施压后,发现TIME_WAIT连接很快会得到释放
最后通过tcpdump在服务器上,发现了问题,每个请求都是走了新的socket链接,tcp链接快速打开,快速关闭,因此出现了大量了TIME_WAIT状态。针对tcpdump的使用介绍,可以查看我的另一篇博文:tcp链接的几种状态&tcpdump抓包
1.tcpdump -s 0 -i lo port 2200 -nn
2.
3.说明:port 2200为后端应用服务端口,-i 必须指定为loopback网络接口,因为apache和后端应用之间走的是loopback网络接口
网上google了一把无果,也在stackoverflow论坛上逛了一圈,没发现有类似的question提问。最终很无奈,在查看apache maillist的时候,发现了这么一个bug。
Bug 43513 - Persistent backend connections for ProxyPassMatch
1.ProxyPassMatch creates a worker like ProxyPass does,but it uses regex URI for a worker name.
2.For example, your httpd.conf:
3.
4.ProxyPassMatch ^/test/(\d+)/foo.jpg <a href="http://backend.example.com/%241/foo.jpg">http://backend.example.com/$1/foo.jpg</a>
5.
6.then worker name is "http://backend.example.com/$1/foo.jpg", so URL never matches and the worker is no longer used.
打开了apache的LogLevel为debug,看到了的确建立了以$1/$2为url的worker池
的确,我在测试时
1.[Tue Nov 09 22:12:06 2010] [debug] proxy_util.c(1818): proxy: grabbed scoreboard slot 0 in child 7897 for worker <a href="http://localhost:2200/%241/%242">http://localhost:2200/$1/$2</a>
2.[Tue Nov 09 22:12:06 2010] [debug] proxy_util.c(1837): proxy: worker http://localhost:2200/$1/$2 already initialized
按照bug的描述,因为通过最终http://localhost:7001/$1/$2,经过$1,$2渲染后最终的url是一个真实的目标url,它与对应的worker url不匹配,所以无法使用该worker的pool池配置。
最后调整配置为:
1.ProxyPassMatch ^/(home|admin|product|monitor)/(.*)$ http://localhost:7001
分析:
注意最后的worker url没有加$1/$2,而是直接使用了/目录,mod_proxy在匹配到ProxyPassMatch后,会将整个URI和worker url进行拼接,最终的url为: http://localhost:7001/admin/xxx.html,再根据mod_proxy针对worker sharing,子目录url可以重用父目录url的worker连接池。所以这里/admin/xxx.html可以重用/的连接池,这样就实现了公用。
在最后的测试中,抓了下包,基本没有出现TIME_WAIT的异常情况,问题得到圆满解决,希望本文对mod_proxy的使用对你能有所帮助。