電話番号をうまく扱うライブラリ(libphonenumber)の活用方法を考えてみる

1. 背景

社内で共有された以下の資料で、Googleが作成しているlibphonenumberという電話番号のバリデーションができるライブラリが紹介されていました。
自分が担当しているサービスは電話番号の入力が必須であり何かしら使えそうだということで活用方法を考えてみました。

2. libphonenumberの機能

僕はRubyを使用しているのでmobi/telephone_numberでできることをまとめます。

無効な電話番号を判定する

tel = TelephoneNumber.parse("00000000000", :JP)
tel.valid?
#=> false

電話番号から市区町村を特定する

以下のようにすることで電話番号から市区町村を求めることができます。 フリーダイヤルや携帯電話など市区町村が特定できない場合はnilが返却されます。

tel = TelephoneNumber.parse("0864xx29xx", :JP)
tel.location
#=> "Kurashiki, Okayama"

電話番号の種別を判別する

下記コードのように、TelephoneNumber#valid_typesメソッドを使って電話番号の種別を判別できます。

tel = TelephoneNumber.parse("08051xx93xx", :JP)
tel.valid_types
#=> [:mobile]

判別可能な番号種別

valid_types 種別
:area_code_optional ※日本では使われていないよう
:fixed_line 固定電話
:mobile 携帯電話(070, 080, 090)
:no_international_dialling 国際電話をかけることができないフリーダイヤル(0037, 0066, 0077, 0088など00から始まるフリーダイヤル)
:pager ポケベルやM2M等専用番号(020)
:personal_number 個人向け携帯電話番号(060)
:premium_rate ダイヤルQ2(0990から始まる電話番号※現在は使われていない)
:shared_cost ※日本では使われていないよう
:toll_free フリーダイヤル(0120, 0800, 0070, 0077, 0088, 0037など)
:uan ナビダイヤル(0570)
:voicemail ※日本では使われていないよう
:voip IP電話(050)

固定電話・携帯電話・個人向け携帯電話以外に対してはSMSを送信できないため、 SMSを送信するようなサービスの場合、上記 判別可能な番号種別 をもとに送信対象を絞ることでSMS利用料の削減が期待できそうです。

Dockerをawsコマンド実行環境として使いS3からファイルをディレクトリごとダウンロードする

結論

AWSのS3から特定のディレクトリ以下をごっそりダウンロードするにはawscliを使う必要があります。
ただ、awscliを用意するのが面倒だったので、awsコマンド実行環境としてDockerを使用するワンライナーを書きました。

docker run -it --rm -v ~/docker/mnt:/mnt xueshanf/awscli /bin/bash -c 'export AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY" 
export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_ACCESS_KEY"
aws s3 cp --region ap-northeast-1 s3://bucket_name/dir_name/ /mnt --recursive'

上記ワンライナーの中の以下の環境変数に値を設定しバケットとディレクトリ名を指定してあげると、マウントした~/docker/mnt以下にファイルがダウンロードされます。

背景

なぜわざわざDockerを使っているかというと、awscliのセットアップにはPythonとpipのインストールが必要です。
しかしPythonの環境分離ツールには色々あり(pyenv, venv, virtualenvなど)、Python力が鼻くそ以下の僕にはどれを選べば良いかわからなかったので、環境を汚さないという理由でDockerを使うことにしました。

まとめ

これでローカルPCを汚さずにawsコマンドを使用できるようになりました。
Dockerは開発サーバーや本番サーバーの構築だけでなく、コマンド実行環境として使用できるのも素晴らしいですね。
以上です。

AWS LambdaでAWSの障害をslackにメンション付きで通知する

担当しているシステムのエラーの通知や障害の通知まわりで、その問題点と解決策を整理したのでメモしておきます。

背景

エラー通知があふれ見逃す危険があった

担当しているシステムではRailsやJSでExceptionが発生したときや、AWSでなんらかのイベントが発生したときにはSlackのエラー用チャンネルにその内容を通知しています。
しかし、RailsやJSのExceptionはそこそこの頻度で発生しているため、1日に何回もSlackに通知がきてしまいます。
その結果 エラーの通知に慣れてしまう という事態が発生し、重要なエラー(AWSインスタンスが落ちるなど)への対処が遅れることがしばしばありました。

解決の方向性

緊急度の高いエラーにすぐに気付ける状態を目指す

緊急度の高いエラーにすぐに気付ける状態を目標として 緊急度の高いエラーにのみメンション @channelをつける という方向で対応することにしました。

「Slackには即対応が必要なエラーしかこない」という世界にするためにRailsのExceptionやJS Errorを全て解決するぜ!ということができたら格好いいのですが、実際は「解決する工数が大きいわりに発生頻度と重要度(緊急度)は低い」エラーというものも多く、実現は難しいと判断しました。

既存の構成

既存の通知の仕組みは以下のようになっていました。

CloudWatch Alarm + SNSでメール送信 + Email Integrationでslack通知

処理の流れは以下のようになっています。

  1. SlackのEndPointでSNSをSubscribeする。
  2. CloudWatch AlarmからSNSにPublishしメール送信
  3. Email Integrationでslackに通知

これだと背景に記載した通り他のエラー内容に埋もれてしまうという問題がありました。

今回の構成

CloudWatch Alarm + SNS + Lambda

  1. LambdaでSNSをSubscribeする。
  2. CloudWatch AlarmからSNSにPublish。
  3. 通知を受けたLambdaでSlackのweb hookを叩く。

CloudWatch AlarmはAWSリソースのメトリクスを起動条件にすることができます。起動することができるターゲットは以下の3つ

  • SNS(Simple Notification Service)
  • Auto Scalingアクション
  • EC2アクション

直接Slackに通知することはできないため、SNS経由でLambdaを起動して通知するようにします。

他の案

CloudWatch Events + Lambda

CloudWatch Eventsは直接AWS Lambdaを起動できるため便利ですが、今回の用途には適していません。
AWSリソースの状態変化(=AWSAPIコール)をイベントとしているため、CPU UtilizationやMemory UsageなどのMetricsを通知ないからです。

itamaeのgitリソースでエラーが起きたからメモ

itamaeのgitリソースを実行したところ「deployブランチがすでに存在しているためcheckoutできませんでした」という旨のエラーが出ました。
その原因を調べたのでメモとして残しておきます。

エラーの原因

itamaeでgitリソースを実行するときに以下をともに満たしている場合、deploy branch already existとエラーが発生します。

  • checkoutしているブランチがdeployブランチ以外(masterとか)
  • deployブランチはすでに存在している

コードを見てみるとわかりますが、基本的にdeployブランチにcheckoutした状態で実行しないといけないようです。

gitリソースの挙動

ざっくりいうと以下の順でリモートのソースコードを同期するようです。

  1. deployブランチをdeploy-oldブランチにrename
  2. git fetch(このときrevisionオプションを渡していないと常に最新のソースコードを取得する)
  3. deployブランチを作成してdeployブランチにチェックアウト
  4. deploy-oldブランチを削除

deployブランチ以外のブランチにcheckoutした状態で実行すると、以下の分岐に入らずdeployブランチがrenameされません。
その結果上記エラーが発生してしまいます。

if current_branch == DEPLOY_BRANCH
  run_command_in_repo("git branch -m deploy-old")
  deploy_old_created = true
end

github.com

というわけで、deployブランチにcheckoutしたところ正しく動作しました。
以上、メモでした。

Kotlinのtraitとabstractの違い

Kotlinを勉強していたらtraitという機能がでてきて、「abstractと何が違うねん」と思ったのでその違いを調べました。
(至極当たり前の内容です)

結論

abstractは単一継承しかできないが、traitは多重継承(Mixin)できる。

Kotlinのtraitとは

Kotlinのtraitはabstractと似ていて、直接インスタンス生成できません。
そして、抽象メソッドや実装されたメソッドを持たせることができます。

多重継承

