Browse Category: on

ある PHP 案件の振り返り

2015 年、僕がメインで担当したとある PHP 案件の振り返りを行ってみようと思います。おもに技術面から、設計前に想定したこと、実際に導入してうまくいったこと・いかなかったことを振り返ってみたいと思います。

技術的な環境は、次のとおりです。

  • インフラ環境: オンプレミス(だいだい6台くらい、このときのためにほぼハードウェアを新規に調達しました)
  • OS: CentOS 7.0
  • 言語: PHP 5.6
  • フレームワーク: FluelPHP 1.7.x
  • データベース: Postgresql 9.3
  • ミドルウェア
    • ロードバランサー冗長化: Keepalived(新規)
    • ウェブサーバ: Apache から Nginx に変更、PHP は FPM
    • キャッシュ: Redis(新規)
    • 検索: Elasticsearch(新規)
    • ログ: Fluentd(新規)
    • 監視: Nagios & Cacti & Munin(従来と同じですが、新規にセットアップしています)

案件内容的には、とある既存の Java だったサイトを PHP にてフルリニューアルするというものです。

初回の設計時の構成は、次のとおりです。僕の担当は、全体のアーキテクチャ設計と実装とインフラ構築と切り替え時の運用、そしてすべての引き継ぎです。

  • 納期がかなりタイトだったため、データベースは既存の構成のままとしました(テーブル構成・SQL チューニングなどは、フルリニューアル後にお任せすることにしました)
  • サイトのデザインは、そのままの形で移行しますが、あまりにも不要な機能名は移行しない(デザインは、PC、スマフォ、フィーチャーフォン版の3種類ありました)
  • リニューアル後、すぐに引き継ぐため、なるべく新しいミドルウェアは導入しない(あまり技術的な進化はしないですが、業務なので仕方がない面が多いのも事実です)

サーバ構成を図にすると、次のような感じになります。

構成図

まず、インフラ側から、各コンポーネントについて説明します。

OS

  • オンプレのため、OS をインストールが必要ですが、オンプレのホスティング先に設定内容をまとめて依頼することにしました(ネットワーク構成や IPMI になりますが、コストは多少かかりますが個人的にはかなり信頼できるホストティング先でこの面はとても安心できました)
  • ミドルウェア以上の導入は、すべてこちらの管轄として、責任分解点を明確にしました

ロードバランサー

  • オンプレなので、いつもなら Linux のカーネルの変更(LVS ハッシュサイズ変更、TCP WAIT_TIME 変更)+ LVS (DSR) + Keepalived の構成をとりかったですが、引き継ぎ先のエンジニアのスキルセットとそれほどの規模ではなかったので、カーネルの変更は行いませんでした
  • Keepalived + Nginx の Active / Standby 構成としました
  • Keepalived は、VIP のみをもつ設定として、ローカルの Nginx へリクエストを行い、 Nginx をロードバランサーとして各ウェブサーバへプロキシする構成としました
  • L7 Nginx にしましたが、ページの静的ファイルなども、すべてウェブサーバへリクエストを振り分けました(※ロードバランサーにアプリケーションプログラムをデプロイするか、Nginx の静的ファイルキャッシュを使う選択肢も当時は考えましたが、静的ファイルのリクエストはそれほど多くなかったのでキャッシュは不要と判断しました)
  • なので、当然 DSR ではなく、NAT になります

ウェブ

  • ウェブサイトのページを表示するサーバです
  • 一般的な Nginx + PHP FPM の構成としました
  • ページを表示するために必要なデータは、すべてウェブサーバごとに Redis へキャッシュする方針としました(かなり当時の SQL が重かったので、別に PHP でクローラーとして実装してキャッシュしました)
  • ウェブサーバの Redis は、各ウェブサーバごとにキャッシュとしてデータをもっているため、レプリケーションは行っていません)
  • 今後の緊急サーバ増強にあわせて、AWS EC2 のハイブリッド構成も視野に入れていたので、awscli は最初から導入しておきました
  • PHP は、remi にある PHP 5.6 をそのまま導入しました
  • この他に、php-mecabphp_qr を PHP 5.6 で動くようにパッチをあてて使うことにしました

