torutkのブログ

ソフトウェア・エンジニアのブログ

JJUGナイトセミナー「師走のJenkins祭り」に参加

JJUG主催のナイトセミナー「師走のJenkins祭り」に参加してきました。
http://jjug.doorkeeper.jp/events/7490

GREEの岡崎さんから、GREEでのJenkins運用3年間のお話と、Jenkinsの父川口さんから互換性とコード進化の両立のお話の2本でした。

Jenkins運用3年間でわかったこと(岡崎さん)

スライド資料のURLです。
https://speakerdeck.com/okazaki/jjug-jenkins-3-years-at-gree

以下、講演内容のまとめ

1年目(2011年)

3年前にGREEに入社した頃は、CI(継続的インテグレーション)の習慣はなく、その頃のビルドは手作業でした。
マネージャやデザイナーなどが次々と「ビルドをください」とデベロッパーに頼むと都度ビルドして渡し、フィードバックをもらって修正するということをやっていたが、Jenkins導入によってデベロッパーが楽になっていきました。
ただ、ビルドは「秘伝のタレ」状態で、Jenkins化するのには数ヶ月かかったこともありました。Jenkisを広めるのに、社内勉強会、チャットでのサポートなどをしていきました。ボランティア的なのでリッチなマシンが割り当てられず、非力なマシンでJenkinsマスターサーバーを立て、必然的にスレーブでビルドするという形になり、各プロジェクトでJenkinsスレーブを立てていきました。

2年目

GitHub Enterprise版が社内に導入され、Jenkinsと連携するようになりました。
プルリクエスト→レビュー/テストというワークフローができてきました。
Jenkinsで自動化されたことで、ディスターブが少なくなっていました。
また、デベロッパー以外にも翻訳ワークフローなどで活用されるようになっていきました。

3年目

新規プロジェクトは、まずJenkins導入を検討するようになり、着実にJenkinsが普及していきました。
Jenkinsサーバーが落ちると各所からクレームが上がり、Jenkinsがワークフロー(業務)にしっかり組み込まれていることが実感できました。

トラブル事例を幾つか紹介

  • SCMポーリングが1分間に設定され、それがあちこちのプロジェクトでコピーされたことで大量のリクエストがGitHubに発生しDDS攻撃状態になった
  • Jenkinsをバージョンアップしたら動かなくなった

今後は、Jenkinsおよびプラグインのバージョンを上げ、テストしてからリリースする仕組みを作りたい。Vagrant、Chefなどを活用して実現をしていこうかと考えています。

まとめ

  • 現状の業務をとめずに変化を起こすには、年単位の時間が必要
  • 分割統治して多様性を受け入れる体制が必要で、全社導入とか標準化はうまくいかない

互換性とコード進化の両立(川口さん)

スライド資料のURLです。
http://www.slideshare.net/kohsuke/ss-29339493

以下、講演内容のまとめ

はじめに

大規模ソフトウェアではモジュール化が欠かせないが、それによりモジュールを別々にビルドして持ち寄り結合することで実行時結合が発生し、実行時にエラーがでることがあります。LinkageErrorやAbstractMethodErrorなどです。

Jenkinsの構成は、

  • Jenkinsコアでは80個位の外部ライブラリを使用
  • 800個位のプラグインプラグイン同士の依存あり
  • プラグインとコアで、それぞれ同じ外部ライブラリだがバージョンが異なるものを使っていることがある
  • 拡張ポイントとしてinterfaceを設けている(実装クラスに@Extensionアノテーションが付いていた)
  • プラグインは、コアをがんがん使うので、コアのクラス変更は困難
基本編
  • フィールドの隠蔽(超基本)、フィールドを直接アクセスするよりメソッド経由でアクセス(JavaBean)
    • IDEでのコード生成で書くのは簡単
  • インタフェースより抽象クラス(後日追加したメソッドにデフォルト実装が付けられるので)
  • コンストラクタの肥大化は、我慢してセッターで値を設定するか引数をまとめるクラスを導入
中級編

publicなクラスだがアクセスを限定(またはさせない)したいことがあります。

  • パッケージ越しにアクセスしたいが他からは使われたくない
  • 互換性を保つため@Deprecatedしたコードをそろそろ次の段階(削除へ向けて)に進めたい

http://kohsuke.org/access-modifier というツールを作成しました。
利用コードイメージ

class 俺俺アクセス extends AccessRestriction { ... }
  :
@Restricted(俺俺アクセス.class)
public void deprecatedMethod() { ... }

mavenプラグインでクラスファイルを検査します。jenkinsでは標準のpomにプラグインを使用するように設定しているので、標準pomを継承する各モジュールでは自動的に検査が実行されます。意識的にmavenプラグインを外さないとビルドが通りません。

バイナリ互換性とソース互換性という互換性があります。

  • ソース互換性がなくてもバイナリ互換性があるというケース

メソッドシグニチャを変更した場合のバイナリ互換性対処のコードイメージです。

  try {
    a.bark(3);
  } catch (AbstractMethodError e) {
    // 互換性モード
    ...
  }

ジェネリクスは、メソッド・フィールドのイレージャの型が変わらなければOKです。

バイトコード上、例外はinvokevirtualに関与しません。なので例外を替えてもバイナリ互換あります。

外部ライブラリがバージョンアップで互換性を壊す変更をすることがあります。

  • 例えば、guava、asm

そこで、シェーディング・パッケージリネーミングツール(maven shade plugin)を使ってバージョン番号をパッケージ名に加えて利用するような対策をします。

OSGiランタイムを導入するとこの問題を解決することがありますが、新たに持ち込む問題もあるので薦めません。

上級編

インスタンスフィールドをアクセスしていたけど、やっぱりメソッド経由にしたいが、既存のコードとの互換を保ちたい

class Foo {
  static Foo INSTANCE = new Foo();
}

を、メソッド経由に書き換え、ただし旧来のフィールドアクセスをするコードに備えてアノテーションを追加

class Foo {
  @AdoptField(name="INSTANCE")
  static Foo getInstance() {...}

アノテーション処理でMETA-INF(?)に情報を保持し、Jenkins独自のクラスローダーがロード時にチェックして書き換えを行います。
この記述、実は落とし穴があって、class Bar extends Foo のようにサブクラスの中でINSTANCEにアクセスしていた場合、INSTANCEがクラスファイルではBar#INSTANCEとなっているので、Foo#INSTANCEとして認識できずチェック漏れとなります。

ツールはこのURL
http://bridge-method-injector.infradna.com/

1分間で学ぶInvokedynamic

  • 実行時リンク
  • 静的リンクと同じ速度
void foo() {
  int x = 5;
  String y = "hello";
  Object o = intとStringからObjectを返す何か(x, y);
  return o;
}

のように、どのメソッドを呼ぶかは決めず、引数戻り値だけ決めておき(プレースホルダ?)、実行時にリンク(どのメソッドを呼ぶか決定)します。実行時にリンクするリンカ役がブートストラップメソッドです。

このInvokedynamicを使って、ビルド後にクラスファイルを書き換えて、すべてのメソッドコールをInvokedynamicに書き換えて、元々コールしていたメソッドを付加情報に加えておき、必要に応じてメソッドを差し替えることで互換性維持を図るというツールを作りました。
http://no-more-tears.kohsuke.org