Telnet 服务器架构设计

75 阅读3分钟

我们需要编写 Python telnet 服务器,该服务器将允许客户端连接、验证会话,并且发出命令,每个命令对应一个响应。会话具备状态,用户只需验证一次,后续命令将被认为是由该用户执行的。不同会话中的命令/响应操作实际上是独立的,尽管它们涉及到与共享 IO 资源(通常是 Postgres)的读取和偶尔写入,而该共享 IO 资源很大程度上能够管理其自身的并发性。

huake_00152_.jpg 我们的目标是利用少量 8 核或 16 核服务器支持大量用户。我们正在寻找一种有效率的方法来构建服务器的架构。

我们考虑了以下几种方案:

  • 为每个会话使用线程。我们认为,在 GIL 的限制下,这种方案将无法充分利用可用内核。
  • 为每个会话使用多个进程。我们认为,当会话与服务器的比例很高(例如 1000:1)时,1000 个 Python 解释器的开销可能会超出内存限制。它还存在“启动缓慢”的问题,即当用户连接时。
  • 将会话分配给包含 32 个左右进程的进程池。空闲会话可能被分配给所有 32 个进程,从而阻止非空闲会话被处理。
  • 使用某种“路由”系统,其中所有会话都由单个进程处理,然后将各个命令分配给进程池。我们仍认为这基本上是单线程的(因为它存在一个大的单线程瓶颈),并且如果某些命令非常简单,但必须跨越 IPC 边界两次并等待一个空闲进程来获取响应,则该系统可能会引入大量开销。
  • 使用 Jython/IronPython 和多线程。缺乏 C 扩展是一个值得关注的问题。
  • Python 不适合解决这个问题。直接使用 Go/C++/Scala/Java 作为 Python 进程的路由器或完全放弃 Python。

2、解决方案

  • 使用线程:

    如果代码是 IO 绑定的,而不是 CPU 绑定的,那么可以使用线程。因为在 IO 操作时,GIL(全局解释锁)不会影响性能。可以使用 asyncio 库来简化线程的使用。

  • 使用进程:

    如果代码是 CPU 绑定的,那么可以使用进程来提高性能。可以使用 concurrent.futures.ProcessPoolExecutor()multiprocessing.Pool() 来创建进程池。

  • 使用进程池:

    如果代码既有 CPU 绑定部分也有 IO 绑定部分,可以使用进程池来提高性能。进程池可以将 CPU 绑定的任务分配给不同的进程来执行,而将 IO 绑定的任务分配给同一个进程来执行。

代码例子:

from concurrent.futures import ProcessPoolExecutor
from multiprocessing import Pool

def task(n):
    """A simple task that takes a number and returns its square."""
    return n * n

if __name__ == "__main__":
    # Create a process pool with 4 workers.
    pool = ProcessPoolExecutor(4)

    # Submit 10 tasks to the process pool.
    results = pool.map(task, range(10))

    # Print the results.
    for result in results:
        print(result)

在这个例子中,task() 函数是一个简单的任务,它接受一个数字并返回它的平方。ProcessPoolExecutor() 函数创建了一个包含 4 个工作进程的进程池。pool.map() 函数将 task() 函数应用于范围 [0, 9] 中的每个数字,并将结果作为生成器返回。`for