不同数据中心同步处理

很久之前看过facebook关于不同数据中心之间的延迟处理。

目前我们也遇到类似的问题。这次我们使用的是mongodb。
由于国内恶劣的环境和昂贵的价格,我们被迫将一些大量消耗带宽的服务迁移到国外的主机。这导致了大量的网络延迟。
大概200-400ms。 这样,仅仅依靠mongodb的同步是无法解决数据的一致性了。

由于数据同步量很大,又通过VPN,主数据中心和国外同步的时间周期要以分钟甚至小时计算了。

没办法,只能土法了。

当前解决方案是:

1. 数据一律从国内dc进行更新,此时设置更新数据状态为未同步
2. 数据更新后,发送一个消息到国外dc的队列,对应的worker则开始监控此数据的同步情况
3. 数据未同步前,所有的数据均从国内的主数据进行分发处理
4. 同步完毕,国外dc通知国内dc,将数据状态更改为已同步
5. 一旦同步,后续的数据处理工作自动转发到国外,从而实现分流

缺点:

监控worker和队列的工作状态需要监控。否则会导致流量无法分发。

虽然是土法,但也能解决眼前的问题。

Non-blocking/Evented/IO 将会改变我们传统的编程模式

最近Node.js 很火, 我对此很有兴趣,我自己也在使用Perl的AnyEvent。
Event/IO的一个要点是non-blocking。 Nginx能够解决c10k的问题,一个关键是使用
Event Loop取代传统的thread/pre-fork。要想充分发挥nginx的优势,需要后端和对应
模块都能够non-blocking才行。

然而,现实多数的架构都是blocking的,比如PHP/FastCGI, 即便nginx的前端再好,后端的应用
依然是瓶颈。

所以,如何使用Event/IO实现non-blocking编程,是我们要关注的一个领域。

Ruby的EventMachine, Python的Twisted, Perl的AnyEvent都是不错的开始。

Node.js则是另一个选择。 Javascript似乎是天生的适合作Event/IO的编程语言。 上手轻松,简单。

对于我们来说,最困难的是开始,如何适应新的基于callback的处理模式,这是最大的挑战。

在未来一段时间内,我将把注意力集中到node.js和AnyEvent. 对于我目前正在规划的视觉中国C10平台中,这将是一个非常有趣解决方案。

推荐关于node.js的slide:

http://nodejs.org/cinco_de_node.pdf

Why You Should Pay Attention to Node.Js

Gearman::Driver是不错的Geamman worker管理器

最近发现了Gearman::Driver。可以方便的编写gearman的worker。由于使用了Moose和POE,扩展起来相当简便。比如可以对某个class的method
添加一个Job attribute,就能够实现worker了:

package # hide from PAUSE
Gearman::Driver::Test::Live::RuntimeOption;

use base qw(Gearman::Driver::Test::Base::All);
use Moose;

sub job1 : Job :MinProcesses(2): MaxProcesses(10) {
my ( $self, $job, $workload ) = @_;
return $self->foo;
}

1;

job1就自动注册为:’ Gearman::Driver::Test::Live::RuntimeOption::job1′ 了。MinProcesses, MaxProcesses则定义worker要spawn的进程数。

Gearman::Driver还实现了一个console,通过telnet可以动态的更改worker的进程数,非常的方便。

美中不足的是,Gearman::Driver的worker 无法通过配置文件来配置,在部署的时候很不方便。 对于一些运行时要读取某些配置的worker,
只能通过hard code或者$ENV来传递。

于是,我fork后做了写改进,添加了一些选项,启动时使用–configfile就可以从对应的配置文件读取了。

这是我github上fork的Gearman::Driver

ip_conntrack: table full, dropping packet.

流量比较大的服务器上经常出现 “ip_conntrack: table full, dropping packet.” 这类的信息。
由于CentOS/Redhat默认的ip conntrack参数值比较低,要做一些调整:

在/etc/sysctl.conf
net.ipv4.ip_conntrack_max = 131072
在/etc/rc.local 加入:
echo 16384 > /sys/module/ip_conntrack/parameters/hashsize

然后
sysctl -p
echo 16384 > /sys/module/ip_conntrack/parameters/hashsize

这样就调高了ip conntrack table的大小和对应优化ip conttrack模块的hash 大小,对于网络应用链接多的场合适用。

要查看当前链接数:

cat /proc/sys/net/ipv4/netfilter/ip_conntrack_count

node.js-高效的javascript evented I/O framework

node.js是最近时期比较热门的主题。
这个基于V8的javascript io framework,由于是evented,性能很高,符合当前低炭高效的主旋律。
对于那些有大量并发链接的场景,不妨尝试一下。

node.js上手非常容易,因为javascript估计几乎每个web开发人员都或多或少知道。
node.js也证明了server side js是大有前途。当然,这里的所指的js开发者应该是那些真正熟悉
javascript的开发者,对于那些仅仅当作前端脚本的人来说,需要补课的地方还是非常多的。

在node.js基础上,派生了很多的framework和许多有趣的东西,这里列出一些node.js相关的资料:

node.js

关于nodejs的howto

nodejs的一些相关的项目和资料

Mongo DB Native NodeJS Driver

Non-blocking I/O, Event loops and node.js

在CentOS/Redhat 5.4上使用openswan搭建数据中心之间的VPN

最近由于主数据中心的流量过大,为了分流和成本,在国外购买了几个独立主机。这样就需要在2个数据中心之间进行
数据的同步,包括mysql,mongodb,以及反向代理等。通过在使用IPSec创建VPN从而将2个子网连接在一起是
最合适的方案。

在最初曾经考虑过使用OpenVPN,但是由于我们的应用涉及到较大的数据传输,OpenVPN的性能相对于IPSec的方案还是差,
我们也无法通过为openvpn部署硬件加速。出于成本考虑,最后选择纯软的vpn。

首先尝试使用CentOS自带的IPSec进行配置,不过很快失败。RedHat的文档描述的很不清楚,此外,配置比较繁琐,
按照其说明配置也无法连通,实在没有耐心,于是改用openswan来部署。

在部署openswan之前,先要简单画出2个子网的拓扑图,只要分出“左”《-》“右” 即可:

A(192.168.8.x/211.x.x.x) +————+ B(192.168.9.x/174.xx.xx.xx)

按照上图,A为left 节点,B 为 right节电。192.168.8.x和 192.168.9.x分别为2个网络的网段。
在2个网段分别选择一个主机作为网关,这样分别设置这个2个网关就可以了。
实际为:
gate_a(192.168.8.70/211.x.x.x) , gate_b(192.168.9.1/174.x.x.x)

这2个host都需要有2个网卡,分别是链接内网和公网,记住上面的内网和公网ip。

下面开始安装:(gate_a)

1. 安装openswan
gate_a.nightsailer.com> yum install ipsec-tools
gate_a.nightsailer.com> yum install openswan

注意:下面这步很关键,CentOS-5.4有个bug,安装openswan时自动创建cert db并不正确,所以首先需要
重新创建,否则下一步生成key的时候就会报错。

gate_a.nightsailer.com>certutil -N -d /etc/ipsec.d

2. 现在,为gate_a生成密钥:
gate_a.nightsailer.com>ipsec newhostkey –configdir /etc/ipsec.d/ –output /etc/ipsec.d/keys.secrets

3.显示输出gate_a 作为left的公钥:
gate_a.nightsailer.com>ipsec showhostkey –left
ipsec showhostkey nss directory showhostkey: /etc/ipsec.d
# rsakey Axxxx
leftrsasigkey=0sA………

将eftrsasigkey=0sA….. 这行输出记下,后面备用

4. 修改/etc/ip_sec.conf

# /etc/ipsec.conf – Openswan IPsec configuration file
#
# Manual: ipsec.conf.5
#
# Please place your own config files in /etc/ipsec.d/ ending in .conf

version 2.0 # conforms to second version of ipsec.conf specification

# basic configuration
config setup
# Debug-logging controls: “none” for (almost) none, “all” for lots.
# klipsdebug=none
# plutodebug=”control parsing”
# For Red Hat Enterprise Linux and Fedora, leave protostack=netkey
protostack=netkey
nat_traversal=yes
virtual_private=
oe=off
# Enable this if you see “failed to find any available worker”
nhelpers=0

