EatSmartシステム部ブログ

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

JAVAのマーカーインターフェースとアノテーション

非キャッシュの仕組み

弊社のシステムはJAVA + strutsで実装されている所が多いのですが、サイト上のページはアクセスの負荷に耐えられるように、WEBサーバーやAPPサーバーから、キャッシュされた情報を返すようにシステムを構築しています。
ただ、ページによっては毎回Actionを実行して、リアルタイムなデータを表示する必要があります。そのために、Actionが特定のマーカーインターフェースかアノテーションを実装していたら、HTTPレスポンスヘッダに

Cache-Control: no-store
Pragma: no-cache

を出力するようにしようと考えました。

疑問点

やりたい事はマーカーインターフェースでもアノテーションでも可能なのですが、実装するにはリフレクション的な事を行うので、どのやり方が一番処理が軽いのか、疑問が浮かびました。
interfaceを実装しているかどうかを調べる方法も、instanceofを使う方法とClass.isAssignableFromを使う方法があるので、それに関しても検証してみたいと思います。

やってみる

で、疑問点を解消するために、実際に実験をしてみました。

  • interface

マーカーインターフェースなので、中身は無いです。

public interface NoCacheInterface {
}
  • annotation

リフレクションするので、RetentionPolicyはRUNTIMEです。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface NoCache {
}
  • Actionクラス

マーカーインターフェースとアノテーションを実装します。
BaseActionはstrutsのActionを継承して、前/後処理(そこでHTTPレスポンスヘッダの出力も行う)等を行います。
この実験では特に意味は無いですが…。

@NoCache
public class TestAction extends BaseAction implements NoCacheInterface {
  • 検証コード

それぞれのやり方でぐるぐるループさせます。
処理の前にgcをして、後に残っているメモリ量も見てみます。

TestAction action = new TestAction();
int count = 100000000;
long start = 0;

-- インターフェース(instanceof)
System.gc();
start =System.currentTimeMillis();
for(int i=0;i<count;i++){
	if(action instanceof NoCacheInterface){
	}
}
System.out.println((System.currentTimeMillis()-start)+"msec");
System.out.println(Runtime.getRuntime().freeMemory());

-- インターフェース(isAssignableFrom)
System.gc();
start =System.currentTimeMillis();
for(int i=0;i<count;i++){
	if(NoCacheInterface.class.isAssignableFrom(action.getClass())){
	}
}
System.out.println((System.currentTimeMillis()-start)+"msec");
System.out.println(Runtime.getRuntime().freeMemory());

-- アノテーション
System.gc();
start =System.currentTimeMillis();
for(int i=0;i<count;i++){
	if(action.getClass().getAnnotation(NoCache.class)!=null){
	}
}
System.out.println((System.currentTimeMillis()-start)+"msec");
System.out.println(Runtime.getRuntime().freeMemory());

結果

以下のようになりました。

4msec
13829000
8msec
13661280
3674msec
13468800

結果は

  1. マーカーインターフェース(instanceof)
  2. マーカーインターフェース(isAssignableFrom)
  3. アノテーション

の順で、処理速度が速く使用メモリが少ないという事になりました。
なので、マーカーインターフェースで実装してinstanceofで確認することにします。
ただ、1億回もループしてこの程度ですけどね。

タグマネージャを利用した直帰率に影響を与えないイベントトラッキングの設定について

Google アナリティクスのイベントトラッキング集計にGoogleタグマネージャ(Ver2)を使用していますが、今回直帰率に影響を与えないイベントトラッキングの集計を行う必要があったのでその設定方法等についてまとめてみました。

経緯

タグマネージャを利用したイベントトラッキングは既に行っていましたが、ページ内でアクションされた場合はアクティブと考えて直帰ではないと解釈していましたが、今回は、ページ表示時のイベントをトラッキングする必要があり、それをアクションとしてしまうとアクション率が高くなってしまう為、新たにタグを追加する事にしました。

Googleタグマネージャによるタグの追加

タグマネージャーの基本設定は一通り完了している前提で、新たに直帰率に影響を与えないイベントトラッキングを追加します。

トリガの追加

現在のイベントトラッキングと重複しない様に、新たなトリガを追加します。

・イベントを選択
 [カスタムイベント]

・配信するタイミング
 [イベント名].*(正規表現一致を使用にチェック)
 [これらすべての条件が true の場合にこのトリガーを配信]
  [等しい]の条件を選択し[新たなイベント名を追加]
タグの追加

上記で追加したトリガを使用して、新しくタグを発行します。

・プロダクトを選択
 [Google Aanalytics]

・タグの種類を選択
 [ユニバーサル アナリティクス]

・タグを設定
 [トラッキングタイプ]を[イベント]で追加

・カテゴリ/アクション/ラベル/値
 任意の変数を設定

・非インタラクション ヒット
 真を設定※これを設定することで直帰率に影響を与えない様になります。

・配信するタイミング
 上記追加したトリガを設定

プレビューとデバッグ

Googleタグマネージャーを使用すると、Chromeブラウザでリアルタイムにタグのプレビューとデバッグが可能なのでとても便利です。簡単ですがその手順を記載します。

プレビュー

プレビュー方法について

