"FDB table" page produces high load

Hello!

Wen user accesses some page in the web interface, web server prepares an array of graphs to be displayed when user hovers the mouse pointer at the hostname, port, etc.
There is a setting available to disable hover effect and it works perfectly: $config['web_mouseover'] = false;. With this setting graphs are not displayed upon hovering.

However, it seems that web server still prepares all the RRD files in the background, despite this setting set to false.

In our environment this can be checked pretty easy:

  1. Open “Overview” -> “FDB Tables”
  2. Look at the CPU load, PHP-FPM processes or IO load on RRD storage.

Example:
Our FDB table contains 379581 entries. “FDB Tables” page shows all devices with the default view. When a user opens this page, PHP-FPM prepares all the graphs for all the devices, which never finishes even with 2GB max memory limit for a script.
LibreNMS, in a nutshell, just freezes until script execution timeout: http://prntscr.com/l4rt9l

Does anybody know, if it is possible to cut off graph preparation for mini-graphs (the ones that are used for display on hover effect)?

LibreNMS installation with 303 devices, 59976 ports produce this page 45.15s. This installation is a test one and is about 3 months old. ports_fdb table has 124412 entries.
LibreNMS installation with 1428 devices, 198786 ports cannot produce this page. This one is 2 years old. ports_fdb table has 378761 entries.

I have enabled another configuration option, $config['page_gen'] - this one shows how much time does it take a web server to produce a page. Interestingly enough, on a problem device it says

MySQL [21/3.77s]: Cell[11/0.16s] Row[-11/-0.16s] Rows[16/3.76s] Column[5/0.00s] Update[0/0.00s] Insert[0/0.00s] Delete[0/0.00s]
Cached data in memory is 0.000B. Page memory usage is 5.880MB, peaked at 50.600MB.
Generated in 3.987 seconds.

Which leads to thinking the problem is purely on RRD side. PHP workers are executing rrdtool queries in the background, which are producing massive I/O on the disk, hence the CPU load. The page itself is generated in html/pages/search/fdb.inc.php file, there is an SQL query there that fetches all the device ids and hostnames, then sets the results to be shown by default. Seems like LIMITing the SQL query does not help at all - this query part is completely ignored.

Still looking for the ways out, will update as always…

Today I found there is an AJAX call, which actually produces the table contents (AJAX subquery is also not taken into consideration when deploying the page generation summary). There is a html/includes/table/fdb-search.inc.php file, where we can find the real SQL query behind “FDB table” page.

AJAX query:

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(M.ipv4_address SEPARATOR ', ') AS ipv4_address
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 GROUP BY device_id, port_id, mac_address, vlan, hostname, ifAlias, ifAdminStatus, ifDescr, ifOperStatus, ifInErrors, ifOutErrors ORDER BY C.portCount ASC LIMIT 0,50;

Results on AJAX query: 50 rows in set, 287 warnings (36.52 sec)

Errors look like this:

Warning (Code 1260): Row 947 was cut by GROUP_CONCAT()
Warning (Code 1260): Row 1282 was cut by GROUP_CONCAT()
Warning (Code 1260): Row 1632 was cut by GROUP_CONCAT()

With distinct:

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 GROUP BY device_id, port_id, mac_address, vlan, hostname, ifAlias, ifAdminStatus, ifDescr, ifOperStatus, ifInErrors, ifOutErrors ORDER BY C.portCount ASC LIMIT 0,50;

Results: 50 rows in set (36.57 sec). Note there are no warnings produced.

Explain on the AJAX query:

+------+-------------+------------+--------+------------------------+-------------------------+---------+------------------------+--------+---------------------------------+
| id   | select_type | table      | type   | possible_keys          | key                     | key_len | ref                    | rows   | Extra                           |
+------+-------------+------------+--------+------------------------+-------------------------+---------+------------------------+--------+---------------------------------+
|    1 | PRIMARY     | F          | ALL    | NULL                   | NULL                    | NULL    | NULL                   | 123922 | Using temporary; Using filesort |
|    1 | PRIMARY     | D          | eq_ref | PRIMARY                | PRIMARY                 | 4       | librenms.F.device_id   |      1 |                                 |
|    1 | PRIMARY     | P          | eq_ref | PRIMARY,device_ifIndex | PRIMARY                 | 4       | librenms.F.port_id     |      1 | Using where                     |
|    1 | PRIMARY     | V          | eq_ref | PRIMARY,device_id      | PRIMARY                 | 4       | librenms.F.vlan_id     |      1 | Using where                     |
|    1 | PRIMARY     | <derived2> | ref    | key0                   | key0                    | 5       | librenms.F.port_id     |     10 |                                 |
|    1 | PRIMARY     | M          | ref    | mac_address            | mac_address             | 98      | librenms.F.mac_address |      1 |                                 |
|    2 | DERIVED     | ports_fdb  | index  | NULL                   | ports_fdb_port_id_index | 4       | NULL                   | 123922 | Using index                     |
+------+-------------+------------+--------+------------------------+-------------------------+---------+------------------------+--------+---------------------------------+

