扶凯

取势 明道 优术

作者为 扶 凯 发表

我们常常听到一个问题 "在众多 Perl Web 框架中, 我为什么要选择 Mojolicious?", 对于这个问题,我有太多的答案可以告诉你,但我认为最主要的区别是 Mojolicious  的设计是非阻塞的. 你们中很多人可能听说 Node.js 之所有受欢迎的原因是它是设计成非阻塞的. 当你写你的 webapp 的应用使用非阻塞的框架和技术时,你可以创建一个更加快,更加精巧的应用. 只需要很少的服务器资源来处理和其它大量程序处理相同的处理量. 虽然 Perl 有很多 Web 框架. 但只有 Mojolicious 从设计开始就是为非阻塞而生的.

为了演示一个无阻塞应用, 我打算写一个简单的应用使用 Mojolicious 和 Mango, 非阻塞的 MongoDB 的 lib 库(这个的作者也是 Mojo 的作者);

模板技术

我们在看服务的其它代码前, 我们看看模板, 它将构成应用的程序的视图部分(MVC). Mojolicious 有自己的 模板引擎技术 它只是对 Perl 语法简单的包装,所以性能很好,上手非常快.

@@ layouts/basic.html.ep

<!DOCTYPE html>
<html>
  <head>
    <title><%= title =%></title>
    %= stylesheet '//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css'
  </head>
  <body>
    <div class="container">
      <%= content =%>
    </div>
  </body>
</html>

@@ show.html.ep

% title $doc->{title};
% layout 'basic';

%= stylesheet begin
pre.prettyprint {
  background-color:inherit;
  border:none;
}
% end
%= tag h1 => $doc->{title}
%= tag div => class => 'well' => begin
  %= tag pre => class => 'prettyprint' => begin
    <%= $doc->{content} =%>
  % end
% end
%= javascript 'https://google-code-prettify.googlecode.com/svn/loader/run_prettify.js'

@@ submit.html.ep

% title 'Paste your content';
% layout 'basic';

%= form_for '/' => role => form => method => POST => begin
  %= tag div => class => 'form-group' => begin
    %= tag label => for => 'title' => 'Title'
    %= text_field 'title', class => 'form-control'
  % end
  %= tag div => class => 'form-group' => begin
    %= tag label => for => 'content' => 'Paste Content'
    %= text_area  'content', class => 'form-control'
  % end
  %= submit_button 'Paste' => class => 'btn btn-primary'
% end

在第一个模板 layouts/basic.html.ep 会被用任何其它模板, 在其它模板调用的时候,都会请求这个 base 的 HTML 的布局(layout). 请求模板的时候中的内容时指定的 content  字段直接替换成相应的内容. 这的 show.html.ep 和 submit.html.ep 的模板(两个加载的 base 的 layout 层) 是用于当用户想要粘贴和创建新的时候的时候显示用.

注意 show.html.ep  的模板, 可以见到一个 $doc 的变量. 这是模板变量由 controller 使用 stash 命令之后创建的. 你见到的这些功能都是由 Mojolicious 中调用的 helpers 的用法. 另外,这使用了一些  Twitter’s Bootstrap 的样式.

这些模板有二个地方可以放,一个可以插入代码文件本身的 __DATA__ 部分.也可以单独放置在一个叫 templates 的模板目录中.

阻塞的实现方式

由于大多数人都熟悉编写使用阻塞风格(默认)来写你的应用程序,我这以这种方式来写我们的应用, 当然 Mojo 也支持这种方式并且也工作的非常好

#!/usr/bin/env perl

use Mojolicious::Lite;
use Mango;
use Mango::BSON 'bson_oid';

helper mango  => sub { state $mango = Mango->new($ENV{PASTEDB}) };
helper pastes => sub { shift->mango->db->collection('pastes') };

get '/' => 'submit';

post '/' => sub {
  my $self = shift;
  my $title = $self->param('title') || 'Untitled';
  my $content = $self->param('content')
    or return $self->redirect_to('/');
  my $doc = {
    title   => $title,
    content => $content,
  };
  my $oid = $self->pastes->save($doc);
  $self->redirect_to( show => id => "$oid" );
};

get '/:id' => sub {
  my $self = shift;
  my $id = bson_oid $self->stash('id');
  my $doc = $self->pastes->find_one({ _id => $id })
    or return $self->redirect_to('/');
  $self->stash( doc => $doc );
} => 'show';

app->start;

导入必要的库(导入 Mojo 会打开 strict, warningsutf8 并且全部的 v5.10 特性),创建一些我们自己的 helpers. 这有个 mango helper 用于连接到 Mongo 实例, 我会有环境变量中指定连接所需要的信息.  (是的,我可以把它在配置文件, but I had to draw the line on this example somewhere :-) ). 我还有一个 helper 用于返回实例中的  (read: table) 的集合,这是用于存储粘贴的信息. 默认的 Mango 会创建独一无二的文档 ID, 在我们这个应用中, 我们给这个做为我们的页面标识符.

helpers 创建完后, 下面创建了三个路由(route)连接到 controller 的回调的子函数(Lite 版本的 controller 方法), 目的应该是相当自我解释, 进入这个 route 就实现这个功能.我们可以注意到一个事, 就是这个地方并没有显示的调用 render 渲染生成网页. 这个 controller 会自动的找到相同名字的模板来渲染生成网页. 这个名字是 controller 的子函数后面的字符串. 我这个地方可以使用 render 的方法调用,但是没那么简洁,也不是那么有必要.

运行这个应用(完整的程序见 here,  不要忘记数据库的环境变量!)会启动一个 web 服务器. 这个应用可以运行在 CGI 的环境或 PSGI 的服务, 也可以使用 Mojolicious 的应用服务器built-in servers.

该应用程序应该就能像您期望的那样运行, 但它有一个重大的缺点任何单个客户端请求都会导致应用程序请求到数据库查询,然后其它的所有客户端必须等待当前这个请求做出响应后服务器才可能处理下一个客户端的请求. 同时,服务器大量其它的资源都处于闲置状态. 这样低效的, 不是吗?

 

非阻塞的实现方式

现在, 我可以新写一个非常类似的应用程序, 但其中有几个小的调整,主要是防止数据库调用会阻塞应用程序.

#!/usr/bin/env perl

use Mojolicious::Lite;
use Mango;
use Mango::BSON 'bson_oid';

helper mango  => sub { state $mango = Mango->new($ENV{PASTEDB}) };
helper pastes => sub { shift->mango->db->collection('pastes') };

get '/' => 'submit';

post '/' => sub {
  my $self = shift;
  my $title = $self->param('title') || 'Untitled';
  my $content = $self->param('content')
    or return $self->redirect_to('/');
  my $doc = {
    title   => $title,
    content => $content,
  };
  $self->render_later;
  $self->pastes->save($doc, sub {
    my ($coll, $err, $oid) = @_;
    $self->redirect_to( show => id => "$oid" );
  });
};

get '/:id' => sub {
  my $self = shift;
  my $id = bson_oid $self->stash('id');
  $self->render_later;
  $self->pastes->find_one({ _id => $id }, sub {
    my ($coll, $err, $doc) = @_;
    return $self->redirect_to('/') if ( $err or not $doc );
    $self->render( show => doc => $doc );
  });
} => 'show';

app->start;

这个新的代码相差几行, 但它们非常的重要! 首先, 在一个非阻塞调用之前, 我必须调用 render_later 防止上面讲到的自动渲染; 因为这时候子函数执行完立即渲染会没有数据.数据在回调中,需要显示的调用.

    title   => $title,
    content => $content,
  };
-  my $oid = $self->pastes->save($doc);
-  $self->redirect_to( show => id => "$oid" );
+  $self->render_later;
+  $self->pastes->save($doc, sub {
+    my ($coll, $err, $oid) = @_;
+    $self->redirect_to( show => id => "$oid" );
+  });
};

POST 处理程序中, 你可以在见到 save 的方法调用, 不过, 在这个地方, 我并不只是传递文档内容, 我是给了一个包含插入数据的子函数的引用(回调). 当客户端提交新的粘贴的内容到服务器, 它会等待, 直到数据库的插入完成, 然后被重定向到查看文档的位置. 不同于上面第一个例子. 这个时候整个服务在等待这个客户端插入时, 服务器还能处理其它客户端的数据库插入的服务.因为子函数早调用完成.可以接着处理其它请求. 只是在这注册了一个回调.回调完成后自动那个用户就处理完了.其它的请求还是能正常的进来.

get '/:id' => sub {
  my $self = shift;
  my $id = bson_oid $self->stash('id');
-  my $doc = $self->pastes->find_one({ _id => $id })
-    or return $self->redirect_to('/');
-  $self->stash( doc => $doc );
+  $self->render_later;
+  $self->pastes->find_one({ _id => $id }, sub {
+    my ($coll, $err, $doc) = @_;
+    return $self->redirect_to('/') if ( $err or not $doc );
+    $self->render( show => doc => $doc );
+  });
} => 'show';

相似的我们也要更改 show 的 controller 的子函数. 我再次, 调用 render_later 进入一个回调,然后将调用查询数据库的逻辑. 这个时候, 服务器请求的数据库, 用来获得有关文件, 然后继续其他客户提供其它请求的服务, 直到数据库响应. 在当响应返回时, 服务器调用回调, 让客户端看到的请求的页面.

