syntax

2014年1月27日月曜日

システムモニタのグラフ生成にMuninを使用する

Monitでシステムモニタ、プロセス監視も行うのですが、プラスでMuninも導入します。
Muninでは各種システムモニタ、グラフ生成、しきい値超え検知時の通知など行います。

導入するパッケージは各インスタンスに導入するmunin-nodeとmunin-nodeから情報を収集してグラフ生成(RRDtool)、アラート通知を行うmuninの2つで構成されます。
Muninはwatchと名づけたサーバに導入します。Muninでグラフ生成するノードの対象はEC2のNameタグに名前がついているものだけとします。
appサーバ、resqueサーバの中でオートスケールによって増えた分は対象としません。

対象ノードが増えてくるとMunin側でのグラフ生成に時間がかかるようで、色々対策されるかたもいるみたいです。この案件でもノード数が増えてきた場合は何かしらの対策をしたいと思いますが、とりあえずはそのまま使っていきます。

インストール

毎度のyumでいれるだけです。

# 全ec2インスタンス
$sudo yum install munin-node
# watchサーバ(グラフを生成する)
$sudo yum install munin

munin-nodeの設定

ACL

Muninが動いているサーバが各インスタンスのmunin-nodeに接続してくるので、munin-nodeの設定でmuninのIPを接続許可する設定が必要です。
今回はVPC内サブネットのネットワークアドレス全体を許可にしました。

/etc/munin/munin-node.conf

# A list of addresses that are allowed to connect.  This must be a
# regular expression, since Net::Server does not understand CIDR-style
# network notation unless the perl module Net::CIDR is installed.  You
# may repeat the allow line as many times as you'd like

allow ^127\.0\.0.1$
allow ^172\.31\.[0-9]+\.[0-9]+$

# Which address to bind to;
host *
# host 127.0.0.1

# And which port
port 4949

またmunin-node自体はtcp:4949でListenしているので、全セキュリティグループに対して下記のポリシーを追加します。
SecurityGroup=NETからのtcp:4949を受け入れる

プラグインを有効にする

色んなサービスのプラグインがデフォルトで用意されているので、サーバの中にそのサービスが動いていればmuninインストール後からすぐにモニタリングができるようになります。
どのプラグインが有効になっているかはmunin-node-configureで確認できるようです。
参考になった記事

試した所munin_statsというのが有効になっていないようなので有効にしてみました。シンボリックリンクを貼った後、munin-nodeのrestartをします。

$sudo munin-node-configure --shell
  ln -s '/usr/share/munin/plugins/munin_stats' '/etc/munin/plugins/munin_stats'
$sudo ln -s '/usr/share/munin/plugins/munin_stats' '/etc/munin/plugins/munin_stats'
$sudo /etc/init.d/munin-node restart

また、WEBサイトのヘルスチェックをMuninで行いたいと考えていたのですが、どんぴしゃなプラグインがありましたのでこれをそのままwatchサーバのmunin-pluginに導入します。
httpingとmuninでWebサーバのレスポンスをグラフ化した

munin側の設定

監視対象のノードを追加する

/etc/munin/munin.confで収集するノードを下記のように追記していきます。ノード毎にしきい値をデフォルト以外に設定することも可能です。

[example;]
  contacts systemalert

[example;watch-1] #watch-1で名前解決できないとだめ
    address 127.0.0.1
    use_node_name yes
    httping_staging.contacts httping
    load.load.warning 10
    cpu.user.warning  500
    load.load.critical 20
    cpu.user.critical  700

アラート通知設定

また、しきい値超の場合のアラートメール設定は、下記のようなものを任意の別ファイルにして設定しました。
アラートの内容、深刻度によってメールの送信先を分けてるようにします。

  • /etc/munin/munin-conf.d/munin-alert
contact.httping.always_send critical
contact.httping.max_messages 5
contact.httping.command mail -s "${var:worst} ${var:cfields} Munin http healthcheck" systemservicedown@watch.example.com

contact.systemalert.always_send critical
contact.systemalert.max_messages 3
contact.systemalert.command mail -s "${var:worst} ${var:cfields} ${var:group} ${var:host}" systemalert@watch.example.com

