For server and service monitoring, we have long been successfully using a combined solution based on Nagios and Munin. However, this combination has its drawbacks, so we, like many others, are actively utilizing . In this article, we will discuss how to solve performance issues with minimal effort as the number of metrics collected increases and the volume of the MySQL database grows.
Problems of Using MySQL with Zabbix
As long as the database was small and the number of metrics stored in it was limited, everything was fine. The built-in housekeeper process, which runs on the Zabbix Server, effectively removed outdated entries from the database, preventing it from growing. However, once the number of metrics collected increased and the database volume reached a certain size, things took a turn for the worse. The housekeeper could no longer delete data within the allotted time, leaving old data in the database. During the housekeeper's operation, there was increased load on the Zabbix Server, which could persist for a long time. It became clear that the situation needed to be addressed somehow.
This is a well-known problem; practically everyone who has worked with large volumes of monitoring on Zabbix has encountered the same issue. There were several solutions, such as switching from MySQL to PostgreSQL or even Elasticsearch, but the simplest and most tried-and-true solution was to move to partitioning the tables storing metric data in the MySQL database. We decided to pursue this path.
Transitioning from Regular MySQL Tables to Partitioned Ones
Zabbix is well documented, and the tables where it stores metrics are known. These tables include: history, which stores float values, history_str, which stores short string values, history_text, which stores long text values, and history_uint, which stores integer values. There is also a table trends, which stores change dynamics, but we decided not to touch it since its size is small, and we will come back to it later.
Overall, it was clear which tables needed to be processed. We decided to create partitions for each week, except for the last one, based on the days of the month, i.e., four partitions per month: from the 1st to the 7th, from the 8th to the 14th, from the 15th to the 21st, and from the 22nd to the 1st (of the next month). The challenge was to transform the necessary tables into partitioned ones 'on the fly' without interrupting the operation of the Zabbix Server and metric collection.
Strangely enough, the structure of the table data itself helped us with this. For example, the table history has the following structure:
`itemid` bigint(20) unsigned NOT NULL,
`clock` int(11) NOT NULL DEFAULT '0',
`value` double(16,4) NOT NULL DEFAULT '0.0000',
`ns` int(11) NOT NULL DEFAULT '0',with
KEY `history_1` (`itemid`,`clock`) As we can see, each metric is eventually recorded in the table with two very important and convenient fields for us: itemid and clock. Therefore, we can easily create a temporary table, for example, named history_tmp, set it up for partitioning, and then transfer all the data from the table history, and then rename the table history downward API support (simultaneously with this in history_old, and the table history_tmp downward API support (simultaneously with this in history, after which we can add the data that we haven't yet transferred from history_old downward API support (simultaneously with this in history and delete history_old. This can be done completely safely, as we will not lose anything because the aforementioned fields itemid and clock ensure the binding of a specific metric to a specific time, not to some ordinal number.
The transition procedure itself
Attention! It is highly recommended to make a complete backup of the database before starting any actions. We are all human and can make mistakes in the command input, which may lead to data loss. Yes, a backup may not ensure maximum currency, but it is better to have one than none.
So, we do not turn off or stop anything. The main thing is that there is enough free disk space on the MySQL server, i.e., for each of the tables listed above history, history_text, history_str, history_uint, there must be enough space to create a table with the suffix '_tmp', considering that it will be of the same size as the original table.
We will not describe everything several times for each of the above tables and will consider everything using only one example — the table history.
So, let's create an empty table history_tmp based on the structure of the table history.
CREATE TABLE `history_tmp` LIKE `history`;Creating the necessary partitions. For example, we'll do this for a month. Each partition is created based on a partitioning rule derived from the value of the field clock, which we compare with the timestamp:
ALTER TABLE `history_tmp` PARTITION BY RANGE( clock ) (
PARTITION p20190201 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-01 00:00:00")),
PARTITION p20190207 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-07 00:00:00")),
PARTITION p20190214 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-14 00:00:00")),
PARTITION p20190221 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-21 00:00:00")),
PARTITION p20190301 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-01 00:00:00"))
); This operator adds partitioning to the table we created history_tmp. Let’s clarify that the data where the field value clock is less than "2019-02-01 00:00:00" will go into partition p20190201, then the data where the field value clock is greater than "2019-02-01 00:00:00" but less than "2019-02-07 00:00:00" will go into partition p20190207 and so on.
Important note: What will happen if we have data in the partitioned table where the field value clock is greater than or equal to "2019-03-01 00:00:00"? Since there is no suitable partition for this data, it will not enter the table and will be lost. Therefore, you need to remember to create additional partitions in a timely manner to avoid such data loss (as discussed below).
So, the temporary table is prepared. Let's load the data. The process may take a considerable amount of time, but fortunately, it does not block any other queries, so you just need to be patient:
INSERT IGNORE INTO `history_tmp` SELECT * FROM history;The IGNORE keyword during the initial loading is not mandatory since there is no data in the table yet, however, you will need it when loading data later. Additionally, it may prove useful if you had to interrupt the process of loading data and restart it.
So, after some time (possibly even several hours), the first data load was completed. As you understand, now the table history_tmp contains not all the data from the table history, but only those that were present at the moment the query began executing. Here, you have a choice: either we make another pass (if the loading process took a long time), or we proceed directly to renaming the tables mentioned above. Let’s first discuss the second pass. First, we need to determine the time of the last inserted record in history_tmp:
SELECT max(clock) FROM history_tmp;Suppose you received: 1551045645Now we use the obtained value in the second data loading pass:
INSERT IGNORE INTO `history_tmp` SELECT * FROM history WHERE clock>=1551045645;This pass should finish significantly faster. But if the first pass took hours, and the second one also takes a long time, it may be reasonable to perform a third pass, which operates exactly like the second.
In the end, we again execute the operation to get the timestamp of the last record insertion into history_tmp, by executing:
SELECT max(clock) FROM history_tmp;Suppose you have obtained 1551085645. Save this value - we will need it for the additional upload.
And now, when the initial data loading into history_tmp is finished, we proceed to rename the tables:
BEGIN;
RENAME TABLE history TO history_old;
RENAME TABLE history_tmp TO history;
COMMIT; We structured this block as a single transaction to avoid the moment of inserting data into a non-existent table, since after the first RENAME and before the second RENAME, the table history will not exist. But even if some data comes to the table between the RENAME operations, and the table itself does not exist yet (due to the renaming), we will get a small number of insertion errors, which can be ignored (since we have monitoring, not banking). history Now we have a new table
with partitioning, but it lacks the data that was obtained during the last data insertion pass into the table history . But we have this data in the table history_tmpand we will fill it in from there now. For this, we will need the previously saved value 1551085645. Why did we save this value and not use the maximum upload time already from the current table? history_old INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock>=1551045645; history? Потому что новые данные уже в неё поступают и мы получим неверное время. Итак, дозаливаем данные:
After this operation finishes, we will have all the data in the new, partitioned table that were in the old one, plus those that have already arrived after the table renaming. The table history is no longer needed. You can delete it immediately, or make a backup of it before deletion (if you’re paranoid). history_old The entire process described above needs to be repeated for the tables
What needs to be adjusted in the Zabbix Server settings history_str, history_text and history_uint.
What needs to be corrected in the Zabbix Server settings?
From now on, the management of the database regarding historical data is on us. This means that Zabbix will no longer need to delete old data – we will handle it ourselves. To prevent the Zabbix Server from attempting to clean data itself, you need to access the Zabbix web interface, select 'Administration' from the menu, then 'General' from the submenu, and then select 'History Cleanup' from the dropdown on the right. On the page that appears, uncheck all boxes for the 'History' group and click the 'Update' button. This will prevent unnecessary cleanup of tables. history* through housekeeper.
Note on this same page the 'Change Dynamics' group. This is precisely the table trends, which we promised to return to. If this table has also become too large and requires partitioning, uncheck the boxes in this group as well, and then process this table just as it was done for the tables. history*.
Further database maintenance
As mentioned earlier, to ensure proper operation on partitioned tables, partitions must be created in a timely manner. This can be done as follows:
ALTER TABLE `history` ADD PARTITION (PARTITION p20190307 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-07 00:00:00")));Furthermore, since we have created partitioned tables and prohibited the Zabbix Server from cleaning them, the removal of old data is now our responsibility. Fortunately, this poses no problems at all. It can be done simply by deleting the partition whose data we no longer need.
For example:
ALTER TABLE history DROP PARTITION p20190201;Unlike DELETE FROM operators with a specified date range, DROP PARTITION executes in seconds, does not overload server and works just as smoothly in cases of MySQL replication.
Conclusion
The described solution has stood the test of time. Data volume is increasing, but no noticeable performance degradation has been observed.
Source: habr.com
