Browse Source

Add periodic job to prefetch images into cache

Added new periodic job which will run as per interval set using
'cache_prefetcher_interval' configuration option and fetch images
which are queued for caching in cache directory.

DocImpact
Change-Id: If2875f7a215aca10a6ed2febc89b02219f90a10d
tags/19.0.0.0b1
Abhishek Kekane 1 month ago
parent
commit
73fefddd96

+ 14
- 10
doc/source/admin/cache.rst View File

@@ -75,6 +75,8 @@ correctly.
75 75
 - ``filesystem_store_datadirs`` This is used to point to multiple
76 76
   filesystem stores.
77 77
 - ``registry_host`` The URL to the Glance registry.
78
+- ``cache_prefetcher_interval`` The interval in seconds to run periodic
79
+  job 'cache_images'.
78 80
 
79 81
 Controlling the Growth of the Image Cache
80 82
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -107,18 +109,20 @@ The recommended practice is to use ``cron`` to fire ``glance-cache-cleaner``
107 109
 at a semi-regular interval.
108 110
 
109 111
 Prefetching Images into the Image Cache
110
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
112
+---------------------------------------
111 113
 
112
-Some installations have base (sometimes called "golden") images that are
113
-very commonly used to boot virtual machines. When spinning up a new API
114
-server, administrators may wish to prefetch these image files into the
115
-local image cache to ensure that reads of those popular image files come
116
-from a local cache.
114
+In Train, Glance API has added a new periodic job ``cache_images`` which will
115
+run after every predefined time interval to fetch the queued images into cache.
116
+The default time interval for the ``cache_images`` periodic job is 300
117
+seconds. Admin/Operator can configure this interval in glance-api.conf file or
118
+glance-cache.conf file using ``cache_prefetcher_interval`` configuration
119
+option. The ``cache_images`` periodic job will only run if cache middleware
120
+is enabled in your cloud.
117 121
 
118 122
 To queue an image for prefetching, you can use one of the following methods:
119 123
 
120 124
 * If the ``cache_manage`` middleware is enabled in the application pipeline,
121
-  you may call ``PUT /queued-images/<IMAGE_ID>`` to queue the image with
125
+  you may call ``PUT /queued/<IMAGE_ID>`` to queue the image with
122 126
   identifier ``<IMAGE_ID>``
123 127
 
124 128
 * Alternately, you can use the ``glance-cache-manage`` program to queue the
@@ -129,9 +133,9 @@ To queue an image for prefetching, you can use one of the following methods:
129 133
 
130 134
   This will queue the image with identifier ``<IMAGE_ID>`` for prefetching
131 135
 
132
-Once you have queued the images you wish to prefetch, call the
133
-``glance-cache-prefetcher`` executable, which will prefetch all queued images
134
-concurrently, logging the results of the fetch for each image.
136
+Once you have queued the images you wish to prefetch, the ``cache_images``
137
+periodic job will prefetch all queued images concurrently, logging the
138
+results of the fetch for each image.
135 139
 
136 140
 Finding Which Images are in the Image Cache
137 141
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 9
- 1
glance/cmd/api.py View File

@@ -97,7 +97,15 @@ def main():
97 97
                 host=CONF.bind_host
98 98
             )
99 99
 
100
-        server = wsgi.Server(initialize_glance_store=True)
100
+        # NOTE(abhishekk): Added initialize_prefetcher KW argument to Server
101
+        # object so that prefetcher object should only be initialized in case
102
+        # of API service and ignored in case of registry. Once registry is
103
+        # removed this parameter should be removed as well.
104
+        initialize_prefetcher = False
105
+        if CONF.paste_deploy.flavor == 'keystone+cachemanagement':
106
+            initialize_prefetcher = True
107
+        server = wsgi.Server(initialize_glance_store=True,
108
+                             initialize_prefetcher=initialize_prefetcher)
101 109
         server.start(config.load_paste_app('glance-api'), default_port=9292)
102 110
         server.wait()
103 111
     except Exception as e:

+ 36
- 1
glance/common/wsgi.py View File

@@ -30,6 +30,7 @@ import signal
30 30
 import struct
31 31
 import subprocess
32 32
 import sys
33
+import threading
33 34
 import time
34 35
 
35 36
 from eventlet.green import socket
@@ -56,6 +57,7 @@ from glance.common import exception
56 57
 from glance.common import utils
57 58
 from glance import i18n
58 59
 from glance.i18n import _, _LE, _LI, _LW
60
+from glance.image_cache import prefetcher
59 61
 
60 62
 
61 63
 bind_opts = [
@@ -330,6 +332,23 @@ cli_opts = [
330 332
                     'used for inter-process communication.'),
331 333
 ]
332 334
 
