カテゴリー別アーカイブ: プログラミング

Railsのバッチ処理を高速にする方法

古い化石のような技術にも、いいところはあるのだよ。

a1180_006694

Railsで開発していてバッチ処理を書く時に、
毎回runner走らせるのが重くてリソースの無駄遣い。
どうしても好きになれない。

そこで色々考えてみたのだけど、
そもそもバッチで書く処理って単純なものがほとんどだから、
active_recordをわざわざ使う必要ないんじゃない?と。

例えばデータベースがPostgreSQLの場合、


#!/usr/local/bin/ruby

require 'yaml'
require 'pg'

ds = YAML.load(File.read("config/database.yml"))

db = ds[ENV["RAILS_ENV"]]

con = PGconn.connect(db["host"], 5432, nil, nil, db["database"], db["username"], db["password"])

res = con.exec("select * from hoge")

if res.ntuples > 0
    .
    .
    .
end

res.clear()

con.close()

のようなコードで、

cd RAILS_ROOT && RAILS_ENV=[production,development] batch/hoge.rb

とすると高速に動かせる。
database.ymlから接続情報だけ取得して、
後はpgライブラリを使ってそのまま書けばいい。

MySQLなど他のDBでも同じようなことできるはずだよ。

DNSの仕組みをちゃんと理解している?

社内でDNSサーバーの設定の話をしていて、
変更をした際の動作検証の話になった。

慣れてきたら、ログとそのサーバーの問い合わせを確認するだけでいいけど、
慣れないうちは外部サーバーを含めてきちんと確認してね。

という話をしていたのだけど、
設定ファイルの変更方法は理解しているみたいだけど、
そもそもDNSの仕組みをきちんと理解していないなという感じ。

「設定ファイルを変更できること」を求めているわけではなくて、
「仕組みを理解してトラブルにも対応できること」が大事なので、
DNSサーバーが何をしているか簡単に書いてみる。

興味がある方なら技術者じゃなくてもわかるので、
ちょっと遊んでみるといい。

_________________________________________

まず、パソコンをインターネットにつなぐ時、
「DNSサーバー」と呼ばれるサーバーが設定される。
最近だと、接続しているプロバイダの方で自動設定してくれることが多いかな。

このDNSサーバーが pancake.30min.jp というのを、
180.148.170.66 というIPアドレスに変換してくれるのだ。
このIPアドレスがインターネット上の住所みたいなもので、
IPアドレスが分かると、接続ができるようになる。
(IPアドレスでなぜインターネットがつながるかの話はまた別の機会にでも)

では、DNSサーバーは、
世の中に大量にあるドメイン名を、
どうやってIPアドレスに変換しているのだろう?

まず、最初にDNSサーバーは、

a.root-servers.net ~ m.root-servers.net
(aからアルファベット順)

の18か所のIPアドレスしか持っていない。
何か問い合わせがあると、まずこの18か所のどこかにいく。

windowsのコマンドプロンプトで問い合わせを再現してみよう。

root-server

まず問い合わせをするnslookupコマンドを実行。
今回は動作を把握するために、再帰的問い合わせを無効にする。

set norecurse

問い合わせるサーバーを、a.root-servers.netにする。

server a.root-servers.net

として、pancake.30min.jpについて問い合わせをすると、
上記画像のように、

a.dns.jp ~ g.dns.jp が帰ってくる。

.jpについては、これらのサーバーに聞きに行きなさいということだ。
そこで、a.dns.jpにpancake.30min.jpを聞きに行くと。

dns-jp

ようやく弊社のDNSサーバー、
dns.30min.jpとdns2.30min.jpが帰ってくる。
ここにpancake.30min.jpのIPアドレスが書かれている。

DNSサーバーに問い合わせをした場合、
このような問い合わせが順次行われているのだが、
毎回これを聞きに行くと、インターネット上のトラフィックが膨大になる。

そこで、実際にはTTL(Time To Live)という秒数を設定して、
一度他のDNSサーバーに問い合わせをしたら、
TTLの期間中は結果がキャッシュされるようになり、
キャッシュからデータを返すようになる。

キャッシュの生存時間について調べたければ、
nslookupコマンド中の中で、

set d2

としてやると、デバッグモードが有効になり、
問い合わせの結果のTTLなどが詳細に帰ってくる。

