Masalah ngolah pamundut angkatan sareng solusina (bagian 1)

Masalah ngolah pamundut angkatan sareng solusina (bagian 1)Ampir kabéh produk software modern diwangun ku sababaraha layanan. Seringna, waktos réspon saluran antarlayanan anu panjang janten sumber masalah kinerja. Solusi standar pikeun masalah sapertos kitu nyaéta ngabungkus sababaraha pamundut antarlayanan kana hiji pakét, anu disebut batching.

Lamun make processing bets, Anjeun bisa jadi teu senang jeung hasil dina watesan kinerja atawa kajelasan kode. Metoda ieu teu jadi gampang dina panelepon anjeun bisa mikir. Pikeun tujuan anu béda sareng dina kaayaan anu béda, solusi tiasa bénten pisan. Nganggo conto khusus, kuring bakal nunjukkeun pro sareng kontra sababaraha pendekatan.

Proyék démo

Pikeun kajelasan, hayu urang tingali conto salah sahiji ladenan dina aplikasi anu ayeuna nuju dianggo.

Katerangan ngeunaan pilihan platform pikeun contoMasalah kinerja goréng cukup umum sareng henteu mangaruhan basa atanapi platform khusus. Artikel ieu bakal nganggo conto kode Spring + Kotlin pikeun nunjukkeun masalah sareng solusi. Kotlin sami-sami kaharti (atanapi teu kaharti) pikeun pamekar Java sareng C #, salian ti éta, kodeu langkung kompak sareng kaharti tibatan di Java. Pikeun ngagampangkeun kahartos pikeun pamekar Java murni, kuring bakal nyingkahan sihir hideung Kotlin sareng ngan ukur nganggo sihir bodas (dina sumanget Lombok). Bakal aya sababaraha métode extension, tapi maranéhna sabenerna wawuh ka sadaya programer Java salaku métode statik, jadi ieu bakal gula leutik nu moal ngaruksak rasa piring.
Aya jasa persetujuan dokumén. Aya anu nyiptakeun dokumén sareng ngirimkeunana pikeun diskusi, salami éditan dilakukeun, sareng pamustunganana dokumen éta disatujuan. Ladenan persetujuan sorangan henteu terang naon-naon ngeunaan dokumén: éta ngan ukur obrolan ngeunaan approvers kalayan fungsi tambahan anu alit anu urang moal nganggap di dieu.

Janten, aya kamar obrolan (cocog sareng dokumén) kalayan set pamilon anu tos siap di unggal masing-masing. Sapertos dina obrolan biasa, pesen ngandung téks sareng file sareng tiasa janten balesan atanapi payun:

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
)

File sareng tautan pangguna mangrupikeun tautan ka domain sanés. Di dieu urang hirup sapertos kieu:

typealias FileReference Long
typealias UserReference Long

Data pangguna disimpen dina Keycloak sareng dicandak liwat REST. Sami lumaku pikeun file: file na metainformation ngeunaan aranjeunna hirup dina layanan gudang file misah.

Sadaya telepon ka jasa ieu requests beurat. Ieu ngandung harti yén overhead pikeun ngangkut pamundut ieu langkung ageung tibatan waktos anu diperyogikeun pikeun diolah ku jasa pihak katilu. Dina bangku uji kami, waktos telepon umum pikeun jasa sapertos kitu nyaéta 100 ms, janten kami bakal nganggo nomer ieu di hareup.

Urang kedah ngadamel REST controller basajan pikeun nampa pesen N panungtungan kalayan sagala informasi diperlukeun. Nyaéta, kami yakin yén modél pesen dina frontend ampir sami sareng sadaya data kedah dikirim. Beda antara modél hareup-tungtung nyaéta yén file sareng pangguna kedah ditingalikeun dina bentuk anu rada dekripsi supados tiasa nyambungkeun:

/** В таком виде отдаются ссылки на сущности для фронта */
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
)

Urang kedah ngalaksanakeun ieu:

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

Postfix UI hartosna modél DTO pikeun frontend, nyaéta, naon anu kedah urang layanan via REST.

Anu matak héran di dieu nyaéta yén urang henteu ngaliwat ID obrolan sareng modél ChatMessage / ChatMessageUI henteu gaduh. Kuring ngalakukeun ieu ngahaja supados henteu ngaganggu kodeu conto (obrolanna terasing, ku kituna urang tiasa nganggap yén urang ngan ukur gaduh hiji).

Digression filosofisBoh kelas ChatMessageUI jeung métode ChatRestApi.getLast ngagunakeun tipe data Daptar lamun dina kanyataanana mangrupa susunan maréntahkeun. Ieu goréng dina JDK, jadi deklarasi urutan elemen dina tingkat panganteur (ngawétkeun urutan nalika nambahkeun jeung nyoplokkeun) moal jalan. Ku kituna eta geus jadi prakték umum pikeun pamakéan Daptar dina kasus dimana hiji susunan maréntahkeun diperlukeun (aya ogé LinkedHashSet, tapi ieu teu hiji panganteur).
Watesan penting: Urang bakal nganggap yén teu aya ranté panjang balesan atanapi transfer. Nyaéta, aranjeunna aya, tapi panjangna henteu langkung ti tilu pesen. Sakabéh ranté pesen kudu dikirimkeun ka frontend nu.

