20,000 req/sでも耐えられるサーバサイトの作り方

お久しぶりです。AdwaysのチーフSE 孟です。
今日の記事ではサーバサイドの話を書きたいと思います。

皆さんが、普段使ってるミドルウェアは、大体こんな感じですよね?

Nginx, MySQL 弊社Adwaysなら Perl, Ruby

そして、

Nginx + リバースプロキシ + 複数WEBサーバ

この構成は、ほぼWEB業界の定番になってます。

まぁ確かにNginxは早いのですが、動的配信ではどうしてもDB側がボトルネックになってしまい、rpsがなかなか上がりません。サービスの運用が続くとデータ量も増え、さらに悪化してしまいます。

そこでのよくある対処方法は、Slave DBを増やしたり、WEBサーバを増やしたり、FusionIOを買ったり、などなど... お金かかる方法ばっかりですよね 。

実はアプリ側をちょっと頑張れば、お金をかけないでチューニングする方法があるんです。 
そんな、ボトルネックを全て排除した、純粋な動的サイトを作る方法を紹介したいと思います。

全体図はこんな感じです。

アプリ


ざっくり説明すると、Nginxと同じHTTPサーバの機能を持ったWEBアプリを直接作っちゃいます。
データ配信は内部のメモリを直接アクセスするので超高速です。
ボトルネックを無くすために、Nginxを使用せず、DBも使いません。ただアプリとメモリとlibeventだけで頑張る方法です。 

これがどれぐらい速いか?
Intel(R) Xeon(R) CPU E5-2670 0 @ 2.60GHzを積んでる仮想サーバCPUコア1つだけでも、動的配信のrpsは軽く2万を越えます。
Nginxでの静的ファイルの配信速度も1コアでのrps2万越えることも難しいでしょ。

もう一度、上の図を見て下さい。キャッシュ更新用Threadも肝です。
DBからのデータ更新のタイミングをリアルタイム性の低いデータは1時間に1回、リアルタイム性の高いデータなら頻繁に差分を更新するようにすればOKです。
そうすることによって、一番厄介なDBのボトルネックを完全にクリアすることができます。

あともう1つのボトルネックは、コーディングで使う言語そのものです。
もしもPerlで図通り作っても、C言語で作ったものよりおよそ10倍遅くなります。

サンプルコードを用意したので、興味ある方は是非使ってみてください。

※必要ライブラリ

gcc -I/usr/local/include -levent -o http http.c

http.c のソース
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <event.h>
#include <evhttp.h>
#include <pthread.h>

#define HOST "0.0.0.0"
#define PORT 20000
#define CACHE_SLEEP 3600

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *pull_cache(void *arg){
    while(1){
        ※ここでキャッシュ更新
        sleep(CACHE_SLEEP);
    }
    return 0;
}

void cb(struct evhttp_request* req, void *arg){
    struct evbuffer *buf;
    buf = evbuffer_new();
    if(buf == NULL)return;

    evhttp_add_header(req->output_headers,"Content-Type","text/plain; charset=utf-8");
    evhttp_add_header(req->output_headers,"Pragma","no-cache");
    evhttp_add_header(req->output_headers,"Cache-Control","no-cache");
    evhttp_add_header(req->output_headers,"Expires","Thu, 01 Dec, 1994 16:00:00 GMT");
    evhttp_add_header(req->output_headers,"Connection","close");

    struct evkeyvalq keys;
    evhttp_parse_query(req->uri,&keys);
    const char *cmd = evhttp_find_header(&keys,"cmd");
    const char *page_size = evhttp_find_header(&keys,"p");
    
    pthread_mutex_lock(&mutex);
    if(cmd == NULL){
        evbuffer_add_printf(buf,"ng");
    }else if(strcmp(cmd,"ping")==0){
        evbuffer_add_printf(buf,"pong");
    }else if(strcmp(cmd,"hoge")==0){
        evbuffer_add_printf(buf,"fuga");
    }
    ※ここでcmdのパターンを追加して処理する
    pthread_mutex_unlock(&mutex);

    evhttp_send_reply(req,HTTP_OK,"",buf);

    evhttp_clear_headers(&keys);
    evbuffer_free(buf);
}

void create_srv(){
    event_init();
    struct event_base *evbase = NULL;
    struct evhttp *evh = evhttp_new(evbase);
    if(evhttp_bind_socket(evh,HOST,PORT)!=0){
        printf("Error! Can't listen to " HOST ":%d, exit\n",PORT);
        exit(1);
    }else{
        evhttp_set_gencb(evh,cb,NULL);
        event_dispatch();
    }
}

int main(int argc, char * argv[]){
    pthread_t t0;
    pthread_create(&t0,NULL,pull_cache,NULL);

    create_srv();

    return 0;