вторник, 16 апреля 2013 г.

Lazy Iterators on Scala

Exercise:

Implement proper (means lazy) iterator for data structure B-Tree.

Code (Scala):

import org.scalatest.FunSuite

class LazyIteratorTest extends FunSuite {

  trait LazyIt[T] {
    def iterator(): Iterator[T]
    protected def lazyIt[T](
      sq: Seq[LazyIt[T]]): Iterator[T] =
      sq match {
        case Seq(hd) => hd.iterator
        case Seq(hd, tl @ _*) =>
          hd.iterator ++ lazyIt(tl)
        case _ => Iterator.empty
      }
  }

  sealed trait BTree[K, V]
    extends LazyIt[(K, V)] {
    type T = (K, V)
    def map[R](f: T => R): Seq[R]
  }

  case class BTLeaf[K, V](vs: Seq[(K, V)])
    extends BTree[K, V] {
    override def toString =
      "leaf(" + shorten(showSeq(vs)) + ")"
    def iterator(): Iterator[T] = {
      println("* iterator() on " + this + " *")
      vs.iterator
    }
    def map[R](f: T => R): Seq[R] = vs.map(f)
  }

  case class BTNode[K, V](ts: Seq[BTree[K, V]])
    extends BTree[K, V] {
    override def toString =
      "node(" + shorten(showSeq(ts)) + ")"
    def iterator(): Iterator[T] = {
      println("* iterator() on " + this + " *")
      lazyIt(ts)
    }
    def map[R](f: T => R): Seq[R] =
      ts.flatMap(_.map(f))
  }

  def showSeq[T](sq: Seq[T]) = 
      sq.map(_.toString).mkString(",")
  def shorten[T](s: String) = 
      if (s.size <= 30) s else s.take(30) + "..."
  def time = System.currentTimeMillis()

  def leaf[K, V](vs: (K, V)*) = BTLeaf[K, V](vs)
  def node[K, V](ts: BTree[K, V]*) = BTNode[K, V](ts)

  def tree = node(
    node(leaf(1 -> "a", 2 -> "b", 3 -> "c"),
      leaf(4 -> "d", 5 -> "e", 6 -> "f"),
      leaf(7 -> "g", 8 -> "h", 9 -> "i")),
    node(leaf(10 -> "j", 11 -> "k", 12 -> "l"),
      leaf(13 -> "m", 14 -> "n", 15 -> "o"),
      leaf(16 -> "p", 17 -> "q", 18 -> "r")),
    node(leaf(19 -> "s", 20 -> "t", 21 -> "u"),
      leaf(22 -> "v", 23 -> "w", 24 -> "x"),
      leaf(25 -> "y", 26 -> "z", 27 -> "!")))

