Jun 8
注:这篇是3月初在公司内部平台上发布的,搬一份到 blog 存档。
===


我注意到过去几个月有些同学还在踩一个简单的分布式事务Case的坑,而这个坑我们在两年以前就已经有同学踩过了,这里简单解析一下这个case和合适的处理方案,供各位参考。

# 1. 踩过的坑

这个case有很多变种,先说说我们在 X 业务踩过的坑开始,大约是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();
}

乍一看好像是没有什么毛病,测试的case都顺利通过,也没有人去仔细review这一小段代码,于是就这么上线了。但问题很快就暴露出来,PaySvr在支付成功以后尝试回调,业务系统报错”订单不存在”。查询线上日志发现,这笔订单在请求第三方支付通道时网络超时,Curl抛了timeout异常,导致支付记录被回滚。有心的同学可以自己复现一下这个问题,观察BUG的发生过程。

代码修复起来倒是很简单,在请求PaySvr之前提交事务,将支付请求安全落库即可。
$dbTrans = $db->beginTransaction();
try {
    $order = PayRequest::model()->newPayRequest(...);
    $order->setAsPending();
    //其他业务改动
    $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();
        //其他业务改动
    } elseif ($result['code'] == PaySvr::E_FAIL) {
        $order->setAsFailed();
        //其他业务改动
    } else {
        //等待后续cron补单
    }
    $dbTrans->commit();
} catch (Exception $e) {
    $dbTrans->rollback();
}

把这个实现代入多个不同的业务下,还会衍生出更多问题,比如被动代扣业务,就可能因为重试导致用户被多次扣款,引起投诉(支付通道对投诉率的要求非常严格,甚至可能导致通道被关停);更严重的是放款业务,可能出现重复放款,给公司造成直接损失。据说某友商就是因为重复放款倒闭的,所以请各位同学在实现类似业务时特别注意,考虑周全。

# 2. 归纳总结

我们往后退一步再审视这个case,这段简单的代码涉及了两个系统:X 业务系统(本地数据库)、PaySvr(外部系统)。可以看得出这段代码的本意,是期望将当前系统的业务和外部系统的业务,合并到一个事务里面,要么一起成功提交,要么一起失败回滚,从而保持两个系统的一致性。

之所以没能达到预期,直接原因是,在失败(异常)回滚的时候,只回滚了本地事务,而没有回滚远端系统的状态变化。按这个思路去考虑,似乎只要加一个 PaySvr::rollbackRequest($order->id) 好像就可以解决问题。但仔细想想就会发现远没这么简单,从业务上来说,如果已经给用户付款了,那实际上就是要给用户退款,而往往这时候是掉单(支付请求结果未知),我们又无法直接给用户退款;更极端一点,如果这个rollback请求也超时了呢,那本地可以rollback吗?

这就是分布式事务棘手的地方了,只靠这样的逻辑是无法保证跨系统的一致性的。解决这个问题的方法是引入两段式提交(2 Phase Commit,常常简写为2PC),其基本逻辑是,在两个系统分别完成业务,然后再分别提交。

例如我们上面的解决方案,实际上就是2PC的一个实现:我们把业务需求作为一整个事务,它可以拆成两个子事务(第三方支付通道完成代扣,在业务系统记录支付请求成功并修改相应业务状态),每个子事务又分成两个阶段:第一阶段,是在本地先记录支付请求(状态为待确认),并向第三方支付发出代扣请求(结果不确定);第二阶段,在确认第三方代扣成功以后,修改本地支付请求的状态修改为成功,或者是代扣结果为失败,本地支付请求状态记为失败。两个阶段都完成,这个事务才是真的完成了。

# 3. Case变种

仔细思考我们曾经实现过的需求,可能会在很多看似不起眼的地方发现分布式事务,例如我们在存管匹配系统里面,就有这样一个Case。

由于与银行存管系统交互的延迟比较大,所以我们的匹配系统实现是异步的,匹配系统在撮合了资金和资产以后,会生成一条债权关系记录在本地,随后再发送到银行系统执行资金的划拨。为了提高执行的效率,我们希望在债权关系生成以后,尽快执行资金的划拨,因此我们会把资金划拨的指令通过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) {
            HengfengApi::invest($relation->capital_id, $relation->project_id, $amount);
        }
    }
}

在实际执行这段代码的时候,如果没有仔细测试(尤其是在有补单逻辑,捞出未执行成功的划拨指令再发送给银行),可能就不会发现,实际上有很多指令并不是马上被执行的,因为relation_id被送进list以后,worker马上就会读出来执行,但这时事务可能还没有提交。但这只是影响了业务的效率,还没有对业务的正确性产生影响。


为了修复这个问题,似乎可以这么做:把 [capital_id, project_id, amount] 发送到redis,worker直接取出执行,这样就不用从数据库读取relation,保证尽快将请求发送到银行。但如果因为某些原因,事务最终没有被提交呢?找银行rollback这些指令的执行,那就麻烦多了。

正确的做法是,在事务提交了以后,再lPush到Redis里:
[code] #匹配系统
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,或者就算不知道也不会犯这样的错误,因此也许会觉得没必要专门揪出来这样分享 —— 但“知识的诅咒”就是这样,“我会的东西都是简单的”,然而对于没有踩过坑的同学来说,其实都是宝贵的经验;另一方面,有些别人觉得简单的问题、踩过的坑,也许自己是不知道的。所以,希望大家都能分享自己在工作学习中踩过的坑、解决过的问题,互相交流,互相提高。
Mar 29

浅析嵌套数据库事务 不指定

