COMSE项目技术笔记
1.2017/1/9 head成员变量与指针的选择问题
代码修改
修改前结构体定义:
typedef struct index_hash_value
{
uint32_t my_pos; // 用于锁定
uint32_t del_data_num; // 用于检查是否需要收缩
uint32_t use_data_num; // 用于检查是否需要收缩
uint32_t sum_node_num; // 用于遍历,仅递增
struct index_node head; // head计数为1
}INDEX_HASH_VALUE;
修改后结构体定义:
typedef struct index_hash_value
{
uint32_t my_pos; // 用于锁定
uint32_t del_data_num; // 用于检查是否需要收缩
uint32_t use_data_num; // 用于检查是否需要收缩
uint32_t sum_node_num; // 用于遍历,仅递增
struct index_node *head_p; // head指针,计数为1
}INDEX_HASH_VALUE;
问题原因:
当仅存在head和last节点时,发现last节点的prev_p和next_p指针与head不一致,导致段错误。
分析:
1.在shrink_index函数中:
1.2.生成新的hash_values
index_hash_value new_one_hash_value = {0};//初始化为0
index_hash_value * p_hash_value = &new_one_hash_value;
1.3.删除旧值并插入新值
{index_map.insert( std::pair<std::string,index_hash_value>(index_hash_key,new_one_hash_value) );}
当使用成员变量时,在index_map.insert过程中,value及其内部的head内存地址会发生改变,而新分配的节点new_one_hash_value中的last_node仍然指向局部变量new_one_hash_value中的head,导致后续遍历时出现问题。
总结:
指针隐式指向了局部变量,使用时导致段错误。
2.2017/1/10 代码审查
2.1 _hash_now_num 在shrink和clear时的递减问题
不能递减,因为insert时是递增,而clear和shrink不是从最后一个元素开始,如果递减会造成my_pos值重复的问题。
2.2 shrink中可能发生的内存泄漏
if (!hit_hash_key) {return false;}
这会导致新创建的2.generate new hash_values产生的内存不被释放吗? 不会,因为AutoLock_Mutex auto_lock0(&index_update_lock)确保了对index_map的更新操作都是串行化的,而之前已经有判断 if (query_out.size() <= 0 ) {return false;},所以hit_hash_key应该为true。
2.3代码审查要点
- shrink_index
- clear_all_index
- clear_index
- delete_index
- insert_index
- all_query_index
- cross_query_index
3.内存泄漏检查和多线程业务验证
3.1内存泄漏检查代码
int main()
{
while(1)
{
Index_Core idx_core(128,3);
for (int i = 1 ; i < 100;i++)
for (char j = 'a';j<='z';j++)
{
std::string str(i,j);
for (int k = 0 ;k < 1000;k++)
{idx_core.insert_index(str,k);}
printf("insert %s\n",str.c_str());
}
printf("==================\n");
for (int i = 1 ; i < 60;i++)
for (char j = 'a';j<='m';j++)
{
std::string str(i,j);
for (int k = i ;k < 300;k++)
{idx_core.delete_index(str,k);}
printf("delete %s\n",str.c_str());
}
for (int i = 1 ; i < 80;i++)
for (char j = 'a';j<='z';j++)
{
std::string str(i,j);
idx_core.shrink_index(str);
printf("shrink %s\n",str.c_str());
}
for (int i = 1 ; i < 90;i++)
for (char j = 'a';j<='z';j++)
{
std::string str(i,j);
idx_core.clear_index(str);
printf("clear %s\n",str.c_str());
}
fflush(stdout);
sleep(1);
}
}
结论:运行12小时后,内存使用RSS保持稳定。
3.2多线程业务验证
#define THREAD_NUM 1
Index_Core idx_core(8,64);
void *worker_thread(void *arg)
{
while(1)
{
#if 1
for (int i = 1 ; i < 15;i++)
for (char j = 'a';j<='z';j++)
{
std::vector<uint32_t> query_out;
std::string str(i,j);
idx_core.all_query_index(str,query_out);
if (query_out.size() > 0)
{
printf("%s[%d]:",str.c_str(),query_out.size());
for (int k = 0 ; k < query_out.size(); k++)
{printf("%d ",query_out[k]);}
printf(":%s\n",str.c_str());
}
}
#endif
fflush(stdout);
sleep(1);
}
}
int main(int argc,char *argv[])
{
pthread_t tid[THREAD_NUM];
int thread_ids[THREAD_NUM] = {0};
for (int i = 0; i < THREAD_NUM; i++)
{
thread_ids[i] = i;
if (pthread_create(&tid[i],NULL,&worker_thread,(void*)&thread_ids[i]) != 0)
{
fprintf(stderr,"线程创建失败\n");
return -1;
}
}
while(1)
{
for (int i = 1 ; i < 10;i++)
for (char j = 'a';j<='z';j++)
{
std::string str(i,j);
for (int k = 0 ;k < 30;k++)
{idx_core.insert_index(str,k);}
}
#if 1
for (int i = 1 ; i < 10;i++)
for (char j = 'a';j<='m';j++)
{
std::string str(i,j);
for (int k = (i+2);k < 10;k++)
{idx_core.delete_index(str,k);}
}
#endif
#if 1
for (int i = 1 ; i < 10;i++)
for (char j = 'a';j<='z';j++)
{
std::string str(i,j);
idx_core.shrink_index(str);
}
#endif
#if 1
for (int i = 3 ; i < 10;i++)
for (char j = 'a';j<='z';j++)
{
std::string str(i,j);
idx_core.clear_index(str);
}
#endif
}
for (int i = 0 ;i < THREAD_NUM; i++)
pthread_join(tid[i],NULL);
}
结论:运行至少12小时,打印的vector检查均为升序,且内存保持稳定。
4.单元测试
5.待办事项
1.使用type或view分离检索数据
2.请求时携带归并数量,返回时携带分词结果。
2017/3/3
发现jsoncpp的可能bug,内存被破坏
且jsoncpp占用太多内存
187字节数据循环生成100万次,占用1GB内存。
每个数据约耗1073字节。
2017/3/7
AWS EC2环境测试不同JSON库性能:
rapidjson:
- 空对象:96字节
- 示例JSON(100字节),10万次,内存占用440MB
jsoncpp:
- 空对象:40字节
- 示例JSON(100字节),50万次,内存占用700MB
cjson:
- 空对象:64字节
- 示例JSON(100字节),50万次,内存占用500MB
2017/3/14
解决了2017/3/3的bug,并非jsoncpp引起,而是之前调用std::sort导致。
这也解释了为什么sort函数之前jisuan_score时没问题,sort之后取breif会崩溃。
由于std::sort中重载<或>符号时,==情况必须返回false,否则会崩溃,详见 http://blog.sina.com.cn/s/blog_79d599dc01012m7l.html
导致程序其他部分问题,引发了jsoncpp的崩溃。
分析:
1.崩溃时除了注意发生位置,还要关注代码上下文或相关"代码环境"
2.问题除了直接产生,可能是其他问题影响了"代码环境",比如公用的堆、栈等
总结:
1.外部集成的库最好经过集成和压力测试,至少备注为潜在风险点
2.参考外界代码时,尽量不要更改重要代码,如果不知道什么是重要的,在完成功能的基础上,最好保持原样
2017/3/15
CLOSE_WAIT过多问题
原始Reactor模型:
[图示]
分析:
netstat观察到CLOSE_WAIT过多
1.CLOSE_WAIT是被动关闭时,服务端未调用close导致,说明服务端accept处理成功
2.服务端未关闭连接
原因:
线程1accept成功时向sock0发送了4字节的建连sock的fd
线程2异步调用接收sock1的信息,如果接收全是4字节没问题,但如果有一次收到的buffer < 4字节,会导致数据不以4字节对齐,接收到的fd不是有效的连接fd。
修改:
多个线程使用同一个listenfd,并accept(加互斥锁)之后的nfd,加入本线程的异步事件调度。
[图示]
线程自适应处理任务,任务处理不过来就accept不过来不建连,直接丢弃。
处理性能提高,减少了一个事件的注册和触发。
解决了CLOSE_WAIT堆积问题。
2017/3/16
编译时需要升级gcc到4.8版本
Makefile时要加上-lrt库,否则执行时找不到clock_time函数?
2017/3/17
1.每达到1万就输出一次max_index_num
2.load_from_file时内容出错时打印日志
待办事项:
max_index_num达到最大值时如何处理?
下一版本解决,dump出全部di,用另一个search_engine加载,然后双buffer交换。
1.日志格式化(完成于2017/5/4)
git:在service中生成log文件夹
2.增加日志记录状态:
ret1 = parse_in_json(); ret2 = get_out_json();
3.加快OR检索(完成于2017/5/4)
检查每个term重要性,即term倒排数量,超过阈值认为不重要,跳过不参与召回
4.search_engine.h中policy_compute_score函数声明和定义不一致(完成于2017/5/4)
5.load进去再dump数量不一致
1282887 dump.json.file 1284625 load.json.file
load时部分数据未加载进去
cat comse.log | grep "json parse error" | wc -l
1739
应该是load入口和服务的add入口不一致,add进去但load没进去
6.dump日志时格式有问题(完成于5/12)
把之前query的脏数据也输出了
recv和send发送数据时buff还有些脏数据未被清理
解决方式:在recv和send后面追加\0
7.max_ret_num确定实际返回数量(完成于2017/5/10)
8.生成md5的json内容,字段颠倒有无问题(完成于2017/5/16)
字段颠倒后用json_writer.write输出的字符串都是按字母序排列的
10.记录各过程耗时(完成于2017/5/12)
11.增加view或type过滤(在打分时匹配type打0分,在search_filter阶段过滤0分,type:0代表所有)
12.传递参数过多,考虑封装成struct传递?
13.All_time打日志记录时间时,增加query和召回数量打印
增加一个logid?
14.加快search过程
A:对每个term进行重要性分析,过滤掉重要性低的term?
B:在召回过程中实现文本相关性打分?不符合程序原始架构和策略一致性,召回、打分...
C:把词为key的term变成数字为key的term?
数字为key的term:测试1ms执行500次,使用时需记录term长度,使用map?
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
#include <string>
#include <sys/time.h>
#define TIMER(FUNC) { \
struct timeval prev_time,cur_time; \
int count_time = 0; \
gettimeofday(&prev_time,NULL); \
gettimeofday(&cur_time,NULL); \
while(1) \
{ \
gettimeofday(&cur_time,NULL); \
FUNC; \
count_time++; \
if (cur_time.tv_sec - prev_time.tv_sec >= 1) { \
prev_time = cur_time; \
printf("output timer count %d\n",count_time); \
fflush(stdout); \
count_time=0; \
} \
} \
}
int test_int_key(std::vector<int> & term_list,std::set<int> & term4se_set )
{
int or_terms_length = 0;
int or_terms_num = 0;
if ( term4se_set.size() > 0)
{
std::set<int>::iterator term_hash_it;
for (int i = 0 ; i < term_list.size();i++)
{
term_hash_it = term4se_set.find(term_list[i]);
if (term_hash_it != term4se_set.end())
{
or_terms_length += term_list[i];
or_terms_num++;
}
}
}
int ret = (or_terms_num * 1000 + or_terms_length);
return ret;
}
int main () {
std::vector<int> term_list;
term_list.push_back(1);
term_list.push_back(8);
term_list.push_back(16);
term_list.push_back(3012);
term_list.push_back(18);
term_list.push_back(7);
term_list.push_back(462);
term_list.push_back(129992);
std::set<int> term4se_set;
term4se_set.insert(1);
term4se_set.insert(16);
term4se_set.insert(7);
term4se_set.insert(8);
term4se_set.insert(1234);
term4se_set.insert(34111111);
TIMER(test_int_key(term_list,term4se_set));
return 0;
}
字符串为key的term:测试1ms执行311次
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
#include <string>
#include <sys/time.h>
#define TIMER(FUNC) { \
struct timeval prev_time,cur_time; \
int count_time = 0; \
gettimeofday(&prev_time,NULL); \
gettimeofday(&cur_time,NULL); \
while(1) \
{ \
gettimeofday(&cur_time,NULL); \
FUNC; \
count_time++; \
if (cur_time.tv_sec - prev_time.tv_sec >= 1) { \
prev_time = cur_time; \
printf("output timer count %d\n",count_time); \
fflush(stdout); \
count_time=0; \
} \
} \
}
int test_string_key(std::vector<std::string> & term_list,std::set<std::string> & term4se_set )
{
int or_terms_length = 0;
int or_terms_num = 0;
if ( term4se_set.size() > 0)
{
std::set<std::string>::iterator term_hash_it;
for (int i = 0 ; i < term_list.size();i++)
{
term_hash_it = term4se_set.find(term_list[i]);
if (term_hash_it != term4se_set.end())
{
or_terms_length += term_list[i].size();
or_terms_num++;
}
}
}
int ret = (or_terms_num * 1000 + or_terms_length);
return ret;
}
int main () {
std::vector<std::string> term_list;
term_list.push_back("book");
term_list.push_back("apple");
term_list.push_back("zoo");
term_list.push_back("school");
term_list.push_back("action");
term_list.push_back("gogogogo");
term_list.push_back("computer");
term_list.push_back("machine");
std::set<std::string> term4se_set;
term4se_set.insert("abcd");
term4se_set.insert("action");
term4se_set.insert("computer");
term4se_set.insert("book");
term4se_set.insert("zzzzzzzzzzz");
term4se_set.insert("abcd");
TIMER(test_string_key(term_list,term4se_set));
return 0;
}
已上线,效果提升约1倍,符合测试预期,但内存占用较大。
优化后性能数据:
[2017-06-08 15:42:51]cal_time recall=5488|compute=64173|sort=5711|package=373 search_mode=2:recall_num=17417:query=《三生三世十里桃花》终极预告花絮首发
[2017-06-08 15:42:51]Thread[a1bb7700] Client[20]All_time[accept2recv_event=211,recv=4,parse_recv=53,do_policy=84612,send=213]:Send=HTTP/1.1 200 OK
20913 video 20 0 2393m 2.0g 1748 S 0.0 2.2 4:00.27 comse_test
优化前性能数据:
[2017-06-08 16:56:17]cal_time recall=5489|compute=127615|sort=5730|package=463 search_mode=2:recall_num=17417:query=《三生三世十里桃花》终极预告花絮首发
[2017-06-08 16:56:16]Thread[4f0ae700] Client[18]All_time[accept2recv_event=144,recv=4,parse_recv=46,do_policy=145616,send=188]:Send=HTTP/1.1 200 OK
23150 video 20 0 2979m 2.6g 1712 S 0.0 2.8 3:13.20 comse_test
15.加快召回过程
使用bitmap节省空间,求交使用bit求交
16.title示例:
<我们的爱>第十二集看点
召回的关键词是"我们"
问题原因:一个作品名称的term由许多常见单词构成。
解决:term合并,n-gram,多层次检索,关键是各层次之间的打分体系如何融合。
16.ret_num 和max_ret_num 过大
导致中间数据占用过多内存,output_buff有限制不会吐出,浪费中间过程的内存和计算。
17.排序不稳定
展现数据偶现乱序
解决:使用stable_sort,缺点是时间会长些
18.优先级队列归并
两个有序数组,merge成一个有序数组
两个有序数组,merge成一个满足数量为n的有序数组
两个有序数组,merge成一个满足数量为n的,且第一个数组元素优先存在的有序数组
解决方案:
利用一个空有序数组,查找第二个有序数组中不存在第一个数组的元素,查找m个。
归并第一个有序数组和m,和正好为n。
19.大数据量add
add超过索引数量(如20万)时不会插入,那么数据的dict也不应该插入,md5值可以插入(方便以后去重)。
20.发现有时会出现abort
已解决,发现是jsoncpp解析错误导致
更改config.h的异常设置:
#ifndef JSON_USE_EXCEPTION
#define JSON_USE_EXCEPTION 0
#endif
注释jsoncpp的 assertions.h
//# define JSON_ASSERT(condition) assert(condition)
# define JSON_ASSERT(condition) ;
// The call to assert() will show the failure message in debug builds. In
// release builds we abort, for a core-dump or debugger.
#if 0
# define JSON_FAIL_MESSAGE(message) \\
{ \\
JSONCPPOSTRINGSTREAM oss; oss << message; \\
assert(false && oss.str().c_str()); \\
abort(); \\
}
#endif
# define JSON_FAIL_MESSAGE(message) \\
{ \\
; \\
}
21.解析非预期结构体时glibc报free错误(时间20171012)
//parse query
std::string query = json_in["cmd_info"]["title4se"].asString();
json_in["cmd_info"]["title4se"]如果是个object,返回的string会很大。
解决A:
对请求接口解析时,按要求的格式解析,不符合则返回错误
解决B:
封装一个接口,在调用asxxx时判断是否isxxx,比如
asString之前调用isString