maco's life

主にエンジニアリングと読書について書いていきます。

ISUCON5 本戦での学び

ISUCON5の本戦をchatzmersとして参戦してきました。 結果からいうと惨敗でした。

敗因としてRobert C. Pike氏の下記の言葉に全て詰まっていると思います。

推測するな、計測せよ

どういうこと?

今回使用されていたデータベースがpostgresでした。 僕たちは

  • 日常的に業務でmysqlに触れていてpostgresより詳しい
  • 本戦の予行演習で mysqlのチューニング方法を予習していた

以上の判断から最初にpostgresをmysqlに置き換えることをしました。 限られた時間しかない中、もしかしたらdbにボトルネックがないかもしれない という状況で先に移行をするという決断をしたのは判断ミスだったなと思います。

今回で言えばslowqueryをみたり、cpuの使用率をみたら今回はDBに全く 負荷がなかったことはわかっていたようなので、 予選の時にできていた計測して確実に潰していくというスタンスは本戦でも 徹底していくべきでした。

mysqlに移行して何がよくなかったのか

単純に移行に時間がかかりました。 やった作業と時間がかかったポイントとしては

  • 初期データをmysqlでいれられる状態のものに置き換える
  • 移行した際に文字化け問題に悩まされる(これは最後まで解決できなかった)
  • 移行に伴いケアレスミスを連発して、少しずつ時間を削ってしまった

以上の点で苦戦しました。

結局何をしたのか

僕達ができたこととしては、

  • dbとappのserverを分割
  • dbサーバー * 1 + appサーバー * 2の構成にする
  • dbをpostgresからmysqlにする
  • その他アプリの微修正(スコアに全く影響がなかったので割愛)

以上ができたこととなります。 複数台構成にしたので、単純にスコアが初期スコアの1000から約2倍の1900ほどの上がり successで無事11位になれました。

まとめ

結果として力不足を露呈する形となりましたが、学びは多かったです。

僕らが現職で入社した時に行った社内ISUCONではスコアは良かったもののfailに終わってしまい、 スコアが残らなかったという苦い経験をしました。 それを加味して今回は、本戦が始まる前に確実にsuccessにして終わらせよう という話をしていたのでそれは有言実行できて良かったです。 二年半前からはちょっと成長ができたのかな...?

来年は上位目指して頑張りたいとおもいます。

最後に

運営の皆様本当にお疲れ様でした! ISUCONというイベントが行われる大切さと、参加することで得られる多くの経験を知ることできました。 様々な背景をもつエンジニアの方々一つの目標に向かって競い合う機会は滅多になく、そんな有意義なイベントが 今年も開催できたのは運営の大変な努力があったおかげだと考えると、感謝の極みです!本当にありがとうございました!

ISUCON5 予選4位通過でした

社内ISUCONには何度か参加したことあるものの、社外の人も参加するISUCONには初めての参加でした。 チーム名は「chatzmers」で、弊社の2013年新卒同期の @m0t0k1ch1@rg_gs と 僕 で出場し、最終スコア 21242 で予選総合4位で通過しました。

isucon.net

無事予選突破して、先輩エンジニアの方々と本選で戦えるのが本当に嬉しいです!

チームメンバーブログ

チームメンバーのブログエントリーです。 自分のエントリーより、詳細に丁寧に書かれているのであわせて読んでいただけたら幸いです。

前日までにやったこと

ISUCON予選前までに、メンバーで集まってISUCON4の問題にGCPを使って挑戦することをしていました。 主な目的としては、実際にやってみてGCPに慣れることと、 予選で使うであろうミドルウェアまわりの設定の方法やツール等の使い方を予習することでした。

僕は2回あるうちの1回しか練習に参加できなかったので、前準備はあまり力に慣れなかったけど @rg_gsが予選で使うであろう自動デプロイのツールつくったり、supervisorの設定をおぼえる、リポジトリつくるなどし、 @m0t0k1ch1がGazellekataribe, pt-query-digestを使ってみたりなどをやってくれて頼もしかったです。 詳しくは2人が、ブログ書いたらこちらにもリンクをはります。

当日やったこと

僕らが選択した言語はみんな大好きPerlでした。 そして最初に

  • alp, pt-query-digest etcを使って遅いと思われる箇所を洗い出す (@m0t0k1ch1)
  • systemdの設定 (@rg_gs)
  • アプリのコードを完璧に理解する(自分)

をやってみて、「あーボドルネックだらけだな」ってなったので、 おもに重かった、//friends/footprints/entriesを すごいことをやろうとせず地道に直そうという方針で対応していきました。