  1. タグマネージャの管理画面右上の「公開」ボタン右の下矢印をクリック
  2. 「プレビューとデバッグ」の項目にある「プレビュー」ボタンを押す
  3. 編集中のバージョンが「プレビュー中のバージョン」となり、プレビューモードに切り替わる
  4. Chromeブラウザの新しいタブを開き対象のサイトにアクセスすると画面下部にタグのプレビュー状態が表示される
デバッグ

デバッグChromeのアドオンでGoogle Analytics Debuggerを使用しました。今回は、ある条件でのみ表示する誘導リンクの表示イベントについて確認を行いました。

  1. Google Analytics Debugger - Chrome Web Store(アドオン)を追加する
  2. 対象のサイトでCtrl+shift+Iにて検証モードを表示する
  3. console画面に切り替えて、対象のサイト再読み込みする
  4. タグが正しく設定されていると、console画面に対象イベントが表示されます。※正しく表示されない場合、タグの確認と併せて、トリガの確認も行ってみて下さい。
  5. 最後に、イベントの「▼」をクリックして詳細を確認します。※noninteraction(非インタラクション )がtrueであることが確認できます。


皆さんも機会があったら活用してみて下さい。

もぐナビで行ったデプロイの改善

昨年までもぐナビで行ったデプロイの改善を書きたいと思います。

これまでのデプロイには手作業が含まれるためいくつか問題点がありました。
これを、Jenkins/Balancer Managerを利用しすることで、安全かつサービスが停止することなくデプロイを行えるようにしました。

これまでのデプロイ

もぐナビはJavaをベースに各種ライブラリ、独自フレームワークを利用して構築されています。
リバースプロキシにApacheを、バックエンドにTomcatを利用しています。
これまでは、以下の手順でデプロイを行っていました。

  1. 各バックエンドサーバにsshでログインを行う
  2. svnから最新のファイルをチェックアウト
  3. antでjavaをビルド
  4. Tomcatのリスタート

この方法には、以下の様な問題点がありました。

  1. 全て手作業のため、人為的ミスが発生する可能性がある
  2. デプロイ中に503エラーが発生する
  3. デプロイに失敗した場合にサービスが停止してしまう

最初の改善:Jenkinsの導入

最初の改善として、Jenkinsの導入を行い手作業で行っている箇所を自動化することにしました。
合わせてビルドを各サーバで行うのではなく、Jenkinsで行う用にします。
これにより、デプロイごとの成果物を管理することができるため、切り戻しや障害発生時の調査が行えるようになりました。

Jenkinsのジョブで以下の処理をシェルスクリプトで実行するようにしました。

