故障描述
运维反馈说,redis 出现了 cpu100%+、QPS=14W+ 的情况,需要解决。
error.png
redis CPU=100%+ 的故障分析
问题是 cpu 打满了,所以猜测是计算量太多,同时,是 redis 和 mq 的消费者两端的进程都出现 cpu=100% 的情况,于是初步定为:
- mq 和 redis 计算量太多。
- mq 的消费者是多进程运行的,其中包含了 redis 的操作,必然是 mq 操作 redis 不当,才导致 mq 进程作为 redis 的 client,导致 redis 的 client 和 server 都 cpu=100%+。
定位 mq (redis client)中计算量大的程序片段
public function consumeToDo(AMQPMessage $message){
list($result, $role) = $this->handleMessage($message);
if($result){
while(0 == Yii::$app->redis->setnx('xxx_lock', 1)){
usleep(1000);
}
$counter = Yii::$app->redis->get('teacher_student_job_counter');
list($teacher_job_counter, $student_job_counter) = explode('.', $counter);
if($role == 'teacher'){
Yii::$app->redis->set('teacher_student_job_counter', --$teacher_job_counter.'.'.$student_job_counter);
}else{
Yii::$app->redis->set('teacher_student_job_counter', $teacher_job_counter.'.'.--$student_job_counter);
}
Yii::$app->redis->del('xxx_lock');
}
return $result;
}
分析上述代码:为避免多进程操作 redis.teacher_student_job_counter 造成的结果异常,增加了“锁保护”,具体是:在操作前,争抢 xxx_lock 锁,在操作完 redis.teacher_student_job_counter 后在释放 xxx_lock 锁。但存在的问题有:
- setnx 的 sleep 时间太短,导致单位时间内操作 redis 的次数太多:1000次/秒。
- xxx_lock 锁没有过期时间,如果程序中途异常没有释放,有可能导致永远无法获取到 xxx_lock 锁。
于是修复过后的代码为:
public function consumeToDo(AMQPMessage $message){
list($result, $role) = $this->handleMessage($message);
if($result){
while(0 == Yii::$app->redis->setnx('xxx_lock', 1)){
usleep(100000);
}
Yii::$app->redis->expire('xxx_lock', 10);
$counter = Yii::$app->redis->get('class_student_job_counter');
list($teacher_job_counter, $student_job_counter) = explode('.', $counter);
if($role == 'teacher'){
Yii::$app->redis->set('class_student_job_counter', --$teacher_job_counter.'.'.$student_job_counter);
}else{
Yii::$app->redis->set('class_student_job_counter', $teacher_job_counter.'.'.--$student_job_counter);
}
Yii::$app->redis->del('xxx_lock');
}
return $result;
}
redis QPS=10W+ 的故障分析
首先我让运维帮忙查看了下当前的 redis 连接:
connection.png
由图可以看出,redis 出现了很多 age>86400 的 setnx 连接,估计正是因为
xxx_lock 锁异常的问题导致的。于是将这些连接删除。
- 删除 age>86400 的 redis 连接
redis-cli -a password client list | awk '{split($5, agearr, "="); if(agearr[1]=="age" && int(agearr[2]) > 86400){split($2, addrarr, "="); print "redis-cli -a password client kill "addrarr[2];}}' | sh
- 删除正在执行 setnx 的 redis 连接
redis-cli -a password client list | grep cmd=setnx | awk '{split($2, addr, "="); print "redis-cli -a password client kill "addr[2];}' | sh
心得
在多进程使用 setnx 作为锁控制并发的时候,一定要控制抢锁的频率,避免 redis 的 client 和 server 的 cpu 过高。同时,应该为 setnx 锁设置合理的过期时间,避免在异常情况下造成 redis 连接时间太长、太多的问题(我们的场景是 mq 会根据队列长度,启动更多的进程来消费,于是连接越来越多)。










网友评论