felix021 @ 2018-3-29 22:00 [IT » 数据库] 评论(0) , 引用(0) , 阅读(290) | Via 本站原创

大家都知道,数据库事务提供的强一致性,让我们只需要在业务开始之前执行begin、结束后执行commit,并在异常的情况下执行rollback,就能够保证业务数据的强一致性。

## 1. 转一笔账

以一个转账操作为例,从from账户往to账户转一笔钱,涉及到两个账户的操作,我们用事务来保证数据的一致性:
function actionTransfer(Account $from, Account $to, int $amount)
{
    $this->db->begin();
    try {
        $from->withdraw($amount);
        $to->deposit($amount);
        $this->db->commit();
    } catch (Exception $e){
        $this->db->rollback();
    }   
}

class Account extends Model
{
    protected function changeBalance($amount)
    {
        $this->db->exec(
            "update account set balance = balance + :amount where id = :id",
            ['amount' => $amount, 'id' => $this->id]
        );
        AccountLog::create($this->id, $amount); #创建一条账户流水
    }
    public function withdraw($amount)
    {
        $this->changeBalance(-$amount);
    }
   
    public function deposit($amount)
    {
        $this->changeBalance($amount);
    }
}

如上面的栗子所见,我们往往会把数据库操作封装到底层模块(常常是model)里,对上层提供API,这样既保证了上层业务的可读性,也使得代码的可复用性提高了。

## 2. 充一笔钱,踩一个坑

说到复用性,那就是说,可能会有另一个业务需要调用它,比如充值,这个业务很简单,只涉及到一个账户,似乎可以简单点:
function actionDeposit(Account $account, int $amount)
{
    $account->deposit($amount);
}

这样看起来好像也没啥问题,对吧?但细心的小伙伴可能注意到了,代码里的 `changeBalance` 函数挖了个坑:在修改完账户余额以后会创建一条流水记录。如果没有开启事务的话,极端情况下会出现账户余额改好了,却没记录流水的情况,这就尴尬了。

当然这里只要在 `actionDeposit` 里开启了事务就能解决这个问题,但是基于防御性编程的习惯,`changeBalance` 这个方法本身作为一个独立的业务,应当尽最大努力确保其内业务的一致性,更直白一点说,就是也开启一个事务:
    protected function changeBalance($amount)
    {
        $this->db->begin();
        try {
            $this->db->exec("update account set balance ...", ...);
            AccountLog::create($this->id, $amount);
            $this->db->commit();
        } catch (Exception $e) {
            $this->db->rollback()
            throw $e; //确保上层业务知晓发生了异常
        }
    }

我们期望每一段代码都像这样对自己的业务负责,那么程序员不管在哪一层写代码,都能写得放心。

## 3. 嵌套事务

但到目前为止还没有到到期望的这么美好:如果只是在同一个流程里连续使用  begin ,实际上只会启动一个事务,遇到第一个 commit 就会结束事务,后续如果出现异常,rollback 无法达到预期的效果:
引用

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance + 1 where id = 1;
Query OK, 1 row affected (0.01 sec)

mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> update account set balance = balance - 1 where id = 2;
Query OK, 1 row affected (0.01 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+---------+
| id | balance |
+----+---------+
|  1 |    1001 |
|  2 |    999 |
+----+---------+

还好的是,MySQL提供了嵌套事务的支持,可以使用其 SavePoint 特性来达到这个效果:
引用

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance + 1 where id = 1;
Query OK, 1 row affected (0.00 sec)

mysql> savepoint level1;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance - 1 where id = 2;
Query OK, 1 row affected (0.00 sec)

mysql> release savepoint level1;
Query OK, 0 rows affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+---------+
| id | balance |
+----+---------+
|  1 |    1000 |
|  2 |    1000 |
+----+---------+

利用这一点,我们就可以写出一个支持嵌套事务的数据库模块:
class Database
{
    protected $level = 0;
    public function begin()
    {
        if ($this->level == 0) {
            $this->exec("begin");
        } else {
            $this->exec("savepoint level{$this->level}");
        }
        $this->level += 1;
    }

    public function commit()
    {
        $this->level -= 1;
        if ($this->level == 0) {
            $this->exec("commit");
        } else {
            $this->exec("release savepoint level{$this->level}");
        }
    }

    public function rollback()
    {
        $this->level -= 1;
        if ($this->level == 0) {
            $this->exec("rollback");
        } else {
            $this->exec("rollback to savepoint level{$this->level}");
        }
    }
}

当然,对于不支持 savepoint 特性的DBMS,也不是不能解决这个问题,只要在 `level != 0` 的时候不执行 commit 或者 rollback 就行,只是无法达到 savepoint 的效果。

加上这个改动的具体实现这里就不贴了,感兴趣的小伙伴建议自己动手写写看,实在太懒也可以查看 Yii2.0 中`yii\db\Transaction` 的实现 —— 其实上面这段代码就是对 Yii2.0 实现的一个简化版本。喜欢 Laravel 的小伙伴,在 `Illuminate\Database\Concerns\ManagesTransactions` 也能找到类似的应用。

## 4. 找坑和填坑

我们实现的这个嵌套事务方案,使得每一部分代码都能够健壮地实现其业务。但是,在具体编码过程中会常常还会遇到两个大坑:坑一是 begin 和 commit/rollback 不配对,坑二是出了错忘记反馈给上层。在上面的实现里,要避开这两个坑,完全依赖程序员的编码习惯。但墨菲定律告诉我们……说多了都是泪,总之就是“依赖人来保证正确性”这件事情永远是不靠谱的。

那怎么改变这一点呢?Laravel 的解决方案是由代码本身来填坑,所以实现了 `ManagesTransactions->transaction` 这个方法(以下略作简化,省略了失败重试机制及相应的死锁处理,这一部分建议详读 laravel 的相关代码):
    public function transaction(Closure $callback)
    {
        $this->begin();
        try {
            $value = $callback();
            $this->commit();
            return $value;
        } catch (Exception $e) {
            $this->rollback();
            throw $e;
        }
    }

于是程序员们就可以这么干了:
public function actionTransfer($from, $to, $amount)
{
    DB::transaction(function () use ($from, $to, $amount) {
        $from->withdraw($amount);
        $to->deposit($amount);   
    })
}

当然,这个方案也不是完全没代价的,其中最重要的是,程序员们要严格遵守一个基本约定:**遇到错误的时候抛出异常**,以便封装层自动执行rollback。虽然也是约定,但是不像前面那两个约定那么晦涩,应该更容易遵守吧。

另外,如果在rollback之前还希望做一些清理操作(包括打log之类的),需要在callback里面再包一层 try-catch ;但如果遇到 commit 失败这种极端情况,清理操作仍无法被执行。这可以通过一个更复杂的约定(比如返回一个包含清理操作的 $clean_callback),但鉴于实际上不太有这种需要,得不偿失,所以大概就这样吧。

这种方案,在 C++ 里面有很多类似的例子,甚至还有一个专有名词 RAII (Resource Acquisition Is Initialization),“资源获取即初始化”,念起来有点拗口,其逻辑是在获取一个资源的时候立即初始化,并且在资源生命周期结束的时候自动销毁,避免因为程序员遗忘导致非预期的异常。常见的应用场景,包括打开文件(自动关闭)、多线程并发获取互斥锁(自动解锁)、申请内存资源(自动释放)等等。

如果你觉得看明白了,不妨试着写一个文件锁(flock)的代码练练手?
Mar 26
If you find this article helpful, you may like to donate to my ETH address:

  0x84D5084a0142a26081a2d06F3505cfc2CDaE9009


Detailed guide to compile viabtc_exchange_server on Ubuntu 16.04

## DEPENDENCIES ##
引用
$ sudo apt install -y libev-dev libjansson-dev libmpdec-dev libmysqlclient-dev libcurl4-gnutls-dev libldap2-dev libgss-dev librtmp-dev libsasl2-dev

# librdkafka: 0.11.3+; DO NOT INSTALL BY APT: version too old (0.8.x);
# if you do, remove them by: sudo apt remove librdkafka1 librdkafka-dev
$ wget https://github.com/edenhill/librdkafka/archive/v0.11.3.tar.gz -O librdkafka-0.11.3.tar.gz
$ tar zxf librdkafka-0.11.3.tar.gz
$ cd librdkafka-0.11.3
$ ./configure
$ make
$ sudo make install


## COMPILATION ##

引用
$ git clone https://github.com/viabtc/viabtc_exchange_server.git
$ cd viabtc_exchange_server

$ make -C depends/hiredis
$ make -C network

$ vi utils/makefile #modify INCS
# INCS = -I ../network -I ../depends
$ make -C utils

$ vi accesshttp/makefile #modify INCS & LIBS
# INCS = -I ../network -I ../utils -I ../depends
# LIBS = -L ../utils -lutils -L ../network -lnetwork -L ../depends/hiredis -Wl,-Bstatic -lev -ljansson -lmpdec -lrdkafka -lz -lssl -lcrypto -lhiredis -lcurl -Wl,-Bdynamic -lm -lpthread -ldl -lssl -lldap -llber -lgss -lgnutls -lidn -lnettle -lrtmp -lsasl2 -lmysqlclient
$ make -C accesshttp

$ vi accessws/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C accessws

vi alertcenter/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C alertcenter

$ vi marketprice/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C marketprice

$ vi matchengine/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C matchengine

$ vi readhistory/makefile
{modify INCS and LIBS like accesshttp/makefile}
$ make -C readhistory

Mar 14
之前接触以太坊的时候,确实能搜到很多资料,还有一个看起来很丰富的 Homestead Documentation,但这些材料都太不接地气了,看完还是不知道以太坊区块链到底长什么样,因此整理了这篇说明,希望能够在一定程度上解决这个问题吧。

# 1. 安装 ethereum (@ubuntu)

参考 官方说明
引用
sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum solc


# 2. 生成创世文件

将以下内容保存于 ~/.ethereum/genesis.json
引用
{
    "config": {
        "chainID": 1048576,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0,
        "ByzantiumBlock": 0
    },
    "alloc": { "0xc1de867b55fdb749be0c927ecf7b19451777042b" : { "balance" : "20000000000000000000"} },
    "coinbase": "0x0000000000000000000000000000000000000000",
    "difficulty": "0x0400",
    "extraData": "0x00",
    "gasLimit": "0x2fefd8",
    "nonce": "0xdeadbeefdeadbeef",
    "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "timestamp": "0x00"
}

说明:
1) 来源于 https://g2ex.github.io/2017/09/12/ethereum-guidance/
2) 以太坊公链的chainID是1,私有链需要使用不同的chainID
3) 指定 ByzantiumBlock 这个参数,getTransactionReceipt() 才有 status ,用于判断交易执行结果
4) alloc 中指定了预分配 20个eth 的地址,其私钥是 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449

# 3. 初始化私有链

$ cd ~/.ethereum
$ geth --datadir ~/.ethereum/ init ./genesis.json

# 4. 账户基本操作

关于以太坊的私钥、公钥、地址、钱包的说明,参见第8节。

## 4.1 导入账户

