Apr3

网站加速--服务器编写篇1【转自架构师杨建】

Author: 杨建  Click: 6297   Comments: 0 Category: 架构  Tag: 网站加速,服务器
--提升性能的同时为你节约10倍以上成本
From: http://blog.sina.com.cn/iyangjian

一,如何节约CPU
二,怎样使用内存
三,减少磁盘I/O
四,优化你的网卡
五,调整内核参数
六,衡量Web Server的性能指标
七,NBA js直播的发展历程
八,新浪财经实时行情系统的历史遗留问题 (7 byte = 10.68w RMB/year)
  -----------------------------------------------------------------------------------------

一,如何节约CPU

1,选择一个好的I/O模型(epoll, kqueue)
3年前,我们还关心c10k问题,随着硬件性能的提升,那已经不成问题,但如果想让PIII 900服务器支撑5w+ connections,还是需要些能耐的。

epoll最擅长的事情是监视大量闲散连接,批量返回可用描述符,这让单机支撑百万connections成为可能。linux 2.6以上开始支持epoll,freebsd上相应的有kqueue,不过我个人偏爱linux,不太关心kqueue。

边缘触发ET 和 水平触发LT 的选择:
早期的文档说ET很高效,但是有些冒进。但事实上LT使用过程中,我苦恼了将近一个月有余,一不留神CPU 利用率99%了,可能是我没处理好。后来zhongying同学帮忙把驱动模式改成了ET模式,ET既高效又稳定。

简单地说,如果你有数据过来了,不去取LT会一直骚扰你,提醒你去取,而ET就告诉你一次,爱取不取,除非有新数据到来,否则不再提醒。

重点说下ET,非阻塞模式,
man手册说,如果ET提示你有数据可读的时候,你应该连续的读一直读到返回 EAGAIN or EWOULDBLOCK 为止,但是在具体实现中,我并没有这样做,而是根据我的应用做了优化。因为现在操作系统绝大多数实现都是最大传输单元值为1500。  MTU:1500 - ipheader:20 - tcpheader:20 = 1460 byte .  
HTTP header,不带cookie的话一般只有500+ byte。留512给uri,也基本够用,还有节余。

如果请求的header恰巧比这大是2050字节呢?
会有两种情况发生:1,数据紧挨着同时到达,一次read就搞定。 2,分两个ethernet frame先后到达有一定时间间隔。
我的方法是,用一个比较大的buffer比如1M去读header,如果你很确信你的服务对象请求比1460小,读一次就行。如果请求会很大分几个ethernet frame先后到达,也就是恰巧你刚刚read过,它又来一个新数据包,ET会再次返回,再处理下就是了。

顺便再说下写数据,一般一次可以write十几K数据到内核缓冲区。
所以对于很多小的数据文件服务来说,是没有必要另外为每个connections分配发送缓冲区。
只有当一次发送不完时候才分配一块内存,将数据暂存,待下次返回可写时发送。
这样避免了一次内存copy,而且节约了内存。

选择了epoll并不代表就就拥有了一个好的 I/O模型,用的不好,你还赶不上select,这是实话。
epoll的问题我就说这么多,关于描述符管理方面的细节请参见我早期的一个帖子,epoll模型的使用及其描述符耗尽问题的探讨  大概讨论了18页,我刚才把解决方法放在第一个帖子里了。如果你对epoll有感兴趣,我这有 一个简单的基于epoll的web server例子:
[code="cpp"]
/*-------------------------------------------------------------------------------------------------
gcc -o httpd httpd.c -lpthread
author: wyezl
2006.4.28
---------------------------------------------------------------------------------------------------*/

#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>

#define PORT 8888
#define MAXFDS 5000
#define EVENTSIZE 100

#define BUFFER "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\nContent-Type: text/html\r\n\r\nHello"

int epfd;
void *serv_epoll(void *p);
void setnonblocking(int fd)
{
int opts;
opts=fcntl(fd, F_GETFL);
if (opts < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
opts = opts | O_NONBLOCK;
if(fcntl(fd, F_SETFL, opts) < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
return;
}

int main(int argc, char *argv[])
{
int fd, cfd,opt=1;
struct epoll_event ev;
struct sockaddr_in sin, cin;
socklen_t sin_len = sizeof(struct sockaddr_in);
pthread_t tid;
pthread_attr_t attr;

epfd = epoll_create(MAXFDS);
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) <= 0)
{
fprintf(stderr, "socket failed\n");
return -1;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt));

memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_port = htons((short)(PORT));
sin.sin_addr.s_addr = INADDR_ANY;
if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) != 0)
{
fprintf(stderr, "bind failed\n");
return -1;
}
if (listen(fd, 32) != 0)
{
fprintf(stderr, "listen failed\n");
return -1;
}

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, serv_epoll, NULL) != 0)
{
fprintf(stderr, "pthread_create failed\n");
return -1;
}

while ((cfd = accept(fd, (struct sockaddr *)&cin, &sin_len)) > 0)
{
setnonblocking(cfd);
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
//printf("connect from %s\n",inet_ntoa(cin.sin_addr));
//printf("cfd=%d\n",cfd);
}

if (fd > 0)
close(fd);
return 0;
}

void *serv_epoll(void *p)
{
int i, ret, cfd, nfds;;
struct epoll_event ev,events[EVENTSIZE];
char buffer[512];

while (1)
{
nfds = epoll_wait(epfd, events, EVENTSIZE , -1);
//printf("nfds ........... %d\n",nfds);
for (i=0; i<nfds; i++)
{
if(events[i].events & EPOLLIN)
{
cfd = events[i].data.fd;
ret = recv(cfd, buffer, sizeof(buffer),0);
//printf("read ret..........= %d\n",ret);

ev.data.fd = cfd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, cfd, &ev);
}
else if(events[i].events & EPOLLOUT)
{
cfd = events[i].data.fd;
ret = send(cfd, BUFFER, strlen(BUFFER), 0);
//printf("send ret...........= %d\n", ret);

ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, &ev);
//shutdown(cfd, 1);
close(cfd);

}
}
}
return NULL;
}
[/code]

另外你要使用多线程,还是多进程,这要看你更熟悉哪个,各有好处。
多进程模式,单个进程crash了,不影响其他进程,而且可以为每个worker分别帮定不同的cpu,让某些cpu单独空出来处理中断和系统事物。多线程,共享数据方便,占用资源更少。进程或线程的个数,应该固定在 (cpu核数-1) ~ 2倍cpu核数间为宜,太多了时间片轮转时会频繁切换,少了,达不到多核并发处理的效果。

还有如何accept也是一门学问,没有最好,只有更适用,你需要做很多实验,确定对自己最高效的方式。有了一个好的I/O框架,你的效率想低也不容易,这是程序实现的大局。

关于更多网络I/O模型的讨论请见 中文版
另外,必须强调的是,代码和结构应该简洁高效,一定要具体问题具体分析,没什么法则是万能的,要根据你的服务量身定做。

