ShowMeBug 核心技術內幕
ShowMeBug 是一款遠端面試工具,雙方可透過線上面試板進行實時溝通技術。所以關鍵技術要點在於 “實時同步”。關於實時同步,ShowMeBug 採用了以下技術。
OT 轉換演算法
本質上,ShowMeBug 核心就是多人同時線上實時編輯,難點即在這裡。因為網路原因,操作可能是非同步到達,丟失,與他人操作衝突。想想這就是個複雜的問題。
經過研究,最好使用者體驗的方式是 OT 轉換演算法。此演算法由 1989 年 C. Ellis 和 S. Gibbs 首次提出,目前像 quip,google docs 均用的此法。
OT 演算法允許使用者自由編輯任意行,包括衝突的操作也可以很好支援,不用鎖定。它的核心演算法如下:
文件的操作統一為以下三種型別的操作( Operation ):
- retain(n): 保持 n 個字元
- insert(s): 插入字串 s
- delete(s): 刪除字串 s
然後客戶端與服務端各記錄歷史版本,每次操作都經過一定的轉換後,推送給另一端。
轉換的核心是
S(o_1, o_2) = S(o_2, o_1)
換言之,把正在併發的操作進行轉換合併,形成新的操作,然後應用在歷史版本上,就可以實現無鎖化同步編輯。
下圖演示了對應的操作轉換過程。
https://daotestimg.dao42.com/ipic/070918.jpg
這個演算法的難點在於分散式的實現。客戶端服務端均需要記錄歷史,並且保持一定的序列。還要進行轉換演算法處理。
OT Rails 側的處理
本質上,這是一個基於 websocket 的演算法應用。所以我們沒有懷疑就選用 ActionCable 作為它的基礎。想著應該可以為我們節省大量的時間。實際上,我們錯了。
ActionCable 實際上與 NodeJS 版本的 socket.io 一樣,不具備任何可靠性的保障,做一些玩意性的聊天工具還可以,或者做訊息通知允許丟失甚至重複推送的弱場景是可以的。但像 OT 演算法這種強要求的就不可行了。
因為網路傳輸的不可靠性,我們必須按次序處理每一個操作。所以首先,我們實現了一個互斥鎖,也就是針對某一個面試板,準備一個鎖,同時只有一個操作可以進行操作。鎖採用了 Redis 鎖。實現如下:
def unlock_pad_history(lock_key)
logger.debug "[padable] unlock( lock_key: #{lock_key} )..."
old_lock_key = REDIS.get(_pad_lock_history_key)
if old_lock_key == lock_key
REDIS.del(_pad_lock_history_key)
else
log = "[FIXME] unlock_pad_history expired: lock_key=#{lock_key}, old_lock_key=#{old_lock_key}"
logger.error(log)
e = RuntimeError.new(log)
ExceptionNotifier.notify_exception(e, lock_key: lock_key, old_lock_key: old_lock_key)
end
end
# 為防止死鎖,鎖的時間為 5 分鐘,超時自動解鎖,但在 unlock 時會發出異常
def lock_pad_history(lock_key)
return REDIS.set(_pad_lock_history_key, lock_key, nx: true, ex: 5*60)
end
def wait_and_lock_pad_history(lock_key, retry_times = 200)
total_retry_times = retry_times
while ! lock_pad_history(lock_key)
sleep(0.05)
logger.debug '[padable] locked, waiting 50ms...'
retry_times-=1
raise "wait_and_lock_pad_history(in #{total_retry_times*0.1}s) #{lock_key} failed" if retry_times == 0
end
logger.debug "[padable] locking it(lock_key: #{lock_key})..."
end
服務端的併發控制完畢後,客戶端透過 “狀態佇列” 技術一個個排隊釋出操作記錄,核心如下:
class PadChannelSynchronized {
sendHistory(channel, history){
channel._sendHistory(history)
return new PadChannelAwaitingConfirm(history)
}
}
class PadChannelAwaitingConfirm {
constructor(outstanding_history) {
this.outstanding_history = outstanding_history
}
sendHistory(channel, history){
return new PadChannelAwaitingWithHistory(this.outstanding_history, history)
}
receiveHistory(channel, history){
return new PadChannelAwaitingConfirm(pair_history[0])
}
confirmHistory(channel, history) {
if(this.outstanding_history.client_id !== history.client_id){
throw new Error('confirmHistory error: client_id not equal')
}
return padChannelSynchronized
}
}
class PadChannelAwaitingWithHistory {
sendHistory(channel, history){
let newHistory = composeHistory(this.buffer_history, history)
return new PadChannelAwaitingWithHistory(this.outstanding_history, newHistory)
}
}
let padChannelSynchronized = new PadChannelSynchronized()
export default padChannelSynchronized
以上,便實現了一個排隊傳送的場景。
除此之外,我們設計了一個 PadChannel 用來專門管理與伺服器通訊的事件,維護歷史的狀態,處理斷線重傳,操作轉換與校驗。
定義自己的歷史(history) 協議
解決了編輯器協同的問題,才是真正的問題的開始。每次的 ” 程式碼執行”,“編輯”,“清空終端”,“首次同步” 都是需要記錄的歷史操作。於是,ShowMeBug 定義了以下協議:
# 包含以下: edit( 更新編輯器內容 ), run( 執行命令 ), clear( 清空終端 ), sync( 同步資料 )
# select( 游標 ), locate( 定位 )
# history 格式如下:
#
# {
# op: 'run' | 'edit' | 'select' | 'locate' | 'clear'
# id: id // 全域性唯一操作自增 id, 首次前端傳入時為 null, 服務端進行填充, 如果返回時為空, 則說明此 history 被拒絕寫入
# version: 'v1' // 資料格式版本
# prev_id: prev_id // JS 端生成 history 時上一次收到服務端的 id, 用於識別操作序列
# client_id: client_id // 客戶端生成的 history 的唯一標識
# creator_id: creator_id // 操作人的使用者 id, 為了安全首次前端傳入時為 null,由中臺填充
# event: { // op 為 edit 時, 記錄編輯器 OT 轉化後的資料, see here: https://github.com/Aaaaash/blog/issues/10
# [length, "string", length]
# // op 為 select 時, 記錄編輯器選擇區域 (包括游標)
# }
# snapshot: {
# editor_text: '' // 記錄當前編輯器內容快照, 此快照由服務端填充
# language_type: '' // 記錄當前編輯器的語言種類
# terminal_text: '' // 記錄當前終端快照
# }
# }
# created_at: created_at // 生成時間
值得說明的是,client_id 是客戶端生成的一個 8 位隨機碼,用於去重和與客戶端進行 ACK 確認。
id 是由服務端 Redis 生成的自增 id,客戶端會根據這個判斷歷史是否是新的。prev_id 用來操作轉換時記錄所需要進行轉換操作的歷史佇列。
event 是最重要的操作記錄,我們用 OT 的轉換資料進行儲存,如: [length, "string", length]
透過上述的設計,我們將面試板的所有操作細節涵蓋了,從而實現多人面試實時同步,面試題和麵試語言自動同步,操作回放等核心功能。
總結
篇幅限制,這裡只講到 ShowMeBug 的核心技術,更多的細節我們以後繼續分享。
ShowMeBug 目前承載了 3000 場面試記錄,成功支撐大量的實際面試官的面試,可靠性已得到進一步保障。這裡面有兩種重要程式設計正規化值得考慮:
- 如何在不可信鏈路上設計一種有序可靠的交付協議,定義清晰的協議資料和處理好非同步事件是關鍵。
- 如何平衡研發效率與穩定性之間的關係,比如實現的忙等鎖,允許一定原因的失敗,但處理好使用者的提示與重試。既高效完成了功能,又不影響到使用者體驗。
ShowMeBug( showmebug.com ) 讓你的技術面試更高效,助你找到你想要的候選人。
相關文章
- 讀《etcd 技術內幕》
- 簡述Spring技術內幕Spring
- TiDB 技術內幕 - 說儲存TiDB
- TiDB 技術內幕 - 談排程TiDB
- 「NGW」前端新技術賽場:Serverless SSR 技術內幕前端Server
- PostgreSQL技術內幕(七)索引掃描SQL索引
- [Mysql技術內幕]Innodb儲存引擎MySql儲存引擎
- Mysql技術內幕之InnoDB鎖探究MySql
- Mybatis技術內幕(1):Mybatis簡介MyBatis
- 深入剖析全鏈路灰度技術內幕
- Mybatis技術內幕(2.3.6):反射模組-WrapperMyBatis反射APP
- MySQL技術內幕之“日誌檔案”MySql
- Mybatis技術內幕(2.3.7):反射模組-TypeParameterResolverMyBatis反射
- Mybatis技術內幕(2.3.3):反射模組-InvokerMyBatis反射
- Mybatis技術內幕(2.3.4):反射模組-ObjectFactoryMyBatis反射Object
- Mybatis技術內幕(2.3.1):反射模組-ReflectorMyBatis反射
- PostgreSQL 技術內幕(五)Greenplum-Interconnect模組SQL
- 有贊搜尋系統的技術內幕
- 直播|PostgreSQL 技術內幕(五)Greenplum-Interconnect模組SQL
- 杭州銀行批量交易平臺(HZBAT)技術內幕BAT
- Mybatis技術內幕(2.3.5):反射模組-Property工具類MyBatis反射
- Mybatis技術內幕(2.4):資料來源模組MyBatis
- Spring技術內幕筆記(2):Spring MVC 與 WebSpring筆記MVCWeb
- ShowMeBug 2.0 重磅上線, BAT 都在使用的技術面試神器BAT面試
- 深入分析 Java Web 技術內幕讀後總結JavaWeb
- ## JavaSE核心技術Java
- SpringMVC核心技術SpringMVC
- AJAX核心技術
- Canvas 核心技術Canvas
- 技術內幕 | 阿里雲EMR StarRocks 極速資料湖分析阿里
- Spring技術內幕:設計理念和整體架構概述Spring架構
- Linux核心技術分析Linux
- Spring Boot核心技術Spring Boot
- Apache Flink核心技術Apache
- 不重視技術,何談掌握核心技術?
- 《spring技術內幕》讀書筆記3-AOP的實現Spring筆記
- 《深入分析JavaWeb技術內幕》之讀書筆記(篇三)JavaWeb筆記
- Transfer Learning 核心技術研修