分享

Swift源码分析----swift-account-replicator

tntzbzc 发表于 2014-11-20 15:34:47 [显示全部楼层] 回帖奖励 阅读模式 关闭右栏 0 12004
问题导读
1.哪个类实现复制指定分区数据到指定节点(用以实现数据副本之间的同步)?
2.复制指定分区数据到指定节点有哪些步骤?
3.法_repl_to_node的实现了什么?








概述部分:
实现复制指定分区(账户)数据到指定节点(用以实现数据副本之间的同步);
这里定义的once=True,说明系统默认调用守护进程类Daemon中的run_once方法;
从而最终实现调用Replicator类中的run_once方法;
注:账户之间同步数据主要就是对形如object_file = /srv/node/node['device']/accounts/partition/suffix/hsh****.db的数据库文件执行复制操作;



源码解析部分:

下面是这部分代码的主要执行流程,代码中较重要的部分已经进行了相关的注释;

  1. from swift.account.replicator import AccountReplicator  
  2. from swift.common.utils import parse_options  
  3. from swift.common.daemon import run_daemon  
  4.   
  5. if __name__ == '__main__':  
  6.     conf_file, options = parse_options(once=True)  
  7.     run_daemon(AccountReplicator, conf_file, **options)  
复制代码
  1. class AccountReplicator(db_replicator.Replicator):  
  2.     server_type = 'account'  
  3.     brokerclass = AccountBroker  
  4.     datadir = DATADIR  
  5.     default_port = 6002  
复制代码
  1. class Replicator(Daemon)----def run_once(self, *args, **kwargs):  
  2.      """
  3.      实现复制指定分区数据到指定节点(用以实现数据副本之间的同步);
  4.      数据类型可能是account或container或object;
  5.      """  
  6.          
  7.     # 初始化若干参数的操作;  
  8.     # self.stats = {'attempted': 0, 'success': 0, 'failure': 0, 'ts_repl': 0,  
  9.     #              'no_change': 0, 'hashmatch': 0, 'rsync': 0, 'diff': 0,  
  10.     #              'remove': 0, 'empty': 0, 'remote_merge': 0,  
  11.     #              'start': time.time(), 'diff_capped': 0}  
  12.     self._zero_stats()  
  13.     dirs = []  
  14.     ips = whataremyips()  
  15.     if not ips:  
  16.         self.logger.error(_('ERROR Failed to get my own IPs?'))  
  17.         return  
  18.          
  19.     # 获取环上的设备信息;  
  20.     for node in self.ring.devs:  
  21.         if (node and node['replication_ip'] in ips and node['replication_port'] == self.port):  
  22.             if self.mount_check and not ismount(os.path.join(self.root, node['device'])):  
  23.                 self.logger.warn(_('Skipping %(device)s as it is not mounted') % node)  
  24.                     continue  
  25.                   
  26.             # 删除若干过期文件;  
  27.             unlink_older_than(  
  28.                 os.path.join(self.root, node['device'], 'tmp'),  
  29.                 time.time() - self.reclaim_age)  
  30.                   
  31.             datadir = os.path.join(self.root, node['device'], self.datadir)  
  32.             if os.path.isdir(datadir):  
  33.                 dirs.append((datadir, node['id']))  
  34.          
  35.     self.logger.info(_('Beginning replication run'))  
  36.     for part, object_file, node_id in roundrobin_datadirs(dirs):  
  37.               
  38.          # _replicate_object:复制指定分区数据到指定节点(用以实现数据副本之间的同步),具体步骤如下;  
  39.          #     获取指定分区所在的所有节点nodes(一个分区可能对应多个节点,因为可能有多个副本);  
  40.          #     判断node_id是否在nodes的范围之内(这是合理的);  
  41.          #     循环实现数据到各个目标节点上(的分区)的复制操作;  
  42.          #     通过比较同步点和哈希值来判断复制后的两个版本是否是同步的,即复制操作是否成功;  
  43.         self.cpool.spawn_n(self._replicate_object, part, object_file, node_id)  
  44.     self.cpool.waitall()  
  45.     self.logger.info(_('Replication run OVER'))  
  46.     self._report_stats()  
