MySQL 5.5以降のutf8mb4とPerlのDBD::mysqlのmysql_enable_utf8のワナ

結論から言うと、ひろせが望ましいと思う順にこうすればいいんじゃないの?ってのを列挙します。

  1. my.cnfに[libmysqlclient]グループを追加しそこにdefault-character-set = utf8mb4と書き、DBD::mysqlでは mysql_read_default_file=/etc/my.cnf;mysql_read_default_group=libmysqlclient と指定する
    • mysql_enable_utf8を使いたい場合はSET NAMES指定も必要
  2. my.cnfの[client]グループにdefault-character-set = utf8mb4と書き、DBD::mysqlでは mysql_read_default_file=/etc/my.cnf と指定する。mysqlbinlogなど--default-character-setオプションを解せずエラーになるものは--no-defaultsをつけて実行する
    • mysql_enable_utf8を使いたい場合はSET NAMES指定も必要
  3. DBD::mysqlで接続後に SET NAMES utf8mb4 を実行する

オハマリポイントは

  • DBD::mysql (libmysqlclientなクライアント全般?) は loose-default-character-set の設定値を認識しない
  • select結果をPerlの内部表現文字列(UTF8フラグ)にしようと DBD::mysqlmysql_enable_utf8 を使うと、ついでにクライアント側も文字コード設定を(utf8mb4ではなく)utf8にしてしまう

です。

DBD::mysql (libmysqlclientなクライアント全般?) は loose-default-character-set の設定値を認識しない

my.cnfの[client]グループに書いた設定はクライアント全般に適用されるので、ここにdefault-character-set = utf8mb4 と書ければいいのですが、[client]グループに書くとmysqlbinlogなどこのオプションに対応していないクライアントがエラー終了してしまいます。

my.cnfの設定項目には loose- というprefixをつけることができて、対応していないクライアントはそれを無視することができます。

なので、[client]グループに loose-default-character-set = utf8mb4 と書いておけば、シアワセになれると思っていたのですが、loose- だとDBD::mysql (libmysqlcientを使うクライアント全般かもしれません)が認識しくれず、libmysqlclientのデフォルト文字コード(特に変えてなければ latin1)になってしまいます(ガーン)。

仕方ないので、[libmysqlclient]といったグループを新規で追加してそこにdefault-character-setを書き、DBD::mysqlにはそのグループを読むように指示するか、mysqlbinlogがエラーになるのは --no-defaults オプションをつけて回避するか、の2択になります。

SET NAMESよりdefault-character-setを推す理由は、エスケープ処理に関連するからです。詳しくはこのへんを参照してください。

select結果をPerlの内部表現文字列(UTF8フラグ)にしようと DBD::mysqlmysql_enable_utf8 を使うと、ついでにクライアント側も文字コード設定を(utf8mb4ではなく)utf8にしてしまう

の通りですが、DBD::mysqlmysql_enable_utf8 はPerlの文字列の内部表現化(sv_utf8_decode)するだけでなく、mysql_options(MYSQL_SET_CHARSET_NAME) も実行します。

  • mysql_enable_utf8 が 1
  • mysql_enable_utf8 が 0
  • mysql_enable_utf8を指定しない
    • mysql_options(MYSQL_SET_CHARSET_NAME) は呼ばない

なにが問題になるかというと、mysql_enable_utf8 を 1 にしてネコ (Unicode: U+1F639, UTF-8: \xF0\x9F\x98\xB9) をutf8mb4なテーブルにinsertしても、utf8mb4でなく MYSQL_SET_CHARSET_NAME=utf8 になっているので DBD::mysql::st execute failed: Incorrect string value というエラーでinsertできません。

また、mysql_enable_utf8 を 0 にした場合(MYSQL_SET_CHARSET_NAME=latin1 を実行している)やmysql_enable_utf8を指定しなかった場合(libmysqlclientのデフォルトであるlatin1が使われる)、my.cnfの適切なグループにdefault-character-set = utf8mb4と書いてあれば、character_set_clientや_connectionはutf8mb4になりますが、libmysqlclientのエスケープ処理は(多分)latin1で行われるのでもしかしたら問題があるかもしれません。

