云计算·大数据 频道

Apache Druid 在 Shopee 的云原生架构演进

  Shopee Druid 为什么要演进到云原生架构?如果要实现云原生化,需要做哪些事情?在这个过程中,可能还会踩到哪些坑?

  本次分享将围绕以上三点展开,从 Shopee Druid 物理机架构遇到的问题入手,探索云原生架构的优势,并重点介绍云原生架构设计的技术细节,以及一些落地的实践。

  在 ApacheCon Asia 2022 中,来自 Shopee Data Infra 团队的 Jiayi 分享了 Apache Druid 在 Shopee 的云原生架构演进。本文根据演讲内容整理而成。

  1. 背景

  1.1 Druid 介绍

  Druid 是一款高性能的、实时的、分析型数据库。它的高性能主要体现在这几方面:列式存储、Bitmap 倒排索引、数据压缩、SIMD 矢量化加速、缓存系统等等。

  而 Druid 的 Lambda 架构,也使得其能够支持实时数据的写入和查询。

  同时,Druid 作为一个 OLAP 引擎,内置了丰富的查询算子,以满足各式各样的分析需求。

  通过 Venn 图可以看到,Druid 兼备了时序数据库、数据仓库和全文检索引擎的部分特点:

  如果你对 TSDB 比较了解,那么可以将 Druid 近似理解为 InfluxDB;

  如果你对数仓更加专业,那么也可以简单类比为 Hive;

  而如果你对全文检索更为熟悉,则可以想象成 ElasticSearch。

  当然,实际的内在设计细节,是有诸多不同的,这里就不展开讲了。

  1.2 基于物理机架构

  对 Druid 有了初步的认识之后,先来看一下,之前 Shopee 基于物理机架构构建的服务,遇到了哪些问题。

  主要包括以下几方面:稳定性问题、效率问题、成本问题和安全问题。

  1.2.1 稳定性问题

  首先是稳定性问题。

  这是一幅 Druid 查询 QPS 的监控图。可以看到,在临近中午 12 点的时候,突然出现一个查询流量的尖峰,远远超过了预先设定的告警阈值。之后我们定位的结论是,业务脚本程序的 Bug 导致发送了很多无意义的请求。

  相信大家也或多或少地遇到过类似的情况。比如临近饭点了,甚至还在就餐中,突然就接到一波告警电话,也只能废寝忘食地解决线上问题;而如果故障的时间很不巧是在半夜,那么告警电话就会变成“夺命连环 call”。

  接下来看另外一个例子。

  Druid 慢查询脚本的某一次告警信息中,其中大部分内容都进行了脱敏,我们只需要关注高亮的部分。从中可以看出,这是一条尝试分析过去一整年数据的重查询,其消耗了大量的服务器资源,导致其他业务的正常查询请求都受到了影响。

  尤其是当我们将 SQL 客户端开放给终端用户之后,甚至还会遇到那么一两个不带任何限制条件的 select * 查询。这无异于发起了一波猛烈的 DDoS 攻击。

  类似的稳定性案例还有很多,主要可以分为三类。

  一类是查询相关的,例如:

  一个查询中嵌套了太多层的子查询;

  一次性查询了过去好几年的冷数据;

  对两个或者更多的事实表进行 Join 查询;

  亦或是由于程序故障,在瞬间发起了大量的查询请求。

  第二类是写入相关的,例如:

  写入任务中配置的分区数过多;

  写入的 TPS 速率过快;

  写入任务的数据体量过大。

  第三类是集群自身的,例如:

  我们通过 Crontab 定时检测服务状态,并自动重启进行故障恢复,但是周期往往是分钟级的。

  可见,稳定性问题的诱因是非常多的,实在是防不胜防。

  1.2.2 效率问题

  分析完了稳定性问题,我们再来看下效率问题。

  当因为资源不足而导致性能瓶颈时,我们需要和业务的对应负责人临时沟通,交流的时间成本是比较高的,因此效率也是比较低的。即便我们完成了信息同步和沟通,扩缩容的操作需要人为介入,也很难在短时间内完成。

  而所有业务同在一个大集群中,导致很难给业务进行优先级排序。因为对于每一个业务而言,自身肯定是最重要的。从而,我们也就很难实现自动的流量降级。

  而效率问题,最终会使得业务的发展受到严重制约。

  1.2.3 成本问题

  接下来是企业需要纳入考量的成本问题。

  首先来看机器资源的成本:

  我们往往会因为端口冲突、资源偏好相同等原因,使得在同一台物理机上的多实例部署,变得异常困难;而简单的混合部署,仍然会存在着资源浪费的情况;另外,也无法根据业务规模和写入速率,定制化分配资源。

  从而,一旦资源没有及时给到位,业务的性能需求将无法满足;而如果资源给多了,又很难做到高资源利用率。

  其次再看看人力成本的投入:

  物理机集群的构建复杂度较高,很容易出现不一致的情况。例如,同一个业务在旧的物理机集群上,运行是没问题的,但是迁移到了新物理机集群,就莫名出问题了;并且每一个独享集群的构建,都需要消耗无数的人/天;而当我们辛辛苦苦地将独享的物理机集群构建出来之后,会发现后续的维护成本还将随着集群数量直线上升。

  要知道,即便是支持了自动识别新节点,自动负载均衡的 Druid,也还是会让运维同学感到压力山大。而如果是其他不支持这类功能的引擎,则更是雪上加霜。

  最终,成本问题便会拉低我们服务的竞争力。

  1.2.4 安全问题

  最后,我们来看一下安全问题。

  在我们的低版本集群中,是没有开启鉴权的,这会导致某一个业务的误操作,可能会影响其他的业务,造成难以预料的后果。而即便是开启了鉴权,也会因为没有物理隔离,仍然无法达到 100% 的安全可靠。

  并且,低版本中存在着诸多已知安全漏洞,甚至是类似于 Log4j 这种 0Day 的重大安全漏洞。

  共享大集群的业务数量太多,集群规模过大,导致升级的阻力和风险极大。这也使得升级的事情一拖再拖,与新版本相差越来越大,进而还会出现兼容性阻碍。也就是说,我们必须要先升级到某一个特定版本,才能升级到新版本,这会使得升级的复杂度倍增,随之而来的也是愈发难以评估的操作风险。

  另外,也无法享受到新版本的红利。例如前面提到的重查询问题,新版本 Druid 中支持了快慢查询队列,可以避免个别不合理的重查询,影响到其他正常的查询。这也可以一定程度上缓和部分痛点。

  以上便是大部分物理机架构下的问题了。接着我们来分析一下,云原生架构是如何解决这些问题的。

  1.3 基于云原生架构

  我们在正式进行云原生化进程之前,也在内核层面做了大量的工作,但是奈何都难以达到一种“药到病除”的效果。

  同样,我们也充分进行了调研和测试,对很多技术细节进行权衡和取舍,以找到最适合的方案。并同各个利益方进行了意见的收集,总体而言各方反响强烈,支持我们的架构升级。

  俗话说,混乱是阶梯。也正是因为共享物理机大集群的无序和动荡,才给了业务方足够的动力,来和我们一起完成新架构的落地。

  上图展示了几个主要的利益相关方,以及其各自的诉求。

  本着客户至上的精神,我们先来了解一下业务方的需求。放在重要位置的还是要保障稳定性,并且要能够具备大促流量峰值的抗压能力,支持秒级的自动扩容。

  随后是运维方的需求。他们希望能够保证可观测性,方便实时、清晰地观察到各个组件的健康状态和性能指标;并且,要确保集群的资源利用率足够高;还要能够支持灵活多变的告警策略,做到动态阀值和分级告警等。

  最后是内核方的需求。我们期望能够只专注于内核,完全托管除了 Druid 内核研发以外所有的事宜;要能够支持 CI/CD 持续集成;以及采用 Docker 镜像,代替之前纯代码的交付模式,提高整体的迭代效率。

  而云原生架构包含了高稳定性、高效、低成本和安全稳固等方面的优势,下文将详细介绍它是如何满足各方需求的。

  1.3.1 高稳定性

  首先是高稳定性。

  我们给各个核心业务都建立了独立的 Druid on K8S 集群。并且,各个独享集群的资源是隔离的,所以从根本上解决了资源抢占的问题。

  而针对每一个业务的特征,我们也进行了定制优化。同时,服务出现故障时,可以做到秒级自动恢复,且用户无感知。更进一步,我们还在不同的机房构建了 HA 集群,实现 IDC 级别的高可用,以满足个别核心业务更为严苛的稳定性需求。

  除了一般意义上,针对集群参数配置的定制级优化,我们可能还会遇到类似于需要协调压缩算法版本的场景。

  假设 Druid 上游的业务用了版本 A 的 ZSTD 压缩算法,而 Druid 中默认的是版本 B 的,则需要调整并和业务版本保持一致,否则数据无法正常被反序列化。

  如果是之前的共享集群模式,另外新来了一个业务,用的又是版本 C 的,此时就无法协调了。那么,就需要推动业务,去改造所有的上下游组件,成本将会非常高。

  关键,还不仅是成本的问题,如果恰巧全链路中有一个组件也是共享集群模式,无法进行调整,那么整个链路都将无法正常打通。

  当然,实际会遇到的案例还有很多,这里暂不一一列举。

  1.3.2 高效

  云原生架构的另一个优势是高效。

  由于我们确保了同一个独享集群中,所有项目都是相同的部门或者项目组,使得项目之间的优先级更容易被评估和排序,从而更容易实现自动流量降级。

  并且,因为支持了根据负载情况的自动扩缩容功能,我们不再需要提前和业务沟通,来收集大促流量的增幅,也就不再担心预估不准确的问题了。

  1.3.3 低成本

  1)机器资源利用率

  随着 Druid on K8s 集群规模不断壮大,在某一个时间点,我们进行了一次横向对比。结果发现,云原生架构的集群机器数量少于旧的物理机集群,但是承载的业务写入量却更多。

  在上面这幅柱状图中,左边是物理机架构集群,右边是云原生架构集群。橘黄色表示数据写入峰值,紫色表示数据写入总量。可以看出,无论是峰值还是总量,云原生架构都是比物理机架构更高的。

  因此,可以从这一个侧面反映出 Druid 云原生架构的机器资源利用率更高。

  2)人力成本

  接下来我们再看看人力成本方面。

  因为物理机架构集群搭建的复杂度比较高,从开始构建,到完全可以作为线上正式环境交付,前后大概需要一个月的时间。即便是后续操作更加熟练,脚本化程度更高,仍然需要数天的时间。

  而云原生架构的集群搭建,可以通过 CI/CD 的方式,进行一键部署,做到分钟级交付。

  上图 x 轴表示集群的数量,y 轴表示对应消耗的人/天。黄色的线表示物理机架构,绿色的表示云原生架构。通过这幅图可以更加直观地感受到二者之间的区别。

  1.3.4 安全

  最后是安全方面的优点。

  首先,我们默认会开启鉴权,保障业务的数据安全。

  其次,因为云原生独享集群更加轻量,使得我们更容易跟进 Druid 内核版本。高版本中修复了已知的安全漏洞,会更加安全可靠。

  另外,容器化运行的模式,可以实现物理隔离,避免了资源抢占,很好地控制了故障域。再也不用担心某一个业务的误操作波及到其他业务,线上的风险也进一步降低了。

  当然,新架构的优势远不止这些。并且,优势叠加之后的放大效应,将我们的服务质量提升到了一个完全不同的高度。

  接下来我们总结一下,在云原生化的过程中又遇到了哪些机遇和挑战。

  1.4 挑战与机遇

  首先,关于挑战部分:

  我们需要引入云原生的概念,满足所有利益方的需求,设计出一套全新的云原生架构;还需要攻克众多技术难点,从 0 到 1 构建出 K8s 底座;并利用 Docker 镜像替代之前纯代码的迭代方式,以及使用 Helm Chart 完成 Druid 集群的容器编排和管理;最后,我们需要充分验证和测试大部分业务场景,并建立标杆用户。然后,再大面积推广业务的迁移,并持续进行定制级优化。

  其二,关于机遇部分:

  因为需要构建一整套复杂的云原生架构,也有机会进一步锻炼和提高我们架构的能力;而在实践和落地 K8s 云原生的过程,通过解决具体的技术难题,也增强了相关的技术水平;同时,借助前卫的容器化运行模式,也引领了 Druid 开源社区的云原生化进程;另外,在逐一完成业务的迁移过程中,也有机会和业务深入了解真实的使用场景和痛点,深化了对业务的理解。

  相信到这里,大家已经知道了,我们为什么要演进到云原生架构,以及云原生架构落地过程中遇到的机遇和挑战。

  2. 架构设计

  接下来,就让我们进入架构层面,了解一下 Shopee Druid 总体的云原生架构设计,以及各个组件之间的交互。

  2.1 架构总图

  架构总图主要划分为了六个层次,分别是业务层、平台层、可视化层、引擎层、GitOps 层 和 K8s 层。

  最上层的是业务层,主要包含了用户行为分析、商品推荐、销售数据分析、品牌分析、网络性能分析、核心指标分析、广告收益分析、应用轨迹分析、跨境电商分析、内容推荐等。

  可以看出,Druid 在 Shopee 内部的应用场景是非常多样的。

  第二层是平台层,包含了 DataStudio 数据分析、TrinoDB 联邦查询引擎、DataHub 数据集成、DataMap 和 Metamart 元数据管理等。

  可视化层包含了 Druid 自带的 Web UI、K8s Dashboard、Apache Superset、Grafana,以及业务实现的前端页面等。

  接下来是引擎层面。Druid 下方是 PostgreSQL 作为 Druid 元数据存储;HDFS 作为底层存储,记录全量的业务数据;ZooKeeper 则作为配置中心。

  Druid 右边是 Kafka 实时数据的写入和 HDFS 离线数据的导入;同时,也支持 Spark 数据分析。

  最后,右下角是 ElasticSearch 负责日志的存储;Druid 通过 Metric System 进行自监控;Grafana 再负责告警。

  再来看一下 GitOps 层。我们将整个发版流程划分为四个部分:开发、测试、预发和生产。

  与传统发版不同的是,我们交付的是 Docker 镜像,并使用 Harbor 作为镜像仓库存储。同时,使用云原生架构的集群,通过容器化运行,保障了测试和线上运行环境的一致。再也不用担心通过了测试环节,结果在线上跑不通的尴尬情形了。

  最底层是 kubernetes 集群。我们在不同的 IDC 机房各自搭建了一套对等的 K8s 集群,并在 K8s 集群之上,为各个核心业务构建了独享的 Druid 集群。

  我们的拆分逻辑是按照部门进行划分。某一个部门中可能还会有多个项目,而一个项目下,再对应多个 DataSource 表。

  举例来讲,假设项目 2 和 3 都是同一个部门下的,如果项目 2 是 metric 监控,项目 3 是计算实际业务数据的,我们就能很容易地进行优先级排序,在整体资源出现瓶颈的时候,便可以自动地降级项目 2 ,以保障最核心的项目。

  架构分层图剖析完毕。那么,内部的组件之间又是如何交互与配合的呢?

  2.2 组件内部交互图

  先从流量入口看起。

  通常,读写请求通过可视化页面或者后台程序发起,转而被 Druid 接收到。

  我们可以看到,Druid 内部的各个组件都是多副本的,架构设计上没有任何的单点问题。这也是为什么相比于其他很多数据库,Druid 更容易实现云原生化。因为 Druid 组件的职责划分更为到位,很容易对应到 Pod 进行生命周期管理。而每个组件内部的功能又十分内聚,新节点还可以自动识别,并增添到分布式集群中,使得 HPA 或者 VPA 扩缩容策略的应用,也更加手到擒来。

  然后是 ZooKeeper 作为一个配置中心,也负责了 Overlord 和 Coordinator 节点的选主、任务分发的功能。

  PostgreSQL 是元数据存储引擎,包括 DataSource、Segment 和 Task 的元数据信息等。

  HDFS 则提供了底层存储,所有的业务数据都将全量的存储在 HDFS 集群中,并根据 Retention Rule 加载到 Historical 数据节点,以加速查询。

  接下来是系统监控部分。

  目前有三个层面的指标监控,分别是物理机层面、Service 层面和 Druid Metric 层面:

  首先,Prometheus 从物理机的角度进行指标监控,我们就可以知道某一台机器的负载情况;

  其次,MetricBeat 从 Service 服务层面进行指标监控,还可以知道 Druid 中某一个服务的资源使用情况,作为扩缩容的依据;

  再者,Druid 自身也有 Metric System 指标监控,还能洞察写入查询的负载和性能表现,等更多细节信息;

  当然,还有 Jaeger 可以从另一个视角进行全链路分析,以便快速定位到某一个环节的瓶颈,可以进一步提高解决问题的效率。

  通常,我们会发现,各个监控组合起来,往往会达到 1+1>2 的效果。但是,我们也需要根据实际情况进行取舍。建议大家逐步地迭代,引入新的监控组件,以得到全新的分析视角,而不能一味地进行堆砌。因为,只有控制好架构的复杂度,才能更有效地降低系统的风险。

  最后是 K8s 部分。Ingress 作为流量入口,对 Service 进行了更高维度的抽象,而 Service 会将流量负载均衡之后,重定向到 kube proxy。随后,再代理转发到最小调度单元——Pod 上面。

  而 Pod 也分为无状态和有状态两种,前者包含了 Router、Broker、Coordinator 和 Overlord 等,后者包括 MiddleManager、Historical、ZooKeeper、PostgreSQL 等。并且,这些有状态的 Pod 还需要申明 PV 持久卷,方便数据的保存。而为了避免跨 K8s node 访问,通常我们会增加节点亲和性,提高数据的本地性。

  我们还可以看到,有状态 Pod 组成了 Stateful Set 集合,而无状态 Pod 组成了 Replica Set 集合。为了便于版本控制和生命周期管理,Replica Set 基础之上,还抽象出来了 Deployment 的概念。

  所有的这些集群状态,都被保存在 K8s master 节点的 Etcd 中。此外,Controller Manager 则会维护集群的状态,Scheduler 进行调度,并通过 api server 提供统一入口。

  相信到这里,Druid 云原生化架构对你而言,已经不再是一个“黑盒”了。

  以上便是架构设计的全部内容。接下来的一个小节,将简单介绍 Shopee 是如何封装服务,并形成一套完整的解决方案,以应对多样化场景的业务需求。

  3. 落地实践

  首先,我们会给各个核心业务提供 Druid on K8s 独享集群,以保障稳定、安全、高效和低成本。

  其次,我们还会构建配套的 Grafana on K8S 独享集群,并内置一些基础的监控面板,例如 TPS 写入、QPS 查询等等。并给到业务 Admin 管理员权限,方便业务方根据自己的业务场景需求,定制化设计相应的监控面板。

  另外,部分业务还可能会有二次开发 Grafana 插件的需求。而我们在独享模式下,则更容易进行升级和迭代。即便是新插件出现 Bug,导致 Grafana 故障,也不会影响到其他业务了。

  当然,为了支持多样化的业务场景,我们除了 Grafana,还提供了丰富的可视化方案:

  Turnilo 可以通过“托拉拽”的方式,快速地构建出想要的监控面板;

  K8s Dashboard 可以实现运维和监控的一体化;

  Superset 支持海量的图表类型和细粒度的权限管控;

  Kibana 则可以对日志进行可视化呈现和告警配置。

  总有一款能够很好地契合我们的业务需求。

  4. 总结和展望

  通过演进到云原生架构,我们保障了高稳定性、高性能,实现了高效和低成本,极大地提高了服务质量,促进了业务的发展。

  我们从架构的维度,解决了内核层面无法处理的,或者是解决起来成本过高的问题。而平时如果你也遇到了类似的困境,不妨跳脱出来,换一个角度来思考,可能也会收获意想不到的结果。

  除了继续推动架构的升级和内核的完善,我们还会在集成与被集成、开源社区合作,和打造团队影响等方面,持续发力。

0
相关文章