Web 后端开发中的异步和非堵塞 IO

当下追新的开发者会尝试用 Node.JS 来做 Web 后端开发,相信这部分人在此之前是以 PHP 开发而主,但由于 PHP 的单进程、堵塞 IO 特性而无法获取很高性能而转移阵线到 Node.JS 上。不过我认为这是个错误的决定,因 Node.JS 基于 CallBack 的异步模式实现的非堵塞 IO 操作并不适合 Web 开发场景。

非堵塞与堵塞

堵塞 IO,即业务逻辑发起 IO 操作后会把整个业务堵塞在此处,等待 IO 操作结果后再继续运行,会导致 CPU 空转。而非堵塞 IO 则不会堵塞业务逻辑,发出 IO 操作后可以继续做其他事情。那么那个性能更好就很明显了。

如何使用非堵塞 IO

因非堵塞 IO 在发出 IO 操作后并不知道何时可以收到结果,我们必须为此加入接收结果的代码。看过 Node.JS 的 CallBack 异步回调可以更容易明白

query('SELECT * FROM table', function(result){...})

上面的代码发起一个非堵塞的 IO 操作,并使用一个 function 来异步接收结果。而在 C 或 Java 这些支持多线程的语言中,我们是使用多线程来发起非堵塞 IO 操作的,等结果来时再唤醒线程,继续运行接下去的业务逻辑。

可以说 Node.JS 的 CallBack 异步回调开发模式,简化了业务开发者的工作,不需要开发者自己开启线程和线程调度。这也是现在这么多人会使用 Node.JS 的原因。

但并不只有 CallBack 异步回调的开发模式才能使用非堵塞 IO,可能大家对协程的了解还不多,否则你也会选择协程。其实协程更适合用来做这些工作,协程是专为非堵塞 IO 而生的好东西。它相对于多线程所消耗的资源更少、性能更高,重点是使用协程可以让你以同步逻辑的思维来书写业务代码。

results = query('SELECT * FROM table')

一眼看过去,似乎是 PHP 这样的堵塞 IO 操作啊?非也,我们要看 query 函数里面的东西才能下定论 :)

function query(sql)
send_query(db, sql)
yield()
return read_result(db)
end

看上面的代码是否跟堵塞 IO 的很像?但请注意 yield 这个函数的调用,在协程的世界中 yield 即把当前运行权交出,等待程序主体的 resume 再运行接下去的代码,当我们的程序主体接收到该次 IO 操作的结果,resume 协程即可。如果我们把 yield 函数的调用都去掉,就是堵塞 IO 的逻辑,而加上则支持非堵塞 IO 操作了。

OK! 现在我们知道非堵塞 IO 可以使用异步回调和协程两种方式来操作!

那为什么说 CallBack 模式不适合 Web 场景?

我们在这里举一个业务例子,分别使用两种方式看看代码是怎样的

业务需求:
1. 读取一个会员信息,获得 uid
2. 并根据 uid 查询对应的文章记录
3. 根据 uid 查询对应的回复记录

使用 Node.JS 会这样写业务代码:

query('SELECT uid FROM users WHERE username=\'aaa\'',
function(uid){
query("SELECT * FROM Article WHERE uid="+uid,
function(res){
query("SELECT * FROM Reply WHERE uid="+uid,
function(res2){
console.log(uid, res, res2);
}
)
}
)
}
)

使用 Lua 类协程方式会这样写业务代码:

uid = query('SELECT uid FROM users WHERE username=\'aaa\'')
res = query("SELECT * FROM Article WHERE uid="+uid)
res2 = query("SELECT * FROM Reply WHERE uid="+uid)
print(uid, res, res2)

上面两段代码都使用非堵塞 IO,但很明显的看到,使用协程方式书写的代码其实跟堵塞 IO 的完全没区别,非常精简。而看 Node.JS 基于异步回调方式写的代码,就有很长的嵌套“尾巴”,多个累赘的 function 声明。

上面的代码例子是串行逻辑的情景,也许你想说在这个情景下 Node.JS 会吃亏。好的,我们写个并行的代码来看看

Node.JS

query('SELECT uid FROM users WHERE username=\'aaa\'',
function(uid){
async.parallel({
res: query("SELECT * FROM Article WHERE uid="+uid),
res2: query("SELECT * FROM Reply WHERE uid="+uid)
},
function(result) {
console.log(uid, result.res, result.res2);
});
}
)