  test("lazy iterator") {
    println("warm up before...")
    tree.map(_.toString)
    println("do simple map")
    val t = time
    println(shorten(showSeq(
      tree.map(_.toString))))
    println("it took time: "
      + (time - t) + " ms")
    val lt = time
    println("get lazy iterator {")
    val it = tree.iterator()
    println("} got lazy iterator")
    println("do lazy take(1) {")
    val t1 = it.take(1)
    println("} lazy taken(1)")
    println("do lazy take(2) {")
    val t2 = it.take(2)
    println("} lazy taken(2)")
    println(t1.toList)
    println(t2.toList)
    println("do lazy foreach {")
    it.foreach { println }
    println("} done lazy foreach")
    println("lazily it took time: "
      + (time - lt) + " ms")
  }
}
Output:
warm up before...
do simple map
(1,a),(2,b),(3,c),(4,d),(5,e),...
it took time: 6 ms
get lazy iterator {
* iterator() on node(node(leaf((1,a),(2,b),(3,c)),l...) *
* iterator() on node(leaf((1,a),(2,b),(3,c)),leaf((...) *
* iterator() on leaf((1,a),(2,b),(3,c)) *
} got lazy iterator
do lazy take(1) {
} lazy taken(1)
do lazy take(2) {
} lazy taken(2)
List((1,a))
List((2,b), (3,c))
do lazy foreach {
* iterator() on leaf((4,d),(5,e),(6,f)) *
(4,d)
(5,e)
(6,f)
* iterator() on leaf((7,g),(8,h),(9,i)) *
(7,g)
(8,h)
(9,i)
* iterator() on node(leaf((10,j),(11,k),(12,l)),lea...) *
* iterator() on leaf((10,j),(11,k),(12,l)) *
(10,j)
(11,k)
(12,l)
* iterator() on leaf((13,m),(14,n),(15,o)) *
(13,m)
(14,n)
(15,o)
* iterator() on leaf((16,p),(17,q),(18,r)) *
(16,p)
(17,q)
(18,r)
* iterator() on node(leaf((19,s),(20,t),(21,u)),lea...) *
* iterator() on leaf((19,s),(20,t),(21,u)) *
(19,s)
(20,t)
(21,u)
* iterator() on leaf((22,v),(23,w),(24,x)) *
(22,v)
(23,w)
(24,x)
* iterator() on leaf((25,y),(26,z),(27,!)) *
(25,y)
(26,z)
(27,!)
} done lazy foreach
lazily it took time: 9 ms

четверг, 11 октября 2012 г.

Debug Java annotation processing

Как известно, Java имеет уже довольно таки развитые механизмы мета-программирования посредством использования аннотаций. Самым интересным в этих механизмах является возможность создания так называемого процессора аннотаций, который будет вызываться компилятором автоматически. Однако в процессе разработки таких процессоров аннотаций возникает одна не очень тривиальная проблема - как осуществить тестирование и отладку самого процессора аннотаций?
Гуглинг выдает некоторые инструкции по этому поводу, но они предписывают использовать довольно сложные средства разработки плагинов Eclipse, создавать плагин, в нем определять точку расширения и осуществлять отладку этого плагина. На мой взгляд эту проблему можно решить значительно проще.

С помощью вот такой очень несложной функции становится очень легко тестировать, и (что самое главное!) дебажить процессоры аннотаций в Java:
package ap.util;

import java.io.File;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

public class CompilerUtil {

 public static boolean compile(File... files) {

  final JavaCompiler compiler = 
      ToolProvider.getSystemJavaCompiler();

  return compiler.getTask(null, null, null, null, null,
      compiler.getStandardFileManager(null, null, null)
         .getJavaFileObjects(files)).call();
 }
}
Чтобы это работало необходимо чтобы в classpath присутствовал tools.jar из JDK. В Eclipse этого присутствия можно добиться следующим образом:
В Project Explorer выбрать мышкой проект, нажать правую кнопку мышки, в появившемся контекстном меню нажать на пункт Build Path а в появившемся при этом подменю - пункт Configure Build Path ... . В открывшемся окне выбрать вкладку Libraries и нажать кнопку (справа список кнопок) Add Variable ... . В новом окне нажать сначала кнопку Configure Variables ... (она снизу под списком переменных) и в новом окне создать новую переменную JDK_HOME в которой указать путь к вашему JDK. Нажать OK, и вернувшись в предыдущее окно, выбрать в списке вновь созданную переменную JDK_HOME и нажать справа сверху от списка переменных кнопку Extend. В появившемся окне выбрать в дереве каталог lib, раскрыть его, и выбрать файл tools.jar. Дальше нажимать OK в каждом из диалогов. Всё, библиотека tools.jar подключена к вашему проекту.

Вот пример самого простого annotation processor:
package ap.sample;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes(value = { "*" })
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class APSample extends AbstractProcessor {

  private Messager messager;

  @Override
  public synchronized void init(
         ProcessingEnvironment processingEnv) {

    messager = processingEnv.getMessager();
  }

  @Override
  public boolean process(
        final Set<? extends TypeElement> annotations, 
        final RoundEnvironment env) 
  {

     for (final TypeElement annotation : annotations) {

       for (final Element annotated : 
               env.getElementsAnnotatedWith(annotation)) {

          messager.printMessage(Kind.WARNING, 
            annotation.getSimpleName(), annotated);

       }
     }
     return true;
  }
}
Чтобы он автоматически вызывался при компиляции, нужно в каталоге META-INF/services создать файл с названием javax.annotation.processing.Processor где просто указать полное имя класса, в данном случае:
ap.sample.APSample
Теперь можно создать например такую тестовую аннотацию:
package ap.sample;

public @interface ApSampleAnnotation {
}
и аннотировать ней тестовый класс:
package ap.sample;

@ApSampleAnnotation
public class ApSampleClass {

}
Теперь можно создать такой unit-test:
package ap.sample;

import static org.junit.Assert.assertTrue;
import static ap.util.CompilerUtil.compile;

public class ApSampleTest {

  @Test
  public void testAp() {
    assertTrue(compile(new File(
      "test/ap/sample/ApSampleClass.java")));
  }
}
, поставить в коде процессора аннотаций, то есть в классе APSample, где нибудь точку прерывания отладчика (debug breakpoint), запустить отладку теста ApSampleTest (Debug as ... / JUnit test) и посмотреть как отладчик остановится там где мы поставили ему точку прерывания. Что интересно, стектрейс при этом будет таким:
APSample.process(Set<TypeElement>, RoundEnvironment) line: 31
JavacProcessingEnvironment.callProcessor(Processor, Set<TypeElement>, RoundEnvironment) line: 627
JavacProcessingEnvironment.discoverAndRunProcs(Context, Set<TypeElement>, List<ClassSymbol>, List<PackageSymbol>) line: 556
JavacProcessingEnvironment.doProcessing(Context, List<JCCompilationUnit>, List<ClassSymbol>, Iterable<PackageSymbol>) line: 701
JavaCompiler.processAnnotations(List<JCCompilationUnit>, List<String>) line: 987
JavaCompiler.compile(List<JavaFileObject>, List<String>,Iterable<Processor>) line: 727
Main.compile(String[], Context, List<JavaFileObject>, Iterable<Processor>) line: 353
JavacTaskImpl.call() line: 115
CompilerUtil.compile(File...) line: 14
ApSampleTest.testAp() line: 16
...
А так же мы получим от компилятора сообщение о том что процесс компиляции был прерван изза warning'a который сгенерировал наш процессор, что и докажет нам что всё честно и мы находимся прямо посреди процесса компиляции java-класса.

Затем полученный процессор аннотаций, упакованный в jar, вместе с файлом META-INF/services/javax.annotation.processing.Processor внутри (в котором он прозрачно подключается к процессу компиляции), можно очень удобно использовать в Eclipse через его механизмы поддержки таких процессоров. Подключается процессор аннотаций к проекту в диалоге Properties проекта в пункте Java Compiler/Annotation Processing/Factory Path, там нужно просто нажать Add Jar (или Add Variable, это по сути тоже самое, разница только в том как будет определен путь к файлу) и там выбрать jar с процессором. После этого проект будет автоматически использовать процессор аннотаций прямо в среде разработки.

Подробнее по ссылкам:
  1. http://jcp.org/en/jsr/detail?id=269
  2. http://docs.oracle.com/javase/6/docs/api/javax/annotation/processing/package-summary.html
  3. http://docs.oracle.com/javase/1.5.0/docs/guide/apt/GettingStarted.html
  4. http://docs.oracle.com/javase/tutorial/java/javaOO/annotations.html
  5. http://www.eclipse.org/jdt/apt/introToAPT.html

понедельник, 11 октября 2010 г.

Suspend в Linux на ноутбуке Asus K52F

После установки Linux (любого - это было замечено и на Ubunta и на Mandriva) на ноутбук Asus K52F (и не только K52F, для Asus'ов вообще это характерно) возникает одна проблема - не работает "режим ожидания" (suspend). Симптомы: система пытается входить в режим ожидания, но в результате глухо зависает (не работают даже магические кнопки "Alt+SysRq + B", это говорит о серьезном сбое ядра). К счастью, исправить эту проблему очень легко. Нужно создать текстовый файл (название файла может быть любым) в каталоге

/etc/pm/config.d

и в этом файле написать следующую строку

SUSPEND_MODULES="sdhci_pci ehci_hcd"

(одной строкой, перенос это издержки форматирования в блоге).

Сразу после этого suspend должен начать работать.

пятница, 10 сентября 2010 г.

Эквалайзер звука в Mandriva Linux 2010

Озаботился я отсутствием эквалайзера в плейере Rhytmbox да и в PulseAudio в целом. Я слышал о проекте PulseAudio-Equalizer, но вся проблема в том, что в репозиториях Mandriva этого пакета еще нет. Так же не удалось нигде обнаружить RPM пакетов для этой полезной утилиты. Однако обнаружилась инструкция (на немецком языке, но гугл помог перевести) о том как можно установить этот пакет из репозитория для Ubunta.

Последовательность действий примерно следующая:

1. Скачать пакет по ссылке:

https://launchpad.net/~psyke83/+archive/ppa/+files/pulseaudio-equalizer_2.7_all.deb

2. Выполнить команду

alien -r pulseaudio-equalizer_2.7_all.deb

(для этого может понадобиться установить пакет alien из репозитория Mandriva)

3. Установить полученный в результате пакет .rpm, например командой (под root-ом)

urpmi pulseaudio-equalizer-2.7-2.noarch.rpm


В итоге в Gnome появится пункт "PulseAudio Equalizer" в разделе меню "Аудио и видео".

Update:

Но для того чтобы это действительно работало еще нужно установить пакет swh-plugins.

воскресенье, 5 сентября 2010 г.

Динамический шардинг

Продолжение развития идеи из моего предыдущего поста о динамическом перераспределении записей при шардинге.

Как заметил AnatoLoy, в предложенном мной решении есть один серьезный недостаток - страдает балансировка нагрузки. Однако, мне кажется, что решить эту проблему не так уж и сложно :) Нужно вместо возвращения номера сервера возвращать параметры функции (а именно - количество серверов).

Допустим у нас M серверов и мы распределяем записи по серверам используя функцию

m = n % M

, где m это номер сервера на котором хранится запись, n это целочисленный идентификатор записи (ID) , а % это операция "остаток от целочисленного деления".

То есть говоря проще, для каждой записи номер сервера мы узнаем вычисляя остаток от деления номера записи (точнее идентификатора записи) на количество серверов.

Теперь допустим нам необходимо добавить новый сервер (вообще говоря мы можем добавить их сразу несколько, но для простоты пока возьмем один). Как сделать чтобы процесс перераспределения уже существующих данных между серверами мог происходит параллельно основному обслуживанию, не останавливая основное обслуживание?

Мы запоминаем общее количество существующих записей на момент начала процесса перераспределения (назовем это количество K). Создаем специальную таблицу, в которой запоминаем это число К (или каким нибудь иным надежным образом запоминаем его в системе). Далее мы осуществляем обслуживание следующим образом - мы сразу начинаем работать так как будто мы уже осуществили перераспределение данных и у нас уже действительно работает M+1 сервер (или M + N, где N количество новых серверов, если добавляем не один сервер а сразу много). Но, разумеется, мы делаем это не для всех записей, а только для тех которые для нас "новые", то есть добавились уже после момента добавления нового сервера, то есть для тех записей у которых номер записи n >= K. Для таких записей (у которых n>=K) мы используем следующую функцию (точнее значения в той же функции):

m = n % (M + N)

, где N это количество новых (добавляемых) серверов, например N=1, если мы добавляем всего один сервер.

Параллельно с этим мы запускаем фоновый процесс перераспределения записей. Делаем мы это с "конца", то есть сначала перераспределяем записи с номерами от K-j до K, где j - количество записей перераспределяемых за одну итерацию. В принципе j может быть 1, но лучше (выгоднее) всего взять j=M+N, то есть общему количеству серверов, тем самым итерация будет охватывать сразу все сервера, но при этом на каждом сервере будет происходит миграция одной записи. Говоря проще, сначала мы перераспределяем самые последние записи на момент перед добавлением серверов, потом более ранние, и так до самых первых. Как только мы завершили одну итерацию перераспределения для j записей, мы уменьшаем К на это количество записей. То есть мы сохраняем значение K=K-j. И продолжаем процесс до тех пор пока K не станет равным нулю, то есть пока все записи не будут перераспределены между новыми и старыми серверами по новой формуле.

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

Еще один нюанс - на момент начала перераспределения на существующих серверах должен существовать определенный запас свободного места под новые записи пока осуществляется итерация перераспределения. Но этот требуемый свободный объем, в принципе, не очень большой - как только мы осуществим первую итерацию на сервере сразу появится немного больше свободного места (определенное количество старых записей мигрирует на новые сервера), и чем больше итераций мы успеем осуществить тем больше свободного места будет появляться. Главное успеть перераспределить пока есть место :) Разумеется, чем больше свободного места у нас еще есть на старых серверах, тем будет спокойнее на душе :)

Дальше, есть еще вариант :) Можно вообще не осуществлять перераспределение. Но разумеется, для этого потребуется добавить не один сервер, а сразу много. Мы можем просто дождаться того момента когда у нас (вот-вот) закончится всё свободное место на старых серверах, и после этого сразу добавить много новых серверов, на которых будем продолжать работу. Все что нам нужно будет сделать - запомнить номер записи с которого мы переключаем свою работу на новый набор серверов. Чтение (и модификация) старых записей продолжает осуществляться на старых серверах, но все новые записи сохраняются уже на новых. Тем самым мы осуществляем шардинг самого шардинга. В принципе мы можем осуществлять таким образом бесконечный рост.

