1 of 101

Fighting Legacy code

~本当にあったレガシーな話〜

Daisuke Maki

Japan Perl Association / LINE Corporation

2 of 101

お詫び

  • ピンチヒッターで登壇してます
  • スライドが突貫工事で大変申し訳ありません。

3 of 101

http://codezine.jp/book/modernperl2

4 of 101

5 of 101

livedoorBlog

  • 日本最大のブログサービス
    • 多機能
    • サービス開始:2003年(10年!)
    • これまでにかなりの数のエンジニア達が触ってきたコードベース
  • 現在推定 22万行 の(9割方)Perlコード
    • mod_perl 1.3.x
    • perl 5.8.8

6 of 101

livedoorBlog

  • PV/UU非公表(残念)
    • コード書いててちょっと怖いくらいの量はある
    • コンテンツは全て動的、キャッシュを駆使
    • 動的コンテンツ側は 10億req/day
  • サーバーもン百台
    • システムが古いので構成は古風

7 of 101

livedoorBlog

  • これをガリガリと変えたい
    • もっと開発しやすく!
    • もっと運用しやすく!
    • もっとモダンに!
    • とにかくどうにかしろ!
  • …という作業をしてたこの半年くらいの話

8 of 101

どう実現したのか?!

9 of 101

tl;dr

10 of 101

THERE. IS. NO. SILVER. BULLET.

銀の弾丸など存在しない

11 of 101

orz

12 of 101

〜 糸冬 了 〜

13 of 101

tl;dr (2)

14 of 101

Seek, and you shall find

  • ベストでなくともベターなコードを適用していけば実行可能
  • 20万行のコードベースでもモダンなツールに入れ替えていく事は可能です!
  • 本トークでは革新的なツール・手法は出てきません!地道な努力が正義。

15 of 101

inside

16 of 101

