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

Memory Leak #3198

Open
zqlpaopao opened this issue Nov 22, 2023 · 55 comments
Open

Memory Leak #3198

zqlpaopao opened this issue Nov 22, 2023 · 55 comments

Comments

@zqlpaopao
Copy link

Memory Leak / Memory "not recycled" in Actix 3.3 #1943

The program is active_ On the web, my program has a startup memory of 14MB and requests the interface to perform some operations with a memory of 800MB+. However, the 800MB+in subsequent programs has not been released. What is the reason for this? Some of the language's setting features, or are there any useful tools such as PPROF that can detect memory overflow
image
image

Actix_ When web requests return big data, the memory is not released and will continue to be occupied. Is the original intention of this issue and will it be improved in the future

use actix_web::{get, HttpResponse, Responder};
use crate::controller::link_inspection::check::LinkInspection;
use std::collections::HashMap;
#[get("/check")]
async fn check() -> impl Responder {

// 启动堆分析器
// let res = LinkInspection::new().doing().await;
// match res {
//     Ok(response) => HttpResponse::Ok().json(response),
//     Err(error) => HttpResponse::InternalServerError().body(error.to_string()),
// }
let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
test(&mut h).await;

HttpResponse::InternalServerError().body(format!("{:?}", h))

}

async fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
println!("start");
for v in 0..1000000{
    h.insert(v,v);
}

}

@dzvon
Copy link

dzvon commented Nov 22, 2023

What makes you think this is an issue with actix-web? Could you please provide a minimal reproducible example of this?

@zqlpaopao
Copy link
Author

code

use actix_web::{App, HttpServer};

#[tokio::main]
async fn main() {
    web().await;
}

async fn web() {
    HttpServer::new(|| {
        println!(
            "WEB LISTENING TO  {}:{}",
            "127.0.0.1",
            18080
        );
        // 在这里传入定义的服务
        App::new().service(check)
    })
        .bind(("127.0.0.1", 18080))
        .unwrap()
        .run()
        .await
        .unwrap();
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
#[get("/check")]
async fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h).await;

    HttpResponse::InternalServerError().body(format!("{:?}", h))
}

async fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}

start memory
image

request and response
image

Memory not released
image

@zqlpaopao
Copy link
Author

cargo.toml

actix-web = "4.0.0"
tokio = { version = "1", features = ["full","tracing"] }


@qarmin
Copy link

qarmin commented Nov 23, 2023

I also can reproduce increased memory usage both with debug and release mode.

Project - actix.zip

Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).

simplescreenrecorder-2023-11-23_12.35.54.mp4

Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function HttpResponse::InternalServerError().body(body) is never freed and

@zqlpaopao
Copy link
Author

I also can reproduce increased memory usage both with debug and release mode.

Project - actix.zip

Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).

simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function HttpResponse::InternalServerError().body(body) is never freed and

I don't think that's the problem HttpResponse:: InternalServerError(). body (body)is never freed and
Take a look at the example below me

@zqlpaopao
Copy link
Author

code

use std::io::prelude::*;
use std::net::{TcpListener, ToSocketAddrs};
use std::net::TcpStream;
use serde_json;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use serde::ser::SerializeMap;

 fn main() {
   webs()
}





fn webs() {
    let addr =  "127.0.0.1:18080";
    let socket_address = addr.to_socket_addrs().unwrap().next().unwrap();
    let listener = TcpListener::bind(socket_address).unwrap();
    let pool = ThreadPool::new(20);
    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();
        pool.execute(|| {
            handle_connection(stream);
        });
    }
    println!("Shutting down.");
}
#[derive(Serialize, Deserialize)]
struct Data1 {
    #[serde(serialize_with = "serialize_hashmap")]
    data: HashMap<i64, i64>,
}

fn serialize_hashmap<S>(map: &HashMap<i64, i64>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
{
    let mut seq = serializer.serialize_map(Some(map.len()))?;
    for (key, value) in map {
        seq.serialize_entry(key, value)?;
    }
    seq.end()
}
fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();

    println!("{:?}",String::from_utf8(Vec::from(buffer)));

    let get = b"GET / HTTP/1.1\r\n";
    let ch = b"GET /check HTTP/1.1\r\n";

    let (status_line, res) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK", "OK".to_string())
    } else if buffer.starts_with(ch) {
        let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
        test(&mut h);
        let res = serde_json::to_string(&Data1{data:h}).unwrap();
        ("HTTP/1.1 200 OK", res.to_string())
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404".to_string())
    };


    let response = format!(
        "{}\r\nContent-Length: {}\r\n\r\n{}",
        status_line,
        res.len(),
        res
    );

    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}


fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}




// src/lib.rs
use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv();

            match message {
                Ok(job) => {
                    println!("Worker {id} got a job; executing.");

                    job();
                }
                Err(_) => {
                    println!("Worker {id} disconnected; shutting down.");
                    break;
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

image

curl --location --request GET '127.0.0.1:18080/check'

image

Still not released, but if data is not returned, it can be automatically released

I also can reproduce increased memory usage both with debug and release mode.

Project - actix.zip

Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).

simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function HttpResponse::InternalServerError().body(body) is never freed and

use actix_web::{App, HttpServer};

#[tokio::main]
async fn main() {
    web().await;
}

async fn web() {
    HttpServer::new(|| {
        println!(
            "WEB LISTENING TO  {}:{}",
            "127.0.0.1",
            18080
        );
        // 在这里传入定义的服务
        App::new().service(check)
    })
        .bind(("127.0.0.1", 18080))
        .unwrap()
        .run()
        .await
        .unwrap();
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
#[get("/check")]
async fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h).await;

    HttpResponse::InternalServerError().body(format!("{:?}", "OK"))
}

async fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}

Still not released, but if data is not returned, it can be automatically released

image

image

@zqlpaopao
Copy link
Author

image

@zqlpaopao
Copy link
Author

I also can reproduce increased memory usage both with debug and release mode.

you say I also can reproduce increased memory usage both with debug and release mode.

I also can reproduce increased memory usage both with debug and release mode.

Project - actix.zip

Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).

simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function HttpResponse::InternalServerError().body(body) is never freed and

you say Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).
Do you have any information on this? I'll study it. Thank you

@douggynix
Copy link

douggynix commented Dec 18, 2023

I am using actix as well as a backend web server. And it seems like it is leaking memory. Even though the server is not receiving incoming request, the memory used never goes down and keep increasing upon new incoming request. I am seeing the same behavior (actix-web = "4.4.0"). I am using a tool for memory leak called HeapTrack, and here are some figures below.
The code is built and tested in --release mode with cargo build --release and cargo run --release

It seems like there is no deallocation of the used memory even after hours of idle period. According to HeapTrack, that memory peak allocation is coming from Actix Http Dispatcher. See the figures below for more details:

image

BackTrace of the calls that keeps growing memory usage
image

@zqlpaopao
Copy link
Author

Does anyone explain? Does it mean that Rust is not used by anyone in the production environment? It looks like it's too weak and cannot be truly used

@douggynix
Copy link

douggynix commented Dec 18, 2023

