問題描述
最近公司有個轉發服務,業務邏輯是從kafka消費到大量的數據,然後放入一個隊列中。之後用一個線程池,去消費這個隊列。
但是發現這四個線程消費隊列的地方又嚴重的延遲。特此想解決此問題。
貼代碼
- 往隊列裏push數據
void KafkaConsumer::msgConsume(RdKafka::Message* message, void* opaque)
{
KafkaConsumer::Data cData;
int errcode = message->err();
if (errcode == RdKafka::ERR__TIMED_OUT)
{
return;
}
else if (errcode == RdKafka::ERR_NO_ERROR) //消費數據,放入隊列
{
Data *pData=new Data;
pData->buffer.writeBlock(static_cast<const char*>(message->payload()),static_cast<int>(message->len())); // payload 裝載,載荷;這裏就是裏面的內容
//pData->topic = message->topic()->name();
pData->topic = message->topic_name(); // 注意這裏
pData->ipartition = message->partition();
_cMutex.lock();
_cDataQue.push(pData); // 放入隊列
_cMutex.unlock();
}
else if (RdKafka::ERR__PARTITION_EOF)
{
if (_exit_eof) _run = false;
}
else
{
LOG(INFO) << "kafkaConsumer--error: Consumer failed:" << message->errstr();
}
}
- 取隊列數據,處理篇
void KafkaConsumer::run(void* param)
{
int tag;
memcpy(&tag,¶m,sizeof(int));
while (1)
{
if (tag == CDATA)
{
if(_cDataQue.size() == 0) {
usleep(2000);
continue;
}
_cMutex.lock();
while(_cDataQue.size()>0) // 處理一次就都得處理完?!!
{
Data *pData = _cDataQue.pop(); // 隊列中取出
HandleMsg(pData); // 取數據和處理數據放一起?都在鎖裏?!!
SAFE_DELETE(pData);
}
_cMutex.unlock();
} else {
break;
}
}
}
代碼錯誤分析
_cMutex.lock();
while(_cDataQue.size()>0) // 處理一次就都得處理完?!!
{
Data *pData = _cDataQue.pop(); // 隊列中取出
HandleMsg(pData); // 取數據和處理數據放一起?都在鎖裏?!!
SAFE_DELETE(pData);
}
_cMutex.unlock();
線程在數據隊列_cDataQue中的數據時,先上鎖,然後不斷的循環取出隊列中的數據並處理。(取出數據 和處理數據在一起)
處理完每條數據之後delete.
當鎖定時的整個隊列中的數據處理完畢之後,解鎖。
定義幾個變量:
N : 鎖時隊列的長度
T1: pop 一條數據的時間
T2:HandleMsg 函數執行的時間
T3:push 一條數據的時間
此活動中的動作:
1. kafka消費到數據,鎖隊列,寫隊列,解鎖隊列。
2.數據解析線程,鎖隊列,讀數據,解鎖隊列,處理數據。
此時的處理方式,幾乎沒有發揮多線程的優勢,每次都是把鎖時的隊列的全部內容處理完。其他三個線程和生產數據的線程乾等
t = N * (T1+T2) 的時間。 若此時是程序剛啓動。kafka瞬間消費到很多數據成萬條的數據。 那麼t 將是一個很大的時間。且kafka消費到的數據還不能及時的存放如隊列中。於是就造成了延遲。
隱患就是:
1.根本沒發揮多線程的優勢和能力
2.若數據量大,取數據和處理數據放一起,導致鎖態佔用的時間很長,影響其他線程(往queue裏放數據的線程)幹活
3.其他線程競爭不到,乾等,浪費CPU時間。一個線程死幹活,處理不完,數據堆積。延遲。
改進方法
1. 將取數據的地方放在鎖的裏面,處理數據的地方放在鎖的外面。
2.每次取固定數量的nCount 個數據,放在一個容器裏。然後出鎖後慢慢處理。
同時,每次取固定數量的來處理,鎖佔用的時間是固定的,t = nCount * T1 .也就是說,其他3個處理線程和1個往queue裏塞數據的線程。最多隻等 3 * t 的時間就能拿到 queue的控制權,並對其進行操作。
而數據處理的時間 T2 與queue的操作(加鎖,讀取,塞入)沒有關係。
不過要控制nCount的值,太小。鎖的次數很頻繁; 太大,t 的時間會變大。
這樣多線程就用其來了。隊列應用也靈活了。處理能力大大提升。
void KafkaConsumer::run(void* param)
{
int tag;
memcpy(&tag,¶m,sizeof(int));
while (1){
if(_cDataQue.size() == 0) {
usleep(2000);
continue;
}
std::vector<Data*> vDatas;
_cMutex.lock();
while(_cDataQue.size()>0) {//上鎖的時間儘量短,爲其他線程爭取到和寫入線程騰出時間
Data *pData = _cDataQue.pop(); // 隊列中取出
vDatas.push_back(pData);
if(vDatas.size() > 10){ //這裏能限制這個長度 ,最多弄10條。處理快,節省時間。
break;
}
}
_cMutex.unlock();
// 將處理移除在鎖之外,慢慢處理這些數據,處理完釋放
for(vector<Data*>::iterator iter = vDatas.begin(); iter != vDatas.end(); ++iter){
Data *pData = *iter;
HandleMsg(pData);
SAFE_DELETE(pData);
}
}
}
用生活實例來解釋描述:
1.角色 : 大廚 (生產者) , 取餐檯/口(queue),包子(數據),顧客(消費處理線程)
2.動作:生產數據(push進queue),取出數據(pop出queue),佔住取餐檯(Lock),放開取餐檯(UNLock),吃包子(HandleMsg)
方案一
大廚們生產包子,鎖住取餐口,放下包子。然後顧客1 佔住取餐口,假如這裏有10個包子,他就取一個吃了,再去一個吃了,直到10個取完吃完才離開取餐口。此時,大廚沒法往裏放包子,其他三個顧客都乾等着。
方案二
大廚們生產包子,佔住取餐口,放下包子。顧客1,佔住取餐口,取了10個包子,去一邊吃去。顧客2 ,馬上來也取10個,然後一遍吃去。同理顧客3,4 也一樣。當然這裏只是理想情況,顧客1去完之後,也可能大廚又佔住取餐口,放了1w個包子。
關鍵是,每次取餐口被佔用的時間,之後顧客們取包子的時間。非常短。而且每個顧客取完之後就去一邊吃包子。同時大家可能都在吃包子,實現了多線程處理。
哈哈。就醬紫。