Docker謹製ライブラリのlibchanについて調べてみた
June 28, 2014 - golang
DockerCon2014で発表されたlibchanについて調べたことをまとめてみます。
libchanはlibcontainerやlibswarmと共に発表されました。 libswarmはDockerを中心にしたエコシステムにおけるベンダーロックインを回避するためのソリューションであり、libcontainerはDocker 0.9リリースドキュメント日本語訳: Execution driversとlibcontainer導入 - Happy New Worldを参照するのがよいでしょう。
では、libchanとは何なのでしょうか。
READMEにはlike Go channels over the network
とあります。ネットワーク上のgo channel?よくかりませんね。。。
README.mdとPROTOCOL.md、いくつかのテストコードをもとに簡単な実装をしてみた結果をまとめます。
libchanとはなにか
libchanは超軽量なネットワークライブラリであり、多様な通信プロトコルの土台となるものです。 同一プロセス内でも、同一ホストの別プロセス間でも、ネットワークを超えた別ホストの別プロセス間でも双方向通信可能にするための基礎的なAPIを提供しています。 モダンなmicro-serviesなどRPCやRESTプロトコルにはフィットしない領域で、libchanを使うことが想定されています。
利用例はいまのところlibswarmのみです。というかlibswarmから抽出された汎用的な通信ライブラリだと思います。 APIが安定してくると他のフレームワーク等への導入などもあるかもしれません。
では、具体的にどうやって双方向通信を可能にしてるのでしょう。
libchanのプロトコル
libchanは以下のコンポーネントで構成されています。
- channel
- session
- message
- byte stream
- nesting
channelとは、並行プログラム間の双方向通信用オブジェクトです。goのchannelに似てるけどそのものではありません。Sender/Receiverがそれぞれ1方向ずつの経路を持つため、ソケットというよりパイプの概念に近いです。
この2つの通信経路がsessionとなり、その間を流れるのがbyte stream。 で、byte streamにはmessageが乗っかり、messageにはchannelを含めることができるという概念です。
libchanのメッセージ
メッセージはlibchan.Message。
type libchan.Message struct {
Data []byte // メッセージの内容
Fd *os.File
Ret Sender // 受信者が返答するためのchannel
}
このメッセージ型を、各種通信方式に対応したSender/Receiver経由でやりとりすることになります。
メッセージの構造化
メッセージは以下のようにして生成します。
message := libchan.Message{Data: []byte("Hello, libchan"), Ret: libchan.RetPipe}
で、ただのテキストではなく構造化されたデータも送れます。
import "github.com/docker/libchan/data"
d := data.Empty().Set("foo", "bar")
d.Get("foo") // "bar"
d.Pretty() // foo=bar
message := libchan.Message{Data: d, Ret: libchan.RetPipe))
ちなみに、各メソッド(Add,Set,Get,Del)はMessageのポインタをかえさないので、変数を上書きするかメソッドチェインにする必要があります。
メッセージの送受信
作成したメッセージををSender経由で送ります。(ここでのSender/Receiverは擬似コードです)
sender.Send(&libchan.Message{Data: []byte(msg)})
で、Receiverが受信したメッセージはdata.Decode()
でmap[string][]string
に変換できます。
message, _ := receiver.Receive(0)
decoded, _ := data.Decode(string(message.Data))
fmt.Printf("%$v\n", docoded) // map[string][]string{"foo":[]string{"bar"}}
Sender/Receiver
実際にメッセージを送受信するのは、Send/Receiverメソッドを持つinterfaceを実装したものを使います。 定義はだいたいこんなかんじになっています。
type Sender interface {
Send(msg *Message) (Receiver, error)
}
func (s *Sender) Send(msg *libchan.Message) (libchan.Receiver, error)
type Receiver interface {
Receiver(mode int) (*Message, error)
}
func (r *Receiver) Receive(mode int) (*libchan.Message, error)
主要な実装は以下
inmem (In-memory Go channel)
inmem_test.goを参考に実装してみます。
まずはlibchan.Pipe()
を使って、receiver/senderを取得します。
receiver, sender := libchan.Pipe()
送信側はgorutine内でsender経由でメッセージを送信して、送信側から受信したメッセージに対してさらに返信します。
go func() {
recv, _ := sender.Send(&libchan.Message{
Data: []byte("Hello"),
Ret: libchan.RetPipe,
)
msg, _ := recv.Receive(0) // mode=0で自動Close()
fmt.Println(string(msg.Data)) // "World"
}()
受信側はreceiver経由でメッセージ受信して、それに対して返信します。
msg, err := receiver.Receive(libchan.Ret)
fmt.Println(string(msg.Data)) // "Hello"
// msg.Retが返信用のchannelとなっている
_, err := msg.Ret.Send(&libchan.Message{Data: []byte("World")})
unix/http2の実装
それぞれにテストコードがあるので覗いてみるとだいたいのイメージがつかめます。
まとめ
libchanにおけるchannelとgoのchannel、名前は同じでも別モノです。 今のところこれといった用途が思いつかないですが、libswarmから派生したものとすると多様なプロトコルの差異を吸収するためのアダプター実装を手助けしてくれるレイヤーと考えるのがよさそうです。
というか、DockerConでちょくちょく言及されていた「マイクロサービス」という概念のほうが興味あります。
追記
そういえば書き忘れてたけど、アプリケーションが成長する過程においてlibchanの統一的なインターフェースによって比較的容易にスケールできるんだよ、ってことをDockerConのキーノートでいってたような。。。あとでみかえそう。
— Masaki YOSHIDA (@ReSTARTR) 2014, 6月 28
ということに気づき、再度キーノートを見返してました。libchanについては以下動画の30:45あたりからどうぞ。