Write and Run

it's a simple way, but the only way.

B-Treeを図示するときの細かいテクニック

これは 自作DBMS Advent Calendar 2020 - Adventar の12日目の記事です。
自作 DBMS ネタというか、課題で B-Tree を書かされてる学生向けって気もしますが、いずれにせよ誰かの役に立つと信じて。

Disk-Oriented な DBMS で欠かせないデータ構造といえば B-Tree です。 で、そのデータ構造をちゃんと把握するには図示が有効なわけですが、これがまた大変というか、理解のために書いてるのに理解してないからうまく書けなくて余計混乱するという悪循環に陥りがちなわけです。

というわけでこの記事では B-Tree を図示するときの細かいテクニックをご紹介します。 (ボケーっとしながら書いたら真の B-Tree と B+Tree が混ざってしまった。適当に脳内で補完して読んでください)

ノードはキーと値(ポインタ)の2階建てにする

素朴に実装する場合、キーと値のペアの構造体を配列で持ったものをノードすると思います。
Rust で書くとこんな感じ。

struct Pair {
    key: K,
    value: V,
}

struct Node {
    pairs: Vec<Pair>,
}

こうすると、メモリ上では キー, 値, キー, 値, … という風にキーと値が交互に並ぶわけですが、これをそのまま図にするのはおすすめできません。
眺めているうちにどれがキーでどれが値だかわからなくなるからです。

そこで、キーと値を分けて2階建にします。こんな感じ。

f:id:koba789:20201211235116p:plain
2階建てにする

左上の斜線が入っている部分は使わないキーのフィールドです。 B-Tree のノードでは必ず値の個数がキーの個数より1個多くなりますから、素朴にペアを交互に並べた場合は必ずどこかに余りが出ます。
この使わない余りを右端にするか左端にするか悩むところですが、おすすめは左端です。
その理由については、図示のテクニックというより実装のテクニックなので今回は省略します。またの機会に。 (2分探索でもさらにもう一本書けるんじゃないか)

一番左の矢印は子ノードの右上に、それ以外は左上に

先程の図で、値のフィールドにキーとの不等式(?)が書いてあったのに気が付いたでしょうか。 キーの直下のポインタの先にあるノードのキーはいずれもそのキー以上であり、キーの左下のポインタの先にあるノードのキーはいずれもそのキー未満である、という意味です。 この関係を矢印に反映させることで混乱を減らせます(主観)。

f:id:koba789:20201212005005p:plain
キーの大小関係を意識して矢印を引く

ルートノードの一番左のポインタ、つまり未使用キーの下から伸びる矢印は一番左のノードの右上を指すようにします。 「一番左のノードには5未満のキーしかない」ことをわかりやすくするためです。
もちろん、メモリ上のアドレス的には左のノードの先頭のキーのあたりを指しているんですが、この場合そっちに忠実になるよりはキーの大小関係に着目したほうがわかりやすいと思います(個人差があります)。

それ以外の矢印については ノードの左上を指しましょう。こちらも決してノードの真上とかを指さないように。

このようにすると、木が B-Tree の要件を満しているかどうかを素早く判断できるようになります。
矢印の向きがそのままキーの大小関係になっているため、左向きの矢印の先でキーが大きくなっていないか、右向きの矢印の先でキーが小さくなっていないかを確認するだけで済みます。簡単ですね。

まとめ

というわけで今回は超小ネタを紹介しました。
もちろん図示の仕方はこれだけが正解ではありません。ぜひ自分にあった作図の方法を探ってみてください。

今回紹介できなかったノードの構造のパターンや2分探索のパターンなどはまた後日気が向いたときに記事にします。アドベントカレンダーにはまだたくさんの空欄がありますからね!

ではまた。

B+Treeのページレイアウトと可変長タプル

これは 自作DBMS Advent Calendar 2020 - Adventar 1日目の記事です。

初日からギリギリで大丈夫なんですかねぇ(主催者)。

先日こういう記事を書きました。

Rustで古典的なDisk-Oriented DBMSを実装した話 - Write and Run

その中で、

