В этом документе мы сначала описываем систему в общих чертах. Затем мы представляем ограничения и инварианты для конкретизации предметной области. Мы последовательно движемся к конкретному решению, описывая ключевые компоненты и поведение по мере продвижения.
У нас есть большой и динамичный набор производителей, вырабатывающих поток логических записей. Эти записи должны быть доступны для поиска потребителем.
+-----------+
P -> | |
P -> | ? | -> C
P -> | |
+-----------+
Производители заботятся о том, чтобы их записи были приняты и сохранены как можно быстрее. В случае сбоев некоторые случаи использования предпочитают обратное давление (например, журналы событий), а другие — буферизацию и отбрасывание (например, журналы приложений). Но в обоих случаях немедленный принимающий компонент должен быть оптимизирован для быстрых последовательных записей.
Потребители заботятся о том, чтобы получать ответы на свои запросы максимально быстро и точно. Так как мы определили запросы так, что они требуют временных границ, мы утверждаем, что можем решить проблему с помощью поиска по временновым разделённым данным. Поэтому окончательный формат записей на диске должен быть глобальным объединением всех потоков производителей, разделённым по времени.``` +-------------------+ P -> | R | P -> | R ? R R R | -> C P -> | R | +-------------------+
## Операционные детали
У нас будет много производителей, порядка тысяч.
(Производителем для нас является процесс приложения плюс передающее агентство.)
Наша система журналирования должна быть меньше, чем система производства, которую она обслуживает.
Поэтому у нас будут несколько приемников, и каждый из них должен обрабатывать записи от нескольких производителей.
Кроме того, нам нужно обслуживать системы производства, генерирующие большое количество данных журналирования.
Поэтому мы не будем делать упрощающих предположений о объёмах данных.
Мы предполагаем, что даже минимальный рабочий набор данных журналирования будет слишком большим для хранения на одном узле.
Поэтому потребителям придётся обязательно запрашивать данные у нескольких узлов.
Это означает, что окончательный, временно разделённый набор данных будет распределённым и повторно созданным.
+---+ +---+
P -> F -> | I | | Q | --. P -> F -> | | +---+ | +---+ +---+ '-> +---+ ? | Q | ----> C P -> F -> | I | +---+ .-> P -> F -> | | +---+ | P -> F -> | | | Q | --' +---+ +---+
Мы внедрили распределение, что требует решения вопроса координации.
## КоординацияКоordinatsiya является смертельной для распределённых систем.
Наш логический систем будет свободен от координации.
Давайте рассмотрим, что это требует на каждом этапе.
Исправленный текст:
Коordination является смертельной для распределённых систем.
Наш логическая система будет свободна от коordination.
Давайте рассмотрим, что это требует на каждом этапе.Производители — или более точно, передатчики — должны иметь возможность подключаться к любому экземпляру сборщика и начинать отправку записей.
Эти записи должны сохраняться непосредственно на диск данного экземпляра сборщика, с минимальной промежуточной обработкой.
Если сборщик выходит из строя, его передатчики должны иметь возможность просто пересоединиться к другим экземплярам и продолжить работу.
(При переходе они могут предоставлять обратное давление, буферизовать или игнорировать записи в зависимости от своей конфигурации.)
Сказано это всё для того, чтобы отметить, что передатчики не должны зависеть от знаний о том, какой сборщик считается «правильным».
Любой сборщик должен быть равноценным.Как оптимизация, сильно нагруженные сборщики могут перенаправлять нагрузку (соединения) к другим сборщикам.
Сборщики будут распределять информацию о нагрузке между собой, такой как количество соединений, операций ввода-вывода и т.д.
Затем, сильно нагруженные сборщики могут отказывать в новых соединениях, тем самым направляя отправителей к менее нагруженным экземплярам.
Очень сильно нагруженные сборщики даже могут закрывать существующие соединения, если это необходимо.
Это должно быть аккуратно управляемым, чтобы предотвратить случайное прекращение обслуживания.
Например, одновременно отказывать в соединениях не должны более нескольких сборщиков.Потребители должны иметь возможность выполнять запросы без предварительных знаний о временных диапазонах, распределении реплик и т.д.
Без этих знаний это означает, что запросы всегда будут распределяться по каждому узлу запросов, собираются и деконфликтуются.
Узлы запросов могут выходить из строя в любой момент времени или запускаться пустыми, поэтому операции запросов должны грациозно управлять частичными результатами.Как оптимизация, потребители могут выполнять чтение-восстановление.
Запрос должен вернуть N копий каждой совпадающей записи, где N — это фактор репликации.
Любые записи с меньшим колич�数比N的副本可能是未充分复制的。
可以创建一个新的段,其中包含可疑记录,并将其复制到集群中。
作为进一步的优化,单独的过程可以执行连续的时间空间查询以进行读取-恢复。数据在加载层和请求层之间的传输也需要关注。
理想情况下,任何加载节点都应能够将段传递给任意或所有请求节点。
这种传输应该完成的工作,确保整个系统的进展。
我们还应该优雅地从故障中恢复;例如,在事务处理过程中的任何时候出现网络分区。现在让我们考虑如何安全地混合数据,从收集层到请求层。## Сегменты сбора
Сборщики получают N независимых потоков записей от N передатчиков.
Каждую запись следует префиксировать сборщиком временным UUID и [ULID](https://github.com/oklog/ulid).
Важно, чтобы каждая запись имела метку времени с разумной точностью, чтобы создать некий глобальный порядок.
Но неважно, были ли часы глобально синхронизированы или записи строго линеаризованы.
Также допустимо, если записи, пришедшие в одном минимальном временном окне, будут выглядеть в случайном порядке, при условии, что этот порядок будет стабилен.
Приходящие записи записываются в так называемый активный сегмент, который представляет собой файл на диске.
+---+
P -> F -> | I | -> Активный: R R R... P -> F -> | | P -> F -> | | +---+
Как только он запишет B байт или будет активен S секунд, активный сегмент сбрасывается на диск.
+---+
P -> F -> | I | -> Активный: R R R... P -> F -> | | Сброшенный: R R R R R R R R R P -> F -> | | Сброшенный: R R R R R R R R +---+
Сборщик последовательно потребляет записи из каждого соединения передатчика.
Следующая запись потребляется после успешной записи текущей записи в активный сегмент.
Активный сегмент обычно синхронизируется сразу после того, как он был сброшен.
Это режим долговечности по умолчанию, условно называемый быстрым.Производители могут дополнительно подключаться к отдельному порту, обработчик которого будет синхронизировать активный сегмент после каждой записи. Это обеспечивает более высокую долговечность за счёт снижения пропускной способности. Это отдельный режим долговечности, условно называемый надёжным.
Может существовать третий, ещё более высокий режим долговечности, условно называемый объёмным. Передатчики могут писать целые файлы сегментов одновременно в сборщик. Каждый файл сегмента будет подтверждён только после успешного реплицирования среди узлов хранения. Затем передатчик может отправить следующий полный сегмент.
Сборщики предоставляют API для обслуживания сброшенных сегментов.
- GET /next — возвращает самый старый сброшенный сегмент и помечает его ожидающим
- POST /commit?id=ID — удаляет ожидающий сегмент
- POST /failed?id=ID — возвращает ожидающий сегмент обратно в сброшенныеСостояние сегмента контролируется расширением файла, и мы используем файловую систему для атомарных переименований.
Состояния: .active, .flushed или .pending, и всегда существует только один активный сегмент во время соединения передатчика.```
+---+
P -> F -> | I | Активный +---+
P -> F -> | | Активный | Q | --.
| | Очищенный +---+ |
+---+ +---+ '->
+---+ ? | Q | ----> C
P -> F -> | I | Активный +---+ .->
P -> F -> | | Активный | Q | --'
P -> F -> | | Активный
P -> F -> | | Очищенный
+---+Обратите внимание, что инжекторы являются состоятельными системами, поэтому они требуют плавного процесса завершения работы.
Сначала они должны закрывать соединения и слушатели.
Затем они должны ждать, пока все очищенные сегменты будут использованы.
Наконец, их можно будет выключить.
### Употребление сегментов
Инжекторы действуют как своего рода очередь, буферизируя записи на диск в группах, называемых сегментами.
Хотя это защищено от таких проблем, как отказ электричества, в конечном итоге мы считаем эту хранилище временной.
Сегменты следует дублировать как можно быстрее на уровне запросов.
Здесь мы берём страницу из руководства Prometheus.
Вместо того чтобы инжекторы отправляли очищенные сегменты на узлы запросов, узлы запросов получают очищенные сегменты от инжекторов.
Это позволяет моделировать согласованную модель для масштабирования производительности.
Чтобы принимать более высокую скорость приёма данных, добавьте больше узлов приёма данных с быстрыми дисками.
Если узлы приёма данных заблокированы, добавьте больше узлов запросов для получения данных от них.Употребление моделируется трёхэтапной транзакцией.
Первая стадия — это этап чтения.
Каждый узел запросов регулярно запрашивает каждый узел приема данных о его самом старом очищённом сегменте через GET /next.
(Это может быть случайный выбор, циклический метод или любой более продвинутый алгоритм. В настоящее время используется случайный выбор.)
Полученные сегменты читаются запись за записью и объединяются в другой составной сегмент.
Процесс повторяется, потребляя несколько сегментов уровня приема данных и объединяя их в тот же составной сегмент.Как только составной сегмент достигнет **B** байт или будет активен в течение **S** секунд, он закрывается, и мы переходим к стадии дублирования.
Дублирование означает запись составного сегмента на **N** различных узлов запросов, где **N** является коэффициентом дублирования.
На данный момент мы просто POST сегмента на конечную точку дублирования на **N** случайных узлах хранения. После того как сегмент был подтверждён как скопирован на **N** узлах, мы переходим к этапу коммита.
Узел запроса коммитирует исходные сегменты на всех узлах принятия данных через POST /commit.
Если составной сегмент не удается скопировать по какой-либо причине, узел запроса считает все исходные сегменты неудачными через POST /failed.
В обоих случаях транзакция завершена, и узлу запросу можно начать новый цикл.```
Q1 I1 I2 I3
-- -- -- --
|-Далее--->| | |
|-Далее------->| |
|-Далее----------->|
|<-S1-----| | |
|<-S2---------| |
|<-S3-------------|
|
|--.
| | S1∪S2∪S3 = S4 Q2 Q3
|<-' -- --
|-S4------------------>| |
|-S4---------------------->|
|<-OK------------------| |
|<-OK----------------------|
|
| I1 I2 I3
| -- -- --
|-Коммит->| | |
|-Коммит----->| |
|-Коммит--------->|
|<-OK-----| | |
|<-OK---------| |
|<-OK-------------|
Рассмотрим сценарий отказа на каждом этапе.Если узел запроса выходит из строя во время этапа чтения, ожидающие сегменты остаются в состоянии "зависания" до истечения времени ожидания. После чего они становятся доступными для использования другим узлом запроса. Если исходный узел запроса мертв навсегда, это не является проблемой. Если же исходный узел запроса восстанавливается, он может иметь записи, которые уже были прочитаны и скопированы другими узлами. В этом случае будут записаны повторяющиеся записи на уровне запросов, и один или несколько окончательных коммитов могут завершиться ошибкой. Если это происходит, всё в порядке: записи переизбыточены, но они будут декоммутированы при чтении и в конечном итоге компактифицированы. Поэтому ошибки коммита следует отметить, но можно безопасно игнорировать их.Если узел запроса выходит из строя во время этапа репликации, ситуация аналогична. Предположим, что узел не вернётся; ожидающие сегменты загрузки истекут по времени и будут повторно попробованы другими узлами. И если узел действительно вернётся, процесс репликации продолжится без ошибок, а один или несколько окончательных коммитов могут завершиться ошибкой. Как и раньше, это нормально: переизбыточные записи будут декоммутированы при чтении и в конечном итоге компактифицированы.
Если узел запроса выходит из строя во время этапа коммита, один или несколько сегментов загрузки застрянут в состоянии "ожидания". В конечном итоге они истекут по времени и вернутся в состояние "flush". Как и раньше, записи станут переизбыточены, будут декоммутированы при чтении и в конечном итоге компактифицированы.
Чтобы защититься от этого типа отказа, клиенты должны использовать режим массовой загрузки. Это не позволит продвигаться до тех пор, пока файл сегмента не будет реплицирован в уровень хранения. Если узел магазина выходит из строя навсегда, записи остаются безопасными на других узлах (коэффициент репликации — 1). Но чтобы восстановить потерянные записи до желаемого коэффициента репликации, требуется выполнение операции read repair. Можно запустить специальный процесс перемещения во времени, который будет выполнять эту процедуру восстановления. Он фактически может запросить все журналы с самого начала времени и выполнить read repair для недореплицированных записей.
Альтернативный подход заключается в том, чтобы называть каждый файл как FROM-TO, где FROM и TO являются наименьшей и наибольшей временной UUID в файле соответственно. Тогда, при задании временного диапазона запроса T1–T2, сегменты могут быть выбраны с помощью двух простых сравнений для выявления пересечения временных диапазонов. Установите два диапазона (A, B) (C, D) так, чтобы A ≤ B, C ≤ D, и A ≤ C. В нашем случае, первый диапазон — это диапазон, указанный запросом, а второй диапазон — для данного файла сегмента. Если B ≥ C, то диапазоны пересекаются, и следует запрашивать файл сегмента.
A--B B ≥ C?
C--D да
A--B B ≥ C?
C--D нет
A-----B B ≥ C?
C-D да
A-B B ≥ C?
C----D да
Это позволяет нам выбирать файлы сегментов для выполнения запросов.## Компакция
Компакция служит двумя целями: удалению дубликатов записей и устранению перекрытия сегментов. Дублирование записей может произойти во время сбоев, таких как сетевые разделения. Однако сегменты будут регулярно и естественно перекрываться.
Рассмотрим три перекрывающихся файла сегментов на данном узле запроса.
t0 t1
+-------+ |
| A | |
+-------+ |
| +---------+ |
| | B | |
| +---------+ |
| +---------+
| | C |
| +---------+
Компакция сначала объединяет эти перекрывающиеся сегменты в единственный агрегирующий сегмент в памяти. При слиянии дубликаты записей можно обнаружить по ULID и удалить. Затем компакция делит агрегирующий сегмент для достижения желаемого размера файлов и создает новые, неперекрывающиеся сегменты.
t0 t1
+-------+-------+
| | |
| D | E |
| | |
+-------+-------+
Компакция снижает количество сегментов, необходимых для чтения для обслуживания запросов за данный период времени. В идеальном состоянии каждое время будет соответствовать точно одному сегменту. Это помогает производительности запросов путем уменьшения колич�数字转换错误,应为:
数量转换错误,应为:
t0 t1
+-------+-------+
| | |
| D | E |
| | |
+-------+-------+
正确翻译应为:
t0 t1
+-------+-------+
| | |
| D | E |
| | |
+-------+-------+
最终结果如下:
t0 t1
+-------+-------+
| | |
| D | E |
| | |
+-------+-------+
因此,完整的文本应该是:
Компакция служит двумя целями: удалению дубликатов записей и устранению перекрытия сегментов. Дублирование записей может произойти во время сбоев, таких как сетевые разделения. Однако сегменты будут регулярно и естественно перекрываться.
Рассмотрим три перекрывающихся файла сегментов на данном узле запроса.
t0 t1
+-------+ |
| A | |
+-------+ |
| +---------+ |
| | B | |
| +---------+ |
| +---------+
| | C |
| +---------+
Компакция сначала объединяет эти перекрывающиеся сегменты в единственный агрегирующий сегмент в памяти. При слиянии дубликаты записей можно обнаружить по ULID и удалить. Затем компакция делит агрегирующий сегмент для достижения желаемого размера файлов и создает новые, неперекрывающиеся сегменты.
t0 t1
+-------+-------+
| | |
| D | E |
| | |
+-------+-------+
Компакция снижает количество сегментов, необходимых для чтения для обслуживания запросов за данный период времени. В идеальном состоянии каждое время будет соответствовать точно одному сегменту. Это помогает производительности запросов путем уменьшения количества операций чтения. Обратите внимание, что компакция улучшает производительность запросов, но не влияет ни на корректность, ни на использование пространства.Сжатие может применяться к сегментам совершенно независимо от процесса, описанного здесь.
Правильное сжатие может значительно увеличить срок хранения за счёт использования некоторого времени центрального процессора.
Оно также может препятствовать обработке файлов сегментов общими UNIX-инструментами, такими как grep
, хотя это может иметь значение или нет.
Поскольку записи являются отдельно адресуемыми, дедубликация происходит в реальном времени на уровне каждой записи.
Поэтому отображение записи на сегмент можно оптимизировать полностью независимо каждым узлом без координации.
Расписание и агрессивность компакции является важным фактором производительности.
На данный момент однопоточный компактор выполняет каждый из задач компакции последовательно и постоянно.
Он запускается максимум один раз в секунду.
Необходимо гораздо больше анализа производительности и исследований в реальных условиях.
Каждый узел запроса обслуживает API запросов, GET /query
.
Запросы могут отправляться любому API узлу запроса пользователями.
При получении запрос передаётся всем узлам уровня запросов.
Ответы собираются, результаты объединяются и дедублицируются, а затем возвращаются пользователю.
Фактическая работа поиска выполняется каждым узлом запроса отдельно.Сначала определяются файлы сегментов, соответствующие временным границам запроса.
Затем к каждому файлу прикрепляется читатель сегмента, который фильтрует совпадающие записи из файла.
Наконец, объединяющий читатель принимает результаты каждого файла сегмента, упорядочивает их и возвращает обратно в исходный узел запроса.
Эта конвейерная линия лениво создаётся из io.ReadCloser
, и затраты возникают при фактических чтениях.
Иными словами, когда HTTP-ответ записывается обратно в исходный узел запроса.
Обратите внимание, что читатель сегмента запускается в своём собственном потоке, и чтение/фильтрация происходят параллельно.
В настоящее время нет жёсткого ограничения на количество активных потоков, допустимых для чтения файлов сегментов.
Это следует улучшить.
Запрос имеет несколько полей:
From
, To
time.Time
— границы запросаQ
string
— термин для поиска, пустое значение допустимо и соответствует всем записямRegex
bool
— если true
, компилировать и проверять Q
как регулярное выражениеStatsOnly
bool
— если true
, вернуть только статистику, без фактических результатовОтвет запроса включает несколько полей:
NodeCount
int
— количество запрошенных узловSegmentCount
int
— количество прочтённых сегментовSize
int
— размер файлов сегментов, используемых для получения результатовResults
io.Reader
— объединённые и отсортированные по времени результатыИспользование StatsOnly позволяет «анализировать» и итерировать запрос до тех пор, пока он не будет уточнён до удобного набора результатов.Это рабочий черновик компонентов системы.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )