貝殼找房基於Milvus的向量搜索實踐(二)

摘要:此篇爲該系列文章第二部分,第一部分主要講基本概念、背景、選型及服務的整體架構;本部分主要講針對低延時、高吞吐需求,我們對Milvus部署方式的一種定製;第三部分主要講實現數據更新、保證數據一致性,以及保證服務穩定及提高資源利用率做的一些事情。

1.遇到了哪些問題

在項目調研、實施以及最終上線使用過程中,我們遇到了不少的問題,包括:

  • 如何解決在滿足響應時間的條件下,解決橫向擴展的問題。
  • 在引擎本身不穩定的情況下,如何實現數據T+1更新時的一致性。
  • 在引擎本身不穩定且問題暫時無法明確定位/解決的情況下,如何實現服務的高可用。
  • 如何實現資源的動態調整,以提高資源的利用率。

2.低時延、高吞吐的要求

互聯網垂直搜索領域,特別是電商行業,對於特定業務的搜索,熱數據的量級一般是可控的(百萬級、千萬級),一般情況下,對響應時間和整體的吞吐量(QPS)都有比較高的要求。

其中,響應時間是首要條件,其次是吞吐量;如果單機在小流量下能滿足響應時間要求,但是無法滿足吞吐量要求時,集羣部署/橫向擴展能力,就是一個很自然的解決思路了。

3.解決方案

3.1 Mishards -Milvus原生解決方案

圖1 Milvus分佈式方案 - Mishards

我們可以先了解下Milvus是如何解決低時延、高吞吐問題的。如圖1所示,Milvus藉助了一個外圍服務Mishards來代理Milvus引擎,來實現分佈式部署的。處理具體請求的流程大概是這樣:

  1. 請求流量進入Mishards請求隊列。
  2. Mishards從請求隊列中取出請求,藉助自身維護的數據段信息,把請求拆分成子請求(只查詢部分段),並把子請求分發給負責不同段的Milvus讀實例。
  3. Milvus讀實例處理段請求,並返回結果。
  4. Mishards把聚合返回的結果後,最終返回。

另外,需要知道的是,Milvus底層的數據存儲可以分段存儲(不同的數據文件,文件大小可以在配置文件中設定),如果數據量足夠大的情況下,數據最終會存儲在多個文件中;相應地,Milvus支持對指定文件(可以是多個文件)的查詢。

由以上分析可知,在數據量比較大的情況下(比如百億級數據),數據在同一個物理機上無法全部加載到內存中,查詢時勢必會導致大量的數據加載,從而導致單個查詢的響應時間就會讓人無法忍受;Mishards剛好就可以滿足數據量量大時,單個查詢的響應時間提升,使用多個物理資源來分擔單個查詢的開銷。

然而,在數據量相對小時,如前面所說的百萬級、千萬級數據量,在數據的維度比較小時(如500以內),常見的物理機完全可以加載到內存裏邊。在這種情況下,通過實驗發現,分段存儲數據反而會使用整體的響應時間變差,因此,我們下面討論的場景都是數據存儲在一個段內。

數據存儲在一個分段內,當單個查詢(小流量查詢)響應時間可以滿足需求時,我們無法使用Mishards來實現整體吞吐量的增加(因爲數據只有一份,而且只能在一個Milvus讀實例中被處理,即使我們部署了多個讀實例)。

那麼,在數據只需要存儲在一個分段中,而且小流量、響應時間可以滿足需求時,如何實現整體吞吐能力的橫向擴展呢?

3.2 使用envoy+headless service實現擴展

由圖1可以知道,Mishards實現了讀寫分離,以及大數據量下單個請求的負載拆分。但是,在互聯網垂直搜索領域,特別是電商行業,熱數據一般量級並不大,完全可以放在一個分段(文件)中。我們把問題轉換成以下兩個目標:

  • 讀寫分離
  • 讀結點可橫向擴展

對於目標1,其實就是一個請求轉發的問題,milvus採用的grpc通信協議,本質上是http2請求,可以通過請求的路徑區分開,而且業界已經有比較成熟的工具如nginx,envoy等。所以,問題就集中在如何實現讀結點的橫向擴展。

由於部署採用是是docker+k8s環境,所以嘗試採用envoy[2]這個專門爲雲原生應用打造的方案來解決橫向擴展的問題。目標1可以簡單解決,envoy配置片段[3]如下:

       ...  ...
       filter_chains:
          filters:
          - name: envoy.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
              codec_type: auto
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                - name: backend
                  retry_policy:
                    retry_on: unavailable
                  domains:
                  - "*"
                  routes:
                  - match:
                      prefix: "/milvus.grpc.MilvusService/Search"
                    route:
                      cluster: milvus_backend_ro
                      timeout: 1s
                      priority: HIGH
                  - match:
                      prefix: "/"
                    route:
                      cluster: milvus_backend_wo
                      timeout: 3600s
                      priority: HIGH 
              ...  ...

我們可以把實現第二個目標(讀結點可橫向擴展)細化爲兩個步驟:1.實現讀結點集羣部署,並支持增加/減少結點;2.實現請求讀結點的負載均衡。

1.實現讀結點集羣部署

kubernetes下有一個抽象概念service[4],其含義就對應於域名,我們可以通過將service指向一組Pod(kubernetes下另外一個概念,一個Pod對應一個讀結點)[5];我們可以通過kubernetes下的Deployment[6]/Daemonset[7]來管理這組Pod,實現Pod數的增加/減少。

另外,我們需要詳細分析的是kubernetes是如何進行DNS解析的,具體來講就是要分析service是如何解析到所對應Pod的ip:port的。

