恐れ多くもtokyo tyrantのソースに文句をつけてみる。

とあるシステムで tokyo tyrant を導入するべく、負荷テストをやってもらっている。
で、負荷テストで.tct ファイルで双方向レプリケーションしていた tokyo tyrant に 負荷をかけていたら応答を返さなくなったらしい。
接続は pecl php tokyo tyrant経由 で tokyo tyrant に接続し、php(httpd) に負荷をかけて計測していた。
そしたら、 php の session error が表示されてしまったらしい。
#session_handler = tokyo_tyrant でやってます。

ただ、そうなったのは、一回だけで、それ以降はどんなに負荷をかけて再現しなかったとのこと。
復旧は とりあえず tokyo tyrant を再起動したら直ったそうだ。

うーん。不安だ。
まだ構築段階だからいいけど、なんかサービスインが怖い気がする。
運が悪かったで済む問題なんだろうか。

このままでは不安で困るので、トラブルが起きた時刻に何がおきたか tokyo tyrant のログを見てみる。

ログには、

ERROR: do_slave: tcreplopen failed

と、書かれていた。

むむ。

レプリケーションかー。もしかしたら双方向レプリケーションがよくないのかもしれない。
次はそれをoffにして負荷検証をする予定だ。

一応いろいろ調べてみる。

まずは google 検索。
ERROR: do_slave: tcreplopen failed で調べてもあんまり事例は出ない。うーん。

こうなったら仕方ないので、エラーの内容で tokyo tyrant のソースを検索してみる。
#こーゆーのが簡単にできるところはオープンソースの素晴らしいところだなぁ。

/* replicate master data */
static void do_slave(void *opq){
  REPLARG *arg = opq;
  TCADB *adb = arg->adb;
  TCULOG *ulog = arg->ulog;
  uint32_t sid = arg->sid;
  if(arg->fatal) return;
  if(arg->host[0] == '\0' || arg->port < 1) return;
  if(arg->mts > 0){
    char rtsbuf[NUMBUFSIZ];
    int len = sprintf(rtsbuf, "%llu\n", (unsigned long long)arg->mts);
    if(!tcwritefile(arg->rtspath, rtsbuf, len))
      ttservlog(g_serv, TTLOGERROR, "do_slave: tcwritefile failed");
    arg->mts = 0;
  }
  int rtsfd = open(arg->rtspath, O_RDWR | O_CREAT, 00644);
  if(rtsfd == -1){
    ttservlog(g_serv, TTLOGERROR, "do_slave: open failed");
    return;
  }
  struct stat sbuf;
  if(fstat(rtsfd, &sbuf) == -1){
    ttservlog(g_serv, TTLOGERROR, "do_slave: stat failed");
    close(rtsfd);
    return;
  }
  char rtsbuf[NUMBUFSIZ];
  memset(rtsbuf, 0, NUMBUFSIZ);
  arg->rts = 0;
  if(sbuf.st_size > 0 && tcread(rtsfd, rtsbuf, tclmin(NUMBUFSIZ - 1, sbuf.st_size)))
    arg->rts = tcatoi(rtsbuf);
  TCREPL *repl = tcreplnew();
  pthread_cleanup_push((void (*)(void *))tcrepldel, repl);
  if(tcreplopen(repl, arg->host, arg->port, arg->rts + 1, sid)){      ←この関数が失敗するらしい。
    ttservlog(g_serv, TTLOGINFO, "replicating from sid=%u (%s:%d) after %llu",
              repl->mid, arg->host, arg->port, (unsigned long long)arg->rts);
    arg->fail = false;
    arg->recon = false;
    bool err = false;
    uint32_t rsid;
    const char *rbuf;
    int rsiz;
    uint64_t rts;
    while(!err && !ttserviskilled(g_serv) && !arg->recon &&
          (rbuf = tcreplread(repl, &rsiz, &rts, &rsid)) != NULL){
      if(rsiz < 1) continue;
      bool cc;
      if(!tculogadbredo(adb, rbuf, rsiz, ulog, rsid, repl->mid, &cc)){
        err = true;
        ttservlog(g_serv, TTLOGERROR, "do_slave: tculogadbredo failed");
      } else if(!cc){
        if(arg->opts & RDBROCHKCON){
          err = true;
          arg->fatal = true;
          ttservlog(g_serv, TTLOGERROR, "do_slave: detected inconsistency");
        } else {
          ttservlog(g_serv, TTLOGINFO, "do_slave: detected inconsistency");
        }
      }
      if(lseek(rtsfd, 0, SEEK_SET) != -1){
        int len = sprintf(rtsbuf, "%llu\n", (unsigned long long)rts);
        if(tcwrite(rtsfd, rtsbuf, len)){
          arg->rts = rts;
        } else {
          err = true;
          ttservlog(g_serv, TTLOGERROR, "do_slave: tcwrite failed");
        }
      } else {
        err = true;
        ttservlog(g_serv, TTLOGERROR, "do_slave: lseek failed");
      }
    }
    tcreplclose(repl);
    ttservlog(g_serv, TTLOGINFO, "replication finished");
  } else {
    if(!arg->fail) ttservlog(g_serv, TTLOGERROR, "do_slave: tcreplopen failed");   ←ここ
    arg->fail = true;
  }
  pthread_cleanup_pop(1);
  if(close(rtsfd) == -1) ttservlog(g_serv, TTLOGERROR, "do_slave: close failed");
}

