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

时间:2015-04-05 16:37:49

标签: asynchronous elixir

我正在使用不同的异步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)

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

$ 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)

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

3 个答案:

答案 0 :(得分:8)

使用此设置,Elixir应该完全没问题。不同之处在于您的node.js代码正在为每个请求创建与数据库的连接。但是,在您的Elixir代码中,init被调用一次(而不是每个请求!),因此您最终会使用一个进程向Postgres发送查询以查找所有请求,然后成为您的瓶颈。

最简单的解决方案是将连接移出init并进入call。但是,我建议您use Ecto,它也将建立一个连接池到数据库。您还可以使用pool configuration来获得最佳效果。

答案 1 :(得分:5)

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)

答案 2 :(得分:3)

以下是我在José's answer后面提出的代码:

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

正如José所说,我通过稍微调整配置获得了最佳性能:

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

以下是我的基准测试结果(经过几次运行以便池有时间“预热”)并使用默认配置:

$ 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)