前两篇文章借由/fabric/peer/main.go这个线头,简单分析了fabric的配置和日志系统。虽然还有一部分可说的内容,如common.InitCrypto()
调用,但现在暂且按下main.go不表,而把目光投到/fabric/peer/node/start.go中的serve()函数。具体原因参见peer命令结构一文的最后。
ledger,中文就是账本,账册的意思。先来讨论Fabric账册的原因来自于peer node start所执行的函数serve第一句代码即为ledgermgmt.Initialize()
,字面上看属于对账册进行初始化操作,换句话说,对账本相关环境的初始化,是其他peer命令执行的基础之一。
- common/ledger
- core/ledger
也就是说Fabric把关于ledger的源码把其他模块用得到的,有共享属性的部分放到了common目录下,核心的代码放到了core目录下。
Fabric中的ledger说白了就是一系列数据库存储操作。对应所选用的数据库,主要有两种:goleveldb和couchDB。在core.yaml配置文件中ledger区域中,stateDatabase选项即为指定所选用的数据库,默认选用goleveldb。本文章着意于ledger的操作,因此下文也只以goleveldb为例进行介绍。
>
goleveldb:主要使用了第三方库leveldb。下载地址github.com/syndtr/goleveldb/leveldb。leveldb是一个典型的key-value数据库,也是谷歌自己做做出来的,有多种语言版本,在此自然是使用goleveldb。把Fabric的ledger也称为kvledger的原因,这很好的体现了账本的特性,数据的操作都是基于键-值。关于goleveldb数据库定义,操作的代码,集中在
common/ledger/util/leveldbhelper
目录下。
>
couchDB:在Fabric用go语言手工实现的一个数据库,源码集中在
core/ledger/util/couchdb
下。couchDB在Fabric目前只用于版本数据库所使用的两个方案中的一个。
db, err:=leveldb.OpenFile("./db", nil)
。作用就是在当前目录下创建一个db文件夹作为数据库的目录。db.Put([]byte("key1"),[]byte("value1"),nil)
。作用就是在数据库在数据库中存储 键值对 key1-value1。leveldb数据库中对键值的操作都是byte格式化的数据。data,_ := db.Get([]byte("key1"),nil)
。获取key1对应的值。iter := db.NewIterator(nil, nil)
,for iter.Next(){ fmt.Printf("key=%s,value=%s\n",iter.Key(),iter.Value()) }
,iter.Release()
。作用就是建立迭代器iter,然后依次遍历数据库中所有的数据并打印键和值,最后释放迭代器iter。db.Close()
。Fabric到处都是接口,各个层级的代码编写风格和习惯很一致,甚至使用的函数名,对象名都有大量雷同的,因而在有些源码处十分的绕,其实阅读源代码我们应该遵循以下2条原则:
因此,追溯并查看ledgermgmt.Initialize()函数,在core/ledger/ledgermgmt/ledger_mgmt.go中,其直接once.Do了initialize()函数。在initialize()函数中,除了日志操作和锁保护(一般在初始化的函数中,都会进行锁保护,各位可自行参看其他源码)外,所做的只是初始化了三个对象:openedLedgers,ledgerProvider和initialized,该三个对象均为文件中的全局变量:
initialized = true
openedLedgers = make(map[string]ledger.PeerLedger)
provider, err := kvledger.NewProvider()
ledgerProvider = provider
其中initialized和openedLedgers分别赋了初值和分配了内存,openedLedgers应该是存放peer的账本映射的,initialized则是是否初始化的标识,两者并未有进一步操作,可暂时搁置一旁。而ledgerProvider则被赋于由kvledger.NewProvider()函数返回的值,从其名字我们就可以知道该对象是一个账本服务提供者,因此我们的目光停留在此即可。其原型为ledger.PeerLedgerProvider接口,在core/ledger/ledger_interface.go中定义:
type PeerLedgerProvider interface {
Create(genesisBlock *common.Block) (PeerLedger, error)
Open(ledgerID string) (PeerLedger, error)
Exists(ledgerID string) (bool, error)
List() ([]string, error)
Close()
}
根据Fabric的惯例,有Provider字样的对象,或大或小,都是某一主题模块服务的提供者,提供该主题模块的一系列操作服务。而接口类型的Provider对象,则在具体实现上则会分为多种具体的Provider以供使用(同时也留下了扩展空间)。ledger的Provider亦然如此,kvledger.NewProvider()函数返回的就是PeerLedgerProvider接口的一个具体实现,kv ledger provider,即键值账本提供者,在core/ledger/kvledger/kv_ledger_provider.go中定义:
type Provider struct {
idStore *idStore //ledgerID数据库
blockStoreProvider blkstorage.BlockStoreProvider //block数据库存储服务对象
vdbProvider statedb.VersionedDBProvider //状态数据库存储服务对象
historydbProvider historydb.HistoryDBProvider //历史数据库存储服务对象
}
还是根据Fabric的惯例,在每个定义对象结构的文件里,通常都会有一个专门用于生成该对象的函数,kvledger.NewProvider()既是用于生成键值账本服务提供者的函数。通过追溯,我们会发现,这个对象中的四个成员对象其实就是四个数据库,分别用于存储不同的数据,也是账册存储所需要的。而kvledger.NewProvider()所做的,就是分别按照配置生成这四个数据库对象,这也符合和上文我们所说的两个原则。四个数据库除了idStore和blockStoreProvider有自己特殊的配置外,都共同使用的leveldb数据库存储服务提供者,后文将以块数据库服务对象为例,详细介绍。
以block数据库存储服务对象blockStoreProvider的结构为例,其代码集中在commom/ledger/blkstorage下(本节中除介绍leveldb数据库存储服务对象外,所示路径皆以此路径为基准)。blockStoreProvider的原型BlockStoreProvider依然是个接口,在blockstorage.go中定义此接口,具体实现为用文件系统存储,即fsblkstorage/fs_blockstore_provider.go中定义的FsBlockstoreProvider,对象结构如下:
也就是说,与块数据存储服务对象blockStoreProvider最终对接的是三个成员,其中两个配置项成员conf和indexConfig,是相较于其他数据库服务对象所独有的,一个leveldb数据库存储服务提供者leveldbProvider,则和其他数据库服务对象一样。而专门用于初始化FsBlockstoreProvider的函数即为fsblkstorage.NewProvider()。
在kvledger.NewProvider()中,以下三句代码用于初始化blockStoreProvider对象:
attrsToIndex := []blkstorage.IndexableAttr{
blkstorage.IndexableAttrBlockHash,
blkstorage.IndexableAttrBlockNum,
blkstorage.IndexableAttrTxID,
blkstorage.IndexableAttrBlockNumTranNum,
blkstorage.IndexableAttrBlockTxID,
blkstorage.IndexableAttrTxValidationCode,
}
indexConfig := &blkstorage.IndexConfig{AttrsToIndex: attrsToIndex}
//传递两个配置项,并在内部使用专用函数生成一个leveldb数据库对象,最终生成块数据存储服务对象
blockStoreProvider := fsblkstorage.NewProvider(
fsblkstorage.NewConf(ledgerconfig.GetBlockStorePath(),
ledgerconfig.GetMaxBlockfileSize()),
indexConfig)
用于存储块索引字段值,这点blockstorage.go中进行了硬编码。可以将其想象成数据库表中准备为哪些字段建立索引,因而在此记录一下。
该配置对象在fsblkstorage/config中定义,两个字段blockStorageDir和maxBlockfileSize指定了块数据库存储服务对象所使用的路径和存储文件的大小。在fsblkstorage.NewProvider()中,传入的config是用NewConf(ledgerconfig.GetBlockStorePath(),ledgerconfig.GetMaxBlockfileSize())进行创建的,而NewConf又使用了ledgerconfig下的函数分别获取了路径和大小。而通过追溯ledgerconfig,发现其最终形成的路径值为/var/hyperledger/production/ledgersData/chains,大小为64M(关于路径和配置,将在以配置和路径为主题的文章中详细介绍)。
实际的,最终操作数据库数据的对象,账本所使用的四个数据库服务对象均使用此数据库对象对数据进行操作。在common/ledger/util/leveldbhelper/leveldb_provider.go中定义:
type Provider struct {
db *DB
dbHandles map[string]*DBHandle
mux sync.Mutex
}
leveldb数据库存储服务对象包含了封装leveldb数据库对象的db,和一个数据库映射dbHandles,和一把保护锁mux。因为数据库是对数据的读写操作,因此有一把保护锁很正常也很应该。其处于同一个文件中专用的初始化函数leveldbhelper.NewProvider(),在fsblkstorage.NewProvider()函数中即有体现:p := leveldbhelper.NewProvider(&leveldbhelper.Conf{DBPath: conf.getIndexDir()})
在此仍需关注的是DB结构中自带一个leveldbhelper.Conf配置选项,定义了leveldb数据库所在的目录。在leveldbhelper.NewProvider()初始化的过程中,使用了上层对象-块数据库存储服务对象中的conf所挂载的函数getIndexDir(),在common/ledger/blkstorage/fsblkstorage/config.go中定义,获取了一个路径,最终被初始化在chains/index下。
回到kvledger.NewProvider()函数中,其他几个数据库的初始化过程和块数据库存储服务对象类似,但更简单一些,基本都只是用专用函数初始化了一个leveldb数据库存储服务对象。至此,整个账本服务对象初始化完毕。以下列出账本服务对象整个对象的结构和所形成的目录结构,具象化一下。
对象结构:
目录结构:
>
- /var/hyperledger/production(core.yaml定义的flieSystemPath的值)
- ledgersData //账本目录
- ledgerProvider //ledgerID数据库目录
- chains //block块存储数据库目录
- index //block索引数据库目录
- chains
- 账本ID1
- 账本ID2
- …
- stateLeveldb //状态数据库目录
- historyLeveldb //历史数据库目录
在kvledger.NewProvider()函数中接近结尾的地方,有一句代码需要注意:provider.recoverUnderConstructionLedger()
,该句调用了账本服务对象的一个函数,主要是用于恢复处理一些之前账本初始化失败的操作,从中牵扯出了一大堆函数调用。但对这些操作的理解需要建立在理解账本操作的基础之上,因此在此埋下伏笔,之后的文章中将回过头来详细介绍此句。