技術的な話題    trashbox / trashbox

DynamoDB 使ったほうがいいんじゃない!?!?という話になったので、調べたメモ。まじでメモ。

+ 金額感
    + ストレージ利用料 + キャパシティユニットと呼ばれる処理単位 + リクエスト数
    + キャパシティユニット
        + 書き込みの制約
            + 1つの書き込みキャパシティユニットで秒間1回書き込める
            + 1つのキャパシティユニットで1KBまで
        + 読み込みの制約
            + 1つの読み込みキャパシティユニットで秒間2回読み込める
            + 1つのキャパシティユニットで4KBまで
        + これを消費する量をスループットと呼んでいる
            + Dynamoを使っている上でスループットが上がらない、はキャパシティユニットを上げればいい
        + 読み書きそれぞれ別の値を設定できる
        + オートスケールも可能。70%をしきい値に増やしたり減らしたりできる
        + この制約があるので一括でドッバー!みたいなことができない
    + キャパシティユニットの課金が厳しいのでがっつりDBストアというよりは、無限にスケールできるKVSのイメージ
+ ストレージ
    + 縦にも横にも自動でめっちゃ伸びる
    + 実態はSSD。ソフトウェア的なものはAWS独自?まあ気にしなくて良い領域
+ インデックスの設定が必要
    + なぜ必要?
        + 無限にスケールが出来る、一方でどうやってデータを検索するの?
        + 高速に検索するために、データに対して、一意に絞りこめるようなキーを設定してあげる必要がある
    + どんなインデックスがある?
        + プライマリ
            + HashKey or HashKey + RangeKey
            + 一意に絞りこめるキーと値の幅のインデックスが設定できる
        + LSI
            + ローカルなセカンダリインデックス
            + HashKey + RangeKey のときだけ使える
            + 別のRangeKeyを増やすイメージ
        + GSI
            + グローバルなセカンダリインデックス
            + プライマリキーはそれとして、別のインデックスを設定できる
        + セカンダリインデックスを設定するときのオプションで金額が変わる
            + 「射影しなかった属性データはインデックスでのクエリ結果に含まれない」っぽい
                + もし射影してない属性データが欲しい場合は改めてテーブル側から取得する必要があり
                + その分余計なユニットを消費してしまうので逆にコストが高くなることも
    + HasKeyは分散させないとスループットが出ない
        + ストレージがいい感じに分散されるため
        + 単に日付をべべっとしたデータを入れると分散されない
+ 整合性
    + 2つある
    + つよい
        + つよい。ちゃんと整合性を見てくれる
        + スループットを倍消費する
    + ふつう
        + ふつう。書き込みは同期じゃなくてちょい遅延がある
        + スループットは通常(1キャパシティユニットで秒間2回読み込める)
+ データバックアップ
    + 勝手に分散保存されるので不要(S3といっしょ)
    + バックアップしようとするとキャパシティユニットがつらい
+ RDS, Dynamo, S3 を比べてみる
    + RDS
        + スケール
            + スケールアップ、縦横分割は自分でやる
        + スピード
            + インデックスが効けば早い
            + データ量が増えるとつらい
                + 日ごとにめちゃめちゃ増えるデータに対しては難しい
            + Auroraで多少改善できるかも?
        + 検索
            + わりかし柔軟に検索できる
        + 一貫性
            + ◎トランザクション
        + その他
            + 要するにMySQLなので理解が容易
    + Dynamo
        + スケール
            + 無限にスケール出来る、縦横分割を考えなくていい
        + スピード
            + 強制的にインデックスが効くので早い
            + DAXというオプションで加速できる
        + 検索
            + インデックスに基づく簡単な検索は出来る
            + テーブルをくっつけるような複雑目なものは無理
                + ElasticSearchと連携して最高の検索ソリューションを提供できる
        + 一貫性
            + トランザクションはない
        + その他
            + キャパシティユニットを超えるとエラーになるのでスパイク対応が難しい?
                + オートスケール+ライブラリがリトライ対応してると最高
    + S3
        + スケール
            + 無限にスケール
        + スピード
            + 他と比べると遅め
            + おそすぎてやばい!というほどではなく、耐えられるくらいの遅さ
        + 検索
            + 検索することはできない、一意に絞りこめるキーを決める必要がある
            + ElasticSearchにまるっと放り込んで検索することもできるっちゃ出来る
        + 一貫性
            + トランザクションはない
        + その他
            + 要するにファイルストレージ
            + 保存コストは一番安いが、取り出しコストは大きめ?
+ 余談
    + GoogleAnalyticsはBigTableがバックエンド

勉強した色々な記事

   技術的な話題    trashbox / trashbox

この記事は Webスクレイピング Advent Calendar 2017 - Adventar の 5 日目の記事です。

お仕事都合による発生するスクレイピングについてと、それつらたん~~~なことになってきたので解消するようにブチアゲした話を書きます。

お仕事都合による発生するスクレイピング

お仕事都合、例えば営業や企画といった人達から「こういうこと出来ないかなあ…?」とかって話が来るとおよそ7,8割の確立でスクレイピングできないっすかというお願いです。それが 1 回だけならいいんですが、異なる人から、異なることを、ちょこちょことお願いが来たりします。
さらに厄介なのが「XX さんに先月お願いしたやつ、今月もよろしく」みたいなやつ。ぼくは XX さんじゃないし、その事があったことを知らないぞ💢💢

そしてその "お願い" の中身としては、例えばこういう感じ。

  • 指定する URL 群から XXX と YYY の情報を集めたい
    • (これがいちばんおおい)
  • 電車案内のサイトから駅名一覧を集めたい
  • 求人系のサイトから職種一覧、募集企業の情報を集めたい
  • IRまとめ系サイトから業種や会社名、連絡先を集めたい
  • 特定のメディアサイトに入っている画像を集めたい
  • 〇〇のメーカーの商品、型番の一覧を集めたい
  • ムフフなコンテンツ集めたい
  • etc...

そしてこういったお願いはかなりの速度感を求められることがあります。

