クラウドコンピューティングとマイクロサービスアーキテクチャの急速な発展の文脈では、セキュリティ、スケーラビリティ、高パフォーマンスを保証しながら、さまざまなプログラミング言語のコードを動的に実行する機能を提供する必要性が高まっています。この記事では、分離された環境でコード実行を実装するプロジェクトについて説明し、最新のWEB IDEに選択されたアーキテクチャソリューションの利点について説明します。このシステムは、
システムの主要コンポーネントがどのように構成されているか、代替ソリューションとどのように異なるか、これらのテクノロジを選択するとなぜ高いパフォーマンスとセキュリティを実現できるのかを詳しく説明します。
このプロジェクトは、機能を独立したサービスに分割できるマイクロサービス アーキテクチャの原則に基づいて構築されています。各コンポーネントは高度に専門化されたタスクを担当し、システムの柔軟性、スケーラビリティ、フォールト トレランスを保証します。
主なコンポーネント:
Goの利点:
パフォーマンスとスケーラビリティ: Go は実行速度が速く、これは多数の並列リクエストを処理する場合に特に重要です。
組み込みの同時実行サポート: ゴルーチンとチャネルのメカニズムにより、複雑なマルチスレッド パターンを使用せずにコンポーネント間の非同期の相互作用を実装できます。
gRPC の利点:
比較: REST API とは異なり、gRPC はサービス間のより効率的で信頼性の高い通信を提供します。これは、高度な同時実行システムにとって重要です。
なぜRedisなのか?
高いパフォーマンス: Redis は 1 秒あたり多数の操作を処理できるため、タスク キューや結果キューに最適です。
Pub/Sub およびリストのサポート: キューとサブスクリプション メカニズムの実装が簡単なため、サービス間の非同期のやり取りを簡単に整理できます。
他のメッセージ ブローカーとの比較: RabbitMQ や Kafka とは異なり、Redis は必要な構成が少なく、リアルタイム システムに十分なパフォーマンスを提供します。
Docker の役割:
環境の分離: Docker コンテナを使用すると、完全に分離された環境でコードを実行できるため、実行の安全性が向上し、メイン システムとの競合のリスクが軽減されます。
管理性と一貫性: Docker を使用すると、ホスト システムに関係なく、コードをコンパイルおよび実行するための同じ環境が提供されます。
比較: ホスト上で直接コードを実行するとセキュリティ上のリスクが生じ、依存関係の競合が発生する可能性がありますが、Docker を使用するとこれらの問題を解決できます。
このプロジェクトではマイクロサービス アプローチを採用しており、次のような大きな利点がいくつかあります。
リモート コード実行用の最新の WEB IDE を構築する場合、さまざまなアーキテクチャ ソリューションが比較されることがよくあります。次の 2 つのアプローチを検討してみましょう。
アプローチ A: マイクロサービス アーキテクチャ (gRPC + Redis + Docker)
特徴:
このアプローチは、高速で信頼性の高いサービス間通信、コード実行の高度な分離、コンテナ化による柔軟なスケーリングを実現します。応答性とセキュリティが重要な最新の WEB IDE に最適です。
アプローチ B: 従来のモノリシック アーキテクチャ (HTTP REST + 集中実行)
特徴:
初期のバージョンの Web IDE でよく使用されていたモノリシック ソリューションは、HTTP REST と集中コード実行に基づいています。このようなシステムでは、スケーリングの問題、レイテンシの増加、および他のユーザーのコードを実行する際のセキュリティの確保の難しさなどが発生します。
注: WEB IDE 開発の最新の状況では、HTTP REST と集中実行のアプローチは、必要な柔軟性とスケーラビリティを提供しないため、マイクロサービス アーキテクチャの利点よりも劣ります。
グラフは、マイクロサービス アーキテクチャ (アプローチ A) がモノリシック ソリューション (アプローチ B) と比較して、レイテンシが低く、スループットが高く、セキュリティとスケーラビリティが優れていることを明確に示しています。
システムのセキュリティと安定性の重要な要素の 1 つは、Docker の使用です。当社のソリューションでは、すべてのサービスが個別のコンテナにデプロイされるため、次のことが保証されます。
以下は、システムの仕組みを示すコードの主要セクションの縮小バージョンです。
システムはグローバル レジストリを使用し、各言語に独自のランナーがあります。これにより、ランナー インターフェイスを実装して登録するだけで、新しい言語のサポートを簡単に追加できます。
package languages import ( "errors" "sync" ) var ( registry = make(map[string]Runner) registryMu sync.RWMutex ) type Runner interface { Validate(projectDir string) error Compile(ctx context.Context, projectDir string) (<-chan string, error) Run(ctx context.Context, projectDir string) (<-chan string, error) } func Register(language string, runner Runner) { registryMu.Lock() defer registryMu.Unlock() registry[language] = runner } func GetRunner(language string) (Runner, error) { registryMu.RLock() defer registryMu.RUnlock() if runner, exists := registry[language]; exists { return runner, nil } return nil, errors.New("unsupported language") }
新しい言語を登録する例:
func init() { languages.Register("python", NewGenericRunner("python")) languages.Register("javascript", NewGenericRunner("javascript")) }
したがって、リクエストを受信すると、システムは以下を呼び出します。
runner, err := languages.GetRunner(req.Language)
コードを実行するための対応するランナーを受け取ります。
各ユーザー コード要求ごとに、個別の Docker コンテナーが作成されます。これは、ランナー メソッド内 (たとえば、Run) で実行されます。コンテナーを実行するための主なロジックは、RunInDockerStreaming 関数にあります。
package compiler import ( "bufio" "fmt" "io" "log" "os/exec" "time" ) func RunInDockerStreaming(image, dir, cmdStr string, logCh chan < -string) error { timeout: = 50 * time.Second cmd: = exec.Command("docker", "run", "--memory=256m", "--cpus=0.5", "--network=none", "-v", fmt.Sprintf("%s:/app", dir), "-w", "/app", image, "sh", "-c", cmdStr) cmd.Stdin = nil stdoutPipe, err: = cmd.StdoutPipe() if err != nil { return fmt.Errorf("error getting stdout: %v", err) } stderrPipe, err: = cmd.StderrPipe() if err != nil { return fmt.Errorf("error getting stderr: %v", err) } if err: = cmd.Start();err != nil { return fmt.Errorf("Error starting command: %v", err) } // Reading logs from the container go func() { reader: = bufio.NewReader(io.MultiReader(stdoutPipe, stderrPipe)) for { line, isPrefix, err: = reader.ReadLine() if err != nil { if err != io.EOF { logCh < -fmt.Sprintf("[Error reading logs: %v]", err) } break } msg: = string(line) for isPrefix { more, morePrefix, err: = reader.ReadLine() if err != nil { break } msg += string(more) isPrefix = morePrefix } logCh < -msg } close(logCh) }() doneCh: = make(chan error, 1) go func() { doneCh < -cmd.Wait() }() select { case err: = < -doneCh: return err case <-time.After(timeout): if cmd.Process != nil { cmd.Process.Kill() } return fmt.Errorf("Execution timed out") } }
この関数は、docker run コマンドを生成します。
したがって、ランナーの Run メソッドを呼び出すと、次のことが起こります。
コード実行のメインロジックの最小化されたフラグメント (executor.ExecuteCode):
func ExecuteCode(ctx context.Context, req CodeRequest, logCh chan string) CodeResponse { // Create a temporary directory and write files projectDir, err: = util.CreateTempProjectDir() if err != nil { return CodeResponse { "", fmt.Sprintf("Error: %v", err) } } defer os.RemoveAll(projectDir) for fileName, content: = range req.Files { util.WriteFileRecursive(filepath.Join(projectDir, fileName), [] byte(content)) } // Get a runner for the selected language runner, err: = languages.GetRunner(req.Language) if err != nil { return CodeResponse { "", err.Error() } } if err: = runner.Validate(projectDir); err != nil { return CodeResponse { "", fmt.Sprintf("Validation error: %v", err) } } // Compile (if needed) and run code in Docker container compileCh, _: = runner.Compile(ctx, projectDir) for msg: = range compileCh { logCh < -"[Compilation]: " + msg } runCh, _: = runner.Run(ctx, projectDir) var output string for msg: = range runCh { logCh < -"[Run]: " + msg output += msg + "\n" } return CodeResponse { Output: output } }
この最小限の例では、
これらの重要なフラグメントは、システムが拡張性(新しい言語の簡単な追加)をサポートし、リクエストごとに個別の Docker コンテナーを作成することで分離を提供する方法を示しています。このアプローチにより、プラットフォームのセキュリティ、安定性、スケーラビリティが向上します。これは、最新の WEB IDE にとって特に重要です。
この記事では、gRPC + Redis + Docker スタックを使用したマイクロサービス アーキテクチャ上に構築されたリモート コード実行プラットフォームについて説明します。このアプローチにより、次のことが可能になります。
比較分析により、マイクロサービス アーキテクチャは、すべての主要な指標において従来のモノリシック ソリューションを大幅に上回るパフォーマンスを発揮することが示されています。このアプローチの利点は実際のデータによって確認されており、高性能でフォールト トレラントなシステムを作成するための魅力的なソリューションとなっています。
著者: オレクシイ・ボンダル
日付: 2025–02–07