复制代码



1.for node in self.ring.devs:从环上获取所有设备,遍历并执行以下操作:
通过IP地址判断并获取属于本机的且已经挂载的设备,并存储设备对应的datadir = /srv/node/node['device']/accounts和node['id']作为元素储存在字典dirs中;
注:这里实际上就是获取属于本机的设备,且明确文件路径/srv/node/node['device']/accounts(对应于账户);

2.循环遍历node['device']/accounts下面的每一个文件object_file(文件路径形如object_file = /srv/node/node['device']/accounts/partition/suffix/hsh****.db,为账户中具体分区下的以.db为后缀的文件),调用方法_replicate_object实现复制本地指定分区数据到指定节点(用以实现数据副本之间的同步);


转到2,来看方法_replicate_object的实现:

  1. def _replicate_object(self, partition, object_file, node_id):  
  2.     """
  3.     复制指定分区数据到指定节点(用以实现数据副本之间的同步),具体步骤如下;
  4.     获取指定分区所在的所有节点nodes(一个分区可能对应多个节点,因为可能有多个副本);
  5.     判断node_id是否在nodes的范围之内(这是合理的);
  6.     循环实现数据到各个目标节点上(的分区)的复制操作;
  7.     通过比较同步点和哈希值来判断复制后的两个版本是否是同步的,即复制操作是否成功;
  8.     object_file = /srv/node/node['device']/accounts/partition/suffix/hsh****.db
  9.     """  
  10.     start_time = now = time.time()  
  11.     self.logger.debug(_('Replicating db %s'), object_file)  
  12.     self.stats['attempted'] += 1  
  13.     self.logger.increment('attempts')  
  14.     shouldbehere = True  
  15.          
  16.     try:  
  17.         broker = self.brokerclass(object_file, pending_timeout=30)  
  18.         broker.reclaim(now - self.reclaim_age, now - (self.reclaim_age * 2))  
  19.         # 获取关于数据库复制需求的信息;  
  20.         info = broker.get_replication_info()  
  21.         full_info = broker.get_info()  
  22.         bpart = self.ring.get_part(full_info['account'], full_info.get('container'))  
  23.         if bpart != int(partition):  
  24.             partition = bpart  
  25.             # Important to set this false here since the later check only  
  26.             # checks if it's on the proper device, not partition.  
  27.             shouldbehere = False  
  28.             name = '/' + quote(full_info['account'])  
  29.             if 'container' in full_info:  
  30.                 name += '/' + quote(full_info['container'])  
  31.             self.logger.error(  
  32.                 'Found %s for %s when it should be on partition %s; will '  
  33.                 'replicate out and remove.' % (object_file, name, bpart))  
  34.     except (Exception, Timeout) as e:  
  35.         if 'no such table' in str(e):  
  36.             self.logger.error(_('Quarantining DB %s'), object_file)  
  37.             quarantine_db(broker.db_file, broker.db_type)  
  38.         else:  
  39.             self.logger.exception(_('ERROR reading db %s'), object_file)  
  40.         self.stats['failure'] += 1  
  41.         self.logger.increment('failures')  
  42.         return  
  43.   
  44.     # The db is considered deleted if the delete_timestamp value is greater  
  45.     # than the put_timestamp, and there are no objects.  
  46.     delete_timestamp = 0  
  47.     try:  
  48.         delete_timestamp = float(info['delete_timestamp'])  
  49.     except ValueError:  
  50.         pass  
  51.     put_timestamp = 0  
  52.     try:  
  53.         put_timestamp = float(info['put_timestamp'])  
  54.     except ValueError:  
  55.         pass  
  56.     if delete_timestamp < (now - self.reclaim_age) and delete_timestamp > put_timestamp and info['count'] in (None, '', 0, '0'):  
  57.         if self.report_up_to_date(full_info):  
  58.             self.delete_db(object_file)  
  59.         self.logger.timing_since('timing', start_time)  
  60.         return  
  61.     responses = []  
  62.          
  63.     # 获取指定分区所在的所有节点(一个分区可能对应多个节点,因为可能有多个副本);  
  64.     nodes = self.ring.get_part_nodes(int(partition))  
  65.     if shouldbehere:  
  66.         shouldbehere = bool([n for n in nodes if n['id'] == node_id])  
  67.     # See Footnote [1] for an explanation of the repl_nodes assignment.  
  68.     i = 0  
  69.     while i < len(nodes) and nodes[i]['id'] != node_id:  
  70.         i += 1  
  71.     repl_nodes = nodes[i + 1:] + nodes[:i]  
  72.     more_nodes = self.ring.get_more_nodes(int(partition))  
  73.          
  74.     # 实现数据到各个目标节点上(的分区)的复制操作;  
  75.     for node in repl_nodes:  
  76.         success = False  
  77.               
  78.         # _repl_to_node:复制数据库文件到指定node;  
  79.         #     建立到目标分区的连接;  
  80.         #     实现一个HTTP REPLICATE复制请求;  
  81.         #     获取请求操作的响应信息;  
  82.         #     通过比较同步点和哈希值来判断复制后的两个副本是否是同步的,即复制操作是否成功;  
  83.         #     如果复制成功则直接返回True;  
  84.         try:  
  85.             success = self._repl_to_node(node, broker, partition, info)  
  86.         except DriveNotMounted:  
  87.             repl_nodes.append(more_nodes.next())  
  88.             self.logger.error(_('ERROR Remote drive not mounted %s'), node)  
  89.         except (Exception, Timeout):  
  90.             self.logger.exception(_('ERROR syncing %(file)s with node %(node)s'),  
  91.                                    {'file': object_file, 'node': node})  
  92.         self.stats['success' if success else 'failure'] += 1  
  93.         self.logger.increment('successes' if success else 'failures')  
  94.         responses.append(success)  
  95.     if not shouldbehere and all(responses):  
  96.         # If the db shouldn't be on this node and has been successfully  
  97.         # synced to all of its peers, it can be removed.  
  98.         self.delete_db(object_file)  
  99.     self.logger.timing_since('timing', start_time)  
