背景
线上的devops服务,通过websocket与k8s连接,来查看pod的日志。但是经常出现在打开日志查看后,devops服务占用CPU资源上升的情况。后来经过打印日志,才发现在websocket通信中,pod每产生一条日志,就会通过websocket向devops服务发送日志信息。而devops服务处理websocket消息的方式是每收到一条消息,就开一个线程来处理消息。可见在查看日志的整个过程中,有很多线程被创建以及被销毁,线程的频繁创建和频繁销毁也就导致CPU资源上升。
解决
想到的一种解决方式是减少消息发送次数,也就相当于减少了devops服务接收处理websocket消息次数,其创建线程数也就变少。
在本文,减少消息发送次数,就是把多条消息合并成一条消息,然后进行发送。我们把这个操作称之为防抖。
实现
防抖逻辑
首先要明确的一点是,每一次向websocket发送消息,并不是真的发出去,而是把消息发送到消息接收通道里面,然后用一个string变量把消息存储起来。等达到真正的消息发送时机,再把这个消息发送出去。
在本文里,防抖操作逻辑有三个要素:
- 单次发送消息最大等待时间(timeout):就是每一次发送消息的时间间隔。
比如第n次发送消息和第n+1次发送消息:
如果第n次发送消息后,在timeout时间内第n+1次发送消息始终没有触发,那么就真正的向websocket发送消息,然后重新计时 timeout 和 interval (interval作用下面说明);
如果第n次发送消息后,在timeout时间内第n+1次发送消息触发,那么不会向websocket发送消息,而是把消息存储起来,然后重新计时 timeout 和 interval (interval作用下面说明)。
- 总共最大等待时间(interval):
在要素1中,有一个问题:如果每次发消息的时间之差都小于timeout,那么消息内容就会被一直存储起来而发不出去。
所有就有了总共最大等待时间。该等待时间就是为解决每次发送消息的时间间隔小于timeout而不能把消息发送出去的问题。
假如每次发送消息时间间隔都小于timeout,那么interval就一定会触发,interval触发后,就向websocket发送消息,然后重新计时 timeout 和 interval (interval作用下面说明)
- 消息接收通道
用来接收/存储消息
在实际中,该方法将会以协程的方式运行
代码实现
func trafficAntiShake(conn *websocket.Conn, done chan bool, msgChan chan []byte, errors chan error) {
// 最大等待时间
interval := time.NewTimer(500 * time.Millisecond)
// 读取超时时间
timeout := time.NewTimer(TimeoutTime)
message := make([]byte, 0)
message = append(message, <-msgChan...)
for {
select {
// 如果done可读,退出协程
case <-done:
return
// 达到最大发送等待时间,立即发送,并重置定时器
case <-interval.C:
if len(message) == 0 {
if !timeout.Stop() {
select {
case <-timeout.C:
default:
}
}
timeout.Reset(TimeoutTime)
interval.Reset(IntervalTime)
continue
}
if err := conn.WriteMessage(websocket.BinaryMessage, message); err != nil {
errors <- err
return
}
message = []byte{}
if !timeout.Stop() {
select {
case <-timeout.C:
default:
}
}
timeout.Reset(TimeoutTime)
interval.Reset(IntervalTime)
// 读超时了,立即发送消息,并重置定时器
case <-timeout.C:
if len(message) == 0 {
if !interval.Stop() {
select {
case <-interval.C:
default:
}
}
timeout.Reset(TimeoutTime)
interval.Reset(IntervalTime)
continue
}
if err := conn.WriteMessage(websocket.BinaryMessage, message); err != nil {
errors <- err
return
}
message = []byte{}
if !interval.Stop() {
select {
case <-interval.C:
default:
}
}
timeout.Reset(TimeoutTime)
// 从websocket读到了消息,把消息写到message中,并重置定时器
case msg := <-msgChan:
message = append(message, msg...)
if !timeout.Stop() {
select {
case <-timeout.C:
default:
}
}
timeout.Reset(TimeoutTime)
}
}
}
上面的方法有4个参数:
- conn就是websocket的操作对象
- done用来控制这个方法的退出
- msgChan用来接收消息
- 接收该方法产生的错误消息。
该方法首先声明了3个变量:
- timeout: 消息与消息之间的最大时间间隔
- interval: 最大等待时间
- message: 存储消息
接着就是最主要的部分:select和case
- case <-done:这个case是读取done通道,比如其他协程想控制调用这个方法的协程退出
- case <-interval.C:该通道可读,表示要素2逻辑被触发。
首先判断是否有消息可发送,没有就先停止timeout计时器,然后重置计时器,最后循环。
有消息,那么发送消息后 清空message,停止timeout计时器,然后重置计时器,然后开始循环
- case <-timeout.C:该通道可读,表示要素1的第一种情况触发。
首先判断是否有消息可发送,没有就先停止interval计时器,然后重置计时器,最后循环。
有消息,那么发送消息后 清空message,停止interval计时器,然后重置计时器,然后开始循环 4. case <-msgChan:该通道可读,表示要素1的第二种情况触发。
读取通道中的消息,存储到message中,然后重置timeout计时器,最后循环。
总结
对于防抖逻辑的实现,主要用到了golang中的select/case、channel以及定时器。需要注意的地方就是消息发送的时间,在timeout计时结束或者interval计时结束后,都要发送消息。并且每一次计时结束后,都要重置定时器,否则会造成在后面的循环中,定时器不再生效
有关定时器的使用,可以参考这篇文章: 点击这里