摘要:此篇爲該系列文章第二部分,第一部分主要講基本概念、背景、選型及服務的整體架構;本部分主要講針對低延時、高吞吐需求,我們對Milvus部署方式的一種定製;第三部分主要講實現數據更新、保證數據一致性,以及保證服務穩定及提高資源利用率做的一些事情。
1.遇到了哪些問題
在項目調研、實施以及最終上線使用過程中,我們遇到了不少的問題,包括:
-
如何解決在滿足響應時間的條件下,解決橫向擴展的問題。 -
在引擎本身不穩定的情況下,如何實現數據T+1更新時的一致性。 -
在引擎本身不穩定且問題暫時無法明確定位/解決的情況下,如何實現服務的高可用。 -
如何實現資源的動態調整,以提高資源的利用率。
2.低時延、高吞吐的要求
互聯網垂直搜索領域,特別是電商行業,對於特定業務的搜索,熱數據的量級一般是可控的(百萬級、千萬級),一般情況下,對響應時間和整體的吞吐量(QPS)都有比較高的要求。
其中,響應時間是首要條件,其次是吞吐量;如果單機在小流量下能滿足響應時間要求,但是無法滿足吞吐量要求時,集羣部署/橫向擴展能力,就是一個很自然的解決思路了。
3.解決方案
3.1 Mishards -Milvus原生解決方案
我們可以先了解下Milvus是如何解決低時延、高吞吐
問題的。如圖1所示,Milvus藉助了一個外圍服務Mishards來代理Milvus引擎,來實現分佈式部署的。處理具體請求的流程大概是這樣:
-
請求流量進入Mishards請求隊列。 -
Mishards從請求隊列中取出請求,藉助自身維護的數據段信息,把請求拆分成子請求(只查詢部分段),並把子請求分發給負責不同段的Milvus讀實例。 -
Milvus讀實例處理段請求,並返回結果。 -
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-robinlc
: least connection (smallest number of open connections)dh
: destination hashingsh
: source hashingsed
: shortest expected delaynq
: 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。
4.生產環境多集羣部署
解決了橫向擴展的問題,我們就解決服務整體在生產環境的可用性問題。接下來,我們需要考慮如何更方便地部署服務。整體思路如圖3,我們使用helm[17]將所有涉及的服務,包括envoy、milvus讀、milvus寫、mysql(存放milvus的元數據信息)打包成一個chart。最後,我們可以把這個chart放到鏡像倉庫中(如harbor[18]),以進行集中管理。圖3中還涉及到存儲部分,包括PVC和glusterfs,其具體實現我們後續詳細講。
helm是kubernetes下的包管理工具,支持將一個有複雜結構的應用及所涉及到的所有配置模板化,並打包成一個chart(相當於一個模板),然後可以通過helm安裝這個chart(爲chart提供所需配置),生成一個release(即一個可用的應用)。
5.參考文獻
-
https://github.com/milvus-io/milvus/tree/0.11.1/shards -
https://www.envoyproxy.io -
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 -
https://kubernetes.io/docs/concepts/services-networking/service/ -
https://kubernetes.io/docs/concepts/workloads/pods/ -
https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ -
https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ -
https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ -
https://kubernetes.io/docs/concepts/services-networking/service/#headless-services -
https://en.wikipedia.org/wiki/List_of_DNS_record_types -
https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies -
https://grpc.io/docs/what-is-grpc/core-concepts -
https://developers.google.com/protocol-buffers/docs/proto3 -
https://grpc.io/blog/grpc-on-http2/#resolvers-and-load-balancers -
https://www.envoyproxy.io/docs/envoy/v1.11.0/intro/arch_overview/upstream/service_discovery#strict-dns -
https://www.envoyproxy.io/docs/envoy/v1.11.0/api-v2/api/v2/cds.proto.html?highlight=lb_policy -
https://helm.sh/ -
https://goharbor.io/
作者簡介
下期精彩
針對數據更新、保證數據一致性,以及保證服務穩定及提高資源利用率做的相關工作。
本文分享自微信公衆號 - ZILLIZ(Zilliztech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。