検索

  • 今まで SQL like 的な感じだったので、思い切って Elasticsearch を導入しました
  • 定期的にデータベースから Elasticsearch へデータを入れるプログラムを cron で動作させるようにしました

キャッシュ

  • ウェブサーバごとの Redis とは別に共通に必要なデータを Redis を、別のキャッシュサーバとして設けました
  • キャッシュ1の Redis をマスターとして、各ウェブサーバに別の Redis でスレーブとしました(ページを表示するために共通のキャッシュを取得するためにウェブサーバのローカルのスレーブ Redis から取得する構成としました)

データベース

  • 一般的な Postgresql 9.3 のマスター x スレーブ x 1 構成になります
  • リプレイス後、スレーブを 1 台追加、Keepalived を使って HA 構成としました

監視まわり

  • Sensu & Grafana & Kibana の構成で考えましたが、引き継ぎ先のエンジニアのスキルセットを考慮して見送りました
  • Munin は、別の非エンジニアチームの方が使っていたため、そのまま引き継いで導入しました
  • Mackrel を無料の範囲で導入しました
  • Cacti は、引き継ぎ先のエンジニアのスキルセットを考えて導入しましたが、個人的にはかなり久しぶりだったのでかなり設定に手間取りました

その他のツール

  • 構成管理ツールは、Ansible にしました(本当は Itamae にしたかったですが、Ruby だったので厳しかったです・・・)、Nagios の設定ファイルも自動生成したりしていました
  • Serverspec も導入したかったのですが、Ruby になってしまうため、こちらも引き継ぎ先のエンジニアのスキルセットを考慮して見送りました
  • 案件管理は Backlog、コード管理も Backlog の git、チャットは Slack(※もちろん有料!)、共通系は Qiita Team、を導入していただきました
  • コードのデプロイは、PHP 縛りということで Capistrano ではなく、同等の Rocketeer を使いました、このツールいろいろと罠があったので、別の機会にブログにまとめておきたいと思います

と、こんな感じです。

インフラのオンプレサーバは、サーバ選定からすべて僕一人で行いました。(※ラッキングと OS のインストールは、前述のとおりすべてホスティング先にお任せしました。)

サーバの選定にあたっては、台数が増えると運用コストが増えるため、できるかぎり予算の範囲でスペックの高いサーバとしていますが、次のようなスペックにしました。NIC は、安定の Intel 製にしました。

  • ロードバランサー: CPU 8 コア、メモリ 16GB、ディスク SAS 300GB RAID 1
  • ウェブ: CPU 32 コア、メモリ 64GB、ディスク SAS 300GB x 4、RAID10
  • キャッシュ: CPU 32 コア、メモリ 64GB、ディスク SSD 120GB x 4、RAID10
  • データベース: ioDrive サーバを導入できる見込みもありましたが、コストの問題、そこまでデータベースネックではない(SQL をちゃんとチューニングすれば問題ないはず)様子だったので、既存のサーバをそのまま転用しました、ただしディスク 4 台 SAS 300GB x 2 RAID1 + Spare 構成だったので RAID10 構成に統一しました

次に PHP の部分です、PHP の実装は、引き継ぎ先のエンジニアにも最初から担当していただきました。

  • フレームワークは、CodeIgniter か Laravel かどうか迷いましたが、けっこうシンプルな FuelPHP にしました(どうも現在 FuelPHP は、作者の人が病気?のため、かなり開発が停滞してしまっているようなので、もしかしたら選定に失敗したかもしれません)
  • FuelPHP は、基本的に MySQL での用途となっていたので、Model 部分を頑張って Postgresql に対応しました(今回の案件は画像のデータはそのまま Postgresql に格納されていて、その部分の Model 対応が一番大変でした)
  • テストも同梱されていることが魅力的で、僕の実装ではほぼ PHPunit によるユニットテストを書きましたが、他のエンジニアの方までテストを書く体制がとれなかったです
  • なので、非エンジニアが行うブラックボックステスト QA にみの品質管理体制となっていました(エンジニア側での動作確認は、すべてのブラウザを F5 リロードになっていました)
  • Redis へキャッシュするプログラムは、バッチ処理のように記述してデータベースの変更にあわせてほぼリアルタイムに更新できるようしました(そのため、FuelPHP の tasks + Supervisord の組み合わせにしました)

本番リリース前には、wrk を使って、それぞれの UserAgent で簡易ベンチマークを行いました。そのといは結果的にギリギリのパフォーマンスでした。

リリース後、いくつか問題が発生しました。。。

まずロードバランサー側で iptables の ip_conntack 設定ミス問題が発生しました。これ完全に僕の設定ミスだったので、すぐに設定を変更しました。Nagios での監視も漏れてもれていたことが原因でした。やはり、Serverspec を導入しておけよかったと思います。

Ansible 的には、次の設定を追加しました。

- sysctl: name="{{ item.name }}" value="{{ item.value }}" sysctl_file=/etc/sysctl.d/10-lb.conf
  with_items:
  - { name: net.nf_conntrack_max, value: 2097152 }

次に、それほどアクセスが多くないページの部分的な、キャッシュを使っていない部分でデータベースのボトルネックが発生しました。すぐにすべてキャッシュに入れる構成に変更して再リリースしました。納期的な問題から、あとで対応しようと考えていたのですが、やはりかなり甘い考えだったようです。

さらに PHP でキャッシュデータを生成していたクローラーデーモンがメモリリークして、OOM Killer を抑制していたため、サーバに SSH できない問題が発生しました・・・。調べてみると、どうも FuelPHP の Model まわりで発生しているようで、かなり根本解決が難しかったため、一定時間おきに PHP クローラーデーモンの内部で実際にキャッシングしている処理を別の PHP で実行する方式に変更しました。
クローラーデーモンは、Supervisor 経由で FuelPHP の task を、次のような設定に変更しました。次の設定では、FuelPHP tasks の crawler::run_fork を実行して、foo_cache という crawler に定義されている関数を別 PHP で 10 秒おきに実行するという意味になります。


[program:crawler]
command= /opt/remi/php56/root/usr/bin/php oil refine crawler:run_fork foo_cache --wait=10

そして、キャッシュの方のパフォーマンス問題がピーク時間帯やピークを少し越えたときに発生してしまいました。原因とすると、キャッシュ構造性の設計が起因していました。具体的には、ページのリクエストおきに ZSET なキーを取得して、そのキーをもとにすべての HASH を取得してキャッシュデータとデータベースから取得したデータを作成していましたが、この構成だとパフォーマンス的な限界があったようです。
この修正はかなり時間がかなりそうだったので、取り急ぎ PHP から Redis へアクセスする部分を Redis_db から phpredis に変更しましたが、大きくパフォーマンスは改善しませんでした。

この改善は、キャッシュデータを構造を大幅に見直して、PHP Serialize したデータのみにアクセスする構成にしましたが、結局時間がかかってしまい、引き継ぎもあってこの変更は受け入れられませんでした。。。

結局時間がかかったこともあって、ロードバランサー側にすべての動的なページを 10 分間キャッシュするように設定変更することになかったようです。パフォーマンス問題があったとはいえ、個人的にはちょっとありえない施策だったと思います。

こうしていくつかの問題があったのですが、引き継ぎもすべて完了して、この案件は僕としては終了しました。個人的な反省は、次のとおりです。

  • PHP を常時起動しているクローラーとして実装しない方がよい: やはり Apache の MaxRequestsPerChild や PHP FPM の pm.max_requests にみるように PHP は一定量処理したあとは再起動するのが鉄板だったようです、Supervisor を使っていたので、わざとデーモンプログラムを終了するようにすれば一定時間おきに起動することができました
  • キャッシュ構造の設計ミス: 今回二段階のキャッシュの構造でパフォーマンス問題がありました、二段階にした理由はキャッシュデータ量を減らすためそうしたのですが、キャッシュする量が対象増えても一段階に最初からするべきでした

