Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

ラクーンのエンジニア評価制度について

こんにちは、たむらです。
今回は、ラクーンのエンジニア評価制度についてのアレコレを書こうと思います。
ラクーンがエンジニアに活き活きと仕事してもらう為にどの様に評価制度を考えているかを知ってもらうキッカケになればと思っています。

評価制度の一般論
 さて、会社の評価制度というものはエンジニアという職種に限らなくてもどの会社でも非常に苦慮して作り上げているものです。働くすべての人のあらゆる状況を正しく評価することはとても難しく、過不足やいびつさを抱えているものが殆どなのではないかと思います。みなさんの会社ではどの様な評価制度で評価されているでしょうか?またその評価制度に満足されていますか?
 私は人事の専門家では無いので正確性は危ういところがありますが、経験上エンジニア界隈で適用されている評価制度は以下の様なものが一般的かと思います。
1. 職能給制度
  スキルレベルにより評価されるものです。号棒と呼ばれるレベル表を定義し、その昇降により給与(=評価)を決めるような方式になります。スキル評価としての建前がありますが、実際にスキルを詳細に定義することは難しく、得てして年功序列的な運用になりやすい面があります。
2. 成果主義による評価制度
  その名の通り、業務の過程は考慮せず、成果のみによって評価する方式です。売上や利益といったもので定義されることが多く、評価基準の分かりやすさがメリットと言えます。エンジニア職種に対しては絶対的な評価基準は定義し難い為、指標が多岐にわたることが殆どです。その為分かりやすさは一段劣る印象があります。
3.目標管理との連携
  多くは期初に業務に則した目標を立て、その達成度により評価をします。個々人のレベルに合せてそれぞれが定義することが多く、成果主義を属人化させたものといえるかもしれません。売上等の統一の評価基準を用いない場合はメンバー間での絶対的な評価が分かりにくくなる傾向にあります。

実際のところ多くの企業では、上述のどれかを適用しているというよりは、それぞれの良い所をうまく取り入れつつ評価制度を組み立てているところが殆どなのではないかと思います。中には名目上は「成果主義」だが、実際にはろくに予実管理も面談もせずに上司の胸先三寸で決まる「ブラックボックス評価制度」というところもあるのかもしれません。


過去のラクーンのエンジニア評価制度
 ではラクーンではどうなのかというと、過去ラクーンでは上で述べたものの1つである「目標管理と連携した評価制度」をエンジニアの評価に利用していました。しかし、この制度には職務的に合わない部分があり見直されることになりました。
 合わない点は2点あります。1つ目は業務内容が目標設定時から変化することが多い点です。例えば目標設定時はプロジェクトAに参画することが予定されていた為、「プロジェクトAでの不具合摘出率を前回参画プロジェクトの半分にする」という目標を立てたが、プロジェクトA自体が実施されなかった、というようなケースです。ラクーンでは実施する案件はその時の状況により都度見直しが入るので、この様な目標倒れがしばしば起きました。
 2つ目は実業務との乖離です。1つ目の反省を踏まえて、目標を実業務から離れたものに設定したことがありました。例えば「未経験のRuby on Railsをマスターし、公開アプリを作成する」などです。業務の隙間時間でスキルを向上させてもらい、その成長を評価しようとの目論見でしたが、逆に本来評価の対象とすべき実業務の評価と離れたものとなってしまい、妥当性を欠く結果となってしまいました。


評価制度見直しに際してのポリシー
 この様な経緯から然るべき評価制度に対しての必要性が高まったのを踏まえて、評価制度の見直しが行われることになりました。策定にあたりポリシーとしたのは以下の点です。
「エンジニアのスキルアップの指針(キャリアパス)となり得るもの」
「客観性・納得感があるもの」
「エンジニア以外に対しても分かり易い評価軸を持ったもの」
「モチベーションの向上に繋がるもの」

このポリシーに則り、実際に働くメンバーにも検討に加わってもらいながら、評価制度の見直しを行いました。
その結果、現在適用している評価制度は「スキルベース評価」、「スキルシート管理」、「資格制度」、「360度評価」に表されるものです。順に概要を説明させてもらいます。


スキルベース評価
 まず、前提とするのはラクーンでのエンジニア評価はスキルをメインとしているということです。エンジニアという職種である以上、その技術力を評価の中心に据えようという思いがあります。そこで観点とする「スキル項目」を選定し、それに沿って評価することを評価の主軸としました。「スキル項目」は大きく分けて5つのカテゴリに分かれており、①技術スキル、②言語スキル、③ヒューマンスキル、④業務スキル、⑤アドオンスキル となっています。①~③は6段階評価、④、⑤は3段階評価としています。

スキルカテゴリの概要
スキルカテゴリ














評価の際に一番重きを置かれるのは ①技術スキル になります。なぜ ②言語スキル や ④業務スキル が入らないのかというと、言語やシステムは新陳代謝がある為です。そこで、よりエンジニアスキルの本質となる要素にターゲットを絞ることで、評価に恒常性をもたせることを意図しています。


スキルシート管理
 スキル項目に沿って、全エンジニアのスキルをスキルシートという表にまとめ、一覧できる様にしています。これは部門内で誰でも参照できる様にしていてスキルの見える化に繋げています。但し、過剰に上下関係を表し人間関係が崩れてしまうことがないように、①技術スキル~③ヒューマンスキル に関しての数値は評価者及び本人以外には公開せず、全体公開するのはある一定のレベル以上かどうかのみ公開するようにしています。
 元々見える化というのは会社にエンジニアの評価を分かり易く伝えたいという意図の他に、メンバー間での教育関係が生まれやすくすることも目的の一つとしています。その為メンバー間においては上下関係を知るというよりも誰に聞けばいいのか?誰が教えるべきなのかが分かることが見える化の大事なポイントだと考えています。

実際のスキルシート
スキルシート


















資格制度
 ラクーンでは会社独自の資格制度を持っていて、その中に「特定分野の専門知識、経験を持つ人」に与えられるスペシャリスト資格(社内では通称S資格と呼んでいます)が存在します。レベルによりS1,S2と2段階あり、年収の想定レンジで言えば750万円~1000万円以上をイメージしています。
技術戦略部のS資格としては、シニアエンジニアや、プロジェクトリーダ、インフラエンジニア等が定義されています。
 さて、このS資格はそれぞれ資格取得条件を定義することになるのですが、それをスキルレベルと紐付ける形で定義しています。それにより、「自身の現在のスキル」、「伸ばすべきスキルの方向性」、「会社的な評価(S資格の取得)」が同じ軸で考えられるようにしています。


技術部で現在用意しているS資格とその資格条件
スキルマップ1







スキルマップ2









360度評価
 評価査定は年に2回半期毎に行いますが、その時に上述のスキルシートの更新を行い、評価資料として用います。
スキルシートの更新の際にはなるべく多角的な評価を集めるため、メンバー間相互評価を集めたり、資格所有者に更新内容の妥当性の検証をしてもらったりといった360度評価の仕組みを取り入れています。
上司からの評価だけだと実業務の目線から離れていることも多く、実際の技術スキルを測るには限界があります。その補完としてメンバー同士からの評価を用いています。また、いつも進捗が遅れがちだが、実際には他メンバーの相談や質問に親身にのっていて他メンバーからの信頼がとても厚い人等、目に見える成果では測れない貢献者を認知する手段にもなっています。

まとめ
 ザックリとした説明になってしまいましたが、こんな仕組みでラクーンではエンジニア評価をしています。
この評価方針はここ1年位で見直した結果なのですが、この事実が物語る通り、今後も必要があれば都度見直しをしてどんどん変化していくことになると思います。ただ、あくまでその目的は、エンジニアが活き活きと仕事ができることや、長くラクーンで働いていきたいと思えることに繋がるべきであると思っています。

最後に・・・
私達は一緒に働く仲間を随時募集しています。
この評価制度の話なり、ラクーンのビジネスモデルなりをちょっとでも面白そうだなと思ってくれた方、是非一緒に働いてみませんか?
ご応募お待ちしております!!

DoS攻撃からサーバーを守る、mod_dosdetector導入事例のご紹介

こんにちは、羽山です。今回はDoS攻撃の話題です。

弊社の運営するファッション・雑貨向けB2Bサイトスーパーデリバリー(以下 SUPER DELIVERY)にはブラウザからの普通のアクセスを逸脱した過度なHTTPリクエストがしばしばやってきます。大半は独自クローラーなどの悪意がある攻撃とは言えないものですがサイトに与える負荷は高く対応の必要がありました。
 
今回の記事では弊社で行ったDoS攻撃対策の顛末をお送りします。
記事の性質上、設定内容など一部ぼやかして表現している点もありますがご容赦ください。
また、当記事では過度なHTTPリクエストをすべてDoS攻撃という呼称で統一しています。


DoS攻撃とは?
そもそもDoS攻撃と一口に言ってもTCPレベルからHTTPレベルまで様々な手法が存在します。
例えばTCPレベルで有名なのはSYN floodという攻撃で、接続開始を表すSYNパケットを攻撃対象に大量に送りつけることでサーバーのリソースを大量に消費させます。しかし幸いなことにTCPレベルのDoS攻撃の多くはFirewallやLoadBalancer(以下LB)に保護機能があることが多く、特別な対策を意識するケースは減ってきています。
しかしその一方で問題が顕在化しているのはHTTPレベルのDoS攻撃です。例えば「正常なHTTPアクセスを大量に行う」などステートレスな手法では防げないものが多いため、防御する仕組みや機器は複雑・高度・高価になりがちです。 


