資安

Envoy源碼分析之Stats Scope

Scope

在上一篇文章中提到Envoy中通過Scope來創建Metrics,為什麼要搞一個Scope的東西出來呢?Scope誕生的目的其實是為了更好的管理一組stats,比如關於集群的stats,這類stats的名稱有個特點就是都是以cluster.作為前綴,那麼可以以cluster.來創建一個Scope,這樣就可以通過這個Scope來管理所有的集群相關的stats,而且通過這個Scope創建的stats其名稱可以省略掉cluster.前綴,這樣可以節約很多內存資源。通過Scope還可以創建Scope,創建的Scope的名字會帶上父Scope的名稱。

scope.jpg

上面這張圖表示的是兩個集群的upstream_rq_total這個指標使用Scope的表示形式。完整的指標名稱是cluster.http1_cluster.upstream_rq_totalcluster.http2_cluster.upstream_rq_total在Envoy中會首先創建一個cluster.Scope,然後通過這個Scope創建一個http1_cluster.Scope,然後再創建一個http2_cluster.Scope,最後分別利用這兩個Scope創建upstream_rq_total stats。通過Scope一來可以有效的管理一組stats,另外通過Scope可以讓一類stats共享stats前綴。避免冗餘的stats字符串。例如上面的upstream_rq_total只需要存放upstream_rq_total這個字符串即可,可以共享對應Scope提供的前綴

  ScopePtr root_scope = store_->createScope("cluster.");
  auto http1_scope = root_scope->createScope("http1_cluster.");
  auto http2_scope = root_scope->createScope("http2_cluster.");
  auto upstream_rq_total_http1 = http1_scope->counter("upstream_rq_total");
  auto upstream_rq_total_http2 = http2_scope->counter("upstream_rq_total");

Store、ThreadLocalStore、 TlsScope

有了Scope後那如何去創建Scope呢?,如何去管理所有的Scope創建的Metrics呢?

store.jpg

Store繼承自Scope接口,並額外增加了countersgaugeshistograms三個方法用於從所有的Scope中彙總所有的MetricsStoreRoot繼承Store並添加了和TagProducerStatsMatcherSink相關的三個方法,最後ThreadLocalStoreImpl實現了這三個接口。首先來看下createScope方法,這是用來創建一個Scope然後返回,所有的Scope都存放在scopes_成員中。這裡返回的Scope具體類型是ScopeImpl,繼承自TlsScope

ScopePtr ThreadLocalStoreImpl::createScope(const std::string& name) {
  auto new_scope = std::make_unique<ScopeImpl>(*this, name);
  Thread::LockGuard lock(lock_);
  scopes_.emplace(new_scope.get());
  return new_scope;
}

接著我們看下TlsScope

class TlsScope : public Scope {
public:
  ~TlsScope() override = default;
  virtual Histogram& tlsHistogram(StatName name, ParentHistogramImpl& parent) PURE;
};

只是額外添加了一個tlsHistogram方法而已,繼續看下它的實現。

  struct ScopeImpl : public TlsScope {
    ......
    ScopePtr createScope(const std::string& name) override {
      return parent_.createScope(symbolTable().toString(prefix_.statName()) + "." + name);
    }
        ....
    static std::atomic<uint64_t> next_scope_id_;

    const uint64_t scope_id_;
    ThreadLocalStoreImpl& parent_;
    StatNameStorage prefix_;
    mutable CentralCacheEntry central_cache_;
  };

  struct CentralCacheEntry {
    StatMap<CounterSharedPtr> counters_;
    StatMap<GaugeSharedPtr> gauges_;
    StatMap<ParentHistogramImplSharedPtr> histograms_;
    StatNameStorageSet rejected_stats_;
  };

每一個Scope都有一個CentralCacheEntry成員用於存放緩存的MetricscreateScope方法最終調用的還是ThreadLocalStoreImpl::createScope,所以ThreadLocalStoreImpl中可以保存所有創建的Scope。接下來看下ScopeImpl是如何創建Metrics的。

