集成 CherryPy Web 框架、SQLAlchemy 会话和 lighttpd 以提供高负载 Web 服务的最佳实践

发布于 2024-07-14 15:39:36 字数 2560 浏览 6 评论 0原文

我正在 lighttpd 后面开发一个 CherryPy FastCGI 服务器,并使用以下设置来启用在 CherryPy 控制器内使用 ORM SQLAlchemy 会话。 但是,当我对大约 500 个循环的 14 个并发请求运行压力测试时,它开始出现类似 AttributeError: '_ThreadData' object has no attribute 'scoped_session_class' in open_dbsession() 的错误code> 或 AttributeError: 'Request' 对象在 close_dbsession() 中没有属性 'scoped_session_class' 一段时间后。 总体错误率约为50%。

仅当我在lighttpd后面运行服务器时才会发生这种情况,而不是直接通过cherrypy.engine.start()运行时会发生这种情况。 已确认 connect() 不会引发异常。

我还尝试将 scoped_session 的返回值分配给 GlobalSession (就像它此处),但随后它给出了诸如 UnboundExceptionError 和其他 SA 级错误之类的错误。 (并发数:10,循环数:1000,错误率:16%。即使直接运行也会发生。)

有一些可能的原因,但我缺乏足够的知识来选择一个。
1. FastCGI环境下start_thread订阅是否不可靠? 似乎是在 connect() 之前调用了 open_dbsession()
2. cherrypy.thread_data 是否因某种原因被清除?

服务器代码

import sqlalchemy as sa  
from sqlalchemy.orm import session_maker, scoped_session

engine = sa.create_engine(dburi, strategy="threadlocal")  
GlobalSession = session_maker(bind=engine, transactional=False)

def connect(thread_index):  
    cherrypy.thread_data.scoped_session_class = scoped_session(GlobalSession)

def open_dbsession():  
    cherrypy.request.scoped_session_class = cherrypy.thread_data.scoped_session_class

def close_dbsession():  
    cherrypy.request.scoped_session_class.remove()


cherrypy.tools.dbsession_open = cherrypy.Tool('on_start_resource', open_dbsession)  
cherrypy.tools.dbsession_close = cherrypy.Tool('on_end_resource', close_dbsession)  
cherrypy.engine.subscribe('start_thread', connect)

lighttpd fastcgi 配置

...
var.server_name = "test"
var.server_root = "/path/to/root"
var.svc_env = "test"
fastcgi.server = (
  "/" => (
    "cherry.fcgi" => (
      "bin-path" => server_root + "/fcgi_" + server_name + ".fcgi",
      "bin-environment" => (
        "SVC_ENV" => svc_env
      ),
      "bin-copy-environment" => ("PATH", "LC_CTYPE"),
      "socket" => "/tmp/cherry_" + server_name + "." + svc_env + ".sock",
      "check-local" => "disable",
      "disable-time"    => 1,
      "min-procs"       => 1,
      "max-procs"       => 4,
    ),
  ),
)

编辑

  • 从原始源代码中恢复了代码示例中缺少的 thread_index 参数(感谢评论)
  • 澄清错误不会立即发生
  • 将条件范围缩小到 lighttpd

I'm developing a CherryPy FastCGI server behind lighttpd with the following setup to enable using ORM SQLAlchemy sessions inside CherryPy controllers. However, when I run stress tests with 14 concurrent requests for about 500 loops, it starts to give errors like AttributeError: '_ThreadData' object has no attribute 'scoped_session_class' in open_dbsession() or AttributeError: 'Request' object has no attribute 'scoped_session_class' in close_dbsession() after a while. The error rate is around 50% in total.

This happens only when I run the server behind lighttpd, not when it's run directly through cherrypy.engine.start(). It's confirmed that connect() isn't raising exceptions.

I also tried assigning the return value of scoped_session to GlobalSession (like it does here), but then it gave out errors like UnboundExceptionError and other SA-level errors. (Concurrency: 10, loops: 1000, error rate: 16%. Occurs even when run directly.)

