visit
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
You can log how long a function execution took in Go using this function in combination with
defer
like so:func benchmarkQuery(count int) {
defer timeTrack(time.Now(), "benchmarkQuery")
for i := 0; i < count; i++ {
funcUnderTest()
}
}
Above, the arguments to
timeTrack()
are evaluated immediately, meaning the first argument is set to time.Now()
- the time benchmarkQuery()
was invoked.But
defer
waits to invoke timeTrack()
until right before the function benchmarkQuery()
returns. Which, in this case, will be after the for loop executing funcUnderTest()
count
times has completed. Neat trick.With a timing function in place, I wrote out some benchmarks. We want to benchmark two different kinds of behavior:func benchmarkOpen(count int) {
defer timeTrack(time.Now(), "benchmarkOpen")
for i := 0; i < count; i++ {
db := openDb()
defer db.Close()
rows, err := db.Query("SELECT id from purchase_orders limit(100);")
rows.Close()
if err != nil {
log.Printf("Got error: %v", err)
}
}
}
func benchmarkQuery(count int) {
defer timeTrack(time.Now(), "benchmarkQuery")
db := openDb()
defer db.Close()
for i := 0; i < count; i++ {
rows, err := db.Query("SELECT id from purchase_orders limit(100);")
rows.Close()
if err != nil {
log.Printf("Got error: %v", err)
}
}
}
(Note: It's important to run
rows.Close()
, otherwise the connection is not immediately released - which means Go's database/sql
will default to just opening a new connection for the next request, defeating this test.)Using these two functions, we're prepared to find out:Many applications, including those built on modern serverless architectures, can have a large number of open connections to the database server, and may open and close database connections at a high rate, exhausting database memory and compute resources. Amazon RDS Proxy allows applications to pool and share connections established with the database, improving database efficiency and application scalability.
1. RDS Proxy only supports specific database engine versions – and the latest RDS Postgres is not one of them.
The first time I went to setup a new RDS proxy, I didn't see any available databases in the dropdown:After much trial and error, I ended up spinning up a new Aurora db. At last, I saw something populate the dropdown.
It wasn't until much later I learned that Postgres 12 - the engine I use everywhere - just isn't supported yet.(What would have been nice: Showing me all my RDS databases in that drop-down, but just greying out the ones that are not supported by Proxy.)
2. You can only connect to your RDS Proxy from inside the same VPC
This one was a time sink. There are few things I dread more than trying to connect to something on AWS and getting a timeout. There are about a half-dozen layers (security groups, IGWs, subnets) that all have to be lined up just so to get a connection online. The bummer is that there's no easy way to debug where a given connection has failed.
So, the first wrench was when I discovered - after much trial and error - that I couldn't connect to my RDS Proxy from my laptop.(How about a small bone, right next to "Proxy endpoint," that lets me know the limitations associated with this endpoint?)RDS vs RDS Proxy
With my RDS Proxy 101 hard-won, we're ready to run some benchmarks.I copied the benchmark binary up to an EC2 server co-located with the RDS database and RDS proxy.Per above:benchmarkOpen
opens a connection and then runs a query on each loop (sequential)benchmarkQuery
opens a connection first, then each loop is a query on that connection (also sequential)# RDS direct, same datacenter
$ ./main
2020/12/25 00:08:14 Starting benchmarks...
2020/12/25 00:08:19 benchmarkOpen took 5.846447324s
2020/12/25 00:08:30 benchmarkQuery took 504.32703ms
benchmarkOpen
- ~5.85ms per loopbenchmarkQuery
- ~0.500ms per loopI recognize that in
benchmarkOpen
, instead of deferring db.Close()
I could be nicer to my database and close it immediately. But I kind of like the DB slow boil.Let's move on to the RDS Proxy. Remember, RDS Proxy has to be in the same VPC as the entity calling it as well as the database. So all three are co-located:# RDS Proxy, same datacenter
$ ./main
2020/12/25 00:15:21 Starting benchmarks...
2020/12/25 00:15:28 benchmarkOpen took 7.094809555s
2020/12/25 00:15:39 benchmarkQuery took 1.239892867s
We'll see soon how all this performance translates to Lambda, but the motivation for RDS Proxy is clear: It's less about the client, more about the database. RDS Proxy is just a layer of indirection for our database connections. It doesn't speed up establishing new connections - in fact, we pay a small tax on connection opening. It just saves our database from the thundering herd.
Cross-region
Because we're here, I'm curious: What happens if we run this benchmark cross-region, from US-East-1 to US-West-2?# RDS - different datacenter
$ ./main
2020/12/25 00:11:12 Starting benchmarks...
2020/12/25 00:15:53 benchmarkOpen took 4m41.607514892s
2020/12/25 00:17:15 benchmarkQuery took 1m11.767803425s
benchmarkOpen
- ~281.6ms per loopbenchmarkQuery
- ~71.77ms per loopAPI Gateway -> Lambda -> RDS
API Gateway -> Lambda -> RDS Proxy -> RDS
Each lambda function call will open a new database connection and issue a single query. I just adapted the function above to make one open/query as opposed to doing so inside a
for
loop.I'll run a benchmark locally on my computer that will call the API Gateway endpoint. Because I'm running locally, I'm able to save a little work by using Go's native benchmarking facilities:func BenchmarkRequest(b *testing.B) {
for i := 0; i < b.N; i++ {
resp, err := http.Post("//[REDACTED]/run", "application/json", nil)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Println("Received non-200 response. Continuing")
}
}
}
$ go test -bench=. -benchtime 1000x
goos: darwin
goarch: amd64
pkg: github.com/acco/rds-proxy-bench
BenchmarkRequest-8 1000 128684974 ns/op
PASS
ok github.com/acco/rds-proxy-bench 130.351s
$ go test -bench=. -benchtime 1000x
goos: darwin
goarch: amd64
pkg: github.com/acco/rds-proxy-bench
BenchmarkRequest-8 1000 118576056 ns/op
PASS
ok github.com/acco/rds-proxy-bench 120.529s
The end-to-end Lambda benchmark takes it all home: The purpose of RDS Proxy is foremost to tame connections (and related load) on the database. Any happy gains on the client-side will be a result of a relieved database.
Also published at