バッチク゚リ凊理の問題点ずその解決策前線

バッチク゚リ凊理の問題点ずその解決策前線ほずんどすべおの最新の゜フトりェア補品は、耇数のサヌビスで構成されおいたす。 倚くの堎合、サヌビス間チャネルの長い応答時間はパフォヌマンスの問題の原因になりたす。 この皮の問題に察する暙準的な解決策は、耇数のサヌビス間リク゚ストを XNUMX ぀のパッケヌゞにパックするこずであり、これはバッチ凊理ず呌ばれたす。

バッチ凊理を䜿甚する堎合、パフォヌマンスやコヌドの明瞭さの点で結果に満足できない堎合がありたす。 この方法は、呌び出し偎にずっおは思ったほど簡単ではありたせん。 目的や状況が異なれば、゜リュヌションは倧きく異なりたす。 具䜓的な䟋を䜿甚しお、いく぀かのアプロヌチの長所ず短所を瀺したす。

実蚌プロゞェクト

わかりやすくするために、珟圚取り組んでいるアプリケヌションのサヌビスの XNUMX ぀の䟋を芋おみたしょう。

プラットフォヌム遞定䟋の説明パフォヌマンスの䜎䞋の問題は非垞に䞀般的なものであり、特定の蚀語やプラットフォヌムには圱響したせん。 この蚘事では、Spring + Kotlin のコヌド䟋を䜿甚しお問題ず解決策を瀺したす。 Kotlin は、Java 開発者ず C# 開発者にずっおも同様に理解できたす (たたは理解できたせん)。さらに、コヌドは Java よりもコンパクトで理解しやすいです。 玔粋な Java 開発者にずっお理解しやすいように、Kotlin の黒魔術を避け、(Lombok の粟神に埓っお) 癜魔術のみを䜿甚したす。 拡匵メ゜ッドはいく぀かありたすが、実際には静的メ゜ッドずしお Java プログラマヌには銎染みのあるものなので、料理の味を損なわない皋床の砂糖になりたす。
曞類承認サヌビスがございたす。 誰かが文曞を䜜成しお議論のために提出し、その間に線集が行われ、最終的に文曞が合意されたす。 承認サヌビス自䜓はドキュメントに぀いおは䜕も知りたせん。これは、ここでは考慮しない小さな远加機胜を備えた承認者のチャットにすぎたせん。

したがっお、チャット ルヌム (ドキュメントに盞圓) があり、それぞれの参加者が事前に定矩されおいたす。 通垞のチャットず同様、メッセヌゞにはテキストずファむルが含たれおおり、返信たたは転送するこずができたす。

data class ChatMessage(
  // nullable Ñ‚ак ÐºÐ°Ðº Ð¿ÐŸÑÐ²Ð»ÑÐµÑ‚ся Ñ‚ПлькП Ð¿ÐŸÑÐ»Ðµ persist
  val id: Long? = null,
  /** Ð¡ÑÑ‹Ð»ÐºÐ° ÐœÐ° Ð°Ð²Ñ‚Пра */
  val author: UserReference,
  /** Ð¡ÐŸÐŸÐ±Ñ‰ÐµÐœÐžÐµ */
  val message: String,
  /** Ð¡ÑÑ‹Ð»ÐºÐž ÐœÐ° Ð°Ñ‚тачО */
  // ÐžÐ·-за ÐŸÑÐŸÐ±ÐµÐœÐœÐŸÑÑ‚ей ÑÐ²ÑÐ·ÐºÐž JPA+СУБД Ð¿Ñ€ÐŸÑ‰Ðµ Ð¿ÐŸÐŽÐŽÐµÑ€Ð¶ÐžÐ²Ð°Ñ‚ÑŒ Ðž null, Ðž Ð¿ÑƒÑÑ‚ые ÑÐ¿ÐžÑÐºÐž
  val files: List<FileReference>? = null,
  /** Ð•ÑÐ»Ðž ÑÐ²Ð»ÑÐµÑ‚ся ÐŸÑ‚ветПЌ, Ñ‚П Ð·ÐŽÐµÑÑŒ Ð±ÑƒÐŽÐµÑ‚ ÐŸÑ€ÐžÐ³ÐžÐœÐ°Ð» */
  val replyTo: ChatMessage? = null,
  /** Ð•ÑÐ»Ðž ÑÐ²Ð»ÑÐµÑ‚ся Ð¿ÐµÑ€ÐµÑÑ‹Ð»ÐºÐŸÐ¹, Ñ‚П Ð·ÐŽÐµÑÑŒ Ð±ÑƒÐŽÐµÑ‚ ÐŸÑ€ÐžÐ³ÐžÐœÐ°Ð» */
  val forwardFrom: ChatMessage? = null
)

ファむルずナヌザヌのリンクは、他のドメむンぞのリンクです。 ここでは私たちは次のように暮らしおいたす。

typealias FileReference Long
typealias UserReference Long

ナヌザヌデヌタはKeycloakに保存され、REST経由で取埗されたす。 ファむルに぀いおも同様です。ファむルずそのメタ情報は、別のファむル ストレヌゞ サヌビスに存圚したす。

これらのサヌビスぞのすべおの呌び出しは、 重いリク゚スト。 これは、これらのリク゚ストの転送にかかるオヌバヌヘッドが、サヌドパヌティ サヌビスによるリク゚ストの凊理にかかる時間よりもはるかに倧きいこずを意味したす。 私たちのテストベンチでは、このようなサヌビスの䞀般的な呌び出し時間は 100 ミリ秒であるため、将来的にはこれらの数倀を䜿甚する予定です。

必芁な情報をすべお含む最埌の N メッセヌゞを受信するための単玔な REST コントロヌラヌを䜜成する必芁がありたす。 ぀たり、フロント゚ンドのメッセヌゞ モデルはほが同じであり、すべおのデヌタを送信する必芁があるず考えられたす。 フロント゚ンド モデルの違いは、ファむルずナヌザヌをリンクさせるために、わずかに埩号化された圢匏で提瀺する必芁があるこずです。

/** Ð’ Ñ‚акПЌ Ð²ÐžÐŽÐµ ÐŸÑ‚Ўаются ÑÑÑ‹Ð»ÐºÐž ÐœÐ° ÑÑƒÑ‰ÐœÐŸÑÑ‚О ÐŽÐ»Ñ Ñ„рПМта */
data class ReferenceUI(
  /** Ð˜ÐŽÐµÐœÑ‚ОфОкатПр ÐŽÐ»Ñ url */
  val ref: String,
  /** Ð’ОЎОЌПе Ð¿ÐŸÐ»ÑŒÐ·ÐŸÐ²Ð°Ñ‚елю ÐœÐ°Ð·Ð²Ð°ÐœÐžÐµ ÑÑÑ‹Ð»ÐºÐž */
  val name: String
)
data class ChatMessageUI(
  val id: Long,
  /** Ð¡ÑÑ‹Ð»ÐºÐ° ÐœÐ° Ð°Ð²Ñ‚Пра */
  val author: ReferenceUI,
  /** Ð¡ÐŸÐŸÐ±Ñ‰ÐµÐœÐžÐµ */
  val message: String,
  /** Ð¡ÑÑ‹Ð»ÐºÐž ÐœÐ° Ð°Ñ‚тачО */
  val files: List<ReferenceUI>,
  /** Ð•ÑÐ»Ðž ÑÐ²Ð»ÑÑ‚ся ÐŸÑ‚ветПЌ, Ñ‚П Ð·ÐŽÐµÑÑŒ Ð±ÑƒÐŽÐµÑ‚ ÐŸÑ€ÐžÐ³ÐžÐœÐ°Ð» */
  val replyTo: ChatMessageUI? = null,
  /** Ð•ÑÐ»Ðž ÑÐ²Ð»ÑÑ‚ся Ð¿ÐµÑ€ÐµÑÑ‹Ð»ÐºÐŸÐ¹, Ñ‚П Ð·ÐŽÐµÑÑŒ Ð±ÑƒÐŽÐµÑ‚ ÐŸÑ€ÐžÐ³ÐžÐœÐ°Ð» */
  val forwardFrom: ChatMessageUI? = null
)

以䞋を実装する必芁がありたす。

interface ChatRestApi {
  fun getLast(nInt): List<ChatMessageUI>
}

UI 接尟語は、フロント゚ンドの DTO モデル、぀たり REST 経由で提䟛すべきものを意味したす。

ここで驚くべきこずは、チャット ID を枡しおおらず、ChatMessage/ChatMessageUI モデルですらチャット ID を持たないこずです。 サンプルのコヌドが乱雑にならないように、これは意図的に行いたした (チャットは分離されおいるため、チャットは XNUMX ぀だけであるず想定できたす)。

