たごもりすメモ

コードとかその他の話とか。

Hadoop(libhdfs)各バージョンとscribeの微妙な関係

調べた内容を忘れそうなのでメモ。Hadoopのリリース元およびバージョンごとにあれこれとscribeから使えるかどうかの制約があるので、書き出してみる。

scribeから使えるHadoopのバージョン

Apacheリリース版 stable 0.20.2:ダメ

scribeをApacheリリース版の 0.20.2 (以下 0.20.2) の libhdfs/hdfs.h を参照しつつビルドしようとすると、以下のようなエラーが出る。

HdfsFile.cpp: In member function ‘void* HdfsFile::connectToPath(const char*)’:
HdfsFile.cpp:227: error: ‘hdfsConnectNewInstance’ was not declared in this scope
HdfsFile.cpp:255: error: ‘hdfsConnectNewInstance’ was not declared in this scope

これは以下のスレッドで議論されていた。
http://groups.google.com/group/scribe-server/browse_thread/thread/6a2133858789c5e0/6688ff95ccd85047

簡単に内容をまとめると「0.21 で採用されたAPIを使ってるから、0.21 もしくはAPIがバックポートされた Cloudera版のHadoopを使ってね」らしい。
確かに0.20.2の hdfs.h にこんな関数の定義はない。 0.21.0 にはある。

Apacheリリース版 unstable 0.21.0:ダメ

じゃあとApacheリリース版の 0.21.0 (以下 0.21.0) と組合せてビルドしようとすると、以下のようなエラーが出る。

/usr/local/hadoop/hdfs/src/c++/libhdfs/hdfs.h: In member function ‘virtual void HdfsFile::deleteFile()’:
/usr/local/hadoop/hdfs/src/c++/libhdfs/hdfs.h:283: error: too few arguments to function ‘int hdfsDelete(void*, const char*, int)’
HdfsFile.cpp:147: error: at this point in file

なぜだ、と思って hdfs.h を確認する。0.21.0 での定義はこんな。 hdfs/src/c++/libhdfs/hdfs.h 275行目付近

    int hdfsDelete(hdfsFS fs, const char* path, int recursive);

scribeの呼び出し側のコードはこんな。src/HdfsFile.cpp 145行目付近

void HdfsFile::deleteFile() {
  if (fileSys) {
    hdfsDelete(fileSys, filename.c_str());
  } 
  LOG_OPER("[hdfs] deleteFile %s", filename.c_str());
} 

なんで2引数での呼び出しなの! と思ったら 0.20.2 のコードを見てみると2引数で宣言されていた。src/c++/libhdfs/hdfs.h 269行目付近

    int hdfsDelete(hdfsFS fs, const char* path);

つまりこのAPIについては 0.20.2 のスタイルで呼び出している、と。ねえ、これ詰んでない?

Clouderaリリース版 CDH

で、CDHではそのあたりがうまく整合するようバックポートされているらしい。
というよりscribeはそもそもCDHだけをベースに書かれてて、Apache版は完全無視されているご様子。ひどす。

libhdfsが使用できるCDHのバージョン

CDH3 beta3 0.20.2+737:ダメ

ということで新しい方から試すぜ! とCDH3beta3の 0.20.2+737*1 を落としてきてscribeをビルドするとさくっと通る。ので起動してhdfsに接続させると、以下のようなエラーログが大量に吐かれて南無三。

[Wed Jan  5 20:42:19 2011] "[hdfs] ERROR: HDFS is not configured for file: hdfs://node01.hadoop-cluster:50071/scribe/HOGE/HOGE-2011-01-05_00000" 
[Wed Jan  5 20:42:19 2011] "[hdfs] Before hdfsConnectNewInstance(node01.hadoop-cluster, 50071)" 
could not find method newInstance from class org/apache/hadoop/fs/FileSystem with signature  (Ljava/net/URI;Lorg/apache/hadoop/conf/Configuration;Ljava/lang/String;)Lorg/apache/hadoop/fs/FileSystem;
Exception in thread "main" java.lang.NoSuchMethodError: newInstance
Call to org.apache.hadoop.fs.Filesystem::newInstance(URI, Configuration) failed!
[Wed Jan  5 20:42:19 2011] "[hdfs] After hdfsConnectNewInstance" 
[Wed Jan  5 20:42:19 2011] "[HOGE] Failed to open file <hdfs://node01.hadoop-cluster:50071/scribe/HOGE/HOGE-2011-01-05_00000> for writing" 
[Wed Jan  5 20:42:19 2011] "[HOGE] File failed to open FileStore::handleMessages()" 
[Wed Jan  5 20:42:19 2011] "[HOGE] WARNING: Re-queueing 42974 messages!"