I would not state about the language. i would state about the specific library we're using. We should understand the problem and have feedbacks about why this is happening. there will be an explanation for sure about why Actix Heap memory keeps growing. No programming language is safe from Memory leak if you have a long running thread that keeps a memory reference alive. Rust can't decide to clear a variable reference if it is being used. This issue here has nothing to do with rust. Rust is used in many prod environments. I have seen it myself in action in one of the organizations i was working for. Let's focus instead on the problem instead of having let go with opinionated view about a programming language.

@zqlpaopao
Copy link
Author

You overthink it. I'm definitely using it because I trust and value it. Of course, I hope it's better, not because I have any bias against Rust

@douggynix
Copy link

Rust is just a tool, if you used it not properly it will yield undesirable effects. it's just what needed to be understood here. Hope we have an answer from the main devs of this framework. Actix is great.

@douggynix
Copy link

douggynix commented Dec 18, 2023

By the way, I also use the Same Actix.zip file, and i am able to reproduce the memory leak with a rust load testing tool called drill.

cargo install drill
drill --benchmark benchmark.yml

Here is a heaptrack memory dump you can use to analyze with heaptrack along with the project.
actix.zip

I see the memory is growing at the same component location as mine which is Actix Http Dispatcher. It clearly shows that Actix Web is leaking 5GB of memory when launching drill upon "/check" endpoint in this example. The memory leak is really relevant and should be adressed with a Pull Request.

image

image

I also can reproduce increased memory usage both with debug and release mode.

you say I also can reproduce increased memory usage both with debug and release mode.

I also can reproduce increased memory usage both with debug and release mode.
Project - actix.zip
Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).
simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function HttpResponse::InternalServerError().body(body) is never freed and

you say Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300). Do you have any information on this? I'll study it. Thank you

I al

@zqlpaopao
Copy link
Author

By the way, I also use the Same Actix.zip file, and i am able to reproduce the memory leak with a rust load testing tool called drill.

cargo install drill
drill --benchmark benchmark.yml

Here is a heaptrack memory dump you can use to analyze with heaptrack along with the project. actix.zip

I see the memory is growing at the same location as mine which is active http dispatcher. It clearly shows that Actix Web is leaking 5GB of memory when launching drill upon "/check" endpoint in this example. The memory leak is really relevant and should be adressed with a Pull Request.

image

image

I also can reproduce increased memory usage both with debug and release mode.

you say I also can reproduce increased memory usage both with debug and release mode.

I also can reproduce increased memory usage both with debug and release mode.
Project - actix.zip
Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).
simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function HttpResponse::InternalServerError().body(body) is never freed and

you say Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300). Do you have any information on this? I'll study it. Thank you

I al

Yes, what you said makes sense. I have also posted on the forum before and asked about it. Most people mean that Rust releases memory, but the system memory collector did not trigger the collection or did not reach the pressure point to release this part of the memory. At the same time, this part of the memory is expected to be used in the future to avoid duplicate applications and releases

@douggynix
Copy link

douggynix commented Dec 18, 2023

By the way, I also use the Same Actix.zip file, and i am able to reproduce the memory leak with a rust load testing tool called drill.

cargo install drill
drill --benchmark benchmark.yml

Here is a heaptrack memory dump you can use to analyze with heaptrack along with the project. actix.zip
I see the memory is growing at the same location as mine which is active http dispatcher. It clearly shows that Actix Web is leaking 5GB of memory when launching drill upon "/check" endpoint in this example. The memory leak is really relevant and should be adressed with a Pull Request.
image
image

I also can reproduce increased memory usage both with debug and release mode.

you say I also can reproduce increased memory usage both with debug and release mode.

I also can reproduce increased memory usage both with debug and release mode.
Project - actix.zip
Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).
simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function HttpResponse::InternalServerError().body(body) is never freed and

you say Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300). Do you have any information on this? I'll study it. Thank you

I al

Yes, what you said makes sense. I have also posted on the forum before and asked about it. Most people mean that Rust releases memory, but the system memory collector did not trigger the collection or did not reach the pressure point to release this part of the memory. At the same time, this part of the memory is expected to be used in the future to avoid duplicate applications and releases

Yes, your observation is right. and I also try to use "actix::main" wrapper instead of "tokio::main" , and it yields the same. I think, there may be leaked references. Rust ensures from compilation that active memory references are still valid. So, from a developer perspective, you may think a variable is not used. but, in the underlying system, there may be an active reference pointing to it. and being used by a sleeping thread. Actix is running on daemon mod, the variable lifecycle is bound to the threads maintaining alive by Tokio. On runtime, if there is any active reference pointing to a heap memory location, there can be memory Leaks. and rust official documentation states that as well with the use of RefCell smart pointers there can be memory leak if not being used properly. There is no where in rust documentation that they mention there can't be memory leak.
There should be a closer look between Tokio and the way Actix Web uses variables that serializes data as response to the Client. in your case, you return a Hashmap with 100 000 values, and it seems that Actix Responder after serializing that data to the client still uses reference that avoids those values to be dropped. I think that can be the root cause of that memory leaks that makes memory usage grows everytime without going down.

@qarmin
Copy link

qarmin commented Dec 18, 2023

This issue may be related to this problem - rust-lang/rust#73307

@douggynix
Copy link

This issue may be related to this problem - rust-lang/rust#73307

@qarmin Thanks for pointing us to one similar issue opened at Rust lang. It seems like the issue is closed without code change. or any sort of solution on how to fix it.

@zqlpaopao
Copy link
Author

This issue may be related to this problem - rust-lang/rust#73307

@qarmin Thanks for pointing us to one similar issue opened at Rust lang. It seems like the issue is closed without code change. or any sort of solution on how to fix it.

I was thinking that this should be a very, very common question, but there is very little relevant information

@zhuxiujia
Copy link

zhuxiujia commented Dec 19, 2023

By the way, I also use the Same Actix.zip file, and i am able to reproduce the memory leak with a rust load testing tool called drill.

cargo install drill
drill --benchmark benchmark.yml

Here is a heaptrack memory dump you can use to analyze with heaptrack along with the project. actix.zip
I see the memory is growing at the same location as mine which is active http dispatcher. It clearly shows that Actix Web is leaking 5GB of memory when launching drill upon "/check" endpoint in this example. The memory leak is really relevant and should be adressed with a Pull Request.
image
image

I also can reproduce increased memory usage both with debug and release mode.

you say I also can reproduce increased memory usage both with debug and release mode.

I also can reproduce increased memory usage both with debug and release mode.
Project - actix.zip
Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).
simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function is never freed andHttpResponse::InternalServerError().body(body)

you say Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300). Do you have any information on this? I'll study it. Thank you

I al

Yes, what you said makes sense. I have also posted on the forum before and asked about it. Most people mean that Rust releases memory, but the system memory collector did not trigger the collection or did not reach the pressure point to release this part of the memory. At the same time, this part of the memory is expected to be used in the future to avoid duplicate applications and releases

I agree. On Mac OS, my memory seems to grow between 29M-30M and may return to 29M after a few attempts
my rustc version is rustc 1.76.0-nightly (3f28fe133 2023-12-18)

@zqlpaopao
Copy link
Author

By the way, I also use the Same Actix.zip file, and i am able to reproduce the memory leak with a rust load testing tool called drill.