それなら「abstractと何が違うねん」ってなりますが、traitは多重継承できるという違いがあります。
多重継承はJavaではできず、interfaceを複数implementsする必要がありました。
その代わりJavaのinterfaceは実装を持っていなかったのでimplementsした複数のinterfaceに同一のメソッドが定義されていても問題がありませんでした。

一方で、Kotlinのtraitは実装を持つことができるので多重継承は危険なのでは?と思いましたが、
多重継承したときに実装されている同一シグネチャのメソッドが存在した場合コンパイルエラーになります。
おかげで安心、というわけです。

至極当たり前のことだと思いますが、Kotlinを触り始めて気になったので書きました!

pandas公式サイトのデータ構造のセクションを和訳してみました

pandasの使い方を調べるためにpandas公式サイトの英語のドキュメントを読みました。
英語の知識が大学受験時代で止まっている僕にとって英語のドキュメントを読むことは大変な苦行だったので、みなさんが同じ思いをしないように和訳をしてみました。
今回はpandasのデータ構造のセクションの途中までを和訳しています。(かなり意訳してるので、不自然なところや間違っているところがあればご指摘いただけるとうれしいです)

以下、和訳です。

データ構造入門

pandasの基本的なデータ構造の概要から始めましょう
データの型、index、ラベル付けについての基本的な振る舞いは全てのオブジェクトに対して適用されます。
では、始めるためにnumpyとpandasをimportしましょう

import numpy as np
import pandas as pd

ここで、 データの整列機能は備わっている という基本的な考え方を覚えておいてください。 ラベルとデータ間の関連はあなたが明示的に破壊しない限り、壊れることはありません。
これからデータ構造について手短に説明し、それから多様な機能とメソッドについて別々のセクションで説明します。

1. Series

Seriesはあらゆる型(整数値, 文字列, 浮動小数点, pythonオブジェクト等)を格納可能な一次元の配列です。
ラベルはindexとみなされます。
Seriesを生成する基本的なメソッドは次です。

s = pd.Series(data, index=index)

dataには以下のように異なるデータを入れることが可能です。

indexはラベルのリストです。
そして、dataに何が入るかによってindexは以下のように複数のケースにわけられます。

ndarrayから生成する場合

dataがndarrayの場合、indexはdataの長さと同じでなければなりません。
もしindexが渡されなかったら、[0, ..., len(data) - 1]の値をラベルとして持つSeriesが生成されます。

>>>s = pd.Series(np.random.randn(5), index=['a', 'b', 'c', 'd', 'e'])

>>>s
"""
a    0.2735
b    0.6052
c   -0.1692
d    1.8298
e    0.5432
dtype: float64
"""

>>>s.index
"""
Index([u'a', u'b', u'c', u'd', u'e'], dtype='object')
"""

>>>pd.Series(np.random.randn(5))
"""
0    0.3674
1   -0.8230
2   -1.0295
3   -1.0523
4   -0.8502
dtype: float64
"""
pythonのdictから生成する場合

dataがdictでindexが指定された場合、indexのラベルに合わせて一致するdataの値が使用されます。
そうでなければdictのキーをソートしてindexを構築します。

>>>d = {'a' : 0., 'b' : 1., 'c' : 2.}

>>>pd.Series(d)
"""
a    0.0
b    1.0
c    2.0
dtype: float64
"""

>>>pd.Series(d, index=['b', 'c', 'd', 'a'])
""" 
b    1.0
c    2.0
d    NaN
a    0.0
dtype: float64
"""
スカラー値から生成する場合

dataがスカラー値の場合、indexは必須です。 値はindexの長さに合うように繰り返されます。

>>>pd.Series(5., index=['a', 'b', 'c', 'd', 'e'])
""" 
a    5.0
b    5.0
c    5.0
d    5.0
e    5.0
dtype: float64
"""

Seriesはndarrayライク

Seriesはndarrayに非常に似た振る舞いをし、NumPyの関数のほとんどに対して有効な引数として利用できます。しかしながら、スライスしたものはindexもスライスします。

>>>s[0]
"""
0.27348116325673794
"""

>>>s[:3]
""" 
a    0.2735
b    0.6052
c   -0.1692
dtype: float64
"""

>>>s[s > s.median()]
""" 
b    0.6052
d    1.8298
dtype: float64
"""

>>>s[[4, 3, 1]]
""" 
e    0.5432
d    1.8298
b    0.6052
dtype: float64
"""

>>>np.exp(s)
""" 
a    1.3145
b    1.8317
c    0.8443
d    6.2327
e    1.7215
dtype: float64
"""

Seriesはdictライク

Seriesはindexに指定したラベルを使って値をセットしたり取得したりできる固定長のdictのようなものです。

>>>s['a']
"""
0.27348116325673794
"""

>>>s['e'] = 12.

>>>s
""" 
a     0.2735
b     0.6052
c    -0.1692
d     1.8298
e    12.0000
dtype: float64
"""

>>>'e' in s
"""
True
"""

>>>'f' in s
"""
False
"""

存在しないラベルを指定した場合、例外が投げられます

>>> s['f']
KeyError: 'f'

getメソッドを使用することで、存在しないラベルを指定した場合に何も返却しない or 指定したデフォルト値を返却するようになります。

>>>s.get('f')

>>>s.get('f', np.nan)
"""
nan
"""

Seriesのベクトル化とラベル付け

データ分析をするとき、NumPy配列が全要素に対して操作できるのと同様に、Seriesも値毎にループすることは通常必要ありません。
また、ndarrayが引数として渡されることを期待するNumPyメソッドのほとんどに、Seriesも渡すことができます。

>>>s + s
""" 
a     0.5470
b     1.2104
c    -0.3385
d     3.6596
e    24.0000
dtype: float64
"""

>>>s * 2
""" 
a     0.5470
b     1.2104
c    -0.3385
d     3.6596
e    24.0000
dtype: float64
"""

>>>np.exp(s)
""" 
a         1.3145
b         1.8317
c         0.8443
d         6.2327
e    162754.7914
dtype: float64
"""

Seriesとndarrayの決定的な違いは、Series同士の操作は自動的にラベルを基にデータを並べて適用されることです。
以下のように、Seriesが同じラベルを持つかどうか考慮することなく、計算処理を記述することができます。

>>>s[1:] + s[:-1]
""" 
a       NaN
b    1.2104
c   -0.3385
d    3.6596
e       NaN
dtype: float64
"""

並べ替えていないSeries同士の操作の結果は、indexを結合したものになります。 もし一方のSeriesにラベルが見つからなかった場合、結果はNaNになります。
明示的にデータを並べ替えずにコードをかけることは、対話型データ解析や調査に大きな自由と柔軟性をもたらします。
pandasのデータ構造が持つ統合されたデータ整列の機能は、ラベル付けされたデータへの操作を提供する関連ツールの大部分とは区別されます。

Name属性

SeriesはName属性を持つことができます。

>>>s = pd.Series(np.random.randn(5), name='something')

>>>s
""" 
0    1.5140
1   -1.2345
2    0.5666
3   -1.0184
4    0.1081
Name: something, dtype: float64
"""

>>>s.name
"""
'something'
"""

SeriesのName属性は多くの場合自動的に付与されます。
特にDataFrameの一次元を切り出した場合は、以下のようになります。 SeriesのName属性は、pandas.Series.rename()メソッドで変更することができます。

>>>s2 = s.rename("different")

>>>s2.name
"""
'different'
"""

2. DataFrame

DataFrameはカラム毎に異なる型を持つことができる二次元のラベル付けされたデータ構造です。
表計算ソフトやSQLのテーブル、Seriesオブジェクトをもつdictのようなものとみなせます。
一般的にもっとも使われるpandasのオブジェクトです。
Seriesのように、DataFrameも多くの異なる入力値を受け付けます。

  • 一次元のndarray, lists, dict, SeriesのDictオブジェクト
  • 二次元のndarrayオブジェクト
  • 構造化されたdtypeを要素に持つndarrayオブジェクト
  • Seriesオブジェクト
  • 他のDataFrameオブジェクト

