DDIA第一章笔记:数据系统架构中的权衡

0 阅读15分钟

数据系统架构中的权衡

建议配合第一章食用。

当下数据系统的挑战

在Web和移动应用场景下,用户的数据被放在服务端数据基础设施中。用户每次与应用交互,既会读取服务端存储的数据,也会产生一些新的数据。当数据流较小,单机可以存储和处理时,问题很简单。但是随着数据规模和用户与服务器交互次数增长,就会产生一系列问题,比如说数据单机存不下,必须分散到多台机器上,还可能单机承受不了这么高的并发,Web服务或应用的性能会极速下跌。随着需求变得越来越复杂,仅仅依赖单一系统通常已不足够,可能需要组合具备多个不同能力的存储与处理系统,比如说:数据库存储数据、缓存加速读取速度、搜索索引允许用户按照关键字搜索和过滤数据、流处理和批处理等等。

在数据量和需求不断的演进下,诞生了两种不同类型的系统:数据密集型和计算密集型。在数据密集型应用中,我们通常关心如何存储海量数据、如何管理数据变化、如何在多机器部署下保证响应速度和一致性、以及如何让服务保持高可用。在计算密集型应用中,我们通常关心算法的效率,将串行代码拆分为并行任务,考虑SIMD优化加速计算,可拓展性等等。

在构建Web服务和应用时,我们通常会选择若干软件系统,在应用层将他们拼接起来,利用这些软件系统的特性去解决我们的实际需求。

但是你如果想优化应用的性能或者支持更多的需求时,就会有很多问题。数据库有很多种,典型的关系型有Mysql,可以做ACID事务;还有很多Nosql,比如说MongoDB,Redis,分别用于灵活schema和kv访问;如果考虑到写和读的权衡,就还有数据库底层使用LSM-Tree和B+树的选择。缓存也有很多种做法,搜索引擎也有多种构建方式,如何进行权衡?

这个问题在看完DDIA,了解一些常见的中间键设计理念之后就知道了。

DDIA的第一章主要基于企业软件中数据的使用方式,因为只有这类软件才有足够大的数据规模,值得投入复杂的技术方案。而且在企业软件的数据使用中,有一个难点在于不同的业务需要使用同一份数据做完全不同的事情,下面也会讲解这个问题。

分析型与事务型系统

在同一个应用或者web服务当中,不同的业务有不同的数据使用需求。

比如说后端负责实时处理用户在前端发起的业务操作,例如下单、注册或者支付,此时服务需要读取和更新数据,在事务ACID边界内写入数据库,这种系统被称为事务型系统。还比如说业务分析和搜广推需要通过单独的MQ、ETL或者CDC读取数据,针对分析所需的数据类型进行了优化,然后对这些数据进行聚合计算,这种系统被称为分析型系统。

事务型系统以写为主,一致性和实时性要求高,面向业务操作;分析型系统以读为主,可以容忍延迟,面向数据分析。二者可以通过ETL或者CDC实现解耦,这样业务团队可以只专注于事务操作,数据团队也可以只专注于查询性能与数据优化,互不干扰。

在分析型事务系统中,系统通常通过某个键来查找少量记录(点查询),或者基于用户的输入来插入或者更新数据,因为是交互式的应用,所以是OLTP的访问模式。在分析型事务系统中,分析查询通常会扫描大量记录,并聚合统计信息,而不是将单个记录返回给用户,所以是OLAP的访问模式。

在OLTP系统中通常不允许用户构建自定义 SQL 查询并在数据库上运行它们,因为这可能会允许他们读取或修改他们没有权限访问的数据。此外,他们可能编写执行成本高昂的查询,从而影响其他用户的数据库性能。出于这些原因,OLTP 系统主要运行嵌入到应用程序代码中的固定查询集,只偶尔使用一次性的自定义查询来进行维护或故障排除。但是在OLAP系统中,用户通常可以自由地手动编写SQL查询。

数据仓库

在一开始时,在线事务处理和分析查询使用的是相同的数据库,然而,一家大型企业可能有几十个联机事务处理系统,这些系统独立运行。在这种情况下,查询数据并做数据分析直接查询这些OLTP系统不可取,原因如下:

  • 想要查询的数据可能分布在多个数据库中。
  • OLTP的模式和数据布局不太适合分析。
  • 分析查询相当重,可能会影响其他运行在OLTP系统上的用户的性能。

