CUBE SUGAR CONTAINER

技術系のこと書きます。

「Linuxで動かしながら学ぶTCP/IPネットワーク入門」という本を書きました

表題のとおり TCP/IP に関する本を書きました。 今回は、そのご紹介です!

どんな本なの?

Linux を使って実際にネットワークを組んで動かしながら TCP/IP について学べる本です。 実際に手を動かすことで、より実践的で風化しにくい知識と技術を身につけることが本の目的です。

こんな人にオススメ

次のいずれかに当てはまるような方には、この本が参考になると思います。

  • ネットワークが専門ではない IT エンジニア、またはそれを志す学生さん
  • 他の TCP/IP に関する本を読んだことはあるけど、身についている実感が少ない
  • インターネットやインフラの技術についてよく知らないけど興味はある
  • ネットワークを気軽に組んで実験できる環境の作り方に興味がある

そして、この本を読んで試した後には、次のような効果が見込めます。

  • 「インターネットの本質を理解できた」という実感が得られる
  • ネットワークのトラブルシュートで見るべきポイントがわかる
  • なんとなく持っていたネットワークに対する苦手意識がなくなる
  • 「ネットワークがこういう状況では何が起こるんだろう」と思ったときに、試す環境が作れるようになる

執筆のきっかけ

執筆のきっかけは、実際に手を動かしながら TCP/IP について学べる本が世の中に必要なんじゃないか、と感じたことです。 TCP/IP という技術は、理屈について書いてある本はたくさんあるものの、実際に手を動かせるものとなると少なくなります。 さらに、手を動かすための環境構築までカバーした本となると、より限られます。

その原因は、TCP/IP を学ぶための環境構築が難しい点が挙げられると思います。 一般的に、ネットワークを組んで勉強しようとすると、複数台のコンピュータを用意する必要があるためです。 もちろん、仮想マシンを使えば物理的なコンピュータは 1 台で済みます。 ですが、それでもネットワークの構築という手間のかかる作業は残っています。

この本では、その問題を Linux の Network Namespace という機能を使って解決しています。 Network Namespace というのは、Docker といった Linux のコンテナ仮想化技術を支えている機能のひとつです。 Network Namespace を使うと、1 台の Linux マシンの中に、ネットワーク的にはシステムから独立した空間を作れます。 実験用のネットワークを作るのに必要な作業はいくつかのコマンドライン操作だけで、完了するまでには数秒もかかりません。

また、Linux を動かす環境には仮想マシンが使えるため、用意すべき物理的なコンピュータは今使っているラップトップで十分です。 もちろん、利用するソフトウェアはどれもフリーなものなので、追加で必要となる費用もありません。 読みながら試すときは Linux のコマンドライン操作が必要になりますが、あまり親しみのない人にも読んでもらえるように、付録には簡単にですが操作ガイドも用意しました。

どんなことが書いてあるの?

TCP/IP を理解する上で、重要なポイントをギュギュッと詰め込みました。 この本は、プロトコルのヘッダに含まれるすべてのフィールドを網羅的に解説していくような性格の本ではありません。 そのため、TCP/IP に詳しい方からすると「これは書いてないのか」と思う点もあるかもしれません。 ですが、この本を読んで「TCP/IP という技術の地図とコンパス」が頭に入れば、他の本も読みやすくなると思います。 また、書く内容を重要なポイントに絞っても、尚 B5 版で 200 ページを超えるボリュームになりました (付録含む)。

本書は、7 つの章に分かれています。 OSI 参照モデルでいうと L2 から L7 までを一冊でひととおりカバーしています。 説明の順序としては「L3 -> L2 -> L4 -> L7 -> NAT -> ソケットプログラミング」という流れです。 ざっくりと、どんなことを書いてあるのか次に示します。

  • 「TCP/IP とは」
    • この章では TCP/IP の概要とインターネットの動く仕組みについて学びます
    • インターネットでパケットがバケツリレーされる仕組みについて、実際に ping や traceroute コマンドを実行しながら説明を進めます
  • 「Network Namespace」
    • この章では Network Namespace を使って実験用ネットワークを組む方法を学びます
    • 「ルータなし → ルータあり → ルータ 2 台」という流れで、少しずつ複雑なネットワークを組みながら説明を進めます
    • ping を使ってネットワークの疎通を確認したり、パケットを tcpdump コマンドを使って観察します
  • 「イーサネット」
    • この章ではイーサネットが近隣までパケットを運ぶ仕組みについて学びます
    • MAC アドレスを使ってフレームが届く仕組みや、ブリッジ (Linux Network Bridge) でブロードキャストドメインを広げる方法を扱います
  • 「トランスポート層のプロトコル」
    • この章では、トランスポート層のプロトコルを使って、通信の種類を識別したり、信頼性のある通信を実現する方法を学びます
    • はじめに、UDP でポート番号を使って通信するアプリケーションを識別する仕組みについて扱います
    • その次に、TCP が信頼性のある通信を実現している仕組みを扱います
  • 「アプリケーション層のプロトコル」
    • この章では、TCP/IP の集大成としてインターネットでやり取りされる実用的な通信について学びます
    • 扱うプロトコルは HTTP、DNS、DHCP です
  • 「NAT」
    • この章では、IPv4 で組まれた家庭やオフィスなどのネットワークのほとんどで用いられている NAT という技術を学びます
    • はじめに、グローバルアドレスを節約するために用いられる Source NAT について扱います
    • その次に、「ポートを空ける」という表現でよく知られている Destination NAT について扱います
  • 「ソケットプログラミング」
    • この章では、TCP/IP をソフトウェアから扱う方法について学びます
    • プログラミング言語としては Python を使います
    • 取り扱う題材は、HTTP クライアント、エコーサーバ、独自の足し算バイナリプロトコルです

どんな人が書いたの?

著者は、とあるインターネット関連企業で働いているソフトウェアエンジニアです。 ネットワーク機器の開発で IPv6 プロトコルスタックを担当していた経験があります。 一時期は SDN (Software Defined Network) 関連のソフトウェアの研究開発や OSS コントリビューションを仕事としてやっていました。 また、時期によっては Web アプリケーションの開発もしていたので、L2 ~ L7 にかけてひととおりの開発経験があります。 なので、出自は「ソフトウェア寄りのネットワーク屋さん」といったところです。

なお、過去にブログで TCP/IP について扱って話題となったエントリだと、次のようなものがあります。

blog.amedama.jp

blog.amedama.jp

中身を軽く読んでみたい

本の雰囲気を確かめてから買いたいという方もいらっしゃるかと思うので、以下に立ち読み版の PDF をご用意しました。

drive.google.com

おわりに

私の好きな言葉のひとつに「特定の分野におけるプロフェッショナルとは、その分野でひととおりの失敗を経験した人のことをいう」という言葉があります。 ぜひ、この本を使って、たくさん失敗できる環境を手にしていただければ幸いです。

謝辞

この場を借りて、本の表紙を書いてくれた Asano Sonoko さん、技術的なレビューをしてくださった @ttsubo さんに心からお礼を申し上げます。 おかげさまで、プライベートで 2 年ほどかけて少しずつ書いた本を世に出すことができました。 本当にありがとうございました。

Python: Optuna を使って QWK の閾値を最適化してみる

最近、Twitter のタイムラインで QWK (Quadratic Weighted Kappa: 二次の重み付きカッパ係数) の最適化が話題になっていたので個人的に調べていた。 QWK は順序つきの多値分類問題を評価するための指標で、予測を大きく外すほど大きなペナルティが与えられるようになっている。 また、予測値の分布が結果に影響する点も特徴的で、この点が今回取り扱う最適化にも関係してくる。

QWK の最適化については、Kaggle 本と、その著者 @Maxwell_110 さんによる次のブログエントリが詳しい。 ようするに、真のラベルの分布に沿った形で予測しないと最適な結果が得られない、ということ。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

note.com

Kaggle 本や、Web をざっと調べた限りでは scipy.optimize.minimize() 関数を使って Nelder Mead 法で最適化するのが一般的なようだった。

docs.scipy.org

今回は、せっかくなので Optuna を使って TPE (Tree-structured Parzen Estimator) で最適化してみた。

使った環境は次のとおり。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V                                                                
Python 3.7.6

下準備

はじめに、利用するパッケージをインストールしておく。

$ pip install scikit-learn optuna lightgbm pandas

その上で、Kaggle で開催された次のコンペのデータセットをダウンロードしておく。

www.kaggle.com

これは、前述した Kaggle 本で QWK の最適化が有効に働く例として挙げられているもの。 カレントワーキングディレクトリに、次のファイルがある状態にしておく。

