Асинхронный удар

imageКак уже наверное кто-то догадался, в этой статье речь пойдет о сокетах, и фреймфорках облегчающих работу с ними. Недавно я начал работу надо новым проектом, онлайн игрой. Для таких проектов довольно критично время ответа от сервера, если это конечно не пошаговая стратегия, хотя и в этом случае пожалуй тоже. Так как же этого добиться при суровой ограниченности ресурсов?
  • Облегчить сервер от ненужной работы, например отрисовки самой странички, используя вместо этого javascript шаблонизатор.
  • Использовать хороший front-end, например nginx, учитывая пункт первый, динамики у нас нет, и это нам вполне подходит.
  • Распределяя нагрузку на frontend, например используя Tornado.

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

Предыстоия

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

Скрипты

Пишем простой клиент для тестов на twisted.

from twisted.internet.protocol import ClientFactory
from twisted.protocols.basic import LineReceiver
from twisted.internet import epollreactor
epollreactor.install()
from twisted.internet import reactor
import sys,time

clients=30000
host='127.0.0.1'
port=8001
file = open( 'log.dat', 'w')

class glob():
    connections=0
    crefuse=0
    clost=0
    def enchant(self):
        self.connections+=1
    def refuse(self):
        self.crefuse+=1
    def lost(self):
        self.clost+=1
a=glob()

class EchoClient(LineReceiver):
    measure=True
    def connectionMade(self):
        self.sendLine("Hello, world!")
        self.t1 = time.time()

    def lineReceived(self, line):
        if self.measure:
            self.t2 = time.time() - self.t1
            file.write('%s    %s    %s    %s    %s\n' % (a.connections+1,self.t2,a.crefuse,a.clost,line))
            self.measure=False
            if a.connections+1 < clients:
                a.enchant()
                reactor.connectTCP(host, port, EchoClientFactory())
            else:
                self.transport.loseConnection()

class EchoClientFactory(ClientFactory):
    protocol = EchoClient

    def clientConnectionFailed(self, connector, reason):
        a.refuse()

    def clientConnectionLost(self, connector, reason):
        a.lost()

def main():
    f = EchoClientFactory()
    reactor.connectTCP(host, port, f)
    reactor.run()
    file.close()

if __name__ == '__main__':
    main()

И сервер на каждом фреймворке.

gevent

clients=[]
host=''
port = 8001

def echo(socket, address):

    clients.append(socket)
    while True:
        line = socket.recv(1024)
        for client in clients:
            try:
                client.send(str(len(clients))+'\r\n')
            except:
                clients.remove(client)
            
if __name__ == '__main__':
    from gevent.server import StreamServer
    StreamServer((host, port), echo).serve_forever()


tornado

import errno
import functools
import socket
from tornado import ioloop, iostream

host=''
port = 8001
clients=[]

class Connection(object):
    def __init__(self, connection):
        clients.append(self)
        self.stream = iostream.IOStream(connection)
        self.read()

    def read(self):
        self.stream.read_until('\r\n', self.eol_callback)

    def eol_callback(self, data):
        for c in clients:
            try:
                c.stream.write(str(len(clients))+'\r\n')
            except:
                clients.remove(c)
        self.read()

