Подписка

Шардинг в MySQL

Шардинг - хранение разбитых по некоторому принципу данных на нескольких серверах. Например, хранение большой таблицы истории покупок в интернет магазине на 3х серверах, где выбор сервера зависит от первой цифры идентификатора пользователя.

Обычно этот вид масштабирования используется в последнюю очередь, когда количество данных растет и партиционирование (больше тут) уже не помогает.

Так как MySQL не поддерживает автоматического шардинга, его приходиться делать вручную. Ниже рассмотрим небольшой практический пример реализации.

Для начала опишем тип данных который будем хранить в базе, пусть это будет некоторый заказ сделанный пользователем интернет магазина:

Order
 - id: int
 - name: varchar
 - date: datetime
 - user_id: int
 - sum: float

Таким образом получим класс:

class Order {
     public $id;
     public $name;
     public $date;
     public $user_id;
     public $sum;

     public function __construct($name, $date, $user_id, $sum) {
          $this->name = $name;
          $this->date = $date;
          $this->user_id = $user_id;
          $this->sum = $sum;
     }
}

Для упрощения делаем все поля публичными, по-хорошему они должны быть приватными c геттерами и сеттерами.

Теперь перейдем к описанию класса, который будет заниматься сохранением объектов Order в базе данных:


class OrderStorage {
     public function insert(Order $order) {
          //добавить запись и вернуть объект с id
     }
     public function update() {
          //обновить объект
     }
     public function delete() {
          //удалить объект
     }
}

Если бы нашей целью НЕ был шардинг мы бы могли на этом остановиться и использовать эти 2 класса для работы с данными:

$storage = new OrderStorage();

$someOrder = new Order('test order1', date('Ymd'), 1, 100);
$storage->insert($someOrder);

Но нам еще нужно прикрутить выбор сервера в зависимости от идентификатора пользователя. Для этого создадим синглтон класс ShardingStragegy, который будет выдавать соединение к базе данных в зависимости от значений свойств объекта Order.

class ShardingStragegy {
    protected static $instance = null;
    protected $server1;
    protected $server2;

    protected function __construct() {
         $this->server1 = mysql_connect('server1', 'user1', 'pass1');
         $this->server2 = mysql_connect('server2', 'user2', 'pass2');
    }

    public static function getInstance() {
        if (static::$instance == null) {
            static::$instance = new self();
        }

        return static::$instance;
    }

    public function getConnection(Order $order) {
         $server = $this->server1;
         if ($order->user_id % 2 == 0) $server = $this->server2; 
         return $server;
    }
}

При создании инстанса подключаемся к серверам, а при вызове метода getConnection, в зависимости от парности идентификатора пользователя, возвращаем нужное соединение.

Осталось добавить в OrderStorage метод который будет выполнять запрос на правильном сервере и заиспользовать его в insert/update/delete:


class OrderStorage {
     protected function runQuery($query, Order $order) {
          mysql_query($query, ShardingStragegy::getInstance()->getConnection($order));
     }

     public function insert(Order $order) {
          //добавить запись и вернуть объект с id
          $this->runQuery("insert lalalal", $order);
          return mysql_insert_id();
     }

     public function update(Order $order) {
          //обновить объект
          $this->runQuery("update lalala", $order);
     }

     public function delete(Order $order) {
          //удалить объект
          $this->runQuery("delete lalalal", $order);
     }
}

Вот и все, данные заказов будут распределяться в зависимости от идентификатора пользователя.

Если же мы захотим шардить еще например и пользователей, класс ShardingStragegy можно запросто расширить, не усложняя структуру:

abstract class ObjectShardingStrategy {
     abstract public function getConnection($object, $server1, $server2);
}

class OrderShardingStrategy extends ObjectShardingStrategy {
     public function getConnection($order, $server1, $server2) {
          $server = $this->server1;
          if ($order->user_id % 2 == 0) $server = $this->server2; 
          return $server;
     }
}

class UserShardingStrategy extends ObjectShardingStrategy {
     public function getConnection($user, $server1, $server2) {
          $server = $this->server1;
          if ($user->id % 2 == 0) $server = $this->server2; 
          return $server;
     }
}

class ShardingStragegy {
    protected static $instance = null;
    protected $server1;
    protected $server2;

    protected function __construct() {
         $this->server1 = mysql_connect('server1', 'user1', 'pass1');
         $this->server2 = mysql_connect('server2', 'user2', 'pass2');
    }

    public static function getInstance() {
        if (static::$instance == null) {
            static::$instance = new self();
        }
        return static::$instance;
    }

    public function getConnection($object) {
         $strategy = null;

         if ($object instanceof Order) $strategy = new OrderShardingStrategy();
         elseif ($object instanceof User) $strategy = new UserShardingStrategy();

         if ($strategy == null) throw new Exception('No strategy found');

         return $strategy->getConnection($object, $server1, $server2);
    }
}

Логика выбора сервера перенесена в отдельные для каждого класса стратегии OrderShardingStrategy и UserShardingStrategy, а ShardingStragegy просто выбирает необходимую стратегию на основе типа объекта.

Конечно жаль что MySQL не поддерживает шардинг из коробки, было бы намного проще прописать несколько правил в конфигурации и не усложнять архитектуру уровня приложения.

В тему:

Если пост понравился - нажмите на +1 - мне будет приятно.

@kkooler

@kkooler

Занимаюсь разработкой высоконагруженных проектов и распределенных систем на PHP.
В свободное время разрабатываю нано-проекты:

Следить за блогом

RSS канал Twitter