专注业务连续性支持与数据保护
2009-08-25关注学习

RouterOS之备份和恢复设置已关闭评论

RouterOS之备份和恢复设置

1.备份和恢复设置
你想想辛辛苦苦设置好的防火墙规则,网卡设置,各个路由,端口映射万一弄错了或重新安装时,是不是都要重新自已设置?这个巨麻烦!!!但rouetos却为你考虑得很周到,可以手工备份你的设置文件,需要时只要一个命令即可恢复!
大家可以用winbox登陆,注意用admin帐号,在左边是不是有个files?点开,再点对话框上面的backup,这样就把当前的设置保存一个文件里面了,再用ie登陆你的路由,用网管帐号:密码@路由ip:端口,登陆到你的路由后就会到你保存的文件了!用ie直接下载吧!/" target=_blank>ftp://网管帐号:密码@路由ip:端口,登陆到你的路由后就会到你保存的文件了!用ie直接下载吧!
当你重新安装时,只要把内网弄通,用ie再登陆你的路由,把这个设置文件传上去,在winbox左边下面有个te开头的英文,这是终端模拟,点开后就像在路由上操作一样,用以下命令恢复你以前的设置:
system回车
backup回车
load name=你保存的设置文件名 回车
提示重启就一下子恢复到你以前设置了!!
是不是方便实用啊?
大家可能会说用winbox备份不爽,那我们也可以用终端备份呀!
在winbox左边下面有个te开头的英文,这是终端模拟,点开后就像在路由上操作一样,用以下命令备份你以前的设置:
system回车
backup回车
save name=你保存的设置文件名 回车
建议文件名用日期表示可以很直观。这样就按你的文件名保存了。
用load name命令就是恢复了.
2.恢复路由本身默认值。
如果设错了规则或者地址,造成win不能进入管理界面,可以这样复原:
使用 admin 登陆
system 回车
reset 选择 y
将删除所有改动,恢复新装的状态
这个是恢复到出厂设置,很适合刚开始设置routeos时用用!
===============
2.导入导出配置命令:
使用export命令导出,使用import命令导入。
如:
导出全部配置命令为:/export file=xxx
导入配置命令:/import file=xxx
导出防火墙配置的命令:/ip firewall export file=xxx

2009-08-24关注学习

RouterOS流量控制限速批量脚本已关闭评论

RouterOS流量控制限速批量脚本

:for aaa from 1 to 254 do={/queue simple add name=(queue . $aaa) dst-address=(192.168.0. . $aaa) limit-at=0/0 max-limit=2000000/2000000}

 

其中的1 to 254 是192.168.0.1-192.168.0.254全网段,请删除代理服务器IP地址和局域网主机IP地址。

其中的max-limit=2000000/2000000 为每个IP限速下载 256K/秒 与 上传 256K/秒。

 

使用:
WinBox-System-Scripts-+
Name(脚本名程)
Source(脚本)

 

把以上脚本复制到Source 项目里面,点击
OK-选择要运行的脚本-Run Script

2009-08-18关注学习

Memcached深入解析已关闭评论

Memcached深入解析

Memcached是danga.com(运营LiveJournal的技术团队)开发的一套分布式内存对象缓存系统,用于在动态系统中减少数据库负载,提升性能。关于这个东西,相信很多人都用过,本文意在通过对memcached的实现及代码分析,获得对这个出色的开源软件更深入的了解,并可以根据我们的需要对其进行更进一步的优化。末了将通过对BSM_Memcache扩展的分析,加深对memcached的使用方式理解。
本文的部分内容可能需要比较好的数学基础作为辅助。

 

◎Memcached是什么
在阐述这个问题之前,我们首先要清楚它“不是什么”。很多人把它当作和SharedMemory那种形式的存储载体来使用,虽然memcached使用了同样的“Key=>Value”方式组织数据,但是它和共享内存、APC等本地缓存有非常大的区别。Memcached是分布式的,也就是说它不是本地的。它基于网络连接(当然它也可以使用localhost)方式完成服务,本身它是一个独立于应用的程序或守护进程(Daemon方式)。
Memcached使用libevent库实现网络连接服务,理论上可以处理无限多的连接,但是它和Apache不同,它更多的时候是面向稳定的持续连接的,所以它实际的并发能力是有限制的。在保守情况下memcached的最大同时连接数为200,这和Linux线程能力有关系,这个数值是可以调整的。关于libevent可以参考相关文档。 Memcached内存使用方式也和APC不同。APC是基于共享内存和MMAP的,memcachd有自己的内存分配算法和管理方式,它和共享内存没有关系,也没有共享内存的限制,通常情况下,每个memcached进程可以管理2GB的内存空间,如果需要更多的空间,可以增加进程数。

 

◎Memcached适合什么场合
在很多时候,memcached都被滥用了,这当然少不了对它的抱怨。我经常在论坛上看见有人发贴,类似于“如何提高效率”,回复是“用memcached”,至于怎么用,用在哪里,用来干什么一句没有。memcached不是万能的,它也不是适用在所有场合。
Memcached是“分布式”的内存对象缓存系统,那么就是说,那些不需要“分布”的,不需要共享的,或者干脆规模小到只有一台服务器的应用,memcached不会带来任何好处,相反还会拖慢系统效率,因为网络连接同样需要资源,即使是UNIX本地连接也一样。 在我之前的测试数据中显示,memcached本地读写速度要比直接PHP内存数组慢几十倍,而APC、共享内存方式都和直接数组差不多。可见,如果只是本地级缓存,使用memcached是非常不划算的。
Memcached在很多时候都是作为数据库前端cache使用的。因为它比数据库少了很多SQL解析、磁盘操作等开销,而且它是使用内存来管理数据的,所以它可以提供比直接读取数据库更好的性能,在大型系统中,访问同样的数据是很频繁的,memcached可以大大降低数据库压力,使系统执行效率提升。另外,memcached也经常作为服务器之间数据共享的存储媒介,例如在SSO系统中保存系统单点登陆状态的数据就可以保存在memcached中,被多个应用共享。
需要注意的是,memcached使用内存管理数据,所以它是易失的,当服务器重启,或者memcached进程中止,数据便会丢失,所以memcached不能用来持久保存数据。很多人的错误理解,memcached的性能非常好,好到了内存和硬盘的对比程度,其实memcached使用内存并不会得到成百上千的读写速度提高,它的实际瓶颈在于网络连接,它和使用磁盘的数据库系统相比,好处在于它本身非常“轻”,因为没有过多的开销和直接的读写方式,它可以轻松应付非常大的数据交换量,所以经常会出现两条千兆网络带宽都满负荷了,memcached进程本身并不占用多少CPU资源的情况。

 

