Pull to refresh

Comments 19

А зачем вам в (s *ServiceKeeper) release() буферизированный канал по числу сервисов, если ждете вы все равно первую ошибку только?

Если канал не будет буферизированный, то в случае, когда все сервисы вернут ошибку, произойдет следующее:

  1. первый сервис записал в канал ошибку, мы прочитали и вышли из функции

  2. второй сервис пытается записать - а никто уже не читает. висим

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

Интересно, почему на верхнем уровне абстракции Resources определено интерфейсом, а Application структурой? Почему не что нибудь вроде

type Application interface {

Run(context.Context)

Halt()

State() int32

Resources

}

Хорошее замечание. Тут явно видна какая то моя заделка которая не дошла до своего финала?

С интерфейсом Resources такая история: фактически Application совершенно не волнует какая ему предоставлена реализация, он хочет получить возможность управлять ресурсами на верхнем уровне, просто командуя Init, Watch и прочее. Так же и с реализацией ServiceKeeper - он просто хочет массив интерфейсов Service, чтобы управлять зоопарком ресурсов. Я написал базовую реализацию ServiceKeeper только для того, чтобы можно было видеть, как этим пользоваться и, если бы разделил services.go от application.go на разные пакеты в своем репозитории, то имел бы законченную мысль.

Однако это все еще не отвечает на Ваш вопрос по поводу Application interface. Я не вижу какого то случая, при котором бы понадобился такой интерфейс. Куда вы его примостите и зачем? Если в основе лежит идея о том, что Application запускает MainFunc и следит за Health приложения, то кто будет следить за Application и для чего?

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

Хорошо. Понятно.
В продолжении дискуссии interface Application мог бы пригодиться, например, для оркестровки микросервисов. Структура Application и ещё с не экспортированными полями, с другой стороны на мой взгляд, вносит в архитектуру дополнительную сущность имеющую состояние(state) недоступное для конечного разработчика собственно сервиса, то есть разработчика функции MainFunc func(ctx context.Context, holdOn <-chan struct{}) error

Для разработчика этой функции абсолютно не важно, есть ли вообще такая структура Application, потому, что жизненный цикл MainFunc должен быть не дольше времени перехода между srvStateRunning и srvStateShutdown. Т.е. глобально в MainFunc вся логика построена на работе как в обычном main, только мы уже сразу имеем канал (holdOn <-chan struct{}) сигнализирующий о завершении работы, как <-chan os.Signal, который мы обычно создаем сами

Однако, хорошее замечание про оркестрацию! И вот мой ответ: в том месте, где вы начинаете оркестрировать экземплярами Application, и нужно размещать интерфейс Application с его

  • Run() error - для старта (блокирующий, кстати)

  • Halt() - для остановки

  • Shutdown() - для экстренной "жесткой" остановки

и больше ничего не нужно. Состояние state используется исключительно для синхронизации вызовов внутри реализации Application и выдавать наружу не нужно

Еще вопрос - почему в случае отказа какого-то сервиса вы завершаете работу приложения? Если не прошел пинг на внешнюю зависимость не стоило ли перевести приложение в некоторое состояние, которое не проходит readiness пробу, но при этом вполне себе проходит liveness и пытается восстановить потерянное соединение? Не слишком жестко вызывать Shutdown?

Чаще всего приложения запускается в докере или кубере. И они самостоятельно выполнят рестарт (если есть соответствующие настройки). Так проще отследить и выявить проблемы в работе системы.

А вот если запускать все на bare-metal, то уже стоит думать о попытках восстановить работоспособность силами самого приложения.

Ну вообще вопросы kubernetes livenes/readines probes мы тут пока не трогаем. Их очередь еще придет.

А в случае отказа какого-то сервиса не должно происходить остановки приложения, в этом я с Вами согласен. Остановка происходит, когда на запрос Ping сервис отвечает ошибкой - это да. Тут пока мало документации и поэтому не понятно, что принятие решения в случае возникновения любой ошибки не лежит на плечах ServiceKeeper, вовсе нет, когда сервис понимает, что у него проблемы, есть два пути решения:

  1. отдать error, и, да, тут приложение завершится

  2. отдать nil и войти в режим восстановления, а это означает: поведение по-умолчанию для вызова своих АПИ и фоновые попытки восстановить коннекты/ресурсы и вернуться к нормальной работоспособности (на уровне конкретного ресурса без уведомления приложения)

    Второе можно применить только, если такое применить возможно, например, так может работать memcached - он будет отвечать Cache Miss на попытки получить что-то из кеша, а в фоне попытается восстановить связь с кеш-сервером.

т.е. чтобы меня правильно поняли: Ping возвращает error только в том случае, если работу продолжить невозможно

Спасибо, за статью! Почерпнул для себя полезности для работы.

А как это структурно разнести? Допустим у меня есть дефолтные примитивная архитектура server-handler-(repository, broker, ...). В данном случае за Application из вашей статьи можно взять handler?

На подходе вторая часть этой статьи, где будет представлен пример маленького http-бэкенда. Там должно быть понятнее.

В любом случае в следующей статье можно будет уже общаться предметно. В этой я лишь задаю вектор рассуждений при разработке фундамента приложения.

Какой такой handler за Application? Ваш server будет всего лишь одним из сервисов.


Извиняюсь, перепутал сервисы и ресурсы благодаря хитрому плану автора. Но handler за Application — всё равно странная идея, надо запускать server внутри Application.MainFunc

не работает ссылка. Пишет что статья еще в черновиках.

Спасибо за статью. Предположим, что

var svc = app.ServiceKeeper{
	Services: []Service{
			&service1,
			&service2,
		},
}
var app = app.Application{
	Resources:          &svc,
}
... 
func (s1 service1) Init (ctx context.Context) error {
  s2.Connect()
}

Как лучше всего в инит обратиться из сервис1 в сервис2? Спасибо.

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

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

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

Предположим, что у вас есть база данных, как сервис1 и сервис очередей, построенный на этой базе данных (да, такое бывает), как сервис2. Если в инициализации сервиса2 вам нужен рабочий коннект от сервиса1, но по какой то причине сервис1 еще не готов, то инициализация сервиса2 будет давать ошибку и все приложение остановится. Вы долго будете пытаться понять, в чем же дело и почему база данных не готова, прежде чем поймете, что она на самом деле готова, просто не вовремя. Да, пример простой и на самом деле в нем легко понять, что происходит, но проблему очень легко усложнить неправильными логами или другими bad-design практиками. И попробуйте потом отделить ошибку сервиса1 от ошибки сервиса2, когда пинг сервиса2 вернет error("service unavailable").

И все-же проблему необходимо решить. Я предлагаю вам два решения, можете выбрать любое:

  1. считаем что БД - это сервис для сервиса2, т.е. не показываем сервис1 в ServiceKeeper, но считаем его приватным полем структуры сервис2 и запускаем инициализацию сервис1 изнутри инита сервис2, тем же принципом поступаем в реализации пинга - пингуем заодно и сервис1. Если коннект к базе данных сервис1 требуется всему приложению, то просто создаем сервис3, который будет работать на все остальное приложение и регаться в ServiceKeeper, пока сервис2 будет иметь собственный коннект сервис1 и никому его не показывать.

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

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

Большое спасибо за комментарий. В первой итерации моего софта было примерно так:

type Service1{
		app       appInterface.App
}
...
func (s *Service1) Init () error {
  s.app.service2.Connect();
}

Умом понимаю, что криво звать из одного сервиса другой, но в тот момент решил оставить как есть ;) ... Пока не наткнулся на Вашу статью. Мне показалось что Ваша архитектура логична и самодостаточна.

Попробую предложенные варианты потом отпишусь как получилось.

Sign up to leave a comment.