Запуск нескольких сайтов в одном и том же процессе python

В нашей компании мы создаем новостные порталы для довольно большого количества локальных газет (в настоящее время 13, 30 в следующем месяце и более в будущем), каждый из которых отображает 2 тыс. страниц на 100 тыс. страниц в день. Поскольку мы эволюционируем из ситуации, когда каждый сайт сильно настраивается на тот, где каждая разница зависит от конфигурации или пользовательского шаблона, наше программное обеспечение уже почти одинаково для всех сайтов. В настоящее время наша стратегия развертывания - это один экземпляр пушки для каждого сайта (с 1-17 работниками каждый, в зависимости от трафика сайта), на 16-ядерном сервере и 12 ГБ оперативной памяти. Проблема с этой установкой заключается в том, что каждый рабочий (обычный pre-forked gunicorn) принимает 110 МБ, независимо от того, используется ли он или нет. Теперь с новыми сайтами нам нужно будет добавить больше ОЗУ для обслуживания не так много запросов, поэтому в основном он не масштабируется. Кроме того, поскольку мы переходим от этой модели, где каждый сайт независим, каждый сайт имеет свою собственную базу данных, и мне это очень нравится, тем более что мы используем реляционные базы данных (mysql, но переходим к pgsql), поэтому гораздо проще осколочно.

Я занимаюсь некоторыми исследованиями и экспериментирует с запуском всех сайтов на одном экземпляре для пушек, поэтому я мог полностью использовать серверы и добавлять дополнительные серверы за балансировщик нагрузки, когда он пришел к нему. Проблема в том, что django предполагает во многих местах, что на один процесс запускается только один сайт, поэтому, по моему мнению, до сих пор мне пришлось бы реализовать:

  • Среднее ПО, которое принимает HTTP_HOST из запроса и помещает идентификатор в переменную threadlocal.
  • Загрузчик шаблонов, который использует эту переменную для загрузки настраиваемых шаблонов.
  • Monkey patch django.db.model.Model, возможно, добавив метакласс (даже не уверенный, что это возможно, но я думаю, что мне это понадобится из-за привычных менеджеров, которые мы иногда должны использовать), которые перезаписывали бы менеджеров для одного, сначала вызовет db_manager (идентификатор) в исходном менеджере, а затем вызовет предполагаемый метод. Мне также нужно будет перезаписать методы сохранения и удаления, чтобы всегда включать параметр use = identifier.
  • Думаю, мне нужно было бы прекратить использование конструкторов include_tag, а не большую проблему, но мне нужно подумать о других случаях, подобных этому.
  • Тяжелое и уродливое исправление urlresolvers, если мне нужны пользовательские или дополнительные URL-адреса для каждого сайта. Я не нуждаюсь в них сейчас, но, вероятно, в какой-то момент.

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

Ответы

Ответ 1

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

Вы правы, что Django не помогает в таких сценариях, как это из коробки, но на самом деле удивительно легко работать с ним. Вот краткое описание того, что я сделал.

Я мог видеть синергию между тем, чего я хотел достичь, и django.contrib.sites. Также потому, что многие сторонние приложения Django знают, как работать с ним и использовать его, например, для создания абсолютных URL-адресов для текущего сайта. Основная проблема с sites заключается в том, что он хочет, чтобы вы указали текущий идентификатор сайта в settings.SITE_ID, что очень наивный подход к проблеме с несколькими узлами. Естественно, что вы хотите, и то, что вы также упоминаете, - это определить текущий сайт из заголовка запроса Host. Чтобы исправить эту проблему, я заимствовал идею hook от django-multisite: https://github.com/shestera/django-multisite/blob/master/multisite/threadlocals.py#L19

Далее я создал приложение, инкапсулирующее все функциональные возможности, связанные с аспектом multi-хоста моего проекта. В моем случае приложение было вызвано stores, и среди прочего он показал два важных класса: stores.middleware.StoreMiddleware и stores.models.Store.

Класс модели является подклассом django.contrib.sites.models.Site. Хорошая вещь о подклассе Site заключается в том, что вы можете передать Store любой функции, где ожидается Site. Таким образом, вы по-прежнему используете только старую, хорошо документированную и проверенную структуру sites. В класс Store я добавил все поля, необходимые для настройки всех разных магазинов. Таким образом, он получил поля типа urlconf, theme, robots_txt и еще что.

Функция класса middleware должна соответствовать заголовку Host с соответствующим экземпляром Store в базе данных. После получения соответствия Store он будет исправлять SITE_ID способом, аналогичным https://github.com/shestera/django-multisite/blob/master/multisite/middleware.py. Кроме того, он посмотрел на Store urlconf, и если он не был None, он установил бы request.urlconf для применения своих специальных требований к URL-адресу. После этого текущий экземпляр Store хранился в request.store. Это оказалось невероятно полезным, потому что я мог делать такие вещи в моих взглядах:

def homepage(request):
    featured = Product.objects.filter(featured=True, store=request.store)
    ...

request.store стал естественным дополнительным размером объекта request во всем проекте для меня.

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

def get_absolute_url(self, to='/'):
    """
    Return an absolute url to this `Store` or to `to` on this store.

    The URL includes http:// and the domain name of the store.

    `to` can be an object with `get_absolute_url()` or an absolute path as string.

    """
    if isinstance(to, basestring):
        path = to
    elif hasattr(to, 'get_absolute_url'):
        path = to.get_absolute_url()
    else:
        raise ValueError(
            'Invalid argument (need a string or an object with get_absolute_url): %s' % to
        )

    url = 'http://%s%s%s' % (
        self.domain,
        # This setting allowed for a sane development environment
        # where I just set it to ".dev:8000" and configured `dnsmasq`.
        # The same value was also removed from the `Host` value in the middleware
        # before looking up the `Store` in database. 
        settings.DOMAIN_SUFFIX,
        path
    )

    return url

Поэтому я мог бы легко создавать URL-адреса для объектов, отличных от текущего хранилища, например:

# Redirect to `product` on `store`.
redirect(store.get_absolute_url(product)) 

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