1. 進(jìn)程標(biāo)志設(shè)置:
消息和binary內(nèi)存:erlang:process_flag(min_bin_vheap_size, 1024*1024),減少大量消息到達(dá)或處理過程中產(chǎn)生大量binary時(shí)的gc次數(shù)
堆內(nèi)存:erlang:process_flag(min_heap_size, 1024*1024),減少處理過程中產(chǎn)生大量term,尤其是list時(shí)的gc次數(shù)
進(jìn)程優(yōu)先級(jí):erlang:process_flag(priority, high),防止特殊進(jìn)程被其它常見進(jìn)程強(qiáng)制執(zhí)行reductions
進(jìn)程調(diào)度器綁定:erlang:process_flag(scheduler, 1),當(dāng)進(jìn)程使用了port時(shí),還需要port綁定支持,防止進(jìn)程在不同調(diào)度器間遷移引起性能損失,如cache、跨numa node拷貝等,當(dāng)進(jìn)程使用了port時(shí),主要是套接字,若進(jìn)程與port不在一個(gè)scheduler上,可能會(huì)引發(fā)嚴(yán)重的epoll fd鎖競(jìng)爭(zhēng)及跨numa node拷貝,導(dǎo)致性能嚴(yán)重下降
2. 虛擬機(jī)參數(shù):
+S X:X :啟用調(diào)度器數(shù)量,多個(gè)調(diào)度器使用多線程,有大量鎖爭(zhēng)用
-smp disable :取消smp,僅使用單線程,16個(gè)-smp_disabled虛擬機(jī)性能高于+S 16:16
+sbt db :將scheduler綁定到具體的cpu核心上,再配合erlang進(jìn)程和port綁定,可以顯著提升性能,但是如果綁定錯(cuò)誤,反而會(huì)有反效果
3. 消息隊(duì)列:
消息隊(duì)列長度對(duì)性能的影響主要體現(xiàn)在以下兩個(gè)方面:進(jìn)程binary堆的gc和進(jìn)程內(nèi)消息匹配,前者可以通過放大堆內(nèi)存來減少gc影響,后者需要謹(jǐn)慎處理。
若進(jìn)程在處理消息時(shí)是通過消息匹配方式取得消息,同時(shí)又允許其它進(jìn)程無限制投遞消息到本進(jìn)程,此時(shí)會(huì)引發(fā)災(zāi)難,匹配方式取得消息會(huì)引發(fā)遍歷進(jìn)程消息隊(duì)列,如果此時(shí)仍然有其它進(jìn)程投遞消息,會(huì)導(dǎo)致進(jìn)程消息隊(duì)列暴漲,遍歷過程也將增大代價(jià),引發(fā)惡性循環(huán)。已知模式有:在gen_server中使用file:write(raw模式)或gen_tcp:send等,這些操作都是erlang虛擬機(jī)內(nèi)部通過port driver實(shí)現(xiàn)的,均有內(nèi)部receive匹配接收,對(duì)于這些操作,最好的辦法是將其改寫為nif,直接走進(jìn)程堆進(jìn)行操作,次之為將file:write或gen_tcp:send改寫為兩階段,第一階段為port_command,第二階段由gen_server接收返回結(jié)果,這種異步化可能有些正確性問題,對(duì)于gen_tcp:send影響不大,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求本身要么同步化要么異步化,都需要內(nèi)部的確認(rèn)機(jī)制;對(duì)于file:write影響較大,file:write的錯(cuò)誤通常為目錄不存在或磁盤空間不足,確保這兩個(gè)錯(cuò)誤不造成影響即可,同時(shí)如果進(jìn)程的其它部分需要使用file的其它操作,必須首先清空之前file:write產(chǎn)生的所有file的port消息,否則有可能產(chǎn)生消息序列紊亂的問題。
對(duì)于套接字的接口調(diào)用,可以參考rabbitmq的兩階段套接字發(fā)送方法,而對(duì)于文件接口調(diào)用,可以參考riak的bitcask引擎將文件讀寫封裝為nif的方法
4. 內(nèi)存及ets表:
ets表可以用于進(jìn)程間交換大數(shù)據(jù),或充當(dāng)緩存,以及復(fù)雜匹配代理等,其性能頗高,并發(fā)讀寫可達(dá)千萬級(jí)qps,并有兩個(gè)并發(fā)選項(xiàng),在建立表時(shí)設(shè)置,分別是{write_concurrency, true} | {read_concurrency, true},以允許ets的并發(fā)讀寫
使用ets表可以繞過進(jìn)程消息機(jī)制,從而在一定程度上提高性能,并將編程模式從面向消息模式變?yōu)槊嫦蚬蚕韮?nèi)存模式
5. CPU密集型操作:
erlang執(zhí)行流程的問題:
1. 其指令都是由其虛擬機(jī)執(zhí)行的,一條指令可能需要cpu執(zhí)行3-4條指令,一些大規(guī)模的匹配或遍歷操作會(huì)嚴(yán)重影響性能;
2. 其bif調(diào)用執(zhí)行過程類似于操作系統(tǒng)的系統(tǒng)調(diào)用,需要對(duì)傳入?yún)?shù)進(jìn)行轉(zhuǎn)換,在大量小操作時(shí)損失性能較為嚴(yán)重
3. 其port driver流程較為繁冗復(fù)雜,需要經(jīng)歷大量的回調(diào)等,一般的小功能操作,不要通過port driver實(shí)現(xiàn)
建議:
字符串匹配不要通過list進(jìn)行,最好通過binary;單字節(jié)匹配,尤其是語法解析,如xmerl、mochijson2、lexx等,盡管使用binary,但是它們是一個(gè)字節(jié)一個(gè)字節(jié)匹配的,性能會(huì)退化到list的水平,應(yīng)該盡量將其nif化;
對(duì)于一些小操作,反而應(yīng)該去bif化、去nif化、去port driver化,因?yàn)檫M(jìn)入erlang內(nèi)部函數(shù)的執(zhí)行代價(jià)也不小;
已知的性能瓶頸:re、xmerl、mochijson2、lexx、erlang:now、calendar:local_time_to_universal_time_dst等
6. 數(shù)據(jù)結(jié)構(gòu):
減少遍歷,盡量使用API提供的操作
由于各種類型的變量實(shí)際可以當(dāng)做c的指針,因此erlang語言級(jí)的操作并不會(huì)有太大代價(jià)
lists:reverse為c代碼實(shí)現(xiàn),性能較高,依賴于該接口實(shí)現(xiàn)的lists API性能都不差,避免list遍歷,[||]和foreach性能是foldl的2倍,不在非必要的時(shí)候遍歷list
dict:find為微秒級(jí)操作,內(nèi)部通過動(dòng)態(tài)hash實(shí)現(xiàn),數(shù)據(jù)結(jié)構(gòu)先有若干槽位,后根據(jù)數(shù)據(jù)規(guī)模變大而逐步增加槽位,fold遍歷性能低下
gb_trees:lookup為微秒級(jí)操作,內(nèi)部通過一個(gè)大的元組實(shí)現(xiàn),iterator+next遍歷性能低下,比list的foldl還要低2個(gè)數(shù)量級(jí)
其它常用結(jié)構(gòu):queue,set,graph等
7. 計(jì)時(shí)器:
erlang的計(jì)時(shí)器timer是通過一個(gè)唯一的timer進(jìn)程實(shí)現(xiàn)的,該進(jìn)程是一個(gè)gen_server,用戶通過timer:send_after和timer:apply_after在指定時(shí)間間隔后收到指定消息或執(zhí)行某個(gè)函數(shù),每個(gè)用戶的計(jì)時(shí)器都是一條記錄,保存在timer的ets表timer_tab中,timer的時(shí)序驅(qū)動(dòng)通過gen_server的超時(shí)機(jī)制實(shí)現(xiàn)。若同時(shí)使用timer的用戶過多,則tiemr將響應(yīng)不過來,成為瓶頸。
更好的方法是使用erlang的原生計(jì)時(shí)器erlang:send_after和erlang:start_timer,它們把計(jì)時(shí)器附著在進(jìn)程自己身上。
8. 尾調(diào)用和尾遞歸:
尾調(diào)用和尾遞歸是erlang函數(shù)式語言最強(qiáng)大的優(yōu)化,盡量保持函數(shù)尾部有尾調(diào)用或尾遞歸
9. 文件預(yù)讀,批量寫,緩存:
這些方式都是局部性的體現(xiàn):
預(yù)讀:讀空間局部性,文件提供了read_ahead選項(xiàng)
批量寫:寫空間局部性
對(duì)于文件寫或套接字發(fā)送,存在若干級(jí)別的批量寫:
1. erlang進(jìn)程級(jí):進(jìn)程內(nèi)部通過list緩存數(shù)據(jù)
2. erlang虛擬機(jī):不管是efile還是inet的driver,都提供了批量寫的選項(xiàng)delayed_write|delay_send,
它們對(duì)大量的異步寫性能提升很有效
3. 操作系統(tǒng)級(jí):操作系統(tǒng)內(nèi)部有文件寫緩沖及套接字寫緩沖
4. 硬件級(jí):cache等
緩存:讀寫時(shí)間局部性,讀寫空間局部性,主要通過操作系統(tǒng)系統(tǒng),erlang虛擬機(jī)沒有內(nèi)部的緩存
10.套接字標(biāo)志設(shè)置:
延遲發(fā)送:{delay_send, true},聚合若干小消息為一個(gè)大消息,性能提升顯著
發(fā)送高低水位:{high_watermark, 128 * 1024} | {low_watermark, 64 * 1024},輔助delay_send使用,delay_send的聚合緩沖區(qū)大小為high_watermark,數(shù)據(jù)緩存到high_watermark后,將阻塞port_command,使用send發(fā)送數(shù)據(jù),直到緩沖區(qū)大小降低到low_watermark后,解除阻塞,通常這些值越大越好,但erlang虛擬機(jī)允許設(shè)置的最大值不超過128K
發(fā)送緩沖大小:{sndbuf, 16 * 1024},操作系統(tǒng)對(duì)套接字的發(fā)送緩沖大小,在延遲發(fā)送時(shí)有效,越大越好,但有極值
接收緩沖大小:{recbuf, 16 * 1024},操作系統(tǒng)對(duì)套接字的接收緩沖大小
11. 序列化/反序列化:
通常情況下,為了簡化實(shí)現(xiàn),一般將erlang的term序列化為binary,傳遞到目的地后,在將binary反序列化為term,這通常涉及到兩個(gè)操作:
term_to_binary及binary_to_term,這兩個(gè)操作性能消耗極為嚴(yán)重,應(yīng)至多只做一次,減少甚至消除它們是最正確的,例如直接構(gòu)造binary進(jìn)行跨虛擬機(jī)數(shù)據(jù)交換;
但對(duì)比與其它的序列化和反序列化方式,如利用protobuf等,term_to_binary和binary_to_term的性能是高于這些方式的,畢竟是erlang原生格式,對(duì)于力求簡單的應(yīng)用,其序列化和反序列化方式推薦term_to_binary和binary_to_term
12. 并發(fā)化
在一些場(chǎng)景下,如web請(qǐng)求、數(shù)據(jù)庫請(qǐng)求、分布式文件系統(tǒng)等,單個(gè)接入接口已經(jīng)不能滿足性能需求,需要有多個(gè)接入接口,多個(gè)數(shù)據(jù)通道,等等,這要求所有請(qǐng)求處理過程必須是無狀態(tài)的,或者狀態(tài)更改同步進(jìn)入一個(gè)公共存儲(chǔ),而公共存儲(chǔ)也必須是支持并發(fā)處理的,如并發(fā)數(shù)據(jù)庫、類hdfs、類dynamo存儲(chǔ)等,若一致性要求較高,最好選用并發(fā)數(shù)據(jù)庫,如mysql等,若在此基礎(chǔ)上還要求高可用,最好選擇同步多結(jié)點(diǎn)存儲(chǔ),
mnesia、zk都是這方面的典型;若不需要較高的一致性,類hdfs、類dynamo這類no sql存儲(chǔ)即可滿足
13. hipe
將erlang匯編翻譯成機(jī)器碼,減少一條erlang指令對(duì)應(yīng)的cpu指令數(shù)
本人主要從事游戲后端開發(fā),所以本文只從游戲開發(fā)角度分析Erlang使用中應(yīng)注意的問題和優(yōu)化點(diǎn)。
- 單節(jié)點(diǎn)還是多節(jié)點(diǎn)
Erlang節(jié)點(diǎn)之間的通信是透明的,節(jié)點(diǎn)內(nèi)部和外部之間的調(diào)用一致。基于這樣的特性,很多人會(huì)選用多節(jié)點(diǎn),把各子系統(tǒng)(登陸節(jié)點(diǎn),玩家節(jié)點(diǎn),地圖節(jié)點(diǎn),全局節(jié)點(diǎn)等)分配到不同的節(jié)點(diǎn)中,以支持更多的在線玩家。這樣做的出發(fā)點(diǎn)是好點(diǎn)是好的,但會(huì)引起一列表的問題:登陸、轉(zhuǎn)場(chǎng)邏輯復(fù)雜,節(jié)點(diǎn)間的消息廣播頻繁,玩家數(shù)據(jù)同步、一致問題,內(nèi)存消耗,運(yùn)維復(fù)雜化等。相比之下,單節(jié)點(diǎn)就簡單多了,不用考慮節(jié)點(diǎn)通信,玩家數(shù)據(jù)保證一致,運(yùn)維方便,一機(jī)多服。在開啟SMP的情間下,單節(jié)點(diǎn)的性能已經(jīng)很好。對(duì)頁游的業(yè)務(wù),同時(shí)在線達(dá)到5000人已經(jīng)非常少見,即使達(dá)到也是首服當(dāng)天才會(huì)出現(xiàn),單節(jié)點(diǎn)完全可以應(yīng)付這樣的情況,所以沒必要用多節(jié)點(diǎn),增加系統(tǒng)復(fù)雜性。
2. 消息廣播
消息廣播是游戲中的性能消耗大頭,主要包括地圖的行走、PK廣播,世界聊天廣播。世界聊天廣播可以通過CD等策劃手段限制,行走和PK包的廣播實(shí)時(shí)性高,只能通過技術(shù)手段解決。地圖中的廣播包,只需發(fā)給視野內(nèi)的玩家就可以,不用全地圖廣播。視野內(nèi)的玩家可以通過九宮格劃分,以 X,Y為主鍵,映射到對(duì)應(yīng)的玩家數(shù)據(jù)。以九宮格方式查找玩家非常高效,我第一個(gè)游戲,地圖中的玩家起初是保存在一個(gè)列表中,每次廣播時(shí)都要遍歷列表,找出同屏玩家,消息廣播非常低效,特別是在PK時(shí),CPU占用高。用九宮格優(yōu)化后,一切問題都解決了。還有一個(gè)優(yōu)化廣播問題的方法是數(shù)據(jù)包緩存。
3. 緩存-數(shù)據(jù)庫,網(wǎng)絡(luò)
緩存是用空間換時(shí)間,它是性能優(yōu)化中常用的方法。數(shù)據(jù)庫緩存,開服時(shí)把玩家的必要數(shù)據(jù)加載到內(nèi)存中,可以減少玩家的登陸延時(shí),應(yīng)對(duì)玩家并發(fā)登陸,刷新也很有效。同時(shí)玩家數(shù)據(jù)沒必要實(shí)現(xiàn)存庫,對(duì)于坐標(biāo),經(jīng)驗(yàn),金錢等變化頻繁的值,如果實(shí)時(shí)存在,會(huì)很容易壓跨數(shù)據(jù)庫或?qū)Υ鎺爝M(jìn)程造成消息阻塞。玩家改變的數(shù)據(jù)可以緩在內(nèi)存中,定時(shí)存庫,或下線時(shí)再存庫。網(wǎng)絡(luò)中的消息包也可以在應(yīng)用層給緩存起來,達(dá)到一定長度或延時(shí)一定時(shí)間后再發(fā)出去。雖然虛擬機(jī)和TCP層會(huì)做緩存,最好還是在應(yīng)用層做一次緩存。
4. 進(jìn)程-每玩家應(yīng)該有幾個(gè)進(jìn)程
其實(shí)每玩家一個(gè)進(jìn)程已經(jīng)足夠,代碼簡單,方便維護(hù),性能開銷小。沒必要為每個(gè)玩家開啟了網(wǎng)絡(luò),物品、任務(wù)等進(jìn)程,多個(gè)進(jìn)程不但造成進(jìn)程間通信開銷,還不好維護(hù)。
5. 善用進(jìn)程字典
Erlang中是不建議用進(jìn)程字典的,但進(jìn)程字典是數(shù)據(jù)存取最快的方式,對(duì)于游戲這種高性能要求的應(yīng)用,進(jìn)程字典是不二的選擇。使用進(jìn)程字典時(shí)要切記在對(duì)應(yīng)的進(jìn)程中操作,最好按功能把put,get操作封裝到模塊接口中,避免誤用。
6. 代碼規(guī)范
a. 代碼應(yīng)該簡單,邏輯清晰,把功能細(xì)分到函數(shù)中。函數(shù)一般不多于30行,每個(gè)模塊不多于1000行。
b. 寫尾遞歸函數(shù)一定要有清淅的退出條件,不要在函數(shù)中改變退出條件。一個(gè)退出條件不明確的尾遞歸,是造成消息阻塞,內(nèi)存耗盡的主要原因之一。
- %% 一個(gè)明確的尾遞歸函數(shù):
- loop([H |T]) ->
- do_something,
- loop(T);
- loop([]) ->
- ok.
- %% 存在錯(cuò)誤風(fēng)險(xiǎn)的寫法
- %% NewLiist不可預(yù)期,存在死循環(huán)風(fēng)險(xiǎn)
- loop([H | T]) ->
- NewList = do_something(H,T),
- loop(NewList);
- loop([]) ->
- ok.
c. 不要相信客戶端,上行的數(shù)據(jù)都需要驗(yàn)證,前端的請(qǐng)求都要做合法性判斷,防止出現(xiàn)外掛、刷錢刷物品、刷金幣的情況。
d 不要寫過多的case ,if嵌套,最好不要大于3個(gè)嵌套,通過 try catch 方法寫扁平化的代碼。
7. 自動(dòng)化工具
自動(dòng)化工具可以避免出錯(cuò),還把開發(fā)人員解放出來,提高生產(chǎn)效率。對(duì)于重復(fù)性,有規(guī)律的代碼(如數(shù)據(jù)存取,通信協(xié)議),可以分離出來,讓工具自動(dòng)生成。有了生成工具后,修改協(xié)議,新加字段等操作,簡單方便,不用為增加數(shù)據(jù)表中一個(gè)字段,而改十多個(gè)函數(shù)接口的修改;也不用擔(dān)心前后端協(xié)議不一致的問題。
8. 監(jiān)控系統(tǒng)
通過erlang:system_monitor/2,監(jiān)控系統(tǒng)long_gc,large_heap等情況。
9. 性能分析工具
準(zhǔn)備好top memory,top message_queue等查看系統(tǒng)屬性的工具,出問題時(shí)可以隨時(shí)查看。
本文鏈接:http://www.kongqingquan.com/archives/221
這幾天在弄個(gè)ERLANG的長連接測(cè)試程序,主要是要在一個(gè)服務(wù)器上建20萬條長連接.
于是找到了以下內(nèi)容.
---------------------------------------------
前些天給echo_server寫了個(gè)非常簡單的連接壓力測(cè)試程序,
代碼:- -module(stress_test).
-
- -export([start/0, tests/1]).
-
- start() ->
- tests(12345).
-
- tests(Port) ->
- io:format("starting~n"),
- spawn(fun() -> test(Port) end),
- spawn(fun() -> test(Port) end),
- spawn(fun() -> test(Port) end),
- spawn(fun() -> test(Port) end).
-
- test(Port) ->
- case gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}]) of
- {ok, _} ->
- test(Port);
- _ ->
- test(Port)
- end.
于是就求助于公司的linux編程牛人,結(jié)果讓我一倒... 客戶端沒有修改文件描述符個(gè)數(shù). windows上得在注冊(cè)表里面改.
牛人開始對(duì)這東西的性能感興趣了,剛好我摸了一陣子erlang的文檔,于是我倆就走向了erlang網(wǎng)絡(luò)連接的性能調(diào)優(yōu)之旅啦~~過程真是讓人興奮。 我們很快通過了1024這一關(guān)~~到了4999個(gè)連接,很興奮.
但為什么4999個(gè)連接呢, 檢查一下代碼終于發(fā)現(xiàn)echo_server.erl定義了一個(gè)宏, 最大連接數(shù)為5000. 我又倒~~
修改編譯之后, 連接數(shù)跑到101xx多了, 太哈皮了!
再測(cè)102400個(gè)連接時(shí),到32767個(gè)連接數(shù)erl掛了~說是進(jìn)程開得太多了. 好在記得這個(gè)erl的參數(shù)+P,可以定義erlang能生成的進(jìn)程數(shù). 默認(rèn)是32768. 改了!
后面不知怎么著,在81231個(gè)連接停止了. 新的性能瓶頸又卡了我們. 好在牛人對(duì)linux熟, 用strace(這東西會(huì)莫名地退出), stap查出一些苗頭. 我也想到在otp文檔好像提過另一個(gè)limit,那就是端口數(shù)...在此同時(shí)我們發(fā)現(xiàn)erlang在linux上是用的傳統(tǒng)poll模型. 但查erlang的源代碼發(fā)現(xiàn)是支持epoll的. 在網(wǎng)上搜了半天,終于搜到了個(gè)maillist的帖子.
- $./configure --enable-kernel-poll
由于我們的測(cè)試服務(wù)器是雙核的,我們?cè)谂渲玫臅r(shí)候也打開了smp支持. 歡快的make & make install之后....
把 /proc/sys/net/ipv4/ip_local_port_range 的內(nèi)容改成了1024到65535. 最多也也能改成65535 :)
- $echo 1024 65535 > ip_local_port_range
另外再添加一個(gè)erl的環(huán)境變量
- $export ERL_MAX_PORTS=102400
于是開始跑了,不過這次跑不一樣了
- $erl -noshell +P 102400 +K true +S 2 -smp -s echo_server start
- $erl -noshell +P 102400 +K true +S 2 -smp -s stress_test start
這里的+K true,表示使用內(nèi)核poll,+S 2 表示兩個(gè)核. 這樣可歡快啦~~~ 10w大關(guān)過咯! 而且比剛才沒用epoll的速度快暴多~~
于是我們又開始了204800個(gè)連接發(fā)測(cè)試了~~~
用top一看cpu占用率極低,服務(wù)器只在5%左右。 內(nèi)存也不是很大~~