哲孊的な䜙談ChatMessageUI クラスず ChatRestApi.getLast メ゜ッドはどちらも、実際には順序付き Set である堎合に List デヌタ型を䜿甚したす。 これは JDK では問題があるため、むンタヌフェむス レベルで芁玠の順序を宣蚀する (远加および削陀するずきに順序を維持する) こずは機胜したせん。 そのため、順序付けられた Set が必芁な堎合には List を䜿甚するこずが䞀般的になっおいたす (LinkedHashSet もありたすが、これはむンタヌフェむスではありたせん)。
重芁な制限: 長い返信や転送の連鎖はないず仮定したす。 ぀たり、メッセヌゞは存圚したすが、その長さは XNUMX メッセヌゞを超えたせん。 メッセヌゞのチェヌン党䜓をフロント゚ンドに送信する必芁がありたす。

倖郚サヌビスからデヌタを受信するには、次の API がありたす。

interface ChatMessageRepository {
  fun findLast(nInt): List<ChatMessage>
}
data class FileHeadRemote(
  val id: FileReference,
  val name: String
)
interface FileRemoteApi {
  fun getHeadById(idFileReference): FileHeadRemote
  fun getHeadsByIds(idSet<FileReference>): Set<FileHeadRemote>
  fun getHeadsByIds(idList<FileReference>): List<FileHeadRemote>
  fun getHeadsByChat(): List<FileHeadRemote>
}
data class UserRemote(
  val id: UserReference,
  val name: String
)
interface UserRemoteApi {
  fun getUserById(idUserReference): UserRemote
  fun getUsersByIds(idSet<UserReference>): Set<UserRemote>
  fun getUsersByIds(idList<UserReference>): List<UserRemote>
}

倖郚サヌビスは最初はバッチ凊理を提䟛しおおり、どちらのバヌゞョンでも Set 経由 (芁玠の順序を保持せず、䞀意のキヌを䜿甚) ず List 経由 (重耇がある可胜性がありたす - 順序は保持されたす) を提䟛しおいるこずがわかりたす。

単玔な実装

単玔な実装

REST コントロヌラヌの最初の単玔な実装は、ほずんどの堎合次のようになりたす。

class ChatRestController(
  private val messageRepository: ChatMessageRepository,
  private val userRepository: UserRemoteApi,
  private val fileRepository: FileRemoteApi
) : ChatRestApi {
  override fun getLast(nInt) =
    messageRepository.findLast(n)
      .map it.toFrontModel() }
  
  private fun ChatMessage.toFrontModel(): ChatMessageUI =
    ChatMessageUI(
      id = id ?: throw IllegalStateException("$this must be persisted"),
      author = userRepository.getUserById(author).toFrontReference(),
      message = message,
      files = files?.let files ->
        fileRepository.getHeadsByIds(files)
          .map it.toFrontReference() }
      } ?: listOf(),
      forwardFrom = forwardFrom?.toFrontModel(),
      replyTo = replyTo?.toFrontModel()
    )
}

すべおが非垞に明確であり、これは倧きな利点です。

バッチ凊理を䜿甚し、倖郚サヌビスからデヌタをバッチで受信したす。 しかし、私たちの生産性はどうなるでしょうか?

メッセヌゞごずに、䜜成者フィヌルドのデヌタを取埗するために UserRemoteApi が XNUMX 回呌び出され、すべおの添付ファむルを取埗するために FileRemoteApi が XNUMX 回呌び出されたす。 それだけのようです。 ChatMessage の forwardFrom フィヌルドず ReplyTo フィヌルドが、䞍必芁な呌び出しを必芁ずしない方法で取埗されたずしたす。 ただし、それらを ChatMessageUI に倉えるず再垰が発生し、呌び出しカりンタヌが倧幅に増加する可胜性がありたす。 前に述べたように、ネストがあたりなく、チェヌンが XNUMX ぀のメッセヌゞに制限されおいるず仮定したしょう。

その結果、メッセヌゞごずに倖郚サヌビスぞの呌び出しが 2  1 回発生し、メッセヌゞのパッケヌゞ党䜓で JPA 呌び出しが 6 回発生したす。 呌び出しの合蚈数は、1*N+20 から 4*N+10 たで倉化したす。 これは実際の単䜍ではいくらですか? ペヌゞをレンダリングするのに 500 個のメッセヌゞが必芁だずしたす。 受信には XNUMX 秒から XNUMX 秒かかりたす。 ひどい XNUMXms以内に抑えたいです。 そしお、フロント゚ンドでシヌムレスなスクロヌルを実珟するこずを倢芋おいたため、この゚ンドポむントのパフォヌマンス芁件は XNUMX 倍になる可胜性がありたす。

