天天看點

設計資料密集型應用第一部分:資料系統的基石

  《Designing Data-Intensive Applications》這本書,今年在不同的地方都看到有推薦,簡單浏覽了一下内容,感覺還是值得一讀的。由于是英文,讀起來還是有點慢,最近讀完了本書的第一部分,寫篇文章記錄一下。本文主要是讀書摘要和筆記,也有一些自己的總結和思考。

  對我而言,看這本書的收獲在于擴寬了知識面,對一些以前隻是知其然的東西,知其是以然。另外,本書該出了大量詳實資料的連結,有助于對某一領域的進一步學習。

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

  于我而言,還是第一次聽說資料密集型(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

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.

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),隻要描述需要做什麼,而不需關心具體怎麼做,給使用者提供的是一個更簡潔的程式設計界面。

  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是一個不錯的選擇。

  資料有兩種形态:

  記憶體中:稱之為對象(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經常用于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)

  JSON協定的二進制進化版本核心是為了使用更少的空間,包括 MessagePack, BSON, BJSON, UBJSON, BISON等,其中由于MongoDB采樣了BSON作為序列化協定,使用比較廣泛。

  除了更小的空間,Binary JSON還有以下優點

區分整數浮點數

支援binary string

  下面是一個記憶體對象,後文用來對比各種序列化協定的效率(編碼後size)

  在這裡用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)

  然後就可以通過工具轉化成響應語言的代碼,在代碼裡面,就包含了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 tag之後,序列化後的資料就不在是自包含的,需要結合schema定義檔案(産生的代碼)來解讀資料。那麼在這種情況下如何保證相容性呢。

  首先向前相容不是什麼問題,即使在新的資料定義中增加了字段,舊代碼隻用忽略這個字段就行了。當然,在新的資料定義中如果要删除字段,那麼隻能删除可選的(optional)字段,而且不能使用相同的field tag

  向後相容性也好說,如果增加了字段,那麼這個字段隻要是可選的(optioanl),或者有預設值就行(default value)。

  資料從一個節點(程序)流向另一個節點,大約有以下幾種形式

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。

Designing Data-Intensive Applications

protocol buffers:encoding

本文版權歸作者xybaby(博文位址:http://www.cnblogs.com/xybaby/)所有,歡迎轉載和商用,請在文章頁面明顯位置給出原文連結并保留此段聲明,否則保留追究法律責任的權利,其他事項,可留言咨詢。

繼續閱讀