MariaDBからRedisへ一部の機能を移行したこと

記録しておかないとなんで移行したのか忘れてしまいそうだから書いておく。

MySQL5.6からMariaDBへ移行

だいたい毎月100万PVほどあるWEBサイトのデータベースをMySQL5.6からMariaDBへの移行と同時に見直して、一部の機能をRedisへ移行した。

MySQL5.7の5.6からの変更がうちには合わないし、もうMySQLは見限って良さそうだし、MariaDBへ移行することにした。やってみると、インストールと設定の移行はいろいろ細かい調整は必要だったけれど特に問題なく移行できた。

ただ、MariaDBへの移行で同時に解消しないかなあと思っていた問題も引きずってしまった。

DB接続の断続的な切断問題

問題というのは、高負荷になるとDBアクセスが断続的に切れてしまってアプリケーションでの接続し直しが頻発して結果的にWEBサイトのレスポンスが悪くなってしまうこと。MySQL Server goneとかなんとか。

当該サイトは11月ごろからアクセスが増えて12月から1月がピークを迎える。10月中にはなんとかしたかった。

どのSQLで接続が切れるかは特定できない。同じSQLでも切れることもあれば切れないこともある。SQLのログをとりつつどこで起きているのかをリストアップするとだいたい傾向がわかってきた。あれじゃないかなあと予想はしてたところだった。

テーブル定義: CREATE TABLE kvs(k int NOT NULL PRIMARY KEY, v text)

切断が起きる前後のSQLで80%程度をしめていたのが、このテーブルへのこういうSQL: SELECT v FROM kvs WHERE k in (1, 2, … 256)

SQLで柔軟に検索して集めてきたkvsテーブルのキーの値(kvs.v)を最終的にドンと持ってくる部分。

こういうkvsテーブルへのクエリーが60クエリー/秒程度を超えてくると結構きつく切断と再接続が頻発する。

アプリケーションではローカルディスクでのキャッシュもしているし、MySQLのクエリーキャッシュも設定しているけど、WHERE句のバリエーションの方が多くてあまりキャッシュにヒットしない。

売り上げを考えるとこれ以上のメモリー増設やCPUアップグレードもやりにくい。RDB的な使い方でもないしなんだかなあと思ってはいたので、いよいよKVSを試してみることにした。

Redisで解決

キーの値をまとめてもってくるkvsテーブルへのアクセス部分だけをRedisに移行することにした。この部分はシンプルだし、アプリケーションの手直しも容易(容易ならなぜ早くやらなかった)。

SELECT v FROM kvs WHERE k in (1, 2, … 256)的なSQL発行をRedisのMGET 1 2 … 256に置き換えるだけでMariaDBの負荷は半分以下になるし、100クエリー/秒を発行してみたけれどRedisはなんでもない。RedisとMariaDBの合計で使うメモリー量はほぼ変わらないか、MariaDBへの接続数が減ったのでその分トータルのメモリ使用量は減っているように見える。いいことばかり。

適材適所ってこと。おわり。

Introducing Lektor — A Static File Content Management System For Python | Armin Ronacher’s Thoughts and Writings

FlaskやWerkzeug、Jinjaで有名なArmin Ronacherがまた新しいプロダクトを公開した。スタティックファイルを使ったCMSみたい。

Introducing Lektor — A Static File Content Management System For Python | Armin Ronacher’s Thoughts and Writings

WordPressとかDjangoといったデータベースから動的にコンテンツを生成するシステムの複雑さが嫌になったって。他にもいろいろスタティックファイルを使ったCMSからはあるけど結局気に入らなくて自分で作ってしまったということらしい。

実は常々スタティックファイルの方がいいなあと思っているサイトがある。この人の作ったものは品質が高いしスタイルも好きだから、年が明けたら使ってみたい。使ってみたらまた感想を書いとこう。


年が明けてちょっと暇な時間にドキュメントを読んでみた。
いやあ、簡単に使ってみようって具合にはなかなかいかないぞ。
Jinja2は使ってるし、Flaskっぽい考え方がそこここにあって馴染みある感じなんだけど学ぶことのボリュームがかなりある。
ちゃんとやればいい感じでライフサイクルを管理できそうなのはよくわかった。さあどうしようか。

strftimeの%aで曜日を出力するのはやめた

localeを使ってみたりいろいろと試してみたけれど、無理にstrftimeの%aで曜日を漢字で出力するのは今ひとつだった。

ディストリビューションによって挙動が変わったり、OSのメジャーアップデートで文字化けしてみたり。それぞれに事情はいろいろあって、単にバグだったり、必要なパッケージが変わってたりと、まあ仕方ないかなあと思うことも少なくない。

ああでもないこうでもないと、忘れてしまっても問題ない方法を考えてみた。これでいいのだ。

#!/usr/bin/env python
# coding: utf-8
import datetime
WEEKDAY = ('月','火','水','木','金','土','日')
print(WEEKDAY[datetime.datetime.today().weekday()])

続きを読む strftimeの%aで曜日を出力するのはやめた

さくらのVPSのローカルネットワーク接続まとめ

さくらのVPSを5台借りてあれこれ負荷をかけてテストしてみている。実際にテストできるようになるまではいろいろあったけれど。

ローカルネットワークは1Gbのベストエフォートがなかなかよい感じに速いと思います。きちんとチェックしてないので数字は出しませんが。VPNだとかトンネリングだとかいろいろ工夫して複数台構成をさくらのVPSのグローバルネットワーク上に構築しているならば移る価値ありだと思います。3台あればいろいろ試すこともできるし6千円弱/月の価値はあるかと。うちは某VPSを2台解約することにさっき決めました。

本登録したし今後も使いますけれど、お試し期間中はローカルネットワーク接続が使えないというのはなんだかなあとは思いました。複数同時に申し込むとしたらそれが一番お試ししたいことのはずなのに。そうそう、複数台の申し込みが面倒くさいのもなんとかならないもんでしょうか。らしいといえばらしいのですが。

注意するべき点がひとつ。
さくらのサポート情報 よくある質問:さくらのVPSで回線速度が遅くアクセスに時間がかかります。

さくらのVPSにおいて、不特定のVPS収容ホストとクライアント環境(プロバイダ等)の組み合わせにより、さくらのVPSからのダウンロード方向の通信に遅延が発生する場合があります。

 ・http,ftp,scp等でのファイルのダウンロード速度が極端に遅い。
 ・ファイルをダウンロード中に通信が切断される。
 ・ホームページの静的なコンテンツ表示に異常に時間がかかる。

上記のような現象が確認された場合、設定変更を行うことで回避できる場合があります。

このQAとは現象は違うけれど、ローカルネットワーク経由の通信で1406bytes以上のデータをやりとりできない問題が起きる場合がある。

eth0=グローバルIPのネットワークインターフェースに対しては自動的にoffload tsoをoffにする設定が入っているからこの問題は起きない。しかし、eth1やeth2を自分で追加して使う場合、つまりローカルネットワークを使う場合は自分でoffload tsoを切らないと問題が起きる場合がある=切る必要がある。

どういう場合にこの設定が必要かは明らかにされないので、さくらのVPSを借りてローカルネットワーク接続を使う場合は必須の設定ということになるでしょう。問題起きたら設定でもいいんだろうけど僕はもう自動的に全部設定すると思う。

ちなみにUbuntu12.04ではこんな感じで設定しました。

auto eth1
iface eth1 inet static
address 192.168.1.1
netmask 255.255.255.0
network 192.168.1.0
broadcast 192.168.1.255
offload-tso off

auto eth2
iface eth2 inet static
address 192.168.2.1
netmask 255.255.255.0
network 192.168.2.0
broadcast 192.168.2.255
offload-tso off

もうひとつ。関係あるかもしれない注意点。

デフォルトでインストールされるCentOS6であれこれ試行錯誤していたときに、eth1とeth2のインターフェース設定の中でMACアドレスを指定しないと、OS起動時にインターフェースが見えなくて使用できない仮想マシンがありました。

その仮想マシンがoffload tsoを明示的に切る必要のある仮想マシンだったのかどうかはもう覚えていないのです。メモも見当たらないのです。でもそうだったような気がするなあ。

UbuntuにしてからはOSがNICを見つけられない状況は起きていないし、今後CentOSを使うつもりはないので確かめることもないのです。

さくらのVPSの複数台構成ローカルネットワーク接続でハマり中

「さくらのVPS」プラン拡充・機能追加などサービスリニューアルのお知らせ | さくらインターネット

このニュースにのって、今月1日にさくらのVPSのローカルネット対応等を期待して2Gを5台追加。とにかく試しに使ってみて、負荷をかけてみて今の本番環境と比較してみるのだ。
と張り切ってみたのだけど、二日間ぐらいはカスタムOSのインストールがスムーズにいかない。

こんなにカスタムOSのインストールに手間と時間がかかるのでは本末転倒なので、デフォルトのCentOS6で使ってみるかと試してみるも、こっちはこっちで慣れていなくて基本的な環境整備にひどく時間を取られる。はあ、もういやだ、とUbuntuのカスタムOSインストールをもう一度ためしてみると、数日前とはエライ違い。単に混み合っていたせいなのか、何か不具合が治ったのかはわからない。でもUbuntuなら慣れてるし助かる。

