电脑变云服务器 作者:张铭辉(希铭) 前言:WebSocket 的技术演进与时代价值 1.1 什么是 WebSocket? WebSocket 是一种基于 TCP 协议的全双工通信协议(RFC 6455[1]),通···
电脑变云服务器
作者:张铭辉(希铭)
前言:WebSocket 的技术演进与时代价值
1.1 什么是 WebSocket?
WebSocket 是一种基于 TCP 协议的全双工通信协议(RFC 6455[1]),通过一次 HTTP 握手即可建立持久化连接,实现客户端与服务端的双向数据传输。以下是一次 WebSocket 通信的示意图[2]:
可以看到,和 HTTP 不同,Client 会先向 Server 端基于 HTTP 协议发起一次握手请求,Server 返回响应握手成功。在这之后,已有的 TCP 连接会被升级为 WebSocket 连接,Client 和 Server 之间可以进行全双工通信。TCP 连接会一直持续到其中一侧认为需要关闭,且对方同意关闭之时。
为了更好理解后续 WebSocket 的全链路可观测方案,有必要对 WebSocket 的协议细节进行解读,本节剩余内容部分翻译 + 总结自 WebSocket Protocol[3]。
1.1.1 URI 格式与语法
和 HTTP 协议族非常类似,WebSocket 也有普通协议和他的安全版本,用 ws 和 wss 来区分,wss 的安全也采用 TLS 协议实现。由于 WebSocket 依赖 HTTP 协议进行握手,后续复用原 TCP 连接,故 WebSocket 默认的端口也是 80(ws)和 443(wss)。URI 整体的格式也和 HTTP 非常类似。
1.1.2 启动连接握手(基于 HTTP/1.1)
传统的 WebSocket 握手是一次典型的 HTTP 请求/响应。客户端主动发起一个 WebSocket 握手请求(一个特殊的 GET),如果服务器支持且允许使用 WebSocket 协议通信,则会返回一个 WebSocket 握手响应。WebSocket Connection 就建立起来了。
握手请求包含以下头:
如果服务端接受 WebSocket 协议,则发送一个 StatusCode 为 101 的响应:
HTTP/1.1101Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=响应包括:
HTTP/1.1 101 Switching Protocols:表示成功从 HTTP 升级到 WebSocket。Upgrade: websocket:确认协议升级。Connection: Upgrade:表示连接已升级。Sec-WebSocket-Accept:一个根据客户端的 Sec-WebSocket-Key 计算出的值,用于验证服务器理解了 WebSocket 握手请求。HTTP/2 与 HTTP/3 升级到 WebSocket 的过程有一些不同,但不是本文讨论的关键,在此不再赘述,欢迎阅读 WebSocket Protocol 原文[3]。
1.1.3 WebSocket 消息与数据帧
在握手完毕后,连接会被升级为 WebSocket 连接,此时客户端和服务端可以随时双向发送 WebSocket 消息(message),用来交换数据和指令。WebSocket 中的最小通信单元是数据帧,每个消息有可能由一个或者多个数据帧组成。
数据帧根据其用途可以分为以下三种类型:
文本帧:载荷为 UTF-8 编码的文本数据二进制帧:载荷为二进制数据控制帧:用于传递协议信号,如 ping、pong、close 帧等一个数据帧的数据组成如下图所示:
关于数据帧中每段数据的含义,如有兴趣,欢迎阅读 WebSocket Protocol 原文[3]。
1.1.4 关闭连接握手
当客户端或服务端某一方认为连接可以关闭时,会向对端发送一个关闭帧(是控制帧的一种),对端收到关闭帧后会尽快发送另一个关闭帧作为响应。发送完关闭帧后,该端不应该再发送任何数据帧。双方交换完关闭帧后,TCP 连接将关闭。
1.2 为什么用 WebSocket?
不难看出,WebSocket 核心特性体现在:
长连接保持:连接建立后持续存在,避免重复握手开销双向数据通道:客户端与服务端可随时发送数据帧(Text/Binary)低延迟特性:省去 HTTP 轮询的请求头传输成本消息分帧机制:支持超大数据量的分片传输(单帧最大 2^64 字节)与传统 HTTP 协议对比,WebSocket 在通信模式上实现了根本性突破:
这种协议特性使其成为大数据量下实时通信场景的首选方案。
1.3 AI 时代 WebSocket 协议的复兴
随着大模型技术的爆发,越来越多需要实时交互的场景开始出现,智能化赋予了 WebSocket 协议新的活力:
支持实时对话与交互的智能客服或机器人车载 AI 助手与云端模型实时交互自动翻译、智能识图的 AI 智能眼镜除实时性外,WebSocket 为有状态的连接,多轮对话的记忆保持、即时打断输出等功能也比传统的 HTTP 更加容易实现。到目前为止,主流的大模型提供商大多都提供了 WebSocket 的交互 API 及配套的 SDK,帮助用户更好地构建后端服务系统,例如:
OpenAI 支持基于 WebSocket 的 Realtime API[4]百炼大模型服务平台发布基于 WebSocket 的实时多模态交互协议[5]Google Gemini 支持基于 WebSocket 的 Live API[6]WebSocket 在赋能 AI 应用实时性的同时,也为应用系统的可观测性带来了很大的挑战。WebSocket 协议高度的灵活性与扩展性注定了它不能像 HTTP 和 gRPC 那样非常方便地做到全链路可观测,本文接下来将具体分析 WebSocket 场景下全链路可观测的实现痛点与解决方案。
WebSocket 全链路可观测痛点分析
2.1 协议灵活性带来的链路追踪困境
2.1.1 链路信息注入难
对于常规的 HTTP 调用,为了保证链路的连通性,调用方会在 HTTP headers 中额外添加一组用于承载链路上下文的键值对,确保被调用方在解析协议时能够正确地还原调用方的链路上下文,进而保证上下文可以被继续传递下去。图示是使用 W3C 链路追踪协议[7]时,链路上下文的 header 的一个具体示例:
而在 1.1.3 节我们了解到,一个 WebSocket 数据帧其实仅由数字节的控制位和数据载荷构成。除建立连接时握手以外,没有其他的机会传输 header 这些元数据。因此,传统 OpenTelemetry 的 W3C 链路上下文无法直接植入每个数据帧中。而在实际应用场景中,对于一次 WebSocket 连接,往往并不代表仅一次 WebSocket 调用,仅依赖建立连接时的 HTTP 请求与响应是远远不够的。同时,这也牵扯出第二个困难——Span 作用域界定模糊。
2.1.2 Span 作用域界定模糊
在可观测领域,我们一般把调用链路上一次关键的操作称为一条 Span(跨度)[8],一条调用链一般由一组树状结构的 Span 组成。在可观测前端的帮助下,我们可以把同属于一条调用链的 Span 召回,并根据父子关系(也就是调用关系)以及发生时间渲染为下图所示的瀑布图,以此来帮助我们了解一条链路发生的所有关键操作以及调用关系。
然而,在 WebSocket 场景下,操作粒度的定义可以非常灵活。如图所示,一个 Span 有可能对应一次 WebSocket 连接从开始到结束的全过程,也有可能对应每一次消息的收发,甚至也可以对应每一次数据帧的传递过程。对 Span 粒度定义的高度灵活也导致了链路上下文在注入与管理上也会有非常大的变化,这也增大了业务上落地的难度。
2.1.3 链路上下文的反向扩散问题
虽然我们根据 WebSocket 连接的发起方与接收方将两端分为了 Client 和 Server,但实际业务的处理过程是高度灵活的双向流,可能存在由 Server 侧发起请求,Client 进行处理的情况。例如,允许 Client 主动与 Server 建立连接并将自身服务注册给 Server 端,由 Server 发送消息来对 Client 进行回调。对于这种交互方式而言,消息生产方(调用方)是 Server,消费方(被调用方)是 Client,因此链路上下文应该由 Server 注入到消息中,由 Client 还原并进一步传递。
2.2 异步调用引发的断链危机
在 WebSocket 应用中,为了提高连接利用率,两端也常用异步的方式来解耦消息接收过程与处理过程,以下是一个典型的异步消息处理架构。在这个过程中,消息有可能会直接被提交到线程池,也有可能存放在一个进程内的队列,甚至直接写入 Redis 等外部存储。这种灵活多变的异步方式也给链路上下文的进程内透传带来了困难,非常容易出现断链问题。
基于 LoongSuite 的全链路观测最佳实践
3.1 方案基本原理
通过上两节的讨论,我们可以得到两个基本结论:
WebSocket 的用法相当灵活,链路追踪的实现很大程度上取决于业务实现,需要开发者自主实现一些扩展来保证链路完整性高频业务场景缺少一些最佳落地范式,导致自主实现链路追踪困难此外,由于 WebSocket 链路上也难免存在一些 NoSQL、HTTP 等其他类型的调用,依然需要无侵入探针来保证各种调用的串联,这就要求无侵入探针与自定义扩展产生的链路上下文可以很好地互通。LoongSuite 无侵入探针提供的基于 OpenTelemetry API 的扩展机制就是解决这些问题的最佳手段[9]。
3.1.1 OpenTelemetry API 与 LoongSuite 探针工作原理
OpenTelemetry API 是 OpenTelemetry 社区定义的可观测数据采集标准的重要组件之一[10],它定义了一整套可观测领域使用的 API 行为标准和功能说明,比如可观测数据创建、上下文管理/透传、数据上报等逻辑,并为许多语言提供了配套的 SDK 实现。使用者可以基于 API 与 SDK 比较容易地实现上下文的管理与透传。以下是使用 Tracer API 定义 Span 的示意:
privateintdoWork(){// 创建 spanSpan doWorkSpan = tracer.spanBuilder("doWork").startSpan();// 激活 span 所在上下文try(Scope scope = doWorkSpan.makeCurrent()) {intresult =0;for(inti =0; i <10; i++) { result += i; }returnresult; }finally{// 结束 spandoWorkSpan.end(); } }LoongSuite 探针是阿里云可观测团队基于 OpenTelemetry 探针构建的,面向 AI 应用的开源的进程内可观测采集组件。对于热门的开源组件,例如 LangChain、OpenAI SDK、Tomcat 等,LoongSuite 探针提供了丰富的预定义插桩实现。使用者不再需要基于 OpenTelemetry API 进行开发,只需要修改编译或运行时命令,探针就能把可观测数据创建、上下文管理/透传、数据上报等关键逻辑自动完成,从而达成无侵入可观测的目标。
LoongSuite 探针可以满足生产应用绝大多数场景下的可观测需求,但对于一些高度自定义的场景,如消息系统中的复杂消费过程、部分 MQTT 场景以及 WebSocket 通信场景,使用 OpenTelemetry API/SDK 添加自定义埋点则是弥补无侵入探针监控盲区的最优方案。
3.1.2 LoongSuite 探针与自定义扩展交互示意
呼叫中心云服务器
对于 Java、Golang 这类包管理相对严格(需要明确指定版本)的语言来说,探针与应用可能会存在版本不一致的依赖,比如 Jackson、gRPC 和 OpenTelemetry API/SDK 等等。为了避免依赖冲突,常采用 shadow 的方式进行依赖隔离。但这也会导致用户在使用 OpenTelemetry API 和 SDK 自主埋点的时候,产生的链路上下文并不能与探针内互通,进而导致调用链断裂。
OpenTelemetry 和 LoongSuite 探针同样采用代码增强机制保证了链路上下文的共享,具体整体示意如下:
探针和应用共用一套 API,API 自身保证向前兼容探针初始化时,会将初始化好的实例对象注册到 GlobalHolder,应用中自定义埋点时,直接从 API 中的 GlobalHolder 就可以获取到探针的实例对象对于 SDK 中定义的一些方法和静态的 API,如 Context、Baggage 等,通过代码增强的方式,跳过这些函数原本的调用,转而使用探针中对应的实现通过以上机制,LoongSuite 探针可以很好地和 OpenTelemetry API/SDK 创建的 Span 串联在一起,保证了链路的完整性。
3.2 WebSocket 分布式链路追踪最佳实践
了解了这几个组件,关键的问题是,我的应用应该怎么添加这些自定义的埋点呢?在 WebSocket 全链路的实现中,需要先根据业务诉求明确几个问题:
会话粒度问题:一次 WebSocket 连接对应一条 Trace 还是多条 Trace?
对应一条 Trace:一次 WebSocket 连接是为了完成一系列相关性强的操作,且持续时间一般仅在数分钟;对应多条 Trace:一次 WebSocket 连接会在建立完成后留存下来持续复用,持续时间可能持续几小时。调用建模问题:WebSocket 内部的数据传输过程能否建模为离散的请求与响应?
如果连接建立后只用于双方传递数据,则不需要为每条消息专门创建 Span,一个 Span 的生命周期应该对应双方传递消息的完整过程;如果连接建立后,一方发送消息,另一方处理消息并返回响应,则每组这类调用都可以创建一对父子 Span,对应的数据结构需要允许承载序列化后的链路上下文。应对以上几个不同场景,自定义埋点的实现推荐也会有所差异,接下来将分别展开介绍。
3.2.1 引入 OpenTelemetry API 依赖
探针对 API 的兼容为向前兼容,对于最新版本的 API 适配可能比较有限,生产环境中 API 包的版本不需要过新,基本 API 足够使用即可。
对于 Java 语言,建议在 pom.xml 中引入。API 文档:https://javadoc.io/doc/io.opentelemetry/opentelemetry-api/1.28.0/index.html
<dependency><groupId>io.opentelemetry
发表评论
最近发表
标签列表