qemu

FORK: QEMU emulator
git clone https://git.neptards.moe/neptards/qemu.git
Log | Files | Refs | Submodules | LICENSE

151 (17475B)


      1 #!/usr/bin/env python3
      2 # group: rw
      3 #
      4 # Tests for active mirroring
      5 #
      6 # Copyright (C) 2018 Red Hat, Inc.
      7 #
      8 # This program is free software; you can redistribute it and/or modify
      9 # it under the terms of the GNU General Public License as published by
     10 # the Free Software Foundation; either version 2 of the License, or
     11 # (at your option) any later version.
     12 #
     13 # This program is distributed in the hope that it will be useful,
     14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16 # GNU General Public License for more details.
     17 #
     18 # You should have received a copy of the GNU General Public License
     19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
     20 #
     21 
     22 import math
     23 import os
     24 import subprocess
     25 import time
     26 from typing import List, Optional
     27 import iotests
     28 from iotests import qemu_img
     29 
     30 source_img = os.path.join(iotests.test_dir, 'source.' + iotests.imgfmt)
     31 target_img = os.path.join(iotests.test_dir, 'target.' + iotests.imgfmt)
     32 
     33 class TestActiveMirror(iotests.QMPTestCase):
     34     image_len = 128 * 1024 * 1024 # MB
     35     potential_writes_in_flight = True
     36 
     37     def setUp(self):
     38         qemu_img('create', '-f', iotests.imgfmt, source_img, '128M')
     39         qemu_img('create', '-f', iotests.imgfmt, target_img, '128M')
     40 
     41         blk_source = {'id': 'source',
     42                       'if': 'none',
     43                       'node-name': 'source-node',
     44                       'driver': iotests.imgfmt,
     45                       'file': {'driver': 'blkdebug',
     46                                'image': {'driver': 'file',
     47                                          'filename': source_img}}}
     48 
     49         blk_target = {'node-name': 'target-node',
     50                       'driver': iotests.imgfmt,
     51                       'file': {'driver': 'file',
     52                                'filename': target_img}}
     53 
     54         self.vm = iotests.VM()
     55         self.vm.add_drive_raw(self.vm.qmp_to_opts(blk_source))
     56         self.vm.add_blockdev(self.vm.qmp_to_opts(blk_target))
     57         self.vm.add_device('virtio-blk,id=vblk,drive=source')
     58         self.vm.launch()
     59 
     60     def tearDown(self):
     61         self.vm.shutdown()
     62 
     63         if not self.potential_writes_in_flight:
     64             self.assertTrue(iotests.compare_images(source_img, target_img),
     65                             'mirror target does not match source')
     66 
     67         os.remove(source_img)
     68         os.remove(target_img)
     69 
     70     def doActiveIO(self, sync_source_and_target):
     71         # Fill the source image
     72         self.vm.hmp_qemu_io('source',
     73                             'write -P 1 0 %i' % self.image_len);
     74 
     75         # Start some background requests
     76         for offset in range(1 * self.image_len // 8, 3 * self.image_len // 8, 1024 * 1024):
     77             self.vm.hmp_qemu_io('source', 'aio_write -P 2 %i 1M' % offset)
     78         for offset in range(2 * self.image_len // 8, 3 * self.image_len // 8, 1024 * 1024):
     79             self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
     80 
     81         # Start the block job
     82         result = self.vm.qmp('blockdev-mirror',
     83                              job_id='mirror',
     84                              filter_node_name='mirror-node',
     85                              device='source-node',
     86                              target='target-node',
     87                              sync='full',
     88                              copy_mode='write-blocking')
     89         self.assert_qmp(result, 'return', {})
     90 
     91         # Start some more requests
     92         for offset in range(3 * self.image_len // 8, 5 * self.image_len // 8, 1024 * 1024):
     93             self.vm.hmp_qemu_io('source', 'aio_write -P 3 %i 1M' % offset)
     94         for offset in range(4 * self.image_len // 8, 5 * self.image_len // 8, 1024 * 1024):
     95             self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
     96 
     97         # Wait for the READY event
     98         self.wait_ready(drive='mirror')
     99 
    100         # Now start some final requests; all of these (which land on
    101         # the source) should be settled using the active mechanism.
    102         # The mirror code itself asserts that the source BDS's dirty
    103         # bitmap will stay clean between READY and COMPLETED.
    104         for offset in range(5 * self.image_len // 8, 7 * self.image_len // 8, 1024 * 1024):
    105             self.vm.hmp_qemu_io('source', 'aio_write -P 3 %i 1M' % offset)
    106         for offset in range(6 * self.image_len // 8, 7 * self.image_len // 8, 1024 * 1024):
    107             self.vm.hmp_qemu_io('source', 'aio_write -z %i 1M' % offset)
    108 
    109         if sync_source_and_target:
    110             # If source and target should be in sync after the mirror,
    111             # we have to flush before completion
    112             self.vm.hmp_qemu_io('source', 'aio_flush')
    113             self.potential_writes_in_flight = False
    114 
    115         self.complete_and_wait(drive='mirror', wait_ready=False)
    116 
    117     def testActiveIO(self):
    118         self.doActiveIO(False)
    119 
    120     def testActiveIOFlushed(self):
    121         self.doActiveIO(True)
    122 
    123     def testUnalignedActiveIO(self):
    124         # Fill the source image
    125         result = self.vm.hmp_qemu_io('source', 'write -P 1 0 2M')
    126 
    127         # Start the block job (very slowly)
    128         result = self.vm.qmp('blockdev-mirror',
    129                              job_id='mirror',
    130                              filter_node_name='mirror-node',
    131                              device='source-node',
    132                              target='target-node',
    133                              sync='full',
    134                              copy_mode='write-blocking',
    135                              buf_size=(1048576 // 4),
    136                              speed=1)
    137         self.assert_qmp(result, 'return', {})
    138 
    139         # Start an unaligned request to a dirty area
    140         result = self.vm.hmp_qemu_io('source', 'write -P 2 %i 1' % (1048576 + 42))
    141 
    142         # Let the job finish
    143         result = self.vm.qmp('block-job-set-speed', device='mirror', speed=0)
    144         self.assert_qmp(result, 'return', {})
    145         self.complete_and_wait(drive='mirror')
    146 
    147         self.potential_writes_in_flight = False
    148 
    149     def testIntersectingActiveIO(self):
    150         # Fill the source image
    151         result = self.vm.hmp_qemu_io('source', 'write -P 1 0 2M')
    152 
    153         # Start the block job (very slowly)
    154         result = self.vm.qmp('blockdev-mirror',
    155                              job_id='mirror',
    156                              filter_node_name='mirror-node',
    157                              device='source-node',
    158                              target='target-node',
    159                              sync='full',
    160                              copy_mode='write-blocking',
    161                              speed=1)
    162 
    163         self.vm.hmp_qemu_io('source', 'break write_aio A')
    164         self.vm.hmp_qemu_io('source', 'aio_write 0 1M')  # 1
    165         self.vm.hmp_qemu_io('source', 'wait_break A')
    166         self.vm.hmp_qemu_io('source', 'aio_write 0 2M')  # 2
    167         self.vm.hmp_qemu_io('source', 'aio_write 0 2M')  # 3
    168 
    169         # Now 2 and 3 are in mirror_wait_on_conflicts, waiting for 1
    170 
    171         self.vm.hmp_qemu_io('source', 'break write_aio B')
    172         self.vm.hmp_qemu_io('source', 'aio_write 1M 2M')  # 4
    173         self.vm.hmp_qemu_io('source', 'wait_break B')
    174 
    175         # 4 doesn't wait for 2 and 3, because they didn't yet set
    176         # in_flight_bitmap. So, nothing prevents 4 to go except for our
    177         # break-point B.
    178 
    179         self.vm.hmp_qemu_io('source', 'resume A')
    180 
    181         # Now we resumed 1, so 2 and 3 goes to the next iteration of while loop
    182         # in mirror_wait_on_conflicts(). They don't exit, as bitmap is dirty
    183         # due to request 4.
    184         # In the past at that point 2 and 3 would wait for each other producing
    185         # a dead-lock. Now this is fixed and they will wait for request 4.
    186 
    187         self.vm.hmp_qemu_io('source', 'resume B')
    188 
    189         # After resuming 4, one of 2 and 3 goes first and set in_flight_bitmap,
    190         # so the other will wait for it.
    191 
    192         result = self.vm.qmp('block-job-set-speed', device='mirror', speed=0)
    193         self.assert_qmp(result, 'return', {})
    194         self.complete_and_wait(drive='mirror')
    195 
    196         self.potential_writes_in_flight = False
    197 
    198 
    199 class TestThrottledWithNbdExportBase(iotests.QMPTestCase):
    200     image_len = 128 * 1024 * 1024  # MB
    201     iops: Optional[int] = None
    202     background_processes: List['subprocess.Popen[str]'] = []
    203 
    204     def setUp(self):
    205         # Must be set by subclasses
    206         self.assertIsNotNone(self.iops)
    207 
    208         qemu_img('create', '-f', iotests.imgfmt, source_img, '128M')
    209         qemu_img('create', '-f', iotests.imgfmt, target_img, '128M')
    210 
    211         self.vm = iotests.VM()
    212         self.vm.launch()
    213 
    214         result = self.vm.qmp('object-add', **{
    215             'qom-type': 'throttle-group',
    216             'id': 'thrgr',
    217             'limits': {
    218                 'iops-total': self.iops,
    219                 'iops-total-max': self.iops
    220             }
    221         })
    222         self.assert_qmp(result, 'return', {})
    223 
    224         result = self.vm.qmp('blockdev-add', **{
    225             'node-name': 'source-node',
    226             'driver': 'throttle',
    227             'throttle-group': 'thrgr',
    228             'file': {
    229                 'driver': iotests.imgfmt,
    230                 'file': {
    231                     'driver': 'file',
    232                     'filename': source_img
    233                 }
    234             }
    235         })
    236         self.assert_qmp(result, 'return', {})
    237 
    238         result = self.vm.qmp('blockdev-add', **{
    239             'node-name': 'target-node',
    240             'driver': iotests.imgfmt,
    241             'file': {
    242                 'driver': 'file',
    243                 'filename': target_img
    244             }
    245         })
    246         self.assert_qmp(result, 'return', {})
    247 
    248         self.nbd_sock = iotests.file_path('nbd.sock',
    249                                           base_dir=iotests.sock_dir)
    250         self.nbd_url = f'nbd+unix:///source-node?socket={self.nbd_sock}'
    251 
    252         result = self.vm.qmp('nbd-server-start', addr={
    253             'type': 'unix',
    254             'data': {
    255                 'path': self.nbd_sock
    256             }
    257         })
    258         self.assert_qmp(result, 'return', {})
    259 
    260         result = self.vm.qmp('block-export-add', id='exp0', type='nbd',
    261                              node_name='source-node', writable=True)
    262         self.assert_qmp(result, 'return', {})
    263 
    264     def tearDown(self):
    265         # Wait for background requests to settle
    266         try:
    267             while True:
    268                 p = self.background_processes.pop()
    269                 while True:
    270                     try:
    271                         p.wait(timeout=0.0)
    272                         break
    273                     except subprocess.TimeoutExpired:
    274                         self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
    275         except IndexError:
    276             pass
    277 
    278         # Cancel ongoing block jobs
    279         for job in self.vm.qmp('query-jobs')['return']:
    280             self.vm.qmp('block-job-cancel', device=job['id'], force=True)
    281 
    282         while True:
    283             self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
    284             if len(self.vm.qmp('query-jobs')['return']) == 0:
    285                 break
    286 
    287         self.vm.shutdown()
    288         os.remove(source_img)
    289         os.remove(target_img)
    290 
    291 
    292 class TestLowThrottledWithNbdExport(TestThrottledWithNbdExportBase):
    293     iops = 16
    294 
    295     def testUnderLoad(self):
    296         '''
    297         Throttle the source node, then issue a whole bunch of external requests
    298         while the mirror job (in write-blocking mode) is running.  We want to
    299         see background requests being issued even while the source is under
    300         full load by active writes, so that progress can be made towards READY.
    301         '''
    302 
    303         # Fill the first half of the source image; do not fill the second half,
    304         # that is where we will have active requests occur.  This ensures that
    305         # active mirroring itself will not directly contribute to the job's
    306         # progress (because when the job was started, those areas were not
    307         # intended to be copied, so active mirroring will only lead to not
    308         # losing progress, but also not making any).
    309         self.vm.hmp_qemu_io('source-node',
    310                             f'aio_write -P 1 0 {self.image_len // 2}')
    311         self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
    312 
    313         # Launch the mirror job
    314         mirror_buf_size = 65536
    315         result = self.vm.qmp('blockdev-mirror',
    316                              job_id='mirror',
    317                              filter_node_name='mirror-node',
    318                              device='source-node',
    319                              target='target-node',
    320                              sync='full',
    321                              copy_mode='write-blocking',
    322                              buf_size=mirror_buf_size)
    323         self.assert_qmp(result, 'return', {})
    324 
    325         # We create the external requests via qemu-io processes on the NBD
    326         # server.  Have their offset start in the middle of the image so they
    327         # do not overlap with the background requests (which start from the
    328         # beginning).
    329         active_request_offset = self.image_len // 2
    330         active_request_len = 4096
    331 
    332         # Create enough requests to saturate the node for 5 seconds
    333         for _ in range(0, 5 * self.iops):
    334             req = f'write -P 42 {active_request_offset} {active_request_len}'
    335             active_request_offset += active_request_len
    336             p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
    337             self.background_processes += [p]
    338 
    339         # Now advance the clock one I/O operation at a time by the 4 seconds
    340         # (i.e. one less than 5).  We expect the mirror job to issue background
    341         # operations here, even though active requests are still in flight.
    342         # The active requests will take precedence, however, because they have
    343         # been issued earlier than mirror's background requests.
    344         # Once the active requests we have started above are done (i.e. after 5
    345         # virtual seconds), we expect those background requests to be worked
    346         # on.  We only advance 4 seconds here to avoid race conditions.
    347         for _ in range(0, 4 * self.iops):
    348             step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
    349             self.vm.qtest(f'clock_step {step}')
    350 
    351         # Note how much remains to be done until the mirror job is finished
    352         job_status = self.vm.qmp('query-jobs')['return'][0]
    353         start_remaining = job_status['total-progress'] - \
    354             job_status['current-progress']
    355 
    356         # Create a whole bunch of more active requests
    357         for _ in range(0, 10 * self.iops):
    358             req = f'write -P 42 {active_request_offset} {active_request_len}'
    359             active_request_offset += active_request_len
    360             p = iotests.qemu_io_popen('-f', 'nbd', self.nbd_url, '-c', req)
    361             self.background_processes += [p]
    362 
    363         # Let the clock advance more.  After 1 second, as noted above, we
    364         # expect the background requests to be worked on.  Give them a couple
    365         # of seconds (specifically 4) to see their impact.
    366         for _ in range(0, 5 * self.iops):
    367             step = math.ceil(1 * 1000 * 1000 * 1000 / self.iops)
    368             self.vm.qtest(f'clock_step {step}')
    369 
    370         # Note how much remains to be done now.  We expect this number to be
    371         # reduced thanks to those background requests.
    372         job_status = self.vm.qmp('query-jobs')['return'][0]
    373         end_remaining = job_status['total-progress'] - \
    374             job_status['current-progress']
    375 
    376         # See that indeed progress was being made on the job, even while the
    377         # node was saturated with active requests
    378         self.assertGreater(start_remaining - end_remaining, 0)
    379 
    380 
    381 class TestHighThrottledWithNbdExport(TestThrottledWithNbdExportBase):
    382     iops = 1024
    383 
    384     def testActiveOnCreation(self):
    385         '''
    386         Issue requests on the mirror source node right as the mirror is
    387         instated.  It's possible that requests occur before the actual job is
    388         created, but after the node has been put into the graph.  Write
    389         requests across the node must in that case be forwarded to the source
    390         node without attempting to mirror them (there is no job object yet, so
    391         attempting to access it would cause a segfault).
    392         We do this with a lightly throttled node (i.e. quite high IOPS limit).
    393         Using throttling seems to increase reproductivity, but if the limit is
    394         too low, all requests allowed per second will be submitted before
    395         mirror_start_job() gets to the problematic point.
    396         '''
    397 
    398         # Let qemu-img bench create write requests (enough for two seconds on
    399         # the virtual clock)
    400         bench_args = ['bench', '-w', '-d', '1024', '-f', 'nbd',
    401                       '-c', str(self.iops * 2), self.nbd_url]
    402         p = iotests.qemu_tool_popen(iotests.qemu_img_args + bench_args)
    403         self.background_processes += [p]
    404 
    405         # Give qemu-img bench time to start up and issue requests
    406         time.sleep(1.0)
    407         # Flush the request queue, so new requests can come in right as we
    408         # start blockdev-mirror
    409         self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}')
    410 
    411         result = self.vm.qmp('blockdev-mirror',
    412                              job_id='mirror',
    413                              device='source-node',
    414                              target='target-node',
    415                              sync='full',
    416                              copy_mode='write-blocking')
    417         self.assert_qmp(result, 'return', {})
    418 
    419 
    420 if __name__ == '__main__':
    421     iotests.main(supported_fmts=['qcow2', 'raw'],
    422                  supported_protocols=['file'])