Docker謹製ライブラリのlibchanについて調べてみた

DockerCon2014で発表されたlibchanについて調べたことをまとめてみます。

libchanはlibcontainerlibswarmと共に発表されました。 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。

1
2
3
4
5
type libchan.Message struct {
  Data []byte // メッセージの内容
  Fd *os.File
  Ret Sender  // 受信者が返答するためのchannel
}

このメッセージ型を、各種通信方式に対応したSender/Receiver経由でやりとりすることになります。

メッセージの構造化

メッセージは以下のようにして生成します。

1
message := libchan.Message{Data: []byte("Hello, libchan"), Ret: libchan.RetPipe}

で、ただのテキストではなく構造化されたデータも送れます。

1
2
3
4
5
6
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は擬似コードです)

1
sender.Send(&libchan.Message{Data: []byte(msg)})

で、Receiverが受信したメッセージはdata.Decode()map[string][]stringに変換できます。

1
2
3
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を実装したものを使います。 定義はだいたいこんなかんじになっています。

1
2
3
4
5
6
7
8
9
10
11
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を取得します。

1
receiver, sender := libchan.Pipe()

送信側はgorutine内でsender経由でメッセージを送信して、送信側から受信したメッセージに対してさらに返信します。

1
2
3
4
5
6
7
8
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経由でメッセージ受信して、それに対して返信します。

1
2
3
4
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については以下動画の30:45あたりからどうぞ。

Dockerを使って任意のrubyバージョンのrpmを作成する

CentOSを使ってて不便なのがruby2系のインストール。

rbenvやruby-buildを使うかソースからインストールするのですが、この場合、環境を構築するたびにビルドすることになり時間もかかるし大変面倒です。

なのでDockerを使ってクリーンな環境でRubyの最新rpmをビルドしてみました。

Dockerを使えばビルド環境がすぐに立ち上げられるので、rpmビルドに必要なパッケージを確認したりするのに最適です。

今回のソースはgithubにpushしてあります。

環境

  • ゲストOS:Vagrantで起動したUbuntu-14.04
  • コンテナ:centos:6.4

ファイルの配置

以下ファイルを同一ディレクトリに入れておきます。

  • Vagrantfile
  • provision.sh
  • Dockerfile
  • rubybuild.sh
  • ruby-2.0.0-p481.spec

手順

  1. ゲストOSの起動(Vagrant)
  2. ゲストOSにsshログイン
  3. Dockerコンテナのビルド
  4. Dockerコンテナ内でrpmビルド

1. ゲストOSの起動

Vagrantfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- mode: ruby -*-
# vi: set ft=ruby :
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "ubuntu/trusty64"

  config.vm.synced_folder ".", "/vagrant"

  config.vm.provision "shell", path: 'provision.sh'

  # お好みで
  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id, "--memory", "2048", "--cpus", "2", "--ioapic", "on"]
  end
end

provision.sh

1
2
3
4
5
6
sudo apt-get update
sudo apt-get install -y language-pack-ja

# Docker
sudo apt-get install -y docker.io
sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker

2. ゲストOSにsshログイン

1
2
3
$ vagrant up
$ vagrant ssh
$ cd /vagrant

3. Dockerコンテナのビルド

ビルド用specファイルを用意して、シェルスクリプトでビルドします。

すると、コンテナにマウントしたボリュームにビルド済みのrpmファイルがコピーされます。

ビルド用specシェルスクリプト

環境変数RUBY_VERSIONに”2.0.0-p481”や”2.0.0-p451”を設定すれば任意のバージョンをビルドします。

※デフォルトの2.0.0-p481以外はそれ用のspecファイルが必要になります。

rubybuild.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/env bash
WORKDIR=/opt/rpmbuild

RUBY_VER=${RUBY_VER:-"2.0.0-p481"}
RUBY_MAJOUR_VER=$(echo $RUBY_VER|cut -d"-" -f1)
RUBY_MINOR_VER=$(echo $RUBY_VER|cut -d"-" -f2)
if [ -z "$RUBY_MINOR_VER" ]; then
  RUBY_TARBALL="ruby-$RUBY_MAJOUR_VER.tar.gz"
else
  RUBY_TARBALL="ruby-$RUBY_MAJOUR_VER-$RUBY_MINOR_VER.tar.gz"
fi
RUBY_REMOTE_FILE="http://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOUR_VER:0:3}/$RUBY_TARBALL"

mkdir -p $WORKDIR/rpm/{BUILD,SRPMS,SPECS,SOURCES,RPMS}
echo "%_topdir $WORKDIR/rpm" > $WORKDIR/.rpmmacros
if [ ! -f $WORKDIR/rpm/SOURCES/$RUBY_TARBALL ]; then
  wget $RUBY_REMOTE_FILE -O $WORKDIR/rpm/SOURCES/$RUBY_TARBALL
fi
cp $WORKDIR/ruby-${RUBY_VER}.spec $WORKDIR/rpm/SPECS/ruby.spec
rpmbuild -bb $WORKDIR/rpm/SPECS/ruby.spec

cp $WORKDIR/rpm/RPMS/x86_64/* /shared/

4. Dockerコンテナ内でrpmビルド

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM centos:6.4

# setup
RUN yum update -y
RUN yum install -y rpm-build gcc

# epel
RUN rpm --import http://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6
RUN rpm -ihv http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm

# ruby-depends
RUN yum install —enablerepo=epel -y libyaml-devel
RUN yum install -y byacc readline-devel ncourses-devel tcl-devel openssl-devel gdbm-devel db4-devel

# build rpm
WORKDIR /opt/rpmbuild
ADD rubybuild.sh /opt/rpmbuild/rubybuild.sh
ADD ruby-2.0.0-p481.spec /opt/rpmbuild/ruby-2.0.0-p481.spec

コンテナ起動してrpmをビルド

コンテナを起動する際に、カレントディレクトリをマウントしておきます。

--rmオプションでrpm作成完了したらコンテナは破棄します

1
2
$ sudo docker build -t <USERNAME>/rpmbuild .
$ sudo docker run --rm -v $PWD:/shared:rw -it <CONTAINER_ID> /bin/sh ./rubybuild.sh

成功すれば、ゲストOSのカレントディレクトリにrpmができてます。

1
2
$ ls *.rpm
ruby-2.0.0p481-2.el6.x86_64.rpm

これで、rpmコマンド一発で最新のrubyをインストールできます。

rubyインストール済みコンテナを作るなら、Dockerfileに以下のように書けばruby実行環境のできあがりです。

1
2
3
4
5
6
7
8
9
10
FROM centos:6.4

# EPEL
RUN rpm --import http://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6
RUN rpm -ihv http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm

# Install ruby rpm
ADD ruby-2.0.0p481-2.el6.x86_64.rpm ruby-2.0.0p481-2.el6.x86_64.rpm
RUN yum install -y --enablerepo=epel libyaml-devel
RUN rpm -ihv ruby-2.0.0p481-2.el6.x86_64.rpm