Archive for the ‘互联网’ Category

本文参考了:http://www.grepmymind.com/2008/01/11/memcached-php-semaphore-cache-expiration-handling/

问题描述

memcached作为互联网经常使用的缓存工具,大大减少了数据库的压力。但是当并发度较高时,可能会有多个请求都检测到expire,从而发起数据库请求,于是会对数据库造成冲击。

问题重现

当采用传统的设置expire time时,使用httpload工具并发100,在数据库请求函数里sleep 1秒钟。memcache封装里打印是从mc读取还是db读取。expire time设置为2秒。可以看到有重复的数据库请求:

[2012-02-21 10:17:13] NOTICE [1]: mc(127.0.0.1,/test.php)
[2012-02-21 10:17:14] NOTICE [1]: mc(127.0.0.1,/test.php)
[2012-02-21 10:17:14] NOTICE [1]: db(127.0.0.1,/test.php)
[2012-02-21 10:17:14] NOTICE [1]: db(127.0.0.1,/test.php)
[2012-02-21 10:17:15] NOTICE [1]: mc(127.0.0.1,/test.php)
[2012-02-21 10:17:15] NOTICE [1]: mc(127.0.0.1,/test.php)

解决方法

                function call_high($func, $args, $expire=60){/*{{{*/
                        // Calulate key
                        $key = md5(json_encode($func).json_encode($args));

                        // Get from memcached
                        return $this->get_high($key, $func, $args, $expire);

                }/*}}}*/

                // lock_expire should longer than db_func running time
                function get_high($key/* string or array*/, $db_func/*mixed*/, $db_args/*mixed*/, $expire, $lock_expire=10){/*{{{*/
                        if (!$this->mem){
                                return false;
                        }

                        $val = $this->get($key);

                        // if(!$val){ // Maybe empty when FIRST get, otherwise, must something wrong, like the lock&work thread haven't refresh item before expire time. So we use a short lock expire time to allow more thread trying to refresh. }   

                        if ( $val && (!isset($val['t']) || !isset($val['i'])) ){ // don't have expire index? must be wrong. For logic layer to process.
                                #Core_Log::get_instance()->notice("mc, no expire index");
                                return $val;
                        }

                        if ($val && ($val['t'] - $this->get_high_cal_time($expire) > time())){ // not expired
                                #Core_Log::get_instance()->notice("mc");
                                return $val['i'];
                        }

                        // expired, allowed one process to refresh it
                        if (!($this->get_high_lock($key, $lock_expire))){ // other process has refreshing it, so we quit with old data(or may be empty)
                                #Core_Log::get_instance()->notice("other lock, we quit");
                                return $val?$val['i']:false;
                        }

                        #Core_Log::get_instance()->notice("db");
                        // get from user func, and set into memcache
                        $db_info = call_user_func_array($db_func, $db_args); // may be empty, we also set into mc
                        $this->set_high($key, $db_info, MEMCACHE_COMPRESSED, $expire, $this->get_more_expire_time($expire));

                        $this->free_high_lock($key);
                        #Core_Log::get_instance()->notice("save and free lock");

                        return $db_info;
                }

                // These two functions should guarantee the refresh thread can do all work before realy expired.
                // So the work_time < cal_time + more_time
                protected function get_high_cal_time($expire){
                        return intval(min(10, max(0, $expire/5))); // [0, 10]
                }
                protected function get_more_expire_time($expire){
                        return intval(max(100, $expire*0.1)); // [100, ...)
                }                 

                function get_high_lock($key, $expire=100){
                        return $this->mem->add($key.self::HIGH_MC_LOCK_TAIL, 1, 0, $expire); // return true when not exists already, otherwise return false, use as mutex
                        //return $this->mem->increment($key.self::HIGH_MC_LOCK_TAIL, 1); //can't, because increment will return false, if elem not exists already
                }

                function free_high_lock($key){
                        return $this->del($key.self::HIGH_MC_LOCK_TAIL);
                        //return $this->mem->decrement ($key.self::HIGH_MC_LOCK_TAIL, 1);
                }

                function set_high($key, $val, $flag, $expire/*second*/, $more_expire=100){
                        if (!$this->mem){
                                return false;
                        }

                        // save expire time
                        $expire_time = time()+$expire;
                        $val = array('t'=>$expire_time, 'i'=>$val);

                        $expire += $more_expire; // need more expire time, because we don't want return null when get_high
                        return $this->set($key, $val, $flag, $expire);
                }/*}}}*/

基本思想就是利用mutex锁,保证仅有一个进程更新该item。

有几个点说明一下:

  • 首次请求时,非refresh线程会返回为空,在业务逻辑上是否可接受?解决方法可以是等待一定时间后再次获取
  • 如果refresh线程工作太久,导致memcached中相应item已经expire了,那么其他线程get from mc即为空,则也会尝试获取lock后从db读取。这里通过设置lock time、cal time和more expire time已解决,允许其他线程也请求db,这种情况一般是数据库阻塞导致的,允许重复读取可能会加大数据库压力,所以存疑。
  • 如果memcached内存不足,导致item expire,则正常情况下也只有一个refresh线程从db读取
  • 如果memcached内存不足,导致lock expire,则会有多个refresh线程从db读取(一般不会设置这么小的memcached吧?)

使用php发送邮件很简单,实际利用的是linux自带的smtp服务。这里不加赘述。

邮件服务,我的理解是数据包与包的交互。发送邮件时,smtp服务接收client程序的邮件标题、正文、from、to、优先级等信息,拼装为smtp数据包,网络发送给to地址所在的邮件服务器。接收邮件时,smtp服务从网络上的邮件服务器接收数据包,解析出to的email地址,利用推或拉的方式传递给client。

php中的mail函数,作为一种client形式,在其help里写的很清楚:

reject note 返回值

