2012年1月20日星期五

12306性能问题之我见

铁道部12306订票网站可谓是生不逢时。在春运的大潮下,没能经受住考验。既然是网络订票,其使用的主体必然是广大的“网民”,其中不乏身手不凡的“极客”和专家。继令人叹为观止的使用Firebug抢火车票(不知农民工兄弟们会怎么想)以后,又有人本着“得一火车票胜造七级浮屠”的仁慈之心,开发出了”造福万民“的12306 Helper火狐插件。这些极客手段和插件的诞生对12306来说更是雪上加霜。

要解决这个问题也不是不可能,网上除了上述这些损人利己的极客手段外,还涌现出很多为12306”支招“的文章,其中有很多关于大容量web站点架构的思想值得我们去学习和思考。比如酷壳风云这里。作为一个研究网站架构技术的从业人员,我也谈一下这个问题。 

大型购票系统的设计

用过12306.cn的人,尤其是互联网从业人员,均会觉得这个网站很“烂”。依我之见,烂体现在两个方面,一个是界面以及使用体验之烂;另一个是架构理念上的烂 -- 仍然是在用N年前做ERP的思路在做一个web 2.0时代大型的电子商务网站。第一点是顾客能够感受到的,而第二点则不是那么明显了。

那么,这个系统正确的做法是什么呢?上文引用的几篇博文无一例外地为铁道部指明了正确的方向:使用队列系统。我完全赞同他们的思路,只是在实现的细节上有我自己的思考。

使用流程

我设想的购票流程是这样的:

1. 车次查询

客户输入所需的出发地、目的地和出发日期/时间查询列车班次。 在这一步骤中,只有出发和目的地是关键信息。客户可以一次性按照他自己的优先次序选择多个不同日期/时间出发的车次,全部加入“购物篮”。

2. 订单确认和预付款
 
在订单确认环节,客户输入乘车人的身份证件信息,提交订单。系统将首先验证用户输入的手机号确为客户所拥有(发送验证码),然后引导客户进行网上支付。注意:在此步骤中支付的款项为预付款,即押金,支付并不代表已经成功购得车票。在支付之前,系统会请用户确认若购票不成功,押金何时返还。客户可以选择押金在操作后的第二天立即返还,也可以选择押金在若干天后如仍无法成功购得车票才返还。支付以后,系统给出一个6~8位字符的确认码,通过短信发送到客户的手机上。

3. 竞购

系统后台建立一个队列,分下单和订单处理两个阶段运作。例如,设定每个周期为20分钟,其中每个周期的后10分钟为订单处理阶段。没能在周期前10分钟内完成押金支付的客户的订单将累积到下个周期的订单处理阶段中处理。每周期的竞购结束后,若客户购票成功,系统将发送一条短信至客户手机,此短信包含在订单确认环节中生成的确认码;若客户购票不成功,订单将被自动累积到下一个处理周期,同时,若此轮为该客户第一次参与竞购,系统也将发送一条提示短信(后续周期中如果该客户仍然未成功购得车票,系统将不再发送短信提示)。

4. 定位和取票

竞购成功的客户可以凭手机号码(或身份证号码)以及确认码登录取票网站选择自己的座位,也可以在代售点或车站进行座位确认,并取票,取票时需使用身份证。

5. 退款

发生退款的情形有三种:
  • 未成功购得车票,这里又分两种原因:
    • 身份信息不正确(如:姓名和身份证号不匹配):押金将退回到付款所用的银行账户
    • 竞购失败:押金将退回到乘车人所拥有的银行账户
  • 退票:押金将退回到乘车人所拥有的银行账户 
退款操作系统自动执行,无需用户或运营管理人员干预。

6. 查询及退票

客户可以通过专门的网站查询界面通过手机号、身份证号以及确认码来查询订票情况、执行退票操作(包括输入退票所用的账户信息)。

为什么这样设计

