4月 16

こんにちは。新規事業推進室の石田です。

先週、店舗maticの新バージョンを本番環境へリリースしました。どんなに準備しても本番に適用すると思ってもやっぱり本番環境ではなにか問題が発生するわけで・・・

今日は、本番環境への適用後に発覚してパッチリリースを余儀なくされたバグについてお話します。

バグの原因は、Javaのインクリメント演算子(++)を変数への代入式の中でつけていたときの評価順というもので、原因究明にはかなり手こずりました。

以下は、i = 1 の状態で、以下の各言語でインクリメント演算子つきの変数同士の演算を試してみた結果です。

Java (Sun JVM 1.6.0_17)
++i = 2
i++ = 1
++i + ++i = 5
i++ + i++ = 3
++i + i++ = 4
i++ + ++i = 4
++i + i++ + ++i = 8
i++ + ++i + i++ = 7

C (gcc 4.1.0 (i586))
++i = 2
i++ = 1
++i + ++i = 6
i++ + i++ = 2
++i + i++ = 4
i++ + ++i = 4
++i + i++ + ++i = 7
i++ + ++i + i++ = 6

Perl 5.8.8
++i = 2
i++ = 1
++i + ++i = 6
i++ + i++ = 3
++i + i++ = 5
i++ + ++i = 4
++i + i++ + ++i = 9
i++ + ++i + i++ = 7

JavaScript (IE6/IE7/IE8/Firefox3.6)
++i = 2
i++ = 1
++i + ++i = 5
i++ + i++ = 3
++i + i++ = 4
i++ + ++i = 4
++i + i++ + ++i = 8
i++ + ++i + i++ = 7

Ruby 1.8.3 (parse error で計算不能)

なんと、言語によって結果がバラバラ。JavaとJavaScriptが一番自然ですかね。Perlなんてどこをどういう順序で評価したらそうなるのか意味不明です。

さらに、今回のバグなんですが、式の計算結果がJVMによるHotSpotの最適化の前後で変わってしまっているような状況です。

不具合の内容もさすがにテストで見逃すようなものではなかったし・・・同じバイナリが検証環境では正しく動いていたし・・・と訳分りません。

まあ、副作用のあるインクリメント/デクリメント演算子を式の中で使うなという事以外対策の取りようがなさそうです。

皆様もお気をつけください。では。

Tagged with:
3月 17

こんにちは。新規事業推進室の石田です。

各方面で報道されていますがMIX10で、IE9のプレビューバージョンが公開されました。

というわけで、さっそくダウンロードして試しています。

「HTML5に仕様通り対応します」はそりゃちゃんとやってくれるでしょうからあまり心配はしていないのですが、問題はこれまでのIE6向けIE7向けハックにあふれたHTMLをどの程度互換性をもって対応できているかに興味があります。

そこで、来月リリースする予定の店舗maticの新バージョンにIE9previewでアクセスしてみました。

店舗maticは、「X-UA-Compatible: IE=EmulateIE7」が指定されているためか、あっさりとふつうに表示されました。

つまらないので、強制的にIE9モードに変更すると、無理やりCSSで角丸のdivを表現していたりしたような箇所が軒並み崩れてしまいました。└(T_T;)┘

まあこれは、もともとのIEのCSSバグを使って実現していたhackなので仕方ありません。そのうちブラウザごとにCSSをスイッチできるように修正します。

しばらく触ってみて感じた大きな問題は、ポップアップウィンドウにCookieが引き継がれていないようです。

同様の問題はIE8のβ段階でも、タブごとにプロセスを分離した影響で結構最後まで残ってましたからこのあたりはリリースまで要注意かもしれません。

ポップアップ使うなよっていうツッコミもありますが・・

また、「マルチコアでJavaScriptの実行を高速化」というのも、IE8でも似たような最適化に伴なうレースコンディションバグと思しき現象で、今日現在も苦しめられているのでいまいち信用できません。(この件は、もうちょっとで解決出来そうなので解決できたらまた改めて書こうと思います)

まあ、まだ品質について語る段階ではないのでしょうけど、マイクロソフトさん頑張ってください。