エスケープ処理が本当にlatin1で行われるのかと、その場合具体的にどういう問題があるのか、はちょっと調べきれてないので情報あったら教えてください><)

エスケープの問題を別にすれば、mysql_enable_utf8を有効にしてもしなくてもいずれにせよ、MySQLに接続した直後にSET NAMES utf8mb4を実行すれば、ネコもちゃんとハンドリングできます。

生のDBD::mysqlなら、Callbacksのconnectedでやるといいと思います。

my $dbh = DBI->connect($dsn, $user, $password, {
    ...
    Callbacks => {
        connected => sub {
            $_[0]->do('SET NAMES utf8mb4');
            return;
        },
    },
});

bashでsplitを書いてみた

空白絡むとどうにも配列で返せなかったんで、裏変数(_split)経由で結果渡すようにしてるのがイマイチ。。。

#!/bin/bash

set -u
set -e
export LANG="C"

split() {
  sep=$1
  str=$2

  _split=()
  if [[ $str =~ $sep ]]; then
    while IFS= read -r e; do
      _split+=("$e")
    done < <(echo "${str//$sep/$'\n'}")
  fi

  # declare -p _split >&2
}

split ':' 'foo:bar:baz'
declare -p _split
echo ">${_split[0]}<"

split ':' 'f o o:b a r:baz'
declare -p _split
echo ">${_split[0]}<"

split ':' ':'
declare -p _split
echo ">${_split[0]}<"

exit

結果:

declare -a _split='([0]="foo" [1]="bar" [2]="baz")'
>foo<
declare -a _split='([0]="f o o" [1]="b a r" [2]="baz")'
>f o o<
declare -a _split='([0]="" [1]="")'
><

PerlのDBD::mysqlをlibmysqlclient.aとstatic linkしたい話

static linkするにあたっての動機、諸注意(ハメがあるので必読)は [twitter:@sonots] さんの

を参照してください。

ここではDBD::mysqlをビルドする際のオプションのみ記します。

http://dev.mysql.com/downloads/mysql/ からダウンロードできるrpm

の場合、

$ ldconfig -p | grep libmysqlclient
        libmysqlclient.so.18 (libc6,x86-64) => /usr/lib64/libmysqlclient.so.18
        libmysqlclient.so (libc6,x86-64) => /usr/lib64/libmysqlclient.so

$ rpm -ql MySQL-devel | grep libmysqlclient.a
/usr/lib64/mysql/libmysqlclient.a

$ mysql_config --libs
-L/usr/lib64 -lmysqlclient -lpthread -lm -lrt -ldl

こういう感じの構成になっているので、

  • /usr/lib64の代わりにlibmysqlclient.aがある/usr/lib64/mysqlにライブラリパスを通す
  • libstdc++もリンクする

するように、このように

$ mysql_config --libs | sed -e 's@-L/usr/lib64@-L/usr/lib64/mysql@' -e 's@$@ -lstdc++@'
-L/usr/lib64/mysql -lmysqlclient -lpthread -lm -lrt -ldl -lstdc++

します。


ソースを展開してMakefile.PLを使ってビルドする場合は、--libsにこれを指定すればよいです。

$ perl Makefile.PL --libs="$(mysql_config --libs | sed -e 's@-L/usr/lib64@-L/usr/lib64/mysql@' -e 's@$@ -lstdc++@')"
$ make

$ ldd blib/arch/auto/DBD/mysql/mysql.so
        linux-vdso.so.1 =>  (0x00007fff9a52a000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f3434572000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f34342ee000)
        librt.so.1 => /lib64/librt.so.1 (0x00007f34340e5000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f3433ee1000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f3433ccb000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f3433936000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3434d84000)
# ↑libmysqlclient.so を dynamic linkしていない

$ perl -Iblib/lib -Iblib/arch -MDBD::mysql -e 1
# ↑ちゃんとモジュールをロードできる