Full table scan on ports_fdb is possibly the thing that breaks the query the most, but we already have all the indexes created:

+-----------+------------+---------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table     | Non_unique | Key_name                  | Seq_in_index | Column_name  | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-----------+------------+---------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| ports_fdb |          0 | PRIMARY                   |            1 | ports_fdb_id | A         |      123922 |     NULL | NULL   |      | BTREE      |         |               |
| ports_fdb |          1 | mac_address               |            1 | mac_address  | A         |       30980 |     NULL | NULL   |      | BTREE      |         |               |
| ports_fdb |          1 | ports_fdb_port_id_index   |            1 | port_id      | A         |       20653 |     NULL | NULL   |      | BTREE      |         |               |
| ports_fdb |          1 | ports_fdb_device_id_index |            1 | device_id    | A         |         512 |     NULL | NULL   |      | BTREE      |         |               |
| ports_fdb |          1 | ports_fdb_vlan_id_index   |            1 | vlan_id      | A         |         904 |     NULL | NULL   |      | BTREE      |         |               |
+-----------+------------+---------------------------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

I’m ignoring “using temporary” intentionally, since LibreNMS generates tons of those over time and I really doubt it is only from “FDB table” page. With GROUP BY statement this big it is also highly unlikely DB will be fine without temporary table.
OK, filesort: https://www.percona.com/blog/2009/03/05/what-does-using-filesort-mean-in-mysql/
Since filesort means trouble sorting results, let’s try to increase sort_buffer_size. We run at default 2MB value of course (sad smiley), so tried to increase it drastically:
64MB - 50 rows in set (26.06 sec)
128MB - 50 rows in set (15.71 sec)
256MB - 50 rows in set (16.07 sec)
OK, let’s try in PROD environment with 256MB sort_buffer_size and without DISTINCT: 50 rows in set, 3142 warnings (2 min 58.88 sec). Load average spike up too 722. 24k sort operations. Kind of better, but still unusable.

After this test, it became clear things are not as easy as variable adjusting. So let’s have a deeper look at the query itself. If we remove hostname, ifAlias, ifAdminStatus, ifDescr, ifOperStatus, ifInErrors, ifOutErrors from GROUP BY section, the results will be as follows:

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address 
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 
GROUP BY device_id, port_id, mac_address, vlan ORDER BY C.portCount ASC LIMIT 0,50;

50 rows in set (3.30 sec)

Astonishing! Note the amount of results did not change - if we compare the original query and this one without limits, there will be same amount of results.
In our production LibreNMS with 389780-rows large ports_fdb table this query results are: 50 rows in set (53.64 sec). There is a load average spike up to 160 and still produces 23k sort operations in MySQL, but hey, this is three times better than previously.

@laf, @murrant, say please, what would be the possible results if I submit PR with changes done to html/includes/table/fdb-search.inc.php as described? What could I have missed with this change, since visually (in the web interface) it does not look like something changed.

P.S. Due to my findings I have renamed the topic.

Without looking at the code it’s hard to say. Submit the PR and we can discuss there :slight_smile:

PR has been done: https://github.com/librenms/librenms/pull/9338

Unfortunately, I cannot find any more options to optimize this page or DB for this specific query. As read in Percona blogs mainly, sort_buffer_size should not exceed 2MB in majority of cases, so I set it to 2MB currently. It does not impact the speed of the query anymore, the GROUP BY directive adjusting plays the main role.

Here is a very nice article on the sorting behind “Using temporary; Using filesort”: http://s.petrunia.net/blog/?p=24
I’m even afraid I cannot do anything here anymore. The time has come to make /tmp a RAM disk, since it is used by both DB and RRD.

And some more testing results below with different options and sorting counters.

First iteration.

+---------------------------+-----------+
| Variable_name             | Value     |
+---------------------------+-----------+
| Sort_merge_passes         | 1881      |
| Sort_priority_queue_sorts | 135       |
| Sort_range                | 573429    |
| Sort_rows                 | 118170684 |
| Sort_scan                 | 1174112   |
+---------------------------+-----------+

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address 
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 
GROUP BY device_id, port_id, mac_address, vlan ORDER BY C.portCount ASC;