あわせていくつか課題が残りました。

  • PHP ユニットテスト: 僕が実装した範囲はほぼテストコードを書いていましたが、その他のところはまったくテストコードがない状態となってしまいました、今回の案件の場合すべてのテストを書く必要はないと思いますが、最低限 Model と Controller の部分はテストコードを書いておけば QA するときの工数がかなり下がるため、総合的にみると工数は減るはずです、ヒアリングしてみるとテストコードを書いた経験がないため難しいと言っておられましたが、誰も最初は初めてなのでテストコードを書いてほしかったと思います
  • インフラの運用: オンプレなので、日々の運用で PHP や Nginx やセキュリティパッチをあてていく運用が必要となってきます、必要なものはすべて Qita Team にまとめておきましたが、PHP のバージョンが 3 つくらい古い様子なので、ちゃんと引き継ぎできているのか心配です

ということで、貴重な体験を振り返ってみました。
2016 年も始まっているので、引き続きウェブサービスを開発していきたいと思います。

Fluentd s3 + ローカルファイルに出力する設定の紹介

このエントリは、Fluentd Advent Calendar 2015 – Qiita の 4 日目のエントリです。当日、勝手にエントリしています。

今日は、最近実施した非常によくある構成 Fluend 経由で s3 + ローカルファイルに出力する設定を本番環境(※ただし、正式本番稼働前)で設定したので、どこにもある情報ですが、一緒に設定された設定ファイルの例がなかなかなかったので紹介してみます。

OS は、CentOS 7 になります。td-agent は、公式の方でインストール します。

やりたいことしては、S3 のバケット上に、「YYYY/MM/DD/HH/タグ1_タグ2」ファイルとして出力して、ローカルファイルにも同じディレクトリ構成のファイルで、同時出力することになります。

まず、設定ファイルですが、次のようになりました。

<source>
  type forward
  bind 127.0.0.1
  port 24224
</source>

<match hoge.**>
  type copy

  <store>
    type forest
    subtype s3
    <template>
      id hoge-s3-${tag_parts[1]}
      path ${tag_parts[1]}/

      aws_key_id <AWS_ACCESS_KEY_ID>
      aws_sec_key <AWS_SECRET_ACCESS_KEY>
      s3_bucket hoge-bucket
      s3_region <AWS_DEFAULT_REGION>
      format json
      s3_object_key_format %{path}%{time_slice}_%{index}.log
      store_as json

      time_slice_format %Y/%m/%d/%H/${tag_parts[1]}_%Y%m%d%H%M_${tag_parts[2]}
      flush_interval 60s
      flush_at_shutdown true

      buffer_type file
      buffer_path /var/log/td-agent/.s3-${tag_parts[1]}_%Y%m%d%H%M_${tag_parts[2]}.log
      buffer_chunk_limit 64m
      buffer_queue_limit 32

      num_threads 1
    </template>
  </store>

  <store>
    type forest
    subtype file_alternative
    <template>
      id hoge-file-${tag_parts[1]}
      path /var/log/td-agent/${tag_parts[1]}/%Y/%m/%d/%H/${tag_parts[1]}_%Y%m%d%H%M_${tag_parts[2]}.log
      symlink_path /var/log/td-agent.log
      dir_mode 0750

      time_slice_format %Y%m%d%H%M
      flush_interval 0s
      flush_at_shutdown true

      buffer_type file
      buffer_path /var/log/td-agent/.${tag_parts[1]}_%Y%m%d%H%M_${tag_parts[2]}.log
      buffer_chunk_limit 64m
      buffer_queue_limit 32

      num_threads 1
    </template>
  </store>
</match>

