aaashun's site

home

openresty用于计算密集型服务的实践

03 Dec 2014

这篇文章更多的是记录了设计的过程, 不仅仅是设计的结果.

我把计算密集型服务分为三类: 1. 收到请求后异步计算 2. 收到请求后同步计算 3. 收到请求时实时计算 对于第1类, 一般接收到请求后把任务插入队列, 异步处理完后通知客户端即可, 对于第2类, 用多nginx worker即可很好解决, 本文主要讲的是第3类, 在我的实际项目中与客户端的双向通信协议是websocket, 数据流封装在多个webwsocket frame里边接收边处理, 处理过程中还会有一些临时结果需要用websocket frame实时返回给客户端, 这样计算和网络IO放在一起会冲突. (多谢OpenResty社区G_will, mem phis, Nero.Ping等指正, 在此说明一下分类)

1. 原服务那些事

时间回到2011年, 几个初生牛犊的工程师用c语言完整实现了server/client, server端又是分三层: httpa, core-proxy, core-service. server端实现时主要参考了nginx, 但是仅实现了我们所需要的特性, 当时的两个NB理由:


图1. 原服务端架构图

时过境迁, 2年后当年NB服务的诸多问题了慢慢呈现出来:

到后来已经渐渐没人愿意, 也没人有能力维护好这一代的服务程序了, 再也跟不上业务发展的需要了.

2. 基于openresty的新服务

2014年初我们开始重新思考计算密集型服务程序的设计, 只有一个核心目标: 简单, 一定要简单. 简单才易于开发和维护, 简单才易于整体架构, 简单才易于扩展, 简单才易于做到稳定, 只有做简单才有资格谈性能, 这也是UNIX哲学里最重要的KISS原则.

这次从开始就没打算自已撸, 由于略懂nginx, 平时又受益于lua, 便尝试openresty(nginx + ngx_lua)作为计算服务的容器.

原本nginx作为单线程EventLoop的代表, 以其每秒轻松5万的QPS的出色性能, 一般是作为网络IO密集型的web服务器或代理服务器. openresty的出现让nginx具备应用服务器的能力, 但作为计算服务容器还有两个主要问题需要解决:

openresty的lua代码执行是在事件循环里的, 在lua代码里不能有任何复杂的计算或其它可能阻塞主循环的代码, 常见的做法是通过fastcgi与计算服务通信, 就像php-fpm那样


图2. nginx + fastcgi

fastcgi在这里只是代理的角色, 让应用服务程序与web服务程序解耦, 这样应用服务程序便可以用python, php等其它开发语言实现了. fastcgi的确可以解决这两个问题, 但是想利用lua作为胶水语言还得费点事. 所以我们又尝试完全在openresty里做, 如下图所示:


图3. all in openresty

2.1 只使用一个nginx worker

因为是计算密集型服务, 一台服务器只能处理不到一百个并发请求甚至更少, 一个nginx worker处理网络IO足够了.

2.2 posix.fork()创建计算进程

ngx_lua没有提供fork方法, 我使用的是luaposix, luaposix提供了丰富的posix方法的lua绑定.

另外由于还需要对计算进程有更灵活的控制, 比如某个计算耗时异常时需要强行kill, 某个计算进程crash时的特殊处理, 所以选择了自已fork并管理计算进程.

2.3 nginx worker和计算进程间通过tcp连接通信

为了避免block主循环, nginx worker端的服务代码里使用cosocket与计算进程通信即可.

2.4 所有服务逻辑都用lua实现

lua作为服务端脚本有诸多优点:

我倾向于所有服务逻辑都用lua编写, 一些核心的计算内核的c代码binding到lua, 作为lua模块在lua里调用.

另外ffi也能完成绑定, 我相信性能上也更好, 这里没有用ffi是因为有的计算模块不仅要运行在服务器还有可能运行在没有luajit的移动app里, 为了保证接口一致, 目前是全部做了lua绑定, 并提供一致的lua接口, 需要再优化时会考虑ffi. (多谢OpenResty社区G_will建议)

2.5 对计算进程的一些特殊要求

不能使用ngx.socket, ngx.sleep, ngx.timer, ngx.log, ngx.thread等ngx_lua里的'同步非阻塞'的方法, 在计算进程用luasocket与nginx worker进行同步阻塞的tcp通信, 在计算进程里只需埋头计算, 计算完成后直接退出进程即可.

2.6 简单的进程间通信协议


图4. 简单的进程间通信协议

这只是简单的文本协议示例, 第一行是请求类型标识, 第二行是数据长度, 紧接着是数据内容. 使用文本协议, 简单直白可读易解析.

2.7 一些优化的设计和实现

写在最后: 最终那5万多行c代码被简化成了两千行lua代码, 以前要维护的三个服务程序也变成了一个, 逻辑架构也是如上图那么简洁.

comments powered by Disqus