コマンド直列化スクリプト
並列で動いているバッチプロセスが叩く外部コマンド(ffmpeg)がメモリーをバカ食いするケースが有って、タイミングが重なるとサーバーメモリーが枯渇するのでそこだけ直列化した。
作ってみたら便利だったので記事にした
#!/usr/bin/bash set +e PID_FILE=/tmp/`echo $1 | sed 's/\//_/g'`.pid LOCK_FILE=/tmp/`echo $1 | sed 's/\//_/g'`.lock WAITING='1' while [ "$WAITING" = '1' ]; do if [ -e $LOCK_FILE ]; then echo 'Locked' sleep 1 continue fi touch $LOCK_FILE exec 9> $LOCK_FILE flock -n 9 if [ "$?" = "1" ]; then echo 'Flocked' sleep 1 continue fi WAITING="0" echo touch $LOCK_FILE if [ -e $PID_FILE ]; then PID=`cat $PID_FILE` if [ "$PID" != '' ]; then ps -v -p $PID | grep -c $1 > /dev/null if [ "$?" = "0" ]; then echo "Wait for $PID ($1)" WAITING='1' rm $LOCK_FILE sleep 1 fi fi fi done echo '====START====' echo "$*" $* & echo $! > $PID_FILE rm $LOCK_FILE set -e wait `cat $PID_FILE` echo '====END===='
mac の場合は brew install flock
が必要。
axiosのソケットリークでハマった
このシンプルなコードでソケットリークが起こる事に気付いた。
実際には膨大なコードの中からココに疑いも持った段階で9合目って感じだが・・・
const axios = require('axios'); const consumers = require('stream/consumers'); const get = (url) => { return new Promise(async (resolve, reject) => { try { const res = await axios.get(url, {responseType: 'stream'}); resolve(await consumers.buffer(res.data)); } catch (e) { reject(e); } }); };
responseType: 'stream'
を指定した場合は、Stream (Socket)がまだ開いていて自前で処理してあげなきゃならない。
consumers.buffer(res.data)
は、普通にchunk処理してBlobに渡してるだけだが、Streamを最後まで読んでいるのでcloseまで到達しているはずである。
ここは問題ない
問題はここ
} catch (e) { reject(e); }
盲点だったのだが、接続エラーで到達するならば問題ないが、なんとBad Request などでも到達するのだ。当然ペイロードがある。
そして responseType: 'stream'
ならば未処理のStreamである。
コイツがリークする!!
例外で飛んできてるので、response が受け取れてないが、例外の中に埋められている。
const _ = require('lodash'); const axios = require('axios'); const consumers = require('stream/consumers'); const get = (url) => { return new Promise(async (resolve, reject) => { try { const res = await axios.get(url, {responseType: 'stream'}); resolve(await consumers.buffer(res.data)); } catch (e) { const socket = _.get(e, 'response.data.socket'); if (socket) { socket.destroy(); } reject(e); } }); };
これで解決。
全然情報が無かったのだけど、、
レスポンスが正常に返ってくれば問題ないから気付き難いだけで、みんなリーク喰らってるんじゃないかな?
dockerコンテナ(ubuntu 20.04 LTS arm64)内でChromiumを動かすのが大変だった件
経緯
ubuntu 18.04 LTSのサポートが1年を切ったのでubuntu 20.04 LTSにアップデートしようとしていた。
実サーバー側の検証はそれ程大きな問題はなかったがCI環境でE2Eテストを行う部分で、コンテナ内でChromium headlessを使っており、これが思いの外難産だった。
技術的背景
1. Google Chromeが使えない
本来、googleがバイナリ配布しているChromeが使えれば良いのだが、Linux arm64バイナリは提供されていない。 なのでChromiumを使う必要がある
2. Ubuntu 19.10以降、ChromiumのAPT(deb)パッケージが廃止
バイナリはsnap版に移行し、debパッケージはsnap installに迂回するようになった。
3. Dockerコンテナ内でsnapが使えない
普通にDocker buildするとsnapdが立てられないのでsnap install が失敗する。
# snap install chromium error: cannot communicate with server: Post http://localhost/v2/snaps/chromium: dial unix /run/snapd.socket: connect: no such file or directory
解決法
Dockerコンテナでsnapdを動かす
偉い人が作ったDockerコンテナジェネレータがあったので、これを改造して使ってみた。
これを紐解くと以下のようなDockerfileになる。
なるほど、、systemd経由でsnapdを起動する構造はそのままで、/sbin/init
から上げて行くわけね。
Dockerfile
FROM --platform=linux/arm64 ubuntu:20.04 ENV container docker ENV PATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y fuse snapd snap-confine squashfuse sudo init && apt-get clean && dpkg-divert --local --rename --add /sbin/udevadm && ln -s /bin/true /sbin/udevadm RUN systemctl enable snapd VOLUME ["/sys/fs/cgroup"] STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"]
ハマった点
無事snapdは上がり、snapコマンドは通る
snap list Name Version Rev Tracking Publisher Notes core 16-2.56.2+git3949.06393d8a6 13434 latest/edge canonical** core
ところが、実際のinstallは失敗する
# snap install chromium error: cannot perform the following tasks: - Run configure hook of "chromium" snap if present (run hook "configure": aa_is_enabled() failed unexpectedly (No such file or directory): No such file or directory)
調べていくと、workaroundを提示している人が居た
# mount -t securityfs securityfs /sys/kernel/security
確かに、/sys/kernel/security
が空だったのでmountすることで見事に解決した。
# snap install chromium chromium 102.0.5005.115 from Canonical✓ installed
しかしDockerfileで解決出来ないのは困ったな。。
MongoDBの統一トポロジー(useUnifiedTopology)について
最近のMongoDBドライバーはエラーが出る
(node:8746) [MONGODB DRIVER] Warning: Current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. To use the new Server Discover and Monitoring engine, pass option { useUnifiedTopology: true } to the MongoClient constructor.
で、その新トポロジーの説明はこれ。
すごくザックリ内容を紹介すると
- 接続中(connected)という状態とはなんぞや?
- 実際のネットワークが接続状態という事に意味はあるのか?
- 別にになっていなくても処理可能な状態を保っていれば良いよね?
- だからisConnectedも将来廃止するよ。
- 直ちに処理出来ない状態でもドライバー内でなんとか辻褄取るよ
- でも失敗したら30秒位でエラー返すよ
なので、useUnifiedTopology
と autoReconnect
を両方指定すると怒られる。
(node:8962) [MONGODB DRIVER] DeprecationWarning: The option `autoReconnect` is incompatible with the unified topology, please read more by visiting http://bit.ly/2D8WfT6
新しいトポロジーでは、利用者側は接続状態を気にしなくて良い事になっているからね。
ザックリ図解
現行ReplicaSet
readPreferenceに合致するコネクションを割り当てる事しかしない。
一度コネクションを取得した後は素通し。
コネクションプール内のコネクションが全部エラーで破棄されるまでエラーが続く。
新統一トポロジー
1層咬んでいるのでエラーにならない所を勝手に探してくれる。
全部ダメならタイムアウトserverSelectionTimeoutMS
までリトライする。
実際の挙動
createConnection
現行ReplicaSet
require('mongoose').createConnection('mongodb://localhost:27017/foo', { autoReconnect: true, readPreference: 'secondaryPreferred' })
直ちにエラーが帰る
MongoNetworkError: failed to connect to server [localhost:27017] on first connect [Error: connect ECONNREFUSED 127.0.0.1:27017 at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1161:16) at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) { name: 'MongoNetworkError' }]
新統一トポロジー
require('mongoose').createConnection('mongodb://localhost:27017/foo', { useUnifiedTopology: true, readPreference: 'secondaryPreferred', serverSelectionTimeoutMS: 30000, })
約30秒後にこうなる
> Uncaught MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017 at NativeConnection.Connection.openUri (/****/node_modules/mongoose/lib/connection.js:847:32) at Mongoose.createConnection (/****/node_modules/mongoose/lib/index.js:291:17) { reason: TopologyDescription { type: 'Unknown', setName: null, maxSetVersion: null, maxElectionId: null, servers: Map(1) { 'localhost:27017' => [ServerDescription] }, stale: false, compatible: true, compatibilityError: null, logicalSessionTimeoutMinutes: null, heartbeatFrequencyMS: 10000, localThresholdMS: 15, commonWireVersion: null } }
fetch
図解の通りに動くので、動作確認のコード例だけ。
このコードを動かしてレプリカセットのノードを落としたり上げたりすればいい。
新統一トポロジーの方は全ノードを落として、再起動せずにタイムアウトに抵触するまではエラーが出ない。
現行ReplicaSet
let conn = null; require('mongoose').createConnection('mongodb://localhost:27018/foo', { replicaSet: 'RS', autoReconnect: true, readPreference: 'secondaryPreferred'}).then(r => conn=r).catch(r => console.log('---', r)); (async () => { for(let i = 0; i < 100000;i++) { try { let r = await conn.db.collection('tests').findOne({i: 1}); console.log(i, r); } catch(e) { console.log(e); } } })();
新統一トポロジー
let conn = null; require('mongoose').createConnection('mongodb://localhost:27018/foo', { replicaSet: 'RS', useUnifiedTopology: true, readPreference: 'secondaryPreferred'}).then(r => conn=r).catch(r => console.log('---', r)); (async () => { for(let i = 0; i < 100000;i++) { try { let r = await conn.db.collection('tests').findOne({i: 1}); console.log(i, r); } catch(e) { console.log(e); } } })();
注意
良い改善だとは思うが、ハイロードな環境下では、30秒ものオペレーションを貯めたり再送して重複処理するのは危険極まりない。
しっかり設計せずに移行するのは危険だ。
10TBクラスのmongodb ReplicaSet 運用ナレッジ
オススメの構成
[primary] - priority = 2 [secondary] - priority = 1 : (必要なだけ) : [backup] - priority = 0 - hidden = true
POINT
1. Primaryが好き勝手変動すると運用上の取り回しが悪いのでなるべく同じノードがPrimaryになるようにする
2. バックアップ中はOplog反映遅延が起こるのでクライアントクエリーをさせないhidden属性にしておく
3. 増強やバックアップや復旧を円滑に行うため、全てのノードでデータディレクトリは別のEBSボリュームにしておく
例えば10TBクラスのEBSでヘビーユースのReplicaSetだと毎日差分スナップショットを取っても4時間ほど掛かりバックアップ後にOpTimeが追いつくまで5~6時間かかる。
スナップショット(バックアップ)のとり方
データ整合性を確保するために、スナップショット中の書き込みを抑止する必要がある。
rs:PRIMARY> db.fsyncLock(); ** ここでスナップショットを取る ** rs:PRIMARY> db.fsyncUnlock();
スクリプトにしてcron.dailyにでも放り込んでおくと良い
#!/bin/bash cd /path/to bash snapshot.sh --tmp mongo --host backup-prd1 -u root -p `cat safe_file` <<<'db.fsyncLock()'; bash snapshot.sh mongo --host backup-prd1 -u root -p `cat safe_file` <<<'db.fsyncUnlock()';
EBSの場合、差分スナップショットなので直前に一回捨てスナップショットを取っておくと差分が小さくなり、lock期間を短くする事が出来る。
snapshot.shは、awsコマンドなりSDKなりで適当に実装すればいい
Oplog設定・設計
config
replication: oplogSizeMB: 614400 #600GB
起動中に変更する場合
db.adminCommand({replSetResizeOplog: 1, size: 614400})
サイズの決め方
以下のコマンドで確認しながら最低3日分位は確保する。理由は当記事を読んでいけば解る。出来れば7日分程度欲しいが更新が激しい場合は非効率になってしまう場合もある。
rs:PRIMARY> rs.printReplicationInfo() configured oplog size: 614400MB log length start to end: 183206secs (50.89hrs) oplog first event time: Mon Jul 05 2021 21:52:36 GMT+0900 (JST) oplog last event time: Thu Jul 08 2021 00:46:02 GMT+0900 (JST) now: Thu Jul 08 2021 00:46:02 GMT+0900 (JST)
うわ。。足りてないね・・・
oplog first event time
がこのノードが保持している一番古いOplogなのでこれ以上遅れてしまったSecondaryはもう同期出来なくなる。
遅延状態の確認方法
rs:PRIMARY> rs.status().members.map((member)=> [member.name, member.optimeDate.toLocaleString()]) [ [ "primary-prd1:27017", "Thu Jul 8 00:16:10 2021" ], [ "secondary-prd1-prd4:27017", "Thu Jul 8 00:16:09 2021" ], [ "secondary-prd2:27017", "Thu Jul 8 00:16:09 2021" ], [ "secondary--prd3:27017", "Thu Jul 8 00:16:09 2021" ], [ "backup-prd1:27017", "Wed Jul 7 16:18:33 2021" ] ]
ノードが壊れた場合
パターン
1. データファイル破損
エラーになってmongodが起動しない
→ ノード再構築
2. [rsBackgroundSync] replSet error RS102 too stale to catchup...
反映しなければならないOplog古すぎ、ReplicaSetメンバーの誰も持っていない
状態は1. と同じ。
3. [initandlisten] Taking 999 samples and assuming that each section of oplog contains approximately...
mongodを再起動した際に、エラーにはならないがこのログで止まる
→ ひたすら待つ。1日待っても良い。Secondary復帰後もOplog反映が遅くどんどん遅延して行くが1日位で正常なパフォーマンスに戻り追いつき始める
ノード再構築
1. 単純にデータディレクトリ以下を全て消す
この場合のmongodの挙動は以下
1. mongod起動(STARTUP2)
2. 他のreplicaSetメンバーから接続され同期が始まる(STARTUP2)
3. 同期元のノードからCollection毎にデータ取得 -> Index構築
を繰り返す(STARTUP2)
4. 全てのDB/Collectionの構築が終わったら2.時点のOplogを起点に差分を反映する(STARTUP2)
5. Oplogが追いついたら完了(SECONDARY)
大きなCollectionでIndexが大量あれば、3.は時間がかかる。
私の運用しているReplicaSetでは余裕で2週間はかかるのでOplogを巨大にしない限り4.の段階で Stale
となる。
それをやったとしてもOplogが追い付くまでに掛かる時間も膨大で完了まで1ヶ月以上は覚悟しなければならない。
TBクラス以上の大きなReplicaSetでは現実的ではない。
2. なるべく新しいスナップショットからデータディレクトリを復旧(本命)
この場合のmongodの挙動は以下
1. mongod起動(STARTUP2)
2. データディレクトリ上の最後のcheckpointの状態でデータ復旧。1日近く掛かる事もある
3. 2.終了時に直ちにSECONDARYとなるがSnapshotを取った時点のデータなのでOpTimeが激しく古い状態(SECONDARY)
4. 通常のReplicaSetメンバーと同様の扱いでOplog反映を始める(SECONDARY)
3. の時点でクライアントクエリーを処理するとセマンティック上の問題が起きるので、プロセス起動前に rs.reconfig()
を行いhiddenノードにしておく必要がある
4.が始まっても原因不明のパフォーマンス劣化状態が続く。1日程度で通常のパフォーマンスになるので3日分程度のOplogは必ず確保する必要がある。
Oplog遅延見積もり
snapshotに掛かる時間(5時間)+(3.)に掛かる時間(24時間)+(4.)開始後24時間で2時間程度しか反映が進まない = 5 + 24 + 24 - 2 = 最大51時間遅延する
手順
SECONDARYが壊れたら?
1. rs.reconfig()
で壊れたノードをhiddenに変更する
2. dailyで取っているバックアップノードのスナップショットからノード再構築
3. rs.reconfig()
で再構築したノードのhiddenを解除する
バックアップノードが壊れたら?
1. rs.reconfig()
で1つのSECONDARYをhiddenに変更する
2. hiddenセカンダリーの捨てスナップショットを2〜3回取る(1回目はフルスナップショット、2回目は長時間のフルスナップショット分の差分、3回目は2回目のスナップショットも本来最短で取れる差分よりは大きい分の差)
3. fsyncLock()
を掛けて本スナップショットを取り完了後にfsyncUnlock()
4.3.のスナップショットからバックアップノード再構築
5.3.で起きた遅延が解消したら、rs.reconfig()
で再構築したノードのhiddenを解除する
ARM環境まとめ
最近のARM(aarch64)周り
この1年で本番環境を全てarm64サーバーに切り替えたりDocker環境を作ったりしてarm64周りのナレッジが溜まってきた。
ハマり続けたメモ。思い出したら追記する
ブチ当たった問題の情報がネットに無い場合も多く、もしかしたらarm64周りを弄ってるエンジニアの中でもカッティングエッジに近い所まで来てるんじゃないかな・・・・
ビジネス環境
勢いがある。時間を先行投資する価値を感じる。
株主不安
SoftbankがARMの競合と目されているNVIDIAへ売却
AWS
Graviton2インスタンスが提供されており、intel CPUインスタンスより2割程安く、HWアクセラレータ周りも充実している
ElasticCache(Redis)
5%程割安で、しかし明らかに2倍くらい速度が遅い。
Redisは1CPUしか使わないので影響が出やすいのと、Speculative_executionの差なのか?
Apple
M1チップ搭載のMacが販売開始
Linux
amd64 / arm64
良く目検に失敗する・・・aarch64に統一してほしい・・・
Ubuntuパッケージ
ミラーサーバーがaarch64をサポートしていない場合がある
原因特定までメチャクチャ時間がかかる
Ubuntu 日本語環境
ubuntu-ja.listが使えない
Docker on Mac
ARM版のLinuxも一応動く
しかしカーネルエミュレータ周りの互換性がイマイチでそこかしこでエラーを吐く
遅い、固有の問題多すぎ。
互換性に問題のあるdockerコンテナなぞ価値なし!
Chrome
Chromeが提供されておらず、Chromiumを使うしかない
Ruby webdrivers
何も考えずx86_64のライブラリを引っ張ってきて起動でコケる
Feature specではOSにインストール済みのChromiumをバイナリを使う
JS karma
環境変数CHROME_BINにインストール済みのChromiumバイナリのパスを指定
JS puppeteer
launchのパラメータexecutablePathにインストール済みのChromiumバイナリのパスを指定
screen
化石エンジニア御用達の仮想コンソール
なぜかアタッチに分単位の時間がかかる
ElasticSearch
docker imageは7.8以降のみ対応
完全対応は7.13以降
大体、2020年6月以前のバージョンは非対応
7.13も起動が異常に遅い問題を確認している
rubyjs/libv8
https://github.com/rubyjs/libv8/issues/261
RailsでJS実行する際に必要だがビルド出来ない問題が2018年から放置
docker使って自前ビルドしたlibv8をvender/cacheに放り込ん動かす
1時間超えのCIを10分以下にした話
経緯
今までwercker
を使っていたのだが、数年前は良いサービスだったのにOracleなんかに買われたせいで最近はインスタンスが目に見えて遅かったり、不安定だったりやってられないので、引っ越しを検討していた。
比較したサービス
wercker
最近明らかに遅いインスタンスが混ざり込む様になった
(超相乗り状態?)
- 並列実行数=2
- 1vCPU 2GB RAM
無料プランのみ??
情報が出てこない・・・
CircleCI
言わずと知れた最大手。CI業界の基準
ピュアなコンテナ環境ではなさそう(chrootっぽい何か?)
その影響か、ホストのカーネルの問題か解らないが、稀にCircleCI無いでしか起きないSEGV等が起きたり互換性周りの面倒事が起きる。
実行環境のリソースは細かく選択でき、料金が異なる。
料金はクレジットの単位で設定されており
25000クレジット$15で購入するので
$15 / 25000credit = $0.0006 / credit
resource class
Class | vCPUs | RAM | credit |
---|---|---|---|
small | 1 | 2GB | 5 / 分 |
medium (default) | 2 | 4GB | 10 / 分 |
medium+ | 3 | 6GB | 15 / 分 |
large | 4 | 8GB | 20 / 分 |
xlarge | 8 | 16GB | 40 / 分 |
2xlarge(2) | 16 | 32GB | 80 / 分 |
2xlarge+(2) | 20 | 40GB | 100 / 分 |
なぜか、料金表が見当たらず、、赤字は予想。
Free Plan
- 無料枠 2500 クレジット / week
- 並列実行数=1
- 3ユーザまで
- Mediumのみ
よって無料枠は
250分 / 週 = 4.2時間 / 週
- パブリックリポジトリの場合 400000クレジット/月を付与
Performance Plan
- 並列実行数=80
- 3ユーザ($15)+ ユーザ毎($15)
- Docker レイヤー キャッシュへのアクセス(実行毎200クレジット)
- 1口辺り25000クレジット($15)x 毎月最低2口 = $30〜
仮にsmallを100時間稼働させた場合(他にも、dockerイメージのキャッシュで実行毎200クレジット掛かったりするが、イメージDLと実行時間の相殺の問題なので一旦無視。)
(ユーザ料金)$15 + 6000分 * 5credit * $0.0006 = $33(支払いは$45)
となる。
スタートアップなら此位で収まるプロダクトも多いだろう。
エンジニアが少ない場合でも、多い場合でも、ユーザ毎$15が重くなりがちな料金体系(少数エンジニアで生産性が高ければお得)
セルフホスティング
どうやら自前のホスティング環境で実行することも出来るようだが、わざわざCircleCIを選ぶ理由が思いつかないので、調べていない。
Github actions
今まで仲良くしていたCIサードパーティに手の平を返して牙を向いたGithub様の刺客!
バックエンドはMicrosoft Azure。
明らかにインフラリソースの優遇を受けているであろうダンピング価格でCI環境を提供している。
パブリックリポジトリは全部無料
プライベートリポジトリでも巨大な無料枠が用意されている。
rootのHOMEが"/home/github" になっておりイメージの環境調整が面倒
プライベートリポジトリの場合は、JOBの実行時間とストレージ使用量がそれぞれ無料枠+従量の料金体系になっている。CIとして使う場合はストレージ使用量は気にならず、JOB実行時間だけ考えれば良い。
ジョブ実行時間の従量部分は 0.008$ / 分
Free
- 無料枠2000分 / 月、ストレージ500MB
- 並列実行数=20
Pro(月額$4)
- 無料枠3000分 / 月、ストレージ1GB
- 並列実行数=20
仮に100時間稼働させた場合(細かいことを言えば、どのCIを使おうが掛かる月額4$は除外しても良いが・・・)
$4 + 3000分 * $0 + 3000分 * $0.008 = $28
一見、CircleCIより少し安い位だけど、プルリが少なくて50時間程度だった場合はほぼ無料になってしまう。
セルフホストランナー
サーバを自前で用意する。無料
CIが走った時にspot instanceでランナーを起動したいが、ランナーをGithubに接続する際に必要なワンタイムトークンを生成するAPIが提供されていないので困難。
実際の引っ越しの成果
元々の状態
71分、これは早いインスタンスを引いた時で、ハズレを引くと3倍遅いしハズレ率が5割以上。
安定しないCIなんぞ糞である
Github actionsに引っ越し
65分、あまり変わらないように見えるがハズレが無く安定したのでかなりのストレス軽減
だが、、もっと早くなるのが解っている!
並列化
.github/workflows/main.yml
name: CI on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: jobs: test: runs-on: ubuntu-latest strategy: max-parallel: 12 matrix: test_script: - run_sherpajs.sh - run_sherpa_server.sh - run_sherpa_admin_advertisement_br.sh - run_sherpa_admin_advertisement_br_multi.sh - run_sherpa_admin_advertisement_br_movie.sh - run_sherpa_admin_advertisement_sn.sh - run_sherpa_admin_advertisement_internal.sh - run_sherpa_admin_action_widget.sh - run_sherpa_admin_campaigns_sherpa.sh - run_sherpa_admin_campaigns_company_user.sh - run_sherpa_admin_others.sh - run_sherpa_admin_report.sh container: image: crumbjp/sherpa_ci credentials: username: crumbjp password: ${{ secrets.DOCKERHUB_PASSWORD }} steps: - uses: actions/checkout@v2 - name: prepare run: bash ci/prepare.sh - name: services run: bash ci/services.sh - name: run ${{ matrix.test_script }} run: bash ci/run_parallel.sh ${{ matrix.test_script }}
マトリックスを使うと並列テストが作れる。
12分!これが無料だから恐ろしい!!
しかし12並列ともなると10分のテストで120分ものジョブ時間を消費するので、我々の開発状況では3000分の無料枠は数日で使い切ってしまった。このペースだと従量課金で$300/月程度になりそうなのでセルフホストランナーも検討。
t4g.medium (vCPUx2 4GB RAM) x 720時間 = $31.1
2ランナー/サーバー x 6 = 12ランナー $186.6
コスト的には検討に乗りそうだ。
ARMのdocker imageは色々苦労があるがそれはまた別の機会に・・・
中を見るとInitialize containers(docker イメージDL)に結構時間を掛けている。
Githubが用意しているランナーに我々のdockerイメージがキャッシュされている可能性はほぼ無い。
セルフホストランナーであればここが削れるはずだ。
セルフホストランナー化
10分切った!
我々のプロダクトでは固定のStaging環境を維持しており、これをセルフホストランナーに流用して元々余らせていたリソースを活用。
本番サーバーでもリザーブドインスタンスの期間の関係でリソースを余らせているサーバーを幾つか見繕って流用。 (一時的な処置)
結果、実質の出費無しで12本のランナーを確保することができた。
目論見通りInitialize containersは数秒に収まっている。
またランナーにジョブが飛んでくるまでに30秒〜2分位のラグがあるようだ。
ローカルマシンより早い
今までローカルテストで確認してからプルリを作っていたが、もうカジュアルにプルリ作ってCIでテスト通す方が楽。 しかも料金は気にしなくて良い。
開発スタイルがガラっと変わりました。
もう腿を火傷する必要は無い!!
まとめ
1. CI環境は多くの場合、github actions が良い選択
2. ヘビーユースならばセルフホストランナーの方が良い
3. 並列テスト最高