Akata Works

東京エンジニア。主にRuby,Go,たまにAWSとiOS。ゲーム音楽が好きです。連絡はTwitterかakata.onen@gmail.comまで

collectdとstatsd-graphite-grafanaのDockerイメージで監視体制を作る

Lightsailがかなりお得になったので、何か作ろうかなと思っているんですが、どんなサーバを可動させようにも「まずは監視だな」ということでこんな記事を書いてみました。

サーバーはamazon-linux-2018.03.0を想定しています。

今回はメトリクスの収集はアプリケーション用にstatsdとシステム用にcollectdを使います。他はデータストアにgraphiteをグラフ化にgrafanaをといった構成です。
まとまっているDocker Imageがあって楽だったというのが一番の理由ですかね......(笑)

statsdも入れてますが、今回の記事にstatsdの使い方等は一切書いておりません。アプリケーションよりも先にシステムのメトリクスだと思うので。

Install and Setup

statsd-graphite-grafanaの環境は前述のDocker Imageがあるのでこれで構成してしまい、collectdのみパッケージをインストールします。collectdもDocker Imageがあるので使えばいいんですが、めんどくさいので今回はスルーしました。

git clone git@github.com:kamon-io/docker-grafana-graphite.git
cd docker-grafana-graphite
docker-compose up -d

collectdのインストールにはEPELリポジトリが必要なので同時に入れてしまいましょう。また、一部のプラグインはcollectdに同封されていないため、必要であれば適宜インストールしてください。diskすら入っていません。

sudo yum install -y epel-release collectd
# Add plugins
sudo yum install -y collectd-disk.x86_64

プラグインが入っていないと、/var/log/messagesに以下のようなログが流れます。

plugin_load: Could not find plugin "disk" in /usr/lib64/collectd

次に/etc/collectd.confを編集します。write_graphiteプラグインと、追加したプラグインの記述をコメントアウトして、後は好みで設定してください。
詳しいプラグインの設定方法は以下やGitHubに記述されています。

Table of Plugins - collectd Wiki

Docker Imageでaggregater用のstatsdがすでに起動しているので、モニターサーバーのシステムメトリックスはgraphiteに直接投げていますが、collectdのstatsdプラグイン使っても問題ないと思います。

サービスを起動して、/var/log/messagesを確認しましょう。うまく設定できていなければ、エラーが出たり、メトリクスが取れなかったりします。

sudo service collectd start

問題なければ、後はGrafanaでをグラフを作成すれば完成です。

f:id:akatakun:20181119233400p:plain

今回はモニターサーバの準備と、モニターサーバのシステムメトリックスにフォーカスしました。アプリケーションサーバを追加したら、同じようにstatsdやcollectdをインストールして、モニターサーバにデータを送信すればいいと思います。

少し走り書きしてしまったので、間違っている点や分からない点がありましたらコメント等いただけると幸いです。

参考

RubyのCSI多言語化について調べたらpack, unpackについて理解できた話

Rubyでバイト列の先頭にマジックナンバーが付いたようなものを扱う機会があったのでArray#pack, String#unpackについて調べてみました。
このあたりの認識がふわっとしてると、使うときに障害の種になるので要注意ですね。

結構調べたつもりですが、間違えていたらスミマセン。

Rubyの多言語化手法はCSI方式

まず第一に、Rubyの多言語化手法がCSI方式であるという点について知っておかないと、pack, unpackがふわっとします(僕はしました)

多くの言語では、文字集合文字コードを統一するUCS方式という多言語化手法が使われており、例えばPerlだと文字集合Unicode文字コードUTF-8として統一しています。なので、外から入ってくるバイト列は必ずUTF-8エンコードし、外に出すときにまたエンコードするといった感じです。

対してRubyのようなCSI方式では、文字列オブジェクト自体が自らの文字コード情報を持っています。つまり、文字列オブジェクトが持つ主な情報はバイト列文字コードです。

> puts "".encoding
UTF-8
> puts "".bytes
227 # 0xE3
129 # 0x81
130 # 0x82