新しいjpドメインを取得した時に、
取得した業者でDNSサーバーのIPアドレス設定をすることになるけど、
これは業者の方で、 a.dns.jp ~ g.dns.jp に、
DNSサーバーのIPアドレスの記述を追加するよう、
申請してもらっているわけだ。

それが完了すると、自分のDNSサーバーに問い合わせが来るようになるので、
自分で書く設定が有効になる。

ここまでがDNSの仕組みの話。

_________________________________________

仕組みを理解すると、DNSの設定を変更する際には、
何をすべきかわかるようになる。

例えばIPアドレスの変更を遅延なく行いたい場合。

現在のDNSサーバーのTTLが3600秒(1時間)の場合、
まず、TTLを1秒などに設定変更をする。

変更をした後も、外部に1時間はキャッシュが生存するので、
そのまま1時間待つ。

そうすると、その後の問い合わせ結果は1秒で
キャッシュから削除されるようになるので、
IPアドレスの変更などが即座に世界中に反映されるようになる。
この状態になってから、IPアドレスを変更して、
順調に切り替わっていることを確認したら、
またTTLを適切な長さに戻せばよい。

動作検証には、nslookupやdigなどで外部のネームサーバーを指定して、
そちらに問い合わせを投げてみて、
きちんと変更が反映されていることを確認すればOK。

単に設定ファイルを変更すればいいと考えている
思考停止状態の人も多いけど、
きちんと仕組みを理解して、
運用計画やトラブルシューティングができるようになるといいね。

Nginxのimage_filterのパッチを作った

先日の記事で書いたNginxでの画像サーバーの構成は順調稼働中。
ただ、一つだけ変更したいなという点が出てきた。

構成3

image_filterモジュールなのだが、
resizeやcropの指定サイズが元画像より大きい時は、
画像に手を加えずにそのまま返すようになっているようだ。

無駄なリソース使わないためはそうなるよね、
と思うのだけど、jpegのqualityを指定したい時には、
その指定も無視されてしまう。

そもそも元画像のクオリティ落とせばいいんじゃない?
と言われそうだけど、一応quality高い画像を保持しておいて、
表示する時にだけquality落としたい時もある。
そこで、ngx_http_image_filter_module.cに手を入れることにする。

だいぶ前に書いた、
nginxを一台のサーバーで複数動かす
で、8080ポートでもう一つNginxを動かしているので、
そちらのソースコードを修正して、
ログを出すようにして問題箇所を探る。
(今回のバージョンは1.5.13)

545行目~の


if (rc == NGX_OK
    && ctx->width <= ctx->max_width
    && ctx->height <= ctx->max_height
    && ctx->angle == 0
    && !ctx->force)
{
    return ngx_http_image_asis(r, ctx);
}

と、773行目~の


if (!ctx->force
    && ctx->angle == 0
    && (ngx_uint_t) sx <= ctx->max_width
    && (ngx_uint_t) sy <= ctx->max_height)
{
    gdImageDestroy(src);
    return ngx_http_image_asis(r, ctx);
}

が原因だね。

resizeやcropで指定されたサイズが元画像より大きい時に、
処理を止めてしまうのが原因のようだ。

そこで、元画像より大きくても、
nginx.conf中のimage_filter_jpeg_qualityの指定と同じ個所で、

image_filter_jpeg_quality_force 1

と指定してやることで、jpegのquality指定が効くようにするパッチを作ってみた。

ngx_http_image_filter_module.c-1.5.13.patch

やっていることは、image_filter_jpeg_quality_forceの指定の追加と、
指定があれば、上に書いた条件分岐を回避するようにしただけ。

こういうのできるのが、オープンソースの面白いところだよね。
ApacheやNginxの仕組みを調べたり、
簡単なモジュールを自作してみたりすると、
開発する時の考え方やアプローチが変わってくると思うので、
やったことない人は、勉強がてら遊んでみるといいと思うよ。

…ちなみに社内で

「image_filterモジュールのパッチ作って」

と言ったらドン引きされたので、
仕方なく自作したのは内緒です。

面白い仕事だと思うんだけどなぁ。

「2019年までXP継続」って大変だろうなぁ

東京電力、「2019年までXP継続」の報道に対しコメント

こんな感じのイメージなんだろうなぁ。

a0002_002882

老朽化したシステムの入れ替え。
システム管理の担当者が頭を悩ませる仕事。

確かに大変な労力を使うのだけど、
僕は結構好きな仕事だったりする。
社内でもちょうどその話をしていたところ。

多くの人の意見を聞くと大概面倒なことになる。
だから2019年になっちゃうんだろうなぁと想像。

