Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS/SSL asyncio leaks memory #109534

Open
rojamit opened this issue Sep 18, 2023 · 76 comments
Open

TLS/SSL asyncio leaks memory #109534

rojamit opened this issue Sep 18, 2023 · 76 comments
Labels
extension-modules C modules in the Modules dir performance Performance or resource usage topic-asyncio type-bug An unexpected behavior, bug, or error

Comments

@rojamit
Copy link

rojamit commented Sep 18, 2023

Bug report

Bug description:

python3.9 without uvloop doesn't leaks memory (or noticeably smaller).
python3.11+ (and others?) leaks memory A LOT under load (with or without uvloop) - up to +2gb per every test!

test commands:
ab -n50000 -c15000 -r https://127.0.0.1/
(apt install apache2-utils)

import asyncio, ssl, uvloop

class HTTP(asyncio.Protocol):
    
    def __init__(self):
        self.transport = None
        
    def connection_made(self, transport):
        self.transport = transport

    def data_received(self, data):
        self.transport.write(
            b'HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'
        )
        self.transport.close()
        
def make_tls_context():
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain('cert.crt', 'cert.key')
    return ctx

tls_context = make_tls_context()
loop = uvloop.new_event_loop()

async def start_server(loop):
    return await loop.create_server(
        HTTP, '127.0.0.1', 443, backlog=65535,
        ssl=tls_context)

loop.run_until_complete(start_server(loop))
loop.run_forever()

CPython versions tested on:

3.9, 3.11, 3.12

Operating systems tested on:

Debian Linux

Linked PRs

@rojamit rojamit added the type-bug An unexpected behavior, bug, or error label Sep 18, 2023
@selevit
Copy link

selevit commented Nov 17, 2023

Can confirm that the issue exists - Python 3.11, and uvloop 0.17.0, when trying to set up many client SSL connections that sometimes reconnects - even directly with asyncio.create_connection, and it's cleaned up not properly in some place.

Sometimes it leaked several megabytes per second for me.

I was able to track this by attaching to a running python process via gdb, and when it leaked, I put a catchpoint for brk syscalls, and examined 40 sequential breakpoint events, when all were related to /usr/lib/python3.10/ssl.py.

(gdb) py-bt
Traceback (most recent call first):
  File "/usr/lib/python3.10/ssl.py", line 917, in read
    v = self._sslobj.read(len)
  <built-in method buffer_updated of uvloop.loop.SSLProtocol object at remote 0xffff758f90c0>
  <built-in method run of _contextvars.Context object at remote 0xffff758c7340>
  File "/home/ubuntu/py-core/env/lib/python3.10/site-packages/aiohttp/web.py", line 544, in run_app
    loop.run_until_complete(main_task)
  File "/home/ubuntu/py-core/server_app.py", line 39, in <module>
    web.run_app(app_factory(), loop=LOOP, host=args.host, port=args.port)
(gdb)

Also, a gc.get_objects() count was not actually growing, that's why I suppose that this is related a lower-level native implementation.

@arhadthedev arhadthedev added performance Performance or resource usage topic-asyncio extension-modules C modules in the Modules dir labels Nov 18, 2023
@arhadthedev
Copy link
Member

@1st1, @asvetlov, @graingert, @gvanrossum, @kumaraditya303 (as asyncio module experts)

@gvanrossum
Copy link
Member

