bigchaindb源碼分析(一)分析了bigchaindb如何解析命令列參數與設定檔,並據此啟動了日誌publisher與subscriber。對於bigchaindb start命令,將調用_run_init來初始化後端儲存,隨之利用pipeline來啟動block\vote等進程(bigchaindb源碼分析(二))。本節介紹bigchaindb的後端儲存。
_run_init由run_start所調用,代碼位於commands/bigchaindb.py
def _run_init(): # Try to access the keypair, throws an exception if it does not exist b = bigchaindb.Bigchain() schema.init_database(connection=b.connection) b.create_genesis_block() logger.info('Genesis block created.')
該函數首先建立一個Bigchain執行個體,Bigchain類位於core.py,其__init__函數將從bigchaindb.config中載入設定檔的配置,包括本節點的公私密金鑰、聯盟中其他節點的公開金鑰、一致性演算法外掛程式、以及定位串連資料庫的類等等。之後根據串連資料庫的類來初始化資料庫。最後建立創世區塊。 建立bigchain執行個體
Bigchain類__init__函數串連資料庫代碼如下所示。當類執行個體化的connect參數為None時,將調用backend.connect。
self.connection = connection if connection else backend.connect(**bigchaindb.config['database'])
其形參前的**相當於將形參中字典的key value一一對應到函數參數列表中。舉例來說:
>>> def test(a, b, c):... print(a, b, c)... >>> test(1,2,3)1 2 3>>> x=(1,2,3)>>> test(*x)1 2 3>>> y={'a':1,'b':2,'c':3}>>> test(**y)1 2 3
backend.connect參數列表如下。故而在調用時,將設定檔中database下的值一一對應到參數列表中。
# 參數列表def connect(backend=None, host=None, port=None, name=None, max_tries=None, connection_timeout=None, replicaset=None, ssl=None, login=None, password=None, ca_cert=None, certfile=None, keyfile=None, keyfile_passphrase=None, crlfile=None):# 設定檔中的database項"database": { "password": null, "connection_timeout": 5000, "ssl": false, "replicaset": "bigchain-rs", "login": null, "max_tries": 3, "name": "bigchain", "port": 27017, "host": "localhost", "backend": "mongodb"},
bigchaindb後端儲存的代碼位於backend目錄下,目前包含了mongodb\rethinkdb兩種後端儲存資料庫。backend/connect.py中也指定了目前支援的兩種後端儲存。
BACKENDS = { 'mongodb': 'bigchaindb.backend.mongodb.connection.MongoDBConnection', 'rethinkdb': 'bigchaindb.backend.rethinkdb.connection.RethinkDBConnection'}
backend.connect尋找後端儲存類的代碼也就可以分為以下幾步了:首先是擷取設定檔中backend的值,之後從變數BACKENDS中找到對應的類,再通過反射機制根據類的字串路徑載入類
# backend為mongodbbackend = backend or bigchaindb.config['database']['backend']module_name, _, class_name = BACKENDS[backend].rpartition('.')Class = getattr(import_module(module_name), class_name)return Class(host=host, port=port, dbname=dbname, max_tries=max_tries, connection_timeout=connection_timeout, replicaset=replicaset, ssl=ssl, login=login, password=password, ca_cert=ca_cert, certfile=certfile, keyfile=keyfile, keyfile_passphrase=keyfile_passphrase, crlfile=crlfile)
rpartition將返回一個三元組,分別為最後一個分隔字元左邊的字串、分隔字元本身、以及最後一個分隔字元右邊的字串。因此,class_name將對應於MongoDBConnection。而後利用getattr函數載入該類,變數Class也就對應於MongoDBConnection類的一個執行個體了,最後則調用Class類。
>>> x='bigchaindb.backend.mongodb.connection.MongoDBConnection'>>> print(x.rpartition('.'))('bigchaindb.backend.mongodb.connection', '.', 'MongoDBConnection')
我們再來看bigchaindb.backend.mongodb.connection模組的MongoDBConnection類。該類繼承backend.connection.Connection類。實際上,所有後端儲存的串連類都應該繼承Connection類,然後再實現自己所獨立的操作資料庫的代碼。MongoDBConnection類的__init__函數將調用父類的初始化函數,之後將一些mongodb所專屬的配置儲存為類的成員,如replicaset。而父類的__init__則將一些後端儲存所共有的配置儲存為類的成員,如host\port\dbname等等。
class MongoDBConnection(Connection): def __init__(self, replicaset=None, ssl=None, login=None, password=None, ca_cert=None, certfile=None, keyfile=None, keyfile_passphrase=None, crlfile=None, **kwargs): super().__init__(**kwargs) self.replicaset = replicaset or bigchaindb.config['database']['replicaset']
初始化資料庫
_run_init函數在建立Bigchain執行個體後,將對資料庫進行初始化。根據之前的分析,形參中的connection實際上是MongoDBConnection類的執行個體了。
schema.init_database(connection=b.connection)# backend/schema.pydef init_database(connection=None, dbname=None): connection = connection or connect() dbname = dbname or bigchaindb.config['database']['name'] create_database(connection, dbname) create_tables(connection, dbname) create_indexes(connection, dbname)
從函數名稱上可以看出,init_database的步驟為依次建立資料庫、表以及索引。然而,在backend/schema.py中這三個函數實際上居然都沒有函數體,不過有個裝飾器singledispatch
@singledispatchdef create_database(connection, dbname): raise NotImplementedError@singledispatchdef create_tables(connection, dbname): raise NotImplementedError@singledispatchdef create_indexes(connection, dbname): raise NotImplementedError
裝飾器singledispatch的文檔在https://pypi.python.org/pypi/singledispatch。閱讀這些例子,可以看出相當於實現了函數多態,即可以通過不同的形參來調用同一個函數名的不同函數。文檔中的執行個體如下: 定義:通過裝飾器singledispatch來定義需要多態的函數,通過@fun.register(int)或者fun.register來進行註冊
>>> from singledispatch import singledispatch>>> @singledispatch... def fun(arg, verbose=False):... if verbose:... print("Let me just say,", end=" ")... print(arg)>>> @fun.register(int)... def _(arg, verbose=False):... if verbose:... print("Strength in numbers, eh?", end=" ")... print(arg)>>> def nothing(arg, verbose=False):... print("Nothing.")...>>> fun.register(type(None), nothing) 調用
>>> fun("Hello, world.")Hello, world.>>> fun("test.", verbose=True)Let me just say, test.>>> fun(42, verbose=True)Strength in numbers, eh? 42>>> fun(None)Nothing.
由此,我們來尋找後端儲存中對create_database進行了註冊的函數。mongodb後端儲存backend/mongodb/schema中有以下代碼:
from bigchaindb.backend.utils import module_dispatch_registrarregister_schema = module_dispatch_registrar(backend.schema)@register_schema(MongoDBConnection)def create_database(conn, dbname): if dbname in conn.conn.database_names(): raise exceptions.DatabaseAlreadyExists('Database `{}` already exists' .format(dbname)) logger.info('Create database `%s`.', dbname) # TODO: read and write concerns can be declared here conn.conn.get_database(dbname)
module_dispatch_registrar的實現代碼為:
def module_dispatch_registrar(module): def dispatch_wrapper(obj_type): def wrapper(func): func_name = func.__name__ try: dispatch_registrar = getattr(module, func_name) return dispatch_registrar.register(obj_type)(func) except AttributeError as ex: raise ModuleDispatchRegistrationError( ('`{module}` does not contain a single-dispatchable ' 'function named `{func}`. The module being registered ' 'was not implemented correctly!').format( func=func_name, module=module.__name__)) from ex return wrapper return dispatch_wrapper
看著好複雜的樣子。。按照之前裝飾器的思路,我們先來進行替換吧,首先展開module_dispatch_registrar,代碼相當於
@module_dispatch_registrar(backend.schema)(MongoDBConnection)def create_database(conn, dbname): ...
也即
@dispatch_wrapper(MongoDBConnection)def create_database(conn, dbname): ...# 其中變數module為backend.schemadef dispatch_wrapper(obj_type): def wrapper(func): ... return wrapper
再把dispatch_wrapper展開
wrapper(create_database)# 其中變數module為backend.schema,變數obj_type為MongoDBConnectiondef wrapper(func): ...return wrapper
所以這段代碼實際上是調用了(注意,這裡將所有變數寫成了字串,實際上均指得是對應的對象)
dispatch_registrar = getattr(backend.schema, create_database)dispatch_registrar.register(MongoDBConnection)(create_database)
因此,當init_database執行代碼create_database(connection, dbname)時,由於connection的類型為MongoDBConnection類,故而將調用的函數為backend/mongodb/schema.py中的create_database函數。create_tables以及create_indexes也是如此。 建立資料庫
如此,建立資料庫所調用的函數為backend.mongodb.schema.create_database,形參conn對應MongoDBConnection類的執行個體
@register_schema(MongoDBConnection)def create_database(conn, dbname): if dbname in conn.conn.database_names(): raise exceptions.DatabaseAlreadyExists('Database `{}` already exists' .format(dbname)) logger.info('Create database `%s`.', dbname) # TODO: read and write concerns can be declared here conn.conn.get_database(dbname)
conn.conn實際上是MongoDBConnection的父類Connection類的一個屬性,由於在該類__init__時,self._conn值為None,所以將調用self.connect(),而connect又將調用_connect函數self.max_tries_counter次,直到串連成功,或者拋出異常
@propertydef conn(self): if self._conn is None: self.connect() return self._conndef connect(self): attempt = 0 for i in self.max_tries_counter: attempt += 1 try: self._conn = self._connect() except ConnectionError as exc: ... else: break
_connect函數在去掉異常處理、函數調用時的參數列表後如下所示。函數邏輯也很清晰了,首先是初始化複本集,然後利用pymongo串連到mongodb資料庫,再用設定檔中的帳號與口令驗證,最後返回串連並驗證成功的client對象。注意最後返回的client對象將賦值給MongoDBConnection類的conn成員屬性。
def _connect(self): try: initialize_replica_set(self.host,...) if self.ca_cert is None or self.certfile is None or \ self.keyfile is None or self.crlfile is None: client = pymongo.MongoClient(self.host,...) if self.login is not None and self.password is not None: client[self.dbname].authenticate(self.login, self.password) else: logger.info('Connecting to MongoDB over TLS/SSL...') client = pymongo.MongoClient(self.host,...) if self.login is not None: client[self.dbname].authenticate(self.login, mechanism='MONGODB-X509') return client except ...
initialize_replica_set函數也是首先串連mongodb資料庫,然後檢查資料庫是否已經配置了複本集,並與設定檔中的複本集的名字進行匹配,若匹配成功,則說明已經初始化了複本集,否則則初始化。。 建立表與建立索引
在建立資料庫成功後的下一步是建立表,這一步的代碼就比較簡單了:直接調用pymongo的create_collection函數,在資料庫下建立四個表:bigchain、backlog、votes、assets
@register_schema(MongoDBConnection)def create_tables(conn, dbname): for table_name in ['bigchain', 'backlog', 'votes', 'assets']: logger.info('Create `%s` table.', table_name) # create the table # TODO: read and write concerns can be declared here conn.conn[dbname].create_collection(table_name)
在成功建立表之後,bigchaindb為這四個表分別建立二級索引,以方便進行查詢,如建立名為transaction_id的索引,用來支援對事務id進行查詢
@register_schema(MongoDBConnection)def create_indexes(conn, dbname): create_bigchain_secondary_index(conn, dbname) create_backlog_secondary_index(conn, dbname) create_votes_secondary_index(conn, dbname) create_assets_secondary_index(conn, dbname)def create_bigchain_secondary_index(conn, dbname): logger.info('Create `bigchain` secondary index.') # to order blocks by timestamp conn.conn[dbname]['bigchain'].create_index([('block.timestamp', ASCENDING)], name='block_timestamp') # to query the bigchain for a transaction id, this field is unique conn.conn[dbname]['bigchain'].create_index('block.transactions.id', name='transaction_id') ...
至此,資料庫初始化完成,_run_init中唯一還沒有閱讀的是如何建立創世區塊,這一部分下次再學習。。