cargo install drill
drill --benchmark benchmark.yml

Here is a heaptrack memory dump you can use to analyze with heaptrack along with the project. actix.zip
I see the memory is growing at the same location as mine which is active http dispatcher. It clearly shows that Actix Web is leaking 5GB of memory when launching drill upon "/check" endpoint in this example. The memory leak is really relevant and should be adressed with a Pull Request.
image
image

I also can reproduce increased memory usage both with debug and release mode.

you say I also can reproduce increased memory usage both with debug and release mode.

I also can reproduce increased memory usage both with debug and release mode.
Project - actix.zip
Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).
simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function is never freed andHttpResponse::InternalServerError().body(body)

you say Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300). Do you have any information on this? I'll study it. Thank you

I al

Yes, what you said makes sense. I have also posted on the forum before and asked about it. Most people mean that Rust releases memory, but the system memory collector did not trigger the collection or did not reach the pressure point to release this part of the memory. At the same time, this part of the memory is expected to be used in the future to avoid duplicate applications and releases

I agree. On Mac OS, my memory seems to grow between 29M-30M and may return to 29M after a few attempts my rustc version is rustc 1.76.0-nightly (3f28fe133 2023-12-18)

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web().await;
}

#[derive(Serialize)]
struct Res{
    h : Vec<i64>
}

impl Responder for Res{
    type Body = BoxBody;
    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
        let body  = serde_json::to_string(&self).unwrap();
        HttpResponse::Ok()
            .content_type(ContentType::json())
            .body(body)
    }
}

impl Drop for Res{
    fn drop(&mut self) {
        println!("drop res")
    }
}

async fn web() {
    HttpServer::new(|| {
        println!(
            "WEB LISTENING TO  {}:{}",
            "127.0.0.1",
            18080
        );
        // 在这里传入定义的服务
        App::new().service(check)
    })
        .bind(("127.0.0.1", 18080))
        .unwrap()
        .run()
        .await
        .unwrap();
}

WEB LISTENING TO  127.0.0.1:18080
WEB LISTENING TO  127.0.0.1:18080
WEB LISTENING TO  127.0.0.1:18080
start
drop res
start
drop res
start
drop res

This request output shows that res was dropped after use

@douggynix
Copy link

douggynix commented Dec 19, 2023

By the way, I also use the Same Actix.zip file, and i am able to reproduce the memory leak with a rust load testing tool called drill.

cargo install drill
drill --benchmark benchmark.yml

Here is a heaptrack memory dump you can use to analyze with heaptrack along with the project. actix.zip
I see the memory is growing at the same location as mine which is active http dispatcher. It clearly shows that Actix Web is leaking 5GB of memory when launching drill upon "/check" endpoint in this example. The memory leak is really relevant and should be adressed with a Pull Request.
image
image

I also can reproduce increased memory usage both with debug and release mode.

you say I also can reproduce increased memory usage both with debug and release mode.

I also can reproduce increased memory usage both with debug and release mode.
Project - actix.zip
Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300).
simplescreenrecorder-2023-11-23_12.35.54.mp4
Valgrind not show any memory leaks

==190166== HEAP SUMMARY:
==190166==     in use at exit: 32,452 bytes in 84 blocks
==190166==   total heap usage: 3,632 allocs, 3,548 frees, 237,465,785 bytes allocated
==190166== 
==190166== LEAK SUMMARY:
==190166==    definitely lost: 0 bytes in 0 blocks
==190166==    indirectly lost: 0 bytes in 0 blocks
==190166==      possibly lost: 1,988 bytes in 7 blocks
==190166==    still reachable: 30,464 bytes in 77 blocks
==190166==         suppressed: 0 bytes in 0 blocks
==190166== Rerun with --leak-check=full to see details of leaked memory
==190166== 
==190166== For lists of detected and suppressed errors, rerun with: -s
==190166== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It is possible that data passed to body function is never freed andHttpResponse::InternalServerError().body(body)

you say Maybe this is problem with memory allocator, that not have enough pressure to clear memory and do this only when necessary(for me, when I got ~300). Do you have any information on this? I'll study it. Thank you

I al

Yes, what you said makes sense. I have also posted on the forum before and asked about it. Most people mean that Rust releases memory, but the system memory collector did not trigger the collection or did not reach the pressure point to release this part of the memory. At the same time, this part of the memory is expected to be used in the future to avoid duplicate applications and releases

I agree. On Mac OS, my memory seems to grow between 29M-30M and may return to 29M after a few attempts my rustc version is rustc 1.76.0-nightly (3f28fe133 2023-12-18)

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web().await;
}

#[derive(Serialize)]
struct Res{
    h : Vec<i64>
}

impl Responder for Res{
    type Body = BoxBody;
    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
        let body  = serde_json::to_string(&self).unwrap();
        HttpResponse::Ok()
            .content_type(ContentType::json())
            .body(body)
    }
}

impl Drop for Res{
    fn drop(&mut self) {
        println!("drop res")
    }
}

async fn web() {
    HttpServer::new(|| {
        println!(
            "WEB LISTENING TO  {}:{}",
            "127.0.0.1",
            18080
        );
        // 在这里传入定义的服务
        App::new().service(check)
    })
        .bind(("127.0.0.1", 18080))
        .unwrap()
        .run()
        .await
        .unwrap();
}
WEB LISTENING TO  127.0.0.1:18080
WEB LISTENING TO  127.0.0.1:18080
WEB LISTENING TO  127.0.0.1:18080
start
drop res
start
drop res
start
drop res

This request output shows that res was dropped after use

It seems it has to do with the underlying system allocator as well as argued in the issue closed by one of the rust maintainer.
One in the thread suggested that he has seen improvement by using a custom global memory allocator which is "jemalloc".
I tried it. the memory usage gets improved and may go up and down. but it doesn't completely free away the memory that's being deallocated. There is a thin line between the operating system allocator and the one used by rust (malloc subsystem calls) to manage memory allocation and deallocation.

Memory issue is being seen with some object types like Hashmap or vectors because they reside on the Heap and not on the stack. And actix web as well is doing some heavy lifting using those types as wells.

@douggynix
Copy link

This issue may be related to this problem - rust-lang/rust#73307

@zqlpaopao Thanks once again for this link above about the issue which is opened to Rust lang itself.
I found out with jemalloc library preloaded before running my rust application gives a little improvement to memory consumption. Memory may go up and down. And the memory usage is reasonable. But, it doesn't solve the leak effect.
I used it myself on my linux system (require to install jemalloc package for your distribution or container image), here is how i run my app with "jemalloc". Make sure you install it before running this command

LD_PRELOAD=/usr/lib/libjemalloc.so.2 cargo run --release

Without having to use memory allocator crate that will grow my executable after compilation, it really performs well and my actix web application responds even better under heavy load testing with 8000 concurrent connections with 200 000 http requests that makes access to database to return info.

I even use it as well under alpine linux. require to set the environment variable for your container with the one above such that:

LD_PRELOAD=/usr/lib/libjemalloc.so.2 cargo

That should help a bit those who are struggling with their heap memory that grows(especially under linux/unix like system).
I have not tried to compile jemalloc for windows and tried to use it. That can be an alternative or pain reliever for those who are struggling with this on a production environment.

@zqlpaopao
Copy link
Author

This issue may be related to this problem - rust-lang/rust#73307

@zqlpaopao Thanks once again for this link above about the issue which is opened to Rust lang itself. I found out with jemalloc library preloaded before running my rust application gives a little improvement to memory consumption. Memory may go up and down. And the memory usage is reasonable. But, it doesn't solve the leak effect. I used it myself on my linux system (require to install jemalloc package for your distribution or container image), here is how i run my app with "jemalloc". Make sure you install it before running this command

LD_PRELOAD=/usr/lib/libjemalloc.so.2 cargo run --release

Without having to use memory allocator crate that will grow my executable after compilation, it really performs well and my actix web application responds even better under heavy load testing with 8000 concurrent connections with 200 000 http requests that makes access to database to return info.

I even use it as well under alpine linux. require to set the environment variable for your container with the one above such that:

LD_PRELOAD=/usr/lib/libjemalloc.so.2 cargo

That should help a bit those who are struggling with their heap memory that grows(especially under linux/unix like system). I have not tried to compile jemalloc for windows and tried to use it. That can be an alternative or pain reliever for those who are struggling with this on a production environment.

Thank you for the experiment and answers. Below are my test results

If I don't specify the size of the memory allocator startup to be around 7.1m

./test
image

If I specify

LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 ./test
image

I have made 100 concurrent requests and my memory is growing

image

After the request ends, the memory returns to a result that is not much different from the initial value
image

@douggynix
Copy link

This issue may be related to this problem - rust-lang/rust#73307

@zqlpaopao Thanks once again for this link above about the issue which is opened to Rust lang itself. I found out with jemalloc library preloaded before running my rust application gives a little improvement to memory consumption. Memory may go up and down. And the memory usage is reasonable. But, it doesn't solve the leak effect. I used it myself on my linux system (require to install jemalloc package for your distribution or container image), here is how i run my app with "jemalloc". Make sure you install it before running this command

LD_PRELOAD=/usr/lib/libjemalloc.so.2 cargo run --release

Without having to use memory allocator crate that will grow my executable after compilation, it really performs well and my actix web application responds even better under heavy load testing with 8000 concurrent connections with 200 000 http requests that makes access to database to return info.
I even use it as well under alpine linux. require to set the environment variable for your container with the one above such that:

LD_PRELOAD=/usr/lib/libjemalloc.so.2 cargo

That should help a bit those who are struggling with their heap memory that grows(especially under linux/unix like system). I have not tried to compile jemalloc for windows and tried to use it. That can be an alternative or pain reliever for those who are struggling with this on a production environment.

Thank you for the experiment and answers. Below are my test results

If I don't specify the size of the memory allocator startup to be around 7.1m

./test
image

If I specify

LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 ./test
image

I have made 100 concurrent requests and my memory is growing
image

After the request ends, the memory returns to a result that is not much different from the initial value image

@zqlpaopao
Jemalloc doesn't actually fix totally the memory allocation issue, it does a bit reduce its usage. It can grow but at a later time, reduce it though it's not complete to our expectation. I deep dive myself into the topic and does a lot of reading. This may have not to do with rust. This may have to do with the system default memory allocator. For linux, we're using GLibc malloc. And GLibc Malloc is the default memory allocator provided to all processes if not overriden. Hence, our rust process is using GLibc malloc. GLibC malloc can only return memory freed by processes to the Operating System by the order they were allocated. If a chunk of memory is freed before a previous allocated one. It won't be returned by GlibC malloc to the Operating system. So, memory allocations and time usage depends on applications and it's beyond Rust itself.

And it seems like all system memory allocators are trying to solve one common problem which is "Memory Fragmentation".

And i stumble on upon a blog From CloudFlare team that was dealing with a similar issue we're dealing with. And they really explain it very clear. Google has implemented a memory allocator called TCMalloc. And it seems that it solves their problem.
Facebook had also given it a try. All the big giants as well has been dealing with the issue we're dealing with now. we're just new to it . I am going to give TCMalloc a try.
https://blog.cloudflare.com/the-effect-of-switching-to-tcmalloc-on-rocksdb-memory-use

image
image

@douggynix
Copy link

douggynix commented Dec 20, 2023

I did a test with libtcmalloc.so as LD_PRELOAD. it doesn't perform better than libjemalloc.so in my case. For my scenario with actix jemalloc really performs very well. This is a view of my process and memory consumption and deallocation in real time seen with atop linux utility. To have this view on atop, you have to press "m" to toggle the memory usage panel view.

With Jemalloc it really decreases my memory by looking at the VGROW and RGROW field with minus values "-0.2G" and "-0.1G" i.e reduces the virtual memory to 200M and the RAM to 100M. Jemalloc perform better for my case by launching my app with

LD_PRELOAD=/usr/lib/libjemalloc.so ./my_application

image

Feel free to use the malloc implementation that performs better in your case either the default one provided by your Operating Systems(Linux Glibc Malloc), or a custom one like TCMalloc(from Google), Jemalloc and so on.

Jemalloc is being maintained by Facebook.
Here is a list of other custom system allocators you may test that may suite your scenarios:
https://github.com/daanx/mimalloc-bench

@zqlpaopao
Copy link
Author

我用libtcmalloc.so作为 LD_PRELOAD进行了测试。在我的例子中,它的性能并不比libjemalloc.so更好。对于我使用 actix jemalloc 的场景来说,它确实表现得非常好。这是使用atop linux 实用程序实时查看我的进程、内存消耗和释放的视图。要将此视图置于顶部,您必须按“m”来切换内存使用面板视图。

使用 Jemalloc,它通过查看VGROWRGROW字段的负值“-0.2G”和“-0.1G”确实减少了我的内存,即将虚拟内存减少到 200M,将 RAM 减少到 100M。Jemalloc通过启动我的应用程序来更好地处理我的情况

LD_PRELOAD=/usr/lib/libjemalloc.so ./my_application

图像

请随意使用在您的情况下表现更好的 malloc 实现,可以是操作系统提供的默认实现 ( Linux Glibc Malloc ),也可以是自定义实现,例如TCMalloc(来自 Google)、Jemalloc等。

Jemalloc 由 Facebook 维护。 以下是您可以测试的其他自定义系统分配器的列表,这些分配器可能适合您的场景: https: //github.com/daanx/mimalloc-bench

I have a requirement to run an agent monitoring program on Linux, but the requirement is that the memory cannot exceed 100mb, otherwise it will be killed. Therefore, I have encountered two problems: if libjemalloc is used, the memory will be a bit high, and for single threads, it still needs to reach 200mb+. TCmalloc has not been tested yet, but from your test screenshot, it is not low. If glibc is used, the initialization memory is not high, but it continues to increase, and it will also be killed. Is there a method for initializing the memory size under the control of libjemalloc, which would be perfect

@douggynix
Copy link

@zqlpaopao I am wondering by curiosity what's your target hardware. is it an embedded system or a bare metal , Virtual machine, a Container deployed under Kubernetes with such limitation about memory. Or is this program will be running under a LXC cgroup which limits the maximum memory. In either scenario, i don't think a memory allocator is gonna solve your problem.
I am not seeing any option from Jemalloc to set Maximum memory size. Jemalloc is implementing the kernel interface for memory allocation for "malloc" calls. I don't know if you set that with an environment Variables.

