SELECT文で本番環境を落としたお話

本番環境でやらかしちゃった人 Advent Calendarで、このパターンのやらかしはなかったのでキーボードを叩くことにしました。
番外編のつもりでお楽しみください。

この記事が、新たな障害発生を防ぐことにつながれば幸いです。

何をやったのか

ある日、ちょっとした調査のために本番データベースのデータを確認することになりました。
(個人情報が格納されているようなシステムではなかったので、必要であれば本番データベースへのアクセスが許されていました)

もしメンテナンスがあればそのタイミングでやればよかったのですが、直近では特に予定はないとのことでした。そのため、システムが動いている状態のまま作業をすることにしました。
ごく単純な SELECT を実行するだけのつもりだったので、システムに影響がないと判断したためです。

その際、万が一コピペをミスって更新系の SQL を実行してしまったら怖いので、念のためトランザクションをかけてからSQLを実行することにしました。
具体的には、psqlPostgreSQL のターミナル)で本番データベースに繋いで、以下の SQL を実行しました。

BEGIN;
SELECT * FROM user_setting WHERE xxx = 1;

結果はすぐに帰ってきました。確か2行程度だったと思います。

続けてさらに SQL を実行しようとしました。しかし、ここで同僚から「ソースコードでわからないところがあるんですが…」と声をかけられました。
こちらは急ぎの作業ではなかったので、ターミナルをそのままにして同僚の質問に回答することにしました。

そして約10分後…。

「システムがダウンしてるー!」

本番障害となりました。

何が悪かったのか

トランザクションをかけて SELECT 文を打ったお前が悪い」ということになりました。

何が起きていたのか

ログからシステムの動きを確認したところ、あるスレッドで user_setting テーブルをロックしようとしていたことが分かりました。具体的には、以下の SQL が発行されていました。

LOCK TABLE user_setting;

この SQL には、ロックモードの指定がありません。この場合、PostgreSQLACCESS EXCLUSIVE ロックが指定されたものとみなされます。
明示的ロック - PostgreSQL 9.4.5文書

この ACCESS EXCLUSIVE ロックは最も強いテーブルロックです。
SELECT 実行時に自動的に獲得される最も弱いテーブルロックである ACCESS SHARE ロックとも競合します。

つまり、システムが LOCK TABLE 文によって user_setting テーブルの ACCESS EXCLUSIVE ロックを獲得しようとしましたが、私が先に SELECT 文によって ACCESS SHARE ロックを獲得していたことで、ロック解除待ちに入って処理が止まってしまいました。

さらに、このあと別のスレッドが user_setting テーブルに対し SELECT を実行しようとしていました。しかし、user_setting テーブルは ACCESS EXCLUSIVE ロックの獲得待ちが発生しているので、この SELECT 文も止まってしまっていました。

結果、一つのスレッドが LOCK TABLE で、多数のスレッドが SELECT で止まってしまい、データベースとのコネクションプールが枯渇。システムダウンに至りました。

どうすればよかったのか

二度と惨劇を起こさないために、以下の知見を得ました。

  • SELECT しかしないとはいえ、油断しない
  • トランザクションを開始したなら、放置しない
    • なるべく早く COMMITROLLBACK をする

また、私がシステムを設計する際に以下の点に気を付けるようになりました。

  • ロック粒度に注意する
  • LOCK TABLE は極力使用しない
    • やむを得ず LOCK TABLE を使用するなら、可能な限り弱いロックモードを使用する
      • その際は、LOCK TABLE との競合について周知する1

MySQL では

MySQL だとテーブルロックのロックモードには READ と WRITE の2種類があります。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.3.5 LOCK TABLES および UNLOCK TABLES 構文

このうち WRITE ロックは SELECT と競合します。
また、MySQL のテーブルロックはセッション単位です。ドキュメントにあるように、ROLLBACK しただけではロックが解除されないので注意が必要です。

  • トランザクションを (たとえば、START TRANSACTION で) 開始すると、現在のトランザクションはすべて暗黙的にコミットされ、既存のテーブルロックが解放されます。
  • (中略)
  • ROLLBACK は、テーブルロックを解放しません。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.3.5.1 テーブルロックとトランザクションの通信