Ruby 2.0から標準内部エンコーディングUTF-8なので文字コードは標準ではUTF-8UTF-8における"あ"は 16進数ではE3 81 82なので上記のような結果になります。

CSI方式では言語仕様の文字集合文字コードに依存しないなど、それぞれの方式には得手不得手があるのですが、ここでは省略します。詳しくは => Ruby M17N の設計と実装

String#unpack

本題になりますが、String#unpackについてで、これは文字列オブジェクトが持つバイト列情報をテンプレート文字列(第一引数)に従って変換するメソッドです。出力結果は配列で返ってきますが、テンプレート文字列によっては一要素しか含みません。

テンプレート文字列は多いので端折ります => pack テンプレート文字列 (Ruby 2.5.0)

> puts "".unpack("H*") # H: 上位ニブルが先の16進数表現
e38182
> puts "".unpack("C*") # H: 8bit符号なし整数表現
227 # 0xE3
129 # 0x81
130 # 0x82

UTF-8における"あ"E3 81 82でしたね。

Array#pack

続いて、Array#packについてです。これは逆にString#unpackメソッドとテンプレート文字列で表現可能な配列を文字列オブジェクトに戻せるメソッドです。

本来、packの説明をベースにunpackはそれを戻すものみたいに言われてますが、個人的には文字列のほうが直感的かと思ったので、先にunpackを説明して、packはそれを戻すもとと解釈したほうが分かりやすかったです。

> p ["e38182"].pack("H*")
"\xE3\x81\x82"
> p [227, 129, 130].pack("C*")
"\xE3\x81\x82"

ここで「話が違うぞ!!」と慌てる前に、まずは文字コードを確認するといいです。

> puts ["e38182"].pack("H*").encoding
ASCII-8BIT
> puts [227, 129, 130].pack("C*").encoding
ASCII-8BIT

Ruby 2.0以降の標準文字コードUTF-8でしたが、packの返却値はどうやらASCII-8BITみたいですね。同じバイト列でも、文字コードが違うだけで見え方が変わっています。

ちなみに、IOオブジェクトの標準外部エンコーディングはいい感じに設定されているので、上のコードはあえてpで出力してますが、普通にputsで出力すると、自分でUTF-8に戻さなくても出力結果はUTF-8(環境によりけり)になります。

> p ["e38182"].pack("H*").force_encoding("UTF-8")
""
> p [227, 129, 130].pack("C*").force_encoding("UTF-8")
""

うまく戻りましたね。やっぱり物事を理解するには深掘りするに限る。

参考

NodeJSとnodejsコマンド・nodeコマンドの違い

前々からちょこちょこ気になってたことだったので調べてみた。

RubyイメージのDockerコンテナ内とかUbuntuでNodeJSを使おうと思って調べると、すぐにapt-get install nodejsに辿り着くんですが、このコマンドでインストールされたバイナリはnodeじゃなくてnodejsになるのでいろんな所でちょっとメンドイ。例えばwebpacker実行したら/usr/bin/env: ‘node’: No such file or directoryとなる。

ということで調べてみたところ、こちらもすぐに辿り着いた。

There is a naming conflict with the node package (Amateur Packet Radio Node Program), and the nodejs binary has been renamed from node to nodejs. You'll need to symlink /usr/bin/node to /usr/bin/nodejs or you could uninstall the Amateur Packet Radio Node Program to avoid that conflict.

要するに単に名前が被ってたからnode使えなかったってことみたい。iPhoneのカタカナ表記が「アイフォーン」になってるノリですね。

手元にあったUbuntu 16.04.1 LTSサーバにもRubyイメージにもnodeコマンドは入ってなかったので単純にリンクを通せば大丈夫です。

sudo ln -s `which nodejs` /usr/bin/node

備考

Dockerならrubyとnodeでコンテナ分けたら解決するし、責務も明確になるしその方がいいかも。
webpacker使うならrailsに依存するから、コンテナ分けずにmulti-stage buildした方がいいかも。

参考

DockerHubにあるイメージのタグリストを出力するスクリプト