主に自分がやった修正は一番多く叩かれてその上遅かった/のまわりのコードの 改修で、 自分がした修正で効いたーってなったのは以下でした。

  • friendsは数しか必要ないのでcountするようにする
  • LIMIT 1000で取ってきている処理をよしなに直す
  • entriesはページ上でcontentを使ってないので、titleだけ取ってくるようにする

これらの修正でスコアが伸びて1位に躍り出て「これがISUCONか」と鳥肌たちつつ、わいわいしてました。 f:id:Maco_Tasu:20150928185552p:plain

(その後抜かれたのは言うまでもないです)

他にもN+1問題を直してみたりといったアプローチを入れてみたものの、 クエリの実行計画かわって逆に遅くなったりなどあり思うように伸びず。 少し直してはベンチ回して検証して、良かったらmasterへmergeということを地道にしていました。

@m0t0k1ch1は事前準備の知識を活かしながら

  • InnoDB => MyISAM変更
  • Gazelleを使うようにする
  • userのデータはプロセスキャッシュに乗せてN+1の箇所でクエリが飛ばないようにする
  • friendsはoneだけつかって取ってくるようにする
  • Nginxで静的ファイルを返すようにする
  • etc...

をやっていました。 Gazelleに変えた変更や、MyISAMに変えた変更でだいぶスコア伸びててさすがって感じでした。 (事前準備のかいあってシュッと変更していた@m0t0k1ch1もすごかった)

@rg_gsは主に

  • 自動デプロイの設定
  • footprintsで変なGroupByをしているクエリを適切にindexはりつつ直す
  • entryのコメントの数をRedisを使ってうまくできないかチャレンジ

などをしてくれていました。commit&pushしたら自動でデプロイしてくれるなどの地盤作りがあったおかげで 本当に快適ベンチ回すことができました。最高です。 また、entryのcommentの数をRedisに入れるのは地味に苦戦して、予選当日はつらーってなってたけど 先程@fujiwaraさんisucon予選のエントリーブログを見て aofを使ってて、そういう手があるのかーってなりました。さすがです...orz

それぞれがやったことベースでまとめましたが、時系列順にどう対応していったかはきっと他のチームメンバーがまとめてくれるはず...。 あと書き忘れありそう

やり残したこと

  • entryのbodyがだいぶ大きいので、 gzip圧縮したら結構スコアが上がったんじゃないだろうか(予選中は思いつかなかった)
  • CPUを使い切ることができなかったけど、ボトルネックは他にどこにあったのか

この2つができなかったので、またベンチかけれるなら挑戦してみたいです。

最後に

今回一緒のチームで参加した同期は、入社した時に社内ISUCONで皆FAILをするというつらいおもいを経験していたこともあり、 事前準備で予習と対策をたて、予選に臨んだことが結果としていい方向に事が運んだかなといった感じでした。 また予選前に、弊社の先輩エンジニアの@tkuchikiさんがalpというLTSVで吐かれたログをよしなに集計するツールを作っていらっしゃったので、そちらを使わせていただきシュッと遅いAPIを手軽に探ることが出来ました。ありがとうございます!

最後にこんなにやっていて楽しいと思える問題と、それに挑戦する機会をくださった運営の皆様、本当にありがとうございました!お疲れ様でした!

本選も頑張ります🙏

DateTimeオブジェクトそのまま比較するのと、DateTime->epochで比較するのどっちが速いか

ふと気になったのでベンチマーク取ってみた。

ベンチマーク

#!/usr/bin/perl

use strict;
use utf8;
use warnings;

use DateTime;
use Benchmark qw(:all) ;

my $now_1 = DateTime->now;
my $now_2 = $now_1->clone;
my @times = ($now_1, $now_2);
my $today = DateTime->today;

cmpthese(0, {
    'datetime' => sub {
        [grep { $_ >= $today } @times];
    },
    'epoch' => sub {
        [grep { $_->epoch >= $today->epoch } @times];
    },
});

1;
MacoTasu% perl -Ilib datetime-vs-epoch.pl
             Rate datetime    epoch
datetime  32462/s       --     -84%
epoch    205263/s     532%       --

epoch is faster than datetime !

なんとなく予想はできていたけど、epochのほうがやはり速かった。 性能差約5倍かな。

比較の方法

そもそもDateTimeオブジェクトとかDateTime->epochの比較とかどうなってるんだってばよ ってなったのでちょっとだけコードみた。

DateTime->epoch

epochの実装をみると

package DateTime;

...

