ShowMeBug 核心技術內幕

ShowMeBug 是一款遠程面試工具,雙方可經過在線面試板進行實時溝通技術。因此關鍵技術要點在於 「實時同步」。關於實時同步,ShowMeBug 採用瞭如下技術。前端

OT 轉換算法

本質上,ShowMeBug 核心就是多人同時在線實時編輯,難點即在這裏。由於網絡緣由,操做多是異步到達,丟失,與他人操做衝突。想一想這就是個複雜的問題。 git

通過研究,最好用戶體驗的方式是 OT 轉換算法。此算法由 1989 年 C. Ellis 和 S. Gibbs 首次提出,目前像 quip,google docs 均用的此法。 github

OT 算法容許用戶自由編輯任意行,包括衝突的操做也能夠很好支持,不用鎖定。它的核心算法以下: web

文檔的操做統一爲如下三種類型的操做( Operation ):面試

  1. retain(n): 保持 n 個字符
  2. insert(s): 插入字符串 s
  3. delete(s): 刪除字符串 s

而後客戶端與服務端各記錄歷史版本,每次操做都通過必定的轉換後,推送給另外一端。 算法

轉換的核心是 編程

S(o_1, o_2) = S(o_2, o_1) 安全

換言之,把正在併發的操做進行轉換合併,造成新的操做,而後應用在歷史版本上,就能夠實現無鎖化同步編輯。 服務器

下圖演示了對應的操做轉換過程。 websocket

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/bl...
# [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 場面試記錄,成功支撐大量的實際面試官的面試,可靠性已獲得進一步保障。這裏面有兩種重要編程範式值得考慮:

  1. 如何在不可信鏈路上設計一種有序可靠的交付協議,定義清晰的協議數據和處理好異步事件是關鍵。
  2. 如何平衡研發效率與穩定性之間的關係,好比實現的忙等鎖,容許必定緣由的失敗,但處理好用戶的提示與重試。既高效完成了功能,又不影響到用戶體驗。

ShowMeBug( showmebug.com ) 讓你的技術面試更高效,助你找到你想要的候選人。

相關文章
相關標籤/搜索