2,关闭不必要的标准输入和标准输出
close(0);  //stdin
close(1);  //stdout
如果你不小心,有了printf输出调试信息,这绝对是一个性能杀手。
一个高性能的服务器不出错是不应该有任何输出的,免得耽误干活。
这样做,至少能为你节约两个描述符资源。

3,避免用锁 (i++ or ++i )
多线程编程用锁是普遍现象,貌似已经成为习惯。
但各线程最好是独立的,不需要同步机制的。
锁会消耗资源,而且造成排队,甚至死锁,尽量想办法避免。
非用不可时候,比如,实时统计各线程的负载情况,多个线程要对全局变量进行写操作。
请用 ++i ,因为它是一个原子操作。

4,减少系统调用
系统调用是很耗的,因为它通常需要钻进内核再钻出来。
我们应该避免用户空间和内核空间的切换。
比如我要为每个请求打个时间戳,以计算超时,我完全可以在返回一批可用描述符前只调用一次time(),而不用每个请求都调用一次。 time()只精确到秒,一批请求处理都是毫秒级,所以也没必要那么做,再说了,计算超时误差那么一秒有什么影响吗?

5, Connection: close vs  Keep-Alive ?
谈httpd实现,就不能不提长连接Keep-Alive 。
Keep-Alive是http 1.1中加入的,现在的浏览器99。99%应该都是支持Keep-Alive的。

先说下什么是Keep-Alive:
这是基于tcp的connections说的,也就是一个描述符(fd),它并不代表独立占用一个进程或线程。一个线程用非阻塞模式可以保持成千上万个长连接。

先说一个完整的HTTP 1.0的请求和响应:
建立tcp连接 (syn; ack, syn2; ack2; 三个分组握手完成)
请求
响应
关闭连接 (fin; ack; fin2; ack2  四个分组关闭连接)

再说HTTP 1.1的请求和响应:
建立tcp连接 (syn; ack, syn2; ack2; 三个分组握手完成)
请求
响应
...
...

请求
响应
关闭连接 (fin; ack; fin2; ack2  四个分组关闭连接)

如果请求和响应都只有一个分组,那么HTTP 1.0至少要传输11个分组(补充:请求和响应数据还各需要一个ack确认),才拿到一个分组的数据。而长连接可以更充分的利用这个已经建立的连接,避免的频繁的建立和关闭连接,减少网络拥塞。

我做过一个测试,在2cpu*4core服务器上,不停的accept,然后不做处理,直接close掉。一秒最多可以accept  7w/s,这是极限。那么我要是想每秒处理10w以上的http请求该怎么办呢?
目前唯一的也是最好的选择,就是保持长连接。
比如我们NBA JS直播页面,刚打开就会向我的js服务器发出6个http请求,而且随后平均每10秒会产生两个请求。再比如,我们很多页面都会嵌几个静态池的图片,如果每个请求都是独立的(建立连接然后关闭),那对资源绝对是个浪费。

长连接是个好东西,但是选择 Keep-Alive必须根据你的应用决定。比如NBA JS直播,我肯定10秒内会产生一个请求,所以超时设置为15秒,15秒还没活动,估计是去打酱油了,资源就得被我回收。超时设置过长,光连接都能把你的服务器堆死。

为什么有些apache服务器,负载很高,把Keep-Alive关掉负载就减轻了呢?
apache 有两种工作模式,prefork和worker。apache 1.x只有,prefork。
prefork比较典型,就是个进程池,每次创建一批进程,还有apache是基于select实现的。在用户不是太多的时候,长连接还是很有用的,可以节约分组,提升响应速度,但是一旦超出某个平衡点,由于为了保持很多长连接,创建了太多的进程,导致系统不堪重负,内存不够了,开始换入换出,cpu也被很多进程吃光了,load上去了。这种情况下,对apache来说,每次请求重新建立连接要比保持这么多长连接和进程更划算。


6,预处理 (预压缩,预取lastmodify,mimetype)
预处理,原则就是,能预先知道的结果,我们绝不计算第二次。

预压缩:我们在两三年前就开始使用预压缩技术,以节约CPU,伟大的微软公司在现在的IIS 7中也开始使用了。所谓的预压缩就是,从数据源头提供的就是预先压缩好的数据,IDC同步传输中是压缩状态,直到最后web server输出都是压缩状态,最终被用户浏览器端自动解压。

预取lastmodify:  文件的lastmodify时间,如果不更新,我们不应该取第二次,别忘记了fsat这个系统调用是很耗的。

预取mimetype: mimetype,如果你的文件类型不超过256种,一个字节就可以标识它,然后用数组下标直接输出,而且不是看到一个js文件,然后strcmp()了近百种后缀名后,才知道应该输出Content-Type: application/x-javascript,而且这种方法会随文件类型增加而耗费更多cpu资源。当然也可以写个hash函数来做这事,那也至少需要一次函数调用,做些求值运算,和分配比实际数据大几倍的hash表。

如何更好的使用cpu一级缓存
数据分解
CPU硬亲和力的设置
待补充。。。。

二,怎样使用内存

1,避免内存copy (strcpy,memcpy)
虽然内存速度很快,但是执行频率比较高的核心部分能避免copy的就尽量别使用。如果必须要copy,尽量使用memcpy替代sprintf,strcpy,因为它不关心你是否遇到'\0'; 内存拷贝和http响应又涉及到字符串长度计算。如果能预先知道这个长度最好用中间变量保留,增加多少直接加上去,不要用strlen()去计算,因为它会数数直到遇见'\0'。能用sizeof()的地方就不要用strlen,因为它是个运算符,在预编的时被替换为具体数字,而非执行时计算。

2,避免内核空间和用户进程空间内存copy (sendfile, splice and tee)
sendfile: 它的威力在于,它为大家提供了一种访问当前不断膨胀的Linux网络堆栈的机制。这种机制叫做“零拷贝(zero-copy)”,这种机制可以把“传输控制协议(TCP)”框架直接的从主机存储器中传送到网卡的缓存块(network card buffers)中去,避免了两次上下文切换。详细参见 <使用sendfile()让数据传输得到最优化> 。据同事测试说固态硬盘SSD对于小文件的随机读效率很高,对于更新不是很频繁的图片服务,读却很多,每个文件都不是很大的话,sendfile+SSD应该是绝配。

splice and tee: splice背后的真正概念是暴露给用户空间的“随机内核缓冲区”的概念。“也就是说,splice和tee运行在用户控制的内核缓冲区上,在这个缓冲区中,splice将来自任意文件描述符的数据传送到缓冲区中(或从缓冲区传送到文件描述符),而tee将一个缓冲区中的数据复制到另一个缓冲区中。因此,从一个很真实(而抽象)的意义上讲,splice相当于内核缓冲区的read/write,而tee相当于从内核缓冲区到另一个内核缓冲区的memcpy。”。本人觉得这个技术用来做代理,很合适。因为数据可以直接从一个soket到另一个soket,不需要经用户和内核空间的切换。这是sendfile不支持的。详细参见 ,具体实例请参见  man 2  tee ,里面有个完整的程序。