サーバー構成の説明
まずは対策を行う弊社のサーバ環境を簡単に説明します。
以下の図のようにFirawallとL7のLB以下に複数台のウェブサーバが配置されて、LBから各ウェブサーバはリバースプロキシの構成をとっています。
ウェブサーバは主にApache2.2/2.4系を利用しています。

l7proxy

対策方法の検討
1. 専用アプライアンスの導入
ある程度の予算がとれるならばDoS対策専用のアプライアンス導入が第一候補として考えられます。
すでに多岐にわたるアプライアンスが出そろっていてある程度成熟しているといえるでしょう。
シグネチャや振る舞いに基づいた保護を基礎として、SQLインジェクション[1]などのWEBアプリケーションの脆弱性まで保護してくれるものもあります。

[1] SQLインジェクションなどはアプリ側できちんと対策する前提ではありますが、アプライアンス導入には安全性を保証する客観的な指標となるメリットがあります。DoS対策アプライアンスは元々L7をベースに動作しているためHTTPプロトコルを詳細に解析するたぐいの機能追加とは相性がよいことから、付加価値を高めるために両者が同時に提供されることがあります。


弊社の場合、「まずはDoS対策を導入」というステージだったため、いきなり大きく予算を取るリスクを避けたかったこともありアプライアンスの導入は保留にして別の案を検討しました。

2. 各ノードで個別にDoS検査
次に検討したのは各ノード、つまりウェブサーバのApacheレベルでの検査です。各ノードでの検査の場合はソフトウェアで対応できるので必要な費用を最低限におさえることができます。
DoS攻撃によるアクセスがLBによって各サーバに分散されてしまうため検出しにくいという弱点がありますが、幸いSUPER DELIVERYのサーバはLBがノードを選択する際に持続接続機能を利用していて同一クライアントは同一ノードに転送されるため、ノード側でのDoS検査でも上流での検査とさほど変わらないレベルで実施可能でした。
Apache側での防御手段を調べたところmod_dosdetectorというid:stanaka氏が開発・公開してくださっているモジュールがApache2.2/2.4で利用可能で弊社の要件にも近いということが分かりました。
LB配下では接続元IPがLBの内部IPに置き換えられてしまい利用できないためX-Forwarded-Forヘッダを代わりに確認する必要がありますが、dosdetectorはその機能にも対応しています。

まずはこの案を採用してみることになりました。


mod_dosdetectorの基本機能
すでにいくつものサイトで紹介されているので詳細は省略しますが、基本的には下記のような動作をします。
1. クライアントのIPアドレス一覧を共有メモリに保持
2. IPに紐付けてアクセス数やアクセス時間を記録
3. 一定時間内のアクセス数が設定した閾値を超えたらSuspectDoSという環境変数に1をセットする

mod_dosdetector自体の動作は以上で終わりです。
環境変数に入れるだけの動作なのでその後はmod_rewriteなど既存のモジュールで柔軟に処理できます。


いくつか課題が発生
基本機能はほぼ要件にマッチしたのですが、いくつか解決が必要な課題が出てきました。
以下に挙げる点はrewriteで頑張れば回避できるものもありますが、複雑なrewriteルールを避けたかったため、結論としてはmod_dosdetector自体を改修して機能追加することで解決しました。
弊社で利用しているモジュールを記事の最下部でダウンロードできるようにしています。
LB配下の一般的な環境で使いやすいよう最適化しています、無保証ですが興味ある方はご利用ください。

課題1 X-Forwarded-ForからのIP抽出方法
mod_dosdetectorは接続元IPの代わりにX-Forwarded-Forを参照するDoSForwardedというオプションがあります。
基本的にはこの設定で問題ないのですが、X-Forwarded-Forに複数のIPが含まれるパターンで問題となる場合があります。

mod_dosdetectorのX-Forwarded-ForからIPの選択方法は下記のようになっています。
1. X-Forwarded-Forの先頭からカンマ(含まない場合は末尾)までの文字列をIPとして利用
2. X-Forwarded-Forヘッダが存在していて、その文字列が有効なIPではなかった場合はDoS検査の対象外になる

ここで2点問題が出てきますが、その話の前にX-Forwarded-Forヘッダの一般的な動きを理解しましょう。
一般的にプロキシサーバやLBを経由するとX-Forwarded-Forの末尾に接続元IPを追加して、接続元IPを自身のIPとした新しいHTTPリクエストを目的のサーバに送信します。

X-Forwarded-Forヘッダーについて

通常は社内プロキシで転送する場合はX-Forwarded-Forに内部IPを残さない設定にしますが、設定が不十分でそのまま残ってしまってるケースもよくあります。
この例では最終的なX-Forwarded-Forの値は192.168.0.100, 203.0.113.54, 198.51.100.23となるため、検査対象のIPアドレスを先頭から取得すると正しい検査を行えません。

dosdetectorの標準動作である先頭のIPを採用した場合、下記の2つの問題が発生します。

1. 社内プロキシサーバ経由のアクセスの場合に内部ネットワークのIPアドレスが入っているケースがある
2. X-Forwarded-Forヘッダは容易に偽造可能

1つめは不適切なIPアドレスを元にDoS検査を行ってしまうことになり、全く関係のないクライアント同士が同一IPという扱いを受けてしまったり、内部IPを無視する設定を行った場合は検査が作動しなかったりします。
2つめは根本的な問題で、原則としてSUPER DELIVERYネットワークの管理下にあるLBが付与した一番最後のIP以外はすべて信頼できません。それ以外のIPは偽造可能なため例えばX-Forwarded-Forヘッダに毎回異なるIPを適当に設定してアクセスすれば実質DoS検査を無効化できてしまいます。

課題2 X-Forwarded-Forが存在しない場合
X-Forwarded-Forヘッダが存在しなかった場合、dosdetectorは接続元IPを利用してDoS検査を行います。
しかし一般的にLBからのリバースプロキシ構成ではX-Forwarded-Forヘッダを常に付与する設定にできるので、LBを経由するアクセスなのにX-Forwarded-Forヘッダが付与されないということはありません。

しかし各ノードに流れてくる一部のアクセスにはX-Forwarded-Forヘッダが付与されないことがあります。それらはLBから生存確認のために送るheartbeatであったり、サーバー間通信などシステム内部的なアクセスです。X-Forwarded-Forヘッダがない場合に接続元IPを代わりに利用してしまうと、これらの通信を阻害してしまい問題になることがあります。

課題3 DoS検査の対象について
今回DoS検査の対象としたサーバではいくつかのシステムが並列で稼働していて、そのうち一部をDoS検査から除外する必要がありました。それらはシステムが内部的にAjaxで呼び出すAPIなど、連続で叩かれることが元々想定されているものなどです。
根本的にはアクセス頻度の大きく異なるサービスが同一のサーバに同居していることが問題なんですが、レガシーなシステムも含むためあまり手を入れずに対処したいという事情もありました。
dosdetectorは検査対象がVirtualHost単位になるため、ディレクトリ毎に対象・除外の設定を行うことはできません。
mod_rewriteで除外したいパスを表面上素通しすることは可能ですがDoS検査自体は動作するため、対象外のページでもアクセスがカウントされ、そのまま検査対象のページに移動したらDoS検査に引っかかるなんてことがおきてしまいます。

課題4 社内IP
当然ですが弊社社内の人間は頻繁にSUPER DELIVERYにアクセスします。
そして社内ネットワークのグローバルIPは1つなので簡単にDoS検査に引っかかってしまうことが分かりました。mod_rewriteで省くことは簡単なのですが、mod_rewriteの設定がごちゃごちゃするのでどうせならこれも要件に入れちゃえ!と、ついでに入れました。


モジュールの変更ポイント
<DoSIgnoreIpRange>
※複数行で設定可能
DoS検査の対象から除外するIP範囲を指定可能で、下記3種類の指定方法を認識します。
・192.168.0.0/16
・10.0.0.0/255.0.0.0
・203.0.113.56

<IncludePath/ExcludePath>
※複数行で設定可能
DoS検査を行う対象をrequest_uriで絞り込みます。
1. ExcludePath優先、ExcludePathにマッチしたらDoS検査を行わない
2. IncludePathを次に検査、IncludePathが1つも設定されていない場合はIncludePathに / が指定されているのと同じ動作をする
3. IncludePathが1つ以上ある場合は明示的にIncludePathに指定されたパスのみDoS検査の対象とする

IncludePath/ExcludePathともに、複数設定した場合はOR結合です。

X-Forwarded-Forに複数IPを含む場合の処理を変更
X-Forwarded-Forヘッダの末尾からIP候補を探します。カンマ区切りで順に取得して、IPとして無効な文字列やDoSIgnoreIpRangeに含まれるIPを除外した最初(より末尾に近い)のIPをDoS検査対象とします。
これで X-Forwarded-Forヘッダを偽装されても正しくDoS検査を行うことができます。

DoSForwardedがOnでX-Forwarded-Forが存在しない場合の処理を変更
DoSForwardedがONの場合は、X-Forwarded-Forが存在しなければDoS検査を行わないよう変更しました。


導入
ダウンロードして適当なディレクトリに解凍

同じディレクトリに上記ファイルをダウンロード

apxsのパスが異なる場合はMakefileを修正します。
以下、パッケージ版のapacheを利用している前提で進めます。

$ patch < mod_dosdetector.patch
$ make
$ make install
$ vi /etc/conf/httpd.conf
モジュールが読み込まれていることを確認します。 
LoadModule dosdetector_module modules/mod_dosdetector.so
具体的な数値や設定を載せるといろいろと怒られるのであくまで設定例としてご紹介します。
下記は弊社で実際に運用している設定とは異なります。
 
DoSDetection On
DoS検査機能をOn
 
DoSForwarded On
接続元IPの代わりにX-Forwarded-Forを利用する場合はOn
 
