[nginx] 由Lua 粘合的Nginx生态环境-- agentzh tech-club.org

[nginx] 由Lua 粘合的Nginx生态环境-- agentzh tech-club.org 演讲听录 [复制连接]javascript

kindlephp


LT管理团队前端

Rank: 9Rank: 9Rank: 9java

未绑定新浪微博node

签到222python

注册时间1970-1-1最后登陆2015-6-5在线时间168 小时阅读权限200积分19025帖子119主题35精华2UID9223mysql

LT总司令 LT元老 LT教授linux

串个门加好友打招呼发消息 nginx

电梯直达跳转到指定楼层 1#git

 发表于 2013-1-12 12:43:47 |只看该做者 |倒序浏览

转自:http://blog.zoomquiet.org/pyblosxom/2012/03/


    活动: Tech-Club技术沙龙(2012年2月)活动小结

    幻灯: ngx_openresty: an Nginx ecosystem glued by Lua

    录音: http://vdisk.weibo.com/s/2Qcon

    笔录: Zoom.Quiet <zoomquiet+nginx@gmail.com>

    Chnangelog:

        120312 fixed as s/hhttp/http/g ,thanx muxueqz

        120309 fixed kindel->kindle, thanx for milel liu;

        120308 fixed ahcking->hacking,thanx weakish

        120306 fixed agentzh ID name,thanx himself alert

        120305 finished

        120301 init.


        很早就一直关注 agentzh 对 nginx 的给力 hacking,此次总算有个阶段性的说明,虽然没法现场交流, 好在有录音,为了其它没有时间听的人们,以及给搜索引擎更好的搜索数据,俺义务听录全文;


1. 免责聲明


    录音/幻灯来自做者,版权固然属于他们

    文字听录来自 Zoom.Quiet,一切文字问题都是我形成的,与原著无关

    由于本人技术有限,仅经过幻灯和录音,记错的地方负责在我,与原著者无关

    任何不满和意见,请直接与我联系以便改进

        zoomquiet+nginx@gmail.com


2. Lua 粘合的 Nginx 生态环境


很高今天和你们进行分享,以前,在北京进行过相关的分享; 今天咱们的話題是 Nginx 也能够說是关于 Lua 的; 介绍过去3年以来咱们的工做, 工程名字是,openresty,能够追溯到2007年,那会儿,我刚刚进入 Yahoo! 中国, 第一份工做就是架构一个开放平台, Yahoo! 自个儿的开放平台, 系统做到后来逐渐偏离了初衷, 咱们开始为大型的互联网公司做一些和web 前端打交道的系统支持;


我在 Yahoo!和 TaoBao 分别工做了两年,就辞职了; 主要由于,咱们的开源做品,愈来愈多人使用了, 而我一方面,要应付所謂业务需求,另方面要响应来自国内外积极开发者们的要求或是bug; 因此,干脆辞了专心做事儿; 原本,我想搬到厦门,但是我老婆在福州找到了工做,因而,, 如今,我不拿工资,义务为全球的愛好者开发 ;-) 如今,已经在福州呆了7个月,这是我老婆给拍的照片; 我习惯,先在纸上写好代码,而后输入电脑,


前面放的是 kindle ; 这台 kindle 的来历比较有意思,


    在TaoBao 的时候,我打算将 openresty 重写,由于一开始是用 Perl 来写的

    而在Yahoo! 的时候虽然已经使用 openresty 統一了搜索功能,但性能的确通常

    当时,本想基于 Apache 来改写,不过一位师傅跟我讲:"你就直接拿c 写吧,基于 Apache 写没有前途的!"

    俺很郁闷,就问,那怎么整? 师傅回答,你研究一下 nginx 的源代码吧,而后就没再理我 而看代码是很累的,因此,俺一到 TaoBao 就买了台 Kindel 来看代码...


2.1. openresty


刚刚提过, openresty 在开发过程当中逐偏离了原计划; 再面对后来,更加具体的公司业务后, 这时,已经能够看出所謂 Ajax/Servise 化了, 在我接触过的各类繁忙的互联网公司,都有种趋势,就是:


    对看起来是个总体的web 应用

    习惯在后台拆成不少 Service

    有些Service 是供給客户端发起請求来访问的,

    而有些Service 根本就是为其它服务而服务的,也使用了 http 协议进行发布


