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

star2017 1年前 ⋅ 449 阅读

贝壳找房 | 基于 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共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出

更多内容请访问:IT源点

全部评论: 0

    我有话说: