Nginx与Lua

nginx与apache的区别

最常被提及的是,epoll与select的区别。

1. select仅会告知app有read、write or timeout事件发生,至于是哪个fd ready了,需要app遍历检查,一旦fd个数较多,性能就会比较低下。而epoll会直接告知app哪些fds ready了,直接操作即可。

2. select在每次调用时,都需要把fd列表从用户态copy至内核态,而epoll仅在初始化时copy一次。

 

在与php等脚本语言结合方式上也有所不同。

apache一般通过module的方式,加载php_mod.so,php运行在apache进程空间里,形成Process Per Connection or Thread Per Connection方式,而web请求一般都是IO阻塞型的,需要访问后端服务、DB、cache等读取、整合数据,在这个过程中,对应的httpd进程也就被占用住了,无法提供服务。一旦达到httpd进程数上限,就只能refuse了。

nginx通过fastcgi协议,网络交互的方式与php_fpm进程通信。php处理事务的时候,nginx仍然可以接收请求、定位可用的php进程、排队。

 

扩展性方面,lua与nginx完美结合,而lua又可以方便的与c/c++ so包交互,使功能的扩展非常方便。

 

其实在多种细节角度,nginx也做的很好。(再翻翻nginx内核,然后补充吧)

nginx的防攻击实现(request limit等module的用法)

nginx的ngx_http_limit_req_module可以限制某一IP在一段时间内的访问频率,有burst模式和匀速模式。

其不适合我们的地方是:单机版,没有白名单机制,规则太简单,无法抗DDOS(主要指7层DDOS)。

所以,首先用lua对其做了一层封装,当limit module返回非200 code时,检查ip是否在白名单dict里,否则就返回403 forbidden。

随后,引入防攻击离线计算模块,规避单机版和规则问题。

lua exception时,如何不影响整体流程

采用pcall的方式,一旦出错不会导致进程退出,而是返回err code。

nginx封禁名单的过期机制

lua_shared_dict本身支持expire机制。

nginx dict的实现原理

利用shared memory实现,被当前nginx实例的worker进程共享。(如果一个服务器上运行多个nginx实例,相互间无法访问)

也利用LRU机制释放内存,利用set写入数据时,如果分配的内存空间满了,则会优先释放expired数据,如果还不够,则会根据LRU算法,flush掉一些not-yet-expired data。如果是使用safe_set,则不会flush掉一些not-yet-expired data,而是返回false。

nginx读取header、body时需注意的事情

nginx对body采取延迟读取的策略, 而我们在做灰度发布时,会希望在access phase就获取body内容。所以需要收工操作。

首先根据content_length检查是否存在http body(这里当chunked时,可能会有问题),由于nginx会对http body做延迟读,所以需要显示调用pcall(ngx.req.read_body)进行读取(若之前已读取过,该方法会立刻返回),然后通过ngx.req.get_post_args方法获取POST数组,并取出对应的POST参数(可能为空)。

ngx.req.read_body()   --- 触发读取到内存或文件
local args = ngx.req.get_post_args()  ---- 从内存中读取post为hash table

可以看到,简单调用get_post_args可能无法完整获取数据,如果post请求太大,被刷到文件里的话。具体参考:http://wiki.nginx.org/HttpLuaModule#ngx.req.read_body

接入层对nginx phase的扩展

1.启动阶段:

分配内存空间,lua所需dict,包括:

  • IP白名单,5M
  • router_rule_dict,路由规则,可用于灰度发布、流量隔离、封禁等,10M
  • 防攻击相关,inner_ip_dict, only_ip_dict, ip_ua_dict, ip_uri_dict, ip_refer_dict,10M

并初始化limit_req_zone,区分为one(1.0流量)、static(静态请求)、dynamic(动态请求)三种。

还需设置lua的cpath和path,分别用于加载so包和lua代码。

通过init_by_lua,从文件加载白名单到dict,从文件加载多个模块的router rule到dict。

2.请求执行阶段:

  •     NGX_HTTP_POST_READ_PHASE = 0,    /* 读取请求 */
  •     NGX_HTTP_SERVER_REWRITE_PHASE,   /* server级别的rewrite */
  •     NGX_HTTP_FIND_CONFIG_PHASE,      /* 根据uri查找location */
  •     NGX_HTTP_REWRITE_PHASE,          /* localtion级别的rewrite */
  •     NGX_HTTP_POST_REWRITE_PHASE,     /* server、location级别的rewrite都是在这个phase进行收尾工作的 */
  •     NGX_HTTP_PREACCESS_PHASE,        /* 粗粒度的access */
  •     NGX_HTTP_ACCESS_PHASE,           /* 细粒度的access,比如权限验证、存取控制 */
  •     NGX_HTTP_POST_ACCESS_PHASE,      /* 根据上述两个phase得到access code进行操作 */
  •     NGX_HTTP_TRY_FILES_PHASE,        /* 实现try_files指令 */
  •     NGX_HTTP_CONTENT_PHASE,          /* 生成http响应 */
  •     NGX_HTTP_LOG_PHASE               /* log模块 */