复制代码

2.1.获取指定文件object_file = /srv/node/node['device']/accounts/partition/suffix/hsh****.db所处在的分区;
2.2.获取上面的分区所在的所有节点nodes(一个分区可能对应多个节点,因为可能有多个副本);
2.3.循环遍历所有副本节点(除去本节点),调用方法_repl_to_node实现复制本地数据到副本节点上;
2.4.当针对所有副本节点的数据同步操作都完成之后,并且判断数据不需要再存储到本地,则执行操作实现删除本地的数据文件;




继续
转到2.3,来看方法_repl_to_node的实现:

  1. def _repl_to_node(self, node, broker, partition, info):  
  2.      """
  3.      复制数据库文件到指定node;
  4.           建立到目标分区的连接;
  5.           实现一个HTTP REPLICATE复制请求;
  6.           获取请求操作的响应信息;
  7.           通过比较同步点和哈希值来判断复制后的两个副本是否是同步的,即复制操作是否成功;
  8.           如果复制成功则直接返回True;
  9.      """  
  10.     with ConnectionTimeout(self.conn_timeout):  
  11.         http = self._http_connect(node, partition, broker.db_file)  
  12.     if not http:  
  13.         self.logger.error(_('ERROR Unable to connect to remote server: %s'), node)  
  14.         return False  
  15.          
  16.      # 实现一个HTTP REPLICATE复制请求;  
  17.     with Timeout(self.node_timeout):  
  18.         response = http.replicate(  
  19.             'sync', info['max_row'], info['hash'], info['id'],  
  20.             info['created_at'], info['put_timestamp'],  
  21.             info['delete_timestamp'], info['metadata'])  
  22.          
  23.      # 若干异常处理;  
  24.     if not response:  
  25.         return False  
  26.     elif response.status == HTTP_NOT_FOUND:  # completely missing, rsync  
  27.         self.stats['rsync'] += 1  
  28.         self.logger.increment('rsyncs')  
  29.         return self._rsync_db(broker, node, http, info['id'])     
  30.     elif response.status == HTTP_INSUFFICIENT_STORAGE:  
  31.         raise DriveNotMounted()  
  32.          
  33.      # 响应状态说明了复制操作完成;  
  34.      # 通过比较同步点和哈希值来判断复制后的两个副本是否是同步的,即复制操作是否成功;  
  35.      # 如果复制成功则直接返回True;  
  36.     elif response.status >= 200 and response.status < 300:  
  37.         rinfo = simplejson.loads(response.data)  
  38.         local_sync = broker.get_sync(rinfo['id'], incoming=False)  
  39.               
  40.            # 比较rinfo(远程复制数据信息)和info(本地复制数据信息)的同步点和哈希值,  
  41.            # 来判断完成复制操作的两个副本间是否是同步的;  
  42.         if self._in_sync(rinfo, info, broker, local_sync):  
  43.             return True  
  44.   
  45.         # if the difference in rowids between the two differs by  
  46.         # more than 50%, rsync then do a remote merge.  
  47.         if rinfo['max_row'] / float(info['max_row']) < 0.5:  
  48.             self.stats['remote_merge'] += 1  
  49.             self.logger.increment('remote_merges')  
  50.             return self._rsync_db(broker, node, http, info['id'],  
  51.                                   replicate_method='rsync_then_merge',  
  52.                                   replicate_timeout=(info['count'] / 2000))  
  53.   
  54.         # else send diffs over to the remote server  
  55.         return self._usync_db(max(rinfo['point'], local_sync), broker, http, rinfo['id'], info['id'])  