With that requirement, it means that you really need to have control over the memory usage. Allocators behavior won't solve that issue. No matter which one you choose. For example, i ran my app within a Alpine Docker container image of 17MB of size.
And Alpine is using Musl LibC which is another implementation of Glibc and embeds as well it's own memory allocator.
Memory usage is 50% lower than if i were running the app on my Linux distrib which based on GLibc.
Musl seems to perform at a similar rate as Jemalloc.

@zqlpaopao
Copy link
Author

@zqlpaopao我好奇地想知道你的目标硬件是什么。它是嵌入式系统还是裸机、虚拟机、部署在 Kubernetes 下的容器,对内存有这样的限制。或者这个程序将在限制最大内存的 LXC cgroup 下运行。在任何一种情况下,我认为内存分配器都不能解决你的问题。 我没有看到 Jemalloc 提供任何设置最大内存大小的选项。Jemalloc 正在实现“malloc”调用内存分配的内核接口。我不知道你是否使用环境变量设置了它。

有了这个要求,这意味着您确实需要控制内存的使用。分配者的行为并不能解决这个问题。无论您选择哪一个。例如,我在大小为 17MB 的Alpine Docker 容器映像中运行我的应用程序。 Alpine 使用 Musl LibC,它是 Glibc 的另一种实现,并且嵌入了它自己的内存分配器。 与我在基于 GLibc 的 Linux 发行版上运行该应用程序相比,内存使用量减少了 50%。 Musl 的表现似乎与****Jemalloc相似。
Yes, I actually run it on a physical machine, which is a daemon in Linux. However, it requires a lot of memory usage, so it is currently not possible to use Rust. However, with Golang, the CPU of the program will consume and fluctuate due to the need for GC

@douggynix
Copy link

if you revert back to using glibc , don't do library preload with LD_PRELOAD.
Set this variable and run your program with it.

MALLOC_ARENA_MAX=2 run_my_program

https://devcenter.heroku.com/articles/tuning-glibc-memory-behavior

@douggynix
Copy link

I would advise you as well to run your application outside of your IDE(Integrated Development Environment) such as Vscode shell or Intellij IDEA shell.
I am getting different memory usage figure. I see my app consumes less memory when i run the application out of the shell created from the IDE.
Run your program directly from Gnome or your Linux terminal. My program doesn't exceed 100MB when i am running it under heavy load from there.
image

install atop utility on your machine to monitor memory usage. if you press "m" it will toggle the memory panel for all the processes. and you will have a view similar like mine

@zqlpaopao
Copy link
Author

我还建议您在 IDE(集成开发环境)之外运行您的应用程序,例如 Vscode shell 或 Intellij IDEA shell。 我得到了不同的内存使用情况。当我从 IDE 创建的 shell 中运行应用程序时,我发现我的应用程序消耗的内存更少。 直接从 Gnome 或 Linux 终端运行您的程序。当我在重负载下运行我的程序时,它不会超过 100MB。 图像

在您的计算机上安装 atop 实用程序来监视内存使用情况。如果您按“m”,它将切换所有进程的内存面板。你会看到和我类似的景色

My experiments were all conducted on Centos7 in a Linux environment. Is there a large amount of concurrent memory in those allocators? How about the memory usage in multi-threaded mode after a thousand requests

@douggynix
Copy link

douggynix commented Dec 21, 2023

Each memory allocator has different algorithm and heuristics. I don't know what type of allocator CentOs uses by default whether or not it is the slab allocator provided along with the linux kernel.
Me, i am using an Archlinux Distribution with the latest kernel version 6.6.3. What's your Centos Kernel version ?

I would advise you to install Docker on your CentOs Machine and

docker pull alpine:latest
docker run --rm  -it  --name my_container  alpine:latest sh

Open another shell and run this command to push your program onto the docker image:

docker push target/release/my_program my_container:/bin/

Go back to your docker container shell prompt opened previously. and run your program:

/bin/my_program

Go to your linux host and use htop or any other similar utility to look at your program memory usage

@zqlpaopao
Copy link
Author

每个内存分配器都有不同的算法和启发式方法。我不知道CentOs默认使用什么类型的分配器,是否是 随linux内核提供的slab分配器。 我正在使用带有最新内核版本6.6.3 的Archlinux 发行版。你的 Centos 内核版本是多少?

我建议你在 CentOs 机器上安装 Docker,然后

docker pull alpine:latest
docker run --rm  -it  --name my_container  alpine:latest sh

打开另一个 shell 并运行以下命令将程序推送到 docker 映像上:

docker push target/release/my_program my_container:/bin/

返回到之前打开的 docker 容器 shell 提示符。并运行你的程序:

/bin/my_program

转到您的 Linux 主机并使用 htop 或任何其他类似的实用程序来查看程序内存使用情况

centos7 3.10.0-327.28.3.el7.x86_64

@douggynix
Copy link

douggynix commented Dec 21, 2023

I am wondering why you're choosing CENTOS 7 as RedHat will no longer support CENTOS 7 within 6 months. The kernel you're using is very old comparing to the latest state of the kernel development and improvement added. This centOs will reach its end of life support in June 2024. I would consider moving to another distribution as this one will no longer receives updates any time soon. Why don't you move onto long term support Linux server distribution such as Debian or Ubuntu that has the latest kernels.
you should try to test your rust application on newer distribution than this old one.

image
image

image

@zqlpaopao
Copy link
Author

我想知道为什么您选择 CENTOS 7,因为 RedHat 将在 6 个月内不再支持 CENTOS 7。与内核开发和改进的最新状态相比,您使用的内核非常旧。这个 centOs 将于 2024 年 6 月达到生命周期支持结束。我会考虑转移到另一个发行版,因为这个发行版很快将不再收到更新。为什么不转向长期支持 Linux 服务器发行版,例如具有最新内核的 D​​ebian 或 Ubuntu。 您应该尝试在比旧发行版更新的发行版上测试您的 Rust 应用程序。

图像 图像

图像

The main reason is that the company has over 300000 servers, most of which are versions of Centos7. Currently, there are relatively few versions of Centos8, and the company's business and traffic usually do not allow for large-scale system replacement, which poses great risks. However, the program needs to adapt to Centos7 and 8

@douggynix
Copy link

Oh you have your organizations constraints. So, they will be forced anyway to move away from it anytime if obsolescence and security/vulnerabilty will be exposed them to risk of being hacked.
Does your app need to run exactly on the server to do what it needs to do? Or can it be run within a docker container to see the difference if the host system memory allocator is not a good fit for you?

@zqlpaopao
Copy link
Author

哦,你有你的组织限制。因此,如果过时和安全/漏洞将使他们面临被黑客攻击的风险,他们将被迫随时放弃它。 您的应用程序是否需要完全在服务器上运行才能完成其需要执行的操作?或者,如果主机系统内存分配器不适合您,它可以在 docker 容器中运行以查看差异吗?

Well, I'll try running it on Centos8 or Docker to see the results

@douggynix
Copy link

Try to run it under alpine:3.18 docker image. this image has a small size 17MB. and a small memory footprint.