◎Memcached的工作方式
以下的部分中,读者最好能准备一份memcached的源代码。
Memcached是传统的网络服务程序,如果启动的时候使用了-d参数,它会以守护进程的方式执行。创建守护进程由daemon.c完成,这个程序只有一个daemon函数,这个函数很简单(如无特殊说明,代码以1.2.1为准):
CODE:
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int
daemon(nochdir, noclose)
    int nochdir, noclose;
{
    int fd;
    switch (fork()) {
    case -1:
        return (-1);
    case 0:
        break; 
    default:
        _exit(0);
    }
    if (setsid() == -1)
        return (-1);
    if (!nochdir)
        (void)chdir(”/”);
    if (!noclose && (fd = open(”/dev/null”, O_RDWR, 0)) != -1) {
        (void)dup2(fd, STDIN_FILENO);
        (void)dup2(fd, STDOUT_FILENO);
        (void)dup2(fd, STDERR_FILENO);
        if (fd > STDERR_FILENO)
            (void)close(fd);
    }
    return (0);
}
这个函数 fork 了整个进程之后,父进程就退出,接着重新定位 STDIN 、 STDOUT 、 STDERR 到空设备, daemon 就建立成功了。
Memcached 本身的启动过程,在 memcached.c 的 main 函数中顺序如下:
1 、调用 settings_init() 设定初始化参数
2 、从启动命令中读取参数来设置 setting 值
3 、设定 LIMIT 参数
4 、开始网络 socket 监听(如果非 socketpath 存在)( 1.2 之后支持 UDP 方式)
5 、检查用户身份( Memcached 不允许 root 身份启动)
6 、如果有 socketpath 存在,开启 UNIX 本地连接(Sock 管道)
7 、如果以 -d 方式启动,创建守护进程(如上调用 daemon 函数)
8 、初始化 item 、 event 、状态信息、 hash 、连接、 slab
9 、如设置中 managed 生效,创建 bucket 数组
10 、检查是否需要锁定内存页
11 、初始化信号、连接、删除队列
12 、如果 daemon 方式,处理进程 ID
13 、event 开始,启动过程结束, main 函数进入循环。
在 daemon 方式中,因为 stderr 已经被定向到黑洞,所以不会反馈执行中的可见错误信息。
memcached.c 的主循环函数是 drive_machine ,传入参数是指向当前的连接的结构指针,根据 state 成员的状态来决定动作。
Memcached 使用一套自定义的协议完成数据交换,它的 protocol 文档可以参考: http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
在API中,换行符号统一为\r\n

 

