TwistedWeb на многоядерном/многопроцессорном

Какие методы используются для использования нескольких процессоров/ядер при запуске сервера TwistedWeb? Есть ли рекомендуемый способ сделать это?

Моя веб-служба, основанная на twisted.web, работает на экземплярах Amazon EC2, которые часто имеют несколько ядер процессора (8, 16) и тип работы, которую служба выполняет преимущества от дополнительной вычислительной мощности, поэтому я бы очень как использовать это.

Я понимаю, что перед несколькими экземплярами Twisted можно использовать haproxy, squid или веб-сервер, настроенный как обратный прокси. Фактически, мы в настоящее время используем такую ​​настройку, при этом nginx выступает в качестве обратного прокси-сервера нескольким восходящим службам twisted.web, работающим на одном и том же хосте, но каждый на другом порту.

Это отлично работает, но меня действительно интересует, это решение, в котором нет "фронтального" сервера, но все твист-процессы каким-то образом привязаны к одному и тому же сокету и принимают запросы. Возможно ли это даже... или я сумасшедший? Операционная система Linux (CentOS).

Спасибо.

Антон.

Ответ 1

Существует несколько способов поддержки многопроцессорной работы для приложения Twisted. Однако одним из важных вопросов для ответа является то, что вы ожидаете от своей модели concurrency и того, как ваше приложение работает с общим состоянием.

В одном процессе Twisted application, concurrency является все кооперативным (с помощью API-интерфейсов Twisted asynchronous I/O), а общее состояние может храниться в любом месте объекта Python. Ваш код приложения работает, зная, что до тех пор, пока он не откажется от контроля, больше ничего не будет запущено. Кроме того, любая часть вашего приложения, которая хочет получить доступ к какой-либо части общего состояния, может, вероятно, сделать это довольно легко, поскольку это состояние, вероятно, хранится в скучном старом объекте Python, к которому легко получить доступ.

Если у вас есть несколько процессов, даже если все запущенные приложения на основе Twisted, то у вас есть две формы concurrency. Один из них аналогичен предыдущему случаю - в рамках конкретного процесса concurrency является кооперативным. Тем не менее, у вас новый вид, в котором работают несколько процессов. Планировщик планирования платформы может переключать выполнение между этими процессами в любое время, и у вас очень мало контроля над этим (а также очень мало видимости того, когда это произойдет). Он может даже планировать два из ваших процессов одновременно работать на разных ядрах (возможно, это даже то, на что вы надеетесь). Это означает, что вы теряете некоторые гарантии относительно согласованности, поскольку один процесс не знает, когда второй процесс может прийти и попытаться работать в каком-то общем состоянии. Это приводит к другой важной области рассмотрения, как вы фактически разделяете состояние между процессами.

В отличие от единой модели процесса, у вас больше нет удобных, легкодоступных мест для хранения вашего состояния, где весь ваш код может достичь этого. Если вы поместите его в один процесс, весь код в этом процессе может легко получить доступ к нему как к обычным объектам Python, но любой код, запущенный в любом из ваших других процессов, больше не имеет легкого доступа к нему. Возможно, вам придется найти систему RPC, чтобы ваши процессы взаимодействовали друг с другом. Или вы можете архивировать свой процесс, чтобы каждый процесс получал запросы, которые требуют сохранения состояния в этом процессе. Примером этого может быть веб-сайт с сеансами, где все состояние о пользователе хранится в их сеансе, а их сеансы идентифицируются с помощью файлов cookie. Интерфейсный процесс может получать веб-запросы, проверять куки файлы, искать, какой серверный процесс отвечает за этот сеанс, а затем перенаправить запрос на этот внутренний процесс. Эта схема означает, что back-end обычно не требуется связываться (пока ваше веб-приложение достаточно простое - то есть, пока пользователи не взаимодействуют друг с другом или не работают с общими данными).

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

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

С учетом того, что с очень новыми версиями Twisted (невыпущенными на данный момент), довольно легко разделить прослушивающий TCP-порт между несколькими процессами. Вот фрагмент кода, который демонстрирует один способ, которым вы можете использовать некоторые новые API для этого:

from os import environ
from sys import argv, executable
from socket import AF_INET

from twisted.internet import reactor
from twisted.web.server import Site
from twisted.web.static import File