このあたりで固定長のページをノードとした B+Tree に、可変長のデータを入れようとすると途端に実装が面倒になることに気が付きます。 と書いたのですが、具体的にどう面倒になるのかについてはなにも説明しませんでした。 今回は固定長のページに可変長のタプルを入れるとなにがどう面倒なのかという話を解説します。

テーブルヒープにおける可変長タプル

まず、B+Tree ではなく単純なテーブルヒープを考えます。 PostgreSQL のテーブルデータなどがそういう実装ですね。

これは CMU の例の講義で説明されています。

03 - Database Storage I (CMU Databases Systems / Fall 2019) - YouTube

可変長のタプルはページの末尾に詰め、そこの各タプルへのポインタをページの頭に詰めるという方法です。 Slotted pages などと呼ばれているデータ構造です。 ポインタは固定長なので、インデックスを用いて可変長のタプルへ O(1) でアクセスできるようになります。 こうすると1ページあたりに入るタプルの数はそこに入るタプルのサイズによってまちまちになりますが、テーブルヒープではこれは問題になりません。

B+Tree のリーフノードにおける可変長タプル

続いて、B+Tree のリーフノードに可変長のタプルを入れる事を考えます。 インメモリで実装される B+Tree ではノードあたりのタプル数を固定にすることが多いですが、前述のとおり固定長のページをリーフノードとする場合はタプル数を固定にできません。 B+Tree にはノード分割という操作があるため、この性質が厄介なことを引き起こします。 タプル数固定のノードの分割ではタプル数が1/2になるように分割すればよく、またタプル数を1/2にしたノードには1つは必ずタプルを追加できるため困ることはありません。 固定長ページ・要素数可変のノードの分割では、分割によって作られた新しいノードに含まれるタプルの合計サイズが元のノードのそれをちょうど超えるように分割し、新規に挿入するタプルは元のノードの方に追加します。

さて、ここで問題になるのは「分割後のノードの空き容量は挿入するタプルに対して十分か」ということです。 最悪の場合、せっかく分割したのに結局入らないということになりかねません。

これを解決する最も簡単な方法は、ノードに含められるキャパシティの半分のサイズをタプルのサイズの上限とすることです。 こうすることで、分割後のノードには必ずタプルを追加できるようになります。 InnoDB などもこのようになってますね。

二分探索と slotted pages

Slotted pages の問題点として、二分探索と相性が悪いという点があります。 値の比較をするためにポインタを辿らなければならないため、CPU のキャッシュをぜんぜん活かせません。 そのため、キーが固定長であればキーを連続して埋めたほうがよいのですが、私の書いた qp ではキーが固定長であるにも関わらずそのような最適化はしていません。 これは同じデータ構造を使いまわしてサボるためです。 (両方実装してベンチマークで比較とかしてみたいですね)

おわり

日付変更1時間前に書き始めたのであまりおもしろいことが書けませんでした。 まだアドベントカレンダーの枠はたくさんあまっているので、追加のトピックについては適当な日の枠を使って書いていこうかと思います。

Rustで古典的なDisk-Oriented DBMSを実装した話

KOBA789 です。みなさん DBMS は好きですか。私は好きです。

最近、自作 DBMS をずっと作っていて、ようやく最低限の機能ができたので公開をしました。 (とはいえコードを書いていたのは正味2日ほど。設計と勉強に2週間かかった)

github.com

この記事ではこれを作った目的と、そのちょっとした詳細についてご紹介します。

目的

Disk-Oriented DBMS の学習に適している Rust で書かれた実装が欲しかった、というのが理由です。

DBMS の勉強に適している実装というのは意外と多くありません。
MySQLPostgreSQL といった有名な実装は実用的である一方でコード量は非常に多く、また細かな最適化によって教科書的なアルゴリズムと実際のコードの差が大きくなっているため、初学者にとっては構造を把握しづらくなっています。
教科書的な実装の Disk-Oriented DBMS というのは実装がクソめんどくさい割にそのままでは性能が悪く非実用的なためにわざわざ実装する人はおらず、現代では勉強に適した典型的な Disk-Oriented DBMS の実装はほとんどありません。

