说一下场景
线上总有反馈说从直播间掉线,然后测试开始压测,发现对于低端设备在我们业务中推流场景下只能到60,即会发生异常;
先说一下优化前的问题
- 业务逻辑层,在一个协程里顺序处理每一个 ws 接收者,eg:IM,Inner Notification,直播间..., 处理完后,才会开始开始下一条 ws 消息的处理;
- 对于 IM 的ws 消息,也会走进 直播间 ws 消息的处理,只是直播间 ws 解析 command 后,发现自己无需处理,才会return;
- 直播间有个特殊命令「简称 special cmd」确保用户始终和直播间保持链接,若 n 秒内未接收到 special cmd,则会执行退房处理. 问题是在第1条的描述中,ws 会有堆积,若之前的消息处理逻辑过多,则会导致这个 special cmd 始终无法处理;
- 在直播间中,用户会刷评论/送礼等 level 较低的消息,若设备低端,无法处理大量低 level 消息时,可做丢弃或用其他 coroutine 处理,不要影响直播间中处理主流程 ws 的coroutine;
- ws 中 jsonstring 重复解析问题,Gson 解析耗时问题众所周知,重复解析且解析方式问题,导致 CPU 占用增大 and 发热 and 耗时增多,进而降低设备 CPU 处理能力,恶性循环;
优化思路
- 不同场景只处理属于自己的 ws;
- 解决 ws 积压导致后面消息无法及时处理的问题;
- 对于直播间等重 ws 的业务场景,low_level 且对 UI 线程影响较大的 msg 单独 coroutine 处理,eg:评论,送礼「有动画」;
- Gson 解析中高频 key 解析优化 and 优化 Gson 解析使用方式;
优化实战
二次分发
- 目前所有的 ws 都在 totalFlow 中,需要做二次分发. 根据目前公司提供的后端数据,只能根据 ws 中携带的 command 做 filter 过滤;
- 对于少量且低频的 msg,eg:IM, InnerNotification 等封装为一个 otherFlow;
- 对于大量且重要的 msg,eg:直播间流程中的各 msg 封装为一个 liveFlow;
- 对于大量且可根据设备处理能力丢弃 or 延迟处理的msg,eg:评论,送礼 等封装为一个 highFreqFlow;
otherFlow, liveFlow, highFreqFlow 均为 MutableSharedFlow;
其中 extraBufferCapacity 分别为 5, 800, 300「5:基本不会有积压; 800:重要流程消息,为防止被丢弃设为 较高阀值,服务端目前最高并发貌似也不会超过 800; 300:评论,送礼都是可丢弃的,且评论区最高存储消息 count 为 150」;
onBufferOverflow 均为 BufferOverflow.DROP_OLDEST;
处理之后也解决了 special cmd 无法及时处理,导致退房的bug.
Gson 解析
对于刚需字段 command,初次解析后,在 ws 的 data 里增加一个 command 即可, so easy.
data class Broadcast(val message: JSONObject, val command: Int)
对于 Gson 解析,改之前写法如下
val a = jsonString.optJSONObject("A")?.toString() ?: ""
val b = jsonString.optJSONObject("B")?.toString() ?: ""
val aa = try {
GsonManager.gson().fromJson(a, object : TypeToken() {}.type)
} catch (e: Exception) {
User()
}
val bb = GsonManager.gson().fromJson(b, object : TypeToken() {}.type)
val c = jsonString.optLong("c")
val d = jsonString.optInt("d")
var e = jsonString.optString("e", "")
val f = jsonString.optBoolean("f", false)
val g = jsonString.optJSONObject("g")?.toString() ?: ""
val extra = try {
GsonManager.gson().fromJson(g, object : TypeToken() {}.type)
} catch (e: Throwable) {
null
}
改完后
val data = try {
GsonManager.gson().fromJson(jsonString, object : TypeToken() {}.type)
} catch (e: Throwable) {
return
}
data class AB(
@SerializedName("a")
var a: User,
@SerializedName("b")
var b: User,
@SerializedName("c")
var c: Long,
@SerializedName("d")
var d: Int,
@SerializedName("e")
var e: String,
@SerializedName("f")
var f: Boolean? = null,
@SerializedName("g")
var g: CCC? = null,
)
至于 Gson 解析耗时原理的话,自行查阅吧,我也不太熟...
其他优化
在对上面的 totalFlow 做初步解析的时候,用到了很多 filter 方法,每一次 filter 都是新建的一个 flow,尽量一次 filter 完成功能,如下
public inline fun Flow.filter(crossinline predicate: suspend (T) -> Boolean): Flow = transform { value ->
if (predicate(value)) return@transform emit(value)
}
//优化前如下:
flow.filter {
//**
}.map {
//***
}.filter {
//**
}.catch {
//**
}
//优化后如下:
flow.mapNotNull{
//**
}.catch {
//**
}
另外对于疯狂刷评论等操作,肯定会导致 评论区的UI 疯狂刷新,可以新建一个队列缓存 comment msg,每秒取 3-4 次,每次取的 msg count 根据设备 level 来定,减少 UI 绘制压力;
成果
单丛直播压测的角度来讲,对于低端设备,结论如下:
优化前 推流-评论-60 掉线;
注:以上数字为每秒发送聊天消息*条;
卡顿检测工具
说一下发现 gson 耗时的检测工具:
这版优化前,还有一版优化,当时发现的问题是大量 gson 解析发生在 UI thread, 看下图:
原因是: ws 接收的 coroutine dispatcher 是 Main.
不过也能发现 gson 耗时问题.
拓展
目前进一步的优化所用工具为 tencent matrix.