История «Питон на помощь», которая никогда не устареет

Это ощущение, когда ваше приложение настолько велико - и настолько важно, - что вы просто знаете, что в ближайшее время потребуются значительные объемы памяти. «Инструмент« циклопического »анализа вот-вот будет уничтожен другим направлением бизнеса, приобретением и стремительным уровнем органического роста одновременно. Да, и время удвоения было сокращено вдвое ». Похоже, кому-то нужно построить новую ферму серверов баз данных.

Со стороны идея подготовки фермы серверов баз данных NoSQL в такой ситуации кажется довольно простой. Выделите серверы. Выделите хранилище. Соберите программу. Все ли остальные конфигурации баз данных необходимы для соответствия корпоративным стандартам, стандартам InfoSec и стандартам производственной поддержки. Разве не в этом вся суть DevOps? Это просто «Эй! Престо! База данных! » Верно?

Однако если углубиться в детали, все не так просто. И да, это превратится в историю «Питон спешит на помощь».

Что задействовано? Нет, правда?

Базовая схема подготовки фермы серверов баз данных требует нескольких ресурсов. Самый главный ресурс - терпение. Это, как правило, большие серверы с ОГРОМНЫМ дисковым хранилищем. (ОЗУ от 30 до 60 ГБ.) Для некоторых баз данных, таких как Cassandra, кажется наиболее разумным строить узлы последовательно. В этом сценарии первый и последний узлы обрабатываются иначе, чем другие, причем первый узел содержит начальную информацию, необходимую другим. Поэтому кажется самым простым не создавать детали базы данных до тех пор, пока все узлы не будут построены и не начнут разделять роли, пользователей и другие определения. Это сокращает время для создания следующего выпуска «Циклопической базы данных».

Если смотреть абстрактно, мы собираемся сделать следующие вещи для создания каждого отдельного узла серверной фермы:

1. Выполните рецепт Chef на сервере инициализации для создания каждого узла.

2. Обновите доменное имя.

3. Создайте записи в любой базе данных управления конфигурацией, которая используется для отслеживания активов.

4. Запланировать резервное копирование (если необходимо)

По большей части это просто вызовы API; они довольно просты, особенно в Python 3. (отложите эту мысль на потом) Проблема рецепта Chef - это то, где проявляется настоящая работа. Нам нужно найти соответствующий баланс между использованием простых сценариев Chef и сохранением гибкости. Кроме того, в рецептах должно быть достаточно параметров, чтобы мы могли не изменять их каждый раз, когда что-то меняется в том, что мы создаем.

Проблема в том, что настройки среды меняются очень быстро, потому что это не стандартные установки: они настроены для нужд нашего предприятия. Вчерашняя передовая практика для предприятий сегодня - это «недостаточно эффективная» практика. Мы не хотим, чтобы рецепт Chef постоянно изменялся. Одно из альтернативных решений - динамический сбор данных. Но рецепт, который собирает данные динамически, означает, что у нас могут возникнуть проблемы с восстановлением части динамических данных для восстановления существующего сервера.

Где золотая середина?

Обретение гибкости

Создание рецептов Chef заставило нас увидеть, что есть большое количество параметров, которые определяют Chef. Так много, что нам потребовались внешние инструменты для сбора данных и создания из них чего-то полезного. Параметры делятся на несколько сегментов:

- Вещи, которые должен предоставить пользователь. Названия приложений («Cyclopean»). Ориентировочные размеры («1 ТБ»). Их сфера деятельности и информация об их МВЗ. Ясно, что это движет запросом на построение базы данных.

- Вещи, которые необходимо предоставить предприятию. Сведения о конфигурации облака, подсети и другие сведения на корпоративном уровне. Некоторые из этих деталей конфигурации часто меняются. Есть API для получения некоторых из них. Выполнение этих поисков в рецепте Chef казалось неправильным, потому что рецепт становится динамическим и теряет свою идемпотентность.

- Вещи, которые необходимо определить инженерам баз данных. Соглашения об именах, предпочтения хранения, расчеты размеров и другие сведения об окружающей среде. Это могло быть частью самого рецепта шеф-повара.

