Envoy 源码分析--buffer

申明:本文的 Envoy 源码分析基于 Envoy1.10.0。

Envoybuffer 在 1.10.0 前是基于 libevent 的 evbuffer 进行封装。在 1.10.0 开始为了提高性能,要使用 libev 或 libuv 来替代 libevent 重写了个 buffer 来消除 evbuffer 的依赖。想要具体了解可看 issue#4952issue#5441。下面我们先来看下 buffer 相关的类图:

上面四个 slice 相关的类是重写后的 bufferslice 封装了 buffer 相关操作,OwnedSlice 是创建 slice 的类,SliceDeque 管理 OwnedSlice 操作队列,UnownedSlice 则是兼容 BufferFragment 的类。

LibEventInstance 继承自 InstanceInstance 的接口进行扩充新增了两个接口。OwnedImplLibEventInstance 接口的实现。WatermarkBuffer 则在 OwnedImpl 的基础上增加水位的高低告警回调。WatermarkBufferFactory 继承 WatermarkFactory 只是生成 WatermarkBuffer 的适配器。

BufferFragment

针对外部数据和大小创建一个适配器,这个类不拷贝内存,只是一个指针,外部需要自己确定数据是有效的。调用 done() 会释放数据。

代码详情如下:

class BufferFragmentImpl : NonCopyable, public BufferFragment {
public:
BufferFragmentImpl(
const void* data, size_t size,
const std::function<void(const void*, size_t, const BufferFragmentImpl*)>& releasor)
: data_(data), size_(size), releasor_(releasor) {} // Buffer::BufferFragment
const void* data() const override { return data_; }
size_t size() const override { return size_; }
void done() override {
if (releasor_) {
releasor_(data_, size_, this);
}
} private:
const void* const data_;
const size_t size_;
const std::function<void(const void*, size_t, const BufferFragmentImpl*)> releasor_;
};

RawSlice

只是一个内存切片的数据结构体。

struct RawSlice {
void* mem_ = nullptr;
size_t len_ = 0; bool operator==(const RawSlice& rhs) const { return mem_ == rhs.mem_ && len_ == rhs.len_; }
};

Slice

Slice 是一个连续的内存块的管理。其块的管理方式如下:

 *                   |<- data_size() -->|<- reservable_size() ->|
* +-----------------+------------------+-----------------------+
* | Drained | Data | Reservable |
* | Unused space | Usable content | New content can be |
* | that formerly | | added here with |
* | was in the Data | | reserve()/commit() |
* | section | | |
* +-----------------+------------------+-----------------------+
* ^
* |
* data()

前面是个无用内存块,中间这块就是数据自身,后面是还可用的内存块长度。增加数据支持 prependappend 即可追加数据也可以前置增加。

Slice 的数据存储方式如下:

  /** 切片指针 */
uint8_t* base_{nullptr}; /** 存储数据相对于切片的偏移量 */
uint64_t data_; /** 可用空间相对于切片的偏移量 */
uint64_t reservable_; /** 切片总长度 */
uint64_t capacity_; /** 是否被`Limter`限制以延迟操作 */
bool reservation_outstanding_{false};

现在来看几个主要的操作。

append 在后面追加数据。增加尽量多的数量,不保证所有数据都增加成功。这个在空间不够时,没有重新申请空间。先判断数据是否被锁住了,接着取可用长度和增加长度的最小值,可用空间偏移量后移,copy 数据到块中。

  uint64_t append(const void* data, uint64_t size) {
if (reservation_outstanding_) {
return 0;
}
uint64_t copy_size = std::min(size, reservableSize());
uint8_t* dest = base_ + reservable_;
reservable_ += copy_size;
memcpy(dest, data, copy_size);
return copy_size;
}

prepend 前置增加数据。同 append 一样没有申请空间,只是增加尽量多的数据,不保证所有数据都增加成功。先判断数据是否被锁住了,然后判断切片内是否有数据。如果没有数据直接增加到最后面,加在最后面的目的是为了后面继续 prepend 时有空间可加(这里没想明白,为啥要在最后面加,这样我下次用 append 不也一样没法增加数据)。后面取出前置可用长度和增加长度的最小值,data 的偏移量后移, copy 数据块。

  uint64_t prepend(const void* data, uint64_t size) {
if (reservation_outstanding_) {
return 0;
}
const uint8_t* src = static_cast<const uint8_t*>(data);
uint64_t copy_size;
if (dataSize() == 0) {
copy_size = std::min(size, reservableSize());
reservable_ = capacity_;
data_ = capacity_ - copy_size;
} else {
if (data_ == 0) {
return 0;
}
copy_size = std::min(size, data_);
data_ -= copy_size;
}
memcpy(base_ + data_, src + size - copy_size, copy_size);
return copy_size;
}

