关于异步IO及链接复用
分布式搜索系统中,一般都有merge和search两种角色,分别部署在不同的机器上。由于数据量巨大,每台search只能加载部分索引数据,多台search机器合在一起组成全量索引。一个查询为了获得正确结果,需要把这个查询发给所有的search机器,经过检索后,再把所有search机器的结果聚合在一起,产生最终的正确结果;这个分发再聚合的过程,就是由merge来实现的。
可以简单的认为,merge对外接收用户请求,对内分发到所有机器,等待search返回结果,聚合排序然后返回给用户。可以看到merge和多台search之间,保持长连接是必要的。假设merge同时接收到两条请求,有两种简单做法做分发:1, 使用一个长连接,串行做; 2, 在merge和每台search间建立两个长连接,并行做。首先1是不靠谱的,因为search一般都是多线程的,串行就完全利用不到这个特性;方案2,最开始我们有套系统是这么做的,merge和每台search之间保持的长连接数量跟search的线程数有关。由于集群规模越来越大,长连接数量也成比例增长,给集群带来很大压力。
有一种改进方式,同时可以有多个查询串在一个长连接上并行。大致实现是,merge和search之间每个长连接Connection都包含一个ChannelPool对象,这个对象可以理解为map,包含了channelid和query的映射。merge分发请求时,封装一个自己的数据包,包头含有channelid,search检索结束返回结果时,同时把这个channelid也送回来。merge通过这个channelid和这个链接的channelmap,就知道是哪个query的查询结果了。通过这种方式,一个长连接上,可以同时由多个查询并行进行,降低了大集群的链接压力。
search端从一个长连接中陆续读出检索请求,多个检索请求在search内部由线程池处理,处理完后再通过接收到这个请求的长连接,写出去(包含channelid)。merge端的该链接收到search响应后,通过channelpool获得这个请求的对象,把结果写到该对象中。收集到所有search的返回结果后,该请求就可以进行下一步操作,比如合并,排序,返回给用户等。
关于线程池
原来线程池的实现方式(比如java的各种threadpool),会根据系统的负载情况,调整线程数量。现在,计算机内存越来越大,在内存中维护一定数量的线程,不会对系统造成多大压力。在线程池开始,就起固定数量的线程,不做调整,可以很大简化线程池的实现。
21 class Runnable {22 public:
23 virtual void run() = 0;
24 };
25
26 class ThreadPool {
27 public:
28 /* 构造函数
29 * param thread_num : 线程数量
30 * param queue_size : 任务队列大小
31 */
32 ThreadPool(int thread_num, int queue_size);
33 ~ThreadPool() {};
34
35 // 阻塞的添加和删除任务 使用信号量做信息通知
36 void enqueueTask(Runnable *job);
37 Runnable* dequeTask();
38
39 void start() {
40 for(int i=0; i<thread_num; i++)
41 pthread_create(thr_id[i], NULL, &ThreadPool::threadFunc, this);
42 };
43
44 private:
45 static void* threadFunc(void *param) {
46 ThreadPool *pool = (ThreadPool*)param;
47 Runnable *job = NULL;
48 while((job = pool->dequeTask()) != NULL) {
49 job->run();
50 }
51 };
52 };