クリスマスを支える  
俺たちとJava
阪田  浩一  
フリュー株式会社  
関西Javaエンジニアの会
1
2
2
@jyukutyo
著書 3冊
関西Javaエンジニアの会
(関ジャバ) 中の人・発起人
blog:Fight the Future
http://jyukutyo.hatenablog.com/
フリュー株式会社 所属
エンジニア歴12年 36歳
SI業界での客先常駐 9年
Web系? 3年
文学部哲学科卒
塾講師アルバイト
阪田 浩一
要約
3
普通のエンジニアたちが
要約
4
いろんな  
技術的課題、  
プロセスの課題、  
チームの課題に
要約
5
ぶつかりつつも  
サービスを開発、運用  
しているお話です
6
扱えないこと:  
大規模サービスを  
運用する  
ベストな方法
目次
7
• 対象となるアプリケーションについて  
• わき上がる課題とその対処  
• RDBMS編  
• JVM編  
• サーバ編  
• まとめ
要約
8
本題へ
9
僕たちのアプリケーションは  
クリスマスの負荷に  
耐えられずにいた
Photo by Yuichi Sakuraba https://flic.kr/p/9JYRSQ
10
そう、クリスマスは  
1年で最もアクセスが  
ある日
Photo by Yuichi Sakuraba https://flic.kr/p/9JYRSQ
11
恋人たちがみんな  
撮影をする日だからだ
Photo by Yuichi Sakuraba https://flic.kr/p/9JYRSQ
12
撮影?  
そう、プリントシール機  
での撮影…
プリントシール機とは
13
14
弊社フリュー株式会社は  
プリントシール機の  
トップメーカーです
15
全国の機械の  
半分以上がフリューです
16
なのですが、  
今回はこの筐体の  
実装ではなく
簡単な図
17
ここの詳細に  
ついてです
Internet
Webとの関係は?
18
撮影した画像は  
Webサイト/アプリから  
取得できます
19
アプリを除く  
Webシステム部分  
(サイト、API、DB、  
ファイルサーバなど)  
について話します
20
会員数1000万人:  
10代から20代の女性  
(女子高生は  
クラスほぼ全員が会員)
サービスの概要
21
サービス名 ピクトリンク
会員数
1000万人  
(有料会員数は秘密…)
主な機能
プリントシール機で  
撮影した画像を  
取得できる
提供
Webサイト  
iOSアプリ  
Androidアプリ
課金
携帯電話キャリア課金  
(月額制)
PV 数百万PV/日
会員数の推移
22
23
画像は10億枚弱  
(うかつにcount(*)も  
できない)
24
大規模と思っています
25
現システムは  
4年前に構築  
(前身サービスは  
2003年に開始)
私の立ち位置
26
私は3年前にJoinし、  
今サイト開発のリーダ  
という立場です  
(つまりリリース後参画)
開発チーム
27
アプリ開発  
(ネイティブ/API)  
7名
サイト開発  
7名
インフラ  
(サーバ/NW/DBA)  
開発部門全体2名  
チーム内2名  
ログ分析  
2名
HTML/CSS  
2名
28
リリース後1,2年は  
クリスマスに  
サービスが瀕死
Photo by Yuichi Sakuraba https://flic.kr/p/9JYRSQ
29
なぜか?
30
まず、Webサーバの  
ロードアベレージが  
非常に高かった
31
平日は数百万PV/日  
だが  
クリスマスはその4倍
Photo by Yuichi Sakuraba https://flic.kr/p/9JYRSQ
32
夕食の時間「前後」に  
とくに集中する  
(ピークタイムが  
数時間単位となる)
Photo by Yuichi Sakuraba https://flic.kr/p/9JYRSQ
33
リクエストが  
さばけていない
34
プリントシール機で  
撮影した画像(プリ画像)  
を保存する処理なので  
軽い処理ではない
35
ここでサーバ構成を  
紹介
現在のサーバ構成
36台数は概数です
ユーザが保存処理
を実行するまでの
1週間分のみ  
37
ちょっと横道へ
38
MogileFSとは:  
分散ファイルシステム  
(OSS)
MogileFSとは
39
tracker:  
管理サーバ。  
クライアントとやり
取りする。
storage:  
ファイル保存  
サーバ。  
同一ファイルを  
複数台で保持。  
ファイルにはHTTP  
でアクセスできる。
metadata:  
RDBMS。  
ファイル情報や  
全体の設定を  
保存する。
40
ストレージノードが  
数十台  
(まだ増え続ける…)
41
ここまで大きな問題は  
出ていない
42
ファイルやノードを  
管理するMySQLが  
ボトルネックとなる  
かもしれない
43
ストレージノードは  
現実的には何台まで  
いけるのかわからないので、
だんだん怖くなっている
44
リクエストが  
さばけていない件
45
対応:Webサーバを  
台数追加した
46
教訓:お金を払おう
47
解決(えっ
48
サーバは  
増やせば対応できたが
49
今度は  
RDBMSが  
ネックとなってくる
50
next:RDBMS編
51
ここでアーキテクチャを  
紹介
アーキテクチャ
52
Javaフレームワーク
Seasarファミリー  
(Cubby/S2Dao  etc)
ファイルシステム  
(画像ファイル保存)
MogileFS
RDBMS
Oracle  11g(メイン)  
MySQL  5.6
Webサーバ/  
サーブレットコンテナ
Apache/  
Tomcat
Javaバージョン 6  -­>  8
53
• 近い将来メモリが不足する予
測ができた  
• IO性能が悪かった  
• レコード件数が多い  
• (テーブル設計もベストではな
いが)
54
クエリやインデックスを  
変更してコストを削減、  
でどうにかなる  
レベルではなかった
55
RDBMSの移行
56
変更前
Oracle  Database  11g    
Standard  Edition
1台
変更後
Oracle  Database  11g    
Real  Application  
Clusters(RAC)
複数台
57
RACとは:  
ロードバランス型の  
Oracleクラスタリング構成
58
Copyright© 2011, Oracle. All rights reserved.
5.7
Oracle
Instance
Oracle
Instance
Oracle
Instance
Active-Activeの構成をとることが可能
RACは、ノード追加によるスケールアウトで性能向上
process
process
process
process
process
process
「実践!高可用性システム構築  〜~RAC基本編〜~」より引用
59
全ノードが全データに  
アクセスできる  
Acitve-­Active構成  
となる
60
さらに  
ハードウェアも変更した
61
パフォーマンスは  
劇的に改善した
62
解決(えっ
63
教訓:お金を払おう
64
ただ、そうした増強分も  
使い果たしつつある
65
レコードが増えたことで  
見えていなかった問題が  
浮き彫りになる
66
DBAが来るまで、  
DBサーバのOS、  
Oracleとも  
適切にパラメータが  
設定されていなかった
67
そうしたパラメータを  
1つずつ精査しつつ…
68
クエリのコストや  
実行回数を  
調査すると…
69
DBA「夜間バッチの  
1クエリがコスト  
500万です!」
70
理由:テーブルを  
フルスキャンしている
71
テーブルに  
インデックスは…
72
貼られている!
73
クエリもインデックスを  
使ったカラムを  
where句で  
指定している
74
ログに出したSQL文を  
SQLDeveloperで  
実行してもすぐ終わる。  
実行計画でも  
インデックスを使っている
詳細:
75
しかし実環境では  
DATE型カラムへの  
インデックスが  
使われていない。
詳細:
76
???
詳細:
77
Oracleを見てみる
詳細:
78
SELECT SA.SQL_ID,
NAME,
WAS_CAPTURED,
BC.LAST_CAPTURED,
VALUE_STRING,
anydata.accesstimestamp(value_anydata),
datatype_string,
sql_text
FROM GV$SQL_BIND_CAPTURE BC, GV$SQLAREA SA  
WHERE BC.HASH_VALUE = SA.HASH_VALUE
AND SA.SQL_ID = 'hoge'  
ORDER BY LAST_CAPTURED DESC;
詳細:
79
datatype_string
-> TIMESTAMP
詳細:
80
バインド変数の  
値の型(Oracle上)が  
TIMESTAMPと  
なっている!
81
Javaアプリケーションでの型 java.util.Date
永続化ライブラリでの型 java.sql.Timestamp
JDBCでセットする時の型 java.sql.Types.Timestamp
Oracleでの型 TIMESTAMP
結論:
82
OracleのDATEは  
ANSI定義と異なり  
時間の情報を持つ
結論:
83
Types.Timestampとすることで  
時間情報も保持していた  
が、そのために  
OracleでTIMESTAMPとなり  
インデックスが使われなかった
結論:
84
対応:Oracle独自の  
PreparedStetementを  
使うことにした
対応:
85
if (ps instanceof OraclePreparedStatement){
OraclePreparedStatement ops =
(OraclePreparedStatement) ps;
// Oracle JDBCドライバ独自のメソッド、引数の型を利用する
ops.setDATE(index, new oracle.sql.DATE(new
java.sql.Timestamp(toDate(value).getTime())));
} else {
ps.setDate(index, toSqlDate(value));
}
結果:
86
コスト:  
500万  -­>  8万
結果:
87
大勝利
結果:
88
ってか何年間も  
誰も気づかなかったのか
結果:
89
教訓:実行計画を  
SQLクライアントで  
見るだけでなく  
実環境の実行コストも  
見よう
結果:
90
RDBMS関連だと  
こんな問題も  
ありました
91
企画「アプリでiOSの  
絵文字を使えるように  
したい!」
92
お、Unicodeで  
定義された  
絵文字だな
93
SELECT VALUE FROM
NLS_DATABASE_PARAMETERS
WHERE PARAMETER=‘NLS_CHARACTERSET';
!
-> JA16SJISTILDE
94
Shift_JISェ…
95
「NVARCHAR2に  
カラムの型を  
変更すればいける!」
96
JDBCでNVARCHAR2列  
に絵文字を入れる場合、  
JVMパラメータに  
「-Doracle.jdbc.defaultNChar=true」  
を追加する必要がある
http://otndnld.oracle.co.jp/document/products/oracle10g/102/doc_cd/java.102/B19275-03/global.htm
97
先にJVMパラメータだけ  
つけてアプリケーション  
起動しておくか  
(ステージングでは  
問題なし)
98
そう、あのときの  
俺たちには  
何の疑念も  
なかったんだ…
結果:
99
再起動した瞬間に  
全ユーザが  
ログインできなくなる  
(正確にはものすごく遅い)
100
理由:テーブルを  
フルスキャンしている
詳細:
101
defaultNChar=true
すると、VARCHAR2カラム  
のバインド変数に  
SYS_OP_C2C関数が  
適用される
詳細:
102
とあるテーブルのカラムhogeが  
VARCHAR2型のとき、  
defaultNChar=trueがあると…
PreparedStetemetの  
SQL
where hoge = ?
Oracleで実行される  
SQL
where
hoge = SYS_OP_C2C(:1)
詳細:
103
hogeへの  
インデックスが  
使われない
対応:
104
SYS_OP_C2C関数を  
使った  
関数インデックスを  
すべて作成した
結果:
105
教訓:実行計画を  
SQLクライアントで  
見るだけでなく  
実環境の実行コストも  
見よう
将来:
106
UTF-­8への移行を  
計画中
将来:
107
next:JVM編
108
ある日、  
JVMオプションがほとんど  
設定されていないことに気づく  
(XmxとPermGenくらい)
109
僕「GCログ出してみよう。」  
-Xloggc:gc.log
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:NumberOfGCLogFiles=3
-XX:GCLogFileSize=10M
110
数日後
111
112
僕「なんてこった…」
結果:
113
確保しているヒープ:  
最大3.2GB  
フルGC時間:  
最大0.9秒/回
114
こんなにヒープ領域  
使うほどのシステムでは  
ないような…  
これかなりSTWしてるよね
115
対応:GCアルゴリズムを  
CMSにしよう!  
※CMS  =  Concurrent  Mark  &  Sweep  
!
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+CMSClassUnloadingEnabled
詳細:
116
GCアルゴリズムの変更前後
変更前
Parallel GC【デフォルトGC】
(マイナーGCのみをパラレルで)
変更後
CMS GC【J2SE 1.4から】
ご存じない方は、ぜひsugarlife's blogさんの
ブログエントリ「CMS GC おさらい」を!
http://cco.hatenablog.jp/entry/2014/12/01/162240
結果:
117
確保した  
最大ヒープ領域
STW時間
変更前 3.2GB 最大 0.9秒/平均0.5秒
変更後 1.2GB 最大0.23秒/平均0.2秒
0
1
2
3
4
変更前 変更後
1.2
3.2
0
0.225
0.45
0.675
0.9
変更前 変更後
0.23
0.9
118
大幅改善!
119
120
ただし、CMSは  
CPUリソースを使うので、  
スループットが低下する  
恐れがある
121
僕らの場合、  
スループットが1%低下し  
CPUの負荷も  
やや上がった
結果:
122
教訓:JVMは  
デフォルトでも  
かなりいける  
(人類の叡智)
結果:
123
教訓:ただし、  
測定はしておき、  
増加傾向が出たら  
対処していく  
(最初から考えすぎない)
124
ある日、  
アクセスの少ない夜中に  
OutOfMemoryError  
が発生し、  
再起動する
125
「-­XX:+HeapDumpOnOutOfMemoryError」  
しているので  
ダンプファイルがある
126
java_pid<9999>.hprof  
4.3GB…
127
このダンプファイル  
どうしたらいいんだっけ?
128
Eclipse  Memory  
Analyzer(mat)  
を使う
https://eclipse.org/mat/
インストール方法
129
• Eclipseプラグインとして  
   Update  Managerからインストール  
• スタンドアローン版をダウンロード
130
いろいろな方法で  
可視化してくれた
131
132
ふむ…
133
134
135
136
うん…
137
138
ん!?
139
net.rubyeye.xmemcached.impl.MemcachedTCPSession
のインスタンスが  
ヒープの85.28%を  
占めてる!
140
1インスタンスが200MB  
とか…
141
対応:このライブラリの  
バージョンを上げよう!
結果:
142
教訓:  
「-­XX:+HeapDumpOnOutOfMemoryError」  
ほんとに役立つ!  
(これまで本番で  
OOME出たことなかった)
143
ある日、Java  SE  8が  
リリースされる
144
もう6から8に  
したくてしょうがなくなる
145
まずTomcatの  
起動VMを6から8に  
してみた
146
結果:何も問題  
なかった
147
プロジェクトのJDKを  
6から8にしてみた
148
結果:ユニットテストが  
ほとんど全部エラーになる
149
Caused  by:  java.lang.ArrayIndexOutOfBoundsException:  31038  
   at  org.objectweb.asm.ClassReader.<init>(Unknown  Source)  
   at  org.objectweb.asm.ClassReader.<init>(Unknown  Source)  
   at  org.objectweb.asm.ClassReader.<init>(Unknown  Source)  
   at  org.objectweb.asm.ClassReader.<init>(Unknown  Source)  
   at  
jp.co.dgic.testing.common.AsmClassModifier.getModifiedClass(AsmClas
sModifier.java:49)  
150
JUnit  +  djUnit…
151
indyが入ったことで  
djUnitが依存している  
ASMがエラーとなる
152
ASMを最新にすると  
APIが変わり  
djUnitがエラーとなる
153
うーん…
154
djUnitを止め、  
違うモックライブラリを  
使うことにする
155
対応:jMockitにして  
テストケースを  
すべて書き直す
156
jMockitにした理由:  
djUnitにある機能が  
すべてそろっていたから  
(意味を考えずに  
置換できる)
157
その他対応:  
Javassistでエラーが  
出たりしたので、  
これもバージョンを  
上げた
結果:
158
教訓:  
Java  SE  8を  
導入しよう  
(せめて実行環境  
からでも!)
159
next:サーバ編
160
そうこうしている間に  
サーバ台数が  
増えてきた
161
再起動時に  
各サーバのログ見るの  
面倒だなあ
162
全台SSHで  
tail  -­f  
(less  +Fも僕してたよ!)
163
見てられん…
164
ログ収集:  
fluentd  +  Elasticsearch  
+  Kibana  
で見るのが楽になった
165
166
fluentdでサーバ全台の  
ログを集め、  
それ1つを見るだけで  
済む
167
さらに
168
169
Kibanaで可視化  
される
170
教訓:  
「ログの可視化は  
エンジニアの責任」
171
ログはHDFSにも  
入れているので  
将来的に利用する  
予定だ
172
話は変わり、  
継続的に  
サーバが追加される状況
173
サーバ追加で  
いちいちインストール  
してられん…
174
サーバ構築:  
Ansible化を  
進めている
175
結局、去年は  
クリスマスに  
何も起こりませんでした
Photo by Yuichi Sakuraba https://flic.kr/p/9JYRSQ
176
今日の教訓のまとめ  
• お金を払おう  
• 実行計画をSQLクライアントで見るだけでなく実環境
の実行コストも見よう  
• JVMはデフォルトでもかなりいける(人類の叡智)  
• ただし、測定はしておき、増加傾向が出たら対処し
ていく(最初から考えすぎない)  
• Java  SE  8を導入しよう(せめて実行環境からでも!)  
• ログの可視化はエンジニアの責任
まとめ
177
スーパーエンジニアが  
いるわけじゃない
まとめ
178
普通のエンジニアたちが  
悪戦苦闘しながら  
運用してるよ
まとめ
179
かっこいい方法じゃ  
ない部分もあるけど、  
運用できてる!
まとめ
180
また  
こうやって得たものを  
発信したいです!
181
ご清聴  
ありがとうございました

クリスマスを支える俺たちとJava(JJUG CCC 2015 Spring AB4)