Returns TRUE if the mail was successfully accepted for delivery, FALSE otherwise.

It is important to note that just because the mail was accepted for delivery, it does NOT mean the mail will actually reach the intended destination.

即mail的返回值仅代表php.ini里配置的smtp server是否接收发送请求成功,而并不代表最终的to email接收成功。

进而推测,smtp对mail请求应当是异步的,那么也许smtp的log中会有邮件接收的真正状态。进入centos系统的/var/log,果然有maillog!

Feb  7 09:21:45 v080106 sendmail[32612]: q171LiwL032612: to=abc@sohu.com, ctladdr=admin (500/500), delay=00:00:01, xdelay=00:00:01, mailer=relay, pri=32760, relay=[127.0.0.1] [127.0.0.1], dsn=2.0.0, stat=Sent (Ok: queued as AC58F1060001)

这是一条发送成功的邮件记录。各字段意义为:

Feb  7 09:21:45 发送时间
v080106 发送者机器名称
sendmail[32612]: 进程号
q171LiwL032612: 唯一Message-ID,可以根据该字段,找到log里与本次发送相关的所有记录
to=abc@sohu.com, 接收者邮箱
ctladdr=admin (500/500)
delay=00:00:01, xdelay=00:00:01, 延时
mailer=relay, 邮件服务器,是relay,则参考下面的relay字段可知具体信息
pri=32760, 优先级
relay=[127.0.0.1] [127.0.0.1], 中继
dsn=2.0.0, smtp code (200表示成功)
stat=Sent (Ok: queued as AC58F1060001) 状态说明,Sent代表发送成功

Feb  8 01:38:48 v080106 postfix/smtpd[11264]: F1E521060007: client=localhost.localdomain[127.0.0.1]

Feb  8 01:38:49 v080106 postfix/cleanup[11267]: F1E521060007: message-id=<201202071738.q17HcmEB011273@v080106.isv>

Feb  8 01:38:49 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 01:38:49 v080106 sendmail[11273]: q17HcmEB011273: to=abc@vip.sohu.com, ctladdr=admin (500/500), delay=00:00:01, xdelay=00:00:01, mailer=relay, pri=32807, relay=[127.0.0.1] [127.0.0.1], dsn=2.0.0, stat=Sent (Ok: queued as F1E521060007)

Feb  8 01:38:49 v080106 postfix/smtp[11270]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=0.25, delays=0.05/0/0.15/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 02:04:59 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 02:05:00 v080106 postfix/smtp[22089]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=1572, delays=1571/0.85/0.13/0.04, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 02:38:26 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 02:38:26 v080106 postfix/smtp[28707]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=3577, delays=3577/0.01/0.14/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 03:45:54 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 03:45:54 v080106 postfix/smtp[10772]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=7625, delays=7625/0.03/0.08/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 05:08:20 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 05:08:30 v080106 postfix/smtp[19312]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=12581, delays=12572/9.5/0.11/0.04, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 06:31:47 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 06:31:47 v080106 postfix/smtp[21697]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=17578, delays=17578/0/0.08/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 07:55:00 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 07:55:00 v080106 postfix/smtp[22783]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=22572, delays=22571/0.02/0.11/0.37, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 09:01:41 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 09:01:41 v080106 postfix/smtp[23869]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=26572, delays=26572/0/0/0, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 10:08:21 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 10:08:21 v080106 postfix/smtp[24922]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=30572, delays=30572/0/0/0, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

以上是一组失败的记录,唯一号是F1E521060007。可以看到,当接收到失败之后,sendmail进行了多次重试。里面还有一些疑问,之后再解决,现在主要关心错误信息:

relay=mx.vip.sohu.com[61.135.132.86]:25, 中继
dsn=4.1.8, # 错误码
status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))  # 具体描述,延迟发送,mx.vip.sohu.com认为无法找到发送者的domain

还有一类统计数据:

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: start interval Feb  8 10:41:42

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: domain lookup hits=1692 miss=209 success=89%

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: address lookup hits=947 miss=9428 success=9%

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: max simultaneous domains=4 addresses=9 connection=30

有了以上这些信息,就可以准备的跟踪真实的邮件到达率了。当然,是否被认为是垃圾邮件之类,是无法从这里看出来的。

———————————————————

以gmail为例,可以点击邮件右上方的more按钮->show original查看到邮件头信息:

Delivered-To: abc@gmail.com
Received: by 10.182.176.71 with SMTP id cg7cs142014obc;
        Tue, 7 Feb 2012 15:44:47 -0800 (PST)
Received: by 10.68.218.167 with SMTP id ph7mr39734334pbc.110.1328658286305;
        Tue, 07 Feb 2012 15:44:46 -0800 (PST)
Return-Path: <201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com>
Received: from smtp-out-184-237.amazon.com (smtp-out-184-237.amazon.com. [207.171.184.237])
        by mx.google.com with ESMTP id e6si262857pbj.138.2012.02.07.15.44.45;
        Tue, 07 Feb 2012 15:44:46 -0800 (PST)
Received-SPF: pass (google.com: domain of 201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com designates 207.171.184.237 as permitted sender) client-ip=207.171.184.237;
Authentication-Results: mx.google.com; spf=pass (google.com: domain of 201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com designates 207.171.184.237 as permitted sender) smtp.mail=201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com
Date: Tue, 7 Feb 2012 23:44:45 +0000 (UTC)
From: "Amazon.cn" <store-news@amazon.cn>
To: "abc@gmail.com" <abc@gmail.com>
Message-ID: <677952103.17204211328658285803.JavaMail.em-build@massmail-sender-fe-9004.sea19.amazon.com>