- Вещи, которые уникальны для каждого продукта базы данных. Например, уникальный способ определения пользователей, ролей, привилегий, подключений к корпоративному LDAP. Нам нужна гибкость, чтобы это варьировалось в зависимости от сферы деятельности. Вместо того, чтобы создавать множество вариантов рецептов, мы предпочли бы использовать их как часть общего набора параметров для сборки.

- Вещи, которые представляют собой простые буквальные значения, но будут меняться чаще, чем потребуется менять рецепт. Например, номера версий дистрибутивов изменятся, но их необходимо параметризовать. Атрибуты шеф-повара уже предлагают приятные способы справиться с этим. Таблица приоритетов атрибутов из 15 шагов показывает, как рецепт может предоставлять атрибуты по умолчанию, файл атрибутов может предоставлять значения, а среда может переопределять значения.

Наша цель - свести к минимуму настройку рецепта Chef, и опыт показывает, что это связано с множеством параметров. Для некоторых из наших рецептов установки базы данных NoSQL это может быть до 200 различных значений.

Python дает нам удобный способ собирать данные из различных источников и создавать необходимые атрибуты и параметры конфигурации, чтобы можно было использовать относительно стабильный рецепт Chef. (помните, где мы говорили во вступлении, что это станет историей «Питон спешит на помощь»?) Идея состоит в том, чтобы кэшировать параметры с рецептом Chef, позволяя нам перестроить любой узел в любое время. Наличие файла статического шаблона дает нам необходимую идемпотентность в нашей подготовке.

Следующий вопрос, который необходимо решить, - это разработка приложения Python для поддержки повторяемых, но гибких сборок.

Наивный дизайн со словарями Python

Давайте поговорим о Python. Параметры рецепта можно сериализовать как большой документ JSON (или YAML). Python упрощает это, если мы создадим структуру словарей-словарей, которую можно тривиально сериализовать как файл JSON / YAML. (Это тривиальный уровень «json.dump (объект, файл)».)

Как мы построим этот словарь-словари?

Давайте в качестве примера рассмотрим определение хранилища. У нас есть некоторые параметры, которые необходимо включить в рецепт нашего шеф-повара. Детали включают некоторые вычисления и некоторые литералы. Мы можем попробовать это:

storage = {
    'devices': [ 
        'device_name': '/dev/xvdz',
        'configuration': {
            'volume_type': get_volume_type(),
            'iops': get_iops(),
            'delete_on_termination': True,
            'volume_size': get_volume_size(),
            'snapshot_id': get_snapshot_id(get_line_of_business()),
        }
    ]
}

Акцент на слове «попробовать».

Это создаст аккуратную структуру данных «словарь-словари». Детали заполняются функциями, которые собирают и вычисляют значения. Для единообразия мы могли бы даже обернуть буквальные значения как функции, чтобы сделать все параметры более единообразными:

def device_name():
 return ‘/dev/xvdz’

Проблема в том, что функции либо имеют несколько параметров, либо вынуждены использовать глобальные переменные. Оказывается, существует множество внешних источников информации о конфигурации. Передавать их все как параметры громоздко; объект пространства имен конфигурации потребуется для каждой функции.

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

Декларативный Python

Как мы можем предложить лучший подход к использованию параметров вместо глобальных переменных? И как мы можем получить объекты с отслеживанием состояния, которые заполняют наш шаблон?

Наш ответ - использовать декларативный стиль программирования. Мы можем - без какой-либо существенной работы - создать своего рода предметно-ориентированный язык, используя определения классов Python. Идея состоит в том, чтобы создавать ленивые объекты, которые при необходимости будут выдавать значения.

Если придерживаться примера хранилища, подход будет выглядеть так:

class Storage(Template):
    device_name = Literal("/dev/xvdz")
    configuration = Document(
        volume_size = VolumeSizeItem("/dev/xvdz", Request('size'),
        "volume_size", conversion=int),
        snapshot_id = ResourceProfileItem(Request('lob'),
            Request('env'), Request('dc'), "Snapshot"),
        delete_on_termination = Literal(True),
        volume_type = ResourceProfileItem (Request('lob'),
            Request('env'), Request('dc'), "VolumeType"),
        iops = ResourceProfileItem (Request('lob'),
            Request('env'), Request('dc'), "IOPS", conversion=int)
)

