пятница, 28 мая 2021 г.

Краткое руководство по ассемблеру Go: константы и символы

Константы

Хотя ассемблер руководствуется ассемблерами Plan 9, это отдельная программа, поэтому есть некоторые отличия. Первое, что он находится в постоянной оценке. Константные выражения в ассемблере анализируются с использованием приоритета операторов Go, а не C-подобного приоритета оригинала. Таким образом, 3&1<<2 равно 4, а не 0 - он анализирует как (3&1)<<2, а не 3&(1<<2). Кроме того, константы всегда оцениваются как 64-разрядные целые числа без знака. Таким образом, -2 - это не целое число минус два, а 64-битное целое число без знака с тем же битовым шаблоном. Различие редко имеет значение, но во избежание двусмысленности деление или сдвиг вправо, когда установлен старший бит правого операнда, отклоняется.

Символы

Некоторые символы, такие как R1 или LR, предопределены и относятся к регистрам. Точный набор зависит от архитектуры.

Есть четыре заранее объявленных символа, которые относятся к псевдорегистрам. Это не настоящие регистры, а скорее виртуальные регистры, поддерживаемые цепочкой инструментов, такие как указатель кадра (frame pointer). Набор псевдорегистров одинаков для всех архитектур:

  • FP: (Frame pointer) Указатель кадра: аргументы и локальные переменные.
  • PC: (Program counter) Счетчик программы: прыжки и ветки.
  • SB: (Static base pointer) Статический базовый указатель: глобальные символы.
  • SP: (Stack pointer) Указатель стека: верх стека.

Все определяемые пользователем символы записываются как смещения в псевдорегистры FP (аргументы и локальные переменные) и SB (глобальные переменные).

Псевдорегистр SB можно рассматривать как источник памяти, поэтому символ foo(SB) - это имя foo как адрес в памяти. Эта форма используется для именования глобальных функций и данных. Добавление <> к имени, как в foo<>(SB), делает имя видимым только в текущем исходном файле, как статическое объявление верхнего уровня в файле C. Добавление смещения к имени относится к этому смещению от адреса символа, поэтому foo+4(SB) находится на четыре байта после начала foo.

Псевдо-регистр FP - это указатель виртуального кадра, используемый для ссылки на аргументы функции. Компиляторы поддерживают указатель виртуального кадра и ссылаются на аргументы в стеке как на смещения от этого псевдорегистра. Таким образом, 0(FP) - это первый аргумент функции, 8(FP) - второй (на 64-битной машине) и так далее. Однако при таком обращении к аргументу функции необходимо поместить имя в начало, как в first_arg+0(FP) и second_arg+8(FP). (Значение смещения - смещение от указателя кадра - отличается от его использования с SB, где это смещение от символа.) Ассемблер обеспечивает соблюдение этого соглашения, отклоняя простые 0(FP) и 8(FP). Фактическое имя семантически не имеет значения, но должно использоваться для документирования имени аргумента. Стоит подчеркнуть, что FP всегда является псевдорегистром, а не аппаратным регистром, даже на архитектурах с аппаратным указателем кадра.

Для функций сборки с прототипами Go go vet проверит совпадение имен аргументов и смещений. В 32-битных системах младшие и старшие 32 бита 64-битного значения различаются добавлением суффикса _lo или _hi к имени, как в arg_lo+0(FP) или arg_hi+4(FP). Если прототип Go не называет свой результат, ожидаемое имя сборки - ret.

Псевдо-регистр SP - это указатель виртуального стека, используемый для ссылки на локальные переменные кадра и аргументы, подготавливаемые для вызовов функций. Он указывает на верхнюю часть кадра локального стека, поэтому ссылки должны использовать отрицательные смещения в диапазоне [−framesize, 0): x-8(SP), y-4(SP) и так далее.

В архитектурах с аппаратным регистром SP префикс имени отличает ссылки на указатель виртуального стека от ссылок на архитектурный регистр SP. То есть x-8(SP) и -8(SP) являются разными ячейками памяти: первая относится к псевдорегистру виртуального указателя стека, а вторая - к регистру SP оборудования.

На машинах, где SP и PC традиционно являются псевдонимами физического нумерованного регистра, в ассемблере Go имена SP и PC по-прежнему обрабатываются особым образом; например, ссылки на SP требуют символа, как и FP. Для доступа к фактическому регистру оборудования используйте истинное имя R. Например, в архитектуре ARM аппаратный SP и PC доступны как R13 и R15.

Ответвления и прямые переходы всегда записываются как смещения к PC или как переходы к меткам:

label:
	MOVW $0, R1
	JMP label

Каждая метка видна только в той функции, в которой она определена. Поэтому для нескольких функций в файле разрешено определять и использовать одни и те же имена меток. Прямые переходы и инструкции вызова могут нацеливаться на текстовые символы, такие как name(SB), но не на смещения от символов, такие как name+4(SB).

Инструкции, регистры и директивы ассемблера всегда пишутся ВЕРХНИМ РЕГИСТРОМ, чтобы напомнить вам, что программирование на ассемблере - это рискованное занятие. (Исключение: переименование g регистра на ARM.)

В объектных файлах и двоичных файлах Go полное имя символа - это путь к пакету, за которым следует точка и имя символа: fmt.Printf или math/rand.Int. Поскольку синтаксический анализатор ассемблера обрабатывает точку и косую черту как знаки препинания, эти строки нельзя использовать непосредственно как имена идентификаторов. Вместо этого ассемблер разрешает использование символа средней точки U+00B7 и разделительной косой черты U+2215 в идентификаторах и заменяет их на точку и косую черту. В исходном файле ассемблера указанные выше символы записываются как fmt·Printf и math∕rand·Int. Списки сборок, сгенерированные компиляторами при использовании флага -S, показывают точку и косую черту непосредственно вместо замены Unicode, требуемой ассемблерами.

Большинство рукописных файлов сборки не включают полный путь к пакету в именах символов, потому что компоновщик вставляет путь к пакету текущего объектного файла в начало любого имени, начинающегося с точки: в исходном файле сборки в math/rand реализации пакета, функция пакета Int может называться ·Int. Это соглашение позволяет избежать необходимости жесткого кодирования пути импорта пакета в его собственном исходном коде, что упрощает перенос кода из одного места в другое.


Читайте также:


четверг, 27 мая 2021 г.

Краткое руководство по ассемблеру Go

Эта серия постов представляет собой краткое описание необычной формы языка ассемблера, используемого компилятором gc Go.

Ассемблер основан на стиле ввода ассемблеров Plan 9.

Самое важное, что нужно знать об ассемблере Go, - это то, что он не является прямым представлением базовой машины. Некоторые детали точно соответствуют машине, а некоторые нет. Это связано с тем, что пакет компилятора не требует передачи ассемблера в обычном конвейере. Вместо этого компилятор работает с полуабстрактным набором инструкций, и выбор инструкций происходит частично после генерации кода. Ассемблер работает с полуабстрактной формой, поэтому, когда вы видите такую инструкцию, как MOV, то, что фактически генерирует инструментальная цепочка для этой операции, может быть вовсе не инструкцией перемещения, возможно, очисткой или загрузкой. Или он может точно соответствовать машинной инструкции с таким именем. В общем, машинно-зависимые операции имеют тенденцию появляться сами по себе, в то время как более общие концепции, такие как перемещение памяти, вызов и возврат подпрограмм, являются более абстрактными. Детали меняются в зависимости от архитектуры, и возможны неточности; ситуация не совсем ясна.

Программа на ассемблере - это способ проанализировать описание этого полуабстрактного набора инструкций и превратить его в инструкции для ввода компоновщику (linker). Если вы хотите увидеть, как выглядят инструкции в сборке для данной архитектуры, скажем amd64, есть много примеров в исходных кодах стандартной библиотеки, в таких пакетах, как runtime и math/big. Вы также можете проверить, что компилятор испускает как ассемблерный код (фактический результат может отличаться от того, что вы видите здесь):

$ cat x.go
package main

func main() {
	println(3)
}
$ GOOS=linux GOARCH=amd64 go tool compile -S x.go        # or: go build -gcflags -S x.go
"".main STEXT size=74 args=0x0 locals=0x10
	0x0000 00000 (x.go:3)	TEXT	"".main(SB), $16-0
	0x0000 00000 (x.go:3)	MOVQ	(TLS), CX
	0x0009 00009 (x.go:3)	CMPQ	SP, 16(CX)
	0x000d 00013 (x.go:3)	JLS	67
	0x000f 00015 (x.go:3)	SUBQ	$16, SP
	0x0013 00019 (x.go:3)	MOVQ	BP, 8(SP)
	0x0018 00024 (x.go:3)	LEAQ	8(SP), BP
	0x001d 00029 (x.go:3)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (x.go:3)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (x.go:3)	FUNCDATA	$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (x.go:4)	PCDATA	$0, $0
	0x001d 00029 (x.go:4)	PCDATA	$1, $0
	0x001d 00029 (x.go:4)	CALL	runtime.printlock(SB)
	0x0022 00034 (x.go:4)	MOVQ	$3, (SP)
	0x002a 00042 (x.go:4)	CALL	runtime.printint(SB)
	0x002f 00047 (x.go:4)	CALL	runtime.printnl(SB)
	0x0034 00052 (x.go:4)	CALL	runtime.printunlock(SB)
	0x0039 00057 (x.go:5)	MOVQ	8(SP), BP
	0x003e 00062 (x.go:5)	ADDQ	$16, SP
	0x0042 00066 (x.go:5)	RET
	0x0043 00067 (x.go:5)	NOP
	0x0043 00067 (x.go:3)	PCDATA	$1, $-1
	0x0043 00067 (x.go:3)	PCDATA	$0, $-1
	0x0043 00067 (x.go:3)	CALL	runtime.morestack_noctxt(SB)
	0x0048 00072 (x.go:3)	JMP	0
...

Директивы FUNCDATA и PCDATA содержат информацию для использования сборщиком мусора; они вводятся компилятором.

Чтобы увидеть, что помещается в двоичный файл после компоновки, используйте go tool objdump:

$ go build -o x.exe x.go
$ go tool objdump -s main.main x.exe
TEXT main.main(SB) /tmp/x.go
  x.go:3		0x10501c0		65488b0c2530000000	MOVQ GS:0x30, CX
  x.go:3		0x10501c9		483b6110		CMPQ 0x10(CX), SP
  x.go:3		0x10501cd		7634			JBE 0x1050203
  x.go:3		0x10501cf		4883ec10		SUBQ $0x10, SP
  x.go:3		0x10501d3		48896c2408		MOVQ BP, 0x8(SP)
  x.go:3		0x10501d8		488d6c2408		LEAQ 0x8(SP), BP
  x.go:4		0x10501dd		e86e45fdff		CALL runtime.printlock(SB)
  x.go:4		0x10501e2		48c7042403000000	MOVQ $0x3, 0(SP)
  x.go:4		0x10501ea		e8e14cfdff		CALL runtime.printint(SB)
  x.go:4		0x10501ef		e8ec47fdff		CALL runtime.printnl(SB)
  x.go:4		0x10501f4		e8d745fdff		CALL runtime.printunlock(SB)
  x.go:5		0x10501f9		488b6c2408		MOVQ 0x8(SP), BP
  x.go:5		0x10501fe		4883c410		ADDQ $0x10, SP
  x.go:5		0x1050202		c3			RET
  x.go:3		0x1050203		e83882ffff		CALL runtime.morestack_noctxt(SB)
  x.go:3		0x1050208		ebb6			JMP main.main(SB)


Читайте также:


суббота, 8 мая 2021 г.

Клиент Go для Elasticsearch: массовая индексация

Один из наиболее распространенных вариантов использования любого клиента Elasticsearch - как можно быстрее и эффективнее индексировать документы в Elasticsearch. Самый простой вариант, использующий простой Elasticsearch Bulk API, имеет множество недостатков: вам нужно вручную подготовить пары метаданных и данных полезной нагрузки, разделить полезную нагрузку на пакеты, десериализовать ответ, проверить результаты на наличие ошибок, отобразить отчет и так далее. Пример по умолчанию в репозитории довольно красноречиво демонстрирует, насколько все это задействовано.

По этой причине клиент предоставляет вспомогательный компонент esutil.BulkIndexer, аналогичный массовым помощникам в других клиентах:

$ go doc -short github.com/elastic/go-elasticsearch/v7/esutil.BulkIndexer
type BulkIndexer interface {
  // Add добавляет элемент в индексатор.
  // ...
  Add(context.Context, BulkIndexerItem) error

  // Close ожидает, 
  // пока все добавленные элементы не будут записаны, 
  // и закрывает индексатор.
  Close(context.Context) error

  // Stats возвращает статистику индексатора.
  Stats() BulkIndexerStats
}

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

Индексатор настраивается с помощью структуры esutil.BulkIndexerConfig, переданной в качестве аргумента конструктору:

$ go doc -short github.com/elastic/go-elasticsearch/v7/esutil.BulkIndexerConfig
type BulkIndexerConfig struct {
  NumWorkers    int           // Количество воркеров. По умолчанию используется runtime.NumCPU().
  FlushBytes    int           // Порог записи в байтах. По умолчанию 5MB.
  FlushInterval time.Duration // Порог записи по продолжительности. По умолчанию 30 секунд.

  Client      *elasticsearch.Client   // Elasticsearch клиент.
  Decoder     BulkResponseJSONDecoder // Пользовательский JSON декодер.
  DebugLogger BulkIndexerDebugLogger  // Дополнительный регистратор для отладки.

  OnError      func(context.Context, error)          // Вызывается при ошибках индексатора.
  OnFlushStart func(context.Context) context.Context // Вызывается при запуске записи.
  OnFlushEnd   func(context.Context)                 // Вызывается по окончании записи.

  // Параметры Bulk API.
  Index               string
  // ...
}

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

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

Поле Client позволяет передать экземпляр elasticsearch.Client с любой желаемой конфигурацией для ведения журнала, безопасности, повторных попыток, настраиваемого транспорта и т. д.

Следуя общей теме расширяемости, поле Decoder принимает тип, реализующий интерфейс esutil.BulkResponseJSONDecoder, что позволяет использовать более эффективный кодировщик JSON, чем encoding/json стандартной библиотеки.

Документы для индексации добавляются в индексатор как esutil.BulkIndexerItem:

go doc -short github.com/elastic/go-elasticsearch/v7/esutil.BulkIndexerItem
type BulkIndexerItem struct {
  Index           string
  Action          string
  DocumentID      string
  Body            io.Reader
  RetryOnConflict *int

  OnSuccess func(context.Context, BulkIndexerItem, BulkIndexerResponseItem)        // Для каждого элемента
  OnFailure func(context.Context, BulkIndexerItem, BulkIndexerResponseItem, error) // Для каждого элемента
}

Давайте объединим всю эту информацию, пройдя по коду примера репозитория. Клонируйте репозиторий и запустите cd _examples/bulk && go run indexer.go, чтобы запустить его локально.

В примере индексируется структура данных, определяемая типами Article и Author:

type Article struct {
  ID        int       `json:"id"`
  Title     string    `json:"title"`
  Body      string    `json:"body"`
  Published time.Time `json:"published"`
  Author    Author    `json:"author"`
}

type Author struct {
  FirstName string `json:"first_name"`
  LastName  string `json:"last_name"`
}

Сначала мы создадим клиент Elasticsearch, используя сторонний пакет cenkalti/backoff/ для реализации экспоненциальной отсрочки.

// Используйте сторонний пакет для реализации функции отсрочки
//
retryBackoff := backoff.NewExponentialBackOff()

es, err := elasticsearch.NewClient(elasticsearch.Config{
// Повторить попытку при 429 TooManyRequests статусе
//
RetryOnStatus: []int{502, 503, 504, 429},

// Настраиваем функцию отсрочки
//
RetryBackoff: func(i int) time.Duration {
  if i == 1 {
    retryBackoff.Reset()
  }
  return retryBackoff.NextBackOff()
},

// Повтор до 5 попыток
//
MaxRetries: 5,
})

Далее мы создадим массовый индексатор:

// Создаем BulkIndexer
//
bi, err := esutil.NewBulkIndexer(esutil.BulkIndexerConfig{
  Index:         indexName,        // Имя индекса по умолчанию
  Client:        es,               // Elasticsearch клиент
  NumWorkers:    numWorkers,       // Количество рабочих goroutines
  FlushBytes:    int(flushBytes),  // Порог записи в байтах
  FlushInterval: 30 * time.Second, // Интервал периодической записи
})

Создадим данные для индексации:

var articles []*Article
names := []string{"Alice", "John", "Mary"}
for i := 1; i <= numItems; i++ {
  articles = append(articles, &Article{
    ID:        i,
    Title:     strings.Join([]string{"Title", strconv.Itoa(i)}, " "),
    Body:      "Lorem ipsum dolor sit amet...",
    Published: time.Now().Round(time.Second).UTC().AddDate(0, 0, i),
    Author: Author{
      FirstName: names[rand.Intn(len(names))],
      LastName:  "Smith",
    },
  })
}

Примечание. Переменные indexName, numWorkers, flushBytes и numItems устанавливаются с помощью флагов командной строки; смотрите go run indexer.go --help.

Теперь мы можем перебрать коллекцию статей, добавляя каждый элемент в индексатор:

var countSuccessful uint64
start := time.Now().UTC()

for _, a := range articles {
  // Готовим полезные данные: кодируем статью в JSON
  //
  data, err := json.Marshal(a)
  if err != nil {
    log.Fatalf("Cannot encode article %d: %s", a.ID, err)
  }

  // Добавляем элемент в BulkIndexer
  //
  err = bi.Add(
    context.Background(),
    esutil.BulkIndexerItem{
      // Поле Action настраивает операцию для выполнения (index, create, delete, update)
      Action: "index",

      // DocumentID это (необязательный) идентификатор документа
      DocumentID: strconv.Itoa(a.ID),

      // Body это `io.Reader` с полезной нагрузкой
      Body: bytes.NewReader(data),

      // OnSuccess вызывается для каждой успешной операции
      OnSuccess: func(ctx context.Context, item esutil.BulkIndexerItem, res esutil.BulkIndexerResponseItem) {
        atomic.AddUint64(&countSuccessful, 1)
      },

      // OnFailure вызывается для каждой неудачной операции
      OnFailure: func(ctx context.Context, item esutil.BulkIndexerItem, res esutil.BulkIndexerResponseItem, err error) {
        if err != nil {
          log.Printf("ERROR: %s", err)
        } else {
          log.Printf("ERROR: %s: %s", res.Error.Type, res.Error.Reason)
        }
      },
    },
  )
  if err != nil {
    log.Fatalf("Unexpected error: %s", err)
  }
}

Индексатор будет отправлять элементы в кластер партиями в соответствии с настроенными пороговыми значениями. В нашем случае коллекция исчерпана, поэтому мы закроем индексатор, чтобы очистить все оставшиеся буферы:

if err := bi.Close(context.Background()); err != nil {
  log.Fatalf("Unexpected error: %s", err)
}

Индексатор собирает ряд показателей с помощью типа esutil.BulkIndexerStats:

$ go doc -short github.com/elastic/go-elasticsearch/v7/esutil.BulkIndexerStats
type BulkIndexerStats struct {
  NumAdded    uint64
  NumFlushed  uint64
  NumFailed   uint64
  NumIndexed  uint64
  NumCreated  uint64
  NumUpdated  uint64
  NumDeleted  uint64
  NumRequests uint64
}

Давайте воспользуемся им, чтобы отобразить простой отчет обо всей операции, используя пакет dustin/go-humanize для лучшей читаемости:

biStats := bi.Stats()
dur := time.Since(start)

if biStats.NumFailed > 0 {
  log.Fatalf(
    "Indexed [%s] documents with [%s] errors in %s (%s docs/sec)",
    humanize.Comma(int64(biStats.NumFlushed)),
    humanize.Comma(int64(biStats.NumFailed)),
    dur.Truncate(time.Millisecond),
    humanize.Comma(int64(1000.0/float64(dur/time.Millisecond)*float64(biStats.NumFlushed))),
  )
} else {
  log.Printf(
    "Sucessfuly indexed [%s] documents in %s (%s docs/sec)",
    humanize.Comma(int64(biStats.NumFlushed)),
    dur.Truncate(time.Millisecond),
    humanize.Comma(int64(1000.0/float64(dur/time.Millisecond)*float64(biStats.NumFlushed))),
  )
}

// => Successfully indexed [10,000] documents in 1.622s (6,165 docs/sec)

Этот пример иллюстрирует внутреннюю работу массового индексатора и доступные параметры конфигурации. Чтобы увидеть, как использовать его в реалистичном приложении, взгляните на пример _examples/bulk/kafka в репозитории. Он использует Docker для запуска полной среды с Zookeeper, Kafka, Confluent Control Center, Elasticsearch, APM Server и Kibana и демонстрирует получение данных, полученных из темы Kafka. Чтобы попробовать его локально, просто следуйте инструкциям в репозитории.


Читайте также:


среда, 5 мая 2021 г.

Клиент Go для Elasticsearch: кодирование и декодирование полезной нагрузки JSON

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

Самый простой вариант - просто использовать пакет encoding/json из стандартной библиотеки для декодирования ответа в map[string]interface{} или настраиваемый тип структуры:

var r map[string]interface{}

res, _ := es.Search(es.Search.WithTrackTotalHits(true))
json.NewDecoder(res.Body).Decode(&r)

fmt.Printf(
  "[%s] %d hits; took: %dms\n",
  res.Status(),
  int(r["hits"].(map[string]interface{})["total"].(map[string]interface{})["value"].(float64)),
  int(r["took"].(float64)),
)

// => [200 OK] 1 hits; took: 10ms

Несмотря на простоту, этот вариант далеко не самый удобный или эффективный: обратите внимание, как вам нужно преобразовать каждую часть структуры, чтобы сделать значение полезным в вашем коде. Есть способы получше.

Если все, что вас интересует, это получение пары значений из ответа и их использование или отображение, привлекательным вариантом является использование пакета tidwall/gjson. Это позволяет вам использовать "точечную нотацию", чтобы легко и более эффективно "извлекать" значения из ответа:

var b bytes.Buffer

res, _ := es.Search(es.Search.WithTrackTotalHits(true))
b.ReadFrom(res.Body)

values := gjson.GetManyBytes(b.Bytes(), "hits.total.value", "took")
fmt.Printf(
  "[%s] %d hits; took: %dms\n",
  res.Status(),
  values[0].Int(),
  values[1].Int(),
)
// => [200 OK] 1 hits; took: 10ms

Еще один вариант, особенно для более сложной кодовой базы, - использовать такой пакет, как mailru/easyjson, который использует генерацию кода для эффективного кодирования и декодирования полезной нагрузки JSON в настраиваемые типы структур - соответствующий пример и связанную с ним папку модели.

Примечание. Запустите бенчмарки в своей собственной среде, чтобы сравнить производительность различных пакетов JSON.

Когда дело доходит до кодирования (сериализации) тела запроса, самый простой вариант - использовать тип, поддерживающий интерфейс io.Reader, например bytes.Buffer:

var b bytes.Buffer
b.WriteString(`{"title" : "`)
b.WriteString("Test")
b.WriteString(`"}`)

res, _ := es.Index("test", &b)
fmt.Println(res)
// => [201 Created] {"_index":"test","_id":"uFeRWXQBeb...

Поскольку структуры кодирования или значения map[string]interface{} встречаются очень часто, пакет esutil предоставляет помощник, который выполняет сериализацию и преобразование в io.Reader, поэтому эквивалент приведенного выше кода будет выглядеть следующим образом:

type MyDocument struct {
  Title string `json:"title"`
}

doc := MyDocument{Title: "Test"}

res, _ := es.Index("test", esutil.NewJSONReader(&doc))
fmt.Println(res)
// [201 Created] {"_index":"test","_id":"wleUWXQBe...

Примечание. Помощник хорошо работает с пользовательскими кодировщиками JSON. Если тип реализует интерфейс esutil.JSONEncoder, автоматически используется метод EncodeJSON(); в противном случае он возвращается к стандартной библиотеке.

Чтобы понять, как использовать клиент в обычном приложении, потратьте некоторое время на ознакомление с исчерпывающим примером xkcdsearch. Он индексирует информацию из JSON API и позволяет искать ее в командной строке и в браузере. Он демонстрирует несколько методов, таких как встраивание клиента в собственный тип, построение запросов, анализ ответов, выделение совпадающих фраз в результатах, имитация клиента для тестов и многое другое. Вы можете предварительно просмотреть приложение в Интернете.


Читайте также:


Клиент Go для Elasticsearch: конфигурация и кастомизация

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

Конструктор клиента Go принимает тип elasticsearch.Config{}, который предоставляет ряд параметров для управления поведением:

$ go doc -short github.com/elastic/go-elasticsearch/v7.Config
type Config struct {
  Addresses []string // Список используемых узлов Elasticsearch.
  // ...
}

Рассмотрим доступные варианты и примеры их использования.

Конечные точки и безопасность

Первое, что вы можете сделать, если вы просто не экспериментируете с клиентом локально, - это указать ему на удаленный кластер. Самый простой способ сделать это - экспортировать переменную ELASTICSEARCH_URL со списком URL-адресов узлов, разделенных запятыми. Это соответствует рекомендации Двенадцати факторов, полностью исключает конфигурацию из кодовой базы и хорошо работает с облачными функциями/лямбда-выражениями и системами оркестровки контейнеров, такими как Kubernetes.

// В main.go
es, err := elasticsearch.NewDefaultClient()
// ...

// В командной строке
$ ELASTICSEARCH_URL=https://foo:bar@es1:9200 go run main.go

// Для Google Cloud Function
$ gcloud functions deploy myfunction --set-env-vars ELASTICSEARCH_URL=https://foo:bar@es1:9200 ...

Чтобы настроить конечные точки кластера напрямую (например, когда вы загружаете их из файла конфигурации или извлекаете их из системы управления секретами, такой как Vault), используйте поле Addresses со срезом строк с URL-адресами узлов, к которым вы хотите подключиться, и поля имени пользователя и пароля для аутентификации:

var (
  clusterURLs = []string{"https://es1:9200", "https://es2:9200", "https://es3:9200"}
  username    = "foo"
  password    = "bar"
)
cfg := elasticsearch.Config{
  Addresses: clusterURLs,
  Username:  username,
  Password:  password,
}
es, err := elasticsearch.NewClient(cfg)
// ...

Используйте поле APIKey для аутентификации с помощью ключей API, которыми проще управлять через Elasticsearch API или Kibana, чем через имена пользователей и пароли.

При использовании Elasticsearch Service в Elastic Cloud вы можете указать клиенту на кластер, используя Cloud ID:

cfg := elasticsearch.Config{
  CloudID: "my-cluster:dXMtZWFzdC0xLZC5pbyRjZWM2ZjI2MWE3NGJm...",
  APIKey:  "VnVhQ2ZHY0JDZGJrUW...",
}
es, err := elasticsearch.NewClient(cfg)
// ...

Примечание. Не забывайте, что вам все равно необходимо предоставить учетные данные для аутентификации при использовании Cloud ID.

Обычная потребность в настраиваемых развертываниях или демонстрациях, ориентированных на безопасность, - предоставить клиенту центр сертификации, чтобы он мог проверить сертификат сервера. Это может быть достигнуто несколькими способами, но наиболее простой вариант - использовать поле CACert, передав ему часть байтов с полезной нагрузкой сертификата:

cert, _ := ioutil.ReadFile("path/to/ca.crt")
cfg := elasticsearch.Config{
  // ...
  CACert: cert,
}
es, err := elasticsearch.NewClient(cfg)
// ...

Взгляните на папку _examples/security репозитория, чтобы увидеть полную демонстрацию, в которой вы можете генерировать пользовательские сертификаты с помощью утилиты командной строки elasticsearch-certutil и запускать кластер с соответствующей конфигурацией.

Глобальные заголовки

В определенных сценариях, например, при использовании аутентификации на основе токенов или при взаимодействии с прокси-серверами, вам может потребоваться добавить заголовки HTTP к клиентским запросам. Хотя вы можете добавлять их в вызовы API, используя метод WithHeader(), удобнее устанавливать их глобально, в конфигурации клиента:

cfg := elasticsearch.Config{
  // ...
  Header: http.Header(map[string][]string{
    "Authorization": {"Bearer dGhpcyBpcyBub3QgYSByZWFs..."},
  }),
}
es, err := elasticsearch.NewClient(cfg)
// ...

Ведение журнала и метрики

Во время разработки крайне важно внимательно следить за тем, что отправляется в кластер Elasticsearch и что он получает. Самый простой способ добиться этого - просто распечатать детали запроса и ответа на консоли или в файле. Клиентский пакет Go предоставляет несколько отдельных компонентов регистратора. Для отладки во время разработки, возможно, наиболее полезен estransport.ColorLogger - он выводит на консоль сжатую, отформатированную и раскрашенную информацию:

cfg := elasticsearch.Config{
  Logger: &estransport.ColorLogger{Output: os.Stdout},
}
es, _ := elasticsearch.NewClient(cfg)
es.Info()
// > GET http://localhost:9200/ 200 OK 11ms

По умолчанию тело запроса и ответа не печатается - для этого установите соответствующие параметры регистратора:

cfg := elasticsearch.Config{
  Logger: &estransport.ColorLogger{
    Output:             os.Stdout,
    EnableRequestBody:  true,
    EnableResponseBody: true,
  },
}
es, _ := elasticsearch.NewClient(cfg)
es.Info()
// > GET http://localhost:9200/ 200 OK 6ms
// >     « {
// >     «   "name" : "es1",
// >     «   "cluster_name" : "go-elasticsearch",
// >     ...

Примечание. Компонент estransport.TextLogger печатает информацию без использования каких-либо специальных символов и цветов.

Обращаясь за помощью на форумах, часто бывает полезно представить список операций в независимом формате, чтобы их можно было легко "воспроизвести" локально, без понимания или установки определенного языка программирования. Идеальный способ сделать это - отформатировать вывод как последовательность исполняемых команд curl с помощью estransport.CurlLogger:

cfg := elasticsearch.Config{
  Logger: &estransport.CurlLogger{
    Output:             os.Stdout,
    EnableRequestBody:  true,
    EnableResponseBody: true,
  },
}
es, _ := elasticsearch.NewClient(cfg)
es.Index(
  "test",
  strings.NewReader(`{"title" : "logging"}`),
  es.Index.WithRefresh("true"),
  es.Index.WithPretty(),
  es.Index.WithFilterPath("result", "_id"),
)
// > curl -X POST -H 'Content-Type: application/json' 'http://localhost:9200/test/_doc?pretty&filter_path=result%2C_id&refresh=true' -d \
// > '{
// > "title": "logging"
// > }'
// > # => 2020-07-23T13:12:05Z [201 Created] 65ms
// > # {
// > #  "_id": "_YPNe3MBdF-KdkKEZqF_",
// > #  "result": "created"
// > # }

При регистрации клиентских операций в производственной среде вывод в виде обычного текста не подходит, потому что вы хотите хранить журналы в виде структурированных данных, вполне возможно, в самом Elasticsearch. В этом случае вы можете использовать estransport.JSONLogger для вывода записей в виде документов JSON в формате, совместимом с Elastic Common Schema (ECS):

cfg := elasticsearch.Config{
  Logger: &estransport.JSONLogger{Output: os.Stdout},
}
es, _ := elasticsearch.NewClient(cfg)
es.Info()
// > {"@timestamp":"2020-07-23T13:12:05Z","event":{"duration":10970000},"url":{"scheme":"http","domain":"localhost","port":9200,"path":"/","query":""},"http":{"request":{"method":"GET"},"response":{"status_code":200}}}

В соответствии с духом расширяемости клиента все перечисленные регистраторы являются всего лишь реализациями интерфейса estransport.Logger. Чтобы использовать настраиваемый регистратор, просто реализуйте этот интерфейс:

$ go doc -short github.com/elastic/go-elasticsearch/v7/estransport.Logger
type Logger interface {
  LogRoundTrip(*http.Request, *http.Response, error, time.Time, time.Duration) error
  // ...
}

Пример _examples/logging/custom.go демонстрирует, как использовать пакет github.com/rs/zerolog в качестве "драйвера" журналирования путем реализации интерфейса для типа CustomLogger.

Еще одна функция клиента, полезная в производстве или для отладки, - это возможность экспортировать различные метрики о себе: количество запросов и сбоев, коды состояния ответа и подробные сведения о соединениях. Установите для параметра EnableMetrics значение true и используйте метод Metrics() для получения информации. Пример _examples/instrumentation/expvar.go показывает интеграцию с пакетом expvar.

Повторные попытки

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

По умолчанию клиент повторяет запрос до трех раз; чтобы установить другой предел, используйте поле MaxRetries. Чтобы изменить список кодов состояния ответа, которые следует повторить, используйте поле RetryOnStatus. Вместе с опцией RetryBackoff вы можете использовать его для повторения запросов, когда сервер отправляет ответ 429 Too Many Requests:

cfg := elasticsearch.Config{
  RetryOnStatus: []int{429, 502, 503, 504},
  RetryBackoff:  func(i int) time.Duration {
    // Простая экспоненциальная задержка
    d := time.Duration(math.Exp2(float64(i))) * time.Second
    fmt.Printf("Attempt: %d | Sleeping for %s...\n", i, d)
    return d
  },
}
es, err := elasticsearch.NewClient(cfg)
// ...

Поскольку функция RetryBackoff возвращает только time.Duration, вы можете обеспечить более надежную реализацию отката, используя сторонний пакет, такой как github.com/cenkalti/backoff:

import "github.com/cenkalti/backoff/v4"

retryBackoff := backoff.NewExponentialBackOff()
retryBackoff.InitialInterval = time.Second

cfg := elasticsearch.Config{
  RetryOnStatus: []int{429, 502, 503, 504},
  RetryBackoff: func(i int) time.Duration {
    if i == 1 {
      retryBackoff.Reset()
    }
    d := retryBackoff.NextBackOff()
    fmt.Printf("Attempt: %d | Sleeping for %s...\n", i, d)
    return d
  },
}
es, err := elasticsearch.NewClient(cfg)
// ...

Обнаружение узлов

Параметры DiscoverNodesOnStart и DiscoverNodesInterval определяют, должен ли клиент выполнять обнаружение узлов ("прослушивание") во время инициализации или периодически; оба отключены по умолчанию.

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

Поле ConnectionPoolFunc позволяет предоставить настраиваемую реализацию пула соединений, которая удовлетворяет интерфейсу estransport.ConnectionPool. Однако это обычно полезно только в сложных топологиях сети.

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

Наиболее инвазивным вариантом конфигурации является поле Transport, которое позволяет полностью заменить HTTP-клиент по умолчанию, используемый пакетом, а именно http.DefaultTransport. Возможно, вы захотите сделать это в ситуациях, когда вы хотите настроить таймауты, параметры TLS или прокси-сервера или любые другие низкоуровневые детали HTTP:

cfg := elasticsearch.Config{
  // ...
  Transport: &http.Transport{
    Proxy: ...
    MaxIdleConnsPerHost:   ...,
    ResponseHeaderTimeout: ...,
    DialContext:           (&net.Dialer{
      Timeout:   ...,
      KeepAlive: ...,
    }).DialContext,
    TLSClientConfig: &tls.Config{
      MinVersion: ...,
      // ...
    },
  },
}
es, err := elasticsearch.NewClient(cfg)
// ...

Кастомный транспорт

Поскольку поле Transport принимает любую реализацию http.RoundTripper, можно передать настраиваемую реализацию. Давайте посмотрим на пример, в котором мы подсчитываем количество запросов, следуя демонстрации _examples/customization.go:

type CountingTransport struct {
  count uint64
}

func (t *CountingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  atomic.AddUint64(&t.count, 1)
  return http.DefaultTransport.RoundTrip(req)
}

tp := CountingTransport{}
cfg := elasticsearch.Config{Transport: &tp}
es, err := elasticsearch.NewClient(cfg)
// ...
fmt.Printf("%80s\n", fmt.Sprintf("Total Requests: %d", atomic.LoadUint64(&tp.count)))

Как правило, нет необходимости заменять HTTP-клиент по умолчанию на пользовательскую реализацию, за одним конкретным исключением: имитация клиента в модульных тестах. В следующем примере тип mockTransport определяет поле RoundTripFunc, которое позволяет возвращать конкретный ответ для определенных тестов.

// mockTransport определяет фиктивный транспорт для модульных тестов
type mockTransport struct {
  RoundTripFunc func(req *http.Request) (*http.Response, error)
}

// RoundTripFunc реализует http.RoundTripper
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  return t.RoundTripFunc(req)
}

func TestClientSuccess(t *testing.T) {
  tp := &mockTransport{
    RoundTripFunc: func(req *http.Request) (*http.Response, error) {
      // Возвращаем успешный фиктивный ответ
      return &http.Response{
        Status:     "200 OK",
        StatusCode: 200,
        Body:       ioutil.NopCloser(strings.NewReader("HELLO")),
      }, nil
    },
  }

  cfg := elasticsearch.Config{Transport: &tp}
  es, _ := elasticsearch.NewClient(cfg)
  res, _ := es.Info()

  t.Log(res)
}

Фиктивный ответ распечатывается при выполнении теста:

go test -v tmp/client_mocking_test.go
// === RUN   TestClientSuccess
//     TestClientSuccess: client_mocking_test.go:42: [200 OK] HELLO
// --- PASS: TestClientSuccess (0.00s)
// ...

Примечание. В определенных ситуациях желательно заменить HTTP-клиент из стандартной библиотеки на более производительный, например github.com/valyala/fasthttp, который имеет заметную разницу в производительности. Запустите тесты в _examples/fasthttp, чтобы измерить разницу в вашей собственной среде.


Читайте также:


воскресенье, 2 мая 2021 г.

Клиент Go для Elasticsearch: пакет estransport

До сих пор мы были озабочены первой обязанностью клиента: раскрытием API-интерфейсов Elasticsearch как типов на языке программирования Go. Давайте переключимся на компонент, отвечающий за отправку и получение данных по сети: пакет estransport.

Основным типом, предоставляемым пакетом, является estransport.Client, который реализует estransport.Interface. Он определяет единственный метод Perform(), который принимает *http.Request и возвращает *http.Response:

$ go doc -short github.com/elastic/go-elasticsearch/v7/estransport.Interface
type Interface interface {
  Perform(*http.Request) (*http.Response, error)
}

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

Прежде чем клиент сможет отправить запрос в кластер, он сначала должен знать, куда его отправить. Для локальной разработки это довольно просто: вы просто сохраняете значение по умолчанию (http://localhost:9200) или настраиваете клиент с одним адресом. В этом случае используется "единичный" пул соединений, который возвращает только одно соединение. Это также относится к использованию Elasticsearch Service в Elastic Cloud, которое предоставляет только одну конечную точку для кластера, поскольку имеет собственную логику балансировки нагрузки. И то же самое, естественно, применимо к любому кластеру за балансировщиком нагрузки или прокси.

Однако локальные производственные кластеры работают с несколькими узлами, и было бы неоптимально отправлять запросы только на один узел, что делает его узким местом. Вот почему пакет экспортирует интерфейс estransport.ConnectionPool, который позволяет выбрать соединение из списка:

$ go doc -short github.com/elastic/go-elasticsearch/v7/estransport.ConnectionPool
type ConnectionPool interface {
  Next() (*Connection, error)  // Next возвращает следующее доступное соединение.
  OnSuccess(*Connection) error // OnSuccess сообщает, что соединение было успешным.
  OnFailure(*Connection) error // OnFailure сообщает, что соединение не удалось.
  URLs() []*url.URL            // URLs возвращает список URL-адресов доступных подключений.
}
...

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

Метод Next() пула соединений "статуса" делегирует еще один интерфейс: estransport.Selector, с реализацией по умолчанию циклического селектора, который обычно является наиболее эффективным способом распределения нагрузки между узлами кластера. Опять же, пользовательская реализация селектора может быть передана в конфигурации, когда в сложных топологиях сети требуется более сложный механизм выбора соединения.

Например, упрощенная реализация селектора "hostname" может выглядеть так:

func (s *HostnameSelector) Select(conns []*estransport.Connection) (*estransport.Connection, error) {
  // Блокировка доступа снята

  var filteredConns []*estransport.Connection

  for _, c := range conns {
    if strings.Contains(c.URL.String(), "es1") {
      filteredConns = append(filteredConns, c)
    }
  }

  if len(filteredConns) > 0 {
    s.current = (s.current + 1) % len(filteredConns)
    return filteredConns[s.current], nil
  }

  return nil, errors.New("No connection with hostname [es1] available")
}

Однако настраиваемый селектор более полезен с другой функцией клиента: способностью обнаруживать узлы в кластере, в просторечии известной как "сниффинг". Он использует Nodes Info API для получения информации об узлах в кластере и динамически обновляет конфигурацию клиента в зависимости от состояния кластера. Это позволяет вам, например, указывать клиенту только на координирующие узлы кластера и позволяет ему автоматически обнаруживать данные или принимать узлы.

Клиент Go предоставляет метод DiscoverNodes() для выполнения операции вручную, а также параметры конфигурации DiscoverNodesInterval и DiscoverNodesOnStart для выполнения операции при инициализации клиента и через определенные промежутки времени. Помимо получения списка узлов и создания динамической конфигурации клиента, он также хранит метаданные, прикрепленные к узлам, такие как роли или атрибуты.

Еще одна полезная функция транспортного компонента - это возможность повторить неудачный запрос, что особенно важно в распределенной системе, такой как Elasticsearch. По умолчанию он повторяет запрос до трех раз при получении сетевой ошибки или ответа HTTP с кодом состояния 502, 503 или 504. Количество попыток повторения и коды ответа HTTP настраиваются вместе с дополнительной задержкой отсрочки. Эту функцию можно вообще отключить.


Читайте также:


суббота, 1 мая 2021 г.

Клиент Go для Elasticsearch: пакет esapi

Пакет esapi обеспечивает доступ к API-интерфейсам Elasticsearch через типы данных языка программирования Go. Например, чтобы проиндексировать документ, вы вызываете соответствующий метод на клиенте:

res, err := client.Index(
  "my-index",
  strings.NewReader(`{"title":"Test"}`),
  client.Index.WithDocumentID("1"))
fmt.Println(res, err)

Пакет Go предоставляет тот же API, что и клиенты на других языках, обеспечивая единообразие взаимодействия с пользователем на разных языках программирования и облегчая общение в командах полиглотов. Следовательно, различные пространства имен API Elasticsearch доступны в качестве пространств имен на клиенте. Например, чтобы проверить работоспособность кластера, вы вызываете метод Cluster.Health() на клиенте; чтобы создать индекс, вы вызываете метод Indices.Create().

Метод возвращает esapi.Response и error. Ошибка возвращается всякий раз, когда запрос терпит неудачу; например, когда конечная точка недоступна или время ожидания запроса истекло. Тип esapi.Response - это легкая оболочка для *http.Response. Помимо отображения статуса ответа, заголовков и тела, он предоставляет несколько вспомогательных методов, таких как IsError(). Обратите внимание, что ответ 500 Internal Server Error по-прежнему является допустимым ответом, и поэтому в этом случае ошибка не возвращается - вызывающий код должен проверить статус ответа, чтобы правильно обработать ответ. Тип esapi.Response также реализует интерфейс fmt.Stringer, позволяющий печатать ответ во время разработки и отладки, как показано в примере выше.

Если вы присмотритесь, вы обнаружите, что вызов метода создает новый экземпляр структуры IndexRequest и вызывает его метод Do(). Вполне возможно создать экземпляр и вызвать метод самостоятельно - соответствующий код будет выглядеть так:

req := esapi.IndexRequest{
  Index:      "my-index",
  DocumentID: "1",
  Body:       strings.NewReader(`{"title":"Test"}`),
}
req.Do(context.Background(), client)

Оба варианта функционально эквивалентны. API-интерфейс, ориентированный на методы, имеет незначительные накладные расходы, но также имеет определенные преимущества.

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

Не менее важно, что он четко определяет, какие параметры API требуются (в приведенном выше примере это имя индекса и полезная нагрузка JSON), а какие - необязательные (идентификатор документа). Сравните с API Create(), который в сигнатуре метода дает понять, что требуется идентификатор документа:

$ go doc -short github.com/elastic/go-elasticsearch/v7/esapi.Create
type Create func(index string, id string, body io.Reader, o ...func(*CreateRequest)) (*Response, error)
...

Это особенно полезно для разработчиков, использующих автозавершение кода в своих IDE и редакторах.

Еще одно удобство, обеспечиваемое API-интерфейсом, ориентированным на методы, связано с параметрами, принимающими логические и числовые значения. Поскольку все типы имеют значение по умолчанию в Go, пакет не сможет определить, установлено ли значение false вызывающим кодом или это просто значение по умолчанию для типа bool; аналогичная проблема существует с типом int и значением 0. Это обычно решается в Go путем принятия указателя на значение в качестве аргумента, но это делает вызывающий код довольно многословным.

Следовательно, вместо того, чтобы объявлять переменную и передавать на нее указатель, вызывающий код может просто передать значение соответствующему методу, и он автоматически создаст указатель:

client.Reindex(
  strings.NewReader(`{...}`),
  client.Reindex.WithRequestsPerSecond(100),
  client.Reindex.WaitForCompletion(true),
)

Примечание. Пакет объявляет вспомогательные функции esapi.BoolPtr() и esapi.IntPtr(), чтобы упростить использование полей, принимающих указатель, при прямом использовании структур запроса.

С более чем 300 различными, постоянно развивающимися API-интерфейсами Elasticsearch практически невозможно управлять базой кода вручную - единственный устойчивый режим обслуживания - это полностью сгенерированная база кода. К счастью, репозиторий Elasticsearch содержит исчерпывающую спецификацию для каждого API в виде набора документов JSON. Это основной инструмент для обеспечения согласованности между клиентами и для того, чтобы идти в ногу с эволюцией API Elasticsearch.

Генератор Go, который является частью набора внутренних пакетов, переводит определение API из JSON в исходный код Go, генерирует отдельные файлы, форматирует их с помощью gofmt и создает файл с типом esapi.API с "картой" всех API, используя отражение в Go. Затем этот тип встраивается в клиент.

Как отмечалось выше, одной из задач клиента Elasticsearch является отправка и получение данных, по умолчанию это полезная нагрузка JSON. Клиент Go представляет тело запроса и ответа просто как io.Reader, оставляя все кодирование и декодирование вызывающему коду. Для такой реализации есть несколько причин. Прежде всего, нет формальной спецификации для чрезвычайно изменчивой полезной нагрузки API, и поэтому о генерации кода не может быть и речи.

Примечание: предпринимаются многочисленные попытки улучшить эту ситуацию с целью сделать возможным создание полностью сгенерированного API, например, результатов поиска, компонентов Query DSL и т. д.

Другая причина связана с производительностью и расширяемостью. Оставляя кодирование и декодирование вызывающему коду, между ним и клиентом существует четкая граница, что упрощает рассуждения о производительности. Опытным путем кодирование и декодирование JSON имеет наибольшее влияние на производительность клиента, обычно выше, чем стоимость передачи по сети. Еще с одной точки зрения, передав кодирование и декодирование вызывающему коду, можно легко использовать сторонние пакеты JSON, которые в большинстве случаев значительно превосходят стандартную библиотеку. Чтобы увидеть примеры и тесты для различных пакетов JSON смотрите папку _examples/encoding в репозитории.

Примечание. Чтобы упростить передачу пользовательских полезных данных в API, клиент предоставляет тип esutil.JSONReader.


Читайте также:


Клиент Go для Elasticsearch

Официальный клиент Go для Elasticsearch - одно из последних дополнений к семейству клиентов, разработанных и поддерживаемых Elastic. Первоначальная версия была опубликована в начале 2019 года и за год получила развитие, получив такие функции, как повторные запросы, обнаружение узлов кластера и различные вспомогательные компоненты. Также предоставлены примеры, чтобы облегчить использование клиента.

В серии постов мы рассмотрим архитектуру и дизайн клиента Go, выделим конкретные детали реализации и предоставим примеры и рекомендации по его использованию.

В этом посте блога мы сосредоточимся на общей архитектуре клиента.

Клиентская архитектура

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

  • Предоставление API-интерфейсов Elasticsearch на соответствующем языке программирования
  • Отправка и получение данных из кластера

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

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

$ go list github.com/elastic/go-elasticsearch/v7/...
github.com/elastic/go-elasticsearch/v7
github.com/elastic/go-elasticsearch/v7/esapi
github.com/elastic/go-elasticsearch/v7/estransport
...

Пакеты esapi и estransport напрямую соответствуют двум упомянутым выше проблемам.

Это имеет серьезные последствия для ремонтопригодности, расширяемости и гибкости клиента. Во-первых, просто разделить код, тесты и другие эксперименты, относящиеся только к одной задаче; если вы посмотрите на модульные и интеграционные тесты estransport, становится очевидным, что они вообще не имеют отношения к Elasticsearch API. Во-вторых, просто использовать изолированно только один пакет: например, только пакет, связанный с API. Распространенный вопрос: "Зачем кому-то это делать?" Правильный ответ - это шаг назад: пакет не хочет принимать решения, которые мешают пользователям достичь определенной цели, независимо от того, насколько они редки или необычны.

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

Примечание. Вы можете задаться вопросом, почему пакеты не называются просто api и transport. Мотивация состоит в том, чтобы предотвратить "кражу" законного пакета или имени переменной у пользователя - в конце концов, пакет или переменная с именем api встречается довольно часто.

Как же тогда эти пакеты связаны вместе? Это ответственность зонтичного пакета elasticsearch.

$ go doc -short github.com/elastic/go-elasticsearch/v7
...
type Client struct{ ... }
    func NewClient(cfg Config) (*Client, error)
    func NewDefaultClient() (*Client, error)
type Config struct{ ... }

Размер этого пакета намеренно невелик, а его наиболее важными компонентами являются типы Client и Config; последний предоставляет способ настройки клиента, а первый включает API-интерфейсы Elasticsearch и транспорт HTTP.

$ go doc -short github.com/elastic/go-elasticsearch/v7.Client
type Client struct {
  *esapi.API // Встраивает методы API
  Transport  estransport.Interface
}
func NewClient(cfg Config) (*Client, error)
func NewDefaultClient() (*Client, error)
...

Пакет экспортирует методы инициализации клиента: NewDefaultClient() и NewClient().


Читайте также: