Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

2016年01月

CORECを支える技術 ~ログ監視編~ fluentd-AmazonSNS連携

こんにちは。なべです。
CORECの開発を担当しています。
CORECを支える技術 ~チケット駆動編~』に続き、『CORECを支える技術』シリーズの第2弾としてCORECのログ監視について書きたいと思います。

旧ログ監視の問題と対策

当初、CORECのログ監視はログ集約にくっつける形で始まりました。
ログ集約はログファイルを一定時間ごとにバックアップする仕組みで、各サーバーのログをfluentdで集め、1時間ごとに圧縮しS3に格納しています。
ログ監視は、その集約されたログを社内の監視システムで取得し、エラーログがあった場合にメールとIRCで通知するというものです。

この仕組には大きく以下の2つの問題がありました。

  • タイムラグ
    ログの監視をできるタイミングがS3に圧縮ファイルがアップされた後なので、検知するまで1時間のタイムラグが発生する可能性がある。
  • サービスレベル
    ログ監視を社内のサーバーに配置していて、消防点検や停電または、サーバーメンテナンス時に監視できないなどサービスレベルにも問題がある。

これらの問題に対応するためfluentdに集めたタイミングでエラーログを抽出し、AmazonSNS経由でメール通知することにしました。
これにより、社内のサーバーも経由しなくなり、タイムラグの問題・サービスレベルの問題を同時に解消できました。

CORECログ監視概要図

実施内容

この対応で実施したことは以下の4つです

  1. fluentdにプラグイン追加
  2. AmazonSNSに通知用Topic作成
  3. メールBody整形用のERB追加
  4. fluentdの設定ファイル記述

1. fluentdにプラグイン追加

以下の2つのプラグインを追加しました。

fluent-plugin-sns
Amazon SNS通知用
fluent-plugin-rewrite-tag-filter
エラーのフィルタ・レベルごとの分岐処理用
fluentdはrubyで動いているのでRubyistならお馴染みのgemコマンドでインストールできます。
この時fluentdが使っているrubyの環境を指定する必要があります。
特にfluentdにバンドルされたrubyを使用している場合は注意してください。
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-sns
sudo /usr/lib/fluent/ruby/bin/gem install fluent-plugin-rewrite-tag-filter

2. AmazonSNSに通知用Topic作成

Amazonの管理コンソールからSNSサービスを選択し、画面に従ってTopic及びSubscritionを作成します。 特に迷うことはないかと思いますが、注意するポイントは以下の2点です。

  • メールアドレスを登録するとメールがくるのでクリックして認証すること
  • 作成したTopicの権限設定を確認する
    権限設定とは作成したTopicに対して、通知に利用するAWSアカウントがこのトピックに投稿する権限のことです。もし権限がない場合には、[ポリシーの編集]から許可を与えてください。

3. メールBody整形用のERB追加

CORECの場合、fluentdが受け取るレコードは改行が\nにエスケープされていて読みづらいため、テンプレートで改行コードに変換しました。
以下のコードをerbファイルとして保存しておきます。
これだけのコードですが受け取った側の負担がだいぶ違います。特にスタックトレースで威力を発揮します。

<%record.each do |k,v|%>
<%="#{k}:#{v.to_s.gsub(/\\n/,"\n")}" %>
<%end%>

4. fluentdの設定ファイル記述

それでは最後にメインとなるfluentdの設定ファイルの編集をします。
変更/追加のポイントは以下の3点です。

4-a. copyプラグインで処理を2系統に分岐する
4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する
4-c. snsプラグインで通知するためのmatchディレクティブを追加する

それぞれ簡単に説明します。

4-a. copyプラグインで処理を2系統に分岐する

fluentdは最初にマッチしたmatchディレクティブだけを実行しますので、同じレコードに対して複数の処理をしたい場合にはcopyプラグインを使用します。
※copyプラグインは標準のプラグインなのでインストールをする必要はありません。

matchディレクティブと同等の内容をstoreタグに書くことができ、同じレコードに対して処理を複数記述できるようになります。
今回の対応では従来のS3への送信する処理をcopyで分岐し、SNSに送信する処理を追加します。

<match tag.log>
  type copy
  <store>
        従来の処理
  </store>
  <store>
        追加したい処理
  </store>
</match>

4-b. rewrite_tag_filterプラグインでエラーレベルごとにフィルタリングして、新しいタグを設定する

ただ単に前項の「追加したい処理」にSNS送信のディレクティブの内容を書いてしまうと、アプリケーションが出力したログすべて(エラーでないものも含めて)通知されてしまいます。
そこでログの内容によって処理を振り分けるにはrewrite_tag_filterプラグインを使用します。
レコードのキーを指定して、検索する文字列または正規表現を指定し、それにマッチした場合に新しいタグを指定する仕組みです。
今回はログレベルがFATALもしくはERRORの場合にシステム通知がしたかったので、ログレベルが格納されているキー(lebel)に対して、FATAL・ERRORそれぞれルールを作成しました。
複数のルールを設定するにはrewriteruleの後の数字をインクリメントして記述します。

  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>

4-c. snsプラグインで通知するためのmatchディレクティブを追加する

typeにsnsを指定して、内容にはAWSユーザーにはお馴染みのパラメータ群を設定していきます。 sns_body_templateのパスはtd-agentが参照可能な場所にERBを配置して指定してください。

<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>

fluentd設定ファイルイメージ(全体)

変更前後の設定ファイルは以下のとおりです。
行はだいぶ増えていますが、それぞれの設定内容に難しいところはありませんでした。

  • 設定ファイル:修正前
<match corec.rails>
    S3に送信する設定(省略)
</match>
  • 設定ファイル:修正後
<match corec.rails>
  type copy
  <store>
        従来のS3に送信する設定(省略)
  </store>
  <store>
    type rewrite_tag_filter
    rewriterule1 level FATAL corec.rails.sns_fatal
    rewriterule2 level ERROR corec.rails.sns_error
  </store>
</match>
<match corec.rails.sns_fatal>
  type sns
  sns_topic_name for_system_notice_topic
  aws_key_id xxxxxxxxxx
  aws_sec_key xxxxxxxxxx
  sns_endpoint sns.ap-northeast-1.amazonaws.com
  sns_subject [COREC ALERT]corec/rails
  sns_body_template /etc/td-agent/erb/sns_body_template.erb
</match>
<match corec.rails.sns_error>
 (省略)
</match>

設定ファイル修正後、fluentdを再起動すれば反映されます。
※fluentdの連携をしている場合には送信元のfluentdも再起動することをおすすめします。
 私が設定した時には送信側が受信側を見失ってログの送信が止まってしまいました。

新たな問題

前項までの対応により、旧ログ監視の問題は解消し、 リアルタイムで、かつサービスレベルの高い仕組みができました。
が、新たな問題がいくつか発生しました。

  1. フィルタリングが無くなったのでERROR・FATALのレベルのログが全部飛んでくる
  2. 異常が発生するとガンガンメールがくる
  3. IRCに通知されない

新たな問題 1. ERROR・FATALのレベルのログが全部飛んでくる
→ 対策: 随時チケット化してエラーログの根本原因を潰す!

実はログを検索してメールを飛ばす処理の中でホワイトリストでフィルタリングして、通知するものを絞っていました。
その仕組みが無くなったのでERROR・FATALのレベルのログが全部飛んでくるようになってしまいました。 fluentdでフィルタリングするという選択肢もありましたが、対応の必要のないFATALやERRORを出すシステム側を修正すべきということで、チケット化してエラーの根本対応を実施。

新たな問題 2. 異常が発生するとガンガンメールがくる
→ 対策: 覚悟しておく!

従来のログ監視は1時間に1回ログを検索してメールを飛ばす処理だったため過去1時間に何件エラーが発生していても1通の通知にまとまっていました。
それがリアルタイムでエラー1件毎に通知されるため、異常事態にはメールが大量に送信されてしまいます。
実際に外部ベンダー側にトラブルがあり、復旧までに4時間かかった際に断続的に300通送信されてきました。orz
しかし、異常が発生しているのだから通知はあるべきでそれを止めるのは本末転倒ということで受け入れることにしました。

新たな問題 3. IRCに通知されない
→ 対策:社内で導入を進めている新監視システムへ

メールと同時にIRCに通知して、業務時間中はメールよりも気づきやすいように対策していましたが、IRCは社内サーバーで稼働しているため利用できませんでした。
現状は通知のために旧監視システムと並行稼動をしていますが、実際にはリアルタイムに飛んでくるメールに十分に気づけています。
今後は社内で進めているSlackのサービスを利用した障害対応フローに移行する予定です。
AmazonSNSに通知先を増やすだけで対応できるはずです。

まとめ

  • fluentdにプラグインを追加することで簡単にAmazonSNS経由の通知を実現できました。
    こういったプラグインが充実しているのもfluentdの魅力だと思います。(fluentdの導入については『fluentd(td-agent) の導入』を参照してください。)

  • ログ監視も即時性・対障害性の高いシステムを構築して確実で迅速な検知が必要です!
    ログ監視は最後の砦であり将来発生する障害の芽を発見するアンテナだと思っています。これからもCORECとともにCORECを支える運用システムもブラッシュアップしていきます。

  • 無駄なログは技術負債。根本から除去しましょう!
    無駄なログがたくさん通知されてくると、本来拾わなければいけないログを見落としてしまう可能性があります。そのために通知するログを絞ることは大切ですが、安易にフィルタリングをするのではなく、一つ一つ原因を突き止めて対応すべきところはプログラムで対応し、技術負債を残さないようにしたいですね。

