Go在百萬億級搜尋引擎中的應用

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

Poseidon 系統是由 360 開源的記錄搜尋平台,目前已經用到了生產環節中,可以在數百萬億條、數百 PB 大小的日誌資料中快速分析和檢索特定字串。因為 Golang 得天獨厚的支援並發編程,Poseidon 的核心搜尋引擎、發報器、查詢代理是用 Golang 開發的,在核心引擎查詢、多天查詢、多天資料非同步下載中大量使用了 goroutine+channel 。

大家上午好,我是郭軍,很高興今天在這裡和大家交流。我今天演講題目,Golang 在百萬億搜尋引擎中的應用。Poseidon在希臘意思是海神,在這裡是海量資料集的主宰者。

之前我的工作一直面向海量使用者,去年年中我接觸大資料以及海量資料這樣的情境,在今天的演講中,主要會涉及以下幾方面內容:

設計目標

Go 應用情境與遭遇的挑戰

怎樣應對?

開源的改變

總結

設計目標

首先說一下為什麼要做這個系統。這是一個安全公司,APT ( 高危威脅持久性事件)。在追查APT事件的時候,我們通常會找一個樣本在某一樣時間之內到底做了什麼事情。在海量日誌中找這些資訊的話,運氣好不堵塞的時候,大約兩、三小時可以跑出來,如果運氣不好,跑的任務太多堵塞的話就要修複,可能一天兩天才能出來資料,顯然這樣的效率是不高的。

我們的設計目標,我們總的資料量保留三年的曆史資料,一共有一百萬億條,大小有 100 PB。秒級互動式搜尋響應,從前端發起請求到某一天資料,我們會在幾秒鐘之內給你返回。我們之前設定秒級60秒返回就可以,實際上做完之後測試的結果都在3秒到5秒之內,90%請求在10秒之內。每天要支援兩千億資料量灌入,未經處理資料僅存一份,對現有 MR 任務無侵略。ES 未經處理資料不止存一份,會再存一份,我們這麼大資料量來說,再存副本的話,維護成本以及代價是非常大的。ES 支援不了百萬億級資料量,現在業界做到一千億,我們只做到300多G。然後自訂的分詞策略,我們每一個業務的日誌格式都不一樣,分詞策略需要特別靈活;然後容錯移轉節點負載平衡,自動回復,支援原始日誌的批量下載。


圖1

圖1是我們總體流程,這個圖比較複雜,我們之前有同事分享過這個架構。如果今天再分享架構可能時間會不夠,圖2是它的一個非常簡單的粗略圖。


圖2

Go 應用情境與遭遇的挑戰

首先原始日誌。 在轉化的時候我們把每 128 行原始日誌抽取出來作為一個文檔,多個文檔連接在一起形成一個檔案。這裡會有人問為什麼選擇 128 行,我們每天日誌量是700億,按照每一行一個文檔我們有700 億文檔。一行日誌一個文檔,700 億文檔佔用空間太大;700 億資料會膨脹。選擇 128 行是因為:第一,700 億除 128 ,大約是 5.46 億左右,在一定範圍內可以承受;第二,因為我們的ID都是數字形式,以發號器形式發出來的,我們壓縮數位時候,肯定要採取各種各樣的壓縮辦法,我們在這個地方用的插分,對於128 數位壓縮是比較好的。壓縮 128 行日誌對比壓縮1行日誌高很多。我們每天原始日誌,我說的業務每天原始日誌有 60 ,壓縮之後我們能打成 10 左右,這是每天的資料。我們在輸出的時候,這個是原始的日誌,最後就要到原始日誌裡面找,最後就要構建資料。因為我們要存入進去的時候,剛剛我說的一句話,很多人不明白,多個串連起來形成一個檔案。有一個非常大的優勢,裡面的資料我放到另外一個檔案裡面,我一直疊加,最後這個檔案可以被解壓。換一種方式來說,把檔案都輸出到一個檔案裡面,作為這一個檔案,我從這個檔案裡面取出某一段來,我就可以解壓出來,這是一個非常大的特性。因為我需要讀一段日誌,我肯定要知道這個我從哪個地方讀到哪個地方,我要知道我讀的壓縮檔,解壓出來就是128行日誌。我們把整個原資料放到這裡面,去建索引以及原資料,大體就是這樣一個流程。首先看一下離線引擎,用戶端請求日誌,包括 PC 衛士、網路以及瀏覽器等等,這塊相當於傳統搜尋引擎的爬蟲。下面會具體講到,離線產生 DocGz 、DocGzmeta ,然後構建原資料。線上引擎,web 我們做簡單的頁面開發,到 proxy 叢集,再發到 searcher 叢集,然後走到 readHDFS ,readHDFS這個服務是用 Java開發,用 Java 開發有很多坑,但是又不得不用,因為java仍然是操作hadoop最合適的語言。

來說一下資料結構。 我們用 ProtrBuffer 描述核心資料結構。每一個 ID 下面分為兩段,那個 docID 就是我這個文檔的編號;第二是 rowIndex,每個裡面都會對應多行日誌,我這裡面對應 128 行裡面哪一行日誌,就是這個做的定位。我們用 map 的形式描述出來,這個是由 DocID 形成的列表,每一個裡面會對應多個DocIDList。map 和 string 裡面,我要先找到 map ,然後再把資料拿出來。3所示。


圖3

說一下搜尋引擎的核心技術。 首先倒排索引,倒排索引有一個趨勢,DocidList 非常長。我們一個分詞會先計算出來 hashid ,知道 hashid 之後要查詢的時候我們要做一個平台,給出要查詢哪一個業務,比如我要查網路等等這些,我們以業務的簡寫拼接上hashid,然後要查詢的時間,查詢哪一天的資料,我們引擎不是即時,因為資料量太大做不了即時,只能做到今天查昨天。然後解析 invertedindex 拿到對應的文檔資訊在裡面,找到這個位置之後,把我們所有的需要的原資料抽出來,然後解壓。我們就知道某一個分詞對應著 DocidList 是哪一個,根據 DocidList 去查要查的 map 資訊在哪個地方,擷取之後再拼一個路徑,把未經處理資料拿出來。拿出未經處理資料之後,一個檔案裡面會有 128 行日誌,這 128 行日誌Doc裡面rowindx 找到文檔在哪一行,做過濾就可以了。用非常簡單的話來總結一下,因為 Docid 比較長,我們存一個位置,我們的 DocidList 每一個 Docid 對應的文檔也比較多,我們讀原始文檔的時候,也會存一個位置,在電腦領域中,各種難以解決的問題都可以添加一個間接的中介層來解決這個問題。4所示。這句話在我們系統中有了很好的嘗試,不僅是這一塊。


圖4

再來說一下 idgeneratror 。 按照每天業務 27700 億來算,分詞以後是 100 億,每一個分詞對應 277 行日誌,這是平均數,每天 Docid 有 27700 億個。按照每個 4 位元組來計算,光是 Docid 數字將近 11TB。在這裡進行了處理,採用分段區間擷取降低 qps,每天的 id 重新從 0 開始分配。我們每天 Docid 倒排索引量在2.4T。每天 27700 億我們做起來也稍微有點發怵,我們想了一個辦法,我們業務名加時間作為 key,每天id 從零開始重新分配,這樣就可以保證我每天的量不至於太高,而且分出來的 Docid 不用太大,如果太大的話,可能資料就會比較膨脹。我現在建了索引是哪個業務,什麼時間段,哪一天的,我這次要請求哪一個區段,如果說我請求了 1 到 100 個這個區段,在 idgeneratro 會提前預留出 1 到 100 這個空隙。

Proxy/Searcher詳細設計。 Searcher核心引擎就是走四級索引裡面做的事情,其中包括過濾和模糊查詢等等,這些不是主幹業務我沒有說。從裡面拿出map資料,然後再取未經處理資料,取完資料以後,我們有很多未經處理資料非常大,大約有幾十兆左右,如果放在處理器前端,前面會直接卡死,我們會把未經處理資料比較大的業務,在頁面上面給大家展示,點擊查看未經處理資料這麼一個連結,點了以後再過來請求一遍,這是一個非常簡單的架構。5所示。


圖5

Searcher並行存取模型。 因為讀 四級索引的時候,讀 Docid 的過程一模一樣,所以我在這裡用讀 Docid 舉例子,比如我拿到 DocidList 的資料,我會給每一個 Docid 分配一個 Goroutine ,拼接出來 doc path ,讀取原始日誌,然後做過濾,最後返回給前端。6所示。


圖6

怎樣應用

第一個瓶頸。我們團隊的基礎組件全是 c++,我們團隊核心業務,以及線上引擎、核心引擎都是c++ 來做的。我們用到 gdb 進行調試,進程過多,用 c++ 組件一開始想偷懶,然後編輯進C,再放到 Go 裡面去。每一個讀取 Docid 中,每一個檔案都會去讀,我們的運用程式經常就掛,當時也沒有原因,最後我們才看到執行 CGO 的時候,我們收到一個訊號,就是 signal exit,然後我們進行GDB調試,說是進程太多,因為CGO在執行的時候會建立一個M。

解決方案:用Go重新實現一遍,將組件作為http服務,Go Client調用,做集中式處理。

第二個瓶頸。在系統中,我們大量使用 Goroutine,子寫程 panic 在主寫程不能被處理掉。

