WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

JenkinsのSlaveが原因不明で切断されるのを力技で解決する

R&D事業部の岩原です。

今回は、クラッシュフィーバーのJenkinsの構成と、長い間悩まされ続けていた問題の解決案をご紹介したいと思います。

クラッシュフィーバーのJenkins構成

以前書いた記事に似た構成ではありますが、
大体こんな感じになってます。

f:id:m_iwahara:20161028160827p:plain

EC2上にJenkinsマスターを建て、スレーブとして社内MacMini2台とEC2を1台ぶら下げているような構成です。
MacMiniはそれぞれiOS用とAndroid用のアプリビルド&配布用サーバーへのアップロードを行うようにし、EC2はツールのデプロイ用にしています。
よくある構成ですね。

しかし、この構成にしてからずっと悩まされ続けている問題がありました。
その問題は、題名にもありますが「社内のSlaveが原因不明で切断される」というものです。

社内Slave切断問題

運用してしばらくすると、社内のMac MiniのスレーブがConnection Resetを吐いてよく切断されてしまう事象がよく発生していました。
吐き出されるログは以下の様なものでした。

10 27, 2016 5:26:29 午後 hudson.remoting.SynchronousCommandTransport$ReaderThread run
重大: I/O error in channel channel
java.net.SocketException: Connection reset
    at java.net.SocketInputStream.read(SocketInputStream.java:209)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
    at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
    at java.io.BufferedInputStream.read(BufferedInputStream.java:265)
    at hudson.remoting.FlightRecorderInputStream.read(FlightRecorderInputStream.java:82)
    at hudson.remoting.ChunkedInputStream.readHeader(ChunkedInputStream.java:72)
    at hudson.remoting.ChunkedInputStream.readUntilBreak(ChunkedInputStream.java:103)
    at hudson.remoting.ChunkedCommandTransport.readBlock(ChunkedCommandTransport.java:39)
    at hudson.remoting.AbstractSynchronousByteArrayCommandTransport.read(AbstractSynchronousByteArrayCommandTransport.java:34)
    at hudson.remoting.SynchronousCommandTransport$ReaderThread.run(SynchronousCommandTransport.java:48)

この状態になると、マスター側は接続していると認識しているが、スレーブ側は切断した扱いになってしまい、
ジョブのビルドが上手くいかなくなりました。
初めは、Jenkinsのバージョンが古いからか?と思いましたが、EC2側のスレーブは切断されたことが無かったので、
社内環境的な問題かと推測しましたが、未だに原因不明です。

このままではおちおち家に帰っていられないので、対処療法的に対応することにしました。

対応

Slave起動のスクリプト化

まず取り掛かったのは、Slave起動のスクリプト化です。
Slaveの処理はフォアグラウンドで動く&切断されるまで処理が戻ってこないので、それを利用して無限ループ化することにしました。
ただし、その場合再接続してもマスター側がSlave接続エラー状態になるまで「すでに接続しているよエラー」がマスターから帰ってきてしまうので、
再接続前にSlaveの切断をしてから再接続を行うようにしています。Slaveの切断はJenkins-cliを利用して行います。
また、規則性などの調査のため、Slaveの処理が終了した際はSlackへ通知をするようにしてから、再接続を行うようにしています。
こちらはシェルスクリプトで書くことにしました。
大体こんな感じです。

[JenkinsのURL]、[SSH秘密鍵へのパス]、[Slave名]、[シークレットキー]、 [SlackのInComming WebhookのURL]、[Jenkinsのログインユーザー名]、[Jenkinsのトークン]あたりは秘密情報なので伏せさせてもらいます。
Jenkinsに認証を掛けているため、Jenkins-cliではSSH秘密鍵、Jenkins WebAPIではAPIトークンを使用しています。
スクリプトと同じディレクトリに「slave.jar」、「jenkins-cli.jar」、「jenkins_slave_job_restarter.py(後述)」が存在している前提です。

#!/bin/bash
while :
do
java -jar jenkins-cli.jar -s [JenkinsのURL] -i [SSH秘密鍵へのパス] disconnect-node [Slave名]
java -jar -Dorg.jenkinsci.plugins.gitclient.Git.timeOut=120 slave.jar -jnlpUrl [JenkinsのURL]/computer/mac_slave/slave-agent.jnlp -secret [シークレットキー]
sleep 60
curl -X POST --data-urlencode 'payload={ "username": "[Slave名]", "text": "私は死にました(´・ω・`)", "icon_url": "http://mirrors.jenkins.io/art/jenkins-logo/48x48/logo.png"}' [SlackのInComming WebhookのURL]

python jenkins_slave_job_restarter.py [Jenkinsのログインユーザー名] [Jenkinsのトークン] [Slave名]

done

上記に出てくる[jenkins_slave_job_restarter.py]は後ほど紹介します。

スレーブで実行していたジョブの再実行

再接続時、Slaveで実行していたジョブは、切断されたことが検知できていないので、実行され続けてしまいます。
このままでは終了しないので、いったん該当Slaveのジョブのビルドを中断し、再度実行し直すようにします。
こちらはJenkins-cliでは出来ないので、JenkinsのWebAPIを使用します。
こちらはJsonなどを扱うのでPythonで書くことにしました。

[JenkinsのURL]は秘密情報なので伏せさせてもらいます。

# -*- coding: utf-8 -*-

import sys
import json
import urllib2
import base64

import os
import traceback

JENKINS_URL_BASE = "[JenkinsのURL]"

def start_job(user, pw, job_url):
    try:
        url = os.path.join(job_url, "../build")
        auth_header = 'Basic ' + base64.b64encode('{}:{}'.format(user, pw)).strip()
        headers = {'Authorization': auth_header}

        req = urllib2.Request(url, "\r\n" ,headers )
        urllib2.urlopen(req)
        return True
    except Exception as e:
        print(e)
        print(traceback.format_exc())
        return False

def stop_job(user, pw, job_url):
    try:
        url = os.path.join(job_url, "stop")
        auth_header = 'Basic ' + base64.b64encode('{}:{}'.format(user, pw)).strip()
        headers = {'Authorization': auth_header}

        req = urllib2.Request(url, "\r\n" ,headers )
        urllib2.urlopen(req)
        return True
    except Exception as e:
        print(e)
        print(traceback.format_exc())
        return False

def get_node_info(node_info_list_json, node_name):
    computer_list = node_info_list_json["computer"]
    node_info = [computer for computer in computer_list if computer["displayName"] == node_name]
    return node_info

def get_node_info_list(user, pw):
    url = os.path.join(JENKINS_URL_BASE, "computer/api/json?pretty=true&depth=1")
    auth_header = 'Basic ' + base64.b64encode('{}:{}'.format(user, pw)).strip()
    headers = {'Authorization': auth_header}

    req = urllib2.Request(url, "\r\n" ,headers )
    r = urllib2.urlopen(req)
    return json.loads(r.read())

def main(args):
    if len(args) < 4:
        print("引数が足りない")
        return 0
    user = args[1]
    pw = args[2]
    node_name = args[3]
    node_info_list_json = get_node_info_list(user, pw)
    node_info_json_list = get_node_info(node_info_list_json, node_name)
    if len(node_info_json_list) < 1:
        print("ノードが見つかりませんでした")
        return 0
    node_executors_list = node_info_json_list[0]["executors"]
    for executors in node_executors_list:
        if executors["currentExecutable"] is None:
            print("ビルドジョブなし")
            continue
        job_url = executors["currentExecutable"]["url"]
        ret = stop_job(user, pw, job_url)
        if not ret:
            print("ジョブの停止に失敗したわ")
            continue
        ret = start_job(user, pw, job_url)
    return 0


if __name__ == "__main__":
    args = sys.argv
    sys.exit(main(args))

ざっくり解説

Slaveで実行されているジョブを取得するためには、まずはSlaveのリストを取得する必要があります。
Slaveのリストは「computer/api/json」で取得できますが、depth=1を追加することで、さらに深い情報を取得することができ、Executor(ビルド実行状態)も合わせて取得できます。
あとは、コードを読んでいただければなんとなく理解できるかと。
なお、JenkinsのWebAPIはPython用の口もありますが、evalしてオブジェクト化するようなので、なんとなく避けました。

まとめ

上記で書いたシェルスクリプトとPythonスクリプトを組み合わせれば、SlaveがConnection Resetで突然切断されても、

Slave切断

Slaveで実行されているジョブの停止&開始

Slaveの再接続

を行うことができるようになります。

あくまでも対処療法なので、いつか原因を突き止めたいのですが、当面の間はしのげるかと思います。

ワンダープラネットでは、この問題を根本解決出来るような人やJenkinsおじさんを探してます(´・ω・`)