PostgreSQL流式复制和pgpool配置

PostgreSQL本身是非常强大而好用的数据库管理软件,可惜国内用的人不多,周边工具支持和中文文档相对于MySQL差了不少。前不久近距离接触了一把PostgreSQL在9.x版本引入的streaming replication和第三方负载均衡工具pgpool,在这里以四台CentOS 6.x服务器(一台frontend/前端、一台master/主、一台slave/从、一台archive/归档)和PostgreSQL 9.3为例,记录下配置过程,供有类似需求的同学参考。

流式复制(streaming replication)用于实现PostgreSQL实例间的数据/日志同步,而pgpool则是一款不错的PostgreSQL前端,让我们能够像访问单机PostgreSQL服务器那样访问PostgreSQL数据库集群,扩展数据库集群的读性能,并在不停止对外服务的前提下实现master-slave切换,从而有机会对集群中任意节点进行单独的升级维护,提高数据库服务器集群的整体可用性。

首先,在master和slave服务器上安装PostgreSQL。对于裸的CentOS 6.x而言,最简单的方式是配置官方安装源[1],然后

yum install yum postgresql93-server
service postgresql-9.3 initdb
chkconfig postgresql-9.3 on

完成后建议记录下PostgreSQL的数据目录( PGDATA=/var/lib/pgsql/9.3/data )和可执行文件目录( PGBIN=/usr/pgsql-9.3/bin ),方便后续配置和维护操作。对于正式环境,我们当然还需要考虑数据容量,甚至是使用定制的路径等,在此略去不表。

其次,在archive服务器上配置rsync服务(可选)。依然以CentOS 6.x为例

yum install rsync xinetd
vim /etc/xinet.d/rsync # 设置 disable = no
mkdir -p /db/pg_archive

vim /etc/rsyncd.conf
    [wal]
        path = /db/pg_archive
        comment = DB WAL Archives
        uid = root
        gid = root
        read only = false
        hosts allow = master,slave # 这里换成masterslave的IP地址
        hosts deny = *

然后,在master上配置PostgreSQL。

vim $PDGDATA/postgresql.conf

    archive_mode = on
    archive_command = 'rsync -a %p archive::wal/%f' # 这里换成archive的IP地址(可选)
    wal_level = hot_standby
    max_wal_senders = 5

vim $PDGDATA/pg_hba.conf

    host   replication   rep_user   xxx.xxx.xxx.xxx/32   trust # 换成slave的IP地址,允许其连接做streaming replication

service postgresql-9.3 restart

以postgres用户执行

echo 'CREATE USER rep_user WITH REPLICATION;' | psql

接下来,在slave上配置PostgreSQL,实现从master/主服务器的流式复制。

mkdir -p /db/pg_archive
vim /etc/cron.d/postgres

    * * * * * postgres flock /tmp/wal_sync rsync -a --del archive::wal/ /db/pg_archive # 换成archive的IP地址

$PGBIN/pg_basebackup -D $PGDATA -h xxx.xxx.xxx.xxx -U rep_user # 换成master的IP地址

vim $PGDATA/recovery.conf

   standby_mode = on
   primary_conninfo = 'host=xxx.xxx.xxx.xxx user=rep_user' # 换成master的IP地址
   restore_command = 'cp /db/pg_archive/%f %p 2>/dev/null'

service postgresql-9.3 restart

回到master,以postgres用户连上PostgreSQL数据库,执行如下SQL检查流式复制是否正常工作(此时应该能看到slave已连接)

SELECT client_addr, usename, state FROM pg_stat_replication;

最后,在frontend上安装和配置pgpool。

yum remove nfs-utils
yum install pgpool-II-93

cd /etc/pgpool-II-93/
cp pgpool.conf.sample-stream pgpool.conf

vim pgpool.conf

    listen_addresses = '*'
    # master
    backend_hostname0 = 'xxx.xxx.xxx.xxx' # 换成master的IP地址
    backend_weight0 = 1
    backend_data_directory0 = '$PAGDATA'
    backend_flag0 = 'ALLOW_TO_FAILOVER'
    # slave
    backend_hostname1 = 'xxx.xxx.xxx.xxx' # 换成slave的IP地址
    backend_weight1 = 1
    backend_data_directory1 = '$PGDATA'
    backend_flag1 = 'ALLOW_TO_FAILOVER'
    num_init_children = 25
    max_pool = 10
    replication_mode = off
    load_balance_mode = on
    master_slave_mode = on
    master_slave_sub_mode = 'stream'
    parallel_mode = off

vim pool_passwd # 该文件的格式为<用户名>:<密码哈希>,可以用pg_md5命令生成指定用户名的密码哈希
vim pool_hba.conf # 参考PostgreSQL的pg_hba.conf

sudo service pgpool start

有关pgpool更详细的配置说明,可参考[2]。

以上配置完成后,我们即可通过PostgreSQL客户端,以9999端口对数据库集群进行访问,写操作会被自动路由至master,而读操作则是被均分到master和slave两台机器上:

psql -h xxx.xxx.xxx.xxx -p 9999 -U xxxx # 换成frontend的地址和真实的数据库用户名

[1] http://www.postgresql.org/download/linux/redhat/#yum
[2] http://www.pgpool.net/docs/latest/pgpool-en.html

CORS笔记

相信有不少服务端开发同学和我类似,初次接触CORS的体验是这样的:你这个服务调不通啊,“跨域”了,要打开CORS。什么?CORS干嘛的?然后有人告知你要在服务端响应header里加上这个、这个和这个,就好了,几乎是口口相传,代码拷贝粘帖,然后bingo,然后就没有然后了。

作为有情怀的服务端开发,怎么甘心被前端同学这么牵着鼻子走呢你说是不是?DISCLAMER:想全面学习CORS请考虑购买《CORS in Action》[1],目前最好的介绍CORS的书。这里只是简单结合实际项目中的心得做个笔记。

CORS的全称是Cross-Origin Resource Sharing,即跨源资源共享,常用于Web前端代码访问服务端暴露的API,比如去flickr加载图片之类。什么是“源”?为什么不说“跨域”?这里要澄清几个概念。首先,参与CORS的角色主要有:JavaScript代码(客户端)、浏览器、服务端。浏览器说白了就是给JavaScript的运行提供一个沙箱,为了保护最终用户的安全,随着网页打开而运行JavaScript代码能做的事情相当受限,其中一个重要的约束就是,JavaScript默认只能对同“源”的服务端资源发起请求。除非服务端特别允许,浏览器会拦截掉JavaScript的跨“源”请求。也就是说,要实现CORS,JavaScript代码、浏览器和目标服务端都必须主动参与,JavaScript要发起请求,浏览器加工header并允许调用,而服务端也必须声明允许来自某个或某些“源”的JavaScript请求,整个调用才会成功。注意这里特别强调的浏览器的作用,如果是用curl之类的命令行工具,CORS调用并没有“浏览器”的参与,也就没有来自浏览器的隐含限制,只有curl和服务端之间的交互,至少在curl执行时看起来并不会被自动带上CORS的痕迹,而浏览器通常会。

那么,对于浏览器而言,如何判断JavaScript对外发起的请求属于“同源”还是“不同源”呢?所谓的“源”,就是资源来自哪里,比如承载JavaScript的网页所在的站点,以及JavaScript试图访问的服务端地址,等等。构成“源”的信息包括:协议(http或https)、主机地址(比如laogao.me)、端口。如下是一些示例:

URL
http://localhost:9700 http://localhost:9700
http://localhost:9700/a.html http://localhost:9700
https://localhost:9443/login https://localhost:9443
http://127.0.0.1/some_api http://127.0.0.1

注意localhost和127.0.0.1不会被识别为相同的主机地址,浏览器几乎是靠文本匹配来判断网页地址和请求目标地址是否同“源”。比如在 http://localhost:9700/a.html 这个网页上,有一段JavaScript,想对 http://127.0.0.1/some_api 发起请求,浏览器会认为这是跨“源”调用。

知道了浏览器如何判定跨“源”,对于那些我们希望成功的客户端对服务端资源的跨“源”请求,我们如何让浏览器和服务端允许呢?主要的开发工作还是在服务端:

  1. JavaScript客户端发起跨“源”请求。
  2. 浏览器给请求加上Origin头,内容为前面我们表格里看到的那样的“地址”。
  3. 服务端收到带Origin头,并且确实来自不同“源”的请求,如果允许,则需要在响应中加上HTTP头Access-Control-Allow-Origin: *,其中*表示所有源都允许,当然我们也可以明确指出我们允许某个具体的“源”。
  4. 浏览器截获该响应,判定服务端已允许来自发起调用的JavaScript代码所在“源”的请求,将结果返回给JavaScript客户端。

这是最简单的支持CORS的方式,只要服务端加上一个响应头Access-Control-Allow-Origin: *即可。不过现实情况很少会这么简单,通常服务端资源都不是完全公开的,因此需要某种限制或保护,比如仅针对某些特定“源”放开,甚至是查验cookie等。针对特定“源”相对好办,服务端判断一下,来自白名单的“源”,显示允许即可;查验cookie稍微麻烦点,除了客户端JavaScript调用时给XMLHttpRequest对象设置withCredentials为true之外,还需要服务端响应Access-Control-Allow-Credentials: true,如果不这样做,浏览器默认是不会在请求中带上cookie的。而一旦加上了Access-Control-Allow-Credentials: true,浏览器将不允许Access-Control-Allow-Origin: *这样的通配写法,而必须显式指出允许的“源”,且要与请求中Origin头的值一致。

