د بیچ پوښتنو پروسس کولو ستونزې او د دوی حلونه (1 برخه)

د بیچ پوښتنو پروسس کولو ستونزې او د دوی حلونه (1 برخه)نږدې ټول عصري سافټویر محصولات ډیری خدمتونه لري. ډیری وختونه، د انټرنیټ چینلونو اوږد غبرګون وختونه د فعالیت ستونزو سرچینه ګرځي. د دې ډول ستونزې لپاره معیاري حل دا دی چې په یوه بسته کې د څو خدماتو غوښتنې بسته بندي کړئ ، کوم چې د بیچینګ په نوم یادیږي.

که تاسو د بیچ پروسس کاروئ، تاسو ممکن د فعالیت یا کوډ وضاحت په شرایطو کې د پایلو څخه خوښ نه یاست. دا طریقه په زنګ وهونکي کې دومره اسانه نه ده لکه څنګه چې تاسو فکر کوئ. د مختلفو موخو لپاره او په مختلفو شرایطو کې، حلونه خورا توپیر لري. د ځانګړو مثالونو په کارولو سره، زه به د څو طریقو ګټې او زیانونه وښیم.

د مظاهرې پروژه

د وضاحت لپاره، راځئ چې په غوښتنلیک کې د خدماتو څخه یو مثال وګورو چې زه دا مهال کار کوم.

د مثالونو لپاره د پلیټ فارم انتخاب تشریحد ضعیف فعالیت ستونزه خورا عمومي ده او په کوم ځانګړي ژبو یا پلیټ فارمونو اغیزه نه کوي. دا مقاله به د پسرلي + کوټلین کوډ مثالونه وکاروي ترڅو ستونزې او حلونه وښیې. کوټلین د جاوا او C# پراختیا کونکو لپاره په مساوي ډول د پوهیدو وړ (یا د پوهیدو وړ) دی ، سربیره پردې ، کوډ د جاوا په پرتله خورا کمپیکٹ او د پوهیدو وړ دی. د خالص جاوا پراختیا کونکو لپاره پوهیدل اسانه کولو لپاره ، زه به د کوټلین تور جادو څخه مخنیوی وکړم او یوازې سپین جادو وکاروم (د لومبوک په روح کې). د تمدید یو څو میتودونه به وي ، مګر دا واقعیا د جاوا ټولو پروګرام کونکو ته د جامد میتودونو په توګه پیژندل کیږي ، نو دا به یو کوچنی بوره وي چې د ډش خوند به خراب نکړي.
د سند تصویب خدمت شتون لري. یو څوک یو سند جوړوي او د بحث لپاره یې وړاندې کوي، چې په ترڅ کې یې سمونونه کیږي، او بالاخره په سند موافقه کیږي. د تصویب خدمت پخپله د اسنادو په اړه هیڅ نه پوهیږي: دا یوازې د کوچني اضافي دندو سره د تصویب کونکو خبرې دي چې موږ به یې دلته په پام کې ونیسو.

Итак, есть комнаты чатов (соответствуют документам) с предопределенным набором участников в каждой из них. Как в обычных чатах, сообщения содержат текст и файлы и могут быть ответами (reply) и пересылками (forward):

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

د کارونکي معلومات په کیکلوک کې زیرمه شوي او د REST له لارې ترلاسه کیږي. ورته د فایلونو لپاره ځي: فایلونه او د دوی په اړه میټین معلومات په جلا فایل ذخیره کولو خدمت کې ژوند کوي.

دې خدماتو ته ټول زنګونه دي درنې غوښتنې. دا پدې مانا ده چې د دې غوښتنو لیږدولو سر د هغه وخت څخه خورا ډیر دی چې دوی یې د دریمې ډلې خدمت لخوا پروسس کیږي. زموږ د ازموینې بنچونو کې، د دې ډول خدماتو لپاره د زنګ وهلو وخت 100 ms دی، نو موږ به په راتلونکي کې دا شمیرې وکاروو.

موږ اړتیا لرو یو ساده REST کنټرولر جوړ کړو ترڅو د ټولو اړین معلوماتو سره وروستي N پیغامونه ترلاسه کړو. دا دی، موږ باور لرو چې په فرنټ اینډ کې د پیغام ماډل تقریبا ورته دی او ټول معلومات باید لیږل کیږي. د مخکښې پای ماډل تر مینځ توپیر دا دی چې فایل او کارونکي باید په یو څه کوډ شوي شکل کې وړاندې شي ترڅو دوی لینکونه رامینځته کړي:

/** В таком виде отдаются ссылки на сущности для фронта */
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 ماډل یو نلري. ما دا په قصدي توګه ترسره کړل ترڅو د مثالونو کوډ ګډوډ نه کړم (چیټونه جلا شوي، نو موږ فرض کولی شو چې موږ یوازې یو لرو).

