Redisのクローンを作ろうとして作らなかった

Posted by hiroki.kanaの日常 on December 17, 2017
このエントリーをはてなブックマークに追加

この記事は ドワンゴ Advent Calendarの18日目です。

はじめに

今年もAdvent Calendarの季節となりました。もはやこの時期しかまとまった文章を書かなくなり、もともと無い文章表現力が皆無に近くなり危機感を感じています。昨年は確かストレージの容量が無くて困った話でしたね。あの件は追加で20TBぐらいのストレージを買って解決しました。本当に良かった。

さて、今年はどのようなことをやったかと言うとRedisクローンを作ろうとしました。結論としては全くできてないのですが、ここまでで得られた知見をみなさんに披露したいと思います。

きっかけと動機

私はよく安いからというだけで何かを買ったりします。そのような経緯から安くなったKindle本をよく買います。ちなみに読書のスピードは絶望的に遅く、多くの本は積読となります。その多くの本の中から「ガベージコレクション 自動的メモリ管理を構成する理論と実装」という本をそれとなく読み進めていました。この本を読む中で気づいたことがあります。

  • 読んだところによるとC言語はガベージコレクションを言語でサポートしていないらしい
  • そういえばRedisはANSI Cで書かれていると書いてあった気がする
  • ガベージコレクションをサポートした言語で書き直せばRedisの概要もソースコードで把握できるし何か面白そうではないか

このようなきっかけから私はRedisをなんらかの言語で書き直し、その過程で新たな言語の学習とRedisのソースコードの読み方を習得することを目的として動き出したわけです。そういえば私はGoに興味がありつつもなかなか手をつけることができずにいました。これはちょうど良いきっかけであると感じGoで書き直してみようと思ったわけです。

Redisを理解する

まずはRedisとはどこにソースがあり、どのような構造になっているのかというのを理解する必要があろうと思いました。

公式ページをみるとどうやらGithubでソースが管理されているようでしたのでおもむろにForkをしました。そのうえで標準的な内容でコンパイルし、動作することを確認しました。今回は動作確認ができれば良いので make install は行いませんでした。

$ make
$ src/redis-server #Redisが起動することを確認
$ src/redis-cli #起動したRedisにアクセスできることを確認

はまることもなく動作確認することができました。

しかしながらこれだけではどこにどのような処理が存在しているのかすらわかりませんでした。わからなくなったら多くの場合はREADMEに何かのヒントがあります。RedisのREADMEを読み進めてみると「Redis internals」というまさに探していそうな内容の記述があります。様々な内容が書かれていたのですが、ここを読んで私が理解したのは

  • src ディレクトリ以下にRedisの実装がある
  • server.c がRedisサーバーのエントリーポイントとなっている
  • 何か処理をする新たなRedisコマンドを作るのが入門に良さそう

以上の3点です。私の目標は新しいRedisコマンドを作るということになりました。

独自コマンドを考える

もう私はこの時点でGoのことは完全に頭から離れています。なぜなら、現在の理解度とやろうとしていること、そして期限を見て当初の目的を果たすのは無理だろうと思い、一旦Goのことは忘れようと思ったわけです。

さて、では独自コマンドとはどのようなものが良いだろうかと考え始めました。まず、RedisとはKVSであるわけですが、であれば格納されているデータを何か加工する処理等を追加するのが深い理解に役立つだろうと思いました。しかし、私の今の理解状況から見てRedisが保持しているデータを加工等するのは少々ハードルが高かろうと諦めました。何か読み込んでそのまま返せるようなデータをただただ返すというネタが無いかと探しました。

やはりパッと思いつくのはサーバーの時間でしょう。「時間返すだけならすぐにできるだろう」と私は余裕を持ち始め、その後3日程度作業を進めるのを怠りました。さてゆっくり休んだところで、実装しようとする前に「もしかして既に同じ実装があったらヤバいな…」と思い、念のためコマンドリファレンスを確認しました。そして私の目に飛び込んできたのはこのページでした。そう、私が考えるようなことなんて大体誰かが考えて実装しているわけです。

焦りだした私は別の情報を取得して返せないかと考え、サーバーのアーキテクチャを返すのはどうだろうと考えました。 調べたところこのようなページを見つけ、なるほどこのようにして取得することができるのかと理解しました。さて実装しようとする前に、もしかしてまた実装されているのではないかと思い今度は server.cutsname で検索をかけました。…そう、私が考えるようなことなんて大体誰かが考えて実装しているわけです。

ここまではもしかしたら役に立つかもしれないコマンドを追加してみようという気持ちで考えてきたわけですが、役立ちそうなことで私が思いつくようなことは大体誰かが考えて実装しているわけです。ここは学習目的でありますし、役に立つかもしれないあわよくば本流にマージしてもらえるかもというような欲を捨てることにしました。そこで私が辿りついたのがサーバーのCPUの名前を返すというコマンドでした。

CPU名の取得方法

