RubyのCassandraクライアントで指定したIPに接続できない怪現象。

rubyのcassandraクライアントから別サーバのcassandraに接続する場合に、指定したIPに接続されずローカルホストに接続してしまう怪現象が発生してハマったのでメモ。
(正直、Forkwellのパブリケーションに追加したい衝動に駆られて書きましたw)

環境


○サーバ1
・cassandra 1.0.8
  ip: 192.168.0.11
  keyspace: ExampleKeyspace
  column family: ExampleCF
  ※optionの指定なしで作成

○サーバ2
ruby1.9.3
  ・cassandra-0.12.1
  ・thrift-0.7.0
  ・thrift_client-0.7.1

現象確認

まず、サーバ2からサーバ1のcassandraにcassandra-cliから接続できることを確認。

$ cassandra-cli -h192.168.0.11 -kExampleKeyspace
Connected to: "Test Cluster" on 192.168.0.11/9160
Welcome to Cassandra CLI version 1.0.8

Type 'help;' or '?' for help.
Type 'quit;' or 'exit;' to quit.

[default@ExampleKeyspace]


問題なし。

次は、Rubyから接続してみる。

こんなスクリプトを用意して、

require 'rubygems'
require 'cassandra'

client = Cassandra.new('ExampleKeyspace', '192.168.0.11:9160')
client.insert('ExampleCF', SimpleUUID::UUID.new.to_s, {})


スクリプトを実行。

$ ruby insert.rb
/usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/thrift-0.7.0/lib/thrift/transport/socket.rb:53:in `rescue in open': CassandraThrift::Cassandra::Client::TransportException
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/thrift-0.7.0/lib/thrift/transport/socket.rb:36:in `open'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/thrift-0.7.0/lib/thrift/transport/framed_transport.rb:37:in `open'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/thrift_client-0.7.1/lib/thrift_client/connection/socket.rb:11:in `connect!'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/thrift_client-0.7.1/lib/thrift_client/abstract_thrift_client.rb:105:in `connect!'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/thrift_client-0.7.1/lib/thrift_client/abstract_thrift_client.rb:144:in `handled_proxy'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/thrift_client-0.7.1/lib/thrift_client/abstract_thrift_client.rb:60:in `describe_keyspace'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/cassandra-0.12.1/lib/cassandra/cassandra.rb:177:in `schema'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/cassandra-0.12.1/lib/cassandra/columns.rb:31:in `column_family_property'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/cassandra-0.12.1/lib/cassandra/columns.rb:20:in `column_name_class_for_key'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/cassandra-0.12.1/lib/cassandra/columns.rb:12:in `column_name_class'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/cassandra-0.12.1/lib/cassandra/helpers.rb:21:in `extract_and_validate_params'
from /usr/local/ruby-1.9.3-p125/lib/ruby/gems/1.9.1/gems/cassandra-0.12.1/lib/cassandra/cassandra.rb:442:in `insert'
from test.rb:6:in `
'

エラー発生。

原因


ソースを追ってみると、cassandraクライアントのデフォルトの挙動は、インスタンス生成時に指定したサーバに書き込みに行くのではなく、
指定したサーバのクラスタリング情報を読み取り、リング上のIP1つに対して書き込みに行く模様。

サーバ1からcassandraのリング情報を取得してみる。

$ nodetool -hlocalhost ring
Address DC Rack Status State Load Owns Token
127.0.0.1 datacenter1 rack1 Up Normal 752.7 KB 100.00% 17515755121036504136650759168839277356

つまり、リング上のIPが127.0.0.1になっているから、サーバ2から127.0.0.1へ書き込みにいっているということっぽい。
サーバ2にはcassandraが立ち上がっていないため、ネットワークエラーになる。

対象ソース:

cassandra-0.12.1/lib/cassandra/cassandra.rb

1052 def all_nodes
1053 if @auto_discover_nodes && !@keyspace.eql?("system")
1054 temp_client = new_client
1055 begin
1056 ips = (temp_client.describe_ring(@keyspace).map {|range| range.endpoints}).flatten.uniq
1057 port = @servers.first.split(':').last
1058 ips.map{|ip| "#{ip}:#{port}" }
1059 ensure
1060 temp_client.disconnect!
1061 end
1062 else
1063 @servers
1064 end
1065 end

対策


解決策としては、ぱっと2つ思いつく。
1. リング情報にcassandraサーバのプライベートIPを設定する。
2. リング情報ではなく指定したサーバ情報にアクセスするように設定する。

対策1はクラスタリングへのアクセスをCassandraに任せる場合、対策2はサーバ2とCassandraの間に別のロードバランサーなどを挟む場合かな?
(2の場合Consistency LevelをQUORUMとかALLにしたらどうなるんだろう?試してない。)

解決策1:
cassandra.yamlの設定を変更。


175 # Address to bind to and tell other Cassandra nodes to connect to. You
176 # _must_ change this if you want multiple nodes to be able to
177 # communicate!
178 #
179 # Leaving it blank leaves it up to InetAddress.getLocalHost(). This
180 # will always do the Right Thing *if* the node is properly configured
181 # (hostname, name resolution, etc), and the Right Thing is to use the
182 # address associated with the hostname (it might not be).
183 #
184 # Setting this to 0.0.0.0 is always wrong.
185 listen_address: 192.168.0.11 # ローカルIPを指定

再起動してリング上のIPを確認。
$ nodetool -h localhost ring
Address DC Rack Status State Load Owns Token
192.168.0.11 datacenter1 rack1 Up Normal 765.67 KB 100.00% 17515755121036504136650759168839277356

解決策2:
cassandraクライアントのdisable_node_auto_discovery!メソッドを呼べば、
インスタンス生成時に指定したIPのcassandraサーバに直接接続するようになる。
require 'rubygems'
require 'cassandra'

client = Cassandra.new('ExampleKeyspace', '192.168.0.11:9160')
client.disable_node_auto_discovery! # ここ
client.insert('ExampleCF', SimpleUUID::UUID.new.to_s, {})