作者:李萧明

深入PHP使用技巧之变量

No Comments 专业IT吐槽

众所周知,PHP与其他脚本语言一样,属于弱变量类型的语言。同时PHP本身也是通过C语言来实现。本文主要介绍PHP内部是如何实现弱变量类型的,并且据此分析在PHP开发中需要注意的一些使用技术。其中会重点分析PHP中的copy on write机制和引用相关方面的话题。 本章节属于《深入PHP使用技巧》的第一部分。

如何实现弱变量

在了解PHP实现弱变量类型之前,可以先思考下:如何通过C/C++来实现弱变量类型的效果呢?

这个问题我在BIT培训课上基本上有两种答案:

方法1:采用C++的继承机制。首先定义一个基础类型

1
Class Var

2
{

3
}

然后基于Var,派生出不同的子类型IntVar/FloatVar/StringVar等等。
方法2:基于C语言的 Struct。其中一个字段用于标识类型,另外一个字段用于存储数据,由于数据要是各种类型,所以通常需要采用指针

比如:

1
struct var {

2
Int type;

3
Void *data;

4
};

两种思路本身并没有太大区别,也都基本上能够满足需求。在PHP中采用了第二种思路,并且做了比较多的优化。在PHP中,所有的变量都会对应同一种类型zval,其中zval也就是struct _zval_struct,具体定义如下:

01
typedef union _zvalue_value {

02
long lval;                  /* long value */

03
double dval;                /* double value */

04
struct {

05
char *val;

06
int len;

07
} str;

08
HashTable *ht;              /* hash table value */

09
zend_object_value obj;

10
} zvalue_value;

11
struct _zval_struct {

12
/* Variable information */

13
zvalue_value value;     /* value */

14
zend_uint refcount;

15
zend_uchar type;    /* active type */

16
zend_uchar is_ref;

17
};

从zval可以看出,PHP在细节方面的确做了不少优化的功夫。

1.zend_uchar type。采用uchar节省内存。
2.zvalue_value value; 采用union来替换void *,这样同样能节省空间,并且比void *更能表义清晰。
3.在字符串类型中,默认保留了字符串的长度。这样很容易做到字符串的二进制安全,并且在计算字符串长度的时候不需要进行扫描。
观察PHP弱变量的实现,也会有以下疑惑:

1.为什么会没有int类型呢?其实在PHP中是有的,只是说默认int数据就保存在long中。
2.资源类型咋表现的呢?资源在PHP内部其实就是一数字。详细后续会介绍。
3.refcount和is_ref是干嘛的呢?呵呵,这就是第二部分要介绍的了。

Reference counting & Copy-on-Write

PHP和其他语言类似,在其语法中有两种赋值方式:引用赋值和非引用赋值(普通的==赋值)。

1
<?php

2
$a = 1;

3
$b = $a;//非引用赋值

4
$c = &$a;//引用赋值

5
?>

引用赋值和非引用赋值在PHP内部是如何实现的呢?一种通常的认识是:“引用赋值就是两个变量对应同一个Zval,非引用赋值则是直接产生一个新的zval,同时把对应的值直接copy过来。”也就是该代码的内存结构如下:

(该图是大多数人认为的PHP内存结构,是错误的)

这样的确能够满足大部分情况下的需求,但显然不是最佳的解决方案,尤其是在内存管理上,比如说以下代码就会显得非常的低效。

1
<?php

2
$arr = array(...);//定义一个非常大的PHP数组

3
myfunc($arr);//每一个函数调用都是一次隐性的非引用赋值

------中间广告---------

4
myfunc($arr);

5
?>

因为每次函数调用会进行一次内存dump,而大内存的内存dump是非常耗CPU的。在C语言中,一种解决方案是采用指针,所有函数调用尽量传递指针。的确很灵活高效,但也很难维护~指针可以说是C语言程序员心头的痛(当然也是福~^_^)。还有一种更高级更有效的方法是采用引用计数(Reference counting)。

在PHP中,也可以采用引用来解决这样的问题,但你见过采用在PHP中大量使用引用的吗?显然很少。

在PHP内核中,Zval的实现正是采用了引用计数的概念,说起引用计数就不得不谈到copy-on-write 机制。这样前面谈到的refcount和is_ref就有作用了。

  • refcount:引用次数。在zval初始创建的时候就为1。每增加一个引用,则refcount ++。
  • is_ref:用于表示一个zval是否是引用状态。zval初始化的情况下会是0,表示不是引用。

在Zend/Zend.h内部有一些关于ZVAL的宏定义,里面比较清晰的解析了引用计数的一些规则,其中重点关注以下几个宏定义

01
#define INIT_PZVAL(z)       \

02
(z)->refcount = 1;       \

03
(z)->is_ref = 0;

04
#define SEPARATE_ZVAL_IF_NOT_REF(ppzv)      \//非引用下的变量分离

05
if (!PZVAL_IS_REF(*ppzv)) {             \

06
SEPARATE_ZVAL(ppzv);                \

07
}

08
#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)  \//非引用下的变量分离,并且设置引用

09
if (!PZVAL_IS_REF(*ppzv)) {             \

10
SEPARATE_ZVAL(ppzv);                \

11
(*(ppzv))->is_ref = 1;               \

12
}

13
#define SEPARATE_ARG_IF_REF(varptr) \     //引用下的变量分离

14
if (PZVAL_IS_REF(varptr)) { \

15
zval *original_var = varptr; \

16
ALLOC_ZVAL(varptr); \

17
varptr->value = original_var->value; \

18
varptr->type = original_var->type; \

19
varptr->is_ref = 0; \

20
varptr->refcount = 1; \

21
zval_copy_ctor(varptr); \

22
} else { \

23
varptr->refcount++; \

24
}

这里面谈到两个重要的概念:

1、非引用下的变量分离。
非引用下的变量分离,是指在一堆非引用变量中插入引用的情况下,在PHP内部进行的一种内存操作。以下面的列子来看:

1
$a = 1;

2
$b = $a;

3
$c = &$b;

在前两句执行之后,内存结构如下图

在第三句 $c = &$b;语句中则会执行“非引用下的变量分离。”,具体步骤是:

将b分离出来,同时把a对应的zval的refcount-1。
copy 出一个新的zval,并把zval的is_ref设置成1.
把C指向这个新的zval,同时refcount ++
最终效果如下图:

2、引用下的变量分离。

引用下的变量分离,是指在一堆引用变量中进行一个非引用赋值操作,这个时候会直接执行copy内存的操作。

以下面的例子来说

1
$a = 1;

2
$b = &$a;

3
$c = $b;

在执行完前两行后,PHP中内存结构如下:

在第三句,则会执行“引用下的变量分离”也就是真正的copy,最终内存结构如下图

据此,基本上对PHP变量内部的一些原理比较清楚了,但还有一些需要注意点的:
1、PHP变量的引用计数特性,对于数组同样也存在。但注意,对于key则不生效。(具体在后面章节会分析到。)
2、PHP变量中的对象比较特殊,在PHP5之后,默认都是采用引用赋值的方式。具体实现可以参考Zend_objects.*系列代码。
3、对于分析PHP内部变量,推荐采用xdebug_debug_zval,而不要采用内置的debug_zval_dump。因为PHP内置的debug_zval_dump函数一方面无法处理is_ref,而且采用了引用的方式来处理,从而导致看到结果会有误解。

使用技巧结论

据此可以得出分析出不少结论:

1、在PHP开发中不推荐采用引用。因为PHP内部对内存优化本身做了不少工作,引用不会带来太多优化。(但注意推荐非强制)

2、在PHP中strlen是o(1)的。

李萧明吐槽 只能说百度拿这么高工资还是有道理的。

支持快速迭代的LAMP解决方案 ——贴吧LAMP解决方案

No Comments 专业IT吐槽

摘要:天下武功,唯快不破,互联网竞争的利器就是快!且听贴吧LAMP解决方案如何全面支持快速迭代。

关键词:LAMP,快速迭代

领域:架构

总概

贴吧是功能性产品,唯快不破是永恒的准则,这一特点决定了快速迭代是需要解决的关键性问题。快速迭代,分解开来有如下部分:开发阶段,快速开发;测试阶段,包含了环境快速搭建、自动化测试工具;运维阶段,包含了集群管理技术、自动化运维工具;同时,这三方面的工作需要一个整体性的解决方案衔接起来。

早期的贴吧,作为一个高性能社区,功能相对单一,全部采用C语言开发,系统可重用程度低,开发、测试效率低,运维方面的积累也很少。为了提高效率,开始尝试LAMP架构,经过几年的发展,贴吧已全部迁移到了LAMP。随着产品规模急剧膨胀,30+子系统,150+模块,500+机器,10亿+流量,在LAMP架构方面积累了很多经验,逐渐形成了快速迭代的一体化方案。如下图所示:

该解决方案由开发阶段、测试阶段、运维阶段组成。开发阶段又分成接入层、业务逻辑层、存储层。该解决方案支撑大规模的线上应用,同时保持了快速迭代的特性。基于该解决方案,开发人员能专注于业务逻辑开发,测试人员能专注于持续集成,运维成本能大大降低。

开发

开发方面分为接入层、业务逻辑层、存储层。

接入层处于浏览器和后端服务之间,用来解析http协议并组织成相应的协议格式,完成客户端和服务器之间的通信,还包括攻击防范、页面缓存、负载均衡等多种功能。Web server是其核心组成部分。接入层的目标是通过统一的方案提供简单可依赖的接入层架构,经过全面调研nginx具有通用性强、效率高、功能全面、配置灵活等特点,是webserver未来发展的主力军,确定采用nginx统一接入层。

业务逻辑层包含了PHP框架、业务逻辑、LIB库、交互层。业务逻辑层常常包含一些开发规范,这些规范就像法律一样,我们不仅要有法可依,还要有法必依。在我们的解决方案中,PHP框架=规范+库,规范比如目录部署规范、URL规范、配置规范等,这些规范通过相应的库实现,以达到有法必依的目的。LIB库封装常用的功能。基于这个解决方案,开发者开发应用,只需完成业务逻辑部分。

中间层,如下图所示,包含在业务逻辑层中,对于业务逻辑层的快速迭代非常重要。中间层对下做交互抽象,支持各种协议屏蔽协议细节;通过资源定位屏蔽部署细节;通过负载均衡提高系统稳定性。中间层对上做接口抽象,支持服务整合、接口适配、公共逻辑。中间层首先建立系统–子系统–模块的体系,进行服务整合,图中的API-LIB就是根据子系统划分,将各模块的接口(MIDL: Module IDL)转化为子系统接口(SIDL: Service IDL);接口适配,SERVICE的接口通过SIDL描述,让接口描述、接口文档、线上代码等自动同步,可维护性大大提高,同时通过元数据规范保证全系统的接口一致,易用性大大提高;收敛公共逻辑,对于公共逻辑,比如权限逻辑,收敛起来可维护性大大提高。

存储层,提供各种通用服务、组件。其中的通用数据存储框架提供通用的数据存储和访问解决方案,以一种统一的设计模式来支持大多数数据存储模块的设计和实现;统一数据访问接口,对外部屏蔽数据拆分和存储的细节;做到数据存储的良好扩展性,通过通用的数据拆分模式来应对数据增长;将具有共性的需求抽象成通用服务或通用库,以简化设计和开发。

基于该解决方案,开发一个应用只需要:在接入层配置相应的分流,在业务逻辑层开发业务逻辑,使用存储层合适的服务或基于框架完成数据模块开发。能大大的提高开发效率,支持快速迭代。

测试

测试方面,为了支持快速迭代,必须提高自动化程度。而影响自动化的首要因素就是环境自动构建,常见的问题有:环境复杂,比如关联关系复杂;环境搭建代价过大;环境功能不完整等。采用基准环境能解决这一问题,项目上线后自动从scmpf更新到基准环境;测试环境/开发环境从基准环境同步。基于基准环境,系统级别的持续集成也成为可能,同时可以集中大量测试工具。

运维

运维方面面临着很多问题:服务迁移成本高,环境不一致带来各种回滚,机器利用率不均衡,运维自动化程度低。为了解决这些问题,提出PHP系统运维方案。环境同步方面,主要是代码同步的问题,采用运维规范+监控的方案;性能监控方面,基于交互层完成请求状态、交互性能监控,基于调度中心获取机器状态;机器调度方面,通过调度中心完成动态/半自动机器调度。如下图所示:

展望

通过该LAMP解决方案,在开发、测试、运维方面都能极大的提高效率。未来在LAMP架构方面,需要更多的在规范化、平台化上下功夫。规范之后才能开展这种自动化的工作提高效率;平台化可以把各种规范固化下来,提供自动化的支持。

by zhouren

 

李萧明吐槽 当初10年公司挖人,盛大旅游一哥们跑过来和CEO大侃快速迭代,尼玛被CEO骂了说你说的啥JB玩意,后来……。后来我们公司就倒闭了干。又一个悲催的回忆。

蚂蚁变大象:浅谈常规网站是如何从小变大的

No Comments 专业IT吐槽

2005年,我开始和朋友们开始拉活儿做网站,当时第一个网站是在linux上用jsp搭建的,到后来逐步的引入了多种框架,如webwork、hibernate等。在到后来,进入公司,开始用c/c++,做分布式计算和存储。(到那时才解开了我的一个疑惑:C语言除了用来写HelloWorld,还能干嘛?^_^)。

总而言之,网站根据不同的需求,不同的请求压力,不同的业务模型,需要不同的架构来给予支持。我从我的一些经历和感受出发,大体上总结了一下的一些阶段。详情容我慢慢道来。

【第一阶段 : 搭建属于自己的网站】

我们最先开始的网站可能是长成这个样子的:

拿Java做例子,我们可能会引入struts、spring、hibernate等框架,用来做URL分流,C、V、M隔离,数据的ORM等。这样,我们的系统中,数据访问层可以抽取出很多公用的类,业务逻辑层也可以抽取出很多公用的业务类,同一个业务逻辑可以对应多个展示页面,可复用性得到极大的增强。

不过,从性能上看,引入框架后,效率并不见得比第一种架构高,有可能还有降低。因为框架可能会大量引入“反射”的机制,来创建对应的业务对象;同时,也可能增加额外的框架逻辑,来增强隔离性。从而使得整体服务能力下降。幸好,在这个阶段,业务请求量不大,性能不是我们太care的事情。J

【第三阶段 :降低磁盘压力

可能随着业务的持续发展,或者是网站关注度逐步提升(也有可能是搜索引擎的爬虫关注度逐步提升。我之前有一个网站,每天有超过1/3的访问量,就是各种爬虫贡献的),我们的请求量逐步变大,这个时候,往往出现瓶颈的就是磁盘性能。在linux下,用vmstat、iostat等命令,可以看到磁盘的bi、bo、wait、util等值持续高位运行。怎么办呢?

