この記事は、暗号通貨 Advent Calendar 2017の24日目の記事です。
昨日の記事は、@redshogaさんのまだ急変動で消耗してるの? 暗号通貨(仮想通過)の急変動をつぶやくbotを作ったお話でした。なんと2日連続でBotの話題ですね。
作ったもの
BitZenyプール稼働状況Botというのを作りました。
【定期】プール稼働状況
— BitZenyプール稼働状況Bot (@bitzenypoolbot) December 24, 2017
LA Pool ✅
MDpool ✅
MDpool(new) ✅
うさぎコイン ✅
みそスープール ✅
大人の自由研究 ✅
寛永通宝 ✅
選民プール ✅
wmapool ✅
みずたまり ✅
Knyacki ✅
コインラッシュ ✅
とある鯖管 ✅
銭プール ✅
Hoge Pool ✅
powerpool ✅
(2017/12/24 20:00:00 JST)#BitZeny
こんな感じで、BitZenyという通貨のマイニングプールがきちんと動いているかどうかを定期的に呟いてくれます。
また、数分(現状、5分)間隔でプールを巡回し、障害が発生していたら警告をツイートします。
環境
- Node.js v8.2.1
- AWS EC2 t2.micro(無料期間)
経緯
BitZenyマイニングをやってみた
2017年12月6日あたりに、BitZenyの価格が20円ほどまで急騰して話題になりました。
それだけならこれまで散々起きた一過性の祭りとなんら変わりませんが、BitZenyには「日本中心の活発なコミュニティの存在」「CPUで手軽にマイニングできる」という特徴があったので興味を持ちました。
丁度自宅PC(自作機)のグラフィックボードを更新して、マイニングを始めてみようと掘る通貨を探していたこともあり、プールに登録して掘り始めました。
CPUでBitZeny、グラボでMonacoinを掘っているのですが、PCのサイドパネルを開けておくとそこそこ部屋が暖まっていい感じです。
夏になったらやめそうとか言わない
マイニングプールの多くが不安定に
当たり前ですが、私が考え付くようなことは皆さん同じように考えるわけで、BitZenyのマイニングを協同で行うマイニングプールでは日々ユーザが増えていきました。
特に、解説記事で紹介されたプールにマイナーの大部分が集中し、土日のマイナー激増を経て負荷分散が呼びかけられる事態に発展。しかし、分散先のプールにも受け入れの余力があるとは限らず、移行先を選んだはいいが通信ができず採掘できていないこともありました。
Botの制作
プールと接続できない時は、言及したツイートがないか検索するのですが、すぐ気付く人がいるとは限りません。どのプール落ちているのか、即座に分かるような仕組みがなかったため自作することにしました。
bitZenyのプール、今後もちょくちょく落ちるようなら疎通確認するBotの需要あるんかな
— 絶望の吉祥 (@kn1cht) 2017年12月11日
自分が欲しいから作るにせよ、折角なので公開の場で動かそうということでTwitterのBotとして開発することにしました。
専用サイトと違い鯖落ち情報がすぐに共有できますし、利用者がアクセスするのはTwitterなので負荷を考える必要がない利点がありますね。
障害発生の判定
プールを使う際、マイナーが接続する先は二種類あります。
-
Webのダッシュボード画面
- プールのWebサイトで、ログインして統計閲覧や各種設定が可能
-
Stratumポート
- 採掘ソフトが接続し、演算結果を送信する
- マイニング専用のプロトコルである
Stratum
を使用
このいずれかだけが停止するということも当然起こり得ます。
例えば、Web側がエラーを返していても、Stratumの方は動作していて正常に採掘できていることもあるのです。従って、この両方をチェックする必要があります。
Webダッシュボード
ほとんどのマイニングプールがMPOS
というシステムを使っており、APIで各種情報を取得できます。
リファレンスを読むと、登録ユーザに発行されるAPIキーが必要なメソッドがほとんどですが、public
だけは認証不要となっています。プールウェブサイトのindex.php?page=api&action=public
にアクセスするだけで、以下のようにJSONでデータを得られます。
$ curl 'https://lapool.me/bitzeny/index.php?page=api&action=public' | python -mjson.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 149 0 149 0 0 49 0 --:--:-- 0:00:02 --:--:-- 50
{
"hashrate": 17313.810700595,
"last_block": 1083778,
"network_hashrate": 26499835,
"pool_name": "LA Bitzeny Pool",
"shares_this_round": 119133,
"workers": 6284
}
巡回リストのプールそれぞれについてpublic
メソッドを呼び出し、
- エラーなくデータを取得できること
- 受信したデータがJSONとしてパースできること
を満たす場合に障害なしと判断することにします。
Stratumポート
マイニングソフトウェアがプールと通信する際は、Stratumという専用のプロトコルを使用します。TCPでデータをやり取りするので、プールが指定したポートとハンドシェイクできれば正常と判定できます。
例えば、LA Bitzeny PoolのBitZeny用アドレスの一つにstratum+tcp://jp.lapool.me:3014
があります。jp.lapool.meの3014番ポートを使っていることがわかるので、nc
コマンドなどで接続することができます。
$ nc -z jp.lapool.me 3014
Connection to jp.lapool.me port 3014 [tcp/broker_service] succeeded!
実装
慣れているNode.jsで実装しました。
リポジトリはこちらです。
APIのチェック
/*** check MPOS API reachability ***/
const checkAPI = async(uri) => {
const data = await new Promise((resolve) => {
request.get(uri, { timeout : config.timeout.api || 1000 }, (error, response, body) => {
if(error) { resolve({ error }); }
else { resolve({ body }); }
});
});
if(!data.error) {
try {
data.json = JSON.parse(data.body);
} catch(err) {
if(err.name === 'SyntaxError') { data.error = err.name; }
else { console.error(err); }
}
}
if(data.error) { return false; }
return true;
};
少しごちゃごちゃしていますが、request
でAPIを見に行って、エラーが起きたらdata.error
に入れています。取得したデータをJSONパーサに入力し、文法エラー(=MPOS APIが正常でない)になったらやはりdata.error
に書きます。
最後にdata.error
の有無を確認して判定完了です。
Stratumのチェック
/*** check Stratum Port reachability ***/
const checkStratum = async(host, port) => {
const portStatus = await portscanner.checkPortStatus(port, {
host,
timeout : config.timeout.stratum || 1000
});
return portStatus === 'open' ? true : false;
};
こちらの方がシンプルです。portscanner
というnc -z
と同じことをしてくれるモジュールがあるので導入しました。
ホスト、ポートを指定してcheckPortStatus()
を呼ぶだけでチェックが完了します。
ちなみに、用途別に複数のポートを持つプールも存在しますが、落ちるときは一緒に落ちるだろうという雑な推論により適当に一つを選んでチェックしています。
巡回先プールの設定
各プールについて、名称やURL、ホスト、Stratumのポートなどを設定しておく必要があります。
node-config
を使ってconfig/
以下の設定ファイル(YAML形式)を読み込んで使います。
まとめツイート用の短縮名や、鯖落ちや復帰をツイートするかの設定なども含めて以下のような具合です。
apipath: /index.php?page=api&action=public
timeout:
api: 10000
stratum: 1000
pools:
-
name: LA Bitzeny Pool
shortname: LA Pool
id: lapool
url: https://lapool.me/bitzeny
alert_enabled: true
stratum:
host: jp.lapool.me
port: 3014
...
プログラム側でconfig
をrequireするとconfigに上記の設定を入れてくれます。
新たに巡回先を増やしたい場合や、タイムアウトの待ち時間を変えたい場合でも設定ファイルを編集するだけでよく、メンテナンスの負担がかなり減りました。
なお、Twitter APIのトークンなど隠したい設定は、別途dotenv
で管理し、コミット対象から除外しています。
プールの状況チェック(checkCurrentStatus)
const checkCurrentStatus = async() => {
for(const pool of config.pools) {
console.info(`[${new Date()}] Checking ${pool.name}...`);
const previous = previousStatus[pool.id];
const current = { api : false, stratum : false };
for(let retry = 0; retry < MAX_RETRY; ++retry) {
current.api = await checkAPI(pool.url + config.apipath);
if(current.api) { break; }
}
for(let retry = 0; retry < MAX_RETRY; ++retry) {
current.stratum = await checkStratum(pool.stratum.host, pool.stratum.port);
if(current.stratum) { break; }
}
if(previous.api !== current.api || previous.stratum !== current.stratum) {
previousStatus[pool.id] = { api : current.api, stratum : current.stratum };
// ...(ツイート本文を作成してツイート)
}
}
};
各プールに対してWeb・Stratumのチェックをします。それぞれにループがあるのは、指定したリトライ回数(現在は3回)までは接続しようとするためです。
短時間繋がりにくいだけの状態を停止と判定しないための措置です。
previousStatus
に各プールの前回の状態が入っているので、状態が変化(停止or復旧)した時だけツイートを行います。
定期まとめツイート(tweetAllStatus)
こちらは、previousStatus
の中身に応じ、config/default.yaml
で指定したプール全ての状況をツイートします。
ツイート
node-twitter
でTwitterのAPIを叩きます。
手元でちょっと試すだけのときにはツイートして欲しくないので、DEBUG=1
として実行するとスキップするようにしました。
const postTweet = (status) => {
if(process.env.DEBUG) { return; }
bot.post('statuses/update', { status }, (err/*, tweet, response*/) => {
if (err) { console.error(err); }
});
};
定期実行とデーモン化
node-cron
とforever
です。どちらもド定番だと思うので説明は控えます。
cron.schedule('*/3 * * * *', () => {
checkCurrentStatus();
});
cron.schedule('0 * * * *', async() => {
tweetAllStatus();
});
$ forever start main.js
warn: --minUptime not set. Defaulting to: 1000ms
warn: --spinSleepTime not set. Your script will exit if it does not stay up for at least 1000ms
info: Forever processing file: main.js
反響と今後
思いつきで作ってとりあえずリリースしたようなBotですが、お陰さまで現在250人程度の方にフォロー頂いています。プールの利用者の方はもちろん、管理者の方も参考にしてくださっているようで嬉しい限りです。
最低限の障害警告ツイート機能だけで公開したのが12月12日頃になります。これは時期が悪く、どのプールも負荷激増で頻繁にタイムアウトするため、数分おきに大量の障害ツイートが流れてしまいました。
作ったばかりのアカウントで似た内容を大量に呟いているということで、**即刻スパム判定され一時アカウントをロックされました。**ロックは認証により解除されましたが、その後しばらく検索結果に表示して貰えませんでした
その後、
- ハッシュタグをつけるツイートを減らす
- 接続チェックする頻度を減らしてツイート頻度を抑制する
- 接続できなくてもすぐに異常と判定せず、3回まではリトライする
などの対策によりツイート頻度は徐々に落ち着いて(もちろん中の人達の努力によりプール自体が安定したのも大きいです!)いきます。
コミュニティーの方々もリツイートなどで広めてくださり、検索に出ずとも皆さんに知って頂けるようになりました。
今後も、新たなプールを追加していきたいと思っています。ただ、毎時のまとめツイートが既に140字ギリギリなので、分割ツイートする機能をつけなければなりません。
また、MPOSを使用しないプールもあり、APIの動作が異なるので別途対応することになりそうです。
明日最終日は、@you21979@githubさんです。