また Disk-Oriented DBMS は複数のスレッドが1つのバッファを奪い合うという設計上、当然ながら latching のテクニックが重要になります。
C 言語で実装する場合には教科書に載っているアルゴリズム擬似コードをそのまま実装すればよいのですが、これを Rust で実装するとどうなるのかに興味がありました。
Rust で Disk-Oriented DBMS を実装しようだなんて思う人間はまずいないため、やはり自分で実装するしかありませんでした。

勉強と設計

Rust 特有の難しさはあとでなんとかすることにしても、まずは Disk-Oriented DBMS の実装テクニックを知る必要があります。

とっかかりとしては CMU のこの講義が参考になりました。

www.youtube.com

この講義では BusTub という C++ で書かれた Disk-Oriented DBMS を課題の題材に使っており、この実装も参考になります。

github.com

課題で学生に実装を書かせるために重要な部分はだいたいスケルトンになっており、詳細を知るには課題のヒントを見ながら自力で実装しなければなりません。 スケルトンになっているとはいえ、クラス設計はほぼ完成しているのでそこは大変参考になります。

このあたりで一度、バッファプールとテーブルヒープの実装を書き上げ、フルスキャンしかできない DBMS を作りました。
しかしフルスキャンしかできず、トランザクションもない DBMS というのは大変つまらないものです。
ここで次に B+Tree を実装するかトランザクションを実装するか悩んだのですが、フルスキャンしかできない DBMS で 2PL を実装するとすべてがテーブルロックになることに気づいたので、先に B+Tree を実装することにしました。
Predicate Lock を実装すればインデックスがなくても細かいロックで 2PL を実装できるとか、2PL ではなく PostgreSQL 的な Append-Only MVCC なら(Serializable にはならないけど)できるんじゃないかとかあったんですが、それはそれです。

B+Tree の実装については上記の講義動画に加え、Modern B-Tree Techniques という本が非常に参考になりました。

now publishers - Modern B-Tree Techniques

200ページに渡ってほぼ B-Tree についてのみ書かれているのですが、その分非常に丁寧でだいたい全部書いてあるのでおすすめです。
困ってもこれを読めば解決するという安心感があります。

このあたりで固定長のページをノードとした B+Tree に、可変長のデータを入れようとすると途端に実装が面倒になることに気が付きます。
データ構造の解説などで登場する B-Tree もほぼすべてメモリ上のデータ構造で、かつデータは固定長であることがほとんどです。
これはぜひ可変長にせねばならんと思い、可変長のデータを入れられる設計にしました。

詳細

コンポーネントは大きく分けて Disk Manager, Buffer Pool Manager, Access Method の3部からなります。
トランザクションや複雑なクエリ言語は実装していないため、Lock Manager や Query Planner などはありません。
B+Tree の Access Method がほとんど直接インターフェースとして露出しています。
クエリのインターフェースとしては TCP 上で1行の JSON を送り合う素朴なプロトコルを採用しています。

Disk Manager

一番簡単なコンポーネントです。4KB 単位でバイト列を読み書きするだけです。説明不要でしょう。

qp/disk.rs at master · KOBA789/qp · GitHub

Buffer Pool Manager

すべてのスレッドが共有するバッファプールを管理します。
キャッシュ管理のアルゴリズムは LRU ではなく Clock Sweep です。PostgreSQL とかで使われているやつですね。
ちなみにバッファの free-list は実装していません。 free-list が一度枯れてしまったら DROP TABLE とかしないと復活しないわけですが、この DBMS には物理削除が実装されていないので不要という判断です。 起動直後にすべてのバッファを usage_count=0 で埋めてすべてを Clock Sweep に任せています。

意外と一番 Rust っぽいパズルがあったのがこの Buffer Pool です。 Rust の Mutex は保護対象のデータのコンテナのようになるため、無駄なロックを避けるためには Mutex に含める構造体のフィールドを最小限にする必要があります。 結果として細かな構造体がたくさん入れ子になり、深い階層構造を作っています。
前述の BusTub の実装と比べると対象的だと思います。

