Istio Mixer Cache工作原理与源码分析(3)-主流程

经过前面基本概念和实现原理的介绍,大家对mixer check cache应该有了基本的了解,下面我们开始展开源代码来细细研读。

Check Cache的主要流程

Check Cache的调用入口

对mixer cache的调用在代码 proxy/src/istio/mixerclient/client_impl.cc 中的方法Check()中,此处跳过quota cache的内容:

CancelFunc MixerClientImpl::Check(
    const Attributes &attributes,
    const std::vector<::istio::quota_config::Requirement> &quotas,
    TransportCheckFunc transport, CheckDoneFunc on_done) {
	++total_check_calls_;
    
    std::unique_ptr<CheckCache::CheckResult> check_result(
        new CheckCache::CheckResult);
    // 在这里调用了CheckCache.Check()方法,进行检查
    check_cache_->Check(attributes, check_result.get()); 
    CheckResponseInfo check_response_info;

    check_response_info.is_check_cache_hit = check_result->IsCacheHit();
    check_response_info.response_status = check_result->status();

    // 如果check cache命中,并且结果不OK,则直接结束处理
    if (check_result->IsCacheHit() && !check_result->status().ok()) {
        on_done(check_response_info);
        return nullptr;
    }
    ......
    CheckCache::CheckResult *raw_check_result = check_result.release();
    ......
    // 如果check cache没有命中,则需要发起请求到mixer得到response
    // 然后将response加入check cache中
    return transport(
      request, response,
      [this, request_copy, response, raw_check_result, raw_quota_result,
       on_done](const Status &status) {
        raw_check_result->SetResponse(status, *request_copy, *response);
        ......
	});
}

我们先来看看缓存的保存方式,再返回来看Check()方法的具体实现,这样方便理解。

这里的transport是一个TransportCheckFunc,具体定义在include/istio/mixerclient/environment.h 头文件中:

// Defines a function prototype to make an asynchronous Check call
using TransportCheckFunc = std::function<CancelFunc(
    const ::istio::mixer::v1::CheckRequest& request,
    ::istio::mixer::v1::CheckResponse* response, DoneFunc on_done)>;

其中DoneFunc的定义如下:

// Defines a function prototype used when an asynchronous transport call
// is completed.
// Uses UNAVAILABLE status code to indicate network failure.
using DoneFunc = std::function<void(const ::google::protobuf::util::Status&)>;

总结说,Check()方法会通过TransportCheckFunc对mixer发起请求,在得到response之后,再调用DoneFunc。

在这个匿名的DoneFunc中,最关键的代码是:

raw_check_result->SetResponse(status, *request_copy, *response);

这里的 raw_check_result 类型是 CheckCache::CheckResult。

保存Check结果

CheckResult.SetResponse()方法的源代码在 istio/proxy 项目, src/istio/mixerclient/check_cache.h 文件中

void SetResponse(const ::google::protobuf::util::Status& status,
                 const ::istio::mixer::v1::Attributes& attributes,
                 const ::istio::mixer::v1::CheckResponse& response) {
    if (on_response_) {
        // 调用on_response_这个Func
        status_ = on_response_(status, attributes, response);
    }
}

// The function to set check response.
using OnResponseFunc = std::function<::google::protobuf::util::Status(
    const ::google::protobuf::util::Status&,
    const ::istio::mixer::v1::Attributes& attributes,
    const ::istio::mixer::v1::CheckResponse&)>;
OnResponseFunc on_response_; // on_response_在此定义

on_response_这个OnResponseFunc的设定在 src/istio/mixerclient/check_cache.cc 文件中的 CheckCache::Check() 方法中设置:

result->on_response_ = [this](const Status &status,
                              const Attributes &attributes,
                              const CheckResponse &response) -> Status {
    if (!status.ok()) {
        // status表示对mixer的远程调用的结果
        // 如果调用都没有成功,则没有check结果可言,自然不必缓存
        if (options_.network_fail_open) {
            // 注意这里有个选项,如果打开,则在mixer调用没有成功时视为check成功
            return Status::OK;
        } else {
            return status;
        }
    } else {
        // 如果对mixer调用成功,拿到了response,则进行缓存
        return CacheResponse(attributes, response, system_clock::now());
    }
};

下面是保存缓存的关键代码了,CacheResponse() 方法,清晰起见,忽略分支处理和错误处理代码,日志打印等:

Status CheckCache::CacheResponse(const Attributes &attributes,
                                 const CheckResponse &response, Tick time_now) {
  ......
  // 类Referenced用来保存引用属性,也就是哪些属性被mixer adapter使用了
  // 记得前面讲述实现原理时的例子吗?这里的Referenced就是"a,b,c"
  Referenced referenced;
  // Fill()方法解析response的precondition的referenced_attributes并填充到referenced
  if (!referenced.Fill(attributes,
                       response.precondition().referenced_attributes())) {
	......
  }
  
  std::string signature;
  // 调用Signature()方法进行签名
  if (!referenced.Signature(attributes, "", &signature)) {
	......
  }
  
  // 进行第一层缓存的保存
  // referenced_map用于保存各种引用属性的组合
  // 在实现原理中的例子,就是"a,b,c"和带absence key的"a,b","a,c"等
  std::string hash = referenced.Hash();
  // 计算当前引用属性的hash值,比如"a,b,c"的hash值
  // 然后保存进referenced_map
  if (referenced_map_.find(hash) == referenced_map_.end()) {
    referenced_map_[hash] = referenced;
  }
  
  // 进行第二层缓存的保存
  // 用计算而来的签名来在第二层缓存中查找
  CheckLRUCache::ScopedLookup lookup(cache_.get(), signature);
  if (lookup.Found()) {
    // 如果已经存在则更新CacheElem
    lookup.value()->SetResponse(response, time_now);
    return lookup.value()->status();
  }
  
  // 如果不存在则插入新的CacheElem
  CacheElem *cache_elem = new CacheElem(*this, response, time_now);
  cache_->Insert(signature, cache_elem, 1);
  return cache_elem->status();
}

缓存的结构和保存方式

我们现在可以结合代码来详细的展开缓存的结构和保存方式了:

// 第一层缓存,用于保存引用属性
// 这个map中,value是Referenced对象,key是Referenced的hash值
std::unordered_map<std::string, Referenced> referenced_map_;

// 第二层缓存,用于保存check的结果
// 这是一个 LRU Cache,key是请求的签名,value是check的结果,封装为CacheElem
using CheckLRUCache = utils::SimpleLRUCache<std::string, CacheElem>;
std::unique_ptr<CheckLRUCache> cache_;

Referenced 中保存使用的属性,包括absence的属性:

// The keys should be absence.
std::vector<AttributeRef> absence_keys_;

// The keys should match exactly.
std::vector<AttributeRef> exact_keys_;

以前面的例子为例,“a,b,c不存在"这种引用属性组合,在保存时就是a/b保存在exact_keys中,而c保存在absence_keys中。

CacheElem中保存的是检查结果:

::google::protobuf::util::Status status_;
std::chrono::time_point<std::chrono::system_clock> expire_time_;
int use_count_;

其中status字段是结果,而expire_time和use_count是这个缓存项的过期时间和使用次数,这个细节后面再展开。

两层缓存key值的计算,会比较有意思,我们会在下一节中详细展开,这一节我们先关注在缓存的主要流程。我们继续看缓存的check()方法是如何实现的,即怎么匹配请求和缓存。

匹配请求和缓存

每一个请求,都要和 mixer check cache 匹配一下,看是否可以命中缓存从而避免对mixer的远程调用。有了前面的铺垫,再来看缓存是如何匹配就简单了:

check_cache_->Check(attributes, check_result.get()); 

源代码在 src/istio/mixerclient/check_cache.cc 中,去除错误处理和细节处理代码之外,主要代码为:

Status CheckCache::Check(const Attributes &attributes, Tick time_now) {
    // 游历第一层缓存,
    // referenced_map中保存的是所有保存的引用属性组合中
    // 在我们前面的例子中,这里保存有 {“k1”: “a,b,c”, “k2”: “a,b,c不存在” } 等
    for (const auto &it : referenced_map_) {
        const Referenced &reference = it.second;
        std::string signature;
        // 在所有保存的引用属性组合中,逐个匹配看请求是否具备保存的引用属性结合
        // 如果引用属性和请求中的属性匹配,则计算签名
        if (!reference.Signature(attributes, "", &signature)) {
          continue;
        }

        // 进行第二层缓存的查找
        // 第二层缓存的key是签名,因此简单通过key查找就OK
        CheckLRUCache::ScopedLookup lookup(cache_.get(), signature);
        if (lookup.Found()) {
          CacheElem *elem = lookup.value();
          return elem->status();
        }
      }

  // 如果游历完第一层缓存,也没能找到匹配的引用属性,则只能返回NOT_FOUND
  return Status(Code::NOT_FOUND, "");
}

注意在Signature()方法中实际做了两个事情:

  1. 在referenced_map查找看是否有匹配的引用属性组合
  2. 如果有,则计算签名,以便通过签名来进行第二层缓存的查找

这个Signature()方法的实现是整个mixer check cache的重中之重,核心所在。

篇幅原因,我们将在下一节中详细展开Signature()方法实现的源代码解析。

总结

Mixer Check Cache的主流程代码,在了解了基本概念和工作原理之后,理解起来并不困难。代码本身并没有特别费解的地方,麻烦之处在于对 Referenced Attribute / 引用属性这个概念的理解。

敖小剑
敖小剑
新时代农民工 * 中年码农

我目前研究的方向主要在Microservice、Servicemesh、Serverless等Cloud Native相关的领域,全职从事Dapr开发,欢迎交流和指导。