Tao Hui http://weibo.com/taohui3
English Version
设计理念
lushan 在1.0版本的时候是一个轻量级的key-value数据库,使用memcached协议,可以挂载多个库。使用lushan你可以很容易的像memcached一样在几台机器上搭建一个集群。lushan 2.0 同时是一个轻量级的应用框架,可以挂载多个共享库,这样你在一个进程内同时具有访问数据和计算的能力,这使得编写大数据量、高性能的服务变得前所未有的简单。特别适合互联网推荐、广告和搜索的业务场景。目前lushan 已经在新浪微博推荐和广告业务中应用多年。
大概在2013年,我在开发“错过的微博“推荐的时候。我需要在线提供几份数据存储来实验不同的算法,以及他们的在线和测试版本。这样需要部署几套系统。这种做法太low了。所以,在某个周末时间我开发了lushan,他可以把你从这些事情中解放出来,非常cool。
事实也是如此,lushan后来已经成为微博推荐和广告业务的基础设施。现在运维着两个集群,分别有12台机器,服务着上T的在线查询数据,每天10亿以上的查询。
在开发完lushan 第一个版本的时候,我一直有让lushan 同时能够挂载共享库的冲动。但迟迟没有下定决心,一是因为我相信一个框架最好能有自己的定位,二是架构上应该把易变更部分和稳定部分分开。但在2015年,开发微博广告的时候,用户的兴趣数据、关系数据、CTR预估的特征数据都很容易通过Hadoop整理后存储到lushan,这个时候很简单的编写两个模块即可实现targeting和ctr预估功能,而且性能非常强劲。于是我放弃了原来的想法,实现了第二版,让lushan 功能更加强大。实际应用中你仍然可以把lushan 只作为key-value database 使用,或者把只提供数据的lushan 集群和同时提供计算的集群分开单独部署,在微博广告内部也是这么使用的。
在examples目录下已经提供了一个样例库,按照下面的步骤去挂载:
输出就是key 123456所对应的value.
解释一下每一步:
作为计算框架来使用的时候lushan 支持了两种协议,类似于HTTP GET的单行"URL"协议,以及类似于HTTP POST的指定发送value长度的协议,后者同时支持发送二进制数据。在modules 目录下提供了两个例子:lproxy和lecho,分别演示了这两个协议。
lproxy 的例子,对于一个请求的key, 先查询redis,redis里有的则直接返回,redis没有的查询本机挂载的库。在简单的情况下这个例子也可以用于生产环境,如果有更复杂的需求请修改此代码。
按下面步骤操作:
创建一个文本文件x.txt,输入下面两行, 第一个tab之前的是key, 后面的是value,如下:
168 hello lushan
187 line 2
用tools里的lushan_line_maker 转成lushan 的文件格式,假如为hdict_20180428192000,通过前面数据存取例子挂载到lushan 编号为1的目录下。
在本机起一个redis, set dbno 为1, 然后加入一条记录, key为168, value为"hello redis"
在hproxy目录下执行make, 把生成的hmodule.so和hmodule.conf放到hmod/15/1.0.0目录下, 修改hmodule.conf里的host 和port 为你部署的redis 的ip 和端口。
执行:
echo -ne "hmod_open /mnt/lushan/hmod/15/1.0.0/ 15rn" | nc 127.0.0.1 9999
如果返回OPENED 则打开成功,否则检查libhiredis 是否在LD_LIBRARY_PATH 里。
查询:
echo -ne "get m15?k=1-168rn" | nc 127.0.0.1 9999
VALUE m15?k=1-168 0 11
hello redis
END
echo -ne "get m15?k=1-187rn" | nc 127.0.0.1 9999
VALUE m15?k=1-187 0 6
line 2
END
则如我们预期,redis 存在则返回redis 结果,否则查询lushan 数据。
关闭一个模块:
echo -ne "hmod_close 15rn" | nc 127.0.0.1 9999
如果你的所有模块都没有全局变量,可以用hmod_open 把一个新的库直接替换旧的库,这样对线上的服务没有任何损失。
lecho 的例子类似,只是简单返回你请求的数据,这个例子很简单,不再详细描述。
hdict 是lushan所挂载的库格式。他非常简单。在hdict_xxxx目录下有两个必须的文件,dat和idx。前者包含你的数据,后者是key到value在dat文件中位置偏移的映射。 定义:
typedef struct {
uint64_t key;
uint64_t pos;
} idx_t;
key 是64位无符号长整形,不包含库编号。 pos 是由value的长度和其在dat文件中的偏移组合而成的:
pos = (length << 40) | offset;
idx文件必须是按照idx_t.key升序排列的。dat文件则不需要。你既可以对已经存在的一个dat文件创建索引,也可以在输出文件的时候同时生成索引。
有序文件在map-reduce计算模型中非常常见。你可以在hadoop中指定输出文件格式,从而生成hdict格式的库。例如如下命令:
job.setOutputFormat(LushanFileOutputFormat.class);
有三个命令可以获得统计状态数据: stats, info 和 hmod_info。前者输出全局数据,后两者输出每个库和每个模块的数据。
echo -n -e "statsrn" | nc 127.0.0.1 9999
STAT pid 13810
STAT uptime 1435075686
STAT curr_connections 1411
STAT connection_structures 4061
STAT cmd_get 2099151223
STAT get_hits 3950240117
STAT get_misses 2443878402
STAT threads 16
STAT timeouts 117
STAT waiting_requests 0
STAT ialloc_failed 0
END
echo -n -e "inforn" | nc 127.0.0.1 9999
id label state ref num_qry idx_num open_time path
----------------------------------------------------------------
1 interest_CF_trends OPEN 0 139922 18419392 150824-042654 /mnt/lushan/hdb/12/hdict_20150711204737
2 interest_CF_trends OPEN 0 190508 26175141 150824-050246 /mnt/lushan/hdb/12/hdict_20150711204737
echo -ne "hmod_inforn" | nc 127.0.0.1 9999
id label state ref num_qry open_time path
----------------------------------------------------------------
0 OPEN 0 267130787 180419-174502 /mnt/lushan/hmod/0
5 OPEN 0 336829974 180419-174503 /mnt/lushan/hmod/5
你可以利用lushan.php创建图形化的统计状态页面。
如果你有mysql的经验,会很容易搭建一个简单的集群。首先,你要把你的数据分成若干组,通常是你机器数的某个倍数。然后考虑你要部署几套服务,通常是两套分布在不同的IDC。然后按照分组的规则去通过memcached客户端查询你的数据。
尽管非常简单,但lushan仍然提供了一个简单的框架去帮助你处理一些数据传输方面的细节,名字叫做transfer.py,可以帮助你:
memcached 的协议比较简单,get只支持简单的请求,但可以返回相对复杂的结果。set类命令支持复杂的请求,但只支持相对简单的结果。lushan对此做了两个修改,使用的时候类似于HTTP的GET和POST协议,下面的key通常可以理简单对应HTTP里的URL:
get请求的“key”可以超过250字节限制。 在发送的时候设置:
memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_VERIFY_KEY, 0);
这样通过libmemcached发送没有问题。返回结果的时候需要返回在250字节内的key,可以选择的是返回请求的签名,也可以返回前250个字节。只要读取的时候按照截断的key读取,并且截断后的key不冲突即可。
用gets来支持发送多行的请求。通常简单get请求就够用了,但如果想要发送的是类似json的请求则需要更复杂的协议。lushan重新修改了gets协议,修改成和set一样的协议。在使用客户端的时候同样按上面设置不检查key的合法性,然后发送一个下面格式的包过去:
gets key 0 0 value_lenrn
valuern
在返回结果里直接fetch 这个key即可。在lutil.h里做了封装,直接调用hrequest_pack即可。
stat里timeout比较多,或者你从客户端记录大于等于你的配置的TIMEOUT时间的请求比较多,你可以尝试设置更大的NUM_THREADS.
upload某个编号下有很多没有传完的hdict文件,这通常意味着你的传输脚本在没有传完的时候就中断。