Оптимальнее всего, думаю, будет объединить две эти стратегии в одну еще более сложную и комплексную. То есть в критические моменты мы осуществляем вторую стратегию, а в фоновом режиме, когда у нас есть для этого ресурсы, мы плавно осуществляем первую стратегию, причем возможно не сразу для всего общего набора серверов, а для некоего подмножества, которое может постепенно расти, вытесняя собой первую стратегию. Разумеется, такая комплексная стратегия еще больше усложняет архитектуру системы, но в идеале игра может стоить свечей :)

пятница, 11 сентября 2009 г.

SQLJet 1.0.0 beta

Проект, которым я был практически полностью занят последнее время, SQLJet, достиг стадии beta 1.0.0. Это достаточно амбициозный проект, призванный создать pure Java реализацию механизма баз данных SQLite.

Причина, по которой такой проект имеет смысл, заключается во все более возрастающей популярности использования SQLite в качестве формата данных для приложений. Например, так поступают Firefox и Skype. Но это еще пол беды, настоящая беда в том, что так поступили и Subversion, а это уже, я бы сказал, хуже. Хуже потому, что существует фактически только одна реализация SQLite и сделана она на языке Си, со всеми вытекающими из этого обстоятельства последствиями (как хорошими так и, к сожалению, плохими). Одним из таких достаточно плохих последствий является зависимость от платформы. Иногда такая зависимость становится серьезной проблемой. Особенно остро это чувствуется в случае необходимости доступа к данным в формате SQLite из среды JVM. Да, существуют варианты (я тестировал http://www.zentus.com/sqlitejdbc/) использования SQLite через JNI или даже эмулятор NestedVM. Однако, эти варианты имеют свои недостатки. Поэтому настоящая чистая реализация формата данных SQLite в среде JVM, на мой взгляд, имеет очень серьезные перспективы.

В данный момент SQLJet имеет, как минимум, два очевидных преимущества для использования в качестве pure Java SQLite-engine:

1. Более высокая производительность. Разумеется, по сравнению только с именно pure Java реализациями, из которых в настоящий момент, кроме SQLJet, существует только основанная на NestedVM эмуляция.

Вот вывод запуска простого сравнения производительности на базе тестов JUnit, которые я использовал для оптимизации SQLjet:

[junit] Testsuite: org.tmatesoft.sqljet.benchmarks.SqlJetBenchmark
[junit] Tests run: 7, Failures: 0, Errors: 0, Time elapsed: 11,935 sec
[junit] Testcase: nothing took 0,59 sec
[junit] Testcase: updateAll took 2,509 sec
[junit] Testcase: deleteAll took 0,491 sec
[junit] Testcase: insertRandoms took 2,89 sec
[junit] Testcase: clear took 0,21 sec
[junit] Testcase: locate took 4,534 sec
[junit] Testcase: selectAll took 0,482 sec

[junit] Testsuite: org.tmatesoft.sqljet.benchmarks.NestedVmBenchmark
[junit] Tests run: 7, Failures: 0, Errors: 0, Time elapsed: 62,108 sec
[junit] Testcase: nothing took 1,612 sec
[junit] Testcase: updateAll took 4,001 sec
[junit] Testcase: deleteAll took 1,065 sec
[junit] Testcase: insertRandoms took 4,821 sec
[junit] Testcase: clear took 0,664 sec
[junit] Testcase: locate took 46,961 sec
[junit] Testcase: selectAll took 2,874 sec

2. Возможность создания расширений и специфической обработки данных в среде JVM. При использовании SQLite через JNI или даже посредством NestedVM, при необходимости создания расширений к SQLite Вам придется это делать тоже на Си что не всегда имеет смысл. Именно в этом случае SQLJet открывает полный контроль над обработкой данных - Вам ничто не мешает вызывать любой Ваш код на любой стадии работы с данными. Вы полностью контроллируете все аспекты работы Вашего приложения с Вашими данными. Я считаю, это очень серьезное преимущество, по крайней мере, в некоторых случаях.

P.S. Да, совсем забыл сказать что SQLJet уже реально используется в новом SVNKit (очень хороший продукт кстати, рекомендую).

вторник, 19 мая 2009 г.

Прозрачное перераспределение данных при добавлении нового сегмента (сервера) в шардинге

Почитал довольно интересный материал о масштабировании баз данных. Больше всего понравился метод так называемого шардинга. Это когда большую базу данных разбивают на мелкие части (части соответственно помещаются на отдельные сервера) таким образом, что всегда известно в какой из частей находится необходимая нам запись. Достигается это с помощью некой математической функции от первичного ключа - например, остаток от деления первичного ключа (или его эквивалента, например hash-функции для строк) на количество серверов. Очень хороший метод. Единственный недостаток - при добавлении нового сервера возникает необходимости полного перераспределения данных между частям.

(Пока что это был краткий пересказ прочитанной статьи, дальше идут уже мои собственные идеи)

Вот этот единственный недостаток такого замечательного метода бесконечного масштабирования баз данных меня и озадачил. Я решил попробовать устранить (хотя бы отчасти) именно этот последний недостаток шардинга - необходимость перераспределения данных между частями базы при добавлении новых серверов. И такой способ устранения необходимости перераспределения, мне кажется, существует. Разумеется, он не лишен в свою очередь своих собственных недостатков, но при разумном использовании, я думаю, может принести огромную пользу.

Суть метода очень проста. Работать такой метод может пока только на простых синетических первичных ключах типа "автоинкремент" (ну то есть каждая новая запись имеет некий порядковый номер который больше предудущей записи, это для тех кто не совсем понимает о чем идет речь). Так вот, все что необходимо сделать при подключении очередного нового сервера шардинга, это изменить функцию распределения (вычисления номера сервера от значения первичного ключа) таким образом что после известного нам (а именно последнего перед добавлением нового сервера) значения первичного ключа N и до следующего известного значения M (которое будет равно N + K, где K - количество записей в одном сегменте шардинга), функция
будет вести себя иначе, а именно - будет возвращать не остаток от деления как раньше, а просто номер нового сервера.

Возможно, объяснение кажется слишком сложным, но думаю что при внимательном обдумывании становится ясно что идея проста до смешного. Уверен, что такое уже не раз приходило кому нибудь в голову и раньше. Но пока не встречал еще упоминания о такой модификации алгоритма. Поэтому решил записать идею:)