とはいえ、どばーーーーってスクレイピングしたら、対象のサイトに対して、迷惑を掛けることにつながりますし、訴訟リスクも存在します。前提として 1 日目の vaaaaanquish さんが書かれているようなこと( PythonでWebスクレイピングする際の規約回りの読み込み - Stimulator ) を
確認しつつ、良心的な範囲でやらなければいけないので、その点については説明し納得をしてもらっています。

そんなこんなで、これまではまあいっかーという気持ちで、毎回 0 からコードを書いて、ぐるぐるしたりしてやっているのですが、いやいやちょっとそろそろ面倒になってきましたよ、ということでもうちょっとやることにしました。

「コレ使えばおおよそいいよ」セットを作る

毎回コードを書くのはまあいいとして、でも毎回 0 スタートはかなり面倒です。ボイラープレートというか、フレームワークというか、何かそういう「コレ使えばおおよそいいよ」セットを作ります。
このセットがあることによって、コードがざっくりと統一されるので、引き継ぎだったり、並行開発だったり(するか?)、そのあたりの勝手が良くなります(なりました)。

Guzzel 使えばいいじゃん、とかそういう話でもあるんですが、スクレイピングしたときってデータの入出力はもちろん、スクレイピングのやり方によってはキューイングしたり、一時データをもったりするじゃないですか。そういうところ全部扱いたいんですよ。で、後述するんですけど、速度感よくやっていこうとすると、あーだこーだ composer require するってよりは、ファイル 1 個読めばいいじゃん、という環境にしたいのです。

制約とか

お仕事の都合によって、言語の制約は PHP になります。他の言語でもいいのですが、周りの人の取り回しを考えるとこれがベストマッチです。

また composer や PEAR は利用できません。お願いを解決するにあたって速度感よくやるために「コレ使えばおおよそいいよ」セットは、何も考えずにとにかくサクッと使えることが必要です。なんなら PHP の設定も気にせずに使えるように、どこかのサーバにファイルを設置して eval(file_get_contents()) みたいに使えると最高です。

そうして出来たものがこちらです

easy scraping kit
※ CSS セレクタを扱う箇所がざっくり過ぎて正しくないです

コレを使って、例えば「Mapion を利用して、指定する都道府県・市区町村の駅名を集める」ものを書くとこんな用になります。

eval(file_get_contents('https://.../scraping_kit.txt'));

HTMLDoc::$waitMin = 5000;
HTMLDoc::$waitMax = 15000;

$baseUrl = 'https://www.mapion.co.jp';

foreach(Console::getListFile('list.csv') as $zip) {
	Console::out("start {$zip[0]}");

	// search pref
	$topDoc = HTMLDoc::loadURL($baseUrl . '/station/');
	$prefLinks = $topDoc->findCSSPath('.section.type-a a');
	foreach($prefLinks as $prefLink) {
		if ($prefLink->textContent !== $zip[0]) {
			continue;
		}
		Console::out("found {$zip[0]}");
		Console::out("start {$zip[1]}");

		// search city
		$prefDoc = HTMLDoc::loadURL($baseUrl . $prefLink->attributes['href']->value);
		$cityLinks = $prefDoc->findCSSPath('.section.type-a a');
		foreach($cityLinks as $cityLink) {
			if (mb_strpos($cityLink->textContent, $zip[1]) === false) {
				continue;
			}

			Console::out("found {$zip[1]}");

			// search station
			$cityDoc = HTMLDoc::loadURL($baseUrl . $cityLink->attributes['href']->value);
			$h1 = $cityDoc->findCSSPath('h1.type-a-ttl');
			$h1 = explode('の', $h1[0]->textContent);
			$h1 = $h1[0];

			$trainLinks = $cityDoc->findCSSPath('table.list-table tr');
			foreach($trainLinks as $train) {
				$item = [
					$h1,
				];
				$tds = $cityDoc->findCSSPath('td', $train);
				foreach($tds as $td) {
					$item[] = str_replace('[MAP]', '', trim($td->textContent));
				}
				Console::outputResult($item);
			}

			break;
		}

		Console::out("done {$zip[0]}{$zip[1]}");
		break;
	}
}

今ままで 0 から書いて、1,2 時間かかってコードを書いていたのがサクッと終わるようになりました。
また、簡単にではありますが、キューやキャッシュも持てるようになるので、 cron などを使って定期的に回したり、 web サイト A の情報を元に web サイト B の情報を取得するといった複数のステップがあるようなスクレイピングもわりかし簡単にできるようになりました。

ごく簡単なスクレイピングはコードを書かずに済ませたい

「コレ使えばおおよそいいよ」が出来ましたが、そもそもとして、よく発生するお願いは「指定する URL から XXX と YYY の情報を集めたい」です。これってコードを書く必要はほとんどないはずで、入力として URL と CSS セレクタ( もしくは XPath セレクタ)さえあれば、あとはよろしく処理するような仕組みがあれば十分なはずです。

というわけで一旦雑に作って様子を見ています

11 月くらいに作って様子を見ようと思っていたのですが、特に何かあったわけでもないんですが、急にお願いが減っちゃって作らなくていいかなあと思ってきました。とはいえ怠けているとあとが大変になりそうな予感がするので、近いうちにやりたい気持ち。

おわりに

「コレ使えばおおよそいいよ」を作ってお仕事都合のスクレイピングを簡単にしたり、別の開発者が入るときも問題の起きにくい状況になりました。
しかし、もはやこの件についてコードを書くことをなるべくやめたいので、そういう「スクレイピングいい感じにできるツール」みたいなのあったら教えてください。

(Google スプレッドシートで出来ることは知っていますが、時間系のあたりをもうちょっと自由度が効く感じで…)


6 日目は _kjou さんで「何か書きたい」です。こちらもお楽しみに!

Webスクレイピング Advent Calendar 2017 - Adventar

   技術的な話題    trashbox / trashbox

いや Ruby でええやん、 Python でええやん、みたいな話だとは思うのだが、やっぱり PHP でやりたいよね、という一定の需要がギョームで発生してしまったので、 Ruby のコードを見ながら PHP に書き換えた。 Packagist にも登録してあるので composer からどうぞ。

sters/extract-content - Packagist

$url = 'http://labs.cybozu.co.jp/blog/nakatani/2007/09/web_1.html';
$extractor = new \ExtractContent\ExtractContent(file_get_contents($url));
$result = $extractor->analyse();
file_put_contents(__DIR__ . '/result', $result);

