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: 10
和 lazy: 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)
我正在尝试使用不同的异步 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: 10
和 lazy: 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)