さて、5台分のOSを設定して、データベース・サーバとして2台、アプリケーションサーバを2台、リバースプロキシサーバを1台の構成を試す。が、MySQLがおかしい。1台だけどうしてもおかしい。mysqlクライアントでローカルネットワーク経由でデータベースサーバに接続して、SELECTするとハングアップしてしまう。色々試すと、だいたいパターンが見えてきた。
以下、ハングアップは、

  1. ローカルネットワーク接続の時にだけ起きる。グローバルネットワーク経由だと起きない。
  2. データ量として1kbぐらい(勘です)に閾値がある感じ。SELECT LIMITで件数を制限するとあるデータ量まではSELECTできるから。
  3. 接続先データベースサーバではなく、接続するクライアントマシン側の問題。接続先データベースサーバを変えても発生する場合も発生しない場合も状況は変化しない。
  4. 同じ構成のマシンで起きるマシンと起きないマシンがある(どうも収容ホストが違う気がする)。
  5. ローカルネットワークの割り当てを削除して再割当てしたりしてみたけど状況に変化ない。
  6. CentOS6で試したときにも起きていた。Ubuntuにしてからも起きている。条件は(たぶん)同じ。(CentOSのときはちゃんと調べなかった)

この件については、いろいろと検索してみたけれど、似たような状況は見当たらないし、サポートに問い合わせてみることにした。アプリケーションの話でしょ、と言われると弱いんだけどMySQLなんだからなんとか聞いてもらえないかなあ。ホントは他のアプリケーションでも現象を確認できるといいんだけど。続報を待て。

2013.11.5 追記
どうもその後の調査で特定の1台(a1とする)からのみハングアップするらしいことがわかった。a1,a2,b1,b2の4台の仮想マシンがある。b1とb2にMySQLサーバがインストールされている。a1,a2にはMySQLクライアントがインストール済み。
この時、a1からb1、a1からb2のデータベースへローカル接続した時のみハングアップの問題が起きる。b1からb2、b2からb1、a2からb1、a2からb2では問題は発生しない。いよいよa1の生成時に何かあったんじゃないかと考えてしまう。

2013.11.5 追記その2
mysqlだけじゃなあ、と思い立ってscpで何かコピーしてみよう。おおっ、stalled。ってことは。ファイルサイズを変えながらコピーしてみると、1405bytesまではコピーできる。MTUかも。と検索してみると、
networking – Why does SCP hang on copying files larger than 1405 bytes? – Unix & Linux Stack Exchange
ピタリ。これだ。母さん、これだよ。

sudo ip link set eth1 mtu 1400

してからコピーしてみるとできるできる。mysqlクライアントもちゃんと動く動く。ネットワークの問題や。このリンクをサポートに投げてみよう。

2013.11.5 追記その3
結局設定しろと言われる。さくらのVPSで回線速度が遅くアクセスに時間がかかります。
現象が違うので検索しても見つからんわなあ。 ubuntuの場合は、/etc/network/interfaceにoffload-tso offと書いておけば良い感じ。eth0は勝手に設定されているけど、eth1やeth2は自分で書くのでその時に忘れずに。少なくともさくらのVPSでは必須の設定みたい。収容ホストによって問題が出たり出なかったりするのかなあ。
まあこれはこれでよし。Close。

2013.11.7 追記
今日になってサポートから連絡。

本件についてご提示の情報を元に確認いたしましたところ、ホストサーバのMTU が適切でなかったことが判明いたしましたため、修正させていただきました。ご不便をおかけしまして、申し訳ございません。

ん?それじゃあ、offload-tso offの設定は生きてるのか生きてないのかどっちだ。今はMTU1500で通信できてるんだけど、一昨日の時点ではどうだったんだろ。こちらでは調べようもないし、これ以上手間かけたくないしほんとにClose。

Simple HTTP CheckerにTCPタイムアウト値を追加

Simple HTTP Checker – シンプルなHTTPサーバの監視ツールで公開しているツールですが、手元の環境では随分前にTCPタイムアウトの設定ができるように変更して運用しています。

ちょっと問い合わせをいただいて思い出したのでGist:343248に反映させました。なおタイムアウト値(設定変数名=tcp_timeout)は1未満にはできません。1以上の整数を指定してください。

[test_HEAD]
url: http://example.com/

[test_GET]
url: http://example.com/
method: GET

[test_POST]
url: http://example.com/
method: POST
data: Hello World

[test_notfound]
url: http://example.com/notfound.html
notify_interval: 10


[DEFAULT]
#************************************************
;  DEFAULT values
#************************************************

#Target URL.
#url=http://example.com/

#HTTP method
method: HEAD

#POST data
#data: Hello World

#When HTTP error occuered, repeatedly notified with interval seconds.
notify_interval: 600

#after checking, wait bellow seconds.
wait_seconds: 0

#tcp connection timeout seconds.
tcp_timeout = 10
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""#8 Simple HTTP Checker
  Usage:
       htpchk (htpchk.conf is needed at same directory.)
       htpchk URL
       htpchk config-file
"""
__NAME__ = '#8 Simple HTTP Checker'
__VERSION__ = '1.2'
__ABOUT__ = 'http://jinim.jp/archives/2136'
__USER_AGENT__ = 'Mozilla/5.0 (compatible; %s/%s; +%s)' % (__NAME__, __VERSION__, __ABOUT__)
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"

import os
import sys
import time
import socket
import urllib2
from urllib2 import URLError, HTTPError
from ssl import SSLError
from socket import timeout as SocketTimeout
import urlparse
import tempfile
from ConfigParser import ConfigParser

#url to filename with sha hash.
try:
    import hashlib
    def _urlhash_filename(url): return hashlib.sha224(url).hexdigest()
except:
    import sha
    def _urlhash_filename(url): return sha.new(url).hexdigest()

opener = urllib2.build_opener()
opener.addheaders = [('User-agent', __USER_AGENT__)]
urllib2.install_opener(opener)


class ParamError(Exception): pass


class HeadRequest(urllib2.Request):
    def get_method(self): return "HEAD"


class Site(object):
    def __init__(self, name, url, notify_interval=60*10, method="HEAD", data=None, wait_seconds=0, tcp_timeout=3):
        """When medhod is POST, data is requred."""
        self.name = name
        self.url = url
        self.method = method
        self.data = data
        self.notify_interval = notify_interval
        self.wait_seconds = wait_seconds
    #minimum timeout is 1sec.
        if tcp_timeout<1: tcp_timeout = 1        self.tcp_timeout = tcp_timeout        self.notified = os.path.join(tempfile.gettempdir(), "htpcheck-%s" % _urlhash_filename(self.url))

 def check(self):
 socket.setdefaulttimeout(self.tcp_timeout)
 if self.method=="HEAD":            u = urllib2.urlopen(HeadRequest(self.url))        elif> (last_notified + self.notify_interval):
            sys.stderr.write(msg)
            file(self.notified, 'a').write("%s\tnotified\n" % msg[:-1])
        else:
             sys.stdout.write(log_message(self, "[SUPPRESSED] %s" % err))


def log_message(site, msg=None):
    """if msg is not None, message means error.
    """
    if msg:
        format = '%s\tNG\t%s\t%s\t%s\t%s\n'
        return format % (time.strftime(DATETIME_FORMAT), site.name, site.method, site.url, msg)
    else:
        format = '%s\tOK\t%s\t%s\t%s\n'
        return format % (time.strftime(DATETIME_FORMAT), site.name, site.method, site.url)


def config_parse(conf):
    parser = ConfigParser()
    parser.readfp(open(conf))
    sites = list()
    for section in parser.sections():
        url = parser.get(section, 'url')
        method = parser.get(section, 'method')
        if method=="POST":
            data = parser.get(section, 'data')
        else:
            data = None
        notify_interval = parser.getfloat(section, 'notify_interval')
        wait_seconds = parser.getfloat(section, 'wait_seconds')
        tcp_timeout = parser.getint(section, 'tcp_timeout')
        sites.append(Site(section, url, notify_interval, method, data, wait_seconds, tcp_timeout))
    return sites


def main(sites):
    for site in sites:
        try:
            site.check()
            sys.stdout.write(log_message(site))
            if os.path.exists(site.notified):
                site.recovered()
        except (SocketTimeout, SSLError, URLError, HTTPError), err:
            site.notify(err)
        if site.wait_seconds>0: time.sleep(site.wait_seconds)


if __name__=='__main__':
    if len(sys.argv)>1: config = sys.argv[1]
    else: config = os.path.splitext(__file__)[0]+'.conf'

    if not os.path.exists(config):
        if config.startswith('http://') or config.startswith('https://'):
            name = urlparse.urlparse(config)[1].split(':')[0]
            sites = (Site(name, config), )
        else:
            raise ParamError, "config file %s not found." % config
    else:
        sites = config_parse(config)

    main(sites)

名刺がホンモノかどうかを確認する方法

長男が宿題の町探検をしているときに、NHKのディレクターに声を掛けられて名刺を貰ってきた。もしコメントしても良いということならば、親から電話をして欲しいとのこと。

まあ、たぶん、ホントのNHKのディレクターなんだろうけれど、この名刺の内容が間違いないと組織としてのNHKが保証してくれればそれに越したことはない。

でもそれを確認する方法は、あまりない。現状できる方法としては、NTTの番号案内にNHKの電話番号を聞いて電話して、いろんな部署をグルグルして名刺の本人にたどり着くといった感じかなあ。

ITを使ってこの確認作業を機械的に実現する仕組みは可能だろうか?なんかできそうな気がする。SSLの仕組みを参考に考えればできそうな気がするので少し考えてみようか。

と、ここまで書いてみて、まあ、、、僕はいらんな。と思ったので考えるのをやめた。

追記 2012.9.14
iOS6の秘密兵器Passbookのクーポン・チケット発行管理システムを電通が開発
目的は違うけどイメージしていたのはまさにこんな感じの仕組み。これを使えば従業員の名刺が正当かどうかを確認するような仕組みを提供できると思う。やっぱりこういう規模の会社じゃないとなかなか手を出しにくい分野だな。

WordPressテーマ Codium Extend の日本語翻訳ファイル

頼まれてセットアップしたサイトで使ったCodium Extendテーマがシンプルでいいなあと思ったので、ここでも使ってみることにしました。

そのサイトではできるだけ英語は出てきて欲しなくて、日本語の翻訳ファイルを作ったのでここで公開しておきます。

Codium Extend ver.1.0.9用の日本語翻訳ファイル: codium-extend_ja.zip

このzipファイルを解凍して、ja.moファイルをwp-content/themes/codium-extend/languages/に置けば使えるはずです。ja.poファイルの最新版はGist:1205390で公開しておきます。どうぞ自由にお使い下さい。

バージョンアップに追随できるかどうか心配なのでこのサイトは英語にするかも。

Linodeからさくらインターネットへ(一部)移行した理由

今年の3月にさくらインターネットからLinodeへ移行した理由というエントリを書いた。内容は専用サーバを解約してLinodeにすべてのサイトを移行した理由を連ねたもの。このエントリでは半年後の9月に一部のサイトをさくら(のVPS)に戻した理由を書いておくことにした。次に何か別のサービスが出てきたときに見返すためにも。

Linodeは管理するための機能が洗練されていてとても使いやすい。iPhone/iPod Touch用の管理ツールまであってこの出来もすばらしい。

が、日本に一番近いデータセンターでもやはり遅延が気になる。AdSenseで稼ぎ頭のサイトが4月以降20%ほど稼ぎが減ったのは、遅延が理由じゃないかと推測している。特にたくさんのアクセスがあるサイトほど(=稼ぎのいいサイトほど)、遅延が影響しているように感じる。感じる、気になるばかりだけれど、ちゃんと計測したわけではないので。
将来Linodeが日本にデータセンターを設置することも考えにくい。

7月にさくらインターネットがVPSサービスのベータテストを始めたときに、すぐに申し込んだ。一番アクセスの多いサイトのシステムをセットアップして、負荷テストをしてみたところかなり速いことがわかった。特にネットワークはさすがに速い。移行に乗り気になっていたのだけれど、この時点では価格が公表されていなかったので様子見。

その後、サービス名がVPS980ということがわかって、一気に乗り気になる。だって980円/月ってことだよね。サービスはこの円高の中でもLinodeの一番安いメモリ、ディスクが同程度のサービスのだいたい半額かあ。9月の正式発表を待って移行決定。

結局、

  • Linodeに移行したサイトのうち一番稼いでいるサイトだけをさくらのVPSに移行。
  • Linodeで借りていたメモリ2GBのコースを1GBのコースに変更。

という構成に落ち着き、9月4日にはさくらのVPSで本番稼働をはじめた。

この移行作業で、

  • 月々の支払いは約8千円弱から約5千円弱に圧縮できた。
  • 某サイトのアクセスが8月よりも30%ほど伸びた=今年最高(季節要因もある)。
  • 某サイトの9月のAdSenseの稼ぎが今年最高を記録した(季節要因もある)。

という成績。概ね移行は成功だった模様。

さくらのVPSに全部移行していないのは、

  • LinodeのDNS Manager(とDNSサーバの管理)が秀逸で千円/月ぐらいはこのシステムの使用料のつもり。
  • 他にも6つ運用しているサイトは遅延がそれほど問題にはならない(そんなにAdSenseにもアクセス数にも影響していない)。
  • VPS980では全部のサイトを動かすにはメモリが足りない。VPSを分けると増えすぎて今度は管理が面倒。
  • 今後さくらのVPSで別メニューがでてきたら、再考すればいいよね。

といった感じ。

先のエントリにも書いたけれど、そもそも、VPSの利点を生かせないこととコストの問題を除けば、さくらインターネットの品質、サービスには満足していた。さくらのVPSのサービス開始で先のエントリに書いた僕の不満はほぼ解消された。管理インターフェースのブラッシュアップと、早期の上位サービスの提供は期待したいところだけれど。


-(ハイフン)で始まるファイルがrmで消せない

ちゃちゃっとWEBサーバの出力を確認しようとしてwgetの出力を標準出力にするつもりを、手が滑って”–”としてしまった。結果、”–”というファイルができてしまった。
$ wget http://localhost/ -O –
削除しようにも、rm –ではシェルがオプションと解釈してしまって削除できない。
どうすべぇと調べてみたら、”–”の後にファイル名(–)を指定するといいみたい。

$ touch -- -cantremove
$ ls
-cantremove
$ rm -cantremove
rm: invalid option -- 'c'
Try `rm ./-cantremove' to remove the file `-cantremove'.
Try `rm --help' for more information.
$ rm *
rm: invalid option -- 'c'
Try `rm ./-cantremove' to remove the file `-cantremove'.
Try `rm --help' for more information.
$ rm -- -cantremove
$ ls
$ 

を、rmのメッセージによると、「rm ./- 」でも良かったんだ。
まだまだ知らないことがたくさんある。


Ubuntu 10.04にaptでRedmineをセットアップ

管理しているリポジトリをささっと見渡したり、プロジェクトのタスク管理にRedmineはとても便利。しばらくサーバの移転やなんやかんやでちゃんと動く自前のRedmineを持っていなかった。
今回、Linodeで借りているUbuntuにRedmineをセットアップすることにした。
一人で使うのであんまり凝ったこともしたくないので、aptでさくっといれよう。…が、20分ぐらいハマった。
ソースから最新版を入れる手順はあちこちで見つかるのだけど、aptでUbuntuのパッケージからMySQLの環境に入れるときの注意点としてメモ。

  • MySQLの照合順序がデフォルトではlatin1_swedish_ciになる。
  • aptのRedmine設定ではMySQLの照合順序を意識していない。
  • latin1_swedish_ciのデータベースにテープルを作るので、テーブルの照合順序もlatin1_swedish_ciになる。
  • データベース作成、テーブル作成が済んだ後で、初期投入されるトラッカーやロールの日本語文字が化ける。

ということらしい。

結局、以下の手順でなんとかなった。

  1. apt-get install redmineで一旦最後まで導入。次にテーブルを全部削除。データベースは削除しない。(あるいはデータベース毎削除しておいてCREATE DATABASEしてもいいと思う。)
  2. データベースの照合順序をutf8_general_ciに変更する。
    ALTER DATABASE  `redmine_default` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
    
  3. dpkg-reconfigure redmine を実施。データベースの作成の処理はすでに存在しているので、スキップされ、テーブルが作成される。このとき、先に指定したデーベースの照合順序utf8_general_ciが使われる。
  4. reconfigureによって正しい照合順序が設定されたテーブルに初期データが投入される。

aptのMySQLのデータベース作成、テーブル作成スクリプト部分に照合順序をutf8_general_ciにするように書いといてもらうことはできないものかな。
latin1で問題がでる場合はあっても、utf8で問題がでる場合はほとんどないような気がするのだけど。なんか理由があるのかもしれない。

Related posts:

  1. Ubuntu 9.04 serverを9.10へアップグレード


Xperiaは見送り。iPod Touch + WiMAXルータでiPad待ち?

43歳の誕生日。自分プレゼント、どうしようかなあ。
Xperiaは見送り決めたし、iPhoneはいまさら感もあってのれないし。
それなら、iPod Touch + WiMAXルータで、iPadが出てくるのを待ってみるのはどうだ。
時々持ち出すMacBookでも使えるじゃないか。
僕の使い方ならWiMAXはとてもいいのは、もうすでにTry WiMAXで経験済みなんだから。
ちょっと書き出してみよう。

初期: 18,810円 iPod Touch + 16,749円 AtermWM3300R = 35,559円
月額: 4,480円 UQ WiMAX

月額でマイナスできるのは、ドコモの基本料、通話料以外の約5,400円 (パケホーダイダブル4,200円 + Uスタンダードプラン500円 + その他 700円)。
単純に月々の通信費は1,000円ぐらいマイナスにできるな。ワイヤレスゲート(380円/月)もあった。

この分をSkypeにまわせるなあ。アレ、ドコモは止めてもいいのかも。通話料が毎月200円以下なんて。ああ、待ち受け時間の問題が残るか。WiMAXルータにケータイみたいなバッテリの持ちは期待できないなあ。なんかしらケータイはもっとかないとダメか。
あとカメラは…WiFiカメラっ。
そうだ、GPSが使えないか。ってことは、GoogleマップとGPSの連動したやつも使えないのか。ちょっと残念。
あと、荷物増えるな。

持ち運ぶデバイスと回線を分離できるのは、エンジニアとしては良い感じがするなあ。心地よい。
Kindleが欲しくなっても、iPadにしても、ほかのAndroid端末でも追加できるもんなあ。
でも、やっぱりいろいろひっかかるところも多い。どうしたものか。

p.s.
今ふと、はっ、僕はこんなにがんばってネット接続が必要なのか?これはなんだかがんばりすぎじゃないか?と思った。


Simple HTTP Checker – シンプルなHTTPサーバの監視ツール

WEBサイトの死活監視のサービスはいろいろあるけれど、どれも監視できるURL数に制限があったり、制限を解除してもらうには費用がかかるわけなのだけれど求める以上の機能があってちょっと高くついたり。なかなかピッタリこない。

仕方がない、さくらのレンタルサーバが1つあるのでそこで監視するようにしてみよう。社内のサーバ監視をしているNagiosは便利なんだけれど、さくらの500円/月のレンタルサーバに入れるのはちょっとアレだし、必要最小限のスクリプトをPythonで書くことにする。

求めた要件としては、

  • HTTPで接続できなかったらメールでお知らせが届く。
  • 一回メールを送ったら、しばらくはメールしないで欲しい。
  • HEADとGETとPOSTに対応。
  • HTTPステータスコードだけをチェックする(レスポンスに含まれるコンテンツのチェックは不要)。
  • 設定ファイルで複数のURLをまとめてチェックして欲しい。
  • (さくらなので)常駐するデーモンではなくて、単独のコマンドで実行できる(cronで繰り返し実行)。
  • お知らせメールはcrontabのMAILTOで送るからstderrに出力してくれればいい。
  • OKだったときのログはstdoutに出力してくれればいい。
  • スクリプトは1ファイルで完結させる。
  • Python2.4以降、標準ライブラリだけで動く。


使い方は、
1. python htpchk.py URL
とURLを引数に指定する方法。これだと細かい設定はできませんが、HEADでチェック、お知らせメールは多くて10分に1回、となります。

2. python htpchk.py htpchk.conf
と引数に設定ファイルを指定する(あるいは引数指定を省略して、htpchk.pyと同じディレクトリにhtpchk.confという設定ファイルを置いておく)と、複数のURLを一括してチェックできるようになります。
設定ファイルは、Windowsのiniファイルに良く似た書き方です。DEFAULTセクションは全セクションのデフォルト値として使われます。あとはチェックするURL毎にセクションを作ってurlパラメータを書いておくだけで1と同様にチェックはできます。セクション毎にDEFAULTセクションの値を上書きできるので、細かい変更がしたい場合はセクションに値を書きます。詳しい設定ファイルの書き方は後の方のサンプルをご覧ください。

cronにこのスクリプトを実行するように登録しておく(さくらのレンタルサーバではこんな感じ)。
/home/example/bin/にhtpchk.pyとhtpchk.confを置いたとするとこんな感じ。

MAILTO=alert@example.com, mobile@example.com
# NAME: htpchck
*/5     *       *       *       *       /usr/local/bin/python /home/example/bin/htpchk.py >/dev/null

これで、5分に一回htpchk.confに書いてあるサイトをチェックして、HTTPステータスが200じゃなければ、標準エラーにメッセージが出力されて、そのメッセージはMAILTOに書いたメールアドレスに送られます。標準出力は/dev/nullに向けてあるので捨てられます。

手元では47のURLをチェックする設定ファイルを書いて運用中。概ね良好なんだけれど、まだチェックするURLを増やしたいのでもう少しチェックにかかる時間を短縮したい。HTTPのリクエストを送る部分をスレッドにして同時実行すれば速くなるのはわかっているので、困ったらスレッド化するかも。

以下はスクリプト本体(htpchk.py)と設定ファイル(htpchk.conf)のサンプルです。最新版はGist:343248に置いてあります。

htpchk.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""#8 Simple HTTP Checker
  Usage:
       htpchk (htpchk.conf is needed at same directory.)
       htpchk URL
       htpchk config-file
"""
__NAME__ = '#8 Simple HTTP Checker'
__VERSION__ = '1.0'
__ABOUT__ = 'http://jinim.jp/archives/2136'
__USER_AGENT__ = 'Mozilla/5.0 (compatible; %s/%s; +%s)' % (__NAME__, __VERSION__, __ABOUT__)
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"