解決方案:我們在通道類型裡面為struct,封裝正常資料和error,在主協程取取出資料,統一做處理。

經驗小結。

即使精通很多語言,最好不要混用,要非常謹慎引入其他語言的解決方案。

不要完全相信recover,它不能恢複runtime的一些panic。

看一下我們的Proxy多天並發查詢設計。 7所示。要做 多天查詢有兩種方案。第一種方案把多天查詢加上,這樣使我們核心查詢引擎變得非常臃腫,我們還是那句話,加一個中介層。把多天變成單天,然後在Proxy 拿到所有的單天資料,就形成了多天查詢。


圖7

我們還有另外一個項目,請求Poseidon的資料,我們想到兩種解決方案,第一種解決方案,你在自己第三方系統裡面做緩衝,要不我們做緩衝,我們是這樣取捨。如果第三方系統裡面做緩衝,所有的查詢,緩衝只能在第三方系統裡面用。如果在我們這裡緩衝,他們發了請求到我們這來,其他所有第三方裡面都有可能能用上。我們是這樣做的,首先請求 Searcher 拿到當天的資料,比如查一個月的資料,請求 Searcher 單天的資料,如果每一個Goroutine 去查一天,每一個 Goroutine 拿到 Searcher 單天資料之後,把它解出來,看一下是不是錯誤資料。如果是錯誤資料的話,直接給用戶端把這條資料返回錯誤,並不是給用戶端整個錯誤,因為只是這一天某一條資料有錯誤。而不至於我們在查詢 30 天資料的時候,裡面只要某一天某一條資料有錯誤,就直接返回給使用者,我這個系統不可用。如果不是錯誤資料,會根據請求參數,請求參數有很多。除了這些之外,還有查詢的時間,根據這個來做一個Cace Key,然後打回給前端。

我們遇到一個問題,每一個使用者會把整個索引流程都跑一遍,也就是說使用者會給我們即時測試。在同一個時間之內,同一份資料在緩衝時間之內不會走完整個 readhdfs 流程。build index 程式化,我們會有監控,如果程式化我們會知道,程式掛了會警示感知,但是資料錯誤卻是未知,我們現在還沒有做到這種監控。但是這個資料錯誤是未知的,我們修複索引就會花費大量時間,去重新寫日誌,跑 Docid,還要解決漏洞。

我們的解決方案,第一個減少緩衝時間,在可容忍錯誤資料時間之內,使用者查詢能及時發現問題,恢複一天兩天資料還可以,不至於緩衝 30 天或者一、兩個月,到最後錯誤資料會越來越多。第二個解決方案,參考 NSQ,利用 for+select 的不確定性來分餾,隨機流量到 chanel 和 hdfs 做熱測試。缺點,就是開發成本相對第一種方案來說有點高。這塊要注意,開發成本並不是非常高,因為 select 而只能從 chanel 拿資料。

第二個經驗小結。 不要選擇非常高大上的一些技術,或者說一些我們所說的黑科技,簡單、有效、夠用能解決問題完全可以。利用 Goroutine 設計並發程式很方便,但是並發運行模型一定要 hold 住。我們之前Gopher 群裡面發過一個部落格,裡面發了很多動態圖,一些 Go 的 Goroutine 和 channel 如何並發,動態圖畫的非常炫。我們在寫自己業務的時候,我們看了 Goroutine 以及 Goroutine 和 channel 怎麼聯動,我們自己有概念。我要表達觀點的時候,我一時也找不到非常恰當的名詞來描述,我不知道這個名詞之前有沒有,或者有沒有其他的意義。

Proxy多天非同步下載。 8所示。前端發起請求,要選擇下載多少天,下載多少資料,服務端接受到請求之後,馬上給用戶端返回,我已經收到了,把這個訊息寫到channel。剛開始我們已經說過在readHDFS是是用JAVA寫的,Goroutine太多,底層掛掉。兩個Searcher到HDFS的時候,一個分詞對應上百個Docid,可能對應著上百個檔案,因為每一個Docid不一定在一個檔案裡面。在Searcher裡面的時候,看起來進來一個請求,實際上往後會越來越大,到最後可能就是指數級的增長,像我們滾雪球一樣。


圖8

首先JAVA做了簡單的串連池,然後有熔斷機制,如果超出一定的串連數,直接返回error。像我們很早之前的時候,保險絲,家裡面的電率大的時候,保險絲是用鉛絲做的,鉛絲會熔化掉。

再說一下GC的變化。 首先我說一下GC在我們整個系統中,從來都不是瓶頸。在這裡說的幾點,是我們升級之後簡單做的測試,在這裡和大家交流一下。如果有其他做測試比我們更細的同學,可以交流一下。

Go 1.7。 我們之前用的 1.5,升級到 1.7 之後,我們的 GC 下降到了三分之一。

