Jekyll2022-01-13T14:11:00+00:00http://blog.hirokikana.com/feed.xmlhiroki.kanaの日常hiroki.kanaの何気ない日常Kolla Ansible を利用して VirtualBox 上の仮想マシンに OpenStack をデプロイする2022-01-13T22:50:00+00:002022-01-13T22:50:00+00:00http://blog.hirokikana.com/dev/kolla-ansible<h2 id="kolla-ansible-とは">Kolla Ansible とは</h2>
<p>Kolla Ansible とは OpenStack の各サービスやコンポーネントを Docker コンテナの形で導入する仕組みです。</p>
<p>もともとは <a href="https://logmi.jp/tech/articles/320926">OpenStackコンポーネントをKubernetes上に構築 Yahoo! JAPAN巨大インフラの運用と舞台裏 - Part1 - ログミーTech</a> この OpenStack のコンポーネントをコンテナにし Kubernetes で管理している仕組みをみて、自前でコンテナイメージを作ろうとしていました。</p>
<p>その中で、 Kolla Ansible の存在を知り「これでいいじゃん」となり利用してみたという経緯です。
実際に利用してみたところ(手動導入時と比べて)非常に簡単に OpenStack を導入することができたため、利用方法の備忘録として残すことにしました。</p>
<h2 id="環境">環境</h2>
<p>私は色々考える中で物理マシンを買いがちなのですが、色々試すという意味ではやはり仮想マシンで作成するのが本当に楽です。
NIC やディスクが手軽に追加できたりしますしね。</p>
<p>今回は macOS 上の VirtualBox を vagrant から利用して検証をしました。
理屈上、 Windows でも動くとは思うんですが、検証してないのとちょっとした癖があってこのままの手順ではないかもしれません。</p>
<h2 id="virtualbox-特有の設定等">VirtualBox 特有の設定等</h2>
<p>VirtualBox 特有の設定としては下記があります。</p>
<ul>
<li>Nested Virtualization を有効にする</li>
<li>それぞれの NIC でプロミスキャスモードを許可する</li>
<li>ディスクのサイズを拡張する</li>
</ul>
<p>最近の VirtualBox のバージョンにおいては、Intel / AMD どちらのプロセッサにおいても Nested Virtualization が利用できます。
そのため、各仮想マシンにおいては Nested Virtualization を有効にしておきましょう。</p>
<pre><code class="language-Vagrantfile"> config.vm.provider "virtualbox" do |vb|
(略)
vb.customize ["modifyvm", :id, "--nested-hw-virt", "on"]
</code></pre>
<p>プロミスキャスモードですが、許可されていない場合 Floating IP アドレスが利用できない状態になります。
そのため、仮想マシンの設定において各 NIC のプロミスキャスモードを許可しておきます。</p>
<pre><code class="language-Vagrantfile"> vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
vb.customize ["modifyvm", :id, "--nicpromisc3", "allow-all"]
</code></pre>
<p>ディスクのサイズは何もしないと 10 GB で作成されます。
公式ドキュメント (参考リンク[1]) の Host machine requirements にあるように 40GB に変更しておきます。</p>
<pre><code class="language-Vagrantfile"> server.vm.disk :disk, size: "40GB", primary: true
</code></pre>
<p>上記をしていした場合、 <code class="language-plaintext highlighter-rouge">vagrant up</code> をする際に環境変数において明示的に有効化する必要があります。
起動の際は <code class="language-plaintext highlighter-rouge">VAGRANT_EXPERIMENTAL="disks" vagrant up</code> と指定することで有効になります。(参考リンク[2])
また、起動したあとにおいて OS 上で拡張する必要もあるため、Provisioning からシェルを実行し拡張するようにします。</p>
<pre><code class="language-Vagrantfile"> config.vm.provision "shell", inline: <<-SHELL
yum update -y ; yum install -y openssh-server python3-devel libffi-devel gcc openssl-devel python3-libselinux git python3-pip cloud-utils-growpart
(略)
growpart /dev/sda 1
xfs_growfs -d /
</code></pre>
<p>上記の内容を含めた Vagrantfile は下記です。</p>
<pre><code class="language-Vagrantfile"># -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "centos/stream8"
config.vm.define "ctrl01" do |server|
server.vm.hostname = "ctrl01"
server.vm.network "private_network", ip: "192.168.56.100"
server.vm.network "private_network", type: "dhcp", auto_config: false, name: 'vboxnet1'
server.vm.disk :disk, size: "40GB", primary: true
end
config.vm.define "comp01" do |server|
server.vm.hostname = "comp01"
server.vm.network "private_network", ip: "192.168.56.101"
server.vm.network "private_network", type: "dhcp", auto_config: false, name: 'vboxnet1'
server.vm.disk :disk, size: "40GB", primary: true
end
config.vm.define "comp02" do |server|
server.vm.hostname = "comp02"
server.vm.network "private_network", ip: "192.168.56.102"
server.vm.network "private_network", type: "dhcp", auto_config: false, name: 'vboxnet1'
server.vm.disk :disk, size: "40GB", primary: true
end
config.vm.provider "virtualbox" do |vb|
vb.memory = "8192"
vb.customize ["modifyvm", :id, "--nested-hw-virt", "on"]
vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
vb.customize ["modifyvm", :id, "--nicpromisc3", "allow-all"]
end
config.vm.provision "shell", inline: <<-SHELL
yum update -y ; yum install -y openssh-server python3-devel libffi-devel gcc openssl-devel python3-libselinux git python3-pip cloud-utils-growpart
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
sed -i s/PasswordAuthentication\\ no/PasswordAuthentication\\ yes/ /etc/ssh/sshd_config
systemctl restart sshd
growpart /dev/sda 1
xfs_growfs -d /
dnf remove -y python3-pyyaml
pip3 install -U pip
pip3 install 'ansible<5.0'
ip addr flush eth2
SHELL
end
</code></pre>
<p>その他にも sshd でパスワード認証を有効にしたり、Ansible を導入したりして定型的に処理できる部分はここで実施されるようにしておきます。
上記の構成ではコントローラーノード 1 台とコンピュートノード 2 台が作成されます。
手元の環境 (Mac mini 2018 / Mem 32 GB) ではこれが全部立ち上がるとメモリリソースが結構厳しい感じになりました。</p>
<h2 id="コンピュートノードにパスワードなしでログインできるようにする">コンピュートノードにパスワードなしでログインできるようにする</h2>
<p>コンピュートノードに対しては Ansible で導入を実行するわけですが、その際パスワード認証を利用していると毎回パスワードを聞かれてしまい、作業になりません。
そのため、コンピュートノードには鍵認証でログインできるようにしておきます。</p>
<p>まず、root で SSH ログインをできるようにしておきます。
デフォルトでは root ログインもパスワード認証も許可されていないので、sshd の設定を変更する必要がありますが、そこは Provisioning の際の shell で実行しておきます。</p>
<pre><code class="language-Vagrantfile"> echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
sed -i s/PasswordAuthentication\\ no/PasswordAuthentication\\ yes/ /etc/ssh/sshd_config
systemctl restart sshd
</code></pre>
<p>このうえで、各コンピュートノードで root のパスワードも設定しておきます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant ssh comp01
sudo passwd
</code></pre></div></div>
<p>そのうえで、コントローラーノードの root の公開鍵を登録し、パスワードなしでログインできるようにします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant ssh ctrl01
sudo su -
ssh-keygen
ssh-copy-id root@192.168.56.101 #comp01
ssh-copy-id root@192.168.56.102 #comp02
</code></pre></div></div>
<p>最後にそれぞれのコンピュートノードにパスワードなしでログインすることが可能であることを確認しておきます。</p>
<h2 id="kolla-ansible-を利用してセットアップ">Kolla Ansible を利用してセットアップ</h2>
<p>基本的には Quick Start (参考リンク[1]) の内容のとおりではあるのですが、手元で検証した際の流れを紹介します。
Python の venv は利用せず、 OS に予め導入されている Python を利用しました。
そのため、 Quick Start を参照される際は <code class="language-plaintext highlighter-rouge">If not using a virtual environment:</code> とある方の手順を進めていく感じです。</p>
<p>Ansible の導入までは Provisioning で終了しているので Quick Start の <code class="language-plaintext highlighter-rouge">Install Kolla-ansible</code> の内容を進めていきます。
手順をすすめるにあたり、私は <code class="language-plaintext highlighter-rouge">sudo su -</code> して root になって作業をしました。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip3 install git+https://opendev.org/openstack/kolla-ansible@master
mkdir -p /etc/kolla
chown $USER:$USER /etc/kolla
cp -r /usr/local/share/kolla-ansible/etc_examples/kolla/* /etc/kolla
cp /usr/local/share/kolla-ansible/ansible/inventory/* .
</code></pre></div></div>
<p>上記を実行すると、Kolla Ansible が導入され、必要な設定ファイルが <code class="language-plaintext highlighter-rouge">/etc/kolla</code> 以下に、カレントディレクトリにノードの設定を行う <code class="language-plaintext highlighter-rouge">all-in-one</code> と <code class="language-plaintext highlighter-rouge">multinode</code> のファイルがコピーされます。</p>
<p>まず、ノードの設定では <code class="language-plaintext highlighter-rouge">multinode</code> を利用します。
<code class="language-plaintext highlighter-rouge">multinode</code> においては <code class="language-plaintext highlighter-rouge">[compute]</code> で今回作成した <code class="language-plaintext highlighter-rouge">192.168.56.101</code> / <code class="language-plaintext highlighter-rouge">192.168.56.102</code> を指定し、その他の項目については <code class="language-plaintext highlighter-rouge">localhost</code> を指定します。</p>
<pre><code class="language-multinode">[control]
localhost ansible_connection=local
[network]
localhost ansible_connection=local
[compute]
192.168.56.101
192.168.56.102
[monitoring]
localhost ansible_connection=local
[storage]
localhost ansible_connection=local
[deployment]
localhost ansible_connection=local
(略)
</code></pre>
<p>次に Kolla Ansible の設定を行うため、 <code class="language-plaintext highlighter-rouge">/etc/kolla/globals.yml</code> を編集します。
ここでは変更点だけ紹介します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kolla_base_distro: "centos"
kolla_install_type: "binary"
</code></pre></div></div>
<p>まず <code class="language-plaintext highlighter-rouge">kolla_base_distro</code> は CentOS であるため、<code class="language-plaintext highlighter-rouge">centos</code> を指定します。
<code class="language-plaintext highlighter-rouge">kolla_install_type</code> ですが、デフォルトでは <code class="language-plaintext highlighter-rouge">source</code> でドキュメントによると <code class="language-plaintext highlighter-rouge">source</code> のほうが信頼性が高いとのことです。
私は、きっと <code class="language-plaintext highlighter-rouge">binary</code> のほうがビルドとかたぶんしないんじゃないかという根拠のない予想から、早く作業が完了しそうと考え <code class="language-plaintext highlighter-rouge">binary</code> にしました。(実際のところは測定していません)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kolla_internal_vip_address: "192.168.56.100"
network_interface: "eth1"
neutron_external_interface: "eth2"
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">kolla_internal_vip_address</code> は <code class="language-plaintext highlighter-rouge">network_interface</code> で指定した NIC の IP アドレスを指定します。
<code class="language-plaintext highlighter-rouge">network_interface</code> は、今回の構成でいうところの Management network で利用する NIC を指定します。
作成された VM の eth0 はインターネットと疎通させるために NAT の NIC にしているので、今回の場合は eth1 を指定します。
<code class="language-plaintext highlighter-rouge">neutron_external_interface</code> は Neutron でパブリックネットワークと接続する NIC を指定するわけですが、今回の場合は eth2 を指定します。
ネットワーク構成の名称と役割については Oracle Openstack の Configraion Guide にわかりやすい図がありました。(参考リンク [3])</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>enable_haproxy: "no"
</code></pre></div></div>
<p>これはちょっと深堀りできていないのですが、 <code class="language-plaintext highlighter-rouge">enable_haproxy</code> が <code class="language-plaintext highlighter-rouge">yes</code> の場合失敗したので <code class="language-plaintext highlighter-rouge">no</code> にしました。
おそらく複数のコントローラーノードのときに利用するものであると認識しているのですが、よく調べていないので間違っているかもしれません。
ただ、今回利用する範囲では <code class="language-plaintext highlighter-rouge">no</code> にしても支障はなかったです。</p>
<p>Quick Start には <code class="language-plaintext highlighter-rouge">enable_cinder</code> に関して記載がありますが、今回は Cinder を利用せず最小限の構成で動作をさせます。</p>
<p>ここまで終わればあとは下記のコマンドを実行し、すべての Ansible の処理が正常に完了すれば導入完了です。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kolla-genpwd
kolla-ansible -i ./multinode bootstrap-servers
kolla-ansible -i ./multinode prechecks
kolla-ansible -i ./multinode deploy
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">deploy</code> が一番時間がかかるのでしばらく放置しておきます。</p>
<h2 id="実際に-openstack-を利用する">実際に OpenStack を利用する</h2>
<p>最後に admin ユーザの作成等を実施します。
まず作業を始める前に OpenStack Client が必要となるため、インストールします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install python-openstackclient -c https://releases.openstack.org/constraints/upper/master
</code></pre></div></div>
<p>次に admin ユーザの作成等の作成を実施します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kolla-ansible post-deploy
</code></pre></div></div>
<p>この状態で <code class="language-plaintext highlighter-rouge">. /etc/kolla/admin-openrc.sh</code> すると OpenStack が利用できる形となります。
また、<code class="language-plaintext highlighter-rouge">http://192.168.56.100</code> にアクセスすると Horizon のログイン画面が表示されると思います。
ユーザ名に <code class="language-plaintext highlighter-rouge">admin</code> パスワードには <code class="language-plaintext highlighter-rouge">/etc/kolla/admin-openrc.sh</code> 内の <code class="language-plaintext highlighter-rouge">OS_PASSWORD</code> に設定されている値を利用し、ログインが可能になっていると思います。</p>
<p>最後に動作確認のための OS イメージやネットワークを <code class="language-plaintext highlighter-rouge">/usr/local/share/kolla-ansible/init-runonce</code> を利用して用意します。
ただ、このファイルを実行すると外部ネットワークの IP アドレスが異なるので、手元にコピーし編集したうえで実行します。</p>
<pre><code class="language-init-runonece">EXT_NET_CIDR=${EXT_NET_CIDR:-'192.168.56.0/24'}
EXT_NET_RANGE=${EXT_NET_RANGE:-'start=192.168.56.170,end=192.168.56.199'}
EXT_NET_GATEWAY=${EXT_NET_GATEWAY:-'192.168.56.1'}
</code></pre>
<p>最後に <code class="language-plaintext highlighter-rouge">openstack server create --image cirros --flavor m1.tiny --key-name mykey --network demo-net demo1</code> を実行するとインスタンスが作成されます。
作成されたインスタンスは Floating IP アドレスを付与することで VM のホストマシンからも疎通できることが確認できると思います。</p>
<h2 id="まとめ">まとめ</h2>
<p>OpenStack の導入はコマンドひとつで導入できず、時間や手間、導入の段階でトラブルシューティングが必要であったかと思います。
しかし、この方法であれば割と簡単に導入することができました。
また、それぞれのモジュールが Docker コンテナの形で動作するため、必要なくなったらコンテナを削除するだけできれいな環境に戻すことができたりする点も利点かと思います。</p>
<h2 id="参考リンク">参考リンク</h2>
<ul>
<li>[1] <a href="https://docs.openstack.org/kolla-ansible/latest/user/quickstart.html">Quick Start kolla-ansible 13.1.0.dev131 documentation</a></li>
<li>[2] <a href="https://www.vagrantup.com/docs/disks/usage">Vagrant Disk Usage Vagrant by HashiCorp</a></li>
<li>[3] <a href="https://docs.oracle.com/cd/E90981_01/E90982/html/kolla-openstack-network.html">Configuring Network Interfaces for OpenStack Networks</a></li>
</ul>Kolla Ansible とは Kolla Ansible とは OpenStack の各サービスやコンポーネントを Docker コンテナの形で導入する仕組みです。最低限の理解でDNSサーバーを実装する2019-10-22T04:50:00+00:002019-10-22T04:50:00+00:00http://blog.hirokikana.com/dev/femtDNS<h2 id="はじめに">はじめに</h2>
<p>私は、雰囲気でDNSを使っていました。</p>
<p>最近雰囲気で使い続けることに限界を感じ、少しでも雰囲気で使ってる部分を解消するためにDNSサーバーを実装しました。とはいえ、DNSに関連するRFCを見ると、かなりのバリエーションがあり見れば見るほど私は雰囲気で使っていたと自覚するとともに、やはり私は雰囲気で使うことを抜けることはまだまだできないなと思うばかりでした。</p>
<p>まずはDNSメッセージのフォーマットを紹介し、最後に理解したDNSメッセージのフォーマットを元に作ったDNSサーバーの<a href="https://github.com/hirokikana/femtoDNS">femtoDNS</a>を紹介します。</p>
<h2 id="dnsメッセージフォーマット">DNSメッセージフォーマット</h2>
<p><a href="https://tools.ietf.org/html/rfc1035#section-4.1">RFC 1035 4.1 Format</a> にDNSパケットのフォーマットが定義されています。
下記はパケットの構成を示した図の引用です。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> +---------------------+
| Header |
+---------------------+
| Question | the question for the name server
+---------------------+
| Answer | RRs answering the question
+---------------------+
| Authority | RRs pointing toward an authority
+---------------------+
| Additional | RRs holding additional information
+---------------------+
</code></pre></div></div>
<p>とりあえず正引きができれば良いという目的から、Header / Question / Answerのみに今回は注目しました。
かなり雑な列挙ですが、ひとまずこれがわかればなんとなくどうにかなります……。</p>
<h3 id="header-セクション">Header セクション</h3>
<p>Header セクションはDNSパケットには必ず存在し、パケットの内容についての情報が格納されています。</p>
<p><a href="https://tools.ietf.org/html/rfc1035#section-4.1.1">RFC 1035 4.1.1. Header section format</a>からHeader セクションの構成について引用した図こちらです。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
</code></pre></div></div>
<p>ID / QDCOUNT / ANCOUNT / NSCOUNT / ARCOUNTはそれぞれ16bit(2byte)で、IDの後方16bitにフラグ群が含まれています。
すべての項目について説明することが望ましいのですが、今回はレスポンスメッセージを作る際に意識する必要があった部分だけ列挙します。</p>
<ul>
<li>ID
<ul>
<li>問い合わせごとに付与されるID</li>
<li>リクエスト元はこのIDが送ったときと同一であるかを確認するので、レスポンスには同じリクエストと同じIDを付与する</li>
</ul>
</li>
<li>QR
<ul>
<li>0は問い合わせ、1はレスポンス</li>
</ul>
</li>
<li>RCODE
<ul>
<li>レスポンスコード。正常な場合は0</li>
</ul>
</li>
<li>QDCOUNT
<ul>
<li>Question セクションのエントリー数</li>
</ul>
</li>
<li>ANCOUNT
<ul>
<li>Answer セクションのエントリー数</li>
</ul>
</li>
</ul>
<h3 id="question-セクション">Question セクション</h3>
<p>どのような内容の問い合わせであるかを格納するセクションです。
ヘッダ内のQDCOUNT には、このQestion セクションのエントリー数が保存されています。
このセクションは、その内容から多くの場合は1つなので、QDCOUNT も多くの場合は1です。</p>
<p>Question セクションはQNAME / QTYPE / QCLASS の3つから構成されています。</p>
<ul>
<li>QNAME
<ul>
<li>ドメイン名の情報がラベル(FQDNを.で分割した文字列)という単位で分割されている</li>
<li>1オクテット(8bit = 1byte)目にラベルの長さが定義され、ラベル本体と続く</li>
<li>1オクテットの0がQNAMEの末尾のマーク</li>
</ul>
</li>
<li>QTYPE
<ul>
<li>問い合わせレコードのタイプ</li>
<li>Aレコードの場合は1</li>
<li>詳細はこちら <a href="https://tools.ietf.org/html/rfc1035#section-3.2.2">3.2.2. TYPE values</a></li>
</ul>
</li>
<li>QCLASS
<ul>
<li>問い合わせレコードのクラス</li>
<li>概ねはインターネットを示すINなので1</li>
</ul>
</li>
</ul>
<h3 id="answer-セクション">Answer セクション</h3>
<p>問い合わせを受けて、正常に引けた場合の返答となるリソースレコード(RFC 1035 ではRRと書かれています)を格納するセクションです。
リソースレコードのフォーマットについてRFC 1035から引用した図はこちらです。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
</code></pre></div></div>
<p>それぞれのフィールドは下記のような意味があります。</p>
<ul>
<li>NAME
<ul>
<li>レコードのドメイン名</li>
<li>フォーマットはQNAMEと同一</li>
</ul>
</li>
<li>TYPE
<ul>
<li>レコードのタイプ</li>
<li>フォーマットはQTYPEと同一</li>
</ul>
</li>
<li>CLASS
<ul>
<li>レコードのクラス</li>
<li>フォーマットはQCLASSと同一</li>
</ul>
</li>
<li>TTL
<ul>
<li>レコードの有効期限(秒)</li>
</ul>
</li>
<li>RDLENGTH
<ul>
<li>リソースデータの長さ</li>
<li>IPv4アドレスの場合は4オクテット</li>
</ul>
</li>
<li>RDATA
<ul>
<li>実際のレコードのデータ</li>
</ul>
</li>
</ul>
<h2 id="femtodnsの概要">femtoDNSの概要</h2>
<p>ここまで私がDNSメッセージのフォーマットについてかなり端折って紹介しましたが、ここまでの理解でDNSサーバーを実装しました。
実際に常用できるところまで実装するとなると、かなり道のりが先になりそうだったので下記を目的としました。</p>
<ul>
<li>dig コマンドで問い合わせができる</li>
<li>localhost の問い合わせに対して 127.0.0.1 Aレコードを返す</li>
<li>ファイルにIPアドレスとホスト名のマッピングを追記すると、その内容を元にAレコードを返す
Pythonのsocketserverモジュールを利用してUDPサーバーを実装しました。</li>
</ul>
<h3 id="動作例">動作例</h3>
<p>Python 3.7以上があれば動作させることができます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git clone git@github.com:hirokikana/femtoDNS.git
$ cd femtoDNS
$ python3 start.py
</code></pre></div></div>
<p>この状態で、ポート番号9999でUDPサーバーが立ち上がります。
digを使って問い合わせしてみます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ dig @127.0.0.1 -p 9999 localhost
; <<>> DiG 9.10.6 <<>> @127.0.0.1 -p 9999 localhost
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13985
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;localhost. IN A
;; ANSWER SECTION:
localhost. 255 IN A 127.0.0.1
;; Query time: 2 msec
;; SERVER: 127.0.0.1#9999(127.0.0.1)
;; WHEN: Tue Oct 22 12:24:11 JST 2019
;; MSG SIZE rcvd: 52
</code></pre></div></div>
<p>ANSWER SECTIONにリソースレコードが含まれる形でレスポンスがあったことが確認できます。</p>
<h2 id="まとめ">まとめ</h2>
<p>今回の実装はRFC 1035に書かれてることのほんの一部しか実装していません。
しかし、普段利用しているdigを利用して、正しい形に見えるレスポンスが返ってくるとちゃんと実装できたという満足感があります。
最後に、socketserverモジュールは余計なことを気にせずTCP/UDPサーバーを実装できるので雑にサーバーを実装するときはおすすめです。</p>
<h2 id="参考サイト">参考サイト</h2>
<ul>
<li><a href="https://tools.ietf.org/html/rfc1035">RFC 1035 - Domain names - implementation and specification</a></li>
<li><a href="https://www.atmarkit.co.jp/ait/articles/1601/29/news014.html">DNS Tips:DNSパケットフォーマットと、DNSパケットの作り方 (1/2)</a></li>
<li><a href="https://qiita.com/n-i-e/items/482dee0706547dcc6a95">Qiita - DNS関連RFC一覧</a></li>
</ul>はじめに 私は、雰囲気でDNSを使っていました。TOP500と同じベンチマークをMacBook Air上の仮想マシンで実行する2019-10-06T06:00:00+00:002019-10-06T06:00:00+00:00http://blog.hirokikana.com/dev/hpl<p>用途もないのにスペックの高いマシンを買って、時間のかかるミドルウェア等のコンパイルやメディアのエンコードの速度が向上しているのを見ると楽しいと思いませんか?私は楽しいです。
さて、そのような趣向がある私ですが、もう少し比較対象が多いベンチマークをやってみたくなり、TOP500に注目しました。</p>
<p>TOP500は、よくスーパーコンピュータの性能ランキングの話の際に出てくるアレです。「2位じゃダメなんですか」的な話のときにも話題になったアレです。
今まで勝手にいわゆる建物ごと作るようなスーパーコンピュータでないと、ベンチマークすら走らせられないのかなと勝手に思っていたのですが、よくよく調べてみると手元のマシンでも動かせることがわかったので、今回はその記録です。</p>
<h2 id="top500とは">TOP500とは</h2>
<p><a href="https://www.top500.org/">TOP500</a>とはスーパーコンピュータの性能ランキングとしてよく話題に出ていますが、実際にはHPLというベンチマークの結果のランキングであることがわかりました。
HPLはLINPACKというベンチマークを並列に実行するために新たに作られたもので、TOPS50ではそれがずっと使われているようです。
<a href="https://www.top500.org/lists/2019/06/">ランキング</a>は公開されており、どの国のどのようなシステムがどのようなハードウェアで実行し、どのような結果であったかということが参照できます。</p>
<p>ランキングを見ると、2019年6月ランキングの<a href="https://www.top500.org/site/50808">136位</a>にはAmazon EC2 C5 instanceを使ってクラスタを組んだ構成がランクインしています。
これを見るとなんとなく、自分でも現実的に用意できるのではないかという気持ちになりませんか?Amazon EC2 C5 instanceなら買おうと思えば、一応すぐ用意できそうじゃないですか(そんな大量に立ち上げるお金ないですが……
)。</p>
<h2 id="目的">目的</h2>
<p>今回は、私が持ってるような誰でも買えるマシンを使って、TOP500で利用されているHPLで性能測定をするということを目的としています。
数年前の500位以内に入るようにどうこうしようとか、そのようなことは全く考えていないです。</p>
<p>実際に何をやっているか理解しないで値だけを見るというのは、自分のふるまいとして正しいのかという気持ちにはなりましたが、まあまず動かすところからかな……ということで気持ちを落ち着けました。</p>
<h2 id="環境">環境</h2>
<p>下記の環境で検証を実施しました。</p>
<ul>
<li>Host
<ul>
<li>MacBook Air / 1.3 GHz Intel Core i5 / 8 GB 1600 MHz DDR3</li>
</ul>
</li>
<li>Guest
<ul>
<li>VirtualBox 6.0.12</li>
<li>CentOS 7.2 / 1 GB</li>
</ul>
</li>
</ul>
<p>HPLは<a href="https://www.netlib.org/benchmark/hpl/">テネシー大学の Innovative Computing Laboratory (ICL)で実装が提供</a>されています。これをベースにIntel MKLを利用した最適化されたバイナリが提供されており、試すだけならこれが一番手軽に試せると思います。今回は動かすのが目的ですし、せっかくなのでICL提供のものを利用してみることにしました。
HPLはMPI(Message Passing Interface / 1.1準拠)とBLAS(Basic Linear Algebra Subprograms)もしくはVSIPL(Vector Signal Image Processing Library)の実装が必要です。一貫性がないことを言いますが、これはソースから入れるのがめんどくさかったのでCentOSで提供されているものyumでインストールしました。</p>
<ul>
<li>HPL 2.3</li>
<li>OpenMPI 1.10.0</li>
<li>OpenBLAS 0.2.15</li>
</ul>
<h2 id="構築">構築</h2>
<p>CentOSの環境がある前提で進めます。
まず、MPIとBLASの実装であるOpenMPI / OpenBLASをyumでインストールします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo yum install -y openmpi openblas-devel openblas* openmpi-devel
export PATH=$PATH:/usr/lib64/openmpi/bin
</code></pre></div></div>
<p>OpenMPIに含まれるmpicc / mpirunを利用するのですが、なぜか/usr/lib64/openmpi/bin以下に配置されているので、PATHを通しておきます。
恒常的に設定するには.bashrc等に含めておく必要があります。</p>
<p>ここまででHPLをコンパイルする準備が整いました。
HPLをダウンロード・展開します。Makefileが環境ごとにいくつかあるのですが、参考サイトにあったものを利用しました。
名前からPentium IIでCBLASをを利用する際に適切な設定がされているものなのでしょうか。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://www.netlib.org/benchmark/hpl/hpl-2.3.tar.gz
tar xzvf hpl-2.3.tar.gz
cd hpl-2.3
cp setup/Make.Linux_PII_CBLAS_gm .
</code></pre></div></div>
<p>環境に合わせてMakefileを編集する必要があります。下記がdiffで、ご自身の環境に合わせて設定してください。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>70c70
< TOPdir = /home/vagrant/hpl-2.3
---
> TOPdir = $(HOME)/hpl
95c95
< LAdir = /usr/lib64
---
> LAdir = $(HOME)/netlib/ARCHIVES/Linux_PII
97c97
< LAlib = $(LAdir)/libopenblas.a
---
> LAlib = $(LAdir)/libcblas.a $(LAdir)/libatlas.a
</code></pre></div></div>
<p>最後にコンパイルするとバイナリが出力されるので、実行します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make arch=Linux_PII_CBLAS_gm
cd bin/Linux_PII_CBLAS_gm/
mpirun -np 4 ./xhpl
</code></pre></div></div>
<p>下記のような出力が出るのですが、全部passedとなっているのできちんと動いています。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(略)
--------------------------------------------------------------------------------
||Ax-b||_oo/(eps*(||A||_oo*||x||_oo+||b||_oo)*N)= 3.53029565e-03 ...... PASSED
================================================================================
T/V N NB P Q Time Gflops
--------------------------------------------------------------------------------
WR00R2R4 1000 4 4 1 0.09 7.5169e+00
HPL_pdgesv() start time Sun Oct 6 03:55:03 2019
HPL_pdgesv() end time Sun Oct 6 03:55:03 2019
--------------------------------------------------------------------------------
||Ax-b||_oo/(eps*(||A||_oo*||x||_oo+||b||_oo)*N)= 4.02169456e-03 ...... PASSED
================================================================================
Finished 864 tests with the following results:
864 tests completed and passed residual checks,
0 tests completed and failed residual checks,
0 tests skipped because of illegal input values.
--------------------------------------------------------------------------------
End of Tests.
================================================================================
</code></pre></div></div>
<p>ベンチマークで利用するパラメーターはカレントディレクトリのHPL.datで指定するのですが、これが何を示しているのかまだ理解していません。
Intel MKLのドキュメントにある<a href="https://software.intel.com/en-us/articles/performance-tools-for-software-developers-hpl-application-note/">HPL Application Note</a>のTuningを雑に読んだところによると問題サイズとブロックサイズが重要な要素になっているように見受けられるので、クラスタの構成に合わせてチューニングしていくということなのでしょうか。</p>
<p>私が行った直近の結果を見ると7.5Gflopsとあるので、比較のために1994年11月TOP500のランキングを見たところ、<a href="https://www.top500.org/site/47530">89位のコンピュータ</a>が7.4Gflopsでした。1994年のスーパーコンピューターが持ち運べていると思うと、どこか感慨深いものがありますね。</p>
<h2 id="まとめ">まとめ</h2>
<p>単一のノードで実行しましたが、複数のノードで並列に実行するところから本番のようです。実際にどのあたりが勘所なのか理解が及んでいませんが、各種パラメータやクラスタの構成あたりがHPLで最適化・パフォーマンスチューニングを行ううえでの醍醐味なのだろうと思っています。</p>
<p>今後の目的としては、そもそもLinkpackベンチマークが何をしているか理解するあたりでしょうか。</p>
<h2 id="参考サイト">参考サイト</h2>
<ul>
<li><a href="https://www.netlib.org/benchmark/hpl/">HPL - A Portable Implementation of the High-Performance Linpack Benchmark for Distributed-Memory Computers</a></li>
<li><a href="http://fujish.hateblo.jp/entry/2018/01/11/003736">電子計算記 個人的な検証を - 15. MPIクラスターを作ろう! - HPLを動かしてみる</a></li>
<li><a href="https://software.intel.com/en-us/articles/intel-mkl-benchmarks-suite">Intel® Math Kernel Library (Intel® MKL) Benchmarks</a></li>
<li><a href="https://www.top500.org/">TOP500 Supercomputer Sites</a></li>
</ul>用途もないのにスペックの高いマシンを買って、時間のかかるミドルウェア等のコンパイルやメディアのエンコードの速度が向上しているのを見ると楽しいと思いませんか?私は楽しいです。 さて、そのような趣向がある私ですが、もう少し比較対象が多いベンチマークをやってみたくなり、TOP500に注目しました。LinuxカーネルをARM向けにビルドしてQEMUで起動する2019-05-06T06:45:00+00:002019-05-06T06:45:00+00:00http://blog.hirokikana.com/dev/runnning-linux-kernel-for-arm-on-qemu<h2 id="はじめに">はじめに</h2>
<p>Linuxのカーネルは誰でも参照できる形でソースコードが公開されています。そのため自らビルドを行い適用することができます。
しかし、私はほとんどの場合ディストリビューションで提供しているカーネルのアップデートパッチを当てるという以上のことをするというのは少なく、Linuxカーネルをビルドして起動するということを行う機会がほとんどありませんでした。</p>
<p>今回はLinuxカーネルのビルド・動作確認方法に関する理解とLinux起動プロセスの全体像を把握するため、可能な限り最低限の手順で最新版のカーネルを起動プロセスを理解するために行った内容を紹介します。せっかくなのでx86ではなくARM向けにビルドするためカーネルをクロスコンパイルを行う手順も合わせて行うこととしました。また、実際にハードウェアで確認するのは面倒なのでQEMUで確認します。</p>
<h2 id="準備">準備</h2>
<p>なんらかのPCと仮想マシンを起動できる環境があればこの手順は実行することができます。
私の場合は下記のような環境で行いました。</p>
<ul>
<li>Hardware
<ul>
<li>Mac mini(2018)
<ul>
<li>CPU: Core i7 3.2GHz</li>
<li>Memory: 32GB</li>
<li>OS: macOS 10.14.3 Mojave</li>
</ul>
</li>
</ul>
</li>
<li>Software
<ul>
<li>VirtualBox 5.2.22</li>
<li>Vagrant 2.2.1</li>
</ul>
</li>
</ul>
<p>仮想マシンを1台用意するために下記のようなVagrantfileを利用しました。仮想マシン上のディストリビューションはUbuntu 16.04です。</p>
<pre><code class="language-:Vagrantfile">Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-16.04"
config.vm.provider "virtualbox" do |vb|
vb.memory = "8192"
end
config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get upgrade
SHELL
end
</code></pre>
<p>仮想マシンを<code class="language-plaintext highlighter-rouge">vagrant up</code>で起動し<code class="language-plaintext highlighter-rouge">vagrant ssh</code>で仮想マシンに入れることを確認します。
仮想マシンでは下記コマンドでQEMUとカーネルのビルド/クロスコンパイルに必要なものをインストールしておきます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-get install -y qemu gcc-arm-linux-gnueabi build-essential bison flex libncurses-dev libelf-dev
</code></pre></div></div>
<h2 id="linuxカーネルのビルド">Linuxカーネルのビルド</h2>
<h3 id="エミュレーション可能なボードとdevice-tree">エミュレーション可能なボードとDevice Tree</h3>
<p>ARMといっても実際にARMが実装されているボードによってハードウェア固有の情報が異なります。そのために作業にあたりQEMUでエミュレート可能なボードを選択するのとそのボードのDevice Treeを用意しておく必要があります。</p>
<p>まず、QEMUでは下記コマンドで対応しているボードを確認することができます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ qemu-system-arm -M ?
Supported machines are:
akita Sharp SL-C1000 (Akita) PDA (PXA270)
borzoi Sharp SL-C3100 (Borzoi) PDA (PXA270)
canon-a1100 Canon PowerShot A1100 IS
cheetah Palm Tungsten|E aka. Cheetah PDA (OMAP310)
collie Sharp SL-5500 (Collie) PDA (SA-1110)
connex Gumstix Connex (PXA255)
cubieboard cubietech cubieboard
highbank Calxeda Highbank (ECX-1000)
imx25-pdk ARM i.MX25 PDK board (ARM926)
integratorcp ARM Integrator/CP (ARM926EJ-S)
kzm ARM KZM Emulation Baseboard (ARM1136)
lm3s6965evb Stellaris LM3S6965EVB
lm3s811evb Stellaris LM3S811EVB
mainstone Mainstone II (PXA27x)
midway Calxeda Midway (ECX-2000)
musicpal Marvell 88w8618 / MusicPal (ARM926EJ-S)
n800 Nokia N800 tablet aka. RX-34 (OMAP2420)
n810 Nokia N810 tablet aka. RX-44 (OMAP2420)
netduino2 Netduino 2 Machine
none empty machine
nuri Samsung NURI board (Exynos4210)
realview-eb ARM RealView Emulation Baseboard (ARM926EJ-S)
realview-eb-mpcore ARM RealView Emulation Baseboard (ARM11MPCore)
realview-pb-a8 ARM RealView Platform Baseboard for Cortex-A8
realview-pbx-a9 ARM RealView Platform Baseboard Explore for Cortex-A9
smdkc210 Samsung SMDKC210 board (Exynos4210)
spitz Sharp SL-C3000 (Spitz) PDA (PXA270)
sx1 Siemens SX1 (OMAP310) V2
sx1-v1 Siemens SX1 (OMAP310) V1
terrier Sharp SL-C3200 (Terrier) PDA (PXA270)
tosa Sharp SL-6000 (Tosa) PDA (PXA255)
verdex Gumstix Verdex (PXA270)
versatileab ARM Versatile/AB (ARM926EJ-S)
versatilepb ARM Versatile/PB (ARM926EJ-S)
vexpress-a15 ARM Versatile Express for Cortex-A15
vexpress-a9 ARM Versatile Express for Cortex-A9
virt ARM Virtual Machine
xilinx-zynq-a9 Xilinx Zynq Platform Baseboard for Cortex-A9
z2 Zipit Z2 (PXA27x)
</code></pre></div></div>
<p>今回は <code class="language-plaintext highlighter-rouge">versatilepb</code> が参考サイトでもよく使われていたのでこれを利用します。</p>
<p>Device Treeはボード固有のハードウェア情報を記述したもので、これにより様々なボードで同じLinuxカーネルを利用することができるようになります。Device Treeが利用される以前はLinuxカーネルに直接ボード固有のハードウェア情報が記述されていてメンテナンスを継続するのが困難だったという状況だったようです。今回はQEMUで起動する際にカーネルに同梱されているDevice Treeが記述されたバイナリ(dtb: Device Tree Blob)を渡して起動を行います。</p>
<h3 id="ビルド">ビルド</h3>
<p>まずはLinuxカーネルをcloneし、<code class="language-plaintext highlighter-rouge">versatiblepb</code> 向けのデフォルト設定を適用します。ちなみにここで指定できるデフォルト設定は <code class="language-plaintext highlighter-rouge">arch/arm/configs/</code> から参照することができます。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/torvalds/linux.git kernel
<span class="nb">cd </span>kernel
make <span class="nv">ARCH</span><span class="o">=</span>arm versatile_defconfig
</code></pre></div></div>
<p>次に<code class="language-plaintext highlighter-rouge">make ARCH=arm menuconfig</code> を利用してカーネルパラメータを編集します。</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Device Drivers -> Graphics support -> Direct Rendering Manager (XFree86 4.1.0 and higher DRI support)</code> のチェックを外す
<ul>
<li>起動時に初期化処理でTimeout待ちをしているようだったのと、特に今回は利用する予定がないので無効</li>
</ul>
</li>
</ul>
<p>編集が終わったら <code class="language-plaintext highlighter-rouge">Save</code> で <code class="language-plaintext highlighter-rouge">.config</code> に保存したうえで <code class="language-plaintext highlighter-rouge">Exit</code> します。</p>
<p>そしてカーネルをビルドします。マルチコア環境では <code class="language-plaintext highlighter-rouge">-j</code> オプションを指定して並列で実行する方が良いと思います。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- zImage dtbs
</code></pre></div></div>
<p>ビルドが終わったらzImageとdtbがそれぞれ生成されていることを確認します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ls arch/arm/boot/dts/versatile-pb.dtb
ls arch/arm/boot/zImage
</code></pre></div></div>
<h2 id="動作確認">動作確認</h2>
<h3 id="linuxの起動プロセスとqemuでの例">Linuxの起動プロセスとQEMUでの例</h3>
<p>動作確認の前にBIOSを利用したLinuxの起動プロセスについて整理したいと思います。PC上のLinuxは下記のようなプロセスで起動が行われます。</p>
<ol>
<li>BIOSがMBRにあるブートローダをメモリ上に読み込む</li>
<li>ブートローダーがディスクのファイルシステム等をMBRのパーティションマップから解釈する</li>
<li>ブードローダーがinitrd(初期RAMディスク)等のオプションを指定してカーネルを起動</li>
<li>初期RAMディスクをrootファイルシステムとしてマウントし初期化プロセス(/sbin/init)を実行</li>
<li>初期化プロセスが必要な処理を実行しrootファイルシステムをディスクにあるものに変更</li>
</ol>
<p>この際にブートローダー、カーネル、初期化プロセスはそれぞれ依存関係はありません。このことから下記のような内容で動作確認を行っていきます。</p>
<ul>
<li>カーネルを起動するが初期化プロセスが無い状態</li>
<li>初期化プロセスが何もしないプログラムである状態</li>
<li>BusyBoxを利用して初期化プロセスを起動しシェルを利用できる状態</li>
</ul>
<p>また、QEMUにおいてはブートローダを用意する必要はなく <code class="language-plaintext highlighter-rouge">-kernel</code> / <code class="language-plaintext highlighter-rouge">-initrd</code> オプションを指定することでカーネルや初期RAMディスクの指定をすることができます。</p>
<h3 id="カーネルを起動する">カーネルを起動する</h3>
<p>まずは初期化プロセスが無い状態でLinuxカーネルを起動していきたいと思います。下記のコマンドでビルドしたカーネルで起動することができます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qemu-system-arm -M versatilepb -kernel ./arch/arm/boot/zImage -dtb ./arch/arm/boot/dts/versatile-pb.dtb -nographic -append "console=ttyAMA0"
</code></pre></div></div>
<p>主なオプションの内容は下記の通りです。</p>
<ul>
<li>-M : 利用するボードを指定</li>
<li>-kernel : 起動するカーネルを指定</li>
<li>-dtb : 利用するDevice Tree Blobファイルを指定</li>
</ul>
<p>起動するとrootファイルシステムが無いというメッセージとともにKernel panicが発生したのを確認できると思います。<code class="language-plaintext highlighter-rouge">C-a x</code> を押すとQEMUを終了することができる</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Exception stack(0xc7821fb0 to 0xc7821ff8)
1fa0: 00000000 00000000 00000000 00000000
1fc0: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
1fe0: 00000000 00000000 00000000 00000000 00000013 00000000
---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---
</code></pre></div></div>
<h3 id="初期化プロセスを作成する">初期化プロセスを作成する</h3>
<p>次に<code class="language-plaintext highlighter-rouge">Hello World</code> とメッセージを出すだけの初期化プロセス用のプログラムを用意し起動します。起動プロセスが終了するとKernel panicになってしまうため無限ループにしておきます。</p>
<pre><code class="language-test.c">#include <stdio.h>
int main(int argc, char* argv[])
{
printf("Hello world\n");
while(1);
}
</code></pre>
<p>ARM向けのバイナリとしてコンパイルし、QEMUを使ってコンパイルしたバイナリが正しく動作することを確認します</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>arm-linux-gnueabi-gcc -static -g test.c -o test
qemu-arm -L /usr/arm-linux-gnueabi test
</code></pre></div></div>
<p>次に初期RAMディスクとして利用できるようにcpioを利用してイメージを作成します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo test | cpio -o --format=newc > rootfs
</code></pre></div></div>
<p>用意したイメージを <code class="language-plaintext highlighter-rouge">-initrd</code> に指定し起動します。<code class="language-plaintext highlighter-rouge">Hello World</code>と出力されKernel panicを起こさないかったら正しく動作しています。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qemu-system-arm -M versatilepb -kernel ./arch/arm/boot/zImage -dtb ./arch/arm/boot/dts/versatile-pb.dtb -nographic -append "console=ttyAMA0 root=/dev/ram0 rdinit=/test" -initrd rootfs
</code></pre></div></div>
<h3 id="シェルを利用できるようにする">シェルを利用できるようにする</h3>
<p>本来はここから先で必要なコマンド類をインストールしていくということになるわけですが、それはとても手間がかかります。
そのため今回は様々なUNIXコマンドを1つのバイナリの形で提供しているBusyBoxを利用します。サイズも小さく、様々なコマンドを用意する手間も無いので非常に手軽に用意することができます。
まずはcloneをしソースを取得します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git://git.busybox.net/busybox
cd busybox
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig
</code></pre></div></div>
<p>設定で一部のパラメータを編集します。</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Settings -> Build static binary (no shared libs)</code> をチェックする
<ul>
<li>単一のバイナリで動作するように静的リンクを行う</li>
</ul>
</li>
<li><code class="language-plaintext highlighter-rouge">Networking Utilities -> Enable IPv6 support</code> のチェックを外す
<ul>
<li>ビルドするときにエラーになってしまって、今回はIPv6は利用しないのでビルドの対象から外した</li>
</ul>
</li>
</ul>
<p>最後にビルドを行い、installを行うと<code class="language-plaintext highlighter-rouge">_install</code> 以下にrootファイルシステムとして利用できる形でファイルが生成されます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install
</code></pre></div></div>
<p>その他必要なデバイスファイルを含んだディレクトリやファイルを生成します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd _install/
mkdir dev
sudo mknod dev/null c 1 3
mkdir proc
mkdir sys
mkdir -p etc/init.d
</code></pre></div></div>
<p>/etc/init.d/rcS</p>
<pre><code class="language-/etc/init.d/rcS">#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s
</code></pre>
<p>/etc/inittab</p>
<pre><code class="language-/etc/inittab">::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::respawn:/sbin/getty -L ttyS5 115200 vt100
::ctrlaltdel:/bin/umount -a -r
</code></pre>
<p>最後にrootファイルシステムとして利用するためにイメージを作るために <code class="language-plaintext highlighter-rouge">_install</code> ディレクトリ以下で下記を実行します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find . | cpio -o --format=newc > ../rootfs
</code></pre></div></div>
<p>最後にQEMUを起動しシェルが利用できるか確認します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qemu-system-arm -M versatilepb -kernel /path/to/arch/arm/boot/zImage -dtb /path/to/arch/arm/boot/dts/versatile-pb.dtb -nographic -append "console=ttyAMA0 root=/dev/ram0 rdinit=/sbin/init" -initrd rootfs
</code></pre></div></div>
<p>下記のようにビルドしたバージョンのkernelで実行されていることがわかります。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/ # uname -a
Linux (none) 5.1.0-rc7+ #7 Mon May 6 03:42:54 UTC 2019 armv5tejl GNU/Linux
</code></pre></div></div>
<h2 id="おわりに">おわりに</h2>
<p>実際のハードウェアで起動するには至りませんでしたが、Linuxカーネルのビルドと正しくビルドできたかということに関して確認できるような手順を整理することができました。
この次の流れとしては<a href="http://www.linuxfromscratch.org/lfs/">Linux From Scratch</a>の手順でBusyBoxを使わずに環境を準備したりするのが良いのではないかという気がしています。</p>
<h2 id="参考リンク">参考リンク</h2>
<ul>
<li><a href="https://designprincipia.com/compile-linux-kernel-for-arm-and-run-on-qemu/">Cross-compile Linux kernel for ARM and run on QEMU</a></li>
<li><a href="https://qiita.com/tobira-code/items/f76610983902a4ae2441">QEMU user-mode emulation for ARM - Qiita</a></li>
<li><a href="https://qiita.com/kaishuu0123/items/33621af2aca44d8d2c72">Linux を(わりと)シンプルな構成でビルドして Qemu で起動する - Qiita</a></li>
<li><a href="https://balau82.wordpress.com/2010/03/22/compiling-linux-kernel-for-qemu-arm-emulator/">Compiling Linux kernel for QEMU ARM emulator | Freedom Embedded</a></li>
<li><a href="http://atelier-orchard.blogspot.com/2013/02/arm-qemulinux.html">Atelier Orchard: ARM QEMUでlinuxカーネルを起動</a></li>
<li><a href="http://keropo.hatenadiary.com/entry/2013/04/17/230405">BusyBoxのコンパイル - keropoの備忘録</a></li>
<li><a href="https://keichi.net/post/linux-boot/">Linuxがブートするまで · Keichi Takahashi</a></li>
</ul>はじめに Linuxのカーネルは誰でも参照できる形でソースコードが公開されています。そのため自らビルドを行い適用することができます。 しかし、私はほとんどの場合ディストリビューションで提供しているカーネルのアップデートパッチを当てるという以上のことをするというのは少なく、Linuxカーネルをビルドして起動するということを行う機会がほとんどありませんでした。RTMPの理解を深めようとしたが深まらなかった話2018-12-22T15:00:00+00:002018-12-22T15:00:00+00:00http://blog.hirokikana.com/dev/rtmp-client<p>この記事は <a href="http://qiita.com/advent-calendar/2018/dwango">ドワンゴ Advent Calendar 2018</a>の23日目です。</p>
<h3 id="はじめに">はじめに</h3>
<p>去年の記事があまりに雑であると反省し、書いた直後は「もう枠を無駄に使ってしまうのは申し訳ないな、来年からはやめよう」という気持ちでいました。
故に今年も枠を使うことに迷いがありました。しかし、私のようなものが雑な記事を書くことで他のメンバーの記事が引き立つのであればそれは全然関係無い時期にただ書くよりは意味があるのではないかという考えに至り今年も懲りずに記事を書くことにしました。まとまりの無い長文になってしまいましたが、お時間のある方は最後までお付き合いください。</p>
<p>さて、近年ではWebRTCといった新たなライブストリーミングを実現にあたる様々な解決策の選択肢が増えています。しかしながらRTMP(Real Time Message Protocol)で配信を行い、HLS(HTTP Live Streaming)等に変換し視聴するというスタイルは現在ではまだ広く利用されるライブストリーミングの方法であると考えられます。
今回はまだ広く利用されるストリーミングプロトコルであるRTMPについて理解を深めることを目的としてRTMPのクライアントを作成し、その際にぶつかったこと等を紹介します。</p>
<h3 id="目的と今回のゴール">目的と今回のゴール</h3>
<p>今回まずはRTMPプロトコルへの理解を深めるということを目的としました。このような記事でよくある困ってることを解決したいというようなキレイな目的はありません。
理解を深めるのであれば仕様を丁寧に読み込めばそれは理解したと言えるかもしれませんが、私の理解力はそれほど高くないためもう少し手を動かす必要があります。そのためには実現したいことをでっち上げて何らかのゴールを作る必要がありました。</p>
<p>ともあれAdobeから公開されている<a href="https://www.adobe.com/jp/devnet/rtmp.html">RTMPの仕様書</a>を読みながら考えていこうということにしました。その読む段階でRTMPで送ることができるのは映像・音声だけでなく任意のデータも送ることができるということを知りました。これは映像と音声のデータに合わせて文字列のデータを送り字幕やコメントとして利用できるのではないかと考えました。</p>
<h3 id="概要">概要</h3>
<p>RTMPサーバーをnginx-rtmpモジュールを利用したものとし、RTMPのクライアント(配信および視聴用)を実装するという方針としました。最終的に映像・音声を表示しながらコメントを表示するというのはさすがに間に合わないだろうというところから、まずは任意のデータを送受信できるところまでは実施しました。</p>
<h3 id="rtmp通信のシーケンス">RTMP通信のシーケンス</h3>
<p>RTMP通信は下記のようなシーケンスで通信が行われます。</p>
<ul>
<li>Handshake</li>
<li>connect</li>
<li>createStream</li>
<li>配信を行う場合
<ul>
<li>publish</li>
</ul>
</li>
<li>視聴を行う場合
<ul>
<li>play</li>
</ul>
</li>
</ul>
<h4 id="handshake">Handshake</h4>
<p>まずはクライアントからC0とC1というメッセージをサーバに送ります。C0は1byteで利用するRTMPバージョンが入り、現在では0〜2が非推奨となり3が格納されます。C1は1536byteで4byteのタイムスタンプ、4byteの0で埋められたメッセージ、1528byteのランダムな値が格納されています。ランダムな値は暗号的に安全である必要は無いとのことで今回は <code class="language-plaintext highlighter-rouge">random.getrandbits</code> で生成した値を利用しました。C0を受け取ったサーバはS0とS1を返します。S0とC0、S1とC1と内容のフォーマットは同一です。</p>
<p>その後クライアントからC2サーバーからS2を返してHandshakeが完了となります。C2/S2は4byteのタイムスタンプとパケットを読み取ったタイムスタンプ、続いてそれぞれS1/C2に含まれたランダムな値が1528byte格納されて返されます。今回利用したnginx-rtmpを利用したRTMPサーバーの場合にはC0+C1を送るとS0+S1+S2が返却され、クライアントからC2を送るという流れになっていました。C2を送っても何かHandshakeが終わった等のメッセージはありません。</p>
<p><img src="/img/post/2018-12-23_handshake_capture.png" alt="handshake" /></p>
<h4 id="connect">connect</h4>
<p>Handshakeが終わったら次にNetConnection Commandsの <code class="language-plaintext highlighter-rouge">connect</code> メソッドを利用してどのRTMPサーバーのアプリケーションに接続します。
よくあるRTMPは以下のような形になっており、概ねパスの最初に指定されているのがアプリケーションです。</p>
<p>HandshakeまではRTMPとしては特殊なフォーマットをしていたのですが、ここから先にはチャックというフォーマットで送られます。
チャンクはチャンクヘッダとチャンクデータにわかれそれぞれ下記のようなものが含まれています。</p>
<ul>
<li>チャンクヘッダ
<ul>
<li>Basicヘッダ(1byte~3byte) : チャンクを識別するチャンクストリームIDとメッセージヘッダのフォーマットを示す識別子</li>
<li>メッセージヘッダ(0, 3, 7, 11byte) : チャンクデータの種類(映像や音声なのかコマンドなのか等)やチャンクデータの長さ等の情報</li>
<li>Extend Timestamp(0, 4byte) : タイムスタンプが16777215を超えた際に利用される</li>
</ul>
</li>
<li>チャンクデータ</li>
</ul>
<p>それぞれWiresharkで見るとRTMP Header / RTMP Bodyという形でわかりやすく表示されています。</p>
<p><img src="/img/post/2018-12-23_wireshark_rtmp.png" alt="wireshark_rtmp" /></p>
<p><code class="language-plaintext highlighter-rouge">connect</code> は接続に必要なデータがチャンクに含まれています。メッセージヘッダ内にあるチャンクデータ種を表すType IDは本来20(AMF0 Command Message)になりそうですがffmpegから行った際にはここが14になっていました。詳細な理由を調べれば何か歴史的経緯があったりしたかもしれないのですが、ここはひとまず動いているやりとりにしてしまった方が良いだろうと14を利用しました。</p>
<p>チャンクデータにはひとまず下記のものだけを含めましたが、正しく利用できました。仕様書にはこれ以外にも多くの指定することが可能な項目がありますが必要な際に付与すれば良いと思います。<code class="language-plaintext highlighter-rouge">connect</code> メソッドのチャンクデータAMF(Action Message Format)0というAction Scriptで利用されることを想定したシリアライズフォーマットが利用されます。AMF0の仕様はこちらの<a href="https://www.adobe.com/content/dam/acom/en/devnet/pdf/amf0-file-format-specification.pdf">PDF</a> で公開されています。</p>
<ul>
<li>コマンド名(<code class="language-plaintext highlighter-rouge">connect</code>)</li>
<li>トランザクションID(<code class="language-plaintext highlighter-rouge">connect</code> の場合は1)</li>
<li>コマンドオブジェクト
<ul>
<li>app : アプリケーション名</li>
<li>tcUrl : サーバーのURL</li>
<li>flashVer : 接続するFlash Playerのバージョン</li>
<li>オブジェクト終了のマーカー</li>
</ul>
</li>
</ul>
<p>nginx-rtmpモジュールにおいては <code class="language-plaintext highlighter-rouge">connect</code>メソッドが正しくサーバ側で受け取ると下記のようなメッセージがサーバから返ってきます。</p>
<ul>
<li>Windows Acknowlegement Size</li>
<li>Set Peer Bandwidth</li>
<li>Set Chunk Size</li>
</ul>
<h4 id="createstream">createStream</h4>
<p>次にNetConnection Commandsの <code class="language-plaintext highlighter-rouge">createStream</code> メソッドを利用し実際に流す論理的なストリームを生成します。
チャンクデータは下記のもので構成されています。</p>
<ul>
<li>コマンド名(<code class="language-plaintext highlighter-rouge">createStream</code>)</li>
<li>トランザクションID</li>
<li>コマンドオブジェクトもしくはnull</li>
</ul>
<p>トランザクションIDとコマンドオブジェクトに何を指定するのが正しいのかいまいち判断がつかなかったのでffmpegで配信した際のキャプチャした内容で、トランザクションIDが4、コマンドオブジェクトはnullが指定されていたのでひとまず今回はそれを指定しました。</p>
<h4 id="publish">publish</h4>
<p>配信する際にはNetStream Commandsというストリームを操作するコマンドの <code class="language-plaintext highlighter-rouge">publish</code> メソッドを利用します。</p>
<ul>
<li>コマンド名(<code class="language-plaintext highlighter-rouge">publish</code>)</li>
<li>トランザクションID(<code class="language-plaintext highlighter-rouge">publish</code> の場合は0)</li>
<li>null</li>
<li>ストリーム名</li>
<li>publish種別(ライブストリーミングの場合は<code class="language-plaintext highlighter-rouge">live</code>を指定)
送信後 <code class="language-plaintext highlighter-rouge">onStatus</code> コマンドで<code class="language-plaintext highlighter-rouge">NetStream.Publish.Start</code>が返されたら成功しており、配信を開始することができるようになります。</li>
</ul>
<h4 id="play">play</h4>
<p>一方、視聴を行う場合はNetStream Commandの <code class="language-plaintext highlighter-rouge">play</code> メソッドを利用します。</p>
<ul>
<li>コマンド名(<code class="language-plaintext highlighter-rouge">play</code>)</li>
<li>トランザクションID(<code class="language-plaintext highlighter-rouge">play</code> の場合は0)</li>
<li>null</li>
<li>ストリーム名</li>
<li>スタート時間(ストリーミングの際はおそらく0?)
<code class="language-plaintext highlighter-rouge">publish</code> と同じく <code class="language-plaintext highlighter-rouge">onStatus</code> コマンドで <code class="language-plaintext highlighter-rouge">NetStream.Play.Start</code>が返されたら成功しており、接続したストリームに配信が行われていたら映像・音声等のデータがサーバから送信されてきます。</li>
</ul>
<h3 id="便利なテクニック">便利なテクニック</h3>
<h4 id="wiresharkを使ったデバッグ">Wiresharkを使ったデバッグ</h4>
<p>Wiresharkはパケットキャプチャを行うソフトウェアです。同じ役割でCLI上で利用する際はtcpdumpをよく利用します。RTMPに限らずネットワーク上のデータを確認する際にはサーバー側でtcpdumpを走らせ、クライアント側でWiresharkを走らせるというのは原因の切り分けを行ううえでは非常に有効な手段となります。</p>
<p>仕様書を見て間違いが無く実装できれば全く問題無いのですが、多くの場合は間違っていたりあとは仕様書では具体的な値がわからなかったりという部分が出てきます。
今回はffmpegを使って配信して内容をWiresharkで確認しそれを正しい値として採用することにしました。そこになぜその値をきちんと理解していない部分も正直多くあったのですが、まずはそれっぽく動かしたいという気持ちが先行しました。</p>
<p>Wiresharkは便利なことにRTMPの通信のみをフィルタに<code class="language-plaintext highlighter-rouge">rtmpt</code>と入力することで絞り込むことができます。さらにチャンクヘッダとチャンクデータはParseされ見やすい形に表示されます。WiresharkでParseができない場合はTCPの通信として表示されるのでPayloadのバイナリを参照して正しい挙動の際との差分を確認します。</p>
<p><img src="/img/post/2018-12-23_wireshark_filter.png" alt="wireshark-filter" /></p>
<h4 id="python3でのバイナリの表現">Python3でのバイナリの表現</h4>
<p>Wiresharkやtcpdumpでキャプチャした際によくわからないけどバイナリで見ると差分がある際にとりあえず意味はわからずとも同内容のバイナリを送りたいということがありました。例えば <code class="language-plaintext highlighter-rouge">70 6c 61 79</code> というバイナリを作りたいときには下記のようにします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>>> (0x70).to_bytes(1,'big') + (0x6c).to_bytes(1, 'big') + (0x61).to_bytes(1,'big') + (0x79).to_bytes(1,'big')
b'play'
</code></pre></div></div>
<p>上記の結果からわかるように<code class="language-plaintext highlighter-rouge">70 6c 61 79</code>は<code class="language-plaintext highlighter-rouge">play</code>という文字列をバイナリで表記したものです。
とりあえず試してみたいという部分は上記の方法で無理やり同じバイナリを送って動作確認をしました。</p>
<p>このような方法はPython3から実現可能になっており、Pythonでバイナリ操作をするならPython2ではなくPython3を利用するのが望ましいです。</p>
<h3 id="作成したもの">作成したもの</h3>
<p><a href="https://github.com/hirokikana/rtmp-client">https://github.com/hirokikana/rtmp-client</a></p>
<p><code class="language-plaintext highlighter-rouge">bin/rtmp-client</code> コマンドでは<code class="language-plaintext highlighter-rouge">publish</code>と<code class="language-plaintext highlighter-rouge">play</code>の両方を行うことができます。
まず<code class="language-plaintext highlighter-rouge">play</code>で待ち受けて<code class="language-plaintext highlighter-rouge">test message</code>という文字列を<code class="language-plaintext highlighter-rouge">publish</code>するのは下記のようなコマンドで実施します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ python3 bin/rtmp-client play
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ echo "test message" | python3 bin/rtmp-client publish
</code></pre></div></div>
<p>このようにすることで<code class="language-plaintext highlighter-rouge">play</code>で待機しているターミナルで<code class="language-plaintext highlighter-rouge">test message</code>と表示されるでしょう。</p>
<p><img src="/img/post/2018-12-23_run_demo.gif" alt="demo" /></p>
<h3 id="課題">課題</h3>
<h4 id="video--audio-以外のデータをクライアントに直接送ることはできない">Video / Audio 以外のデータをクライアントに直接送ることはできない</h4>
<p>仕様書を見ると任意のデータを配信者からサーバーを介して複数の視聴者に送ることができるように見えました。しかし実際行ってみるとRTMPサーバーから先へ任意のデータを送ることができませんでした。
nginx-rtmpをきちんとデバッグしたわけではないのですが<a href="https://github.com/arut/nginx-rtmp-module/blob/master/ngx_rtmp_live_module.c#L1126-L1130">このあたり</a>から察するというか、雰囲気としてもしかしてVideo / Audio以外のメッセージは素通ししないようにしているのではないかと考えています。</p>
<p>RTMPのプロトコル上では任意データを送信することができるが、サーバから視聴者へのデータを送るかどうかについてはサーバの実装によるということのようでした。これに気づいた時には目論見がはずれでっち上げたゴールには締切までにたどり着けないなという気持ちになりました。今回とりあえず任意のデータを送るためにVideo / Audioのメッセージであるように見せかけて送るようにしました。</p>
<h4 id="受信した内容が送信した内容と一致しない">受信した内容が送信した内容と一致しない</h4>
<p>当初小さなJPEG画像をpublishしてテストをしたのですが、受信側で正しく展開されませんでした。受信したバイナリを確認してみるといくつかのサイズからバイナリが異なっていました。Wiresharkで確認してみるとその段階では送信したバイナリと一致している様子だったのでPythonで受信した際に何か余計なことになってしまっているのではないかということまではわかりました。</p>
<p>これに気づいたのがつい最近ということもあり、クライアントだけ新しく作って検証するのも時間が無いうえ、rtmpdump等のクライアントでも内容を確認できなかったので今回は課題として残る形になりました。</p>
<h3 id="まとめ">まとめ</h3>
<p>当初の目的であるRTMPへの理解を深めるというのは、最初から相対的に見ると深まったとはいえゴールとしていた成果にたどり着けなかったところからすると目的を達せたとは言えないでしょう。</p>
<p>しかしながら今回</p>
<ul>
<li>Wireshark / tcpdump はとても便利</li>
<li>Pythonでバイナリ操作をするならPython3</li>
<li>RTMPでは任意データも視聴者に簡単に送れるわけではない</li>
</ul>
<p>ということがわかったのは良かったという気持ちではいます。</p>
<h3 id="参考リンク">参考リンク</h3>
<ul>
<li><a href="https://developers.cyberagent.co.jp/blog/archives/13739/">RTMP 1.0 準拠のサーバーをGo言語で実装する</a></li>
<li><a href="http://wwwimages.adobe.com/www.adobe.com/content/dam/acom/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf">Adobe’s Real Time Messaging Protocol</a></li>
<li><a href="https://github.com/arut/nginx-rtmp-module/">arut/nginx-rtmp-module</a></li>
<li><a href="https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol">Real-Time Messaging Protocol - Wikipedia</a></li>
<li><a href="https://www.adobe.com/content/dam/acom/en/devnet/pdf/amf0-file-format-specification.pdf">Action Message Format – AMF0</a></li>
<li><a href="https://en.wikipedia.org/wiki/Action_Message_Format">Action Message Format - Wikipedia</a></li>
</ul>この記事は ドワンゴ Advent Calendar 2018の23日目です。Redisのクローンを作ろうとして作らなかった2017-12-17T17:30:00+00:002017-12-17T17:30:00+00:00http://blog.hirokikana.com/dev/redis-clone<p>この記事は <a href="http://qiita.com/advent-calendar/2017/dwango">ドワンゴ Advent Calendar</a>の18日目です。</p>
<h3 id="はじめに">はじめに</h3>
<p>今年もAdvent Calendarの季節となりました。もはやこの時期しかまとまった文章を書かなくなり、もともと無い文章表現力が皆無に近くなり危機感を感じています。昨年は確かストレージの容量が無くて困った話でしたね。あの件は追加で20TBぐらいのストレージを買って解決しました。本当に良かった。</p>
<p>さて、今年はどのようなことをやったかと言うとRedisクローンを作ろうとしました。結論としては全くできてないのですが、ここまでで得られた知見をみなさんに披露したいと思います。</p>
<h3 id="きっかけと動機">きっかけと動機</h3>
<p>私はよく安いからというだけで何かを買ったりします。そのような経緯から安くなったKindle本をよく買います。ちなみに読書のスピードは絶望的に遅く、多くの本は積読となります。その多くの本の中から「ガベージコレクション 自動的メモリ管理を構成する理論と実装」という本をそれとなく読み進めていました。この本を読む中で気づいたことがあります。</p>
<ul>
<li>読んだところによるとC言語はガベージコレクションを言語でサポートしていないらしい</li>
<li>そういえばRedisはANSI Cで書かれていると書いてあった気がする</li>
<li>ガベージコレクションをサポートした言語で書き直せばRedisの概要もソースコードで把握できるし何か面白そうではないか</li>
</ul>
<p>このようなきっかけから私はRedisをなんらかの言語で書き直し、その過程で新たな言語の学習とRedisのソースコードの読み方を習得することを目的として動き出したわけです。そういえば私はGoに興味がありつつもなかなか手をつけることができずにいました。これはちょうど良いきっかけであると感じGoで書き直してみようと思ったわけです。</p>
<h3 id="redisを理解する">Redisを理解する</h3>
<p>まずはRedisとはどこにソースがあり、どのような構造になっているのかというのを理解する必要があろうと思いました。</p>
<p><a href="http://redis.io">公式ページ</a>をみるとどうやら<a href="https://github.com/antirez/redis">Github</a>でソースが管理されているようでしたのでおもむろにForkをしました。そのうえで標準的な内容でコンパイルし、動作することを確認しました。今回は動作確認ができれば良いので <code class="language-plaintext highlighter-rouge">make install</code> は行いませんでした。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ make
$ src/redis-server #Redisが起動することを確認
$ src/redis-cli #起動したRedisにアクセスできることを確認
</code></pre></div></div>
<p>はまることもなく動作確認することができました。</p>
<p>しかしながらこれだけではどこにどのような処理が存在しているのかすらわかりませんでした。わからなくなったら多くの場合はREADMEに何かのヒントがあります。RedisのREADMEを読み進めてみると「Redis internals」というまさに探していそうな内容の記述があります。様々な内容が書かれていたのですが、ここを読んで私が理解したのは</p>
<ul>
<li>src ディレクトリ以下にRedisの実装がある</li>
<li>server.c がRedisサーバーのエントリーポイントとなっている</li>
<li>何か処理をする新たなRedisコマンドを作るのが入門に良さそう</li>
</ul>
<p>以上の3点です。私の目標は新しいRedisコマンドを作るということになりました。</p>
<h3 id="独自コマンドを考える">独自コマンドを考える</h3>
<p>もう私はこの時点でGoのことは完全に頭から離れています。なぜなら、現在の理解度とやろうとしていること、そして期限を見て当初の目的を果たすのは無理だろうと思い、一旦Goのことは忘れようと思ったわけです。</p>
<p>さて、では独自コマンドとはどのようなものが良いだろうかと考え始めました。まず、RedisとはKVSであるわけですが、であれば格納されているデータを何か加工する処理等を追加するのが深い理解に役立つだろうと思いました。しかし、私の今の理解状況から見てRedisが保持しているデータを加工等するのは少々ハードルが高かろうと諦めました。何か読み込んでそのまま返せるようなデータをただただ返すというネタが無いかと探しました。</p>
<p>やはりパッと思いつくのはサーバーの時間でしょう。「時間返すだけならすぐにできるだろう」と私は余裕を持ち始め、その後3日程度作業を進めるのを怠りました。さてゆっくり休んだところで、実装しようとする前に「もしかして既に同じ実装があったらヤバいな…」と思い、念のためコマンドリファレンスを確認しました。そして私の目に飛び込んできたのは<a href="https://redis.io/commands/time">このページ</a>でした。そう、私が考えるようなことなんて大体誰かが考えて実装しているわけです。</p>
<p>焦りだした私は別の情報を取得して返せないかと考え、サーバーのアーキテクチャを返すのはどうだろうと考えました。 調べたところ<a href="https://linuxjm.osdn.jp/html/LDP_man-pages/man2/uname.2.html">このようなページ</a>を見つけ、なるほどこのようにして取得することができるのかと理解しました。さて実装しようとする前に、もしかしてまた実装されているのではないかと思い今度は <code class="language-plaintext highlighter-rouge">server.c</code> を <code class="language-plaintext highlighter-rouge">utsname</code> で検索をかけました。…そう、私が考えるようなことなんて大体誰かが考えて実装しているわけです。</p>
<p>ここまではもしかしたら役に立つかもしれないコマンドを追加してみようという気持ちで考えてきたわけですが、役立ちそうなことで私が思いつくようなことは大体誰かが考えて実装しているわけです。ここは学習目的でありますし、役に立つかもしれないあわよくば本流にマージしてもらえるかもというような欲を捨てることにしました。そこで私が辿りついたのがサーバーのCPUの名前を返すというコマンドでした。</p>
<h3 id="cpu名の取得方法">CPU名の取得方法</h3>
<p>Linuxでは <code class="language-plaintext highlighter-rouge">/proc/cpuinfo</code> 見ればCPU名の取得ができますし、まあきっと簡単に取得できるだろうと思ったわけですが、それは私の浅慮であったということが割とすぐにわかりました。動作確認はMac OSでやっていたわけですが <code class="language-plaintext highlighter-rouge">/proc/cpuinfo</code> は存在しません。他の方法が必要なんだなということで探し、参考にしたのは<a href="http://nanoappli.com/blog/archives/3963">こちらのページ</a>なのですがアセンブラのCPUID命令を使えば取得できる、インラインアセンブラという仕組みがあるということを理解しました。</p>
<p>いきなりRedisに組み込む前にまずは単体で動かしてみようと書いたコードが下記です。</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <stdio.h>
</span>
<span class="cp">#include <string.h>
</span>
<span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mi">4</span><span class="o">*</span><span class="mi">4</span><span class="p">];</span>
<span class="kt">char</span> <span class="o">*</span><span class="nf">cpuid</span><span class="p">(</span><span class="kt">int</span> <span class="n">op</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">int</span> <span class="n">eax</span><span class="p">,</span> <span class="n">ebx</span><span class="p">,</span> <span class="n">ecx</span><span class="p">,</span> <span class="n">edx</span><span class="p">;</span>
<span class="n">__asm__</span><span class="p">(</span><span class="s">"cpuid"</span>
<span class="o">:</span> <span class="s">"=a"</span> <span class="p">(</span><span class="n">eax</span><span class="p">),</span>
<span class="s">"=b"</span> <span class="p">(</span><span class="n">ebx</span><span class="p">),</span>
<span class="s">"=c"</span> <span class="p">(</span><span class="n">ecx</span><span class="p">),</span>
<span class="s">"=d"</span> <span class="p">(</span><span class="n">edx</span><span class="p">)</span>
<span class="o">:</span><span class="s">"0"</span> <span class="p">(</span><span class="n">op</span><span class="p">));</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">eax</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="o">+</span><span class="mi">4</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">ebx</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="o">+</span><span class="mi">8</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">ecx</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="o">+</span><span class="mi">12</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">edx</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">buf</span><span class="p">[</span><span class="mi">15</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="k">return</span> <span class="n">buf</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">char</span> <span class="n">s1</span><span class="p">[</span><span class="mi">16</span><span class="o">*</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="s">""</span><span class="p">;</span>
<span class="n">strcat</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="n">cpuid</span><span class="p">(</span><span class="mh">0x80000002</span><span class="p">));</span>
<span class="n">strcat</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="n">cpuid</span><span class="p">(</span><span class="mh">0x80000003</span><span class="p">));</span>
<span class="n">strcat</span><span class="p">(</span><span class="n">s1</span><span class="p">,</span> <span class="n">cpuid</span><span class="p">(</span><span class="mh">0x80000004</span><span class="p">));</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%s"</span><span class="p">,</span> <span class="n">s1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>これを見ていただければ私のCの理解度をわかっていただけると思います。これを実行してみると下記のようになります。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ gcc cpuid.c
$ ./a.out
Intel(R) Xeon(R CPU 5570 @ 2.93GHz%
</code></pre></div></div>
<p>良さそうですね。</p>
<h3 id="独自コマンドとして処理を追加する">独自コマンドとして処理を追加する</h3>
<p>それでは独自コマンドとして実装してみます。今回は下記のようにしました。</p>
<ul>
<li>コマンド名:cpuinfo</li>
<li>引数:無し</li>
<li>戻り値:CPU名</li>
</ul>
<p>まずはRedisではコマンドの定義を redisCommandTable に追加することで、実際に処理をコマンドで呼び出すことができるようになります。今回は下記のように追加しました。</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s">"cpuinfo"</span><span class="p">,</span><span class="n">cpuinfoCommand</span><span class="p">,</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="s">"rF"</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="nb">NULL</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">}</span>
</code></pre></div></div>
<p>それぞれの役割はあるのですが、一旦は使いそうな先頭から下記の4つだけを理解しました。</p>
<ul>
<li>コマンド名</li>
<li>コマンドを定義した関数</li>
<li>引数の数</li>
<li>コマンドフラグ(後述)</li>
</ul>
<p>コマンドフラグはいくつかあるのですが、今回していした <code class="language-plaintext highlighter-rouge">rF</code> について説明します。</p>
<ul>
<li>r:読み込みコマンド</li>
<li>F:高速に処理を完了させることができるコマンド</li>
</ul>
<p>コマンドフラグや引数の意味、実際の効果についてもっときちんと説明をしたかったのですが、私の今の理解力では正しく説明ができる自信が無いので省略します。詳細な説明はソースコードにあるので、私でなければそれほど多くの時間もかからず理解できるのではないかと思っています。</p>
<p>さて、コマンドの定義を redisCommandTable に追加したところで具体的な処理である cpuinfoCommandを実装していきます。</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">getCpuBrandName</span><span class="p">(</span><span class="kt">int</span> <span class="n">op</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">brandname</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">int</span> <span class="n">eax</span><span class="p">,</span> <span class="n">ebx</span><span class="p">,</span> <span class="n">ecx</span><span class="p">,</span> <span class="n">edx</span><span class="p">;</span>
<span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>
<span class="n">__asm__</span><span class="p">(</span><span class="s">"cpuid"</span>
<span class="o">:</span> <span class="s">"=a"</span> <span class="p">(</span><span class="n">eax</span><span class="p">),</span>
<span class="s">"=b"</span> <span class="p">(</span><span class="n">ebx</span><span class="p">),</span>
<span class="s">"=c"</span> <span class="p">(</span><span class="n">ecx</span><span class="p">),</span>
<span class="s">"=d"</span> <span class="p">(</span><span class="n">edx</span><span class="p">)</span>
<span class="o">:</span><span class="s">"0"</span> <span class="p">(</span><span class="n">op</span><span class="p">));</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">eax</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="o">+</span><span class="mi">4</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">ebx</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="o">+</span><span class="mi">8</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">ecx</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="o">+</span><span class="mi">12</span><span class="p">,</span> <span class="p">(</span><span class="kt">char</span><span class="o">*</span><span class="p">)(</span><span class="o">&</span><span class="n">edx</span><span class="p">),</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">buf</span><span class="p">[</span><span class="mi">15</span><span class="p">]</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span>
<span class="n">strcat</span><span class="p">(</span><span class="n">brandname</span><span class="p">,</span> <span class="n">buf</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">cpuinfoCommand</span><span class="p">(</span><span class="n">client</span> <span class="o">*</span><span class="n">c</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">char</span> <span class="n">brandname</span><span class="p">[</span><span class="mi">16</span><span class="o">*</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="s">""</span><span class="p">;</span>
<span class="n">getCpuBrandName</span><span class="p">(</span><span class="mh">0x80000002</span><span class="p">,</span> <span class="n">brandname</span><span class="p">);</span>
<span class="n">getCpuBrandName</span><span class="p">(</span><span class="mh">0x80000003</span><span class="p">,</span> <span class="n">brandname</span><span class="p">);</span>
<span class="n">getCpuBrandName</span><span class="p">(</span><span class="mh">0x80000004</span><span class="p">,</span> <span class="n">brandname</span><span class="p">);</span>
<span class="n">addReplyBulkCBuffer</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">brandname</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">brandname</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>ここで重要なポイントは コマンドの引数にはクライアントを取るということと、そのクライアントにレスポンスを返すということです。CPU名を取得する部分については前述の内容を少し変えたものにしていますが処理は同じです。ここで作った文字列を addReplyBulkCBuffer という関数でクライアントに送信しています。 addReplyBulkCBuffer は network.c に実装があり、文字列を返す際に利用される処理です。</p>
<p>ここまでで実装は終了です。最後にコンパイルし実際に利用してみます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ make
$ ./src/redis-server
$ ./src/redis-cli
127.0.0.1:6379> cpuinfo
"Intel(R) Xeon(R CPU 5570 @ 2.93GHz"
</code></pre></div></div>
<p>きちんとCPU名が返ってくるようになりました。</p>
<h3 id="まとめ">まとめ</h3>
<p>さて、ようやくRedisの基本的な処理の詳細を見ていけるようになったのではないかというところで今日という日が来てしまいました。当初のRedisのクローンをGoで作りたいという目標は達成することができませんでした。その前段階であるRedisの理解を深めるという部分でも、あまり深まったとは言えないのが正直なところです。しかしながら、このように誰かが書いたコードを読み始める第1歩で大事なことは意図した場所に意図した処理を差し込めるようになることであると思っています。そのような観点から見ればここまでのことも無駄ではなかったかなと思っています。</p>
<p>Redisとはきっと今後もどこかで付き合っていくことになりそうではあるので、今回のことをきっかけに理解を深めていくというのを来年の目標にしつつ2017年を終えようと思います。</p>
<h3 id="参考ページ">参考ページ</h3>
<ul>
<li><a href="https://linuxjm.osdn.jp/html/LDP_man-pages/man2/uname.2.html">Man page of UNAME</a></li>
<li><a href="http://nanoappli.com/blog/archives/3963">[gcc]CPUID命令を使用して、CPUの情報を取得する</a></li>
</ul>この記事は ドワンゴ Advent Calendarの18日目です。ストレージがいっぱいになってしまったのでどうにかしたいと思った話2016-12-19T15:52:45+00:002016-12-19T15:52:45+00:00http://blog.hirokikana.com/dev/s3-encfs-fuse<p>この記事は <a href="http://qiita.com/advent-calendar/2016/dwango">ドワンゴ Advent Calendar 2016 </a>20日目です。</p>
<h3 id="ある日私は広大な領域が狭くなっていることに気づいた">ある日、私は広大な領域が狭くなっていることに気づいた</h3>
<p>私は12TBのストレージを3年程前に導入しました。導入当初は「もうこれでストレージを購入する必要は無いな」と思うぐらいの無限の領域に見えていたのですが、そのストレージの容量が最近底をつきつつあります。「人間とはあればあれだけ使ってしまうものなのだな」と感慨深くなりながらも、この状況を放っておくわけにはいきません。新しいストレージを購入しようと計画するわけですが、最近は大容量HDDがすっかり安くなったとはいえ「これも欲しい、あれも欲しい」と要求を追加していくとすぐに6桁価格になってしまい、かなり躊躇してしまうわけです。</p>
<p>ふと冷静になるために立ち止まってみると、最近ではクラウドストレージが大容量かつ低価格になりつつあるということが見えてきました。よく売られている安価なWindowsタブレットについているOfficeにはOneDriveの1TB領域もついてますし、Amazon DriveにいたってはUnlimitedプランでは容量無制限で13800円とストレージを構成するHDDの価格と冗長化、年間の電気代、その他心配事を考えるとかなりお手軽に感じます。クラウドストレージをつかううえで障壁となってくるアップロード制限は、このようなクラウドストレージの利用に合わせてプロバイダによっては(<a href="http://www.ocn.ne.jp/info/rules/qualityimprovement/">http://www.ocn.ne.jp/info/rules/qualityimprovement/</a>)廃止の方向に向いつつあるようにも見えます。</p>
<h3 id="クラウドストレージを利用するうえでの不安">クラウドストレージを利用するうえでの不安</h3>
<p>これはクラウドストレージにほとんどのファイルをアップロードするのが良いのでは無いだろうかと思いましたが、ふと漠然とした不安が頭をよぎりました。よくあるクラウドストレージは便利なことにファイルを簡単に共有することができます。これは非常に便利な機能ではありますが、同時に操作ミスやその他の問題で意図せずファイルがインターネット上に公開されてしまう可能性を否定することができません。インターネットを経由して預けている以上、このような心配はどうしても考えてしまいます。</p>
<p>そのような懸念を解消するためにクラウドストレージに対応した一部のサードパーティクライアントでは内容を暗号化するようなオプションがあります。しかし多くはWindowsクライアントであって、MacやLinuxでのクライアントは少ないようでした。</p>
<h3 id="クラウドストレージにちょっとだけ内容を読みにくくして保存したい">クラウドストレージにちょっとだけ内容を読みにくくして保存したい</h3>
<p>そこで「MacとLinuxで使える内容をちょっとだけ読みにくくして保存するクラウドストレージクライアント」を自分で作ってみることにしました。これによってファイルが意図せず公開されてしまっても中身はパッと見ることはできません。</p>
<p>今回はFUSE(Filesystem in Userspace)という仕組みを利用しました。これはファイルアクセスの際にユーザーが独自に用意したプログラムを呼び出し処理を行うことができる仕組みです。FUSEはカーネルとの橋渡しを行い、独自に用意したプログラムはユーザー空間で動作させることができ、仮想ファイルシステムなどを実装際によく利用される仕組みです。FUSEでマウントすれば、OSの標準的なファイル入出力でやりとりすることができるようになるうえ、Linuxを始めとしたUNIX系OSとMac OSなどで動作します。</p>
<p>FUSEから呼び出す任意のプログラムは、CやPythonなどで書くことができますが今回は自身の習熟度の関係からPythonとfusepy(<a href="https://github.com/terencehonles/fusepy">https://github.com/terencehonles/fusepy</a>)を利用しました。このプログラムには各システムコール毎に行う処理の内容を記述します。今回書いた内容はファイルの内容をBASE64で符号化した後に指定したパスワードとXORをかけてS3に保存するようにしました。XORは暗号化と復号化で同じ処理で済む点であったり仕組みが単純なため採用しました。コードは <a href="https://github.com/hirokikana/s3-encfs-fuse">こちら</a>です。</p>
<p><a href="http://blog.hirokikana.com/wp-content/uploads/2016/12/スクリーンショット-2016-12-20-0.57.22.png"><img src="/assets/スクリーンショット-2016-12-20-0.57.22.png" alt="s3fsenc" /></a></p>
<h3 id="利用方法">利用方法</h3>
<p><a href="https://github.com/hirokikana/s3-encfs-fuse">https://github.com/hirokikana/s3-encfs-fuse</a> からコードをcloneした後、まずは下記を用意します。</p>
<ul>
<li>AmazonWebSerivceのアクセスキーとシークレットアクセスキー</li>
<li>Amazon S3の任意のバケット</li>
</ul>
<p>それぞれを用意する方法は別のまとまった情報に任せるとして、上記がある前提で話を進めます。</p>
<p>まずは下記のような設定ファイル(s3.conf)を用意します。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>aws]
<span class="nv">key</span><span class="o">=</span>XXXXXXXXXXXXXXXXXXX
<span class="nv">secret_key</span><span class="o">=</span>XXXXXXXXXXXXXXXXXXXXXX
<span class="nv">bucket_name</span><span class="o">=</span>BUCKET_NAME
<span class="o">[</span>core]
<span class="nv">encrypt_password</span><span class="o">=</span>ENCRYPT_PASSWORD
</code></pre></div></div>
<p>awsセクションのkey/secret_key/bucket_nameはそれぞれ事前に準備したアクセスキーとシークレットキー、そして任意のバケットの名前です。ファイルはここに保存されるようになります。coreセクションのencrypt_passwordはファイル読み書きの際にXORする文字列を指定します。この文字列を知らない場合はファイルの内容を読むことは出来ません。</p>
<p>設定ファイルを用意することができたら、s3-encfs-fuse.pyを実行します。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>python s3-encfs-fuse.py /path/to/mountpoint
</code></pre></div></div>
<p>第2引数にはマウントする場所を指定します。ここで指定したパスにS3がマウントされます。この状態で下記のようにファイルの読み書きができるか確認します。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo </span>abcde <span class="o">></span> /path/to/mountpoint/test
<span class="nv">$ </span><span class="nb">cat</span> /path/to/mountpoint/test
abcde
</code></pre></div></div>
<p>この状態でAmazon S3から直接ダウンロードし中身を確認してみると異なる内容になっていることがわかるはずです。</p>
<h3 id="今後の課題">今後の課題</h3>
<p>ここまででS3にちょっと読みにくい形で保存することができました。しかしながら実用にはかなり課題が残っています。</p>
<ul>
<li>まともに使えるパフォーマンスではない
<ul>
<li>40KBぐらいの画像を開くのに20秒ぐらいかかった</li>
<li>システムコールの回数分HTTPSリクエストをするうえ、Keep-Aliveに対応していないため効率がとても悪いように見える</li>
<li>しかしながら、そもそもexampleにあったMemoryに保存するプログラムでもそれほどパフォーマンスが出てなかったのでそもそもパフォーマンスが出ない可能性もある</li>
</ul>
</li>
<li>大容量ファイルの読み書きはできない
<ul>
<li>BASE64する際に内容をメモリに展開するようにしているので大きなファイルを読み書きするとたぶん落ちる</li>
</ul>
</li>
<li>BASE64エンコードだと空間効率が悪い
<ul>
<li>BASE64エンコードする分、元ファイルより余分に容量を消費してしまう</li>
</ul>
</li>
<li>S3以外のクラウドストレージしていない
<ul>
<li>S3は従量課金制なので大量に保存するとそれなりに費用がかかりそう</li>
<li>最終的には定額・容量無制限のAmazon Driveに書き込みたいけど、そもそもAmazon DriveがAPIを提供しているのか(US版はAPIがあるようですが)確認するところから始める必要がある</li>
</ul>
</li>
</ul>
<p>私の予定ではAdventCalendar当日までにはもう少し実用的になっている予定だったのですが、私の力不足によりこのような食べ散らかした海鮮丼みたいな状況になっているものを紹介するような形となってしまいました。とはいえ、自分自身が誰よりも早く使えるようなってほしいと思っているのでそれほど遠く無い時期には課題をクリアした形のものを紹介できればと思います。</p>{"login"=>"admin", "email"=>"hiroki@hirokikana.com", "display_name"=>"hiroki.kana", "first_name"=>"Hiroki", "last_name"=>"Takayasu"}hiroki@hirokikana.comこの記事は ドワンゴ Advent Calendar 2016 20日目です。Aerospikeを使う前におさえておきたい5つのこと2015-12-24T01:20:23+00:002015-12-24T01:20:23+00:00http://blog.hirokikana.com/dev/aerospike<h1 id="はじめに">はじめに</h1>
<p>この記事は <a href="http://qiita.com/advent-calendar/2015/dwango">ドワンゴ Advent Calendar 2015 </a>の24日目の記事です。</p>
<p>みなさん、クリスマス・イヴをどのように過ごす予定でしょうか。「世の中にはこんなにカップルというものが存在したのか」というほどカップルで溢れかえる街をを通り過ぎ、帰宅した後に読むと心のイルミネーションが灯るような話をクリスマス・イヴの今日はみなさんにお届けしたいと考え、高速な分散KVSであるAerospikeの仕組みと使う前に把握しておいた方が良さそうな点をお届けします。</p>
<p>これは私からのみなさんへのクリスマスプレゼントだと思っていただいても構いません。「もらってばかりでは悪いのでお返しをしたい」と万が一感じてしまったら個人的に連絡をください、Amazonのwishlistを送ります。</p>
<h1 id="aerospikeとは">Aerospikeとは</h1>
<p>Aerospikeとは<strong>高速な処理を得意とする分散KVS</strong>です。2014年6月24日にCommunity Editionとしてオープンソース化され,<a href="https://github.com/aerospike/aerospike-server">github</a>を通じて公開されています。最近WEB+DB PRESS Vol.87で紹介されてご存知の方も多いかとは思います。</p>
<p>Aerospikeはデータをメモリ上にに保持するmemcachedのような形態から、ディスクやSSDなどに永続化するような形態、さらに2つを組み合わせたような形態を取ることができます。大量のデータを保持するようなケースでは、高速に処理を行うとともにメモリより容量単価が安いSSDなどの領域を利用することができます。</p>
<h1 id="aerospikeの特長">Aerospikeの特長</h1>
<p>Aerospikeは高速性を売りにしています。同等のNoSQLと比較し10倍のパフォーマンス、そしてデータの99%を1ms以下、99.9%を5ms以下で処理することができるとしています。パフォーマンスに関する事例の一例として、Google Cloud Platform Japan Blog内の<a href="http://googlecloudplatform-japan.blogspot.jp/2015/10/aerospike-400-tps.html">この</a>記事によると、Google Cloud Platform 仮想マシン 20ノードで400万TPS(トランザクション/秒)を実現しています。また、その性能はマシンを増やせばリニアにスケールするとされています。</p>
<p>さらにサービスを止めることなく、ノードの追加・削除・アップデートを行い、クラスタを拡張することができます.その際のデータの配置し直し(Aerospikeでは<strong>migration</strong>と呼びます)も自動で行われるうえ、migrationをサービスに影響が無いような速度で行うことができます。このような仕組みのおかげで、Aerospike社が扱っているシステムでは<strong>3年間無停止で稼働</strong>し続けることができているとされ、安定性の高さをうたっています。</p>
<h1 id="aerospike大まかな仕組み">Aerospike大まかな仕組み</h1>
<p>次にAerospikeの仕組みを簡単に紹介します。</p>
<h4 id="aerospikeクラスタの構成">Aerospikeクラスタの構成</h4>
<p>Aerospikeは複数のノードで構成されたクラスタを組むことができます。クラスタには 、各ノードに同一のマルチキャストアドレスと同一の設定がされていれば自動的にクラスタに参加することができます。クラスタ構成後はそれぞれのノード同士がメッシュ構造で接続し、ハートビートで生存確認をしています。特定のノードが消失した場合には自動的にクラスタの再構成とmigrationが行われる仕組みになっています。それはノードが追加された時も同様です。</p>
<h4 id="aerospikeのデータ配置">Aerospikeのデータ配置</h4>
<p>Aerospikeではデータをクラスタ内のノードに分散して保持するだけでなく、データを冗長化することも可能です。読み書きに利用されるデータをmaster、バックアップとして利用されるデータをreplicaと呼びます。<strong>replicaの数はreplication factorで設定し、2の場合はmasterとreplicaが1つづつ、3の場合はmasterが1つ、replicaが2つ</strong>という意味になります。</p>
<p>データは4096個に分け、クラスタの各ノードに均等に配置されます。この4096個に分かれた単位を<strong>パーティション</strong>と呼び、それぞれにパーティションID(0〜4095)が割り振られています。パーティションIDは<strong>RIPEMD-160でハッシュ化されたハッシュ値のうちの12bitを利用がパーティションID</strong>として利用しています。これによりデータが一部のパーティションに偏らないように工夫されています。</p>
<p>それぞれのノードは<strong>パーティション単位でデータを受け持ち</strong>ます。どのノードがどのパーティションのmasterを持つべきであるか、またreplicaを持つべきであるかという情報は<strong>パーティションマップ</strong>という表のようなもので管理されています。パーティションマップはPAXOSアルゴリズムと呼ばれるゴシッププロトコルを用いてすべてのノードで同期され、ノードの追加・削除時にクラスタの再構成が行われパーティションマップが変更されても同じパーティションマップを持ち続けています。</p>
<h4 id="aerospikeへのデータの配置と読み書き">Aerospikeへのデータの配置と読み書き</h4>
<p>AerospikeクライアントからAerospikeクラスタにデータを読み書きする流れを紹介します。</p>
<p>クライアントはまずクラスタの任意のノードに接続し、<strong>クラスタの構成情報とパーティションマップ</strong>を取得します。リクエスト時にはキーのパーティションIDを計算し、取得したパーティションマップから、どのノードがmasterのデータを持つノードか把握し、<strong>クライアントから1ホップでデータの読み書き</strong>することができます。パーティションマップは各ノードと同様、クライアントとクラスタの間でも同期が取られ、更新があった場合にはクライアントが保持しているパーティションマップも更新されます。</p>
<p>読み込みの際、前述の通り直接masterノードへクライアントが直接アクセスし、replicaノードのデータは利用されません。</p>
<p>書き込みの際には、まずmasterとなるノードに直接クライアントからmasterとなるノードにデータが届き、メモリ上に書き込まれます。masterとなるノードはこのデータをreplicaとなるノードに転送し、そのノードのメモリ上に書き込みます。master / replicaとなるノードのメモリ上に書き込みが完了してからクライアントに書き込み成功のレスポンスをします。このメモリ上に書き込まれたデータは<strong>非同期でSSDなどのディスク上に書き込まれ</strong>ます。これがAerospikeの書き込み処理を高速化している一因とも言えるでしょう。</p>
<h1 id="aerospikeを使う前におさえておきたい5つのこと">Aerospikeを使う前におさえておきたい5つのこと</h1>
<p>少々前置きが長くなりましたがここからが本題です.ここからはAerospikeを使う前におさえておきたいことを5つ紹介します。</p>
<h4 id="1保存されたデータからキーを参照することはできない">1.保存されたデータからキーを参照することはできない</h4>
<p>各データはrecordと呼ばれる単位で保存されます。recordは下記のものから構成されています。</p>
<ul>
<li>key
<ul>
<li>レコードを一意に識別するキー。ユーザーが指定したキーをRIPEMD-160によって出されたハッシュ値が入る</li>
</ul>
</li>
<li>metadata
<ul>
<li>recordの編集回数(generation)とTTL(生存時間)</li>
</ul>
</li>
<li>bins(fields)
<ul>
<li>Aerospikeでは単純な値ではなく、値に名前をつけて複数のデータを格納することができる。それぞれをbinと呼び、binの集合をbinsと呼んでいる</li>
</ul>
</li>
</ul>
<p>これらを見てわかるようにkeyはハッシュ化されてしまっているため、キーを指定するのではなく特定領域のすべてのデータを取り出した場合(例えばScanやバックアップ)は上記のデータしか存在しないため元のキーを確認することはできません。</p>
<p>これを回避するためには、冗長ではあるうえ余分に領域を消費しますがbinsにキーを格納しておくことで回避することができます。</p>
<p><em><strong>[2015/12/24 追記]</strong></em></p>
<p>はてブコメントにてクライアントのWritePolicyでキーを値として保存するオプションがあると教えていただきました。ありがとうございます!Cクライアントではas_policy_keyにAS_POLICY_KEY_SENDを指定、JavaクライアントではPolicyクラスのsendKeyをtrueにすることでkeyという名前でbinを新たに作成し、その値に元のキーを入れるようです。</p>
<ul>
<li>http://www.aerospike.com/apidocs/java/com/aerospike/client/policy/Policy.html#sendKey</li>
<li>http://www.aerospike.com/apidocs/c/db/d65/group__client__policies.html</li>
</ul>
<p>アップデートが継続的にされているクライアントには概ねこの機能があるのを見かけた(ソースをgrepしただけなんですが…)んですが、どうやらErlangとPerlのクライアントではこの機能が利用できないようでした…。</p>
<h4 id="2-バックアップをスナップショットで取ることはできない">2. バックアップをスナップショットで取ることはできない</h4>
<p>Aerospikeでバックアップには<strong>asbackup</strong>コマンドを利用します。asbackupコマンドは、バックアップクライアントで実行すると各ノードにmasterデータの取得リクエストが送られます。リクエストを受けた各ノードでは、それらのデータをクライアントに転送しますが、仕組み上データの量に比例してレスポンスが完了するのに時間がかかってしまいます。そのため、サービスを停止せずにバックアップを行うとバックアップ開始時点とバックアップ終了時点で全体でデータの差が生じ、クラスタ全体でのデータの一貫性は崩れてしまいます。</p>
<p>RDBMSでは当たり前のようにMVCC(MultiVersion Concurrency Control)の構造が取られており、サービスを停止せずにバックアップを行っても全体のデータの一貫性が崩れることはありません。そのため、当たり前にできると思ってしまいがちである(かもしれません)が、バックアップの際には注意が必要です。</p>
<h4 id="3-データ個数に比例して再起動に時間がかかる">3. データ個数に比例して再起動に時間がかかる</h4>
<p>Aerospikeでは特定のデータにアクセスする際に、SSDなどのストレージ上のどこに目的のデータが保存されているかを示すPrimary Indexを利用します。Primary Indexはメモリ上に作られ、新たにデータが作成されるたびに増加していきます。</p>
<p>ノードでAerospikeプロセスを起動する際、SSDなどのストレージにデータが存在している場合は、そのデータを読み込みPrimary Indexをメモリ上に作成します。Primary Indexの作成が終了するまでは、そのノードでサービスを開始することはできません。そのため、非常に多くのデータがある場合はPrimary Indexの作成に時間がかかり、すぐに起動することができません。このようなPrimary Indexを作り直しながら起動することをcold startと呼んでいます。</p>
<p>有償版であるEnterprise Editionでは、この問題を解決するためにFast Restartという機能が備わっています。このFast Restartは、Primary Indexをshared memoryに作成することでAerospikeプロセスを起動し直した場合でもPrimary Indexの再作成を省略することができる機能です。しかしOSの再起動などを伴ったAerospikeプロセスの再起動の場合はcold startになり、Primary Indexの再作成が必要になります。</p>
<h4 id="4-削除したデータが削除できていないように見えることがある">4. 削除したデータが削除できていないように見えることがある</h4>
<p>Aerospikeでデータの削除は該当データのPrimary Indexを削除することで実現しています。実際にSSDなどの永続化された内容が削除されるのはEvictionと呼ばれる処理が行われた段階であり、少量のデータを扱っており容量にかなりの余裕がある場合にはこの処理が開始されません。そのため、例えば削除したデータがあるノードでAerospikeプロセスをcold startした場合、残っているデータからPrimary Indexを再度作成してしまい、データを再び読むことができるようになってしまいます。</p>
<p>このような現象が発生するのは条件があります。有効期限を非常に長く、もしくは有効期限を無しとしてデータを扱った時です。有効期限はrecordに保存されているためcold start時に有効期限を確認し、有効期限が切れていたデータであった場合にはPrimary Indexを作成しません。</p>
<p>これを回避するためには削除時にクライアント側で非常に短い有効期限を設定し、その後に削除のリクエストを行うようにする必要があります。</p>
<h4 id="5-ホットキーが存在する場合は特定ノードに負荷が集中する">5. ホットキーが存在する場合は特定ノードに負荷が集中する</h4>
<p>前述の通り、Aerospikeでは読み書きの際にmasterのノードしか利用されません。Aerospikeのクラスタではデータの偏りが発生しないようにし考慮されていますが、リクエストが大量に集中するようなホットキーがある場合は、ホットキーのmasterとなるノードに負荷が集中します。</p>
<p>読み込みの際にreplicaからも読み込みを行うような機能が将来的に実装されれば、この問題は多少改善されるかもしれませんが、現状ではホットキーが発生しないように設計を行う必要があるでしょう。</p>
<h1 id="まとめ">まとめ</h1>
<p>今日は私がAerospikeを見てきた中で「ここは利用前に抑えておいたほうが良い」と思ういうのを5点厳選して紹介しました。</p>
<p>Aerospikeは素晴らしい分散KVSです。非常に高速に処理をすることができるだけではなく、今日ここで紹介しきれていない機能がたくさんあります。しかし、どのようなミドルウェアでも世の中すべての要求のすべてを満たすことができるものはなく今の要求に合っているのか、仕様として妥協できるのかというのを見極めて利用していくべきであると思います。</p>
<p>この記事がこれからAerospikeを利用しようと考えている方々が今の目の前にある要求に合っているかどうかを判断する際の助けになれば幸いです。</p>
<h1 id="参考リンク">参考リンク</h1>
<ul>
<li>http://www.aerospike.com/docs/architecture/data-model.html</li>
<li>http://www.aerospike.com/docs/architecture/primary-index.html</li>
</ul>{"login"=>"admin", "email"=>"hiroki@hirokikana.com", "display_name"=>"hiroki.kana", "first_name"=>"Hiroki", "last_name"=>"Takayasu"}hiroki@hirokikana.comはじめに基本的なカーネルモジュールの実装2015-01-03T10:52:38+00:002015-01-03T10:52:38+00:00http://blog.hirokikana.com/dev/basic_kernel_module<h1 id="はじめに">はじめに</h1>
<p>Open vSwitchのソースを読み進めようと思いたったのですが、カーネルモジュールとして実装されている部分があったのと、その部分が非常に重要になってくる(高速に処理を行うためにカーネルモジュールにしているという売りがあります)ので、今回カーネルモジュールの基本をおさえておこうと思ってまとめた次第です。</p>
<p>カーネルモジュールとは、一般的なプログラムと違い下記のような違いがあります。</p>
<ul>
<li>ユーザー空間ではなくカーネル空間で動作するため、カーネルがアクセスするリソースを使って動作する</li>
<li>カーネルに静的リンクする場合と異なり、Linux起動中に動的に機能を追加・削除することができる</li>
</ul>
<p>カーネルモジュールが必要となるケースの多くは新しいデバイスに対してのデバイスドライバとして利用されることが多く、デバイスの開発者以外の多くの人はユーザー空間で動作するプログラムで十分かもしれません。しかしながら、今回のように読むシチュエーションが出てきた際に非常に役立つと思います。今回は基本的なカーネルモジュールの実装から簡単なキャラクタデバイスを作成するところまでを説明します。</p>
<h1 id="最も簡単なカーネルモジュールhello-world">最も簡単なカーネルモジュール(Hello World)</h1>
<p>まず、カーネルモジュールの開発にあたり必要なカーネルのヘッダやソースなどを導入します。今回はCentOS6.6で行いました。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>yum <span class="nb">install </span>gcc make kernel-devel kernel-headers
</code></pre></div></div>
<p>次に任意の場所にカーネルモジュールを開発するためのディレクトリを作り、そこにソースなどのファイルを作成しています。まずはカーネルモジュール本体を実装します。下記が一番簡単なカーネルモジュールのhello.cです。</p>
<p><strong>hello.c</strong></p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <linux/module.h>
#include <linux/kernel.h>
</span>
<span class="n">MODULE_DESCRIPTION</span><span class="p">(</span><span class="s">"hello kernel module"</span><span class="p">);</span>
<span class="n">MODULE_AUTHOR</span><span class="p">(</span><span class="s">"hiroki.kana@gmail.com"</span><span class="p">);</span>
<span class="n">MODULE_LICENSE</span><span class="p">(</span><span class="s">"GPL"</span><span class="p">);</span>
<span class="k">static</span> <span class="kt">int</span> <span class="nf">init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span><span class="s">"hello module is loaded</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="err"> </span><span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">cleanup</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span><span class="s">"hello module is unloaded</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">module_init</span><span class="p">(</span><span class="n">init</span><span class="p">);</span>
<span class="n">module_exit</span><span class="p">(</span><span class="n">cleanup</span><span class="p">);</span>
</code></pre></div></div>
<p>これはモジュールを登録および削除時に文字列をログに出力するだけのカーネルモジュールです。</p>
<p>1行目と2行目は必要なヘッダをincludeしています。3行目〜5行目はモジュールの概要、開発者、ライセンスに関して記述しています。これらの情報はコンパイル後のkoファイルからmodinfoコマンドで参照することができます。</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">modinfo</span> <span class="n">hello</span><span class="p">.</span><span class="n">ko</span>
<span class="n">filename</span><span class="o">:</span><span class="err"> </span> <span class="n">hello</span><span class="p">.</span><span class="n">ko</span>
<span class="n">license</span><span class="o">:</span><span class="err"> </span> <span class="n">GPL</span>
<span class="n">author</span><span class="o">:</span><span class="err"> </span> <span class="n">hiroki</span><span class="p">.</span><span class="n">kana</span><span class="err">@</span><span class="n">gmail</span><span class="p">.</span><span class="n">com</span>
<span class="n">description</span><span class="o">:</span><span class="err"> </span> <span class="n">hello</span> <span class="n">kernel</span> <span class="n">module</span>
<span class="n">srcversion</span><span class="o">:</span><span class="err"> </span> <span class="n">D2E166AEEDC9CC2AD8EE760</span>
<span class="n">depends</span><span class="o">:</span><span class="err"> </span>
<span class="n">vermagic</span><span class="o">:</span><span class="err"> </span> <span class="mf">2.6.32</span><span class="o">-</span><span class="mf">504.3.3</span><span class="p">.</span><span class="n">el6</span><span class="p">.</span><span class="n">x86_64</span> <span class="n">SMP</span> <span class="n">mod_unload</span> <span class="n">modversions</span>
</code></pre></div></div>
<p>init関数およびcleanup関数は後ほどmodule_init / module_exitで登録をするモジュールを追加・削除した際に行う処理です。登録の際には正常な処理が行えた場合は0を返します。それぞれの関数内で呼ばれているprintkはカーネルコンソールに出力するための関数です。これらはdmesgで参照することができます。カーネルモジュールの実装はこれで以上です。次にコンパイルするためにMakefileを作成します。</p>
<p><strong>Makefile</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>obj-m := hello.o
clean-files := *.o *.ko *.mod.[co] *~
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
</code></pre></div></div>
<hr />
<p>ディレクトリにはhello.cとMakefileの2つが出来上がったと思います。これで準備は完了です。次にコンパイルを行います。ディレクトリ内で引数無しでmakeコマンドを実行すると下記のような出力がされ、hello.koファイルが作成されると思います。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>make
make <span class="nt">-C</span> /lib/modules/2.6.32-504.3.3.el6.x86_64/build <span class="nv">M</span><span class="o">=</span>/home/cloud-user/hello_module modules
make[1]: ディレクトリ /usr/src/kernels/2.6.32-504.3.3.el6.x86_64<span class="s1">' に入ります
CC [M] /home/cloud-user/hello_module/hello.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/cloud-user/hello_module/hello.mod.o
LD [M] /home/cloud-user/hello_module/hello.ko.unsigned
NO SIGN [M] /home/cloud-user/hello_module/hello.ko
make[1]: ディレクトリ /usr/src/kernels/2.6.32-504.3.3.el6.x86_64'</span> から出ます
</code></pre></div></div>
<p>エラーがでなければカーネルモジュールのコンパイルは終了です。最後にカーネルモジュールの追加と削除を行い、dmesgでログが正しく出ているかを確認します。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>insmod hello.ko
<span class="nv">$ </span>lsmod |grep hello
hello 985 0
<span class="nv">$ </span>dmesg |grep hello
hello module is loaded
<span class="nv">$ </span><span class="nb">sudo </span>rmmod hello
<span class="nv">$ </span>dmesg |grep hello
hello module is loaded
hello module is unloaded
</code></pre></div></div>
<p>カーネルモジュールの追加削除にはroot権限が必要でinsmodで追加rmmodで削除を行います。カーネルモジュールの追加を行って正しく組み込まれたかはlsmodコマンドを利用して確認することができます。</p>
<h1 id="ランダムなアルファベットを返すキャラクタデバイスの作成">ランダムなアルファベットを返すキャラクタデバイスの作成</h1>
<p>カーネルモジュールを作成しましたが、次は簡単なキャラクタデバイスのドライバを作成してみます。Linuxにはキャラクタデバイスとブロックデバイスと呼ばれるものがあり、キャラクタデバイスは1文字ずつ文字を送るようなデバイスで、テキストコンソールやシリアルポートなどもキャラクタデバイスの一種です。一方、ブロックデバイスはディスクなどのブロックと呼ばれる単位で読み込みや書き込みを行うデバイスです。今回は実装が簡単なキャラクタデバイスのドライバを作成しました。</p>
<p>実際のコードはgithub上(<a href="https://github.com/hirokikana/randchar">https://github.com/hirokikana/randchar</a>)においてあります。</p>
<p>今回新たに実装したポイントはopen / close / readのシステムコールでデバイスが呼び出された場合の動作をrandchar_open / randchar_release / randchar_readで実装しています。それぞれのシステムコールで呼び出された場合にどの関数を呼び出すかどうかはfile_operations構造体に登録します。</p>
<p><strong>randchar.c (open / close / readの実装)</strong></p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">randchar_open</span><span class="p">(</span> <span class="k">struct</span> <span class="nc">inode</span><span class="o">*</span> <span class="n">inode</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">file</span><span class="o">*</span> <span class="n">filep</span> <span class="p">)</span>
<span class="p">{</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span> <span class="n">KERN_INFO</span> <span class="s">"%s:open() called</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">module_name</span> <span class="p">);</span>
<span class="err"> </span>
<span class="err"> </span><span class="n">spin_lock</span><span class="p">(</span><span class="o">&</span><span class="n">randchar_spin_lock</span><span class="p">);</span>
<span class="err"> </span><span class="k">if</span> <span class="p">(</span> <span class="n">access_num</span> <span class="p">)</span> <span class="p">{</span>
<span class="err"> </span><span class="n">spin_unlock</span> <span class="p">(</span><span class="o">&</span><span class="n">randchar_spin_lock</span><span class="p">);</span>
<span class="err"> </span><span class="k">return</span> <span class="o">-</span><span class="n">EBUSY</span><span class="p">;</span>
<span class="err"> </span><span class="p">}</span>
<span class="err"> </span>
<span class="err"> </span><span class="n">access_num</span><span class="o">++</span><span class="p">;</span>
<span class="err"> </span><span class="n">spin_unlock</span><span class="p">(</span><span class="o">&</span><span class="n">randchar_spin_lock</span><span class="p">);</span>
<span class="err"> </span>
<span class="err"> </span><span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">static</span> <span class="kt">int</span> <span class="nf">randchar_release</span><span class="p">(</span> <span class="k">struct</span> <span class="nc">inode</span><span class="o">*</span> <span class="n">inode</span><span class="p">,</span> <span class="k">struct</span> <span class="nc">file</span><span class="o">*</span> <span class="n">filep</span> <span class="p">)</span>
<span class="p">{</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span> <span class="n">KERN_INFO</span> <span class="s">"%s:close() called</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">module_name</span> <span class="p">);</span>
<span class="err"> </span><span class="n">spin_lock</span><span class="p">(</span><span class="o">&</span><span class="n">randchar_spin_lock</span><span class="p">);</span>
<span class="err"> </span><span class="n">access_num</span><span class="o">--</span><span class="p">;</span>
<span class="err"> </span><span class="n">spin_unlock</span><span class="p">(</span><span class="o">&</span><span class="n">randchar_spin_lock</span><span class="p">);</span>
<span class="err"> </span><span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">static</span> <span class="kt">ssize_t</span> <span class="nf">randchar_read</span><span class="p">(</span> <span class="k">struct</span> <span class="nc">file</span><span class="o">*</span> <span class="n">filep</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span> <span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">count</span><span class="p">,</span> <span class="n">loff_t</span><span class="o">*</span> <span class="n">pos</span><span class="p">)</span>
<span class="p">{</span>
<span class="err"> </span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="p">;</span>
<span class="err"> </span><span class="kt">char</span><span class="o">*</span> <span class="n">alpha</span> <span class="o">=</span> <span class="s">"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"</span><span class="p">;</span>
<span class="err"> </span><span class="n">get_random_bytes</span><span class="p">(</span><span class="o">&</span><span class="n">i</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="err"> </span><span class="k">if</span> <span class="p">(</span><span class="n">copy_to_user</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="o">&</span><span class="n">alpha</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="n">strlen</span><span class="p">(</span><span class="n">alpha</span><span class="p">)],</span> <span class="mi">1</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span><span class="n">KERN_INFO</span> <span class="s">"%s:copy_to_user failed</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">module_name</span><span class="p">);</span>
<span class="err"> </span><span class="k">return</span> <span class="o">-</span><span class="n">EFAULT</span><span class="p">;</span>
<span class="err"> </span><span class="p">}</span>
<span class="err"> </span><span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">static</span> <span class="k">struct</span> <span class="nc">file_operations</span> <span class="n">randchar_fops</span> <span class="o">=</span>
<span class="p">{</span>
<span class="err"> </span><span class="n">owner</span> <span class="o">:</span><span class="n">THIS_MODULE</span><span class="p">,</span>
<span class="err"> </span><span class="n">read</span> <span class="o">:</span><span class="n">randchar_read</span><span class="p">,</span>
<span class="err"> </span><span class="n">open</span> <span class="o">:</span><span class="n">randchar_open</span><span class="p">,</span>
<span class="err"> </span><span class="n">release</span> <span class="o">:</span><span class="n">randchar_release</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>今回作るrandcharデバイスは複数のプロセスで開けないようにしました。そのため、アクセスしているデバイス数をaccess_numに保存します(実際は1もしくは0になります)。access_numの内容を変更する際に競合しないようにspin_lockを利用しています。randchar_readではアルファベットの中からランダムな1文字を返すようにするために、ユーザーバッファにランダムな文字列をコピーし、returnでコピーしたサイズ(今回の場合は必ず1文字)を返します。countにはreadシステムコールで呼ばれた際に返すべきバイト数が入っていますが今回は無視しています。</p>
<p>今回、利用するデバイスファイルは/dev/randcharという名前でメジャー番号(デバイスを認識する番号)を77としました。下記コマンドをrootユーザーで作成することで利用できるようになります。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo mknod</span> /dev/randchar c 77 0
</code></pre></div></div>
<p>デバイス名とメジャー番号はrandchar_initのregister_chrdevでキャラクタデバイスを登録し、モジュール削除時にunregister_chrdevでキャラクタデバイスの登録を解除します。</p>
<p><strong>randchar.c (キャラクタデバイスの登録・削除)</strong></p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">randchar_init</span><span class="p">(</span> <span class="kt">void</span> <span class="p">)</span>
<span class="p">{</span>
<span class="err"> </span><span class="k">if</span> <span class="p">(</span> <span class="n">register_chrdev</span><span class="p">(</span><span class="n">devmajor</span><span class="p">,</span> <span class="n">devname</span><span class="p">,</span> <span class="o">&</span><span class="n">randchar_fops</span> <span class="p">))</span> <span class="p">{</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span> <span class="n">KERN_INFO</span> <span class="s">"%s:register randchar failed</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">module_name</span><span class="p">);</span>
<span class="err"> </span><span class="k">return</span> <span class="o">-</span><span class="n">EBUSY</span><span class="p">;</span>
<span class="err"> </span><span class="p">}</span>
<span class="err"> </span><span class="n">spin_lock_init</span><span class="p">(</span><span class="o">&</span><span class="n">randchar_spin_lock</span><span class="p">);</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span><span class="n">KERN_INFO</span> <span class="s">"%s: loaded into kernel</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">module_name</span><span class="p">);</span>
<span class="err"> </span><span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">randchar_cleanup</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="err"> </span><span class="n">unregister_chrdev</span><span class="p">(</span><span class="n">devmajor</span><span class="p">,</span> <span class="n">devname</span><span class="p">);</span>
<span class="err"> </span><span class="n">printk</span><span class="p">(</span> <span class="n">KERN_INFO</span> <span class="s">"%s: removed fron kernel</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">module_name</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>実際に利用する際にはhello.cと同じくmakeをした後にinsmodし、作成した/dev/randcharに対して読み込みを行うことでランダムなアルファベットを返します。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>make
<span class="nv">$ </span><span class="nb">sudo mknod</span> /dev/randchar c 77 0
<span class="nv">$ </span><span class="nb">sudo </span>insmod randchar.ko
<span class="nv">$ </span><span class="nb">cat</span> /dev/randchar | <span class="nb">fold</span> <span class="nt">-c10</span> |head <span class="nt">-n1</span>
jpdjxnwTmo
</code></pre></div></div>
<p>上記ではfoldで10文字区切りにし、headで1行目を取得するようにしています。これでランダムな10文字のアルファベットが取得できます。</p>
<h1 id="おわりに">おわりに</h1>
<p>今回はカーネルモジュールの基本的な実装方法についてまとめました。冒頭で書いたとおり、デバイスドライバなどを実装するシチュエーションは少なく、いくら読む必要があるからカーネルモジュールやデバイスドライバの開発を行うためのモチベーションを維持するのは難しいと思います。この記事を書くのにあたり参考にした「RaspberryPiで学ぶ ARMデバイスドライバープログラミング 」という書籍ではRaspberry Piにつないだ7セグメントLEDなどの操作を行うためのデバイスドライバを書く方法が説明されており、実際に開発したデバイスドライバでハードウェアが動作するところまで体験することができ、カーネルモジュールやデバイスドライバの開発のモチベーションを維持し続けることができると思います。特に7セグメントLEDのダイナミック駆動時にユーザー空間で動作させる場合にはチラつきが出てしまうため、カーネルモジュールで実装する必然性というのが出てきており、カーネルモジュールで実装する必要性を感じることができ、学習するうえで非常に良いと感じました。とはいえ私もまだ読み切っていないので、すべてを読み切ってからまたまとめようと思います。</p>
<iframe style="width:120px;height:240px;" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" src="//rcm-fe.amazon-adsystem.com/e/cm?lt1=_blank&bc1=000000&IS2=1&bg1=FFFFFF&fc1=000000&lc1=0000FF&t=hirokikana-22&o=9&p=8&l=as4&m=amazon&f=ifr&ref=as_ss_li_til&asins=488337940X&linkId=5373079cca4d637aef235a65b8ceb665"></iframe>
<p>この記事ではカーネルモジュールの基本的な部分しか説明することができていませんが「カーネルモジュールっていうだけでなんとなく難しい」や「デバイス開発者以外関係ないでしょ」と思っていた方が意外と基本的な部分は単純であると感じてもらえたら幸いです。そんな私もまだ実装されたカーネルモジュールを読んだわけではないので、これをきっかけにOpen vSwitchのソースコードを読み進めていこうと思います。</p>{"login"=>"admin", "email"=>"hiroki@hirokikana.com", "display_name"=>"hiroki.kana", "first_name"=>"Hiroki", "last_name"=>"Takayasu"}hiroki@hirokikana.comはじめにOpen vSwitchとRyuを使った最も単純なOpenFlowネットワークの構築2014-12-21T15:42:59+00:002014-12-21T15:42:59+00:00http://blog.hirokikana.com/dev/open-vswitch_ryu_openflow<h1 id="はじめに">はじめに</h1>
<p>この記事は<a href="http://qiita.com/advent-calendar/2014/dwango">ドワンゴ Advent Calendar 2014</a>の22日目の記事です。22日目の今回はOpen vSwitchとRyuを使ったOpenFlowネットワークの構築方法を紹介します。<br />
今回の記事、ドワンゴ Advent Calendarですが、私の現在や過去の業務に密接にOpenFlowが結びついていたわけでもなく、本当に単なる私の趣味によるものであるということを最初に書いておきます。</p>
<h1 id="openflowとは">OpenFlowとは</h1>
<p>SDN(Software Defined Network)という単語を耳にする機会が最近多くなってきたとは感じないでしょうか。SDNというのは今までネットワーク機器が行っていたパケットの経路の制御をソフトウェアで行い、柔軟に制御できるようにすることです。そのソフトウェアはプログラミングすることができ、ネットワークを流れるデータの特性に合わせた制御も可能になります。そのSDNを実現するための技術として注目をされているのがOpenFlowです。</p>
<p>OpenFlowは、ネットワーク機器の制御を行うためのプロトコルでOpen Networking Foundationが中心となり策定を進めています。OpenFlowスイッチ、OpenFlowコントローラー、そしてそれらのやりとりに使われるOpenFlowプロトコルによって構成されています。</p>
<ul>
<li>OpenFlowスイッチ
<ul>
<li>PCや他のネットワーク機器とつながるスイッチ。一般的なスイッチとは異なり、OpenFlowコントローラーにパケットの制御方法を問い合わせし様々な動作をする。一般的にはハードウェアにより提供されるが、Open vSwitchなどのソフトウェア実装もある。</li>
</ul>
</li>
<li>OpenFlowコントローラー
<ul>
<li>OpenFlowスイッチよりパケットの情報を受け取り、経路を決定するソフトウェア。</li>
</ul>
</li>
</ul>
<p>OpenFlowの基本的な動きは「OpenFlowスイッチに流れてきたパケット情報をOpenFlowコントローラーに問い合わせ、パケットの内容によって、パケットを制御をする」という流れです。パケットの内容で確認し判定条件として利用できる部分はヘッダフィールドと呼ばれ下記のようなものがあります。</p>
<ul>
<li>スイッチの入力ポート</li>
<li>送信元MACアドレス</li>
<li>宛先MACアドレス</li>
<li>Ethernetタイプ</li>
<li>VLAN ID</li>
<li>VLAN 優先度</li>
<li>送信元IPアドレス</li>
<li>宛先IPアドレス</li>
<li>IPプロトコル番号</li>
<li>ToSビット</li>
<li>送信元ポート番号</li>
<li>宛先ポート番号</li>
</ul>
<p>ヘッダフィールドを条件として、下記操作を1つもしくは複数指示することができます。</p>
<ul>
<li>Forward
<ul>
<li>パケットを指定ポートへ転送する</li>
</ul>
</li>
<li>Enqueue
<ul>
<li>指定のキューに入れる</li>
</ul>
</li>
<li>Drop
<ul>
<li>パケットを破棄</li>
</ul>
</li>
<li>Modify-Field
<ul>
<li>指定のフィールドを書き換え</li>
</ul>
</li>
</ul>
<p>これらの条件判定をパケットが送られてくるごとにされることなく、1度問い合わせた内容はOpenFlowスイッチのフローテーブルという場所に格納されます。フローテーブルに無い条件のパケットのみがOpenFlowコントローラーに問い合わせが行われます。</p>
<p>これがOpenFlowの動きのすべてで、OpenFlowの細かい動きはすべてOpenFlowコントローラー内で動作するプログラムで定義することができます。</p>
<h1 id="openflowを利用したネットワークを構築するには">OpenFlowを利用したネットワークを構築するには</h1>
<p>必要になるのはOpenFlowコントローラーとOpenFlowスイッチです。OpenFlowコントローラーはオープンソースで提供されているものもありますが、OpenFlowスイッチはハードウェアで提供されているもの(NEC UNIVERGE PFシリーズなど)を揃えようとすると手軽に手を出せる価格のものはありませんし、個人で購入するのも難しいでしょう。</p>
<p>そこでOpenFlowスイッチをソフトウェアとして実装されているものを利用するのが現実的です。今回はOpenFlowスイッチの代表的な実装のOpen vSwitch、また、OpenFlowコントローラーはPythonでコントローラーを実装することができるRyuというフレームワークを利用しOpenFlowネットワークを構築する方法を紹介します。</p>
<h1 id="今回行うこと">今回行うこと</h1>
<p>今回はOpenFlowスイッチとなるマシンとOpenFlowコントローラーを同一のマシンに用意します。さらに仮想マシンだけがアクセスできる内部ネットワークに、内部ネットワークのみにつながっているマシンを用意し、ホストマシンから内部ネットワークのみにつながっているマシンに対してOpenFlowスイッチを経由してアクセスできるようにします。それぞれ仮想マシンはCentOS 6.5、ホストマシンはMac OS X 10.10を利用しました。</p>
<p><a href="http://blog.hirokikana.com/wp-content/uploads/2014/12/dwango_adventcalendar2014_1.jpg"><img src="/assets/dwango_adventcalendar2014_1.jpg" alt="dwango_adventcalendar2014_1" /></a></p>
<h1 id="各マシンの準備">各マシンの準備</h1>
<p>OpenFlowコントローラーおよびOpenFlowスイッチの役割をするマシンを以後ofnw01と呼び、内部ネットワーク内の仮想マシンをinternal01と呼びます。構成ですが、2台のマシンに共通している部分は下記のとおりです。</p>
<ul>
<li>OS
<ul>
<li>CentOS 6.6 x86_64 minimal</li>
</ul>
</li>
<li>メモリ
<ul>
<li>1GB</li>
</ul>
</li>
<li>HDD
<ul>
<li>20GB</li>
</ul>
</li>
<li>CPUコア数
<ul>
<li>1</li>
</ul>
</li>
</ul>
<p>2台のマシンで異なるのはNICの構成です。</p>
<ul>
<li>ofnw01
<ul>
<li>NIC1 : ホストオンリーネットワーク、プロミスキャスモードをすべて許可</li>
<li>NIC2:内部ネットワーク、プロミスキャスモードをすべて許可</li>
<li>NIC3:NAT(インターネットに接続するため)</li>
</ul>
</li>
<li>internal01
<ul>
<li>NIC1:内部ネットワーク</li>
<li>NIC2:NAT(インターネットに接続するため)</li>
</ul>
</li>
</ul>
<p>NATのNICについてはOSインストール後にDHCPでIPアドレスを取得するようにし、インターネットに接続できるようにしておきます。2つのマシンが用意できたら下記コマンドを実行し、OSを最新の状態にアップデートしておきます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># yum update
# reboot
</code></pre></div></div>
<h1 id="openflowスイッチの準備">OpenFlowスイッチの準備</h1>
<p>ofnw01にまずOpenFlowスイッチとして動作するOpen vSwitchをインストールします。今回は最も手軽に導入することができる方法としてRedHat系OS向けのOpenStackリポジトリであるRDOを利用します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># yum install http://rdo.fedorapeople.org/openstack-icehouse/rdo-release-icehouse.rpm
# yum install openvswitch
# service openvswitch start
# chkconfig openvswitch on
</code></pre></div></div>
<p>上記のコマンドを実行するだけで導入することができ、非常に手軽です。執筆時点では2.1.3がインストールされました。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ovs-vsctl show
6d7c9a72-02a7-41cb-abb8-f11bccad3e62
ovs_version:
"2.1.3"
</code></pre></div></div>
<p>次にOpen vSwitchでブリッジの設定を行います。このとき内部ネットワークとホストオンリーネットワークで利用するNICにはIPアドレスが振られていないことを確認したうえで作業を行います。下記コマンドでbr0ブリッジを新規作成し、eth0とeth1をbr0ブリッジにポートとして登録します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ovs-vsctl add-br br0
# ovs-vsctl add-port br0 eth0
# ovs-vsctl add-port br0 eth1
</code></pre></div></div>
<p>図にすると下記のようなイメージになります。</p>
<p><a href="http://blog.hirokikana.com/wp-content/uploads/2014/12/dwango_adventcalendar2014_2.jpg"><img src="/assets/dwango_adventcalendar2014_2.jpg" alt="dwango_adventcalendar2014_2" /></a></p>
<p>この状態でbr0はスイッチングハブとして動作していますので、内部ネットワークとホストオンリーネットワークは疎通している状態になっています。それを確認するために、internal01のeth0に対してホストオンリーネットワーク内で利用できるIPアドレスを付与し、ホストマシンと疎通が取れるか確認します。</p>
<p><strong>internal01の/etc/sysconfig/network-scripts/ifcfg-eth0</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DEVICE=eth0
TYPE=Ethernet
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=static
HWADDR=08:00:27:40:52:0D
IPADDR=172.16.0.200
NETMASK=255.255.255.0
</code></pre></div></div>
<p>上記設定を確認し、ホストマシンからinternal01へpingをしてみます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ping 172.16.0.200
PING 172.16.0.200 (172.16.0.200): 56 data bytes
64 bytes from 172.16.0.200: icmp_seq=0 ttl=64 time=2.424 ms
64 bytes from 172.16.0.200: icmp_seq=1 ttl=64 time=2.436 ms
64 bytes from 172.16.0.200: icmp_seq=2 ttl=64 time=3.052 ms
64 bytes from 172.16.0.200: icmp_seq=3 ttl=64 time=0.877 ms
</code></pre></div></div>
<p>試しにofnw01のopenvswitchを止めてみると疎通しなくなるのがわかると思います。</p>
<p>これでOpenFlowスイッチとして利用するブリッジの準備は完了です。</p>
<h1 id="openflowコントローラーの準備">OpenFlowコントローラーの準備</h1>
<p>次にOpenFlowコントローラーを準備します。今回はOpenFlowスイッチを構築したofnw01上でOpenFlowコントローラーを動作させます。今回利用するOpenFlowコントローラーはRyu(<a href="http://osrg.github.io/ryu/">http://osrg.github.io/ryu/</a>)を利用します。RyuはPythonでOpenFlowコントローラーを実装することができるフレームワークです。まずはRyuを利用するためにPythonのパッケージマネージャーであるpipのインストール後にRyuのインストールを行います。</p>
<p>まずpipを利用するためにepelリポジトリを有効にします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># yum install epel-release
</code></pre></div></div>
<p>次にpipとRyuのビルドに必要なものをインストールし、最後にpipを使いRyuをインストールします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># yum install python-pip python-devel gcc gcc-c++
# pip install ryu
# pip install --upgrade netaddr
</code></pre></div></div>
<p>これでRyuを利用する環境の構築は完了です。</p>
<p>次にRyuでスイッチングハブの役割およびトラフィックモニターを行うこちら(<a href="http://osrg.github.io/ryu-book/ja/html/traffic_monitor.html">http://osrg.github.io/ryu-book/ja/html/traffic_monitor.html</a>)のサンプルを利用します。このサンプルをほとんど利用しているのですが、少し修正をいれてOpenFlowスイッチがOpenFlowコントローラーに接続したときにログ出力を行うようにしたのが下記のコードです。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">operator</span> <span class="kn">import</span> <span class="n">attrgetter</span>
<span class="kn">from</span> <span class="nn">ryu.app</span> <span class="kn">import</span> <span class="n">simple_switch_13</span>
<span class="kn">from</span> <span class="nn">ryu.controller</span> <span class="kn">import</span> <span class="n">ofp_event</span>
<span class="kn">from</span> <span class="nn">ryu.controller.handler</span> <span class="kn">import</span> <span class="n">MAIN_DISPATCHER</span><span class="p">,</span> <span class="n">DEAD_DISPATCHER</span><span class="p">,</span> <span class="n">CONFIG_DISPATCHER</span>
<span class="kn">from</span> <span class="nn">ryu.controller.handler</span> <span class="kn">import</span> <span class="n">set_ev_cls</span>
<span class="kn">from</span> <span class="nn">ryu.lib</span> <span class="kn">import</span> <span class="n">hub</span>
<span class="k">class</span> <span class="nc">SimpleMonitor</span><span class="p">(</span><span class="n">simple_switch_13</span><span class="p">.</span><span class="n">SimpleSwitch13</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="nb">super</span><span class="p">(</span><span class="n">SimpleMonitor</span><span class="p">,</span> <span class="bp">self</span><span class="p">).</span><span class="n">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">datapaths</span> <span class="o">=</span> <span class="p">{}</span>
<span class="bp">self</span><span class="p">.</span><span class="n">monitor_thread</span> <span class="o">=</span> <span class="n">hub</span><span class="p">.</span><span class="n">spawn</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">_monitor</span><span class="p">)</span>
<span class="o">@</span><span class="n">set_ev_cls</span><span class="p">(</span><span class="n">ofp_event</span><span class="p">.</span><span class="n">EventOFPSwitchFeatures</span><span class="p">,</span> <span class="n">CONFIG_DISPATCHER</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">switch_features_handler</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ev</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">'switch joind: datapath: %016x'</span> <span class="o">%</span> <span class="n">ev</span><span class="p">.</span><span class="n">msg</span><span class="p">.</span><span class="n">datapath</span><span class="p">.</span><span class="nb">id</span><span class="p">)</span>
<span class="o">@</span><span class="n">set_ev_cls</span><span class="p">(</span><span class="n">ofp_event</span><span class="p">.</span><span class="n">EventOFPStateChange</span><span class="p">,</span>
<span class="p">[</span><span class="n">MAIN_DISPATCHER</span><span class="p">,</span> <span class="n">DEAD_DISPATCHER</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">_state_change_handler</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ev</span><span class="p">):</span>
<span class="n">datapath</span> <span class="o">=</span> <span class="n">ev</span><span class="p">.</span><span class="n">datapath</span>
<span class="k">if</span> <span class="n">ev</span><span class="p">.</span><span class="n">state</span> <span class="o">==</span> <span class="n">MAIN_DISPATCHER</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">datapath</span><span class="p">.</span><span class="nb">id</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">datapaths</span><span class="p">:</span>
<span class="bp">self</span><span class="p">.</span><span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">'register datapath: %016x'</span><span class="p">,</span> <span class="n">datapath</span><span class="p">.</span><span class="nb">id</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">datapaths</span><span class="p">[</span><span class="n">datapath</span><span class="p">.</span><span class="nb">id</span><span class="p">]</span> <span class="o">=</span> <span class="n">datapath</span>
<span class="k">elif</span> <span class="n">ev</span><span class="p">.</span><span class="n">state</span> <span class="o">==</span> <span class="n">DEAD_DISPATCHER</span><span class="p">:</span>
<span class="k">if</span> <span class="n">datapath</span><span class="p">.</span><span class="nb">id</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">datapaths</span><span class="p">:</span>
<span class="bp">self</span><span class="p">.</span><span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">'unregister datapath: %016x'</span><span class="p">,</span> <span class="n">datapath</span><span class="p">.</span><span class="nb">id</span><span class="p">)</span>
<span class="k">del</span> <span class="bp">self</span><span class="p">.</span><span class="n">datapaths</span><span class="p">[</span><span class="n">datapath</span><span class="p">.</span><span class="nb">id</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">_monitor</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="k">for</span> <span class="n">dp</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">datapaths</span><span class="p">.</span><span class="n">values</span><span class="p">():</span>
<span class="bp">self</span><span class="p">.</span><span class="n">_request_stats</span><span class="p">(</span><span class="n">dp</span><span class="p">)</span>
<span class="n">hub</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_request_stats</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">datapath</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">'send stats request: %016x'</span><span class="p">,</span> <span class="n">datapath</span><span class="p">.</span><span class="nb">id</span><span class="p">)</span>
<span class="n">ofproto</span> <span class="o">=</span> <span class="n">datapath</span><span class="p">.</span><span class="n">ofproto</span>
<span class="n">parser</span> <span class="o">=</span> <span class="n">datapath</span><span class="p">.</span><span class="n">ofproto_parser</span>
<span class="n">req</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">OFPFlowStatsRequest</span><span class="p">(</span><span class="n">datapath</span><span class="p">)</span>
<span class="n">datapath</span><span class="p">.</span><span class="n">send_msg</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
<span class="n">req</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">OFPPortStatsRequest</span><span class="p">(</span><span class="n">datapath</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">ofproto</span><span class="p">.</span><span class="n">OFPP_ANY</span><span class="p">)</span>
<span class="n">datapath</span><span class="p">.</span><span class="n">send_msg</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
<span class="o">@</span><span class="n">set_ev_cls</span><span class="p">(</span><span class="n">ofp_event</span><span class="p">.</span><span class="n">EventOFPFlowStatsReply</span><span class="p">,</span> <span class="n">MAIN_DISPATCHER</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_flow_stats_reply_handler</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ev</span><span class="p">):</span>
<span class="n">body</span> <span class="o">=</span> <span class="n">ev</span><span class="p">.</span><span class="n">msg</span><span class="p">.</span><span class="n">body</span>
<span class="bp">self</span><span class="p">.</span><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">'datapath '</span>
<span class="s">'in-port eth-dst '</span>
<span class="s">'out-port packets bytes'</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">'
</span></code></pre></div></div>{"login"=>"admin", "email"=>"hiroki@hirokikana.com", "display_name"=>"hiroki.kana", "first_name"=>"Hiroki", "last_name"=>"Takayasu"}hiroki@hirokikana.comはじめに