大企業の場合はいろいろと違う問題があるのだろうし、
会社によってはいろいろな事情があるでしょうから、
他社さんのことは何も言いません。

そこで、自社案件などで、
僕がシステムのリプレイスを短期間で行うコツと思っていることを。
それは、

必要だと思われるものしか移行しないこと

既存のシステムの完全移行をしようとせず、
新しい環境を作って、そこに必要なものだけを移すことだ。
で、移行期間を作って一定の短期間古いものを残しておく。

何年かシステムやPCに改変を加えていくと、
古くて使ってないものや、
サポートされていないソフトウェアなどが出てくる。

そういうのは無理に移行して、全て動くようにするよりは、
「新しい環境作っといたから、そっちで設定し直して」
くらいの方がいい。

「必要ですか?」と聞くと「一応必要」と言われるのだけど、
実際は全く使われてないものが多数。

なので、必要と思われるものだけ移しちゃって、
「足りないものあったら今月中に言ってください」
くらいでよかったりする。

もし本当に必要なものなら、
すぐに「あれが動かない!」と言われるので、
その時きちんと対応すればいいんだから。
何も言われないものは、そもそも不必要ってことだしね。

仕事をしていて、よく、
「古いバージョン使っている人もいるから、
それもそのまま動くようにしておいて」
と言われることがあるけど、
iOSの開発環境やTwitter、FacebookのAPIなどのように、
「○○日までにこのAPI終了だから対応してね」
とバッサバッサ切っていくスタンスの方が好き。

※システムについては古いものはどんどん切りますが、
古くからの友達は割と大事にする方だと思います、たぶん。

YOLP APIとオープンデータで遊ぶ

最近オープンデータが流行りですが、
これを利用してYOLPのAPI(Yahoo!地図)を使って表示する方法。
自分への備忘録として。

使用するデータベースはPostgreSQLのGIS拡張版。
いわゆるPostGIS。
サーバーサイドはRuby on Railsで。

まず国土数値情報ダウンロードサービスから、
何かしら利用したいデータをダウンロードしてくる。
利用するのはshape形式のファイルだ。

○○.shp
○○.shx
○○.def

の3つのファイルを利用してデータベースを作る。
(EPSGコードは4326を利用)

shp2pgsql -W cp932 -s 4326 ○○.shp テーブル名 | psql DB名

とすることで指定したDBにテーブルが作れる。
テーブルを新規に作るのではなく、既存のものに追加するには、

shp2pgsql -W cp932 -s 4326 -a ○○.shp テーブル名 | psql DB名

と「-a」を追加してやればよい。

このデータがPolygonの場合、これを地図に表示させるには、
Polygonを構成する点のリストが必要になる。

これを広範囲で取ってくると描画処理が大変なので、
地図に表示する範囲のデータのみを取って来たい。

そこで、地図の中心点の緯度、経度とズームレベル、
縦、横のピクセル数から、四隅の緯度、経度を取得する方法を考える。

map4

YOLPのAPIから色々調べてみると、
ズームレベルが1の時はwidthが256px。
これがズームレベルが増えるにつれて、2倍ずつになっている。

360度 = 256pxなので、ズームレベルが1の時
1px = 1.40625px。
ズームレベルをzoomとして、縮尺zを求めると。

z = 2 ** (zoom - 1)

中心位置の経度をlongitude、
左端(西端)をlongitude_left、
右端(東端)をlongitude_rightとすると、
地図の横幅がwidth pxの時は。


longitude_left = longitude - width / 2 * 1.40625 / z
longitude_right = longitude + width / 2 * 1.40625 / z

さて、問題は緯度だ。
メルカトル図法だと、北に行くに従って、
実際の寸法よりも拡大されていく。

そこでWikipediaでメルカトル図法の投影法について調べてみる。

…こういう時に学生の時にもっとちゃんと勉強していたら、
と思うのだけど、とりあえず、

緯度から座標を求める => 逆グーデルマン関数(角度)
座標から緯度を求める => グーデルマン関数(座標)

で求められそうだ。
グーデルマン関数とは何だかわからないので、
再びWikipediaの内部リンク先のグーデルマン関数を調べる。

アークタンジェントとか、exとか出てきて頭が痛くなるが、
rubyのMathモジュールに実装されているので、
グーデルマン関数、逆グーデルマン関数はこんなに簡単に実装できる。

グーデルマン関数


