Mar
2
我注意到过去几个月我司有些同学还在踩一个简单的分布式事务Case的坑,而这个坑在两年以前就已经有同学踩过了,这里简单解析一下这个case和合适的处理方案,供参考。
1. 踩过的坑
这个case有很多变种,先从我们在XX业务踩过的坑开始,大约是16年9月,核心业务需求是很简单的:在用户发起支付请求的时候,从用户的银行卡扣一笔钱。负责这个需求的同学是这么写的代码(去除其他业务逻辑的简化版):
乍一看好像是没有什么毛病,测试的case都顺利提供过,也没有人去仔细review这一小段代码,于是就这么上线了。但问题很快就暴露出来,PaySvr在支付成功以后尝试回调,XX业务系统报错”订单不存在”。查询线上日志发现,这笔订单在请求第三方支付通道时网络超时,Curl抛了timeout异常,导致支付记录被回滚。有心的同学可以自己复现一下这个问题,观察BUG的发生过程。
代码修复起来倒是很简单,在请求PaySvr之前提交事务,将支付请求安全落库即可。
把这个实现代入多个不同的业务下,还会衍生出更多问题,比如被动代扣业务,就可能因为重试导致用户被多次扣款,引起投诉(支付通道对投诉率的要求非常严格,甚至可能导致通道被关停);更严重的是放款业务,可能出现重复放款,给公司造成直接损失。据说某友商就是因为重复放款倒闭的,所以在实现类似业务时特别注意,考虑周全。
2. 归纳总结
我们往后退一步再审视这个case,这段简单的代码涉及了两个系统:XX业务系统(本地数据库)、PaySvr(外部系统)。可以看得出这段代码的本意,是期望将当前系统的业务和外部系统的业务,合并到一个事务里面,要么一起成功提交,要么一起失败回滚,从而保持两个系统的一致性。
之所以没能达到预期,直接原因是,在失败(异常)回滚的时候,只回滚了本地事务,而没有回滚远端系统的状态变化。按这个思路去考虑,似乎只要加一个 PaySvr::rollbackRequest($order->id) 好像就可以解决问题。但仔细想想就会发现远没这么简单,从业务上来说,如果已经给用户付款了,那实际上就是要给用户退款,而往往这时候是掉单(支付请求结果未知),我们又无法直接给用户退款;更极端一点,如果这个rollback请求也超时了呢,那本地可以rollback吗?
这就是分布式事务棘手的地方了,只靠这样的逻辑是无法保证跨系统的一致性的。解决这个问题的方法是引入两段式提交(2 Phase Commit,常常简写为2PC),其基本逻辑是,在两个系统分别完成业务,然后再分别提交。
例如我们上面的解决方案,实际上就是2PC的一个实现:我们把业务需求作为一整个事务,它可以拆成两个子事务(第三方支付通道完成代扣,在XX业务系统记录支付请求成功并修改相应业务状态),每个子事务又分成两个阶段:第一阶段,是在本地先记录支付请求(状态为待确认),并向第三方支付发出代扣请求(结果不确定);第二阶段,在确认第三方代扣成功以后,修改本地支付请求的状态修改为成功,或者是代扣结果为失败,本地支付请求状态记为失败。两个阶段都完成,这个事务才是真的完成了。
3. Case变种
仔细思考我们曾经实现过的需求,可能会在很多看似不起眼的地方发现分布式事务,例如我们在的存管匹配系统里面,就有这样一个Case。
由于与XX银行存管系统交互的延迟比较大,所以我们的匹配系统实现是异步的,匹配系统在撮合了资金和资产以后,会生成一条债权关系记录在本地,随后再发送到XX银行执行资金的划拨。为了提高执行的效率,我们希望在债权关系生成以后,尽快执行资金的划拨,因此我们会把资金划拨的指令通过LPush放进Redis的list里;List的另一端,那些使用BLPOP监听数据的worker会立刻被激活去执行。
如果没有仔细思考,代码可能会这么写:
在实际执行这段代码的时候,如果没有仔细测试(尤其是在有补单逻辑,捞出未执行成功的划拨指令再发送给银行),可能就不会发现,实际上有很多指令并不是马上被执行的,因为relation_id被送进list以后,worker马上就会读出来执行,但这时事务可能还没有提交。但这只是影响了业务的效率,还没有对业务的正确性产生影响。
为了修复这个问题,似乎可以这么做:把 [capital_id, project_id, amount] 发送到redis,worker直接取出执行,这样就不用从数据库读取relation,保证尽快将请求发送到银行。但如果因为某些原因,事务最终没有被提交呢?找银行rollback这些指令的执行,那就麻烦多了。
正确的做法是,在事务提交了以后,再lPush到Redis里:
注:foreach要放到try-catch后面。
最后想说,我相信有很多同学知道这个Case,或者就算不知道也不会犯这样的错误,因此也许会觉得没必要专门揪出来这样分享 —— 但“知识的诅咒”就是这样,“我会的东西都是简单的”,然而对于没有踩过坑的同学来说,其实都是宝贵的经验;另一方面,有些别人觉得简单的问题、踩过的坑,也许自己是不知道的。所以希望大家都能分享自己在工作学习中踩过的坑、解决过的问题,互相交流,互相提高。
1. 踩过的坑
这个case有很多变种,先从我们在XX业务踩过的坑开始,大约是16年9月,核心业务需求是很简单的:在用户发起支付请求的时候,从用户的银行卡扣一笔钱。负责这个需求的同学是这么写的代码(去除其他业务逻辑的简化版):
$dbTrans = $db->beginTransaction();
try {
$order = PayRequest::model()->newPayRequest(...); #在数据库中插入一条支付请求记录,状态为待支付
//其他业务改动
$result = PaySvr::pay($order->id, $order->amount); #请求PaySvr(或第三方支付通道)扣款
if ($result['code'] == PaySvr::E_SUCCESS) {
$order->setAsSucceeded();
} else {
$order->setAsPending();
}
$dbTrans->commit();
} catch (Exception $e) {
$dbTrans->rollback();
}
try {
$order = PayRequest::model()->newPayRequest(...); #在数据库中插入一条支付请求记录,状态为待支付
//其他业务改动
$result = PaySvr::pay($order->id, $order->amount); #请求PaySvr(或第三方支付通道)扣款
if ($result['code'] == PaySvr::E_SUCCESS) {
$order->setAsSucceeded();
} else {
$order->setAsPending();
}
$dbTrans->commit();
} catch (Exception $e) {
$dbTrans->rollback();
}
乍一看好像是没有什么毛病,测试的case都顺利提供过,也没有人去仔细review这一小段代码,于是就这么上线了。但问题很快就暴露出来,PaySvr在支付成功以后尝试回调,XX业务系统报错”订单不存在”。查询线上日志发现,这笔订单在请求第三方支付通道时网络超时,Curl抛了timeout异常,导致支付记录被回滚。有心的同学可以自己复现一下这个问题,观察BUG的发生过程。
代码修复起来倒是很简单,在请求PaySvr之前提交事务,将支付请求安全落库即可。
$dbTrans = $db->beginTransaction();
try {
$order = PayRequest::model()->newPayRequest(...);
//其他业务改动
$dbTrans->commit(); #先将支付请求落地
} catch (Exception $e) {
$dbTrans->rollback();
}
#再请求PaySvr
$result = PaySvr::pay($order->id, $order->amount);
#根据PaySvr结果修改支付请求和其他业务记录的状态
$dbTrans = $db->beginTransaction();
try {
if ($result['code'] == PaySvr::E_SUCCESS) {
$order->setAsSucceeded();
//其他业务改动
} else {
$order->setAsPending();
//其他业务改动
}
} catch (Exception $e) {
$dbTrans->rollback();
}
try {
$order = PayRequest::model()->newPayRequest(...);
//其他业务改动
$dbTrans->commit(); #先将支付请求落地
} catch (Exception $e) {
$dbTrans->rollback();
}
#再请求PaySvr
$result = PaySvr::pay($order->id, $order->amount);
#根据PaySvr结果修改支付请求和其他业务记录的状态
$dbTrans = $db->beginTransaction();
try {
if ($result['code'] == PaySvr::E_SUCCESS) {
$order->setAsSucceeded();
//其他业务改动
} else {
$order->setAsPending();
//其他业务改动
}
} catch (Exception $e) {
$dbTrans->rollback();
}
把这个实现代入多个不同的业务下,还会衍生出更多问题,比如被动代扣业务,就可能因为重试导致用户被多次扣款,引起投诉(支付通道对投诉率的要求非常严格,甚至可能导致通道被关停);更严重的是放款业务,可能出现重复放款,给公司造成直接损失。据说某友商就是因为重复放款倒闭的,所以在实现类似业务时特别注意,考虑周全。
2. 归纳总结
我们往后退一步再审视这个case,这段简单的代码涉及了两个系统:XX业务系统(本地数据库)、PaySvr(外部系统)。可以看得出这段代码的本意,是期望将当前系统的业务和外部系统的业务,合并到一个事务里面,要么一起成功提交,要么一起失败回滚,从而保持两个系统的一致性。
之所以没能达到预期,直接原因是,在失败(异常)回滚的时候,只回滚了本地事务,而没有回滚远端系统的状态变化。按这个思路去考虑,似乎只要加一个 PaySvr::rollbackRequest($order->id) 好像就可以解决问题。但仔细想想就会发现远没这么简单,从业务上来说,如果已经给用户付款了,那实际上就是要给用户退款,而往往这时候是掉单(支付请求结果未知),我们又无法直接给用户退款;更极端一点,如果这个rollback请求也超时了呢,那本地可以rollback吗?
这就是分布式事务棘手的地方了,只靠这样的逻辑是无法保证跨系统的一致性的。解决这个问题的方法是引入两段式提交(2 Phase Commit,常常简写为2PC),其基本逻辑是,在两个系统分别完成业务,然后再分别提交。
例如我们上面的解决方案,实际上就是2PC的一个实现:我们把业务需求作为一整个事务,它可以拆成两个子事务(第三方支付通道完成代扣,在XX业务系统记录支付请求成功并修改相应业务状态),每个子事务又分成两个阶段:第一阶段,是在本地先记录支付请求(状态为待确认),并向第三方支付发出代扣请求(结果不确定);第二阶段,在确认第三方代扣成功以后,修改本地支付请求的状态修改为成功,或者是代扣结果为失败,本地支付请求状态记为失败。两个阶段都完成,这个事务才是真的完成了。
3. Case变种
仔细思考我们曾经实现过的需求,可能会在很多看似不起眼的地方发现分布式事务,例如我们在的存管匹配系统里面,就有这样一个Case。
由于与XX银行存管系统交互的延迟比较大,所以我们的匹配系统实现是异步的,匹配系统在撮合了资金和资产以后,会生成一条债权关系记录在本地,随后再发送到XX银行执行资金的划拨。为了提高执行的效率,我们希望在债权关系生成以后,尽快执行资金的划拨,因此我们会把资金划拨的指令通过LPush放进Redis的list里;List的另一端,那些使用BLPOP监听数据的worker会立刻被激活去执行。
如果没有仔细思考,代码可能会这么写:
#匹配系统
function matcher() {
$dbTrans = $db->beginTransaction();
try {
foreach (matchCapitalAndProject() as $match_result) {
list($capital_id, $project_id, $amount) = $match_result;
$relation = Relation::model()->create($capital_id, $project_id, $amount);
$redis->lPush($relation->id);
}
$dbTrans->commit();
} catch (Exception $e) {
$dbTrans->rollback();
}
}
#Worker
function Worker() {
while (true) {
$id = $redis->brPop();
$relation = Relation::model()->findByPk($id);
if ($relation) {
BankApi::invest($relation->capital_id, $relation->project_id, $amount);
}
}
}
function matcher() {
$dbTrans = $db->beginTransaction();
try {
foreach (matchCapitalAndProject() as $match_result) {
list($capital_id, $project_id, $amount) = $match_result;
$relation = Relation::model()->create($capital_id, $project_id, $amount);
$redis->lPush($relation->id);
}
$dbTrans->commit();
} catch (Exception $e) {
$dbTrans->rollback();
}
}
#Worker
function Worker() {
while (true) {
$id = $redis->brPop();
$relation = Relation::model()->findByPk($id);
if ($relation) {
BankApi::invest($relation->capital_id, $relation->project_id, $amount);
}
}
}
在实际执行这段代码的时候,如果没有仔细测试(尤其是在有补单逻辑,捞出未执行成功的划拨指令再发送给银行),可能就不会发现,实际上有很多指令并不是马上被执行的,因为relation_id被送进list以后,worker马上就会读出来执行,但这时事务可能还没有提交。但这只是影响了业务的效率,还没有对业务的正确性产生影响。
为了修复这个问题,似乎可以这么做:把 [capital_id, project_id, amount] 发送到redis,worker直接取出执行,这样就不用从数据库读取relation,保证尽快将请求发送到银行。但如果因为某些原因,事务最终没有被提交呢?找银行rollback这些指令的执行,那就麻烦多了。
正确的做法是,在事务提交了以后,再lPush到Redis里:
#匹配系统
function matcher() {
$arr_relation = [];
$dbTrans = $db->beginTransaction();
try {
foreach (matchCapitalAndProject() as $match_result) {
list($capital_id, $project_id, $amount) = $match_result;
$relation = Relation::model()->create($capital_id, $project_id, $amount);
$arr_relation[] = $relation;
}
$dbTrans->commit();
} catch (Exception $e) {
$dbTrans->rollback();
}
foreach ($arr_relation as $relation) {
$redis->lPush($relation->id);
}
}
function matcher() {
$arr_relation = [];
$dbTrans = $db->beginTransaction();
try {
foreach (matchCapitalAndProject() as $match_result) {
list($capital_id, $project_id, $amount) = $match_result;
$relation = Relation::model()->create($capital_id, $project_id, $amount);
$arr_relation[] = $relation;
}
$dbTrans->commit();
} catch (Exception $e) {
$dbTrans->rollback();
}
foreach ($arr_relation as $relation) {
$redis->lPush($relation->id);
}
}
注:foreach要放到try-catch后面。
最后想说,我相信有很多同学知道这个Case,或者就算不知道也不会犯这样的错误,因此也许会觉得没必要专门揪出来这样分享 —— 但“知识的诅咒”就是这样,“我会的东西都是简单的”,然而对于没有踩过坑的同学来说,其实都是宝贵的经验;另一方面,有些别人觉得简单的问题、踩过的坑,也许自己是不知道的。所以希望大家都能分享自己在工作学习中踩过的坑、解决过的问题,互相交流,互相提高。
Feb
28
# 1. 编译安装 (@ubuntu)
# 2. 启动 regtest 网络
$ bitcoind -regtest -txindex -daemon
说明:
-regtest 表示启动的是 regtest 网络;不同网络之间的区别在于创世区块不同。
-txindex 表示对所有的交易进行索引;否则默认只对与钱包地址有关的交易索引
-daemon 表示在后台运行
也可以通过写入配置文件 ~/.bitcoin/bitcoin.conf 的方式 :
然后直接调用 bitcoind 就好了。
# 3. 基本命令
引用
sudo mkdir -p /usr/local/services/bitcoin
sudo apt-get install autoconf pkg-config libtool build-essential libdb++-dev libboost-dev libssl-dev libevent-dev libboost-system-dev libboost-filesystem-dev libboost-program-options-dev libboost-thread-dev libboost-test-dev libboost-*-dev
git clone https://github.com/bitcoin/bitcoin.git
cd bitcoin
git checkout v0.15.1
./autogen.sh
./configure --prefix=/usr/local/services/bitcoin --with-incompatible-bdb
make -j 6 &> make.log &
sudo make install
echo 'export PATH=$PATH:/usr/local/services/bitcoin/bin' >> ~/.bashrc
sudo apt-get install autoconf pkg-config libtool build-essential libdb++-dev libboost-dev libssl-dev libevent-dev libboost-system-dev libboost-filesystem-dev libboost-program-options-dev libboost-thread-dev libboost-test-dev libboost-*-dev
git clone https://github.com/bitcoin/bitcoin.git
cd bitcoin
git checkout v0.15.1
./autogen.sh
./configure --prefix=/usr/local/services/bitcoin --with-incompatible-bdb
make -j 6 &> make.log &
sudo make install
echo 'export PATH=$PATH:/usr/local/services/bitcoin/bin' >> ~/.bashrc
# 2. 启动 regtest 网络
$ bitcoind -regtest -txindex -daemon
说明:
-regtest 表示启动的是 regtest 网络;不同网络之间的区别在于创世区块不同。
-txindex 表示对所有的交易进行索引;否则默认只对与钱包地址有关的交易索引
-daemon 表示在后台运行
也可以通过写入配置文件 ~/.bitcoin/bitcoin.conf 的方式 :
引用
regtest=1
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
rpcuser=test
rpcpassword=test
server=1
daemon=1
txindex=1
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
rpcuser=test
rpcpassword=test
server=1
daemon=1
txindex=1
然后直接调用 bitcoind 就好了。
# 3. 基本命令
引用
$ bitcoin-cli -regtest generate 101 #挖101个block;挖矿的奖励要在100个block以后才能使用
[
"26d4fb4dd449b93ebbda9a36f390d2c7b1dd9557e662840fe61b1f6e3a09d218",
.... #共101块
]
$ bitcoin-cli -regtest getaccountaddress "" #钱包默认账户的地址; 测试网络的地址是m或n开头的
mmmSrYNiRQzeiTypt8rF9L3F742PW4ksmL
$ bitcoin-cli -regtest getbalance #查看挖矿奖励(默认账户)
50.00000000
$ bitcoin-cli -regtest getnewaddress #分配一个新的地址
mn41xHRzXuUAAVtujPLLo3Hzbe63GSP117
$ bitcoin-cli -regtest sendtoaddress mn41xHRzXuUAAVtujPLLo3Hzbe63GSP117 10 #往这个新生成的地址转10个BTC;输出是交易的hash
4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af
$ bitcoin-cli -regtest getrawtransaction 4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af true #查看交易详细信息
(输出json,包含size、vin、vout等,因为还没被打包进区块链,所以没有confirmations)
$ bitcoin-cli -regtest generate 1
[
"4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af"
]
$ bitcoin-cli -regtest getrawtransaction 4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af true #查看交易详细信息
(输出json,这次可以看到 blockhash 和 confirmations 了,说明交易已经被打包)
$ bitcoin-cli -regtest listreceivedbyaddress #查看收到的BTC
[
{
"address": "mn41xHRzXuUAAVtujPLLo3Hzbe63GSP117",
"account": "",
"amount": 10.00000000,
"confirmations": 1,
"label": "",
"txids": [
"4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af"
]
}
]
[
"26d4fb4dd449b93ebbda9a36f390d2c7b1dd9557e662840fe61b1f6e3a09d218",
.... #共101块
]
$ bitcoin-cli -regtest getaccountaddress "" #钱包默认账户的地址; 测试网络的地址是m或n开头的
mmmSrYNiRQzeiTypt8rF9L3F742PW4ksmL
$ bitcoin-cli -regtest getbalance #查看挖矿奖励(默认账户)
50.00000000
$ bitcoin-cli -regtest getnewaddress #分配一个新的地址
mn41xHRzXuUAAVtujPLLo3Hzbe63GSP117
$ bitcoin-cli -regtest sendtoaddress mn41xHRzXuUAAVtujPLLo3Hzbe63GSP117 10 #往这个新生成的地址转10个BTC;输出是交易的hash
4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af
$ bitcoin-cli -regtest getrawtransaction 4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af true #查看交易详细信息
(输出json,包含size、vin、vout等,因为还没被打包进区块链,所以没有confirmations)
$ bitcoin-cli -regtest generate 1
[
"4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af"
]
$ bitcoin-cli -regtest getrawtransaction 4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af true #查看交易详细信息
(输出json,这次可以看到 blockhash 和 confirmations 了,说明交易已经被打包)
$ bitcoin-cli -regtest listreceivedbyaddress #查看收到的BTC
[
{
"address": "mn41xHRzXuUAAVtujPLLo3Hzbe63GSP117",
"account": "",
"amount": 10.00000000,
"confirmations": 1,
"label": "",
"txids": [
"4deb2fbb98d9029a4e5808b8236d1119ad7315efdfe7406a88c923f5d4b0d5af"
]
}
]
Feb
28
留档备查
脚本里设置了 ServerActive ,会主动尝试到zabbix server注册,但需要先在zabbix frontend的 configuration->actions->auto registration 配置好 add host 动作,这样才会自动添加。
#!/bin/bash
set -x
ZABBIX_SERVER=192.168.1.100
wget http://repo.zabbix.com/zabbix/3.2/ubuntu/pool/main/z/zabbix-release/zabbix-release_3.2-1+xenial_all.deb
dpkg -i zabbix-release_3.2-1+xenial_all.deb
apt update
apt -y install zabbix-agent
ip=`ifconfig | grep -o 'inet addr:172\.[0-9.]*' | awk -F: '{print $2}'`
sed -i \
-e 's/^Server=.*$/Server='$ZABBIX_SERVER'/' \
-e 's/^ServerActive=.*$/ServerActive=lan.zabbix.thebitplus.com/' \
-e 's/^Hostname=.*$/Hostname='`hostname`'/' \
/etc/zabbix/zabbix_agentd.conf
sudo update-rc.d zabbix-agent enable
sudo service zabbix-agent restart
echo "Done"
脚本里设置了 ServerActive ,会主动尝试到zabbix server注册,但需要先在zabbix frontend的 configuration->actions->auto registration 配置好 add host 动作,这样才会自动添加。
引用
#!/bin/bash
set -x
ZABBIX_SERVER=192.168.1.100
wget http://repo.zabbix.com/zabbix/3.2/ubuntu/pool/main/z/zabbix-release/zabbix-release_3.2-1+xenial_all.deb
dpkg -i zabbix-release_3.2-1+xenial_all.deb
apt update
apt -y install zabbix-agent
ip=`ifconfig | grep -o 'inet addr:172\.[0-9.]*' | awk -F: '{print $2}'`
sed -i \
-e 's/^Server=.*$/Server='$ZABBIX_SERVER'/' \
-e 's/^ServerActive=.*$/ServerActive=lan.zabbix.thebitplus.com/' \
-e 's/^Hostname=.*$/Hostname='`hostname`'/' \
/etc/zabbix/zabbix_agentd.conf
sudo update-rc.d zabbix-agent enable
sudo service zabbix-agent restart
echo "Done"
Feb
11
莫名其妙的一个错误,手头的两个sentry实例里都没有这个表,但是还是会报这个错。
没研究具体的代码,但是通过查找源码里的 sentry_email 发掘了表结构,建表并授权即可:
没研究具体的代码,但是通过查找源码里的 sentry_email 发掘了表结构,建表并授权即可:
引用
$ psql -h $HOST -U root -W sentry
sentry=> create extension citext;
CREATE EXTENSION
sentry=> create table sentry_email (id bigserial primary key, email CITEXT, date_added timestamp with time zone);
CREATE TABLE
sentry=> grant all privileges on table sentry_email to sentry;
GRANT
sentry=> GRANT USAGE, SELECT ON SEQUENCE sentry_email_id_seq1 to sentry;
GRANT
sentry=> create extension citext;
CREATE EXTENSION
sentry=> create table sentry_email (id bigserial primary key, email CITEXT, date_added timestamp with time zone);
CREATE TABLE
sentry=> grant all privileges on table sentry_email to sentry;
GRANT
sentry=> GRANT USAGE, SELECT ON SEQUENCE sentry_email_id_seq1 to sentry;
GRANT
Feb
1
不知道为什么这些年写脚本一直没有解决这个小需求:把命令行参数传递给脚本中的某个命令继续执行
例如我想写一个 colored-echo 命令:
然后这么调用
但参数的数量是可变的,另一种实现是
但个实现也很奇怪,如果某个参数里面出现了引号或者空格,会因为bash奇葩的转义逻辑导致跟预期不一致。
可能因为痛感不强烈,所以拖了几年也没真正花心思去解决它,今天搜了一会,总算找到靠谱的解决方案了,而且超级简单:
例如我想写一个 colored-echo 命令:
引用
#!/bin/bash
color=$1
shift
echo -ne "\x1b[$color"
echo -n $1 $2 $3 $4 $5
echo -e "\x1b[0m"
color=$1
shift
echo -ne "\x1b[$color"
echo -n $1 $2 $3 $4 $5
echo -e "\x1b[0m"
然后这么调用
引用
colored-echo 41m hello world
但参数的数量是可变的,另一种实现是
引用
cmd=echo
for ((i=1; i<=$#; i++))
do
cmd="cmd ${@:i:1}"
done
$cmd
for ((i=1; i<=$#; i++))
do
cmd="cmd ${@:i:1}"
done
$cmd
但个实现也很奇怪,如果某个参数里面出现了引号或者空格,会因为bash奇葩的转义逻辑导致跟预期不一致。
可能因为痛感不强烈,所以拖了几年也没真正花心思去解决它,今天搜了一会,总算找到靠谱的解决方案了,而且超级简单:
引用
echo "$@"
Feb
1
平台:AWS Aurora (MySQL 5.6.10a 兼容版本)
测试软件:sysbench 0.5 --test=oltp.lua (与之前一样的测试脚本)
DB配置:db.r4.large (2核,约16GB RAM),Aurora Shared Disk
sysbench配置:m4.xlarge(4核,16GB RAM),20GB+100GB EBS(300/3000 IOPS)
测试结果:
tps: 276 tps
reads: 6400 tps
writes: 1100 tps
response time: 130ms (95%)
测试结果:
[ 10s] threads: 32, tps: 174.89, reads: 4077.58, writes: 705.68, response time: 305.44ms (95%), errors: 0.00, reconnects: 0.00
[ 20s] threads: 32, tps: 230.00, reads: 5302.10, writes: 916.00, response time: 236.40ms (95%), errors: 0.00, reconnects: 0.00
[ 30s] threads: 32, tps: 248.90, reads: 5680.20, writes: 994.70, response time: 219.35ms (95%), errors: 0.00, reconnects: 0.00
[ 40s] threads: 32, tps: 250.10, reads: 5776.60, writes: 1000.00, response time: 219.16ms (95%), errors: 0.00, reconnects: 0.00
[ 50s] threads: 32, tps: 253.20, reads: 5811.50, writes: 1012.00, response time: 205.62ms (95%), errors: 0.00, reconnects: 0.00
[ 60s] threads: 32, tps: 252.30, reads: 5832.70, writes: 1009.60, response time: 197.89ms (95%), errors: 0.00, reconnects: 0.00
...
...
[1780s] threads: 32, tps: 279.00, reads: 6403.60, writes: 1115.30, response time: 135.88ms (95%), errors: 0.00, reconnects: 0.00
[1790s] threads: 32, tps: 276.30, reads: 6391.30, writes: 1106.80, response time: 155.70ms (95%), errors: 0.00, reconnects: 0.00
[1800s] threads: 32, tps: 276.90, reads: 6343.60, writes: 1106.80, response time: 131.83ms (95%), errors: 0.00, reconnects: 0.00
OLTP test statistics:
queries performed:
read: 11371476
write: 1977648
other: 988824
total: 14337948
transactions: 494412 (274.66 per sec.)
read/write requests: 13349124 (7415.78 per sec.)
other operations: 988824 (549.32 per sec.)
ignored errors: 0 (0.00 per sec.)
reconnects: 0 (0.00 per sec.)
General statistics:
total time: 1800.0982s
total number of events: 494412
total time taken by event execution: 57593.8148s
response time:
min: 24.28ms
avg: 116.49ms
max: 505.90ms
approx. 95 percentile: 142.84ms
Threads fairness:
events (avg/stddev): 15450.3750/15.24
execution time (avg/stddev): 1799.8067/0.02
测试软件:sysbench 0.5 --test=oltp.lua (与之前一样的测试脚本)
DB配置:db.r4.large (2核,约16GB RAM),Aurora Shared Disk
sysbench配置:m4.xlarge(4核,16GB RAM),20GB+100GB EBS(300/3000 IOPS)
测试结果:
tps: 276 tps
reads: 6400 tps
writes: 1100 tps
response time: 130ms (95%)
测试结果:
引用
[ 10s] threads: 32, tps: 174.89, reads: 4077.58, writes: 705.68, response time: 305.44ms (95%), errors: 0.00, reconnects: 0.00
[ 20s] threads: 32, tps: 230.00, reads: 5302.10, writes: 916.00, response time: 236.40ms (95%), errors: 0.00, reconnects: 0.00
[ 30s] threads: 32, tps: 248.90, reads: 5680.20, writes: 994.70, response time: 219.35ms (95%), errors: 0.00, reconnects: 0.00
[ 40s] threads: 32, tps: 250.10, reads: 5776.60, writes: 1000.00, response time: 219.16ms (95%), errors: 0.00, reconnects: 0.00
[ 50s] threads: 32, tps: 253.20, reads: 5811.50, writes: 1012.00, response time: 205.62ms (95%), errors: 0.00, reconnects: 0.00
[ 60s] threads: 32, tps: 252.30, reads: 5832.70, writes: 1009.60, response time: 197.89ms (95%), errors: 0.00, reconnects: 0.00
...
...
[1780s] threads: 32, tps: 279.00, reads: 6403.60, writes: 1115.30, response time: 135.88ms (95%), errors: 0.00, reconnects: 0.00
[1790s] threads: 32, tps: 276.30, reads: 6391.30, writes: 1106.80, response time: 155.70ms (95%), errors: 0.00, reconnects: 0.00
[1800s] threads: 32, tps: 276.90, reads: 6343.60, writes: 1106.80, response time: 131.83ms (95%), errors: 0.00, reconnects: 0.00
OLTP test statistics:
queries performed:
read: 11371476
write: 1977648
other: 988824
total: 14337948
transactions: 494412 (274.66 per sec.)
read/write requests: 13349124 (7415.78 per sec.)
other operations: 988824 (549.32 per sec.)
ignored errors: 0 (0.00 per sec.)
reconnects: 0 (0.00 per sec.)
General statistics:
total time: 1800.0982s
total number of events: 494412
total time taken by event execution: 57593.8148s
response time:
min: 24.28ms
avg: 116.49ms
max: 505.90ms
approx. 95 percentile: 142.84ms
Threads fairness:
events (avg/stddev): 15450.3750/15.24
execution time (avg/stddev): 1799.8067/0.02
Jan
27
AWS的http/https负载均衡挺好用的,但是有一点比较麻烦,因为是应用层协议,所以在后端的nginx(以及下面挂的php-fpm)看到的 REMOTE_ADDR是负载均衡的IP,而直连LB的IP,则是保存在了 HTTP_X_FORWARD_FOR 这个header里面。
当然,在应用里面增加一小段代码去解析这个header也不是什么难事,但是毕竟有些框架已经有一套解析的方案了,而且碰到客户端自己还用代理的时候,这个字段的value是一串IP列表(直连负载均衡的ip是最后一个),就变得更复杂了。比如:
$ curl https://test.com/ -H "X-FORWARDED-FOR: 8.8.8.8"
php记录下来的 $_SERVER 变量就长这样了:
array (
'HTTP_X_FORWARDED_PORT' => '443',
'HTTP_X_FORWARDED_PROTO' => 'https',
'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 13.31.11.23',
'SERVER_PORT' => '80', 'SERVER_ADDR' => '172.24.22.33',
'REMOTE_PORT' => '56247',
'REMOTE_ADDR' => '172.24.22.34',
)
还是有点头疼的。幸好nginx有提供一个 ngx_http_realip_module 模块,可以解决这个问题,只要在配置里加上这么两行:
set_real_ip_from 172.24.0.0/16; #注:这个ip端是负载均衡所处VPC的CIDR
real_ip_header X-Forwarded-For;
重启nginx以后,再次访问就可以看到,REMOTE_ADDR 变成了 HTTP_X_FORWARDED_FOR 的 IP列表里最后一个IP。
当然,在应用里面增加一小段代码去解析这个header也不是什么难事,但是毕竟有些框架已经有一套解析的方案了,而且碰到客户端自己还用代理的时候,这个字段的value是一串IP列表(直连负载均衡的ip是最后一个),就变得更复杂了。比如:
$ curl https://test.com/ -H "X-FORWARDED-FOR: 8.8.8.8"
php记录下来的 $_SERVER 变量就长这样了:
array (
'HTTP_X_FORWARDED_PORT' => '443',
'HTTP_X_FORWARDED_PROTO' => 'https',
'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 13.31.11.23',
'SERVER_PORT' => '80', 'SERVER_ADDR' => '172.24.22.33',
'REMOTE_PORT' => '56247',
'REMOTE_ADDR' => '172.24.22.34',
)
还是有点头疼的。幸好nginx有提供一个 ngx_http_realip_module 模块,可以解决这个问题,只要在配置里加上这么两行:
set_real_ip_from 172.24.0.0/16; #注:这个ip端是负载均衡所处VPC的CIDR
real_ip_header X-Forwarded-For;
重启nginx以后,再次访问就可以看到,REMOTE_ADDR 变成了 HTTP_X_FORWARDED_FOR 的 IP列表里最后一个IP。
Sep
22
(瞅一眼才发现四个月没写了,确实是好久没写代码了,没啥心得,不过想想好像可以写个提纲凑个数)
我们的业务主要还是用 MySQL 存储业务数据。
MySQL 一个很麻烦的问题是,alter table 的时候往往要锁表,而业务在最初设计的时候,又没法为未来的所有改动预留合适的字段,结果就是,要么另外建一张表横向扩展,要么熬到半夜,忍受锁表带来的业务中断;不过在多次实践中还是有一些心得体会,可以简单列一下。
1. alter table 是否都会锁表?
不都会,有些情况可以不锁表,例如,修改默认值,或者对 enum 类型字段增加一个 value
2. 对 enum 类型字段加 value 就不会锁表吗?
不一定,如果新增的 value 是最后一个就不会锁表,但也要注意,还是有坑(不能超过当前的bit数能表示的最大值)(为什么?)
3. 有没办法即 alter table 但又不长时间锁表?
有,percona-toolkit 有个工具能做到,原理很简单,新建一个表A的副本A',在A'上加字段,并同步数据,最后用一个 alter 语句对换两张表,但据说有BUG
4. 安利一下与 MySQL 协议基本兼容的 TiDB ,可以直接在线不锁表 alter table
我们的业务主要还是用 MySQL 存储业务数据。
MySQL 一个很麻烦的问题是,alter table 的时候往往要锁表,而业务在最初设计的时候,又没法为未来的所有改动预留合适的字段,结果就是,要么另外建一张表横向扩展,要么熬到半夜,忍受锁表带来的业务中断;不过在多次实践中还是有一些心得体会,可以简单列一下。
1. alter table 是否都会锁表?
不都会,有些情况可以不锁表,例如,修改默认值,或者对 enum 类型字段增加一个 value
2. 对 enum 类型字段加 value 就不会锁表吗?
不一定,如果新增的 value 是最后一个就不会锁表,但也要注意,还是有坑(不能超过当前的bit数能表示的最大值)(为什么?)
3. 有没办法即 alter table 但又不长时间锁表?
有,percona-toolkit 有个工具能做到,原理很简单,新建一个表A的副本A',在A'上加字段,并同步数据,最后用一个 alter 语句对换两张表,但据说有BUG
4. 安利一下与 MySQL 协议基本兼容的 TiDB ,可以直接在线不锁表 alter table