このブログはこちらの英語ブログの機械翻訳です。

毎日数百万件のOkta Workflowsの実行を支えるエンジンは、非同期Rustエコシステムの早期採用者でした。現在では、Tokioが非同期ランタイムの自然な選択肢となっていますが、Workflowsをリリースしたのは、Tokio 1.0のリリース前であり、非同期Rustのベストプラクティスとパターンが本当に確立されるよりもずっと前のことでした。

この記事では、10万行を超えるソースコードからなる Workflows エンジンを、サービスの中断や機能開発の一時停止なしに、continuation ベースの Tokio 0.1 から async / await Tokio 1.0 に移行した方法について詳しく説明します。

継続ベースの futures

初期の頃、Rust futures の非同期実行は、最終的な値を表す関数(継続と呼ばれる)を渡すことによって行われました。これらの継続は連鎖され、Tokio などのランタイムによって非同期的に実行され、最終的に目的の値に解決されます。移行前は、次のようなコードが数十万行ありました。
 

移行前のコード

 

これは、async / await 構文が安定化するまでの async Rust の状態でした。Workflows にとって、早期導入者であるということは、async / await の安定化よりもずっと前に出荷する必要があったことを意味します。このタイプのコードは読んだり保守したりするのが難しいのですが、可能な限りプッシュしました。

Okta Workflows を今日の姿に成長させるのに役立ちました。時が経ち、約 1 年前、亀裂が耐え難くなり始め、最終的に最新の async / await 構文と Tokio 1.0 への移行を開始する必要が生じました。

Async / await ベースの futures

Tokio 1.0 では、非同期制御フローは、(現在では長期的に安定している)async / await 構文で記述でき、コードが大幅に簡素化され、ボイラープレートが削減されます。上記の例は、次のように記述できます。

 

Tokio 1.0と同じコード

 

Rustにあまり詳しくなくても、async / awaitを使用することの読みやすさとシンプルさの利点は明らかです。Rustに詳しい方にとっては、async / awaitがawaitポイントを越えて借用できるため、Flowオブジェクトの所有権を渡す必要がなくなり、FlowErrorに保存する必要もなくなりました。これは、エラーを顧客に伝える上で非常に重要であり、コードパスが必然的にFlowをエラーに保存するのを忘れた場合、多くのバグの原因となりました。これは、async / awaitに移行することで排除できるバグの一例にすぎません。

大きなリフト

Tokio 1.0への移行のメリットは明らかでしたが、それを行うのは困難な作業でした。移行は簡単ではなく、Workflows Engineの完全な書き換えが必要になります。ビジネスと顧客の信頼性の両方の観点から、停止を伴う根本的な書き換えは非現実的で危険です。完全な書き換えを行うために製品開発を一時停止することはできませんでした。実際、これはソフトウェア製品にとって致命的な間違いでしょう。

さらに問題を複雑にしているのは、リライトの調査を行っていた頃に、新機能が続々と登場していたことです。たとえば、Tokio 1.0への移行作業と並行して、わずか数か月後にはWorkflowをキャンセルする機能をリリースする予定でした。これほどの規模のリライトは、まるで走行中の列車で車輪を交換するように、少しずつ行う必要がありました。

互換性レイヤーと機能フラグ

Okta Workflowsは、hyper HTTPライブラリを使用してWebと通信し、Google DriveAmazon S3などの外部サービスを呼び出します。これらすべてがWorkflowsを非常に強力にしている理由の一部です。この基盤となるライブラリは、最初に置き換えられたものの1つでした。Tokio 0.1を搭載したhyper 0.12を、Tokio 1.0およびasync / awaitに基づいて構築されたhyper 0.14に置き換えました。これは、Workflows EngineのTokio 0.1からTokio 1.0に移植された最初の部分の1つであり、この経験は移行プロセス中の多くのアプローチとアーキテクチャの決定に役立ちました。

Rust の非同期エコシステムのほとんどを支える futures ライブラリには、継続ベースの futures から 互換性シム に移行するのに役立つ一連の async / await ベースの futures があります。ただし、これらのシムはランタイムの違いを考慮に入れることができません。当社の継続ベースのレガシーコードは Tokio 0.1 ランタイムでのみ実行できましたが、この新しい HTTP 処理コードは Tokio 1.0 ランタイムで実行する必要があり、決して交わることはありません。Tokio 1.0 は、Tokio 0.1 エグゼキューターで実行される互換性 future を渡されると、永久にハングします。

