天天看点

腾讯云 Redis 集群版配置管理揭秘 ( 上 )

腾讯云 Redis(CRS)集群版已经有数千用户,售出数十T容量,那么 CRS 是如何做配置管理的呢?通用的集群系统都需要做配置管理分发,成员健康度检查,希望能带给您启发。

CRS 集群版改造自 QQ 后台存储数据库 Grocery,拥有十多年的技术积累与传承,由 SNG 即通平台部公共组件组多年研发,数据运营部 DBC 组持续运维运营。目前部署有上万台的集群,每秒承受上亿的访问。CRS 集群主要是由管理机、接入机、存储机三种角色组成。配置中心会部署在管理机上,配置客户端则会部署在集群的每台机器上。

腾讯云 Redis 集群版配置管理揭秘 ( 上 )

配置管理模块,由上图的“配置中心” 与 “配置客户端”组成,是一套C++实现的CS架构。配置中心是一个单进程多线程架构,每个线程负责单独的功能。配置中心进程启动后,首先进行初始化,然后启动各个工作线程。初始化的分析因为不是重点,所以放在文末。现在介绍各个主要的工作模块:

配置加载:将配置信息从DB加载到内存中 (DB-> 服务端配置)

存活更新:将VSERVER的存活状态做改变 (客户端状态 -> 服务端配置)

筛选机器:筛选出需要接收配置的机器 (客户端状态 compare 服务端配置)

推送配置:将配置推送到指定机器 (服务端配置 -> 客户端)

接收心跳:接收心跳并更新客户端状态 (客户端 -> 服务端配置)

腾讯云 Redis 集群版配置管理揭秘 ( 上 )

主要的角色:配置客户端 <-> 配置中心 (客户端状态,服务端配置) <- DB

该模块由一个独立的线程LoadConfigThread实施,会把数据库中最新的配置信息加载到共享内存,加载前会有一些合法性校验。

我们一般是如何来更新集群的配置信息呢?

a. 运营系统提交的 DML语句 更改DB中的配置信息;

b. 执行 "<code>update config_state_t set cfg_seq=cfg_seq+1;</code>" 更新序列号;

该模块的逻辑包裹在while循环中,循环间隔 sleep(1),这也就是说,当我们在db中更新配置后,大概1秒会加载到共享内存中;

线程会在DB中执行 "<code>Select need_load,cfg_seq From T_SYSCONF</code>",主要是为了取得<code>cfg_seq</code>这个序列号。(历史上会用<code>need_load</code>字段的信息,但现在已弃用,不作为需要加载的标志);

线程每读一次MySQL,都会执行这么一条语句,以更新最后一次读MySQL的时间。<code>update T_SYSCONF set master_cc_read_db_last_time = unix_timestamp() ;</code>这是用于主备配置中心的死机切换判断的。更新失败上报 "646280 主CC设置最后读db时间:失败",更新成功上报 " 和 "646281 主CC设置最后读db时间:成功"。我们通过<code>select from_unixtime(master_cc_read_db_last_time) from T_SYSCONF;</code>就能看到,每秒<code>master_cc_read_db_last_time</code>值都会被更新一次。

如果 <code>cfg_seq</code>已经与进程的全局变量 <code>g_ddwDbSeq</code>不同,则意味着需要把DB的最新配置信息加载到共享内存中了,因为运维人员对Mysql中的配置信息可能有误操作,所以在加载到共享内存前,程序有严格的合法性校验,以免取得错误的配置信息,破坏集群的安全。

那么合法性校验具体是怎么做的呢?

a. 把所有配置加载到待更新的临时配置中

b. 把数组两个元素指向的配置,进行一个比对,这里检验条件就非常多了,如新加的<code>server_id</code>与旧的<code>server_id</code>要行程等差数列,<code>server_name</code>相同的条目<code>copy_id</code>必须不同等等,这里就不一一列举。