在这种情况下诞生了数据仓库,这是一个独立的数据库,数据分析师可以随意查询,不会影响OLTP系统的性能。数据仓库适配OLAP系统,所以会尽可能地做这方面的优化,比如说列式存储、预聚合和分区等等方面。

由于用户在应用层面是通过OLTP系统进行交互的,所以用户的权威数据源一般留在OLTP系统中,此时如果使用数据仓库做OLAP分析的话,就必然要包含应用中各种OLTP系统中的派生数据。此时就肯定会有一条数据流从各种OLTP系统流向数据仓库,这条数据流就是ETL操作。

数据从OLTP数据库中提取,转化为适合做数据分析的派生数据,然后加载到数据仓库中,这种数据流被称为提取-转化-加载。有时转化和加载的顺序会互换,也就是先加载到数据仓库,再在数据仓库中进行转化,此时就变为ELT。

下面给出DDIA中的图例: ETL 到数据仓库的简化概述

在计算机领域中,通常会有把两个适配不同领域的技术结合起来的方法,把各自优势拼接在一起,得到更强的能力。比如说兼顾时间局部性与频率的LRU-K,比如说结合实时性的长轮训,还有B+树和LSM树的混合架构,得到更快的读写性能。那么OLTP和OLAP肯定也有一种途径组合到一起,这就是HTAP系统(混合事务/分析处理)。此类系统的目标是在单个系统中同时支持在线事务和数据分析,而无需从一个系统ETL到另一个系统,减少复杂度。

然而,许多HTAP系统内部由一个OLTP系统与一个单独的分析系统耦合而成,其中ETL被隐藏在了公共接口后面,所以本质上还是两个系统分开,各司其职。随着工作负载变得越来越苛刻,系统会变得更加专业化,并针对特定工作负载进行优化。通用系统可以舒适地处理小数据量,但规模越来越大,系统往往变得越专业化。

数据湖

数据仓库通常使用通过SQL进行查询的关系数据模型,这种关系模型很适合业务分析师所需要的查询,但是不太适合以下场景:

  • 特征工程,将数据库的行和列转化为特征向量或者矩阵,进行机器学习的训练。
  • 面对文本数据或者照片,并使用自然语言处理技术或计算机视觉技术尝试从中提取结构化信息。

因此,单纯使用关系型的数据仓库没法解决全部的问题,我们需要一种新的数据仓库,可以提供大部分业务要求的数据格式,这就是数据湖:一个集中的数据存储库,保存任何可能对分析有用的数据副本,通过ELT过程从事务型系统获得。此时为ELT是因为通常将数据原封不动入湖,等需要消费的时候再由消费者进行转化。

数据湖与数据仓库的区别在于,数据湖不强制任何特定的文件格式或数据模型,数据湖中的文件可能同时包含文本,图像,特征向量等多种类型的数据模式。数据湖包含事务型系统产生的原始形式的数据,没有转化为关系型数据仓库模式,所以每个数据消费者都可以将原始数据转换为最适合其需求的形式。

记录系统与派生数据

个人觉得这一段写的很好,看完之后对后端项目有了更清晰的认知。

主要就是说系统中的数据存在于记录系统与派生数据系统中。其中记录系统是权威数据源,保存某类数据的权威版本,比如说前面的OLTP数据库,还有常见后端项目中的单机数据库。在这个系统中,每个用户的操作反应在这上面才表示成功,而且每个事实只会表示一遍。如果其他系统与记录系统不一致,则按定义以记录系统为准。

派生数据系统是对其他系统中已有数据进行转化或处理后的结果。如果派生数据丢失,可以从原始数据源重新构建。比如说之前提到的数据仓库,还有常见后端项目中的缓存,命中的话就由缓存直接返回,未命中的话就回退到底层数据库中寻值。

其实,从技术上来说派生数据完全是冗余的,因为它消费的是别处产生的数据,但是派生数据系统往往是高性能读取的关键,我们可以选择从同一个权威数据源派生出多个不同的数据集,以便进行不同查询的优化。

分析系统通常属于派生数据系统,因为它消费的是别处产生的数据。事务型服务往往同时包含记录系统和派生数据系统:前者是数据首先写入的主数据库,后者则是用于加速常见读取操作的索引与缓存,尤其针对记录系统难以高效回答的查询。

在项目中只要明确哪些数据由哪些数据派生而来,哪些是权威数据系统,哪些是派生数据系统,项目的架构就会清晰很多。当一个系统的数据由另一个系统的数据派生而来时,你需要在权威数据系统原始数据变化时同步更新派生数据。

云服务与自托管

