Software Design 202309
JavaScript 非同期処理
非同期処理の変遷
- 変遷
- コールバック
- Promise チェーン
- async/await
- 非同期処理の構成要素
- タスク
- プログラムの実行単位
- キュー
- タスクキュー
- イベント
- タスクが格納されるタイミングに関係する
- プログラムが特定の状態になったときに発生する
- e.g. ボタンがクリックされた、通信処理が完了した、など
- タスク
なぜ非同期処理は難しいのか
- 非同期処理の目的
- ブロッキングを防ぎながら、効率的なコードを書くこと
- 並行と並列
- 並行 / Concurrency / ある時間内に複数の処理を実行すること
- 並列 / Parallelism / あるタイミングで複数の計算が実際に実行されている
- 並行だけど並列でないとか、並列だけど並行でないとかが、ありえる
- 非同期処理には 2 つの種類がある
- JavaScript の機能として提供されるもの
- e.g. Promise, async / await
- JavaScript の範疇で動くため、シングルスレッドである
- JavaScript の機能は基本的に並行であり並列ではない
- 待ち時間を重ね合わせて効率化している程度の話
- 実行環境から API として提供されるもの
- e.g.
setTimeout()
など - JavaScript の範疇ではないため、マルチスレッド・マルチプロセスもありうる
- 同期 API と非同期 API がある。後者が非同期処理の本質
- 種類
- Web API / Browser API
- 標準仕様がある
- ブラウザ環境でも非ブラウザ環境でも動くことが多い
setTimeout()
,fetch()
など
- Runtime API
- 標準仕様がない
- 非ブラウザ環境で独自実装されている
- Node.js の
fs
やhttp
など - 便宜上そう呼んだだけで正式名称ではない
- Web API / Browser API
- e.g.
- JavaScript の機能として提供されるもの
- タスクの種類
- タスク
- コード全体の初回評価、UI レンダリング、
setTimeout()
など - 優先度が低い
- コード全体の初回評価、UI レンダリング、
- マイクロタスク
Promise.then()
など- 優先度が高い
- タスク
- 実行順
- イベントループ
- 1 つのタスクを終わらせる
- マイクロタスクがあればすべて片付ける
- 繰り返す
- イベントループ
Promise とは
then()
の仕組み- 新たな Promise オブジェクトを返す
- コールバック関数が:
- Promise を返したら、そのオブジェクト
- Promise 以外を返したら、その値を解決値とする Promise オブジェクト
- 例外を送出したら、その値を失敗値として返す Promise オブジェクト
- コールバック関数が:
catch()
はthen()
の糖衣構文であるthen()
の第 2 引数に失敗時のコールバック 関数を渡した場合と同じ
- 新たな Promise オブジェクトを返す
- このあたりは自前で Promise を実装してみると一発で分かる
async / await 深掘り
- async / await は単なる then の糖衣構文ではなく、動作が全く異なるので注意
- for 文の中で利用した場合など
- 余談だが、
then()
という名前の関数は使わないほうが吉
AbortSignal
- Promise を命令的に中断する手段
- AbortSignal というオブジェクトで制御する
- 使える場所の例:
fetch()
の第 2 引数内window.addEventListener()
の第 3 引数内
- 使える場所の例:
- JavaScript ではなく DOM 仕様の一部
- だが Node.js 環境等にも広く移植されている
- signal により中断された場合、非同期処理の結果は失敗として扱われるのが通常
- DOMExeption という例外が発生する
- name が
AbortError
である
- 実装
- 内部処理が fetch 等の AbortSignal に対応しているコード場合は、そのまま渡すだけ
- 対応していない場合は、以下のようなプロパティを使いながら自前で実装する
signal.aborted
signal.addEventListener()
signal.reason
signal.throwIfAborted()
- AbortSignal の作成
- AbortController というオブジェクトを介してつくるのが一般的
const controller = new AbortController()
const signal = controller.signal
- 中止済みの AbortSignal を作る
const signal = AbortSignal.abort()
- 一定時間後に中止する AbortSignal を作る
const signal = AbortSignal.timeout(1000)
- AbortSignal の合成
const signal = AbortSignal.combine(signal1, signal2)
- 現状、v116 以降の Chrome でのみ使える?
- AbortController というオブジェクトを介してつくるのが一般的
AsyncLocalStorage
- Node.js 等で利用可能。ブラウザでは使えない。
- 1 つの非同期コールスタックに対して 1 つ、共有可能な値を保持・提供するためのもの
- 用途
- 構造化ロギング
- ロギングはメインの処理と直交する性質がある
- ロギングのために引数が増減したりインターフェースが変わったりするのは好ましくない
- 依存注入
- 引数を変えることなくピンポイントで依存を注入できる
- 構造化ロギング
- コールバック地獄になりがちなので注意
const transactionIdStorage = new AsyncLocalStorage();
transactionIdStorage.run({ transactionId: '123' }, async () => {
// これらの関数の中では`transactionIdStorage.getStore()`とすることで
// { transactionId: '123' } が取得できる
await doSomething();
await doSomething2();
await doSomething3();
});
カオスエンジニアリング
入門
- カオスエンジニアリングとは
- 自信を持つための訓練方法である
- 本番環境で実験する
- 本番環境における不安定な状態にシステムが耐えうるかを検証する
- Netflix 発祥
- カオスとは
- 分散システムに内在する、予測できない障害を引き起こすもの
- 分散システム / ネットワーク越しに複数のコンピュータで構成されたシステム
- カオス実験
- 正常な状態の指標を測定
- 2 つのグループの一方にカオスを注入する
- 2 つのグループ間の指標を比較する
- テストとの違い
- 新しい発見が得られるかどうか
- なぜカオスエンジニアリングか
- クラウドの不安定さ
- マイクロサービスの不安定さ
- オブザーバビリティとの関係
- オブザーバビリティとは
- どの程度システムを理解・説明できるかを示す尺度
- たとえシステムが意味不明な挙動をしていたとしても
- マイクロサービスでは従来手法でのデバッグはできない
- テレメトリー
- ログ
- メトリクス
- トレース
- プロファイル
- カオスエンジニアリングにはオブザーバビリティが必須、じゃないとただのカオス
- オブザーバビリティとは
- DevOps との関係
- カオスエンジニアリングは CI/CD に続く CV/継続的検証というカテゴリとして確立しつつある
- SRE との関係
- SLI / Service Level Indicator / システムの信頼性を示す指標
- e.g. API レスポンスタイム
- SLO / Service Level Objective / SLI の目標値
- e.g. 99% のリクエストが 1 秒以内に返る
- カオスエンジニアリングはこれらを改善するためのプロアクティブなアプローチ
- SLI / Service Level Indicator / システムの信頼性を示す指標
- 導入にあたっての検討事項
- システムが十分に複雑か
- 大規模マイクロサービスでないなら不要
- DepOps や SRE の取り組みで十分
- タイミングは適切か
- オブザーバビリティは十分でないならまずはそちらを先に
- 大規模障害 後や、大規模イベント前は最適
- 必要なスキル
- SRE が比較的近い
- システムが十分に複雑か
(あとは省略、必要になったら読む)
脆いテスト
- 自動テストを書く動機
- 不具合混入を防止する
- 問題箇所の絞り込みを容易にする
- 動く仕様書になる
- ソフトウェアの成長を持続可能なものにする(コレが一番大事)
- 変更しやすく、変更に対応できるソフトウェアが競争力になる時代だから
- 保守性
- モジュール性
- 再利用性
- 解析性(理解容易性)
- 修正性(変更容易性)
- 試験性(テスト容易性)
- 自動テストは最後の 2 つを向上させる
- 保守性とは、現状維持力ではなく、反応速度、改善力、推進力と考えろ
- よくある意見
- 設計が変わりやすいのでテストを書くコストが高い
- テストの修正コストが大きい
- 設計が固まらないからテストが書けない
- 脆いテスト
- 偽陽性のひとつ
- コードが正しいにもかかわらずテストが失敗する誤検知のこと
- ちな、コードに一切手を加えていないのにテストが失敗するのは「信頼不能テスト(Flaky Test)」という
- 脆いテストの原因
- テストコードとテスト対象の高すぎる構造的結合度
- 指針
- 公開 API 経由でテストする
- 実装の詳細はテストしない
- 構造単位ではなく振る舞い単位でテストする
- クラスやメソッド、関数単位でテストを書かない
- 振る舞いや責務に対してテストを書く
- 1 つの関数が 3 つの仕事を請け負うなら 3 つのテストを書く
- 相互作用ではなく事後状態をテストする
- テスト対象の依存対象までモックしてテストなどをやり出すのは諸刃の剣
- あくまで外部から見た振る舞いをテストする
- e.g. テスト対象のメソッドの戻り値
- e.g. テスト対象のメソッド実行後の状態
- 公開 API 経由でテストする
AWS のクラウドセキュリティ
- クラウドセキュリティとは
- 以下の集合体
- セキュリティポリシー
- ベストプラクティス
- テクノロジー
- クラウド環境内のアプリケーション、データ、その他リソースを保護するためのもの
- 以下の集合体
- 責任共有モデル
- クラウドのセキュリティは AWS が担保する
- クラウド内のセキュリティはユーザーが担保する
- Iaas, Paas, Saas の順に、ユーザーの責任範囲は軽くなる
- 代表的なセキュリティ事故要因
- 公開された静的かつ長期間有効なクラウド認証情報
- 認証無しでインターネットに公開されて いる Elasicsearch インスタンス
- 認証無しでパブリックからアクセスできるデータストレージサービス
SRE プラクティス: Renovate による依存関係の更新
- なぜ依存関係を更新するのか
- セキュリティ
- 対応コスト
- ちょくちょく対応していれば 1 回あたりのコストは下がる
- 副次的に情報のキャッチアップにもなる
- ツール
- Dependabot
- Renovate
- auto merge, PR のグルーピング、正規表現による更新ルールなどが魅力的
- しくみ
- コード(e.g.
package.json
)を監視しバージョンを確認 - ルールに従って最新かどうか判定する
- 古ければ PR を作成する
- コード(e.g.
- 設定
- 設定ファイル
- スケジュール、レビュアー、PR のラベル、タイムゾーンなど
- json5 でも書けるので補完が効く
- マネージャー
- 言語やツールごとの処理が定義されたモジュール
- npm, terraform などの種類がある
- 正規表現で振る舞いを定義できる regex manager というのもある
- 設定ファイル
- 組み込み方法はいくつかある
- Github App で実行 (簡単、コンソールも使える、セキュリティ的なリスクは 微増)
- Github Actions などで実行
- ローカル実行
以下、応用編
- こんなこともできる
- リポジトリをまたいで設定を共有する
- 設定ファイルの検証をする
- Github でのオートマージする
- 正規表現で更新ルールを独自定義する
- 運用の工夫
- PR をマージするための時間を確保しておく
- CI 関連ライブラリの patch, minor 更新はオートマージする
- Test, Lint, 静的解析の類
- CD が絡むものはだめ