コードベース

  • perl-5.8.8
  • mod_perl 1.3.xベッタリ
  • Makefile.PL?なにそれおいしいの?
  • CPAN依存関係が古いッ(6~8年前)
  • 使ってるのかわからないコードもたくさん
  • 絵に描いたような(ry

17 of 101

これまで→これから

  • エンジニア・スタッフの腕と力業でやってきた
    • 注:このトークでは悪い面ばかり書くけど、実際コードを触って運用してみると大分創意工夫が有り、なかなか良いシステムなのは事実
  • ここからはもう少し実装をクリーンにして将来の発展につなげたい

18 of 101

目標

  • perlのバージョンをあげる(今後もあげられるようにする)
  • パッケージングを近代化する
  • mod_perlとさよならする
  • ある程度ブートストラップを自動化する
  • 将来の変更を邪魔する可能性のあるものを可能な限りなくす

19 of 101

要は

20 of 101

ここから

21 of 101

こうしたい

22 of 101

perl upgrade

  • mod_perl 1.3.x built on perl-5.8.8
  • PSGI化するまで入れ替え無理だ…
  • これは長い…

23 of 101

DaveArnoldPhoto.com

24 of 101

システム分割

  • 配信システム (jp2)
    • PC, smartphone, mobile
    • 生Apacheハンドラ
  • コメント、トラックバック等 (blogsys)
    • Sledge
  • CMS
    • Sledge

25 of 101

Road to PSGI

  • まず配信システムをmod_perlからひっぺがそう!
  • (最適化のため)生Apacheハンドラがわんさか → WAF使ってるところより簡単そう

26 of 101

Phase 1: 配信システムをリファクタリング

  • よし、やるぞ!
  • で、このコードは何してるの?
    • 仕様書?HA HA HA!
    • え?ねぇねぇ、ログどこ?どこ?

27 of 101

sub hoge {

my ($self, $mogo, $muga, $moge) = @_;

# warn “Got $mogo $muga $moge”;

if (! $mogo) {

# warn ‘$mogo is FALSE’

}

}

FAIL

28 of 101

ログ

  • 本日一番重要なポイントです

29 of 101

You WILL log. Or else.

http://www.layoutsparks.com

30 of 101

ログ

  • 本日一番重要なポイントです
  • コメントアウトしてるwarn() とか意味無い
  • スイッチ一発でログ出力できない?
    • いちいち# warn直してコミットするとか・・・
    • 最後にコミットするときに消し忘れたらうざい
    • そもそもそんなに面倒くさいとエンジニア達がログを追加したり見たりするのを面倒くさがる

31 of 101

ログ

  • 基本はLog::Minimal
    • もうオブジェクト指向とかそういうレベルの話じゃない
    • とにかくサクッとログを入れらるようにしないと!
  • 本番では最適化したい
    • 必要ないところではログ出力のコードをそもそも消したい
    • 定数畳み込みしましょう

32 of 101

use constant LDBLOGNG_DEBUG

=> $ENV{LDBLOGNG_DEBUG};

use Log::Minimal;

33 of 101

sub hoge {

my ($self, $mogo, $muga, $moge) = @_;

if (LDBLOGNG_DEBUG) {

debugf(“Got %s %s %s”,

$mogo, $muga, $moge);

}

if (! $mogo) {

if (LDBLOGNG_DEBUG) {

debugf(‘$mogo is TRUE’);

}

}

}

34 of 101

ログ

  • 巨大なアプリケーションではログは面倒でも重要な分岐ごとに細かく仕込むべき
    • ログに”x = 1”とか意味のないことを書かない
    • 「○○が1、この後○○をする」とか、ポイントを押さえて
  • コンポーネントが複数絡んでくる場合はマーカーをつけると便利

35 of 101

[START PSGIApp]

[START BlogMapper]

Found blog “lestrrat”, id = 1

[END BlogMApper]

[START Handler::Index]

Cache MISS for key “lestrrat…”

Generating content from DB

[END Handler::Index]

[START Deflater]

Content-Length 12345 > threshold (1096)

Going ahead with compression

[END Deflater]

[END PSGIApp]

...

36 of 101

ログ

  • やっとコードの実行結果を追える!
    • レガシーコードの作業ではまずコードを追えるのがスタートラインだと思います!

37 of 101

tl;dr

mod_perl

38 of 101

mod_perl Yak Shaving

Oh mod_perl,

How do I hate thee?

Let me count the ways.

….

39 of 101

リファクタリング

  • とりあえず野放図なコードを眺める
    • 気が遠くなるが、ふんばる

  • ハンドラの親子関係とかも考え直す

40 of 101

$r の追放

  • $r は邪悪そのもの
  • Apache::Request->instance はさらなる悪
  • $r は多機能すぎる
  • $r は死ぬべき

  • 極力$rはメソッド内に隠蔽、引数として渡す

41 of 101

sub handler : method {

my ($class, $r) = @_;

if (...) {

$r->status(404);

$r->send_http_headers();

} else {

$r->status(200);

$r->send_http_headers();

$r->print($data);

}

return Apache::OK;

}

FAIL

42 of 101

sub handler : method {

my ($class, $r) = @_;

if (...) {

$class->not_found($r);

} else {

$class->output_data($r, {

status => 200, # default

body => $data

});

}

return Apache::OK;

}

43 of 101

クラス関係の見直し

  • mod_perlハンドラはクラスベース
    • それ自体は問題ないが、コピペが横行・・・
  • フラグ・スイッチを親クラスに集約
  • 後々mod_perlハンドラをオブジェクトに変更した際にオブジェクトのアトリビュートとして使えるように設計

44 of 101

とりあえずmod_perlのままデプロイ

  • ログまわりやツールなどが追加
  • クラス関係の変更
  • テスト・確認

45 of 101

FAIL

なぜか404ページが真っ白になる

46 of 101

ErrorDocument

  • mod_perl から ErrorDocument 404 を表示
    • $r->status(404) + return OK (or NOT_FOUND): ステータスは404、でもErrorDocument設定は無視される
    • $r->status(404) + $r->send_http_header + return OK (or NOT_FOUND): ErrorDocumentは送信されるが、ヘッダも2重になる
    • 正解: $r->status(200) + return 404

47 of 101

commit 5f9872cbf992777dd8ec8bd67ea38c945f6ebe9c

Author: Daisuke Maki <....@....>

Date: Mon Dec 17 17:47:32 2012 +0900

ErrorDocumentが表示されない現象を修正(or "Why mod_perl must die")

* mod_perlは$r->statusとハンドラの戻り値の関係が非常に複雑で

場合によっては動かない

例:

* $r->status(404) + return OK (or NOT_FOUND)

-> 404レスポンスは正しいがErrorDocumentがトリガーされない

* $r->status(404) + $r->send_http_header + return OK (or NOT_FOUND)

-> 404レスポンスは送信されるが、ヘッダーが2重送信されるので

格好悪いHTTPレスポンスがtext/plainで表示される

というわけでこのあたりを正しく処理する必要がある

…中略…

* そしてmod_perlは死ぬべき

ちなみに

48 of 101

Phase 2: 配信システム→PSGI

  • Apacheハンドラを全部POPOに
  • Apache::RequestをPlack::Requestに
  • 直す→走らせる→落ちる→直す→走らせる→落ちる

POPO = Plain Old Perl Object

49 of 101

Geest

  • RubyのKageをAnyEventで
    • https://metacpan.org/release/Geest
    • 「ヘイスト」と読みます
  • 本番との差分を見つつ 直す→走らせる→直す→走らせる…
  • ちなみに動作がわかってからはもう使ってない

50 of 101

Plack ツール

  • PSGIにしたので色々使えるものが・・・!
    • File::RotateLogs (rotate every 2h)
    • Plack::Middleware::AxsLog
    • Plack::Middleware::ReverseProxy
    • Plack::Middleware::ErrorDocument
    • ServerStatus::Lite
    • Server::Starter

51 of 101

$perl++

  • perlバージョンをあげるならここだ!
    • perl-5.16.3 へ
    • 7年分の時空を超えた(問題無し。perl++)
  • perlバージョンをあげるなら依存関係も入れ直すから、全部cpanfileで管理だ!
  • cpanfileを使うならcartonも使おう!

52 of 101

Carton + cpanfile

  • cpanfile >= 1.6
    • 全部バージョン指定 requires ‘DBI’ => ‘==1.628’
  • carton < 1.0
    • そのうちあげる予定

53 of 101

依存関係

  • 一応依存モジュールxバージョンわかってたのでそこからしこしこと移転
  • 追加→確認→バージョンあげる→確認・・・

54 of 101

CPANにあがってないモジュール

  • だいたい内製モジュール
  • 社内のあちこちからtarballを探してきてまとめる
  • 微妙なインストール順とかがあるのでcpanfileとかだけで自動化しにくい・・・
    • 注:多分このトークをする直前か直後にcartonとcpanmの最新版をmiyagawaさんがshipitしてる気がするのでそれを使えばこれの90%はgit:// 指定で終了

55 of 101

セットアップ

  • よく考えたらmod_perl → PSGIの移行を開発陣全員に教えるのだるい
  • わかった、PSGI版のコマンドを叩いたら全部セットアップするようにしよう
    • 依存関係のゴタゴタもこのあたりで処理
    • 必要だったらmysqlまでいれちゃうよ!
    • 必要だったらパッチ当ててインストールもしちゃうよ!

56 of 101

DEPLOY

57 of 101

PSGIデプロイ テイク1

  • 数台だけ入れ替え
  • PSGIが動き出した!
  • が、パフォーマンスが30% 落ちた

58 of 101

FAIL

59 of 101

ワーカー数調整

  • CloudForecast見ながら微調整
    • → ワーカー数をあげる
    • → LAが上がる
    • → アラート来まくる
    • →挫折して真面目にコードを読み始める

60 of 101

Devel::NYTProf

  • 一方kazeburoさんがNYTProfでプロファイリング
  • 結構色々見つかる

61 of 101

不要なマウントの削除

  • 一部でmount()を多用していた
  • ハッシュルックアップに変更したらパフォーマンスが2倍になった
    • https://gist.github.com/kazeburo/5087266

62 of 101

Plack::Request (Plack < 1.0018)

  • query_parameters() が重い
    • キャッシュしないので uri() が2回も呼ばれる
  • モンキーパッチしたら 10~15%速くなった
  • 1.0018~ で修正

63 of 101

URL::Encode

  • URL:Encode::XS
    • XSでデコード
    • かっとなってText::QueryStringを書いたが、URL::Encode::XSのほうが速い+正しい
  • Plack::Request::parametersをさらにモンキーパッチして 5~10%改善

64 of 101

Apacheさんは偉かった

  • こうしてみてみると Apacheさんはそれなりに速かった
    • mod_perlからのHTTPリクエストパースは速かった
    • Apache++
  • でも思い出は美しく思えるもの。さよなら…

65 of 101

そして

  • 再デプロイ
  • 全て落ち着く… svc -h するまでは

66 of 101

FAIL

svc -hしたら突然LAがあがる

67 of 101

サーバー再起動

  • Server::Starterで管理してても-hで一気にサーバーが立ち上がってリソースを食う→リクエストが止まる
  • どう見ても既知の問題です
    • http://blog.kazuhooku.com/2011/04/web-serverstarter-parallelprefork.html

68 of 101

carton exec -- \

start_server \

--port 5000 -- \

plackup \

-s Starlet \

--max-workers=80 \

--max-reqs-per-child=500 \

--min-reqs-per-child=350 \

BEFORE

69 of 101

carton exec -- \

start_server \

--port 5000 --signal-on-hup=USR1 -- \

plackup \

-s Starlet \

--max-workers=80 \

--max-reqs-per-child=500 \

--min-reqs-per-child=350 \

--spawn-interval=0.5

AFTER

70 of 101

Slow-restart++

  • 0.5秒ごとに1プロセスあがり、1プロセス死ぬ
  • watch -n 1 ‘ps -ef | grep plackup’ とかしてると大変気持ちよい
  • やっと安心して再起動できる!
    • 実はまだ足りない・・・

71 of 101

RELAX

72 of 101

Phase 3: Sledgeさん

  • 記事作成画面とかはSledgeさん
  • 自分が最後にSledge触ったの2005年や…

73 of 101

Sledge::PSGI

  • 風の噂でちょっと遅いと聞いた
    • (%ENVのコピーがなんちゃら)
  • 極限まで削りたいので、Sledge::PSGI::Dispatcher::Propertiesだけ使って後は自前で書く!
    • LDBlogNG::PSGIHandler::Sledge

74 of 101

Sledgeさんと$r

  • SledgeさんてばApache::Request前提・・・
  • Apacheハンドラの時よりおもしろい使われ方を広域でしている・・・手で書き換えるとかしたくない
  • $r をなんか違うものに入れかえよう!

75 of 101

Sledgeさんと$r

  • CPANのヤツ
    • Plack::App::FakeApache
    • Plack::App::FakeApache1::Request
    • ほしいのとちょっと違う・・・

  • LDBlogNG::FakeApacheReqを自分で作った
  • PSGIアプリからSledgeに↑を渡す

76 of 101

mod_perl

mod_perlの世界へ……

77 of 101

mod_perlさんの世界

  • Apache->param()
    • キーのリストを返す
  • Apache->param($upload_name)
    • アップロードされたファイルの名前を返す
  • Apache->param($name, $value)
    • フォーム引数を代入する

78 of 101

mod_perlさんの世界

  • Apache->upload()
    • リストコンテキスト: 全てのアップロードオブジェクトを返す
    • スカラーコンテキスト:最初のアップロードオブジェクトを返す
  • Apache->upload($name)
    • 指定された名前のアップロードオブジェクト返す

79 of 101

mod_perlさんの世界

  • その他、独自関数たくさん。
  • 全部Plack::Request/Responseにプロキシ
  • うまくマッピングできない場合はがんばる
    • 使い方がおかしい?ものとかはログを吐きまくって書き換えるように文句を書いておく

80 of 101

sub post_dispatch_add {

...

# Expects %ENV!!!!

my %cookies = CGI::Cookie->fetch();

...

}

FAIL

81 of 101

%ENV is a _global_, you...

http://www.layoutsparks.com

82 of 101

%ENV参照

  • CGI::Cookie->fetch
    • $ENV{HTTP_COOKIE}を見る
    • 前に書いたように最適化してるので%ENVにそんなものはない
  • その他のコードも一部%ENVを参照してる・・・
  • 泣きたい。そうだ、泣こう。

83 of 101

%ENV参照

  • 基本local $ENV{...} = $r->header(...)
    • 一部どうしても$env/$rを渡せなかったので、グローバル変数$CURRENT_REQUEST = $r でなんとかしのぐ
  • あと env_guard()というのを作って、この小細工をするとログが出るようにしてXXX FIXME状態に

84 of 101

Sledgeさんは以上

  • あとは確認とか
    • 細かい修正はいくらでもあった
    • けれども根本的な問題はそれくらい
    • 残りのSledgeベースのヤツも同様に・・・
  • あとは意外と簡単に動いた

85 of 101

Phase 4: さらに先へ

  • もう動く事は動いたので、今度は
    • もう少しずつキレイに
    • もう少しずつ便利に

86 of 101

完全停止しないといけない問題

  • start_server はコマンドを「そのまま」fork+execする
  • HUPした時は前のコマンドを引き継いでいる
  • コマンド引数を変えても、HUPでは変わらない・・・

87 of 101

carton exec -- \

start_server \

--port 5000 --signal-on-hup=USR1 -- \

plackup \

-s Starlet \

--max-workers=80 \

--max-reqs-per-child=500 \

--min-reqs-per-child=350 \

--spawn-interval=0.5

aww...

88 of 101

F

89 of 101

FAI

90 of 101

FAIL

(my son nudged me to let him drink my lemonade w/o any sugar)

No babies were harmed during the shoot of this photo

91 of 101

完全停止しないといけない問題

  • 解決方法:start_serverから呼び出されるところに直書きせずにラッパーに包めばよい

92 of 101

carton exec -- \

start_server \

--port 5000 --signal-on-hup=USR1 -- \

bin/jp2_app

step1

93 of 101

# bin/jp2_app

use LDBlogNG::PSGIRunner;

my $runner = LDBlogNG::PSGIRunner->new(

app => 'LDBlogNG::App::JP2App',

);

$runner->run;

step2

94 of 101

完全停止しないといけない問題

  • PSGIRunner でcarton exec server_starterまでの諸々を行う
  • appで渡されたクラスがPlack::Loader->load()して、サーバーを起動
  • あとはappクラスを変更してHUPすれば設定が諸々読み込まれる!

95 of 101

plackupありすぎ問題

  • 一台のマシンにいくつかのPSGIアプリを同居
  • ps | grep plackup だといらないものまでひっかかる
  • $0 を変える! 簡単だけど意外とみんなやらない

96 of 101

$0 = sprintf "%s (%s %s=[%s])",

$0,

$apppath, # etc/app.psgi

$handler_name, # Starlet

join ", ",

map { "$_=$handler_args{$_}" }

keys %handler_args;

# run_psgi (etc/app.psgi Starlet=[max_workers=80 … ])

runner

97 of 101

plackupありすぎ問題

  • さらに処理中のリクエストがわかるとささった時の運用が便利

98 of 101

sub handle_psgi {

my ($self, $env) = @_;

local $0 = sprintf “%s [%s (%s)] %s”,

Scalar::Util::blessed($self),

$env->{REQUEST_URI},

$env->{HTTP_HOST},

scalar localtime

;

}

# HandlerClass [/foo/bar/baz (blog.livedoor.jp)] Tue Sep 17 10:42:03 2013

app

99 of 101

(俺の)野望

  • 会社や上司には相談してない野望
  • もっと大規模なクラスタリング
  • 完全自動テスト
  • キャッシュキーの管理、整理、抜き出し
  • データの正規化
  • データベースのアップグレード
  • DEBUGのかわりにDTrace

100 of 101

http://codezine.jp/book/modernperl2

101 of 101

(俺の)野望

  • 「増補改訂」とか言ってるけど実質8割方書き直しです。
  • 時代の流れって速い
  • 今回のトークの内容が大分入ってます