其实,在我们刚刚踏进大学校门的时候,第一门计算机课程——《计算机导论》里面就给出了解决方案。依稀记得下面这个图:

在我们的存储体系里面,磁盘一般是机械的(现在Flash、SSD等开始逐步大规模使用了),读取速度最慢,而内存访问速度较快(读取一个字节约10μs,速度较磁盘能高几百倍),速度最快的是CPU的cache。不过价格和存储空间却递减。

话题切换回来,当我们的磁盘出现性能瓶颈的时候,我们这个时候,就要考虑其他的存储介质,那么我们是用cpu cache还是内存呢,或是其他形态的磁盘?综合性价比来看,到这个阶段,我个人还是推荐使用内存。现在内存真是白菜价,而且容量持续增长(我现在就看到64G内存的机器[截止2012-4-3])。

但是问题来了,磁盘是持久化存储的,断电后。数据不会丢失,而内存却是易失性存储介质,断电后内容会丢失。因此,内存只能用来保存临时性数据,持久性数据还是需要放到磁盘等持久化介质上。因此,内存可以有多种设计,其中最常见的就是cache(其他的设计方式会在后面提及)。这种数据结构通常利用LRU算法(现在还有结合队列、集合等多种数据结构,以及排序等多种算法的cache),用于记录一段时间的临时性数据,在必要的时候可以淘汰或定期删除,以保证数据的有效性。cache通常以Key-Value形式来存储数据(也有Key-SubKey-Value,或者是Key-List,以及Key-Set等形式的)。因为数据存放在内存,所以访问速度会提高上百倍,并且极大的减少磁盘IO压力。

Cache有多种架构设计,最常见的就是穿透式和旁路式。穿透式通常是程序本身使用对应的cache代码库,将cache编译进程序,通过函数直接访问。旁路式则是以服务的方式提供查询和更新。在此阶段,我们通常使用旁路式cache,这种cache往往利用开源的服务程序直接搭建就可以使用(如MemCache)。旁路式结构如下图:

请求来临的时候,我们的程序先从cache里面取数据,如果数据存在并且有效,就直接返回结果;如果数据不存在,则从数据库里面获取,经过逻辑处理后,先写入到cache,然后再返回给用户数据。这样,我们下次再访问的时候,就可以从cache中获取数据。

Cache引入以后,最重要的就是调整内存的大小,以保证有足够的命中率。根据经验,好的内存设置,可以极大的提升命中率,从而提升服务的响应速度。如果原来IO有瓶颈的网站,经过引入内存cache以后,性能提升10倍应该是没有问题的。

不过,有一个问题就是:cache依赖。如果cache出问题(比如挂了,或是命中率下降),那就杯具了L。这个时候,服务就会直接将大的压力压向数据库,造成服务响应慢,或者是直接500。

另外,服务如果重新启动时,也会出现慢启动,即:给cache充数据的阶段。对于这种情况,可以采取回放日志,或是从数据库抽取最新数据等方式,在服务启动前,提前将一部分数据放入到cache中,保证有一定命中率。

通过这样的拆分后,我们就迈出了多机的第一步。虽然看起来比较简单和容易,但是这也是非常具有里程碑意义的。这样的优化,可能会提升20-30%左右的一个CPU idle。能够使得我们的网站能够经受更大的压力。

【第五阶段 逻辑程序的多机化】

当我们的访问量持续增加的时候,我们承受这成长的快乐和痛苦。流量刷刷往上涨,高兴!但是呢,服务器叫苦不绝,我们的程序已经快到不能服务的边缘了。怎么办?

“快使用分布式,哼哼哈嘿”J

这个时候,我们就需要针对CPU瓶颈,将我们的程序分别放在多台服务器上,让他们同时提供服务,将用户请求分摊到多个提供服务的机器。

好,如果提供这样的服务,我们会遇到什么样的问题?怎么样来解决?

我们一个个的来分析:

一、WebServer怎么样来分流用户请求?That’s a good question!

在考虑这个问题的时候,我们常见的WebServer早已给我们想好了解决方案。现在主流的WebServer几乎都提供一个叫“Load Balance”的功能,翻译过来就是负载均衡。我们可以在WebServer上配置一组机器列表,当请求来临的时候,WebServer会根据一定的规则选取某一台机器,将请求转发到对应的逻辑处理程序上。

有同学马上就会问了,“一定的规则”是怎么样的规则?他能解决什么样的问题?如果机器宕了怎么办?……

哈哈,这里的一定规则就是“Load Balance”。负载均衡其实是分布式计算和存储中最基础的算法,他的好坏,直接决定了服务的稳定性。我曾经设计和开发了一个负载均衡算法(现在正在大规模使用),有一次就因为一个很小的case,导致服务大面积出现问题。

负载均衡要解决的就是,上游程序如何在我们提供的一堆机器列表中,找到合适的机器来提供下游的服务。因此,我们可以将负载均衡分成两个方向来看:第一,根据怎样的规则来选机器;第二,符合规则的机器中,哪些是能提供服务的。对于第一个问题,我们通常使用随机、轮询、一致Hash等算法;对于第二个问题,我们要使用心跳、服务响应判定等方法检测机器的健康状态。关于负载均衡,要谈的话点其实很多,我之前也写过专门的一篇文章来介绍,后续有空了,我再详细的描述。

总之,分流的问题,我们可以通过负载均衡来比较轻松的解决了。

二、用户的session如何来同步?That’s a good question,TOO!

虽然HTTP协议是一个无状态的服务协议,但是,用户的基本信息是要求能够保证的。比如:登录信息。原来在单机的时候,我们可以很简单的使用类似setSession(“user”, ”XXX”)的函数来解决。当使用多机的时候,该怎么样来解决呢?

其实这个问题也是当年困扰我很久的一个问题。如果用setSession,用户在某一台机器上登录了,当下次请求来的时候,到其他机器了,就变成未登录了。Oh,My God!

Ok,让我们一个个的来看:

1、一台机器登录,其他机器不知道;

2、用户请求可能到多台机器。

对于第一个问题,如果我们在一台机器的登录信息让其他机器知道,不就OK了嘛。或者,大家都在一台机器上登录,不就可以了嘛。     对于第二个问题,如果我们让同一个用户的请求,只落在同一台机器上,不就OK了嘛。因此,我们可以提出三种解决方案:

1、提供session同步机制;

2、提供统一session服务;

3、将同一用户分流到同一机器。

嗯,这三种方式,你会选哪个呢?如果是我,我就选最后一个。因为我是一个懒虫,我会选最简单的一个,我信奉的一个原则既是:简单粗暴有效!哈哈。在WebServer层使用一致Hash算法,按session_id进行分流(如果WebServer没有提供该功能,可以简单写一个扩展,或者干脆在WebServer后面做一个代理即可)。但是这种方案有一个致命的问题,当一台机器宕机了以后,该机器上的所有用户的session信息即会丢失,即使是做了磁盘备份,也会有一段时间出现session失效。

好,那看看第一种方案。其实现在有一些框架已经提供了这样的服务机制。比如Tomcat就提供session同步机制。利用自有的协议,将一台机器上的session数据同步到其他的机器上。这样就有一个问题,我需要在所有的机器上配置需要同步的机器,机器的耦合度瞬间就增加了,烦啊!而且,如果session量比较大的话,同步的实效性还是一个问题。

那再来看看第二种方案,提供统一session服务。这个就是单独再写一个逻辑程序,来管理session,并且以网络服务的方式提供查询和更新。对于这样的一个阶段的服务来讲,显得重了一些。因为,我们如果这样做,又会面临一堆其他的问题,比如:这个服务是否存在单点(一台服务器,如果宕机服务就停止),使用什么样的协议来进行交互等等。这些问题在我们这个阶段都还得不到解决。所以,看起来这个方案也不是很完美。

好吧,三种方案选其一,如果是你,你会选哪一种呢?或者还有更好的方案?如果我没钱没实力(传说中的“屌丝”,哈哈),我就可能牺牲一下服务的稳定性,采用代价最低的。

三、数据访问同步问题。

当多个请求同时到达,并且竞争同一资源的时候(比如:秒杀,或是定火车票),我们怎么来解决呢?

这个时候,因为我们用到了单机数据库,可以很好的利用数据库的“锁”功能来解决这个问题。一般的数据库都提供事务的功能。事务的级别分多种,比如可重复读、串行化等,根据不同的业务需求,可能会选择不同的事务级别。我们可以在需要竞争的资源上加上锁,用于同步资源的请求。但是,这个东东也不是万能的,锁会极大的影响效率,所以尽量的减少锁的使用,并且已经使用锁的地方尽量的优化,并检查是否可能出现死锁。

cache也有对应的解决方案,比如延迟删除或者冻结时间等技术,就是让资源在一段时间处于不可读状态,用户直接从数据库查询,这样保证数据的有效性。

好了,上述三个问题,应该涵盖了我们在这个阶段遇到的大部分问题。那么,我们现在可以把整体的架构图画出来看看。

这样的结构,足够我们撑一段时间了,并且因为逻辑程序的无状态性,可以通过增加机器来扩展。而接下来我们要面对的,就是提交增长和查询量增加带来的存储性能的瓶颈。

【第六阶段 读写分离,提升IO性能】

好了,到现在这个阶段,我们的单机数据库可能已经逐步成为瓶颈,数据库出现比较严重的读写冲突(即:多个线程或进程因为读写需要,争抢磁盘,使得磁盘的磁头不断变换磁道或盘片,造成读写都很缓慢)。

那我们针对这样的问题,看看有哪些方法来解决。

一、减少读取量。我们所有的问题来源就是因为读写量增加,所以看起来这个是最直接最根源的解决办法。不过,用户有那么大请求量我们怎么可能减少呢?其实,对于越后端的系统,这是越可能的事情。我们可以在每一层都减少一部分往后传输的请求。具体到数据库的话,我们可以考虑通过增加cache命中率,减少数据库压力。增加cache命中率有很多中方法,比如对业务访问模式进行优化、多级cache模式、增加内存容量等等。业务模式的修改不是太好通用,因此这里我们考虑如何通过增加内存容量来解决问题。

对于单机,现在通用的cache服务一般都可以配置内存大小,这个只需要很简单的配置即可。另外,我们也可以考虑多机cache的方案,通过增加机器来扩充内存容量。因此,我们就引入了分布式cache,现在常用的cache(如:memcache),都带有这样的功能,支持多机cache服务,可以通过负载均衡算法,将请求分散到多台不同的机器上,从而扩充内存容量。

这里要强调一点。在我们选择均衡算法的时候,是有考虑的。这个时候,常常选贼一致Hash算法,将某一系列ID分配到固定的机器,这样的话,能放的KV对基本等于所有机器相加。否则,如果不做这样的分配,所有机器内存里面的内容会有大量重复,内存并没有很好的利用。另外,因为采用一致Hash,即使一台机器宕掉,也会比较均匀的分散到其他机器,不会造成瞬间其他机器cache大量失效或不命中的问题。

二、减少写入量。要减少用户的提交,这个看起来是不太现实的。确实,我们要减少写入的量似乎是很难的一件事。不过也不是完全不可能。这里我们会提到一个思想:合并写入。就是将有可能的写入在内存里进行合并,到一定时间或是一定条件后,再一起写入。其实,在mysql等存储引擎内部,都是有这样的机制的。打个比方,比如有一个逻辑是修改用户购买物品的数量,每次用户购买物品后,计数都加一。如果我们现在不是每次都去实时写磁盘,而是到一定的时间或一定次数后,再写入,这样就可以减少大量的写入操作。但是,这里需要考虑,如果服务器宕掉以后,内存数据的恢复问题(这一部分会在后面来描述)。因此,如果想简单的使用数据合并,最好是针对数据重要性不是很强的业务,即使丢掉一部分数据,也没有关系。

三、多机承担请求,分散压力。如果我们能将原来单机的服务,扩充成多机,这样我们就能很好的将处理能力在一定限度内很好的扩展。那怎么来做呢?其实有多种方法,我们常用的有数据同步和数据订阅。

数据同步,我们将所有的更新数据发送到一台固定的数据服务器上,由数据服务程序处理后,通过日志等方式,同步到其他机器的数据服务程序上。如下图:

这种结构的好处就是,我们的数据基本能保证最终一致性(即:数据可能在短暂时间内出现不一致,但最后的数据能达到一致),而且结构比较简单,扩展性较好。另外,如果我们需要实时数据,我们可以通过查询Master就行。但是,问题也比较明显,如果负责处理和分发的机器挂掉了,我们就需要考虑单点备份和切换方案。

数据订阅,我们也可以通过这样的方式来解决数据多机更新的问题。这种模式既是在存储逻辑和数据系统前,增加一个叫做Message Queue(消息队列,简称MQ)的东东,前端业务逻辑将数据直接提交到MQ,MQ将数据做排队等操作,各个存储系统订阅自己想要的数据,然后让MQ推送或自己拉取需要的数据。

MQ不带任何业务处理逻辑,他的作用就是数据转发,将数据转发给需要的系统。其他系统拿到数据后,自行处理。

这样的结构,好处是扩展比较方便,数据分发效率很高。但是问题也比较明显,因为处理逻辑分散在各个机器,所以数据的一致性难以得到保证。另外,因为这种模式看起来就是一个异步提交的模式,如果想得到同步的更新结果,要做很多附加的工作,成本很高且耦合度很大。还有,需要考虑MQ的单点备份和切换问题。

因为现在数据库(如Mysql)基本带有数据同步功能,因此我们在这个阶段比较推荐数据同步的方法。至于第二种方式,其实是很好的一种思想,后续我们会有着重的提及。那再来看我们的架构,就应该演变成这样的结构。

到目前这个阶段,我们基本上就实现了从单机到多机的转变。数据的多机化,必然带来的问题:一致性!这个是否有解决方案?这个时候我们需要引入一个著名的理论:CAP原理。

CAP原理包含了三个要素:一致性(Consistency)、可用性(Availability)、分区容忍性(Partition tolerance)。三个要素中,最多只能保证两个要素同时满足,不能三者兼顾。架构设计时,要根据业务需要进行取舍。比如,我们为了保证可用性和分区容忍性,可能会舍去一致性。

我们将数据分成多机,提高了系统的可用性,因此,一致性的保证很难做到强一致性。有可能做到最终一致性。这也是分布式引入以后的烦恼。

这样的一个系统,也是后续我们分布式架构的一个雏形,虽然比较粗糙,但是他还是比较简单实用,对于一般中型网站,已经能很好的解决问题。

【第七阶段 拆分】

