上周二晚上十点,我正准备睡觉,手机突然响了。

是运维同事打来的,说线上的AI数字人直播服务出问题了,有几个直播间的数字人突然不动了,嘴型和语音对不上,观众在弹幕里刷"卡了""假人不动了"。运营那边急得不行,因为这几个直播间正在带货,每停一分钟都在亏钱。

我二话不说,打开电脑,开始排查。这一查,就是一整夜。

这篇文章就来记录一下这次排查的过程,以及从中总结的经验和教训。做AI数字人直播的朋友,可能会遇到类似的问题,希望我的经历能给大家一些参考。

问题现象

先说说问题现象。

我们的AI数字人直播系统,大概的架构是这样的:用户在前端看直播,后端有一个数字人驱动服务,负责接收文本(来自LLM生成的话术),然后调用TTS生成语音,同时驱动数字人的模型,让数字人的嘴型、表情、动作和语音同步。视频流通过RTMP推到直播平台。

出问题的现象是:

  • 数字人的画面卡住了,不动了,但语音还在继续播放。
  • 有时候画面恢复了,但嘴型和语音不同步,慢了好几秒。
  • 不是所有直播间都出问题,只有几个同时在播的直播间出问题。
  • 重启服务之后能好一会儿,但过十几分钟又卡住了。
  • 服务器的CPU、内存、GPU使用率都不高,没有明显的资源瓶颈。

这些现象看起来很奇怪。如果是资源不够,应该所有直播间都卡;如果是代码bug,应该一直卡,不会重启之后好一会儿。这种间歇性的、部分直播间的问题,通常是某种资源泄漏或者死锁导致的。

第一步:看日志

排查问题的第一步,永远是看日志。

我先看了数字人驱动服务的日志,发现了一些异常。在出问题的时间段,日志里有大量的"frame queue full"警告,意思是帧队列满了。还有一些"audio buffer overflow"的错误,音频缓冲区溢出了。

帧队列满了,说明数字人渲染的速度跟不上,生成的视频帧堆积在队列里,来不及发送。音频缓冲区溢出,说明音频生成的速度比播放的速度快,音频数据堆积了。

这两个问题加在一起,说明系统的处理速度跟不上输入的速度。但奇怪的是,GPU和CPU的使用率都不高,说明不是算力不够,而是某个环节卡住了,导致数据堆积。

我又看了TTS服务的日志,发现TTS的响应时间突然变长了。平时TTS生成一句话的音频大概需要200毫秒,出问题的时候变成了2到3秒。而且TTS服务的队列里堆积了大量的请求。

看起来问题出在TTS服务上?但TTS服务的资源使用率也不高,而且单独压测TTS服务的时候,响应时间是正常的。

这就更奇怪了。

第二步:监控链路

单个服务的日志看不出问题,我开始看整个链路的监控。

我们用了分布式追踪系统,每个请求都有一个trace ID,可以看到请求在各个服务之间的传递情况。

我找了一个出问题的直播间的trace,发现了一个有趣的现象:LLM生成文本的速度很快,TTS生成音频的速度也正常,但数字人渲染服务的处理时间越来越长。

具体来说,数字人渲染服务处理第一帧只需要30毫秒,但处理到第100帧的时候,需要500毫秒,处理到第500帧的时候,需要2秒以上。而且这个时间是线性增长的,越处理越慢。

这就很明显了,是数字人渲染服务里有什么东西在累积,导致处理速度越来越慢。累积到一定程度,队列就满了,然后就卡住了。重启服务之后,累积的东西被清空了,所以又能正常工作一会儿,但过一会儿又累积起来了。

那到底是什么在累积呢?

第三步:内存分析

处理时间线性增长,最常见的原因是内存泄漏或者数据结构没有清理。

我用了Go的pprof工具(我们的数字人渲染服务是用Go写的),抓取了服务的内存快照和CPU profile。

内存快照显示,服务的堆内存在持续增长,从启动时的200MB,增长到了出问题时的2GB。增长最快的是一个叫"frameCache"的对象,里面存了大量的视频帧数据。

frameCache是我们做的一个帧缓存,用来缓存已经渲染好的视频帧,避免重复渲染。设计的时候,这个缓存是有大小限制的,最多存100帧,超过之后会淘汰最旧的帧。

但我看了代码之后发现,缓存的淘汰逻辑有bug。我们用的是一个简单的切片来存帧,每次新帧进来的时候,append到切片末尾。当切片长度超过100的时候,我们把第一个元素删掉。但Go的切片删除第一个元素,是用切片的切片操作(cache = cache[1:]),这种操作不会释放底层数组的内存。

也就是说,虽然逻辑上缓存只有100帧,但底层数组一直在增长,从100、200、500、1000……一直增长下去。而且每次删除第一个元素,都要把后面的元素往前移动,这个操作的时间复杂度是O(n),n越大,移动越慢。

这就解释了为什么处理时间线性增长,为什么内存持续增长,为什么重启之后能好一会儿。

找到原因了!

第四步:修复问题

找到原因之后,修复就简单了。

我把frameCache的实现从切片改成了环形缓冲区(ring buffer)。环形缓冲区用一个固定大小的数组和两个指针(读指针和写指针)来实现,写入的时候如果满了就覆盖最旧的数据,不需要移动元素,时间复杂度是O(1),也不会有内存泄漏。

改完之后,我本地测试了一下,处理时间稳定在30毫秒左右,内存也稳定在200MB左右,不再增长了。