後日談

この件などでストレスが重なったことで胃痙攣をやらして、今度は私がダウンしました2。人間の体も本番障害が起きるんですね…。

みなさんも、障害発生後の体調管理には十分ご注意ください!


  1. 今回の場合、私が LOCK TABLE するコードを書いたわけではなかったので、このシステムでこのような競合が起きることを知りませんでした。

  2. 布団の上で数時間のたうち回りました。

Object#clone() メソッドからスローされる CloneNotSupportedException はどのようにハンドリングするべきか

JavaObject#Clone() メソッドは throws CloneNotSupportedException が宣言されています。

protected native Object clone() throws CloneNotSupportedException;

しかし、クラスが Cloneable インタフェースを実装していれば CloneNotSupportedException はスローされません。 それにもかかわらず CloneNotSupportedException はキャッチ例外のため1、スローするかキャッチする必要があります。

                            /* 👇実装している! */
public class Example implements Cloneable {
    @Override
    public Object clone() {
        // エラー: 例外CloneNotSupportedExceptionは報告されません。
        // スローするには、捕捉または宣言する必要があります
        return super.clone();
    }
}

正しく実装していれば何もしなくてもいいハズなのですが…。 このようなとき、どのようにハンドリングすればいいのでしょうか。

そこで、Java API ではこのよう場合にどのように実装されているかを確認してみました。 Search · CloneNotSupportedException path:/src/java.base/share/classes/java/

InternalError でラップする

圧倒的に多いのが、CloneNotSupportedExceptionInternalError でラップするという実装です。 Cloneable インタフェースを実装しているのに CloneNotSupportedException がスローされたということは、VM 内で何か問題があったということでこのようにしているようです。

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

AssertionError でラップする

ArrayDeque, EnumMap, EnumSetCloneNotSupportedExceptionAssertionError でラップしていました。 プログラムがバグっているということを示すために、AssertionError を使用しているようです。直接、この例外を投げるのは珍しい気がします。2

public ArrayDeque<E> clone() {
    try {
        @SuppressWarnings("unchecked")
        ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
        result.elements = Arrays.copyOf(elements, elements.length);
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

RuntimeException でラップする

発生しないはずだから、とりあえず非キャッチ例外にしてしまえ!ということのようです。 ただ、RuntimeException だと型でエラーの概要が分からない、catch(Excetption e) で捕まってしまうという問題があります。

public Object clone() {
    try {
        return super.clone();
    } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e.getMessage());
    }
}

握りつぶして、null を返す

java.util.Date では、キャッチして握りつぶしていました。 ただ、これは発生しないとはいえ null が返されてしまうルートができてしまうので、紛らわしいと思います。

public Object clone() {
    Date d = null;
    try {
        d = (Date)super.clone();
        if (cdate != null) {
            d.cdate = (BaseCalendar.Date) cdate.clone();
        }
    } catch (CloneNotSupportedException e) {} // Won't happen
    return d;
}

ちなみに

Cloneable インタフェースを実装しているクラスの clone メソッドで、throws CloneNotSupportedException を宣言しているのは見当たりませんでした。

まとめ

いかがでしたか。3
Cloneable インタフェースを実装していれば CloneNotSupportedException はスローされないので、どのようにハンドリングしても問題はないです。とはいえ、throws してしまうと呼び元に余計な手間をかけてしまいます。そのため、キャッチして何かしらのハンドリングをしておいた方がいいと思います。

そして、Java API の実装に合わせるのであれば InternalError にラップしてスローするのがいいようです。


  1. 設計ミスだと思います。

  2. 本来は、-enableassertions オプション付きで Java を実行しているときに、assert 文の条件が成立していないときにスローされるエラーです。

  3. これ書くとアフィリエイトブログっぽいですね。

Ansible で Mackerel を操作するプラグインを作りました!

