Сервер Tornado использует большую часть процессора при использовании tornado-sockjs и только двух клиентов.

Я использую Tornado Server, 4.4.2 и pypy 5.9.0 и python 2.7.13, размещенные на Ubuntu 16.04.3 LTS.

Новый клиент входит в систему, создается новый класс и передается сокет, поэтому диалог можно поддерживать. Я использую глобальный список клиентов[] для хранения классов. начальный диалог выглядит так:

clients = []

class RegisterWebSocket(SockJSConnection):
  # intialize the class and handle on-open (some things left out) 

    def on_open(self,info):
        self.ipaddress = info.headers['X-Real-Ip']

    def on_message(self, data):
        coinlist = []
        msg = json.loads(data)
        if 'coinlist' in msg:
            coinlist = msg['coinlist']
        if 'currency' in msg:
            currency = msg['currency']
            tz = pendulum.timezone('America/New_York')
            started = pendulum.now(tz).to_day_datetime_string()
            ws = WebClientUpdater(self, self.clientid, coinlist,currency, 
                 started, self.ipaddress)
            clients.append(ws)

Класс ws показан ниже, и я использую периодический обратный вызов tornado для обновления клиентов с их конкретной информацией каждые 20 секунд.

class WebClientUpdater(SockJSConnection):

    def __init__(self, ws,id, clist, currency, started, ipaddress):
        super(WebClientUpdater,self).__init__(ws.session)
        self.ws = ws
        self.id = id
        self.coinlist = clist
        self.currency = currency
        self.started = started
        self.ipaddress = ipaddress
        self.location = loc
        self.loop = tornado.ioloop.PeriodicCallback(self.updateCoinList, 
                  20000, io_loop=tornado.ioloop.IOLoop.instance())                                    
        self.loop.start()
        self.send_msg('welcome '+ id)

    def updateCoinList(self):
        pdata = db.getPricesOfCoinsInCurrency(self.coinlist,self.currency)
        self.send(dict(priceforcoins = pdata))

    def send_msg(self,msg):
        self.send(msg)

Я также начинаю с 60-секундного периодического обратного вызова при запуске, чтобы контролировать клиентов на наличие закрытых соединений и удалять их из списка client[]. Который я поместил в строку запуска, чтобы вызвать внутренне определение, например

if __name__ == "__main__":
    app = make_app()
    app.listen(options.port) 
    ScheduleSocketCleaning()

а также

def ScheduleSocketCleaning():
    def cleanSocketHouse():
        print "checking sockets"
        for x in clients:
            if x.is_closed:
              x = None

    clients[:] = [y for y in clients if not y.is_closed ]

    loop = tornado.ioloop.PeriodicCallback(cleanSocketHouse, 60000,                             
          io_loop=tornado.ioloop.IOLoop.instance())
    loop.start()

Если я наблюдаю за сервером с помощью TOP, я вижу, что он использует 4% типичного процессора с пиками до 60+ сразу, но позже, скажем, через несколько часов, он становится на уровне 90% и остается там.

Я использовал strace и вижу огромное количество вызовов Stat для одних и тех же файлов с ошибками, показанными в представлении strace -c, но я не могу найти никаких ошибок в текстовом файле, используя -o trace.log. Как я могу найти эти ошибки?

Но я также заметил, что большую часть времени занимает epoll_wait.

%время

  • 41,61 0,068097 7 9484 epoll_wait
  • 26,65 0,043617 0 906154 2410 стат
  • 15,77 0,025811 0 524072 чтение
  • 10,90 0,017840 129 138 брк
  • 2,41 0,003937 9 417 Мэдвайз
  • 2,04 0,003340 0 524072 лсик
  • 0,56 0,000923 3 298 отправить
  • 0,06 0,000098 0 23779
  • 100,00 0,163663 1989527 2410 всего

Обратите внимание на ошибки 2410 выше.

Когда я просматриваю выходной поток strace, используя прикрепленный pid, я просто вижу бесконечные вызовы Stat для одних и тех же файлов.

Может ли кто-нибудь посоветовать мне, как лучше отладить эту ситуацию? Имея всего два клиента и 20 секунд между обновлениями клиентов, я ожидаю, что загрузка ЦП (на этом этапе прототипа нет других пользователей сайта) будет менее 1% или около того.


person Jack Mullen    schedule 18.12.2017    source источник
comment
Я также должен добавить, что у меня есть сервер nginx перед запросами на переадресацию торнадо. Также на стороне клиента у меня есть reconnect javascript, выполняющий переподключения, если соединение закрывается. Я следил за его правильным поведением, и, похоже, он работает так, как хотелось.   -  person Jack Mullen    schedule 18.12.2017
comment
У вас утечка памяти. Вы никогда не закрываете PeriodicCallbacks. Таким образом, количество таймеров увеличивается, и через некоторое время их становится так много, что они занимают все процессорное время. Это видно на epoll_wait, потому что именно там происходит планирование.   -  person freakish    schedule 18.12.2017
comment
@freakish Спасибо .. Как мне закрыть PeriodicCallbacks?   -  person Jack Mullen    schedule 19.12.2017


Ответы (1)


Вам нужно закрыть PeriodicCallbacks, иначе это утечка памяти. Вы делаете это, просто вызывая .close() для объекта PeriodicCallback. Один из способов справиться с этим — периодическая уборка:

def cleanSocketHouse():
    global clients
    new_clients = []
    for client in clients:
        if client.is_closed:
            # I don't know why you call it loop,
            # .timer would be more appropriate
            client.loop.close()
        else:
            new_clients.append(client)
    clients = new_clients

Я не уверен, насколько точен .is_closed (требуется некоторое тестирование). Другой способ — изменить updateCoinList. Метод .send() должен дать сбой, когда клиент больше не подключен, верно? Поэтому try: except: должен помочь:

def updateCoinList(self):
    global clients
    pdata = db.getPricesOfCoinsInCurrency(self.coinlist,self.currency)
    try:
        self.send(dict(priceforcoins = pdata))
    except Exception:
        # log exception?
        self.loop.close()
        clients.remove(self)  # you should probably use set instead of list

Если ,send() на самом деле не выходит из строя (по какой-то причине, я не так хорошо знаком с Tornado), придерживайтесь первого решения.

person freakish    schedule 19.12.2017