1670971020
Recently I’ve been playing around with Netlify and as a result I’m becoming more familiar with caching strategies commonly found with content delivery networks (CDN). One such strategy makes use of ETag identifiers for web resources.
In short, an ETag identifier is a value, typically a hash, that represents the version of a particular web resource. The resource is cached within the browser along with the ETag value and that value is used when determining if the particular cached resource has changed remotely.
We’re going to explore how to simulate the requests that the browser makes when working with ETag identifiers, but using simple cURL requests instead.
To get started, we’re going to make a request for a resource:
$ curl -I https://www.thepolyglotdeveloper.com/css/custom.min.css
HTTP/2 200
accept-ranges: bytes
cache-control: public, max-age=0, must-revalidate
content-length: 7328
content-type: text/css; charset=UTF-8
date: Wed, 04 Sep 2019 00:41:04 GMT
strict-transport-security: max-age=31536000
etag: "018b8b0ecb632aab770af328f043b119-ssl"
age: 0
server: Netlify
x-nf-request-id: 65a8e1aa-03a0-4b6c-9f46-51aba795ad83-921013
In the above request I’ve only requested the header information from the response. For this tutorial, the body of the response isn’t important to us.
Take note of the cache-control
and the etag
headers as well as the response code.
In the scenario of Netlify, the cache-control
header tells the browser to cache the resource, but also not to trust the cache. This is done so the client always attempts to get the latest resource. The etag
header represents the version of the resource and it is sent with future requests. If the server says the etag
hasn’t changed between requests then the response will have a 304 code and the cached resource will be used instead.
So let’s check to see if the resource has changed with cURL:
$ curl -I -H 'If-None-Match: "018b8b0ecb632aab770af328f043b119-ssl"' https://www.thepolyglotdeveloper.com/css/custom.min.css
HTTP/2 304
date: Wed, 04 Sep 2019 00:53:24 GMT
etag: "018b8b0ecb632aab770af328f043b119-ssl"
cache-control: public, max-age=0, must-revalidate
server: Netlify
x-nf-request-id: eca29310-c9bf-4742-87e1-3412e8852381-2165939
With the new request to the same resource, the If-None-Match
header is included and the value is the etag
hash from the previous request.
Notice that this time around the response status code was 304 as anticipated. Had the etag
been different, a 200 response would have happened with a new etag
hash.
If you look at your browser’s network inspector you might notice that etag
hashes for resources have a -df
value appended to them. For example, for the same resource, my browser is showing the following:
018b8b0ecb632aab770af328f043b119-ssl-df
While similar, it isn’t totally the same as the etag
hash that came back with the previous cURL requests. Try to run a cURL request with the above etag
value:
$ curl -I -H 'If-None-Match: "018b8b0ecb632aab770af328f043b119-ssl-df"' https://www.thepolyglotdeveloper.com/css/custom.min.css
HTTP/2 200
accept-ranges: bytes
cache-control: public, max-age=0, must-revalidate
content-length: 7328
content-type: text/css; charset=UTF-8
date: Wed, 04 Sep 2019 01:03:13 GMT
strict-transport-security: max-age=31536000
etag: "018b8b0ecb632aab770af328f043b119-ssl"
age: 0
server: Netlify
x-nf-request-id: 2734ffab-c611-4fc9-841e-460f172aa3b4-1604468
The response was not a 304 code because the -df
means that it is a compressed version of the URL. As it stands, our cURL requests have been for uncompressed versions of the URL.
A Support Engineer at Netlify pointed this difference out to me in this forum thread.
In most circumstances the web browser will include the appropriate header information to work with compressed resources, so in cURL we have to do something different.
To get beyond this with cURL, the following request would work:
$ curl --compressed -I -H 'If-None-Match: "018b8b0ecb632aab770af328f043b119-ssl-df"' https://www.thepolyglotdeveloper.com/css/custom.min.css
HTTP/2 304
date: Wed, 04 Sep 2019 01:07:36 GMT
etag: "018b8b0ecb632aab770af328f043b119-ssl-df"
cache-control: public, max-age=0, must-revalidate
server: Netlify
vary: Accept-Encoding
x-nf-request-id: 65a8e1aa-03a0-4b6c-9f46-51aba795ad83-1301670
Notice in the above request that we’re now using the --compressed
flag with cURL. As a result, we get a 304 response indicating that the resource hasn’t changed and we should used the locally cached copy.
Alternatively, we could execute the following cURL request:
$ curl -I -H 'If-None-Match: "018b8b0ecb632aab770af328f043b119-ssl-df"' -H 'Accept-Encoding: gzip, deflate, br' https://www.thepolyglotdeveloper.com/css/custom.min.css
HTTP/2 304
date: Wed, 04 Sep 2019 01:12:34 GMT
etag: "018b8b0ecb632aab770af328f043b119-ssl-df"
cache-control: public, max-age=0, must-revalidate
server: Netlify
vary: Accept-Encoding
x-nf-request-id: eca29310-c9bf-4742-87e1-3412e8852381-2432816
Instead of using the --compressed
flag, we are including an accept-encoding
header.
Again, information around compressed versions were provided to me by Luke Lawson from Netlify in this forum thread.
You just saw how to simulate the same caching that happens in the web browser using cURL instead. Since I’m new to content delivery networks (CDN) and how they handle caching, this was very useful to me when it came to testing how caching worked with the etag
hash for any given resource. The 304 response will always be received quicker and with a smaller payload than a 200 response which saves bandwidth and performance without sacrificing the freshness of content.
In theory, the CDN will maintain versioning information for a given resource and as a result will be able to validate etag
values for freshness. It is not up to the browser to determine if the etag
is stale.
Original article source at: https://www.thepolyglotdeveloper.com/
1670424383
Serving map clusters to a mobile app can cause a significant performance bottleneck. Fortunately, it’s a problem that can be solved with this caching strategy.
Map components that feature location markers are common in mobile apps today. For example, the Airbnb app prominently features such markers, fetched from a web service, to represent available properties on a map.
To ensure that the volume of fetched data does not become unmanageable as the number of markers grows, the server groups those markers together before sending them to the client. A map cluster is a specialized marker whose position equals the average position of the markers it subsumes. It is labeled with the number of markers it represents.
Still, serving clusters can create performance bottlenecks because a web service must retrieve every single marker within a given geographical region from the database. Fortunately, this issue can be resolved with a caching strategy. To better understand how to design and maintain a cache, let’s look at an example map API endpoint I built for the Playsports app.
In the Playsports map, each marker represents a sports facility. The map’s API endpoint needs to return a list of markers and marker clusters, given a zoom level and a bounding box.
A bounding box, markers, and clusters on a map
If the number of markers is small enough, we can load all markers in the bounding box from the database, cluster as necessary, and return the resultant markers and clusters to the client.
At the outset, the maximum number of markers in any reachable bounding box in Playsports was ~400, resulting in an endpoint speed of ~0.5 seconds. Implementing the naive solution was sufficient for this use case.
As the number of markers grew, however, the mechanism’s inefficiency became obvious. After we added ~10,000 new sports facility markers, the endpoint speed slowed to ~4.3 seconds under some zoom levels. This rate is far below the one-second duration that is generally considered to be the maximum acceptable delay for a user action in a mobile app.
To better understand the naive solution’s inefficiencies, let’s analyze it in the context of the marker query:
As the number of markers increases, performance deteriorates in Steps 1 and 2:
Assuming a constant window size, when a bounding box is relatively large, the zoom level is usually low (i.e., zoomed out far). Under low zoom levels, results tend to favor clusters to avoid visual crowding. Thus, the greatest potential for optimization lies in the first step of the solution, where thousands of individual markers are loaded. We don’t need most of these markers in the result. We only need each of them to count toward a cluster.
The naive solution takes significantly longer to complete a worst-case query: a maximum bounding box in a marker-dense region. In contrast, the optimized solution would take just 73 ms, representing a ~58x speedup. From a high level, it looks like this:
The main complexity lies in the architecture of the cache (i.e., the first step).
This main step consists of six components:
Redis is a fast, in-memory database that is commonly used as a cache. Its built-in geospatial indexing uses the Geohash algorithm for the efficient storage and retrieval of latitude-longitude points, which is precisely what we need for our markers.
The degree of map clustering (whether single markers or clusters are returned) is determined by the zoom level passed to the endpoint.
Google Maps supports zoom levels from zero to a maximum of 20, depending on the area. (Ranges supported by other map services are similar. For instance, Mapbox uses zoom levels from zero to a maximum of 23.) Since these zoom levels are also integer values, we can simply create a separate cache for each zoom level.
To support all zoom levels in Google Maps—except level zero, which is too far zoomed out—we will create 20 distinct caches. Each cache will store all markers and clusters for the entire map, as clustered for the zoom level it represents.
Comparison of two zoom-level views
Each cache would store clusters and individual markers. For either type of entry, we’ll need to populate several fields:
Field Name | Note |
---|---|
Type | Cluster or marker |
Latitude and longitude | We duplicate Redis’ internal geospatial storage for convenience. |
ID (for markers only) | In Step 2, we can use this value to fetch details beyond location, such as user interaction history. |
Quantity of subsumed markers (for clusters only) | In Step 2, we can fetch aggregate data (e.g., the number of unviewed markers) for these too. |
However, Redis allows users to store only the location, plus a single string, as the value in a geospatial set. To get around this restriction, we serialize the above fields in a JSON string. We then use this string as the value in Redis. The maximum size of both keys and values in Redis is 512 MB, which is more than enough for this use case.
To fill the caches, we retrieve all of the markers from the database. For each zoom level, we execute the map-clustering algorithm. We use Redis’ GEOADD
to insert the resulting markers and clusters into the cache of the corresponding zoom level, and pass along the latitude and longitude, plus the previously described JSON string.
Running the map-clustering algorithm on the whole map at this stage (rather than on a bounding box from the user, on demand) might theoretically produce some edge differences in cluster placement, but the overall user experience will remain the same.
For an incoming request, we pass the given bounding box to the Redis GEOSEARCH
command, which queries the cache of the given zoom level to retrieve marker and cluster candidates in the area.
A 20-level cache refresh is expensive, so it’s best to refresh as infrequently as your project requirements allow. For example, the addition or removal of a sports facility in Playsports only marks the cache as stale; an hourly cron job then refreshes the cache, if needed. More on this in the Cache Staleness section.
At this point, most apps will need to fetch details based on individual marker IDs. We could make this detail part of the stringified JSON values in the cache, but in many apps, marker details are user-specific. Since there’s a single, shared cache for all users, it’s not possible to store these additional fields there.
Our optimized solution takes the IDs of the individual markers from the cache result and fetches their details from the database. Now we’re only looking up the individual markers that are left after clustering. There won’t be too many of these when the map is zoomed out (because we’ll have mostly clusters) nor when zoomed in (because the area will be small).
In contrast, the naive solution looks up all markers in the bounding box (and their details) before clustering. Thus, this step—optional, but for many apps, crucial—now runs much faster.
With the clusters built and the individual markers enhanced, we can now deliver these to the client. Aside from some inconsequential edge cases, the resulting data is nearly identical to the naive solution—but we’re able to deliver it much faster.
Now let’s look at two additional factors:
Let’s assume that an app’s map contains N markers total. As there are up to 20 zoom levels, we would need to store, at most, 20N cache items. In practice, however, the actual number of cache items is often much lower due to clustering, especially in the lower zoom levels. In fact, the total number of cache items distributed among all of Playsports’ caches is only ~2N.
Further, if we assume that the length of a cache value (the stringified JSON) is ~250 characters (a reasonable assumption, at least for Playsports) and the string size is 1 byte per character, then the amount of cache storage needed for the JSON values would be approximately 2 * N * 250 bytes.
To this figure we add Redis’ internal data structures for the sorted sets used by GEOADD
. Redis uses 85 MB of memory for 1 million small key-value pairs, so we can assume Redis internals account for less than 100 bytes per cache item. That means we could use a 1 GB-RAM Redis instance to support as many as 1.4 million markers. Even in the unlikely scenario of the markers being evenly spread out across the entire map, resulting in the number of cache items nearing 20N, N could still go up to ~140,000. Since a Redis instance can handle more than 4 billion keys (232, to be exact), this is not a limiting factor.
Depending on the use case, it may not be sufficient to refresh the cache only periodically. In such cases, we could use a rate-limited task queue. It would reduce cache staleness, while still limiting the number of cache refreshes, and thus the load on the system.
After each database update, we would queue a cache refresh job. This queue would limit the number of tasks to M per hour. This compromise allows for faster-than-hourly updates while maintaining a relatively low load on the system (depending on M).
The optimized solution for Playsports—more than 50 times faster than the naive solution—has worked well. It should continue to work well, until we reach 1.4 million markers or more than 100 times the existing data.
For most map-based web service requests, some form of precalculation is needed to allow for scalability. The type of technology to be used, as well as the specific design, would depend on business requirements. Cache staleness needs, marker augmentation, and the number of markers are important characteristics to take into account when designing a solution.
Original article source at: https://www.toptal.com/
1668995760
Caching is typically the most effective way to boost an application's performance.
For dynamic websites, when rendering a template, you'll often have to gather data from various sources (like a database, the file system, and third-party APIs, to name a few), process the data, and apply business logic to it before serving it up to a client. Any delay due to network latency will be noticed by the end user.
For instance, say you have to make an HTTP call to an external API to grab the data required to render a template. Even in perfect conditions this will increase the rendering time which will increase the overall load time. What if the API goes down or maybe you're subject to rate limiting? Either way, if the data is infrequently updated, it's a good idea to implement a caching mechanism to prevent having to make the HTTP call altogether for each client request.
This article looks at how to do just that by first reviewing Django's caching framework as a whole and then detailing step-by-step how to cache a Django view.
Dependencies:
--
Django Caching Articles:
By the end of this tutorial, you should be able to:
Django comes with several built-in caching backends, as well as support for a custom backend.
The built-in options are:
Caching in Django can be implemented on different levels (or parts of the site). You can cache the entire site or specific parts with various levels of granularity (listed in descending order of granularity):
This is the easiest way to implement caching in Django. To do this, all you'll have to do is add two middleware classes to your settings.py file:
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
The order of the middleware is important here.
UpdateCacheMiddleware
must come beforeFetchFromCacheMiddleware
. For more information take a look at Order of MIDDLEWARE from the Django docs.
You then need to add the following settings:
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
Although caching the entire site could be a good option if your site has little or no dynamic content, it may not be appropriate to use for large sites with a memory-based cache backend since RAM is, well, expensive.
Rather than wasting precious memory space on caching static pages or dynamic pages that source data from a rapidly changing API, you can cache specific views. This is the approach that we'll use in this article. It's also the caching level that you should almost always start with when looking to implement caching in your Django app.
You can implement this type of cache with the cache_page decorator either on the view function directly or in the path within URLConf
:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
The cache itself is based on the URL, so requests to, say, object/1
and object/2
will be cached separately.
It's worth noting that implementing the cache directly on the view makes it more difficult to disable the cache in certain situations. For example, what if you wanted to allow certain users access to the view without the cache? Enabling the cache via the URLConf
provides the opportunity to associate a different URL to the view that doesn't use the cache:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
If your templates contain parts that change often based on the data you'll probably want to leave them out of the cache.
For example, perhaps you use the authenticated user's email in the navigation bar in an area of the template. Well, If you have thousands of users then that fragment will be duplicated thousands of times in RAM, one for each user. This is where template fragment caching comes into play, which allows you to specify the specific areas of a template to cache.
To cache a list of objects:
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
Here, {% load cache %}
gives us access to the cache
template tag, which expects a cache timeout in seconds (500
) along with the name of the cache fragment (object_list
).
For cases where the previous options don't provide enough granularity, you can use the low-level API to manage individual objects in the cache by cache key.
For example:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
In this example, you'll want to invalidate (or remove) the cache when objects are added, changed, or removed from the database. One way to manage this is via database signals:
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
For more on using database signals to invalidate cache, check out the Low-Level Cache API in Django article.
With that, let's look at some examples.
Clone down the base project from the cache-django-view repo, and then check out the base branch:
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
Create (and activate) a virtual environment and install the requirements:
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
Apply the Django migrations, and then start the server:
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
Navigate to http://127.0.0.1:8000 in your browser of choice to ensure that everything works as expected.
You should see:
Take note of your terminal. You should see the total execution time for the request:
Total time: 2.23s
This metric comes from core/middleware.py:
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
Take a quick look at the view in apicalls/views.py:
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
This view makes an HTTP call with requests
to httpbin.org. To simulate a long request, the response from the API is delayed for two seconds. So, it should take about two seconds for http://127.0.0.1:8000 to render not only on the initial request, but for each subsequent request as well. While a two second load is somewhat acceptable on the initial request, it's completely unacceptable for subsequent requests since the data is not changing. Let's fix this by caching the entire view using Django's Per-view cache level.
Workflow:
Before adding cache, let's quickly run a load test to get a benchmark baseline using Apache Bench, to get rough sense of how many requests our application can handle per second.
Apache Bench comes pre-installed on Mac.
If you're on a Linux system, chances are it's already installed and ready to go as well. If not, you can install via APT (
apt-get install apache2-utils
) or YUM (yum install httpd-tools
).Windows users will need to download and extract the Apache binaries.
Add Gunicorn to the requirements file:
gunicorn==20.1.0
Kill the Django dev server and install Gunicorn:
(venv)$ pip install -r requirements.txt
Next, serve up the Django app with Gunicorn (and four workers) like so:
(venv)$ gunicorn core.wsgi:application -w 4
In a new terminal window, run Apache Bench:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
This will simulate 100 connections over 10 concurrent threads. That's 100 requests, 10 at a time.
Take note of the requests per second:
Requests per second: 1.69 [#/sec] (mean)
Keep in mind that Django Debug Toolbar will add a bit of overhead. Benchmarking in general is difficult to get perfectly right. The important thing is consistency. Pick a metric to focus on and use the same environment for each test.
Kill the Gunicorn server and spin the Django dev server back up:
(venv)$ python manage.py runserver
With that, let's look at how to cache a view.
Start by decorating the ApiCalls
view with the @cache_page
decorator like so:
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Since we're using a class-based view, we can't put the decorator directly on the class, so we used a method_decorator
and specified dispatch
(as the method to be decorated) for the name argument.
The cache in this example sets a timeout (or TTL) of five minutes.
Alternatively, you could set this in your settings like so:
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
Then, back in the view:
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Next, let's add a cache backend.
Memcached and Redis are in-memory, key-value data stores. They are easy to use and optimized for high-performance lookups. You probably won't see much difference in performance or memory usage between the two. That said, Memcached is slightly easier to configure since it's designed for simplicity and ease of use. Redis, on the other hand, has a richer set of features so it has a wide range of use cases beyond caching. For example, it's often used to store user sessions or as message broker in a pub/sub system. Because of its flexibility, unless you're already invested in Memcached, Redis is much better solution.
For more on this, review this Stack Overflow answer.
Next, pick your data store of choice and let's look at how to cache a view.
Download and install Redis.
If you’re on a Mac, we recommend installing Redis with Homebrew:
$ brew install redis
Once installed, in a new terminal window start the Redis server and make sure that it's running on its default port, 6379. The port number will be important when we tell Django how to communicate with Redis.
$ redis-server
For Django to use Redis as a cache backend, we first need to install django-redis.
Add it to the requirements.txt file:
django-redis==5.0.0
Install:
(venv)$ pip install -r requirements.txt
Next, add the custom backend to the settings.py file:
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Now, when you run the server again, Redis will be used as the cache backend:
(venv)$ python manage.py runserver
With the server up and running, navigate to http://127.0.0.1:8000.
The first request will still take about two seconds. Refresh the page. The page should load almost instantaneously. Take a look at the load time in your terminal. It should be close to zero:
Total time: 0.01s
Curious what the cached data looks like inside of Redis?
Run Redis CLI in interactive mode in a new terminal window:
$ redis-cli
You should see:
127.0.0.1:6379>
Run ping
to ensure everything works properly:
127.0.0.1:6379> ping
PONG
Turn back to the settings file. We used Redis database number 1: 'LOCATION': 'redis://127.0.0.1:6379/1',
. So, run select 1
to select that database and then run keys *
to view all the keys:
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
We can see that Django put in one header key and one cache_page
key.
To view the actual cached data, run the get
command with the key as an argument:
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Your should see something similar to:
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
Exit the interactive CLI once done:
127.0.0.1:6379[1]> exit
Skip down to the "Performance Tests" section.
Start by adding pymemcache to the requirements.txt file:
pymemcache==3.5.0
Install the dependencies:
(venv)$ pip install -r requirements.txt
Next, we need to update the settings in core/settings.py to enable the Memcached backend:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
Here, we added the PyMemcacheCache backend and indicated that Memcached should be running on our local machine on localhost (127.0.0.1) port 11211, which is the default port for Memcached.
Next, we need to install and run the Memcached daemon. The easiest way to install it, is via a package manager like APT, YUM, Homebrew, or Chocolatey depending on your operating system:
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
Then, run it in a different terminal on port 11211:
$ memcached -p 11211
# test: telnet localhost 11211
For more information on installation and configuration of Memcached, review the official wiki.
Navigate to http://127.0.0.1:8000 in our browser again. The first request will still take the full two seconds, but all subsequent requests will take advantage of the cache. So, if you refresh or press the "Get new data button", the page should load almost instantly.
What's the execution time look like in your terminal?
Total time: 0.03s
If we look at the time it takes to load the first request vs. the second (cached) request in Django Debug Toolbar, it will look similar to:
Also in the Debug Toolbar, you can see the cache operations:
Spin Gunicorn back up again and re-run the performance tests:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
What are the new requests per second? It's about 36 on my machine!
In this article, we looked at the different built-in options for caching in Django as well as the different levels of caching available. We also detailed how to cache a view using Django's Per-view cache with both Memcached and Redis.
You can find the final code for both options, Memcached and Redis, in the cache-django-view repo.
--
In general, you'll want to look to caching when page rendering is slow due to network latency from database queries or HTTP calls.
From there, it's highly recommend to use a custom Django cache backend with Redis with a Per-view
type. If you need more granularity and control, because not all of the data on the template is the same for all users or parts of the data changes frequently, then jump down to the Template fragment cache or Low-level cache API.
--
Django Caching Articles:
Original article source at: https://testdriven.io/
1668846300
This article takes a look at how to speed up your Docker-based builds on CircleCI, GitLab CI, and GitHub Actions with Docker layer Caching and BuildKit.
Docker caches each layer as an image is built, and each layer will only be re-built if it or the layer above it has changed since the last build. So, you can significantly speed up builds with Docker cache. Let's take a look at a quick example.
Dockerfile:
# pull base image
FROM python:3.9.7-slim
# install netcat
RUN apt-get update && \
apt-get -y install netcat && \
apt-get clean
# set working directory
WORKDIR /usr/src/app
# install requirements
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# add app
COPY . .
# run server
CMD gunicorn -b 0.0.0.0:5000 manage:app
You can find the full source code for this project in the docker-ci-cache repo on GitHub.
The first Docker build can take several minutes to complete, depending on your connection speed. Subsequent builds should only take a few seconds since the layers get cached after that first build:
[+] Building 0.4s (12/12) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 35B 0.0s
=> [internal] load metadata for docker.io/library/python:3.9.7-slim 0.3s
=> [internal] load build context 0.0s
=> => transferring context: 555B 0.0s
=> [1/7] FROM docker.io/library/python:3.9.7-slim@sha256:bdefda2b80c5b4d993ef83d2445d81b2b894bf627b62bd7b0f01244de2b6a 0.0s
=> CACHED [2/7] RUN apt-get update && apt-get -y install netcat && apt-get clean 0.0s
=> CACHED [3/7] WORKDIR /usr/src/app 0.0s
=> CACHED [4/7] COPY ./requirements.txt . 0.0s
=> CACHED [5/7] RUN pip install -r requirements.txt 0.0s
=> CACHED [6/7] COPY project . 0.0s
=> CACHED [7/7] COPY manage.py . 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:2b8b7c5a6d1b77d5bcd689ab265b0281ad531bd2e34729cff82285f5abdcb59f 0.0s
=> => naming to docker.io/library/cache 0.0s
Even if you make a change to the source code it should still only take a few seconds to build as the dependencies will not need to be downloaded. Only the last two layers have to be re-built, in other words:
=> [6/7] COPY project .
=> [7/7] COPY manage.py .
To avoid invalidating the cache:
COPY . .
) as late as possibleFor more tips and best practices, check out the Docker Best Practices for Python Developers article.
If you're using a Docker version >= 19.03 you can use BuildKit, a container image builder, in place of the traditional image builder back-end inside the Docker engine. Without BuildKit, if an image doesn't exist on your local image registry, you would need to pull the remote images before building in order to take advantage of Docker layer caching.
Example:
$ docker pull mjhea0/docker-ci-cache:latest
$ docker docker build --tag mjhea0/docker-ci-cache:latest .
With BuildKit, you don't need to pull the remote images before building since it caches each build layer in your image registry. Then, when you build the image, each layer is downloaded as needed during the build.
To enable BuildKit, set the DOCKER_BUILDKIT
environment variable to 1
. Then, to turn on the inline layer caching, use the BUILDKIT_INLINE_CACHE
build argument.
Example:
export DOCKER_BUILDKIT=1
# Build and cache image
$ docker build --tag mjhea0/docker-ci-cache:latest --build-arg BUILDKIT_INLINE_CACHE=1 .
# Build image from remote cache
$ docker build --cache-from mjhea0/docker-ci-cache:latest .
Since CI platforms provide a fresh environment for every build, you'll need to use a remote image registry as the source of the cache for BuildKit's layer caching.
Steps:
Log in to the image registry (like Docker Hub, Elastic Container Registry (ECR), and Quay, to name a few).
It's worth noting that both GitLab and GitHub have their own registries for use within your repositories (both public and private) on their platforms -- GitLab Container Registry and GitHub Packages, respectively.
Use Docker build's --cache-from
option to use the existing image as the cache source.
Let's look at how to do this on CircleCI, GitLab CI, and GitHub Actions, using both single and multi-stage Docker builds with and without Docker Compose. Each of the examples use Docker Hub as the image registry with REGISTRY_USER
and REGISTRY_PASS
set as variables in the CI builds in order to push to and pull from the registry.
Make sure to set
REGISTRY_USER
andREGISTRY_PASS
as environment variables in the build environment:
CircleCI:
# _config-examples/single-stage/circle.yml
version: 2.1
jobs:
build:
machine:
image: ubuntu-2004:202010-01
environment:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
steps:
- checkout
- run:
name: Log in to docker hub
command: docker login -u $REGISTRY_USER -p $REGISTRY_PASS
- run:
name: Build from dockerfile
command: |
docker build \
--cache-from $CACHE_IMAGE:latest \
--tag $CACHE_IMAGE:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- run:
name: Push to docker hub
command: docker push $CACHE_IMAGE:latest
GitLab CI:
# _config-examples/single-stage/.gitlab-ci.yml
image: docker:stable
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
stages:
- build
docker-build:
stage: build
before_script:
- docker login -u $REGISTRY_USER -p $REGISTRY_PASS
script:
- docker build
--cache-from $CACHE_IMAGE:latest
--tag $CACHE_IMAGE:latest
--file ./Dockerfile
--build-arg BUILDKIT_INLINE_CACHE=1
"."
after_script:
- docker push $CACHE_IMAGE:latest
GitHub Actions:
# _config-examples/single-stage/github.yml
name: Docker Build
on: [push]
env:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
jobs:
build:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Log in to docker hub
run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASS }}
- name: Build from dockerfile
run: |
docker build \
--cache-from $CACHE_IMAGE:latest \
--tag $CACHE_IMAGE:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- name: Push to docker hub
run: docker push $CACHE_IMAGE:latest
If you're using Docker Compose, you can add the cache_from
option to the compose file, which maps back to the docker build --cache-from <image>
command when you run docker-compose build
.
Example:
version: '3.8'
services:
web:
build:
context: .
cache_from:
- mjhea0/docker-ci-cache:latest
image: mjhea0/docker-ci-cache:latest
To take advantage of BuildKit, make sure you're using a version of Docker Compose >= 1.25.0. To enable BuildKit, set the DOCKER_BUILDKIT
and COMPOSE_DOCKER_CLI_BUILD
environment variables to 1
. Then, again, to turn on the inline layer caching, use the BUILDKIT_INLINE_CACHE
build argument.
CircleCI:
# _config-examples/single-stage/compose/circle.yml
version: 2.1
jobs:
build:
machine:
image: ubuntu-2004:202010-01
environment:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
steps:
- checkout
- run:
name: Log in to docker hub
command: docker login -u $REGISTRY_USER -p $REGISTRY_PASS
- run:
name: Build images
command: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1
- run:
name: Push to docker hub
command: docker push $CACHE_IMAGE:latest
GitLab CI:
# _config-examples/single-stage/compose/.gitlab-ci.yml
image: docker/compose:latest
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
stages:
- build
docker-build:
stage: build
before_script:
- docker login -u $REGISTRY_USER -p $REGISTRY_PASS
script:
- docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1
after_script:
- docker push $CACHE_IMAGE:latest
GitHub Actions:
# _config-examples/single-stage/compose/github.yml
name: Docker Build
on: [push]
env:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
jobs:
build:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Log in to docker hub
run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASS }}
- name: Build Docker images
run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1
- name: Push to docker hub
run: docker push $CACHE_IMAGE:latest
With the multi-stage build pattern, you'll have to apply the same workflow (build, then push) for each intermediate stage since those images are discarded before the final image is created. The --target
option can be used to build each stage of the multi-stage build separately.
Dockerfile.multi:
# base
FROM python:3.9.7 as base
COPY ./requirements.txt /
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r requirements.txt
# stage
FROM python:3.9.7-slim
RUN apt-get update && \
apt-get -y install netcat && \
apt-get clean
WORKDIR /usr/src/app
COPY --from=base /wheels /wheels
COPY --from=base requirements.txt .
RUN pip install --no-cache /wheels/*
COPY . /usr/src/app
CMD gunicorn -b 0.0.0.0:5000 manage:app
CircleCI:
# _config-examples/multi-stage/circle.yml
version: 2.1
jobs:
build:
machine:
image: ubuntu-2004:202010-01
environment:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
steps:
- checkout
- run:
name: Log in to docker hub
command: docker login -u $REGISTRY_USER -p $REGISTRY_PASS
- run:
name: Build base from dockerfile
command: |
docker build \
--target base \
--cache-from $CACHE_IMAGE:base \
--tag $CACHE_IMAGE:base \
--file ./Dockerfile.multi \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- run:
name: Build stage from dockerfile
command: |
docker build \
--cache-from $CACHE_IMAGE:base \
--cache-from $CACHE_IMAGE:stage \
--tag $CACHE_IMAGE:stage \
--file ./Dockerfile.multi \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- run:
name: Push base image to docker hub
command: docker push $CACHE_IMAGE:base
- run:
name: Push stage image to docker hub
command: docker push $CACHE_IMAGE:stage
GitLab CI:
# _config-examples/multi-stage/.gitlab-ci.yml
image: docker:stable
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
stages:
- build
docker-build:
stage: build
before_script:
- docker login -u $REGISTRY_USER -p $REGISTRY_PASS
script:
- docker build
--target base
--cache-from $CACHE_IMAGE:base
--tag $CACHE_IMAGE:base
--file ./Dockerfile.multi
--build-arg BUILDKIT_INLINE_CACHE=1
"."
- docker build
--cache-from $CACHE_IMAGE:base
--cache-from $CACHE_IMAGE:stage
--tag $CACHE_IMAGE:stage
--file ./Dockerfile.multi
--build-arg BUILDKIT_INLINE_CACHE=1
"."
after_script:
- docker push $CACHE_IMAGE:stage
GitHub Actions:
# _config-examples/multi-stage/github.yml
name: Docker Build
on: [push]
env:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
jobs:
build:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Log in to docker hub
run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASS }}
- name: Build base from dockerfile
run: |
docker build \
--target base \
--cache-from $CACHE_IMAGE:base \
--tag $CACHE_IMAGE:base \
--file ./Dockerfile.multi \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- name: Build stage from dockerfile
run: |
docker build \
--cache-from $CACHE_IMAGE:base \
--cache-from $CACHE_IMAGE:stage \
--tag $CACHE_IMAGE:stage \
--file ./Dockerfile.multi \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- name: Push base image to docker hub
run: docker push $CACHE_IMAGE:base
- name: Push stage image to docker hub
run: docker push $CACHE_IMAGE:stage
Example compose file:
version: '3.8'
services:
web:
build:
context: .
cache_from:
- mjhea0/docker-ci-cache:stage
image: mjhea0/docker-ci-cache:stage
CircleCI:
# _config-examples/multi-stage/compose/circle.yml
version: 2.1
jobs:
build:
machine:
image: ubuntu-2004:202010-01
environment:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
steps:
- checkout
- run:
name: Log in to docker hub
command: docker login -u $REGISTRY_USER -p $REGISTRY_PASS
- run:
name: Build base from dockerfile
command: |
docker build \
--target base \
--cache-from $CACHE_IMAGE:base \
--tag $CACHE_IMAGE:base \
--file ./Dockerfile.multi \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- run:
name: Build Docker images
command: docker-compose -f docker-compose.multi.yml build --build-arg BUILDKIT_INLINE_CACHE=1
- run:
name: Push base image to docker hub
command: docker push $CACHE_IMAGE:base
- run:
name: Push stage image to docker hub
command: docker push $CACHE_IMAGE:stage
GitLab CI:
# _config-examples/multi-stage/compose/.gitlab-ci.yml
image: docker/compose:latest
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
stages:
- build
docker-build:
stage: build
before_script:
- docker login -u $REGISTRY_USER -p $REGISTRY_PASS
script:
- docker build
--target base
--cache-from $CACHE_IMAGE:base
--tag $CACHE_IMAGE:base
--file ./Dockerfile.multi
--build-arg BUILDKIT_INLINE_CACHE=1
"."
- docker-compose -f docker-compose.multi.yml build --build-arg BUILDKIT_INLINE_CACHE=1
after_script:
- docker push $CACHE_IMAGE:base
- docker push $CACHE_IMAGE:stage
GitHub Actions:
# _config-examples/multi-stage/compose/github.yml
name: Docker Build
on: [push]
env:
CACHE_IMAGE: mjhea0/docker-ci-cache
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
jobs:
build:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Log in to docker hub
run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASS }}
- name: Build base from dockerfile
run: |
docker build \
--target base \
--cache-from $CACHE_IMAGE:base \
--tag $CACHE_IMAGE:base \
--file ./Dockerfile.multi \
--build-arg BUILDKIT_INLINE_CACHE=1 \
"."
- name: Build images
run: docker-compose -f docker-compose.multi.yml build --build-arg BUILDKIT_INLINE_CACHE=1
- name: Push base image to docker hub
run: docker push $CACHE_IMAGE:base
- name: Push stage image to docker hub
run: docker push $CACHE_IMAGE:stage
The caching strategies outlined in this article should work well for single-stage builds and multi-stage builds with two or three stages.
Each stage added to a build step requires a new build and push along with the addition of the --cache-from
options for each parent stage. Thus, each new stage will add more clutter, making the CI file increasingly more difficult to read. Fortunately, BuildKit supports multi-stage builds with Docker layer caching built using a single stage. Review the following articles for more info on such advanced BuildKit patterns:
Finally, it's important to note that while caching may speed up your CI builds, you should re-build your images without cache from time to time in order to download the latest OS patches and security updates. For more on this, review this thread.
--
The code can be found in the docker-ci-cache repo:
Cheers!
Original article source at: https://testdriven.io/
1666942140
The elegant way to write iOS / macOS REST clients
Drastically simplifies app code by providing a client-side cache of observable models for RESTful resources.
swift-*
branches for legacy support)Want your app to talk to a remote API? Welcome to your state nightmare!
You need to display response data whenever it arrives. Unless the requesting screen is no longer visible. Unless some other currently visible bit of UI happens to need the same data. Or is about to need it.
You should show a loading indicator (but watch out for race conditions that leave it stuck spinning forever), display user-friendly errors (but not redundantly — no modal alert dogpiles!), give users a retry mechanism … and hide all of that when a subsequent request succeeds.
Be sure to avoid redundant requests — and redundant response deserialization. Deserialization should be on a background thread, of course. Oh, and remember not to retain your ViewController / model / helper thingy by accident in your callback closures. Unless you’re supposed to.
Naturally you’ll want to rewrite all of this from scratch in a slightly different ad hoc way for every project you create.
What could possibly go wrong?
Siesta ends this headache by providing a resource-centric alternative to the familiar request-centric approach.
Siesta provides an app-wide observable model of a RESTful resource’s state. This model answers three basic questions:
…and broadcasts notifications whenever the answers to these questions change.
Siesta handles all the transitions and corner cases to deliver these answers wrapped up with a pretty bow on top, letting you focus on your logic and UI.
URLSession
by default, or Alamofire, or inject your own custom adapter).This project started as helper code we wrote out of practical need on several Bust Out Solutions projects. When we found ourselves copying the code between projects, we knew it was time to open source it.
For the open source transition, we took the time to rewrite our code in Swift — and rethink it in Swift, embracing the language to make the API as clean as the concepts.
Siesta’s code is therefore both old and new: battle-tested on the App Store, then reincarnated in a Swifty green field.
Make the default thing the right thing most of the time.
Make the right thing easy all of the time.
Build from need. Don’t invent solutions in search of problems.
Design the API with these goals:
…in that order of priority.
Siesta requires Swift 5.3+ and Xcode 12+. (Use the swift-*
branches branches if you are still on an older version.)
In Xcode:
https://github.com/bustoutsolutions/siesta
in the URL field and click Next.Please note that Xcode will show all of Siesta’s optional and test-only dependencies, including Quick, Nimble, and Alamofire. Don’t worry: these won’t actually be bundled into your app (except Alamofire, if you use it).
In your Podfile
:
pod 'Siesta', '~> 1.0'
If you want to use the UI helpers:
pod 'Siesta/UI', '~> 1.0'
If you want to use Alamofire as your networking provider instead of Foundation’s URLSession
:
pod 'Siesta/Alamofire', '~> 1.0'
(You’ll also need to pass an Alamofire.Manager
when you configure your Siesta.Service
. See the API docs for more info.)
In your Cartfile
:
github "bustoutsolutions/siesta" ~> 1.0
Follow the Carthage instructions to add Siesta.framework
to your project. If you want to use the UI helpers, you will also need to add SiestaUI.framework
to your project as well.
As of this writing, there is one additional step you need to follow that isn’t in the Carthage docs:
$(PROJECT_DIR)/Carthage/Build/iOS/
(In-depth discussion of Carthage in recent Xcode versions is here.)
The code in Extensions/
is not part of the Siesta.framework
that Carthage builds. (This includes optional integrations for other libraries, such as Alamofire.) You will need to include those source files in your project manually if you want to use them.
Clone Siesta as a submodule into the directory of your choice, in this case Libraries/Siesta:
git submodule add https://github.com/bustoutsolutions/siesta.git Libraries/Siesta
git submodule update --init
Drag Siesta.xcodeproj
into your project tree as a subproject.
Under your project's Build Phases, expand Target Dependencies. Click the + button and add Siesta.
Expand the Link Binary With Libraries phase. Click the + button and add Siesta.
Click the + button in the top left corner to add a Copy Files build phase. Set the directory to Frameworks. Click the + button and add Siesta.
If you want to use the UI helpers, you will need to repeat steps 3–5 for SiestaUI
.
Please let us know about it, even if you eventually figure it out. Knowing where people get stuck will help improve these instructions!
Make a shared service instance for the REST API you want to use:
let MyAPI = Service(baseURL: "https://api.example.com")
Now register your view controller — or view, internal glue class, reactive signal/sequence, anything you like — to receive notifications whenever a particular resource’s state changes:
override func viewDidLoad() {
super.viewDidLoad()
MyAPI.resource("/profile").addObserver(self)
}
Use those notifications to populate your UI:
func resourceChanged(_ resource: Resource, event: ResourceEvent) {
nameLabel.text = resource.jsonDict["name"] as? String
colorLabel.text = resource.jsonDict["favoriteColor"] as? String
errorLabel.text = resource.latestError?.userMessage
}
Or if you don’t like delegates, Siesta supports closure observers:
MyAPI.resource("/profile").addObserver(owner: self) {
[weak self] resource, _ in
self?.nameLabel.text = resource.jsonDict["name"] as? String
self?.colorLabel.text = resource.jsonDict["favoriteColor"] as? String
self?.errorLabel.text = resource.latestError?.userMessage
}
Note that no actual JSON parsing occurs when we invoke jsonDict
. The JSON has already been parsed off the main thread, in a GCD queue — and unlike other frameworks, it is only parsed once no matter how many observers there are.
Of course, you probably don’t want to work with raw JSON in all your controllers. You can configure Siesta to automatically turn raw responses into models:
MyAPI.configureTransformer("/profile") { // Path supports wildcards
UserProfile(json: $0.content) // Create models however you like
}
…and now your observers see models instead of JSON:
MyAPI.resource("/profile").addObserver(owner: self) {
[weak self] resource, _ in
self?.showProfile(resource.typedContent()) // Response now contains UserProfile instead of JSON
}
func showProfile(profile: UserProfile?) {
...
}
Trigger a staleness-aware, redundant-request-suppressing load when the view appears:
override func viewWillAppear(_ animated: Bool) {
MyAPI.resource("/profile").loadIfNeeded()
}
…and you have a networked UI.
Add a loading indicator:
MyAPI.resource("/profile").addObserver(owner: self) {
[weak self] resource, event in
self?.activityIndicator.isHidden = !resource.isLoading
}
…or better yet, use Siesta’s prebaked ResourceStatusOverlay
view to get an activity indicator, a nicely formatted error message, and a retry button for free:
class ProfileViewController: UIViewController, ResourceObserver {
@IBOutlet weak var nameLabel, colorLabel: UILabel!
@IBOutlet weak var statusOverlay: ResourceStatusOverlay!
override func viewDidLoad() {
super.viewDidLoad()
MyAPI.resource("/profile")
.addObserver(self)
.addObserver(statusOverlay)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
MyAPI.resource("/profile").loadIfNeeded()
}
func resourceChanged(_ resource: Resource, event: ResourceEvent) {
nameLabel.text = resource.jsonDict["name"] as? String
colorLabel.text = resource.jsonDict["favoriteColor"] as? String
}
}
Note that this example is not toy code. Together with its storyboard, this small class is a fully armed and operational REST-backed user interface.
Take a look at AFNetworking’s venerable UIImageView
extension for asynchronously loading and caching remote images on demand. Seriously, go skim that code and digest all the cool things it does. Take a few minutes. I’ll wait. I’m a README. I’m not going anywhere.
Got it? Good.
Here’s how you implement the same functionality using Siesta:
class RemoteImageView: UIImageView {
static var imageCache: Service = Service()
var placeholderImage: UIImage?
var imageURL: URL? {
get { return imageResource?.url }
set { imageResource = RemoteImageView.imageCache.resource(absoluteURL: newValue) }
}
var imageResource: Resource? {
willSet {
imageResource?.removeObservers(ownedBy: self)
imageResource?.cancelLoadIfUnobserved(afterDelay: 0.05)
}
didSet {
imageResource?.loadIfNeeded()
imageResource?.addObserver(owner: self) { [weak self] _,_ in
self?.image = self?.imageResource?.typedContent(
ifNone: self?.placeholderImage)
}
}
}
}
A thumbnail of both versions, for your code comparing pleasure:
The same functionality. Yes, really.
(Well, OK, they’re not exactly identical. The Siesta version has more robust caching behavior, and will automatically update an image everywhere it is displayed if it’s refreshed.)
There’s a more featureful version of RemoteImageView
already included with Siesta — but the UI freebies aren’t the point. “Less code” isn’t even the point. The point is that Siesta gives you an elegant abstraction that solves problems you actually have, making your code simpler and less brittle.
Popular REST / networking frameworks have different primary goals:
Which one is right for your project? It depends on your needs and your tastes.
Siesta has robust functionality, but does not attempt to solve everything. In particular, Moya and RestKit address complementary / alternative concerns, while Alamofire and AFNetworking provide more robust low-level HTTP support. Further complicating a comparison, some frameworks are built on top of others. When you use Moya, for example, you’re also signing up for Alamofire. Siesta uses URLSession by default, but can also stack on top of Alamofire if you want to use its SSL trust management features. Combinations abound.
With all that in mind, here is a capabilities comparison¹:
Siesta | Alamofire | RestKit | Moya | AFNetworking | URLSession | |
---|---|---|---|---|---|---|
HTTP requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Async response callbacks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Observable in-memory cache | ✓ | |||||
Prevents redundant requests | ✓ | |||||
Prevents redundant parsing | ✓ | |||||
Parsing for common formats | ✓ | ✓ | ✓ | |||
Route-based parsing | ✓ | ✓ | ||||
Content-type-based parsing | ✓ | |||||
File upload/download tasks | ✓ | ~ | ✓ | ✓ | ||
Object model mapping | ✓ | |||||
Core data integration | ✓ | |||||
Hides HTTP | ✓ | |||||
UI helpers | ✓ | ✓ | ||||
Primary language | Swift | Swift | Obj-C | Swift | Obj-C | Obj-C |
Nontrivial lines of code² | 2609 | 3980 | 13220 | 1178 | 3936 | ? |
Built on top of | any (injectable) | URLSession | AFNetworking | Alamofire | NSURLSession / NSURLConnection | Apple guts |
1. Disclaimer: table compiled by Siesta’s non-omniscient author. Corrections / additions? Please submit a PR.
2. “Trivial” means lines containing only whitespace, comments, parens, semicolons, and braces.
Despite this capabilities list, Siesta is a relatively lean codebase — smaller than Alamofire, and 5.5x lighter than RestKit.
It’s not just the features. Siesta solves a different problem than other REST frameworks.
Other frameworks essentially view HTTP as a form of RPC. New information arrives only in responses that are coupled to requests — the return values of asynchronous functions.
Siesta puts the the “ST” back in “REST”, embracing the notion of state transfer as an architectural principle, and decoupling the act of observing state from the act of transferring it.
If that approach sounds appealing, give Siesta a try.
This repo includes a simple example project. To download the example project, install its dependencies, and run it locally:
pod try Siesta
(Note that there’s no need to download/clone Siesta locally first; this command does that for you.)To ask for help, please post a question on Stack Overflow and tag it with siesta-swift
. (Be sure to include that tag. It triggers a notification to the Siesta core team.) This is preferable to filing an issue because other people may have the same question as you, and Stack Overflow answers are more discoverable than closed issues.
Things that belong on Stack Overflow:
For a bug, feature request, or cool idea, please file a Github issue. Things that belong in Github issues:
Unsure which to choose? If you’re proposing a change to Siesta, use Github issues. If you’re asking a question that doesn’t change the project, and thus will remain valid even after you get an answer, then use Stack Overflow.
Keep in mind that Siesta is maintained by volunteers. Please be patient if you don’t immediately get an answer to your question; we all have jobs, families, obligations, and lives beyond this project.
Please be excellent to one another and follow our code of conduct.
Author: Bustoutsolutions
Source Code: https://github.com/bustoutsolutions/siesta
License: MIT license
1664346550
Superagent with flexible built-in caching.
Now compatible with superagent 2.x
and 3.x
.
Note: superagent-cache is a global patch for superagent. If you prefer the same built-in caching as a superagent plugin, see superagent-cache-plugin.
Upgrading from an older version or seeing a bug? Please see the Breaking Change History section.
Basic Usage
Require and instantiate superagent-cache as follows to get the default configuration:
var superagent = require('superagent');
require('superagent-cache')(superagent);
Now you're ready for the magic! All of your existing GET
and HEAD
requests will be cached with no extra bloat in your queries! Any matching DELETE
, POST
, PUT
, or PATCH
requests will automatically invalidate the associated cache key and value.
superagent
.get(uri)
.end(function (err, response){
// response is now cached!
// subsequent calls to this superagent request will now fetch the cached response
}
);
If you are using import
syntax, setup is as follows:
import superagent from 'superagent';
import superagentCache from 'superagent-cache';
superagentCache(superagent);
Enjoy!
Install
npm install superagent-cache --save
Run Tests
npm test
How Does it Work?
superagent-cache
patches superagent
so that it can evaluate HTTP
calls you make. Whenever a GET
or HEAD
request is made, superagent-cache
generates a cache key by stringifying four properties:
nameSpace
attribute (defaults to undefined
if the property is not set)With the generated cache key, superagent-cache
then checks its internal cache instance (which you have full power to configure). If the key exists, superagent-cache
returns it without performing the HTTP
request and if the key does not exist, it makes the request, caches the response
object (mostly), and returns it.
What Exactly Gets Cached?
If you don't use the .prune()
or .responseProp()
chainables detailed in the API, then superagent-cache
will cache a gutted version of the response
object. There are two reasons it doesn't just cache the entire response
object:
superagent-cache
takes all of the following properties from the response
object and clones each of them into a new object which then gets cached:
If you find yourself occasionally needing more than this, try out the .prune()
or .responseProp()
chainables. If your find yourself consistently needing more than this, make a pull request that adds the properties you need.
Where does superagent-cache store data?
By default, superagent-cache
stores data in a bundled instance of cacheModule, but it can natively handle any cache that matches cache-service's API. See this list of supported caches to see what works best with your use case. Because cache-service
and all of the supported caches have identical APIs, superagent-cache
doesn't care which you use, so pick the one that's best for you or make a new one.
What Does the Default Configuration Give Me?
You get the 'default configuration' when you only provide a superagent instance to the require('superagent-cache')()
command. This will patch the passed instance of superagent
and bundle an instance of cacheModule for storing data. cacheModule
is a slim, in-memory cache.
How Do I Use a Custom Configuration?
To use a custom configuraiton, take advantage of the the two optional params you can hand to superagent-cache
's require
command (cache
, and defaults
) as follows:
//Require superagent and the cache module I want
var superagent = require('superagent');
var redisModule = require('cache-service-redis');
var redisCache = new redisModule({redisEnv: 'REDISCLOUD_URL'});
var defaults = {cacheWhenEmpty: false, expiration: 900};
//Patch my superagent instance and pass in my redis cache
require('superagent-cache')(superagent, redisCache, defaults);
This example patches your instance of superagent
as allowing you to pass in your own, pre-instantiated cache and some defaults for superagent-cache to use with all queries. Here's a list of supported caches.
The cache
param can be either a pre-instantiated cache module, or a cacheModuleConfig
object to be used with superagent-cache's bundled cacheModule
instance.
All data passed in the defaults
object will apply to all queries made with superagent-cache unless overwritten with chainables. See the Available Configuration Options section for a list of all options you can pass.
For more information on require
command params usage, see this section.
Available Configuration Options
All options that can be passed to the defaults
require
param can be overwritten with chainables of the same name. All of the below options are detailed in the API section.
Supported Caches
A tiered caching solution capable of wrapping any number of the below supported caches. Available on NPM.
A redis wrapper for cache-service or standalone use. Available on NPM.
A memcached wrapper for cache-service or standalone use. Available on NPM.
An in-memory cache wrapper for cache-service or standalone use. Available on NPM.
A super-light in-memory cache for cache-service or standalone use. (This module is bundled with superagent-cache
and provided in the default configuration if you do not provide a cache
require
param.) Available on NPM.
API
superagent
is required, but cache
and defaults
are optional.
cache-service
API or a cacheModuleConfig
object to be used with superagent-cache's bundled instance of cacheModule
Same as superagent except that superagent's response object will be cached.
Same as superagent except that the generated cache key will be automatically invalidated when these HTTP
verbs are used.
Same as superagent except it optionally exposes the key superagent-cache generates as the third param in the callback's argument list. See the usage example for a more detailed explanation.
Caution: if you use this function,
supergent-cache
will not gut theresponse
object for you. Be sure that the result of your.responseProp()
call will never be circular and is not larger than it needs to be. Consider using.prune()
if you need to dig several layers into theresponse
object.
If you know you want a single, top-level property from superagent's response object, you can optimize what you cache by passing the property's name here. When used, it causes the .end()
function's response to return superagent's response[prop].
//response will now be replaced with superagent's response.body
//but all other top-level response properties, such as response.ok and response.status, will be ommitted
superagent
.get(uri)
.responseProp('body')
.end(function (error, response){
// handle response
}
);
Caution: if you use this function,
supergent-cache
will not automatically gut theresponse
object for you (although you can use thegutResponse
param to do so manually--more on that below). Be sure that the result of your.prune()
callback function will never be circular and is not larger than it needs to be.
If you need to dig several layers into superagent's response, you can do so by passing a function to .prune()
. Your prune function will receive superagent's response and should return a truthy value or null
. The benefit of using this function is that you can cache only what you need.
Your prune function will optionally receive gutResponse
as its second param. This is the method that superagent-cache
calls internally to simplify responses. If, based on some processing within the function you pass to .prune
, you decide that no custom return value is necessary, or you would like to take advantage of the default behavior plus some of your own, you can call gutResponse(response)
to get the value that superagent-cache
would have returned without a call to .prune
.
var prune = function(r){
return (r && r.ok && r.body && r.body.user) ? r.body.user : null;
}
//response will now be replaced with r.body.user or null
//and only r.body.user will be cached rather than the entire superagent response
superagent
.get(uri)
.prune(prune)
.end(function (error, response){
// handle response
}
);
gutResponse
var prune = function(r, gutResponse){
var output = gutResponse(r);
output.customValue = getCustomValue();
return output;
}
//response will now be superagent-cache-plugin's default output plus `customValue`
//all of which will be cached rather than the entire superagent response
superagent
.get(uri)
.prune(prune)
.end(function (error, response){
// handle response
}
);
In the event that you need certain query params to execute a query but cannot have those params as part of your cache key (useful when security or time-related params are sent), use .pruneQuery()
to remove those properties. Pass .pruneQuery()
an array containing the param keys you want omitted from the cache key.
//the superagent query will be executed with all params
//but the key used to store the superagent response will be generated without the passed param keys
superagent
.get(uri)
.query(query)
.pruneQuery(['token'])
.end(function (error, response){
// handle response
}
);
This function works just like the .pruneQuery()
funciton except that it modifies the arguments passed to the .set()
chainable method (headers) rather than those passed to the .query()
chainable method.
//the superagent query will be executed with all headers
//but the key used to store the superagent response will be generated without the passed header keys
superagent
.get(uri)
.set(options)
.pruneHeader(['token'])
.end(function (error, response){
// handle response
}
);
Use this function when you need to override your cache
's defaultExpiration
property for a particular cache entry.
Tell superagent-cache
whether to cache the response object when it's false
, null
, or {}
.This is especially useful when using .responseProp()
or .prune()
which can cause response
to be falsy. By default, cacheWhenEmpty
is true
.
Tell superagent-cache whether to perform an ajax call if the generated cache key is not found. By default, doQuery is true.
Tells superagent-cache to perform an ajax call regardless of whether the generated cache key is found. By default, forceUpdate is false.
If you're considering using this feature, please first consider whether you can instead consolidate duplicate requests into a single service layer and get data into the necessary spots via eventing or data binding.
I added this feature to prevent duplicate calls when dynamically prefetching content based on user interactions. In my use case, I prefetch content when users hover over a link so that when they click the link it's already in the cache. In the event that the user clicks the link before the prefetch AJAX call has completed, I wanted to prevent a second network call from occurring.
When activated, superagent-cache will keep track of all pending AJAX calls. If a call is attempted while an identical call is already pending, the duplicate call will not be made. When the original AJAX call returns, its response will be used to respond to all duplicate calls.
See the Using Background Refresh section for more information.
Tell the underlying cache
provided in the require
command to enable background refresh for the generated key and value. If a function is provided, it will use the function, if a boolean is provided, it will use the boolean, if nothing is provided, it will default to true.
This is a convenience method that allows you to skip all caching logic and use superagent as normal.
This is the second constructor param you handed in when you instantiated superagent-cache
. If you didn't provide one, then it's an instance of cacheModule
. You can assign it or call functions on it at runtime.
superagent.cache... //You can call any function existing on the cache you passed in
This is the third constructor param you handed in when you instantiated superagent-cache
. If you didn't provide one, then it uses the internal defaults. You can assign it or update it at runtime.
superagent.defaults... //You can call any function existing on the cache you passed in
Using Background Refresh
With a typical cache setup, you're left to find the perfect compromise between having a long expiration so that users don't have to suffer through the worst case load time, and a short expiration so data doesn't get stale. superagent-cache
eliminates the need to worry about users suffering through the longest wait time by automatically refreshing keys for you.
By default, background refresh is off. It will turn itself on the first time you use the .backgroundRefresh()
chainable.
superagent-cache
relies on the background refresh feature of the cache
param you pass into the require
command. When you use the .backgroundRefresh()
chainable, superagent-cache
passes the provided value into cache
. This means that if you're using cache-service
, you almost certainly want cache-service
's writeToVolatileCaches
property set to true
(it defaults to true
) so that the data set by background refresh will propogate forward to earlier caches (cache-service
ONLY background refreshses the final cache passed to it).
If desired, configure the following properties within cache
:
backgroundRefreshInterval
backgroundRefreshMinTtl
backgroundRefreshIntervalCheck
Background refresh is exposed via the .backgroundRefresh()
chainable.
When true
or no param is passed to .backgroundRefresh()
, it will generate a superagent
call identical to the one that triggered it and pass that to cache
.
superagent
.get(uri)
.backgroundRefresh()
.end(function (err, response){
//Response will now be refreshed in the background
}
);
When a function is passed, it will use that function. Read on for background refresh function requirements.
var refresh = function(key, cb){
var response = goGetData();
cb(null, response);
}
superagent
.get(uri)
.backgroundRefresh(refresh)
.end(function (err, response){
//Response will now be refreshed in the background
}
);
When false
is passed, it will do nothing.
refresh(key, cb(err, response))
The refresh
param MUST be a function that accepts key
and a callback function that accepts err
and response
as follows:
var refresh = function(key, cb){
var response = goGetData();
cb(null, response);
}
More Usage Examples
As an optional parameter in the .end(cb)
callback argument list, superagent-cache can give you the key it generated for each query as follows:
superagent
.get(uri)
.end(function (err, response, key){
console.log('GENERATED KEY:', key);
}
);
This can be useful if you need external access to a cache key and for testing purposes.
However, you can only get it when you pass 3 params to the callback's argument list. The following rules will apply when listing arguments in the .end(cb)
callback argument list:
response
err
and response
err
, response
, and key
NOTE: You must pass your own superagent instance or superagent-cache will throw an error.
superagent
is passed//...it will patch the provided superagent and create a cacheModule instance (see 'default configuration')
var superagent = require('superagent');
require('superagent-cache')(superagent)
superagent
and cache
are passedExample 1
//...it will patched the provided superagent instance and consume cache as its data store
var superagent = require('superagent');
var redisModule = require('cache-service-redis');
var redisCache = new redisModule({redisEnv: 'REDISCLOUD_URL'});
var superagent = require('superagent-cache')(superagent, redisCache);
Example 2
//...it will patched the provided superagent instance and consume cache as its cacheModuleConfig for use with the bundled instance of cacheModule
var superagent = require('superagent');
var cacheModuleConfig = {storage: 'session', defaultExpiration: 60};
var superagent = require('superagent-cache')(superagent, cacheModuleConfig);
defaults
The defaults
object can be passed as the third param at any time. It does not affect the superagent
or cache
params. You can see a brief demo here and a list of all the options you can pass in the defaults
object here.
Breaking Change History
2.x
and 3.x
._end
is now ._superagentCache_originalEnd
to prevent future naming colisions.pruneParams
is now .pruneQuery
for clarity.pruneOptions
is now .pruneHeader
for clarityresolve
function passed to .then
no longer exposes the generated cache key like it did when using superagent ^1.3.0
with superagent-cache ^1.5.0
(but using .end
still does)In superagent 1.7.0
, the superagent team introduced some internal changes to how they handle headers. As a result, you must use superagent-cache 1.3.5
or later to be compatible with superagent 1.7.0
or later. All versions of superagent-cache (to this point) should be backwards compatible with all versions of superagent going back to at least version 1.1.0
. To be clear, this was no one's fault. However, I have reached out to the superagent team to see what I can do to help minimize internally breaking changes in the future.
If you're seeing other incompatibilities, please submit an issue.
A bug was introduced that broke superagent-cache's ability to cache while running on the client. This bug was fixed in 1.3.0
. The bug did not affect superagent-cache while running on node.
superagent-cache
is now more flexible, allowing usage of any cache that matches cache-service
's API. To make it lighter, then, the hard dependency on cache-service
was replaced with the much lighter cacheModule
. As a result, superagent-cache
can no longer construct a cache-service
instance for you. If you wish to use cache-service
, you must instantiate it externally and hand it in as cache
--the second param in the require
command.
Author: Jpodwys
Source Code: https://github.com/jpodwys/superagent-cache
License: MIT license
1663936812
In today's post we will learn about 4 Best Caching Rust You Must Know.
What is caching?
Caching is the process of storing copies of files in a cache, or temporary storage location, so that they can be accessed more quickly. Technically, a cache is any temporary storage location for copies of files or data, but the term is often used in reference to Internet technologies. Web browsers cache HTML files, JavaScript, and images in order to load websites more quickly, while DNS servers cache DNS records for faster lookups and CDN servers cache content to reduce latency.
Table of contents:
Memcached client library.
rust-memcache is a memcached client written in pure rust.
The crate is called memcache
and you can depend on it via cargo:
[dependencies] memcache = "*"
// create connection with to memcached server node:
let client = memcache::connect("memcache://127.0.0.1:12345?timeout=10&tcp_nodelay=true").unwrap();
// flush the database
client.flush().unwrap();
// set a string value
client.set("foo", "bar", 0).unwrap();
// retrieve from memcached:
let value: Option<String> = client.get("foo").unwrap();
assert_eq!(value, Some(String::from("bar")));
assert_eq!(value.unwrap(), "bar");
// prepend, append:
client.prepend("foo", "foo").unwrap();
client.append("foo", "baz").unwrap();
let value: String = client.get("foo").unwrap().unwrap();
assert_eq!(value, "foobarbaz");
// delete value:
client.delete("foo").unwrap();
// using counter:
client.set("counter", 40, 0).unwrap();
client.increment("counter", 2).unwrap();
let answer: i32 = client.get("counter").unwrap().unwrap();
assert_eq!(answer, 42);
If you have multiple memcached server, you can create the memcache::Client
struct with a vector of urls of them. Which server will be used to store and retrive is based on what the key is.
This library have a basic rule to do this with rust's builtin hash function, and also you can use your custom function to do this, for something like you can using a have more data on one server which have more memory quota, or cluster keys with their prefix, or using consitent hash for large memcached cluster.
let mut client = memcache::connect(vec!["memcache://127.0.0.1:12345", "memcache:///tmp/memcached.sock"]).unwrap();
client.hash_function = |key: &str| -> u64 {
// your custom hashing function here
return 1;
};
A high performance thread-safe memory-bound Rust cache.
Arc<RwLock<Cache<...>>
for concurrent code, you just need Cache<...>
or AsyncCache<...>
CacheBuilder
/AsyncCacheBuilder
values and you're off and running.[dependencies]
stretto = "0.7"
or
[dependencies]
stretto = { version = "0.7", features = ["sync"] }
[dependencies]
stretto = { version = "0.7", features = ["async"] }
[dependencies]
stretto = { version = "0.7", features = ["full"] }
use std::time::Duration;
use stretto::Cache;
fn main() {
let c = Cache::new(12960, 1e6 as i64).unwrap();
// set a value with a cost of 1
c.insert("a", "a", 1);
// set a value with a cost of 1 and ttl
c.insert_with_ttl("b", "b", 1, Duration::from_secs(3));
// wait for value to pass through buffers
c.wait().unwrap();
// when we get the value, we will get a ValueRef, which contains a RwLockReadGuard
// so when we finish use this value, we must release the ValueRef
let v = c.get(&"a").unwrap();
assert_eq!(v.value(), &"a");
v.release();
// lock will be auto released when out of scope
{
// when we get the value, we will get a ValueRef, which contains a RwLockWriteGuard
// so when we finish use this value, we must release the ValueRefMut
let mut v = c.get_mut(&"a").unwrap();
v.write("aa");
assert_eq!(v.value(), &"aa");
// release the value
}
// if you just want to do one operation
let v = c.get_mut(&"a").unwrap();
v.write_once("aaa");
let v = c.get(&"a").unwrap();
assert_eq!(v.value(), &"aaa");
v.release();
// clear the cache
c.clear().unwrap();
// wait all the operations are finished
c.wait().unwrap();
assert!(c.get(&"a").is_none());
}
Simple function caching/memoization.
cached
provides implementations of several caching structures as well as a handy macros for defining memoized functions.
Memoized functions defined using #[cached]
/#[once]
/#[io_cached]
/cached!
macros are thread-safe with the backing function-cache wrapped in a mutex/rwlock, or externally synchronized in the case of #[io_cached]
. By default, the function-cache is not locked for the duration of the function's execution, so initial (on an empty cache) concurrent calls of long-running functions with the same arguments will each execute fully and each overwrite the memoized value as they complete. This mirrors the behavior of Python's functools.lru_cache
. To synchronize the execution and caching of un-cached arguments, specify #[cached(sync_writes = true)]
/ #[once(sync_writes = true)]
(not supported by #[io_cached]
.
cached::stores
docs cache stores available.proc_macro
for more procedural macro examples.macros
for more declarative macro examples.Features
default
: Include proc_macro
and async
featuresproc_macro
: Include proc macrosasync
: Include support for async functions and async cache storesredis_store
: Include Redis cache storeredis_async_std
: Include async Redis support using async-std
and async-std
tls support, implies redis_store
and async
redis_tokio
: Include async Redis support using tokio
and tokio
tls support, implies redis_store
and async
wasm
: Enable WASM support. Note that this feature is incompatible with all Redis features (redis_store
, redis_async_std
, redis_tokio
)The procedural macros (#[cached]
, #[once]
, #[io_cached]
) offer more features, including async support. See the proc_macro
and macros
modules for more samples, and the examples
directory for runnable snippets.`
Any custom cache that implements cached::Cached
/cached::CachedAsync
can be used with the #[cached]
/#[once]
/cached!
macros in place of the built-ins. Any custom cache that implements cached::IOCached
/cached::IOCachedAsync
can be used with the #[io_cached]
macro.
The basic usage looks like:
use cached::proc_macro::cached;
/// Defines a function named `fib` that uses a cache implicitly named `FIB`.
/// By default, the cache will be the function's name in all caps.
/// The following line is equivalent to #[cached(name = "FIB", unbound)]
#[cached]
fn fib(n: u64) -> u64 {
if n == 0 || n == 1 { return n }
fib(n-1) + fib(n-2)
}
use std::thread::sleep;
use std::time::Duration;
use cached::proc_macro::cached;
use cached::SizedCache;
/// Use an explicit cache-type with a custom creation block and custom cache-key generating block
#[cached(
type = "SizedCache<String, usize>",
create = "{ SizedCache::with_size(100) }",
convert = r#"{ format!("{}{}", a, b) }"#
)]
fn keyed(a: &str, b: &str) -> usize {
let size = a.len() + b.len();
sleep(Duration::new(size as u64, 0));
size
}
Shared Compilation Cache, great for Rust compilation.
sccache is a ccache-like compiler caching tool. It is used as a compiler wrapper and avoids compilation when possible, storing cached results either on local disk or in one of several cloud storage backends.
sccache includes support for caching the compilation of C/C++ code, Rust, as well as NVIDIA's CUDA using nvcc.
sccache also provides icecream-style distributed compilation (automatic packaging of local toolchains) for all supported compilers (including Rust). The distributed compilation system includes several security features that icecream lacks such as authentication, transport layer encryption, and sandboxed compiler execution on build servers. See the distributed quickstart guide for more information.
There are prebuilt x86-64 binaries available for Windows, Linux (a portable binary compiled against musl), and macOS on the releases page. Several package managers also include sccache packages, you can install the latest release from source using cargo, or build directly from a source checkout.
On macOS sccache can be installed via Homebrew:
brew install sccache
On Windows, sccache can be installed via scoop:
scoop install sccache
If you have a Rust toolchain installed you can install sccache using cargo. Note that this will compile sccache from source which is fairly resource-intensive. For CI purposes you should use prebuilt binary packages.
cargo install sccache
Running sccache is like running ccache: prefix your compilation commands with it, like so:
sccache gcc -o foo.o -c foo.c
If you want to use sccache for caching Rust builds you can define build.rustc-wrapper
in the cargo configuration file. For example, you can set it globally in $HOME/.cargo/config.toml
by adding:
[build]
rustc-wrapper = "/path/to/sccache"
Note that you need to use cargo 1.40 or newer for this to work.
Alternatively you can use the environment variable RUSTC_WRAPPER
:
export RUSTC_WRAPPER=/path/to/sccache
cargo build
sccache supports gcc, clang, MSVC, rustc, NVCC, and Wind River's diab compiler.
If you don't specify otherwise, sccache will use a local disk cache.
sccache works using a client-server model, where the server runs locally on the same machine as the client. The client-server model allows the server to be more efficient by keeping some state in memory. The sccache command will spawn a server process if one is not already running, or you can run sccache --start-server
to start the background server process without performing any compilation.
You can run sccache --stop-server
to terminate the server. It will also terminate after (by default) 10 minutes of inactivity.
Running sccache --show-stats
will print a summary of cache statistics.
Some notes about using sccache
with Jenkins are here.
To use sccache with cmake, provide the following command line arguments to cmake 3.4 or newer:
-DCMAKE_C_COMPILER_LAUNCHER=sccache
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
To generate PDB files for debugging with MSVC, you can use the /Z7
option. Alternatively, the /Zi
option together with /Fd
can work if /Fd
names a different PDB file name for each object file created. Note that CMake sets /Zi
by default, so if you use CMake, you can use /Z7
by adding code like this in your CMakeLists.txt:
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}")
endif()
By default, sccache will fail your build if it fails to successfully communicate with its associated server. To have sccache instead gracefully failover to the local compiler without stopping, set the environment variable SCCACHE_IGNORE_SERVER_IO_ERROR=1
.
Thank you for following this article.
Caching Rust Builds
1661149380
Flutter library for fetching, caching and invalidating asynchronous data
How to do API calls in Flutter? Probably, majority would answer by using Dio.
But the real question would be, how to integrate API calls in Flutter arhitecture seamless? One way would be to use FutureBuilder or maybe Bloc like most of the community do.
The thing is, both FutureBuilder and Bloc have flaws.
For example, FutureBuilder is too simple. It does provide status of your query, but communicating with some FutureBuilder is impossible outside of the scope of the current screen.
Problem with Bloc is that will include so many boilerplate code for a simple API call but again, talking to the Bloc is impossible from other screens. Bloc should be number one choice for complicated workflows, but what if you don't have any complex business logic?
flutter_requery to the rescue!
Let's start with this simple example.
/// define data and cache key
final List<String> data = ["Hello"];
final String cacheKey = 'strKey';
/// simulates API call, therefore response is delayed
Future<List<String>> _getData() async {
await Future.delayed(Duration(seconds: 1));
return data;
}
/// later, used in the build method
Query<List<String>>(
cacheKey,
future: _getData,
builder: (context, response) {
if (response.error != null) {
return Text('Error');
}
if (response.loading) {
return Text('Loading...');
}
final children = response.data.map((str) => Text(str)).toList();
return ListView(
children: children
);
},
);
/// and later when you want to invalidate your data
void _onPress() async {
await Future.delayed(Duration(seconds: 1));
data.add("World");
queryCache.invalidateQueries(cacheKey);
}
And that's the gist of it. You can find complete example on pub.dev.
Every query needs a cache key.
Under this key your data will be stored in cache. It can be string, int or list of strings and ints.
/// good
const k1 = 'myKey';
const k2 = 1;
const k3 = ["data", 1]
/// bad
const k1 = 1.0;
const k2 = true;
const k3 = ["data", true];
Idea behind having keys specified as list is that you can invalidate your queries more intelligently.
Take a look at invalidation chapter for more details.
Once the cache key is defined, next step is to write the query.
Query takes 3 arguments:
BuildContext
followed by QueryResponse
object.QueryResponse manages query status. It also has 3 properties:
error
will be null.// use Query widget in the build method
Query<List<String>>(
'myCacheKey',
future: ()async {
await Future.delayed(Duration(seconds: 1));
return ["Hello"]
}
builder: (context, QueryResponse response) {
/// error state
if (response.error != null) {
return Text('Error');
}
/// loading state
if (response.loading) {
return Text('Loading...');
}
final children = response.data.map((str) => Text(str)).toList();
return ListView(
children: children
);
},
);
Data invalidation can come in two different forms.
You can either afford to wait for the API response or you simply need to show the newest data as soon as possible. If you are interested in following, check the next chapter.
Waiting for the API response is more common and flutter_requery supports this by using the queryCache
instance. It's global and already defined by the library. Invalidate your query by passing the cache keys.
// invalidates strKey query
queryCache.invalidateQueries('strKey');
// support for bulk invalidation
queryCache.invalidateQueries(['strKey1', 'strKey2']);
// if your keys are lists, end result would be similar to
queryCache.invalidateQueries([
['strKey', 1],
['strKey2', 2]
]);
Once query is invalidated, every Query
widget subscribed for that query will execute future
again and rebuild its children with the new data.
For cache-level invalidation use:
// invalidate every query stored in cache
queryCache.invalidateAll()
Invalidation works in pair with the keys defined as lists. Cache keys defined as list must be looked upon in a hierarchical manner where the list elements defined before are ancestors of the elements that come after. For example:
// requests is ancestor of 1
const key1 = ["requests", 1]
Reasoning behind this is to support hierarchical invalidation. Sometimes it can get cumbersome managing invalidations and therefore developer can decide to cleverly name keys to support this. For example:
const k1 = ["requests", 1]
const k2 = ["requests", 2]
const k3 = "requests"
// without hierarchical invalidation you need to call
queryCache.invalidateQueries([
["requests", 1], ["requests", 2], "requests"
]);
// but with hierarchical invalidation you only need to call
queryCache.invalidateQueries("requests");
Sometimes waiting period for the API response to be available is too long. Therefore you can immediately update the cache data and rebuild your widget tree by using the optimistic response. Make sure to remove await
keyword before the API call since this will block the thread.
queryCache.setOptimistic("requests", [...oldData, newData]);
In short, reset can be explained as cache-level invalidation without rebuilding the widget tree.
Also, async actions won't be ran immediately but only when the new Query
widget is mounted or the cacheKey
has changed. This is particularly useful for the log out action.
queryCache.reset();
Run this command:
With Flutter:
$ flutter pub add flutter_requery
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
flutter_requery: ^0.1.14
Alternatively, your editor might support flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:flutter_requery/flutter_requery.dart';
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_requery/flutter_requery.dart';
void main() {
runApp(App());
}
final List<String> data = ["Hello"];
class App extends StatelessWidget {
Future<List<String>> _getData() async {
await Future.delayed(Duration(seconds: 1));
return data;
}
Future<void> _addString() async {
await Future.delayed(Duration(seconds: 1));
data.add("World");
}
void _onPress() async {
// Call API and invalidate your query by using the same cache key
await _addString();
queryCache.invalidateQueries('strKey');
// Or if you don't want to wait and you are sure
// your API works use optimistic response.
// _addString();
// queryCache.setOptimistic('strKey', [...data, 'World']);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Flutter requery"),
),
floatingActionButton: FloatingActionButton(
child: Text("Add"),
onPressed: _onPress,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Query<List<String>>(
'strKey',
future: _getData,
builder: (context, response) {
if (response.error != null) {
return Text('Error');
}
if (response.loading) {
return CircularProgressIndicator();
}
return ListView(
children: response.data.map((str) => Text(str)).toList(),
);
},
),
),
),
);
}
}
API | Description |
---|---|
Query | Flutter widget used for data-fetching operations. |
queryCache.invalidateQueries | Invalidates query specified by cache key and rebuilds the widget tree. |
queryCache.invalidateAll | Invalidates every query stored in cache and rebuilds the widget tree. |
queryCache.setOptimistic | Set cache data manually and rebuild the widget tree. |
queryCache.reset | Invalidates every query stored in cache without rebuilding the widget tree. |
Author: CoreLine-agency
Source Code: https://github.com/CoreLine-agency/flutter_requery
License: MIT license
1660914240
Caching is typically the most effective way to boost an application's performance.
This article looks at how to do just that by first reviewing Django's caching framework as a whole and then detailing step-by-step how to cache a Django view.
Source: https://testdriven.io
1660907040
Bộ nhớ đệm thường là cách hiệu quả nhất để tăng hiệu suất của ứng dụng.
Đối với các trang web động, khi hiển thị một mẫu, bạn thường phải thu thập dữ liệu từ nhiều nguồn khác nhau (như cơ sở dữ liệu, hệ thống tệp và API của bên thứ ba, để đặt tên cho một số), xử lý dữ liệu và áp dụng logic nghiệp vụ cho nó trước khi phân phối nó cho khách hàng. Bất kỳ sự chậm trễ nào do độ trễ của mạng sẽ được người dùng cuối nhận thấy.
Ví dụ: giả sử bạn phải thực hiện một cuộc gọi HTTP tới một API bên ngoài để lấy dữ liệu cần thiết để hiển thị một mẫu. Ngay cả trong điều kiện hoàn hảo, điều này sẽ làm tăng thời gian hiển thị, điều này sẽ làm tăng thời gian tải tổng thể. Điều gì sẽ xảy ra nếu API gặp sự cố hoặc bạn có thể bị giới hạn tốc độ? Dù bằng cách nào, nếu dữ liệu không được cập nhật thường xuyên, bạn nên triển khai cơ chế lưu vào bộ nhớ đệm để tránh phải thực hiện lệnh gọi HTTP hoàn toàn cho mỗi yêu cầu của khách hàng.
Bài viết này xem xét cách thực hiện điều đó bằng cách xem xét toàn bộ khuôn khổ bộ nhớ đệm của Django trước tiên và sau đó trình bày chi tiết từng bước cách lưu vào bộ nhớ cache một dạng xem Django.
Sự phụ thuộc:
Đến cuối hướng dẫn này, bạn sẽ có thể:
Django đi kèm với một số phụ trợ bộ nhớ đệm tích hợp, cũng như hỗ trợ cho một phụ trợ tùy chỉnh .
Các tùy chọn tích hợp là:
Bộ nhớ đệm trong Django có thể được thực hiện ở các cấp độ khác nhau (hoặc các phần của trang web). Bạn có thể lưu vào bộ nhớ cache toàn bộ trang web hoặc các phần cụ thể với nhiều mức độ chi tiết khác nhau (được liệt kê theo thứ tự mức độ chi tiết giảm dần):
Đây là cách dễ nhất để triển khai bộ nhớ đệm trong Django. Để làm điều này, tất cả những gì bạn phải làm là thêm hai lớp phần mềm trung gian vào tệp settings.py của bạn :
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
Thứ tự của phần mềm trung gian là quan trọng ở đây.
UpdateCacheMiddleware
phải đến trướcFetchFromCacheMiddleware
. Để biết thêm thông tin, hãy xem Thứ tự của phần mềm MIDDLEWARE từ tài liệu Django.
Sau đó, bạn cần thêm các cài đặt sau:
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
Mặc dù bộ nhớ đệm toàn bộ trang web có thể là một lựa chọn tốt nếu trang web của bạn có ít hoặc không có nội dung động, nhưng nó có thể không thích hợp để sử dụng cho các trang web lớn với phần mềm phụ trợ bộ nhớ đệm dựa trên bộ nhớ vì RAM đắt.
Thay vì lãng phí không gian bộ nhớ quý giá vào bộ nhớ đệm các trang tĩnh hoặc trang động lấy dữ liệu từ một API thay đổi nhanh chóng, bạn có thể lưu vào bộ nhớ cache các chế độ xem cụ thể. Đây là cách tiếp cận mà chúng tôi sẽ sử dụng trong bài viết này. Đây cũng là cấp bộ nhớ đệm mà bạn hầu như luôn phải bắt đầu khi tìm cách triển khai bộ nhớ đệm trong ứng dụng Django của mình.
Bạn có thể triển khai loại bộ nhớ cache này với bộ trang trí cache_page trên hàm xem trực tiếp hoặc trong đường dẫn bên trong URLConf
:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
Bản thân bộ nhớ đệm dựa trên URL, vì vậy, các yêu cầu, ví dụ, object/1
và object/2
sẽ được lưu vào bộ nhớ cache riêng.
Cần lưu ý rằng việc triển khai bộ nhớ cache trực tiếp trên chế độ xem làm cho việc vô hiệu hóa bộ nhớ cache trong một số trường hợp nhất định trở nên khó khăn hơn. Ví dụ: điều gì sẽ xảy ra nếu bạn muốn cho phép một số người dùng nhất định truy cập vào chế độ xem mà không cần bộ nhớ cache? Kích hoạt bộ nhớ cache thông qua URLConf
cung cấp cơ hội liên kết một URL khác với chế độ xem không sử dụng bộ nhớ cache:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
Nếu các mẫu của bạn chứa các phần thường xuyên thay đổi dựa trên dữ liệu, bạn có thể muốn loại bỏ chúng khỏi bộ nhớ cache.
Ví dụ: có thể bạn sử dụng email của người dùng đã xác thực trong thanh điều hướng trong một khu vực của mẫu. Chà, Nếu bạn có hàng nghìn người dùng thì phân đoạn đó sẽ được nhân đôi hàng nghìn lần trong RAM, mỗi người dùng một lần. Đây là lúc bộ nhớ đệm phân đoạn mẫu phát huy tác dụng, cho phép bạn chỉ định các khu vực cụ thể của mẫu để lưu vào bộ nhớ đệm.
Để lưu vào bộ nhớ cache một danh sách các đối tượng:
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
Tại đây, {% load cache %}
cung cấp cho chúng tôi quyền truy cập vào cache
thẻ mẫu, thẻ này dự kiến thời gian chờ bộ nhớ cache tính bằng giây ( 500
) cùng với tên của phân đoạn bộ nhớ cache ( object_list
).
Đối với trường hợp các tùy chọn trước đó không cung cấp đủ chi tiết, bạn có thể sử dụng API cấp thấp để quản lý các đối tượng riêng lẻ trong bộ đệm bằng khóa bộ đệm.
Ví dụ:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
Trong ví dụ này, bạn sẽ muốn vô hiệu hóa (hoặc xóa) bộ đệm khi các đối tượng được thêm, thay đổi hoặc xóa khỏi cơ sở dữ liệu. Một cách để quản lý điều này là thông qua các tín hiệu cơ sở dữ liệu:
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
Để biết thêm về cách sử dụng tín hiệu cơ sở dữ liệu để làm mất hiệu lực bộ nhớ cache, hãy xem API bộ nhớ cache cấp thấp trong bài viết Django .
Cùng với đó, hãy xem một số ví dụ.
Sao chép dự án cơ sở từ repo cache-django-view , sau đó kiểm tra nhánh cơ sở:
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
Tạo (và kích hoạt) một môi trường ảo và cài đặt các yêu cầu:
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
Áp dụng di chuyển Django, sau đó khởi động máy chủ:
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
Điều hướng đến http://127.0.0.1:8000 trong trình duyệt bạn chọn để đảm bảo rằng mọi thứ hoạt động như mong đợi.
Bạn nên thấy:
Ghi lại thiết bị đầu cuối của bạn. Bạn sẽ thấy tổng thời gian thực hiện yêu cầu:
Total time: 2.23s
Số liệu này đến từ core / middleware.py :
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
Xem nhanh chế độ xem trong apicalls / views.py :
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Chế độ xem này thực hiện một cuộc gọi HTTP requests
tới httpbin.org . Để mô phỏng một yêu cầu dài, phản hồi từ API bị trì hoãn trong hai giây. Vì vậy, sẽ mất khoảng hai giây để http://127.0.0.1:8000 hiển thị không chỉ theo yêu cầu ban đầu mà còn cho từng yêu cầu tiếp theo. Mặc dù tải hai giây có phần chấp nhận được đối với yêu cầu ban đầu, nó hoàn toàn không được chấp nhận đối với các yêu cầu tiếp theo vì dữ liệu không thay đổi. Hãy khắc phục điều này bằng cách lưu vào bộ nhớ đệm toàn bộ chế độ xem bằng cách sử dụng mức bộ nhớ cache Per-view của Django.
Quy trình làm việc:
Trước khi thêm bộ nhớ cache, hãy nhanh chóng chạy kiểm tra tải để lấy đường cơ sở điểm chuẩn bằng Apache Bench , để biết sơ bộ về số lượng yêu cầu mà ứng dụng của chúng tôi có thể xử lý mỗi giây.
Apache Bench được cài đặt sẵn trên Mac.
Nếu bạn đang sử dụng hệ thống Linux, rất có thể nó đã được cài đặt và sẵn sàng hoạt động. Nếu không, bạn có thể cài đặt qua APT (
apt-get install apache2-utils
) hoặc YUM (yum install httpd-tools
).Người dùng Windows sẽ cần tải xuống và giải nén tệp nhị phân Apache.
Thêm Gunicorn vào tệp yêu cầu:
gunicorn==20.1.0
Giết máy chủ nhà phát triển Django và cài đặt Gunicorn:
(venv)$ pip install -r requirements.txt
Tiếp theo, cung cấp ứng dụng Django với Gunicorn (và bốn nhân viên ) như vậy:
(venv)$ gunicorn core.wsgi:application -w 4
Trong một cửa sổ đầu cuối mới, chạy Apache Bench:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Điều này sẽ mô phỏng 100 kết nối trên 10 luồng đồng thời. Đó là 100 yêu cầu, 10 yêu cầu cùng một lúc.
Ghi lại các yêu cầu mỗi giây:
Requests per second: 1.69 [#/sec] (mean)
Hãy nhớ rằng Thanh công cụ gỡ lỗi Django sẽ thêm một chút chi phí. Điểm chuẩn nói chung là khó có thể hoàn toàn đúng. Điều quan trọng là tính nhất quán. Chọn một chỉ số để tập trung vào và sử dụng cùng một môi trường cho mỗi bài kiểm tra.
Giết máy chủ Gunicorn và sao lưu máy chủ Django dev:
(venv)$ python manage.py runserver
Cùng với đó, hãy xem cách lưu vào bộ nhớ cache một chế độ xem.
Bắt đầu bằng cách trang trí ApiCalls
khung nhìn với trình @cache_page
trang trí như sau:
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Vì chúng ta đang sử dụng dạng xem dựa trên lớp, chúng ta không thể đặt trình trang trí trực tiếp trên lớp, vì vậy chúng ta đã sử dụng a method_decorator
và chỉ định dispatch
(làm phương thức được trang trí) cho đối số tên.
Bộ nhớ cache trong ví dụ này đặt thời gian chờ (hoặc TTL) là năm phút.
Ngoài ra, bạn có thể đặt điều này trong cài đặt của mình như sau:
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
Sau đó, quay lại chế độ xem:
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Tiếp theo, hãy thêm một phụ trợ bộ nhớ cache.
Memcached và Redis là các kho lưu trữ dữ liệu giá trị, khóa trong bộ nhớ. Chúng dễ sử dụng và được tối ưu hóa để tra cứu hiệu suất cao. Bạn có thể sẽ không thấy nhiều sự khác biệt về hiệu suất hoặc mức sử dụng bộ nhớ giữa hai thiết bị này. Điều đó nói rằng, Memcached dễ cấu hình hơn một chút vì nó được thiết kế để đơn giản và dễ sử dụng. Mặt khác, Redis có bộ tính năng phong phú hơn nên nó có nhiều trường hợp sử dụng ngoài bộ nhớ đệm. Ví dụ: nó thường được sử dụng để lưu trữ các phiên người dùng hoặc làm môi giới thông báo trong hệ thống pub / sub. Vì tính linh hoạt của nó, trừ khi bạn đã đầu tư vào Memcached, Redis là giải pháp tốt hơn nhiều.
Để biết thêm về điều này, hãy xem lại câu trả lời Stack Overflow này.
Tiếp theo, chọn kho lưu trữ dữ liệu của bạn và hãy xem cách lưu vào bộ nhớ cache một chế độ xem.
Tải xuống và cài đặt Redis.
Nếu bạn đang sử dụng máy Mac, chúng tôi khuyên bạn nên cài đặt Redis với Homebrew :
$ brew install redis
Sau khi được cài đặt, trong một cửa sổ đầu cuối mới, hãy khởi động máy chủ Redis và đảm bảo rằng nó đang chạy trên cổng mặc định của nó, 6379. Số cổng sẽ rất quan trọng khi chúng tôi cho Django biết cách giao tiếp với Redis.
$ redis-server
Để Django sử dụng Redis làm phụ trợ bộ nhớ cache, trước tiên chúng ta cần cài đặt django-redis .
Thêm nó vào tệp tin request.txt :
django-redis==5.0.0
Cài đặt:
(venv)$ pip install -r requirements.txt
Tiếp theo, thêm phần phụ trợ tùy chỉnh vào tệp settings.py :
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Bây giờ, khi bạn chạy lại máy chủ, Redis sẽ được sử dụng làm phụ trợ bộ nhớ cache:
(venv)$ python manage.py runserver
Khi máy chủ được thiết lập và đang chạy, hãy điều hướng đến http://127.0.0.1:8000 .
Yêu cầu đầu tiên vẫn sẽ mất khoảng hai giây. Làm mới trang. Trang sẽ tải gần như ngay lập tức. Hãy xem thời gian tải trong thiết bị đầu cuối của bạn. Nó phải gần bằng 0:
Total time: 0.01s
Tò mò dữ liệu được lưu trong bộ nhớ cache trông như thế nào bên trong Redis?
Chạy Redis CLI ở chế độ tương tác trong một cửa sổ đầu cuối mới:
$ redis-cli
Bạn nên thấy:
127.0.0.1:6379>
Chạy ping
để đảm bảo mọi thứ hoạt động bình thường:
127.0.0.1:6379> ping
PONG
Quay lại tệp cài đặt. Chúng tôi đã sử dụng cơ sở dữ liệu Redis số 1 'LOCATION': 'redis://127.0.0.1:6379/1',
:. Vì vậy, hãy chạy select 1
để chọn cơ sở dữ liệu đó và sau đó chạy keys *
để xem tất cả các khóa:
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Chúng ta có thể thấy rằng Django đã đưa vào một khóa tiêu đề và một cache_page
khóa chính.
Để xem dữ liệu thực được lưu trong bộ nhớ cache, hãy chạy get
lệnh với khóa làm đối số:
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Của bạn sẽ thấy một cái gì đó tương tự như:
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
Thoát CLI tương tác sau khi hoàn tất:
127.0.0.1:6379[1]> exit
Bỏ qua phần "Kiểm tra Hiệu suất".
Bắt đầu bằng cách thêm pymemcache vào tệp tin request.txt :
pymemcache==3.5.0
Cài đặt các phụ thuộc:
(venv)$ pip install -r requirements.txt
Tiếp theo, chúng ta cần cập nhật cài đặt trong core / settings.py để bật phụ trợ Memcached:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
Ở đây, chúng tôi đã thêm phần phụ trợ PyMemcacheCache và chỉ ra rằng Memcached sẽ chạy trên máy cục bộ của chúng tôi trên localhost (127.0.0.1) cổng 11211, là cổng mặc định cho Memcached.
Tiếp theo, chúng ta cần cài đặt và chạy trình nền Memcached. Cách dễ nhất để cài đặt nó, là thông qua trình quản lý gói như APT, YUM, Homebrew hoặc Chocolatey tùy thuộc vào hệ điều hành của bạn:
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
Sau đó, chạy nó trong một thiết bị đầu cuối khác trên cổng 11211:
$ memcached -p 11211
# test: telnet localhost 11211
Để biết thêm thông tin về cài đặt và cấu hình Memcached, hãy xem lại wiki chính thức .
Điều hướng lại đến http://127.0.0.1:8000 trong trình duyệt của chúng tôi. Yêu cầu đầu tiên vẫn sẽ mất đủ hai giây, nhưng tất cả các yêu cầu tiếp theo sẽ tận dụng bộ nhớ cache. Vì vậy, nếu bạn làm mới hoặc nhấn nút "Nhận dữ liệu mới", trang sẽ tải gần như ngay lập tức.
Thời gian thực hiện trong thiết bị đầu cuối của bạn như thế nào?
Total time: 0.03s
Nếu chúng ta xem xét thời gian cần để tải yêu cầu đầu tiên so với yêu cầu thứ hai (được lưu trong bộ nhớ cache) trong Thanh công cụ gỡ lỗi Django, nó sẽ giống như:
Cũng trong Thanh công cụ gỡ lỗi, bạn có thể xem các hoạt động trong bộ nhớ cache:
Quay Gunicorn sao lưu một lần nữa và chạy lại các bài kiểm tra hiệu suất:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Yêu cầu mới mỗi giây là gì? Đó là khoảng 36 trên máy của tôi!
Trong bài viết này, chúng tôi đã xem xét các tùy chọn tích hợp sẵn khác nhau cho bộ nhớ đệm trong Django cũng như các mức bộ nhớ đệm khác nhau có sẵn. Chúng tôi cũng trình bày chi tiết cách lưu vào bộ nhớ cache một chế độ xem bằng cách sử dụng bộ đệm ẩn Per-view của Django với cả Memcached và Redis.
Bạn có thể tìm thấy mã cuối cùng cho cả hai tùy chọn, Memcached và Redis, trong kho lưu trữ bộ nhớ cache-django-view .
--
Nói chung, bạn sẽ muốn xem xét bộ nhớ đệm khi kết xuất trang chậm do độ trễ mạng từ các truy vấn cơ sở dữ liệu hoặc cuộc gọi HTTP.
Từ đó, bạn nên sử dụng chương trình phụ trợ bộ nhớ cache Django tùy chỉnh với Redis với một Per-view
loại. Nếu bạn cần kiểm soát và chi tiết hơn, vì không phải tất cả dữ liệu trên mẫu đều giống nhau đối với tất cả người dùng hoặc các phần của dữ liệu thay đổi thường xuyên, thì hãy chuyển xuống bộ nhớ cache phân đoạn Mẫu hoặc API bộ nhớ cache cấp thấp.
Nguồn: https://testdriven.io
1660899660
Кэширование обычно является наиболее эффективным способом повышения производительности приложения.
Для динамических веб-сайтов при отображении шаблона вам часто приходится собирать данные из различных источников (например, базы данных, файловой системы и сторонних API), обрабатывать данные и применять бизнес-логику к перед подачей клиенту. Любая задержка из-за сетевой задержки будет замечена конечным пользователем.
Например, предположим, что вам нужно сделать HTTP-запрос к внешнему API, чтобы получить данные, необходимые для отображения шаблона. Даже в идеальных условиях это увеличит время рендеринга, что увеличит общее время загрузки. Что, если API выйдет из строя или, может быть, вы подпадаете под ограничение скорости? В любом случае, если данные обновляются нечасто, рекомендуется реализовать механизм кэширования, чтобы избежать необходимости выполнять вызов HTTP в целом для каждого клиентского запроса.
В этой статье мы рассмотрим, как это сделать, сначала рассмотрев структуру кэширования Django в целом, а затем подробно описав, как кэшировать представление Django.
Зависимости:
К концу этого урока вы должны уметь:
Django поставляется с несколькими встроенными бэкэндами для кэширования, а также поддерживает пользовательский бэкенд.
Встроенные опции:
Кэширование в Django может быть реализовано на разных уровнях (или частях сайта). Вы можете кэшировать весь сайт или его отдельные части с различными уровнями детализации (перечислены в порядке убывания детализации):
Это самый простой способ реализовать кеширование в Django. Для этого все, что вам нужно сделать, это добавить два класса промежуточного программного обеспечения в ваш файл settings.py :
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
Здесь важен порядок промежуточного программного обеспечения.
UpdateCacheMiddleware
должен прийти раньшеFetchFromCacheMiddleware
. Для получения дополнительной информации взгляните на Order of MIDDLEWARE из документации Django.
Затем вам нужно добавить следующие настройки:
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
Хотя кэширование всего сайта может быть хорошим вариантом, если на вашем сайте мало или совсем нет динамического контента, это может быть нецелесообразно для больших сайтов с кеш-сервером на основе памяти, поскольку оперативная память стоит дорого.
Вместо того, чтобы тратить драгоценное пространство памяти на кэширование статических страниц или динамических страниц, которые получают данные из быстро меняющегося API, вы можете кэшировать определенные представления. Именно этот подход мы будем использовать в этой статье. Это также уровень кэширования, с которого вы почти всегда должны начинать, когда хотите реализовать кэширование в своем приложении Django.
Вы можете реализовать этот тип кеша с помощью декоратора cache_page либо непосредственно в функции представления, либо по пути внутри URLConf
:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
Сам кеш основан на URL, поэтому запросы к, скажем, object/1
и object/2
будут кешироваться отдельно.
Стоит отметить, что реализация кеша непосредственно в представлении усложняет отключение кеша в определенных ситуациях. Например, что если вы хотите разрешить определенным пользователям доступ к представлению без кеша? Включение кеша через the URLConf
дает возможность связать другой URL с представлением, которое не использует кеш:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
Если ваши шаблоны содержат части, которые часто изменяются в зависимости от данных, вы, вероятно, захотите оставить их вне кеша.
Например, возможно, вы используете адрес электронной почты аутентифицированного пользователя на панели навигации в области шаблона. Что ж, если у вас есть тысячи пользователей, то этот фрагмент будет продублирован в оперативной памяти тысячи раз, по одному для каждого пользователя. Именно здесь вступает в действие кэширование фрагментов шаблона, которое позволяет указать определенные области шаблона для кэширования.
Чтобы кэшировать список объектов:
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
Здесь {% load cache %}
дает нам доступ к cache
тегу шаблона, который ожидает тайм-аут кеша в секундах ( 500
) вместе с именем фрагмента кеша ( object_list
).
В случаях, когда предыдущие варианты не обеспечивают достаточной детализации, вы можете использовать низкоуровневый API для управления отдельными объектами в кеше по ключу кеша.
Например:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
В этом примере вы захотите аннулировать (или удалить) кэш при добавлении, изменении или удалении объектов из базы данных. Одним из способов управления этим является использование сигналов базы данных:
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
Чтобы узнать больше об использовании сигналов базы данных для аннулирования кеша, ознакомьтесь со статьей Low-Level Cache API в статье Django.
При этом давайте рассмотрим несколько примеров.
Клонируйте базовый проект из репозитория cache-django-view , а затем проверьте базовую ветку:
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
Создайте (и активируйте) виртуальную среду и установите требования:
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
Примените миграции Django, а затем запустите сервер:
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
Перейдите по адресу http://127.0.0.1:8000 в выбранном вами браузере, чтобы убедиться, что все работает должным образом.
Тебе следует увидеть:
Обратите внимание на свой терминал. Вы должны увидеть общее время выполнения запроса:
Total time: 2.23s
Эта метрика взята из core/middleware.py :
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
Взгляните на представление в apicalls/views.py :
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Это представление выполняет HTTP-вызов requests
с httpbin.org . Для имитации длинного запроса ответ от API задерживается на две секунды. Таким образом, http://127.0.0.1:8000 должно занять около двух секунд , чтобы отобразить не только первоначальный запрос, но и каждый последующий запрос. В то время как двухсекундная загрузка в некоторой степени приемлема для первоначального запроса, она совершенно неприемлема для последующих запросов, поскольку данные не меняются. Давайте исправим это, кэшируя все представление, используя уровень кэширования Django для каждого представления.
Рабочий процесс:
Прежде чем добавлять кеш, давайте быстро запустим нагрузочный тест, чтобы получить базовый уровень производительности с помощью Apache Bench , чтобы получить приблизительное представление о том, сколько запросов наше приложение может обрабатывать в секунду.
Apache Bench предустановлен на Mac.
Если вы работаете в системе Linux, скорее всего, она уже установлена и готова к работе. Если нет, вы можете установить через APT (
apt-get install apache2-utils
) или YUM (yum install httpd-tools
).Пользователям Windows потребуется загрузить и извлечь двоичные файлы Apache.
Добавьте Gunicorn в файл требований:
gunicorn==20.1.0
Убейте сервер разработки Django и установите Gunicorn:
(venv)$ pip install -r requirements.txt
Затем запустите приложение Django с помощью Gunicorn (и четырех рабочих процессов ) следующим образом:
(venv)$ gunicorn core.wsgi:application -w 4
В новом окне терминала запустите Apache Bench:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Это будет имитировать 100 соединений по 10 одновременным потокам. Это 100 запросов, по 10 за раз.
Обратите внимание на количество запросов в секунду:
Requests per second: 1.69 [#/sec] (mean)
Имейте в виду, что панель инструментов Django Debug добавит немного накладных расходов. Сравнительный анализ вообще трудно провести идеально правильно. Главное — постоянство. Выберите метрику, на которой следует сосредоточиться, и используйте одну и ту же среду для каждого теста.
Убейте сервер Gunicorn и запустите резервную копию сервера Django dev:
(venv)$ python manage.py runserver
Теперь давайте посмотрим, как кэшировать представление.
Начните с украшения ApiCalls
представления с помощью @cache_page
декоратора следующим образом:
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Поскольку мы используем представление на основе классов, мы не можем поместить декоратор непосредственно в класс, поэтому мы использовали method_decorator
и указали dispatch
(в качестве метода, который нужно декорировать) для аргумента имени.
Кэш в этом примере устанавливает время ожидания (или TTL) в пять минут.
Кроме того, вы можете установить это в своих настройках следующим образом:
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
Затем снова в представлении:
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Далее давайте добавим кеш-бэкенд.
Memcached и Redis — это хранилища данных «ключ-значение» в памяти. Они просты в использовании и оптимизированы для высокопроизводительного поиска. Вы, вероятно, не увидите большой разницы в производительности или использовании памяти между ними. Тем не менее, Memcached немного проще настроить, поскольку он разработан для простоты и удобства использования. Redis, с другой стороны, имеет более богатый набор функций, поэтому он имеет широкий спектр вариантов использования, помимо кэширования. Например, он часто используется для хранения пользовательских сеансов или в качестве брокера сообщений в системе публикации/подписки. Из-за своей гибкости, если вы еще не инвестировали в Memcached, Redis является гораздо лучшим решением.
Чтобы узнать больше об этом, просмотрите этот ответ Stack Overflow.
Затем выберите хранилище данных по вашему выбору, и давайте посмотрим, как кэшировать представление.
Загрузите и установите Redis.
Если вы работаете на Mac, мы рекомендуем установить Redis с Homebrew :
$ варить установить Redis
После установки в новом окне терминала запустите сервер Redis и убедитесь, что он работает на своем порту по умолчанию, 6379. Номер порта будет важен, когда мы сообщим Django, как общаться с Redis.
$ redis-server
Чтобы Django использовал Redis в качестве бэкенда кэша, нам сначала нужно установить django-redis .
Добавьте его в файл requirements.txt :
django-redis==5.0.0
Установить:
(venv)$ pip install -r requirements.txt
Затем добавьте пользовательский бэкенд в файл settings.py :
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Теперь, когда вы снова запустите сервер, Redis будет использоваться в качестве бэкенда кеша:
(venv)$ python manage.py runserver
Когда сервер запущен и работает, перейдите по адресу http://127.0.0.1:8000 .
Первый запрос все равно займет около двух секунд. Обновите страницу. Страница должна загрузиться почти мгновенно. Посмотрите на время загрузки в вашем терминале. Он должен быть близок к нулю:
Total time: 0.01s
Любопытно, как выглядят кэшированные данные внутри Redis?
Запустите Redis CLI в интерактивном режиме в новом окне терминала:
$ redis-cli
Тебе следует увидеть:
127.0.0.1:6379>
Запустите ping
, чтобы убедиться, что все работает правильно:
127.0.0.1:6379> ping
PONG
Вернитесь к файлу настроек. Мы использовали базу данных Redis №1: 'LOCATION': 'redis://127.0.0.1:6379/1',
. Итак, запустите, select 1
чтобы выбрать эту базу данных, а затем запустите keys *
, чтобы просмотреть все ключи:
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Мы видим, что Джанго вставил один ключ заголовка и один cache_page
ключ.
Чтобы просмотреть фактические кэшированные данные, запустите get
команду с ключом в качестве аргумента:
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Вы должны увидеть что-то похожее на:
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
После завершения выйдите из интерактивного интерфейса командной строки:
127.0.0.1:6379[1]> exit
Перейдите к разделу «Тесты производительности».
Начните с добавления pymemcache в файл requirements.txt :
pymemcache==3.5.0
Установите зависимости:
(venv)$ pip install -r requirements.txt
Затем нам нужно обновить настройки в core/settings.py, чтобы включить бэкенд Memcached:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
Здесь мы добавили серверную часть PyMemcacheCache и указали, что Memcached должен работать на нашем локальном компьютере на локальном хосте (127.0.0.1) с портом 11211, который является портом по умолчанию для Memcached.
Далее нам нужно установить и запустить демон Memcached. Самый простой способ установить его — через менеджер пакетов, такой как APT, YUM, Homebrew или Chocolatey, в зависимости от вашей операционной системы:
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
Затем запустите его в другом терминале на порту 11211:
$ memcached -p 11211
# test: telnet localhost 11211
Для получения дополнительной информации об установке и настройке Memcached посетите официальную вики .
Снова перейдите по адресу http://127.0.0.1:8000 в нашем браузере. Первый запрос по-прежнему будет занимать полные две секунды, но все последующие запросы будут использовать кэш. Итак, если вы обновите или нажмете кнопку «Получить новые данные», страница должна загрузиться практически мгновенно.
Как выглядит время выполнения в вашем терминале?
Total time: 0.03s
Если мы посмотрим на время, необходимое для загрузки первого запроса по сравнению со вторым (кэшированным) запросом на панели инструментов отладки Django, это будет выглядеть примерно так:
Также на панели инструментов отладки вы можете увидеть операции кэширования:
Снова запустите Gunicorn и повторно запустите тесты производительности:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Каковы новые запросы в секунду? На моей машине около 36!
В этой статье мы рассмотрели различные встроенные параметры кэширования в Django, а также доступные уровни кэширования. Мы также подробно описали, как кэшировать представление с помощью кэша Django для каждого представления как с Memcached, так и с Redis.
Вы можете найти окончательный код для обоих вариантов, Memcached и Redis, в репозитории cache-django-view .
--
Как правило, вам следует обратить внимание на кэширование, когда рендеринг страниц замедляется из-за сетевых задержек из-за запросов к базе данных или HTTP-вызовов.
Оттуда настоятельно рекомендуется использовать собственный кеш-сервер Django с Redis с Per-view
типом. Если вам нужно больше детализации и контроля, поскольку не все данные в шаблоне одинаковы для всех пользователей или части данных часто изменяются, перейдите к кэшу фрагментов шаблона или API низкоуровневого кэша.
Источник: https://testdriven.io
1660892220
緩存通常是提高應用程序性能的最有效方式。
對於動態網站,在渲染模板時,您通常需要從各種來源(如數據庫、文件系統和第三方 API 等)收集數據,處理數據並將業務邏輯應用於在將其提供給客戶之前。最終用戶將注意到由於網絡延遲導致的任何延遲。
例如,假設您必須對外部 API 進行 HTTP 調用以獲取呈現模板所需的數據。即使在完美的條件下,這也會增加渲染時間,從而增加整體加載時間。如果 API 出現故障或者您可能受到速率限制怎麼辦?無論哪種方式,如果數據不經常更新,最好實現緩存機制以防止必須為每個客戶端請求完全進行 HTTP 調用。
本文著眼於如何做到這一點,首先回顧 Django 的緩存框架作為一個整體,然後逐步詳細說明如何緩存 Django 視圖。
依賴項:
在本教程結束時,您應該能夠:
Django 帶有幾個內置的緩存後端,以及對自定義後端的支持。
內置選項有:
Django 中的緩存可以在不同級別(或站點的部分)上實現。您可以緩存整個站點或具有各種粒度級別的特定部分(按粒度降序排列):
這是在 Django 中實現緩存的最簡單方法。為此,您只需在settings.py文件中添加兩個中間件類:
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
中間件的順序在這裡很重要。
UpdateCacheMiddleware
必須來之前FetchFromCacheMiddleware
。有關更多信息,請查看 Django 文檔中的Order of MIDDLEWARE。
然後,您需要添加以下設置:
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
儘管如果您的站點很少或沒有動態內容,緩存整個站點可能是一個不錯的選擇,但它可能不適合用於具有基於內存的緩存後端的大型站點,因為 RAM 非常昂貴。
您可以緩存特定視圖,而不是將寶貴的內存空間浪費在緩存靜態頁面或從快速變化的 API 獲取數據的動態頁面上。這是我們將在本文中使用的方法。它也是您在 Django 應用程序中實現緩存時幾乎總是應該從緩存級別開始的。
您可以直接在視圖函數上或在以下路徑中使用cache_page裝飾器來實現這種類型的緩存URLConf
:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
緩存本身是基於 URL 的,因此請求object/1
將object/2
被單獨緩存。
值得注意的是,直接在視圖上實現緩存使得在某些情況下禁用緩存更加困難。例如,如果您想允許某些用戶在沒有緩存的情況下訪問視圖怎麼辦?通過 啟用緩存可以將URLConf
不同的 URL 關聯到不使用緩存的視圖:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
如果您的模板包含經常根據數據更改的部分,您可能希望將它們排除在緩存之外。
例如,您可能在模板區域的導航欄中使用經過身份驗證的用戶的電子郵件。好吧,如果您有成千上萬的用戶,那麼該片段將在 RAM 中復制數千次,每個用戶一個。這就是模板片段緩存發揮作用的地方,它允許您指定要緩存的模板的特定區域。
要緩存對象列表:
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
在這裡,{% load cache %}
我們可以訪問cache
模板標籤,它預計緩存超時(以秒為單位500
)以及緩存片段的名稱(object_list
)。
對於前面的選項沒有提供足夠粒度的情況,您可以使用低級 API 通過緩存鍵來管理緩存中的各個對象。
例如:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
在此示例中,您將希望在從數據庫中添加、更改或刪除對象時使緩存無效(或刪除)。一種管理方法是通過數據庫信號:
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
有關使用數據庫信號使緩存無效的更多信息,請查看Django 中的低級緩存 API文章。
有了這個,讓我們看一些例子。
從cache-django-view 存儲庫中克隆基礎項目,然後檢查基礎分支:
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
創建(並激活)虛擬環境並安裝要求:
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
應用 Django 遷移,然後啟動服務器:
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
在您選擇的瀏覽器中導航到http://127.0.0.1:8000以確保一切正常。
你應該看到:
記下您的終端。您應該看到請求的總執行時間:
Total time: 2.23s
該指標來自core/middleware.py:
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
快速查看apicalls/views.py中的視圖:
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
requests
此視圖對httpbin.org進行 HTTP 調用。為了模擬一個長請求,來自 API 的響應會延遲兩秒鐘。因此,http: //127.0.0.1 :8000不僅在初始請求上呈現,而且對每個後續請求也需要大約兩秒鐘。雖然初始請求的兩秒加載在某種程度上是可以接受的,但對於後續請求來說,這是完全不可接受的,因為數據沒有改變。讓我們通過使用 Django 的 Per-view 緩存級別緩存整個視圖來解決這個問題。
工作流程:
在添加緩存之前,讓我們使用Apache Bench快速運行負載測試以獲得基準基線,粗略了解我們的應用程序每秒可以處理多少請求。
Apache Bench 預裝在 Mac 上。
如果您使用的是 Linux 系統,那麼它很可能已經安裝並準備就緒。如果沒有,您可以通過 APT (
apt-get install apache2-utils
) 或 YUM (yum install httpd-tools
) 安裝。Windows 用戶需要下載並解壓 Apache 二進製文件。
將Gunicorn添加到需求文件中:
gunicorn==20.1.0
殺死 Django 開發服務器並安裝 Gunicorn:
(venv)$ pip install -r requirements.txt
接下來,使用 Gunicorn(和四個worker)為 Django 應用程序提供服務,如下所示:
(venv)$ gunicorn core.wsgi:application -w 4
在新的終端窗口中,運行 Apache Bench:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
這將模擬 10 個並發線程上的 100 個連接。那是 100 個請求,一次 10 個。
記下每秒的請求數:
Requests per second: 1.69 [#/sec] (mean)
請記住,Django 調試工具欄會增加一些開銷。一般來說,基準測試很難完全正確。重要的是一致性。選擇一個指標來關注並為每個測試使用相同的環境。
殺死 Gunicorn 服務器並重新啟動 Django 開發服務器:
(venv)$ python manage.py runserver
有了這個,讓我們看看如何緩存視圖。
首先用裝飾器裝飾ApiCalls
視圖,@cache_page
如下所示:
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
由於我們使用的是基於類的視圖,我們不能將裝飾器直接放在類上,因此我們使用 amethod_decorator
和指定dispatch
(作為要裝飾的方法)作為 name 參數。
此示例中的緩存設置了五分鐘的超時(或 TTL)。
或者,您可以在設置中進行設置,如下所示:
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
然後,回到視圖中:
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
接下來,讓我們添加一個緩存後端。
Memcached和Redis是內存中的鍵值數據存儲。它們易於使用並針對高性能查找進行了優化。您可能不會看到兩者在性能或內存使用方面有太大差異。也就是說,Memcached 的配置稍微容易一些,因為它是為簡單易用而設計的。另一方面,Redis 具有更豐富的功能集,因此除了緩存之外,它還具有廣泛的用例。例如,它通常用於存儲用戶會話或作為發布/訂閱系統中的消息代理。由於它的靈活性,除非您已經投資於 Memcached,否則 Redis 是更好的解決方案。
有關這方面的更多信息,請查看此Stack Overflow 答案。
接下來,選擇您選擇的數據存儲,讓我們看看如何緩存視圖。
下載並安裝 Redis。
如果您使用的是 Mac,我們建議您使用Homebrew安裝 Redis :
$ brew install redis
安裝後,在新的終端窗口中啟動 Redis 服務器並確保它在其默認端口 6379 上運行。當我們告訴 Django 如何與 Redis 通信時,端口號將很重要。
$ redis-server
為了讓 Django 使用 Redis 作為緩存後端,我們首先需要安裝django-redis。
將其添加到requirements.txt文件中:
django-redis==5.0.0
安裝:
(venv)$ pip install -r requirements.txt
接下來,將自定義後端添加到settings.py文件中:
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
現在,當您再次運行服務器時,Redis 將用作緩存後端:
(venv)$ python manage.py runserver
服務器啟動並運行後,導航到http://127.0.0.1:8000。
第一個請求仍然需要大約兩秒鐘。刷新頁面。該頁面應該幾乎立即加載。查看終端中的加載時間。它應該接近於零:
Total time: 0.01s
好奇緩存數據在 Redis 中的樣子?
在新的終端窗口中以交互模式運行 Redis CLI:
$ redis-cli
你應該看到:
127.0.0.1:6379>
運行ping
以確保一切正常:
127.0.0.1:6379> ping
PONG
返回設置文件。我們使用了 Redis 數據庫 1 'LOCATION': 'redis://127.0.0.1:6379/1',
:. 因此,運行select 1
以選擇該數據庫,然後運行keys *
以查看所有鍵:
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
我們可以看到 Django 放入了一個 header key 和一個cache_page
key。
要查看實際緩存的數據,請get
使用鍵作為參數運行命令:
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
您應該看到類似於以下內容的內容:
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
完成後退出交互式 CLI:
127.0.0.1:6379[1]> exit
跳到“性能測試”部分。
首先將pymemcache添加到requirements.txt文件中:
pymemcache==3.5.0
安裝依賴項:
(venv)$ pip install -r requirements.txt
接下來,我們需要更新core/settings.py中的設置以啟用 Memcached 後端:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
在這裡,我們添加了PyMemcacheCache後端,並指示 Memcached 應該在我們的本地計算機上運行 localhost (127.0.0.1) 端口 11211,這是 Memcached 的默認端口。
接下來,我們需要安裝並運行 Memcached 守護進程。安裝它的最簡單方法是通過 APT、YUM、Homebrew 或 Chocolatey 等軟件包管理器,具體取決於您的操作系統:
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
然後,在端口 11211 上的不同終端中運行它:
$ memcached -p 11211
# test: telnet localhost 11211
有關 Memcached 安裝和配置的更多信息,請查看官方wiki。
再次在我們的瀏覽器中導航到http://127.0.0.1:8000。第一個請求仍然需要整整兩秒鐘,但所有後續請求都將利用緩存。因此,如果您刷新或按下“獲取新數據按鈕”,頁面應該幾乎立即加載。
您的終端中的執行時間是什麼樣的?
Total time: 0.03s
如果我們查看在 Django 調試工具欄中加載第一個請求與第二個(緩存)請求所花費的時間,它看起來類似於:
同樣在調試工具欄中,您可以看到緩存操作:
再次旋轉 Gunicorn 並重新運行性能測試:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
每秒有多少新請求?在我的機器上大約是 36!
在本文中,我們研究了 Django 中用於緩存的不同內置選項以及可用的不同級別的緩存。我們還詳細介紹瞭如何使用 Django 的 Per-view 緩存以及 Memcached 和 Redis 來緩存視圖。
您可以在cache-django-view 存儲庫中找到 Memcached 和 Redis 這兩個選項的最終代碼。
--
通常,當由於數據庫查詢或 HTTP 調用的網絡延遲導致頁面呈現速度較慢時,您需要考慮緩存。
從那裡開始,強烈建議使用帶有Per-view
類型的 Redis 的自定義 Django 緩存後端。如果你需要更多的粒度和控制,因為不是所有用戶的模板上的所有數據都是相同的,或者部分數據經常變化,然後跳到模板片段緩存或低級緩存API。
1660884840
La mise en cache est généralement le moyen le plus efficace d'améliorer les performances d'une application.
Pour les sites Web dynamiques, lors du rendu d'un modèle, vous devrez souvent collecter des données à partir de diverses sources (comme une base de données, le système de fichiers et des API tierces, pour n'en nommer que quelques-unes), traiter les données et appliquer une logique métier à avant de le servir à un client. Tout retard dû à la latence du réseau sera remarqué par l'utilisateur final.
Par exemple, supposons que vous deviez effectuer un appel HTTP à une API externe pour récupérer les données nécessaires au rendu d'un modèle. Même dans des conditions parfaites, cela augmentera le temps de rendu, ce qui augmentera le temps de chargement global. Que se passe-t-il si l'API tombe en panne ou si vous êtes soumis à une limitation de débit ? Quoi qu'il en soit, si les données sont rarement mises à jour, il est judicieux d'implémenter un mécanisme de mise en cache pour éviter d'avoir à effectuer l'appel HTTP pour chaque demande client.
Cet article explique comment faire exactement cela en examinant d'abord le cadre de mise en cache de Django dans son ensemble, puis en détaillant étape par étape comment mettre en cache une vue Django.
Dépendances :
À la fin de ce didacticiel, vous devriez être en mesure de :
Django est livré avec plusieurs backends de mise en cache intégrés, ainsi que la prise en charge d'un backend personnalisé .
Les options intégrées sont :
La mise en cache dans Django peut être implémentée à différents niveaux (ou parties du site). Vous pouvez mettre en cache l'intégralité du site ou des parties spécifiques avec différents niveaux de granularité (répertoriés par ordre décroissant de granularité) :
C'est le moyen le plus simple d'implémenter la mise en cache dans Django. Pour cela, il vous suffira d'ajouter deux classes middleware à votre fichier settings.py :
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
L'ordre du middleware est important ici.
UpdateCacheMiddleware
doit venir avantFetchFromCacheMiddleware
. Pour plus d'informations, consultez Order of MIDDLEWARE dans la documentation Django.
Vous devez ensuite ajouter les paramètres suivants :
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
Bien que la mise en cache de l'intégralité du site puisse être une bonne option si votre site a peu ou pas de contenu dynamique, il peut ne pas être approprié de l'utiliser pour les grands sites avec un backend de cache basé sur la mémoire car la RAM est, eh bien, chère.
Plutôt que de gaspiller de l'espace mémoire précieux en mettant en cache des pages statiques ou des pages dynamiques dont les données proviennent d'une API en évolution rapide, vous pouvez mettre en cache des vues spécifiques. C'est l'approche que nous allons utiliser dans cet article. C'est aussi le niveau de mise en cache avec lequel vous devriez presque toujours commencer lorsque vous cherchez à implémenter la mise en cache dans votre application Django.
Vous pouvez implémenter ce type de cache avec le décorateur cache_page soit sur la fonction d'affichage directement, soit dans le chemin dans URLConf
:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
The cache itself is based on the URL, so requests to, say, object/1
and object/2
will be cached separately.
It's worth noting that implementing the cache directly on the view makes it more difficult to disable the cache in certain situations. For example, what if you wanted to allow certain users access to the view without the cache? Enabling the cache via the URLConf
provides the opportunity to associate a different URL to the view that doesn't use the cache:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
If your templates contain parts that change often based on the data you'll probably want to leave them out of the cache.
Par exemple, vous utilisez peut-être l'e-mail de l'utilisateur authentifié dans la barre de navigation d'une zone du modèle. Eh bien, si vous avez des milliers d'utilisateurs, ce fragment sera dupliqué des milliers de fois dans la RAM, un pour chaque utilisateur. C'est là que la mise en cache des fragments de modèle entre en jeu, ce qui vous permet de spécifier les zones spécifiques d'un modèle à mettre en cache.
Pour mettre en cache une liste d'objets :
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
Ici, {% load cache %}
nous donne accès à la cache
balise de modèle, qui attend un délai d'expiration du cache en secondes ( 500
) avec le nom du fragment de cache ( object_list
).
Dans les cas où les options précédentes ne fournissent pas suffisamment de granularité, vous pouvez utiliser l'API de bas niveau pour gérer des objets individuels dans le cache par clé de cache.
Par exemple:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
Dans cet exemple, vous souhaiterez invalider (ou supprimer) le cache lorsque des objets sont ajoutés, modifiés ou supprimés de la base de données. Une façon de gérer cela consiste à utiliser des signaux de base de données :
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
Pour en savoir plus sur l'utilisation des signaux de base de données pour invalider le cache, consultez l' article de l' API de cache de bas niveau dans Django .
Sur ce, regardons quelques exemples.
Clonez le projet de base à partir du référentiel cache-django-view , puis consultez la branche de base :
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
Créez (et activez) un environnement virtuel et installez la configuration requise :
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
Appliquez les migrations Django, puis démarrez le serveur :
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
Accédez à http://127.0.0.1:8000 dans le navigateur de votre choix pour vous assurer que tout fonctionne comme prévu.
Tu devrais voir:
Prenez note de votre terminal. Vous devriez voir le temps d'exécution total de la requête :
Total time: 2.23s
Cette métrique provient de core/middleware.py :
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
Jetez un coup d'œil à la vue dans apicalls/views.py :
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Cette vue effectue un appel HTTP requests
avec httpbin.org . Pour simuler une requête longue, la réponse de l'API est retardée de deux secondes. Ainsi, cela devrait prendre environ deux secondes pour que http://127.0.0.1:8000 s'affiche non seulement sur la requête initiale, mais également pour chaque requête suivante. Alors qu'un chargement de deux secondes est quelque peu acceptable sur la requête initiale, il est totalement inacceptable pour les requêtes suivantes puisque les données ne changent pas. Corrigeons cela en mettant en cache la vue entière à l'aide du niveau de cache par vue de Django.
Flux de travail :
Avant d'ajouter du cache, exécutons rapidement un test de charge pour obtenir une référence de référence à l'aide d' Apache Bench , afin d'avoir une idée approximative du nombre de requêtes que notre application peut gérer par seconde.
Apache Bench est préinstallé sur Mac.
Si vous utilisez un système Linux, il y a de fortes chances qu'il soit déjà installé et prêt à fonctionner également. Sinon, vous pouvez installer via APT (
apt-get install apache2-utils
) ou YUM (yum install httpd-tools
).Les utilisateurs de Windows devront télécharger et extraire les binaires Apache.
Ajoutez Gunicorn au fichier requirements :
gunicorn==20.1.0
Tuez le serveur de développement Django et installez Gunicorn :
(venv)$ pip install -r requirements.txt
Ensuite, servez l'application Django avec Gunicorn (et quatre travailleurs ) comme ceci :
(venv)$ gunicorn core.wsgi:application -w 4
Dans une nouvelle fenêtre de terminal, exécutez Apache Bench :
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Cela simulera 100 connexions sur 10 threads simultanés. C'est 100 demandes, 10 à la fois.
Prenez note des requêtes par seconde :
Requests per second: 1.69 [#/sec] (mean)
Gardez à l'esprit que Django Debug Toolbar ajoutera un peu de surcharge. L'analyse comparative en général est difficile à obtenir parfaitement. L'important, c'est la cohérence. Choisissez une métrique sur laquelle vous concentrer et utilisez le même environnement pour chaque test.
Tuez le serveur Gunicorn et relancez le serveur de développement Django :
(venv)$ python manage.py runserver
Sur ce, regardons comment mettre en cache une vue.
Commencez par décorer la ApiCalls
vue avec le @cache_page
décorateur comme ceci :
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Comme nous utilisons une vue basée sur les classes, nous ne pouvons pas placer le décorateur directement sur la classe, nous avons donc utilisé a method_decorator
et spécifié dispatch
(comme méthode à décorer) pour l'argument name.
Le cache dans cet exemple définit un délai d'attente (ou TTL) de cinq minutes.
Alternativement, vous pouvez définir ceci dans vos paramètres comme suit :
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
Puis, de retour dans la vue :
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Ensuite, ajoutons un backend de cache.
Memcached et Redis sont des magasins de données clé-valeur en mémoire. Ils sont faciles à utiliser et optimisés pour des recherches hautes performances. Vous ne verrez probablement pas beaucoup de différence de performances ou d'utilisation de la mémoire entre les deux. Cela dit, Memcached est légèrement plus facile à configurer car il est conçu pour la simplicité et la facilité d'utilisation. Redis, en revanche, possède un ensemble de fonctionnalités plus riche, ce qui lui permet de proposer un large éventail de cas d'utilisation au-delà de la mise en cache. Par exemple, il est souvent utilisé pour stocker des sessions utilisateur ou comme courtier de messages dans un système pub/sub. En raison de sa flexibilité, à moins que vous ne soyez déjà investi dans Memcached, Redis est une bien meilleure solution.
Pour en savoir plus, consultez cette réponse Stack Overflow.
Ensuite, choisissez le magasin de données de votre choix et voyons comment mettre en cache une vue.
Téléchargez et installez Redis.
Si vous êtes sur Mac, nous vous recommandons d'installer Redis avec Homebrew :
$ brasser installer redis
Une fois installé, dans une nouvelle fenêtre de terminal, démarrez le serveur Redis et assurez-vous qu'il fonctionne sur son port par défaut, 6379. Le numéro de port sera important lorsque nous indiquerons à Django comment communiquer avec Redis.
$ redis-server
Pour que Django utilise Redis comme moteur de cache, nous devons d'abord installer django-redis .
Ajoutez-le au fichier requirements.txt :
django-redis==5.0.0
Installer:
(venv)$ pip install -r requirements.txt
Ensuite, ajoutez le backend personnalisé au fichier settings.py :
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Désormais, lorsque vous relancerez le serveur, Redis sera utilisé comme backend de cache :
(venv)$ python manage.py runserver
Avec le serveur opérationnel, accédez à http://127.0.0.1:8000 .
La première demande prendra encore environ deux secondes. Actualiser la page. La page devrait se charger presque instantanément. Regardez le temps de chargement dans votre terminal. Il doit être proche de zéro :
Total time: 0.01s
Vous voulez savoir à quoi ressemblent les données mises en cache dans Redis ?
Exécutez Redis CLI en mode interactif dans une nouvelle fenêtre de terminal :
$ redis-cli
Tu devrais voir:
127.0.0.1:6379>
Exécutez ping
pour vous assurer que tout fonctionne correctement :
127.0.0.1:6379> ping
PONG
Revenez au fichier de paramètres. Nous avons utilisé la base de données Redis numéro 1 : 'LOCATION': 'redis://127.0.0.1:6379/1',
. Alors, exécutez select 1
pour sélectionner cette base de données, puis exécutez keys *
pour afficher toutes les clés :
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Nous pouvons voir que Django a mis une clé d'en-tête et une cache_page
clé.
Pour afficher les données réelles mises en cache, exécutez la get
commande avec la clé comme argument :
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Vous devriez voir quelque chose de similaire à :
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
Quittez la CLI interactive une fois terminé :
127.0.0.1:6379[1]> exit
Passez à la section "Tests de performance".
Commencez par ajouter pymemcache au fichier requirements.txt :
pymemcache==3.5.0
Installez les dépendances :
(venv)$ pip install -r requirements.txt
Ensuite, nous devons mettre à jour les paramètres dans core/settings.py pour activer le backend Memcached :
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
Ici, nous avons ajouté le backend PyMemcacheCache et indiqué que Memcached devrait être exécuté sur notre machine locale sur le port localhost (127.0.0.1) 11211, qui est le port par défaut pour Memcached.
Ensuite, nous devons installer et exécuter le démon Memcached. Le moyen le plus simple de l'installer est via un gestionnaire de packages comme APT, YUM, Homebrew ou Chocolatey selon votre système d'exploitation :
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
Ensuite, exécutez-le dans un autre terminal sur le port 11211 :
$ memcached -p 11211
# test: telnet localhost 11211
Pour plus d'informations sur l'installation et la configuration de Memcached, consultez le wiki officiel .
Accédez à nouveau à http://127.0.0.1:8000 dans notre navigateur. La première requête prendra toujours les deux secondes complètes, mais toutes les requêtes suivantes tireront parti du cache. Ainsi, si vous actualisez ou appuyez sur le bouton "Obtenir de nouvelles données", la page devrait se charger presque instantanément.
À quoi ressemble le temps d'exécution dans votre terminal ?
Total time: 0.03s
Si nous regardons le temps qu'il faut pour charger la première requête par rapport à la deuxième requête (en cache) dans Django Debug Toolbar, cela ressemblera à :
Également dans la barre d'outils de débogage, vous pouvez voir les opérations de cache :
Faites à nouveau tourner Gunicorn et relancez les tests de performances :
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Quelles sont les nouvelles requêtes par seconde ? C'est environ 36 sur ma machine !
Dans cet article, nous avons examiné les différentes options intégrées de mise en cache dans Django ainsi que les différents niveaux de mise en cache disponibles. Nous avons également détaillé comment mettre en cache une vue à l'aide du cache Per-view de Django avec Memcached et Redis.
Vous pouvez trouver le code final pour les deux options, Memcached et Redis, dans le référentiel cache-django-view .
--
En général, vous souhaiterez rechercher la mise en cache lorsque le rendu de la page est lent en raison de la latence du réseau à partir des requêtes de base de données ou des appels HTTP.
À partir de là, il est fortement recommandé d'utiliser un backend de cache Django personnalisé avec Redis avec un Per-view
type. Si vous avez besoin de plus de granularité et de contrôle, car toutes les données du modèle ne sont pas identiques pour tous les utilisateurs ou certaines parties des données changent fréquemment, passez au cache de fragment de modèle ou à l'API de cache de bas niveau.
Source : https://testdrive.io
1660877580
El almacenamiento en caché suele ser la forma más eficaz de aumentar el rendimiento de una aplicación.
Para sitios web dinámicos, al renderizar una plantilla, a menudo tendrá que recopilar datos de varias fuentes (como una base de datos, el sistema de archivos y API de terceros, por nombrar algunos), procesar los datos y aplicar la lógica comercial a antes de servirlo a un cliente. El usuario final notará cualquier retraso debido a la latencia de la red.
Por ejemplo, supongamos que debe realizar una llamada HTTP a una API externa para obtener los datos necesarios para representar una plantilla. Incluso en perfectas condiciones, esto aumentará el tiempo de renderizado, lo que aumentará el tiempo de carga general. ¿Qué sucede si la API deja de funcionar o tal vez está sujeto a una limitación de velocidad? De cualquier manera, si los datos se actualizan con poca frecuencia, es una buena idea implementar un mecanismo de almacenamiento en caché para evitar tener que realizar la llamada HTTP por completo para cada solicitud del cliente.
Este artículo analiza cómo hacer precisamente eso revisando primero el marco de almacenamiento en caché de Django en su conjunto y luego detallando paso a paso cómo almacenar en caché una vista de Django.
Dependencias:
Al final de este tutorial, debería ser capaz de:
Django viene con varios backends de almacenamiento en caché incorporados , así como soporte para un backend personalizado .
Las opciones integradas son:
El almacenamiento en caché en Django se puede implementar en diferentes niveles (o partes del sitio). Puede almacenar en caché todo el sitio o partes específicas con varios niveles de granularidad (enumerados en orden descendente de granularidad):
Esta es la forma más fácil de implementar el almacenamiento en caché en Django. Para hacer esto, todo lo que tendrá que hacer es agregar dos clases de middleware a su archivo settings.py :
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
El orden del middleware es importante aquí.
UpdateCacheMiddleware
debe venir antesFetchFromCacheMiddleware
. Para obtener más información, consulte Orden de MIDDLEWARE de los documentos de Django.
A continuación, debe agregar la siguiente configuración:
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
Si bien el almacenamiento en caché de todo el sitio podría ser una buena opción si su sitio tiene poco o ningún contenido dinámico, puede no ser apropiado para sitios grandes con un servidor de caché basado en memoria, ya que la memoria RAM es, bueno, costosa.
En lugar de desperdiciar un valioso espacio de memoria almacenando en caché páginas estáticas o páginas dinámicas que obtienen datos de una API que cambia rápidamente, puede almacenar en caché vistas específicas. Este es el enfoque que usaremos en este artículo. También es el nivel de almacenamiento en caché con el que casi siempre debe comenzar cuando busca implementar el almacenamiento en caché en su aplicación Django.
Puede implementar este tipo de caché con el decorador cache_page ya sea en la función de vista directamente o en la ruta dentro de URLConf
:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
El caché en sí se basa en la URL, por lo que las solicitudes a, digamos, object/1
y object/2
se almacenarán en caché por separado.
Vale la pena señalar que implementar el caché directamente en la vista hace que sea más difícil deshabilitar el caché en ciertas situaciones. Por ejemplo, ¿qué sucede si desea permitir que ciertos usuarios accedan a la vista sin el caché? Habilitar el caché a través de URLConf
brinda la oportunidad de asociar una URL diferente a la vista que no usa el caché:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
Si sus plantillas contienen partes que cambian con frecuencia en función de los datos, probablemente querrá dejarlas fuera de la memoria caché.
Por ejemplo, tal vez use el correo electrónico del usuario autenticado en la barra de navegación en un área de la plantilla. Bueno, si tiene miles de usuarios, ese fragmento se duplicará miles de veces en la RAM, uno para cada usuario. Aquí es donde entra en juego el almacenamiento en caché de fragmentos de plantilla, que le permite especificar las áreas específicas de una plantilla para almacenar en caché.
Para almacenar en caché una lista de objetos:
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
Aquí, {% load cache %}
nos da acceso a la cache
etiqueta de plantilla, que espera un tiempo de espera de caché en segundos ( 500
) junto con el nombre del fragmento de caché ( object_list
).
Para los casos en los que las opciones anteriores no brindan suficiente granularidad, puede usar la API de bajo nivel para administrar objetos individuales en el caché por clave de caché.
Por ejemplo:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
En este ejemplo, deseará invalidar (o eliminar) la memoria caché cuando se agreguen, cambien o eliminen objetos de la base de datos. Una forma de gestionar esto es a través de las señales de la base de datos:
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
Para obtener más información sobre el uso de señales de bases de datos para invalidar la memoria caché, consulte el artículo API de caché de bajo nivel en Django .
Con eso, veamos algunos ejemplos.
Clona el proyecto base del repositorio cache-django-view y luego revisa la rama base:
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
Cree (y active) un entorno virtual e instale los requisitos:
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
Aplique las migraciones de Django y luego inicie el servidor:
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
Navegue a http://127.0.0.1:8000 en su navegador de elección para asegurarse de que todo funcione como se espera.
Debería ver:
Toma nota de tu terminal. Debería ver el tiempo total de ejecución de la solicitud:
Total time: 2.23s
Esta métrica proviene de core/middleware.py :
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
Eche un vistazo rápido a la vista en apicalls/views.py :
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Esta vista realiza una llamada HTTP requests
a httpbin.org . Para simular una solicitud larga, la respuesta de la API se retrasa dos segundos. Por lo tanto, http://127.0.0.1:8000 debería tardar unos dos segundos en procesarse no solo en la solicitud inicial, sino también en cada solicitud posterior. Si bien una carga de dos segundos es algo aceptable en la solicitud inicial, es completamente inaceptable para las solicitudes posteriores, ya que los datos no cambian. Arreglemos esto almacenando en caché toda la vista usando el nivel de caché por vista de Django.
Flujo de trabajo:
Antes de agregar la memoria caché, ejecutemos rápidamente una prueba de carga para obtener una referencia de referencia con Apache Bench , para tener una idea aproximada de cuántas solicitudes puede manejar nuestra aplicación por segundo.
Apache Bench viene preinstalado en Mac.
Si está en un sistema Linux, es probable que ya esté instalado y listo para funcionar también. Si no, puede instalar a través de APT (
apt-get install apache2-utils
) o YUM (yum install httpd-tools
).Los usuarios de Windows deberán descargar y extraer los archivos binarios de Apache.
Agregue Gunicorn al archivo de requisitos:
gunicorn==20.1.0
Elimine el servidor de desarrollo de Django e instale Gunicorn:
(venv)$ pip install -r requirements.txt
A continuación, sirva la aplicación Django con Gunicorn (y cuatro trabajadores ) así:
(venv)$ gunicorn core.wsgi:application -w 4
En una nueva ventana de terminal, ejecute Apache Bench:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Esto simulará 100 conexiones en 10 subprocesos simultáneos. Son 100 solicitudes, 10 a la vez.
Toma nota de las peticiones por segundo:
Requests per second: 1.69 [#/sec] (mean)
Tenga en cuenta que la barra de herramientas de depuración de Django agregará un poco de sobrecarga. La evaluación comparativa en general es difícil de acertar perfectamente. Lo importante es la constancia. Elija una métrica en la que centrarse y utilice el mismo entorno para cada prueba.
Elimine el servidor de Gunicorn y vuelva a hacer girar el servidor de desarrollo de Django:
(venv)$ python manage.py runserver
Con eso, veamos cómo almacenar en caché una vista.
Comience decorando la ApiCalls
vista con el @cache_page
decorador así:
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Como estamos usando una vista basada en clases, no podemos poner el decorador directamente en la clase, así que usamos un method_decorator
y especificado dispatch
(como el método que se va a decorar) para el argumento del nombre.
La caché de este ejemplo establece un tiempo de espera (o TTL) de cinco minutos.
Alternativamente, puede configurar esto en su configuración de esta manera:
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
Luego, de vuelta en la vista:
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
A continuación, agreguemos un backend de caché.
Memcached y Redis son almacenes de datos clave-valor en memoria. Son fáciles de usar y están optimizados para búsquedas de alto rendimiento. Probablemente no verá mucha diferencia en el rendimiento o el uso de la memoria entre los dos. Dicho esto, Memcached es un poco más fácil de configurar ya que está diseñado para ser simple y fácil de usar. Redis, por otro lado, tiene un conjunto más rico de funciones, por lo que tiene una amplia gama de casos de uso más allá del almacenamiento en caché. Por ejemplo, a menudo se usa para almacenar sesiones de usuario o como intermediario de mensajes en un sistema pub/sub. Debido a su flexibilidad, a menos que ya haya invertido en Memcached, Redis es una solución mucho mejor.
Para obtener más información sobre esto, revise esta respuesta de desbordamiento de pila.
A continuación, elija el almacén de datos de su elección y veamos cómo almacenar en caché una vista.
Descargue e instale Redis.
Si está en una Mac, le recomendamos que instale Redis con Homebrew :
$ preparar instalar redis
Una vez instalado, en una nueva ventana de terminal, inicie el servidor Redis y asegúrese de que se esté ejecutando en su puerto predeterminado, 6379. El número de puerto será importante cuando le digamos a Django cómo comunicarse con Redis.
$ redis-server
Para que Django use Redis como backend de caché, primero debemos instalar django-redis .
Agréguelo al archivo requirements.txt :
django-redis==5.0.0
Instalar:
(venv)$ pip install -r requirements.txt
A continuación, agregue el backend personalizado al archivo settings.py :
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Ahora, cuando vuelva a ejecutar el servidor, Redis se usará como backend de caché:
(venv)$ python manage.py runserver
Con el servidor en funcionamiento, vaya a http://127.0.0.1:8000 .
La primera solicitud aún tardará unos dos segundos. Recarga la página. La página debería cargarse casi instantáneamente. Echa un vistazo al tiempo de carga en tu terminal. Debe estar cerca de cero:
Total time: 0.01s
¿Tiene curiosidad por saber cómo se ven los datos almacenados en caché dentro de Redis?
Ejecute Redis CLI en modo interactivo en una nueva ventana de terminal:
$ redis-cli
Debería ver:
127.0.0.1:6379>
Ejecutar ping
para asegurarse de que todo funciona correctamente:
127.0.0.1:6379> ping
PONG
Regrese al archivo de configuración. Utilizamos la base de datos Redis número 1: 'LOCATION': 'redis://127.0.0.1:6379/1',
. Entonces, ejecute select 1
para seleccionar esa base de datos y luego ejecute keys *
para ver todas las claves:
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Podemos ver que Django puso una clave de encabezado y una cache_page
clave.
Para ver los datos almacenados en caché reales, ejecute el get
comando con la clave como argumento:
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Deberías ver algo similar a:
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
Salga de la CLI interactiva una vez hecho:
127.0.0.1:6379[1]> exit
Pase a la sección "Pruebas de rendimiento".
Comience agregando pymemcache al archivo requirements.txt :
pymemcache==3.5.0
Instala las dependencias:
(venv)$ pip install -r requirements.txt
A continuación, debemos actualizar la configuración en core/settings.py para habilitar el backend de Memcached:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
Aquí, agregamos el backend de PyMemcacheCache e indicamos que Memcached debería estar ejecutándose en nuestra máquina local en el puerto localhost (127.0.0.1) 11211, que es el puerto predeterminado para Memcached.
A continuación, debemos instalar y ejecutar el demonio Memcached. La forma más fácil de instalarlo es a través de un administrador de paquetes como APT, YUM, Homebrew o Chocolatey, según su sistema operativo:
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
Luego, ejecútelo en una terminal diferente en el puerto 11211:
$ memcached -p 11211
# test: telnet localhost 11211
Para obtener más información sobre la instalación y configuración de Memcached, consulte la wiki oficial .
Navegue a http://127.0.0.1:8000 en nuestro navegador nuevamente. La primera solicitud aún tomará los dos segundos completos, pero todas las solicitudes posteriores aprovecharán el caché. Entonces, si actualiza o presiona el botón "Obtener nuevos datos", la página debería cargarse casi instantáneamente.
¿Cómo es el tiempo de ejecución en tu terminal?
Total time: 0.03s
Si observamos el tiempo que se tarda en cargar la primera solicitud frente a la segunda solicitud (almacenada en caché) en la barra de herramientas de depuración de Django, se verá similar a:
También en la barra de herramientas de depuración, puede ver las operaciones de caché:
Vuelva a girar Gunicorn y vuelva a ejecutar las pruebas de rendimiento:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
¿Cuáles son las nuevas solicitudes por segundo? ¡Son alrededor de 36 en mi máquina!
En este artículo, analizamos las diferentes opciones integradas para el almacenamiento en caché en Django, así como los diferentes niveles de almacenamiento en caché disponibles. También detallamos cómo almacenar en caché una vista usando el caché por vista de Django con Memcached y Redis.
Puede encontrar el código final para ambas opciones, Memcached y Redis, en el repositorio cache-django-view .
--
En general, querrá buscar el almacenamiento en caché cuando la representación de la página sea lenta debido a la latencia de la red de las consultas de la base de datos o las llamadas HTTP.
A partir de ahí, se recomienda encarecidamente utilizar un backend de caché de Django personalizado con Redis con un Per-view
tipo. Si necesita más granularidad y control, porque no todos los datos en la plantilla son los mismos para todos los usuarios o partes de los datos cambian con frecuencia, vaya a la caché de fragmentos de plantilla o a la API de caché de bajo nivel.
Fuente: https://testdriven.io
1660870260
O armazenamento em cache geralmente é a maneira mais eficaz de aumentar o desempenho de um aplicativo.
Para sites dinâmicos, ao renderizar um modelo, muitas vezes você terá que coletar dados de várias fontes (como um banco de dados, o sistema de arquivos e APIs de terceiros, para citar alguns), processar os dados e aplicar lógica de negócios a antes de servi-lo a um cliente. Qualquer atraso devido à latência da rede será notado pelo usuário final.
Por exemplo, digamos que você precise fazer uma chamada HTTP para uma API externa para obter os dados necessários para renderizar um modelo. Mesmo em condições perfeitas, isso aumentará o tempo de renderização, o que aumentará o tempo total de carregamento. E se a API cair ou você estiver sujeito a limitação de taxa? De qualquer forma, se os dados forem atualizados com pouca frequência, é uma boa ideia implementar um mecanismo de cache para evitar a necessidade de fazer a chamada HTTP para cada solicitação do cliente.
Este artigo analisa como fazer exatamente isso, primeiro revisando a estrutura de cache do Django como um todo e, em seguida, detalhando passo a passo como armazenar em cache uma visualização do Django.
Dependências:
Ao final deste tutorial, você deverá ser capaz de:
O Django vem com vários backends de cache integrados , bem como suporte para um backend personalizado .
As opções incorporadas são:
O cache no Django pode ser implementado em diferentes níveis (ou partes do site). Você pode armazenar em cache todo o site ou partes específicas com vários níveis de granularidade (listados em ordem decrescente de granularidade):
Esta é a maneira mais fácil de implementar o cache no Django. Para fazer isso, tudo o que você precisa fazer é adicionar duas classes de middleware ao seu arquivo settings.py :
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # NEW
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # NEW
]
A ordem do middleware é importante aqui.
UpdateCacheMiddleware
deve vir antesFetchFromCacheMiddleware
. Para mais informações, dê uma olhada em Order of MIDDLEWARE dos documentos do Django.
Em seguida, você precisa adicionar as seguintes configurações:
CACHE_MIDDLEWARE_ALIAS = 'default' # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600' # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = '' # should be used if the cache is shared across multiple sites that use the same Django instance
Embora o cache de todo o site possa ser uma boa opção se o seu site tiver pouco ou nenhum conteúdo dinâmico, pode não ser apropriado usá-lo para sites grandes com um back-end de cache baseado em memória, pois a RAM é muito cara.
Em vez de desperdiçar um precioso espaço de memória no cache de páginas estáticas ou páginas dinâmicas que originam dados de uma API que muda rapidamente, você pode armazenar em cache visualizações específicas. Essa é a abordagem que usaremos neste artigo. É também o nível de cache com o qual você quase sempre deve começar ao tentar implementar o cache em seu aplicativo Django.
Você pode implementar esse tipo de cache com o decorador cache_page diretamente na função de visualização ou no caminho dentro de URLConf
:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def your_view(request):
...
# or
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
O cache em si é baseado na URL, portanto, solicitações para, digamos, object/1
e object/2
serão armazenadas em cache separadamente.
Vale a pena notar que implementar o cache diretamente na visualização torna mais difícil desabilitar o cache em determinadas situações. Por exemplo, e se você quiser permitir que determinados usuários acessem a visualização sem o cache? Habilitar o cache por meio do URLConf
fornece a oportunidade de associar uma URL diferente à visualização que não usa o cache:
from django.views.decorators.cache import cache_page
urlpatterns = [
path('object/<int:object_id>/', your_view),
path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]
Se seus modelos contiverem partes que mudam frequentemente com base nos dados, você provavelmente desejará deixá-las fora do cache.
Por exemplo, talvez você use o email do usuário autenticado na barra de navegação em uma área do modelo. Bem, se você tiver milhares de usuários, esse fragmento será duplicado milhares de vezes na RAM, uma para cada usuário. É aqui que o cache de fragmentos de modelo entra em ação, o que permite especificar as áreas específicas de um modelo para armazenar em cache.
Para armazenar em cache uma lista de objetos:
{% load cache %}
{% cache 500 object_list %}
<ul>
{% for object in objects %}
<li>{{ object.title }}</li>
{% endfor %}
</ul>
{% endcache %}
Aqui, {% load cache %}
nos dá acesso à cache
tag template, que espera um tempo limite de cache em segundos ( 500
) junto com o nome do fragmento de cache ( object_list
).
Para casos em que as opções anteriores não fornecem granularidade suficiente, você pode usar a API de baixo nível para gerenciar objetos individuais no cache por chave de cache.
Por exemplo:
from django.core.cache import cache
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
objects = cache.get('objects')
if objects is None:
objects = Objects.all()
cache.set('objects', objects)
context['objects'] = objects
return context
Neste exemplo, você desejará invalidar (ou remover) o cache quando os objetos forem adicionados, alterados ou removidos do banco de dados. Uma maneira de gerenciar isso é por meio de sinais de banco de dados:
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
cache.delete('objects')
@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
cache.delete('objects')
Para saber mais sobre como usar sinais de banco de dados para invalidar o cache, confira o artigo API de cache de baixo nível no Django .
Com isso, vejamos alguns exemplos.
Clone o projeto base do repositório cache-django-view e, em seguida, confira o branch base:
$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view
Crie (e ative) um ambiente virtual e instale os requisitos:
$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt
Aplique as migrações do Django e inicie o servidor:
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver
Navegue até http://127.0.0.1:8000 no navegador de sua preferência para garantir que tudo funcione conforme o esperado.
Você deveria ver:
Tome nota do seu terminal. Você deve ver o tempo total de execução da solicitação:
Total time: 2.23s
Esta métrica vem de core/middleware.py :
import logging
import time
def metric_middleware(get_response):
def middleware(request):
# Get beginning stats
start_time = time.perf_counter()
# Process the request
response = get_response(request)
# Get ending stats
end_time = time.perf_counter()
# Calculate stats
total_time = end_time - start_time
# Log the results
logger = logging.getLogger('debug')
logger.info(f'Total time: {(total_time):.2f}s')
print(f'Total time: {(total_time):.2f}s')
return response
return middleware
Dê uma olhada rápida na visualização em apicalls/views.py :
import datetime
import requests
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Essa visualização faz uma chamada HTTP com requests
para httpbin.org . Para simular uma solicitação longa, a resposta da API é atrasada por dois segundos. Portanto, deve levar cerca de dois segundos para http://127.0.0.1:8000 renderizar não apenas na solicitação inicial, mas também em cada solicitação subsequente. Embora um carregamento de dois segundos seja aceitável na solicitação inicial, é completamente inaceitável para solicitações subsequentes, pois os dados não são alterados. Vamos corrigir isso armazenando em cache a visão inteira usando o nível de cache Per-view do Django.
Fluxo de trabalho:
Antes de adicionar o cache, vamos executar rapidamente um teste de carga para obter uma linha de base de referência usando o Apache Bench , para ter uma ideia aproximada de quantas solicitações nosso aplicativo pode manipular por segundo.
O Apache Bench vem pré-instalado no Mac.
Se você estiver em um sistema Linux, é provável que ele já esteja instalado e pronto para funcionar também. Caso contrário, você pode instalar via APT (
apt-get install apache2-utils
) ou YUM (yum install httpd-tools
).Os usuários do Windows precisarão baixar e extrair os binários do Apache.
Adicione Gunicorn ao arquivo de requisitos:
gunicorn==20.1.0
Mate o servidor de desenvolvimento do Django e instale o Gunicorn:
(venv)$ pip install -r requirements.txt
Em seguida, sirva o aplicativo Django com Gunicorn (e quatro workers ) assim:
(venv)$ gunicorn core.wsgi:application -w 4
Em uma nova janela de terminal, execute o Apache Bench:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Isso simulará 100 conexões em 10 threads simultâneos. São 100 solicitações, 10 de cada vez.
Anote as solicitações por segundo:
Requests per second: 1.69 [#/sec] (mean)
Tenha em mente que a barra de ferramentas de depuração do Django adicionará um pouco de sobrecarga. O benchmarking em geral é difícil de acertar perfeitamente. O importante é a consistência. Escolha uma métrica para focar e use o mesmo ambiente para cada teste.
Mate o servidor Gunicorn e gire o servidor de desenvolvimento Django de volta:
(venv)$ python manage.py runserver
Com isso, vamos ver como armazenar em cache uma visualização.
Comece decorando a ApiCalls
vista com o @cache_page
decorador assim:
import datetime
import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Como estamos usando uma visão baseada em classe, não podemos colocar o decorador diretamente na classe, então usamos a method_decorator
e especificamos dispatch
(como o método a ser decorado) para o argumento name.
O cache neste exemplo define um tempo limite (ou TTL) de cinco minutos.
Alternativamente, você pode definir isso em suas configurações assim:
# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5
Então, de volta na visão:
import datetime
import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView
BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
template_name = 'apicalls/home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
response = requests.get(f'{BASE_URL}/delay/2')
response.raise_for_status()
context['content'] = 'Results received!'
context['current_time'] = datetime.datetime.now()
return context
Em seguida, vamos adicionar um back-end de cache.
O Memcached e o Redis são armazenamentos de dados de valor-chave na memória. Eles são fáceis de usar e otimizados para pesquisas de alto desempenho. Você provavelmente não verá muita diferença no desempenho ou no uso de memória entre os dois. Dito isso, o Memcached é um pouco mais fácil de configurar, pois foi projetado para simplicidade e facilidade de uso. O Redis, por outro lado, possui um conjunto mais rico de recursos, portanto, possui uma ampla variedade de casos de uso além do armazenamento em cache. Por exemplo, geralmente é usado para armazenar sessões de usuário ou como agente de mensagens em um sistema pub/sub. Devido à sua flexibilidade, a menos que você já tenha investido no Memcached, o Redis é uma solução muito melhor.
Para saber mais sobre isso, revise esta resposta do Stack Overflow.
Em seguida, escolha o armazenamento de dados de sua preferência e vejamos como armazenar em cache uma visualização.
Baixe e instale o Redis.
Se você estiver em um Mac, recomendamos instalar o Redis com Homebrew :
$ brew instalar redis
Uma vez instalado, em uma nova janela de terminal, inicie o servidor Redis e verifique se ele está rodando em sua porta padrão, 6379. O número da porta será importante quando dissermos ao Django como se comunicar com o Redis.
$ redis-server
Para o Django usar o Redis como um backend de cache, primeiro precisamos instalar o django-redis .
Adicione-o ao arquivo requirements.txt :
django-redis==5.0.0
Instalar:
(venv)$ pip install -r requirements.txt
Em seguida, adicione o back-end personalizado ao arquivo settings.py :
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Agora, quando você executar o servidor novamente, o Redis será usado como back-end de cache:
(venv)$ python manage.py runserver
Com o servidor funcionando, navegue até http://127.0.0.1:8000 .
A primeira solicitação ainda levará cerca de dois segundos. Recarregue a página. A página deve carregar quase instantaneamente. Dê uma olhada no tempo de carregamento em seu terminal. Deve ser próximo de zero:
Total time: 0.01s
Curioso como são os dados armazenados em cache dentro do Redis?
Execute o Redis CLI no modo interativo em uma nova janela de terminal:
$ redis-cli
Você deveria ver:
127.0.0.1:6379>
Execute ping
para garantir que tudo funcione corretamente:
127.0.0.1:6379> ping
PONG
Volte para o arquivo de configurações. Usamos o banco de dados Redis número 1: 'LOCATION': 'redis://127.0.0.1:6379/1',
. Então, execute select 1
para selecionar esse banco de dados e, em seguida, execute keys *
para visualizar todas as chaves:
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Podemos ver que o Django colocou uma chave de cabeçalho e uma cache_page
chave.
Para visualizar os dados reais em cache, execute o get
comando com a chave como argumento:
127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"
Você deve ver algo semelhante a:
"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n </div>\n <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n </button>\n </a>\n </div>\n Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."
Saia da CLI interativa uma vez feito:
127.0.0.1:6379[1]> exit
Pule para a seção "Testes de desempenho".
Comece adicionando pymemcache ao arquivo requirements.txt :
pymemcache==3.5.0
Instale as dependências:
(venv)$ pip install -r requirements.txt
Em seguida, precisamos atualizar as configurações em core/settings.py para habilitar o back-end do Memcached:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
Aqui, adicionamos o backend PyMemcacheCache e indicamos que o Memcached deve ser executado em nossa máquina local na porta 11211 do host local (127.0.0.1), que é a porta padrão para o Memcached.
Em seguida, precisamos instalar e executar o daemon Memcached. A maneira mais fácil de instalá-lo é através de um gerenciador de pacotes como APT, YUM, Homebrew ou Chocolatey, dependendo do seu sistema operacional:
# linux
$ apt-get install memcached
$ yum install memcached
# mac
$ brew install memcached
# windows
$ choco install memcached
Em seguida, execute-o em um terminal diferente na porta 11211:
$ memcached -p 11211
# test: telnet localhost 11211
Para obter mais informações sobre instalação e configuração do Memcached, consulte o wiki oficial .
Navegue até http://127.0.0.1:8000 em nosso navegador novamente. A primeira solicitação ainda levará os dois segundos completos, mas todas as solicitações subsequentes aproveitarão o cache. Portanto, se você atualizar ou pressionar o botão "Obter novos dados", a página deve carregar quase instantaneamente.
Como é o tempo de execução no seu terminal?
Total time: 0.03s
Se observarmos o tempo que leva para carregar a primeira solicitação versus a segunda solicitação (em cache) na barra de ferramentas de depuração do Django, ela será semelhante a:
Também na barra de ferramentas de depuração, você pode ver as operações de cache:
Gire o Gunicorn novamente e execute novamente os testes de desempenho:
$ ab -n 100 -c 10 http://127.0.0.1:8000/
Quais são as novas solicitações por segundo? É cerca de 36 na minha máquina!
Neste artigo, examinamos as diferentes opções internas para armazenamento em cache no Django, bem como os diferentes níveis de armazenamento em cache disponíveis. Também detalhamos como armazenar em cache uma visualização usando o cache Per-view do Django com Memcached e Redis.
Você pode encontrar o código final para ambas as opções, Memcached e Redis, no repositório cache-django-view .
--
Em geral, você deve procurar o cache quando a renderização da página estiver lenta devido à latência da rede de consultas ao banco de dados ou chamadas HTTP.
A partir daí, é altamente recomendável usar um backend de cache Django personalizado com Redis com um Per-view
tipo. Se você precisar de mais granularidade e controle, porque nem todos os dados no modelo são os mesmos para todos os usuários ou partes dos dados mudam com frequência, vá para o Cache de fragmento de modelo ou API de cache de baixo nível.
Fonte: https://testdrive.io