复制代码





2.3.1.调用方法replicate实现通过HTTP协议调用REPLICATE方法,实现本地指定文件到远程指定节点的同步操作,并获取相应信息;
      注:这里具体还有很多细节,就不进行进一步解析了;

2.3.2.通过上述的响应信息判断同步操作是否完成,如果完成,则进一步比较rinfo(远程复制数据信息)和info(本地复制数据信息)的同步点和哈希值,来判断完成复制操作的两个副本间是否是同步的,如果同步,则说明复制操作成功,直接返回True;

2.3.3.如果同步操作不成功,则通过rinfo['max_row']/float(info['max_row'])的比值判断,如果远程节点数据和本地复制数据差异超过50%,说明数据差异较大,则调用方法_rsync_db通过命令rsync实现全部数据的同步;

2.3.4.通过rinfo['max_row']/float(info['max_row'])的比值判断,如果远程节点数据和本地复制数据差异没有超过50%,说明数据差异较小,则调用方法_usync_db实现数据的同步;

转到2.3.3,来看方法_rsync_db的实现:
  1. def _rsync_db(self, broker, device, http, local_id, replicate_method='complete_rsync', replicate_timeout=None):
  2.      """
  3.      通过命令rsync实现节点间全部数据的同步;
  4.      """
  5.     device_ip = rsync_ip(device['replication_ip'])
  6.     if self.vm_test_mode:
  7.         remote_file = '%s::%s%s/%s/tmp/%s' % (device_ip, self.server_type, device['replication_port'], device['device'], local_id)
  8.     else:
  9.         remote_file = '%s::%s/%s/tmp/%s' % (device_ip, self.server_type, device['device'], local_id)
  10.     mtime = os.path.getmtime(broker.db_file)
  11.     if not self._rsync_file(broker.db_file, remote_file):
  12.         return False
  13.     # perform block-level sync if the db was modified during the first sync
  14.     if os.path.exists(broker.db_file + '-journal') or os.path.getmtime(broker.db_file) > mtime:
  15.         # grab a lock so nobody else can modify it
  16.         with broker.lock():
  17.             if not self._rsync_file(broker.db_file, remote_file, False):
  18.                 return False
  19.     with Timeout(replicate_timeout or self.node_timeout):
  20.         response = http.replicate(replicate_method, local_id)
  21.     return response and response.status >= 200 and response.status