3,如何清空一块内存(memset ?)
比如有一个buffer[1024*1024],我们需要把它清空然后strcat(很多情况下可以通过记录写的起始位置+memcpy来代替)追加填充字符串。
其实我们没有必要用memset(buffer,0x00,sizeof(buffer))来清空整个buffer, memset(buffer,0x00,1)就能达到目的。 我平时更喜欢用buffer[0]='\0'; 来替代,省了一次函数调用的开销。

4,内存复用  (有必要为每个响应分配内存 ?)
对于NBA JS服务来说,我们返回的都是压缩数据,99%都不超过15k,基本一次write就全部出去了,是没有必要为每个响应分配内存的,公用一个buffer就够了。如果真的遇到大数据,我先write一次,剩下的再暂存在内存里,等待下次发送。

5,避免频繁动态申请/释放内存(malloc)
这个似乎不用多说,要想一个Server启动后成年累月的跑,就不应该频繁地去动态申请和释放内存。原因很简单一,避免内存泄露。二,避免碎片过多。三,影响效率。一般来说,都是一次申请一大块内存,然后自己写内存分配算法。为http用户分配的缓冲区生命期的特点是,可以随着fd的关闭,而回收,避免漏网。还有Server的编写者应该对自己设计的程序达到最高支撑量的时候所消耗的内存心中有数。

6,字节对齐
先看下面的两个结构体有什么不同:
struct A {
        short size; 
        char *ptr;
        int left;
} a ;

struct B {
        char *ptr;
        short size; 
        int left;
} b ;

仅仅是一个顺序的变化,结构体B顺序是合理的:
在32bit linux系统上,是按照32/8bit=4byte来对齐的, sizeof(a)=12 ,sizeof(b)=12 。
在64bit linux系统上,是按照64/8bit=8byte来对齐的, sizeof(a)=24 ,sizeof(b)=16 。
32bit机上看到的A和B结果大小是一样的,但是如果把int改成short效果就不一样了。

如果我想强制以2byte对齐,可以这样:
#pragma pack(2)
struct A {
        short size; 
        char *ptr;
        int left;
} a ;
#pragma pack()
注意pack()里的参数,只能指定比本机支持的字节对齐标准小,而不能更大。

7,内存安全问题
先举个好玩的例子,不使用a,而给a赋上值:
int main()
{
        char a[8];
        char b[8];
        memcpy(b,"1234567890\0",10);
        printf("a=%s\n",a);
        return 0;
}
程序输出  a=90 。
这就是典型的溢出,如果是空闲的内存,用点也就罢了,可是把别人地盘上的数据覆盖了,就不好了。
接收的用户数据一定要严格判断,确定不会越界,不是每个人都按规矩办事的,搞不好就挂了。

8,云风的内存管理理论 (sd2c大会所获 blog & ppt
没有永远不变的原则
大原则变化的慢
没有一劳永逸的解决方案
内存访问很廉价但有代价
减少内存访问的次数是很有意义的
随机访问内存慢于顺序访问内存
请让数据物理上连续
集中内存访问优于分散访问
尽可能的将数据紧密的存放在一起
无关性内存访问优于相关性内存访问
请考虑并行的可能性、即使你的程序本身没有使用并行机制
控制周期性密集访问的数据大小
必要时采用时间换空间的方法
读内存快于写内存
代码也会占用内存,所以、保持代码的简洁

物理法则
晶体管的排列
批量回收内存
不释放内存,留给系统去做
list map  vector (100次调用产生13次内存分配和释放)
长用字符串做成hash,使用指针访问
直接内存页处理控制

三,减少磁盘I/O
这个其实就是通过尽可能的使用内存达到性能提高和i/o减少。从系统的读写buffer到用户空间自己的cache,都是可以有效减少磁盘i/o的方法。用户可以把数据暂存在自己的缓冲区里,批量读写大块数据。cache的使用是很必要的,可以自己用共享内存的方法实现,也可以用现成的BDB来实现。欢迎访问我的公益站点 berkeleydb.net ,不过我不太欢迎那种问了问题就跑的人。BDB默认的cache只有256K,可以调大这个数字,也可以纯粹使用Mem Only方法。对于预先知道的结果,争取不从磁盘取第二次,这样磁盘基本就被解放出来了。BDB取数据的速度每秒大概是100w条(2CPU*2Core Xeon(R) E5410 @ 2.33GHz环境测试,单条数据几十字节),如果你想取得更高的性能建议自己写。


四,优化你的网卡
首先ethtool ethx 看看你的外网出口是不是Speed: 1000Mb/s 。
对于多核服务器,运行top命令,然后按一下1,就能看到每个核的使用情况。如果发现cpuid=0的那颗使用率明显高于其他核,那就说明id=0的cpu将来也许会成为你的瓶颈。然后可以用mpstat(非默认安装)命令查看系统中断分布,用cat /proc/interrupts 网卡中断分布。

下面这个数据是我们已经做过优化了的服务器中断分布情况:
[yangjian2@D08043466 ~]$ mpstat -P ALL 1
Linux 2.6.18-53.el5PAE (D08043466)      12/15/2008
01:51:27 PM  CPU   %user   %nice    %sys %iowait    %irq   %soft  %steal   %idle    intr/s
01:51:28 PM  all    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00   1836.00
01:51:28 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    179.00
01:51:28 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    198.00
01:51:28 PM      1.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    198.00
01:51:28 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    346.00
01:51:28 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    207.00
01:51:28 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    167.00
01:51:28 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    201.00
01:51:28 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00    339.00

没优化过的应该是这个样子:
yangjian2@xk-6-244-a8 ~]$ mpstat -P ALL 1
Linux 2.6.18-92.1.6.el5 (xk-6-244-a8.bta.net.cn)        12/15/2008
02:05:26 PM  CPU   %user   %nice    %sys %iowait    %irq   %soft  %steal   %idle    intr/s
02:05:27 PM  all    0.00    0.00    0.00    0.12    0.00    0.00    0.00   99.88   1593.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00   1590.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00      0.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00      2.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00      0.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00      0.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00      0.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00      0.00
02:05:27 PM      0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00      0.00

对于32bit的centos5,mpstat -P ALL 1表现跟第一种情况一样,分布比较平均,但是一但有了访问量,就可以看到差距。cat /proc/interrupts 看起来更直观些,很清楚的知道哪个网卡的中断在哪个cpu上处理。

其实,当你遇到网卡中断瓶颈的时候证明你的网站并发度已经相当高了,每秒三五万个请求还至于成为瓶颈。除非你的应用程序同时也在消耗cpu0的资源。对于这种情况,建议使用多进程模式,每个进程用 sched_setaffinity绑定特定的cpu,把cpu0从用户事物中解放出来,专心处理系统事物,当然包括中断。这样你的极限应该能处理20w+ http req/s (2CPU*4Core服务器)。但是对于多线程模式来说,我们就显得无能为力了,因为我们如果想使用多核,就没法不用cpu0。目前的方法只有两个:一,转化为多进程,然后进程内再使用多线程。二,让你的网卡中断分散在多个cpu上(目前只有硬件解决方案,感谢xiaodong2提供的技术支持)。 (修正:后来仔细读了几遍man手册,发现sched_setaffinity绑定特定的cpu对于多线程也是适用的,并且实验通过,只需要将第一个参数置为0。这对cpu0的解放是个很好的发现。)

