Nginx与Lua: 一种利用error_page实现鉴权服务故障转移的尝试

2015-11-27 Li Shuai 更多博文 » 博客 » GitHub »

Nginx Lua 项目

原文链接 https://cyrusin.github.io/2015/11/27/lua-20151127/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


为了响应总菊的要求, 互联网智能电视盒子必须接入牌照方的播控平台, 登陆时必须认证一下, 认证通过才让你接入我们的服务, 否则你就呵呵了。我们有两个接口会代理一下用户的请求, 大致逻辑是:

客户端首先请求接口A, 会把MAC地址带过来, 接口A的handler会做一些校验, 然后根据一些其他的参数, 重新组装一个请求去访问牌照方的接口, 根据参数的不同会请求不同的接口 牌照方会根据用户的MAC地址和其他信息返回鉴权的结果, 返回里面会有某个字段表示成功还是失败 接口A对牌照方的接口做些校验, 没问题的返给客户端, 客户端根据鉴权结果, 成功则继续进行剩下的鉴权,失败则终止请求, 用户可能就无法使用剩下的服务

看起来是个很简单的只读性的接口, 一开始的时候把这个东西和其他的业务放在一起, 但是这个接口相当敏感, 尤其是在每天的使用高峰的时候, 这个时候盒子的开启操作数量很大, 由于每次开启进入盒子都要做认证, 所以这个接口的请求量随着盒子用户的增加也是与日剧增, 关键是高峰期的时候难免接口响应的会慢一点, 这时候根据一开始的设计, 用户总是无法进入盒子, 什么操作也做不了, 只能投诉客服。

这个接口, 按要求是不允许使用缓存的, 因为上面要求鉴权必须实时生效。后来我们追查这个接口超时多的原因, 发现调用的牌照方的接口性能不是很好, 请求量稍微多点, 比如每天的高峰期、周末, 基本就瘫掉了。超时一大堆, 接口也跟着超时一大堆, 一到高峰期就504, 投诉量太多, 甚至惊动了我们部门的老大。

先是把这个服务独立出来, 但是由于超时来自上游, 我们在调接口的时候虽然加了超时就自动返回默认鉴权成功的功能, 但由于高峰期超时数量实在太大, 超时后又得打日志什么的, 反而服务自己的IO好像又影响了Tornado的性能, 不管你怎么优化, Tornado就好象有一个瓶颈, 始终跨不过那道槛。高峰期504还是有一些。

都知道Tornado是异步非阻塞的高性能服务器, 但在这个问题上, 我一直觉得我们使用的方式有问题, 就喜欢加进程数, 以为进程多了就一定好, 根据我自己的实验, 从压测的结果来看, 当进程数远大于你的CPU核数的时候, 其实性能是不升反而又微弱下降的, 而且进程数太多, 反而超时的请求多了起来。我的理解是, 当进程数太多的时候, CPU花费了大量时间在进程切换上, 使得每个进程占有CPU的平均时间反而少了, 造成了在一定时间内, 无谓等待的请求反而多了, 超时也就来了。

这次我们老大也被这个问题搞得挺纠结, 我决定好好分析解决一下这个问题。又轮到哥出手了。

我只好拿出杀招了, 之前他们总是把焦点聚集到Tornado和Tornado的上游, 我的思路是, 人家的东西超时, 你期待人家替你改, 这就成了坐以待毙了。现在的问题就是: Tornado总是在高峰期超时, 客户端面对504没有好好处理, 保守的认为是鉴权失败, 用户就什么也干不了了。

这次我解决这个问题继续使用我的暗器, Lua, 我们有一部分服务的接入过滤使用了Lua在Nginx中处理逻辑, 不论可维护性还是性能都很好。这次我还是用Lua。具体解决方案就是:

Nginx捕获upstream的异常(504, 502)等, 内部跳转, 由Lua接管异常请求, 通过Lua实现一部分接口的逻辑, 根据不同的请求返回默认鉴权成功的结果, 优先让用户使用服务。

由于超时主要发生在高峰期, 所以平时的时候只有极其微小的概率Tornado会超时, 所以这个基本是不影响正常鉴权逻辑的。只是在高峰期的时候, 当Tornado及Tornado的上游扛不住的时候, 我们在Nginx里内嵌的Lua会把异常请求接管, 然后返回给用户默认成功的结果, 这样用户就不会在高峰期由于服务响应的问题, 被踢掉了。

实践证明Nginx结合Lua的这种解决方案在线上取得了很好的效果, 毕竟Nginx的性能是足足的, 而Lua也是名副其实的快, 这俩黄金组合的搭配可以说基本实现了全天服务无504, 高峰期也是兢兢业业, 十分扛打, 从日志来看, 牌照方依然在每天给你千八个超时, Tornado在高峰期依然会有搞不定的赶脚, 但这些通通都被Nginx和Lua洗地了, Nginx和Lua实在是十分可靠的接盘侠。

部分代码:

location ~ /url {
    access_by_lua_file <path>/check_***.lua; #Lua做校验

    proxy_pass http://tornado;
    proxy_redirect      default;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header    X-Real-IP $x_remote_addr;
    proxy_set_header    Host $http_host;
    proxy_set_header    Range $http_range;

    error_page 502 504 =200 @device_entry_default; #故障转移
} 

location @device_entry_default {
    #lua来接管异常请求
    content_by_lua '  
        local device_entry = ""
        if ngx.var.arg_pid==... then
            ...
        elseif ngx.var.arg_static=="0" then
            ...
        else
            ...
        end
        ngx.header.content_type = "application/json; charset=UTF-8"
        ngx.say(default_entry)
    ';
}