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