// // 抽出結果
// Webページの自動カテゴライズ の続き。
// 前回書いたとおり、パストラックで行っている Web ページのカテゴライズでは、Web ページの本文抽出がひとつの鍵になっています。今回はその本文抽出モジュールを公開しつつ、使っている技法をざっくり解説などしてみます。
// 本モジュールの利用は至極簡単。require して analyse メソッドに解析したい html を与えるだけ。文字コードは UTF-8 です。
// ...

ExtractContent は Cybozu の Nakatani Shuyo さんが2009年に作成したもので、正規表現を主として Web 記事上の本文に関するちょっとの知見が加わることで成り立っている。以下の記事へ。

Webページの本文抽出 (nakatani @ cybozu labs)

今回、移す際に参考にしたのは、もともとのものではなく、Ruby 1.9 に対応したソース。というのもこっちの記事を見つけたのは後で、 Github 上で ExtractContent を先に見つけていたので、まあいっか、と。

mono0x/extractcontent: ExtractContent for Ruby 1.9+

Ruby はチョットヨメルので inject とかわからないメソッドだけドキュメント見つつ脳内補完して、同じような感じの処理になるよう PHP へ書き直した。工夫したところはとくになく、クラスで扱うようにした、チョットテスト書いたくらいで、ほぼそのまま移してきた。Wikipedia と Medium、はてなブログあたりで試してみたところで、おおよそうまくいっているように見えたので、たぶん大丈夫。

ただ、試していて、本文っぽいと判断されるのが 2 つ以上あるような 1 記事ページ(例えば記事中に section タグがあってーとか、そいういう)ではうまく取り出せず、スコアが高くなったほうのみ抽出されてしまう。本文しきい値のようなものを設けてそれを超えていたら、結合して出す、とかしないといけないなあと思う。とはいえ、正確に本文が欲しいのか、その記事中の重要な部分にフォーカスするのか、などなど要件にもよるので、とりあえずはいいんじゃないかなの気持ち。

ちなみに PHP による実装もいたのだが、おそらく上記のようなところで、元々のものには無いオプションが増えていたりでチョットわからなかったので、一から書いた次第。

aoiaoi/ExtractContent: extract content from HTML

なお記事解析、本文抽出について Packagist を調べると他にも 3 つのライブラリが出てくる。ざっくり紹介するとこんな感じ。

今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい。

   技術的な話題    trashbox / trashbox

これこれ、このライブラリ。

fzaninotto/Faker: Faker is a PHP library that generates fake data for you

ざっくり翻訳しつつ、日本語データでの使い方へ。

faker とは

faker は PHP のライブラリで、偽のデータを提供する。データベースの初期化や XML ドキュメントの生成、ストレステスト、本番データの匿名化、などに活用できる。faker は Perl の Data::Faker と Ruby の Faker に影響を受けている。

faker の使い方

インストール方法

Pakagist にて提供されているので composer を使ってインストールできる。

composer require fzaninotto/faker

基本的な使い方

$faker = \Faker\Factory::create();
// faker オブジェクトの生成

echo $faker->name;
// 名前。例えば 'Lucy Cechtelar'

echo $faker->address;
// 住所。例えば
// "426 Jordy Lodge
// Cartwrightshire, SC 88120-6700"

echo $faker->text;
// 文章…というよりは Lorem 。例えば。
// Dolores sit sint laboriosam dolorem culpa et autem. Beatae nam sunt fugit
// et sit et mollitia sed.
// Fuga deserunt tempora facere magni omnis. Omnis quia temporibus laudantium
// sit minima sint.

faker には嬉しいことに、日本語を含む色々な言語のダミーデータをサポートしている。ここでは日本語での扱いを試してみる。

// 引数に言語を指定すると利用してくれる
$faker = \Faker\Factory::create('ja_JP');

echo $faker->name;
// 例えば '大垣 直子'

echo $faker->address;
// 例えば '2636573  滋賀県浜田市西区笹田町渡辺1-1-10'

echo $faker->realText;
// text は Lorem なので realText がある
// 例えば 'オン燈と、もうすっかりがきの音にすきっと思いなやさで伝つたわ」「なんだ」カムパネルラはこち見ていたの...'

どういうプロパティが用意されているか

英語プロパティは README に全てあって、n - m の範囲で乱数とか、「Dr.」などの敬称を使えるとか、日時とか、まあなんか色々とあるのでそっちを見ると良さそう。

Faker/readme.md at master · fzaninotto/Faker

日本語のだけ、ソースとにらめっこしないと分からないところがあったので、取り上げる。(網羅できてないかも)

country 国名
prefecture 都道府県
city
ward
streetAddress 町以下
postcode 郵便番号
secondaryAddress マンション名
company 会社名
userName ユーザ名
domainName ドメイン名
email メールアドレス
name 名前(姓 + 名)
lastName
firstName
firstNameMale 名(男性)
firstNameFeMale 名(女性)
kanaName 名前カナ
lastKanaName 姓カナ
firstKanaName 名カナ
firstKanaNameMale 名カナ(男性)
firstKanaNameFemale 名カナ(女性)
phoneNumber 電話番号
realText 日本語文章

ORMとの連携

faker は CakePHP や Laravel の ORM と連携することができる(!?)
試してみたところ、連携というよりは ORM を通して、実際に DB に値を格納してくれるっぽい。

例えば CakePHP3 で使うならこんなふうに。

$faker = \Faker\Factory::create('ja_JP');
$populator = new \Faker\ORM\CakePHP\Populator($faker);
$populator->addEntity('Users', 5, [
    'name'       => function() use ($faker) { return $faker->name; },
    'prefecture' => function() use ($faker) { return $faker->prefecture; },
    'created'    => null,
    'modified'   => null,
]);
$inserted = $populator->execute();

