サーバーを監視するサーバーを立てることをダラダラ考えてみる

今現在のところ私はサーバーを複数台稼働させているのだけれど、数が増えてくると手間がかかるため一台一台順調に動いているかどうかチェックするのが煩わしくなってくる。そこで、その問題を解消するために、サーバーを監視するサーバーを立てるのはどうかと思った。サーバーが1台や2台だったらそんなことをする必要はないのだけれど、3台4台、あるいは5台6台となってくると・・・やはり必要かなと。監視サーバーにアクセスするとすべてのサーバーの状況が一瞬でわかるような構成を考えていて、まあ監視サーバーだから当然だけれども。

ぶっちゃけて言うと、今自分が使っているウインドウズで、サーバーの状況を取得するマクロを作ってそれをバッチファイルで定期的に実行して結果を適当なファイル形式で自分が見やすいように出力するようにしたら監視サーバーを立てることは必要ないような気がする・・・。あるいはそれをWindowsアプリケーションとして作ればもっと使いやすく構築することもできるかもしれない。監視サーバーはあくまで趣味として、勉強として、作る。

監視サーバーとしての必要な要件

  • 24時間常時稼働
  • 稼働中のサーバーが問題無く稼動しているかどうかのチェック

とりあえず最低限の要件を満たせればいい。稼働中のサーバーが稼働しているかのチェック方法は、その監視サーバーからリモートアクセスしてレスポンスが返ってくればとりあえず稼働していると見なす。もっと踏み込んでアプリケーションが正常に処理しているかとかもチェックした方がいいが、それはおいおい考えていくとして。

監視サーバーとして必要な具体的なソフトウェアなど

  • ubuntuかlinuxならなんでも
  • python
  • mysql
  • cron
  • php(※pythonでも可)
  • html,css
  • apache

つまり、監視サーバーは定期的に稼働中の各サーバーにアクセスして、その結果をDBに保存(例えばサーバーの負荷の状態やアクセス可否など)、その結果をウェブアプリケーションとしてHTML出力、ウェブブラウザからアクセスして確認できるようにする・・・みたいな感じ。手間がかかるだけで難しいところは多分ないと思われる。この方法のメリットはウェブサーバーを立てるから外出先からでも容易にチェックすることができるという点。

myriこのシステムを構築するために私が思う最適なマシンは、ラズベリーパイであると思う。現時点では。噂でラズパイはすごいと聞いていたが、実際に使ってみるとそのすごさに衝撃を受けた。ラズベリーパイ3の良さを語らしてもらうと、トランプの箱程の大きさにもかかわらずクアッドコアのCPUと1GBのメモリというスペックを持っていてしかも省電力。さらに標準で無線lanモジュールも搭載されていてその上ファンがないので無音。左が現在稼働中である私のラズベリーパイ3だけれども(これが監視サーバーという事実に今でも動揺してしまう)、電源コード1本挿しているだけのシンプルさで今までタワー型サーバー使っていたのはなんだったのかと思いたくなるくらいだ。肝心の性能はというと、ソフトウェアのインストールで若干もたつくかなと思うくらいでまったくストレスを感じない(HDDの代わりにmicroSDを使用しているのでその書き込み速度がボトルネックになっているのかもしれない)。今のところmysql,php,apacheといったLAMP環境で監視サーバーを構築しているがレスポンスはきびきびして快適な印象。以前似たようなので数年前に玄箱というものに手を出したことがあったが、それとは比較にならないほどに高性能かつ省スペースになっている。

mysqlのインストールとデータベースとテーブルの作成

監視サーバーが監視対象のサーバーにアクセスした結果はデータベースに保存することにする。ssh接続できた時点でサーバーがほぼ正常に稼働中であるのは確認できるが、監視サーバーっぽくするために色々情報を取得する。取得する情報は、私はサーバーOSにUbuntuを利用していて、ログイン時に、

  System load: 0.01               Memory usage: 71%   Processes:       145
  Usage of /:  71.3% of 70.32GB   Swap usage:   0%    Users logged in: 0

  Graph this data and manage this system at https://landscape.canonical.com/

みたいな情報が表示されるのでそれを取得、DBに保存してみる。その情報はpythonで/usr/bin/landscape-sysinfoを実行すれば取得できる。したがってDBに保存する情報はサーバーIP,稼働状況、HDD容量、プロセス数、メモリ使用状況、CPU負荷、日付といった具合になり、テーブル定義としては下記のような感じになった。

