Envoy源码分析之Stats基础

简介

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: 统计进来的连接和请求,指标来源于 ListenersHTTP connection managerTCP proxy filter 等。
  • Upstream: 统计出去的连接和请求,指标来源于 connection poolsrouter filterTCP 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 还有类型,总共有三种,就是上文中提到的CountersGaugeHistogram等。我们在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, CounterImplGaugeImpl 等都是继承 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] == '

简单点理解,前缀字符串一定是一个完整的部分,我们知道指标名称是按照".“号将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 来进行规则的提取。

参考文献:

https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/statistics