铁路售票系统最需要解决的问题就是:在僧多粥少的情况下,如何公平地分配稀缺资源?引申出来的问题是:如何设计一个高效的在线售票系统应对大流量?如何防范黄牛或恶意攻击?在流程设计层面,我的设计方案做到了两点:

有效而且公平

与本文开头所引用的几篇博文的一个小区别是,我在设计队列系统的时候特别考虑了所谓“秒杀”的做法,并在流程设计上加以杜绝。同时,方案中又保留了队列系统的基本特征,即先来后到。

鲁棒而且安全

本系统最大的特色是免注册、免登录(查询除外)。通过简化流程和技术手段的配合,可以极大地提升系统的吞吐量,而又不降低系统的鲁棒性和安全性:
  1. 充分利用车票实名制,完全避免注册和登录过程。
  2. 通过预付款方式,避免黄牛、恶意攻击者和“秒杀”者对服务器产生过大压力。
  3. 通过退款账户限制,从根本上消灭黄牛的生存空间。同时,由于使用了完全异步化的队列系统和唯一的编号(身份证),系统无需想方设法去防止“占着茅坑不拉屎”(或者说DOS攻击)或者重复不合理购买(比如一人购买同一天的多个车次等),这样也就避免了“错杀”(即一个心怀不轨的人乱输入别人的身份证号码,导致真正想购票的人无法购票),提高了用户体验。
设计上的要点

我自信这套购票流程设计是比较完善的(当然,如果你发现了问题或有所欠缺,欢迎留言或通过email和我联系)。但它也不是没有问题的。在设计上,它的主要问题就是和外部系统的互通上。它需要和公安系统联系,验证身份信息的有效性(验证是后台异步进行的,这样可以避免流量压力,也可以保护公民身份信息);还需要和银行支付系统联系,尤其是退款通道上是否会有由于银行系统规范而导致的一些问题,我不得而知。

但与外部系统沟通这个问题现有的12306网站也是要做的,而且我相信这些事情对于铁道部来说是小菜一碟了。这并不是本文所要讨论的内容,不展开了。

技术上的要点

客户端查询

导致当前系统的压力过大的主要原因是购票过程中必不可少的查询,包括车次的查询和剩余票量的查询。我的解决方案是:车次查询完全在客户端完成;不提供实时的剩余票量查询。

具体的解决方案是,客户告诉服务器他的出发和到达地点,凭这点信息,服务器立即返回一套这两个地点之间的车次的所有数据,包括车次、时间、是否有票或剩余票量(不区分车型是否动车、不区分是否始发站,所有数据一次性返回)。这些数据以gzip形式压缩存储于内存数据库(如redis)中,web服务器返回此类数据的速度可以超过读取磁盘文件的速度。客户端取得数据后,以javascript查询本地数据,往“购物篮”中添加订单,直到订单提交时才会再次与服务器沟通。 注意:剩余票量在开始取回以后不实时更新,直到下一个竞购周期才会刷新。

通过这种手段,服务器端的链接数和PV可以至少有数量级以上的下降。用这里提到的数据估算,12306的峰值访问量可能达到两个小时5亿PV,也就是说,每秒钟大约7万个hit。假设新方案降低PV到原来的1/10,也就是不超过7000hits/s。这种量也就一台NginX就可以搞定。当然,实际可能还有HA、热备但也不至于太复杂了。而查询所需的基于JavaScript技术的“列车时刻表”,我没有测算过全国有多少个车站,又有多少种换乘方案,随便估算一下,弄两台128G内存的服务器做Master-Slave,肯定可以搞定了(其实不会有那么多,因为只有热数据才会保存在内存中)。

按功能集群

这个问题我引用的博文中被多次提到,也是架构设计中需注意的“最佳实践”之一。例如,订票网站叫做ticket.12306.cn,提供查询功能,下单服务器是order.12306.cn,而后台还有一堆集群负责订单的处理,这才是本系统工作量集中的地方。后台订单处理系统的任务分配可以根据出发地和到达地进行合理的聚合,即同一(或近似)路线的放在同一服务器上处理。