def connection_ready(sock, fd, events):

    while True:
        try:
            connection, address = sock.accept()
        except socket.error, e:
            if e[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
                raise
            return
        connection.setblocking(0)

        Connection(connection)

if __name__ == '__main__':
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setblocking(0)
    sock.bind((host, port))
    sock.listen(30000)

    io_loop = ioloop.IOLoop.instance()
    callback = functools.partial(connection_ready, sock)
    io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
    try:
        io_loop.start()
    except KeyboardInterrupt:
        io_loop.stop()
        print "exited cleanly"


twisted

from twisted.protocols import basic

class MyChat(basic.LineReceiver):
        def connectionMade(self):
            self.factory.clients.append(self)

        def connectionLost(self, reason):
            self.factory.clients.remove(self)

        def dataReceived(self, line):
                for c in self.factory.clients:
                    c.message(str(len(factory.clients))+'\r\n')

        def message(self, message):
            self.transport.write(message)

from twisted.internet import epollreactor
epollreactor.install()
from twisted.internet import reactor, protocol
from twisted.application import service, internet

factory = protocol.ServerFactory()
factory.protocol = MyChat
factory.clients = []

reactor.listenTCP(8001,factory)
reactor.run()


Небольшое пояснение

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

При измерениях я снимал зависимость времени ответа от количества подключенных пользователей. Клиент и сервер запускались на локалхосте. Машина при этом используется как десктоп, так что результаты могут быть немного искажены, однако суть при этом ясна. Планировалось измерить зависимость вплоть до 30000 подлючений, но к сожалению для всех трех фреймворков это оказалось слишком долго.
image
По оси отклика, время, разумеется, в секундах.
На графике у tornado видно две линии. Это не два теста, а один, просто результаты получились такого вида:
11476 2.45670819283
11477 0.405035018921
11478 2.42619085312
11479 0.392680883408
11480 2.5216550827
11481 0.401995897293

где первое число это количество коннектов, а второе время ответа. Я не знаю с чем это связано.

Выводы

Привожу список всех плюсов и минусов, в рамках поставленной задачи
Gevent

  • + Высокая скорость работы.
  • + Лаконичный код.
  • — Не очень удобно.

Twisted

  • + Понятный код.
  • + Множество дополнительных функций.
  • + Обширная документация.
  • — Тормознутый домашний сайт.
  • — Медленный.

Tornado

  • + С ним быстрее всего получилось разобраться.
  • — Нет документации
  • — Не очень удобно.
  • — Нестабильное время отклика.

Что-ж из всего вышесказаного, каждый сможет сделать выводы для себя сам. Я для себя и своих задач выберу gevent.

UPD. немного ошибся со скриптом на gevent. На него было меньше нагрузки. Перемерил, результат получился немного хуже, однако остался по прежнему лучшим. вот примерные результаты:
10336 1.01536607742 0 0 10338
10337 0.955881118774 0 0 10339
10338 0.947958946228 0 0 10340
10339 1.02578997612 0 0 10341
Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 59
  • –3
    >> совершает какое либо дайствие?
    Опечаточка))
    • 0
      Прошу прощения, если я кого-то обидел, указав на опечатку. Топик мне понравился и ничего против него я не имею.
      • 0
        Такие вещи в личку автору и будет щастье, неужели не понятно
        • 0
          помните, ныли раньше, что непонятно как отправлять сообщения в личку?
          теперь, видимо, два клика это много :)
    • +1
      gevent использует гринлеты. Против libevent ничего не имею, красивая штука. Но гринлеты — бууу. Stack slicing, лежащий в основе технологии, глюкав по определению. Слишком хакерская процедура. Не один раз наблюдал, как гринлеты и C Extensions входили в клинч. Упадет процесс или просто повиснет — дело случая.
      Т.е. оно работает, но шаг влево-вправо приводит к неожиданным последствиям.
      • 0
        И каково ваше предложение?
        • +2
          Мое предложение? Не понял вопроса.
          • 0
            Вы высказали критику в сторону гринлетов, а есть у вас предложение как «правильно» реализовать поставленную задачу?
            • 0
              Все еще не понимаю. Если речь идет о работе с сетью — то есть разные способы. Библиотек — дюжина.
              А если конкретно о гринлетах — то никак не починишь. stackless делает переключение «правильно», но для этого нужно менять ceval. Т.е. stackless — это другой питон, а не библиотека для классического cpython.
        • 0
          У нас на gevent-wsgi-сервере крутится крупный проект. Уже около года. Обрабатывает порядка 200 запросов / сек. Там юзается lxml. Ничего подобного не замечал. Очень доволен gevent-ом.
          • 0
            lxml прокатит. Многие библиотеки работают — иначе бы о gevent вообще никто бы не говорил.
            Беда случается, если вызывается callback. И если этот callback, скажем, работает со старым стеком, поврежденным stack slicing.
            Это встречается нечасто, да и проявляться может не с первого раза. Зато уж если выползло — тушите свет. Доверие к Питону как к платформе резко падает, ломается просто непредсказуемо. Чуть-чуть подшаманив, можно восстановить работу. До следующего раза…
            Очень неприятная ситуация. Корень зла — очень хакерский подход к подсовыванию стека для питоноввского фрейма. Оно работает. Время от времени.
            Что касается gevent, то monkey-patching и покрытие тестами тоже доставляют.
          • 0
            А где можно почитать про stack slicing? У меня что-то не нагуглилось.
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            А выбор есть?
            • +1
              как костыль для веб-сокетов на ИЕ — флешу замены нет, но от фразы «а js не умеет работать с сокетами» меня слегка передернуло

              dev.w3.org/html5/websockets/
          • +2
            А что это на картинке?
            • 0
              «Подвеска в виде пули, на которой выгравирован католический крест и текст молитвы.»
              • 0
                Вы слышали когда-нибудь про silver bullet? :)
                • +12
                  Вы тоже увидели дилдо на цепочке? :)
                • 0
                  Как насчëт socket.io/? И тогда github.com/MrJoes/tornadio/
                  • –8
                    tornadio? А не проще перейти на node.js? Оно быстрее любого питона.
                    • +3
                      LOL. Жаль не все поймут эту шутку ;)
                      • 0
                        объясните?
                        • 0
                          Очень не надежно в продакшине.
                          В то время как питоновские демоны работают годами без вмешательства.
                          • 0
                            Как раз сейчас выбираю в каком направлении податься к торнаде ли, к ноде ли, аль и вовсе в сторону ерланга какого…
                            • 0
                              Erlang хорош когда на нем пишет кто то другой ))
                              NodeJS сыроват.
                              Простые системные админстраторы рекомендуют Python.
                      • +1
                        5 баллов, честное слово. :)
                        • +3
                          Перешел с socket.io-node на tornadio, т.к. такое решение проще интегрировать с не-реалтайм частью сайта, да и писать на питоне приятнее. У node.js тоже свои преимущества есть (главное — больше готовых асинхронных библиотеках), но в сумме преимущества чисто питоньего решения перевесили, ни разу не пожалел.
                        • –1
                          Пробовал торнадио. Пока еще, увы, слишком рано для продакшена.
                          • +1
                            а с какими трудностями столкнулись?
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • 0
                                Почему объемы кода должны быть обязательно меньше? Какой еще beaker.session, какие зависимости? И что не так с flashpolicy?

                                Интересно даже, в чем конкретно-то проблемы с tornadio. Мне-то просто показалось, что код там по делу, все работает, пользоваться удобно.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • +1
                                    1) Мы о разных библиотеках говорим: tornadio — это совсем не SocketTornad.IO.

                                    2) flashpolicy-сервер — это не пример использования, а встроенная штука для того, чтобы из коробки иметь работающий flash-транспорт. В продакшне я для этого nginx настраиваю. К слушанию сокетов он не имеет ну никакого отношения, и отвечать xml он должен не на каждый запрос. Почему он написан так, как написан — судить не берусь, т.к. с flash знаком слабо. Как пользователя это меня не волнует, т.к. в продакшне эта штука не используется, а локально работает и есть не просит.

                                    Примеры можно посмотреть в папке с примерами, очевидно) Например, вот чат: github.com/MrJoes/tornadio/blob/master/examples/chatroom/chatroom.py — куда уж проще?
                                    • НЛО прилетело и опубликовало эту надпись здесь
                            • 0
                              tornadio как то упустил. Попробую, спасибо.
                              • 0
                                а ещё под PyPy пробуйте запускать
                            • –1
                              А почему Вы не попробовали напрямую работать с сокетами? Будет и быстро, и стабильно, и не слишком сложно (тут можно поспорить, но тем не менее, при грамотном подходе получится не сложнее фреймворка), и логику работы Вам легко будет подогнать под себя в случае чего.
                              • 0
                                А как предлагаете организовать сокеты на клиенте?
                                • 0
                                  У вас же они уже организованы через флеш, вам остаётся переписать серверную часть.
                                  • 0
                                    По-моему, автор как раз решает вопрос выбора инструмента для серверной части.
                                    П.с. Я не автор, лишь любопытствующий;)
                                • 0
                                  А с чем он, по-вашему, работает? Автор перечислил популярнейшие инструментарии для работы с неблокирующими сокетами.

                                  Не писать же все заново!
                                  • +2
                                    Его задачей было сделать онлайн игру, причём время ответа от сервера критично. Вот я и интересуюсь, почему используются именно сторонние библиотеки, а не своя реализация асинхронного сервера, без лишних прослоек.
                                    • 0
                                      Может тогда ещё и на Си писать?
                                      «Своя реализация» и всё равно будет прослойкой, только поддерживать её придётся самому.
                                      Не стоит изобретать пылесос.
                                      • +2
                                        Стоит, если свой пылесос лучше справляется с нужной вам задачей, чем существующие решения. Если бы не было сподвижек в изобретении пылесосов, мы бы до сих пор заводили пылесосы на бензине из 19 века.
                                        • 0
                                          Угу… А если бы каждый изобретатель пылесоса начинал в высечения искры из камня и придумыванию колеса, то мы бы до технологий девятнадцатого века не добрались.
                                          Я думаю, что в данном случае всё уже придумано. Автор показал нам три инструмента, и только если их не хватит, нужно будет начинать выдумывать что-то своё. А то правда, проект надо писать прямо сразу под конкретное железо на асме… Да и асм… Да и само железо…
                                        • 0
                                          Кстати, а почему бы и нет? libev вроде не такой страшный. Конечно, нужно обладать опеделенными скилами, что бы писать на таком «остром как бритва» языке.
                                          • 0
                                            Я даже не знаю, как ответить на это. Наверное потому, что питон больше подходит для данных целей.
                                  • 0
                                    Gevent
                                    — Не очень удобно.

                                    Почему?
                                    • 0
                                      У twisted есть обработчики событий, например connectionLost. У gevent и tornado их нет.
                                    • 0
                                      Не знаю, чем вам не понравились исходники торнады, там ведь всего ~10'000 строк, все написано с кучей комментариев и само по себе весьма pythonic.
                                      • 0
                                        Еще бы про клиента написали.
                                        • 0
                                          про клиент можно почитать тут.
                                          Единственное что, используя такой способ, надо чтобы дополнительно висел сервер политик на 843 порту, либо эти самые политики раздавались на основном сервере. Например в пример сервера на twisted в dataReceived добавить:

                                          file_policy="""<?xml version="1.0" encoding="UTF-8"?>\n
                                          <cross-domain-policy xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:noNamespaceSchemaLocation='http://www.adobe.com/xml/schemas/PolicyFileSocket.xsd'>\n
                                                  <allow-access-from domain="*" to-ports="*" secure="false" />\n
                                                  <site-control permitted-cross-domain-policies="master-only" />\n
                                          </cross-domain-policy>\0"""
                                          
                                          if line=='<policy-file-request/>\0':
                                             self.transport.write(file_policy)
                                          

                                        • 0
                                          Сразу оговорюсь, что работал с ними очень и очень поверхностно, но впечатления такие:

                                          Tornado — слишком тоненький, мало функций из коробки. И, насколько я знаю, нет поддержки пулов потоков (соединений с БД например)
                                          Gevent — это очень не-мейнстрим и собрано на костылях и хаках со стеком и манкипатчингом
                                          Twisted — весьма и весьма неплохо, но говорят что медленный

                                          Думаю в случае возникновения необходимости держать постоянные соединения предпочту Erlang
                                          • +1
                                            Выше я уже выразил свою мысль: сначала реализуем на том, что есть, а потом оптимизируем.
                                            В случае проблем с производительностью сокетов, Erlang действительно в тему. Но не раньше.
                                        • 0
                                          Я в свое время написал библиотечку для NOC'а. Она легковесная и умещается в одном файле: lib/nbsocket.py. Можете попробовать. Для наших задач вполне хватает.
                                          • 0
                                            Облегчить сервер от ненужной работы, например отрисовки самой странички, используя вместо этого javascript шаблонизатор.


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

                                            Вообще, на своем опыте убедился что *очень* часто дешевле отрисовать HTML и вернуть клиенту, чем толкать все нужные для шаблона данные.

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.