計画メンテナンス時などでサービスダウンしている場合に毎回ご丁寧にアラートメールを送ら無くても良いので、アラート通知を気軽にオン、オフにできるようにします。
上記のmunin-alertが/etc/munin/munin-conf.d/にあると通知オン、なければオフとなるので、このファイルを置いたり消したりするデプロイタスクを後ほど用意します。

グラフを見る

グラフはhttp://muinが動いているサーバ/muninで閲覧できます。
Muninを入れたサーバでApacheが動いているものとします。Muninを入れた時点で/etc/httpd/conf.d/munin.confが用意されます。
社内からのアクセスだったらパスワード認証なしでグラフを見れるようにして、社外であればベーシック認証というように変更します。

  • /etc/httpd/conf.d/munin.conf
AuthUserFile /etc/munin/munin-htpasswd
AuthName "Munin"
AuthType Basic
require valid-user

Satisfy Any
Order deny,allow
Deny from all
Allow from 127.0.0.1 172.31. 会社のIP

Written with StackEdit.

Monitでインスタンス内のシステムモニタ、プロセス死活監視する

各EC2インスタンスにMonitを導入して、CPU使用率、メモリ空き容量、Disk容量といった数値の監視とプロセスの死活監視を行います。例えば、Disk容量が90%超えを検知したら指定のメールアドレスに送信することもできます。
モニタをグラフで見る機能はないので、それについてはあとで記事にする予定のMuninを導入します。

インストール

$sudo yum install monitで入ります。
設定は/etc/monit.d/以下に.confを配置します。サーバの役割毎に監視したいプロセスなども変わってくるので、役割毎にファイルを分けて用意します。
base.confは全サーバ共通の監視項目、web.confはWEBサーバとしての監視項目、app.confはAPPサーバとしての監視項目・・・と言った具合です。
インスタンスがAPPサーバとして立ち上がった時は、base.confとapp.confを/etc/monit.d/下に配置されるようにします。

設定内容

confの例を書いておきます。こんなことを設定しています。

  • アラートの通知先と文面
  • cpuロードアベレージ、メモリ使用率、ディスク使用率
  • pidの存在でrsyslogdを監視
  • ps -efして”eye monitoring”がマッチするかどうかでeyeを監視
set alert systemalert@watch.example.com but not on { INSTANCE pid } with mail-format { 
  from: monit@$HOST
  subject: $SERVICE $EVENT $HOST
  message: 
Monit $ACTION $SERVICE at $DATE on $HOST: $DESCRIPTION.
}

set mailserver localhost  port 25

# System
check system system
group system
if loadavg (1min) > 12 then alert
if loadavg (5min) > 8 then alert
if memory usage > 90% then alert
if swap usage > 25% then alert
if cpu usage (user) > 95% then alert
if cpu usage (system) > 75% then alert
if cpu usage (wait) > 50% then alert

check filesystem rootfs with path /
  if space usage > 80% then alert

# syslogd
check process syslogd with pidfile /var/run/syslogd.pid
 start program = "/etc/init.d/rsyslog start"
 stop program = "/etc/init.d/rsyslog stop"
 if 5 restarts within 5 cycles then timeout
check file syslogd_file with path /var/log/messages

#eye
check process eye matching "eye monitoring"
  start program = "/etc/init.d/eye start"
  stop program = "/etc/init.d/eye stop"
  if 5 restarts within 5 cycles then timeout

monitの死活監視はどうしようとなりますね。upstartで制御というのが良いかもしれません。
参考になる記事

Written with StackEdit.

HAProxyでMySQL slaveの通信を分散する

初めにだらだらと概要を書く

MySQLの負荷分散として更新系のmaster、参照系のslaveでインスタンスを複数台用意することにします。slaveについては負荷に応じて台数を増減する予定です。

アプリからみてslaveサーバが何台あるか意識しなくてもいいようにHAProxyを用意します。
HAProxyがアプリとMySQLサーバの通信の中継役を行ってくれるので、アプリからは常にHAProxyだけ見えていればOKとなります。
HAProxy側で複数のslaveサーバに接続を分散してくれます。もしslaveサーバのどれかがダウンしてるときはそのサーバには接続を振らないようにもしてくれます。

