SlideShare a Scribd company logo
Javaの進化にともなう
運用性の向上はシステム設計に
どういう変化をもたらすのか
TIS株式会社
アプリケーション開発センター
川島 義隆
Java Day Tokyo 2016 3-E
Javaの機能進化おさらい
Java8
● Lambda expression
● Method reference
● Default methods
● Stream API
● Optional
● Map#computeIfAbsent
Java9
● Jigsaw
● JShell
● HTTP/2
● SO_REUSEPORT
シンタックスの進化に
目が奪われがちだけれども
設計のやり方も
アップデートしましょう
Embrace null
Java8 〜
NullPointerExceptionの典型例
HttpSession session = request.getSession();
User user = session.getAttribute("user");
if (user.isAdmin()) {
// ...
}
ここでNullPointerException発生
引数なしでgetSessionを呼ぶのでセッションがない
場合は空のものが作られる
空のSessionの場合、userはnullになる。
Guard against null
HttpSession session = request.getSession(false);
if (session == null) {
response.sendRedirect(LOGIN_URL);
return;
}
User user = session.getAttribute("user");
if (user == null) {
response.sendRedirect(LOGIN_URL);
return;
}
if (user.isAdmin()) {
// ...
}
nullガード戦法
Java有史以来、最も多く使われている戦術だが、
ガードの漏れが検出しにくく、実際それが本番障害の
原因としてたびたび登場している
Optional
Optional.empty(value)
Optional.ofNullable(value)
Optional.of(value)
optional.get()
optional.orElse(T other)
optional.orElseGet(Supplier other)
optional.orElseThrow(Supplier exceptionSupplier)
ここの間が重要
Optionalの生成
Optionalから値の取り出し
Optionalの意義
User user = Optional
.ofNullable(request.getSession(false))
.map(session -> session.getAttribute("user"))
.orElse(new AnonymousUser());
if (user.isAdmin()) {
// ...
}
Operation中にnullが出現しうる一連の操作を、
Optionalで包みこむこと
Embrace null
標準APIがOptional前提で設計されていないJavaにおいては、
Optionalを使ってどうAPIを設計しようかと考えるよりも
一連のnull発生箇所を包み込むことを主眼において考
えた方が実用的である。
ラムダの扱いをもう少し工夫した事例
http://qiita.com/kawasima/items/744914c2fb0b81686a9c
Mixin
Java8 〜
Default method
●
インタフェースに実装が持てるようになった
●
インスタンス変数は当然ながら持てない
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
Use case
ミドルウェアパターン
Request
TraceMiddleware
AuthenticateMiddleware
Request
writeLog()
setRequestTime()
Request
writeLog()
setRequestTime()
setUserPrincipal()
適用するミドルウェアに応じて、メソッドを追加したい
多重継承できないJavaでは…
従来は継承でやるしか無かったので、使う使わざる
に関わらず、多くのメソッドを予め用意しておく設計
が多い
java.sql.ResultSetは実に、193のメソッドをもつ
更新可能ResultSetの機能や、全てのJDBC型のメ
ソッドを持つため。
Mixin using default methods
public interface Traceable {
Logger LOG = LoggerFactory
.getLogger(Traceable.class);
default void writeLog() {
LOG.info(toString());
}
}
こういうインタフェースを用意して、implementsすれば、
writeLogメソッドが使えるようになる。
Default methodでインスタンス変数を
持てないことへの対策
任意のデータを出し入れするインタフェースを用意して、
public interface Extendable {
Object getExtension(String name);
void setExtension(String name, Object extension);
}
public interface Traceable extends Extendable {
default void setRequestTime(Long time) {
setExtension("requestTime", time);
}
}
デフォルトメソッドではそのインタフェースとのやり取りをする。
private Map<String, Object> extensions;
@Override
public void setExtension(String name, Object extension) {
if (extensions == null) {
extensions = new HashMap<>();
}
extensions.put(name, extension);
}
@Override
public Object getExtension(String name) {
if (extensions == null) {
extensions = new HashMap<>();
}
return extensions.get(name);
}
It's not beautiful, but pragmatic.
実装をこんな感じにすれば、データを持つMixinが実現可能
適用するミドルウェアによって
implementsするインタフェースが決まる。
ミドルウェアが要求するインタフェースを
動的にMixinできるとよいのでは?
動的mixin
予めインタフェースをimplementsしてなくても動的に
implementsして振る舞いを足す。
Proxy.newProxyInstance(
classloader,
new Class[]{ Request.class, Traceable.class },
new MixinProxyHandler());
Default methodの呼び出し
MethodHandles.lookup()
.in(declaringClass)
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args);
MethodHanldeを使うと、実装してないクラスに対して
もdefault methodが呼び出せる。
http://qiita.com/kawasima/items/f735ef0c0a9fa96f6eb4
注意事項
● Java8のJVM実装では、MethodHandleによる
invokeSpecialは非常に遅い。
● 通常のリフレクションの10倍〜100倍のコスト
開発時は、試行錯誤するので動的Mixinを使い
運用時は、性能を優先し静的Mixinを使う。
という戦術がよいのでは。
多重継承できないことに起因する
- クラス数の増加
- メソッドを沢山持つクラス
を抑制できる。
Mixinの効能
システム挙動の動的な変更
(using REPL)
Java8 or 9〜
Use case
アプリケーションの一部サービスを閉鎖し、
Sorryページにリダイレクトする
503
Service Temporarily Unavailable
従来よく見られた設計
こんなテーブルを用意し、閉塞するときにはフラグを
"1"にアップデートする
全リクエストにおいて、この閉塞テーブルを
検索するSQLが実行されてしまう…
機能ID
閉塞フラグ
閉塞
�
管理用APIを用意する
これは最近のJavaでないとできないわけではないけれども…
「閉塞状態であること」を永続化する必要はないので
メモリ内の閉塞フラグを書き換えてやればよい
https://[host]:[port]/admin/service/close
限られた工数で、管理用のAPI用意するのが
難しい場合もある
�
@GET
public String closeService(@QueryParam String serviceCd) {
serviceClosingService.add(serviceCd);
return "OK";
}
REPLがあれば大丈夫
(な日がくるかもしれない)
Enkanフレームワークの擬似REPL
enkan> /middleware app list
ANY defaultCharset (enkan.middleware.DefaultCharsetMiddleware
NONE serviceUnavailable (enkan.middleware.ServiceUnavailableM
ANY stacktrace (enkan.middleware.StacktraceMiddleware@545872d
enkan> /middleware app predicate serviceUnavailable ANY
enkan> /middleware app list
ANY defaultCharset (enkan.middleware.DefaultCharsetMiddleware
ANY serviceUnavailable (enkan.middleware.ServiceUnavailableMi
ANY stacktrace (enkan.middleware.StacktraceMiddleware@545872d
REPL上からMiddlewareの適用条件を操作できる
https://enkan.github.io/
Enkanフレームワークの起動・再起動時間
REPLからの起動 → 1~3 sec
    再起動 → < 1sec
MTTRを短くしようという時代の流れの中で、
アプリケーションの起動の速さはより重要になっていく
http://www.slideshare.net/kawasima/enkankotowarirepl
速さの秘密はこちらをご覧ください
JShellはどうなっているのか?
●
コマンドのカスタマイズはできない
●
動いているプロセスにアタッチできない
https://bugs.openjdk.java.net/browse/JDK-8157208
Java10では、このあたりが解消するかも。
Try Artifact
JShellの中で、Mavenの依存関係を動的に解決し、その
機能を試してみることができる。
-> /resolve org.apache.commons:commons-lang3:jar:3.4
| Path /home/kawasima/.m2/repository/org/apache/commons/commons-
lang3/3.4/commons-lang3-3.4.jar added to classpath
->
org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(10)
| Expression value is: "peXXUYGin6"
| assigned to temporary variable $1 of type String
https://github.com/kawasima/try-artifact
 現段階でのJShellカスタマイズ事例
Java9時代の無停止デプロイ
Java9 〜
Java8までの無停止デプロイ
Server#1
WebApplication
Load balancer
Server#2
WebApplication
縮退しながら、1台ずつリスタート
少なくとも2台の仮想サーバが必要�
Application serverの無停止デプロイ機能
Server
WebLogic Server
WebApplication WebApplication
プロダクション再デプロイメント
前述のサーバでやっていたことをWebLogic Serverの中で実現する
アプリケーションの作りによってはメモリリークが
発生してしまう�
Server
Java9時代
Process
WebApplication
Process
WebApplication
同一サーバ内で同じポートをListenするプロセスを複数立ち上げ、
順次再起動する
SO_REUSEPORT
● 複数のプロセスから同じポートをListenできる
– 従来は"Address already in use:bind"になっていた
● 同じuidである必要がある。
● Linux kernel 3.9より利用可能
(WindowsではUnsupportedOperationException)
SO_REUSEPORTを使うコード
ServerSocketChannel serverChannel =
ServerSocketChannel.open();
serverChannel.setOption(
StandardSocketOptions.SO_REUSEPORT, true);
setOptionメソッドがJava 1.7からなので、各Webサーバプロダクト
(Jetty, Undertow, and so on)は直ぐには呼び出せない
SO_REUSEPORT in Jetty
class ReusePortAvailableConnector extends ServerConnector {
public ReusePortAvailableConnector(@Name("server") Server server) {
super(server);
}
@Override
public void open() throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.setOption(StandardSocketOptions.SO_REUSEPORT, true);
InetSocketAddress bindAddress = getHost() == null ? new
InetSocketAddress(getPort()) : new InetSocketAddress(getHost(), getPort());
serverChannel.socket().setReuseAddress(getReuseAddress());
serverChannel.socket().bind(bindAddress, getAcceptQueueSize());
serverChannel.configureBlocking(true);
addBean(serverChannel);
try {
Field acceptChannel = ServerConnector.class
.getDeclaredField("_acceptChannel");
acceptChannel.setAccessible(true);
acceptChannel.set(this, serverChannel);
} catch (Exception ex) {
throw new IOException(ex);
Falchion container
● JVMプロセスのコンテナ
● Anotherプロセスが起動するのを待ってから、古い
JVMプロセスをKillできる。
● SO_REUSEPORTとの組合せで、リクエストをロストす
ることなく、論理的なアプリケーションの再起動が
可能になる。
https://github.com/kawasima/falchion
experimental
Falchion Container
Falchion container architecture
JVM real process
WebApplication
JVM pool
JVM virtual process
JVM virtual process
JVM real process
WebApplication
Listen the same port
Monitoring JVM
● JMXによるDropwizard Metricsの取得
– Request数
– Error数
– スループット
● jstatによるGC Metricsの取得
REST APIでの状態取得
アプリケーションの再起動が
より手軽で身近なものに
アプリケーションの再起動 = 一大事
当然ながらご利用は慎重かつ計画的に…
JVM auto tuning
● JVMのパラメータを少しづつ変えながら、性能の変
化を計測する。
●
最適なものを選択
– Full GC countを最小に
– 1回あたりのFull GC Timeが最長のものが一番短く
(Full GC Timeのマクシミン)
JVMコンテナの活用可能性
『柔軟な設計』の変化
これまでに手にした武器
●
無停止デプロイの気軽さ
●
高速な起動
●
システムの挙動の動的変更
課題設定
ETC割引の計算ロジックを実装します。
– ただし、平日朝夕割引は実際には後日還元なの
ですが、ここでは他の割引と同じく即時適用かつ
走行距離による還元率の変化はないものとしま
す。
– 走行記録は、24時間を超えないものとします。
https://github.com/kawasima/kata/tree/master/ex01-business-rules
http://www.driveplaza.com/traffic/tolls_etc/ より。
ルールは簡単のため多少いじっています。
割引ルール
●
平日朝夕割引
– 平日「朝:6時〜9時」、「夕:17時〜20時」
–
地方部 
– 当月の利用回数が5回〜9回 30%割引、10回以上 50%割引
●
休日割引
– 普通車、軽自動車等(二輪車)限定
–
土曜・日曜・祝日
–
地方部
– 30%割引
●
深夜割引
–
すべての車種
– 毎日0〜4時
– 30%割引
システム運用負荷を下げたいので…
できるだけ止めずに
従来の柔軟な設計
ルールをRDBMSに登録し、これを
読み込んで、割引率を算出する。
データを追加・変更すればシステム改修
なしに、ルールの追加・変更が可能だ!!
ルールID
ルール名
開始時刻1
終了時刻1
開始時刻2
終了時刻2
月曜日フラグ
火曜日フラグ
  ︙
日曜日フラグ
走行エリア
割引率
適用開始日
適用終了日
ルールID
車種コード
車種コード
車種名
ルール
許容車種 車種
実際運用してみると発生する課題
● 1つのルールで、時間帯を3つ以上持ちたいという要件。
増えてくカラム。
● 用意していない属性(排気量やハイブリッド車優遇など)を
使ったルールの追加。増えてくカラム。プログラム改修。
● 予期しないデータが入って、Exceptionが発生。
増えていく再発防止策。
●
結局ルールを変更するのも開発チームに
依頼してやってもらう運用に…
「自由度の高いシステムを作ったぜ」
という思惑とは裏腹に…
●
高すぎる自由度は、十分にテストするのが難しい
ことを意味する
●
特にデータの組合せにテストケースが隠されると、
十分なテストが出来てないことが多くなる
再起動が容易であれば…
●
ふつうにルールの変更にしたがい
●
コードを書いて
●
テストを書いて
●
安全に簡単にデプロイしてしまえばよい。
ルールの実装例
public class DiscountInMorningOrEvening implements DiscountRule {
private RulePeriod morning = new RulePeriod(6, 9);
private RulePeriod evening = new RulePeriod(17, 20);
@Override
public boolean isApplicable(HighwayDrive drive) {
return ((!morning.isHoliday(drive) && morning.isIn(drive))
|| (!evening.isHoliday(drive) && evening.isIn(drive)))
&& drive.getRouteType() == RouteType.RURAL;
}
@Override
public long discountPercentage(HighwayDrive drive) {
int count = drive.getDriver().getCountPerMonth();
if (count >= 10) {
return 50;
} else if (count >= 5){
return 30;
} else {
return 0;
}
YAGNI has come to Java
運用の大変さ(のイメージ)から、過剰設計になりがち
だったJavaの世界も、今必要なものだけを設計し作れ
ばよい時代に
まとめ
堅い、固い、硬いイメージのJavaも、工夫次第で
は、時代に合わせて柔軟な運用・設計が可能
シンタックスのキャッチアップだけでなく、
設計の手法のアップデートもお忘れなく

More Related Content

Javaの進化にともなう運用性の向上はシステム設計にどういう変化をもたらすのか