EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

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アプリでの優先度が以下のようになります。

  1. WEB-INF/classes
  2. 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. ユーティリティやフレームワークなど、複数のアプリケーション間で共通に使っているモジュール
  2. バイスごとのサイトを実装する際に、アプリケーションの共通のビジネスロジックをモジュール化したもの

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の導入について」の記事を掲載しましたが、導入後の効果について報告したいと思います。

eatsmart.hatenablog.com

2018年9月から導入に向けたシステム検証を重ね、本番環境へは2018年10月24日に導入しました。 今回は、導入前・後の1ヶ月間で効果測定してみました。

効果測定条件

効果測定条件は以下の通りです。

  • 測定期間
    変更前 9月23日~10月23日
    変更後 10月24日~11月23日

  • 測定ページ
    240px×240pxのサムネイル画像が8枚表示されているページ

  • 画像の種別
    概ねJPEG(一部pngあり)

  • 1か月のブラウザ利用状況
    safari 50%
    Chrome 25%
    safari(in-app) 10%
    Android Webview 10%
    その他 5%

  • ImageFluxのキャッシュヒット率
    2018年11月のトータルリクエストに対して、キャッシュヒット率は約65%でした。

効果測定結果

上記条件下で、GAにて計測を行ってみました。

ページ速度

導入前・後の1ヶ月間で15%速度改善しました

f:id:eatsmart:20190117210813p:plain

離脱率

導入前・後の1ヶ月間で30%離脱率が改善しました

f:id:eatsmart:20190117205244p:plain

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年に入ってからは実行に移すフェーズでした。

eatsmart.hatenablog.com

eatsmart.hatenablog.com

  • 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すると、UFWfirewallを解放するので、今回のさくらインターネットのサーバーのように直接WANへのネットワークインターフェースがある場合、そこのポートも解放されている、という事が発覚しました。
基本的にはdockerネットワークを使用し不要なポートはpublishしていないので、実際に解放されていても大きな問題では無いのですが、とても気持ち悪かったので気付いてよかったです。

7月〜9月

少しづつインフラ以外に着手

同じタイミングではないのですが、さくらインターネットに移転して、サービス利用の簡便さやネットワーク的な利点なども考えて、さくらのコンテンツ配信系サービスを導入してみました。
eatsmart.hatenablog.com

完全https化も実施して、http2利用など配信系の最適化に取り組みました。

  • 新構成でマイクロサービス構築

dockerコンテナでの稼働が基本になったので、インフラ環境を気にせずに新しい構成でのサービスを立てやすくなりました。なので、新しい機能を実装する際には、既存のモノリシックなアプリケーションに実装するのではなく、閉じた機能のマイクロサービスとして構築することに挑戦しています。

eatsmart.hatenablog.com

もともと既存アプリケーションのソースリポジトリとしてsubversionを使用しているのですが、ローカルとリモートのコミット戦略的な所ではgitの方が使いやすいかなと思っていました。今回、新機能をマイクロサービスで構築していくようになったことによって、新ソースのリポジトリをgitを使うようにしました。

新人2名採用

もともと通年で中途の採用活動を行なっていたのですが、応募も少なくなかなか採用に至りませんでした。そこで、経験の浅い新人を採用して業務を通して成長してもらおうという方針にして、2018年は第二新卒の人材紹介を通して採用を行いました。それが実り、2名も(!)採用することができました。

10月〜12月

がっつり機能開発

  • 「カロリーチェックAPI

弊社サービスの「カロリーチェックAPI」に機能追加をするにあたり、spring-bootを使用してRESTサービスを実装しました。

eatsmart.hatenablog.com

  • 「クスパ」の先生向け有料サービス

弊社サービスの 料理教室・パン教室・お菓子教室の総合情報サイト「クスパ」 に、新たな有料サービスの実装を行いました。ここでも、独立した機能についてspring-bootでマイクロサービスを作りました。
久しぶりに大きめのサービス開発だったので、新人含めほとんどのメンバーをアサインして開発しました。

実は上記の2サービスの稼働環境は、前半に移転したサーバーと異なっており、コンテナ化されていません。でも、spring-bootをFully Executable JAR化することで、JARファイルをデプロイするだけで、簡単にサービスを稼働させる事ができました。(サービスのポート番号など可搬性以外の所では、dockerの方が楽ですが。)

これからやりたいこと

2018年にインフラ環境が変わり、技術的自由度が上がり、新人参加によるチーム開発体制に変わりました。そこからの発展と挑戦として、2019年は以下の事などに取り組んでいけたらなあと思っています。

  • gitを使ったチーム開発手法の確立
  • マイクロサービス化の促進
    • ドメインによるサービス分割
    • 現行のモノリシックな構造の分解
    • データも分散化させたい
  • フロントエンドの取り組み
    • モジュール化
    • Vueやらreactやら

あまりまとまっていないですが、今年もありがとうございました。それでは良いお年を。

Apache / Tomcatの基本設定と連携

こんにちは、EatSmartの新人エンジニアです。

弊社サービスであるもぐナビの開発に入るにあたり 環境構築を行いました。ApacheTomcatの導入、基本設定について振り返ってみたいと思います。

WEBサーバー:Apache
APサーバー:Tomcat
言語:Java
IDEeclipse

Tomcatの基本設定(eclipseで起動させる)

eclipseからtomcatを起動できるように設定する

ウィンドウ → ビューの表示→ その他
サーバーを選択 → 新規サーバー
参照ボタンからインストールしたディレクトリを指定する
サーバーから追加される
アイコン「起動」「停止」「再起動」が表示される
f:id:eatsmart:20181219180935p:plain

●インストール完了

ブラウザでlocalhost:8080を開きtomcatの画面が表示されればインストールが完了している

tomcatの設定ファイル(context.xml)

コンテキストにはアプリケーション群の動作に必要なJSP,Servlet,HTML,war,画像などが含まれる

tomcatのログを出力させる

アクセスログ
アクセスログはコンテキストに設定されたJSP,サーブレット,HTMLなどのファイルのアクセス状況を表すログ
・サーバーログ

Apache基本設定

●設定ファイルの位置

Apacheに対する設定はhttpd.conf に記述する
インストールしたディレクトリからファイルの位置を確認する

●Servernameの設定

デフォルトでは既に設定されている
コメントアウトを外す
f:id:eatsmart:20181219180337p:plain

上記はローカル環境で動かしたときの例です

●リクエストを受け付けるときのポート番号の設定

Listen 80
Listen 443(HTTPS通信)
f:id:eatsmart:20181219182017p:plain

ポート番号は複数記述できる

●ServerRootの設定

Apacheがインストールされているディレクトリを指定する

●Includeで必要なファイルを取り込む

f:id:eatsmart:20181219182138p:plain

Apacheをサービスとして起動する

f:id:eatsmart:20181219180815p:plain

Apache httpdTomcatを連携する

●ApahceとTomcatを連携させる

インターネットからのリクエストをhttpdで受け付けて、Javaの動的な処理が必要なページの処理はTomcatで実行させる
そのためにAJPというプロトコルを用いて連携させる

AJP(Apache JServ Protcol)とは

AJPとはTomcatと連携させるプロトコルのこと
Apache httpdが受け付けたリクエストをTomcatに連携させるためにこのAJPプロトコルを用いて通信する
mod_proxy_ajpというモジュールをインストールして使用する

Tomcatで確認

Tomcatのconfディレクトリにあるserver.xmlファイルに設定が記述されている
f:id:eatsmart:20181220190651p:plain

「ポート8080番で HTTP 1.1 の通信を受け付ける」

f:id:eatsmart:20181220190848p:plain

「ポート8009番で AJP 1.3 の通信を受け付ける」

Apache httpdの設定

Apahce httpd の設定を行い連携させる
mod_proxy_ajp と mod_proxyを読み込ませるためにコメントアウトを外し、有効にする

次にどのパスにアクセスされた場合にTomcatと連携するのかを設定する
f:id:eatsmart:20181221093646p:plain

●最後にApacheTomcatを再起動して設定を反映させる