但这时候已经是凌晨三点了,我不敢直接把代码发到线上。因为这是一个核心服务,万一改出问题,影响更大。我先在测试环境跑了一个小时的压测,确认没有问题之后,才灰度发布到线上。

灰度发布之后,我盯着监控看了半个小时,确认内存稳定、处理时间稳定、没有再出现帧队列满的问题,才松了一口气。

这时候已经是凌晨四点了。

第五步:复盘和总结

问题虽然修复了,但我觉得有必要做一个复盘,避免以后再出现类似的问题。

第一个教训是:缓存一定要用正确的数据结构。我们当时图省事,用切片实现了一个简单的缓存,没有考虑到Go切片的底层数组不会自动收缩的问题。如果用环形缓冲区或者LRU缓存库,就不会有这个问题。

第二个教训是:要有内存增长的监控和告警。我们的监控系统只监控了CPU和GPU的使用率,没有监控内存的增长趋势。如果有内存增长率的告警,这个问题在上线初期就能发现,不会等到线上出故障了才知道。

第三个教训是:压测要做长时间的。我们上线前做了压测,但只压了10分钟,内存泄漏的问题在短时间内看不出来。如果做一个小时以上的长时间压测,就能发现内存持续增长的问题。

第四个教训是:灰度发布很重要。这次修复的时候,我没有直接全量发布,而是先灰度,确认没问题再全量。虽然多花了一些时间,但保证了安全性。线上服务,稳定永远是第一位的。

第五个教训是:日志要打全。这次排查能这么快定位到问题,得益于我们的日志打得比较全,有帧队列满的警告,有音频溢出的错误。如果日志不全,排查时间会更长。

数字人直播的其他坑

除了这次遇到的内存泄漏问题,做AI数字人直播还有一些其他常见的坑,顺便也分享一下。

第一个坑是音视频同步。数字人的嘴型要和语音同步,这需要精确的时间戳对齐。如果TTS的响应时间波动大,或者渲染的帧率不稳定,就会出现嘴型和语音不同步的问题。我们的解决方案是用一个统一的时间轴,音频和视频都按照时间轴来播放,而不是各自播放。

第二个坑是LLM生成的稳定性。数字人直播的话术是LLM实时生成的,如果LLM生成太慢或者失败了,数字人就会没话说,出现冷场。我们做了几层保障:首先是话术预生成,提前生成几句话缓存着;其次是有兜底话术,LLM失败的时候播放兜底的话;最后是有背景音乐和动作,即使没话说,数字人也不会完全静止。

第三个坑是直播推流的稳定性。数字人渲染好的视频流,需要通过RTMP推到直播平台。如果网络不稳定,推流就会中断,直播间就会卡。我们做了推流的断线重连和本地缓存,网络恢复之后能继续推,不会中断直播。

第四个坑是多直播间的资源隔离。如果多个直播间共享一个GPU,一个直播间出问题(比如渲染了一个特别复杂的场景),可能会影响其他直播间。我们做了GPU资源的隔离和限制,每个直播间最多用多少GPU资源是固定的,不会互相影响。

第五个坑是内容安全。AI数字人直播的内容是实时生成的,如果生成了违规内容,直播间可能会被封禁。我们做了内容审核,LLM生成的话术先经过审核,没问题再送给TTS和数字人。虽然增加了一些延迟,但保证了内容安全。

排查线上问题的思路

最后,总结一下排查线上问题的一般思路,这次排查也是按照这个思路来的。

第一步,明确问题现象。先搞清楚到底出了什么问题,是所有用户都有问题还是部分用户,是一直有问题还是间歇性的,有没有什么规律。问题现象描述得越清楚,排查越有方向。

第二步,看日志和监控。日志是排查问题的第一手资料,先看错误日志,再看警告日志,最后看正常日志。监控能告诉你系统的整体状态,哪些指标异常,哪些服务有问题。

第三步,缩小范围。根据日志和监控的信息,把问题范围缩小到某个服务、某个模块、甚至某段代码。不要一上来就看所有代码,那样效率太低。

第四步,复现问题。如果能在测试环境复现问题,排查就容易多了。可以用相同的输入、相同的配置,在测试环境跑一遍,看能不能复现。复现之后,可以加日志、打断点、用工具分析,找到根本原因。

第五步,验证修复。找到原因之后,修复代码,然后在测试环境验证。确认修复有效,而且没有引入新的问题,再发布到线上。

第六步,复盘总结。问题修复之后,一定要做复盘。总结问题的根本原因、排查过程、修复方案,以及以后怎么避免类似问题。把经验沉淀下来,团队才能不断进步。

写在最后

这次线上故障,从晚上十点排查到凌晨四点,花了六个小时。虽然很累,但最终找到了问题的根本原因,也修复了,还总结了不少经验。

做线上服务,出bug是难免的。重要的不是不出bug,而是出了bug之后能不能快速定位、快速修复,以及能不能从bug中学习,避免以后再犯。

AI数字人直播是一个比较新的领域,涉及LLM、TTS、数字人渲染、视频推流等多个技术栈,系统比较复杂,容易出问题。但只要有完善的监控、详细的日志、规范的发布流程,大部分问题都是可以快速定位和修复的。

希望这篇文章能给做类似系统的朋友一些参考。如果你也有线上故障排查的经历,或者有数字人直播的经验,欢迎交流讨论。

最后,愿大家的线上服务永远稳定,永远不用半夜起来排查bug。