Kafka defaults that you should re-consider (I)

Image taken from http://www.htbdpodcast.com

There is a vast number of configuration options for Apache Kafka, mostly because the product can be fine-tuned to perform in various scenarios (e.g., low latency, high throughput, durability). These defaults span across brokers, producers and consumers (plus other sidecar products like Connect or Streams).

The guys at Kafka do their best to provide a comprehensive set of defaults that will just work, but some of them can be relatively dangerous if used blindly, as they might have unexpected side effects, or be optimized for a use case different to yours.

In this topic, I’d like to review the most obvious ones in the brokers’ side, explain what they do, why their default can be problematic and propose an alternative value.

Change these defaults

auto.create.topics.enable

Defaults to ‘true’. You definitively want to change this one to false. Applications should be responsible for creating their topics, which the correct configuration settings for the various use cases.

If you keep it true, some other configuration values kick in to fulfill the default topic configuration:

  • log.retention.hours: by default, logs will be retained 7 days. Think carefully if this default is good enough. Any data older than that is not be available when replaying the topic.

  • min.insync.replicas: Default to 1. As the documentation mentions, a typical configuration is replication-factor minus 1, meaning with a replication factor of 3, min.insync.replicas should be 2. The problem with 1 is it puts you in a dangerous position, where the cluster accepts messages for which you only have 1 copy. On the other hand, a value equal to the replication factor means losing one node temporarily stops your cluster from accepting values until the missing partition has rebalanced to a healthy node.

  • default.replication.factor: Default to 1. This is a bad value since it effectively creates only one copy of an auto-created topic. If the disk that stores a partition of this topic dies, the data is lost. Even if there are backups, the consumers don’t benefit from automatic rebalancing to other brokers that have copies of the partition, resulting in consumption interruptions. I would suggest a value like 3 and then fine-tune topics that require more or less, independently.

  • num.partitions: Default to 1. Another bad value. If a topic only has one partition, it can be consumed by only one instance of an application at a time, hindering any parallelization that we might hope to achieve using Kadka. While partitions are not free and Kafka clusters have a limit on how many they can handle, a minimum value of 3 partitions per topic seems like a safer and more sensible default.

offsets.retention.minutes

Defaults to 1400 minutes (24 hours). This is a dangerous default. Some applications might be idle over the weekend, meaning they don’t publish to Kafka during that period.

The morning after, if they restart before they consume from Kafka, the new instances don’t find any committed offsets for their consumer group, since they have expired.

At that point, the auto.offset.reset configuration in the consumer kicks in, sending the application to the earliest message, latest, or failing. In any case, this is not desirable.

The recommendation is to increase this value to something like 7 days for extra safeties.

Keep these defaults

auto.leader.rebalance.enable

Defaults to true. Unless you know what you’re doing, you don’t want to rebalance partitions manually. Let Kafka do it for you.

delete.topic.enable

Defaults to true. If you find yourself in a highly regulated environment, you might not be allowed to delete anything, ever. Otherwise, allowing topic deletion guarantees that you can get rid of data quickly and easily.

That is especially useful in development clusters. Don’t set this to false there; you will shoot yourself on foot.

log.flush.scheduler.interval.ms

Default to ‘never’ (represented as a ridiculously long number of ms). Kafka is so performant because it enables zero-copy data transfers from producers to consumers.

While that is a fantastic mechanism for moving tons of data quickly, the durability aspect can be a concern. To account for that, Kafka proposes using replication across nodes to guarantee the information is lost, instead of explicitly flushing messages to disk as they come. The result of that is a lack of certainty about when the messages are actually written to the disk.

You could effectively force Kafka to flush to disk using this and other configuration properties. However, you would most likely kill Kafka performance in the process. Hence, the recommendation is to keep the default value.

offsets.commit.required.acks

Defaults to ‘-1’, which means messages are not acknowledged by a leader until they the min.in.sync.replicas value for the topic is honored.

That is a safe default, falling on the side of durability, versus lower latency. You should consider particular configurations at the topic level, dependent on the nature of the stored information (e.g., ‘logs’ been a lower value than ‘orders’).

offsets.topic.num.partitions

Defaults to ’50’. Kafka automatically created the topic __consumer_offsets with this number of partitions. Since this is likely to be the busiest topic in your cluster, it’s a good idea to keep the number of partitions high so that the load is spread across as many nodes as possible.

__consumer_offsets cannot be changed for the lifetime of the cluster, so even if you are not planning to have 50 brokers in your cluster, it falls on the safe side to maintain this number as it is.

offsets.topic.replication.factor

Defaults to ‘3’. Similar to the previous value, but to configure how many copies of your __consumer_offsets you want. 3 copies is a safe default and should probably only be changed to rise to a more significant number.

More copies of the topic would make your cluster more resilient in the event of broker failure since there would be more followers ready to that the role of the fallen leader.

unclean.leader.election.enable

