多是因爲經驗太少,工做中常常會遇到問題,探究和解決問題的過程總想記錄一下,因此我寫博客常常是問題驅動,首先介紹一下今天要解決的問題:css
咱們在開發過程當中可能會遇到這樣的狀況:html
如我上篇文章 小時到分鐘 - 一步步優化巨量關鍵詞的匹配 中介紹的文本匹配服務,它是消息處理流程中的一環,被多個消息處理進程依賴,每次初始化進程要 6秒
左右時間構造 Trie 樹,並且服務讀取關鍵詞大文件、使用樹組構造 Trie 樹,會佔用大量(目前設置爲 256M
)內存。git
我已經把進程寫成了守護進程的形式,讓它們長時間執行,雖然不用更多地考慮初始化時間了,但佔用內存量巨大的問題沒有辦法。若是關鍵詞量再大一些,一臺機器上面跑十來個消息處理進程後就幹不了其餘了。github
並且,若是有需求讓我把文本匹配服務封裝爲接口給外部調用呢?咱們知道,web 服務時,每個請求處理進程的生存週期是從受理請求到響應結束,若是每次請求都用大量內存和時間來初始化服務,那接口響應時間和服務器壓力可想而知。web
這樣,服務形式必需要改變,咱們但願這個文本匹配這個服務能作到:算法
解決辦法也很簡單,就是把這個文本匹配的服務抽取出來,單獨做爲一個守護進程來運行,像一個特殊的服務器,多個「消息處理服務」在有須要時能調用此服務進程。數據庫
如今,咱們須要考慮文本匹配服務進程如何與外界通訊,接受匹配請求,響應匹配結果。繞來繞去,問題仍是回到了 進程間通訊
。json
進程間通訊(IPC,Inter-Process Communication),指至少兩個進程或線程間傳送數據或信號的一些技術或方法。進程是計算機系統分配資源的最小單位(嚴格說來是線程)。每一個進程都有本身的一部分獨立的系統資源,彼此是隔離的。爲了能使不一樣的進程互相訪問資源並進行協調工做,纔有了進程間通訊。服務器
進程間通訊的方式有不少,網上對此介紹的也不少,下面根據文章的需求來分析一下這些方式:網絡
FIFO
,它經過一個文件來進行進程間數據交互,但服務於多個進程時,須要添加鎖來保證原子性,從而避免寫入和讀取不對應。固然仍是有完美的方式的,這就是今天的主角 - Unix Domain Sockets
,它能夠理解爲一種特殊的 Socket,但它不須要通過網絡協議棧,不須要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另外一個進程,因此在系統內通訊效率更高。並且免去了網絡問題,它也更能保證消息的完整性,既不會丟失也不會順序錯亂。
做爲特殊的 Socket,它的建立、調用方式和網絡 Socket 同樣,一次完整的交互,服務端都要通過create、bind、listen、accept、read、write
,客戶端要經過create、connect、write、read
。與普通 Socket 不一樣的是它綁定一個系統內的文件,而不是 IP 和端口。
建立代碼這裏再也不多介紹了,以前的一篇文章 用C寫一個web服務器(一) 基礎功能 的功能實現
小節裏詳細介紹了 socket 通訊的具體步驟,C 系的語言都是類似的,很容易理解。
Unix Domain Sockets 真的是進程間通訊的一個重型武器,用它能夠快速實現進程間的數據、信息交互,並且不須要鎖等繁雜操做,也不用考慮效率,可謂是簡單高效。
固然,「重型武器」 的在各類場景下也有適合不適合。Unix Domain Sockets適用於如下場景:
接下來要 show code 了,不過學 PHP 的都知道,PHP 不太適合處理 CPU 密集形的任務,我恰好學了點 Go,一時手癢,就用 Go 實現了下 Trie 樹,因此才牽扯到 PHP 和 Go 之間的通訊,有了今天的文章。固然介紹的方法,並不僅適合 PHP 與 Go 通訊,其餘語言也能夠,至少 C系語言中是通用的。
完整代碼見 IPC-GitHub-枕邊書,裏面還附帶了一份隨手寫的 PHP 版本的 Unix Domain Sockets server 端。
Trie樹再也不是今天的主題,這裏介紹一下數據結構和須要注意的點。
// trie樹結點定義 type Node struct { depth int children map[int32]Node // 用map實現key-value型的 字符-節點 對應 }
須要注意:
append()
函數保存遞增的匹配結果時,有可能因爲 slice 容量不夠而從新分配地址,因此要傳入 slice 的地址來保存遞增後的匹配結果結果,*result = append(*result, word)
,最後再將遞增以後的 slice 地址傳回。utf-8
,不用像 PHP 同樣判斷字符的邊界,因此在進行關鍵詞拆散和消息拆散時,直接使用 int32()
方法將關鍵詞和消息都轉換爲成員爲 int32
類型的 slice,匹配過程當中就使用 int32
類型的數字來表明這個中文字符,匹配完成後再使用fmt.Printf("%c", int32)
將其轉換爲中文。Go 中建立一個 socket 並使用的步驟很是簡單,只是 Go 沒有異常,判斷 error 會比較噁心一點,不知道有沒有大神有更好的寫法。下面爲了精簡,把 error 全置空了。
// 建立一個Unix domain soceket socket, _ := net.Listen("unix", "/tmp/keyword_match.sock") // 關閉時刪除綁定的文件 defer syscall.Unlink("/tmp/keyword_match.sock") // 無限循環監聽和受理客戶端請求 for { client, _ := socket.Accept() buf := make([]byte, 1024) data_len, _ := client.Read(buf) data := buf[0:data_len] msg := string(data) matched := trie.Match(tree, msg) response := []byte("[]") // 給響應一個默認值 if len(matched) > 0 { json_str, _ := json.Marshal(matched) response = []byte(string(json_str)) } _, _ = client.Write(response) }
下面是 PHP 實現的客戶端:
$msg = "msg"; // 建立 鏈接 發送消息 接收響應 關閉鏈接 $socket = socket_create(AF_UNIX, SOCK_STREAM, 0); socket_connect($socket, '/tmp/keyword_match.sock'); socket_send($socket, $msg, strlen($msg), 0); $response = socket_read($socket, 1024); socket_close($socket); // 有值則爲匹配成功 if (strlen($response) > 3) { var_dump($response); }
這裏總結一下這套設計的效率表現:
純粹用 Go 進行文本關鍵詞匹配,一千條數據運行一秒多,差很少是 PHP 效率的兩倍。不過說好的 8倍效率呢?果真測評都是騙人的。固然,也多是我寫法有問題或者 Trie 樹不在 Go 的發揮範圍以內。而後是 PHP 使用 Unix Domain Socket 調用 Go 服務的耗時,多是進程間複製數據耗時或 PHP 拖了後腿,3秒多一點,跟純 PHP 腳本差很少。
用 PHP 的都知道,PHP 由於解釋型語言的特性和其高度的封裝,致使其雖然在開發上速度很快,但是執行與其餘語言相比略差。對此,業界的 FB 有 HHVM,PHP7 有 opcache 新特性,聽說還要在 PHP8 添加 JIT,用以彌補其先天硬傷。
不過,對於開發者,特別是跟我同樣對於效率有執著追求的人來講,在瞭解使用 PHP 的新特性以外,本身再掌握一門較高執行效率、開發效率略低的語言,用來寫一些高計算量,邏輯單一的代碼,與 PHP 互補或許會更好一點。
因而,在考慮良久,也見識了各類 Go 的支持者和反對者之間的撕逼後,我以爲仍是要相信一下谷歌爸爸,畢竟也沒什麼其餘我以爲可選的語言了。PS:請不要針對這一段發表意見,謝謝:)
另外C呢,雖然暫時開發中用不到,但是畢竟是當代N多語言的起源,偶爾寫寫數據結構、算法什麼的以避免生鏽。並且學了些C,從 PHP 到 Go,切換起來還略有些駕輕就熟的感受~
關於本文有什麼問題能夠在下面留言交流,若是您以爲本文對您有幫助,能夠點擊下面的 推薦 支持一下我。博客一直在更新,歡迎 關注 。