データに加え、オプションでindex(行のラベル)とcolumns(列のラベル)を渡すことができます。
もしindexとcolumnsのいずれか、もしくは両方を渡した場合、生成したDataFrameのそれらを自身で決定することができます。
以下のように、Seriesと指定してindexのdictオブジェクトはindexに一致しない全てのデータを破棄します。
もし軸のラベルが渡されなかった場合、共通のルールに基いたデータから構築されます。

Series、もしくはdictを持つdictから生成する場合

生成結果のindexはDataFrameに含まれるSeriesのindexを結合したものになります。
もしネストしたdictがあった場合、最初にSeriesに変換されます。
もしcolumnsが渡されなかった場合、columnsはdictのキーをソートしたものになります。

>>>d = {'one' : pd.Series([1., 2., 3.], index=['a', 'b', 'c']), 'two' : pd.Series([1., 2., 3., 4.], index=['a', 'b', 'c', 'd'])}

>>>df = pd.DataFrame(d)

>>>df
""" 
   one  two
a  1.0  1.0
b  2.0  2.0
c  3.0  3.0
d  NaN  4.0
"""

>>>pd.DataFrame(d, index=['d', 'b', 'a'])
""" 
   one  two
d  NaN  4.0
b  2.0  2.0
a  1.0  1.0
"""

>>>pd.DataFrame(d, index=['d', 'b', 'a'], columns=['two', 'three'])
""" 
   two three
d  4.0   NaN
b  2.0   NaN
a  1.0   NaN
"""

行と列のラベルは、それぞれindex属性とcolumns属性にアクセスすることで参照できます。

>>>df.index
"""
Index([u'a', u'b', u'c', u'd'], dtype='object')
"""

>>>df.columns
"""
Index([u'one', u'two'], dtype='object')
"""

ndarray、もしくはlistを持つdictから生成する場合★

ndarrayはすべて同じ長さでなければなりません。indexを渡す場合も、明らかに配列と同じ長さである必要があるます。
indexが渡されない場合、結果は配列の長さの範囲内になります。

>>>d = {'one' : [1., 2., 3., 4.], 'two' : [4., 3., 2., 1.]}

>>>pd.DataFrame(d)
""" 
   one  two
0  1.0  4.0
1  2.0  3.0
2  3.0  2.0
3  4.0  1.0
"""

>>>pd.DataFrame(d, index=['a', 'b', 'c', 'd'])
"""
   one  two
a  1.0  4.0
b  2.0  3.0
c  3.0  2.0
d  4.0  1.0
"""

構造化されたdtypeを要素に持つ配列から生成する場合

この場合は配列のdictと全く同様に扱われます。

>>>data = np.zeros((2,), dtype=[('A', 'i4'),('B', 'f4'),('C', 'a10')])

>>>data[:] = [(1,2.,'Hello'), (2,3.,"World")]

>>>pd.DataFrame(data)
""" 
   A    B      C
0  1  2.0  Hello
1  2  3.0  World
"""

>>>pd.DataFrame(data, index=['first', 'second'])
"""
        A    B      C
first   1  2.0  Hello
second  2  3.0  World
"""

>>>pd.DataFrame(data, columns=['C', 'A', 'B'])
"""
       C  A    B
0  Hello  1  2.0
1  World  2  3.0
"""

dictのリストから生成する場合

>>>data2 = [{'a': 1, 'b': 2}, {'a': 5, 'b': 10, 'c': 20}]

>>>pd.DataFrame(data2)
"""
   a   b     c
0  1   2   NaN
1  5  10  20.0
"""

>>>pd.DataFrame(data2, index=['first', 'second'])
"""
        a   b     c
first   1   2   NaN
second  5  10  20.0
"""

>>>pd.DataFrame(data2, columns=['a', 'b'])
"""
   a   b
0  1   2
1  5  10
"""

tupleのdictから生成する場合