فلسفي انحرافدواړه د ChatMessageUI ټولګي او ChatRestApi.getLast میتود د لیست ډیټا ډول کاروي کله چې په حقیقت کې دا یو ترتیب شوی سیټ وي. دا په JDK کې خراب دی ، نو د انٹرفیس په کچه د عناصرو ترتیب اعلان کول (د اضافه کولو او لرې کولو پرمهال د ترتیب ساتل) به کار ونکړي. نو دا په داسې قضیو کې د لیست کارولو لپاره معمول تمرین شوی چیرې چې امر شوي سیټ ته اړتیا وي (دلته یو LinkedHashSet هم شتون لري ، مګر دا انٹرفیس ندی).
مهم محدودیتونه: موږ به فرض کړو چې د ځوابونو یا لیږدونو اوږد سلسله شتون نلري. دا دی، دوی شتون لري، مګر د دوی اوږدوالی له دریو پیغامونو څخه زیات نه دی. د پیغامونو ټوله سلسله باید مخکینۍ برخې ته ولیږدول شي.

د بهرنیو خدماتو څخه د معلوماتو ترلاسه کولو لپاره لاندې APIs شتون لري:

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>
}

دا لیدل کیدی شي چې بهرني خدمتونه په پیل کې د بیچ پروسس کولو لپاره چمتو کوي، او په دواړو نسخو کې: د سیټ له لارې (پرته له دې چې د عناصرو ترتیب ساتل، د ځانګړو کلیدونو سره) او د لیست له لارې (ممکن نقلونه وي - ترتیب ساتل کیږي).

ساده پلي کول

په ساده ډول پلي کول

Первая наивная реализация нашего 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 для получения данных по полю author и один вызов FileRemoteApi для получения всех приложенных файлов. Вроде бы, все. Допустим, что поля forwardFrom и replyTo для ChatMessage получаются так, что это не потребует лишних вызовов. Но вот превращение их в ChatMessageUI приведет к рекурсии, то есть показатели счетчиков вызовов могут сильно вырасти. Как мы отметили ранее, допустим, что большой вложенности у нас не бывает и цепочка ограничена тремя сообщениями.

د پایلې په توګه، موږ به په هر پیغام کې بهرني خدماتو ته له دوو څخه تر شپږو زنګونو پورې او د پیغامونو د ټولې کڅوړې لپاره یو JPA کال ترلاسه کړو. د ټولو زنګونو شمیر به له 2*N+1 څخه تر 6*N+1 پورې توپیر ولري. دا په اصلي واحدونو کې څومره دی؟ راځئ چې ووایو دا د یوې پاڼې د وړاندې کولو لپاره 20 پیغامونه اخلي. د دوی ترلاسه کولو لپاره، دا به له 4 څخه تر 10 ثانیو پورې وخت ونیسي. وحشتناکه! زه غواړم دا په 500 ms کې وساتم. او له هغه ځایه چې دوی په مخکني پای کې د بې سیمه سکرول کولو خوب لیدلی ، د دې پای ټکي لپاره د فعالیت اړتیاوې دوه چنده کیدی شي.

پرو:

  1. کوډ لنډ او پخپله مستند دی (د ملاتړ ټیم خوب).
  2. کوډ ساده دی، نو د ځان په پښو کې د ډزو کولو لپاره تقریبا هیڅ فرصت شتون نلري.
  3. د بیچ پروسس کول د اجنبی په څیر نه ښکاري او په منظم ډول په منطق کې مدغم شوي.
  4. منطقي بدلونونه به په اسانۍ سره رامینځته شي او محلي به وي.

منفي:

د خورا کوچني کڅوړو له امله وحشتناک فعالیت.

دا طریقه ډیری وختونه په ساده خدماتو یا پروټوټایپونو کې لیدل کیدی شي. که چیرې د بدلونونو سرعت مهم وي، نو دا د سیسټم پیچلې کولو ارزښت نلري. په ورته وخت کې، زموږ د خورا ساده خدمت لپاره فعالیت خورا خطرناک دی، نو د دې طریقې د تطبیق ساحه خورا تنګ ده.

ناپاک موازي پروسس کول

تاسو کولی شئ په موازي ډول د ټولو پیغامونو پروسس پیل کړئ - دا به تاسو ته اجازه درکړي چې د پیغامونو شمیر پورې اړوند په وخت کې د خطي زیاتوالي څخه ځان خلاص کړئ. دا په ځانګړي ډول ښه لاره نه ده ځکه چې دا به په بهرني خدمت کې د لوی لوړ بار لامل شي.

د موازي پروسس پلي کول خورا ساده دي:

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