Возможно, будут необходимы дополнительные объяснения сути моего предложения, и даже, возможно, будет необходимо нарисовать какие либо схемы. Но пока, мне кажется, приведенного выше краткого объяснения более менее достаточно, если конечно вообще понимать что такое шардинг. По-крайней мере, у меня такое впечатление. Я могу и заблуждаться, даже вообще о сути явления шардинга. Возможно даже что мой метод просто чистый бред. Всё может быть.

Конечно, это несколько усложняет алгоритм, но не настолько чтобы как то существенно снизить эффективность обработки данных. При росте количества добавленных серверов, алгоритм конечно будет еще более усложняться, но нам ничто не мешает автоматизировать функцию добавления серверов и соответсвующего изменения функции. Более того, мы можем сделать функцию динамической и менять её (функцию) паралельно с выполнением процесса перераспределения. Когда процесс перераспределения будет завершен, мы окончательно возвращаемся к классической функции "остаток от деления на количество серверов", до следующего нового сервера, когда мы опять вводим "исключение из правил" для нового сервера и запускаем процесс перераспределения. Это позволит нам добиться полностью прозрачного процесса наращивания шардинга.

Причем процесс изменения функции мы можем реализовать путем использования упомянутого в статье другого способа шардинга, а именно - использованием таблицы соотвествия "ключ-значение". Только хранить мы можем в таблице диапазоны значений ключа и соответствующие диапазонам признак что мы используем для этого диапазона - алгоритм или статическое значение для нового номера сервера. Таких диапазонов может быть несколько и такая таблица удобна именно тем что мы можем гибко и быстро менять эти диапазоны.

