在WSGI / apache应用程序中按IP强制执行并发线程限制

时间:2018-09-12 17:02:54

标签: python multithreading apache flask mod-wsgi

我们正在运行一个Flask应用程序,它公开了存储在数据库中的数据。它返回很多503错误。我的理解是,当并发线程达到最大数量时,它们是由apache生成的。

根本原因很可能是应用程序的性能不佳,但是在此阶段,我们无法承受更多的开发时间,因此我正在寻找便宜的部署配置技巧来缓解该问题。

  • 数据提供者正在高速发送数据。我相信他们的程序会得到很多503的帮助,只是尝试/抓住它们重试直到成功。

  • 数据消费者以较低的速度使用该应用程序,我希望他们不会被这些问题所困扰。

我正在考虑限制每个提供商的IP并发访问的数量。他们可能会获得较低的吞吐量,但是他们已经按照原样使用了,这将使休闲消费者的生活更加轻松。


我确定了mod_limitipconn似乎可以做到这一点。

  

mod_limitipconn [...]允许管理员限制从单个IP地址允许的同时请求的数量。

我想确保我了解它的工作原理以及如何设置限制。

由于WSGI设置:threads=5,我一直认为最多有5个同时连接。但是我在mod_wsgi文档中读了Processes and Threading,感到很困惑。

考虑以下配置,这些假设是否正确?

  • 一次仅运行一个应用程序实例。

  • 最多可以产生5个并发线程。

  • 当处理5个请求时,如果第六个请求到达,则客户端会得到一个503

  • 限制同时请求IP x.x.x.x的数量。在apache级别上,将3个线程确保该IP只能使用这5个线程中的3个,而将2个留给其他IP。

  • 在WSGI配置中增加线程数可以通过在速率限制中提供更多的粒度来帮助在客户端之间共享连接池(您可以将4个提供者中的每个限制为3个,并保留另外5个,总共17个) ),但即使服务器有空闲的内核,也不会提高整体性能,因为the Python GIL prevents several threads to run at the same time

  • 将线程数增加到较高的数量(如100)可能会使请求更长,但会限制503响应。如果客户端将自己的并发请求限制设置得不太高,甚至还不够,我可以用mod_limitipconn这样的方式来强制执行。

  • 过多增加线程数将使请求变得如此之长,以至于客户端将获得超时,而不是503,但这并不是更好。


下面的当前配置。不确定什么重要。

apachectl -V

Server version: Apache/2.4.25 (Debian)
Server built:   2018-06-02T08:01:13
Server's Module Magic Number: 20120211:68
Server loaded:  APR 1.5.2, APR-UTIL 1.5.4
Compiled using: APR 1.5.2, APR-UTIL 1.5.4
Architecture:   64-bit
Server MPM:     event
  threaded:     yes (fixed thread count)
    forked:     yes (variable process count)

/etc/apache2/apache2.conf

# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
#
KeepAlive On

#
# MaxKeepAliveRequests: The maximum number of requests to allow
# during a persistent connection. Set to 0 to allow an unlimited amount.
# We recommend you leave this number high, for maximum performance.
#
MaxKeepAliveRequests 100

/etc/apache2/mods-available/mpm_worker.conf(但是在另外event中应该没关系吧?)

<IfModule mpm_worker_module>
        StartServers                     2
        MinSpareThreads          25
        MaxSpareThreads          75
        ThreadLimit                      64
        ThreadsPerChild          25
        MaxRequestWorkers         150
        MaxConnectionsPerChild   0
</IfModule>

/etc/apache2/sites-available/my_app.conf

WSGIDaemonProcess my_app threads=5

2 个答案:

答案 0 :(得分:1)

我希望他们不要被打扰,以便将数据提供者的请求与数据使用者分开(我不熟悉apache,因此我不会向您展示可用于生产环境的配置,整体方法):

<VirtualHost *>
    ServerName example.com

    WSGIDaemonProcess consumers user=user1 group=group1 threads=5
    WSGIDaemonProcess providers user=user1 group=group1 threads=5
    WSGIScriptAliasMatch ^/consumers_ulrs/.* /path_to_your_app/consumers.wsgi process-group=consumers
    WSGIScriptAliasMatch ^/providers_ulrs/.* /path_to_your_app/providers.wsgi process-group=providers

    ...

</VirtualHost>

通过限制每个IP的请求数量,您可能会损害用户体验,但仍然无法解决您的问题。例如,请注意,由于NAT和ISP的工作方式,许多独立用户可能具有相同的IP。

P.S。 ThreadsPerChild=25WSGIDaemonProcess my_app threads=5很奇怪。您确定通过该配置,WSGI服务器将利用Apache创建的所有线程吗?

答案 1 :(得分:0)

我最终采用了另一种方法。我在应用程序代码中添加了一个限制器以解决此问题。

"""Concurrency requests limiter

Inspired by Flask-Limiter
"""

from collections import defaultdict
from threading import BoundedSemaphore
from functools import wraps

from flask import request
from werkzeug.exceptions import TooManyRequests


# From flask-limiter
def get_remote_address():
    """Get IP address for the current request (or 127.0.0.1 if none found)

    This won't work behind a proxy. See flask-limiter docs.
    """
    return request.remote_addr or '127.0.0.1'


class NonBlockingBoundedSemaphore(BoundedSemaphore):
    def __enter__(self):
        ret = self.acquire(blocking=False)
        if ret is False:
            raise TooManyRequests(
                'Only {} concurrent request(s) allowed'
                .format(self._initial_value))
        return ret


class ConcurrencyLimiter:

    def __init__(self, app=None, key_func=get_remote_address):
        self.app = app
        self.key_func = key_func
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        self.app = app
        app.extensions = getattr(app, 'extensions', {})
        app.extensions['concurrency_limiter'] = {
            'semaphores': defaultdict(dict),
        }

    def limit(self, max_concurrent_requests=1):
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                # Limiter not initialized
                if self.app is None:
                    return func(*args, **kwargs)
                identity = self.key_func()
                sema = self.app.extensions['concurrency_limiter'][
                    'semaphores'][func].setdefault(
                        identity,
                        NonBlockingBoundedSemaphore(max_concurrent_requests)
                    )
                with sema:
                    return func(*args, **kwargs)
            return wrapper
        return decorator


limiter = ConcurrencyLimiter()


def init_app(app):
    """Initialize limiter"""

    limiter.init_app(app)
    if app.config['AUTHENTICATION_ENABLED']:
        from h2g_platform_core.api.extensions.auth import get_identity
        limiter.key_func = get_identity

然后我要做的就是将该装饰器应用于我的视图:

@limiter.limit(1)  # One concurrent request by user
def get(...):
    ...

在实践中,我只保护那些产生高流量的网站。

在应用程序代码中执行此操作非常好,因为我可以为每个经过身份验证的用户而不是每个IP设置一个限制。

要做的是,我要做的就是用返回用户唯一标识的函数替换get_remote_address中的默认key_func

请注意,这为每个视图功能设置了不同的限制。如果该限制需要设置为全局限制,则可以采用其他方式实施。实际上,这将更加简单。