Статьи

Go Microservices: Часть 8. Централизованная настройка с помощью Viper и Spring Cloud Config

Централизация чего-либо при работе с микросервисами может показаться немного странной, учитывая, что микросервисы, в конце концов, предназначены для декомпозиции вашей системы на отдельные независимые части программного обеспечения. Однако то, что мы обычно ищем, это изоляция процессов. Другие аспекты микросервисных операций должны решаться централизованно. Например, журналы должны заканчиваться вашим решением для ведения журналов, таким как стек ELK , мониторинг включается в выделенный мониторинг — в этой части серии блогов мы будем иметь дело с внешней и централизованной конфигурацией с использованием Spring Cloud Config и git.

Управление конфигурацией для различных микросервисов, из которых состоит наше приложение, централизованно, на самом деле также вполне естественно. Особенно при работе в контейнерной среде на неизвестном количестве базовых аппаратных узлов управление файлами конфигурации, встроенными в каждый образ микросервиса или из подключенных томов, может быстро стать настоящей головной болью. Есть несколько проверенных проектов, помогающих справиться с этим, например, etcd , consul и zookeeper . Однако следует отметить, что эти проекты предоставляют намного больше, чем просто обслуживание конфигурации. Поскольку эта серия блогов посвящена интеграции микросервисов Go с экосистемой поддерживающих сервисов Spring Cloud / Netflix OSS, мы будем основывать нашу централизованную конфигурацию наSpring Cloud Configuration , часть программного обеспечения, предназначенная именно для этого.

Spring Cloud Config

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

  • Поддержка нескольких различных конфигурационных бэкэндов, таких как git (по умолчанию), файловые системы и плагины для использования etcd , consul и zookeeper в качестве хранилищ.
  • Прозрачная расшифровка зашифрованных свойств.
  • Сменная безопасность
  • Механизм push с использованием git hooks / REST API и Spring Cloud Bus (например, RabbitMQ) для распространения изменений в конфигурационных файлах на сервисы, что делает возможным перезагрузку конфигурации в реальном времени.

Более подробную статью о Spring Cloud Config, в частности, можно найти в недавнем сообщении моего коллеги Магнуса .

В этой записи блога мы интегрируем наш «аккаунт-сервис» с сервером Spring Cloud Config, поддерживаемым общедоступным git-репозиторием на GitHub, из которого мы получим конфигурацию, зашифруем / расшифруем свойство, а также осуществим динамическую перезагрузку свойств конфигурации.

Вот простой обзор общего решения, к которому мы стремимся:

configserver.png

обзор

Поскольку мы запускаем Docker в режиме Swarm, мы продолжим использовать механику Docker различными способами. Внутри Swarm мы должны запустить хотя бы один (желательно несколько) экземпляров серверов Spring Cloud Configuration. Когда один из наших микросервисов запускается, все, что им нужно знать, это следующее:

  • Логическое имя службы и порт сервера конфигурации. Т.е. мы развертываем наши серверы конфигурации на Docker Swarm в качестве сервисов, скажем, мы называем этот сервис «configserver». Это означает, что это единственное, что микросервисы должны знать об адресации, чтобы сделать запрос на ее настройку.
  • Как их зовут, например, «accountservice».
  • В каком профиле выполнения он работает, например, «dev», «test» или «prod». Если вы знакомы с концепцией spring.profiles.active , это отечественный аналог, который мы можем использовать для Go.
  • Если мы используем git в качестве бэкэнда и хотим получить конфигурацию из определенной ветки, это нужно знать заранее. (Необязательный)

Учитывая четыре приведенных выше критерия, пример запроса GET для конфигурации может выглядеть следующим образом в коде Go:

resp, err := http.Get("http://configserver:8888/accountservice/dev/P8")

то есть:

protocol://url:port/applicationName/profile/branch

Настройка сервера конфигурации Spring Cloud в вашем Swarm

В части 8 вы, вероятно, захотите клонировать ветвь P8, поскольку она содержит исходный код для сервера конфигурации:

git clone https://github.com/callistaenterprise/goblog.git
git checkout P8

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

Как правило, каждый необходимый компонент поддержки будет либо простым Dockerfile для удобного создания и развертывания компонентов, которые мы можем использовать «из коробки», либо это будет (java) исходный код и конфигурация (приложения Spring Cloud обычно основаны на Spring Boot), который мы Придется строить себя с помощью gradle. (Не беспокойтесь, все, что вам нужно, это установить JDK).

(Большинство из этих приложений Spring Cloud было подготовлено моим коллегой Магнусом для его серии блогов по микросервисам .)

Давайте начнем с сервера конфигурации, хорошо?

RabbitMQ

Какая? Разве мы не собирались устанавливать сервер Spring Cloud Configuration? Хорошо — эта часть программного обеспечения зависит от наличия брокера сообщений для распространения изменений конфигурации с использованием Spring Cloud Bus при поддержке RabbitMQ. В любом случае иметь RabbitMQ — это очень хорошая вещь, которую мы будем использовать в следующем посте в блоге, поэтому начнем с того, что RabbitMQ будет запущен в качестве службы в нашем Swarm.

Я подготовил Dockerfile внутри / goblog / support / rabbitmq, чтобы использовать предварительно запеченный образ, который мы развернем как сервис Docker Swarm.

Мы создадим новый скрипт bash (.sh), чтобы автоматизировать его для нас, если / когда нам нужно что-то обновить.

В папке root / goblog создайте новый файл support.sh :

#!/bin/bash

# RabbitMQ
docker service rm rabbitmq
docker build -t someprefix/rabbitmq support/rabbitmq/
docker service create --name=rabbitmq --replicas=1 --network=my_network -p 1883:1883 -p 5672:5672 -p 15672:15672 someprefix/rabbitmq

(Вам может понадобиться chmod, чтобы сделать его исполняемым)

Запустите его и подождите, пока Docker загрузит необходимые изображения и развернет RabbitMQ в вашем Swarm. Когда это будет сделано, вы сможете открыть RabbitMQ Admin GUI и войти в систему, используя guest / guest по адресу:

open http://$ManagerIP:15672/#/

Ваш веб-браузер должен открыться и отобразить что-то вроде этого: RabbitMQ

Если вы видите графический интерфейс администратора RabbitMQ, мы можем быть уверены, что он работает так, как рекламируется.

Spring Cloud Configuration Server

В / support / config-server вы найдете приложение Spring Boot, предварительно настроенное для запуска сервера конфигурации. Мы будем использовать репозиторий git для хранения и доступа к нашей конфигурации с использованием файлов yaml .

Не стесняйтесь взглянуть на /goblog/support/config-server/src/main/resources/application.yml, который является файлом конфигурации сервера конфигурации:

---
# For deployment in Docker containers
spring:
  profiles: docker
  cloud:
    config:
      server:
        git:
          uri: https://github.com/eriklupander/go-microservice-config.git

# Home-baked keystore for encryption. Of course, a real environment wouldn't expose passwords in a blog...          
encrypt:
  key-store:
    location: file:/server.jks
    password: letmein
    alias: goblogkey
    secret: changeme

# Since we're running in Docker Swarm mode, disable Eureka Service Discovery
eureka:
  client:
    enabled: false

# Spring Cloud Config requires rabbitmq, use the service name.
spring.rabbitmq.host: rabbitmq
spring.rabbitmq.port: 5672

Мы видим несколько вещей:

  • Мы говорим серверу конфигурации, чтобы он выбирал конфигурацию из нашего git-repo по указанному URI.
  • Хранилище ключей для шифрования (самоподписанное) и дешифрования (мы вернемся к этому)
  • Поскольку мы работаем в режиме Docker Swarm, Eureka Service Discovery отключен.
  • Конфигурационный сервер ожидает найти хост RabbitMQ по адресу «rabbitmq», который как раз и называется именем сервиса Docker Swarm, которое мы только что дали нашему сервису RabbitMQ.

Dockerfile для конфиг-сервера достаточно прост:

FROM davidcaste/alpine-java-unlimited-jce

EXPOSE 8888

ADD ./build/libs/*.jar app.jar
ADD ./server.jks /

ENTRYPOINT ["java","-Dspring.profiles.active=docker","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

(Не берите в голову java.security.egd , это обходной путь для проблемы, которая нас не волнует в этой серии блогов)

Несколько примечаний здесь:

  • Мы используем базовый образ Docker, основанный на Alpine Linux, с установленным расширением Java для неограниченной криптографии, это требование, если мы хотим использовать функции шифрования / дешифрования Spring Cloud Config.
  • Домашнее хранилище ключей добавляется в корневую папку образа контейнера.

Построить хранилище ключей

Чтобы использовать зашифрованные свойства позже, мы настроим на сервере конфигурации самозаверяющий сертификат. (Вам нужно иметь keytool на вашем PATH).

В папке / goblog / support / config-server / запустите:

keytool -genkeypair -alias goblogkey -keyalg RSA \
-dname "CN=Go Blog,OU=Unit,O=Organization,L=City,S=State,C=SE" \  
-keypass changeme -keystore server.jks -storepass letmein \
-validity 730

Это должно создать server.jks . Не стесняйтесь изменять любые свойства / пароли, просто не забудьте обновить application.yml соответственно!

Построить и развернуть

Время построить и развернуть сервер. Давайте создадим скрипт оболочки, чтобы сэкономить нам время, если или когда нам нужно будет сделать это снова. Помните — вам нужна Java Runtime Environment для создания этого! В папке / goblog создайте файл с именем springcloud.sh . Мы поместим все вещи, которые действительно нуждаются в создании (и это может занять некоторое время):

#!/bin/bash

cd support/config-server
./gradlew build
cd ../..
docker build -t someprefix/configserver support/config-server/
docker service rm configserver
docker service create --replicas 1 --name configserver -p 8888:8888 --network my_network --update-delay 10s --with-registry-auth  --update-parallelism 1 someprefix/configserver

Запустите его из папки / goblog ( сначала вам может понадобиться chmod + x):

> ./springcloud.sh

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

> docker service ls

ID                  NAME                MODE                REPLICAS            IMAGE
39d26cc3zeor        rabbitmq            replicated          1/1                 someprefix/rabbitmq
eu00ii1zoe76        viz                 replicated          1/1                 manomarks/visualizer:latest
q36gw6ee6wry        accountservice      replicated          1/1                 someprefix/accountservice
t105u5bw2cld        quotes-service      replicated          1/1                 eriklupander/quotes-service:latest
urrfsu262e9i        dvizz               replicated          1/1                 eriklupander/dvizz:latest
w0jo03yx79mu        configserver        replicated          1/1                 someprefix/configserver

Попробуйте вручную загрузить конфигурацию «accountservice» как JSON, используя curl:

     > curl http://$ManagerIP:8888/accountservice/dev/master
     {"name":"accountservice","profiles":["dev"],"label":"master","version":"b8cfe2779e9604804e625135b96b4724ea378736",
     "propertySources":[
        {"name":"https://github.com/eriklupander/go-microservice-config.git/accountservice-dev.yml",
        "source":
            {"server_port":6767,"server_name":"Accountservice DEV"}
        }]
     }

(Отформатировано для краткости)

Фактическая конфигурация хранится в свойстве «source», где все значения из файла .yml будут отображаться в виде пар ключ-значение. Загрузка и синтаксический анализ свойства «source» в удобную для использования конфигурацию в Go является центральным элементом этого сообщения в блоге.

Файлы конфигурации YAML

Перед тем, как перейти к коду Go, давайте заглянем в корневую папку ветви P8 конфигурации-репо :

accountservice-dev.yml
accountservice-test.yml

Оба эти файла в настоящее время очень малонаселены:

server_port: 6767
server_name: Accountservice TEST
the_password: (we'll get back to this one)

Единственное, что мы настраиваем на этом этапе, — это HTTP-порт, к которому мы хотим привязать наш сервис. Реальный сервис, вероятно, будет содержать гораздо больше материала.

Использование шифрования / дешифрования

Отличительной особенностью Spring Cloud Config является его встроенная поддержка прозрачного дешифрования значений, зашифрованных непосредственно в файлах конфигурации. Например, взгляните на accountservice-test.yml, где у нас есть фиктивное свойство «the_password»:

server_port: 6767
server_name: Accountservice TEST
the_password: '{cipher}AQB1BMFCu5UsCcTWUwEQt293nPq0ElEFHHp5B2SZY8m4kUzzqxOFsMXHaH7SThNNjOUDGxRVkpPZEkdgo6aJFSPRzVF04SXOVZ6Rjg6hml1SAkLy/k1R/E0wp0RrgySbgh9nNEbhzqJz8OgaDvRdHO5VxzZGx8uj5KN+x6nrQobbIv6xTyVj9CSqJ/Btf/u1T8/OJ54vHwi5h1gSvdox67teta0vdpin2aSKKZ6w5LyQocRJbONUuHyP5roCONw0pklP+2zhrMCy0mXhCJSnjoHvqazmPRUkyGcjcY3LHjd39S2eoyDmyz944TKheI6rWtCfozLcIr/wAZwOTD5sIuA9q8a9nG2GppclGK7X649aYQynL+RUy1q7T7FbW/TzSBg='

Приставляя зашифрованную строку к {cipher} , наш сервер конфигурации Spring Cloud будет знать, как автоматически расшифровывать значение для нас, прежде чем передать результат службе. В работающем экземпляре со всем, настроенным правильно, запрос curl к REST API для получения этой конфигурации вернет:

...
      "source": {
        "server_port": 6767,
        "server_name": "Accountservice TEST",
        "the_password": "password"
....

Довольно аккуратно, правда? Свойство «the_password» может храниться в виде зашифрованной строки в виде открытого текста на общедоступном сервере (если вы доверяете алгоритму шифрования и целостности ключа подписи) и на сервере Spring Cloud Config (который ни при каких обстоятельствах не может быть доступен незащищенным и / или видимый за пределами вашего внутреннего кластера !!) прозрачно расшифровывает свойство в фактическое значение «пароль».

Конечно, вам необходимо зашифровать значение, используя тот же ключ, который Spring Spring Config использует для расшифровки, что можно сделать через HTTP API сервера конфигурации:

curl http://$ManagerIP:8888/encrypt -d 'password'
AQClKEMzqsGiVpKx+Vx6vz+7ww00n... (rest omitted for brevity)

гадюка

Нашей платформой конфигурации на основе Go является Viper . Viper имеет хороший API для работы, он расширяемый и не мешает нашему обычному коду приложения. Хотя Viper не поддерживает загрузку конфигурации с серверов Spring Cloud Configuration, мы напишем небольшой фрагмент кода, который сделает это за нас. Viper также обрабатывает многие типы файлов в качестве источника конфигурации — например, JSON, YAML и простые файлы свойств. Viper также может читать переменные окружения из операционной системы для нас, что может быть довольно аккуратным. После инициализации и заполнения наша конфигурация всегда доступна с использованием различных функций viper.Get * . Очень удобно, действительно.

Помните картинку вверху этого поста? Ну, а если нет — вот оно снова

configserver.png

Мы заставим наши микросервисы выполнять HTTP-запрос при запуске, извлекать «исходную» часть ответа JSON и помещать его в Viper, чтобы мы могли получить там порт HTTP для нашего веб-сервера. Поехали!

Загрузка конфигурации

Как уже продемонстрировано с помощью curl, мы можем сделать простой HTTP-запрос на сервер конфигурации, где нам просто нужно знать наше имя и наш «профиль». Мы начнем с добавления некоторого разбора флагов в наш «accountservice» main.go, чтобы мы могли указать «профиль» среды при запуске, а также необязательный URI для сервера конфигурации:

var appName = "accountservice"

// Init function, runs before main()
func init() {
        // Read command line flags
        profile := flag.String("profile", "test", "Environment profile, something similar to spring profiles")
        configServerUrl := flag.String("configServerUrl", "http://configserver:8888", "Address to config server")
        configBranch := flag.String("configBranch", "master", "git branch to fetch configuration from")
        flag.Parse()

        // Pass the flag values into viper.
        viper.Set("profile", *profile)
        viper.Set("configServerUrl", *configServerUrl)
        viper.Set("configBranch", *configBranch)
}

func main() {
        fmt.Printf("Starting %v\n", appName)

        // NEW - load the config
        config.LoadConfigurationFromBranch(
                viper.GetString("configServerUrl"),
                appName,
                viper.GetString("profile"),
                viper.GetString("configBranch"))
        initializeBoltClient()
        service.StartWebServer(viper.GetString("server_port"))    // NEW, use port from loaded config 
}

Функция config.LoadConfigurationFromBranch (..) входит в новый пакет, который мы называем config . Создайте / goblog / accountservice / config и следующий файл с именем loader.go :

// Loads config from for example http://configserver:8888/accountservice/test/P8
func LoadConfigurationFromBranch(configServerUrl string, appName string, profile string, branch string) {
        url := fmt.Sprintf("%s/%s/%s/%s", configServerUrl, appName, profile, branch)
        fmt.Printf("Loading config from %s\n", url)
        body, err := fetchConfiguration(url)
        if err != nil {
                panic("Couldn't load configuration, cannot start. Terminating. Error: " + err.Error())
        }
        parseConfiguration(body)
}

// Make HTTP request to fetch configuration from config server
func fetchConfiguration(url string) ([]byte, error) {
        resp, err := http.Get(url)
        if err != nil {
                panic("Couldn't load configuration, cannot start. Terminating. Error: " + err.Error())
        }
        body, err := ioutil.ReadAll(resp.Body)
        return body, err
}

// Pass JSON bytes into struct and then into Viper
func parseConfiguration(body []byte) {
        var cloudConfig springCloudConfig
        err := json.Unmarshal(body, &cloudConfig)
        if err != nil {
                panic("Cannot parse configuration, message: " + err.Error())
        }

        for key, value := range cloudConfig.PropertySources[0].Source {
                viper.Set(key, value)
                fmt.Printf("Loading config property %v => %v\n", key, value)
        }
        if viper.IsSet("server_name") {
                fmt.Printf("Successfully loaded configuration for service %s\n", viper.GetString("server_name"))
        }
}

// Structs having same structure as response from Spring Cloud Config
type springCloudConfig struct {
        Name            string           `json:"name"`
        Profiles        []string         `json:"profiles"`
        Label           string           `json:"label"`
        Version         string           `json:"version"`
        PropertySources []propertySource `json:"propertySources"`
}

type propertySource struct {
        Name   string                 `json:"name"`
        Source map[string]interface{} `json:"source"`
}

По сути, мы выполняем HTTP GET для сервера конфигурации с нашими appName, profile и git branch, затем демаршируем ответ JSON в структуру springCloudConfig, которую мы объявляем в том же файле. Наконец, мы просто перебираем все пары ключ-значение в cloudConfig.PropertySources [0] и вставляем каждую пару в viper, чтобы мы могли обращаться к ним всякий раз, когда захотим, используя viper.GetString (key) или другой типизированный метод get Viper API предоставляет.

Обратите внимание, что если у нас есть проблема, связывающаяся с сервером конфигурации или анализирующая его ответ, мы паникуем () весь микросервис, который его убьет. Docker Swarm обнаружит это и попытается развернуть новый экземпляр за несколько секунд. Типичная причина такого поведения — запуск кластера с нуля, а микросервис на основе Go запускается намного быстрее, чем сервер конфигурации на основе Spring Boot. Пусть Рой попытается несколько раз, и все должно уладиться.

Мы разделили реальную работу на одну общедоступную функцию и несколько функций, охватываемых пакетами, для упрощения модульного тестирования. Модульный тест для проверки, чтобы мы могли преобразовать JSON в реальные свойства гадюки, выглядит следующим образом, используя стиль тестов GoConvey:

func TestParseConfiguration(t *testing.T) {

        Convey("Given a JSON configuration response body", t, func() {
                var body = `{"name":"accountservice-dev","profiles":["dev"],"label":null,"version":null,"propertySources":[{"name":"file:/config-repo/accountservice-dev.yml","source":{"server_port":6767"}}]}`

                Convey("When parsed", func() {
                        parseConfiguration([]byte(body))

                        Convey("Then Viper should have been populated with values from Source", func() {
                                So(viper.GetString("server_port"), ShouldEqual, "6767")
                        })
                })
        })
}

Запустите из goblog / accountservice, если вы хотите:

> go test ./...

Обновления в Dockerfile

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

goblog / accountservice / Dockerfile :

FROM iron/base
EXPOSE 6767

ADD accountservice-linux-amd64 /
ADD healthchecker-linux-amd64 /

HEALTHCHECK --interval=3s --timeout=3s CMD ["./healthchecker-linux-amd64", "-port=6767"] || exit 1
ENTRYPOINT ["./accountservice-linux-amd64", "-configServerUrl=http://configserver:8888", "-profile=test", "-configBranch=P8"]

Наша ENTRYPOINT теперь предоставляет значения, позволяющие настраивать, откуда загружать конфигурацию.

В Рой

Вы, вероятно, заметили, что мы больше не используем 6767 в качестве номера порта с жестким кодом, то есть:

service.StartWebServer(viper.GetString("server_port"))

Используйте сценарий copyall.sh для сборки и повторного развертывания обновленного «accountservice» в Docker Swarm.

> ./copyall.sh

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

(Обратите внимание, что порты, представленные в Dockerfiles, Healthcheck CMD и заявлениях Docker Swarm «docker service create» ничего не знают о серверах конфигурации. В конвейере CI / CD вы, вероятно, извлечете релевантные свойства, чтобы их можно было внедрить при сборке. сервер во время сборки.)

Давайте посмотрим на вывод журнала нашей учетной записи:

> docker logs -f [containerid]
Starting accountservice
Loading config from http://configserver:8888/accountservice/test/P8
Loading config property the_password => password
Loading config property server_port => 6767
Loading config property server_name => Accountservice TEST
Successfully loaded configuration for service Accountservice TEST

(На самом деле выводить значения конфигурации плохо, вышеприведенный вывод приведен только для образовательных целей!)

Обновления конфигурации в реальном времени

- "Oh, did that external service we're using for [some purpose] change their URL?"     
- "Darn. None told us!!" 

Я предполагаю, что многие из нас сталкивались с ситуациями, когда нам нужно либо перестроить целое приложение, либо хотя бы перезапустить его, чтобы обновить какое-либо недопустимое или измененное значение конфигурации. Spring Cloud имеет концепцию @RefreshScope, в которой бины могут обновляться в реальном времени с измененной конфигурацией, передаваемой из ловушки git commit .

На этом рисунке представлен обзор того, как пуш к git-репо распространяется на наши микросервисы на основе Go:

/assets/blogg/goblog/part8-springcloudpush.png

В этом сообщении мы используем репозиторий GitHub, у которого нет абсолютно никакой возможности узнать, как выполнить операцию перехвата после фиксации на сервере Spring Cloud моего ноутбука, поэтому мы будем эмулировать перехват фиксации фиксации с помощью встроенного / контролировать конечную точку нашего сервера Spring Cloud Config.

curl -H "X-Github-Event: push" -H "Content-Type: application/json" -X POST -d '{"commits": [{"modified": ["accountservice.yml"]}],"name":"some name..."}' -ki http://$ManagerIP:8888/monitor

Сервер Spring Cloud Config будет знать, что делать с этим POST, и отправлять RefreshRemoteApplicationEvent в обмен на RabbitMQ (абстрагируется от Spring Cloud Bus). Если мы посмотрим на графический интерфейс администратора RabbitMQ после успешной загрузки Spring Cloud Config, этот обмен должен был быть создан:

Обменять имя

Как обмен связан с более традиционными структурами обмена сообщениями, такими как издатель, потребитель и очередь?

Publisher -> Exchange -> (Routing) -> Queue -> Consumer

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

Поэтому, чтобы использовать сообщения RefreshRemoteApplicationEvent (я предпочитаю называть их токенами обновления ), все, что нам нужно сейчас сделать, — это заставить наш сервис Go прослушивать такие сообщения в обмене springCloudBus и, если мы являемся целевым приложением, выполнить перезагрузку конфигурации. Давайте сделаем это.

Использование протокола AMQP для потребления сообщений в Go

Доступ к брокеру RabbitMQ можно получить по протоколу AMQP. Есть хороший клиент Go AMQP, который мы будем использовать, называемый streadway / amqp . Большая часть сантехнического кода AMQP / RabbitMQ должна входить в какую-то утилиту многократного использования, возможно, мы ее рефакторинг позже. Сантехнический код основан на этом примере из репозитория streadway / amqp.

В /goblog/accountservice/main.go добавьте новую строку внутри функции main () , которая запустит для нас потребителя AMQP:

func main() {
        fmt.Printf("Starting %v\n", appName)

        config.LoadConfigurationFromBranch(
                viper.GetString("configServerUrl"),
                appName,
                viper.GetString("profile"),
                viper.GetString("configBranch"))
        initializeBoltClient()

        // NEW
        go config.StartListener(appName, viper.GetString("amqp_server_url"), viper.GetString("config_event_bus"))   
        service.StartWebServer(viper.GetString("server_port"))
}

Обратите внимание на новые свойства amqp_server_url и config_event_bus , они загружаются из загружаемого нами файла конфигурации _accountservice-test.yml  .

Функция StartListener входит в новый файл /goblog/accountservice/config/events.go . В этом файле много шаблонов AMQP, которые мы пропустим, поэтому сосредоточимся на интересных частях:

func StartListener(appName string, amqpServer string, exchangeName string) {
        err := NewConsumer(amqpServer, exchangeName, "topic", "config-event-queue", exchangeName, appName)
        if err != nil {
                log.Fatalf("%s", err)
        }

        log.Printf("running forever")
        select {}   // Yet another way to stop a Goroutine from finishing...
}

Функция NewConsumer — это то, куда идет весь шаблон. Мы перейдем к коду, который фактически обрабатывает входящее сообщение:

 func handleRefreshEvent(body []byte, consumerTag string) {
         updateToken := &UpdateToken{}
         err := json.Unmarshal(body, updateToken)
         if err != nil {
                 log.Printf("Problem parsing UpdateToken: %v", err.Error())
         } else {
                 if strings.Contains(updateToken.DestinationService, consumerTag) {
                         log.Println("Reloading Viper config from Spring Cloud Config server")

                         // Consumertag is same as application name.
                         LoadConfigurationFromBranch(
                                 viper.GetString("configServerUrl"),
                                 consumerTag,
                                 viper.GetString("profile"),
                                 viper.GetString("configBranch"))
                 }
         }
 }

 type UpdateToken struct {
         Type string `json:"type"`
         Timestamp int `json:"timestamp"`
         OriginService string `json:"originService"`
         DestinationService string `json:"destinationService"`
         Id string `json:"id"`
 }

Этот код пытается проанализировать входящее сообщение в структуре UpdateToken, и если destinationService совпадает с нашим consumerTag (т. Е. AppName «accountservice»), мы будем вызывать ту же функцию LoadConfigurationFromBranch, которая первоначально была вызвана при запуске службы.

Обратите внимание, что в реальном сценарии функции NewConsumer и общему коду обработки сообщений потребуется больше работать с обработкой ошибок, следя за тем, чтобы обрабатывались только соответствующие сообщения и т. Д.

Модульное тестирование

Давайте напишем модульный тест для функции handleRefreshEvent () . Создайте новый тестовый файл /goblog/accountservice/config/events_test.go :

var SERVICE_NAME = "accountservice"

func TestHandleRefreshEvent(t *testing.T) {
        // Configure initial viper values
        viper.Set("configServerUrl", "http://configserver:8888")
        viper.Set("profile", "test")
        viper.Set("configBranch", "master")

        // Mock the expected outgoing request for new config
        defer gock.Off()
        gock.New("http://configserver:8888").
                Get("/accountservice/test/master").
                Reply(200).
                BodyString(`{"name":"accountservice-test","profiles":["test"],"label":null,"version":null,"propertySources":[{"name":"file:/config-repo/accountservice-test.yml","source":{"server_port":6767,"server_name":"Accountservice RELOADED"}}]}`)

        Convey("Given a refresh event received, targeting our application", t, func() {
                var body = `{"type":"RefreshRemoteApplicationEvent","timestamp":1494514362123,"originService":"config-server:docker:8888","destinationService":"accountservice:**","id":"53e61c71-cbae-4b6d-84bb-d0dcc0aeb4dc"}
`
                Convey("When handled", func() {
                        handleRefreshEvent([]byte(body), SERVICE_NAME)

                        Convey("Then Viper should have been re-populated with values from Source", func() {
                              So(viper.GetString("server_name"), ShouldEqual, "Accountservice RELOADED")
                        })
                })
        })
}

Я надеюсь, что GoConvey в стиле BDD передает (каламбур!) Работу теста. Обратите внимание, как мы используем gock для перехвата исходящего HTTP-запроса на новую конфигурацию и что мы предварительно заполняем viper некоторыми начальными значениями.

Работает

Время проверить это. Повторно разверните, используя наш верный скрипт copyall.sh :

> ./copyall.sh

Проверьте журнал учетной записи сервиса :

> docker logs -f [containerid]
Starting accountservice
... [truncated for brevity] ...
Successfully loaded configuration for service Accountservice TEST    <-- LOOK HERE!!!!
... [truncated for brevity] ...
2017/05/12 12:06:36 dialing amqp://guest:guest@rabbitmq:5672/
2017/05/12 12:06:36 got Connection, getting Channel
2017/05/12 12:06:36 got Channel, declaring Exchange (springCloudBus)
2017/05/12 12:06:36 declared Exchange, declaring Queue (config-event-queue)
2017/05/12 12:06:36 declared Queue (0 messages, 0 consumers), binding to Exchange (key 'springCloudBus')
2017/05/12 12:06:36 Queue bound to Exchange, starting Consume (consumer tag 'accountservice')
2017/05/12 12:06:36 running forever

Теперь мы внесем изменения в файл accountservice-test.yml в моем репозитории git, а затем подделаем хук коммитов с помощью / POST API POST, показанного ранее в этом посте:

Я изменяю accountservice-test.yml и его свойство service_name с Accountservice TEST на временную тестовую строку! и толкая изменения.

Далее, используйте curl, чтобы сообщить нашему серверу Spring Cloud Config об обновлении:

> curl -H "X-Github-Event: push" -H "Content-Type: application/json" -X POST -d '{"commits": [{"modified": ["accountservice.yml"]}],"name":"what is this?"}' -ki http://192.168.99.100:8888/monitor

Если все работает, то это должно вызвать токен обновления с сервера Config , который наша AccountService улавливает. Проверьте журнал еще раз:

> docker logs -f [containerid]
2017/05/12 12:13:22 got 195B consumer: [accountservice] delivery: [1] routingkey: [springCloudBus] {"type":"RefreshRemoteApplicationEvent","timestamp":1494591202057,"originService":"config-server:docker:8888","destinationService":"accountservice:**","id":"1f421f58-cdd6-44c8-b5c4-fbf1e2839baa"}
2017/05/12 12:13:22 Reloading Viper config from Spring Cloud Config server
Loading config from http://configserver:8888/accountservice/test/P8
Loading config property server_port => 6767
Loading config property server_name => Temporary test string!
Loading config property amqp_server_url => amqp://guest:guest@rabbitmq:5672/
Loading config property config_event_bus => springCloudBus
Loading config property the_password => password
Successfully loaded configuration for service Temporary test string!      <-- LOOK HERE!!!!

Как видите, в последней строке теперь выводится «Успешно загруженная конфигурация для службы Временная тестовая строка!»  Исходный код для этой строки:

if viper.IsSet("server_name") {
        fmt.Printf("Successfully loaded configuration for service %s\n", viper.GetString("server_name"))
}    

Мы динамически изменили значение свойства, ранее сохраненное в Viper, во время выполнения, не касаясь нашего сервиса! Это действительно круто!

Важное примечание: хотя динамическое обновление свойств очень круто, само по себе оно не будет обновлять такие вещи, как порт нашего запущенного веб-сервера, существующие объекты Connection в пулах или (например) активное соединение с брокером RabbitMQ. Такие «уже работающие» вещи требуют гораздо больше усилий при перезапуске с новыми значениями конфигурации и выходят за рамки этого конкретного сообщения в блоге.

(Если вы не настроили свой собственный репозиторий Git, это демо не воспроизводимо, но я надеюсь, что вам все равно понравилось).

След и производительность

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

CONTAINER                                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
accountservice.1.pi7wt0wmh2quwm8kcw4e82ay4   0.02%               4.102MiB / 1.955GiB   0.20%               18.8kB / 16.5kB     0B / 1.92MB         6
configserver.1.3joav3m6we6oimg28879gii79     0.13%               568.7MiB / 1.955GiB   28.41%              171kB / 130kB       72.9MB / 225kB      50
rabbitmq.1.kfmtsqp5fnw576btraq19qel9         0.19%               125.5MiB / 1.955GiB   6.27%               6.2MB / 5.18MB      31MB / 414kB        75
quotes-service.1.q81deqxl50n3xmj0gw29mp7jy   0.05%               340.1MiB / 1.955GiB   16.99%              2.97kB / 0B         48.1MB / 0B         30

Даже с интеграцией AMQP и Viper в качестве инфраструктуры конфигурации мы имеем начальную площадь ~ 4 МБ. Наш сервер конфигурации на основе Spring Boot использует более 500 МБ ОЗУ, а RabbitMQ (который, я думаю, написан на Erlang?) Использует 125 МБ.

Я вполне уверен, что мы можем израсходовать сервер конфигурации до начального размера кучи 256 МБ, используя некоторые стандартные аргументы JVM -xmx, но, тем не менее, это определенно много ОЗУ. Однако в производственной среде я ожидаю, что мы будем запускать ~ 2 экземпляра сервера конфигурации, а не десятки или сотни. Когда дело доходит до вспомогательных сервисов из экосистемы Spring Cloud, использование памяти не так уж сложно, так как у нас обычно не будет более одного или нескольких экземпляров любого такого сервиса.

Резюме

В этой части серии блогов о микросервисах Go мы развернули сервер Spring Cloud Config и его зависимость RabbitMQ в нашем Swarm. Затем мы написали небольшой код на Go, который использует простой HTTP, JSON и инфраструктуру Viper, загружает конфигурацию с сервера конфигурации при запуске и передает ее в Viper для удобного доступа ко всей нашей базе кода микросервиса.

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