HTTP 服务器执行异步数据库查询的最小示例?

Minimal example of HTTP server doing asynchronous database queries?

我正在尝试使用不同的异步 HTTP 服务器,看看它们如何处理多个同时连接。为了强制执行耗时的 I/O 操作,我使用 pg_sleep PostgreSQL 函数来模拟耗时的数据库查询。例如,这是我对 Node.js:

所做的
var http = require('http');
var pg = require('pg');
var conString = "postgres://al:al@localhost/al";
/* SQL query that takes a long time to complete */
var slowQuery = 'SELECT 42 as number, pg_sleep(0.300);';

var server = http.createServer(function(req, res) {
  pg.connect(conString, function(err, client, done) {
    client.query(slowQuery, [], function(err, result) {
      done();
      res.writeHead(200, {'content-type': 'text/plain'});
      res.end("Result: " + result.rows[0].number);
    });
  });
})

console.log("Serve http://127.0.0.1:3001/")
server.listen(3001)

所以这是一个非常简单的请求处理程序,它执行一个 SQL 查询,耗时 300 毫秒,returns 一个响应。当我尝试对其进行基准测试时,我得到以下结果:

$ ab -n 20 -c 10 http://127.0.0.1:3001/
Time taken for tests:   0.678 seconds
Complete requests:      20
Requests per second:    29.49 [#/sec] (mean)
Time per request:       339.116 [ms] (mean)

这清楚地表明请求是并行执行的。每个请求需要 300 毫秒才能完成,因为我们并行执行了 2 批 10 个请求,所以总共需要 600 毫秒。

现在我正尝试对 Elixir 做同样的事情,因为我听说它是​​透明的异步 I/O。这是我天真的方法:

defmodule Toto do
  import Plug.Conn

  def init(options) do
    {:ok, pid} = Postgrex.Connection.start_link(
      username: "al", password: "al", database: "al")
    options ++ [pid: pid]
  end

  def call(conn, opts) do
    sql = "SELECT 42, pg_sleep(0.300);"
    result = Postgrex.Connection.query!(opts[:pid], sql, [])
    [{value, _}] = result.rows
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Result: #{value}")
  end
end

如果可能相关,这是我的主管:

defmodule Toto.Supervisor do
  use Application

  def start(type, args) do
    import Supervisor.Spec, warn: false

    children = [
      worker(Plug.Adapters.Cowboy, [Toto, []], function: :http),
    ]
    opts = [strategy: :one_for_one, name: Toto.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

如您所料,这并没有给我预期的结果:

$ ab -n 20 -c 10 http://127.0.0.1:4000/
Time taken for tests:   6.056 seconds
Requests per second:    3.30 [#/sec] (mean)
Time per request:       3028.038 [ms] (mean)

看起来没有并行性,请求一个接一个地处理。我做错了什么?

Elixir 应该完全适合这个设置。不同之处在于您的 node.js 代码正在为每个请求创建到数据库的连接。然而,在你的 Elixir 代码中,init 被调用一次(而不是每个请求!)所以你最终得到一个进程,它向 Postgres 发送所有请求的查询,然后成为你的瓶颈。

最简单的解决方案是将与 Postgres 的连接从 init 移到 call。但是,我建议您 use Ecto which will set up a connection pool to the database too. You can also play with the pool configuration 以获得最佳结果。

UPDATE 这只是测试代码,如果您想执行类似操作,请参阅@AlexMarandon 的 Ecto 池答案。

我一直在尝试按照 José 的建议移动连接设置:

defmodule Toto do
  import Plug.Conn

  def init(options) do
    options
  end

  def call(conn, opts) do
    { :ok, pid } = Postgrex.Connection.start_link(username: "chris", password: "", database: "ecto_test")
    sql = "SELECT 42, pg_sleep(0.300);"
    result = Postgrex.Connection.query!(pid, sql, [])
    [{value, _}] = result.rows
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Result: #{value}")
  end
end

结果:

% ab -n 20 -c 10 http://127.0.0.1:4000/
Time taken for tests:   0.832 seconds
Requests per second:    24.05 [#/sec] (mean)
Time per request:       415.818 [ms] (mean)

这是我在 :

之后想出的代码
defmodule Toto do
  import Plug.Conn

  def init(options) do
    options
  end

  def call(conn, _opts) do
    sql = "SELECT 42, pg_sleep(0.300);"
    result = Ecto.Adapters.SQL.query(Repo, sql, [])
    [{value, _}] = result.rows
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Result: #{value}")
  end
end

为此,我们需要声明一个 repo 模块:

defmodule Repo do
  use Ecto.Repo, otp_app: :toto
end

并在主管中启动该回购:

defmodule Toto.Supervisor do
  use Application

  def start(type, args) do
    import Supervisor.Spec, warn: false

    children = [
      worker(Plug.Adapters.Cowboy, [Toto, []], function: :http),
      worker(Repo, [])
    ]
    opts = [strategy: :one_for_one, name: Toto.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

正如何塞提到的,我通过稍微调整配置获得了最佳性能:

config :toto, Repo,
  adapter: Ecto.Adapters.Postgres,
  database: "al",
  username: "al",
  password: "al",
  size: 10,
  lazy: false

这是我的基准测试结果(运行几次后池有时间 "warm up"),默认配置:

$ ab -n 20 -c 10 http://127.0.0.1:4000/
Time taken for tests:   0.874 seconds
Requests per second:    22.89 [#/sec] (mean)
Time per request:       436.890 [ms] (mean)

这是 size: 10lazy: false 的结果:

$ ab -n 20 -c 10 http://127.0.0.1:4000/
Time taken for tests:   0.619 seconds
Requests per second:    32.30 [#/sec] (mean)
Time per request:       309.564 [ms] (mean)