聊天系统中的用户列表并发问题分析

1.问题描述

上周末一个做视频直播的朋友向我咨询他们遇到的一个关于大量内存对象并发的问题,具体问题描述是这样的,在游戏视频直播的时候,需要向观看直播的人提供一个可以自由聊天的功能(相当于QQ群),这就要涉及到在服务器端实现一个管理用户列表的功能,这个用户列表可能很大(最大可以容纳300万人观看和聊天)。他们的做法是在后端服务分为两层,如图:

图-1

gate用来做客户端连接和消息分发的服务,chat是用来做用户认、管理和消息转发。那么需要在chat上维护一下用户列表。他们遇到的问题就是当用户列表比较大的情况下,chat的处理能力急剧下降。我详细询问了他关于用户列表维护的数据结构和并发控制,初步定位到了问题所在。

2.问题分析

我们先来分析一下他们的实现,他们采用的是C++和STL,熟悉C++/STL的朋友很快就会想到使用std::map 来实现管理,对的,这正是他们的思路,下面是他们实现的简单描述:

class user{
public:
	uint64_t	user_id;
	/*todo:用户信息基本信息*/
pthread_mutex_t mutex;		/*用于保护user的多线程并发*/
}

std::map<uint64_t, user*> user_map;
pthread_mutex_t	user_map_mutex; /*多线程操作时保护user_map*/

对map管理的用户列表需要提供增、删、改、查和遍历。例如向某一个用户进行操作:

LOCK(user_map_mutex);
std::map<uint64_t, user*>::iterator it = user_map.find(id);
if(it != user_map.end()){
UNLOCK(user_map_mutex);

	LOCK(it->second->mutex);
	operator(it->second); /*可能时间比较长,可能是发送网络报文、信息写盘、RPC调用等*/
	UNLOCK((it->second->mutex);
}
else
UNLOCK(user_map_mutex);

其他操作类似。这个实现有几个严重的并发问题:

1.每次操作都需要对user_map进行LOCK

2.每次对某个用户操作都需要对用户LOCK

                3.每次对用户操作的函数可能时间会比较长,例如socket发包、RPC调用等。

3.user并发竞争优化

由于chat是个单点多线程并发系统,在网络事件多的情况下,会发生大量的线程锁竞争问题。最为明显的就是第二三个问题,其实这两个是一个问题。解决这个我们只要把user中的mutex去掉即可。怎么去?我的想法是采用user对象引用计数来实现。例如:

class user{
public:
	string	user_id;
	/*其他的一些用户信息*/
	Int ref;	/*引用计数为0时,free掉这个对象*/
}
 void add_ref(user* u){
	u->ref ++;
}
void release_ref(user* u){
	u->ref --;
	if(u->ref <= 0) delete u;
}

引用计数的操作规则:

在用户信息加入用户列表的时候,add_ref

在用户从用户列表中删除的时候,release_ref

在用户信息被引用的时候,add_ref

在用户信息引用完毕的时候,release_ref

那么对某个用户的操作就会变成:

LOCK(user_map_mutex);
std::map<uint64_t, user*>::iterator it = user_map.find(id);
if(it != user_map.end()){
	user* u = it->second;
	add_ref(u);
	UNLOCK(user_map_mutex);

	operator(it->second); /*有可能时间比较长*/
	release_ref(u);
}
else
	UNLOCK(user_map_mutex);

User对象引用计数很好的解决的User加锁的问题,但引用计数的引入了一个新的问题就是在多个线程同时修改某一个用户信息时,会引发数据无法保护的问题。我们处理里这个问题很简单。不管是增加操作、修改操作和删除操作,都遵循先必须将user_map中已存在的对应的user信息从map中删除,再做信息新增。例如修改操作:

LOCK(user_map_mutex);
std::map<uint64_t, user*>::iterator it = user_map.find(id);
if(it != user_map.end()){
/*将旧的信息拷贝出来*/
	user* u =it->second;
	user_map.erase(it);
	copy(update_u, u);
	release_ref(u);	   /*解除引用*/

	update(update_u); /*修改用户数据*/
	Add_ref(update_u);
	user_map.insert(update_u);
UNLOCK(user_map_mutex);

}
else
	UNLOCK(user_map_mutex);

增加和删除的实现类似。对象引用计数很好的解决的用户数据锁竞争的问题,但在user_map的用户数小于1万以下,使用引用计数可以把增删改查操作的并发问题避免掉。不能解决全map扫描并发问题,也不能解决在user_map很大时大量需要操作用户信息的并发问题。问题出在不管是全map扫描还是对单个用户都需要对user_map进行上锁,这就是第一个问题了。在高并发请求下,这个user_map锁会产生大量的竞争,造成资源损耗。

4.放弃std::map

要去掉这个锁,这就回到了在问题分析中的第一个问题上,众所周知,std::map是不支持多线程并发的,而且std::map操作对CPU cache并不友好。去掉这个全局锁改用更小粒度的锁,那就需要放弃std::map。在大量数据的情况下,一般会采用hash table或则btree来组织数据(研究过数据库存储引擎的人都知道,呵呵!)。简单起见,这里就以hash table为例来展开分析。

图-2

图-2是一个hash table的结构图,其中hash buckets是个数组。数组内有一个指向user结构的指针。好,了解了hash table的结构我们再回到前面缩小锁粒度的问题上来。例如我们定义了一个hash table,它的buckets个数为1024,我们再定义一个pthread_mutex_t数组,长度为256。缩小锁的粒度很简单。

第一个mutex数组单元负责0 256 512 768序号bucket的互斥,第二个负责1 257 512 769序号的并发互斥,类推。计算一个bucket序号是由哪个mutex负责互斥的其实就是:

mutex下标 = bucket_seq % mutex_array_size;

这样实现很容易理解,在内部的user对象操作我们还是采用引用计数的方法。细分了锁粒度,能让整个用户列表具有非常好的并发性,同时因为buckets是个连续的数组,对CPU L1/L2 cahce也非常的友好,也大大提高了CPU Cache的命中率问题。一般优化到此,基本上可以说做到了90%的工作。但还是有几个疑问:

?  为什么要用pthread_mutex_t?在高并发下它会不会引起不必要的操作系统上下文切换?

?  除了hash table之外还有什么数据结构能支持细粒度的锁?

针对上面第一个疑问,我们可以使用CPU原子操作来实现一个简单的mutex。例子如下:

void LOCK(int* q){
	int i;
	while(__sync_lock_test_and_set(q, 1)){
		for(i = 0; i < 32; i ++) cpu_pause();
		sched_yield(); /*释放CPU执行权,让操作系统重新调度本线程*/
	}
};

#define UNLOCK(q) __sync_lock_release((q))

那么就可以将pthread_mutex_t数组去掉,由一个int数组来代替他的工作。

为什么可以这样实现?在lock函数里面难道空转不耗CPU么?这个可以结合我们的hash table来分析,一次hash table的增删改查操作,一般几百个CPU指令周期就可以完成(不计算hash函数运行时间,因为计算hash(key)无需等待锁),也就是说在LOCK等待的时间不长,而且CPU的指令执行速度远远大于CPU从内存中载入数据的速度,所以用CPU spin等待来换取操作系统因为pthread
lock造成的上下文切换损耗是值得的。这个可以自行去测试,呵呵。

对于这种hashtable结构并发量,我做了个初步的测试,测试机配置:4核2.4GCPU,内存16G,程序启动8个线程进行测试,hashtable存有800万个用户信息,每秒可以支持100万个左右查询,50万左右的增删改。

5.思考

回到最初的问题,其实就是在内存中管理一个海量内存对象的问题,这不是什么新技术,在数据库存储引擎中,随处可以看到这样的解决方案,例如:memcache的索引实现、innodb的自适应hash索引实现和btree实现、lsm树的memtable实现,无一不是解决此类问题的。通过这个问题的分析,可以得到以下几个认识:

1.     C++从业人员在高并发设计上应该慎用stl/boost,它们的很多数据结构对多核并发并不友好,这里仅仅是针对C++说的。

2.     很多看似非常难的问题,其实很多其他领域的系统有很好的解决方案。作为C/C++从业人员,应该多去了解数据库内核、操作系统内核或者编程语言内核(JVM/GOruntime)。这三个地方有挖不完的技术宝藏。

3.     C/C++语言在多核并发控制上可以说很原始,作为C/C++从业人员的我们,应该多去了解CPU的工作机制、C/C++的内存模型等,这样有利于我们去分析系统瓶颈和优化系统。

4. 放弃意味着收获更多,放弃C++,选用更容易编写并发程序的语言编写此类系统,例如go、scala、erlang。

遗留的思考题

1.     用CPU CAS + memory barrier怎么实现hash table的无锁并发?可以尝试去实现一下看看。

2.     除了用hash table解决海量用户列表问题,还可以用skip list、btree等数据结构来实现,怎么实现?skip list、btree和hash table对比优劣势在什么地方?

3.     hashtable在管理海量用户列表时,它有缺点么?有什么样的缺点?

时间: 2024-10-11 05:44:26

聊天系统中的用户列表并发问题分析的相关文章

在线用户列表

[转载]Asp.Net在线用户列表的開發匯總 这是转载的别人的一篇,解决了困扰我已久的问题,虽然文章里少了两张图,但是不影响阅读. 1.在线用户列表的实现在ASP时代,要实现一个网站的在线用户列表显示功能的惯用做法是修改global.asa文件中的:Application_Start.Session_Start和Session_End这三个函数.在ASP.NET时代,我依然这样做.但是必须注意很多问题.首先来看看最简单的代码实现: protected void Application_Start

Live555源码分析[2]:RTSPServer中的用户认证

http://blog.csdn.net/njzhujinhua @20140601 说到鉴权,这是我多年来工作中的一部分,但这里rtsp中的认证简单多了,只是最基本的digest鉴权的策略. 在Live555的实现中, 用户信息由如下类维护,其提供增删查的接口.realm默认值为"LIVE555 Streaming Media" class UserAuthenticationDatabase { public: UserAuthenticationDatabase(char con

构建用户列表,并使用列表中的帐户登陆.

'''构建用户列表,格式为:[{'user': 'k1', 'value': 'v1'},{'user': 'k2', 'value': 'v2'},{'user': 'k3', 'value': 'v3'}]Q为停止录入,然后使用用户列表中的内容登陆,失败则重新输入.'''lst = []while 1: dic = {} a = input('用户名:') if a.upper() == "Q": break b = input('密码:') dic['user'] = a dic

python案例:金融营销活动中欺诈用户行为分析

下午学习了python数据分析的应用案例---金融营销活动中欺诈用户行为分析.数据来源于DC竞赛数据:https://www.dcjingsai.com/common/cmpt/2018%E5%B9%B4%E7%94%9C%E6%A9%99%E9%87%91%E8%9E%8D%E6%9D%AF%E5%A4%A7%E6%95%B0%E6%8D%AE%E5%BB%BA%E6%A8%A1%E5%A4%A7%E8%B5%9B_%E7%AB%9E%E8%B5%9B%E4%BF%A1%E6%81%AF.ht

基于用户电影评价的分析预测

故事背景 在我们的日常生活中,人们已经习惯了看电影.但是,每个人的偏好是不同的,有的人可能喜欢战争片,有人可能更喜欢艺术片,而有的人则可能喜欢爱情片,等等.现在,我们收集了一些的客户和电影的相关信息,目的是找出客户对特定影片的评分,从而预测出客户有可能喜爱的电影并推荐给客户.本次的大数据处理,使用了单词统计.基于用户的协同过滤算法等. 分析预测技术 分析工具:基于Hadoop的MapReduce 数据预处理:利用单词统计将一部分重复的.无用的数据过滤掉 算法:基于用户的协同过滤算法 数据可视化:

Android 实现用户列表信息滑动删除功能和选择删除功能

在项目开发过程中,常常需要对用户列表的信息进行删除的操作.Android中常用的删除操作方式有两种 ,一种就是类似微信的滑动出现删除按钮方式,还有一种是通过CheckBox进行选择,然后通过按钮进行删除的方式.本来的实例集成上述的两种操作方式来实现用户列表删除的效果. 设计思路:在适配器类MyAdapter一个滑动删除按钮显示或隐藏的Map,一个用于CheckBox是否选中的Map和一个与MainAcitivyt进行数据交互的接口ContentsDeleteListener,同时该接口包含两个方

Collections中sort()方法源代码的简单分析

Collections的sort方法代码: public static <T> void sort(List<T> list, Comparator<? super T> c) { Object[] a = list.toArray(); Arrays.sort(a, (Comparator)c); ListIterator i = list.listIterator(); for (int j=0; j<a.length; j++) { i.next(); i.

[Python]从豆瓣电影批量获取看过这部电影的用户列表

前言 由于之后要做一个实验,需要用到大量豆瓣用户的电影数据,因此想到了从豆瓣电影的“看过这部电影 的豆瓣成员”页面上来获取较为活跃的豆瓣电影用户. 链接分析 这是看过"模仿游戏"的豆瓣成员的网页链接:http://movie.douban.com/subject/10463953/collections. 一页上显示了20名看过这部电影的豆瓣用户.当点击下一页时,当前连接变为:http://movie.douban.com/subject/10463953/collections?st

Android 实现用户列表信息的功能,然后选择删除幻灯片删除功能

在项目开发过程中.经常须要对用户列表的信息进行删除的操作.Android中经常使用的删除操作方式有两种 .一种就是类似微信的滑动出现删除button方式,另一种是通过CheckBox进行选择.然后通过button进行删除的方式.本来的实例集成上述的两种操作方式来实现用户列表删除的效果. 设计思路:在适配器类MyAdapter一个滑动删除button显示或隐藏的Map,一个用于CheckBox是否选中的Map和一个与MainAcitivyt进行数据交互的接口ContentsDeleteListen