- 定義
- 影響分析
- パフォーマンスレビュー
- 枠にとらわれない
- データセット
- クエリプランとデータベース構造
- クエリ数
- 可能な限りリードレプリカを使用
- CTEを賢く使う
- キャッシュクエリ
- クエリのループ実行
- バッチ処理
- タイムアウト
- データベーストランザクションを最小限に
- 熱心なロード
- メモリ使用量
- UI 要素の遅延レンダリング
- キャッシュの使用
- ページネーション
- バッジカウンター
- 機能フラグの使用法
- ストレージ
マージリクエストのパフォーマンスガイドライン
新しく導入されるマージリクエストは、デフォルトでパフォーマンスが高いものであるべきです。
マージリクエストがGitLabのパフォーマンスに悪影響を与えないようにするために、_すべての_マージリクエストはこの文書で説明されているガイドラインに従うべきです。バックエンドのメンテナーとパフォーマンススペシャリストが特に話し合って合意しない限り、このルールに例外はありません。
また、以下のガイドを読むことも強く推奨します:
定義
RFC 2119による用語SHOULD
:
この言葉、あるいは「推奨される」という形容詞は、特定の状況において、特定の項目を無視する正当な理由が存在するかもしれないが、別の道を選択する前に、その意味を完全に理解し、注意深く吟味しなければならないことを意味します。
理想的には、このようなトレードオフをそれぞれ個別のイシューに文書化し、それに応じてラベルを付け、元のイシューとエピックにリンクさせるべきです。
影響分析
まとめ:あなたのマージリクエストがパフォーマンスやGitLabのセットアップをメンテナーしている人たちに与える影響について考えてみましょう。
提出された変更は、アプリケーションそのものだけでなく、それをメンテナーとしている人たちや稼働させている人たち (たとえばプロダクションエンジニア) にも影響を与える可能性があります。そのため、マージリクエストがアプリケーションだけでなく、それを維持・稼働させている人々にも与える影響について慎重に考える必要があります。
使用されるクエリは、クリティカルなサービスをダウンさせる可能性があり、エンジニアが夜間に起こされるような結果になるでしょうか?悪意のあるユーザーがコードを悪用して GitLab インスタンスをダウンさせることはできますか?私の変更によって特定のページの読み込みが遅くなることはありますか?データベースに十分な負荷やデータがある場合、実行時間は指数関数的に増加しますか?
これらはすべて、マージリクエストを提出する前に自分自身に問いかけるべき質問です。影響を評価するのが難しい場合もありますが、その場合はパフォーマンスの専門家にコードのレビューを依頼すべきです。詳細については、以下の「レビュアー」のセクションを参照してください。
パフォーマンスレビュー
要約:影響について確信が持てない場合は、パフォーマンスの専門家にコードのレビューを依頼してください。
マージリクエストの影響を評価するのが難しいことがあります。このような場合は、マージリクエストレビューアーにあなたの変更をレビューしてもらうべきです。(レビュアーはパフォーマンススペシャリストに変更をレビューするよう依頼することができます。
枠にとらわれない
新機能の使い方は人それぞれです。ユーザーがその機能をどのように使うかを常に考えてください。通常、ユーザーはブルートフォースやエッジコンディションの悪用など、非常に型破りな方法で私たちの機能をテストします。
データセット
マージリクエストが処理するデータセットは既知であり、文書化されるべきです。この機能が処理するデータセットがどのようなもので、どのような問題を引き起こす可能性があるのかを明確に文書化する必要があります。
処理されるデータセットに重点を置いた次の例について考えてみましょう。Git リポジトリにあるファイルの一覧をフィルタリングしたいのです。あなたの機能は、リポジトリにあるすべてのファイルのリストを要求し、そのファイル群を検索します。作成者として、この問題の文脈で次のことを考えるべきです:
- サポートされる予定のリポジトリは?
- Linux kernelのような大きなリポジトリにはどれくらいの時間がかかりますか?
- このような大きなデータセットを処理しないために、何か別の方法があるのでしょうか?
- 計算の複雑さを抑制するフェイルセーフ・メカニズムを構築すべきでしょうか?通常、すべてのユーザーではなく、一人のユーザーに対してサービスを低下させる方が良いのです。
クエリプランとデータベース構造
クエリプランから、インデックスの追加や(シーケンシャルスキャンなどの)高価なフィルタリングが必要かどうかを知ることができます。
各クエリプランは、相当な大きさのデータセットに対して実行されるべきです。例えば、特定の条件のイシューを探す場合、クエリを少数のイシュー(数百件)と多数のイシュー(100_000件)に対して検証することを検討すべきです。結果が数個と数千個の場合、クエリがどのように動作するかを確認してください。
これは、GitLab を非常に大きなプロジェクトや型破りな方法で使っているユーザーがいるために必要なことです。そのような大きなデータセットが使用される可能性は低いと思われるとしても、私たちの顧客の一人がこの機能で問題に遭遇する可能性は十分にあります。
たとえそれを受け入れたとしても、規模が大きくなったときにどのような挙動を示すかを前もって理解しておくことは、望ましい結果です。私たちは常に、より高い使用パターンに対して機能を最適化するために必要な計画や理解を持つべきです。
すべてのデータベース構造は最適化されるべきであり、簡単な拡張に備え、時には過剰に記述されるべきです。ある時点から最も難しいのはデータマイグレーションです。数百万行のマイグレーションは常に面倒で、アプリケーションに悪影響を及ぼしかねません。
クエリプランのレビューの手助けを得る方法をよりよく理解するために、データベースレビューのためのマージリクエストの準備方法に関するこのセクションを読んでください。
クエリ数
要約:マージ・リクエストは、絶対に必要な場合を除き、実行された SQL クエリの総数を増加させるべきではありません。
マージリクエストによって変更または追加されたコードによって実行されるクエリの総数は、絶対に必要な場合を除き、増加してはなりません。機能をビルドする際に追加のクエリが必要になることは十分ありえますが、最小限に抑えるようにしなければなりません。
例として、データベースの複数の行を同じ値で更新する機能を導入するとします。これを次のような擬似コードで書くのはとても魅力的でしょう(そして簡単でしょう):
objects_to_update.each do |object|
object.some_field = some_value
object.save
end
これは、更新するオブジェクトごとに1つのクエリを実行することを意味します。更新するオブジェクトごとに1つのクエリを実行することになります。更新する行数が十分であるか、このコードのインスタンスが多数並列に実行されている場合、このコードは簡単にデータベースに過負荷をかけることができます。この特定の問題は、“N+1 クエリ問題” として知られています。QueryRecorderを使ってテストを書くことで、この問題を検出し、リグレッションを防ぐことができます。
この場合、回避策はとても簡単です:
objects_to_update.update_all(some_field: some_value)
ActiveRecordのupdate_all
メソッドを使用して、1回のクエリですべての行を更新します。これにより、このコードがデータベースをオーバーロードすることが難しくなります。
可能な限りリードレプリカを使用
DBクラスターには多くのリードレプリカとプライマリがあります。DBをスケーリングする典型的な用途は、読み取り専用のアクションをレプリカで実行することです。この負荷を分散するためにロードバランシングを使用します。これにより、DBへの負荷が高まるにつれてレプリカを増やすことができます。
デフォルトでは、クエリは読み取り専用のレプリカを使いますが、プライマリが固着しているため、GitLabはしばらくの間プライマリを使い、追いつくか30秒後にセカンダリに戻します。これは、プライマリに不必要な負荷をかけることになります。プライマリへの切り替えを防ぐためにマージリクエスト56849は without_sticky_writes
ブロックを導入しました。通常、この方法は、同じセッションの次のクエリに影響を与えないような些細な書き込みの後にプライマリが固着するのを防ぐために適用されます。
使用タイムスタンプの更新がどのような場合にセッションをプライマリに固着させるのか、また、without_sticky_writes
を使用してそれを防ぐ方法については、マージリクエスト57328を参照してください。
without_sticky_writes
ユーティリティと対をなすものとして、マージリクエスト59167は use_replicas_for_read_queries
を導入しました。このメソッドは、現在のプライマリの固着度に関係なく、そのブロック内の全ての読み取り専用クエリにレプリカを読み取らせます。このユーティリティはクエリがレプリケーションの遅延を許容できる場合に使用されます。
内部的には、データベースロードバランサはクエリをメインステートメント(select
、update
、delete
など)に基づいて分類します。疑わしい場合は、クエリをプライマリデータベースにリダイレクトします。そのため、ロードバランサがクエリを不必要にプライマリに送るケースがよくあります:
- カスタムクエリ (
exec_query
,execute_statement
,execute
など) - 読み取り専用トランザクション
- 機内接続設定
- Sidekiqバックグラウンドジョブ
上記のクエリが実行された後、GitLabはプライマリに固執します。内部クエリがレプリカを好んで使うようにするために、マージリクエスト59086で fallback_to_replicas_for_ambiguous_queries
。このMRは、高コストで時間のかかるクエリをレプリカにリダイレクトした例でもあります。
CTEを賢く使う
CTEの使用方法については、リレーション・オブジェクトの複雑なクエリについてを参照してください。CTE を使用すると問題が発生する場合があることが分かっています (上記の N+1 の問題と同様です)。特に、AuthorizedProjectsWorkerのCTEのような階層的な再帰CTEクエリは、最適化が非常に困難であり、スケールしません。何らかの階層構造を必要とする新機能を実装する際には、このようなクエリは避けるべきです。
CTEは、この例のような多くの単純なケースで最適化フェンスとして効果的に使用されています。現在サポートされているPostgreSQLのバージョンでは、MATERIALIZED
キーワードで最適化フェンスの動作を有効にする必要があります。デフォルトでは、CTEはインライン化され、最適化されます。
CTE文を構築する際には、Gitlab::SQL::CTE
GitLab 13.11で導入されたクラスを Gitlab::SQL::CTE
使用してください。デフォルトではGitlab::SQL::CTE
、この Gitlab::SQL::CTE
クラスはMATERIALIZED
キーワードを追加することで実体化を強制します。
キャッシュクエリ
要約: マージ・リクエストは、重複するキャッシュ・クエリを実行すべきではありません。
RailsはSQLクエリキャッシュを提供しており、リクエストの間、データベースクエリの結果をキャッシュするのに使います。
キャッシュされたクエリが良くないとされる理由と、それを検出する方法をご覧ください。
マージリクエストによって導入されたコードは、重複した複数のキャッシュクエリを実行すべきではありません。
マージリクエストによって変更または追加されたコードによって実行されるクエリ (キャッシュされたクエリを含む) の総数は、絶対に必要な場合を除き、増加すべきではありません。実行されるクエリ (キャッシュされたクエリを含む) の数は、コレクションのサイズに依存してはなりません。QueryRecorderにskip_cached
変数を渡してテストを書くことで、これを検出し、リグレッションを防ぐことができます。
例として、CIパイプラインがあるとします。すべてのパイプラインのビルドは同じパイプラインに属しており、したがって、それらは同じプロジェクト (pipeline.project
) にも属しています:
pipeline_project = pipeline.project
# Project Load (0.6ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
build = pipeline.builds.first
build.project == pipeline_project
# CACHE Project Load (0.0ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
# => true
build.project
を呼び出すと、データベースにはアクセスせず、キャッシュされた結果を使用しますが、同じパイプラインプロジェクトオブジェクトを再度インスタンス化します。関連するオブジェクトは同じメモリ内オブジェクトを指していないことがわかります。
各ビルドをシリアライズしようとすると
pipeline.builds.each do |build|
build.to_json(only: [:name], include: [project: { only: [:name]}])
end
同じインメモリオブジェクトを使用するのではなく、ビルドごとにプロジェクトオブジェクトをインスタンス化し直します。
この場合、回避策はとても簡単です:
ActiveRecord::Associations::Preloader.new(records: pipeline, associations: [builds: :project]).call
pipeline.builds.each do |build|
build.to_json(only: [:name], include: [project: { only: [:name]}])
end
ActiveRecord::Associations::Preloader
同じプロジェクトには同じメモリ内オブジェクトを使用します。これにより、SQLクエリのキャッシュを回避し、ビルドごとにプロジェクト・オブジェクトを再インスタンスする必要がなくなります。
クエリのループ実行
要約:SQLクエリは、絶対に必要な場合を除き、ループ内で実行してはいけません。
SQLクエリをループ内で実行すると、ループ内の繰り返し回数に応じて多くのクエリが実行されることになります。データの少ない開発者環境では問題ないかもしれませんが、本番環境ではすぐに制御不能に陥る可能性があります。
これが必要な場合もあります。このような場合は、マージリクエストの説明に明確に記述する必要があります。
バッチ処理
要約:外部サービス(例えば、PostgreSQL、Redis、オブジェクトストレージ)への単一プロセスの反復は、接続のオーバーヘッドを減らすためにバッチスタイルで実行されるべきです。
バッチ式で様々なテーブルから行を取得する方法については、Eager Loadingのセクションを参照してください。
例オブジェクトストレージから複数のファイルを削除する場合
GCSのようにオブジェクトストレージから複数のファイルを削除する場合、1つのREST APIコールを複数回実行するのはかなりコストがかかります。理想的には、これはバッチ・スタイルで行われるべきです。例えば、S3はバッチ削除APIを提供しているので、そのようなアプローチを検討するのは良いアイデアでしょう。
FastDestroyAll
モジュールがこの状況を助けてくれるかもしれません。これは、データベースの行とその関連データを一括で削除するための小さなフレームワークです。
タイムアウト
要約: システムが外部サービス(Kubernetesなど)へのHTTPコールを呼び出す際には、妥当なタイムアウトを設定する必要があり、それはPumaスレッドではなくSidekiqで実行する必要があります。
GitLabはしばしばKubernetesクラスターなどの外部サービスと通信する必要があります。この場合、外部サービスが要求された処理を終えるタイミングを見積もるのは難しく、例えば何らかの理由で非アクティブになっているユーザー所有のクラスターであれば、GitLabはいつまでも応答を待つことになるかもしれません(例)。これはPumaのタイムアウトにつながる可能性があるため、絶対に避けるべきです。
適切なタイムアウトを設定し、例外を優雅に処理し、UIやロギング内部でエラーを表面化する必要があります。
ReactiveCaching
を使用することは、内部データを取得するための最良のソリューションのひとつです。
データベーストランザクションを最小限に
そうしないと、オープンなトランザクションが基本的にPostgreSQLバックエンド接続のリリースをブロックするため、深刻な競合問題につながります。
可能な限りトランザクションを最小化するために、AfterCommitQueue
モジュールやafter_commit
AR フックの使用を検討してください。
トランザクション中のGitalyインスタンスへの1つのリクエストが、~"priority::1 "のイシューを引き起こした例です。
熱心なロード
要約: 複数の行を取得する場合は、常に関連付けをイーガー・ロードします。
関連付けが必要な複数のデータベースレコードを取得する際には、関連付けをイーガーロードしなければなりません。たとえば、ブログ記事の一覧を取得してその作成者を表示したい場合は、作成者の関連付けを読み込みます。
言い換えると、この代わりに
Post.all.each do |post|
puts post.author.name
end
の代わりに
Post.all.includes(:author).each do |post|
puts post.author.name
end
また、QueryRecoder テストを使用することで、イーガーローディング時のリグレッションを防ぐこともできます。
メモリ使用量
要約:マージリクエストは、絶対に必要な場合を除き、メモリ使用量を増やしてはなりません。
マージリクエストは、GitLab のメモリ使用量をコードで必要な最小限の量以上増やしてはいけません。つまり、大きなドキュメント (例えば HTML ドキュメント) をパースする必要がある場合は、入力全体をメモリに読み込むのではなく、可能な限りストリームとしてパースするのがよいということです。これが不可能な場合もありますが、その場合はマージリクエストの中で明示する必要があります。
UI 要素の遅延レンダリング
概要:UI要素は実際に必要なときだけレンダリングします。
特定の UI 要素は常に必要とは限りません。例えば、差分行にカーソルを置くと、新しいコメントを作成するための小さなアイコンが表示されます。このような要素を常にレンダリングするのではなく、実際に必要なときだけレンダリングするようにします。こうすることで、Haml/HTML を使用しないときに生成に時間を費やすことがなくなります。
キャッシュの使用
要約:データがトランザクションで複数回必要な場合や、一定期間保持する必要がある場合に、メモリやRedisにデータをキャッシュします。
トランザクション中に特定のデータを別の場所で再利用しなければならないことがあります。このような場合、データを取得するために複雑なオペレーションを実行する必要性をなくすために、このデータをメモリにキャッシュする必要があります。データをトランザクションの期間ではなく一定期間キャッシュする必要がある場合は、Redisを使うべきです。
たとえば、ユーザ名に関する言及を含む複数のスニペット(たとえば、Hello @alice
とHow are you doing @alice?
)を処理するとします。ユーザーオブジェクトをユーザー名ごとにキャッシュすることで、@alice
について言及するたびに同じクエリを実行する必要がなくなります。
トランザクションごとにデータをキャッシュするには、RequestStoreを使用します(Gitlab::SafeRequestStore
を使用すると、RequestStore.active?
を確認する手間を省くことができます)。Redisにデータをキャッシュするには、Railsのキャッシュシステムを使います。
ページネーション
アイテムのリストをテーブルとしてレンダリングする各機能には、ページネーションが必要です。
ページネーションの主なスタイルは次のとおりです:
- オフセットベースのページネーション: ユーザーは1のような特定のページに移動します。このスタイルはGitLabの全てのコンポーネントでサポートされています。
- Offset-based pagination, but without the count: ユーザーは1のような特定のページに移動します。ユーザーは次のページ番号だけを見ますが、総ページ数は見ません。
- キーセットベースのページ分割を使用した次のページ: ページ数がわからないため、ユーザーは次のページにしか移動できません。
- 無限スクロールのページ分割: ユーザーがページをスクロールすると、次のアイテムが非同期で読み込まれます。これは理想的で、前のものとまったく同じ利点があります。
究極的にスケーラブルなページネーションのソリューションは、キーセットを使ったページネーションです。しかし、今のところ GitLab ではサポートしていません。APIで進捗を追うことができます:Keyset Pagination をご覧ください。
ページネーション戦略を選択する際には、以下を考慮してください:
- フィルタリングを通過したオブジェクトの量を計算するのは非常に非効率的で、このオペレーションには通常数秒かかり、タイムアウトする可能性があります、
- 1000のような高い序数のページのエントリを取得するのは非常に非効率的です。データベースは以前のすべての項目をソートして反復しなければならず、このオペレーションは通常、データベースにかなりの負荷をかけることになります。
ページ分割に関する有用なヒントはページ分割ガイドラインにあります。
バッジカウンター
カウンターは常に切り捨てられます。ある閾値を超えた正確な数を表示したくないということです。その理由は、正確な数を計算したい場合、一致する正確な数を知るために、効果的にそれぞれをフィルタリングする必要があるからです。
UXの観点からは、パイプラインが40000本以上あるのではなく、パイプラインが1000本以上あることを示す方が受け入れられますが、ページの読み込みが2秒長くなるというトレードオフがあります。
このパターンの例はパイプラインとジョブのリストです。1000+
に数字を切り捨てていますが、最も興味深い情報である実行中のパイプラインの正確な数を表示しています。
そのために使用できるヘルパーメソッドとして、NumbersHelper.limited_counter_with_delimiter
があります。このメソッドでは、カウント行の上限を指定できます。
場合によっては、バッジカウンタを非同期にロードしたいこともあります。そうすることで、最初のページの読み込みを高速化し、全体としてよりよいユーザー体験を提供することができます。
機能フラグの使用法
パフォーマンス上重要な要素を持つ機能、またはパフォーマンス上の欠陥が知られている機能には、それを無効にする機能フラグが必要です。
機能フラグがあることで、私たちのチームはシステムを監視し、ユーザーに問題を気づかれることなく迅速に対応することができるため、より満足しています。
パフォーマンスの欠陥は、最初の変更をマージした後、すぐにアドレスするべきです。
機能フラグをいつ、どのように使うべきかについてはGitLab 開発における機能フラグ をご覧ください。
ストレージ
以下のような保管方法が考えられます:
-
ローカル一時ストレージ(超短期ストレージ) このタイプのストレージはシステムが提供するストレージで、
/tmp
フォルダのようなものです。このタイプのストレージは、すべての一時タスクに使用するのが理想的です。各ノードが独自の一時ストレージを持つことで、スケーリングが大幅に容易になります。また、このストレージは多くの場合SSDベースであるため、非常に高速です。ローカルストレージは、TMPDIR
変数を使用することで、アプリケーションに合わせて簡単に設定できます。 -
共有一時ストレージ(短期ストレージ) このタイプのストレージはネットワークベースの一時ストレージで、通常は一般的なNFSサーバーで実行されます。2020年2月現在、ほとんどの実装でこのタイプのストレージを使用しています。これによって上記の上限を大幅に大きくすることができるといっても、実際にはそれ以上使用できるわけではありません。共有一時ストレージはすべてのノードで共有されます。したがって、その領域を大量に使うジョブや大量のオペレーションを行うジョブは、アプリケーション全体で他のすべてのジョブやリクエストの実行に競合を引き起こし、GitLab 全体の安定性に影響を与えやすくなります。そのことを尊重しましょう。
-
共有永続ストレージ(長期保存)このタイプのストレージは、共有ネットワークベースのストレージ(例えばNFS)を使用します。このソリューションは、数ノードからなる小規模インストールを実行している顧客が主に使用します。共有ストレージ上のファイルには簡単にアクセスできますが、データをアップロードまたはダウンロードするジョブは、他のすべてのジョブに対して深刻な競合を引き起こす可能性があります。これはOmnibusがデフォルトで使用している方法でもあります。
-
オブジェクトベースの永続ストレージ(長期保存)このタイプのストレージは、AWS S3のような外部サービスを使用します。オブジェクトストレージは無限に拡張可能で冗長性があります。このストレージにアクセスするには、通常、ファイルをダウンロードして操作する必要があります。オブジェクトストレージは、定義上、ファイルの無制限の同時アップロードとダウンロードを処理できると仮定できるので、究極のソリューションと考えることができます。これはまた、アプリケーションがコンテナ化されたデプロイメント(Kubernetes)で簡単に実行できるようにするために必要な究極のソリューションでもあります。
一時ストレージ
本番ノードのストレージは本当にまばらです。アプリケーションは、非常に限られた一時ストレージで動作するように構築する必要があります。あなたのコードが実行されるシステムには、合計1G-10G
の一時ストレージがあると予想できます。しかし、このストレージは実行されるすべてのジョブで共有されます。もし、あなたのジョブが100MB
以上の領域を使用する必要があるのであれば、これまでのアプローチを再考する必要があります。
どのようなニーズがあるにせよ、ファイルを処理する必要がある場合は、明確に文書化する必要があります。100MB
を超える容量が必要な場合は、メンテナーに助けを求め、より良い解決策を一緒に考えてもらうことを検討してください。
ローカル一時保存
Kubernetesクラスターにアプリケーションをデプロイする作業を行っているため、ローカルストレージの使用は特に望ましいソリューションです。Dir.mktmpdir
を使いたい時は?例えば、アーカイブの抽出/作成、既存データの大規模な操作などを行いたい場合。
Dir.mktmpdir('designs') do |path|
# do manipulation on path
# the path will be removed once
# we go out of the block
end
共有の一時ストレージ
オブジェクトストレージではなく、ディスクベースのストレージにファイルを永続化する場合は、共有一時ストレージを使用する必要があります。Workhorseのダイレクトアップロードはファイルを受け取るときに共有ストレージに書き込むことができ、後でGitLab Railsは移動オペレーションを実行することができます。同じ保存先への移動オペレーションは瞬時に行われます。システムはcopy
オペレーションを実行する代わりに、ファイルを新しい場所に再アタッチします。
これはアプリケーションに余計な複雑さをもたらすので、再実装するのではなく、よく確立されたパターン(たとえばObjectStorage
関心)を再利用するようにしましょう。
共有一時記憶域の使用は、それ以外のすべての使用において非推奨です。
永続ストレージ
オブジェクトストレージ
永続ファイルを保持するすべての機能は、オブジェクトストレージへのデータ保存をサポートする必要があります。ノード間で共有ボリューム形式で永続ストレージを持つことは、すべてのノードでデータアクセスの競合が発生するため、スケーラブルではありません。
GitLabは共有ストレージとオブジェクトストレージベースの永続ストレージをシームレスにサポートするObjectStorageを提供しています。
データアクセス
データのアップロードやダウンロードを許可する各機能は、Workhorseダイレクトアップロードを使用する必要があります。つまり、アップロードはWorkhorseによってオブジェクトストレージに直接保存される必要があり、ダウンロードは全てWorkhorseによって提供される必要があります。
プーマ経由でアップロード/ダウンロードを実行すると、アップロードの間、処理スロット(スレッド)全体がブロックされるため、高価なオペレーションになります。
プーマ経由のアップロード/ダウンロードには、オペレーションがタイムアウトする可能性があるという問題もあります。クライアントがアップロード/ダウンロードに時間がかかると、リクエスト処理のタイムアウト(通常30~60秒)のために処理スロットが停止することがあります。
上記の理由から、すべてのファイルのアップロードとダウンロードにWorkhorseダイレクトアップロードを実装する必要があります。