ページを fetch する際は BufferPool の巨大なロックを取り、ページテーブルで対象フレームが見つかったらフレームの中の Arc を clone して返します。
もし目的のページがフレームになければファイルから読み込んで victim フレームに書き込んでから、同様に Arc を clone して返します。
この状態ではページを Pin したのみでページのデータのバイト列へのアクセスはできませんので、実行スレッドが実際に読み書きする際は更に内側の RwLock を獲得してから操作します。

Mutex<
  BufferPool {
    frames: Vec<
      page_id,
      usage_count,
      body: Arc<
          RwLock<
            Page {
              is_dirty,
              data,
            }
          >
        >
      >
    >
  }
>

qp/buffer.rs at master · KOBA789/qp · GitHub

Access Method

公開しているコードに含まれている Access Method は B+Tree のみです。
フルスキャンしかできないテーブルヒープは消してしまいました。

qp/btree.rs at master · KOBA789/qp · GitHub

B+Tree の実装で特徴的な部分は特にありませんが、Latch Crabbing のために独自の latch 実装を書いています。
latch の実装としては parking_lot を使っているのですが、parking_lot の RwLock には owned な lock guard を獲得する方法がありません。
たとえば、tokio の Mutex には Arc<Mutex<T>> から owned な lock guard を獲得するメソッドがあります。
前述の通り、ページを Arc<RwLock<T>> で保護しているため、このようなメソッドがあるとロックを獲得したままデータを move できて便利です。
というか、ないと Latch Crabbing のようなテクニックを実現できません。

unsafe コードが含まれる唯一のモジュールです。

qp/lock.rs at master · KOBA789/qp · GitHub (lock.rs は名前がややこしいので latch.rs のほうがいいですね)

可変長のデータを固定長のページに格納するために Cell や Slot と呼ばれる構造があります。
それを実装したのが slotted.rs です。
もともとテーブルヒープを実装した際に書いたものですが、そのまま B+Tree の内部構造に流用しています。

qp/slotted.rs at master · KOBA789/qp · GitHub

DBMS を実装してみて

まだ全然要素が足りていないものの、実装をすることで Disk-Oriented DBMS のつらい部分を体験することができました。
既存の DBMS はすげーわと思うとともに、Disk-Oriented な DBMS をメニーコアでスケールさせるのはマジで無謀だわという実感を得ることもできました。
今後も暇を見つけて CC やリカバリなどを実装していければと思っています。

トランザクション中の文の失敗の扱いの違い

(読みづらいタイトルだな)

ことの発端はこのツイート。

さすがの MySQL でもそこを破ってくることはないだろうと思いつつ、トランザクション野郎としてはちゃんと確かめねばならないと思い、早朝にも関わらず布団から出てラップトップを開いた(午前10時)。

実験1

以下のような docker-compose.ymlsql/script.sql を用意し、実験をする。

version: '3.3'
services:
  db:
    image: mysql:8
    environment:
      MYSQL_DATABASE: test_db
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: rootpassword
    volumes:
      - "./sql:/tmp/sql"
create table tbl(id int primary key);
insert into tbl values (1);
begin;
insert into tbl values (1);
insert into tbl values (2);
commit;

まずはおもむろに mysql のプロンプトを開き、sql/script.sql の内容を1行ずつ手で実行する。 (余計な warning などは消した)

# mysql -h 127.0.0.1 -u user -ppassword test_db
mysql> create table tbl(id int primary key);
Query OK, 0 rows affected (0.02 sec)

mysql> insert into tbl values (1);
Query OK, 1 row affected (0.01 sec)

mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> insert into tbl values (1);
ERROR 1062 (23000): Duplicate entry '1' for key 'tbl.PRIMARY'
mysql> insert into tbl values (2);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tbl;
+----+
| id |
+----+
|  1 |
|  2 |
+----+
2 rows in set (0.00 sec)

なんと、insert into tbl values (1); が失敗したのにコミットできている。

一旦考察と仮説

トランザクション中の文が失敗しても、その失敗をクライアントに通知できているなら原子性違反にはならないのではないか。

つまり、クライアントに失敗を通知できない非対話実行(バッチ実行)では途中で実行が止まるはずである。

