wetter.com - Relaunch with symfony2, Assetic, Varnish and Twig

After a lot of hard work, on march 10 a complete rebuild of wetter.com went online. The site is #1 or #2 in germany for weather forecasts since years with millions of visits/day. It was built completely new on PHP 5.4, Symfony 2, MySQL, SolR, Varnish and NginX. The task to re-build the whole site from scratch was given to our partner TFT (Tomorrow Focus Technologies) who invited 100 DAYS into the project to develop the symfony2 parts. This post describes the experiences we made when developing and launching this large scale application.

Background


www.wetter.com is a high traffic site. Alexa ranks it number 1101 globally and number 56 in germany. According to IVW online, it is on rank 14 in 02/2012 for germany. The site exists since more than 10 years. The original codebase became outdated and especially the layout was very 90ish. So wetter.com AG decided that a complete rewrite should be made using symfony2. TFT (Tomorrow Focus Technologies) took over all CMS- and search related work while 100 DAYS was developing a sf2 application that combines CMS content, search results, weather data and map server output to one highly scaleable, consistent frontend application. The following only describes our part of the development process. The TFT press release can be seen here (german).

Team Setup

Since symfony2 project experience was quite rare at the time the project started in april 2011, our team members had a background mainly in Symfony 1 and Zend Framework. We had played with SF2 before, but it was our first large scale symfony2 project. Since symfony2 was still beta when we started, we included a senior consultant of SensioLabs (FR) in the team for ramp-up and architecture. Later in the project we replaced him by a developer from newly founded sensiolabs (DE). 

The good parts


1. Twig


Symfony2 comes with Twig. Twig is the best template system i have ever seen so far. Coming from template systems like smarty i was really sceptical when i started using twig but once i got used to it (and this didn't take long!) i felt like an idiot having used other template systems before. Twig is very well coded, twig is fast, twig is easily extensible (we have done quite a few extensions - simple methods as well as completly new twig nodes) and it really helps you to organize your views in a better way using blocks. It even has a C extension to replace one of the expensive functions in twig (which we are of course using).

We also implemented our own loader. Some of the templates are being provided by the CMS and - no surprises here - doing your own loader is very simple and quickly done.

2. Symfony2 - The Kernelinterface


The core of the Symfony 2 full stack framework is the Kernelinterface.

function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);

This will return a response. This is surprisingly simple but a huge deal. Everything passes through this and especially note the $type. www.wetter.com has a lot of widgets which are reused all over the site. You simply specify

{% render 'MyBundle:Controller:widgetaction' {'param1':'foo'} %}

and symfony2 will create a subrequest for you, call the widget and include the subresponse in your masterresponse. This is cool because a widget is not something special but exactly the same as a normal (master) action. In symfony2 you are developing consistent actions.

This interface has another side effect. We are using varnish as a reverse proxy/http cache and varnish supports ESI (later more on this topic). Having every (sub)action represented as a request makes ESI a no brainer in your application:

{% render 'MyBundle:Controller:widgetaction' {'param1':'foo'} with {'standalone':true} %}

It is a simple as this. Symfony2 will render an ESI tag and you are done. Not having a varnish or any other system supporting ESI at hand? Take the built in HttpCache of symfony2 and you are caching parts of your site having written no code at all.

That's really cool.


3. More on caching in symfony2


Ok. Our widgets/subactions are being cached. How do i control expiry etc? HTTP/1.1 defines quite a few caching headers and varnish uses these headers to determine how long a response should be cached. Symfony2 is shipped with the SensioFrameworkExtraBundle. This allows you to control caching with annotations:

* @Cache(expires="+7 days")

When the response has been created a response listener in that bundle will intercept the response, analyze caching annotations and modify the headers so that varnish would cache the response for 7 days in this case. Simple, quickly done and extremely useful when doing a high traffic website :). The only drawback which we encountered when the site went live:

This is a static expiry time. For real high traffic it would be cool to add some kind of optional rand() as otherwise, many responses will expire at the same time.

It's not a "real" problem (otherwise we would have simply overrided the CacheListener) but would be a nice-to-have-out-of-the-box thing.

4. Symfony2 DI


Dependency Injection: Is it new? No it is not. It is available on Java platforms since around 2003/2004. In the PHP world it is new and currently a big hype and everyone just loves it and emphasises that you can't live without it.

It is certainly useful to de-couple components and re-use them across the application. We are mainly using it to override default symfony2 behaviour. For example we override the default symfony2 actionshelper (the one behind the {%render %} stuff above) as we need some specials in there.
<parameter key="templating.helper.actions.class">MyBundle\Templating\Helper\ActionsHelper</parameter>

And BAM. Symfony2 will now use your actions helper throughout your application. Symfony2 will take your DI configuration and compile it into PHP code. However there is still some magic. Some of the information needed for DI can't be expressed using configuration and the DependencyInjection/XYExtension.php classes handle this. Unfortunatly there are some cases where these Extensions do very hard stuff:

$container->getDefinition('service')->replaceArgument(1, $hardCoded);

What does it do? Well it takes the service definition and whatever you configured in your DI-config is ignored and an argument 1 in this case is replaced by whatever is $hardCoded - which results in a big "WTF!" and brings us back from configuration to coding :).

However in general i think the Symfony2 DI Container provides a very good DI implementation.

Furthermore when doing performance analysis the sf2 DI container has never been on my "need to hack it to save performance" -list as it is compiled to raw PHP code which is then cached by the byte code cache of PHP.

To debug the DI "console container:debug" is your invaluable source of information.

5. varnish


For those of you who don't know: varnish is a reverse proxy and http cache and we are using it extensivly. In fact the site could not live without it.

As said above varnish integrates very well with sf2 and really helps a lot. Although sf2 is quite fast, a site like this couldn't live without a strong caching server (unless you want to spend huge amounts of money on web frontends and database servers).