create database servers;
create table server_stats(
sId integer unsigned not null primary key auto_increment,
sIp varchar(75) not null,
sIsWork bool not null,
sStorage float,
sProcess integer,
sMem integer,
sCpu float,
sRecDate datetime not null);

・・・正直プロセス数とかメモリ使用状況とかわかっても意味はわからないんだけれども。

check-server.py

上記のテーブルに取得したデータを保存するプログラムは下記のような感じ。結構適当でざっくりとした感じで納品するわけでもないからただ動けばいいやと作ったので参考までに。言語はpythonでparamikoを使用してssh接続している。argvs = sys.argvの部分は監視対象のサーバーIPを引数で取得するようにしているため、このファイルを実行するときは sudo python check-server.py 192.168.1.10 といった感じになる。サーバーのディスク容量とかメモリ使用状況といった情報は正規表現で取り出して変数に入れておいて最後にDBにinsertするといった感じ。ところどころprintがあるのはデバッグの名残で意味はない。それでこのファイルをcronで1分毎に実行するように設定した。SSH接続して軽いコマンドを1回打つだけなので負荷はまったく問題にならないと思われる。そこまで密に監視する必要がなければ10分とか1時間おきとかに設定する。

import sys
import paramiko
import re
import MySQLdb

global sHdd,sMem,sProc,sCpu,sStats,sIp

sStats = 0

argvs = sys.argv
sIp=argvs[1]
port=22
username='user'
password='pass'

cmd='/usr/bin/landscape-sysinfo'

ssh=paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

try:
	ssh.connect(sIp,port,username,password)
	stdin,stdout,stderr=ssh.exec_command(cmd)
	list=stdout.readlines()

	sStats = 1

	for line in list:

		pattern1 = r'.*Usage of /:(.*)% of .*GB.*'
		pattern2 = r'.*Memory usage:(.*)%.*'
		pattern3 = r'.* Processes: .* (.*)'
		pattern4 = r'.*System load:.*(\d+\.\d+).*'

		#storage usage
		if re.match(pattern1,line):
			m = re.search(pattern1,line)
			print 'hdd = %s' % m.group(1).strip(' ')
			sHdd = float(m.group(1).strip(' '))

		#memory usage
		if re.match(pattern2,line):
			m = re.search(pattern2,line)
			print 'mem = %s' % m.group(1).strip(' ')
			sMem = int(m.group(1).strip(' '))

		#current process
		if re.match(pattern3,line):
			m = re.search(pattern3,line)
			print 'prcess = %s' % m.group(1).strip(' ')
			sProc = int(m.group(1).strip(' '))

		#cpu load
		if re.match(pattern4,line):
			m = re.search(pattern4,line)
			print 'cpu = %s' % m.group(1).strip(' ')
			sCpu = float(m.group(1).strip(' '))

	stdout.close()

except:
	print 'server some trouble occured.'

ssh.close()

print 'mysql test start.'
connector = MySQLdb.connect(host="localhost", db="servers", user="user", passwd="pass", charset="utf8")
cursor = connector.cursor()
sql = u"insert into server_stats(sIp,sIsWork,sStorage,sProcess,sMem,sCpu,sRecDate) values('%s',%d,%f,%d,%d,%f,now())" % (sIp,sStats,sHdd,sProc,sMem,sCpu,)
cursor.execute(sql)
connector.commit()
cursor.close()
connector.close()
print 'mysql test end.'


※随時加筆

Internet Explorer11がやたら重くなった

マザーボードのcmosクリアとかセッティング初期化とか色々やっていたらInternet Explorer11(以下ie)がやたら重くなった。重くなったというか、HDDアクセスランプが点きっぱなしで一時保存ファイルにキャッシュが保存できなくなっている様子。ieのツール-インターネットオプションから閲覧の履歴の設定と進むと、一時ファイル設定をする画面が表示されるが、そこの設定内容の使用するディスク領域が0MB、保存場所が空白、ディスク領域を新たに設定しようにも8~8の値を入力しろとか意味不明なエラーが出て設定できない、というような現象を確認。

以下、環境と修正するためにやったことをメモ(あくまで参考までに。試してみようと思った人は自己責任で。)

環境

  • Windows 7
  • Internet Explorer 11

不具合の現象

  • Internet Explorerの起動時の読込が遅くなった
  • Internet Explorerの設定の一時保存ディレクトリの使用するディスク領域が0MB
  • Internet Explorerの設定の一時保存ディレクトリの使用するディスク領域の容量変更不可
  • Internet Explorerの設定のキャッシュ保存ディレクトリへのパスが空白表示

