最速KVS研究第3弾(memcached V.S. redis 第2弾)

memcachedとredisのKVS 速度対決をやってみて,やっぱり低遅延性というかレイテンシではmemcachedの勝ちという結果が出たのですが,
KVS最速テスト第二弾(memcached V.S. Redis) - なぜか数学者にはワイン好きが多い
逆に圧倒的にredisの方が速いという結果を提示しているブログがありました.
Redis vs Memcached | /sys/toilet

As you can see here, Redis came up short by a range of 20-26% the speed of memcache.
memcacheよりredisの方が2割ほど早いようだ.

ただし環境設定が結構違うので,単純にこちらのベンチと上記のベンチのどちらが正しいというのは言えないので,少し考えてみました.
違いは沢山あります.

  1. こちらはPocoライブラリ,上記ブログではC言語の専用APIであるCredisを使っている
  2. 記載はされていませんが,恐らくベンチマークに使ったサーバスペックが違う.こちらは6年以上も前のノートPC(Panasonic Let's note Y2)なので,恐らくこちらの方がスペックが低い.すなわち,サーバスペックが高ければredisが性能を発揮し,スペックが低い場合はmemcachedの方が有利になるという可能性が残される.
  3. こちらはkey, valueともに30文字前後という小規模キャッシュを想定しているのに比べ,上記のものはvalueが3バイトなど非常に極端にkeyやvalueの長さを変えている
  4. 上記のテストは複数のクライアントからアクセスが来る,通常のアプリケーションが遭遇すると思われるマルチコネクションのテストをしていない

そこで,こちら側で順次検証していきます.

おっと,その前に...
前回までの結果紹介で誤解を受けそうな点があったので注意を.
最速KVS研究第一弾(memcached/Tokyo Tyrant/Kyoto Tycoon) - なぜか数学者にはワイン好きが多い
KVS最速テスト第二弾(memcached V.S. Redis) - なぜか数学者にはワイン好きが多い
↑どちらもグラフで,縦軸のラベルが「set/秒」「get/秒」「set+get/秒」となっていて,一秒に何回のクエリをさばけるかの回数のように思えてしまうのですが,実際にはdefineしているMAXの回数だけさばくのに何秒かかったかという具合に単位が違います.毎秒の回数じゃなくて終了までかかった時間(秒)です.酔っぱらって書いてたもので深く気にしていませんでした.スミマセン.

さて,まずは,こちらの環境で,Pocoライブラリを使った場合とlibCredisを使った場合の比較をしてみます.
というわけでCredisをFreeBSDにインストールします.

> wget http://credis.googlecode.com/files/credis-0.2.3.tar.gz
> tar xvf credis-0.2.3.tar.gz
> cd credis-0.2.3
> ls
Makefile        README          credis-test.c   credis.c        credis.h
> make
"Makefile", line 7: Missing dependency operator
"Makefile", line 9: Need an operator
"Makefile", line 11: Need an operator
make: fatal errors encountered -- cannot continue

おっと,これはFreeBSDではmake=gmakeじゃないせいです.改めまして...

> gmake
cc -g -O2 -Wall   -c -o credis-test.o credis-test.c
credis-test.c: In function 'main':
credis-test.c:126: warning: format '%zu' expects type 'size_t', but argument 2 has type 'long unsigned int'
credis-test.c:135: warning: format '%zu' expects type 'size_t', but argument 2 has type 'long unsigned int'
credis-test.c:136: warning: format '%zu' expects type 'size_t', but argument 2 has type 'long unsigned int'
cc -c -fPIC -g -O2 -Wall  -o credis.o credis.c
In file included from credis.c:42:
/usr/include/netinet/tcp.h:40: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'tcp_seq'
/usr/include/netinet/tcp.h:50: error: expected specifier-qualifier-list before 'u_short'
/usr/include/netinet/tcp.h:175: error: expected specifier-qualifier-list before 'u_int8_t'
gmake: *** [credis.o] Error 1

あり?
あ,これはtcp.hの40行,

typedef u_int32_t tcp_seq;

↑こいつのせい.FreeBSDの場合は,先にtypes.hをincludeするか,credis.cの場合はincludeの順序を変えてtimes.hを先に持ってきてもOk.

/* credis.c -- a C client library for Redis
 *
 * Copyright (c) 2009-2010, Jonas Romfelt <jonas at romfelt dot se>
 * All rights reserved.
 *
(中略)
#ifdef WIN32
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_DEPRECATE
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h> // tcp.hが先
#include <sys/select.h>
#include <sys/time.h>    // time.hが後
#include <sys/socket.h>
#include <unistd.h>
#endif
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
#ifdef WIN32
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_DEPRECATE
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/time.h>     // ← 順番変更
#include <netinet/tcp.h>  // ← 順番変更
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>
#endif

これでコンパイルは通りますので,インストールもしちゃいます.

> gmake
cc -c -fPIC -g -O2 -Wall  -o credis.o credis.c
ar -cvq libcredis.a credis.o
a - credis.o
cc -g -O2 -Wall   -o credis-test credis-test.o libcredis.a
cc -shared -Wl,-soname,libcredis.so -o libcredis.so credis.o
> su
# cp -pv libcredis.so libcredis.a /usr/local/lib
libcredis.a -> /usr/local/lib/libcredis.a
libcredis.so -> /usr/local/lib/libcredis.so
# cp -pv credis.h /usr/local/include/
credis.h -> /usr/local/include/credis.h

このlibcredisを使ったベンチマークプログラムはこんな感じです.

#include <sstream>
#include <iostream>

using namespace std;
#include <vector>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <md5.h>
#include <omp.h>
#include "credis.h"

void set_key_val(MD5_CTX *con, int seed, char *key, char *val)
{
  sprintf(key,"stress-test-%010d",seed);
  sprintf(val,"%010d",seed);
  MD5Init(con);
  MD5Update(con, key, strlen(key));
  MD5End(con, key);
}
#define MAX 10000
#define BUFFER_SIZE 256
int main(int argc, char *argv[])
{
  int suc = 0, i;
  time_t start, set_end, get_end;
  int n = omp_get_max_threads();
  vector<REDIS > pool(n);
  for(vector<REDIS >::iterator i=pool.begin();
       i!=pool.end();++i)
  {
    *i = credis_connect(argv[1], 11211, 1000);
  }
  start = time(NULL);
#pragma omp parallel for shared(suc,i,pool)
  for(i=0;i<MAX;++i)
  {
    REDIS client=pool.at((int)(i/(MAX/n)));
    MD5_CTX con;
    char key[33], val[11];
    set_key_val(&con, i, key, val);
    int sent_len, recv_len;
    credis_ping(client);
    int rc = credis_set(client, key, val);
    // sent contents
    //cout<<key<<endl;
    // received status
    //cout<<rc<<endl;
    if (rc < 0)
    {
      cout<<"Error message: "<<credis_errorreply(client)<<endl;
    }else
    {
      ++suc;
    }
  }
  set_end = time(NULL);
#pragma omp parallel for
  for(i=0;i<MAX;++i)
  {
    REDIS client=pool.at((int)(i/(MAX/n)));
    MD5_CTX con;
    char key[33], val[11], *val_buffer;
    set_key_val(&con, i, key, val);
    int sent_len, recv_len;
    credis_ping(client);
    int rc = credis_get(client, key, &val_buffer);
    // get contents
    //cout<<val_buffer<<endl;
    // received status
    //cout<<rc<<endl;
    if (rc < 0 || strcmp(val, val_buffer)!=0)
    {
      cout<<"Error message: "<<credis_errorreply(client)<<endl;
      cout<<"returned value="<<val_buffer<<endl;
    }else
    {
      ++suc;
    }
    //free(val_buffer);
  }
  get_end = time(NULL);
  printf("try=%d success=%d ratio=%f\n",MAX,suc/2,(double)suc/2/MAX*100.0);
  printf("set=%d[sec], get=%d[sec], total=%d[sec]\n",
           set_end-start, get_end-set_end, get_end-start);
  return(0);
}

コンパイルは,pocoライブラリのリンクの代わりにCredisのリンクが入る感じです.

> c++ -fopenmp redisgetset-bench.cc -o redisgetset-bench -I/usr/local/include -L/usr/local/lib -lcredis -lmd

では,スレッド数を変えて,Redis with PocoとRedis with Credisの対決行きます.

まず,100万回Key+Valueのペアをsetするのにかかった時間.

次にget.


最後に,setとgetを合計した時間.

Credisの方が2倍ほど遅い...余りにも差が大きい原因が分からないので,まだまだ研究が必要そうです.
ネットワーク的には,ギガのハブを一個はさんでいるだけなので,memcachedとredisだったらかなりの接戦になると思ったのですが...