You are not logged in.
Pages: 1
While migrating my projects from Windows/Delphi to Linux/FPC I discovered a problem with HTTP server threading model and need an advice how to solve it.
The problem:
- In case of THttpApiServer, we have a fixed size thread pool of "workers" where I can run a business logic, URI methods are passed to these threads to be executed, the number of threads in the pool is defined in a call to THttpApiServer.Clone(threadPoolSize), threads in the pool are stopped when HTTP server is stopped
- in case of THttpServer "worker" threads are created and stopped depending on client's Keep-Alive and request size
Every "worker" thread in my case creates a database connection using ThreadSafeConnection and JS engine, thus, since we have a fixed size thread pool (in case of HTTP API server) we know the number of connections to a database will not exceed threadPoolSize.
In the case of THttpServer every Keep-Alive session will create its own thread and in my case DB connection + JS engine. This thread is terminated and closes DB connection and release JS engine after keepalive timeout is expired.
Connecting/disconnecting to a database can produce a huge overhead. In case of Oracle, we have very hi-load systems and cost of connecting is very expensive, in case of Postgre even for a small load connect is expensive, because of Postgre forks on every connection. And initializing of JS engine is expensive too.
That's why we definitely need to have I fixed size thread pool of "worker" threads for THttpServer, the same as in case of THttpApiServer.
The possible solution:
I can create my own TSynThreadPoolTHttpServer descendant to implement my requirements (and make a THttpServer.ThreadPool property writable). Or can you suggest a better solution?
Offline
Could you not just use a nginx front-end with keep-alive on this side, and non persistent HTTP connections on the mORMot loopback server?
Then it will use the thread pool of THttpServer.
Offline
I'll try this idea and answer about results here. Looks like the variable THREADPOOL_BIGBODYSIZE should be set to infinity for this case, but this is a solution, for sure. Thanks!
Offline
Otherwise we could with not much problem implement an event-driven HTTP server, starting from TPollSockets for instance.
But if we can just tune a nginx front-end, it is a better solution, since nginx is very much tuned and proven.
Please forward your finding about this matter!
Thanks!
Offline
I agree turning nginx is a "Unix way" - mORMot/SyNode for business logic, nginx for HTTP, static, cache. I will publish a final nginx config/mORMot patch after solve all problems.
Offline
I'm made two small changes in SynCrtSock - one for disable KeepAlive for TWinHTTP and one to prevent reading on GET request body if Content-Length header is missing (browsers do not send a Content-Length header for GET requests). See - this commit. For second change - see this git commit I think we can remove and (KeepAliveClient or (fMethod = 'GET')) condition at all?
Offline
@ab - can you, please, accept a 69 merge request. It make a ResolveIPToName function the same for Delphi & FPC.....
Offline
Status report: After fixing unexpected THTTPServer main loop termination - see pull request 78 and explicitly force server to use a thread pool only by introducing a DisableKeepAlive property we able to run out software on Linux behind a nginx with a good performance.
One more feature what should be implemented is sending files asynchronous like in HTTP.sys by using X-Accel-Redirect nginx feature, and we will be ready to replace Windows+HTTP.sys to Linux+nginx on hiload
Offline
Good news!
I made some changes after this pull request, which broke some part of the framework.
See https://synopse.info/fossil/info/1174112c23826fa7
But there was indeed a nasty bug!
About X-Accel-Redirect it will be easy to add, I guess - and very helpful in respect to the current load-in-memory-then-send-on-the-loopback implementation.
Is there similar options for other web servers?
Offline
Check https://synopse.info/fossil/info/0d130cce62
(not tested....)
Offline
Dear @ab!
Thanks a lot for the latest changes and sorry for broke some other parts of the framework.
I'm currently trying to wrap up all the things and found that you missed ThreadPoolContentionAbortDelay property initialization. And currently this is a read-only property, should it be writable, what do you think?
Offline
And one more thing - it could be handy to get back ThreadPoolContentionCount property but increase it only once - just before the repeat loop starts. This would allow average contention delay calculation dividing ThreadPoolContentionTime by ThreadPoolContentionCount
Offline
Please check https://synopse.info/fossil/info/b94adf736c
Offline
I'm fix implementation of THttpServer.OnNginxAllowSend - see [f3080e88a]. Previous not compiled in Delphi and work with fNginxSendFileFrom as a string instead of array..
Offline
fNginxSendFileFrom[] was changed into an array, and your fix was incorrect due to wrongfNginxSendFileFrom[f] index, and a non intialized n variable.
Please check https://synopse.info/fossil/info/c2e8707720
Offline
On my side was a check of contention parameters calculation and I've found that it is needed to add 'tix := GetTickCount;' assignment immediately after SleepHiRes block. Otherwise, the last loop before a successful fThreadPoolPush call is not counted in fThreadPoolContentionTime.
Removing 'break' after a successful fThreadPoolPush call is not an alternative because we should not include the time spent on the fThreadPoolPush call itself.
Offline
I guess that the time spent on the fThreadPoolPush call itself is much lower than one millisecond - so won't inflect the whole time reported.
Less than a microsecond for sure, to loop over the whole threads list.
But flow is more obvious with an explicit break, anyway.
Offline
I will merge curl http fix to the trunk manually (Sergey make pull request to the fb_synode_311 - this is wrong)
Another fix in pull request 83 - about usage of HTTP proxy under Linux. We fix it on the SyNode level by getting a default proxy server from http_proxy environment variable - this is a common way to specify a proxy under Linux. But I think will be good do this to the SynCrtSock. @ab - what do you think?
Offline
@ab - I commit to fossil, but can't commit to github (my company setup a app firewall ) Please, apply SynCrtSock changes to github trunk..
UPD. I push it...
Last edited by mpv (2018-02-23 14:24:11)
Offline
Nice TCurlHTTP issue catch!
But for http_proxy, sometimes it is not needed (e.g. if server is on loopback)...
Perhaps it is up to the client application to properly set the proxy value, according to http_proxy, before calling SynCrtSock...
Offline
Ok. Let's it live inside SyNode. In case certain app need proxy I can run it as `http_proxy=http://bla-bla.com && myapp'. We use a pm2 or docker as a process managers, for both of them configuration using environment variables is a native way.
Offline
@mpv Could you pls paste your nginx configuration ? Same keep-alive performance prob occurs, and not familiar with nginx, That will be very helpful to me !
Offline
Don't forgot to set a THttpServer.RemoteIPHeader, disable KeepAlive by setting a KeepAliveTimeOut parameter to 0 duruing call to THttpServer.create(). And use a THttpServer.NginxSendFileFrom to send static files (if you need it - see internal location definition below)
The simplest config:
server {
listen 80 default_server;
listen [::]:80 default_server;
#proxy all requests to the mORMot backend
location / {
proxy_pass http://localhost:8881;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $realip_remote_addr;
proxy_redirect off;
}
#BLOB stores internal locations
location /ubstatic-autotest/simple {
internal;
alias D:/app/documents;
}
}
Last edited by mpv (2018-06-25 14:44:19)
Offline
thanks a lot mpv, just have made nginx_proxy_mormot works fine on linux, and performance seems great!
Tested with Jmeter (60 thread target 6 service-url ,and test 3 round), backend mormot is SOA-service, mainly issue sql query aginst oracle-db then return JSON to Jmeter.
client-Jmeter keepalive, Post to mormot, result is: 466KB/sec, 7QPS;
client-Jmeter keepalive, Post to nginx_proxy_mormot, result is: 13MB/sec, 190QPS;
client-Jmeter shortconnect , Post to mormot, result is: 15MB/sec, 225QPS;
client-Jmeter shortconnect , Post to nginx_proxy_mormot, result is: 14MB/sec, 207QPS;
when service on windows HTTP.sys:
client-Jmeter keepalive, Post to mormot(HTTP.sys), result is: 4MB/sec, 70QPS;
good news for nginx&mormot@linux
Offline
I'm discover a problem while working with THttpRequest under Linux using libcurl. THttpRequest expect what library implementation will throw in case of errors, and TWinINet / TWinHTTP throw. But TCurlHTTP.InternalRetrieveAnswer just return 404 as status and a libcurl error in body. This cause a different behavior for windows/linux. I'm create a pull request #127 to fix this problem.
@ab - please, merge it to master
Offline
@ab - about your improvements for Linux HTTP server thread pool.
It's a big step forward, but endless length of PendingContext queue will cause several problems:
1) critical for me - we lost ability to gracefully load balance our server farms since all request will be putted to the queue of first server in the balancing pool
2) "out of memory" in case of huge amount of pending requests in queue
I consider to create a fixed length PendingContext array because:
- we already have something similar for HTTP.SYS in THttpApiServer.HTTPQueueLength. So HTTPQueueLength property can be moved to THttpServerGeneric level and used by both type of servers.
- we can allocate fixed length fPendingContext on startup and use it as a Ring-Cache - this prevent memory move inside TSynThreadPool.PopPendingContext
- in case cache is overflow server should response 503 Service Unavailable ASAP - in this case load balancer (at last nginx) will mark this server as unavailable for a while ( see Health checks section here)
The default HTTPQueueLength in HTTP.SYS is 1000 and this enough for 99% of my apps.
In my practice I increase this value only once to 5000 because we do not have a load balancing where. Usually we keep it default and relay on load balancing.
What do you think - is it possible to implement fixed length pending queue?
Offline
Good idea.
I was waiting for feedback for you, indeed.
So I guess that if the queue is too big, the input socket should be rejected/shutdown immediately, so that load-balancing (via nginx?) will be possible?
Do you make a pull request or do you want me to do it?
Offline
I'm not sure for 100% socket rejection/shutdown is OK. HTTP.sys report 503 if request queue is overflow. But I think nginx is smart enough to decide socket rejection is equal to "response from a particular server fails with an error" as described in their documentation.
If you can, I would prefer you to make such a complex change, if not - I can try to write a pull request on weekend.
Offline
Offline
I look at the problem from different points and now I quite sure your first propose abut "input socket should be rejected/shutdown immediately" is a good choice.
I found documentation about what is unsuccessful attempt for nginx, so even more - we do not need to "accept" incoming connection if our http queue is overflow.
Having this config
location / {
proxy_pass http://balancing_upstream;
proxy_next_upstream error timeout invalid_header http_500 http_503;
proxy_connect_timeout 2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
- in case we do not accept incoming connection Nginx will wait for 2 second (proxy_connect_timeout = 2) and use next server from upstream.
- in case connection as already accepted but we shutdown socket without sending anything nginx will use next server from upstream because of invalid_header parameter in proxy_next_upstream directive
- in case we have one mORMot server we just keep a default proxy_connect_timeout (60 second) timeout and nginx will wait for our server to "accept" incoming connection
Offline
Please see https://synopse.info/fossil/info/714d2e83a8
I tested it with https://github.com/synopse/mORMot/tree/ … 0Benchmark and
ab -n 10000 -c 1000 http://localhost:8888/root/abc
command.
See https://gist.github.com/synopse/c0b2346 … 608636bf7b
with HTTPQueueLength set to 100.
Failed requests: 472
And https://gist.github.com/synopse/98c2343 … 39a027bd04
with HTTPQueueLength set to 1000 (default).
Failed requests: 0
Note that performance numbers seem pretty consistent.
80-100MB/s with a SQlite3 request returning 78,000 bytes of JSON, on both Windows (http.sys) and Linux (our socket server).
(I run abbench from the Windows WSL layer, so actual performance on a real Linux server would actually be higher)
Offline
Perfect! This is exactly what i need.
All my CI are passed for all environment (Linux, Windows). Today I put this version on the pre-production environments for a several days. So we can see how it work in the real life. Thanks again!
Offline
Since last post we setup two production on Linux with latest Socket server changes, both behind nginx as a reverse proxy. Client is browser. Real load is small - average is 10RPS, max is 600 RPS. All work as expected. And this is GOOD
But two days ago I try to setup Kerberos (and LDAP-baser custom auth) for server behind nginx and found a problem for both HTTP.SYS / Socket based server types.
The problem is in client ConnectionID used to identify NTLM/Kerberos and LDAP in my case. Situation is the same as with Real IP described above. We should "proxy" connectionID from nginx side.
I create a pull request 162 and introduce a RemoteConnIDHeader property (as we did with RemoteIPHeader).
But some changes need to be discussed:
- I use need to SynCommons for GetInt64 function. May be move functions required by SynCrtSock from SynCommons to SynStrUtils ( we have a copy of the same functions in both SynCommons and CrtSock - UpperCase, GetCardinal, Trim etc.). Or use a SynCommons in CrtSock?
- I think we should change ConnectionID type from Integer to Int64. With a load of 3000RPS we overflow Int for 8 days
Offline
Thanks to @Chaa & @ab for review. All issues is resolved. Actually I miss GetNextItemUInt64 in SynCrtSock.
Today I test RemoteConnIDHeader functionality on Windows with both Socket/HTTP.SYS based servers with and without reverse proxy and browser client (both FF & Chrome). NTLM auth work as expected now.
Hope tomorrow I setup a Kerberos on my domain and test a Linux part, but even without it request can be merged into master IMHO.
Offline
I'm still have a troubles with Active Directory based domain and server on Unix, but this is likely my infrastructure problems. @ab - please, merge #162 into master - at last on Windows it verified and works. I need this path for my productions...
Offline
Just for information - we successfully verify Negotiate authentication when server side is on Linux, Domain is Active Directory and clients is a Chromium browser.
In case server side is HTTP1.1 with KeepAlive then all work without any additional step
In case of server is HTTP 1.0 (no keep alive) or behind nginx:
// in Pascal server
HttpServer.RemoteConnIDHeader := 'X-Conn-ID'
// in nginx config
proxy_set_header X-Conn-ID $connection
Offline
Pages: 1