将网卡中断分散在多个cpu硬件解决方案: 我们新加了一块网卡(前提是这个网卡支持中断分布),然后通过通过linux bonding将两个网卡比如eth0,eth1联合成一个通道bond0(当然这里还涉及到交换机的调整),然后bond0就有了2G的带宽吞吐量。把eth0的中断处理帮定在cpu 0-3,把eth1中断处理帮定在cpu 4-7,这样中断就被分布开了。这样会带来一些额外的cpu开销,但是跟好处相比可以忽略不计。我在网卡优化过的32bit服务器上测试http请求处理极限为 40w+ req/s,将近提升了一倍。


五,调整内核参数

我的内核心参数调整原则是,哪个遇到瓶颈调哪个,谨慎使用,不能凭想象乱调一气。看下面例子,其中default是我们公司定做的系统默认的一些参数值。add by yangjian2并非全部都要调整,我只挑几个比较重要的参数说明一下,更多TCP方面的调优请参见 man 7 tcp 。
#++++++++++++++++++default++++++++++++++++++++++++++++++
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_tw_buckets = 180000
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096        87380   4194304
net.ipv4.tcp_wmem = 4096        16384   4194304
#++++++++++++++++++add by yangjian2++++++++++++++++++++++
net.ipv4.tcp_max_syn_backlog = 65536
net.core.netdev_max_backlog =  32768
net.core.somaxconn = 32768

net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2

net.ipv4.tcp_tw_recycle = 1
#net.ipv4.tcp_tw_len = 1
net.ipv4.tcp_tw_reuse = 1

net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_max_orphans = 3276800
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++

maxfd: 对于系统所能打开的最大文件描述符fd,可以通过以root启动程序,setrlimit()设置maxfd后,再通过setuid()转为普通用户提供服务,我用的 int set_max_fds(int maxfds); 函数是zhongying提供的。这比用ulimit来的方便的多,不晓得为什么那么多开源软件都没这样用。

net.ipv4.tcp_max_syn_backlog = 65536 : 这个参数可以肯定是必须要修改的,默认值1024,我google了一下,几乎是人云亦云,没有说的明白的。 要讲明白得从man listen说起,int listen(int sockfd, int backlog);  早期的网络编程都中描述,int backlog 代表未完成队列SYN_RECV状态+已完成队列ESTABLISHED的和。但是这个意义在Linux 2.2以后的实现中已经被改变了,int backlog只代表已完成队列ESTABLISHED的长度,在AF_INET协议族中(我们广泛使用的就是这个),当int backlog大于SOMAXCONN  (128 in Linux 2.0 & 2.2)的时候,会被调整为常量SOMAXCONN大小。这个常量可以通过net.core.somaxconn来修改。而未完成队列大小可以通过net.ipv4.tcp_max_syn_backlog来调整,一般遭受syn flood攻击的网站,都存在大量SYN_RECV状态,所以调大tcp_max_syn_backlog值能增加抵抗syn攻击的能力。

net.ipv4.tcp_syncookies = 1 : 当出现syn等候队列出现溢出时象对方发送syncookies。目的是为了防止syn flood攻击 , 默认值是 0。 不过man listen说当启用syncookies时候,tcp_max_syn_backlog的sysctl调整将失效,和这个描述不是很符合。参见下面两个描述分别是man listen和man 7 tcp:
When syncookies are enabled there is no logical maximum length and this tcp_max_syn_backlog sysctl  setting  is  ignored.
Send out  syncookies  when the syn backlog queue of a socket overflows.
但我可以肯定的说这个选项对你的性能不会有提高,而且它严重的违背TCP协议,不允许使用TCP扩展,除非遭受攻击,否则不推荐使用。

net.ipv4.tcp_synack_retries = 2 : 对于远端的连接请求SYN,内核会发送SYN + ACK数据报,以确认收到上一个 SYN连接请求包。这是所谓的三次握手( threeway handshake)机制的第二个步骤。这里决定内核在放弃连接之前所送出的 SYN+ACK 数目。如果你的网站SYN_RECV状态确实挺多,为了避免syn攻击,那么可以调节重发的次数。

net.ipv4.tcp_syn_retries = 2 : 对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃。不应该大于255,默认值是5,对应于180秒左右。这个对防止syn攻击其实是没有用处的,也没必要调节。

net.ipv4.tcp_max_orphans = 3276800 : 这个最好不要修改,因为每增加1,将消耗~64k内存。即使报错 TCP: too many of orphaned sockets 也有可能是由于你的net.ipv4.tcp_mem过小,导致的Out of socket memory,继而引发的。

net.ipv4.tcp_wmem = 4096 16384 4194304 :  为自动调优定义每个socket使用的内存。第一个值是为socket的发送缓冲区分配的最少字节数。第二个值是默认值(该值会被 wmem_default覆盖),缓冲区在系统负载不重的情况下可以增长到这个值。第三个值是发送缓冲区空间的最大字节数(该值会被wmem_max覆盖)。

net.ipv4.tcp_rmem = 4096 87380 4194304 : 接收缓冲区,原理同上。

net.ipv4.tcp_mem = 94500000 915000000 927000000 :
low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。
pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入pressure模式,当内存消耗低于low值时则退出pressure状态。
high:允许所有tcp sockets用于排队缓冲数据报的内存页数。

一般情况下这个值是在系统启动时根据系统内存数量计算得到的,如果你的dmesg报 Out of socket memory,你可以试着修改这个参数,顺便介绍3个修改方法:
1, echo "94500000 915000000 927000000" > /proc/sys/net/ipv4/tcp_wmem
2, sysctl -w "net.ipv4.tcp_mem = 94500000 915000000 927000000"
3, net.ipv4.tcp_mem = 94500000 915000000 927000000  (vi /etc/sysctl.conf 然后 sysctl -p生效)

下面命令也许能提供些信息,在你修改tcp参数时做个参考:
[sports@xk-6-244-a8 nbahttpd_beta4.0]$ cat  /proc/net/sockstat
sockets: used 1195
TCP: inuse 1177 orphan 30 tw 199 alloc 1181 mem 216
UDP: inuse 0 mem 0
RAW: inuse 0
FRAG: inuse 0 memory 0

其他我就不多说了,知道这些基本就能解决绝大部分问题了。


六,衡量Web Server的性能指标

我认为一个好的Server应该能在有限的硬件资源上将性能发挥到极限。
Web Server的衡量指标并非单一,要根据具体应用类型而定。比如财经实时图片系统,我们关注它每秒输出图片数量。NBA js直播放系统,我们关心他的同时在线connections和当时的每秒请求处理量。行情系统,我们关心它connections和请求处理量的同时还要关心每个请求平均查询多少支股票。但总体来说同时在线connections和当时的每秒请求处理量是两个最重要的指标。

对于图片系统再说一句,我觉得大图片和小图片是应该区别对待的,小图片不应该产生磁盘 I/O 。

Nginx是我见过的Web Server中性能比较高的一个,他几乎是和我的server同时诞生,可能还更早些,框架很不错,我觉得目前版本稍微优化下,支持10w connections不成问题。 lighttpd也不错,我对他的认识还是停留在几年前的性能测试上,它的性能会比nginx逊色一些。他们都支持epoll,sendfile,可以起多个进程worker,worker内部使用非阻塞,这是比较优良的I/O的模型。Squid,Apache,都是骨灰级软件了,好处就是支持的功能多,另许多轻量级Server望尘莫及,可是性能太一般了,祝愿他们早日重写。

插点小插曲,我在财经项目组的时候,有的同事来我们组一年多了,问我是不是管机器的,我点点头,后来又有比较了解我的同事说我是系统管理员,我说“恩”。其实我的主业是写程序的。也许是我太低调了,觉得那些陈年往事不值再提,以至于别人对我做的东西了解甚少, 今天我就高调一把,公布一些我写的程序的性能指标。我们的系统近几年来说在性能上是领先业内的(不争世界第一,那样压力太大,第二就很好,也许正在看我blog的你一不留神就把我超了呢 ^-^ ),高效的原因很重要的一点是由于它是根据服务特点量身订做的。

实验环境数据: 我写了个HTTP服务框架,不使用磁盘I/O,简化了逻辑处理部分,只会输出 "hello world!"  程序部署在192.168.0.1上(2cup*4Core,硬件和系统都做过优化),我在另外8台同等配置服务器上同时执行命令  ./apache/bin/ab -c 1000  -n 3000000   -k  "http://192.168.0.1/index.html" 几乎同时处理完毕,总合相加 40w req/s,我相信这是目前硬件水平上的极限值 。

真实环境数据:2cup*4Core Mem 16G, 64bit centos5,单机23w+ connections, 3.5w req/s时,CPU总量消耗 1/8,内存消耗0.4%(相当于正好消耗了一个Core+64M Mem)。在30w+ connections, 4.6w req/s 时,CPU总量消耗 1/4,内存消耗 0.5% 。保守地说,只要把网卡中断分散一下,单机50w+ connections很easy。  更多数据图文参见“NBA js直播的发展历程”一节。

有些人了解我是由于财经的实时行情系统,虽然每天处理近百亿的http请求处理量还不错,但那并非我的得意之作,相反我觉得那个写的有些粗糙,至少有一倍以上的性能提升空间。对于行情系统,我还是很想把它做成push的,目标仍然是单机50w+在线,无延迟推送,可惜本人js功底太烂,所以要作为一个长期的地下项目去做,如果可能,我想一开始就把它作为一个开源项目来做。

我个人比较喜欢追求性能极限,公司对此暂时还不是很认可,或者说重视程度还不够,可能是由于我们的硬件资源比较充裕吧。尽管如此,只要我认为对企业有价值的,就依然会坚持做下去,我的目标是获得业界的认可。同时我相信中国的未来不缺乏互联网用户,当有人烧不起钱的时候想起了我,那我就是有价值的。
Mar27

某大型论坛负载均衡实战配置【转载】

Author: 猪头  Click: 9520   Comments: 0 Category: 架构  Tag: 负载均衡,php,mysql,nginx,apache

某大型BBS为国内人气最旺的股票论坛,注册会员已超过100万,并以每月60000人的速度稳定递增,每日页面访问量超过200万,并保持稳定增长的趋势,60分钟在线平均约2万多人,最高记录3万3千多。 目前主题超过30万,帖子接近1千万,数据库大小5.8GB,附件总大小大约150GB
之前论坛有三台服务器,两台WEB服务器以及一台数据库服务器,访问已经渐渐出现瓶颈,在猪头的建议下,站长决定增加一台服务器放数据库,另外三台做WEB,并且对原有的服务器的操作系统进行升级。
硬件具体情况
MySQL服务器: DualXeon 5335/8GB内存/73G SAS硬盘(RAID0+1)/CentOS5.1-x86_64/MySQL5
三台WEB服务器如下:
N1. Dual Xeon 3.0 2GB 内存
N1. Dual Xeon 3.0 4GB 内存
N1. Dual Xeon 3.0(双核) 4G内存
另外有三块300G的SCSI硬盘准备做RAID5,用来存放附件,四台机器通过内网连接
猪头考虑过的解决方案如下:
1. ZEUS + PHP5 + eAccelerator
2. squid + Apache2 + PHP + eAccelerator
3. nginx + PHP(fastcgi) + eAccelerator
4. nginx + Apache2 + PHP + eAccelerator

第一个方案,属于比较完美的,而且很稳定,但是最大的问题是ZEUS是收费软件,用盗版总会受良心责备的,所以暂时押后做候补方案
第二个方案,squid转发请求给Apache2,很多网站都采用这种方式,而且效率也非常高,猪头也测试了一下,但是问题非常严重,因为squid是把文件缓存起来的,所以每一个访问过的文件,squid都要把它打开,论坛拥有150G的附件,而且访问量巨大,这种情况下只有打开squid,机器很快就会因为打开文件过多而拒绝响应任何请求了,看来也不适合,只适合缓存文件只有几百M以内的网站.
第三个方案,猪头对第三个方案的测试结果是访问量大的时候,PHP经常会出现bad gateway,看来通过TCP连接Fastcgi执行PHP的方法不够稳定,猪头也测试了通过Unix Socket连接执行PHP,同样还是不稳定.

对比之下,猪头目前使用了第四种解决方案.

Apache2的安装。
(由于服务器采用FreeBSD7,所以大部分软件将会通过ports安装)
由于Apache2只需要处理PHP请求,所以其他模块基本上都不需要,所以不要选择安装其他模块,即使rewrite也不需要,因为rewrite将会在nginx上面实现,如果熟悉,还可以修改Makefile删掉不需要的部分,这样经过优化之后,apache将会以最稳定最高效的方式处理PHP请求

cd /usr/ports/www/apache20
make install clean

修改httpd.conf(这里仅列出要修改/增加的部分)
vi /usr/local/etc/apache2/httpd.conf
把KeepAlive On修改为KeepAlive Off,在下面添加
ServerLimit 2048
MaxClients增加到512
Listen 127.0.0.1:81 #由于httpd服务器不需要对外开放,仅仅处理nginx转发过来的PHP请求,所以仅仅需要监听本地的端口.
另外增加对PHP的支持
AddType application/x-httpd-php .php
AddType application/x-httpd-php-source .phps
至于添加虚拟主机的部分将不再罗嗦,注意虚拟主机也监听本地81端口就可以了

PHP5的安装(GD库等模块请提前装好)
cd /usr/ports/lang/php5
修改一下Makefile,把需要的东西加上去吧
本来应该有这样一段的

CONFIGURE_ARGS= \
–with-layout=GNU \
–with-config-file-scan-dir=${PREFIX}/etc/php \
–disable-all \
–enable-libxml \
–with-libxml-dir=${LOCALBASE} \
–enable-reflection \
–program-prefix=”"

我们要把它修改成

CONFIGURE_ARGS= \
–with-layout=GNU \
–with-config-file-scan-dir=${PREFIX}/etc/php \
–disable-all \
–enable-libxml \
–with-libxml-dir=${LOCALBASE} \
–enable-reflection \
–program-prefix=”" \
–with-config-file-path=/etc –enable-mbstring –enable-ftp –with-gd –with-jpeg-dir=/usr/local –with-png-dir=/usr/local –enable-magic-quotes –with-mysql=/usr/local –with-pear –enable-sockets –with-ttf –with-freetype-dir=/usr/local –enable-gd-native-ttf –with-zlib –enable-sysvsem –enable-sysvshm –with-libxml-dir=/usr/local –with-pcre-regex –enable-xml

make install clean
cp work/php-5.2.5/php.ini-dist /etc/php.ini

安装eAccelerator
cd /usr/ports/www/eaccelerator
make install clean
把以下部分添加到php.ini尾端:

extension_dir=”/usr/local/lib/php/20060613/”
extension=”eaccelerator.so”
eaccelerator.cache_dir=”/tmp/eaccelerator”
eaccelerator.shm_size=”64″
eaccelerator.enable=”1″
eaccelerator.optimizer=”1″
eaccelerator.check_mtime=”1″
eaccelerator.debug=”0″
eaccelerator.filter=”"
eaccelerator.shm_max=”0″
eaccelerator.shm_ttl=”60″
eaccelerator.shm_prune_period=”60″
eaccelerator.shm_only=”0″
eaccelerator.compress=”1″
eaccelerator.compress_level=”9″
eaccelerator.keys=”shm_and_disk”
eaccelerator.sessions=”shm_and_disk”
eaccelerator.content=”shm_and_disk”

建立缓存目录以及修改权限

mkdir /tmp/eaccelerator
chmod 777 /tmp/eaccelerator
chown nobody:nobody /tmp/eaccelerator

nginx的安装以及配置

cd /usr/ports/www/nginx
make install

有几个module是我们需要的,要选上

HTTP module
http_addition module
http_rewrite module
http_realip module
http_stub_status module

其他的看自己需要了
修改配置文件
vi /usr/local/etc/nginx/nginx.conf

user nobody nobody;
worker_processes 4;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid /var/log/nginx.pid;
events {
worker_connections 10240;
}
http {
include mime.types;
default_type application/octet-stream;
limit_zone one $binary_remote_addr 10m;
#log_format main ‘$remote_addr – $remote_user [$time_local] $request ‘
# ‘”$status” $body_bytes_sent “$http_referer” ‘
# ‘”$http_user_agent” “$http_x_forwarded_for”‘;
sendfile off;
tcp_nopush off;
#keepalive_timeout 0;
keepalive_timeout 10;
gzip off;

server {
listen 80;
server_name www.55188.net www.55188.com www1.55188.com www2.55188.com 55188.com 55188.net www.55188.cn 55188.cn bbs.55188.net bbs.55188.com bbs.55188.cn;
index index.html index.htm index.php;
root /home/www;
access_log /dev/null combined;
limit_conn one 5;#限制一个IP并发连接数为五个
error_page 404 /404.html;
error_page 403 /403.html;
location /status {
stub_status on;
access_log off;
auth_basic “NginxStatus”;
auth_basic_user_file conf/htpasswd;
}

#在根目录使用Discuz6.0 rewrite规则,如果你的论坛在二级目录下面,则要相应修改location
location / {
rewrite ^/archiver/((fid|tid)-[\w\-]+\.html)$ /archiver/index.php?$1 last;
rewrite ^/forum-([0-9]+)-([0-9]+)\.html$ /forumdisplay.php?fid=$1&page=$2 last;
rewrite ^/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ /viewthread.php?tid=$1&extra=page\%3D$3&page=$2 last;
rewrite ^/space-(username|uid)-(.+)\.html$ /space.php?$1=$2 last;
rewrite ^/tag-(.+)\.html$ /tag.php?name=$1 last;
break;
error_page 404 /404.html;
error_page 403 /403.html;
}

#对附件做防盗链,没有正确的referer将会返回403页面
location ~* ^.+\.(gif|jpg|png|swf|flv|rar|zip|doc|pdf|gz|bz2|jpeg|bmp|xls)$ {
valid_referers none blocked server_names *.55188.net *.55188.com;
if ($invalid_referer) {
rewrite ^/ http://www.55188.com/403.html;
}
}

#转发PHP请求到本地的81端口,让Apache处理.
location ~ \.php$ {
proxy_pass http://127.0.0.1:81;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_hide_header Content-Type;
}
}

}

测试一下你的配置文件是否都正确

/usr/local/sbin/apachectl configtest
/usr/local/sbin/nginx -t

都没问题的话就启动服务器吧

/usr/local/sbin/apachectl start
/usr/local/sbin/nginx -c /usr/local/etc/nginx/nginx.conf

浏览一下主页,应该正常了

后继讨论,
1.数据库.
数据库的编译安装不再重复讨论,仅仅讨论环境,由于论坛的数据库比较大,而且发展比较快,所以要作比较前一点的预算,硬盘需要使用15K RPM的SAS硬盘做RAID0+1,操作系统需要使用64位版本,因为服务器需要8GB内存,要注意的时,使用了64位系统之后部分比较老的软件可能你无法找到64位的版本,这台机器就专门做MySQL服务器吧,如果数据库超过10G,应该考虑MySQL_Cluster

2.附件.
因为有三台服务器做WEB,所以附件要使用nfs的方式通过内网进行共享,至于如何设置nfs这里不再讨论,如果有不明白的请将学费交给Google

3.WEB.
由于三台机器硬件配置不一致,所以有必要考虑一下负载平衡的问题,nginx本身附带有负载平衡的功能,但是如果启用负载平衡的功能的话,每台机器都将会把客户端请求的数据缓存到本机,这样增加了硬盘的IO,对于论坛的访问量来说,这是个不小的开销,最后我们是使用DNS查询的方式来分配流量, 通过不同的A记录,配置好点的机器,多分一条A记录,配置差的就少一条A记录,这样从整体上看,流量分配应该比较平衡.

4.关于nginx并发连接
猪头给nginx限制了每个IP的并发连接,因为对于大论坛来说,总是比较出名的,不说人家攻击你什么的.采集都特别多,如果不限制,很容易出问题,经常会导致PHP罢工.

以上只是猪头愚见,如果有其他进展,猪头会更新本贴,如有疑问或者不同见解,欢迎提出讨论

当然还有很多很疯狂的方法,例如说把WEB文件(附件除外)全部放内存里面,MYSQL如果小于5G,也可以全部放内存里面,不过这些方法都是太极端的了,优化效果须然好,但是风险很大。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

