source upload
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# Simple mORMot server for REST benchmark
|
||||
|
||||
## Socket based server
|
||||
|
||||
- compile and run RESTBenchmark
|
||||
- test it with browser:
|
||||
- http://localhost:8888/root/abc
|
||||
- http://localhost:8888/root/xyz
|
||||
- test it with Apache Bench
|
||||
```
|
||||
ab -n 10000 -c 1000 http://localhost:8888/root/abc
|
||||
```
|
||||
|
||||
## Keep alive
|
||||
By default mROMot HTTP server runs in KeepAlive mode.
|
||||
|
||||
To disable KeepAlive run `RESTBenchmark` with secont parameter `false`
|
||||
```
|
||||
./RESTBenchmark 8888 false
|
||||
```
|
||||
|
||||
Disabling KeepAlive make sence in case mORMotserver is behind the reverse proxy.
|
||||
In this case reverse proxy cares about KeepAlive connection pool and mormot can
|
||||
operate with fixed thread pool size.
|
||||
|
||||
## Unix Domain Socket (Linux)
|
||||
|
||||
### When to use
|
||||
In case mORMot is behind a local reverse proxy on the environment with a
|
||||
huge number of incoming connection it's make sence to use a UDS to minimize
|
||||
unnecessary TCP handshakes between mORMot and reverse proxy.
|
||||
|
||||
To emulate such environment on the syntetic test we can disable keep alive
|
||||
in RESTBEnchmark by passing `false` to then second parameter
|
||||
```
|
||||
./RESTBenchmark unix false
|
||||
./RESTBenchmark 8888 false
|
||||
```
|
||||
|
||||
### How to run
|
||||
|
||||
- compile a program and run with `unix` parameter
|
||||
```
|
||||
./RESTBenchmark unix
|
||||
```
|
||||
|
||||
Benchmark will listen on Unix Domain Socket `/tmp/rest-bench.socket`
|
||||
|
||||
- test it with curl
|
||||
```
|
||||
curl --unix-socket /tmp/rest-bench.socket http://localhost/root/abc
|
||||
```
|
||||
|
||||
- setup nginx as a reverse proxy
|
||||
```
|
||||
sudo ln -s "$(pwd)/mormot-rest-nginx.conf" /etc/nginx/sites-available
|
||||
sudo ln -s /etc/nginx/sites-available/mormot-rest-nginx.conf /etc/nginx/sites-enabled
|
||||
sudo nginx -s reload
|
||||
```
|
||||
|
||||
- test it using ab (or better - wrk)
|
||||
```
|
||||
wrk http://localhost:8888/root/abc
|
||||
```
|
||||
|
||||
|
||||
## systemd integration
|
||||
|
||||
- socket activation
|
||||
- journald logging (can be exported for LogView program)
|
||||
- auto shutdown on inactivity
|
||||
|
||||
With combination of [Systemd Template Unit](https://fedoramagazine.org/systemd-template-unit-files/), `* A` DNS zone and
|
||||
nginx virtual hosts we can build a "cloud solution" using mORMot services with per-customer process level isolation.
|
||||
|
||||
### Install Unit
|
||||
Add our service to systemd (should be executed once).
|
||||
This command activate `rest_benchmark` socket on port 8889
|
||||
```bash
|
||||
sudo ./installSystemSocket.sh
|
||||
```
|
||||
|
||||
### Socket activation, auto shutdown and logs
|
||||
Kill possible instances of RESTBenchmark program
|
||||
```bash
|
||||
killall -TERM RESTBenchmark
|
||||
```
|
||||
|
||||
Now program is **NOT running**, lets send an HTTP request to a port configured in `rest_benchmark.socket` unit (8889)
|
||||
```bash
|
||||
curl http://localhost:8889/root/xyz
|
||||
```
|
||||
Magic - we got a response :) This is how systemd socket activation works.
|
||||
|
||||
Inside our demo program, in case it executed by systemd it:
|
||||
- log all activity into journald (see EchoToConsoleUseJournal)
|
||||
- stops after 10 seconds without GET requests (see inactivityWatchdog in RESTBenchmark.dpr)
|
||||
|
||||
Open new terminal window and run a command to watch logs from `rest_benchmark` services
|
||||
```bash
|
||||
journalctl -u rest_benchmark.service -f
|
||||
```
|
||||
|
||||
Now wait for 10 second - service should shut down itself.
|
||||
|
||||
### journald and LogView (Samples/11 - Exception logging)
|
||||
|
||||
Logs from journald can be exported to format LogView understand. Example:
|
||||
```bash
|
||||
journalctl -u rest_benchmark.service --no-hostname -o short-iso-precise --since today | grep "RESTBenchmark\[.*\]: . " > todaysLog.log
|
||||
```
|
||||
better to grep by program PID `"RESTBenchmark\[12345]: . "` to include only one start/stop circle (on 2020-06 LogView do not allow to filter on PIDs)
|
||||
|
@@ -0,0 +1,218 @@
|
||||
/// minimal REST server for a list of Persons stored in SQlite3
|
||||
program RESTBenchmark;
|
||||
|
||||
{
|
||||
run the Server executable then e.g.
|
||||
- ab -n 1000 -c 100 http://localhost:8888/root/abc
|
||||
for latency measure (return the current timestamp as a few bytes)
|
||||
- ab -n 1000 -c 100 http://localhost:8888/root/xyz
|
||||
for bandwidth measure (returns some ORM query as 77KB of JSON)
|
||||
}
|
||||
{$ifndef UNIX}
|
||||
{$APPTYPE CONSOLE}
|
||||
{$endif}
|
||||
|
||||
{$I Synopse.inc} // LINUXNOTBSD
|
||||
|
||||
uses
|
||||
{$I SynDprUses.inc} // use FastMM4 on older Delphi, or set FPC threads
|
||||
SysUtils,
|
||||
SynCommons, // framework core
|
||||
SynTable,
|
||||
SynCrtSock, // direct access to HTTP server
|
||||
SynLog, // logging features
|
||||
mORMot, // RESTful server & ORM
|
||||
mORMotSQLite3, // SQLite3 engine as ORM core
|
||||
SynSQLite3Static, // staticaly linked SQLite3 engine
|
||||
{$ifdef UNIX}
|
||||
BaseUnix,
|
||||
{$endif}
|
||||
{$ifdef LINUXNOTBSD}
|
||||
SynSystemd,
|
||||
mORMotService,
|
||||
{$endif}
|
||||
mORMotHttpServer; // HTTP server for RESTful server
|
||||
|
||||
type
|
||||
TSQLPerson = class(TSQLRecordNoCase)
|
||||
private
|
||||
fFirstName: RawUTF8;
|
||||
fName: RawUTF8;
|
||||
fBirth: TDateTime;
|
||||
published
|
||||
property Name: RawUTF8 read fName write fName;
|
||||
property FirstName: RawUTF8 read fFirstName write fFirstName;
|
||||
property Birth: TDateTime read fBirth write fBirth;
|
||||
end;
|
||||
|
||||
TMyServices = class(TSynPersistent)
|
||||
protected
|
||||
fDb: TSQLRestServer;
|
||||
procedure PopulateWithRandom;
|
||||
public
|
||||
constructor Create(aDB: TSQLRestServer); reintroduce;
|
||||
published
|
||||
procedure abc(ctxt: TSQLRestServerURIContext);
|
||||
procedure xyz(ctxt: TSQLRestServerURIContext);
|
||||
end;
|
||||
|
||||
{ TMyServices }
|
||||
|
||||
constructor TMyServices.Create(aDB: TSQLRestServer);
|
||||
begin
|
||||
inherited Create;
|
||||
fDB := aDB;
|
||||
fDB.ServiceMethodRegisterPublishedMethods('', self); // root/abc and root/xyz
|
||||
if not fDb.TableHasRows(TSQLPerson) then
|
||||
PopulateWithRandom;
|
||||
end;
|
||||
|
||||
procedure TMyServices.PopulateWithRandom;
|
||||
var
|
||||
aBatch: TSQLRestBatch;
|
||||
aPerson: TSQLPerson;
|
||||
aTimer: TPrecisionTimer;
|
||||
i: integer;
|
||||
begin
|
||||
aTimer.Start;
|
||||
aPerson := TSQLPerson.Create;
|
||||
try
|
||||
aBatch := TSQLRestBatch.Create(fDB, TSQLPerson, 10000);
|
||||
try
|
||||
for i := 1 to 1000 do begin
|
||||
aPerson.Name := FormatUTF8('% name', [CardinalToHexShort(i * 777777)]);
|
||||
aPerson.FirstName := FormatUTF8('first %', [i]);
|
||||
aPerson.Birth := i + 40000;
|
||||
aBatch.Add(aPerson, true);
|
||||
end;
|
||||
fDb.BatchSend(aBatch);
|
||||
finally
|
||||
aBatch.Free;
|
||||
end;
|
||||
finally
|
||||
aPerson.Free;
|
||||
end;
|
||||
writeln('Created 1000 entries in ', aTimer.Stop);
|
||||
end;
|
||||
|
||||
procedure TMyServices.xyz(ctxt: TSQLRestServerURIContext);
|
||||
var
|
||||
s: RawUTF8;
|
||||
begin
|
||||
FormatUTF8('xyz %', [NowUTCToString], s);
|
||||
ctxt.Results([s]);
|
||||
end;
|
||||
|
||||
procedure TMyServices.abc(ctxt: TSQLRestServerURIContext);
|
||||
var
|
||||
s: RawUTF8;
|
||||
begin
|
||||
s := fDB.RetrieveListJSON(TSQLPerson, '', '', true);
|
||||
ctxt.Returns(s);
|
||||
end;
|
||||
|
||||
var
|
||||
url: AnsiString;
|
||||
keepAlive: boolean;
|
||||
aRestServer: TSQLRestServer;
|
||||
lastReadCount: TSynMonitorCount64;
|
||||
|
||||
procedure DoTest(const url: AnsiString; keepAlive: boolean);
|
||||
var
|
||||
aHttpServer: TSQLHttpServer;
|
||||
aServices: TMyServices;
|
||||
begin
|
||||
// create the main mORMot server
|
||||
aRestServer := TSQLRestServerDB.CreateWithOwnModel([TSQLPerson],'test.db',False,'root'); // authentication=false
|
||||
try
|
||||
// create tables or fields if missing
|
||||
aRestServer.CreateMissingTables;
|
||||
// initialize the services implementation class
|
||||
aServices := TMyServices.Create(aRestServer);
|
||||
try
|
||||
// serve aRestServer data over HTTP
|
||||
aHttpServer := TSQLHttpServer.Create(url,[aRestServer]);
|
||||
if not keepAlive and (aHttpServer.HttpServer is THttpServer) then
|
||||
THttpServer(aHttpServer.HttpServer).ServerKeepAliveTimeOut := 0;
|
||||
try
|
||||
aHttpServer.AccessControlAllowOrigin := '*'; // allow cross-site AJAX queries
|
||||
write('Background server is running on ', url, ' keepAlive ');
|
||||
if (keepAlive) then
|
||||
writeLn('is enabled') else
|
||||
writeLn('is disabled');
|
||||
{$ifdef LINUX}
|
||||
SynDaemonIntercept(nil);
|
||||
writeln('Press [Ctrl+C] or send SIGINT/SIGTERM to close the server.');
|
||||
fpPause;
|
||||
{$else}
|
||||
write('Press [Enter] to close the server.');
|
||||
readln;
|
||||
{$endif}
|
||||
finally
|
||||
aHttpServer.Free;
|
||||
end;
|
||||
finally
|
||||
aServices.Free;
|
||||
end;
|
||||
finally
|
||||
FreeAndNil(aRestServer);
|
||||
end;
|
||||
end;
|
||||
|
||||
const
|
||||
UNIX_SOCK_PATH = '/tmp/rest-bench.socket';
|
||||
|
||||
{$ifdef LINUX}
|
||||
/// killa process after X second without GEt requests
|
||||
function inactivityWatchdog(p: pointer): ptrint;
|
||||
var currentRC: TSynMonitorCount64;
|
||||
begin
|
||||
repeat
|
||||
sleep(10000); /// once per 10 second
|
||||
if aRestServer = nil then // not initialized
|
||||
continue;
|
||||
currentRC := aRestServer.Stats.Read;
|
||||
if (currentRC - lastReadCount) <= 0 then begin
|
||||
SQLite3Log.Add.Log(sllServer, 'Terminating due to inactivity..');
|
||||
FpKill(GetProcessID, SIGTERM);
|
||||
break;
|
||||
end;
|
||||
lastReadCount := currentRC;
|
||||
until false;
|
||||
Result := 0;
|
||||
end;
|
||||
{$endif}
|
||||
begin
|
||||
// set logging abilities
|
||||
SQLite3Log.Family.Level := LOG_VERBOSE;
|
||||
//SQLite3Log.Family.EchoToConsole := LOG_VERBOSE;
|
||||
SQLite3Log.Family.PerThreadLog := ptIdentifiedInOnFile;
|
||||
SQLite3Log.Family.NoFile := true; // do not create log files for benchmark
|
||||
{$ifdef UNIX}
|
||||
{$ifdef LINUXNOTBSD}
|
||||
if SynSystemd.ProcessIsStartedBySystemd then begin
|
||||
SQLite3Log.Family.EchoToConsole := SQLite3Log.Family.Level;
|
||||
SQLite3Log.Family.EchoToConsoleUseJournal := true;
|
||||
if sd.listen_fds(0) = 1 then
|
||||
url := '' // force to use socket passed by systemd
|
||||
else
|
||||
url := '8888';
|
||||
// set a wachdog to kill our process after 10 sec of inactivity
|
||||
// just for demo - in real life verifiing only read operations is not enought
|
||||
lastReadCount := 0;
|
||||
BeginThread(@inactivityWatchdog, nil);
|
||||
end else
|
||||
{$endif}
|
||||
if (ParamCount>0) and (ParamStr(1)='unix') then begin
|
||||
url := 'unix:' + UNIX_SOCK_PATH;
|
||||
if FileExists(UNIX_SOCK_PATH) then
|
||||
DeleteFile(UNIX_SOCK_PATH); // remove socket file
|
||||
end else
|
||||
{$endif}
|
||||
url := '8888';
|
||||
if (ParamCount>1) and (ParamStr(2)='false') then
|
||||
keepAlive := false else
|
||||
keepAlive := true;
|
||||
DoTest(url, keepAlive);
|
||||
end.
|
||||
|
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CONFIG>
|
||||
<ProjectOptions>
|
||||
<Version Value="11"/>
|
||||
<General>
|
||||
<Flags>
|
||||
<MainUnitHasCreateFormStatements Value="False"/>
|
||||
<MainUnitHasTitleStatement Value="False"/>
|
||||
<MainUnitHasScaledStatement Value="False"/>
|
||||
</Flags>
|
||||
<SessionStorage Value="InProjectDir"/>
|
||||
<MainUnit Value="0"/>
|
||||
<Title Value="RESTBenchmark"/>
|
||||
<UseAppBundle Value="False"/>
|
||||
<ResourceType Value="res"/>
|
||||
</General>
|
||||
<BuildModes Count="1">
|
||||
<Item1 Name="Default" Default="True"/>
|
||||
</BuildModes>
|
||||
<PublishOptions>
|
||||
<Version Value="2"/>
|
||||
<UseFileFilters Value="True"/>
|
||||
</PublishOptions>
|
||||
<RunParams>
|
||||
<FormatVersion Value="2"/>
|
||||
<Modes Count="0"/>
|
||||
</RunParams>
|
||||
<Units Count="1">
|
||||
<Unit0>
|
||||
<Filename Value="RESTBenchmark.dpr"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit0>
|
||||
</Units>
|
||||
</ProjectOptions>
|
||||
<CompilerOptions>
|
||||
<Version Value="11"/>
|
||||
<Target>
|
||||
<Filename Value="RESTBenchmark"/>
|
||||
</Target>
|
||||
<SearchPaths>
|
||||
<IncludeFiles Value="../..;../../..;$(ProjOutDir;$(ProjOutDir)"/>
|
||||
<Libraries Value="../../../static/$(TargetCPU)-$(TargetOS)"/>
|
||||
<OtherUnitFiles Value="../..;../../.."/>
|
||||
<UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
|
||||
</SearchPaths>
|
||||
</CompilerOptions>
|
||||
<Debugging>
|
||||
<Exceptions Count="1">
|
||||
<Item1>
|
||||
<Name Value="Unknown"/>
|
||||
<Enabled Value="False"/>
|
||||
</Item1>
|
||||
</Exceptions>
|
||||
</Debugging>
|
||||
</CONFIG>
|
@@ -0,0 +1,28 @@
|
||||
unit RESTModel;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
SynCommons,
|
||||
SynTable, // for TSynValidateText
|
||||
mORMot;
|
||||
|
||||
type
|
||||
TPerson = class(TSQLRecord) // TSQLRecord has already ID: integer primary key
|
||||
private
|
||||
fName: RawUTF8;
|
||||
published
|
||||
/// ORM will create a NAME VARCHAR(80) column
|
||||
property Name: RawUTF8 index 80 read fName write fName;
|
||||
end;
|
||||
|
||||
const
|
||||
SERVER_ROOT = 'root';
|
||||
SERVER_PORT = '888';
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
|
||||
end.
|
@@ -0,0 +1,5 @@
|
||||
ln -s "`pwd`/rest_benchmark.socket" /etc/systemd/system
|
||||
ln -s "`pwd`/rest_benchmark.service" /etc/systemd/system
|
||||
systemctl enable rest_benchmark.socket
|
||||
systemctl start rest_benchmark.socket
|
||||
systemctl status rest_benchmark.socket #output status to console
|
@@ -0,0 +1,59 @@
|
||||
upstream mormot-uds {
|
||||
server unix:/tmp/rest-bench.socket;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream mormot-sock {
|
||||
server localhost:8888;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8889;
|
||||
|
||||
server_name localhost;
|
||||
|
||||
access_log /dev/null;
|
||||
# prevent nginx version exposing in Server header
|
||||
server_tokens off;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
# Tell upstream real IP address of client
|
||||
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# Do not rewrite a URL while pass it to upstream
|
||||
proxy_redirect off;
|
||||
# Let's upstream handle errors
|
||||
proxy_intercept_errors on;
|
||||
tcp_nodelay on;
|
||||
|
||||
# proxy all requests to the beckend
|
||||
location / {
|
||||
proxy_pass http://mormot-sock;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8887;
|
||||
|
||||
server_name localhost;
|
||||
|
||||
access_log /dev/null;
|
||||
# prevent nginx version exposing in Server header
|
||||
server_tokens off;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
# Tell upstream real IP address of client
|
||||
proxy_set_header X-Real-IP $realip_remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# Do not rewrite a URL while pass it to upstream
|
||||
proxy_redirect off;
|
||||
# Let's upstream handle errors
|
||||
proxy_intercept_errors on;
|
||||
tcp_nodelay on;
|
||||
|
||||
# proxy all requests to the beckend
|
||||
location / {
|
||||
proxy_pass http://mormot-uds;
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=mORMot socket activation demo
|
||||
Documentation=https://synopse.info
|
||||
RefuseManualStart=true
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
ExecStart="/home/pavelmash/dev/tmp/crypto_ssl/mORMot/SQLite3/Samples/36 - Simple REST Benchmark/RESTBenchmark"
|
||||
# starting from systemd v244 is possible to configure log limits per service as below
|
||||
# befote - only inside journald.conf
|
||||
# LogRateLimitIntervalSec=1s
|
||||
# LogRateLimitBurst=50000 # ~10 000 req / sec
|
@@ -0,0 +1,7 @@
|
||||
[Socket]
|
||||
ListenStream=127.0.0.1:8889
|
||||
NoDelay=true
|
||||
KeepAlive=true
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
Reference in New Issue
Block a user