$ echo 22a0b3688dd46ab1a37d6237871913037681d57f628862336bc9c3c468c4a449 > ~/.ethereum/coinbase.key
$ geth account import ~/.ethereum/coinbase.key

输入密码后,会在 ~/.ethereum/keystore/ 目录下生成一个账户文件,对应的地址就是创世文件里预分配了 20 eth 的那个 0xc1de867b55fdb749be0c927ecf7b19451777042b

## 4.2 生成新账户

执行以下命令,输入密码后会随机生成一个新的账户

$ geth account new
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {a90bbc1870e06a84b3dcf92a98f55453e6404a32}

## 4.3 列出账户

$ geth account list
Account #0: {c1de867b55fdb749be0c927ecf7b19451777042b} keystore:///data/www/.ethereum/keystore/UTC--2018-02-09T13-11-19.026113731Z--c1de867b55fdb749be0c927ecf7b19451777042b
Account #1: {7aa99ede746e863e87cdd1e686abadb31325cb92} keystore:///data/www/.ethereum/keystore/UTC--2018-02-18T09-00-05.982653587Z--7aa99ede746e863e87cdd1e686abadb31325cb92

# 5. 启动私有链

$ geth --identity "TestNode" --rpc --rpcport 8545 --port 30303 --nodiscover console

说明:
1) nodiscover 表示不会去连接其他节点(单节点私有链)
2) console 表示启动 geth 的 javascript 控制台

# 6. 控制台操作

在上一步启动的控制台里执行以下命令;或者通过 geth attach 连接到已经启动的 geth 实例执行。

## 6.1 查看账户

引用
> eth.accounts
["0xc1de867b55fdb749be0c927ecf7b19451777042b", "0x7aa99ede746e863e87cdd1e686abadb31325cb92"]
> eth.coinbase
"0xc1de867b55fdb749be0c927ecf7b19451777042b"

一般来说 coinbase 账户(挖矿的收益账户)是第一个导入的账户,也就是 eth.accounts[0]。

## 6.2 查看余额

引用
> eth.getBalance(eth.coinbase)
20000000000000000000


## 6.3 转账

引用
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var ether = Math.pow(10, 18)
> var gwei = Math.pow(10, 9)
> tx_hash = eth.sendTransaction({from:eth.coinbase, to: eth.accounts[1], value: 1 * ether, gasPrice: 2 * gwei, gas: 21000, data: null})
"0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"
> eth.getTransaction(tx_hash)
{
  blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: null,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gas: 21000,
  gasPrice: 2000000000,
  hash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
  input: "0x",
  nonce: 0,
  r: "0xe99aa75fdb2d071632f7d7796229e3d84df60407007032f29b69296677df798c",
  s: "0x616be42ff2a41a153f953685dc497d58ab4cca6ace4eac70405c611f078fcf4b",
  to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
  transactionIndex: 0,
  v: "0x200024",
  value: 1000000000000000000
}


说明:
1) 以太币的最小单位 wei = 10^-18 ether
2) sendTransaction 的几个参数说明:
    from: 交易发起方地址,必填,需要先解锁
    to: 交易接收方地址,转账交易必填
    value: 转账金额,单位为 wei
    gasPrice: gas的价格,选填(geth会用估算值自动填充)
    gas: 交易消耗的最大gas数量,选填(转账交易正好消耗21000,少于这个数字geth会拒绝创建交易); 多出的部分会在交易成功以后返还
    data: 交易的输入数据,选填(转账交易不需要)
3) getTransaction 根据交易的hash获取交易详情,包括sendTransaction给出的参数,签名数据,和包含该交易的区块信息。由于当前还没有被确认,所以blockHash和blockNumber都是无效数据;特别提一下,nonce 其实也是 sendTransaction 的一个参数,相当于由该账户发起的交易的订单号,以太坊要求nonce严格有序递增(即下一笔订单的nonce一定是上一笔的nonce+1)。

关于gas和gasPrice,参见第9节。

由于交易还未被确认,所以暂不能得知交易的执行结果。

## 6.4 挖矿

引用
> eth.blockNumber
0
> miner.start();
> admin.sleepBlocks(1);
> miner.stop();
> eth.blockNumber
1
> eth.getBlock(1)
{
  difficulty: 131072,
  extraData: "0xd783010802846765746887676f312e392e34856c696e7578",
  gasLimit: 4076476,
  gasUsed: 21000,
  hash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
  logsBloom: "0x000(省略部分)000",
  miner: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  mixHash: "0x16639b8a9e2adaddbb9a48f1963b16ebdb997a528c4a1aeb4ac06e04501b56e9",
  nonce: "0x1cf42797e7b2c1d8",
  number: 1,
  parentHash: "0xc75321a9915b2a2c973528f713a2a587e28244a032e3fcd46a5915112269c6a4",
  receiptsRoot: "0xd95b673818fa493deec414e01e610d97ee287c9421c8eff4102b1647c1a184e4",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 764,
  stateRoot: "0x0de36a5d34f176a081fde3f26646b9368f28736ad01e96000c7ac05565213d53",
  timestamp: 1521027698,
  totalDifficulty: 35081353,
  transactions: ["0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6"],
  transactionsRoot: "0x4ae833bcf2114a237b86755453117da68876888d8c4215da8fb750016d5470f6",
  uncles: []
}


## 6.5 查看交易执行结果

引用
> eth.getTransaction(tx_hash)
{
  blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
  blockNumber: 267,
  ...
}
> eth.getTransactionReceipt(tx_hash)
{
  blockHash: "0xefbecf5907f9116d2f3abf46b0e6a2798939cdbd799249b46ad0a0169eaac8de",
  blockNumber: 267,
  contractAddress: null,
  cumulativeGasUsed: 42000,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gasUsed: 21000,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  status: "0x1",
  to: "0x45703fdcb21e1e129c9f61cec65a65b6aa8b7acd",
  transactionHash: "0x18629cdb404ef96873ebcbfbc97fee6e18a04c85eabecf70d57b0010684aecc6",
  transactionIndex: 1
}
> eth.getBalance(eth.accounts[1])
1000000000000000000


说明:

1) 交易已经被确认,所以 getTransaction 可以看到区块信息
2) getTransactionReceipt 得到交易回执,其中:
    gasUsed: 实际消耗的gas(转账交易总是21000,除非指定了额外的data)
    status: "0x1" 表示成功, "0x0" 表示失败
    contractAddress: 新部署的合约地址;这个交易只是普通转账交易,不会部署合约,所以是null
    logs: 记录发送给合约的交易触发的事件,普通转账交易不会产生事件,所以是 [] 。

# 7. 合约

以太坊的账户分成两种,一种是EOA账户(Externally Owned Account,由私钥控制),里面存储了账户的以太币余额信息和由该账户发起的交易数量(nonce),上面创建的都是EOA账户。另一种是合约账户(Contract Account),是由合约部署交易创建的,里面除了存储以太币余额,还存储了合约代码,以及代码的状态(存储)。通过创建并发送由某个EOA账户私钥签名的交易,可以从该账户给其他EOA账户转账,或者向某个合约账户发起一笔交易(调用合约账户里的某个方法);而合约账户只能被动触发,接受某个EOA账户(或其他被触发的合约账户)发起的交易,根据该交易的指令执行相应的动作,修改内部状态,或向其他账户发出交易。

所谓合约,并不是某个要被执行的合同,其实质是一段代码,可以类比为一个Class,其中定义了一些方法、状态、事件。将这段代码编译成以太坊虚拟机(Ethereum Virtual Machine,简称EVM)机器码,就可以作为 sendTransaction() 方法的 data 参数,被部署到以太坊区块链上(会自动分配一个地址),这样就创建了一个合约账户。后续按照EVM ABI约定发送到该合约账户的交易,可以指定触发这段代码的相应方法执行预定的操作。

这样的描述有点抽象,下面就用具体的栗子来说明合约账户。

## 7.1 ERC20

以太坊上线几年了,目前为止最成熟的应用就是发行 ERC20 Token ,圆了好多人的会所嫩模梦。所谓 ERC20 ,实际上就是一个 Interface ,凡是遵守这个 Interface 的合约,都可以被相同的方式触发。

具体的 ERC20 Interface 实现了多个方法和事件,详见Wiki:https://theethereum.wiki/w/index.php/ERC20_Token_Standard

下面的栗子里,只挑最重要的 balanceOf 和 transfer 这两个方法,以及 Transfer 这个事件:
引用
contract ERC20Interface {
    function balanceOf(address tokenOwner) public constant returns (uint balance);
    function transfer(address to, uint tokens) public returns (bool success);
    event Transfer(address indexed from, address indexed to, uint tokens);
}

从interface的定义可以比较直接的看懂,balanceOf用于查询某个地址的余额,tranfer用于转账到某个地址;如果转账成功,transfer方法会触发一个 Transfer 事件,记录在区块链上。

下面看一个具体的代码 (保存为 token.sol ):

引用
pragma solidity ^0.4.0; //指定solidity编译器的最小版本

contract MyToken {
    string public name; //token的名称,例如 Tether USD
    string public symbol; //token的代码,例如 USDT
    uint8 public decimals; //token的精度,例如4表示该token精确到小数点后4位
    mapping (address => uint256) public balances; //该合约的账本,记录某地址的token余额

    event Transfer(address indexed from, address indexed to, uint256 value);

    /* 合约的初始化函数,在部署合约时被调用,指定供应量等信息 */
    function MyToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) {
        balances[msg.sender] = initialSupply;
        name = tokenName;
        symbol = tokenSymbol;
        decimals = decimalUnits;
    }

    function transfer(address _to, uint256 _value) {
        /* 检查发送方余额充足,且不会溢出 */
        require(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value); //关注该合约的用户可以得知发生了一笔转账事件
    }

    function balanceOf(address tokenOwner) public constant returns (uint balance) {
        return balances[tokenOwner]; //查询某个地址的token余额
    }
}


## 7.2 编译合约

引用
$ solc token.sol --bin --abi
...(编译器warning等)...
======= token.sol:MyToken =======
Binary:
6060604052341{合约被编译后产生的EVM字节码}fd000029
Contract JSON ABI
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balances","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"tokenOwner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"},{"name":"tokenName","type":"string"},{"name":"tokenSymbol","type":"string"},{"name":"decimalUnits","type":"uint8"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]


## 7.3 部署合约

从上面的编译结果 copy 字节码 和 abi json,执行下面的命令:

引用
$ geth attach #连接到 geth 的 console
> var code = '0x6060604052341{EVM字节码}fd000029'; //这里以字符串形式保存字节码
> var abi = [{"constant":true,....:"event"}]; //注意这里不是字符串,而是json数组
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> var contract = eth.contract(abi).new(1000000, 'Test Token', 'TEST', 2, {from: eth.coinbase, data: code, gas: 1000000})
> contract.transactionHash
"0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c"

创建合约交易:初始化参数、合约部署方、gas limit;因为没有接收方,也不需要发送以太币,所以省略to和value

挖矿确认交易,然后看看部署的结果:
引用
> miner.start(); admin.sleepBlocks(1); miner.stop();
true
> eth.getTransaction(contract.transactionHash)
{
  blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
  blockNumber: 2,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gas: 1000000,
  gasPrice: 1000000000,
  hash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
  input: "0x606060{EVM字节码}fd000029{合约初始化参数的ABI编码}",
  nonce: 1,
  r: "0x32c5cb0c44b03dd273b6973f59f85c8e09d0d6212a38263b3c199329dbbc9ef",
  s: "0x758efe028c494fc8e637792aa3e09adceb1e7cdafb8567d4a7eecf95f54accc4",
  to: null,
  transactionIndex: 0,
  v: "0x200024",
  value: 0
}
> eth.getTransactionReceipt(contract.transactionHash)
{
  blockHash: "0xe9ec25459ba61cd2f4026cdf4ace12f450ec263ece677f543a92565e50376d9f",
  blockNumber: 2,
  contractAddress: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
  cumulativeGasUsed: 594886,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gasUsed: 594886,
  logs: [],
  logsBloom: "0x0000{省略一些0}0000",
  status: "0x1",
  to: null,
  transactionHash: "0xddc0bf88974d203ac022210dff19d315c1b2eac7de27cbee2800789e0fa9619c",
  transactionIndex: 0
}
> contract.address
"0x52ce1b10f214abb88517ceb3de2fbfbbfc305791"


可以看到,发送的交易在被挖矿确认以后,部署到了 0x52ce1b10f214abb88517ceb3de2fbfbbfc305791 这个地址,消耗了 594886 个 gas 。这样就“发行”了一个ERC20 Token,名字是 Test Token,代码 TEST ,一共发行 10000 个,每个 token 精确到小数点后 2 位。

## 7.4 调用合约

上面的 contract 变量就是合约对象了,但不能每次想调用一个合约都得创建它,通常我们是需要直接调用某个合约地址里的方法,那就需要用这种方式创建合约对象:
引用
> var abi = [{"contract"...}];
> var contract = eth.contract(abi).at("0x52ce1b10f214abb88517ceb3de2fbfbbfc305791")

余额查询:
引用
> contract.balanceOf.call(eth.coinbase)
1000000

可以看到,由于我们是用 coinbase 帐号发起的交易,合约在初始化的时候,根据合约代码的实现,把初始发行的 10000 个 TEST 都记到了 coinbase 帐号名下。要特别注意的是,这个账,不是记在 coinbase 这个EOA帐号的存储里,而是记在这个合约帐号的存储(代码里balances这个map)里面。

转账(误):
引用
> contract.transfer.call(eth.accounts[1], 100)
[]
> contract.balanceOf(eth.accounts[1])
0

可以看到,虽然调用了transfer方法,但是 eth.accounts[1] 的余额并没变化。这是因为transfer方法是要修改存储的,而以太坊上的状态,只能由被区块链确认了的交易出发的动作修改。

那就试试往区块链上发送一笔转账交易:
引用
> personal.unlockAccount(eth.coinbase)
Unlock account 0xc1de867b55fdb749be0c927ecf7b19451777042b
Passphrase:
true
> tx_hash = contract.transfer.sendTransaction(eth.accounts[1], 100, {from:eth.coinbase, gas:100000})
"0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad"


挖矿确认这笔交易,然后查看执行结果:
引用
> miner.start(); admin.sleepBlocks(1); miner.stop();
true
> contract.balanceOf(eth.accounts[1])
100
> contract.balanceOf(eth.coinbase)
999900
> eth.getTransaction(tx_hash)
{
  blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
  blockNumber: 3,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gas: 100000,
  gasPrice: 1000000000,
  hash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
  input: "0xa9059cbb00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd0000000000000000000000000000000000000000000000000000000000000064",
  nonce: 2,
  r: "0x53b6312127838fe8cdca5209b51d33175f370b2a0921215d07f2df9a572c120d",
  s: "0xd18471632a066386c42a42226f441442f73e0ba39b5a9b6db58a283d957579a",
  to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
  transactionIndex: 0,
  v: "0x200024",
  value: 0
}
> eth.getTransactionReceipt(tx_hash)
{
  blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
  blockNumber: 3,
  contractAddress: null,
  cumulativeGasUsed: 51595,
  from: "0xc1de867b55fdb749be0c927ecf7b19451777042b",
  gasUsed: 51595,
  logs: [{
      address: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
      blockHash: "0x6c993eb1b003a3e315ad15a4a8544c8a001d5f76d4dce614ea840c0d3da4b49d",
      blockNumber: 271,
      data: "0x0000000000000000000000000000000000000000000000000000000000000064",
      logIndex: 0,
      removed: false,
      topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b", "0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
      transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
      transactionIndex: 0
  }],
  logsBloom: "0x0000010{省略部分数据}0000000000000",
  status: "0x1",
  to: "0x52ce1b10f214abb88517ceb3de2fbfbbfc305791",
  transactionHash: "0x061fc0ff8945257e30fc2d243e2888a4a1668856b7b7df8996fb9f01b5fa3bad",
  transactionIndex: 0
}

向区块链发送一个交易,等到交易被确认以后,再查询 eth.accounts[1] 的 token 余额,就可以看到发生了变化。

这里有几个比较重要的信息:

1) getTransaction 返回结果里的 input

