当前位置: 首页 > 后端技术 > PHP

swoole_table的实现原理分析

时间:2023-03-29 21:57:51 PHP

Swoole项目自2012年推出至今已经5年了,现在越来越多的互联网公司使用Swoole开发各种后台应用。受限于PHP的ZendVM实现,PHP程序不能使用多线程进行编程开发。应用程序中的并行处理只能使用多进程模式。做过多进程开发的PHPer都知道进程的内存隔离。程序中声明的global全局数组实际上并没有被data共享。在一个进程中修改数组的值在另一个进程中是无效的。$array=array();functionprocess1(){global$array;$array['test']='helloworld';}functionprocess2(){global$array;//这里不能读取test的值var_dump($array['test']);}这种进程隔离给程序的开发带来了很大的麻烦。比如实现一个聊天室程序,用户A在进程1中处理,用户B在进程2中处理。如果A和B在同一个组,那么这个组在多线程环境下直接用set表示,并且将A和B加入到相应的组的集合中即可。但是在多进程环境下,用PHP的数组是无法实现的。一般有两种方式解决问题:进程间通信,可以使用管道向另一个进程发送请求,通过存储的方式获取数据值。Redis、MySQL、files这两种方案虽然都可以实现,但是存在明显的问题。缺点。解决方案1实施起来更复杂,也更难开发。方案2实现简单,但是有额外的IO消耗,不是纯内存操作,有性能瓶颈。基于/dev/shm读写内存文件的方案是一个很好的方案,但是需要注意锁的操作,读写需要额外的系统调用开销。为了解决这个问题,必须实现基于共享内存的数据结构。PHP中还有一些可用的扩展模块。如APCu、Yac、shm_put_var/shm_get_varYac:性能较高,但由于底层实现的限制,无法保证一致性。APCu只能作为Cache使用:支持Key-Value数据的读写,缺点是实现简单粗暴,锁的粒度太粗。高并发时锁争用较多,性能较差。shm系列功能:虽然这个方案可以实现共享内存操作,但是底层实现其实很简单。一方面,底层没有锁。如果要在并发环境下使用,需要自己实现加锁操作。另外,底层其实是一个链表结构。当数据量很大时,查询性能很差。swoole_table简介为了解决多进程程序中的数据共享问题,Swoole扩展提供了swoole_table数据结构。Table的实现非常精致,使用起来最方便,性能也是最好的。$table=newswoole_table(1024);$table->column('id',swoole_table::TYPE_INT,4);$table->column('name',swoole_table::TYPE_STRING,64);$table->column('num',swoole_table::TYPE_FLOAT);$table->create();$table->set('tianfenghan@qq.com',array('id'=>145,'name'=>'rango','num'=>3.1415));$table->set('350749960@qq.com',array('id'=>358,'name'=>"Rango1234",'num'=>3.1415));$table->set('hello@qq.com',array('id'=>189,'name'=>'rango3','num'=>3.1415));$ret1=$table->get('350749960@qq.com');$ret2=$table->get('tianfenghan@qq.com');$table->del('350749960@qq.com');Table实现了一个二维的Map结构,有点像PHP的二维数组,使用方便。在最新的1.9.19中,也可以使用ArrayAccess接口来操作数组中的Table:$table=newswoole_table(1024);$table->column('id',swoole_table::TYPE_INT);$table->column('name',swoole_table::TYPE_STRING,64);$table->column('num',swoole_table::TYPE_FLOAT);$table->create();$table['apple']=array('id'=>145,'name'=>'iPhone','num'=>3.1415);$table['google']=array('id'=>358,'name'=>"AlphaGo",'num'=>3.1415);$table['microsoft']['name']="Windows";$table['microsoft']['num']='1997.03';var_dump($table['apple']);var_dump($table['microsoft']);$table['google']['num']=500.90;var_dump($table['google']);Table的优点是性能极高,都是纯内存操作,没有任何syscall和IO开销。在CoreI5机器上测试,Table单进程单线程每秒可完成300万次写操作,每秒可完成150万次读操作。在24核的服务器上,理论上每秒可以进行数千万次的读写操作。使用数据行锁,底层使用数据行锁和自旋锁。多个进程并发执行时,读写不同的key时不会争锁。只有在同一个CPU时间读写同一个Key时,才需要加锁操作。而且表本身的锁粒度很小。get和set操作中只有少量内存读写指令,可在数百纳秒内完成。Table的限制Key的最大长度不能超过64个字节。必须在创建之前规划容量。一旦满了,设置新数据时内存分配就会失败,无法实现动态扩展。因此,在使用Table时,尽量设置。内存容量大,虽然这会带来一定的内存浪费,但实际上现代服务器内存很便宜,这个限制在实际项目中问题不大。swoole_table实现原理Table底层是基于共享内存实现的,占用内存取决于表的大小,冲突率(默认20%),列的设置(如上例,每一行需要8+64+8bytes),64字节KEY的存储空间和管理结构的内存消耗。表内存申请size_trow_num=table->size*(1+table->conflict_proportion);size_trow_memory_size=sizeof(swTableRow)+table->item_size;size_tmemory_size=row_num*row_memory_size;memory_size+=sizeof(swMemoryPool)+sizeof(swFixedPool)+((row_num-table->size)*sizeof(swFixedPool_slice));memory_size+=table->size*sizeof(swTableRow*);void*memory=sw_shm_malloc(memory_size);swoole_table本身是一个HashTable结构,Key会被计算为一个哈希值来哈希到每一行。HashTable结构会遇到Hash冲突问题。两个完全不同的Key可能会计算出相同的哈希值。这种情况下,就需要使用链表来解决Hash冲突。Swoole底层会创建一个浮动内存池swFixedPool结构来管理这些冲突键的内存。默认情况下,将创建一个大小为*20%的浮动内存池。在1.9.19中,您可以定义自己的冲突率。$table=newswoole_table(65536,0.9);如果你的场景Hash冲突比较多,可以提高冲突率申请更大的浮动内存池。静态swTableRow*swTable_hash(swTable*table,char*key,intkeylen){#ifdefSW_TABLE_USE_PHP_HASHuint64_thashv=swoole_hash_php(key,keylen);#elseuint64_thashv=swoole_hash_austin(key,keylen);#endifuint64_tindex=hashv&table->面具;断言(索引<表->大小);returntable->rows[index];}swTableRow*swTableRow_set(swTable*table,char*key,intkeylen,swTableRow**rowlock){if(keylen>SW_TABLE_KEY_SIZE){keylen=SW_TABLE_KEY_SIZE;}swTableRow*row=swTable_hash(table,key,keylen);*行锁=行;swTableRow_lock(row);#ifdefSW_TABLE_DEBUGint_conflict_level=0;#endifif(row->active){for(;;){if(strncmp(row->key,key,keylen)==0){break;}elseif(row->next==NULL){table->lock.lock(&table->lock);swTableRow*new_row=table->pool->alloc(table->pool,0);#ifdefSW_TABLE_DEBUGconflict_count++;如果(_conflict_level>conflict_max_level){conflict_max_level=_conflict_level;}#endiftable->lock.unlock(&table->lock);如果(!new_row){返回NULL;}//添加row_numbzero(new_row,sizeof(swTableRow));sw_atomic_fetch_add(&(table->row_num),1);行->下一个=新行;行=新行;休息;}else{row=row->next;#ifdefSW_TABLE_DEBUG_conflict_level++;#endif}}}else{#ifdefSW_TABLE_DEBUGinsert_count++;#endifsw_atomic_fetch_add(&(table->row_num),1);}memcpy(行->密钥,密钥,keylen);行->活动=1;returnrow;}使用swTable_hash计算hash值,hash到对应行。当Key冲突时,需要调用table->pool->alloc从浮动内存池分配当内存浮动内存池不足时,alloc失败,数据无法写入Table数据自旋锁。当多个进程在同一CPU时间读取某一行时,需要竞争锁swTableRow_lock(row);//内存操作swTableRow_unlock(_rowlock);swTableRow_lock本身是一个可选锁。这里使用gcc编译器提供的__sync_bool_compare_and_swap函数来进行CPU原子操作。当多个进程同时读写一行数据时,先获得锁的进程会进行内存读写操作,没有获得锁的进程会进行CPU自旋等待进程释放锁。静态sw_inlinevoidsw_spinlock(sw_atomic_t*lock){uint32_ti,n;while(1){if(*lock==0&&sw_atomic_cmp_set(lock,0,1)){返回;}if(SW_CPU_NUM>1){for(n=1;n列计算内存指针的偏移量,得到对应字段的值。staticinlinevoidphp_swoole_table_row2array(swTable*table,swTableRow*row,zval*return_value){array_init(return_value);swTableColumn*col=NULL;swTable_string_length_tvlen=0;双dval=0;int64_tlval=0;字符*k;while(1){col=swHashMap_each(table->columns,&k);如果(col==NULL){中断;}if(col->type==SW_TABLE_STRING){memcpy(&vlen,row->data+col->index,sizeof(swTable_string_length_t));sw_add_assoc_stringl_ex(return_value,col->name->str,col->name->length+1,row->data+col->index+sizeof(swTable_string_length_t),vlen,1);}elseif(col->type==SW_TABLE_FLOAT){memcpy(&dval,row->data+col->index,sizeof(dval));sw_add_assoc_double_ex(return_value,col->name->str,col->name->length+1,dval);}else{switch(col->type){caseSW_TABLE_INT8:memcpy(&lval,row->data+col->index,1);sw_add_assoc_long_ex(return_value,col->name->str,col->name->length+1,(int8_t)lval);休息;caseSW_TABLE_INT16:memcpy(&lval,row->data+col->index,2);sw_add_assoc_long_ex(return_value,col->name->str,col->name->length+1,(int16_t)lval);休息;caseSW_TABLE_INT32:memcpy(&lval,row->data+col->index,4);sw_add_assoc_long_ex(return_value,col->name->str,col->name->length+1,(int32_t)lval);休息;默认值:memcpy(&lval,row->data+col->index,8);sw_add_assoc_long_ex(返回值,col->name->str,col->name->length+1,lval);休息;}}}}