Lua

uid = query("SELECT uid FROM users WHERE username='aaa'")
wait(
newthread(function() res = query("SELECT * FROM Article WHERE uid="+uid) end),
newthread(function() res2 = query("SELECT * FROM Reply WHERE uid="+uid) end)
)
print(uid, res, res2)

Node.JS 即使引入 Async 类的功能支持都无法回避嵌套,只能把嵌套的层级尽量缩小而已,嵌套达到5层的代码就很难维护了。而 Lua 这些基于协程方式实现的非堵塞 IO 操作则精简得多,业务代码很清晰,也更容易维护。

CallBack 回调模式适合什么业务场景?

CallBack 回调的开发模式很适合客户端场景,因客户端与客户有很多交互动作,并且这些动作都是未知的,我们总不能每时每刻去问下,客户点击某个按钮了吗?按了我就得做什么事情去了。在这样场景下,我们更希望在每个按钮上绑定一个事件通知的回调,让客户点击某个按钮时会触发我去做相应的事情。

而 Web 后端开发场景上明显不是上面所说的,完全不同。所有事件的产生你都知道,因为是你发起的非堵塞 IO 操作,你可以用更好的方式来得到事件的结果,而不是 CallBack 回调。

对协程开发有兴趣的同学可以考虑:

OpenResty: http://openresty.org/
一个基于 Nginx + Lua 的高性能 Web 应用服务器

aLiLua: http://alilua.com/
一个基于 Epoll + Lua 的高性能 Web 开发框架
(PS:这也是我的开源项目,主打提供一整套 Web 后端开发所需的支持)

为什么重复制造轮子 – aLiLua

其实早在 2010 年我就在 Lua 用于 Web 开发方面做过尝试,当时还以 PaaS 形式对外提供服务的。但工作重点转移到云存储方向,并且服务也没见起色就停止维护了。

而到了 2013 年工作稍微轻松,想搞点小东西玩玩,也尝了把 node.js,但过程中发现基于回调的形式进行 Web 开发还是比较累的,开发效率相对于 PHP 低得多,尤其是业务逻辑多起来的时候。但 node.js 的异步处理效率很高,换用 node.js 是有得也有失啊。这让我再次想起 Lua,这个开发效率跟 PHP 一样快,同步逻辑的思维相对 node.js 来说清晰太多了。但之前做的 yo2lua 项目由于个人对 Lua 的了解不够深入,基于堵塞 IO 实现,可以说性能一败涂地。当时居然还搞什么 PaaS,搞笑了~(个人经验:回头看自己,会发现有很多sb,哈哈)既然这样我就重新搞搞 Lua 吧,aLiLua.com 这个域名在 2010 年做 yo2lua 的时候就注册好了,现在就以此名开展这个开源项目。

设计目的:
1. 开发简单、效率高
2. 运维简单、性能高
3. 稳定性高、资源消耗少
4. 满足 Web 开发中的常用功能需求

Lua 语言跟 PHP 类似的同步逻辑开发模式,满足开发效率要求。而 Lua 语言的运行效率可以说是这么多高级语言当中的佼佼者,满足性能高的要求。并且该语言至今已迎来第 20 个生日,在语言本身而言是非常稳定的。

为了简化运维和提高性能,所以决定采用 epoll 事件来驱动整个系统,而不是基于 nginx 来实现。(在第一版成熟后,会考虑其他平台的支持)当然完全自主实现有各有利弊,好处是可定制性高、相对的业务逻辑少,更纯粹于提供 Web 服务。

aLiLua 第一版所支持的功能列表:
1. cosocket (异步网络IO)
2. connection pool
3. HTTP/1.1 (keepalive)
4. gzip/deflate 压缩
5. session
6. 基于共享内存的 key/value cache (Yac weibo.com 在用)
7. template 模板引擎
8. sandbox (可用于做 PaaS 服务)
9. iconv/字符串处理
10. hooker/filter 钩子和过滤器
11. CLI 支持命令行模式
12. writev/sendfile 高性能的网络操作
13. ssl socket
14. mysql/memcached/redis/http client (由 ngx_lua 提供)

计划功能列表:
1. cron 定时任务
2. queue 任务队列
3. aio 文件异步IO
4. cluster key/value 存储支持集群分布
5. mbstring 字符串处理
6. Web Framework 基于 aLiLua 的 Web 开发框架
7. ssl server
8. msgpack rpc
9. …

在完成第一版时我对 Lua 和 node.js 等做了很多压力测试,性能跟 node.js 差不多,但如用 LuaJit 则高出 30%+。最主要的是 Lua 所消耗的内存和 CPU 都比 node.js 少,且占有率比较稳定。压力测试中使用的业务逻辑会比实际项目少,很可能实际效果比 node.js 更好一些。

aLiLua 是个新生的开发模式,底层兼容 ngx_lua ,大家可以先尝试用 aLiLua 来做些小应用 :) 我们提供免费终生保养服务。更希望大家在使用过程中能提出宝贵意见,以使 aLiLua 更完善

下载地址: https://github.com/yo2oneoo/alilua

又拍云存储介绍

  从目前云计算发展情况看来,大家对云服务的稳定、安全问题很关心。以我们所理解的云计算并不应该存在丢失数据等类似的严重问题,云计算的主要使命就是解决这些问题而存在,否则就不能称之为云计算了。如 SAE 类 PaaS 云计算平台是保障应用网站的正常服务,高度容错且可扩展,而又拍云存储则属 IaaS 类云计算平台,又拍云存储是向客户提供基础的存储服务,存储可用性和访问保障是又拍云存储最主要的工作。
  使用云存储服务不应该有数据风险,这是最基本的要求,所以请大家不用担心这个问题。到目前又拍云存储系统已经稳定的为又拍网、图片管家等网站服务两年多时间。存储系统已通过严格的业务测试,才于2011年面向互联网全面开放。可以说对又拍云存储系统的服务能力是有绝对信心!
  简单的从技术层面来说明又拍云存储的架构,让大家对又拍云存储有更深入的认识。热备、无单点这两个词语从头到尾贯穿整个系统架构,用户的一个数据文件存储于又拍云的数据中心,并自动分配到三台且不在同一机柜的服务器上,就是说任何一台服务器故障,都不影响正常访问,甚至某个机柜整体出现故障,因是分布到不同机柜的三台,所以还是不影响正常访问。为避免机房线路故障的情况发生,又拍云的数据中心自主接入了7线(电信两根、网通两根、移动、教育网、华数宽带)。再考虑更恶劣的情况发生,如机房断电,机房因政策问题而暂时无法提供服务等,所以又拍云的数据中心也做到无单点,目前在浙江杭州和萧山分别建立两个数据中心,这两个数据中心是互相热备的。现在大家能放下心头大石了吧 :)
  总结又拍云存储的服务高可用,仅一句话:“分布式的存储架构,且任何设备都做热备份,甚至机房”。
  在数据安全得到充分保障的基础上,又拍云存储为提高数据文件的访问速度,自主建立了覆盖全国的 CDN 网络系统(肯定的,地方节点也跟数据中心架构一样,多服务器热备份,无单点故障隐患)。并在传统 CDN 服务的基础上加入人性化的服务,如:域名防盗链、地址防盗链、IP 防盗链、客户端防盗链和 Token 签名防盗链等,涵盖当前主流防盗链系统的功能。并且这一切只需要用户在管理后台完全管理,在10分钟内全网设置成功。同时又拍云存储更革命性的使用实际流量进行计费,不再像传统 CDN 的带宽计费方式,做到用多少付多少,绝对不多一分钱。
  现在云服务已遍地开花,有做云主机的,云数据库的,更有云存储服务的,可以现在建立网站的成本比以前低很多。我建议大家可以先尝试一下云服务,如考虑把托管的服务器迁移到云主机,而论坛附件、用户上传类文件可考虑存储到又拍云存储上,又拍云存储已提供丰富的接入方式,Discuz和phpwind论坛只要到后台设置 FTP 信息就完才接入工作,很方便的。而对于数据库这类有高要求的数据,我还是建议大家仍旧使用托管的物理服务器,毕竟目前云主机的性能和稳定性方面仍有待观察。