335
+cache_opts = [
336
+    cfg.FloatOpt('cache_prefetcher_interval',
337
+                 default=300,
338
+                 help=_("""
339
+The interval in seconds to run periodic job cache_images.
340
+
341
+The cache_images method will fetch all images which are in queued state
342
+for caching in cache directory. The default value is 300.
343
+
344
+Possible values:
345
+    * Positive integer
346
+
347
+Related options:
348
+    * None
349
+"""))
350
+]
351
+
333 352
 LOG = logging.getLogger(__name__)
334 353
 
335 354
 CONF = cfg.CONF
@@ -338,6 +357,7 @@ CONF.register_opts(socket_opts)
338 357
 CONF.register_opts(eventlet_opts)
339 358
 CONF.register_opts(wsgi_opts)
340 359
 CONF.register_opts(store_opts)
360
+CONF.register_opts(cache_opts)
341 361
 profiler_opts.set_defaults(CONF)
342 362
 
343 363
 ASYNC_EVENTLET_THREAD_POOL_LIST = []
@@ -502,7 +522,8 @@ class BaseServer(object):
502 522
     This class requires initialize_glance_store set to True if
503 523
     glance store needs to be initialized.
504 524
     """
505
-    def __init__(self, threads=1000, initialize_glance_store=False):
525
+    def __init__(self, threads=1000, initialize_glance_store=False,
526
+                 initialize_prefetcher=False):
506 527
         os.umask(0o27)  # ensure files are created with the correct privileges
507 528
         self._logger = logging.getLogger("eventlet.wsgi.server")
508 529
         self.threads = threads
@@ -512,6 +533,18 @@ class BaseServer(object):
512 533
         # NOTE(abhishek): Allows us to only re-initialize glance_store when
513 534
         # the API's configuration reloads.
514 535
         self.initialize_glance_store = initialize_glance_store
536
+        self.initialize_prefetcher = initialize_prefetcher
537
+        if self.initialize_prefetcher:
538
+            self.prefetcher = prefetcher.Prefetcher()
539
+
540
+    def cache_images(self):
541
+        # After every 'cache_prefetcher_interval' this call will run and fetch
542
+        # all queued images into cache if there are any
543
+        cache_thread = threading.Timer(CONF.cache_prefetcher_interval,
544
+                                       self.cache_images)
545
+        cache_thread.daemon = True
546
+        cache_thread.start()
547
+        self.prefetcher.run()
515 548
 
516 549
     @staticmethod
517 550
     def set_signal_handler(signal_name, handler):
@@ -553,6 +586,8 @@ class BaseServer(object):
553 586
         self.default_port = default_port
554 587
         self.configure()
555 588
         self.start_wsgi()
589
+        if self.initialize_prefetcher:
590
+            self.cache_images()
556 591
 
557 592
     def start_wsgi(self):
558 593
         workers = get_num_workers()

+ 29
- 22
glance/image_cache/prefetcher.py View File

@@ -19,51 +19,58 @@ Prefetches images into the Image Cache
19 19
 
20 20
 import eventlet
21 21
 import glance_store
22
+from oslo_config import cfg
22 23
 from oslo_log import log as logging
23 24
 
24 25
 from glance.common import exception
25 26
 from glance import context
26 27
 from glance.i18n import _LI, _LW
27 28
 from glance.image_cache import base
28
-import glance.registry.client.v1.api as registry
29 29
 
30
+CONF = cfg.CONF
30 31
 LOG = logging.getLogger(__name__)
31 32
 
32 33
 
33 34
 class Prefetcher(base.CacheApp):
34
-
35 35
     def __init__(self):
36
+        # NOTE(abhishekk): Importing the glance.gateway just in time to avoid
37
+        # import loop during initialization
38
+        import glance.gateway  # noqa
36 39
         super(Prefetcher, self).__init__()
37
-        registry.configure_registry_client()
38
-        registry.configure_registry_admin_creds()
40
+        self.gateway = glance.gateway.Gateway()
39 41
 
40 42
     def fetch_image_into_cache(self, image_id):
41 43
         ctx = context.RequestContext(is_admin=True, show_deleted=True)
42 44
 
43 45
         try:
44
-            image_meta = registry.get_image_metadata(ctx, image_id)
45
-            if image_meta['status'] != 'active':
46
-                LOG.warn(_LW("Image '%s' is not active. Not caching.") %
47
-                         image_id)
48
-                return False
49
-
46
+            image_repo = self.gateway.get_repo(ctx)
47
+            image = image_repo.get(image_id)
50 48
         except exception.NotFound:
51
-            LOG.warn(_LW("No metadata found for image '%s'") % image_id)
49
+            LOG.warn(_LW("Image '%s' not found") % image_id)
52 50
             return False
53 51
 
54
-        location = image_meta['location']
55
-        image_data, image_size = glance_store.get_from_backend(location,
56
-                                                               context=ctx)
57
-        LOG.debug("Caching image '%s'", image_id)
58
-        cache_tee_iter = self.cache.cache_tee_iter(image_id, image_data,
59
-                                                   image_meta['checksum'])
60
-        # Image is tee'd into cache and checksum verified
61
-        # as we iterate
62
-        list(cache_tee_iter)
63
-        return True
52
+        if image.status != 'active':
53
+            LOG.warn(_LW("Image '%s' is not active. Not caching.") % image_id)
54
+            return False
64 55
 
65
-    def run(self):
56
+        for loc in image.locations:
57
+            if CONF.enabled_backends:
58
+                image_data, image_size = glance_store.get(loc['url'],
59
+                                                          None,
60
+                                                          context=ctx)
61
+            else:
62
+                image_data, image_size = glance_store.get_from_backend(
63
+                    loc['url'], context=ctx)
64
+
65
+            LOG.debug("Caching image '%s'", image_id)
66
+            cache_tee_iter = self.cache.cache_tee_iter(image_id, image_data,
67
+                                                       image.checksum)
68
+            # Image is tee'd into cache and checksum verified
69
+            # as we iterate
70
+            list(cache_tee_iter)
71
+            return True
66 72
 
73
+    def run(self):
67 74
         images = self.cache.get_queued_images()
68 75
         if not images:
69 76
             LOG.debug("Nothing to prefetch.")

+ 5
- 0
glance/tests/functional/__init__.py View File

@@ -759,9 +759,14 @@ class RegistryServer(Server):
759 759
         self.policy_file = policy_file
760 760
         self.policy_default_rule = 'default'
761 761
         self.disable_path = None
762
+        self.image_cache_dir = os.path.join(self.test_dir,
763
+                                            'cache')
764
+        self.image_cache_driver = 'sqlite'
762 765
 
763 766
         self.conf_base = """[DEFAULT]
764 767
 debug = %(debug)s
768
+image_cache_dir = %(image_cache_dir)s
769
+image_cache_driver = %(image_cache_driver)s
765 770
 bind_host = %(bind_host)s
766 771
 bind_port = %(bind_port)s
767 772
 log_file = %(log_file)s

+ 5
- 0
glance/tests/functional/test_client_exceptions.py View File

@@ -15,6 +15,7 @@
15 15
 #    under the License.
16 16
 
17 17
 """Functional test asserting strongly typed exceptions from glance client"""
18
+import os
18 19
 
19 20
 import eventlet.patcher
20 21
 import httplib2
@@ -70,6 +71,10 @@ class TestClientExceptions(functional.FunctionalTest):
70 71
     def setUp(self):
71 72
         super(TestClientExceptions, self).setUp()
72 73
         self.port = utils.get_unused_port()
74
+        self.image_cache_dir = os.path.join(self.test_dir,
75
+                                            'cache')
76
+        self.config(image_cache_dir=self.image_cache_dir)
77
+        self.config(image_cache_driver='sqlite')
73 78
         server = wsgi.Server()
74 79
         self.config(bind_host='127.0.0.1')
75 80
         self.config(workers=0)

+ 5
- 0
glance/tests/functional/test_client_redirects.py View File

@@ -14,6 +14,7 @@
14 14
 #    under the License.
15 15
 
16 16
 """Functional test cases testing glance client redirect-following."""
17
+import os
17 18
 
18 19
 import eventlet.patcher
19 20
 from six.moves import http_client as http
@@ -85,6 +86,10 @@ class TestClientRedirects(functional.FunctionalTest):
85 86
         super(TestClientRedirects, self).setUp()
86 87
         self.port_one = utils.get_unused_port()
87 88
         self.port_two = utils.get_unused_port()
89
+        self.image_cache_dir = os.path.join(self.test_dir,
90
+                                            'cache')
91
+        self.config(image_cache_dir=self.image_cache_dir)
92
+        self.config(image_cache_driver='sqlite')
88 93
         server_one = wsgi.Server()
89 94
         server_two = wsgi.Server()
90 95
         self.config(bind_host='127.0.0.1')

+ 9
- 7
glance/tests/functional/test_wsgi.py View File

@@ -19,19 +19,21 @@ import os
19 19
 import socket
20 20
 import time
21 21
 
22
-from oslo_config import cfg
23
-import testtools
22
+import fixtures
24 23
 
25 24
 from glance.common import wsgi
25
+from glance.tests import functional
26 26
 
27
-CONF = cfg.CONF
28 27
 
29
-
30
-class TestWSGIServer(testtools.TestCase):
28
+class TestWSGIServer(functional.FunctionalTest):
31 29
     """WSGI server tests."""
