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

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

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

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

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

Благодарю.

Антон.

3 ответа

Решение

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

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

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

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

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

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

Тем не менее, с очень новыми версиями 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. Последнее, вероятно, больше ответственности, чем стоит принять.

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

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

Если вы хотите обслуживать свой веб-контент также по протоколу 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:]))
Другие вопросы по тегам