プラグインは、設定ファイルから分かるとおり、fluentd-plugin-forestfluent-plugin-file-alternative、が必要なので、td-agent-gem 経由でインストールしておきます。


$ sudo /opt/td-agent/usr/sbin/td-agent-gem install --no-ri --no-rdoc fluentd-plugin-forest fluent-plugin-file-alternative

ここでちょっとハマったのは、ローカルファイルへ出力する file_alternative の実際の出力ファイルが「/var/log/td-agent/.YYYY/MM/DD/HH/ファイル名」となってしまって、/var/log/td-agent 以下とみるとドットディレクトリとしてしまったところでした。上の設定で、正しくファイルが出力されています。

これから、この環境で本番稼働前のベンチマークを実施して、buffer まわりのパラメータ、thread の調整などを行ってみるつもりです。

今回、監視まわりは Datadog を使っているので、別のエントリーで紹介したいと思います。

Windows Explorer の関連づけがおかしくなった

Windows Explorer on Windows XP Professional で、テキストファイルを xyzzy に関連づけに設定していたけれど、xyzzy のプログラムがおいてあるところ変えたところ、再度 Windows Explorer で関連づけの設定が正しくできなかった。

たぶん、以前設定したのが原因だと思ったので、レジストリを探してみたら、次の場所に設定がありました。

HKEY_CLASSES_ROOT\Applications Reigstory

上のところに、プログラム名のキーがあって、その中に Windows Explorer から開くときの設定があるので、その設定を変更すれば無事開けるようになりました。

これはかなり分かりづらいなと思いました。

emacs の日本語文字コード認識方法

今まで、.emacs に次の設定をしていたのですが、UTF-8 なファイルと EUC-JP なファイルが混在して使うとき、文字コードが固定になっていました。

(set-language-environment "Japanese")