sub epoch {
    my $self = shift;

    return $self->{utc_c}{epoch}
        if exists $self->{utc_c}{epoch};

    return $self->{utc_c}{epoch}
        = ( $self->{utc_rd_days} - 719163 ) * SECONDS_PER_DAY
        + $self->{utc_rd_secs};
}

のようなになっていて、utc_rd_*などに日にちと秒数が1970年1月1日までの経過日数を引いてそこに1日分の秒数をかけて秒に変換している。 単純に数字の変換の処理をして、その後比較してるだけなので速い。

Datetimeオブジェクトそのまま

Datetimeオブジェクト同士の比較は、闇っぽくてoperatorをoverloadしている。 ゆるふわ脳なので、なんかPerlがよしなに比較してくれてるんだろと思ったけど、頑張ってたのはDateTimeのモジュールでした。

比較しているところは

package DateTime;

...

sub _compare_overload {

    # note: $_[1]->compare( $_[0] ) is an error when $_[1] is not a
    # DateTime (such as the INFINITY value)

    return undef unless defined $_[1];

    return $_[2] ? -$_[0]->compare( $_[1] ) : $_[0]->compare( $_[1] );
}

sub _compare {
    my ( $class, $dt1, $dt2, $consistent ) = ref $_[0] ? ( undef, @_ ) : @_;

    return undef unless defined $dt2;

    if ( !ref $dt2 && ( $dt2 == INFINITY || $dt2 == NEG_INFINITY ) ) {
        return $dt1->{utc_rd_days} <=> $dt2;
    }

    unless ( DateTime::Helpers::can( $dt1, 'utc_rd_values' )
        && DateTime::Helpers::can( $dt2, 'utc_rd_values' ) ) {
        my $dt1_string = overload::StrVal($dt1);
        my $dt2_string = overload::StrVal($dt2);

        Carp::croak( 'A DateTime object can only be compared to'
                . " another DateTime object ($dt1_string, $dt2_string)." );
    }

    if (   !$consistent
        && DateTime::Helpers::can( $dt1, 'time_zone' )
        && DateTime::Helpers::can( $dt2, 'time_zone' ) ) {
        my $is_floating1 = $dt1->time_zone->is_floating;
        my $is_floating2 = $dt2->time_zone->is_floating;

        if ( $is_floating1 && !$is_floating2 ) {
            $dt1 = $dt1->clone->set_time_zone( $dt2->time_zone );
        }
        elsif ( $is_floating2 && !$is_floating1 ) {
            $dt2 = $dt2->clone->set_time_zone( $dt1->time_zone );
        }
    }

    my @dt1_components = $dt1->utc_rd_values;
    my @dt2_components = $dt2->utc_rd_values;

    foreach my $i ( 0 .. 2 ) {
        return $dt1_components[$i] <=> $dt2_components[$i]
            if $dt1_components[$i] != $dt2_components[$i];
    }

    return 0;
}

こんな感じの実装になっていたところまで確認したけど、不思議なことに途中からの記憶が無い。

結果

DateTimeオブジェクトを使った比較をする時は、DateTime->epochで比較したほうが速いのでいいのかもしれないですね。

参考資料

Dave Rolsky / DateTime-1.20 - search.cpan.org

追記(9/1)

とのこと疑問点をいっちー先生があげいてたのでキャッシュっぽいところコメントアウトして回してみたところ

MacoTasu% perl -Ilib datetime-vs-epoch.pl
             Rate datetime    epoch
datetime  34357/s       --     -81%
epoch    178069/s     418%       --

以上の結果になりました。ちょっとだけスコア落ちたけどそれでも約4倍ぐらいはでました。

commitにhookして、gofmtを実行する

golangっぽいコードに整形してくれるgofmtをcommit直前に実行し、 gofmtの結果追加の差分がでたら、1度commitを取りやめるようなcommit hookを書きました。 gofmtは差分があってかつ、拡張子が.goのファイルにのみ実行されます。

デモ

f:id:Maco_Tasu:20150822184311g:plain

コード

gist.github.com

このコードを.git/hooks/pre-commitに書いておけば、commit直前にhookされるようになります。

最後に

複数人でコードを書くような場合に、共通のhooksとしてそれぞれ設定おけば、 必ず整形されたコードのみがcommitされる良い状態になるのかなと考えて書いてみました。

※エディタをきちんと設定しようっていう話で済む気もしますが、別の試みとしてやってみた感じです。

変更のあったテストファイルだけ実行したい

ローカルPCで新しく追加したり、修正加えたテストを実行する時

prove -v t/hoge.t t/fuga.t ...

みたいに普通にやっていたんだけど、 git使ってるし、変更あったファイルをテストするのもっと楽にできるやんって思って