  1. Jenkinsでsvnから最新のファイルをチェックアウト
  2. Jenkinsでantでjavaをビルド
  3. サーバAへデプロイ
  4. サーバBへデプロイ
  5. 以下同様

これにより、デプロイはボタンひとつで安全に行う事ができるようになり、問題点の1が解決しました。

またこれとは別に、Jenkinsで定期的なテストも実行しています。
これにより、不完全な状態でコミットされた場合もリリース前に検知出来るようになりました。

次の改善:Balancer Managerの利用

デプロイは安全に出来るようになりましたが、引き続き問題点の2,3が残っています。
特に問題点の2はサービスを快適に利用してもらう上で解決しなければならないものです。
これを解決するため、ApacheにBalancer Managerを導入し、デプロイ中のサーバにリクエストを振り分けを行わないようにします。

Balancer Managerの導入

httpd.confの以下の行のコメントを解除します。

LoadModule proxy_balancer_module modules/mod_proxy_balancer.so

アクセスするための設定を追加します。
今回は社内とJenkinsからのみアクセス出来るようにしました。

ProxyPass /balancer-manager !
<Location /balancer-manager>
  SetHandler balancer-manager
  Order deny,allow
  Deny from all
  Allow from xxx.xxx.xxx.xxx
</Location>

/balancer-managerへ接続すると、管理画面にアクセスすることが出来るようになります。

デプロイへの組み込み

Jenkinsのジョブを以下のように変更しました。
デプロイを行う前にリクエストの振り分けを停止し、起動を確認してから再開するようにしました。

  1. Jenkinsでsvnから最新のファイルをチェックアウト
  2. Jenkinsでantでjavaをビルド
  3. サーバAへリクエストの振り分けを停止
  4. サーバAへデプロイ
  5. サーバAの起動を確認
  6. サーバAへリクエストの振り分けを再開
  7. サーバBへリクエストの振り分けを停止
  8. 以下同様

これにより、問題点の2を解決することが出来ました。

リクエストの振り分けを再開する前に正常にデプロイが行えたかを確認し、万が一失敗した場合は以降の処理を停止するようにしました。
これにより、問題点の3を解決することが出来ました。

リクエストの振り分けの停止・再開には以下のようなスクリプトを用意して行っています。

#!/bin/bash

# 1:balancer_managerのURL
# 2:balancer名(balancer://xxxxのxxxx)
# 3:enable/disable
# 4:Worker URL

set -e

print_usage()
{
 echo "Usage: $0 http://balancer-manager-url balancer-name enable|disable http://worker-url"
}

WORKER_URL=$4
ACTION=$3

if [ "${WORKER_URL}" == "" ]; then
  print_usage
  exit 1
fi
if [ "${ACTION}" == "" ]; then
  print_usage
  exit 1
fi

BALANCER_MANAGER_URL=$1
BALANCER_NAME=$2
BALANCER_NONSE=`curl -s ${BALANCER_MANAGER_URL}  | sed -n "/href=/s/.*href=\([^>]*\).*/\1/p" | tail -1 | sed -n "s/.*nonce=\(.*\)\"/\1/p"`

if [ "${BALANCER_NONSE}" == "" ]; then
  echo "Could not extract nonce from ${BALANCER_MANAGER_URL}"
  exit 1
fi

curl -s "${BALANCER_MANAGER_URL}?b=${BALANCER_NAME}&w=${WORKER_URL}&dw=${ACTION}&nonce=${BALANCER_NONSE}"

まとめ

以上で安全かつサービスが停止することなくデプロイを行えるようになりました。
リバースプロキシとしてApacheを利用している場合はBalancer Managerの導入は容易なので、同様の問題がある場合は手軽に改善出来ると思います。

CentOS6.4にDockerをインストール

先日パーティションを拡張したサーバーに色々とインストールしようと思っていたのですが、実はサーバーがCentOS6.4だったため、今後移行する事も考え仮想化したゲストOSの上に構築しようと考えました。

で、色々調べたところ、CentOS6.4でDockerが使える?らしいので、Dockerの環境を構築することにしました。

Dockerのインストール

先ほどのリンク先を参考に

# rpm --import http://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6
# yum install -y http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
Loaded plugins: fastestmirror
Determining fastest mirrors
* base: www.ftp.ne.jp
* extras: www.ftp.ne.jp
* updates: www.ftp.ne.jp

Complete!

