
こんにちは、Habr!
ITMOのGoToフォールスクールの終わりからわずか11577635秒しかかかりませんでした。 Distributed Systems Weekは、Cloud Haskellでの分散システムのプロトタイピングから始まりました。 私たちは精力的に始めたため、博士号なしで既存のドキュメントを理解することは難しいことをすぐに発見し、トレーニングマニュアルを作成することにしました。
カットの下で、p2pクラウドhaskellの紹介、PCのプロトタイピング、やる気、そして「なぜ」のわずかに機能的なスタック。
あなたがそう分散した何かをしたいとします
そのような問題の声明から適用されたプログラマは、すぐに「ライブラリ」という単語を思いつきます。 そして本当に。 たとえば、標準のNATパンチメカニズム(STUN、TURN、ICE)も一般的に知られているKademliaから発見とルーティングのピースを取得できます。など
しかし、それでも多くの時間と専門知識が必要になります。 投資家には忍耐がないかもしれません。
ここでは、より経験豊富な同僚が「フレームワークが必要です!」という考えを思いつきます。 そして本当に。 分散システムおよびアプリケーションのプロトタイピング用。
そして誰かが言うことさえあります: 神 、それはlibp2pです! そして彼は正しいでしょう。 部分的に。
libp2pは、トランスポート、その多重化と暗号化、検出、ピアルーティング、NATブレークダウン、接続アップグレードなどの問題を解決します。 -一般的に、分散アプリケーションの多くのネットワークおよび暗号化のニーズ。 On GoとJS。
これは素晴らしいフレームワークですが、いくつかの問題があります。 これがGoとJSです。 さらに、レプリケーションのフレームワークに何かがあると便利です。
チュートリアルの断片的な性質(一部はまったく機能しませんでした)により、Cloud Haskellを使用しないように説得されました
http://www.scs.stanford.edu/14sp-cs240h/projects/joshi.pdf 、言い換え
私たちのプロジェクトは、Haskellでブロックチェーン(申し訳ありませんが、イノベーション)を作成するという野心から始まりました。したがって、libp2pはありませんでした。 私たちはネットワーク(トランスポート、ディスカバリー、シリアル化)を作るものを探し始めました。 Cloud Haskellが見つかりました。 ドキュメントが複雑であることがわかりました。 私の紹介を書くことにしました。 だから:
Cloud HaskellでSky Beesを書く
この例では、ミツバチのシステムを作成します。ハイブ(マシンのクラスター)とミツバチ(ノード)があります。 ミツバチは花を探すためにスカウトに送られ、おいしい花の座標とともに巣に戻ります。他のすべての蜂はこれらの座標を知っている必要があります。
プログラムを複数のコンピューターで実行する必要はまったくありません。プログラムを並行して実行するラップトップだけです。
完全なコードはリポジトリにあります 。
Cloud Haskellは、ノードが共通のリソーススペース(RAMなど)を共有しないため、ノード間でメッセージを交換するという原則に基づいて動作します(そのようなモデルはメッセージパッシングと呼ばれます )。 アクターモデルは、 アクターが他のアクターにメッセージを送信し、 メールボックスでメッセージを受信する場合のメッセージパッシングモデルのプライベートな例です。これが、Cloud Haskellでのメッセージパッシングの様子です。
1.最初に、ミツバチが代表するデータのタイプを定義します: src / Types.hs
type Flower = (Int, Int) -- type Flowers = GSet Flower -- Grow-Only Set
- ミツバチは、ハイブ内の少なくとも1つのミツバチが知っているすべての花について知っている必要があります。したがって、ミツバチは、ミツバチの脳で花の「データベース」の単一の状態を維持する必要があります- データ複製の問題を解決するために、 コンセンサスに到達する必要がある:異なる蜂のデータベース間の競合を解決。 これを行うために、 GSet (Grow-only Set)データ構造を使用します-要素を追加することはできますが、削除することはできません 。 これは、 CRDTデータ構造の1つです。 HaskellでGSetを使用するために、Yuri Syrovetskyの優れたcrdtライブラリ( @cblp )を使用しました。
Log A Log B | | logA.append("one") logA.append("two") | | vv +-----+ +-------+ |"one"| |"hello"| +-----+ +-------+ | | logA.append("two") logA.append("world") | | vv +-----------+ +---------------+ |"one","two"| |"hello","world"| +-----------+ +---------------+ | | | | logA.join(logB) <----------+ | v +---------------------------+ |"one","hello","two","world"| +---------------------------+
CRDTを使用したコンセンサス構築スキーム( https://github.com/haadcode/ipfs-logから) - 蜂の受容体として機能するインターフェースが必要です。GSetに要素を追加し、ハイブが知っているおいしい花を表示します。これをREPL(インタラクティブシェル)の形式で実装します。
2.ノードの実装を開始しましょう。将来、コマンドラインから実行します: app / Main.hs
main = do -- [port, bootstrapPort] <- getArgs -- (1) bootstrap let hostName = "127.0.0.1" -- IP P2P.bootstrap -- : hostName port -- (\port -> (hostName, port)) initRemoteTable -- (2) remote table [P2P.makeNodeId (hostName ++ ":" ++ bootstrapPort)] -- bootstrap spawnNode -- ,
- 最初に、ノードは何らかの方法でお互いについて学習する必要があります。つまり、 ピア発見を行います。 Cloud Haskellにはすぐに使用できるソリューションがあります。ノードを初期化するときに、少なくとも1つの他のブートストラップノードを指定するだけで十分です。ノードは、 Peer Exchangeノードをブートストラップで作成します-知っているノード(別名、 ピア )のアドレスを交換します。
- リモートテーブルは、ピアがシリアル化をサポートしている場合、ピアがhaskellタイプを交換できるようにするものです。つまり、ネットワークを介して送信し、Haskellオブジェクトに復元できる形式で表すことができます。 型は
class (Binary a, Typeable) => Serializable a
実装する場合、直列化をサポートしますclass (Binary a, Typeable) => Serializable a
。Serializable
、Binary
、Typeable
実装をTypeable
で発明する必要はありません-haskellはこれをあなたのために行います(魔法の自動導出メカニズムを使用):
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} -- , Binary data Example = Example deriving (Typeable, Generic) instance Binary Example
instance Binary
deriving ...
、instance Binary
およびプラグマを省略しinstance Binary
。
3.次に、ノードを起動するためのロジックを記述します。
spawnNode :: Process () -- (1) spawnNode = do liftIO $ threadDelay 3000000 -- bootstrap let flowers = S.initial :: Flowers -- GSet self <- getSelfPid -- (3) Pid REPL repl <- spawnLocal $ runRepl self -- (2) REPL register "bees" self -- "bees" spawnLocal $ forever $ do -- (3) : send self Tick -- liftIO $ threadDelay $ 10^6 -- 0.1 , runNode (NodeConfig repl) flowers -- (5)
- Cloud Haskellでは、主な機能単位は
Process
(OSプロセスと混同しないでください)。 それらは軽量の緑のスレッドに基づいており、他のプロセスにメッセージを送信できます(特定のプロセスに送信するsend
機能またはすべての使い慣れたノードに送信するP2P.nsendPeers
)、 メールボックスでメッセージを受信します(expect
またはreceive*
機能)、他のプロセスを開始します(たとえば、ローカルでspawnLocal
を使用するなど) - REPLを別のスレッドに実装する必要があります。そうしないと、メインスレッド(ノード)がブロックされるため、REPLとノードの両方で変更できるようにGSetのスレッドセーフインターフェイスを作成する必要があります。 システムはアクターに基づいているため、セットを変更するメッセージを送信し、メインスレッドでメッセージを処理する無限のサイクルで順次処理します。
- REPLを個別のクラウドハスケルプロセスとして(つまり、緑色のスレッドとして)実行し、メインプロセスのPid(一意のプロセス識別子)を渡して、REPLがユーザーが入力したコマンドをメッセージの形式で送信する場所を認識できるようにします。 次に、PID REPL(spawnLocalが返す)を取得して、コマンドへの応答を送信します。 REPLコードはこちらです。
- 花の複製はどのように機能しますか?
- 各ノードはその状態を定期的にすべてのピアに送信し( ブロードキャスト )、CRDTとともにこれにより複製の問題を解決します。
ノードA
とB
があるとしますB
A
は要素xがなく、B
xがあるとします。B
がブロードキャストを行った後、A
はxを追加します-コンセンサスに達しました、など。
GSetではなく通常のセットがあれば、何も起こりませんでしたA
とB
要素yがあるとします。A
yを削除させます。B
がブロードキャストを行った後、A
はyを返します。 - すべてのノードにメッセージを送信する場合、 サービスの名前を指定する必要があります。実際、このサービスをサポートするものとしてレジスタに登録されているノードにのみメッセージを送信します。 ここでは、ノードを「bees」サービスをサポートするものとして登録します。
register "bees" self
ます。 - 野田は、他の人にいつ幸運を送るべきかを知らなければなりません。 最も簡単な解決策は、タイマーでそれを行うことです。1秒待ってから行動しますが、その後、メインのメッセージ処理フローをブロックします。 ここでは、
spawnLocal
をspawnLocal
てプロセスを開始します。最初にTickメッセージをメインプロセスに送信し(メインプロセスがTickを検出すると、その状態をノードに送信します)、1秒待機して繰り返します。
- 各ノードはその状態を定期的にすべてのピアに送信し( ブロードキャスト )、CRDTとともにこれにより複製の問題を解決します。
4. OK、今(最終的に!)メインプロセスのロジック(ノード実行コード)を開始できます。
runNode :: NodeConfig -> Flowers -> Process () -- (1) runNode config@(NodeConfig repl) flowers = do let run = runNode config receiveWait -- (2) [ match (\command -> -- (3) - Command REPL, newFlowers <- handleReplCommand config flowers -- run newFlowers) , match (\Tick -> do -- , P2P.nsendPeers "bees" flowers -- run flowers) , match (\newFlowers -> do -- - run $ newFlowers `union` flowers) -- - ]
- シグネチャを見てみましょう
runNode
は、タイプNodeConfig
ノードの構成を受け入れます-実行時に変更されない情報。 私たちの場合、これは単なるPID REPLです。 彼女は現在の状態である花のGSetも受け入れます。 しかし、GSetは不変のデータ型なので、花を追加する方法は? 非常に簡単:関数を再帰的にし、状態が変化するたびに関数を再起動します。 -
receiveWait
は、1つの引数(着信メッセージ)を持つ関数のリストを受け取り、メッセージを引き出して、メッセージのタイプに適した関数を呼び出します。 - このタイプのメッセージを受信した場合:
data Command = Add Flower | Show
data Command = Add Flower | Show
、これはREPLからのコマンドです。handleReplCommmand
コマンドを処理するための関数:
handleReplCommand :: NodeConfig -> Flowers -> Command -> Process Flowers handleReplCommand (NodeConfig repl) flowers (Add flower) = do -- send repl (Added flower) -- REPL , return $ S.add flower flowers -- handleReplCommand (NodeConfig repl) flowers Show = do -- send repl (HereUR $ toList flowers) -- return flowers
- ティックがティッカーから来た場合、ステータスを送信する必要があります:
P2P.nsendPeers "bees" flowers
。 ここで、「bees」はサービスの名前です。つまり、「bees」として登録されているノードにのみ花を転送します。 - 他のミツバチから花を受け取った場合、馴染みのない花をすべて自分自身に追加する必要があります。つまり、多くの新しい花と多くの既存の花を単純に組み合わせます。
5.以上です! 完全なソースコードをダウンロードしてコンパイルします。
git clone https://github.com/SenchoPens/cloud-bees.git cd cloud-bees stack setup # Stack GHC stack build #
1つのターミナルで次の行を実行します。
stack exec cloud-bees-exe 9000 9001 2>/dev/null
そして別のこれで:
stack exec cloud-bees-exe 9001 9000 2>/dev/null
REPLがプロンプトを出します。 1つの端末に
Add (1, 2)
を追加してみてください。 座標(1、2)を持つ花を追加し、別の-Showで、2番目のノードにもそのような花があることがわかります。
- パート
2>/dev/null
、Cloud Haskellがログインするstderrを隠すために必要です。 これを行わないと、REPLを正常に使用できなくなります。/dev/null
をlog.txt
置き換えて、表示内容を確認できます。
Haskellで分散システムを作成することはそれほど怖くないことをあなたに納得させていただければ幸いです:)
同様のシステムの多くの実際のユーザーケースを考え出すことができます:たとえば、公共交通機関で野ウサギの問題を解決する:カード上の交通機関に通行する人はログイン済みとしてマークされ(最初のGSetに自分のIDを追加)、出力時に-ログアウト済みとして(IDを追加します) 2番目のGSet)。 夜(輸送が機能しないとき)に、チェックが行われます-人が出入りした場合、彼は野ウサギではありません。
興味のある方は、シフト中に行った暗号化を使用したより大きなプロジェクトをご覧ください。
愛を込めて、アーセニーと仲間、9年生。 wldhxの穏やかな指導の下で