这种结构,致使总体系统变得很是分散


    由多个部门,分别实现一部分系統

    而每一个部门,暴露給其它部门的,都是 http 协议,resful 形式的接口而已


    好比说, 去哪儿 网,就是很是很是松散的服务组合成的;

        一个請求进入后,当即分解成各类請求分别进行

        而有些就在 Service 之间进行了


        既然,http 协议如此常见,咱们就须要强大的实现基础; nginx 是咱们调研的各类平台中,最不烂的一个!

            其它真心都特别烂,,, Apache 最大的问题是其 I/O 模型,没法完成很是高效的响应; 可是优势是:开发接口规整,基于它来写 mod 很是方便; Lighttpd 正好相反,其 I/O 很是高效,可是开发接口不怎么友好; 而 Nginx 融合了二者的优势 ;-)


            <<< 5:11


    一方面使用了 lighttpd 多路复用的 I/O 模型

    另外一方面以借鉴了 apache 的模块开发支持


    在(openresty)开发过程当中,常常有人问,为何 nginx 如此之快?

    咱们知道 nginx 是单线程的,

    而单线程的模型,为何能够承担上万甚至上几十万的并发請求?! 由于 nginx 的工做方式,如动画所示,这是我刚刚用 perl 生成的一个简单 git 动画:

    这实际上是操做系统线程做的事儿

    前面3个,分别对应不一样的 http 请求

    每一个方块表明一个读或是写操做

    最后的 epoll_wait 就是 linux 系統中最高效的一种事件接口


    也就是説 nginx 内部实际上是种事件驱动的机制

    只有相关事件发生时,才处理具体数据

    若是当前接口没有数据时,就会当即切换出去,处理其它请求

    因此,虽然只有一个线程,可是,能够同时处理不少不少线程的請求处理 那么,这种形式的 web 系統,能够很轻易的将 cpu 跑满,即便带宽没有跑满的状况下; 而 apache 这类多进程多线程模型的服务器,则很难将 cpu 跑满:

        由于并发达到必定量时

        内存首先将耗尽

        由于在 linux 系统中,线程数是有限的,每一个线程必须预分配8m大小的栈,不管是否使用!

        因此,线程增长时,内存首先成为瓶颈

        即便挺过内存问题,当并发请求足够多时,cpu 争用线程的调度问题又成为系統瓶颈


        <<< 8:31


        因此,nginx 这样简单的单进单线模型,反而被 memcached 等高性能系统定为I/O 模型; 那么,咱们做了什么呢?

        主要是为 nginx 提供了不少补丁,进行了 bugfix

        同时利用 nginx 提供的开发者接口,贡献了不少模块

        咱们还将以前说起的 Lua 嵌入 nginx ,使其具备全功能的交互能力

        更加把 Lux 一些经常使用库,也放进去了

        而后打成一个大包,命名为 openresty ...这是使用 Tiddlywiki 随便做的一个 主頁: http://openresty.org


2.2. 配置小语言


nginx 自己有个很重要的特色,这在维基百科的条目中也强調过:


    其配置文件记法是很是灵活,并可读的

    nginx.conf 配置文件,本地其实就是个小語言 好比:

    location = '/hello' {

           set_unescape_uri $person $arg_person;

           set_if_empty $person 'anonymous';

           echo "hello, $person!";

       }

复制代码

这段配置,对于 apache 用户来説,也很熟悉

    咱们首先使用相似正则表达式的形式来约定一个响应的 url

    而后,可使用各类 nginx 的指令对内部变量进行到系列操做

    变量也是配置文件的一部分,很象一种编程語言

    好比,这里,咱们就将 person 这个变量使用 arg_person 进行赋值

    而后,用 'anonymous' 做为空值时的默认值给 $person

    最后直接使用 echo 将結果输出 这样,咱们就可使用 curl 模拟浏览器访问,给 /hello 提供一个utf8 编码的字串值, 以 ?person= 的GET 方式变量, 就能够得到預期的反馈: hello, 章亦春 不給参数的話,刚刚的 anonymouse 就起做用了;


    因此,总体上,咱们指望在 nginx 中实现服务接口,就这样写点配置就好,不用写什么认真的c 代码;-) 而跑起来就象飞同样,由于,这么来写,实际和用c 现实没有什么区别;


    事实上,全世界的开发者都在使用 nginx 的开发接口,在拼命丰富这种配置文件小語言的词汇表!


    而真正决定其表达能力的是:"vicabulary"

    好比说,咱们看这个例子,这是我写向第2或是第3个nginx 模块:

        用以直接访问 memcached 的所謂上游模块

        nginx 有自个儿的一套术语,在其后的各类服务好比memcached ,在nginx 而言就是上游

        对应的,那些访问 nginx 的浏览器等等客户端,就视为下游

        # (not quite) REST interface to our memcached server

        #  at 127.0.0.1:11211

        location = /memc {

            set $memc_cmd $arg_cmd;

            set $memc_key $arg_key;

            set $memc_value $arg_val;

            set $memc_exptime $arg_exptime;

            memc_pass 127.0.0.1:11211;

            }

复制代码