Linuxでは /proc/cpuinfo 見ればCPU名の取得ができますし、まあきっと簡単に取得できるだろうと思ったわけですが、それは私の浅慮であったということが割とすぐにわかりました。動作確認はMac OSでやっていたわけですが /proc/cpuinfo は存在しません。他の方法が必要なんだなということで探し、参考にしたのはこちらのページなのですがアセンブラのCPUID命令を使えば取得できる、インラインアセンブラという仕組みがあるということを理解しました。

いきなりRedisに組み込む前にまずは単体で動かしてみようと書いたコードが下記です。

#include <stdio.h>

#include <string.h>


char buf[4*4];

char *cpuid(int op) {
  int eax, ebx, ecx, edx;
  __asm__("cpuid"
          : "=a" (eax),
          "=b" (ebx),
          "=c" (ecx),
          "=d" (edx)
          :"0" (op));
  memcpy(buf, (char*)(&eax), 4);
  memcpy(buf+4, (char*)(&ebx), 4);
  memcpy(buf+8, (char*)(&ecx), 4);
  memcpy(buf+12, (char*)(&edx), 4);
  buf[15] = '\0';
  return buf;
}

int main(void) {
  char s1[16*3] = "";
  strcat(s1, cpuid(0x80000002));
  strcat(s1, cpuid(0x80000003));
  strcat(s1, cpuid(0x80000004));
  printf("%s", s1);
}

これを見ていただければ私のCの理解度をわかっていただけると思います。これを実行してみると下記のようになります。

$ gcc cpuid.c
$ ./a.out
Intel(R) Xeon(R CPU           5570  @ 2.93GHz%

良さそうですね。

独自コマンドとして処理を追加する

それでは独自コマンドとして実装してみます。今回は下記のようにしました。

  • コマンド名:cpuinfo
  • 引数:無し
  • 戻り値:CPU名

まずはRedisではコマンドの定義を redisCommandTable に追加することで、実際に処理をコマンドで呼び出すことができるようになります。今回は下記のように追加しました。

{"cpuinfo",cpuinfoCommand,-1,"rF",0,NULL,0,0,0,0,0}

それぞれの役割はあるのですが、一旦は使いそうな先頭から下記の4つだけを理解しました。

  • コマンド名
  • コマンドを定義した関数
  • 引数の数
  • コマンドフラグ(後述)

コマンドフラグはいくつかあるのですが、今回していした rF について説明します。

  • r:読み込みコマンド
  • F:高速に処理を完了させることができるコマンド

コマンドフラグや引数の意味、実際の効果についてもっときちんと説明をしたかったのですが、私の今の理解力では正しく説明ができる自信が無いので省略します。詳細な説明はソースコードにあるので、私でなければそれほど多くの時間もかからず理解できるのではないかと思っています。

さて、コマンドの定義を redisCommandTable に追加したところで具体的な処理である cpuinfoCommandを実装していきます。

void getCpuBrandName(int op, char* brandname) {
  int eax, ebx, ecx, edx;
  char buf[16];
  __asm__("cpuid"
          : "=a" (eax),
            "=b" (ebx),
            "=c" (ecx),
            "=d" (edx)
          :"0" (op));
  memcpy(buf, (char*)(&eax), 4);
  memcpy(buf+4, (char*)(&ebx), 4);
  memcpy(buf+8, (char*)(&ecx), 4);
  memcpy(buf+12, (char*)(&edx), 4);
  buf[15] = '\0';
  strcat(brandname, buf);
}
 
void cpuinfoCommand(client *c) {
  char brandname[16*3] = "";
  getCpuBrandName(0x80000002, brandname);
  getCpuBrandName(0x80000003, brandname);
  getCpuBrandName(0x80000004, brandname);
  addReplyBulkCBuffer(c, brandname, strlen(brandname));
}

ここで重要なポイントは コマンドの引数にはクライアントを取るということと、そのクライアントにレスポンスを返すということです。CPU名を取得する部分については前述の内容を少し変えたものにしていますが処理は同じです。ここで作った文字列を addReplyBulkCBuffer という関数でクライアントに送信しています。 addReplyBulkCBuffer は network.c に実装があり、文字列を返す際に利用される処理です。

ここまでで実装は終了です。最後にコンパイルし実際に利用してみます。

$ make
$ ./src/redis-server
$ ./src/redis-cli
127.0.0.1:6379> cpuinfo
"Intel(R) Xeon(R CPU           5570  @ 2.93GHz"

きちんとCPU名が返ってくるようになりました。

まとめ

さて、ようやくRedisの基本的な処理の詳細を見ていけるようになったのではないかというところで今日という日が来てしまいました。当初のRedisのクローンをGoで作りたいという目標は達成することができませんでした。その前段階であるRedisの理解を深めるという部分でも、あまり深まったとは言えないのが正直なところです。しかしながら、このように誰かが書いたコードを読み始める第1歩で大事なことは意図した場所に意図した処理を差し込めるようになることであると思っています。そのような観点から見ればここまでのことも無駄ではなかったかなと思っています。

Redisとはきっと今後もどこかで付き合っていくことになりそうではあるので、今回のことをきっかけに理解を深めていくというのを来年の目標にしつつ2017年を終えようと思います。

参考ページ

このエントリーをはてなブックマークに追加