nginx 代理問題,之前我做分享的時候,有同學問我在 Go 前端要不要加nginx代理。我之前做的系統面向海量使用者,我們只把 GoServer 打包成二進位的可執行包,請求打到 lvs 的80 連接埠然後再轉寄到 GoServer 8080,非常簡單。在這個項目我們用了 nginx,我們有用它的理由。

存取控制和負載平衡。 負載平衡我們可以用 LVS 做,我們這個項目的情境,使用的人非常少。第一我們是一個內部項目,許可權問題,我們所在前端連接埠只能讓開放的一些機器來訪問,除了我們自己的前端器會訪問以外,其實還有其他的一些團隊,會過來直接寫指令碼請求我們的資料。我們nginx裡面直接用了這兩個,這樣我不需要在Go裡面做,前面就可以直接用nginx做了簡單的負載平衡。要不要nginx,完全取決於自己業務的情境。因為在這個情境中,加了nginx也只是給營運稍微增加了負擔,但是ip限制和負載平衡不需要重新開發了,之前沒有用因為它沒有在裡面起到任何作用,而且之前是對外的服務,不需要有任何的限制,任何人都可以過來請求。

開源的改變

我們考慮開源。 在去年11月份的時候,我們開源了系統,系統有66%代碼是用Golang寫的。我們有兩個問題需要解決,第一個問題第三方依賴的問題,我們開源主體方案沒有用到我們自己的內部依賴包,這些第三方的組件,我們應該如何維護它,我當時和很多人交流過,這種方式也比較多,但是他們各有各的優點和缺點,幾乎沒有一個非常完美的方案,能解決到依賴裡面再套依賴,以及多層依賴關係,至少我沒有找到,既然沒有的話,就選擇最福士化,最簡單的方案,用這個方式來解決。

在我們整個服務裡面,我們自己開發了幾個服務,一共有五個。我們當時考慮過,如果讓使用者部署五個服務,即使我們寫好了指令碼,部署起來在每個使用者端作業系統不同,CPU位元不同等等,都會出各種各樣的問題。排查起問題來,不知道排查哪一個服務,對於我們這些開發人員來說,我們排查問題的時候,也會根據日誌一個服務一個服務去找。我們考慮到,我們把所有的服務打成一個ALL in One一個包。在實際交流試用中,我們瞭解到有很多人沒有選擇All in One而選擇這五個服務獨立部署。

我們開源有五個月,有很多人想讓我們把模糊查詢以及過濾開源出來。模糊查詢我們做的非常簡單,我們用了一個資料庫,有並發能力。我們先把我們需要模糊查詢的分詞給分出來,放到資料庫裡面,在資料庫裡面我就可以操作,我們平常用到的模糊查詢關鍵詞,也就是幾十億左右,幾十億的量做一個操作,那簡直太簡單了,查到之後就知道關鍵詞,拿到關鍵詞之後,接下來的方案就是一個用多個關鍵詞查詢多天的情境,用多個關鍵詞和單個關鍵詞是一樣的。多個關鍵詞去查詢和用多天查詢是一樣的,每個關鍵詞分一個Goroutine去查詢,就可以解決問題了。

總結回顧

首先Go的開發體驗比較好,效能比較高,服務很穩定,我們除了線上有一次事故之後,好像就再也沒有過。我們線上是用自己寫的做監控,如果它掛掉就會自動拉起來,當然這是一種比較low的方式,因為它可能沒有掛,但是它的確死掉了。可以滿足大部分的需求情境,GO語言程式開發需要在代碼可讀性和效能之間做平衡取捨,應用程式並行存取模型需要在控制之內。我們有很多人在群裡面問串連池以及對象池,串連池我們不說,因為很多用戶端都會實現串連池這個功能,我們考慮對象池。對象池優點的確很大,因為它可以複用對象減輕壓力,這是最核心的功能。複用對象解決了gc壓力,但還有一個代碼可讀性的問題,引進對象池,對象池和業務沒有關係,你要看對象池怎麼做,代碼可讀性會非常差。還要說的是,對象池這種解決方案,在Go1.2的時候,用起來很爽,但是目前為止1.4到1.7的時候,對象池這種方案已經遠遠用不到了,因為gc已經不是那麼明顯。除非在非常極端的情況下,我們可能會用到這種非常極端的方式解決問題,但是我想大部分的公司都不太會遇到這種問題。我們知道Go在開發安卓,我們現在用的最多就是它和c++以及c的配合然後在用CGO引入到GO,謹慎與其他語言合用,即使對語言都非常熟,你也並不知道他們兩個結合起來說不定引發一個問題,可能是你永遠解決不了的問題。要合理引進第三方解決方案,在營運成本和系統維護成本要做平衡。

原文連結

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.