这样简单的配置一下,经过 set 将url 上的各类参数映射给几个变量,

        而后经过 memc_pass 链接到远端一个memcached 服务,固然后面也能够是个集群

        当即,咱们就获得一个,应该說是种伪 restfule 的 memcached 的使用接口服务

        咱们可使用 curl 来操做目标 memcached 了

        好比说,著名的 flush_all 命令,就能够直接经过 url 来执行


    经过这种形式,咱们能够快速扩展成对memcached 集群的简洁管理服务,进行各类操做

    这样做的好处在于::

        不論其它相关应用使用 php 什么乱七八糟的語言写的,均可以統一包装成 http 接口

        令整个业务系统变成http 协议,这样系统的复杂度就可以有效下降

    一样能够这样对 MySQL 等等其它集群服务进行包装

    包括你们知道的 taobao 集群,对外部开发来說,好象是专门为外部扩展发布的服务,

        其实在 taobao 内部各类服务也是以两样形式組合起来的

        你们知道 taobao 是java 系的,它不少服务是经过定制 jvm 完成的

        因此,对于ali 原先业务,以及合做方的业务,还有咱们数据统计部门的业务,对于jvm是没法直接使用的

        怎么办?因此,经过开放平台业务,将各类内部服务,封装成一系列 http 接口方便使用

        包括taobao 的登陆,其实也封装成 http 接口,供给,taobao 子域名应用来使用

    不论使用什么开发語言,老是能够对http 协议进行访问的

        并且 http 协议自己很是简单

        咱们能够方便的获取许多现成的工具进行调试/追踪/优化,,,

        另外,因为选择了 nginx,这使得http 的开销,代价变的很是很是的低

    记得 去哪儿网,原先有业务使用了几十台 MySQL

        前端使用 java 的jodb 进行链接

        而由于代码写的比较糟糕,由于业务部门嘛,写的时候不会注意链接池的效率,

        因此,每台主机的负载都很是 很是 高

        而,咱们后来改成nginx 做前端,結果一台nginx 就将之前几十台java 主机的业务抗了下来

        经过封装成 http 接口,业务代码随便长链接/短链接,随便它搞,都撑得住了!

        因而,被他们java 程序员描述成不可能的任务,被一台 nginx 主机就解决了


        <<< 17:00 (插入提问): 封装具体做了什么?为何比原先的方式效率高? 虽然改为了 http 实际链接MySQL 时不一样样要消耗?

    由于,封装成 http 接口的数据库,咱们内部使用了链接池

    已经优化的高效数据库链接池,而通常工程师不用关注链接池的技巧,专心完成业务代码就好,不容易出错

    并且,使用语言专用中间件的话,牵涉到其它问题:

        中间件自己是否稳定?高效率?

        中间件自己是否易于扩展好维护?

        等等一系列问题,远没有統一成 http 服务于全部語言实现的应用来的干脆简洁

    甚至于,咱们后来引入了完整的 Lua 語言,它基本足够完备,能够支持咱们直接完成业务

        taobao 的数据魔方,就直接使用脚本在 nginx 中完成的

        相比原先php的版本,仅仅这一项,就提升响应速度一个量級!

    因此,不论 memcached 仍是什么数据库,咱们能够統一到一个中间件

        并且 http 协议的中间件,还有个好处是能够直接公开給外部使用

        由于 http 上的访问控制很好做,复杂度也低

        咱们的量子統計,就是直接和taobao 主站服务经过 http 良好整合在了一块儿

        能够简单的一个参数处理就发布給外部或是内部来安全使用


2.3. ngx_drizzle


经过模块,咱们能够创建应用和 MySQL 间的非阻塞通訊


    这点很是重要!

    由于,当前端访问后端很大的数据集群的时候,其自己的并发能力就成为瓶颈

    设想后端有近百台 MySQL 时,后台自己的并发量就已经很是大了

    而前端相似 php 技术根本没法将后端全部主机的能力都应用起来

    因此,咱们很是须要非阻塞技术

    须要一种数据库代理,就象很高能的网关同样,将后端全部MySQL 服务器的能力都激发出来

    而不用期待前端应用来自行完成并发调度 基于以上认知,咱们开发了各类数据的非阻塞上游模块:

        包括对 MySQL/Postgres/redis 等等

        也尝试过对 Oracole ,可是,其官方的 c 驱动有些限制,虽然也提供了非阻塞接口,可是不完整

        在创建链接和銷毁链接时,只能以阻塞方式进行,因此,很纠结

        MySQL 官方的 c 驱动也只提供了阻塞方式!

        那只好寻求第三方的驱动,咱们选择了 Drizzle 这个驱动,并整合进来 成为 ngx_drizzle 模块

        upstream my_mysql_backend {

                   drizzle_server 127.0.0.1:3306 dbname=test

                               password=some_pass user=monty

                               protocol=mysql;

                   # a connection pool that can cache up to

                   #   200 mysql TCP connections

                   drizzle_keepalive max=200 overflow=reject;

               }