د موازي پیغام پروسس کولو په کارولو سره، موږ په مثالي توګه 300-700 ms ترلاسه کوو، کوم چې د ساده پلي کولو په پرتله خورا ښه دی، مګر بیا هم په کافي اندازه ګړندی نه دی.

د دې کړنلارې سره، د کاروونکي ریپوزیټري او فایل ریپوزټري ته غوښتنې به په همغږي توګه اجرا شي، کوم چې خورا اغیزمن ندي. د دې د حل کولو لپاره، تاسو باید د کال منطق خورا ډیر بدل کړئ. د مثال په توګه، د CompletionStage له لارې (aka Completable Future):

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 ms ترلاسه کړو، کوم چې دمخه زموږ د تمې سره نږدې دی.

له بده مرغه، دا ډول ښه موازي شتون نلري، او د تادیه کولو قیمت خورا ظالمانه دی: یوازې د یو څو کاروونکو سره په ورته وخت کې کار کوي، د غوښتنو بیرغ به په خدماتو کې راشي، چې په هر صورت کې به موازي پروسس نشي، نو موږ بیرته به زموږ غمجن 4s ته راستون شي.

زما پایله د داسې خدمت کارولو په وخت کې د 1300 پیغامونو پروسس کولو لپاره 1700-20 ms دی. دا د لومړي پلي کولو په پرتله ګړندی دی ، مګر لاهم ستونزه نه حل کوي.

د موازي پوښتنو بدیل کارولЧто если в сторонних сервисах не предусмотрена пакетная обработка? Например, можно спрятать отсутствие реализации пакетной обработки внутри методов интерфейсов:

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 سره پسرلي وکاروئ ، او تاسو کولی شئ تل چمتو شوي کیچ لکه EhCache په لاسي ډول وکاروئ.

یوه عامه ستونزه به دا وي چې کیچونه یوازې هغه وخت ګټور دي چې دوی یې وهلي وي. زموږ په قضیه کې، د لیکوال په ساحه کې هیټونه خورا احتمال لري (راځئ چې ووایو، 50٪)، مګر په فایلونو کې به هیڅ هټ نه وي. دا طریقه به ځینې پرمختګونه وړاندې کړي، مګر دا به په بنسټیز ډول فعالیت بدل نه کړي (او موږ یو پرمختګ ته اړتیا لرو).

Intersession (اوږده) کیچ پیچلي باطل منطق ته اړتیا لري. په عموم کې ، هرڅومره چې تاسو د انټرسیشن کیچونو په کارولو سره د فعالیت ستونزو حل کولو ته ښکته شئ ، ښه.

پرو:

  1. د کوډ بدلولو پرته کیشینګ پلي کړئ.
  2. د تولید زیاتوالی څو ځله (په ځینو مواردو کې).

ضمیمه:

  1. که په غلطه توګه وکارول شي د فعالیت کمیدو امکان.
  2. لوی حافظه په سر کې ، په ځانګړي توګه د اوږدې کیچونو سره.
  3. پیچلي باطلول، هغه تېروتنې چې د چلولو په وخت کې د بیا تولید لپاره ستونزمنې ستونزې رامنځته کوي.

Очень часто кэши используются только для того, чтобы быстро залатать проблемы проектирования. Это не означает, что их не нужно использовать. Однако всегда стоит относиться к ним с осторожностью и вначале оценивать полученный прирост производительности, а уже потом принимать решение.

زموږ په مثال کې، کیچونه به د شاوخوا 25٪ فعالیت زیاتوالی چمتو کړي. په ورته وخت کې ، کیچونه خورا ډیر زیانونه لري ، نو زه به یې دلته ونه کاروم.

پایلې

Итак, мы рассмотрели наивную реализацию сервиса, использующего пакетную обработку, и несколько простых способов ее ускорить.

د دې ټولو میتودونو اصلي ګټه سادگي ده، چې له هغې څخه ډیری خوندورې پایلې شتون لري.

د دې میتودونو سره یوه عامه ستونزه ضعیف فعالیت دی ، په عمده توګه د کڅوړو اندازې له امله. له همدې امله، که دا حلونه ستاسو سره مناسب نه وي، نو دا د نورو بنسټیزو میتودونو په پام کې نیولو سره ارزښت لري.

دلته دوه اصلي لارښوونې شتون لري چې تاسو کولی شئ د حل په لټه کې شئ:

  • د معلوماتو سره غیر متناسب کار (د تمثیل بدلون ته اړتیا لري، نو پدې مقاله کې بحث نه کیږي)؛
  • د سنکرونس پروسس ساتلو پرمهال د بستونو پراخول.

د بستونو پراخول به د بهرني تلیفونونو شمیر خورا کم کړي او په ورته وخت کې کوډ همغږي وساتي. د مقالې راتلونکې برخه به دې موضوع ته وقف شي.

سرچینه: www.habr.com

Add a comment