また、「負荷に備えてslaveを増設したけど暖気が終わるまで接続を振りたくない」という場合に、設定を一時的に変更することでそのサーバに接続を振らないようにもできます。
接続数の重みづけの調整もできるので、じわじわと接続数を増やしていくということもできそうです。

当初MySQLだけでなくRedisの中継も行おうと考えていたのですが、RedisはElastiCache側でうまく分散してくれるようなので、MySQLのSlaveだけに使用することにします。

インストールと設定

インストールはyumですんなり入ります。
またコマンド経由でhaproxyを制御できるようにhaproxyctlを入れます。
こちらはgemでして、$gem install haproxyctl ; rbenv rehashでインストールします。

設定内容は以下の感じです
/etc/haproxy.cfg

global
  maxconn 4096
  user haproxy
  group haproxy
  daemon
        stats socket /tmp/haproxy level admin
        quiet
        nbproc 1
        log 127.0.0.1 local0 info

defaults
  log global
  mode  tcp
  retries 3
  option redispatch
        option tcplog
        option dontlognull
  maxconn 4096
        timeout connect 10s
        timeout client  10s
        timeout server  10s
        grace 1000

listen mysql-slave
  bind    :3307
  mode    tcp
  balance roundrobin
  option  mysql-check user haproxy
  server  slave1 staging-slave-1.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  slave2 staging-slave-2.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  slave3 staging-slave-3.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  slave4 staging-slave-4.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  master1 staging-master-1.example.internal:3306 check port 3306 inter 5s rise 2 fall 2 backup

MySQLのヘルスチェック

option mysql-check user haproxyという設定で、HAProxyがhaproxyユーザを使ってMySQLサーバに接続を試みてヘルスチェックを行います。
MySQLサーバ側で、haproxyユーザを作っておく必要があります。

grant usage on *.* to 'haproxy'@'%';
flush privileges;

運用中にやりそうな事

例えば、staging-slave-1を何らかの理由で使用したくないという場合は、sudo haproxyctl disable server mysql-slave/slave1で使用されなくなります。
復活させたい場合はsudo haproxyctl enable server mysql-slave/slave1です。

slaveを立ち上げたばかりなので少ない接続数から慣らしていきたいという場合は、他のslaveの重み(weight)より低い数値に変更します。
コマンドとしてはこんな感じです。
sudo haproxyctl set weight mysql-slave/slave1 1
weightの数値は0~256のようで、数が大きいほど接続数も多くなります。

なお、stats socket /tmp/haproxy level adminとしておかないとsetコマンドはpermission deniedで拒否されます。

Written with StackEdit.

各インスタンスのリゾルバを設定する

VPC内インスタンスの名前解決は下記の順に行います。

  1. /etc/hosts見る
  2. /etc/resolv.confに書かれたDNSサーバに問い合わせる

/etc/resolv.confは下記のように設定します。

options single-request-reopen
search example.internal
nameserver 127.0.0.1
nameserver 自前のDNSのIP
nameserver AWSの用意したDNSのIP

ローカルにdnsmasqをいれてDNSキャッシュする

127.0.0.1が何かというと、dnsmasqというDNSサーバで、各インスタンスのローカルで起動させておきます。
毎回DNSサーバに問い合わせしないようにDNSキャッシュ機能をするためです。dnsmasqにキャッシュがない場合は/etc/resolv.confに書かかれたIPにフォワードしてくれます。

dnsmasqの導入は$sudo yum install dnsmasqですんなり入ります。
ネガティブキャッシュ(名前解決できなかったということをキャッシュする)はしないようにします。

  • /etc/dnsmasq.conf
# If you want to disable negative caching, uncomment this.
no-negcache

キャッシュを全クリアしたい場合は$sudo /etc/init.d/dnsmasq force-reloadします。

インスタンス起動時に/etc/resolv.confが初期化されてしまう

最初/etc/resolv.confに上記の内容を直接ベタ書きしていたのですが、インスタンス再起動時に/etc/resolv.confが初期化?されてしまい
AWSの用意したDNSサーバのIPのみに書き換わっていたりしました。
DHCPクライアントなので/etc/resolv.confも設定されるのでしょう。