SPF的状态我看到的有pass(可信)、neutral(中性),有些垃圾邮件的状态也是pass的。
大批量发送广告邮件时,目前收集到的需要注意:

  1. 控制发送频率,10个以上ip并行发送,一天控制在几十万的数量
  2. 更真实的header信息,通过配置发件域名等信息
  3. 如果可能,同邮件服务商沟通,争取进入白名单

邮件域名配置:

  1. 主机A记录abc.com
  2. 主机A记录mail.abc.com
  3. 主机A记录www.abc.com
  4. 主机A记录ftp.abc.com
  5. abc.com 到 mail.abc.com的MX记录

另外还有反向解析相关的配置。比如我们的发件地址是news@mail.flykobe.com,则需要在邮件服务器里将HELO地址配成mail.flykobe.com,并且按照上面的方法配置好各种相关域名到固定ip。同时,还需要将ip到mail.flykobe.com做一个映射(这个需要联系DNS提供商)。我们既无固定ip,就无法实现这个了!可以参考:http://hi.baidu.com/yangming/blog/item/323d034ff87a103fafc3ab8a.html

————————————————————-

记录一下利用postfix,即开源的邮件传输代理,搭建邮件服务器的过程。

网上资料可见,需要以下几个套件:

  • SMTP 認證套件: cyrus-sasl ( cyrus-sasl-1.5.27-5mdk 以及其他認證機制函式庫 )
  • Postfix 郵件伺服器: postfix ( postfix-1.1.11-4mdk )
  • POP3 伺服器: imap ( imap-2001a-9mdk, imap-devel-2001a-9mdk 兩個 ) # 最后没有用这个
  • 郵件分析軟體:procmail ( procmail-3.22-3mdk )

我们的支付宝悬赏项目今天上线了,整理一下开发过程中遇到的问题,以及相应的解决方法。我们线上项目地址:http://yingyong.taobao.com/show.htm?app_id=155001

测试环境的ip限制

支付宝项目大多涉及到money,所以对安全性要求较高。支付宝一般要求第三方提供固定ip的白名单,仅允许白名单ip访问其系统接口(注:测试开发阶段,访问页面接口,也需通过固定ip)。

测试开发阶段,也同样需要固定ip的服务器,并在这些服务器上配置支付宝提供的hosts。我们的测试、开发服务器出口ip不固定,但是可以通过ssh的端口转发、本地代理等方式解决。以下,将称呼该固定ip服务器为代理服务器。

为chrome安装proxy switch,并用putty建立一个到代理服务器的ssh代理通道,指定给proxy switch。在本地开发机上,将hosts配置为淘宝所需。这样就可以访问其页面接口了。下面通过linux curl函数代理解决固定ip访问系统接口的问题。

Linux curl函数代理

绑定帐号之类的页面接口访问,可以通过浏览器代理实现。但是之后的创建悬赏任务等,都是系统接口,在server端执行的,需要设置linux下curl函数的sock代理。原理一样,也是通过ssh代理实现:ssh -TnNv -D local-port -p ssh-proxy-port  user@ssh-proxy-ip # 如果需要后台执行,则ssh -qTfnN 。

之后,在代码里设置代理信息:

curl_setopt($process, CURLOPT_PROXY, ‘127.0.0.1:7070′);
curl_setopt($process, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);

ssh代理建立完成后,可以写一些小测试代码,检查代理是否正确。这时,有些情况会没有响应,这时可以换一个目标url试试。比如我之前用baidu测试,就没有响应,但是退出ssh代理时可以看到:

debug1: channel 2: free: direct-tcpip: listening port 7070 for 119.75.218.77 port 80, connect from 127.0.0.1 port 21856, nchannels 3
debug1: channel 3: free: direct-tcpip: listening port 7070 for 119.75.217.56 port 80, connect from 127.0.0.1 port 21857, nchannels 2
debug1: channel 4: free: direct-tcpip: listening port 7070 for 119.75.217.56 port 80, connect from 127.0.0.1 port 21864, nchannels 1

说明还是连接上了,但是baidu没给影响而已。

这样就彻底解决了固定IP的问题,以下正式开发。

绑定帐号

我们的功能需求是,定期给我们的用户打款,人和金额都不定。

由于支付宝接口中,用户都是用支付宝唯一用户号(2088开头,16位纯数字)来标识的,可以通过用户绑定的方式获取其支付宝id号(也有其他途径,比如taobao.user.get)。针对悬赏系列接口而言,仅创建任务者需要通过该接口绑定。其他如收款者,只需要获取到支付宝唯一号即可,不需要强制绑定。

例如我们的用户flykobe登录之后,希望绑定支付宝帐号,则页面会由站内跳转到alipay,flykobe在那里输入支付宝的帐号、密码手工授权,授权成功后,会跳转到return_url指定的我们的站内页面来,提示授权成功,并且该return_url会将绑定的结果(包括支付宝唯一用户号、部分屏蔽的支付宝帐号、绑定key)记录在我们的本地存储里,留待后用。

sns.account.bind参数和返回值解释:

Args:sns_user_id – 第三方站内用户id;sns_user_name – 第三方站内用户名; type – 我使用的是common,因为不清楚buyer、seller、common之后的区别。注意:之后悬赏接口的outer_account_name和outer_account_id就对应这里的sns_user_name和sns_user_id。

Returns:alipay_login_id – 支付宝帐号,可能是邮箱、手机号等;key – 在解除绑定时需要,之后的悬赏接口未见使用;alipay_card_no – 支付宝用户号,16位纯数字,目前以2088开头。

注意:在测试阶段,使用的支付宝帐号是测试帐号,需要请支付宝帮忙生成,否则就会一直报“请求参数不正确。”

注意2:对支付宝返回值进行sign verify的时候,仅需要将支付宝提供的参数作为verify的对象,而return_url等中自带的参数不需要!否则就会verify失败!

创建任务