試してみたこと

  • WebCacheフォルダ内にあるファイルを全て別の場所へ移動(≒削除)
  • 上記後、PCを再起動

試した結果

  • 再起動後、WebCacheフォルダ内には新たにファイルが作成されていた。
  • ieの起動時の重さが解消
  • 一時ファイル設定の0MBと保存場所空白の問題も解消

メモ

  • 一言で言えば、WebCacheフォルダ内のデータベースが何らかの理由により不具合を起こしてしまったため、それを再作成することで問題を解消した、ということになる
  • WebCacheフォルダは C:\Users\ユーザー名\AppData\Local\Microsoft\Windows に存在し、システムフォルダとして保護されているのでフォルダのメニューにある 整理-フォルダーと検索のプションから表示タブの詳細設定内にある 保護されたオペレーティングシステムファイルを表示しない(推奨)のチェックを外すことで表示される
  • 何故一時ファイルが保存できなくなったのかという原因は不明。マザーボードの設定初期化が怪しい気もするが
  • レジストリで保存場所等を変更といった作業をしてもおそらく問題解消にはならないと思われる。
  • 上記一連の記述は当方の環境に限定される状況であるかもしれないので、注意されたし。以上

SpriteKitのrunActionを遅延させる

SKspriteNodeを遅延させるには

let wait = SKAction.waitForDuration(1)
someNode.runAction(wait)

とかすればすればいい。ループの中でSKActionを実行するとループ処理中のアクションはほぼ同時に実行されているように目視では見えるためループの回数だけ遅延時間を延長させる。

for var i = 0; i < 5; i++{
    let wait = SKAction.waitForDuration(1*i)
    someNode.runAction(wait)
}

あくまで見た目上の演出のための遅延というか。これを関数にしてメインスレッドで複数実行とかするとSKActionの遅延中にメインスレッドが先に進行する。

swift didbegincontactが呼ばれない

SpriteKitの衝突判定のデリゲートメソッドdidbegincontactが呼ばれなくてハマった。シーンにデリゲートの設定をして、衝突するオブジェクトそれぞれにcategoryBitMaskとcontactTestBitMaskを衝突判定させるように設定していて、なぜこれでデリゲートが呼ばれないのかと試行錯誤

失敗していたコードは下記のような感じ。

    var slowCarBody = SKSpriteNode(textture:sCar)
    slowCarBody.categoryBitMask = slowCarBitMask
    slowCarBody.contactTestBitMask = playerCarBitMask
    slowCar.physicsBody = slowCarBody
    gameNode.addChild(slowCar)

そして下記のような感じに修正するとデリゲートが呼ばれるようになった。

    var slowCarBody = SKSpriteNode(textture:sCar)
    slowCar.physicsBody = slowCarBody
    slowCarBody.categoryBitMask = slowCarBitMask
    slowCarBody.contactTestBitMask = playerCarBitMask
    gameNode.addChild(slowCar)

違いは、オブジェクトの判定サイズの設定と判定グループの設定の順番を入れ替えた点。physicsBodyをイニシャライズする前にカテゴリを設定しようとしているんだからそりゃ効かない。spriteノードを作ったから大丈夫だろうと無意識でいたので気づかなかった

swift – itunes store にサインイン画面が何度も表示される

iPhoneアプリ開発中にStoreKitの購入処理をデバッグしていたら、アプリ起動時にサインインを求められる画面が表示されるようになった。これが表示されるようになった原因は、直前の購入処理がうまくいかず、購入の処理が終わってないことが原因だと思われる。トランザクションの終了処理が未実行となっているのだと予想。

だが、アプリ起動時にいきなりストアへのサインイン画面が呼ばれるというのがわからない。何のメソッドが呼ばれてサインイン画面が表示されているのかというのがわからないので手の打ちようがない。アカウントをテストユーザーでサインインしていたが、別のテストユーザーでサインインしたところ起動時のサインイン画面には前のユーザー名が表示されて消えないという現象も確認した。

現在のトランザクションの数を数えてみる。下記がレストア終了した後に呼ばれるメソッドでキューの数をコンソールに表示してみたら6という数字になった。おそらくこれが原因で起動時にサインイン画面が表示されていたのではないかと思う。

    func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {

        print(queue.transactions.count)

    }