到上面一个阶段,我们初步接触到了逻辑、存储等的多机模式。这样的结构,对于逻辑不是特别复杂的网站,足以撑起千万级的压力。所以大多数网站,只要能够用好上面的结构就可以很好的应对服务压力了。只不过还有很多细节的工作需要精细化,比如:多机的运维、稳定性的监控、日志的管理、请求的分析与挖掘等。

如果流量持续增长,或者是业务持续的扩展,上述的架构可能又将面临挑战。比如,多人开发常常出现版本冲突;对于数据库的更新量变大;一个表里面的记录数已经超过千万甚至过亿等等。

怎么解决呢?还记得我们之前介绍过一个CAP理论嘛?三要素里面有一个东东叫:分区容忍性(Partition tolerance)。其实,这个就是我们接下来解决问题的基础:切分!

一、从数据流向来看,切分包括:请求的切分、逻辑的切分、数据的切分。

数据的切分:将不同的数据放到不同的库中,将原来的单一的一个库,切分成多个库。

逻辑的切分:将不同的业务逻辑拆分成多份代码,用不同的代码管理路径来管理(如svn目录)。

请求的切分:将不同的逻辑请求分流到不同的机器上。比如:图片请求、视频请求、注册请求等。

二、从数据组织来看,切分包括:水平切分、垂直切分。

数据库的变大通常是朝着两个方向来进行的,一个是功能增加,导致表结构横向扩展;一个是提交数据持续增多,导致数据库表里的数据量持续纵向增加。

数据量变大以后,单机性能会下降很明显,因此我们需要在合适的时候对数据进行切分(这个我没有太深入的研究过相关数据库的最合适的切分点,只是从经验上来讲,单表的字段数控制在20个以内,记录数控制在5千万以内会比较好些)。

垂直切分和水平切分,其实是挺纠结的两个词。我之前对这两个词经常搞混。后来自己画了个图,就很直接明了了。

水平切分:

水平切分就是因为记录数太多了,需要横着来一刀,将原来一张表里面的数据存入到多张表中,用于减少单张表里的数据量。

垂直切分:

垂直切分就是因为业务逻辑需要的字段太多,需要竖着来一刀,将原来放在一张表里的所有字段,拆分成多张表,通过某一个Key来做关联(如关系数据库中的外键),从而避免大表的产生。

好了,有了上述的基础以后,我们再来看实际问题如何来解决。

假设,现在我们有一个博客网站,这个网站拥有多个功能,如:图片、博客、用户信息等的插查删改操作。而现在博客数据膨胀比较厉害。

首先,我们从数据流向来看,用户访问博客、图片、用户信息等这几个逻辑没有直接的耦合,对应的业务逻辑关联也很少。

因此,我们第一步从入口上就可以把三者分开。最简单的方式就是通过域名来切分,比如:img.XXX.com、blog.XXX.com、user.XXX.com。然后通过不同的WebServer来接收这些请求。

第二步,我们的业务逻辑代码,很明显可以将这些逻辑分开(从部署上分开)。一部分专门处理图片的请求,如ImageUploadAction/ImageDisplayAction/ImageDeleteAction,一部分专门处理博客请求,如:BlogDisplayAction/BlogDeleteAction,一部分专门处理用户相关请求,如:UserModifyAction/UserDisplayAction等等。

第三步,从数据库存储上,将三者剥离开。简单的就是分成三个不同的库。

这样,从数据流向上,我们就按不同的功能,将请求进行了拆分。

其次,从数据存储上来看,由于博客数据量增长比较快,我们可以将博客的数据进行水平的拆分。拆分方法很多,比如常用的:

1、按区间拆分。假定我们用blog_id作为Key,那么我们可以每1千万,做一次切分。比如[1,1kw)、[1kw,2kw)等等。这样做的好处就是可以不断的增长。但访问可能会因为博客新旧的原因,集中到最新的几个库或表中。另外,要根据数据的增长动态的建表。

2、按取模拆分。比如我们预估我们的blog_id最多不超过10亿,如果每张表里面我们预估存入1千万的数据,那么我们就需要100张表(或库)。我们就可以按照blog_id % 100 这样来做切分。这样做的好处是简单,表在一开始就全部建立好了。且每个表或者库的访问都比较均匀。但问题就是,如果数据持续扩张,超出预期,那么扩展性就成为最主要的问题。

3、其他还有一些衍生的方式,比如按Hash值切分等等,大多大同小异。

这样一来,我们通过访问模式、数据组织等多个维度的拆分以后,我们单机能够提供服务的能力就变的比较强悍了。具体的架构如下图。

上述结构看似比较完美,但是在实际的使用中可能会遇到以下几个问题:

1、业务关联问题。多个Service之间不可能没有任何关联,如果出现关联,怎么办?特别是如果是提交的信息要修改多个业务的数据的时候,这个会比较头疼。

2、服务运维问题。这样拆分以后,随着机器数量的膨胀,对于机器的管理将会变的愈发的困难。这个问题直接会影响到整体架构的设计。面向运维的设计是架构设计中必须要考虑的重要因素。

3、还有一个问题是我们WebServer始终是单机的,如果出现宕机等问题,那影响将是致命的。这个我们还没有解决。

这些问题都会在接下来的部分详细来解决。

【第八阶段 : WebServer多机化】

         上面说了这么多,我们的业务都基本上运转在只有一个WebServer的条件下。如果出现宕机,所有服务就停掉了;如果压力大了,单机不能承载了,怎么办?

         说到这个话题,我们需要来回顾一下在大学时学习的关于网络的基本知识。^_^

         抛开复杂的网络,我们简化我们的模型。我们的电脑通过光纤直接连入互联网。当我们在浏览器地址栏里面输入http://www.XXX.com时,到我们的浏览器展现出页面为止,中间出现了怎么样的数据变化?(注意:为了不那么麻烦,我简化了很多东西,比如:NAT、CDN、数据包切片、TCP超时重传等等)

上面的图我们应该比较熟悉,同时也应该比较清晰的表达了我们简化后,从输入网址到页面展现的一个过程。中间有两个东西我们比较关注,也是解决我们WebServer多机化的关键。

         1、DNS服务是否能帮我们解决多机化?

         2、www.XXX.com服务器的WebServer如何多机化?

         首先,如果DNS解析能够根据我们的请求来区分,对于同一域名,将不同的用户请求,绑定到不同的ip上,这样,我们就(友情提示:word统计此处已经达到10000字)能部署多个WebServer,对应不同的ip,剩下的无非就是多申请几个ip地址而已。

         当我们网站比较小的时候,我们都是在代理商处购买域名并由代理商的服务域名解析服务器帮我们做域名解析。但是,对于许多大型的网站,都需要对类似于www.XXX.com、blog.XXX.com、img.XXX.com等在XXX.com根下的所有服务的进行域名解析,这样便于对服务进行控制和管理。而域名的解析往往有专门的策略来处理,比如根据IP地域、根据不同请求IP的运营商等返回不同的服务器IP地址。(大家可能以前也有过这样的经验:在不同的地方,ping几个大的网站,看到的ip是不一样的)。

DNS策略分析和处理服务是对请求IP进行分析和判断的系统,判断请求来自哪个地域、哪个运营商,然后根据内部的一些库的判断,决定应该返回哪个WebServer的IP。这样,就能尽量保证用户以最快的速度访问到对应的服务。

         但是,如果我们有大量的WebServer,那每个Server都要有一个IP,另外,我们要增加一个新机器,又要申请一个IP地址,好像很麻烦,且不可接受。怎么办呢?

         第二点,我们需要考虑对于服务器的WebServer的多机化方式。

         我们为什么要WebServer多机化?原因就是因为单机的处理性能不行了,我们要提升处理能力。

         那WebServer要做哪些事情?Hold住大量用户请求连接;根据URL将请求分流到不同逻辑处理的服务器上;有可能还有一些防攻击策略等。其实这些都是消耗CPU的。

         如果我们在WebServer前端增加一层,什么逻辑都不处理,就是利用一定的负载均衡策略将数据包转发给WebServer(比如:工作在IP层,而非TCP层)。那这一层的处理能力跟WebServer比是否是要强悍很多?!这样的话,这一层后面就可以挂载很多的WebServer,而无需增加外网IP。我们暂且叫这一层叫VS(Virtual Server)。这一层服务要求稳定性较高,且处理逻辑要极为简单,同时最好工作在网络模型中较低的层次上。

这样的话,我们就只需要几个这样的VS服务器组,就可以组建大量的WebServer集群。当一个群组出现问题,直接可以通过改变IP绑定,就可以切换到其他服务器组上。

         现在这样的VS实现有多种。有靠硬件方式实现的,也有靠软件方式实现的。硬件方式实现的话,成本较高,但稳定性和效率较好。软件方式实现的,则成本较低,但稳定性和效率较硬件方式要低一些。

         现在用的比较多的有开源的LVS(Linux Virtual Server),是由我国的一个博士写的,NB!以及根据LVS改写后的一些变种。

         另外还有F5 Networks公司出的收费的F5-BIG-IP-GTM等。(注:这个确实没用过,以前在网上看过,写到此处记不清,在百度上搜的。如有错误,敬请雅正)。

         好了,通过上述的方式,我们基本实现了WebServer的多机化。

【第九阶段 逻辑关联和层次划分】

         在第七阶段的时候,我们提到了几个问题,其中有一个就是业务关联问题。当我们将业务拆分以后,多个业务之间没有了耦合(或者是极弱的耦合),能够独立的运转。这个看起来是多么美妙的事情。但是实际情况真是如此嘛?

         这样的业务还真是存在的。比如我们有两个业务blog和image。blog可以上传和展示图片。那image.XXX.com就提供两个HTTP服务,一个是上传的,一个是显示的。这样,blog业务就可以通过简单的URL耦合来实现了图片的这些功能。

         但真是所有的情况都是如此的嘛?

         我们再看一个例子。比如blog和用户相关的业务。用户可以在blog登录、注销等,blog需要实时判断某一个用户是否登录等。登录和注销两个操作似乎可以通过类似account.XXX.com提供的login和logout这样的URL接口实现。但是每次页面浏览要判断用户是否已经登录了,出于安全性等多方面的考虑,就不好通过URL来提供这样的服务。

         那看起来,我们在第七阶段提出的按业务切分的理想情况,在实施的时候,并不是那样的完美。在实际的运行中,耦合是不可避免的。

         有了耦合,我的第一反应基本上就是看看是否能够借助设计模式来解决这些的问题。其实呢,设计模式早已经给我们比较好的解决方案(但绝对不是完美的解决方案。俗话说的:没有最好,只有更好!)。在这篇文章的最初已经提到过了,为了增强网站代码的可重用性,我们引入了一些框架,比如:struts、spring、hibernate等等。其实这些框架,基本上是围绕着MVC的原则来设计的。struts、webwork等框架,将视图和逻辑控制分离;spring负责组织业务逻辑的数据;hibernate很好的做了数据访问层的工作,实现了ORM。

         那现在我们采用多机分布式的时候,是否可以借鉴这些思想呢?其实也是可以的。

         我们来分析一下我们的业务。

         其实我们的业务大多可以分为两类:

         一、与实际的产品相关的业务,比如:blog、news等等。这些业务之间的耦合度不是很高,往往可以通过提供HTTP的接口即可实现业务需要的互通。因此,从这个层面上来看,是可以基本做到业务垂直拆分的。

         二、基础服务,比如:用户帐号管理、消息通知等等。这些服务往往被多个业务所依赖。他们需要提供更通用的、更安全的、更稳定的接口和服务。但是,关于基础业务的理解和划分,是没有一个特定的规则的。比如,image图片服务,他有可能刚开始是一个业务服务,到一定阶段以后,多个系统需要对他有强的依赖,自然也就成为了一个基础服务。

         所以,从上面的描述,我们可以发现服务的类型,并不是固定的。要很好的解决服务耦合的问题,也并没有一个十分完美的解决方案。我们能做的就是尽量降低耦合,通过某种方式,能够很好的达到耦合和可维护性的平衡。

         总的来看,MVC模式其实给我们提供了一个比较好的方案。

         我们把系统从两个维度上进行划分:垂直(业务)和水平(逻辑)。

我们做了这样的划分以后,似乎看起来没有实质性的改变。但是,我们可以明确了我们设计的原则,并强化了代码的可复用性。另外,最关键的是,服务之间的依赖和耦合关系,有了明确的地方来做。同时,我们还可以将业务内部的结构进行拆分,更好的增强复用。

         数据访问和组织层、数据存储层,这两个位于下游的层次,应该是属于系统内部的层次,原则上是最好不要对外开放接口的,否则,系统间的耦合就会非常的大。并且可维护性会非常的难。而逻辑控制和视图层,实际上是提供对外(对用户或者是外系统)最好的访问的入口。当然,这个入口可以是HTTP协议的,也可以是非HTTP协议的。

         比如,对于account服务,可以提供基于HTTPS对用户开放的login和logout服务,也提供基于XXX Protocol数据交换的协议的给内部的get_session服务。从简单的设计上来看,只是根据服务不同,提供不同的数据交换格式、以及不同的安全控制。这样也是秉承了一个高内聚低耦合的原则。

         这里还有几个及其重要的问题没有详细的提及:系统内外的数据传输协议、接口API、服务访问定位。这几个问题实际上还跟运维问题紧密相关,都会放到后面来详细讨论。

