[Erlang23]怎麼有效的遍歷ETS表?


 
最近處理的Bug,記錄下:
出現的問題:
不穩定出現gen_server:call/3 的timeout;
直接原因:是call的timeout時間爲10s,但遍歷ets表處理時間大於10s[居然會有進程處理一個請求大於10s,真是奇蹟,下面再解釋原因].
 
1.最原始的代碼處理:
 
%%遍歷表並回寫數據
%%do_update_data/2裏面做了ets:instert/2更新的操作和其它處理
handle_call({update_data,Request}, _From, State) ->
  [begin do_update_data(Item,Request)end|| Item<-ets:tab2list(?TABLE_NAME)],
  {reply, ok, State};
一看,居然有ets:tab2list/1這種取數據的操作,當表數據記錄少時,不會出問題,但是當數量達到14w時,這樣一次性取數據就會把內存驟然加大【一直想不通爲什麼ETS設計者要把這種函數導出來...】。
 
2.改進後代碼處理:
handle_call({update_data,Func}, _From, State) ->
  ets:safe_fixtable(?TABLE_NAME, true),
  update_data(ets:first(?TABLE_NAME), Func),
  ets:safe_fixtable(?TABLE_NAME, false),
  {reply, ok, State}.

update_data('$end_of_table',_) ->
  ok;
update_data({key, Value1, Value2, _, _, _}=Idx,Func)
  when Value1 == "test"; value1 == '_' ->
  case ets:lookup(?TABLE_NAME, Idx) of
    [Ele] -> Func(Ele);
    [] -> ok
  end,
  update_data(ets:next(?TABLE_NAME, Idx),Func);
update_data(Idx,Func) ->
  update_data(ets:next(?TABLE_NAME, Idx), Func).

鎖定一個類型是 set,bag 或 duplicate_bag 的表,使其可以安全遍歷表裏的數據;

在一個進程裏調用 ets:safe_fixtable(Tab, true) 可以鎖定一個表,直到在進程裏調用 ets:safe_fixtable(Tab, false) 纔會解鎖,或進程崩潰。

如果同時有幾個進程鎖定一個表,那麼表會一直保持鎖定狀態,直到所有進程都釋放它(或崩潰)。有一個引用計數器記錄着每個進程的操作,有 N 個持續的鎖定操作必須有 N 個釋放操作,表纔會真正被釋放。

當一個表被鎖定,一序列的 ets:first/1 和 ets:next/2 的調用都會保證成功執行,並且表裏的每一個對象數據只返回一次,即使在遍歷的過程中,對象數據被刪除或插入。在遍歷過程中插入到表裏的新數據可能由 ets:next/2 返回(這取決有鍵的內部順序)。
 
所以不用擔心你在遍歷同時又做了ets:insert/2操作手,遍歷還是不是有效的【絕對有效】.
 
3.那麼還有什麼不對麼?
  對於gen_server進程還除了處理這種請求外還有其它事,如果一個請求時間處理過長,其它的請求就會連鎖timeout.
再改進:
handle_call({update_data,Func}, _From, State) ->
  ets:safe_fixtable(?TABLE_NAME, true),
  update_data(ets:first(?TABLE_NAME), Func,0),
  ets:safe_fixtable(?TABLE_NAME, false),
  {reply, ok, State}.

update_data('$end_of_table',_,_) ->
  ok;
update_data({key, Value1, Value2, _, _, _}=Idx,Func,Counter)
  when Value1 == "test"; value1 == '_' ->
  case ets:lookup(?TABLE_NAME, Idx) of
    [Ele] -> Func(Ele);
    [] -> ok
  end,
  NewCounter =
    if Counter >=50 ->
      timer:sleep(10),1;
      true ->
        Counter+1
    end,
  update_data(ets:next(?TABLE_NAME, Idx),Func,NewCounter);
update_data(Idx,Func,Counter) ->
  update_data(ets:next(?TABLE_NAME, Idx), Func,Counter).
加了一個參數counter,每處理50個就會sleep 10ms,這樣sleep時間一到時,就會所有的消息隊列重新有均等的機會來競爭;
 
4. 上面這個方案有一個缺點:會把這個遍歷ETS表的操作時間再增加Length*10ms的時間。明顯很不好。
我們再換換:
 
 
這樣就可以把讀寫分開,並且把每一條記錄單獨處理,完全解決了被請求進程處理時間過長的問題,
代碼改動太多,就不貼了,基本思路就是新造一個gen_server用來把這個非常耗時的請求分成衆多小的請求
 
也許你會想:爲什麼不直接在請求進程裏面分成小請求,就不用新起進程了,
如果你的請求進程做的事不多,也可以用這樣做,但是在實際中請求進程也會處理大量的請求,所以爲了不把請求進程負擔加重,
最好的方法還是新起進程來做特定的事。
 
 
通過這個Bug,我明顯感覺到了寫代碼心態很重要:
其實這個Bug最直接的修復方式就是把Timeout時間變成infinity就可以解決Bug,
但是通過分析原因,我們可以把最根本的設計問題解決掉,這種感覺真是太棒啦!!!
 
最後:大家以後千萬不要在項目中使用ets:tab2list/1來做遍歷啦
 

 
坐車坐過站,就是這樣子的。。。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章