复制代码

咱们这样简单配置:

        经过 drizzle_server 配置链接口令和协议,由于模块能够链接 MySQL 和 drizzle 两种数据源,因此,要声明协议模式

        使用 drizzle_keepalive 创建一个链接池,限定上限为200,当超过链接限制时就 reject,至关对数据库的简单保护

        而后这样定义一个 cat 接口

        location ~ '^/cat/(.*)' {

               set $name $1;

               set_quote_sql_str $quoted_name $name;

               drizzle_query "select *

                   from cats

                   where name=$quoted_name";

               drizzle_pass my_mysql_backend;

               rds_json on;

           }

复制代码

cat 以后是这猫的名字,使用 set 得到,这是 nginx 自己的功能

        而后使用 set_quote_sql_str 对查询语句进行转义,以防止SQL注入攻击

        经过 drizzle_query 组合成查询語句

        drizzle_pass 来完成对后端数据集群的查询,由于前面的 drizzle_server·可聲明一组 MySQL服务器

        甚至于,咱们为查询返回的結果集,定制了一种格式,叫 rds_json

            这种格式是面向各类关系型数据库的

            咱们针对这种格式,开发了一系列过滤器能够自由输出 csd或是json格式

            这样,几乎全部报表接口,都经过这种方式实现的

            taobao 直通车就使用了 csd 格式,由于他们是将这当成中间件来使用的

            而咱们是直接经过 json 以 Ajax 形式对外的

        这样,经过 curl 访问 cat 接口查詢 Jerry ,就能够得到名叫Jerry 的猫的相关数据

        这里json 的输出,能够经过一系列方式,进行自由的调整

        好比说,有的要求每行数据都是 key/value 的格式,有的要求紧凑格式,第一行包含key以后,之后的所有是数据等等,,,


2.4. ngx_postgres


那么 portsgres 访问接口模块名叫:ngx_postgres


    这是一位波兰的 hacker 在咱们的ngx_drizzle 基础上完成的

    由于它仿造了咱们的接口形式

    pg 的官方模块是没法使用的,因而他花了两个月的时间,完成了这个模块

    去哪儿网,有不少地方就使用了这一模块

    咱们能够看到如何使用 Lua 来调用这个标准模块 由于在 web 开发中,每向上一层,速度会降低一级,可是,功能会丰富不少

    可是,使用 nginx 模块来完成,速度损失颇有限

    upstream my_pg_backend {

           postgres_server 10.62.136.3:5432 dbname=test

                   user=someone password=123456;

           postgres_keepalive max=50 mode=single overflow=ignore;

       }

复制代码

这里,咱们配置 overflow 时 ignore ,忽略,就是説,链接超过限定时,直接进入短链接模式

  location ~ '^/cat/(.*)' {

               set $name $1;

               set_quote_pgsql_str $quoted_name $name;

               postgres_query "select *

                   from cats

                   where name=$quoted_name";

               postgres_pass my_pg_backend;

               rds_json on;

           }

复制代码

这样定义一个 pg 版本的 cat 接口

        注意,进行SQL 转义时问的是 set_quote_pgsql_str, 由于pg 的SQL转义和其它的不一样


2.5. ngx_redis2


而后,去年的时候,我为了好玩,写了个 redis 的模块: ngx_redis2


    依然是100%非阻塞,去哪儿和天涯也都大量使用了这一模块

upstream my_redis_node {

           server 127.0.0.1:6379;

           keepalive 1024 single;

       }

复制代码

一样使用 upstream 定义一个或是多个链接池

        使用 keepalive 定义并发策略,这种场景中 tcp 在 http 的链接消耗是很是低的

# multiple pipelined queries

           location /foo {

               set $value 'first';

               redis2_query set one $value;

               redis2_query get one;

               redis2_pass my_redis_node;

           }

复制代码

这里,我使用 redis2_query 定义了两个请求

        经过流水线形式,一次請求发送了两个命令过去,响应时,就有两个响应,按照顺序返回


2.6. ngx_srcache


ngx_srcache 是个颇有趣的通用缓存模块


    以前为 apache 写过一些模块,其中一个比较有趣的,就是针对mod_cache 模块,写了个 memcached 的模块,就能够经过 memcached 对apache 中任意的响应进行缓存!

    这模块当初是为 Yahoo! 的搜索业务中,爬虫的抽取系統进行設計的

    固然我就发现,apache 里对 memcached 进行阻塞访问时,有点虚焦? 随着并发数增长,响应速度极速降低

    因此,在nginx 时,就不会有这种问题,保证全部处理都是非阻塞的!包括访问 memcached

    因此,咱们能够在配置文件中自行决定使用什么后端来存储缓存

  location /api {

           set $key "$uri?$args";

           srcache_fetch GET /memc key=$key;

           srcache_store PUT /memc key=$key&exptime=3600;

           # proxy_pass/drizzle_pass/postgres_pass/etc

       }