◎Memcached的内存管理方式
Memcached有一个很有特色的内存管理方式,为了提高效率,它使用预申请和分组的方式管理内存空间,而并不是每次需要写入数据的时候去malloc,删除数据的时候free一个指针。Memcached使用slab->chunk的组织方式管理内存。
1.1和1.2的slabs.c中的slab空间划分算法有一些不同,后面会分别介绍。
Slab可以理解为一个内存块,一个slab是memcached一次申请内存的最小单位,在memcached中,一个slab的大小默认为1048576字节(1MB),所以memcached都是整MB的使用内存。每一个slab被划分为若干个chunk,每个chunk里保存一个item,每个item同时包含了item结构体、key和value(注意在memcached中的value是只有字符串的)。slab按照自己的id分别组成链表,这些链表又按id挂在一个slabclass数组上,整个结构看起来有点像二维数组。slabclass的长度在1.1中是21,在1.2中是200。
slab有一个初始chunk大小,1.1中是1字节,1.2中是80字节,1.2中有一个factor值,默认为1.25
在1.1中,chunk大小表示为初始大小*2^n,n为classid,即:id为0的slab,每chunk大小1字节,id为1的slab,每chunk大小2字节,id为2的slab,每chunk大小4字节……id为20的slab,每chunk大小为1MB,就是说id为20的slab里只有一个chunk:
CODE:
void slabs_init(size_t limit) {
    int i;
    int size=1;
    mem_limit = limit;
    for(i=0; i<=POWER_LARGEST; i++, size*=2) {
        slabclass[i].size = size;
        slabclass[i].perslab = POWER_BLOCK / size;
        slabclass[i].slots = 0;
        slabclass[i].sl_curr = slabclass[i].sl_total = slabclass[i].slabs = 0;
        slabclass[i].end_page_ptr = 0;
        slabclass[i].end_page_free = 0;
        slabclass[i].slab_list = 0;
        slabclass[i].list_size = 0;
        slabclass[i].killing = 0;
    }
    /* for the test suite:  faking of how much we’ve already malloc’d */
    {
        char *t_initial_malloc = getenv(”T_MEMD_INITIAL_MALLOC”);
        if (t_initial_malloc) {
            mem_malloced = atol(getenv(”T_MEMD_INITIAL_MALLOC”));
        }
    }
    /* pre-allocate slabs by default, unless the environment variable
       for testing is set to something non-zero */
    {
        char *pre_alloc = getenv(”T_MEMD_SLABS_ALLOC”);
        if (!pre_alloc || atoi(pre_alloc)) {
            slabs_preallocate(limit / POWER_BLOCK);
        }
    }
}
在1.2中,chunk大小表示为初始大小*f^n,f为factor,在memcached.c中定义,n为classid,同时,201个头不是全部都要初始化的,因为factor可变,初始化只循环到计算出的大小达到slab大小的一半为止,而且它是从id1开始的,即:id为1的slab,每chunk大小80字节,id为2的slab,每chunk大小80*f,id为3的slab,每chunk大小80*f^2,初始化大小有一个修正值CHUNK_ALIGN_BYTES,用来保证n-byte排列 (保证结果是CHUNK_ALIGN_BYTES的整倍数)。这样,在标准情况下,memcached1.2会初始化到id40,这个slab中每个chunk大小为504692,每个slab中有两个chunk。最后,slab_init函数会在最后补足一个id41,它是整块的,也就是这个slab中只有一个1MB大的chunk:
CODE:
void slabs_init(size_t limit, double factor) {
    int i = POWER_SMALLEST – 1;
    unsigned int size = sizeof(item) + settings.chunk_size;
    /* Factor of 2.0 means use the default memcached behavior */
    if (factor == 2.0 && size < 128)
        size = 128;
    mem_limit = limit;
    memset(slabclass, 0, sizeof(slabclass));
    while (++i < POWER_LARGEST && size <= POWER_BLOCK / 2) {
        /* Make sure items are always n-byte aligned */
        if (size % CHUNK_ALIGN_BYTES)
            size += CHUNK_ALIGN_BYTES – (size % CHUNK_ALIGN_BYTES);
        slabclass[i].size = size;
        slabclass[i].perslab = POWER_BLOCK / slabclass[i].size;
        size *= factor;
        if (settings.verbose > 1) {
            fprintf(stderr, “slab class %3d: chunk size %6d perslab %5d\n”,
                    i, slabclass[i].size, slabclass[i].perslab);
        }      
    }
    power_largest = i;
    slabclass[power_largest].size = POWER_BLOCK;
    slabclass[power_largest].perslab = 1;
    /* for the test suite:  faking of how much we’ve already malloc’d */
    {
        char *t_initial_malloc = getenv(”T_MEMD_INITIAL_MALLOC”);
        if (t_initial_malloc) {
            mem_malloced = atol(getenv(”T_MEMD_INITIAL_MALLOC”));
        }      
    }
#ifndef DONT_PREALLOC_SLABS
    {
        char *pre_alloc = getenv(”T_MEMD_SLABS_ALLOC”);
        if (!pre_alloc || atoi(pre_alloc)) {
            slabs_preallocate(limit / POWER_BLOCK);
        }
    }
#endif
}
由上可以看出,memcached的内存分配是有冗余的,当一个slab不能被它所拥有的chunk大小整除时,slab尾部剩余的空间就被丢弃了,如id40中,两个chunk占用了1009384字节,这个slab一共有1MB,那么就有39192字节被浪费了。
Memcached使用这种方式来分配内存,是为了可以快速的通过item长度定位出slab的classid,有一点类似hash,因为item的长度是可以计算的,比如一个item的长度是300字节,在1.2中就可以得到它应该保存在id7的slab中,因为按照上面的计算方法,id6的chunk大小是252字节,id7的chunk大小是316字节,id8的chunk大小是396字节,表示所有252到316字节的item都应该保存在id7中。同理,在1.1中,也可以计算得到它出于256和512之间,应该放在chunk_size为512的id9中(32位系统)。
Memcached初始化的时候,会初始化slab(前面可以看到,在main函数中调用了slabs_init())。它会在slabs_init()中检查一个常量DONT_PREALLOC_SLABS,如果这个没有被定义,说明使用预分配内存方式初始化slab,这样在所有已经定义过的slabclass中,每一个id创建一个slab。这样就表示,1.2在默认的环境中启动进程后要分配41MB的slab空间,在这个过程里,memcached的第二个内存冗余发生了,因为有可能一个id根本没有被使用过,但是它也默认申请了一个slab,每个slab会用掉1MB内存
当一个slab用光后,又有新的item要插入这个id,那么它就会重新申请新的slab,申请新的slab时,对应id的slab链表就要增长,这个链表是成倍增长的,在函数grow_slab_list函数中,这个链的长度从1变成2,从2变成4,从4变成8……:
CODE:
static int grow_slab_list (unsigned int id) {
    slabclass_t *p = &slabclass[id];
    if (p->slabs == p->list_size) {
        size_t new_size =  p->list_size ? p->list_size * 2 : 16;
        void *new_list = realloc(p->slab_list, new_size*sizeof(void*));
        if (new_list == 0) return 0;
        p->list_size = new_size;
        p->slab_list = new_list;
    }
    return 1;
}
在定位item时,都是使用slabs_clsid函数,传入参数为item大小,返回值为classid,由这个过程可以看出,memcached的第三个内存冗余发生在保存item的过程中,item总是小于或等于chunk大小的,当item小于chunk大小时,就又发生了空间浪费。

 

