Caching strategy

Phil Karlton

"There are only two hard things in Computer Science: cache invalidation and naming things."

A cache is a component that stores data temporarily so that future requests for that data can be served faster. This temporal storage is used to shorten our data access times, reduce latency, and improve I/O. We can improve the overall performance using different types of caches in our microservice architecture. Let's take a look at this subject.

General caching strategy

To maintain the cache, we have algorithms that provide instructions which tell us how the cache should be maintained. The most common algorithms are as follows:

  • Least Frequently Used (LFU): This strategy uses a counter to keep track of how often an entry is accessed and the element with the lowest counter is removed first.
  • Least Recently Used (LRU): In this case, the recently-used items are always near the top of the cache and when we need some space, elements that have not been accessed recently are removed.
  • Most Recently Used (MRU): The recently-used items are removed first. We will use this approach in situations where older items are more commonly accessed.

The perfect time to start thinking about your cache strategy is when you are designing each of the microservices required by your app. Every time your service returns data, you need to ask to yourself some questions:

  • Are we returning sensible data we can't store at any place?
  • Are we returning the same result if we keep the input the same?
  • How long can we store this data?
  • How do we want to invalidate this cache?

You can add a cache layer at any place you want in your application. For example, if you are using Percona/MySQL/MariaDB as a data storage, you can enable and set up the query cache correctly. This little setup will give your database a boost.

You need to think about cache even when you are coding. You can do lazy loading on objects and data or build a custom cache layer to improve the overall performance. Imagine that you are requesting and processing data from an external storage, the requested data can be repeated several times in the same execution. Doing something similar to the following piece of code will reduce the calls to your external storage:

    <?php 
    class MyClass 
    { 
       protected $myCache = []; 
 
       public function getMyDataById($id) 
       { 
           if (empty($this->myCache[$id])) { 
               $externalData = $this->getExternalData($id); 
               if ($externalData !== false) { 
                   $this->myCache[$id] = $externalData; 
               } 
           } 
 
                return $this->myCache[$id]; 
       } 
    } 

Note that our examples omit big chunks of code, such as namespaces or other functions. We only want to give you the overall idea so that you can create your own code.

In this case, we will store our data in the $myCache variable every time we make a request to our external storage using an ID as the key identifier. The next time we request an element with the same ID as a previous one, we will get the element from $myCache instead of requesting the data from the external storage. Note that this strategy is only successful if you can reuse the data in the same PHP execution.

In PHP, you have access to the most popular cache servers, such as memcached and Redis; both of them store their data in a key-value format. Having access to these powerful tools will allow us to increase the performance of our microservices application.

Let's rebuild our preceding example using Redis as our cache storage. In the following piece of code, we will assume that you have a Redis library available in your environment (for example, phpredis) and a Redis server running:

    <?php 
    class MyClass 
    { 
      protected $myCache = null; 
 
      public function __construct() 
      { 
        $this->myCache  = new Redis(); 
        $this->myCache->connect('127.0.0.1',  6379); 
      } 
 
      public function getMyDataById($id) 
      { 
        $externalData = $this->myCache->get($id); 
          if ($externalData === false) { 
            $externalData = $this->getExternalData($id); 
            if ($externalData !== false) { 
              $this->myCache->set($id, $externalData); 
            } 
          } 
 
          return $externalData; 
      } 
    }

Here, we connected to the Redis server first and adapted the getMyDataById function to use our new Redis instance. This example can be more complicated, for example, by adding the dependence injection and storing a JSON in the cache, among other infinite options. One of the benefits of using a cache engine instead of building your own is that all of them come with a lot of cool and useful features. Imagine that you want to keep the data in cache for only 10 seconds; this is very easy to do with Redis--simply change the set call with $this->myCache->set($id, $externalData, 10) and after ten seconds your record will be wiped from the cache.

Something even more important than adding data to the cache engine is invalidating or removing the data you have stored. In some cases, it is fine to use old data but in other cases, using old data can cause problems. If you do not add a TTL to make the data expire automatically, ensure that you have a way of removing or invalidating the data when it is required.

Keep this example and the previous one in mind, we will be using both strategies in our microservice application.

As a developer, you don't need to be tied to a specific cache engine; wrap it, create an abstraction, and use that abstraction so that you can change the underlying engine at any point without changing all the code.

This general caching strategy can be used in any scope of your application--you can use it inside the code of your microservice or even between microservices. In our application example, we will deal with secrets; their data doesn't change very often, so we can store all this information on our cache layer (Redis) the first time they are accessed.