第4回技術部LT大会 開催

こんにちは、たむらです。

先日第4回技術部LT大会を開催しました。もう定着している感のあるLT大会ですが、
今回もみんなしっかり準備をしていて、内容も非常にバラエティに富んだものになっていました。

~発表されたLTタイトル~
「軽い気持ちでリモデ」
「SSL証明書やドメインなどの期限管理」
「パソコン少年の成れの果ての懐古(または、劣化移植への限りなき愛情)」
「キャッシュフロー計算書を作ってみた」
「URIHOの社内ツールのモック作成でLESS使ってみた感想」
「コレックウォッチ ~ログ監視のはなし~」
「YeomanでAngularの開発環境を構築して何か作ってみた」
「今年やりたいこと」
「個人事業主や中小企業の主に小の方に売掛保証を広めたいと思った結果」
「キーワードで振り返る2015年IoTトピックス」
「PayPal APIのネガティブテストを作ってみた」
「新監視体制について」
「Seasar2から卒業しよう」

LT大会の様子
注) 窓に貼ってある習字は会社の年始イベントの書初めです。こちらも毎年恒例となりつつあります(笑)。。
DSC_0943
DSC_0951
DSC_0953


みんなで選ぶ優秀発表者は、
1位:「今年やりたいこと」
2位:「キーワードで振り返る2015年IoTトピックス」
3位:「Seasar2から卒業しよう」
となりました。

1位の内容は、年始ということもあり自分の今年の抱負を語るというものでした。仕事に繋がることもプライベートなことも盛り沢山の話で、みんなの支持を集めていました。2位は、Raspbery Pi2からmyThings、SORACOMなどの2015年のIoTを自身の感想を含めて振り返るというもの。本当に多くのサービスと商品が増えてきたIoTですが2016年も更に賑やかになりそうですね。3位は弊社でも利用していて今年の秋にサポートが終了するSeasar2についての見解を述べたものでした。業務上の課題をみんなに共有できていて有意義な内容となっていました。

次回はまた夏から秋ぐらいに行なう予定です。

[Rails]Model/テーブル設計で必ず覚えておきたいSTI

開発チームの下田です。

Ruby on Railsについて、基本的なこと(modelやcontrollerが何か知っている程度)を理解されている方を対象としています。

BtoBクラウド受注・発注システムCORECはRailsで開発しています。 開発初期のコードを見直してみると失敗したなと思うことがあります。 中でも特に開発効率に影響しているなと思うのが、RailsのActiveRecordのSTI(Single Table Inheritance、単一テーブル継承)を活用しなかったことです。

STIを簡単に言うと、モデルを永続化するときに、どのクラスなのかというメタ情報を含めてデータベースに保存します。 これだけではわかりづらいと思うので、失敗した例と改善例からご紹介したいと思います。

悪い設計

仮に下記の仕様のコードを書くとします。

要件1:
 CORECではWEBアプリから発注書を送信できます。送信方法は次の3つから選べます。

  • WEBアプリ内で送信する
  • メールで送信する
  • FAXで送信する

この時、次のようなコードにしていました。
(項目名、メソッド名は意味が通じるように和名にしています)

# 注文書model 
class Order < ActiveRecord::Base
  include FAX送信モジュール
  validates :送信方法, :presence => true
 
  # WEBで送信するなら送信先IDが必須 
  validates :送信先ユーザ_id, :presence => true, if: ->{ 送信方法 == 'WEB' }
 
  # メールならメールアドレスが必須 
  validates :メールアドレス, :presence => true, if: ->{ 送信方法 == 'メール' }
 
  # FAXならFAX番号が必須 
  validates :FAX番号, :presence => true, if: ->{ 送信方法 == 'FAX' }
 
  has_many :FAX送信結果
 
  def 送信する
    self.送信済 = true
    validate!
    case 送信方法
    when 'メール'
      注文Mailer.メール注文書送信.deliver
    when 'FAX'
      FAX送信する
    end
    save
  end
 
  private
 
  def FAX送信する
    FAX送信結果.create(送信結果: FAX送信モジュール.送信)
  end
end


何が問題か

当初は送信するときだから、送信手段ごとに処理をcase-whenで振り分けることに違和感はありませんでした。しかし、開発を進めていると、同じコードがあちこちに現れて読みづらいコードになり始めました。

要件2:

  • FAXは送信に失敗することがあるので、結果判定をする
  • WEBとメールは必ず成功したとみなす
class Order < ActiveRecord::Base
  # ~~ 略 ~~ 
 
  def 送信成功?
    # メソッドごとに送信方法による分岐がある状態 
    case 送信方法
    when 'FAX'
      FAX送信結果.送信成功?
    default
      true
    end
  end
