Scope
在上一篇文章中提到Envoy中通過Scope
來創建Metrics
,為什麼要搞一個Scope
的東西出來呢?Scope
誕生的目的其實是為了更好的管理一組stats
,比如關於集群的stats
,這類stats
的名稱有個特點就是都是以cluster.
作為前綴,那麼可以以cluster.
來創建一個Scope
,這樣就可以通過這個Scope
來管理所有的集群相關的stats
,而且通過這個Scope
創建的stats
其名稱可以省略掉cluster.
前綴,這樣可以節約很多內存資源。通過Scope
還可以創建Scope
,創建的Scope
的名字會帶上父Scope
的名稱。
上面這張圖表示的是兩個集群的upstream_rq_total
這個指標使用Scope
的表示形式。完整的指標名稱是cluster.http1_cluster.upstream_rq_total
和cluster.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
繼承自Scope
接口,並額外增加了counters
、gauges
、histograms
三個方法用於從所有的Scope
中彙總所有的Metrics
。StoreRoot
繼承Store
並添加了和TagProducer
、StatsMatcher
、Sink
相關的三個方法,最後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
成員用於存放緩存的Metrics
,createScope
方法最終調用的還是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
呢?,TlsCacheEntry
和CentralCacheEntry
是什麼關係呢?
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 = ¢ral_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;
}
整個Scope
的TlsCache
、Central cache
以及Metrics
的的關係可以用下面這張圖來表示。
IsolatedStoreImpl
最後來講解下IsolatedStoreImpl
,總的來說Envoy的stats store存在兩個類別,一類就是ThreadLocalStore
,這類store可以通過StoreRoot
接口添加TagProducer
、StatsMatcher
以及設置Sink
,也就是說這類Store存儲的stats可以進行Tag的提取、可以通過配置的Sink
把stats發送到其他地方,目前Envoy支持的Sink
有statsd
、dog_statsd
、metrics_service
、hystrix
等,發送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 cache
和Tls cache
的設計避免了加鎖操作,每個線程都會創建Scope
還有對應的,每一個Scop
e都有一個central cache
以及在ThreadLocal
中有一個TlsCache
,所有的這些Cache
引用的Metrics
是共享的。另外一類是IsolatedStoreImpl
,是非線程安全的,在Envoy中主要用於兩個地方,一個是per host的stats統計,另外一個則是單元測試,充當一個簡單的stats store來進行stats統計相關的測試。