http://rubyamqp.info/articles/error_handling/ の和訳。脚注は訳者による。
エラーの取扱いと復旧
このガイドについて
送信側であれ受信側であれ、多岐に渡る異常系をどうエレガントに扱うかが、AMQPと関わりのあるアプリケーションを頑健にしていくうえでは不可避と言えましょう。プロトコルの誤り、ネットワークの不調、ブローカー1の異常などが思い浮かぶことでしょう。これらを正しく処理して上手に正常状態に回復することは、容易ではないでしょう。以下では、amqp gemを使うことでアプリケーションが
- ブローカーとの接続ができなかったとき
- ネットワークが切断されたとき
- AMQPコネクションに例外2が発生したとき
- AMQPチャンネル3に例外が発生したとき
- ブローカーの異常に遭遇したとき
- TLS(SSL)関連の障害があったとき
のような状況をどのように切り抜けることができるか、のみならず
- ネットワーク切断からの復旧をどうするか
- 自動回復モードとは何か、それはいつ使うべきで、いつ使うべきでないか
を解説いたしましょう。
この 作品 は クリエイティブ・コモンズ 表示 3.0 非移植 ライセンスの下に提供されています4。原文のソースはGithubから入手可能でございます。
対象とするバージョン
このガイドは Ruby amqp gem のバージョン v0.8.0RC14 以降が対象でございます。
サンプルプログラムについて
私達の git レポジトリには、この件に関連したサンプルプログラムがいくつかございます。もしも新しいサンプルをお書きになった方がおられましたら、この一覧に加えたく思いますので、ご連絡いただけますでしょうか。
ブローカーとの接続失敗について
アプリケーションは当然にブローカーと接続するわけですが、このときの接続失敗をどうにかして取り回すことが必要でございましょう。信頼性のあるネットワークなどというものは幻想でございましょう。ChefやPuppetといった現代的なシステム設定ツールを用いても設定ファイルの書き間違いは防げませんでしょうし、ブローカーのプロセスは何かの弾みで落ちることもございましょう。これらの状況はできるだけ速やかに検知される必要がございましょう。
私達はTCP層の接続障害を検知するための方法を二種類ご用意さしあげております。ひとつめは、Rubyレベル例外を捕捉することです:
begin
AMQP.start(connection_settings) do |connection, open_ok|
raise "This should not be reachable"
end
rescue AMQP::TCPConnectionFailed => e
puts "Caught AMQP::TCPConnectionFailed => TCP connection failed, as expected."
end
上記を含んだプログラム一式だとこうなります:
AMQP.connect (及び AMQP.start) においては、接続に失敗すると AMQP::TCPConnectionFailed 例外を raise いたします。これを rescue していただきますと、アプリケーションは例えばログを吐いたりとか、再接続を試みたりといったことが可能になりますことでしょう。
ところでブローカーへの接続に失敗した時というのは、ようするにネットワーク障害だとか、設定ファイルの書き間違いだとか、そういった理由であることが大半と思われます。このような状況下におきまして、安易に再接続を試みるというのは、いかがなものでしょうか? 同じエンドポイントに対して再接続を試みても同じエラーが何度も起きるだけである蓋然性が高うございましょう。(フェイルオーバーとクラスタ構成にかんして後で書く5)
障害検知のその他の方法としてはエラーバック(特定のエラーを処理するためのコールバック)によるものがございます:
handler = Proc.new { |settings| puts "Failed to connect, as expected"; EventMachine.stop }
connection_settings = {
:port => 9689,
:vhost => "/amq_client_testbed",
:user => "amq_client_gem",
:password => "amq_client_gem_password",
:timeout => 0.3,
:on_tcp_connection_failure => handler
}
上記を含んだプログラム一式だとこうなります:
:on_tcp_connection_failure には #call メッセージに反応する任意のオブジェクトを指定していただけます。
もしも、サンプルプログラムのようにトップレベルから接続を張るのではなくて、クラスの中から接続しにいくことを選択された場合、 Object#method でインスタンスメソッドをオブジェクト化して渡していただけるようになってございます(例をあとで書く)。
認証失敗について
接続に失敗するその他の理由としては認証に失敗することがあげられましょう。認証の失敗に関しましては接続の失敗とおおむね同様に処理していただけます:
デフォルトハンドラ
このエラーバックが省略された場合には、認証に失敗すると AMQP::PossibleAuthenticationFailureErrorをraiseいたします。
この例外名に含まれる “possible” に一抹の不安がよぎる方もおられましょう。これには理由がございます。AMQP 0.9.1 プロトコル仕様によりますと、ブローカーの実装は、AMQPコネクションが確立する以前に発生した例外的状況(認証の失敗もこれに含まれましょう)においては、なんらの応答を返すことなく、単にTCPのセッションをシャットダウンせよと規定されているのです。
ただ、実のところ、TCPセッション・ハンドシェイクが終了してから、AMQPコネクションが確立するまでに、どのような状況が起こりうるかを考えますと、認証の失敗以外にないのではと存じております。
ネットワーク切断のとりまわし
小規模なプロダクト、小規模なプロジェクトであっても、こんにちにあっては複数のアプリケーションで構成されているのはあたりまえでございますし、それらが複数のマシンにまたがっているのも、日常的に見かける景色と言えましょう。現代のソフトウエア・システムにおいては、ネットワークに発生する問題は悪夢と言わざるを得ません。Ruby amqp gemでは、それらのTCPコネクションの問題を AMQP::Session#on_tcp_connection_loss で登録したブロックで処理していただくことができるようになっております。このブロックはTCPコネクションに問題が発生した時に、コネクションのオブジェクトと、それまでのコネクションの接続設定を引数にとりながら呼ばれます。
connection.on_tcp_connection_loss do |connection, settings|
# reconnect in 10 seconds, without enforcement
connection.reconnect(false, 10)
end
アプリケーションの作りによってはAMQP::Session以外の構成要素にてネットワーク切断への対処をしたい場合もございましょう。かような要求をかんがみまして、amqp gem 0.8.0以降では他にも多数のハンドラを用意しております。これらのハンドラを総称して私達は「シャットダウン・プロトコル」と呼んでおります(ここでいうプロトコルは日本語でいえば「約束」くらいの意味でございます。ネットワーク・プロトコルではございません)。
シャットダウン・プロトコルに対応しているのはAMQP::Session, AMQP::Channel, AMQP::Exchange, AMQP::Queue, AMQP::Consumerですのでこれらはすべて同様に取り扱っていただくことができます。また AMQP::SessionとAMQP::Channelに関しましては追加でいくつかのハンドラがございます。
シャットダウン・プロトコルはおおむね以下の二つの事象を扱います:
- ネットワークの切断
- ブローカーからのコネクションの切断
前者に関して注目してご説明致しましょう。ネットワークが切断した際には、まず下位層がそれを検知して AMQP::Sessionのコードを起動します。その結果、ネットワーク切断の事象は上位の AMQP::Channelへ伝播します。AMQP::Channelは事象をさらにAMQP::ExchangeやAMQP::Queueに伝播させて、AMQP::Queueは(あれば)AMQP::Consumerに伝播させます。このようにして、セッションに参加しているすべてのオブジェクトはネットワークの切断を知ることができ、アプリケーションが指定したコールバックを実行することができるようになっております。
AMQP::Session6 のシャットダウン・プロトコル
AMQP::Session#on_tcp_connection_loss
AMQP::Session#on_connection_interruption
これらの何が違うかというお話でございますが、AMQP::Session#on_tcp_connection_lossはTCPコネクションが切断された 初回 にて実行されます。ところが、再接続の要求は必ずしも初回にて達成されるとも限りませんものですから、何回かは失敗する事が想定されましょう。それらに毎回反応したいニーズがございましたら、AMQP::Session#on_connection_interruptionをお使いいただけるということになってございます7。
先述の通りこれらのメソッドにはブロックをつけて呼んでいただいております。ブロックが呼ばれる際にはコネクションオブジェクト自身が引数として渡ってまいりますので、Procではなくメソッドオブジェクトを渡していただく場合であっても、インスタンス変数に余計な状態変数のようなものを保存していただく必要はございません:
connection.on_connection_interruption do |conn|
puts "Connection detected connection interruption"
end
# or
class ConnectionInterruptionHandler
#
# API
#
def handle(connection)
# handling logic
end
end
handler = ConnectionInterruptionHandler.new
connection.on_connection_interruption(&handler.method(:handle))
もし余所でもハンドラを定義しているのであれば、AMQP::Session#on_connection_interruptionにて登録されたコールバックは他のチャンネルやキューへ事象が伝搬するよりも 前 に呼ばれるという点にご注意くださいませ。
コネクションの切断にどう対処すべきかは、アプリケーションによるとしか申し上げられません。しかしながらよくある戦略としましては、AMQP::Session#reconnectを用いて同じホストに再接続するか、あるいはAMQP::Session#reconnect_toを用いて別のホストを試すといったものがございましょう。
アプリケーションによってはそのようなことをせず、単にプロセスを終了してしまって、外部のたとえばNagiosであるとかMonitといった監視システムに再起動させるので充分かもしれません。
AMQP::Channel8 のシャットダウン・プロトコル
AMQP::ChannelにはAMQP::Channel#on_connection_interruptionのみがございます。このメソッドは前の章のメソッドとだいたい同じで、渡されたブロックを登録するものでございます:
channel.on_connection_interruption do |ch|
puts "Channel #{ch.id} detected connection interruption"
end
しかしながらご注意いただきたいのは、AMQP::Channel#on_connection_interruptionにて登録されたコールバックはエクスチェンジやキューに事象を伝播した 後 だということでございます9。チャンネルの状態がリセットされた直後からこれらのエラーハンドリングが開始されるとお考えください。
なお多くのアプリケーションではチャンネル単位でのネットワークエラーの取り回しは不要かと存じます。
AMQP::Exchange10 のシャットダウン・プロトコル
AMQP::ExchangeにはAMQP::Exchange#on_connection_interruptionのみがございます。このメソッドは前の章のメソッドとだいたい同じで、渡されたブロックを登録するものでございます:
exchange.on_connection_interruption do |ex|
puts "Exchange #{ex.name} detected connection interruption"
end
なお多くのアプリケーションではエクスチェンジ単位でのネットワークエラーの取り回しは不要かと存じます。
AMQP::Queue11 のシャットダウン・プロトコル
AMQP::QueueにはAMQP::Queue#on_connection_interruptionのみがございます。このメソッドは前の章のメソッドとだいたい同じで、渡されたブロックを登録するものでございます:
queue.on_connection_interruption do |q|
puts "Queue #{q.name} detected connection interruption"
end
AMQP::Queue#on_connection_interruptionで登録されたコールバックはこの事象がコンシューマに伝播された 後 から動き出すことにご注意ください。
なお多くのアプリケーションではキュー単位でのネットワークエラーの取り回しは不要かと存じます。
AMQP::Consumer12 のシャットダウン・プロトコル
AMQP::ConsumerにはAMQP::Consumer#on_connection_interruptionのみがございます。このメソッドは前の章のメソッドとだいたい同じで、渡されたブロックを登録するものでございます:
consumer.on_connection_interruption do |c|
puts "Consumer with consumer tag #{c.consumer_tag} detected connection interruption"
end
なお多くのアプリケーションではコンシューマ単位でのネットワークエラーの取り回しは不要かと存じます。
ネットワーク切断からの復旧
ネットワーク障害を検知しただけで復旧できないのであれば、検知した意味はほとんどないと言えましょう。復旧は「エラーの取り回しと復旧」のなかでも困難な部類に属する話題ではございますが、幸いにしてだいたいのアプリケーションにおいて同じ戦略が有効ですので、Ruby amqp gemでは自動でそれを行うことも可能になっております。
自動であれ手動であれ、障害からの復旧はまずAMQPのコネクションを再接続して、チャンネルを開き直すところから始まります。
シャットダウン・プロトコルと同様に、復旧する側にもリカバリ・プロトコルを提供してございます。コネクション、チャンネル、キュー、コンシューマ、エクスチェンジのすべてで、以下の3メソッドがご利用いただけます:
AMQP::Session#before_recovery
AMQP::Session#auto_recover
AMQP::Session#after_recovery
これらもやはりコールバックを登録するのでございますが、たとえばAMQP::Session#before_recoveryの場合におきましてはTCP層のセッションが再確立した後でかつAMQPのコネクションを再構成する前に呼ばれ、あるいはAMQP::Session#after_recoveryですとAMQPコネクションが再構成された後に呼ばれるというふうになっております。
AMQP::Channel, AMQP::Queue, AMQP::Consumer, AMQP::Exchangeのすべてにおきましてこれらのメソッドの挙動は同様でございます。
大抵の場合、コネクション切断からの復旧手順は以下のようになることでしょう。
- AMQPコネクションを再確立します
- そのコネクションを使ってチャンネルを再構成します
- それぞれの復活したチャンネルにおいて、エクスチェンジを宣言しなおします
- それぞれの復活したチャンネルにおいて、キューを宣言しなおします
- キューの宣言が終わったら、バインディング14を作成しなおします
- キューの宣言が終わったら、それぞれの復活したキューにおいて、コンシューマを作成しなおします
自動復旧
大多数のアプリケーションでは:
- チャンネルを開きなおして、
- 各チャンネルでエクスチェンジを宣言しなおして
- 各チャンネルでキューを宣言しなおして
- 各キューでバインディングを作成しなおして
- 各キューでコンシューマを作成しなおす
という、同じ戦略の復旧が行われますことから、amqp gemではこの戦略を行う自動復旧の機能がオプトイン(明示的に指定した場合のみ)で提供してございます。AMQP::Channelにて以下の属性をご指定ください:
ch = AMQP::Channel.new(connection)
ch.auto_recovery = true
より冗長には以下のようにしていしていただくこともできます:
ch = AMQP::Channel.new(connection, AMQP::Channel.next_channel_id, :auto_recovery => true)
この場合には第二引数が明示してございますが、これは本来省略可能でございますので、オプションを指定しない限りは明示する必要はございません(AMQP::Channel.next_channel_idは省略時のデフォルトでございます)
あるチャンネルが自動復旧モードかどうかはAMQP::Channel#auto_recovering?でご確認いただけます。
自動復旧モードは任意の回数だけ有効/無効を切り替えてお使いいただけますが、実際にそのように使われる用途はほとんどないかと存じます。ごく常識的に考えまして、自動復旧モードを使われるかどうかはアプリケーションの設計上の問題でございましょう。
プログラム一式だとこうなります(スクリプトが起動したら、既に裏で動いているはずのAMQPブローカーをシャットダウンして、もう一回立ち上げなおしてください。rabbitmqctlのようなブローカーの側のツールでキュー、エクスチェンジ、バインディング、コンシューマが復旧している事をご確認ください):
なおキューの名前空間の衝突を防ぐため、サーバが勝手に名前をつける(ようにこちらからブローカに依頼した)キューが自動復旧すると 名前が変わります ことにご注意くださいませ。
よく分からないときは、まず自動復旧をお試しください。それでアプリケーションのニーズに沿わないとご判断された場合には、手動復旧の節でご紹介したコールバックをお使いいただけます。
ブローカー側障害の検出
AMQPを用いるアプリケーションにおきましてはブローカーの障害はTCPコネクションの切断として観測されます。ネットワークの障害とブローカーの障害を切り分ける信頼性のある方法はございません。
AMQPコネクション例外15
コネクション例外の取扱い
コネクション例外は稀ではございますが、発生の暁にはクライアント側のライブラリに重大な問題が発生しているか、さもなくばネットワーク上でデータが化けているなどの状況が想定されます。かような状況下におきましては、もはやこのコネクションは使用できませんので、切断する必要がございましょう。この種のシチュエーションは、いずれにせよなんらかの対処が必要な状況であるといえましょう。AMQP::Session#on_errorにてハンドラをご登録ください。このブロックには、二つのオブジェクトが渡ってまいります:
connection.on_error do |conn, connection_close|
puts "Handling a connection-level exception."
puts
puts "AMQP class id : #{connection_close.class_id}"
puts "AMQP method id: #{connection_close.method_id}"
puts "Status code : #{connection_close.reply_code}"
puts "Error message : #{connection_close.reply_text}"
end
上記でステータスコードと呼んでいるものはHTTPに類似しております。網羅的な一覧が必要な方はAMQP 0.9.1 constants referenceをご参照ください。
なおコネクション例外ハンドラは一つしか登録できませんのでご注意ください。最後に登録したものが勝ちます。
上記を含んだプログラム一式だとこうなります:
グレースフル・シャットダウン
AMQPブローカーが正常に終了するときには、コネクションを終了するための正規の手順というものが規定されてございます。ブローカーはAMQPメソッドconnection.closeを発行してまいりますので、クライアント側ではそのステータスコードが320 CONNECTION_FORCEDであるかどうかでグレースフルなシャットダウンかどうかを判定していただけます。たとえばRabbitMQの場合、サーバが
rabbitmqctl stop
にて終了した場合は、ステータスラインは
CONNECTION_FORCED - broker forced connection closure with reason 'shutdown'
となります。
グレースフル・シャットダウンに対してアプリケーション側がどのように振る舞うべきかにはこれといった普遍的な方針はございません。したがいましてamqp gemの自動復旧モードではグレースフル・シャットダウンに対しては再接続を行わないようにしてございます。そのような場合にも再接続を望まれる場合はAMQP::Session#periodically_reconnect16をお使いいただけます:
connection.on_error do |conn, connection_close|
puts "[connection.close] Reply code = #{connection_close.reply_code}, reply text = #{connection_close.reply_text}"
if connection_close.reply_code == 320
puts "[connection.close] Setting up a periodic reconnection timer..."
# every 30 seconds
conn.periodically_reconnect(30)
end
end
コネクションが再開した後には自動復旧の手順はネットワーク切断からの復旧と同様に行われます。
コネクション例外をRubyのオブジェクト指向プログラミングに統合する
エラーの取り回しはRubyのオブジェクト指向プログラミングに素直に統合していただけます。むしろ推奨されている、とここでは申し上げておきましょう。よく使うのはObject#methodとMethod#to_procを組み合わせてエラーハンドラにメソッドを登録する方式です:
class ConnectionManager
#
# API
#
def connect(*args, &block)
@connection = AMQP.connect(*args, &block)
# combines Object#method and Method#to_proc to use object
# method as a callback
@connection.on_error(&method(:on_error))
end # connect(*args, &block)
def on_error(connection, connection_close)
puts "Handling a connection-level exception."
puts
puts "AMQP class id : #{connection_close.class_id}"
puts "AMQP method id: #{connection_close.method_id}"
puts "Status code : #{connection_close.reply_code}"
puts "Error message : #{connection_close.reply_text}"
end # on_error(connection, connection_close)
end
上記を含んだプログラム一式だとこうなります:
詳細はあとで書く。
AMQP チャンネル例外
チャンネル例外の取り扱い
チャンネル例外はコネクション例外よりはまだありがちと言えましょう。ただ、どのみち同じようにAMQP::Channel#on_errorを使ってコールバックを登録していただくことで対処可能です。このブロックはチャンネル例外発生時に二つの引数とともに呼ばれます:
channel.on_error do |ch, channel_close|
puts "Handling a channel-level exception."
puts
puts "AMQP class id : #{channel_close.class_id}"
puts "AMQP method id: #{channel_close.method_id}"
puts "Status code : #{channel_close.reply_code}"
puts "Error message : #{channel_close.reply_text}"
end
上でstatus codeと呼んでいるものはHTTPと同様でございます。網羅的な一覧とその解説がAMQP 0.9.1 constants referenceにございます。
なおチャンネル例外ハンドラは一つしか登録できませんのでご注意ください。最後に登録したものが勝ちます。
上記を含んだプログラム一式だとこうなります:
コネクション例外をRubyのオブジェクト指向プログラミングに統合する
エラーの取り回しはRubyのオブジェクト指向プログラミングに素直に統合していただけます。むしろ推奨されている、とここでは申し上げておきましょう。よく使うのはObject#methodとMethod#to_procを組み合わせてエラーハンドラにメソッドを登録する方式です。コネクション例外の章にあります例もご参照ください。
チャンネル例外は様々に無関係な理由により上がってまいりますうえに、おおむねの場合は設定ミスが根本的な原因でございましょう。したがいまして、これをどう対処するべきかと申しますのは一概に申し上げるのは大変に困難でごさいましょう。ログでも吐いて別のチャンネルを開いて使う、程度のことしか申し上げられません。
よく起こるチャンネル例外とその意味
いくつかのチャンネル例外はよく起こりますと同時に、若干の注意を要します。
406 Precondition Failed
- 概論
- クライアントの要求は拒否されましたが、それは何らかの事前条件を満たさなかったためです。
- 考えられる原因
-
-
AMQPエンティティ(キューとかエクスチェンジ)が指定した設定とは異なる設定で既に存在している場合。接続しているアプリケーションが2つあって、互いに違った設定で動いている、などが考えられましょう。またAMQPクライアントライブラリのデフォルトがバージョン間で違うために起こる可能性もございましょう。
- `AMQP::Channel#tx_commit`か`AMQP::Channel#tx_rollback`を呼んだが、そのチャンネルではまだ`AMQP::Channel#tx_select`を宣言することでトランザクションモードに遷移する前だった場合。
- たとえばRabbitMQだとこんなエラーが
-
- PRECONDITION_FAILED - parameters for queue ‘amqpgem.examples.channel_exception’ in vhost ‘/’ not equivalent
- PRECONDITION_FAILED - channel is not transactional
405 Resource Locked
- 概論
- クライアントはAMQPエンティティを操作しようとしましたが、それは他のクライアントが現在操作中でした
- 考えられる原因
-
- キューにはexclusiveという設定が可能です。複数のアプリケーション(あるいは同じアプリケーションでも別のスレッドとかプロセス)が同時にそのようなキューを宣言しようとすると、そのうちの一つのみが成功してあとはこの例外になります。
- コンシューマにもexclusiveという設定が可能です。コンシューマのexclusiveはキュー単位です。ひとつのキューに複数のコンシューマがexclusive登録しようとした場合に発生します。
- たとえばRabbitMQだとこんなエラーが
- RESOURCE_LOCKED - cannot obtain exclusive access to locked queue ‘amqpgem.examples.queue’ in vhost ‘/’
403 Access Refused
- 概論
- セキュリティ上の理由によりクライアントの要求は拒否されました
- 考えられる原因
- アプリケーションが認証に用いたユーザーでは、キューやエクスチェンジにアクセスする権限がない、あるいは全くないってわけじゃないんだけど書き込み権限がないなど、不適切であることが考えられましょう。
- たとえばRabbitMQだとこんなエラーが
- ACCESS_REFUSED - access to queue ‘amqpgem.examples.channel_exception’ in vhost ‘amqp_gem_testbed’ refused for user ‘amqp_gem_reader’
TLS (SSL) 関連
あとで書く。17
まとめ
プログラマ諸賢におかれましては分散アプリケーションのエラー状況はそうじゃない場合と比べてまったく異質であることをご認識いただく必要がございましょう。それらの多くが、ネットワークの信頼性(のなさ)に起因するものでございましょう。この分野で有名な分散コンピューティングの落とし穴という、
ソフトウエアエンジニアが陥りがちな誤った認識のリストがございます:
- ネットワークには信頼性がある
- ネットワークにはレイテンシが存在しない
- ネットワークの帯域幅は無制限である
- ネットワークはセキュアである
- ネットワークのトポロジーは変化しない
- ネットワーク管理者は、必ず存在するし、たかだか一人である
- ネットワークに転送コストはない
- ネットワークはホモジニアスな構成である
この一覧は90年代に作られましたが、こんにちいささかも色褪せてはいないでしょう。残念ながらRubyやAMQPを使ったところでこれらの問題から逃れられるわけではございませんでしょう。開発者はこの事実を心に留めておく必要がございましょう。
0.8.x以降のruby amqp gemではアプリケーション側からハンドラを登録することで、コネクション例外、チャンネル例外や、TCP層での接続の問題を取り回していただけます。セッションが復旧したあとのAMQPエンティティの再宣言が複雑なのでして、AMQPの例外やネットワーク切断は、比較的容易な部類と言えましょう。rubyのオブジェクトをエラーハンドラとして用いると、AMQPエンティティの宣言を一ヶ所にまとめることができるでしょう。これは理解も容易になりますし、保守の観点からも好ましいでしょう。
amqp gemのエラーハンドラはJava版RabbitMQクライアントのShutdown Protocolのコピペではこざいませんが、結果的にネットワーク切断やコネクション例外に関しては似たような感じに収斂進化したようです。
まだあとで書く。
著者について
このガイドはMichael Klishinによって書かれ、Chris Duncanの手が入っています18。
ご感想をお待ちしています
このガイドに関するご感想をTwitter上かRuby AMQPメーリングリストでお聞かせください。分からないことはございましたか? 書き漏れている点はございませんでしょうか? スペルミス、文法の誤り、お前の口調が気にくわない等ございませんか? 読者の声がドキュメントをよくする一番の方法です。よろしくおねがいします。
何らかの理由でMLでは連絡をとりたくない場合は、ガイド作者にじかにご連絡いただくこともできます