之前的普通转账交易,input的内容(就是sendTransaction里指定的data)都是 "0x" ,也就是 0 字节,无数据。而这个交易,有了一个比较复杂的值。这个值可以被拆成3段:

    0xa9059cbb
        => 按照evm abi的约定,这是合约方法的签名的前4个字节。签名是用sha3对方法的定义进行hash计算出来的
        => 例如这个 0xa9059cbb 就来源于 sha3("transfer(address,uint256)")
    00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd
        => 这就是接收方的地址,但用32字节表示,前面用0补齐
    0000000000000000000000000000000000000000000000000000000000000064
        => 这是转账金额,32字节无符号整数,0x64 = 100

2) getTransactionReceipt 返回结果里的 logs
    logs是个数组,其中的每一项代表一个被触发的 event ,这个栗子里,logs[0].topics 包含3个元素:
   
    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
        => 这是事件的签名,sha3("Transfer(address,address,uint256)")
    "0x000000000000000000000000c1de867b55fdb749be0c927ecf7b19451777042b"
        => 这是发送方地址,也就是 eth.coinbase
    "0x00000000000000000000000045703fdcb21e1e129c9f61cec65a65b6aa8b7acd"],
        => 这是接受方地址,也就是 eth.coinbase

    因为 Transfer 事件的定义里,from和to都带了一个 indexed 修饰符,所以会被记录到 topics 里,而没带 indexed 修饰符的 value ,则被记录在了 logs[0].data 里。

# 8. 以太坊的私钥、公钥、地址、钱包

以太坊和比特币一样,都是用的椭圆曲线加密算法 ECDSA-secp256k1。通常将私钥记为 k ,公钥记为 K 。下面使用node,安装bitcore-lib来演示。
引用
$ node
> var bitcore = require("bitcore-lib")


## 8.1 私钥

私钥 k 通常是随机选出的 32 字节数据(可以看作一个 uint256 大整数),但要特别注意,不能使用伪随机数生成器。更准确地说,私钥可以是1和n-1之间的任何数字,其中n是一个常数(n=1.158 * 10^77,略小于2^256)。从编程的角度来看,一般是通过在一个密码学安全的随机源中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。如果运算结果小于n-1,我们就有了一个合适的私钥。否则,我们就用另一个随机数再重复一次。

引用
> var k = bitcore.PrivateKey()
> k.toBuffer().hexSlice()
'3f4d9a11b162c61af91c63e601b0dadaec09c74abb7f6659364549011b0dcf15'


## 8.2 公钥

将私钥 k 与一个指定的生成点 G 运算,就能得到公钥 K = k * G。K是椭圆曲线上的一个点 (x, y)。其中x和y都是uint256大整数。在以太坊里,公钥的二进制存储格式为 x + y,一共64字节。

引用
> var K = k.toPublicKey()
> K.point.x.toBuffer().hexSlice()
'fbc1994297bd2bdd5a4464ae7f6c88338587a175faffeb414fb2717207638435'
> K.point.y.toBuffer().hexSlice()
'b5d1a6015fc9fc6cf947046213cfb7d3341491934a90a2e3880af6a940cdd1df'
> var K_buffer = Buffer.concat([K.point.x.toBuffer(), K.point.y.toBuffer()])


## 8.3 地址

通过对公钥K做sha3(keccak256)哈希,得到一个32字节的结果,取其中的后20字节,就是以太坊的地址了:

引用
> const SHA3 = require('keccakjs')
> addr = '0x' + SHA3(256).update(K_buffer).digest('hex').slice(-40)
'0x2864cdd53410820bc684f746871e9820fbb57243'


这样就得到了一个以太坊的地址。为了检测在地址传递过程中发生的错误,EIP55给出了一个校验码方案,其思路很简单,对地址再做一次sha3,根据哈希结果修改地址中对应位置的字符大小写,详细方案参见:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md

加上校验码后,地址长这样:0x2864Cdd53410820BC684F746871E9820fBb57243

## 8.4 钱包

由于以太坊和比特币都用的是 secp256k1 ,所以很多以太坊钱包是参考BIP32+BIP44实现的(包括KeepKey这样的硬件钱包),其中Coin Type用的是60H。

详情参考BIP44:https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

# 9. Gas/GasLimit/GasPrice

为了避免死循环导致的拒绝服务攻击,比特币区块链的目标是实现一个点对点的电子现金系统,因此其内置的脚本不是图灵完备的,不支持循环,只能完成一些条件判断。

以太坊有更大的野心,实现了一个图灵完备的EVM。为了解决这个拒绝服务攻击的问题,以太坊实现了这样一个机制:为了让EVM这台机器运转起来,你需要给它提供“加密燃料”——gas。gas是用以太币计价的,因此在发送交易的时候,需要指定购买gas的价格——gasPrice。由于停机问题不可解,为了避免过度消耗,交易发送者还需要指定gasLimit(对应sendTransaction方法的gas参数),表示他愿意为这笔交易的执行支付的最大gas数量。

当EVM收到开始执行一个交易的时候,会先冻结 value + gas * gasPrice 这些以太币(转账金额 + 为这笔交易愿意支付的最大金额),然后开始执行交易。以太坊的黄皮书里定义了不同操作需要消耗的gas数量,EVM在执行过程中,会根据定义好的规则累计消耗的gas总量。如果交易尚未执行完,但所有gas都消耗完,EVM会回滚所有改动,并将交易标记为失败(getTransactionReceipt返回的status='0x0')。无论交易最终是否成功,EVM累计消耗的gas都会被扣除,记录在 receipt 的 gasUsed 字段里,剩余的gas则按gasPrice换成以太币,退给交易发送者。特别地,普通转账交易消耗的gas数量正好是21000。作为以太坊网络的维护者和验证者,挖出包含这个交易的block的矿工可以获得这些被消耗的gas。

因此,如果有人要用死循环来恶意攻击以太坊网络,他就要付出无限的以太币。