@zqlpaopao
Copy link
Author

Try to run it under alpine:3.18 docker image. this image has a small size 17MB. and a small memory footprint.

thank you very much ,i will for trying it out

@zhuxiujia
Copy link

zhuxiujia commented Dec 21, 2023

Actually, I want to determine whether it is caused by the underlying rust or by the actix-web. Currently, I have tried tokio+HashMap and there have been no memory issues

@douggynix
Copy link

douggynix commented Dec 21, 2023

are you running your program from inside the IDEA Tools like IDE Shell.
Or do you run it outside all of that. I notice that running the process outside of the IDEA in my case makes the memory drops at least by 50%. I am using RustRover as my IDE of choice for rust programming. But, running your native process under this IDE for heavy load test will make it inherit the memory allocator from your IDE.
image

Here is the result when i run the rust application from a separate terminal outside of my IDE:
image

Depending on the context you run your application, such as the IDE, the Operating System, if it's linux what's the kernel version. And what is the underlying memory allocator your system is using. Those details really make sense for troubleshooting.

@zqlpaopao
Copy link
Author

其实我想确定是底层生锈造成的还是actix-web. 目前,我尝试过tokio+HashMap,没有出现内存问题

Hello, do you have the corresponding code? As you said, I tested it with Tokio. This is my code, but the last memory was not released. I ran it on Mac m1, and I feel that it is related to the memory allocator

code

// use link_inspection::controller::init::config::Config;
// use link_inspection::controller::init::web::web;
// use link_inspection::model::mysql::proxy::MyPool;
// #[tokio::main]
// async fn main() {
//     Config::default();
//     MyPool::default();
//     web().await;
// }

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web();
    std::thread::sleep(std::time::Duration::from_secs(1000));
}

 fn web()  {
    // HttpServer::new(|| {
    //     println!(
    //         "WEB LISTENING TO  {}:{}",
    //         "127.0.0.1",
    //         18080
    //     );
    //     // 在这里传入定义的服务
    //     App::new().service(check)
    // })
    //     .workers(5)
    //     .bind(("0.0.0.0", 18080))
    //     .unwrap()
    //     .run()
    //     .await
    //     .unwrap();
    for i in 0..10{
        std::thread::sleep(std::time::Duration::from_secs(30));

        check();
    }
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
// #[get("/check")]
 fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h);

    HttpResponse::InternalServerError().body(format!("{:?}", h))
}

 fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}


image

end

start
start
start
start
start
start
start
start
start
start


image

@zqlpaopao
Copy link
Author

Actually, I want to determine whether it is caused by the underlying rust or by the actix-web. Currently, I have tried tokio+HashMap and there have been no memory issues

I also ran the same code on Linux

This is the memory for program initialization

image

The memory used for executing this program
image

In Actix_web The return of the web is defined as follows

pub trait Responder {
    type Body: MessageBody + 'static;

    /// Convert self to `HttpResponse`.
    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body>;

    /// Wraps responder to allow al
...

Is it related to the static lifecycle of the return value that it has not been released? If so, the memory will become larger and larger as the request volume increases

@zqlpaopao
Copy link
Author

您是从 IDEA 工具(例如 IDE Shell)内部运行您的程序吗? 或者你在这一切之外运行它。我注意到在我的例子中,在 IDEA 之外运行进程会使内存至少下降 50%。我使用 RustRover 作为 Rust 编程的首选 IDE。但是,在此 IDE 下运行本机进程进行重负载测试将使其继承 IDE 的内存分配器。 图像

以下是我从 IDE 之外的单独终端运行 rust 应用程序时的结果: 图像

根据您运行应用程序的上下文,例如 IDE、操作系统,如果是 Linux,则内核版本是什么。您的系统正在使用的底层内存分配器是什么。这些细节对于故障排除确实很有意义。

My tests were all conducted on Centos7 version on Linux

are you running your program from inside the IDEA Tools like IDE Shell. Or do you run it outside all of that. I notice that running the process outside of the IDEA in my case makes the memory drops at least by 50%. I am using RustRover as my IDE of choice for rust programming. But, running your native process under this IDE for heavy load test will make it inherit the memory allocator from your IDE. image

Here is the result when i run the rust application from a separate terminal outside of my IDE: image

Depending on the context you run your application, such as the IDE, the Operating System, if it's linux what's the kernel version. And what is the underlying memory allocator your system is using. Those details really make sense for troubleshooting.

My tests were all conducted on Centos7 version on Linux 3.10.0-327.28.3.el7.x86_64

@zhuxiujia
Copy link

zhuxiujia commented Dec 22, 2023

其实我想确定是底层生锈造成的还是actix-web. 目前,我尝试过tokio+HashMap,没有出现内存问题

Hello, do you have the corresponding code? As you said, I tested it with Tokio. This is my code, but the last memory was not released. I ran it on Mac m1, and I feel that it is related to the memory allocator

code

// use link_inspection::controller::init::config::Config;
// use link_inspection::controller::init::web::web;
// use link_inspection::model::mysql::proxy::MyPool;
// #[tokio::main]
// async fn main() {
//     Config::default();
//     MyPool::default();
//     web().await;
// }

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web();
    std::thread::sleep(std::time::Duration::from_secs(1000));
}

 fn web()  {
    // HttpServer::new(|| {
    //     println!(
    //         "WEB LISTENING TO  {}:{}",
    //         "127.0.0.1",
    //         18080
    //     );
    //     // 在这里传入定义的服务
    //     App::new().service(check)
    // })
    //     .workers(5)
    //     .bind(("0.0.0.0", 18080))
    //     .unwrap()
    //     .run()
    //     .await
    //     .unwrap();
    for i in 0..10{
        std::thread::sleep(std::time::Duration::from_secs(30));

        check();
    }
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
// #[get("/check")]
 fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h);

    HttpResponse::InternalServerError().body(format!("{:?}", h))
}

 fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}

image

end

start
start
start
start
start
start
start
start
start
start

image

this is my test code(System: MacOS M1 CPU 32GMEM)

  • test_vec There is no memory issue with the Vec data structure("all done" is 2.3MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        test_vec(Vec::new());
        //test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}
  • test_hashmap There is have memory overflow (when 'all done,now wait' here is 32MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        //test_vec(Vec::new());
        test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}

This seems to be a leak(HashMap) on macos, But most of the memory has indeed been released, and I guess the rest should be memory fragments。
I guess maybe the hashmap structure is more complex than vec and should generate more memory fragments

@zhuxiujia
Copy link

其实我想确定是底层生锈造成的还是actix-web. 目前,我尝试过tokio+HashMap,没有出现内存问题

Hello, do you have the corresponding code? As you said, I tested it with Tokio. This is my code, but the last memory was not released. I ran it on Mac m1, and I feel that it is related to the memory allocator
code

// use link_inspection::controller::init::config::Config;
// use link_inspection::controller::init::web::web;
// use link_inspection::model::mysql::proxy::MyPool;
// #[tokio::main]
// async fn main() {
//     Config::default();
//     MyPool::default();
//     web().await;
// }

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web();
    std::thread::sleep(std::time::Duration::from_secs(1000));
}

 fn web()  {
    // HttpServer::new(|| {
    //     println!(
    //         "WEB LISTENING TO  {}:{}",
    //         "127.0.0.1",
    //         18080
    //     );
    //     // 在这里传入定义的服务
    //     App::new().service(check)
    // })
    //     .workers(5)
    //     .bind(("0.0.0.0", 18080))
    //     .unwrap()
    //     .run()
    //     .await
    //     .unwrap();
    for i in 0..10{
        std::thread::sleep(std::time::Duration::from_secs(30));

        check();
    }
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
// #[get("/check")]
 fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h);

    HttpResponse::InternalServerError().body(format!("{:?}", h))
}

 fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}