说到这里可能很多人都会有疑问,都已经到响应阶段了,该带的cookie也都带了,这样的头有意义吗?其实前面我们忽略了一个细节,就是preflight请求。不同浏览器实现稍有不同,并且默认也不是每一类或每一个请求都做,但基本逻辑是:在实际的CORS请求发起之前,浏览器向服务端发起一个试探性的OPTIONS请求,询问服务端是否支持CORS以及支持哪些方法和哪些请求头,这些都确认OK之后,才发起真正的请求。如果我们想做精细控制,服务端就得感知这件些请求/调用细节,并作出正确合理的响应。

最后列一下最常见的CORS相关请求头/属性和响应头,看上去还挺工整的,使用时注意下应该不至于搞错:

请求头或属性 响应头 备注
Origin Access-Control-Allow-Origin 发起源和服务端允许的源
withCredentials Access-Control-Allow-Credentials 请求是否带cookie和服务端是否允许带
Access-Control-Request-Methods Access-Control-Allow-Methods 请求使用的HTTP方法和服务端允许的方法
Access-Control-Request-Headers Access-Control-Allow-Headers 请求带上头和服务端允许的头

[1] http://www.manning.com/hossain/

超短域名下种cookie的一个坑

不久前踩了个略不容易踩到的坑儿,做个记录。

接到任务要做个统一的Web登录入口:通过某特定子域名的地址在主域下种cookie,以便为部署在其他子域的Web应用提供登录服务。从开发到测试再到预发,一切都很顺利,关联系统也都全部跑通。可到了线上环境,IE和IE内核的浏览器却跟商量好了似的,瞬间全线飘红,cookie无法顺利下发,与此同时,Chrome正常,Firefox正常,Safari正常,Opera也正常。什么情况?!

仔细对比了一下预发和线上两个环境的区别,配置项方面,只有域名不同,预发使用login.pre.xx.yy(主域.pre.xx.yy),线上使用login.xx.yy(主域.xx.yy),莫非……?

缓过神来,google了一圈,原来IE对下发xx.yy这样的超短域名的cookie有个比较奇葩的限制[1]:除.pl和.gr等极少数顶级域名外,如果域名最后一段为2个字符且倒数第二段少于3个字符,即类似co.uk这样的超短域名,不允许在Set-Cookie时显式指定domain,要想下发匹配超短域名的cookie,除非当前提供服务的域名本身不多不少就是xx.yy。从某种意义上,尤其站在历史视角,似乎也能理解,因为除了官方机构,哪个公司、组织或个人需要在co.uk这样的域下去种cookie呢,想干坏事吧?于是乎IE就这样被加上了这么个当时看上去充满正能量的逻辑,而这个逻辑直到最新的IE版本中依然存在。

好了,问题找到,要怎么解决呢?首先我的需求是要在一个登录系统中种下其他子域可以获取的Http-Only的cookie,比如我Set-Cookie下去,希望这个cookie活在xx.yy这个主域下,其他所有abc.xx.yy、def.xx.yy、blahblah.xx.yy等等,都能在服务端取到这个cookie。既然IE不允许我指定domain,看来也只有硬着头皮在xx.yy这个域名下占块地儿了(本来不想这么干)。跟运维同学商量,得,给,/login,拿去吧。

占地儿的事情告一段落,接下来就是技术实现,这里又发现个挺有趣的实现细节:IE内核浏览器,cookie的domain前面不加.(比如xx.yy),访问子域时才会自动带上;其他浏览器,Set-Cookie时通过指定domain,domain名称前面默认加.(比如.xx.yy),访问子域时会自动带上主域的cookie,如果不显式指定domain则访问子域时不会自动带上主域的cookie。好嘛,至少这种不一致表现起来还是挺“一致”的。唔,有戏!上User-Agent!

至此,这个坑终于可以宣告“已填”。

P.S. 事后回想,可能最大的疏漏,就是在开发测试阶段没有严格模拟线上环境的所有参数,导致问题直到线上环境才暴露出来,经验教训啊!话说回来,职业生涯能用得上几回xx.yy这么短的域名?能踩上这样的坑,也够奢侈的。

[1] http://crisp.tweakblogs.net/blog/ie-and-2-letter-domain-names.html

《快学Scala》勘误

第11页(练习):

在Scala REPL中键入3,然后按Tab键 应为 在Scala REPL中键入3.,然后按Tab

第19页(正文):

util方法返回一个并不包含上限的区间 应为 until方法返回一个并不包含上限的区间

第19页(代码):

0 util s.length 应为 0 until s.length

第31页(正文):

util是RichInt类的方法 应为 untilRichInt类的方法

第34页(代码):

ArraryBuffer 应为 ArrayBuffer
b.sorted(_ < _) 应为 b.sorted
b.sorted(_ > _) 应为 b.sortWith(_ > _) 

第44页(代码):

scala.collections.immutable.SortedMap 应为 scala.collection.immutable.SortedMap

第107页(代码):

val tokens = source.mkString.split("\\S+") 应为 val tokens = source.mkString.split("\\s+")

第341页(代码):

var count: (Int => Double) => null 应为 var count: (Int => Double) = null