Boosting Go App Performance with Connection Pooling

AnshulAnshul
5 min read

Recently, I worked on a project where I had to implement dynamic connection pooling. I know it sounds strange, and I thought it would be as complex as it sounds. But trust me, it's actually quite easy. Let me share the whole story with you.

Let's begin. I hope you know a little about networking and backend stuff. I was trying to send data to a custom processing server over a TCP connection. I needed a connection pool that would let me process data simultaneously over TCP connections and send responses back through these connections.

So, I chose the most popular language known for its concurrency performance, and yes, you guessed it right, it's Golang. It made it very easy to create a connection pool from scratch.

Alright, let's get to the point. I used goroutines to manage the concurrent processing. To efficiently share some TCP connections between goroutines, I used channels in GoLang.


Here are the steps:

Step 1: Represent the pool

type Pool struct {
    addr      string        // the server address we want to connect to
    min       int           // minimum number of connections to start the pool with
    max       int           // maximum number of connections the pool can handle
    active    int           // current number of active (open/available) connections
    mutx      sync.Mutex    // used to safely update 'active' when adding/removing connections
    idle_conn chan net.Conn // channel that holds idle (ready-to-use) connections
    timeout   time.Duration // how long to wait when trying to create a new TCP connection
}

Step 2 : How To Make a Pool


func NewPool(addr string, min int, max int, timeout time.Duration) *Pool {
    pool := &Pool{ // creating a new pool
        addr:      addr,
        min:       min,
        max:       max,
        idle_conn: make(chan net.Conn, max), // creating a new channel with length as max 
        timeout:   timeout,
        active:    0,
    }
    // Channels are like a way for goroutines (tiny, lightweight threads) to chat and collaborate.

    for i := 0; i < min; i++ { // seeding the pool with some intial set of connections
        conn, err := net.DialTimeout("tcp", addr, timeout) // creating a new connection

        if err != nil {
            cfg.ERRORLogger.Log.Panic(err) // it's just a custom logger i have creating for 
            // better logging
        }

        pool.idle_conn <- conn // pushing new connection to the channel
        pool.active++ // updating no. of available connections free to use.
    }

    return pool
}
// That's it our new pool is created and ready to use with some set of intial connections.
// The intial number of connections is defined by "min" passed to the function.
  • We set up a new pool with a few initial connections.

  • Now, this pool keeps these connections ready to go. Whenever a function or Go routine needs to send data to the server, it borrows a connection from the pool. Once it's done, it returns the connection back to the pool. I'll show you how this works in the steps below.

  • By keeping a bunch of connections with the server always ready and active, we boost the system's performance. This way, we don't have to create a new connection for every Go routine and drop it after each use.


Now up to this point, we've tackled most of the tricky stuff. All that's left is to figure out a way to grab a connection from the pool and then put it back when we're done.

Step 3 : How to Borrow a Connection from the Pool

func (pool *Pool) Get() (net.Conn, error) {
    select {
    case conn := <-pool.idle_conn: // borrow a new connetion from that channel/pool we have created and store it in conn
        return conn, nil // return that connection

    // Remember, we never pushed the pool to its limits. 
    // We just set it up with a small number of connections, say 10. 
    // Now, if you try to do more work than that, the pool won't have a connection
    // ready for you.

    // So, to deal with that, we say if our pool hasn't reached its max limit yet,
    // we can just create a new connection on the fly, use it, and then put it back 
    // in the pool.
    default:
        if pool.active < pool.max {
            conn, err := net.DialTimeout("tcp", pool.addr, pool.timeout)

            if err != nil {
                return nil, err
            }

            pool.active++
            return conn, nil
        } else {
    // But if our pool has hit its limit, we'll just hang tight 
    // and wait for an idle connection.
            return <-pool.idle_conn, nil
        }
    }
}

With lots of connections, we also have the responsibility to return them so others can use them efficiently.

Let's wrap this up with one final touch.

Step 4. How to Return a Connection Back to the Pool

func (pool *Pool) Put(conn net.Conn) {
    select {
    case pool.idle_conn <- conn: // trying to put this connection back into the pool for later use
    default: // but what if our pool is already full
        conn.Close() // Close the connection if there's no space to keep it.
        // Then, lock the pool, decrease the active count by 1, and unlock it.
        pool.mutx.Lock() 
        pool.active--
        pool.mutx.Unlock()
    }
}

Congratulations! You've just learned how easy it is to create your own connection pooling setup.

Let's check out how to use this in action!


var GlobalPool = pools.NewPool("127.0.0.1:6379", 10, 100, 10*time.Second)

func UseConnectionFromGlobalPool() error {
    // Get a connection from the global pool
    conn, err := GlobalPool.Get()
    if err != nil {
        return err // return if getting connection failed
    }
    // Always put back the connection when done
    defer GlobalPool.Put(conn)

    // Do your work with the connection here
    // For example: send/receive data using conn
    return nil // success
}

Thank you for reading! If you enjoyed this post and want to stay updated with more insights and tips, make sure to follow me for more content like this. Happy coding!

1
Subscribe to my newsletter

Read articles from Anshul directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anshul
Anshul

I am a passionate Full Stack Web Developer From India.