Future petitions will obtain the secrets' data from our cache layer instead of getting it from our data storage, improving the performance of our app. Note that the service that retrieves and stores the secrets data is the one that is responsible for managing this cache.

Let's see some other caching strategies that we will be using in our microservices application.

HTTP caching

This strategy uses some HTTP headers to determine whether the browser can use a local copy of the response or it needs to request a fresh copy from the origin server. This cache strategy is managed outside your application, so you don't have much control over it.

Some of the HTTP headers we can use are as listed:

  • Expires: This sets a time in the future when the content will expire. When this point in the future is reached, any similar requests will have to go back to the origin server.
  • Last-modified: This specifies the last time that the response was modified; it can be used as part of your custom validation strategy to ensure that your users always have fresh content.
  • Etag: This header tag is one of the several mechanisms that HTTP provides for web cache validation, which allows a client to make conditional requests. An Etag is an identifier assigned by a server to a specific version of a resource. If the resource changes, the Etag also changes, allowing us to quickly compare two resource representations to determine if they are the same.
  • Pragma: This is an old header, from the HTTP/1.0 implementation. HTTP/1.1 Cache-control implements the same concept.
  • Cache-control: This header is the replacement for the expires header; it is well supported and allows us to implement a more flexible cache strategy. The different values for this header can be combined to achieve different caching behaviors.

The following are the available options:

  • no-cache: This says that any cached content must be revalidated on each request before being sent to a client.
  • no-store: This indicates that the content cannot be cached in any way. This option is useful when the response contains sensitive data.
  • public: This marks the content as public and it can be cached by the browser and any intermediate caches.
  • private: This marks the content as private; this content can be stored by the user's browser, but not by intermediate parties.
  • max-age: This sets the maximum age that the content may be cached before it must be revalidated. This option value is measured in seconds, with a maximum of 1 year (31,536,000 seconds).
  • s-maxage: This is similar to the max-age header; the only difference is that this option is only applied to intermediary caches.
  • must-revalidate: This tag indicates that the rules indicated by max-age, s-maxage, or the expires header must be obeyed strictly.
  • proxy-revalidate: This is similar to s-maxage, but only applies to intermediary proxies.
  • no-transform: This header tells caches that they are not allowed to modify the received content under any circumstances.

In our example application, we will have a public UI that can be reached through any web browser. Using the right HTTP headers, we can avoid requests for the same assets again and again. For example, our CSS and JavaScript files won't change frequently, so we can set up an expiry date in the future and the browser will keep a copy of them; the future requests will use the local copy instead of requesting a new copy.

You can add an expires header to all .jpg, .jpeg, .png, .gif, .ico, .css, and .js files with a date of 123 days in the future from the browser access time in NGINX with a simple rule:

    location ~*  .(jpg|jpeg|png|gif|ico|css|js)$ { 
        expires 123d; 
    } 

Static files caching

Some static elements are very cache-friendly, among them you can cache the following ones:

  • Logos and non-auto generated images
  • Style sheets
  • JavaScript files
  • Downloadable content
  • Any media files

These elements tend to change infrequently, so they can be cached for longer periods of time. To alleviate your servers' load, you can use a Content Delivery Network (CDN) so that these infrequently changed files can be served by these external servers.

Basically, there are two types of CDNs:

  • Push CDNs: This type requires you to push the files you want to store. It is your responsibility to ensure that you are uploading the correct file to the CDN and the pushed resource is available. It is mostly used with uploaded images, for example, the avatar of your user. Note that some CDNs can return an OK response after a push, but your file is not really ready yet.
  • Pull CDNs: This is the lazy version, you don't need to send anything to the CDN. When a request comes through the CDN and the file is not in their storage, they get the resource from your server and it stores it for future petitions. It is mostly used with CSS, images, and JavaScript assets.

You need to have this in mind when you are designing your microservice application because you may allow your users to upload some files.

Where are you going to store these files? If they are to be public, why not use CDN to deliver these files instead of them being gutted from your servers.

Some of the well-known CDNs are CloudFlare, Amazon CloudFront, and Fastly, among others. What they all have in common is that they have multiple data centers around the world, allowing them to try to give you a copy of your file from the closest server.

By combining HTTP with static files caching strategies, you will reduce the asset requests on your server to a minimum. We will not explain other cache strategies, such as full page caching; with what we have covered, you have enough to start building a successful microservice application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.39.142