DockerHubにあるイメージのタグを検索するだけのスクリプト。docker searchだとそこまで分からないから便利。

curl -s -S "https://registry.hub.docker.com/v1/repositories/#{image_name}/tags" | jq '.[]["name"]'

jqコマンドに依存するので、便利だからこの際入れよう。

関数化してbashrc(zshrc)に追加していればもっと便利。

function docker-search-tags() {
  curl -s -S "https://registry.hub.docker.com/v1/repositories/$1/tags" | jq '.[]["name"]'
}
$ docker-search-tags ruby | head
"latest"
"1"
"1-onbuild"
"1-slim"
"1-wheezy"
"1.9"
"1.9-onbuild"
"1.9-slim"
"1.9-wheezy"
"1.9.3"

短いけど終わり。

スクリプトからgsutilを使用するときの認証などのポイント

はじめに

RubyからGoogle Cloud StorageのAPIを叩くにはgoogle-cloud-storage gemを使うのが一般的なので、この記事はどうしてもスクリプトからgsutilを実行したい人向けです。

今回はGoogle Cloud SDKの一部としてgsutilをインストールした前提で書いていますが、スタンドアローンのgsutilを使用している方も同様に処理できると思います。

gsutilのインストールがまだな人はここからどうぞ!(Pythonバージョンには注意ね)

認証

gcloudだと別々の認証情報を使ったプロセスが並列したときに問題になりますし、.botoファイルだと細かい制御が難しいのでトップレベルの-oオプションを使うといいです。

-oオプション使用することで.botoで設定できるような項目をコマンド単位で上書きできます。

gsutil -o 'Credentials:gs_service_key_file=etc/credential.json' cp gs://hoge.huga/foo/bar .

その他

並列処理させる場合は、-mオプションに加えて、GSUtil:parallel_process_count,GSUtil:parallel_thread_countを上書きしてください。

gsutil -m -o 'Credentials:gs_service_key_file=etc/credential.json' -o 'GSUtil:parallel_process_count=8' -o 'GSUtil:parallel_thread_count=8' cp gs://hoge.huga/foo/bar .

-qオプションを指定することでアップロード中などのプログレスなどを非表示にできます。crontabなどで実行するときはこれも付けるといいです。

gsutil -q -o 'Credentials:gs_service_key_file=etc/credential.json' cp gs://hoge.huga/foo/bar .

探せばいろいろありますがgcsはまだまだawsに比べて日本語情報が少ないですね。

参考

Top-Level Command-Line Options  |  Cloud Storage Documentation  |  Google Cloud

Railsでオンメモリにキャッシュするとハマるので注意

Railsのフラグメントキャッシュでオンメモリを指定してたら、外部ミドルウェアを使うのと挙動が違ってハマったのでメモ。
開発環境デフォルトのオンメモリは準備が楽ですが、本番でDalliとか使ってると混乱するので統一しておきたいですね。

検証

恐らく最もよく使われると思われるRails.cache.fetchの結果をそれぞれ見てみます。

Dalli(Memcached)

設定値config.cache_store = :mem_cache_store

[1] pry(main)> @tmp = Rails.cache.fetch('hoge'){puts 'no cache'; {hoge: 'huga'}}
Dalli::Server#connect localhost:11211
no cache
=> {:hoge=>"huga"}
[2] pry(main)> @tmp[:foo] = 'bar'
=> "bar"
[3] pry(main)> @tmp = Rails.cache.fetch('hoge'){puts 'no cache'; {hoge: 'huga'}}
=> {:hoge=>"huga"}
[4] pry(main)> @tmp[:foo] = 'bar'
=> "bar"
[5] pry(main)> @tmp = Rails.cache.fetch('hoge'){puts 'no cache'; {hoge: 'huga'}}
=> {:hoge=>"huga"}

on Memory

設定値config.cache_store = :memory_store