$Users = \Cake\ORM\TableRegistry::get('Users');
debug($Users->find('all')->toList());
// 例としてこのような出力になる
// [
//     (int) 0 => object(App\Model\Entity\User) {
//         'id' => (int) 1,
//         'name' => '津田 裕樹',
//         'prefecture' => '栃木県',
//         ....
//         'created' => object(Cake\I18n\FrozenTime) {
//             'time' => '2017-11-20T13:47:32+09:00',
//             'timezone' => 'Asia/Tokyo',
//             'fixedNowTime' => false
//         },
//         'modified' => object(Cake\I18n\FrozenTime) {
//             'time' => '2017-11-20T13:47:32+09:00',
//             'timezone' => 'Asia/Tokyo',
//             'fixedNowTime' => false
//         },
//     },
//     ...
// ] 

特に指定をしなかったカラムについては DB の形を見て、結構いい感じに適当に faker な値で埋めてくれる。指定はクロージャで、 faker を使ってランダムなデータを作ることも出来るし、固定値にすることもできる。また、例としては 1 エンティティしか作らなかったが、addEntity を複数呼んで、 まとめて execute することで複数のエンティティも作ることができる。

シード値を設定する

テストで使うときやっぱりランダムだとつらいよね、というときのためにシード値を設定することができる。

$faker = \Faker\Factory::create();
$faker->seed(1234);

echo $faker->name;
// 何度コードを流しても以下の順番で流れる
// 'Miss Lorna Dibbert'
// 'Litzy Emard'
// 'Odessa Collins'
// ...

ランダムに提供される部分はそのとおりなのだが、日付については引数を指定しないと now() な値が使われるのでシードは同じでも出力される値が変わってしまうので、なんでもいいが毎回同じ値を入れる必要がある。

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->dateTime();
// ランダムになる

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->dateTime('2017/01/01 12:00:00');
// 固定パターンになる

faker には乱数を提供する関数もあり、シード値を設定することで、こういったものたちも固定パターン化される。便利ちゃん。

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->numberBetween(0,100);
// 固定パターンになる
// 81, 50, 62, 18, 56, ...

faker の内部、プロバイダの話

何の気無しに \Faker\Factory::create() を呼び出していたが、実は内部でこんなような処理をしている。

$faker = new \Faker\Generator();
$faker->addProvider(new \Faker\Provider\en_US\Person($faker));
$faker->addProvider(new \Faker\Provider\en_US\Address($faker));
$faker->addProvider(new \Faker\Provider\en_US\PhoneNumber($faker));
$faker->addProvider(new \Faker\Provider\en_US\Company($faker));
$faker->addProvider(new \Faker\Provider\Lorem($faker));
$faker->addProvider(new \Faker\Provider\Internet($faker));

※実際のコードはこのあたり。
Faker/Factory.php at master · fzaninotto/Faker

プロバイダの土台として \Faker\Provider\Base がいるので、これを継承した適当なクラスを作り、 public なメソッドを作って return すれば良い。そのプロバイダクラスを new して addProvider すれば使えるようになる。

class Book extends \Faker\Provider\Base
{
  public function title($nbWords = 5)
  {
    $sentence = $this->generator->sentence($nbWords);
    return substr($sentence, 0, strlen($sentence) - 1);
  }

  public function ISBN()
  {
    return $this->generator->ean13();
  }
}


$faker->addProvider(new Book($faker));

echo $faker->ISBN;

faker を使ってみて

説明にもあるが、テストコードにおいて、いや、そこはなんでも良いんだけど、それっぽい住所や名前をよろしく入力して欲しい、みたいな時。あるいは、DBの初期化時に、多いに力を発揮しそうな感触。

テストコードとして利用する場合、値が毎回ランダムになって使い所がイマイチわかりにくい気もちょっとするが、それで詰まるときってそんなに無いのでは…。値が被って~~~うあああ!!!!みたいなことはあると思う。

話はそれるが最近見かけた property-based testing という方法にはめちゃんこ合っていると思う。

Property-Based Testing for Godly Tests

そもそも PHPUnit なら Data Provider の仕組みがあるし、あるいは CakePHP なら Fixture が、 Laravel には Factory の仕組みがそれぞれにある(きっと他のフレームワークにもあるんじゃないかな、調べてない。。)ので、そういうこともやりやすいと思う。
ちなみに Laravel には faker が既に含まれていて、 Factory の仕組みを使うとガンガンにテストデータを作ってくれる。スゴイ。

Database Testing - Laravel - The PHP Framework For Web Artisans

ORMとの連携はできるとはいえ、既に書いたとおり、各フレームワークがそういう仕組みを提供しているので、それとバッティングするなあという気持ちも。その中で使っていく、みたいなイメージなのかな。。ちょっとわからないや。。

   技術的な話題    trashbox / trashbox

CakePHP3 ではデフォルトでは論理削除の機能は実装されていない。調べると1番上にいい感じに利用できるプラグインがあるので、これを利用するとよい。

PGBI/cakephp3-soft-delete

とりあえず PGBI/cakephp3-soft-delete を使ってみる

composer でパッケージを追加して。

$ composer require pgbi/cakephp3-soft-delete "~1.0"

プラグインの読み込みを追加して。

// config/bootstrap.php
Plugin::load('SoftDelete');

テーブルで SoftDeleteTrait を読み込んで。

// src/Model/Table/UserTable.php

...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;
    ...

該当のテーブルに deleted というカラムを追加して。

// config/Migrations/20171114120000_CreateUsers.php
...

$table->addColumn('deleted', 'datetime', [
    'default' => null,
    'null' => true,
]);

...

Enjoy!

// コンソール機能を使って動作確認をしてみる
//==============================
$ bin/cake console


// Table の読み込み
//==============================
>>> use Cake\ORM\TableRegistry;
>>> $Users = TableRegistry::get('Users');
=> App\Model\Table\UsersTable {#214
     +"registryAlias": "Users",
     +"table": "users",
     +"alias": "Users",
     +"entityClass": "App\Model\Entity\User",
     +"associations": [],
     +"behaviors": [
       "Timestamp",
     ],
     +"defaultConnection": "default",
     +"connectionName": "default",
   }