実験2(非対話実行)

上で作ったテーブルは適当に drop して、次の実験をする。

仮説では非対話実行では途中で実行が止まり、結果として id=1 の行しか残らないはずである。 (余計な warning などは消した)

# mysql -h 127.0.0.1 -u user -ppassword test_db < /tmp/sql/script.sql
ERROR 1062 (23000) at line 4: Duplicate entry '1' for key 'tbl.PRIMARY'
# mysql -h 127.0.0.1 -u user -ppassword test_db
mysql> select * from tbl;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

仮説どおり、4行目のエラーで実行が止まり、1行だけのテーブルになった。

実験3(PostgreSQL)

さて、ここで気になるのは PostgreSQL の挙動である。

私は仕事でも趣味でも PostgreSQL やその派生を使うことが多い。

PostgreSQL は対話モードでも途中でコケたらコミット不能になるはずであるが、いい機会なのでちゃんと確かめておこう。

MySQL 同様、docker-compose を用いて適当に用意する。

version: '3.3'
services:
  db:
    image: postgres:12
    environment:
      POSTGRES_PASSWORD: password
    volumes:
      - "./sql:/tmp/sql"

そしてまずは対話実行で確認。

# psql -U postgres
postgres=# create table tbl(id int primary key);
CREATE TABLE
postgres=# insert into tbl values (1);
INSERT 0 1
postgres=# begin;
BEGIN
postgres=# insert into tbl values (1);
ERROR:  duplicate key value violates unique constraint "tbl_pkey"
DETAIL:  Key (id)=(1) already exists.
postgres=# insert into tbl values (2);
ERROR:  current transaction is aborted, commands ignored until end of transaction block
postgres=# commit;
ROLLBACK
postgres=# select * from tbl;
 id
----
  1
(1 row)

うむ、やはり途中でコケると後続の文は無視される。実家のような安心感。

ついでに非対話実行もためそう。

# psql -U postgres < /tmp/sql/script.sql
CREATE TABLE
INSERT 0 1
BEGIN
ERROR:  duplicate key value violates unique constraint "tbl_pkey"
DETAIL:  Key (id)=(1) already exists.
ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK
# psql -U postgres
postgres=# select * from tbl;
 id
----
  1
(1 row)

こちらも同様。納得。

まとめ

MySQL は対話実行だとトランザクション中の文がコケても勝手にトランザクションがアボートしたりせず、トランザクションを続行することができる。

MySQL でも非対話実行なら途中の文がコケるとバッチ全体の実行が停止する。

PostgreSQL を使っている身からすると MySQL のこの挙動は結構新鮮だった。

長いトランザクションの途中でも衝突する可能性のある INSERT を投機的に(?)実行して衝突の有無で分岐したりできるので便利かもしれない。

なお、PostgreSQL でそのようなことをしたい場合は SAVEPOINT を使って予防線を張っておくとよい。長大なトランザクションを失敗一撃で無に帰さずに済む。

追証用資料

適当な英語で書かれた README を同梱しているのでそのとおりにやれば試せます(たぶん)。

github.com

自宅の潤沢な計算資源に対して一時的にグローバルIPが欲しいとき

みなさんが夏の訪れを感じるのはいつですか。 私は自宅サーバーのファンの音がうるさくなったときです。

私が普段自宅サーバーでサービスを公開するときは1つだけのグローバル IP アドレスを使って Virtual Host でやりくりしています。 (ロードバランサとして k8s 上の Contour(Envoy) を使っています。便利ですよ)

しかし、HTTP(s) ではないサービスをちょっと露出させたかったりするときに困ります。

かといって固定 IP アドレスをたくさんもらえる ISP 契約というのは一時的な用途のために契約するのにはちょっとお値段が。

今回はそんな自宅サーバーでグローバル IP アドレスをちょっと使いたいときに使えるテクを紹介します。

TL;DR

AWS の Elastic IP を EC2 につけて、IPIP6 でトンネルする

やりかた

前提として、対象の自宅サーバーは IPv6 でインターネットに出られるものとします。

