許多復(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è)庫述寡,趕緊看看吧!