import os
import sys
import time
import urllib2
from urllib2 import HTTPError
import urlparse
import tempfile
from ConfigParser import ConfigParser

#url to filename with sha hash.
try:
    import hashlib
    def _urlhash_filename(url): return hashlib.sha224(url).hexdigest()
except:
    import sha
    def _urlhash_filename(url): return sha.new(url).hexdigest()

opener = urllib2.build_opener()
opener.addheaders = [('User-agent', __USER_AGENT__)]
urllib2.install_opener(opener)


class ParamError(Exception): pass


class HeadRequest(urllib2.Request):
    def get_method(self): return "HEAD"


class Site(object):
    def __init__(self, name, url, notify_interval=60*10, method="HEAD", data=None, wait_seconds=0):
        """When medhod is POST, data is requred."""
        self.name = name
        self.url = url
        self.method = method
        self.data = data
        self.notify_interval = notify_interval
        self.wait_seconds = wait_seconds
        self.notified = os.path.join(tempfile.gettempdir(), "htpcheck-%s" % _urlhash_filename(self.url))

    def check(self):
        if self.method=="HEAD":
            u = urllib2.urlopen(HeadRequest(self.url))
        elif self.method=="POST":
            u = urllib2.urlopen(self.url, data=self.data)
        elif self.method=="GET":
            u = urllib2.urlopen(self.url)
        else:
            raise ParamError, "method: %s not supported. Supported methods are (HEAD,GET,POST)." % self.method
        return u.info()

    def recovered(self):
        os.rename(self.notified, os.path.join(os.path.dirname(self.notified), os.path.basename(self.notified) + time.strftime("-recovered.%Y%m%d%H%M%S")))

    def notify(self, err):
        msg = log_message(self, err)
        last_notified = 0
        if os.path.exists(self.notified):
            try:
                last_notified = time.mktime(time.strptime(file(self.notified).readlines()[-1].split("\t")[0], DATETIME_FORMAT))
            except ValueError:
                last_notified = 0
        if time.time() > (last_notified + self.notify_interval):
            sys.stderr.write(msg)
            file(self.notified, 'a').write("%s\tnotified\n" % msg[:-1])
        else:
             sys.stdout.write(log_message(self, "[SUPPRESSED] %s" % err))


