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. A Boring Question (打表)

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

  2. python selenium web自动化测试完整项目实例

    问题: 好多想不到的地方,中间经历了一次重构,好蛋疼: xpath定位使用的不够熟练,好多定位问题,只能靠强制等待解决: 存在功能重复的方法,因为xpath定位不同,只能分开写,有时间可以继续优化: ...

  3. webservice常用两种身份验证方式

    在项目开发,我们经常会使用WebService,但在使用WebService时我们经常会考虑以下问题:怎么防止别人访问我的WebService?从哪里引用我的WebService?对于第一个问题,就涉 ...

  4. Linux 下的分屏利器-tmux安装、原理及使用

    >> 原文地址

  5. Vue 组件&组件之间的通信 之 子组件向父组件传值

    子组件向父组件传值:子组件通过$.emit()方法以事件形式向父组件发送消息传值: 使用步骤: 定义组件:现有自定义组件com-a.com-b,com-a是com-b的父组件: 准备获取数据:父组件c ...

  6. 拒绝QQ空间-手把手教你美化博客

    为什么要美化? 博客园的主题看起来是有一些年代感了,应该是不符合当代大学生的审美了,起码我就觉得不行,所以我们要进行一些美化,但是搞技术的人的博客不应该搞得花里胡哨,最好是简洁一些(个人想法),网上有 ...

  7. VMware虚拟机扩展Ubuntu系统磁盘空间

    1 首先给虚拟机扩容 虚拟机->设置->硬盘->实用程序->扩展磁盘容量 2 启动Ubuntu系统 2.1 打开终端安装gparted,sudo apt-get install ...

  8. SpringMVC,SpringBoot利用ajax上传文件到后台

    1.传递单文件 首先html文件中有个<input type=”file” name=”file” id=”file”/>元素. 前台js写法: var formData=new Form ...

  9. javascript面向对象知识

    <html> <head> <script></script> </head> <body> <!-- <scrip ...

  10. 王之泰/王志成《面向对象程序设计(java)》第十一周学习总结

    第一部分:理论知识学习部分 第十一章理论知识主要为集合类的介绍,在实验中都有所体现且本周主要复习回顾上周的泛型程序设计 第二部分:实验部分 ——集合 1.实验目的与要求 (1) 掌握Vetor.Sta ...