[elixir! #0014][譯] 與 socket 來一次零距離接觸 by Saša Jurić

2016-01-25html

幾個月前,咱們目擊了Phoenix團隊在單個服務器上創建了200萬個併發鏈接。在此過程當中,他們還發現並消除了一些瓶頸。整個過程記錄在這篇優秀的文章裏。這個成就絕對是偉大的,但閱讀的過程當中我想到了一個問題:咱們真的須要一堆昂貴的服務器來研究咱們的系統在負載下的行爲嗎?git

在我看來,許多問題能夠在開發人員的機器上發現和處理,在這篇文章中,我將解釋如何完成。特別是,我將討論如何以編程方式「驅動」一個Phoenix套接字,談談傳輸層,而後在個人開發機器上建立一個五十萬鏈接的Phoenix套接字, 並探索進程休眠對內存使用的影響。github

目標

主要思想至關簡單。我將開發一個做爲 helper 的 SocketDriver模塊,這將容許我在一個單獨的 Erlang 進程中建立一個Phoenix套接字,而後經過向它發送通道專屬的消息來控制它。web

假設咱們有一個具備一個套接字( socket )一個通道( channel )的Phoenix應用程序,咱們將可以在一個單獨的進程中建立一個套接字:shell

iex(1)> {:ok, socket_pid} = SocketDriver.start_link(
          SocketDriver.Endpoint,
          SocketDriver.UserSocket,
          receiver: self
        )

receiver: self 語句指定了全部的傳出消息(由套接字發送到另外一方的消息)將做爲純Erlang消息發送到調用者進程。編程

如今我能夠要求socket進程加入通道:安全

iex(2)> SocketDriver.join(socket_pid, "ping_topic")

而後,我能夠驗證套接字發回的響應:服務器

iex(3)> flush

{:message,
 %Phoenix.Socket.Reply{payload: %{"response" => "hello"},
  ref: #Reference<0.0.4.1584>, status: :ok, topic: "ping_topic"}}

最後,我還能夠將消息推送到套接字並驗證傳出的消息:websocket

iex(4)> SocketDriver.push(socket_pid, "ping_topic", "ping", %{})

iex(5)> flush
{:message,
 %Phoenix.Socket.Message{event: "pong", payload: %{}, ref: nil,
  topic: "ping_topic"}}

有了這樣的驅動程序,我如今能夠輕鬆地從iex shell建立一堆套接字,並與它們一塊兒玩。稍後你會看到一個簡單的演示,但首先讓咱們先來探討如何開發這樣的驅動程序。網絡

可能的方法

建立和控制套接字能夠在Phoenix.ChannelTest模塊的幫助下輕鬆完成。使用宏和函數,如connect/2subscribe_and_join/4push/3,您能夠輕鬆建立套接字,加入通道和推送消息。畢竟,這些宏就是爲了在單元測試中以編程方式驅動套接字而建立的。

這種方法應該在單元測試中能很好地工做,但我不肯定它是適合負載測試。最重要的緣由是這些函數本是在測試進程中調用的。這能完美適用於單元測試,但在負載測試中我想更接近真實的東西。也就是說,我想在一個單獨的進程中運行每一個套接字,在這一點上,我須要作的內部操做量增長了,我實際上實現了一個phoenix 套接字傳輸層(我會在一分鐘內解釋這意味着什麼) 。

此外,Phoenix.ChannelTest彷佛依賴於套接字和通道的一些內部函數,而且它的函數爲每一個鏈接的客戶端建立了一個%Socket{}結構體,這是目前現有的Phoenix傳輸層不能完成的。

因此,我將實現SocketDriver來做爲部分的Phoenix傳輸層,也就是一個能夠用於建立和控制套接字的 GenServer。這將使我更接近現有的傳輸層。此外,這是一個有趣的進程,瞭解 phoenix 內部的東西。最後,這種套接字驅動程序能夠用於超出負載的測試目的,例如暴露可能存在於 Cowboy 和 Ranch 以外的不一樣接入點。

套接字,通道,傳輸層和套接字驅動

在進一步以前,讓咱們來討論一些術語。

套接字和通道的想法很簡單,但很是優雅。套接字是客戶端和服務器之間抽象的長時間運行的鏈接。消息能夠經過websocket,長輪詢,或幾乎任何其餘東西來傳輸層。

一旦套接字創建,客戶端和服務器可使用它來進行各類話題下的多人交流。這些對話稱爲通道,它們共同交換消息和管理每一側的通道的特定狀態。

相應的進程模型是至關合理的。一個進程用於一個套接字,一個進程用於一個通道。若是客戶端打開了2個套接字並在每一個套接字上鍊接了20個話題,咱們將最終有42個進程:2 *(1個套接字進程+ 20個通道進程)。

phoenix套接字傳輸層是長期運行的鏈接的驅動。多虧了傳輸層,咱們能夠放心地假設 Phoenix.SocketPhoenix.Channel和你本身的通道,正在穩定的,長期運行的鏈接上運做,而無論這個鏈接其實是如何驅動的。

您能夠實現本身的傳輸層,從而向您的客戶端公開各類通訊機制。另外一方面,實現傳輸層有點複雜,由於在這個層混合了各類需求。特別是,一個傳輸層必須:

    管理雙向狀態性鏈接
    接受傳入的消息並將其分派到通道
    對通道消息作出反應並分派對客戶端的響應
    在HashDict(一般也使用反向映射)中管理話題到通道進程的映射
    捕獲退出,對通道進程的退出做出反應
    提供底層http服務器庫的適配器,例如Cowboy

在我看來,捆綁在一塊兒的不少職責,使得一個傳輸層的實現更復雜,引入了一些重複代碼,並使傳輸層不那麼靈活。我和Chris和José分享了對這些見解,因此有可能在將來改進它們。

因此,若是你想實現一個傳輸層,你須要解決上面的點,可能保留一個:若是你的傳輸層不須要經過http端點暴露,你能夠跳過最後一點,例如, 你不須要實現Cowboy(或一些其餘Web庫)適配器。這意味着你再也不是phoenix傳輸層了(由於你不能經過終端訪問),但你仍然可以建立和控制一個phoenix套接字。這就是我所謂的套接字驅動。

實現

按照上面的列表,SocketDriver的實現是至關直接的,但有些複雜,因此我將避免逐步解釋。你能夠在這裏找到完整的代碼,包括一些基本的意見。

它的要點是,你須要在適當的時刻調用一些Phoenix.Socket.Transport函數。首先,須要調用connect/6建立套接字。而後,對於每一個傳入消息(即由客戶端發送的消息),您須要調用dispatch/3。在這兩種狀況下,您都會獲得一些必須處理的限定於通道的響應。

此外,您須要對從通道進程和PubSub層發送的消息作出反應。最後,您須要檢測通道進程的終止,並從你的內部狀態中刪除相應的條目。

我應該提到,這SocketDriver使用一個沒有文檔的Phoenix.ChannelTest.NoopSerializer - 一個不編碼/解碼消息的串行器。這使得事情保持簡單,但測試中也就沒有了編碼/解碼的工做。

建立500k套接字和通道

使用SocketDriver,咱們如今能夠輕鬆地在本地建立一系列套接字。我將在prod環境中這樣作,以更真實地模仿生產。

一個簡單的套接字/通道的基本Phoenix服務器能夠在這裏找到。我須要在prod(MIX_ENV = prod mix compile)編譯它,而後我就能夠啓動它:

MIX_ENV=prod PORT=4000 iex --erl 「+P 10000000」 -S mix phoenix.server

—erl 「+ P 10000000」選項將缺省最大進程數增長到1000萬。我計劃建立500k套接字,因此我須要一百多萬個進程,但爲了安全起見,我選擇了一個更大的數字。建立套接字如今很簡單:

iex(1)> for i <- 1..500_000 do
          # Start the socket driver process
          {:ok, socket} = SocketDriver.start_link(
            SocketDriver.Endpoint,
            SocketDriver.UserSocket
          )

          # join the channel
          SocketDriver.join(socket, "ping_topic")
        end

在個人機器上建立全部這些套接字須要一分鐘,而後我能夠啓動觀察者。看看系統選項卡,我能夠看到大約一百萬的進程正在運行,如預期:

圖片描述

我還應該提到我已經將默認記錄器級別設置更改成:warn 在 prod 環境。默認狀況下,此設置爲:info, 將把一堆日誌轉儲到控制檯。這反過來可能會影響你的負載生成器的吞吐量,因此我提升了這個級別, 靜音沒必要要的消息。

此外,爲了使代碼能夠開箱即用,我刪除了對prod.secret.exs文件的須要。顯然是一個很是糟糕的作法,但這只是一個演示,因此咱們應該沒問題。請記住,避免在個人(或你本身的)黑客實驗之上開發任何產品:-)

休眠進程

若是你仔細看看上面的圖片,你會看到大約6GB的內存使用有點高,雖然我不會稱之爲過多的, 畢竟建立了這麼多的套接字。我不知道Phoenix團隊是否作了一些內存優化,因此有可能這個開銷在將來的版本可能會減小。

就這樣,讓咱們看看進程休眠是否能夠幫助咱們減小內存開銷。注意這是一個初步的實驗,因此不要得出任何肯定的結論。這將更像一個簡單的演示,咱們能夠經過在咱們的開發盒上建立一堆套接字,並在本地瀏覽各類路由,快速得到一些看法。

首先一點理論。您能夠經過使用如下命令來減小進程的內存使用:erlang.hibernate/3。這將觸發進程的垃圾回收,收縮堆,截斷堆棧,並使進程處於等待狀態。該進程將在收到消息時被喚醒。

當談到GenServer時,您能夠經過在回調函數中添加:hibernate 原子到大多數返回元組來請求休眠。因此例如代替{:ok,state}{:reply,response,state},你能夠從init/1handle_call/3回調中返回{:ok,state,:hibernate}{:reply,response,state,:hibernate}

休眠能夠幫助減小不常常活動的進程的存儲器使用。你增長了一些CPU的負載,但你回收了一些內存。像生活中的大多數其餘的東西,休眠是一個工具,而不是一個銀彈。

所以,讓咱們看看咱們是否能夠經過休眠套接字和通道進程得到一些東西。首先,我將經過在SocketDriver中添加:hibernateinithandle_casthandle_info 回調, 來修改SocketDriver。有了這些更改,我獲得如下結果:

圖片描述

這大約減小了40%的內存使用,這彷佛頗有但願。值得一提的是,這不是一個決定性的測試。我休眠我本身的套接字驅動程序,因此我不知道是否相同的保存將發生在websocket傳輸層,這不是基於GenServer。可是,我稍微更肯定休眠可能有助於長輪詢,在那裏一個套接字由GenServer進程驅動,這相似於SocketDriver(事實上,我在開發SocketDriver時查閱了Phoenix不少代碼)。

在任何狀況下,這些測試應該在實際傳輸層中重試,這是爲何這個實驗有點勉強和不肯定的一個緣由。

不管如何,讓咱們繼續,嘗試休眠通道進程。我修改了deps/phoenix/lib/phoenix/channel/server.ex使通道進程休眠。從新編譯deps和建立500k套接字後,我注意到額外的內存節省800MB:

圖片描述

休眠套接字和通道後,內存使用量減小了50%以上。不是太寒酸 :-)

固然,值得重複的是休眠帶來的是CPU使用的增長。經過休眠,咱們迫使一些工做當即完成,因此應該仔細使用,並應衡量對性能的影響。

此外,讓我再次強調,這是一個很是初步的測試。最多這些結果能夠做爲一個指示,一個線索是否休眠可能有幫助。就我的而言,我認爲這是一個有用的提示。在真實系統中,您的通道狀態可能會更復雜,而且可能會執行各類轉換。所以,在某些狀況下,偶爾的休眠可能帶來一些不錯的節省。所以,我認爲Phoenix應該容許咱們經過回調元組請求咱們的通道進程的休眠。

結論

本文的主要內容是,經過驅動Phoenix套接字,你能夠快速得到一些關於你的系統在更重要負載下的行爲的看法。你能夠啓動服務器,啓動一些綜合加載器,並觀察系統的行爲。你能夠收集反饋,更快地嘗試一些備選方案,在過程當中你不須要爲大型服務器支付大量資金,也不須要花費大量時間調整操做系統設置以適應大量開放網絡套接字。

固然,不要誤認爲這是一個完整的測試。雖然驅動套接字能夠幫助你獲得一些看法,但它不描繪整個畫面,由於網絡I / O被繞過。此外,因爲加載器和服務器在同一機器上運行,所以競爭相同的資源,結果可能偏斜。密集加載器可能會影響服務器的性能。

爲了獲得整個畫面,你可能想要在相似於生產的服務器上使用單獨的客戶端機器運行最終的端到端測試。可是你能夠更少地這樣作,而且更有信心在處理更復雜的測試階段以前處理了大多數問題。在個人經驗中,許多簡單的改進能夠經過在本地執行系統來完成。

最後,不要對綜合測試過度信任,由於它們不能徹底模擬現實生活的混亂和隨機模式。這並不意味着這樣的測試是無用的,但它們絕對不是決定性的。正如老話說的:「沒有像生產同樣的測試!」:-)

Copyright 2014, Saša Jurić. This article is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
The article was first published on the old version of the Erlangelist site.
The source of the article can be found here.

相關文章
相關標籤/搜索