Предисловие

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


Герц

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

Hertz по умолчанию использует собственную высокопроизводительную сетевую библиотеку Netpoll. В некоторых особых сценариях Hertz имеет определенные преимущества в QPS и задержке по сравнению с выходом в сеть.

Во внутренней практике некоторые типовые сервисы, такие как сервисы с высокой долей фреймворков, шлюзов и других сервисов, после миграции Hertz, по сравнению с фреймворком Gin, значительно снижают использование ресурсов, Использование ЦП снижается на 30%-60% в зависимости от размера трафика.

Подробнее см. cloudwego/герц.


Расширение обнаружения службы

Hertz поддерживает настраиваемые модули обнаружения. Пользователи могут самостоятельно расширять и интегрировать другие реестры. Расширение определяется в pkg/app/client/discovery.


Интерфейс расширения


Определение и реализация интерфейса обнаружения службы

В интерфейсе обнаружения служб есть три метода.

  1. Resolve как основной метод Resolve, он получит нужный нам результат обнаружения службы из целевого ключа.
  2. Target разрешает уникальную цель, которая Resolve необходимо использовать одноранговую TargetInfo, предоставленную Hertz, и эта цель будет использоваться в качестве уникального ключа кэша.
  3. Name используется для указания уникального имени Resolver, и Hertz будет использовать его для кэширования и повторного использования Resolver.
type Resolver interface {
    // Target should return a description for the given target that is suitable for being a key for cache.
    Target(ctx context.Context, target *TargetInfo) string

    // Resolve returns a list of instances for the given description of a target.
    Resolve(ctx context.Context, desc string) (Result, error)

    // Name returns the name of the resolver.
    Name() string
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Эти три метода реализованы в последующем коде в discovery.go.

// SynthesizedResolver synthesizes a Resolver using a resolve function.
type SynthesizedResolver struct {
    TargetFunc  func(ctx context.Context, target *TargetInfo) string
    ResolveFunc func(ctx context.Context, key string) (Result, error)
    NameFunc    func() string
}

func (sr SynthesizedResolver) Target(ctx context.Context, target *TargetInfo) string {
    if sr.TargetFunc == nil {
        return ""
    }
    return sr.TargetFunc(ctx, target)
}

func (sr SynthesizedResolver) Resolve(ctx context.Context, key string) (Result, error) {
    return sr.ResolveFunc(ctx, key)
}

// Name implements the Resolver interface
func (sr SynthesizedResolver) Name() string {
    if sr.NameFunc == nil {
        return ""
    }
    return sr.NameFunc()
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Здесь в SynthesizedResolver есть три функции разрешения для каждой из трех реализаций, которые необходимо разрешить.


Определение TargetInfo

Как упоминалось выше, Target метод разрешает единственную цель, которая Resolve необходимо использовать от TargetInfo.

type TargetInfo struct {
    Host string
    Tags map[string]string
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


определение и реализация интерфейса экземпляра

Экземпляр содержит информацию из целевого экземпляра службы. Есть три метода.

  1. Address адрес целевой службы.
  2. Weight служит вес цели.
  3. Tag — это тег целевой службы в виде пар ключ-значение.
// Instance contains information of an instance from the target service.
type Instance interface {
    Address() net.Addr
    Weight() int
    Tag(key string) (value string, exist bool)
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Эти три метода реализованы в последующем коде в discovery.go.

type instance struct {
    addr   net.Addr
    weight int
    tags   map[string]string
}

func (i *instance) Address() net.Addr {
    return i.addr
}

func (i *instance) Weight() int {
    if i.weight > 0 {
        return i.weight
    }
    return registry.DefaultWeight
}

func (i *instance) Tag(key string) (value string, exist bool) {
    value, exist = i.tags[key]
    return
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Новый экземпляр

NewInstance создает экземпляр с заданной сетью, адресом и тегами.

// NewInstance creates an Instance using the given network, address and tags
func NewInstance(network, address string, weight int, tags map[string]string) Instance {
    return &instance{
        addr:   utils.NewNetAddr(network, address),
        weight: weight,
        tags:   tags,
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Результат

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

// Result contains the result of service discovery process.
// the instance list can/should be cached and CacheKey can be used to map the instance list in cache.
type Result struct {
    CacheKey  string
    Instances []Instance
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


промежуточное ПО клиента

Клиентское промежуточное ПО определено в разделе pkg/app/client/middlewares/client.


Открытие

Discovery будет использовать BalancerFactory для создания промежуточного программного обеспечения. Сначала прочитайте и примените конфигурацию, которую мы передали через Apply метод. Подробная информация о конфигурации указана в разделе pkg/app/client/middlewares/client/sd/options.go. Затем назначьте центр обнаружения служб, балансировщик нагрузки и конфигурацию балансировки нагрузки, которые мы установили для lbConfigвызов NewBalancerFactory пройти в lbConfigи, наконец, вернуть анонимную функцию типа client.Middleware.

// Discovery will construct a middleware with BalancerFactory.
func Discovery(resolver discovery.Resolver, opts ...ServiceDiscoveryOption) client.Middleware {
    options := &ServiceDiscoveryOptions{
        Balancer: loadbalance.NewWeightedBalancer(),
        LbOpts:   loadbalance.DefaultLbOpts,
        Resolver: resolver,
    }
    options.Apply(opts)

    lbConfig := loadbalance.Config{
        Resolver: options.Resolver,
        Balancer: options.Balancer,
        LbOpts:   options.LbOpts,
    }

    f := loadbalance.NewBalancerFactory(lbConfig)
    return func(next client.Endpoint) client.Endpoint {
        // ...
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Принцип реализации

Принцип реализации промежуточного программного обеспечения обнаружения служб фактически является последней частью Discovery что мы не разобрали выше. Мы сбросим хост в промежуточном программном обеспечении. Когда конфигурация в запросе не пустая и IsSD() настроен как True, мы получаем экземпляр и вызываем SetHost для сброса хоста.

return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
  if req.Options() != nil && req.Options().IsSD() {
    ins, err := f.GetInstance(ctx, req)
    if err != nil {
      return err
    }
    req.SetHost(ins.Address().String())
  }
  return next(ctx, req, resp)
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Анализ реализации службы обнаружения


Регулярное обновление

На практике информация об обнаружении наших служб часто обновляется. Герц использует refresh метод для периодического обновления информации об обнаружении наших служб. Мы будем обновлять через цикл for range, где интервал между циклами равен RefreshInterval в конфигурации. Затем мы обновляем, просматривая пары ключ-значение в кеше через Range метод в sync библиотечная функция.

// refresh is used to update service discovery information periodically.
func (b *BalancerFactory) refresh() {
    for range time.Tick(b.opts.RefreshInterval) {
        b.cache.Range(func(key, value interface{}) bool {
            res, err := b.resolver.Resolve(context.Background(), key.(string))
            if err != nil {
                hlog.SystemLogger().Warnf("resolver refresh failed, key=%s error=%s", key, err.Error())
                return true
            }
            renameResultCacheKey(&res, b.resolver.Name())
            cache := value.(*cacheResult)
            cache.res.Store(res)
            atomic.StoreInt32(&cache.expire, 0)
            b.balancer.Rebalance(res)
            return true
        })
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


кеш распознавателя

В комментариях NewBalancerFactory, мы можем знать, что когда мы получим тот же ключ, что и цель в кеше, мы получим и повторно используем этот балансировщик нагрузки из кеша. Кратко разберем его реализацию. Мы передаем центр обнаружения служб, балансировщик нагрузки и конфигурацию балансировки нагрузки вместе в cacheKey функция для получения uniqueKey .

func cacheKey(resolver, balancer string, opts Options) string {
    return fmt.Sprintf("%s|%s|{%s %s}", resolver, balancer, opts.RefreshInterval, opts.ExpireInterval)
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Затем мы будем использовать Load метод, чтобы узнать, есть ли в карте тот же самый uniqueKey, если да, мы сразу вернемся к балансировщику нагрузки. Если нет, мы добавим его в кеш.

func NewBalancerFactory(config Config) *BalancerFactory {
    config.LbOpts.Check()
    uniqueKey := cacheKey(config.Resolver.Name(), config.Balancer.Name(), config.LbOpts)
    val, ok := balancerFactories.Load(uniqueKey)
    if ok {
        return val.(*BalancerFactory)
    }
    val, _, _ = balancerFactoriesSfg.Do(uniqueKey, func() (interface{}, error) {
        b := &BalancerFactory{
            opts:     config.LbOpts,
            resolver: config.Resolver,
            balancer: config.Balancer,
        }
        go b.watcher()
        go b.refresh()
        balancerFactories.Store(uniqueKey, b)
        return b, nil
    })
    return val.(*BalancerFactory)
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

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


Подвести итог

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

Наконец, если статья оказалась для вас полезной, пожалуйста, поставьте лайк и поделитесь ею, это для меня самое большое поощрение!


Ссылка