有个小缺点, 你必须使用 Mojolicious 原生的 Web 服务器来支持事件. 你没必要担心这个问题, 它们很强大.

Ok this is cool but …

是的, 看起来差别并不大, 但是回报还是不错. 如果你执行你的数据库在同一台主机, 你可能只见到很少或者没有性能增加. 当然, 如果你使用的异地数据库主机象 MongoHQ 之类,还有查询需要时间很长的应用, 你可以通过 wrk 清楚地看到重写成非阻塞应用程序的好处:

$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad blocking_paste.pl
$ wrk -t 10 -c 10 -d 1m http://localhost:8080/0
Running 1m test @ http://localhost:8080/0
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   286.87ms  331.49ms   1.45s    90.25%
    Req/Sec     5.91      4.32    22.00     81.12%
  3745 requests in 1.00m, 2.83MB read
Requests/sec:     62.41
Transfer/sec:     48.32KB
$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad -s blocking_paste.pl

… 非阻塞应用:

$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad nonblocking_paste.pl
$ wrk -t 10 -c 10 -d 1m http://localhost:8080/0
Running 1m test @ http://localhost:8080/0
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    59.41ms   18.69ms 365.66ms   97.45%
    Req/Sec    16.73      2.19    21.00     75.56%
  10290 requests in 1.00m, 7.78MB read
Requests/sec:    171.49
Transfer/sec:    132.77KB
$ PASTEDB=mongodb://demo:pass@linus.mongohq.com:10025/MangoTest hypnotoad -s nonblocking_paste.pl

注意, 这个应用只是跑在一个旧电脑和免费数据库中的玩具. 上面表明, 在传输和请求的处理, 有非常明显的改善. 你可以自己试下. 二个程序可以在这找到 here

结论

Mojolicious 是一个非常强大Web 开发框架, 使难的事情变得更加容易, 尤其是非阻塞代码. 这篇文章是使用 Mojolicious 系列的第一个无阻塞的 webapps , 希望有多一般应用程序使用 Mojolicious. Happy Perling!

P.S. 看看 Mojo::IOLoop::Delay 有关的 Mojolicious 中关于非阻塞的 tool suite 可以使你写 non-blocking 更加容易.

来了就留个评论吧! 7个评论



    akawhy 2013年11月5日 的 04:38

    感谢凯哥分享;)

    strider 2013年11月14日 的 04:45

    凯哥,你近来写的都是Perl的啊,能不能写点别的技术种类的,哈哈

    haryzhou 2014年01月21日 的 09:48

    # Scalar context
    $delay = Mojo::IOLoop::Delay->new;
    for my $i (1, 2) {
    my $end = $delay->begin(0);
    Mojo::IOLoop->timer(0 => sub { $end->($i) });
    }
    is scalar $delay->wait, 1, ‘right return value’;

    Delay这个东西还是没有深刻理解, 看了下Mojo::IOLoop::Delay源代码, 没理清楚头绪。 能分析下Mojolicious自带的t/mojo/delay.t这里面的测试用例么?

      扶 凯 2014年01月22日 的 07:14

      就是建个 delay 的对象,然后每 begin 一次就加一次,然后 $end-> 调一次就减少一次,直到为零,然后完.

    seufy88 2014年02月15日 的 02:56

    请问下面这句怎么理解
    “该应用程序应该就能像您期望的那样运行, 但它有一个重大的缺点!任何单个客户端请求都会导致应用程序请求到数据库查询,然后其它的所有客户端必须等待当前这个请求做出响应后服务器才可能处理下一个客户端的请求. 同时,服务器大量其它的资源都处于闲置状态. 这样是低效的, 不是吗?”

    每个单独的客户端独立的访问WEB Server,SERVER会为每个客户的请求进行处理,相当于即使每个客户端请求上述的阻塞CGI,有什么问题吗?服务器难道不是并行的处理每个客户的请求吗?

    “然后其它的所有客户端必须等待当前这个请求做出响应后服务器才可能处理下一个客户端的请求”
    —>这句描述有点疑问,我并没做过服务器开发,但是Client A 请求 example.com/test.cgi
    和Client B 请求example.com/test.cgi难道相互有什么影响吗?(如果test.cgi是以例子是所谓的阻塞方式编写),在服务器程序来说,不都是要同时起动两个”程序实例”来处理A,B的请求吗?

    总结来说,阻塞和非阻塞没有十分理解透.希望能够说明的更好明白一点.

    batman 2014年03月22日 的 09:24

    维基百科,自由的百科全书

    MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。