【第十阶段 数据存储优化】

         在前面的阶段中,我们都使用数据库作为默认的存储引擎,很少谈论关于关于数据存储的话题。但是,数据的存储却是我们现在众多大型网站面临的最核心的问题。现在众多网络巨头纷纷推出自己的“高端”存储引擎,也吸引了众多的眼球。比如:google的BigTable、facebook的cassandra、以及开源的Hadoop等等。国内众多IT巨头也纷纷推出自己的“云”存储引擎。

         其实这些存储引擎用的一些关键技术有许多的共性,比如:Meta信息管理、分片、冗余备份、数据自动恢复等。因为之前我也做过一些工作和研究,但是不是特别深入,不敢在此指手画脚、高谈阔论。相关的资料网上比比皆是,大家有兴趣有search吧。^_^

         关系型数据库常用的有几个,比如:MySQL、PostgreSQL、SQLServer、DB2等,当然还有NB的Oracle(唉,作为DB科班出生的屌丝,没有真正意义上使用过这样的高帅富数据库,惭愧啊)。

         互联网使用频率最高的应该要算是MySQL。最重要的是开源;其次是他提供的一些特性,比如:多种存储引擎、主从同步机制等,使用起来非常的方便;再次,就是一个单词:LAMP,几乎成为搭建网站的必备利器;还有,较高服务的稳定性。

         关系型数据使得建立网站变得及其轻松,几乎是个网站都会有一个数据库。试想一下,如果没有这种通用的关系型数据库,我们的生活会是怎么样的?

         关系型数据库在95%的场景下是工作的非常好的。而且只要配置得当、数据切分合理、架构设计符合要求,性能上是绝对能够承受业务的需求。现在很多大型网站的后台,几乎都是数据库作为标准的存储引擎。

         另外,最近炒的比较热的一个概念就是NoSQL。说起来,其实就是放弃关系型数据库中许多的特性,比如:事务、外键等等。简化设计,将视线更关注于存储本身。比较有名的,比如:BDB、MongoDB、Redis等等。这些存储引擎提供更为直接的Key-Value存储,以满足互联网高效快速的业务需求。其实,从另外一个角度来看,关系型数据库(比如MySQL),如果不使用那么多的关系型数据库特性,也可以简化成KV模式,提升效率。

         不过,有些时候,为了节省机器资源,提升存储引擎的效率,就不得不开发针对业务需要的专用存储引擎。这些存储引擎的效率,往往较关系数据库效率高10-100倍。比如,当一个图片服务,存储的图片量从1亿到10亿,甚至100亿;现在流行的微薄,假如发布总量达到10亿或者100亿。这样级别的数据量,如果用数据库来存储固然可以,但是有可能需要耗费相当多的机器,且维护成本和代价不小。

         其实分析我们通常的业务,我们对数据的操作无非就是四个:查、插、删、改。对应数据库的操作就是select、insert、delete、update。那自己设计的存储引擎无非就是对这四个操作中的某几个做针对性的优化,让其中几个根据不同的业务特点,使其变的更加的高效。比如:对于微薄而言,可能就需要插入和查询具有很高的效率,而删除和修改的需求不是那么高。这个时候,就可以牺牲一部分删除和修改的性能,而重点放在插入和查询上。

         在业务上,我们的提交通常可以看作在某一个维度上是有序的。比如,每一个微薄或者博客可能都会有一个id,这些id可能是按照序列递增、或是时间递增等。而查询的时候,则是按照另外一个维度的顺序组织的,比如:按关注的人组织微薄的信息。这样就造成了一个冲突,就是提交的组织顺序和查询的组织顺序不一致。

         再来看看我们的磁盘。我们现在常规磁盘还是机械方式运转的:有盘片、有磁头等等。当需要写入或者读取的时候,磁头定位到不同的盘片的不同扇区上,然后找到或修改对应的信息。如果信息分散在不同的盘片、扇区上,那么磁头寻道的时间就会比较长。

         反过来,我们再来看看我们的业务和磁盘的组织。A、如果我们按照写入有序的方式存储数据,那么磁盘会以很高的效率,将数据连续的写入到盘片中,无需多次寻道。那么读取的时候,可能就会出现按照另外的维度来组织数据,这样就有可能需要在多个地方来读取。从而造成我们磁盘来回寻道定位,使得查询效率低下。B、如果我们按照某一种查询维度来存储数据(因为同一业务往往有多种查询模式),那读取的时候,就让磁盘顺序读取即可。但带来的麻烦就是,写入的时候有可能需要反复的寻道定位,将提交的数据一条条写入。这样就会给写入带来麻烦。

         其实矛盾的主体就是:僵化的磁盘存储方式不能满足网站日益增长的提交和查询需求!

         是否有解决方案呢?那必须有!

         要解决这个问题,可以从两方面入手。

         1、改变现有的磁盘存储方式。随着硬件快速的发展,磁盘本身的效率得到了极大提升。磁盘的转速,盘片的个数等都大幅增加,本身寻道的速度提升很快。加上缓存等的加强,效率提升还是很明显。另外,Flash Disk、Solid State Disk等新技术的引入,改变了原来的随机读取的低效(没有数据,根据经验,Flash或SSD的效率可以达到10-100倍普通硬盘的随机访问的效率)。

         2、根据不同的业务,有效的组织数据。比如微薄(我没有写过微薄,但是做过微薄类似的东东),因为读取业务组织的维度是按人,而提交组织的维度则是时间。所以,我们可以将某个人一段时间提交的数据,在内存里面进行合并,然后再一次性的刷入到磁盘之中。这样,某个人一段时间发布的数据就在磁盘上连续存储。当读取的时候,原来需要多次读取的数据,现在可以一次性的读取出来。

         第一点提到的东东现在也逐步的开始普及,其实他给我们的改变是比较大的。不用花太多的精力和时间,去精心设计和优化一个系统,而只需要花一些钱就能使得性能大幅提升,而且这样的成本还在降低。

         但是,资源永远是不够的。多年前,当内存还是64K的时候,我们畅想如果内存有个32M该多美妙啊。但是,随着数据的膨胀,即使现在64G内存,也很快就不能满足我们的需求。

         所以,在一些特殊的应用下面,我们还是需要更多的关注第二个点。

         对于存储优化,一直是一个持久的话题,也有很多成熟的方案。我这里可能提几个点。

【阶段性小结】

         经过了上述的架构扩展和优化以后,我们的系统无论是从前端接入,还是后端存储都较最初的阶段有了质的变化。这样的架构足以支撑起10亿级别的流量和10亿级别的数据量。我们具体的来看一下整体的架构。

  上述的模型是我个人觉得的一个比较理想的模型。Virtual Server Cluster接收数据包,转发给Web Server Cluster或者Private Protocol Server Cluster(如果有的话)。然后视图和逻辑层server负责调用cache或者数据访问组织层的接口,返回处理后的数据。定制存储系统、通用存储系统和数据库集群,提供基本的数据。

         每一个层次通过负载均衡和一定的协议来获取下一层提供的数据,或者提交数据。在存储系统内部,通过Meta信息管理、主从同步、消息订阅等方式,实现数据的同步。

         如果我们再要扩大规模,比如:机器数扩展到上千台、万台。对于我们来说,管理机器就成为了机器头等的大问题。

         同时,我们之前还有几个问题没有很详尽的描述,比如:数据传输协议、远程系统调用、系统的异构性等等,这些都是会影响到我们系统可维护性的大问题。

         我7年前就开始使用Java,到现在,总算能看懂一些东西了。J2EE我个人觉得确实是一个比较伟大的东东。里面其实早已经提出了一套比较完善的解决大型或者超大型网站的整体解决方案。

         比如:

         1、JNDI(Java Naming and Directory Interface):描述了如何使用命名和目录等的规范,使得我们能够将服务作为资源挂接到命名服务器上,并且通过标准的接口进行访问。这对我们管理巨大的机器资源和服务提供了很好的方案。

         2、RMI(Remote Method Invoke):远程过程调用。即,通过标准的RMI接口,可以轻松实现跨系统的远程调用。

         3、IDL(Interface Definition Language):接口定义语言。这个本来是CORBA中用来访问异构系统对象的统一语言,其实也给我们提供了跨系统调用中,对外接口的定义方案。

         4、JDBC(Java Database Connectivity):数据库访问接口。屏蔽了不同数据库访问的实现细节,使得数据库开发变得轻松。

         5、JSP(Java Server Page):实现将视图和控制层很好分离的方式。

         6、JMS(Java Message Service):用于和面向消息的中间件相互通信的应用程序接口。提供了很好的消息推送和订阅的机制。

         以上这些组件其实很好的协助构建了J2EE整体架构。我的很多想法都来源于这些东东。后续会结合这些,详细来分析诸如资源命名位置服务、数据传输协议、异构系统接口定义等解决大规模机器运维问题的方案。

【第十一阶段 :命名位置服务】

         在前面我们不止一次提到了命名位置服务(Naming & Location Service)。在不同的架构或者公司里面,这个名字往往不一样,比如,在java里面叫JNDI(Java Naming & Directory Interface),在有些地方可能会叫做资源位置系统(Resource Location System)。

         总之,不管叫什么名字,我们要知道的就是为什么要有这样的系统?他能做哪些事情?他有哪些实现方式?等等。

         在我们之前的章节中,我们的服务从一台单机扩展到十台左右的多机,到成百上千台机器。我们的服务从单一的一个服务扩展到成百上千的服务。这么多的机器、服务,如果不好好管理,我们就崩溃了。比如,我们的服务A要连接服务B,如果现在采用配置IP的方式,可能需要配置几十台机器的IP,如果其中某些机器出现了变更,那所有服务A连接服务B的IP都要改变。如果所有的服务都是这样,这将是多么痛苦的一件事情 Orz。

         如果我们只是简单的将我们的服务看成是一个个的资源(Resource),这些资源可以是数据库,可以是cache,可以是我们自己写的服务。他们都有一个共同的特点,就是在某一个IP上,打开一个PORT,遵循一定的协议,提供服务。

         我们先简单的来构建这样一个模型。我们这里先抽象一个接口,叫做Interface Resource。这个接口下面有多个实现,比如:DBResource、CacheResource、ImageResource等等。具体类图如下:

有了这样的一个层次结构以后,我们为了得到某一个实例,有多种方式,比如:

         1、直接生成的方式:

                   Resource r = new ImageResource();

         2、间接生成的方式:

                   A、比如我们在设计模式中经常使用到的工厂模式:

                   Resource r = ResourceFactory.get(“Image”);

                   B、IoC(Inversion of Control)方式:

                   Resource r = (Resource)Container.getInstance(“ImageResource”);

         我们打一个不是很完全匹配的比方。我们如果直接在服务中采用IP配置的方式,就类似于直接生成实例一样,如果实例发生变化,或者要调整生成的对象,就需要修改调用者的代码。也就是说,如果其他服务的IP发生变化,我们调用者就需要修改配置,重启程序等等。而且对于如果有很多很多这样的服务,我们就崩溃了。

         那么,我们觉得更好的一种方式呢,就是,如果有一个工厂,或者一个容器,来帮我们管理这一堆的服务IP、端口、协议等等,我们要用的时候,就只需要叫一声:“给我XXX服务的实例”,那是多么美妙的事情啊!

         其实呢,我们的命名位置服务要做的就是这样的事情。他类似于一个Meta Server,记录所有服务的IP、port、protocol等基础信息,以及检查这些服务的健康状态,提供给调用者最基础的信息服务。同时,再结合调用时的负载均衡策略,就可以帮我们提供很好的资源管理方式。

         这个服务,提供注册、注销、获取列表等接口。他的存在,就将直接关联的两个服务给很好的解耦了。我们看看对比:

在没有Naming Location Service的时候,我们的服务相互直接依赖,到最后,关联关系及其复杂,可能完全没有办法维护。

         如果我们增加Naming Location Service以后,这个状态就可以得到极大的改善。

这个时候,我们所有的服务都在NLS上注册,同时向NLS获取其他服务的信息,所有的信息都汇聚到NLS上管理。

         有了这个服务,就好类比成我们生成一个类的时候,采用间接的方式生成。

         Service s = (Service) NamingService.getService(“Image”);

         好,有了这样一个架构以后,我们可能会关注这个NLS如何来实现。

         实现这个服务有简单的方式,也有复杂的方式。关键是要考虑以下几个方面:

         1、如何找到这个NLS。NLS是所有服务的入口,他应该是有一个不变的地址来保证我们的服务。因此,我们可以使用我们之前提到过的Virtual Server的方案,通过一个(或多个)固定的域名或者IP来绑定这个服务。

         2、可用性(Availability)和数据一致性(Consistency)。因为这个服务是一个最基础的服务,如果这个服务挂掉了,其他服务就没有办法来定位了。那么这个服务的稳定和可靠性就是及其重要的。解决方案有如下几种:

         A、单机实现服务,本地增加备份。我们用单机来实现这样一个服务,这样可以保证绝对的数据一致性。同时,每次请求数据后,每个服务本地保留一份备份数据。当这个服务挂掉了,就使用最近的一次备份。这个方案对于大多数情况是足以应付的,而且具备简单粗暴有效的特点。

         B、多机服务,数据同步。采用多机提供服务,信息更新时进行数据同步。这种方式的优点就是服务可以保证7*24小时服务,服务稳定性高。但是,问题就是维护成败会比上面一种方式高。

         3、功能。实现服务名称到IP、Port、Protocol等信息的一个对应。如果要设计的更通用,比如可以注册任何信息,就只需要实现Key-Value的通用数据格式。其中,Value部分需要支持更多更丰富的结构,比如List、Set等。

         有了这样的一个系统,我们就可以很方便的扩展我们的服务,并且能很好的规范我们服务的获取、访问接口。

【第十二阶段 :传输协议、接口、远程调用】

         这一部分主要谈谈关于协议、接口和远程调用相关的内容。本来这一部分应该在之前就有比较详细的讨论,不过我放到后面来,足见其重要性。特别是在系统越来越多的时候,这几个东东直接决定了我们的开发速度和运维成本。

         好,接下来我们一个个的看。

1、传输协议

         到目前为止,在不同系统之间获取数据的时候,你是采用那种方式呢?

         我们简单看一个例子:

以上这个可能是我们最(|两万字的分隔线|)初学习网络编程的时候,最常使用的一种C-S交互方式。其实这里面我们已经定义了一种交互协议,只是这种方式显得比较山寨,没有规范。扩充性等等都没有充分考虑。

         我们在学习网络编程的时候,老师就给我们讲过,网络分层的概念,经典的有5层和7层模型。在每一层里面,都有自己的协议。比如:IP协议、TCP协议、HTTP协议等等。这些协议基本上都由两部分组成:头+数据。

         【头信息】

         我们来看看TCP协议:

(注:以上是我从百度百科上截取的)

         头信息中,一般可能会包含几个重要的元素:协议标识符、版本号、串号(或是本次交互的id)、数据包长度、数据校验等信息,可能有些协议还会带一些其他数据,比如数据发出方名称、接收方名称、时间、保留字段等等信息。

         这些信息的目的,就是为了清晰的表达,我是怎么样的一个协议,我有哪些特征,我带的数据有多大。方便接收方能够清晰的辨认出来。

         【数据部分】

         数据部分是为了让应用层更好的通讯和表达数据。要达到的目标就是简洁高效、清晰明了。说起来很容易,但是实现起来要考虑的东西就比较多,比如:数据压缩、字符转移、二进制数据表达等等。

         我们通常有多种格式来作为数据部分的协议。大体上可以分为:

         A、二进制流。比如:C里面的结构体、JAVA里面的Serializable,以及像Google的protobuf等。将内存里面实体的数据,按字节序列化到缓冲区。这种方式的好处就是数据非常紧凑,几乎没有什么浪费。但是,问题也比较明显。双方必须很清楚协议,面向的语言基本上是要求一样的,很难做兼容,跨平台差。且扩展性比较差。另外,还有网络大小端字节序(Big-Endian、Little-Endian)的问题需要考虑。

         B、文本传输协议。就是以字符串的方式来组织信息。常见的有XML、JSON等等。这种方式的好处就是扩展性强,跨平台兼容能力好,接口标准且规范。问题就是传输量比较大,需要考虑做压缩或者优化。

         XML方式:

JSON方式:

因此,我们可以按照我们实际的需求,来定制我们想要的数据格式,从而达到高效和易于表达和扩展的效果。

         经过上述分析,我们基本上对传输协议有了一个比较大致的了解。有了传输协议之后,我们的跨机器间的数据交互,才显得比较规范和具有扩展性。如果我们还不想自己来定义协议,我们可以用现有的协议进行组合,比如:HTTP+XML、HTTPS+JSON等等。只要在一个平台上,大家都遵守这样的规范,后续开发起来就变得轻松容易。

         2、接口(或者API

         接口是我们的服务对外表达的窗口。接口的好坏直接决定了我们服务的可表达性。因此,接口是一个承上启下的作用。对外,很好的表达提供的服务名称、参数、功能、返回的数据等;对内,能够自动生成描述所对应的代码函数框架,让开发者编写实现。

         我们描述我们接口的方式有很多,可以利用描述性语言来表达。比如:XML、JAVA里提供的IDL(Interface Definition Language)等等。我们把这种描述接口的语言统一称为IDL(Interface Definition Language)。

我们可以自己开发一些工具,将IDL进行翻译,转换成方便阅读的HTML格式、DOC、CHM等等。方便其他开发者查阅。

         同时,另外一方面,我们可以将IDL转换成我们的接口代码,让服务接口的开发者和调用方的开发者按规范和标准来实现。

对于下层代码如何来实现数据的解析、函数的调用、参数的传递、数据的转换和压缩、数据的交换等等工作,则由工具来生成。对上完全屏蔽。详细的内容,我们将在接下来的远程调用中来分析。

         3、远程调用

         我们最初写代码的时候,就被教授了函数的概念。我们可以将一些公用的代码,或者实现一定含义或逻辑的代码,做成一个函数,方便重复的使用。

         最开始,这些函数往往在同一个文件里,我们只要先申明,即可使用。

         后来,我们开始使用库里面的函数,或是将函数封装成一个个的库(比如C里面的静态库.a或者动态库.so,或者是Java里面的.jar)。

         以上对于函数的调用,以及函数自身的处理都是在本地。假如,当我们单机不能满足需求的时候,我们就需要将函数的处理放到其他机器上,让机器做到并行的计算。这个时候,我们就需要远程的函数调用,或者叫做远程过程调用(Remote Procedure Call 或者 Remote Method Invoke,简称RPC或者RMI)。

         我们在这之前讲过几个东东:负载均衡、命名位置服务、协议、接口。其实前面讲这几个东东都是为了给远程过程调用做铺垫。RPC都是建立在以上部分的基础上。

         还是按照我们之前分析问题的思路:为什么要这个东东?这个东西解决什么问题?如何实现?有哪些问题?等等来分析RPC吧。

         RPC的目的,就是使得从不同服务上获取数据如同本地调用一样方便和自然。让程序调用者不需要了解网络细节,不用了解协议细节,不用了解服务的机器状态细节等等。如果没有RPC,其实也是可以的,就是我们写程序的时候难受点而已,哈哈。

         接下来,我们看看如何来实现。

以上是整个的一个大体静态逻辑。最先编写调用的IDL,完成后由工具生成接口说明文档(doc);同时,生成客户端调用代码(stub,我们叫做存根);另外,需要生成server端接口框架(skeleton),接口开发者实现具体的代码逻辑。

以上就是客户端调用的整个逻辑。

【第十三阶段 :分布式计算和存储的运维设计与考虑】

         以上的部分已经从前到后的将系统架构进行了描述,同时针对我们会遇到的问题进行了分析和处理,提出了一些解决方案,以保证我们的系统在不断增长的压力之下,如何的良好运转。

         不过,我们很少描述运维相关的工作,以及设计如何和运维相关联。系统运维的成败,直接决定了系统设计的成败。所以系统的运维问题,是设计中必须考虑的问题。特别是当我们有成千上万的(tens of thousands)台机器的时候,运维越发显得重要。因此,在系统设计初期,就应当把运维问题纳入其中来进行综合的考虑。

         如果我们用人来管机器,在几台、几十台机器的时候是比较可行的。出了问题,人直接上,搞定!不过,当我们有几百台、几千台、几万台、几十万台机器的时候,我们如果要让人去搞定,那就未见得可行了。

         首先,人是不一定靠谱的。即使再聪明可靠的人,也有犯错误的时候。按照一定概率计算,如果我们机器数量变多,那么出错的绝对数量也是很大的。同时,人和人之间的协作也可能会出现问题。另外,每个人的素质也是不一样的。

         其次,随着机器数量的膨胀,需要投入更多的人力来管理机器。人的精力是有限的,机器增多以后,需要增加人力来管理机器。这样的膨胀是难以承受的。

         再次,人工恢复速度慢。如果出现了故障,人工来恢复的速度是比较慢的,一般至少是分钟级别。这对于要提供7×24小时的服务来说,系统稳定运行的指标是存在问题的。同时,随着机器的增多,机器出问题的概率一定的条件下,绝对数量会变多,这也导致我们的服务会经常处于出错的情况之下。

         还有,如果涉及到多地机房,如何来管理还是一个比较麻烦的事情。

         如果我们能转换思维,在设计系统的时候,如果能有一套自动化管理的模式,借助电脑的计算能力和运算速度,让机器来管理机器,那我们的工作就轻松了。

         比如,我们可以设计一套系统,集成了健康检查、负载均衡、任务调度、自动数据切片、自动数据恢复等等功能,让这套系统来管理我们的程序,一旦出现问题,系统可以自动的发现有问题的机器,并自动修复或处理。

         当然以上都是比较理想的情况。凡事没有绝对之说,只是需要尽可能达到一个平衡。

         好了,说了这么多的问题,要表述的一点就是:人来管理机器是不靠谱的,我们需要尽量用机器来管理机器!

         接下来,我们比较简单的描述一下一个比较理想的自动化管理模型。

         我们将我们的系统层次进行初步的划分。

我们将系统粗略的划分为三个层次:访问接入层、逻辑处理层和数据存储层。上一次对下一层进行调用,获取数据,并返回。

         因此,如果我们能够做到,说下一层提供给上一层足够可靠的服务,我们就可以简化我们的设计模型了。

         好,那如果要提供足够可靠的服务,方便调用的话,应该如何来做呢?

         我们可以针对每一层来看。

         首先,看看访问接入层:

所有的Web Server和 Private Protocol Server将服务注册到命名和定位服务上。一旦注册后,NLS会定期去检查服务的存活,如果服务宕掉,会自动摘除,并发出报警信息,供运维人员查阅。等服务恢复后,再自动注册。

         Virtual Server从NLS获取服务信息,并利用负载均衡策略去访问对应的后继服务。而对外,只看到有一台Virtual Server。

         接下来,我们看看逻辑层:

由于HTTP的无状态性,我们将逻辑代码按标准接口写成一个个的逻辑处理单元,放入到我们的逻辑处理容器之中,进行统一的运行。并通过容器,到NLS中注册。Virtual Server通过NLS获取到对应的信息,并通过负载均衡策略将数据转发到下游。

         这里比较关键的数据处理单元,实际就是我们要写的业务逻辑。业务逻辑的编写,我们需要严格按照容器提供的规范(如IDL的标准等),并从容器获取资源(如存储服务、日志服务等)。这样上线也变的简单,只需要将我们的处理单元发布到对应的容器目录下,容器就可以自动的加载服务,并在NLS上注册。如果某一个服务出现异常,就从NLS上将其摘掉。

         容器做的工作就相对比较多。需要提供基本的服务注册功能、服务分发功能等。同时,还需要提供各种资源,如:存储服务资源(通过NLS、API等,提供存储层的访问接口)、日志服务资源等,给处理单元,让其能够方便的计算和处理。

         总体来说,因为HTTP的无状态特性,以及不存在数据的存储,逻辑层要做到同构化是相对比较容易的,并且同构化以后的运维也就非常容易了。

         最后,看看数据存储层。

存储层是运维设计中最难的一部分。因为根据不同的业务需要,可能提供不同的存储引擎,而不同的存储引擎实现的机理和方式可能完全不一样。比如,为了保证数据的有效性和一致性,有些存储系统需要使用事务;而有些业务,可能为了追求高效,可能会牺牲一些数据的一致性,而提供快速的KV查询等等。

         因此,数据存储层的异构性就是运维设计中亟待解决的问题。

         一种比较理想的方式,就是让各个存储系统,隐藏内部的实现,对外提供简单的访问接口。而在系统内部,通过meta server、data assemble server、status manager、message queue等管理单元来管理数据存储单元。当然,这只是其中一种方式。也可以利用mysql类似的主从级联方式来管理,这种方式也是可行的。

         数据存储系统的设计,也没有一个固定的规范(比如:Big Table、Cassandra、Oracle数据库集群等),所以运维的设计需要在系统中来充分考虑。上述图示只是提供了一种简单的设计方案。

         好了,有了多个层次的详细分析以后,上一层次调用下一层次,就直接通过固定的地址进行访问即可。

         不过有一个问题就是,如果我们的Virtual Server是一个单点,出现故障后,该层就不能提供服务。这个是我们不可接受的。怎么办呢?

         要解决这样的问题,就是利用冗余。我们可以将我们的服务划分为多个组,分别由多个VS来管理。上层调用下层的时候,通过一定的选择策略来选择即可。这样,如果服务出现问题,我们就能通过冗余策略,将请求冗余到其他组上。

         我们来看一个实例:

       用户通过域名访问我们的服务,DNS通过访问IP解析,返回对应访问层的IP地址。访问层将请求转发到对应的逻辑处理组,逻辑处理组从不同的存储系统里获取数据,并返回处理结果。

         通过以上的分析,我们通过原有的一些技术手段,可以做到比较好的自动化运维的方式。不过这种运维方式也不是完全智能的。有些时候也需要人工的参与。最难的一点就是存储系统的设计和实现。如果要完全的自动化的话,是一件比较难的事情。

         说明:以上的描述是一个比较理想化的模型,要真正实现这种模型,需要很多的辅助手段,并且需要搭建很多基础设施。可能会遇到我们没有提到的很多的实际问题,比如:跨机房网络传输延迟、服务间隔离性、网卡带宽限制、服务的存活监控等等。因此,在具体实施的时候,需要仔细分析和考虑。

by wangbo

 

李萧明吐槽 哥02年开始玩网站的,06年开始转网络方向,现在呢妹的ASP JSP都忘光了,当时好像PHP不流行,唉。好文章,大家好好看看。悲惨的回意。

诡异提交失败问题追查

No Comments 专业IT吐槽

摘要:

自四月份以来,贴吧遇到了发帖失败的问题,现象比较诡异。经过追查发现是操作系统刷磁盘时,阻塞write系统调用导致。本文主要分享问题追查过程,希望对大家日常工作中定位问题有一定帮助。

TAG:

提交、问题追查、脏页

1 背景

很久前知道上有个问题:“从前天开始,跟帖就是发帖失败,换个ID开始能发,后来又变成发帖失败,很迷惑。谁知道怎么回事么。是系统问题么,还是网络问题?”最佳答案是:“很大部分是网络出现问题,你可以重新提交下就可以了”。

前段时间,贴吧的提交UI老是报警,晚上的时候手机叮叮咣咣地响,每次看都是apache进程数上千hold不住了,只好逐台重启。后来OP怒了,直接写了个脚本,发现apache进程数上来就自动重启。

好景不长,某天图1被PM截下来发到群上,自己发几个贴测试下居然复现了!看来真不是网络的问题,必须好好追查下了。

2 提交系统综述

先整理下贴吧提交的逻辑和涉及的模块。图2是贴吧提交系统的架构,一个完整的发帖流程需要经过下述模块的处理。

l  提交UI。提交UI是接收用户提交的帖子信息,进行合法性验证后将数据提交给后端的PHP模块,使用apache作为服务器。

l  提交后端。某些提交操作比如发贴和删帖对时序存在要求,所有与发帖有关的操作都经提交后端序列化后,持久化到本地di数据文件。消息队列读取di文件,转发给订阅相关消息的后端模块。

l  提交代理层,简称proxy。贴吧除了发帖外,还有消息推送、消费吧豆等提交操作,因此提交后端以集群的形式存在,并且每个都是单点。proxy对UI层面屏蔽了各种提交后端的划分,自动根据UI中的命令号转发到相应的提交后端上。

提交UI通过RPC与proxy通信,短连接。proxy使用rpc_client与提交后端通信,短连接。proxy和提交后端都是使用rpc框架编写的C模块。

3 问题追查

发表一个帖子经过这么多模块,是在没有头绪,只好辛苦UI的同学看看什么情况下会出现那个未知错误的哭脸。UI同学很给力,马上给出一个case,3000代表提交UI与cm_proxy交互失败,从交互时间看,与cm_proxy交互时间为1秒。恰好UI设置的超时为1秒,去后端看看发生什么回事。

拿着这个logid查cm_proxy和postcm日志,发现两个模块都接收到UI的请求,并且把数据转发到相应的后端模块。继续看warning日志,发现cm_proxy等待postcm回复超时!

3.1  RPCClient问题?

难道是提交后端处理这么慢么?查看本条请求处理时间,只有十几毫秒,理论上不会超时。带着这个问题请教以前负责这个模块的高同学,得知以前曾经出现类似的问题,猜测是RPCClient在压力上千时,会出现大量读超时。

为了让真凶现形,在OP mm的帮助下搭建好一套线下测试环境,使用压力工具给予proxy 2000/s的压力(线上峰值是1000/s)。一个小时,两个小时……出现时的只是proxy queue full错误(等待连接池满),没有读超时问题。然后wiki一下,也没有找到类似错误的记录,看来RPC库是可依赖的。

3.2  TIME_WAIT问题?

一时找不到头绪,看能不能从日志中挖掘到一些线索。统计cm_proxy日志情况,根据错误号查看代码,主要出现两种类型错误。

读超时:

连接postcm拒绝:

处理时间为0居然还读超时,太诡异了!统计下5月22日一天proxy与提交后端交互失败的分布。

图4 cm_proxy与提交后端交互失败分布

为啥tc cm00这个机器的连接拒绝这么多,读超时这么少呢?原来提交后端单点部署在这台机器上,提交后端和proxy同机部署可能带来一些问题。查看机器监控,发现这台机器处于TIME_WAIT状态的socket达到十几万。但是查看操作系统参数/proc/sys/net/ipv4/tcp_tw_reuse,值为1。证明目前端口复用已经打开。为了让问题收敛,把tc cm00的proxy下掉,继续跟进。

3.3  Backlog大小问题?

为什么会连接拒绝?带着这个问题请教我们的小强同学。不愧是大牛,一下就发现tcp listen的时候,Backlog可能设置的太小了。翻阅资料充电:Backlog是listen系统调用的         第二个参数,这个参数所指明的是linux处理tcp连接是所设置的全连接队列的长度。在socket程序设计中,当三次握手完成后,会把刚刚建立好的连接放入这个全连接队列中,当服务器端调用accept系统调用的时候,会从这个全连接队列里取出已经建立好的连接供上层应用使用。这个值在系统中设置了上限,可以通过/proc/sys/net/core/somaxconn查看。当listen系统调用使用的Backlog值小于这个值得时候系统取backlog值为实际值,当Backlog的值大于这个值的时候,系统取SOMAXCONN的值为默认值。

查看提交后端上系统SOMAXCONN的值为2048,而listen时Backlog大小只有100,貌似有点小。5月28日,OP操作把这个值调到1024,观察效果。

图5  22日和28日cm_proxy与提交后端交交互失败分布

调整后,交互失败下降到原来的三分一,有点进度。但是如果仅仅是Backolg大小问题,为什么依然存在这么多的交互失败呢,看来幕后凶手还没有找到。

3.4  现场缉凶!

目前掌握的证据还不充分,实时观察日志或许能发现些东西。tail一台proxy的错误日志,发现每隔一段时间刷出一批错误日志。统计每秒错误日志数,发现一个规律,很多时候每隔15秒左右会一下子刷子40条交互失败的日志。这个40有点眼熟,就是proxy的线程数!这意味这个时间点所有的交互都失败了。

火速赶往提交后端机器,iostat一下,发现一个很有意思的现象。IO的情况随着时间上下波动,然后每隔一段时间会有一次大的IO操作(>80M/s,持续1~3秒),此时proxy会有较大几率出现交互失败。

为了确认案情,统计6.2一天提交后端日志,共有477个请求处理时间大于等于1000ms。这477个请求处理时间几乎平均分布在[1000,3995]ms中。目前proxy与提交后端连接超时为1000ms,意味着477个请求持续时间内,proxy与提交后端有可能出现读超时(根据IO被阻塞时间和请求达到提交后端时间确定)

真正的原因是在流量高峰期,postcm提交量上升, 当脏页占系统内存的比例超/proc/sys/vm/dirty_ratio的时候, write系统调用会被被阻塞,主动回写dirty page,直到脏页比例低于/proc/sys/vm/dirty_ratio。由于提交后端单线程的工作模型,会导致提交后端短时间内不能响应请求,造成上级模块陆续超时或连接失败。

4 解决办法

整理一下思路,目前造成提交不稳定主要以下三个。

4.1  cm_proxy读超时

l  修改操作系统参数,观察效果并不断调整。

由基础架构的同学总结,操作系统会在下面三种情况下回写脏页:

1) 定时方式。定时回写是基于这样的原则:/proc/sys/vm/dirty_writeback_centisecs的值表示多长时间会启动回写pdflush线程,由这个定时器启动的回写线程只回写在内存中为dirty时间超过(/proc/sys/vm/didirty_expire_centisecs / 100)秒的页(这个值默认是3000,也就是30秒),一般情况下dirty_writeback_centisecs的值是500,也就是5秒,所以默认情况下系统会5秒钟启动一次回写线程,把dirty时间超过30秒的页回写,要注意的是,这种方式启动的回写线程只回写超时的dirty页,不会回写没超时的dirty页。

