memcached 1.4の到来

第2回バイナリプロトコルの扱い

株式会社ミクシィの前坂です。第1回でmemcached 1.4の簡単な紹介をしました。今回は新しく正式導入されたバイナリプロトコルの扱い方をご紹介いたします。

バイナリプロトコルの扱い

バイナリプロトコルを扱うには、アプリケーションのプログラミング言語に合ったバイナリプロトコル対応のクライアントライブラリが必要です。バイナリプロトコルは最近導入されたこともあり、ネイティブ対応していると報告されているクライアントライブラリはC言語のlibmemcachedとJavaのspymemcachedだけです(2009年8月時点⁠⁠。ただし、世の中にはlibmemcachedをwrapした、さまざまの言語で記述されたクライアントライブラリがいくつかあり、それらを使ってバイナリプロトコルを扱うことが可能です。

今回の記事ではそれらのクライアントも含めて, C、Java、Python、PHPでのデモコードを紹介いたします。

バイナリプロトコルを扱うデモコード

バイナリプロトコルを扱うにはバイナリモードに適応するというコードを1行追加するだけで, ほとんどの場合は従来のテキストプロトコルと扱いは変わりません。以下が今回の検証に使ったクライアントライブラリ集です。

デモコードはmemcachedがlocalhostのデフォルトポート(11211)に立ち上がっている事を想定しており、単純に1つのレコードをSET/GETします。

libmemcached(C言語)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libmemcached/memcached.h>

int main(int argc, char **argv) {
  memcached_st *memc = NULL;
  memcached_return rv; 
  const char *key = "some_key";
  const char *val = "this is a value";
  int data;

  if ((memc = memcached_create(NULL)) == NULL) {
    fprintf(stderr, "failed to allocate memory\n");
    return 1;
  }

  rv = memcached_server_add(memc, "localhost", 11211);

  if (rv != MEMCACHED_SUCCESS) {
    fprintf(stderr, "failed to set server\n");
    return 1;
  }

  rv = memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, data);

  if (rv != MEMCACHED_SUCCESS) {
    fprintf(stderr, "failed to enable binary protocol\n");
    return 1;
  }

  rv =  memcached_set(memc, key, strlen(key), val, strlen(val), 0, 0); 

  if (rv != MEMCACHED_SUCCESS) {
    fprintf(stderr, "failed to set record\n");
    return 1;
  }

  char *result;
  uint32_t flags;
  size_t result_length;
  result = memcached_get(memc, key, strlen(key), &result_length, &flags, &rv); 

  if (rv != MEMCACHED_SUCCESS) {
    fprintf(stderr, "failed to fetch record\n");
    return 1;
  }

  printf("Key: %s\n", key);
  printf("Fetched: %s\n", result);
  printf("Data Length: %d\n", (int)result_length);

  free(result);
  memcached_free(memc);
  return 0;
}
spymemcached(Java)
import net.spy.memcached.AddrUtil;
import net.spy.memcached.BinaryConnectionFactory;
import net.spy.memcached.MemcachedClient;

public class BinaryProtocolExample {
    public static void main(String[] args) {
        MemcachedClient memc = null;

        try {
            memc = new MemcachedClient(new BinaryConnectionFactory(),
                                       AddrUtil.getAddresses("localhost:11211"));
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        }   

        memc.set("some_key", 0, "this is a value");
        System.out.println(memc.get("some_key"));
        memc.shutdown();
    }   
}
pylibmc(Python)
#!/usr/bin/python

import sys
import pylibmc

memc = pylibmc.Client(["localhost:11211"])

if memc is None:
    print "failed to create client"
    exit(1)

memc.behaviors["binary_protocol"] = 1;

if memc.set("some_key", "this is a value") is None:
    print "failed to set record"
    exit(1)

print memc.get("some_key")
PECL memcached(PHP)
<?php
    $memc = new Memcached();

    if (!$memc) {
        echo("failed to instantiate memcached object\n");
        exit(1);
    }

    $memc->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
    $memc->addServer('localhost', 11211);

    if (!$memc->set('some_key', 'this is a value')) {
      echo("failed to set an item\n");
      exit(1);
    }

    echo($memc->get('some_key') . "\n");
?>

バイナリプロトコル仕様

プロトコルには「要求」「返答」という概念が存在し、プロトコル設計においてこれらの形式を明記する必要があります。memcachedのバイナリプロトコルではヘッダ(通信の内容や情報が含まれるデータ)がリクエスト用とレスポンス用に2種類存在します。どちらも24バイトの固定長ヘッダであり、固定長なゆえにmemcachedのバイナリプロトコルハンドラは高速な処理を施すことが可能です。

バイナリプロトコルの詳細な話に関しては以前執筆させていただいた、memcachedの削除メカニズムと今後の動向にて解説しましたので、ご興味のある方はそちらをご覧ください。

バイナリプロトコルとコネクション制約

memcachedは1つのコネクションに対して、テキストかバイナリの1つのプロトコルしか扱えないという制限があります。これはmemcachedがコネクションとプロトコルハンドラを内部的に固定するからです。したがって、1つのコネクションでテキストとバイナリプロトコルを両方処理する事はできません。

ライブラリ移行における運用面の注意点

memcachedのクライアントライブラリは世の中に多数存在しますが、今までアプリケーションで使ってきたクライアントライブラリを安易に変更してはいけません。その理由はクライアントライブラリの分散アルゴリズムが違った場合に多数のキャッシュミスが発生し、システムのインフラに多大な負荷をかけてしまうからです。

クライアントライブラリの移行を考えている場合は新しいライブラリの動作を調査し、必要に応じてサーバ群をウォームアップ(キャッシュサーバにデータを予め蓄積する)などの戦略を練る必要があります。

気になる性能

プロトコルの違いによるスループットの差をlibmemcached付属のmemslapというベンチマークツールで測定してみました。今回のベンチマークは10万レコードをキャッシュしているサーバに対して、全レコードを各々のスレッドが並走してGETするものです。クライアントとサーバは同じスイッチに繋がっている別々の物理サーバです。

並走度バイナリプロトコルテキストプロトコル
114.228秒14.367秒
215.047秒15.984秒
417.310秒17.794秒
822.073秒23.097秒
1623.783秒25.342秒

上記の結果を見る以上、バイナリプロトコルの方が若干スループットが高いという傾向が見られます。誤差にすら見える結果ですが、実運用で何千ものコネクションを同時に処理することを想定すれば、差は広がると仮定できます。もっと本格的な負荷をエミュレートしたいところですが、私の手持ちのハードウェアでは16以上の並走度は信頼性に欠けると判断し、検証は16スレッドまでとしました。

次回予告

今回はC言語、Java、Python、PHPを用いたバイナリプロトコルの使用例や簡単な性能検証を紹介しました。次回はmemcached 1.4から強化された統計システムや、新しく得られる統計情報を紹介いたします。

おすすめ記事

記事・ニュース一覧