時計を壊せ

駆け出してからそこそこ経ったWebプログラマーの雑記

クソコード、あるいは技術的負債

クソコードについてここ数日で考えたことを書いてみる。

技術的負債まわりのえらいひとたちの議論を眺めてて、技術的負債って言うとなんかプロっぽいけど、クソコードって言ったほうが示したいモノを素直に表してるし分かりやすいきがしてきた。

 

 

クソコードを書くなとは思わないけど、クソコードをいつまでも放置するのはやめようって思う。

クソコードは次なるクソコードを生み出すし、バグを隠蔽するし、メンテナンスコスト増大の悪循環のキッカケになるし、新人の教育上良くないので無くて済むならもちろんないほうがいい。

 

 

ただ、ギークな人たちを除いて、さらっと60点*1のコードなんて書けない。僕を含め大多数のエンジニアは自分自身が書いたクソコードをリファクタリングして60点以上のコードを目指すための時間が必要になる。

そのうえ、そういうコードを書いてもだいたい時間経過に伴って事情が変わって、60点のコードの挙動を壊さないように慎重に書き換えてビジネス上の目的を達成していったら、どんどんコードはクソになってくとか、ザラだと思う。

そういうわけで、クソコード書くなって言っちゃうと、生産性*2は落ちると思う。

それに、だいたいビジネス上の目的を達成するためにはアホみたいに短い締め切りを守る必要があって、さもなくばチャンスを逃してしまい予算が達成できない。あるいはプロジェクトの継続が困難になってしまう。あるいは経理上ヤバイ数字になってしまってどうなってんだと株主から怒られる。あるいはユーザーに呆れられて製品から人が離れていく。いろんな最悪のシナリオが考えられて辛い気持ちになる。

そんなときはどんなにクソなコードを量産しようとも、ビジネス上の目的を達成しないといけない。いま自分の給料を稼いでくれているシステムはクソコードも厭わないビジネス上の目的を達成するための努力の結果出来上がったものかもしれない。

もちろん、最初からクソコードなしでそういうビジネス上の目的が達成出来れば最高だと思う。そこを目指すこと自体に異議は無い。

ただ、クソコード書くなって言っちゃうとビジネス上の目的を達成する事が(ただでさえ困難なのに)、より難しくなってしまう。

だから、クソコード書くなって言うのはやめた方がいいと思う。間に受けてひたすらコードを綺麗にするために命を懸けるみたいな事をする人が生まれかねない。*3

ただし、新人の場合は「こういうコードを目指せば良いのか」と思ってクソコードを量産するように育っても、自分達も本人も困る事になるのでちゃんとレビューしてクソコードは理由を説明しつつ突っぱねて良いコードが書けるように教育していくべきだと思う。

要するに、僕が思ったのは、やむなくクソコードを書いても、それで目的が達成出来るならいいじゃないか。ということだ。

 

 

ただし、クソコードはクソコードのままメンテナンスし続ける事は出来ない。

なぜなら、メンテナンスコストが増大し、ビジネス上の目的を達成するための変更を加えるのにかかる時間が増えていってしまうからだ。*4

クソコードの度合いにもよるが、時間経過に対して正比例的にメンテナンスコストは増大すると思う。(このへんは技術的負債が云々でホッテントリしてた人の記事が詳しく書いてたので読むといいと思う。僕はその記事にすごく共感した。)

だから、クソコードは直していかないとそのシステムでお給料を貰うのはどんどん困難になっていくと思う。

クソコードをメンテナンスし続けても給与3倍から遠ざかって行くし夢がない。

なので、クソコードは少しづつ改善しながらメンテナンスし続けていくのがいいと思う。これは最初からクソコードを書かないって事より遥かに難しいけど、給与3倍になると思えば出来ると思う。疲れてきた。

 

 

そして、時間的制約の中でどれだけ良いコードが書けるかというのは非常に大事だと思う。

クソコードは止むを得ず書くもので、クソコード自体は良いものではないので、書かなくて済めば書かないほど良い。

だから、スケジュールに影響が出ない程度に良いコードを目指すのはとても良い事だと思う。クソコードをちょっと直して60点以上になればビジネス上の目的を達成した上でコードは綺麗に保てるし最高だ。誰もが幸せになれる。

ただ、ぼくたちはだいたい未熟だしいつだってどんな仕様だって60点以上に出来る実力はないから、良いコードを書けるようになるために常日頃から努力を重ねる必要があると思う。だけど、ビジネス上の目的を達成出来ないと給与3倍から遠ざかっていくし、バランスが大事だと思う。その上で良いバランスを目指すために良いコードをさらっと書けるよう努力していくとみんな幸せになれそうだ。

 

 