それはそうと、HTML5は最近自分の中ではかなりのブームになってます。IE9の動向もあり、あと2年もするとWebアプリは別次元のユーザーエクスペリエンスになりそうで楽しみですね。

では。

Tagged with:
3月 05

getElementsByClassName()の落とし穴

そらまめのつぶやき, 未分類 コメントは受け付けていません。

こんにちは。新規事業推進室の石田です。

HTMLで、特定のクラス名を持つDOMエレメントを列挙してなにか操作をしたいときに、prototype.jsにあるgetElementsByClassName()を使うと探すエレメントを探す手間を省いてくれます。

で、このprototype.jsのgetElementsByClassName()は、エレメントを列挙してクラス名をチェックするという実装になっているので、当然遅いです。

そこで、Firefox3やChrome、Safariは、このgetElementsByClassName()がネイティブ実装されるようになりました。prototype.jsでもネイティブ実装があればそちらを優先するという風になってます。

ところが、このネイティブ実装版のgetElementsByClassName()は結果として返す配列のエレメントのクラス名を変更すると配列自体が変更されてしまうという挙動を示します。

わかりにくいと思うので、サンプルのHTMLを用意しました。

以下のHTMLですが、testというクラス名を持つdivが3つあって、ボタンを押すとそのdivのクラス名をtestからchangeに変更し、文字の色を赤くするという動作を期待して作りました。

function test() {
var elements = document.getElementsByClassName("test");
elements = Array.slice(elements, 0);
for (i = 0; i < elements.length; i++) {
elements[i].className = "change";
elements[i].style.color = "#ff0000";
}
}
<div class="test">壱</div>
<div class="test">弐</div>
<div class="test">参</div>

これをIEで表示すると prototype.js の getElementsByClassName() をつかうことになるので期待したとおりに、壱弐参の文字が赤くなります。

このHTMLを Firefox3 で表示した場合は、ボタンのクリックで弐だけが赤くなり、JavaScriptのエラーが報告されます。

さて、何が起きているのでしょうか?

どうやらFirefox3では、ループの中の、elements[i].className = “change”; を実行した時点で、elements の配列の中からこのエレメントがなくなってしまうようです。

ゲゲッ!

そんな仕様ないだろうと思って、HTML5のWorking Draft仕様を読んでみると確かに When called, the method must return a live NodeList object と書いてある。えー、ありえないでしょう。

http://www.whatwg.org/specs/web-apps/current-work/#dom-document-getelementsbyclassname

というわけで上記のコードは正しくは、

function test() {
var elements = document.getElementsByClassName("test");
elements = Array.slice(elements, 0);
for (i = 0; i < elements.length; i++) {
elements[i].className = "change"; // クラス名を変更する
elements[i].style.color = "#ff0000"; // 文字の色を変更する
}
}

と、一旦配列をコピーしてからクラス名の変更を行う必要があります。

これ、知らなかったのは私だけでしょうか・・・ひょっとして常識?

では。

Tagged with:
3月 05

JavaでCPU利用率を取得する方法

そらまめのつぶやき コメントは受け付けていません。

こんにちは。新規事業推進室の石田です。

さて、今日は2010年4月にリリースする店舗maticの新バージョンむけに書いたコードでの話しです。

店舗maticでは、朝の始業時などシステムの負荷がピークを迎える時間帯にはすぐにやらなくてもいい処理を後回しにするために、タスクキューの仕組みを作ってタスクキューにタスクを入れてすぐにユーザーにレスポンスを返すといった処理をやっています。

サーバーが暇になったら、タスクキューからタスクを取り出してタスクを実行するという風に動作します。

サーバーが暇かどうかを判断するには、LinuxとかのUnix系のOSだと一般的にはロードアベレージ値が使えます。当然JavaにもMXBean経由でロードアベレージを取得することができる OperatingSystemMXBean#getSystemLoadAverage() なんていうメソッドが用意されています。(このメソッドはJava6からですね)

問題はWindowsなどロードアベレージ値がないOS上でどうするかなんですが(といってもお前の作っている製品の対応OSはLinuxだけじゃないかというツッコミはさておき)、脊髄反射でCPU利用率を取得すればいいじゃないかと思ったところ、JavaにはドンズバCPU利用率を取得する方法は用意されていないということにいまさら気がつきました。(しってました?)