ngx_xxtea 模块发布

  就如其名,这是一个基于 XXTEA 加密算法实现的 NGINX 模块。至于用途,我也不知道。我是用在 URL 防盗方面的(隐藏真实地址)

安装:

configure –add-module=../ngx_xxtea-1.0

配置:

#加密方法,其中 content 和 key 支持 nginx 内部变量
set xxtea_data “aaa”;
set xxtea_key “123″;
set $var1 $xxtea_encrypto;

#解密方法,其中 q+tThwMc2vg= 是需解密的内容
set xxtea_data “q+tThwMc2vg=”;
set xxtea_key “123″;
set $var1 $xxtea_decrypto;

场景:

  我需要对 http://a.com/*.url 下的所有图片资源进行防盗处理,我会在调用页面上使用 php 对这些 url 进行加密。如:

xxtea_encode($URI, $REMOTE_ADDR); // 使用用户的客户端IP,当然你也可以用 COOKIE

  这样页面上所有的图片请求,都应该是:http://a.com/e/q+tThwMc2vg= 类的地址(注意别对 URI 进行 encode)。并且该地址是通过客户端IP进行加密的,也就是说其他人无法使用这个地址。

配置 NGINX 对这类地址进行解密操作:

location /e {
if ( $request_uri ~* “/e/(.*)$”)
{
set $xxtea_data $1;
set $xxtea_key $remote_addr;
rewrite (.*) $xxtea_decrypto;
}
}

  ngx_xxtea 模块 下载地址 (.tar.gz) 压缩包中包含 PHP XXTEA 方法

  另外,章哥也有一个类似功能的模块,地址:https://github.com/agentzh/encrypted-session-nginx-module ,大家按需求选择 :)

充分发挥服务器的各项资源

在当前的网站架构中,经常会有较多的角色分工,如:缓存服务(文件型)、缓存加速服务(内存型)、PHP/Java 计算服务和数据库服务等等。每种服务对服务器的资源占用各有不同,例如:缓存加速服务就需要大量的内存,而 PHP/Java 计算服务则需要占用大量的 CPU 资源。如何充分发挥单机的资源利用,往往是运维的同学负责规划。认为的来进行分配,必然会有所疏漏,也正因此现在发展出 VMware 类虚拟服务器的云计算,可以实现一定的自动分配资源,达到节省成本的效果。

但使用 VMware 类方案并不太适合单个企业选用,原因一:架设一套自主的虚拟机集群,会产生软硬件成本,二:虚拟服务器系统本身要消耗掉一小部分性能。或者我们可以考虑以下方案 :) 可在原有的业务基础上进行简单的梳理,以达到需要的效果。

我们把服务器抽象出,磁盘、内存和 CPU 三项资源。服务角色可把 PHP/Java 归为计算类型,而 Memcached/Redis 等归为内存类型,对文件/页面缓存归为磁盘类,数据库较为特殊,它对三类资源的需求都比较高,将其定位特殊的全需求类型。

现在资源和服务类型都很清晰了,那么我们就可以针对服务器的三项资源进行网络化,抽象出三小块分布式服务。PHP/Java 计算类型,可在Web Server层加入分布式逻辑,使得业务可自动均衡的分配到服务器群;对于 Memcached/Redis 这些缓存加速服务基本都带有分布式支持能力;而磁盘类型的业务则需要找到一个适合业务所需要的解决方案(如业务本身对文件名没要求,那可考虑 FastDFS 类分布式存储系统,否则可考虑 MongoFS 类存储系统)以此我们可以构建出下图所绘的架构:

此架构不仅能让服务器的各项资源得到充分发挥,还具备分布式计算的热备能力,并且单台服务器的软件、配置环境标准化,可减少运维的工作量,日常维护也更加方便。是一举几得的好事!以下为我们某个业务的服务器组状态:

上面这组服务器,我们部署有 PHP/Memcached/MySQL 和一个分布式文件系统。

题外话:这素云计算的基础形态麽?嗯,有点云的意思,当然未能达到这个高度。主要因这个方案是非常基础的,仅适合单网站(公司)使用,如果非要说云,那只能说私有云。而要做公有云,我们还需要在此架构基础上,加入安全隔离、资源租赁和统计等很多子系统。

此文撰写于昨晚国航灰机晚点的几个小时里~晚上10点的票居然凌晨3点才到家~故留此印记。