Unfortunately I am really bad at this type of low level stuff. :-( If you suspect a leak in the C accelerator, you can disable it and see if the leak goes away. (Or at least becomes less severe.)

@gvanrossum
Copy link
Member

OTOH if the problem is in the SSL code, we need a different kind of expert.

@gvanrossum
Copy link
Member

Finally please figure out if this is in uvloop or not. If it’s tied to uvloop this is the wrong tracker.

@selevit
Copy link

selevit commented Nov 19, 2023

Unfortunately, this bug was reproduced only in our production, and we weren't able to cause it in other environments.

It's already mitigated, as the root cause was in poor stability of connections.

I don't think I would be able to reproduce it again in production without uvloop, because it caused a lot of harm for our system.

But ready to assist and provide more details, if anyone wants to ask me some clarifying questions about it.

Also, there are several related tickets, and I'm not sure if they are all about uvloop:

  1. asyncio: potential leak of TLS connections #106684 - very similar cpython issue, already closed though.
  2. Memory leak with Python 3.11.2 aio-libs/aiohttp#7252 - also, an already closed, but recent aiohttp issue, they were able to mitigate it somehow, but not sure if it was the same bug.

@selevit
Copy link

selevit commented Nov 19, 2023

I actually was able to reproduce this leak isolated several days ago, but it leaked for only ~5 megabytes in several hours. I adopted a repro script from this comment to the aiohttp bug mentioned above to be ready for run, and ran it for about 4 hours.

The initial memory consumption (RSS) had initially stabilised on 258120 bytes, but after several hours increased to ~264000. Hope it's the same bug.

#!/usr/bin/env python3
import aiohttp
import tracemalloc
import ssl
import asyncio

def init():
    timeout = aiohttp.ClientTimeout(connect=5, sock_read=5)
    ssl_ctx = ssl._create_unverified_context()
    conn = aiohttp.TCPConnector(ssl=ssl_ctx, enable_cleanup_closed=True)
    session = aiohttp.ClientSession(connector=conn, timeout=timeout, cookie_jar=aiohttp.CookieJar(unsafe=True))
    return session

async def fetch(client):
    try:
        async with client.request('GET', url=f'https://api.pro.coinbase.com/products/BTC-USD/ticker') as r:
            msg = await r.text()
            print(msg)
    except asyncio.CancelledError:
        raise
    except Exception as err:
        print("error", err)
        pass

async def main(): 
    requests = 600
    clients = [init() for _ in range(requests)]
    tracemalloc.start()
    try:
        while True:
            await asyncio.gather(*[fetch(client) for client in clients])
            await asyncio.sleep(5)
    except asyncio.CancelledError:
        pass  # end and clean things up
    finally:
        memory_used = tracemalloc.get_traced_memory()
        snapshot = tracemalloc.take_snapshot()
        stats = snapshot.statistics('lineno')
        for stat in stats[:10]:
            print(stat)
        try:
            await asyncio.gather(*[client.close() for client in clients.values()])
        except:
            pass

asyncio.run(main())

@gvanrossum
Copy link
Member

gvanrossum commented Nov 19, 2023

Could you find a reproducer without aiohttp?

@Faolain
Copy link

Faolain commented Nov 27, 2023

Unsure if this is helpful at all or related but I came across this issue encode/uvicorn#2078 in which in the thread it is discussed/concluded that the issue of memory not being released is not isolated to uvicorn but seen in granian/gunicorn/hypercorn and as a result could be interpreter level (apologies for butchering the summary). Thread has some great charts/analysis though it is on the server level, however the different implementations w.r.t granian and uvicorn can help approximate where the issue might be surfacing if it's related. Example repo: https://github.com/Besedo/memory_issue/tree/main by @EBazarov Apologies in advance if it is unrelated and will remove the comment/create one in the right place.

gi0baro:

Given everything we stated in the upper replies, I suspect there's no much to do about this. Given the number of frameworks and servers tested, it just seems the Python interpreter is the culprit here, as even if the heap memory gets released, the RSS remains high.

@gvanrossum
Copy link
Member

Upstream is pretty hard pressed to debug this (I'm no web developer or admin). Can I ask one of the framework owners or users who are struggling with this to help us reduce their example to the point where we have a small self-contained program that demonstrates the issue?

@rojamit
Copy link
Author

rojamit commented Dec 10, 2023

Upstream is pretty hard pressed to debug this (I'm no web developer or admin). Can I ask one of the framework owners or users who are struggling with this to help us reduce their example to the point where we have a small self-contained program that demonstrates the issue?

i provided working example of leak, testing with ab gaves 1+gb/min leak, openssl version 1.1
i can provide access to a dedicated server where this bug happens (not only one server affected)

IMPORTANT: there is no problem if using python3.9 WITHOUT uvloop, with uvloop it leaks, without is not.
BUT python3.12 with or without uvloop leaks even more...

@rojamit
Copy link
Author

rojamit commented Dec 10, 2023

shorten the mvp even more

import asyncio, ssl, uvloop

class HTTP(asyncio.Protocol):
    
    def __init__(self):
        self.transport = None
        
    def connection_made(self, transport):
        self.transport = transport

    def data_received(self, data):
        self.transport.write(
            b'HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'
        )
        self.transport.close()
        
def make_tls_context():
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain('cert.crt', 'cert.key')
    return ctx

tls_context = make_tls_context()
loop = uvloop.new_event_loop()
#loop = asyncio.new_event_loop()

async def start_server(loop):
    return await loop.create_server(
        HTTP, '127.0.0.1', 443, backlog=65535,
        ssl=tls_context)

loop.run_until_complete(start_server(loop))
loop.run_forever()

@rojamit
Copy link
Author

rojamit commented Dec 10, 2023

Updated first post for clarity.
Problem not with openssl itself, it leaks without uvloop even with python3.12

@rojamit
Copy link
Author

rojamit commented Dec 10, 2023

example certificates
certs.zip

@gvanrossum
Copy link
Member

I only have a Mac, and I got the example to work and even managed to install the apache2 utils. I modified the example to print the process size once a second (rather than bothering with graphs) using psutil. Then it got messy, the ab tool often ends with an error. It does show a huge process size increase when ab sort-of works.

Now I've got an idea. Given the complexity of the ssl module (a C extension that wraps OpenSSL) it is much more likely that the problem (if there is really a problem) is due to the code in that extension module than in asyncio, whose TLS support is just a small amount of Python code.

So maybe someone who is interested in getting to the bottom of this issue could rewrite the example without the use of asyncio. Instead you could use the socketserver module. This is ancient tech, but showing the leak when just using this will prove that the problem is in the ssl extension. We can then try to narrow down what happens in the ssl module to find the leak (I'm suspecting there's some path through the code that doesn't release a buffer it owns, but that covers a lot of cases).

(If you can't figure out how to use socketserver with TLS, maybe just a basic multi-threader hand-written serving loop could work. In either case you have to make sure to follow all the rules for serving TLS, of course.)

@rojamit
Copy link
Author

rojamit commented Dec 14, 2023

@gvanrossum
Important note: Python 3.9.2 without uvloop (pure asyncio) doesn't leak, same code. But leaks using uvloop.
As far i know, after Python 3.9 the SSL code is ported from uvloop to cpython?

Biggest leak i saw is at Python 3.12 (without uvloop)

@gvanrossum
Copy link
Member

This doesn’t really help. We need someone to try and find which objects are being leaked.

@rojamit
Copy link
Author

rojamit commented Dec 15, 2023

This doesn’t really help. We need someone to try and find which objects are being leaked.

i tried different python memory profilers with no result at all, idk how to create useful dump, but inside coredump are tons of server certificates info

@gvanrossum
Copy link
Member

Might one of the magical functions in the gc module that give you all the objects be helpful?

@graingert
Copy link
Contributor

objgraph is great for this https://objgraph.readthedocs.io/en/stable/#memory-leak-example

@stalkerg
Copy link

stalkerg commented Dec 20, 2023

Sorry, what without repro (but I will try to do it anyway) but I also got the same issue with Tornado + SSL. And I can confirm, such a leak exist even without uvloop, and sometimes (if I response 404 to cloudflare proxy a lot) it's became significant.
@rojamit sorry but in that case you should try valgrind, I also will try on weekend
PS @rojamit do your project use cloudflare? I found a very annoying behavior bounded with keep-alive and which can also produce illusion of memory leak.

@ordinary-jamie
Copy link
Contributor

ordinary-jamie commented Dec 20, 2023

Summary

Findings:

  • This appears to be an issue when the SSL handshake fails here
  • When you turn of SSL verification, the memory leak won't be detected.
  • The key memory allocation that is not being cleaned is at sslproto.py:275

Minimum replication

This snippet runs the server and runs two separate pings (one insecure) in a subprocess, capturing the tracemalloc snapshots before and after each ping.

The output is as follows

Δ Memory Allocations = 2727.56kb
Δ Memory Allocations = 6.66kb # without SSL

Notice the n_iter=10

import asyncio
import asyncio.sslproto
import ssl
import tracemalloc

class HTTP(asyncio.Protocol):
    def __init__(self):
        self.transport = None

    def connection_made(self, transport):
        self.transport = transport

    def data_received(self, data):
        self.transport.write(
            b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"
        )
        self.transport.close()


def make_tls_context():
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(".jamie/iss109534/server.crt", ".jamie/iss109534/server.key")
    return ctx


async def start_server(loop):
    tls_context = make_tls_context()

    return await loop.create_server(
        HTTP, "127.0.0.1", 4443, backlog=65535, ssl=tls_context, start_serving=True
    )


async def ping(delay: float = 1.0, n_iter: int = 1, insecure: bool = False):

    await asyncio.sleep(delay)

    # ----------------------------------------------------------------------------------
    # Before
    current_1, _ = tracemalloc.get_traced_memory()

    # ----------------------------------------------------------------------------------
    # Run a single request
    if insecure:
        cmd = "curl --insecure"
    else:
        cmd = "curl"

    for _ in range(n_iter):
        proc = await asyncio.create_subprocess_shell(
            f"{cmd} https://127.0.0.1:4443",
            stderr=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE
        )
        await proc.communicate()

    # ----------------------------------------------------------------------------------
    # After
    current_2, _ = tracemalloc.get_traced_memory()

    print(f"Δ Memory Allocations = {(current_2 - current_1)/1000:.2f}kb")


if __name__ == "__main__":
    tracemalloc.start()

    loop = asyncio.new_event_loop()
    loop.run_until_complete(start_server(loop))
    # Run with SSL verification
    loop.run_until_complete(ping(delay=0.5, n_iter=10))
    # Run without SSL verification
    loop.run_until_complete(ping(delay=0.5, insecure=1, n_iter=10))

    loop.close()

Trace malloc snapshot

I updated ping to also take a tracemalloc.take_snapshot before and after and we see that the key allocation is here: sslproto.py:275

async def ping(delay: float = 1.0, n_iter: int = 1, insecure: bool = False):

    # ...

    snapshot_1 = tracemalloc.take_snapshot()

    # ...
    # Same as before
    # ....

    snapshot_2 = tracemalloc.take_snapshot()

    print('-'*40)
    if insecure:
        print("Insecure")

    print(f"Δ Inner Memory Allocations = {(current_2 - current_1)/1000:.2f}kb")

    top_stats = snapshot_2.compare_to(snapshot_1, 'traceback')
    print("\n[ Top stat ]")
    for stat in top_stats[:1]:
        print(stat)
        for line in stat.traceback.format(limit=25):
            print("\t", line)

    print('-'*40)

And, now we can see where the allocations are happening (with n_iter=1)

Δ Inner Memory Allocations = 283.75kb

[ Top stat ]
/Users/jamphan/git/github.com/ordinary-jamie/cpython/.jamie/iss109534/tmp02.py:91: size=256 KiB (+256 KiB), count=2 (+2), average=128 KiB
           File "/Users/jamphan/git/github.com/ordinary-jamie/cpython/.jamie/iss109534/tmp02.py", line 91
             loop.run_until_complete(ping(delay=0.5))
           File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/base_events.py", line 671
             self.run_forever()
           File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/base_events.py", line 638
             self._run_once()
           File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/base_events.py", line 1971
             handle._run()
           File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/events.py", line 84
             self._context.run(self._callback, *self._args)
           File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/selector_events.py", line 224
             transport = self._make_ssl_transport(
           File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/selector_events.py", line 83
             ssl_protocol = sslproto.SSLProtocol(
           File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/sslproto.py", line 275
             self._ssl_buffer = bytearray(self.max_size)

Dev Mode

We can further confirm this when we run python -X dev we see the following warnings

Error on transport creation for incoming connection
handle_traceback: Handle created at (most recent call last):
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/base_events.py", line 1963, in _run_once
    handle._run()
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/events.py", line 84, in _run
    self._context.run(self._callback, *self._args)
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/selector_events.py", line 966, in _read_ready
    self._read_ready_cb()
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/selector_events.py", line 998, in _read_ready__get_buffer
    self._protocol.buffer_updated(nbytes)
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/sslproto.py", line 436, in buffer_updated
    self._do_handshake()
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/sslproto.py", line 561, in _do_handshake
    self._on_handshake_complete(exc)
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/sslproto.py", line 586, in _on_handshake_complete
    self._wakeup_waiter(exc)
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/sslproto.py", line 366, in _wakeup_waiter
    self._waiter.set_exception(exc)
protocol: <__main__.HTTP object at 0x1039294b0>
transport: <asyncio.sslproto._SSLProtocolTransport object at 0x103d6a210>
Traceback (most recent call last):
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/selector_events.py", line 235, in _accept_connection2
    await waiter
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/sslproto.py", line 576, in _on_handshake_complete
    raise handshake_exc
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/asyncio/sslproto.py", line 557, in _do_handshake
    self._sslobj.do_handshake()
  File "/Users/jamphan/.pyenv/versions/3.12.1/lib/python3.12/ssl.py", line 917, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [SSL: TLSV1_ALERT_UNKNOWN_CA] tlsv1 alert unknown ca (_ssl.c:1000)

Edit

Using curl --cacert /path/to/cert.crt as opposed to curl -k will get the same results; just to confirm the insecure flag isn't an invalid test.

Edit 2, updated minimal replication

A simpler code example for replicating the issue...

import asyncio
import ssl
import tracemalloc


async def main(certfile, keyfile):
    tracemalloc.start()

    # Start server with SSL
    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ssl_context.load_cert_chain(certfile, keyfile)
    await asyncio.start_server(
        lambda _, w: w.write(b"\0"), "127.0.0.1", "4443", ssl=ssl_context
    )

    current_1, _ = tracemalloc.get_traced_memory()

    # Ping the server with cURL
    proc = await asyncio.create_subprocess_shell(
        f"curl https://127.0.0.1:4443 2> /dev/null"
    )
    await proc.communicate()

    current_2, _ = tracemalloc.get_traced_memory()
    print(f"{(current_2-current_1)/1000:.2f}KB")


if __name__ == "__main__":

    asyncio.run(
        main(certfile="server.crt", keyfile="server.key")
    )

@gvanrossum
Copy link
Member

@mjpieters Just curious -- have you ever experienced anything similar?

@gvanrossum
Copy link
Member

@ordinary-jamie

The key memory allocation that is not being cleaned is at sslproto.py:275

Excellent find!! Can you think of a suitable place to free this memory? I think it must be passed into OpenSSL via a C wrapper and that C wrapper must somehow forget to free it? So it would have to be in the C code for _ssl where it receives that buffer.

I don't think the leak is in the Python code, even though that's where it's being allocated (Python objects don't easily leak attribute values).

@mjpieters
Copy link
Contributor

@mjpieters Just curious -- have you ever experienced anything similar?

I haven't, the code using asyncio HTTPS is relatively short-running (a few minutes at a time at most).

@geraldog
Copy link

geraldog commented Feb 6, 2024

Hi @stalkerg please, no worries.

But I'm afraid you must let go of refcounts and assume this is a problem with a malloc or realloc somewhere.

Try this:

ab -s 240 -k -n10000 -c10000 -r https://127.0.0.1:44300/ with the reproducer I suggested.

With preventive fix in geraldog@e08719a memory resource usage is 866 megabytes of RAM.

Without preventive fix in mentioned commit memory resource usage is 3155+ megabytes of RAM and it never gets freed - when the connections created by ab -s 240 -k -n10000 -c10000 -r https://127.0.0.1:44300/ end for example.

Something is obviously awry with bytearray/memoryview as class members here.

I'm on gentoo by the way, but I doubt this matters much. I believe you will have the same excessive memory usage without the preventive fix, and very much lighter memory usage by never declaring the bytearray/memoryview as class member.

@ordinary-jamie
Copy link
Contributor

ordinary-jamie commented Feb 6, 2024

I did test on my gentoo machine with 3.11 and 3.12

Hi @stalkerg -- there has been a fix merged and back-ported for 3.11 and 3.12, although I'm not sure if you're building from source.

I beg to differ this is fixed upstream in OpenSSL.

Hi @geraldog -- I believe the earlier discussion was just making aware that the issue was differentially observed for different distros/openssl versions (something that I'm yet to commit time for to investigate why). Was not suggesting there is a fix (or no fix) in openssl.

Do you understand why moving bytearray from SSLProtocol to selector is solve the problem?

One issue observed (and now patched) was that the SSLProtocol object was not deallocating when the handshake fails due to a reference cycle. As such, the protocol instance and, in particular, its buffers would consume noticeable memory.

@geraldog's suggested patch overrides the BufferedProtocol.buffer_updated API so that an independent bytearray not bound to the protocol is passed along. So even as the Protocol continues to stay alive, the buffer will fall out of scope and deallocate. This won't address the root cause of the issue; it can similarly be achieved by releasing and removing references to the protocol's bytearray on failure.

UV Loop never leaked for me

In the PR, there is a brief discussion on why this may be the case.

Please let me know if you continue to experience a memory issue post-fix :)

@geraldog
Copy link

geraldog commented Feb 6, 2024

Hi @ordinary-jamie thanks for working on a fix.

Unfortunately I must say I had the opportunity to test your fix very early on, as soon as it appeared on this thread, and it does not fix the underlying issue for me. Presumably because the connections ab creates never failed handshake either.

With preventive fix in geraldog@e08719a memory usage is ~800 megabytes. Without preventive fix, 3100+ megabytes of RAM, never gets deallocated. Why?

The real reason is to be found deep within the guts of how memoryview and bytearray are allocated. I still don't know the real reason. That's why I called my commit a preventive fix: it does not solve the underlying issue of bytearray making the leak happen when present as class member (I tested and leak happens independent of memoryview).

valgrind has been of little use to me to help trace it by the way, and that gdb script helps but is very confusing and may lead to false positives.

@geraldog
Copy link

geraldog commented Feb 7, 2024

@stalkerg @ordinary-jamie perhaps I am going insane or maybe my gentoo setup is haunted... could you guys replicate the experiment? Did you also get 3100+ megabytes of RAM that never gets deallocated with default python and ab concurrency of 10,000 clients? Did you try my workaround and observe any improvement along the reported memory usage of the python process?

@rojamit
Copy link
Author

rojamit commented Feb 18, 2024

@stalkerg @ordinary-jamie perhaps I am going insane or maybe my gentoo setup is haunted... could you guys replicate the experiment? Did you also get 3100+ megabytes of RAM that never gets deallocated with default python and ab concurrency of 10,000 clients? Did you try my workaround and observe any improvement along the reported memory usage of the python process?

That's why i created this issue. Problem exists, leaks as hell on my production servers.
Leak never deallocs, 16gb+ or more after weeks, and only restart releases ram

So there any patches i can test?
I tested different python versions, and got leak even with uvloop on some.

@geraldog
Copy link

@rojamit good to hear from you. Could you give this a try:

diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py
index 8e888d26ea..85cb05a2de 100644
--- a/Lib/asyncio/selector_events.py
+++ b/Lib/asyncio/selector_events.py
@@ -989,7 +989,7 @@ def _read_ready__get_buffer(self):
             return
 
         try:
-            self._protocol.buffer_updated(nbytes)
+            self._protocol.buffer_updated(nbytes, buf[:nbytes])
         except (SystemExit, KeyboardInterrupt):
             raise
         except BaseException as exc:
diff --git a/Lib/asyncio/sslproto.py b/Lib/asyncio/sslproto.py
index fa99d4533a..3d6944d05a 100644
--- a/Lib/asyncio/sslproto.py
+++ b/Lib/asyncio/sslproto.py
@@ -275,8 +275,8 @@ def __init__(self, loop, app_protocol, sslcontext, waiter,
         if ssl is None:
             raise RuntimeError("stdlib ssl module not available")
 
-        self._ssl_buffer = bytearray(self.max_size)
-        self._ssl_buffer_view = memoryview(self._ssl_buffer)
+        self._ssl_buffer = None
+        self._ssl_buffer_view = None
 
         if ssl_handshake_timeout is None:
             ssl_handshake_timeout = constants.SSL_HANDSHAKE_TIMEOUT
@@ -427,13 +427,12 @@ def get_buffer(self, n):
         want = n
         if want <= 0 or want > self.max_size:
             want = self.max_size
-        if len(self._ssl_buffer) < want:
-            self._ssl_buffer = bytearray(want)
-            self._ssl_buffer_view = memoryview(self._ssl_buffer)
-        return self._ssl_buffer_view
+        _ssl_buffer = bytearray(want)
+        _ssl_buffer_view = memoryview(_ssl_buffer)
+        return _ssl_buffer_view
 
-    def buffer_updated(self, nbytes):
-        self._incoming.write(self._ssl_buffer_view[:nbytes])
+    def buffer_updated(self, nbytes, buffer):
+        self._incoming.write(buffer)
 
         if self._state == SSLProtocolState.DO_HANDSHAKE:
             self._do_handshake()

@rojamit
Copy link
Author

rojamit commented Feb 18, 2024

Maybe i need to clarify the situation even more?
OS: Debian GNU/Linux 11 (bullseye)
cmd: ab -n50000 -c15000 -r https://127.0.0.1/

openssl version
OpenSSL 1.1.1t 7 Feb 2023

Leak is INSANE.
Python 3.12.0 without any patches, loop = asyncio:

# ab -n50000 -c15000 -r https://127.0.0.1/
Requests count: 1 RAM: 59 MB, runtime: 3 s
Requests count: 1 RAM: 3644 MB, runtime: 4 s
Requests count: 38 RAM: 4577 MB, runtime: 5 s
Requests count: 3536 RAM: 4626 MB, runtime: 6 s
Requests count: 15001 RAM: 4852 MB, runtime: 7 s
Requests count: 15001 RAM: 7746 MB, runtime: 8 s
Requests count: 15038 RAM: 8228 MB, runtime: 9 s
Requests count: 16686 RAM: 8262 MB, runtime: 10 s
Requests count: 23111 RAM: 8264 MB, runtime: 11 s
Requests count: 29127 RAM: 8283 MB, runtime: 12 s
Requests count: 30100 RAM: 8286 MB, runtime: 13 s
Requests count: 32179 RAM: 8433 MB, runtime: 14 s
Requests count: 35102 RAM: 8433 MB, runtime: 15 s
Requests count: 37668 RAM: 8436 MB, runtime: 16 s
Requests count: 43231 RAM: 8436 MB, runtime: 17 s
Requests count: 47725 RAM: 8442 MB, runtime: 18 s
Requests count: 48866 RAM: 8442 MB, runtime: 19 s
Requests count: 49139 RAM: 8444 MB, runtime: 20 s
Requests count: 50000 RAM: 8445 MB, runtime: 21 s
Requests count: 50000 RAM: 8444 MB, runtime: 22 s
# ... RAM never releases.

Python 3.12.0 without any patches, loop = uvloop:

Requests count: 0 RAM: 25 MB, runtime: 4 s
# ab -n50000 -c15000 -r https://127.0.0.1/
Requests count: 1 RAM: 307 MB, runtime: 5 s
Requests count: 5269 RAM: 451 MB, runtime: 6 s
Requests count: 9576 RAM: 563 MB, runtime: 7 s
Requests count: 14878 RAM: 715 MB, runtime: 8 s
Requests count: 16992 RAM: 762 MB, runtime: 9 s
Requests count: 22868 RAM: 775 MB, runtime: 10 s
Requests count: 27176 RAM: 784 MB, runtime: 11 s
Requests count: 29878 RAM: 791 MB, runtime: 12 s
Requests count: 30885 RAM: 801 MB, runtime: 13 s
Requests count: 37458 RAM: 810 MB, runtime: 14 s
Requests count: 42356 RAM: 823 MB, runtime: 15 s
Requests count: 44878 RAM: 857 MB, runtime: 16 s
Requests count: 47379 RAM: 866 MB, runtime: 17 s
Requests count: 50000 RAM: 876 MB, runtime: 18 s
Requests count: 50000 RAM: 877 MB, runtime: 19 s
Requests count: 50000 RAM: 862 MB, runtime: 20 s
# ...  ab done, nothing changed over time.
# second run: ab -n50000 -c15000 -r https://127.0.0.1/
Requests count: 50000 RAM: 862 MB, runtime: 115 s
Requests count: 50001 RAM: 1151 MB, runtime: 116 s
Requests count: 50001 RAM: 1202 MB, runtime: 117 s
Requests count: 58060 RAM: 1288 MB, runtime: 118 s
Requests count: 65001 RAM: 1318 MB, runtime: 119 s
Requests count: 65001 RAM: 1341 MB, runtime: 120 s
Requests count: 72918 RAM: 1353 MB, runtime: 121 s
# ... RAM never releases.

Python 3.9.2 without any patches, loop = asyncio:

Requests count: 1 RAM: 723 MB, runtime: 2 s
Requests count: 70 RAM: 824 MB, runtime: 3 s
Requests count: 7277 RAM: 843 MB, runtime: 4 s
Requests count: 15001 RAM: 899 MB, runtime: 5 s
Requests count: 15001 RAM: 1196 MB, runtime: 6 s
Requests count: 16561 RAM: 1268 MB, runtime: 7 s
Requests count: 18495 RAM: 1278 MB, runtime: 8 s
Requests count: 28995 RAM: 1260 MB, runtime: 9 s
Requests count: 30001 RAM: 1283 MB, runtime: 10 s
Requests count: 30872 RAM: 1316 MB, runtime: 11 s
Requests count: 31293 RAM: 1317 MB, runtime: 12 s
Requests count: 36768 RAM: 1319 MB, runtime: 13 s
Requests count: 44162 RAM: 1326 MB, runtime: 14 s
Requests count: 45504 RAM: 1337 MB, runtime: 15 s
Requests count: 46196 RAM: 1339 MB, runtime: 16 s
Requests count: 48254 RAM: 1340 MB, runtime: 17 s
Requests count: 50000 RAM: 1341 MB, runtime: 18 s
Requests count: 50000 RAM: 1341 MB, runtime: 19 s

Python 3.9.2 without any patches, loop = uvloop:

Requests count: 1 RAM: 833 MB, runtime: 4 s
Requests count: 2725 RAM: 915 MB, runtime: 5 s
Requests count: 2725 RAM: 924 MB, runtime: 6 s
Requests count: 15001 RAM: 932 MB, runtime: 7 s
Requests count: 15001 RAM: 1359 MB, runtime: 8 s
Requests count: 15001 RAM: 1589 MB, runtime: 9 s
Requests count: 16329 RAM: 1597 MB, runtime: 10 s
Requests count: 18467 RAM: 1605 MB, runtime: 11 s
Requests count: 30001 RAM: 1642 MB, runtime: 12 s
Requests count: 30001 RAM: 1860 MB, runtime: 13 s
Requests count: 31480 RAM: 1875 MB, runtime: 14 s
Requests count: 34787 RAM: 1881 MB, runtime: 15 s
Requests count: 45001 RAM: 1899 MB, runtime: 16 s
Requests count: 45001 RAM: 1922 MB, runtime: 17 s
Requests count: 45001 RAM: 1938 MB, runtime: 18 s
Requests count: 50000 RAM: 1939 MB, runtime: 19 s
Requests count: 50000 RAM: 1900 MB, runtime: 20 s
Requests count: 50000 RAM: 1900 MB, runtime: 21 s

Every run of ab increasing allocated ram.

@rojamit
Copy link
Author

rojamit commented Feb 18, 2024

@geraldog Python 3.12.0, patch reduces but didnt resolves the leak:

Just realised: with your patch RAM usage doesn't grow up after multiple ab runs!

# first ab run
Requests count: 0 RAM: 22 MB, runtime: 2 s
Requests count: 1 RAM: 254 MB, runtime: 3 s
Requests count: 1 RAM: 824 MB, runtime: 4 s
Requests count: 133 RAM: 864 MB, runtime: 5 s
Requests count: 14765 RAM: 885 MB, runtime: 6 s
Requests count: 15001 RAM: 1132 MB, runtime: 7 s
Requests count: 15001 RAM: 1220 MB, runtime: 8 s
Requests count: 19318 RAM: 1226 MB, runtime: 9 s
Requests count: 25375 RAM: 1181 MB, runtime: 10 s
Requests count: 28363 RAM: 1254 MB, runtime: 11 s
Requests count: 30001 RAM: 1268 MB, runtime: 12 s
Requests count: 30453 RAM: 1271 MB, runtime: 13 s
Requests count: 37035 RAM: 1275 MB, runtime: 14 s
Requests count: 44049 RAM: 1280 MB, runtime: 15 s
Requests count: 44989 RAM: 1293 MB, runtime: 16 s
Requests count: 44989 RAM: 1376 MB, runtime: 17 s
Requests count: 46032 RAM: 1378 MB, runtime: 18 s
Requests count: 50000 RAM: 1331 MB, runtime: 19 s
Requests count: 50000 RAM: 1577 MB, runtime: 20 s
Requests count: 50000 RAM: 1577 MB, runtime: 21 s
# second ab run:
Requests count: 50000 RAM: 1577 MB, runtime: 52 s
Requests count: 50001 RAM: 1306 MB, runtime: 53 s
Requests count: 50536 RAM: 1307 MB, runtime: 54 s
Requests count: 51402 RAM: 1306 MB, runtime: 55 s
Requests count: 65001 RAM: 1306 MB, runtime: 56 s
Requests count: 65001 RAM: 1340 MB, runtime: 57 s
Requests count: 65186 RAM: 1342 MB, runtime: 58 s
Requests count: 65953 RAM: 1322 MB, runtime: 59 s
Requests count: 75548 RAM: 1322 MB, runtime: 60 s
Requests count: 78316 RAM: 1373 MB, runtime: 61 s
Requests count: 79346 RAM: 1414 MB, runtime: 62 s
Requests count: 81693 RAM: 1419 MB, runtime: 63 s
Requests count: 87228 RAM: 1415 MB, runtime: 64 s
Requests count: 92837 RAM: 1415 MB, runtime: 65 s
Requests count: 94148 RAM: 1421 MB, runtime: 66 s
Requests count: 95209 RAM: 1421 MB, runtime: 67 s
Requests count: 97426 RAM: 1423 MB, runtime: 68 s
Requests count: 100000 RAM: 1423 MB, runtime: 69 s
Requests count: 100000 RAM: 1489 MB, runtime: 70 s
# third ab run:
Requests count: 100000 RAM: 1489 MB, runtime: 90 s
Requests count: 100001 RAM: 1422 MB, runtime: 91 s
Requests count: 100931 RAM: 1422 MB, runtime: 92 s
Requests count: 101772 RAM: 1422 MB, runtime: 93 s
Requests count: 115001 RAM: 1422 MB, runtime: 94 s
Requests count: 115001 RAM: 1427 MB, runtime: 95 s
Requests count: 115004 RAM: 1427 MB, runtime: 96 s
Requests count: 116939 RAM: 1427 MB, runtime: 97 s
Requests count: 127994 RAM: 1427 MB, runtime: 98 s
Requests count: 130001 RAM: 1428 MB, runtime: 99 s
Requests count: 130001 RAM: 1428 MB, runtime: 100 s
Requests count: 131034 RAM: 1428 MB, runtime: 101 s
Requests count: 140741 RAM: 1428 MB, runtime: 102 s
Requests count: 141053 RAM: 1428 MB, runtime: 103 s
Requests count: 145001 RAM: 1429 MB, runtime: 104 s
Requests count: 145001 RAM: 1429 MB, runtime: 105 s
Requests count: 150000 RAM: 1429 MB, runtime: 106 s
Requests count: 150000 RAM: 2346 MB, runtime: 107 s
Requests count: 150000 RAM: 2346 MB, runtime: 108 s

Untouched python (with or without uvloop) will leak more and more after every ab re-run, but looks like this patch solves it

But. New interesting moment: sometimes ram usage grows a bit after ab run already done:

Requests count: 194868 RAM: 1429 MB, runtime: 650 s
Requests count: 200000 RAM: 1429 MB, runtime: 651 s # ab done
Requests count: 200000 RAM: 1942 MB, runtime: 652 s
Requests count: 200000 RAM: 2517 MB, runtime: 653 s
Requests count: 200000 RAM: 2517 MB, runtime: 654 s

@geraldog
Copy link

@rojamit thanks for the thorough testing.

I appreciate the confirmation that my patch seems to mitigate the issue for you.

This last note of yours:

But. New interesting moment: sometimes ram usage grows a bit after ab run already done:

Requests count: 194868 RAM: 1429 MB, runtime: 650 s
Requests count: 200000 RAM: 1429 MB, runtime: 651 s # ab done
Requests count: 200000 RAM: 1942 MB, runtime: 652 s
Requests count: 200000 RAM: 2517 MB, runtime: 653 s
Requests count: 200000 RAM: 2517 MB, runtime: 654 s

This is definitely not good, it probably means there's another source of leaks.

Also, the bad news is my patch is only a workaround, it doesn't clarify why having bytearray as class member generates leaks, it just doesn't do that and goes its merry way, but there must be an underlying reason (I bet on C side of Python memory allocation) why it leaks.

Since you confirmed the patch is a bona fide workaround that prevents memory from leaking, I'll go ahead and file a PR. Hopefully we can work together with the Python maintainers to end this nightmare of sorts.

geraldog added a commit to geraldog/cpython that referenced this issue Feb 19, 2024
Workaround horrendous memory leaks, does not solve the
underlying issue of why bytearray as class member like
that will cause leaks, at least in this case
@stalkerg
Copy link

stalkerg commented Feb 19, 2024

Did you also get 3100+ megabytes of RAM that never gets deallocated with default python and ab concurrency of 10,000 clients?

I can reproduce it on 3.12 on Gentoo but I can't see it inside container base on alpine or debian, it's de allocate properly.
Also, I can't see constant leak, it's just allocate a lot and keep it without de allocation (more like slot for TCP connection).
Also, I can't reproduce it by wrk even if I try to use same amount of connections.

@geraldog
Copy link

While I do confirm @stalkerg observation that wrk does not leak - it allocates 3000+ megabytes of RAM but that gets deallocated at the end of the test - it might be wise to notice that while ab does a clean SSL shutdown, wrk does not, and that itself is enough to trigger the memory cleanup.

So I think we need to explore that angle perhaps, that when connection_lost() is called there's actually memory cleanup. I'll try confirmation of such hypothesis.

But @stalkerg please don't kid yourself that this is "pooling" of some sort. While I did observe like you, repeated ab tests didn't leak anymore memory if - big if there - you kept the number of clients constant, it is behavior that wasn't present for certain in 3.9, since 3.9 doesn't leak as per mine and specially @rojamit observations. This is a real live one, a big leak, it's going to be hard to sell the version that this is some optimization of some kind, that RAM is being glutton-ed for some good reason, a trade-off of sorts.

@stalkerg
Copy link

stalkerg commented Feb 19, 2024

This is a real live one, a big leak, it's going to be hard to sell the version that this is some optimization of some kind, that RAM is being glutton-ed for some good reason, a trade-off of sorts.

oh, it was a good hint. It's can confuse everything even more, but still. I tried to use jemalloc (jemalloc.sh python3.12) and results became a different:

  1. Peak mem usage similar (very big)
  2. But mem usage reduce by 70% after ab stop working.
  3. If I run wrk after that it's de-allocate even more, and my process consume 70-80Mb only.

btw on my Gentoo I use glibc 2.38, on debian is 2.36 and in alpine is not a glibc.
My theory:

  1. We have a leak but it's not so big itself. And yes, it seems like somehow tied with proper SSL close.
  2. At the same time, intarray or something else is making a new glibc allocator a little bit sick. He never return to OS memory in a such situation.

UPDATE:
I can now reproduce issue on debian bookworm inside container, but alpine still working very well.

UPDATE2:
I connected to the process by gdb and did call malloc_trim(0) and memory is released on my gentoo machine.

@geraldog
Copy link

oh, it was a good hint. It's can confuse everything even more, but still. I tried to use jemalloc (jemalloc.sh python3.12) and results became a different:

1. Peak mem usage similar (very big)

2. But mem usage reduce by 70% after `ab` stop working.

I confirm @stalkerg observation that with jemalloc there is deallocation of the buffers after ab testing.

UPDATE2: I connected to the process by gdb and did call malloc_trim(0) and memory is released on my gentoo machine.

I also confirm @stalkerg observation that issuing call malloc_trim(0) from the gdb prompt will release the memory back to the system. So I was wrong, and this was an "optimization" after all.

@stalkerg
Copy link

stalkerg commented Feb 19, 2024

@graingert @rojamit seems like any numbers for MALLOC_TRIM_THRESHOLD_ env var can mitigate the situation. It should be MALLOC_TRIM_THRESHOLD_=131072 by default (128KB) but for some reason even that is change behavior a little.

UPDATE: Okey, it's happened because such env var disable dynamic threshold. https://github.com/lattera/glibc/blob/master/malloc/malloc.c#L5031

UPDATE: this is actual values during our test: {trim_threshold = 532480, top_pad = 131072, mmap_threshold = 266240, arena_test = 8, arena_max = 0, thp_pagesize = 0, hp_pagesize = 0, hp_flags = 0, n_mmaps = 3, n_mmaps_max = 65536, max_n_mmaps = 5, no_dyn_threshold = 0, mmapped_mem = 610304, max_mmapped_mem = 1142784, sbrk_base = 0x555555558000 "", tcache_bins = 64, tcache_max_bytes = 1032, tcache_count = 7, tcache_unsorted_limit = 0}

@geraldog
Copy link

Confirming that setting MALLOC_TRIM_THRESHOLD_=131072 or any other number will release memory back to the system. Thank you very much @stalkerg that was impressive!

@rojamit
Copy link
Author

rojamit commented Feb 26, 2024

@geraldog maybe wrk doesnt leak because it uses Keep-Alive by default?

Some tests with malloc_trim(0) call every second (in other thread) and huge requests count:

Python 3.12 with patch, asyncio loop:

Requests count: 1850000 RAM: 919 MB, runtime: 698 s # ab end
Requests count: 1850000 RAM: 2403 MB, runtime: 699 s
Requests count: 1850000 RAM: 2511 MB, runtime: 700 s
Requests count: 1850000 RAM: 1978 MB, runtime: 701 s
Requests count: 1850000 RAM: 1978 MB, runtime: 702 s
# allocated ram amount shrinks while ab running & increases after ab done, strange

Python 3.9, asyncio loop:

Requests count: 542830 RAM: 832 MB, runtime: 186 s
Requests count: 546091 RAM: 803 MB, runtime: 187 s
Requests count: 548113 RAM: 816 MB, runtime: 188 s
Requests count: 550000 RAM: 750 MB, runtime: 189 s
Requests count: 550000 RAM: 710 MB, runtime: 190 s
Requests count: 550000 RAM: 470 MB, runtime: 191 s
Requests count: 550000 RAM: 166 MB, runtime: 192 s # ab end

Python 3.12 with patch, uvloop, desired result:

Requests count: 550000 RAM: 760 MB, runtime: 146 s # ab end
Requests count: 550000 RAM: 720 MB, runtime: 147 s
Requests count: 550000 RAM: 525 MB, runtime: 148 s
Requests count: 550000 RAM: 73 MB, runtime: 149 s
Requests count: 550000 RAM: 73 MB, runtime: 150 s

(patch doesnt affect uvloop as i remember it has own ssl module)

Looks like export MALLOC_TRIM_THRESHOLD_=131072 has no effect for me, only malloc_trim(0) does.

@stalkerg
Copy link

@rojamit why for your first test you have much more requests? Anyway, you should try update glibc and maybe linux kernel.

Looks like export MALLOC_TRIM_THRESHOLD_=131072 has no effect for me, only malloc_trim(0) does.

as I said, it's just mitigation, it's also not solve my issue on a long run. Did you try to replace allocator? If malloc_trim(0) is work, it means issue not in Python but in glibc allocation algorithm. However probably recent changes and memoryview screw it up.
Also who can wrote to glibc mail list? This case should be helpful for them.

@rojamit
Copy link
Author

rojamit commented Mar 1, 2024

@stalkerg higher requests count is not linked with memory consumption in this test

uname -r
5.10.0-26-amd64

Debian GNU/Linux 11 (bullseye)

@stalkerg
Copy link

@rojamit @geraldog sorry for mention you, but can somebody a way to write to glibc maillist? And in general we can close this ticket.

@geraldog
Copy link

geraldog commented Apr 5, 2024

@rojamit @geraldog sorry for mention you, but can somebody a way to write to glibc maillist? And in general we can close this ticket.

Sorry @stalkerg but I think the burden is on you to write a Bug Report to https://sourceware.org/mailman/listinfo/libc-stable and Cc: the relevant people involved in glibc development. It was you who discovered it.

There's a high possibility they won't fix however, citing that it's an actual optimization for some cases and that the burden is on the user to tell glibc to deallocate. And then we're basically stuck, all that is left is noting in documentation that this is a known issue...

@fifdee
Copy link

fifdee commented Apr 24, 2024

Hello, I experienced the problem with my Django ASGI web app and finally found this topic. I use railway.app as a hosting service, I also described how to reproduce the problem with minimal Django ASGI app on stackoverflow: https://stackoverflow.com/questions/78339166/python-django-asgi-memory-leak-updated-2
I tried Python 3.9 without uvloop config but it also leaks. Can't check Python 3.12 though, as nixpacks doesn't support it yet.

@stalkerg
Copy link

@fifdee did you try use different allocator? You can find instructions in this topic.

@fifdee
Copy link

fifdee commented Apr 24, 2024

@stalkerg I haven't. I'm not sure if it's possible with PaaS host like Railway, currently I don't have much experience with this kind of stuff.

@fifdee
Copy link

fifdee commented Apr 25, 2024

I tried setting environment variable PYTHONMALLOC=malloc but it didn't change anything.
It looks like malloc_trim(0) is working for me, I need to observe how web server behaves in the long run.
I used piece of code found here: https://www.softwareatscale.dev/p/run-python-servers-more-efficiently

And for those who use Django ASGI and suffer the same problem, this is modified asgi.py:

import ctypes
import random
import time
from threading import Thread
import psutil
import os

import django
from django.core.handlers.asgi import ASGIHandler

MEMORY_THRESHOLD = 1024 * 1024 * 256  # 256MB


def trim_memory() -> int:
    libc = ctypes.CDLL("libc.so.6")
    return libc.malloc_trim(0)


def should_trim_memory() -> bool:
    # check if we're close to our OOM limit
    # through psutil
    process = psutil.Process(os.getpid())
    return process.memory_info().rss > MEMORY_THRESHOLD


def trim_loop() -> None:
    while True:
        time.sleep(random.randint(30, 60))  # jitter between 30 and 60s
        if not should_trim_memory():
            continue

        ret = trim_memory()
        print("trim memory result: ", ret)


def get_asgi_application():
    """
    The public interface to Django's ASGI support. Return an ASGI 3 callable.

    Avoids making django.core.handlers.ASGIHandler a public API, in case the
    internal implementation changes or moves in the future.
    """

    django.setup(set_prefix=False)

    thread = Thread(name="TrimThread", target=trim_loop)
    thread.daemon = True
    thread.start()

    return ASGIHandler()


os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'v45.settings')

application = get_asgi_application()

@geraldog
Copy link

geraldog commented Apr 25, 2024

@fifdee did you try MALLOC_TRIM_THRESHOLD_?

Like this:

Confirming that setting MALLOC_TRIM_THRESHOLD_=131072 or any other number will release memory back to the system. Thank you very much @stalkerg that was impressive!

@fifdee
Copy link

fifdee commented Apr 25, 2024

@fifdee did you try MALLOC_TRIM_THRESHOLD_?

Like this:

Confirming that setting MALLOC_TRIM_THRESHOLD_=131072 or any other number will release memory back to the system. Thank you very much @stalkerg that was impressive!

Ah, I missed this one. I'll check it also, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extension-modules C modules in the Modules dir performance Performance or resource usage topic-asyncio type-bug An unexpected behavior, bug, or error
Projects
Status: Todo
Development

No branches or pull requests