2) 内存不足的时候。这时并不将所有的dirty页写到磁盘,而是每次写大概1024个页面,直到空闲页面满足需求为止。

3) 写操作时发现脏页超过一定比例: 当脏页占系统内存的比例超过/proc/sys/vm/dirty_background_ratio 的时候,write系统调用会唤醒pdflush回写dirty page,直到脏页比例低于/proc/sys/vm/dirty_background_ratio,但write系统调用不会被阻塞,立即返回.当脏页占系统内存的比例超/proc/sys/vm/dirty_ratio的时候, write系统调用会被被阻塞,主动回写dirty page,直到脏页比例低于/proc/sys/vm/dirty_ratio。

修改刷脏页频率,从5s调整到3s

echo 300 > /proc/sys/vm/dirty_writeback_centisecs

修改脏页存在的时间限制,从30s调整到10s

echo 1000 > /proc/sys/vm/dirty_expire_centisecs

效果描述:

从iostat信息来看,刷脏页的频率变高,每次刷的脏页数量变少。从上层应用程序来看,性能变得平稳。修改后每天超过1000ms的请求在200个左右。存在继续优化的空间。

l  增大提交UI与proxy和proxy与提交后端的读超时,从1000ms修改为3000ms。那么每天在提交后端两百多个超过1秒刷脏页的时间范围内,用户的提交会延迟而不会失败。

4.2  cm_proxy连接拒绝

当IO被阻塞期间,到达提交后端请求数超过连接队列长度,则拒绝连接。通过实验观察提交后端Backlog的最佳大小,目前操作系统参数上限为2048。根据实验结果,将提交后端Backlog大小调整为2048。

4.3  cm_proxy queue full

当proxy与提交后端交互失败期间,若前端请求过多,若proxy工作线程数不足或proxy连接提交后端连接池连接数不足时出现。根据实验结果和目前proxy压力状态(最大1000/s),将proxy线程数和提交后端连接池连接数修改为100。

总结

通过这次追查得出的经验是一个问题的出现可能有很多现象,有些可能是表面原因,修改对问题会有不少的改善,但只有真正找到引发问题的原因后,才能找到最恰当的解决办法。本文小结了网络交互失败追查的一些方向和方法,希望对大家有所帮助。

by  chenyuzhe

 

李萧明吐槽,别尼妹的没事就说网络问题,网络也是个妹纸好不,不要动不动就怪别人。妹纸哥会护着你的。

解析nginx负载均衡

No Comments 专业IT吐槽

摘要:对于一个大型网站来说,负载均衡是永恒的话题。随着硬件技术的迅猛发展,越来越多的负载均衡硬件设备涌现出来,如F5 BIG-IP、Citrix NetScaler、Radware等等,虽然可以解决问题,但其高昂的价格却往往令人望而却步,因此负载均衡软件仍然是大部分公司的不二之选。nginx作为webserver的后起之秀,其优秀的反向代理功能和灵活的负载均衡策略受到了业界广泛的关注。本文将以工业生产为背景,从设计实现和具体应用等方面详细介绍nginx负载均衡策略。

关键字:nginx 负载均衡 反向代理

1.前言

随着互联网信息的爆炸性增长,负载均衡(load balance)已经不再是一个很陌生的话题,顾名思义,负载均衡即是将负载分摊到不同的服务单元,既保证服务的可用性,又保证响应足够快,给用户很好的体验。快速增长的访问量和数据流量催生了各式各样的负载均衡产品,很多专业的负载均衡硬件提供了很好的功能,但却价格不菲,这使得负载均衡软件大受欢迎,nginx就是其中的一个。

nginx第一个公开版本发布于2004年,2011年发布了1.0版本。它的特点是稳定性高、功能强大、资源消耗低,从其目前的市场占有而言,nginx大有与apache抢市场的势头。其中不得不提到的一个特性就是其负载均衡功能,这也成了很多公司选择它的主要原因。本文将从源码的角度介绍nginx的内置负载均衡策略和扩展负载均衡策略,以实际的工业生产为案例,对比各负载均衡策略,为nginx使用者提供参考。

2.   源码剖析

nginx的负载均衡策略可以划分为两大类:内置策略和扩展策略。内置策略包含加权轮询和ip hash,在默认情况下这两种策略会编译进nginx内核,只需在nginx配置中指明参数即可。扩展策略有很多,如fair、通用hash、consistent hash等,默认不编译进nginx内核。由于在nginx版本升级中负载均衡的代码没有本质性的变化,因此下面将以nginx1.0.15稳定版为例,从源码角度分析各个策略。

2.1.           加权轮询(weighted round robin)

轮询的原理很简单,首先我们介绍一下轮询的基本流程。如下是处理一次请求的流程图:

图中有两点需要注意,第一,如果可以把加权轮询算法分为先深搜索和先广搜索,那么nginx采用的是先深搜索算法,即将首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器;第二,当所有后端机器都down掉时,nginx会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在timeout的状态,从而导致整个前端被夯住。

接下来看下源码。nginx源码的目录结构很清晰,加权轮询所在路径为nginx-1.0.15/src/http/ngx_http_upstream_round_robin.[c|h],在源码的基础上,针对重要的、不易理解的地方我加了注释。首先看下ngx_http_upstream_round_robin.h中的重要声明:

从变量命名中,我们就可以大致猜出其作用。其中,current_weight和weight的区别主要是前者为权重排序的值,随着处理请求会动态的变化,后者是配置值,用于恢复初始状态。

接下来看下轮询的创建过程,代码如下图所示。

这里有个tried变量需要做些说明。tried中记录了服务器当前是否被尝试连接过。他是一个位图。如果服务器数量小于32,则只需在一个int中即可记录下所有服务器状态。如果服务器数量大于32,则需在内存池中申请内存来存储。对该位图数组的使用可参考如下代码:

最后是实际的策略代码,逻辑很简单,代码实现也只有30行,直接上代码。

2.2.           ip hash

ip hash是nginx内置的另一个负载均衡的策略,流程和轮询很类似,只是其中的算法和具体的策略有些变化,如下图所示:

ip hash算法的核心实现如下图:

从代码中可以看出,hash值既与ip有关又与后端机器的数量有关。经过测试,上述算法可以连续产生1045个互异的value,这是该算法的硬限制。对此nginx使用了保护机制,当经过20次hash仍然找不到可用的机器时,算法退化成轮询。因此,从本质上说,ip hash算法是一种变相的轮询算法,如果两个ip的初始hash值恰好相同,那么来自这两个ip的请求将永远落在同一台服务器上,这为均衡性埋下了很深的隐患。

2.3.           fair

fair策略是扩展策略,默认不被编译进nginx内核。其原理是根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行分流。这种策略具有很强的自适应性,但是实际的网络环境往往不是那么简单,因此要慎用。

2.4.           通用hash、一致性hash

这两种也是扩展策略,在具体的实现上有些差别,通用hash比较简单,可以以nginx内置的变量为key进行hash,一致性hash采用了nginx内置的一致性hash环,可以支持memcache。

3.   对比测试

本测试主要为了对比各个策略的均衡性、一致性、容灾性等,从而分析出其中的差异性,并据此给出各自的适用场景。为了能够全面、客观的测试nginx的负载均衡策略,我们采用了两个测试工具、在不同场景下做测试,以此来降低环境对测试结果造成的影响。首先简单介绍测试工具、测试网络拓扑和基本的测试流程。

3.1.           测试工具

3.1.1  easyABC

easyABC是公司内部开发的性能测试工具,采用epool模型实现,简单易上手,可以模拟GET/POST请求,极限情况下可以提供上万的压力,在公司内部得到了广泛的使用。由于被测试对象为反向代理服务器,因此需要在其后端搭建桩服务器,这里用nginx作为桩webserver,提供最基本的静态文件服务。

3.1.2  polygraph

polygraph是一款免费的性能测试工具,以对缓存服务、代理、交换机等方面的测试见长。它有规范的配置语言PGL(Polygraph Language),为软件提供了强大的灵活性。其工作原理如下图所示:

polygraph提供client端和server端,将测试目标nginx放在二者之间,三者之间的网络交互均走http协议,只需配置ip+port即可。client端可以配置虚拟robot的个数以及每个robot发请求的速率,并向代理服务器发起随机的静态文件请求,server端将按照请求的url生成随机大小的静态文件做响应。这也是选用这个测试软件的一个主要原因:可以产生随机的url作为nginx各种hash策略的key。

另外,polygraph还提供了日志分析工具,功能比较丰富,感兴趣的同学可以参考附录中的相关材料。

3.2.           测试环境

本测试运行在5台物理机上,其中被测对象单独搭在一台8核机器上,另外四台4核机器分别搭建了easyABC、webserver桩和polygraph,如下图所示:

3.3.           测试方案

首先介绍下关键的测试指标:

均衡性:是否能够将请求均匀的发送给后端

一致性:同一个key的请求,是否能落到同一台机器

容灾性:当部分后端机器挂掉时,是否能够正常工作

以上述指标为指导,我们针对如下四个测试场景分别用easyABC和polygraph进行测试:

场景1      server_*均正常提供服务;

场景2      server_4挂掉,其他正常;

场景3      server_3、server_4挂掉,其他正常;

场景4      server_*均恢复正常服务。

上述四个场景将按照时间顺序进行,每个场景将建立在上一个场景基础上,被测试对象无需做任何操作,以最大程度模拟实际情况。另外,考虑到测试工具自身的特点,在easyabc上的测试压力在17000左右,polygraph上的测试压力在4000左右。以上测试均保证被测试对象可以正常工作,且无任何notice级别以上(alert/error/warn)的日志出现,在每个场景中记录下server_*的qps用于最后的策略分析。

3.4.           测试结果

表1和图1是轮询策略在两种测试工具下的负载情况。对比在两种测试工具下的测试结果会发现,结果完全一致,因此可以排除测试工具的影响。从图表中可以看出,轮询策略对于均衡性和容灾性都可以做到很好的满足。(点击图片查看大图)

表2和图2是fair策略在两种测试工具下的负载情况。fair策略受环境影响非常大,在排除了测试工具的干扰之后,结果仍然有非常大的抖动。从直观上讲,这完全不满足均衡性。但是从另一个角度出发,恰恰是由于这种自适应性确保了在复杂的网络环境中能够物尽所用。因此,在应用到工业生产中之前,需要在具体的环境中做好测试工作。(点击图片查看大图)

以下图表是各种hash策略,所不同的仅仅是hash key或者是具体的算法实现,因此一起做对比。实际测试中发现,通用hash和一致性hash均存在一个问题:当某台后端的机器挂掉时,原有落到这台机器上的流量会丢失,但是在ip hash中就不存在这样的问题。正如上文中对ip hash源码的分析,当ip hash失效时,会退化为轮询策略,因此不会有丢失流量的情况。从这个层面上说,ip hash也可以看成是轮询的升级版。(点击图片查看大图)

图5为ip hash策略,ip hash是nginx内置策略,可以看做是前两种策略的特例:以来源ip为key。由于测试工具不便于模拟海量ip下的请求,因此这里截取线上实际的情况加以分析,如下图所示:

图5 ip hash策略

图中前1/3使用轮询策略,中间段使用ip hash策略,后1/3仍然是轮询策略。可以明显的看出,ip hash的均衡性存在着很大的问题。原因并不难分析,在实际的网络环境中,有大量的高校出口路由器ip、企业出口路由器ip等网络节点,这些节点带来的流量往往是普通用户的成百上千倍,而ip hash策略恰恰是按照ip来划分流量,因此造成上述后果也就自然而然了。

