文章列表
 
2010年03月26日 星期五 下午 11:13
欢迎访问我的新主页:http://huoding.com/

作者:老王

PHP社区有很多框架可供选择,比如CodeIgniterYiiSolarZendSymfonyCakePHPLithium等。一直以来,为了争做头牌,各个框架的粉丝团喋喋不休,好事者还试图通过运行Hello World来以性能决胜负,但难以服众,其实问题的本质在于大家对框架的理解不同所致,属于审美观的差异。

从整体结构上来看,对框架而言,微内核很重要。所谓微内核说白了就是代码要迷你。代码越多Bug越多,而且伴随着复杂度的升高,一方面会造成运行效率的下降,另一方面在使用时的易用度也好不到哪去。反之,避开高,大,全,的模式套路,只保留最核心的功能,便能有足够的精力保证研发工作的可持续性发展,至于其他非核心功能,则留给第三方去实现,如此一来,才能形成一个欣欣向荣的生态社区,这点是非常重要的,对一个项目而言,即便它在技术上是非常优秀的,但如果没有形成自己的生态社区,也必将没落,生态社区是任何项目存在的基石,只要你营造了一个和谐的生态社区,它就会自然而然的保证新陈代谢向前发展,这方面的例子很多,比如Firefox,JQuery等等,都有大把大把的第三方优秀插件,它们都是生态社区里优胜劣汰的产物。

细化到局部的设计,对一个Web框架而言,遵循Web的本质去设计框架很重要,一个半开玩笑的判断框架好坏的方法是看它是否实现了request和response封装,对一次HTTP请求而言,一个request进来,一个response出去,是很正常的流程,如果没有封装这些基本元素,自然不是一个完善的Web框架,使用起来就有可能出现问题,比如说下面一段取自Zend框架的演示代码:

01 class FooController extends Zend_Controller_Action
02 {
03     public function createAction()
04     {
05         if ($this->some->save()) {
06             $this->_redirect(...);
07         }
08
09         // ...
10     }
11 }


需要注意的是,这里标红的redirect方法实际上含有一个exit操作,也就是说,脚本执行到这里,就被硬编码中断了,如果控制器需要存在一个操作前后的before,after触发器(在Zend框架里称作preDispatch,postDispatch),那这这种情形下就只能仰天长叹了,因为执行过程由于前面提到的硬编码中断已经退出了,after(postDispatch)触发器永远没机会执行。

那么看看更好的设计应该是什么样的,意淫一段伪代码:

01 class FooAction extends Action
02 {
03     public function execute($request)
04     {
05         if ($this->some->save())
06         {
07             return $this->redirector(...);
08         }
09
10         // ...
11     }
12 }


非常自然的一个操作流程,request进来(作为参数),response出去(作为返回值),这里没有使用硬编码的中断,而是返回的redirector,这里的redirector实际就是一个response类型,由于是return而不是exit,所以通过前端控制器的协调,操作前后的before,after触发器都可以被执行到。

一直在强调简单,自然。下面就可以看到简单,自然带来的好处了。假如一个操作涉及缓存管理,会话管理等逻辑,由于他们,可能存在复杂的关系,在传统的处理方式中,代码免不了涉及很多if,else之类的嵌套逻辑,这无疑是坏味道,下面看看如果在简单自然的架构上面扩展这样的功能:

01 class FooAction extends Action
02 {
03     protected $wrappers = array('Cache', 'Session');
04
05     public function execute($request)
06     {
07         // ...
08     }
09 }


可以看到这里全然没有if,else的困扰,不过要想明确操作过程,还得看看里面的wrapper是如何设计的:

01 class CacheWrapper extends Wrapper
02 {
03     public function execute($request)
04     {
05         // ...
06     }
07 }


01 class SessionWrapper extends Wrapper
02 {
03     public function execute($request)
04     {
05         // ...
06     }
07 }


执行方法看似复杂,实际就是一个装饰模式的简单应用而已,如下:

01 $object = new CacheWrapper(
02     new SessionWrapper(
03         new FooAction()
04     )
05 );
06
07 $response = $object->execute($request);


其实这样的方式在Python框架里很常见,但PHP框架里鲜有耳闻。如果还不清楚,欢迎仔细围观下图:




程序其实就是一个洋葱头,一层一层的,简单却又不失扩展的灵活性,这就是本文的Web框架审美观。

补充:有人会诧异为什么上面把控制器设计成单Action风格,而不是现在流行的多Action风格?这是因为只有使用单Action风格,接口才是稳定的(只有一个execute方法),这样才能使用装饰模式,当然如果是多Action的话,也可以使用__call来装饰,但那样显得不太优雅,所以我不喜欢。

后记:如果对文中提及的装饰模式实现方式感兴趣,可以参考我很久以前写的文章:半透明的装饰模式

思考后的补充:

通过和很多网友的交流,我又反思了一下,感觉对PHP而言,封装request和response对象虽然很优雅,但是显得有点“重”了,对request而言,或许直接操作GPC更直接,对response而言,可以参照Python WSGI的风格,预定义一个数组结构,比如array('headers' => ..., 'body' => ..., ...),或许更好用,Action也不用再以类的形式存在,返璞归真为最简单的脚本(脚本也可以有返回值,return我们上面说的预定义好的数组结构,通过include操作接收返回值),或者function形式,不过这样的话,简单的request,response需求无所谓,一旦涉及复杂的request,response操作,由于都是数组形式,没有对象可以封装相关逻辑,代码会显得杂乱,而且装饰器的使用也要重新考虑了,我还不能确认是否值得,存疑。

我新写了一篇《返璞归真:面向过程的装饰模式实现》,欢迎参阅。
 
2010年03月13日 星期六 下午 9:35
欢迎访问我的新主页:http://huoding.com/

作者:老王

依赖注入Dependency Injection(也有人称作控制反转IoC)可以有效的改善程序质量。

Fabien Potencier写了一些列的文章来说明Dependency Injection,可供参考:

* Part 1: What is Dependency Injection?
* Part 2: Do you need a Dependency Injection Container?
* Part 3: Introduction to the Symfony Service Container
* Part 4: Symfony Service Container: Using a Builder to create Services
* Part 5: Symfony Service Container: Using XML or YAML to describe Services
* Part 6: The Need for Speed

如果你喜欢短平快,也可以先看个下面这个例子:

01 class Foo
02 {
03     private $db;
04
05     public function __construct()
06     {
07         $this->db = new DB($host, $user, $password);
08     }
09
10     public function bar()
11     {
12         $this->db->query();
13     }
14 }


可以明显看出Foo和DB是通过硬编码紧密耦合在一起的,丧失了变化的余地,看看改善后的代码:

01 class Foo
02 {
03     private $db;
04
05     public function __construct($db)
06     {
07         $this->db = $db;
08     }
09
10     public function setDB($db)
11     {
12         $this->db = $db;
13     }
14
15     public function bar()
16     {
17         $this->db->query();
18     }
19 }


通过constructor或者setter的方式,我们可以注入自己需要的对象,比如说正常的调用代码如下:

$foo = new Foo(new DB($host, $user, $password));
$foo->bar();


如果是测试的话,则可以创建一个MockDB对象,以其屏蔽数据库本身对测试的影响:

$foo = new Foo(new MockDB($host, $user, $password));
$foo->bar();


注意对象配置参数的问题,比如说实例化一个数据库对象,对象配置参数不仅要涉及主机,用户,密码,还要涉及数据库,编码等等,一旦类似的对象很多,问题就会复杂起来,代码会显得杂乱无章。此时可以通过Dependency Injection Container来统一控制对象的实例化,屏蔽参数带来的额外复杂度。

PHP社区的Dependency Injection Container,既有极轻量级的Twittee,也有极重量级的Symfony Dependency Injection。当然,并不是所有人都认可这样的设计思路,比如说Lithium的开发者就抛弃了Dependency Injection Container的固有思维,转而采用了类本身作为容器的方法,不过要注意的是虽然可以使用类本身作为容器,但是实例化类的时候可能需要很多参数,这同样是需要配置文件的,不过不会采用XML格式,而是使用原生的PHP语法,然后在实例化的时候通过public __construct(array $config = array())的形式传递进去,这点在他们的幻灯片Lithium: The Framework for People Who Hate Frameworks里有详细的介绍。

回到开头说的,Dependency Injection所带来的好处之一在于增加了代码的可测试性。不过采用Dependency Injection的结果是系统架构必须小心设计,但对于脚本语言PHP来说,问题似乎不是那么严重,一个名为test_helpers的PHP扩展有助于提高代码的可测试性,它能动态Mock掉硬编码的new代码,这点很神奇,强烈推荐看看官方的演示代码。

参考:Dependency Injection with PHP and PHP 5.3
 
2010年03月01日 星期一 上午 9:58
欢迎访问我的新主页:http://huoding.com/

作者:老王

一直以来,多数人在使用MyISAM时都是按照增大Key_read_requests / Key_reads的原则来设置key_buffer_size的,没想到这竟然是错误的!这次给大家醍醐灌顶的仍然是MySQL Performance Blog,详细描述参考:Why you should ignore MySQL’s key cache hit ratio

Key_read_requests和Key_reads就是两个计数器,它们的含义如下:

Key_read_requests:从缓存读取索引的请求次数。
Key_reads:从磁盘读取索引的请求次数。

通常人们认为Key_read_requests / Key_reads越大越好,否则就应该增大key_buffer_size的设置,但通过计数器的比例来调优有两个问题:

问题一:比例并不显示数量的绝对值大小
问题二:计数器并没有考虑时间因素

虽说Key_read_requests大比小好,但是对于系统调优而言,更有意义的应该是单位时间内的Key_reads:

Key_reads / Uptime

你可以通过命令行得到一个实时的数据结果,比如:

# mysqladmin ext -ri10 | grep Key_reads

| Key_reads                         | 83777189     |
| Key_reads                         | 211          |
| Key_reads                         | 177          |
| Key_reads                         | 202          |



提示:命令里的mysqladmin ext其实就是mysqladmin extended-status,你甚至可以简写成mysqladmin e。