def gudermannian(x)
  return 2 * Math.atan(Math.exp(x)) - Math::PI / 2
end

逆グーデルマン関数
(Math.tanの引数の単位はラジアン:角度×π/180)


def inverse_gudermannian(x)
  return Math.asinh(Math.tan(x * Math::PI / 180))
end

これで緯度の計算がなんとかなるメドがついた。
ここでの座標は、地球を半径1の単位円とした場合の座標。
YOLPの場合はズームレベル1の時が256pxなので、
ズームレベル1の時の半径をrとすると、
円周の公式から、2πr = 256pxとなるので、

r = 128 / Math::PI

1px当たりの座標の変化量は、

1 / r / z

となる。

地図の上端(北端)、下端(南端)と中心との座標の変化量から、
上端(北端)、下端(南端)の座標を求め、
それを経度に変換してやればよさそうだ。

中心の緯度をlatitudeから中心の座標center_yを求めるには、

center_y = inverse_gudermannian(latitude)

地図の縦幅をheight pxとすると、
上端(北端)の座標top_yは、

center_y + height / 2 / r / z

下端(南端)の座標bottom_yは、

bottom_y = center_y - height / 2 / r / z

これらをグーデルマン関数を通して、
上端(北端)の座標latitude_topと、
下端(南端)の座標latitude_bottomを求めてやる。
(ラジアンから角度へ戻すのも含めて)


latitude_top = gudermannian(top_y) * 180 / Math::PI
latitude_bottom = gudermannian(bottom_y) * 180 / Math::PI

これで4隅の座標が出るので、
描画領域のPolygonをPostGISで作成する。


