01 / 本文
結論:コマンドの失敗ではなく、接続が張れていなかった
毎日決まった時刻に走らせている同期処理が、断続的に失敗していた。4日のうち3日が失敗、1日だけ成功という状態だった。
ログの最後はこれだけだった。
dial tcp 203.0.113.10:10022: i/o timeout
コマンドが失敗したのではない。TCPの接続すら張れていない。 サーバー側の一時的な混雑か、接続数の制限に当たっていたと考えられる。
こういう失敗は、待って掛け直せば通ることが多い。
02 / 本文
接続の待ち時間が30秒だった
使っていたのは appleboy/ssh-action で、timeout の既定値は30秒だった。これは接続を確立するまでの待ち時間で、コマンドの実行時間(command_timeout)とは別物だ。
混雑しているときに30秒で見切ると、通るはずの接続まで落とすことになる。120秒に延ばした。
03 / 本文
再試行をどう書くか
appleboy/ssh-action に再試行の機能はない。外部のアクションを足す方法もあるが、依存を増やしたくなかった。
continue-on-error と steps.<id>.outcome を組み合わせると、追加の依存なしで書ける。
1回目と2回目は continue-on-error: true にして失敗を許す。3回目だけは普通に書き、ここで落ちたときにジョブを失敗させる。
間隔は3分と5分にした。混雑が理由なら、少し待てば状況が変わる。
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: 同期(1回目)
id: try1
continue-on-error: true
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
port: 10022
timeout: 120s # 接続の待ち時間(既定30秒)
command_timeout: 30m # コマンドの実行時間
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e
cd "${{ secrets.DEPLOY_PATH }}"
php artisan app:sync
- name: 3分待つ
if: steps.try1.outcome == 'failure'
run: sleep 180
- name: 同期(2回目)
id: try2
if: steps.try1.outcome == 'failure'
continue-on-error: true
uses: appleboy/ssh-action@v1.0.3
with: {} # 1回目と同じ設定を書く
- name: さらに5分待つ
if: steps.try1.outcome == 'failure' && steps.try2.outcome == 'failure'
run: sleep 300
# 3回目で落ちたら、そこで初めてジョブを失敗にする
- name: 同期(3回目)
if: steps.try1.outcome == 'failure' && steps.try2.outcome == 'failure'
uses: appleboy/ssh-action@v1.0.3
with: {}04 / 本文
outcome と conclusion の違い
continue-on-error: true を付けたステップでは、2つの値が食い違う。
outcome — 実際の結果。失敗していれば failure。
conclusion — continue-on-error を適用したあとの結果。失敗しても success になる。
再試行の判定に使うのは outcome だ。conclusion を見ると、1回目が失敗しても success に見えるので、2回目が走らない。
05 / 本文
冗長でも、依存を増やさない選択
同じブロックを3回書くので、見た目は冗長だ。ループで書けないのがGitHub Actionsの制約でもある。
それでも、この書き方を選んだ理由は2つある。
1. 依存が増えない。 再試行のために別のアクションを入れると、そのアクションの更新やセキュリティも見ることになる。
2. 何回目で通ったかがログに残る。 ステップが分かれているので、実行結果の一覧を見るだけで「1回目で通った」「3回目で通った」が分かる。接続の不安定さを継続的に観測できる。
06 / 本文
結果
修正後の初回実行は、こうなった。
同期(1回目)success / 同期(2回目)skipped / 同期(3回目)skipped
1回目で通った。再試行は出番なしだ。ただし、次に接続が失敗しても、1回のタイムアウトで止まることはなくなった。
毎日の処理が4日ぶりに通り、取り込むデータも増えた。
07 / 本文
まとめ
dial tcp のタイムアウトは、コマンドの失敗ではなく接続の失敗。待って掛け直せば通ることが多い。
appleboy/ssh-action の timeout は接続の待ち時間で、既定30秒。混雑するなら延ばす。
再試行は continue-on-error と outcome の組み合わせで、依存を増やさずに書ける。判定に使うのは conclusion ではなく outcome。
ステップを分けて書くと、何回目で通ったかがログに残る。不安定さの観測にもなる。
コメント
この記事へのコメント
お名前だけで投稿できます。アカウント登録やログインは不要です。 いただいたコメントは確認のうえ公開します。