Pikeun nampi data tina jasa éksternal aya API ieu:

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

Ieu bisa ditempo yén jasa éksternal mimitina nyadiakeun keur processing bets, sarta dina duanana versi: ngaliwatan Set (tanpa preserving urutan elemen, kalayan konci unik) jeung ngaliwatan Daptar (bisa jadi aya duplikat - urutan dilestarikan).

palaksanaan basajan

palaksanaan naif

Palaksanaan naif munggaran tina REST controller kami bakal katingali sapertos kieu dina kalolobaan kasus:

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

Sagalana jelas pisan, sarta ieu téh tambah badag.

Kami nganggo pangolahan bets sareng nampi data tina jasa éksternal dina bets. Tapi naon anu lumangsung kana produktivitas urang?

Pikeun unggal pesen, hiji panggero ka UserRemoteApi bakal dilakukeun pikeun meunangkeun data dina widang panulis sarta hiji panggero ka FileRemoteApi pikeun meunangkeun sakabéh file napel. Sigana mah kitu. Hayu urang nyebutkeun yén widang forwardFrom na replyTo pikeun ChatMessage dimeunangkeun ku cara nu teu merlukeun telepon teu perlu. Tapi ngarobah kana ChatMessageUI bakal ngakibatkeun recursion, nyaeta, counters panggero bisa ngaronjat sacara signifikan. Salaku urang nyatet saméméhna, hayu urang nganggap yen urang teu boga loba nyarang sarta ranté nu dugi ka tilu pesen.

Hasilna, urang bakal nampi ti dua dugi ka genep telepon ka jasa éksternal per pesen sareng hiji telepon JPA pikeun sadaya pakét pesen. Jumlah total telepon bakal rupa-rupa ti 2*N+1 nepi ka 6*N+1. Sabaraha ieu dina unit nyata? Sebutkeun butuh 20 pesen pikeun ngajantenkeun halaman. Pikeun nampa aranjeunna, éta bakal nyandak tina 4 s ka 10 s. Heureuy! Abdi hoyong tetep dina 500 ms. Sarta saprak maranéhna ngimpi nyieun seamless ngagulung dina frontend, sarat kinerja pikeun titik tungtung ieu bisa dua kali.

pro:

  1. Kodena singket sareng ngadokumentasikeun diri (impian tim dukungan).
  2. Kodeu basajan, jadi ampir euweuh kasempetan pikeun némbak diri dina suku.
  3. processing bets teu kasampak kawas hal alien sarta organik terpadu kana logika.
  4. Parobihan logika bakal gampang dilakukeun sareng bakal lokal.

Minus:

Kinerja pikareueuseun kusabab pakét leutik pisan.

Pendekatan ieu tiasa sering ditingali dina jasa saderhana atanapi dina prototipe. Lamun laju nyieun parobahan penting, éta boro patut complicating sistem. Dina waktos anu sami, pikeun jasa anu saderhana pisan, prestasina pikareueuseun, janten ruang lingkup panerapan pendekatan ieu sempit pisan.

Ngolah paralel naif

Anjeun tiasa ngamimitian ngolah sadaya pesen paralel - ieu bakal ngamungkinkeun anjeun ngaleungitkeun kanaékan liniér dina waktos gumantung kana jumlah pesen. Ieu sanés jalur anu saé pisan sabab bakal ngahasilkeun beban puncak anu ageung dina jasa éksternal.

Ngalaksanakeun pamrosesan paralel saderhana pisan:

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

Ngagunakeun ngolah pesen paralel, urang meunang 300-700 ms ideally, nu leuwih hade tinimbang kalawan palaksanaan naif, tapi tetep teu cukup gancang.