$ ls | grep .zip
sample_submission.csv.zip
test.csv.zip
train.csv.zip

多値分類問題として解いてみる

そもそも、順序つきのラベルを予測するタスクの場合、多値分類問題として解く方法と回帰問題として解く方法がある。 まずは、ベーシックな考え方として多値分類問題として解く方法を試してみる。

早速だけど、サンプルコードは次のとおり。 LightGBM を使って多値分類問題として解いている。 目的関数は Multi LogLoss で、5-Fold CV で QWK について評価している。

#!/usr/bin/env python3

import numbers

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import KFold
from sklearn.metrics import cohen_kappa_score


def lgb_custom_metric_qwk_multiclass(preds, data):
    """LightGBM のカスタムメトリックを計算する関数

    多値分類問題として解いた予測から QWK を計算する"""
    # 正解ラベル
    y_true = data.get_label()
    # ラベルの数
    num_labels = data.params['num_class']
    # 多値分類問題では本来は二次元配列が一次元配列になって提供される
    reshaped_preds = preds.reshape(num_labels, len(preds) // num_labels)
    # 最尤と判断したクラスを選ぶ 
    y_pred = np.argmax(reshaped_preds, axis=0)  # 最尤と判断したクラスを選ぶ
    # QWK を計算する
    return 'qwk', qwk(y_true, y_pred), True


def qwk(y_true, y_pred):
    """QWK (Quadratic Weighted Kappa) を計算する関数"""
    return cohen_kappa_score(y_true, y_pred, weights='quadratic')


def _numerical_only_df(df_):
    """数値のカラムだけ取り出す関数"""
    number_cols = [name for name, dtype in df_.dtypes.items()
                   if issubclass(dtype.type, numbers.Number)]
    numerical_df = df_[number_cols]
    return numerical_df


def main():
    # データセットを読み込む
    train_df = pd.read_csv('train.csv.zip')

    # 説明変数
    x_train = train_df.drop('Response', axis=1)
    # 簡単にするため、とりあえずすぐに使えるものだけ取り出して使う
    x_train = _numerical_only_df(x_train)

    # 目的変数
    y_train = train_df.Response.astype(float)
    # 目的変数はゼロからスタートにする
    y_train -= 1

    # LightGBM のデータセット表現にする
    lgb_train = lgb.Dataset(x_train, y_train)

    # 多値分類問題として解く
    lgbm_params = {
        'objective': 'multiclass',
        'metric': 'multi_logloss',
        'num_class': len(y_train.unique()),
        'first_metric_only': True,
        'verbose': -1,
    }

    # データ分割の方法
    folds = KFold(n_splits=5, shuffle=True, random_state=42)

    # 5-Fold CV で検証する
    result = lgb.cv(lgbm_params,
                    lgb_train,
                    num_boost_round=1000,
                    early_stopping_rounds=100,
                    folds=folds,
                    verbose_eval=10,
                    feval=lgb_custom_metric_qwk_multiclass,
                    seed=42,
                    )

    # early stop したラウンドでの QWK を出力する
    print(f'CV Mean QWK: {result["qwk-mean"][-1]}')


if __name__ == '__main__':
    main()

上記を実行した結果が次のとおり。

$ python qwkmc.py
[10]   cv_agg's multi_logloss: 1.37765 + 0.0076621   cv_agg's qwk: 0.442683 + 0.00532416
[20]   cv_agg's multi_logloss: 1.25563 + 0.0095355   cv_agg's qwk: 0.507798 + 0.00703211
[30]   cv_agg's multi_logloss: 1.20349 + 0.0100597   cv_agg's qwk: 0.519766 + 0.00748977
...(省略)...
[220]  cv_agg's multi_logloss: 1.14153 + 0.0126548   cv_agg's qwk: 0.560446 + 0.00516118
[230]  cv_agg's multi_logloss: 1.14198 + 0.0126144   cv_agg's qwk: 0.56074 + 0.00444787
[240]  cv_agg's multi_logloss: 1.14242 + 0.0130142   cv_agg's qwk: 0.561591 + 0.0046819
CV Mean QWK: 0.5574903141807788

交差検証では QWK の平均として約 0.5574 が得られている。

回帰問題として解いてみる

続いては、同じデータを回帰問題として解くパターンについて。 ラベルが順序つきであれば、回帰問題として解いた上で結果を clip + 四捨五入するような方法もある。

サンプルコードは次のとおり。 やっていることは同じで、解き方が違うだけ。

#!/usr/bin/env python3

import numbers

import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import cohen_kappa_score


def lgb_custom_metric_qwk_regression(preds, data):
    """LightGBM のカスタムメトリックを計算する関数

    回帰問題として解いた予測から QWK を計算する"""
    # 正解ラベル
    y_true = data.get_label()
    # 予測ラベル
    y_pred = np.clip(preds, 0, 7).round()  # 単純に予測値を clip して四捨五入する
    # QWK を計算する
    return 'qwk', qwk(y_true, y_pred), True


def qwk(y_true, y_pred):
    """QWK (Quadratic Weighted Kappa) を計算する関数"""
    return cohen_kappa_score(y_true, y_pred, weights='quadratic')


def _numerical_only_df(df_):
    """数値のカラムだけ取り出す関数"""
    number_cols = [name for name, dtype in df_.dtypes.items()
                   if issubclass(dtype.type, numbers.Number)]
    numerical_df = df_[number_cols]
    return numerical_df


def main():
    # データセットを読み込む
    train_df = pd.read_csv('train.csv.zip')

    # 説明変数
    x_train = train_df.drop('Response', axis=1)
    # 簡単にするため、とりあえずすぐに使えるものだけ取り出して使う
    x_train = _numerical_only_df(x_train)

    # 目的変数
    y_train = train_df.Response.astype(float)
    # 目的変数はゼロからスタートにする
    y_train -= 1

    # LightGBM のデータセット表現にする
    lgb_train = lgb.Dataset(x_train, y_train)

    # 回帰問題として解く
    lgbm_params = {
        'objective': 'regression',
        'metric': 'rmse',
        'first_metric_only': True,
        'verbose': -1,
    }

    # データ分割の方法
    folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

    # 5-Fold CV で検証する
    result = lgb.cv(lgbm_params,
                    lgb_train,
                    num_boost_round=1000,
                    early_stopping_rounds=100,
                    folds=folds,
                    verbose_eval=10,
                    feval=lgb_custom_metric_qwk_regression,
                    seed=42,
                    )

    # early stop したラウンドでの QWK を出力する
    print(f'CV Mean QWK: {result["qwk-mean"][-1]}')


if __name__ == '__main__':
    main()

上記の実行結果は次のとおり。

$ python qwkreg.py
[10]   cv_agg's rmse: 2.02046 + 0.0085424    cv_agg's qwk: 0.409782 + 0.0042456
[20]   cv_agg's rmse: 1.91647 + 0.0125972    cv_agg's qwk: 0.506518 + 0.00716283
[30]   cv_agg's rmse: 1.87532 + 0.0140473    cv_agg's qwk: 0.553375 + 0.00638559
...(省略)...
[220]  cv_agg's rmse: 1.83657 + 0.0177681    cv_agg's qwk: 0.607652 + 0.00776567
[230]  cv_agg's rmse: 1.83694 + 0.0177138    cv_agg's qwk: 0.608083 + 0.00775286
[240]  cv_agg's rmse: 1.83728 + 0.0176133    cv_agg's qwk: 0.608366 + 0.00780103
CV Mean QWK: 0.6064040204540543

交差検証では QWK の平均として約 0.6064 が得られている。 先ほどよりもスコアが改善している。

回帰問題として解いた上で OOF Prediction を最適化する

続いては、今回の本題である QWK の閾値の最適化について。 OOF で予測した値を、閾値を最適化することでスコアが改善するかどうか確認してみる。

サンプルコードは次のとおり。 OptunaRounder というクラスを導入して、OOF Prediction を最適化している。

#!/usr/bin/env python3

import numbers

import optuna
import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import KFold
from sklearn.metrics import cohen_kappa_score


class OptunaRounder:
    """Optuna を使って QWK の最適な閾値を探索するクラス"""

    def __init__(self, y_true, y_pred):
        # 真のラベル
        self.y_true = y_true
        # 予測したラベル
        self.y_pred = y_pred
        # ラベルの種類
        self.labels = np.unique(y_true)

    def __call__(self, trial):
        """最大化したい目的関数"""
        # 閾値を Define by run で追加していく
        thresholds = []
        # ラベルの数 - 1 が必要な閾値の数になる
        for i in range(len(self.labels) - 1):
            # 閾値の下限 (既存の最大 or ラベルの最小値)
            low = max(thresholds) if i > 0 else min(self.labels)
            # 閾値の上限 (ラベルの最大値)
            high = max(self.labels)
            # 閾値の候補を追加する
            t = trial.suggest_uniform(f't{i}', low, high)
            thresholds.append(t)

        # 閾値の候補を元に QWK を計算する
        opt_y_pred = self.adjust(self.y_pred, thresholds)
        return qwk(self.y_true, opt_y_pred)

    def adjust(self, y_pred, thresholds):
        """閾値にもとづいて予測を補正するメソッド"""
        opt_y_pred = pd.cut(y_pred,
                            [-np.inf] + thresholds + [np.inf],
                            labels=self.labels)
        return opt_y_pred


def lgb_custom_metric_qwk_regression(preds, data):
    """LightGBM のカスタムメトリックを計算する関数

    回帰問題として解いた予測から QWK を計算する"""
    # 正解ラベル
    y_true = data.get_label()
    # 予測ラベル
    y_pred = np.clip(preds, 0, 7).round()  # 単純に clip して四捨五入する
    # QWK を計算する
    return 'qwk', qwk(y_true, y_pred), True


def qwk(y_true, y_pred):
    """QWK (Quadratic Weighted Kappa) を計算する関数"""
    return cohen_kappa_score(y_true, y_pred, weights='quadratic')


class ModelExtractionCallback(object):
    """lightgbm.cv() から学習済みモデルを取り出すためのコールバックに使うクラス"""

    def __init__(self):
        self._model = None

    def __call__(self, env):
        # _CVBooster の参照を保持する
        self._model = env.model

    def _assert_called_cb(self):
        if self._model is None:
            # コールバックが呼ばれていないときは例外にする
            raise RuntimeError('callback has not called yet')

    @property
    def boosters_proxy(self):
        self._assert_called_cb()
        # Booster へのプロキシオブジェクトを返す
        return self._model

    @property
    def raw_boosters(self):
        self._assert_called_cb()
        # Booster のリストを返す
        return self._model.boosters

    @property
    def best_iteration(self):
        self._assert_called_cb()
        # Early stop したときの boosting round を返す
        return self._model.best_iteration


def _numerical_only_df(df_):
    """数値のカラムだけ取り出す関数"""
    number_cols = [name for name, dtype in df_.dtypes.items()
                   if issubclass(dtype.type, numbers.Number)]
    numerical_df = df_[number_cols]
    return numerical_df


def main():
    # データセットを読み込む
    train_df = pd.read_csv('train.csv.zip')

    # 説明変数
    x_train = train_df.drop('Response', axis=1)
    # 簡単にするため、とりあえずすぐに使えるものだけ取り出して使う
    x_train = _numerical_only_df(x_train)
    # Id 列をインデックスに設定する
    x_train = x_train.set_index('Id')

    # 目的変数
    y_train = train_df.Response.astype(float)
    # 目的変数はゼロからスタートにする
    y_train -= 1

    # LightGBM のデータセット表現にする
    lgb_train = lgb.Dataset(x_train, y_train)

    # 回帰問題として解く
    lgbm_params = {
        'objective': 'regression',
        'metric': 'rmse',
        'first_metric_only': True,
        'verbose': -1,
    }

    # データ分割の方法
    folds = KFold(n_splits=5, shuffle=True, random_state=42)

    # 学習済みモデルを取り出すためのコールバックを用意する
    extraction_cb = ModelExtractionCallback()
    callbacks = [
        extraction_cb,
    ]

    # 5-Fold CV で検証する
    result = lgb.cv(lgbm_params,
                    lgb_train,
                    num_boost_round=10000,
                    early_stopping_rounds=100,
                    folds=folds,
                    verbose_eval=10,
                    feval=lgb_custom_metric_qwk_regression,
                    callbacks=callbacks,
                    seed=42,
                    )

    # early stop したラウンドでの QWK を出力する
    print(f'CV Mean QWK: {result["qwk-mean"][-1]}')

    # 学習データを学習済みモデルを使って OOF で予測する
    boosters = extraction_cb.raw_boosters
    best_iteration = extraction_cb.best_iteration
    oof_y_pred = np.zeros_like(y_train)
    for (_, val_index), booster in zip(folds.split(x_train, y_train), boosters):
        x_val = x_train.iloc[val_index]
        oof_y_pred[val_index] = booster.predict(list(x_val.to_numpy()),
                                                num_iteration=best_iteration)

    # ひとまず clipping + 四捨五入する
    oof_y_pred = np.clip(oof_y_pred, 0, 7).round()

    # 最適化していない状態での OOF Prediction の QWK
    raw_oof_qwk = qwk(y_train, oof_y_pred)
    print(f'Raw OOF QWK: {raw_oof_qwk}')

    # Optuna を使って QWK の閾値を最適化する
    objective = OptunaRounder(y_train, oof_y_pred)

    # 探索に 100 秒かける
    study = optuna.create_study(direction='maximize')
    study.optimize(objective, timeout=100)

    # 見つけた閾値
    best_thresholds = sorted(study.best_params.values())
    print(f'Optimized thresholds: {best_thresholds}')

    # 閾値を最適化した場合の QWK
    optimized_oof_y_pred = objective.adjust(oof_y_pred, best_thresholds)
    optimized_oof_qwk = qwk(y_train, optimized_oof_y_pred)
    print(f'Optimized OOF QWK: {optimized_oof_qwk}')

    # テスト用データを読み込む
    test_df = pd.read_csv('test.csv.zip')
    test_df = test_df.set_index('Id')

    # 数値のカラムだけ取り出す
    numerical_test_df = _numerical_only_df(test_df)

    # 学習済みモデルの Averaging で予測する
    boosters_proxy = extraction_cb.boosters_proxy
    test_y_preds = boosters_proxy.predict(numerical_test_df,
                                          num_iteration=best_iteration)
    test_y_pred_avg = np.mean(test_y_preds, axis=0)

    # サンプル提出ファイルを読み込む
    submission_df = pd.read_csv('sample_submission.csv.zip')
    submission_df = submission_df.set_index('Id')

    # 最適化していない予測値 (単純なクリッピングと四捨五入)
    submission_df.Response = np.clip(test_y_pred_avg, y_train.min(), y_train.max() + 1).round().astype(int) + 1
    submission_df.to_csv('unoptimized_submission.csv')

    # 最適化した予測値
    optimized_test_y_pred_avg = objective.adjust(test_y_pred_avg, best_thresholds)
    submission_df.Response = optimized_test_y_pred_avg.astype(int) + 1
    submission_df.to_csv('optimized_submission.csv')


if __name__ == '__main__':
    main()

上記を実行した結果は次のとおり。

$ python qwkregopt.py
[10]   cv_agg's rmse: 2.01959 + 0.0101767    cv_agg's qwk: 0.408957 + 0.00513284
[20]   cv_agg's rmse: 1.9165 + 0.008155  cv_agg's qwk: 0.507252 + 0.00192446
[30]   cv_agg's rmse: 1.87492 + 0.00797763   cv_agg's qwk: 0.553388 + 0.00129979
...(省略)...
[200]  cv_agg's rmse: 1.8374 + 0.00729844    cv_agg's qwk: 0.60726 + 0.00206979
[210]  cv_agg's rmse: 1.83764 + 0.00757892   cv_agg's qwk: 0.607434 + 0.00210639
[220]  cv_agg's rmse: 1.83771 + 0.00759351   cv_agg's qwk: 0.607882 + 0.00205622
CV Mean QWK: 0.6054069736940326
Raw OOF QWK: 0.605407593931218
[I 2020-03-01 12:39:28,349] Finished trial#0 resulted in value: 0.14760909846772208. Current best value is 0.14760909846772208 with parameters: {'t0': 0.6069999513322623, 't1': 6.071871351250824, 't2': 6.421127597260041, 't3': 6.6499083619322334, 't4': 6.825786239070231, 't5': 6.94896643994053, 't6': 6.988042174746154}.
[I 2020-03-01 12:39:28,461] Finished trial#1 resulted in value: 0.3273196067078864. Current best value is 0.3273196067078864 with parameters: {'t0': 1.3022593301640533, 't1': 3.3523582269835885, 't2': 5.775809715879871, 't3': 6.748066158840195, 't4': 6.813600862422318, 't5': 6.952492269479133, 't6': 6.986909664863027}.
[I 2020-03-01 12:39:28,580] Finished trial#2 resulted in value: 0.1845825834695184. Current best value is 0.3273196067078864 with parameters: {'t0': 1.3022593301640533, 't1': 3.3523582269835885, 't2': 5.775809715879871, 't3': 6.748066158840195, 't4': 6.813600862422318, 't5': 6.952492269479133, 't6': 6.986909664863027}.
...(省略)...
[I 2020-03-01 12:41:08,371] Finished trial#660 resulted in value: 0.3366380459371958. Current best value is 0.6461578531751463 with parameters: {'t0': 1.9366086554521658, 't1': 2.765496885821397, 't2': 3.711341767628701, 't3': 3.8616368179727982, 't4': 4.309923993071266, 't5': 5.4467521001848525, 't6': 5.753790447645578}.
Optimized thresholds: [1.9366086554521658, 2.765496885821397, 3.711341767628701, 3.8616368179727982, 4.309923993071266, 5.4467521001848525, 5.753790447645578]
Optimized OOF QWK: 0.6461578531751463

最適化する前の OOF Prediction の QWK が約 0.6054 なのに対して、閾値を最適化すると約 0.6461 と、大きくスコアが向上している。

手元の交差検証だけでは何か間違っている恐れがあるので、テストデータに対するサブミッションまでやってみた。 先ほどのモジュールを実行すると unoptimized_submission.csv という最適化前のファイルと、optimized_submission.csv という最適化後のファイルができる。 すると、最適化する前の Public LB / Private LB が 0.61931 / 0.62190 だったのに対して、最適化した後は 0.65801 / 0.66005 と、ちゃんと効果があることが確認できた。

所感など

QWK の最適化は、真のラベルの分布に依存している。 そのため、学習済みモデルを使って予測する対象が学習時の分布と異なる場合、最適化の効果が得られない。 この点から、Private データの分布が異なる場合には最適化が有効ではないと考えられる。

だとすると、Private データの分布が学習時と同じであるという確証が得られない限りは使うのは賭けになるのでは、という疑問が浮かんだ。 とはいえ、うまくいった場合の効果は絶大なので、ある程度の仮定のもとに使う、という選択肢は現実的なのかもしれない。

いじょう。

参考

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

www.kaggle.com

blog.amedama.jp

blog.amedama.jp

Linux の Network Namespace と radvd / dnsmasq で IPv6 SLAAC (+RDNSS) を試す

今回は、Linux の Network Namespace と radvd / dnsmasq を使って IPv6 の SLAAC を試してみる。 IPv6 では、アドレスの自動設定にいくつかのやり方がある。 SLAAC というのは、そのひとつで RFC 4862 で定義されている IPv6 Stateless Address Autoconfiguration のことを指す。 SLAAC では ICMPv6 NDP (Neighbor Discovery Protocol) の RA (Router Advertisement) というメッセージでアドレスとデフォルトルートを設定する。 その上で、RFC 8415 で定義されている Stateless DHCPv6 というプロトコルを使って DNS や NTP サーバを設定する。 なお、現在では RFC 8106 で定義されている RDNSS というオプションを使うことで、RA 単独でも DNS サーバを設定することができる。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.4 LTS"
$ uname -r
4.15.0-76-generic
$ dpkg -l | egrep "(radvd|dnsmasq|isc-dhcp-client)"
ii  dnsmasq                         2.79-1                              all          Small caching DNS proxy and DHCP/TFTP server
ii  dnsmasq-base                    2.79-1                              amd64        Small caching DNS proxy and DHCP/TFTP server
ii  isc-dhcp-client                 4.3.5-3ubuntu7.1                    amd64        DHCP client for automatically obtaining an IP address
ii  radvd                           1:2.16-3                            amd64        Router Advertisement Daemon

もくじ

作るネットワーク

用意するネットワークの物理的な構成は次のとおり。 白抜きになっている箱が Network Namespace を表している。 また、端点をもった線は veth インターフェイスを表している。

ネットワークの物理的な構成

ネットワークの論理的な構成は次のとおり。

ネットワークの論理的な構成

実験では host のアドレスやデフォルトルートを設定することになる。

下準備

下準備として、必要なパッケージをインストールしておく。

$ sudo apt-get -y install radvd dnsmasq isc-dhcp-client tcpdump

ネットワークを作る

まずは Network Namespace を作る。

$ sudo ip netns add host
$ sudo ip netns add router

そして、Network Namespace 同士をつなぐ veth インターフェイスを作る。

$ sudo ip link add ht-veth0 type veth peer name gw-veth0

作ったインターフェイスを Network Namespace に所属させる。

$ sudo ip link set ht-veth0 netns host
$ sudo ip link set gw-veth0 netns router

デフォルトでは EUI-64 を使ってアドレスの下位 64 ビットが生成されるため、わかりやすいように MAC アドレスを変更しておく。

$ sudo ip netns exec host ip link set dev ht-veth0 address 00:00:5E:00:53:01
$ sudo ip netns exec router ip link set dev gw-veth0 address 00:00:5E:00:53:02

veth インターフェイスの状態を UP に設定する。

$ sudo ip netns exec host ip link set ht-veth0 up
$ sudo ip netns exec router ip link set gw-veth0 up

router の方にリンクローカルアドレス (fe80::1 と、グローバルアドレスを模したドキュメンテーションアドレス (2001:db8::1) を付与しておく。

$ sudo ip netns exec router ip address add fe80::1/64 dev gw-veth0
$ sudo ip netns exec router ip address add 2001:db8::1/64 dev gw-veth0

次のようにアドレスが付与された。

$ sudo ip netns exec router ip address show gw-veth0
16: gw-veth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 2001:db8::1/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::1/64 scope link 
       valid_lft forever preferred_lft forever
    inet6 fe80::200:5eff:fe00:5302/64 scope link 
       valid_lft forever preferred_lft forever

あとは router の方で IPv6 のルーティングを有効にしておく。 ちなみに、このカーネルパラメータのフラグを立てておかないと radvd が起動しない。

$ sudo ip netns exec router sysctl -w net.ipv6.conf.all.forwarding=1

radvd

はじめに、radvd から試してみる。 このプログラムは ICMPv6 NDP の RA を送ることができる。

はじめに、radvd の設定ファイルを用意する。 次の設定で、プレフィックスとして 2001:db8::/64 を広告しつつ、デフォルトルートを 2001:db8::1 に向けることができる。

cat << 'EOF' > radvd.conf
interface gw-veth0 {

    # 定期的にルータ広告を送る
    AdvSendAdvert on;

    # SLAAC でアドレスの自動生成に使うプレフィックスを広告する
    prefix 2001:db8::/64 { };

};
EOF

通信を観察するために tcpdump をしかけておく。

$ sudo ip netns exec host tcpdump -tnlvv -i ht-veth0 ip6
tcpdump: listening on ht-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes

準備ができたら、用意した設定ファイルを使って radvd を起動する。

$ sudo ip netns exec router radvd -C radvd.con

すると、次のように prefix オプションを含む RA メッセージが出る。

$ sudo ip netns exec host tcpdump -tnlvv -i ht-veth0 ip6
tcpdump: listening on ht-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes
IP6 (flowlabel 0xa72b0, hlim 255, next-header ICMPv6 (58) payload length: 56) fe80::1 > ff02::1: [icmp6 sum ok] ICMP6, router advertisement, length 56
    hop limit 64, Flags [none], pref medium, router lifetime 1800s, reachable time 0ms, retrans timer 0ms
      prefix info option (3), length 32 (4): 2001:db8::/64, Flags [onlink, auto], valid time 86400s, pref. time 14400s
        0x0000:  40c0 0001 5180 0000 3840 0000 0000 2001
        0x0010:  0db8 0000 0000 0000 0000 0000 0000
      source link-address option (1), length 8 (1): 00:00:5e:00:53:02
        0x0000:  0000 5e00 5302

host のインターフェイスは、RA を受信して prefix オプションを使ってアドレスを自動設定する。

$ sudo ip netns exec host ip address show dynamic ht-veth0
11: ht-veth0@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 2001:db8::200:5eff:fe00:5301/64 scope global dynamic mngtmpaddr 
       valid_lft 86379sec preferred_lft 14379sec

また、同時にルータ広告を送ってきたリンクローカルアドレスにデフォルトルートを設定する。

$ sudo ip netns exec host ip -6 route show
2001:db8::/64 dev ht-veth0 proto kernel metric 256 expires 86388sec pref medium
fe80::/64 dev ht-veth0 proto kernel metric 256 pref medium
default via fe80::1 dev ht-veth0 proto ra metric 1024 expires 1788sec hoplimit 64 pref medium

RDNSS の設定を追加してみる

RA の基本的な動作が確認できたので、つづいては RDNSS の設定を追加してパケットを観察してみる。

radvd の設定ファイルに RDNSS の設定を追加する。 配布する DNS サーバのアドレスは 2001:db8::dead:beef に指定した。

$ cat << 'EOF' > radvd.conf
interface gw-veth0 {

    # 定期的にルータ広告を送る
    AdvSendAdvert on;

    # SLAAC でアドレスの自動生成に使うプレフィックスを広告する
    prefix 2001:db8::/64 { };

    # RDNSS (RFC 8106) で DNS サーバを広告する
    RDNSS 2001:db8::dead:beef { };
};
EOF

radvd のプロセスに SIGHUP を送って設定ファイルを読み直させる。

$ sudo kill -HUP $(cat /var/run/radvd.pid)

すると、次のとおり RDNSS オプションを含む RA が観察できた。

$ sudo ip netns exec host tcpdump -tnlvv -i ht-veth0 ip6
tcpdump: listening on ht-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes

...(snip)...

IP6 (flowlabel 0xa72b0, hlim 255, next-header ICMPv6 (58) payload length: 80) fe80::1 > ff02::1: [icmp6 sum ok] ICMP6, router advertisement, length 80
    hop limit 64, Flags [none], pref medium, router lifetime 1800s, reachable time 0ms, retrans timer 0ms
      prefix info option (3), length 32 (4): 2001:db8::/64, Flags [onlink, auto], valid time 86400s, pref. time 14400s
        0x0000:  40c0 0001 5180 0000 3840 0000 0000 2001
        0x0010:  0db8 0000 0000 0000 0000 0000 0000
      rdnss option (25), length 24 (3):  lifetime 600s, addr: 2001:db8::dead:beef
        0x0000:  0000 0000 0258 2001 0db8 0000 0000 0000
        0x0010:  0000 dead beef
      source link-address option (1), length 8 (1): 00:00:5e:00:53:02
        0x0000:  0000 5e00 5302

dnsmasq (RA + Stateless DHCPv6)

つづいては dnsmasq を使って RA + Stateless DHCPv6 のパターンを検証してみる。

どうやら dnsmasq には RA を送る機能もあるようなので、いったん radvd のプロセスは kill しておく。

$ sudo kill $(cat /var/run/radvd.pid)

あらためて tcpdump をしかけておく。

$ sudo ip netns exec host tcpdump -tnlvv -i ht-veth0 ip6

そして、dnsmasq を起動する。 --enable-ra オプションが RA を送る指定になっている。

$ sudo ip netns exec router dnsmasq \
  --enable-ra \
  --dhcp-range=::,constructor:gw-veth0,ra-stateless \
  --dhcp-option=option6:dns-server,[2001:db8::dead:beef] \
  --dhcp-option=option6:ntp-server,[2001:db8::dead:beef] \
  --no-daemon

準備ができたら isc-dhcp の dhclient を起動する。 -6 オプションと -S オプションを組み合わせることで IPv6 SLAAC のモードになる。

$ sudo ip netns exec host dhclient -6 -S ht-veth0

tcpdump のターミナルを確認すると、次のとおり RA と Stateless DHCPv6 のパケットがやり取りされていることがわかる。 なお、RA にはデフォルトで RDNSS オプションが付与されるらしい。 まあ、たしかに Stateless DHCPv6 と同じ DNS サーバのアドレスを配布するなら、オプションがあっても副作用はとくにないのかな?

$ sudo ip netns exec host tcpdump -tnlvv -i ht-veth0 ip6
tcpdump: listening on ht-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes

... (snip) ...

IP6 (class 0xc0, flowlabel 0xa72b0, hlim 255, next-header ICMPv6 (58) payload length: 88) fe80::1 > ff02::1: [icmp6 sum ok] ICMP6, router advertisement, length 88
    hop limit 64, Flags [other stateful], pref medium, router lifetime 1800s, reachable time 0ms, retrans timer 0ms
      prefix info option (3), length 32 (4): 2001:db8::/64, Flags [onlink, auto], valid time 3600s, pref. time 3600s
        0x0000:  40c0 0000 0e10 0000 0e10 0000 0000 2001
        0x0010:  0db8 0000 0000 0000 0000 0000 0000
      mtu option (5), length 8 (1):  1500
        0x0000:  0000 0000 05dc
      source link-address option (1), length 8 (1): 00:00:5e:00:53:02
        0x0000:  0000 5e00 5302
      rdnss option (25), length 24 (3):  lifetime 3600s, addr: 2001:db8::dead:beaf
        0x0000:  0000 0000 0e10 2001 0db8 0000 0000 0000
        0x0010:  0000 dead beaf

IP6 (flowlabel 0x5f40a, hlim 1, next-header UDP (17) payload length: 48) fe80::200:5eff:fe00:5301.546 > ff02::1:2.547: [bad udp cksum 0xafc9 -> 0x2ffe!] dhcp6 inf-req (xid=29221d (client-ID hwaddr/time type 1 time 634459916 00005e005301) (option-request DNS-server DNS-search-list Client-FQDN SNTP-servers) (elapsed-time 0))

IP6 (class 0xc0, flowlabel 0x48785, hlim 64, next-header UDP (17) payload length: 76) fe80::1.547 > fe80::200:5eff:fe00:5301.546: [bad udp cksum 0xaf61 -> 0x705b!] dhcp6 reply (xid=29221d (client-ID hwaddr/time type 1 time 634459916 00005e005301) (server-ID hwaddr/time type 1 time 634459172 00005e005302) (DNS-server 2001:db8::dead:beaf) (lifetime 3600))

めでたしめでたし。

参考文献

tools.ietf.org

tools.ietf.org

tools.ietf.org

linux.die.net

linux.die.net

linux.die.net

Linux で VXLAN を扱ってみる

久しぶりに VXLAN について調べたところ、カーネルの機能で VXLAN インターフェイスが作れるようになってたので試してみた。 ここでいう VXLAN というのは、RFC7348 で定義されている Virtual eXtensible Local Area Network というプロトコルを指す。 このプロトコルを使うと Layer 2 のトンネリングが実現できる。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.4 LTS"
$ uname -r
4.15.0-76-generic
$ dpkg -l | grep iproute2
ii  iproute2                             4.15.0-2ubuntu1                     amd64        networking and traffic control tools
$ lsmod | grep -i vxlan
vxlan                  57344  0
ip6_udp_tunnel         16384  1 vxlan
udp_tunnel             16384  1 vxlan

もくじ

ネットワークの構成

検証には Network Namespace と veth インターフェイスを使って作ったネットワークを用いる。

ネットワークの物理的な構成は次のとおり。

f:id:momijiame:20200208004155p:plain
物理的なネットワーク構成

ネットワークは、全部で 3 つの Network Namespace からできている。 真ん中の router はルータとして動作し、残りの ns1ns2 はホストとして動作する。 端点をもった線はインターフェイスのつながりを表している。 その中でも、緑の破線はトンネリングによって仮想的なつながりがあることを示している。

ネットワークの論理的な構成は次のとおり。

f:id:momijiame:20200208004242p:plain
論理的なネットワーク構成

ネットワークは、全部で 3 つのセグメントからできている。 192.0.2.0/24198.51.100.0/24 は、通常のルーティングをする。 対して、203.0.113.0/24 は VXLAN によって延伸されたブロードキャストドメインで動作する。

下準備

あらかじめ、必要なパッケージをひととおりインストールしておく。

$ sudo apt-get -y install iproute2 iputils-ping tcpdump

ネットワークを作る

まずは Network Namespace を用意する。

$ sudo ip netns add ns1
$ sudo ip netns add router
$ sudo ip netns add ns2

つづいて veth インターフェイスを用意する。

$ sudo ip link add ns1-veth0 type veth peer name gw-veth0
$ sudo ip link add ns2-veth0 type veth peer name gw-veth1

作成した veth インターフェイスを Network Namespace に所属させていく。

$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set gw-veth0 netns router
$ sudo ip link set gw-veth1 netns router
$ sudo ip link set ns2-veth0 netns ns2

インターフェイスの状態を UP に設定する。

$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip netns exec router ip link set gw-veth0 up
$ sudo ip netns exec router ip link set gw-veth1 up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up

インターフェイスに IP アドレスを付与する。

$ sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
$ sudo ip netns exec router ip address add 192.0.2.254/24 dev gw-veth0
$ sudo ip netns exec router ip address add 198.51.100.254/24 dev gw-veth1
$ sudo ip netns exec ns2 ip address add 198.51.100.1/24 dev ns2-veth0

ns1ns2 のデフォルトルートを router の IP アドレスに向ける。

$ sudo ip netns exec ns1 ip route add default via 192.0.2.254
$ sudo ip netns exec ns2 ip route add default via 198.51.100.254

rotuer がルータとして動作するようにカーネルパラメータを設定する。

$ sudo ip netns exec router sysctl net.ipv4.ip_forward=1

ひとまず、通常のルーティングが動作することを確認しておく。

$ sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1
PING 198.51.100.1 (198.51.100.1) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 198.51.100.1: icmp_seq=1 ttl=63 time=0.217 ms
64 bytes from 198.51.100.1: icmp_seq=2 ttl=63 time=0.043 ms
64 bytes from 198.51.100.1: icmp_seq=3 ttl=63 time=0.191 ms

--- 198.51.100.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2026ms
rtt min/avg/max/mdev = 0.043/0.150/0.217/0.077 ms

VXLAN を設定する

つづいて、今回の主眼である VXLAN のインターフェイスを用意する。 VNI には 100 を使った。

まずは ns1 に VXLAN インターフェイスを作る。

$ sudo ip netns exec ns1 \
     ip link add ns1-vxlan0 \
     type vxlan \
     id 100 \
     remote 198.51.100.1 \
     dstport 4789 \
     dev ns1-veth0

つづいて ns2 にも VXLAN インターフェイスを作る。

$ sudo ip netns exec ns2 \
     ip link add ns2-vxlan0 \
     type vxlan \
     id 100 \
     remote 192.0.2.1 \
     dstport 4789 \
     dev ns2-veth0

あとは、作った VXLAN インターフェイスに IP アドレスを付与したら状態を UP に設定するだけ。

$ sudo ip netns exec ns1 ip link set ns1-vxlan0 up
$ sudo ip netns exec ns1 ip address add 203.0.113.1/24 dev ns1-vxlan0
$ sudo ip netns exec ns2 ip link set ns2-vxlan0 up
$ sudo ip netns exec ns2 ip address add 203.0.113.2/24 dev ns2-vxlan0

パケットキャプチャするために ns2 のインターフェイスに tcpdump をしかけておく。

$ sudo ip netns exec ns2 tcpdump -tnl -i ns2-veth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ns2-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes

ns1 から ns2 に向けて Ping を打つ。 指定する IP アドレスは VXLAN インターフェイスのものを使う。

$ sudo ip netns exec ns1 ping -c 3 203.0.113.2 -I 203.0.113.1
PING 203.0.113.2 (203.0.113.2) from 203.0.113.1 : 56(84) bytes of data.
64 bytes from 203.0.113.2: icmp_seq=1 ttl=64 time=0.541 ms
64 bytes from 203.0.113.2: icmp_seq=2 ttl=64 time=0.094 ms
64 bytes from 203.0.113.2: icmp_seq=3 ttl=64 time=0.074 ms

--- 203.0.113.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2010ms
rtt min/avg/max/mdev = 0.074/0.236/0.541/0.215 ms

ちゃんとトンネリングが成功して、Ping の疎通がある。

先ほどしかけた tcpdump にも、VXLAN のパケットがキャプチャされている。 VXLAN のペイロードになっているパケット (フレーム) も一度に確認できるようだ。

$ sudo ip netns exec ns2 tcpdump -tnl -i ns2-veth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ns2-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 192.0.2.1.52760 > 198.51.100.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9404, seq 1, length 64
IP 198.51.100.1.52760 > 192.0.2.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9404, seq 1, length 64
IP 192.0.2.1.52760 > 198.51.100.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9404, seq 2, length 64
IP 198.51.100.1.52760 > 192.0.2.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9404, seq 2, length 64
IP 192.0.2.1.52760 > 198.51.100.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9404, seq 3, length 64
IP 198.51.100.1.52760 > 192.0.2.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9404, seq 3, length 64

VXLAN インターフェイスの方で tcpdump をかけると、もちろん ICMP しか観測できない。

$ sudo ip netns exec ns2 tcpdump -tnl -i ns2-vxlan0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ns2-vxlan0, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9408, seq 1, length 64
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9408, seq 1, length 64
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9408, seq 2, length 64
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9408, seq 2, length 64
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9408, seq 3, length 64
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9408, seq 3, length 64

いじょう。

参考文献

tools.ietf.org

https://www.kernel.org/doc/Documentation/networking/vxlan.txt

備考

ネットワークの物理的な構成を図示するのに使った blockdiag の定義は次のとおり。

blockdiag {
  ns1-vxlan0 [shape = endpoint];
  ns1-veth0 [shape = minidiamond];
  gw-veth0 [shape = minidiamond];
  gw-veth1 [shape = minidiamond];
  ns2-veth0 [shape = minidiamond];
  ns2-vxlan0 [shape = endpoint];

  group ns1 {
    orientation = portrait;
    label = 'ns1';
    color = '#CCCCFF';
    shape = line;
    ns1-veth0;
    ns1-vxlan0;
  }

  group router {
    label = 'router';
    color = '#CCCCFF';
    shape = line;
    gw-veth0;
    gw-veth1;
  }

  group ns2 {
    orientation = portrait;
    label = 'ns2';
    color = '#CCCCFF';
    shape = line;
    ns2-veth0;
    ns2-vxlan0;
  }

  ns1-vxlan0 -- ns1-veth0 [style = dotted];
  ns1-veth0 -- gw-veth0;
  gw-veth1 -- ns2-veth0;
  ns2-veth0 -- ns2-vxlan0 [style = dotted];
  ns1-vxlan0 -- ns2-vxlan0 [color = '#77FF77', style = dashed];
}

ネットワークの論理的な構成を図示するのに使った nwdiag の定義は次のとおり。

nwdiag {

  network {
    address = '192.0.2.0/24';
    ns1[address = 'ns1-veth0, 192.0.2.1'];
    router[address = 'gw-veth0, 192.0.2.254'];
  }

  network {
    address = '198.51.100.0/24';
    router[address = 'gw-veth1, 198.51.100.254'];
    ns2[address = 'ns2-veth0, 198.51.100.1'];
  }

  network {
    address = '203.0.113.0/24';
    ns1[address = 'ns1-vxlan0, 203.0.113.1'];
    ns2[address = 'ns2-vxlan0, 203.0.113.2'];
  }
}

Lenovo ThinkPad E595 を買ってメモリとストレージを交換してみた

我が家には、10 年前に購入した、OS が Windows 7 のノートパソコンが 1 台あった。 活躍する機会はさほど多くないものの、EOL を迎える製品を使い続けるリスクを考えて、以前からリプレースの機会をうかがっていた 1。 今回は、その買いかえに関する備忘録について書いてみる。

もくじ

次の機種に求められる要件について

まず、購入するからには長く使いたい。 そのため、ハードウェアの要件については次のように定めた。

  • CPU は 4 コア以上
  • メモリは 8GB 以上
  • ストレージは SSD で 256GB 以上
  • ディスプレイの解像度はフル HD 以上

また、互換性を考えて純正の Microsoft Office が使いたい。 できれば、バンドル版が安く提供されていると嬉しかった。

ただ、バンドル版か否かに関わらず、Microsoft Office の購入費用を加えて上記のスペックを満たしたものとなると、大抵の製品は 10 万円をこえる。 これだと、あまりお値打ち感はない。 そうした中で、なかなか機種が選定できずにいたものの、あるとき次の機種が目にとまった。

kakaku.com

これは、コストパフォーマンスを重視した ThinkPad の E シリーズで、15 インチのディスプレイを持ったモデル。 これをカスタマイズして、ハードウェアを最小構成 2 にした上で Microsoft Office をバンドルすると、5.7 万円で買えることがわかった 3

で、ここからが本題なんだけど、このモデルはメモリやストレージがオンボードになっていない。 また、公式通販でもさまざまな組み合わせでカスタマイズできるとおり、パーツの相性が特に厳しいということもないようだ。 そのため、Web を調べると、メモリやストレージを交換して使っている人が割と見つかる。 なるほど、であればこれに自分でパーツを入れかえて使えば安くあがるのでは?と考えて試算してみた。

ちなみに、この時点ではまだ ThinkPad に E495 というモデルがあることを知らなかった。 こちらは、同じシリーズの 14 インチで、E595 よりも重量が 350g ほど軽い。 筐体のサイズ以外には値段もほとんど変わらないので、今ならこちらを選んでいたかもしれない。

kakaku.com

なお、E495 の場合でも使えるパーツの規格は変わらない。 また、パーツの交換に必要な手順も基本的に同じはず。 ただし、背面パネルにはあらかじめ NVMe SSD のヒートシンクが付属しているらしい。

適合するパーツを選ぶ

まず、メモリは DDR4-2400 の SO-DIMM が使える。 なお、DDR4 は基本的に上位互換があるので、似たような値段の DDR4-2666 を選んでも問題ない。 要件を上回る 16GB を買っても、今なら ¥8,000 前後から手に入る。 メモリのスロットは 2 つあるので、デュアルチャネルを活かすために 8GB のモジュールを 2 枚買う。 ちなみに、ThinkPad E595 は最大で合計 32GB までのメモリに対応している。

続いて、ストレージとしてはファームファクタが M.2 2280 の NVMe SSD を選ぶ。 こちらも、要件を上回る 500GB のモデルを買っても、今なら ¥8,000 前後から手に入る。

なお、M.2 タイプの SSD は発熱がはげしいので、ただ読み書きのパフォーマンスが良いモデルを選べば良いわけではない。 ノートパソコンに関しては、デスクトップよりも排熱が問題になりやすい。 今回も、パフォーマンスは抑えめのモデルを選んだ上で、気休めに次のヒートシンクを貼り付けることにした。 ノートパソコンの場合、筐体の余裕から使えるヒートシンクの厚みが限られる点に注意が必要になる。 このヒートシンクであれば、サイドパネルを外すことでかなり薄くなるようだ。

なお、筐体には SATA の 2.5 インチスロットも用意されている。 そのため、読み書きの速度はインターフェイス的にやや落ちる 4 ものの、こちらを選んでも問題ない。

ということで、次の要件を満たしつつ 7.5 万円ほどで買えることがわかった。

  • CPU は 4 コア 8 スレッド (Ryzen 5 3500U)
  • メモリは 16GB
  • ストレージは 500GB の NVMe SSD
  • バンドル版 の Microsoft Office 2019 (Home and Business)
  • ディスプレイの解像度はフル HD

購入した後の手順について

試算にもとづいて、本体とパーツをひととおり購入した。 Lenovo のブランドになってからは、はじめての ThinkPad になる。

ここからは、パーツを交換する手順を見ていこう。

リカバリ用の USB メモリを作る

リカバリ用の USB メモリ (USB リカバリードライブ) を作成する手順は以下のページに記載されている。

pcsupport.lenovo.com

必要な手順は次のとおり。

  1. Lenovo リカバリーページ5 にアクセスしてリカバリーメディアを注文する
  2. Lenovo USB Recovery Creator ツールをダウンロードする
  3. Lenovo USB Recovery Creator ツールを実行する
  4. (1) で注文したリカバリーメディアを選択してUSBリカバリードライブを作成する

上記の手順を実施して、あらかじめリカバリ用の USB メモリを作っておく。 Web には Windows のリカバリメディアを使う方法を試している人もいるけど、こちらの方が完全に製品出荷時の状態にできるので良いと思う。 使う USB メモリは余裕を持って 64GB 以上のものを選んだ方が良いと思う。

パーツを交換する

パーツを交換する前には、あらかじめバッテリーの給電を含めて完全に電源を落としておく。 それには、起動画面で Lenovo のロゴが出ているときに F1 キーを押して BIOS 画面に入る。 Config > Power > Disable built-in buttery を Disabled にして電源を完全に切る。

このモデルは、筐体の開け方にややクセがある。 ドライバーで裏側のネジをすべてゆるめただけでは、底面のパネルが取れない。 サイドにある隙間にヘラなどを差し込んで、固定されているツメをぐるっと一周すべて外す必要がある。 YouTube なんかに、開け方の動画が上がってるので参考にすると良い。

底面パネルを外すと、こんな感じ。

あらかじめインストールされているメモリのモジュールを外して、購入したモジュールを差し込む。 そして、M.2 のスロットには、ヒートシンクを貼り付けた SSD を固定する。 デフォルトの 2.5 インチ HDD は、外すと 100g ほど軽量化になるけど、せっかくなのでそのままにした。

用意した USB メモリを使ってリカバリする

パーツの交換がおわったら、リカバリ用の USB メモリを挿した状態で起動して、また BIOS 画面に入る。 そして、Startup > Boot から起動するデバイスの順序でリカバリ用の USB メモリをもっとも上にした状態で F10 を押下して起動する。 すると、リカバリがはじまるのであとはしばらく待つだけ。

なお、注意点としてストレージの中に OS がインストールされているものが残っているとリカバリがうまくいかない。 そのため、リカバリツールでは思い切って既存のすべてのストレージを消去するオプションを選択する必要がある。 あるいは、パーツを交換する作業で既存のストレージを取り外してしまうのも手だと思う。

つかう

リカバリがおわったら、あとは Windows 10 をセットアップして使う。 なお、気になる人は、UEFI に対応した Memtest86+ を使ってメモリに初期不良がないか確かめておくと良いと思う。

www.memtest.org

はじめに、負荷をかけたときの M.2 SSD の発熱が気になっていたのでベンチマークした結果は次のとおり。 負荷をかけた状態で 50℃ いくかいかないか、負荷をかけない状態なら 38℃ 前後で落ち着いた。

なお、Microsoft Office に関してはリカバリツールではインストールされない。 そのため、Microsoft にアカウントを作ってプロダクトキーを入力してメディアをダウンロードする必要がある。 Microsoft Office のプロダクトキーは、本体の取扱説明書なんかと一緒に入っているので捨てないように注意しよう。

www.office.com

リカバリ直後にインストールされるアプリケーションに関しては、日本のメーカーのように謎のソフトが山のように入る、ということはなかった。 デフォルトでノートンアンチウイルスが入る点は余計と感じたものの、アンインストールしてしまえば問題はない。

Lenovo Vantage という管理ソフトに関しては、なかなかよくできていると感じた。 このソフトを使うと、たとえばバッテリーの充電が自動で中止される閾値を設定できる。 リチウムイオンバッテリーは 50 ~ 75% くらい充電した状態で使うと、もっとも長持ちすると言われている。 反対に 0% や 100% 前後で使っているとバッテリーが劣化する原因になるので、この機能は地味にうれしかった。 また、ハードウェアに固有の部分、BIOS やドライバのアップデートがひとつのソフトウェアで管理できるようになっている。

いじょう。


  1. OS を Windows 10 にして使い続ける、という選択肢もなくはなかったもののの、すでにストレージを SSD に交換するなど延命措置をとった上でバッテリーなどのヘタりもはげしく先が見えていた
  2. メモリを 4GB にして、ストレージから SSD をなくして代わりに 500GB の HDD にする
  3. ThinkPad の公式通販は定期的に値段を上げたり下げたりキャンペーンをよくやるので、現時点 (2020/02/02) ではもう少し高くなる (6.3 万円)
  4. とはいえ、転送速度で 500MB/s と 1500MB/s の違いを体感できるか?というと、おそらく一般的なユースケースでは難しい
  5. https://pcsupport.lenovo.com/jp/ja/lenovorecovery

VirtualBox で仮想マシンが入れ子 (Nested Virtualization) できるようになった

先日リリースされた VirtualBox 6.0 からは AMD の CPU で、6.1 からは Intel の CPU で Nested Virtualization がサポートされた。 Nested Virtualization というのは、仮想マシンの中に仮想マシンを入れ子に作ることを指す。 ようするに、仮想マシンをマトリョーシカのようにする。 この機能は、すでに VMware や KVM といったハイパーバイザではサポートされていたものの、今回それが VirtualBox でも使えるようになったというわけ。 この機能があると、サーバ周りのインフラ系をやっている人たちは、検証環境が作りやすくなってうれしい。 ただし、この機能を実現するには、仮想マシンの中で CPU の仮想化支援機能 (Intel-VT / AMD-V) が有効になっている必要がある 1

VirtualBox 6.1 のリリースノート 2 を見ると、次のような記載がある。

Virtualization core: Support for nested hardware-virtualization on Intel CPUs (starting with 5th generation Core i, codename Broadwell), so far tested only with guest running VirtualBox

どうやら、Intel であれば第 5 世代 Core i 以降の CPU で仮想化支援機能を使った Nested Virtualization ができるようになったらしい。 このニュースは、個人的に感慨深いものだった。 というのも、次のチケットを見てもらいたい。

www.virtualbox.org

このチケットは、VirtualBox に Nested Virtualization の機能を要望したものになっている。 問題は、チケットが作成された日付で、見ると "Opened 11 years ago" とある。 つまり、11 年という歳月をこえて、ユーザに要望されてきた機能がついに実現したというわけ。 ちなみに、これまで開発側の反応はどうだったかというと、チケットには「便利だろうけど実装するの大変だから...」みたいなコメントがあった。 なお、この機能について自分で調べていた頃のブログを調べると、ポストした日付が 8 年前になっていた。

Mac で仮想マシンの入れ子 (Nested Virtualization) をする | CUBE SUGAR STORAGEmomijiame.tumblr.com

今回は、せっかくなので VirtualBox を使った Nested Virtualization を試してみる。 使った環境は次のとおり。

$ sw_vers       
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G2022
$ sysctl -a | grep brand_string
machdep.cpu.brand_string: Intel(R) Core(TM) m3-7Y32 CPU @ 1.10GHz
$ vagrant version | head -n 1
Installed Version: 2.2.7
$ vboxmanage | head -n 1
Oracle VM VirtualBox Command Line Management Interface Version 6.1.2

もくじ

下準備

はじめに、Homebrew を使って Vagrant と VirtualBox をインストールしておく。 もちろん、Vagrant を使わずに VirtualBox の GUI フロントエンドを使ってもかまわない。

$ brew cask install vagrant virtualbox

Vagrant + VirtualBox で仮想マシンを用意する (L1)

物理的なハードウェア上で直接動作する仮想化のことを L1 と呼ぶことがあるようだ。 ようするに、一般的な状況としての仮想マシンがこれ。 まずは L1 の仮想マシンとして Vagrant + VirtualBox を使って Ubuntu 18.04 LTS をインストールする。

仮想マシンのイメージをダウンロードしたら、設定ファイルを生成する。

$ vagrant box add ubuntu/bionic64
$ vagrant init ubuntu/bionic64

次のように Vagrant の設定ファイルができる。

$ head Vagrantfile 
# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at

ここで、設定ファイルを編集する必要がある。 ポイントは最後の vb.customize に渡している引数の --nested-hw-virt on で、これがないと L1 の仮想マシンで CPU の仮想化支援機能が有効にならない。 あと、Nested Virtulization をするには、かなり処理のオーバーヘッドがあるので仮想マシンのリソースは多めに確保しておいた方が良い。

  config.vm.provider "virtualbox" do |vb|
    vb.cpus = "2"
    vb.memory = "2048"
    vb.customize ["modifyvm", :id, "--nested-hw-virt", "on"]
  end

なお、Vagrant ではなく VirtualBox の GUI フロントエンドを使って操作しているときは、仮想マシンの設定画面を開いて次の項目にチェックをつければ良い。

f:id:momijiame:20200202080627p:plain
VirtualBox で Nested Virtualization するのに必要な GUI 設定画面のチェック項目

仮想マシンを起動したらログインする。

$ vagrant up
$ vagrant ssh

これで L1 の仮想マシンとして Ubuntu 18.04 LTS が利用できるようになった。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-72-generic

CPU に仮想化支援機能のフラグが立っていることを確認する

それでは、CPU に仮想化支援機能のフラグが立っていることを確認してみよう。 Linux では proc ファイルシステムの /proc/cpuinfo で CPU のフラグが確認できる。 今回使っているのは Intel の CPU なので "vmx" というフラグを探す。

$ grep vmx /proc/cpuinfo
flags    : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti tpr_shadow flexpriority fsgsbase avx2 invpcid rdseed clflushopt md_clear flush_l1d
flags    : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti tpr_shadow flexpriority fsgsbase avx2 invpcid rdseed clflushopt md_clear flush_l1d

ちゃんと "vmx" フラグが立っていることがわかった。

仮想マシンの中に Vagrant + Libvirt (KVM) で仮想マシンを作る (L2)

続いては、L1 の仮想マシンの中に、さらに仮想マシンを作る。 先ほどのチケットには L1 / L2 共に VirtualBox を使った検証しかしていない、とあった。 そこで、せっかくなので L2 に KVM を使っても動くのかどうか調べてみることにした。 使う環境としては Libvirt 経由で KVM を Vagrant から扱う。

まずは必要なパッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install vagrant-libvirt qemu-kvm libvirt-bin gawk

KVM が使える状態になっていることを kvm-ok コマンドや、カーネルモジュールがロードされていることから確認する。

$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used
$ lsmod | grep kvm
kvm_intel             217088  0
kvm                   610304  1 kvm_intel
irqbypass              16384  1 kvm

現在のユーザを libvirt および kvm グループに参加させる。

$ sudo usermod -aG libvirt,kvm $(who am i | awk '{print $1}')

ここで、いったん L1 の仮想マシンを再起動しておく。

$ exit
$ vagrant reload

そして、もう一度 L1 の仮想マシンにログインする。

$ vagrant ssh

L2 の仮想マシンとしては、違いがわかりやすいように CentOS 7 を使うことにした。 次のようにして仮想マシンを起動する。

$ vagrant box add centos/7 --provider=libvirt
$ vagrant init centos/7
$ vagrant up

ちなみに、前述したとおり Nested Virtualization はオーバーヘッドが大きいので、この作業には大変に時間がかかる。 作業の進捗状況を確認したいときは、次のようにして仮想マシンのコンソールを取って見ると良い。

$ virsh list
$ virsh console <name>

仮想マシンが起動したら、ログインする。

$ vagrant ssh

確認すると、ちゃんと CentOS 7 が動作している。 これで、macOS / Ubuntu 18.04 LTS / CentOS 7 という仮想マシンのマトリョーシカが完成した。

$ cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core) 
$ uname -r
3.10.0-957.12.2.el7.x86_64

なんとも感慨深い。

OpenStack 実践ガイド (impress top gear)

OpenStack 実践ガイド (impress top gear)

  • 作者:古賀 政純
  • 出版社/メーカー: インプレス
  • 発売日: 2016/08/25
  • メディア: 単行本(ソフトカバー)


  1. 完全仮想化をサポートしたハイパーバイザ (Xen など) であれば、その限りではないものの遅い

  2. https://www.virtualbox.org/wiki/Changelog-6.1

Ubuntu 18.04 LTS で Sphinx の PDF をビルドする

今回は Ubuntu 18.04 LTS を使って、Sphinx の PDF をビルドする方法について。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-72-generic

下準備

Sphinx は TeX を使って PDF をビルドするんだけど、TeX Live のパッケージはかなり大きい。 そのため、APT でミラーリポジトリを使えるようにしておいた方が良い。

blog.amedama.jp

以下のコマンドを実行すればミラーリポジトリが有効になる。

$ sudo sed -i.bak -e 's%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g' /etc/apt/sources.list

Sphinx をインストールする

つづいて、Sphinx をインストールする。 最新のバージョンを使いたいので PIP からインストールすることにした。

まずは PIP をインストールする。

$ sudo apt update
$ sudo apt -y install \
      python3-pip \
      python3-setuptools \
      python3-wheel

PIP を使って、最新の Sphinx をインストールする。

$ sudo pip3 install sphinx

もし、バージョンが少し古くても構わないのであれば、次のように APT を使ってインストールすることもできる。

$ sudo apt -y install python3-sphinx

Sphinx のプロジェクトを作成する

sphinx-quickstart コマンドを使ってプロジェクトのテンプレートを作る。

$ sphinx-quickstart

ウィザード形式でプロジェクトの設定を聞かれるので答えていく。

Sphinx のプロジェクトに設定を追加する

日本語を含む PDF をビルドするときは、Sphinx の設定ファイル conf.py に最低限次の設定をした方が良いようだ。

language = 'ja'
latex_docclass = {'manual': 'jsbook'}

TeX Live をインストールする

つづいて、PDF をビルドするのに必要な TeX Live の関連パッケージをインストールする。

$ sudo apt -y install \
   texlive-latex-recommended \
   texlive-latex-extra \
   texlive-fonts-recommended \
   texlive-fonts-extra \
   texlive-lang-japanese \
   texlive-lang-cjk \
   latexmk

あとからパッケージが足りなくてつらい思いをしたくないときは、次のようにしてすべてのパッケージを入れてしまっても良い。 ただし、インストールするのにめちゃくちゃ時間がかかる。

$ sudo apt -y install texlive-full

PDF をビルドする

あとは make コマンドを使って latexpdf ターゲットを実行するだけ。

$ make latexpdf

実行がおわったら、成果物が入るディレクトリに PDF のファイルができているはず。

$ file _build/latex/*.pdf
_build/latex/example.pdf: PDF document, version 1.5

いじょう。

参考文献

sphinx-users.jp

sphinx-users.jp