Envoy Redis 源码分析 第2章 - x-lambda/note GitHub Wiki
序
本来这章应该直接贴redis
模块源码,然后一顿战术分析。但是考虑到envoy
对新手不太友好,因此还是再补充点相关知识。
协议
envoy
根据代理的协议把过滤器分为2种
L4
: TCP proxy
L7
: HTTP proxy
而实现这些代理的功能就是靠filter
实现的,L4
是TCP filter
,L7
是HTTP filter
。当然有的filter
很常用,envoy
已经帮我们写好了,例如HTTP
相关的,redis
,MySQL
之类的。当我们要扩展envoy
时,支持自定义协议时,实际上就是去添加这些filter
。
如果我们使用的是tcp
协议,就需要实现tcp filter proxy
。实现也不复杂,先声明一个FilterProxy class
,这个class
实现了指定的方法,然后把class
注册到envoy
中。这样我们启动envoy
时,通过配置文件中声明的listener
,就可以接管下游的流量。
filter
L4过滤器的接口非常简单,只有4个方法
class ReadFilter {
public:
// 当下游连接有数据过来时调用
virtual FilterStatus onData(Buffer::Instance& data, bool end_stream) PURE;
// 当下游有新的连接事件时调用
virtual FilterStatus onNewConnection() PURE;
// 初始化用于和过滤器管理器交互的读过滤器回调
// 过滤器被注册时,将被过滤器管理器调用一次,
// 任何需要用到底层连接的构造,需要在此函数的回调中执行
virtual void initializeReadFilterCallbacks(ReadFilterCallbacks& callbacks) PURE;
};
class WriteFilter {
public:
// 当在此连接上发生数据写入时调用
virtual FilterStatus onWrite(Buffer::Instance& data, bool end_stream) PURE;
};
class Filter: public ReadFilter, public WriteFilter {};
每个filter
都要实现以上的方法,然后根据配置和代码的规则依次去回调对应的方法,从而构成完整的代理链路。
envoy
收到客户端的请求时,可以通过一些列filter func
,这些函数各有各的功能,如果不符合预期,还可以将请求拦截,不再往下透传。
熟悉select/epoll
编程的人大概一眼就看明白了。
自定义filter
官方给了两个自定义filter
的示例,其中一个就是tcp filter
,项目地址:https://github.com/envoyproxy/envoy-filter-example
,核心文件如下图展示
这个filter
实现功能很简单,收到客户端的数据,打印一条日志,然后将数据原封不动的返回回去。下面我们挨个文件分析。
-
查看配置文件,如何配置tcp代理
# 官方给的配置文件有点问题,需要读者替换一些端口,envoy才能正常运行 # 这里配置一个管理后台,用户在浏览器输入 http://127.0.0.1:8080/ 可以看到envoy的一些内部信息 admin: access_log_path: /dev/null address: socket_address: address: 127.0.0.1 port_value: 8080 static_resources: clusters: # 这里本该配置一些服务端实例,但是由于这个proxy在filter中直接把数据回吐出去了,不需要转发给上游机器 # 因此这里的 clusters 其实没必要配置 name: cluster_0 connect_timeout: 0.25s load_assignment: cluster_name: cluster_0 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 0 listeners: # 这里就是配置tcp代理 # 代理端口 12345 # proxy在这个端口接收下游数据,收到数据之后,会依次寻找 filter,这里只有1个filter叫 echo2 name: listener_0 address: socket_address: address: 127.0.0.1 port_value: 12345 filter_chains: - filters: - name: echo2 config:
-
如何注册
filter
在上面的配置文件,我们看到
envoy
监听着12345
端口,并且收到数据之后,会找一个叫echo2
的filter
,告诉他收到下游的数据,你来处理一下。那么envoy
是怎么找到我们添加的filter
呢?我们接着看echo2_config.cc
文件/** * Config registration for the echo2 filter. @see NamedNetworkFilterConfigFactory. */ class Echo2ConfigFactory : public NamedNetworkFilterConfigFactory { public: Network::FilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message&, FactoryContext&) override { return [](Network::FilterManager& filter_manager) -> void { filter_manager.addReadFilter(Network::ReadFilterSharedPtr{new Filter::Echo2()}); }; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return ProtobufTypes::MessagePtr{new Envoy::ProtobufWkt::Struct()}; } // filter 的名字叫 echo2 std::string name() const override { return "echo2"; } bool isTerminalFilter() override { return true; } }; /** * Static registration for the echo2 filter. @see RegisterFactory. * 这里就是在向envoy注册一个filter */ static Registry::RegisterFactory<Echo2ConfigFactory, NamedNetworkFilterConfigFactory> registered_;
根据上面的代码,加上猜测可以知道,配置中指定了一个代理端口,这个端口上收到的数据,会交给
echo2
这个filter
处理,然后我们在代码中注册了一个filter:Filter::Echo2
,这filter
的名字就叫echo2
。事实也确实如此。 -
filter
如何实现功能我们看见在上面的文件里,已经注册了一个
filter
,而根据之前说的每个filter
会实现几个方法供envoy
回调,我们再接着看回调的逻辑#pragma once #include "envoy/network/filter.h" #include "common/common/logger.h" namespace Envoy { namespace Filter { /** * Implementation of a basic echo filter. */ class Echo2 : public Network::ReadFilter, Logger::Loggable<Logger::Id::filter> { public: // Network::ReadFilter Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; // 收到下游的连接请求,这里表示放行,即允许建立连接 Network::FilterStatus onNewConnection() override { return Network::FilterStatus::Continue; } void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) override { read_callbacks_ = &callbacks; } private: Network::ReadFilterCallbacks* read_callbacks_{}; }; } // namespace Filter } // namespace Envoy
#include "echo2.h" #include "envoy/buffer/buffer.h" #include "envoy/network/connection.h" #include "common/common/assert.h" namespace Envoy { namespace Filter { /* * envoy 收到下游的数据之后会回调这个方法 */ Network::FilterStatus Echo2::onData(Buffer::Instance& data, bool) { // 打印日志 ENVOY_CONN_LOG(trace, "echo: got {} bytes", read_callbacks_->connection(), data.length()); // 把收到的数据回写 read_callbacks_->connection().write(data, false); // 不再往下执行 // 意思是有多个filter的话,收到的数据不用再传给后面filter处理 return Network::FilterStatus::StopIteration; } } // namespace Filter } // namespace Envoy
-
构建
将我们的
filter
代码集成到envoy
中,这是bazel
项目的构建方式bazel build //:envoy
如上,这是一个简单自定义filter
的过程,如果还有更复杂的协议,只需要在onData()
函数中做对应的逻辑即可。
思考
上面的示例,没有添加真正的处理服务机器,如果需要将请求转发给上游,代码该如何写?