 としてからの

# yum install -y docker-io
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
epel/metalink | 5.9 kB 00:00
* base: www.ftp.ne.jp
* epel: ftp.riken.jp
* extras: www.ftp.ne.jp
* updates: www.ftp.ne.jp

Complete!

で、インストールは出来たようです。

まずは以下で起動します。

# /etc/init.d/docker status
docker は停止しています
# /etc/init.d/docker start
Starting cgconfig service: [ OK ]
Starting docker: [ OK ]

無事起動できたようです。

Dockerを起動できません

しかし

# docker run hello-world

Cannot connect to the Docker daemon. Is 'docker -d' running on this host?
# docker -d
WARN[0000] You are running linux kernel version 2.6.32-358.23.2.el6.x86_64, which might be unstable running docker. Please upgrade your kernel to 3.10.0.
INFO[0000] Listening for HTTP on unix (/var/run/docker.sock)
docker: relocation error: docker: symbol dm_task_get_info_with_deferred_remove, version Base not defined in file libdevmapper.so.1.02 with link time reference

 と、カーネルのバージョンの問題で、起動できていないようです。

試しに

# yum -y install libdevmapper.so.1.02
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
* base: www.ftp.ne.jp
* epel: ftp.kddilabs.jp
* extras: www.ftp.ne.jp
* updates: www.ftp.ne.jp

 Complete!

としたのですが、

# /etc/init.d/docker start

Starting docker: [ OK ]

# docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from hello-world
3f12c794407e: Pull complete
975b84d108f1: Pull complete
Digest: sha256:c96eeb93f2590858b9e1396e808d817fa0ba4076c68b59395445cb957b524408
Status: Downloaded newer image for centos:latest
Error response from daemon: Cannot start container b0468016e12ee2aa8b141f83537154c0941dc66e2ecd37a6b3418c4261a9791c: no such file or directory

となってしまいます。 

OSのアップデート

仕方が無いので、以下のようにCentOSをアップデート、

# yum clean all
# yum -y upgrade

としてから

# shutdown -r now

# uname -a

Linux chromium.eatsmart.lan 2.6.32-573.8.1.el6.x86_64 #1 SMP Tue Nov 10 18:01:38 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux  

 で、無事にアップデートできました。

#まだDockerの要求するバージョンには程遠いですが…

動きました

やっと無事にDockerを動かすことができました。

# docker run hello-world

Hello from Docker.
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
https://hub.docker.com

For more examples and ideas, visit:
https://docs.docker.com/userguide/ 

 で、Dockerで何を動かすんだっけ?

ブロードバンド回線の見直しについて

社員から社内ネットワークが遅いという意見が上がり、改善を図る事にしました。

改善前の設備

改善前の設備はこんな感じです。

WAN環境
 回線:光ネクストファミリーハイスピード
    下り200Mbps/上り100Mbps(ベストエフォート)
 TA:NTTよりレンタル
 プロバイダ:某プロバイダ(固定IP)を使用
LAN環境
 ルーター:WAN/LAN共に100Mbps対応
 スイッチングハブ:100Mbps対応
 LANケーブル:カテゴリ5e以上


定点観測
まずは、どれくらの速度が出ているのかをLAN環境から定点観測してみる事にしました。
また、計測には、Linuxcurl(speed_download)コマンドを使用しました。

1週間程計測した結果、平均6~7Mbpsの速度が出ている事が分かりました。


ブロードバンド回線見直し
いろいろ検討した結果、低コストでネットワークインフラへの影響も無い事から、ブロードバンド回線を見直して、定点観測を継続する事にしました。

[見直し後]
 光ネクストギガラインファミリー 下り1Gbps/上り1Gbps(ベストエフォート)


切替工事実施
切替工事は、予定通りネットワークインフラへの影響もなく無事完了しました。
そこで、切替後の定点観測結果を確認した所、まったく速度改善されていない事が判明しました。
そこで、改めて原因の切り分けを実施する事にしました。


原因切り分け
まずは、LAN環境から調査を実施します。