4.   总结与展望

通过实际的对比测试,我们对nginx各个负载均衡策略进行了验证。下面从均衡性、一致性、容灾性以及适用场景等角度对比各种策略。(点击图片查看大图)

 

以上从源码和实际的测试数据角度分析说明了nginx负载均衡的策略,并给出了各种策略适合的应用场景。通过本文的分析不难发现,无论哪种策略都不是万金油,在具体的场景下应该选择哪种策略一定程度上依赖于使用者对这些策略的熟悉程度。希望本文的分析和测试数据能够对读者有所帮助,更希望有越来越多、越来越好的负载均衡策略产出。

5.   参考资料

http://wiki.nginx.org/HttpUpstreamConsistentHash

http://wiki.nginx.org/HttpUpstreamFairModule

http://wiki.nginx.org/HttpUpstreamRequestHashModule

http://www.web-polygraph.org/

http://nginx.org/

漫谈社区PHP 业务开发

No Comments 专业IT吐槽

在当前这个互联网业务飞速发展时期,新的产品如雨后春笋般涌出,老产品线新业务也在不断突破和尝试。这就对快速开发迭代提出了更高的要求。

一、基础运行环境

针对新产品的开发,必须能够快速搭建一套LAMP架构。那么无外乎选择一个webserver,选择一个php版本,选择一个mysql版本,再选择一个PHP开发框架和选择一些php通用扩展和基础库等。这个过程读者可能觉得已经很快了,能不能更快?

选择的过程要求研发同学对相关技术方向有一定的积累,权衡利弊和优先点,又是一番调研和学习。如果有一键安装程序,提供自动化安装webserver,php,mysql,以及携带高性能灵活的php开发框架,并提供标准化、安全、常用的配置文件,可以大大缩短产品线LAMP系统调研的成本,缩短工作周期。

一键安装四步骤:(1)下载;(2)少量配置;(3)make install;(4)start;(当然有end啦,简单的运维工具),运行环境OK。

二、业务开发框架

社区产品线各自为政,封闭得开发各自的业务逻辑。而事实上,各个产品线之间存在很多通用业务逻辑处理,如session验证、权限判断、参数验证、日志打印等。不同产品线,所有请求都需要做这些处理,能不能不重复开发?无线业务开发和PC上的业务逻辑有很多的不同,但不同产品线之间也有很多通用性。能不能不重复开发?

产品线在内部通常对这些通用逻辑的处理做了一定的抽象,设计为ActionChain的形式或者通过基类的方案。框架将更彻底:将这些所有请求都要处理的通用逻辑以业务逻辑框架的形式提供,研发同学只需要关注用户请求专有的逻辑处理。

一个用户请求的处理逻辑如下图:蓝色部分是控制器框架处理流程,绿色部分和控制器框架相结合,处理所有请求通用的业务逻辑。而真正需要研发同学关注和开发的该用户请求专有的业务处理,即黄色部分(当然一个不仅仅是一个Action脚本,一个请求的处理会横向做mvc分层,这块后续会有涉及。)

业务逻辑框架继承在一键安装程序中提供,简简单单就可以获得。

原生的PHP业务和模板耦合很深,没有做任何的分层设计,其结果是代码的复用性差。这样的原始的PHP系统现在已几乎消亡。PHP开发框架统一处理路由、渲染、AutoLoad,通用业务逻辑的抽象和基础库的抽象,专有业务MVC分层,已大大加快了产品线业务逻辑的开发。如下图所示:

从上而下,分别是接入层(高性能webserver),PHP开发框架(路由、自动加载、视图引擎等),应用和基础库,存储引擎。

三、通用服务

社区产品线存在很多共同的需求,如日志处理、配置文件的处理、字符串处理、数据库交互、网络交互等。这些算法和工具封装成phplib给产品线使用已比较成熟。

社区类产品线的业务功能存在很多的通用性,诸如评论功能、Tag功能、好友功能、图册、任务系统等,在众多社区产品线都有类似的新功能新需求,各自设计开发?

这些需求在各产品线的UI上有个性化需求,但是后端实现方案大同小异,具有一定的通用性。功能服务化,提供API接口给不同产品线使用,产品线只需要关注展现逻辑和私有数据的处理逻辑即可,且服务统一运维,降低产品下的系统复杂度。

四、垂直拆分子系统

那么随着我们业务的拓展,单个应用内部的ui和module的数量越来越多,Action和Logic(对应MVC中的M层,内部可以再进一步做分层处理,此次不详述)的交互,logic和logic之间的交互变得越来越复杂。开发同学需要了解整个应用的逻辑,某个logic的升级,需要排查整个应用下是否存在其他ui或logic的反向依赖。在快速开发的要求下,开发同学对logic之间的相互耦合关系的梳理不清楚,势必引发越来越多的问题,影响项目质量,难以开始开发。

单一系统的问题暴露越来越多,就到了系统拆分的时候了。如何拆?按业务逻辑垂直拆分。将功能独立的业务逻辑剥离出来,做成独立的子系统。这个时候还需要考虑业务的通用性,是否可以服务化?应用已有相同需求的通用服务?此时通用业务逻辑封装成通用服务或使用了通用服务,旁路的业务逻辑独立成子系统,如此一来就将原先单一庞大的系统做了大量减负。完成此阶段的重构后,系统加入变成如下:

单一系统被拆分成多个APP(APP内部仍然有横向的MVC分层),并复用大量的通用服务。如此一来研发团队在人员分工并行开发上都得到了极大提高。

五、跨系统调用框架

然而真实的现状,在拆分后的子系统之间并不能完全消除依赖。为了解决多个子系统之间数据依赖的关系,需要一套统一的解决方案:API框架。子系统成为独立的应用(APP),APP之间存在相互的数据依赖,这些依赖以API的形式对外提供。如下图:

当APP1依赖APP2或APP3的数据后,APP2和APP3会将一部分数据接口以API的形式提供,数据做统一的打包,通过标准规范的URL提供产品线内部其他APP调用。这种形式非常类似于一个产品对外开放API(对第三方开放API,我们称为openAPI,遵守统一的协议,并经过必要的权限验证),而解决内部子系统之间数据依赖的API接口可以进一步简化。

APP提供的API解决提供接口描述(输入、输出),处理API的URL,Logic的转发实现。API_LIB统一来管理所有的API接口,并提供统一的API_Server::call接口供调用。完全对上屏蔽内部的转发和实现细节。通常产品线内部为了达到运维的简化和统一,所有的子系统是同机部署的,API接口的会带来额外的网络消耗,以及增大qps。在此部署前提下,API_Server的实现方式可以通过HTTP调用或优化为直接PHPRequire方式实现。优势:

(1)框架统一,接口收敛,业务解耦;

(2)性能提升,易用性高,扩展性高;

六、UI拆分模型

此时独立出来的子系统可以专注做其业务逻辑了,核心的系统也得到减负。但是核心系统的升级更新频率是最高的,业务逻辑也最复杂。到了一定时期,核心系统又变得臃肿,难以维护。此时可以通过一些设计模式来降低程序的可扩展性和可维护性。但即便是如此,还是有一定的学习成本,在一个App内部,开发同学或多或少需要关注其他模块的代码,逐渐发展为升级一点就需要排查很多点。这时候又到了进一步减负的时候。如果减负?分为两部:

第一步:异步模型

页面渲染分为两个阶段:主题页面数据和其他非主题页面数据。根据页面的不同部分由不同的数据源提供数据。按此逻辑将app进一步做垂直拆分。

PHPService是由PHPmodule+一层很薄的UI,返回格式化数据。

第二步:同步模型

Module做拆分,不同业务逻辑拆分为不同的Module,区分为多个数据源,分别提供不同数据内容,由统一的UI调度不同的数据源后,统一进行渲染页面返回响应。

如此持续减负后,产品线内部的子系统和模块将越来越多,需要维持部署和运维的统一。对团队成员的分工很细,业务理解很专注和深入,合作、并行的效率也会更高,从而使整个开发周期缩短。

七、 小结

随着业务逻辑的不端壮大,每个子系统或模块的业务功能如果过于臃肿就需要不断做减分,以保持在可控的规模内。如此随着产品的发展,产品线内部的子系统和模块将越来越多,需要维持部署和运维的统一,保持简单。对团队成员的分工更细,业务理解保持专注和深入,合作、并行的效率也会更高,从而使整个开发周期缩短。

by luhaixia

多IDC环境下的分布式id分配方案

No Comments 专业IT吐槽

id分配是社区类产品的提交环节中必不可少的一步。任何UGC类内容产生时往往需要分配一个对应的id。

id分配的几种方式

方式一:单点自增分配。全局由一个模块来负责生成id,可保证id从0开始连续递增,数据一般放在本地文件。简洁,但致命的问题是单点故障会导致服务整体不可用。

方式一改进:为该模块提供主从复制的能力,或者干脆将数据放在mysql里,利用mysql的主从复制,都一定程度上增强了可用性,减轻了单点故障的影响。

方式二:随机/散列分配。通过一些hash算法,比如以时间+随机串为key的md5生成一个唯一的id,关键点在于算法和key的选择要避免冲突。

最典型的就是UUID,UUID的标准型式包含32个16进位数字,以连字号分为五段,形式为8-4-4-4-12的32个字符,如550e8400-e29b-41d4-a716-446655440000。libuuid提供了以时间或者随机数为基的UUID。UUID的最大缺点是位数太长,128位,在绝大多数应用和语言里对128位整数的支持都不好。

方式二改进:有条件的进行压缩。twitter的snowflake使用 time – 41 bits + configured machine id – 10 bits + sequence number – 12 bits的形式分配id,共63位,最高部分使用毫秒级的时间戳,保证了一定程度的有序性,机器标示使用10位,最多可容纳1024个分配器,最后的12位序列号可以支持在1ms内产生4096个不重复的id。从工程角度,这些都足够用了。但对系统时间的依赖性非常强,需要关闭ntp的时间同步功能,或者当检测到ntp时间调整后,拒绝分配id。

我们的需求和多IDC的挑战

我们的实际情况是:

· 一些老模块依赖于从0开始自增的id,数据在内存或者文件中以id为偏移来存储的。

· 一些系统依赖于id的增长做数据分片,例如按取除后分表,因此要求id在整体上是比较均衡的增长。

· 在多IDC环境,高延迟加不稳定的网络环境,要求各个分配器彼此之间无需协作,或者可以容忍短期内不可协作。

· 对于一些古董级的老系统来说,还在使用32位的id,63位id还是太大了。

因此,我们需要一种分布式高可用、从0开始自增、基本均衡、能够兼容老系统的id分配方案。

取模或分段的分布式分配

基于方案一再改进一步,将整个id空间按取模或分段等分为若干个独立的id子空间,每个id子空间由一个独立的分配器负责。

优点:简单,各个id分配器无需协作,即使发生网络划分时,也可保证可用性和id的不冲突。

如果在国际化环境的多IDC里进行部署,需要预先将id空间划分为N份,每个国家里部署若干份。每个IDC内应用只连本IDC的id分配服务。

在均衡性上的不足:在同一个IDC内,均衡性可以在接入层均衡算法保证,但是在多个IDC里,ID分配器个数的比例和id增长的服务往往是不吻合的,因此在多个IDC内,id是无法保证均衡增长的。

均衡性上的改进

将id分配分为两层:

· 上层的“id分配器”对应用暴露,提供一次申请一个id的接口,一般本IDC的应用只连本IDC的id分配器。

·下层的“段分配器”对“id分配器”提供服务。id分配器“知晓”所有IDC的所有段分配器的存在,使用均衡策略向段分配器申请一个id段,当所持有的id段快耗尽时,再请求下一个段。

唯一性:全局中,根据分片规则,每个段分配器会持有不同的id段。例如下表中,每个段的大小是100,段分配器A持有分片0和分片1。对于每个分片而言,是一个个跳跃的id段。特殊的,当段大小为1时,段分配器就是改进前的id分配器。

均衡策略:均衡策略在id分配器来实现,简单的讲,是一个轮询策略。每个id分配器会轮询下游段分配器的状态,并选中id段的最小的那个,然后发起id段申请。由于不会加锁,当多个id分配器同时竞争时,可能会出现获取的id段不是全局最小的,可以附加一些策略来调优,比如再多获取一次,并本地排序。从整体上而言,id还是比较均衡的,可满足需求。

可用性:当发生网络划分时,本IDC的id分配器可以只连接本IDC的段分配器,成功的申请到id段。整个系统可容忍一定时间内不可协作,长时间不可协作的唯一危害是id增长不均衡,此时,就退化为改进前的方案。

多IDC环境的适应性:id分配器需要和所有IDC的段分配器交互,但是交互频率很低,同时和提供id分配服务是两个独立的阶段,不会受到多IDC网络环境的干扰。

效果

改进后的id分配方案成功的满足了图片系统重构过程中的兼容需求,并且部署在全球多个IDC内为图片系统提供全局唯一的id分配服务。

by Lizhe

深度探讨PHP之性能

No Comments 专业IT吐槽

1.缘起

关于PHP,很多人的直观感觉是PHP是一种灵活的脚本语言,库类丰富,使用简单,安全,非常适合WEB开发,但性能低下。PHP的性能是否真的就 如同大家的感觉一样的差呢?本文就是围绕这么一个话题来进行探讨的。从源码、应用场景、基准性能、对比分析等几个方面深入分析PHP之性能问题,并通过真 实的数据来说话。

2.从原理分析PHP性能

从原理分析PHP的性能,主要从以下几个方面:内存管理、变量、函数、运行机制来进行分析。

2.1内存管理

类似Nginx的内存管理方式,PHP在内部也是基于内存池,并且引入内存池的生命周期概念。在内存池方面,PHP对PHP脚本和扩展的所有内存相关操作都进行了托管。对大内存和小内存的管理采用了不同的实现方式和优化,具体可以参考以下文档:https://wiki.php.net/internals/zend_mm。在内存分配和回收的生命周期内,PHP采用一次初始化申请+动态扩容+内存标识回收机制,并且在每次请求结束后直接对内存池进行重新mask。

2.2变量

总所周知,PHP是一种弱变量类型的语言,所以在PHP内部,所有的PHP变量都对应成一种类型Zval,其中具体定义如下:

大话PHP之性能

图一PHP变量

