第四章 資料編碼與演化

学学学习的菜鸟程序员發表於2024-11-10

應用程式總是增增改改,而修改程式大多數情況下也在修改儲存的資料

  • 資料格式發生改變時,需要程式碼更改:
    服務端:rolling update/ staged rollout,即灰度釋出
    客戶端:使用者可能相當長一段時間都不會升級軟體
  • 存在問題:新舊版本的程式碼,以及新舊版本資料格式在系統中同時共存。為了系統正常執行,需要保持雙向相容性:
    向後相容:新程式碼可以讀舊資料--可透過保留舊程式碼即可讀取舊資料
    向前相容:舊程式碼可以讀新資料--比較棘手,舊版程式需要忽略新版資料格式中新增的部分
  • 解決方案:透過幾種編碼資料的格式,對新舊程式碼資料需要共存的系統提供支援

編碼資料的格式

程式中至少使用兩種形式的資料

  • 在記憶體中,資料儲存在物件,結構體,列表,陣列,雜湊表,樹中。這些資料結構針對CPU的高校訪問和操作進行了最佳化(通常使用指標)
  • 如果要將資料寫入檔案,或透過網路傳送,則必須將其encoding為某種自包含的位元組序列(如,JSON文件),由於每個程序都有自己獨立的地址空間,一個程序中的只針對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同(第三章中記憶體資料庫更快的原因:省去了將記憶體資料結構編碼為磁碟資料結構的開銷)

語言特定的格式

程式語言對將記憶體物件編碼為位元組序列的支援:java中的java.io.Serializable,Python中的pickle,golang中的encoding/gob
存在問題:

  • 與特定的程式語言繫結
  • 為了恢復相同物件型別的資料,解碼過程需要例項化任意類的能力,這是安全問題的來源
  • 資料版本控制不方便,通常事後才考慮,忽略了前向後向相容性帶來的問題
  • 只適合臨時使用,例如java其java.io.Serializable效能較差

JSON, XML,和二進位制變體

  • XML和CSV不能區分數字和字串,JSON雖然能區分字串和數字,但不區分整數和浮點數,而且不能指定精度
  • 處理大量資料困難。大於\(2^{53}\)的整數不能再IEEE 754雙精度浮點數中精確表示
  • JSON 和 XML 對 unicode(人類可讀的文字)有很好的支援,但是不支援二進位制。透過 base64 繞過這個限制。
  • CSV沒有模式,應用程式需要定義每行和每列的含義,格式模糊

二進位制編碼

JSON比XML簡潔,但與二進位制格式相比還是太佔空間,現在有很多二進位制格式的 JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)。