Counter& ScopeImpl::counter(const std::string& name) override {
  StatNameManagedStorage storage(name, symbolTable());
  return counterFromStatName(storage.statName());
}
Counter& ScopeImpl::counterFromStatName(StatName name) {
  // Setp1: 先通過StatsMatcher模塊檢查是否拒絕產生Stats,如果是就直接返回的一個NullCounter
  if (parent_.rejectsAll()) {
    return parent_.null_counter_;
  }

  // Setp2: 拼接完整的stat name
  Stats::SymbolTable::StoragePtr final_name = symbolTable().join({prefix_.statName(), name});
  StatName final_stat_name(final_name.get());

  // Setp3: 從thread local緩存中獲取scope的緩存
  StatMap<CounterSharedPtr>* tls_cache = nullptr;
  StatNameHashSet* tls_rejected_stats = nullptr;
  if (!parent_.shutting_down_ && parent_.tls_) {
    TlsCacheEntry& entry = parent_.tls_->getTyped<TlsCache>().scope_cache_[this->scope_id_];
    tls_cache = &entry.counters_;
    tls_rejected_stats = &entry.rejected_stats_;
  }
    // Setp4: 創建Counter
  return safeMakeStat<Counter>(
      final_stat_name, central_cache_.counters_, central_cache_.rejected_stats_,
      [](Allocator& allocator, StatName name, absl::string_view tag_extracted_name,
         const std::vector<Tag>& tags) -> CounterSharedPtr {
        return allocator.makeCounter(name, tag_extracted_name, tags);
      },
      tls_cache, tls_rejected_stats, parent_.null_counter_);
}

為什麼創建一個Counter要去拿TlsCache呢?,TlsCacheEntryCentralCacheEntry是什麼關係呢?

struct TlsCache : public ThreadLocal::ThreadLocalObject {
  absl::flat_hash_map<uint64_t, TlsCacheEntry> scope_cache_;
};

struct TlsCacheEntry {
    StatMap<CounterSharedPtr> counters_;
    StatMap<GaugeSharedPtr> gauges_;
    StatMap<TlsHistogramSharedPtr> histograms_;
    StatMap<ParentHistogramSharedPtr> parent_histograms_;
    StatNameHashSet rejected_stats_;
  };

可以看出這個TlsCache中存放的內容是一個Map,key是Scope id(目的是為了可以在ThreadLocal中存放多個Scope,通過Scope id來區分),value是一個TlsCacheEntry,這個結構和Scope內的CentralCacheEntry是一模一樣的。做這些的目的其實還是為了能讓Envoy可以在核心流程中無鎖的進行stats的統計。如果多個線程共享同一個Scope,那麼每一個線程都通過同一個Scope來訪問CentralCacheEntry,那麼自然會存在多線程的問題,也就是說每次訪問CentralCacheEntry都需要加鎖。如果每一個線程都有一個自己獨立的Scope,每一個Scope共享相同的Metrics,每個線程訪問自己的Scope是線程安全的,然後找到對應的Metrics,這個Metrics本身的操作是線程安全的,這樣就可以使得整個過程是無鎖的了。為此Scope和內部存放的Metrics是解耦的,默認CentralCacheEntry為空,每當獲取一個stats的時候,先查ThreadLocal中是否存在,不存在就去看CentralCacheEntry,沒有的話就創建stats,然後放入CentralCacheEntry中,然後再存一份到ThreadLocal中,這樣做的目的是為了可以在主線程可以通過遍歷所有的Scope拿到CentralCacheEntry來最最後的彙總,具體的代碼分析可以看下面的註釋。

