《Designing Data-Intensive Applications》这本书,今年在不一样的地方都看到有推荐,简单浏览了一下内容,感受仍是值得一读的。因为是英文,读起来仍是有点慢,最近读完了本书的第一部分,写篇文章记录一下。本文主要是读书摘要和笔记,也有一些本身的总结和思考。html
对我而言,看这本书的收获在于扩宽了知识面,对一些之前只是知其然的东西,知其因此然。另外,本书该出了大量详实资料的连接,有助于对某一领域的进一步学习。java
本文地址:http://www.javashuo.com/article/p-xqxwmsvc-ho.htmlnode
于我而言,仍是第一次据说数据密集型(data-intensive)这个属于。以前在分析一个程序(软件)的时候,常常用到的CPU Bound、IO Bound之类的词汇。那么什么是data-intensive呢python
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.mysql
便是说,应用的核心挑战是数据:大量的数据,复杂、丰富多样的数据,快速变化的数据。每一个程序员或多或少都在于数据系统打交道,包括但不限于:database、message queues、 caches,、search indexes, frameworks for batch and stream processing。不一样的数据系统知足了不一样的应用需求,即便是同一种数据系统,如database,也有各类不一样的设计哲学与实现方案。git
固然,也许不少人并不直接从事数据系统的开发工做,但了解这些数据系统的工做原理是颇有益处的。当咱们了解了原理以后,能为咱们的应用需求选择最合适的数据系统,能解释系统的一些约束与现象,能将这些数据系统有效的组合起来,服务于应用。程序员
在DDIA这本书中,对这些数据系统有概要的介绍,而后是区分各自的优缺点与特性,而后分析这些特性是如何实现的。web
DDIA一书分为三部分,第一部分是数据系统的基石,一些基本的思想和组件;第二部分是分布式数据系统;第三部分是派生数据系统。本文介绍第一部分。redis
一个应用每每是由多个数据系统组合而来,包括但不限于:sql
• 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
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
人是不可靠的,尽可能自动化能减小悲剧的产生。
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服务,那么负载就是每秒的请求数,而性能就是系统每秒能处理的请求数目。
当负载增大的时候,有两种方式衡量性能:
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.
Make it easy for new engineers to understand the system
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),只要描述须要作什么,而不需关心具体怎么作,给用户提供的是一个更简洁的编程界面。
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(并为旧数据 增长默认值, 或者当即处理旧数据)。
适合用于解决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),索引的添加删除并不会影响到原始数据。但索引并非银弹:在加速查询的同时,也会影响到写入速度,即在写入(更新)原始数据的同时,也须要同步维护索引数据。
前面的这个最简单的数据库,就是就是一个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支持不太好。
在前面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是最为经常使用的索引结构,在关系型数据库以及大多数Nosql中都有普遍应用。以下图:
Btree中的基本单元称之为page,通常来讲大小为4KB,读写都是以page为单位。
非叶子节点的page会有ref指向child page,这个ref有点像指针,只不过是在指向的是磁盘上的位置而不是内存地址。page的最大child page数目称之为branching factor(上图中branching factor为6),在存储引擎中,branching factor通常是好几百,所以,这个Btree深度只要三四层就足够了。
前面介绍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。
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是一个不错的选择。
数据有两种形态:
数据常常要在这两种形态之间转换。
in-memory representation to a byte sequence:encoding (serialization、marshalling), and the reverse is called decoding (parsing, deserialization, unmarshalling).
在本文中,翻译为序列化与反序列化。
应用在持续运营、迭代的过程当中,代码和数据格式也会跟着发生变化。但代码的变动并非一簇而就的,对于服务端应用,一般须要灰度升级(rolling upgrade),而客户端应用不能保证用户同时更新。所以,在必定的时间内,会存在新老代码、新老数据格式并存的问题。这就存在产生了兼容性问题.
在本章中,讨论了几种数据序列化协议、各个协议兼容性问题,以及数据是如何在各个进程之间流动的。
大多数编程语言都自然支持内存数据与字节流的相互转换(即序列化与反序列化),如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.
Json和Xml是两种使用很是普遍的序列化协议,两者最大的特色在于跨语言、自描述、可读性好。Json常常用于http请求的参数传递。
json和xml也有如下缺陷:
JSON协议的二进制进化版本核心是为了使用更少的空间,包括 MessagePack, BSON, BJSON, UBJSON, BISON等,其中因为MongoDB采样了BSON做为序列化协议,使用比较普遍。
除了更小的空间,Binary JSON还有如下优势
下面是一个内存对象,后文用来对比各类序列化协议的效率(编码后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序列化后的内容对比,很容易发现哪里使用了更少的字节.
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字节有巨大的提高。优化来自于一下几点:
Thrift两种格式:BinaryProtocol and CompactProtocol,后者采用了与Protocol Buffer相似的压缩策略
使用field tag以后,序列化后的数据就不在是自包含的,须要结合schema定义文件(产生的代码)来解读数据。那么在这种状况下如何保证兼容性呢。
首先向前兼容不是什么问题,即便在新的数据定义中增长了字段,旧代码只用忽略这个字段就好了。固然,在新的数据定义中若是要删除字段,那么只能删除可选的(optional)字段,并且不能使用相同的field tag
向后兼容性也好说,若是增长了字段,那么这个字段只要是可选的(optioanl),或者有默认值就行(default value)。
数据从一个节点(进程)流向另外一个节点,大约有如下几种形式
对于database,须要注意的是:当新加filed以后,旧的application level code(DAO)读到新代码所写入的数据(包含new filed)的时候,会忽略掉new field,那么旧代码以后写入到数据库的时候,会不会覆盖掉new filed。
service call有两种形式REST和RPC。
message queue相比RPC优势:
第一章介绍了数据系统的衡量指标: 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。