Reducing High CPU on MySQL: A Case Study
Look at how we approached the resolution of a MySQL performance issue and used Percona Monitoring and Management PMM to support troubleshooting.
Join the DZone community and get the full member experience.Join For Free
In this post, I want to share a case we worked on a few days ago. I'll show you how we approached the resolution of a MySQL performance issue and used Percona Monitoring and Management PMM to support troubleshooting. The customer had noticed a linear high CPU usage in one of their MySQL instances and was not able to figure out why as there was not much traffic hitting the app. We needed to reduce the high CPU usage on MySQL. The server is a small instance:
Models | 6xIntel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz 10GB RAM
This symptom can be caused by various different reasons. Let's see how PMM can be used to troubleshoot the issue.
It's important to understand where the CPU time is being consumed: user space, system space, iowait, and so on. Here we can see that CPU usage was hitting almost 100 percent, and the majority of the time was being spent on user space. Once we determined that the time was being spent on user space, we could discard other possible issues. For example, we could eliminate the possibility that a high amount of threads were competing for CPU resources since that would cause an increase in context switches, which in turn would be taken care of by the kernel — system space.
With that, we decided to look into MySQL metrics.
As expected, there weren't a lot of threads running —10 on average — and MySQL wasn't being hammered with questions/transactions. It was running from 500 to 800 QPS (queries per second). The next step was to check the type of workload that was running on the instance:
In red, we can see that almost all commands are SELECTS. With that in mind, we checked the handlers using
SHOWSTATUS LIKE 'Handler%' to verify if those selects were doing an index scan, a full table scan, or what.
The blue in this graph represents
Handler_read_rnd_next , which is the counter MySQL increments every time it reads a row when it's doing a full table scan. Bingo! Around 350 selects were reading 2.5 million rows. But wait, why was this causing CPU issues rather than IO issues? If you refer to the first graph (CPU graph), we cannot see iowait.
That is because the data was stored in the InnoDB Buffer Pool, so instead of having to read those 2.5M rows per second from disk, it was fetching them from memory. The stress had moved from disk to CPU. Now that we identified that the issue had been caused by some queries or query, we went to QAN to verify the queries and check their status:
For the first query, a
SELECT on table
store.clients was responsible for 98 percent of the load and was executing in 20+ seconds.
EXPLAIN confirmed our suspicions. The query was accessing the table using type ALL, which is the last type we want, as it means "Full Table Scan." Taking a look into the fingerprint of the query, we identified that it was a simple query:
The query was filtering clients based on the status field SELECT * FROM store.clients WHERE status = ? As shown in the indexes, that column was not indexed. Talking with the customer, this turned out to be a query that was introduced as part of a new software release.
From that point, we were confident that we had identified the problem. There could be more, but this particular query was definitely hurting the performance of the server. We decided to add an index and also sent an annotation to PMM so we could refer back to the graphs to check when the index has been added, check if CPU usage had dropped, and also check Handler_read_rnd_next.
To run the alter, we decided to use pt-online-schema-change, as it was a busy table, and the tool has safeguards to prevent the situation from becoming even worse. For example, we wanted to avoid pausing or even aborting the alter in the case of the number of Threads_Running exceeding a certain threshold. The threshold is controlled by --max-load (25 by default) and --critical-load (50 by default):
mm-admin annotate "Started ALTER store.clients ADD KEY (status)" && \ pt-online-schema-change --alter "ADD KEY (status)" --execute u=root,D=store,t=clients && \ pmm-admin annotate "Finished ALTER store.clients ADD KEY (status)" Your annotation was successfully posted. No slaves found. See --recursion-method if host localhost.localdomain has slaves. Not checking slave lag because no slaves were found and --check-slave-lag was not specified. Operation, tries, wait: analyze_table, 10, 1 copy_rows, 10, 0.25 create_triggers, 10, 1 drop_triggers, 10, 1 swap_tables, 10, 1 update_foreign_keys, 10, 1 Altering `store`.`clients`... Creating new table... Created new table store._clients_new OK. Altering new table... Altered `store`.`_clients_new` OK. 2019-02-22T18:26:25 Creating triggers... 2019-02-22T18:27:14 Created triggers OK. 2019-02-22T18:27:14 Copying approximately 4924071 rows... Copying `store`.`clients`: 7% 05:46 remain Copying `store`.`clients`: 14% 05:47 remain Copying `store`.`clients`: 22% 05:07 remain Copying `store`.`clients`: 30% 04:29 remain Copying `store`.`clients`: 38% 03:59 remain Copying `store`.`clients`: 45% 03:33 remain Copying `store`.`clients`: 52% 03:06 remain Copying `store`.`clients`: 59% 02:44 remain Copying `store`.`clients`: 66% 02:17 remain Copying `store`.`clients`: 73% 01:50 remain Copying `store`.`clients`: 79% 01:23 remain Copying `store`.`clients`: 87% 00:53 remain Copying `store`.`clients`: 94% 00:24 remain 2019-02-22T18:34:15 Copied rows OK. 2019-02-22T18:34:15 Analyzing new table... 2019-02-22T18:34:15 Swapping tables... 2019-02-22T18:34:27 Swapped original and new tables OK. 2019-02-22T18:34:27 Dropping old table... 2019-02-22T18:34:32 Dropped old table `store`.`_clients_old` OK. 2019-02-22T18:34:32 Dropping triggers... 2019-02-22T18:34:32 Dropped triggers OK. Successfully altered `store`.`clients`. Your annotation was successfully posted.
As we can see above, CPU usage dropped to less than 25 percent, which is 1/4 of the previous usage level. Handler_read_rnd_next dropped, and we can't even see it once pt-osc has finished. We had a small increase on Handler_read_next as expected because now MySQL is using the index to resolve the WHERE clause. One interesting outcome is that the instance was able to increase its QPS by 2x after the index was added as CPU/Full Table Scan was no longer limiting performance. On average, query time has dropped from 20s to only 661ms.
- Applying the correct troubleshooting steps to your problems is crucial:
- Understand what resources have been saturated.
- Understand what if anything is causing an error.
- From there you can divert into the areas that are related to that resource and start to narrow down the issue.
- Tackle the problems bit by bit.
- Having the right tools for the job key for success. PMM is a great example of a tool that can help you quickly identify, drill in, and fix bottlenecks.
- Have realistic load tests. In this case, they had tested the new release on a concurrency level that was not like their production
- By identifying the culprit query we were able to:
- Drop average query time from 20s to 661ms
- Increase QPS by 2x
- Reduce the usage of CPU to 1/4 of its level prior to our intervention
Disclosure: For security reasons, sensitive information, such as database, table, and column names have been modified, and graphs were recreated to simulate a similar problem.
Published at DZone with permission of Marcelo Altmann, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.