IPv4 connectivity しかない場合は IPIP6(4-over-6) を IPIP(4-over-4) に読み替えればできる気がします。

IPIP ではなく IPIP6 にしている理由はフレッツだと IPv6 の方が圧倒的に速いからです。

VPC の用意

まずは AWS 側で IPv6 を有効にした VPC を用意します。

既存のやつを流用してもいいし、新規で作ってもいいと思います。

アドレスプールは Amazon のやつでいいです。

サブネットに IPv6 の CIDR を設定するのも忘れずに。

Elastic Network Interface と Elastic IP の用意

次に Elastic Network Interface(ENI) を作ります。

サブネットはさっき作った VPC のやつをちゃんと選びましょう。

IPv4 Private も IPv6 も Auto-Assign でいいです。

Security Group は用途に合わせてよしなに。

ENI ができたら Elastic IP をアロケートして Associate してやります。

複数付けたければ Manage IP Addresses で Private IP を足して Associate します。

ここで気をつけなければならないのは、ENI に紐付けられる IP アドレスの数は EC2 によって制限されていることです。

この上限はインスタンスタイプによってちがうのでよく確認しましょう。

また、1つのインスタンスに紐付けられる ENI の数にも制限があるため、ENI を分割しても限界があります。

たくさん欲しい場合はインスタンスタイプを変えたりインスタンスを増やしたりする必要があるでしょう。

docs.aws.amazon.com

EC2 インスタンスの用意

用意した ENI をくっつけたインスタンスを立ち上げます。

VPC とサブネットが ENI と食い違っていると作成できないので注意しましょう。

また、OS は Amazon Linux 2 を使っておくのがオススメです。

Amazon Linux 2 以外だと複数の IP アドレスをいい感じにする設定がダルいので。

Ubuntu だとダルいという例: Make Secondary Network Interface Work in Ubuntu EC2 Instance

インスタンスタイプは前述のとおり ENI 数や IP アドレス数に合わせて選びましょう。

IP パケットを右から左(または左から右)に流すだけなので CPU もメモリも問題になりません。一番安いやつを選べばいいと思います。

t2.nano でも 2ENI x 2IP で 4IP いけるのでだいたいのケースで十分だと思います。

ディスクもどうせほとんど使わんし超小さくていいです。

IPIP6 トンネルを掘る

EC2 が起動したらトンネルを掘っていきます。

以下のような構成を想定しています:

  • 自宅側のトンネル終端はルーター(私は IX2105 使ってます)
  • 自宅側のトンネル終端の IPv6 アドレス: 2001:DB8::789
  • 自宅サーバーの CIDR : 192.168.0.0/24
  • 対象の自宅サーバーの Private IP アドレス(自宅内): 192.168.0.1
  • VPC の CIDR : 10.123.0.0/16
  • ENI の Private IPv4 アドレス: 10.123.0.1
  • ENI の Public IPv4 アドレス: 203.0.113.1
  • ENI の IPv6 アドレス 2001:DB8::EC2

適宜読み替えてください。

自宅側

先にトンネルの自宅側の設定をしましょう。

ここでは NEC の IX 系ルーターの設定例だけを紹介します。

tunnel sourceIPv6 アドレスがあるインターフェースに、ip unnumbered自宅サーバーがあるサブネットに繋がってるインターフェースにするとよいです。

interface Tunnel0.0
  tunnel mode 4-over-6
  tunnel adjust-mtu auto
  tunnel destination 2001:DB8::EC2
  tunnel source GigaEthernet0
  ip unnumbered GigaEthernet1
  ip tcp adjust-mss auto
  no shutdown

トンネルの設定を入れたらルーティングの設定もします。

ポリシールーティングを使って自宅サーバーからのトラフィックをトンネルに流すことにします。

ip access-list aws-public permit ip src 192.168.0.0/16 dest any
route-map aws-public permit 10
  match ip address access-list aws-public
  set interface Tunnel0.0
interface GigaEthernet1
  ip address 192.168.0.254/24
  ip policy route-map aws-public
  no shutdown