复制代码

这里咱们定义两种调用,所謂 fetch 是在 apache 中一种模板,c級别的调用可是,技法和 http 的 get 一樣

        这样声明的 location,咱们能够同时即对外提供调用,也能够对配置内部其它 location 进行调用!

    location /memc {

              internal;

              set_unescape_uri $memc_key $arg_key;

              set $memc_exptime $arg_exptime;

              set_hashed_upstream $backend my_memc_cluster $memc_key;

              memc_pass $backend;

          }

复制代码

这样,其实就是在收到請求时,实际调用了 /memc 接口,访问后端缓存

        收到結果后,再使用 srcache_store 接口整理put 回请求的入口 location, 設置相应的格式

        而 /memc 接口经过 internal 标记,成为仅仅对内服务的接口

        后面咱们经过一系列指令,从 url 参数 ;-)

            即便是内部调用,依然是个标准的 http 請求界面

        而后使用 set_hashed_upstream 对 memcached 的集群.进行基于鍵的模的 hash 将結果放到 $backend

        最后使用 memc_pass 完成对集群的查询

        这里的 my_memc_cluster 是怎么定义的呢?

  upstream memc1 {

               server 10.32.126.3:11211;

           }

           upstream memc2 {

               server 10.32.126.4:11211;

           }

复制代码

upstream_list my_memc_cluster memc1 memc2;

    使用 upstream 定义两个服务,使用upstream_list 声明为一个集群

    这里其实也有限制的:

        在咱们动态追加主机时

        咱们要从新生成配置文件,而后使用 touch 命令通知 nginx 从新加载

        而这一限制,咱们将看到,在基于 Lua 的实现中会不存在 ;-)


        前面咱们看到,通过简单的配置,咱们就能够得到一系列强大的 api 服务;


        <<< 29:51


2.7. ngx_iconv


实际使用中,还有一个重要的需求就是字符串编码:


    由于,有的业务是基于 gbk的,有的又是 utf-8 的

    通常咱们能够在数据库层面进行处理

    可是,对于一些功能弱些的产品,好比说,memcache/redis 等,就没办法了

    因此,咱们完成了本身的动态编码转换模块:


    ngx_iconv


    无论你们在访问 MySQL 时,使用的什么途径,好比习惯的反向代理什么的

    均可以经过 iconv_filter 对响应体进行编码转换!

    并且是流式的转换,也就是說,不须要 buffer,来一点数据就当即完成转换

location /api {

           # drizzle_pass/postgres_pass/etc

           iconv_filter from=UTF-8 to=GBK;

       }

复制代码

以上这是从 utf-8 到 gbk 的转换


        <<< 30:54


2.8. 嵌入 Lua


后面咱们化了很大力气将 Lua 嵌入到了裏面:


    这样使得,能够实现任意复杂的业务了

# nginx.conf

       location = /hello {

           content_by_lua '

               ngx.say("Hello World")

           ';

       }

复制代码

这样咱们就完成了一个 hallo world

        ngx.say 是 lua 显露給模块的接口

    另外固然也能够调用外部脚本

    如同咱们写php 应用时,习惯将业务脚本单独组织在 .php 文件中同样

# nginx.conf

   location = /hello {

       content_by_lua_file conf/hello.lua;

   }

复制代码

经过 content_by_lua_file 调用外部文件:

-- hello.lua

       ngx.say("Hello World")

复制代码

这里的脚本能够任意复杂,也可使用Lua 本身的库


    早先,咱们很是依赖,ngninx 的子请求,来复用 nginx 的请求模块:

    好比说,咱们一个模块,须要同时访问 memcached/mysql/pg 等許多后端

    这时,怎么办? 这么来:

location = /memc {

       internal;

       memc_pass ...;

   }

   location = /api {

       content_by_lua '

           local resp = ngx.location.capture("/memc")

           if resp.status ~= 200 then

               ngx.exit(500)

           end

           ngx.say(resp.body)

       ';

   }

复制代码

先在 /memc 中创建到 memcache 的链接,并声明为内部接口

    而后,在 /api 中使用 ngx.location.capture 发起一个 location 請求

    就象发起一个正当的 http 请求同样,请求它,可是,其实没有http的开销,由于,这是c 级别的内部调用!

    并且是个异步调用,虽然咱们是以同步的方式来写的

    而后咱们能够检验响应是否 200,不然访问 500

    最后就能够将响应体输出出来


2.8.1. 同步形式异步执行!