Сам процесс перераспределения мы тоже можем разбить на три этапа:
  1. Добавление записей оттуда где они были раньше туда где они должны быть теперь (для всех сегментов) - на этом этапе у нас возникает дублирование записей но это временное явление.
  2. После завершения предыдущего этапа мы можем смело переключаться на новый алгоритм поиска записей - все необходимое у нас для этого уже есть там где нам нужно (мы как раз об этом и позаботились на первом этапе).
  3. На последнем этапе остается просто удалить уже не нужное оттуда где оно было раньше и тем самым устранить возникшее дублирование записей.
И самое интересное, мы можем разбить перераспределение на много мелких последовательных итераций из трех указанных выше стадий цикла: скопировали X записей, модифицировали алгоритм, удалили Х записей, скопировали X записей, модифицировали алгоритм, удалили X записей, ... и так до полного завершения перераспределения.

Надеюсь, я достаточно понятно объяснил свои идеи:) На самом деле, если что то еще осталось не понятно то мы можем рассмотреть базу данных как колоду карт, в которой каждая карта представляет собой запись в базе данных. Шардинг будет представлять собой раскладывание карт на какое либо известное нам количество "кучек". При этом мы раскладываем карты в строго определенном порядке, таким образом всегда имея возможность сразу найти необходимую нам карту в опредленной "кучке" карт. Теперь мы решили увеличить количество "кучек" (то есть добавляем новый сервер) - нам нужно полностью перетасовать все "кучки". На большой базе данных в нагруженной системе, которая должна работать без остановок и сбоев, такая "перетасовка" может представлять собой серьезную проблему. Именно решение этой проблемы "перетасовки" я и пытаюсь предложить.

Возможно, Вам еще не совсем ясно зачем вообще это все нужно и что означает - тогда Вам просто необходимо сначала прочитать статью, ссылку на которую я указал в самом начале этого поста.

Если вопросы остались, рад буду попробовать на них ответить.