业务背景:MySQL数据库中有一份十万左右的域名白名单数据。一般不会变动。
业务需求:查询一个URL的域名是否在白名单中。
业务要求:占用内存小,高效,达到1s几百万。
以下性能测试环境均基于:
内存:16G
CPU:8 Intel(R) Xeon(R) CPU E5-14100 @ 2.80GHz
一、直接查询MySQL
没有做性能调查,但是肯定达不到业务的要求。
二、C++ set容器
将白名单数据全部读入set容器中,占用11M左右内存,内存达到性能要求,但是性能呢?
当时我循环遍历MySQL中的白名单数据进行匹配set容器中的数据,没有做记录模糊记得十万左右的数据大约1s中左右,这还没有考虑待查URL中包含二级、三级域名的情况,所以这种直接哈希也达不到业务的性能要求。
三、trie树
以上两种方法不行我就想到了trie树。trie树也称字典树,其效率很高,利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高,但是其高效率是以空间为代价的(起初不觉得会占用多大空间,但是......)。我将白名单数据从后往前存储到trie树中,正好符合域名匹配不要考虑二级域名、三级域名的问题,后面我会做一个优化处理。
借用百度的图片来说明一下trie树:
trie的基本性质:根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。都满足业务要求。
先看一下我初始定义的节点结构体:
typedef struct _whitelist_tree_node_ { uint8_t white_type; //是否是白名单,代表下一步执行的动作 struct _whitelist_tree_node_ *childs[128]; //子节点的指针 } whitelist_tree_node;
我这里在节点中没有定义保存节点值char变量,因为本业务用不到,因root节点不存内容,从root节点开始判断是否含有每个字符‘c’,只需判断该节点的childs[c] 是否等于NULL。white_type是我的一个属性,根据他我就可以不用判断二级、三级域名,在初始化树的时候赋值。他的值我用枚举定义的:
enum { WHITELIST_UNDONE = 0, //该白名单URL未结束 WHITELIST_CONTINUE, //白名单URL第一个字符,需要继续向前查找 WHITELIST_DONE //匹配白名单,不需继续查 };
写好算法之后,开始测试。初始化之后,我一看占用内存,我擦,我惊呆了了,800M左右,这还了得,赶紧优化。
肯定是childs[128]占用的内存。所以首先考虑域名的合法值,域名的合法字符为 '0-9'、'.'、'-'、'A-z',不区分大小写,这个好,因'-'和'z'区间跨度太大,所以将小写转为大写,降低区间跨度,又因最小值为'-'=45,所以这里操作的下标统一减去45操作。OK,整理后的结构体是:
typedef struct _whitelist_tree_node_ { uint8_t white_type; //是否是白名单,代表下一步执行的动作 struct _whitelist_tree_node_ *childs[46]; //子节点的指针 } whitelist_tree_node;
抱着希望再测试内存,我擦,还将进200M,不行,继续优化。
考虑这里用的静态数组,而且域名还算比较规则的,肯定浪费了不少的空间,并且我这白名单只需程序运行的时候初始化一次,中途不会修改树的结构,所以我考虑用动态申请数组来存储子节点,但是这样我怎么利用“只需判断该节点的childs[c] 是否等于NULL”这个快速的优点,难道循环?肯定不行,那么就得记录子节点中字母的插入的顺序,这个好记因子节点最多46个,也就是说子节点数组的下标最大45,用一个字节就可以表示了,OK,整理之后的节点结构体:
typedef struct _whitelist_tree_node_ { uint8_t white_type; //匹配白名单是否结束,代表下一步执行的动作 uint8_t child_count; //该节点的子节点个数 uint8_t child_order[MAX_CHILDS_NUM];//子节点字母在childs的位置(起始位置认为1) struct _whitelist_tree_node_ *childs;//子节点数组 } whitelist_tree_node;
这里多了一个child_count,就是为了好为child_order,并且记录动态数组的大小。
好了,再测试内存,43M,哈哈,总算可以用了,不容易啊。
那么性能呢,测试一下:
whitelist count: 99496
success num: 99496, failure num: 0
start: 1398762622.868052
stop: 1398762622.920456
time: 0.052404
待查10万URL全部是白名单的速率大约为0.05s,也是说1s约200万纯白名单的查询速度。
正常环境中肯定不全是白名单,测试1/10是白名单的100万数据,结果如下:
whitelist count: 99496
success num: 131922, failure num: 846934
start: 1398764581.688160
stop: 1398764581.956054
time: 0.267894
百万级别大约0.26s,也就是1s约400万,OK,达到性能要求。
综上,学任何一种算法都应该举一反三,结合自己的业务要求考虑算法会更好。下面是我完整的算法代码。或者下载附件查看,代码环境Linux。不对的地方请指正,谢谢。
头文件:
#define MIN_ASCII_VALUE 45 #define MAX_CHILDS_NUM 46 enum { WHITELIST_UNDONE = 0, //该白名单URL未结束 WHITELIST_CONTINUE, //白名单URL第一个字符,需要继续向前查找 WHITELIST_DONE //匹配白名单,不需继续查 }; typedef struct _whitelist_tree_node_ { uint8_t white_type; //匹配白名单是否结束,代表下一步执行的动作 uint8_t child_count; //该节点的子节点个数 uint8_t child_order[MAX_CHILDS_NUM]; //子节点字母在childs的位置(起始位置认为1) struct _whitelist_tree_node_ *childs; //子节点数组 } whitelist_tree_node; typedef struct _whitelist_tree_ { whitelist_tree_node *root; //根节点 unsigned int whitelist_count; //URL白名单个数 } whitelist_tree, *PUrlWhiteListControl; /** * [InitWhiteListTree 初始化一颗空树] * @param p_tree [白名单树结构,用完释放] * @return [0: success, other: failure code] */ int InitWhiteListEmptyTree(whitelist_tree **p_tree); /** * [AddUrlToWhiteListTree 将URL添加到树中] * @param url [URL] * @param p_tree [白名单树指针] * @return [description] */ int AddUrlToWhiteListTree(const char *url, whitelist_tree *p_tree); /** * [MatchWhiteLIst 查询是否匹配上白名单] * @param beg_pos [待查URL起始位置,调用者保证有效] * @param end_pos [待查URL结束位置,调用者保证有效] * @param p_tree [白名单树结构] * @return [0: match success, < 0: run failure code, > 0: not match] */ int MatchWhiteLIst(const char *beg_pos, const char *end_pos, whitelist_tree *p_tree); /** * [FreeWhiteListNode 释放节点内存] * @param p_node [待释放节点] */ void FreeWhiteListNode(whitelist_tree_node *p_node); /** * [FreeWhiteListTree 释放整棵白名单树] * @param p_tree [待释放树指针] */ void FreeWhiteListTree(whitelist_tree *p_tree);
c文件:
int InitWhiteListEmptyTree(whitelist_tree **p_tree) { if(p_tree == NULL) { return ERROR_PARAMETER_IS_NULL; } *p_tree = (whitelist_tree *)malloc(sizeof(whitelist_tree)); if(*p_tree == NULL) { return ERROR_MALLOC; } (*p_tree)->whitelist_count = 0; (*p_tree)->root = (whitelist_tree_node *)malloc(sizeof(whitelist_tree_node)); if((*p_tree)->root == NULL) { free(*p_tree); *p_tree = NULL; return ERROR_MALLOC; } memset((*p_tree)->root, 0, sizeof(whitelist_tree_node)); return 0; } int AddUrlToWhiteListTree(const char *url, whitelist_tree *p_tree) { if(p_tree == NULL || url == NULL) { return ERROR_MALLOC; } whitelist_tree_node *p_node = p_tree->root; whitelist_tree_node *p_new_childs = NULL; //指向URL末尾,从后向前插 const char *end_pos = url + strlen(url); //为空 if(end_pos <= url) { return ERROR_URL_ILLEGAL; } while(--end_pos && end_pos >= url) { int offset_pos = toupper(*end_pos)-MIN_ASCII_VALUE; if(offset_pos < 0 || offset_pos >= MAX_CHILDS_NUM) { return ERROR_URL_ILLEGAL; } //该子节点已经存在 if(p_node->child_order[offset_pos] != 0) { //此处添加的是二级域名,以及域名已经是白名单 if(p_node->childs[p_node->child_order[offset_pos]-1].white_type == WHITELIST_DONE) { return 0; } p_node = p_node->childs + p_node->child_order[offset_pos]-1; continue; } ++(p_node->child_count); p_node->child_order[offset_pos] = p_node->child_count; p_new_childs = (whitelist_tree_node *)malloc(p_node->child_count*sizeof(whitelist_tree_node)); if(p_new_childs == NULL) { return ERROR_MALLOC; } memcpy(p_new_childs, p_node->childs, (p_node->child_count-1)*sizeof(whitelist_tree_node)); memset(p_new_childs + (p_node->child_count-1), 0, sizeof(whitelist_tree_node)); free(p_node->childs); p_node->childs = p_new_childs; p_node->childs[p_node->child_count-1].white_type = WHITELIST_UNDONE; p_node = p_node->childs + p_node->child_count-1; } p_node->white_type = WHITELIST_CONTINUE; //每个完整URL白名单追加一个结束点 int offset_pos = toupper('.')-MIN_ASCII_VALUE; ++(p_node->child_count); p_node->child_order[offset_pos] = p_node->child_count; p_new_childs = (whitelist_tree_node *)malloc(p_node->child_count*sizeof(whitelist_tree_node)); if(p_new_childs == NULL) { return ERROR_MALLOC; } memcpy(p_new_childs, p_node->childs, (p_node->child_count-1)*sizeof(whitelist_tree_node)); memset(p_new_childs + (p_node->child_count-1), 0, sizeof(whitelist_tree_node)); free(p_node->childs); p_node->childs = p_new_childs; p_node->childs[p_node->child_count-1].white_type = WHITELIST_DONE; return 0; } int MatchWhiteLIst(const char *beg_pos, const char *end_pos, whitelist_tree *p_tree) { if(beg_pos == NULL || end_pos == NULL || end_pos <= beg_pos) { return ERROR_PARAMETER_IS_NULL; } whitelist_tree_node *p_node = p_tree->root; while(--end_pos && end_pos >= beg_pos) { int offset_pos = toupper(*end_pos)-45; if(offset_pos < 0 || offset_pos >= 46) { return ERROR_URL_ILLEGAL; } if(p_node->child_order[offset_pos] == 0) { return NO_MATCH_URL_WHITE_LIST; } p_node = p_node->childs + p_node->child_order[offset_pos] - 1; if(p_node->white_type == WHITELIST_DONE) { return 0; } } if(p_node->white_type == WHITELIST_UNDONE) { return NO_MATCH_URL_WHITE_LIST; } return 0; } void FreeWhiteListNode(whitelist_tree_node *p_node) { //首先递归释放子节点 uint8_t i = 0; for(; i < p_node->child_count; ++i) { FreeWhiteListNode(p_node->childs + i); } free(p_node->childs); } void FreeWhiteListTree(whitelist_tree *p_tree) { FreeWhiteListNode(p_tree->root); free(p_tree); }