设计数据密集型应用第一部分:数据系统的基石

  《Designing Data-Intensive Applications》这本书,今年在不同的地方都看到有推荐,简单浏览了一下内容,感觉还是值得一读的。由于是英文,读起来还是有点慢,最近读完了本书的第一部分,写篇文章记录一下。本文主要是读书摘要和笔记,也有一些自己的总结和思考。

  对我而言,看这本书的收获在于扩宽了知识面,对一些以前只是知其然的东西,知其所以然。另外,本书该出了大量详实资料的链接,有助于对某一领域的进一步学习。

  本文地址:https://www.cnblogs.com/xybaby/p/9363943.html

DDIA讲了什么

  于我而言,还是第一次听说数据密集型(data-intensive)这个属于。之前在分析一个程序(软件)的时候,经常用到的CPU Bound、IO Bound之类的词汇。那么什么是data-intensive呢

  We call an application data-intensive if data is its primary challenge—the quantity of data, the complexity of data, or the speed at which it is changing—as opposed to compute-intensive, where CPU cycles are the bottleneck.

  即是说,应用的核心挑战是数据:大量的数据,复杂、丰富多样的数据,快速变化的数据。每个程序员或多或少都在于数据系统打交道,包括但不限于:database、message queues、 caches,、search indexes, frameworks for batch and stream processing。不同的数据系统满足了不同的应用需求,即使是同一种数据系统,如database,也有各种不同的设计哲学与实现方案。

  当然,也许很多人并不直接从事数据系统的开发工作,但了解这些数据系统的工作原理是很有益处的。当我们了解了原理之后,能为我们的应用需求选择最合适的数据系统,能解释系统的一些约束与现象,能将这些数据系统有效的组合起来,服务于应用。

  在DDIA这本书中,对这些数据系统有概要的介绍,然后是区分各自的优缺点与特性,然后分析这些特性是如何实现的。

  DDIA一书分为三部分,第一部分是数据系统的基石,一些基本的思想和组件;第二部分是分布式数据系统;第三部分是派生数据系统。本文介绍第一部分。

数据系统的衡量标准

  一个应用往往是由多个数据系统组合而来,包括但不限于:

• Store data so that they, or another application, can find it again later (databases)
• Remember the result of an expensive operation, to speed up reads (caches)
• Allow users to search data by keyword or filter it in various ways (search indexes)
• Send a message to another process, to be handled asynchronously (stream processing)
• Periodically crunch a large amount of accumulated data (batch processing)

  这些数据系统就像积木,通过程序员的精心搭配构建成应用这座大厦。

  对于一个系统(应用),都希望达到以下标准:Reliable, Scalable, and Maintainable

Reliability

The system should continue to work correctly (performing the correct function at the desired level of performance) even in the face of adversity (hardware or software faults, and even human error). 

  即使系统中的某些部分出错了,整个系统也能继续对外提供服务,因此可靠性也经常称为容错性( fault-tolerant)。错误可能来源于硬件错误(hardware hardware)、软件错误(software error)以及人工错误(human error)

  在一个7*24运行的大型分布式系统中,硬件错误是非常常见的,但硬件错误一般影响范围介绍 -- 只会影响出问题的计算机或者磁盘,一般通过冗余来应对硬件错误。相比而言。软件错误影响范围更大,例如:代码的bug影响每一个程序实例;单个程序耗光共享资源(CPU 内存 网络 service);一个底层service挂掉或者异常影响所有上层服务。

  不容忽视的是human error,这个时有发生,比如数据库、网络的错误配置,比如经常看到的“从删库到跑路”。

  one study of large internet services found that configuration errors by operators were the leading cause of outages

  人是不可靠的,尽量自动化能减少悲剧的产生。

Scalability

As the system grows (in data volume, traffic volume, or complexity), there should be reasonable ways of dealing with that growth

  伸缩性,当系统的规模增长的时候,系统能保持稳定的性能。这就有两个问题:如何定义负载(load parameter)、如何衡量性能(performance)。

  这两个参数(指标)都取决于应用类型,比如web服务,那么负载就是每秒的请求数,而性能就是系统每秒能处理的请求数目。

  当负载增大的时候,有两种方式衡量性能:

  • 如果系统资源不变,系统性能会有什么变化
  • 为了保证性能不变,需要增加多少资源

Maintainability

  Over time, many different people will work on the system (engineering and operations,both maintaining current behavior and adapting the system to new use cases), and they should all be able to work on it productively.

  可维护性是衡量代码的一个重要标准,软件写出来之后,还要修bug、满足新需求、添加新功能、配合其他产品升级等,维护软件的人很可能不是写代码的人,因此可维护性就显得尤为重要。

  以下三个原则有助于提高软件的可维护性:

  • Operability

  Make it easy for operations teams to keep the system running smoothly.

  • Simplicity

  Make it easy for new engineers to understand the system

  • Evolvability

  Make it easy for engineers to make changes to the system in the future

