のぴぴのメモ

自分用のLinuxとかの技術メモ

Linuxのテスト用に、ハングアップやパニック状態にするカーネルモジュールを作ってみた

テストのために、AWSAmazon Linuxインスタンスカーネルレベルでハングアップさせたかったので、テスト用のカーネルモジュールを作ってみました。

できること

  • カーネルのハングアップ(ping応答もできないレベル。プリエンプションをdisableにして実現)
  • 中途半端なハングアップ(ping応答は帰るレベル。リエンプションはenable)
  • カーネルパニック(echo c > /proc/sysrq-triggerと同じ)

使い方

カーネルモジュールのビルド

コードはGitHubにあります。
github.com
(1) 必要なパッケージのインストール
カーネルモジュールのビルドに、gccとkernel-develが必要になります。(Amazon Linuxの場合)

sudo yum -y install git gcc kernel-devel

(2) ソースコードをローカルに持ってきます

git clone https://github.com/Noppy/HangAndPanicKernelModule.git
cd HangAndPanicKernelModule/

(3)ビルドします

make
ls *.ko
hang_panic.ko

使い方

(1)モジュールのインストール

sudo insmod hang_panic.ko
lsmod |grep hang_panic

(2)使い方の確認方法(要root権限)

cat /proc/hang_panic 
<<Hang&Panic module>>
'echo c > /proc/hang_panic' >>> panic
'echo h > /proc/hang_panic' >>> hang(disable local irq and preempt)
'echo H > /proc/hang_panic' >>> hang(disable only local irq)

(3)ハングアップさせる(要root権限)
(a) Local IRQ と preemptionをdisableにしてハング(おそらく、ping応答もできない)

echo h > /proc/hang_panic

(b) Local IRQのみdisable(ping応答はできるレベル)

echo H > /proc/hang_panic

(4)パニック(要root権限)

# echo c > /proc/hang_panic

#蛇足
10年前のkernel2.6時代は、ジャイアントロック(BKL:Big Kernel Lockともいう)があったので、もっと綺麗にハングさせることができたのですが、ソースコードの中を探したのですが、今はシステムワイドなロックのコードがないんですね・・。RHEL3時代に、お客さん先のシステムでBKLに苦しめられたのも、今は昔ということなんですね。

AWS CLIのセットアップ(RHEL/Mac)

1.Amazon LinuxAWS CLIをインストール

Amazon Linuxはデフォルトで、awsコマンドがインストールされています。公式ドキュメントはこちらです。

2.RHELAWS CLIをインストール

こちらを参照

上記の公式ドキュメントの内容をベースにインストールします。

(1) unzipをインストールする

$ sudo yum -y install unzip

(2) AWS CLIバンドルインストーラをダウンロードする

$ curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip"

(3) ダウンロードしたファイルを解凍する

$ unzip awscli-bundle.zip

(4) インストーラを実行する

$ sudo ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws

(5) AWS CLIの動作確認

$ aws --version
aws-cli/1.16.77 Python/2.7.14 Linux/4.14.72-73.55.amzn2.x86_64 botocore/1.12.67

3.MacにAWSCLIをインストール

こちらの記事を参照して下さい。

公式のマックセットアップ手順は、こちらを参照

4.AWS CLIの設定

4.1 管理用のPCからAWS CLIを利用する場合(アクセスキーによる認証)

設定手順は、こちらの公式ページを参照して下さい。
アクセスキーの漏洩によるアカウント乗っ取りの可能があるので、EC2インスタンス上でAWS CLIを利用する場合は、次のIAMロールをEC2に割り当てる方法の利用を検討します。*1
(1)IAMユーザのアクセスキー ID とシークレットアクセスキーの生成と取得
以下は公式ドキュメントからの抜粋です。

  • IAM コンソールを開きます。
  • コンソールのナビゲーションペインで、[Users] を選択します。
  • IAM のユーザー名 (チェックボックスではありません) を選択します。
  • [Security credentials] タブを選択し、次に [Create access key] を選択します。
  • 新しいアクセスキーを表示するには、[Show] を選択します。認証情報は以下のようになります。

(2)AWS CLI設定
aws cliに、取得したアクセスキーとシークレットアクセスキー、デフォルトリージョンを設定します。

$ aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXX      <==アクセスキーを設定
AWS Secret Access Key [None]: YYYYYYYYYYYY  <==シークレットアクセスキーを設定
Default region name [None]: ap-northeast-1  <==デフォルトリージョン設定( ap-northeast-1は東京リージョン)
Default output format [None]:

4.2 EC2からAWS CLIを利用する場合(EC2にIAMロール割り当て)

ユーザガイドは私にはわかりずらかったです。こちらの公式ブログの記事の方がわかりやすかったです。
(1)IAM ロールの作成

  • IAM コンソールを開きます。
  • コンソールのナビゲーションペインで、[Roles] を選択します。
  • [Create role]を選択します。
  • [AWS Service]を選び、許可したいサービスを選び[Next Permission]で進む。
  • 適切なpolicyを選び[next:Review]で進む。
  • Roleの名前と詳細説明を入力し、[Create Role]でロールを作成する

(2)EC2へのRole割り当て

  • EC2コンソールを開きます。
  • コンソールのナビゲーションペインで、[Instances] を選択します。
  • [Create Instance]インスタンス作成中に、作成したRoleを割り当てるか、
  • 割り当てたいインスタン(起動中も可)を選択し、[Attach/Replace IAM Role]で作成したロールをEC2に適用

(3)AWS CLI設定
ロールを割り当てる場合は、アクセスキーとシークレットキーは設定しないようにします。

$ aws configure
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [None]: ap-northeast-1  <==デフォルトリージョン設定( ap-northeast-1は東京リージョン)
Default output format [None]:

5.その他

5.1 コマンド保管

awsコマンドを[TAB]キーで保管してくれるAWSコンプリータという機能があります。これをシェルに組み込めばコマンドを保管してくれます(pythonだからかわかりませんが、保管までに微妙な間があり使いづらいですが)。公式ドキュメントはこちらです。
(1)AWSコンプリータの場所を確認する

$ which aws_completer
/usr/bin/aws_completer

(2)プロファイルに組み込む
環境に依存しますが、例えば "~/.bash_profile"の末尾に下記行を追加します。

#Enable aws command autocompletion
complete -C '/usr/bin/aws_completer' aws

(3)反映
プロファイルを読み込み設定を反映させます。

source .bash_profile

5.2 AWS CLIの設定ファイル

コマンドの設定ファイルは、ホームディレクトリの.awsというディレクトリ(~/.aws)に格納されます。

  • ~/.awsディレクト
    • config    デフォルトリージョンなどの設定
    • credentials アクセスキーとシークレットアクセスキー

MacでVisual Studio CodeをインストールしGit連携するまでの手順

1.はじめに

職場の人にコード何で書いてるか聞いたら、Visual Studio Code(以下、VS Code)を使っているという話しだったので、Macにセットアップしました。かつgit連携もしたのでメモっておきます。

2.VS Codeのセットアップ

2.1 VS Codeのインストール

MicrosoftのページのMacセットアップ手順に従ってセットアップします。→こちらのページ

  • Download Visual Studio CodeからMac用のイメージをダウンロードします
  • ダウンロードしたファイルをアプリケーションフォルダに移動します

f:id:nopipi:20180421160818p:plain:w500

  • 「アプリケーション」からVC Codeを起動します
  • 必要に応じ、Dockに固定します
    • 起動時のアイコンを2本指でクリック(副ボタン)して
    • 「オプション」→「Dock追加」
  • VC Codeを起動します

2.2 コマンドラインからのVS Code設定

コマンドラインから、”code”でVS Codeが起動するように設定を追加します。

  • VC Code上で「Command Palette (⇧⌘P) 」を実行する
  • "shell command: "と入れて絞られた中から、"shell command: Install 'code' command in PATH"を選択して実行(下図参照)

f:id:nopipi:20180421162931p:plain
ちなみに、これを実行すると、"/usr/local/bin/"にcodeという名前で、”/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code”へのシンボリックリンクができます。実行タイトルや公式サイトのセットアップ手順には以下のように$PATH環境変数VS Codeディレクトリが追加されるような記載がありますが、私はこの方法ではできませんでした。*1

Restart the terminal for the new $PATH value to take effect. You'll be able to type 'code .' in any folder to start editing files in that folder.

3.git連携

3.1 gitのセットアップ

VS Codeとgit連携の前に、gitが入っていない場合はまずgitをセットアップします。

  • gitのインストール手順は、googleで探してください。
  • gitの初期設定は、こちらの記事を参照して下さい。

3.2 git連携設定

  • ターミナルを起動し、gitコマンドのpathを控える
$ which git
/usr/local/bin/git
  • VC Codeで「設定」を開き"git.path"に、gitコマンドのパスを設定する

f:id:nopipi:20180421165608p:plain

{
    "git.path" : "/usr/local/bin/git"
}

4.gitを使ってみる

4.1 gitリポジトリをcloneする

ターミナルから普通に操作しても構わないですが、VC Code内で実行したい場合は「統合ターミナル」を起動し操作します。

  • メニューから開く: 「表示」→「統合ターミナル」
  • ショートカット: 「⌃⇧@」(Control + Shift + @)

クローンの方法は、各自調べて下さい。

4.2 VC Codeでgitリポジトリを開いて操作する

VC Code上でのgit利用方法の詳細は、Visual Studio Code で Git を 使う | 験なきものを思はずは を参考にして下さい。
ちなみに私は、VC CodeからAWS CodeCommit上のリポジトリをcloneして利用しています。AWS CodeCommitの使い方については、下記記事を参照して下さい。
nopipi.hatenablog.com

*1:VS Codeのどこかのバージョンで仕様が変わったのかもしれないですね

AWS CodeCommitを使ってみた

1.AWS CodeCommitとは?

AWS CodeCommit は、プライベート Git リポジトリをホストする、安全で高度にスケーラブルなマネージド型のソース管理サービス」です。平たく言えば、「簡単にプライベートなGitリポジトリが利用できるAWSサービス」です。
料金は、5ユーザまで、ストレージ50GB/月まで、10,000Gitリクエスト/月まで、無料で利用できます。
詳細は下記AWSのCodeCommitの概要を参照して下さい。

2.使ってみる

2.1 リポジトリを作ってみる

  • AWSコンソールにログインし、サービスからCodeCommitを選択する。
  • 下記画面が出るので「今すぐ始める」をクリックする

f:id:nopipi:20180421083159p:plain

  • リポジトリの作成
    • リポジトリ名 ※必須。半角英数字と「. _ -」 のみ。
    • 説明 ※任意。日本語可。
  • Eメール通知の設定
    • AWS SNS(Amazon Simple Notification Service)を利用した、プルリクされたなどのイベントのEメール通知設定
    • 通知設定が不要なら「スキップ」する
  • リポジトリに接続」というのが出るので、その内容に従い次の節で、レポジトリ接続用の IAMアカウントを作成します

2.2 gitアクセス用のIAMアカウントを作ってみる

設定内容
  • git接続方式: httpsssh接続が利用でき、かつ複数の認証方式があります。今回はCodeCommitで最も簡単な方法の「AWS CodeCommit 用に HTTPS Git 認証情報を設定」する方法で行います。 *1
  • IAMユーザ: IAMのベストプラクティス *2では、個々のユーザに権限割り当てを行うのではなくグループに権限を割り当てるのを推奨しています。というのを踏まえて、 git管理専用に以下の設定のグループとユーザを作ります。
    • グループDevAdminGrpを作成し必要なポリシーを付与する
    • ユーザgit_adminを作成し、DevAdminGrpに所属させる
手順
  • AWSコンソールから、IAMサービスに移動する
  • グループ作成
    • ダッシュボードから「グループ」を選択し、「新しいグループの作成」を押して「DevAdminGrp」グループを作成する
    • ポリシーのアタッチで、以下の3つのポリシーをアタッチする
      • CodeCommit接続に必要なポリシー (2つ)
        • IAMSelfManageServiceSpecificCredentials*3
        • IAMReadOnlyAccess*4
      • Gitレポジトリ操作用に必要なポリシー(1つ)
        • AWSCodeCommitFullAccess
  • ユーザー作成
    • ダッシュボードから「ユーザ」を選び「ユーザを追加」を押す
    • ユーザ名に「git_admin」を入れる。「プログラムによるアクセス」は後で削除するので、どちらかを適当に選択する。
    • 先ほど作成した「DevAdminGrp」ユーザを追加する
    • 確認をしてユーザ作成を行う。アクセスキーやパスワードはこの後削除するので控えは不要。
  • ユーザ認証情報の設定
    • 作成した「git_admin」ユーザを選び、「認証情報」のタブをクリックします
    • 「サインイン認証情報」で、「コンソールのパスワード」が「有効」になっている場合は「無効」に変更する
    • 「アクセスキー」が存在する場合、右端の「X」ボタンを押し、アクセスキーを削除する
    • AWS CodeCommit の HTTPS Git 認証情報」で「生成」を押し、gitへのhttps接続用のユーザとパスワードを生成する。(ユーザ名とパスワードを必ず控える!)

2.3 gitコマンドでcloneしてみる

gitの初期設定

PCにgitがインストールされてない場合は、gitをインストールします。
インストール後に、gitの初期設定として、(1)メールアドレス、(2)名前、(3)push時のモードを指定して、最後に設定内容を確認します。

$ git config --global user.email "xxxxxxx@gmail.com"
$ git config --global user.name "xxxxxxxx"
$ git config --global push.default simple

$ git config --global -l
CodeCommitと接続しリポジトリをcloneする
  • AWSコンソールで、CodeCommitの対象レポジトリに移動し「クローンURL」の「https」のURLをコピーする
  • コンソールを開き、下記コマンドでcloneする
    • 実行するとユーザ名とパスワードを聞かれるので、控えた「HTTPS Git 認証情報」を入力する
    • "git branch"はcloneされたことを示すためのコマンドで必須ではない
$ git clone  <https://リポジトリcloneURL>
Cloning into 'リポジトリ名...
Username for 'https://git-codecommit.ap-northeast-1.amazonaws.com': XXXXXXXX <==控えたユーザ名を入力
Password for 'https://XXXXXXXX@git-codecommit.ap-northeast-1.amazonaws.com':  XXXXX <== 控えたパスワードを入力
$ cd <リポジトリ名称>
$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

3.gitコマンドでの、HTTPS Git認証情報の扱いについて

cloneする時に入力したHTTPS接続ときのユーザ名とパスワードは、gitのcredential.helper機能を使い管理することができます。
管理モードは以下の5つです。*5

  • 管理しない
  • メモリ上に一時キャッシュする: cache
  • テキストファイルで保存する: store
  • (Macのみ)Macのキーチェーンを利用する: osxkeychain
  • (Winのみ)Windowsの管理機能(Windows Credential Store)を利用する: wincred

プラットホームごとの確認はしてないですが、少なくともMacはosxkeychainがデフォルトのようですので、特に意識しなくても大丈夫そうです。(git公式ページのgit-osx-installerからセットアップした場合)

*1:https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/setting-up.html

*2:https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/best-practices.html#bp-use-aws-defined-policies

*3:IAMSelfManageServiceSpecificCredentialsは、自分のIAMの認証設定の参照/変更用ポリシーのようです

*4:IAMReadOnlyAccessは、IAM情報の参照用ポリシーのようです

*5:Git - Credential Storage

clock_gettime()で負荷をかけたEC2をNetflix FlameScopeでのぞいてみた

はじめに

ablogさんのこちらの記事=>Netflix のオープンソース可視化ツール FlameScope を使ってみた - ablogの二番煎じですが、FlameScopeを教えてもらったので使ってみました。

FlameScopeとは

Netflixのパフォーマンスエンジニアチームがリリースしたperfで取得した性能情報をブラウザでビジュアルに分析できるツールです。詳しくはNetflixのblogablogの記事を参照してください。

この記事で書いていること

  • MacにFlameScopeをセットアップする時の注意事項
  • AWS EC2のt2.microインスタンス場で、clock_gettime(gettimeofdayを実行した時に叩かれるkernelのAPI)で負荷をかけた時のサンプル

セットアップ手順①(MacにFlameScopeをセットアップする)

基本は、GitHub - Netflix/flamescope: FlameScope is a visualization tool for exploring different time ranges as Flame Graphs.に書いてあります。
基本FrameScopeのgithubに記載されている手順でいけますが、私のMacは前提のツールが何も入っていなかったのでもう一手間かかりました。

(1)前提パッケージのセットアップ

(a)gitコマンドのセットアップ

下のqiita記事を参考にgitコマンドをインストールしてください。

gitはAppleの認証がなくインストールでエラーになる場合がありますが、その場合はエラーになった後に"システム環境設定"->"セキュリティーとプライバシー"からインストールを実行してください。

(b)pipコマンドのセットアップ
  • Xcodeのセットアップ

pipのセットアップの中でccコマンドでビルドしているようなので、 ccコマンドを使えるようにするためにMacの開発環境であるXcodeを先にインストールします。
インストールは、”AppStore”から”Xcode”で検索してインストールします。

  • pipインストール

こちらの記事を参考にpipをインストールします。

手順は、
(i)get-pip.pyダウンロード
https://pip.pypa.io/en/stable/installing/ から、get-pip.pyファイルをダウンロードする。
(ii)pipインストール

python get-pip.py --user
export PATH="$HOME/Library/Python/2.7/bin:$PATH"
echo 'export PATH="$HOME/Library/Python/2.7/bin:$PATH"' >> ~/.bash_profile

(iii)既存パッケージのアップデート

pip freeze --local | grep -v '^\-e' | cut -d = -f 1 | xargs pip install -U --user

(2)FlameScopeのセットアップ

FlameScopeのgithubにあるインストール手順をそのまま実行します。

cd ~/
git clone https://github.com/Netflix/flamescope
cd flamescope
pip install -r requirements.txt

セットアップ手順②(測定対象のLinuxにperfをインストールする)

今回はAWSのEC2(t2.micro)のAmazon Linux 2を利用しています。FlameScopeのgithubの説明通りです。

sudo yum install perf

テスト用にワークロードを準備する

負荷がかかってない状態で測定しても何も面白くないので、何からかの負荷がけをします。負荷は何でもよいのですが、今回は時刻取得を約3355万回(2^25)繰り返す負荷ツールを作りました。clock_gettime()を呼び出す部分をアセンブラで書いていますが、ただの趣味です。こんなキモいことしなくても大丈夫です。

/* work.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>

#define MAX 1LU << 25   /* MAX = 2^25  */

int main() {
    struct timespec tp;
    char *mes;
    int  ret;
    unsigned long loop;
 
    for(loop=0; loop<MAX; loop++){ 
        asm volatile(
            /* call clocl_gettime api */
            "mov %3, %%rsi;"
            "mov %2, %%rdi;"
            "mov %1, %%rax;"
            "syscall;"
            "mov %%eax, %%eax;"
            "mov %%eax, %0;"
            : "=&m"(ret)
            : "g"(SYS_clock_gettime),
              "g"(CLOCK_REALTIME),
              "g"(&tp)
            :
        );
    }
    printf("execute %lu times.\n",loop);
    return ret;
}

このコードをgccでビルドします。(デフォルトではgccが入っていないのでyumでインストールしてビルドします)

sudo yum install gcc
gcc work.c

linuxで測定する

負荷は、作成したツールを4回、10秒のインターバルを置いて実行するようにしました。

sleep 15;for i in `seq 1 4`;do time ./a.out;sleep 10;done

それと同時に、別のターミナルでperfでプロファイルの収集を開始します(秒間49回で、120秒間測定)。

sudo perf record -F 49 -a -g -- sleep 120
  • -F: 49:秒間49回データ収集を行う。(秒間50回が分析にちょうど良い分解能度で、でも50回だと周期性が出てハマってしまう(lock-step)可能性があるから、周期性が出ないよう半端な49回にするということ?)
  • -a: 全てのCPUコアを対象にシステムワイドでプロフィル収集する
  • -g: call-graphを有効化する

perfのデータ収集が完了したらレポートを出力しgzip圧縮する

sudo perf script -f --header > stacks.log
gzip stacks.log

FlameScopeで分析する

  • 収集し、圧縮したstacks.log.gzファイルをMacに転送します。
  • 転送したstacks.log.gzを”flamescope/example”に移動します*1
  • FlameScopeを起動します。
cd ~/flamescope
python run.py
  • ブラウザで、http://127.0.0.1:5000/にアクセスする。
  • stack.log.gzを選択する
  • 右上のRawsから、perfの分解能度に合わせた行数を選択する(49を選択)

すると、こんな感じで見えます。
赤いところがイベントが多数出ている→負荷がかかっている、場所になります。
f:id:nopipi:20180411014615p:plain

FladeScopeでドリルダウン分析してみる

上記の赤い濃淡の図の中から気になる部分があったら、その部分をFlameGraphでドリルダウン分析します。例えば最初の赤い時間帯の開始タイミングと終了タイミングで2回クリックするとその範囲が選択され、下記のFlameGraphで詳細分析することができます。
f:id:nopipi:20180411015940p:plain

t2.microのクロックソース

上記の図を見ると、

  • a.outのmain()から、システムコール呼び出し(entry_SYSCALL_64_after_hwframe)からdo_syscall_64()が呼ばれ、
  • そこから、sys_clock_gettime()が呼ばれて以下実行され、
  • 最終的に、pvclock_clocksource_read()が呼び出されているのがわかります。

なおpvclock_clocksource_read()のコードがどこにあるかカーネルを別途見ると、arch/x86/kernel/pvclock.cにあるようです*2。pvclockという名前からparavirtualのclocksourceであることが推測でき、コードタイトルにも”paravirtual clock -- common code used by kvm/xen”とあるので、t2.microのクロックソースコードはパラバーチャルであることが裏付けられます。( ちなみに、lvm/xenで共通化されているんですね)

処理負荷が高いポイント

clock_gettimeを回して負荷をかけているので当然、処理時間の大半がシステムコール処理にリソースを費やされています。実際FlameGraphを見るとdo_syscall_64が処理時間全体の7割弱を占めているのがわかります。ただ、実際に時刻取得処理を行うdo_clock_gettime()は、do_syscall_64()の半分の処理時間割合であることから、システムコールによるユーザ空間からカーネル空間へのコンテキストスイッチは相当のオーバーヘッドがあることが伺えます。
f:id:nopipi:20180411023148p:plain

(蛇足)/sysからt2.microのクロックソースを確認

蛇足ですが最後に、実機の/sysからカレントのクロックソースを確認します。

kernel的には、xen, tsc, hpet, acpi_pmの4つが選択可能に見えます。

$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource 
xen tsc hpet acpi_pm 
  • (2)カレントのクロックソース

xenですね。

$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource 
xen

subprocessでコマンド実行し、例外処理でOSErrorとコマンドのリターンコードの非ゼロ(エラー)をそれぞれハンドルするサンプル

サンプルコード

私自身の勉強用です。コードは、python2.7ベースです。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import subprocess

def call_subprocess(cmd):
    try:
        r = subprocess.check_output(
            cmd,
            stderr=subprocess.STDOUT
            )
        sys.stdout.write(r+'\n')
    except OSError as e:
        sys.stderr.write('file ='+str(e.filename)+'\n')
        sys.stderr.write('errno='+str(e.errno)+'\n')
        sys.stderr.write('error='+e.strerror+'\n')
        return(1)
    except subprocess.CalledProcessError as e:
        sys.stderr.write('ret='+str(e.returncode)+'\n')
        sys.stderr.write('cmd='+str(e.cmd)+'\n')
        sys.stderr.write('output='+e.output+'\n')
        return(1)
    return(0)

#test1 a command is success
print('<<<<<test1>>>> a command is success.' )
cmd = [ 'echo','hoge hoge' ]
ret = call_subprocess(cmd)
print('function return = '+str(ret) )

#test2 command is failed
print('<<<<<test2>>>> a command is failed.' )
cmd = [ 'cat','hoge hoge' ]
ret = call_subprocess(cmd)
print('function return = '+str(ret) )

#test3 OS error
print('<<<<<test3>>>> OS Error.' )
cmd = [ 'NonexistingFile','hoge hoge' ]
ret = call_subprocess(cmd)
print('function return = '+str(ret) )

プロセスのVSZ,RSSとfree,meminfoの関係を実機で確認する

1.はじめに

1-1.この記事の要旨

psコマンドのVSZ(仮想メモリ)、RSS(物理メモリ)の挙動について質問を受けたので、簡単な検証プログラムを作ってmalloc/freeのメモリ確保/解放や、データの読み込み・書き込みとVSZ/RSSの関係性及び、freeコマンドとmeminfo情報でシステムワイドなメモリの挙動について確認しました。
検証結果から以下の挙動を実機で確認しました。

  • 検証結果
    1. malloc()の時点では、VSZのみ増加し、RSSやfreeコマンドのusedは増加しない
    2. データreadでは、物理メモリの割り当てが発生しないため、RSSとfreeコマンドのusedは増加しない(COW(Copy-On-Write)実装によるもの)
    3. データwriteして初めて、物理メモリの割り当てが発生し、RSSとfreeコマンドのusedが増加する

f:id:nopipi:20171115024528p:plain
psコマンドのVSZ/RSS挙動
f:id:nopipi:20171115024541p:plain
freeコマンドのused, free,availableの挙動
f:id:nopipi:20171115024535p:plain
meminfoの挙動*1

1-2.(予習)メモリに関する指標とlinuxのメモリ挙動について

この記事に出てくる、メモリに関する指標値の説明です。

  • psコマンド
    • VSZ : プロセスが確保している仮想メモリサイズ
    • RSS : プロセスが確保している物理メモリサイズ
  • freeコマンド
    • used : カーネルや各プロセスが利用しているメモリの総量です。*2
    • buffer /cache: キャッシュやバッファとして利用されいるメモリ量です。(この記事では、単にbufferと記載しています)
    • free : 未使用のメモリ総量です。
  • meminfo
    • MemFree : 未使用のメモリ量です。freeコマンドの"free値"の元になっています。
    • AnonymousPage(無名ページ): swapアウト対象となるページ。ざっくりというとプロセスが普通使っているメモリ。
    • FilePage(ファイルページ):メモリ不足時、解放されるページ。ざっくりいうとファイルキャッシュのメモリ。
    • active: AnonymousPage/FilePageのうち、比較的最近アクセス(read or write)されたページ。swapアウトや解放されないページではない。*3
    • inactive:AnonymousPage/FilePageのうち、しばらくアクセスされていないページ

2.検証環境と検証方法

2-1.検証環境

検証環境は以下の通りです。

  • RHEL7.4(3.10.0-693.el7.x86_64)
  • サーバ構成: CPU 1core、メモリ 2GB(VirtualBox利用)

2-2.検証方法

下記動作を行う検証プログラムを作成し実行します。なお検証をわかりやすくするために、動作の間に10秒のsleepをかけています。

  1. malloc()で512MBのメモリを確保する
  2. 確保した512MBのメモリ領域を20秒かけてreadする(1秒間隔で1/20のサイズずつ20回readする)
  3. 同じ領域に、20秒かけてデータを書き込む
  4. もう一度同じ領域を、20秒かけて読み込む
  5. free()で確保したメモリを解放する

プログラムは以下のリンク先にありますので、これをgccで"gcc verifying_memory.c"とかしてビルドします。
Verifying on Memory behavior · GitHub
ちなみにglibcmalloc()は、サイズが128kb(変更可)より大きい場合はmmap()取得になります。*4
ですので512MB確保している今回の検証は、必ずmmap()になります。

2-3.測定方法

(1)psコマンドによるVSZ,RSS情報の取得

簡単ですが、下記のワンライナーコマンドで1秒間隔で取得しました。(検証プログラムのファイル名は、"a.out")

echo -n 'DATE               '; ps -aux|head -n 1;while true;do printf '%s ' "`env LANG=C date '+%X.%N'`";env LANG=C ps -aux|grep -e a.out|grep -v grep;sleep 1;done
(2)freeコマンドとmeminfo情報の取得

こちらの記事のシェルで取得しました。
freeコマンドとmeminfoを取得してCSV形式で保存するシェルスクリプト - のぴぴのメモ

3.結果

3-1.全体の結果

psコマンドのVSZ/RSS、freeコマンド、meminfoの推移を並べると以下のようになります。

f:id:nopipi:20171115024528p:plain
psコマンドのVSZ/RSS挙動
f:id:nopipi:20171115024541p:plain
freeコマンドのused, free,availableの挙動
f:id:nopipi:20171115024535p:plain
meminfoの挙動

3-2.プロセスのVSZ/RSS挙動

f:id:nopipi:20171115024528p:plain
psコマンドのVSZ/RSS挙動

ポイント① malloc()した時の挙動→VSZのみ増加

malloc()で512MBを取得したタイミングで、VSZが増加しているのがわかると思います。
一方RSSは、malloc()しただけでは増えてなく、このタイミングでは物理メモリの割り当てが発生していないことがわかります。(次のfreeコマンドやmeminfoの結果と照らし合わせて見るとよりわかりやすいです。)

ポイント② 1回目のデータread時→RSSは増えない

malloc()した後、そのデータをreadしても、物理メモリの割り当ては発生しません。そのため、RSSも増加しません。
実はプロセスがカーネルからメモリ確保したタイミングでは、確保した仮想メモリのページは、”zero page”という1ページがすべて0データで埋められた特殊なページにマッピングされており、データをreadした時は、その"zero page"の値を参照するためです。*5

ポイント③ データwrite→RSSが増加する

データのwriteが発生した時点で、RSSが増加していることがわかります。つまりデータへのwriteのタイミングで、そのwriteした仮想メモリのページに、晴れて物理メモリが割り当てられたということです。
これはカーネルのCOW(Copy-On-Write)という手法で、mallocなどでメモリを確保したタイミングでは実際には物理メモリの割り当てはせず、必要になったタイミングで初めて物理メモリを割り当ているからです。*6
もう少し詳しく書くと、先ほど説明したzero pageは書き込み禁止設定がされており、そこにデータを書こうとするとページフォルトが発生します。ページフォルトが発生すると、ページフォルトの割り込み処理の中にあるCOWの実装で、zero pageの内容を新しいページにコピーしして、そのページを改めて仮想ページとマッピングします。その時にRSSが+1ページ加算されます。

3-3.システムワイドな挙動(freeコマンド/meminfo)

f:id:nopipi:20171115024541p:plain
freeコマンドのused, free,availableの挙動
f:id:nopipi:20171115024535p:plain
meminfoの挙動

ポイント① malloc()した時の挙動→usedもAnonymousPageも増えない

malloc()した時には、RSSが増えてない、つまり物理メモリへのマッピングがされていないため、freeコマンドやmeminfoでシステムワイドで見た物理メモリの利用状況でも、変化はありません。

ポイント②1回目のデータread時→変化しない。

同じですね。物理メモリの割り当てが発生しないため、freeコマンドやmeminfoも変化はありません。

ポイント③ データwrite→used上昇、AnonymousPage上昇

Writeするタイミングで、物理メモリの割り当てが発生するため、Usedが上昇します。meminfoでさらに詳しく見るとその上昇しているところが、AnonymousPage(無名ページ)であることが確認できます。

*1:kernelは、”MemTotal - MemFree - AnonymousePage(Active/InActive) - FilePage(Active/InActive) - Unevictable+Mlocked”で算出しています。

*2:"total - free - buffers - cache"で計算された値になります。RHEL6まではbuffer/cacheを含んでいましたが、RHEL7からはbuffer/cacheが含まれない値になりました。

*3:/proc/meminfo の Inactive は利用可能なメモリ領域ではない - ablog

*4:glibcmalloc()は、指定するメモリサイズが小さい場合(閾値MMAP_THRESHOLDで設定されており、デフォルトは128kb)は、Java vmのヒープのように、カーネルから取得済みで未使用となったメモリ領域をglibc内で保持して再利用する実装があるため、malloc()により必ずしもVSZが増加するとは限りません。詳しくはmalloc()のmanの注意(NOTES)を参照。

*5:memory management - Linux kernel: Role of zero page allocation at paging_init time - Stack Overflow

*6:コピーオンライト - Wikipedia

Linuxのclock_gettime()でナノ秒の時刻取得をするCのサンプル

1.はじめに

clock_gettime()で時刻を取得し時刻をナノ秒で表示するサンプルです。時刻取得といえばgettimeofday()ですが、POSIXではgettimeofdayは廃止予定で、clock_gettime()の利用を推奨しているので、こちらを利用しています。

2.コードと実行例

(1)コード

#include <stdio.h>
#include <time.h>
int main(){
	struct timespec ts;
	struct tm tm;
	// 時刻の取得
	clock_gettime(CLOCK_REALTIME, &ts);  //時刻の取得
	localtime_r( &ts.tv_sec, &tm);       //取得時刻をローカル時間に変換
	// 出力
	printf("tv_sec=%ld  tv_nsec=%ld\n",ts.tv_sec,ts.tv_nsec);
	printf("%d/%02d/%02d %02d:%02d:%02d.%09ld\n",
		tm.tm_year+1900,
		tm.tm_mon+1,
		tm.tm_mday,
		tm.tm_hour,
		tm.tm_min,
		tm.tm_sec,
		ts.tv_nsec);
	return(0);
}

(2)実行例

$ gcc sample.c
 $./a.out 
tv_sec=1508516823  tv_nsec=736416218
2017/10/21 01:27:03.736416218

RHEL7でのビルド&実行例です。version 2.17より前のgccの場合はリンク時に”-lrt”オプションを付与する必要があります。(多分、RHEL5以前)

3.説明

(1)clock_gettime(CLOCK_REALTIME, &ts)

時刻を取得します。最初の引数は取得する時刻の種類を指定しており、"CLOCK_REALTIME"はシステム全体で一意な精度の高い実時間情報を取得します。取得した情報は、2つ目の引数のtimespec構造体に格納されます。

struct timespec {
	time_t   tv_sec;       ==> 1970年1月1日からの秒数が格納されます。
	long     tv_nsec;      ==> 秒未満の時刻(ナノ秒)で格納されます。
};

(2)localtime_r( &ts.tv_sec, &tm)

取得した時刻のうち、tv_sec(1970年1月1日からの秒数)を年/月/日/時/分/秒に変換し、tm構造体に格納します。
localtime_r()は、localtime()と同じ処理になりますが、localtime()はスレッドセーフでなく、localtime_r()はスレッドセーフな実装になります。スレッドセーフでないlocaltime()はマルチスレッド環境で利用すると想定外の挙動を起こす可能性があるため、今回のサンプルではマルチスレッドでも使えるように、スレッドセーフなlocaltime_r()を利用しています。

(3)printfで出力

localtime_rで変換した時刻情報を、printf()で出力します。秒未満は、timespecのtv_nsecに格納されているので、tv_nsecを使って秒の小数点以下の値を出力しています。


f:id:nopipi:20171028095337p:plain
clock_gettime()で時刻取得して変換出力する概要

最小構成でmanがない場合の対処方法->man-pagesをインストール

結論

タイトル通りですが、備忘録です。
CentOSRHELで最小構成でインストールした場合、最小限のコマンドマニュアルしかなくて、man(2)、man(3)・・・は、入っていないのですね。
結論から言うと、そんな時は"man-pages"(日本語も必要な場合は、man-pages-jaを追加で)のパッケージをインストールするとman見れるようになります。

yum install man-pages man-pages-ja

お世話になったページ

おそらく同じ悩みをした人がいるとググってヒットした、こちらのページを参考にしました。
hyperneetprogrammer.hatenablog.com
記事は苦労が伺える内容になっていますが、結論だけ使わせていただきました。すみません。先人の方の努力に感謝します。
リンク先記事は、CentOS6.4ですが、RHEL7でも問題なくmanが使えるようになりました。

freeコマンドとmeminfoを取得してCSV形式で保存するシェルスクリプト

はじめに

昔作った/proc/meminfoを取得時刻情報取得スクリプトを改造して作った、freeコマンド+/proc/meminfoを取得して、CSV形式で出力するシェルスクリプトです。
nopipi.hatenablog.com

ツール説明

使い方

  1. シェルのファイルを配置して、"chmod +x collect_mem.sh"で実行権限を付与する。
  2. 実行する。(カレントディレクトリに、"meminfo.YYYYMMDDHHMMSS"形式で3秒間隔でデータが出力されます)
  3. 終了したい場合は、”Ctrl+c”で終了する。

出力結果

CSV形式で以下の情報がファイルに出力されます。

  • 取得日と時刻
  • freeコマンド 2行目(Mem:)の各情報
  • freeコマンド 3行目(Swap:)の各情報
  • /proc/meminfo情報
"DATE","TIME","free-mem-total(kb)","free-mem-used(kb)","free-mem-free(kb)","free-mem-share(kb)","free-mem-buff/cache(kb)","available(kb)","free-swap-total(kb)","free-swap-used(kb)","free-swap-free(kb)","MemTotal:(kB)","MemFree:(kB)","MemAvailable:(kB)","Buffers:(kB)","Cached:(kB)","SwapCached:(kB)","Active:(kB)","Inactive:(kB)","Active(anon):(kB)","Inactive(anon):(kB)","Active(file):(kB)","Inactive(file):(kB)","Unevictable:(kB)","Mlocked:(kB)","SwapTotal:(kB)","SwapFree:(kB)","Dirty:(kB)","Writeback:(kB)","AnonPages:(kB)","Mapped:(kB)","Shmem:(kB)","Slab:(kB)","SReclaimable:(kB)","SUnreclaim:(kB)","KernelStack:(kB)","PageTables:(kB)","NFS_Unstable:(kB)","Bounce:(kB)","WritebackTmp:(kB)","CommitLimit:(kB)","Committed_AS:(kB)","VmallocTotal:(kB)","VmallocUsed:(kB)","VmallocChunk:(kB)","HardwareCorrupted:(kB)","AnonHugePages:(kB)","HugePages_Total:()","HugePages_Free:()","HugePages_Rsvd:()","HugePages_Surp:()","Hugepagesize:(kB)","DirectMap4k:(kB)","DirectMap2M:(kB)"
2017/09/27, 03:46:11,1883560,386348,792416,70516,704796,1237452,839676,0,839676,1883560,792680,1237716,5652,618276,0,313016,390848,80848,69604,232168,321244,0,0,839676,839676,0,0,79948,23984,70516,80868,45604,35264,1744,4952,0,0,0,1650384,365624,34359738367,7444,34359728128,0,6144,128,128,0,0,2048,44992,2052096
2017/09/27, 03:46:14,1883560,386144,792616,70516,704800,1237656,839676,0,839676,1883560,792680,1237720,5652,618280,0,313044,390848,80872,69604,232172,321244,0,0,839676,839676,4,0,79956,23984,70516,80868,45604,35264,1808,4924,0,0,0,1650384,365624,34359738367,7444,34359728128,0,6144,128,128,0,0,2048,44992,2052096

*補足
HugePage周りの挙動を確認するための情報収集用にこしらえたものです。

"LB + Web x 2 + RDB"マルチAZ”でWordPressをインストールするCloudFormation

はじめに

前にAWSさんの体験ハンズオンで経験した"LB + Web x 2 + DB x 2"環境構築して、そこに動作確認としてWordPressをインストールして動かすところまでの手順を、CloudFormationを使って自動化して見ました。WordPressのインストールは、CloudFormationのcfn-init ヘルパースクリプトを使っています。

構成概要

f:id:nopipi:20170910213131p:plain

  • 東京リージョンでの利用前提(手抜きです)
  • Webサーバ用のPublicSubnetを2つと、RDB用のPrivateSubnetを2つ作成
  • Subnetは2つのAZ(AvailabilityZone)にサブネットをそれぞれ配備する
  • WebサーバへのWordPressインストールは、cfn-init ヘルパースクリプトを利用。下記作業時実施。
    • 前提パッケージと、WordPressのダウンロードと解凍コピー
    • 定義ファイルの作成
    • サービスの起動

CloudFormationコード

SRPM展開&Gun global解析&squashfs化する自作ツールの説明

1.ツールができること

"srpm2html.py"は、SRPMソースコードを展開、Gnu global処理、その他諸々を自動化する自作ツールです。
具体的には、このツールを使うと以下の一連の作業をサボることができます。

コードはgithub(gist)に登録してあります。

2.インストール方法

2.1 前提環境

Linuxであれば多分動くと思います。(私は、CentOS6,AmazonLinux上で利用しています)

2.2 前提パッケージ

この自作ツールを実行するためには以下のrpmパッケージが必要です。*1*2

下記コマンドでRPMパッケージをインストールします。

sudo yum install rpm-build squashfs-tools gcc make ncurses-devel

2.3 Gnu Globalのインストー

Gnu globalはソースコードをダウンロードしてmakeします。

wget http://tamacom.com/global/global-6.5.6.tar.gz
tar -xxvf global-6.5.6.tar.gz
cd global-6.5.6
./configure
make
sudo make install

2.4 ツールのセットアップ

srpm2html.pyは、githubに登録してあるのでそこからダウンロードします。

cd "セットアップしたいディレクトリパスを指定"
wget https://gist.githubusercontent.com/Noppy/027ea703dd7084be0c3d4d99ce618109/raw/1e3eefeb83c7542d37c8c455701a0f727df128d4/srpm2html.py
chmod +x srpm2html.py 

2.5 ディレクトリの準備

デフォルトでは下記ディレクトリが必要になります。ディレクトリは実行時の引数(後述)または、pythonの先頭のデフォルト設定を書き換えることで変更可能です。

  • 必要なディレクト
    1. /tmp : srpmのダウンロードと追加するfstabの作成に使用。容量は利用しない。
    2. /data/rpmbuild : srpmインストール、コード展開、globl解析をするメイン作業用。
      • 大量のファイルを作成するため、ファイルシステムフォーマット時にするinodeを潤沢に確保したほうが良い
        • フォーマット例:mkfs.ext4 -L rpmbuild -i 2048 /dev/sdb1
      • メモリが潤沢にある場合は、tmpfs(メモリファイルシステム)の利用が良い。
    3. /data/squshfs : 作成したsquashfsファイルの格納先
    4. /data/kernel : Globalが生成したhtmlのsquashfs化したもののマウントポイントを作成するディレクトリ(kernelのsrpmを解析した場合)
    5. /data/tools : 上記と同じ(kernel以外の、srpmを解析した場合)
    6. /data/source:ソースコードsquashfs化したもののマウントポイントを作成するディレクト

2.6 sudo設定

最後のfstab更新とマウント実行時に、sudoコマンドを利用しています。srpm2html.py実行ユーザがsudoでroot昇格できるように設定を事前にして下さい。またパスワード入力が面倒な方は、NOPASSWDもして下さい。

3.使い方

3.1 簡単な使い方

簡単な使い方は以下のとおりです。

./srpm2html.py "SRPMファイルのURL"

例えば、CentOS7.3のカーネルソースを展開したい場合は以下の通りになります。

srpm2html.py http://vault.centos.org/7.3.1611/os/Source/SPackages/kernel-3.10.0-514.el7.src.rpm

3.2 引数の説明

  • 構文
usage: srpm_to_html.py [-h] [-d] [-t TMPDIR] [-r RPMBUILDDIR] [-s SQUASHFSDIR]  
                       [-K HTTP_KERNELSDIR] [-T HTTP_TOOLSDIR] [-S SOURCEDIR]  
                       SRPM_FilePath_or_URL
  • 必須引数
    • SRPM_FilePath_or_URL: srpmのURLまたは、ローカルに格納しているsrpmのファイルパスを指定します。
  • 主なオプション詳細
    • -h, --help : ヘルプの表示
    • -t TMPDIR, --tmpdir TMPDIR : tmpディレクトリの指定
    • -r RPMBUILDDIR, --rpmbuilddir RPMBUILDDIR : srpmの展開先と作業用ディレクトリの指定
    • -s SQUASHFSDIR, --squashfsdir SQUASHFSDIR : squashfsファイルの格納先ディレクトリ指定
    • -K HTTP_KERNELSDIR, --http_kernelsdir HTTP_KERNELSDIR : マウントポイントを作成するディレクトリ指定(kernel)
    • -T HTTP_TOOLSDIR, --http_toolsdir HTTP_TOOLSDIR : マウントポイントを作成するディレクトリ指定(kernel以外)
    • -S SOURCEDIR, --sourcedir SOURCEDIR : マウントポイントを作成するディレクトリ指定(ソースコード)

*1:gcc,make,ncurses-develは、Gnu globalインストール時に必要

*2:pythonも必要ですが、標準で大抵インストールされているので割愛

SRPMインストール先を指定する方法

ソースコードをパッケージ化したSRPM(拡張子がsrc.rpmのファイル)のインストール先は、デフォルトでは"${HOME}/rpmbuild"になります。(CentOS5/RHEL5以前は"/usr/src/redhat")。このデフォルトのインストール先を変更する方法を説明します。

1.普通にSRPMをインストールした場合

SRPMrpmコマンドでインストールします。

$rpm -ivh lsof-4.87-4.el7.src.rpm 

デフォルトでインストールした場合は、ホームディレクトリ配下のrpmbuildディレクトリにインストールされます。

/home/n/rpmbuild/
├── SOURCES
│   ├── lsof_4.87-rh.tar.xz
│   └── upstream2downstream.sh
└── SPECS
    └── lsof.spec

2.インストールディレクトリを指定する方法

2.1 RPMのマクロ定義ファイル("~/.rpmmacros")で指定

ネットを調べるとたいてい出てくる方法はこちらの方法になります。rpmコマンドでのSRPMインストールと、その後のrpmbuildコマンドによるビルド操作で都度、引数でディレクトリを指定する手間を考えると、マクロ定義ファイルで予め定義するほうが現実的な気はします。

(1)マクロ定義ファイルの設定

インストール先のトップディレクトリは、"%_topdir"変数になるので、この変数をマクロ定義ファイルで変更します。

$echo "%_topdir /data/rpmbuild" > ~/.rpmmacros
(2)SRPMのインストール
$rpm -ivh lsof-4.87-4.el7.src.rpm 

2.2 コマンド引数でインストールディレクトリを指定する方法

ホームディレクトリに".rpmmacros"ファイルを作成したくない場合や、一時的にインストールディレクトリを変更したい場合は、"--define="引数でマクロの変数を変更します。

$rpm -ivh --define='%_topdir /data/rpmbuild' lsof-4.87-4.el7.src.rpm

cloudformation VPC(PubSub x 2, PrivateSub x 2) + 1 Instance(t2.micro, AmazonLinux) を作るテンプレート

概要

自分用のメモです。"一つ前の記事"VPCのPublicASubにインスタンスを一つ追加したテンプレートです。

作成されるもの

  • VPC × 1
  • サブネット × 4
    • PubASub CIDR:10.0.1.0/24, AZ: ap-northeast-1a ルートテーブル: インターネットへのルーティングあり
    • PubBSub CIDR:10.0.2.0/24, AZ: ap-northeast-1c ルートテーブル: インターネットへのルーティングあり
    • PrivateASub CIDR:10.0.11.0/24, AZ: ap-northeast-1a
    • PrivateBSub CIDR:10.0.12.0/24, AZ: ap-northeast-1c
  • セキュリティグループ × 2
    • WebSecurityGroup: HTTP(port80),HTTPS(port443)を全て許可
    • SSHSecurityGroup: SSH(port22)を全て許可
  • インスタンス × 1
    • 利用イメージ: AmazonLinux("ami-56d4ad31")
    • インスタンスタイプ : t2.micro
    • サブネット: PubASub(インターネット接続あり)
    • SSHキーペア: "xxxxxxxxxxxxx"となっているので適切に修正
    • 追加EBS:
      • タイプ: デフォルトの「汎用 SSD (gp2)」
      • サイズ:20GiB
      • バイス名:/dev/xvdb

※AZは東京の場合

構成概要

f:id:nopipi:20170212222757p:plain

テンプレート

cloudformation VPC(2つのパブリックsub+2つのプライベートsub)を作るテンプレート

概要

自分用のメモです。下記のようなvpcを作るテンプレートです。

作成されるもの

  • VPC × 1
  • サブネット × 4
    • PubASub CIDR:10.0.1.0/24, AZ: ap-northeast-1a ルートテーブル: インターネットへのルーティングあり
    • PubBSub CIDR:10.0.2.0/24, AZ: ap-northeast-1c ルートテーブル: インターネットへのルーティングあり
    • PrivateASub CIDR:10.0.11.0/24, AZ: ap-northeast-1a
    • PrivateBSub CIDR:10.0.12.0/24, AZ: ap-northeast-1c
  • セキュリティグループ × 2
    • WebSecurityGroup: HTTP(port80),HTTPS(port443)を全て許可
    • SSHSecurityGroup: SSH(port22)を全て許可

※AZは東京の場合

構成概要

f:id:nopipi:20170211175348p:plain

テンプレート