Cross the hedgehog (Marathon) with the snake (Spring Cloud). Episode 2

  • Tutorial

In the first episode , we managed to pull information from Mesos Marathon directly into the Spring Cloud bins . At the same time, we had the first problems, one of which we will analyze in the current part of the story. Let's recall our Marathon connection configuration :


spring:  
    cloud:
        marathon:
            scheme: http       #url scheme
            host: marathon     #marathon host
            port: 8080         #marathon port

What problems do we see here? First, we do not have any authorization when connecting, which is strange for industrial use. The second - we can specify only one host and port. In principle, one could try to push several masters into one balancer or DNS, but we would like to avoid this additional point of failure. How to do it is written under a cat.


Password all over the head


There are two authorization schemes available to us: Basic and Token . Basic authorization is so commonplace that almost every developer has met it. Its essence is painfully simple. Take username and password. Glue them through :. We encode in Base64. Add a title Authorizationwith a value . ProfitBasic


A token is a bit more complicated. In an open-source implementation, it is not available, so this method is suitable for those who use DC / OS . To do this, simply add a slightly different authorization header:


Authorization: token=

Thus, we can add several properties we need to our configuration:


spring:  
    cloud:
        marathon:
            ...
            token: dcos_acs_token
            username: marathon
            password: mesos

And then we can be guided by simple priorities. If a token is specified, then we take it. Otherwise, take the username and password and do basic authorization. Well, in the absence of both, we create a "naked" client.


Feign.Builder builder = Feign.builder()
        .encoder(new GsonEncoder(ModelUtils.GSON))
        .decoder(new GsonDecoder(ModelUtils.GSON))
        .errorDecoder(new MarathonErrorDecoder());
if (!StringUtils.isEmpty(token)) {
    builder.requestInterceptor(new TokenAuthRequestInterceptor(token));
}
else if (!StringUtils.isEmpty(username)) {
    builder.requestInterceptor(new BasicAuthRequestInterceptor(username,password));
}
builder.requestInterceptor(new MarathonHeadersInterceptor());
return builder.target(Marathon.class, baseEndpoint);

The Marathon client is implemented using the Feign http client , into which any interceptors can be easily added . In our case, they add the necessary http headers to the request. After that, builder will construct an object for us on the interface in which possible requests are declared declaratively:


public interface Marathon {
    // Apps
    @RequestLine("GET /v2/apps")
    GetAppsResponse getApps() throws MarathonException;
    //Other methods
}

So, the warm-up is over, now let's take a more difficult task.


Failover client


If we have deployed an industrial installation of Mesos -a and Marathon -a, then the number of masters from whom we can read the data will be more than one. Moreover, some of the masters may accidentally be unavailable, slow down, or be in a state of upgrade. Failure to obtain information will either lead to obsolescence of information on clients, and, therefore, at some point to milk shots. Or, say, when updating the application software, we won’t get a list of instances at all and will refuse to service the client. All this is not good. We need client balancing of requests.


It is logical that Ribbon should be chosen as a candidate for this role , since it is used in client balancing of requests inside Spring Cloud . We’ll talk more about query balancing strategies in the next episodes, but for now we will restrict ourselves to the most basic functionality that we need to solve the problem.


The first thing we need to do is implement the balancer in our feign client:


Feign.Builder builder = Feign.builder()
            .client(RibbonClient.builder().lbClientFactory(new MarathonLBClientFactory()).build())
            ...;

Looking at the code begs a logical question. What is lbClientFactory and why do we do our own? In short, this factory is designing us a client request balancer. By default, the created client is deprived of one feature we need: a repeated request in case of problems during the first request. To make it possible to make retry , we will add it when constructing the object:


public static class MarathonLBClientFactory implements LBClientFactory {
    @Override
    public LBClient create(String clientName) {
        LBClient client = new LBClientFactory.Default().create(clientName);
        IClientConfig config = ClientFactory.getNamedConfig(clientName);
        client.setRetryHandler(new DefaultLoadBalancerRetryHandler(config));
        return client;
    }
}

Do not be afraid that our retry handler has the Default prefix . Inside it has everything we need. And here we come to the question of the configuration of all this stuff.


Since there can be several clients in our application, and the client for Marathon is only one of them, the settings have a certain pattern of the form:


client_name.ribbon.property_name

In our case:


marathon.ribbon.property_name

All the properties for a client balancer stored in the Configuration Manager - Archarius -e . In our case, these settings will be stored in memory, because we add them on the fly. To do this, we will add an auxiliary method in our modified client setMarathonRibbonProperty, inside which we will set the property value:


ConfigurationManager.getConfigInstance().setProperty(MARATHON_SERVICE_ID_RIBBON_PREFIX + suffix, value);

Now before creating a feign client we need to initialize these settings:


setMarathonRibbonProperty("listOfServers", listOfServers);
setMarathonRibbonProperty("OkToRetryOnAllOperations", Boolean.TRUE.toString());
setMarathonRibbonProperty("MaxAutoRetriesNextServer", 2);
setMarathonRibbonProperty("ConnectTimeout", 100);
setMarathonRibbonProperty("ReadTimeout", 300);

What is interesting here. First of all listOfServers. In fact, this is an enumeration of all possible pairs host:porton which Marathon masters are located , separated by a comma. In our case, we simply add the ability to specify them in our configuration:


spring:  
    cloud:
        marathon:
            ...
            listOfServers: m1:8080,m2:8080,m3:8080

Now each new request to the master will go to one of these three servers.


For retry to work, we must set the value OkToRetryOnAllOperationsto true.


The maximum number of repetitions is set using the option MaxAutoRetriesNextServer. Why is there an indication of NextServer in it ? Everything is simple. Because there is another option MaxAutoRetriesthat indicates how many times you need to try to call the first server again (the one that was selected for the very first request). By default, this property matters 0. That is, after the first failure, we will go to ask for data from the next candidate. It is also worth remembering that MaxAutoRetriesNextServerindicates the number of attempts minus attempts to request data from the first server.


Well, not to hang on the line indefinitely, set the properties ConnectTimeoutand ReadTimeoutwithin reasonable limits.


Total


In this part, we have made our client to Marathon more customizable and fault tolerant. Moreover, using ready-made solutions, we did not have to fence too many gardens. But we are still far from completing the work, because we still do not have the most interesting part of the ballet - client-side query balancing for application software.


To be continued


Also popular now: