Methodology

Load testing was performed against our preview environment using scrubbed data from real interactions with the chatbot. All conversations included at least five guest interactions to simulate actual engagement. All client/server interactions were accounted for and reproduced faithfully, including non-critical functionality such as rating responses.

Differences from Production

The most notable difference with load testing was using a zero second interval between polling calls. Normally polling is performed in one second intervals, though we decided to remove the delay between calls so the entire test could run within time limits. Being faithful to the source data, the same number of polling calls are made, however, these are now back-to-back with now pause between. This - in our opinion - will produce equal, if not greater, stress on the server.

Another challenge was scaling the application in a natural manner. The chatbot server, being hosted in Google App Engine, has built in scaling capabilities. This scaling handles normal web traffic well, where traffic will ramp up over a period of time. The testing performed, in contrast, spiked load in a less-than-natural manner, resulting in "hard starts". To counteract the spikes, we tuned the scaling parameters so that we had more idle instances to "smooth out" the initial spike. Some of the changes are candidates for inclusion in the mainline code base.

We also encountered some issues with our Dialogflow projects. All our environments were configured for Dialogflow Essentials Edition, which had an upgraded quota of 3,000 text queries per minute, which surpassed. As such, we submitted a request to have this quota increased to 6,000 per minute which allowed us to proceed without the results being tainted by errors.

Lastly, we increased capacity of the database in our preview environment. This was to determine the role the database played in scalability. The result was favourable and will be very likely implemented in production before any new chatbots are added to the platform. 

Configuration

Preview

Production

​vCPUs41​
Memory15GB3.75GB
Max Connections80004000

Results

Overview

During our test, we were able to simulate about 11,000 concurrent users


The primary optimization, adding client-side caching to OPTIONS calls, reduced the number of requests and thus the number of instances required

With 2.1, we had 1,713 created instances and 344 active instances. The optimizations slashed these number significantly.


As a direct result, we reduce the number of active database connections from about 2,600 to 660. 


The overall CPU utilization was well in the safe zone at 33%, meaning we should be able to scale even higher than observed.

Data

module method endpoint minimum latency average latency maximum latency average pending maximum pending calls
v1 GET /v1/listen 0.005814 0.064754 15.684197 0.013804 15.052087 2381618
v1 PUT /v1/talk 0.003332 0.100979 13.303746 0.047544 12.099215 8638
v1 PUT /v1/trigger 0.056535 0.289955 13.907069 0.078147 12.056342 5785
v1 OPTIONS /v1/listen 0.002227 0.264699 14.162539 0.159387 10.457785 1731
v1 OPTIONS /v1/product/{product}/loader 0.003509 0.072225 6.844845 0.029410 2.467902 1432
v1 POST /v1/start 0.030852 0.364308 15.948447 0.213212 14.874535 1432
v1 OPTIONS /v1/start 0.003129 0.312106 15.026576 0.220793 10.451347 1432
v1 GET /v1/client/configuration 0.003403 0.093802 10.787438 0.044880 10.191730 1432
v1 OPTIONS /v1/client/configuration 0.003501 0.081803 1.991555 0.037026 1.810825 1432
v1 GET /v1/product/{product}/status 0.003214 0.088996 4.020589 0.043021 3.951815 1432
v1 OPTIONS /v1/product/{product}/status 0.003137 0.079331 2.879372 0.034322 2.860465 1432
v1 GET /v1/product/{product}/config 0.004115 0.082810 2.906476 0.037763 2.475531 1432
v1 OPTIONS /v1/product/{product}/config 0.003633 0.073426 9.224409 0.029035 1.227639 1432
v1 GET /v1/product/{product}/loader 0.004097 0.072809 4.460998 0.032217 4.269055 1432
v1 OPTIONS /v1/talk 0.002294 0.131691 10.329461 0.072742 10.153406 1418
v1 OPTIONS /v1/goodbye 0.002119 0.055697 9.386118 0.031516 9.159125 1221
v1 POST /v1/goodbye 0.025190 0.116825 3.276963 0.017951 0.998088 1221
v1 OPTIONS /v1/trigger 0.003009 0.248369 13.620989 0.138863 10.348752 1074
v1 GET /_ah/warmup 7.988969 12.215853 18.610605 0.000000 0.000000 356
v1 PUT /v1/feedback/message/{messageid} 0.042882 0.189455 10.626019 0.017763 0.184453 206
v1 OPTIONS /v1/feedback/message/{messageid} 0.002656 0.067476 0.799274 0.033715 0.675436 201
v1 POST /v1/product/{product}/webhook/dialogflow 0.042807 0.815533 2.895538 0.028044 0.651854 143
v1 GET /v1/cron/sessioncleanup 0.046256 0.100638 0.663077 0.000515 0.030924 60

(info) Latency and Pending values are in seconds

Analysis

By caching the results of CORS Pre-flight requests, we were able to significantly decrease overhead. This, in turn, increased our concurrent user support by nearly 15%. Additionally, we've also reduced demands on the database as a side effect.

One thing we did notice is that this wasn't a 100% successful test. There was one session that had failed to establish as the /v1/start call had been aborted as it took too long to warmup an instance to handle it. I believe this was the result of the servlet taking more than ten seconds to start, and as such, addressed the start up time in a future release.

Given that we were unable to cause any apparent bottleneck in the system (eg: database CPU utilization), one can assume that we've hit the limitation of our current load testing framework. We will need to determine a better approach for future load tests.

Attachments:

appengine-instances.png (image/png)
appengine-summary.png (image/png)
database-connections.png (image/png)
database-cpu.png (image/png)
data.csv (text/csv)
data.csv (text/csv)
data.csv (text/csv)