nginx lua module的一些指令只能在特定phase执行,这样对编码造成了影响。例如,在针对Hessian API做路由时,我们需要先读取body内容并decode后,才可以根据router rule,决定如何分流。

而这里有一个矛盾点,就是我们一般会在set_by_lua里根据分流规则,返回待路由的upstream分组。而set_by_lua里是不允许读取request body内容的。所以,很自然,我们决定使用rewrite_by_lua计算分流,但若根据rewrite_by_lua对某个全局变量,类似bUse2,进行赋值。再在后面用if判断bUse2的值,进行proxy或者fastcgi的话,由于if也是在rewrite阶段,且默认会在rewrite_by_lua前执行,那就意味着,执行顺序反了,rewrite永远都不会用到rewrite_by_lua赋值的结果!当然,我们可以用rewrite_by_lua_no_postpone on把rewrite_by_lua放到rewrite前执行,但又遇到一个问题,rewrite_by_lua里是不能给未初始化的ngx.var变量赋值的(未分配内存),而初始化需要用nginx的set指令,该指令又是在rewrite阶段执行……

最终的解决方案是,在access_by_lua里调用dispatcher.dispatch方法,读取Body、反序列化,并利用ngx.exec调用内部location方法进行分流。

一些限制总结:

access_by_lua在phase: access tail执行,set_by_lua在phase: rewrite执行,rewrite_by_lua在phase: rewrite tail执行。http://wiki.nginx.org/HttpLuaModule针对每个lua命令,会指明其运行phase。

那么nginx module或addones的指令分别在哪些phase执行呢?http://wiki.nginx.org/Phases, 这里有一张持续更新的表。(记住,set和if都是rewrite module提供的)

phase optional exits modules / directives
server selection* listenserver_name
post read HttpRealIpModule
server rewrite rewrite
location selection location
location rewrite location selectionfinalize request rewrite
preaccess degradationlimit_zonelimit reqHttpRealIpModule
access finalize request allowdenyauth_basic
try files location selection try_files
content autoindexCoreDAVEmptyGifFastCGIFLVgzip_staticindexmemcachedperlproxyrandom_indexscgi,stub_statusuwsgi
log access_log
post action* post_action

基于以上限制,我们的lua代码最终干预的主要是access和content phases。

具体说,首先在http rewrite阶段,nginx set指令初始化nginx var变量并设置一堆默认值。并if判断,但由于if body的指令,例如proxy、fastcgi都是content阶段执行的,所以当前并不会生效。

在access阶段,根据路由规则、流量异常标识,计算路由目标(proxy、fastcgi or forbidden)。并由lua代码通过ngx.exe,调用nginx internal location实现路由分发。

如果access phase执行失败,则请求还会继续执行下去,会执行content phase,执行默认设置的proxy或者fastcgi规则(与上面的if指令相呼应)。

 

HBase

部署情况

6台服务器,同一机房。1 master,1个secondary master。6台都作为zookeeper服务器了。6台也都运行region server了。在多台上搭建了thrift服务。

服务器都是1.3T普通硬盘,64G内存,12核cpu。

配置及可优化点

未进行特殊的配置优化。

还是默认的3version版本(可以精简),数据也未配置过期时间(其实可以配置),这样可以节省存储空间。

LRU cache size: 6台 * 4G * 0.25 * 0.99=6G。

典型查询与表结构设计

我们的查询,主要是根据userid、type、time range,进行scan查询。由于userid本身具备散列性质,故没有再进行特殊处理。

为了保证尽量顺序的读取,尽量采用上述顺序,保证相同userid、type的数据在同一个block上。

存储的data是经过protobuf序列化后的,由于读取时大多场景是同时读出,故没有采用hbase自身的column方式。

开启了gzip压缩,由于存储的大部分是日志,冗余度较高,压缩比约10:1。

kv len比约为:37:212,仍存在优化空间。

读写压力及优化思路

遇到过查询失败的情况,瓶颈在thrift server。据称hbase自带的thrift服务稳定性和性能都一般。

 

Leave a Reply