Defaults to ‘false’. Used to be ‘true’ by default because it was optimized for availability. In the case of a leader dying without any follower been up to date, the cluster to continue operating if this value is set to ‘true’. Unfortunately, data loss would result..

However, after Aphyr roasted Kafka for this data loss scenario, Kafka introduced this configuration value and eventually changed it to ‘false’ to prevent data loss. With this default, the cluster stops operating until a follower that was up to date with the fallen leader arises (potentially, the fixed leader itself), preventing any loss.

Summary

There are many more configuration values that play essential roles in the broker side, and we haven’t even mentioned any of the values in the client side (e.g., consumers, producers). In following posts, I’ll jump into those and describe what sensible defaults are and what you should think twice before blindly embracing.

WCF4 – Configuración simplificada

Creo que muchos de los que hemos trabajado con Windows Communication Foundation podemos estar de acuerdo en que, en mayor o menor medida, no resulta una tecnología fácil de configurar. Es cierto que aporta una gran flexibilidad en el desarrollo, pero a la hora de hacer el despliegue hay que ser cuidado con el web.config/app.config de turno, si no queremos llorar.

Seguramente esto ya había sido identificado por Microsoft como punto negro del framework, pues muchas de las mejoras que para el lanzamiento de .NET 4 va a haber respecto a WCF, se centran en simplificar dicha configuración, tirando sobretodo de valores por defecto.

En el próximo post vamos a ver las diferencias que habría entre configurar un servicio “típico” de WCF en 3.5 y hacerlo en 4.0.

Hola Mundo en 3.5

Vamos a empezar por hacernos un servicio sencillo con .NET 3.5 y Visual Studio 2008. No me detendré mucho en ello, porque el proceso es bien conocido por todos. Empezamos por el contrato de servicio:

/// <summary>
/// Service that says hello world.
/// </summary>
[ServiceContract]
public interface IHelloWorld
{
    /// <summary>
    /// Gets "Hello Word from X", where X is the name parameter.
    /// </summary>
    /// <param name="name">Who says hello.</param>
    /// <returns>String with hello world message.</returns>
    [OperationContract]
    string GetHelloWord(string name);
}

La implementación para este servicio podría ser algo como lo siguiente:

/// <summary>
/// Implementation of the IHelloWorld service contract.
/// </summary>
public class HelloWorld : IHelloWorld
{
    /// <summary>
    /// Gets "Hello Word from X", where X is the name parameter.
    /// </summary>
    /// <param name="name">Who says hello.</param>
    /// <returns>String with hello world message.</returns>
    public string GetHelloWord(string name)
    {
        return string.Format("Hello world from {0} - Request from {1}", name, OperationContext.Current.Channel.LocalAddress);
    }
}

Por último, lo que realmente nos ocupa en este post, la configuración. Digamos que necesitamos desplegar este servicio de forma que pueda ser consumido mediante un binding lo más interoperable posible (BasicHttpBinding es nuestro hombre) y otro que sea más específico para aplicaciones .NET y que nos dé más rendimiento (vamos a tirar por NetTcpBinding). Además, todavía necesitamos información sobre errores que haya en el servidor, puesto que no está totalmente terminado, y no nos vendría mal un punto MEX para poder crear nuestros proxies.

<system.serviceModel>
  <services>
    <service behaviorConfiguration="HelloWordService.ServiceBehavior"
             name="HelloWordService.HelloWorld">
      <!-- Binding para peticiones HTTP -->
      <endpoint address=""
                binding="basicHttpBinding"
                contract="HelloWordService.IHelloWorld" />
      <!-- Binding para peticiones TCP -->
      <endpoint address=""
                binding="netTcpBinding"
                contract="HelloWordService.IHelloWorld" />
      <!-- Binding para obtener metadatos -->
      <endpoint address="mex"
                binding="mexHttpBinding"
                contract="IMetadataExchange" />
      <host>
        <baseAddresses>
          <add baseAddress="http://localhost:9999/HelloWorldService"/>
          <add baseAddress="net.tcp://localhost:9998/HelloWorldService"/>
        </baseAddresses>
      </host>
    </service>
  </services>
  <behaviors>
    <serviceBehaviors>
      <behavior name="HelloWordService.ServiceBehavior">
        <!-- Activar la generación de metadatos para el binding MEX -->
        <serviceMetadata httpGetEnabled="True"/>
        <!-- Incluir información detallada para excepciones -->
        <serviceDebug includeExceptionDetailInFaults="False" />
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

Con esta configuración tendríamos a nuestro servicio escuchando en las dos direcciones base definidas, una para TCP y otra para HTTP. También tendríamos funcionando un endpoint MEX para poder generar nuestros proxies y consultar el fichero WSDL. Este servicio podríamos desplegarlo bien en IIS o bien hostearlo mediante una aplicación de consola. El resultado sería parecido, con la salvedad de que las baseAddress no son tenidas en cuenta por IIS, sino que se toma la ruta en la que se haga el despliegue de la aplicación. Recordad que es necesario configurar los bindings del Site que aloja a la aplicación web para poder tener funcionando el endpoint sobre net.tcp.

