Reversion1817

Reversion1817

A Schrödinger's database adapter: Ecto&PostgreSQL perform faster when observed with Observer

Objectives

I am trying to parallelize a little bit of database workload with Elixir and Ecto. There is no data inconsistency expected, as each SQL query is expected to read the database and then write its own unique result. In other words, the queries do not depend on each other.

It looks like trying to run multiple independent connections with a properly configured connection pool might help. In fact, it might be more efficient than using PosgreSQL’s parallel query. Therefore, I tried to implement the following approach: it works better if you queue work

My objective is to use multiple connections to PostgreSQL so that it can run several queries simultaneously.

Background

I have implemented a very simple script that for N amount of independent queries runs an Enum.each that makes a query against the database.

Then I upgraded it so that for N amount of independent queries it splits them in batches of size M and runs Task.async for each batch.

def parallel_split(times) do
  if times > @split do
    Logger.info "Splitting"
    1..@split
    |> Enum.map(fn _x -> Task.async(fn -> run_postgresql_query() end) end)
    |> Enum.map(&Task.await/1)
    parallel_split(times - @split)
  else
    1..times
    |> Enum.map(fn _x -> Task.async(fn -> run_postgresql_query() end) end)
    |> Enum.map(&Task.await/1)
  end
end

The Task.async version seemed to run faster, because apparently many threads are used to issue and queue queries (Elixir/Ecto) and many threads are used by PostgreSQL to run the queries. Here is how it looks when running:

htop screenshot

Let’s say it takes around 84 seconds to process 50_000 queries.

Problem

However, when I running my script with mix run --no-halt I noticed that htop showed 8 busy threads for some time, but then only 1 thread stayed busy (around 90% of one CPU core load) for some time.

htop screenshot

In fact, while running, the script uses all 8 cores for about 25 seconds and then the rest of the time it uses only 1.

Let us call this “Issue #02”.

I was curious to investigate, because it appeared to me that for some reason after correctly firing async tasks against the Ecto’s queueing mechanism and getting the results back my script got stuck apparently waiting for some response from the database.

I enabled telemetry for Ecto, but looking at query/queue time for each individual query did not help much.

Observer

Then I figured that I could see the queue table with Observer.

I use :observer.start in the same iex session before starting my main script.

Before the script starts, the Elixir.DBConnection.ConnectionPool.Queue table has 16 records, which matches the pool_size in the Repo config of my app.

After the script starts, the Elixir.DBConnection.ConnectionPool.Queue gets around M records (the batch size) which eventually go away (I guess this is the actual queuing process in action).

htop screenshot

All is as expected, but the running time for the same 50_000 queries gets down to just 24 seconds!

I double-checked and ran the script again with mix and without Observer - same slow 84 seconds again!

And, I double-checked running it with iex and Observer - that “only one CPU thread working” issue (Issue #02) does not happen when using iex and Observer.

Question

Why a function that runs many database queries with a separate Task.async call for each query, runs faster when launched from iex and observed with observer?

Marked As Solved

dimitarvp

dimitarvp

I see. Well, as a start using Enum.map + Task.async + Task.await is a tad inefficient – though I wouldn’t think that amounts to more than a rounding error for the performance difference between executing the code in mix run --no-halt and iex -S mix but… let’s address one problem at a time.

I also don’t see a reason to use recursion in your scenario, by the way. That’s probably the main culprit because you end up processing batches serially.

Still, try this:

@maximum_parallel_db_connections 15

@doc "Process one batch serially within a single DB transaction"
def process_batch(items) do
  Repo.transaction(fn ->
    Enum.each(items, fn -> item
      do_stuff_with(item) # 👈 your processing code here
    end)
  end)
end

@doc "Process all items in parallel"
def parallel_process_items(items) do
  items
  |> Stream.chunk_every(100) # 👈 your desired batch size here
  |> Task_async_stream(&parallel_process_batch/1, max_concurrency: @maximum_parallel_db_connections)
end

So, IMO first change your code to something close to the above, post it, and also post if there are still differences between the both ways you are executing your code. That will put everyone in a better position to try and help you further.

Also Liked

BartOtten

BartOtten

The GUI monitor = Observer. As observer is always busy observing, it might keep a process in “performance mode”.

The other was a joke about your database being faster when observed (Schrödinger). I applied this effect to this topic :slight_smile: Rest assured: observing this topic 24/7 won’t make any difference in solving speed. Feel free to take a nap.

BartOtten

BartOtten

Can you tell us:

  1. The CPU architecture
  2. Erlang/OTP version

There is another thread where another performance issue was hunted down and those parameters mattered. From what I recalled it had to do with an Apple M2 putting processes on slower cores. Having the GUI monitor open could prevent such thing to happen (???)

Observing this topic will resolve the issue faster.

hst337

hst337

This might be related to

  1. PostgreSQL planner
  2. Virtual machine (WSL)
  3. Kernel scheduler
  4. Kernel network stack dispatch
  5. General memory layout

Anyway, I’d suggest using Task.await_many and publishing your code, since it will definitely speak more precisely than your description

Reversion1817

Reversion1817

BartOtten, thanks for your response.

CPU: 11th Gen Intel Core i7-1165G7 2.80 GHz, 4 Cores, 8 Logical Processors
Erlang: Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Elixir: IEx 1.13.4 (compiled with Erlang/OTP 25)
OS: Windows 11 Pro 22H2 OS Build 22621.1702, WSL2 Ubuntu 20.04

Having the GUI monitor open could prevent such thing to happen

Which GUI monitor do you mean?

Observing this topic will resolve the issue faster.

I am not sure I understand this, could you please elaborate?

hst337

hst337

You can share the code, and I will test it in my local setup. Otherwise, this can be related to anything

Where Next?

Popular in Questions Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New

Other popular topics Top

gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement