編碼解碼,不得不再次搬出這個話題园欣, 編碼解碼就是程序語言之間交流的語言!
一旦你適應(yīng)了用粗暴的思考架設(shè)了你的設(shè)計峭拘, 再從頭來會很難俊庇,一是你沒有膽量推倒重來狮暑,二是沒有時間,三是你沒有耐心辉饱; 如果搬男,反過來從最粗陋的底層開始, 倒有可能雕琢出更加精致的上層建筑彭沼!
撇開一堆高大上的RPC缔逛、分布式等等上層建筑,如何編碼解碼是個最基礎(chǔ)的環(huán)節(jié)姓惑,在自己進(jìn)程內(nèi)褐奴,怎么交流都無障礙, 就好比大家都是中國人于毙,都在說漢語敦冬,基本能夠理解你說話的意思.
但凡需要跨進(jìn)程,就需要編碼解碼你的信息唯沮, 不管是同一個還是不同語言脖旱; 這就比方你需要和一個吼達(dá)不到的人交流,你需要電話介蛉,或者書信來傳遞萌庆。 這里首要做的事情就是, 把你要表達(dá)的意思币旧,轉(zhuǎn)換成書信践险、電流信號;如果轉(zhuǎn)換的言不達(dá)意吹菱,再好的通道也是雞同鴨講巍虫。
在計算機的世界里面你將收獲一堆亂碼! 解編碼毁葱,就是程序之間通訊的一道巴別塔:
程序之間的交流和任何信息的交流過程其實都一樣垫言, 上面說對面講話如同進(jìn)程內(nèi)通訊贰剥,其實還是不太精確倾剿,任何即使面對面的對話,其實都是有解編碼過程蚌成,你腦子里面想的電信號前痘,驅(qū)動你發(fā)音系統(tǒng)產(chǎn)生氣流的震動,通過空氣傳遞到對方担忧,對方的聽覺系統(tǒng)把震動轉(zhuǎn)換成電流信號芹缔,給大腦皮層理解。
概要
我們想要的解編碼協(xié)議需要滿足:
- 夠傻夠呆瓶盛, 字節(jié)碼打出來最欠,即使你用筆手動算也能算出來
- 跨語言示罗,小到內(nèi)部系統(tǒng)之間交流, 大到對外APP, 客戶端是個程序給個套路就可以解析
- 夠小芝硬,不能臃腫如XML 等之類蚜点,但是無需為了個把字節(jié)問題影響標(biāo)準(zhǔn) 1
- 夠快,夠快才能顯示我們的優(yōu)勢拌阴,同理不能為了快影響標(biāo)準(zhǔn) 1
滿足上面幾點的其實很難绍绘, 因為大部分不能滿足要求 一; 確實如此迟赃,這個猶如時間陪拘、成本、質(zhì)量鐵三角或者 CAP 一樣難以都滿足纤壁。
其實很簡單左刽, 所有的項目、工具酌媒、套路最終都是個權(quán)衡悠反,權(quán)衡和取舍!首先你不是什么消息都需要交流馍佑, 對交流的內(nèi)容和范圍做限制和取舍斋否, 可以很容易滿足上面的幾點。 大部分的IT 項目其實為了那 20% 甚至 5% 的功能拭荤,占據(jù)80%以上的代碼茵臭, 貢獻(xiàn)了絕大部分的BUG. 程序因人而生,但是公平的對待程序和自己可以省掉很多的麻煩舅世,需要和外圍通訊的消息需要滿足:
- 絕大部分是 primitive 類型: 這個好理解旦委, primitive 所有語言解釋套路一致,歧義少雏亚!
- 字段定長
- 如果實在需要個類似string 呢缨硝? 很簡單定長, 8/12/16/24/36/48 隨便你選罢低, 超過這個呢查辩,對不起,走其他通道网持。
- 少group 屬性
- group 也就是屬性是個列表(list)宜岛, 列表給整個消息帶來不確定性。于是少設(shè)計列表屬性功舀,能拆分就拆分萍倡, 不能拆分說明你業(yè)務(wù)模型有問題。
有人說這樣一分其實很簡單辟汰, 用個基本的分隔符列敲,就可以把消息編解碼出來阱佛, 你說對了,用個豎線( | ) 或者逗號(,)戴而,從筆者角度確實是個非常非常不錯的選擇瘫絮,緊湊、好懂填硕,非常完美的滿足上面的四點麦萤, 而且部分十分微小的消息這樣處理非常好用。
經(jīng)過如此篩選扁眯,最終可能上百個消息壮莹,最后剩下幾十個,最后帶group 的只有區(qū)區(qū)數(shù)的過來的幾個姻檀。 OK 了下面的選擇就比較寬泛 容易了命满。
FIX
最初對FIX 我是抗拒的,程序員容易犯的錯誤绣版,是對一些古老胶台、或者不時髦的東西有偏見,會一直以為最新的東西才是最NB, 最cool, 會了面子上過得去杂抽。但是這個協(xié)議是如此的簡單和健壯诈唬,已致他一直被金融界廣泛的使用,這里 有介紹缩麸,Key+Value+SOH(分隔符铸磅,字節(jié)碼 1 ) 其實就是這個協(xié)議的全部;什么狗日的 hack 幫我省那十來個字節(jié)沒有的杭朱, 比較完美符合標(biāo)準(zhǔn)阅仔; 對,其實我們在內(nèi)部也大膽的使用了FIX 作為通訊協(xié)議弧械, 如和 websocket 客戶端交流八酒, 我們也使用此風(fēng)格消息進(jìn)行交互。 看到 LOG 一竄竄漂亮的K=V 打印出來刃唐,有種賞心悅目的感覺羞迷!
SBE
SBE Simple Binary Encoding, 聽到這個詞就有種對眼的感覺, Simple 多謙虛直白的表達(dá)方式唁桩,中文的SB編碼又有點氣吞山河如虎的傲嬌闭树! 讀罷耸棒,果然簡單直接荒澡! 好帥真的一個協(xié)議。
SBE設(shè)計準(zhǔn)則, 我就不一一翻譯与殃,直接COPY Jdon 翻譯:
- Copy-Free:不采取中間緩沖单山,因為其在多次字節(jié)復(fù)制中有性能損耗碍现。采取直接與底層緩沖編碼與解碼,其限制是消息大小不能超過傳輸緩存的大小米奸,可以進(jìn)行碎片分段昼接。
- Native Type Mapping:copy-free的設(shè)計也通過將數(shù)據(jù)直接編碼為底層緩沖的原生類型中得到巨大好處,比如一個64位整數(shù)型能直接作為x86_64 MOV匯編指令被編碼進(jìn)入底層緩沖悴晰。通過這種原生的類型映射慢睡,一個字段能夠以類似高階語言如C++/Java中class類似和struct字段一樣高效率訪問。
- Allocation-Free:對象的分配會導(dǎo)致CPU緩存流失從而降低效率铡溪,這些被分配的對象后來得收集并刪除漂辐,對于Java是使用垃圾回收機制,導(dǎo)致stop-the-world暫停棕硫。SBE編碼采取flyweight 模式髓涯,基于底層緩沖的flyweight窗口直接對消息編碼與解碼,相應(yīng)類型的享元是基于消息頭部模板id選擇的哈扮。
- Streaming Access:現(xiàn)代內(nèi)存子系統(tǒng)已成為愈加復(fù)雜纬纪,該算法能夠?qū)π阅芎鸵恢滦杂泻艽髱椭瑢崿F(xiàn)最好的性能與最一致的延遲滑肉,這是以一種上升順序方式訪問內(nèi)存的方式實現(xiàn)的包各,也就是一種流。
- Word Aligned Access:當(dāng)word以非word大小邊界訪問時靶庙,多CPU架構(gòu)表現(xiàn)出顯著性能問題髓棋,一個word的起始地址應(yīng)該是其以字節(jié)為單位大小的倍數(shù),64位整數(shù)只能從字節(jié)地址能被8整除的地方開始惶洲,32位整數(shù)只能從被4整除的字節(jié)地址開始按声。
簡單粗暴, 是我個人對SBE 最直接的感受恬吕, 但是好用签则!
SBE 一個消息(Frame)包含哪些內(nèi)容:
在HTTP中涉及久了大家基本都記得一些這樣的圖形, 基本就是個header + payload 設(shè)計手法:
當(dāng)然SBE 屬于第六層(OSI)铐料, 也就是表示層(Presentation Layer)渐裂, 最終還需要上面的應(yīng)用也就是你的業(yè)務(wù)邏輯層來處理這些數(shù)據(jù)的。
Header 保持簡單钠惩, 只保留最最基本的信息柒凉, 或者你消息路由里面需要的信息, 避免解析整個消息流篓跛。
<composite name="messageHeader">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
這樣的header 占據(jù)8個字節(jié)的長度膝捞, 包括信息:
- blockLength: payload 的長度
- templateId: 消息模板, 哪種類型消息愧沟, 編碼解碼需要對象的解碼編碼器蔬咬。
- schemaId: 屬于哪個 schema 比如和FIX 哪個兼容
- version: 整個是你消息的版本鲤遥, 建議不適用,如果消息變了林艘,本人更趨向創(chuàng)建個新的類盖奈,即使加上個V1..N 也可以。
對于最簡單的消息:
<sbe:message description="Internal Time Price" id="916" name="Price" semanticType="Price">
<field id="1" name="product" presence="required" type="VARCHAR12"/>
<field id="2" name="bid" presence="required" type="double"/>
<field id="3" name="ask" presence="required" type="double"/>
<field id="4" name="timestamp" presence="required" type="int64"/>
</sbe:message>
product 是一個12長度的字符串狐援, 然后是一bid 和 ask 報價钢坦, 然后是一個epoch 時間戳。
可以看到這個對象的固定長度是 : 36+8 = 44 字節(jié)啥酱, 固定長度消息场钉。 比如對于bid 字段:
offset 為 header offset 也就是8個字節(jié) + 前面的 product 12, 也就是bid 偏移是 20
編碼
buffer.putDouble(offset + 12, value, java.nio.ByteOrder.LITTLE_ENDIAN);
解碼
>buffer.getDouble(offset + 12, java.nio.ByteOrder.LITTLE_ENDIAN)
是不是好舒服懈涛, 用筆都可以算出來逛万。一個協(xié)議簡單到這個程度也令人咂舌, 好就好在這個協(xié)議沒有完備的周邊設(shè)施批钠,個人理解這個是弊端也是好處宇植!弊端不是拿過來就用, 好處是給實現(xiàn)的人留下很多的空間埋心, 而很多的協(xié)議都是全家桶指郁, 周邊都幫你實現(xiàn), 一旦出現(xiàn)亂子拷呆, 你再去扒拉闲坎,非常的艱難。
一旦你適應(yīng)了用粗暴的思考架設(shè)了你的設(shè)計茬斧, 再從頭來很難腰懂,一是你沒有膽量推倒重來,二是沒有時間项秉,三是你沒有耐心绣溜; 如果,反過來從最粗陋的底層開始娄蔼, 倒有可能雕琢出更加精致的上層建筑怖喻!
實踐
上面說的基本就是SBE 全部, 和他的一個GIT版本, 其實這個項目不能說實現(xiàn)岁诉,因為SBE 和FIX 一樣太簡單了锚沸, 這個項目僅僅是個TOOL, 生成了最基礎(chǔ)的幾個語言的 stub, 和你 application 層美好的結(jié)合還需要自己去實現(xiàn)!
首先和你底層通訊框架集合起來(RPC, MQ 等)哗蜈, 然后和你的業(yè)務(wù)bean結(jié)合起來。 現(xiàn)在的RPC, 或者M(jìn)Q 框架都有plugin 進(jìn)自己的 codec 實現(xiàn)恬叹, 好說候生, 但是和自己業(yè)務(wù)bean 結(jié)合起來就比較麻煩.
你不能decoder/encoder 這些 stub 類往你業(yè)務(wù)層傳啊, 這個薄的一層非常的讓人苦惱须蜗。 必須要自動代碼生成一些偽類硅确, 有這些自動生成的類有:
- Adapter: 反射封裝encoder/decoder get/set 之類
- Enum: 映射, 這個比較暴力就switch case
- Stub: 分兩種其實如果你需要 Lazy 模式明肮,生成一個業(yè)務(wù)bean 偽類菱农,覆蓋get方法, 對group 這里需要特別對待柿估, 如果是 Eager 模式不需要使用偽類循未,直接 adapter 中 解碼new一個然后 get/set 上去。
- 反射秫舌,由于SBE 是基礎(chǔ)XML的妖, 自開發(fā)一套annotation, 方便一次掃描出來!
由于以前我們一直使用kryo序列化方式足陨,效率和壓縮比還是不錯嫂粟, 但是對其他語言不兼容,換成 SBE 后對于一些小消息墨缘,由于有頭(部分頭不止8字節(jié)長度星虹,Domain 消息有29字節(jié)長度),還有對于optional 字段其實 SBE 也要占空間镊讼, 所以消息的大小沒有多大的優(yōu)勢宽涌,但是解碼編碼還是飛快不少!
特別是如果你根據(jù)消息的ID 也就是 tempalteId 做路由可以說非车澹快护糖,記得你的 templateId 固定位置, 直接截取byte 相關(guān)位置就可以路由了嚼松。
好久沒有更新嫡良,過年前(2018新年)將會有幾大篇幅,分享一些非業(yè)務(wù)東西献酗, 下一篇將講監(jiān)控寝受,有的放矢,方能胸有成竹罕偎、控制自如很澄。
參考
- SBE 協(xié)議
- Simple Binary Encoding, a new ultra-fast marshalling API in C++, Java and .NET
- 簡單二進(jìn)制編碼(SBE)
- OSI model
- GIT Simple binary encoding
GoXTX 下一代交易平臺技術(shù)供應(yīng)商
GoXTX one-stop solution for neXT generation eXchange