本文為 Design Data Intensive Applications 的書摘 + 個人心得。
Avro
最後一個要來談的 binary encoding 方式是 Apache Avro ,閞始於 Hadoop 底下的子專案,它很明顯的跟 Thrift 和 Protocol Buffers 不同,Avro 一樣需要 schema 來定義欄位,Avro 能用 2 個 schema 語言,一個 (Avro IDL – Interface description language) 比較適合人讀,另一個 JSON 版本比較適合機器讀,我們一樣拿這份 JSON 資料來看看 Avro 的 2 種 schema 長什麼樣子,
{
"userName": "Martin",
"favoriteNumber": 1337,
"interests": ["daydreaming", "hacking"]
}
首先是 Avro IDL:
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}
再來是 JSON 版 schema:
{
"type": "record",
"name": "Person",
"fields": [
{"name": "userName", "type": "string"},
{"name": "favoriteNumber", "type": ["null", "long"], "default": null}, {"name": "interests", "type": {"type": "array", "items": "string"}}
]
}
這裡可以看到,Avro 沒有在 schema 中使用 欄位標籤 (field tags),它 encoding 後的檔案大小是最強大的 32 位元組,encoding 結果細節如下圖:
![figure_4-5](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_4-5.png)
Avro 沒有欄位標籤 (field tags) ,也沒有欄位型態,上面只看到說 長度 為何,然後後面就直接接資料了,所以同樣長度下你可以是數字或其他東西,然後 Avro 的數字也是用可變動長度儲存,這點跟 Thrift 和 Protocol Buffers 一樣,
既然沒有欄位標籤跟型態,所以我們如何跟 schema 做對應呢?我們只能用欄位 排序 告知 Avro 每個欄位的資料型態是什麼,也就是說在 decoding 資料時,必須使用 與寫資料時相同的 schema ,任何不匹配的欄位都會造成 decoding 時不正確。
Writer’s schema and Reader’s schema
這裡 Avro 把 encoding 跟 decoding 動作分成 Writer (寫入者) 和 Reader (讀取者),也就是寫入時會有 寫入者的 schema,讀取時也會有讀取者的 schema,因為應用軟體會進化、改變的關係, 2 邊的 schema 有時會不同,所以 Avro 是怎麼做到並存呢?
當 Avro 讀取資料時,它會同時看寫入者跟讀取者的 schema,然後從寫入者的 schema 翻譯資料到讀取者的 schema 裡,如下圖說明:
![figure_4-6](https://it-iron.s3-ap-northeast-1.amazonaws.com/2020/figure_4-6.png)
這裡可以看到,不會因為 2 邊欄位的順序不同造成問題,Avro 在解析時會一個欄位名接著一個欄位名解析,若寫入者 schema 有該欄而讀取者 schema 沒有,則 Avro 會忽略,若讀取者有新欄位而寫入者沒有,則會以讀取者 schema 所設定的方式給定預設值,整成成表格後如下:
寫入者的 schema | 讀取者的 schema | App Action |
---|---|---|
Y | N | 忽略 |
N | Y | 用讀取者 schema 定義的來給預設值 |
schema 進化的規則
向後兼容代表你寫入者的 schema 是新的,而讀取者的 schema 是舊的,反過來說,
向前兼容代表你寫入者的 schema 是舊的,而讀取者的 schema 是新的,
所以為了保持一致性,你可能只能 新增 或 刪除 帶有 預設值 的欄位,舉例來說,如果你 新增 一欄位是沒有預設值的,新讀取者將無法讀取舊寫入者的資料,這就破壞了向前兼容,再繼續說若你 刪除 了一個沒有預設值的欄位,則舊的讀取者將無法讀取新寫入者寫入的資料,這就破壞了向後兼容。
在某些程式語言,null 是可被允許的預設值,但它不是一個 Avro 用的欄位型態,你可以用另一種 Avro 支援的方式: union type 來讓預設值設為 null;舉例來說,有一欄位型態為 union { null, long, string}
,它說明了該欄位可以是 null、long 或 string,此時你就可以把預設值設定為 null,只要它是在 union 中的某個分支型態之一就 OK,
因此,Avro 沒有像 Thrift 和 Protocol Buffers 那樣的 required 和 optional 的標註,取而代之就是用 union 加預設值來實現。
其他的改變可能嗎?
改變欄位資料型態是沒問題的,Avro 會幫我們轉換型態,
改變欄位名稱也是沒問題但有點 tricky,讀取者的 schema 可以為改變後欄位名取 alias name (別名),之後它就可以對舊寫入者資料用 alias name 來解析,所以向後兼容沒問題,但向前兼容就 GG 了 (舊讀取者遇到不認識的 schema 會忽略),同理新增 union 裡頭的型態也是一樣的。