1

Recent Discovery

Among everything else I tried, I replaced my JMeter profile with a custom JavaScript that hit each of my API endpoints in turn in an infinite loop, and then ran this script in parallel in different browsers (one Firefox, one Chrome, one Safari) - to try and rule out issues related to all of my connections coming from the same source (same user agent, same cookies, same session ID, etc)

When I did this, I noticed that all of my issues went away. The queries ran in parallel and the app was a lot more responsive than JMeter would have you believe

It seems impossible to me that JMeter would be serializing the requests, since it's a de facto standard for load testing. So I started trying to reproduce the behavior

In an attempt to re-create the JMeter I created the following two PHP scripts which (hopefully) simulated my Yii application:

slow.php

<?php

session_start();

$_SESSION['some'] = 'value';
// Yii is calling session_write_close() almost immediately after
// the session is initialized, but to try and exacerbate issues,
// I've commented it out:
// session_write_close();

$dsn = "mysql:host=localhost;dbname=platypus;unix_socket=/tmp/mysql.sock";
$pdo = new PDO($dsn, "user", "password");
// Yii was using whatever the default persistence behavior was,
// but to try and exacerbate issues I set this flag:
$pdo->setAttribute(PDO::ATTR_PERSISTENT, true);
// Simulate a query running for 1 second by issuing a 1-second sleep
$pdo->query("DO SLEEP(1)");

echo "Done";

fast.php

<?php

session_start();

$_SESSION['some'] = 'value';

$dsn = "mysql:host=localhost;dbname=platypus;unix_socket=/tmp/mysql.sock";
$pdo = new PDO($dsn, "user", "password");
$pdo->setAttribute(PDO::ATTR_PERSISTENT, true);
// Simulate a query running for 0.1 seconds
$pdo->query("DO SLEEP(0.1)");

echo "Done";

Running JMeter against these two new endpoints there was no serialization of requests. Everything ran in parallel. fast.php always returned in 100-150ms and slow.php always returned in 1000-1050ms even as I scaled up to 3, 4, and 5 threads. I was able to watch things collapse at 11 threads, but that's because I exceeded the number of worker threads in PHP

So to summarize:

  • The issue only occurs when profiling my API with JMeter and is not inherent in the app itself
  • The issue isn't just a JMeter bug, but is somehow tied to my application or Yii 1.1
  • I tried but could not come up with a minimum repro case

Despite the issue being non-existent when profiling with others tools, lots of people responded and gave lots of helpful information:

  • Avoid persistent connections in PHP (could cause multiple requests to share a connection, probably not)
  • Avoid session locking by calling session_write_close() as early as possible
  • Ensure you have enough PHP worker threads to handle the number of simultaneous connections
  • MySQL fully supports parallel requests (if the hardware can handle it)
  • Be wary of table locking (any transaction with an UPDATE statement could potentially lock the tables)
  • MyISAM does table-level locking instead of row-level locking

Original Post

I inherited a web application and I'm trying to make sense of its performance profile so I can start optimizing it for speed.

One thing I noticed pretty early on is that requests to the server are being serialized. For instance, suppose I have three endpoints with response times like so:

/api/endpoint1 --> 50ms
/api/endpoint2 --> 1000ms
/api/endpoint3 --> 100ms

If I hit a single endpoint, I measure the appropriate response times. But when I set up a script to hit all 3 at once I will sometimes see something like the following:

endpoint1: 50ms
endpoint2: 1050ms
endpoint3: 1150ms

Clearly the call to endpoint3 was queued and waiting for the call to endpoint2 to finish before it got a response.

My first thought was that this should be trivially solved with multithreading, so I took a look at the server configuration. PHP-FPM's process manager was set to "dynamic" with "start_servers" of 1, "max_children" of 5, and "max_spare_servers" of 2. For testing purposes I swapped this to "static" so that 5 PHP processes would remain open for handling connections in parallel (more than the 3 for the number of endpoints I was hitting, so they should be able to process simultaneously)

This had no impact on performance, so I looked at my nginx config. "worker_processes" was set to 1 with "worker_connections" set to 1024. I know that nginx uses an event loop model, so it shouldn't be blocking while it waits for a response from PHP-FPM. But just in case, I bumped up "worker_processes" to 5