Для этого детали создаются экземплярами классов, которые помогают создавать объект конфигурации JSON, и экземплярами классов, которые заполняют элементы в объекте конфигурации. Существует иерархия этих классов, которые предоставляют различные типы значений и вычислений. Все они являются расширениями базового класса Item.

Идея состоит в том, чтобы создать экземпляр класса Template, который содержит все сложные данные, которые необходимо собрать и экспортировать в виде большого документа JSON для использования рецептами Chef. Тонкость заключается в том, что мы хотим сохранить порядок, в котором представлены атрибуты. Это не требование, но значительно упрощает чтение JSON, если он тривиально соответствует Python.

Кроме того, нам нужно немного расширить модель наследования Python, чтобы каждый подкласс Template имел конкретный список собственных атрибутов плюс родительские атрибуты. Это тоже упрощает отладку вывода.

Мы собираемся настроить определение метакласса для Template, чтобы обеспечить эти дополнительные функции. Выглядит это так:

class TemplateMeta(type):
    @classmethod
    def __prepare__(metaclass, name, bases):
    """Changes the internal dictionary to a :class:`bson.SON` object."""
        return SON()
 
    def __new__(cls, name, bases, kwds):
    """Create a new instance by merging attribute names.
       Sets the ``_attr_order`` to be parent attributes + child attributes.
    """
    local_attr_list = [a_name
        for a_name in kwds
            if isinstance(kwds[a_name], Item)]
    parent_attr_list = []
    for b in bases:
        parent_attr_list.extend(b._attr_order)
    for name in local_attr_list:
        if name not in parent_attr_list:
            parent_attr_list.append(name)
    kwds['_attr_order'] = parent_attr_list
    return super(TemplateMeta, cls).__new__(cls, name, bases, kwds)

Метакласс заменяет объект __dict__ уровня класса на объект bson.SON. (Да, мы часто используем Mongo.) Объект SON сохраняет ключевую информацию о порядке ввода, как и собственный OrderedDict Python.

Определение метакласса также создает дополнительный атрибут уровня класса _attr_order, который предоставляет полный список атрибутов этого подкласса Template и всех его родительских классов. Порядок всегда начинается с родительских атрибутов. Обратите внимание, что мы не зависим от всех родителей, предоставляющих атрибут _attr_order; мы действительно ищем каждый родительский класс, чтобы убедиться, что мы все нашли.

Метод substitute () шаблона собирает все необходимые данные. Мы могли бы создать здесь данные JSON, мы предпочитаем дождаться запроса вывода.

def substitute(self, sourceContainer, request, **kw):
    self._source = sourceContainer
    self._request = request.copy()
    self._request.update(kw)
    return self

Параметры для построения данных берутся из трех мест: sourceContainer, который имеет все различные файлы конфигурации, начальный запрос, который указывает детали того, сколько узлов для следующего выпуска «Cyclopean», и любые переопределения ключевых слов, которые могут отображаться вверх.

Выходные данные поступают, когда шаблон создается как объект SON, сериализуемый в нотации JSON.

def to_dict(self):
    result = SON()
    for key in self._attr_order:
        item = getattr(self.__class__, key)
        value = item.get(self._source, self._request)
        if value is not None:
            result[key]= value
    return result

Все экземпляры Item с заполнением атрибутов имеют общий метод get (), который выполняет любые вычисления. Это также может обновить любое внутреннее состояние элемента. Шаблон выполняет итерацию по всем элементам, оценивая get ().

Методу get () каждого объекта Item даются подробные сведения о конфигурации. Вместо свободно плавающих глобальных переменных в шаблоне есть краткий список четко определенных деталей конфигурации; они предоставляются каждому отдельному элементу.

Это позволяет не полагаться на (возможно, расплывчатый) набор глобальных переменных. Бонус! Поскольку они являются объектами, вычисления с отслеживанием состояния не включают ужасающую технику обновления глобальных переменных. Состояние может быть инкапсулировано в экземпляр Item. Модульное тестирование работает хорошо, потому что каждый элемент можно тестировать изолированно.

Это дает нам то, что легко проверить и не намного сложнее, чем наивный дизайн. У нас может быть стабильный простой рецепт от Chef. Все поиски и вычисления для подготовки значений для Chef находятся в нашем приложении Python. В частности, они изолированы в определении подклассов предметов и шаблонов.

Ценность Python

Python работает для нас по двум причинам:

1. Гибкость.