在变量方面,PHP做了大量的优化工作,比如说Reference counting和copy on writer机制。这样能够保证内存使用上的优化,并且减少内存拷贝次数(请参考http://blog.xiuwz.com/2011/11/09 /php-using-internal-zval/)。在数组方面,PHP内部采用高效的hashtable来实现。

2.3函数

在PHP内部,所有的PHP函数都回转化成内部的一个函数指针。比如说扩展中函数


  1. 		ZEND_FUNCTION ( my_function );//类似function my_function(){}  

在内部展开后就会是一个函数


  1. 		void zif_my_function ( INTERNAL_FUNCTION_PARAMETERS );  
  2. void zif_my_function(  
  3. int ht,  
  4. zval * return_value,  
  5. zval * this_ptr,  
  6. int return_value_used,  
  7. zend_executor_globals * executor_globals  
  8. );   

从这个角度来看,PHP函数在内部也是对应一个函数指针。

2.4运行机制

在话说PHP性能的时候,很多人都会说“C/C++是编译型,JAVA是半编译型,PHP是解释型”。也就是说PHP是先动态解析再代码运行的,所以从这个角度来看,PHP性能必然很差。

的确,从PHP脚本运行来输出,的确是一个动态解析再代码运行的过程。具体来说,PHP脚本的运行机制如下图所示:

大话PHP之性能

图二 PHP运行机制

PHP的运行阶段也分成三个阶段:

  • Parse。语法分析阶段。
  • Compile。编译产出opcode中间码。
  • Execute。运行,动态运行进行输出。

所以说,在PHP内部,本身也是存在编译的过程。并且据此产生了大量的opcode cache工具,比如说apc、eacc、xcache等等。这些opcode cache在生产环境基本上在标配。基于opcode cache,能到做到“PHP脚本编译一次,多次运行”的效果。从这点上,PHP就和JAVA的半编译机制非常类似。

所以,从运行机制上来看,PHP的运行模式和JAVA是非常类似的,都是先产生中间码,然后运行在不同虚拟机上。

2.5动态运行

从上面的几个分析来看,PHP在内存管理、变量、函数、运行机制等几个方面都做了大量的工作,所以从原理来看,PHP不应该存在性能问题,性能至少也应该和Java比较接近。

这个时候就不得不谈PHP动态语言的特性所带来的性能问题了,由于PHP是动态运行时,所以所有的变量、函数、对象调用、作用域实现等等都是在执行 阶段中才确定的。这个从根本上决定了PHP性能中很难改变的一些东西:在C/C++等能够在静态编译阶段确定的变量、函数,在PHP中需要在动态运行中确 定,也就决定了PHP中间码不能直接运行而需要运行在Zend Engine上。

说到PHP变量的具体实现,又不得不说一个东西了:Hashtable。Hashtable可以说在PHP灵魂之一,在PHP内部广泛用到,包含变量符号栈、函数符号栈等等都是基于hashtable的。

以PHP变量为例来说明下PHP的动态运行特点,比如说代码:


  1. 		<?php 
  2. $var = “hello, blog.xiuwz.com”;  
  3. ?>   

该代码的执行结果就是在变量符号栈(是一个hashtable)中新增一个项

大话PHP之性能

当要使用到该变量时候,就去变量符合栈中去查找(也就是变量调用对出了一个hash查找的过程)。

同样对于函数调用也基本上类似有一个函数符号栈(hashtable)。

其实关于动态运行的变量查找特点,在PHP的运行机制中也能看出一些。PHP代码通过解释、编译后的流程下图:

大话PHP之性能

图3 PHP运行实例

从上图可以看出,PHP代码在compile之后,产出的了类符号表、函数符号表、和OPCODE。在真正执行的时候,zend Engine会根据op code去对应的符号表中进行查找,处理。

从某种程度上,在这种问题的上,很难找到解决方案。因为这是由于PHP语言的动态特性所决定的。但是在国内外也有不少的人在寻找解决方案。因为通过这样,能够从根本上完全的优化PHP。典型的列子有facebook的hiphop(https://github.com/facebook/hiphop-php)。

2.6结论

从上面分析来看,在基础的内存管理、变量、函数、运行机制方面,PHP本身并不会存在明显的性能差异,但由于PHP的动态运行特性,决定了PHP和 其他的编译型语言相比,所有的变量查找、函数运行等等都会多一些hash查找的CPU开销和额外的内存开销,至于这种开销具体有多大,可以通过后续的基准 性能和对比分析得出。

因此,也可以大体看出PHP不太适合的一些场景:大量计算性任务、大数据量的运算、内存要求很严格的应用场景。如果要实现这些功能,也建议通过扩展的方式实现,然后再提供钩子函数给PHP调用。这样可以减低内部计算的变量、函数等系列开销。

3.基准性能

对于PHP基准性能,目前缺少标准的数据。大多数同学都存在感性的认识,有人认为800QPS就是PHP的极限了。此外,对于框架的性能和框架对性能的影响很没有响应的权威数字。

本章节的目的是给出一个基准的参考性能指标,通过数据给大家一个直观的了解。

具体的基准性能有以下几个方面:

1.裸PHP性能。完成基本的功能。

2.裸框架的性能。只做最简单的路由分发,只走通核心功能。

3.标准模块的基准性能。所谓标准模块的基准性能,是指一个具有完整服务模块功能的基准性能。

3.1环境说明

测试环境:

Uname -a

Linux db-forum-test17.db01.baidu.com 2.6.9_5-7-0-0 #1 SMP Wed Aug 12 17:35:51 CST 2009 x86_64 x86_64 x86_64 GNU/Linux

Red Hat Enterprise Linux AS release 4 (Nahant Update 3)

8  Intel(R) Xeon(R) CPU           E5520  @ 2.27GHz

软件相关:

Nginx:

nginx version: nginx/0.8.54  built by gcc 3.4.5 20051201 (Red Hat 3.4.5-2)

Php5:(采用php-fpm)

PHP 5.2.8 (cli) (built: Mar  6 2011 17:16:18)

Copyright (c) 1997-2008 The PHP Group

Zend Engine v2.2.0, Copyright (c) 1998-2008 Zend Technologies

with eAccelerator v0.9.5.3, Copyright (c) 2004-2006 eAccelerator, by eAccelerator

bingo2:

PHP框架。

其他说明:

目标机器的部署方式:大话PHP之性能 脚本。

测试压力机器和目标机器独立部署。

3.2裸PHP性能

最简单的PHP脚本。


  1. 		<?php 
  2. require_once ‘./actions/indexAction.php’;  
  3. $objAction = new indexAction();  
  4. $objAction->init();  
  5. $objAction->execute();  
  6. ?> 
  7. Acitons/indexAction.php里面的代码如下  
  8. <?php 
  9. class indexAction  
  10. {  
  11. public function execute()  
  12. {  
  13. echo ‘hello, world!’;  
  14. }  
  15. }  
  16. ?>   

通过压力工具测试结果如下:

大话PHP之性能

3.3裸PHP框架性能

为了和3.2的对比,基于bingo2框架实现了类似的功能。代码如下


  1. 		<?php 
  2. require_once ‘Bingo/Controller/Front.php’;  
  3. $objFrontController = Bingo_Controller_Front::getInstance(array(  
  4. ‘actionDir’ => ‘./actions’,  
  5. ));  
  6. $objFrontController->dispatch(); 

压力测试结果如下:

大话PHP之性能

从该测试结果可以看出:框架虽然有一定的消耗,但对整体的性能来说影响是非常小的。

3.4标准PHP模块的基准性能

所谓标准PHP模块,是指一个PHP模块所必须要具体的基本功能:

路由分发。

自动加载。

LOG初始化&Notice日志打印。所以的UI请求都一条标准的日志。

  • 错误处理。
  • 时间校正。
  • 自动计算每个阶段耗时开销。
  • 编码识别&编码转化。
  • 标准配置文件的解析和调用

采用bingo2的代码自动生成工具产生标准的测试PHP模块:test。

测试结果如下:

大话PHP之性能

3.5结论

从测试数据的结论来看,PHP本身的性能还是可以的。基准性能完全能够达到几千甚至上W的QPS。至于为什么在大多数的PHP模块中表现不佳,其实 这个时候更应该去找出系统的瓶颈点,而是简单的说OK,PHP不行,那我们换C来搞吧。(下一个章节,会通过一些例子来对比,采用C来处理不见得有特别的 优势)

通过基准数据,可以得出以下几个具体的结论:

1.PHP本身性能也很不错。简单功能下能够达到5000QPS,极限也能过W。

2.PHP框架本身对性能影响非常有限。尤其是在有一定业务逻辑和数据交互的情况下,几乎可以忽略。

3.一个标准的PHP模块,基准性能能够达到2000QPS(80 cpu idle)。

4.对比分析

很多时候,大家发现PHP模块性能不行的时候,就来一句“ok,我们采用C重写吧”。在公司内,采用C/C++来写业务逻辑模块的现象到处都有,在前几年甚至几乎全部都是采用C来写。那时候大家写的真是一个痛苦:调试难、敏捷不要谈。

文章出自:baidu-tech.com

Nginx应用案例分享:压力测试

No Comments 专业IT吐槽

在运维工作中,压力测试是一项非常重要的工作。比如在一个网站上线之前,能承受多大访问量、在大访问量情况下性能怎样,这些数据指标好坏将会直接影响用户体验。

但是,在压力测试中存在一个共性,那就是压力测试的结果与实际负载结果不会完全相同,就算压力测试工作做的再好,也不能保证100%和线上性能指标相同。面对这些问题,我们只能尽量去想方设法去模拟。所以,压力测试非常有必要,有了这些数据,我们就能对自己做维护的平台做到心中有数。

目前较为常见的网站压力测试工具有webbench、ab(apache bench)、tcpcopy、loadrunner

软件名称简介优缺点

webbench由Lionbridge公司开发,主要测试每秒钟请求数和每秒钟数据传输量,同时支持静态、动态、SSL

部署简单,静动态均可测试。适用于小型网站压力测试(单例最多可模拟3万并发)

ab(apache bench)Apache自带的压力测试工具,主要功能用于测试网站每秒钟处理请求个数

多见用于静态压力测试,功能较弱,非专业压力测试工具

tcpcopy基于底层应用请求复制,可转发各种在线请求到测试服务器,具有分布式压力测试功能,所测试数据与实际生产数据较为接近后起之秀,主要用于中大型压力测试,所有基于 tcp的packets均可测试

loadrunner压力测试界的泰斗,可以创建虚拟用户,可以模拟用户真实访问流程从而录制成脚本,其测试结果也最为逼真模拟最为逼真,并可进行独立的单元测试,但是部署配置较为复杂,需要专业人员才可以。

下面,笔者就以webbench为例,来讲解一下网站在上线之前压力测试是如何做的。

安装webbench

#wget http://home.tiscali.cz/~cz210552/distfiles/webbench-1.5.tar.gz

#tar zxvf webbench-1.5.tar.gz

#cd webbench-1.5

#make && make install

进行压力测试

并发200时

# webbench -c 200 -t 60 http://blog.luwenju.com/index.php

参数解释:-c为并发数,-t为时间(秒)

Webbench – Simple Web Benchmark 1.5

Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://blog.luwenju.com/index.php

200 clients, running 60 sec.

Speed=1454 pages/min, 2153340 bytes/sec.

Requests: 1454 susceed, 0 failed.

当并发200时,网站访问速度正常

并发800时

#webbench -c 800 -t 60 http://blog.luwenju.com/index.php

Webbench – Simple Web Benchmark 1.5

Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://blog.luwenju.com/index.php

800 clients, running 60 sec.

Speed=1194 pages/min, 2057881 bytes/sec.

Requests: 1185 susceed, 9 failed.

当并发连接为800时,网站访问速度稍慢

并发1600时

#webbench -c 1600 -t 60 http://blog.luwenju.com/index.php

Webbench – Simple Web Benchmark 1.5

Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://blog.luwenju.com/index.php

1600 clients, running 60 sec.

Speed=1256 pages/min, 1983506 bytes/sec.

Requests: 1183 susceed, 73 failed.

当并发连接为1600时,网站访问速度便非常慢了

并发2000时

#webbench -c 2000 -t 60 http://blog.luwenju.com/index.php

Webbench – Simple Web Benchmark 1.5

Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://blog.luwenju.com/index.php

2000 clients, running 60 sec.

Speed=2154 pages/min, 1968292 bytes/sec.

Requests: 2076 susceed, 78 failed.

当并发2000时,网站便出现“502 Bad Gateway”,由此可见web服务器已无法再处理用户访问请求

总结:

1、压力测试工作应该放到产品上线之前,而不是上线以后

2、测试时尽量跨公网进行,而不是内网

3、测试时并发应当由小逐渐加大,比如并发100时观察一下网站负载是多少、打开是否流程,并发200时又是多少、网站打开缓慢时并发是多少、网站打不开时并发又是多少

4、 应尽量进行单元测试,如B2C网站可以着重测试购物车、推广页面等,因为这些页面占整个网站访问量比重较大

28 个必备的 Linux 命令行工具

No Comments Linux


dstat
& sar

iostat, vmstat, ifstat and much more in one.

dstat screenshot

slurm

网络流量图形化工具

slurm screenshot

vim & emacs

这个没人不知道吧~

vim screenshot

screen, dtach, tmux, byobu

保持你的终端连接活跃。

gnu screen screenshot

multitail

在不同的窗口查看日志文件。

multitail screenshot

tpp

命令行下面的PPT工具!

tpp screenshot

xargs & parallel

根据输入执行任务,多线程哦!

xargs screenshot

duplicity & rsyncrypto

加密备份工具。

duplicity screenshot

nethack & slash’em

这个星球上最复杂的游戏 =,=

nethack screenshot

lftp

FTP工具。

lftp screenshot

ack

比grep更好的检索源码的工具。

ack screenshot

calcurse & remind + wyrd

日历

calcurse screenshot

newsbeuter & rsstail

命令行RSS阅读器

newsbeuter screenshot

powertop

帮助Linux系统省电工具。

powertop screenshot

htop & iotop

进程,内存,IO,CPU监控工具。

htop screenshot

ttyrec & ipbt

终端操作录像/回放工具。

ipbt screenshot

rsync

文件系统同步工具,SSH哦!

rsync screenshot

mtr

traceroute 2.0.

mtr screenshot

socat & netpipes

在socket接口中导入或者导出信息。

socat screenshot

iftop & iptraf

看看你的流量都到哪里去了?

iftop screenshot

siege & tsung

命令行压力测试工具。

siege screenshot

ledger

会计工具!

ledger screenshot

taskwarrior

任务管理工具

taskwarrior screenshot

curl

做HTTP的都知道吧~

curl screenshot

rtorrent & aria2

命令行BT下载~

rtorrent screenshot

ttytter & earthquake

命令行twitter工具,哈哈!

ttytter screenshot

vifm & ranger

midnight 控制工具的替代者~

vifm screenshot

cowsay & sl

牛牛~~~~

cowsay screenshot