[1] pry(main)> @tmp = Rails.cache.fetch('hoge'){puts 'no cache'; {hoge: 'huga'}}
no cache
=> {:hoge=>"huga"}
[2] pry(main)> @tmp[:foo] = 'bar'
=> "bar"
[3] pry(main)> @tmp = Rails.cache.fetch('hoge'){puts 'no cache'; {hoge: 'huga'}}
=> {:hoge=>"huga"}
[4] pry(main)> @tmp[:foo] = 'bar'
=> "bar"
[5] pry(main)> @tmp = Rails.cache.fetch('hoge'){puts 'no cache'; {hoge: 'huga'}}
=> {:hoge=>"huga", :foo=>"bar"}

このようにオンメモリではキャッシュされた値に、その後の変更が適応されています。

原因

恐らく、オンメモリだとrailsプロセスと同一のメモリ空間に保存されるため、ポインタのその後の変更も適応されているんだと思います。三回目の呼び出しまで適応されない理由はわかりませんが......(ドキュメントとかコードちゃんと読んだ人教えて!)

結論

あんまりオンメモリストア使うもんじゃないね。

Deployment TargetとBase SDKと互換性について

XcodeのDeployment TargetとBase SDKがややこしかったのでまとめてみました。なるべくわかりやすく書いたつもりです。「間違っているぞー」とい点があればご指摘いただけると幸いです。

前提

そもそもの話として、Appleは各種OS(Mac OSiOS)とそのバージョンごとに異なるSDKを用意しており、それぞれの端末には対応するSDKが事前に組み込まれています。これにより以下のようなメリットがあります。

  • 同一実行ファイルでありながら、機能が組み込まれていればそれを活用し、組み込まれていくてもそれなりに対処できる
  • Base SDKからある程度の上位・下位バージョンへの互換性が担保できる

で、この「組み込まれているか、組み込まれていないか」を判断する必要があるSDKの範囲を決めるのがDeployment TargetとBase SDKです。

Deployment Target(配布ターゲット)

開発者がソフトウェアの動作を保証する最も古いSDKのバージョンを表します。つまり、これ以上のSDK上では動作を保証する必要があるけど、これより古いSDK上ではクラッシュしても知りませんってことです。

動作を保証する必要がないため、これ以下のバージョンで実装された機能にはがっつりと強リンク(機能がなければクラッシュする)が張られます。

Base SDK

ソフトウェアのコンパイル時に基準とするSDKのバージョンを表します。つまり、これより新しいSDK上で定義された機能は使えません(コンパイルエラー)。

また、このバージョンで廃止予定とされている機能はこれ以降のバージョンでは存在しない可能性があるためさっさと移行したほうがいいです。多分警告が出ると思います。

廃止予定の機能さえ使用していなければ上位互換性はほぼ保証されます。なぜなら、コンパイル時により新しいSDKで定義された機能は使ってないし、廃止予定でない機能が急に消されることはない(はず)だからです。

Deployment Target〜Base SDK

前項までで、Deployment Target以前の機能は気にする必要がないので無視して問題がないこと、Base SDK以上の機能は廃止予定のものを使っていなければほぼ安全であるということがわかりました。

ただ、これらのバージョン間で実装(変更)された機能に関してはユーザの端末に定義されているとは限りません。メソッドが追加されたり、挙動が変更されたりといろいろ考えられます。

故に、これらの機能にはソースコード上で動的に変更が可能なように弱リンク(機能がなければヌルポになる)が張られ、機能が定義されているか事前に確認する必要があります。

具体的な実装方法に関しては省きますが、参考URLにあるAppleの公式ドキュメントが一番まとまっていると思います。

まとめ

まとめると、このあたりをきちんと守っていれば互換性に関しては問題ないと思います。

f:id:akatakun:20180614194429p:plain (参考URL: Appleの公式ドキュメントから)

  • Deployment Target以前: 動作を保証をしない
  • Deployment Target以上〜Base SDK以下: 動作を保証する。追加・変更された機能はフレームワーク読み込み設定やソースコードで順応させる必要がある
  • Base SDK以降: 動作を保証する。廃止予定の機能は極力使わない

おわり。

参考URL

https://developer.apple.com/jp/documentation/cross_development.pdf