There are some possible causes but I lack sufficient knowledge to pick one.
1. Are start_thread subscriptions unreliable under FastCGI environment? It seems that open_dbsession() is called before connect()
2. Does cherrypy.thread_data get cleared for some reason?

server code

import sqlalchemy as sa  
from sqlalchemy.orm import session_maker, scoped_session

engine = sa.create_engine(dburi, strategy="threadlocal")  
GlobalSession = session_maker(bind=engine, transactional=False)

def connect(thread_index):  
    cherrypy.thread_data.scoped_session_class = scoped_session(GlobalSession)

def open_dbsession():  
    cherrypy.request.scoped_session_class = cherrypy.thread_data.scoped_session_class

def close_dbsession():  
    cherrypy.request.scoped_session_class.remove()


cherrypy.tools.dbsession_open = cherrypy.Tool('on_start_resource', open_dbsession)  
cherrypy.tools.dbsession_close = cherrypy.Tool('on_end_resource', close_dbsession)  
cherrypy.engine.subscribe('start_thread', connect)

lighttpd fastcgi config

...
var.server_name = "test"
var.server_root = "/path/to/root"
var.svc_env = "test"
fastcgi.server = (
  "/" => (
    "cherry.fcgi" => (
      "bin-path" => server_root + "/fcgi_" + server_name + ".fcgi",
      "bin-environment" => (
        "SVC_ENV" => svc_env
      ),
      "bin-copy-environment" => ("PATH", "LC_CTYPE"),
      "socket" => "/tmp/cherry_" + server_name + "." + svc_env + ".sock",
      "check-local" => "disable",
      "disable-time"    => 1,
      "min-procs"       => 1,
      "max-procs"       => 4,
    ),
  ),
)

edits

  • Restored the missing thread_index argument in the code example from the original source code (thanks to the comment)
  • Clarified that errors do not occur immediately
  • Narrowed down the conditions to lighttpd

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

谁把谁当真 2024-07-21 15:39:36

如果您查看 plugins.ThreadManager.acquire_thread,您将看到 self.bus.publish('start_thread', i) 行,其中 i 是所见线程的数组索引。 订阅 start_thread 通道的任何侦听器都需要接受该 i 值作为位置参数。 因此,重写您的连接函数以读取: def connect(i):

我猜这会以某种方式默默失败; 我会看看是否可以找到它、测试并修复它。

If you look at plugins.ThreadManager.acquire_thread, you'll see the line self.bus.publish('start_thread', i), where i is the array index of the seen thread. Any listener subscribed to the start_thread channel needs to accept that i value as a positional argument. So rewrite your connect function to read: def connect(i):

My guess it that's failing silently somehow; I'll see if I can track that down, test and fix it.

心意如水 2024-07-21 15:39:36

我还尝试将scoped_session的返回值分配给GlobalSession(就像这里所做的那样),但随后它给出了诸如UnboundExceptionError和其他SA级错误之类的错误。 (并发数:10,循环数:1000,错误率:16%)

如果我没有显式实例化scoped_session类,则不会发生此错误。

IE

GlobalSession = scoped_session(session_maker(bind=engine, transactional=False))

def connect(thread_index):  
    cherrypy.thread_data.scoped_session_class = GlobalSession

def open_dbsession():  
    cherrypy.request.scoped_session_class = cherrypy.thread_data.scoped_session_class

def close_dbsession():  
    cherrypy.request.scoped_session_class.remove()

I also tried assigning the return value of scoped_session to GlobalSession (like it does here), but then it gave out errors like UnboundExceptionError and other SA-level errors. (Concurrency: 10, loops: 1000, error rate: 16%)

This error did not occur if I didn't instantiate the scoped_session class explicitly.

i.e.

GlobalSession = scoped_session(session_maker(bind=engine, transactional=False))

def connect(thread_index):  
    cherrypy.thread_data.scoped_session_class = GlobalSession

def open_dbsession():  
    cherrypy.request.scoped_session_class = cherrypy.thread_data.scoped_session_class

def close_dbsession():  
    cherrypy.request.scoped_session_class.remove()
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文