end


あるべき姿

FAXもメールもすべて注文書の情報なのでOrderクラスにしたことは正しいのですが、送信方法によってふるまいが違います。役割が同じでふるまいが違うなら、サブクラスを作りポリモーフィズムを持たせるべきです。

sti


class Order < ActiveRecord::Base
  def 送信する
    self.送信済 = true
    save
  end
 
  def 送信成功?
    false
  end
end
 
# WEBで送信する注文書クラス
class Order::Web < Order
  validates :送信先ユーザ_id, :presence => true
 
  def 送信する
    validate!
    super
  end
end
 
# メールで送信する注文書クラス
class Order::Mail < Order
  validates :メールアドレス, :presence => true
 
  def 送信する
    validate!
    注文Mailer.メール注文書送信.deliver
    super
  end
end
 
# FAXで送信する注文書クラス 
class Order::Fax < Order
  validates :FAX番号, :presence => true
 
  has_many :FAX送信結果
 
  def 送信する
    validate!
    FAX送信する
    super
  end
 
  def 送信成功?
    FAX送信結果.送信成功?
  end
 
  private
 
  def FAX送信する
    FAX送信結果.create(送信結果: FAX送信モジュール.送信)
  end
end

メソッドの中から冗長なcase文が消えます。また、relationやvalidationのふるまいも送信方法ごとに整理され、見通しが良くなりました。

このようなクラス設計にしたい場合はSTIを使用します。STIを使用するにはtype:stringカラムを追加します。

  create_table "orders", force: true do |t|
    t.string   "type" # 追加するとSTIになる 
    t.integer  "送信先ユーザ_id"
    t.string   "メールアドレス"
    t.string   "FAX番号"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

typeカラムがある場合とない場合を比較してみましょう。

# typeカラムがない場合 
order = Order::Mail.new(メールアドレス: 'hoge@example.com')
order.save
# id: 1 
# メールアドレス: "hoge@example.com" 
 
# OrderからロードするとOrderになる 
Order.find(order.id)
=> #<Order id: 1, メールアドレス: "hoge@example.com"> 
 
# Order::MailからロードすればOK 
Order::Mail.find(order.id)
=> #<Order::Mail id: 1, メールアドレス: "hoge@example.com"> 
# typeカラムがあり、STIになっている場合 
order = Order::Mail.new(メールアドレス: 'hoge@example.com')
order.save
# type: 'Order::Mail' # クラス名がtypeに自動的にsaveされる 
# id: 1 
# メールアドレス: "hoge@example.com" 
# created_at: 2015-01-01 12-34-56 
# updated_at: 2015-01-01 12-34-56 
 
# Orderからロードしても、きちんとOrder::Mailになる 
Order.find(order.id)
=> #<Order::Mail id: 1, メールアドレス: "hoge@example.com"> 
 
# サブクラスからロードすると、typeを検索条件に自動で加えてくれる 
Order::Mail.all.to_sql
=> "select * from orders where type = 'Order::Mail'"

データベースに保存し永続化した後でもふるまいを維持し続けてくれるので、クラス設計を適切に行えば複雑な仕様が追加されても、良い状態のコードを保てると思います。

運用上の注意

開発には非常に便利なSTIですが、運用を考えて開発しなければならないことが1点あります。typeカラムに存在しないclass名が設定されていると、ActiveRecord::SubclassNotFound例外が発生します。 ActiveRecord::SubclassNotFound例外が発生するのは、2パターンがあります。

  • リリース時に並行稼動している時に、新しいAPで保存した新しいサブクラスを古いAPでロードした
  • リリースしたがロールバックし、新しいAPで保存した新しいサブクラスを古いAPでロードした<

どちらの場合も、何も変更しないサブクラスを作り、先行してリリースしておくと、予期せぬ例外が起きません。フェイルソフトになります。

# 仮置きする。 
class Order::NewClass < Order
end

もしくはデフォルトスコープに現バージョンで存在しているclassのみを指定すると、ロードされないので例外が発生しません。サブクラスを追加した時に追加し忘れたり、where句が思わぬところで効いたりしないか確認が必要です。

class Order < ActiveRecord::Base
  # 条件に加えてしまう。
  default_scope ->{ where(type: [Order, Order::Corec, Order::Mail, Order::Fax]) }
  # ~~ 略 ~~ 
end

おわりに

STI以外にも、ああすればよかったと思うことは多くあります。その中でもSTIについて書いたのは、知っているか知らないかでModelの設計に差が出てしまうため、気づいた時には手軽にはリファクタリングできなくなっているからです。その反面、ある程度の規模のアプリケーションでなければ使い道が無くRuby on Rails チュートリアルに載っていなかったりと軽視されがちです。 新しくmodelを設計するときには頭の片隅に置いておくといいと思います。

記事検索