Ansible で Mackerel の設定を取得したり変更したりできたらなーと思う時があります。 例えば…

  • アラートが鳴ってしまうのを防ぐために、Ansible 実行中はホストのステータスを standby にしたい
  • 特定のロールが設定されているホストの場合だけ、ファイルをコピーしたい

でも、Ansible の command モジュールで mackerel-agent を実行してこのような処理をさせるのは手間がかかります。 そこで、 Ansible の設計にのっとった形で簡単に Mackerel を操作できるように、プラグインを作ってみました! 1

https://github.com/YujiSoftware/ansible-mackerel-module

どういう風に使えるの?

例えば、Ansible 実行中はホストのステータスを standby にしたいとき。

- name: Set Mackerel Host Status
  mackerel_host:
    status: standby

# ~ホストの更新処理~

- name: Set Mackerel Host Status
  mackerel_host:
    status: working

ホストのロールを設定したいとき。

- name: Set Mackerel Host Role Fullnames
  mackerel_host:
    role_fullnames:
      - test_project:web

ロールによって動作を変えたいとき。

- name: Get Mackerel host configuration
  mackerel_host:
  register: mackerel

- name: Copy file if roles has "test_project"
  copy:
    src: ../files/example.conf
    dest: /etc/httpd/conf.d/
  when: mackerel['host']['roles']['test_project'] is defined

こんな感じに使えます! (ちなみに、API Key や Host ID はサーバ内にある mackerel-agent の設定ファイルをデフォルトで読み取るので、記述は不要です)

詳しい使い方は、GitHub の README をご覧ください!

まとめ

まだ ホストの情報取得/ステータス設定/ロール設定 しか対応していません。2 今後、JOJOに対応する操作を増やしていく予定です。

もし、こういうことができるようにしたい!という要望がありましたら、GitHub の Issues へ書き込んでいただければと思います。もしくは Up vote 👍 をお願いします。 https://github.com/YujiSoftware/ansible-mackerel-module/issues

ぜひご利用ください!


  1. すでに mackerelio/ansible-mackerel-agent というのがあるのですが、これは mackerel-agent をインストールするための Ansible テンプレートです。一方で、今回作ったプラグインは Mackerel API のラッパーです。

  2. Mackerel Advent Calendar が空いてる!チャンスだ!と思って急いで作りました。