常见数据模型

  Data model是数据的组织形式,在这一部分,介绍了relational model、document model、graph-like data model,不同的数据模型的存储方式、查询方式差异很大。因此,应用需要根据数据本身的关联关系、常用查询方式来来选择合适的数据模型。

  数据与数据之间,有不同的关联形式:one to one,one to many,many to one,many to many。one to one,one to many都较好表示,困难的是如何高效表示many to one,many to many。早在1970s年代,就有两个流派尝试来解决many to many的问题,relational model, network model,自然,network model是更加自然、更好理解的抽象,但是相比relational model而言,难以使用,难以维护。因此relational model逐渐成为了主流的解决方案。

  relatioal model将数据抽象为关系(relation,sql中称之为table),每一个关系是一组形式类似的数据的集合。对于many to many的数据关联,relational model将数据分散在不同的relation中,在查询时通过join聚合。

  sql是典型的声明式查询语言(declarative query language),只要描述需要做什么,而不需关心具体怎么做,给用户提供的是一个更简洁的编程界面。

Nosql

  2009年左右,Nosql(not only sql)逐渐进入人们的视野,近几年在各个领域得到了广泛的发展与应用。NoSQL具有以下特点:

  • 天生分布式,更好的伸缩性,更大的数据规模与吞吐
  • 开源
  • 满足应用的特定需求
  • 避免sql约束,动态数据模型

  在Nosql阵营中,其中一支是以mongodb为代表的document db,对于one 2 many采用了层次模型的nested record;而对于many 2 one、many 2 many类似关系数据库的外键

  这里有两个很有意思的概念:

  schema-on-read (the structure of the data is implicit, and only interpreted when the data is read)

  schema-on-write (the traditional approach of relational databases, where the schema is explicit and the database ensures all written data conforms to it)

  显然,前者是document db采用的形式,后者是关系型数据采用的形式。前者像动态类型语言,后者则像静态类型语言,那么当schema修改的时候,前者要在代码中兼容;后者需要alter table(并为旧数据 增加默认值, 或者立即处理旧数据)。

Graph model

  适合用于解决many to many的数据关联关系。

  A graph consists of two kinds of objects: vertices (also known as nodes or entities) and edges (also known as relationships or arcs)

  data model:property graph model; triple-store model

  declarative query languages for graphs: Cypher, SPARQL, and Datalog

数据的存储与查询

  在这一部分,主要是讲从数据库的角度来看,如何存储数据(store the data),如何查询数据(give data back to user)。涉及到两种存储引擎: log-structured storage engines, and page-oriented storage engines such as B-trees.

  一个最简单的数据库:

  

  这两个命令组成了一个数据库需要的最基本的操作:存储数据(db_set),读取数据(db_get)。不难发现,db_set是非常高效的,但db_get性能会非常之差,尤其是db中拥有大量数据的时候。

  事实上,绝大多数数据库写入性能都很好,而为了提高读取效率,都会使用到索引(Index):

the general idea behind them is to keep some additional metadata on the side, which acts as a signpost and helps you to locate the data you want

  索引是从原始数据(primary data)派生而来的结构,其目的是加速查询(query),索引的添加删除并不会影响到原始数据。但索引并不是银弹:在加速查询的同时,也会影响到写入速度,即在写入(更新)原始数据的同时,也需要同步维护索引数据。

Hash Index

  前面的这个最简单的数据库,就是就是一个Log structure的例子,数据以append only的形式组织,即使是对同一个key的修改,也是添加一条新的数据记录。

  hash是最为常见的数据结构中,在绝大多数编程语言都有对应的实现。hash在通过key获取value时速度很快,因此也非常适合用在DB查询。具体而言,value是key在文件中的偏移,这样,在db_set的同时修改key对用的文件偏移,在db-get的时候先从hash index中通过key读取偏移位置,然后再从文件读取数据。

  hash index的优点在于以很简单的形式加速了查询,但缺点也很明显:hashindex是内存中的数据结构,因此需要内存足够大以容纳所有key-value对,另外hash index对于range query支持不太好。