したがって、間接的な追加の手順が必要でした。「asyncification」の初期の頃、Workflows で HTTP コネクターカードを使用する場合、フロー処理は最初に Tokio 0.1 コンテキスト内で発生していました。HTTP リクエストを行う場合、Tokio 0.1 は Shim を呼び出し、Shim は別のスレッドで実行されている Tokio 1.0 ランタイムでリクエストを生成し、チャネルを介してリクエストが返されるのを待っていました。これにより、あるランタイム用である future を別のランタイムで混在させることを回避できました。Tokio 0.1 の実装とライブラリを Tokio 1.0 と互換性のあるものに徐々に置き換えていくにつれて、すべての Flow 関数呼び出し、および Redis へのすべての呼び出しが同様の Shim を通過するというこのアプローチを後で限界まで追求しました。

 

Tokio 0.1からTokio 1.0への移行プロセス

 

互換性シムは一方向でした。レガシーfuturesコードからasync / awaitコードを呼び出すには、async / awaitコードからレガシーfuturesコードを呼び出すのとは異なるシムが必要でした。

お客様に信頼性の低下や動作の違いを感じさせないようにするために、機能フラグを最大限に活用して、コードパスのレガシー実装とTokio 1.0実装をランタイム時に切り替えました。問題が発生した場合は、新しいコードの問題に対処している間、以前の実装にすばやく切り替えることができました。互換性シムは、async / await構文で書き換える準備ができるまで、2つの世界間の有用なファイアギャップを提供しました。レガシーパスとTokio 1.0パスを並行して実行し、レガシーコードパスを非推奨にすることで、Workflows Engineの基盤を徐々に置き換えることができました。

課題

Tokio 1.0への移行の過程で、Tokio 0.1の世界とTokio 1.0の間で互換性シムを迅速に作成するための小さなツールキットを最終的に構築しました。これは、書き換え作業にもかかわらず、新機能の提供を継続する上で、非常に重要なツールとなりました。

Workflows Engine のコア部分がこの書き換えを受けている間、その間にリリースされた機能は、Tokio 0.1 で 1 回、Tokio 1.0 で 1 回の 2 回書き換える必要がありました。互換性シムを作成するためのツールキットを使用することで、Tokio 1.0 で新しいコードをすべて記述し、既存の Tokio 0.1 コードベースにブリッジすることができました。場合によっては、シムだけでは不十分でした。たとえば、前述のキャンセルフローの場合、両方のランタイムでコードパスをサポートするために、実装の一部を2回記述する必要がありました。

レガシーTokio 0.1とTokio 1.0のランタイムには、対応する必要のあるいくつかの重要な違いがありました。Tokio 0.1では、すべてのfutureがヒープに割り当てられていましたが、async / awaitを使用すると、futureはデフォルトでスタックに割り当てられるようになりました。注意しないと、ワーカースレッドのスタックがすぐにオーバーフローしてしまいます。デフォルトのワーカースレッドのスタックサイズを大きくし、ヒープに割り当てるfutureを慎重に選択することで、この問題を軽減できました。

従来の Tokio 0.1 コードパスでは、複数のエグゼキューターが並行して動作し、高度な同時実行出力を実現していました。Tokio 1.0 を使用した初期の実装では、すべてのフロー実行を処理するために単一のエグゼキューターのみを使用しました。これは、複数のエグゼキューターを使用するよりも約 15% 遅いことが判明しましたが、それ自体は印象的です。最終的には、お客様が期待するのと同じレベルのスループットを可能にするために、複数の Tokio 1.0 ランタイムに戻しましたが、今では、Tokio 0.1 とレガシーコードパスで可能だったことを超えてプッシュするために、将来調整するためのいくつかの新しいノブがあります。

より高速でクリーンなコードベース

現在、Workflowsでは、従来のTokio 0.1に大きく依存することはなくなりました。当社のコードベースは、これまで以上に高速で、読みやすく、保守しやすくなっています。これにより、フローの実行パフォーマンスを最適化し、新しい機能をこれまで以上に迅速にお客様に提供する機会が増えました。

 

時間の経過に伴うコードの正味行数を示すグラフ

 

async / await に移行することで、Workflows Engine から 20,000 行以上のコードを削除することができました。

ついに長らく無視されてきたTokio 0.1の世界から抜け出し、基盤となる依存関係を常に最新の状態に保つことができるようになりました。これは、Oktaが世界で最も安全な企業になるための取り組みにおいて非常に重要です。そしてもちろん、私たちのチーム全体にとって、何千行もの定型コードがあるものよりも、クリーンで読みやすいコードベースで毎日作業する方がはるかに楽しいです。

Okta Workflows を使用して、重要な IT およびセキュリティタスクを大規模に自動化する方法について詳しくはこちらをご覧ください。

このブログ投稿について質問がありますか?eng_blogs@okta.comまでご連絡ください。

Oktaからのより洞察に満ちたエンジニアリングブログを探索して、知識を広げてください。

卓越したエンジニアの情熱的なチームに参加しませんか?採用情報ページをご覧ください。

組織向けの最新かつ洗練されたアイデンティティ管理の可能性を解き放ちます。

詳細については、営業にお問い合わせください。

アイデンティティ施策を推進