調べてたところ/etc/dhcp/dhclient-enter-hooksのファイルを用意していると、DHCP更新時の/etc/resolv.confの書き換え処理を再定義できるようです。

make_resolv_conf() {
  rscf=`mktemp /tmp/XXXXXX`;
  echo 'options single-request-reopen' > $rscf
  echo 'search example.internal' > $rscf
  echo 'nameserver 127.0.0.1' >> $rscf
  echo 'nameserver 172.31.4.75' >> $rscf
  echo 'nameserver 172.31.0.2' >> $rscf
  change_resolv_conf $rscf
  rm -f $rscf
}

今のところこれで正常に動作しているようです。

Written with StackEdit.

内部用の名前解決DNSを用意する

物理サーバでインフラ環境を作っていた時は、サーバに固定IPをつけたりネットワークアドレスも「10.1.1.0/24がAPPSセグメントで10.1.2.0/24がDBセグメントで・・・」と設計することがほとんどでした。
EC2を試した時はデフォルトでDHCPな環境だったので、AWSで構築するときはネットワークアドレス、IPアドレスをなるべく意識しなくても良いようにしたいと考えています。

$ssh staging-app-1のようにhost名でどのサーバかある程度識別できるようにはしたいので、自前で名前解決を用意することにします。
自分にとってはBINDでの構築が手っ取り早いので、即決でBINDを採用します。

このDNSサーバはVPC内のクライアントからDNS問い合わせを受け付けて、この案件用の内部ドメイン例えばexample.internalの名前解決を行います。
yahoo.co.jpのような他のドメインの問い合わせの場合はAWSで用意しているDNSにフォワードします。

/etc/named.conf一部

options {
  ....
  forwarders{
    172.31.0.2; 
  };
  ...
};

zone "example.internal" {
        type master;
        file "example.internal.db";
        allow-query { any; } ;
        //allow-transfer{ x.x.x.x; };
        //notify yes;
        //also-notify { x.x.x.x; };
};

いつものDNS運用ならゾーンファイルのレコード情報をたまに更新する程度なのですが、EC2環境の場合だと「インスタンスを新規に起動したからそのインスタンスの名前とIPを調べて、ゾーンファイルに書いて、namedリロードして、、、」なんてことを頻繁にやりそうです。そんな細かい作業を手で毎回やるのもしんどいのでスクリプト化します。

内容としてはVPC内にいるインスタンスのタグNameとプライベートIPを組み合わせたAレコードをゾーンファイルに書き出し、/usr/sbin/named-checkzoneでゾーンファイルに異常がなければnamedをリロードします。

AWS::EC2.new.instances.each {|ins|
  if ins.tags['Name'] && ins.private_ip_address.match(/\d+\.\d+\.\d+\.\d+/)
    p "#{ins.tags['Name']}   IN   A   #{ins.private_ip_address}"
  end
}

Written with StackEdit.

最初のec2インスタンスを作る

AWS management consoleのEC2 Dashboardから早速EC2インスタンスを一つ作り、これをテンプレートマシンと呼ぶことにします。
このテンプレートマシンに一通りのソフトウェア、設定を手でごりごり入れていきます。
アプリケーションサーバが1台ほしいという時になったら、このテンプレートマシンのAMIをベースに「アプリケーションサーバ」という環境変数的なものを与えてマシンを起動するとアプリケーションサーバとして立ち上がってくるというようにします。

色々調べているとChefを使って構成管理をする記事をよく見かけるので、「自分もやりたい!」と思いつつもすぐ「時間足りないかなぁ」となって、使うのは見送っていました。最近になってまた勉強する時間もできそうなのでChefにトライしたいところです。

インスタンス起動後は以下を進めます。手順詳細はここに書かずに、参考になった記事をリンクさせて頂きます。

yum updateする

/etcをgitで管理する

etckeeperを導入します。
手順参考

dateコマンド打った時に日本時間で表示されるようにする

cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ruby環境準備