(set-default-coding-systems 'utf-8)

(set-buffer-file-coding-system 'utf-8)

(set-terminal-coding-system 'utf-8)

(set-keyboard-coding-system 'sjis)

これで相当不便だったので、設定をコメントにしてみたところ、文字コードを自動認識してくれるようになりました。日本語入力も問題ないみたいなので、このまま試してみたいと思います。

今のところ、次の設定だけ有効にしてあります。

(set-language-environment "Japanese")

人に好かれるために

プライベートでも、ビジネスでも、人とのつきあい方はとても大事だと思います。

最近、すこし人とのつきあい方がおかしいと思っていた矢先、ふと立ち寄った本屋さんで、次の本を見つけて買ってしまいました。

この本はとても分かりやすく書かれていて、とても参考になりました。

人とのつきあい方で悩んでいる人は、ぜひ読んでみてください。

「なぜか人に好かれる人」の共通点 PHP文庫 (PHP文庫)
斎藤 茂太
PHP研究所 (2004/04/01)
売り上げランキング: 68415
おすすめ度の平均: 5.0

5 人生で役立つ本です

nkf をまとめて変換するスクリプト

を作ってみた。名付けて、nkf_all.sh 。

#!/bin/sh

DIR=$1

for i in `ls -rt $1/*.html`; do

echo $i

nkf -e --overwrite $i

done

使い方は、nkf_all.sh `pwd` とすればカレントディレクトリにあるすべての .html ファイルを EUC-JP に変換してくれる。

FastCGI on MT 3.35 part2

FastCGI on MT 3.35で、MT を FastCGI 上で動かしていたのですが、まれに次のエラーがでてしまった MT が落ちる現象が発生しました。

[alert] (11)Resource temporarily unavailable: setuid: unable to change to uid: 48

uid 48 というのは、apache ユーザにあるのですが、どうも原因が分からないので、mod_fcgi から mod_fcgid に切り替えてみました。

mod_fcgid は、パッケージとして提供されているので yum install mod_fcgid で済みます。

前の mod_fcgi の設定の代わりに、次の設定をしました。

<IfModule mod_fcgid.c>

SocketPath /tmp/fcgid_sock/

AddHandler fcgid-script .fcgi

</IfModule>

この状態で一晩たったのですが、いくつか notice が出ているのですが、上のエラーが出ていないので問題みたいです。

FastCGI on MT 3.35

会社のブログが設置してあったサーバのハードディスクがクラッシュして、MT を再インストールすることになりました。

いい機会なので、前からずっと試してみたかった MT を FastCGI で動かしてみました。

MT 3.34 から、公式に FastCGI がサポートされたようで、 Running Movable Type Under FastCGIに公式情報がのっています。

サーバの環境は、次の通りです。

・OS: Fedora Core 6

・HTTP: Apache HTTP Server 2.2.4

・MT: 3.35 日本語版

Apache は普通に yum でインストールすることができます。FastCGI と mod_fcgi は yum にないので、手動でインストールします。あわせて、httpd-devel パッケージもコンパイル時に必要なのでインストールします。

FastCGI は、公式サイトからダウンロードして、インストールします。

# tar zxf fcgi-2.4.0.tar.gz

# cd fcgi-2.4.0

# ./configure --prefix=/opt/fcgi

# make && make install

次に mod_fcgi ですが、こちらも公式サイトからダウンロードしてインストールします。

# tar zxf mod_fastcgi-2.4.2.tar.gz

# cd mod_fastcgi-2.4.2

# cp Makefile.AP2 Makefile

# make

ただし、apache 2.2.4 だとうまくコンパイルが、次のようなメッセージがでてコンパイルが通りません。

Makefile:12: /home/httpd/apache2/build/special.mk: No such file or directory

make: *** No rule to make target `/home/httpd/apache2/build/special.mk’. Stop.

これを直すには、mod_fastcgi にパッチを当てる必要があります。このパッチを、fastcgi-pathとして保存してパッチを当てます。

# patch -p1 < fastcgi-path # make

mod_fastcgi は、make すると自動的に /usr/lib/httpd/modules/fcgi_buf.so にインストールされて上で、httpd.conf に自動的に LoadModule 行を足してくれます。

あとは、MT 側の設定ですが、まず mt-config.cgi に次の設定を追加します。

AdminScript mt.fcgi

CommentScript mt-comments.fcgi

TrackbackScript mt-tb.fcgi

SearchScript mt-search.fcgi

# XMLRPCScript mt-xmlrpc.pl

ViewScript mt-view.fcgi

そして、対応するファイルを fcgi という拡張子に変更してコピーします。

最後に apache の設定に次の内容を書いておきます。

<IfModule mod_fastcgi.c>

FastCgiIpcDir /tmp/fcgi_ipc/

AddHandler fastcgi-script .fcgi

FastCGIConfig -autoUpdate -idle-timeout 120 -killInterval 3600 -maxClassProc

esses 6 -maxProcesses 15

</IfModule>

最後に apache を再起動して、mt.fcgi にアクセスすれば MT を FastCGI 上で使うことができます。

結果としては、体感できるくらい快適に使えるようになったと思います。

Twitter API で、Favorites 関連が強化された

Twitter APIで、Favorites 関連が強化されて、API 経由で Favorite の追加や削除できるようになったようなので、さっそく試してみました。

Favorite の追加は、次の URL でアクセスできるようになっています。

http://twitter.com/friendships/create/id.format

ためしにブラウザで、僕のステータスを Favorite に追加してしようと、次の URL をたたいてみた。

http://twitter.com/favorites/create/241990292.xml

そうすると、次のような画面が。。。

Twitter Page Not Found

もしかして、まだ正式にリリースされていないのだろうか?

linux のコンソール画面でパワーセーブを無効にする方法

忘れてしまうので、メモしておく。

linux のコンソール画面でパワーセーブを無効にするには、次のコマンドを実行する。

# setterm -blank -powersave off

注意点としては、SSH などで遠隔からログインしている場合は、上のコマンドは実行できないので直のコンソール画面から実行する。

以下の情報が参考になった。

http://search.luky.org/linux-users.a/msg04811.html