Скрестить ежа (Marathon) с ужом (Spring Cloud). Эпизод 2

habr.png

В первом эпизоде у нас получилось вытянуть информацию из Mesos Marathon прямиком в бины Spring Cloud-а. Вместе с тем у нас появились первые проблемы, одну из которых мы разберём в текущей части повествования. Давайте вспомним нашу конфигурацию подключения к Marathon-у:


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


Какие мы видим тут проблемы? Первая — у нас нет никакой авторизации при подключении, что для промышленного использования странно. Вторая — мы можем указать только один хост и порт. В принципе, можно было бы попробовать упихнуть несколько мастеров за один балансировщик или DNS, но эту дополнительную точку отказа хотелось бы избежать. Как это сделать написано под катом.


Пароль всему голова


Существуют две доступные нам схемы авторизации: Basic и Token. Basic-авторизация настолько банальна, что с ней встречался почти каждый разработчик. Суть её до боли проста. Берём логин и пароль. Склеиваем их через :. Кодируем в Base64. Добавляем заголовок Authorization со значением Basic . Профит.


С токеном несколько сложнее. В open-source реализации он недоступен, поэтому такой способ подойдёт тем, кто использует DC/OS. Для этого нужно просто добавить чуть по другому сформированный авторизационный заголовок:


Authorization: token=


Таким образом к нашей конфигурации мы можем добавить несколько нужных нам свойств:


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


И далее мы можем руководствоваться простыми приоритетами. Если указан токен, то берём его. Иначе берём логин и пароль и делаем basic-авторизацию. Ну и при отсутствии того и другого создаём «голый» клиент.


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);


Клиент к Marathon реализован с помощью http-клиента Feign, в который можно легко добавлять любые interceptor-ы. В нашем случае они добавляют нужные http-заголовки в запрос. После этого builder сконструирует нам объект по интерфейсу, в котором декларативно объявлены возможные запросы:


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

    //Other methods
}


Итак, разминка закончилась, теперь займёмся более сложной задачей.


Отказоустойчивый клиент


Если у нас развёрнута промышленная инсталляция Mesos-a и Marathon-а, то количество мастеров из которых мы можем читать данные будет больше одного. Более того, какой-то из мастеров может случайно быть недоступен, тормозить, или быть в состоянии апгрейда. Невозможность получить информацию приведёт либо к устареванию информации на клиентах, и, следовательно, в какой-то момент к выстрелам в молоко. Либо, скажем при апдейте прикладного ПО, мы вообще не получим список инстансов и будет отказывать клиенту в обслуживании. Всё это не есть хорошо. Нам нужна клиентская балансировка запросов.


Логично, что кандидатом на эту роль должен быть выбран Ribbon, так как он используется в клиентской балансировке запросов внутри Spring Cloud-а. Подробнее о стратегиях балансировки запросов мы поговорим в следующих эпизодах, а пока ограничимся самой базовой функциональностью, которая нужна нам для решения задачи.


Первое, что нам нужно сделать, это внедрить балансировщик в наш feign-клиент:


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


Глядя на код напрашивается логичный вопрос. Что такое lbClientFactory и зачем мы делаем свою? Если кратко, то эта фабрика конструирует нам клиентский балансировщик запросов. По-умолчанию создаваемый клиент лишён одной нужной нам фичи: повторного запроса в случае проблем при первом запросе. Чтобы была возможность сделать retry, мы добавим его при конструировании объекта:


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;
    }
}


Не стоит пугаться что наш retry-обработчик имеет префикс Default. Внутри него есть всё, что нам нужно. И тут мы подходим к вопросу конфигурации всего этого добра.


Так как клиентов у нас в приложении может быть несколько, и клиент для Marathon-а лишь один из них, то настройки имеют определённый шаблон вида:


client_name.ribbon.property_name


В нашем случае:


marathon.ribbon.property_name


Все свойства для клиентского балансировщика хранятся в менеджере конфигураций — Archarius-е. В нашем случае эти настройки будут храниться в памяти, потому что добавляем мы их «на лету». Для этого в нашем модифицированном клиенте добавим вспомогательный метод setMarathonRibbonProperty, внутри которого будем устанавливать значение свойства:


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


Теперь перед тем, как создать feign-клиент нам нужно проинциализировать эти настройки:


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


Что тут есть интересного. Во-первых это listOfServers. Фактически это перечисление всех возможных пар host:port, на которых расположены мастера Marathon-а, разделённые запятой. В нашем случае мы просто добавим в нашу конфигурацию возможность их указать:


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


Теперь каждый новый запрос к мастеру будет идти на один из этих трёх серверов.


Чтобы retry вообще заработал мы должны выставить значение OkToRetryOnAllOperations в true.


Максимальное количество повторение задаём с помощью опции MaxAutoRetriesNextServer. Почему в ней есть указание на NextServer? Всё просто. Потому что есть ещё опция MaxAutoRetries, которая указывает сколько раз нужно попробовать повторно вызвать первый сервер (тот что был выбран для самого первого запроса). По-умолчанию это свойство имеет значение 0. То есть после первой неудачи мы пойдём просить данные у следующего кандидата. Также стоит помнить, что MaxAutoRetriesNextServer указывает на количество попыток за вычетом попыток запросить данные с первого сервера.


Ну и, чтобы не висеть на линии бесконечно долго, установим свойства ConnectTimeout и ReadTimeout в разумных пределах.


Итого


В этой части мы сделали наш клиент к Marathon-у более кастомизуемым и отказоустойчивым. Причём воспользовавшись уже готовыми решениями нам не пришлось городить слишком много огорода. Но мы всё ещё далеки от завершения работы, потому что самой интересной части балета — клиентской балансировки запросов для прикладного ПО — у нас пока ещё нет.


Продолжение следует

© Habrahabr.ru