2. И гибкость.

Во-первых, у нас есть возможность изменять документы JSON, которые используются для подготовки Chef. Скрипты Chef утомительно отлаживать, потому что каждый раз, когда мы тестируем один из них, подготовка узла занимает - по-видимому - целую вечность. Документы, входящие в состав рецепта Chef, можно определить с помощью модульных тестов, и запуск набора тестов после внесения изменений занимает менее секунды. Каждую новую идею можно быстро изучить.

Например, рассмотрим изменение способа выделения подсетей на предприятии. Вчера «Циклоп» был в одной подсети и жизнь шла хорошо. Теперь, когда он становится огромным, его нужно переместить, а базы данных отделить от веб-серверов. Спецификации подсети перешли от простого подкласса Literal к элементу Item до сложного поиска, основанного на среде и назначении сервера.

Раньше у нас было такое:

class SubnetTemplate(Template):
    subnet_id = Literal('name of net')

Теперь у нас есть это:

class Expanded_SubnetTemplate(Template):
    subnet_id = ResourceProfileField(Request('env'),
        Request('purpose'), 'Subnet')

И все же это изменение никак не повлияло на рецепт Chef. Он добавил кучу деталей в файлы конфигурации и несколько дополнительных поисков в этих файлах. Мы можем быстро разработать изменения и провести модульное тестирование.

Во-вторых, у нас есть возможность интегрировать все этапы подготовки в единую унифицированную структуру. Большая часть работы выполняется через RESTful API. Использование Python 3 и нового urllib делает это относительно простым. Дополнительные библиотеки для различных поставщиков облачных вычислений соответствуют мировоззрению Python в отношении модулей расширения для решения уникальных задач.

Для этого мы используем шаблон проектирования Команда. Каждый шаг в процессе сборки является подклассом NodeCommand.

class NodeCommand:
    """Abstract superclass for all commands related to building a node.
    """
    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__name__)
 
    def __repr__(self):
        return self.__class__.__name__
 
    def execute(self, configuration, build, node_number):
        """Executes command, returns a dictionary with 'status', 
           'log'.
 
        Sources for some parameters::
 
        build_id = build[‘_id’]
        node = build[‘nodes’][node_number]
 
        :param configuration: Global configuration
        :param build: overall :class:`dbbuilder.model.Build` 
               document
        :param node_number: number for the node within the sequence 
               of nodes
        :returns: dictionary with final status information plus any 
                  additional details created by this command.
       """
       raise NotImplementedError

Одним из наиболее важных подклассов NodeCommand является ChefCommand, который выполняет сценарий подготовки шеф-повара со всеми нужными параметрами.

Использование нескольких экземпляров Command означает, что мы можем - с помощью простого импорта - обернуть множество функций в высокоуровневый скрипт Python. И на этом интеграция не заканчивается. Механизм автоматизации подготовки доступен через контейнер Flask. Оператор импорта позволяет контейнеру Flask предоставлять возможности сложных пакетных сценариев любому внутреннему клиенту, который может написать запрос curl или короткий сценарий Python.

Python спешит на помощь

Это ощущение, когда ваше приложение настолько велико - и настолько важно, - что вы просто знаете, что в ближайшее время потребуются значительные объемы памяти ...

Мы думаем, что нашли способ предоставлять расширенные услуги TechOps непосредственно подразделениям бизнеса в виде пакетов кода, а также веб-сервисов RESTful. И мы считаем, что Python является неотъемлемой частью удовлетворения потребностей бизнеса в масштабном создании баз данных NoSQL.

Мы пытались использовать Chef напрямую, но нам нужно было больше гибкости, чем кажется подходящим для этого инструмента. Идея постоянного изменения рецептов не казалась лучшим способом получить инструмент, который надежно воссоздает любой сервер.

Мы пытались создать параметры Chef, используя относительно наивный Python, но это привело к слишком большому количеству глобальных переменных и слишком большому количеству явных параметров для функций. Это стало кошмаром для модульного тестирования.

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

Теперь мы можем создать обширные фермы серверов для следующего выпуска Cyclopean.

Чтобы узнать больше об API, открытом исходном коде, мероприятиях сообщества и культуре разработчиков в Capital One, посетите DevExchange, наш универсальный портал для разработчиков. Https://developer.capitalone.com/