長所

  1. コヌドは簡朔で自己文曞化されおいたす (サポヌト チヌムの倢です)。
  2. コヌドはシンプルなので、足を撃たれる機䌚はほずんどありたせん。
  3. バッチ凊理は異物のようには芋えず、ロゞックに有機的に統合されおいたす。
  4. ロゞックの倉曎は簡単に行われ、ロヌカルで行われたす。

マむナス

パケットが非垞に小さいため、パフォヌマンスが䜎䞋したす。

このアプロヌチは、単玔なサヌビスやプロトタむプでよく芋られたす。 倉曎の速床が重芁である堎合、システムを耇雑にする䟡倀はほずんどありたせん。 同時に、私たちの非垞に単玔なサヌビスのパフォヌマンスはひどいものであるため、このアプロヌチの適甚範囲は非垞に狭いです。

単玔な䞊列凊理

すべおのメッセヌゞの凊理を䞊行しお開始できたす。これにより、メッセヌゞの数に応じた時間の盎線的な増加を避けるこずができたす。 これは倖郚サヌビスに倧きなピヌク負荷をもたらすため、特に良いパスではありたせん。

䞊列凊理の実装は非垞に簡単です。

override fun getLast(nInt) =
  messageRepository.findLast(n).parallelStream()
    .map it.toFrontModel() }
    .collect(toList())

䞊列メッセヌゞ凊理を䜿甚するず、理想的には 300  700 ミリ秒が埗られたす。これは単玔な実装よりもはるかに優れおいたすが、それでも十分な速床ではありたせん。

このアプロヌチでは、userRepository ず fileRepository ぞのリク゚ストが同期的に実行されるため、あたり効率的ではありたせん。 これを修正するには、呌び出しロゞックを倧幅に倉曎する必芁がありたす。 たずえば、CompletionStage (別名 CompletableFuture) 経由では次のようになりたす。

private fun ChatMessage.toFrontModel(): ChatMessageUI =
  CompletableFuture.supplyAsync {
    userRepository.getUserById(author).toFrontReference()
  }.thenCombine(
    files?.let {
      CompletableFuture.supplyAsync {
        fileRepository.getHeadsByIds(files).map it.toFrontReference() }
      }
    } ?: CompletableFuture.completedFuture(listOf())
  ) authorfiles ->
    ChatMessageUI(
      id = id ?: throw IllegalStateException("$this must be persisted"),
      author = author,
      message = message,
      files = files,
      forwardFrom = forwardFrom?.toFrontModel(),
      replyTo = replyTo?.toFrontModel()
    )
  }.get()!!

最初は単玔なマッピング コヌドが理解しにくくなっおいるこずがわかりたす。 これは、結果が䜿甚される倖郚サヌビスぞの呌び出しを分離する必芁があったためです。 これ自䜓は悪いこずではありたせん。 しかし、コヌルを組み合わせるずあたり゚レガントに芋えず、兞型的な反応性の「ヌヌドル」に䌌おいたす。

コルヌチンを䜿甚するず、すべおがより適切に芋えるようになりたす。

private fun ChatMessage.toFrontModel(): ChatMessageUI =
  join(
    userRepository.getUserById(author).toFrontReference() },
    files?.let fileRepository.getHeadsByIds(files)
      .map it.toFrontReference() } } ?: listOf() }
  ).let (author, files) ->
    ChatMessageUI(
      id = id ?: throw IllegalStateException("$this must be persisted"),
      author = author,
      message = message,
      files = files,
      forwardFrom = forwardFrom?.toFrontModel(),
      replyTo = replyTo?.toFrontModel()
    )
  }

ここで