DoSPeriod 60
DoSThreshold 200
DoSHardThreshold 300
DoSBanPeriod 30
DoSTableSize 100
DoSPeriodはDoS検出する時間単位で、指定した秒数の間にDoSThreshold回以上のアクセスがあるとSuspectDoS=1がセットされます。
さらに、DoSHardThreshold回以上のアクセスがあるとSuspectHardDoS=1もセットされます。
DoS判定されるとDoSBanPeriodで指定した秒数の間SuspectDoS=1がセットされ続け、その後いったん規制がクリアされます。
DoSTableSizeは保持するIP一覧の数で、この数だけ共有メモリに領域が確保されます。そのため多くすれば検出の幅は広がりますが、IP一覧からIPを検索する負荷が上昇するため効率が下がります。IP一覧はLRUで管理されるため頻繁にアクセスしてくるDoS攻撃のIPは残りやすため、よほど高負荷な環境でなければ100もあれば十分だと考えられます。

DoSIgnoreContentType ^(image/|application/|text/javascript|text/css)
Apacheがローカルで解決できるレベルでコンテンツタイプによる除外を行います。
しかし、mod_proxyなど外部リソースから動的にコンテンツタイプが返却される場合は除外できないため、この設定に頼ってしまうと危険です。動的コンテンツの場合は後述のmod_rewriteで除外することを検討します。

DoSIgnoreIpRange 192.168.0.0/16
DoSIgnoreIpRange 172.16.0.0/12
DoSIgnoreIpRange 10.0.0.0/8
DoSIgnoreIpRange 203.0.113.56
内部IPと特定のIP(社内ネットワークのグローバルIPを想定)を除外する例です。サーバー間でなんらか直接やりとりしている場合やLBからのheartbeatなどのために内部IPは除外しておいた方が無難です。必要に応じて社内ネットワークのグローバルIPなども除外します。

DoSIncludePath /path1/
DoSIncludePath /path2/
DoSExcludePath /path1/exclude1
DoSExcludePath /path1/exclude2
DoSIncludePathにはDoS検査対象のパスを前方一致で指定、未指定の場合はすべてのパスが対象となります。
DoSExcludePathにはDoS検査対象から除外するパスを前方一致で指定します。
DoSExcludePathがDoSIncludePathよりも優先され、両方とも複数のパスを設定した場合はOR結合となります。

RewriteCond %{ENV:SuspectDoS} =1
RewriteCond %{HTTP_USER_AGENT} !googlebot [NC]
RewriteRule .  - [E=DoS:1]
dosdetectorの結果を受け取ってmod_rewriteで除外する例です。
ここではSuspectDoS変数に1がセットされていて、かつユーザーエージェントにgooglebotを含まない場合を最終的なDoS対象としていて、新しい変数DoSに1をセットします。

ErrorDocument 500 /error503.html
RewriteCond %{ENV:DoS} =1
RewriteCond %{REQUEST_URI} !=/error503.html
RewriteRule . - [R=503,L]
CustomLog /var/log/httpd/sd-static-dosdetector.log combinedr env=DoS
最終的なアクションです。DoS=1がセットされている場合は503ページを表示しつつログも出力します。
503ページは /error503.html というパスに存在する前提です。


最後に
本モジュールを導入して2ヶ月以上経ちますが問題なく安定稼働しています。
大規模サイトでは当たり前のように行われているであろうDoS攻撃への対策ですが、中・小規模のサイトではなかなかノウハウを含め情報が行き渡っていないのではないでしょうか?
かく言うSUPER DELIVERYでもようやく必要性が出てきたところでした。この記事がそういったステージの方の一助になることを願います。
 
最後になりましたが、 非常に有用なモジュールを開発および公開してくださっているid:stanaka氏に感謝いたします。

RubyのPDFライブラリ "Prawn" のご紹介

こんにちは。開発チームのハッタです。
前職が業務系SIerだったこともあり、主にバックオフィス系のシステムを担当しています。
好きな言語はVB.NETとPL/SQLです。

業務系システムといえば必ず出てくるのが帳票類です。
.NETならGUIでサクサク作れるのですが、Webサービスメインの会社では雰囲気的にそうもいかないことが多いです。
(自分ひとりで保守を続けることになったり、、)

そこで、郷に入っては郷に従えの精神でWeb系言語による帳票開発をやってみました。

今回は、言語はRuby、ライブラリはPrawnを使ってPDFを作成します。
Prawnはググった限りでは数年前からあるようなのですが、日本語の情報があまりありません。
この記事が、リファレンスとしてみなさんのお役に立てれば幸いです。

導入
gemでインストールし、requireするだけです。

帳票レイアウトを決める
帳票のサイズや向き、上下左右の余白はコンストラクタで指定します。
  Prawn::Document.new(:page_size => 'A4',
                      :page_layout => :portrait,
                      :top_margin => 40,
                      :bottom_margin => 30,
                      :left_margin => 20,
                      :right_margin => 10)
または、
  Prawn::Document.generate("test.pdf",
                          :page_size => 'A4',
                          :page_layout => :portrait,
                          :top_margin => 40,
                          :bottom_margin => 30,
                          :left_margin => 20,
                          :right_margin => 10)
という書き方をします。
generate()はオブジェクトの生成と、指定したファイルパスへの保存を行います。

オプションの説明:
  :page_size      帳票のサイズです。'A4'、'B5'といった規格の他、[100, 200]という指定もできます。[横, 縦]、単位はポイント(pt)です。
  :page_layout    帳票の向きを縦 or 横で指定します。縦 = :portrait、横 = :landscape です。
  :top_margin     上余白をptで指定します。
  :bottom_margin  下余白をptで指定します。
  :left_margin    左余白をptで指定します。
  :right_margin   右余白をptで指定します。

上記のコードで作られるPDFの余白と印刷領域を塗り分けると、以下のようなイメージになります。
01_margin

































帳票のレイアウトを決めたら、文字や線を描いていきます。
それらの処理は以下のようにblockで渡す形で書きます。
  Prawn::Document.new(...) do |pdf|
    pdf.foo ...
  end

座標を把握する
PrawnではX座標、Y座標をptで指定して、文字や線の出力位置を決めます。

Xの値は、印刷領域の左端が起点です。
印刷領域の右端は、
 pdf.bounds.right
で取得できます。
Yの値は、印刷領域の下端が起点です。
印刷領域の上端は、
 pdf.bounds.top
で取得できます。

また、
 pdf.y
 pdf.cursor
で、現在のポインタのY座標が取得できます。
オブジェクト生成時の初期値は、印刷領域の上端 = pdf.bounds.topと同値が設定されています。

この値は、後述する文字出力などの処理を行うと、下方向へ移動していきます。(数値が減少)
y:bottom_marginを考慮せず帳票下端からの位置、
cursorは:bottom_marginを含めた下端からの位置になります。

以下のようなコードで、ポインタのY座標を動かすことができます。
  pdf.move_up 100           #現在位置から100pt上へ移動
  pdf.move_down 100         #現在位置から100pt下へ移動
  pdf.move_cursor_to(100)   #帳票下端から(100 + :bottom_margin)pt上の位置へ移動
上下左右に100ptの余白を設定したA4横帳票で、
座標移動処理を実行した後のyおよびcursorの位置は以下の図のようになります。
(枠線は印刷領域と余白の境界を表しています。)

02_point

















ページヘッダ、フッタを出力するには
ページヘッダ、フッタの出力など、毎ページ同じ処理を行いたい場合はrepeat()を使います。
  pdf.repeat :all do
    ...
  end
block内に文字や線の出力など、各種処理を書きます。
上記の例では、:allで全ページ対象、他に:oddで奇数ページ、:evenで偶数ページといった指定もできます。

後述するグリッド処理(table())で改ページが発生すると、
次のページは印刷領域の上端からグリッドの印字が始まるため、
repeat()内で行う出力に印刷領域内の座標を指定すると、
グリッドと重なってしまいます。
その場合は、座標にpdf.bounds.topを超える値を指定して、
上余白部分へ印字することで対応できます。

フォントを指定する
まず、文字を出力する際に必要となるのがフォントです。
Prawnでは何も指定しない場合、Helveticaが使われます。

日本語出力をしたい場合など、別のフォントを使いたい場合は以下のように書きます。
  pdf.font "/foo/bar.ttf", :size => 10
指定するファイルは.ttfファイルです。サイズの指定は省略できます。デフォルトでは12ptになります。
フォントファイルを指定後、サイズのみ変更したい場合は
  pdf.font_size = 10  #ptで指定
と書きます。

文字を出力する際、縦方向に必要となる長さは
  pdf.height_of(string)
で取得できます。
横方向は
  pdf.font.compute_width_of(string)
で取得できます。
これらを使うと、下図のように文字列と同じ長さの線を引くことなどが容易になります。
03_font










文字を出力する
Prawnには文字を出力するメソッドが3つあります。

1. text
  pdf.text 'foo'
  [0, pdf.cursor]の位置に指定された文字を出力します。
  文字の上端が指定されたY座標に当たる形で出力されます。
  このメソッドを実行すると、文字列1行分(= height_of(text))、Y座標が下に移動します。

2. draw_text
  pdf.draw_text 'foo', :at => [100, 200]
  :atに指定された座標に文字を出力します。
  文字の下端が指定されたY座標に当たる形で出力されます。
  このメソッドは、Y座標の移動をしません。

3. text_box
  pdf.text_box "foo", :at => [100, 200], :width => 200, :height => 100, :align => :center, :valign => :center
  :atに指定された座標を起点として、:width:heightで指定された枠内に文字を出力します。
  文字列が1行に収まらない場合、:heightの範囲内で折り返して出力されます。
  枠内に収まらない部分は切り捨てられ、戻り値として返します。
  文字列に半角スペースがある場合、それを区切り文字として単語ごとに分解されるため、想定外の箇所で折り返されることがあります。
  その場合は出力する文字列の半角スペースを以下のように置き換えることで、単語分解されずに出力されます。
    string.gsub(" ", Prawn::Text::NBSP)
  :alignは横の位置、:valignは縦の位置を指定できます。
  帳票タイトルを印字する場合に、:widthに印刷領域の最大幅を指定し、:align => :centerをするなどの活用方法があります。

  文字の上端が指定されたY座標に当たる形で出力されます。
  このメソッドは、Y座標の移動をしません。

各メソッドの座標の動きや、text_box()の折り返しをまとめると、以下の図のようになります。
04_text


















線を引く
線の始点と終点を指定するline()、線を描画するstroke()を実行します。
  pdf.line([0, 0], [0, 100])      #左辺
  pdf.line([0, 0], [100, 0])      #下辺
  pdf.line([100, 0], [100, 100])  #右辺
  pdf.line([0, 100], [100, 100])  #上辺
  pdf.stroke                      #描画
上記のコードで、帳票左下に一辺100ptの正方形が描画されます。

線の太さや色の変更、破線の出力もできます。
  pdf.line_width = 2          #太さ
  pdf.dash = 2                #破線間隔
  pdf.stroke_color("FF0000")  #色
太さ、破線はpt、色はRGB16進数で指定します。
太さ等はstroke()が実行される直前に指定された値が採用されます。
太さ、破線間隔ともに指定する数値の単位はptです。

破線を実線に戻す場合は、
  pdf.undash
と書きます。

line_width = 1..3dash = 1..3 をそれぞれ出力すると、以下のようになります。
06_line



















複数の出力項目をグルーピングする

bounding_box()というメソッドを使って枠を定義し、
その中の相対的な座標位置で文字出力などを行うことができます。

たとえば、
  pdf.bounding_box([0, 100], :width => 160, :height => 90) do
    pdf.draw_text "★", :at => [0, 0]
  end
上記のコードでは、
"★"は帳票全体の左下ではなく、
帳票下端から100pt上の位置に置かれた高さ:90pt、幅:160ptの枠の左下に出力されます。
block内でpdf.ypdf.cursorを実行した場合も、枠内の相対的な座標を取得します。

bounding_box()に渡す座標位置を変えることで、
枠内の文字や線の位置関係は変えずに、帳票全体の中での出力位置を変えることができます。
たとえば、
差出人の情報と宛先の情報がある帳票で、
それぞれの住所・氏名等をそれぞれのbounding_box内でまとめておけば、
位置の入替などが容易に行えます。

block内で
  pdf.stroke_bounds
と書くことで、枠線を出力することもできます。
2つbounding_boxを作成し、その中に同じ座標を指定した文字出力処理を書いた場合、
以下のように親のbounding_boxに依存して出力位置が変わります。
07_bounding_box


















データグリッドを出力する
今までにご紹介したtext()line()を駆使すれば、データグリッドを作成することは可能ですが、
Prawnにはtable()という簡単にグリッドを作成できるメソッドがあります。
  pdf.table(
    data,
    :column_widths => [70, 80, 90],
    :header => true,
    :row_colors => ["FF0000", "0000FF"]
  ) do |t|
    t.cells.border_width = 0.1
    t.columns(0).style :align => :left
    t.columns(1).style :align => :center
    t.columns(2).style :align => :right
    t.row(0).style :align => :center, :background_color => "CCCCCC"
  end
dataには二次元配列を渡します。
ヘッダ行を出力したい場合は、dataの先頭にヘッダ行の配列を格納します。

:column_widthsには各列の幅を指定します。
省略された場合は、最長データに合わせて幅が設定されます。
最長データよりも短い幅を指定した場合は、折り返して出力します。

:headerにはdata[0]の値をヘッダ行として扱うかを指定します。
trueが指定された場合は、上記のコード例でblockに書いた
  t.row(0).style :align => :center, :background_color => "CCCCCC"
などのような、ヘッダ行個別の設定が有効になります。
falseまたは指定なしの場合、ヘッダ行個別の設定は効きませんが、data[0]の値自体は明細行の先頭として出力されます。

:row_colorsには明細行の色を指定します。
明細行が交互に配列で指定された色で塗られます。
配列には3種類以上の色も指定可能で、その場合は3行、4行と指定された色の数ごとに明細行の色が分かれます。

blockでは、上記のコード例のようにグリッド全体や各列のスタイルなどを設定できます。

table()の出力位置は、[0, pdf.cursor]となります。
出力位置を調整する場合は、move_down()bounding_box()を使う必要があります。

データ件数が多く、明細行が改ページ位置にかかった場合は、自動的に改ページされます。

ページトップから出力した場合と、bounding_boxを使って特定の位置に出力する場合の例です。
08_table



















以上、一般的な帳票を作る上で基本となりそうな部分を紹介させていただきました。
これらを組み合わせるだけでも、以下のような簡単な請求書などが作れます。
99_sample



































上図作成時に実際に書いたコードです。
フォントはIPAフォントを使っています。
Prawn::Document.generate("99_sample.pdf",
                        :page_size => 'A4',
                        :page_layout => :portrait,
                        :top_margin => 170,
                        :bottom_margin => 20,
                        :left_margin => 20,
                        :right_margin => 20,
                        :compress => true
                        ) do |pdf|
  total = 0
  data = []
  data.push ["商品名", "単価", "数量", "金額"]
  w = (pdf.bounds.right / 10).floor
  ws = [w*5, w*2, w, w*2]
  for i in 1..20 do
    data.push [
      "商品 #{"%06d" % i}",
      (i * 100).to_s.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse,
      i,
      (i * i * 100).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
      ]
    total += (i * i * 100)
  end

  page_head = 150
  pdf.font "ipam.ttf"
  pdf.repeat :all do
    pdf.font_size = 20
    text = "請   求   書"
    y = pdf.cursor + page_head
    pdf.text_box text, :at => [0, y], :width => pdf.bounds.right, :align => :center
    y -= 40

    pdf.font_size = 16
    pdf.text_box("発行日:#{Time.now.strftime("%Y年%m月%d日")}",
                :at => [0, y],
                :width => pdf.bounds.right,
                :align => :right)
    y -= 25

    w = pdf.bounds.right / 2
    h = pdf.height_of(text) * 3
    pdf.bounding_box [0, y], :width => w, :height => h do
      pdf.text "【宛先】"
      pdf.text "株式会社○○ 様"
    end
    pdf.bounding_box [w, y], :width => w, :height => h do
      pdf.text "【差出人】"
      pdf.text "株式会社□□"
    end

    y -= h
    pdf.font_size = 18
    text = "合計金額: \\#{total.to_s.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse}"
    pdf.text_box(text,
                :at => [0, y],
                :width => pdf.bounds.right,
                :align => :right)
    y -= pdf.height_of("a")
    x1 = pdf.bounds.right - pdf.font.compute_width_of(text)
    x2 = pdf.bounds.right
    pdf.line([x1, y], [x2, y])
    pdf.stroke

    pdf.move_cursor_to pdf.bounds.top
  end

  pdf.table(data,
          :header => true,
          :column_widths => ws,
          :row_colors => ["FFFFFF", "CCCCCC"]
    ) do |t|
    for i in 1..3 do
      t.columns(i).style :align => :right
    end
    t.rows(0).style :align => :center, :background_color => "CCCCCC"
  end
end

ところどころソースを読みながらこのブログを書きましたが、
他にも使えそうなオプションやメソッドがまだまだありそうです。

ソースはgithubに公開されているので、(https://github.com/prawnpdf/prawn)
もっと複雑な帳票を作りたい、という方は是非ソースコードも読んでみてはいかがでしょうか。

fluentd(td-agent) の導入

はじめまして。開発チームの yuzuki です。

7/1に弊社の 決済サービスPaid(ペイド) のサーバー群へ
ログ集約の改善を目的として導入した fluentd(td-agent) の導入手順などをまとめてみました。

ログ集約を改善する動機

弊社ではこれまで(今も大部分は) cron + rsync を使い、週次バッチでWebサーバー上のログファイルをファイルサーバーへ転送することで一応の集約をさせてきました。
(集約というよりはバックアップといった意味合いの方が強いかもしれません)
サーバー台数が少なかった頃はこの仕組みでも特に大きな問題はなかったと思いますが、
サービスの成長にあわせて、サーバー台数が増え、リリース時に運用系と待機系を相互に切り替える運用へ変わったことで、
ログファイルから問合せや障害などの調査を行う際に、まずログファイルを取得することに手間が掛かる状況になっていました。

下記は弊社のもう1つのサービス ファッション・雑貨向けB2Bサイトスーパーデリバリー
(以下 SUPER DELIVERY)のサーバー構成です。

sd-network


当日分のログファイルについては、まず1系と2系のどちらが運用系なのかを調べてから、
片系で4台あるサーバー全てからログファイルをscp/sftpでせっせと取得し調査していました。

過去分のログファイルについては、各サーバーでローテーションされ週次バッチでの集約を待つ状態と、
週次バッチで各サーバーからファイルサーバーへ集約された状態の2つの状態があります。
※この集約は1台のサーバーに集まるというだけで、1ファイルにまとまるわけではありません。

過去分は、運用系/待機系の切り替えも考慮すると、両方のログを調査対象とするのが確実であるため
  • 集約待ちであれば全8台のサーバーからログファイルを取得
  • 集約済みであれば1台のサーバーの8つのディレクトリからログファイルを取得
といった手間が調査の度に発生していました。

「ここ、何とかならないかな?」と、ずっと思っていたところに現れたのが fluentd です。
fluentd を各サーバーへインストールして、ログの転送を任せてしまえば
1日のログを1台のファイルサーバーの1つのファイルにまとめることが出来てしまいます。


余談:
SUPER DELIVERYが動機になっているのに、なぜPaidへの導入記事なのかというと、
先に導入のコンセンサスが取れたのがPaidだったから、というだけの理由です。
もうPaidで実績ができましたし、SUPER DELIVERYへの導入ハードルも下がったように思います。

fluentd(td-agent) とは?

"Fluentd" is an open-source tool to collect events and logs.
http://fluentd.org/

fluentd は、ほぼリアルタイムでログの読み込み、加工、転送、さらには書き込みまでも行ってくれる非常に素晴らしい Ruby ベースのミドルウェアです。
ログ以外にも、メッセージと表現されることもあります。
※cron + rsync と異なり、扱えるのはログファイルだけではありません!


fluentd はプラグインを書くことで拡張も可能で、既に多くのプラグインが開発・公開されています。
http://fluentd.org/plugin/

fluentd は標準プラグインがバンドルされた Ruby の gem です。
http://rubygems.org/gems/fluentd

td-agent は fluentd 専用の Ruby や jemalloc などがバンドルされ、
安定感のある fluentd の配布パッケージ(rpm, deb など)です。
http://docs.treasuredata.com/articles/td-agent

導入対象のシステムには Ruby 1.9.3 が入っていたこともあり、
既存機能への干渉を避けるため、td-agent を選びました。

td-agent のインストール

執筆時点の弊社では CentOS 5.x on x86_64 が主流なので、
下記のページを参考にインストールしました。
http://help.treasuredata.com/customer/portal/articles/1246904-installing-td-agent-on-rhel-and-centos
※以下、x86_64環境を前提として書いています。

まずはroot権限を持っているユーザーで下記のコマンドを実行し、ファイルを作成します。
# touch /etc/yum.repos.d/td.repo

次に、お好みのエディタで下記の通りに編集して保存します。
[treasuredata]
name=TreasureData
baseurl=http://packages.treasure-data.com/redhat/$basearch
gpgcheck=0

最後に下記のコマンドを実行し、td-agent をインストールします。
# yum install td-agent

インストール手順は上記のページに書かれている内容に従っています。

td-agent のファイル配置場所など

■td-agent の起動スクリプト
/etc/rc.d/init.d/td-agent

■td-agent の設定ファイル テンプレート
/etc/td-agent/td-agent.conf.tmpl

■ td-agent にバンドルされた Ruby のインストール先
/usr/lib64/fluent/ruby/

■ td-agent のログ出力先
/var/log/td-agent/

■ td-agent の.pid出力先
/var/run/td-agent/

fluentd 内部でのログのルーティング

プラグインについて触れる前に、タグについて簡単に説明します。

現在の fluentd v10 は内部で、タグと呼ばれる文字列でログのルーティングを行っています。
タグはピリオードで区切られた文字列で、プラグインで処理を行う度に remove_tag_prefix, add_tag_prefix などでタグの書き換えを行い、
いくつかのプラグインを経由していくのが一般的かと思います。

基本的なタグの書き換えは下記のような設定になっています。
#ログ読み込み時のタグは raw.apache.access
<source>
  type tail  #標準のin_tailプラグイン
  tag raw.apache.access
  ...
</source>

#タグが filtered.apache.access と完全一致する場合、ログをファイルに出力
#※本来は一番下に書くべき設定ですが、この位置に書いても動作することの説明のため
# あえてここに書いています
<match filtered.apache.access>
  type file  #標準のout_fileプラグイン
  ...
</match>

#タグが raw.apache.access と完全一致する場合、先頭の raw を除去し apache.access とする
#そして新しいタグ apache.access で先頭からタグのルーティングをやり直す
<match raw.apache.access>
  type my_remove_tag_prefix_filter  #このようなプラグインは存在しません
  remove_tag_prefix raw
</match>

#タグが apache.access と完全一致する場合、先頭に filtered を追加し filtered.apache.access とする
#そして新しいタグ filtered.apache.access で先頭からタグのルーティングをやり直す
<match apache.access>
  type my_add_tag_prefix_filter     #このようなプラグインは存在しません
  add_tag_prefix filtered
</match>

この例では完全一致のみ説明していますが、パターンによるマッチもサポートされています。
詳しくは下記のページにある 「*」 「**」 「{X,Y,Z}」 の3ヶ所を参照してください。
http://docs.fluentd.org/articles/config-file#2-ldquomatchrdquo-tell-fluentd-what-to-do

なお、次のメジャーバージョンである fluentd v11 ではラベルなるとか・・・?

fluentd プラグインのインストール

今回は fluent-plugin-route と fluent-plugin-rewrite-tag-filter の2つのプラグインを追加でインストールして使用しました。
この2つのプラグインを簡単に説明します。

fluent-plugin-route は、上記で説明した remove_tag_prefix, add_tag_prefix を使い
柔軟なログのルーティングを実現することが出来ます。
今回は残念ながら大した使い方をしていないため、詳しくはプラグインのページを参照してください。
https://github.com/tagomoris/fluent-plugin-route

fluent-plugin-rewrite-tag-filter は、apache httpdのmod_rewriteのようなプラグインです。
ログ内の項目に対して正規表現によるパターンマッチングを試行し、
マッチした場合、またはマッチしなかった場合に、タグの書き換えを行うことで
柔軟かつ強力なログのルーティングを実現することが出来ます。
なお、フィルタリングの用途にも適しています。
https://github.com/fluent/fluent-plugin-rewrite-tag-filter

では、上記のプラグインをインストールします。
root権限を持っているユーザーで下記のコマンドを実行します。
(※gemに関する環境変数などは設定していません)
# /usr/lib64/fluent/ruby/bin/gem install fluent-plugin-route
# /usr/lib64/fluent/ruby/bin/gem install fluent-plugin-rewrite-tag-filter

エラーが何も出なければ、
/usr/lib64/fluent/ruby/lib/ruby/gems/1.9.1/gems/
にプラグインがインストールされるはずです。
(バンドルされる Ruby のバージョンが上がれば、パスの1.9.1の部分も変わるでしょう)

導入時は /usr/lib64/fluent/ruby/bin/gem を使いましたが、
他にも /usr/lib64/fluent/ruby/bin/fluent-gem を使う方法や、
gem を /etc/td-agent/plugin/ へ配置してインストールする方法もあるようです。

サーバーの構成

Paidのユーザー向けサービスが動いているWebサーバーは2台の運用系(pd01, pd02)と、
1台の待機系(pd04)で構成されています。
残りの1台(pd03)は社内向けサービスが動いているため、今回のログ集約の対象からは除外しました。

このうち運用系をログ転送元、待機系をログ転送(集約)先としてセットアップしました。
(残念ながら、この時点ではまだログ転送先がSPOFです・・・)


paid-network

■ログ集約の構成変更前
ログ集約・変更前

■ログ集約の構成変更後
ログ集約・変更後

起動スクリプトと設定ファイルの構成

Paidのサーバー群には、原則として全てのサーバーが全ての役割を果たせるようセットアップしておくというルールがあるため、ログ転送元とログ転送先に同じファイルを配置しておく必要があります。
他のサービスへの展開なども含め、色々な試行錯誤を繰り返した結果、下記の構成に落ち着きました。
(production環境以外に、staging環境と、test環境用の設定ファイルも作成しました)

■ 起動スクリプト
/etc/rc.d/init.d/
├ td-agent-paid-web-aggregator
└ td-agent-paid-web-forwarder

■ 設定ファイル
/etc/td-agent/
├ config.d/
│ └ paid/
│    ├ forward/
│    │ ├ production/
│    │ │ ├ aggregate.conf
│    │ │ └ forward.conf
│    │ ├ staging/
│    │ │ ├ aggregate.conf
│    │ │ └ forward.conf
│    │ └ test/
│    │    ├ aggregate.conf
│    │    └ forward.conf
│    ├ input/
│    │ ├ java-pd.conf
│    │ └ java-pd-web-service.conf
│    └ output/
│       ├ java-pd.conf
│       └ java-pd-web-service.conf
├ td-agent-common.conf
├ td-agent-paid-web-aggregator-production.conf
├ td-agent-paid-web-aggregator-staging.conf
├ td-agent-paid-web-aggregator-test.conf
├ td-agent-paid-web-forwarder-production.conf
├ td-agent-paid-web-forwarder-staging.conf
└ td-agent-paid-web-forwarder-test.conf

起動スクリプト


■ ログ転送元の起動スクリプト
/etc/rc.d/init.d/td-agent-paid-web-forwarder

サービスへの登録はroot権限を持っているユーザーで下記を実行します。
# chkconfig --add td-agent-paid-web-forwarder
# chkconfig --level 345 td-agent-paid-web-forwarder on

起動スクリプトは、実行環境毎に読み込む設定ファイルを変えたかったため、
/etc/rc.d/init.d/td-agent を少々改変しています。主な改変箇所はスクリプト内の赤字部分です。

※fluentd の新しいバージョンでは、configtest オプションが実装されていますが、
 導入時のバージョンは td-agent 1.1.14(fluentd 0.10.35) であったため
 ここに書いてある起動スクリプトには configtest オプションが含まれていません。


■■ ログ転送元の起動スクリプトの内容
#!/bin/bash
#
# /etc/rc.d/init.d/td-agent-paid-web-forwarder
#
# chkconfig: - 80 20
# description: td-agent
# processname: td-agent-paid-web-forwarder
# pidfile: /var/run/td-agent/td-agent-paid-web-forwarder.pid
#
### BEGIN INIT INFO
# Provides:          td-agent
# Default-Stop:      0 1 6
# Required-Start:    $local_fs
# Required-Stop:     $local_fs
# Short-Description: td-agent's init script
# Description:       td-agent is a data collector
### END INIT INFO

# Source function library.
. /etc/init.d/functions

name="td-agent-paid-web-forwarder"
prog=$name
hostname=`hostname -s`
if [[ $hostname =~ ^pd[0-9]{2}$ ]]; then
    mode=production
elif [[ $hostname =~ ^stg\-pd[0-9]{2}$ ]]; then
    mode=staging
else
    mode=test
fi

fluentd=/usr/lib/fluent/ruby/bin/fluentd
if [ -f "/usr/lib64/fluent/ruby/bin/fluentd" ]; then
    fluentd=/usr/lib64/fluent/ruby/bin/fluentd
fi

if [ -f /etc/sysconfig/$prog ]; then
    . /etc/sysconfig/$prog
fi

RUN_USER=td-agent
RUN_GROUP=td-agent
PIDFILE=/var/run/td-agent/$prog.pid
CONF_FILE=/etc/td-agent/$prog-$mode.conf
LOG_FILE=/var/log/td-agent/$prog.log
FLUENTD_ARGS="--group $RUN_GROUP -d $PIDFILE -c $CONF_FILE -o $LOG_FILE -q"

if [ -n "${PIDFILE}" ]; then
    mkdir -p $(dirname ${PIDFILE})
    chown -R $RUN_USER:$RUN_GROUP $(dirname ${PIDFILE})
fi

# use jemalloc to avoid fragmentation
if [ -f "/usr/lib/fluent/jemalloc/lib/libjemalloc.so" ]; then
    export LD_PRELOAD=/usr/lib/fluent/jemalloc/lib/libjemalloc.so
fi
if [ -f "/usr/lib64/fluent/jemalloc/lib/libjemalloc.so" ]; then
    export LD_PRELOAD=/usr/lib64/fluent/jemalloc/lib/libjemalloc.so
fi

RETVAL=0

start() {
    echo -n "Starting $name[$mode]: "
    daemon --pidfile=$PIDFILE --user=$RUN_USER $fluentd "$FLUENTD_ARGS"
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$prog
    return $RETVAL
}

stop() {
    echo -n "Shutting down $name[$mode]: "
    if [ -e "${PIDFILE}" ]; then
        killproc -p ${PIDFILE} || killproc $prog
    else
        killproc $prog
    fi
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && rm -f $PIDFILE && rm -f /var/lock/subsys/$prog
    return $RETVAL
}

restart() {
    stop
    start
}

reload() {
    echo -n "Reloading $name[$mode]: "
    killproc -p $PIDFILE $prog -HUP
    echo
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
    reload)
        reload
        ;;
    condrestart)
        [ -f /var/lock/subsys/$prog ] && restart || :
        ;;
    status)
        status -p $PIDFILE $prog
        ;;
    *)
        echo "Usage: $prog {start|stop|reload|restart|condrestart|status}"
        exit 1
        ;;
esac
exit $?


■ログ転送先の起動スクリプト
/etc/rc.d/init.d/td-agent-paid-web-aggregator

サービスへの登録はroot権限を持っているユーザーで下記を実行します。
# chkconfig --add td-agent-paid-web-aggregator
# chkconfig --level 345 td-agent-paid-web-aggregator on

■■ログ転送先の起動スクリプトの内容
   forwarder を aggregator へ置換しただけです。
#!/bin/bash
#
# /etc/rc.d/init.d/td-agent-paid-web-aggregator
#
# chkconfig: - 80 20
# description: td-agent
# processname: td-agent-paid-web-aggregator
# pidfile: /var/run/td-agent/td-agent-paid-web-aggregator.pid
#
### BEGIN INIT INFO
# Provides:          td-agent
# Default-Stop:      0 1 6
# Required-Start:    $local_fs
# Required-Stop:     $local_fs
# Short-Description: td-agent's init script
# Description:       td-agent is a data collector
### END INIT INFO

# Source function library.
. /etc/init.d/functions

name="td-agent-paid-web-aggregator"
prog=$name
hostname=`hostname -s`
if [[ $hostname =~ ^pd[0-9]{2}$ ]]; then
    mode=production
elif [[ $hostname =~ ^stg\-pd[0-9]{2}$ ]]; then
    mode=staging
else
    mode=test
fi

fluentd=/usr/lib/fluent/ruby/bin/fluentd
if [ -f "/usr/lib64/fluent/ruby/bin/fluentd" ]; then
    fluentd=/usr/lib64/fluent/ruby/bin/fluentd
fi

if [ -f /etc/sysconfig/$prog ]; then
    . /etc/sysconfig/$prog
fi

RUN_USER=td-agent
RUN_GROUP=td-agent
PIDFILE=/var/run/td-agent/$prog.pid
CONF_FILE=/etc/td-agent/$prog-$mode.conf
LOG_FILE=/var/log/td-agent/$prog.log
FLUENTD_ARGS="--group $RUN_GROUP -d $PIDFILE -c $CONF_FILE -o $LOG_FILE -q"

if [ -n "${PIDFILE}" ]; then
    mkdir -p $(dirname ${PIDFILE})
    chown -R $RUN_USER:$RUN_GROUP $(dirname ${PIDFILE})
fi

# use jemalloc to avoid fragmentation
if [ -f "/usr/lib/fluent/jemalloc/lib/libjemalloc.so" ]; then
    export LD_PRELOAD=/usr/lib/fluent/jemalloc/lib/libjemalloc.so
fi
if [ -f "/usr/lib64/fluent/jemalloc/lib/libjemalloc.so" ]; then
    export LD_PRELOAD=/usr/lib64/fluent/jemalloc/lib/libjemalloc.so
fi

RETVAL=0

start() {
    echo -n "Starting $name[$mode]: "
    daemon --pidfile=$PIDFILE --user=$RUN_USER $fluentd "$FLUENTD_ARGS"
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$prog
    return $RETVAL
}

stop() {
    echo -n "Shutting down $name[$mode]: "
    if [ -e "${PIDFILE}" ]; then
        killproc -p ${PIDFILE} || killproc $prog
    else
        killproc $prog
    fi
    RETVAL=$?
    echo
    [ $RETVAL -eq 0 ] && rm -f $PIDFILE && rm -f /var/lock/subsys/$prog
    return $RETVAL
}

restart() {
    stop
    start
}

reload() {
    echo -n "Reloading $name[$mode]: "
    killproc -p $PIDFILE $prog -HUP
    echo
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
    reload)
        reload
        ;;
    condrestart)
        [ -f /var/lock/subsys/$prog ] && restart || :
        ;;
    status)
        status -p $PIDFILE $prog
        ;;
    *)
        echo "Usage: $prog {start|stop|reload|restart|condrestart|status}"
        exit 1
        ;;
esac
exit $?

設定ファイル

■ログ転送(送信)用の設定ファイル
/etc/td-agent/td-agent-paid-web-forwarder-production.conf
/etc/td-agent/td-agent-paid-web-forwarder-staging.conf
/etc/td-agent/td-agent-paid-web-forwarder-test.conf
ログ転送元の起動スクリプト /etc/rc.d/init.d/td-agent-paid-web-forwarder から
実行環境毎に、上記の3ファイルのうち1つが読み込まれます。