iPhoneでベットでぬくぬくしながらてきとーに書いてたらつかれた。今日は有給だ。

*1:人によって基準は違うし、だいたいクソコードの基準を決めるのはギークな人たちなので、ここでは便宜上60点が合格点であると定義する

*2:時間あたりでどれだけの成果物を生み出せるかという意味合いでの

*3:もちろん、それはそれで良い事なんだけどビジネス上のメリットは殆んど無いケースが多いと思う

*4:そんな変更を加える必要も無いくらい完璧にシステムが作られているなら別だが、そうそうないでしょう

redis-sentinel管理のredisを手動failoverさせる

やんごとなき理由でredis-sentinel管理のredisのmasterを再起動したくなって、
手動failoverのやりかた調べてたら公式のドキュメントにちゃんと書いてあった。
公式のドキュメントちゃんと読まないとダメだ。


適当なsentinelに入って、

redis 127.0.0.1:26379> SENTINEL failover <master name>

としたら良い感じにfailoverしてくれた。
redis-sentinelべんり。

configファイルのmasterのIPも新しいものに変えておかないと次回sentinelを再起動したタイミングで狂うので忘れずに換えて置かなければならないのだけ注意したい。
というか先にconfigのIP変えてから手動failoverしたほうがいいかもしれない。

参考: http://redis.io/topics/sentinel

2013年のKPT

2013年終わったのでKPTやってみる。

Keep

新しい分野に挑戦した

  • Android
  • フロントエンド開発
    • こっちはお手伝いとお遊び程度だけど

新しい言語に挑戦した

新しいモジュールを作った

  • Parallel::Async
  • SQL::Maker::Plugin::Case
  • Hash::Compact::Lite

ISUCON3出た

  • 予選通過
  • 本戦惨敗

作曲した


ぼくの干し椎茸 by karupanerura on SoundCloud - Hear the world’s sounds

帰り道(demo) by karupanerura on SoundCloud - Hear the world’s sounds

帰り道はこのデモをもとにThe Monzllisでバンドバージョンを作った。そっちは2番もある。

ライブやった

高円寺界隈の人と仲良くなれた

  • バンドつながりで知り合いがふえた

資格試験受けた

温泉発火村やった

  • 熱海行った
  • 旅館がすばらしかった

地域PMに参加した

  • Hachioji.pm
  • Chiba.pm

エンジニアのコミュニティにより深く関われた

  • kenjiskywalkerさんとかstudio3104さんとかuzullaさんとかxtetsujiさんpython_spameggsさんとかsongmuさんとさyusukebeさんとか、挙げてくとキリが無いくらいの人と知り合えた。

ITエンジニア平成会に参加できた

  • 意外とあの人と歳近かったのか!みたいなのあってよかった。
  • 新年会やりましょう

Problem

デスマった

  • 見積もりミスこわい

引っ越しの片付けが済んでいない

  • 言い訳のしようも無い

Yomiuri完成してない

  • Text::Markdown::Hoedownのlow-level APIを使って組んでたがAPI変更があったり

ISUCON3惨敗

  • インフラ周り本格的に勉強したいしノウハウ積みたい

情報セキュリティスペシャリスト試験落ちた

  • 午前Iでマークシート写し損ねがあって減点したのが痛かった
  • 自己採点だとたしか午前Iだけ落ちてたはず。かなしい

IT芸人になりつつある


自分が作った代表的なプロダクト的なものが無いまま名前だけそこそこ知られてるって状況になりつつあってエンジニアとしての危機を感じている。

Try

新しい言語を会得する

新しい分野に挑戦する

  • サーバー運用
  • 言語処理系

新しいモジュールをつくる

  • ORM
  • 痒い所に手が届く系

遠方の地域PM/参加した事の無い地域PMに参加する

  • Hokkaido.pm

Perl/MySQL以外の勉強会に参加する

  • rbとかgo

英語を勉強する

  • YAPCで英語でトークしたい

数学を勉強する

ソロでライブやる

  • 弾き語り
  • のために曲作る

ISUCON3でまたもや惨敗したはなし

ISUCON3への参加から日が経ってしまいましたが、来年の自分へのメッセージとして書きます。

だいたいの内容

坂パスタチームの仲間の面々が書いてくれているのでそちらをご参照下さい。
id:kfly8 http://kfly8.hatenablog.com/entry/2013/11/30/200331
id:masasuz http://masasuzu.hatenablog.jp/entry/2013/11/10/isucon

事前準備

生憎、仕事もプライベートも予定がキツキツになってしまって準備に割ける時間が直前でなくなってしまって全然動けなかった記憶があります。
もっとはやく準備してればよかったなーと思いました。足回り系とか、準備が主な仕事なので役割果たせてない。
id:kfly8 先生にげんこつされる夢を見ました。うそです。
でも、げんこつされても反論できないくらい、仕事できてなかったので来年がんばる。

ぼくがやったこと

  • 緊張する
  • gitとか整える(ここまで平常運転)
  • とりあえずconvertコマンドがボトルネックになってる事がわかったのでこいつを速くするぞ!まずは置き換えてみよう!というアウトな方針を立てる
  • convertコマンドを他のcpanモジュールで置換しようと延々と試行錯誤を行い時間を無駄に潰す
    • 画像のdiffが多くてコケる
  • 慌てる
  • ImageMagicのperl bindingなら大丈夫だろ
    • 画像のdiffが多くてコケる
  • 慌てる

以上です。アウトー!!

反省

事前準備に時間を割かなさすぎた

時間がない!って分かった時点で他の人に事前準備の役割を移譲するべきだったし、
そもそもぎりぎりになって準備をしようと考えている時点で甘かった。

方針決定に時間を割かなさすぎた

ほぼ思い付きの方針みたいな感じだったし、冷静に考えればまずはリソースを使いきれるようスケールアウトさせるのが先決だった。
チューニングの順序も、本来は「処理数を減らす」「処理を高速化する」の2通りあるはずだったが、
予選でmarkdownコマンドをHoedownに変更した事が功を奏した事などから短絡的に「処理を高速化する」方向に舵を切ってしまった。
方針変更を躊躇した原因もここにあり、なぜなら他のチューニング方針を考えていないので、
方針変更は新たな方針を立てる事から始めなければならず、方針変更のコストが高くなってしまっていた。
ましてや、普段触ってるアプリとは違う性質のアプリをチューニングする上で、
僕みたいな経験の浅い若者にベストな答えが一発で出せるわけがなく、
色々なチューニング方法を検討した上で、ダメだった場合のプランBも決めて、
一気に手を動かしたほうが確実に効率は良かった。

慌てた

慌てんな。

感想記事を書くのが遅すぎた

覚えている事が少なくて反省の記録として質が悪い。

感想

毎度、反省が多く、勉強になり素晴らしいイベントだなと思います。
今回も多くの学びがありました。
技術的な部分だけではなく、短期間で成果をあげるためのアンチパターンやら、
チームで仕事をする上でのアンチパターンみたいなものにもハマるので、
本当に、参加するだけで価値のあるイベントになっているとおもいます。
特に、新卒や学生の人こそ参加してみて、プロの仕事の品質を身を持って体感してみると良いと思います。
運営の方々、本当にありがとうございました。来年も楽しみにしております。

Amon2で非同期レスポンスを使う方法と、非同期WebAppのハマりどころ

この記事はPerl Advent Calendar 2013の15日目の記事です。

Amon2とは

@tokuhirom さんが開発しているPerl製のWAF*1です。
Plackを軽くwrapしたような軽量でシンプルなWAFです。
現在、Version 6.00がリリースされていますが、Version 3.50からwebsocketのサポートが入り、
その関係でPSGIの遅延レスポンス/ストリーミングレスポンスのインターフェースに対応しています。

Amon2で非同期レスポンスを使う

Amon2::Plugin::Web::Streamingを使う事により非同期でレスポンスを返す事が出来ます。
例えば、index.txを5秒後にrenderして返す場合は以下のようになります。

use strict;
use warnings;
use utf8;
use Amon2::Lite;
use AnyEvent;

get '/' => sub {
    my $c = shift;
    return $c->streaming(sub {
       my $responder = shift;

       my $w; $w = AnyEvent->timer(after => 5, cb => sub {
           my $res = $c->render('index.tt')->finalize;
           $responder->($res);
           undef $w;
       });
   });
};

# load plugins
__PACKAGE__->load_plugin('Web::CSRFDefender' => { post_only => 1 });
__PACKAGE__->load_plugin('Web::Streaming');
__PACKAGE__->enable_session();

__PACKAGE__->to_app(handle_static => 1);
__DATA__
@@ index.tt
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Streaming</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script type="text/javascript" src="[% uri_for('/static/js/main.js') %]"></script>
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
    <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
    <link rel="stylesheet" href="[% uri_for('/static/css/main.css') %]">