fun <ABjoin(a: () -> Ab: () -> B) =
  runBlocking(IO{
    awaitAll(async a() }async b() })
  }.let {
    it[0as to it[1as B
  }

理論的には、このような䞊列凊理を䜿甚するず 200  400 ミリ秒が埗られたすが、これはすでに予想に近い倀です。

残念ながら、そのような優れた䞊列化は存圚せず、支払わなければならない代償は非垞に残酷です。同時に少数のナヌザヌだけが䜜業しおいるず、倧量のリク゚ストがサヌビスに降りかかり、いずれにしおも䞊列凊理されたせん。悲しい4時代に戻っおしたいたす。

このようなサヌビスを䜿甚した堎合の結果は、1300 個のメッセヌゞを凊理するのに 1700  20 ミリ秒でした。 これは最初の実装よりも高速ですが、それでも問題は解決されたせん。

䞊列ク゚リの別の䜿甚法サヌドパヌティのサヌビスがバッチ凊理を提䟛しおいない堎合はどうなりたすか? たずえば、むンタヌフェむス メ゜ッド内のバッチ凊理実装の欠劂を隠すこずができたす。

interface UserRemoteApi {
  fun getUserById(idUserReference): UserRemote
  fun getUsersByIds(idSet<UserReference>): Set<UserRemote> =
    id.parallelStream()
      .map getUserById(it}.collect(toSet())
  fun getUsersByIds(idList<UserReference>): List<UserRemote> =
    id.parallelStream()
      .map getUserById(it}.collect(toList())
}

将来のバヌゞョンでバッチ凊理を期埅する堎合、これは理にかなっおいたす。
長所

  1. メッセヌゞベヌスの䞊列凊理を簡単に実装したす。
  2. 優れた拡匵性。

短所

  1. 異なるサヌビスぞのリク゚ストを䞊行しお凊理する堎合、デヌタの取埗ずその凊理を分離する必芁性。
  2. サヌドパヌティのサヌビスの負荷が増加したす。

適甚範囲は単玔なアプロヌチずほが同じであるこずがわかりたす。 他者の容赊ない悪甚によりサヌビスのパフォヌマンスを数倍向䞊させたい堎合は、䞊列リク゚スト方匏を䜿甚するのが合理的です。 この䟋では、パフォヌマンスが 2,5 倍向䞊したしたが、これでは明らかに十分ではありたせん。

キャッシング

倖郚サヌビスの JPA の粟神に基づいおキャッシュを実行できたす。぀たり、受信したオブゞェクトを (バッチ凊理䞭を含む) 再床受信しないようにセッション内に栌玍したす。 このようなキャッシュは自分で䜜成するこずも、@Cacheable を備えた Spring を䜿甚するこずもでき、さらに EhCache のような既補のキャッシュをい぀でも手動で䜿甚するこずもできたす。

よくある問題は、キャッシュがヒットした堎合にのみ圹立぀こずです。 この堎合、䜜成者フィヌルドにはヒットする可胜性が非垞に高くなりたす (たずえば 50%) が、ファむルにはたったくヒットしたせん。 このアプロヌチにより倚少の改善は埗られたすが、パフォヌマンスが根本的に倉わるわけではありたせん (ブレヌクスルヌが必芁です)。

セッション間 (長い) キャッシュには、耇雑な無効化ロゞックが必芁です。 䞀般に、セッション間キャッシュを䜿甚しおパフォヌマンスの問題を解決するのは遅ければ遅いほど良いでしょう。

長所

  1. コヌドを倉曎せずにキャッシュを実装したす。
  2. 生産性が数倍向䞊したした堎合によっおは。

短所

  1. 誀っお䜿甚するずパフォヌマンスが䜎䞋する可胜性がありたす。
  2. 特に長いキャッシュの堎合、メモリのオヌバヌヘッドが倧きくなりたす。
  3. 耇雑な無効化。実行時に再珟が困難な問題を匕き起こす゚ラヌ。

倚くの堎合、キャッシュは蚭蚈䞊の問題を迅速に解決するためだけに䜿甚されたす。 これは、それらを䜿甚しおはいけないずいう意味ではありたせん。 ただし、それらを垞に慎重に扱い、最初に結果ずしお埗られるパフォヌマンスの向䞊を評䟡しおから、決定を䞋す必芁がありたす。

この䟋では、キャッシュによりパフォヌマンスが玄 25% 向䞊したす。 同時に、キャッシュには非垞に倚くの欠点があるため、ここでは䜿甚したせん。

結果

そこで、バッチ凊理を䜿甚するサヌビスの単玔な実装ず、それを高速化するいく぀かの簡単な方法を怜蚎したした。

これらすべおの方法の䞻な利点はその単玔さであり、そこから倚くの楜しい結果が埗られたす。

これらの方法に共通する問題は、䞻にパケットのサむズが原因でパフォヌマンスが䜎䞋するこずです。 したがっお、これらの解決策があなたに合わない堎合は、より根本的な方法を怜蚎する䟡倀がありたす。

解決策を探すには、䞻に XNUMX ぀の方向がありたす。

  • デヌタの非同期䜜業 (パラダむム シフトが必芁なため、この蚘事では説明したせん)。
  • 同期凊理を維持しながらバッチを拡倧したす。

バッチを拡倧するず、倖郚呌び出しの数が倧幅に枛り、同時にコヌドの同期が維持されたす。 蚘事の次の郚分では、このトピックに぀いお説明したす。

出所 habr.com

コメントを远加したす