エラーログがわかりにくい(JNIのせいだ)が、newInstance(URI, Configuration, String) を呼ぼうとして NoSuchMethodError になっているようだ。こういうエラーを眺めていると、静的言語だから云々とか言う人の顔をまじまじと3分くらい眺めてやりたい心境になる。
で、該当のコードはどこかなーと 0.20.2+737 から探してみる。まずは呼び出し側。src/c++/libhdfs/hdfs.c の361行目付近から。

    else if (!strcmp(host, "default") && port == 0) {
    /* 中略 */
      if (invokeMethod(env, &jVal, &jExc, STATIC, NULL,
                         HADOOP_FS, "newInstance",
                         JMETHOD3(JPARAM(JAVA_NET_URI),
                                  JPARAM(HADOOP_CONF),
                                  JPARAM(JAVA_STRING),
                                  JPARAM(HADOOP_FS)),
                         jURI, jConfiguration, jUserString) != 0) {
          errno = errnoFromException(jExc, env, "org.apache.hadoop.fs."
                                       "FileSystem::newInstance");
          goto done;                   
      }   
      jFS = jVal.l;
    } 
    else {
    /* 中略 */
        if (invokeMethod(env, &jVal, &jExc, STATIC, NULL, HADOOP_FS, "newInstance",
                         JMETHOD3(JPARAM(JAVA_NET_URI),
                                  JPARAM(HADOOP_CONF), JPARAM(JAVA_STRING),
                                  JPARAM(HADOOP_FS)),
                         jURI, jConfiguration, jUserString) != 0) {
            errno = errnoFromException(jExc, env, "org.apache.hadoop.fs."
                                       "Filesystem::newInstance(URI, Configuration)");
            goto done;                 
        }   
        
        jFS = jVal.l;
    }   

JNIがわかんなくても、なんとなくJavaのコードの "newInstance" を jURI, jConfiguration, jUserString を引数に呼んでるらしいのはわかる。invokeMethod関数を読めば呼び出し機構の詳細がわかるけど、ここでは割愛。
んでこれに対応する呼ばれるJava側のコード org.apache.hadoop.fs.Filesystem::newInstance を探す。src/core/org/apache/hadoop/fs/FileSystem.java の 230行目付近から。

  public static FileSystem newInstance(URI uri, Configuration conf) throws IOException {
    String scheme = uri.getScheme();
    String authority = uri.getAuthority();
  
    if (scheme == null) {                       // no scheme: use default FS
      return newInstance(conf);
    }
    
    if (authority == null) {                       // no authority
      URI defaultUri = getDefaultUri(conf);
      if (scheme.equals(defaultUri.getScheme())    // if scheme matches default
          && defaultUri.getAuthority() != null) {  // & default has authority
        return newInstance(defaultUri, conf);              // return default
      }
    }     
    return CACHE.getUnique(uri, conf);             
  }   
    
  /** Returns a unique configured filesystem implementation.
   * This always returns a new FileSystem object. */
  public static FileSystem newInstance(Configuration conf) throws IOException {
    return newInstance(getDefaultUri(conf), conf);
  }

1引数と2引数しかないwwwおまww
で、もしやと思って 0.21.0 のコードを確認してみると、3引数のものがあった。あーあ。libhdfsの側だけバックポートしてJavaのコードの側はバックポートし忘れてるらしい。何やってんの。ていうかテストしてねーんじゃん。信頼性を向上させるパッチとかどこの口のセリフですか。笑い死にさせる気か。

で、問題に目処がついてから調べてみたら、ひっかかった。
https://groups.google.com/a/cloudera.org/group/cdh-dev/browse_thread/thread/17d9544e762bf063
10月末に報告されてから2ヶ月以上放置かあ。ひどい話だね。

CDH3 beta2 0.20.2+320:大丈夫!

ということで CDH3 beta2 リリースの 0.20.2+320 を眺めてみると、以下のような感じ。src/c++/libhdfs/hdfs.c の499行目付近より。

    else if (!strcmp(host, "default") && port == 0) {
        //fs = FileSystem::get(conf); 
        if (invokeMethod(env, &jVal, &jExc, STATIC, NULL,
                         HADOOP_FS, "newInstance",
                         JMETHOD1(JPARAM(HADOOP_CONF),
                                  JPARAM(HADOOP_FS)),
                         jConfiguration) != 0) {
            errno = errnoFromException(jExc, env, "org.apache.hadoop.fs."
                                       "FileSystem::newInstance");
            goto done;
        }
        jFS = jVal.l;
    }   
    else {
        // fs = FileSystem::newInstance(URI, conf);
    /* 中略 */
        if (invokeMethod(env, &jVal, &jExc, STATIC, NULL, HADOOP_FS, "newInstance",
                         JMETHOD2(JPARAM(JAVA_NET_URI),
                                  JPARAM(HADOOP_CONF), JPARAM(HADOOP_FS)),
                         jURI, jConfiguration) != 0) {
            errno = errnoFromException(jExc, env, "org.apache.hadoop.fs."
                                       "Filesystem::newInstance(URI, Configuration)");
            goto done;
        }
        
        jFS = jVal.l;    
    } 

わーい2引数呼び出しだよヤッタネー。ということでビルドして動作させてみたら動きました! おつかれさま!

*1:この+NってのはClouderaが当てたパッチの数らしい