◎Memcached的NewHash算法
Memcached的item保存基于一个大的hash表,它的实际地址就是slab中的chunk偏移,但是它的定位是依靠对key做hash的结果,在primary_hashtable中找到的。在assoc.c和items.c中定义了所有的hash和item操作。
Memcached使用了一个叫做NewHash的算法,它的效果很好,效率也很高。1.1和1.2的NewHash有一些不同,主要的实现方式还是一样的,1.2的hash函数是经过整理优化的,适应性更好一些。
NewHash的原型参考:http://burtleburtle.net/bob/hash/evahash.html。数学家总是有点奇怪,呵呵~
为了变换方便,定义了u4和u1两种数据类型,u4就是无符号的长整形,u1就是无符号char(0-255)。
具体代码可以参考1.1和1.2源码包。
注意这里的hashtable长度,1.1和1.2也是有区别的,1.1中定义了HASHPOWER常量为20,hashtable表长为hashsize(HASHPOWER),就是4MB(hashsize是一个宏,表示1右移n位),1.2中是变量16,即hashtable表长65536:
CODE:
typedef  unsigned long  int  ub4;   /* unsigned 4-byte quantities */
typedef  unsigned       char ub1;   /* unsigned 1-byte quantities */
#define hashsize(n) ((ub4)1<<(n))
#define hashmask(n) (hashsize(n)-1)
在assoc_init()中,会对primary_hashtable做初始化,对应的hash操作包括:assoc_find()、assoc_expand()、assoc_move_next_bucket()、assoc_insert()、assoc_delete(),对应于item的读写操作。其中assoc_find()是根据key和key长寻找对应的item地址的函数(注意在C中,很多时候都是同时直接传入字符串和字符串长度,而不是在函数内部做strlen),返回的是item结构指针,它的数据地址在slab中的某个chunk上。
items.c是数据项的操作程序,每一个完整的item包括几个部分,在item_make_header()中定义为:
key:键
nkey:键长
flags:用户定义的flag(其实这个flag在memcached中没有启用)
nbytes:值长(包括换行符号\r\n)
suffix:后缀Buffer
nsuffix:后缀长
一个完整的item长度是键长+值长+后缀长+item结构大小(32字节),item操作就是根据这个长度来计算slab的classid的。
hashtable中的每一个桶上挂着一个双链表,item_init()的时候已经初始化了heads、tails、sizes三个数组为0,这三个数组的大小都为常量LARGEST_ID(默认为255,这个值需要配合factor来修改),在每次item_assoc()的时候,它会首先尝试从slab中获取一块空闲的chunk,如果没有可用的chunk,会在链表中扫描50次,以得到一个被LRU踢掉的item,将它unlink,然后将需要插入的item插入链表中。
注意item的refcount成员。item被unlink之后只是从链表上摘掉,不是立刻就被free的,只是将它放到删除队列中(item_unlink_q()函数)。
item对应一些读写操作,包括remove、update、replace,当然最重要的就是alloc操作。
item还有一个特性就是它有过期时间,这是memcached的一个很有用的特性,很多应用都是依赖于memcached的item过期,比如session存储、操作锁等。item_flush_expired()函数就是扫描表中的item,对过期的item执行unlink操作,当然这只是一个回收动作,实际上在get的时候还要进行时间判断:
CODE:
/* expires items that are more recent than the oldest_live setting. */
void item_flush_expired() {
    int i; 
    item *iter, *next;
    if (! settings.oldest_live)
        return;
    for (i = 0; i < LARGEST_ID; i++) {
        /* The LRU is sorted in decreasing time order, and an item’s timestamp
         * is never newer than its last access time, so we only need to walk
         * back until we hit an item older than the oldest_live time.
         * The oldest_live checking will auto-expire the remaining items.
         */
        for (iter = heads[i]; iter != NULL; iter = next) {
            if (iter->time >= settings.oldest_live) {
                next = iter->next;
                if ((iter->it_flags & ITEM_SLABBED) == 0) {
                    item_unlink(iter);
                }      
            } else {
                /* We’ve hit the first old item. Continue to the next queue. */
                break; 
            }      
        }      
    }
}
CODE:
/* wrapper around assoc_find which does the lazy expiration/deletion logic */
item *get_item_notedeleted(char *key, size_t nkey, int *delete_locked) {
    item *it = assoc_find(key, nkey);
    if (delete_locked) *delete_locked = 0;
    if (it && (it->it_flags & ITEM_DeleteD)) {
        /* it’s flagged as delete-locked.  let’s see if that condition
           is past due, and the 5-second delete_timer just hasn’t
           gotten to it yet… */
        if (! item_delete_lock_over(it)) {
            if (delete_locked) *delete_locked = 1;
            it = 0;
        }      
    }
    if (it && settings.oldest_live && settings.oldest_live <= current_time &&
        it->time <= settings.oldest_live) {
        item_unlink(it);
        it = 0;
    }
    if (it && it->exptime && it->exptime <= current_time) {
        item_unlink(it);
        it = 0;
    }
    return it;
}
Memcached的内存管理方式是非常精巧和高效的,它很大程度上减少了直接alloc系统内存的次数,降低函数开销和内存碎片产生几率,虽然这种方式会造成一些冗余浪费,但是这种浪费在大型系统应用中是微不足道的。

 

◎Memcached的理论参数计算方式
影响 memcached 工作的几个参数有:
常量REALTIME_MAXDELTA 60*60*24*30
最大30天的过期时间
conn_init()中的freetotal(=200)
最大同时连接数
常量KEY_MAX_LENGTH 250
最大键长
settings.factor(=1.25)
factor将影响chunk的步进大小
settings.maxconns(=1024)
最大软连接
settings.chunk_size(=48)
一个保守估计的key+value长度,用来生成id1中的chunk长度(1.2)。id1的chunk长度等于这个数值加上item结构体的长度(32),即默认的80字节。
常量POWER_SMALLEST 1
最小classid(1.2)
常量POWER_LARGEST 200
最大classid(1.2)
常量POWER_BLOCK 1048576
默认slab大小
常量CHUNK_ALIGN_BYTES (sizeof(void *))
保证chunk大小是这个数值的整数倍,防止越界(void *的长度在不同系统上不一样,在标准32位系统上是4)
常量ITEM_Update_INTERVAL 60
队列刷新间隔
常量LARGEST_ID 255
最大item链表数(这个值不能比最大的classid小)
变量hashpower(在1.1中是常量HASHPOWER)
决定hashtable的大小
根据上面介绍的内容及参数设定,可以计算出的一些结果:
1、在memcached中可以保存的item个数是没有软件上限的,之前我的100万的说法是错误的。
2、假设NewHash算法碰撞均匀,查找item的循环次数是item总数除以hashtable大小(由hashpower决定),是线性的。
3、Memcached限制了可以接受的最大item是1MB,大于1MB的数据不予理会。
4、Memcached的空间利用率和数据特性有很大的关系,又与DONT_PREALLOC_SLABS常量有关。 在最差情况下,有198个slab会被浪费(所有item都集中在一个slab中,199个id全部分配满)。

 

◎Memcached的定长优化
根据上面几节的描述,多少对memcached有了一个比较深入的认识。在深入认识的基础上才好对它进行优化。
Memcached本身是为变长数据设计的,根据数据特性,可以说它是“面向大众”的设计,但是很多时候,我们的数据并不是这样的“普遍”,典型的情况中,一种是非均匀分布,即数据长度集中在几个区域内(如保存用户 Session);另一种更极端的状态是等长数据(如定长键值,定长数据,多见于访问、在线统计或执行锁)。
这里主要研究一下定长数据的优化方案(1.2),集中分布的变长数据仅供参考,实现起来也很容易。
解决定长数据,首先需要解决的是slab的分配问题,第一个需要确认的是我们不需要那么多不同chunk长度的slab,为了最大限度地利用资源,最好chunk和item等长,所以首先要计算item长度。
在之前已经有了计算item长度的算法,需要注意的是,除了字符串长度外,还要加上item结构的长度32字节。
假设我们已经计算出需要保存200字节的等长数据。
接下来是要修改slab的classid和chunk长度的关系。在原始版本中,chunk长度和classid是有对应关系的,现在如果把所有的chunk都定为200个字节,那么这个关系就不存在了,我们需要重新确定这二者的关系。一种方法是,整个存储结构只使用一个固定的id,即只使用199个槽中的1个,在这种条件下,就一定要定义DONT_PREALLOC_SLABS来避免另外的预分配浪费。另一种方法是建立一个hash关系,来从item确定classid,不能使用长度来做键,可以使用key的NewHash结果等不定数据,或者直接根据key来做hash(定长数据的key也一定等长)。这里简单起见,选择第一种方法,这种方法的不足之处在于只使用一个id,在数据量非常大的情况下,slab链会很长(因为所有数据都挤在一条链上了),遍历起来的代价比较高。
前面介绍了三种空间冗余,设置chunk长度等于item长度,解决了第一种空间浪费问题,不预申请空间解决了第二种空间浪费问题,那么对于第一种问题(slab内剩余)如何解决呢,这就需要修改POWER_BLOCK常量,使得每一个slab大小正好等于chunk长度的整数倍,这样一个slab就可以正好划分成n个chunk。这个数值应该比较接近1MB,过大的话同样会造成冗余,过小的话会造成次数过多的alloc,根据chunk长度为200,选择1000000作为POWER_BLOCK的值,这样一个slab就是100万字节,不是1048576。三个冗余问题都解决了,空间利用率会大大提升。
修改 slabs_clsid 函数,让它直接返回一个定值(比如 1 ):
CODE:
unsigned int slabs_clsid(size_t size) {
        return 1;
}
修改slabs_init函数,去掉循环创建所有classid属性的部分,直接添加slabclass[1]:
CODE:
slabclass[1].size = 200;                //每chunk200字节
slabclass[1].perslab = 5000;        //1000000/200

 

◎Memcached客户端
Memcached是一个服务程序,使用的时候可以根据它的协议,连接到memcached服务器上,发送命令给服务进程,就可以操作上面的数据。为了方便使用,memcached有很多个客户端程序可以使用,对应于各种语言,有各种语言的客户端。基于C语言的有libmemcache、APR_Memcache;基于Perl的有Cache::Memcached;另外还有Python、Ruby、Java、C#等语言的支持。PHP的客户端是最多的,不光有mcache和PECL memcache两个扩展,还有大把的由PHP编写的封装类,下面介绍一下在PHP中使用memcached的方法:
mcache扩展是基于libmemcache再封装的。libmemcache一直没有发布stable版本,目前版本是1.4.0-rc2,可以在这里找到。libmemcache有一个很不好的特性,就是会向stderr写很多错误信息,一般的,作为lib使用的时候,stderr一般都会被定向到其它地方,比如Apache的错误日志,而且libmemcache会自杀,可能会导致异常,不过它的性能还是很好的。
mcache扩展最后更新到1.2.0-beta10,作者大概是离职了,不光停止更新,连网站也打不开了(~_~),只能到其它地方去获取这个不负责的扩展了。解压后安装方法如常:phpize & configure & make & make install,一定要先安装libmemcache。使用这个扩展很简单:
CODE:
<?php
$mc = memcache();    // 创建一个memcache连接对象,注意这里不是用new!
$mc->add_server(‘localhost’, 11211);    // 添加一个服务进程
$mc->add_server(‘localhost’, 11212);    // 添加第二个服务进程
$mc->set(‘key1′, ‘Hello’);    // 写入key1 => Hello
$mc->set(‘key2′, ‘World’, 10);    // 写入key2 => World,10秒过期
$mc->set(‘arr1′, array(‘Hello’, ‘World’));    // 写入一个数组
$key1 = $mc->get(‘key1′);    // 获取’key1′的值,赋给$key1
$key2 = $mc->get(‘key2′);    // 获取’key2′的值,赋给$key2,如果超过10秒,就取不到了
$arr1 = $mc->get(‘arr1′);    // 获取’arr1′数组
$mc->delete(‘arr1′);    // 删除’arr1′
$mc->flush_all();    // 删掉所有数据
$stats = $mc->stats();    // 获取服务器信息
var_dump($stats);    // 服务器信息是一个数组
?>
这个扩展的好处是可以很方便地实现分布式存储和负载均衡,因为它可以添加多个服务地址,数据在保存的时候是会根据hash结果定位到某台服务器上的,这也是libmemcache的特性。libmemcache支持集中hash方式,包括CRC32、ELF和Perl hash。
PECL memcache是PECL发布的扩展,目前最新版本是2.1.0,可以在pecl网站得到。memcache扩展的使用方法可以在新一些的PHP手册中找到,它和mcache很像,真的很像:
CODE:
<?php
$memcache = new Memcache;
$memcache->connect(‘localhost’, 11211) or die (“Could not connect”);
$version = $memcache->getVersion();
echo “Server’s version: ”.$version.“n”;
$tmp_object = new stdClass;
$tmp_object->str_attr = ‘test’;
$tmp_object->int_attr = 123;
$memcache->set(‘key’, $tmp_object, false, 10) or die (“Failed to save data at the server”);
echo “Store data in the cache (data will expire in 10 seconds)n”;
$get_result = $memcache->get(‘key’);
echo “Data from the cache:n”; var_dump($get_result);
?>
这个扩展是使用php的stream直接连接memcached服务器并通过socket发送命令的。它不像libmemcache那样完善,也不支持add_server这种分布操作,但是因为它不依赖其它的外界程序,兼容性要好一些,也比较稳定。至于效率,差别不是很大。
另外,有很多的PHP class可以使用,比如MemcacheClient.inc.php,phpclasses.org上可以找到很多,一般都是对perl client API的再封装,使用方式很像。