可以在https://shang.alipay.com/qiugou/index.htm看看支付宝的悬赏任务。我的理解是,由一些商家或者组织,发布任务提供赏金(即冻结这部分钱),一旦用户完成了这些任务,并由商家人工审核通过后,就可以标记用户,并自动完成支付赏金的过程。创建任务接口,顾名思义,就是创建一个新的悬赏任务。

在支付宝提供的悬赏接口中分为页面接口和系统接口。前者实际上是带着第三方提供的url参数跳转到支付宝里,需要人工输入一些密码、验证码等信息来完成的;后者则是可以完全代码后台运行,不需要人工干预的。

创建任务是一个页面接口,解释一下参数和返回值。

Args:outer_task_id – 第三方的任务id,由第三方保证其唯一性;outer_task_freeze_no – 第三方流水号,也由第三方保证唯一性;task_amount – 赏金金额(总额);task_type – 目前测试阶段,由支付宝提供了一个字符串作为我们的任务类型,不清楚之后应该传入什么值;task_title – 标题;task_expired_time – 截止时间;outer_account_name – 绑定的第三方用户名;outer_account_id – 绑定的第三方用户id。未使用增值服务,所以增值字段都置为空了。

Returns:仅多返回了paid_time – 支付时间。其他还返回了outer_task_id、outer_task_freeze_no、task_amount、additional_profit_amount、additional_profit_transfer_no。所以,具体支付的细节,需要在调用该create接口前,第三方自己记录下来。

注意:在测试环境下,验证码都是“8888”!

注意2:线上环境里,任务的创建者必须是与支付宝签约的帐号,否则在进行后续的pay动作时,会报错:PLATFORM_AUTHORITY_ILLEGAL

注意3:线上环境,刚上线时,创建的任务截止时间不要太长、金额不要太大。如果用户有未结束的创建任务,则无法解除绑定。非签约用户可创建任务,但该任务之后无法支付赏金,也无法结束。

打款

alipay.witkey.task.pay.by.platform接口是真正付款的接口,是一个系统接口。可以针对不同的用户,支付不同的金额。需要创建者的sns_user_id或者支付宝id,以及收款用户的支付宝id和sns_user_name,收款用户可以不绑定帐号。支付宝id可以通过taobao.user.get获取(app授权后可用)。

在测试时,开始少传递了return_url和notify_url,则返回ILLEGAL_ARGUMENT错误。

该接口调用时,有以下几点需要注意的:

  1. 请求参数必须使用POST方式传递
  2. 返回分为同步和异步两种:
    1. 同步返回:仅表示支付宝是否成功受理该请求,若成功,第三方可以修改支付为“处理中”的状态;若失败,则代表参数有问题,或者金额不足等,需要自行处理
    2. 异步返回:至notify_url。表示赏金是否支付成功,第三方可修改支付为“已支付”状态。并且,第三方必须打印“success”作为响应,否则支付宝会多次调用notify_url,直至超时。

解释下参数:

Args:outer_task_id – 对应create里的outer_task_id;outer_account_id – 创建者的第三方用户id;alipay_user_id – 创建者的支付宝id;transfer_detail – 字符串,放款明细。记录数不超过1000。 多条记录之间用(*@|$)分割, 记录内部属性之间用(~*@^)分割。 具体内容: 赏金分配流水号~*@^打款金额 ~*@^支付宝用户号~*@^合作网站用户号。其中,赏金分配流水号由第三方自行指定;打款金额以元为单位,精确到分。支付宝用户号是收款人的支付宝id;合作网站用户号是收款人的第三方用户号,如果绑定的话,需要跟绑定时传入的sns_user_id一致。

异步返回给notify_url的数据是分条的。及transfer_detail中若含有N条数据,则会异步返回N次。异步返回成功后,可以在创建者绑定的支付宝帐号的账单明细中看到支付记录。

注意:失败时返回的错误提示码并不是很友好,比如,当传入的收款人不存在时,返回的是ILLEGAL_SIGN。并且如果传入的第三方用户号与支付宝用户号不一致的话,也报ILLEGAL_SIGN。

注意2:传入的notify_url中,不可以有GET参数!比如http://abc.com/ri.php?action=notify,否则支付宝会解析为:“http://abc.com/ri.php?action=notify,charset=utf-8,params={name=sign,”

最后,感谢支付宝合作方的各种帮助,我在开发过程中,提了N多的傻问题,不好意思了 :(

tokyo cabinet与tyrant一起,组成了一个可用的NoSQL数据库,cabinet是lib,tyrant是server。其性能和应用性毋庸置疑,希望可以学习,同时也对其稳定性持怀疑态度,希望可以看看是我用的不对,还是其源码有可改进的地方。

从tyrant/ttserver.c的main函数入手:

main(){
    ……
    g_serv = ttservnew();
    int rv = proc(dbname, host, port, thnum, tout, dmn, pidpath, kl, logpath,
                  ulogpath, ulim, uas, sid, mhost, mport, rtspath, ropts,
                  skelpath, mulnum, extpath, extpcs, mask);
    ttservdel(g_serv);
    if(extpcs) tclistdel(extpcs);
    return rv;
  }
}

省略的部分是命令行参数处理,ttservnew函数对TTSERV进行初始化,其中初始化了4个锁:

ttservnew(){
    ……
    if(pthread_mutex_init(&serv->qmtx, NULL) != 0) tcmyfatal("pthread_mutex_init failed");
    if(pthread_cond_init(&serv->qcnd, NULL) != 0) tcmyfatal("pthread_cond_init failed");
    if(pthread_mutex_init(&serv->tmtx, NULL) != 0) tcmyfatal("pthread_mutex_init failed");
    if(pthread_cond_init(&serv->tcnd, NULL) != 0) tcmyfatal("pthread_cond_init failed");
    ……
}
typedef struct _TTSERV {
    ……
    pthread_mutex_t qmtx;                  /* mutex for the queue */
    pthread_cond_t qcnd;                   /* condition variable for the queue */
    pthread_mutex_t tmtx;                  /* mutex for the timer */
    pthread_cond_t tcnd;                   /* condition variable for the timer */
    ……
}