JJUG CCC 2019 Fall ( #jjug_ccc ) - セッション資料の一覧

JJUG CCC 2019 Fall に行ってきました!


最近 Java のシステムをまたいじるようになって*1Java 欲が高まってきていたところにいろんなセッションを聞けてすごいテンション上がりました。
特に、「開け!ドメイン駆動設計の扉」がちょうどこれから一部のシステムをリプレースするにあたって導入したいけど「DDD、さっぱりわからん」と思っていたところだったのでとても助かりました。


また、マニアックな話も多数あってやっぱり JJUG CCC 最高!という気持ちです。
CLR の ValueType を起点に Project Valhala を覗いてみる」とか「オレ流OpenJDK「の」開発環境」とか、普段の JJUG では聞けない、でもとても知りたかった内容でした。こういうの、もっと聞きたい!


そして、懇親会で話しかけてくださった方、ありがとうございました!
すごく楽しかったです、そしてあっという間でした。もっとお話したいのでぜひ次の機会に!!


さて、最後にいつものを。

今回、残念ながら時間がかぶってしまって参加できなかったセッションがいっぱいあった*2ので、あとで読むために現時点で発表者の方が公開されている資料一覧をまとめしました。*3
(あとで JJUG CCC 2019 Fall のページにもリンクが載るかもしれませんが、とりあえず自分の方で調べました)

10:00-10:45

11:00-10:45

12:00-12:45 (ランチセッション)

13:30-14:15

14:30-15:15

15:45-16:30

*1:弊社、いろんな言語が混ざりすぎ!

*2:あと、今回はボランティアスタッフやっていたので、その間のセッションでも気になったのがありました。

*3:発表者のお名前は敬称略とさせていただきました。

Stack Overflow のタグから、どのフレームワークについてよく質問されているのかプログラミング言語ごとに分析してみた

Stack Overflow の質問には、その内容を示すタグが付与されています。

このタグ、よく見ると「言語」+ 「フレームワークやライブラリの名称」という構成が多いです。 つまり、これを分析すれば、言語ごとに質問の多い(つまり、よく使われている、人気がある)フレームワークやライブラリが何か分かるのではないでしょうか。

そこで今回は、kaggle で公開されていた Stack Overflow Data ^dataをもとに、質問に付与されたタグの出現回数を言語ごと×1年ごとに集計し、ランキングを算出しました。

より詳細なデータは こちら

解析結果

それぞれの言語ごとに、簡単な概要と解析結果(年度ごとのタグ出現回数ランキング)をまとめました。

Java

  • Spring Boot が登場1と同時に急上昇
  • ORM マッパは依然 Hibernate が上位
  • Swing の順位が低下すると入れ替えに、Java FX が上昇
タグ 2018 2017 2016 2015 2014 2013 2012 2011 2010 2009
android 1 1 1 1 1 1 1 1 1 20
spring 2 2 2 2 3 4 4 4 4 4
spring-boot 3 3 13 33 145 1567 10490 8932 6891 4822
hibernate 4 4 4 6 6 6 5 5 3 2
maven 5 5 7 7 7 12 15 22 87 498
arrays 6 6 5 5 5 5 11 12 24 26
json 7 8 8 8 12 11 18 28 59 174
javafx 8 13 14 19 29 72 160 595 422 295
eclipse 9 9 6 4 4 3 3 3 5 3
swing 10 7 3 3 2 2 2 2 2 1

Go

  • 基本的な言語仕様に関するタグが多い
  • Docker や MongoDB といったモダンな技術の組合わせが多い
  • ORM マッパーは GORM が人気
タグ 2018 2017 2016 2015 2014 2013 2012 2011 2010 2009
json 1 1 1 1 1 2 7 20 19 16
struct 2 2 3 3 3 9 4 45 9 92
http 3 3 2 4 4 3 7 2 44 92
goroutine 4 4 6 6 5 4 14 3 2 1
docker 5 6 16 33 48 173 567 283 130 92
mongodb 6 7 5 7 8 23 23 82 130 92
slice 7 5 7 5 19 7 18 10 19 92
concurrency 8 9 10 10 7 5 2 5 44 7
arrays 9 9 8 7 16 17 23 13 2 92
go-gorm 10 19 27 47 163 858 567 283 130 92

Python

  • 2016 年に Python3.x が Python-2.7 を上回った
    • Python 3.0 が2008年登場ということを考えるとかなり遅い
  • 機械学習や計算ライブラリの質問が多言語に比べ多い
  • Django (Webアプリケーションライブラリ) が一貫して上位にいる
タグ 2018 2017 2016 2015 2014 2013 2012 2011 2010 2009
python-3.x 1 1 3 4 5 4 6 7 18 21
pandas 2 2 2 3 4 6 20 1055 4807 3368
django 3 3 1 1 1 1 1 1 1 1
numpy 4 5 5 5 3 3 2 4 6 16
tensorflow 5 9 18 251 9023 5310 7081 6103 4807 3368
dataframe 6 6 9 23 55 93 251 2258 2827 3368
python-2.7 7 4 4 2 2 2 4 78 314 850
matplotlib 8 7 7 7 7 8 8 8 21 54
list 9 8 6 6 6 5 3 3 4 3
flask 10 14 11 10 11 13 12 74 259 3368

PHP

  • Laravel タグが急上昇
  • 全体的に Web 関連のタグがとても多い
  • Codeigniter が10年間安定した順位を保っている
タグ 2018 2017 2016 2015 2014 2013 2012 2011 2010 2009
laravel 1 2 4 6 9 19 195 3248 5129 3341
mysql 2 1 1 1 1 1 1 1 1 1
javascript 3 3 2 2 2 2 2 2 2 2
wordpress 4 6 6 5 7 8 9 11 10 19
html 5 4 3 3 3 3 4 4 3 3
jquery 6 5 5 4 4 4 3 3 4 6
ajax 7 8 8 8 8 6 6 6 8 8
arrays 8 7 7 7 6 5 5 5 5 7
codeigniter 9 9 9 9 10 9 7 7 9 16
sql 10 13 12 12 5 7 8 9 11 9

Ruby

タグ 2018 2017 2016 2015 2014 2013 2012 2011 2010 2009
ruby-on-rails 1 1 1 1 1 1 1 1 1 1
ruby-on-rails-5 2 3 13 635 3910 3822 3612 3206 2469 1702
rubygems 3 6 5 15 16 11 8 3 2 3
ruby-on-rails-4 4 2 2 2 2 3 1218 1307 1284 908
activerecord 5 5 4 5 6 5 3 4 3 2
rspec 6 7 3 6 4 4 5 7 9 15
arrays 7 4 6 4 5 7 10 9 7 5
javascript 8 9 8 8 9 16 16 14 15 25
postgresql 9 11 12 13 15 24 34 63 88 132
ruby-on-rails-3 10 8 7 3 3 2 2 2 4 588

JavaScript

  • ライブラリは jQuery, React.js, Angurlar の順にランクイン
  • Node.js は毎年順位が上がってきている
  • Ajax が下がってきている(当たり前に使えるようになった?)
タグ 2018 2017 2016 2015 2014 2013 2012 2011 2010 2009
jquery 1 1 1 1 1 1 1 1 1 1
html 2 2 2 2 2 2 2 2 2 2
reactjs 3 6 9 15 111 942 7551 6176 4367 2899
node.js 4 4 5 6 7 9 9 15 48 491
css 5 3 4 4 3 3 4 4 5 5
angular 6 11 13 239 6645 8935 7551 6176 4367 2899
php 7 7 6 5 5 4 3 3 3 6
arrays 8 9 10 10 10 12 17 17 19 21
ajax 9 8 7 7 6 5 5 5 4 3
typescript 10 12 17 71 134 158 256 6176 4367 2899

SQL

  • SQL Server が予想外の1位
    • T-SQL 3も7位につけているところを見ると、意外と使われているっぽい
  • Oracle も安定して上位
  • 言語仕様に関するものでは Join がランクイン
タグ 2018 2017 2016 2015 2014 2013 2012 2011 2010 2009
sql-server 1 1 1 1 2 2 2 2 1 1
mysql 2 2 2 2 1 1 1 1 2 2
oracle 3 3 3 3 4 4 5 7 6 5
postgresql 4 4 6 6 8 9 9 11 10 17
php 5 5 4 4 3 3 4 4 9 8
database 6 6 5 5 6 6 6 6 5 6
tsql 7 7 7 8 9 8 7 5 3 3
c# 8 8 8 7 5 7 8 8 7 7
java 9 11 10 10 10 10 11 13 17 20
join 10 10 11 11 11 11 10 12 12 13

おわりに

当然の結果にだったものもあれば意外な結果だったものも多数あるのではないでしょうか。 元データの方では、上位30位まで見れますので興味があれば合わせてご覧ください。 Tag trends by language for Stack Overflow | Kaggle


  1. 2014年4月1日にバージョン 1.0 が GA

  2. バージョンアップしないの…?

  3. SQL Server で使用する、SQLを手続型に拡張した言語

Java のバージョンを上げるだけで、プログラムは速くなるのか

よく Java の実行バージョンを上げるだけで速くなるという話を聞きます。 でも、本当にそうなのでしょうか。また、本当だとしたらどれぐらい速くなるのでしょうか。

そこで、簡単なプログラムで実験してみました。

実験概要

実験用に、数独を解く Java のプログラムを作成しました。 このプログラムは単純な演算を繰り返し行ってるだけなので、Webアプリケーションのような複雑なプログラムとはおそらく傾向が違いますが、参考程度にはなるかなと思います。

これをJava 1.1 でコンパイルし、Java 1.1 ~ 12 の各 Oracle JDK (32bit/64bit) で数独100万問のデータセットを読み込んで解き終わるまでの時間を測定しました。1

細かい測定条件は以下の通り。

  • 実行環境
    • Windwos 10 Home 1809 (64bit)
    • Intel Core i7-7500 CPU @ 2.70GHz
  • Java の実行オプション
    • -mx512m -ms512mを付与2
    • Java 1.3.1 以降は、-server を付与3
  • 測定方法
    • 各バージョンごとに5回測定し、中央値を取得

実験結果

処理は二つのパートに分かれているので、その処理ごとグラフ軸を分けました。

  • 数独100万問のデータセットを読み込む処理 (下図の青い部分)
    • この部分は、主に GC の性能に左右される
  • 数独を解く処理 (下図の赤い部分)
    • この部分は、主に JIT 最適化の性能に左右される

その実験結果がこちら。 処理時間(ms)

グラフの元データ...

|Java|Version|データ読み込み(ms)|数独解析(ms)|合計(ms)| |--:|--:|--:|--:|--:| |Java 1.1 (32bit)|1.1.8|8,313|4,231|12,544| |Java 1.2 (32bit)|1.2.2_017|6,647|4,360|11,006| |Java 1.3 (32bit)|1.3.0_05|10,747|6,905|17,652| |Java 1.3.1 (32bit)|1.3.1_28|7,647|5,093|12,740| |Java 1.4 (32bit)|1.4.0_04|8,061|4,741|12,802| |Java 1.4.1 (32bit)|1.4.1_07|7,810|4,796|12,606| |Java 1.4.2 (32bit)|1.4.2_19|3,437|3,281|6,717| |Java 5 (32bit)|1.5.0_22|3,050|3,289|6,338| |Java 6 (32bit)|1.6.0_45|3,530|4,077|7,607| |Java 7 (32bit)|1.7.0_80|3,411|3,211|6,622| |Java 8 (32bit)|1.8.0_202|3,239|3,257|6,496| |Java 6 (64bit)|1.6.0_45|5,029|3,312|8,341| |Java 7 (64bit)|1.7.0_80|7,019|3,148|10,167| |Java 8 (64bit)|1.8.0_202|5,050|3,054|8,104| |Java 9 (64bit)|9.0.4|2,101|2,961|5,061| |Java 10 (64bit)|10.0.2|2,056|3,054|5,110| |Java 11 (64bit)|11.0.3|1,922|3,343|5,265| |Java 12 (64bit)|12.0.1|1,934|3,422|5,356|

考察

「今回のプログラムでは」という前提が付きますが、データから以下のことが見て取れます。

  • Java 1.1 に比べて、最近の Java は2倍以上速くなっている
  • Java 1.3 で急激に遅くなっている。まだ、HotSpot が成熟していなかった?
  • Java 1.4.1 までは遅かったが、Java 1.4.2 でぐっと速くなっている
  • Java 6 ~ 8は、64bit VM より 32bit の方がデータ読み込みが速い
  • Java 9 でぐっと速くなっている

まとめ

Java のバージョンを上げるだけで、プログラムは速くなる。


  1. 後方互換性があるので、Java 1.1 でコンパイルしたクラスファイルは、以降のバージョンでもそのまま動きます。

  2. 「あれ -Xmx -Xms じゃないの?」って思うかもしれませんが、Java 1.1 ではこのオプションは -mx -ms になっています。Java 1.2 で VM オプションの -Xmx -Xms に変更になったのですが、いまだに -mx -ms も使えます。

  3. このオプションがないと HotSpot Client VM が使われてしまい、倍ぐらい遅くなります…。

  4. Concurrent Mark Sweep Garbage Collector。このGCについての詳細は、KUBOTA Yuji さんの資料を参照。

  5. https://twitter.com/skrb/status/1161642952420016130

strptime は環境によって挙動が違う

PHP には、日付文字列をパースするための strptime という関数があります。 これを使って、Sun, 19 Apr 2015 11:43:30 GMT という文字列を %a, %d %b %Y %H:%M:%S %Z というフォーマットでパースした結果、以下の通り環境によって異なる結果になりました。 strptime は環境によって挙動が違うんですね。

OS tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Linux 115 3 19 11 43 30 "GMT"
Mac 115 3 19 20 43 30 ""

Linux の場合は GMT で返ってきましたが、Mac の場合は JST で返ってきました。 (どちらも、OS のタイムゾーン設定は JST です)

どうしてこのような違いが起きたのか、ドキュメントを確認してみました。

PHP のドキュメント

ちゃんとOSにより挙動が異なることが明記されています。

注意: 内部では、 この関数はシステムの C ライブラリ関数 strptime() をコールしています。 このライブラリ関数は、OS によって挙動が異なることがあります。 PHP: strptime - Manual

ただ、どのように挙動が異なるかは記載されていません。そのため、次に各OSのドキュメントを確認してみました。

Linux の場合

フォーマットの指定として %Z (タイムゾーン) を指定することはできるが、無視すると書かれています。

原文: For reasons of symmetry, glibc tries to support for strptime() the same format characters as for strftime(3). (In most cases the corresponding fields are parsed, but no field in tm is changed.) This leads to %Z The timezone name.

日本語: 対象性のために、glibc では strptime() に strftime(3) と同じフォーマット文字をサポートさせようとしている。多くの場合、対応するフィールドが解釈されるが、tm フィールドは変更されない。使用可能なフォーマット文字を以下に示す。 %Z タイムゾーン

そのため、文字列中のタイムゾーンの指定が GMT だろうが JST だろうが、以下のように同じ結果が返ってきます。「11時って書いてあるんだから11時だろ」という解釈です。

入力 tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Sun, 19 Apr 2015 11:43:30 GMT 115 3 19 11 43 30 "GMT"
Sun, 19 Apr 2015 11:43:30 JST 115 3 19 11 43 30 "JST"

Mac (BSDUNIX) の場合

現地のタイムゾーンに変換してくれるそうです。

原文: The strptime() function parses the string in the buffer buf, according to the string pointed to by format, and fills in the elements of the struc-ture structure ture pointed to by tm. The resulting values will be relative to the local time zone.

日本語: strptime() 関数は、formatが指す文字列に従ってバッファbuf内の文字列を解析し、timeptr が指す構造体の要素にあてはめます。 結果の値は現地のタイムゾーンを基準にしたものになります。

Mac OS X Manual Page For strptime(3)

ただし、文字列中のタイムゾーンとして指定できるのは GMT かローカルタイムゾーン (今回の場合は JST) のみだそうです。

原文: The %Z format specifier only accepts time zone abbreviations of the local time zone, or the value "GMT". This limitation is because of ambiguity due to of the over loading of time zone abbreviations. One such example is EST which is both Eastern Standard Time and Eastern Australia Summer Time.

日本語: %Z フォーマット指定子は、ローカルタイムゾーンタイムゾーンの省略形、または値 "GMT"のみを受け入れます。 この制限は、タイムゾーンの略語のオーバーロードによるあいまいさのためです。 そのような例の1つは、東部標準時と東オーストラリアの夏時間の両方であるESTです。

やってみると、こうなりました。「GMT の11時って書いてあるんだから、現地時間の20時だろ」という解釈です。

入力 tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Sun, 19 Apr 2015 11:43:30 GMT 115 3 19 20 43 30 ""
Sun, 19 Apr 2015 11:43:30 JST 115 3 19 20 43 30 ""

OSによらず、同じ結果になるようにしたい場合は?

マニュアルに書いてありました。date_parse_from_format 関数を使いましょう。

date_parse_from_format() はこの問題の影響を受けないので、PHP 5.3.0 以降ではこちらの関数を使うことを推奨します。 PHP: strptime - Manual

まとめ

PHP は Write once, Run anywhere じゃなかった。