3
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

BitZenyマイニングプールの死活監視をするTwitter Bot

この記事は、暗号通貨 Advent Calendar 2017の24日目の記事です。
昨日の記事は、@redshogaさんのまだ急変動で消耗してるの? 暗号通貨(仮想通過)の急変動をつぶやくbotを作ったお話でした。なんと2日連続でBotの話題ですね。


作ったもの

BitZenyプール稼働状況Botというのを作りました。

こんな感じで、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の制作

プールと接続できない時は、言及したツイートがないか検索するのですが、すぐ気付く人がいるとは限りません。どのプール落ちているのか、即座に分かるような仕組みがなかったため自作することにしました。

自分が欲しいから作るにせよ、折角なので公開の場で動かそうということでTwitterのBotとして開発することにしました。
専用サイトと違い鯖落ち情報がすぐに共有できますし、利用者がアクセスするのはTwitterなので負荷を考える必要がない利点がありますね。

障害発生の判定

プールを使う際、マイナーが接続する先は二種類あります。

  1. Webのダッシュボード画面
    • プールのWebサイトで、ログインして統計閲覧や各種設定が可能
  2. 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のチェック

main.js
/*** 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のチェック

main.js
/*** 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形式)を読み込んで使います。

まとめツイート用の短縮名や、鯖落ちや復帰をツイートするかの設定なども含めて以下のような具合です。

config/default.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)

main.js
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として実行するとスキップするようにしました。

main.js
const postTweet = (status) => {
  if(process.env.DEBUG) { return; }
  bot.post('statuses/update', { status }, (err/*, tweet, response*/) => {
    if (err) { console.error(err); }
  });
};

定期実行とデーモン化

node-cronforeverです。どちらもド定番だと思うので説明は控えます。

main.js
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日頃になります。これは時期が悪く、どのプールも負荷激増で頻繁にタイムアウトするため、数分おきに大量の障害ツイートが流れてしまいました。
作ったばかりのアカウントで似た内容を大量に呟いているということで、即刻スパム判定され一時アカウントをロックされました。ロックは認証により解除されましたが、その後しばらく検索結果に表示して貰えませんでした :sweat_smile:

その後、

  • ハッシュタグをつけるツイートを減らす
  • 接続チェックする頻度を減らしてツイート頻度を抑制する
  • 接続できなくてもすぐに異常と判定せず、3回まではリトライする

などの対策によりツイート頻度は徐々に落ち着いて(もちろん中の人達の努力によりプール自体が安定したのも大きいです!)いきます。
コミュニティーの方々もリツイートなどで広めてくださり、検索に出ずとも皆さんに知って頂けるようになりました。

今後も、新たなプールを追加していきたいと思っています。ただ、毎時のまとめツイートが既に140字ギリギリなので、分割ツイートする機能をつけなければなりません。
また、MPOSを使用しないプールもあり、APIの動作が異なるので別途対応することになりそうです。


明日最終日は、@you21979@githubさんです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?