ここでは CIDR でガバっとやってますが、サブネット内に EC2 に流したいサーバーと流したくないサーバーがいる場合は /32 とかで細かくやることになると思います。

私は EC2 経由で公開するサーバーを置く専用の VLAN を切ってるのでガバっとやっています。

EC2 側設定

コマンドは sudo とか付けずに書きますが必要に応じて root でやるとか sudo つけるとかしてください。

まずは Linuxルーターっぽいことをするときに必ず必要なやつ。

sysctl -w net.ipv4.ip_forward=1

次に IPIP6 トンネルを作成します。今の時代は ip コマンドでなんでもできて便利。

# ip -6 tunnel add ipip0 mode ipip6 local <ENI IPv6 アドレス> remote <自宅終端 IPv6 アドレス>
ip -6 tunnel add ipip0 mode ipip6 local 2001:DB8::1 remote 2001:DB8::789 encaplimit none
ip link set ipip0 up

トンネルが掘れたらルーティングを設定します。

ip route add 192.168.0.0/24 dev ipip0

これで自宅宛てのトラフィックがトンネルに潜って抜けていくようになります。

試しに自宅サーバーの IPv4 アドレス宛に ping 送って試したりするとよいです。

ping 192.168.0.1

これで返ってこなかったら何かがミスっているので確認しましょう。

正しくトンネルを掘れてパケットが行って帰ってこられるようになったら NAT の設定をします。

Public IPv4 アドレス1つにつき、行きと帰りで計2行ずつ設定が必要です。

欲張ってたくさん Elastic IP を貼り付けたひとはがんばって設定しましょう。

# iptables -t nat -A POSTROUTING -s <自宅サーバー Private IP アドレス(自宅内)> -j SNAT --to-source <ENI Private IPv4 アドレス>
# iptables -t nat -A PREROUTING -d <ENI Private IPv4 アドレス> -j DNAT --to-destination <自宅サーバー Private IP アドレス(自宅内)>
iptables -t nat -A POSTROUTING -s 192.168.0.1 -j SNAT --to-source 10.123.0.1
iptables -t nat -A PREROUTING -d 10.123.0.1 -j DNAT --to-destination 192.168.0.1

これでインターネットから 203.0.113.1(ENI の Public IPv4 アドレス) を使って自宅サーバーにアクセスできるはずです。

このままだと EC2 インスタンスを再起動したときに EC2 側のトンネル設定が吹き飛ぶのでいい感じに永続化してください。

自宅サーバー側の設定

ハマりポイントとして、自宅内の DNSゾルバを見るようになってて名前解決ができねぇ、みたいなことがあったりします(ハマった)。

DNSゾルバが同じブロードキャストドメインにいるなら大丈夫なんですが、そうでない場合はポリシールーティングで全部 EC2 にぶん投げちゃってるので見えなくなって困ります(困った)。

ゾルバの設定を DHCP で配ってたりすると面倒ですね(面倒だった)。まぁうまくやってください。

まとめ

Elastic IP は文字通り Elastic なので、パッと調達して取り付けることができて便利ですね。

私は ISUCON の練習用 VM をチームメイトに貸すときに使っています。

一時的にしか使わないのに3台分のグローバル IP アドレスを調達するのってめんどうですからねぇ。

ではよき自宅サーバーライフを!

Special Thanks

このアイデアは同僚の id:sora_h にもらいました。 最初は真面目に暗号化された VPN を張ろうとがんばってたんですが

てかインターネットに出たいトラフィックなら無暗号の ipip tunnelやGREで十分よ

との助言でこのような雑トンネルとなりました。いやー賢いね。

WASIを叩くWASMを手書きしてLucetでHello, world!する

WASI がなんだとか WASM がなんだとかは詳しく説明しません。各位やって。

まず Lucet を入れる。

github.com

はい

Hello, world! するには fd_write があればよさそう。

wasmtime/WASI-api.md at master · CraneStation/wasmtime · GitHub

引数はだいたい見りゃわかる。が、__wasi_ciovec_t っつーのが引っかかる。

とはいえこれも見りゃわかる。

wasmtime/WASI-api.md at master · CraneStation/wasmtime · GitHub