polygon = ::DB::Table.find_by_sql("
  select
    ST_GeomFromText('
      POLYGON((
        " + longitude_left.to_s + " " + latitude_top.to_s + ",
        " + longitude_left.to_s + " " + latitude_bottom.to_s + ",
        " + longitude_right.to_s + " " + latitude_bottom.to_s + ",
        " + longitude_right.to_s + " " + latitude_top.to_s + ",
        " + longitude_left.to_s + " " + latitude_top.to_s + "
      ))
    ', 4326) as view_area
").first

この描画領域と重なるPolygonの構成点を取得。


points = ::DB::Table.find_by_sql("
  select
    gid,
    ST_X((dp).geom) as longitude,
    ST_X((dp).geom) as latitude
  from
    (
      select
        gid,
        ST_DumpPoints(ST_Intersection(geom, '" + polygon.view_area + "')) as dp
      from
        Table
      where
        (
          ST_Transform(geom, 32653)
          && ST_Transform('" + polygon.view_area + "', 32653)
        )
        and ST_DWithin(
          ST_Transform(geom, 32653),
          ST_Transform('" + polygon.view_area + "', 32653),
          0
        )
      order by
        gid, dp
    ) as foo
")

この結果をJSONなどで出力し、
地図の描画領域が変わった時に取得して、
YOLPのPolygon表示方法に合わせて、
緯度、経度のリストを渡してあげればよい。

最近オープンデータは流行りなので、
国土数値情報ダウンロードサービスなどの
利用を検討している方がいらっしゃるようでしたら、
相談に乗りますのでお気軽にご連絡ください。

最近はYOKUMOKUのシガールが大好きです。

Nginxを改造して欲しかった画像サーバーを作った

スマートフォンサイトに向けての最適化だったり、
Webページのレイアウトの変更だったりで、
同じ画像でも様々なサイズを利用することがある。

サイトの運用を続けていると、
同じ画像でも数通りのファイルを保存していて、
ファイル数が大量になってIOがキツくなってきた。

数多くなるとIO辛いから、
ファイル増やさないように気をつけてね、
と社内で言い続けていたら…

Webサイトでは、本来小さい画像で良いところに
大きい画像を使用して、
JavaScriptでリサイズする処理が多用されはじめ、
今度は転送量が増加しはじめて、
これ以上増えるとデータセンターの月額費用が
増加してしまいそうに…。

なんとかせねば!と対応をはじめるわけです。

そこでまず最初にやったのが、
本当は速いImageMagick
と同じように、Apacheモジュールを自作して、
ImageMagickでリサイズする構成。

構成1

画像がアップされた時に、
RailsからWebDavで画像サーバーにファイルをアップ。

画像サーバーでは、

/resize/100x100q80/hoge/hoge.jpg
/crop/100x100q80/hoge/hoge.jpg

などとして、パスの前にresizeかcropかを明記し、
width×heightと、必要であればJPEGのクオリティ指定も。
Apacheで動的に処理させるので、
保存する画像は1枚だけで良いし、
サイズやクオリティの調整で転送量も調整できる。

これはすばらしい!と思いながら、
こちらへの移行を開始しはじめたのだが…。

大量に処理をさせると、CPU負荷が辛くなってきた。
そこでキャッシュを入れる仕組みを検討する。

また、画像処理については、
データセンターのサービスでVMWare Vcenterを
安く借りられるプランがあったので、
そちらを利用して分散することにする。

そして、
簡単!リアルタイム画像変換をNginxだけで行う方法
を参照して、画像処理はNginxの、
http_image_filter_moduleを使用するように。

考えていた構成はこんな感じだ。

構成2

しかし、この構成には一つ問題があり、
採用に踏み切れなかった。

画像のキャッシュ削除の方法がないのである。

Nginxのcache_purgeモジュールなどで削除する方法はあるが、
リサイズしたすべてのファイルを抽出することはできない。

Nginxはキャッシュファイル名をmd5化する。
md5は不可逆なので、キャッシュファイル名から、
元のファイル名に関連するものを
ピックアップするのは不可能だ。

違う方法も検討したけど、
Nginxのcache managerは古いファイルを自動削除してくれるので、
余計なファイルが大量生成されるのを防ぐために、
どうしてもNginxのキャッシュの仕組みを利用したいなぁと。

そこで、ファイルの更新があった場合に、
/var/spool/nginx/update というディレクトリを作り、
その下に元ファイルのmd5データを入れておき、
キャッシュファイルの更新日時がそれより古い場合は、
キャッシュHITさせないように、
Nginxの処理を変更させてやろうと思った。

具体的な処理は次のような感じだ。

ロジック

最初はNginxのモジュールを書こうと思ったのだが、
色々調べているうちに、
src/http/ngx_http_upstream.c
に直接手を入れた方がやりやすそうだったので、
ソースコードをいじることにした。
(セットアップしていたのが少し古くて1.5.6ベース)

※改造したngx_http_upstream.cはこちら

まずキャッシュファイル名の生成に使うので、
ngx_md5.hをincludeしてやる。

#include <ngx_md5.h>

独自関数の定義。

static ngx_int_t ngx_http_upstream_sanzero_cache(ngx_http_request_t *r);

ngx_http_upstream_cachの中で
NGX_DECLINEDを返せばキャッシュを読まなくなるので、
750行目あたりの、
rc = ngx_http_file_cache_open(r);
の後に独自のチェックへの処理を追加してやる。


if (ngx_http_upstream_sanzero_cache(r) == NGX_DECLINED) {
  u->cache_status = NGX_HTTP_CACHE_BYPASS;
  return NGX_DECLINED;
}

独自の関数の処理。


static ngx_int_t ngx_http_upstream_sanzero_cache(ngx_http_request_t *r)
{
  ngx_str_t                    *key;
  ngx_md5_t                     md5;
  ngx_uint_t                    n;
  ngx_uint_t                    i;
  ngx_file_info_t               fi;
  ngx_file_info_t               cache_fi;
  ngx_http_cache_t             *c;
  ngx_regex_compile_t           rc;
  ngx_regex_compile_t           rc2;
  ngx_http_file_cache_t        *cache;
  int                           ovector[21];
  int                           ovector2[24];
  int                           result;
  int                           result2;
  time_t                        now;
  size_t                        j;
  size_t                        level;
  size_t                        len;
  u_char                        errstr[NGX_MAX_CONF_ERRSTR];
  u_char                        errstr2[NGX_MAX_CONF_ERRSTR];
  u_char                        original_key[NGX_HTTP_CACHE_KEY_LEN];
  u_char                       *p;
  uint32_t                      crc32;

#if (NGX_PCRE)
  ngx_str_t pattern = ngx_string("^/(resize|crop)/([0-9]+)x([0-9]+)(q([0-9]+))?(/.+)$");
  ngx_str_t pattern2 = ngx_string("^(.*)?(/resize|/crop)/([0-9]+)x([0-9]+)(q([0-9]+))?(/.+)$");

  ngx_memzero(&rc, sizeof(ngx_regex_compile_t));

  rc.pattern = pattern;
  rc.pool = r->pool;
  rc.options = NGX_REGEX_CASELESS;
  rc.err.len = NGX_MAX_CONF_ERRSTR;
  rc.err.data = errstr;

  if (ngx_regex_compile(&rc) != NGX_OK) {
    return NGX_ERROR;
  }

  result = ngx_regex_exec(rc.regex, &r->unparsed_uri, ovector, 21);

  if (result < 7) {
    return NGX_OK;
  }

  c = r->cache;
  key = c->keys.elts;

  len = 0;

  ngx_memzero(&rc2, sizeof(ngx_regex_compile_t));

  rc2.pattern = pattern2;
  rc2.pool = r->pool;
  rc2.options = NGX_REGEX_CASELESS;
  rc2.err.len = NGX_MAX_CONF_ERRSTR;
  rc2.err.data = errstr2;

  if (ngx_regex_compile(&rc2) != NGX_OK) {
    return NGX_ERROR;
  }

  ngx_crc32_init(crc32);
  ngx_md5_init(&md5);

  for (i = 0; i < c->keys.nelts; i ++) {
    result2 = ngx_regex_exec(rc2.regex, (ngx_str_t *)&key[i], ovector2, 24);

    if (result2 >= 8) {
      int original_len = key[i].len - ovector2[14] + ovector2[4];
      u_char *original_path = ngx_pstrdup(r->pool, &key[i]);
      u_char *original_data = ngx_pnalloc(r->pool, original_len);

      if (original_data == NULL) {
        return NGX_ERROR;
      }

      if (ovector2[4] > 0) {
        ngx_memcpy(original_data, key[i].data, ovector2[4]);
      }

      original_path += ovector2[14];

      ngx_memcpy(original_data + ovector2[4], original_path, key[i].len - ovector2[14]);

      ngx_str_t original_key_str = { original_len, original_data };

      ngx_crc32_update(&crc32, original_data, original_len);
      ngx_md5_update(&md5, original_key_str.data, original_key_str.len);
    }
  }

  ngx_crc32_final(crc32);
  ngx_md5_final(original_key, &md5);

  cache = c->file_cache;

  ngx_str_t update_path = ngx_string("/var/spool/nginx/update");

  ngx_str_t original_file_name = {
    update_path.len + 1 + cache->path->len + 2 * NGX_HTTP_CACHE_KEY_LEN,
    ngx_pnalloc(r->pool, update_path.len + 1 + cache->path->len + 2 * NGX_HTTP_CACHE_KEY_LEN + 1)
  };

  if (original_file_name.data == NULL) {
    return NGX_ERROR;
  }

  ngx_memcpy(original_file_name.data, update_path.data, update_path.len);

  p = original_file_name.data + update_path.len + 1 + cache->path->len;
  p = ngx_hex_dump(p, original_key, NGX_HTTP_CACHE_KEY_LEN);
  *p = '\0';

  len = original_file_name.len;
  j = update_path.len + 1;

  original_file_name.data[update_path.len + cache->path->len] = '/';

  for (n = 0; n < 3; n ++) {
    level = cache->path->level[n];

    if (level == 0) {
      break;
    }

    len -= level;
    original_file_name.data[j - 1] = '/';

    ngx_memcpy(&original_file_name.data[j], &original_file_name.data[len], level);
    j += level + 1;
  }

  if (ngx_file_info(original_file_name.data, &fi) == NGX_FILE_ERROR) {
    return NGX_OK;
  }

  now = ngx_time();

  if (now - fi.st_mtime > cache->inactive) {
    ngx_delete_file(original_file_name.data);
    return NGX_OK;
  }

  if (ngx_file_info(c->file.name.data, &cache_fi) == NGX_FILE_ERROR) {
    return NGX_OK;
  }

  if (cache_fi.st_mtime > fi.st_mtime) {
    return NGX_OK;
  }
#else
  return NGX_OK;
#endif

  return NGX_DECLINED;
}

そして、/var/spool/nginx/update
にファイルを置くための独自モジュールを作成。

ngx_http_sanzero_purge_module.c

nginx.confにはこんな感じで書けばOK。


proxy_temp_path /var/spool/nginx/temp;
proxy_cache_path /var/spool/nginx/cache keys_zone=cache:512m levels=2:2:2 inactive=1d max_size=100g;

upstream resize {
  server 192.168.0.1
  server 192.168.0.2
  server 192.168.0.3
  server 192.168.0.4
}

server {
  listen 80;

  location ~ /purge(/.*) {
    # 削除を許すIPアドレス
    allow 192.168.0.0/24

    deny all;

    proxy_cache cache;
    proxy_cache_key "$host:/$1";

    sanzero_purge;
  }

  location / {
    allow all;

    proxy_cache cache;
    proxy_cache_key "$host:/$request_uri";
    proxy_cache_valid 200 1d;

    proxy_pass http://resize;
  }
}

これで /purge/hoge/hoge.jpg にGETリクエストを送ると、
/hoge/hoge.jpg 由来のキャッシュは、
順次更新されるようになる。

最終的な構成はこんな感じだ。

構成3

あとは削除されたファイルは、
/var/spool/nginx/update
に残り続けることがあるので、
キャッシュの有効期間(例えば1日)以前のものは、
findコマンドなどで定期的に消してやれば良い。

運用の仕方によりそうだけど、
更新、削除されるファイルはそれ程多くないだろうから、
こちらの処理はそんなに重たくはならないだろう。

今のところ順調に稼働しているので、
これでしばらく画像のインフラに悩まず過ごせそうだ。

プログラミング宗教戦争

プログラマーにはそれぞれの仕事の流儀がある。
時としてその手法の違いにより、論争が起こることがあり、
これを皮肉を込めて「宗教戦争」と呼ぶことがある。

a1180_009052

Webのフォームからの入力データだったり、
なんらかのinputのデータに不備がないかチェックする。
みな当たり前のようにそんな仕事をしている。

その一部を共通関数として、
皆が利用できるようにすることもあるだろう。
英数字のチェックだったり、電話番号のチェックだったり…。

僕はその部分を共通化することはあまり好きではない。
処理によって特殊な条件があることが多いのと、
よく考えないで”なんとなく”利用する習慣がついて、
チームのスキルが上がらないことがあるからだ。

ただ、社内のスタッフが書いた処理があって、
今回そちらに倣おうかなと思い、
コードを読み始めてみた。

・・・・・・・・

今は使われていないそうだけど、
全角カナをチェックする関数があった。
その中にまだ実装されていない処理について、
コメントが記載されている。

「全角スペースも追加」

なるほどねぇ…。
なんらかの処理で全角カナをチェックする際、
2種類の用途があるだろう。
1つは氏名や住所の入力などの文字列のチェック
(例を挙げているだけなので自動変換しろとは言わないでね)
として、間のスペースを許容したい場合。
もう1つは誤植やミスがないかのチェックとして、
完全に全角カナの連続であることをチェックしたい場合。

おそらく、将来スタッフの数が増えたりした時、
誰かが、どちらかの用途に向けて改変処理をするだろう。
そして、今までどちらかを前提としていた処理に、
不具合が出る可能性があるんじゃないか?
そんなことを思い始めた。

いろいろ悩んだ結果、使う人は使っていいよーということで、
自分が書くところは、それを使わずに個別に書くことにした。
後でその処理の内容に手が入る可能性がありそうだし、
個別に書いても大した量ではないしね。

共通化するのかしないのか、
その時の状況や、スタッフの性格、
どういう風にマネージメントしているかの状況によって、
どちらがベターか変わってくると思う。

例えばしっかり統制を取って、
その処理については厳しく管理する、ということであれば、
共通化した方が便利だろうしね。

宗派対立を収拾させてベターな方向に向けるのが、
CTOのお仕事なのです。

サービス開発の考え方

Ruby on Railsのように、
MVCモデルでの開発をしていると、

「MVCとしてこうあるべきだ」

という議論が出たり、
セオリーから外れたことをすると
悪人のような扱いを受けたりする。

僕はどちらかというと悪人らしい。

システム開発のセオリーの一つに、
同じような処理を何度も書いたりするのを嫌う、
Don’t Repeat Yourself.
いわゆるDRY原則というものがある。

以前は僕もDRY信者で、
汎用的な共通処理をいかにまとめるか、
ということに一生懸命だったこともあるし、
いわゆるMVC的な書き方みたいなものが
ベターだと思っていたこともある。

これはシステムを効率良く保守していくことを
主軸に置いた考え方じゃないかと思う。

僕は若い頃から特殊な現場に入れてもらうことが多く、
例えば自分が担当しておらず、担当者がすでに退職した案件の
サーバーのリプレイス(古いソフトのバージョン上げながら)
をいきなり頼まれるなど、結構シビアな運用を経験させてもらってきた。

なので、最近はシステムを効率よく保守するというより、
いつでもリファクタリングして移行しやすいようにと、
システムを効率良く壊すことを主軸において考えている。

ハードウエアの老朽化や、
使用ソフトのバージョンが古くてサポートが切れるくらい、
数年間の運用が進んだ後だと、
当然システムにも問題が出てくる。

当初の設計の意図と違う用件の継ぎ足し開発など、
コードの保守が大変になってきているので、
そういうのも直した方がいい、となってくる。
そこでリファクタリングが必要になるのだが、
システムを止める時間を短くリファクタリングをするのは、
相当の時間と神経を使う仕事だ。

一つのシステムで複数のものが動いていたりするとカオスだ。
共通クラスという名の下に、全ての案件がそのクラスを使っていたり…。
段階を分けて部分的に移植のような戦略を立てるのが大変になる。

こういう経験をしてくると、
必要なRepeat Yourselfもあるんじゃないかと。

各処理ごと、ツールごとの依存関係をなくしておくと、
リファクタリングが非常に楽になる。
また、何かトラブルが起きても、影響範囲が小さくなる。

同じシステムを保守していく考え方と、
いつでも壊せるようにする考え方では、
同じツールでも使い方変わってくるなーと。
いわゆる「セオリー」と言われていることは、
正解なことがほとんどだけど、時々当てはまらないこともある。
なので、これはセオリーだからと思考停止しないように、
「考える」習慣をつけた方がいいと思う。

僕の師匠の教えは二つ。

「システムには寿命がある」
「フレームワークは人の思考を停止させる」

こういう考え方もあるんだよと。

ST_TransformのエラーとProj.4のお話

最近自分が何している人かわからなくなっていますが、
僕も一応ジオメディアのプログラマーだったりします。

昨日データセンターにてサーバーを搬入、セットアップをした。
社員が作業をしない夜中の内に一部のDBを新しいサーバーに移行しておいて、
朝出社したら何事もないように動いているけど、
DBサーバーだけ変更になっている…。
なんてことをやろうとしたところ、ちょっとトラブルが。

古いバージョンで使っていたpostgisなどを、
今回は新しいものにしようとセットアップをしたが、
どうも挙動がおかしい。

ST_Transform関数でのメルカトル図法への変換の時に、
EPSG:32653から遠く外れた座標のものが、
エラーになってしまうようだ。
transform: couldn’t project point (○○ ○○ 0): latitude or longitude exceeded limits (-14)
のように。

そりゃ正しい動作だよ、と言われると思うのだが、
大量の位置情報を扱っていると、ごく稀に不正確なデータがある。
このごく稀なデータのせいで、
ST_DWithin(ST_Transform(geom1, 32653), ST_Transform(geom2,32653), 1000)
のような検索が全てエラーになってしまう。

こんな変換しなければいいと思われそうだが、
これは今はもうない某有名ブログの記事に書かれていたもので、
indexが有効になるので、パフォーマンスが全然違う。

もちろん、球体計算をしていないものなので、
正確な距離が必要な時には使えないが、
普通の位置情報サービスで近いものを抽出するのなどは、
精密さよりも速度の方が大事になってくる。

…で、どうしても別のSQLにするとかはしたくなかったので、
どうにかできないかと色々探ってみたところ、
どうもProj.4ライブラリの4.8.0だとエラーを出すようだ。
4.6.0ではこんなエラーが出ていなかったのだが…。
(間のバージョンは調査してない)

調べてみると、4.8.0のsrc/PJ_tmerc.cの中で、
90度以上外れているものはエラーを帰す仕様になっていた。
(35行目と175行目)

ここをコメントアウトすると動くには動くのだけど、
変換後の座標が変な値になってしまっているようだ。

遠いデータはそもそも間違っているものなので、
4.8.0のエラーをコメントアウトして利用すべきか、
今まで通りの安心の4.6.0にするか…ちょっと悩んだけど、
座標の値が変になるのが気持ち悪いので、
今回は4.6.0のままにしようかなー。

昨晩から今朝までと、今日の昼からずっとはまっていたのだけど、
4.6.0と4.8.0で/usr/local/lib以下に配置される
ライブラリのファイル名が違うので、
4.8.0の後に4.6.0をインストールする場合、
ファイルを消しておかないと4.8.0の方の
ライブラリが残って、そっちを見ちゃうんだね…。

まぁなんというか、普通の人はあまりやらないようなことやっているので、
誰の得にもならなさそうな文章になっちゃったけど、
自分用の備忘録として。