这里就牵涉到两个重要的概念,timer线程和worker线程,稍候会介绍。qmtx和qcnd主要是供主线程和worker线程使用,tmtx和tcnd主要供主线程和timer线程使用。

可以看出,真正的处理逻辑是在proc函数里。这个函数里进行了大量的稳定、log、容错等处理,先都忽略(但是这些才保证了一份好代码的存在!)。

proc函数中主要进行了以下工作:

  1. 如果以daemon模式运行,则调用ttdaemonize函数,建立可运行的子子进程,父进程和子进程都退出
  2. tcadbopen函数,根据数据库文件,为TCADB对象赋值,对于hash类型的db而言,会将数据库文件mmap到共享内存中
  3. 调用ttservaddtimedhandler函数,建立一个tserv的timer对象,其handler是do_slave
  4. 如果有外部脚本相关的,则再调用ttservaddtimedhandler函数,建立N个timer对象,其handler是do_extpc
  5. 调用ttservsettaskhandler函数,将tserv的do_task赋值为do_task函数
  6. 调用ttservsettermhandler函数,将tserv的do_term赋值为do_term函数,处理退出
  7. do{ ttservstart(); }while(g_restart); 之类的g_restart值可以有signal改变,从而使ttserver可以再次执行ttservstart,以达到重启、reload的效果
  8. 在ttservstart函数中,是真正的业务逻辑

以下简单看看ttservstart函数:

  1. 根据host和port,建立socket,可以是unix原始套接口,也可以是TCP套接口
  2. 新建timer个数个线程,等待执行timer->do_timer函数
  3. 新建thnum个worker线程,以空的reqs初始化,等待执行timer->do_task函数
  4. epoll_wait:
    1. 如果是请求建立连接,则accept连接,并将accept的socket端口加入到epoll的监听队列里
    2. 如果是其他请求,则请求qmtx锁,将该端口push到serv->queue里,释放qmtx锁,并发送qcnd信号,触发worker线程

worker线程的主体就是do_task函数,其读取socket中的数据,如果以magic word打头,则是二进制格式的命令;否则就是memcached兼容的命令格式。根据请求内容,再分别调用如do_put、do_get等函数进行处理。

以do_get函数为例,主要是调用tcadbget函数,读取到内容,再通过socket返回给客户端。这里的tcadbget函数,会根据数据库的类型,调用其get函数进行读取,比如hash database,就使用tchdbget方法。tchdbget中,首先请求HDBLOCKMETHOD 的read lock(允许其他读,但是不允许写),对整个db加锁,然后调用tchdbbidx函数计算出key对应在hash数组中的位置,和二次hash的key(为了解决hash碰撞,对于第一次hash key相同的元素,使用tree再进行存储)。调用tchdbgetimpl函数,进行真正的获取数据。

tchdbgetimpl函数中:由于hash database使用了on-memory hash database作为缓存,所以,如果cache不为空,则先会执行tcmdbget在cache中查找,如果找到则返回,否则继续在真正的hash database里查找。首先找到bucket的偏移位置,对于hash database来说,一部分数据会被map到共享内存,如果bucket属于这部分数据,则从map中读取;否则从数据文件读取(可能存在bucket的一部分数据在map,另一部分在数据文件的情况)。

bucket的header数据结构为:

magic word | hash | left child off | right child off | padding size | size of key | size of value |

buckey里的数据是以树存储的,用上面计算出的二次hash的key,与bucket key比较,若二次key较小,则向左子树找;若较大则向右子树找;否则相等时,比较关键字,若相等,则找到结果,返回;如果虽然二次hash的值相等,但是比较关键字不等,则需要解决二次 hash的碰撞。

二次hash碰撞的解决方法,是沿着当前节点的左子树或者右子树下行直至找到二次hash值等、关键字也等的节点,或者找不到匹配的节点,则返回失败。

———————————- 小分割 ——————————————

2012-1-19

今天想解决3个问题:

  1. 复制数据库文件,与tcrmgr copy的区别;后者对锁的使用情况
  2. B+tree数据文件格式,为什么异常退出会导致数据丢失?
  3. 尝试用c代码读取ulog和数据文件的内容
tcrmgr copy的奥秘

tcrmgr copy的响应函数是proccopy,首先tcrdbnew数据库,并init了一个mutex锁,继而调用tcrdbcopy函数,先请求这个mutex锁,再进行具体的copy操作。

这里请求mutex锁的tcrdblockmethod函数,在多处被调用,比如get、put等等。这里是简单的CLI模式,每次数据库都是new的,mutex锁也是新建的,但是可以猜想,在多线程模式下,线程间的读写操作都是互斥的。(这里存疑,为什么用互斥锁,而非条件变量呢?)

tcrdbcopyimpl组装socket的copy包,通过tcrdbsend发送给ttserver。并通过ttsockgetc获取返回值。

可以看出,copy的奥秘不在tcrmgr这里,而是在ttserver怎样处理copy请求里。

ttserver.c的do_copy函数具体处理copy请求,针对B+tree,其实际处理函数为tcbdbcopy。

首先对数据库加写锁,排除一切的读写操作,从cache里读取叶节点和非叶节点,分别存储到lids和nids里。解锁。由于cache里的数据还未写入到数据文件里,所以针对每个节点,调用对应的save函数,写入到数据文件中(写入每个节点的前后加写锁、解锁)。

调用tcbdbtranbegin启动事务,加读锁,调用tchdbcopy函数(据说B+tree的底层存储其实是hash格式)将数据写到目的文件中,解锁。