def main(fd=None):
    root = File("/var/www")
    factory = Site(root)

    if fd is None:
        # Create a new listening port and several other processes to help out.                                                                     
        port = reactor.listenTCP(8080, factory)
        for i in range(3):
            reactor.spawnProcess(
                    None, executable, [executable, __file__, str(port.fileno())],
                childFDs={0: 0, 1: 1, 2: 2, port.fileno(): port.fileno()},
                env=environ)
    else:
        # Another process created the port, just start listening on it.                                                                            
        port = reactor.adoptStreamPort(fd, AF_INET, factory)

    reactor.run()


if __name__ == '__main__':
    if len(argv) == 1:
        main()
    else:
        main(int(argv[1]))

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

from os import fork

from twisted.internet import reactor
from twisted.web.server import Site
from twisted.web.static import File

def main():
    root = File("/var/www")
    factory = Site(root)

    # Create a new listening port
    port = reactor.listenTCP(8080, factory)

    # Create a few more processes to also service that port
    for i in range(3):
        if fork() == 0:
            # Proceed immediately onward in the children.
            # The parent will continue the for loop.
            break

    reactor.run()


if __name__ == '__main__':
    main()

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

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

Ответ 2

Рекомендуемый способ, которым ИМО должен использовать haproxy (или другой балансировщик нагрузки), как вы уже это знаете, узким местом не должно быть балансировщик нагрузки, если он настроен правильно. Кроме того, вам понадобится некоторый метод fallover, который haproxy предоставляет в случае, если один из ваших процессов опустится.

Невозможно связать несколько процессов с одним и тем же TCP-сокетом, но это возможно с помощью UDP.

Ответ 3

Если вы хотите также обслуживать свой веб-контент через HTTPS, это то, что вам нужно сделать в дополнение к фрагменту @Jean-Paul.

from twisted.internet.ssl import PrivateCertificate
from twisted.protocols.tls import TLSMemoryBIOFactory

'''
Original snippet goes here
..........
...............
'''

privateCert = PrivateCertificate.loadPEM(open('./server.cer').read() + open('./server.key').read())
tlsFactory = TLSMemoryBIOFactory(privateCert.options(), False, factory)
reactor.adoptStreamPort(fd, AF_INET, tlsFactory)

Используя fd, вы будете обслуживать HTTP или HTTPS, но не оба. Если вы хотите иметь оба, listenSSL в родительском процессе и включить ssl fd, вы получаете из ssl-порта в качестве второго аргумента при рождении дочернего процесса.

Заключительный нож:

from os import environ
from sys import argv, executable
from socket import AF_INET

from twisted.internet import reactor
from twisted.web.server import Site
from twisted.web.static import File

from twisted.internet import reactor, ssl
from twisted.internet.ssl import PrivateCertificate
from twisted.protocols.tls import TLSMemoryBIOFactory

def main(fd=None, fd_ssl=None):
    root = File("/var/www")
    factory = Site(root)

    spawned = []
    if fd is None:
        # Create a new listening port and several other processes to help out.                                                                     
        port = reactor.listenTCP(8080, factory)
        port_ssl = reactor.listenSSL(8443, factory, ssl.DefaultOpenSSLContextFactory('./server.key', './server.cer'))
        for i in range(3):
            child = reactor.spawnProcess(
                None, executable, [executable, __file__, str(port.fileno()), str(port_ssl.fileno())],
                childFDs={0: 0, 1: 1, 2: 2, port.fileno(): port.fileno(), port_ssl.fileno(): port_ssl.fileno()},
                env=environ)
            spawned.append(child)
    else:
        # Another process created the port, just start listening on it.                                                                            
        port = reactor.adoptStreamPort(fd, AF_INET, factory)
        cer = open('./server.cer')
        key = open('./server.key')
        pem_data = cer.read() + key.read()
        cer.close()
        pem.close()
        privateCert = PrivateCertificate.loadPEM(pem_data )
        tlsFactory = TLSMemoryBIOFactory(privateCert.options(), False, factory)
        reactor.adoptStreamPort(fd_ssl, AF_INET, tlsFactory)

    reactor.run()

    for p in spawned:
        p.signalProcess('INT')


if __name__ == '__main__':
    if len(argv) == 1:
        main()
    else:
        main(int(argv[1:]))