其中第一行表示的是汇总数值,所以这里不必考虑,下面的每行数值都表示10秒内的数据变化,从这份数据可以看出每10秒系统大约会出现200次Key_reads访问,折合到每1秒就是20次左右,至于这个数值到底合理与否,就由服务器的磁盘能力而定了。

顺便说一句,为啥数据按10秒取样,而不是直接按1秒取样?这里看看按1秒的结果:

# mysqladmin ext -ri1 | grep Key_reads

| Key_reads                         | 83776743     |
| Key_reads                         | 7            |
| Key_reads                         | 7            |
| Key_reads                         | 38           |


可以看到,由于时间段过小,数据变化比较剧烈,不容易直观估计大小,所以通常数据按照10秒或者60秒之类的时间段来取样是更好的。

忘记:Key_read_requests / Key_reads
牢记:Key_reads / Uptime

补充链接:http://www.pythian.com/news/9035/database-tuning-ratio-vs-rate/
 
2010年02月22日 星期一 下午 3:40
欢迎访问我的新主页:http://huoding.com/

作者:老王

这个问题遇到好几次了,今天翻看以前代码的时候看到,便记下来,先用一段代码重现一下问题:

<pre>
<?php
$string = <<<EOF
<data>
<foo><bar>hello</bar></foo>
<foo><bar>world</bar></foo>
</data>
EOF;

$data = simplexml_load_string($string);

print_r($data);
print_r($data->foo);
?>
</pre>


乍一看,结果很让人费解:
SimpleXMLElement Object
(
[foo] => Array
(
[0] => SimpleXMLElement Object
(
[bar] => hello
)
[1] => SimpleXMLElement Object
(
[bar] => world
)
)
)
SimpleXMLElement Object
(
[bar] => hello
)

明明print_r显示foo是一个有两个bar元素的数组,但是最后却仅仅显示了一个bar元素!

原因其实很简单,在如上所示simplexml_load_string的结果里,foo并不是数组,而是一个迭代对象!

可以这样确认:

foreach ($data->foo as $v) print_r($v);
foreach ($data->children() as $v) print_r($v);


看来,print_r或者var_dump之类的表象并不完全可信,自己多留心吧。
 
2010年02月22日 星期一 下午 2:27
欢迎访问我的新主页:http://huoding.com/

作者:老王

获得客户端IP原本是一个非常简单的事情,程序里很多时候使用REMOTE_ADDR环境变量就可以了,但随着网络结构复杂性越来越高,比如说Apache前有Nginx,或者Nginx前有Squid,这时就复杂了,还得考虑诸如:HTTP_CLIENTADDRESS,HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR等环境变量,当然可以通过编写程序代码来处理此类逻辑,不过这样的解决方法不够透明,毕竟很多第三方程序并没有考虑这些情况,而且后面服务器的日志里面记录的IP全变成了前面服务器的IP,如此一来,Awstats之类的软件就失效了。下面看看如何透明处理:

案例1:Apache前有Nginx

有些网站使用这样的方式来分离静态请求和动态请求,Nginx放在前面处理静态请求,然后再把动态请求转发给后面的Apache,不过如此一来,Apache日志里看到的IP就是Nginx的IP了,为了能让Apache透明获取IP,可以使用mod_rpaf

配置很简单,只需在配置文件里加上如下内容:

LoadModule rpaf_module libexec/apache2/mod_rpaf-2.0.so
RPAFenable On
RPAFsethostname On
RPAFproxy_ips 127.0.0.1 192.168.0.1

RPAFheader X-Forwarded-For


说明:192.168.0.1指的是Nginx内网IP,可以设置多个IP。

最后确认一下Nginx配置文件里在把动态请求转发给Apache的时候是否设置了如下内容:

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;


重新加载一下服务,差不多就OK了。

案例2:Nginx前有Squid

有些网站使用这样的方式来搭建分布式缓存,若干台Squid放在前面提供缓存服务,内容从后面的Nginx获取。不过如此一来,Nginx日志里看到的IP就是Squid的IP了,为了能让Nginx透明获取IP,可以使用NginxHttpRealIpModule

NginxHttpRealIpModule缺省并没有激活,可以在编译的时候使用--with-http_realip_module选项激活它。

配置很简单,只需在配置文件里加上如下内容:

set_real_ip_from   192.168.1.0/24;
set_real_ip_from   192.168.2.1;
real_ip_header     [X-Real-IP|X-Forwarded-For];


需要说明的地方就是设置IP源的时候可以设置单个IP,也可以设置IP段,另外是使用X-Real-IP还是X-Forwarded-For,取决于前面的服务器有哪个头

重新加载一下服务,差不多就OK了。

另外:Lighttpd有一个类似的模块叫mod_extforward,功能差不多,就不多说了。
 
   
 
 
文章存档
 
     
 
最新文章评论
  

[表情]
 

不错!
 

linux大师之路,www.linuxmr.com
 

引导一直没有整明白说。
 

[表情]
   
帮助中心 | 空间客服 | 投诉中心 | 空间协议
©2012 Baidu