$ sudo make install

ちなみに -lstdc++ していない場合はこのようにモジュールのロードで失敗します。

$ perl -Iblib/lib -Iblib/arch -MDBD::mysql -e 1
Can't load 'blib/arch/auto/DBD/mysql/mysql.so' for module DBD::mysql: blib/arch/auto/DBD/mysql/mysql.so: undefined symbol: __cxa_pure_virtual at /usr/lib64/perl5/DynaLoader.pm line 200.
 at - line 0
Compilation failed in require.
BEGIN failed--compilation aborted.


cpanmでインストールする場合は、--configure-args="--libs=..." に指定すればよいです。

$ mylib=$(mysql_config --libs | sed -e 's@-L/usr/lib64@-L/usr/lib64/mysql@' -e 's@$@ -lstdc++@')
$ cpanm -n DBD::mysql --configure-args="--libs='${mylib}'"


Oracle謹製のrpmには先程書いた http://dev.mysql.com/downloads/mysql/ で配布しているものの他に、yumレポジトリで配布しているものもあります。

なぜか両者ではライブラリファイルの配置が異なっているので気をつけてください。

このように、

$ ldconfig -p | grep libmysqlclient
        libmysqlclient.so.18 (libc6,x86-64) => /usr/lib64/mysql/libmysqlclient.so.18
        libmysqlclient.so (libc6,x86-64) => /usr/lib64/mysql/libmysqlclient.so

$ rpm -ql mysql-community-devel |  grep libmysqlclient.a
/usr/lib64/mysql/libmysqlclient.a

$ mysql_config --libs
-L/usr/lib64/mysql -lmysqlclient -lpthread -lm -lrt -ldl

yumで配布しているrpmのはlibmysqlclient.soも.aも同じディレクトリ/usr/lib64/mysqlにあります。

このままではstatic linkできないので、*.aを別のディレクトリにコピーして、そこにライブラリパスを通してビルドしないといけません。