◎BSM_Memcache
从C client来说,APR_Memcache是一个很成熟很稳定的client程序,支持线程锁和原子级操作,保证运行的稳定性。不过它是基于APR的(APR将在最后一节介绍),没有libmemcache的应用范围广,目前也没有很多基于它开发的程序,现有的多是一些Apache Module,因为它不能脱离APR环境运行。但是APR倒是可以脱离Apache单独安装的,在APR网站上可以下载APR和APR-util,不需要有Apache,可以直接安装,而且它是跨平台的。
BSM_Memcache是我在BS.Magic项目中开发的一个基于APR_Memcache的PHP扩展,说起来有点拗口,至少它把APR扯进了PHP扩展中。这个程序很简单,也没做太多的功能,只是一种形式的尝试,它支持服务器分组。
和mcache扩展支持多服务器分布存储不同,BSM_Memcache支持多组服务器,每一组内的服务器还是按照hash方式来分布保存数据,但是两个组中保存的数据是一样的,也就是实现了热备,它不会因为一台服务器发生单点故障导致数据无法获取,除非所有的服务器组都损坏(例如机房停电)。当然实现这个功能的代价就是性能上的牺牲,在每次添加删除数据的时候都要扫描所有的组,在get数据的时候会随机选择一组服务器开始轮询,一直到找到数据为止,正常情况下一次就可以获取得到。
BSM_Memcache只支持这几个函数:
CODE:
zend_function_entry bsm_memcache_functions[] =
{
    PHP_FE(mc_get,          NULL)
    PHP_FE(mc_set,          NULL)
    PHP_FE(mc_del,          NULL)
    PHP_FE(mc_add_group,    NULL)
    PHP_FE(mc_add_server,   NULL)
    PHP_FE(mc_shutdown,     NULL)
    {NULL, NULL, NULL}
};
mc_add_group函数返回一个整形(其实应该是一个object,我偷懒了~_~)作为组ID,mc_add_server的时候要提供两个参数,一个是组ID,一个是服务器地址(ADDRORT)。
CODE:
/**
* Add a server group
*/
PHP_FUNCTION(mc_add_group)
{
    apr_int32_t group_id;
    apr_status_t rv;
    if (0 != ZEND_NUM_ARGS())
    {
        WRONG_PARAM_COUNT;
        RETURN_NULL();
    }
    group_id = free_group_id();
    if (-1 == group_id)
    {
        RETURN_FALSE;
    }
    apr_memcache_t *mc;
    rv = apr_memcache_create(p, MAX_G_SERVER, 0, &mc);
    add_group(group_id, mc);
    RETURN_DOUBLE(group_id);
}
CODE:
/**
* Add a server into group
*/
PHP_FUNCTION(mc_add_server)
{
    apr_status_t rv;
    apr_int32_t group_id;
    double g;
    char *srv_str;
    int srv_str_l;
    if (2 != ZEND_NUM_ARGS())
    {
        WRONG_PARAM_COUNT;
    }
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, “ds”, &g, &srv_str, &srv_str_l) == FAILURE)
    {
        RETURN_FALSE;
    }
    group_id = (apr_int32_t) g;
    if (-1 == is_validate_group(group_id))
    {
        RETURN_FALSE;
    }
    char *host, *scope;
    apr_port_t port;
    rv = apr_parse_addr_port(&host, &scope, &port, srv_str, p);
    if (APR_SUCCESS == rv)
    {
        // Create this server object
        apr_memcache_server_t *st;
        rv = apr_memcache_server_create(p, host, port, 0, 64, 1024, 600, &st);
        if (APR_SUCCESS == rv)
        {
            if (NULL == mc_groups[group_id])
            {
                RETURN_FALSE;
            }
            // Add server
            rv = apr_memcache_add_server(mc_groups[group_id], st);
            if (APR_SUCCESS == rv)
            {
                RETURN_TRUE;
            }
        }
    }
    RETURN_FALSE;
}
在set和del数据的时候,要循环所有的组:
CODE:
/**
* Store item into all groups
*/
PHP_FUNCTION(mc_set)
{
    char *key, *value;
    int key_l, value_l;
    double ttl = 0;
    double set_ct = 0;
    if (2 != ZEND_NUM_ARGS())
    {
        WRONG_PARAM_COUNT;
    }
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, “ss|d”, &key, &key_l, &value, &value_l, ttl) == FAILURE)
    {
        RETURN_FALSE;
    }
    // Write data into every object
    apr_int32_t i = 0;
    if (ttl < 0)
    {
        ttl = 0;
    }
    apr_status_t rv;
    for (i = 0; i < MAX_GROUP; i++)
    {
        if (0 == is_validate_group(i))
        {
            // Write it!
            rv = apr_memcache_add(mc_groups[i], key, value, value_l, (apr_uint32_t) ttl, 0);
            if (APR_SUCCESS == rv)
            {
                set_ct++;
            }
        }
    }
    RETURN_DOUBLE(set_ct);
}
在mc_get中,首先要随机选择一个组,然后从这个组开始轮询:
CODE:
/**
* Fetch a item from a random group
*/
PHP_FUNCTION(mc_get)
{              
    char *key, *value = NULL;
    int key_l;
    apr_size_t value_l;
    if (1 != ZEND_NUM_ARGS())
    {
        WRONG_PARAM_COUNT;
    }
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, “s”, &key, &key_l) == FAILURE)
    {
        RETURN_MULL();
    }
    // I will try …
    // Random read
    apr_int32_t curr_group_id = random_group();
    apr_int32_t i = 0;
    apr_int32_t try = 0;
    apr_uint32_t flag;
    apr_memcache_t *oper;
    apr_status_t rv;
    for (i = 0; i < MAX_GROUP; i++)
    {
        try = i + curr_group_id;
        try = try % MAX_GROUP;
        if (0 == is_validate_group(try))
        {
            // Get a value
            oper = mc_groups[try];
            rv = apr_memcache_getp(mc_groups[try], p, (const char *) key, &value, &value_l, 0);
            if (APR_SUCCESS == rv)
            {
                RETURN_STRING(value, 1);
            }
        }
    }
    RETURN_FALSE;
}
CODE:
/**
* Random group id
* For mc_get()
*/
apr_int32_t random_group()
{
    struct timeval tv;
    struct timezone tz;
    int usec;
    gettimeofday(&tv, &tz);
    usec = tv.tv_usec;
    int curr = usec % count_group();
    return (apr_int32_t) curr;
}
BSM_Memcache的使用方式和其它的client类似:
CODE:
<?php
$g1 = mc_add_group();    // 添加第一个组
$g2 = mc_add_group();    // 添加第二个组
mc_add_server($g1, ‘localhost:11211′);    // 在第一个组中添加第一台服务器
mc_add_server($g1, ‘localhost:11212′);    // 在第一个组中添加第二台服务器
mc_add_server($g2, ‘10.0.0.16:11211′);    // 在第二个组中添加第一台服务器
mc_add_server($g2, ‘10.0.0.17:11211′);    // 在第二个组中添加第二台服务器
mc_set(‘key’, ‘Hello’);    // 写入数据
$key = mc_get(‘key’);    // 读出数据
mc_del(‘key’);    // 删除数据
mc_shutdown();    // 关闭所有组
?>
APR_Memcache的相关资料可以在这里找到,BSM_Memcache可以在本站下载。

 

◎APR环境介绍
APR的全称:Apache Portable Runtime。它是Apache软件基金会创建并维持的一套跨平台的C语言库。它从Apache httpd1.x中抽取出来并独立于httpd之外,Apache httpd2.x就是建立在APR上。APR提供了很多方便的API接口可供使用,包括如内存池、字符串操作、网络、数组、hash表等实用的功能。开发Apache2 Module要接触很多APR函数,当然APR可以独立安装独立使用,可以用来写自己的应用程序,不一定是Apache httpd的相关开发。

 

◎后记
这是我在农历丙戌年(我的本命年)的最后一篇文章,由于Memcached的内涵很多,仓促整理一定有很多遗漏和错误。感谢新浪网提供的研究机会,感谢部门同事的帮助。

2009-08-18关注学习

常用开源Jabber(XMPP) 服务器(即时通讯)介绍已关闭评论

常用开源Jabber(XMPP) 服务器(即时通讯)介绍

1. Openfire (Wildfire) 3.x
授权: GPL or 商用
操作系统平台:所有(使用Java开发)
XMPP Jabber 协议实现情况:98%
Tim 评价:
安装和使用非常简单,安装后进入Web界面进行2~3分钟的配置所有的东西都设好了。使用Java语言开发,在目前Java开发人员到处普及的情况
下进行维护和扩展的成本非常低。在我的测试中加上 Connection Manager 的情况下单台服务器可支持 30 万并发用户。缺点是目前还不支持
cluster。如果企业内部部署IM使用 Wildfire + Spark 是最佳的组合。
安装方法见:http://www.blog-dragon.com/2007/10/jabber-xmpp-openfire-spark.html
2. ejabberd
授权: GPL
操作系统平台:Linux, Windows, MacOS X 等
XMPP Jabber 协议实现情况:91%
Tim 评价:
Ejabberd目前是可扩展性最好的一种Jabber/XMPP服务器,支持分布多个服务器,并且具有容错处理,单台服务器失效不影响整个cluster运作。
顾虑就是它采用一种大家都没听过的语言Erlang开发,所以很多人可能会象我一样因为这个原因放弃了它。
官方地址:http://www.process-one.net/en/ejabberd/
3. Jabberd 2.x
授权: GPL
操作系统平台:主要是 Linux, (Windows 也支持,但不知道性能怎样)
XMPP Jabber 协议实现情况:76%
Tim 评价:
自从jabber.org改用ejabberd之后,Jabberd一直都在走下坡路。扩展性比不上ejabberd, 易用性比不上 Wildfire,唯一的优势是使用C开发,
如果你坚持要用C开发,那么还是选择jabberd吧。
官网地址:http://jabberd.jabberstudio.org/2/
4. Jabberd 1.x
授权: GPL
操作系统平台:主要是 Linux, (Windows 也支持,但不知道性能怎样)
XMPP Jabber 协议实现情况:45%
Tim 评价:
在几年前 jabberd 就是 Jabber 的代名词,至今很多 Jabber 文档仍然介绍的是 Jabber 1.4,
在我以前写的《Jabber 服务器占有率比较》中仍然排名第一。但是它很多新的规范都不支持,相信大部分用户都将转向新的服务器。
官网地址:http://jabberd.jabberstudio.org/1.4/
5. 后起之秀 DJabberd
授权: open source
操作系统平台:主要是 Linux, (Perl写的,其他平台应该也支持)
XMPP Jabber 协议实现情况:N/A
Tim 评价:
djabberd 使用 epoll 技术,理论上单台服务器可以支持更多用户。Djabberd目前主要应用在LiveJournal上,大部分XMPP协议都支持,稳定性
也不用置疑。但是因为推出时间尚短,很多细节功能可能需要时间慢慢完善

2009-08-18关注学习

Perlbal简介已关闭评论

Perlbal简介

Perlbal 是一个用 Perl 编写的单线程的事件驱动服务器,可充当 Web 服务器 和 HTTP 负载均衡。

Perlbal被使用在 http://www.livejournal.com/

 

特点

  • 快,小,可管理的http web 服务器/代理
  • 可以在内部进行转发
  • 使用Perl开发
  • 单线程,异步,基于事件,使用epoll , kqueue
  • 支持Console管理与http远程管理,支持动态配置加载
  • 多种模式:web服务器,反向代理,插件
  • 支持插件:GIF/PNG互换?

http://www.danga.com/perlbal/

2009-08-18关注学习

mogileFS简介已关闭评论

mogileFS简介