tupleのディクショナリを渡すことで、自動的に複数のindexを持つframeを生成することができます。

>>>pd.DataFrame({('a', 'b'): {('A', 'B'): 1, ('A', 'C'): 2},
...('a', 'a'): {('A', 'C'): 3, ('A', 'B'): 4},
...('a', 'c'): {('A', 'B'): 5, ('A', 'C'): 6},
...('b', 'a'): {('A', 'C'): 7, ('A', 'B'): 8},
...('b', 'b'): {('A', 'D'): 9, ('A', 'B'): 10}})

"""
       a              b      
       a    b    c    a     b
A B  4.0  1.0  5.0  8.0  10.0
  C  3.0  2.0  6.0  7.0   NaN
  D  NaN  NaN  NaN  NaN   9.0
"""

Seriesから生成する場合

入力値に使用したSeriesのindexと同じindexを持ち、Seriesの元の名前と同じ名前を持つカラムを持ったDataFrameが生成される(ただし、他にカラム名が提供されていない場合に限る)

Missing Data

Much more will be said on this topic in the Missing data section. To construct a DataFrame with missing data, use np.nan for those values which are missing. Alternatively, you may pass a numpy.MaskedArray as the data argument to the DataFrame constructor, and its masked entries will be considered missing.

選択可能なコンストラクタ

DataFrame.from_dict

DataFrame.from_dictは、dictのdictもしくは配列のようなシーケンスなデータ構造のdictを引数にとり、DataFrameを返却します。 これは方向に関するパラメータ以外DataFrameのコンストラクタの用に振る舞います。方向に関するパラメータはデフォルトではcolumnsになりますが、indexに設定してdictのキーを行のラベルとして使うことができます。

DataFrame.from_records

DataFrame.from_recordsはtupleのリスト、もしくは構造化されたdtypeを型に持つndarrayを引数にとります。
このメソッドは普通のDataFrameのコンストラクタと同じように働きます。ただし、dtype内の指定したフィールドの値をindexにできる点が異なります。 例は以下:

>>>data
"""
array([(1, 2.0, 'Hello'), (2, 3.0, 'World')], 
      dtype=[('A', '<i4'), ('B', '<f4'), ('C', 'S10')])
"""

>>>pd.DataFrame.from_records(data, index='C')
"""
       A    B
C            
Hello  1  2.0
World  2  3.0
"""
DataFrame.from_items

DataFrame.from_itemsはキーバリューのペアの集まりを引数にとるdictのコンストラクタと同様に働き、キーは列(orient='index'のように指定した場合は行)の名前になり、値は列の値(もしくは行の値)になります。
これは明示的に列のリストを渡すことなく、特定の順序のカラムを持ったDataFrameを構築するのに便利です。

>>>pd.DataFrame.from_items([('A', [1, 2, 3]), ('B', [4, 5, 6])])
"""
   A  B
0  1  4
1  2  5
2  3  6
"""

orient='index'を渡した場合、キーは行のラベルになります。
しかし、この場合は希望する列名も渡さなければなりません。

>>>pd.DataFrame.from_items([('A', [1, 2, 3]), ('B', [4, 5, 6])],
...orient='index', columns=['one', 'two', 'three'])
 
"""
   one  two  three
A    1    2      3
B    4    5      6
"""

列の追加、削除

DataFrameは意味的にSeriesオブジェクトのdictのように扱うことができます。列の取得・設定・削除はdictの操作と同様の文法で動きます。

>>>df['one']
"""
a    1.0
b    2.0
c    3.0
d    NaN
Name: one, dtype: float64
"""

>>>df['three'] = df['one'] * df['two']

>>>df['flag'] = df['one'] > 2

>>>df
"""
   one  two  three   flag
a  1.0  1.0    1.0  False
b  2.0  2.0    4.0  False
c  3.0  3.0    9.0   True
d  NaN  4.0    NaN  False
"""

列はdictのように削除・ポップすることが可能です。

>>>del df['two']

>>>three = df.pop('three')