</head>
<body>
    <div class="container">
        <header><h1>Streaming</h1></header>
        <section class="row">
            This is a Streaming
        </section>
        <footer>Powered by <a href="http://amon.64p.org/">Amon2::Lite</a></footer>
    </div>
</body>
</html>

@@ /static/js/main.js

@@ /static/css/main.css
footer {
    text-align: right;
}

このように$c->streamingにcallbackを渡すとそれがそのまんまPSGIの非同期レスポンスとして使えてあとは$responderをすきに料理すればいいというスンポーです。簡単ですね!

ハマりどころ

ブロックする処理を混ぜてはいけない

現実的に、現状は非同期レスポンスを返すアプリケーションを書く場合はAnyEventのイベントループの上で動くことになる為、イベントループを止めるような処理を書いてはいけません。
例えばネットワークIOがあるものなどはAnyEvent::Socketを使うライブラリを使い、sleepやwait(pid)?の代わりにAnyEvent->timer/AnyEvent->childを使うなど、イベントループを止めずに終了を通知させるAPIを使う必要があります。
イベントループを止めてしまうとその間他の処理が一切出来ないため、たとえばDBに1秒かかるクエリを投げた場合、1秒間リクエストの受付すら出来なくなってしまうという状態が起こりえます。

context毎に他のサーバーへのconnectionを保持するとリクエストの度にconnectionが増加して死に至る

HTTPの同時接続数と同じだけmysqlへの接続数が増えるという事になると、
非同期レスポンスが中心になるアプリケーションでは同時接続数が非常に膨らむ為、問題になります。

非同期レスポンス中ではAmon2->contextを使ってはいけない

Amon2->contextはAmon2における現在のcontextオブジェクトがどこからでも取れるという便利な代物ですが、
非同期レスポンスではこれはハマりどころになります。


なぜなら、Amon2の中でのリクエストの開始と終了というのはAmon2::Liteにおけるget '/'などに登録したcallbackが終わるまでの間*2だからです。
つまり、$c->streaming()の中ではcontextは基本的にundefになります。
また、同時に複数のリクエストを捌いているケースではタイミングによっては他のリクエストのcontextを参照してしまう為気付きにくいです。
シーケンスで表現すると以下のような感じです。*3



解決策

Object::Containerなどを使い、globalにリソースを持つ

globalにリソースを持つ事によって、それを複数リクエストで共有する事が出来ます。
もちろん、トランザクションなど、リソースに状態変化が起こる処理を行うと問題になりますが、
そのような処理を行わない場合は検討しても良いかもしれません。
例えば、トランザクションが必要な処理はGearmanなどのWorkerに任せてAnyEvent::Gearmanなどでタスクとして投げてその終了をcallbackで受けるなど、別プロセスで同期的に処理するのが良いかと思います。
もっとシンプルにやる方法もあるとベターかとは思いますが現状ではなかなか辛そうな印象です。
(あるはAnyEvent::DBIが同時に複数のトランザクションが走らないようなアーキテクチャになれば問題は解決するかもです)
他に良い方法があれば教えて下さい!

まとめ

非同期レスポンスむずい!!!!!!!!!!!
明日はid:mackee_wさんです!

*1:Web Application Framework

*2:もっと厳密に言えばAmon2::Web#handle_requestが終了するまでの間

*3:seqdiag.jsを使っています。walf443++

state変数は初期化のタイミングで例外が発生しても二度と初期化が行われる事は無い

タイトルの通り。
まあ、初期化は1度しか行われないので、あたりまえといえばあたりまえですがハマったのでメモ。


以下検証コード。undef,2,2となるかと思いきや初期化は1度しか行われないのでundef,undef,undefとなる。

static variable be undef if throw when initialize ...

#isucon 3予選を運良く通過したはなし

予選通過、かなり高い壁だと思っていたので通過出来て大変喜んでおります。


ちなみに、当日記録できた最高スコアは10846.599609375でした。workloadは8です。
これ、結果がブレた原因はなんとなくわかってるので後のほうに書きます。

今回気を付けた事

既に2回参加したisucon、これで3回目の参加となりますが、
流石に3連敗とはいかんのでせめて予選通過をというのが今回の個人的な目標でした。*1
なので、ある程度は確実に作業をすすめるために幾つかチームとして動くためのささやかな作戦を用意していました!
作戦と言えば聞こえは良いですが、チームとして動く上である程度当たり前のことだと思います。

ざっくりとしたスケジュールを決めてとりかかる