mogileFS是一个分布式文件存储的解决方案,他由Six Apart开发下面列出了他的一些特性(由mogileFS页面http://www.danga.com/mogilefs/ 介绍翻译而来)

* 应用层——不需要特殊的核心组件
* 无单点失败——MogileFS安装的三个组件(存储节点、跟踪器、跟踪用的数据库),均可运行在多个 机器上,因此没有单点失败。(你也可以将跟踪器和存储节点运行在同一台机器上,这样你就没有必要用4台机器)推荐至少两台机器。
* 自动的文件复制——基于不同的文件“分类”,文件可以被自动的复制到多个有足够存储空间的存储节点上,这样可以满足这个“类别”的最少复制要求。比如你有一个图片网站,你可以设置原始的JPEG图片需要复制至少三份,但实际只有1or2份拷贝,如果丢失了数据,那么Mogile可以重新建立遗失的拷贝数。用这种办法,MogileFS(不做RAID)可以节约 磁盘,否则你将存储同样的拷贝多份,完全没有必要。
* “比RAID好多了”——在一个非存储区域网络的RAID(non-SAN RAID)的建立中,磁盘是冗余的,但主机不是,如果你整个机器坏了,那么文件也将不能访问。 MogileFS在不同的机器之间进行文件复制,因此文件始终是可用的。
* 传输中立,无特殊协议——MogileFS客户端可以通过NFS或HTTP来和MogileFS的存储节点来通信,但首先需要告知跟踪器一下。
* 简单的命名空间——文件通过一个给定的key来确定,是一个全局的命名空间。你可以自己生成多个命名空间,只要你愿意,不过这样可能在同一MogileFS中会造成key冲突。
* 不用共享任何东西——MogileFS不需要依靠昂贵的SAN来共享磁盘,每个机器只用维护好自己的磁盘。
* 不需要RAID——在MogileFS中的磁盘可以是做了RAID的也可以是没有,如果是为了安全性着想的话RAID没有必要买了,因为MogileFS已经提供了。
* 不会碰到文件系统本身的不可知情况——在MogileFS中的存储节点的磁盘可以被格式化成多种格式(ext3,reiserFS等等)。MogilesFS会做自己内部目录的哈希,所以它不会碰到文件系统本身的一些限制,比如一个目录中的最大文件数。你可以放心的使用。

Mogilefs 的网站地址(http:// www.danga.com /mogilefs )

php 扩展 的地址(http://www.capoune.net/mogilefs/ )提供了一个php扩展用来在php中使用mogileFS。
这儿也有一个地址,svn的源码库 http://svn.usrportage.de/php-mogilefs/trunk/

mogileFS 安装步骤( http://durrett.net/mogilefs_setup.html
mogileFS 使用perl 编写的,在安装前你应该先安装好perl。同时mogileFS也需要一个数据库用来保存文件数据的跟踪信息(目前好像可以使用MySQL推荐 , SQLite,Oracle,Postsql)。

这儿有一个兄弟的中文安装学习笔记 mogileFS学习

mogileFS 适合于静态存储,就是那种一次保存,多次读取型的资源,比如以html方式静态化处理的动态文件,图片文件,其他只提供下载的文件等。

2009-08-17生活琐记

《MySQL性能调优与架构设计》示例库结构脚本已关闭评论

《MySQL性能调优与架构设计》示例库结构脚本

示例库结构脚本的所有创建语句:

 

–创建数据库
Create DATABASE example;

–创建表
–索引是根据应用中使用的 Query 的情况而决定,所有表开始都仅仅只有主键,没有其他的索引
–各表没有特定存储引擎和字符集,各位读者朋友可根据自己的环境决定

Create TABLE event (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
user_id int(11) NOT NULL,
event_type varchar(8) NOT NULL,
event_msg varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);

–组内讨论信息summary

Create TABLE group_message (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
group_id int(11) NOT NULL,
user_id int(11) NOT NULL,
author varchar(32) NOT NULL,
subject varchar(128) NOT NULL,
PRIMARY KEY (id)
);

–组内讨论信息内容

Create TABLE group_message_content (
group_msg_id int(11) NOT NULL,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
content text NOT NULL,
PRIMARY KEY (group_msg_id)
);

–组信息

Create TABLE groups (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
name varchar(32) NOT NULL,
status varchar(16) NOT NULL,
description varchar(1024) DEFAULT NULL,
PRIMARY KEY (id)
);

–特殊消息

Create TABLE message (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
subject varchar(64) NOT NULL,
status varchar(16) NOT NULL,
PRIMARY KEY (id)
);

–消息

Create TABLE message_content (
msg_id int(11) NOT NULL,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
content varchar(512) DEFAULT NULL,
PRIMARY KEY (msg_id)
);

–照片表

Create TABLE photo (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
user_id int(11) NOT NULL,
status varchar(16) NOT NULL,
subject varchar(64) NOT NULL,
url varchar(64) NOT NULL,
PRIMARY KEY (id)
);

–相册表

Create TABLE photo_album (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
album_name varchar(64) NOT NULL,
user_id int(11) NOT NULL,
PRIMARY KEY (id)
);

–照片相册关系表

Create TABLE photo_album_relation (
id int(11) NOT NULL auto_increment,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
album_id int(11) NOT NULL,
photo_id int(11) NOT NULL,
PRIMARY KEY (id)
);

–照片回复信息表

Create TABLE photo_comment (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
photo_id int(11) NOT NULL,
comments varchar(512) NOT NULL,
PRIMARY KEY (id)
);

–系统各组广播消息表

Create TABLE top_message (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
user_id int(11) NOT NULL,
author varchar(32) NOT NULL,
subject varchar(128) NOT NULL,
PRIMARY KEY (id)
);

–用户基本信息表

Create TABLE user (
id int(11) NOT NULL AUTO_INCREMENT,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
nick_name varchar(32) NOT NULL,
password char(64) DEFAULT NULL,
email_b varchar(32) DEFAULT NULL,
status varchar(16) DEFAULT NULL,
PRIMARY KEY (id)
);

–用户个人详细信息表

Create TABLE user_profile (
user_id int(11) NOT NULL,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
sexuality char(1) NOT NULL,
msn varchar(32) DEFAULT NULL,
sign varchar(64) DEFAULT NULL,
birthday date DEFAULT NULL,
hobby varchar(64) DEFAULT NULL,
location varchar(64) DEFAULT NULL,
description varchar(1024) DEFAULT NULL,
PRIMARY KEY (user_id)
);

–用户分组表

Create TABLE user_group (
user_id int(11) NOT NULL,
gmt_create datetime NOT NULL,
gmt_modified datetime NOT NULL,
group_id int(11) NOT NULL,
user_type int(11) NOT NULL,
status varchar(16) NOT NULL,
PRIMARY KEY (user_id,group_id)
);