Para terminar esta parte, si nos creáramos un cliente de este servicio en su versión hosteada en IIS, en la parte de la configuración podríamos ver un fragmento como éste:

<client>
    <endpoint address=http://localhost/HelloWorldService binding="basicHttpBinding"
        bindingConfiguration="BasicHttpBinding_IHelloWorld" contract="RemoteServices.IHelloWorld"
        name="BasicHttpBinding_IHelloWorld" />
    <endpoint address="net.tcp://localhost/HelloWorldService"
        binding="netTcpBinding" bindingConfiguration="NetTcpBinding_IHelloWorld"
        contract="RemoteServices.IHelloWorld" name="NetTcpBinding_IHelloWorld">
        <identity>
            <userPrincipalName value=j.holguera@xxx.local />
        </identity>
    </endpoint>
</client>

Con estos dos endpoints definidos, podríamos consumir el servicio mediante los dos protocolos (HTTP y TCP), como muestra la siguiente imagen.

using (RemoteServices.HelloWorldClient proxy = new RemoteServices.HelloWorldClient("BasicHttpBinding_IHelloWorld"))
{
    Console.WriteLine("HelloWorld using HTTP: " + proxy.GetHelloWord("Javier"));
}

using (RemoteServices.HelloWorldClient proxy = new RemoteServices.HelloWorldClient("NetTcpBinding_IHelloWorld"))
{
    Console.WriteLine("HelloWorld using TCP: " + proxy.GetHelloWord("Javier"));
}

El resultado sería sendas cadenas de texto en las que, además de aparecer el mensaje de saludo, se nos indica la dirección desde la que se ha recibido la petición en el servicio, útil para ver que efectivamente consumimos la funcionalidad mediante los dos protocolos.

 ConsoleClient35

Como se puede ver, la única parte que realmente existe un buen  puñado de líneas es la configuración del servicio. Veamos cómo quedaría usando WCF 4.

Hola Mundo en WCF 4

Partamos de la base de que hemos creamos un proyecto WCF con Visual Studio 2010 y hemos replicado el servicio anterior, es decir, el contrato de servicio IHelloWorld y su implementación HelloWorld. Sólo nos quedaría, por tanto, configurar el servicio en el fichero app.config. Empezamos definiendo los bindings para HTTP y TCP. La configuración necesaria sería ésta:

<configuration>
</configuration>

¿No hay configuración? ¿Cómo es posible? Pues gracias a los Default Endpoints. Son endpoints “preconfigurados” que se cargan automáticamente para cada dirección base creada. Al llamarse al método Open del ServiceHost, ya sea automáticamente por parte de IIS o manualmente cuando se hostea el servicio en una aplicación de consola, se crean estos bindings predeterminados haciendo uso del método AddDefaultEndpoints. Un par de cuestiones a tener en cuenta:

  • Si se configura un endpoint en el fichero de configuración, ya no se hace efectiva la llamada a AddDefaultEndpoints.
  • Si aún así quisiéramos tener esos endpoints por defecto, siempre es posible llamar explícitamente al método AddDefaultEndpoints y añadir, a los endpoints definidos en el fichero de configuración, los que crea él por defecto.

Los bindings traen una configuración por defecto que se considera como más habitual. Por ejemplo, con una dirección base con protocolo http, el binding por defecto es basicHttpBinding. Esta nueva técnica ha sido bautizada como Default Protocol Mapping. En cualquier caso, este mapeo también es configurable; si quisiéramos que, por defecto, la direcciones con protocolo se resolvieran con un binding de tipo wsHttpBinding, sería necesario configurar lo siguiente en el app.config/web.config:

  <system.serviceModel>
    <protocolMapping>
      <add scheme="http" binding="wsHttpBinding"/>
    </protocolMapping>
  </system.serviceModel>

En este punto, tendríamos nuestro servicio hosteado en IIS. Nos quedaría activar los metadatos del servicio para generar el fichero WSDL y enviar información detallada en caso de error. Ambas no son configuraciones por defecto del servicio, por lo que tendríamos que editar el fichero app.config/web.config e introducir lo siguiente:

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

Si nos fijamos, veremos que no ha sido necesario ni darle un nombre al nuevo behavior del servicio, ni tampoco definir el servicio y asociarle el behavior, como hacíamos en 3.5. Esto se conoce como Default Behavior Configurations, configuraciones sin nombre que se van a aplicar a cualquier servicio que definamos. Tienen su contrapartida en los Default Binding Configurations, que realizan la misma función pero asociados a un tipo de binding.

 

Como se ha podido ver a lo largo del post, la configuración en WCF4, al menos la más habitual, se ha simplificado enormemente. Los nostálgicos de la sencillez de los servicios ASMX han perdido la principal razón para no dar el paso a WCF.

 

Bibliografía:

A Developer’s Introduction to Windows Communication Foundation 4

Descargas: