2009年4月26日 星期日

Passenger architectural overview

Ruby on Rails 架設原理
Passenger architectural overview
1.1. Typical web applications
1. 一個單純的web application 接受 http request 從某種I/O channel, 處理, 然後丟 http response 回去 client, 這種情況會一直loop 下去直到 application 結束.

這樣並不代表web application 必須能直接跟 HTTP 溝通, 而只是代表 web application 能夠接受某種http request representation

2. 很少 web application 直接跟HTPP client 溝通, 常見的方式有
Few web applications are accessible directly by HTTP clients. Common setups are:

1. web application 由 application server 控制, 一個 application server 可能有多個 web application, 然後 application server 在連結 web server . web server <-> application server <-> web application. A typical example of such a setup is a J2EE application, contained in the Tomcat web server, behind the Apache web server.

2. web application 由 web server 控制, 在這種情況 web server 就像 application server, 這個例子就是像 apacher server 用 mod_php 控制 php application. 注意這樣不代表 web application 都是跑在和web server 的 procsss 上, 它只是代表由 web server 來管理 web application

3. web application 本身就是 web server 能夠直接收 HTTP request , 這樣的代表是 Trac bug tracking system , running in its standalone server, 在很多的佈署時, 像這樣的 web application 會被設定在不同的 web server 的後段, 不讓他們直接收 HTTP request, 前端的web server 便如 reverse HTTP proxy 一般

4. web application 不能夠夠直接說 HTTP directly 但是卻能夠直接與web server 透過某種介面溝通, 這樣的例子是CGI, FastCGI 和 SCGI

上面這些都是真的有在運作的佈署模式, 沒有一個模式能做到而其他做不到, 對於client 而言 web server, application server , web application 就如一個 black box

另外也需注意的是上面各種模式並沒有特別針對偏向某種 I/O processing , The web servers, application servers, web applications, etc. could process I/O serially (i.e. one request at a time), could multiplex I/O with a single thread (e.g. by using select(2) or poll(2)) or it could process I/O with multiple threads and/or multiple processes.

1.2. Ruby on Rails
每個 Rails application 都有一個 dispatch . 這個dispacther 負責處理 HTTP request 但是它也不直接受HTPP request, 相反地, 而是它接受的是包含 HTTP request 資訊的某種格式, 所以了解這個dispatcher 對於想要發展能夠與 Rails 透過 HTTP 溝通軟體(例如 web server)的人就很重要

Rails dispatcher 只能過一次處理一個request, 因為rails 不是 thread-safe, 但實際上這也不是什麼大問題

另外一個需要注意的是 Rails application 需要很多memory 來存 program code , 啟動一隻 rails application 在 bootstrapping 花費很多時間



Handling of concurrent requests
就如上面提到的, 一個 Rails application instance 只能一次處理一個request, 這很明顯是不受歡迎的, 但是在我們去找解決方案時, 讓我們先來看一下"競爭對手"如何解決這個問題, PHP 也有同樣類似的問題, 一個 PHP script 只能夠一次處理一個 HTTP request

* mod_php 解決這個問題利用apache 的MPM 機制, 換句話說, mod_php 本身不做些什麼, 一個apache worker process/thread 一樣指能夠處理一個 php request, 但是Apache spawns multiple worker processes/threads.

*PHP-FastCGI 處理這個問題是藉由spawn multiple persistent PHP server , PHP server 之間的數目是和apache worker process/threads 的數目是不相關的

Passenger 不能使用mod_php的方法, 因為它會使我們 spawn a new Rails application 對於每一個來的request 這樣會導致非常的慢, 相反地Passenger 用PHP-FastCGI的方法, 我們 maintain 一個 application instance pool 當有request 來的時候我們將forward 這個request 到其中一個application instance 去, 這個 pool size 是可以設定的, 這對於管理者而言能夠去控制loading 和 memory



Apache
The Apache web server has a pluggable I/O multiprocessing (the ability to handle more than 1 concurrent HTTP client at the same time) architecture.

An Apache module which implements a particular multiprocessing strategy, is called a Multi-Processing Module (MPM). The prefork MPM — which also happens to be the default — appears to be the most popular one.

This MPM spawns multiple worker child processes. HTTP requests are first accepted by a so-called control process, and then forwarded to one of the worker processes. The next section contains a diagram which shows the prefork MPM's architecture.



Passenger architecture
Passenger's architecture is a lot like setup #2 described in Typical web applications.

In other words, Passenger extends Apache and allows it to act like an application server. Passenger's architecture — assuming Apache 2 with the prefork MPM is used — is shown in the following diagram:

Passenger consists of an Apache module, mod_passenger. This is written in C++, and can be found in the directory ext/apache2.

The module is active in the Apache control process and in all the Apache worker processes.

When an HTTP request comes in, mod_passenger will check whether the request should be handled by a Ruby on Rails application.

If so, then mod_passenger will spawn the corresponding Rails application (if necessary) and forward the request to that application.

It should be noted that the Ruby on Rails application does not run in the same address space as Apache.

This differentiates Passenger from other application-server-inside-web-server software such as mod_php, mod_perl and mod_ruby.

If the Rails application crashes or leak memory, it will have no effect on Apache. In fact, stability is one of our highest goals. Passenger is carefully designed and implemented so that Apache shouldn't crash because of Passenger.



Spawning and caching of code and applications

A very naive implementation of Passenger would spawn a Ruby on Rails application every time an HTTP request is received, just like CGI would.

However, spawning Ruby on Rails applications is expensive. It can take 1 or 2 seconds on a modern PC, and possibly much longer on a heavily loaded server. This overhead is particularily unacceptable on shared hosts.

A less naive implementation would keep spawned Ruby on Rails application instances alive, similar to how Lighttpd's FastCGI implementation works. However, this still has several problems:

1. The first request to a Rails website will be slow, and subsequent requests will be fast. But the first request to a different Rails website - on the same web server - will still be slow.

2. As we've explained earlier in this article, a lot of memory in a Rails application is spent on storing the AST of the Ruby on Rails framework and the application. Especially on shared hosts and on memory-constrained Virtual Private Servers (VPS), this can be a problem.

Both of these problems are very much solvable, and we've chosen to do just that.

The first problem can be solved by preloading Rails applications, i.e. by running the Rails application before a request is ever made to that website.

This is the approach taken by most Rails hosts, for example in the form of a Mongrel cluster which is running all the time. However, this is unacceptable for a shared host: such an application would just sit there and waste memory even if it's not doing anything. Instead, we've chosen to take a different approach, which solves both of the aforementioned problems.

We spawn Rails applications via a spawn server. The spawn server caches Ruby on Rails framework code and application code in memory.

Spawning a Rails application for the first time will still be slow, but subsequent spawn attempts will be very fast. Furthermore, because the framework code is cached independently from the application code, spawning a different Rails application will also be very fast, as long as that application is using a Rails framework version that has already been cached.

Another implication of the spawn server is that different Ruby on Rails will share memory with each other, thus solving problem #2. This is described in detail in the next section.

But despite the caching of framework code and application code, spawning is still expensive compared to an HTTP request. We want to avoid spawning whenever possible. This is why we've introduced the application pool.

Spawned application instances are kept alive, and their handles are stored into this pool, allowing each application instance to be reused later. Thus, Passenger has very good average case performance.

The application pool is shared between different worker processes.

Because the worker processes cannot share memory with each other, either shared memory must be used to implement the application pool, or a client/server architecture must be implemented.

We've chosen the latter because it is easier to implement. The Apache control process acts like a server for the application pool. However, this does not mean that all HTTP request/response data go through the control process.

A worker process queries the pool for a connection session with a Rails application. Once this session has been obtained, the worker process will communicate directly with the Rails application.

The application pool is implemented inside mod_passenger. One can find detailed documentation about it in the C++ API documentation, in particular the documentation about the ApplicationPool, StandardApplicationPool and ApplicationPoolServer classes.

The application pool is responsible for spawning applications, caching spawned applications' handles, and cleaning up applications which have been idle for an extended period of time.



The spawn server
The spawn server is written in Ruby, and its code can be found in the directory lib/passenger. Its main executable is bin/passenger-spawn-server. The spawn server's RDoc documentation documents the implementation in detail.

The spawn server consists of 3 logical layers:

1. The spawn manager. This is the topmost layer, and acts like a fascade for all the underlying layers. Clients who use the spawn server only communicate with this layer.

2. The framework spawner server. The spawn manager spawns a framework spawner server for each unique Ruby on Rails framework version.

Each framework spawner server caches the code for exactly one Ruby on Rails framework version. A spawn request for an application is forwarded to the framework spawner server that contains the correct Ruby on Rails version for the application.

3. The application spawner server. This is to the framework spawner server what the framework spawner server is to the spawn manager.

The framework spawner server spawns an application spawner server for each unique Ruby on Rails application (here “application” does not mean a running process, but a set of (source code) files). An application spawner server caches the code for exactly one application.

As you can see, we have two layers of code caching: when the spawn server receives a request to spawn a new application instance, it will forward the request to the correct framework spawner server (and will spawn that framework spawner server if it doesn't already exist), which — in turn — will forward it to the correct application spawner server (which will, again, be created if it doesn't already exist).

Each layer is only responsible for the layer directly below. The spawn manager only knows about framework spawner servers, and a framework spawner server only knows about its application spawner servers. The application spawner server is, however, not responsible for managing spawned application instances.

If an application instance is spawned by mod_passenger, its information will be sent back to mod_passenger, which will be fully responsible for managing the application instance's life time (through the application pool).

Also note that each layer is a seperate process. This is required because a single Ruby process can only load a single Ruby on Rails framework and a single application.



Memory sharing
On most modern Unix operating systems, when a child process is created, it will share most of its memory with the parent process.

Processes are not supposed to be able to access each others' memory, so the operating system makes a copy of a piece of memory when it is written to by the parent process or the child process.

This is called copy-on-write (COW). Detailed background information can be found on Ruby Enterprise Edition's website.

The spawn server makes use of this useful fact.

Each layer shares its Ruby AST memory with all of its lower layers, as long as the AST nodes in question haven't been written to.

This means that all spawned Rails applications will — if possible — share the Ruby on Rails framework's code, as well as its own application code, with each other. This results in a dramatic reduction in memory usage.

沒有留言: