Web系の自分が想像と障害で学んだバッチ処理・設計の基本

バッチ処理というのはそれ単体で勉強しようとするとなかなか何を勉強したらいいのかわからないことが多い。 特に経験がWeb系ばっかりだと、いざバッチ処理を実装しようとした時に基本的なノウハウを知らないままに書いてしまうことが多い。

バッチ処理というのは実態を整理すると「何らかのトリガーを期に起動し、データをロード・加工・変換・集計してから、出力する」という事になる。 まぁ、INがあって処理してOUTがあるという点では関数だと考えてもいいだろう。 システムの利用者(人に限らない)のアクションとは直接関係ない処理であったり、利用者のアクションをトリガーとしていても、即時にレスポンスがいらないor返せない場合に バッチ処理を選択する事が多い。 実現方式はシェルスクリプトLL言語、実行可能バイナリだったりするし、デーモンとして立ち上げる場合もある。

利用者の操作に対して対話的・同期的な処理はオンライン処理、そうでない処理はバッチ処理と言われることが多い気がする。

バッチ処理のフェイズ

では「何らかのトリガーを機に起動し、データをロード・加工・変換・集計してから、出力する」について細かく考えていこう。 このあたりを本当は細かく説明したほうがいいのかもしれないけれど、今は気力がない。

起動タイミングについての適当な説明

起動タイミングについては大きく二つ種類がある。 定時起動とトリガー起動だ。

  • 定時起動
    • 毎時N分
    • 毎日N時
    • 毎週N曜日
    • 毎月N日
  • トリガー
    • WebUIの操作
    • WebAPIへのリクエスト
    • scpでファイルが置かれる
    • メッセージキュー

ざっくりこんな感じだ。

データをロード

システム内部・外部のデータを特定期間・範囲でロードする。 ロード元はいろいろあるけどだいたいこのあたりだろう。

  • CSVファイル
  • ログファイル
  • 画像ファイル
  • RDBMS
  • 外部Webサービス
  • SCPからファイル取得or受信
  • メッセージキュー

加工・変換・集計する

ロードしたデータを処理していく。 処理内容はいろいろあるけど大体このあたりの処理をする。

  • クリーニング・ベリファイ
  • 加工
  • 変換
  • 集計

データの出力

さて、データを加工・集計しても、それをどこかに出力しないと意味がない。 ので、出力する。出力先はまぁいろいろある。ここが外部へ影響を与える瞬間なので一番怖い。

  • RDBMSへ保存・更新
  • CSVへ保存
  • メール・メッセージを送る
  • 外部Webサービスに送信
  • 次のバッチを起動する

テクニック・運用のためにやっておきたいこと

さて、ここからがこの記事のメインだ。バッチ処理を実装する際には、結構いろいろなテクニックがある。 これらすべてを対応する必要はないかもしれないけど、実装する際に一度は考えておいたほうがいいことばかりだと思っている。

データの更新はギリギリまで避ける

RDBMSなどのばあい、トランザクション複数のデータを更新する際に早い段階からレコードを一行ずつ更新なんかしたりしていると、 ロック範囲が広がるばっかりでデッドロックの温床になったりして何もいいことがない。 一時テーブルなどに変更結果やインサートデータを投入しておいて、更新クエリ一発で対象データをガッツリ書き換える様な実装をするほうがよいだろう。 ファイルなどの場合も、書き込み途中で次のジョブに参照されるみたいな不幸を避けるために一時テーブルに書き込んでおいて最後にリネームするとかしたほうがよい。

可能であれば冪等に実装する

冪等についてはいろいろなところでかなり触れられているので、特に言うことは無い。 これをきちんとやっておかないと、集計にバグがあったりデータが届いてないとかで再集計を強いられた時に「じゃぁ、この日次バッチを今年の頭から再集計やっていきますか 」というつらいことになる。 ただ、これは設計センスがかなり問われるし、自分も失敗した経験があるので「可能な限りがんばっていきましょう!」みたいな気持ちがある。

並列化可能なポイントを抑えておく

バッチ処理は大体時間がかかるからこそバッチ処理になってるのだけど、一連のバッチ処理をナイーブに直列に実行していたらいつまで立っても処理が終わらないということも ままある。 バッチ処理のフェイズ毎だとかユーザー毎に、影響範囲が重なっていなければ並列で実行することで処理が早く終わることもあるので抑えておきましょう。 ただ、並列処理すると当然負荷も上がる場合が多いので、主目的のサービスが死なないよう最大同時スレッド数など、実行負荷の調整ができる機構を備えておきましょう。

時刻の記録

バッチの対象とするデータの範囲・開始時刻・終了時刻は必ずログやテーブルに記録しよう。 ログにはかかった時間も出力するとよい。パフォーマンスの劣化を監視するための指標にできる。 問題が起きた時に開始時刻・終了時刻から計算するのは面倒なので計算してログに出しておこう。 RRDToolsなどの運用ツールに放り込みやすいようになってるとよりよい。 RDBMSに投入するときは、バッチ処理そのもののトランザクションと実行記録を残すためのトランザクションを同一にしていると、処理失敗時に記録用レコードもロールバックされてしまうので、実行記録はログに出力しておくほうがよいだろう。

処理対象範囲を引数で指定可能にする

不幸なことに障害が数日続くという時があるかもしれない、問題解消後に日次バッチを手動集計します。みたいなときに助かる。 指定しないときは動かないようにするか適切なデフォルト値を自分で判定するかについては、実装者の好みで決めればよい。

ロックファイルによる多重起動禁止

たとえば /var/lock/hogehoge~batch~.pid みたいなファイルがあれば、既にバッチが起動しているので新たに起動したバッチは処理を続行しない。 みたいな制御を入れておくと多重起動してはいけない処理を重複して実行されずに済む。

ロックファイルではなくて、DBの設定レコードを見るとかでもよいと思う。 むしろ、バッチサーバーが複数台にまたがっている場合などは一台のDBでロックをとるほうが安心だ。

停止ファイルによる起動禁止

たとえば /var/lock/maintenance みたいなファイルがあれば、メンテナンスモードに入っているため新たに起動したバッチは処理を続行しない。 みたいな制御を入れておくとリリースや障害対応の際に crontab のコメントアウトし忘れたり、外部からのトリガーが不意に来たりすることがないのでよい。 停止ファイルではなくて、DBの設定レコードを見るとかでもよいと思う。

バッチ処理の実行、停止はコントローラブルにしておこう。

データの保持期限・削除基準の決定

バッチ処理の処理結果や処理の副産物として生成されたファイルとかテーブルの後始末について、カウボーイコーディングをしていたり仕様の検討から漏れてたりで、考慮が抜 けてしまう事が多い。

そのまま残しておくとディスク容量を圧迫するけれど、システムからはほぼ利用される事が無い。 そういうデータは定期的に消すような処理(これもバッチ処理だ)を忘れずに入れておきましょう。

法律的な理由で一定期間残さなければならないだとか、実装後半年たったら本当に消してもいいデータなのかどうかよくわからなくなった。 というケースも多い。最初からデータのライフサイクルは意識しておこう。

デーモンの採用基準

バッチの実行プロセスをデーモンにする理由は、単にカッコイイからだとちょっと弱い。感覚の話でアーキテクチャを決めてはいけない。

  • 起動処理が遅く、期待するレスポンスタイムを満たせない
  • トリガーの頻度が多く、常時起動のほうが負荷を制御しやすい
  • トリガーとなるアクションを常に監視しなければいけない

みたいな理由をでっちあげて君だけのかっこいいデーモンを作り上げよう。 面倒なら delayed~job~ や webアプリサーバーにエンドポイント作る方法でもいい。 webアプリサーバーに組み込んでしまうと複数バッチ処理をひとつのインスタンスで実行するということになるので、再起動のタイミングなどが少しシビアになってしまうかもしれない。 このあたりはトレードオフをきちんと考えておこう。

以上、ざっくり踏みまくった地雷から得た知見をまとめたので、各位には「お前の書いていることはインチキだ」だとか「これを読めば全部わかる」だとか「これは間違っている」などどんどん殴って来て欲しい。俺はもうこれ以上障害を起こしたくないんだ!

追記

はてぶで意見をいっぱいもらったので続きも書きました。 mitomasan.hatenablog.com

Doma勉強会

Doma勉強会に行ってきた。具体的にはこれ。

kanjava.connpass.com

資料はこれ

Doma実践

メモから

  • View層までDomaのエンティティやドメインオブジェクトを持ち込む?
    • うらがみさんとしては結構やってるとのこと。
    • JAX-RSでControllerの引数として受け取るところからドメインオブジェクトにする方法を説明していたので、DB層のエンティティがViewまで来ているというよりビジネスモデルがViewやDBのレイヤでシームレスに扱えてるという事だと思った。
      • Domaがエンティティにドメインオブジェクトを持てることと、黒魔術が無いからこういうことが実現できている気がする。View層やDB層のフレームワークPOJOをそのまま扱えるように発展してきたからからこそできることっぽい感。
  • Publicフィールド使うほうが多い?(Beanにしないことが多い?)
    • Beanでも別にいい。ただし、JPAのようにMethodに@Idや@Columnアノテーションをつけて云々みたいなのはできないみたい。
    • フィールドに詰め込むときは、リフレクションで詰め込んでいるけどもだからといってパフォーマンス的にそこで問題を感じたことは無いとのこと。
  • Domaの設計思想的にあまりサロゲートキーは使わない(使う必要がない?)
    • これは聞き忘れた。複数主キー指定できるのでなくても困らない感じではある。
    • 追記 ↑↑上記は私が発表を聞いた時に感じた感想で、実際にはサロゲートキーを使われているとのこと。
  • Batch用ConfigとAppServer用Config、両方で使えるDaoって作れるの?
    • 使えるよ。DaoにConfig受け取る引数がある
    • 複数DBとかもConfig工夫したらいけそうですね。
  • 複合主キーをつけるときは @Id を複数につける
    • 複合主キーに何か意味があるのかな。@Update時にうまいこと使ってくれる?
    • 使ってくれる
  • Lombokとの対応状況現状
    • 問題はあるけど結構共存できるよとのこと
      • 識者の意見
  • JAX-RSとの併用について
    • jaxbのXmlAdapter 書けば jackson もそれを使ってくれるのでエエカンジとのこと。
    • ただし、ドメインオブジェクト分の XmlAdapter 書くのはだるいので APT プロセッサーを書いているとのこと。
    • github.com
  • ダイアレクトはページング時に使われるのが代表的な使われ方
  • 1フィールド1カラムが原則

所感

Java のようにあまり高機能でない静的型の言語でORMでいい感じにインピーダンスミスマッチを解消するって言うのはなかなかうまく行かない場合が多いなぁと思っている。SELECT文の結果というのは型がある。なのでSELECT句が動的に変わる場合(JOINとかだね)というのは静的型ではどうやったってミスマッチを吸収できない。

HibernateJPAはSELECT句とEntityをマッピングするわけではなくテーブルとEntityをマッピングするので、テーブル定義とSELECT句が一致する場合は問題ないのだけれども、一致しない場合に途端に辛いことになってくる。

Domaの場合、そもそも ORM とも言ってないし結果セットをEntityにしてくれるだけというシンプルさがある。そしてSQLを書かせる代わりにメソッドの情報として利用しているのでコンパイル時に強力に型チェックしてくれる。みたいなのはやっぱりいいなぁと思う。

ひとつだけ気になっているのはDomaのいろいろ割り切った故のほぼ完成しちゃっている感。うらがみさんにも「今後のロードマップとかあるんですかね?」って聞こうと思ってたけど忘れてた。完成しちゃってメンテナンスフェイズとかに入ると活発に見えなくなっちゃってプロダクトの死に近づいちゃうみたいなの、どうしてもあるので。

それはそれとして、今度レイヤードアーキテクチャだとかDDD的な設計とか話したいということもワイワイ話せたので良かった。

今度、梅田で夕飯などを食べながら技術を語る会のようなものをできたらいいなぁと思ってます。

DevLOVE関西行ってきた。

具体的にはこれ。

https://devlove-kansai.doorkeeper.jp/events/36446

以下、自分の勝手な解釈が多々含まれるけど感じたこと、きいたことをまとめる。

 

「エンジニアが幸せな人生を過ごすための学び方、関わり方、あり方」
というエモーショナルなタイトルで、現場よりの話。

仕事のできないメンバーをできるように変えていくにはという話。

仕事ができるとは何か。できないとは何か。
まずそこからして人によって受け止めかたは様々だろうけどまぁそれはそれ。

とにかく人から期待した結果を引き出すためその人に変わってもらおうとするとき、どう変えたらいいのか、本当に変えていいのかという不安が出てきがちである。

そして人によってはそれは簡単なことだと感じられ、人によっては困難な道のりに感じられる。
その差は人によってそれぞれ抱えるメンタルやとるべき責任、思い込みなどの背景が違うからだという話。

んで、それじゃぁ行動を変えるには、習慣化するにはどうすればいいだろう。
こういうとき、たいてい気合いとか気の持ちようだとかの解決策が提示されがちだが、そういう解決策は実行・継続不可能なことが多い。

よって、小さく行動を変えさせ、分かりやすいフィードバックを与えてやり、その射幸性によって習慣付けさせ、その人を期待通りの行動を取ってくれるように計らうのが行動学的に理にかなっているという話。