drain 删除数据,这个只是偏移量后移。

  void drain(uint64_t size) {
ASSERT(data_ + size <= reservable_);
data_ += size;
if (data_ == reservable_ && !reservation_outstanding_) {
data_ = reservable_ = 0;
}
}

reservecommit 是合起来一起用的。reserve 返回可用内存块同时将切片内存块锁住,不让增加和移除,直到 commit 才恢复正常。reserve 返回的内存块可由用户自己填充。reserve 时如果增加进来的内存块不是同个切片下的会增加失败。

  Reservation reserve(uint64_t size) {
if (reservation_outstanding_ || size == 0) {
return {nullptr, 0};
}
uint64_t available_size = capacity_ - reservable_;
if (available_size == 0) {
return {nullptr, 0};
}
uint64_t reservation_size = std::min(size, available_size);
void* reservation = &(base_[reservable_]);
reservation_outstanding_ = true;
return {reservation, reservation_size};
} bool commit(const Reservation& reservation) {
if (static_cast<const uint8_t*>(reservation.mem_) != base_ + reservable_ ||
reservable_ + reservation.len_ > capacity_ || reservable_ >= capacity_) {
// The reservation is not from this OwnedSlice.
return false;
}
ASSERT(reservation_outstanding_);
reservable_ += reservation.len_;
reservation_outstanding_ = false;
return true;
}

OwnedSlice

只是创建和删除 Slice 指针。

  static SlicePtr create(uint64_t capacity) {
uint64_t slice_capacity = sliceSize(capacity);
return SlicePtr(new (slice_capacity) OwnedSlice(slice_capacity));
} static void operator delete(void* address) { ::operator delete(address); }

SliceDeque

Slice 管理队列。支持前后置增加和删除。默认队列长度 8,当长度不够用时,会成倍自增长。

这代码比较简单。这里只列出自增长函数。

  void growRing() {
if (size_ < capacity_) {
return;
}
const size_t new_capacity = capacity_ * 2;
auto new_ring = std::make_unique<SlicePtr[]>(new_capacity);
for (size_t i = 0; i < new_capacity; i++) {
ASSERT(new_ring[i] == nullptr);
}
size_t src = start_;
size_t dst = 0;
for (size_t i = 0; i < size_; i++) {
new_ring[dst++] = std::move(ring_[src++]);
if (src == capacity_) {
src = 0;
}
}
for (size_t i = 0; i < capacity_; i++) {
ASSERT(ring_[i].get() == nullptr);
}
external_ring_.swap(new_ring);
ring_ = external_ring_.get();
start_ = 0;
capacity_ = new_capacity;
}

UnownedSlice

管理 BufferFragment 的特殊 Slice

class UnownedSlice : public Slice {
public:
UnownedSlice(BufferFragment& fragment)
: Slice(0, fragment.size(), fragment.size()), fragment_(fragment) {
base_ = static_cast<uint8_t*>(const_cast<void*>(fragment.data()));
}
~UnownedSlice() override { fragment_.done(); }
private:
BufferFragment& fragment_;
};

OwnedImpl

OwnedImplLibEventInstance 接口的实现,同时为了支持新 buffer 在数据操作时,会有一个 bool 值来判断调用的是新 buffer 还是旧 buffer。新的 buffer 在 1.10.0 默认情况下是关闭的,可通过配置 –use-libevent-buffers 0 打开。

OwnedImpl::OwnedImpl() : old_impl_(use_old_impl_) {
if (old_impl_) {
buffer_ = evbuffer_new();
}
}

现在我们在分析下几个重要的操作:

add 增加数据。几个 add 的操作最终都是调用 add(const void* data, uint64_t size)。

void OwnedImpl::add(const void* data, uint64_t size) {
//判断是否为旧的buffer
if (old_impl_) {
//直接调用
evbuffer_add(buffer_.get(), data, size);
} else {
const char* src = static_cast<const char*>(data);
//检查是否需要新加切片。如果切片为空,需要新加。
bool new_slice_needed = slices_.empty();
//循环加入。数据加入切片,一个切片并不一定能把数据全都加进去,所以需要循环加入。
while (size != 0) {
if (new_slice_needed) {
//创建切片并加入切片队列
slices_.emplace_back(OwnedSlice::create(size));
}
//切片加入数据并返回加入的长度
uint64_t copy_size = slices_.back()->append(src, size);
//增加偏移量
src += copy_size;
//看是否还有数据未加入。size 大于 0 说明上个切片已满,需要重新创建切片。
size -= copy_size;
length_ += copy_size;
new_slice_needed = true;
}
}
}

prepend 这个和 add 接口类似,需要的自行看源码。

readIoHandle 中读取数据,并加入 buffer

Api::IoCallUint64Result OwnedImpl::read(Network::IoHandle& io_handle, uint64_t max_length) {
//长度判断,一次读取最大长度不能为0.
if (max_length == 0) {
return Api::ioCallUint64ResultNoError();
}
//申明一个RawSlice
constexpr uint64_t MaxSlices = 2;
RawSlice slices[MaxSlices];
//锁住操作的内存块
const uint64_t num_slices = reserve(max_length, slices, MaxSlices);
//readv读取数据,数据存在slices
Api::IoCallUint64Result result = io_handle.readv(max_length, slices, num_slices);
//旧buffer
if (old_impl_) {
if (!result.ok()) {
//错误直接返回
return result;
}
uint64_t num_slices_to_commit = 0;
//读取到的长度
uint64_t bytes_to_commit = result.rc_;
ASSERT(bytes_to_commit <= max_length);
//RawSlice检测,检查slices有几个。
while (bytes_to_commit != 0) {
//空间判断,看下一次能加多少数据长度。
slices[num_slices_to_commit].len_ =
std::min(slices[num_slices_to_commit].len_, static_cast<size_t>(bytes_to_commit));
ASSERT(bytes_to_commit >= slices[num_slices_to_commit].len_);
bytes_to_commit -= slices[num_slices_to_commit].len_;
num_slices_to_commit++;
}
//不能大于RawSlice本身数组长度,否则直接断言。
ASSERT(num_slices_to_commit <= num_slices);
//提交数据到buffer
commit(slices, num_slices_to_commit);
//新buffer
} else {
uint64_t bytes_to_commit = result.ok() ? result.rc_ : 0;
ASSERT(bytes_to_commit <= max_length);
//新的buffer reserve操作返回的是slice的个数。循环判断。决定 Slice的长度。
for (uint64_t i = 0; i < num_slices; i++) {
slices[i].len_ = std::min(slices[i].len_, static_cast<size_t>(bytes_to_commit));
bytes_to_commit -= slices[i].len_;
}
//提交数据到buffer
commit(slices, num_slices);
}
return result;
}

writebuffer 中读数据。调用writev。

Api::IoCallUint64Result OwnedImpl::write(Network::IoHandle& io_handle) {
constexpr uint64_t MaxSlices = 16;
RawSlice slices[MaxSlices];
//获取写入的数据和大小。
const uint64_t num_slices = std::min(getRawSlices(slices, MaxSlices), MaxSlices);
//写入数据
Api::IoCallUint64Result result = io_handle.writev(slices, num_slices);
if (result.ok() && result.rc_ > 0) {
//写入成功,将数据从buffer中清除。
drain(static_cast<uint64_t>(result.rc_));
}search
return result;
}

WatermarkBuffer

WatermarkBuffer 是在 OwnedImpl 的基础上增加了高低水位告警回调,在代码还是调用了 OwnedImpl

  std::function<void()> below_low_watermark_;
std::function<void()> above_high_watermark_; // Used for enforcing buffer limits (off by default). If these are set to non-zero by a call to
// setWatermarks() the watermark callbacks will be called as described above.
uint32_t high_watermark_{0};
uint32_t low_watermark_{0};

我们来看其中一个函数:

Api::IoCallUint64Result WatermarkBuffer::read(Network::IoHandle& io_handle, uint64_t max_length) {
//调用OwnedImpl的函数
Api::IoCallUint64Result result = OwnedImpl::read(io_handle, max_length);
//高水位检测
checkHighWatermark();
return result;
}

WatermarkBufferFactory

WatermarkBuffer 的适配器,只是用来创建 WatermarkBuffer 的。

ZeroCopyInputStreamImpl

ZeroCopyInputStreamImpl 可以把它认为是 Buffer::InstancePtr 的一个迭代器。

  //数据在buffer中的位移
uint64_t position_{0};
//结束标志
bool finished_{false};
//NEXT过的所有字节数。
uint64_t byte_count_{0};

Envoy 源码分析--buffer的更多相关文章

  1. Envoy 源码分析--network L4 filter manager

    目录 Envoy 源码分析--network L4 filter manager FilterManagerImpl addWriteFilter addReadFilter addFilter in ...

  2. Envoy 源码分析--network

    目录 Envoy 源码分析--network address Instance DNS cidr socket Option Socket ListenSocket ConnectionSocket ...

  3. Envoy 源码分析--event

    目录 Envoy 源码分析--event libevent Timer SignalEvent FileEvent RealTimeSystem 任务队列 延迟析构 dispacth_thread E ...

  4. Envoy 源码分析--程序启动过程

    目录 Envoy 源码分析--程序启动过程 初始化 main 入口 MainCommon 初始化 服务 InstanceImpl 初始化 启动 main 启动入口 服务启动流程 LDS 服务启动流程 ...

  5. Envoy 源码分析--LDS

    Envoy 源码分析--LDS LDS 是 Envoy 用来自动获取 listener 的 API. Envoy 通过 API 可以增加.修改或删除 listener. 先来总结下 listener ...

  6. Netty源码分析第7章(编码器和写数据)---->第3节: 写buffer队列

    Netty源码分析七章: 编码器和写数据 第三节: 写buffer队列 之前的小节我们介绍过, writeAndFlush方法其实最终会调用write和flush方法 write方法最终会传递到hea ...

  7. Netty源码分析第7章(编码器和写数据)---->第4节: 刷新buffer队列

    Netty源码分析第七章: 编码器和写数据 第四节: 刷新buffer队列 上一小节学习了writeAndFlush的write方法, 这一小节我们剖析flush方法 通过前面的学习我们知道, flu ...

  8. netty(六) buffer 源码分析

    问题 : netty的 ByteBuff 和传统的ByteBuff的区别是什么? HeapByteBuf 和 DirectByteBuf 的区别 ? HeapByteBuf : 使用堆内存,缺点 ,s ...

  9. Buffer的创建及使用源码分析——ByteBuffer为例

    目录 Buffer概述 Buffer的创建 Buffer的使用 总结 参考资料 Buffer概述 注:全文以ByteBuffer类为例说明 在Java中提供了7种类型的Buffer,每一种类型的Buf ...

随机推荐

  1. centos上发布部署python的tornado网站项目完整流程

    先说下大体上的做法,开发环境上要新弄一个 virtualenv的环境,在这个里面放你的开发调试,当然这个其实也不是必须的,但是这样会方便管理一些. 再在centos上也弄一个 virtualenv虚拟 ...

  2. document.wrtie()用法

    推荐看这篇文章,非常的好 http://www.softwhy.com/article-8326-1.html

  3. A Boring Question (打表)

    题意:由m个0到n组合的数的相邻两项的组合数的乘积. 思路:好好打表!!!找规律!!! #include<bits/stdc++.h> using namespace std; typed ...

  4. Angular4 响应式编程

  5. 如何提高单片机C语言代码效率

    代码效率包括两个方面内容:代码的大小和代码执行速度.如果代码精简和执行速度快,我们就说这个代码效率高.一般情况下,代码精简了速度也相应提上来了.单片机的ROM和RAM的空间都很有限,当您编程时遇到单片 ...

  6. windows 系统验证是否为正版

    博客园里边写这种帖子,足以证明我有多无聊.话不多说,上干货. 一台计算器如果没有操作系统,就是一块大的板砖,拿起来抡人太重,放地上做床又太小. 如何查看自己操作系统呢?windows7 桌面找到我的电 ...

  7. 解决Postman User-Agent 设置失效

    问题: 设置header中的UserAgent选项,抓包以后依然还是默认头信息 test Domain www.baidu.com Iphone6 UserAgent访问效果 User-Agent: ...

  8. 解决ping不通win7主机

    之前在路由器上ping笔记本发现ping不通,但是笔记本ping路由器通,也没多想.今天想起来可能是win7的防火墙作怪,以前上课虚拟机好像也是ping不通宿主机,但是宿主机能ping通虚拟机. 简单 ...

  9. ldap集成bitbucket

    confluence ldap配置跟jira ldap集成一样,请参考:https://www.cnblogs.com/imcati/p/9378668.html 需在 Global permissi ...

  10. [c/c++] programming之路(31)、位运算(二)

    一.取反的高级用法 #include<stdio.h> #include<stdlib.h> //取反的作用:末位清零 取反适用于各种位数不同的数据 void main0(){ ...