恰当的防DDOS系统

为了提供最鲁棒的系统,我认为没有必要限制每个用户每次能购买多少票,在设计中也做了种种考虑来实现这种“完全开放”的下单方式。但在IT层面,恰当的防止攻击还是需要的,比如防止同一个IP过快地访问服务器等等。

吾道一以贯之

谈到架构,很多人可能认为是“后端” 的问题,脑子里面浮现出来的可能是CISCO的路由器、IBM的服务器、NginX等等。经验丰富的工程师同时会考虑前端的优化,比如使用AJAX技术、 页面静态化、用YSlow去分析一下等等。但较少有人会将架构和产品设计联系在一起。其实,“架构”是贯穿互联网产品方方面面的灵魂。一个好的架构可以令 你的产品光芒四射,而一个坏的产品设计则会让架构师伤透脑筋,让耗资不菲的硬件设备不堪重负。

上面,我从12306这个活生生的例子阐述了在“架构”这个问题上我的思考:架构问题是一个从需求分析到产品设计到技术实现的大问题。一个优秀的架构师不仅仅要懂得技术实现的手段,而且还必须是一个合格的需求分析人员和产品设计师。

2011年11月2日星期三

PHP Mixin的改进

前段时间写了一篇关于PHP Mixin的文章,用着还挺顺手的。今日对我的Mixin类做了一点改进,让它能够用shell风格的通配符来过滤注入的方法。新的Mixin类代码如下。

class Mixable {
    private $_mixin_methods;
    private $_mixin_classes;
    public function mixin() {
        $behaviors = func_get_args();
        if ((count($behaviors) == 0) ||
            !isset($this->_mixin_methods) ||
            !isset($this->_mixin_classes)) {
            $this->_mixin_methods = array();
            $this->_mixin_classes = array();
        }
        foreach($behaviors as $b) {
            list($c, $m) = explode('-', $b.'-');
            if (empty($m)) {
                list($c, $m) = explode('+', $b.'+');
                $skip = false;
            } else {
                $skip = true;
            }
            $mfilter = empty($m) ? array() : explode(',', $m);
            if(isset($this->_mixin_classes[$c])) {
                foreach($this->_mixin_classes[$c] as $m) {
                    if ($this->_mixin_methods[$m] == $c) unset($this->_mixin_methods[$m]);
                }
                unset($this->_mixin_classes[$c]);
            }
            $methods = get_class_methods($c);
            if (is_array($methods)) {
                if (empty($mfilter)) {
                    foreach($methods as $m) {
                        $m = strtolower($m);
                        $this->_mixin_methods[$m] = $c;
                        $this->_mixin_classes[$c][] = $m;
                    }
                } else {
                    foreach($methods as $m) {
                        $m = strtolower($m);
                        $matched = false;
                        foreach($mfilter as $wildcard) {
                            if (fnmatch($wildcard, $m, FNM_CASEFOLD)) {
                                $matched = true;
                                break;
                            }
                        }
                        if ($matched == $skip) continue;
                        $this->_mixin_methods[$m] = $c;
                        $this->_mixin_classes[$c][] = $m;
                    }
                }
            }
        }
    }
    public function __call($method, $args) {
        $m = strtolower($method);
        if (isset($this->_mixin_methods[$m])) {
            $class = $this->_mixin_methods[$m];
            array_unshift($args, $this);
            return call_user_func_array(array($class, $m), $args);
        }
        trigger_error('Call to undefined function '.$method, E_USER_ERROR);
    }
}

用两段代码来说明用法:

例1:行为的继承

