hideden.hatenablog.com

はてなぶろぐー。URLなげー。

nginx+squidで画像キャッシュサーバーの作り方

仕事で画像キャッシュサーバーを構築した時のメモ。大規模事例の設定例が検索してもあまり見つからなかったので同じような境遇の誰かの参考になれば。

  • ピーク時のトラフィックは数Gbps
  • 画像総容量は数十TB
  • バックエンドのstorageが複数種類

規模とアクセス量とアクセスされる画像の種類が多いので、squidでdisk cacheを使用するとCOSS等を使用してもdiskIOで詰まる為、全てon memory cache。cache容量を確保する為に必然的にcacheサーバーの台数も数十台。

1. squidをsibling構成で並列に並べる


cache_peer 10.0.1.1 sibling 80 3130 no-query no-digest proxy-only
cache_peer 10.0.1.2 sibling 80 3130 no-query no-digest proxy-only
cache_peer localhost parent 8000 0 no-query originserver

自分がcacheを持っていなかった場合、ICPでsiblingしてるpeer全てに問い合わせ、cacheを保持してるpeerが見つかった場合はそこから取得して転送。無かった場合のみparentに取得しに行く方式。設定自体は一番簡単。

  • よくやる設定なので実績もあってなにかと安心
  • キャッシュを削除する際、全台にPURGEリクエストを送る必要がある (=台数が多いと大変)
  • configが1台1台で微妙に違うのでdeployめんどくさい

特に数十台並列に並べているとアプリケーション側で行うとpurgeだけで時間がかかるので、purge jobをqueueに投げるとかめんどくさい事をする羽目に。

squid3.1からcache_peerに『htcp-forward-clr』というoptionが追加されていて、ICP(Internet Cache Protocol)の代わりにHTCP(HyperText Caching Protocol)を使えば、ICPには無かったキャッシュを削除するCLRリクエストを送ってくれるらしい。(未検証)
HTTP経由でPURGEを送った場合にもそれをHTCPのCLRとしてsibling先に送ってくれるのを期待したけど駄目だった。HTCPを話すPerlのモジュールも見あたらなかった上に、バイナリなプロトコルで自分で書くには面倒。まだunstableなsquid3.1のみでしかサポートしてないという事もあり断念。

ほんとはsiblingでCARPがやれれば一番いいんだけどなぁ。siblingで全台に問い合わせるのも無駄だし、IDC的な事情でmulticastも使いづらい。Cache-Control: max-age=1とかを連射された場合に複数台に同じファイルがcacheされるのもちょっと微妙。

2. squidを2段構成にしてCARPを使った分散を行う


1段目
cache_peer 10.0.1.1 parent 80 0 no-query carp proxy-only
cache_peer 10.0.1.2 parent 80 0 no-query carp proxy-only

2段目
cache_peer localhost parent 8000 0 no-query originserver

1段目のsquidでCARPによってURLベースのhashingで特定のURLに対する2段目のcacheサーバーを特定の1つに決める方法。URL単位かつ1段目はbalancingだけを行いcacheしない為、キャッシュを削除する時は1段目のどれかに対しPURGEリクエストを1回送ればいい。

  • PURGEが楽
  • cacheも重複しない
  • 同一サーバーにsquidを2つ起動するのが微妙な為、サーバーを分けるしかない
  • 1段目はキャッシュしない為、全トラフィックが1段目を通過する事になる
  • 貧乏性なので1段目のメモリがもったいなく思えてしょうがない

squidを2つ起動しちゃ駄目って書いてあるわけではないんだけど、なんとなく気分的に。それなりに負荷あるし。1段目のsquidは普通にproxyとして動作するので、当然全てのデータが通過する。たとえば画像のトラフィックが3Gbpsあった場合、1段目のサーバー群全体でバックエンドから3Gbpsで受信し、クライアントに3Gbpsで送信する事になり、送受信が同じスイッチを経由してたりすると、、、とか、上流と下流で分けるのはそれはそれでラック的に、、、とかなんか微妙らしい。
その辺は専門外なので良くわからないけど、1段目のメモリがもったいないって事だけはわかる。


3. 1段目をnginx、2段目にsquidを置いて同居

ちょっと長いけどconfig例

user  nginx nginx;
worker_processes  4;

error_log  logs/error.log;
pid        logs/nginx.pid;
worker_rlimit_nofile 200000;

events {
    use epoll;
    worker_connections  10000;
    # max_connections = worker_processes * worker_connections / 4 (reverce proxy)
}

