在 Jupyter Notebook 中运行 Tornado 服务器

Posted

技术标签:

【中文标题】在 Jupyter Notebook 中运行 Tornado 服务器【英文标题】:Running a Tornado Server within a Jupyter Notebook 【发布时间】:2019-03-16 21:39:44 【问题描述】:

采用标准 Tornado 演示并将 IOLoop 推入后台线程允许在单个脚本中查询服务器。这在 Tornado 服务器是交互式对象时很有用(参见 Dask 或类似内容)。

import asyncio
import requests
import tornado.ioloop
import tornado.web

from concurrent.futures import ThreadPoolExecutor

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

pool = ThreadPoolExecutor(max_workers=2)
loop = tornado.ioloop.IOLoop()

app = make_app()
app.listen(8888)
fut = pool.submit(loop.start)

print(requests.get("https://localhost:8888"))

以上在标准 python 脚本中工作得很好(尽管它缺少安全关闭)。 Jupyter notebook 是这些交互式 Tornado 服务器环境的最佳环境。然而,当涉及到 Jupyter 时,这个想法就被打破了,因为已经有一个活跃的运行循环:

>>> import asyncio
>>> asyncio.get_event_loop()
<_UnixSelectorEventLoop running=True closed=False debug=False>

在 Jupyter 笔记本中运行上述脚本时会出现这种情况,服务器和请求客户端都试图在同一个线程中打开一个连接并且代码挂起。构建一个新的 Asyncio 循环和/或 Tornado IOLoop 似乎没有帮助,我怀疑我在 Jupyter 本身中遗漏了一些东西。

问题:是否可以在 Jupyter 笔记本的后台运行实时 Tornado 服务器,以便标准 python requests 或类似的可以从主线程连接到它?如果可能的话,我宁愿在呈现给用户的代码中避免使用 Asyncio,因为它对于新手用户来说相对复杂。

【问题讨论】:

【参考方案1】:

根据我的recent PR to streamz,这是可行的,类似于您的想法:

class InNotebookServer(object):
    def __init__(self, port):
        self.port = port
        self.loop = get_ioloop()
        self.start()

    def _start_server(self):
        from tornado.web import Application, RequestHandler
        from tornado.httpserver import HTTPServer
        from tornado import gen

        class Handler(RequestHandler):
            source = self

            @gen.coroutine
            def get(self):
                self.write('Hello World')

        application = Application([
            ('/', Handler),
        ])
        self.server = HTTPServer(application)
        self.server.listen(self.port)

    def start(self):
        """Start HTTP server and listen"""
        self.loop.add_callback(self._start_server)


_io_loops = []

def get_ioloop():
    from tornado.ioloop import IOLoop
    import threading
    if not _io_loops:
        loop = IOLoop()
        thread = threading.Thread(target=loop.start)
        thread.daemon = True
        thread.start()
        _io_loops.append(loop)
    return _io_loops[0]

在笔记本中调用

In [2]: server = InNotebookServer(9005)
In [3]: import requests
        requests.get('http://localhost:9005')
Out[3]: <Response [200]>

【讨论】:

【参考方案2】:

第 1 部分:让嵌套龙卷风

要找到您需要的信息,您必须遵循以下 crumbtrails,首先查看IPython 7 的发行说明中描述的内容 特别是它会向您指出有关 documentation 和 this discussion 中的 async 和 await 部分的更多信息, 建议使用nest_asyncio。

关键在于:

A) 要么你欺骗 python 运行两个嵌套的事件循环。 (nest_asyncio 做了什么) B) 您在已经存在的事件循环上安排协程。 (我不知道如何处理龙卷风)

我很确定你知道这一切,但我相信其他读者会很感激。

不幸的是,没有办法让它对用户完全透明——除非你像在 jupyterhub 上那样控制部署,并且可以将这些行添加到自动加载的 IPython 启动脚本中。但我认为以下内容已经足够简单了。

import nest_asyncio
nest_asyncio.apply()


# rest of your tornado setup and start code.

第 2 部分:Gotcha 同步代码块事件循环。

上一节只关注能够运行龙卷风应用程序。但请注意,任何同步代码都会阻塞事件循环;因此,当运行print(requests.get("http://localhost:8000")) 时,服务器似乎无法工作,因为您阻塞了事件循环,只有在等待事件循环重新启动的代码完成执行时才会重新启动......(了解这是留给读者的练习)。您需要从另一个内核发出print(requests.get("http://localhost:8000")),或者使用aiohttp。

这里是如何以与请求类似的方式使用 aiohttp。

import aiohttp
session =  aiohttp.ClientSession()
await session.get('http://localhost:8889')

在这种情况下,由于 aiohttp 是非阻塞的,因此看起来可以正常工作。你在这里可以看到一些额外的 IPython 魔法,我们自动检测异步代码并在当前事件循环上运行它。

一个很酷的练习可能是在另一个内核的循环中运行request.get,并在运行tornado 的内核中运行sleep(5),然后看到我们停止处理请求...

第三部分:免责声明及其他途径:

这是相当棘手的,我建议不要在生产中使用,并警告您的用户这不是推荐的做事方式。

这并不能完全解决您的问题,您需要在主线程中运行一些东西,我不确定这是否可行。

您也可以尝试与trio 和curio 等其他循环运行器一起玩;它们可能允许你做一些默认情况下你不能用 asyncio 做的事情,比如嵌套,但是here be dragoons。我强烈推荐 trio 和 multiple blog posts around its creation,尤其是在你教授异步的时候。

享受,希望对您有所帮助,并请报告错误以及确实有效的事情。

【讨论】:

【参考方案3】:

您可以使用%%script --bg 魔术命令使龙卷风服务器在后台运行。 --bg 选项告诉 jupyter 在后台运行当前单元格的代码。

只需使用魔法命令在一个单元中创建龙卷风服务器并运行该单元。

例子:

%%script python --bg

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

loop = tornado.ioloop.IOLoop.current()

app = make_app()
app.listen(8000) # 8888 was being used by jupyter in my case

loop.start()

然后您可以在单独的单元格中使用requests 连接到服务器:

import requests

print(requests.get("http://localhost:8000"))

# prints <Response [200]>

这里要注意的一点是,如果您在任何单元上停止/中断内核,后台脚本也会停止。所以你必须再次运行这个单元来启动服务器。

【讨论】:

这确实很好用。我确实认为这属于“命令行子进程”,但是因为这实际上是在后台发生的。我想避免这种解决方案,因为 app/loop 在这种特定情况下不是交互式的。 @Daniel 你能定义“非交互式”吗? 无法从 Jupyter 的其他部分访问 app。例如,如果我有一个方法 app.await_results() 将阻塞直到操作完成,这在当前模型下是不可能的。

以上是关于在 Jupyter Notebook 中运行 Tornado 服务器的主要内容,如果未能解决你的问题,请参考以下文章

为啥jupyter notebook代码运行一直没有反应

为啥jupyter notebook代码运行一直没有反应

如何在conda 环境中运行jupyter notebook

在Jupyter Notebook中运行FEniCS

怎么打开jupyter notebook?

在 Ubuntu 上启动时运行 Jupyter-notebook