JSON,二進位制編碼長度為66
{
    "userName": "Martin",
    "favoriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}

image

Thrift和Protocol Buffers

  • Protocol Buffers最初是在Google開發的,Thrift最初是在Facebook開發的,並且在2007~2008年都是開源的,都是二進位制編碼庫

  • Thrift和Protocol Buffers都需要一個模式來編碼任何資料

  • Thrift 有兩種不同的二進位制編碼格式,分別稱為 BinaryProtocol 和 CompactProtocol
    BinaryProtocol: 對上面的資訊編碼只需要59個位元組。每個欄位都有一個型別註釋(指示是一個字串,整數,列表等),還可以根據需要執行長度(字串的長度,列表中的iterm數)。透過欄位標籤取代欄位名
    image

    CompactProtocol: 語義上等同於BinaryProtocol,相同資訊打包只有34位元組。會將欄位型別和標籤號打包到單個位元組中,並使用可變長度證書來實現。將數字1337編碼成2個位元組,每個位元組的最高為表示是否還有更多位元組
    image

Thrift
struct Person {
    1: required string       userName,
    2: optional i64          favoriteNumber,
    3: optional list<string> interests
}
  • Protocol Buffers:與Thrift的CompactProtocol相似,可以將相同資訊打包到33位元組中
Protocol Buffers
message Person {
    required string user_name       = 1;
    optional int64  favorite_number = 2;
    repeated string interests       = 3;
}

image
欄位是否必需?對欄位如何編碼沒有影響(二進位制資料中沒有任何欄位指示是否需要欄位)

欄位標籤和模式演變

  • 欄位標記不能改變(否則導致現有的編碼資料無效),欄位名可以改變
  • 向前相容:新增新的欄位到架構,給每個欄位新的標籤號嗎。就的程式碼讀取新寫入的資料,如果標籤號碼不能識別,簡單忽略
  • 向後相容:新增的每個欄位必須是可選的或具有預設值的,否則之前的程式碼會檢查失敗
  • 刪除欄位:只能刪除可選欄位;不能再次使用相同的號碼標籤

資料型別和模式演變

  • 資料型別可以被改變:int32 升級 int64,新程式碼可以讀取舊程式碼寫入的資料(補0);但是舊程式碼不能解析新資料(int32 讀取 int64 會被截斷)
  • Protobuf 一個細節:沒有列表或陣列型別,只有 repeated,因此可以把可選欄位改為重複欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表。讀取新資料的舊程式碼只能那個看到列表的最後一個元素
  • Thrift 不能把更改為列表引數,但優點是可以巢狀列表

Avro

Avro是作為Hadoop的子專案在2009年開始的,因為Thrift不適合Hadoop的用例。也使用模式來指定正在編碼的資料的結構。 它有兩種模式語言:一種(Avro IDL)用於人工編輯,一種(基於JSON),更易於機器讀取。

Avro IDL編寫的示例模式
record Person {
    string                userName;
    union { null, long }  favoriteNumber = null;
    array<string>         interests;
}
等價的JSON表示
{
    "type": "record",
    "name": "Person",
    "fields": [
        {"name": "userName", "type": "string"},
        {"name": "favoriteNumber", "type": ["null", "long"], "default": null},
        {"name": "interests", "type": {"type": "array", "items": "string"}
    ] 
}
  • 沒有標籤號嗎,僅32個位元組長,是所有編碼中最近湊的。並且編碼只是連在一起的值,不能識別欄位和資料型別
    image
  • 必須按照順序遍歷欄位才能解碼
  • 編解碼必須使用完全相同的模式

Writer模式和Reader模式

  • Avro的關鍵思想是Writer模式和Reader模式不必是相同的 - 他們只需要相容
  • 資料讀取的時候,會對比 Writer模式 和 Reader模式 的欄位,然後就知道怎麼讀了
    image

模式演變規則

  • 為了保持相容性,只能新增或刪除具有預設值的欄位
  • 如果要新增一個沒有預設值的欄位,新的閱讀器將無法讀取舊作者寫的資料,所以會破壞向後相容性。如果要刪除沒有預設值的欄位,舊的閱讀器將無法讀取新作者寫入的資料,因此會打破相容性
  • Avro不包含任何標籤號碼,因此對動態生成的模式更友善。因為使用Thrift或者PB需要手動寫欄位標籤。而Avro在資料庫發生變化時,可以直接生成新的Avro模式,匯出資料,自動相容

模式的優點

  • Protocol Buffers,Thrift和Avro都使用模式來描述二進位制編碼格式,比XML和JSON簡單,也更支援更詳細的驗證規則
  • 基於模式的二進位制編碼相對於JSON,XML和CSV等文字資料格式的優點:
    1. 它們可以比各種“二進位制JSON”變體更緊湊,因為它們可以省略編碼資料中的欄位名稱
    2. 模式是一種有價值的文件形式,因為模式是解碼所必需的,所以可以確定它是最新的(而手動維護的文件可能很容易偏離現實)
    3. 維護一個模式的資料庫允許您在部署任何內容之前檢查模式更改的向前和向後相容性
    4. 對於靜態型別程式語言的使用者來說,從模式生成程式碼的能力是有用的,因為它可以在編譯時進行型別檢查

資料流的型別

資料庫中的資料流

  • 一般來說,會有多個程序訪問資料庫,可能會有某些程序執行較新程式碼、某些執行較舊的程式碼。因此資料庫也經常需要向前相容
  • 假設增加欄位,那麼較新的程式碼會寫入把該值寫入資料庫。而舊版本的程式碼將讀取記錄,理想的行為是舊程式碼保持領域完整
  • 用舊程式碼讀取並重新寫入資料庫時,有可能會導致資料丟失

在不同時間寫入不同的值

架構演變允許整個資料庫看起來好像是用單個模式編碼的,即使底層儲存可能包含用模式的各種歷史版本編碼的記錄

歸檔儲存

  • 建立資料庫快照,比如備份或者載入到資料倉儲:即使有不同時代的模式版本的混合,但通常使用最新模式進行編碼
  • 由於資料轉儲是一次寫入的,以後不變,所以 Avro 物件容器檔案等格式非常適合

服務中的資料流:REST與RPC

Web服務

當服務使用HTTP作為底層通訊協議時,可稱之為Web服務
REST

  • 它強調簡單的資料格式,使用URL來標識資源,並使用HTTP功能進行快取控制,身份驗證和內容型別協商
  • 與SOAP相比,REST已經越來越受歡迎,至少在跨組織服務整合的背景下,並經常與微服務相關
  • 根據REST原則設計的API稱為RESTful
    SOAP
  • SOAP是用於製作網路API請求的基於XML的協議
  • SOAP Web服務的API使用稱為Web服務描述語言(WSDL)的基於XML的語言來描述。 WSDL支援程式碼生成,客戶端可以使用本地類和方法呼叫(編碼為XML訊息並由框架再次解碼)訪問遠端服務
  • 儘管SOAP及其各種擴充套件表面上是標準化的,但是不同廠商的實現之間的互操作性往往會造成問題

RPC

RPC的缺陷

  • 本地函式呼叫是可預測的,並且成功或失敗僅取決於受您控制的引數。而網路請求是不可預知的
  • 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它可能會返回沒有結果。無法得知遠端服務的響應發生了什麼。
  • 如果響應丟失而出發請求充實,會導致該操作被多次執行,需要引入冪等操作
  • 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會變成問題。
  • 客戶端和服務端可以用不同的程式語言實現,RPC 框架必須把資料型別做翻譯,可能會出問題

訊息傳遞中的資料流

訊息代理

RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka等訊息佇列

  • 訊息代理通常不會執行任何特定的資料模型,訊息知識包含一些後設資料的位元組序列,可以用任何編碼格式

分散式的Actor框架

  • Actor模型是單個程序中併發的程式設計模型
  • 邏輯被封裝在actor中,而不是直接處理執行緒(以及競爭條件,鎖定和死鎖的相關問題)
  • 每個actor通常代表一個客戶或實體,它可能有一些本地狀態(不與其他任何角色共享),它透過傳送和接收非同步訊息與其他角色通訊。
  • 不保證訊息傳送:在某些錯誤情況下,訊息將丟失。
  • 由於每個角色一次只能處理一條訊息,因此不需要擔心執行緒,每個角色可以由框架獨立排程

分散式Actor框架

  • 在分散式Actor框架中,此程式設計模型用於跨多個節點伸縮應用程式。
  • 不管傳送方和接收方是在同一個節點上還是在不同的節點上,都使用相同的訊息傳遞機制。
  • 如果它們在不同的節點上,則該訊息被透明地編碼成位元組序列,透過網路傳送,並在另一側解碼

位置透明

  • 位置透明在actor模型中比在RPC中效果更好,因為actor模型已經假定訊息可能會丟失,即使在單個程序中也是如此。
  • 儘管網路上的延遲可能比同一個程序中的延遲更高,但是在使用actor模型時,本地和遠端通訊之間的基本不匹配是較少的

相關文章