《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。