What i really like about varnish is that you program it (using the varnish configuration language VCL). This way all the specialties of the site were easily implemented into varnish. I can't imagine how that should have worked if varnish had a normal configuration syntax.

Varnish also allows us to include C plugins:

C{
    printf("I am C Code\n");
}C

We did not want to use PHP sessions in this project as that would have been a major performance drawback. What we did instead: We implemented client side signed cookies as a C plugin in varnish (more about that in a separate post somewhen later). This way, varnish can cache the content for different user levels and distinguish between them without the need to invoke PHP.

6. Debug toolbar


The debug toolbar in symfony2 is a really really cool invention. It helps a lot during development and can be easily extended.

So far we did 4 custom toolbar items (SoLR, CMS interaction, Webservices, ESI Requests) and the toolbar is absolutely invaluable. As the caching (and the underlying caching strategy) for example is a crucial part of the project, we implemented an ESI debug toolbar item which tells us how the page and the ESI requests will be cached. When doing something wrong (i.e. breaking caching strategy) you are seeing it on the next page refresh. Database queries etc. are displayed and you get a very quick and usable overview on why the page might be slow. Need details? Click on the panel item and you will see an in depth overview of what is really happening. Existing items may be modified quite easily as well. For example the default Doctrine panel item displays the number of queries but you don't see the total time spent for these queries. So we extended the default toolbar item template to display the time as well.

All in all the debug toolbar is a HUGE win in sf2.

7. php


Originally we started the project with php 5.3 but as php 5.4 came out we did some benchmarks and pages loaded 10-30% faster. So we upgraded to PHP 5.4.

Furthermore we are using NginX with php-fpm. Apart from the better configuration possibilities one of the really cool things about php-fpm is the possibility to reload it gracely:

As we need every bit of performance we set APC so that it doesn't check for file changes. This works very well as long as the code is really constant. However when deploying a new version this is a problem. The "old" fastcgi php can only be restarted meaning currently active requests will be killed. With php-fpm no request will be killed.


The bad parts


1. Assetic


Assetic is very cool in theory and in production. But in the development mode of sf2 it really is a pain. As the project grew the number of assets grew as well. At some point the time needed to render a page took around 60 seconds and everybody in the project at least once debugged what is really going on there.
Let's have a look at a possible twig template with assets:

{% block head %}
    {% stylesheets filter="staticfilter,?yui_css" output="css/site.css"
       '@MyBundle/Resources/assets/css/main.css'
       '@MyBundle/Resources/assets/css/stuff1.css'
       '@MyBundle/Resources/assets/css/stuff2.css'
    %}
       <link href="{{ asset_url }}" rel="stylesheet" />
    {% endstylesheets %}
    {% javascripts
       '@MyBundle/Resources/assets/js/jquery-1.7.1.js'
       '@MyBundle/Resources/assets/js/jquery-ui.js'
       '@MyBundle/Resources/assets/js/jquery.ui.datepicker-de.js'
       filter="?yui_js" output="js/site.js"
    <script src="{{ asset_url }}" type="text/javascript"></script>
    {% endjavascripts %}
{% endblock %}

What assetic (or the AsseticBundle to be precise) will do in the dev environment here is to scan ALL twig files of the WHOLE project for changes to build up a list of assets. This is a very expensive operation especially when working via vmwares hgfs.

But it gets even worse: By default sf2 will make not combine the assets in the dev environment. So this will will generate 6 requests which are ALL slow.

The sf2 manual is covering this issue and suggests to disable the controller and instead dump the assets manually every time (which is slow as well).

This is just unusable when doing javascript or CSS stuff.

IMHO every bundle should define one file where all assets are listed and then the templates should only reference on these. However this would be more or less a rewrite of the AsseticBundle.

Our quick fix to make it work for us was to add "combine=true" to all asset declarations. This way assetic combines all assets to one big asset even in the dev environment. This saves most of the time.

2. Varnish


Huh? Varnish has been in the good parts? Yes it is as it is a topic here. As caching becomes more and more complex you find yourself debugging in varnish more often than you want. Especially when doing lots of ESI requests debugging will become a real pain. Varnishlog will help you of course but we had a lot of situations where we were speculating what might be going on or we were even debugging in the C code. Varnish is working very well but be sure to reserve enough trial/error-time if you want to implement sophisticated caching strategies.

3. SecurityBundle/Components


As mentioned before we are doing authentication via client side signed cookies and so we had to change the standard authentication mechanisms. This is unfortunately not as easy as one might think (which is ok as we really did special stuff) but what i would like to point out here is that the SecurityBundle and security components are really really hard to understand. I have implemented and used numerous authentication and login mechanisms in different projects and this one really took longer than expected.
There are several components which rely on a session, lots of stuff in the SecurityBundle relies on events (which is cool but really hard to debug). It was a pain and it seems to me that the security stuff is a bit too overengineered. I mean - i just want to do a login. How hard can that be.

Conclusion


The project was real fun, the result is quite appealing and we all learned a lot during the project. We used a lot of new stuff and i think this is one of the biggest symfony2 projects so far proving that symfony2 can be used to do big projects. As every framework, symfony2 has a few rough edges but it is mostly fun and works as intended. Don't be afraid to use symfony2 and even if you cannot use the full stack framework: Have a look at the symfony2 components which can be used independently. Actually the first thing i do when starting a new non sf2 project is to include the sf2 ClassLoader. Seems i have become a fan...

BTW: The people at wetter.com are a very cool team and nice to work with. And they are searching for a PHP/JS developer. Check out their Job Offer here.


Auf Facebook teilen

« Plat_Forms 2012 announcement - Varnish and saint mode: "no backend connection" »