http {
    include                mime.types;
    default_type           text/html;
    sendfile               on;
    tcp_nopush             on;
    send_timeout           10;
    keepalive_timeout      5 3;
    keepalive_requests     30;
    output_buffers         1 64k;
    postpone_output        1460;

    client_header_timeout        5;
    client_body_timeout          5;
    client_body_temp_path        /tmp/nginx/client_temp 1 2 3;
    client_max_body_size         10m;
    client_body_buffer_size      32k;
    client_header_buffer_size    2k;
    large_client_header_buffers  4 8k;

    log_format         main  '$remote_addr - $remote_user [$time_local] "$request" '
                             '$status $body_bytes_sent "$http_referer" '
                             '"$http_user_agent" "$http_x_forwarded_for"';
    access_log         logs/access.log  main;

    # consistent hash balancing
    upstream image-cache {
        consistent_hash $uri;
        server 10.0.1.1:3128;
        server 10.0.1.2:3128;
        server 10.0.1.3:3128;
    }

    # failover (normal balancing)
    upstream image-cache-failover {
        server 10.0.1.1:3128;
        server 10.0.1.2:3128;
        server 10.0.1.3:3128;
    }

    # dispatcher
    upstream image-cache-dispatcher {
        server 10.0.1.1:10080;
        server 10.0.1.2:10080;
        server 10.0.1.3:10080;
    }

    server {
        listen       80;
        server_name  img.example.com;
        expires      30d;

        proxy_connect_timeout 5;
        proxy_send_timeout    5;
        proxy_read_timeout    10;
        proxy_buffering       off;
        proxy_set_header      Host       $host;
        proxy_set_header      X-Real_IP  $remote_addr;
        # squidの付けるヘッダを消す
        proxy_hide_header     X-Cache;
        proxy_hide_header     X-Cache-Lookup;
        proxy_hide_header     X-Squid-Error;
        proxy_hide_header     Warning;
        proxy_hide_header     Via;

        # squidが死んでた場合に502になるのでその場合はfailover
        error_page 502 = /failover;

        location / {
            # rewriteでquery string削る
            if ( $is_args ) {
                rewrite ^(.*)$ $1?;
            }
            # GET, HEAD, PURGEの場合はsquidへ
            if ( $request_method = GET ) {
                proxy_pass http://image-cache;
                break;
            }
            if ( $request_method = HEAD ) {
                proxy_pass http://image-cache;
                break;
            }
            if ( $request_method = PURGE ) {
                proxy_pass http://image-cache;
                break;
            }
            # 画像サーバーはPOST受け付けてないよ
            if ( $request_method = POST ) {
                return 405;
            }
            # その他(WebDAV的なのとか)はsquidを迂回してapacheへ
            proxy_pass http://image-cache-dispatcher;
        }

        location /failover {
            if ( $is_args ) {
                rewrite ^(.*)$ $1?;
            }
            if ( $request_method = GET ) {
                proxy_pass http://image-cache-failover;
                break;
            }
            if ( $request_method = HEAD ) {
                proxy_pass http://image-cache-failover;
                break;
            }
            if ( $request_method = PURGE ) {
                proxy_pass http://image-cache-failover;
                break;
            }
            if ( $request_method = POST ) {
                return 405;
            }
            proxy_pass http://image-cache-dispatcher;
        }
    }
}

1段目のsquidをnginxに置き換えて同居させたもの。upstreamへの分散は通常均等に分散されるのだが、3rd party pluginのngx_http_upstream_consistent_hashを使えばCARPのような事がやれる。そのままだとバックエンドのsquidが落ちた場合に一部URLだけ見られなくなってしまうので、その場合のみconsistent hashingな分散を諦めてランダムにどれかのサーバーにrequestが行く。

  • なんとなく最近nginxが流行ってる気がする
  • nginx軽い(メモリ使用量もこれだけやるなら10MB以下)
  • いっぱい接続しても大丈夫らしい
  • ngx_http_limit_zone_moduleやngx_http_limit_req_moduleで同時接続数やリクエスト頻度を制限出来たりもするらしい
  • load-balancerから全cacheサーバーにトラフィックが分散するのでネットワーク的に楽っぽい

って感じで最終的に3番になりました。

いくつか注意点

    client_header_buffer_size    2k;
    large_client_header_buffers  4 8k;

いくつかの携帯電話や、何らかの理由でCookieを食いまくってる場合(画像サーバーでcookie食うのはトラフィックの無駄なのでよくない。ドメイン分けてcookie撲滅すべき。)とかに、リクエストヘッダが肥大化している場合があるので、それを見越したサイズにしないとエラーになる。auとかauとかauとかで。


大量のconnectionがある場合はsquidコンパイル時に

ulimit -n 100000
./configure --with-descriptors=32768 ...

などとしておく。ulimitする前にconfigureすると1024とかに戻って萎える。システム全体のfile descriptorの最大はlinuxの場合は/proc/sys/fs/file-maxで確認/設定。


上記設定等は例であり動作未確認です。間違ってたらごめんなさい。