template <class StatType>
StatType& ThreadLocalStoreImpl::ScopeImpl::safeMakeStat(
    StatName name, StatMap<RefcountPtr<StatType>>& central_cache_map,
    StatNameStorageSet& central_rejected_stats, MakeStatFn<StatType> make_stat,
    StatMap<RefcountPtr<StatType>>* tls_cache, StatNameHashSet* tls_rejected_stats,
    StatType& null_stat) {
    // Setp1: 這個stats是否被rejected
  if (tls_rejected_stats != nullptr &&
      tls_rejected_stats->find(name) != tls_rejected_stats->end()) {
    return null_stat;
  }
    // Setp2: 查看Tls cache是否存在,存在就直接返回
  // If we have a valid cache entry, return it.
  if (tls_cache) {
    auto pos = tls_cache->find(name);
    if (pos != tls_cache->end()) {
      return *pos->second;
    }
  }

  // We must now look in the central store so we must be locked. We grab a reference to the
  // central store location. It might contain nothing. In this case, we allocate a new stat.
  // Setp3: 搜索central_cache,如果不存在就創建stats,這裡要加鎖的,因為主線程會訪問            
  //                 central_cache,其他線程也會操作central_cache。
  Thread::LockGuard lock(parent_.lock_);
  auto iter = central_cache_map.find(name);
  RefcountPtr<StatType>* central_ref = nullptr;
  if (iter != central_cache_map.end()) {
    central_ref = &(iter->second);
  } else if (parent_.checkAndRememberRejection(name, central_rejected_stats, tls_rejected_stats)) {
    // Note that again we do the name-rejection lookup on the untruncated name.
    return null_stat;
  } else {
    TagExtraction extraction(parent_, name);
    RefcountPtr<StatType> stat =
        make_stat(parent_.alloc_, name, extraction.tagExtractedName(), extraction.tags());
    ASSERT(stat != nullptr);
    central_ref = &central_cache_map[stat->statName()];
    *central_ref = stat;
  }
    
  // Step4: 往Tls中也插入一份,使得Tls cache和central cache保持一致
  // If we have a TLS cache, insert the stat.
  if (tls_cache) {
    tls_cache->insert(std::make_pair((*central_ref)->statName(), *central_ref));
  }

  // Finally we return the reference.
  return **central_ref;
}

整個ScopeTlsCacheCentral cache以及Metrics的的關係可以用下面這張圖來表示。

scope-tls.jpg

IsolatedStoreImpl

最後來講解下IsolatedStoreImpl,總的來說Envoy的stats store存在兩個類別,一類就是ThreadLocalStore,這類store可以通過StoreRoot接口添加TagProducerStatsMatcher以及設置Sink,也就是說這類Store存儲的stats可以進行Tag的提取、可以通過配置的Sink把stats發送到其他地方,目前Envoy支持的Sinkstatsddog_statsdmetrics_servicehystrix等,發送stats的時候還可以根據配置的StatsMatcher有選擇的發送符合要求的stats,另外一類的stats store就是IsolatedStoreImpl,這類stats store僅僅是用來存儲Envoy內部使用的一些stats,比如per upstream host的stats統計。這類stats量很大,它使用的就是IsolatedStoreImpl,也不會通過admin的stats接口暴露出去。IsolatedStoreImpl另外的一個用途就是單元測試。

總結

本文首先講解了Scope的設計意圖,通過Scope可以管理一組stats,還可以共享stats前綴,避免不必要的字符串冗餘,接著講解了stats store,一類是ThreadLocalStore,這類store通過central cacheTls cache的設計避免了加鎖操作,每個線程都會創建Scope還有對應的,每一個Scope都有一個central cache以及在ThreadLocal中有一個TlsCache,所有的這些Cache引用的Metrics是共享的。另外一類是IsolatedStoreImpl,是非線程安全的,在Envoy中主要用於兩個地方,一個是per host的stats統計,另外一個則是單元測試,充當一個簡單的stats store來進行stats統計相關的測試。

Leave a Reply

Your email address will not be published. Required fields are marked *