Google先生に聞いても、JNIとか身の毛もよだつ方法ばかりがヒット。

で、いろいろ調べてみると、OperatingSystemMXBeanのSunJVMでの拡張機能である、com.sun.management.OperatingSystemMXBeanには、getProcessCpuTime()というメソッドがあり、これを使うとJVMプロセスのCPU時間が取得できるらしいということがわかり、JConsoleのソースも参考にして作ったのが以下のクラス。

package com.dreamarts.system.utils;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Method;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class CpuUsageAnalyzer {

private static final CpuUsageAnalyzer singleton = new CpuUsageAnalyzer();

private AnalyzerThread analyzer = null;

public static float getCpuUsage() {
return singleton.analyzer.getCpuUsage();
}

private CpuUsageAnalyzer() {
analyzer = new AnalyzerThread();
analyzer.setDaemon(true);
try {
analyzer.start();
} catch (UnsupportedOperationException e) {
System.err.println("CpuUsageAnalyzer does not support this platform.");
}
}

private static class AnalyzerThread extends Thread {

private AtomicInteger cpuUsage = new AtomicInteger(Float.floatToIntBits(-1F));

private CountDownLatch startupLatch = new CountDownLatch(1);

private long prevCpuTime = 0L;

private long prevUpTime = 0L;

@Override
public void run() {
this.setName("CpuUsageAnalyzer");

try {
checkCompatibility();
} catch (UnsupportedOperationException e) {
startupLatch.countDown();
throw e;
}

while (true) {
updateCpuUsage();
sleep1Sec();
}
}

private void sleep1Sec() {
long after1Sec = System.currentTimeMillis() + 1000L;
while (System.currentTimeMillis() < after1Sec) {
try {
Thread.sleep(50L);
} catch (InterruptedException e) {
}
}
}

private void checkCompatibility() {
OperatingSystemMXBean osmx = ManagementFactory.getOperatingSystemMXBean();
if (osmx == null) {
throw new UnsupportedOperationException("Failed to get OperatingSystemMXBean");
}

Class[] interfaces = osmx.getClass().getInterfaces();
boolean hasInterface = false;
if (interfaces != null) {
for (Class i : interfaces) {
if ("com.sun.management.OperatingSystemMXBean".equals(i.getName())
|| "com.sun.management.UnixOperatingSystemMXBean".equals(i.getName())) {
hasInterface = true;
break;
}
}
}
if (!hasInterface) {
throw new UnsupportedOperationException("Incompatible OperatingSystemMXBean class: "
+ osmx.getClass().getName());
}

RuntimeMXBean rtmx = ManagementFactory.getRuntimeMXBean();
if (rtmx == null) {
throw new UnsupportedOperationException("Failed to get RuntimeMXBean");
}
}

private void updateCpuUsage() {
long cpuTime = getCpuTime();
long upTime = getUpTime();

long elapsedCpu = cpuTime - prevCpuTime;
long elapsedTime = upTime - prevUpTime;

int numProcessors = Runtime.getRuntime().availableProcessors();

float percentile = Math.min(100F, elapsedCpu / (elapsedTime * 10000F * numProcessors));

prevCpuTime = cpuTime;
prevUpTime = upTime;

cpuUsage.set(Float.floatToIntBits(percentile));

if (startupLatch.getCount() > 0) {
startupLatch.countDown();
}
}

private long getUpTime() {
RuntimeMXBean rtmx = ManagementFactory.getRuntimeMXBean();
return rtmx.getUptime();
}

private long getCpuTime() {
OperatingSystemMXBean osmx = ManagementFactory.getOperatingSystemMXBean();
try {
Method getProcessCpuTime = osmx.getClass().getDeclaredMethod("getProcessCpuTime");
getProcessCpuTime.setAccessible(true);
Object o = getProcessCpuTime.invoke(osmx);
if (o instanceof Long) {
return (Long) o;
}
} catch (Throwable e) {
}
return 0L;
}

private float getCpuUsage() {
try {
startupLatch.await(1500L, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}

return Float.intBitsToFloat(cpuUsage.get());
}

}
}

こんな感じで呼び出せば、CPU利用率っぽい値が取得できます。

float usage = CpuUsageAnalyzer.getCpuUsage();
if (usage < 50F) {
// CPU利用率が50%以下のときだけ実行
}

まあ、Unix系ならロードアベレージでいいので自分的にはお蔵入りのコードなんですが、Windowsむけのアプリを書いている人で同じところでハマった人がいれば参考にしてください。

では。

Tagged with:
3月 01

GCとキャッシュの悩ましい関係

そらまめのつぶやき コメントは受け付けていません。

こんにちは。新規事業推進室の石田です。
今日からDAブログに書くことになりました。よろしくお願いします。

さて、私が担当するのは、DA製品を開発する中でハマった技術的なあれこれです。

今日は、Javaのガベージコレクション(GC)について書いてみたいと思います。

DA製品でも、ひびきSm@artDBや、ひびきSALES、そして私が担当している店舗maticはJavaで開発されています。Javaを使うことで開発者はメモリリークに頭を悩ましノイローゼになることもなくなりますが、その代償として散らかしたメモリを片付ける処理が必要になります。これガベージコレクション(GC)です。

今日の本題は、GCとキャッシュの微妙な関係についてです。

さて、Webアプリケーションのパフォーマンスを向上させる常套手段としてキャッシュがあります。アプリケーションサーバーのメモリにオブジェクトをキャッシュしている場合、その情報の取得には0.01msぐらいしかかかりません。ところが同じ情報をデータベースから持ってくると、コネクションプールされているデータベース上でクエリーキャッシュにヒットするような最高の条件であっても10msぐらいはかかってしまいます。

なんと1000倍もの時間差があります。

小規模なシステムであれば10msであっても十分速くユーザーはその違いに気がつかないぐらいですが、大規模なシステムになるとリクエストされる回数が桁違いに多くなるのでこの10msが命取りになります。

CPUが1つで、プロセスも1つという単純なモデルで10msの仕事を逐次処理するとすると、1秒間に最大で100回までしか処理することができません。実際にはDBサーバーはマルチプロセッサで、マルチプロセスでマルチスレッドでと並行して処理をする仕組みがあるのでここまでひどいことにはならないのですが、だいたいのそこらのIAサーバー上で動くデータベースは、1秒間にせいぜい数百回の処理ぐらいしかできません。

ですから、毎回データベースに問い合わせるようなシステムだとユーザー数が大規模になったときにまともに動かないということになります。

そこで、アプリケーションサーバーのメモリ上にデータベースから読み込んだ結果のオブジェクトをキャッシュするという戦略をとっています。

キャッシュすることで、パフォーマンスは劇的に向上するのですが、キャッシュしたデータは限られたメモリ上にあるわけで、データが更新されてキャッシュが古くなるか、メモリが満杯になるかした場合はキャッシュを破棄しなければなりません。

このキャッシュの破棄とGCの組み合わせがイマイチなのです。

現在主流のJVMは、みな世代別GCというGCアルゴリズムを採用しています。世代別GCはほとんどのオブジェクトは、生成してすぐに消滅し、長時間生き残るオブジェクトは稀であるということを前提にしています。

一般的なプログラムであればこれは正しく、世代別GCは優れたアルゴリズムなのですが、上記のように積極的にキャッシュしようという戦略をとった場合に世代別GCは暗黒面を見せ始めます。

キャッシュするようなオブジェクトは短くても数分、長いと何日も生き残るようなものが多く、そのほとんどは世代別GCの旧世代領域に送られてしまいます。

旧世代領域を片付けるには、”Stop The World”と呼ばれるすべてのアプリケーションのスレッドを停止させて実行するFullGCが必要になります。FullGCが発生するとヒープメモリのサイズにもよりますが、数秒から長くて1分以上もの間アプリケーションが停止してしまいます。

大規模なシステムではたいていの場合、複数のアプリケーションサーバーをロードバランスしているので、GCによる停止が発生したサーバーをロードバランス対象から切り離したりして対応することもできますが、不幸にもリクエスト処理中にGCに巻き込まれたユーザーはGCの完了まで待たされてしまいます。

どうしようもない事態に見えますが、この問題はGCのチューニングでほとんど回避することが可能です。

まず、以下のJVMの起動オプションを見てください。

これは、私が設定した店舗maticの本番運用環境におけるTomcatを起動するときのJVMのパラメータです。(わかりやすいように1行に1個ずつ書いていますが実際にはひと続きです)

-server
-XX:+DisableExplicitGC
-verbose:gc
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-Xmx1280m
-Xms768m
-Xss512k
-XX:NewSize=512m
-XX:MaxNewSize=512m
-XX:SurvivorRatio=2
-XX:MaxTenuringThreshold=32
-XX:TargetSurvivorRatio=90
-XX:+UseConcMarkSweepGC
-XX:+CMSIncrementalMode
-XX:+CMSIncrementalPacing
-XX:CMSIncrementalDutyCycleMin=0
-XX:+CMSParallelRemarkEnabled
-XX:+CMSClassUnloadingEnabled
-XX:+CMSPermGenSweepingEnabled
-XX:+UseParNewGC
-XX:MaxPermSize=128m
-Djava.awt.headless=true
  • -server
    HotSpotをサーバー版で起動します。起動時間は長くなりますがよりアグレッシブに最適化コンパイラーがJavaのバイトコードをマシンコードに変換します。
  • -XX:+DisableExplicitGC
    System.GC()によるFullGCの起動を抑止します。お行儀の悪いライブラリが内部でSystem.GC()を呼び出しているので必ず指定します。
  • -verbose:gc -XX:+PrintGCTimeStams -XX:+PrintGCDetails
    GCの動作状況を標準出力に出力します。Tomcatの場合、標準出力は catalina.out に出力されますので、このログファイルがローテーションされるように注意が必要です。
  • -Xmx1280m
    ヒープメモリの最大サイズを1.2Gに設定しています。32bit版のLinuxですのでこのあたりが最大値になります。キャッシュするデータの量を増やしたいのでもう少し取れるといいのですが・・・。
  • -Xms768m起動時のヒープメモリのサイズを指定します。
  • -Xss512k
    スタックサイズです。これはこのままでいいと思います。
  • -XX:NewSize=512m
    世代別GCの新世代領域のサイズを512Mに設定しています。セオリーに比べると非常に大きなサイズですがなるべく旧世代に送らないことを目的に大きめに指定します。
  • -XX:MaxNewSize=512m
    世代別GCの新世代領域の最大サイズを512Mに設定しています。新世代領域のサイズは固定されたことになります。
  • -XX:SurvivorRatio=2
    新世代領域の中でもEden領域を大きめにします。ほとんどのオブジェクトはEdenで一生を終えるようになります。
  • -XX:MaxTenuringTHreshold=32
    Survivor領域にいる期間を長く指定してなるべくOld領域に送られないようにします。
  • -XX:TargetSurvivorRatio=90
    新世代領域が満杯だと判断される閾値を90%にします。これもなるべくOld領域に送られないようにするためです。
  • -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled
    FullGCで全スレッド停止時間を最小限にしてなるべくアプリケーションスレッドと並行して実行するように指定します。スループットは落ちますが完全停止時間がごくわずかになります。
  • -XX:+CMSIncrementalMode -XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0
    FullGCをインクリメンタルモードで動かします。一気にすべてのメモリを片付けないで細かく何度も片付けることで停止時間を小さくします。
  • -XX:+CMSParallelRemarkEnabled
    FullGCのremarkフェーズをマルチスレッドで実行します。FullGCの停止時間がCPU数に応じて短くなります。
  • -XX:+UseParNewGC
    新世代領域のGCをマルチスレッドで実行します。
  • -XX:MaxPermSize=128m
    パーマネント領域の最大サイズを128Mに設定します。

利用する機能やアクセスの頻度で最適値は変わりますが、この設定を参考に、是非お客様の環境でもDA製品のパフォーマンスを最大限に引き出してあげてください。よろしくお願いします。

では。

Tagged with:
グループウェア  | Webデータベース  | 多店舗オペレーション  | 課題一覧  | お客様事例  | お問い合せ
個人情報保護方針  | ご利用規約  | ご利用方法  | 会社情報  | 採用情報  | サイトマップ
Copyright© 2012, DreamArts Corporation. All Rights Reserved.
preload preload preload