$ mkdir /tmp/oreno-lib
$ cp /usr/lib64/mysql/*.a /tmp/oreno-lib/

$ mysql_config --libs
-L/usr/lib64/mysql -lmysqlclient -lpthread -lm -lrt -ldl
$ mylib="-L/tmp/oreno-lib -lmysqlclient -lpthread -lm -lrt -ldl -lstdc++"

$ perl Makefile.PL --libs="${mylib}"
  OR
$ cpanm -n DBD::mysql --configure-args="--libs='${mylib}'"

VirtualBoxのスナップショットを簡単に管理できるツールを書きました。GO言語で。

VagrantではSahara pluginを使うことで、VMの状態を以前の状態に巻き戻すことができます(sandobx mode)。

VMの中でいろいろいじっている際に変更前の状態に戻せるのはとても便利なのですが、Saharaでは戻せるチェックポイントをひとつしか作れません。

自分の場合、深遠なChefのレシピを書いている過程で、戻せるポイントを何個か置きたくなることがよくあります。


さて、VagrantのバックエンドとしてVirtualBoxを使っている人は多いかと思います。

バックエンドがVritualBoxの場合、SaharaのsandboxはVirtualBoxのsnapshotを使って実現されています。

VirtualBoxのsnapshotはひとつだけでなくいくつでも作ることができます。

CUIVirtualBoxの操作(snapshotを作ったり)をするには、vboxmanage コマンドを使えばできるのですが、VMの名前が長かったりしてちょっと使いづらいです。

そこでsnapshotに関連する操作を簡単に実行できるラッパーツールを作りました。

使い方

動作中のVMの一覧を得る
$ vboxss list
vm1                 vm1_default_1404895653615_55181
vm2                 vm2_default_1404967162355_44921

VMの正規名は右側のながーいやつですが、vboxssは左の短縮名でも受け付けます。多くの場合、短縮名はvagrant initしたディレクトリ名になると思います。

あるVMのsnapshotの一覧を得る
$ vboxss list vm1
List of the snapshots of vm1_default_1404895653615_55181
initial                           e718f597-22b4-4bef-adf6-239fff78215f
install-apps                      5337e220-0075-46a5-8aaa-901361f352df
before-apply-chef                 703071da-26c1-4504-8c81-4535de33c2f2

listの後にVM名を指定すると、そのVMのsnapshotの一覧が表示されます。VM名は短縮名でも長い正規名でもOKです。

snapshotをとる
$ vboxss take vm1 provisioned

takeの後に、VM名と、snapshotにつける名前を指定します。

snapshotを使ってVMをレストアする
$ vboxss restore vm1 before-apply-chef

vm1という名のVMの状態を、before-apply-chefという名のsnapshotの状態に戻します。

vboxss restoreを実行すると、一旦VMが停止される点に留意してください。(レストア後、自動でまた立ち上がります)

snapshotを消す
$ vboxss delete vm1 provisioned

落穂ひろい

Goをまともに書いたのはこれが初めてなので「こう書いたほうがイイヨ!!」とかあったら教えてください><


バイナリのビルドと配布は Wercker を使ってます。[twitter:@lestrrat] さんと [twitter:@motemen] さんのを参考にさせてもらいましたー&Werckerのboxは lestrrat/peco-build をそのまま使わせてもらってます!!

シンボリックリンク絡みでtail -Fが追従しないケース

tail -Fしているfluent-agent-liteでハマったのでメモ。

存在しないファイルをtail -Fした後、その名前のシンボリックリンクを作った場合

$ rm -fr ~/oreno-tmp && cd ~/oreno-tmp

$ tail -F tailme &
tail: cannot open `tailme' for reading: No such file or directory

$ ln -s real-file tailme
tail: cannot watch `tailme': No such file or directory

$ date >  real-file
$ date >> real-file
$ date >> real-file
  # tail -Fから流れてこない


ちなみに、存在するファイルを指すシンボリックをtail -Fしてる場合、途中で指す実ファイルが存在しなくなっても、実ファイルができ次第、追従してくれます。

$ rm -fr ~/oreno-tmp && cd ~/oreno-tmp

$ touch real-file.yesterday
$ ln -s real-file.yesterday tailme
$ tail -F tailme &
tail: inotify cannot be used, reverting to polling

$ date >> real-file.yesterday
Mon Jul 28 21:20:59 JST 2014

$ ln -sf real-file.today tailme
tail: `tailme' has become inaccessible: No such file or directory

$ date >> real-file.today
tail: `tailme' has appeared;  following end of new file
Mon Jul 28 21:21:39 JST 2014

$ date >> real-file.today
Mon Jul 28 21:21:48 JST 2014


つまり、既に存在する日付入りファイル名の実ファイルを指すシンボリックリンクがある場合は、00:00に今日の日付の名前を持つ実ファイルにシンボリックリンクを切り替えたときに、今日の日付の名前の実ファイルはあってもなくてもよい。

が、ド新規で追加したログファイルとかで、実ファイルもシンボリックリンクもない場合はハマる。

実ファイルがシンボリックリンクに変わった場合

$ rm -fr ~/oreno-tmp && cd ~/oreno-tmp

$ date > tailme
$ tail -F tailme &
Mon Jul 28 21:07:06 JST 2014

$ date >> tailme
Mon Jul 28 21:07:54 JST 2014

$ ln -sf real-file tailme
tail: `tailme' has become inaccessible: No such file or directory
tail: cannot watch `tailme': No such file or directory
$ date >> real-file
$ date >> real-file
$ date >> real-file
  # tail -Fから流れてこない

daemontoolsなserviceをハンドリングするための Chef::Provider::Service::Daemontools を書いてみました

Chefでdaemontools配下のサービスをハンドリングするときは、コミュニティクックブックの daemontools を使ってる人が多いと思います。

こんな感じで。

daemontools_service "tinydns-internal" do
  directory "/etc/djbdns/tinydns-internal"
  template false
  action [:enable,:start]
end

notification も送れます。

template '...' do
  ...
  notifies :restart, 'daemontools_service[tinydns-internal]'
end

自分もこれを使おうと思ったのですが、いくつか不満点がありました

  • serviceとdaemontools_serviceの両対応のレシピを書く場合、
    • service と daemontools_service とでほぼ同じ記述をしないといけない
    • notifies を送る側でも service か daemontools_service か意識しないといけない
  • action :stop で svc -p (SIGSTOP) している
    • なんで svc -d (SIGTERM) じゃないんでしょうか。。。
  • サービスのハンドリングだけしたいので、daemontools のインストールとかは別に要らない
    • 依存で ucspi-tcp もインストールされるが使ってないので要らない
    • RedHat系だと "daemontools" という名前のパッケージを入れようとするが、(内部のyum reposにある)"daemontools-toaster" を入れたい
      • attribute によるパッケージ名の変更はできない
  • run ファイルの生成機能は別に要らない(あっても使わなければいいだけだけど)


ので、service リソース (http://docs.opscode.com/resource_service.html) の provider として指定可能な Chef::Provider::Service::Daemontools を書いてみました。

gem install chef-provider-service-daemontools でインストールして、こんな感じで使えます。

require 'chef/provider/service/daemontools'

template '...' do
  ...
  notifies :restart, 'service[oreno-daemon]'
end

service 'oreno-daemon' do
  provider Chef::Provider::Service::Daemontools
  service_dir '/service'
  directory '/usr/oreno/daemon/oreno-daemon'
  supports :restart => true, :reload => true
  action [:enable, :start]
end
  • provider: 「provider Chef::Provider::Service::Daemontools」は必須です
  • service_dir: svscanが監視しているディレクトリです。この下にsymlinkが作られます。デフォルトは /service です
  • directory: symlinkが指すrunファイル等があるディレクトリです。デフォルトは /usr/oreno/daemon/#{service_name} です

notifies でも既存の service と同じように 「service[oreno-daemon]」 と指定できるのがミソです。

Special thanks!

daemontools.rb を書くにあたり、ルビーカの低い自分を [twitter:@niku4i] さんと [twitter:@sonots] さんに助けていただきました! あざっっっっっっっっっっっっっす!!!

github:eで管理しているChefのクックブックを、Berkshelf APIサーバーを立てていい感じに依存解決する方法

ちょっと前にリリースされた Berkshelf 3から、Berkshelf APIというインデックスサーバーからクックブックの情報を得るようになりました。

Berkshelf APIを使うと、外部に公開していないクックブックをBerksfileで指定する際の記述が簡潔になります。

まだ日が浅いせいか、Berkshelf APIgithub:e の情報が少ないので、備忘録も兼ねて残しておきます。

環境は以下のとおりです。

  • berkshelf (3.1.3)
  • berkshelf-api (2.0.0)

Berkshelf APIが必要な理由

Berksfile で cookbook キーワードに git: を添えることで、github:e からクックブックを持ってくることができます。

source 'https://api.berkshelf.com'

cookbook 'oreno-apache', git: 'git@github.oreno:cookbooks/oreno-apache.git'

さて、クックブック oreno-apache が oreno-iptables に依存している(oreno-apache/metadata.rbで depends 'oreno-iptables' している)とします。

この状態で berks を実行すると、oreno-iptables が見つけられなくてエラーになります。

$ berks vendor ./cookbooks
Resolving cookbook dependencies...
Fetching 'oreno-apache' from git@github.oreno:cookbooks/oreno-apache.git (at master)
Fetching cookbook index from https://api.berkshelf.com...
Unable to satisfy constraints on package oreno-iptables, which does not exist, due to solution constraint (oreno-apache = 0.1.0). Solution constraints that may result in a constraint on oreno-iptables: [(oreno-apache = 0.1.0) -> (oreno-iptables >= 0.0.0)]
Missing artifacts: oreno-iptables
Demand that cannot be met: (oreno-apache = 0.1.0)
Unable to find a solution for demands: oreno-apache (0.1.0)

原因は、内部の github:e にしかない oreno-iptables の情報をBerkshelf API (https://api.berkshelf.com) から得たインデックス情報から探そうとして、当然そこには情報がないためです。

なので、以下のようにBerksfileに oreno-iptables の情報を書けば、うまくいきます。

source 'https://api.berkshelf.com'

cookbook 'oreno-apache', git: 'git@github.oreno:cookbooks/oreno-apache.git'
cookbook 'oreno-iptables, git: 'git@github.oreno:cookbooks/oreno-iptables.git'

しかしこれでは使いたいクックブックが依存しているクックブックまでいちいち意識してトップレベルのBerksfileに書かないといけません。ひとつやふたつならまだいいですが、増えてくると面倒このうえないでしょう。

そこで内部のクックブックの情報を返すBerkshelf APIを立てることで、このようなBerksfileを書けばよいようになります。

source 'http://berks-api.oreno:26200'
source 'https://api.berkshelf.com'

cookbook 'oreno-apache'

簡潔ですよね?

最初のsourceを参照してクックブックが見つからなかった場合は、次のsourceを参照するので、これで内部クックブックとコミュニティクックブックの両方の情報を取得できるようになるわけです。

github:e の構成

Berkshelf APIサーバーから参照する場合、github:e 上のクックブックの管理は以下のような構成になっている必要があります。

  • organizationの下にクックブックのレポジトリを置く
    • ユーザーの下ではダメです (少なくとも berkshelf-api 2.0の実装では)
    • クックブック以外のレポジトリがあっても無視されますが、クックブック専用のorganizationを用意した方がいいでしょう
  • metadata.rbのversionをgit tagする
    • 「version '1.2.3'」なら「v1.2.3」というタグをつける必要があります
    • タグがついていないとクックブックと認識されなくてハマります

metadata.rb の書き方

Berkshelf APIサーバーは、github:e から当該クックブックの metadata.rb の「内容」を得て、それをもって instance_eval で評価します。

したがって、metadata.rb に外部ファイルを参照するようなコードを書いていると、例外が発生してスキップされクックブックとみなされなくなってしまいます。

例えば knife cookbook create で作ったクックブックの metadata.rb には、

long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))

と書かれているので修正する必要がります。(berks cookbookで作ったものは大丈夫です)

自分は ../recipes/*.rb を開いて「include_recipe」しているクックブックを全部 depends するRubyコードを書いていたのでハマりました。

Berkshelf APIサーバーを上げる

berkshelf-api のインストール方法は https://github.com/berkshelf/berkshelf-api を参照してください。

github:e にあるクックブックの情報を返すために、次のような設定ファイルを作って、

{
  "endpoints": [
    {
      "type": "github",
      "options": {
        "organization": "cookbooks",
        "access_token": "YOUR_ACCESS_TOKEN",
        "api_endpoint": "https://github.oreno/api/v3",
        "web_endpoint": "https://github.oreno",
        "ssl_verify": true
      }
    }
  ]
}

起動します。

berks-api -c /path/to/config.json

うまくいけば、26200番ポートの /universe にアクセスすればクックブックの情報が返ってきます。

$ curl http://127.0.0.1:26200/universe | jq .
{
  "oreno-apache": {
    "0.1.2": {
      "location_path": "https://github.oreno/cookbooks/oreno-apache",
      "location_type": "github",
      "dependencies": {
        "oreno-iptables": ">= 0.0.0"
      },
...

berks-api はかなり短い周期で github:e にアクセスしているようで、push された情報はほぼすぐに反映されるようです。

berks の設定をする

これで適切な URL からクックブックをダウンロードできる情報は揃ったのですが、github:e から実際にダウンロードするには berks の設定が必要です。

~/.berkshelf/config.json に次のように書きます。

{
  "github": [
    {
      "access_token": "YOUR_ACCESS_TOKEN",
      "api_endpoint": "https://github.oreno/api/v3",
      "web_endpoint": "https://github.oreno",
      "ssl_verify": true
    }
  ]
}

これで berks vendor で自家製とコミュニティの両方のクックブックが取得できるようになったはずです!