如何成功構建大規模 Web 搜索引擎架構?

Web 搜索引擎十分複雜,我們的產品是一個分佈式系統,在性能和延遲方面有非常苛刻的要求。除此之外,這個系統的運營也非常昂貴,需要大量人力,當然也需要大量金錢。

這篇文章將探討我們使用的一些技術棧,以及我們做出的一些選擇和決策。

如何成功構建大規模 Web 搜索引擎架構?

作者 | Cliqz
譯者 | 彎月,責編 | 郭芮出品 | CSDN (ID:CSDNnews)

以下爲譯文:

在本文中,我們將系統地介紹我們的私有搜索產品,經過多年的迭代,來滿足外部和內部的用戶。我們結合使用了很多有名的開源技術,以及雲原生技術,這些技術都經受了嚴格的測試。對於哪些未能從開源或商業系統中找到解決方案的領域,我們只能深入研究,並自行從頭編寫系統。這種方式十分適合我們現在的規模。免責聲明:本文描述的只是系統現在的情況。當然最初的系統並非如此。多年來我們採用過多種架構,並不斷思考諸如成本、流量和數據大小等約束。但本文並不是構建搜索引擎的指南,而只是我們目前正在使用的系統,高德納曾說:“過早優化是萬惡之源。”我們完全同意這句話。我們真心建議所有人不要一次性把所有食材都扔進鍋裏。但也不必逐個放,而是每次一小步,逐步增加複雜性。

如何成功構建大規模 Web 搜索引擎架構?

搜索引擎的經驗——下拉菜單和 SERP

Cliqz 的搜索引擎有兩類客戶,他們有不同的需求。搜索提示

如何成功構建大規模 Web 搜索引擎架構?

瀏覽器中的 Cliqz 下拉菜單瀏覽器的地址欄中可以搜索,搜索結果顯示在下拉菜單中。這類搜索要求的結果很少(通常是 3 條),但對於延遲的要求十分苛刻(一般在 150 毫秒以內),否則就會影響用戶體驗。在 SERP 中搜索

如何成功構建大規模 Web 搜索引擎架構?

Cliqz 搜索引擎的結果頁面 beta.cliqz.com 在網頁上進行搜索,顯示人所共知的搜索結果頁面。這裏,搜索的深度是無限的,但與下拉菜單相比,它對於延遲的要求較低(只需在 1000 毫秒以內就可以)。

如何成功構建大規模 Web 搜索引擎架構?

全自動和近乎實時的搜索

考慮一個查詢,如“拜仁慕尼黑”。這個查詢似乎非常普通,但該查詢會使用我們系統中的數個服務。如果考慮這個查詢的意圖,就會發現用戶可能想要:研究拜仁慕尼黑俱樂部(這種情況下顯示維基百科的小窗可能會有用)

  • 想訂票、購物或者註冊成爲正式的粉絲(顯示官方網站)

  • 想了解有關該俱樂部的新聞:

  • 賽前有關比賽的新聞

  • 比賽中的信息,如實時比分、實時更新或解說

  • 賽後分析

  • 季後信息,如俱樂部的內部情況,轉會期間的活動,聘用新教練等

  • 搜索舊的網頁和內容、俱樂部歷史、過去的比賽記錄等。

你也許會注意到,這些意圖遠非“相關網頁”能概括。這些信息不僅從語義上相關,而且還與時間有關。搜索的時間敏感度對於用戶體驗非常重要。

爲提供合理的用戶體驗,這些信息必須由不同的信息源提供,並以近乎實時的方式轉換成可以被搜索的索引。我們要保證所有模型、索引和相關文件都是最新的(例如,加載的圖像必須反映當前的事件,標題和內容也必須隨時根據正在發展的事件而更新)。在大規模的條件下,儘管這一切看似很難,但我們堅持認爲我們應該永遠給用戶推送最新的信息。這個理念貫穿了我們整個系統架構的基礎。Cliqz 的數據處理和服務平臺採用了多層的 Lambda 架構。該架構根據內容索引的即時性分成三層,分別是:近乎實時的索引

  • 完全自動,由 Kafka (生產者、消費者和流處理器)、Cassandra、Granne 和 RocksDB 負責提供

  • Cassandra 將索引信息存儲在多個表中。不同表中的記錄有不同的生存時間(TTL),這樣可以在數據稍後被重新索引時清理存儲空間

  • 該組件還負責根據趨勢或流行程度進行排名,這樣可以協助在不同大小的移動窗口中找出趨勢。這項功能使用了 KafkaStreams 提供的流處理功能

  • 這些技術造就了產品特性,包括搜索結果中的最新內容、最流行新聞等

每週或基於滑動窗口的批次索引

  • 基於過去 60 天的內容

  • 每週重建索引(使用 Jenkins 上的端到端自動流水線中的批處理作業)

  • 根據最新的數據執行機器學習和數據流水線,提高搜索結果的質量

  • 有一個很好的框架,利用一小部分數據測試新的機器學習模型和算法的改變並建立原型,避免在全部數據上進行端到端試驗造成的高昂成本

  • 利用 Luigi 實現基於 Map-Reduce 和 Spark 的批處理工作流管理,並利用 Jenkins Pipeline 進行回顧管理

  • 利用 Keyvi、Cassandra、qpick 和 Granne 提供服務