バッファへのポインタとバッファの長さを持ってる構造体。

で、fd_write にはそいつのポインタを渡してやればいいということがわかる。

というところまでわかったら WAT を書く。S 式。

(module
  (import "wasi_unstable" "fd_write" (func $__wasi_fd_write (param i32 i32 i32 i32) (result i32)))
  (func $_start
    i32.const 1
    i32.const 16
    i32.const 13
    i32.const 24
    call $__wasi_fd_write
    drop)
  (memory 1)
  (data (i32.const 0) "Hello, world!\00\00\00\00\00\00\00\0d\00\00\00")
  (export "_start" (func $_start)))

import ってやつでリンクをやってて、data でメモリの初期値を埋めてる。export はエントリポイントの公開。

で、data に埋まってる初期値が大切で、ここに

"Hello, world!"(13bytes) + padding[0x00 0x00 0x00](3bytes) + buf address[0x00 0x00 0x00 0x00] (4bytes = i32) + buf length[0x0d 0x00 0x00 0x00](4bytes = i32)

が埋まっている。

buf address と buf length ってのが __wasi_ciovec_t の中身。

ので、スタックに

1  ;; stdout の fd
16 ;; buf address へのアドレス
13 ;; "Hello, world!" のバイト数。buf length とは別にこれも必要らしい
24 ;; `nwritten` へのアドレスっぽい。戻り値はスタックに返ってくるんじゃねーのかよ(スタックにも返ってくるんだよな)

というかんじで積んでやって、call $__wasi_fd_write してやる。

最後、スタックに nwritten と思しき値が残ってしまうのでそいつを drop で消し飛ばせば完了。

hello.wat みたいな名前で保存してやって、以下のように実行できる。

lucetc-wasi -o hello.so hello.wat
lucet-wasi hello.so

楽しいね

デカいテレビとXbox One Xを買ってランボルギーニを乗り回す

f:id:koba789:20181216034813j:image

はい

 

サイバーマンデーにそそのかされてデカいテレビを買いました。

2018年も暮れなので、4K HDR な OLED です。ナウい

サイズは65インチ。デカい。

なお部屋は8畳1K。狭い。

 

で、テレビだけ買っても 4K HDR コンテンツを吐ける箱がなけりゃ意味なかろうということで、Xbox One X という箱を買いました。

Forza Motorsport 7 と Forza Horizon 4 というブーブーゲームが付いてるモデル。

 

私が暮らしている恵比寿(正確には違うが)という街は大変イカれた場所で、昼飯を食いに外に出るだけでランボルギーニを眺められるようなシティなのですが、デカいテレビと箱があるとなんと外に出ずとも部屋でランボルギーニを運転できます。

はえらい。

 

冒頭の画像はランボルギーニラカンで崖から飛び降りる瞬間を写したもの。

ゲームの世界では一軒家並みの値段の車で紐なしバンジーしても無料。

はえらい。

 

私は人間を歩かせたり武器を構えて何かを射るようなゲームが苦手なのですが、Forza は人間を歩かせたり武器を構えて何かを射たりということがないので助かっています。

 

ちなみに据え置きのゲーム機を手に入れるのはこれがやっと2台目で、ちなみに1台目は2ヶ月前に中古で買った Wii です。

携帯ゲーム機はというと DSi をバイト先の忘年会のビンゴ大会で当てたのが最後。

ゲーム機ってゲームできてすごいですね。

 

現実世界に置いてあるテレビの方は、現在こたつの上に曖昧に乗せてあるだけという状態で邪魔だしデカいしデカいし邪魔なので、いろいろして壁掛けにする算段です。

これもいい感じにシュッとしたら記事にしたい。

 

なお、テレビの登場によって今まで使っていたプロジェクターは失職となりました。

WXGA のくせに今までよく頑張ったと思います。解像度はともかくとしてランプがクソ明るかったのはよかった。

 

最後は崖からゴロゴロと転がってボコボコになり、リアスポイラーがバイバイしてしまった WRX STI で締めようと思います。

f:id:koba789:20181216040808j:image

ちなみに耳も取れてる👂