#!/bin/sh
git diff --name-only HEAD t/ | xargs prove -v

こんな感じのshell script書いて、dpってうったら差分のあったファイルだけテストする感じにしみてた。 便利な気がする

go-irceventでbotを作った

go-irceventでbotを作成しました。 本当は既にあるperlで書かれたbot使えば良かったのだけれども、 どうぜやるならgolangbot書きたいなって思って勢いで作りました。

気にした点とか

何も考えずにgo-ircevent使うとif Aというコマンドの時はみたいな条件分岐がふえて可読性が悪くなるので、hubのコードを参考にして、commands.goのCallを呼べばよしなにコマンドに対応するfuncを取ってきて実行するような形を採用しました。 基本的にコマンド特有の色が濃い処理は commands/hoge.goの中に書いて闇を封じ込めて、処理として切り離せる(使いまわせる)部分はmodels/以下に書いていくような感じです。

設定情報などはconfig/config.ymlを読み込みにいきますが、外に出したくない情報などを扱いたい場合は、config/config_local.ymlを作ることでそちらの情報を優先して読み込むようにしています。config_local.ymlは.gitignoreに入れているので、まちがってコミットされたりすることもなさそうなので便利😇

以下がソースです。

github.com ※今実装されているコマンドは自分都合でつくったものなので、他の場面ではほぼ使えないとおもいます。

golangっぽい書き方とかわかってないので、ご指摘いただけますと幸いです。

はやくテストかかねば

参考にした記事・プロダクト

実装するにあたって以下の記事・プロダクトを参考にさせていただきました。

go-irceventでお手軽IRC bot作成 - Qiita

github/hub · GitHub

m0t0k1ch1/gis · GitHub

ありがとうございます🙏

Multi-AZの役割

前回の記事でMulti-AZについてふれましたが、その続きです。

今回は自分の整理のために、Multi-AZが設定されている場合と無い場合で 障害が起こった際にどういった復旧手順の違いがでてくるか考察します。

Multi-AZを設定していない場合

f:id:Maco_Tasu:20150709221514p:plain

Multi-AZが設定されてないので、一つのAZにのみMaster RDB 1台がある想定です。またその下に2つReadReplicaが並んでいます。 MasterとReadReplicaが1:1だと、ReadReplicaにそこそこ参照系のクエリが飛んでいて、ReadReplicaが落ちた場合に 全てのクエリがMaster 1台に飛んでしまい、Masterも道連れで落ちてしまうため可能性がでるため、ReadReplicaは2台という想定にしています。

ここでMasterのRDSインスタンスの障害で、参照も更新もできない状態になってしまったとします。 ここからの復旧作業だと、おそらく次の手順をする必要があるのかなと思います。

  1. メンテナンスにいれる
  2. ReadReplicaをMasterに昇格させる
  3. Masterから新しいReadReplicaを作成する
  4. DBの状態が最新かbinログ?をみて確認する
  5. 問題なければメンテナンスを空ける

みたいな手順になるかなとおもいます。 実際にそんな障害にあったことないので、正確な手順としては色々かけてそうですが、だいたいこんな感じのことをするのかなという想像。

Multi-AZを設定していた場合

Multi-AZが設定されていると次のような図になると思います。

f:id:Maco_Tasu:20150709223107p:plain

設定していない場合との違いは、別のAZにMasterのDBの複製を常に作っています。 もしこの状態で、今のAZまたはRDSインスタンスに障害が起こった場合に、自動的に別のAZにスタンバイしているインスタンスにフェイルオーバーします。 公式ドキュメントによると 1~2 分間完了するらしいです。 また同じRegionにあり、endpointが変わらないのでApp側のRDSの接続設定とか書き換えないでそのまま使えるます。 フェイルオーバー完了までにかかった時間 = ダウンタイムと考えると、Multi-AZを設定していない場合と比較してかなりはやく復旧できそうです。 手順でいうと

  1. 自動フェイルオーバー
  2. ReadReplica作成

ぐらいで、こちらでするのはReadReplicaを作成し直す作業かな。

以上がMulti-AZを設定していない場合としていた場合の大きな違いになると思います。 ただMulti-AZ配置にするとやっぱり2つインスタンス立ててる状態になるので、料金も高かった。 費用対効果を考えて、使うかどうか判断しないとですね。

記事を書くにあたって色々調べつつ、他の方に聴きつつまとめました。 自分の理解が及んでいなく、間違っている部分が多々あるかもしれません。 間違いなどありましたら、ご指摘いただけますと幸いです。