naoki86star

インターネットの片隅でなにかしら書いてみる

今までのプログラミングとClojureプログラミングとを比べてみる(ネットワーク系)

 ネットワークの情報を集めるとかコマンドを送るみたいなプログラムだといわゆるインターネットプロトコルを取り扱うことが多いと思います。pythonスクリプトに限らず大抵のプログラミング言語はコードが書かれた通り逐次実行されていくものだと考えています。マルチスレッドの場合にしてもそれぞれのタスクそのものはそれぞれの処理がステップを踏んでいくのをイメージしそれの相互作用の有無を考えていくと思います。

 clojureに触れていて、遅延実行というものの存在に注意する反応するようになりました。ネットワークプログラミングだと、プロトコルというだけあって、いやプロトコルという言葉と直接は関係ないですけども、意図したタイミングで順番に呼ばれないとライブラリとかうまく動かないことがあります。

snmp4jの助けを借りて、snmpbulkwalkでできる機能を作ろうとしたときの話。

次のsnmpコマンドでできる同じことを、clojureで呼んで、それでもってwebインタフェースで流そうか、みたいなことを考えました。

snmpbulkwalk -v2c -c public 192.168.0.1 1.3.6.1.2.1.31.1.1.1.1

 このmibはターゲットのinteface name (ifName, linuxとかだと eth0とかのやつ) を返してくれます。実際はPCよりもルータに対して使おうとするものです。

 次の書き方が、単純ケースは動いてくれるんだけれどもNGでした。*1*2

(defn snmpbulkwalk-v2c [host community oid]
(do
  (def transport (DefaultUdpTransportMapping.))
  (. transport listen)
  (def snmp (Snmp. transport))
  (def events (. (TreeUtils. snmp (DefaultPDUFactory. ))
                 (getSubtree (build-target host community) (OID. oid)))
  (. snmp close)
  (decodeEvents events host oid)
))

上の中でdecodeEventsは、getSubtreeメソッドが返してくれたorg.snmp4j.util.TreeEventオブジェクト(これにmibとvalueが入ってくる)を読み取って最後にmapを返す関数です。build-targetはorg.snmp4j.CommunityTarget.のインスタンスを返します。いずれも同じns内で別に関数定義してあります。DefaultUdpTransportMapping,Snmp,TreeUtils,DefaultPDUFactory,OID,..はsnmp4jのクラスです。
 

(map deref 
    [(future (snmpbulkwalk-v2c "192.168.0.100" "public" "1.3.6.1.2.1.31.1.1.1.1"))
     (future (snmpbulkwalk-v2c "192.168.0.102" "public" "1.3.6.1.2.1.31.1.1.1.1"))])

 シングルスレッド・同期的に呼び出す場合には、正常に結果が得られます。が、上記みたいに呼び出すとNGです。複数host宛てでfutureでパラレルで呼び出すと、うまく結果が戻るときもあれば、実行が止まるとか、java exceptionでることあるとか結果が安定しません。1hostすなわち1スレッドだと鉄板でうまくいきます。しばらくfutureの使い方が悪いのか、snmp4jが実はスレッドsafeでないのかとか、そっちから調べてしまってました。*3

 明確に理由を説明することが今の自分にできません。が推測するには、スレッドの結果を取り出そうとするときに、初めて`(def snmp...`の部分から評価されて、実際にUDPポートが開いて、みたいな処理が非同期で動いてしまってるのでないかと思います。評価順番そのものは前後しないはずなのですが。このmibだと、とってくるinterface 数によりますが、1callでsnmp-bulkgetを1パケなげて1パケ受け取るのを複数回以上繰り返すのようなsnmpの動作が走ります。それが2スレッド平行になったときに、処理上の同期がとれなくなる...説明になってませんですね。

 最初、ただの関数形でdefを使って書いてしまって動いたのをそのままfutureで複スレッド平行に適用してしまいましたが、気になってたdefを書き直したらご機嫌に動くようになりました。

(defn snmpbulkwalk-v2c [host community oid]
  (let [transport (DefaultUdpTransportMapping.)]
    (. transport listen)
    (let [snmp (Snmp. transport)]
      (let [events (. (TreeUtils. snmp (DefaultPDUFactory. ))
                      (getSubtree (build-target host community) (OID. oid)))]
        (. snmp close)
        (decodeEvents events host oid)
)))))

 clojure tutorialとか読んでから始めたばかりだと、とりあえず変数のつもりでdef使って試し始めます。私だけですかね。defとはmacroって説明にあったのは覚えていつつ、rpelでもdef使うとあたかも変数のようにふるまってくれるので、ついお試しだとこれからやってしまいます。

 遅延実行なるものは、評価のタイミング・計算機にロードするタイミングをアルゴリズム的に最適なところで行うことで最速・最効率な結果を得られるためのテクニック、といえると理解してます。今までも、例えばループの中で一回評価すればいい値はループの外に外すとか、そういうように思考してきたのですが、clojure(というかここが関数型たるひとつのなにか)でプログラムを書くなら呼び出し構造そのものに着目することになる、ということなのかと、思いました。

 ネットワーク操作系・監視系のスクリプトは、構造うんぬんよりも手順が決まっていてそのまま順に落としていくのに慣れてしまっているので、その辺を勘案して、clojureの使いどころ考えてみます。*4
 パケットのデータは効率重視で詰め込んであるとか取りやすい順に詰めてあるとかで必ずしもデータ構造がきれいなリスト処理に向かないこともありそうな気もしてます。

*1:snmp4j 2.5.0を使っていて適宜 :importしてあります

*2:java のsnmpライブラリはmaven repositoryにもういくらか比較検討しておきたいのもあるんですが、とりあえずこれで

*3:futureの使い方自体はそれはそれでもう少し学ばないと、せいぜい30host程度のために書き始めましたけど1000ノードとかだったら...

*4:まあ今回のは単にdefの誤用なだけですが