あった。
tcreplopen関数が 失敗したときの else 句で arg->fail が false の時にこのメッセージが表示されるらしい。

なるほど。
これで、どこで問題が発生したかはよくわかった。
しかし、これでは、なぜ問題が発生したのかさっはりわからん。


失敗した tcreplopen関数の実装を見てみる。

/* Open a replication object. */
bool tcreplopen(TCREPL *repl, const char *host, int port, uint64_t ts, uint32_t sid){
  assert(repl && host && port >= 0);
  if(repl->fd >= 0) return false;
  if(ts < 1) ts = 1;
  if(sid < 1) sid = INT_MAX;
  char addr[TTADDRBUFSIZ];
  if(!ttgethostaddr(host, addr)) return false;
  int fd = ttopensock(addr, port);
  if(fd == -1) return false;
  unsigned char buf[TTIOBUFSIZ];
  unsigned char *wp = buf;
  *(wp++) = TTMAGICNUM;
  *(wp++) = TTCMDREPL;
  uint64_t llnum = TTHTONLL(ts);
  memcpy(wp, &llnum, sizeof(llnum));
  wp += sizeof(llnum);
  uint32_t lnum = TTHTONL(sid);
  memcpy(wp, &lnum, sizeof(lnum));
  wp += sizeof(lnum);
  repl->fd = fd;
  repl->sock = ttsocknew(fd);
  repl->rbuf = tcmalloc(TTIOBUFSIZ);
  repl->rsiz = TTIOBUFSIZ;
  if(!ttsocksend(repl->sock, buf, wp - buf)){
    tcreplclose(repl);
    return false;
  }
  repl->mid = ttsockgetint32(repl->sock);
  if(ttsockcheckend(repl->sock) || repl->mid < 1){
    tcreplclose(repl);
    return false;
  }
  return true;
}

これは。。。

えーと、なんというか、これだと、どこの return false で抜けたのかさっぱり分からないぞ。

なんという無口なプログラムだ。
そして、みごとにエラーを握りつぶしてくれている。
無口クールにエラーを握り潰すなんてひどいや。


tokyo tyrant は、すぐれたソフトだし、速度も申し分ないけど、ここまで無口だと障害時の調査が困る。
もうちょっとおしゃべりなプログラムにできないものだろうか。

tokyo tyrantソースコードをいろいろ眺めてみると、エラーをログに出力をちゃんとやっているところもあれば、 return false ;で抜けているだけのところもある。
高速化したい部分や、下位レイヤーは return false;で無口になって、上位レイヤーになればなるほど log を吐くようにしているようにも思える。
ポリシーの問題だろうか?

私は保守運用もしなくてはいけないから言わしてもらえば、すべてのエラーはなぜ発生したのかをログに残していてほしい。
そうでないと、エラーが起きたときの原因解決が高く付いて困ってしまう。

たとえば、fopenが失敗したとしても、なぜ失敗したのかわからないと原因究明が難しい。
fopenが失敗する理由としては、一番おこりやすいファイルがない系エラーからディスクリプタ不足ぐらいまで幅広い原因が考えられる。
このどれがエラーを引き起こしているか、なぜダメなのかがわからないと困るのだ。

エラートラップについて考えてみる。

ついでなので、エラートラップについて考えてみる。
一番うれしいのが、 C言語の errno や windows系の GetLastError のようにその関数がなぜ失敗したのか教えてくれる機能だ。
これとどこで問題が起きたのかが分かれば、なぜ問題がおきたのか大体分かる。

if ( ! API() )
{//エラー
	DWORD err = GetLastError();
	logger("API呼び出し失敗 エラー %d",err);
}


その次にうれしいのは、 戻り値や引数でエラー番号を返してくれる実装だ。

DWORD ret = API();
if (ret <= 0)
{//エラー
	logger("API呼び出し失敗 エラー %d",err);
}

//あんまりこのタイプを使う人はいないけどさw
DWORD ret;
if (! API(&ret))
{//エラー
	logger("API呼び出し失敗 エラー %d",err);
}


そのまた次にうれしいのは、例外などでエラーが発生すれば上位層にエラーを連番してくれる仕組みだ。
または、これと同じぐらいうれしいのが、エラーをログファイルに書いてくれる実装だ。
前者はプログラムでもハンドリングできるし、後者は人間が見れば内容が分かる。
ただし、両方とも副作用の欠点がある。例外はハンドルが面倒だったり、忘れたりすると悲惨な結果を生むし、ログは人間が見ないと意味がない。
ともかく何がおきたのかを知ることはできる。


次は、現在の tokyo tyrant のように エラーがあったら return false するプログラムだ。
これは、エラーが起きたことは分かるが、なぜエラーが起きたのかさっぱり分からずに、サポートや保守で死ねる。


サイテーなのが、エラー処理ってなんですか?食えるんですかってプログラム。
これは論外。


そんなわけでエラーが発生したら、それを握りつぶさないでなぜエラーが発生したのか教えてほしいんです。
そうしないと原因追求できやしないんよ。