溜まったトランザクションに対して終了処理を実行する。とりあえず上記メソッドに一回だけ下記のようなコードを追加・実行する

    func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
        
        for transaction:SKPaymentTransaction in queue.transactions{
            SKPaymentQueue.defaultQueue().finishTransaction(transaction)
        }
        
        print(queue.transactions.count)
    }

上記を実行すると溜まっていたトランザクションはとりあえず消える。で、再度アプリを起動させてみたところ、やはりサインイン画面は消えなくて相変わらず表示される。その後サインアウトしたり別のアカウントでサインインしてみたりとやっているうちに、起動時に表示されていた意味不明のサインイン画面は表示されなくなった。結局、明確な解決方法はわからずじまい。未終了のトランザクションが溜まっていてそれを削除したのは関係がありそうだけれども、なんとも言えない。

xcode – appstore – file must contain a higher version than that of the previously uploaded version

This bundle is invalid. The key CFBundleVersion in the Info.plist file must 
contain a higher version than that of the previously uploaded version.

バージョン1.13のバイナリを新しくアップデートしてバージョン1.2をapp storeに登録しようとしたら上記エラーが出て怒られた。appstoreの解釈では1.13 > 1.2ということになっているらしい。不本意ながら仕方がないのでバージョンを1.20としてビルドしてアップロードしたらエラーなく通った。

swift – SKNodeからSKSpriteNodeへ変換

SKViewからnodeAtPointで取得したノードはSKNodeとして受け取るため、それをSKSpriteNodeにキャストする。Objective-cの時はAnyObjectに突っ込んでからキャストしてればよかったがswiftだと型の判定が厳しくなっている模様

下記コードはSKNodeからSKSpriteNodeへ変換・・・しているというわけではない。SKSpriteNodeであると確信があるので決め打ちしてキャストしているだけ。

サンプルコード

override func touchesBegan(touches: Set, withEvent event: UIEvent?) {
        
        for touch: AnyObject in touches {
            
            var touchPos: CGPoint = touch.locationInNode(self)
            var node = self.nodeAtPoint(touchPos)

            if (node.name == "test"){
                        let sknode :SKSpriteNode = (node as? SKSpriteNode)!
            }
        }
}

swift – app内課金の実装

itunes connectやdeveloper centerで下準備は済ませているものとして。コードの実装部分のみ。課金の商品は1度きりの購入のよくある広告非表示の商品をイメージ。
処理の流れとしては、viewDidLoadにて初めの画面表示の時に製品IDで商品情報を取得して、ボタンを押したら購入処理を開始するというもの。商品が一つだけなら下記のような感じにできるが、普通は複数でテーブルビューなどに表示して配列で商品管理をやっている模様。出来るだけ簡単で少ないコードでの実装を目指す・・・

購入処理

デリゲートの追加

class ClassName: UIViewController SKProductsRequestDelegate, SKPaymentTransactionObserver{

}

必要なデリゲートメソッド

func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {

}

func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){

}

実装例

import UIKit
import StoreKit

class ClassName: UIViewController SKProductsRequestDelegate, SKPaymentTransactionObserver{

var storeBt:UIButton!
var productID = “com.sample.product100”
var vProduct: SKProduct!

override func viewDidLoad() {

storeBt = UIButton()
storeBt.frame = CGRectMake(0, 0, 200, 22)
storeBt.setTitle(“”, forState: UIControlState.Normal)
storeBt.setTitleColor(UIColor.orangeColor(), forState: UIControlState.Normal)
storeBt.setTitle(“”, forState: UIControlState.Highlighted)
storeBt.setTitleColor(UIColor.blackColor(), forState: UIControlState.Highlighted)
storeBt.titleLabel!.adjustsFontSizeToFitWidth = true
storeBt.addTarget(self, action: “byAc:”, forControlEvents: .TouchUpInside)
self.view.addSubview(storeBt)

SKPaymentQueue.defaultQueue().addTransactionObserver(self)
getProductInfo()

}

func getProductInfo(){
if SKPaymentQueue.canMakePayments() {
let request = SKProductsRequest(productIdentifiers:
NSSet(objects: self.productID) as! Set)
request.delegate = self
request.start()
} else {
print(“Please enable In App Purchase in Settings”)
}

func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {

let count : Int = response.products.count
if (count>0) {
var validProducts = response.products
var validProduct: SKProduct = response.products[0] as SKProduct
if (validProduct.productIdentifier == self.produto_value) {
print(validProduct.localizedTitle)
print(validProduct.localizedDescription)
print(validProduct.price)
vProduct = validProduct

//
storeBt.setTitle(“\(validProduct.localizedTitle) \(validProduct.price)円”, forState: UIControlState.Normal)
storeBt.setTitle(“\(validProduct.localizedTitle) \(validProduct.price)円”, forState: UIControlState.Highlighted)

} else {
print(validProduct.productIdentifier)
}

} else {
print(“not found”)
}
}

func byAc(sender: AnyObject){

if(vProduct != nil){
let payment = SKPayment(product:vProduct)
SKPaymentQueue.defaultQueue().addPayment(payment)
}
}

func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){

for transaction: SKPaymentTransaction in transactions {
switch transaction.transactionState {
case .Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
NSLog(“%@”, “失敗”)

case .Purchasing:
NSLog(“%@”, “購入中”)

case .Restored:
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
NSLog(“%@”, “リストア”)

case .Purchased:
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
NSLog(“%@”, “成功”)

// レシートを取得して検証
var receiptURL: NSURL = NSBundle.mainBundle().appStoreReceiptURL!
var receiptData: NSData = NSData(contentsOfURL: receiptURL)!
var base64receiptData: String = receiptData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
NSLog(“%@”, base64receiptData)

default:
print(transaction.transactionState.rawValue)
break
}
}
}
}