复制代码




2.3.3.1 第一次调用方法_rsync_file,通过应用命令rsync,实现两个节点间数据的同步,这里设置whole_file=True,说明进行了全数据的复制操作;
2.3.3.2.如果文件路径下存在以-journal为后缀的文件,说明在第一次数据同步的过程中,数据文件有被修改;所以第二次调用方法_rsync_file,但是这里对文件处理操作加锁,以防止在数据同步的过程中,数据文件再被修改;这里设置参数whole_file=Flase,说明没有进行全数据的复制操作,而是进行了差异部分的数据复制操作;
2.3.3.3.在执行完成数据同步操作之后,调用方法replicate实现通过HTTP推送REPLICATE方法,进而调用设定的rsync_then_merge方法,实现部分+部分地从container数据表中整合相关属性信息到container数据表中(这里有些细节需要进一步理解);
来看方法_rsync_file的实现:
  1. def _rsync_file(self, db_file, remote_file, whole_file=True):
  2.     """
  3.     通过应用命令rsync,实现两个节点间数据的同步;
  4.     """
  5.     popen_args = ['rsync', '--quiet', '--no-motd',
  6.                   '--timeout=%s' % int(math.ceil(self.node_timeout)),
  7.                   '--contimeout=%s' % int(math.ceil(self.conn_timeout))]
  8.     if whole_file:
  9.         popen_args.append('--whole-file')
  10.     popen_args.extend([db_file, remote_file])
  11.     proc = subprocess.Popen(popen_args)
  12.     proc.communicate()
  13.     if proc.returncode != 0:
  14.         self.logger.error(_('ERROR rsync failed with %(code)s: %(args)s'),
  15.                           {'code': proc.returncode, 'args': popen_args})
  16.     return proc.returncode == 0
复制代码




注:(1)可见当命令行组成之后,将会调用方法communicate实现命令行的远程执行;
    (2)命令rsync常用于节点间的数据同步和备份操作;rsync命令有特性:第一次同步时rsync会复制全部内容,但在下一次只传输修改过的文件,这样的效率就比较高的;
    (3)whole_file参数决定了是否进行全数据的复制操作;
再来看方法rsync_then_merge的实现:
  1. def rsync_then_merge(self, drive, db_file, args):
  2.     old_filename = os.path.join(self.root, drive, 'tmp', args[0])
  3.         
  4.     if not os.path.exists(db_file) or not os.path.exists(old_filename):
  5.         return HTTPNotFound()
  6.         
  7.     new_broker = self.broker_class(old_filename)
  8.     existing_broker = self.broker_class(db_file)
  9.         
  10.     point = -1
  11.     objects = existing_broker.get_items_since(point, 1000)
  12.     while len(objects):
  13.         new_broker.merge_items(objects)
  14.         point = objects[-1]['ROWID']
  15.         objects = existing_broker.get_items_since(point, 1000)
  16.         sleep()
  17.     new_broker.newid(args[0])
  18.     renamer(old_filename, db_file)
  19.     return HTTPNoContent()
复制代码
  1. def get_items_since(self, start, count):
  2.     self._commit_puts_stale_ok()
  3.     with self.get() as conn:
  4.         curs = conn.execute('''
  5.             SELECT * FROM %s WHERE ROWID > ? ORDER BY ROWID ASC LIMIT ?
  6.         ''' % self.db_contains_type, (start, count))
  7.         curs.row_factory = dict_factory
  8.         return [r for r in curs]
