简介
Envoy官方文档中提到One of the primary goals of Envoy is to make the network understandable
,让网络变的可理解,为了实现这个目标Envoy中内置了stats
用于统计各类网络相关的指标,Envoy没有选择使用Prometheus
SDK,而是选择自己实现了stats
,目的是为了适配Envoy的线程模型以及Envoy自身的一些需求。Envoy中的stats
主要用于统计三类指标信息(每一个指标又被称为Metric
):
- Downstream: 统计进来的连接和请求,指标来源于
Listeners
、HTTP connection manager
、TCP proxy filter
等。 - Upstream: 统计出去的连接和请求,指标来源于
connection pools
、router filter
、TCP proxy filter
等。 - Server: 统计Envoy实例自身的一些状态信息,指标来源于启动时间、分配的内存等。
这些指标有的是持续递增的(比如传输的字节总数、接收的请求数等),有的则会增长,也会递减(比如活跃的连接数),有的则需要统计分布情况(比如RT的分布情况)等,为了满足这些需求Envoy使用了三种类型的stats
来表示。
- Counter: 64位的无符号整型,只递增。
- Gauge: 64位的无符号整型,可以递增也可以递减。
- Histogram: 一组表示范围的值,统计的数据会映射到这组值中,随着统计的数据增多,这组表示范围的值会自动调整。典型的比如统计请求的耗时,那么这组表示范围的值可以是(0~5ms, 0~10ms, 0~20ms....)等。
为了让所有的stats
信息在热重启的时候可以传递给新启动的进程,stats
的数据最初默认是存放在共享内存中的,这样在热重启的时候就可以通过共享内存在两个进程之间传递stats
。但是基于共享内存的这种方式存在诸多限制,比如共享内存的大小是预先分配的,固定大小,没办法动态增长。而这在大规模的集群场景下将会变得更加糟糕会导致耗费大量内存(每一个stats
都分配固定大小的内存,因为需要用来计算要申请的共享内存大小,但是实际上很多stats
并没有使用这么多内存)。为此最新的Envoy其stats
是存储在堆上进行动态分配,然后通过RPC协议在新老进程中传递。关于这部分的讨论可以关注这个stats: Consider communicating stats across hot-restart via RPC rather than shared memory
基本概念
stats
的目的是为了统计各类指标,每一个指标被称为Metric
,在Envoy中所有类型的指标都继承自Metric基类,Metric
有名称(实际被称为extraced metric name
,不包含tag的名称)、值、还有附加的一些tag(就是key/value对),Metric
还有类型,总共有三种,就是上文中提到的Counters、Gauge、Histogram等。我们在Envoy中看到的Metric
名称通常指的就是Metric
的完整名称(包含了tag信息)。为了从完整的名称中提取extraced metric name
名称和tag就有了TagProducer的东西出来了,核心就一个方法produceTags
,传入一个完整的指标名称,返回一系列的tag和extraced metric name
。那么Envoy是按照什么样的规则来提取Tag
呢?Envoy中会通过TagExactor来提取Tag,它包含了Tag name
和一个正则用于正则匹配来提取对应的Tag value
。
Metric
MetricImpl
是一个模版类,其核心就是存储了extracted的指标名称和对应的tag,CounterImpl
、GaugeImpl
等都是继承MetricImpl
。
template <class BaseClass> class MetricImpl : public BaseClass {
public:
.......
private:
// 核心数据成员
MetricHelper helper_;
};
class MetricHelper {
public:
.......
private:
StatNameList stat_names_;
};
通过上面的代码我们可以知道核心的类成员是StatNameList
,extracted的指标名称和对应的Tag
就是存储在StatNameList
中,这个数据结构会在下一篇文章中介绍。
TagExactor
这个类是用来存放Tag Name
以及如何从完整指标提取出Tag Value
的正则,Envoy默认已经提供了一系列的提取Tag Value
的正则well_known_names,此外还可以通过配置文件的方式来自定义的Tag Name和正则来提取Tag Value。核心就是extractTag
方法,用于从一个完整的指标名称中提取出Tag value。
class TagExtractorImpl : public TagExtractor {
public:
static TagExtractorPtr createTagExtractor(const std::string& name, const std::string& regex,
const std::string& substr = "");
TagExtractorImpl(const std::string& name, const std::string& regex,
const std::string& substr = "");
std::string name() const override { return name_; }
bool extractTag(absl::string_view tag_extracted_name, std::vector<Tag>& tags,
IntervalSet<size_t>& remove_characters) const override;
absl::string_view prefixToken() const override { return prefix_; }
bool substrMismatch(absl::string_view stat_name) const;
private:
static std::string extractRegexPrefix(absl::string_view regex);
const std::string name_;
const std::string prefix_;
const std::string substr_;
const std::regex regex_;
};
比如下面这个例子:
TEST(TagExtractorTest, TwoSubexpressions) {
// 这是Tag Name和提取Tag Value的正则
TagExtractorImpl tag_extractor("cluster_name", "^cluster\\.((.+?)\\.)");
EXPECT_EQ("cluster_name", tag_extractor.name());
// 这是一个完整的指标名称,通过正则来提取Tag value
std::string name = "cluster.test_cluster.upstream_cx_total";
std::vector<Tag> tags;
IntervalSetImpl<size_t> remove_characters;
ASSERT_TRUE(tag_extractor.extractTag(name, tags, remove_characters));
std::string tag_extracted_name = StringUtil::removeCharacters(name, remove_characters);
// 这是extracted后的名称
EXPECT_EQ("cluster.upstream_cx_total", tag_extracted_name);
ASSERT_EQ(1, tags.size());
// 这是提取出来的Tag Name和 Tag value
EXPECT_EQ("test_cluster", tags.at(0).value_);
EXPECT_EQ("cluster_name", tags.at(0).name_);
}
TagProducer
TagProducer
依靠TagExactor
得到一系列的Tag
,核心的方法就是produceTags
,其数据结构如下。
class TagProducerImpl : public TagProducer {
public:
......
private:
std::vector<TagExtractorPtr> tag_extractors_without_prefix_;
std::unordered_map<absl::string_view, std::vector<TagExtractorPtr>, StringViewHash>
tag_extractor_prefix_map_;
std::vector<Tag> default_tags_;
};
核心的数据成员就是default_tags_
这个是用来给每一个指标名称都要默认添加的默认Tag
(是用户自定义配置的)。调用produceTags
产生Tag
的时候会自动将default_tags_
添加到集合中。
TagProducerImpl::TagProducerImpl(const envoy::config::metrics::v2::StatsConfig& config) {
// To check name conflict.
reserveResources(config);
std::unordered_set<std::string> names = addDefaultExtractors(config);
for (const auto& tag_specifier : config.stats_tags()) {
const std::string& name = tag_specifier.tag_name();
.......
} else if (tag_specifier.tag_value_case() ==
envoy::config::metrics::v2::TagSpecifier::kFixedValue) {
// 从StatsConfig将用户配置的固定的Tag Name和Tag Value放到default_tags_中
// 这类Tag是不用通过正则提取,直接作为指标的Tag返回。
default_tags_.emplace_back(Stats::Tag{name, tag_specifier.fixed_value()});
}
}
}
std::string TagProducerImpl::produceTags(absl::string_view metric_name,
std::vector<Tag>& tags) const {
// 默认将default_tags_放到集合中
tags.insert(tags.end(), default_tags_.begin(), default_tags_.end());
.....
}
除了默认的Tag
外,剩余的Tag
需要依靠TagExactor
从指标名称中提取,两部份组合起来就是最终返回的Tag集合了。另外一个重要的数据成员是tag_extractors_without_prefix_
是用来保存默认提供的所有的不带前缀的TagExtractor
(比如_rq(_(\\d{3}))$", "_rq_
这个正则是用来提取response code
,这个Tag Value
的提取规则就是不带前缀的),每当需要产生Tag的时候就遍历这个vector,通过调用extractTag
方法来提取Tag。
std::string TagProducerImpl::produceTags(absl::string_view metric_name,
std::vector<Tag>& tags) const {
.....
// 遍历所有的TagEactor,一个个调用extractTag方法来产生Tag
forEachExtractorMatching(
metric_name, [&remove_characters, &tags, &metric_name](const TagExtractorPtr& tag_extractor) {
tag_extractor->extractTag(metric_name, tags, remove_characters);
});
....
}
可想而知,这些TagExactor
并不是每一个都会产生Tag,比如和集群相关的Tag提取规则肯定是没办法用来提取http相关的指标名称的。因此为了加速这部分的查找,就搞出了tag_extractor_prefix_map_
将TagExactor
按照前缀来分类(比如R"(^tcp\.((.*?)\.)\w+?$)"
这个正则就是用来提取tcp相关指标的,前缀就是tcp
)。提取Tag的时候,先提取指标的前缀,然后通过前缀找到可以用来提取Tag的TagExactor
,最后只需要遍历这些TagExactor
就可以高效的提取Tag了。
void TagProducerImpl::forEachExtractorMatching(
absl::string_view stat_name, std::function<void(const TagExtractorPtr&)> f) const {
IntervalSetImpl<size_t> remove_characters;
for (const TagExtractorPtr& tag_extractor : tag_extractors_without_prefix_) {
f(tag_extractor);
}
const absl::string_view::size_type dot = stat_name.find('.');
if (dot != std::string::npos) {
// 找指标的前缀
const absl::string_view token = absl::string_view(stat_name.data(), dot);
// 通过前缀找到对应的TagExactor
const auto iter = tag_extractor_prefix_map_.find(token);
if (iter != tag_extractor_prefix_map_.end()) {
// 遍历TagExactor进行Tag的提取
for (const TagExtractorPtr& tag_extractor : iter->second) {
f(tag_extractor);
}
}
}
上文中提到带前缀的提取规则和不带前缀的提取规则,我也列举了一些例子,下面我们来通过代码更深层次的理解一下。
std::string TagExtractorImpl::extractRegexPrefix(absl::string_view regex) {
std::string prefix;
//带前缀的正则一定是"^"开头,然后跟上一串前缀字符,最后结束是$、\\.或者?=\\.
if (absl::StartsWith(regex, "^")) {
for (absl::string_view::size_type i = 1; i < regex.size(); ++i) {
// 遍历正则,找到前缀字符串,前缀字符串的特点就是不是数字和下划线
// 直到遇到"点号"分割就结束,或者形如"^前缀字符串$"的形式。
if (!absl::ascii_isalnum(regex[i]) && (regex[i] != '_')) {
if (i > 1) {
const bool last_char = i == regex.size() - 1;
if ((!last_char && regexStartsWithDot(regex.substr(i))) ||
(last_char && (regex[i] == '$'))) {
prefix.append(regex.data() + 1, i - 1);
}
}
break;
}
}
}
return prefix;
}
// 没有\\.或$或?=\\.结束
EXPECT_EQ("", extractRegexPrefix("^prefix(foo)."));
// \\.结束
EXPECT_EQ("prefix", extractRegexPrefix("^prefix\\.foo"));
// ?=\\.结束
EXPECT_EQ("prefix_optional", extractRegexPrefix("^prefix_optional(?=\\.)"));
// 没有找到结束符
EXPECT_EQ("", extractRegexPrefix("^notACompleteToken"));
// ^前缀字符串$
EXPECT_EQ("onlyToken", extractRegexPrefix("^onlyToken$"));
// 没有^开头
EXPECT_EQ("", extractRegexPrefix("(prefix)"));
// 没有找到结束符
EXPECT_EQ("", extractRegexPrefix("^(prefix)"));
// 没有^开头
EXPECT_EQ("", extractRegexPrefix("prefix(foo)"));
简单点理解,前缀字符串一定是一个完整的部分,我们知道指标名称是按照"."号将Tag Value链接起来的,因此一个完整的前缀字符一定是"."号作为其结束部分,又或者这个指标只有一个部分组成,也就是不需要"."号结束。最后来看下通过TagProducer
提取Tag Value
的例子:
TEST(UtilityTest, createTagProducer) {
envoy::config::bootstrap::v2::Bootstrap bootstrap;
auto producer = Utility::createTagProducer(bootstrap);
ASSERT(producer != nullptr);
std::vector<Stats::Tag> tags;
auto extracted_name = producer->produceTags("http.config_test.rq_total", tags);
ASSERT_EQ(extracted_name, "http.rq_total");
ASSERT_EQ(tags.size(), 1);
}
// http.config_test.rq_total 是完整的指标名称,包含了tag,通过produceTags方法将这个完整的指标
// 进行了extracted。extracted后的名字就是http.rq_total,解析完后的Tag value就是config_test。
总结
本文主要讲解了Metric指标的基本组成,主要包含两个部分,一个是extraced指标名称,另外一个是Tag也就是Key/Value对,然后介绍到如何从完整的指标名称中提取Tag,Envoy中依赖正则来提取,默认提供了许多正则来提取Tag,用户也可以通过配置文件的方式来自定义Tag的提取规则。这部分主要是通过TagExactor
来完成。使用的时候是通过TagProducer
来完成,TagProducer
默认会构造好TagExactor
,当传入一个完整的指标名称时就通过遍历所有的TagExactor
来从指标中提取Tag Value,最后返回提取好的Tag和extraced指标名称。为了加速提取的速度,Envoy会将提取规则划分为带有前缀的和不带前缀的,查找的时候先提取指标的前缀,然后找到可以用来提取规则的一系列的TagExactor
来进行规则的提取。