124412 rows in set (3.54 sec)

+---------------------------+-----------+
| Variable_name             | Value     |
+---------------------------+-----------+
| Sort_merge_passes         | 1881      |
| Sort_priority_queue_sorts | 135       |
| Sort_range                | 573429    |
| Sort_rows                 | 118594420 |
| Sort_scan                 | 1174116   |
+---------------------------+-----------+
sort rows delta = 423736

Second iteration.

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address 
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 
GROUP BY device_id, port_id, mac_address, vlan;

124412 rows in set (3.09 sec)

+---------------------------+-----------+
| Variable_name             | Value     |
+---------------------------+-----------+
| Sort_merge_passes         | 1881      |
| Sort_priority_queue_sorts | 135       |
| Sort_range                | 573429    |
| Sort_rows                 | 118893744 |
| Sort_scan                 | 1174117   |
+---------------------------+-----------+
sort rows delta = 299324

Third iteration

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address 
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 
GROUP BY device_id, port_id, mac_address, vlan ORDER BY C.portCount ASC LIMIT 0,50;

50 rows in set (3.26 sec)

+---------------------------+-----------+
| Variable_name             | Value     |
+---------------------------+-----------+
| Sort_merge_passes         | 1881      |
| Sort_priority_queue_sorts | 136       |
| Sort_range                | 573429    |
| Sort_rows                 | 119193118 |
| Sort_scan                 | 1174119   |
+---------------------------+-----------+
sort rows delta = 299374

Fourth iteration

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address 
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 
GROUP BY device_id, port_id, mac_address, vlan LIMIT 0,50;

50 rows in set (2.57 sec)

+---------------------------+-----------+
| Variable_name             | Value     |
+---------------------------+-----------+
| Sort_merge_passes         | 1881      |
| Sort_priority_queue_sorts | 136       |
| Sort_range                | 573429    |
| Sort_rows                 | 119492442 |
| Sort_scan                 | 1174120   |
+---------------------------+-----------+
sort rows delta = 299324

We have created a 2GB RAM disk for starters, but even with limited SQL query it consumes all FS space and drops the query. After expanding it to 10GB, we now see that altered SQL query produces 2.3GB large temporary table in our production environment. The results are:

50 rows in set (25.70 sec) Finally!..

Out of curiosity, I have launched the original SQL query with RAM disk - it took all 10GB of space and dropped the query.

Interesting, but won’t that cause an error when ONLY_FULL_GROUP_BY is enabled?

How many IPs per Mac do you have max?

Perhaps the ip grouping could be moved to php, but the table paging would be funky then…

Yes, with ONLY_FULL_GROUP_BY sql_mode the query produces: ERROR 1055 (42000): 'librenms.P.ifInErrors' isn't in GROUP BY.

I guess max count of IPs per MAC can be retrieved like this:
SELECT COUNT(mac_address) AS macs FROM ipv4_mac GROUP BY mac_address HAVING macs > 1;
In TEST environment: top results are: 516, 37, 31. The rest are around 2-10 mostly. 178 total results.
In PROD environment: 2845 total results. top results are.

|  113 |
|  122 |
|  138 |
|  148 |
|  161 |
|  168 |
|  178 |
|  181 |
|  186 |
|  236 |
|  551 |
|  552 |
|  660 |
+------+

660? Does that seem correct to you?

It would be interested to know the kind of device that comes with such a big amount of IP per MAC but I could imagine that in a private vlan environnement for instance, or may with Proxy-Arp feature.

Yeah proxy arp on a transport link could do it. We absolutely avoid that in our network, huge possibility for breakage.

I do avoid it as well but I could imagine some situations where this is necessary (let say to service provider environnements etc etc).

@angryp: any environnements details you can share ?

After inspecting the IP/MAC addresses with network team, we found possible root cause of this amount of entries. All examples given below are for the 660 IPs per MAC entry.

Total entries per MAC - 660:
SELECT COUNT(*) FROM ipv4_mac WHERE mac_address="ffffffffffff" ORDER BY port_id;
Total entries per MAC with most popular IP address - 526:
SELECT COUNT(*) FROM ipv4_mac WHERE ipv4_address="10.10.10.10" ORDER BY port_id;
Total unique entries per MAC with most popular IP address - 64:
SELECT DISTINCT * FROM ipv4_mac WHERE ipv4_address="10.10.10.10" ORDER BY port_id;

So basically, in a lot of those entries there is duplicate data, since there is no uniqueness criteria in the table:

+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| port_id      | int(11)      | NO   | MUL | NULL    |       |
| device_id    | int(11)      | YES  |     | NULL    |       |
| mac_address  | varchar(32)  | NO   | MUL | NULL    |       |
| ipv4_address | varchar(32)  | NO   |     | NULL    |       |
| context_name | varchar(128) | NO   |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+

So we made a copy of PROD DB and removed the duplicates:

ALTER TABLE ipv4_mac ADD id INT PRIMARY KEY AUTO_INCREMENT;

DELETE entry1 FROM ipv4_mac entry1 
INNER JOIN ipv4_mac entry2 
WHERE entry1.id > entry2.id AND entry1.port_id=entry2.port_id AND entry1.device_id=entry2.device_id AND entry1.mac_address=entry2.mac_address AND entry1.ipv4_address=entry2.ipv4_address AND entry1.context_name=entry2.context_name;

Total entries per MAC - 96:
SELECT COUNT(*) FROM ipv4_mac WHERE mac_address="ffffffffffff" ORDER BY port_id;
Total entries per MAC with most popular IP address - 64:
SELECT COUNT(*) FROM ipv4_mac WHERE ipv4_address="10.10.10.10" ORDER BY port_id;
Total unique entries per MAC with most popular IP address - 64:
SELECT DISTINCT * FROM ipv4_mac WHERE ipv4_address="10.10.10.10" ORDER BY port_id;

And now top results for IPv4 per MAC overall look like this:
SELECT COUNT(mac_address) AS macs FROM ipv4_mac GROUP BY mac_address HAVING macs > 1 ORDER BY macs DESC LIMIT 10;

+------+
| macs |
+------+
|  547 |
|  401 |
|  230 |
|  180 |
|  177 |
|  171 |
|  148 |
|  117 |
|  115 |
|  114 |
+------+

Hm… I guess this does not solve all the problems. OK, here is how the network team explains this:

One of the specifics to mention is that one device can have over X VLANS and a separate unique /30 subnet in each that is used for VRRP signalling. That already makes at least 2 IP addresses per VLAN. If we add, like, 4 devices that have an IP in each VLAN - their count increases rapidly.

For testing purposes, here are some results for the FDB table SQL query, when duplicate entries are absent:

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(M.ipv4_address SEPARATOR ', ') AS ipv4_address
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 GROUP BY device_id, port_id, mac_address, vlan, hostname, ifAlias, ifAdminStatus, ifDescr, ifOperStatus, ifInErrors, ifOutErrors ORDER BY C.portCount ASC LIMIT 0,50;

Before - ERROR 3 (HY000): Error writing file ‘/tmp/MYf3v9dc’ (Errcode: 28 - No space left on device) (10GB all used up).
After - ERROR 3 (HY000): Error writing file ‘/tmp/MYe0NDnX’ (Errcode: 28 - No space left on device) (10GB all used up).

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 GROUP BY device_id, port_id, mac_address, vlan, hostname, ifAlias, ifAdminStatus, ifDescr, ifOperStatus, ifInErrors, ifOutErrors ORDER BY C.portCount ASC LIMIT 0,50;

Before - ERROR 3 (HY000): Error writing file ‘/tmp/MYUDeZ2a’ (Errcode: 28 - No space left on device) (10GB all used up).
After - ERROR 3 (HY000): Error writing file ‘/tmp/MYfJYXB9’ (Errcode: 28 - No space left on device) (10GB all used up).

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(M.ipv4_address SEPARATOR ', ') AS ipv4_address 
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 
GROUP BY device_id, port_id, mac_address, vlan ORDER BY C.portCount ASC LIMIT 0,50;

Before - 50 rows in set, 3286 warnings (24.28 sec)
After - 50 rows in set, 3247 warnings (22.05 sec)

SELECT F.port_id AS port_id, F.device_id, ifInErrors, ifOutErrors, ifOperStatus, ifAdminStatus, ifAlias, ifDescr, mac_address, V.vlan_vlan AS vlan, hostname, hostname AS device, group_concat(distinct M.ipv4_address SEPARATOR ', ') AS ipv4_address 
FROM ports_fdb AS F 
LEFT JOIN devices AS D USING(device_id) 
LEFT JOIN ports AS P USING(port_id, device_id) 
LEFT JOIN vlans AS V USING(vlan_id, device_id) 
LEFT JOIN (SELECT port_id, COUNT(*) portCount FROM ports_fdb GROUP BY port_id) AS C ON C.port_id=F.port_id 
LEFT JOIN ipv4_mac AS M USING(mac_address) 
WHERE 1 
GROUP BY device_id, port_id, mac_address, vlan ORDER BY C.portCount ASC LIMIT 0,50;

Before - 50 rows in set (24.43 sec)
After - 50 rows in set (22.70 sec)