全批次索引

  • 基於全部數據

  • 每兩個月重建一次索引

  • 用 Luigi 管理的基於 MapReduce 和 Spark 的批處理工作流

  • 用於在大數據集上訓練大規模的機器學習模型。例如,查詢和詞嵌入、近似最近鄰居模型、語言模型等

  • 利用 Keyvi、Cassandra、qpick 和 Granne 提供服務

值得指出的是,近乎實時的索引和每週索引負責了 SERP 上搜索相關內容的一大部分。其他搜索引擎也採用了類似的做法,即更看重某個話題最新的內容,而不是歷史內容。批次索引負責處理與時間無關的查詢、長尾查詢,以及針對罕見內容、歷史內容或語境苛刻的查詢。這三者的組合能爲我們提供足夠多的結果,因此 Cliqz 搜索才做到了今天的樣子。所有系統都能夠應答所有查詢,但是最終結果是所有索引上的結果的混合。

如何成功構建大規模 Web 搜索引擎架構?

部署——歷史上下文

“只有當你明白何時不該使用某個工具,纔算真正掌握了它。”——Kelsey Hightower 從一開始我們就專注於使用雲服務商提供搜索服務,而不是自己搭建基礎設施。在過去的十年內,雲服務已經成了行業的標準,與自己搭建數據中心相比,無論從複雜性還是從資源需求的角度,雲服務都有巨大的優勢,而且使用很方便,創業公司還可以按量付費。對於我們而言,AWS 十分方便,我們不需要管理自己的機器和基礎設施。要是沒有 AWS,我們就得花很多精力纔會有現在的成就。(但是,AWS 雖然很方便,但也很昂貴。這篇文章裏會介紹一些可以降低成本的手段,但我們建議你在大規模情況下使用雲服務時務必要謹慎。)我們通常會避免那些可能會有用的服務,因爲在我們的規模下,成本可能會高到無法接受。爲了便於理解,我舉一個 2014 年的例子,當時我們遇到的一個增長的問題就是如何在 AWS 上可靠地分配資源並部署應用。剛開始的時候,我們嘗試在 AWS 上構建自己的基礎設施和配置管理系統。我們的做法是用 python 實現了一套解決方案,這樣開發者更容易上手。這套解決方案基於 Fabric 項目,並與 Boto 集成,只需要幾行代碼就可以建立新的服務器並配置好應用程序。當時 docker 還剛剛起步,我們採用的是傳統的方法,直接發佈 python 包,或者純文本的 python 文件,這種方式在依賴管理上有很大困難。儘管項目收到了許多關注,在 Cliqz 也被用於管理很多產品中的服務,但以庫爲基礎的基礎設施和配置管理方式總是有一些不足。全局狀態管理、基礎設施變更的中心鎖、無法集中地查看某個項目或開發者使用的雲資源、依賴外部工具來清理孤立資源、功能有限的配置管理、很難查看開發者的資源使用量、使用者的環境泄露等,這些問題帶來了不便,逐漸讓操作變得越來越複雜。因此我們決定尋找一種新的外部管理解決方案,因爲我們沒有足夠的資源自行開發。我們最終決定的方案是採用來自 Hashicorp 的解決方案組合,包括 Consun、Terraform 和 Packer,還有配置管理工具如 Ansible 和 Salt。Terraform 使用優秀的聲明式方式定義基礎設施管理,雲原生領域的許多最新技術都採用了這個概念。因此我們在謹慎地評估之後決定,放棄了自己基於 fabric 的部署庫,轉而採用 Terraform。除了技術上的優劣之外,我們還必須考慮人的因素。一些團隊接受改變比較緩慢,有可能是因爲缺乏資源,有可能是因爲轉變的代價在各個團隊之間並不一致。我們花了整整一年的時間才完成遷移。Terraform 的一些開箱即用的特性是我們以前沒有的,如:

  • 基礎設施的中心狀態管理

  • 詳盡的計劃、補丁和應用支持

  • 很容易關閉資源,最小化孤立資源

  • 支持多種雲

同時,我們在使用 Terraform 的過程中也面臨着一些挑戰:

  • 複雜的 DSL,一般不遵循 DRY 原則

  • 很難融合到其他工具中

  • 模板支持有限,有時非常複雜

  • 服務健康狀態方面沒有反饋

  • 無法很容易地回滾

  • 缺乏某些關鍵功能,需要依靠第三方的實現,如 terragrunt

Terraform 當然在 Cliqz 有用武之地,時至今日,我們依然用它來部署大多數 Kubernetes 基礎設施。

**
**

如何成功構建大規模 Web 搜索引擎架構?

搜索系統的複雜性

如何成功構建大規模 Web 搜索引擎架構?