Kalayan pendekatan ieu, pamundut ka userRepository sareng fileRepository bakal dieksekusi sacara sinkron, anu henteu épisién pisan. Pikeun ngalereskeun ieu, anjeun kedah seueur ngarobih logika telepon. Contona, via CompletionStage (alias 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()!!

Ieu bisa ditempo yén kode pemetaan mimitina basajan geus jadi kirang kaharti. Ieu kusabab urang kedah misahkeun telepon ka jasa éksternal ti mana hasilna dianggo. Ieu sorangan teu goréng. Tapi ngagabungkeun telepon teu kasampak pisan elegan jeung nyarupaan "mi" réaktif has.

Upami anjeun nganggo coroutines, sadayana bakal katingali langkung saé:

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

dimana:

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

Téoritis, ngagunakeun processing paralel misalna, urang bakal meunang 200-400 mdet, nu geus deukeut ka ekspektasi urang.

Hanjakalna, paralelisasi anu saé sapertos kitu henteu aya, sareng harga anu mayarna rada kejam: ngan ukur sababaraha pangguna anu damel dina waktos anu sami, seueur panyuwunan bakal tumiba kana jasa, anu henteu bakal diolah paralel, janten kami bakal balik deui ka sedih urang 4 s.

Hasil kuring nalika nganggo jasa sapertos kitu nyaéta 1300-1700 mdet pikeun ngolah 20 pesen. Ieu leuwih gancang ti dina palaksanaan munggaran, tapi tetep teu ngajawab masalah.

Pamakéan alternatif tina queries paralelKumaha upami jasa pihak katilu henteu nyayogikeun pamrosesan bets? Salaku conto, anjeun tiasa nyumputkeun kurangna palaksanaan pangolahan bets dina metode antarmuka:

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

Ieu masuk akal upami anjeun ngarepkeun ngolah angkatan dina versi anu bakal datang.
pro:

  1. Gampang nerapkeun pamrosésan paralel dumasar pesen.
  2. Skalabilitas anu saé.

kontra:

  1. Kabutuhan pikeun misahkeun akuisisi data tina ngolahna nalika ngolah pamundut ka jasa anu béda sacara paralel.
  2. Ngaronjatkeun beban dina jasa pihak katilu.

Ieu bisa ditempo yén wengkuan applicability téh kurang leuwih sarua jeung pendekatan naif. Masuk akal ngagunakeun metode pamundut paralel upami anjeun hoyong ningkatkeun kinerja jasa anjeun sababaraha kali kusabab eksploitasi anu teu karunya ka batur. Dina conto urang, kinerja ngaronjat ku 2,5 kali, tapi ieu jelas teu cukup.

Caching

Anjeun tiasa ngalakukeun cache dina sumanget JPA pikeun jasa éksternal, nyaéta, nyimpen obyék anu ditampi dina sési supados henteu nampi deui (kalebet nalika ngolah angkatan). Anjeun tiasa ngadamel cache sapertos nyalira, anjeun tiasa nganggo Spring sareng @Cacheable na, tambah anjeun tiasa nganggo cache siap-siap sapertos EhCache sacara manual.

Masalah umum nyaéta yén cache ngan ukur kapaké upami aranjeunna gaduh hits. Dina kasus urang, hits dina widang panulis pisan dipikaresep (hayu urang sebutkeun, 50%), tapi moal aya hits dina file pisan. pendekatan ieu bakal nyadiakeun sababaraha perbaikan, tapi moal radikal ngarobah kinerja (jeung urang kudu narabas a).

Intersession (panjang) caches merlukeun logika invalidation kompléks. Sacara umum, engké anjeun turun pikeun ngarengsekeun masalah kinerja nganggo cache intersession, langkung saé.

pro:

  1. Ngalaksanakeun cache tanpa ngarobah kode.
  2. Ningkatkeun produktivitas sababaraha kali (dina sababaraha kasus).

kontra:

  1. Kamungkinan ngirangan kinerja upami dianggo teu leres.
  2. overhead memori badag, utamana ku caches panjang.
  3. Invalidation kompléks, kasalahan nu bakal ngakibatkeun hard-to-reproduksi masalah dina runtime.

Sering pisan, cache ngan ukur dianggo pikeun nambal masalah desain gancang. Ieu henteu hartosna aranjeunna henteu kedah dianggo. Najan kitu, anjeun kudu salawasna ngubaran eta kalawan caution sarta mimiti evaluate hasil gain kinerja, sarta ngan lajeng nyandak kaputusan.

Dina conto urang, cache bakal nyadiakeun kanaékan kinerja sabudeureun 25%. Dina waktos anu sami, cache ngagaduhan seueur kalemahan, janten kuring henteu bakal ngagunakeunana di dieu.

hasil

Janten, urang ningali palaksanaan naif tina jasa anu ngagunakeun pangolahan bets, sareng sababaraha cara saderhana pikeun nyepetkeunana.

Kauntungan utama sadaya metodeu ieu nyaéta kesederhanaan, ti mana aya seueur akibat anu pikaresepeun.

Masalah umum sareng metode ieu nyaéta kinerja goréng, utamina kusabab ukuran pakét. Kukituna, upami solusi ieu henteu cocog sareng anjeun, maka éta patut mertimbangkeun metode anu langkung radikal.

Aya dua arah utama dimana anjeun tiasa milarian solusi:

  • karya Asynchronous kalawan data (merlukeun shift paradigma, jadi teu dibahas dina artikel ieu);
  • enlargement of bets bari ngajaga processing sinkron.

Enlargement of bets bakal greatly ngurangan jumlah telepon éksternal sarta dina waktos anu sareng tetep kode sinkron. Bagian saterusna artikel bakal devoted kana topik ieu.

sumber: www.habr.com

Tambahkeun komentar