本文分享自华为云社区《提升网络协议服务器的定位能力》,作者:张俭。

近期,我再次涉足于协议服务器相关的工作领域,致力于定位并解决各种问题。简单总结一些心得给大家。如果想要定位出协议服务器的问题,那么这些能力可能至关重要。

注:我这里比较偏向协议本身的问题,不涉及一些通用的网络问题(如网络吞吐量上不去、响应时间长等等)

对CPU和内存的通用分析能力

首先,网络协议服务器本质上也是一个应用程序。因此,需要具备一些关于CPU和内存的通用分析能力。PU/内存火焰图,内存dump分析,锁分析,以及远程调试(研发态手段)这些手段都要具备。

日志和网络连接的关联

为了有效地定位网络问题,日志需要精确到毫秒级别。没有毫秒级别的精度,定位网络问题就会变得极其困难。所以golang的logrus默认只有秒级别,我觉得不太好,用rfc3339就很好。

在打印日志时,我们不能太过随意。例如,“connection lost”这样的日志,在调试阶段可能看似无大碍,但当真正的业务量和连接数大幅增加时,这种模糊的日志信息就会让人束手无策。

理想的日志至少应包含网络地址信息,这样我们可以根据网络地址和时间点来查阅日志。如果有抓包的话,那就更好了,可以从中获取大量信息。

当然,我们并不需要在所有的日志中都包含网络地址信息。例如,一旦完成了用户身份的鉴定,我们就可以打印用户的身份信息,这样更方便与后续的业务流程进行整合。如果需要查询网络地址信息,可以回溯到建立连接时的日志。举个🌰

2023-05-30 23:59:01.000 [INFO] 127.0.0.1:62345 connected
2023-05-30 23:59:02.000 [INFO] 127.0.0.1:62345 authed, username is Wolverine
2023-05-30 23:59:03.000 [INFO] Wolverine killed magneto

假设一条数据链上有大量的消息呢?在现代的网络环境中,一条TCP链接可以轻易达到5M bit/s以上的数据流。即使我们提供了时间点信息,仍然很难找到具有问题的报文(在同一秒内可能有上千条报文)。在这种情况下,就需要引入会话的ID信息。许多TCP协议会携带这种信息,换句话说,支持IO复用的协议都会有这种信息(比如MQTT的messageId,Kafka的correlationId等)。此类信息应该被正确地打印在日志中。

针对特征值的跟踪能力

你可能已经在调试日志中包含了非常详尽的信息,然而在实际环境中,这可能并没有太大用处。

原因是一旦全面开启debug日志,性能消耗会大幅增加。除非你的系统性能冗余极大,否则根本无法正常运行。

为此,我们可以提升debug的能力,针对特定的特征值开启debug,例如网络地址、mqtt的clientId、消息中间件的topic等。应用程序仅针对这些特征值打印详细的日志,这样的开销就相对较小,而且这种方法已经在生产环境中被我多次验证。

将网络报文与业务trace关联起来

在网络协议服务器中,我们需要将网络报文与业务trace关联起来。这种关联能力的实现可以大大提高我们定位业务端到端问题的效率和准确性。 理想情况下,我们应该能够根据网络报文来查找相关的业务trace,反之亦然,根据业务trace来查找对应的网络报文。但这些手段都需要业务端的配合,比如在报文中携带traceId,或者在业务trace中携带网络地址信息。

以mqtt协议为例,可以在payload中带上:

{
    "traceId": "xxxx",
    "data": "xxxx"
}

在这个例子中,traceId就是我们为业务trace设定的唯一标识符,而data则是实际的业务数据。通过在网络报文中携带这些信息,我们就可以轻松地将网络报文与其对应的业务trace关联起来。

然而,这种方法在研发和测试环境中实现相对容易,但在生产环境中可能会遇到更多的困难。首先,对于在网络报文中携带traceId这一做法,业界并未形成统一的规范和实践。这导致在生产环境,极难做到。