实际执行复制操作的是tchdbcopyimpl,这里封装了两种操作,一种是直接调用tccopyfile进行复制,另一种是当目的文件以@开头,则执行命令。仅关注前者,很简单,就是将数据库文件中的内容读取出来,写入到目标文件。

那么tcrmgr copy和直接copy有哪些区别呢?

  • tcrmgr copy会先将cache中的数据写回到文件中;而直接copy不会,所以cache中的数据就丢失了
  • tcrmgr copy写备份文件的时候,会加读锁,排斥写,保证备份文件的数据格式是正确的;而直接copy时没有锁,如果正好遇到cache中的数据同步到数据库文件,则备份文件的格式可能是损坏的
B+tree数据库异常情况分析

从上面的copy过程,猜想,异常退出导致文件格式损坏的原因可能有:

  • 数据写在cache里,没有存入文件,cache里的新数据丢失
  • 存入文件过程中,异常退出,导致文件格式损坏,可能很多数据甚至整个文件都会丢失

具体需要看tcbdbputimpl函数。在该函数中,首先从history里查找key对应的叶节点的pid,如果找不到则从根节点开始二分查找,直至找到或者新建了一个leaf节点的pid(细节没看)。然后将新内容作为rec写入pid对应的leaf位置。之后如果需要,会对整个树进行调整

如果该节点所在的recs数目超过了启动ttserver时的预设值lmemb,或者该节点的的size超过了lsmax,则调整。调整的具体过程忽略,可以参考算法中的B+ tree部分。

否则,如果该节点的recs数目<1,则说明put失败,需要调用tcbdbleafkill函数切断该leaf节点与树的联系(疑问,这个leaf里应该还有其他旧数据,岂不是也丢失了?)。

如果不在事务里,则将数据写入cache。如果页节点的cache数目大于设定值lcnum,则需要将叶节点的cache内容写入到数据文件(又调用了hash的put函数,不清楚那边是否也有cache缓存),并清除叶节点cache的内容。同样处理非叶节点。那么,如果非正常退出,cache中的数据就有可能会丢失。

在将数据写入数据文件的过程中,注意到其rbuf的大小是BDBPAGEBUFSIZ,即32768,这就意味着,一个leaf的最大字节数是32768(数据部分应该是比这个要小)。

留个问题,cache里的数据难道不存在于数据文件中吗?

在csdn上看到有人问:mysql_connect后是不是非要mysql_close,是不是自动释放?

大家伙们,PHP数据库连接是不是在页面执行完之后自己释放,即使不用mysql_close()也可以释放掉?
同理PDO连接到数据库后,是不是非要将PDO实例赋值为NULL,才能将PDO连接断掉?

在N久以前我查过资料,但己经模糊不清了,具说PHP处理每一个被请求的页面时,里面有MYSQL连接的,当页面处理完就立刻释放掉所有用到的资料。如:mysql_connect后的连接。这个说法对吗?PDO同理吗?

第一反应是CGI模式下,mysql_connect是当前请求结束自动释放,而mysql_pconnect是可以被同一web进程(线程)中的多个请求重复使用的。但是为什么呢?

查看php.net上的帮助:

mysql_pconnect() acts very much like mysql_connect() with two major differences.

First, when connecting, the function would first try to find a (persistent) link that’s already open with the same host, username and password. If one is found, an identifier for it will be returned instead of opening a new connection.

Second, the connection to the SQL server will not be closed when the execution of the script ends. Instead, the link will remain open for future use (mysql_close() will not close links established by mysql_pconnect()).

This type of link is therefore called ‘persistent’.

首先,mysql_pconnect建立的链接不会在script结束后释放,那么对于CLI模式呢?从道理和实践代码中检测,都是会释放的。因为进程退出时,所有的资源都会被自动回收。通过以下的代码可以简单的验证:

$db = mysql_pconnect(‘localhost’, ‘mysql’, ‘pwd’);
sleep(100);
var_dump($db);
exit;
同时在mysql中执行:
mysql> show processlist;  # sleep期间
+—-+——-+—————–+——–+———+——+——-+——————+
| Id | User  | Host            | db     | Command | Time | State | Info             |
+—-+——-+—————–+——–+———+——+——-+——————+
|  1 | mysql | localhost:63950 | tshare | Sleep   |    1 |       | NULL             |
| 68 | mysql | localhost       | NULL   | Query   |    0 | NULL  | show processlist |
| 73 | mysql | localhost       | NULL   | Sleep   |   20 |       | NULL |
+—-+——-+—————–+——–+———+——+——-+——————+
3 rows in set (0.01 sec)
mysql> show processlist;  # 进程结束
+—-+——-+—————–+——–+———+——+——-+——————+
| Id | User  | Host            | db     | Command | Time | State | Info             |
+—-+——-+—————–+——–+———+——+——-+——————+
|  1 | mysql | localhost:63950 | tshare | Sleep   |    0 |       | NULL             |
| 68 | mysql | localhost       | NULL   | Query   |    0 | NULL  | show processlist |
+—-+——-+—————–+——–+———+——+——-+——————+
2 rows in set (0.00 sec)

那么CLI模式呢?将上面这段代码cp至一个web页面中,通过浏览器访问结束后,show processlist仍然可以看到有连接存在!这就使得多个web请求可以复用同一mysql链接,从而节省了建立连接的过程。但是这样也可能造成问题,就是当允许的web进程(线程)数目超过mysql的max_connections时,会造成web进程连接mysql服务器的阻塞,因为每个web进程都不释放自己的mysql连接,从而可能造成too many connections的mysql错误。可以参考我之前的blog:http://flykobe.com/index.php/2011/01/20/mysql-too-many-connections%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/

那么它会存在多久呢?与mysqld的wait_timeout相关。如果一直没有被复用,那么它会存在wait_timeout时长,单位是秒。