Still, no effect. So next I looked at the database. All 3 endpoints had to hit the database, and I know as a fact that the 1000ms response time is mostly spent waiting on a long-running database query. I tried setting "thread_pool-size" to 5 and also within the MySQL REPL I set "innodb_parallel_read_threads" and "mysqlx_min_worker_threads" to 5

Still, my requests were getting serialized. When I log into the MySQL REPL and type show processlist; while my script is running (using a while-true loop to repeatedly hit those 3 API endpoints) I noticed that there was only ever one connection to the web application's user

Unfortunately I'm not sure if my issue lies with the database (not allowing more than one connection), with PHP-FPM (not processing more than one request at a time), or with nginx (not forwarding more than one request at a time to PHP-FPM). I'm also not sure how to figure out which one is acting as the bottleneck

Update

Looking around some more I found this SO post which seems to suggest that MySQL doesn't support parallel queries from the same user (e.g. from the web application user)

Is this true? Surely such a ubiquitous database engine wouldn't have such a performance flaw, especially given how commonly it's used with AWS for massively scaled applications. I understand that for simple "read from disk" queries parallelizing them wouldn't improve performance since they'd just have to sit in a queue waiting on disk I/O, but modern databases have in-memory caches, and most of the really slow operations like filesort tend to happen in memory. There's no reason a disk-bound query couldn't run in parallel (make a request to disk and start waiting on I/O) while a cpu-bound query is busy sorting a table in RAM. The context switching may slightly slow down the cpu-bound queries, but if slowing those down from 1000ms to 1200ms means my 5ms query can run in 5 ms, I think that's worth it.

My queries

Here are the queries for my 3 endpoints. Note that the timings listed are the response time for the full HTTP pipeline (from browser request to response) so this includes overhead from nginx and PHP, plus any post-processing of the query done in PHP. That said, the query in endpoint 2 makes up 99% of the runtime, and locks the database so that endpoints 1 and 3 are queued up instead of returning quickly.

endpoint1 (50ms)

SELECT * FROM Widget WHERE id = 1 LIMIT 1

(Note that 50ms is the full response time for the endpoint, not how long the query takes. This query is clearly on the order of microseconds)

endpoint2 (1000ms)

USE platypus;
SELECT `t`.`(49 fields)` AS `t0_cX`,
       `site`.`(29 fields)` AS `t2_cX`,
       `customer`.`(26 fields)` AS `t4_cX`,
       `domain`.`(20 fields)` AS `t6_c0`,
       `domain-general_settings`.`(18 fields)` AS `t8_cX`,
       `domain-access_settings`.`(17 fields)` AS `t9_cX`,
       `customer-general_settings`.`(18 fields)` AS `t10_cX`,
       `customer-access_settings`.`(17 fields)` AS `t11_cX`,
       `site-general_settings`.`(18 fields)` AS `t12_cX`,
       `site-access_settings`.`(17 fields)` AS `t13_cX`,
       `backup_broadcast`.`(49 fields)` AS `t14_cX`,
       `playlists`.`(11 fields)` AS `t16_cX`,
       `section`.`(10 fields)` AS `t17_cX`,
       `video`.`(16 fields)` AS `t18_cX`,
       `general_settings`.`(18 fields)` AS `t19_cX`,
       `access_settings`.`(17 fields)` AS `t20_cX`,
FROM   `broadcast` `t`
       LEFT OUTER JOIN `site` `site`
                    ON ( `t`.`site_id` = `site`.`id` )
       LEFT OUTER JOIN `customer` `customer`
                    ON ( `site`.`customer_id` = `customer`.`id` )
       LEFT OUTER JOIN `domain` `domain`
                    ON ( `customer`.`domain_id` = `domain`.`id` )
       LEFT OUTER JOIN `generalsettings` `domain-general_settings`
                    ON ( `domain`.`general_settings_id` =
                         `domain-general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `domain-access_settings`
                    ON
       ( `domain`.`access_settings_id` = `domain-access_settings`.`id` )
       LEFT OUTER JOIN `generalsettings` `customer-general_settings`
                    ON ( `customer`.`general_settings_id` =
                         `customer-general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `customer-access_settings`
                    ON ( `customer`.`access_settings_id` =
                         `customer-access_settings`.`id` )
       LEFT OUTER JOIN `generalsettings` `site-general_settings`
                    ON ( `site`.`general_settings_id` =
                         `site-general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `site-access_settings`
                    ON ( `site`.`access_settings_id` =
                         `site-access_settings`.`id` )
       LEFT OUTER JOIN `broadcast` `backup_broadcast`
                    ON ( `t`.`backup_broadcast_id` = `backup_broadcast`.`id` )
                       AND ( backup_broadcast.deletion IS NULL )
       LEFT OUTER JOIN `playlist_broadcast` `playlists_playlists`
                    ON ( `t`.`id` = `playlists_playlists`.`broadcast_id` )
       LEFT OUTER JOIN `playlist` `playlists`
                    ON
       ( `playlists`.`id` = `playlists_playlists`.`playlist_id` )
       LEFT OUTER JOIN `section` `section`
                    ON ( `t`.`section_id` = `section`.`id` )
       LEFT OUTER JOIN `video` `video`
                    ON ( `t`.`video_id` = `video`.`id` )
                       AND ( video.deletion IS NULL )
       LEFT OUTER JOIN `generalsettings` `general_settings`
                    ON ( `t`.`general_settings_id` = `general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `access_settings`
                    ON ( `t`.`access_settings_id` = `access_settings`.`id` )
WHERE
(
    (
        t.id IN (
    SELECT `broadcast`.id FROM broadcast
       LEFT JOIN `mediashare` `shares`
              ON ( `shares`.`media_id` = `broadcast`.`id` )
                 AND `shares`.media_type = 'Broadcast'
    WHERE 
    (
        (
            broadcast.site_id IN(
                '489', '488', '253', '1083', '407'
            )
            OR
            shares.site_id IN(
                '489', '488', '253', '1083', '407'
            )
        )
    )
        )
    )
    AND
    (
        (
            (
                (t.deletion IS NULL)
            )
        )
        AND
        (
            IF(
                t.backup_mode IS NULL,
                t.status,
                IF(
                    t.backup_mode = 'broadcast',
                    backup_broadcast.status,
                    IF(
                        t.backup_mode = 'embed',
                        IF(
                            t.backup_embed_status,
                            t.backup_embed_status,
                            IF(
                                '2020-01-08 16:34:52' < t.date,
                                1,
                                IF(
                                    t.date > Date_sub(
                                        '2020-01-08 16:34:52',
                                        INTERVAL IF(t.expected_duration IS NULL, 10800, t.expected_duration) second
                                    ),
                                    10,
                                    12
                                )
                            )
                        ),
                        t.status
                    )
                )
            ) != 0
        )
    )
)
LIMIT  10;

This query takes roughly 1000ms to run, but the PHP for the endpoint is extremely simple (run the query, return the results as JSON) and only adds a couple milliseconds of overhead

endpoint 3 (100ms)

SELECT * FROM platypus.Broadcast
    WHERE deletion IS NULL
    AND site_id IN (SELECT id FROM platypus.Site
               WHERE deletion IS NULL
                 AND customer_id = 7);

There's additional validation on the PHP side here which makes this endpoint take 100ms. The SQL, as you can see, is still fairly simple.

Create Table Statements

As there is a post length limit in StackOverflow, I cannot show the CREATE TABLE for every single table touched by endpoint 2, but I can show at least one table. Others use the same engine.

CREATE TABLE `Widget` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `widget_name` varchar(255) NOT NULL,
  `widget_description` varchar(255) NOT NULL,
  `status` varchar(255) NOT NULL,
  `date_created` datetime NOT NULL,
  `date_modified` datetime NOT NULL,
  `auto_play` varchar(255) NOT NULL,
  `on_load_show` varchar(255) NOT NULL,
  `widget_content_source` varchar(255) NOT NULL,
  `associated_sites` text NOT NULL,
  `author_id` int NOT NULL,
  `associated_sections` text,
  `after_date` datetime DEFAULT NULL,
  `before_date` datetime DEFAULT NULL,
  `show_playlists` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `is_classic` tinyint(1) NOT NULL,
  `default_site` int unsigned DEFAULT NULL,
  `auth_code_url` varchar(255) DEFAULT NULL,
  `widget_layout_id` int unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_Widget_widget_layout_id_WidgetLayout_id` (`widget_layout_id`),
  CONSTRAINT `fk_Widget_widget_layout_id_WidgetLayout_id` FOREIGN KEY (`widget_layout_id`) REFERENCES `WidgetLayout` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1412 DEFAULT CHARSET=utf8

Note

Notice that endpoint 2 doesn't even touch the Widget table, but endpoint 1 (which ONLY touches the Widget table) is also queued up. This eliminates the possibility of table locking.

When observing the process list in MySQL, only one connection is ever being made to the database from the application user. The issue may therefore lie in my PHP configuration.

Explain for query 2

Attached is the EXPLAIN SELECT ... query for endpoint 2

enter image description here

Simpler experiments

To try and determine where the parallel pipeline was falling apart, I created two simple scripts:

sleep.php

<?php
sleep(5);
echo "Done sleeping";

return.php

<?php
echo "That's all";

Doing this (sleeping in PHP) and running my script to hit these two endpoints with 3 threads I saw no issues. return.php always came back in ~11 milliseconds, despite sleep.php taking 5066 on average. I then tried doing the sleeping in MySQL:

sleep.php

<?php
$pdo = new PDO("...", "user", "pass");
$pdo->query("DO SLEEP(5)");
echo "Done sleeping";

This, again, had no issues. The sleeping endpoint did not block the non-sleeping one.

This means that the issue does not exist at the nginx, PHP, or PDO level - but that there must be some kind of table or row locking going on. I'm going to re-enable the general query log and scan through every query being performed to see if I can figure out what's happening.

Final Update

If you scroll up to "Recent Discovery" at the top of this post, you'll notice that I've modified my understanding of the problem.

I was not having an issue with parallelization, but with JMeter. I have failed to create a simple repro case, but I know now that the issue does not lie with my application but rather with how I'm profiling it.

stevendesu
  • 15,753
  • 22
  • 105
  • 182
  • What version of MySQL? I don't think `innodb_parallel_read_threads` existed until 8.0.14 and has limited utility. `mysqlx_min_worker_threads` seems unrelated. – Rick James Jan 30 '20 at 23:37
  • "while my script is running" - does it wait before moving on?? – Rick James Jan 30 '20 at 23:38
  • 2
    "MySQL doesn't support parallel queries from the same user" -- False. – Rick James Jan 30 '20 at 23:39
  • Show us the 1000ms query. – Rick James Jan 30 '20 at 23:39
  • "There's no reason... " -- I agree. Can you add some print statements that show the timestamps (to the ms) for various actions? – Rick James Jan 30 '20 at 23:40
  • More on lack of parallelism: https://stackoverflow.com/a/59996241/1766831 – Rick James Jan 30 '20 at 23:59
  • Which Engine is being used for the tables? – Rick James Jan 31 '20 at 00:40
  • how did you setup "a script to hit all 3 at once" ? – Vo Kim Nguyen Jan 31 '20 at 06:37
  • @RickJames I'm on MySQL v8.0.19. My "script" is just a JMeter config to hit all 3 endpoints infinitely and in parallel using 3 threads. The engine is InnoDb. – stevendesu Jan 31 '20 at 15:59
  • Please post the EXPLAIN of enpoint2 - this query has multiple issues: lots of joined tables, a sub-select, OR conditions and an conditional where clause with an horrbily complicated condition - that alone can make any index obsolete, as the database has to fetch far more records in order to fullfil the condition... Database/table size does matter ALOT. A database is fastest, if data and index fit into memory. Anything else WILL involve disk-i/o as either index or data (or both) have to be loaded from the harddrive. SSD will help here, nonetheless, it's still a hundred times slower than RAM. – Honk der Hase Jan 31 '20 at 20:30
  • @LarsStegelitz I've posted the explain query. Please bear in mind that the issue with this problem is NOT that I'm trying to optimize the query (although that's obviously worth doing) -- it's that when I make multiple parallel HTTP requests the responses are being queued and processed serially, causing fast endpoints to wait on slow ones. I'm not trying to figure out why my query is slow. I'm trying to figure out why my whole API isn't processing requests in parallel. – stevendesu Jan 31 '20 at 20:38
  • Astonishing! The EXPLAIN looks quite good, hadn't expected that... Ok, screw that. What about the persistent db connection, like @Luxian suggested? – Honk der Hase Jan 31 '20 at 21:02
  • I have `pdo_odbc.connection_pooling=off` in my `php.ini` as well as `mysqli.allow_persistent=off`, so I don't ***think*** PHP is connection pooling, but I don't know how to verify. I ***do*** know that I only ever see one connection to the database from PHP at a time. It's possible a change to my PHP configuration will fix this. – stevendesu Jan 31 '20 at 21:06
  • Which database driver is the API actually using? For PDO, persistence can be activated from the constructor (eg. check the code where the connections are established). PDO_ODBC would be a bad choice anyway (PDO_MYSQL would be better). – Honk der Hase Jan 31 '20 at 21:29
  • It's Yii 1.1, and I know that Yii uses PDO, but I don't know if it's using ODBC or MYSQL. I didn't see any `pdo_mysql...` settings in the `php.ini`, but that doesn't mean PDO_MYSQL isn't installed, just that I'm not overriding any defaults. A quick search through the Yii source code found that they have a `setPersistent` method on the database connection object, but I can't find a default value. – stevendesu Jan 31 '20 at 21:52
  • @LarsStegelitz I think we're on to something with this persistent setting! I took a look at `Yii::db->getPersistent()` and it returned `null` (no setting, default to whatever PDO wants to do). I explored the Yii docs and found I could set `'persistent' => true/false` in the config file. This caused `Yii::db->getPersistent()` to return `0` or `1` accordingly. Now strangely when set to `false` I never saw more than 1 connection to the database (current behavior), but when set to `true` I saw one connection ***per PHP worker***. Something finally changed! Still serialized requests, though – stevendesu Jan 31 '20 at 22:07
  • So, you now have paralell connections to the DB? Still only one query being processed at a time... on which isolation level do your transactions run? (lemme guess: SERIALIZEABLE ?) – Honk der Hase Feb 01 '20 at 09:15
  • Run your tests with simple scripts using `sleep()` (both in PHP and MySQL - but in separate tests). – Paul Spiegel Feb 01 '20 at 09:29
  • Your simpler experiments are not using sessions, so you cannot reproduce the session lock. Now knowing that you are using JMeter, it seems [according to this](https://stackoverflow.com/questions/1389464/session-cookie-management-in-apache-jmeter) and depending upon your JMeter configuration, that your cookies are shared between your JMeter threads, and experiencing session locks. Have you tried with `session_start();` in your simpler experiments ? – hsibboni Feb 03 '20 at 22:25
  • @hsibboni See the "recent discovery" bit at the top of my post. I wanted to interrupt anyone new who stumbled upon the question before they got too deep in the weeds. I created a test using `session_start()` and did not experience session locks. But that doesn't mean there isn't a session locking problem - it just means that I'm not doing enough to trigger it in my simple repro case. Maybe something about how Yii has the sessions configured. – stevendesu Feb 04 '20 at 15:22
  • 1
    **Major update!** I figured out how to reproduce the session lock! Reading through the Yii app deeper, I found that it was using a custom `session_name`. My JMeter config wasn't getting tripped up on the session lock in my minimum repro case because it was using the wrong cookie name. When I updated my JMeter profile to use the default session name, or when I used `session_name(...)`, I reproduced the session lock and saw the same serial behavior! – stevendesu Feb 04 '20 at 15:34

4 Answers4

4

MySQL + PHP + Apache has 'always' been very good at running separate SQL statements in 'parallel'. If separate users issue HTTP requests, they will naturally go through Apache quickly (probably in sequence, but fast) and get to separate instances of PHP (assuming Apache has configured enough 'children'). Each PHP script will make its own connection MySQL. MySQL will accept multiple connections quite rapidly (assuming max_connections is high enough, which it is by default). Each MySQL connection will work independently (baring low-level database locks, mutexes, etc). Each will finish when it finishes, ditto for PHP, and Apache returning results to the user.

I assume (without knowing for sure) that nginx works similarly.

Note: I suggest that Apache (and nginx) does things serially. But I suspect it takes on the order of a millisecond to hand off an HTTP request to PHP, so this "serial" step won't explain the timings you found.

I conclude that one of these is not really happening:

  • The configuration at each step is not allowing 3 children/connections/etc, or
  • There are 3 separate HTTP requests.
  • There are 3 separate PHP scripts.
  • The 3 SQL statements are not blocking each other. (Please provide the SQL.) Note: ENGINE=MyISAM uses table locking; this, alone, might explain the problem. (Please provide SHOW CREATE TABLE.)

It may be possible (after seeing the SQL) to speed up the SQL, thereby diminishing the overall problem of sluggishness.

Queries

Assuming id is the PRIMARY KEY of each table, then these other indexes may be beneficial at speeding up Query 2:

backup_broadcast:  (deletion, id)
shares:  (media_type, media_id, site_id)
broadcast:  (site_id, id)
video:  (deletion, id)
playlists_playlists:  (playlist_id, broadcast_id)

playlist_broadcast smells like a "many-to-many mapping" table. If so, I recommend following the tips in http://mysql.rjweb.org/doc.php/index_cookbook_mysql#many_to_many_mapping_table . (Ditto for any similar tables.)

OR and IN ( SELECT ... ) tend to be inefficient constructs. But it sounds like you don't have any control over the queries?

Is that a LIMIT without an ORDER BY?? Do you care which 10 rows you get?? It will not be predictable.

What happens with that huge number of columns? Seems like most of them will be the same every time you run the query, thereby making it mostly a waste of time??

For query 3, site needs INDEX(deletion, customer_id) (in either order). However reformulating it to use a JOIN or EXISTS would probably run faster.

Rick James
  • 135,179
  • 13
  • 127
  • 222
  • Speeding up the SQL is definitely an ultimate goal (and great improvements have already been made, bringing it down from 4 seconds to 1 second), but it's a complex query being generated by an ORM on top of a poorly designed schema - and it's mostly legacy stuff that we'll have trouble adjusting. I'll update my original post with the queries shortly – stevendesu Jan 31 '20 at 16:01
  • I've updated my post with one of the create table statements so you can see the engine and basic database setup (it's not super complex). I've also included the queries being run. Table locking is not occurring, but there is only one connection being made to the MySQL database at a time from PHP, despite 3 separate PHP worker processes running. – stevendesu Jan 31 '20 at 16:23
  • @stevendesu - I don't think it is possible for 3 "separate worker processes" to share a single connection. Do `SHOW PROCESSLIST;` to see how fast the "pids"are growing. – Rick James Jan 31 '20 at 18:05
  • @stevendesu - (I tweaked the index recommendations again.) – Rick James Jan 31 '20 at 18:16
  • Observing the pid it does increase, but not very quickly. About half the time when I run `show processlist;` I get back that the "Command" is `Sleep`, and the pid will remain unchanged for 1-2 seconds at a time, then suddenly jump by 4 or 5. As far as changing the query, I have limited ability to do that. The query is generated from a rather old and massive Yii 1.1 application with lots of scopes and filters. Instead of editing the query directly, I have to make modifications to the PHP code that's generating it, which can be tedious to trace. – stevendesu Jan 31 '20 at 18:52
  • I added the indexes you recommended (note that we already have a bunch of indexes, so some of them may have been duplicates) and re-ran my JMeter script just to see what would happen. No affect on performance. – stevendesu Jan 31 '20 at 18:59
  • @stevendesu - 1-2, then 4-5 -- Is that consistent with 3 new pids every 1150ms? As opposed to 1 new pid in 1150ms. If "3", then I conclude that the nginx/php/mysql is doing 3 things in parallel, but the queries are blocking each other. – Rick James Jan 31 '20 at 19:31
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/206993/discussion-between-stevendesu-and-rick-james). – stevendesu Jan 31 '20 at 19:33
  • @stevendesu - Can you provide `EXPLAIN SELECT ...` for each of the 3 queries? How much RAM on the database server? How big is the dataset? What is the value of `innodb_buffer_pool_size`? – Rick James Jan 31 '20 at 19:33
  • After further profiling I've discovered that WHATEVER my issue is, it only reproduces when using JMeter. Something about JMeter, Yii 1.1, and my application are not playing nice together (I suspect it's related to all of my JMeter requests sharing a PHP session ID). The issue did not occur when I wrote a JavaScript test that I ran in the browser. For this reason, I'm going to put this question down and mark your answer for the bounty as it provided the most useful information. – stevendesu Feb 03 '20 at 22:07
3

I think you have an issue with php session locking : your second and third query are trying to access the same php session, and are waiting.

Try to call session_write_close as soon as you can, to free your php session. As soon as you can : when you are sure you will not write any more data in your php session.

An easy way to check this is to try with 2 browsers or in anonymous/incognito mode : your cookies will not be shared, and you should have 2 sessions, not blocking each other.

hsibboni
  • 433
  • 2
  • 8
2

MySQL can handle a lot of parallel queries, but you can't do more than one query at the time for each connection. The way PHP is usually setup is that each request goes to a different thread/process, so each process will have its own connection to MySQL, thus the problem mentioned is avoided. Unless you use persistent connection inside PHP and then you might end up using the same connection for each request. If that's the case it should be easy to disable it and go back to the standard one database connection per request model.

My first guess is that endpoint 2 triggers some locking on the database and that's why endpoint3 query is queued until enpoint2's query finishes. This can be fixed by changing the logic in the code (avoid or minimize the locking of the database), or by changing database configuration or table engines used to better suit application needs. Example: InnoDB uses row level locking while MyISAM locks the whole table lock.

Profiling will be really helpful if you don't mind configuring it. I suggest to have a look at Blackfire.io, New Relic or xdebug profiling if you go this route. You will be able to find the bottlenecks faster this way.

Luxian
  • 676
  • 7
  • 14
  • I've attempted to use xdebug for profiling, but I wasn't able to garner a lot of useful information. I ended up with a massive graph and thousands of lines of logs and metrics that were nearly impossible to sift through. After a whole day of reading I concluded "we're waiting on I/O from the database". This led me to check things out from the database side, where I found there's only one connection being made at a time despite having 3 PHP worker processes running – stevendesu Jan 31 '20 at 16:26
  • How can I verify if PHP is using persistent connections? I have connection pooling disabled for PDO in `php.ini`, but I don't know if that's the only setting I need to tweak or if it's being overridden somehow – stevendesu Jan 31 '20 at 21:16
  • Search for `PDO::ATTR_PERSISTENT`, or see your ORm docs (since you said you are using an ORM). – Luxian Feb 02 '20 at 08:55
  • The ORM docs specify that persistence can be enabled (using `Yii::app->db->setPersistent(true)`) but don't specify the default behavior. I experimented and found that when I enabled persistence I started seeing more than one connection to the database in `show processlist;`, but only one connection was utilized at a time and requests were still serialized. So the issue seems to lie not with MySQL, but with PHP. It almost seems like one worker thread is handling ***all*** requests. Perhaps an FPM setting? – stevendesu Feb 03 '20 at 14:18
0

HM... too long for a comment.

a little bit simplified every engine has one queue where it gathers querys to be computed, depending on hardware it uses 2 or 3 or even more threads to compute every query. More threads are running more time every query needs, because of locks, like it locks an entire Table, when it inserts a new row with autoincrement.(you will find with a search many examples for locks). Of course every query needs memory and other resources that they have to share with the rest of all computer software that is running on a server.

With clustes you pay the price with overhead to manage multiple sql servers.

So from sql server side, it is parallel, however you need the hardware to support many threads/many engines(which should only be uses very carefully)

Of course you can have many users in sql, but for convenience sake, you have usually one per APP or sometimes even one per server. But the same user can access the database simultaneously, but you can disable that of course.

Your php runs parallel, because webserver are build to run papallel requests and and there it doesn't matter if it runs php, Python(django) or javascript(nodejs) , apache, IIS, nginx and there are a lot more, every technology has there perks and of cause more module you add to en engine, so much slower it gets.

So everything is parallel to a certain degree and you can increase the power of such systems as you see in cloud providers or virtual servers and so on.

The limits you only notice when like the introduction of Pokemon go or new games where even the huge cloud providers crash. Or the disaster with ObamaCare where nothing was tested on that scale, whichever idi... was responsible,

Parallelizing such tasks is difficult, because in case of a webserver and sqlserver it has to a degree caches where they park requests that are often made, but usually every request needs its own data.

In reality everything is much more complicated, starting with cpus with 3 Pipelines , Multiple cores and shared memory(which caused Meltdown and their brothers), goes over tables or databases that reside only in memory for high performance or web server that run only in cache of cpus, which is much faster than memory or harddrives.....

nbk
  • 45,398
  • 8
  • 30
  • 47
  • MySQL specifically has one "thread" or one "process" (depending on OS) per _connection_. Hence, separate logins (connections) are running virtually independently. – Rick James Jan 30 '20 at 23:42