由[8]可知,kubernets集羣中的每個service,包括DNS服務器,都被分配了一個DNS名,集中的任一Pod可以通過DNS來訪問其它Pod。另外,service還分兩種,Normal和Headless[9],兩種service的的解析方式不同;Normal類型的service會被分配一個DNS的A記錄[10],格式如my-svc.my-namespace.svc.cluster-domain.example,該記錄被解析到service所對應ip(cluster ip);headless類型的service也會被分配一個相同格式的DNS的A記錄[10],但是這個A記錄被解析到service指向的一組Pod的ip,客戶端可以根據自己的策略來處理這些ip。

帶着這個問題,我們可以先了解下,kubernetes環境下,請求的轉發是如何實現的。由[11]可知,kubernetes藉助kube-proxy來實現請求的轉發(即到達具體的pod),kube-proxy有三種工作模式user space、iptables、ipvs;詳細查看三種模式的實現細節我們可以知道,三者除了設計思路和性能差異之外,流量轉發規則沒有本質區別(當然,ipvs所支持的策略多些)。

2.實現請求讀結點的負載均衡

在我們已經完成讀結點的集羣部署並且可以根據配置不同類型的service來實現不同的DNS解析方式前提下,如果我們用envoy作爲整體引擎集羣的入口,如何實現envoy對Milvus讀實例的負載均衡呢?

附ipvs所支持的流量轉規則

  • rr: round-robin
  • lc: least connection (smallest number of open connections)
  • dh: destination hashing
  • sh: source hashing
  • sed: shortest expected delay
  • nq: never queue

當服務暴露的接口是http時,kube-proxy直接就實現了流量的負載均衡,但是,Milvus當前暴露的是grpc接口,在我們的實踐過程中,kube-proxy在轉發gRPC請求時,並沒有實現所預期的負載均衡。

我們先了解下grpc的通信機制。gRPC[12]是谷歌開源的,基於Protocol Buffers[13],支持多語言的開發框架、通信框架。由於gRPC是基於長連接進行通信的,在基於域名/DNS來創建連接時,只會創建一個連接(如果對同一個ip:port連續多次創建連接,也會有多個連接)。我們以前面中描述的headless service爲例,客戶端(即envoy)請求DNS服務器時,會獲取一組pod所對應的ip。那麼,就剩下最後一個問題,envoy如何創建多個連接呢?

由[15]可知,在採用Strict DNS服務發現類型時,envoy會爲每一個下游服務對應的ip地址建立一個連接,並且會定時刷新ip地址列表,從而實現了流量的負載均衡。envoy的配置片段[16]如下:

      clusters:
      - name: milvus_backend_ro
        type: STRICT_DNS
        connect_timeout: 1s
        lb_policy: ROUND_ROBIN
        dns_lookup_family: V4_ONLY
        http2_protocol_options: {}
        circuit_breakers:
          thresholds:
            priority: HIGH
            max_pending_requests: 20480
            max_connections: 20480
            max_requests: 20480
            max_retries: 1
        load_assignment:
          cluster_name: milvus_backend_ro
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: milvus-ro-servers
                    port_value: 19530
                    protocol: TCP

至此,實現橫向擴展的目的達到,整體的方案如下圖2。

圖2 使用envoy+headless service實現橫向擴展

4.生產環境多集羣部署

圖3 ALL IN ONE

解決了橫向擴展的問題,我們就解決服務整體在生產環境的可用性問題。接下來,我們需要考慮如何更方便地部署服務。整體思路如圖3,我們使用helm[17]將所有涉及的服務,包括envoy、milvus讀、milvus寫、mysql(存放milvus的元數據信息)打包成一個chart。最後,我們可以把這個chart放到鏡像倉庫中(如harbor[18]),以進行集中管理。圖3中還涉及到存儲部分,包括PVC和glusterfs,其具體實現我們後續詳細講。

helm是kubernetes下的包管理工具,支持將一個有複雜結構的應用及所涉及到的所有配置模板化,並打包成一個chart(相當於一個模板),然後可以通過helm安裝這個chart(爲chart提供所需配置),生成一個release(即一個可用的應用)。

5.參考文獻

  1. https://github.com/milvus-io/milvus/tree/0.11.1/shards
  2. https://www.envoyproxy.io
  3. https://www.envoyproxy.io/docs/envoy/v1.11.0/api-v2/config/filter/network/http_connection_manager/v2/http_connection_manager.proto.html?highlight=http_connection_manager
  4. https://kubernetes.io/docs/concepts/services-networking/service/
  5. https://kubernetes.io/docs/concepts/workloads/pods/
  6. https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
  7. https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/
  8. https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
  9. https://kubernetes.io/docs/concepts/services-networking/service/#headless-services
  10. https://en.wikipedia.org/wiki/List_of_DNS_record_types
  11. https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
  12. https://grpc.io/docs/what-is-grpc/core-concepts
  13. https://developers.google.com/protocol-buffers/docs/proto3
  14. https://grpc.io/blog/grpc-on-http2/#resolvers-and-load-balancers
  15. https://www.envoyproxy.io/docs/envoy/v1.11.0/intro/arch_overview/upstream/service_discovery#strict-dns
  16. https://www.envoyproxy.io/docs/envoy/v1.11.0/api-v2/api/v2/cds.proto.html?highlight=lb_policy
  17. https://helm.sh/
  18. https://goharbor.io/

作者簡介

下期精彩

針對數據更新、保證數據一致性,以及保證服務穩定及提高資源利用率做的相關工作。


本文分享自微信公衆號 - ZILLIZ(Zilliztech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章