image
end

start
start
start
start
start
start
start
start
start
start

image

this is my test code(System: MacOS M1 CPU 32GMEM)

  • test_vec There is no memory issue with the Vec data structure("all done" is 2.3MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        test_vec(Vec::new());
        //test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}
  • test_hashmap There is have memory overflow (when 'all done,now wait' here is 32MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        //test_vec(Vec::new());
        test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}

This seems to be a leak(HashMap) on macos, But most of the memory has indeed been released, and I guess the rest should be memory fragments。 I guess maybe the hashmap structure is more complex than vec and should generate more memory fragments

HashMap is constantly inserting data, which may cause memory reallocation. When HashMap expands or redistributes internal nodes, it may allocate more memory to accommodate more elements, which may lead to an increase in memory usage. In addition, the internal structure of HashMap may lead to fragmentation, especially after a large number of insertion and deletion operations.
but.we can change code to

  • after done,this is (1.9MB mem, no leak )
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        test_hashmap(HashMap::with_capacity(10000000));
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
    println!("ok");
}

@zqlpaopao
Copy link
Author

其实我想确定是底层生锈造成的还是actix-web. 目前,我尝试过tokio+HashMap,没有出现内存问题

Hello, do you have the corresponding code? As you said, I tested it with Tokio. This is my code, but the last memory was not released. I ran it on Mac m1, and I feel that it is related to the memory allocator
code

// use link_inspection::controller::init::config::Config;
// use link_inspection::controller::init::web::web;
// use link_inspection::model::mysql::proxy::MyPool;
// #[tokio::main]
// async fn main() {
//     Config::default();
//     MyPool::default();
//     web().await;
// }

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web();
    std::thread::sleep(std::time::Duration::from_secs(1000));
}

 fn web()  {
    // HttpServer::new(|| {
    //     println!(
    //         "WEB LISTENING TO  {}:{}",
    //         "127.0.0.1",
    //         18080
    //     );
    //     // 在这里传入定义的服务
    //     App::new().service(check)
    // })
    //     .workers(5)
    //     .bind(("0.0.0.0", 18080))
    //     .unwrap()
    //     .run()
    //     .await
    //     .unwrap();
    for i in 0..10{
        std::thread::sleep(std::time::Duration::from_secs(30));

        check();
    }
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
// #[get("/check")]
 fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h);

    HttpResponse::InternalServerError().body(format!("{:?}", h))
}

 fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}

image
end

start
start
start
start
start
start
start
start
start
start

image

this is my test code(System: MacOS M1 CPU 32GMEM)

  • test_vec There is no memory issue with the Vec data structure("all done" is 2.3MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        test_vec(Vec::new());
        //test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}
  • test_hashmap There is have memory overflow (when 'all done,now wait' here is 32MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        //test_vec(Vec::new());
        test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}

This seems to be a leak(HashMap) on macos, But most of the memory has indeed been released, and I guess the rest should be memory fragments。 I guess maybe the hashmap structure is more complex than vec and should generate more memory fragments

HashMap is constantly inserting data, which may cause memory reallocation. When HashMap expands or redistributes internal nodes, it may allocate more memory to accommodate more elements, which may lead to an increase in memory usage. In addition, the internal structure of HashMap may lead to fragmentation, especially after a large number of insertion and deletion operations. but.we can change code to

  • after done,this is (1.9MB mem, no leak )
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        test_hashmap(HashMap::with_capacity(10000000));
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
    println!("ok");
}

Yes, you don't have this one, but it seems like you and I created a new object in a function and then released it. After generating a reference, it was not released, but the scope of the drink was also exceeded. This seems to be caused by the language itself

Are you Chinese? How about adding contact information to discuss together

@zhuxiujia
Copy link

Finally, I suggest that

  • actix-web must use HashMap::with_capacity not HashMap::new

  • the code of the questioner should be changed to
    async fn test () -> HashMap<usize,usize>{ let len = 1000000; let mut h: HashMap<usize,usize> = HashMap::with_capacity(len); for v in 0..len{ h.insert(v,v); } h }

@zhuxiujia
Copy link

其实我想确定是底层生锈造成的还是actix-web. 目前,我尝试过tokio+HashMap,没有出现内存问题

Hello, do you have the corresponding code? As you said, I tested it with Tokio. This is my code, but the last memory was not released. I ran it on Mac m1, and I feel that it is related to the memory allocator
code

// use link_inspection::controller::init::config::Config;
// use link_inspection::controller::init::web::web;
// use link_inspection::model::mysql::proxy::MyPool;
// #[tokio::main]
// async fn main() {
//     Config::default();
//     MyPool::default();
//     web().await;
// }

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web();
    std::thread::sleep(std::time::Duration::from_secs(1000));
}

 fn web()  {
    // HttpServer::new(|| {
    //     println!(
    //         "WEB LISTENING TO  {}:{}",
    //         "127.0.0.1",
    //         18080
    //     );
    //     // 在这里传入定义的服务
    //     App::new().service(check)
    // })
    //     .workers(5)
    //     .bind(("0.0.0.0", 18080))
    //     .unwrap()
    //     .run()
    //     .await
    //     .unwrap();
    for i in 0..10{
        std::thread::sleep(std::time::Duration::from_secs(30));

        check();
    }
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
// #[get("/check")]
 fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h);

    HttpResponse::InternalServerError().body(format!("{:?}", h))
}

 fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}

image
end

start
start
start
start
start
start
start
start
start
start

image

this is my test code(System: MacOS M1 CPU 32GMEM)

  • test_vec There is no memory issue with the Vec data structure("all done" is 2.3MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        test_vec(Vec::new());
        //test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}
  • test_hashmap There is have memory overflow (when 'all done,now wait' here is 32MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        //test_vec(Vec::new());
        test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}

This seems to be a leak(HashMap) on macos, But most of the memory has indeed been released, and I guess the rest should be memory fragments。 I guess maybe the hashmap structure is more complex than vec and should generate more memory fragments

HashMap is constantly inserting data, which may cause memory reallocation. When HashMap expands or redistributes internal nodes, it may allocate more memory to accommodate more elements, which may lead to an increase in memory usage. In addition, the internal structure of HashMap may lead to fragmentation, especially after a large number of insertion and deletion operations. but.we can change code to

  • after done,this is (1.9MB mem, no leak )
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        test_hashmap(HashMap::with_capacity(10000000));
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
    println!("ok");
}

Yes, you don't have this one, but it seems like you and I created a new object in a function and then released it. After generating a reference, it was not released, but the scope of the drink was also exceeded. This seems to be caused by the language itself

Are you Chinese? How about adding contact information to discuss together

