您正在查看: EOS 分类下的文章

使用history-tool 同步链上数据到PostgreSQL

由于官方的工具history-tool,对比mongo插件,history-tool效率更高,支持PostgreSQL/RocksDB。并且避免了因为意外写入导致链程序意外退出后,数据脏的麻烦问题,并且pg会修复微分叉的交易,所以今天试用下history-tool方案

编译 fill-pg

主要参考 https://eosio.github.io/history-tools/build-ubuntu-1804.html

安装环境依赖

安装Clang 8 和其他需要的工具

sudo apt update && sudo apt install -y wget gnupg

cd ~
sudo wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -

sudo vi /etc/apt/sources.list
## 文件尾部添加
deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic main
deb-src http://apt.llvm.org/bionic/ llvm-toolchain-bionic main
deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-8 main
deb-src http://apt.llvm.org/bionic/ llvm-toolchain-bionic-8 main

sudo apt update && sudo apt install -y \
    autoconf2.13        \
    build-essential     \
    bzip2               \
    cargo               \
    clang-8             \
    git                 \
    libgmp-dev          \
    libpq-dev           \
    lld-8               \
    lldb-8              \
    ninja-build         \
    nodejs              \
    npm                 \
    pkg-config          \
    postgresql-server-dev-all \
    python2.7-dev       \
    python3-dev         \
    rustc               \
    zlib1g-dev

sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-8 100
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-8 100

生成并安装Boost 1.70。调整-j10以匹配您的机器。如果您没有足够的RAM用于所使用的内核数量,则会发生不好的事情

cd ~
wget https://dl.bintray.com/boostorg/release/1.70.0/source/boost_1_70_0.tar.gz
tar xf boost_1_70_0.tar.gz
cd boost_1_70_0
./bootstrap.sh
sudo ./b2 toolset=clang -j10 install

生成并安装CMake 3.14.5。调整--parallel=并-j匹配您的机器。如果您没有足够的RAM用于所使用的内核数量,则会发生不好的事情:

cd ~
wget https://github.com/Kitware/CMake/releases/download/v3.14.5/cmake-3.14.5.tar.gz
tar xf cmake-3.14.5.tar.gz
cd cmake-3.14.5
./bootstrap --parallel=10
make -j10
sudo make -j10 install

编译history-tools

cd ~
git clone --recursive https://github.com/EOSIO/history-tools.git
cd history-tools
mkdir build
cd build
cmake -GNinja -DCMAKE_CXX_COMPILER=clang++-8 -DCMAKE_C_COMPILER=clang-8 ..
git submodule update --init --recursive
bash -c "cd ../src && npm install node-fetch"
ninja

此时查看build目录则生成了fill-pg

配置使用

参考 https://eosio.github.io/history-tools/database-fillers.html
首次运行需要创建所需的表,执行参数--fpg-create
如果肺首次执行,需要清理,则添加参数--fpg-drop --fpg-create
--fill-connect-to 需要连接的state-history-plugin endpoint,默认值127.0.0.1:8080
我们先演示不加其他参数的运行实例,如果需要其他的参数,比如过滤,请查看文档--fill-trx

测试运行的命令行参数如下

export PGUSER=       // PostgreSQL用户名
export PGPASSWORD=   // PostgreSQL密码
export PGDATABASE=   // PostgreSQL数据库名
export PGHOST=       // PostgreSQL访问host
export PGPORT=       // PostgreSQL端口

./fill-pg --fill-connect-to 127.0.0.1:8080 --fpg-create

连接后创建的表如下

表名 介绍
account
account_metadata
action_trace
action_trace_auth_sequence
action_trace_authorization
action_trace_ram_delta
action_trace_v1
block_info
code
contract_index_double
contract_index_long_double
contract_index64
contract_index128
contract_index256
contract_row
contract_table
fill_status
generated_transaction
permission
permission_link
protocol_state
received_block
resource_limits
resource_limits_config
resource_limits_state
resource_usage
transaction_trace

测试

查看端口有没有正常监听

sudo lsof -i -P -n | grep LISTEN

如果想测试history节点ws端口可以使用