优化之后的效果
由于还有两台机器升级没完成,只帖一下其中一台WEB的状况了。目前
Active connections: 1143
server accepts handled requests
1211445 1211445 6221785
Reading: 67 Writing: 136 Waiting: 940

Apache最优化要关闭不用的模块,因为httpd请求全部让nginx处理了,Apache仅仅需要处理PHP就可以了,目前我开启的模块
LoadModule access_module libexec/apache2/mod_access.so
LoadModule setenvif_module libexec/apache2/mod_setenvif.so
LoadModule mime_module libexec/apache2/mod_mime.so
LoadModule autoindex_module libexec/apache2/mod_autoindex.so
LoadModule negotiation_module libexec/apache2/mod_negotiation.so
LoadModule alias_module libexec/apache2/mod_alias.so
LoadModule rewrite_module libexec/apache2/mod_rewrite.so
LoadModule php5_module libexec/apache2/libphp5.so

autoindex negotiation以及rewrite这些应该都关闭的,但是要做相应的修改.目前跑起来绝对比Fastcgi要好

转载自 http://www.discuz.net/thread-882980-1-2.html

Mar24

分表处理设计思想和实现

Author: heiyeluren (黑夜路人)  Click: 5462   Comments: 0 Category: 架构  Tag: 分表
一、概述
分表是个目前算是比较炒的比较流行的概念,特别是在大负载的情况下,分表是一个良好分散数据库压力的好方法。
首先要了解为什么要分表,分表的好处是什么。我们先来大概了解以下一个数据库执行SQL的过程:
接收到SQL --> 放入SQL执行队列 --> 使用分析器分解SQL --> 按照分析结果进行数据的提取或者修改 --> 返回处理结果
当然,这个流程图不一定正确,这只是我自己主观意识上这么我认为。那么这个处理过程当中,最容易出现问题的是什么?就是说,如果前一个SQL没有执行完毕的话,后面的SQL是不会执行的,因为为了保证数据的完整性,必须对数据表文件进行锁定,包括共享锁和独享锁两种锁定。共享锁是在锁定的期间,其它线程也可以访问这个数据文件,但是不允许修改操作,相应的,独享锁就是整个文件就是归一个线程所有,其它线程无法访问这个数据文件。一般MySQL中最快的存储引擎MyISAM,它是基于表锁定的,就是说如果一锁定的话,那么整个数据文件外部都无法访问,必须等前一个操作完成后,才能接收下一个操作,那么在这个前一个操作没有执行完成,后一个操作等待在队列里无法执行的情况叫做阻塞,一般我们通俗意义上叫做“锁表”。
锁表直接导致的后果是什么?就是大量的SQL无法立即执行,必须等队列前面的SQL全部执行完毕才能继续执行。这个无法执行的SQL就会导致没有结果,或者延迟严重,影响用户体验。
特别是对于一些使用比较频繁的表,比如SNS系统中的用户信息表、论坛系统中的帖子表等等,都是访问量大很大的表,为了保证数据的快速提取返回给用户,必须使用一些处理方式来解决这个问题,这个就是我今天要聊到的分表技术。
分表技术顾名思义,就是把若干个存储相同类型数据的表分成几个表分表存储,在提取数据的时候,不同的用户访问不同的表,互不冲突,减少锁表的几率。比如,目前保存用户分表有两个表,一个是user_1表,还有一个是 user_2 表,两个表保存了不同的用户信息,user_1 保存了前10万的用户信息,user_2保存了后10万名用户的信息,现在如果同时查询用户 heiyeluren1 和 heiyeluren2 这个两个用户,那么就是分表从不同的表提取出来,减少锁表的可能。
我下面要讲述的两种分表方法我自己都没有实验过,不保证准确能用,只是提供一个设计思路。下面关于分表的例子我假设是在一个贴吧系统的基础上来进行处理和构建的。(如果没有用过贴吧的用户赶紧Google一下)
二、基于基础表的分表处理
这个基于基础表的分表处理方式大致的思想就是:一个主要表,保存了所有的基本信息,如果某个项目需要找到它所存储的表,那么必须从这个基础表中查找出对应的表名等项目,好直接访问这个表。如果觉得这个基础表速度不够快,可以完全把整个基础表保存在缓存或者内存中,方便有效的查询。
我们基于贴吧的情况,构建假设如下的3张表:
1. 贴吧版块表: 保存贴吧中版块的信息
2. 贴吧主题表:保存贴吧中版块中的主题信息,用于浏览
3. 贴吧回复表:保存主题的原始内容和回复内容
“贴吧版块表”包含如下字段:
版块ID       board_id          int(10)
版块名称    board_name      char(50)
子表ID       table_id            smallint(5)
产生时间    created             datetime
“贴吧主题表”包含如下字段:
主题ID          topic_id        int(10)
主题名称        topic_name     char(255)
版块ID          board_id          int(10)
创建时间       created           datetime
“贴吧回复表”的字段如下:
回复ID        reply_id           int(10)
回复内容      reply_text        text
主题ID        topic_id           int(10)
版块ID        board_id         int(10)
创建时间      created            datetime
那么上面保存了我们整个贴吧中的表结构信息,三个表对应的关系是:
版块 --> 多个主题
主题 --> 多个回复
那么就是说,表文件大小的关系是:
版块表文件 < 主题表文件 < 回复表文件
所以基本可以确定需要对主题表和回复表进行分表,已增加我们数据检索查询更改时候的速度和性能。
看了上面的表结构,会明显发现,在“版块表”中保存了一个"table_id"字段,这个字段就是用于保存一个版块对应的主题和回复都是分表保存在什么表里的。
比如我们有一个叫做“PHP”的贴吧,board_id是1,子表ID也是1,那么这条记录就是:
board_id | board_name | table_id | created
1 | PHP | 1 | 2007-01-19 00:30:12
相应的,如果我需要提取“PHP”吧里的所有主题,那么就必须按照表里保存的table_id来组合一个存储了主题的表名称,比如我们主题表的前缀是“topic_”,那么组合出来“PHP”吧对应的主题表应该是:“topic_1”,那么我们执行:
SELECT * FROM topic_1 WHERE board_id = 1 ORDER BY topic_id DESC LIMIT 10
这样就能够获取这个主题下面回复列表,方便我们进行查看,如果需要查看某个主题下面的回复,我们可以继续使用版块表中保存的“table_id”来进行查询。比如我们回复表的前缀是“reply_”,那么就可以组合出“PHP”吧的ID为1的主题的回复:
SELECT * FROM reply_1 WHERE topic_id = 1 ORDER BY reply_id DESC LIMIT 10
这里,我们能够清晰的看到,其实我们这里使用了基础表,基础表就是我们的版块表。那么相应的,肯定会说:基础表的数据量大了以后如何保证它的速度和效率?
当然,我们就必须使得这个基础表保持最好的速度和性能,比如,可以采用MySQL的内存表来存储,或者保存在内存当中,比如Memcache之类的内存缓存等等,可以按照实际情况来进行调整。
一般基于基础表的分表机制在SNS、交友、论坛等Web2.0网站中是个比较不错的解决方案,在这些网站中,完全可以单独使用一个表来来保存基本标识和目标表之间的关系。使用表保存对应关系的好处是以后扩展非常方便,只需要增加一个表记录。
优势】增加删除节点非常方便,为后期升级维护带来很大便利
劣势】需要增加表或者对某一个表进行操作,还是无法离开数据库,会产生瓶颈
三、基于Hash算法的分表处理
我们知道Hash表就是通过某个特殊的Hash算法计算出的一个值,这个值必须是惟一的,并且能够使用这个计算出来的值查找到需要的值,这个叫做哈希表。
我们在分表里的hash算法跟这个思想类似:通过一个原始目标的ID或者名称通过一定的hash算法计算出数据存储表的表名,然后访问相应的表。
继续拿上面的贴吧来说,每个贴吧有版块名称和版块ID,那么这两项值是固定的,并且是惟一的,那么我们就可以考虑通过对这两项值中的一项进行一些运算得出一个目标表的名称。
现在假如我们针对我们这个贴吧系统,假设系统最大允许1亿条数据,考虑每个表保存100万条记录,那么整个系统就不超过100个表就能够容纳。按照这个标准,我们假设在贴吧的版块ID上进行hash,获得一个key值,这个值就是我们的表名,然后访问相应的表。
我们构造一个简单的hash算法:
function get_hash($id){
     $str = bin2hex($id);
     $hash = substr($str, 0, 4);
     if (strlen($hash)<4){
         $hash = str_pad($hash, 4, "0");
     }
     return $hash;
}
算法大致就是传入一个版块ID值,然后函数返回一个4位的字符串,如果字符串长度不够,使用0进行补全。
比如:get_hash(1),输出的结果是“3100”,输入:get_hash(23819),得到的结果是:3233,那么我们经过简单的跟表前缀组合,就能够访问这个表了。那么我们需要访问ID为1的内容时候哦,组合的表将是:topic_3100、reply_3100,那么就可以直接对目标表进行访问了。
当然,使用hash算法后,有部分数据是可能在同一个表的,这一点跟hash表不同,hash表是尽量解决冲突,我们这里不需要,当然同样需要预测和分析表数据可能保存的表名。
如果需要存储的数据更多,同样的,可以对版块的名字进行hash操作,比如也是上面的二进制转换成十六进制,因为汉字比数字和字母要多很多,那么重复几率更小,但是可能组合成的表就更多了,相应就必须考虑一些其它的问题。
归根结底,使用hash方式的话必须选择一个好的hash算法,才能生成更多的表,然数据查询的更迅速。
优点hash算法直接得出目标表名称,效率很高】通过
劣势】扩展性比较差,选择了一个hash算法,定义了多少数据量,以后只能在这个数据量上跑,不能超过过这个数据量,可扩展性稍差
四、其它问题
1. 搜索问题
现在我们已经进行分表了,那么就无法直接对表进行搜索,因为你无法对可能系统中已经存在的几十或者几百个表进行检索,所以搜索必须借助第三方的组件来进行,比如Lucene作为站内搜索引擎是个不错的选择。
2. 表文件问题
我们知道MySQL的MyISAM引擎每个表都会生成三个文件,*.frm、*.MYD、*.MYI 三个文件,分表用来保存表结构、表数据和表索引。Linux下面每个目录下的文件数量最好不要超过1000个,不然检索数据将更慢,那么每个表都会生成三个文件,相应的如果分表超过300个表,那么将检索非常慢,所以这时候就必须再进行分,比如在进行数据库的分离。
使用基础表,我们可以新增加一个字段,用来保存这个表保存在什么数据。使用Hash的方式,我们必须截取hash值中第几位来作为数据库的名字。这样,完好的解决这个问题。
五、总结
在大负载应用当中,数据库一直是个很重要的瓶颈,必须要突破,本文讲解了两种分表的方式,希望对很多人能够有启发的作用。当然,本文代码和设想没有经过任何代码测试,所以无法保证设计的完全准确实用,具体还是需要读者在使用过程当中认真分析实施。
Feb27