mysql> show variables like ‘wait_timeout’;
+—————+——-+
| Variable_name | Value |
+—————+——-+
| wait_timeout  | 28800 |
+—————+——-+
1 row in set (0.00 sec)
当然,如果web server的进程(线程)结束,mysql_pconnect建立的连接也会结束。

我们在淘宝开放平台上有一款店铺模块,顾名思义,其分为前台和后台模块两部分。前台模块展现在淘宝卖家的店铺页面里,比如店铺首页、列表页、宝贝详情页等。后台模块仅供购买了该APP的商家使用。

根据淘宝开放平台的规则,前台模块任何人可见,后台模块仅购买该APP的商家可见。这里的可见,即授权成功,由淘宝开放平台控制,仅当授权成功后,才会将控制权交到我们的APP上。

这里比较特殊的一点是,如果用子帐号,如“开心赚宝:开心分享”,登录淘宝,店铺后台模块是无法授权成功的。而同时,其访问店铺页面,又会以edit的模式来请求店铺后台模块!这样就导致了BUG:

父帐号未授权或应用不支持子帐号
 错误码:122

针对这个bug,个人认为目前开发者是没有办法的,只能依赖淘宝平台修复。

可能还是对前端理解不深入,总是觉得css、html是靠死记硬背,而非理解之后使用的。所以老记不住其中的一些东西。今天下决心要搞明白浮动和清理,以及浏览器兼容的各方面事情!

浮动

在w3c的文档里,如此描述浮动:

浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。由于浮动框不在文档的普通流中,所以文档的普通流中的块框表现得就像浮动框不存在一样。

这个导致的效果是,如果不清除浮动,则:

  • 若父元素里仅包含浮动的子元素,则父元素的宽、高会为0
  • 若子元素A为浮动,B不浮动,则B与A会重叠
  • 若子元素A、B都浮动,则顺序排列

一个div类型的块状元素,本来会占满100%的宽度,浮动后,就会自适应,仅占所需的区域了。

这样的代码:

<div>
       <div style="float:left">left</div>
       <div style="float:right">right</div>
       <div>non float</div>
</div>

产生的效果为:

可见第三个div占满了整个宽度,但是内容还是贴着左浮动的右边框了。

这时可以先清除浮动。

浮动的清理
<div>
    <div style="float:left">left</div>
    <div style="float:right">right</div>
    <div style="clear:both"></div>
    <div style="border:1px solid red;">non float</div>
</div>
产生的效果为:
浮动与块元素
所有被定义为浮动的元素都会被自动变为“块状元素”,也就是代表它可以定义宽度和高度了。
我遇到过的问题

1、多个浮动元素超出父元素宽度时,会折行显示,即超出部分会在下一行显示

2、最好在每组浮动结束后都清除掉浮动,否则会出现莫名其妙的问题。如果在firebug中看到某个父元素的height、width为0,就要注意了

———————————————

绝对定位的问题

子元素A一旦要做绝对定位,就需要将其父元素的postion显示指定,否则在不同浏览器下,会有不同的效果。

这时,需要注意父元素的overflow属性,这将决定着子元素若超出父元素的范围,是否可以显示。可以结合z-index一起使用。

该文章发布时间为2011年12月14日,支付宝的接口之后可能发生变化,本文仅供参考。

支付宝悬赏接口目前仅支持DSA签名方式。

测试阶段:

支付宝会提供4个key给开发者,即合作网站私钥、合作网站公钥、支付宝私钥、支付宝公钥,作为合作网站,我们真正使用的是合作网站私钥和支付宝公钥。在向支付宝发送请求时,使用合作网站私钥对url参数进行签名;在接收支付宝的返回时,使用支付宝公钥验证返回的内容。

php使用openssl_pkey_get_private将字符串形式的私钥转化为可用的resource。这里,由于dsa签名有多种,比如1026bit、是否encrypt之类,所以需要获悉支付宝在生成该私钥时使用的命令参数。经过网上搜索获悉:

1. 生成 DSA 参数
openssl dsaparam -out dsa_param.pem 1024
2. 生成 DSA 私钥
openssl gendsa -out dsa_private_key.pem dsa_param.pem
3. 生成 DSA 公钥
openssl dsa -in dsa_private_key.pem -pubout -out dsa_public_key.pem
4. 将 DSA 私钥转换成 PKCS8 格式
openssl pkcs8 -topk8 -inform PEM -in dsa_private_key.pem -outform PEM -nocrypt
生成的私钥文件如下:
—–BEGIN PRIVATE KEY—–
第一行64个字符
……
第七行64个字符
—–END PRIVATE KEY—–

支付宝提供的密钥若没有换行符,可自行进行换行,并添加头尾,以适合该格式。

支付宝文档里提供的DSA签名命令为:

openssl dgst  -dss1 -sign dsa_private_key.pem  -out dsasign.bin plaintext.tx

可见,其使用的签名算法是dss1,对应到php中就是OPENSSL_ALGO_DSS1,故签名步骤:

//除去待签名参数数组中的空值和签名参数

$querys = $this->paraFilter($querys);

// 排序

$querys = $this->argSort($querys);

$querys['sign'] = $this->sign($this->createLinkstring($querys), $this->private_key);

$querys['sign_type'] = ‘DSA’;

function sign($data, $private_key) {

$res = openssl_pkey_get_private($private_key);

openssl_sign($data, $sign, $res, OPENSSL_ALGO_DSS1);

openssl_free_key($res);

//base64编码

$sign = base64_encode($sign);

return $sign;

}

在使用openssl族的函数进行开发时,可使用如下代码进行调试,查看openssl的出错信息:

          $res = openssl_pkey_get_private($private_key);

          // Show any errors that occurred here

          while (($e = openssl_error_string()) !== false) {

              echo $e . "\n";

          }

          var_dump($res);