配置合法性检查不通过,上报"299726 配置中心装载新配置时,因检查不合格而拒绝装载"。这是一个比较重要的上报,因为同时变量<code>g_bDbConfigIsValid = false</code>,后面介绍的[推送配置]模块,如果发现该变量为false,则会终止推送,那么诸如主备切换等新集群信息,也无法下发了,所以需要尽快处理。而我们的运营系统是通过看日志来检测配置是否加载成功的。成功的话,sleep(10)后会把 成功日志写到文件中。

如果失败,不用sleep任何秒数,直接就会写失败日志:<code>LOG_WATER_MUTEX(&amp;g_stServerLog,_LOG_ERROR_, "LoadConfigFromDB Fail: iRet=%d", iRet);</code>

所以我们校验加载配置是否失败,只需要在数据库update seq后,过1秒后,检验日志文件,是否同时出现了ERR以及LoadConfigFromDB 字眼,如果是,那么就是加载配置失败了。

假如配置的md5的确是发生了改变,那么线程会生成一个配置包待下发,这是因为有时seq发生了改变不一定意味着配置有改变,所以还需看MD5。生成配置包失败会上报"164304 load数据生成配置包失败",成功会上报"182928 server启动,生成配置"。

然后进行临时内存结构体与运行内存结构体的切换,以达到启用新配置的目的,

最后我们还会更新由新配置生成的md5信息。

上述我们看到了ServerConfigsShm结构体,这里我也把相关的需要参考的结构体一并列出。

腾讯云 Redis 集群版配置管理揭秘 ( 上 )

// 管理所有配置信息的数据结构

备注:集群的一台物理存储机,逻辑上会划分为1个或多个VSERVER,每个VSERVER对于集群就是一个独立的存储机,独立提供服务,这有点虚拟化的意思。

该模块由线程UpdateVServerStatusThread单独实施,会跟进客户端状态列表中机器的心跳情况,来更新客户端状态,可能是从<code>WORKDING-&gt;OFF_WORKING</code>,或者从<code>OFF_WORKING-&gt;WORKDING</code>。改变状态后,还会产生推送配置包并放置到推送队列中。

逻辑包裹在while循环中,循环间睡眠间隔0.01秒,usleep(10000)

遍历每一个VSERVER,获取其最后一次上报与当前的时间差距(秒),

如果 tInterval 已经大于某个伐值(如3分钟):

如果当前该VSERVER我们记载是WORKING状态,那么我们就就要将其转为OFF_WORKING了。

如果是OFF_WORKING,则什么都不用做。

如果 tInterval 小于等于某个伐值(如3分钟),这说明了已经有心跳上报了:

如果当前是OFF_WORKING,则转为WORKING。

如果当前是WORKING,则什么都不用做。

按照如上逻辑:死了的机器是过一段时间(可配)才会被置为DEAD,后面的请求才不会转发到这,但机器如果复活了,可能不到1秒就能判断其活了。这就是状态转换的时间差别。

从上述代码我们能看出来,需要进行状态转换的VSERVER,都会把serverid被加到aiChangeServer数组中,所有serverid都加到aiChangeServer数组以后,我们就对该数组进行遍历,并且把对应VSERVER的状态进行变换。

如果配置加载模块,该线程最后会更新服务器配置的MD5值,以及为新的配置生成配置包,并推送到队列中。

线程 PushScheduleThread 负责筛选出需要发送配置的机器,从这里我们知道,配置并不是需要推送到所以机器上的,而之后客户端状态的MD5与服务器状态MD5有差异时,我们才会推送配置给该客户端。这个模块主要就是用来筛选出需要接受配置包的机器。

业务逻辑都在while循环中,循环至少间隔2秒 : sleep(2)

获取当前消息队列 g_MsgHandle 中,未发送的消息数量(机器数量)有多少。这里失败上报"164324 推送调度,获取消息队列数据失败",成功上报"164325 推送调度,消息队列数据还没有处理完"。

如果消息数量不为0,也就是上一轮发送还未完,那么放弃本轮循环的操作,continue进入下一次循环。

如果消息数量为0,也就是之前的消息(机器)都处理完了,那么我们这次就来看看,是否有需要接收配置信息的客户端。