复制代码
  1. def merge_items(self, item_list, source=None):  
  2. """
  3. 整合指定账户下container数据表中的相关属性信息到目标container数据表中;
  4. """  
  5.     with self.get() as conn:  
  6.         max_rowid = -1  
  7.         for rec in item_list:  
  8.             record = [rec['name'], rec['put_timestamp'],  
  9.                       rec['delete_timestamp'], rec['object_count'],  
  10.                       rec['bytes_used'], rec['deleted']]  
  11.             query = '''''
  12.                 SELECT name, put_timestamp, delete_timestamp,object_count, bytes_used, deleted
  13.                 FROM container WHERE name = ?'''  
  14.   
  15.             if self.get_db_version(conn) >= 1:  
  16.                 query += ' AND deleted IN (0, 1)'  
  17.             curs = conn.execute(query, (rec['name'],))  
  18.             curs.row_factory = None  
  19.             row = curs.fetchone()  
  20.             if row:  
  21.                 row = list(row)  
  22.                 for i in xrange(5):  
  23.                     if record[i] is None and row[i] is not None:  
  24.                         record[i] = row[i]  
  25.                 if row[1] > record[1]:  # Keep newest put_timestamp  
  26.                     record[1] = row[1]  
  27.                 if row[2] > record[2]:  # Keep newest delete_timestamp  
  28.                     record[2] = row[2]  
  29.                 # If deleted, mark as such  
  30.                 if record[2] > record[1] and record[3] in (None, '', 0, '0'):  
  31.                     record[5] = 1  
  32.                 else:  
  33.                     record[5] = 0  
  34.             conn.execute('''''DELETE FROM container WHERE name = ? AND deleted IN (0, 1)''', (record[0],))  
  35.             conn.execute('''''INSERT INTO container (name, put_timestamp, delete_timestamp, object_count, bytes_used, deleted) VALUES (?, ?, ?, ?, ?, ?)''', record)  
  36.             if source:  
  37.                 max_rowid = max(max_rowid, rec['ROWID'])  
  38.   
  39.         if source:  
  40.             try:  
  41.                 conn.execute('''''INSERT INTO incoming_sync (sync_point, remote_id) VALUES (?, ?)''', (max_rowid, source))  
  42.             except sqlite3.IntegrityError:  
  43.                 conn.execute('''''UPDATE incoming_sync SET sync_point=max(?, sync_point) WHERE remote_id=?''', (max_rowid, source))  
  44.         conn.commit()  
复制代码




转到2.3.4,来看方法_usync_db的实现:

  1. def _usync_db(self, point, broker, http, remote_id, local_id):  
  2.     """
  3.     由于节点间数据差异不超过50%,所有通过发送自从上一次同步操作以来的所有数据变化记录来实现节点间的数据同步操作;
  4.     """  
  5.     self.stats['diff'] += 1  
  6.     self.logger.increment('diffs')  
  7.     self.logger.debug(_('Syncing chunks with %s'), http.host)  
  8.     sync_table = broker.get_syncs()  
  9.     objects = broker.get_items_since(point, self.per_diff)  
  10.     diffs = 0  
  11.     while len(objects) and diffs < self.max_diffs:  
  12.         diffs += 1  
  13.         with Timeout(self.node_timeout):  
  14.             response = http.replicate('merge_items', objects, local_id)  
  15.         if not response or response.status >= 300 or response.status < 200:  
  16.             if response:  
  17.                 self.logger.error(_('ERROR Bad response %(status)s from %(host)s'),  
  18.                                    {'status': response.status, 'host': http.host})  
  19.             return False  
  20.   
  21.         point = objects[-1]['ROWID']  
  22.         objects = broker.get_items_since(point, self.per_diff)  
  23.   
  24.     if objects:  
  25.         self.logger.debug(_('Synchronization for %s has fallen more than '  
  26.             '%s rows behind; moving on and will try again next pass.'),  
  27.             broker, self.max_diffs * self.per_diff)  
  28.         self.stats['diff_capped'] += 1  
  29.         self.logger.increment('diff_caps')  
  30.     else:  
  31.         with Timeout(self.node_timeout):  
  32.             response = http.replicate('merge_syncs', sync_table)  
  33.         if response and response.status >= 200 and response.status < 300:  
  34.             broker.merge_syncs([{'remote_id': remote_id, 'sync_point': point}],  
  35.                                incoming=False)  
  36.             return True  
  37.     return False  
复制代码










博客地址:http://blog.csdn.net/gaoxingnengjisuan
邮箱地址:dong.liu@siat.ac.cn





没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条