class Course {
    public static function greeting($obj) {
        if (get_class($obj) == 'Teacher')
            echo "Good morning class!\n";
        else
            echo "Good morning teacher!\n";
    }
}
class CourseWork extends Course {
    public static function homeworkReview($obj) {
        echo "I am reviewing homework...\n";
    }
}
class HomeWork extends Course {
    public static function homeworkStudy($obj) {
        echo "I am doing my homework...\n";
    }
}
class Teacher extends Mixable {
}
class Student extends Mixable {
}
$t = new Teacher();
$t->mixin('CourseWork');
$s = new Student();
$s->mixin('HomeWork');
$t->greeting();
$t->homeworkReview();
$s->greeting();
$s->homeworkStudy();

在这个例子中,演示了行为是可以继承的。另外要注意,行为类中的方法一般需要写作static的,虽然不写成static也可以跑,但如果php设置为STRICT模式,会有告警。

例2:选择性注入 

class Behavior {
    public static function adminAction($obj) {
        echo "> This behavior require <admin> privilege\n";
    }
    public static function userAction($obj) {
        echo "> This behavior is normal\n";
    }
}
class Power {
    public static function hyperPower($obj) {
        echo "> PHP Hyper Power...\n";
    }
}
class Main extends Mixable {
}
$m = new Main();
echo "Mixin Behavior without exclusion\n";
$m->mixin('Behavior', 'Power');
echo "Trying user action\n";
$m->userAction();
echo "Trying admin action\n";
$m->adminAction();
$opt = mt_rand() % 3;
switch($opt) {
    case 0:
        echo "Mixin Behavior while excluding admin action\n";
        $m->mixin('Behavior-Admin*');
        break;
    case 1:
        echo "Mixin Behavior while including only user action\n";
        $m->mixin('Behavior+User*');
        break;
    default:
        echo "Mixin Behavior again (clearing Hyper Power)\n";
        $m->mixin();
        $m->mixin('Behavior');
}
echo "Trying hyper power...\n";
$m->hyperPower();
echo "Trying user action\n";
$m->userAction();
echo "Trying admin action\n";
$m->adminAction();

这个例子说明以下几个用法:
  • 通过类名+方法1,方法2...(只注入这些方法)或者类名-方法1,方法2...(注入除这些方法以外的其它方法)这种表达法来选择性地注入行为类中的一部分方法
  • 如果多次注入不同的行为,先前注入的不会被取消,除非以无参数的mixin()调用来清除以前注入的类
  • 在过虑方法名的时候可以用shell通配符(大小写不区分)
  • 最后(代码中没有体现),mixin()方法的参数可变。一次mixin调用可以注入多个类,例如: $obj->mixin('Class1', 'Class2');

2011年10月21日星期五

[转] Ubuntu中设置个人固定文件夹的语言

Ubuntu对中文的支持越来越好,这自然是好事。不过在终端下输入命令的时候,遇到中文文件夹,可不是件好事。多谢谷歌及众多网友,终于让我找到解决的办法。
   
    export LANG=en_US
    xdg-user-dirs-gtk-update
    export LANG=zh_CN.UTF-8

这样基本就解决问题了。


原文地址

2011年10月14日星期五

PHP Closure的一个应用

Closure是一种很灵活的技术,如果运用得当,可以大大提高代码的可维护性,尤其对于象PHP这样的动态语言更是如此。Closure的定义可以在Wikipedia上查到,关于这一技术的学术文章也可以轻易的搜索到,在此就不赘述了。

在工作中,我需要实现一个事件处理系统。我把它设计为一个类,在类的一个方法中,我设计了一个回调函数来处理事件。它的主要逻辑如下:
  1. //main.php
  2. require 'event_handlers/index.php';
  3. $type = 'event1';
  4. function handleEvent() {
  5.     global $type;
  6.     if (function_exists('handle_'.$type)) {
  7.         call_user_func('handle_'.$type);
  8.     }
  9. }
  10.  
  11. //event_handlers/index.php
  12. require 'event1.php';
  13.  
  14. //event1.php
  15. function handle_event1() {...}
在上面的例子中,我使用了一个全局的变量$type,这种做法只是为了少打一些字,突出主要逻辑,在实际代码中handleEvent是一个类的方法。