开发阶段:

使用openssl函数或者命令,生成合作网站自己的私钥和公钥,将公钥提供给支付宝。并从支付宝处获取他们提供的支付宝公钥,即可。

最近学别人,搞了个流式布局的页面,使用了kissy框架。但是该框架要求提前知道img的height,否则若加载图片过慢,会造成图片重叠。

对于前端不熟悉,所以采取了提前获取图片真实高度的方法。

网上搜到了获取jpg图片信息的方法,基本原理是,图片的基本信息如宽、高、size等信息都是存储在文件的前几十个字节里的,所以不用获取全部文件,仅分析前面几十个字节并解析就可以获取这些信息了。

// 获取图片尺寸: 图片的信息都存储的前面几个字节里,获取并分析即可获取尺寸信息(但是我不懂的……)
function getjpegsize($img_loc) {/*{{{*/
        if (!$img_loc){
                return false;
        }
        $handle = fopen($img_loc, "rb");// or die("Invalid file stream.");//die == exit
        $new_block = "";
        if(!feof($handle)) {
                $new_block = fread($handle, 32);
                $i = 0;
                if($new_block[$i]=="\xFF" && $new_block[$i+1]=="\xD8" && $new_block[$i+2]=="\xFF" && $new_block[$i+3]=="\xE0") {
                        $i += 4;
                        if($new_block[$i+2]=="\x4A" && $new_block[$i+3]=="\x46" && $new_block[$i+4]=="\x49" && $new_block[$i+5]=="\x46" && $new_block[$i+6]=="\x00") {
                                // Read block size and skip ahead to begin cycling through blocks in search of SOF marker
                                $block_size = unpack("H*", $new_block[$i] . $new_block[$i+1]);
                                $block_size = hexdec($block_size[1]);
                                while(!feof($handle)) {
                                        $i += $block_size;
                                        $new_block .= fread($handle, $block_size);
                                        if($new_block[$i]=="\xFF") {
                                                // New block detected, check for SOF marker
                                                $sof_marker = array("\xC0", "\xC1", "\xC2", "\xC3", "\xC5", "\xC6", "\xC7", "\xC8", "\xC9", "\xCA", "\xCB", "\xCD", "\xCE", "\xCF");
                                                if(in_array($new_block[$i+1], $sof_marker)) {
                                                        // SOF marker detected. Width and height information is contained in bytes 4-7 after this byte.
                                                        $size_data = $new_block[$i+2] . $new_block[$i+3] . $new_block[$i+4] . $new_block[$i+5] . $new_block[$i+6] . $new_block[$i+7] . $new_block[$i+8];
                                                        $unpacked = unpack("H*", $size_data);
                                                        $unpacked = $unpacked[1];
                                                        $height = hexdec($unpacked[6] . $unpacked[7] . $unpacked[8] . $unpacked[9]);
                                                        $width = hexdec($unpacked[10] . $unpacked[11] . $unpacked[12] . $unpacked[13]);

                                                        $stream_meta_data = stream_get_meta_data($handle); //这里可以得到文件大小
                                                        //print_r($stream_meta_data);
                                                        $result['width']=$width;
                                                        $result['height']=$height;
                                                        foreach($stream_meta_data['wrapper_data'] as $va){
                                                                if(preg_match("/length/iU",$va)){
                                                                        $ts=explode(":",$va);

                                                                        $result['size']=trim(array_pop($ts));
                                                                }
                                                        }
                                                        $stream_meta_data = NULL;
                                                        $new_block=NULL;
                                                        fclose($handle);
                                                        return $result;
                                                } else {
                                                        // Skip block marker and read block size
                                                        $i += 2;
                                                        $block_size = unpack("H*", $new_block[$i] . $new_block[$i+1]);
                                                        $block_size = hexdec($block_size[1]);
                                                }
                                        } else {
                                                fclose($handle);
                                                return false;
                                        }//\xff
                                }//while
                        }//newblock
                }//$new_block[$i]
        }//feof
        fclose($handle);
        return false;
}/*}}}*/

当获取到这些信息之后,按照显示尺寸进行修改:$ret['height'] = intval($pic_info['height'] * 192.0 / $pic_info['width']);

这样,显示时img代码如下:

<img src=”<?php echo $item['pic_url'];?>_250×250.jpg” width=”192″ <?php echo empty($item['height'])? ‘min-height=100′: ‘height=”‘.$item['height'].’”‘;?> >

今天在火丁笔记上看到介绍了Linux运维利器之ClusterShell,试用了一次,正好符合我上线代码的需求!

我们的环境如下:2台db server,4台web server,全部得通过跳板机登录,且跳板机不转发ssh命令(至少我目前没发现怎样操作),之前仅有2台web server的时候,我都是通过上线脚本,操控svn进行代码的更新。但是现在增长到4台之后,就显得麻烦了!

利用clustershell正好可以解决这个问题!在其中一台server上(我选择了一台web server),安装clustershell(它依赖于python),并且建立conf文件,在/etc/clustershell/groups中配置db和web服务器组:

[admin@v080027 clustershell-1.5.1]$ cat /etc/clustershell/groups
db: 192.168.80.103 192.168.80.104
web: 192.168.80.105 192.168.80.106 192.168.80.27 192.168.80.49
并且建立该server到其他机器的免密码ssh登录方式。

这样,就可以方便的管理其他服务器了!

交互的问题:因为上线前,都是要先比对新旧代码,确定无误后,输入“yes”,才正式merge代码的!由于上线脚本是自己写的,会read stdin的yes or no,所以只需要echo “yes” | ./push.sh 即可。当然,在这之前,需要先进入某一台服务器,查看push.sh的diff输出,确定无误!

虽然简陋,但是暂时可以满足我的需求。所以不再进一步学习了