Going to production on a Raspberry Pi

Who am I?

Jérémy DERUSSÉ

 

Web Technical Leader

AptusHealth

 

@jderusse

How to get fast response

from a Symfony application?

you can't

How to get fast response

from a Symfony application?

your backend

you can't

Solution

 

  • using http shared cache
  • describe in RFC-2616 RFC-7234

Integration in Symfony

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;

/**
 * @Cache(smaxage="3600")
 */
public function indexAction()
{
    // ...
}

Advanced usage with FriendsOfSymfony/FOSHttpCacheBundle

Issue #1

Unsharable resource

Reason

  • private resources (ie. invoice, shopping cart, ...)
  • restricted area

Solution

Issue #2

Cache invalidation

Reason

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

Phil Karlton

Cache models

Validation model

  • etag
  • last-modified

 

drawback

  • Kernel booted
  • hard to implement

Expiration model

  • expires
  • cache-control

 

drawback

  • cannot be invalidate

cache model

In the real world

  • Life time is not predictable
  • Backend can't validate every cache HIT

cache model

In the real world

  • Life time is not predictable
  • Backend can't validate every cache HIT

BUT

  • Page is build on top of resources
  • Backend knows when resources change
  • Backend can tell varnish to invalidate things
  • Varnish can partialy invalidate cache based on queries

Varnish tags

curl "http://varnish.myapp.com"
curl \
    -X "BAN" \
    -H "X-Cache-Tags: Foo" \
    "http://varnish.myapp.com"
HTTP/1.1 200 OK
Cache-Control: public, s-maxage=3600
X-Cache-Tags: Foo,Bar
HTTP/1.1 200 OK
Cache-Control: public, s-maxage=3600
X-Cache-Tags: Foo
HTTP/1.1 200 OK
Cache-Control: public, s-maxage=3600
X-Cache-Tags: Bar,Qux

Varnish tags

curl "http://varnish.myapp.com/posts/42"
HTTP/1.1 200 OK
Cache-Control: public, s-maxage=86400
X-Cache-Tags: Post42,Author:12,Comment:314,Comment:1337

{
    "id": 42,
    "title": "My blog post.",
    "body": "Lorem Ipsum.",
    "author": {
        "id": 12,
        "username": "jderusse"
    },
    "comments": [
        {
            "id": 314,
            "message": "Wow such post"
        },
        {
            "id": 1337,
            "message": "much performance"
        }
    ]
}

Automate Tagging

Tagging Response

  1. Collect displayed entities
  2. Generate resource identifier
  3. Tag response 

Invalidate cache

Automate Tagging

Tagging Response - 1. Collect displayed entities

namespace App\EventListener;

use JMS\Serializer\EventDispatcher\Events;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;

class SerializationTagListener implements EventSubscriberInterface
{
    public function onPostSerialize(ObjectEvent $event)
    {
        $entity = $event->getObject();
        
        // TODO
    }

    public static function getSubscribedEvents()
    {
        return [
            [
                'event' => Events::POST_SERIALIZE,
                'format' => 'json',
                'method' => 'onPostSerialize',
            ],
        ];
    }
}

Automate Tagging

Tagging Response - 2. Generate resource identifier

namespace App\EventListener;

use App\Tag\TagExtractorInterface;
use FOS\HttpCacheBundle\Handler\TagHandler;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;

class SerializationTagListener implements EventSubscriberInterface
{
    private $tagExtractor;

    public function __construct(TagExtractorInterface $tagExtractor)
    {
        $this->tagExtractor = $tagExtractor;
    }

    public function onPostSerialize(ObjectEvent $event): void
    {
        $tags = $this->tagExtractor->extract($event->getObject());
    }

    //...
}

Automate Tagging

Tagging Response - 3. Tag response

namespace App\EventListener;

use App\Tag\TagExtractorInterface;
use FOS\HttpCacheBundle\Handler\TagHandler;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;

class SerializationTagListener implements EventSubscriberInterface
{
    private $tagExtractor;
    private $tagHandler;

    public function __construct(TagExtractorInterface $tagExtractor, TagHandler $tagHandler)
    {
        $this->tagExtractor = $tagExtractor;
        $this->tagHandler = $tagHandler;
    }

    public function onPostSerialize(ObjectEvent $event): void
    {
        $tags = $this->tagExtractor->extract($event->getObject());

        $this->tagHandler->addTags($tags);
    }

    //...
}

Automate Tagging

Tagging Response

  1. Collect displayed entities
  2. Generate resource identifier
  3. Tag response 

Invalidate cache

  1. Listen changes
  2. Generate resource identifier
  3. Call varnish

Automate Tagging

Invalidate Cache - 1. Listen changes

namespace App\EventListener;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;

class DoctrineInvalidationTagListener implements EventSubscriber
{
    public function getSubscribedEvents()
    {
        return [Events::onFlush];
    }

    public function onFlush(OnFlushEventArgs $eventArgs)
    {
        $uow = $eventArgs->getEntityManager()->getUnitOfWork();
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            // TODO
        }
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
            // TODO
        }
    }
}

Automate Tagging

Invalidate Cache - 2. Generate resource identifier

namespace App\EventListener;

use App\Tag\TagExtractorInterface;
use Doctrine\Common\EventSubscriber;

class DoctrineInvalidationTagListener implements EventSubscriber
{
    private $tagExtractor;

    public function __construct(TagExtractorInterface $tagExtractor)
    {
        $this->tagExtractor = $tagExtractor;
    }

    public function onFlush(OnFlushEventArgs $eventArgs)
    {
        $uow = $eventArgs->getEntityManager()->getUnitOfWork();
        $tags = [];
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            $tags += $this->tagExtractor->extract($entity);
        }
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
            $tags += $this->tagExtractor->extract($entity);
        }
    }
}

Automate Tagging

Invalidate Cache - 3. Call varnish

namespace App\EventListener;

use App\Tag\TagExtractorInterface;
use Doctrine\Common\EventSubscriber;
use FOS\HttpCache\Handler\TagHandler;

class DoctrineInvalidationTagListener implements EventSubscriber
{
    private $tagExtractor;

    public function __construct(TagExtractorInterface $tagExtractor, TagHandler $tagHandler)
    {
        $this->tagExtractor = $tagExtractor;
        $this->tagHandler = $tagHandler;
    }

    public function onFlush(OnFlushEventArgs $eventArgs)
    {
        // ...

        $this->tagHandler->invalidateTags(array_keys($tags));
    }
}

Automate Tagging

Tagging Response

  1. Collect displayed entities
  2. Generate resource identifier
  3. Tag response 

Invalidate cache

  1. Listen changes
  2. Generate resource identifier
  3. Call varnish

Enjoy

Demo

Software

  • marmelab/admin-on-rest
  • friendsofsymfony/rest-bundle
  • friendsofsymfony/http-cache-bundle
  • jms/serializer-bundle
  • symfony/symfony - env=dev
  • doctrine/doctrine-bundle - fetch=lazy

Demo

Demo

Hardware

  • cluster of 5 ARM machines
  • docker swarm kubernetes vanilla
  • 100$

Silver bullet?

Works well when

  • Read >> Write
  • Know resources used to build response

Drawback

  • Operations are not Atomic
  • Application must handle writes
  • Slows writes

Thank You

Questions

SfLive 2017

By Jérémy Derussé

SfLive 2017

  • 1,048