touchesBeganとtouchesEndedの間にある遅延について

iOSでtouchesBeganイベントが呼ばれてtouchesEndedが呼ばれるまでの間に約0.4秒ほどの遅延があることがわかった。(マシンの調子が悪い時は0.5秒ほど)
touchesBeganからtouchesMovedイベントまでの遅延はほとんど気にならない程度。
この0.4秒の遅延は体感ではまあ普通のアプリでは反応悪いなと許容できる範囲の時間ではあるけれども、応答の即時性が求められるアプリではちょっと長すぎる。この時間をなんとかゼロにできないものかといろいろ調べてみたが、見つからなかった。touchesEndedイベントに必要なディレイ時間として設定されていてプログラミングでプロパティを変更とかできないものかもしれない。
例えば画面をポンッとタップしてその結果がすぐにわかる必要があるような実装をする場合、処理をtouchesEndedに書いていたら0.4秒の遅延が発生するので使えない。この操作だとtouchesMovedイベントも呼ばれない。仕方がないのでtouchesBeganだけで完結するような実装にすることにした。

四国銀行がオンラインバンキングでハードウェアトークンを始めた様子

ワンタイムパスワード(ハードウェアトークン)の取扱い開始 | 四国銀行

四国銀行が振り込みと振替をオンラインバンキングでする場合にはワンタイムパスワードを必須にする様子。ハードウェアで短時間だけ有効なパスワードを生成する仕組みで、はっきりいうとジャパンネット銀行のパクリである。ハードウェアトークンの見た目もシール張り替えただけのそのままのように見える。

ワンタイムパスワード(ハードウェアトークン)とは

パスワード生成機(ハードウェアトークン)により生成・表示される1分ごとに変化する使い捨てのパスワードです。ワンタイムパスワードは1分後には使えなくなることから、第三者に遠隔操作等で不正にログインされるリスクを低減することができます。

振り込みの時にこのワンタイムパスワードを使うらしいから不正ログインのリスクとは関係がないような気がするが・・・。個人的には四国銀行のオンラインバンキングでこんなハイテクそうなことをやって大丈夫なのかという心配がある。昔の四国銀行のオンラインバンキングのUIは素人丸出しで絶対ホームページ・ビルダーで作ってるだろこれと思いながら私は使っていた。それを知っているからか残高照会以外の取引を四国銀行のオンラインバンキングでやるのはちょっと怖い。オンラインバンキングのアドレスを見るとwww.parasol.anser.ne.jpというドメインで運用されている様子でセキュリティ証明書はNTT DATA となっている。ジャパンネット銀行みたいなオンライン専門の銀行じゃないんだからあまり無茶はしないほうがいいとおもうんだけれども。

それにしても、ジャパンネット銀行をオンライン決済で利用している私はこのトークンをすでに使っていて、さらに四国銀行のトークンも管理しなければならないというのは、なんかセキュリティのためとはいえ馬鹿っぽい気がする。私は他にゆうちょ銀行や楽天銀行の口座も持っているが、それらの銀行がもしもトークンの利用開始を始めてしまったらたまったもんではない。財布に溜まっていくコンビニや量販店のポイントカードじゃあるまいし。