它的结构很简单,主程序中引用了一个存放所有事件的处理函数的“索引”文件,即event_handlers/index.php,每个种类的事件处理程序都被放置在一个独立的文件中,由索引文件引用。

现在问题来了。由于系统设计中的不确定性,我不能保证事件类型(即$type)将来不会改名。一旦类型的名字改了,就需要改变每个类型处理函数的名字,而一 类事件会有多个不同的处理函数(即上例中event1.php中的函数个数会有多个)。为了使将来的代码维护尽量简单,我从#php学了一个新的方法,即 利用closure使得事件处理函数的名字是动态的。一旦发生事件类型改变,只需要修改事件处理“函数库”的文件名即可。下面的例子演示了这种用法:

//main.php
$event_type = $argv[1];
require_once "$event_type.php";
$handler = 'handle_'.$event_type;
if (is_callable($$handler)) {
    call_user_func($$handler);
} else {
    echo "$handler is not defined\n";
}

//event1.php
$event_type = basename(__FILE__, '.php');
$handle_this = 'handle_'.$event_type;
$$handle_this = function() {
    global $event_type;
    echo "Testing handler of $event_type...\n";
};

在 这个例子中,event1.php可以任意重命名,主程序只需要以那个名字调用它即可以引用其中定义的事件处理函数。需要注意的是这里必须用 is_callable代替function_exists,因为Closure从本质上说是匿名函数,不能用function_exists检测其存在 与否。另外,两次解引用的方法(即$$handler、$$handle_this等)也需要注意不要弄错了。

PHP的Mixin

日前看了一篇在PHP中实现Mixin的博文,感到非常的有用,就仔细研究了一下作者的思路,并且到#php上去咨询了一下泡在上面的PHP牛,然后对原文作者的实现方案进行了一些改良,记录下来备用。

一、何谓Mixin?

Mixin是一种编程的风格,在Lisp等语言里面早已有了,但我首次知道这个概念是在Ruby语言中。我对Mixin的理解是:一种行为注入(Behavior Injection)的方法。要了解Mixin,也就是“行为注入”的好处,需首先了解OOP中的“多重继承”和“接口”的概念。

在我的印象中,接口是在继承这个概念出现以后才进入主流语言的。首先有了C++的多重继承,然后才有了Java中否定多重继承而代之以接口的概念(当然,一些先驱性或革命性的语言如Lisp、Eiffel可能早就有此类概念了)。接口带来的好处不言而喻,但它仍然有不够灵活的地方:如果一个类声明了某个接口,它就必须实现此接口,而如果它没有声明某接口,也就无法在运行时动态地获取此接口相应的行为。

Mixin则灵活的多。它相对于接口的优势在于,无需规定接口,可以随时添加/删除行为。这种动态添加行为的能力非常有用。它可以让我们写出既面向对象而又面向切面 的代码。比如,在ERP系统中有一个员工类。假设“主管”级别的员工有查看报表的权利,我们可以利用Mixin,使得系统中只需要一个员工类,就可以方便 的实现权限控制 -- 只需要在合适的地方注入“主管”行为即可,而无需针对员工类派生出各种具有不同权限的主管类、工人类等等,或者到处写一大堆复杂的、不那么OO的权限判断代码。

二、在PHP中实现Mixin

先来看一下代码:

<?php
class Greeting {
    public function greet($obj) {
        echo $obj->getName()." says hello\n";
    }
    private function __construct() {} //一个private的构造函数使得这个类不能直接被实例化
}

class Mixable {
    private $_mixinlookup = array();
    public function __construct() {
        if (is_array($this->mixins)) {
            foreach($this->mixins as $mixin) {
                $methods = get_class_methods($mixin);
                if (is_array($methods)) {
                    foreach($methods as $method) {
                        $this->_mixinlookup[$method] = $mixin;
                    }
                }
            }
        }
    }
    public function __call($method, $args) {
        if (isset($this->_mixinlookup[$method])) {
            $class = $this->_mixinlookup[$method];
            array_unshift($args, $this);
            return call_user_func_array(array($class, $method), $args);

        }
        trigger_error('Call to undefined function '.$method, E_USER_WARNING);
    }
}