把这个结构体推送至消息队列 g_MsgHandle,START表明这里开始新的一轮,宏定义如下。失败会上报"推送调度cache,将cache放入消息队列失败"。

接下来还会执行

具体的逻辑是,遍历 interface 机器的客户端状态列表 pstInterfaceServerClientUpdateInfoList ,

如果发现其最后上报心跳时间超时了,说明可能死机了,那么将其上报md5置0,这样MD5不一致会导致配置一直往其发送,这样后续机器复活后也能获得新配置。

如果发现最后上报心跳时间没有超时,比较interface上报的MD5与服务器保存的MD5;

如果一致的话,说明无需推送interface配置已经是最新;

如果不一致,那么上报"184114 推送interface配置,client优先级较低",并将如下结构体压入消息队列,并且将该interface配置的最后推送时间置为当前时间;过程出现问题上报"164329 推送调度interface,将interface放入消息队列失败"

具体的逻辑是,遍历所有 cache 机器,获取每个cache机器上的第一个serverid(一个机器可能有若干个serverid),根据这个serverid,获取该VSERVER的服务器配置与客户端状态信息。如果该机器当前是OFF_WORKING状态,那么很可能该机器就死机了,就将其MD5值置空,以防止机器重启后一直获取不到配置的问题。这里与上述遍历interface机器的机器的逻辑有点相似,但上述是要计算是否已经超时,而cache机器本身就有WORKING与OFF_WORKING状态,就不用再计算。

然后就对比服务器与客户端的MD5值,

如果一致的话,说明无需向该CACHE推送配置,并且continue到下一个循环。

如果不一致,则说明该CACHE需要获取最新的配置了,我们把这个结构体压进消息队列:

压入消息队列成功后,还需要更新该ip下所有 VSERVER 的 最后推送配置时间 dwLastPushConfigTime 更改成当前时间。

如果上面的确有需要推送的机器被压入队列,那么就把END与md5值压到队列中,否则把END与空的md5值压入到队列中。所以你猜到了,后面的[推送模块],看到END这个cRole后,还得判断MD5值是否全0,来判断是否有要推送的机器。

这个模块是将最新的配置信息,推送到 [筛选机器]模块中指定的机器上。这个模块有个特点,为了加速配置的推送,会创建很多线程,数量根据配置文件中的配置项 PushConfigThreadNum 来定(一般是200)

<code>PushConfigThread()</code>中具体的逻辑是什么呢?

线程会不断地循环,每次循环会固定睡眠0.01秒:usleep(10000); 然后会获取消息队列中的一个数据结构(消息结构如上),不能读出(消息队列为空),则sleep(1),读出的话,解释出其IP地址,再解释其cRole字段。

如果cRole = START,这说明还没有需要推送的机器,continue到下一个循环。

如果cRole = END,那么说明本轮配置已经全部下发。这里会比较这个消息结构的MD5与服务器配置的MD5是否一致,

如果不一致(也就是全0的情况):那么说明本轮其实并没有需要下发配置的机器,continue到下个循环。

如果一致,说明本轮确认是进行过机器的配置下发。那么检查一下配置推送的结果,主要看:

a. 成功推送配置的数量 ;

b.应该推送的数量,

c.没有推送错误的数量,如果a小于b(很可能别的线程在发送并且未发送完),并且没有c(错误量)的发生,那么我们稍微等一段时间(可配)。最后将这三者的结果打印日志,以及入库与上报Monitor等等。我们查问题是:<code>select cfg_seq,finish_time,success from T_DISTRIBUTE_STATUS</code>来看某次具体的下发是否成功,success是0代表成功,1代表失败。

如果非START也非END,那么向刚才解释出来的IP,客户端端口,推送配置。这才是最重要的。推送失败上报"626449 推送配置到指定IP失败",并且把对应error结果加1.如果成功,把对应success结果加1.推送成功,会记录推送的耗时时间并上报。

"182940 server下发配置时间在0-1" //单位毫秒,书醒从182940至182956都是记录推送时间的。

接《 腾讯云 Redis 集群版配置管理揭秘 ( 下 )》