如何成功构建大规模 Web 搜索引擎架构?
Web搜索引擎十分復雜,我們的產品是一個分布式系統,在性能和延遲方面有非常苛刻的要求。除此之外,這個系統的運營也非常昂貴,需要大量人力,當然也需要大量金錢。
這篇文章將探討我們使用的一些技術棧,以及我們做出的一些選擇和決策。
作者 |?Cliqz
譯者 |?彎月,責編 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下為譯文:
在本文中,我們將系統地介紹我們的私有搜索產品,經過多年的迭代,來滿足外部和內部的用戶。
我們結合使用了很多有名的開源技術,以及云原生技術,這些技術都經受了嚴格的測試。對于哪些未能從開源或商業系統中找到解決方案的領域,我們只能深入研究,并自行從頭編寫系統。這種方式十分適合我們現在的規模。
免責聲明:本文描述的只是系統現在的情況。當然最初的系統并非如此。多年來我們采用過多種架構,并不斷思考諸如成本、流量和數據大小等約束。但本文并不是構建搜索引擎的指南,而只是我們目前正在使用的系統,高德納曾說:
“過早優化是萬惡之源。”
我們完全同意這句話。我們真心建議所有人不要一次性把所有食材都扔進鍋里。但也不必逐個放,而是每次一小步,逐步增加復雜性。
搜索引擎的經驗——下拉菜單和SERP
Cliqz的搜索引擎有兩類客戶,他們有不同的需求。
搜索提示
瀏覽器中的Cliqz下拉菜單
瀏覽器的地址欄中可以搜索,搜索結果顯示在下拉菜單中。這類搜索要求的結果很少(通常是3條),但對于延遲的要求十分苛刻(一般在150毫秒以內),否則就會影響用戶體驗。
在SERP中搜索
Cliqz搜索引擎的結果頁面 beta.cliqz.com
在網頁上進行搜索,顯示人所共知的搜索結果頁面。這里,搜索的深度是無限的,但與下拉菜單相比,它對于延遲的要求較低(只需在1000毫秒以內就可以)。
全自動和近乎實時的搜索
考慮一個查詢,如“拜仁慕尼黑”。這個查詢似乎非常普通,但該查詢會使用我們系統中的數個服務。如果考慮這個查詢的意圖,就會發現用戶可能想要:
研究拜仁慕尼黑俱樂部(這種情況下顯示維基百科的小窗可能會有用)
想訂票、購物或者注冊成為正式的粉絲(顯示官方網站)
想了解有關該俱樂部的新聞:
賽前有關比賽的新聞
比賽中的信息,如實時比分、實時更新或解說
賽后分析
季后信息,如俱樂部的內部情況,轉會期間的活動,聘用新教練等
搜索舊的網頁和內容、俱樂部歷史、過去的比賽記錄等。
你也許會注意到,這些意圖遠非“相關網頁”能概括。這些信息不僅從語義上相關,而且還與時間有關。搜索的時間敏感度對于用戶體驗非常重要。
為提供合理的用戶體驗,這些信息必須由不同的信息源提供,并以近乎實時的方式轉換成可以被搜索的索引。我們要保證所有模型、索引和相關文件都是最新的(例如,加載的圖像必須反映當前的事件,標題和內容也必須隨時根據正在發展的事件而更新)。在大規模的條件下,盡管這一切看似很難,但我們堅持認為我們應該永遠給用戶推送最新的信息。這個理念貫穿了我們整個系統架構的基礎。
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搜索才做到了今天的樣子。所有系統都能夠應答所有查詢,但是最終結果是所有索引上的結果的混合。
部署——歷史上下文
“只有當你明白何時不該使用某個工具,才算真正掌握了它。”——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基礎設施。
搜索系統的復雜性
搜索系統概覽
這些年來,我們由數十臺服務器組成的分布式架構遷移到了整體式架構,最后又遷移到了微服務架構。
我們相信,每個服務在當時的資源條件下都是最方便的。例如,采用整體式架構是因為絕大多數延遲都是由于集群中的服務器之間的網絡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測試、藍綠部署、金絲雀發布等。
Docker容器和容器編排系統
到目前為止,我們介紹了產品的部分需求和一些細節。我們介紹了怎樣進行部署,以及各種方案的缺點。有了這些經驗教訓,我們最終選擇了Docker作為所有服務的基本組成部分。我們開始使用Docker容器來分發代碼,而不再使用虛擬機+代碼+依賴的形式。有了Docker,代碼和依賴就可以作為Docker鏡像發送到容器倉庫(ECR)。
但隨著服務繼續增長,我們需要管理這些容器,特別是在需要在生產環境中進行伸縮的情況。難點包括(1)浪費很多計算資源 (2)基礎設施的復雜性 (3) 配置管理。
人員和計算力一直是稀缺資源,這是許多資源有限的創業公司都會面臨的困境。當然,為了提高效率,我們必須重點解決那些存在但現有工具不能解決的問題。但是,我們并不希望重新發明輪子(除非這樣做能有效地改變狀況)。我們十分愿意使用開源軟件,開源解決了許多關鍵的業務問題。
Kubernetes 1.0版公布之后我們立即著手嘗試,到1.4版的時候,Kubernetes已經比較穩定,其工具也比較成熟,我們就開始在Kubernetes上運行生產環境的負載。同時,我們還在大型項目(如fetcher)上評測了其他編排系統,如Apache Mesos和Docker Swarm。最后我們決定用Kubernetes來編排一切,因為有足夠的證據表明,Kubernetes采用了非常誘人的措施來解決編排和配置管理的問題,而其他方案并沒有做到這一點。再說Kubernetes還有強力的社區支持。
Kubernetes - Cliqz的技術棧
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中管理并安全地執行這些負載。
使用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 be useful 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 library load("../../libs/tilt/Tiltfile", "validate_environment") env = validate_environment(project, namespace)# Docker repository path for components serving_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 changes watch_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 debugging docker_build(serving_image, "serving", dockerfile="./serving/Dockerfile", build_args={"PIP_INDEX_URL": env["pip_index_url"], "AWS_REGION": env["region"]} #, live_update=[sync('serving/src/', '/some-project/'),] )# Update local helm repos list local("helm repo update")# Remove old download chart in case of changes local("rm -rf {}".format(chart_path))# Decrypt secrets local("export HELM_TILLER_SILENT=true && helm tiller run {} -- helm secrets dec {}".format(namespace, secrets_path))# Convert helm chart to standard k8s manifests template_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 file local("rm {}".format(secrets_dec_path))# Deploy k8s manifests k8s_yaml(yaml_blob)dev_config = read_yaml(values_path)# Port-forward specific resources k8s_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鏡像),同一個圖模板,但模板中的值不一樣,以適應不同的需求(如部署名稱、端點名稱、資源、副本等)。
整套實踐在每個端點和每個項目中都保持一致,這樣新加入團隊的人就非常容易上手,云資源的管理也非常容易。
“只要技術足夠先進,就和魔法沒什么區別。”——阿瑟·克拉克
但這個魔法有一個問題。它通過更有效的資源共享,提高生產力、增加可靠度并降低成本 。但是,當某個東西出問題時,人們很難發現問題在哪里,找出問題的根源變得十分困難,而且這種錯誤特別容易在在人們不方便解決的時候出現。所以,盡管為這些努力感到驕傲,但我們依然保持謙遜的姿態。
優化成本
廉價的基礎設施和互聯網規模的搜索引擎不可能兼得。話雖如此,想要省錢總會有辦法。我來介紹一下我們是怎樣利用基于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卷等。
所有這些措施都可以為我們每年節省大約幾十到幾千歐元。對于擁有高額基礎設施賬單的大公司來說,如果這些措施得當,就能輕易地每年節省幾百萬。
機器學習系統
機器學習系統中的隱藏技術債務——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集群中為服務提供卷管理。
結束語
“在取得成功后,下一個目標就是幫助別人成功。”——Kelsey Hightower
Cliqz的架構很困難,同時也很有趣。我們相信我們還有很長的路要走。隨著開發的進行,我們有多種方案可以選擇。
盡管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 搜索引擎架构?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 集齐最后一块拼图,全栈Serverles
- 下一篇: 华为智能IP网络,加速联接智能化转型