# 10. 其他

## 10.1 以太币的总量

以太坊正式上线时,发行了72002454个以太币,每年会按这个数字的25%增发(即不超过1800万)。截止到目前(2018-03-17),流通总量为9823万(数据来源于 coinmarketcap.com)。

## 10.2 大整数:十六进制

由于以太币的最小单位 wei = 1/10^18 ether,所以连 UINT64 都无法保存所有的以太币。很多语言没有提供原生的大整数支持,所以以太坊的rpc接口传递大整数时都使用16进制编码,由语言自行处理。

## 10.3 版本

以太坊最早于是2015年5月上线 Olympic 测试网,7月30日上线 Frontier 版本。Frontier是beta版本,供开发者学习、体验,直到2016年3月14日(圆周率日)上线了正式的 Homestead 版本。

以太坊后续还规划了 Metropolis 和 Serenity 两个版本,上线时间尚未确定。

## 10.4 挖矿和共识机制

Homestead 的共识机制与比特币一样,都是PoW(Proof of Work),但使用了一个基于DAG的Ethash算法,对内存的要求较高,因此像ASIC矿机这种大规模嵌入运算单元的方式无法加速挖矿效率,从而尽量避免挖矿的中心化,增加网络的安全性。

在接下来的Metropolis版本里,以太坊计划切换为PoS机制,提高出块的效率。

## 10.5 还有啥?

想起来再补吧。
Mar 10

简单磁盘性能测试 不指定

felix021 @ 2018-3-10 20:39 [IT » 硬件] 评论(0) , 引用(0) , 阅读(310) | Via 本站原创
引用
$ sudo apt-get install fio

# 如下命令会创建一个 512M 的文件,测试 64 线程的 4KB 随机读写,75%读,25%写。
# readwrite=randrw表示测试时混合随机读取和随机写入请求,rwmixread指定读请求的占比
$ fio --randrepeat=1 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=test --filename=test --bs=4k --iodepth=64 --size=512M --readwrite=randrw --rwmixread=75

test: (g=0): rw=randrw, bs=4K-4K/4K-4K/4K-4K, ioengine=libaio, iodepth=64
fio-2.2.10
Starting 1 process
test: Laying out IO file(s) (1 file(s) / 512MB)
Jobs: 1 (f=1): [m(1)] [100.0% done] [9268KB/3280KB/0KB /s] [2317/820/0 iops] [eta 00m:00s]
test: (groupid=0, jobs=1): err= 0: pid=13468: Sat Mar 10 19:48:40 2018
  read : io=392888KB, bw=9620.5KB/s, iops=2405, runt= 40839msec
  write: io=131400KB, bw=3217.6KB/s, iops=804, runt= 40839msec
  cpu          : usr=0.54%, sys=1.87%, ctx=32291, majf=0, minf=9
  IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
    submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
    complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
    issued    : total=r=98222/w=32850/d=0, short=r=0/w=0/d=0, drop=r=0/w=0/d=0
    latency  : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
  READ: io=392888KB, aggrb=9620KB/s, minb=9620KB/s, maxb=9620KB/s, mint=40839msec, maxt=40839msec
  WRITE: io=131400KB, aggrb=3217KB/s, minb=3217KB/s, maxb=3217KB/s, mint=40839msec, maxt=40839msec

Disk stats (read/write):
  xvdb: ios=98056/32790, merge=0/6, ticks=923380/318272, in_queue=1241828, util=99.83%


这是一个 aws上 的 1000GB EBS 的测试报告,按照 3 IOPS/GB 计算应该能有 3000 的 IOPS,可以看到确实达到了 aws 宣称的性能水平。

参考文章:https://www.binarylane.com.au/support/solutions/articles/1000055889-how-to-benchmark-disk-i-o
Mar 2
我注意到过去几个月我司有些同学还在踩一个简单的分布式事务Case的坑,而这个坑在两年以前就已经有同学踩过了,这里简单解析一下这个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();
}

乍一看好像是没有什么毛病,测试的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();
}

把这个实现代入多个不同的业务下,还会衍生出更多问题,比如被动代扣业务,就可能因为重试导致用户被多次扣款,引起投诉(支付通道对投诉率的要求非常严格,甚至可能导致通道被关停);更严重的是放款业务,可能出现重复放款,给公司造成直接损失。据说某友商就是因为重复放款倒闭的,所以在实现类似业务时特别注意,考虑周全。

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);
        }
    }
}

在实际执行这段代码的时候,如果没有仔细测试(尤其是在有补单逻辑,捞出未执行成功的划拨指令再发送给银行),可能就不会发现,实际上有很多指令并不是马上被执行的,因为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);
    }
}

注:foreach要放到try-catch后面。

最后想说,我相信有很多同学知道这个Case,或者就算不知道也不会犯这样的错误,因此也许会觉得没必要专门揪出来这样分享 —— 但“知识的诅咒”就是这样,“我会的东西都是简单的”,然而对于没有踩过坑的同学来说,其实都是宝贵的经验;另一方面,有些别人觉得简单的问题、踩过的坑,也许自己是不知道的。所以希望大家都能分享自己在工作学习中踩过的坑、解决过的问题,互相交流,互相提高。
Feb 28

自建 BTC RegTest 测试网络 不指定

felix021 @ 2018-2-28 18:35 [IT » Blockchain] 评论(0) , 引用(0) , 阅读(868) | Via 本站原创
# 1. 编译安装 (@ubuntu)
引用
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


# 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

然后直接调用 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"
    ]
  }
]
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"
分页: 1/93 第一页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]