更具挑战性的是,如果你面对的是一个端到端的复杂系统,将traceId从系统的入口传递到出口可能会遇到许多难以预见的问题。例如系统不支持这类数据的专递,这就封死了这条路。

查看原始报文的能力

查看原始报文的能力极其重要,特别是在协议栈的实现尚不成熟的情况下。如果无法查看原始报文,定位问题就会变得非常困难。我曾说过:“如果拿到了原始报文,还是无法复现问题,那我们的研发能力在哪里?”虽然这句话可能有些极端,但它准确地强调了抓包的重要性。

我们可以从抓包看出网络的连通性、网络的延迟、网络的吞吐量、报文的格式、报文的正确性等等。如果途径了多个网元,那么是谁的错?(一般来说,看抓包,谁先发RST,就从谁身上找原因)

虽然抓包的命令比较简单tcpdump port 8080 -i eth0 -s0 -w my.pcap就抓了,但实际想做成,最大的阻力是这两个,TLS和复杂的现网环境。

在旧版本的TLS密钥交换算法下,只要有私钥和密码,就可以顺利解包,但现在的tls,都支持前向加密,什么叫前向加密呢?简单地来说,就是给你私钥和密码,你也解不出来。有tls debuginfo和ebpf能解决这两个问题,tls debug-info的原理是将密钥交换时的密钥输出持久化到某个地方,然后拿这个去解,实际很少见有人用这个方案。ebpf一需要linux内核高版本,同时还需要开启功能,安装kernel-debug-info,门槛也比较高。

现网环境,像抓包嗅探的这种工具,有时候可能是禁止上传的,或者即使能上传成功,也需要很长的时间。

也许我们可以通过“应用层抓包”来解决上述的问题,在网络层,我们支持受限的抓包能力,比如可以抓针对某个特征值(比如网络地址、messageId)的包,因为我们在应用层,可使用的过滤条件更多,更精细,输出到某个路径,这个报文的组装,完全在应用网络层,虽然看不到物理层的一些信息,但对于应用程序来说,除非我是做nat设备的,一般用不到这些信息。继续用这个报文来分析问题。实现应用层抓包,也要注意对内存的占用等等,不能因为这个功能,把整个进程搞崩溃。

应用层抓包的一些思考

抓包地点的选择

在应用层抓包,第一步就是确定抓包的地点。由于我们是在应用层进行操作,因此抓包地点一般位于应用程序与网络协议栈的交接处。例如,你可以在数据包刚被应用接收,还未被处理之前进行抓包,或者在数据包即将被应用发送出去,还未进入网络协议栈之前进行抓包。

过滤条件的设定

设定过滤条件是抓包的关键,因为在实际环境中,数据流量可能非常大,如果没有过滤条件,抓包的数据量可能会非常庞大,对应用和系统的性能产生影响。在应用层,我们可以设置更多更精细的过滤条件,如网络地址、端口、协议类型、特定的字段等。这些过滤条件可以帮助我们更精确地定位问题,减少无效的数据。

数据存储问题

将抓到的数据存储起来也是很重要的一步。可以选择将数据存储到内存或者硬盘。需要注意的是,如果选择存储到内存,要考虑到内存的大小,避免因为抓包数据过大导致内存溢出。如果选择存储到硬盘,要考虑到硬盘的读写速度和容量,避免因为抓包数据过大导致硬盘满载。

总结

本文首先阐述了网络协议服务器的一些问题定位能力,包括CPU内存分析能力、日志和网络连接的关联能力、针对特征值的跟踪能力,以及查看原始报文的能力,也讨论了将网络报文与业务trace有效关联的重要性和实现挑战。强调了抓包的重要性和对于解密TLS报文的挑战。为了解决网络层抓包遇到的困难,我们可以考虑应用层抓包方案。最后,我们讨论了应用层抓包的一些关键问题,包括抓包地点的选择、过滤条件的设定和数据存储问题。

点击关注,第一时间了解华为云新鲜技术~