这里为何能够同步的写?


    写过 javascript 前端程序的朋友,应该知道要实现异步效果,咱们不少时候,要使用回调

    而在 Lua 中咱们能够这么来,由于 Lua 支持协程,即,concurrent

    这样,咱们能够在一个 Lua 线程中分割出多个Lua 用户级的逻辑线程

    这种伪线程,能够实现比操做系统高的多的多的并发能力,由于系统开销很是的小

    近年有一些技术,也都支持了 concurrent 的技术,能够象http 请求顺序同样,顺着写

    不用象js 程序员那些纠结倒着写,在须要顺序操做时,又必须借重一些技法,而应用技法的代码,又实在难看,没法习惯


    因此,咱们当初选择 Lua 一个很重要的缘由就是支持 协程

    这里咱们假定,同时要访问多个数据源

    并且,查询是没有依赖关系的,那咱们就能够同时发出请求

    这样我总的延时, 是我全部请求中最慢的一个所用时间,而不是原先的全部请求用时的叠加!

    这种方式,就是用并发换取了响应时间

    location = /api {

           content_by_lua '

               local res1, res2, res3 =

                   ngx.location.capture_multi{

                       {"/memc"}, {"/mysql"}, {"/postgres"}

                   }

               ngx.say(res1.body, res2.body, res3.body)

           ';

       }

复制代码

这里咱们就同时发出了3个请求

        同时到 memcached/mysql/pg

        而后全新响应后,将結果放到 res1/2/3 三个变量中返回 因此,这种模型里,实现并发访问也是很方便的 ;-)


        <<< 35:20


2.9. lua_shared_dict