yes,you can see me on https://github.com/zhuxiujia

@zqlpaopao
Copy link
Author

其实我想确定是底层生锈造成的还是actix-web. 目前,我尝试过tokio+HashMap,没有出现内存问题

Hello, do you have the corresponding code? As you said, I tested it with Tokio. This is my code, but the last memory was not released. I ran it on Mac m1, and I feel that it is related to the memory allocator
code

// use link_inspection::controller::init::config::Config;
// use link_inspection::controller::init::web::web;
// use link_inspection::model::mysql::proxy::MyPool;
// #[tokio::main]
// async fn main() {
//     Config::default();
//     MyPool::default();
//     web().await;
// }

use actix_web::{App, HttpServer,HttpRequest, http::header::ContentType};
use actix_web::body::BoxBody;

use serde::Serialize;

#[tokio::main]
async fn main() {
    web();
    std::thread::sleep(std::time::Duration::from_secs(1000));
}

 fn web()  {
    // HttpServer::new(|| {
    //     println!(
    //         "WEB LISTENING TO  {}:{}",
    //         "127.0.0.1",
    //         18080
    //     );
    //     // 在这里传入定义的服务
    //     App::new().service(check)
    // })
    //     .workers(5)
    //     .bind(("0.0.0.0", 18080))
    //     .unwrap()
    //     .run()
    //     .await
    //     .unwrap();
    for i in 0..10{
        std::thread::sleep(std::time::Duration::from_secs(30));

        check();
    }
}




use actix_web::{get, HttpResponse, Responder};
use std::collections::HashMap;
// #[get("/check")]
 fn check() -> impl Responder {
    let mut  h: HashMap<i64,i64> = HashMap::with_capacity(100000);
    test(&mut h);

    HttpResponse::InternalServerError().body(format!("{:?}", h))
}

 fn test (h : &mut HashMap<i64,i64>){

// tokio::time::sleep(tokio::time::Duration::from_secs(25)).await;
    println!("start");
    for v in 0..1000000{
        h.insert(v,v);
    }
}

image
end

start
start
start
start
start
start
start
start
start
start

image

this is my test code(System: MacOS M1 CPU 32GMEM)

  • test_vec There is no memory issue with the Vec data structure("all done" is 2.3MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        test_vec(Vec::new());
        //test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}
  • test_hashmap There is have memory overflow (when 'all done,now wait' here is 32MB mem)
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        std::thread::sleep(std::time::Duration::from_secs(1));
        //test_vec(Vec::new());
        test_hashmap(HashMap::new());
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
}

fn test_vec(mut h:  Vec<i64>) {
    println!("start");
    for v in 0..1000000 {
        h.push( v);
    }
}

This seems to be a leak(HashMap) on macos, But most of the memory has indeed been released, and I guess the rest should be memory fragments。 I guess maybe the hashmap structure is more complex than vec and should generate more memory fragments

HashMap is constantly inserting data, which may cause memory reallocation. When HashMap expands or redistributes internal nodes, it may allocate more memory to accommodate more elements, which may lead to an increase in memory usage. In addition, the internal structure of HashMap may lead to fragmentation, especially after a large number of insertion and deletion operations. but.we can change code to

  • after done,this is (1.9MB mem, no leak )
use std::collections::HashMap;
use serde::Serialize;

#[tokio::main]
async fn main() {
    for i in 0..10 {
        test_hashmap(HashMap::with_capacity(10000000));
    }
    println!("all done,now wait");
    std::thread::sleep(std::time::Duration::from_secs(3600));
}

fn test_hashmap(mut h:  HashMap<i64, i64>) {
    println!("start");
    for v in 0..1000000 {
        h.insert(v, v);
    }
    println!("ok");
}

Yes, you don't have this one, but it seems like you and I created a new object in a function and then released it. After generating a reference, it was not released, but the scope of the drink was also exceeded. This seems to be caused by the language itself
Are you Chinese? How about adding contact information to discuss together

yes,you can see me on https://github.com/zhuxiujia

Okay, I have applied to add Duoduo Communication

@douggynix
Copy link

douggynix commented Dec 27, 2023

Any update on this topic. On my side, things have been improved by running my app outside of the IDE shell.
And that prevents me from overriding and use a custom memory allocator as the one in my ArchLinux distribution(SLAB allocator) is working just fine. And the memory allocator used by Alpine docker image which is Musl Libc, works for me.

@mirandadam
Copy link

I have tried to reproduce this issue.

This is my understanding of it: some people have observed increased memory usage when running their web applications with actix-web, which does not seem to be released even after the requests are completed. This is suspected to be a memory leak in the actix-web library or its dependencies.

My attempt to reproduce

Create a project with the following files

/src/main.rs

#![forbid(unsafe_code)]
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
use std::collections::HashMap;

#[get("/leak")]
async fn leak() -> impl Responder {
    let mut h: HashMap<i64, i64> = HashMap::with_capacity(100000);
    test(&mut h).await;

    HttpResponse::Ok().body(format!("{:?}", h))
}

async fn test(h: &mut HashMap<i64, i64>) {
    //println!("Populating HashMap to 10 times its capacity...");
    for i in 0..1000000 {
        h.insert(i, i);
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(leak))
        .bind(("127.0.0.1", 9000))?
        .run()
        .await
}

Cargo.toml

[package]
name = "leak"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.5.1"
tokio = "1.36.0"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
debug = false

benchmark.yml

---
concurrency: 100
rampup: 10
iterations: 3000
base: 'http://127.0.0.1:9000'
plan:
  - name: Stress test
    request:
      url: /leak

Run the web server

cargo run --release

Install and run drill

cargo install drill
drill --benchmark benchmark.yml --stats

Install and run htop

snap install htop
htop

Environment

  • cargo 1.76.0 (c84b36747 2024-01-18)
  • rustc 1.76.0 (07dca489a 2024-02-04)
  • cat /etc/lsb-release gives DISTRIB_DESCRIPTION="Ubuntu 22.04.4 LTS"

Results

Memory usage increases during the stress test and is partially released after the test is completed. Memory usage starts at essentially 0% of my RAM, caps at 11% and reverts back to 2.4% after the first runs. Without restarting the server, I tried running the benchmarks again and after the peak, memory reverted to 3.3% of my RAM.

I am ill-equipped to get to the conclusion that this is an actix-web issue, or even to assert that this is actually a memory leak and not some caching mechanism. I thought it could even be an issue with HashMap, since the sample code provided elsewhere in this issue overpopulates a HashMap instance by 10 times its capacity, but I have played around a bit with the capacity and the filling loop and I am convinced that's not where the problem lies. I would appreciate any help in understanding this behavior, and if it is indeed a memory leak, to provide enough information for the devs to fix it.

@mirandadam
Copy link

@robjtede, this issue has more than 50 comments. If you are able, please investigate. Thanks for the great job on Actix!

@thalesfragoso
Copy link
Member

@robjtede, this issue has more than 50 comments. If you are able, please investigate. Thanks for the great job on Actix!

Have you tried changing your allocator (and allocator configuration) ?

The culprit might be fragmentation in the allocator. See https://sourceware.org/bugzilla/show_bug.cgi?id=14581

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants