ShowMeBug 核心技术内幕 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
yafeilee
V2EX    程序员

ShowMeBug 核心技术内幕

  •  3
     
  •   yafeilee
    PRO
    windy 2019-11-07 14:38:47 +08:00 6143 次点击
    这是一个创建于 2229 天前的主题,其中的信息可能已经有所发展或是发生改变。

    ShowMeBug 是一款远程面试工具,双方可通过在线面试板进行实时沟通技术。所以关键技术要点在于 “实时同步”。关于实时同步,ShowMeBug 采用了以下技术。

    OT 转换算法

    本质上,ShowMeBug 核心就是多人同时在线实时编辑,难点即在这里。因为网络原因,操作可能是异步到达,丢失,与他人操作冲突。想想这就是个复杂的问题。

    经过研究,最好用户体验的方式是 OT 转换算法。此算法由 1989 年 C. Ellis 和 S. Gibbs 首次提出,目前像 quip,google docs 均用的此法。

    OT 算法允许用户自由编辑任意行,包括冲突的操作也可以很好支持,不用锁定。它的核心算法如下:

    文档的操作统一为以下三种类型的操作( Operation ):

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

    然后客户端与服务端各记录历史版本,每次操作都经过一定的转换后,推送给另一端。

    转换的核心是

    S(o_1, o_2) = S(o_2, o_1)

    换言之,把正在并发的操作进行转换合并,形成新的操作,然后应用在历史版本上,就可以实现无锁化同步编辑。

    下图演示了对应的操作转换过程。

    这个算法的难点在于分布式的实现。客户端服务端均需要记录历史,并且保持一定的序列。还要进行转换算法处理。

    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 场面试记录,成功支撑大量的实际面试官的面试,可靠性已得到进一步保障。这里面有两种重要编程范式值得考虑:

    1. 如何在不可信链路上设计一种有序可靠的交付协议,定义清晰的协议数据和处理好异步事件是关键。
    2. 如何平衡研发效率与稳定性之间的关系,比如实现的忙等锁,允许一定原因的失败,但处理好用户的提示与重试。既高效完成了功能,又不影响到用户体验。

    ShowMeBug( https://www.showmebug.com ) 让你的技术面试更高效,助你找到你想要的候选人。

    31 条回复    2019-11-14 13:35:24 +08:00
    winkidney
        1
    winkidney  
       2019-11-07 15:31:49 +08:00   1
    没想到细节这么多 23333,赞一个。
        2
    yafeilee  
    OP
    PRO
       2019-11-07 16:13:53 +08:00
    这篇公开了 ShowMeBug 的核心技术点,但仍然有优化体验的空间,有兴趣的朋友也可以考虑加入我们团队哈。yafei#dao42.com
    rykka
        3
    rykka  
       2019-11-07 18:08:22 +08:00 via Android
    八错
    iugo
        4
    iugo  
       2019-11-07 18:16:24 +08:00
    两个特点:

    - 多人编辑
    - 网络延迟

    有点像游戏, 如果全部要提交到服务器端是否就会简单一些?

    客户端输入 A -> 服务器 -> 客户端显示 A

    不过如果网络不好, 体验的确差一些.
    Justin13
        5
    Justin13  
       2019-11-07 18:19:36 +08:00 via Android
    sharedb?
    yafeilee
        6
    yafeilee  
    OP
    PRO
       2019-11-07 18:38:53 +08:00
    @rykka 多谢支持

    @iugo 关键在于冲突检测,OT 算法体验算是最好了,因为不管网络如何,本地都可以去实时响应用户操作,之后网络恢复再同步到服务器即可。
    yafeilee
        7
    yafeilee  
    OP
    PRO
       2019-11-07 18:39:30 +08:00
    @Justin13 sharedb 研究过,无法解决冲突问题。
    aptx4689
        8
    aptx4689  
       2019-11-07 18:59:19 +08:00
    @yafeilee 做一下基本安全处理吧,python 没屏蔽危险库,可以做各种操作,还能反弹 shell
    yafeilee
        9
    yafeilee  
    OP
    PRO
       2019-11-07 19:19:15 +08:00
    @aptx4689 放心哈,都做了安全隔离,随便使用。
    yafeilee
        10
    yafeilee  
    OP
    PRO
       2019-11-07 19:20:11 +08:00
    @aptx4689 就是期待给大家一个自由的编程环境,可以考察任何问题。
    xiao0sun
        11
    xiao0sun  
       2019-11-07 19:55:39 +08:00 via iPhone
    你们会收集面试题吗
    zzl22100048
        12
    zzl22100048  
       2019-11-08 08:32:22 +08:00 via iPhone
    选择 ot 而不是 crdt 是有什么原因吗
    yafeilee
        13
    yafeilee  
    OP
    PRO
       2019-11-08 11:19:35 +08:00
    @zzl22100048 CRDT 是个好东西,OT 的诞生主要是处理文本共享,CRDT 是个分布式思想,更通用。https://stackoverflow.com/questions/26694359/differences-between-ot-and-crdt 这里有个解释。

    用 OT 目前来说主要是考虑实现轻便。
    yafeilee
        14
    yafeilee  
    OP
    PRO
       2019-11-08 11:20:30 +08:00
    @xiao0sun 在规划中,等全部搞定了就开放出来。
    chinese_zmm
        15
    chinese_zmm  
       2019-11-08 11:36:42 +08:00 via iPhone
    github 中的冲突解决也是用的这类算法吗?
    yafeilee
        16
    yafeilee  
    OP
    PRO
       2019-11-08 12:05:25 +08:00
    @chinese_zmm 那就不一样了,git 中冲突是留给用户的。它只提供 diff 对比。ShowMeBug 是用交换幂等自动处理冲突。
    guanhui07
        17
    guanhui07  
       2019-11-08 12:49:00 +08:00   1
    赞一个
    rina
        18
    rina  
       2019-11-08 18:21:14 +08:00
    非常厉害,赞!!!!
    kaneg
        19
    kaneg  
       2019-11-08 20:40:34 +08:00 via iPhone
    问楼主可能引发冲突的几个疑问:
    1 )在同一个位置,第一个人插入字符 A,同时另一个人插入 B,结果是 AB 都在那里还是只有一个 A 或 B ?

    2 )如果有人删除了一行,而另一个人同时在该行中间插入了一个字符,那结果应该是什么?是整行都没了还是只留一个字符在中间位置?
    Aruforce
        20
    Aruforce  
       2019-11-08 20:55:04 +08:00 via Android
    分布式锁…我还以为真的实现了并发操作…自动解决冲突……
    yexiaoxing
        21
    yexiaoxing  
       2019-11-08 21:27:55 +08:00
    之前在做 o365 online 的时候也有类似的功能,楼主分享的很好啊
    chenggiant
        22
    chenggiant  
       2019-11-08 22:32:40 +08:00
    有一个小疑问,面试感觉不是多人同时编辑的场景啊?
    wpzero
        23
    wpzero  
       2019-11-08 23:32:26 +08:00 via iPhone
    Codepad
    yafeilee
        24
    yafeilee  
    OP
    PRO
       2019-11-08 23:32:57 +08:00
    @kaneg 你可以去 ShowMeBug 里试一下,结果是随机的,主要看进入服务器的顺序。
    yafeilee
        25
    yafeilee  
    OP
    PRO
       2019-11-08 23:35:05 +08:00
    @Aruforce 不明白你的意思,我们技术实现上有锁,在服务端,对用户无锁,并发操作自动解决冲突。
    yafeilee
        26
    yafeilee  
    OP
    PRO
       2019-11-08 23:36:11 +08:00
    @yexiaoxing 多谢支持呀。
    yafeilee
        27
    yafeilee  
    OP
    PRO
       2019-11-08 23:39:19 +08:00
    @chenggiant 好的面试过程应该是互动的,所以同时讨论是有必要的。
    Aruforce
        28
    Aruforce  
       2019-11-09 09:51:05 +08:00 via Android
    @yafeilee 结合# 19 # 24 来看…你这是相当于有冲突了就 roll 点…roll 中哪个算哪个…修改谁后近服务器谁有理…
    这对于用户来说并不是什么自动解决冲突吧?

    冲突你能无锁解决的话…就不只是 dalao 了…
    即使用锁…你的一个面试一把锁…这锁的粒度也太大了…根据具体的数据结构应该可以弄分段锁…

    要我写的话,长链接加分布式锁就可以…,修改前先取锁,取锁成功…发到服务器做修改…完了推送到各客户端,完了再解锁…并发冲突直接就避免了…并不需要解决什么冲突…
    yafeilee
        29
    yafeilee  
    OP
    PRO
       2019-11-09 10:44:37 +08:00
    @Aruforce 你的理解有偏差,先看看 OT 算法吧兄弟。

    一个用户在删除整行,另一个用户在该行加字符,那当然要看谁先提到服务器而产生不同的结果呀。
    xiao0sun
        30
    xiao0sun  
       2019-11-14 02:49:08 +08:00 via iPhone
    @yafeilee 不太明白你的意思,你的初始客户是企业,你要收集企业的面试题开放给应聘者?还是建题库开放给所有合作的企业?这两个方向区别可大了,别走错了方向啊
    yafeilee
        31
    yafeilee  
    OP
    PRO
       2019-11-14 13:35:24 +08:00
    @xiao0sun 计划是后者,企业之间也可以开放共享,你的建议是?
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1127 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 32ms UTC 17:04 PVG 01:04 LAX 09:04 JFK 12:04
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86