32 30
     def test_client_socket_timeout(self):
33
-        CONF.set_default("workers", 0)
34
-        CONF.set_default("client_socket_timeout", 1)
31
+        test_dir = self.useFixture(fixtures.TempDir()).path
32
+        image_cache_dir = os.path.join(test_dir, 'cache')
33
+        self.config(workers=0)
34
+        self.config(client_socket_timeout=1)
35
+        self.config(image_cache_dir=image_cache_dir)
36
+        self.config(image_cache_driver="sqlite")
35 37
         """Verify connections are timed out as per 'client_socket_timeout'"""
36 38
         greetings = b'Hello, World!!!'
37 39
 

+ 5
- 2
glance/tests/unit/api/test_cmd.py View File

@@ -24,6 +24,7 @@ import glance.common.config
24 24
 from glance.common import exception as exc
25 25
 import glance.common.wsgi
26 26
 import glance.image_cache.cleaner
27
+from glance.image_cache import prefetcher
27 28
 import glance.image_cache.pruner
28 29
 from glance.tests import utils as test_utils
29 30
 
@@ -64,11 +65,13 @@ class TestGlanceApiCmd(test_utils.BaseTestCase):
64 65
         sys.argv = self.__argv_backup
65 66
         super(TestGlanceApiCmd, self).tearDown()
66 67
 
67
-    def test_supported_default_store(self):
68
+    @mock.patch.object(prefetcher, 'Prefetcher')
69
+    def test_supported_default_store(self, mock_prefetcher):
68 70
         self.config(group='glance_store', default_store='file')
69 71
         glance.cmd.api.main()
70 72
 
71
-    def test_worker_creation_failure(self):
73
+    @mock.patch.object(prefetcher, 'Prefetcher')
74
+    def test_worker_creation_failure(self, mock_prefetcher):
72 75
         failure = exc.WorkerCreationFailure(reason='test')
73 76
         self.mock_object(glance.common.wsgi.Server, 'start',
74 77
                          self._raise(failure))

+ 9
- 4
glance/tests/unit/common/test_wsgi.py View File

@@ -36,6 +36,7 @@ from glance.common import exception
36 36
 from glance.common import utils
37 37
 from glance.common import wsgi
38 38
 from glance import i18n
39
+from glance.image_cache import prefetcher
39 40
 from glance.tests import utils as test_utils
40 41
 
41 42
 
@@ -561,13 +562,15 @@ class JSONRequestDeserializerTest(test_utils.BaseTestCase):
561 562
 
562 563
 
563 564
 class ServerTest(test_utils.BaseTestCase):
564
-    def test_create_pool(self):
565
+    @mock.patch.object(prefetcher, 'Prefetcher')
566
+    def test_create_pool(self, mock_prefetcher):
565 567
         """Ensure the wsgi thread pool is an eventlet.greenpool.GreenPool."""
566 568
         actual = wsgi.Server(threads=1).create_pool()
567 569
         self.assertIsInstance(actual, eventlet.greenpool.GreenPool)
568 570
 
571
+    @mock.patch.object(prefetcher, 'Prefetcher')
569 572
     @mock.patch.object(wsgi.Server, 'configure_socket')
570
-    def test_http_keepalive(self, mock_configure_socket):
573
+    def test_http_keepalive(self, mock_configure_socket, mock_prefetcher):
571 574
         self.config(http_keepalive=False)
572 575
         self.config(workers=0)
573 576
 
@@ -588,7 +591,8 @@ class ServerTest(test_utils.BaseTestCase):
588 591
                                                 keepalive=False,
589 592
                                                 socket_timeout=900)
590 593
 
591
-    def test_number_of_workers_posix(self):
594
+    @mock.patch.object(prefetcher, 'Prefetcher')
595
+    def test_number_of_workers_posix(self, mock_prefetcher):
592 596
         """Ensure the number of workers matches num cpus limited to 8."""
593 597
         if os.name == 'nt':
594 598
             raise self.skipException("Unsupported platform.")
@@ -696,7 +700,8 @@ class GetSocketTestCase(test_utils.BaseTestCase):
696 700
         wsgi.CONF.ca_file = '/etc/ssl/ca_cert'
697 701
         wsgi.CONF.tcp_keepidle = 600
698 702
 
699
-    def test_correct_configure_socket(self):
703
+    @mock.patch.object(prefetcher, 'Prefetcher')
704
+    def test_correct_configure_socket(self, mock_prefetcher):
700 705
         mock_socket = mock.Mock()
701 706
         self.useFixture(fixtures.MonkeyPatch(
702 707
             'glance.common.wsgi.ssl.wrap_socket',

Loading…
Cancel
Save