使用并行化策略將Elixir后端服務(wù)從120ms降至20ms

許多復(fù)雜的應(yīng)用需要將性能放在第一位,因?yàn)?高性能會取悅用戶艇肴,提高搜索引擎排名提高轉(zhuǎn)化率扫沼。這篇文章中,會以一個(gè)復(fù)雜的產(chǎn)品展示頁面為例脾歧,講述如何將響應(yīng)時(shí)間從120ms縮減了90%甲捏,到20ms。

首先鞭执,我們來看一個(gè)示例的產(chǎn)品頁面,注意到它展示的數(shù)據(jù)類型芒粹。我們使用關(guān)系型數(shù)據(jù)庫兄纺,所以所有信息都是標(biāo)準(zhǔn)化的,并且在不同的數(shù)據(jù)表中存儲化漆。因此估脆,這個(gè)頁面的大部分信息是基于商品的ID。為了渲染頁面座云,我們需要通過商品ID查找不同表(這些查詢不需要基于上次查詢的結(jié)果)疙赠。我們的初始設(shè)計(jì)并沒有用到ID的標(biāo)準(zhǔn)化結(jié)構(gòu),并且是順序查找每張表朦拖。

items = Item.by_ids(ids)
seller_ids = Enum.map(items, &(&1.user_id))
user_stats = UserStats.user_stats_map(seller_ids)
favorites = Favorite.favorites_map(seller_ids)
stores = User.stores_map(seller_ids)
# more queries...

Enum.map(items, fn item ->
    %{
      item: item,
      user_stats: Map.get(user_stats, item.selller_id),
      is_favorite: Map.get(favorites, item.id),
      store: Map.get(stores, item.seller_id),
      # more Map.gets
    }
  end
)

這種做法的通常性能不會太糟圃阳,但是當(dāng)其中一個(gè)查詢比較慢時(shí),其后的查詢就會被拖累璧帝。在性能測試中我們發(fā)現(xiàn)這個(gè)endpoint的速度并不令人滿意捍岳。

為了解決這個(gè)問題,我們咨詢了 Chris McCord睬隶。他給了我們架構(gòu)上的建議來優(yōu)化這段代碼锣夹。我們使用了 Task.Supervisor 來并行的執(zhí)行這些查詢。

首先苏潜,我們將 Task.Supervisor添加到 supervision tree 來保證我們能優(yōu)雅的處理子進(jìn)程的崩潰和結(jié)果银萍。

children = [
  # other children
  supervisor(Task.Supervisor, [[name: TptApi.TaskSupervisor]]),
]
Supervisor.start_link(children, opts)

現(xiàn)在我們就可以自信的使用 Task.Supervisor.async生成進(jìn)程了,讓我們能夠并行化原來的查詢序列恤左。

items = Item.by_ids(ids)
seller_ids = Enum.map(items, &(&1.user_id))

[]
|> get_user_stats(seller_ids)
|> get_favorites(seller_ids)
|> get_stores_by_user(seller_ids)
|> # more queries
|> Task.yield_many()
|> Enum.reduce(%{}, fn ({task, reply}, acc) ->
     case reply do
       {:ok, result} -> Map.merge(acc, result)
     end
   end)
|> generate_product_results(items)

上述代碼信息量比較大贴唇,我們一行行看

[]
|> get_user_stats(seller_ids)

它首先創(chuàng)建了一個(gè)空列表搀绣,然后pipe到get_user_stats函數(shù)中去。 get_user_stats則會創(chuàng)建一個(gè)新的Task來查詢數(shù)據(jù)庫滤蝠,并且將Task傳到空列表中豌熄。下面是get_user_stats:

def get_user_stats(tasks, seller_ids)
  [Task.Supervisor.async(TptApi.TaskSupervisor, fn ->
    %{user_stats: UserStats.user_stats_map(seller_ids)},
  end) | tasks]
end

每個(gè)pipeline中的函數(shù)功能近似,都是創(chuàng)建一個(gè)Task物咳,將其添加到list锣险,接著傳遞給下面。我們來看下緊接著的pipeline:

[]
|> get_user_stats(seller_ids)
|> get_favorites(seller_ids)

get_favorites創(chuàng)建一個(gè)新的Task并且添加到現(xiàn)有的Tasks列表中去览闰。

def get_favorites(tasks, seller_ids)
  [Task.Supervisor.async(TptApi.TaskSupervisor, fn ->
    %{favorites: Favorite.favorites_map(seller_ids)},
  end) | tasks]
end

我們在輔助函數(shù)中重復(fù) [Task.async | tasks] 芯肤,我們還想添加錯(cuò)誤處理和監(jiān)測功能到每個(gè)Task中去。我們把重復(fù)的功能抽象到一個(gè)獨(dú)立的扶助函數(shù)中去压鉴。

一旦查詢數(shù)據(jù)表的Task被分發(fā)崖咨,我們需要等待所有的Task完成查詢(注意:這意味著查詢總時(shí)間仍然受到最慢的那個(gè)限制)。Task.yield_many 是一個(gè)優(yōu)秀的解決方案油吭,它將等待所有Task完成才繼續(xù)pipeline击蹲。

一旦所有查詢完成之后,我們使用Enum.reduce來匯總結(jié)果:

|> Enum.reduce(%{}, fn ({task, reply}, acc) ->
     case reply do
       {:ok, result} -> Map.merge(acc, result)
     end
   end)

查看 *Task.yield_many example * 文檔來獲取更多信息婉宰。

最終歌豺,我們將保存所有查詢數(shù)據(jù)的 Map 給到 generate_product_results(items) ,這個(gè)函數(shù)將做一些其他小的處理來顯示數(shù)據(jù)心包。

這種方法能讓我們輕松監(jiān)測Task类咧,監(jiān)測能讓我們更好的發(fā)現(xiàn)和處理應(yīng)用的性能瓶頸。這是 Grafana 的圖像:

每條曲線對應(yīng)了一個(gè)異步查詢時(shí)間蟹腾,因此痕惋,總時(shí)間約為最高的那條曲線郭赐。假如查詢是序列化同步的話宪巨,那么總時(shí)間將會是所有曲線的疊加。

所有的這些性能優(yōu)化都讓應(yīng)用提升了速度愈腾,不信你看:

此外珊随,還有人根據(jù)這篇文章寫了個(gè)庫述寡,趕緊看看吧!

https://github.com/Fabianlindfors/parallel_task

原文地址:http://engineering.teacherspayteachers.com/2017/08/02/reducing-elixir-backend-time-from-120ms-to-20ms-with-parallelization.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末叶洞,一起剝皮案震驚了整個(gè)濱河市鲫凶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌衩辟,老刑警劉巖螟炫,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異艺晴,居然都是意外死亡昼钻,警方通過查閱死者的電腦和手機(jī)掸屡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來然评,“玉大人仅财,你說我怎么就攤上這事⊥胩剩” “怎么了盏求?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長亿眠。 經(jīng)常有香客問我碎罚,道長,這世上最難降的妖魔是什么纳像? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任荆烈,我火速辦了婚禮,結(jié)果婚禮上竟趾,老公的妹妹穿的比我還像新娘憔购。我一直安慰自己,他們只是感情好岔帽,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布倦始。 她就那樣靜靜地躺著,像睡著了一般山卦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诵次,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天账蓉,我揣著相機(jī)與錄音,去河邊找鬼逾一。 笑死铸本,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遵堵。 我是一名探鬼主播箱玷,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼陌宿!你這毒婦竟也來了锡足?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤壳坪,失蹤者是張志新(化名)和其女友劉穎舶得,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爽蝴,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沐批,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年纫骑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片九孩。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡先馆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出躺彬,到底是詐尸還是另有隱情煤墙,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布顾患,位于F島的核電站番捂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏江解。R本人自食惡果不足惜设预,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望犁河。 院中可真熱鬧鳖枕,春花似錦、人聲如沸桨螺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灭翔。三九已至魏烫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肝箱,已是汗流浹背哄褒。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留煌张,地道東北人呐赡。 一個(gè)月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像骏融,于是被迫代替她去往敵國和親链嘀。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容