这章主要讨论自托管应用和云服务的取舍,自托管应用就像Mysql和Redis,可以本地部署和依靠IaaS云部署,云服务就是依赖Web或者api调用云提供商提供的软件。

二者利弊

使用云服务就相当于把软件的运维外包给提供商,能够在团队缺乏运维经验、负载波动大等情况显著降低时间成本和人力成本。但是如果团队已经有成熟的运维能力且工作负载相对稳定,自托管往往在长期成本上更具优势。

云服务可以提供弹性伸缩,专业运维和统一SLA,但是可能会功能受限,无法从底层框架适配业务功能。因此,组织倾向于在新项目或可弹性扩展的子系统中采用云服务,或者做混合部署。如果对延迟极度敏感,或者需要硬件深度调优等定制化情况,仍需保留或自行维护内部系统。

云原生

云原生就是多个云服务组合形成的架构,利用了云服务的弹性伸缩,快速故障恢复的优势。比如说有一个微服务电商系统,每个业务(商品、订单和支付)都各自实现为独立的镜像,使用K8s部署服务,如果修改代码的话就push到代码仓库,有一个服务可以把最新代码自动热更到集群。

云原生的系统架构可以实现存储和计算的分离。在传统计算中,磁盘被视为持久存储,只要数据写入磁盘,就假设不会丢失。为例防止单盘故障,通常使用RAID在一台机器的多块磁盘上维护副本,应用层对此完全透明。进入云环境之后,虽然计算实例仍然有本地磁盘,但是云原生系统通常把这些磁盘当作临时缓存,因为实例实效的话,对应实例上的磁盘也会一起消失。为了弥补这一点,云提供商提供可以从一个实例分离并附加到另一个实例的虚拟磁盘存储。这种虚拟磁盘存储是一组单独机器提供的云服务,主要就是模拟磁盘的行为。

但是虚拟磁盘存储的每次块读写都要经过网络延迟,吞吐受限,而且在网络抖动或者底层节点不可用的情况下会影响可用性。因此云原生系统通常避免直接使用虚拟磁盘存储设备,而是转向对象存储来持久化大文件,使用云原生数据库存储小粒度的数据。由于此时把持久化交给独立的存储服务了,计算节点通过网络API读取或写入数据,而不直接依赖本地磁盘,同时在计算节点本地使用缓存层(Redis)来保存热点数据以降低访问延迟。此时就实现了计算与存储的解耦,可以根据自己的瓶颈独立扩缩容。

分布式与单节点系统

涉及多台机器通过网络通信的系统称为分布式系统,参与分布式系统的每个进程称为节点。以下是使用分布式系统的情况:

  • 如果应用程序涉及两个或多个交互用户,每个用户使用自己的设备,那么系统不可避免地是分布式的:设备之间的通信必须通过网络进行。
  • 想要应用程序在一台机器发生故障时可以继续工作,此时就要部署多台提供冗余。
  • 如果你的数据量或计算需求增长超过单台机器的处理能力,你可以潜在地将负载分散到多台机器上。
  • 为了用户可以从地理位置接近他们的服务器获得服务,避免用户进行长时间网络等待。

分布式系统的问题

在计算机的世界中,大部分技术都是有优缺点的,需要进行tradeoff,享受了分布式系统带来的便利就得应对分布式系统带来的问题。

首先就是分布式系统中各个节点通过网络进行的每个请求和API调用都需要处理失败的可能性:网络可能中断,可能会延迟很久,服务可能奔溃,任何请求都可能超时而没有收到响应。在这种情况下我们不知道服务是否收到了全球,简单重试不一定可以保证幂等性。

而且尽管数据中心网络很快,但调用另一个服务仍然比在同一进程中调用函数慢得多。在处理大量数据时,与其将数据从其存储处传输到处理它的单独机器,将计算带到已经拥有数据的机器上可能更快。而且更多的节点并不总是更快:在某些情况下,一个简单的单线程程序在单台计算机上运行的性能可以比在具有100多个CPU核心的集群上更好(经典Redis八股)。

在实际编程中,对分布式系统进行故障排查也十分困难,因为此时不是单机了,没办法打断点进行debug,只可以通过观测海量日志、进行metrics打点的方式进行调试。而且由于分布式系统的并发,bug不一定会规律性重现。

出于所有这些原因,如果你可以在单台机器上做某件事情,与搭建分布式系统相比通常要简单得多,成本也更低,而且CPU、内存和磁盘已经变得更大、更快、更可靠,出错概率其实没那么大。