  • (定点観測している)クライアント-ルーター間は正しく100Mbpsを認識しているか?

[調査結果]
問題ありませんでした。※結果は下記の通り。

# ethtool eth0
Settings for eth0:
        Supported ports: [ TP ]
        Speed: 100Mb/s
        Duplex: Full
        Port: Twisted Pair
        Auto-negotiation: on
        Supports Wake-on: g
        Wake-on: g
        Link detected: yes

 

  • ルーターのWAN/LANポートは正しく100Mbpsを認識しているか?

[調査結果]
いずれも、問題ありませんでした。※結果は下記の通り。

WANポート 

State for ETH instance 0:

  Status ............................ ENABLED
  Link .............................. up
    Configured speed/duplex ......... Auto-negotiate
    Actual speed/duplex ............. 100 Mbps, full duplex 
    Auto-negotiation ................ complete

LANポート

Switch Port Information

  Port ............................. 1
  Status ......................... ENABLED
  Link State ..................... Up
  Configured speed/duplex ........ Autonegotiate
  Actual speed/duplex ............ 100 Mbps, full duplex
  Automatic MDI/MDI-X ............ Enabled

LAN環境には問題が無さそうだったので、次にWAN環境の調査を実施する事にしました。
※WAN環境を調査する為には、社内ネットワークを止める必要があった為、社員が帰宅後に実施しました。

  • WAN(TA)回線に直接接続しみたらどうか?

[調査結果]
LAN環境と速度が変わらない事が判明しました。

どうやらWAN環境に問題がありそうです。
 

  • プロバイダ側で速度制限を実施していないか?

[調査結果]
プロバイダの技術サポートに問い合わせた結果、直近数カ月速度制限は実施していないとの回答でした。

残るは、TAか光回線の問題が考えられる為、NTTのサポート窓口に問い合わせてみる事にしました。

[調査結果]
しばらくするとNTTから回答があり、光回線が不安定になっている事が判明しました。そこで工事に来て頂く事になりました。※工事は即日対応してもらいました。


結論
原因は光回線コネクタに問題があるとの事でした。
また、工事終了後の定点観測で、数十Mbpsまで速度が改善されました。

ネットワーク遅延はいろいろな原因が考えられますが、何かの参考になれば幸いです。

iOS端末でディスプレイの幅を取得する際の注意

EatSmartでは「もぐナビ」という食品クチコミサイトを運営しています。

今回はそのスマートフォン版サイトで発生した問題とその解決に関してです。

 

 

「ページの右に隙間が出来ている」

先日こんな指摘を受けました。普段から動作確認に利用しているiOS端末では再現しませんが、iPhone5sでは確かに右端に隙間ができていました。

経験上、これが発生するのはコンテンツの幅がディスプレイの幅より大きい場合です。

ページを見てみると、どうやら横幅336pxの広告が表示されているようです。

 

該当箇所を確認すると

もぐナビのスマートフォンサイトでは、ディスプレイの幅に合わせて横幅が300pxか336pxの広告を表示しています。

該当の箇所ではjQueryなどのライブラリを利用せず、以下のようなコードでディスプレイの幅を取得して広告を表示していました。

if (window.innerWidth > 320) {
// 横幅336pxの広告を表示
} else {
// 横幅320pxの広告表示
}

iPhone5sでは当然320が返り320pxの広告が表示されると思っていたのですが、実際にiOS9にアップデートしたiPhone5sで実行してみると、980が返っていました。

このことから、ディスプレイの幅が320pxにもかかわらず、横幅336pxの広告が表示されていることが確認できました。

 

正しいディスプレイの幅を取得するには

スマートフォンのサイトでは特にページの読み込み速度に注意を払っています。

jQueryなど便利なライブラリは沢山ありますが、ちょっとしたDOMの操作なら素のJavaScriptで処理したほうが良いと考えています。

今回のようにディスプレイの幅を取得するだけなら、以下のようにscreenから取得する方法を取りました。

if (screen.width > 320) {
// 横幅336pxの広告を表示
} else {
// 横幅320pxの広告表示
}

 

さいごに

今回は幸い実機が手に入ったので再現することが出来ましたが、スマートフォンはOSやバージョン毎に細かな挙動の違いがあります。

 PCブラウザやシミュレータだけでの確認ではなく、複数の実機で確認することの重要性を再確認しました。

CentOSのパーティション拡張

普段自分が使っていない社内用のサーバーに、とあるシステムをセットアップしようと思いログインしていろいろ見ていた所、HDDが500Gのうち450Gが/homeに割り当てられて、ほとんど使われていないという事が判明しました。

幸いにもLVMで構築されているので、今後色々と使う事も考え、パーティションのレイアウトを変更することにしました。

/homeの切り離し

まずは/homeを縮小するので、/homeをumountするために、OSをdisk起動する必要があります。

起動diskは、以前本番サーバーの障害時に使って便利だったSystemRescueCDを使います。SystemRescueCDは色々と必要なプログラムがはじめから入っているので、とても便利です。

CD-ROMに焼いたSystemRescueCDで起動をします。

LVMパーティションの縮小

縮小の手順としては、ファイルシステムのチェック、ファイルシステムのサイズ変更、LVMパーティションの縮小となります。

/dev/mapper/vg_chromium-lv_homeというパーティションを96Gにする場合、以下のようになります。

root@sysresccd /root % e2fsck -f /dev/mapper/vg_chromium-lv_home

root@sysresccd /root % resize2fs /dev/mapper/vg_chromium-lv_home 96G

root@sysresccd /root % lvreduce -L 96G /dev/mapper/vg_chromium-lv_home

LVMの拡張はmountされた状態でも可能なので、この時点で再度OSを通常起動しました。

縮小した結果、パーティション構成は以下のようになりました。

# fdisk -l

ディスク /dev/sda: 500.1 GB, 500107862016 バイト

ディスク /dev/mapper/vg_chromium-lv_root: 53.7 GB, 53687091200 バイト

ディスク /dev/mapper/vg_chromium-lv_swap: 4127 MB, 4127195136 バイト

ディスク /dev/mapper/vg_chromium-lv_home: 103.1 GB, 103079215104 バイト

LVMパーティションの拡張

次に、/dev/mapper/vg_chromium-lv_rootを拡張します。今後拡張する余地も残し、まずは192Gに設定して200G程度は残しておこうと思います。

LVMパーティションの拡張、ファイルシステムのサイズ変更の手順で行います。

# lvextend -L 192G /dev/mapper/vg_chromium-lv_root
Extending logical volume lv_root to 192.00 GiB
Logical volume lv_root successfully resized

# resize2fs /dev/mapper/vg_chromium-lv_root
resize2fs 1.41.12 (17-May-2010)
Filesystem at /dev/mapper/vg_chromium-lv_root is mounted on /; on-line resizing required
old desc_blocks = 4, new_desc_blocks = 12
Performing an on-line resize of /dev/mapper/vg_chromium-lv_root to 50331648 (4k) blocks.
The filesystem on /dev/mapper/vg_chromium-lv_root is now 50331648 blocks long.

最終的に、LVMのパーティション構成は以下のようになりました。

# lvdisplay
--- Logical volume ---
LV Path /dev/vg_chromium/lv_root
LV Name lv_root
VG Name vg_chromium

LV Size 192.00 GiB

--- Logical volume ---

LV Path /dev/vg_chromium/lv_home
LV Name lv_home
VG Name vg_chromium

LV Size 96.00 GiB

--- Logical volume ---

LV Path /dev/vg_chromium/lv_swap
LV Name lv_swap
VG Name vg_chromium

LV Size 3.84 GiB

ディスク容量としては

# df -Th
Filesystem Type Size Used Avail Use% マウント位置
/dev/mapper/vg_chromium-lv_root
ext4 189G 36G 144G 20% /
tmpfs tmpfs 1.9G 666M 1.3G 35% /dev/shm
/dev/sda1 ext4 485M 53M 407M 12% /boot
/dev/mapper/vg_chromium-lv_home
ext4 95G 11G 80G 12% /home

となりました。

案外簡単にパーティションのサイズを変える事ができました。