wget https://github.com/vi/websocat/releases/download/v1.6.0/websocat_arm-linux-static
mv websocat_amd64-linux-static websocat
./websocat ws://127.0.0.1:8080/

注意

目前history-tools方案还处于试验阶段,目前测试遇到问题
https://github.com/EOSIO/history-tools/issues/103
等待后面有时间再继续跟进

根据公钥生成账户名

/**
 * @module AccountName
 */
import * as bs58 from 'bs58';

import * as Long from 'long';

const { PublicKey } = require('./ecc');

/**
    Hashes a public key to a valid FIOIO account name.
    @arg {string} pubkey
    @return {string} valid FIOIO account name
*/
export function accountHash(pubkey : string) : string {
    if(!PublicKey.isValid(pubkey, 'EOS')) {
        throw new TypeError('invalid public key');
    }

    pubkey = pubkey.substring('EOS'.length, pubkey.length);

    const decoded58 = bs58.decode(pubkey);
    const long = shortenKey(decoded58);

    const output = stringFromUInt64T(long);
    return output;
}

function shortenKey(key : [number]) : any {
  var res = Long.fromValue (0, true);
  var temp = Long.fromValue (0, true);
  var toShift = 0;
  var i = 1;
  var len = 0;

  while (len <= 12) {
      //assert(i < 33, "Means the key has > 20 bytes with trailing zeroes...")
      temp = Long.fromValue(key[i], true).and(len == 12 ? 0x0f : 0x1f);
      if (temp == 0) {
          i+=1
          continue
      }
      if (len == 12){
        toShift = 0;
      }
      else{
        toShift = (5 * (12 - len) - 1);
      }
      temp = Long.fromValue(temp, true).shiftLeft(toShift);

      res = Long.fromValue(res, true).or(temp);
      len+=1
      i+=1
  }

  return res;
}

function stringFromUInt64T(temp : any) : string{
  var charmap = ".12345abcdefghijklmnopqrstuvwxyz".split('');

  var str = new Array(13);
  str[12] = charmap[Long.fromValue(temp, true).and(0x0f)];

  temp = Long.fromValue(temp, true).shiftRight(4);
  for (var i = 1; i <= 12; i++) {
      var c = charmap[Long.fromValue(temp, true).and(0x1f)];
      str[12 - i] = c;
      temp = Long.fromValue(temp, true).shiftRight(5);
  }
  var result = str.join('');
  if (result.length > 12) {
      result = result.substring(0, 12);
  }
  return result;
}

测试

describe('accountname', () => {
    it('matches', () => {
        const samplekey = "EOS7isxEua78KPVbGzKemH4nj2bWE52gqj8Hkac3tc7jKNvpfWzYS";
        const accountHash = AccountName.accountHash(samplekey);
        expect(accountHash).toEqual('p4hc54ppiofx');
    })
})

激活 ACTION_RETURN_VALUE

激活动作返回值协议

cleos push action eosio activate '["c3a6138c5061cf291310887c0b5c71fcaffeab90d5deb50d3b9e687cead45071"]'

查询是否已开启

curl -X POST http://localhost:8888/v1/producer/get_supported_protocol_features | jq -r
...
{                                                                                                                                      
    "feature_digest": "c3a6138c5061cf291310887c0b5c71fcaffeab90d5deb50d3b9e687cead45071",
    "subjective_restrictions": {
      "enabled": true,
      "preactivation_required": true,
      "earliest_allowed_activation_time": "1970-01-01T00:00:00.000"
    },
    "description_digest": "69b064c5178e2738e144ed6caa9349a3995370d78db29e494b3126ebd9111966",
    "dependencies": [],
    "protocol_feature_type": "builtin",
    "specification": [
      {
        "name": "builtin_feature_codename",
        "value": "ACTION_RETURN_VALUE"
      }
    ]
  }
...

EOS 链mongo 节点数据分片shard key 选取

由于目前旧链还是基于链mongo同步节点存储数据,mongo服务使用的阿里云云数据库 MongoDB,单片存储已经接近最大存储值(2T),所以需要做分片扩容。
主要的注意点有

  1. 每个表 shard key的选择,以及如何设置
  2. 检查各片均衡同步是否正常

shard key选择

每个表shard key的选择要查看链源码mongo插件源码中的upsert(true)所对应的update_one的查询索引进行一致设置,否则会报错

codeName" : "ShardKeyNotFound", "errmsg" : "Failed to target upsert by query :: could not extract exact shard key"

upsert = true, 是说使用update_one代替insert_one,这样方便去重,如果不存在则insert,存在则update。
对于mongo分片集合建立的索引要与各表upsert=trueupdate_one所需的查询索引一致。

对于upsert=true 各表索引如下

account_controls
  1. controlled_account
  2. controlled_permission
  3. controlling_account
accounts
  1. name
action_traces

block_states
  1. block_id

如果开启了mongodb-update-via-block-num

  1. block_num
blocks
  1. block_id

如果开启了mongodb-update-via-block-num

  1. block_num
pub_keys
  1. account
  2. public_key
  3. permission
transaction_traces

transactions
  1. trx_id

设置shard key

参考: https://helpcdn.aliyun.com/document_detail/99411.html

所在的数据库启用分片功能

sh.enableSharding("EOS")

transactions

db.transactions.createIndex({trx_id:1})             // 对片键的字段建立索引
sh.shardCollection("EOS.transactions",{"trx_id":1}) // 对集合设置数据分片。
trx_id 不存在添加shard key出错
"message" : "There are documents which have missing or incomplete shard key fields ({ : null }). Please ensure that all documents in the collection include all fields from the shard key.",

将没有trx_id的记录删掉,此记录为无效数据

db.transactions.deleteMany({trx_id: {$exists: false}})
generic server error

由于空交易块,会不存在trx_id,导致upsert失败,所以将空块排除
config增加

mongodb-filter-out = eosio:onblock:

blocks

db.blocks.createIndex({block_id:1})             // 对片键的字段建立索引
sh.shardCollection("EOS.blocks",{"block_id":1}) // 对集合设置数据分片。

block_states

db.block_states.createIndex({block_id:1})             // 对片键的字段建立索引
sh.shardCollection("EOS.block_states",{"block_id":1}) // 对集合设置数据分片。

account_controls

db.account_controls.createIndex({controlled_account:1})                // 对片键的字段建立索引
db.account_controls.createIndex({controlled_permission:1})             // 对片键的字段建立索引
db.account_controls.createIndex({controlling_account:1})               // 对片键的字段建立索引

sh.shardCollection("EOS.account_controls",{"controlled_account":1})    // 对集合设置数据分片。

accounts

db.accounts.createIndex({name:1})             // 对片键的字段建立索引
sh.shardCollection("EOS.accounts",{"name":1}) // 对集合设置数据分片。

pub_keys

db.pub_keys.createIndex({account:1})                // 对片键的字段建立索引
db.pub_keys.createIndex({public_key:1})             // 对片键的字段建立索引
db.pub_keys.createIndex({permission:1})             // 对片键的字段建立索引
sh.shardCollection("EOS.pub_keys",{"account":1})    // 对集合设置数据分片。
sh.shardCollection("EOS.pub_keys",{"public_key":1}) // 对集合设置数据分片。
sh.shardCollection("EOS.pub_keys",{"permission":1}) // 对集合设置数据分片。

其他表

对于其他表,并没有用到upsert=true,所以可以根据需要做处理

action_traces
db.action_traces.createIndex({trx_id:1})             // 对片键的字段建立索引
sh.shardCollection("EOS.action_traces",{"trx_id":1}) // 对集合设置数据分片。
transaction_traces
db.transaction_traces.createIndex({id:1})             // 对片键的字段建立索引
sh.shardCollection("EOS.transaction_traces",{"id":1}) // 对集合设置数据分片。

操作细节

买好阿里的云数据库 MongoDB,再附加买一个分片,此时查看db.stats()
能看到2个分片存储

{
    "raw" : {
        "mgset-40806644/..." : {
            "db" : "EOS",
            ...
            "ok" : 1.0
        },
        "mgset-40806643/..." : {
            "db" : "EOS",
            ...
            "ok" : 1.0
        }
    },
    "objects" : 767319,
    ...
}