#You may put your configuration (.conf) file in the “/etc/ipsec.d/” and uncomment this.
include /etc/ipsec.d/*.conf

5. 现在为这个vpn单独创建一个conf文件,放到/etc/ipsec.d/nightsailer.com_vpn.conf

conn nightsailer_vpn
#左节点的公网ip(gate_a)
left=211.x.x.x
#左节点的内网网段
leftsubnet=192.168.8.0/24
#左节点的网关的内网ip(可选)
leftsourceip=192.168.8.70
#左节点的id,可以是ip,也可以是域名:
# leftid=@gate_a.chinavisual.com
# 建议最好用ip,域名需要反向解析,dns没配好很容易出问题
leftid=211.x.x.x
# 上一步显示的gate_a作为left的公钥
leftrsasigkey=0sAQO…
leftnexthop=%defaultroute
#以下参数含义和上面的一样
right=174.x.x.x
rightsubnet=192.168.9.0/24
rightsourceip=192.168.9.1
rightid=174.x.x.x
rightrsasigkey=0sAQOp….
rightnexthop=%defaultroute
#是否在ipsec启动时自动启用这个链接
#auto = add ( 若这个选项,则需要手动up vpn链接)
auto=start

现在,登录到gate_b,重复上面的步骤1-5。

注意,第3步略有不同,因为gate_b是right节点,所以要显示它的right 密钥:
gate_b.nightsailer.com>ipsec showhostkey –right
ipsec showhostkey nss directory showhostkey: /etc/ipsec.d
# rsakey Axxxx
rightrsasigkey=0sA………

将rightrsasigkey=0sA….. 这行输出更新/etc/ipsec.d/nightsailer.com_vpn.conf(包括gate_a上的)

现在分别启动ipsec
gate_a.nightsailer.com>/etc/init.d/ipsec start
gate_b.nightsailer.com>/etc/init.d/ipsec start

好了,现在在gate_a和gate_b上相互ping,就可以ping通了。

网关通了,那么还需要再各自子网添加一个路由:
A网段添加:
route add -net 192.168.9.0 netmask 255.255.255.0 gw 192.168.8.70 eth1
#添加静态路由
在 /etc/sysconfig/network-scripts/route-eth1 追加:
192.168.9.0/24 via 192.168.8.70 dev eth1

B网段添加:
route add -net 192.168.8.0 netmask 255.255.255.0 gw 192.168.9.1 eth1
在 /etc/sysconfig/network-scripts/route-eth1 追加:
192.168.8.0/24 via 192.168.9.1 dev eth1

一切就大功告成了。

快速恢复mysql master-master同步

系统出现错误导致2个Master之间出现不一致。需要立刻同步。
由于错误出现的时间超过了binlog的保留范围(已经被清除),所以只能重新做。

1. 用mmm_control 关闭对应节点
mmm_control set_offline db11
mmm_control set_offline db2

2. 重置master/slave(db2,db11)
reset master;
reset slave;
slave stop

3. 复制最新的master到后备master(from db2 to db11)
使用 mk-parallel-dump和mk-parallel-restore 安全,快速。
xtrabackup 也可以,但不够快速和安全,且需要各个节点配置完全一样。

4. 重新启用replication
db2:
change master to master_host=’192.168.8.11′,master_user=’xxxxxx’,master_password=’xxxx’, master_log_file=’mysql-bin.000001′,master_log_pos=0;
db11:
change master to master_host=’192.168.8.2′,master_user=’xxxxx’,master_password=’xxxx’, master_log_file=’mysql-bin.000001′,master_log_pos=0;

both:
slave start
show master status\G
show slave status\G

5. set db online
mmm_control set_online db2
mmm_control set_online db11

完毕。

令我发狂IE cookie的问题

这个问题基本上可以令人发狂。在 6.0 sp3 091208-2036以后的版本(其他版本也可能有问题)。
如果使用php的set_cookie 如果设置的过期时间比较短,比如180秒后过期,那么cookie将无法创建。
而之前的IE版本以及如Firefox,Chrome,Safari等均没有这个现象。

发现这个原因,是我折腾了一晚上,并且特意弄了台windows,用QQ远程在一个网友的机器上反复折腾,
当临近崩溃的最后一秒发现的。

血泪的教训。 cookie的过期时间最好在1个小时以上,通过将过期时间写入值存入cookie,然后判断,不要依赖浏览器,尤其是
狗屎的IE6.

听说国外有人给IE6举办了葬礼,什么时候国内能有呢,那对于web开发真是个福音。不过,IE7/8对于网银的支持实在
是糟糕透顶,对于我这种mac用户,IE乃至windows的唯一用途就是上网银。。。所以,我的虚拟机用的还是IE6.。。
天大的讽刺!

还好,招商银行支持iphone,方便多了。

PHP:call_user_func_array 导致的诡异事件

最近,在升级原创榜时,发现了Doggy中Dt模版的一个诡异现象。当访问某些页面时,不定期出现Gateway timeout.
PHP没有报任何错误,也没有任何错误日志或者信息输出。我一度认为是模版的Tag有问题,因为当尝试
删除某些Tag后会解决这个问题。好景不长,更多的页面也冒出此类错误,然而当我把模版内部解析缓存关闭后,则不会出现此类错误。 于是转头跟踪Dt模版的内部调用。Dt模版的设计中,当模版第一次被解析后,将模版转换成Doggy_Dt_NodeList对象,并将它缓存到内部cache中。当模版下次被调用时,如果模版没有变动,则直接调用NodeList的render方法,这样可以减少模版解析的时间。然后,当从缓存中被反序列的Doggy_Dt_NodeList,却无法正确执行filter。看下面的一段代码:

if (isset(Doggy_Dt::$filters[$name])) {

foreach ($args as $i => $argument) {
# name args
if (is_array($argument)) {
foreach ($argument as $n => $arg) {
$args[$i][$n] = $this->resolve($arg);
}
}
else {
# resolve argument values
$args[$i] = $this->resolve($argument);
}
}
array_unshift($args, $object);
$object = call_user_func_array(Doggy_Dt::$filters[$name], $args);
}

罪魁祸首是:call_user_func_array(Doggy_Dt::$filters[$name], $args)。

此处调用的call_user_func_array导致无法autoload class。于是php就悄无声息的挂了。

说是诡异,就是因为,如果不是反序列出来的,则能够正常的调用。
此外,在官方手册中,对于call_user_func_array还有一段注释:

Note: Callbacks registered with functions such as call_user_func() and call_user_func_array() will not be called if there is an uncaught exception thrown in a previous callback.

也就是说,如果之前call_user_func_array()导致了未知的异常,那么后续的call_user_func_array则不会被调用。

后来检查,的确模版中也有错误的filter tag。 但,奇怪的是只有是反序列(unserialize)回来的才会重复此类
现象。 因此,未必是完全如注释所说的原因。
我猜测,有可能是call_user_func_array的错误导致了反序列后的对象无法正确再次调用call_user_func_array,至少在call_user_func_array中无法正确autoload class。

解决方法,其实也是修复上面代码的一个bug,在call_user_func_array前增加is_callable判断。
if (isset(Doggy_Dt::$filters[$name]) && is_callable(Doggy_Dt::$filters[$name])) {

}
else {
Doggy_Log_Helper::warn(‘unkown filter:’.$name);
}

于是,世界就太平了。

MongoDB practice: My Perl GridFS Wrapper

简单写了一个Perl版本的GriFS的wrapper:

package CZone::GridFS;
use strict;
use MongoDB::GridFS;
use Path::Class;
use Digest::file qw(digest_file_hex);
use Digest::MD5 qw(md5_hex);
use IO::File;
use Data::Dumper;
use Any::Moose;

has database => (
isa => ‘MongoDB::Database’,
is => ‘ro’,
required => 1
);

has _gridfs => (
isa => ‘MongoDB::GridFS’,
is => ‘ro’,
lazy => 1,
builder => ‘_build__gridfs’,
);

has _file_collection => (
isa => ‘MongoDB::Collection’,
is => ‘ro’,
lazy => 1,
builder => ‘_build__file_collection’
);

sub _build__gridfs {
my $self = shift;
return $self->database->get_gridfs;
}

sub _build__file_collection {
my $self = shift;
return $self->database->get_collection(‘fs.files’);
}

sub get_bytes {
my ($self, $id ) = @_;
my $file = $self->_gridfs->find_one({_id => $id });
my $bytes;
my $fh = new IO::File \$bytes,’>';
$file->print($fh);
return $bytes;
}

sub store_file {
my ($self, $file_path) = @_;
my $file = file($file_path)->absolute;
return undef unless -e $file;
my $md5 = digest_file_hex($file,’MD5′);
my $fh = $file->open(‘r’) or return undef;
return $self->_store_fh($fh,$md5);
}

sub _store_fh {
my ($self,$fh,$md5) = @_;
# $grid_file isa MongoDB::GridFS::File
my $grid_file = $self->_gridfs->find_one({ ‘md5′ => $md5});
if ($grid_file) {
$self->_inc_refs($grid_file->info->{_id});
return $grid_file->info->{_id};
}
else {
my $oid = $self->_gridfs->insert($fh,{
refs => 1,
md5 => $md5,
});
return $oid;
}
}

sub store_bytes {
my ($self, $bytes) = @_;
my $md5 = md5_hex($bytes);
my $fh = new IO::File \$bytes,’<';
# my $fh = FileHandle->new;
# $fh->open(\$bytes,’<');
return $self->_store_fh($fh,$md5);
}

sub unlink {
my ($self, $id ) = @_;
$self->_dec_refs(MongoDB::OID->new(value =>”$id”));
}

sub _inc_refs {
my ($self,$id) = @_;
$self->_file_collection->update({_id => $id },{ ‘$inc’ => { refs => 1}});

}

sub _dec_refs {
my ($self,$id) = @_;
$self->_file_collection->update({_id => $id },{ ‘$inc’ => { refs => -1}});
}

sub gc {
my $self = shift;
$self->_gridfs->remove({refs => 0});
}

no Any::Moose;
__PACKAGE__->meta->make_immutable;
1;

__END__

这是从czone项目中的PHP代码移植过来的。
方便将gridfs中的文件读写到scalar中。同时,通过检查存储文件的md5值,并记录相同文件的引用计数,相同文件只存储一个copy,节省空间。(BSON格式对于空间的需求是非常大的)

Next Page »