■■ ログ転送(送信)用の設定ファイルの内容
include config.d/paid/input/*.conf
include config.d/paid/forward/production/forward.conf
include td-agent-common.conf
上記は /etc/td-agent/td-agent-paid-web-forwarder-production.conf の内容ですが
staging, test環境用はproductionの部分を置換しただけです。

これは include ディレクティブで外部の設定ファイルを読み込むだけの設定です。

■ ログ転送(受信)用の設定ファイル
/etc/td-agent/td-agent-paid-web-aggregator-production.conf
/etc/td-agent/td-agent-paid-web-aggregator-staging.conf
/etc/td-agent/td-agent-paid-web-aggregator-test.conf
ログ転送先の起動スクリプト /etc/rc.d/init.d/td-agent-paid-web-aggregator から
実行環境毎に、 上記の3ファイルのうち1つが読み込まれます。

■■ ログ転送(受信)用の設定ファイルの内容
include config.d/paid/forward/production/aggregate.conf
include config.d/paid/output/*.conf
include td-agent-common.conf
上記は /etc/td-agent/td-agent-paid-web-aggregator-production.conf の内容ですが
これもstaging, test環境用はproductionの部分を置換しただけです。

同じく include ディレクティブで外部の設定ファイルを読み込むだけの設定です。

■共通の設定ファイル
/etc/td-agent/td-agent-common.conf
全体で共通して読み込む設定ファイルです。commonというネーミングにマサカリが飛んできそうですね。

■■共通の設定ファイルの内容
<match null>
  type null
</match>

<match fluent.**>
  type file
  path /var/log/td-agent/fluent
</match>

<match **>
  type file
  path /var/log/td-agent/unmatched
  compress gz
</match>

上から順に解説します。

<match null>
  type null
</match>
上記は、out_nullプラグインを使い、"null"というタグを持つログを破棄するための設定です。
意図的に破棄したいログに対して使おうとしていましたが、導入時は使いませんでした。
/dev/null のようなものですね。

<match fluent.**>
  type file
  path /var/log/td-agent/fluent
</match>
上記は、out_fileプラグインを使い、fluentd 内部のログを
/var/log/td-agent/fluent.20131118_0.log
のような名前のファイルに出力するための設定です。

<match **>
  type file
  path /var/log/td-agent/unmatched
  compress gz
</match>
上記は、同じくout_fileプラグインを使い、いずれにもマッチしなかったタグを持つログを
/var/log/td-agent/unmatched.20131118_0.log
のような名前のファイルに出力するための設定です。
どれだけ出力されるのかが不明だったため、ローテート時にgzip圧縮し、拡張子に.gzが追加される設定を有効にしています。

※失敗談: 実は7/1の導入時は(実は現在も・・・)全体的に
 path /var/log/td-agent/fluent.log
 path /var/log/td-agent/unmatched.log
 のように、path の末尾に.logを書いてしまっていたために、作成されるログファイル名が
 fluent.log.20130701_0.log
 のように.logが重なってしまっていました・・・

 下記のページに path の仕様がちゃんと書かれています。真面目に読まなきゃダメですね・・・
 http://docs.fluentd.org/articles/out_file
 > The Path of the file. The actual path is path + time + ”.log”.


Paidのログファイル
弊社が運営するサービスの多くはJavaで開発されています。
フレームワークはSeasar2, S2Dao, Hibernate, S2Struts, Tilesといったところで、
Apache Tomcat上で動作しています。
(最近はPlay Framework、Ruby on Railsを使った開発もあります)

このため、今回はアプリケーション内で出力しているアクセスログファイルを対象とします。
以下、「Paidのログファイル」とは、このアクセスログファイルのことを指します。

アクセスログファイルは下記のようなフォーマットになっています。
(実際にはユーザーIDや、その他の情報も含まれていますが、ここでは省略しています)
2013-07-01 16:35:02,665 HEAD http://paid.jp/v/contents/index.jsp 7
2013-07-01 16:36:01,675 HEAD http://paid.jp/v/contents/index.jsp 6

Paidのログファイルを読み込みための設定ファイル
/etc/td-agent/config.d/paid/input/java-pd.conf

■■Paidのログファイルを読み込みための設定ファイルの内容
   ※path, formatは改変しています。
# input ( -> raw.paid.tomcat-pd.tomcat.access)
<source>
  type tail
  path /var/log/tomcat6-pd/access.log
  pos_file /var/log/td-agent/paid_tomcat-pd_tomcat_access.pos
  tag raw.paid.tomcat-pd.tomcat.access
  format /^(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) (?<method>[A-Z]+) (?<url>.+) (?<response_millis>\d+)$/
  time_format %Y-%m-%d %H:%M:%S,%L
</source>

# filter (raw.paid.tomcat-pd.tomcat.access -> forward.paid.tomcat-pd.tomcat.access.${hostname})
<match raw.paid.tomcat-pd.tomcat.access>
  type rewrite_tag_filter
  remove_tag_prefix raw
  hostname_command hostname -s

  rewriterule1 method .* forward.${tag}.${hostname}
</match>

上から順に解説します。

# input ( -> raw.paid.tomcat-pd.tomcat.access)
<source>
  type tail
  path /var/log/tomcat6-pd/access.log
  pos_file /var/log/td-agent/paid_tomcat-pd_tomcat_access.pos
  tag raw.paid.tomcat-pd.tomcat.access
  format /^(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) (?<method>[A-Z]+) (?<url>.+) (?<response_millis>\d+)$/
  time_format %Y-%m-%d %H:%M:%S,%L
</source>
上記は、in_tailプラグインを使い、tail -F コマンドのように
ログファイルに追記される度に書き込まれたログ(1行単位)を読み込むための設定です。
ログファイルがローテートされても、もちろん追従してくれます。

# filter (raw.paid.tomcat-pd.tomcat.access -> forward.paid.tomcat-pd.tomcat.access.${hostname})
<match raw.paid.tomcat-pd.tomcat.access>
  type rewrite_tag_filter
  remove_tag_prefix raw
  hostname_command hostname -s

  rewriterule1 method .* forward.${tag}.${hostname}
</match>
上記は、fluent-plugin-rewrite-tag-filterプラグインを使い、最初にタグの先頭の raw を除去し、
次にタグの先頭に forward を、タグの末尾に ${hostname} を付加しています。
${hostname} は hostname_command で指定した hostname -s コマンドの実行結果に置き換わります。
この設定はログの転送後に、そのログがどのサーバーから転送されてきたのかを残すための準備です。

これは /etc/td-agent/config.d/paid/input/java-pd.conf と大して変わらないので省略します。
/etc/td-agent/config.d/paid/input/java-pd-web-service.conf

Paidのログファイルを転送(送信)するための設定ファイル
/etc/td-agent/config.d/paid/forward/{production,staging,test}/forward.conf

■■Paidのログファイルを転送(送信)するための設定ファイルの内容
   ※hostは改変しています。
# filter (forward.paid.*.*.access.${hostname} -> production.forward.paid.*.*.access.${hostname})
<match forward.paid.*.*.access.*>
  type route

  <route forward.paid.*.*.access.{pd01,pd02,pd03,pd04}>
    add_tag_prefix production
  </route>
</match>

# forward-output (production.forward.paid.*.*.access.${hostname} -> )
<match production.forward.paid.*.*.access.*>
  type forward
  heartbeat_type tcp

  buffer_type file
  buffer_path /var/log/td-agent/forward-paid.buf

  <server>
    name pd04
    host 192.168.2.4
  </server>
  #<server>
  #  name pd03
  #  host 192.168.2.3
  #  standby
  #</server>

  <secondary>
    type file
    path /var/log/td-agent/forward-paid.err
    compress gz
  </secondary>
</match>
上記は /etc/td-agent/config.d/paid/forward/production/forward.conf の内容ですが
staging, test環境用はproductionの部分、route内のホスト名、hostの部分を置換しただけです。

上から順に解説します。

# filter (forward.paid.*.*.access.${hostname} -> production.forward.paid.*.*.access.${hostname})
<match forward.paid.*.*.access.*>
  type route

  <route forward.paid.*.*.access.{pd01,pd02,pd03,pd04}>
    add_tag_prefix production
  </route>
</match>
上記は、fluent-plugin-routeプラグインを使い、転送前にホスト名の簡易チェックを行った後に、
チェックがOKであればタグの先頭に production を付加しています。
チェックがNGであればunmatchedのログファイルに出力されます。

※転送時の送信側と受信側で、タグに付加された実行環境毎に異なる文字列をチェックすることで
 誤ってテスト環境から本番環境の fluentd にログを転送するリスクを減らそうとする目論見です。
 本来は iptables などで接続制限を加えるべきなのでしょうが、現在はそこまでしていません。

# forward-output (production.forward.paid.*.*.access.${hostname} -> )
<match production.forward.paid.*.*.access.*>
  type forward
  heartbeat_type tcp

  buffer_type file
  buffer_path /var/log/td-agent/forward-paid.buf

  <server>
    name pd04
    host 192.168.2.4
  </server>
  #<server>
  #  name pd03
  #  host 192.168.2.3
  #  standby
  #</server>

  <secondary>
    type file
    path /var/log/td-agent/forward-paid.err
    compress gz
  </secondary>
</match>
上記は、out_forwardプラグインを使い、ログを転送(送信)するための設定です。
パフォーマンスはかなり落ちますが、転送(送信)時にファイルバッファを使いログ消失を防止したり、
転送(送信)エラーとなったログをファイルへ書き込んで残したりしています。
コメントアウトされているログ転送先のスタンバイは、まだ稼働していません・・・

Paidのログファイルを転送(受信)するための設定ファイル
/etc/td-agent/config.d/paid/forward/{production,staging,test}/aggregate.conf

■■Paidのログファイルを転送(受信)するための設定ファイルの内容
# forward-input ( -> production.forward.paid.*.*.access.${hostname})
<source>
  type forward
</source>

# filter (production.forward.paid.*.*.access.${hostname} -> paid.*.*.access.${hostname})
<match production.forward.paid.*.*.access.*>
  type route

  <route production.forward.paid.*.*.access.*>
    remove_tag_prefix production.forward
  </route>
</match>
上記は /etc/td-agent/config.d/paid/forward/production/aggregate.conf の内容ですが
staging, test環境用はproductionの部分を置換しただけです。

上から順に解説します。

# forward-input ( -> production.forward.paid.*.*.access.${hostname})
<source>
  type forward
</source>
上記は、in_forwardプラグインを使い、ログを転送(受信)するための設定です。

# filter (production.forward.paid.*.*.access.${hostname} -> paid.*.*.access.${hostname})
<match production.forward.paid.*.*.access.*>
  type route

  <route production.forward.paid.*.*.access.*>
    remove_tag_prefix production.forward
  </route>
</match>
上記は、転送されてきたログのタグに、実行環境が同一であることを示す文字列が含まれているかチェックしています。
チェックがOKであれば転送(送信)前にタグの先頭に付加された production.forward を除去します。
チェックがNGであればunmatchedのログファイルに出力されます。

■転送されてきたPaidのログファイルを集約して書き込むための設定ファイル
/etc/td-agent/config.d/paid/output/java-pd.conf

■■転送されてきたPaidのログファイルを集約して書き込むための設定ファイルの内容
# output (paid.tomcat-pd.tomcat.access.${hostname} -> )
<match paid.tomcat-pd.tomcat.access.*>
  type file
  path /var/log/td-agent/aggregate_paid_tomcat-pd_tomcat_access
  compress gz
</match>
上記は、out_fileプラグインを使い、ログをファイルへ書き込む設定です。
だいたい上の方で説明済みですね。

ファイルへは、タイムスタンプ、タグ、JSONの順で、タブ文字区切りで書き込まれます。
2013-07-01T16:35:02+09:00    paid.tomcat-pd.tomcat.access.pd01    {"method":"HEAD","url":"http://paid.jp/v/contents/index.jsp","response_millis":"7"}
2013-07-01T16:36:01+09:00    paid.tomcat-pd.tomcat.access.pd02    {"method":"HEAD","url":"http://paid.jp/v/contents/index.jsp","response_millis":"6"}

これも /etc/td-agent/config.d/paid/output/java-pd.conf と大して変わらないので省略します。
/etc/td-agent/config.d/paid/output/java-pd-web-service.conf

まとめ

7/1の導入から4ヶ月以上経ちましたが、fluentd(td-agent) はノートラブルで動き続けています。
現在はPaidだけですが、次はSUPER DELIVERYへの導入を考えています。

IE8でjQueryが重いなと思ったら、やるべき3つのこと

こんにちは。なべです。

入社半年の新人 ですが、このような場をいただきましたので、入社してすぐに取り組んだInternetExplorer(以下、IE) 8のjQueryのパフォーマンス対策について書いてみたいと思います。

なぜIE8か?

このブログにたどり着くような方はHTML5をいじってみたり、普段使用するブラウザもFirefoxやChromeという場合が多いと思います。
そんな中、この記事のタイトルを見て、なぜ今さらIE8・・・と思ったのではないでしょうか?

というわけで、まずは、なぜ今IE8のパフォーマンス対策なのかを説明したいと思います。
 
sd_brws_share
右のグラフは、スーパーデリバリーにおける訪問者のブラウザのシェアを簡略化したグラフです。
グラフにもあるようにIE8はスーパーデリバリーではおよそ15.4%のユーザーが利用しています。
※2013/07時点のデータです。

IE8が多い理由としては以下のことが考えられます。
・スーパーデリバリーはBtoBサイトで、出展企業様や小売店様が仕事ととして利用されるため、会社やお店などにある仕事用のPCから利用されている。
・その場合、やはりWindowsが多く、さらに購入時にプレインストールされているブラウザを利用するケースが多いためIEが多い。
・会社内のPCは家庭にくらべて新陳代謝が緩やかなので、数年内にリリースされたバージョンが混在する。(IE8,9,10がほぼ同じ割合)
ちなみにIE6もまだ1%強使われています。
 
その前提に立つと、今後、Windows XPのサポート切れに伴って企業内で利用するPCの買い替えが進み、Windows8のシェアが上がってくるにつれて、IE10のシェアが増えてくることが予想されますが、それまでしばらくはIE8は主要ブラウザとして扱う必要があります。

さて、IE8サポートの重要性がわかったところで本題に入りたいと思います。

問題

「IE8のjQueryのパフォーマンス対策」というのは、IE8でスーパーデリバリーの商品一覧ページを開くと、画面が利用できるようになるまでに時間がかかるという問題への対応でした。
プロファイラなどで調査した結果、原因は$(".hoge")のようにクラスをセレクタに指定してjQueryオブジェクトを操作している部分で処理が重くなり、時間がかかっていることがわかりました。

それはなぜでしょうか? 

この現象の説明の前に、jQueryの仕組みを少しお話しておきます。

jQueryはブラウザ間の違いを吸収して共通のインターフェースを提供してくれるJavaScriptのライブラリです。
jQueryはどのブラウザでも同じように利用することができますが、内部的にはブラウザによって処理を分岐し、微妙な違いを吸収して、ブラウザの機能を呼び出したり、jQueryで実装したりして、ドロドロした部分を隠し、我々に使いやすいようにしてくれています。

この実装の違いに落とし穴があります。
ブラウザの機能を使える場合には、ネイティブメソッドを呼び出すことができますが、jQueryで実装となった場合は、ブラウザ上で動くJavaScriptの処理になります。
当然JavaScriptよりもネイティブメソッドのほうが速いためここでパフォーマンスの差がでます。
よって、できるだけjQueryの独自実装部分に入らないように実装することがパフォーマンス向上の一つの施策になります。

さて、IE8の話に戻ります。
ここまでの説明で想像がついているかと思いますが、IE8でクラスをセレクタに指定した処理が重いのは、IE8まではクラスセレクタで要素を取得する機能がブラウザの機能として提供されていない(=ネイティブメソッドにない)ためです。
これはJavaScriptのコンソールなどで以下をそれぞれ打つと確認することができます。
document.getElementById("aaa")
document.getElementsByTagName("aaa")
document.getElementsByClassName("aaa")
よって、jQueryではIE8でクラスをセレクタに指定された場合、JavaScriptにより、対象となる全要素をループ処理で走査するため遅くなっています。

ちなみに、このjQueryの検索ロジックのライブラリをSizzleといいます。
SizzleはjQueryの内部で使われていて、意識して利用することはほとんどないはずですが、github上で公開されているオープンソースのプロジェクトなので、より深く知りたい場合にはこちらをご覧ください。

対策

クラスセレクタを指定してjQueryオブジェクトを取得すると検索処理が実行されて重くなるわけですから、対策としては、クラスをセレクタに指定してノードを検索する処理をできるだけ避ける必要があります。
そこで、以下の3つの対策が考えられます。
     
1. そもそもクラスセレクタを使わない
2. クラスセレクタによる検索対象を減らす
3. クラスセレクタによる検索回数を減らす

それでは個々の対策を説明していきたいと思います。

1. そもそもクラスセレクタを使わない

クラスセレクタを使わずにID("#xxx")で指定できるところはIDで指定します。
IDを指定することにより、ネイティブメソッドでサポートされているgetElementByIdが使用されますので、これだけで速度は改善します。
注意しなければならないのは、IDはDocument上で一意でなければならない点です。
この制約が守れるのであれば、IDに置き換えるのが良いでしょう。

2. クラスセレクタによる検索対象を減らす

とはいえ、IDに単純に置き換えられるところばかりではありません。むしろ、少ないでしょう。
だからといって、
jQueryを使う上で、クラスを使って複数の要素をまとめて操作する機能を使わないというのは考えられません。
ですので、以降はクラスセレクタを使いつつ負荷を最小限にする手段をご紹介します。
 
まずはクラスセレクタで検索する対象を減らし、走査にかかる負荷を減らすことを考えます。
例えばbarクラスを操作したい場合、     
$(".bar")
と書くと、全ての要素が検索対象になってしまいます。
そこで、barクラスの前にかならずIDをもつ親要素があるならば、そこを基点にして     
$("#foo").find(".bar")
と指定します。こうすることで、ID(foo)の以下にある要素だけを検索対象にすることができます。
さらに必ず親要素がID:fooであることがわかっているなら、     
$("#foo").children(".bar")
と指定します。こうすることで、子要素だけに対象を絞り、孫要素より下の階層を検索対象から外すことができます。
ほかにも、next(兄弟要素の次)やsiblings(兄弟要素)、parent(親要素)など基点を中心に周辺を指定できますので活用すると良いと思います。

次に、タグ名を指定することでも負荷の軽減になります。
getElementsByTagNameもネイティブメソッドに存在するため、いきなりクラスを指定するよりも高速になります。    
$("div.bar")
と.いうようにタグと一緒にクラスを指定したり    
$("li").find(".bar")
のようにタグで親になる要素を絞ってから、その下のクラスを指定することで対象を絞ることができます。

このように、できるだけ検索対象の母数を絞ることで、負荷を軽減します。

3. クラスセレクタによる検索回数を減らす

つぎに、クラスセレクタによる重い検索処理を何度も実行して負荷が上がっている場合があります。
例えば以下のコードはどうでしょうか。
$(".bar").css("color","#fff");
$(".bar").css("border","thin solid #000")
$(".bar").width(100);
負荷の高い$(".bar")の検索を3度も実施しています。
この場合は検索をした結果を一度変数に格納し使いまわすことを考えます。
var bar=$(".bar");
bar.css("color","#fff");
bar.css("border","thin solid #000");
bar.width(100);
このように変更することで検索を1回にすることができます。

ちなみに、今回の例のように設定系の処理だけであれば、ドット(.)でつなげて以下のようにも書くことができます。       
$(".bar").css("color","#fff").css("border","thin solid #000").width(100);

以上が3つの対策になります。

まとめ

スーパーデリバリーでは、これら3つの対策をすることで「IE8でjQueryが重い」現象に対応し、パフォーマンス問題を改善することができました。
もし、いまお困りであれば試してみてください!
 
まとめのまとめとして、今回の対策からの学びを、本質的な観点からまとめたいと思います。

今回最も大きな原因は、IE8の機能不足によるものでした。
ただし、使用するライブラリや実行環境の問題というのは多くの場合制御不能なので、”特徴”と捉えてうまく付き合うしかありません。
今回の現象も、「重い」という問題になる前に特徴を把握して、対策をしておけば、顕在化することなく防げていたと思います。

ただ、そもそも、「対策」にあげている、2.検索対象を減らしたり、3.検索回数を減らすといった対策は、通常のプログラムを書く場合や、SQLを考える場合などでも考慮すべき点です。
ひと昔前はJavaScriptというとプログラミング言語というよりはHTMLの一部みたいに考えていた頃もありましたが、改めてプログラミング言語だということを意識して、設計・実装を行うことが必要です。
これらを意識していれば、もし仮に特徴を知らなかったとしても、パフォーマンスが問題にならなかったかもしれません。

また、パフォーマンスの改善の効果の他に、検索回数を減らすために要素の取得処理をまとめ、複数箇所に分散させないことで、修正箇所が絞られ、スクリプト言語にありがちなスペル違いや修正漏れを防ぐことができ、メンテナンス性も改善できます。

日頃からシンプルで保守性の高いコードを心がけたいですね!
Don't repeat yourself!

最後に

スーパーデリバリーのTOPページを2013年7月3日にリニューアルしています。
デザイナーとプログラマのこだわりがぎっしり詰まったNewTOPページをぜひご覧ください
担当したログイン後のスライドショーはスーパーデリバリーの会員様だけにしかお見せできないのが少し残念ですが、、、


 
記事検索