それでも環境依存は残っている~起きたり起きなかったりする問題のお話~
- 1. それでも環境依存は残っている
起きたり起きなかったりする問題のお話
Hiroki Tateno
Senior Principal Engineer
Sustaining Engineering
Oracle Japan
1 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 3. はじめに
Program
Agenda 反則勝ち or 反則負け
ピタゴラスイッチ
完全犯罪
おわりに
3 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 4. はじめに
4 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 5. 自己紹介
氏名:立野広樹
仕事 : WebLogic Server 開発
ペンシルパズル好き ニコリ好き nikoli.com会員
2000年 日本オラクル入社
Java関連 トラブルシューティングひとすじ13年目
Sustaining Engineering =問題解析&障害修正
全世界に400+人以上
解析、解析、解析、そしてまた解析。 時々修正。
5 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 6. 理想の障害調査&修正
目的が明確
問題も明確
調査手法は自明
情報は十分
ゴールの判定も明確
良ゲー
6 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 7. 現実の障害調査&修正
目的が不明
問題が成立してない
調査手法が謎
情報不足
もうゴールしてもいいよね…
無理ゲー
無理ゲーを解けるゲームにする!
7 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 8. そんな私の愛読書は…
8 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 9. 常識(=思い込み)が通用しなくなる瞬間
i を2で割った余りは0か1しかないのか?
i + 1 > i は常に正しいか?
12 + 2l がなぜ33にならないのか?
知らない!! 興味ある!! という方は、今すぐJava
PuzzlersにGO!!
9 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 10. 転ばぬ先の… ?
「罠・落とし穴・コーナーケース」
罠はある 一つ一つは小さいけど
何度でも蘇る 但し別の場所に
Java Puzzlersがおしえてくれたこと
コーナーケースがどこにあるかという「知識」ではなく……
コーナーケースがどこにありうるかという「感覚」
10 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 11. It’s just a joke, but…
プログラムが動かないときの、プログラマの言い訳
第5位 それ、動かないとホントに困るの? (えっうん)
第4位 そのOSには対応してないんだよね (たまにある…)
第3位 使い方が悪い (これも実はよくある…)
第2位 プログラムが壊れたとき、君はどこにいたんだ?(責任転嫁!)
第1位 私のマシンではちゃんと動くよ
(from http://hp.vector.co.jp/authors/VA000092/jokes/ )
11 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 12. Let’s start !
環境依存
コーナーケース
探求の旅へ…
12 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 13. 反則勝ち or 反則負け
13 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 14. 要件
ロードされてるクラスが何なのか知りたい
-verbose:class
プログラムから知りたい…
java.lang.ClassLoaderをチェック!
getLoadedClassNames() きっとあるよね….
存在しない….
14 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 15. たぶんこれが正解だけど…
ClassLoaderを自作して
MBeanも自作する
_人人人人人人人_
> 面倒!!! <
 ̄^Y^Y^Y^Y^Y^Y ̄
15 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 16. OpenJDK 7のClassLoaderを見てみると…
public abstract class ClassLoader {
// The classes loaded by this class loader.
// The only purpose of this table
// is to keep the classes from being GC'ed until
// the loader is GC'ed.
private final Vector<Class<?>> classes = new Vector<>();
いまどき Vector なんて… ( ´,_ゝ`)プッ
privateだけどリフレクション使えば取り出せるぞ
16 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 17. リフレクションを使ってみよう
Field classesField =
ClassLoader.class.getDeclaredField("classes");
classesField.setAccessible(true);
Vector<Class> classes =
(Vector<Class>)classesField.get(
ClassLoader.getSystemClassLoader());
for (Class classObj: classes)
System.out.println("loaded:"+classObj);
17 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 18. 反則勝ち
* JRockit
> java TestLoadedClasses
loaded:class oracle.jrockit.jfr.VMJFR
loaded:class TestLoadedClasses * + 巛ヽ
〒 ! + 。 + 。 *
+ 。 | |
... * + / / イヤッッホォォォオオォオウ!
∧_∧ / /
* HotSpot (´∀` / / +
,-
/ ュヘ
f
|*
。
+ 。
+ 。
+
*
。 +
。
〈_} ) |
> java TestLoadedClasses / !+ 。 + + *
./ ,ヘ |
loaded:class TestLoadedClasses ガタン ||| j / | | |||
――――――――――――
18 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 19. 反則負け
*IBM JDK (on AIX / Linux)
Exception in thread "main"
java.lang.NoSuchFieldException: classes
* + 巛ヽ
〒 ! + 。 + 。 *
| 。 | |
ゴツン |★ / / + 。 + 。 +
Sun JDK由来のクラスライブラリ ___|_∧ / /
(´∀` / / + 。 。 * 。
,- f
以外で動かない / ュヘ
〈_} )
|*
|
+ 。 + 。 +
/ !+ 。 + + *
互換性はTCKの範囲 ./ ,ヘ |
ガタン ||| j / | | |||
――――――――――――
19 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 21. ピタゴラスイッチ
21 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 22. どっちが新しい?
File file1 = new File("file1");
file1.createNewFile();
File file2 = new File("file2");
file2.createNewFile();
System.out.println(
file2.lastModified() > file1.lastModified() ?
“file1 is old" : "file1 is NOT older than file2");
22 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 23. 異なる実行結果
* Windows
> java TestLastModified
file1 is old.
* Linux etc... 何故?
$ java TestLastModified
file1 is NOT older than file2.
23 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 24. ファイルシステムの時刻精度
“通常、秒単位に丸められたファイル変更時刻をサ
ポートしますが、中にはもっと高い精度をサポートす
るものもあります” (from java.io.File)
Windows : NTFSは100ns単位で保持
Linux : ext3は秒単位
lastModifiedの比較は環境によって動作が違う
….で、何が問題なわけ?
24 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 25. 殆どのケースでは問題ではない
どういう時に比較したいか?
よくあるパターン
ソースファイルを生成→コンパイルしてバイナリを生成
ソースファイルが更新→再コンパイルしてバイナリも更新
ソースファイルがバイナリよりも新しかったら問題になる
そういうことはない
….で、何が問題なわけ?
25 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 26. ヒヤリ・ハット
唐突に話題は転換する
ハインリッヒの法則「重大事故の陰に29倍の軽度事故と、300
倍のニアミスが存在する」(from wikipedia「ヒヤリ・ハット」)
1件の重大バグの裏に29倍の軽微な不具合と300倍の勘違
いがある?
「一つ一つの仕様の勘違いは大きな問題でなくて
も、たまたま複数の動作が重なったとき、不具合
になる、ことがあるかも」
26 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 27. ピタゴラスイッチ(1)
よくある要件
「スクリプト」を受け取って、そこから「Javaソース」を作って、
最終的にJavaコンパイラで「クラスファイル」を生成したい。
……大量に。
Java
Java Class
Script Java
Source Class
Class
Script Source File
Script FileJava
Source
File
File
Class
File
Script Source
File File
File
27 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 28. ピタゴラスイッチ(2)
public void generate(List<Task> tasks) {
foreach (Task task : tasks) {
File source = generateSourceCode(task);
compileCode(source, sourceEncoding);
}
}
28 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 29. ピタゴラスイッチ(3)
大量すぎるので中間ファイルはメモリに格納しよう
メモリは正義!!
byte[] Class
Script byte[]
Java Class
Script byte[]
Java File
Class
Script byte[]
Source
Java File
Class
Script Sourcebyte[]
Java File
Class
Script Source
Java File
Source File
Source
29 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 30. ピタゴラスイッチ(4)
public void generate(List<Task> tasks) {
foreach (Task task : tasks) {
byte[] source = generateSourceCode(task);
writeSource(source)
compileCode(source, sourceEncoding);
}
}
30 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 31. ピタゴラスイッチ(5)
1つ1つ処理するのは非効率だからまとめよう!
scripts sources classes
31 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 32. ピタゴラスイッチ(6)
public void generate(List<Task> tasks) {
foreach (Task task : tasks) {
sourceList.add(generateSourceCode(task));
}
compleSources(sourceList, sourceEncoding);
writeSources(sourceList);
}
32 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 33. ピタゴラスイッチ(7)
まとめすぎてOutOfMemoryErrorになった……
適当なサイズでタスク分割しよう
scripts sources classes
scripts sources classes
scripts
scripts sources
sources classes
classes
scripts sources classes
33 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 34. ピタゴラスイッチ(8)
public void generate2(List<Task> tasks) {
List<List<Task>> divided =
divideTasks(tasks, JOB_SIZE);
for (List<Task> dividedTasks: divided) {
generate(dividedTasks);
}
}
34 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 36. ピタゴラスイッチ(10)
public void checkRecompile(File binary) {
File source = getSourceFile(binary);
if (source.lastModified() > binary.lastModified())
doRecompile(source); // 時刻比較してる
}
36 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 37. ピタゴラスイッチ(11)
後日……
『あの、ものすごく大規模なプロジェクトのタスクを日本語で書
いた時、Windowsだけちゃんと動かないんですケド……』
問題発生!!
スクリプトを日本語以外で書いた場合は動く
日本語で書いた場合もWindows以外なら動く
37 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 39. ピタゴラスイッチ(13)
原因1
歴史的経緯により、日本語Windows環境は、javacのデフォルト
ソースコードエンコーディングはMS932。別の文字コードで書か
れてる場合は、明示的にエンコーディングを指定しないとエラー。
原因2
ソースをオンメモリに格納する最適化をした時に、バイナリを先
に書き出して、それからソースを書きだしていた。(バグといえば
バグかもしれないが、ファイルに書き出したソースはコンパイル
とは関係ないはずだった)
39 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 40. ピタゴラスイッチ(14)
原因3
巨大なタスクを分割処理する変更を加えた時に、オンメモリのク
ラスファイルだけではなく、ファイルシステム上のファイルが参
照されるようになっていた。
原因4
ソースとバイナリの日付を比較して、ソースが古い場合はソー
スが自動的に再コンパイルされるようにした。が、ここではソー
スの文字エンコードを明示的に指定できなかった(ファイルしか
見えないので当然ではある)
40 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 41. ピタゴラスイッチ(15)
帰結
原因2+原因3+原因4+Windowsのファイル日付精度が
異なる現象、の複合により、Javaファイルの再コンパイル
が走るようになってしまった。
原因1によりJavaファイルの文字コードとプラットフォーム
デフォルトの文字コードが異なっていた。以上により、コン
パイルエラーが発生した。
Windowsでしか起きない為、テストもすり抜けていた。
41 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 42. ピタゴラスイッチ(15)
対処
どんな時も、ソースを書き出してからバイナリを書きだす
教訓
大山鳴動しても底にいるのはねずみ一匹くらいかも
一匹じゃなくてもっといるかも
小さな違いも、複数組み合わさると、予想しない連鎖をする
(ピタゴラスイッチ的)
それが環境依存の振る舞いだと、より複雑に……
42 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 43. 完全犯罪
43 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 44. よくある処理
ディレクトリを作る
その下にファイルを作る
何かする
ファイルを消す
ディレクトリを消す
→書いてみる
44 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 45. 処理のひな型
File dir = new File("temp");
dir.mkdir();
File file = new File(dir, "file");
file.createNewFile();
doSomething(file);
if (!file.delete()){
throw new IOException("failed to delete:"+file); }
if (!dir.delete()){
throw new IOException("failed to delete:"+dir); }
45 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 46. doSomething (バグ入り)
private static void doSomething(File file)
throws IOException {
FileOutputStream os =
new FileOutputStream(file);
os.write("test test test¥n".getBytes());
os.flush();
// os.close();
}
46 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 47. 結果(1)
$ java TestD
(no error!)
47 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 48. 結果(2)
> java TestD
Exception in thread "main"
java.io.IOException: failed to delete:temp¥file
at TestD.main(TestD.java:15)
48 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 49. 結果(3)
$ java TestD
Exception in thread "main"
java.io.IOException: failed to delete:temp
at TestD.main(TestD.java:18)
$ ls –al temp
(empty!!)
49 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 50. 結果まとめ
環境
結果1 Linux : 正常動作
結果2 Windows : ファイル削除に失敗
結果3 Linux + NFS : ディレクトリ削除に失敗
結果が全部違う…
しかも、結果3 に至っては、空ディレクトリなのになぜ
かディレクトリの削除に失敗している……
50 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 51. Linux と Windowsの動作の違い
UNIXは、オープンしているファイルを削除できる
というか、“delete on last close” という定型コード
削除したファイルはクローズするまで存在が保証されている
Windowsのファイルロックは悲観ロック しかも自動
ファイルをオープンしていたら削除等が出来なくなる
UNIXと同じようには書けない
51 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 52. NFSはもっと恐ろしい
UNIXは、ファイルを削除してもファイルにアクセスできる
NFSプロトコルでは、「ファイル名」がないとファイルにアクセス
できない
すなわち 「オープンされてるファイルは削除できない」
だけどWindowsと違って「悲観ロックではない」「削除は失敗で
きない」
“Silly Rename” (see also: http://nfs.sourceforge.net/ )
52 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 53. Silly Rename
NFS上のファイルをオープン
オープンしたままファイルを削除すると
ファイルは削除されない
temp -> .nfsXXXXに自動的に名前が変更
NFSクライアントプロセス終了時
名前変更されたファイルが自動的に削除される
.nfsXXXX -> 削除
53 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 54. 発生していた現象
NFS上にファイル作成
NFS上でオープン中のファイルをdelete()
NFSがファイルを.nfsXXXXにリネーム
ディレクトリ削除時、ディレクトリ下にファイル.nfsXXXXが存在
ディレクトリ削除失敗(問題発生)
IOExceptionでプログラム終了
プロセス終了時にNFS側で.nfsXXXX削除
(証拠隠滅完了!暗黙的!全自動!)
54 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 55. 後からはわからない
再現すればそれほど難しい問題ではない
deleteに失敗する所にブレークポイントを設定
しかし他の環境で起きた場合は?
痕跡は残らないので調査が困難
close忘れという単純なバグでも起きる
JDK7にしよう! try-with-resources を使おう!
55 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 56. doSomething (バグフリー)
private static void doSomething(File file)
throws IOException {
try (FileOutputStream os =
new FileOutputStream(file)) {
os.write("test test test¥n".getBytes());
os.flush();
}
}
56 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 57. One more thing…
しかし、この問題が本当に恐ろしいのは、バグでなく
ても起きる点である。
57 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 58. doSomething (mmap ver)
private static void doSomething(File file)
throws IOException {
try (RandomAccessFile ras =
new RandomAccessFile(file,"rw")) {
FileChannel fc = ras.getChannel();
MappedByteBuffer mbb =
fc.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
mbb.put("test test test¥n".getBytes());
}}
58 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 59. 原因と結果
クローズ忘れていた時と同じ
unmapされてないfileは…
JavaのFileChannelにはmap()はあるけどunmap()がない
unmap()はどこから呼ばれる?
unmap()はいつ呼ばれる?
Reference Handler Thread上で発見
59 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 60. unmap時のcall stack (java + native)
#0 0x0000003dc22d0d20 in munmap () from
/lib64/libc.so.6
#1 0x00002aaab6870477 in
Java_sun_nio_ch_FileChannelImpl_unmap0 ()
[2] sun.nio.ch.FileChannelImpl$Unmapper.run
(FileChannelImpl.java:746)
[3] sun.misc.Cleaner.clean (Cleaner.java:142)
[4] java.lang.ref.Reference$ReferenceHanlder.run
(Reference.java:141)
60 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 61. sun.nio.ch.FileChannelImpl
public MappedByteBuffer map(MapMode mode, long
position, long size) throws IOException {
....
Unmapper um = new Unmapper(addr, mapSize, isize,
mfd);
....
return Util.newMappedByteBufferR(
isize, addr + pagePosition, mfd, um);
61 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 62. Direct-X-Buffer.java.templete
// For memory-mapped buffers -- invoked by
// FileChannelImpl via reflection
protected Direct$Type$Buffer$RW$(int cap, long addr,
FileDescriptor fd, Runnable unmapper) {
#if[rw]
super(-1, 0, cap, cap, fd);
address = addr;
cleaner = Cleaner.create(this, unmapper);
62 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 63. sun.misc.Cleaner
* General-purpose phantom-reference-based cleaners.
* <p> Cleaners are a lightweight and more robust alternative to finalization.
* They are lightweight because they are not created by the VM and thus do not
長々といろいろ書いてあるけど要は
* require a JNI upcall to be created, and because their cleanup code is
* invoked directly by the reference-handler thread rather than by the
* finalizer thread. They are more robust because they use phantom references,
クラスライブラリの中からだけつかえる
* the weakest type of reference object, thereby avoiding the nasty ordering
* problems inherent to finalization.
ファントムリファレンスベースのFinalizerもどき
* <p> A cleaner tracks a referent object and encapsulates a thunk of arbitrary
* cleanup code. Some time after the GC detects that a cleaner's referent has
でもFinalizerの代わりにはならない
* become phantom-reachable, the reference-handler thread will run the cleaner.
* Cleaners may also be invoked directly; they are thread safe and ensure that
単純で素直なクリーンアップの時だけ使える
* they run their thunks at most once.
* <p> Cleaners are not a replacement for finalization. They should be used
→unmap()はGCされたら呼ばれる
* only when the cleanup code is extremely simple and straightforward.
* Nontrivial cleaners are inadvisable since they risk blocking the
* reference-handler thread and delaying further cleanup and finalization.
63 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 64. mapがunmapされるとき
MappedBufferがGCされる
ReferenceHandlerスレッドが動き出す
Cleanerに登録されていたUnmapperが呼び出される
munmapシステムコールが呼ばれる
64 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 65. 邪道しかない
動かす方法はあるが……
_人人人人人人人_
> System.gc(); <
 ̄^Y^Y^Y^Y^Y^Y ̄
邪道!実装依存!
邪道!実行時依存!
邪道!タイミング依存!
65 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 66. 蛇足
実は、本当に起きた現象では、ここでは言い尽くせない理由
のせいで、テコでもファイルディスクリプタが消えず、結局Silly
Renameのせいでファイルが消えない問題は解決しなかった
でも…
プログラムが動かないときの、プログラマの言い訳
第5位 それ、動かないとホントに困るの? (えっうん)
ユーザは正常に動作させたいだけであって、ディレクトリを消
したいわけではない(場合もある)
66 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 67. おわりに
67 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 68. まとめ
手元で再現しないバグはある
様々な異なる実装があっても仕様を満たしていればJava
同じインタフェースでも返り値の精度が違うことがある
同じインタフェースでも動作の意味が違うことがある
明確なバグがなくても環境によって動かないことがある
実装の抽象化は大事!でも破綻も付き物!
どこに罠があるかという感覚を養いたい
68 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 69. Q&Aコーナー
( ´_ゝ`)フーン
69 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.
- 70. 70 Copyright © 2013, Oracle and/or its affiliates. All rights reserved.