Fork me on GitHub

贝壳找房 | 基于 Milvus 的向量搜索实践(二)

贝壳找房 | 基于 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.exampl,该记录被解析到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
  • l: least connection (smallest number of open connections)
  • dh: destination hashing
  • sh: source hashing
  • s: 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/

作者简介

图片

下期精彩

针对数据更新、保证数据一致性,以及保证服务稳定及提高资源利用率做的相关工作。


本文地址:https://www.6aiq.com/article/1607727190023
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出