そしてフィードバックは早ければ早い方がよい。動物的なレベルでいうと60秒以内が望ましい。理性的な大人は2週間ぐらいはギリ待てる。

ちょっと雑だし、ソシャゲーみたいな表現になったけど、根本はそんな感じかなと思った。

もしかしたら「小さく行動を変えさせ」のところは行動を変えさせる必要もなくて、好ましい指標を外部から計測し、公表してあげるだけでもよいかもしれない。

こういうのは行動科学マネジメントというジャンルらしい。

そのまま他人に適用するよりも、まずは自分に適用してみるのがよさそうだと感じた。
「人はこういう風に振る舞うんだよ」と、生半可な知識で知った風なことを言って逆鱗に触れてはいけない。

「こういう心理作用かあるとのことなので、最近心がけています」みたいな所から始めて効果を確かめつついい感触であれば周囲にも広めていくぐらいが今の自分にはよさそう。

 

こうして、勉強会のフィードバックを早めに出すことも、今回の勉強会で学んだことだ。

と、いいオチがついたのでそれではまた。

fitdesk使用感日記

fitdeskを買ってからというもの、PCに向かうとき=fitdeskぐらいの勢いで乗っている。PCがfitdeskの上においてあるのだから当たり前なんだけど。

実際にfitdeskを買うまでは「これはとてもいいものだからオフィスへの導入を考えてもいいものなのでは」ぐらいに思っていたのだが、実際に乗っていると思った以上に汗をかいてしまうので、だいたいパンツ一丁の状態で乗っていることが多い。職場でパンツ一丁で働いているメンバーが居ると辛いものがあるだろうから導入は厳しそうだ。

まぁ、職場への導入は別に真面目に考えてるわけでもないのでそれはそれとして。

よくあるサイクルマシンのように絶対にダイエットするんだ!みたいに強い意志でガツガツ乗る必要がないので気に入っている。PCで作業したければサイクルマシンに乗る事になるので、自然と運動するようになる。とはいえずっと乗っているとさすがに疲れるのでちょっと疲れた時は普通の机で仕事をする。みたいに使っている。

体重は81kg周辺をさまよっているが、2kgぐらいは普段から増減あるので特に効果は出ていないと言える。

78kgぐらいがコンスタントに出るようになってきたら効果が出てきたと言えるんじゃないかなぁと淡い期待を抱いている。

トマトジュース

トマトジュースは健康にもダイエットにもよいということなので安直ながらのみ始めた。
娘もちびっ子には珍しくトマトジュースが飲めるっぽいので一緒にのんでいる。
健康でも開発でも何事も銀の弾丸はないので継続あるのみなのだ。

プログラマー vs 運動不足

きっかけ

プログラマーは、だいたい運動不足になりやすい仕事だ。

PCに向かって色々調べ物している時やプログラミングしている時が一番楽しい時なので、動かなければいけないという差し迫った状況にないと際限なくPCの前で座っている人も少なくない。

オフィスで仕事をしているプログラマーであれば出社が必須なので少なくとも通勤という名の運動ができるが、今風に在宅勤務するようなプログラマーというのは本当に運動をする機会がない。

私自身、この春から週に一度ずつ在宅勤務をしていたのだが最初の数回は朝時間になったらPCの前に座り、昼休憩以外はずっとPCの前から全く動かないという不健康極まりないありさまだったのだ。

しかし、いろいろな状況が重なって私は2015年9月から週に3回程度の在宅勤務になる予定だ。こうなると運動不足が今以上に深刻な問題になってくると思っている。

そして私は現在35歳。大学入学時に178センチ65キロ程度だったのが、大学卒業時には70キロを超え、中年になった今や83キロもあるのである。18キロ程度、生命活動に不要なことが明らかな肉があるということだ。会社の同僚にも「体脂肪率30%!体の30%が油!」と言われてしまうのだ。

これ以上、運動不足な人生を送って贅肉を増やしていくのはリスクしかない。在宅勤務が増えて加速度的に体重を増やしてしまうと、生産性は脂肪による死亡で0になる。つまり人生を変える時が来たのだ。

冴えた解決策

だいたいにおいて、一人の人間が合理性を追求して出した結論というのは異様に見えることがある。

fitdeskという商品はまさにそれであろう。

FitDesk X-2.0 ジャパンモデル(フィットデスクX-2.0 ジャパンモデル )エアロバイク、パソコン

私にとっては合理性の限りを追求するとこの製品のような結論に達するのはごく自然であることに感じられるのだが、世間様の評判を調べると「何たるネタ商品!」みたいな感じの扱いをされてしまっている。

と言うわけで、我が家にはfitdeskがある。これから、fitdeskに乗りながら本blogを書いていくという算段なのだ。実は昨日書いた記事もこれに乗って書かれている。 mitomasan.hatenablog.com

食事制限をするつもりはあまりないので、業務中にこの机にまたがっているだけでどれだけの効果があるか、気になるところである。

cronのクラスタリングサポート(CentOS7)

ある日、突如 cron に疑問を感じ man cron を実行してみた。 たまたま CentOS7 だったのだが、「CLUSTERING SUPPORT」なるセクションがありびっくりした。 cronでクラスタリングとな!

びっくりしたのはいいが、実際に何をやってくれるのかはさっぱりわからなかったので和訳してみることにした。

以下、和訳。間違いがあればすいません。

*1

クラスタリングサポート

In this version of Cron it is possible to use a network-mounted shared /var/spool/cron across a cluster of hosts and specify that only one of the hosts should run the crontab jobs in this directory at any one time. This is done by starting Cron with the -c option, and have the /var/spool/cron/.cron.hostname file contain just one line, which repre- sents the hostname of whichever host in the cluster should run the jobs. If this file does not exist, or the hostname in it does not match that returned by gethostname(2), then all crontab files in this directory are ignored. This has no effect on cron jobs specified in the /etc/crontab file or on files in the /etc/cron.d directory. These files are always run and considered host-specific.

本バージョンの cron はネットワークマウントされた共有の /var/spool/cron を クラスタのホストをまたがって、どの時点においても一つだけのホストが実行すべき ジョブを実行することができる。これは -c オプションを与えて cron を起動すれば 実現できる。そして、/var/spool/cron/.cron.hostname に一行だけあれば そこに属するクラスタのいずれかのホストがジョブを実行すべきジョブを実行する。 もし、このファイルがないか hostname が gethostname(2) にマッチしない場合、 このディレクトリにあるすべての crontab ファイルが無視される。これは、/etc/crontab や /etc/cron.d 以下を対象とした cron の動作に影響を与えない。それらのファイルは 常に特定のホスト上で実行される。

Rather than editing /var/spool/cron/.cron.hostname directly, use the -n option of crontab(1) to specify the host.

/var/spool/cron/.cron.hostname を直接編集するよりは、crontab(1) の -n オプションを用いてホストに特化した設定を編集するほうがよい。

You should ensure that all hosts in a cluster, and the file server from which they mount the shared crontab directory, have closely synchro- nised clocks, e.g., using ntpd(8), otherwise the results will be very unpredictable.

あなたはクラスタのすべてのホストに対して以下のことを保証すべきである - crontab directory がファイルサーバーをマウントしていること - しっかりと時刻が同期されていること そうでなければ、結果がどうなるかは予測不能である。

Using cluster sharing automatically disables inotify support, because inotify cannot be relied on with network-mounted shared file systems.

クラスタシェアリングは自動で inotify(7) サポートを無効化する。 なぜならば inotify(7) はネットワーク共有されたファイルシステムを中継しないからである。

CAVEATS

All crontab files have to be regular files or symlinks to regular files, they must not be executable or writable for anyone else but the owner. This requirement can be overridden by using the -p option on the crond command line. If inotify support is in use, changes in the symlinked crontabs are not automatically noticed by the cron daemon. The cron daemon must receive a SIGHUP signal to reload the crontabs. This is a limitation of the inotify API.

全ての crontab ファイルは普通のファイルか、そのシンボリックリンクである。 それらは実行可能であったりオーナー以外の誰にでも編集可能であってはいけない。 この要求は crond のコマンドラインを -p オプションで上書きすることができる。 もし inotify サポートを使っている場合、シンボリックリンクされた crontab はデーモンにその変更を自動で通知しない。 これは ionotify API の制限である。

The syslog output will be used instead of mail, when sendmail is not installed.

sendmail が install されていない場合、syslogの出力は代わりに mail コマンドが利用される。

和訳してみてわかったこと

どうやら、/var/spool/cron を複数台にネットワークマウントすることにより、マウントされたサーバーをクラスタとみなしてその中の一台だけがジョブを実行するということができるようだ。

ただし、注釈にあるようにしっかりと時刻が同期されていなければ何が起きるかわからないという大変怖い事が書いてある。 ソースを読み込んで起きうる現象を把握しておかなければ到底プロダクションに投入できるものではない気がする。

私は最近 cron について非常に不安を感じているので、cron の動作をきちんと確認したいと思っている。この記事はその前フリでもある。

*1:@kitora_naoki に和訳の変なところの指摘をもらって修正しました!