news 2026/1/1 18:53:10

Excalidraw如何处理大规模并发连接?后端架构剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Excalidraw如何处理大规模并发连接?后端架构剖析

Excalidraw 如何应对大规模并发?后端架构深度拆解

在远程协作日益成为常态的今天,团队对实时协同工具的需求早已超越简单的文档共享。开发者们需要的是能够即时响应、高度一致且无需繁琐配置的交互体验——而 Excalidraw 正是在这一背景下脱颖而出。

这款以“手绘风”著称的开源白板工具,看似轻巧简单,实则背后藏着一套精妙的后端机制,支撑着成百上千用户在同一画布上流畅协作。它没有强制登录、不依赖复杂账户体系,却能保证每个用户的操作几乎无延迟地同步到所有人屏幕上。这究竟是如何实现的?

关键就在于:如何在去中心化与强一致性之间找到平衡,在低门槛接入的同时维持高并发下的稳定性和数据准确。要解开这个谜题,我们必须深入其通信链路、状态同步逻辑和分布式部署设计。


实时通信的核心:WebSocket 的高效闭环

Excalidraw 的实时性根基,建立在 WebSocket 协议之上。相比传统的 HTTP 轮询或长轮询方式,WebSocket 提供了真正的双向、持久化连接,极大降低了通信开销。

当用户打开一个共享画布链接(如#room=abc123),前端会立即尝试通过加密通道wss://建立 WebSocket 连接。一旦握手成功,客户端与服务器之间就形成了一个全双工的数据通道。这意味着不仅客户端可以随时发送绘图动作,服务器也能主动推送更新,彻底摆脱了“拉取-等待”的被动模式。

整个流程非常紧凑:

  1. 用户添加一个矩形,前端将其封装为结构化消息(例如{ type: 'ADD_ELEMENT', id: 'rect-1', x: 100, y: 200 });
  2. 消息通过已建立的 WebSocket 发送至服务端;
  3. 服务端验证合法性后,将该操作广播给当前画布的所有其他在线成员;
  4. 各客户端接收并应用变更,本地画布随即刷新。

整个过程通常在 100ms 内完成,形成一个“操作 → 广播 → 渲染”的快速闭环。这种即时反馈让用户感觉像是在同一个物理白板前工作。

为什么不用 HTTP?
因为轮询带来的性能损耗太大。假设每秒轮询一次,每个请求都要经历 TCP 握手、TLS 加密、HTTP 头部传输等步骤,即使空载也会消耗大量资源。而 WebSocket 只需一次连接,后续通信几乎没有额外开销。更重要的是,小而频繁的绘图事件流非常适合 WebSocket 的帧结构——头部仅 2~14 字节,远低于 HTTP 的数百字节。

下面是一段典型的 Node.js +ws库的服务端实现片段:

const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8081 }); wss.on('connection', (ws, req) => { const canvasId = extractCanvasId(req.url); console.log(`User connected to canvas: ${canvasId}`); if (!global.canvasClients) global.canvasClients = {}; if (!global.canvasClients[canvasId]) global.canvasClients[canvasId] = new Set(); global.canvasClients[canvasId].add(ws); ws.on('message', (data) => { try { const message = JSON.parse(data); global.canvasClients[canvasId]?.forEach((client) => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); } }); } catch (err) { console.error("Invalid message received:", data); } }); ws.on('close', () => { global.canvasClients[canvasId]?.delete(ws); console.log(`User disconnected from canvas: ${canvasId}`); }); });

这段代码虽然简洁,但已经实现了核心的“房间式”广播模型:基于 URL 中的canvasId将连接归组,同一画布内的所有成员互为订阅者。任何人的操作都会被转发给其余人,构成了协作的基础骨架。

不过,这只是单机版本的理想情况。一旦系统需要扩展到多个服务器实例,问题就开始浮现了。


分布式挑战:跨节点状态同步如何破局?

设想这样一个场景:用户 A 连接到服务器实例 1,用户 B 和 C 连接到实例 2。此时 A 添加了一个新图形,他的操作只能被实例 1 广播给同节点的用户;B 和 C 根本收不到更新。

这就是典型的“孤岛效应”。解决办法不是让客户端绑定特定服务器(即 sticky session),而是引入一个所有实例都能访问的共享状态层 —— Redis 成为了最自然的选择。

Redis 不只是缓存,更是高性能的内存数据库和消息总线。它的 Pub/Sub 机制天然适合用于跨进程、跨机器的消息广播。我们将原本保存在本地内存中的客户端集合,升级为由 Redis 统一协调的状态系统。

具体工作流程如下:

  • 所有 WebSocket 实例都订阅同一个 Redis 频道,比如canvas-updates
  • 当某个实例收到本地用户的操作时,除了向本机连接广播外,还会将消息发布到 Redis;
  • 其他实例监听到该消息后,判断是否属于其所管理的画布,并将更新推送给对应的本地客户端;
  • 同时,使用 Redis Hash 存储画布与连接的映射关系(如canvas:abc123 → [sessionA, sessionB]),便于精准投递。

看一段增强版的跨实例广播实现:

const redis = require('redis'); const publisher = redis.createClient(); const subscriber = redis.createClient(); subscriber.subscribe('canvas-updates'); subscriber.on('message', (channel, message) => { if (channel === 'canvas-updates') { const { canvasId, operation } = JSON.parse(message); broadcastToCanvas(canvasId, operation); // 推送给本地连接 } }); function publishOperation(canvasId, operation) { const payload = { canvasId, operation }; publisher.publish('canvas-updates', JSON.stringify(payload)); broadcastToCanvas(canvasId, operation); // 同时本地广播 }

这套机制的关键优势在于:完全解耦了连接位置与数据传播路径。无论用户连到哪台服务器,只要操作进入 Redis 通道,就能触达全局。这也意味着负载均衡器可以自由分配连接,无需维持会话粘性,提升了系统的弹性和容错能力。

当然,这样的设计也带来了一些新的考量:

  • 消息去重:必须防止某条操作被多次处理。可以通过附加唯一操作 ID 或时间戳来识别重复消息;
  • 网络分区应对:若 Redis 暂时不可用,系统应具备降级能力,例如退回到单机模式运行,待恢复后再重新同步;
  • 权限控制延伸:可在 Redis 中附加元数据,如用户角色、读写权限等,为后续精细化管控打下基础。

数据一致性难题:OT 思想如何隐式落地?

尽管 Excalidraw 官方未明确说明其采用 Operational Transformation(OT)还是 CRDT,但从行为特征来看,其冲突处理机制明显带有 OT 的影子。

想象两个用户同时操作:
- 用户 A 删除了一个箭头;
- 用户 B 修改了同一个箭头的颜色。

这两个操作可能因网络延迟到达顺序不同。如果不加协调,最终结果可能是一个“带颜色的已删除元素”,显然不合理。OT 的核心思想就是通过对操作进行上下文变换,确保无论执行顺序如何,最终状态一致。

虽然 Excalidraw 的图形元素相对独立(不像文本编辑那样高度耦合),但在以下场景中仍需类似机制:

  • 元素层级调整(z-index 变更):多人拖动可能导致层级混乱;
  • 选择状态冲突:两人同时选中同一对象,界面反馈需统一;
  • 文本内容编辑:嵌入文本框时存在字符级并发风险;
  • ID 冲突预防:客户端生成的元素 ID 必须全局唯一或可合并。

实践中,Excalidraw 很可能采用了简化版的 OT 策略:

  • 每个操作携带足够上下文(如目标 ID、版本号、时间戳);
  • 服务端作为协调者,对接收到的操作进行排序与去重;
  • 若检测到潜在冲突(如对同一元素的并发修改),则根据预设规则(如先到先得、权重优先)进行合并;
  • 客户端收到广播后,按统一逻辑应用变更,避免各自为政。

这种方式比完整 OT 实现更轻量,又比“最后写入胜出”更安全。它牺牲了一定的理论完备性,换来了工程上的可维护性与低延迟表现。

值得一提的是,前端还会做一层本地暂存:当网络中断时,用户的操作会被缓存起来,待重连后批量重放。这种“乐观更新 + 异步校正”的策略进一步提升了用户体验,即便在网络波动下也能保持操作连续性。


整体架构图景:从浏览器到集群的完整链条

把上述组件拼接起来,我们可以还原出 Excalidraw 在生产环境中的典型部署形态:

[客户端浏览器] ↓ (HTTPS/WSS) [负载均衡器 (Nginx / ALB)] ↓ [WebSocket 服务器集群 (Node.js + ws)] ↙ ↘ [Redis Pub/Sub] [数据库 (可选)]

各层职责清晰:

  • 前端层:React 驱动的 SPA,负责捕捉鼠标轨迹、生成增量操作、渲染画布;
  • 接入层:Nginx 处理 TLS 终止、HTTP 升级为 WebSocket 请求,并路由到后端集群;
  • 逻辑层:多个 Node.js 实例承载 WebSocket 连接,执行消息验证与转发;
  • 共享层:Redis 支撑跨实例通信与状态共享,是横向扩展的关键枢纽;
  • 持久层:可选地将画布快照定期落盘至 PostgreSQL 或 S3 类存储,用于历史回溯与备份。

整个系统呈现出典型的分层架构风格:越往上越贴近用户交互,越往下越关注可靠性与扩展性。

实际工作流也非常直观:

  1. 用户访问https://excalidraw.com/#room=abc123
  2. 前端提取room参数,发起 WebSocket 连接;
  3. 后端解析canvasId,加入对应房间;
  4. 若存在历史状态,先下发快照进行初始化;
  5. 用户开始绘图,操作被打包为事件发送;
  6. 服务端通过“本地广播 + Redis 发布”双重路径扩散消息;
  7. 所有客户端接收并应用变更,视图保持同步。

这套流程之所以高效,是因为它把复杂的协作问题拆解成了几个可独立优化的模块:通信靠 WebSocket,扩展靠 Redis,一致性靠操作语义控制。每个部分都不追求极致复杂,而是强调组合后的整体稳定性。


工程智慧:简约背后的深思熟虑

Excalidraw 最令人钦佩的地方,并非用了多么前沿的技术,而是它在“够用”与“可靠”之间找到了绝佳平衡点。

它没有引入 Kafka 或 RabbitMQ 来做消息队列,而是直接利用 Redis Pub/Sub,降低运维成本;
它没有强推用户注册,而是通过 Room Token 实现匿名协作,提升易用性;
它没有追求端到端的 CRDT 实现,而是借助服务端协调简化客户端逻辑。

这些选择背后,是一种克制的工程哲学:用最小的技术负担,解决最真实的问题

当然,这也留下了一些可演进的空间:

  • 对于超大型协作场景(如千人级会议白板),可能需要引入分片机制,按画布维度拆分 Redis 流量;
  • 可探索压缩算法(如 Protocol Buffers)进一步减少消息体积,尤其适合移动端弱网环境;
  • 未来若支持权限分级(如只读观众、编辑成员),可在 JWT 中嵌入角色信息,结合 Redis 动态授权。

但无论如何演进,其核心理念不会改变:让用户专注于创作本身,而不是连接、登录或同步等待。


这种高度集成的设计思路,正在引领轻量级协作工具走向更可靠、更高效的新阶段。Excalidraw 不只是一个绘图工具,更是一套关于“如何构建实时系统的”生动教案。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/22 3:00:29

Excalidraw与Slack集成,消息通知及时送达

Excalidraw与Slack集成,消息通知及时送达 在远程协作日益成为常态的今天,团队沟通中的“信息断层”问题愈发突出。一个典型场景是:架构师花了半小时更新了系统设计图,却忘记通知同事;等到评审会议开始时,大…

作者头像 李华
网站建设 2025/12/22 2:58:52

37、PowerShell语言与环境及正则表达式全解析

PowerShell语言与环境及正则表达式全解析 1. PowerShell脚本结构与函数调用 在PowerShell脚本里,命令仅能访问已定义的函数。这常常让大型脚本难以理解,尤其是当脚本开头全是辅助函数时。为让脚本更清晰,可采用如下结构: function Main {(...)HelperFunction(...) } fu…

作者头像 李华
网站建设 2025/12/22 2:57:56

21、Windows 组策略全面解析

Windows 组策略全面解析 1. WQL 语句与 WMI 过滤器 1.1 WQL 语句形式 WQL 语句采用 Select 语句的形式,用于请求特定 WMI 类的所有实例,并为感兴趣的特定属性提供限定符。例如: Select * from Win32_OperatingSystem where Caption="Windows XP Professional"…

作者头像 李华
网站建设 2025/12/30 1:45:32

26、基于Active Directory实现只读域控制器(RODC)的安全部署与管理

基于Active Directory实现只读域控制器(RODC)的安全部署与管理 1. 引言 在分支机构部署域控制器(DC)时,传统的可写DC存在物理安全难以保障、网络带宽不佳导致登录时间长和资源访问效率低等问题。而只读域控制器(RODC)的出现为解决这些问题提供了有效的方案。 2. RODC…

作者头像 李华
网站建设 2025/12/22 2:57:44

27、活动目录安全设计与轻量级目录服务详解

活动目录安全设计与轻量级目录服务详解 1. 利用活动目录快照恢复对象 1.1 连接快照 LDAP 端口 使用 Ldp.exe 连接到之前将快照作为 LDAP 服务器公开时指定的快照 LDAP 端口。 1.2 浏览快照 像浏览任何实时域控制器(DC)一样浏览快照。若要停止 Dsamain,在命令提示符窗口…

作者头像 李华
网站建设 2025/12/22 2:57:40

32、服务器安全与补丁管理全攻略

服务器安全与补丁管理全攻略 在当今数字化的时代,服务器安全和软件补丁管理对于企业的稳定运行和数据安全至关重要。本文将详细介绍服务器角色安全保障、多角色服务器的相关问题,以及补丁管理的四个关键阶段。 服务器角色安全保障 在保障服务器角色安全时,我们需要先深入…

作者头像 李华