>>>df
"""
   one   flag
a  1.0  False
b  2.0  False
c  3.0   True
d  NaN  False
"""

スカラー値を挿入するときは、挿入した列全体がそれで満たされます。

>>>df['foo'] = 'bar'

>>>df
"""
   one   flag  foo
a  1.0  False  bar
b  2.0  False  bar
c  3.0   True  bar
d  NaN  False  bar
"""

DataFrameと同じだけのindexを持っていないSeriesを挿入するときは、DataFrameのindexに従います。

>>>df['one_trunc'] = df['one'][:2]

>>>df
"""
   one   flag  foo  one_trunc
a  1.0  False  bar        1.0
b  2.0  False  bar        2.0
c  3.0   True  bar        NaN
d  NaN  False  bar        NaN
"""

素のndarrayを挿入することができますが、それらの長さはDataFrameのindexの長さと一致していなければなりません。
デフォルトでは、列は最後に挿入されます。insert()メソッドを使って列の特定の場所に挿入することができます。

>>>df.insert(1, 'bar', df['one']) #1列目にbar列として、df['one']を挿入

>>>df
"""
   one  bar   flag  foo  one_trunc
a  1.0  1.0  False  bar        1.0
b  2.0  2.0  False  bar        2.0
c  3.0  3.0   True  bar        NaN
d  NaN  NaN  False  bar        NaN
"""

途中ですが、今回はここまで。
続きは別の機会に和訳していきたいと思います!

nginxがunicornに接続できなくなったのでログローテーション設定の見直しをした。

発生したnginxのエラー

プライベートで運営しているメディアのnginxで
以下のエラーが発生し、unicornに接続できなくなりました。

[error] 13645#0: *624 connect() to unix:/tmp/unicorn.sock failed (111: Connection refused) while connecting to upstream

原因はRailsアプリのログローテーション設定の不備

Railsアプリのログのローテート設定に問題があったことにより、
ログのサイズが肥大化し、EBSのディスク使用率が100%になっていました。

当時のログローテーションの設定

/etc/logrotate.d/railsの設定を確認したところ、以下のようになっていました。
RailsのLoggerではローテートの設定をしていません)

# /etc/logrotate.d/rails

# ローテーション間隔: 1週間
weekly 

# 保持世代数: 4
rotate 4

# ローテートしたログは圧縮する
compress

# 必要な箇所だけ抜粋

この設定で問題だったのは、ローテーション間隔が1週間と長すぎたせいで
ローテートする直前のログファイル(1週間分)が肥大化しすぎていたことでした。
ログファイルのgzip圧縮後のサイズが250MB程度だったのに対し、
ローテートされる直前の圧縮されていないログファイルのサイズは4GBを超えていました。

ディスク使用率の推移イメージ(ローテート設定変更前)

f:id:manchose:20161002202620j:plain

上の図はディスク使用率の推移イメージですが、
このように圧縮前のログファイルが大きくなりすぎてオレンジ色の点でディスク使用率が100%になってしまったのです。

これだとdisk使用率の増減が大きすぎるため、ログローテートの設定を変更しました

変更後のログローテーションの設定

以下のようにローテーション設定を変更しました。

  • ローテート間隔を1日にした
  • 保持世代数を14にした
# /etc/logrotate.d/rails

# ローテーション間隔: 1日
daily

# 保持世代数: 14
rotate 14

# ローテートしたログは圧縮する
compress

# 必要な箇所だけ抜粋

これによりローテート直前の未圧縮状態のログファイルサイズが500MB程度に収まり、disk使用率の増減が少なくなったため、
下のグラフのようにdiskの空き容量に常に余裕ができるようになりました。

ディスク使用率の推移イメージ(ローテート設定変更後)

f:id:manchose:20161002203654j:plain

また、同時に4週間保持していたログファイルを2週間保持に変更しました。 今回はRailsアプリのログが問題になりましたが、nginxなどの他のミドルウェアでも同様の問題が起こりうるので一度見直そうと思います。

今回は以上です!