搜索系統概覽這些年來,我們由數十臺服務器組成的分佈式架構遷移到了整體式架構,最後又遷移到了微服務架構。我們相信,每個服務在當時的資源條件下都是最方便的。例如,採用整體式架構是因爲絕大多數延遲都是由於集羣中的服務器之間的網絡 IO 導致的。當時 AWS 發佈了 X1 實例,它擁有 2TB 的內存。改變架構可以有效地降低延遲,當然成本也會攀升。而下一個架構方面的迭代重點放在了成本上。我們在不影響其他因素的前提下一點點改變每個變量。儘管這個方法看上去並不那麼漂亮,但非常適合我們。“微服務架構風格將應用程序分解成一組小服務,每個服務在自己的進程上運行,通過輕量化的機制(通常是 HTTP 資源 API)與其他進程進行通信。” ——Martin Fowler 理論上,Martin Fowler 給出的微服務的定義是正確的,但過於抽象。對於我們來說,這個定義並沒有說明應當怎樣構建和分割微服務,而這纔是重點。採用微服務給我們帶來了如下好處:

  • 團隊之間更好的模塊化和自動化,以及關注點分離。

  • 水平伸縮和工作負載劃分。

  • 錯誤隔離,更好地支持多語言。

  • 多租戶,更好的安全功能。

  • 更好的運維自動化。

從架構整體以及微服務的結構上來看,每當查詢請求發送到後端時,請求路徑上會觸發多個服務。每個服務都可以看做是微服務,因爲它們都有關注點分離,採用輕量級協議(REST/GRPC),並且可水平伸縮。每個服務都由一系列獨立的微服務組成,可以擁有一個持久層。請求路徑通常包括:

  • Web 應用層防火牆(WAF):應用層防火牆,用於抵禦常見的 Web 漏洞。

  • 負載均衡器:接收請求、負載均衡。

  • Ingress 代理:路由、邊緣可觀測性、發現、策略執行。

  • Eagle:SERP 的服務器端渲染。

  • Fuse:API 網管,結果融合,邊緣緩存,認證和授權。

  • 建議:查詢建議。

  • 排名:用近乎實時的索引和預編譯的批次索引提供搜索結果(Lambda 架構)。

  • 富結果:添加更豐富的信息,如天氣、實時比分的小窗體,以及來自第三方信息源的信息。

  • 知識圖譜和瞬時解答:查找與查詢有關的信息。

  • 地點:基於地理位置的內容推薦。

  • 新聞:來自知名新聞源的實時內容。

  • 跟蹤器:由 WhoTracks.me 提供的特定於某個領域的跟蹤信息。

  • 圖像:與用戶查詢有關的圖像結果。

所有服務都編排至公用的 API 網關,該 API 網關負責處理搜索結果的大小,還提供了其他功能,如針對訪問量激增的保護、根據請求量 /CPU/ 內存 / 自定義基準自動進行伸縮、邊緣緩存、流量模仿和分割、A/B 測試、藍綠部署、金絲雀發佈等。

如何成功構建大規模 Web 搜索引擎架構?

Docker 容器和容器編排系統

到目前爲止,我們介紹了產品的部分需求和一些細節。我們介紹了怎樣進行部署,以及各種方案的缺點。有了這些經驗教訓,我們最終選擇了 Docker 作爲所有服務的基本組成部分。我們開始使用 Docker 容器來分發代碼,而不再使用虛擬機+代碼+依賴的形式。有了 Docker,代碼和依賴就可以作爲 Docker 鏡像發送到容器倉庫(ECR)。但隨着服務繼續增長,我們需要管理這些容器,特別是在需要在生產環境中進行伸縮的情況。難點包括 (1) 浪費很多計算資源 (2) 基礎設施的複雜性 (3) 配置管理。人員和計算力一直是稀缺資源,這是許多資源有限的創業公司都會面臨的困境。當然,爲了提高效率,我們必須重點解決那些存在但現有工具不能解決的問題。但是,我們並不希望重新發明輪子(除非這樣做能有效地改變狀況)。我們十分願意使用開源軟件,開源解決了許多關鍵的業務問題。Kubernetes 1.0 版公佈之後我們立即着手嘗試,到 1.4 版的時候,Kubernetes 已經比較穩定,其工具也比較成熟,我們就開始在 Kubernetes 上運行生產環境的負載。同時,我們還在大型項目(如 fetcher)上評測了其他編排系統,如 Apache Mesos 和 Docker Swarm。最後我們決定用 Kubernetes 來編排一切,因爲有足夠的證據表明,Kubernetes 採用了非常誘人的措施來解決編排和配置管理的問題,而其他方案並沒有做到這一點。再說 Kubernetes 還有強力的社區支持。

如何成功構建大規模 Web 搜索引擎架構?

Kubernetes - Cliqz 的技術棧

如何成功構建大規模 Web 搜索引擎架構?

