Tomcat8のjar読み込み順について
結論
- Tomcat8ではjarの読み込み順が決まっていない
Tomcat7以前のようにアルファベット順に読み込まれると思うと痛い目に遭います。
きっかけ
昨年末からサーバの増強を進めています。サーバの構築は約一年ぶりなので不安も有りましたが、以前作成したAnsibleのplaybookを利用することで問題なく作業も完了しました。 サーバの動作確認も問題無く終え、Webアプリの移行を行い本番環境に投入しました。 ところが、移行したTomcat8で動くアプリの特定のページでエラーが発生しているという報告を受けました。 実際にアクセスしてエラーが発生していることを確認し、エラーログを確認すると該当ページで"java.lang.NoSuchMethodError"が発生していました。
java.lang.NoSuchMethodError発生の原因
このエラーは、クラスにメソッドの定義がない場合に発生します。このエラー自体は知っていましたが、あまりお目にかからないものです。 メソッドの定義が無い場合、該当ページのクラスをビルドするタイミングで発見されるハズです。実行時にエラーが発生するということが不思議です。 もっと不思議なのは、そもそもこのアプリは別のサーバでは正常に動くのです。 気になるのが、該当のメソッドが定義されているクラスが複数のjarに存在することです。
xxx.yyy.Zzz
このようなクラスが aaa.jar と bbb.jar に存在し、該当メソッドは aaa.jar に含まれるクラスにのみ定義されていました。 長年Tomcatを利用したWebアプリを開発している経験では、名前の順で上になる aaa.jar のクラスが利用されるという認識です。 今回も当然 aaa.jar のクラスが利用されていると考えたのですが、念の為クラスが含まれるjarをログに出力させたところ、 bbb.jar となっていました。
jarの読み込み順
Tomcatのクラスローダでは、Webアプリでの優先度が以下のようになります。
- WEB-INF/classes
- WEB-INF/lib
この挙動を利用して、優先させたいクラスやプロパティファイルを WEB-INF/classes に配置することもありました。 WEB-INF/lib は、上で書いたようにアルファベット順に読み込まれるので、jarのファイル名を変更したこともありました。 ですが、その理解が間違っていたようです。検索していたところ、このような書き込みがありました。
https://stackoverflow.com/questions/5474765/order-of-loading-jar-files-from-lib-directory
Order for loaded jars in WEb-INF/lib folder.
For tomcat 5-7 the order is alphabetical. It uses sort.
For tomcat 8 is random decided by underlying file system.
Tomcat8から、ファイルシステムに依存するとあります。これを確認するため、ログにクラスローダが読み込んだjarを出力させてみました。
org.apache.catalina.loader.WebappClassLoader {context-class-loader=true, id=1539094878} file:/usr/local/tomcat/webapps/mognavi-backend/WEB-INF/classes/ file:/usr/local/tomcat/webapps/mognavi-backend/WEB-INF/lib/bbb.jar file:/usr/local/tomcat/webapps/mognavi-backend/WEB-INF/lib/eee.jar file:/usr/local/tomcat/webapps/mognavi-backend/WEB-INF/lib/ccc.jar file:/usr/local/tomcat/webapps/mognavi-backend/WEB-INF/lib/aaa.jar file:/usr/local/tomcat/webapps/mognavi-backend/WEB-INF/lib/ddd.jar
実際とは異なりますが、上記のように aaa.jar が bbb.jar より後に読み込まれていました。このため、メソッドの定義がないためエラーが発生していました。 ちなみに、移行前の環境でも同様のログを出力させたところ、 aaa.jar が bbb.jar より先に読み込まれていました。このため、いままこの問題に気付かなかったようです。
解決
解決方法としては、利用しない bbb.jar に該当するクラスを含まないようにビルドすることにしました。(bbb.jar は社内で制作したライブラリのため、この方法を採ることが出来ました) PreResourcesを利用する、ファイルシステムを変更する、など他の手段もあるようでしたが、そもそも該当クラスが利用されていないことと今後も同様のトラブルが他で発生する可能性があるので、このような対処をとりました。 これらはまた別の機会に調査したいと思います。
プライベートmavenリポジトリでのバージョンの扱いについて
弊社では、Webアプリケーションのビルド用に、かなり昔に作ったAntのスクリプトを使用しているところがあるのですが、JARのバージョン管理や、新しい開発者の環境構築の容易さを考えて、(今更ながら)Mavenを使うように試してみました。
プライベートリポジトリ構築
JARのバージョン管理と言っても、外部のJARについては頻繁にバージョンアップをすることは無いのですが、自社プログラムの共通モジュールは日々更新されているので、自社モジュールのバージョン管理が主目的となります。
そのため、自社環境内にmavenのプライベートリポジトリを構築して、依存関係の配布とモジュールのデプロイを行う必要があります。
すでに、社内向けに閉じたapacheがあるので、今回はWebDAVでリポジトリを構築しました。 WANからはアクセスできないので、以下の設定にしています。
DavLockDB /var/lib/dav/lockdb Alias /repository /var/mvn/repository/ <Location /repository/> DAV on Options +Indexes AuthType None Require all granted </Location> <Directory /var/mvn/repository/> Options Indexes FollowSymLinks Satisfy Any Allow from all </Directory>
あとは、使う側のpom.xmlに
<repositories> <repository> <id>mvn.eatsmart.jp</id> <name>eatsmart.jp mvn repository</name> <url>http://mvn.eatsmart.jp/repository/</url> </repository> </repositories>
を追加して、自社モジュールの依存関係を使えるようになりました。
WebDAVリポジトリへのデプロイ
モジュールをデプロイする側のプロジェクトでは、まず以下のようにpom.xmlにデプロイ先の定義をします。
<distributionManagement> <repository> <id>mvn.eatsmart.jp</id> <name>eatsmart.jp maven repository</name> <url>dav:http://mvn.eatsmart.jp/repository</url> </repository> </distributionManagement>
次に、maven3でWebDAVへデプロイするために、wagonというextensionを使う必要がある(Maven3でWebDavでjarをdeploy:deploy-fileしたら失敗する件)とのことで、以下の定義を追加します。
<build> … <extensions> <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-webdav-jackrabbit</artifactId> <version>2.12</version> </extension> </extensions> </build>
以上で、maven deployで自社リポジトリへデプロイできるようになりました。
モジュールのバージョン管理について
自社モジュールのバージョン管理と言っても、外部のJARと同様にすぐにバージョンアップに追随しない種類のものもあります。
大まかに分けると
1については、外部のJARと同様に、アプリケーションごとにある程度固定のバージョンを使用します。モジュールもバージョンによって機能やインターフェースに微妙な差異があり、それをバージョン管理する必要があります。
2については、デバイスごとで処理内容が異なることはあまり無い(ある場合は共通ロジックとしては使用しない)ので、特にバージョン情報で世代管理をする必要は無く、むしろ常に最新のバージョンに追随する必要があります。
そのため、1のためには通常のmavenの依存性管理を用いて、バージョン番号で世代管理をしようと考えています。バージョン番号としてはこちら(セマンティック バージョニング 2.0.0 | Semantic Versioning)に則り、互換性を保たないバージョンはメジャー番号、互換性を保つバージョンはマイナー番号、バグ修正やパッチ対応はリビジョン番号で表現したいと思います。ただ、社内の共通モジュールでは、そこそこ頻繁に機能追加が行われ、後方互換性とは関係無く新しいインターフェースが追加されるので、その範囲ではリビジョン番号の繰り上げで対応しても良いかなと考えています。
2については、通常のバージョン番号で管理する範囲の更新頻度ではありませんし、そもそも常に最新バージョンに追随する必要があるので、リビジョン 番号だけを延々と繰り上げていく運用か、あえてSNAPSHOTで運用しても良いかなとも考えています。この辺で何か知見があれば良いなあと思っています。
Javaのデータベース操作について
イートスマートの新人エンジニアが、JavaのDB操作について学んだ内容を振り返ってみたいと思います。
大まかなDB操作の流れ
①Connectionでデータベースとの接続を確立。 ②Statement(PreparedStatement)でSQLを実行。 ③ResultSetでSQLの実行した結果にアクセス。
以下、データベースの接続や操作について必要なパッケージ、インターフェースの説明です。
java.sqlについて
リレーショナルデータベースのデータへアクセスし、様々な処理をするAPIを提供。以下で具体的に説明するような機能が含まれている。
Connection
特定のデータベースとの接続を確立する。Connectionのオブジェクトは、自動コミットモードになっており、データベースへの変更が自動保存される。
Statement
SQLを実行し、実行結果を返す。パラメータなしのSQLの実行に用いられ、Connectionから、Statementオブジェクトを生成し、Statementのオブジェクト に対し、executeメソッドでSQLを実行する。
PreparedStatement
パラメータありのSQLの実行に用いられる。パラメータに実際の値を設定する際に、自動でエスケープシーケンスが行われるので、SQLインジェクションの対策になる。 引数として渡すSQLは事前にコンパイルされるため、SQL が多数回実行される場合にも、高速に実行できる。
ResultSet
ResultSetは、Statementの実行の結果、取得したデータへのアクセスができる。さらに、データ(表)の現在行を指すカーソルを保持しており、nextメソッドによって、 カーソルが次行へ移動する。
SQLException
データベースエラーなどの情報を提供(SQL文の誤りなどによって発生する)。
料理教室の先生情報を取得する
上記の説明を踏まえて、データベースへ接続しSQLを実行するコードを組み立てます。
import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class SelectCookingTeacher { public static void main(String[] args) { Connection conn = null; try { // JDBCドライバを読み込み(ここでは、PostgreSQLを使用する想定) Class.forName("org.postgresql.Driver"); // データベースへ接続(接続先のurlを指定) conn = DriverManager.getConnection( "jdbc:DB接続先","userid","password"); // 実行するSQLを定義(先生のID、名前、会員ステータスを取得するSQL) String sql = "SELECT TEACHER_ID,TEACHER_NAME,MEMBERSHIP_STATUS FROM COOKING_TEACHER"; PreparedStatement pStmt = conn.prepareStatement(sql); // SQL(SELECT文)を実行し、ResultSetで実行結果を受け取る ResultSet rs = pStmt.executeQuery(); // 結果に格納されたレコードをループ処理し、一覧表示(nextメソッドは、ResultSetが提供するメソッド) while (rs.next()) { BigDecimal teacherId = rs.getBigDecimal("TEACHER_ID"); String teacherName = rs.getString("TEACHER_NAME"); String membershipStatus = rs.getString("MEMBERSHIP_STATUS"); // (例)料理教室の先生に関するデータを出力している System.out.println("先生ID:" + teacherId); System.out.println("先生の氏名:" + teacherName); System.out.println("会員区分:" +membershipStatus); } } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { // データベース切断 if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
以上となります。
ImageFluxの導入効果について
以前「ImageFluxの導入について」の記事を掲載しましたが、導入後の効果について報告したいと思います。
2018年9月から導入に向けたシステム検証を重ね、本番環境へは2018年10月24日に導入しました。 今回は、導入前・後の1ヶ月間で効果測定してみました。
効果測定条件
効果測定条件は以下の通りです。
測定期間
変更前 9月23日~10月23日
変更後 10月24日~11月23日測定ページ
240px×240pxのサムネイル画像が8枚表示されているページ1か月のブラウザ利用状況
safari 50%
Chrome 25%
safari(in-app) 10%
Android Webview 10%
その他 5%ImageFluxのキャッシュヒット率
2018年11月のトータルリクエストに対して、キャッシュヒット率は約65%でした。
効果測定結果
上記条件下で、GAにて計測を行ってみました。
ページ速度
導入前・後の1ヶ月間で15%速度改善しました
離脱率
導入前・後の1ヶ月間で30%離脱率が改善しました
1ヶ月の短期間ですが、グラフからも分かる様にページ速度が向上したことで離脱率が大幅に改善しました。サイトの特性上季節要因等でトラフィックが増減するので、ロングテールでどうなるのか等引き続き検証していきたいと思います。
サーバのディスクを整理した話
あけましておめでとうございます。 本年もEatSmartシステム部ブログをよろしくお願いします。 新年最初の記事としては地味ではありますが、昨年末に行ったサーバのディスク整理について書きたいと思います。
昨年インフラをデータセンターからクラウドへ移行したことは以前書きました。当時のディスク使用量と増加分を見込んで、サーバのスペックの選定を行いました。 年明けからインフラを増強する計画があり、それに伴いディスクの使用量を確認したところ、当初の見込みより増加のペースが早いことがわかりました。 当初の見込みよりペースが早くなった要因は、主に以下の5つでした。
Dockerの不要なイメージ
クラウドへ移行してから、全てのサービスはDockerコンテナで稼働しています。 このため、以前に比べDockerイメージの数が増えたこと、テストや検証に伴いDockerイメージを作成することが増えたことで、ディスク使用量が増えていました。 不要なDockerイメージを削除するため"docker system prune"を実行しました。
docker system prune | Docker Documentation
この作業で、サーバによっては100GB弱の容量を確保することが出来ました。
Jenkinsのビルド履歴
基本的に全てのジョブで"古いビルドの破棄"を指定することにしており、不要なファイルが残らないようにしています。 同様に成果物も保存していません。これはDockerイメージとして残しているためです。 この設定がされていないものがあり、ディスク使用量が増えていました。 正しく設定することで、数GBの容量を確保することが出来ました。
ログの転送漏れ
バッチ処理等で出力されるログファイルは、全てログサーバへ転送し、一定期間が過ぎるとアーカイブされる仕組みになっています。 Dockerコンテナで稼働するバッチ処理で、ログの出力先が以前と変わったために上記の転送対象から外れてしまっていたものがありました。 この他、td-agentで集約するログの対処もされていなかったので、転送と削除を行うようにしました。 これにより、数十GBの容量を確保することが出来ました。
一時ファイルの削除忘れ
各種検証やログ解析のために一時的に作成したファイルが残っていました。 絶対的な量は少ないですが、これも削除しました。
まとめ
サービスは正常に稼働しており、またディスク使用量が閾値より少ない状態だったので、気付くのが遅れてしまいました。 以上の作業で、当初の見込みとほぼ変わらない増加ペースに戻すことが出来ました。
2018年を振り返って
2018年はサービスのシステム開発だけでなく、色々な挑戦や取り組みをした1年でした。 自分にとってどんな1年だったかを振り返ってみたいと思います。
今年やったこと
1月〜3月
iDC移転
今年の前半は何と言ってもiDC移転が一番大きなトピックでした。 計画・準備は2017年後半から取り組んでいたのですが、2018年に入ってからは実行に移すフェーズでした。
- DB移行
PostgreSQLのバージョンアップと(全サービスで)レプリケーションの構築を行いました。
また、データ移行については、
・旧DB→新DBへのWAN越しレプリケーションを構築し、移行時切り替え
・事前移行+当日差分移行(Postgresql DB移行 - EatSmartシステム部ブログ)
の2種類の移行を行いました。
- サービス移行
サービスの環境としては、dockerコンテナ化(swarmモード)という大きなチャレンジをしました。swarmに関しては一部動作が不安定な事があったので、その後使用をやめました。(kubernetesを選んでいたら違っていたかも?)
サーバー筐体もOSも仕組みも全て刷新されるので、安定性と性能のテストはしっかりして、安心できるようにしました。
4月〜6月
移転後環境の安定化
iDCの移転自体は、失敗する事なく完了したのですが、その後の稼働安定化に対応していました。
- 各サーバーのリソースチューニング
CentOS5からubuntu16+dockerになる事で、今までの知識が単純には使えなかったので、色々と調査しながら対応しました(以前が古すぎただけですが)。
- 諸々のモニタリング構築
元々zabbixでモニタリングをしていたのですが、OSの変更やdockerコンテナ化などの構成の変化によるメトリクス取得の変更を色々としました。
- nginxによるリバースプロキシ+swarmのローリングアップデート問題
サービスの入り口にnginxのリバースプロキシを置き、その後ろにswarmでクラスタする事で冗長性と可用性を実現しようとしていたのですが、swarmのローリングアップデート時にリバースプロキシに失敗するという現象がありました。また、本番環境ではないのですが、swarmのマネージャーが無応答になることがあるなど動作に不安があったので、swarmでのクラスタをやめ、アプリケーションサーバーによるクラスタに変更しました。
- dockerコンテナのpublish portガバガバ問題
dockerコンテナで外部にポートをpublishすると、UFWでfirewallを解放するので、今回のさくらインターネットのサーバーのように直接WANへのネットワークインターフェースがある場合、そこのポートも解放されている、という事が発覚しました。
基本的にはdockerネットワークを使用し不要なポートはpublishしていないので、実際に解放されていても大きな問題では無いのですが、とても気持ち悪かったので気付いてよかったです。
7月〜9月
少しづつインフラ以外に着手
- さくらのWebアクセラレータとImageFluxを導入
同じタイミングではないのですが、さくらインターネットに移転して、サービス利用の簡便さやネットワーク的な利点なども考えて、さくらのコンテンツ配信系サービスを導入してみました。
eatsmart.hatenablog.com
完全https化も実施して、http2利用など配信系の最適化に取り組みました。
- 新構成でマイクロサービス構築
dockerコンテナでの稼働が基本になったので、インフラ環境を気にせずに新しい構成でのサービスを立てやすくなりました。なので、新しい機能を実装する際には、既存のモノリシックなアプリケーションに実装するのではなく、閉じた機能のマイクロサービスとして構築することに挑戦しています。
- リポジトリをsubversionからgitへ
もともと既存アプリケーションのソースリポジトリとしてsubversionを使用しているのですが、ローカルとリモートのコミット戦略的な所ではgitの方が使いやすいかなと思っていました。今回、新機能をマイクロサービスで構築していくようになったことによって、新ソースのリポジトリをgitを使うようにしました。
新人2名採用
もともと通年で中途の採用活動を行なっていたのですが、応募も少なくなかなか採用に至りませんでした。そこで、経験の浅い新人を採用して業務を通して成長してもらおうという方針にして、2018年は第二新卒の人材紹介を通して採用を行いました。それが実り、2名も(!)採用することができました。
- http://eatsmart.hatenablog.com/entry/2018/07/31/151810
- http://eatsmart.hatenablog.com/entry/2018/08/27/113154
10月〜12月
がっつり機能開発
- 「カロリーチェックAPI」
弊社サービスの「カロリーチェックAPI」に機能追加をするにあたり、spring-bootを使用してRESTサービスを実装しました。
- 「クスパ」の先生向け有料サービス
弊社サービスの 料理教室・パン教室・お菓子教室の総合情報サイト「クスパ」 に、新たな有料サービスの実装を行いました。ここでも、独立した機能についてspring-bootでマイクロサービスを作りました。
久しぶりに大きめのサービス開発だったので、新人含めほとんどのメンバーをアサインして開発しました。
実は上記の2サービスの稼働環境は、前半に移転したサーバーと異なっており、コンテナ化されていません。でも、spring-bootをFully Executable JAR化することで、JARファイルをデプロイするだけで、簡単にサービスを稼働させる事ができました。(サービスのポート番号など可搬性以外の所では、dockerの方が楽ですが。)
これからやりたいこと
2018年にインフラ環境が変わり、技術的自由度が上がり、新人参加によるチーム開発体制に変わりました。そこからの発展と挑戦として、2019年は以下の事などに取り組んでいけたらなあと思っています。
- gitを使ったチーム開発手法の確立
- マイクロサービス化の促進
- ドメインによるサービス分割
- 現行のモノリシックな構造の分解
- データも分散化させたい
- フロントエンドの取り組み
- モジュール化
- Vueやらreactやら
あまりまとまっていないですが、今年もありがとうございました。それでは良いお年を。
Apache / Tomcatの基本設定と連携
こんにちは、EatSmartの新人エンジニアです。
弊社サービスであるもぐナビの開発に入るにあたり 環境構築を行いました。ApacheとTomcatの導入、基本設定について振り返ってみたいと思います。
■Tomcatの基本設定(eclipseで起動させる)
●eclipseからtomcatを起動できるように設定する
●インストール完了
●tomcatの設定ファイル(context.xml)
●tomcatのログを出力させる
■Apache基本設定
●設定ファイルの位置
●Servernameの設定
上記はローカル環境で動かしたときの例です
●リクエストを受け付けるときのポート番号の設定
ポート番号は複数記述できる
●ServerRootの設定
●Includeで必要なファイルを取り込む
●Apacheをサービスとして起動する
■Apache httpd と Tomcatを連携する
●ApahceとTomcatを連携させる
●AJP(Apache JServ Protcol)とは
●Tomcatで確認
「ポート8080番で HTTP 1.1 の通信を受け付ける」
「ポート8009番で AJP 1.3 の通信を受け付ける」