配置好同步节点,先往数据库推送链数据
此时看到,只有mgset-40806643在增加数据,因为我们还没有让数据库做分片处理

设置分片

细节就是执行上面的设置shard key
每增加一个shardCollection,发现"mgset-40806644中的collections会对应的增加(需要数量到一定级别(2个chunk以上,1个chunk默认是64MB,sh.status()查看chunk数量)才会做均衡,第一次均衡后新加的分片才会创建对应的集合)
添加shardCollection后,对应集合的索引会自动同步过去
查看对应表的stats,比如db.action_traces.stats(),能看到sharded = true

注意

对于上面的操作,大部分索引已经默认添加,无需重复操作,但是对于用到upsert=true的表shardCollection必须与插件一致,否则报ShardKeyNotFound

参考

https://docs.mongodb.com/manual/core/sharding-shard-key/?spm=a2c4g.11186623.2.25.544e1d8d2ORfYO#choosing-a-shard-key
https://help.aliyun.com/document_detail/100658.html?spm=a2c4g.11186623.6.762.5b3155f12yRKuU
https://helpcdn.aliyun.com/document_detail/99411.html

get_table对于time_point类型得定位查询

根据场景需求需要对已存储得数据做更新查询,所以需要对update_timepoint做查询索引

struct [[eosio::table, eosio::contract("bcskill.com")]] xxxx_info {
        uint64_t id;                                        // 记录id
        eosio::checksum256 trx_id;                          // 交易id
        ....
        time_point create_timepoint;                        // 创建时间
        time_point update_timepoint;                        // 更新时间

        uint64_t primary_key() const { return id; }
        eosio::checksum256 second_key() const { return trx_id; }
        uint64_t third_key() const { return update_timepoint.time_since_epoch().count();} // 把time_point类型转换成微妙(1/1000000 秒)
    };

table中测试数据如下

{
  "rows": [{
      "id": 0,
      "trx_id": "d78c4da408a4d9c75ba03af4a481d79fa80e2b8e215e68a1fe1865fd866cc7bc",
      ...
      "create_timepoint": "2020-12-17T11:24:07.500",
      "update_timepoint": "2020-12-17T11:27:18.500"
    },{
      "id": 1,
      "trx_id": "7bb0038be49b171f8a394bfdd7da15f9a005908b25ca1b6aed31b2975d721730",
      ...
      "create_timepoint": "2020-12-17T11:29:01.500",
      "update_timepoint": "2020-12-17T11:30:01.500"
    },{
      "id": 2,
      "trx_id": "8cf1c32d38692dd93870c7a2376d617f85efa4f093588dbce94f9981d4e1818e",
      ...
      "create_timepoint": "2020-12-17T11:30:01.500",
      "update_timepoint": "2020-12-17T11:30:01.500"
    }
  ],
  "more": false
}

UTC时间转时间戳,目前没有找到现成的工具可以直接转换,
https://tool.lu/timestamp/ 只找到这个北京时间转时间戳的工具(稍后找到其他直接工具,或者单独开发个工具再做文章更新)

UTC 转北京时间(+8)

暂时先将UTC时间加8小时转换成本地时间,测试的UTC时间为2020-12-17T11:27:18.500,转换为本地时间为2020-12-17 19:27:18 其中的.500为0.5秒,后面再单独加上。

北京时间转时间戳

利用上面的在线工具2020-12-17 19:27:18得到1608204438,加上上面的0.500得到1608204438.5

根据合约类型需要转换为微妙

1608204438.5 * 1000000 得到 1608204438500000

根据测试数据演示下查询

cleos get table bcskillsurou bcskillsurou ammtb --index 3 --key-type i64 -L 1608204438500000 -U 1608204438500000

得到指定的记录

{
  "rows": [{
      "id": 0,
      "trx_id": "d78c4da408a4d9c75ba03af4a481d79fa80e2b8e215e68a1fe1865fd866cc7bc",
      ...
      "create_timepoint": "2020-12-17T11:24:07.500",
      "update_timepoint": "2020-12-17T11:27:18.500"
    }
  ],
  "more": false
}