SSTables and LSM-Trees

  在前面simplest db中 log-structured segment中的key是无序的,数据按写入顺序存储。而另外一种格式,Sorted String Table, or SSTable:key则是有序的(磁盘上有序),同一个key在一个SSTable中只会出现一次。

  SSTable具有优势:

  • segment merge很容易,即使超过内存空间,归并排序
  • 由于key有序,更容易查找:
  • 基于Sparse index,可以将两个key之间的record打包压缩有存储,节省磁盘和带宽

  sstable是数据在文件上的组织形式,显然不大可能直接通过移动数据来保证key的有序性。因此都是在内存中用memtable中排序,当memtable的数据量达到一定程度,在以sstable的形式写到文件。关于sstable,memtable,在之前的文章《典型分布式系统分析:Bigtable》有一些介绍。

BTree

  Btree是最为常用的索引结构,在关系型数据库以及大多数Nosql中都有广泛应用。如下图:

  

  Btree中的基本单元称之为page,一般来说大小为4KB,读写都是以page为单位。

  非叶子节点的page会有ref指向child page,这个ref有点像指针,只不过是在指向的是磁盘上的位置而不是内存地址。page的最大child page数目称之为branching factor(上图中branching factor为6),在存储引擎中,branching factor一般是好几百,因此,这个Btree深度只要三四层就足够了。

聚簇索引(clustered index)

  前面介绍hash index,LSM的sparse index的时候,key映射的都是数据在文件中的偏移(offset),在Btree中,value既可以是数据本身,又可以是数据的位置信息。如果value就是数据本身,那么称之为clustered index,聚簇索引。

  mysql常用的两个存储引擎Innodb,myisam都是用了Btree作为索引结构。但不同的是,Innodb的主索引(primary index)使用了聚簇索引,叶子节点的data域保存了完整的数据记录,如果还建立有辅助索引(secondary index),那么辅助索引的date域是主键的值;而对于myisam,不管是主索引还是辅助索引,data域都是数据记录的位置信息。

内存数据库

  In memory db也是使用非常广泛的一类数据库,如redis,memcache,内存数据库的数据维护在内存中,即使提供某种程度上的持久化(如redis),也还是属于内存数据库,因为数据的读操作完全在内存中进行,而磁盘仅仅是为了数据持久化。

  为什么In memory db 更快:核心不是因为不用读取磁盘(即使disk based storage也会缓存);而是不用为了持久化,而encoding in memory data structure。

Transaction Processing or Analytics?

  online transaction processing(OLTP)与online analytic processing (OLAP)具有显著的区别,如下表所示

  

  一般来说,数据库(不管是sql,还是nosql)既支持OLTP,又支持OLAP。但一般来说,线上数据库并不会同时服务OLTP与OLAP,因为OLAP一般是跨表、大量记录的查询与聚合,消耗很大,可能影响到正常的OLTP。

  因此有了为数据分析定制化的数据库--数据仓库(Data Warehousing),数据的仓库的数据通过Extract–Transform–Load (ETL)导入,如下图所示:

  

  数据分析又一个特点:一次分析可能只会使用到table中的很少的几列,为了减少从磁盘读取更少的数据、以及更好的压缩存储,Column-Oriented Storage是一个不错的选择。

数据序列化与数据演进

  数据有两种形态:

  •   内存中:称之为对象(object)或者数据结构( structure)
  •   网络或者文件中:二进制序列

  数据经常要在这两种形态之间转换。

in-memory representation to a byte sequence:encoding (serialization、marshalling), and the reverse is called decoding (parsing, deserialization, unmarshalling).

  在本文中,翻译为序列化与反序列化。

  应用在持续运营、迭代的过程中,代码和数据格式也会跟着发生变化。但代码的变更并不是一簇而就的,对于服务端应用,通常需要灰度升级(rolling upgrade),而客户端应用不能保证用户同时更新。因此,在一定的时间内,会存在新老代码、新老数据格式并存的问题。这就存在产生了兼容性问题.

  • Backward compatibility: Newer code can read data that was written by older code.
  • Forward compatibility:Older code can read data that was written by newer code.

  在本章中,讨论了几种数据序列化协议、各个协议兼容性问题,以及数据是如何在各个进程之间流动的。

语言内置的序列化方式

  大多数编程语言都天然支持内存数据与字节流的相互转换(即序列化与反序列化),如Java的java.io.Serializable, Ruby的Marshal , Python的pickle。但这些内置模块或多或少都有一些缺点:

  • 与特定编程语言绑定,限制了以后的演化
  • 安全性问题:
In order to restore data in the same object types, the decoding process needs to be able to instantiate arbitrary classes.
  • 一般不考虑向前兼容性或向后兼容性问题
  • 效率问题:包括速度与序列化后的size

跨语言的文本序列化协议 JSON XML

  Json和Xml是两种使用非常广泛的序列化协议,二者最大的特点在于跨语言、自描述、可读性好。Json经常用于http请求的参数传递。

  json和xml也有以下缺陷:

  • 对数字的encoding不太友好,会有歧义(XML不能区分number、digital string;JSON不能区分整数与浮点数)
  • 支持text string,但不支持binary string(sequences of bytes without a character encoding)。 所以经常需要额外使用base64先对binary string进行换换,这就是额外增加33%的空间(3Byte的binary string转化成4Byte的text string)

Binary Json

  JSON协议的二进制进化版本核心是为了使用更少的空间,包括 MessagePack, BSON, BJSON, UBJSON, BISON等,其中由于MongoDB采样了BSON作为序列化协议,使用比较广泛。

  除了更小的空间,Binary JSON还有以下优点

  • 区分整数浮点数
  • 支持binary string

  下面是一个内存对象,后文用来对比各种序列化协议的效率(编码后size)

{
    "userName": "Martin",
    "favoriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}

  在这里用python json模块来序列化:

>>> dd = json.dumps(d, separators=(',', ':'))
>>> dd
'{"userName":"Martin","favoriteNumber":1337,"interests":["daydreaming","hacking"]}'
>>> len(dd)
81

  在去除了空格的情况下需要81字节.

  而使用msgpack编码如下:

  

  只需要66字节,与json序列化后的内容对比,很容易发现哪里使用了更少的字节.

Thrift and Protocol Buffers

  binary json相关json而言,优化了空间,但幅度不是很大(81字节到66字节),原因在于,不管是JSON还是BSON都是自描述、自包含的(self-contained):在序列化结果中包含了fileld name。那么如果去掉field name,就能进一步压缩空间。

  Apache Thrift 和 Protocol Buffers就是这样的二进制序列化协议:通过使用格式描述文件(schema),在序列化后的字节流中,不再包含fieldname,而是使用与fieldname对应的filed tag.

  以protocol buffer为例,需要定义格式文件(.proto)

message Person {
    required string user_name = 1;
    optional int64 favorite_number = 2;
    repeated string interests = 3;
}

  然后就可以通过工具转化成响应语言的代码,在代码里面,就包含了fieldname与tag的映射,比如上面user_name就映射到了1。一般来说,数字比字符串更省空间。下面是protocol buffer序列化后的结果:

  

  可以看到总共只需要33字节,相比Magpack的66字节有巨大的提升。优化来自于一下几点:

  • 使用了field tag而不是fieldname, field tag还不到一个字节
  • filed tag 与 field type压缩到了一个字节里面
  • 使用了varint,用最少的字节标识一个整数

  Thrift两种格式:BinaryProtocol and CompactProtocol,后者采用了与Protocol Buffer类似的压缩策略

Field tags and schema evolution

  使用field tag之后,序列化后的数据就不在是自包含的,需要结合schema定义文件(产生的代码)来解读数据。那么在这种情况下如何保证兼容性呢。

  首先向前兼容不是什么问题,即使在新的数据定义中增加了字段,旧代码只用忽略这个字段就行了。当然,在新的数据定义中如果要删除字段,那么只能删除可选的(optional)字段,而且不能使用相同的field tag

  向后兼容性也好说,如果增加了字段,那么这个字段只要是可选的(optioanl),或者有默认值就行(default value)。

数据流动(DataFlow)

  数据从一个节点(进程)流向另一个节点,大约有以下几种形式

  • Via databases
  • Via service calls
  • Via asynchronous message passing

  对于database,需要注意的是:当新加filed之后,旧的application level code(DAO)读到新代码所写入的数据(包含new filed)的时候,会忽略掉new field,那么旧代码之后写入到数据库的时候,会不会覆盖掉new filed。

  service call有两种形式REST和RPC。

  message queue相比RPC优点:

  • 缓存(buffer),提高可用性
  • 可以重复投递消息,提高可靠性
  • 解耦合(无需知道消息消费者)
  • 多个消费者

总结

  第一章介绍了数据系统的衡量指标: reliability, scalability, and maintainability。

  第二章介绍了不同的数据模型与查询语言,包括relational mode, document mode, graph mode,需要解决的问题是如何表示many to one,many to many的数据关系,有两个有意思的概念:schema-on-read 、schema-on-write。

  第三章介绍了存储引擎:即数据是如何在磁盘上存储的,如何通过索引加速查询。内容包括Log structured,update-in-place;OLTP VS OLAP,dataware等。

  第四章介绍数据的序列化与反序列化,以及各种序列化协议的兼容性问题。包括JSON、BSON、Thrift&protobuffer、Arvo。

references

Designing Data-Intensive Applications

protocol buffers:encoding

猜你喜欢

转载自www.cnblogs.com/xybaby/p/9363943.html