マイルストーンが無いと十分に調査が終わらないまま、チューニング作業に入ってしまい、無駄な作業をしてしまう為。
業務でチューニング作業などは殆どやる機会が無い為、勘と経験で乗り切るとかスーパーな事は出来ないので、
なんとなくでやらずに時間に区切りを作って調査を行った。


具体的には以下のような感じ。

時間/時刻 やること
10:00 ~ 12:00 初期状態/ボトルネック調査
12:00 ~ 13:00 お昼食べながら作戦会議
13:00 ~ もくもく
15:00 おやつ
17:30 再起動テスト
18:00 終了

初期調査の役割分担

前回、前々回と同じ作業をみんなでやるなど無駄な事をやりまくっていた為、
ある程度は無駄をなくしたかった為、ある程度決まりきっている作業は事前に役割分担を行い、
調査が終わり、お昼を食べながらチューニング指針を検討し、再度作業分担を行う作戦で行った。


具体的には以下のような感じ。

  • 自分
    • ssh鍵,git化,deploy,restartツール等足回り系を整える
    • ログ調査
    • 各人の突発の役割分担の指示
  • id:masasuz
    • ログ調査(主にMySQLまわり)
  • id:kfly8
    • アプリ仕様調査
    • ログ調査(主にアクセスログまわり)

同時に実行するとマズい作業はロックを取る仕組み

ベンチマークの実行やデプロイは同時に実行すると結果がぶれ、再度計測し直しになったり、タイムロスに繋がります。
今回は1人だけをベンチマークを実行する役にし、他のひとは原則的にベンチマークを実行しないという事にしました。
ただ、これは反省が多く、今回はスクリプトによるベンチマークの実行が可能だったので、スクリプトの実行をwrapしてデプロイツールと共通のファイルで排他ロックを取ってやる感じでシステムでカバー出来たら良かったなと思いました。

ぼくがやったこと

本格的なチューニングはアプリ側は id:kfly8 に、DB側は id:masasuzu におまかせしつつ、
サーバーと足回りの。

  • ツール等足回り系を整える
  • 定期的に「落ち着け」言う
  • nginx入れる
  • bin/makrdownをsystemで呼び出してたのをText::Markdown::Hoedwonに置き換える
    • これと id:kfly8 によるtotal_memosをキャッシュ/incrする変更で2000くらいから5000くらいまで一気にスコアが上がった
    • 提出AMIから起動してrevertしてみたらスコア半分になった
  • StarmanからStarletに変更しkeep-aliveを活用出来るように設定をチューニング
    • max_workersもいくつか試した末、最終的には80にした
    • 冷静に見ると誤差に踊らされた感が否めない
  • deployツールやrestartツールが動かなくなるバグを調査しつつ修正していく
  • やっとこさtoolchain系が安定したと思ったら、時間がなくなってきたのでCache::Memcached::Fastなどのインスタンスをstateに入れるようにするコソドロチューニングを行う
    • ついでにCache::Memcached::Fastのserialize_methods/compress_methodsなどをData::MessagePack/Compress::LZ4に差し替える
  • recent_memosをキャッシュする作戦
    • アプローチがあまりにもあほで失敗に終わる
  • 再起動をかけるも急にスコアが落ちて混乱する
    • supervisordを見落としてデフォルトのStarmanが立ち上がってました!!!111
    • すみませんでした!!!!!!!!!!!!1111
      • 再起動できてボーナスポイント入ってたら最高スコア更新してたはず!!11
    • 設定は直したけど再起動してる時間が無かった為これが最終スコアに
  • 時間切れ間際にボーナスポイントを見付けて対応する
  • 時間切れ


はい!ぼくほとんどなんもできてないですね!!!

今回の反省点

推測するな計測せよ

焦るなつってんだろ!!!!!!!!!111 > 俺

tool chain系は事前に整えておく

すみませんでした!!!!!!!!!!!!1111

リーダーシップを発揮する役目を誰かに

すみませんでした!!!!!!!!!!!!1111

感想

workloadオプションの調整やってなかったチームの事考えると、
ふつーに予選落ちだったんじゃないかとガクブルしています。
準備すごい大変だと思いますし、全員ぶん見るのもっと大変だと思います。
これほど大変なイベントを開催して頂きありがとうございました!!楽しかったです!!

本戦への意気込み

生きて帰れるように頑張ります!!!1111

*1:あと、ITエンジニア平成会のメンバーにスコアで勝つのも目標でしたが、id:mackee_w のチームに負けました。本戦では勝ちにいきます。

*2:推測するな計測せよ違反