В базе данных проекта хранились разные интересные данные. Например:
mysql> show create table movie_titles\G
*************************** 1. row ***************************
Table: movie_titles
Create Table: CREATE TABLE `movie_titles` (
`movie_hash` bigint(20) unsigned NOT NULL,
`title` varchar(255) NOT NULL,
`count` int(10) unsigned NOT NULL,
`min_year` int(10) unsigned NOT NULL,
`max_year` int(10) unsigned NOT NULL,
`min_budget` int(10) unsigned NOT NULL,
`max_budget` int(10) unsigned NOT NULL,
`title_lang_id` varchar(2) NOT NULL DEFAULT '',
PRIMARY KEY (`movie_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> select movie_hash, title from movie_titles order by rand() limit 10;
+----------------------+-----------------------------------+
| movie_hash | title |
+----------------------+-----------------------------------+
| 14466895222078503050 | Короткие встречи |
| 15971437470478867358 | The Intouchables |
| 12235011833542755999 | Карнавальная ночь |
| 6981806602143484496 | Ben-Hur |
| 5919830798737205613 | Зеркало |
| 9358350811894903428 | Ikiru |
| 15397825164883494235 | City Lights |
| 1081902961608193125 | Я шагаю по Москве |
| 6154127428560052859 | The Kid |
| 16809407873175270375 | Безымянная звезда |
+----------------------+-----------------------------------+
10 rows in set (0.01 sec)
mysql> select movie_hash % 4, count(*) from movie_titles group by 1;
+----------------+----------+
| movie_hash % 4 | count(*) |
+----------------+----------+
| 0 | 1217109 |
| 1 | 1217068 |
| 2 | 1217333 |
| 3 | 1216358 |
+----------------+----------+
4 rows in set (2.78 sec)
Программисту понадобилось произвести над каждой записью в этой таблице некоторое довольно тяжелое вычисление. Для увеличения скорости программист решил запустить обработку в несколько процессов:
#!/usr/bin/perl
use strict;
use warnings;
use DBI;
use Parallel::ForkManager;
use Log::Log4perl qw(:easy);
use utf8;
use open ':std' => ':utf8';
Log::Log4perl->easy_init(
{
level => $INFO,
file => ">>process.log",
layout => "[%P] %c %d: %m%n",
}
);
INFO("START");
my $WORKERS = 4;
my $pm = Parallel::ForkManager->new($WORKERS);
for my $num (0 .. $WORKERS - 1) {
my $pid = $pm->start and next;
process_part($num, $WORKERS);
$pm->finish;
}
$pm->wait_all_children;
INFO("FINISH");
exit(0);
sub process_part
{
my ($num, $total) = @_;
INFO("$num/$total - start");
my $dbh = get_dbh();
my $data_to_process = $dbh->selectall_arrayref(
"select movie_hash, title from movie_titles
where movie_hash % ? = ?",
{},
$total, $num
);
INFO("to process: ".scalar(@$data_to_process));
for my $record (@$data_to_process) {
do_some_heavy_processing($record);
}
INFO("$num/$total - finish");
}
sub get_dbh
{
# ...
return $dbh;
}
sub do_some_heavy_processing
{
# ...
}
И все бы хорошо, но в логах видно кое-что странное с распределением работы по воркерам. Что же случилось?
Лог выглядел примерно так:
[18894] 2014/01/01 20:07:25: START
[18907] 2014/01/01 20:07:25: 0/4 - start
[18908] 2014/01/01 20:07:25: 1/4 - start
[18909] 2014/01/01 20:07:25: 2/4 - start
[18910] 2014/01/01 20:07:25: 3/4 - start
[18908] 2014/01/01 20:07:28: to process: 598
[18910] 2014/01/01 20:07:28: to process: 575
[18909] 2014/01/01 20:07:28: to process: 1208
[18910] 2014/01/01 20:07:30: 3/4 - finish
[18908] 2014/01/01 20:07:30: 1/4 - finish
[18909] 2014/01/01 20:07:31: 2/4 - finish
[18907] 2014/01/01 20:08:34: to process: 4865487
[18907] 2014/01/01 21:13:05: 0/4 - finish
[18894] 2014/01/01 21:13:08: FINISH
То есть почти вся работа досталась нулевому воркеру, а остальные бездельничали.
Программист попробовал поменять количество воркеров. При 8 рабочих процессах почти все записи снова достались нулевому, при 6 – нулевому, второму и четвертому, а между 5 воркерами записи распределились равномерно.
Неравномерное распределение записей по рабочим процессам происходит из-за сочетания двух обстоятельств:
При подстановке в запрос через плейсхолдеры (where movie_hash % ? = ?
) переменные заключаются в кавычки (получается where movie_hash % '4' = '3'
).
При выполнении операций над целыми числами, заключенными в кавычки, mysql приводит их к вещественным, и операции производит как с вещественными. А точность вещественных чисел ограничена, и на больших целых ее перестает хватать – младшие биты больших целых, превращенных во float’ы, распределены неравномерно.
Сравните:
mysql> select 17330750813197098651 % 4;
+--------------------------+
| 17330750813197098651 % 4 |
+--------------------------+
| 3 |
+--------------------------+
mysql> select 17330750813197098651 % '4';
+----------------------------+
| 17330750813197098651 % '4' |
+----------------------------+
| 0 |
+----------------------------+
mysql> select 17330750813197098651 % 4e0;
+----------------------------+
| 17330750813197098651 % 4e0 |
+----------------------------+
| 0 |
+----------------------------+
mysql> select 17330750813197098651 % cast('4' as unsigned);
+----------------------------------------------+
| 17330750813197098651 % cast('4' as unsigned) |
+----------------------------------------------+
| 3 |
+----------------------------------------------+
Цитата из документации mysql (описывается приведение при сравнении, но при других операциях аналогично):
The following rules describe how conversion occurs for comparison operations:
If one or both arguments are NULL … – не наш случай
If both arguments in a comparison operation are strings … – не наш случай
If both arguments are integers, they are compared as integers. – не наш случай, у нас второй аргумент – строка
Hexadecimal values … – не наш случай
If one of the arguments is a TIMESTAMP or DATETIME – не наш случай
If one of the arguments is a decimal value – не наш случай
In all other cases, the arguments are compared as floating-point (real) numbers. – мы попадаем сюда, что подтверждается последним запросом с делением на 6e0.
Чтобы все же обеспечить равномерное распределение работы, программист мог бы воспользоваться
либо непосредственной подстановкой переменных в текст запроса (where movie_hash % $total = $num
),
либо воспользоваться конструкцией cast(? as unsigned)
.