这是我去年,花力气完成的 nginx 共享内存字典模块: lua_shared_dict


    由于 nginx 是多 worker 模型,能够有多个进程

    可是,其实 workder 数量和并发无关,这不一样于 apache

    nginx 多worker 的目的是将 cpu 跑满,由于它是单进程的嘛

    nginx 实际只跑了操做系统的一个线程,因此,多核主机中,若是有8核心,咱们通常就起8个 worker 的

    若是业务有硬盘 I/O 的操做时,咱们通常会起比核数略多的 worker 数

        由于在 linux 中,磁盘很难有非阻塞的操做

        虽然有什么 aio 的模型,可是有不少其它问题

        因此,本质上 nginx 多 worker 是为了跑满 cpu

    那么,一但多进程了,就存在满满的共存问题

        好比説,咱们想在多个进程间共存配置/业务数据

        因此,基于共存内存来做

   lua_shared_dict dogs 10m;

           server {

               location = /set {

                   content_by_lua '

                       local dogs = ngx.shared.dogs

                       dogs:set("Tom", ngx.var.arg_n)

                       ngx.say("OK")

                   '

               }

               location = /get {

                   content_by_lua '

                       local dogs = ngx.shared.dogs

                       ngx.say("Tom: ", dogs.get("Tom"))

                   ';

               }

复制代码

这有个例子:

        首先,使用 lua_shared_dict 分配一 10M 的空间

        而后,使用 OOP 方式,来定义两个接口:一个 /set 一个/get

        而后,不论哪一个 worker 具体调用哪一个操做

        可是結果,是終保存一致的

    使用 curl 先set 一下,再 get 就变成了 59,由于内部进行了自增

        共存的实现是经过紅黑树+自旋鎖来达成的:

            紅黑树的查找相似 hash 表查找的一种算法

            为保持读写的数据一致性,使用 自旋鎖来保证

            因此,当并发增大或是更新量增大时, 自旋鎖可能有问题,将来咱们准备进一步修改为报灰的模型

        其实,共享内存的方式,在鎖开销很是小时,效率是很是高的,在腾讯单机并发跑到20万都是小意思;


        另外,在 Lua 中,咱们须要对大数据量的一种非缓存的输出:

    由于,在不少 web 框架中或多或少都有缓存,有的甚至使用了全缓存

    那么,当你输出体积很大的数据时,就很易囧掉

    而,在 Lua 中,咱们就很容易控制这点

  -- api.lua

    -- asynchronous emit data as a response body part

    ngx.say("big data chunk")

    -- won`t return until all the data flushed out

    ngx.flush(true)

    -- ditto

    ngx.say("another big data chunk")

    ngx.flush(true)

复制代码

好比,这里咱们先 ngx.say ,异步的输出一个数据

        这段数据不必定刷得出去,若是网卡没来得及输出这投数据的话,这会在 nginx 的进程中缓存

        若是,我想等待数据输出后,再继续,就使用 ngx.flush ,这时,只有数据真正刷到系统的缓冲区后,才返回

        这样保证咱们 nginx 的缓存是非低的,而后咱们再处理下一个数据段

        如此就实现了流式的大数据输出


        这样,有时网络很慢,而数据量又大,最好的方式就是:

    既然你发的慢,那我也收的慢: 你一点点发,我就一点点收

    这样咱们就可使用不多的资源,来支持不少大数据量的慢链接用户


2.10. Socket形式


然而,还有些慢链接就是恶意攻击:


    我能够生成不少 http 链接,接进来后,慢的发送,甚至就不发送,来拖死你的应用

    这种状况中,你一不注意,服务分配给太多資源的话,整个系统就很容易被拖垮 因此,去年年末,今年年初,我下决心,完成了一个 同步非阻塞的 socket 接口:


    <<< 40:50


    这样,我就不用经过 nginx 的上游模块来访问http 请求:

    咱们就可让 Lua 直接经过 http,或是 unix socket 协议,访问任意后端服务

local sock = ngx.socket.tcp()

    sock:settimeout(1000)   -- one second

    local ok, err = sock:connect("127.0.0.1", 11211)

    if not ok then

       ngx.say("failed to connect: ", err)

       return

    end

复制代码

象这样,创建 socket 端口,并能够设定超时

    咱们就能够进行非阻塞的访问控制,当超时时,nginx 就能够自动挂起,切入其它协程进行处理

    若是全部链接都不活跃,我也能够等待系统的 epoll 调用了 就不用傻傻的彻底呆在那儿了

    local bytes, err = sock:send("flush_all\r\n")

    if not bytes then

        ngx.say("failed to send query: ", err)

        return

    end

     

    local line, err = sock:receive()

    if not line then

        ngx.say("failed to receive a line: ", err)

        return

    end

     

    ngx.say("result: ", line)

复制代码

或是使用 sock:send 直接返回,就能够继续其它请求了

    使用 receive 来接收查询的返回,读失败有失败处理,成功就打印出来 一切都是天然顺序

   local ok, err = sock:setkeepalive(60000, 500)

    if not ok then

        ngx.say("failed to put the connection into pool "

            .. "with pool capacity 500 "

            .. "and maximal idle time 60 sec")

        return

    end

复制代码

这是链接池的调用

    经过 sock:setkeepalive , Lua 模块,就会将当前链接,放入另外一链接池中以供其它請求复用

    也就是說,若是其它請求,請求到同一个url 时, nginx 会直接交給它原先的链接,而省去了开新链接的消耗

    keepalive 的参数比较少:

        头一个是,最大空闲时间,即,一个链接放在链接池里没有任何人来使用的最大时间

            这里是60秒,由于维持一链接的代价仍是很昂贵的,若是一分钟了也没有人来用,我就主动关闭你节省资源

            对于负载比较大的应用,这样能够减小浪费

        第二个参数是,最大链接数,

            这里是500,若是链接数超过限制,就自动进入转移链接的模式


            Unix 域套接字 是 Linux/Unix 系统独特的进程接口

    虽然不走 http 协议,可是调用形式和 tcp 的 socket 彻底相似

  local sock = ngx.socket.tcp()

    local ok, err = sock:connect("/tmp/some.sock")

    if not ok then

        ngx.say("failed to connect to /tmp/some.sock: ", err)

        return

    end

复制代码

一樣经过 ngx.socket.tcp 来创建链接

    而后,使用 sock:connect 来指定一个特殊文件,接入套接字

    就能够进行各类平常的操做了


2.11. concurrent ~ "cosocket"


这个模块是基于 concurrent 的:


    写是顺序写,可是执行是非阻塞的! 这点很是重要!

    协程技术诞生也有些年头了,

    可是,至今 99.9% 的 web 应用依然是阻塞式的

    由于早年,基于阻塞的应用开发太习惯了

    而基于异步的开发,对于工程师的思維能力要求过高,这也是为何 node.js 工程师在开发时的主要痛苦

    由于,要求改变思維方式来考虑问题,咱们的程序员可能是 php 的,要求他们改变思惟是很痛苦的


    因此,不只仅是为了推广咱们的平台

    更是为了兼容工程师的阻塞式思維,同时又能够利用协程来提升系统性能,达到单机上万的响应能力

    咱们引入了 Lua 的协程,并称之为: "cosocket"

    即,concurrent based socket

    而一位资深的 python 粉丝告诉我,python也有优秀的协程库:

        是基于 greenlet 的 Gevent

        固然,相似咱们的系統,都是能够支撑很是高并发的响应


        可是,咱们当初选择 Lua 还有个很重要的缘由是:

    cpu 的执行效率

    当你的并发模式,已是极致的时候

    cpu 很容易成为瓶颈!


    通常状况下是 带宽首先不够了,而后 cpu 被跑满

    而在 apache 模型中,反而是内存首先不足

    常常是24个进程,swap 8G/24G 不断的增加,卡住什么也玩不了了

    而cpu 光在那儿进行上下文切换,没有做什么有意义的事儿 即,所謂内耗


    当咱们将应用从 I/O 模型解放后,拼的都是 CPU:

    由于,内存通常消耗都不太大

    咱们常常在 256M 内存的虚拟机,或是64M 内存的嵌入式设备中跑生产服务 内存,真心不该该是问题所在,,,


    可是,要进行計算时就必定要快!

    而 Lua 近年发展编译器到什么地步?

    有种编译器,能够运行时动态生成机器码

    在咱们的测试中,高过了末启用优化的 gpc

    而启用优化的 gpc ,消耗资源又高过 Lua


    因此, Lua 的性能没有问题

    而后咱们实际,按照 ruby 社区的説法,就是直接基于Lua 扩展出了一种专用小語言

    业务团队实际并无直接使用 Lua 来写,而是使用咱们为业务专门定制的一种专用脚本(DSL)

    因此,代码量很是的少 并且,咱们的定制小語言,是强类型的:

    强类型語言有不少好处

    并且,能够在小語言中,定义对业务領域的高层次約束

    你就能够很方便的查找出业务工程师常范的错误,转化成語言特性包含到约束中,在编译器中实现!

    最后编译成包含优化的 Lua 代码,让它跑的象飞同样! 并且! 哪天,我高兴了,也可让它生成 C 代码让它跑到极致!

    这样,业务不用改一行代码,可是,系统效能能够提升几倍

    等等,这些都是能够实现的,,,


    要,实现这些,要求咱们的基础必须很是很是的高效,同时又很是很是小巧!

    这样咱们才能在上面搭上层建筑

    即,所謂的: "勿在浮沙筑高台"!

    在这一过程当中,咱们也吃过不少苦,,,好在有 nginx ...


    再有,咱们发现 socket 模型,同样能够用来读取下游,即客户端请求数据!

    当请求体很大,好比说,上传一个很大的文件时

    也须要异步处理 ,就省的我操心了

    因此,我就对下游請求,包装了一个只读的 socket,能够对請求数据进行流式读取

   local sock, err = ngx.req.socket()

    if not sock then

        ngx.say("failed to get request socket: ", err)

        return

    end

    sock:settimeout(10000)  -- 10 sec timeout

     

    while true do

        local chunk, err = sock:receive(4096)

        if not chunk then

            if err == "closed" then

                break

            end

            ngx.say("faile to read: ", err)

            return

        end

        process_chunk(chunk)

    end

复制代码

这样,创建一个下游 socket 后,以 4096 字节为一个块(trunk)进行读取

    而后检查是否结束,即便没有结束,我也能够一块块的进行处理

    好比,读一块就写到硬盘上,或是写到远程的一个 tcp 链接,这链接也是非阻塞的!

    象这样,我这层就很是很是高效!


2.12. 高层实现


进行各类高层次的实现就很是方便了


    之前我用几年时间才能实现纯 Lua 的 MySQL 的链接模块

    如今用几百行 Lua 脚本就实现了: lua-resty-mysql

        并且是很是完备的实现

        支持多結果/存储过程等等高級功能

    并且性能很是接近纯 C 写的模块,我评测下来,也就差 10~20% 的响应

    若是将来,我用C 改写其中计算密集型的处理模块,那性能能够进一步大幅度提高!


    lua-resty-memcached 也就500多行就搞掂了!

    是完整的 memcached 协议的支持


    因此,用这种技术,能够很方便的实现公司里固定的或是全新的后端服务;


    redis协议自己設計的很是巧妙,虽然命令多,可是底层传输协议很是简洁

    因此,我只用 200 多行,就实现了:lua-resty-redis


    后面两个模块都比较粗糙,仅仅封装了传输协议,因此,执行效率,高于它们官方c 实现的等价物 ;-)


    lua-resty-upload 就是说起的大文件上传模块

    不过,这模块写的比较粗糙

    api 暴露的不够 优美,,,,


3. abt.


    我在 http://github.com/agentzh 上每天提交代码;

    也刷weibo : http://weibo.com/agentzh/ 不过,最近刷的比较少,,,


    <<< 53:00


QA:


    将 Lua 当成什么来用? 直接业务嘛?

        简单的能够直接来

        也能够架构更高层的領域脚本,编译成 Lua 来执行

        不过,最终,都是经过寄生在 nginx 平台上的 Lua 来实际跑


    那 openresty 主要解决了nginx 的什么问题?是nginx 的缺陷嘛?

        分两个方面来想:

        1.做为 nginx 的补充,不少人也是这么用的,好比说负载的接入,简化 F5 的前端配置,访问的逻辑控制,,,

        2. 直接做为 web 应用的机制,直接实现全部的应用,输出网页,发布 web service,等等


    和 apache 什么的性能差异主要在哪里?

        主要是 I/O 模型的本质差别

        nginx + Lua 能够完成数量级上的提高

        并且,做为应用或是做为 httpd 能够同时胜任双重角色!

的, 录音, kindle

分享到:

QQ空间QQ空间

腾讯微博腾讯微博

腾讯朋友腾讯朋友