Cliqz 使用的開源軟件“開源軟件贏得了世界!”Cliqz 依賴於許多開源軟件項目,特別是依賴於雲原生基金會(Cloud Native Computing Foundation)旗下的諸多項目,來提供整體的雲原生體驗。我們通過提供代碼、博客文章以及 Slack 等其他渠道盡可能回饋開源社區 。下面來介紹一下我們的技術棧中使用的關鍵開源項目:KOPS——Kubernetes 編排在容器編排方面,我們利用 KOPS 和一些自己開發的工具來自行管理橫跨多個區域的 Kubernetes 集羣,管理集羣生命週期和插件等。感謝 Justin Santa Barbara 和 kops 的維護者們做出的優異工作,使得 k8s 的控制平面和工作節點可以非常好地結合在一起。目前我們沒有依賴任何提供商管理的服務,因爲 KOPS 非常靈活,而 AWS 提供的 k8s 控制平面服務 EKS 還非常不成熟。使用 KOPS 以及自行管理集羣意味着我們可以按照自己的節奏行事,可以深入研究問題,可以啓用那些應用程序真正需要、卻僅在某個 Kubernetes 版本中才存在的功能。如果我們依賴於雲服務,那麼達到現狀需要花費更長的時間。Weave Net——網絡覆蓋值得一提的是,Kubernetes 可以對系統的各個部分進行抽象。不僅包括計算和存儲,還包括網絡。我們的集羣可能會增長到幾百個節點,因此我們採用了覆蓋網絡(overlay network)構成了骨幹網絡,爲橫跨多個節點甚至多個區的 Pod 提供基本的網絡功能並實行網絡策略。我們採用了 Weave Net 作爲覆蓋網絡,因爲它很容易管理。隨着規模增長,我們可能會切換到 AWS VPC CNI 和 Calico,因爲它們更成熟,能提供更少的網絡跳數,以及更一致的路由和流量。到現在爲止,Weave Net 在我們的延遲和吞吐量環境下表現良好,所以還沒有理由切換。Helm / Helmfile——包管理和發佈我們最初依賴於 helm (v2)進行 Kubernetes manifest 的包管理和發佈。儘管它有許多痛點,但我們認爲它依然是個優秀的發佈管理和模板工具。我們採用了單一代碼倉庫來存儲所有服務的 heml 圖,並使用 chartmuseum 項目進行打包和分發。依賴於環境的值會保存到另一個代碼倉庫中,以實現關注點分離。這些都通過 Helmfile 提供的的 gitOps 模式來實現,它提供了聲明式的方式,以實現多個 helm 圖的發佈管理,並關聯重要的插件,如 diff、tillerless,並使用 SOPS 進行祕密管理。對該代碼倉庫做出的改變,會通過 Jenkins 的 CI/CD 流水線進行驗證並部署。Tilk / K9s——無壓力的本地 Kubernetes 開發我們面臨的問題之一在於:怎樣才能在開發者的開發週期中引入 Kubernetes。一些需求非常明顯,那就是怎樣才能構建代碼並同步到容器中,怎樣才能做得又快又好。最初我們使用了簡單的自制解決方案,利用文件系統事件來監視源代碼變更,然後 rsync 到容器中。我們還嘗試了許多項目,如 Google 的 Skaffold 和微軟的 Draft,試圖解決同樣的問題。最適合我們的是 Windmill Engineering 的 Tilt (感謝 Daniel Bentley),該產品非常優秀,其工作流由 Tiltfile 驅動,該文件由 starlark 語言編寫。它可以監視文件編輯,可以自動應用修改,實時自動構建容器鏡像,利用集羣構建、跳過容器倉庫等手段來加速構建,還有漂亮的 UI,可以在一個面板中查看所有服務的信息。如果你希望深入研究,我們把這些 k8s 的知識開源成一個名爲 K9s 的命令行工具(https://github.com/derailed/k9s),它能以交互的方式執行 k9s 命令,並簡化開發者的工作流程。今天,所有運行於 k8s 上的工作負載都在集羣中進行開發,並提供統一、快速的體驗,每個新加入的人只需要幾個命令就可以開始工作,這一切都要歸功於 helm / tilt / k9s。Prometheus,AlertManager,Jaeger,Grafana 和 Loki——可觀測性我們依賴 Prometheus 的監視方案,使用時間序列數據庫(tsdb)來收集、統計和轉發從各個服務收集到的指標數據。Prometheus 提供了非常好的查詢語言 PromQl 和報警服務 Alert Manager。Jaeger 構成了跟蹤統計方案的骨幹部分。最近我們將日誌後臺從 Graylog 遷移到了 Loki,以提供與 Prometheus 相似的體驗。這一切都是爲了提供單一的平面,滿足所有可觀測性的需求,我們打算通過圖表解決方案 Grafana 來發布這些數據。爲了編排這些服務,我們利用 Prometheus Operator 項目,管理多租戶 Prometheus 部署的生命週期。在任意時刻,我們都會接收幾十萬條時間序列數據,從中瞭解基礎設施的運行情況,出現問題時判斷從哪個服務開始解決問題。以後我們打算集成 Thanos 或 Cortex 項目來解決 Prometheus 的可伸縮性問題,並提供全局的查詢視圖、更高的可用性,以及歷史分析的數據備份功能。Luigi 和 Jenkins——自動化數據流水線我們使用 Luigi 和 Jenkins 來編排並自動化數據流水線。批處理作業提交到 EMR,Luigi 負責構建非常複雜的批處理工作流。然後使用 Jenkins 來觸發一系列 ETL 操作,這樣我們就能控制每個任務的自動化和資源的使用狀況。我們將批處理作業的代碼打包並添加版本號後,放到帶有版本號的 docker 容器中,以保證開發和生產環境中的體驗一致。插件項目我們還使用了許多社區開發的其他項目,這些作爲插件發佈的項目是集羣生命週期的一部分,它們爲生產環境和開發環境中部署的服務提供額外的價值。下面簡單介紹一下:

  • Argo 工作流和持續部署:我們評測了該項目,作爲 Jenkins 的後備,用於批量處理任務和持續部署。

  • AWS IAM 認證器:k8s 中的用戶認證管理。

  • ChartMuseum:提供遠程 helm 圖。

  • Cluster Autoscaler:管理集羣中的自動伸縮。

  • Vertical Pod Autoscaler:按需要或根據自定義指標來管理 Pod 的垂直伸縮。

  • Consul:許多項目的狀態存儲。

  • External DNS:將 DNS 記錄映射到 Route53 來實現外部和內部的訪問。

  • Kube Downscaler:當不再需要時對部署和狀態集進行向下伸縮。

  • Kube2IAM:透明代理,限制 AWS metadata 的訪問,爲 Pod 提供角色管理。

  • Loki / Promtail:日誌發送和統計。

  • Metrics Server:指標統計,與其他消費者的接口。

  • Nginx Ingress:內部和外部服務的 ingress 控制器。爲了擴展 API 網關的功能,我們在不斷評測其他 ingress 控制器,包括 Gloo、Istio ingress gateway 和 Kong。

  • Prometheus Operator:Prometheus 操作器棧,能夠準備 Grafana、Prometheus、AlertManager 和 Jaeger 部署。

  • RBAC Manager:可以很容易地爲 k8s 資源提供基於角色的訪問控制。

  • Spot Termination Handler:通過提前警戒並清空節點的方式來優雅地處理單點中斷。

  • Istio:我們一直在評測 Istio 的網格、可觀察性、流量路由等功能。許多功能我們都已自己編寫了解決方案,但長時間以來這些方案開始暴露出了限制,我們希望該項目能夠滿足我們的要求。

k8s 的經驗加上豐富的社區支持,我們不僅能夠發佈核心的無狀態服務來提供搜索功能,還能在多個區域和集羣中運行大型有狀態的負載,如 Cassandra、 Kafka、Memcached 和 RocksDB 等,以提供高可用性和副本。我們還開發了其他工具,在 Kubernetes 中管理並安全地執行這些負載。

如何成功構建大規模 Web 搜索引擎架構?

使用 Tilt 進行本地開發——端到端的用例

上述介紹了許多我們使用的工具。這裏我想結合一個具體的例子來介紹怎樣使用這些工具,更重要的是介紹這些工具怎樣影響開發者的日常工作。我們以一名負責開發搜索結果排名的工程師爲例,之前的工作流爲:

  • 使用自定義的 OS 鏡像啓動一個實例,然後利用所有者信息給實例和相關的資源加上標籤。

  • 將代碼 rsync 到實例中,然後安裝應用程序依賴。

  • 學習怎樣設置其他服務,如 API Gateway 和前端,安裝依賴並部署。

  • 通過配置讓這些服務能夠協同工作。

  • 開發排名應用程序。

  • 最後,開發完畢後,要終止該實例。

可見,開發者需要重複進行一系列的操作,團隊中的每個新工程師都要重複這一切,這完全是對開發者生產力的浪費。如果實例丟失,就要重複一遍。而且,生產環境和本地開發環境的工作流還有少許不同,有時會導致不一致。有人認爲在開發排名應用程序時設置其他服務(如前端)是不必要的,但這裏的例子是爲了通用起見,再說設置完整的產品總沒有壞處。此外,隨着團隊不斷增長,需要創建的雲資源越來越多,資源的利用率也越來越低。工程師會讓實例一直運行,因爲他們不想每天重複這一系列操作。如果某個工程師離職,他的實例也沒有加上足夠的標籤,那麼很難判斷是否可以安全地關閉該實例並刪除雲資源。

理想情況是爲工程師提供用於設置本地開發環境的基礎模板,該模板可以設置好完整的 SERP,以及其他排名應用程序需要的服務。這個模板是通用的,它會給用戶創建的其他資源加上唯一的標籤,幫助他們控制應用程序的生命週期。因爲 k8s 已經將創建實例和管理實例的需求抽象化(我們通過 KOPS 來集中管理),所以我們利用模板來設置默認值(在非工作時間自動向下伸縮),從而極大地降低了成本。現在,用戶只需關心他自己編寫的 diamante,我們的工具(由 Docker、Helm 和 Tilt 組成)會在幕後神奇地完成這一系列工作流。下面是 Tiltfile 的例子,描述了設置最小版本的 SERP 所需的服務和其他依賴的服務。要在開發模式下啓動這些服務,用戶只需要執行 tilt up:

                                                                                                                      *
    # -*- mode: Python -*-  
    """This Tiltfile manages 1 primary service which depends on a number of other micro services.Also, it makes it easier to launch some extra ancilliary services which may beuseful during development.Here's a quick rundown of these services and their properties:* ranking: Handles ranking* api-gateway: API Gateway for frontend* frontend: Server Side Rendering for SERP  
    """  
    ##################### Project defaults #####################  
    project = "some-project"namespace = "some-namespace"chart_name = "some-project-chart"deploy_path = "../../deploy"charts_path = "{}/charts".format(deploy_path)chart_path = "{}/{}".format(charts_path, chart_name)values_path = "{}/some-project/services/development.yaml".format(deploy_path)secrets_path = "{}/some-project/services/secrets.yaml".format(deploy_path)secrets_dec_path = "{}/some-project/services/secrets.yaml.dec".format(deploy_path)chart_version = "X.X.X"  
    # Load tiltfile libraryload("../../libs/tilt/Tiltfile", "validate_environment")env = validate_environment(project, namespace)  
    # Docker repository path for componentsserving_image = env["docker_registry"] + "/some-repo/services/some-project/serving"  
    ##################################### Build services and deploy to k8s #####################################  
    # Watch development values file for helm chart to re-execute Tiltfile in case of changeswatch_file(values_path)  
    # Build docker images# Uncomment the live_update part if you wish to use the live_update function# i.e., no container restarts while developing. Ex: Using Python debuggingdocker_build(serving_image, "serving", dockerfile="./serving/Dockerfile", build_args={"PIP_INDEX_URL": env["pip_index_url"], "AWS_REGION": env["region"]} #, live_update=[sync('serving/hide/', '/some-project/'),])  
    # Update local helm repos listlocal("helm repo update")  
    # Remove old download chart in case of changeslocal("rm -rf {}".format(chart_path))  
    # Decrypt secretslocal("export HELM_TILLER_SILENT=true && helm tiller run {} -- helm secrets dec {}".format(namespace, secrets_path))  
    # Convert helm chart to standard k8s manifeststemplate_script = "helm fetch {}/{} --version {} --untar --untardir {} && helm template {} --namespace {} --name {} -f {} -f {}".format(env["chart_repo"], chart_name, chart_version, charts_path, chart_path, namespace, env["release_name"], values_path, secrets_dec_path)yaml_blob = local(template_script)  
    # Clean secrets filelocal("rm {}".format(secrets_dec_path))  
    # Deploy k8s manifestsk8s_yaml(yaml_blob)  
    dev_config = read_yaml(values_path)  
    # Port-forward specific resourcesk8s_resource('{}-{}'.format(env["release_name"], 'ranking'), port_forwards=['XXXX:XXXX'], new_name="short-name-1")k8s_resource('{}-{}'.format(env["release_name"], 'some-project-2'), new_name="short-name-2")  
    if dev_config.get('api-gateway', {}).get('enabled', False):  k8s_resource('{}-{}'.format(env["release_name"], 'some-project-3'), port_forwards=['XXXX:XXXX'], new_name="short-name-3")  
    if dev_config.get('frontend', {}).get('enabled', False):  k8s_resource('{}-{}'.format(env["release_name"], 'some-project-4-1'), port_forwards=['XXXX:XXXX'], new_name="short-name-4-1")  k8s_resource('{}-{}'.format(env["release_name"], 'some-project-4-2'), new_name="short-name-4-2")  

說明:

  • Helm 圖主要用於應用程序打包,以及管理每個發佈的生命週期。我們使用 helm 的模板,並使用自定義 yaml 爲模板提供值。這樣我們就可以對每個發佈進行深入的配置。我們可以配置爲容器分配的資源,很容易地配置每個容器需要連接的服務,可以使用的端口等。

  • 使用 Tilt 加上 helm 圖來設置本地的 k8s 開發環境,並將本地代碼映射到 helm 圖中定義的服務上。利用它提供的功能,我們可以持續地構建 docker 容器並將應用程序部署到 k8s 上,或者進行本地更新(將所有本地修改 rsync 到正在運行的容器上)。開發者也可以利用端口轉發將應用程序映射到本地實例上,以便在開發時訪問服務的端點。我們使用 k8s manifest,從 helm 圖中提取出渲染後的模板,利用它進行部署。這是因爲我們的圖的需求過於複雜,無法完全依靠 Tilt 提供的 helm 的功能。

  • 如果應用程序端點需要與其他團隊成員共享,那麼 helm 圖就可以提供統一的機制來創建內部 ingress 端點。

  • 我們的圖通過公有的 helm 圖倉庫來公開,因此無論是生產環境還是開發環境,我們使用的都是同一套代碼(帶有版本號的 docker 鏡像),同一個圖模板,但模板中的值不一樣,以適應不同的需求(如部署名稱、端點名稱、資源、副本等)。

  • 整套實踐在每個端點和每個項目中都保持一致,這樣新加入團隊的人就非常容易上手,雲資源的管理也非常容易。

“只要技術足夠先進,就和魔法沒什麼區別。”——阿瑟·克拉克

但這個魔法有一個問題。它通過更有效的資源共享,提高生產力、增加可靠度並降低成本 。但是,當某個東西出問題時,人們很難發現問題在哪裏,找出問題的根源變得十分困難,而且這種錯誤特別容易在在人們不方便解決的時候出現。所以,儘管爲這些努力感到驕傲,但我們依然保持謙遜的姿態。

如何成功構建大規模 Web 搜索引擎架構?

優化成本

廉價的基礎設施和互聯網規模的搜索引擎不可能兼得。話雖如此,想要省錢總會有辦法。我來介紹一下我們是怎樣利用基於 k8s 的架構來優化成本的。1. Spot instances我們極度依賴於 AWS spot instances,使用該服務,我們必須在構建系統時考慮可能的失敗。但這樣做是值得的,因爲這些實例要比按需的實例要便宜得多。但要注意不要像我們一樣搬起石頭砸自己的腳。我們早就習慣了 spot instances,因此有時候會高估自己的實力,導致本不應該發生的失敗。而且,不要榨乾高性能服務器的所有性能,否則你就會陷入與其他公司的競價之爭。最後,永遠不要在大型的 NLP/ML 會議之前使用 spot GPU instances。使用 Spot 的混合實例池:我們不僅使用 spot instances 來完成一次性的作業,也利用它來運行服務的工作負載。我們想出了一個絕佳的策略。我們利用多種實例類型(但配置都類似),爲 Kubernetes 資源創建了一個節點池,該節點池分佈在多個可用性區域中。與 Spot Termination handler 配合使用,我們就可以將無狀態的工作負載移動到新建的或空閒的 spot 節點上,避免可能出現的長時間宕機。2. 共享 CPU 內存由於我們完全依靠 Kubernetes,因此在討論工作負載時都是在討論 Kubernetes 需要多少 CPU、多少內存,以及每個服務需要多少個副本。因此,如果 Request 和 Limits 相等,性能就能得到保證。但是,如果 Request 低但 Limit 高(這種情況在零星的工作負載上有用),我們可以多準備一些資源,並將某個實例的資源使用最大化(減少實例上的閒置資源量)。3. 集羣的自動擴展器,Pod 的垂直和水平 Autoscaler我們用集羣自動擴展器來自動化 Pod 的創建和縮小,只有在需求上升時才創建實例。這樣在沒有工作負載時僅啓動最少的實例,也不需要人工干預。4. 開發環境中的部署的 downscaler對於開發設置中的所有服務,我們使用部署的 down-scaler 在特定時間將 pod 的副本數收縮爲 0. 在 Kubernetes 的 manifest 中添加一條註釋,就可以指定啓動計劃:

    annotations:    downscaler/uptime: Mon-Fri 08:00-19:30 Europe/Berlin

也就是說,在非工作時間,部署的大小會收縮爲 0,副本數也會由集羣的自動擴展器進行收縮,因爲實例上沒有活躍的工作負載。5. 成本評估和實例推薦——長期的成本縮減在生產環境中,一旦我們確定了資源使用量,就可以選擇那些負載會很高的實例。這些實例不再採用按需模式,而是採用預留實例(reserved instance)的定價模型,這種模型需要預先支付一年的費用。但是,其成本要比按需啓動的實例要低得多。在 Kubernetes 中,有一些解決方案如 kubecost,可以監視長期的使用成本,然後據此來推薦額外的節約陳本的方法。它還提供了指定工作負載的價格估算功能,這樣就可以算出部署一個系統的總體成本。通過同一個界面,使用者還可以知道哪些資源可能不再被使用,如 ebs 卷等。所有這些措施都可以爲我們每年節省大約幾十到幾千歐元。對於擁有高額基礎設施賬單的大公司來說,如果這些措施得當,就能輕易地每年節省幾百萬。

如何成功構建大規模 Web 搜索引擎架構?

機器學習系統

如何成功構建大規模 Web 搜索引擎架構?

機器學習系統中的隱藏技術債務——Sculley 等人很有意思的是,我們的 Kubernetes 之旅以一種誰也沒想到的方式開始。我們想要搭建一個基礎設施,從而可以用 Tensorflow 運行分佈式深度學習。當時這個想法還很新穎。儘管 Tensorflow 的分佈式訓練已經推出了一段時間,但除了爲數不多的幾個財大氣粗的公司之外,很少有人知道怎樣大規模地從頭到尾運行分佈式訓練。當時也沒有任何雲解決方案能解決這個問題。我們一開始採用了 Terraform 來架設了一個分佈式架構,但很快就意識到這個方案在伸縮性方面有侷限性。同時,我們找到一些社區貢獻的代碼,利用 jinja 模板引擎來生成 Kubernetes manifests,再創建深度學習訓練應用程序的分佈式部署(包括參數服務器和工作模式)。這是我們與 Kubernetes 的第一次接觸。此外,我們還構建了自己的近乎實時的搜索引擎,同時試驗按照新穎程度的排名。就在那時 Kubernetes 給我們帶來了曙光,所以我們決定採用 Kubernetes。作爲機器學習系統之旅的一部分(就像上述所有基礎設施一樣),我們的目標就是向整個公司開放該系統,讓開發者可以很容易地在 Kubernetes 上部署應用程序。我們希望開發者能把精力花費在解決問題上,而不是解決服務帶來的基礎設施問題上。但是,儘管每個人都利用機器學習解決了問題,但我們迅速意識到,維護機器學習系統的確是個非常痛苦的事情。它遠遠不止編寫機器學習代碼或者訓練模型這麼簡單。即使是我們這種規模的公司,也需要解決一些問題。在“Hidden Technical Debt in Machine Learning System”這篇論文中有詳細的描述。任何希望在生產環境中依靠並運行具有一定規模的機器學習系統的人都應該仔細閱讀這篇論文。我們討論了幾種不同的解決方案,例如:

  • MLT

  • AWS SageMaker

  • Kubeflow

  • MLFlow

在所有這些服務中,我們發現 Kubeflow 功能最全、性價比最高,且可以定製。

前一段時間,我們還在 Kubeflow 的官方博客上寫了一些原因。kubeflow 除了能爲我們提供自定義資源,如 TfJob 和 PytorchJob 來運行訓練代碼,它的一大優勢就是自帶 notebook 支持。Cliqz 的 Kubeflow 用例Kubeflow 的許多特性都在我們的近實時排名中得到了應用。工程師可以在集羣中打開一個 notebook,然後直接進入數據基礎設施(批次和實時流)。分享 notebook,讓多人分別處理代碼的不同部分非常容易。工程師們可以很容易地進行各種實驗,因爲他們不需要設置任何 notebook 服務器,也不需要任何訪問數據基礎設施的權限,更不需要深入到部署的細節,只需要使用一個簡單的 Web 界面就可以選擇 notebook 所需的資源(CPU、內存甚至 GPU),分配一個 EBS 卷,然後啓動一個 notebook 服務器。有意思的是,一些實驗是在 0.5 個 CPU 和 1GB 內存上進行的。通常這樣規模的資源在我們的集羣中隨時存在,生成這種 notebook 非常容易,甚至都不需要新建實例。如果不這樣做,那麼來自不同團隊的兩名工程師想要一起工作時,他們很可能會啓動各自的實例,這就會導致成本增加,資源的利用率也不高。此外還可以提交作業,這些作業可以用來在 notebook 中訓練、驗證模型並用模型提供服務。這方面的一個有意思的項目叫做 Fairing。Kubeflow 本身是個非常完善的項目,我們僅僅接觸到了冰山一角。最近我們還開始瞭解其他項目,如 Katib (機器學習模型的超參數調節)、KFServing (在 Kubernetes 上實現機器學習模型的無服務器推斷)和 TFX (創建並管理生產環境下的 ML 流水線)。我們已經利用這些項目創建了一些原型,希望能儘快將其應用到生產環境中。由於有這許多好處,我們衷心地感謝 Kubeflow 背後的團隊打造的這個優秀的項目。隨着我們的增長,隨着我們越來越依賴於機器學習,我們希望圍繞機器學習的處理可以流水線化,可以擁有更高的可重複性。因此,像模型跟蹤、模型管理、數據版本管理變得極其重要。爲了能在這種規模下穩定地運行模型,定期進行更新和評估,我們需要一個數據管理的解決方案,才能在生產環境中運行模型,從而實現模型和索引的自動熱替換。爲了解決這個問題,我們自己搭建了一個解決方案“Hydra”,它能爲下游的服務提供數據集的訂閱服務。它海能在 Kubernetes 集羣中爲服務提供卷管理。

如何成功構建大規模 Web 搜索引擎架構?

結束語

“在取得成功後,下一個目標就是幫助別人成功。”——Kelsey HightowerCliqz 的架構很困難,同時也很有趣。我們相信我們還有很長的路要走。隨着開發的進行,我們有多種方案可以選擇。儘管 Cliqz 已有 120 多名員工,但代碼實際上是由數千名開源開發者編寫併發布的,他們儘可能寫出高質量的代碼,並盡一切努力保證了安全性。沒有他們,我們不可能有今天的成就。我們衷心感謝開源社區提供的代碼,以及在我們遇到問題時幫我們解決問題。通過這篇文章,我們希望分享我們曾經的迷茫、獲得的經驗和解決方案,期待能對遇到類似問題的人有所幫助。懷着開放的心態,我們也想分享我們的資源(https://github.com/cliqz-oss/)來回饋開源社區。原文:https://www.0x65.dev/blog/2019-12-14/the-architecture-of-a-large-scale-web-search-engine-circa-2019.html 本文爲 CSDN 翻譯文章,轉載請註明出處。

【END】

如何成功構建大規模 Web 搜索引擎架構?

更多精彩推薦

☞百年 IBM 終於 All In 人工智能和混合雲!

☞微軟、蘋果、谷歌、三星……這些區塊鏈中的科技巨頭原來已經做了這麼多事!

☞斬獲 GitHub 2000+ Star,阿里雲開源的 Alink 機器學習平臺如何跑贏雙 11 數據“博弈”?| AI 技術生態論

☞微軟爲一人收購一公司?破解索尼程序、寫黑客小說,看他彪悍的程序人生!

☞機器學習項目模板:ML 項目的 6 個基本步驟

☞IBM、微軟、蘋果、谷歌、三星……這些區塊鏈中的科技巨頭原來已經做了這麼多事!

☞資深程序員總結:分析 Linux 進程的 6 個方法,我全都告訴你

今日福利:評論區留言入選,可獲得價值 299 元的「2020 AI 開發者萬人大會」在線直播門票一張。 快來動動手指,寫下你想說的話吧。如何成功構建大規模 Web 搜索引擎架構?點擊閱讀原文,精彩繼續!
如何成功構建大規模 Web 搜索引擎架構?你點的每個“在看”,我都認真當成了喜歡

來源鏈接:mp.weixin.qq.com