Disclaimer
Я здесь описываю некоторые системы, которые сам руками не видел и возможности увидеть не имею(GFS). Люди из Гугла, которые меня читают, если найдете ошибку - не стесняйтесь поправить. Впрочем люди не из Гугла тоже не стесняйтесь.
Часть вторая - репликация.
Начало
здесь.
Если верить статистике в книжке «
Breaking The Availability Barrier» то доступность всех компьютеров всю историю вертелось вокруг цифры 99.5% (надежность технических решений растет примерно одинаково с их сложностью и что взаимокомпенсируется). Это значит, что в среднем в любой момент времени из каждой 1000 компьютеров лежит 5. На сколько я вижу, практикой это вполне подтверждается.
Практически все люди, эксплуатирующие системы с большим количеством серверов относятся к этому факту философски. "Ну полежит и ладно, завтра специальный человек с подносом винтов пройдет, позаменяет их. Админ посмотрит, где место кончилось, понизит карму человеку, который его занял, и что-нибудь почистит. Программист загрузит в gdb корку и посмотрит что там такое. А эту машинку вообще заменят - задолбала глючить. А сейчас фиг с ним, пошли пиво пить - как хорошо, что спроектировали так, что смерть 5 машин не влияет на работу системы."
В принципе все техники безотказности известны и основаны на том, что где-то тут рядом стоит другая машина, на которую система пойдет, когда обнаружит, что данная машина не доступна, и выполнит там такое же действие. Вероятность того что откажет одна машина 0.5%, вероятность того, что две сразу 0.0025% - вполне можно жить.
Некоторая зарисовка из жизни - в Яндексе раньше считалось, что каждая машина обслуживающая критичные задачи должна иметь реплику. Так считалось до того момента как при отказе винчестера на одной из машин, человек пошедший его менять случайно не вытащил винчестер из живой реплики, а не сломанной. Т.е. человек оказался ненадежней железа (что, на самом деле и так известно). После чего считается, что машина должна иметь 2 другие реплики.
При этом, вообще говоря, придумать* схему репликации для поиска сможет ученик 3 класса средней школы. Данные в этом сервисе по факту read-only - поиск не модифицирует в свою поисковую базу. Поэтому за чтением достаточно сходить на любую из реплик, результат будет одинаковый.
* Обращаю внимание на слово "придумать", если его заменить на "реализовать" то учеником 3 класса уже не отделаешься. И вообще в мире не очень много людей, которые это реализовали в масштабах хотя бы несколько сотен машин.
Кому интересно как же обновляется база если она read-only - робот раскладывает новую базу на реплики рядом со старой. После чего как мы убедились, что все разложили, переключаемся на новую. Делать это одновременно не обязательно, несколько минут разницы в результатах поиска у разных юзеров это совершенно нормально, если только у одного пользователя результаты не “пляшут”.
Реплицировать же read-write системы сильно сложней. Мы должны гарантировать, что запись модифицирует все реплики одновременно и приведет их в одинаковое состояние*.
* конечно же существуют системы реплицирующиеся с [серьезной] задержкой или не требующие одинаковости всех реплик - но пока не хочется усложнять.
Допустим, мы хотим иметь простую вещь - у нас есть файловая система, и мы хотим, чтобы она была одинаковой сразу на 2 серверах. Т.е. в своей программе мы вызываем write, и он выполняется атомарно. Т.е. в тот момент, когда write вернул управление уже понятно, он уже либо выполнился на обеих репликах, либо не выполнился ни на одной, и оставил старые данные в неприкосновенности. Вроде все просто и понятно. Только на практике опять возникают сложности.
Представим на минуту, что мы открыли файл на нашей супер синхронизированной файловой системе. Сделали ему seek на середину и выполняем операцию записи 10Mb куска в середину файла. Допустим у нас 2 реплики. Для этого на первой реплике мы берем старую информацию из 10Mb блока и откладываем ее в сторонку. Пишем на ее место новую информацию, после этого одновременно идем на вторую реплику и повторяем операцию там. Если там все хорошо, то мы обоим репликам говорим commit (допустим, он закрывает файл и стирает отложенные 10Mb). Вроде все просто, не совсем приятно только, что реально всю информацию пришлось скопировать 2 раза.
Однако все становится сложней, если появляется проблема. Допустим, на одной из реплик кончилось место, или память, или жесткий диск “кирдык”, или питание вырубилось, или программа упала. В этом случае мы начинает откатывать информацию на первом диске, и молиться, чтобы во время отката проблема не произошла еще раз - иначе данные испорчены на обеих репликах, и вообще не понятно, что делать дальше. Если не стирать отложенные в сторонку данные, то рабочую реплику можно будет вручную восстановить где-нибудь сбоку, но о безотказности уже речи нет.
Что же делать? У этой проблемы существует несколько решений. Все их объединяет то, что придется* от чего-нибудь отказаться. Желательно от чего-нибудь не сильно нужного. Я расскажу одно из них “волшебных” решений - про остальные методы репликации в другой раз .
* Если вы знаете решение где ни от чего не отказались, то нужно проверить, что по дефолту не отказались от производительности.
Давайте вовремя вспомним
первую часть статьи, в которой говорится о том, что при обрабатывании большого количества информации на дисках иногда удобней отказаться от записи в середину файла и писать только в конец. Т.к. мы уже используем диски и конечно же хотим уметь обрабатывать огромное количество информации, можно считать, что мы от этого и так уже отказались. Давайте откажемся второй раз - это совершенно бесплатно.
Теперь единственная доступная нам операция это append, посмотрим, как выглядит на нескольких машинах. Мы добавляем одновременно в конец файла на 2 машинах. Если все прошло замечательно, то делать не надо ничего. При этом обратите внимание, мы отделались единственной записью (в сторону ничего откладывать не надо, т.к. ничего и не было) - и таким образом увеличили throughput вдвое.
В случае же появления проблем достаточно сделать truncate файла на первоначальный размер. Обычно truncate это операция, которая выполняется без проблем - по сравнению с записью от OS тут почти невозможно получить ошибку. А если еще и брать данные о размере файла не из самого файла, а просто хранить рядом в метаданных, то даже truncate-ить ничего не надо, надо просто не update-ить длину.
Кстати, древние украли все наши великие идеи, и самая известная система построенная на этом принипе это GFS (тот который от гугля, а не однофамилец от RedHat)
Если прочитать
ее описание то легко понять что система построена на описанных выше принципах: large streaming reads (small random reads supported [… but ...] applications often batch and sort their small reads to advance steadily through the file rather than go back and forth) и many large, sequential writes that append data to files. При этом явно сказано, что High sustained bandwidth is more important than low latency.
Что характерно - система не поддерживает POSIX API. GFS provides a familiar file system interface, though it does not implement a standard API such as POSIX. We support the usual operations to create, delete, open, close, read, and write files. Зато поддерживает операцию appendRecord и snapshot.
Наверное, стоит для галочки упомянуть о некоторых архитектурных особенностях GFS которые, противоречат вышеописанной модели, которую я преднамеренно упростил, чтобы не усложнять изложение, чтобы потом меня не обвинили в том, что я что-то не вкурил. Во-первых она блочная - файл там не сплошной и попилен на большие блоки, иначе нельзя записать файл размером большим чем одна машина. Во вторых она таки умеет запись в середину, но делает это очень редко (For cluster X, overwriting accounts for under 0.0001% of bytes mutated and under 0.0003% of mutation operations. For cluster Y, the ratios are both 0.05%)
Вот и все по существу на сегодня.
Этим эссе я хотел объяснить важную мысль: очень часто архитектура создана под возможности, которые вам на самом деле не нужны, но вы за них платите (по умолчанию разменной монетой является производительность). Если от них отказаться - можно в обмен получить что-либо сильно более полезное.
Надеюсь, параллельно мне удалось объяснить "на пальцах", что же такое GFS и чем в ней пожертвовано ради эффективности. Так же надеюсь, что в процессе стало понятно для чего она нужна, и следующего человека, который спросит, почему у меня поисковая база не лежит в GFS, можно будет отослать к этому тексту.
P.S. Вот здесь есть феерическая статья
MapReduce: A major step backwards, где серьезные теоретики RDBMS наехали на описываемый подход к работе с данными c общим смыслом "тут очень мало фич". Наезд очень странный т.к. вообще говоря в отказе от фич в обмен на производительность собственно и весь смысл. Я бы на него даже поленился отвечать, но вот
люди не поленились ссылки via
fisher_geekly я их до этого где-то видел, но уже забыл где, а сейчас взял из его ЖЖ.
P.P.S. Из всего вышесказанного можно понять, почему я не верю в эффективные реплицируемые файловые системы с POSIX интерфейсом. При этом для того, чтобы до конца правдивым, а не "объяснять как трудно ездить на велосипеде, вообще практически невозможно...*" стоит сказать, что они вполне успешно существуют. Например, GlusterFS, или Isilon OneFS это вполне себе их примеры. Просто не стоит про них говорить как про "совершенно волшебные решения без недостатков".
*фраза (c) Игорь Ашманов.
Продолжение следует(наверное не завтра, а с некоторой задержкой) - в следующий раз про SQL сервера.