<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>火丁笔记</title>
	<atom:link href="http://huoding.com/feed" rel="self" type="application/rss+xml" />
	<link>http://huoding.com</link>
	<description>记录WEB开发的点点滴滴</description>
	<lastBuildDate>Sun, 13 May 2012 05:50:51 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.2</generator>
		<item>
		<title>Unserialize与Autoload</title>
		<link>http://huoding.com/2012/04/28/149</link>
		<comments>http://huoding.com/2012/04/28/149#comments</comments>
		<pubDate>Sat, 28 Apr 2012 07:32:08 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=149</guid>
		<description><![CDATA[但凡是一个合格的PHP程序员，就应该知道Unserialize与Autoload，但是要说起二者之间的关系，恐怕一清二楚的人就不多了。 说个例子，假设我们可以拿到第三方的序列化数据，但没有相应的类定义，代码如下： &#60;?php $string = 'O:6:"Foobar":2:{s:3:"foo";s:1:"1";s:3:"bar";s:1:"2";}'; $result = unserialize($string); var_dump($result); /* object(__PHP_Incomplete_Class)[1] public '__PHP_Incomplete_Class_Name' =&#62; string 'Foobar' (length=6) public 'foo' =&#62; string '1' (length=1) public 'bar' =&#62; string '2' (length=1) */ ?&#62; 当我们反序列化一个对象时，如果对象的类定义不存在，那么PHP会引入一个未完成类的概念，即：__PHP_Incomplete_Class，此时虽然我们反序列化成功了，但还是无法访问对象中的数据，否则会出现如下报错信息： The script tried to execute a method &#8230; <a href="http://huoding.com/2012/04/28/149">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>但凡是一个合格的PHP程序员，就应该知道<a href="http://www.php.net/manual/en/function.unserialize.php" target="_blank">Unserialize</a>与<a href="http://www.php.net/manual/en/function.autoload.php" target="_blank">Autoload</a>，但是要说起二者之间的关系，恐怕一清二楚的人就不多了。</p>
<p><span id="more-149"></span></p>
<p>说个例子，假设我们可以拿到第三方的序列化数据，但没有相应的类定义，代码如下：</p>
<pre>&lt;?php

$string = 'O:6:"Foobar":2:{s:3:"foo";s:1:"1";s:3:"bar";s:1:"2";}';

$result = unserialize($string);

var_dump($result);

/*

object(__PHP_Incomplete_Class)[1]
  public '__PHP_Incomplete_Class_Name' =&gt; string 'Foobar' (length=6)
  public 'foo' =&gt; string '1' (length=1)
  public 'bar' =&gt; string '2' (length=1)

*/

?&gt;</pre>
<p>当我们反序列化一个对象时，如果对象的类定义不存在，那么PHP会引入一个未完成类的概念，即：__PHP_Incomplete_Class，此时虽然我们反序列化成功了，但还是无法访问对象中的数据，否则会出现如下报错信息：</p>
<blockquote><p>The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition.</p></blockquote>
<p>这不是什么难事儿，只要做一次强制类型转换，变成数组就OK了：</p>
<pre>&lt;?php

$string = 'O:6:"Foobar":2:{s:3:"foo";s:1:"1";s:3:"bar";s:1:"2";}';

$result = (array)unserialize($string);

var_dump($result);

/*

array
  '__PHP_Incomplete_Class_Name' =&gt; string 'Foobar' (length=6)
  'foo' =&gt; string '1' (length=1)
  'bar' =&gt; string '2' (length=1)

*/

?&gt;</pre>
<p>不过如果系统激活了Autoload，情况会变得复杂些。顺便插句话：PHP其实提供了一个名为unserialize_callback_func配置选项，但意思和autoload差不多，这里就不介绍了，咱们就说autoload，例子如下：</p>
<pre>&lt;?php

spl_autoload_register(function($name) {
    var_dump($name);
});

$string = 'O:6:"Foobar":2:{s:3:"foo";s:1:"1";s:3:"bar";s:1:"2";}';

$result = (array)unserialize($string);

var_dump($result);

?&gt;</pre>
<p>执行上面代码会发现，spl_autoload_register被触发了，多数时候这是有意义的，但如果遇到一个定义不当的spl_autoload_register，就悲催了，比如说下面这段代码：</p>
<pre>&lt;?php

spl_autoload_register(function($name) {
    include "/path/to/{$name}.php";
});

$string = 'O:6:"Foobar":2:{s:3:"foo";s:1:"1";s:3:"bar";s:1:"2";}';

$result = (array)unserialize($string);

var_dump($result);

?&gt;</pre>
<p>毫无疑问，因为找不到类定义文件，所以报错了！改改spl_autoload_register肯定行，但前提是你能改，如果涉及第三方代码，我们就不能擅自做主了，此时我们需要一种方法让unserialize能绕开autoload，最简单的方法是把我们需要的类FAKE出来：</p>
<pre>&lt;?php

spl_autoload_register(function($name) {
    include "/path/to/{$name}.php";
});

class Foobar {} // Oh, Shit!

$string = 'O:6:"Foobar":2:{s:3:"foo";s:1:"1";s:3:"bar";s:1:"2";}';

$result = (array)unserialize($string);

var_dump($result);

?&gt;</pre>
<p>不得不说，上面的代码真的很狗屎！那怎么做才好呢？我大致写了一个实现：</p>
<pre>&lt;?php

spl_autoload_register(function($name) {
    include "/path/to/{$name}.php";
});

$string = 'O:6:"Foobar":2:{s:3:"foo";s:1:"1";s:3:"bar";s:1:"2";}';

$functions = spl_autoload_functions();

foreach ($functions as $function) {
    spl_autoload_unregister($function);
}

$result = (array)unserialize($string);

foreach ($functions as $function) {
    spl_autoload_register($function);
}

var_dump($result);

?&gt;</pre>
<p>代码虽然多了点，但至少没有FAKE类，看上去舒服多了。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2012/04/28/149/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>模式物语之装饰器</title>
		<link>http://huoding.com/2012/03/04/144</link>
		<comments>http://huoding.com/2012/03/04/144#comments</comments>
		<pubDate>Sun, 04 Mar 2012 12:51:41 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Pattern]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=144</guid>
		<description><![CDATA[所谓装饰器，英文称之为Decorator，亦或者Wrapper。如果让我选择最喜爱的模式，我想我会毫不犹豫的投它一票。那到底什么是装饰器呢？且听我慢慢道来。 热身问题：顾客来买车，车本身有一个价格，如果要加内饰、镀膜、导航等配件，就需要另加钱。等到结帐的时候，程序应该如何处理呢？最直接的方法就是创建车类，和内饰、镀膜、导航等配件类，然后根据顾客的购买情况遍历获取最终价格。这样做最大的问题是对象的概念被割裂了，计算价格的操作变得过程化。当然我们可以通过创建子类来解决这类问题，比如根据不同的配件组合创建不同的车子类，如：内饰车类，镀膜车类，导航车类，但这还不算完，还有内饰镀膜车类，内饰导航车类等等。如果新加一个配件，还会衍生出若干个新的子类。早晚有一天你会崩溃的。 装饰器可以解决此类问题，其UML如下图所示： 对应到我们的热身问题，车就是ConcreteComponent，至于内饰、镀膜、导航等配件都是ConcreteDecorator，根据顾客的购买情况，我们可以用一个或多个配件来装饰车，看上去就好像嵌套实例化一样，在实际调用operation计算价格的时候，每个装饰器都有机会修正结果，从而实现动态扩展对象的目的。 不过车的问题似乎离我们这些苦逼程序员有点远，所以就不编码实现了。我还是列举一些大家都耳熟能详的问题吧：我们做了一个后台，用户在操作的时候，我们需要判断用户身份，以及是否有权限等等，类似的逻辑很多，如何设计才能保证可扩展性？ 下面，我会利用装饰器模式来解决这个问题，实现一个可扩展的控制器： 首先我们创建一个抽象的Action类，并通过继承它创建一个具体的AdminAction类，并配置好它的Decorators属性（用属性来消灭配置文件）： &#60;?php abstract class Action { public $decorators = array(); abstract public function execute(); } class AdminAction extends Action { public $decorators = array( 'Auth' ); public function execute() { var_dump('execute admin'); } } &#8230; <a href="http://huoding.com/2012/03/04/144">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>所谓<a href="http://en.wikipedia.org/wiki/Decorator_pattern" target="_blank">装饰器</a>，英文称之为Decorator，亦或者Wrapper。如果让我选择最喜爱的模式，我想我会毫不犹豫的投它一票。那到底什么是装饰器呢？且听我慢慢道来。</p>
<p><span id="more-144"></span></p>
<p>热身问题：顾客来买车，车本身有一个价格，如果要加内饰、镀膜、导航等配件，就需要另加钱。等到结帐的时候，程序应该如何处理呢？最直接的方法就是创建车类，和内饰、镀膜、导航等配件类，然后根据顾客的购买情况遍历获取最终价格。这样做最大的问题是对象的概念被割裂了，计算价格的操作变得过程化。当然我们可以通过创建子类来解决这类问题，比如根据不同的配件组合创建不同的车子类，如：内饰车类，镀膜车类，导航车类，但这还不算完，还有内饰镀膜车类，内饰导航车类等等。如果新加一个配件，还会衍生出若干个新的子类。早晚有一天你会崩溃的。</p>
<p>装饰器可以解决此类问题，其UML如下图所示：</p>
<div id="attachment_148" class="wp-caption alignnone" style="width: 510px"><a href="http://huoding.com/wp-content/uploads/2012/03/decorator.png"><img class="size-full wp-image-148" title="装饰器" src="http://huoding.com/wp-content/uploads/2012/03/decorator.png" alt="装饰器" width="500" height="396" /></a><p class="wp-caption-text">装饰器</p></div>
<p>对应到我们的热身问题，车就是ConcreteComponent，至于内饰、镀膜、导航等配件都是ConcreteDecorator，根据顾客的购买情况，我们可以用一个或多个配件来装饰车，看上去就好像嵌套实例化一样，在实际调用operation计算价格的时候，每个装饰器都有机会修正结果，从而实现动态扩展对象的目的。</p>
<p>不过车的问题似乎离我们这些苦逼程序员有点远，所以就不编码实现了。我还是列举一些大家都耳熟能详的问题吧：我们做了一个后台，用户在操作的时候，我们需要判断用户身份，以及是否有权限等等，类似的逻辑很多，如何设计才能保证可扩展性？</p>
<p>下面，我会利用装饰器模式来解决这个问题，实现一个可扩展的控制器：</p>
<p>首先我们创建一个抽象的Action类，并通过继承它创建一个具体的AdminAction类，并配置好它的Decorators属性（用属性来消灭配置文件）：</p>
<pre>&lt;?php

abstract class Action
{
    public $decorators = array();

    abstract public function execute();
}

class AdminAction extends Action
{
    public $decorators = array(
        'Auth'
    );

    public function execute()
    {
        var_dump('execute admin');
    }
}

?&gt;</pre>
<p>接着我们创建一个抽象的Decorator类，并通过继承它创建一个具体的AuthDecortor类和UserDecorator类，需要注意的是装饰器本身也可以被装饰，但这有可能会造成递归死循环，本文出于篇幅的考虑忽略了此问题：</p>
<pre>&lt;?php

abstract class Decorator extends Action
{
    protected $action;

    public function __construct(Action $action)
    {
        $this-&gt;action = $action;
    }
}

class AuthDecorator extends Decorator
{
    public $decorators = array(
        'User'
    );

    public function execute()
    {
        var_dump('begin auth');
        $this-&gt;action-&gt;execute();
        var_dump('end auth');
    }
}

class UserDecorator extends Decorator
{
    public function execute()
    {
        var_dump('begin user');
        $this-&gt;action-&gt;execute();
        var_dump('end user');
    }
}

?&gt;</pre>
<p>最后我们创建一个Dispatcher，它就像一个前端控制器一样：</p>
<pre>&lt;?php

class Dispatcher
{
    public static function run()
    {
        $factory = function($action) use(&amp;$factory) {
            $decorators = array_reverse($action-&gt;decorators);
            foreach ($decorators as $decorator) {
                $decorator .= 'Decorator';
                $action = $factory(new $decorator($action));
            }
            return $action;
        };

        $action = $factory(new AdminAction());
        $action-&gt;execute();
    }
}

?&gt;</pre>
<p>大功告成，我们可以运行一下代码看看效果：</p>
<pre>&lt;?php

Dispatcher::run();

/*
begin user
begin auth
execute admin
end auth
end user
*/

?&gt;</pre>
<p>乍看上去，装饰器模式似乎和很多框架控制器中提供的before/after钩子方法的实现方式差不多，但实际上它们的运行机制完全不同，before/after能实现的效果，用装饰器都可以实现，但反过来却未必，比如：我可以实现一个事务装饰器，在装饰器里try/catch代码，一旦发现有未捕捉的异常就回滚，否则就提交，这个效果用before/after是无法实现的，因为try/catch是一个整体，不能割裂到before/after两个部分中去。</p>
<p>结尾再唠叨一点题外话，Python对装饰器提供了语法级的实现（PEP<a href="http://www.python.org/dev/peps/pep-0318/" target="_blank">0308</a>/<a href="http://www.python.org/dev/peps/pep-3129/" target="_blank">3129</a>），虽然对我们LAMP程序员来说，这只有羡慕嫉妒恨的份儿，但多了解了解总比坐井观天强。</p>
<p>另外贴一张来自Python社区的洋葱图片，生动的诠释了WEB请求的流程，同时也有助于大家深入理解装饰器模式的运行机制：洋葱核心是真正的业务逻辑，外面每层洋葱皮都是一个装饰器。我每次看它，都有一种醍醐灌顶的感觉：</p>
<div id="attachment_147" class="wp-caption alignnone" style="width: 488px"><a href="http://huoding.com/wp-content/uploads/2012/03/onion.png"><img class="size-full wp-image-147" title="透过洋葱看装饰器" src="http://huoding.com/wp-content/uploads/2012/03/onion.png" alt="透过洋葱看装饰器" width="478" height="435" /></a><p class="wp-caption-text">透过洋葱看装饰器</p></div>
<p>补充：有人可能会问为什么我在例子中把控制器设计成单Action风格，而不是现在流行的多Action风格？这主要是因为只有使用单Action风格，接口才是稳定的（只有一个execute方法），如此一来才可以更优雅的使用装饰模式，当然如果是多Action的话，也可以使用魔术方法__call等方法来实现装饰模式，但那样显得太生硬了，我不喜欢。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2012/03/04/144/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Redis消息通知系统的实现</title>
		<link>http://huoding.com/2012/02/29/146</link>
		<comments>http://huoding.com/2012/02/29/146#comments</comments>
		<pubDate>Wed, 29 Feb 2012 12:06:09 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[Redis]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=146</guid>
		<description><![CDATA[最近忙着用Redis实现一个消息通知系统，今天大概总结了一下技术细节，其中演示代码如果没有特殊说明，使用的都是PhpRedis扩展来实现的。 内存 比如要推送一条全局消息，如果真的给所有用户都推送一遍的话，那么会占用很大的内存，实际上不管粘性有多高的产品，活跃用户同全部用户比起来，都会小很多，所以如果只处理登录用户的话，那么至少在内存消耗上是相当划算的，至于未登录用户，可以推迟到用户下次登录时再处理，如果用户一直不登录，就一了百了了。 队列 当大量用户同时登录的时候，如果全部都即时处理，那么很容易就崩溃了，此时可以使用一个队列来保存待处理的登录用户，如此一来顶多是反应慢点，但不会崩溃。 Redis的LIST数据类型可以很自然的创建一个队列，代码如下： &#60;?php $redis = new Redis; $redis-&#62;connect('/tmp/redis.sock'); $redis-&#62;lPush('usr', &#60;USRID&#62;); while ($usr = $redis-&#62;rPop('usr')) { var_dump($usr); } ?&#62; 出于类似的原因，我们还需要一个队列来保存待处理的消息。当然也可以使用LIST来实现，但LIST只能按照插入的先后顺序实现类似FIFO或LIFO形式的队列，然而消息实际上是有优先级的：比如说个人消息优先级高，全局消息优先级低。此时可以使用ZSET来实现，它里面分数的概念很自然的实现了优先级。 不过ZSET没有原生的POP操作，所以我们需要模拟实现，代码如下： &#60;?php class RedisClient extends Redis { const POSITION_FIRST = 0; const POSITION_LAST = -1; public function &#8230; <a href="http://huoding.com/2012/02/29/146">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>最近忙着用Redis实现一个消息通知系统，今天大概总结了一下技术细节，其中演示代码如果没有特殊说明，使用的都是<a href="https://github.com/nicolasff/phpredis" target="_blank">PhpRedis</a>扩展来实现的。</p>
<p><span id="more-146"></span></p>
<h2>内存</h2>
<p>比如要推送一条全局消息，如果真的给所有用户都推送一遍的话，那么会占用很大的内存，实际上不管粘性有多高的产品，活跃用户同全部用户比起来，都会小很多，所以如果只处理登录用户的话，那么至少在内存消耗上是相当划算的，至于未登录用户，可以推迟到用户下次登录时再处理，如果用户一直不登录，就一了百了了。</p>
<h2>队列</h2>
<p>当大量用户同时登录的时候，如果全部都即时处理，那么很容易就崩溃了，此时可以使用一个队列来保存待处理的登录用户，如此一来顶多是反应慢点，但不会崩溃。</p>
<p>Redis的<a href="http://www.redis.io/commands/#list" target="_blank">LIST</a>数据类型可以很自然的创建一个队列，代码如下：</p>
<pre>&lt;?php

$redis = new Redis;
$redis-&gt;connect('/tmp/redis.sock');

$redis-&gt;lPush('usr', &lt;USRID&gt;);

while ($usr = $redis-&gt;rPop('usr')) {
    var_dump($usr);
}

?&gt;</pre>
<p>出于类似的原因，我们还需要一个队列来保存待处理的消息。当然也可以使用LIST来实现，但LIST只能按照插入的先后顺序实现类似FIFO或LIFO形式的队列，然而消息实际上是有优先级的：比如说个人消息优先级高，全局消息优先级低。此时可以使用<a href="http://www.redis.io/commands/#sorted_set" target="_blank">ZSET</a>来实现，它里面分数的概念很自然的实现了优先级。</p>
<p>不过ZSET没有原生的POP操作，所以我们需要模拟实现，代码如下：</p>
<pre>&lt;?php

class RedisClient extends Redis
{
    const POSITION_FIRST = 0;
    const POSITION_LAST = -1;

    public function zPop($zset)
    {
        return $this-&gt;zsetPop($zset, self::POSITION_FIRST);
    }

    public function zRevPop($zset)
    {
        return $this-&gt;zsetPop($zset, self::POSITION_LAST);
    }

    private function zsetPop($zset, $position)
    {
        $this-&gt;watch($zset);

        $element = $this-&gt;zRange($zset, $position, $position);

        if (!isset($element[0])) {
            return false;
        }

        if ($this-&gt;multi()-&gt;zRem($zset, $element[0])-&gt;exec()) {
            return $element[0];
        }

        return $this-&gt;zsetPop($zset, $position);
    }
}

?&gt;</pre>
<p>模拟实现了POP操作后，我们就可以使用ZSET实现队列了，代码如下：</p>
<pre>&lt;?php

$redis = new RedisClient;
$redis-&gt;connect('/tmp/redis.sock');

$redis-&gt;zAdd('msg', &lt;PRIORITY&gt;, &lt;MSGID&gt;);

while ($msg = $redis-&gt;zRevPop('msg')) {
    var_dump($msg);
}

?&gt;</pre>
<h2>推拉</h2>
<p>以前微博架构中推拉选择的问题已经被大家讨论过很多次了。实际上消息通知系统和微博差不多，也存在推拉选择的问题，同样答案也是类似的，那就是应该推拉结合。具体点说：在登陆用户获取消息的时候，就是一个拉消息的过程；在把消息发送给登陆用户的时候，就是一个推消息的过程。</p>
<h2>速度</h2>
<p>假设要推送一百万条消息的话，那么最直白的实现就是不断的插入，代码如下：</p>
<pre>&lt;?php

for ($msgid = 1; $msgid &lt;= 1000000; $msgid++) {
    $redis-&gt;sAdd('usr:&lt;USRID&gt;:msg', $msgid);
}

?&gt;</pre>
<p>Redis的速度是很快的，但是借助<a href="http://redis.io/topics/pipelining" target="_blank">PIPELINE</a>，会更快，代码如下：</p>
<pre>&lt;?php

for ($i = 1; $i &lt;= 100; $i++) {
    $redis-&gt;multi(Redis::PIPELINE);
    for ($j = 1; $j &lt;= 10000; $j++) {
        $msgid = ($i - 1) * 10000 + $j;
        $redis-&gt;sAdd('usr:&lt;USRID&gt;:msg', $msgid);
    }
    $redis-&gt;exec();
}

?&gt;</pre>
<p>说明：所谓PIPELINE，就是省略了无谓的折返跑，把命令打包给服务端统一处理。</p>
<p>前后两段代码在我的测试里，使用PIPELINE的速度大概是不使用PIPELINE的十倍。</p>
<h2>查询</h2>
<p>我们用Redis命令行来演示一下用户是如何查询消息的。</p>
<p>先插入三条消息，其&lt;MSGID&gt;分别是1，2，3：</p>
<pre>redis&gt; HMSET msg:1 title title1 content content1
redis&gt; HMSET msg:2 title title2 content content2
redis&gt; HMSET msg:3 title title3 content content3</pre>
<p>再把这三条消息发送给某个用户，其&lt;USRID&gt;是123：</p>
<pre>redis&gt; SADD usr:123:msg 1
redis&gt; SADD usr:123:msg 2
redis&gt; SADD usr:123:msg 3</pre>
<p>此时如果简单查询用户有哪些消息的话，无疑只能查到一些&lt;MSGID&gt;：</p>
<pre>redis&gt; SMEMBERS usr:123:msg
1) "1"
2) "2"
3) "3"</pre>
<p>如果还需要用程序根据&lt;MSGID&gt;再来一次查询无疑有点低效，好在Redis内置的<a href="http://www.redis.io/commands/sort" target="_blank">SORT</a>命令可以达到事半功倍的效果，实际上它类似于SQL中的JOIN：</p>
<pre>redis&gt; SORT usr:123:msg GET msg:*-&gt;title
1) "title1"
2) "title2"
3) "title3"
redis&gt; SORT usr:123:msg GET msg:*-&gt;content
1) "content1"
2) "content2"
3) "content3"</pre>
<p>SORT的缺点是它只能GET出字符串类型的数据，如果你想要多个数据，就要多次GET：</p>
<pre>redis&gt; SORT usr:123:msg GET msg:*-&gt;title GET msg:*-&gt;content
1) "title1"
2) "content1"
3) "title2"
4) "content2"
5) "title3"
6) "content3"</pre>
<p>很多情况下这显得不够灵活，好在我们可以采用其他一些方法平衡一下利弊，比如说新加一个字段，冗余保存完整消息的序列化，接着只GET这个字段就OK了。</p>
<p>实际暴露查询接口的时候，不会使用PHP等程序来封装，因为那会成倍降低RPS，推荐使用<a href="http://webd.is/" target="_blank">Webdis</a>，它是一个Redis的Web代理，效率没得说。</p>
<p>&#8230;</p>
<p>最近<a href="https://www.tumblr.com/" target="_blank">Tumblr</a>发表了一篇类似的文章：<a href="http://engineering.tumblr.com/post/7819252942/staircar-redis-powered-notifications" target="_blank">Staircar: Redis-powered notifications</a>，介绍了他们使用Redis实现消息通知系统的一些情况，有兴趣的不妨一起看看。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2012/02/29/146/feed</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>记一次TIME_WAIT网络故障</title>
		<link>http://huoding.com/2012/01/19/142</link>
		<comments>http://huoding.com/2012/01/19/142#comments</comments>
		<pubDate>Thu, 19 Jan 2012 15:46:29 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=142</guid>
		<description><![CDATA[最近发现一个PHP脚本时常出现连不上服务器的现象，调试了一下，发现是TIME_WAIT状态过多造成的，本文简要介绍一下解决问题的过程。 遇到这类问题，我习惯于先用strace命令跟踪了一下看看： shell&#62; strace php /path/to/file EADDRNOTAVAIL (Cannot assign requested address) 从字面结果看似乎是网络资源相关问题。这里顺便介绍一点小技巧：在调试的时候一般是从后往前看strace命令的结果，这样更容易找到有价值的信息。 查看一下当前的网络连接情况，结果发现TIME_WAIT数非常大： shell&#62; netstat -nt &#124; awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' TIME_WAIT 28233 重复了几次测试，结果每次出问题的时候，TIME_WAIT都等于28233，这真是一个魔法数字！实际原因很简单，它取决于一个内核参数net.ipv4.ip_local_port_range： shell&#62; sysctl -a &#124; grep port net.ipv4.ip_local_port_range = 32768 61000 因为端口范围是一个闭区间，所以实际可用的端口数量是： &#8230; <a href="http://huoding.com/2012/01/19/142">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>最近发现一个PHP脚本时常出现连不上服务器的现象，调试了一下，发现是TIME_WAIT状态过多造成的，本文简要介绍一下解决问题的过程。</p>
<p><span id="more-142"></span></p>
<p>遇到这类问题，我习惯于先用strace命令跟踪了一下看看：</p>
<pre>shell&gt; strace php /path/to/file
EADDRNOTAVAIL (Cannot assign requested address)</pre>
<p>从字面结果看似乎是网络资源相关问题。这里顺便介绍一点小技巧：在调试的时候一般是从后往前看strace命令的结果，这样更容易找到有价值的信息。</p>
<p>查看一下当前的网络连接情况，结果发现TIME_WAIT数非常大：</p>
<pre>shell&gt; netstat -nt | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}'
TIME_WAIT 28233</pre>
<p>重复了几次测试，结果每次出问题的时候，TIME_WAIT都等于28233，这真是一个魔法数字！实际原因很简单，它取决于一个内核参数net.ipv4.ip_local_port_range：</p>
<pre>shell&gt; sysctl -a | grep port
net.ipv4.ip_local_port_range = 32768 61000</pre>
<p>因为端口范围是一个闭区间，所以实际可用的端口数量是：</p>
<pre>shell&gt; echo $((61000-32768+1))
28233</pre>
<p>问题分析到这里基本就清晰了，解决方向也明确了，内容所限，这里就不说如何优化程序代码了，只是从系统方面来阐述如何解决问题，无非就是以下两个方面：</p>
<p>首先是增加本地可用端口数量。这点可以用以下命令来实现：</p>
<pre>shell&gt; echo "net.ipv4.ip_local_port_range = 10240 61000" &gt;&gt; /etc/sysctl.conf
shell&gt; sysctl -p</pre>
<p>其次是减少TIME_WAIT连接状态。网络上已经有不少相关的介绍，大多是建议：</p>
<pre>shell&gt; sysctl net.ipv4.tcp_tw_reuse=1
shell&gt; sysctl net.ipv4.tcp_tw_recycle=1</pre>
<p>注：通过sysctl命令修改内核参数，重启后会还原，要想持久化可以参考前面的方法。</p>
<p>这两个选项在降低TIME_WAIT数量方面可以说是立竿见影，不过如果你觉得问题已经完美搞定那就错了，实际上这样可能会引入一个更复杂的网络故障。</p>
<p>关于内核参数的详细介绍，可以参考<a href="http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt" target="_blank">官方文档</a>。我们这里简要说明一下tcp_tw_recycle参数。它用来快速回收TIME_WAIT连接，不过如果在NAT环境下会引发问题。</p>
<p><a href="http://tools.ietf.org/html/rfc1323" target="_blank">RFC1323</a>中有如下一段描述：</p>
<blockquote><p>An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the <a href="http://tools.ietf.org/html/rfc1323#page-17" target="_blank">PAWS</a> mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender&#8217;s timestamp clock. Such an extension is not part of the proposal of this RFC.</p></blockquote>
<p>大概意思是说TCP有一种行为，可以缓存每个连接最新的时间戳，后续请求中如果时间戳小于缓存的时间戳，即视为无效，相应的数据包会被丢弃。</p>
<p>Linux是否启用这种行为取决于tcp_timestamps和tcp_tw_recycle，因为tcp_timestamps缺省就是开启的，所以当tcp_tw_recycle被开启后，实际上这种行为就被激活了。</p>
<p>现在很多公司都用LVS做负载均衡，通常是前面一台LVS，后面多台后端服务器，这其实就是NAT，当请求到达LVS后，它修改地址数据后便转发给后端服务器，但不会修改时间戳数据，对于后端服务器来说，请求的源地址就是LVS的地址，加上端口会复用，所以从后端服务器的角度看，原本不同客户端的请求经过LVS的转发，就可能会被认为是同一个连接，加之不同客户端的时间可能不一致，所以就会出现时间戳错乱的现象，于是后面的数据包就被丢弃了，具体的表现通常是是客户端明明发送的SYN，但服务端就是不响应ACK，还可以通过下面命令来确认数据包不断被丢弃的现象：</p>
<pre>shell&gt; netstat -s | grep timestamp
... packets rejects in established connections because of timestamp</pre>
<p>如果服务器身处NAT环境，安全起见，通常要禁止tcp_tw_recycle，至于TIME_WAIT连接过多的问题，可以通过激活tcp_tw_reuse来缓解。</p>
<p>进一步思考，既然必须同时激活tcp_timestamps和tcp_tw_recycle才会触发这种现象，那只要禁止tcp_timestamps，同时激活tcp_tw_recycle，就可以既避免NAT丢包问题，又降低TIME_WAIT连接数量。如果服务器并不依赖于RFC1323，那么这种方法应该也是可行的，不过最好多做测试，以防有其他的副作用。</p>
<pre>shell&gt; sysctl net.ipv4.tcp_timestamps=0
shell&gt; sysctl net.ipv4.tcp_tw_recycle=1</pre>
<p>&#8230;</p>
<p>总体来说，这次网络故障本身并没什么高深之处，本不想罗罗嗦嗦写这么多，不过拔出萝卜带出泥，在过程中牵扯的方方面面还是值得大家品味的，于是便有了这篇文字。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2012/01/19/142/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>MySQL高可用性大杀器之MHA</title>
		<link>http://huoding.com/2011/12/18/139</link>
		<comments>http://huoding.com/2011/12/18/139#comments</comments>
		<pubDate>Sun, 18 Dec 2011 09:20:43 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Failover]]></category>
		<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=139</guid>
		<description><![CDATA[提到MySQL高可用性，很多人会想到MySQL Cluster，亦或者Heartbeat+DRBD，不过这些方案的复杂性常常让人望而却步，与之相对，利用MySQL复制实现高可用性则显得容易很多，目前大致有MMM，PRM，MHA等方案可供选择：MMM是最常见的方案，可惜它问题太多（What’s wrong with MMM，Problems with MMM for MySQL）；至于PRM，它还是个新项目，暂时不推荐用于产品环境，不过作为Percona的作品，它值得期待；如此看来目前只能选MHA了，好在经过DeNA大规模的实践应用证明它是个靠谱的工具。 安装： 作为前提条件，应先配置MySQL复制，并设置SSH公钥免密码登录。下面以CentOS为例来说明，最好先安装EPEL，不然YUM可能找不到某些软件包。 MHA由Node和Manager组成，Node运行在每一台MySQL服务器上，也就是说，不管是MySQL主服务器，还是MySQL从服务器，都要安装Node，而Manager通常运行在独立的服务器上，但如果硬件资源吃紧，也可以用一台MySQL从服务器来兼职Manager的角色。 安装Node： shell&#62; yum install perl-DBD-MySQL shell&#62; rpm -Uvh http://mysql-master-ha.googlecode.com/files/mha4mysql-node-0.52-0.noarch.rpm 安装Manager： shell&#62; yum install perl-DBD-MySQL shell&#62; yum install perl-Config-Tiny shell&#62; yum install perl-Log-Dispatch shell&#62; yum install perl-Parallel-ForkManager shell&#62; rpm &#8230; <a href="http://huoding.com/2011/12/18/139">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>提到MySQL高可用性，很多人会想到<a href="http://www.mysql.com/products/cluster/" target="_blank">MySQL Cluster</a>，亦或者<a href="http://linux-ha.org/wiki/Heartbeat" target="_blank">Heartbeat</a>+<a href="http://www.drbd.org/" target="_blank">DRBD</a>，不过这些方案的复杂性常常让人望而却步，与之相对，利用MySQL复制实现高可用性则显得容易很多，目前大致有<a href="http://mysql-mmm.org/" target="_blank">MMM</a>，<a href="https://launchpad.net/percona-prm" target="_blank">PRM</a>，<a href="http://code.google.com/p/mysql-master-ha/" target="_blank">MHA</a>等方案可供选择：MMM是最常见的方案，可惜它问题太多（<a href="http://www.xaprb.com/blog/2011/05/04/whats-wrong-with-mmm/" target="_blank">What’s wrong with MMM</a>，<a href="http://code.openark.org/blog/mysql/problems-with-mmm-for-mysql" target="_blank">Problems with MMM for MySQL</a>）；至于PRM，它还是个新项目，暂时不推荐用于产品环境，不过作为<a href="http://www.percona.com/" target="_blank">Percona</a>的作品，它值得期待；如此看来目前只能选MHA了，好在经过<a href="http://dena.jp/" target="_blank">DeNA</a>大规模的实践应用证明它是个靠谱的工具。</p>
<p><span id="more-139"></span></p>
<h2>安装：</h2>
<p>作为前提条件，应先配置<a href="http://huoding.com/2011/04/05/59" target="_blank">MySQL复制</a>，并设置<a href="https://help.ubuntu.com/community/SSH/OpenSSH/Keys" target="_blank">SSH公钥免密码登录</a>。下面以CentOS为例来说明，最好先安装<a href="http://fedoraproject.org/wiki/EPEL" target="_blank">EPEL</a>，不然YUM可能找不到某些软件包。</p>
<p>MHA由Node和Manager组成，Node运行在每一台MySQL服务器上，也就是说，不管是MySQL主服务器，还是MySQL从服务器，都要安装Node，而Manager通常运行在独立的服务器上，但如果硬件资源吃紧，也可以用一台MySQL从服务器来兼职Manager的角色。</p>
<p>安装Node：</p>
<pre>shell&gt; yum install perl-DBD-MySQL
shell&gt; rpm -Uvh http://mysql-master-ha.googlecode.com/files/mha4mysql-node-0.52-0.noarch.rpm</pre>
<p>安装Manager：</p>
<pre>shell&gt; yum install perl-DBD-MySQL
shell&gt; yum install perl-Config-Tiny
shell&gt; yum install perl-Log-Dispatch
shell&gt; yum install perl-Parallel-ForkManager
shell&gt; rpm -Uvh http://mysql-master-ha.googlecode.com/files/mha4mysql-node-0.52-0.noarch.rpm
shell&gt; rpm -Uvh http://mysql-master-ha.googlecode.com/files/mha4mysql-manager-0.52-0.noarch.rpm</pre>
<h2>配置：</h2>
<p>配置全局设置：</p>
<pre>shell&gt; cat /etc/masterha_default.cnf
[server default]
user=...
password=...
ssh_user=...</pre>
<p>配置应用设置：</p>
<pre>shell&gt; cat /etc/masterha_application.cnf
[server_1]
hostname=...

[server_2]
hostname=...</pre>
<p>注：MHA配置文件中参数的详细介绍请参考<a href="http://code.google.com/p/mysql-master-ha/wiki/Parameters" target="_blank">官方文档</a>。</p>
<h2>检查</h2>
<p>检查MySQL复制：</p>
<pre>shell&gt; masterha_check_repl --conf=/etc/masterha_application.cnf</pre>
<p>检查SSH公钥免密码登录：</p>
<pre>shell&gt; masterha_check_ssh --conf=/etc/masterha_application.cnf</pre>
<h2>实战</h2>
<p>首先启动MHA进程：</p>
<pre>shell&gt; masterha_manager --conf=/etc/masterha_application.cnf</pre>
<p>注：视配置情况而定，可能会提示read_only，relay_log_purge等警告信息。</p>
<p>然后检查MHA状态：</p>
<pre>shell&gt; masterha_check_status --conf=/etc/masterha_application.cnf</pre>
<p>注：如果正常，会显示『PING_OK』，否则会显示『NOT_RUNNING』。</p>
<p>到此为止，一个基本的MHA例子就能正常运转了，不过一旦当前的MySQL主服务器发生故障，MHA把某台MySQL从服务器提升为新的MySQL主服务器后，如何通知应用呢？这就需要在配置文件里加上如下两个参数：</p>
<ul>
<li><a href="http://code.google.com/p/mysql-master-ha/wiki/Parameters#master_ip_failover_script" target="_blank">master_ip_failover_script</a></li>
<li><a href="http://code.google.com/p/mysql-master-ha/wiki/Parameters#master_ip_online_change_script" target="_blank">master_ip_online_change_script</a></li>
</ul>
<p>说到Failover，通常有两种方式：一种是虚拟IP地址，一种是全局配置文件。MHA并没有限定使用哪一种方式，而是让用户自己选择，虚拟IP地址的方式会牵扯到其它的软件，这里就不赘述了，以下简单说说全局配置文件，以PHP为实现语言，代码如下：</p>
<pre>#!/usr/bin/env php
&lt;?php
$longopts = array(
    'command:',
    'ssh_user:',
    'orig_master_host:',
    'orig_master_ip:',
    'orig_master_port:',
    'new_master_host::',
    'new_master_ip::',
    'new_master_port::',
);

$options = getopt(null, $longopts);

if ($options['command'] == 'start') {
    $params = array(
        'ip'   =&gt; $options['new_master_ip'],
        'port' =&gt; $options['new_master_port'],
    );

    $string = '&lt;?php return ' . var_export($params, true) . '; ?&gt;';

    file_put_contents('config.php', $string, LOCK_EX);
}

exit(0);
?&gt;</pre>
<p>注：用其它语言实现这个脚本也是OK的，最后别忘了给脚本加上可执行属性。</p>
<p>如果要测试效果的话，可以kill掉当前的MySQL主服务器，稍等片刻，MHA就会把某台MySQL从服务器提升为新的MySQL主服务器，并调用master_ip_failover_script脚本，如上所示，我们在master_ip_failover_script脚本里可以把新的MySQL主服务器的ip和port信息持久化到配置文件里，这样应用就可以使用新的配置了。</p>
<p>有时候需要手动切换MySQL主服务器，可以使用<a href="http://code.google.com/p/mysql-master-ha/wiki/masterha_master_switch" target="_blank">masterha_master_switch</a>命令，不过它调用的不是master_ip_failover_script脚本，而是master_ip_online_change_script脚本，但调用参数类似，脚本可以互用。</p>
<pre>shell&gt; masterha_master_switch --conf=/etc/masterha_application.cnf --master_state=dead --dead_master_host=...
shell&gt; masterha_master_switch --conf=/etc/masterha_application.cnf --master_state=alive --new_master_host=...</pre>
<p>注：针对原来的MySQL主服务器是否已经宕机，执行命令所需的参数有所不同。</p>
<p>需要说明的是，缺省情况下，如果MHA检测到连续发生宕机，且两次宕机时间间隔不足八小时的话，则不会进行Failover，之所以这样限制是为了避免ping-pong效应。不过为了自动化，我们往往希望能取消这种限制，此时可以用如下方式启动Manager：</p>
<pre>shell&gt; nohup masterha_manager --conf=/etc/masterha_application.cnf --ignore_last_failover --remove_dead_master_conf &amp;</pre>
<p>注：请确保Manager的运行用户对masterha_application.cnf有写权限。</p>
<p>&#8230;</p>
<p>本文只是MHA的一个简要介绍，至于详细说明，建议大家阅读<a href="http://code.google.com/p/mysql-master-ha/wiki/TableOfContents" target="_blank">官方文档</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/12/18/139/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Redis高可用性之Failover过渡方案</title>
		<link>http://huoding.com/2011/11/29/136</link>
		<comments>http://huoding.com/2011/11/29/136#comments</comments>
		<pubDate>Tue, 29 Nov 2011 10:27:11 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Failover]]></category>
		<category><![CDATA[Redis]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=136</guid>
		<description><![CDATA[从Redis官方路线图来看，大概会在Redis3.0左右正式支持Cluster。不过即便是乐观的估计，至少也得等几个月的时间，为了让我的应用在这段时间内能保持高可用性，我以主从服务器为基础实现了一个Failover过渡方案。 从理论上解释，一旦主服务器下线，可以在从服务器里挑选出新的主服务器，同时重新设置主从关系，并且当下线服务器重新上线后能自动加入到主从关系中去，内容如下： &#60;?php class RedisFailover { public $config = array(); public $map = array(); const CONFIG_FILE = 'config.php'; const MAP_FILE = 'map.php'; public function __construct() { $config = include self::CONFIG_FILE; foreach ((array)$config as $name =&#62; $nodes) { foreach ($nodes as &#8230; <a href="http://huoding.com/2011/11/29/136">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>从<a href="http://antirez.com/post/short-term-redis-plans.html" target="_blank">Redis官方路线图</a>来看，大概会在Redis3.0左右正式支持<a href="http://redis.io/topics/cluster-spec" target="_blank">Cluster</a>。不过即便是乐观的估计，至少也得等几个月的时间，为了让我的应用在这段时间内能保持高可用性，我以主从服务器为基础实现了一个Failover过渡方案。</p>
<h2><span id="more-136"></span></h2>
<p>从理论上解释，一旦主服务器下线，可以在从服务器里挑选出新的主服务器，同时重新设置主从关系，并且当下线服务器重新上线后能自动加入到主从关系中去，内容如下：</p>
<pre>&lt;?php

class RedisFailover
{
    public $config = array();
    public $map    = array();

    const CONFIG_FILE = 'config.php';
    const MAP_FILE    = 'map.php';

    public function __construct()
    {
        $config = include self::CONFIG_FILE;

        foreach ((array)$config as $name =&gt; $nodes) {
            foreach ($nodes as $node) {
                $node = new RedisNode($node['host'], $node['port']);

                if ($node-&gt;isValid()) {
                    $this-&gt;config[$name][] = $node;
                }
            }

            if (empty($this-&gt;config[$name])) {
                throw new Exception('Invalid config.');
            }

            $this-&gt;map[$name] = $this-&gt;config[$name][0];
        }

        if (file_exists(self::MAP_FILE)) {
            $map = include self::MAP_FILE;

            foreach ((array)$map as $name =&gt; $node) {
                $node = new RedisNode($node['host'], $node['port']);

                $this-&gt;map[$name] = $node;
            }
        }
    }

    public function run()
    {
        $set_nodes_master = function($nodes, $master) {
            foreach ($nodes as $node) {
                $node-&gt;setMaster($master-&gt;host, $master-&gt;port);
            }
        };

        foreach ($this-&gt;config as $name =&gt; $nodes) {
            $is_master_valid = false;

            foreach ($nodes as $node) {
                if ($node == $this-&gt;map[$name]) {
                    $is_master_valid = true;

                    break;
                }
            }

            if ($is_master_valid) {
                $set_nodes_master($nodes, $this-&gt;map[$name]);

                continue;
            }

            foreach ($nodes as $node) {
                $master = $node-&gt;getMaster();

                if (empty($master)) {
                    continue;
                }

                if ($master['master_host'] != $this-&gt;map[$name]-&gt;host) {
                    continue;
                }

                if ($master['master_port'] != $this-&gt;map[$name]-&gt;port) {
                    continue;
                }

                if ($master['master_sync_in_progress']) {
                    continue;
                }

                $node-&gt;clearMaster();

                $set_nodes_master($nodes, $node);

                $this-&gt;map[$name] = $node;

                break;
            }
        }

        $map = array();

        foreach ($this-&gt;map as $name =&gt; $node) {
            $map[$name] = array(
                'host' =&gt; $node-&gt;host, 'port' =&gt; $node-&gt;port
            );
        }

        $content = '&lt;?php return ' . var_export($map, true) . '; ?&gt;';

        file_put_contents(self::MAP_FILE, $content);
    }
}

class RedisNode
{
    public $host;
    public $port;

    const CLI = '/usr/local/bin/redis-cli';

    public function __construct($host, $port)
    {
        $this-&gt;host = $host;
        $this-&gt;port = $port;
    }

    public function setMaster($host, $port)
    {
        if ($this-&gt;host != $host || $this-&gt;port != $port) {
            return $this-&gt;execute("SLAVEOF {$host} {$port}") == 'OK';
        }

        return false;
    }

    public function getMaster()
    {
        $result = array();

        $this-&gt;execute('INFO', $rows);

        foreach ($rows as $row) {
            if (preg_match('/^master_/', $row)) {
                list($key, $value) = explode(':', $row);

                $result[$key] = $value;
            }
        }

        return $result;
    }

    public function clearMaster()
    {
        return $this-&gt;execute('SLAVEOF NO ONE') == 'OK';
    }

    public function isValid()
    {
        return $this-&gt;execute('PING') == 'PONG';
    }

    public function execute($command, &amp;$output = null)
    {
        return exec(
            self::CLI . " -h {$this-&gt;host} -p {$this-&gt;port} {$command}", $output
        );
    }
}

?&gt;</pre>
<p>其中提到了两个文件，先说一下config.php：</p>
<pre>&lt;?php

return array(
    'redis_foo' =&gt; array(
        array('host' =&gt; '192.168.0.1', 'port' =&gt; '6379'),
        array('host' =&gt; '192.168.0.2', 'port' =&gt; '6379'),
        array('host' =&gt; '192.168.0.3', 'port' =&gt; '6379'),
    ),
);

?&gt;</pre>
<p>说明：每个别名对应一组服务器，在这组服务器中，有一个是主服务器，其余都是从服务器，主从关系不要在配置文件里硬编码，而应该通过<a href="http://redis.io/commands/slaveof" target="_blank">SLAVEOF</a>命令动态设定。</p>
<p>再说一下map.php文件，内容如下：</p>
<pre>&lt;?php

return array (
    'redis_foo' =&gt; array (
        'host' =&gt; '192.168.0.1', 'port' =&gt; '6379'
    ),
);

?&gt;</pre>
<p>说明：别名对应的是当前有效的服务器。需要注意的是这个文件是自动生成的！程序在使用Redis的时候，都配置成别名的形式，具体的host，port通过此文件映射获得。</p>
<p>明白了以上代码之后，运行就很简单了：</p>
<pre>&lt;?php

$failover = new RedisFailover();
$failover-&gt;run();

?&gt;</pre>
<p>说明：实际部署时，最严格的方式是以守护进程的方式来执行，不过如果要求不是很苛刻的话，CRON就够了。测试时可以手动杀掉主服务器进程，再通过<a href="http://redis.io/commands/info" target="_blank">INFO</a>查看效果。</p>
<p>再补充一些命令行用法的相关说明，本文都是使用redis-cli来发送命令的，通常这也是最佳选择，不过如果因为某些原因不能使用redis-cli的话，也可以使用<a href="http://linux.die.net/man/1/nc" target="_blank">nc</a>（netcat）命令按照<a href="http://redis.io/topics/protocol" target="_blank">Redis协议</a>实现一个简单的客户端工具，比如说PING命令可以这样实现：</p>
<pre>shell&gt; (echo -en "PING\r\n"; sleep 1) | nc localhost 6379</pre>
<p>说明：之所以需要sleep一下是因为Redis的请求响应机制是<a href="http://redis.io/topics/pipelining" target="_blank">Pipelining</a>方式的。</p>
<p>既然说到这里了，就再唠十块钱儿的，通常，我们可以使用telnet命令和服务交互，但是telnet有一点非常不爽的是命令行不支持上下键历史，还好可以借助rlwrap来达成这个目的，视操作系统，可以很容易的用APT或YUM来安装，运行也很简单：</p>
<pre>shell&gt; rlwrap telnet localhost 6379</pre>
<p>说明：通过使用rlwrap，不仅支持上下键历史，而且连Ctrl+r搜索也一并支持了，强！</p>
<p>&#8230;</p>
<p>在Redis Cluster释出前，希望这个脚本能帮到你，其实其他的服务也可以使用类似的方案，比如MySQL，不过复杂性会加大很多，好在已经有类似<a href="http://code.google.com/p/mysql-master-ha/" target="_blank">MHA</a>之类的方案了。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/11/29/136/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Linux运维利器之ClusterShell</title>
		<link>http://huoding.com/2011/11/12/133</link>
		<comments>http://huoding.com/2011/11/12/133#comments</comments>
		<pubDate>Sat, 12 Nov 2011 14:17:56 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=133</guid>
		<description><![CDATA[如果你有若干台数据库服务器，突然你想知道它们当前的即时负载情况，你会怎么办？挨个登录上去uptime一下？感觉有点傻，写个shell？浪费时间，直接用ClusterShell吧！ ClusterShell的安装与配置 ClusterShell的安装很Easy，如果使用APT或YUM包管理方式的话，基本就是一条命令的事儿，我就不说了，这里说一下如何从源代码安装，需要在源代码目录执行如下命令： shell&#62; python setup.py install 为了使用的方便，还需要拷贝配置文件到指定目录： shell&#62; mkdir /etc/clustershell shell&#62; cp conf/* /etc/clustershell 接着配置我们要管理的节点，假设我们配置了一个db组，包含db_[1-3]三个节点： shell&#62; cat /etc/clustershell/groups db: db_1 db_2 db_3 准备就绪，顺着文章开头的例子说：查询所有数据库服务器当前的负载情况： shell&#62; clush -b -g db "uptime" 注：前提是需要在被操作服务器上设置免密码登录，如果不清楚，请看下面的内容。 番外篇：如何配置服务器免密码登录？ 如果没有事先生成ssh密匙的话，需要先生成： shell&#62; ssh-keygen 可选操作：为了方便，我们可以给需要登录的服务器起一个可读性更好的别名，如果你做了类似的操作，那么后面的&#60;USER&#62;@&#60;SERVER&#62;都可以换成对应的&#60;HOST&#62;： shell&#62; cat ~/.ssh/config Host &#8230; <a href="http://huoding.com/2011/11/12/133">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>如果你有若干台数据库服务器，突然你想知道它们当前的即时负载情况，你会怎么办？挨个登录上去uptime一下？感觉有点傻，写个shell？浪费时间，直接用<a href="http://clustershell.sourceforge.net" target="_blank">ClusterShell</a>吧！</p>
<p><span id="more-133"></span></p>
<h2>ClusterShell的安装与配置</h2>
<p>ClusterShell的安装很Easy，如果使用APT或YUM包管理方式的话，基本就是一条命令的事儿，我就不说了，这里说一下如何从源代码安装，需要在源代码目录执行如下命令：</p>
<pre>shell&gt; python setup.py install</pre>
<p>为了使用的方便，还需要拷贝配置文件到指定目录：</p>
<pre>shell&gt; mkdir /etc/clustershell
shell&gt; cp conf/* /etc/clustershell</pre>
<p>接着配置我们要管理的节点，假设我们配置了一个db组，包含db_[1-3]三个节点：</p>
<pre>shell&gt; cat /etc/clustershell/groups
db: db_1 db_2 db_3</pre>
<p>准备就绪，顺着文章开头的例子说：查询所有数据库服务器当前的负载情况：</p>
<pre>shell&gt; clush -b -g db "uptime"</pre>
<p>注：前提是需要在被操作服务器上设置免密码登录，如果不清楚，请看下面的内容。</p>
<h2>番外篇：如何配置服务器免密码登录？</h2>
<p>如果没有事先生成ssh密匙的话，需要先生成：</p>
<pre>shell&gt; ssh-keygen</pre>
<p>可选操作：为了方便，我们可以给需要登录的服务器起一个可读性更好的别名，如果你做了类似的操作，那么后面的&lt;USER&gt;@&lt;SERVER&gt;都可以换成对应的&lt;HOST&gt;：</p>
<pre>shell&gt; cat ~/.ssh/config
Host db_1
Hostname &lt;SERVER&gt;
User &lt;USER&gt;
Port &lt;PORT&gt;

Host db_2
Hostname &lt;SERVER&gt;
User &lt;USER&gt;
Port &lt;PORT&gt;

Host db_3
Hostname &lt;SERVER&gt;
User &lt;USER&gt;
Port &lt;PORT&gt;</pre>
<p>然后把生成的公钥添加到需要登录的服务器指定位置：</p>
<pre>shell&gt; cat ~/.ssh/id_rsa.pub | ssh &lt;USER&gt;@&lt;SERVER&gt; "cat - &gt;&gt; ~/.ssh/authorized_keys"</pre>
<p>如果你和我一样总记不清如何正确拼写authorized_keys，可以接着学一下ssh-copy-id的用法，这个命令可以让操作更简单点：</p>
<pre>shell&gt; ssh-copy-id -i ~/.ssh/id_rsa.pub "&lt;USER&gt;@&lt;SERVER&gt;"</pre>
<p>注：每配置好一台免密码登录的服务器，最好手动实际操作一下，因为第一次连接会要求手动确认是否保存信息到~/.ssh/known_hosts文件。</p>
<p>&#8230;</p>
<p>有的网友会说，监控服务器负载可以用<a href="http://munin-monitoring.org/" target="_blank">Munin</a>之类的工具。不错确实如此，不过Munin之类的工具无法给你一个及时数据，另外，ClusterShell并不局限在查询负载的功能上，跟上不同的命令，就可以查询不同的数据，而在Munin之类的工具里，如果你想监控某个数据，必须有对应的插件才行。ClusterShell是不可或缺的Linux运维利器！</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/11/12/133/feed</wfw:commentRss>
		<slash:comments>11</slash:comments>
		</item>
		<item>
		<title>OAuth的改变</title>
		<link>http://huoding.com/2011/11/08/126</link>
		<comments>http://huoding.com/2011/11/08/126#comments</comments>
		<pubDate>Tue, 08 Nov 2011 03:56:21 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[OAuth]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=126</guid>
		<description><![CDATA[去年我写过一篇《OAuth那些事儿》，对OAuth做了一些简单扼要的介绍，今天我打算写一些细节，以阐明OAuth如何从1.0改变成1.0a，继而改变成2.0的。 OAuth1.0 在OAuth诞生前，Web安全方面的标准协议只有OpenID，不过它关注的是验证，即WHO的问题，而不是授权，即WHAT的问题。好在FlickrAuth和GoogleAuthSub等私有协议在授权方面做了不少有益的尝试，从而为OAuth的诞生奠定了基础。 OAuth1.0定义了三种角色：User、Service Provider、Consumer。如何理解？假设我们做了一个SNS，它有一个功能，可以让会员把他们在Google上的联系人导入到SNS上，那么此时的会员是User，Google是Service Providere，而SNS则是Consumer。 +----------+ +----------+ &#124; &#124;--(A)- Obtaining a Request Token ---------&#62;&#124; &#124; &#124; &#124; &#124; &#124; &#124; &#124;&#60;-(B)- Request Token ----------------------&#124; &#124; &#124; &#124; (Unauthorized) &#124; &#124; &#124; &#124; &#124; &#124; &#124; &#124; +--------+ &#124; &#124; &#8230; <a href="http://huoding.com/2011/11/08/126">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>去年我写过一篇《<a href="http://huoding.com/2010/10/10/8" target="_blank">OAuth那些事儿</a>》，对OAuth做了一些简单扼要的介绍，今天我打算写一些细节，以阐明OAuth如何从1.0改变成1.0a，继而改变成2.0的。</p>
<p><span id="more-126"></span></p>
<h2>OAuth1.0</h2>
<p>在OAuth诞生前，Web安全方面的标准协议只有OpenID，不过它关注的是验证，即WHO的问题，而不是授权，即WHAT的问题。好在FlickrAuth和GoogleAuthSub等私有协议在授权方面做了不少有益的尝试，从而为OAuth的诞生奠定了基础。</p>
<p><a href="http://oauth.net/core/1.0/" target="_blank">OAuth1.0</a>定义了三种角色：User、Service Provider、Consumer。如何理解？假设我们做了一个SNS，它有一个功能，可以让会员把他们在Google上的联系人导入到SNS上，那么此时的会员是User，Google是Service Providere，而SNS则是Consumer。</p>
<pre> +----------+                                           +----------+
 |          |--(A)- Obtaining a Request Token ---------&gt;|          |
 |          |                                           |          |
 |          |&lt;-(B)- Request Token ----------------------|          |
 |          |       (Unauthorized)                      |          |
 |          |                                           |          |
 |          |      +--------+                           |          |
 |          |&gt;-(C)-|       -+-(C)- Directing ----------&gt;|          |
 |          |      |       -+-(D)- User authenticates -&gt;|          |
 |          |      |        |      +----------+         | Service  |
 | Consumer |      | User-  |      |          |         | Provider |
 |          |      | Agent -+-(D)-&gt;|   User   |         |          |
 |          |      |        |      |          |         |          |
 |          |      |        |      +----------+         |          |
 |          |&lt;-(E)-|       -+-(E)- Request Token ------&lt;|          |
 |          |      +--------+      (Authorized)         |          |
 |          |                                           |          |
 |          |--(F)- Obtaining a Access Token ----------&gt;|          |
 |          |                                           |          |
 |          |&lt;-(G)- Access Token -----------------------|          |
 +----------+                                           +----------+</pre>
<p>花絮：OAuth1.0的RFC没有ASCII流程图，于是我敲了几百下键盘自己画了一个，后经网友提示，Emacs可以很轻松的搞定ASCII图：<a href="http://www.cinsk.org/emacs/emacs-artist.html" target="_blank">Emacs Screencast: Artist Mode</a>，VIM当然也可以搞定，不过要借助一个插件：<a href="http://www.vim.org/scripts/script.php?script_id=40" target="_blank">DrawIt</a>。</p>
<p>Consumer申请Request Token（/oauth/1.0/request_token）：</p>
<pre>oauth_consumer_key
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonce
oauth_version</pre>
<p>Service Provider返回Request Token：</p>
<pre>oauth_token
oauth_token_secret</pre>
<p>Consumer重定向User到Service Provider（/oauth/1.0/authorize）：</p>
<pre>oauth_token
oauth_callback</pre>
<p>Service Provider在用户授权后重定向User到Consumer：</p>
<pre>oauth_token</pre>
<p>Consumer申请Access Token（/oauth/1.0/access_token）：</p>
<pre>oauth_consumer_key
oauth_token
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonce
oauth_version</pre>
<p>Service Provider返回Access Token：</p>
<pre>oauth_token
oauth_token_secret</pre>
<p>&#8230;</p>
<p>注：整个操作流程中，需要注意涉及两种Token，分别是Request Token和Access Token，其中Request Token又涉及两种状态，分别是未授权和已授权。</p>
<h2>OAuth1.0a</h2>
<p>OAuth1.0存在<a href="http://oauth.net/advisories/2009-1/" target="_blank">安全漏洞</a>，详细介绍：<a href="http://hueniverse.com/2009/04/explaining-the-oauth-session-fixation-attack/" target="_blank">Explaining the OAuth Session Fixation Attack</a>，还有这篇：<a href="http://www.readwriteweb.com/archives/how_the_oauth_security_battle_was_won_open_web_sty.php" target="_blank">How the OAuth Security Battle Was Won, Open Web Style</a>。</p>
<p>简单点来说，这是一种会话固化攻击，和常见的会话劫持攻击不同的是，在会话固化攻击中，攻击者会初始化一个合法的会话，然后诱使用户在这个会话上完成后续操作，从而达到攻击的目的。反映到OAuth1.0上，攻击者会先申请Request Token，然后诱使用户授权这个Request Token，接着针对回调地址的使用，又存在以下几种攻击手段：</p>
<ul>
<li>如果Service Provider没有限制回调地址（应用设置没有限定根域名一致），那么攻击者可以把oauth_callback设置成成自己的URL，当User完成授权后，通过这个URL自然就能拿到User的Access Token。</li>
<li>如果Consumer不使用回调地址（桌面或手机程序），而是通过User手动拷贝粘贴Request Token完成授权的话，那么就存在一个竞争关系，只要攻击者在User授权后，抢在User前面发起请求，就能拿到User的Access Token。</li>
</ul>
<p>为了修复安全问题，<a href="http://oauth.net/core/1.0a/" target="_blank">OAuth1.0a</a>出现了（<a href="http://tools.ietf.org/html/rfc5849" target="_blank">RFC5849</a>），主要修改了以下细节：</p>
<ul>
<li>Consumer申请Request Token时，必须传递oauth_callback，而Consumer申请Access Token时，不需要传递oauth_callback。通过前置oauth_callback的传递时机，让oauth_callback参与签名，从而避免攻击者假冒oauth_callback。</li>
<li>Service Provider获得User授权后重定向User到Consumer时，返回oauth_verifier，它会被用在Consumer申请Access Token的过程中。攻击者无法猜测它的值。</li>
</ul>
<p>Consumer申请Request Token（/oauth/1.0a/request_token）：</p>
<pre>oauth_consumer_key
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonce
oauth_version
oauth_callback</pre>
<p>Service Provider返回Request Token：</p>
<pre>oauth_token
oauth_token_secret
oauth_callback_confirmed</pre>
<p>Consumer重定向User到Service Provider（/oauth/1.0a/authorize）：</p>
<pre>oauth_token</pre>
<p>Service Provider在用户授权后重定向User到Consumer：</p>
<pre>oauth_token
oauth_verifier</pre>
<p>Consumer申请Access Token（/oauth/1.0a/access_token）：</p>
<pre>oauth_consumer_key
oauth_token
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonce
oauth_version
oauth_verifier</pre>
<p>Service Provider返回Access Token：</p>
<pre>oauth_token
oauth_token_secret</pre>
<p>注：Service Provider返回Request Token时，附带返回的oauth_callback_confirmed是为了说明Service Provider是否支持OAuth1.0a版本。</p>
<p>&#8230;</p>
<p>签名参数中，oauth_timestamp表示客户端发起请求的时间，如未验证会带来安全问题。</p>
<p>在探讨oauth_timestamp之前，先聊聊oauth_nonce，它是用来防止重放攻击的，Service Provider应该验证唯一性，不过保存所有的oauth_nonce并不现实，所以一般只保存一段时间（比如最近一小时）内的数据。</p>
<p>如果不验证oauth_timestamp，那么一旦攻击者拦截到某个请求后，只要等到限定时间到了，oauth_nonce再次生效后就可以把请求原样重发，签名自然也能通过，完全是一个合法请求，所以说Service Provider必须验证oauth_timestamp和系统时钟的偏差是否在可接受范围内（比如十分钟），如此才能彻底杜绝重放攻击。</p>
<p>&#8230;</p>
<p>需要单独说一下桌面或手机应用应该如何使用OAuth1.0a。此类应用通常没有服务端，无法设置Web形式的oauth_callback地址，此时应该把它设置成oob（out-of-band），当用户选择授权后，Service Provider在页面上显示PIN码（也就是oauth_verifier），并引导用户把它粘贴到应用里完成授权。</p>
<p>一个问题是应用如何打开用户授权页面呢？很容易想到的做法是使用内嵌浏览器，说它是个错误的做法或许有点偏激，但它至少是个对用户不友好的做法，因为一旦浏览器内嵌到程序里，那么用户输入的用户名密码就有被监听的可能；对用户友好的做法应该是打开新窗口，弹出系统默认的浏览器，让用户在可信赖的上下文环境中完成授权流程。</p>
<p>不过这样的方式需要用户在浏览器和应用间手动切换，才能完成授权流程，某种程度上说，影响了用户体验，好在可以通过一些其它的技巧来规避这个问题，其中一个行之有效的办法是Monitor web-browser title-bar，简单点说，操作系统一般提供相应的API可以让应用监听桌面上所有窗口的标题，应用一旦发现某个窗口标题符合预定义格式，就可以认为它是我们要的PIN码，无需用户参与就可以完成授权流程。<a href="http://code.google.com/apis/accounts/docs/OAuth2.html#IA" target="_blank">Google</a>支持这种方式，并且有资料专门描述了细节：<a href="https://sites.google.com/site/oauthgoog/oauth-practices/auto-detecting-approval" target="_blank">Auto-Detecting Approval</a>（注：墙！）。</p>
<p>还有一点需要注意的是对桌面或移动应用来说，consumer_key和consumer_secret通常都是直接保存在应用里的，所以对攻击者而言，理论上可以通过反编译之类的手段解出来。进而通过consumer_key和consumer_secret签名一个伪造的请求，并且在请求中把oauth_callback设置成自己控制的URL，来骗取用户授权。为了屏蔽此类问题，Service Provider需要强制开发者必须预定义回调地址：如果预定义的回调地址是URL方式的，则需要验证请求中的回调地址和预定义的回调地址是否主域名一致；如果预定义的回调地址是oob方式的，则禁止请求以URL的方式回调。</p>
<h2>OAuth2.0</h2>
<p>OAuth1.0虽然在安全性上经过修补已经没有问题了，但还存在其它的缺点，其中最主要的莫过于以下两点：其一，签名逻辑过于复杂，对开发者不够友好；其二，授权流程太过单一，除了Web应用以外，对桌面、移动应用来说不够友好。</p>
<p>为了弥补这些短板，<a href="http://tools.ietf.org/html/draft-ietf-oauth-v2" target="_blank">OAuth2.0</a>做了以下改变：</p>
<p>首先，去掉签名，改用SSL（HTTPS）确保安全性，所有的token不再有对应的secret存在，这也直接导致OAuth2.0不兼容老版本。</p>
<p>其次，针对不同的情况使用不同的授权流程，和老版本只有一种授权流程相比，新版本提供了四种授权流程，可依据客观情况选择。</p>
<p>在详细说明授权流程之前，我们需要先了解一下OAuth2.0中的角色：</p>
<p>OAuth1.0定义了三种角色：User、Service Provider、Consumer。而OAuth2.0则定义了四种角色：Resource Owner、Resource Server、Client、Authorization Server：</p>
<ul>
<li>Resource Owner：User</li>
<li>Resource Server：Service Provider</li>
<li>Client：Consumer</li>
<li>Authorization Server：Service Provider</li>
</ul>
<p>也就是说，OAuth2.0把原本OAuth1.0里的Service Provider角色分拆成Resource Server和Authorization Server两个角色，在授权时交互的是Authorization Server，在请求资源时交互的是Resource Server，当然，有时候他们是合二为一的。</p>
<p>下面我们具体介绍一下OAuth2.0提供的四种授权流程：</p>
<p><strong>Authorization Code</strong></p>
<p>可用范围：此类型可用于有服务端的应用，是最贴近老版本的方式。</p>
<pre> +----------+
 | resource |
 |   owner  |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier      +---------------+
 |         -+----(A)-- &amp; Redirection URI ----&gt;|               |
 |  User-   |                                 | Authorization |
 |  Agent  -+----(B)-- User authenticates ---&gt;|     Server    |
 |          |                                 |               |
 |         -+----(C)-- Authorization Code ---&lt;|               |
 +-|----|---+                                 +---------------+
   |    |                                         ^      v
  (A)  (C)                                        |      |
   |    |                                         |      |
   ^    v                                         |      |
 +---------+                                      |      |
 |         |&gt;---(D)-- Authorization Code ---------'      |
 |  Client |          &amp; Redirection URI                  |
 |         |                                             |
 |         |&lt;---(E)----- Access Token -------------------'
 +---------+       (w/ Optional Refresh Token)</pre>
<p>Client向Authorization Server发出申请（/oauth/2.0/authorize）：</p>
<pre>response_type = code
client_id
redirect_uri
scope
state</pre>
<p>Authorization Server在Resource Owner授权后给Client返回Authorization Code：</p>
<pre>code
state</pre>
<p>Client向Authorization Server发出申请（/oauth/2.0/token）：</p>
<pre>grant_type = authorization_code
code
client_id
client_secret
redirect_uri</pre>
<p>Authorization Server在Resource Owner授权后给Client返回Access Token：</p>
<pre>access_token
token_type
expires_in
refresh_token</pre>
<p>说明：基本流程就是拿Authorization Code换Access Token。</p>
<p><strong>Implicit Grant</strong></p>
<p>可用范围：此类型可用于没有服务端的应用，比如Javascript应用。</p>
<pre> +----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier     +---------------+
 |         -+----(A)-- &amp; Redirection URI ---&gt;|               |
 |  User-   |                                | Authorization |
 |  Agent  -|----(B)-- User authenticates --&gt;|     Server    |
 |          |                                |               |
 |          |&lt;---(C)--- Redirection URI ----&lt;|               |
 |          |          with Access Token     +---------------+
 |          |            in Fragment
 |          |                                +---------------+
 |          |----(D)--- Redirection URI ----&gt;|   Web-Hosted  |
 |          |          without Fragment      |     Client    |
 |          |                                |    Resource   |
 |     (F)  |&lt;---(E)------- Script ---------&lt;|               |
 |          |                                +---------------+
 +-|--------+
   |    |
  (A)  (G) Access Token
   |    |
   ^    v
 +---------+
 |         |
 |  Client |
 |         |
 +---------+</pre>
<p>Client向Authorization Server发出申请（/oauth/2.0/authorize）：</p>
<pre>response_type = token
client_id
redirect_uri
scope
state</pre>
<p>Authorization Server在Resource Owner授权后给Client返回Access Token：</p>
<pre>access_token
token_type
expires_in
scope
state</pre>
<p>说明：没有服务端的应用，其信息只能保存在客户端，如果使用Authorization Code授权方式的话，无法保证client_secret的安全。BTW：不返回Refresh Token。</p>
<p><strong>Resource Owner Password Credentials</strong></p>
<p>可用范围：不管有无服务端，此类型都可用。</p>
<pre> +----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      v
      |    Resource Owner
     (A) Password Credentials
      |
      v
 +---------+                                  +---------------+
 |         |&gt;--(B)---- Resource Owner -------&gt;|               |
 |         |         Password Credentials     | Authorization |
 | Client  |                                  |     Server    |
 |         |&lt;--(C)---- Access Token ---------&lt;|               |
 |         |    (w/ Optional Refresh Token)   |               |
 +---------+                                  +---------------+</pre>
<p>Clien向Authorization Server发出申请（/oauth/2.0/token）：</p>
<pre>grant_type = password
username
password
scope</pre>
<p>AuthorizationServer给Client返回AccessToken：</p>
<pre>access_token
token_type
expires_in
refresh_token</pre>
<p>说明：因为涉及用户名和密码，所以此授权类型仅适用于可信赖的应用。</p>
<p><strong>Client Credentials</strong></p>
<p>可用范围：不管有无服务端，此类型都可用。</p>
<pre> +---------+                                  +---------------+
 |         |                                  |               |
 |         |&gt;--(A)- Client Authentication ---&gt;| Authorization |
 | Client  |                                  |     Server    |
 |         |&lt;--(B)---- Access Token ---------&lt;|               |
 |         |                                  |               |
 +---------+                                  +---------------+</pre>
<p>Client向Authorization Server发出申请（/oauth/2.0/token）：</p>
<pre>grant_type = client_credentials
client_id
client_secret
scope</pre>
<p>Authorization Server给Client返回Access Token：</p>
<pre>access_token
token_type
expires_in</pre>
<p>说明：此授权类型仅适用于获取与用户无关的公共信息。BTW：不返回Refresh Token。</p>
<p>&#8230;</p>
<p>流程中涉及两种Token，分别是Access Token和Refresh Token。通常，Access Token的有效期比较短，而Refresh Token的有效期比较长，如此一来，当Access Token失效的时候，就需要用Refresh Token刷新出有效的Access Token：</p>
<pre> +--------+                                         +---------------+
 |        |--(A)------- Authorization Grant -------&gt;|               |
 |        |                                         |               |
 |        |&lt;-(B)----------- Access Token -----------|               |
 |        |               &amp; Refresh Token           |               |
 |        |                                         |               |
 |        |                            +----------+ |               |
 |        |--(C)---- Access Token ----&gt;|          | |               |
 |        |                            |          | |               |
 |        |&lt;-(D)- Protected Resource --| Resource | | Authorization |
 | Client |                            |  Server  | |     Server    |
 |        |--(E)---- Access Token ----&gt;|          | |               |
 |        |                            |          | |               |
 |        |&lt;-(F)- Invalid Token Error -|          | |               |
 |        |                            +----------+ |               |
 |        |                                         |               |
 |        |--(G)----------- Refresh Token ---------&gt;|               |
 |        |                                         |               |
 |        |&lt;-(H)----------- Access Token -----------|               |
 +--------+           &amp; Optional Refresh Token      +---------------+</pre>
<p>Client向Authorization Server发出申请（/oauth/2.0/token）：</p>
<pre>grant_type = refresh_token
refresh_token
client_id
client_secret
scope</pre>
<p>Authorization Server给Client返回Access Token：</p>
<pre>access_token
expires_in
refresh_token
scope</pre>
<p>&#8230;</p>
<p>不过并不是所有人都对OAuth2.0投赞成票，有空可以看看：<a href="http://www.infoq.com/cn/news/2010/09/oauth2-bad-for-web" target="_blank">OAuth 2.0对Web有害吗？</a></p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/11/08/126/feed</wfw:commentRss>
		<slash:comments>14</slash:comments>
		</item>
		<item>
		<title>如何解决SELinux问题</title>
		<link>http://huoding.com/2011/10/01/119</link>
		<comments>http://huoding.com/2011/10/01/119#comments</comments>
		<pubDate>Sat, 01 Oct 2011 12:58:58 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[SELinux]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=119</guid>
		<description><![CDATA[说起SELinux，多数Linux发行版缺省都激活了它，可见它对系统安全的重要性，可惜由于它本身有一定的复杂性，如果不熟悉的话往往会产生一些看似莫名其妙的问题，导致人们常常放弃使用它，为了不因噎废食，学学如何解决SELinux问题是很有必要的。 我们以CentOS环境为例重现一个非常常见的SELinux问题： 首先需要确认SELinux处于激活状态，可以使用getenforce或sestatus命令： shell&#62; getenforce Enforcing shell&#62; sestatus SELinux status: enabled SELinuxfs mount: /selinux Current mode: enforcing Mode from config file: enforcing Policy version: 24 Policy from config file: targeted 注：关于SELinux的基础知识介绍请参考鸟哥的Linux私房菜中相关的介绍。 我们还需要确认系统已经安装并启动了Apache，没有的话就YUM装一个，这很简单，就不多说了，接着在root目录创建一个测试文件test.html，如下： shell&#62; cat /root/test.html hello, world. 然后把这个测试文件拷贝到Apache的DocumentRoot目录，我的Apache是通过YUM安装的话，缺省是/var/www/html目录，如下： shell&#62; cp &#8230; <a href="http://huoding.com/2011/10/01/119">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>说起<a href="http://wiki.centos.org/HowTos/SELinux" target="_blank">SELinux</a>，多数Linux发行版缺省都激活了它，可见它对系统安全的重要性，可惜由于它本身有一定的复杂性，如果不熟悉的话往往会产生一些看似莫名其妙的问题，导致人们常常放弃使用它，为了不因噎废食，学学如何解决SELinux问题是很有必要的。</p>
<p><span id="more-119"></span>我们以CentOS环境为例重现一个非常常见的SELinux问题：</p>
<p>首先需要确认SELinux处于激活状态，可以使用getenforce或sestatus命令：</p>
<pre>shell&gt; getenforce
Enforcing

shell&gt; sestatus
SELinux status:                 enabled
SELinuxfs mount:                /selinux
Current mode:                   enforcing
Mode from config file:          enforcing
Policy version:                 24
Policy from config file:        targeted</pre>
<p>注：关于SELinux的基础知识介绍请参考<a href="http://linux.vbird.org/linux_basic/0440processcontrol.php#selinux" target="_blank">鸟哥的Linux私房菜</a>中相关的介绍。</p>
<p>我们还需要确认系统已经安装并启动了Apache，没有的话就YUM装一个，这很简单，就不多说了，接着在root目录创建一个测试文件test.html，如下：</p>
<pre>shell&gt; cat /root/test.html
hello, world.</pre>
<p>然后把这个测试文件拷贝到Apache的DocumentRoot目录，我的Apache是通过YUM安装的话，缺省是/var/www/html目录，如下：</p>
<pre>shell&gt; cp /root/test.html /var/www/html</pre>
<p>接着浏览一下，如果没出什么幺蛾子，应该一切都在意料之中，如下：</p>
<pre>shell&gt; curl http://localhost/test.html
hello, world.</pre>
<p>看到这，你可能觉得我废话连篇，别着急，下面就是见证奇迹的时候了：</p>
<p>同样还是那个测试文件test.html，不过这次不再是拷贝，而是移动，如下：</p>
<pre>shell&gt; mv /root/test.html /var/www/html</pre>
<p>接着浏览一下，怎么样，结果很出人意料吧，竟然提示权限错误，如下：</p>
<pre>shell&gt; curl http://localhost/test.html
&lt;!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"&gt;
&lt;html&gt;&lt;head&gt;
&lt;title&gt;403 Forbidden&lt;/title&gt;
&lt;/head&gt;&lt;body&gt;
&lt;h1&gt;Forbidden&lt;/h1&gt;
&lt;p&gt;You don't have permission to access /test.html
on this server.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;</pre>
<p>当然，我们现在知道这个问题是由于SELinux引起的，但还不知其所以然，实际上问题的原因此时已经被audit进程记录到了相应的日志里，可以这样查看：</p>
<pre>shell&gt; audit2why &lt; /var/log/audit/audit.log</pre>
<p>如果看不懂的话，推荐安装setroubleshoot套件：</p>
<pre>shell&gt; yum install setroubleshoot</pre>
<p>它本身是一个GUI套件，不过其中包含的一个sealert命令对我们命令行用户很有用：</p>
<pre>shell&gt; sealert -a /var/log/audit/audit.log
Summary:

SELinux is preventing /usr/sbin/httpd "getattr" access to
/var/www/html/test.html.

Detailed Description:

SELinux denied access requested by httpd. /var/www/html/test.html may be a
mislabeled. /var/www/html/test.html default SELinux type is httpd_sys_content_t,
but its current type is admin_home_t. Changing this file back to the default
type, may fix your problem.

File contexts can be assigned to a file in the following ways.

  * Files created in a directory receive the file context of the parent
    directory by default.
  * The SELinux policy might override the default label inherited from the
    parent directory by specifying a process running in context A which creates
    a file in a directory labeled B will instead create the file with label C.
    An example of this would be the dhcp client running with the dhclient_t type
    and creating a file in the directory /etc. This file would normally receive
    the etc_t type due to parental inheritance but instead the file is labeled
    with the net_conf_t type because the SELinux policy specifies this.
  * Users can change the file context on a file using tools such as chcon, or
    restorecon.

This file could have been mislabeled either by user error, or if an normally
confined application was run under the wrong domain.

However, this might also indicate a bug in SELinux because the file should not
have been labeled with this type.

If you believe this is a bug, please file a bug report against this package.

Allowing Access:

You can restore the default system context to this file by executing the
restorecon command. restorecon '/var/www/html/test.html', if this file is a
directory, you can recursively restore using restorecon -R
'/var/www/html/test.html'.

Fix Command:

/sbin/restorecon '/var/www/html/test.html'</pre>
<p>这次应该看懂了吧！原因是说Apache下文件上下文类型应该是httpd_sys_content_t，但是现在是admin_home_t，所以权限错误，并且在结尾处给出了修复命令。</p>
<p>可httpd_sys_content_t，admin_home_t都怎么看啊？很简单，借助ls命令的-Z参数即可：</p>
<pre>shell&gt; ls -Z /path</pre>
<p>回到问题的开始，拷贝之所以没出现问题，是因为cp自动修改上下文属性，而移动之所以出现问题是因为mv保留原文件的上下文属性。</p>
<p>注：关于SELinux和Apache的详细介绍，可以参考『man httpd_selinux』。</p>
<p>知道了如何解决SELinux问题，以后如果遇到类似的情况不要急着武断的关闭SELinux。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/10/01/119/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Subversion钩子</title>
		<link>http://huoding.com/2011/09/26/116</link>
		<comments>http://huoding.com/2011/09/26/116#comments</comments>
		<pubDate>Mon, 26 Sep 2011 06:12:35 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Subversion]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=116</guid>
		<description><![CDATA[Subversion本身有很好的扩展性，用户可以通过钩子实现一些自定义的功能。 所谓钩子实际上是一种事件机制，当系统执行到某个特殊事件时，会触发我们预定义的动作，这样的特殊事件在Subversion里有很多，默认有如下模板可供选择： shell&#62; ls /path/to/repository/hooks post-commit.tmpl post-lock.tmpl post-revprop-change.tmpl post-unlock.tmpl pre-commit.tmpl pre-lock.tmpl pre-revprop-change.tmpl pre-unlock.tmpl start-commit.tmpl 其中最常用的是pre-commit和post-commit，也就是提交前后的钩子，下面以pre-commit为例来说明一下如何自定义Subversion钩子。 假设有一个PHP项目使用Subversion做版本控制，使用中发现了一些问题，比如程序员不写日志，或者提交的文件有BOM，或者提交的文件有语法错误，或者提交的文件不符合编码规范等等，这些问题都可以利用pre-commit钩子来解决，实际上已经有人写了解决类似问题的工具php-svn-hook，不过我们这里选择自己实现： shell&#62; cat /path/to/repository/hooks/pre-commit #!/bin/bash REPOS="$1" TXN="$2" SVNLOOK="/usr/bin/svnlook" PHP="/usr/bin/php" LOG=$($SVNLOOK log -t "$TXN" "$REPOS") if [ "$LOG" = "" ]; then echo "Please input log" &#8230; <a href="http://huoding.com/2011/09/26/116">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>Subversion本身有很好的扩展性，用户可以通过钩子实现一些自定义的功能。</p>
<p><span id="more-116"></span>所谓钩子实际上是一种事件机制，当系统执行到某个特殊事件时，会触发我们预定义的动作，这样的特殊事件在Subversion里有很多，默认有如下模板可供选择：</p>
<pre>shell&gt; ls /path/to/repository/hooks
post-commit.tmpl
post-lock.tmpl
post-revprop-change.tmpl
post-unlock.tmpl
pre-commit.tmpl
pre-lock.tmpl
pre-revprop-change.tmpl
pre-unlock.tmpl
start-commit.tmpl</pre>
<p>其中最常用的是pre-commit和post-commit，也就是提交前后的钩子，下面以pre-commit为例来说明一下如何自定义Subversion钩子。</p>
<p>假设有一个PHP项目使用Subversion做版本控制，使用中发现了一些问题，比如程序员不写日志，或者提交的文件有<a href="http://en.wikipedia.org/wiki/Byte_order_mark" target="_blank">BOM</a>，或者提交的文件有语法错误，或者提交的文件不符合编码规范等等，这些问题都可以利用pre-commit钩子来解决，实际上已经有人写了解决类似问题的工具<a href="http://jeanmonod.github.com/php-svn-hook/" target="_blank">php-svn-hook</a>，不过我们这里选择自己实现：</p>
<pre>shell&gt; cat /path/to/repository/hooks/pre-commit
#!/bin/bash

REPOS="$1"
TXN="$2"

SVNLOOK="/usr/bin/svnlook"
PHP="/usr/bin/php"

LOG=$($SVNLOOK log -t "$TXN" "$REPOS")

if [ "$LOG" = "" ]; then
      echo "Please input log" 1&gt;&amp;2
      exit 1
fi

FILES=$($SVNLOOK changed -t "$TXN" "$REPOS" | awk '/^[AU]/ {print $NF}')

for FILE in $FILES; do
    CONTENT=$($SVNLOOK cat -t "$TXN" "$REPOS" "$FILE")

    if echo "$CONTENT" | grep -q $'\xEF\xBB\xBF'; then
        echo "Please remove BOM from $FILE" 1&gt;&amp;2
        exit 1
    fi

    if [[ "$FILE" =~ \.(php|html)$ ]]; then
        MESSAGE=$(echo "$CONTENT" | $PHP -l 2&gt;&amp;1)

        if [ $? -ne 0 ]; then
            echo "$MESSAGE" | sed "s/ -/ $FILE/g" 1&gt;&amp;2
            exit 1
        fi
    fi
done

/path/to/PHP_CodeSniffer/scripts/phpcs-svn-pre-commit "$REPOS" -t "$TXN" 1&gt;&amp;2 || exit 1

exit 0</pre>
<p>注：代码里使用<a href="http://pear.php.net/package/PHP_CodeSniffer" target="_blank">PHP_CodeSniffer</a>检查编码规范。</p>
<p>配置好脚本后，一定要记着给脚本加上可执行属性，不然脚本执行后会显示不知所云的错误信息：svn: Commit blocked by pre-commit hook (exit code 255) with no output。</p>
<p>本文以pre-commit为例说明了一下钩子的用法，实际上其他脚本也很有用，比如说如果你想在提交代码后发一条微博，就可以利用post-commit来解决，但是记住不要滥用，比如说非常流行的一种做法是利用post-commit来更新线上程序，但由于整个操作过程不能保证原子性，所以有可能出现问题，解决方法请参考Rasmus的<a href="http://talks.php.net/show/phpcon2011/11" target="_blank">描述</a>，我就不多说了。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/09/26/116/feed</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>玩转PMan</title>
		<link>http://huoding.com/2011/09/07/112</link>
		<comments>http://huoding.com/2011/09/07/112#comments</comments>
		<pubDate>Wed, 07 Sep 2011 09:49:08 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Shell]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=112</guid>
		<description><![CDATA[所谓PMan，指的是PHP Man Pages，可以通过它方便的在命令行上查看PHP文档。它就好比Perl里的PerlDoc，或者Python中的PyDoc，亦或者Ruby里的Ri。 安装 假设你的系统已经存在pear命令了，那么接下来就是一招鲜了： shell&#62; pear install doc.php.net/pman 安装好后使用非常方便，就和Linux下常见的man命令一样的用法： shell&#62; pman strlen 显示效果上也和man命令一样，如下图所示： 扩展 以前我习惯于使用CHM格式的PHP文档，因为它的检索功能很方便，只要记住开头几个字母就能查到想要的内容，可惜坏消息是PMan在这方面比较衰，但是好消息是不用重复发明轮子，bash-completion已经实现了我们想要的大部分功能。 下面以CentOS为例（其它Linux可能有差异），前提是先安装EPEL，然后执行命令： shell&#62; yum install bash-completion 缺省会安装不少现成的bash-completion脚本，可以参考它们实现PMan的对应脚本： shell&#62; cat /usr/share/bash-completion/pman # pman(1) completion have pman &#38;&#38; _pman() { local cur manpath COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" manpath="$(tail -n &#8230; <a href="http://huoding.com/2011/09/07/112">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>所谓<a href="http://php.net/archive/2011.php#id2011-06-25-1" target="_blank">PMan</a>，指的是PHP Man Pages，可以通过它方便的在命令行上查看PHP文档。它就好比Perl里的PerlDoc，或者Python中的PyDoc，亦或者Ruby里的Ri。</p>
<p><span id="more-112"></span></p>
<h2>安装</h2>
<p>假设你的系统已经存在pear命令了，那么接下来就是一招鲜了：</p>
<pre>shell&gt; pear install doc.php.net/pman</pre>
<p>安装好后使用非常方便，就和Linux下常见的man命令一样的用法：</p>
<pre>shell&gt; pman strlen</pre>
<p>显示效果上也和man命令一样，如下图所示：</p>
<div id="attachment_113" class="wp-caption alignnone" style="width: 620px"><a href="http://huoding.com/wp-content/uploads/2011/09/pman_command.png"><img class="size-full wp-image-113" title="PMan Command" src="http://huoding.com/wp-content/uploads/2011/09/pman_command.png" alt="PMan Command" width="610" height="390" /></a><p class="wp-caption-text">PMan Command</p></div>
<h2>扩展</h2>
<p>以前我习惯于使用CHM格式的PHP文档，因为它的检索功能很方便，只要记住开头几个字母就能查到想要的内容，可惜坏消息是PMan在这方面比较衰，但是好消息是不用重复发明轮子，<a href="http://bash-completion.alioth.debian.org/" target="_blank">bash-completion</a>已经实现了我们想要的大部分功能。</p>
<p>下面以CentOS为例（其它Linux可能有差异），前提是先安装<a href="http://fedoraproject.org/wiki/EPEL" target="_blank">EPEL</a>，然后执行命令：</p>
<pre>shell&gt; yum install bash-completion</pre>
<p>缺省会安装不少现成的bash-completion脚本，可以参考它们实现PMan的对应脚本：</p>
<pre>shell&gt; cat /usr/share/bash-completion/pman
# pman(1) completion

have pman &amp;&amp;
_pman()
{
    local cur manpath

    COMPREPLY=()

    cur="${COMP_WORDS[COMP_CWORD]}"

    manpath="$(tail -n 1 $(which pman))"
    manpath="$(echo ${manpath%/*} | awk '{print $NF}')"

    if [ -n "$cur" ]; then
        COMPREPLY=($manpath/man*/$cur*)
    else
        COMPREPLY=($manpath/man*/*)
    fi

    COMPREPLY=(${COMPREPLY[@]##*/})
    COMPREPLY=(${COMPREPLY[@]%.*.*})

    COMPREPLY=($(compgen -W '${COMPREPLY[@]}' -- "$cur"))

    return 0
} &amp;&amp;
complete -F _pman pman</pre>
<p>接着还需要在指定目录做一个软连接以便激活脚本：</p>
<pre>shell&gt; ln -s /usr/share/bash-completion/pman /etc/bash_completion.d/pman</pre>
<p>重新登录后，PMan就拥有按TAB键提示的功能了，如下图所示：</p>
<div id="attachment_114" class="wp-caption alignnone" style="width: 620px"><a href="http://huoding.com/wp-content/uploads/2011/09/pman_bash_completion.png"><img class="size-full wp-image-114" title="PMan Bash Completion" src="http://huoding.com/wp-content/uploads/2011/09/pman_bash_completion.png" alt="PMan Bash Completion" width="610" height="390" /></a><p class="wp-caption-text">PMan Bash Completion</p></div>
<h2>技巧</h2>
<p>VIM是命令行下最常用的编辑器之一，PMan和VIM可以完美结合，在命令模式下键入如下指令，就可以即时显示函数的文档内容：</p>
<pre>:!pman strlen</pre>
<p>实际上还可以更方便些，编辑VIM配置文件，加入<a href="http://vimdoc.sourceforge.net/htmldoc/options.html#%27keywordprg%27" target="_blank">keywordprg</a>设置：</p>
<pre>shell&gt; cat ~/.vimrc
autocmd FileType php setlocal keywordprg=pman</pre>
<p>打开PHP文件后，把光标移动到某个函数下，按大写的<a href="http://vimdoc.sourceforge.net/htmldoc/various.html#K" target="_blank">K</a>键即可查看函数的文档内容。如果想退出文档界面，回到VIM界面，只需按q键。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/09/07/112/feed</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>MongoDB与内存</title>
		<link>http://huoding.com/2011/08/19/107</link>
		<comments>http://huoding.com/2011/08/19/107#comments</comments>
		<pubDate>Fri, 19 Aug 2011 08:02:14 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[MongoDB]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=107</guid>
		<description><![CDATA[但凡初次接触MongoDB的人，无不惊讶于它对内存的贪得无厌，至于个中缘由，我先讲讲Linux是如何管理内存的，再说说MongoDB是如何使用内存的，答案自然就清楚了。 据说带着问题学习更有效，那就先看一个MongoDB服务器的top命令结果： shell&#62; top -p $(pidof mongod) Mem: 32872124k total, 30065320k used, 2806804k free, 245020k buffers Swap: 2097144k total, 100k used, 2097044k free, 26482048k cached VIRT RES SHR %MEM 1892g 21g 21g 69.6 这台MongoDB服务器有没有性能问题？大家可以一边思考一边继续阅读。 先讲讲Linux是如何管理内存的 在Linux里（别的系统也差不多），内存有物理内存和虚拟内存之说，物理内存是什么自然无需解释，虚拟内存实际是物理内存的抽象，多数情况下，出于方便性的考虑，程序访问的都是虚拟内存地址，然后操作系统会通过Page Table机制把它翻译成物理内存地址，详细说明可以参考Understanding Memory和Understanding Virtual Memory，至于程序是如何使用虚拟内存的，可以参考Playing &#8230; <a href="http://huoding.com/2011/08/19/107">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>但凡初次接触MongoDB的人，无不惊讶于它对内存的贪得无厌，至于个中缘由，我先讲讲Linux是如何管理内存的，再说说MongoDB是如何使用内存的，答案自然就清楚了。</p>
<p><span id="more-107"></span></p>
<p>据说带着问题学习更有效，那就先看一个MongoDB服务器的top命令结果：</p>
<pre>shell&gt; top -p $(pidof mongod)
Mem:  32872124k total, 30065320k used,  2806804k free,   245020k buffers
Swap:  2097144k total,      100k used,  2097044k free, 26482048k cached

 VIRT  RES  SHR %MEM
1892g  21g  21g 69.6</pre>
<p>这台MongoDB服务器有没有性能问题？大家可以一边思考一边继续阅读。</p>
<h2>先讲讲Linux是如何管理内存的</h2>
<p>在Linux里（别的系统也差不多），内存有物理内存和<a href="http://en.wikipedia.org/wiki/Virtual_memory" target="_blank">虚拟内存</a>之说，物理内存是什么自然无需解释，虚拟内存实际是物理内存的抽象，多数情况下，出于方便性的考虑，程序访问的都是虚拟内存地址，然后操作系统会通过<a href="http://en.wikipedia.org/wiki/Page_table" target="_blank">Page Table</a>机制把它翻译成物理内存地址，详细说明可以参考<a href="http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/mem.html" target="_blank">Understanding Memory</a>和<a href="http://www.redhat.com/magazine/001nov04/features/vm/" target="_blank">Understanding Virtual Memory</a>，至于程序是如何使用虚拟内存的，可以参考<a href="http://www.snailinaturtleneck.com/blog/2011/08/30/playing-with-virtual-memory/" target="_blank">Playing with Virtual Memory</a>，这里就不多费口舌了。</p>
<p>很多人会把虚拟内存和Swap混为一谈，实际上Swap只是虚拟内存引申出的一种技术而已：操作系统一旦物理内存不足，为了腾出内存空间存放新内容，就会把当前物理内存中的内容放到交换分区里，稍后用到的时候再取回来，需要注意的是，Swap的使用可能会带来性能问题，偶尔为之无需紧张，糟糕的是物理内存和交换分区频繁的发生数据交换，这被称之为Swap颠簸，一旦发生这种情况，先要明确是什么原因造成的，如果是内存不足就好办了，加内存就可以解决，不过有的时候即使内存充足也可能会出现这种问题，比如MySQL就有可能出现这样的情况，一个可选的解决方法是限制使用Swap：</p>
<pre>shell&gt; sysctl -w vm.swappiness=0</pre>
<p>查看内存情况最常用的是free命令：</p>
<pre>shell&gt; free -m
             total       used       free     shared    buffers     cached
Mem:         32101      29377       2723          0        239      25880
-/+ buffers/cache:       3258      28842
Swap:         2047          0       2047</pre>
<p>新手看到used一栏数值偏大，free一栏数值偏小，往往会认为内存要用光了。其实并非如此，之所以这样是因为每当我们操作文件的时候，Linux都会尽可能的把文件缓存到内存里，这样下次访问的时候，就可以直接从内存中取结果，所以cached一栏的数值非常的大，不过不用担心，这部分内存是可回收的，操作系统的虚拟内存管理器会按照<a href="http://en.wikipedia.org/wiki/Cache_algorithms" target="_blank">LRU</a>算法淘汰冷数据。还有一个buffers，也是可回收的，不过它是保留给块设备使用的。</p>
<p>知道了原理，我们就可以推算出系统可用的内存是free + buffers + cached：</p>
<pre>shell&gt; echo $((2723 + 239 + 25880))
28842</pre>
<p>至于系统实际使用的内存是used &#8211; buffers &#8211; cached：</p>
<pre>shell&gt; echo $((29377 - 239 - 25880))
3258</pre>
<p>除了free命令，还可以使用sar命令：</p>
<pre>shell&gt; sar -r
kbmemfree kbmemused  %memused kbbuffers  kbcached
  3224392  29647732     90.19    246116  26070160

shell&gt; sar -W
pswpin/s pswpout/s
    0.00      0.00</pre>
<p>希望你没有被%memused吓到，如果不幸言中，重读本文。</p>
<h2>再说说MongoDB是如何使用内存的</h2>
<p>目前，MongoDB使用的是<a href="http://www.mongodb.org/display/DOCS/Caching" target="_blank">内存映射存储引擎</a>，它会把数据文件映射到内存中，如果是读操作，内存中的数据起到缓存的作用，如果是写操作，内存还可以把随机的写操作转换成顺序的写操作，总之可以大幅度提升性能。MongoDB并不干涉内存管理工作，而是把这些工作留给操作系统的虚拟内存管理器去处理，这样做的好处是简化了MongoDB的工作，但坏处是你没有方法很方便的控制MongoDB占多大内存，幸运的是虚拟内存管理器的存在让我们多数时候并不需要关心这个问题。</p>
<p>MongoDB的内存使用机制让它在缓存重建方面更有优势，简而言之：如果重启进程，那么缓存依然有效，如果重启系统，那么可以通过拷贝数据文件到/dev/null的方式来重建缓存，更详细的描述请参考：<a href="http://blog.mongodb.org/post/10407828262/cache-reheating-not-to-be-ignored" target="_blank">Cache Reheating &#8211; Not to be Ignored</a>。</p>
<p>有时候，即便MongoDB使用的是64位操作系统，也可能会遭遇<a href="http://www.mongodb.org/display/DOCS/Out+of+Memory+OOM+Killer" target="_blank">OOM</a>问题，出现这种情况，多半是因为限制了内存的大小所致，可以这样查看当前值：</p>
<pre>shell&gt; ulimit -a | grep memory</pre>
<p>多数操作系统缺省都是把它设置成unlimited的，如果你的操作系统不是，可以这样修改：</p>
<pre>shell&gt; ulimit -m unlimited
shell&gt; ulimit -v unlimited</pre>
<p>注：ulimit的使用是有上下文的，最好放在MongoDB的启动脚本里。</p>
<p>有时候，MongoDB连接数过多的话，会<a href="http://groups.google.com/group/mongodb-user/browse_thread/thread/18e00ec181f8f377" target="_blank">拖累性能</a>，可以通过<a href="http://www.mongodb.org/display/DOCS/serverStatus+Command" target="_blank">serverStatus</a>查询连接数：</p>
<pre>mongo&gt; db.serverStatus().connections</pre>
<p>每个连接都是一个线程，需要一个Stack，Linux下缺省的Stack设置一般比较大：</p>
<pre>shell&gt; ulimit -a | grep stack
stack size              (kbytes, -s) 10240</pre>
<p>至于MongoDB实际使用的Stack大小，可以用如下命令确认（单位：K）：</p>
<pre>shell&gt; cat /proc/$(pidof mongod)/limits | grep stack | awk -F 'size' '{print int($NF)/1024}'</pre>
<p>如果Stack过大（比如：10240K）的话没有意义，简单对照命令结果中的Size和Rss：</p>
<pre>shell&gt; cat /proc/$(pidof mongod)/smaps | grep 10240 -A 10</pre>
<p>所有连接消耗的内存加起来会相当惊人，推荐把Stack设置小一点，比如说1024：</p>
<pre>shell&gt; ulimit -s 1024</pre>
<p>注：从<a href="https://jira.mongodb.org/browse/SERVER/fixforversion/10390" target="_blank">MongoDB1.8.3</a>开始，MongoDB会在启动时自动设置Stack。</p>
<p>有时候，出于某些原因，你可能想释放掉MongoDB占用的内存，不过前面说了，内存管理工作是由虚拟内存管理器控制的，幸好可以使用MongoDB内置的<a href="http://www.mongodb.org/display/DOCS/List+of+Database+Commands" target="_blank">closeAllDatabases</a>命令达到目的：</p>
<pre>mongo&gt; use admin
mongo&gt; db.runCommand({closeAllDatabases:1})</pre>
<p>另外，通过调整内核参数<a href="http://www.kernel.org/doc/Documentation/sysctl/vm.txt" target="_blank">drop_caches</a>也可以释放缓存：</p>
<pre>shell&gt; sysctl -w vm.drop_caches=1</pre>
<p>平时可以通过mongo命令行来监控MongoDB的内存使用情况，如下所示：</p>
<pre>mongo&gt; db.serverStatus().mem:
{
    "resident" : 22346,
    "virtual" : 1938524,
    "mapped" : 962283
}</pre>
<p>还可以通过<a href="http://www.mongodb.org/display/DOCS/mongostat" target="_blank">mongostat</a>命令来监控MongoDB的内存使用情况，如下所示：</p>
<pre>shell&gt; mongostat
mapped  vsize    res faults
  940g  1893g  21.9g      0</pre>
<p>其中内存相关字段的含义是：</p>
<ul>
<li>mapped：映射到内存的数据大小</li>
<li>visze：占用的虚拟内存大小</li>
<li>res：占用的物理内存大小</li>
</ul>
<p>注：如果操作不能在内存中完成，结果faults列的数值不会是0，视大小可能有性能问题。</p>
<p>在上面的结果中，vsize是mapped的两倍，而mapped等于数据文件的大小，所以说vsize是数据文件的两倍，之所以会这样，是因为本例中，MongoDB开启了<a href="http://www.mongodb.org/display/DOCS/Journaling" target="_blank">journal</a>，需要在内存里多映射一次数据文件，如果关闭journal，则vsize和mapped大致相当。</p>
<p>如果想验证这一点，可以在开启或关闭journal后，通过pmap命令来观察文件映射情况：</p>
<pre>shell&gt; pmap $(pidof mongod)</pre>
<p>到底MongoDB配备多大内存合适？宽泛点来说，多多益善，如果要确切点来说，这实际取决于你的数据及索引的大小，内存如果能够装下全部数据加索引是最佳情况，不过很多时候，数据都会比内存大，比如本文所涉及的MongoDB实例：</p>
<pre>mongo&gt; db.stats()
{
    "dataSize" : 1004862191980,
    "indexSize" : 1335929664
}</pre>
<p>本例中索引只有1G多，内存完全能装下，而数据文件则达到了1T，估计很难找到这么大内存，此时保证内存能装下热数据即可，至于热数据是多少，取决于具体的应用，你也可以通过观察faults的大小来判断当前内存是否能够装下热数据，如果faults持续变大，就说明当前内存已经不能满足热数据的大小了。如此一来内存大小就明确了：内存 &gt; 索引 + 热数据，最好有点富余，毕竟操作系统本身正常运转也需要消耗一部分内存。</p>
<p>关于MongoDB与内存的话题，大家还可以参考<a href="http://www.mongodb.org/display/DOCS/Checking+Server+Memory+Usage" target="_blank">官方文档</a>中的相关介绍。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/08/19/107/feed</wfw:commentRss>
		<slash:comments>13</slash:comments>
		</item>
		<item>
		<title>静态类的原罪</title>
		<link>http://huoding.com/2011/08/14/106</link>
		<comments>http://huoding.com/2011/08/14/106#comments</comments>
		<pubDate>Sun, 14 Aug 2011 14:57:25 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=106</guid>
		<description><![CDATA[黑格尔有句名言：存在即合理。以此为论据的话，静态类的存在自然有其合理性。不过物极必反，一旦代码过于依赖静态类，其劣化的结局则不可避免。这就好比罂粟作为一种草本植物，有其在药理上的价值，但如果肆无忌惮的大量使用，它就变成了毒品。 什么是静态类 所谓静态类指的是无需实例化成对象，直接通过静态方式调用的类。代码如下： &#60;?php class Math { public static function ceil($value) { return ceil($value); } public static function floor($value) { return floor($value); } } ?&#62; 此时类所扮演的角色更像是命名空间，这或许是很多人喜欢使用静态类最直接的原因。 静态类的问题 本质上讲，静态类是面向过程的，因为通常它只是机械的把原本面向过程的代码集合到一起，虽然结果是以类的方式存在，但此时的类更像是一件皇帝的新衣，所以可以说静态类实际上是披着面向对象的皮儿，干着面向过程的事儿。 面向对象的设计原则之一：针对接口编程，而不是针对实现编程。这有什么不同？打个比方来说：抛开价格因素，你喜欢独立显卡的电脑还是集成显卡的电脑？我想绝大多数人会选择独立显卡。独立显卡可以看做是针对接口编程，而集成显卡就就可以看做是针对实现编程。如此说来针对实现编程的弊端就跃然纸上了：它丧失了变化的可能性。 下面杜撰一个文章管理系统的例子来具体说明一下： &#60;?php class Article { public function save() { ArticleDAO::save(); } &#8230; <a href="http://huoding.com/2011/08/14/106">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>黑格尔有句名言：存在即合理。以此为论据的话，静态类的存在自然有其合理性。不过物极必反，一旦代码过于依赖静态类，其劣化的结局则不可避免。这就好比罂粟作为一种草本植物，有其在药理上的价值，但如果肆无忌惮的大量使用，它就变成了毒品。</p>
<p><span id="more-106"></span></p>
<h2>什么是静态类</h2>
<p>所谓静态类指的是无需实例化成对象，直接通过静态方式调用的类。代码如下：</p>
<pre>&lt;?php

class Math
{
    public static function ceil($value)
    {
        return ceil($value);
    }

    public static function floor($value)
    {
        return floor($value);
    }
}

?&gt;</pre>
<p>此时类所扮演的角色更像是命名空间，这或许是很多人喜欢使用静态类最直接的原因。</p>
<h2>静态类的问题</h2>
<p>本质上讲，静态类是面向过程的，因为通常它只是机械的把原本面向过程的代码集合到一起，虽然结果是以类的方式存在，但此时的类更像是一件皇帝的新衣，所以可以说静态类实际上是披着面向对象的皮儿，干着面向过程的事儿。</p>
<p>面向对象的设计原则之一：针对接口编程，而不是针对实现编程。这有什么不同？打个比方来说：抛开价格因素，你喜欢独立显卡的电脑还是集成显卡的电脑？我想绝大多数人会选择独立显卡。独立显卡可以看做是针对接口编程，而集成显卡就就可以看做是针对实现编程。如此说来针对实现编程的弊端就跃然纸上了：它丧失了变化的可能性。</p>
<p>下面杜撰一个文章管理系统的例子来具体说明一下：</p>
<pre>&lt;?php

class Article
{
    public function save()
    {
        ArticleDAO::save();
    }
}

?&gt;</pre>
<p>Article实现必要的领域逻辑，然后把数据持久化交给ArticleDAO去做，而ArticleDAO是一个静态类，就好像焊在主板上的集成显卡一样难以改变，假设我们为了测试代码可能需要<a href="http://en.wikipedia.org/wiki/Mock_object" target="_blank">Mock</a>掉ArticleDAO的实现，但因为调用时使用的是静态类的名字，等同于已经绑定了具体的实现方式，Mock几乎不可能，当然，实际上有一些方法可以实现：</p>
<pre>&lt;?php

class Article
{
    private static $dao;

    public static funciton setDao($dao)
    {
        self::$dao = $dao;
    }

    public static function save()
    {
        $dao = self::$dao;

        $dao::save();
    }
}

?&gt;</pre>
<p>有了变量的介入，可以在运行时设定具体使用哪个静态类：</p>
<pre>&lt;?php

Article::setDao('MockArticleDAO');

Article::save();

?&gt;</pre>
<p>虽然这样的实现方式看似解决了Mock的问题，但是首先它修改的原有的代码，违反了开闭原则，其次它引入了静态变量，而静态变量是共享的状态，有可能会干扰其它代码的执行，所以并不是一个完美的解决方案。</p>
<p>补充说明，利用动态语言的特性，其实可以简单的通过require一个不同的类定义文件来实现Mock，但这样做同样有弊端，设想我们在脚本里需要多次变换实现方式，但实际上我们只有一次require的机会，否则就会出现重复定义的错误。</p>
<p>注：某些情况下，利用<a href="http://www.php.net/manual/en/language.oop5.late-static-bindings.php" target="_blank">静态延迟绑定</a>也可以提高静态类的可测试性，参考<a href="http://sebastian-bergmann.de/archives/883-Stubbing-and-Mocking-Static-Methods.html" target="_blank">PHPUnit</a>。</p>
<h2>对象的价值</h2>
<p>如果放弃静态类，转而使用对象，应该如何实现文章管理系统的例子？代码如下：</p>
<pre>&lt;?php

class Article
{
    private $dao;

    public function __construct($dao)
    {
        $this-&gt;setDao($dao);
    }

    public function setDao($dao)
    {
        $this-&gt;dao = $dao;
    }

    public function save()
    {
        $this-&gt;dao-&gt;save();
    }
}

?&gt;</pre>
<p>实际上，这里用到了人们常说的<a href="http://en.wikipedia.org/wiki/Dependency_injection" target="_blank">依赖注入</a>技术，通过构造器或者Setter注入依赖的对象：</p>
<pre>&lt;?php

$article = new Article(new MockArticleDAO());

$article-&gt;save();

?&gt;</pre>
<p>对象有自己的状态，不会发生共享状态干扰其它代码的执行的情况。</p>
<p>…</p>
<p>当然，静态类有好的一面，比如说很适合实现一些无状态的工具类，但多数时候，我的主观倾向很明确，多用对象，少用静态类，避免系统过早的固化。顺便说一句，希望别有人告诉我静态类比对象快之类的说教，谢谢。</p>
<p>相关链接：<a href="http://kore-nordmann.de/blog/0103_static_considered_harmful.html" target="_blank">static considered harmful</a></p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/08/14/106/feed</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>记一次MongoDB性能问题</title>
		<link>http://huoding.com/2011/08/09/104</link>
		<comments>http://huoding.com/2011/08/09/104#comments</comments>
		<pubDate>Tue, 09 Aug 2011 15:14:37 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[MongoDB]]></category>
		<category><![CDATA[Performance]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=104</guid>
		<description><![CDATA[最近忙着把一个项目从MySQL迁移到MongoDB，在导入旧数据的过程中，遇到了些许波折，犯了不少错误，但同时也学到了不少知识，遂记录下来。 公司为这个项目专门配备了几台高性能务器，清一色的双路四核超线程CPU，外加32G内存，运维人员安装好MongoDB后，就交我手里了，我习惯于在使用新服务器前先看看相关日志，了解一下基本情况，当我浏览MongoDB日志时，发现一些警告信息： WARNING: You are running on a NUMA machine. We suggest launching mongod like this to avoid performance problems: numactl &#8211;interleave=all mongod [other options] 当时我并不太清楚NUMA是什么东西，所以没有处理，只是把问题反馈给了运维人员，后来知道运维人员也没有理会这茬儿，所以问题的序幕就这样拉开了。 迁移工作需要导入旧数据。MongoDB本身有一个mongoimport工具可供使用，不过它只接受json、csv等格式的源文件，不适合我的需求，所以我没用，而是用PHP写了一个脚本，平稳运行了一段时间后，我发现数据导入的速度下降了，同时PHP抛出异常： cursor timed out (timeout: 30000, time left: 0:0, status: 0) 我一时判断不出问题所在，想想先在PHP脚本里加大Timeout的值应付一下： &#60;?php MongoCursor::$timeout &#8230; <a href="http://huoding.com/2011/08/09/104">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>最近忙着把一个项目从MySQL迁移到MongoDB，在导入旧数据的过程中，遇到了些许波折，犯了不少错误，但同时也学到了不少知识，遂记录下来。</p>
<p><span id="more-104"></span></p>
<p>公司为这个项目专门配备了几台高性能务器，清一色的双路四核超线程CPU，外加32G内存，运维人员安装好MongoDB后，就交我手里了，我习惯于在使用新服务器前先看看相关日志，了解一下基本情况，当我浏览MongoDB日志时，发现一些警告信息：</p>
<p>WARNING: You are running on a NUMA machine. We suggest launching mongod like this to avoid performance problems: numactl &#8211;interleave=all mongod [other options]</p>
<p>当时我并不太清楚<a href="http://en.wikipedia.org/wiki/Non-Uniform_Memory_Access" target="_blank">NUMA</a>是什么东西，所以没有处理，只是把问题反馈给了运维人员，后来知道运维人员也没有理会这茬儿，所以问题的序幕就这样拉开了。</p>
<p>迁移工作需要导入旧数据。MongoDB本身有一个<a href="http://www.mongodb.org/display/DOCS/Import+Export+Tools#ImportExportTools-mongoimport" target="_blank">mongoimport</a>工具可供使用，不过它只接受json、csv等格式的源文件，不适合我的需求，所以我没用，而是用PHP写了一个脚本，平稳运行了一段时间后，我发现数据导入的速度下降了，同时PHP抛出异常：</p>
<p>cursor timed out (timeout: 30000, time left: 0:0, status: 0)</p>
<p>我一时判断不出问题所在，想想先在PHP脚本里加大Timeout的值应付一下：</p>
<pre>&lt;?php

MongoCursor::$timeout = -1;

?&gt;</pre>
<p>可惜这样并没有解决问题，错误反倒变着花样的出现了：</p>
<p>max number of retries exhausted, couldn&#8217;t send query, couldn&#8217;t send query: Broken pipe</p>
<p>接着使用strace跟踪了一下PHP脚本，发现进程卡在了recvfrom操作上：</p>
<pre>shell&gt; strace -f -r -p &lt;PID&gt;
recvfrom(&lt;FD&gt;,</pre>
<p>通过如下命令查询recvfrom操作的含义：</p>
<pre>shell&gt; apropos recvfrom
receive a message from a socket</pre>
<p>或者按照下面的方式确认一下：</p>
<pre>shell&gt; lsof -p &lt;PID&gt;
shell&gt; ls -l /proc/&lt;PID&gt;/fd/&lt;FD&gt;</pre>
<p>此时如果查询MongoDB的<a href="http://www.mongodb.org/display/DOCS/Viewing+and+Terminating+Current+Operation" target="_blank">当前操作</a>，会发现几乎每个操作会消耗大量的时间：</p>
<pre>mongo&gt; db.currentOp()</pre>
<p>与此同时，运行<a href="http://www.mongodb.org/display/DOCS/mongostat" target="_blank">mongostat</a>的话，结果会显示很高的locked值。</p>
<p>…</p>
<p>我在网络上找到一篇：<a href="http://blog.zawodny.com/2011/03/06/mongodb-pre-splitting-for-faster-data-loading-and-importing/" target="_blank">MongoDB Pre-Splitting for Faster Data Loading and Importing</a>，看上去和我的问题很类似，不过他的问题实质是由于自动分片导致数据迁移所致，解决方法是使用手动分片，而我并没有使用自动分片，自然不是这个原因。</p>
<p>…</p>
<p>询问了几个朋友，有人反映曾遇到过类似的问题，在他的场景里，问题的主要原因是系统IO操作繁忙时，<a href="http://www.mongodb.org/display/DOCS/Excessive+Disk+Space#ExcessiveDiskSpace-DatafilePreallocation" target="_blank">数据文件预分配</a>堵塞了其它操作，从而导致雪崩效应。</p>
<p>为了验证这种可能，我搜索了一下MongoDB日志：</p>
<pre>shell&gt; grep FileAllocator /path/to/log
[FileAllocator] allocating new datafile ... filling with zeroes...
[FileAllocator] done allocating datafile ... took ... secs</pre>
<p>我使用的文件系统是ext4（xfs也不错 ），创建数据文件非常快，所以不是这个原因，但如果有人使用ext3，可能会遇到这类问题，所以还是大概介绍一下如何解决：</p>
<p>MongoDB按需自动生成数据文件：先是&lt;DB&gt;.0，大小是64M，然后是&lt;DB&gt;.1，大小翻番到128M，到了&lt;DB&gt;.5，大小翻番到2G，其后的数据文件就保持在2G大小。为了避免可能出现的问题，可以采用事先手动创建数据文件的策略：</p>
<pre>#!/bin/sh

DB_NAME=$1

cd /path/to/$DB_NAME

for INDEX_NUMBER in {5..50}; do
    FILE_NAME=$DB_NAME.$INDEX_NUMBER

    if [ ! -e $FILE_NAME ]; then
        head -c 2146435072 /dev/zero &gt; $FILE_NAME
    fi
done</pre>
<p>注：数值2146435072并不是标准的2G，这是INT整数范围决定的。</p>
<p>…</p>
<p>最后一个求助方式就是<a href="http://groups.google.com/group/mongodb-user" target="_blank">官方论坛</a>了，那里的国际友人建议我检查一下是不是索引不佳所致，死马当活马医，我激活了<a href="http://www.mongodb.org/display/DOCS/Database+Profiler" target="_blank">Profiler</a>记录慢操作：</p>
<pre>mongo&gt; use &lt;DB&gt;
mongo&gt; db.setProfilingLevel(1);</pre>
<p>不过结果显示基本都是insert操作（因为我是导入数据为主），本身就不需要索引：</p>
<pre>mongo&gt; use &lt;DB&gt;
mongo&gt; db.system.profile.find().sort({$natural:-1})</pre>
<p>…</p>
<p>问题始终没有得到解决，求人不如求己，我又重复了几次迁移旧数据的过程，结果自然还是老样子，但我发现每当出问题的时候，总有一个名叫<a href="http://irqbalance.org/" target="_blank">irqbalance</a>的进程CPU占用率居高不下，搜索了一下，发现很多介绍irqbalance的文章中都提及了NUMA，让我一下子想起之前在日志中看到的警告信息，我勒个去，竟然绕了这么大一个圈圈！安下心来仔细翻阅文档，发现官方其实已经有了<a href="http://www.mongodb.org/display/DOCS/NUMA" target="_blank">相关介绍</a>，按如下设置搞定：</p>
<pre>shell&gt; echo 0 &gt; /proc/sys/vm/zone_reclaim_mode
shell&gt; numactl --interleave=all mongod [options]</pre>
<p>关于zone_reclaim_mode内核参数的说明，可以参考<a href="http://www.kernel.org/doc/Documentation/sysctl/vm.txt" target="_blank">官方文档</a>。</p>
<p>注：从<a href="https://jira.mongodb.org/browse/SERVER/fixforversion/10380" target="_blank">MongoDB1.9.2</a>开始：MongoDB会在启动时自动设置zone_reclaim_mode。</p>
<p>至于NUMA的含义，简单点说，在有多个物理CPU的架构下，NUMA把内存分为本地和远程，每个物理CPU都有属于自己的本地内存，访问本地内存速度快于访问远程内存，缺省情况下，每个物理CPU只能访问属于自己的本地内存。对于MongoDB这种需要大内存的服务来说就可能造成内存不足，NUMA的详细介绍，可以参考<a href="http://jcole.us/blog/archives/2010/09/28/mysql-swap-insanity-and-the-numa-architecture/" target="_blank">老外的文章</a>。</p>
<p>理论上，MySQL、Redis、Memcached等等都可能会受到NUMA的影响，需要留意。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/08/09/104/feed</wfw:commentRss>
		<slash:comments>11</slash:comments>
		</item>
		<item>
		<title>通过IOStat命令监控IO性能</title>
		<link>http://huoding.com/2011/07/13/91</link>
		<comments>http://huoding.com/2011/07/13/91#comments</comments>
		<pubDate>Wed, 13 Jul 2011 14:01:18 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Performance]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=91</guid>
		<description><![CDATA[网站的很多性能问题最终都会归结到IO头上，所以说理解iostat命令是非常有必要的。 小技巧：你知道iostat是从哪里得到IO相关信息的吗？使用strace命令能跟踪到答案： shell&#62; strace -eopen iostat open("/proc/diskstats", O_RDONLY) 注：Strace教程：5 simple ways to troubleshoot using Strace 注：关于diskstats的说明，参见官方文档（主要是其中的field1 ~ field11部分）。 如果你的操作系统里没有iostat命令的话，除了从源代码安装，还可以使用下面方式： Centos/Fedora的安装方式是：yum install sysstat Debian/Ubuntu的安装方式是：aptitude install sysstat 我最常用的iostat命令格式是：『iostat -dx 1』，意思是每隔一秒显示一次IO扩展信息。 shell&#62; iostat -dx 1 Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s sda &#8230; <a href="http://huoding.com/2011/07/13/91">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>网站的很多性能问题最终都会归结到IO头上，所以说理解iostat命令是非常有必要的。</p>
<p><span id="more-91"></span></p>
<p>小技巧：你知道iostat是从哪里得到IO相关信息的吗？使用strace命令能跟踪到答案：</p>
<pre>shell&gt; strace -eopen iostat
open("/proc/diskstats", O_RDONLY)</pre>
<p>注：Strace教程：<a href="http://www.hokstad.com/5-simple-ways-to-troubleshoot-using-strace.html" target="_blank">5 simple ways to troubleshoot using Strace</a></p>
<p>注：关于diskstats的说明，参见<a href="http://www.kernel.org/doc/Documentation/iostats.txt" target="_blank">官方文档</a>（主要是其中的field1 ~ field11部分）。</p>
<p>如果你的操作系统里没有iostat命令的话，除了从源代码安装，还可以使用下面方式：</p>
<ul>
<li>Centos/Fedora的安装方式是：yum install sysstat</li>
<li>Debian/Ubuntu的安装方式是：aptitude install sysstat</li>
</ul>
<p>我最常用的iostat命令格式是：『iostat -dx 1』，意思是每隔一秒显示一次IO扩展信息。</p>
<pre>shell&gt; iostat -dx 1
Device:         rrqm/s   wrqm/s   r/s   w/s   rsec/s   wsec/s
sda               0.18    37.71  0.65  2.63    50.18   322.08
                avgrq-sz avgqu-sz   await  svctm  %util
                  113.46     0.35  107.49   1.67   0.55

Device:         rrqm/s   wrqm/s   r/s   w/s   rsec/s   wsec/s
sda               0.00  4208.00  0.00 165.00     0.00 163872.00
                avgrq-sz avgqu-sz   await  svctm  %util
                  993.16   119.54 1144.36   6.07 100.10</pre>
<p>注：开头显示的是自系统启动开始的平均值，后面显示的是每段时间间隔里的平均值。</p>
<p>介绍一下相关参数的含义：</p>
<ul>
<li>rrqm/s：队列中每秒钟合并的读请求数量</li>
<li>wrqm/s：队列中每秒钟合并的写请求数量</li>
<li>r/s：每秒钟完成的读请求数量</li>
<li>w/s：每秒钟完成的写请求数量</li>
<li>rsec/s：每秒钟读取的扇区数量</li>
<li>wsec/s：每秒钟写入的扇区数量</li>
<li>avgrq-sz：平均请求扇区的大小</li>
<li>avgqu-sz：平均请求队列的长度</li>
<li>await：平均每次请求的等待时间</li>
<li>svctm：平均每次请求的服务时间</li>
<li>util：设备的利用率</li>
</ul>
<p>注：建议对照<a href="http://www.google.com/codesearch#search/&amp;q=%22iostat:%20report%20CPU%20and%20I/O%20statistics%22%20lang:^c$%20file:iostat.c&amp;type=cs" target="_blank">源代码</a>来记忆这些参数都是如何计算出来的。</p>
<p>关于这些参数，相对重要的是后面几个，具体来说是：util，svctm，await，avgqu-sz：</p>
<p>util是设备的利用率。如果它接近100%，通常说明设备能力趋于饱和（并不绝对）。有时候会出现大于100%的情况，这是因为读取数据的时候是非原子操作。</p>
<p>svctm是平均每次请求的服务时间。从源代码里可以看出：(r/s+w/s)*(svctm/1000)=util。举例子：如果util达到100%，那么此时svctm=1000/(r/s+w/s)，假设IOPS是1000，那么svctm大概在1毫秒左右，如果长时间大于这个数值，说明系统出了问题。</p>
<p>await是平均每次请求的等待时间。这个时间包括了队列时间和服务时间，也就是说，一般情况下，await大于svctm，它们的差值越小，则说明队列时间越短，反之差值越大，队列时间越长，说明系统出了问题。</p>
<p>avgqu-sz是平均请求队列的长度。毫无疑问，队列长度越短越好。</p>
<p>说明：svctm参数在未来某个版本的iostat会被删除，<a href="http://sebastien.godard.pagesperso-orange.fr/man_iostat.html" target="_blank">官方文档</a>是这样描述原因的：</p>
<blockquote><p>The average service time (svctm field) value is meaningless, as I/O statistics are calculated at block level, and we don&#8217;t know when the disk driver starts to process a request. For this reason, this field will be removed in a future sysstat version.</p></blockquote>
<p>另外，有时候iostat会显示一些很离谱的结果，<a href="http://sebastien.godard.pagesperso-orange.fr/faq.html" target="_blank">官方FAQ</a>给出了如下的解释：</p>
<blockquote><p>Because of a Linux kernel bug, iostat -x may display huge I/O response times (svctm) and a bandwidth utilization (%util) of 100% for some devices. Indeed these devices have a value for the field #9 (beginning after the device name) in /proc/{partitions,diskstats} which is always different from 0, and even negative sometimes. Yet this field should go to zero, since it gives the number of I/Os currently in progress (it is incremented as requests are submitted, and decremented as they finish). To (temporarily) solve the problem, you should reboot your system to reset the counters in /proc/{partitions,diskstats}.</p></blockquote>
<p>如果大家想要更系统的了解关于IO的相关知识，可以参考如下资料：</p>
<ul>
<li><a href="http://www.symantec.com/connect/articles/getting-hang-iops" target="_blank">Getting the hang of IOPS</a></li>
<li><a href="http://www.pythian.com/news/247/basic-io-monitoring-on-linux/" target="_blank">Basic I/O Monitoring on Linux</a></li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/07/13/91/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>白话BigPipe</title>
		<link>http://huoding.com/2011/06/26/88</link>
		<comments>http://huoding.com/2011/06/26/88#comments</comments>
		<pubDate>Sun, 26 Jun 2011 13:51:13 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Performance]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=88</guid>
		<description><![CDATA[所谓BigPipe，指的是Facebook开发的用来改善客户端响应速度的技术。本质上讲，其实它并不是新事物，原理上等同于Yahoo在Best Practices for Speeding Up Your Web Site里提出的Flush the Buffer Early，不过BigPipe的实现更灵活，所以有必要了解一二。 我们平常浏览网页时的体验通常是串行的：浏览器发起请求，服务器收到后渲染页面，在此期间，浏览器除了等待别无选择，演示代码如下： &#60;?php sleep(1); $header = 'header'; sleep(1); $content = 'content'; sleep(1); $footer = 'footer'; ?&#62; &#60;html&#62; &#60;head&#62; &#60;title&#62;test&#60;/title&#62; &#60;/head&#62; &#60;body&#62; &#60;div id="header"&#62;&#60;?php echo $header; ?&#62;&#60;/div&#62; &#60;div id="content"&#62;&#60;?php echo $content; &#8230; <a href="http://huoding.com/2011/06/26/88">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>所谓BigPipe，指的是<a href="http://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919" target="_blank">Facebook</a>开发的用来改善客户端响应速度的技术。本质上讲，其实它并不是新事物，原理上等同于Yahoo在<a href="http://developer.yahoo.com/performance/rules.html" target="_blank">Best Practices for Speeding Up Your Web Site</a>里提出的Flush the Buffer Early，不过BigPipe的实现更灵活，所以有必要了解一二。</p>
<p><span id="more-88"></span></p>
<p>我们平常浏览网页时的体验通常是串行的：浏览器发起请求，服务器收到后渲染页面，在此期间，浏览器除了等待别无选择，演示代码如下：</p>
<pre>&lt;?php
sleep(1);
$header = 'header';

sleep(1);
$content = 'content';

sleep(1);
$footer = 'footer';
?&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;test&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div id="header"&gt;&lt;?php echo $header; ?&gt;&lt;/div&gt;

&lt;div id="content"&gt;&lt;?php echo $content; ?&gt;&lt;/div&gt;

&lt;div id="footer"&gt;&lt;?php echo $footer; ?&gt;&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</pre>
<p>注：代码里用sleep模拟服务端耗时的操作。</p>
<p>如果我们把串行改成并行的方式呢？每当服务器生成新的内容立刻发送给浏览器，浏览器立刻渲染，不必等到接收到全部数据再处理，毫无疑问会提升用户体验，代码如下：</p>
<p>提醒一下，代码最后运行在Apache + Mod PHP环境，旧版本Apache可能需要关闭GZip。如果是Nginx + PHP FastCGI环境，因为<a href="http://wiki.nginx.org/HttpFcgiModule#fastcgi_buffers" target="_blank">fastcgi_buffers</a>的存在，运行效果会非你所愿。</p>
<pre>&lt;html&gt;
&lt;head&gt;
&lt;title&gt;test&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;?php sleep(1); ?&gt;
&lt;div id="header"&gt;&lt;?php echo str_pad('header', 1024); ?&gt;&lt;/div&gt;
&lt;?php ob_flush(); flush(); ?&gt;

&lt;?php sleep(1); ?&gt;
&lt;div id="content"&gt;&lt;?php echo str_pad('content', 1024); ?&gt;&lt;/div&gt;
&lt;?php ob_flush(); flush(); ?&gt;

&lt;?php sleep(1); ?&gt;
&lt;div id="footer"&gt;&lt;?php echo str_pad('footer', 1024); ?&gt;&lt;/div&gt;
&lt;?php ob_flush(); flush(); ?&gt;

&lt;/body&gt;
&lt;/html&gt;</pre>
<p>注：某些浏览器必须接收到一定长度的内容才开始渲染，所以代码里用到了<a href="http://www.php.net/str_pad" target="_blank">str_pad</a>。</p>
<p>代码里用到<a href="http://www.php.net/ob_flush" target="_blank">ob_flush</a>和<a href="http://www.php.net/flush" target="_blank">flush</a>把页面分块刷新缓存到浏览器，此时如果使用Firebug查看响应头的话，会发现：<a href="http://en.wikipedia.org/wiki/Chunked_transfer_encoding" target="_blank">Transfer-Encoding=chunked</a>，如此一来浏览器就可以分块渲染了。</p>
<p>BigPipe在此基础上更进一步，演示代码如下：</p>
<pre>&lt;html&gt;
&lt;head&gt;
&lt;title&gt;test&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div id="header"&gt;&lt;/div&gt;

&lt;div id="content"&gt;&lt;/div&gt;

&lt;div id="footer"&gt;&lt;/div&gt;

&lt;?php ob_flush(); flush(); ?&gt;

&lt;?php sleep(1); $header = str_pad('header', 1024); ?&gt;
&lt;script&gt;
document.getElementById("header").innerHTML = "&lt;?php echo $header; ?&gt;";
&lt;/script&gt;
&lt;?php ob_flush(); flush(); ?&gt;

&lt;?php sleep(1); $content = str_pad('content', 1024); ?&gt;
&lt;script&gt;
document.getElementById("content").innerHTML = "&lt;?php echo $content; ?&gt;";
&lt;/script&gt;
&lt;?php ob_flush(); flush(); ?&gt;

&lt;?php sleep(1); $footer = str_pad('footer', 1024); ?&gt;
&lt;script&gt;
document.getElementById("footer").innerHTML = "&lt;?php echo $footer; ?&gt;";
&lt;/script&gt;
&lt;?php ob_flush(); flush(); ?&gt;

&lt;/body&gt;
&lt;/html&gt;</pre>
<p>使用BigPipe，先刷新布局（Layout），然后按块（header，content，footer）刷新相应的Javascript代码，从而实现页面内容的填充。</p>
<p>BigPipe之所以使用Javascript渲染页面，是因为这样一来渲染页面的时候，就不会被块的位置束缚住，如果我们的服务器支持多线程，那么就可以同时处理多块内容，哪块先处理好就把哪块刷新到浏览器，即便不支持多线程，服务器也可以按照内容的重要程度分主次先后渲染，不必拘泥于HTML代码的物理顺序。</p>
<p>此外还应注意一下BigPipe和Ajax二者的区别，对于一个分成若干个块的页面而言，如果使用Ajax的话，每一块都需要单独发送一个HTTP请求，而如果使用BigPipe的话，不管有多少块，都仅有一个HTTP请求。所以Ajax对服务器造成的压力会是BigPipe的若干倍。</p>
<p>注：BigPipe不利于SEO，应用时可通过User Agent判断请求是人还是搜索引擎，如果是人的话，则应用BigPipe渲染模式，如果是搜索引擎的话，则应用传统渲染模式。</p>
<p>参考：<a href="http://velocity.oreilly.com.cn/ppts/ChanghaoJiang.pdf" target="_blank">Facebook网站的Ajax化、缓存和流水线</a>（PDF）。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/06/26/88/feed</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>优化InnerHTML操作</title>
		<link>http://huoding.com/2011/06/19/87</link>
		<comments>http://huoding.com/2011/06/19/87#comments</comments>
		<pubDate>Sun, 19 Jun 2011 07:00:22 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[Performance]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=87</guid>
		<description><![CDATA[多数现代浏览器都实现了innerHTML操作，它的方便性让我们爱不释手，但如果使用不当，很容易出现效率问题，本文通过一个例子来说明如何优化innerHTML操作。 例子：我们要实现的效果是当用户点击鼠标的时候，就在旧数据上追加若干新数据。 如果使用标准DOM的话，完整代码如下： &#60;html&#62; &#60;head&#62; &#60;title&#62;test&#60;/title&#62; &#60;/head&#62; &#60;body&#62; &#60;div&#62; &#60;p&#62;data&#60;p&#62; &#60;/div&#62; &#60;script&#62; document.onmousedown = function() { for (var i = 0; i &#60; 10; i++) { var p = document.createElement("p"); p.appendChild(document.createTextNode(Math.random())); document.getElementsByTagName('div')[0].appendChild(p); } }; &#60;/script&#62; &#60;/body&#62; &#60;/html&#62; 注：一旦结构比较复杂的话，标准DOM需要编写冗长的代码。 如果使用innerHTML的话，部分代码如下： &#60;script&#62; &#8230; <a href="http://huoding.com/2011/06/19/87">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>多数现代浏览器都实现了innerHTML操作，它的方便性让我们爱不释手，但如果使用不当，很容易出现效率问题，本文通过一个例子来说明如何优化innerHTML操作。</p>
<p><span id="more-87"></span></p>
<p>例子：我们要实现的效果是当用户点击鼠标的时候，就在旧数据上追加若干新数据。</p>
<p>如果使用标准DOM的话，完整代码如下：</p>
<pre>&lt;html&gt;
&lt;head&gt;
&lt;title&gt;test&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div&gt;
&lt;p&gt;data&lt;p&gt;
&lt;/div&gt;

&lt;script&gt;
document.onmousedown = function() {
    for (var i = 0; i &lt; 10; i++) {
        var p = document.createElement("p");
        p.appendChild(document.createTextNode(Math.random()));

        document.getElementsByTagName('div')[0].appendChild(p);
    }
};
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<p>注：一旦结构比较复杂的话，标准DOM需要编写冗长的代码。</p>
<p>如果使用innerHTML的话，部分代码如下：</p>
<pre>&lt;script&gt;
document.onmousedown = function() {
    var html = "";

    for (var i = 0; i &lt; 10; i++) {
        html += "&lt;p&gt;" + Math.random() + "&lt;p&gt;";
    }

    document.getElementsByTagName('div')[0].innerHTML += html;
};
&lt;/script&gt;</pre>
<p>注：innerHTML没有标准DOM中的appendChild，所以使用了『+=』的方式，效率低下。</p>
<p>我们可以结合使用innerHTML和标准DOM，这样二者的优点就兼得了，部分代码如下：</p>
<pre>&lt;script&gt;
document.onmousedown = function() {
    var html = "";

    for (var i = 0; i &lt; 10; i++) {
        html += "&lt;p&gt;" + Math.random() + "&lt;p&gt;";
    }

    var temp = document.createElement("div");
    temp.innerHTML = html;

    while (temp.firstChild) {
        document.getElementsByTagName('div')[0].appendChild(temp.firstChild);
    }
};
&lt;/script&gt;</pre>
<p>注：创建一个元素，然后注入innerHTML，接着在元素上使用标准DOM操作。</p>
<p>还不算完，<a href="http://james.padolsey.com/javascript/asynchronous-innerhtml/" target="_blank">Asynchronous innerHTML</a>给出了更强悍的解决方法，部分代码如下：</p>
<pre>&lt;script&gt;
document.onmousedown = function() {
    var html = "";

    for (var i = 0; i &lt; 10; i++) {
        html += "&lt;p&gt;" + Math.random() + "&lt;p&gt;";
    }

    var temp = document.createElement('div');

    temp.innerHTML = html;

    var frag = document.createDocumentFragment();

    (function() {
        if (temp.firstChild) {
            frag.appendChild(temp.firstChild);
            setTimeout(arguments.callee, 0);
        } else {
            document.getElementsByTagName('div')[0].appendChild(frag);
        }
    })();
};
&lt;/script&gt;</pre>
<p>注：使用setTimeout防止堵塞浏览器，使用<a href="https://developer.mozilla.org/en/DOM/DocumentFragment" target="_blank">DocumentFragment</a>减少渲染次数。</p>
<p>另：代码在拼接字符串时还可以更快，详见：<a href="http://james.padolsey.com/javascript/fastest-way-to-build-an-html-string/" target="_blank">Fastest way to build an HTML string</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/06/19/87/feed</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>正确重置MySQL密码</title>
		<link>http://huoding.com/2011/06/12/85</link>
		<comments>http://huoding.com/2011/06/12/85#comments</comments>
		<pubDate>Sun, 12 Jun 2011 07:37:22 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=85</guid>
		<description><![CDATA[谁都不想弄丢家门钥匙，但不管多么小心，时间长了，这样的事情总会发生几次。MySQL密码也是一样，把它写在文档上不太安全，记在脑子里又难免会忘记。 如果你忘记了MySQL密码，如何重置它呢？ 下面是错误答案： 首先停止MySQL服务，然后使用skip-grant-tables参数启动它： shell&#62; /etc/init.d/mysql stop shell&#62; mysqld_safe --skip-grant-tables &#38; 此时无需授权就可以进入到MySQL命令行，使用SQL重置MySQL密码： UPDATE mysql.user SET Password=PASSWORD('...') WHERE User='...' AND Host= '...'; FLUSH PRIVILEGES; 为什么说它是错误答案？因为在单纯使用skip-grant-tables参数启动服务后，除非数据库服务器屏蔽了外网访问，否则除了自己，其它别有用心的人也可能访问数据库，尽管重置密码所需的时间很短，但俗话说不怕贼偷就怕贼惦记着，任何纰漏都可能酿成大祸。 下面是正确答案： 关键点是：在使用skip-grant-tables参数的同时，还要加上skip-networking参数： shell&#62; mysqld_safe --skip-grant-tables --skip-networking &#38; 接着使用SQL重置密码后，记得去掉skip-networking，以正常方式重启MySQL服务： shell&#62; /etc/init.d/mysqld restart 上面的方法需要重启两次服务，实际上还能更优雅一点，重启一次即可： 首先需要把用到的SQL语句保存到一个文本文件里（/path/to/init/file）： UPDATE mysql.user SET &#8230; <a href="http://huoding.com/2011/06/12/85">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>谁都不想弄丢家门钥匙，但不管多么小心，时间长了，这样的事情总会发生几次。MySQL密码也是一样，把它写在文档上不太安全，记在脑子里又难免会忘记。</p>
<p><span id="more-85"></span></p>
<p>如果你忘记了MySQL密码，如何重置它呢？</p>
<p>下面是<strong>错误</strong>答案：</p>
<p>首先停止MySQL服务，然后使用<a href="http://dev.mysql.com/doc/refman/5.5/en/server-options.html#option_mysqld_skip-grant-tables" target="_blank">skip-grant-tables</a>参数启动它：</p>
<pre>shell&gt; /etc/init.d/mysql stop
shell&gt; mysqld_safe --skip-grant-tables &amp;</pre>
<p>此时无需授权就可以进入到MySQL命令行，使用SQL重置MySQL密码：</p>
<pre>UPDATE mysql.user SET Password=PASSWORD('...') WHERE User='...' AND Host= '...';
FLUSH PRIVILEGES;</pre>
<p>为什么说它是错误答案？因为在单纯使用skip-grant-tables参数启动服务后，除非数据库服务器屏蔽了外网访问，否则除了自己，其它别有用心的人也可能访问数据库，尽管重置密码所需的时间很短，但俗话说不怕贼偷就怕贼惦记着，任何纰漏都可能酿成大祸。</p>
<p>下面是<strong>正确</strong>答案：</p>
<p>关键点是：在使用skip-grant-tables参数的同时，还要加上<a href="http://dev.mysql.com/doc/refman/5.5/en/server-options.html#option_mysqld_skip-networking" target="_blank">skip-networking</a>参数：</p>
<pre>shell&gt; mysqld_safe --skip-grant-tables --skip-networking &amp;</pre>
<p>接着使用SQL重置密码后，记得去掉skip-networking，以正常方式重启MySQL服务：</p>
<pre>shell&gt; /etc/init.d/mysqld restart</pre>
<p>上面的方法需要重启两次服务，实际上还能更优雅一点，重启一次即可：</p>
<p>首先需要把用到的SQL语句保存到一个文本文件里（/path/to/init/file）：</p>
<pre>UPDATE mysql.user SET Password=PASSWORD('...') WHERE User='...' AND Host= '...';
FLUSH PRIVILEGES;</pre>
<p>接着使用<a href="http://dev.mysql.com/doc/refman/5.5/en/server-options.html#option_mysqld_init-file" target="_blank">init-file</a>参数启动MySQL服务，</p>
<pre>shell&gt; /etc/init.d/mysql stop
shell&gt; mysqld_safe --init-file=/path/to/init/file &amp;</pre>
<p>此时，密码就已经重置了，最后别忘了删除文件内容，免得泄露密码。</p>
<p>提示：本文用到的参数都是通过命令行mysqld_safe传递的，实际上也可以通过my.cnf。</p>
<p>参考：关于重置密码，官方文档里有专门的描述：<a href="http://dev.mysql.com/doc/refman/5.5/en/resetting-permissions.html" target="_blank">How to Reset the Root Password</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/06/12/85/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>MySQL和MongoDB设计实例对比</title>
		<link>http://huoding.com/2011/06/08/84</link>
		<comments>http://huoding.com/2011/06/08/84#comments</comments>
		<pubDate>Wed, 08 Jun 2011 14:51:59 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[MongoDB]]></category>
		<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=84</guid>
		<description><![CDATA[MySQL是关系型数据库中的明星，MongoDB是文档型数据库中的翘楚。下面通过一个设计实例对比一下二者：假设我们正在维护一个手机产品库，里面除了包含手机的名称，品牌等基本信息，还包含了待机时间，外观设计等参数信息，应该如何存取数据呢？ 如果使用MySQL的话，应该如何存取数据呢？ 如果使用MySQL话，手机的基本信息单独是一个表，另外由于不同手机的参数信息差异很大，所以还需要一个参数表来单独保存。 CREATE TABLE IF NOT EXISTS `mobiles` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `brand` VARCHAR(100) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE IF NOT EXISTS `mobile_params` ( `id` int(10) unsigned NOT NULL &#8230; <a href="http://huoding.com/2011/06/08/84">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>MySQL是关系型数据库中的明星，MongoDB是文档型数据库中的翘楚。下面通过一个设计实例对比一下二者：假设我们正在维护一个手机产品库，里面除了包含手机的名称，品牌等基本信息，还包含了待机时间，外观设计等参数信息，应该如何存取数据呢？</p>
<p><span id="more-84"></span></p>
<h2>如果使用MySQL的话，应该如何存取数据呢？</h2>
<p>如果使用MySQL话，手机的基本信息单独是一个表，另外由于不同手机的参数信息差异很大，所以还需要一个参数表来单独保存。</p>
<pre>CREATE TABLE IF NOT EXISTS `mobiles` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(100) NOT NULL,
    `brand` VARCHAR(100) NOT NULL,
    PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS `mobile_params` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `mobile_id` int(10) unsigned NOT NULL,
    `name` varchar(100) NOT NULL,
    `value` varchar(100) NOT NULL,
    PRIMARY KEY (`id`)
);

INSERT INTO `mobiles` (`id`, `name`, `brand`) VALUES
(1, 'ME525', '摩托罗拉'),
(2, 'E7'   , '诺基亚');

INSERT INTO `mobile_params` (`id`, `mobile_id`, `name`, `value`) VALUES
(1, 1, '待机时间', '200'),
(2, 1, '外观设计', '直板'),
(3, 2, '待机时间', '500'),
(4, 2, '外观设计', '滑盖');</pre>
<p>注：为了演示方便，没有严格遵守关系型数据库的范式设计。</p>
<p>如果想查询待机时间大于100小时，并且外观设计是直板的手机，需按照如下方式查询：</p>
<pre>SELECT * FROM `mobile_params` WHERE name = '待机时间' AND value &gt; 100;
SELECT * FROM `mobile_params` WHERE name = '外观设计' AND value = '直板';</pre>
<p>注：参数表为了方便，把数值和字符串统一保存成字符串，实际使用时，MySQL允许在字符串类型的字段上进行数值类型的查询，只是需要进行类型转换，多少会影响一点性能。</p>
<p>两条SQL的结果取交集得到想要的MOBILE_IDS，再到mobiles表查询即可：</p>
<pre>SELECT * FROM `mobiles` WHERE mobile_id IN (MOBILE_IDS)</pre>
<h2>如果使用MongoDB的话，应该如何存取数据呢？</h2>
<p>如果使用MongoDB的话，虽然理论上可以采用和MySQL一样的设计方案，但那样的话就显得无趣了，没有发挥出MongoDB作为文档型数据库的优点，实际上使用MongoDB的话，和MySQL相比，形象一点来说，可以合二为一：</p>
<pre>db.getCollection("mobiles").ensureIndex({
    "params.name": 1,
    "params.value": 1
});

db.getCollection("mobiles").insert({
    "_id": 1,
    "name": "ME525",
    "brand": "摩托罗拉",
    "params": [
        {"name": "待机时间", "value": 200},
        {"name": "外观设计", "value": "直板"}
    ]
});

db.getCollection("mobiles").insert({
    "_id": 2,
    "name": "E7",
    "brand": "诺基亚",
    "params": [
        {"name": "待机时间", "value": 500},
        {"name": "外观设计", "value": "滑盖"}
    ]
});</pre>
<p>如果想查询待机时间大于100小时，并且外观设计是直板的手机，需按照如下方式查询：</p>
<pre>db.getCollection("mobiles").find({
    "params": {
        $all: [
            {$elemMatch: {"name": "待机时间", "value": {$gt: 100}}},
            {$elemMatch: {"name": "外观设计", "value": "直板"}}
        ]
    }
});</pre>
<p>注：查询中用到的<a href="http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24all" target="_blank">$all</a>，<a href="http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24elemMatch" target="_blank">$elemMatch</a>等高级用法的详细介绍请参考官方文档中相关说明。</p>
<p>MySQL需要多个表，多次查询才能搞定的问题，MongoDB只需要一个表，一次查询就能搞定，对比完成，相对MySQL而言，MongoDB显得更胜一筹，至少本例如此 <img src='http://huoding.com/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' /> </p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/06/08/84/feed</wfw:commentRss>
		<slash:comments>12</slash:comments>
		</item>
		<item>
		<title>实例演示SimpleXMLElement的用法</title>
		<link>http://huoding.com/2011/05/29/81</link>
		<comments>http://huoding.com/2011/05/29/81#comments</comments>
		<pubDate>Sun, 29 May 2011 12:39:23 +0000</pubDate>
		<dc:creator>老王</dc:creator>
				<category><![CDATA[Technical]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[XML]]></category>

		<guid isPermaLink="false">http://huoding.com/?p=81</guid>
		<description><![CDATA[使用PHP解析XML时，常用simplexml_load_string，缺省是一个SimpleXMLElement的包装函数，今天不说simplexml_load_string，只说SimpleXMLElement。 本文以Android软件中的AndroidManifest.xml文档为例，先看一下演示文档的内容： &#60;?xml version="1.0" encoding="utf-8"?&#62; &#60;manifest xmlns:android="http://schemas.android.com/apk/res/android" package="PACKAGE" android:versionName="VERSIONNAME"&#62; &#60;application android:icon="ICON" android:label="LABEL" android:name="NAME"&#62; &#60;/application&#62; &#60;uses-sdk android:minSdkVersion="1" /&#62; &#60;uses-sdk android:maxSdkVersion="2" /&#62; &#60;uses-permission android:name="android.permission.FOO"&#62;&#60;/uses-permission&#62; &#60;uses-permission android:name="android.permission.BAR"&#62;&#60;/uses-permission&#62; &#60;/manifest&#62; BTW：APK软件中的AndroidManifest.xml文档是二进制编码的，可以用APKTool还原。 我们的目标是解析若干属性：如package, versionName, icon, label, name等，代码如下： &#60;?php $xml = new SimpleXMLElement(file_get_contents('AndroidManifest.xml')); $nodes = $xml-&#62;xpath('/manifest'); var_dump((string)$nodes[0]-&#62;attributes()-&#62;package); &#8230; <a href="http://huoding.com/2011/05/29/81">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
			<content:encoded><![CDATA[<p>使用PHP解析XML时，常用<a href="http://www.php.net/manual/en/function.simplexml-load-string.php" target="_blank">simplexml_load_string</a>，缺省是一个<a href="http://www.php.net/manual/en/class.simplexmlelement.php" target="_blank">SimpleXMLElement</a>的包装函数，今天不说simplexml_load_string，只说SimpleXMLElement。</p>
<p><span id="more-81"></span></p>
<p>本文以Android软件中的AndroidManifest.xml文档为例，先看一下演示文档的内容：</p>
<pre>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="PACKAGE" android:versionName="VERSIONNAME"&gt;
    &lt;application android:icon="ICON" android:label="LABEL" android:name="NAME"&gt;
    &lt;/application&gt;
    &lt;uses-sdk android:minSdkVersion="1" /&gt;
    &lt;uses-sdk android:maxSdkVersion="2" /&gt;
    &lt;uses-permission android:name="android.permission.FOO"&gt;&lt;/uses-permission&gt;
    &lt;uses-permission android:name="android.permission.BAR"&gt;&lt;/uses-permission&gt;
&lt;/manifest&gt;</pre>
<p>BTW：APK软件中的AndroidManifest.xml文档是二进制编码的，可以用<a href="http://code.google.com/p/android-apktool/" target="_blank">APKTool</a>还原。</p>
<p>我们的目标是解析若干属性：如package, versionName, icon, label, name等，代码如下：</p>
<pre>&lt;?php

$xml = new SimpleXMLElement(file_get_contents('AndroidManifest.xml'));

$nodes = $xml-&gt;xpath('/manifest');

var_dump((string)$nodes[0]-&gt;attributes()-&gt;package);
var_dump((string)$nodes[0]-&gt;attributes('android', true)-&gt;versionName);

$nodes = $xml-&gt;xpath('/manifest/application');

var_dump((string)$nodes[0]-&gt;attributes('android', true)-&gt;icon);
var_dump((string)$nodes[0]-&gt;attributes('android', true)-&gt;label);
var_dump((string)$nodes[0]-&gt;attributes('android', true)-&gt;name);

$nodes = $xml-&gt;xpath('/manifest/uses-sdk');

foreach ($nodes as $node) {
    foreach ($node-&gt;attributes('android', true) as $attribute =&gt; $value) {
        var_dump($attribute, (string)$value);
    }
}

$nodes = $xml-&gt;xpath('/manifest/uses-permission');

foreach ($nodes as $node) {
    foreach ($node-&gt;attributes('android', true) as $attribute =&gt; $value) {
        var_dump($attribute, (string)$value);
    }
}

?&gt;</pre>
<p>因为只是演示，所以代码有点冗余，大家留意命名空间的使用，多余的话我就不说了。</p>
]]></content:encoded>
			<wfw:commentRss>http://huoding.com/2011/05/29/81/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>