// Entity の作成
//==============================
>>> $entity = $Users->newEntity();
=> App\Model\Entity\User {#216
     +"[new]": true,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データの保存
//==============================
>>> $Users->save($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#218
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データ保存の確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#295
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#286
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#285
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// SoftDeleteTrait を読み込むと delete の挙動が SoftDelete に変わる
//==============================
>>> $Users->delete($entity);
=> true


// SoftDelete されたデータが検索できないことを確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'


// 'withDeleted' をつけることによって SoftDelete されたデータを検索できる
//==============================
>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> App\Model\Entity\User {#521
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#512
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#511
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": Cake\I18n\FrozenTime {#522
       +"time": "2017-11-14T12:42:48+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore によって SoftDelete されたデータを元に戻せる
//==============================
>>> $Users->restore($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#229
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore の結果について確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#548
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#539
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#538
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// hardDelete をすることによって、復元できない削除をする
//==============================
>>> $Users->hardDelete($entity);
=> true


// hardDelete の結果について確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'

>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> null

かゆいところに手を伸ばす

デフォルトでは deleted というカラムだが、テーブルにプロパティを追加することで違うカラム名にもできる。

// UserTable.php
...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;

    protected $softDeleteField = 'kokoga_deleted_no_field_desu';

    ..

とはいえ CakePHP3 におけるデフォルトのタイムスタンプのフィールドは created と modified という名前になっているので、そこは deleted という名前で合わせにいくのがわかりやすさも高くて良いと思う。

   技術的な話題    trashbox / trashbox

管理シート、複数の人が編集して担当者設定していく〜みたいな使い方をしたいんだけど!!!って言われたので…。

そもそもとして、スプレッドシートには変更したよ!の通知メールを送ることが出来る。

しかしこの機能はドバーっと送るためのもので、例えばこう、担当者が誰になって、このセルが入力されたときに、誰と誰に送る、といった複雑な処理が必要な場合には使えない。そういったときに Google App Script の出番だ。

Google App Script

Apps Script | Google Developers

GAS は端的に言えば現代におけるサーバレス、 AWS Lamda のようなもので、プログラムを Google ドライブ上に置いて、さまざまなトリガーで起動出来る。cron のように定期的に実行したり、 Google Docs と連携して操作が行われたときに実行したり、はたまた Web サイトとして公開することだって出来る。

今回はその中でも、スプレッドシート内のセル値が変更されたとき、に実行する。

というのが無料で出来るんだからグーグルってばすごい。

スプレッドシート x GAS

手順としてはこう。スプレッドシートからスクリプトエディタを開くだけ。

今回やりたい、変更されたとき、という設定はこのように行う。

GAS から通知を送る

メールなら MailApp というクラスが用意されていて、それを呼ぶだけでメール送信ができる。あるいは Slack などの Webhook な振る舞いを実行するなら UrlFetchApp というクラスを使う。どちらにしても用意されているので簡単だ。

Class MailApp | Apps Script | Google Developers

Class UrlFetchApp | Apps Script | Google Developers

とりあえずメールについてのソースとしてはこんな感じ。

function myFunction() {
  // スプレッドシートを取得
  var activeSheet = SpreadsheetApp.getActiveSheet();
  
  // セル範囲を取得
  var activeRange = activeSheet.getActiveRange();
  
  // メール送信
  var text = '';
  text += activeSheet.getName() + ' の ' + activeRange.getA1Notation() + ' が 変更されました\n';
  text += '新しい値: ' + activeRange.getValue();
  MailApp.sendEmail('....@.....', 'メール通知のテスト', text)  
}

これで設定したセルが変更されたとき、設定された方法で通知を送ることができる。 GAS 便利ちゃん。

便利ちゃんなのだが、メールを送るにあたって注意点がある。自分の持っている、認証されているメールアドレスからのみで送信できる、という話で、適当なメールアドレスからは送れないよというもの。通知としてのメール送信なら Slack 等のチャットの API を実行したら良いんじゃないかなあ。

   技術的な話題    trashbox / trashbox

ギョームで使ってるあるシステムのリポジトリが大きすぎて、クローンめっちゃつらい、というかできないんだけど!となったので調べた。

git リポジトリの肥大化は、長年積み重なったログ、うっかりコミットされた大きなファイル、バイナリファイルの積み重ねが主な原因っぽい。
(バイナリファイルは差分でログにしていくのではなく、フルでログになる)

なので、大きく 2 つの方向があると思う。
1 つめはリポジトリ自体のログをねじ曲げる方向。ログが原因なので、そのログから原因要素を取り除けばいいじゃん!という理解。
もう 1 つはクローンするときに工夫する方向。ログが大きすぎるのでログを最小限にしていく。

方法 1 : ログを書き換える

方法 1-a : リポジトリを作り直す

リポジトリのログをねじ曲げるにあたって、手っ取り早い改善策はリポジトリを作り直すこと。最新の状態を手元に持っておき、リポジトリを捨て、最新の状態でファーストコミットをする。もろもろの再設定の手間をかけられる、ログや Github などのリポジトリに紐づく情報はポイしてもよい、そんな決断ができるなら一番簡単で効果も高い方法です。

ただ、これからクローンするのはを楽にしたいんだけど!という用途にはちょっと違うかも。

方法1-b : filter-branch サブコマンドを使って不要なファイルを歴史から隠蔽する

filter-branchを使うと、ログを遡ってコマンドを適用していけます。これを使ってログに潜む悪のファイルを精査します。ただあんまりつかったことないのでわからんね、かなしみ。

Git - 歴史の書き換え

全コミットからのファイルの削除

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

こういう使い方で、まあまあ、簡単に捨てられそうですね。

試しに適当なリポジトリで実行してみたら、キレイサッパリとファイルが消え去りました。ちなみにコミットハッシュも変わるようです(そりゃそうだ)

$ git log --oneline --graph
* 94a3acd (HEAD -> master) ...
* c5615d4 ...
* 1b58a6d ...
* 958f4b0 ...
* 6aeccd4 ...
* 2d39eca ...
* b22e882 ...
* 554d930 ...
* d620f96 ...
* d0a5a86 ...

$ git filter-branch -f --tree-filter "rm -f package.json"
Rewrite 94a3acd99880461b586931f4d4a283494d3f9fcc (8/10) (3 seconds passed, remaining 0 predicted)
Ref 'refs/heads/master' was rewritten

$ git log --oneline --graph
* ccd7b67 (HEAD -> master) ...
* 5159356 ...
* 5551da3 ...
* 862c676 ...
* c1f6cb5 ...
* 3f1eb70 ...
* 36bc53c ...
* d7c2fa6 ...
* d620f96 ...
* d0a5a86 ...

ということで、方法1はどちらもログをぶっ壊していくので、コミットハッシュが変わります。自分も含め、既にクローンなどによってリポジトリを追跡してた人は、整合性が取れなくなるので、再度ローカルのリポジトリ状況を刷新する必要がありそうです。 fetch しよう!とかそういう話じゃなくて、過去のログを持ち込まないとか、そういうところ。

方法 2 : クローンするときに工夫する

方法 2-a : Shallow Clone

Shallow Clone の方法の1つとして depth というプロパティが設定でき、ログをどこまで遡ってクローンするかを決められます。


$ git clone --depth ...

これをするとログの遡りが少なくなるので、その分早くクローンをすることができます。かなり大きくなったリポジトリで試したらこれくらいの違いでした。 およそ 1 分かかっていたものが 30 秒程度にまとまって非常にすばらしい。

$ time git clone ...
Initialized empty Git repository in ...
remote: Counting objects: 137514, done.
remote: Compressing objects: 100% (31116/31116), done.
remote: Total 137514 (delta 108680), reused 129419 (delta 102311)
Receiving objects: 100% (137514/137514), 215.85 MiB | 8.55 MiB/s, done.
Resolving deltas: 100% (108680/108680), done.

real    1m1.198s
user    0m19.858s
sys     0m6.843s


$ time git clone --depth 1 ...
Initialized empty Git repository in ...
remote: Counting objects: 10862, done.
remote: Compressing objects: 100% (6934/6934), done.
remote: Total 10862 (delta 4852), reused 8236 (delta 3409)
Receiving objects: 100% (10862/10862), 109.83 MiB | 7.15 MiB/s, done.
Resolving deltas: 100% (4852/4852), done.

real    0m27.406s
user    0m6.650s
sys     0m2.897s

足りないログについては git fetch --depth で値を増やすと補完できるっぽい。

$ git log --oneline | wc -l
1

$ git fetch --depth 10000
remote: Counting objects: 126652, done.
remote: Compressing objects: 100% (22233/22233), done.
remote: Total 126652 (delta 103609), reused 123617 (delta 100851)
Receiving objects: 100% (126652/126652), 106.69 MiB | 5.92 MiB/s, done.
Resolving deltas: 100% (103609/103609), completed with 1886 local objects.

$ git log --oneline | wc -l
13101

Shallow Clone したリポジトリは、そういうリポジトリになるようで、深さを再設定しないと永遠とログが出てこないそうです。ちょっと昔までは push と pull もままならなかったようですが、今は改善されたようです。

いつのまにかGitのshallow cloneが”Push”も"Pull"もできるように超進化していたよ! すごーい! - ブログなんだよもん


とりあえず手元でこのブランチの様子を検証したいんやけど!みたいなものなんかは --depth 1 を積極的に使っていくとよさげたん CI ツールの類もクローンするときは全部これでええやんけ。


そのほか勉強になった記事

[Git] git repository size を削減する | deadwood

巨大なリポジトリ を Git で上手く扱う方法 | Atlassian Blogs

   技術的な話題    trashbox / trashbox

某所から送信されるメールが GMail で弾かれてしまい、まったく送らていなかったのでその対応メモ…、メモというほどじゃない。エラー文言で調べたら一発だ。

が、メモってたエラー文言をなくしてしまったのと、サーバにもログがない(なんでやねん)ので実際にでてた文言はわからないけど、多分一緒やろ(震え)

Mail delivery to Gmail account fails: certificate verification failed for *.l.google.com – Plesk Help Center

ca-bundle.key を更新、設定したら良いっていってる。

// OpenSSL, ca-bundle.crt を更新する
$ sudo yum update openssl
..


// 設定値の追加
$ vi /etc/postfix/main.cf

smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt


// postfix の再読込
$ sudo service postfix reload

いじょ。

   技術的な話題    trashbox / trashbox

見慣れないライブラリを使ってるプロジェクトがあったので調査メモ。

php-amqplibとは?

php-amqplib/php-amqplib: AMQP library for PHP

RabbitMQ とよばれるメッセージングキューのためのクライアントライブラリ for PHP 。厳密には RabiitMQ という話ではなく、そこで利用されるAMQP と呼ばれるプロトコルに準拠したものを取り扱う。メッセージのやり取りについてその専用のプロトコルでやりとりしている。
利用するには mbstring と bcmath のエクステンションが必須になる。

php-amqplib は packagist で管理されているので composer からインストールすることになる。

RabbitMQ とは?

RabbitMQ - Messaging that just works

うさぎがかわいい。

AMQP というメッセージプロトコルを語れる、メッセージングキュー用のミドルウェア…、という分類でいいのかな。

Javascript の Promise みたいなイメージでいると多分認識が合う(合わない)
メッセージを送る人と受け取る人、そして運び人のウサギがいる。ウサギが様々なメッセージのやり取りや順序管理をしてくれるので、送る人は誰に届くかわからないけどこの処理できる人やって、受け取る方は誰から来たかわからないけど処理して結果をしまっておく、なんてことが出来る。
(=マイクロサービスなアーキテクチャ)

ちなみに Erlang で書かれている。
ちなみに今年で 10 年経つんだとかでトップページがお祝いムード。

 

手軽に試すなら公式イメージがあるので docker で。

library/rabbitmq - Docker Hub

 

そうでなければ公式サイトを参考にインストールしていくとよい。

RabbitMQ - Downloading and Installing RabbitMQ

CentOS の項目をみるとこんなことが書かれている。

Overview
rabbitmq-server is included in Fedora. However, the versions included are often quite old. You will probably get better results installing the .rpm from PackageCloud or Bintray. Check the Fedora package details for which version of the server is available for which versions of the distribution.

Fedora には rabbitmq-server な名前で既に登録されているものがあるが、古い可能性ので Package Cloud の RPM を使うのがオススメする、って書いてあった。

rabbitmq/rabbitmq-server - Packages - packagecloud.io | packagecloud

 

ちなみに、ちょっと昔の記事なので、バージョンが古くて今と異なる部分があるかもだが、実際に業務運用に向けていくようなところも込で、がっつりとした情報があったので、こちらも合わせて読むと実践向けっぽい。

はじめての RabbitMQ|サイバーエージェント 公式エンジニアブログ

PHPから使ってみる

例によってお手軽に試してみる程度。 docker で準備を進めていく。

// docker でキューサーバを立ち上げてみる
// 5672 ポートをバインドするのを忘れずに
$ sudo docker run -d -p 5672:5672  --name rabbit-server rabbitmq:3
...

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                                                   NAMES
92547dbde579        rabbitmq:3          "docker-entrypoint.s   5 seconds ago       Up 4 seconds        4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, 25672/tcp   rabbit-server


// モジュールが必要なので入れる(mbstringはもともとあった)
$ sudo yum install php-bcmath --enablerepo=remi-php70

// モジュールを移動しないとダメだった(パス設定してないだけだと思う)
$ sudo mv /etc/php.d/20-bcmath.ini /etc/opt/remi/php70/php.d/
$ sudo mv /usr/lib64/php/modules/bcmath.so /opt/remi/php70/root/usr/lib64/php/modules/

$ php -m | grep bcmath
bcmath


// アプリを構築する
$ mkdir php-amqplib-test
$ cd php-amqplib-test

$ composer init
...

$ composer require php-amqplib/php-amqplib:2.7.*
...

ソースを書く。

<?php
// send.php

require('vendor/autoload.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

// デフォルトID/PW = guest/guest
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');

// 送信するチャンネルを設定
$channel = $connection->channel();

// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L597
// string queue = ''
// bool passive = false
// bool durable = false
// bool exclusive = false
// bool auto_delete = true
// bool nowait = false
$channel->queue_declare('hello', false, false, false, false);


// メッセージ作成
$msg = new AMQPMessage('Hello World!');

// メッセージ送信
// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L1086
// string msg
// string exchange = ''
// string routing_key = ''
$channel->basic_publish($msg, '', 'hello');
<?php
// receive.php

require('vendor/autoload.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;

// デフォルトID/PW = guest/guest
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');

// チャンネルを設定
$channel = $connection->channel();

// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L597
// string queue = ''
// bool passive = false
// bool durable = false
// bool exclusive = false
// bool auto_delete = true
// bool nowait = false
$channel->queue_declare('hello', false, false, false, false);


// コールバック関数の用意
$callback = function ($msg) {
        echo $msg->body . "\n";
};

// メッセージ受信
// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L901
// string queue = ''
// string consumer_tag = ''
// bool no_local = false
// bool no_ack = false
// bool exclusive = false
// bool nowait = false
// function callback = null
$channel->basic_consume('hello', '', false, true, false, false, $callback);

// メッセージ受信待ちループ
while(count($channel->callbacks)) {
    $channel->wait();
}

なお引数に関するコメントはデフォルトの値。これが何を意味しているかはまたちょっと分からない…。ドキュメントを読むしか…。

ちなみに PHP の API ドキュメントは見当たらない。 Github のソースを読むか、インタフェースはだいたい同じなので JavaDoc のものを読むかするとよい。

 

ターミナルを2つ開いて実行してみる。

// 一方のターミナル
$ php send.php
$ php send.php
$ php send.php
$ php send.php

// もう一方のターミナル
$ php receive.php
Hello World!
Hello World!
Hello World!
Hello World!

ただし、これには問題があって、このように単純に送るだけだとキューが RabbitMQ を再起動したりマシンが落ちたりすると、中身がきれいさっぱり消える。

$ php send.php
$ sudo docker restart rabbit-server
$ php receive.php

// でてこない!

公式ドキュメントだとこのページの Message durability の項目に書いてあるが、送信時に引数を設定しないといけない。

RabbitMQ - RabbitMQ tutorial - Work Queues

<?php
require('vendor/autoload.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

// デフォルトID/PW = guest/guest
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');

// 送信するチャンネルを設定
$channel = $connection->channel();

// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L597
// string queue = ''
// bool passive = false
// bool durable = false
// bool exclusive = false
// bool auto_delete = true
// bool nowait = false
$channel->queue_declare('hello', false, true, false, false);


// メッセージ作成
// デリバリモードの追加
$msg = new AMQPMessage('Hello World!', ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);

// メッセージ送信
// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L1086
// string msg
// string exchange = ''
// string routing_key = ''
$channel->basic_publish($msg, '', 'hello');
// 一旦再起動しておく
$ sudo docker restart rabbit-server

// 送信, 再起動, 受信
$ php send.php
$ sudo docker restart rabbit-server
$ php receive.php
Hello World!

なるほどね~。

ちなみにの話

ちなみに Laravel のキュードライバとして RabbitMQ を使うライブラリも既にあり、これはもう入れるだけでいい。先人の知恵すごい。

vyuldashev/laravel-queue-rabbitmq: RabbitMQ driver for Laravel Queue

書いてある通りです、以上の説明は出来なくて。要するに QUEUE_DRIVER を変更し、 RabbitMQ 用の設定を幾つか書けば勝手にやってくれるのだそうだ。

ちなみに上記の durable 、メッセージの永続化はデフォルトで有効になっているようだ。

 

ちなみにちなみに、メッセージキューの実装を標準化しよう!という話があるっぽい。知らなかった。動きとしては将来的に PSR になったらいいな、くらいの強制していないけどぼちぼち運動してます!なもの。

queue-interop/queue-interop: Promoting the interoperability of MQs objects. Based on Java JMS

書き方、インタフェースが大きく分かれそうだし、複数対応を謳うようなライブラリもあるし、標準化されるといろいろ嬉しい予感がする。

   技術的な話題    trashbox / trashbox

ISUCON 5, ISUCON 6, と引き続き ISUCON 7 に参加しました。

ISUCON についてはこのあたりを見ると良いです。
ISUCON7 まとめ : ISUCON公式Blog

今回もぼっちーむを回避して、会社の人と組んでいきました。前回の ISUCON 6 と同じメンバーです。

過去の参戦レポはこのあたり参照するとよいです。

今回は、ぼくの参戦時間が実質 3 時間程度、少ない時間しか当てられませんでした。
(ぼくからみた義親、つまりは妻の両親が家庭訪問という重大なイベントが発生して、椅子コンよりも優先度高めだった)

当日までにやっておいたこと

メンバーで集まって、何度か方針や準備するものについて話あっていました。準備するのは当日にあたふたしないように、ある程度は機械的に出来るように、というところからです。
具体的にはこんなものが挙がりました。

  • さくらクラウドのアカウント準備
    • クーポン投入
    • お金をケチらない気持ち。必要なら大規模マシンで殴って戻すことも視野に。
  • 個々人の環境
    • ISUCONイメージがさくらクラウドから提供される?情報まち。
    • されないならubuntu使って空っぽマシン用意
      • 中身をがばっともってくる
  • 簡単に色々なログ仕込める君
    • スクリプトとかansible的なやつでやりたいよね
    • mysql-slowとかアクセスログとか。
    • CPUとかメモリ、ネットワークの様子もみたいよね
    • プロファイリングツールの準備
      • alp
      • new relicをいれたい
        • 準備だけしておきたい
        • PHPの計測もできるが細かに計測する記述をしないといけない
      • Kibana
        • mysql も
        • td-agent でいろいろ飛ばせば全部見れる
  • ssh公開鍵の準備
    • 鍵をcurlするだけでつなげるような状態にしたい
  • gitリポジトリ作る
    • 鍵とssh-configとを準備する
  • つけるべきHTTPヘッダの整理
    • 304 not modified, expire + etag
    • 200 ok
    • 301 / 302 転送
  • 画像圧縮
    • ツールや構築の下準備
  • ご飯@ランチの用意
    • 当日朝各自で買ってこよう
    • 大人なのでお菓子とアルコールはご自由に!
  • FWになれる
  • 実装の読み解き
    • 頭の中でやるとつらい
    • ホワイトボードに書こう
    • 付箋を使おう
  • 当日の流れを作っておく

ISUCON のたびに毎回考えるのはアレなので、そろそろメンテしながら使いまわしをしていきたい所存。

それと、練習しよっ♡、なんて言ってたけど結局一回もやってない。いや、環境構築までは一回やった。 vagrant-isucon という素晴らしいものがあって、これを使わせてもらった。当時行われた時の得点まで伸びはしないだろうが、何をしたらどう伸びるのか、という部分は見ていける、便利ちゃん。

matsuu/vagrant-isucon: ISUCON過去問を構築するためのVagrantfile集

ちなみに結局当日になっても、このリストの大半は実施あるいは準備されませんでした。。熱量の違いだったり、そういうアレ。

当日にやったこと

合流したのは18時過ぎ。それまでに以下のことをチームメンバーが進めていました。

  • phpへの切り替え
  • リポジトリの準備
  • /messages/ の N+1 の解消
  • インデックス設定
  • 画像ファイルを DB から取り出し DB に保存しないようにする
  • nginx, mysql, php-fpm の微調整

合流したときにはスコア的には 1 万くらい?
( っ˘ω˘c).。o○( なんでスコア伸びてないんだ…? )

そこからはこんなことを進めていきました。

  • 画像圧縮するしないで揉めてたっぽいのでやるなって言った
  • それするより画像に 304 つけろって言った
  • LINE 通話をつなげた
  • ハッピーターンでお腹を満たす
  • /fetch の N+1 改善
  • nginx の設定サポート
  • /message や /login あたりの微妙な改善

時間も少なく、現場まで合流するのは厳しかったのでリモートで作業せざるを得ませんでした。チャットだけでも良かったんですが、声が繋がったほうが何かと便利なので LINE 通話を PC で繋ぎっぱなしにして、あーだこーだ言いながらやってました。

最終スコアは 37000 くらい、だったかな?(結果一覧に出ている点数と微妙に違うっぽい?)

ISUCON 5 とくらべて、アプリ上の実装の問題はほとんど消化できたと思っていて、そのあたりは前回からレベルアップしたと思う(チームとして)

画像の304については理解が浅くて結局出来ていなかった。周りの攻略記事を見ると、ここが出来る出来ないでスコアが大きく変わっていたように思える。最後までベンチマーク実行結果に icons のレスポンスが遅くて~、とあって、これが改善できないともうスコア伸びないってのは分かっていたけど、いろいろなものを信用しすぎて何も動けなかった。

結局よくわかんなくて合流してからずっと右往左往したところがあって、ベンチマーク時に 2 つチェックいれたらどうなんねん、というところ。リバースプロキシしてバランシングしたり、外したり、そもそも設定うまく行かなくて、だいぶ時間がかかった。結局 2 つチェックいれたらベンチが倍速になってスコア上がるんかな?どこかに説明あったっけ(見落としたかも?)

サーバ構成的には AP + AP + DB という、多分他と同じ構成。

次に向けて

ISUCON が来年も実施されるかわからないけれども(ものすごく楽しいイベントなので実施されてほしい)、自分のスキルアップはもちのろん、チーム側の意識・熱量・レベル感が合ってないどうすっかなあ、というところをやっていかないと勝てないだろうなって気持ち。例えばあった話だと、とりあえず画像圧縮してプロキシキャッシュすれば勝てるやろ~~、とか真面目に雑な話をしている人とか。19時くらいになってわかんないからスロークエリ入れるか~~~とか言ってたり、先やれよ。とか。うごかないんだけど見てくれ~って、別にこっちも分からないから都度調べてるんだけどなあ…。とかなんとか、こんな人と仕事していたのかとか云々。余談が過ぎた。ようは 3 人で役割を分けてそれぞれ最高のパフォーマンスをしていくべきで、それは 2 人になっても変わらなくて、ぼくが居ないまたは手が出せない状況だろうと頑張ってくれって気持ち。なのでそれが足りないっぽいのでチームビルディングというか、そういう類の云々がだめだめだったんだろうなあ~~。

もし次があったとして、次も同じチームででるかっていうと怪しく、楽しく全力で戦えるチームで出来るといいな。どんどんやっていこう。
結果として ISUCON 7 は予選敗退だったけれども、俺の、俺たちの、 ISUCON 7 はまだ終わってない!(練習しよ

そんなポエムで〆


ISUCON 運営の人たちメチャ忙しいと思いますけど、こうやって今年も楽しいイベントを開催してくれてありがとうございました👏