apache中泛域名的简单案例

Author: 明分空分  Click: 7116   Comments: 0 Category: 架构  Tag: 泛域名,module,rewrite

目的:动态解析home.sst.cn的下级域名,username.home.sst.cn,实现基于虚拟域名(动态域名)的个人空间访问 。

实现步骤:

1.DNS的泛域名解析

[code="bash"]
@ IN SOA ns.sst.cn. zhao.sst.cn. (
20050817 ; Serial
3600 ; Refresh
900 ; Retry
3600000 ; Expire
3600 ) ; Minimum
@  IN NS ns.sst.cn.

home IN A 123.123.123.123
*.home IN A 123.123.123.123
[/code]

以上是在unix bind9下的设置,在windows server中的DNS设置与之同理,略。

2. web服务的设置

修改httpd.conf文件加入虚拟主机(如果需要同时供应多个web服务)
 
[code="bash"]

ServerAdmin zhao@home.sst.cn
DocumentRoot /somewhere/to/wwwhome
ServerName home.sst.cn
ServerAlias *.home.sst.cn
ErrorLog logs/home.sst.cn/error_log
CustomLog logs/home.sst.cn/access_log common


ServerName anotherweb.sst.cn
DocumentRoot /somewhere/to/anotherweb

[/code]

为改变虚拟主机顺序,使提供泛域名的web服务为非中心主机(main host),加入此句:
ServerAlias *.home.sst.cn

  分析访问过程:
    用户输入[username].home.sst.cn
           |
           |
           |
           `-->apache分析主机头的值,不匹配任何一
               个虚拟主机名,则交送中心主机的目录
                        |
                        |
                        |
         接下来应该有两种方法处理:<--'
              |
              |
              ^ 
            '  `
            /    \
① 在home.sst.cn的根目录         ② 用mod_rewrite重写
设跳转文件,ASP代码举例:                URL指向[username]目录。
     |                 |
     |                 |
     |                 |

方法①纯代码方式,无用户目录。

方法②用mod_rewrite重写URL,指向用户目录。

 

在blog的主目录下建立.htaccess文件,内容为:

[code="plain"]

RewriteEngine On
#RewriteRule ^(.*)/home/(.*)$ $1.php?$2

[/code]

分类

标签

归档

最新评论

Abyss在00:04:28评论了
Linux中ramdisk,tmpfs,ramfs的介绍与性能测试
shallwe99在10:21:17评论了
【原创】如何在微信小程序开发中正确的使用vant ui组件
默一在09:04:53评论了
Berkeley DB 由浅入深【转自架构师杨建】
Memory在14:09:22评论了
【原创】最佳PHP框架选择(phalcon,yaf,laravel,thinkphp,yii)
leo在17:57:04评论了
shell中使用while循环ssh的注意事项

我看过的书

链接

其他

访问本站种子 本站平均热度:8823 c° 本站链接数:1 个 本站标签数:464 个 本站被评论次数:94 次