rbenv、ruby-build、ruby2.1(当時は2.0)を入れる
手順参考

シェルからAWSのコマンドを使えるようにする

このインスタンスのシェル上からAWSのAPIを叩けるようにします。
AWS-CLIパッケージはインストール済みだったので、与えたれたアクセスキー、シークレットアクセスキー等を設定します。
手順参考

$aws configure
AWS Access Key ID [****************]: ***
AWS Secret Access Key [****************]: *** 
Default region name [ap-northeast-1]: ap-northeast-1
Default output format [text]: text

すると~/.aws/configが生成されます。

[default]
aws_access_key_id = ***
aws_secret_access_key=***
output = text
region = ap-northeast-1

awsからはじまるコマンドが使えるようになります。
as-、ec2-、rds-から始まるコマンドは昔のもので今はawsに一新されたようです。

AWS SDK for rubyはRuby2.1を入れた後gem installで入れます。

このインスタンスのAMIを定期的に作成できるようにする

任意のインスタンスのAMIを作成するスクリプトを用意します。
AWS manegement consoleからAMI削除、AMI作成をするのが手間だったので、スクリプトにしました。
インスタンスの改造作業の前にスクリプトを実行したり、cronで定時バックアップするようにしておきます。

Ruby環境な会社なので、私も見習ってRuby使っていきます!

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require "aws-sdk"

AWS.config(YAML.load(File.read("config.yml")))

machine_names = [
  "template",
 ]

machine_names.each {|name|
  AWS::EC2.new.images.filter("name", name).each{|ami|
    p "deregister ami name: #{name}, ami_id: #{ami.id}"
    if ami.deregister
      p "success"
    else
      p "fail"
    end
  }
  AWS::EC2.new.instances.tagged("Name").tagged_values(name).each{|ins|
    p "create ami name: #{name}, ec2_id: #{ins.id}"
    new_ami = ins.create_image(name, {:description => Time.now.to_s, :no_reboot => true})
    if new_ami && new_ami.tag("template", :value => name) 
      p "success"
      p "ami.id: #{new_ami.id}"
    else
      p "fail"
    end
  }
}

Written with StackEdit.

AWS全体構成を図にした

構成図

enter image description here

今現在ステージング環境をこの構成で動かしています。
最初この構成内のサービスの機能や役割についてだらだらと書いていたら、やたらかったるい長文になってしまったので書き直しました。
思う事がある所だけ書き留めておきます。

VPC環境下に構築する

用意されたAWSのアカウントでログインするとすでにVPCが設定されていたのでそのままVPC内に作ります。
サブネットもアベイラビリティゾーンA(AZ-A),AZ-Bでそれぞれ1つずつ作られてたのでそのまま使用します。
WEB、DBという用途役割でのサブネットは作りません。(細かいネットワークアドレスでの制御等はしないと思うので)

セキュリティーグループ(SG)は用途役割に応じて作成する

WEB,DB,ADMINといった大体の用途役割別にSGをつくりインスタンスを属させます。複数のSGに属することも可能ですが、この案件ではそうしません。
セキュリティーグループ間の通信は下記のような簡単なパケットフィルタリングで管理します。

staging-appsグループはstaging-proxyグループからくるtcp80(nginx)のパケットは受け入れる。

staging-dbグループはstaging-appsとstaging-backgroundグループからくるtcp3306(mysql),tcp6379(redis)のパケットは受け入れる。

AZ-Aにインスタンスを寄せる

インスタンスをA,B両方に配置して耐障害性を高めるのが推奨かと思いますが、あえてAZ-Aに寄せました。
どうもアプリサーバとDBサーバがA,Bでまたぐ場合、レスポンスが遅くなる問題が頻発するのです。
解決もできないし一旦あきらめました。AZ-Aが崩壊しないことを祈りながら運用することにします。
AZ-Aが全く使えなくなった場合は、その時点からAZ-Bにリストアすることを検討します。

インスタンスのマシンイメージ,DBのバックアップはS3に確保されてるから大丈夫として、mongodb,redisのデータはAZ-Aにしか存在しないので、
これらはAZ-A以外のところにバックアップを退避させておきます。

Written with StackEdit.