def log_message(site, msg=None):
    """if msg is not None, message means error.
    """
    if msg:
        format = '%s\tNG\t%s\t%s\t%s\t%s\n'
        return format % (time.strftime(DATETIME_FORMAT), site.name, site.method, site.url, msg)
    else:
        format = '%s\tOK\t%s\t%s\t%s\n'
        return format % (time.strftime(DATETIME_FORMAT), site.name, site.method, site.url)


def config_parse(conf):
    parser = ConfigParser()
    parser.readfp(open(conf))
    sites = list()
    for section in parser.sections():
        url = parser.get(section, 'url')
        method = parser.get(section, 'method')
        if method=="POST":
            data = parser.get(section, 'data')
        else:
            data = None
        notify_interval = parser.getfloat(section, 'notify_interval')
        wait_seconds = parser.getfloat(section, 'wait_seconds')
        sites.append(Site(section, url, notify_interval, method, data, wait_seconds))
    return sites


def main(sites):
    for site in sites:
        try:
            site.check()
            sys.stdout.write(log_message(site))
            if os.path.exists(site.notified):
                site.recovered()
        except HTTPError, err:
            site.notify(err)
        if site.wait_seconds>0: time.sleep(site.wait_seconds)


if __name__=='__main__':
    if len(sys.argv)>1: config = sys.argv[1]
    else: config = os.path.splitext(__file__)[0]+'.conf'

    if not os.path.exists(config):
        if config.startswith('http://') or config.startswith('https://'):
            name = urlparse.urlparse(config)[1].split(':')[0]
            sites = (Site(name, config), )
        else:
            raise ParamError, "config file %s not found." % config
    else:
        sites = config_parse(config)

    main(sites)

htpchk.conf

[test_HEAD]
url: http://example.com/

[test_GET]
url: http://example.com/
method: GET

[test_POST]
url: http://example.com/
method: POST
data: Hello World

[test_notfound]
url: http://example.com/notfound.html
notify_interval: 10


[DEFAULT]
#************************************************
;  DEFAULT values
#************************************************

#Target URL.
#url=http://example.com/

#HTTP method
method: HEAD

#POST data
#data: Hello World

#When HTTP error occuered, repeatedly notified with interval seconds.
notify_interval: 600

#after checking, wait bellow seconds.
wait_seconds: 0

Related posts:

  1. urllib2でプロキシを参照しないようにする
  2. urllib2.quote()ってアリなんだ
  3. Flickrから自分のアップロードした写真を全部ダウンロードする