class Mixture extends Mixable {
    protected $mixins = array("Greeting");
    public function getName() {
        return get_class();
    }
}

$o = new Mixture();
$o->greet();
?>

在这个例子中,Mixture类被注入了Greeting这种行为而具备了greet()的能力。这种行为是在运行中可以随时改变的。想象一下这种能力的用途吧。

三、针对原文的改进之处

原文中使用了eval方法来实现Mixin,带来了两个弊端:1)被资深PHP人一致痛斥的eval具有很大的安全隐患和很差的性能;2)我发现按照原方案,这些被注入的方法只能接受简单的数字或字符串参数,而不能使用复杂的如array或object类型的参数。

改进的方法是,避免在需要注入的方法中使用$this,而是学习python,在方法的第一个参数传入当前对象的指针(也就是例子代码中greet的参数$obj)。只要习惯了这种做法,其实它和$this是一样方便的。

给网站加个SSL证书

以NginX为例,本文描述两种证书安装方式:

非正式证书

出于测试的目的,可以自行生成证书,步骤相当的简单:

一、生成密钥和证书
  1. openssl genrsa -des3 -out server.key 2048
  2. openssl req -new -key server.key -out server.csr
  3. cp server.key server.key.org
  4. openssl rsa -in server.key.org -out server.key
  5. openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
以上步骤的要点说明如下:
  • 第一步中需要输入密码,这个密码要记住,因为第二步中需要这个密码。
  • 此证书的密钥设置为2048 bit (第一步)、有效期为一年(第五步)。
  • 最终生成的文件有两个:server.key (密钥)、server.crt(证书)。
二、安装证书
  1. 将server.key、server.crt复制到nginx/conf目录下。
  2. 修改nginx.conf文件,打开被注释掉的SSL服务器配置模板,填入所需的密钥和证书文件名。
此处仅说明一点:如果密钥和证书文件在nginx/conf目录下,配置文件中可以直接使用文件名,不然的话,配置文件中需要使用全路径。

正式证书

正式证书的申请制作与非正式的类似。关键是非正式操作中的第五步在正式操作中是由认证机构完成的。你把server.csr提交给认证机构,他们就会发还一个server.crt(注意,可能收到两个文件,其中一个是认证机构的证书,需用cat命令附加在crt文件末尾)。详情可阅读参考文献,并按照认证机构的指示操作。参考文献中介绍了一个值得尝试的免费证书提供商:StartCom的StartSSL(http://www.startssl.com)。

参考文献

http://love.ulnmp.com/?p=271

自动建立SSH链接的辅助脚本

由 于工作需要,我在GNOME的启动程序中添加了一条用autossh建立SSH链接的命令,但它就是无法执行。经研究发现,当这条命令执行的时 候,NetworkManager的WiFi链接尚未建立!网上有人说用wicd来代替NetworkManager,但我怕麻烦,就自己写了一段脚本, 自动检测是否有网络链接,直到检测到网络链接即执行autossh。程序相当简单:

#!/usr/bin/env python
import time
import subprocess
import syslog
import sys

def connected():
    with open('/proc/net/route') as f:
        routes = f.read().strip().split("\n")
        return len(routes) > 1

cmd = sys.argv[:]
cmd[0] = 'autossh'
syslog.openlog()
syslog.syslog(' '.join(cmd))
while not connected():
    syslog.syslog("network disconnected, retry in 5 seconds...")
    time.sleep(5)

syslog.syslog("network connected!")
pid = subprocess.Popen(['killall', '-q', 'autossh']).pid
time.sleep(1)
try:
    pid = subprocess.Popen(cmd).pid
    syslog.syslog("PID of autossh process: " + str(pid))
except OSError as e:
    syslog.syslog(str(e))
syslog.closelog()