网站首页 > 开源技术 正文
1. 微服务框架Tars
Tars是【基于名字服务】【使用Tars协议】的高性能【RPC】开发框架,同时配套一体化的【服务治理平台】,帮助个人或者企业快速的以微服务的方式构建自己稳定可靠的分布式应用。
Tars在腾讯内部名为TAF,内部从08年开始使用,到现在将近10个年头了,今年终于开源。目前该框架在腾讯内部,有100多个业务、1.6多万台服务器上运行使用。
Tars也是一个兼顾易用性、高性能、服务治理的框架,目的是让开发更简单,聚焦业务逻辑,让运营更高效,一切尽在掌握。
目前,我日常开发也是使用该框架,十分方便快捷,而且配套的服务管理系统,为服务监控、运维带来了很大的便利。
代码地址:https://github.com/Tencent/Tars
1.1 设计思想
Tars的设计思路是采用微服务的思想对服务进行治理,同时对整个系统的各个模块进行抽象分层,将各个层次之间相互解耦或者松耦合,如下图:
最底的协议层,设计思路是将业务网络通信的协议进行统一,以IDL(接口定义语言)的方式,开发支持多平台、可扩展、【协议代码自动生成】的统一协议。在开发过程中,开发人员只需要关注通讯的协议字段的内容,不需要关注其实现的细节,大大减轻了开发服务时需要考虑的协议是否能跨平台使用、是否可能需要兼容、扩展等问题。
中间的公共库、通讯框架、平台层,设计思路是让业务开发更加聚焦业务逻辑的本身。因此,从使用者的角度出发,封装了大量日常开发过程中经常使用的公共库代码和远程过程调用,让开发使用更简单方便;从框架本身的角度出发,做到高稳定性、高可用性、高性能,这样才能让业务服务运营更加放心;从分布式平台的角度出发,解决服务运营过程中,遇到的容错、负载均衡、容量管理、就近接入、灰度发布等问题,让平台更加强大。
最上面的运营层,设计思路是让运维只需要关注日常的服务部署、发布、配置、监控、调度管理等操作。
1.2 整体架构
1.2.1 整体架构
整体架构的拓扑图主要分为2个部分:服务节点与公共框架节点。
服务节点:
服务节点可以认为是服务所实际运行的一个具体的操作系统实例,可以是物理主机或者虚拟主机、云主机。随着服务的种类扩展和规模扩大,服务节点可能成千上万甚至数以十万计。
每台服务节点上均有一个Node服务节点和N(N>=0)个业务服务节点,Node服务节点会对业务服务节点进行统一管理,提供启停、发布、监控等功能,同时接收业务服务节点上报过来的心跳。
公共框架节点:
除了服务节点以外的服务,其他服务节点均归为一类。
公共框架节点,数量不定,为了自身的容错容灾,一般也要求在在多个机房的多个服务器上进行部署,具体的节点数量,与服务节点的规模有关,比如,如果某些服务需要打较多的日志,就需要部署更多的日志服务节点。
又可细分为如下几个部分:
Web管理系统:在Web上可以看到服务运行的各种实时数据情况,以及对服务进行发布、启停、部署等操作;
Registry(路由+管理服务):提供服务节点的地址查询、发布、启停、管理等操作,以及对服务上报心跳的管理,通过它实现服务的注册与发现;
Patch(发布管理):提供服务的发布功能;
Config(配置中心):提供服务配置文件的统一管理功能;
Log(远程日志):提供服务打日志到远程的功能;
Stat(调用统计):统计业务服务上报的各种调用信息,比如总流量、平均耗时、超时率等,以便对服务出现异常时进行告警;
Property(业务属性):统计业务自定义上报的属性信息,比如内存使用大小、队列大小、cache命中率等,以便对服务出现异常时进行告警;
Notify(异常信息):统计业务上报的各种异常信息,比如服务状态变更信息、访问db失败信息等,以便对服务出现异常时进行告警;
原则上要求全部的节点之间网络互通,至少每台机器的node能够与公共框架节点之间都是可以连通的。
1.2.2 服务交互流程图
框架服务在整个系统中运行时,服务之间的交互分:业务服务之间的交互、业务服务与框架基础服务之间的交互。
服务发布流程:在Web系统上传server的发布包到patch,上传成功后,在web上提交发布server请求,由registry服务传达到node,然后node拉取server的发布包到本地,拉起server服务。
管理命令流程:Web系统上的可以提交管理server服务命令请求,由registry服务传达到node服务,然后由node向server发送管理命令。
心跳上报流程:server服务运行后,会定期上报心跳到node,node然后把服务心跳信息上报到registry服务,由registry进行统一管理。
信息上报流程:server服务运行后,会定期上报统计信息到stat,打印远程日志到log,定期上报属性信息到property、上报异常信息到notify、从config拉取服务配置信息。
Client访问Server流程:client可以通过server的对象名Obj间接访问server(即通过名字服务来路由,而不是写死IP),Client会从registry上拉取server的路由信息(如ip、port信息),然后根据具体的业务特性(同步或者异步,tcp或者udp方式)访问server(当然client也可以通过ip/port直接访问server)。
1.2.3 web管理系统
web管理系统主要包含以下功能:
- 业务管理:包括已部署的服务,以及服务管理、发布管理、服务配置、服务监控、特性监控等;
- 运维管理:包括服务部署、扩容、模版管理等;
1.2.4 服务结构图
框架核心的服务端与客户端实现结构图如下:
服务端:
- NetThread:网络线程,负责收发包,连接管理,多线程(可配置),采用epoll ET触发实现,支持tcp/udp;
- BindAdapter: 绑定端口类,用于管理servent(业务线程)对应的绑定端口的信息操作;
- ServantHandle:业务线程类,根据对象名分派Servant的对象和接口调用;
- AdminServant: 管理端口的对象;
- ServantImp: 继承Servant的业务处理基类(Servent:服务端接口对象的基类);
客户端:
- NetThread:网络线程, 收发包,连接管理,多线程(可配置),采用epoll ET触发实现,支持tcp/udp;
- AdapterProxy: 具体服务器某个节点的本地代理,管理到服务器的连接,以及请求超时处理;
- ObjectProxy: 远程对象代理,负责路由分发、负载均衡、容错,支持轮询/hash/权重;
- ServantProxy: 远程对象调用的本地代理,支持同步/异步/单向,Tars协议和非Tars协议;
- AsyncThread: 异步请求的回应包处理线程;
- Callback: 具体业务Callback的处理基类对象;
1.3 平台特性
1.3.1 tars协议
tars协议采用接口描述语言(Interface description language,缩写IDL)来实现,它是一种二进制、可扩展、代码自动生成、支持多平台的协议,使得在不同平台上运行的对象和用不同语言编写的程序可以用PRC远程调用的方式相互通信交流, 主要应用在后台服务之间的网络传输协议,以及对象的序列化和反序列化等方面。
协议支持的类型分两种,基本类型和复杂类型。
基本类型包括:void、bool、byte、short、int、long、float、double、string、unsigned byte、unsigned short、unsigned int;
复杂类型包括:enum、const、struct、vector、map,以及struct、vector、map的嵌套。
例如:
struct TestInfo {
require bool b=true;
require byte by=0;
require short si=12;
require int i=1234;
require long l=1234567;
require float f=45.34f;
require double d=0;
require string s="abc":
optional ETest t;
optional map<int, string> mi;
optional vector<string> vs;
optional vector<map<string, string>> vm;
optional map<vector<string>, vector<string>> m;
}
1.3.2 调用方式
通过IDL语言协议,可以定义服务提供的接口,并自动生成客户端和服务端的相关通信代码,服务端只需实现业务逻辑即可对外提供服务,客户端通过自动生成的代码即可调用服务,调用方式支持三种模式:
- 同步调用:客户端发出调用请求后等待服务返回结果后再继续逻辑;
- 异步调用:客户端发出调用请求后继续其他业务逻辑,服务端返回结果又由回调处理类处理结果;
- 单向调用:客户端发出调用请求后就结束调用,服务端不返回调用结果;
1.3.3 负载均衡
框架通过名字服务来实现服务的注册与发现,Client通过访问名字服务获取到被调服务的地址信息列表,Client再根据需要选择合适的负载均衡方式来调用服务,负载均衡支持轮询、hash、权重等多种方式。
1.3.4 容错保护
容错保护通过两种方式实现:名字服务排除和Client主动屏蔽。
名字服务排除的策略:
业务服务主动上报心跳给名字服务,使名字服务知道服务部署的节点存活情况,当服务的某节点故障时,名字服务不在返回故障节点的地址给Client,达到排除故障节点的目标。名字服务排除故障需要通过服务心跳和Client地址列表拉取两个过程,故障排除时间在1分钟左右
Client主动屏蔽:
为了更及时地屏蔽故障节点,Client根据调用被调服务的异常情况来判断是否有故障来更快进行故障屏蔽。具体策略是,当client调用某个svr出现调用连续超时,或者调用的超时比率超过一定百分比,client会对此svr进行屏蔽,让流量分发到正常的节点上去。对屏蔽的svr节点,每隔一定时间进行重连,如果正常,则进行正常的流量分发。
1.3.5 过载保护
为了防止业务因为访问量突增或服务器故障造成系统整体的繁忙,进而导致全部服务的不可用,框架内部做相应设计来应对。实现请求队列,服务调用通过非阻塞方式实现异步系统,从而达到提升系统处理能力的目的。并且对队列的长度进行监控,当超过某个阀值,则拒绝新的请求。对请求设置超时时间,当请求表从队列里读取出来是判断请求是否超时,如果超时则不做处理。
1.3.6 消息染色
框架提供了对某服务某接口的特定请求进行染色的能力,染色的消息可以透传到后面需要访问的所有服务上,对染色的请求,服务自动把日志上报到特定的染色日志服务器上,使用者只需在染色服务器上即可分析请求访问的路径,方便跟踪定位问题。
如下:
1.3.7 IDC分组
为了加快服务间的访问速度,建设跨地区、跨机房调用带来的网络资源消耗,减少网络故障带来的影响,框架提供了跨地区、跨机房,就近接入的功能。
1.3.8 SET分组
为了方便对业务服务部署管理进行标准化和容量化,框架提供了Set部署能力,set之间没有调用关系,互不干扰,故障隔离,提高运维效率和服务可用性。
1.3.9 数据监控
为了更好反映和监控小到服务进程、大到业务的运行质量情况,框架支持以下数据上报的功能:
1.提供了服务模块间调用信息统计上报的功能,方便用户查看服务的流量、延时、超时、异常等情况;
2.提供了用户自定义属性数据上报的功能,方便用户查看服务的某些纬度或者指标,比如内存使用情况、队列大小、cache命中率等;
3. 提供了服务状态变更和异常信息上报的功能,方便用户查看服务的何时发布过、重启过、宕过以及遇到的异常致命错误等;
1.4.0 集中配置
对业务配置进行集中管理并且操作web化,使配置修改更容易,通知更及时,配置变更也更安全;对配置变更进行历史记录,让配置可以轻松回退到前一版本。配置拉取服务化,服务只需调用配置服务的接口即可获取到配置文件。
为了能灵活管理配置文件,配置文件分为几个级别:应用配置、Set配置、服务配置和节点配置。
应用配置为最高一级的配置文件,它是多个服务配置提炼出来的公共配置,服务配置通过引用它来使用其配置内容。
Set配置是具体一个Set分组下所有服务的公共配置,在应用配置的基础上进行补充追加。
服务配置是具体一个服务下所有节点的公共配置,可以引用应用配置。
节点配置是一个应用节点的个性化配置,它和服务配置合并成为具体一个服务节点的配置。
2. 微服务框架Tars源码学习
2.1 内存分配器
原始内存块 TC_MemChunk
TC_MemChunk(简称mc)
- 内部有两个指针,一个指向header,一个指向所管理的内存空间,管理的内存空间的具体信息都在header里,而且header占的空间也在自己的管辖范围内提前分出来(后面也是类似结构)。所管理的内存空间会按照指定数值大小分块管理,称为block。
头部结构tagChunkHead:
- 记录管理区域内的信息:每个block大小、block数量、第一个空闲block的index、总计的空闲block数量。
有两个初始化函数create和connect
- connect只提供管理的内存空间地址就可以
- create要额外指定block的大小和数量,然后会memset清理,以**“链表”**连起所有block。这里的额外两个入参在上层会计算好。
这里的链表应该在后面会用。
// tc_mem_chunk.h
class TC_MemChunk
{
public:
/**
* @brief 初始化, 要保证p指向的内存指针=getMemSize大小
* @brief Initialize to ensure that the memory pointer P points to = getMemSize size
* @param pAddr 地址, 换到应用程序的绝对地址
* @param pAddr Address, to the absolute address of the application
* @param iBlockSize block大小
* @param iBlockSize size of the block
* @param iBlockCount block个数
* @param iBlockCount count of the block
*/
void create(void *pAddr, size_t iBlockSize, size_t iBlockCount);
/**
* @brief 连接上
* @brief Connect
* @param pAddr 地址, 换到应用程序的绝对地址
* @param pAddr Address, to the absolute address of the application
*/
void connect(void *pAddr);
struct tagChunkHead
{
/**block size*/
size_t _iBlockSize; /**区块大小*/
/**Number of blocks*/
size_t _iBlockCount; /**block个数*/
/**First available block index*/
size_t _firstAvailableBlock; /**第一个可用的block索引*/
/**Number of blocks available*/
size_t _blockAvailable; /**可用block个数*/
};
private:
/**
* @brief 区块头指针
* @brief Block Header Pointer
*/
tagChunkHead *_pHead;
/**
* @brief 数据区指针
* @brief Data area pointer
*/
unsigned char *_pData;
}
// tc_mem_chunk.cpp
void TC_MemChunk::create(void *pAddr, size_t iBlockSize, size_t iBlockCount)
{
assert(iBlockSize > sizeof(size_t));
init(pAddr);
_pHead->_iBlockSize = iBlockSize;
_pHead->_iBlockCount = iBlockCount;
_pHead->_firstAvailableBlock = 0;
_pHead->_blockAvailable = iBlockCount;
memset(_pData, 0x00, iBlockCount*iBlockSize);
unsigned char *pt = _pData;
for(size_t i = 0; i != iBlockCount; pt+= iBlockSize)
{
++i;
memcpy(pt, &i, sizeof(size_t)); //每块第一个字节直接指向下一个可用的block编号, 变成一个链表
}
}
void TC_MemChunk::connect(void *pAddr)
{
init(pAddr);
}
void TC_MemChunk::init(void *pAddr)
{
_pHead = static_cast<tagChunkHead*>(pAddr);
_pData = (unsigned char*)((char*)_pHead + sizeof(tagChunkHead));
}
2.2 内存块分配器 TC_MemChunkAllocator
TC_MemChunkAllocator(简称mca):
- 负责管理mc的分配,在头文件里就能看出mc是mca的一个成员变量,所以一个分配器负责的应该就是那一块管辖范围的分配策略,两者是一对一的并列的关系。
- 除了mc外它里面还有两个指针,一个是header,一个是管理的内存空间。header占用的的空间同样还是在init里提前划分出来。
header:
- 记录管辖范围划分的block的大小和总空间的大小,这两个都是入参指定的。
同样两个初始化函数
- create里面减去了所使用的空间,然后计算出了block的数量,传给mc继续create
// tc_mem_chunk.h
class TC_MemChunkAllocator
{
public:
/**
* @brief 初始化
* @brief Initialization
* @param pAddr, 地址, 换到应用程序的绝对地址
* @param pAddr, Address, to the absolute address of the application
* @param iSize, 内存大小
* @param iSize, Memory Size
* @param iBlockSize, block的大小
* @param iBlockSize, Block size
*/
void create(void *pAddr, size_t iSize, size_t iBlockSize);
/**
* @brief 连接
* @brief Connect
* @param pAddr 地址, 换到应用程序的绝对地址
* @param pAddr Address, to the absolute address of the application
*/
void connect(void *pAddr);
struct tagChunkAllocatorHead
{
size_t _iSize;
size_t _iBlockSize;
};
private:
/**
* 头指针
* Head Pointer
*/
tagChunkAllocatorHead *_pHead;
/**
* chunk开始的指针
* Pointer to chunk start
*/
void *_pChunk;
/**
* chunk链表
* Chunk Linked List
*/
TC_MemChunk _chunk;
};
// tc_mem_chunk.cpp
void TC_MemChunkAllocator::init(void *pAddr)
{
_pHead = static_cast<tagChunkAllocatorHead*>(pAddr);
_pChunk = (char*)_pHead + sizeof(tagChunkAllocatorHead);
}
void TC_MemChunkAllocator::initChunk()
{
assert(_pHead->_iSize > sizeof(tagChunkAllocatorHead));
size_t iChunkCapacity = _pHead->_iSize - sizeof(tagChunkAllocatorHead);
assert(iChunkCapacity > TC_MemChunk::getHeadSize());
size_t iBlockCount = (iChunkCapacity - TC_MemChunk::getHeadSize()) / _pHead->_iBlockSize;
assert(iBlockCount > 0);
_chunk.create((void*)((char *)_pChunk), _pHead->_iBlockSize, iBlockCount);
}
void TC_MemChunkAllocator::create(void *pAddr, size_t iSize, size_t iBlockSize)
{
init(pAddr);
_pHead->_iSize = iSize;
_pHead->_iBlockSize = iBlockSize;
initChunk();
}
void TC_MemChunkAllocator::connectChunk()
{
assert(_pHead->_iSize > sizeof(tagChunkAllocatorHead));
size_t iChunkCapacity = _pHead->_iSize - sizeof(tagChunkAllocatorHead);
assert(iChunkCapacity > TC_MemChunk::getHeadSize());
_chunk.connect((void*)((char *)_pChunk));
}
void TC_MemChunkAllocator::connect(void *pAddr)
{
init(pAddr);
connectChunk();
}
2.3 多内存块分配器 TC_MemMultiChunkAllocator
TC_MemMultiChunkAllocator(简称mmca):
- mmca为集成了多个mca的结构,它里面有一个数组来记录所有它管辖的mca的地址,每一个mca的block的大小不同,从而实现了多种内存块的分配。
- mmca内部除了与之前相同的两个指针和一个mca数组之外,还有一个数组_vBlockSize来记录每一个mca的block的大小,还有一个记录mca中block的数量的size_t,没错是一个size_t,所以多个内存分配器管理的block的数量是相同的。还有一个记录索引的数量的size_t(初始化没有用到应该在后续会使用)。最后还有一个mmca的指针,应该是来表示mmca还可以自成链表来进行动态扩展。
header中记录的当前管理内存的总大小、后续分配块和在一起的大小(暂时没看到使用方式),生成block的最大最小值和成长因子,还有一个指向下一个分配器的指针。
初始化也是两个函数
- create里面会先清理所有空间,然后从最小到最大用成长因子依次计算出来每一个mca的block的大小,然后用总空间减去所有的mmca(一个),mca和mc(n个) 的空间,然后平均分配给每个mca,最后遍历所有mca并计算出每个mca应使用的总大小(包括mca和mc的header)传入继续create。
- connect也是差不多的流程,只是最后如果mmca有next的话会递归进去初始化。
//tc_mem_chunk.h
class TC_MemMultiChunkAllocator
{
public:
/**
* @brief 初始化
* @brief Initialization
* @param pAddr 地址, 换到应用程序的绝对地址
* @param pAddr Address, to the absolute address of the application
* @param iSize 内存大小
* @param iSize Memory size
* @param iMinBlockSize block的大小下限
* @param iMinBlockSize Lower size limit for blocks
* @param iMaxBlockSize block的大小上限
* @param iMaxBlockSize Maximum Block Size
* @param fFactor 因子
* @param fFactor Facotor
*/
void create(void *pAddr, size_t iSize, size_t iMinBlockSize, size_t iMaxBlockSize, float fFactor = 1.1);
/**
* @brief 连接上
* @brief Connect
* @param pAddr 地址, 换到应用程序的绝对地址
* @param pAddr Address, to the absolute address of the application
*/
void connect(void *pAddr);
struct tagChunkAllocatorHead
{
/**Current block size*/
size_t _iSize; /**当前块大小*/
/**Size of subsequent allocation blocks combined*/
size_t _iTotalSize; /**后续分配块合在一起的大小*/
size_t _iMinBlockSize;
size_t _iMaxBlockSize;
float _fFactor;
/**Next allocator address, if not 0*/
size_t _iNext; /**下一个分配器地址, 如果没有则为0*/
};
private:
/**
* 头指针
* Head Pointer
*/
tagChunkAllocatorHead *_pHead;
/**
* chunk开始的指针
* Pointer to chunk start
*/
void *_pChunk;
/**
* 区块大小
* block size
*/
vector<size_t> _vBlockSize;
/**
* 每个chunk中block的个数
* Number of blocks per chunk
*/
size_t _iBlockCount;
/**
* chunk链表
* Chunk Linked List
*/
vector<TC_MemChunkAllocator*> _allocator;
/**
* 所有的索引个数
* Number of all indexes
*/
size_t _iAllIndex;
/**
* 后续的多块分配器
* Subsequent multiblock allocators
*/
TC_MemMultiChunkAllocator *_nallocator;
};
// tc_mem_chunk.cpp
void TC_MemMultiChunkAllocator::init(void *pAddr)
{
_pHead = static_cast<tagChunkAllocatorHead*>(pAddr);
_pChunk = (char*)_pHead + sizeof(tagChunkAllocatorHead);
}
void TC_MemMultiChunkAllocator::calc()
{
_vBlockSize.clear();
//每种block大小总和
size_t sum = 0;
for(size_t n = _pHead->_iMinBlockSize; n < _pHead->_iMaxBlockSize; )
{
sum += n;
_vBlockSize.push_back(n);
if(_pHead->_iMaxBlockSize > _pHead->_iMinBlockSize)
{
n = max((size_t)(n*_pHead->_fFactor), n+1); //至少内存块大小要+1
}
}
sum += _pHead->_iMaxBlockSize;
_vBlockSize.push_back(_pHead->_iMaxBlockSize);
assert(_pHead->_iSize > (TC_MemMultiChunkAllocator::getHeadSize() + TC_MemChunkAllocator::getHeadSize()*_vBlockSize.size() + TC_MemChunk::getHeadSize()*_vBlockSize.size()));
//计算块的个数
_iBlockCount = (_pHead->_iSize
- TC_MemMultiChunkAllocator::getHeadSize()
- TC_MemChunkAllocator::getHeadSize() * _vBlockSize.size()
- TC_MemChunk::getHeadSize() * _vBlockSize.size())/sum;
assert(_iBlockCount >= 1);
}
void TC_MemMultiChunkAllocator::create(void *pAddr, size_t iSize, size_t iMinBlockSize, size_t iMaxBlockSize, float fFactor)
{
assert(iMaxBlockSize >= iMinBlockSize);
assert(fFactor >= 1.0);
init(pAddr);
_pHead->_iSize = iSize;
_pHead->_iTotalSize = iSize;
_pHead->_iMinBlockSize = iMinBlockSize;
_pHead->_iMaxBlockSize = iMaxBlockSize;
_pHead->_fFactor = fFactor;
_pHead->_iNext = 0;
calc();
//初始化每种快的分配器
char *pChunkBegin = (char*)_pChunk;
for(size_t i = 0; i < _vBlockSize.size(); i++)
{
TC_MemChunkAllocator *p = new TC_MemChunkAllocator;
size_t iAllocSize = TC_MemChunkAllocator::getHeadSize() + TC_MemChunk::calcMemSize(_vBlockSize[i], _iBlockCount);
p->create(pChunkBegin, iAllocSize, _vBlockSize[i]);
pChunkBegin += iAllocSize;
_allocator.push_back(p);
}
_iAllIndex = _allocator.size() * getBlockCount();
}
void TC_MemMultiChunkAllocator::connect(void *pAddr)
{
clear();
init(pAddr);
calc();
//初始化每种块的分配器
char *pChunkBegin = (char*)_pChunk;
for(size_t i = 0; i < _vBlockSize.size(); i++)
{
TC_MemChunkAllocator *p = new TC_MemChunkAllocator;
p->connect(pChunkBegin);
pChunkBegin += TC_MemChunkAllocator::getHeadSize() + TC_MemChunk::calcMemSize(_vBlockSize[i], _iBlockCount);
_allocator.push_back(p);
}
_iAllIndex = _allocator.size() * getBlockCount();
//没有后续的空间了
if(_pHead->_iNext == 0)
{
return;
}
assert(_pHead->_iNext == _pHead->_iSize);
assert(_nallocator == NULL);
//下一块地址, 注意这里是嵌套的, 扩展分配空间的时候注意
tagChunkAllocatorHead *pNextHead = (tagChunkAllocatorHead *)((char*)_pHead + _pHead->_iNext);
_nallocator = new TC_MemMultiChunkAllocator();
_nallocator->connect(pNextHead);
}
2.4 TODO
- _vBlockSize记录的block的大小其实在mca的header中都有,感觉可以去掉。同理mmca的next指针也是重复存在的。
- mc的block的链表和mmca的all index和total size,还没有看到使用这块,一直没有用到。
- 内存块大小之间不能转化,有些不够精致,之后看下使用环境再思考下。
- 感觉mmca里面的各种参数也可以都缩到header里,可以思考下。
- mmca的connect的递归可能会有坑。
猜你喜欢
- 2024-10-14 3、Tars C++系列之调试Tars微服务应用
- 2024-10-14 2、Tars C++系列之快速入门(c++ tr())
- 2024-10-14 我为什么腾讯Tars微服务框架?(腾讯tars框架官网)
- 2024-10-14 谷歌新机器人现身,人形两足酷似《星际穿越》的TARS
- 2024-10-14 「腾讯开源Tars」c++ RPC 框架 入门:介绍
- 2024-10-14 机械 竞技 热血 未来!吉林大学TARS-GO战队在机甲大师赛中纵情驰骋
- 2024-07-08 「腾讯开源Tars」c++ RPC 框架:服务发布
- 2024-07-08 7、Tars C++系列之依赖CMake(二)(cmake 依赖关系)
- 2024-07-08 7、Tars C++系列之依赖CMake(一)(commons-codec依赖)
- 2024-07-08 7、Tars C++系列之依赖CMake(三)(cmake 依赖另一个共享库)
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)