いつかやろうと思ってた独自ドメイン化を完了しました。 このブログは http://d.sunnyone.org/ になります(もとのhttp://sunnyone41.blogspot.jpからはリダイレクト)
h2が大文字化されるとか、邪魔なスタイルがあったので、一緒にスタイルをがっつり変更。今までより見やすくなったと思いますがどうでしょう。
2014-07-06
VyOS と L2TPv3 でハイパーバイザ内部ネットワーク同士をブリッジ接続する
前回ようやく仮想マシン(VM)達を移設するためのPCの準備ができたのだけど、移設する元のPCには、内部のみのネットワーク(VirtualBoxで言うところのHost-Only Network)があり、そこにいくつかVMがぶら下がっているので、これらも移行しないといけない。
一気に移せば何の問題もないのだけど、徐々に移そうとすると、内部のみのネットワークなので、旧ハイパーバイザに残ったVMと新ハイパーバイザに移し終わったVMで通信ができなくなってしまう。
どうしたらいけるかなと考えた結果、「イーサネットフレームを中継するVMを用意すればよくね?」ということで、それを可能にするL2TPv3と、L2TPv3を簡単に使えそうなVyOS (Vyattaのfork)を使って試してみた。
今回の話を図にすると、以下のとおり。
こちらのブログが絵が張ってあってわかりやすそうなので参考に→『Vyatta』からフォークした『VyOS』をインストールしてみた | 俺的備忘録 〜なんかいろいろ〜
あとはホストとしての設定をする必要があるが、とりあえずこんな感じだけやればSSHでログインできる(ブリッジするだけのホストとして考えているので、インターネットに出していくのは想定していない)。
次に「l2tpeth0」インタフェースを構成する。トンネルID/セッションIDは好きに決めてOK.
いったん適用して様子を見る。
br0を構成し、eth1とブリッジする。
なお、eth1にはIPを振っていない。
これで、vybridge1の準備は終了。
vybridge2の「l2tpeth0」インタフェースには、基本的にはvyatta1と対照にする。
br0を構成し、eth1とブリッジする。
これでおしまい。あとは、新ハイパーバイザのVMから、旧ハイパーバイザのVMに通信してみて通信できればOK。
なお、今回の構成では暗号化はされないので、もし必要ならIPSecの上に乗せるなど、別途考える必要がある。
しかしながら、かなりお手軽にL2ブリッジを構成できるので覚えておいて損はないと思う。
一気に移せば何の問題もないのだけど、徐々に移そうとすると、内部のみのネットワークなので、旧ハイパーバイザに残ったVMと新ハイパーバイザに移し終わったVMで通信ができなくなってしまう。
どうしたらいけるかなと考えた結果、「イーサネットフレームを中継するVMを用意すればよくね?」ということで、それを可能にするL2TPv3と、L2TPv3を簡単に使えそうなVyOS (Vyattaのfork)を使って試してみた。
今回の話を図にすると、以下のとおり。
環境 / 利用ソフトウェア
- 仮想マシンハイパーバイザ 2台。
- VyOS v1.1 Helium (開発版なので注意:今回はVyOS-virt-livecd-1407022200-7e8544a-i386.isoを利用)
設定方法
VyOS インストール
まず、それぞれのハイパーバイザにVyOS をインストールする必要があるが、これはISOイメージでブートしてvyos / vyosでログイン、install imageと打てば指示に従うだけでOK。こちらのブログが絵が張ってあってわかりやすそうなので参考に→『Vyatta』からフォークした『VyOS』をインストールしてみた | 俺的備忘録 〜なんかいろいろ〜
あとはホストとしての設定をする必要があるが、とりあえずこんな感じだけやればSSHでログインできる(ブリッジするだけのホストとして考えているので、インターネットに出していくのは想定していない)。
$ configure # set interfaces ethernet ethX address '192.168.XXX.YYY/24' (あとで出てくるので値は省略) # set system host-name vybridge1 (あるいはvybridge2) # set service ssh port 22 # commit # save以下、saveは省略するが、適宜保存する。
vybridge1の設定
まずeth0インタフェースにIPアドレスを振る。$ configure # set interfaces ethernet eth0 address '192.168.100.241/24'
次に「l2tpeth0」インタフェースを構成する。トンネルID/セッションIDは好きに決めてOK.
# set interfaces l2tpv3 l2tpeth0 # set interfaces l2tpv3 l2tpeth0 local-ip 192.168.100.241 # set interfaces l2tpv3 l2tpeth0 remote-ip 192.168.100.242 # set interfaces l2tpv3 l2tpeth0 tunnel-id 41 # set interfaces l2tpv3 l2tpeth0 peer-tunnel-id 42 # set interfaces l2tpv3 l2tpeth0 session-id 1 # set interfaces l2tpv3 l2tpeth0 peer-session-id 2 # set interfaces l2tpv3 l2tpeth0 source-port 5000 # set interfaces l2tpv3 l2tpeth0 destination-port 5001
いったん適用して様子を見る。
# commit # exit $ show interfaces l2tpv3 detail l2tpeth0:l2tpeth0ができていることを確認する。mtu 1488 qdisc pfifo_fast state UNKNOWN group default qlen 1000 link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff inet6 XXXX::XXXX:XXXX:XXXX:XXXX/64 scope link valid_lft forever preferred_lft forever RX: bytes packets errors dropped overrun mcast 0 0 0 0 0 0 TX: bytes packets errors dropped carrier collisions 738 7 0 0 0 0
br0を構成し、eth1とブリッジする。
$ configure
# set interfaces bridge br0
# set interfaces ethernet eth1 bridge-group bridge br0
# set interfaces l2tpv3 l2tpeth0 bridge-group bridge br0
# commit
# exit
$ show interfaces
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface IP Address S/L Description
--------- ---------- --- -----------
br0 - u/u
eth0 192.168.100.241/24 u/u
eth1 - u/u
l2tpeth0 - u/u
lo 127.0.0.1/8 u/u
::1/128
$ show bridge br0
bridge name bridge id STP enabled interfaces
br0 0000.xxxxxxxxxxxx no eth1
l2tpeth0
br0ができていて、eth1とl2tpeth0が入っていることがわかる。なお、eth1にはIPを振っていない。
これで、vybridge1の準備は終了。
vybridge2の設定
vybridge2の設定もほぼ同様。$ configure # set interfaces ethernet eth0 address '192.168.100.242/24'
vybridge2の「l2tpeth0」インタフェースには、基本的にはvyatta1と対照にする。
# set interfaces l2tpv3 l2tpeth0 # set interfaces l2tpv3 l2tpeth0 local-ip 192.168.100.242 # set interfaces l2tpv3 l2tpeth0 remote-ip 192.168.100.241 # set interfaces l2tpv3 l2tpeth0 tunnel-id 42 # set interfaces l2tpv3 l2tpeth0 peer-tunnel-id 41 # set interfaces l2tpv3 l2tpeth0 session-id 2 # set interfaces l2tpv3 l2tpeth0 peer-session-id 1 # set interfaces l2tpv3 l2tpeth0 source-port 5001 # set interfaces l2tpv3 l2tpeth0 destination-port 5000
br0を構成し、eth1とブリッジする。
$ configure # set interfaces bridge br0 # set interfaces ethernet eth1 bridge-group bridge br0 # set interfaces l2tpv3 l2tpeth0 bridge-group bridge br0 # commit
これでおしまい。あとは、新ハイパーバイザのVMから、旧ハイパーバイザのVMに通信してみて通信できればOK。
備考
実際のところ、今回の要件では一気に移行すればいいだけだったので、こんなことは必要ない。必要な場合であっても、Production環境でこのような使い方をするのであれば、L2VPNはオーバーヘッドが大きいのでパフォーマンスの検証をすべきと思う。(そもそも今のところVyOSの開発版についている機能のようなので、安定版では使えないのだけど)なお、今回の構成では暗号化はされないので、もし必要ならIPSecの上に乗せるなど、別途考える必要がある。
しかしながら、かなりお手軽にL2ブリッジを構成できるので覚えておいて損はないと思う。
参考
- VyattaでL2TPv3 ethernet pseudowire を使ってみる | Blog的な何か
- 今回の機能そのものが、ここで書かれているものが取り込まれたものらしい。取り込まれたものは若干コマンド(sport→source-portとか)が違うので注意。
- L2TPv3を用いたL2VPN
- 例によって自社機器の設定だけでなく技術詳細までわかりやすいYAMAHAさんのページ。
2014-07-03
KVMの仮想マシンをWebブラウザから管理する
自分用にLinux KVM (Kernel-based Virtual Machine) で検証用他の仮想マシン達を立てている。その仮想マシンの管理に、今まではvirshコマンドと仮想マシンマネージャー (virt-manager)を使っていたが、各VMの画面が見たいとなると、VNCでXにログインして... という感じで煩わしかった。そこで、ハイパーバイザとなるPCのリプレースを契機に、Web管理ツールのWebVirtMgrを使って、Webブラウザから管理できるようにしてみた。
こんな感じの画面がWebブラウザから使えるようになる。
WebVirtMgrはPython + Djangoで書かれたWebアプリケーションなので、それが動くようにする。基本的には、https://github.com/retspen/webvirtmgr/wiki/Install-WebVirtMgr に書かれている手順の通り。このページには、RedHat/CentOS/Debianなんかのときの方法も書いてある。
ここで本来の手順は「sudo pip install -r requirements.txt」なのだけど、パッケージ管理されないファイルが増えてしまう。requirements.txtを見ると、書いてあるライブラリがUbuntu 14.04のバージョン的にいけそうだったのでパッケージで入れることにした。
スクリプトで初期設定をする。
ディレクトリをまるっと移動する。
/etc/supervisor/conf.d/webvirtmgr.confに以下の内容を書く。
supervisorのリスタート(これで8000/tcpで立ち上がる)
/etc/nginx/sites-available/webvirtmgrに以下を記述する。
これで http://hostname:8008/ にアクセスすると画面が見えるようになる。
(コメントにnova-novncproxyとか書いてあったのでOpenStack Novaから持ってきたのであろう...)
ここの設定は https://www.webvirtmgr.net/docs/ を参考にした(証明書切れてる...)。
/etc/default/libvirt-binのlibvirtd_optsの行に-lを足して、tcpでlistenするようにする。
/etc/libvirt/libvirtd.conf を以下のように編集する。今回はハイパーバイザが同一ホストなので127.0.0.1でlistenさせる。
libvirt用のパスワードを設定する。
libvirtdを再起動する。
以下のようにして接続できることを確認する。
あとは画面ベースなので詳細は省略するが、以下のように設定すれば使えるようになる。
こんな感じの画面がWebブラウザから使えるようになる。
環境/前提
- Ubuntu 14.04 Server
- libvirtでKVMの仮想マシンを起動できる状態
- (BIOS設定他+apt-get install qemu-kvm libvirt-bin的な状態)
Web側の設定
WebVirtMgrはPython + Djangoで書かれたWebアプリケーションなので、それが動くようにする。基本的には、https://github.com/retspen/webvirtmgr/wiki/Install-WebVirtMgr に書かれている手順の通り。このページには、RedHat/CentOS/Debianなんかのときの方法も書いてある。
Python他のインストール
apt-getで入れる。$ sudo apt-get install git python-pip python-libvirt python-libxml2 novnc supervisor nginx
アプリケーションのダウンロードと設定
gitで落としてきて、いくつか設定する。$ git clone git://github.com/retspen/webvirtmgr.git $ cd webvirtmgr
ここで本来の手順は「sudo pip install -r requirements.txt」なのだけど、パッケージ管理されないファイルが増えてしまう。requirements.txtを見ると、書いてあるライブラリがUbuntu 14.04のバージョン的にいけそうだったのでパッケージで入れることにした。
$ sudo apt-get install python-django gunicorn python-lockfile
スクリプトで初期設定をする。
$ ./manage.py syncdb You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (leave blank to use 'ユーザ名'): Email address: user@example.org Password: パスワード Password (again): パスワード Superuser created successfully. $ ./manage.py collectstatic
ディレクトリをまるっと移動する。
$ sudo mkdir /var/www $ cd .. $ sudo mv webvirtmgr /var/www/ $ sudo chown -R www-data:www-data /var/www/webvirtmgr
バックグラウンド起動の設定
バックグラウンドで起動するようにsupervisorを設定する。/etc/supervisor/conf.d/webvirtmgr.confに以下の内容を書く。
[program:webvirtmgr] command=/usr/bin/python /var/www/webvirtmgr/manage.py run_gunicorn -c /var/www/webvirtmgr/conf/gunicorn.conf.py directory=/var/www/webvirtmgr autostart=true autorestart=true stdout_logfile=/var/log/supervisor/webvirtmgr.log redirect_stderr=true user=www-data
supervisorのリスタート(これで8000/tcpで立ち上がる)
$ sudo service supervisor restart
フロントのWebサーバの設定
フロントに立つnginxを設定する。プロキシしているだけなので、Apacheでもいいと思うが、サンプルがこうなっているのでそうした。/etc/nginx/sites-available/webvirtmgrに以下を記述する。
server {
listen 8008 default_server;
server_name $hostname;
#access_log /var/log/nginx/webvirtmgr_access_log;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Proto $remote_addr;
}
}
$ sudo ln -s /etc/nginx/sites-available/webvirtmgr /etc/nginx/sites-enabled/webvirtmgr $ service nginx restart
これで http://hostname:8008/ にアクセスすると画面が見えるようになる。
novncの設定
ブラウザからコンソールを使うためのnovncの設定。この設定ファイルもwebvirtmgrに入っているので、使うだけでOK.$ cd /var/www/webvirtmgr $ sudo cp conf/initd/webvirtmgr-novnc-ubuntu /etc/init.d/webvirtmgr-novnc $ sudo service webvirtmgr-novnc start $ sudo update-rc.d webvirtmgr-novnc defaults
(コメントにnova-novncproxyとか書いてあったのでOpenStack Novaから持ってきたのであろう...)
libvirtの設定
WebVirtMgrはlibvirtdにTCP接続するので、その設定をする(なので、ハイパーバイザは別ホストでもOK)ここの設定は https://www.webvirtmgr.net/docs/ を参考にした(証明書切れてる...)。
/etc/default/libvirt-binのlibvirtd_optsの行に-lを足して、tcpでlistenするようにする。
libvirtd_opts="-d -l"
/etc/libvirt/libvirtd.conf を以下のように編集する。今回はハイパーバイザが同一ホストなので127.0.0.1でlistenさせる。
listen_tls = 0 listen_tcp = 1 listen_addr = "127.0.0.1"
libvirt用のパスワードを設定する。
$ sudo apt-get install sasl2-bin $ sudo saslpasswd2 -a libvirt ユーザ名 Password: Again (for verification): $ sudo sasldblistusers2 -f /etc/libvirt/passwd.db ユーザ名@ホスト名: userPassword
libvirtdを再起動する。
$ service libvirt-bin restart
以下のようにして接続できることを確認する。
$ virsh -c qemu+tcp://127.0.0.1/system nodeinfo Please enter your authentication name: ユーザ名@ホスト名 Please enter your password: CPU model: x86_64 CPU(s): 8 CPU frequency: 800 MHz CPU socket(s): 1 Core(s) per socket: 4 Thread(s) per core: 2 NUMA cell(s): 1 Memory size: 16318576 KiB
Web画面にログインしての設定
http://hostname:8008/ にアクセスすると、ログイン画面が表示されるので、最初の「syncdb」を実行したときのユーザ/パスワードでログインする。あとは画面ベースなので詳細は省略するが、以下のように設定すれば使えるようになる。
- Servers List画面で127.0.0.1を追加する。
- Storage Pools画面で、仮想マシン用のディレクトリorLVMプールとISOを配置するディレクトリを追加する。
- Networks画面でネットワークを追加する。
- Instances画面でインスタンスを追加する。
感想
OpenStackをこのようにVMを管理するために使おうと検証してみたのだけど、LVMのストレージやOpen vSwitchのブリッジを自前でIDを振って管理したりしていて、トラブルがあったときに追いづらくなるデメリットのほうが大きそうだったのでやめた。一方で、このWebVirtMgrはlibvirtやLVMをそのまま見せる設計思想なので、何かあったときにも追いやすく、1台~数台の管理ならかなり向いていそうだった。2014-07-01
GPT パーティションテーブルを別のディスクにコピーする
sdaからsdbにパーティションテーブルをコピーするには、sgdiskを使うのが簡単。コマンドを先に書くとこう。不用意に実行するとさっくり壊れるので注意。
参考: http://askubuntu.com/questions/57908/how-can-i-quickly-copy-a-gpt-partition-scheme-from-one-hard-drive-to-another
正直デフォルトで入っていて欲しい...
ここで以下を実行することでパーティションテーブルをコピーできる。
これだけでコピーされているのだが、ディスクとパーティションに振られた固有IDまでコピーされてしまっている。
以下はディスク本体と2番目のパーティションの状態。
そこで、sgdisk -Gで振りなおす。
そうすると、IDが新しくなっていることがわかる。
# sgdisk -R=/dev/sdb /dev/sda # sgdisk -G /dev/sdbsgdisk --backup sda.table /dev/sdaとかしておいたほうが安心かも?
参考: http://askubuntu.com/questions/57908/how-can-i-quickly-copy-a-gpt-partition-scheme-from-one-hard-drive-to-another
sgdiskがないとき
GPT版のfdiskであるところのgdiskをインストールする。# apt-get install gdiskcfdisk, sfdiskがあるように、cgdisk, sgdiskも入ってくる。ただし、コマンドラインオプションは違うので注意。
正直デフォルトで入っていて欲しい...
コマンドの説明
前提として、こんなディスクが二つあるとする。$ sudo sgdisk -p /dev/sda Disk /dev/sda: 16777216 sectors, 8.0 GiB Logical sector size: 512 bytes Disk identifier (GUID): 1519615B-B82D-4CA6-9849-28F3198A90E7 Partition table holds up to 128 entries First usable sector is 34, last usable sector is 16777182 Partitions will be aligned on 2048-sector boundaries Total free space is 3102653 sectors (1.5 GiB) Number Start (sector) End (sector) Size Code Name 1 2048 4095 1024.0 KiB EF02 2 4096 13676543 6.5 GiB FD00 $ sudo sgdisk -p /dev/sdb Creating new GPT entries. Disk /dev/sdb: 16777216 sectors, 8.0 GiB Logical sector size: 512 bytes Disk identifier (GUID): 65906D3D-B672-4373-AB55-F0C473EC52ED Partition table holds up to 128 entries First usable sector is 34, last usable sector is 16777182 Partitions will be aligned on 2048-sector boundaries Total free space is 16777149 sectors (8.0 GiB) Number Start (sector) End (sector) Size Code Name
ここで以下を実行することでパーティションテーブルをコピーできる。
$ sudo sgdisk -R=/dev/sdb /dev/sda The operation has completed successfully.
これだけでコピーされているのだが、ディスクとパーティションに振られた固有IDまでコピーされてしまっている。
以下はディスク本体と2番目のパーティションの状態。
$ sudo sgdisk /dev/sda -p -i 2 Disk /dev/sda: 16777216 sectors, 8.0 GiB Logical sector size: 512 bytes Disk identifier (GUID): 1519615B-B82D-4CA6-9849-28F3198A90E7 Partition table holds up to 128 entries First usable sector is 34, last usable sector is 16777182 Partitions will be aligned on 2048-sector boundaries Total free space is 3102653 sectors (1.5 GiB) Number Start (sector) End (sector) Size Code Name 1 2048 4095 1024.0 KiB EF02 2 4096 13676543 6.5 GiB FD00 Partition GUID code: A19D880F-05FC-4D3B-A006-743F0F84911E (Linux RAID) Partition unique GUID: CEA27178-6FB4-4358-B921-C75352CE33D7 First sector: 4096 (at 2.0 MiB) Last sector: 13676543 (at 6.5 GiB) Partition size: 13672448 sectors (6.5 GiB) Attribute flags: 0000000000000000 Partition name: '' $ sudo sgdisk /dev/sdb -p -i 2 Disk /dev/sdb: 16777216 sectors, 8.0 GiB Logical sector size: 512 bytes Disk identifier (GUID): 1519615B-B82D-4CA6-9849-28F3198A90E7 ←同じ Partition table holds up to 128 entries First usable sector is 34, last usable sector is 16777182 Partitions will be aligned on 2048-sector boundaries Total free space is 3102653 sectors (1.5 GiB) Number Start (sector) End (sector) Size Code Name 1 2048 4095 1024.0 KiB EF02 2 4096 13676543 6.5 GiB FD00 Partition GUID code: A19D880F-05FC-4D3B-A006-743F0F84911E (Linux RAID) Partition unique GUID: CEA27178-6FB4-4358-B921-C75352CE33D7 ←同じ First sector: 4096 (at 2.0 MiB) Last sector: 13676543 (at 6.5 GiB) Partition size: 13672448 sectors (6.5 GiB) Attribute flags: 0000000000000000 Partition name: ''
そこで、sgdisk -Gで振りなおす。
$ sgdisk -G /dev/sdb
そうすると、IDが新しくなっていることがわかる。
$ sudo sgdisk /dev/sdb -p -i 2 Disk /dev/sdb: 16777216 sectors, 8.0 GiB Logical sector size: 512 bytes Disk identifier (GUID): CCC50747-7D41-4662-9FFE-93E7EE7644A3 ←変わっている Partition table holds up to 128 entries First usable sector is 34, last usable sector is 16777182 Partitions will be aligned on 2048-sector boundaries Total free space is 3102653 sectors (1.5 GiB) Number Start (sector) End (sector) Size Code Name 1 2048 4095 1024.0 KiB EF02 2 4096 13676543 6.5 GiB FD00 Partition GUID code: A19D880F-05FC-4D3B-A006-743F0F84911E (Linux RAID) Partition unique GUID: 29394950-7433-4913-8BA9-64E36B979394 ←変わっている First sector: 4096 (at 2.0 MiB) Last sector: 13676543 (at 6.5 GiB) Partition size: 13672448 sectors (6.5 GiB) Attribute flags: 0000000000000000 Partition name: ''
2014-06-14
Haswell Refreshで新しいPCを組んだ
KVMでVMをいくつもホストしている自宅のPCが壊れてしまった(といってもおそらくファンだけなのだけど)ので、ケースを捨てるのはたいへんだという理由で中身を入れ替えることにした。備忘の意味で構成を記録しておく。
CPUとHDD以外の部品は秋葉原のツクモの店員さんに相談しながら決めた。アキバのPCショップの店員さんの知識は信じられないくらいマニアックでおもしろいので、荷物は重くなるけれど、通販はしないことにしている。
ATXマシンを作るのは壊れたこのマシンを作ったとき以来で、壊れたマシンを作ったのはPhenom初代なのでPhenom IIが出る2008年より前、つまり4年以上前になる。そしてずっとAMDでやってきたので、Intelに戻ってきたのはCoppermine以来、つまり10年以上前、下手すると15年くらい前。時が経つのはおそろしい…
このPCを新しいVMの親玉にすべくOpenStackを調べているのだけど、どうもそんなチープな世界を想定したプロダクトではないらしく、おそろしく仰々しい。見ていると時代の進化を追えておもしろいのだけど、たぶん本番はもっとローレベルなものになるはず。
CPUとHDD以外の部品は秋葉原のツクモの店員さんに相談しながら決めた。アキバのPCショップの店員さんの知識は信じられないくらいマニアックでおもしろいので、荷物は重くなるけれど、通販はしないことにしている。
- CPU
- Core i7 4790S (Haswell Refresh)
- M/B
- GIGABYTE GA-Z87X-UD3H
- Memory
- CFD ELIXIR W3U1600HQ-8GC11 (8GBx2)
- HDD
- ST3000DM001 x2
- 電源
- Corsair HX650 CP-9020030-JP (650W)
- CPU Cooler
- Thermalright TRUE Spirit 120M(BW)
ATXマシンを作るのは壊れたこのマシンを作ったとき以来で、壊れたマシンを作ったのはPhenom初代なのでPhenom IIが出る2008年より前、つまり4年以上前になる。そしてずっとAMDでやってきたので、Intelに戻ってきたのはCoppermine以来、つまり10年以上前、下手すると15年くらい前。時が経つのはおそろしい…
このPCを新しいVMの親玉にすべくOpenStackを調べているのだけど、どうもそんなチープな世界を想定したプロダクトではないらしく、おそろしく仰々しい。見ていると時代の進化を追えておもしろいのだけど、たぶん本番はもっとローレベルなものになるはず。
2014-06-03
PSCmdletでもasync/awaitを使いたい
PowerShellコマンドレットをC#で作ること自体は簡単で、単にSystem.Management.Automationアセンブリを参照してCmdletかPSCmdletクラスを継承したクラスを含むクラスライブラリを作るだけなのだけど(このへんを参照→How to Write a Simple Cmdlet) うっかりasync/awaitしたあとにWriteObjectでオブジェクトを出力しようとしたりすると、以下のように怒られてしまう。
Microsoft カスタマー サポート サービスにお問い合わせしたらこんなことも教えてくれるの?と思うところだけど、まぁこういう状況ではSynchronizationContextを提供してあげればよくね?ということでオレオレSynchronizationContextを準備して提供するようにしたのがこのAwaitablePSCmdletクラス。
使い方は簡単で、PSCmdletクラスの代わりにこのAwaitablePSCmdletクラスを継承して、BeginProcessing/ProcessRecord/EndProcessingの代わりにAsyncをつけたBeginProcessingAsync/ProcessRecordAsync/EndProcessingAsyncを実装すればいいだけ。
例えばこんな感じ。
本当はStopProcessingにCancellationTokenまわりとか実装しないといけないのだろうけど、たぶんあとは微調整できるレベルだと思うので、必要に応じて。
Get-SampleCmdlet : WriteObject メソッドと WriteError メソッドは、BeginProcessingメソッド、ProcessRecord メソッド、および EndProcessing メソッドの上書きの外側から呼び出すことはできず、同じスレッド内からだけ呼び出すことができます。コマンドレットで呼び出しが正しく作成されていることを確認するか、または Microsoft カスタマー サポート サービスにお問い合わせください。
Microsoft カスタマー サポート サービスにお問い合わせしたらこんなことも教えてくれるの?と思うところだけど、まぁこういう状況ではSynchronizationContextを提供してあげればよくね?ということでオレオレSynchronizationContextを準備して提供するようにしたのがこのAwaitablePSCmdletクラス。
使い方は簡単で、PSCmdletクラスの代わりにこのAwaitablePSCmdletクラスを継承して、BeginProcessing/ProcessRecord/EndProcessingの代わりにAsyncをつけたBeginProcessingAsync/ProcessRecordAsync/EndProcessingAsyncを実装すればいいだけ。
例えばこんな感じ。
本当はStopProcessingにCancellationTokenまわりとか実装しないといけないのだろうけど、たぶんあとは微調整できるレベルだと思うので、必要に応じて。
2014-05-11
MSILでわかるC# のラムダ式
そういえばLINQ to Objectsでコードを書いたときに裏でどういう動きになっているのかなぁ、とILでイメージができなかったので読んでみたら、単にC#のラムダ式がどうなっているかという話だけだったのでまとめる。
長いのでサマリ:
評価スタックでピンとこない方は「C#でHelloWorldプログラムを作成する」を読んでおくのをおすすめする。
今回の説明用のサンプルコードはこちら。「.Count(x => x == val)」がどうなっていくのか、というお話。
サイズ0のint配列に100がいくつあるか調べてWriteするというコード(0に決まっている)。説明の都合、順番などが不自然な感じになっている。
このソースをILに逆アセンブルした結果がこちら。
https://gist.github.com/sunnyone/0d3ed8285b5d91495399
別ウィンドウで開きながら説明を見るといいかも。
さて、これからILを見ていく。書いた部分がコンパイルされているであろうMainメソッドの実装を見ようとすると、そのMainメソッドの前に見知らぬ「<>c__DisplayClass1」という入れ子クラスが作られていることがわかる。
そのクラスの内容はこうなっている。
このクラスは要は「x => x == val」の部分を<Main>b__0というメソッドに実装し、加えてvalフィールドを持っている。
次に本体のMainメソッド。Mainメソッドはこの入れ子クラスを活用して動作する。
まずメソッドとローカル変数の定義。format, array, countのほかに、先ほどの<>c__DisplayClass1が「CS$<>8__locals2」として用意されているのがわかる。valはないことに注意。
ここから処理開始だが、記述したC#コードに対応する部分に先立って、<>c__DisplayClass1がnewされ、ローカル変数に入る。
次に「int val = 100;」の部分。ここがポイント。
C#コード上ではローカル変数に見えているが、実際には内部の<>c__DisplayClass1クラスのフィールドになっている。
入れ子になったクラスにあるラムダ式の実体がこの変数を使うためにフィールドに入れている。
次に「var format = "Count of {0}: {1}";」の部分。このように、ラムダ式と関係ない部分はふつうのローカル変数になる。
次に「var array = new int[0];」だが、ここもローカル変数に入れるだけ。
次に実際のラムダ式が登場する「var count = array.Count(x => x == val);」の部分。
先ほどvalのために生成したDisplayClassのメソッドを使ってFuncオブジェクトを生成し、Countメソッドに渡している。
最後に「System.Console.WriteLine(format, val, count);」の部分。
ポイントはvalを使うのにラムダ式用の<>c__DisplayClass1を使っているところ。
このように、ラムダ式は、必要な変数を入れ子クラスのインスタンスにとっておいて、ラムダ式に書いた内容のメソッドが実行される、という形で実装されている。
しかし、ラムダ式があれば必ず入れ子クラスが作られるかというとそうではなく、変数をとっておく必要がない場合、違う形にコンパイルされる。
たとえば、先のコードのint val = 100;にconstをつけてconst int val = 100;にするだけで、ILはこうなってしまう。
こっちのコードだとご丁寧にもFuncオブジェクトをstaticフィールドにキャッシュしている。
ちなみに、valをローカル変数ではなくフィールドに持つようなクラスを作った場合も上述のconstの形に近くなり、入れ子クラスは作られない(Funcオブジェクトのキャッシュはしなくなる)。
まぁ、この差異はプログラム全体からしたらたいしたことはないと思うので、書くときに意識することはないと思うが、知っておいても悪くない…かな?
(一応、array.Count(x => x == val)を10000000回実行したら、valがconst/フィールド/ローカル変数それぞれの場合でStopwatchクラス読みで240ms/240ms/300msだった)
こうしてILコードを見ていると、ラムダ式がどういうものなのかしっくりきたのだけど、みなさまはどうだろうか?
長いのでサマリ:
- ラムダ式を使うと、基本的に入れ子クラスが作られ、ラムダ式が使う変数をとっておかれる。
- ラムダ式の中身に書いたものは、基本的に入れ子クラスのメソッドとして定義される。
- 入れ子クラスは不要なときには作られない。
評価スタックでピンとこない方は「C#でHelloWorldプログラムを作成する」を読んでおくのをおすすめする。
今回の説明用のサンプルコードはこちら。「.Count(x => x == val)」がどうなっていくのか、というお話。
namespace ConsoleApplicationLinq
{
class Program
{
static void Main(string[] args)
{
int val = 100;
var format = "Count of {0}: {1}";
var array = new int[0];
var count = array.Count(x => x == val);
System.Console.WriteLine(format, val, count);
}
}
}
サイズ0のint配列に100がいくつあるか調べてWriteするというコード(0に決まっている)。説明の都合、順番などが不自然な感じになっている。
このソースをILに逆アセンブルした結果がこちら。
https://gist.github.com/sunnyone/0d3ed8285b5d91495399
別ウィンドウで開きながら説明を見るといいかも。
さて、これからILを見ていく。書いた部分がコンパイルされているであろうMainメソッドの実装を見ようとすると、そのMainメソッドの前に見知らぬ「<>c__DisplayClass1」という入れ子クラスが作られていることがわかる。
そのクラスの内容はこうなっている。
.class auto ansi sealed nested private beforefieldinit '<>c__DisplayClass1'
extends [mscorlib]System.Object
{
(CompilerGeneratedAttributeの部分は省略)
.field public int32 val // valフィールド
(ここにコンストラクタの定義があるが省略)
// <Main>b__0メソッドの定義
.method public hidebysig instance bool
'<Main>b__0'(int32 x) cil managed
{
.maxstack 8
// xをロード(評価スタックにpush)
IL_0000: ldarg.1
// valフィールドをロード
IL_0001: ldarg.0
IL_0002: ldfld int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
// 二つの値=xとvalの値を比較し、同じなら1・異なるときは0
IL_0007: ceq
// return
IL_0009: ret
} // end of method '<>c__DisplayClass1'::'<Main>b__0'
} // end of class '<>c__DisplayClass1'
このクラスは要は「x => x == val」の部分を<Main>b__0というメソッドに実装し、加えてvalフィールドを持っている。
次に本体のMainメソッド。Mainメソッドはこの入れ子クラスを活用して動作する。
まずメソッドとローカル変数の定義。format, array, countのほかに、先ほどの<>c__DisplayClass1が「CS$<>8__locals2」として用意されているのがわかる。valはないことに注意。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 3
.locals init ([0] string format,
[1] int32[] 'array',
[2] int32 count,
[3] class ConsoleApplicationLinq.Program/'<>c__DisplayClass1' 'CS$<>8__locals2')
ここから処理開始だが、記述したC#コードに対応する部分に先立って、<>c__DisplayClass1がnewされ、ローカル変数に入る。
// <>c__DisplayClass1クラスのインスタンスを生成してローカル変数3番目にセット
IL_0000: newobj instance void ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::.ctor()
IL_0005: stloc.3
次に「int val = 100;」の部分。ここがポイント。
C#コード上ではローカル変数に見えているが、実際には内部の<>c__DisplayClass1クラスのフィールドになっている。
入れ子になったクラスにあるラムダ式の実体がこの変数を使うためにフィールドに入れている。
// CS$<>8__locals2と「100」をロードして、CS$<>8__locals2のvalにセット
IL_0006: ldloc.3
IL_0007: ldc.i4.s 100
IL_0009: stfld int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
次に「var format = "Count of {0}: {1}";」の部分。このように、ラムダ式と関係ない部分はふつうのローカル変数になる。
// 説明省略
IL_000e: ldstr "Count of {0}: {1}"
IL_0013: stloc.0
次に「var array = new int[0];」だが、ここもローカル変数に入れるだけ。
// 説明省略
IL_0014: ldc.i4.0
IL_0015: newarr [mscorlib]System.Int32
IL_001a: stloc.1
次に実際のラムダ式が登場する「var count = array.Count(x => x == val);」の部分。
先ほどvalのために生成したDisplayClassのメソッドを使ってFuncオブジェクトを生成し、Countメソッドに渡している。
// ローカル変数1番目:arrayをロード
IL_001b: ldloc.1
// <Main>b__0メソッドのポインタをロード
IL_001c: ldloc.3
IL_001d: ldftn instance bool ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::'<Main>b__0'(int32)
// <Main>b__0メソッドのポインタを使って、Funcオブジェクトを生成
IL_0023: newobj instance void class [mscorlib]System.Func`2::.ctor(object,
native int)
// System.Linq.Enumerable::CountメソッドにarrayとFuncオブジェクトを渡す
IL_0028: call int32 [System.Core]System.Linq.Enumerable::Count(class [mscorlib]System.Collections.Generic.IEnumerable`1,
class [mscorlib]System.Func`2)
// ローカル変数2番目にセット
IL_002d: stloc.2
最後に「System.Console.WriteLine(format, val, count);」の部分。
ポイントはvalを使うのにラムダ式用の<>c__DisplayClass1を使っているところ。
// ローカル変数0番目: formatをロード
IL_002e: ldloc.0
// ローカル変数3番目: CS$<>8__locals2のvalフィールドをロード、intなのでboxingする
IL_002f: ldloc.3
IL_0030: ldfld int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
IL_0035: box [mscorlib]System.Int32
// ローカル変数2番目: countをロード, boxing
IL_003a: ldloc.2
IL_003b: box [mscorlib]System.Int32
// WriteLine
IL_0040: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_0045: ret
} // end of method Program::Main
このように、ラムダ式は、必要な変数を入れ子クラスのインスタンスにとっておいて、ラムダ式に書いた内容のメソッドが実行される、という形で実装されている。
しかし、ラムダ式があれば必ず入れ子クラスが作られるかというとそうではなく、変数をとっておく必要がない場合、違う形にコンパイルされる。
たとえば、先のコードのint val = 100;にconstをつけてconst int val = 100;にするだけで、ILはこうなってしまう。
.class private auto ansi beforefieldinit ConsoleApplicationLinq.Program
extends [mscorlib]System.Object
{
// ProgramクラスそのものにFuncのフィールドが用意される
.field private static class [mscorlib]System.Func`2<int32,bool> 'CS$<>9__CachedAnonymousMethodDelegate1'
(CompilerGeneratedAttributeの部分は省略)
.method private hidebysig static void Main(string[] args) cil managed
{
// ローカル変数の定義などなど
.entrypoint
.maxstack 3
.locals init ([0] string format,
[1] int32[] 'array',
[2] int32 count)
IL_0000: ldstr "Count of {0}: {1}"
IL_0005: stloc.0
IL_0006: ldc.i4.0
IL_0007: newarr [mscorlib]System.Int32
IL_000c: stloc.1
IL_000d: ldloc.1
// CS$<>9__CachedAnonymousMethodDelegate1をロードして、存在すればこの先の処理まで飛ばす
IL_000e: ldsfld class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0013: brtrue.s IL_0026
// なければProgramクラスに定義された<Main>b__0メソッド(「x => x == val」の実装)を使ってFuncオブジェクトを作る
IL_0015: ldnull
IL_0016: ldftn bool ConsoleApplicationLinq.Program::'<Main>b__0'(int32)
IL_001c: newobj instance void class [mscorlib]System.Func`2::.ctor(object,
native int)
// 作ったらフィールドに格納して、ロードしておく
IL_0021: stsfld class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0026: ldsfld class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
// 以下同じ
IL_002b: call int32 [System.Core]System.Linq.Enumerable::Count(class [mscorlib]System.Collections.Generic.IEnumerable`1,
class [mscorlib]System.Func`2)
IL_0030: stloc.2
IL_0031: ldloc.0
IL_0032: ldc.i4.s 100
IL_0034: box [mscorlib]System.Int32
IL_0039: ldloc.2
IL_003a: box [mscorlib]System.Int32
IL_003f: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_0044: ret
} // end of method Program::Main
(コンストラクタの定義は省略)
.method private hidebysig static bool '<Main>b__0'(int32 x) cil managed
{
(CompilerGeneratedAttributeの部分は省略)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.s 100 // 「100」はここに埋め込まれるので、フィールドから取る必要がない
IL_0003: ceq
IL_0005: ret
} // end of method Program::'<Main>b__0'
} // end of class ConsoleApplicationLinq.Program
こっちのコードだとご丁寧にもFuncオブジェクトをstaticフィールドにキャッシュしている。
ちなみに、valをローカル変数ではなくフィールドに持つようなクラスを作った場合も上述のconstの形に近くなり、入れ子クラスは作られない(Funcオブジェクトのキャッシュはしなくなる)。
まぁ、この差異はプログラム全体からしたらたいしたことはないと思うので、書くときに意識することはないと思うが、知っておいても悪くない…かな?
(一応、array.Count(x => x == val)を10000000回実行したら、valがconst/フィールド/ローカル変数それぞれの場合でStopwatchクラス読みで240ms/240ms/300msだった)
こうしてILコードを見ていると、ラムダ式がどういうものなのかしっくりきたのだけど、みなさまはどうだろうか?
登録:
投稿 (Atom)

