
张远 MySQL/MyRocks
2018年11月份来德国参加Percona Live 会议,可能这辈子都难忘吧!觉得有必要记录下,文笔粗鄙,凑合着看吧。 01 法兰克福 2018-11-4 到达德国法兰克福机场,从机场到住处希尔顿酒店13公里,司机是位德国大爷,10分钟就到达了目的地,中间有段不限速高速,大爷比较保守才开到140左右,而两边不断有车超车呼啸而过,要知道当时是德国时间早上6左右天还没有亮,路况并不好,但是打的价格比较感人34.7欧元。 期间在法兰克福市区小逛了下,法兰克福虽然号称是欧洲的金融中心,欧元塔就位于这里,也是德国的主要城市之一,但实际大小也就国内三四线城市水平。 某天早晨约练跑马拉松的小伙伴们来美茵河跑步(我是专业坑队友的),沿河一圈10公里。来回两头都已经跑到郊区了,也就是说沿河的市区不超过5公里,标准的国内小城市。 Percona Live 选择在这里开会应该也是考虑到法兰克福作为欧洲的交通枢纽,其实Percona在欧洲的分部在爱尔兰,据说法兰克福比柏林的航班还要多。 其它关于法兰克福 地铁交通还算便利 中央火车站周围比较乱,类似红灯区的地方,三五人会聚在一起吸毒 街头还碰到了小规模的游行示威,有一大队警察护航 中餐馆偏川菜普遍不错(申根签证类别中针对厨师有专门的类别) 共享单车也没有国内多 出租车大多是奔驰,极少是大众 家用车都是两厢的德国车,日美车系可以忽略 租车基本是手动档,hertz要求驾照满一年,当时同行伙伴就因为带的香港驾照差几天满一年,且我只带了翻译件而未带原价,而错过了开奔驰E级体验德国无限速高速的机会,租车算下来比打车和坐火车划算* 02 护照丢失 切到正题,护照是如何丢的2018-11-7 周三,开完会议,利用多余的一点时间打算去浪一浪大伙儿合计了下就去了附近的斯图加特,参观了保时捷和奔驰的博物馆,正好赶上了保时捷70周年庆 愉快的参观完后,去附近的中餐馆美食一顿。开始返程酒店,在去火车站的途中,还感叹了斯图加特王宫广场的佳景 不料奇葩的经历从此开始 买好车票,坐在火车站站台座椅上等车的时候,随手放在身旁的双肩包被顺走了 直到火车到站我上到火车上准备放包时才发现那里不对,于是赶紧下车寻找,此时包已经不见了。 那一瞬间立刻意识到大麻烦来了,我的护照也在包里一起丢失了。本来是担心揣兜里不安全,专门背个包就是为装护照, 结果护照还是丢了,我了个去..... 火车要开了,告别伙伴们,让他们先回去,我独自留在火车寻找。 当时还是觉得能找回来的,背包基本是个空包,除了护照和一些文件没有什么价值。于是在火车站来回寻找,连垃圾捅丢翻遍了。 随后找了火车站的工作人园寻找帮助,他们告知应去DB information问问看,DB information应该相当于火车站的问讯处,找到DB information后,DB information告知没有收到有丢失物品的信息,并告知我去lost service(失物招领处)看看,我留下了我的联系方式给DB information,让他们有消息给我电话。失物招领处比较隐蔽,问了几个路人都不知道,最后折回找了工作人员带领才找到,结果当然是没找到,他们给了我联系方式,让我第二天再打电话问问看。 在我焦急寻找的同时,同伴们也发给了消息,如果找不到应该的处理方法 报警 联系大使馆 于是我打电话联系中国驻法兰克福领事馆,那里的值班人员说需要准备两点 先报警,得到报警凭证 准备3张1寸白底照片,用于办理旅行证 03 警察局01 火车站逛了一圈没看到一个警察,于是在地图上搜索警察局,警察局离火车站比较近,可以步行过去 很快找到地铁上显示的地方,可是怎么也找不到。这个地方是一个多层建筑,一楼都是一些商铺。地图标示附近位置都找了下也没找到,于是找了好几个路人问都说不知道。最后问了商铺的店主才知道警察局是在二楼,好吧,国内的警察局至少得是个独栋吧! 警察局大门就是一个普通的住宅门口,简直不能称之为大门。需要先在门口按门铃,并在门口和里面的人员对话说明来意,里面才会帮忙开门,进去后是一个不能称之为大厅的大厅,大厅简陋得只放得下几张椅子,会有警察从里面一道门出来,详细了解情况后,让我坐在椅子上等候。 工作人员进去后,会和其他警察讨论如何处理我这种情况,过了一会,就有另一个工作人员带我进去开始录口供了,要了我的护照和签证信息,并填写了丢失的经历,填写完后并给了我一张报警凭证。 同时,我也要求是否可以查看火车站台附近的监控信息,他找其他工作人员确认了下,那里并没有监控。在德国很少有监控,可能认为监控侵犯隐私。 从警察局出来,我又返回火车站,并在火车站徘徊了很久,并多次去问DB information和失物招领处是否有消息。当时已经晚上7点了,回程的火车也没几趟了,再在火车站等也没有多大意义了,于是决定买了回程的票先回去等消息。 德国火车比较随意,没有进站口,但上车后可能会检票,我们去程就没有检票,当时还感叹这太容易逃票了,坐火车其实挺贵的,火车也分快车和慢车,ICU快车巨贵,我们做的还是普快不到200公里竟然需要50欧。但我们回程检票了 本来是明天中午13:45的飞机,想来肯定是来不及来,于是让同伴先帮忙改签到周六的机票,同时将酒店也延住到周六。 回去已经很晚了,也没啥食欲,正好把带来的一碗票面吃了,就等着明早去大使馆了 04 大使馆01 2018-11-8 周四, 一大早先打电话给失物招领处,仍然没有消息。于是出发去打探了大使馆的具体位置,大使馆周一至周五才上班,工作时间只有上午9:00 - 11:30 办理护照服务不能从大门进,需从上图中的侧门进。到达大使馆时离上班还有一个小时,我先去附近的dm超市打印了1寸照片,dm超市还是比较方便的,除了一些生活用品还提供文印服务,价格也公道。 打印回来后,门口也陆续有几个人在等待了9点,大使馆准时开门。其实大使馆内部也有打印机,也提供照相服务 其他来的人基本是办理护照过期的问题,就我一个办理护照丢失的业务。我排到第二个3号窗口办理,工作人员比较亲和,我表明来意,说办完说不定可以赶上今天13:45的飞机,我心里一下子放松了很多。他让我先填了三张表格,递交照片等材料后,让我在2号窗口等待消息,2号窗口是位文静的大姐,她说需要等待国内核实我的身份后就可以办理旅行证了,递给我一张取证单,让我先坐着等候,有消息就通知我付款办证。 到目前为止感觉还挺顺利的,但随着时间的推移,国内还没有传来消息。一个多小时过去了,我开始不淡定了,于是去窗口问她,她说已经帮我查两次了,还没有消息,让我继续等待。 我又继续等了段时间,差不多快到11点。她说她们快下班了,国内还没有消息,让我明天再来看看。我还是抱有希望的,说反正还有半小时,再等等吧 按照她们的说法,一般国内40分钟内会给答复,我都超过2小时了。 如果明天周五没有消息,那么我将要等到下周一,周末他们是不上班的。 雪上加霜是,周六是我在德国有效滞留的最后一天,如果没有旅行证,我没有有效身份证明,会没有任何安全感。 此时随行的同伴已经坐上的回国的飞机,只剩我一人在德国。没有有效证件,我无法预订酒店,住处都没有,当时我就做好了睡大街的准备,下图是大使馆街头的流浪汉,睡在简单的睡袋中 开始做最坏的打算,如果明天还没有消息,那么就要准备和大使馆展开持久战了,先在大使馆赖个十天八天的,签证允许天数已超属于非法居留也只能这样了。 我向工作人员反馈了我签证过期的情况,他们建议我去警察局开个证明。 直到下班, 依然没有消息,折回酒店。 其他: 大使馆上班时间挺短的,就工作日上午半天,来办事的人也不多,期间还多次离开去后面休息或干啥的 那片区域也有越南大使馆等,但中国大使馆是最大的也最气派 05 警察局02 回酒店的路上正好有个警察局,接待的警察是一个帅帅的小哥,他的英语也不太好,我的更渣,沟通了很久才理解了我的来意。他咨询同事得到的结论是要找机场的警察才能解决我的签证过期的问题,并手写给了我一张机场警察的地址 抱着幻想,回到酒店继续给失物招领处打了电话,依然没有消息。 06 大使馆02 2018-11-9 周五 我8点就来到了大使馆,坐在门口的石头上等待9点开门,直到8:40左右门口才陆续来了几个人。这时街头的流浪汉也被街上逐渐变多的车辆炒醒了,从睡袋中探出来,伸了个懒腰,推着车悻悻的走了...... 9点准时开门,我排第一个,依然是昨天的工作人员,我和他们打了招呼,他们也认出了我,查了下结果,说国内有反馈了,可以缴费办证了。缴费只能用本地的EC卡付款,于是我找了办理业务的国内同胞帮忙转了,然后我再微信转他。拿到旅行证后,和几位使馆工作人员表达了真切的感激之情。 谢天谢地,我拿到了,不用流浪了! 拿到旅行证就感觉踏实多了,但还有机场警察局最后一关,不坐上回国的飞机是放不下心来的。 有了旅行证,下午才敢出去逛逛。 吃完晚饭回到酒店,从网上浏览丢护照的相关信息。发现机票和护照是对应的,丢失护照后,机票的护照信息应更新为新办的旅行证信息。于是打电话给航空公司,航空公司表示需要通过预订机票的电话拨打才可以办理,而我们的机票是公司帮买的,于是又联系公司同事帮忙处理。幸好是提前发现,否则去机场值机时才发现又会浪费很多时间。 07 警察局03 2018-11-10 周六 这天是我在德国非法滞留的第一天,同时我的飞机是今天13:45起飞。 这天我在酒店早早吃了早饭,退完房就直奔机场。 8点不到就到了机场,首先去了机场的警察局,接待的工作人员是个很nice的小哥,得知我签证有效期已经过了,表示这种情况有可能呆监狱,我一听脑子快炸开了。小哥接着说,我这种情况需要去法兰克福出入境管理局办理申根延期。之前市区的警察局说在机场警察局就可以解放问题,这跟预想的完全不一样啊。飞机将在13:45起飞,我觉得还有时间去出入境管理局办理后,再赶回机场。于是跟小哥要了具体地址,并在地图上标注出位置,准备去出入境管理局,当时内心是崩溃的。小哥也觉察到了我的囧境,于是说先帮我打电话给出入境管理局问问,小哥打了几通电话都没有人接,当时还没到9点出入境管理局还没有上班。小哥让我坐着等会等上班了再联系 熬到九点,小哥继续打电话还是没有人接,才发现今天是星期六,出入境管理局放假了。小哥告知我先不要担心,他和一帮同事商量看看,过程中他多次出来询问我一些相关信息,我就在大厅焦急等待。同时内心也准备一系列应对措施首先又需要改签机票,具体改到那天还定不了,至少要推迟到下周一以后。接下来要继续订酒店,准备去出入境管理局处理等等...... 正在内心煎熬的时候, 小哥推门出来,手上拿着我的报警记录等材料,我已经做好坏的打算。这时小哥非常肯定的说,不用担心,你今天可以坐飞机回国的,需要做的是先去check in,再带着机票和相关材料返回这里,然后会给我开相关证明文件。 我又看到希望了,于是返回值机的地方。由于太早还不能check in,需要11点才能check in。我找了机场的售票处的工作人员,找了一圈才找到了我的航空公司,表明我要去警察局希望提前check in, 工作人员表示没有办法,只能等待。 08 退税&check in 好吧,那就等吧,还有两个小时到11点。这段时间闲着没事就研究了下怎么退税,流程比较简单。从Terminal 2进来,左边是盖章,需要check in后带着机票和退税单来盖章。盖完章就可以去右边Tax free去领钱了。 来退税的绝大数都是中国人,中国人多购买力也强。顺便提下,法兰克福市中心的采尔购物中心的那些奢侈品牌店都有中文服务员。 每当有国内的航班时,这两个退税的窗口排的队就特别长。 没想到很早我们的航班就已经有很多人开始排队了,我比较着急,就拿着旅行证去找工作人员询问是否可以直接排公务舱的队,工作人员说没问题于是到值机时间,我很快就完成了换票。 由于排在靠前,换完票看到退税的地方还没有人排队,于是花了几分种就迅速完成了退税流程。 09 警察局 退完税,赶回警察局。按门铃,工作人员马上就听出来了,说是早上的来过的吧,于是打开门进去了,我拿着机票和报警凭证给了另一个警察小哥,他要了我在国内的地址,婚姻状况等信息,就进去准备相关材料了。开出来的材料有好几页,分为英文班和德文版,小哥指导我填完表并签字,然后回到里面工位,给我的旅行证盖了个章。 小哥递给了我盖了章的旅行证,说你可以通关了。 我着急的询问他,我该从哪个通道过去呢? 这时小哥热情的说,我带你过去。出乎意料的是,小哥没有带我走正常的通关通道,而是从侧面的空通道直接过去了,是的,直接就过去了。警察小哥还一直领着我,并为我指明了登记口的位置,我向小哥表达了感谢,我们才道别离开。 于是,我顺利的上了飞机。 在飞机上还有个小插曲,广播说是由于啥原因要推迟2小时起飞,我心想不会取消航班吧。幸好,过了一会,广播又说我们可以起飞了。 经过11个小时飞行,顺利到达浦东国际机场。过关时给出旅行证便顺利入境了。 直到此时,我的心才终于踏实下来。 10 最后 如果大使馆没有给旅行证,如果机场警察局没有给通关文件,事情会发展成什么样都很难说 现在想来,还如梦一般 全文完 写于 2018-11-12 杭州
title: MySQL · myrocks · collation 限制 author: 张远 背景 MyRocks中的数据是按索引列以memcmp方式进行排序的。对于一些数字类型,需要进行转化才能直接通过memcmp进行比较, 例如有符号数在计算机中是用补码表示的,那么如果负数和正数直接按字节比较,结果负数会比正数大,实际存储时会将符号会反转存储,读取时再转化回来。对于字符类型,处理更加复杂,涉及到字符集的转换。 记录格式可以参考[[1]](https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format), [[2]](https://yq.aliyun.com/articles/62648) MyRocks索引字段如果包含字符类型,默认只支持binary collation,binary、latin1_bin、 utf8_bin其中的一种 # Error create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set gbk; ERROR 3046 (HY000): Unsupported collation on string indexed column test.t1.c2 Use binary collation (binary, latin1_bin, utf8_bin). # Error create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set utf8 collate utf8_general_ci; ERROR 3046 (HY000): Unsupported collation on string indexed column test.t1.c2 Use binary collation (binary, latin1_bin, utf8_bin). # OK create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set utf8 collate utf8_bin; Query OK, 0 rows affected (0.00 sec) 通过设置rocksdb_strict_collation_check参数为OFF可以跳出binary collation的限制 set global rocksdb_strict_collation_check=OFF; # OK create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set gbk; Query OK, 0 rows affected (0.01 sec) 问题 MyRocks和InnoDB一样支持covering index. MyRocks在使用二级索引查询的时候,应尽量使用covering index, 因为MyRocks回表通过主键随机查询数据的开销比较大。 例如以下场景,idx1作为convering index被使用 create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set utf8 collate utf8_bin; insert into t1 values(1,'ab'); insert into t1 values(2,'cd'); # covering index explain select c2 from t1 where c2='ab'; +----+-------------+-------+-------+---------------+------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------------+ | 1 | SIMPLE | t1 | const | c2 | c2 | 33 | const | 1 | Using index | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------------+ 然而设置set global rocksdb_strict_collation_check=OFF;在某些情况下会导致我们无法使用covering index. set global rocksdb_strict_collation_check=ON; #Error create table t1(c1 int primary key, c2 int, c3 varchar(10), key idx1(c2,c3)) engine =rocksdb character set utf8 collate utf8_general_ci; ERROR 3046 (HY000): Unsupported collation on string indexed column test.t1.c3 Use binary collation (binary, latin1_bin, utf8_bin). set global rocksdb_strict_collation_check=OFF; # OK create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set utf8 collate utf8_general_ci; Query OK, 0 rows affected (0.00 sec) insert into t1 values(1,1,'ab'); insert into t1 values(2,2,'cd'); insert into t1 values(1,'ab'); insert into t1 values(2,'cd'); # non-covering index explain select c2 from t1 where c2='ab'; +----+-------------+-------+-------+---------------+------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------+ | 1 | SIMPLE | t1 | const | c2 | c2 | 33 | const | 1 | NULL | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------+ 1 row in set (0.00 sec) 原因 MyRocks二级索引由于collation的关系导致查询没有走covering index. MyRocks中索引列需要转化为memcomparable的形式,转化分为以下三种情况 1) 直接转换,不需要存储额外信息 例如 binary、latin1_bin、 utf8_bin三种collation可以直接转换 这种情况二级索引列数据可以完整的从二级索引中取到,不影响covering index使用 2) 间接转换,需在value中增加unpack_info 例如latin1_general_ci,latin2_general_ci, ascii_general_ci,greek_general_ci等collation,具体可以参考函数rdb_is_collation_supported 这种情况二级索引列数据可以从key和unpack_info中解析取到,也不影响covering index使用 create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set latin1 collate latin1_general_ci; insert into t1 values(1,'ab'); insert into t1 values(2,'cd'); # covering index explain select c2 from t1 where c2='ab'; +----+-------------+-------+-------+---------------+------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------------+ | 1 | SIMPLE | t1 | const | c2 | c2 | 13 | const | 1 | Using index | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------------+ 1 row in set (0.00 sec) 3) 无法转换 除1,2两种情况外的collation, 例如utf8_general_ci 此时从二级索引中获取不到key的完整信息,需要从主键索引上获取,因此不能走covering index create table t1(c1 int primary key, c2 varchar(10) unique) engine =rocksdb character set utf8 collate utf8_general_ci; insert into t1 values(1,'ab'); insert into t1 values(2,'cd'); # non-covering index explain select c2 from t1 where c2='ab'; +----+-------------+-------+-------+---------------+------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------+ | 1 | SIMPLE | t1 | const | c2 | c2 | 33 | const | 1 | NULL | +----+-------------+-------+-------+---------------+------+---------+-------+------+-------+ 1 row in set (0.00 sec) 此时的数据获取路径如下 0 myrocks::ha_rocksdb::convert_record_from_storage_format 1 myrocks::ha_rocksdb::get_row_by_rowid 2 myrocks::ha_rocksdb::get_row_by_rowid 3 myrocks::ha_rocksdb::read_row_from_secondary_key 4 myrocks::ha_rocksdb::index_read_map_impl 5 myrocks::ha_rocksdb::index_read_map 6 handler::index_read_idx_map 7 handler::ha_index_read_idx_map 8 join_read_const 9 join_read_const_table 10 make_join_statistics 11 JOIN::optimize 12 mysql_execute_select 13 mysql_select 14 handle_select 15 execute_sqlcom_select 16 mysql_execute_command 转换的具体实现可以参考函数Rdb_field_packing::setup MyRocks从索引读取数据时,不能仅通过keyread_only来判断是否可以使用covering index, 还需要判断是否存在collation列数据转换的问题,如果访问的列无法转换就不能使用covering index. MyRocks会在value中存储covered_bitmap,表示索引列是否可以转换, read_row_from_secondary_key/secondary_index_read读取时会根据covered_bitmap来决定是否能使用covering index bool Rdb_key_def::covers_lookup(TABLE *const table, const rocksdb::Slice *const unpack_info, const MY_BITMAP *const lookup_bitmap) const { DBUG_ASSERT(lookup_bitmap != nullptr); if (!use_covered_bitmap_format() || lookup_bitmap->bitmap == nullptr) { return false; } Rdb_string_reader unp_reader = Rdb_string_reader::read_or_empty(unpack_info); // Check if this unpack_info has a covered_bitmap const char *unpack_header = unp_reader.get_current_ptr(); const bool has_covered_unpack_info = unp_reader.remaining_bytes() && unpack_header[0] == RDB_UNPACK_COVERED_DATA_TAG; if (!has_covered_unpack_info || !unp_reader.read(RDB_UNPACK_COVERED_HEADER_SIZE)) { return false; } MY_BITMAP covered_bitmap; my_bitmap_map covered_bits; bitmap_init(&covered_bitmap, &covered_bits, MAX_REF_PARTS, false); covered_bits = rdb_netbuf_to_uint16((const uchar *)unpack_header + sizeof(RDB_UNPACK_COVERED_DATA_TAG) + RDB_UNPACK_COVERED_DATA_LEN_SIZE); return bitmap_is_subset(lookup_bitmap, &covered_bitmap); } 总结 MyRocks在字符collation上的限制需要引起我们关注,使用不当会影响查询效率。
title: MySQL · RocksDB · Write Prepared Policy author: 张远 背景 早期RocksDB TransactionDB将事务的更新操作都缓存在WriteBatch中,事务提交时才写WAL和memtable。RocksDB支持二阶段提交(2PC)后,在prepare阶段写WAL, WriteBatch写memtable依然在commit阶段。这种方式的优点是,事务隔离性比较好,当前事务看不见其它事务的未提交数据, 事务的可见性仅通过sequence大小即可判断,参考这里 , 另外事务回滚也比较简单,只需要释放WriteBatch即可。 但同时也存在以下缺点 事务提交操作比较重,延迟较大 事务都缓存在WriteBatch中,对大事务不友好 无法支持read uncommitted隔离级别 Write Policy 针对TransactionDB的以上缺点, rocksdb引入了新的提交策略(write policy), 共有以下write policy WriteCommitted 即原有的方式,提交时才会写WriteBatch, 默认为WRITE_COMMITTED方式. WritePrepared 将写memtable提前到prepare阶段。 prepare阶段写WAL, 并且写memtable commit阶段写commit标记到WAL。 WritePrepared方式减轻了提交的操作,但并不能解大事务的问题。 WriteUnPrepared 将写memtable提前到每次写操作。 目前此方式还在开发中。 WritePrepared方式减轻了提交的操作,同时也能解大事务的问题。 本文主要介绍WritePrepared的实现方式。 WritePrepared问题 WritePrepared方式将写memtable提前到prepare阶段,会引入以下问题 写入memtable的记录如何判断可见性 WritePrepared方式记录中的sequence是在prepare阶段就分配的,对于某个snapshot来说,snapshot大于此sequence并不代表此记录对snapshot可见。 如何回滚memtable中的记录 不像WriteCommitted方式直接释放WriteBatch就可以回滚事务,WritePrepared方式回滚时memtable中的记录需要以一定的方式回滚 WritePrepared方式在prepare阶段写memtable时会分配sequece, 设为prepare_seq, prepare_seq会存储到记录上。同时提交时会记录一个sequence, 设为commit_seq,commit_seq并不存在记录上,通过commit_seq可判断记录可见性。 snapshot > commit_seq, 此记录可见 这里就存在矛盾了,能判断记录可见性的commit_seq并不存储在记录上 事务可见性分析 对于下图中,设记录的key是唯一的,对于snapshot=8来说,R1, R2两个记录是可见的,因为R1,R2的commit_seq都小于8。而R3,R4,R5的commit_seq都大于8,所以R3,R4,R5是不可见的。 在分析WritePrepared事务可见性实现之前,我们先来看看可见性最简单的实现方式 第一种方式 每个事务开始时, 获取当前已经开启但未提交的事务列表,称之为read_view. 在rocksdb中read_view为prepare_seq的集合, 其中min_seq 为read_view中的最小sequence, max_seq为readview中的最大sequence. 对于snapshot=S事务可见性规则如下: prepare_seq < min_seq, 事务在S前已提交,可见。例如上例R1 prepare_seq > max_seq, 事务在S后开启,不可见。例如上例R5 prepare_seq exist in read_view, 对于S来说,事务已经开启,但未提交,不可见。例如上例R3R4 其它情况,可见。例如上例R2 上例中read_view = {4,5}, min_seq=4, max_seq=5 这种方式不需要commit_seq. 但每个事务都需要维护read_view. innodb 的可见性就是通过此规则来实现的 第二种方式 commit_seq并没有存储在记录中,我们可以在内存中维护commit_seq信息,假设我们将每个已经提交的事务信息对(prepare_seq,commit_seq)都存储起来称为commit_cache 对于snapshot=S事务可见性规则如下 prepare_seq > S 不可见,例如上例R5 prepare_seq exist in commit_cache, 通过对应的commit_seq判断是否可见, 例如上例R1,R2的commit_seq <= S 可见,而R3,R4,R5的commit_seq > S 不可见。 prepare_seq not exist in commit_cache, 未提交事务,不可见。 上例中commit_cache = {<1,2>, <4,9>,<5,10>,<6,7>, <11,12> } 这种方式简单,但需要存储所有已提交的信息,不太可行。 WritePrepared 可见性实现分析 rocksdb WritePrepared的实现折中了以上两种方式。先介绍WritePrepared引入的一些数据结构 commit_cache commit_cache保存所有的已经提交的事务信息对, 但commit_cache会以CommitCache[prepare_seq % array_size] = 方式淘汰, prepared_seq是递增的,所以commit_cache的淘汰大体上是先进先出的, 这样commit_cache基本上保存的是最近提交的事务信息。 其中max_evicted_seq_记录淘汰出的最大的prepare_seq。 prepared_txns_ prepared_txns_是一个最小堆,保存当前prepare但未提交的事务。prepared_txns_在prepare时加入prepare_seq,在commit时踢除. delayed_prepared_ delayed_prepared_保存的是未提交事务。 Commitcache发生evict时, AdvanceMaxEvictedSeq推进max_evicted_seq_,prepared_txns_中小于max_evicted_seq_都踢除,并加入到delayed_prepared_. delayed_prepared_在commit时也会踢除, 小于max_evicted_seq_的未提交事务都在delayed_prepared_中 prepared_txns_和delayed_prepared_都是prepare的但没有commit的事务 也就是说当前prepare的但没有commit的,要么在prepared_txns_中要么在delayed_prepared_中 min_uncommitted_ min_uncommitted_事务开启快照时获取的最小未提交事务即prepared_txns_.top old_commit_map_ old_commit_map_存储snapshot对应的未提交事务列表. Commitcache发生evict时[pre_seq,commit], 存在某个snapshot, 如果满足prepare_seq < snapshot < commit_seq, 这个prepare_seq会加入old_commit_map_. 也就是说old_commit_map_只存储哪些被evict的事务,对应某些snapshot来说是不可见的。事务提交ReleaseSnapshotInternal时从old_commit_map_移除 事务可见性判断以max_evicted_seq_为界, prepare_seq小于等于max_evicted_seq_时按第一种方式处理, prepare_seq大于max_evicted_seq_时按第二种方式处理。 prepare_seq大于max_evicted_seq_ 直接应用第二种方式的规则 prepare_seq > S 不可见 prepare_seq exist in commit_cache,通过对应的commit_seq判断是否可见 prepare_seq not exist in commit_cache, 未提交事务,不可见 prepare_seq小于等于max_evicted_seq_ 基本上对应于第一种方式的规则 prepare_seq exist in delayed_prepared_, 事务未提交,不可见 prepare_seq < min_uncommitted_, 事务在S前已提交,可见。 snapshot > max_evicted_seq, 事务在S前已提交,可见。 prepare_seq exist in old_commit_map_, 对于S来说,事务已经开启,但未提交,不可见。 其它情况,可见。 WritePrepared 可见性判断还是比较高效的prepare_seq大于max_evicted_seq_时可以通过commit_cache快速判断 prepare_seq小于max_evicted_seq_时又分为以下几种情况 prepare_seq < min_uncommitted可以快速判断可见 min_uncommitted 和max_evicted_seq_之间, 未提交的在delayed_prepared_不可见,提交的有一部分在commit_cache,前面已判断。 > 另一部分提交的已evict掉,通过Snapshot > max_evicted_seq_ 可以快速判断可见。 最后的就通过old_commit_map_来判断prepare_seq是否在某个live snapshot中 这种方式对长事务不友好,如果有一个很老的事务未提交,那么min_uncommitted 和max_evicted_seq_之前的区间会比较大,判断就比较低效 如果commit_cache比较大_(默认8M个entry), 且都是短事务的场景,这样基本可以保证新开启事务的Snapshot > max_evicted_seq, 有这个条件就不需要去判断old_commit_map_。 举个例子 源码逻辑如下: inline bool IsInSnapshot(uint64_t prep_seq, uint64_t snapshot_seq, uint64_t min_uncommitted = 0) const { ROCKS_LOG_DETAILS(info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " min_uncommitted %" PRIu64, prep_seq, snapshot_seq, min_uncommitted); // Here we try to infer the return value without looking into prepare list. // This would help avoiding synchronization over a shared map. // TODO(myabandeh): optimize this. This sequence of checks must be correct // but not necessary efficient if (prep_seq == 0) { // Compaction will output keys to bottom-level with sequence number 0 if // it is visible to the earliest snapshot. ROCKS_LOG_DETAILS( info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 1); return true; } if (snapshot_seq < prep_seq) { // snapshot_seq < prep_seq <= commit_seq => snapshot_seq < commit_seq ROCKS_LOG_DETAILS( info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 0); return false; } if (!delayed_prepared_empty_.load(std::memory_order_acquire)) { // We should not normally reach here WPRecordTick(TXN_PREPARE_MUTEX_OVERHEAD); ReadLock rl(&prepared_mutex_); ROCKS_LOG_WARN(info_log_, "prepared_mutex_ overhead %" PRIu64, static_cast<uint64_t>(delayed_prepared_.size())); if (delayed_prepared_.find(prep_seq) != delayed_prepared_.end()) { // Then it is not committed yet ROCKS_LOG_DETAILS(info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 0); return false; } } // Note: since min_uncommitted does not include the delayed_prepared_ we // should check delayed_prepared_ first before applying this optimization. // TODO(myabandeh): include delayed_prepared_ in min_uncommitted if (prep_seq < min_uncommitted) { ROCKS_LOG_DETAILS(info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32 " because of min_uncommitted %" PRIu64, prep_seq, snapshot_seq, 1, min_uncommitted); return true; } auto indexed_seq = prep_seq % COMMIT_CACHE_SIZE; CommitEntry64b dont_care; CommitEntry cached; bool exist = GetCommitEntry(indexed_seq, &dont_care, &cached); if (exist && prep_seq == cached.prep_seq) { // It is committed and also not evicted from commit cache ROCKS_LOG_DETAILS( info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, cached.commit_seq <= snapshot_seq); return cached.commit_seq <= snapshot_seq; } // else it could be committed but not inserted in the map which could happen // after recovery, or it could be committed and evicted by another commit, // or never committed. // At this point we dont know if it was committed or it is still prepared auto max_evicted_seq = max_evicted_seq_.load(std::memory_order_acquire); // max_evicted_seq_ when we did GetCommitEntry <= max_evicted_seq now if (max_evicted_seq < prep_seq) { // Not evicted from cache and also not present, so must be still prepared ROCKS_LOG_DETAILS( info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 0); return false; } // When advancing max_evicted_seq_, we move older entires from prepared to // delayed_prepared_. Also we move evicted entries from commit cache to // old_commit_map_ if it overlaps with any snapshot. Since prep_seq <= // max_evicted_seq_, we have three cases: i) in delayed_prepared_, ii) in // old_commit_map_, iii) committed with no conflict with any snapshot. Case // (i) delayed_prepared_ is checked above if (max_evicted_seq < snapshot_seq) { // then (ii) cannot be the case // only (iii) is the case: committed // commit_seq <= max_evicted_seq_ < snapshot_seq => commit_seq < // snapshot_seq ROCKS_LOG_DETAILS( info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 1); return true; } // else (ii) might be the case: check the commit data saved for this // snapshot. If there was no overlapping commit entry, then it is committed // with a commit_seq lower than any live snapshot, including snapshot_seq. if (old_commit_map_empty_.load(std::memory_order_acquire)) { ROCKS_LOG_DETAILS( info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 1); return true; } { // We should not normally reach here unless sapshot_seq is old. This is a // rare case and it is ok to pay the cost of mutex ReadLock for such old, // reading transactions. WPRecordTick(TXN_OLD_COMMIT_MAP_MUTEX_OVERHEAD); ROCKS_LOG_WARN(info_log_, "old_commit_map_mutex_ overhead"); ReadLock rl(&old_commit_map_mutex_); auto prep_set_entry = old_commit_map_.find(snapshot_seq); bool found = prep_set_entry != old_commit_map_.end(); if (found) { auto& vec = prep_set_entry->second; found = std::binary_search(vec.begin(), vec.end(), prep_seq); } if (!found) { ROCKS_LOG_DETAILS(info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 1); return true; } } // (ii) it the case: it is committed but after the snapshot_seq ROCKS_LOG_DETAILS( info_log_, "IsInSnapshot %" PRIu64 " in %" PRIu64 " returns %" PRId32, prep_seq, snapshot_seq, 0); return false; } 事务可见性的判断会用到数据的读取和compaction过程中的数据是否存在live snapshot上面。 WritePrepared 回滚处理 以prepare_seq-1为snapshot开启事务,如果查找不到,说明之前是第一次插入key, 则通过Delete回滚。如果存在老值,则用老值覆盖来回滚。 源码片段如下 s = db_->GetImpl(roptions, cf_handle, key, &pinnable_val, &not_used, &callback); assert(s.ok() || s.IsNotFound()); if (s.ok()) { s = rollback_batch_->Put(cf_handle, key, pinnable_val); assert(s.ok()); } else if (s.IsNotFound()) { // There has been no readable value before txn. By adding a delete we // make sure that there will be none afterwards either. s = rollback_batch_->Delete(cf_handle, key); assert(s.ok()); } else { // Unexpected status. Return it to the user. } 总结 WritePrepare方式减轻了事务提交的负担,但对事务可见性的处理也引入了复杂性,同时回滚动作的开销也比较大。rocksdb对事务可见性的判断也做了很多优化,比如使用了很多lock-free算法等。而对于MySQL 2pc来说回滚并不多,一般发生在crash recover的时候,因此,回滚的开销也不用太在意。
title: MySQL · myrocks · clustered index特性 author: 张远 Cluster index介绍 最近在RDS MyRocks中,我们引入了一个重要功能,二级聚集索引(secondary clustering index). 我们知道innodb和rocksdb引擎的主键就是clustered index。二级聚集索引和普通二级索引的区别是,普通二级索引只包括索引列和主键列数据,而二级聚集索引列包含表的所有列数据。可以说二级聚集索引是表数据的一个完整的copy. 下面通过例子来看下二级聚集索引和普通二级索引在查询优化上的区别 普通二级索引查询使用了c2普通二级索引,但不是cover index create table t1(c1 int primary key, c2 int, c3 int, key(c2)) engine=rocksdb; explain select * from t1 where c2=22; id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE t1 ref c2 c2 5 const 1 NULL 二级聚集索引查询使用了c2二级聚集索引,并且是cover index create table t1(c1 int primary key, c2 int, c3 int, clustering key(c2)) engine=rocksdb; explain select * from t1 where c2=22; id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE t1 ref c2 c2 5 const 1 Using index Cluster index用法 建表时指定cluster index create table t1(c1 int primary key, c2 int clustering, c3 int) engine=rocksdb; create table t2(c1 int primary key, c2 int clustering unique, c3 int) engine=rocksdb; create table t3(c1 int primary key, c2 int, c3 int, clustering key(c3)) engine=rocksdb; 修改cluster index create clustering index idx1 on t1(c2); alter table t1 add clustering key(c3); 一个表支持同时建多个cluster index create table t1(c1 int primary key, c2 int clustering , c3 int, clustering key(c3)) engine=rocksdb; cluster index的优势 二级聚集索引相对普通二级索引,查询可以走cover index,可以省去二级索引回主键查数据的代价。对于MyRocks读能力不强的引擎来说,cover index显得尤为重要。 那么问题来了,如果我把表的所有列都建成一个普通二级索引,那么和二级聚集索引可以达到一样的效果,一样也可以使用cover index. 然而,二级索引有一些限制 MySQL索引最多支持16列,否则报如下错误1070: Too many key parts specified; max 16 parts allowed MyRocks索引列总长度限制16K max_supported_key_length 另外,二级聚集索引性能更好 全列普通二级索引key的长度较大,排序的开销更大 全列普通二级索引在MyRocks中,数据都在key中,存储key时格式是memcomparable的,存取数据需encode/decode, 而二级聚集索引数据主要在value中,value中的数据不需要encode/decode 二级聚集索引更易维护 对表的执行加减列操作后,全列普通二级索引需要重建,而二级聚集索引则不需要。 cluster index数据格式 普通二级索引 MyRocks中普通二级索引对应的KV存储格式如下: key: index_id,NULL-byte, 二级索引列, 主键列value: unpack_info key由index_id,二级索引键和主键组成, 其中NULL-byte表示索引列是否为空。value只有unpack_info,表示二级索引键和主键列转换为memcomparable格式的信息,如果不需要额外转换信息则unpace_info为null 二级聚集索引 MyRocks中二级聚集索引对应的KV存储格式如下: key: index_id,NULL-byte, 二级索引列, 主键列value: unpack_info, 表中其他所有列 相对普通二级索引,value中还包括索引其他所有列的数据 cluster index更新 由于二级聚集索引包含所有列信息,执行update语句更新非索引列时,二级聚集索引数据也需要更新。例如, t1表c2列为普通二级索引,c3列为二级聚集索引 create table t1(c1 int primary, c2 int unique, c3 int clustering, c4 int) engine=rocksdb; insert into t1 values(1,1,1,1); 执行以下更新时,c2列为普通二级索引不需要更新,但二级聚集索引需要更新(delete+insert)。 update t1 set c4=2 where c1=1; 总结 二级聚集索引是MyRocks表数据的一个完整copy, 结合MyRocks高压缩特性,这种冗余数据的方式在MyRocks上非常合适。二级聚集索引是MyRocks的一个重要feature,它能够让查询尽量走cover index,避免回表操作,提升了MyRocks的读能力。
概述 MyRocks TTL(Time To Live) 特性允许用户指定表数据的自动过期时间,表数据根据指定的时间在compact过程中进行清理。 MyRocks TTL 简单用法如下, 在comment中通过ttl_duration指定过期时间,ttl_col指定过期时间列 CREATE TABLE t1 ( a bigint(20) NOT NULL, b int NOT NULL, ts bigint(20) UNSIGNED NOT NULL, PRIMARY KEY (a), KEY kb (b) ) ENGINE=rocksdb COMMENT='ttl_duration=1;ttl_col=ts;'; 也可以不指定过期时间列ttl_col,插入数据时会隐式将当前时间做为过期时间列存储到记录中。 CREATE TABLE t1 ( a bigint(20) NOT NULL, PRIMARY KEY (a) ) ENGINE=rocksdb COMMENT='ttl_duration=1;'; 分区表也同样支持TTL CREATE TABLE t1 ( c1 BIGINT, c2 BIGINT UNSIGNED NOT NULL, name VARCHAR(25) NOT NULL, event DATE, PRIMARY KEY (`c1`) COMMENT 'custom_p0_cfname=foo;custom_p1_cfname=bar;custom_p2_cfname=baz;' ) ENGINE=ROCKSDB COMMENT="ttl_duration=1;custom_p1_ttl_duration=100;custom_p1_ttl_col=c2;custom_p2_ttl_duration=5000;" PARTITION BY LIST(c1) ( PARTITION custom_p0 VALUES IN (1, 2, 3), PARTITION custom_p1 VALUES IN (4, 5, 6), PARTITION custom_p2 VALUES IN (7, 8, 9) ); RocksDB TTL 介绍MyRocks TTL实现之前,先来看看RocksDB TTL。 RocksDB 本身也支持TTL, 通过DBWithTTL::Open接口,可以指定每个column_family的过期时间。 每次put数据时,会调用DBWithTTLImpl::AppendTS将过期时间append到value最后。 在Compact时通过自定义的TtlCompactionFilter , 去判断数据是否可以清理。具体参考DBWithTTLImpl::IsStale bool DBWithTTLImpl::IsStale(const Slice& value, int32_t ttl, Env* env) { if (ttl <= 0) { // Data is fresh if TTL is non-positive return false; } int64_t curtime; if (!env->GetCurrentTime(&curtime).ok()) { return false; // Treat the data as fresh if could not get current time } int32_t timestamp_value = DecodeFixed32(value.data() + value.size() - kTSLength); return (timestamp_value + ttl) < curtime; } RocksDB TTL在compact时才清理过期数据,所以,过期时间并不是严格的,会有一定的滞后,取决于compact的速度。 MyRocks TTL 实现 和RocksDB TTL column family级别指定过期时间不同,MyRocks TTL可表级别指定过期时间。 MyRocks TTL表过期时间存储在数据字典INDEX_INFO中,表中可以指定过期时间列ttl_col, 也可以不指定, 不指定时会隐式生成ttl_col. 对于主键,ttl_col的值存储在value的头8个字节中,对于指定了过期时间列ttl_col的情况,value中ttl_col位置和valule的头8个字节都会存储ttl_col值,这里有一定的冗余。具体参考convert_record_to_storage_format 读取数据会自动跳过ttl_col占用的8个字节,参考convert_record_from_storage_format 对于二级索引,也会存储ttl_col同主键保持一致,其ttl_col存储在value的unpack_info中, if (m_index_type == INDEX_TYPE_SECONDARY && m_total_index_flags_length > 0) { // Reserve space for index flag fields unpack_info->allocate(m_total_index_flags_length); // Insert TTL timestamp if (has_ttl() && ttl_bytes) { write_index_flag_field(unpack_info, reinterpret_cast<const uchar *const>(ttl_bytes), Rdb_key_def::TTL_FLAG); } } 二级索引ttl_col同主键保持一致。 对于更新显式指定的ttl_col列时,所有的二级索引都需要更新,即使此列不在二级索引列中 MyRocks TTL 清理 MyRocks TTL 清理也发生在compact时,由Rdb_compact_filter定义清理动作, 具体参考should_filter_ttl_rec RocksDB TTL中过期时间和当前时间做比较,而MyRocks TTL 的过期时间是和最老的快照时间(m_snapshot_timestamp )做比较(当没有快照时,也取当前时间)。 bool should_filter_ttl_rec(const rocksdb::Slice &key, const rocksdb::Slice &existing_value) const { uint64 ttl_timestamp; Rdb_string_reader reader(&existing_value); if (!reader.read(m_ttl_offset) || reader.read_uint64(&ttl_timestamp)) { std::string buf; buf = rdb_hexdump(existing_value.data(), existing_value.size(), RDB_MAX_HEXDUMP_LEN); // NO_LINT_DEBUG sql_print_error("Decoding ttl from PK value failed in compaction filter, " "for index (%u,%u), val: %s", m_prev_index.cf_id, m_prev_index.index_id, buf.c_str()); abort(); } /* Filter out the record only if it is older than the oldest snapshot timestamp. This prevents any rows from expiring in the middle of long-running transactions. */ return ttl_timestamp + m_ttl_duration <= m_snapshot_timestamp; } MyRocks TTL 读过滤 前面讲到, RocksDB TTL 过期时间并不严格,取决于compaction速度。MyRocks TTL也有类似问题,因此MyRocks引入参数rocksdb_enable_ttl_read_filtering, 当开启此参数时,过期时间是严格的。每次读取记录会调用should_hide_ttl_rec判断此记录是否过期,当compact操作不及时而没有清理的过期记录,在读取时会被过滤掉。 bool ha_rocksdb::should_hide_ttl_rec(const Rdb_key_def &kd, const rocksdb::Slice &ttl_rec_val, const int64_t curr_ts) { DBUG_ASSERT(kd.has_ttl()); DBUG_ASSERT(kd.m_ttl_rec_offset != UINT_MAX); /* Curr_ts can only be 0 if there are no snapshots open. should_hide_ttl_rec can only be called when there is >=1 snapshots, unless we are filtering on the write path (single INSERT/UPDATE) in which case we are passed in the current time as curr_ts. In the event curr_ts is 0, we always decide not to filter the record. We also log a warning and increment a diagnostic counter. */ if (curr_ts == 0) { update_row_stats(ROWS_HIDDEN_NO_SNAPSHOT); return false; } if (!rdb_is_ttl_read_filtering_enabled() || !rdb_is_ttl_enabled()) { return false; } Rdb_string_reader reader(&ttl_rec_val); /* Find where the 8-byte ttl is for each record in this index. */ uint64 ts; if (!reader.read(kd.m_ttl_rec_offset) || reader.read_uint64(&ts)) { /* This condition should never be reached since all TTL records have an 8 byte ttl field in front. Don't filter the record out, and log an error. */ std::string buf; buf = rdb_hexdump(ttl_rec_val.data(), ttl_rec_val.size(), RDB_MAX_HEXDUMP_LEN); const GL_INDEX_ID gl_index_id = kd.get_gl_index_id(); // NO_LINT_DEBUG sql_print_error("Decoding ttl from PK value failed, " "for index (%u,%u), val: %s", gl_index_id.cf_id, gl_index_id.index_id, buf.c_str()); DBUG_ASSERT(0); return false; } /* Hide record if it has expired before the current snapshot time. */ uint64 read_filter_ts = 0; #ifndef NDEBUG read_filter_ts += rdb_dbug_set_ttl_read_filter_ts(); #endif bool is_hide_ttl = ts + kd.m_ttl_duration + read_filter_ts <= static_cast<uint64>(curr_ts); if (is_hide_ttl) { update_row_stats(ROWS_FILTERED); } return is_hide_ttl; } MyRocks TTL 潜在问题 Issue#683 中谈到了MyRocks TTL 有个潜在问题, 当更新显式指定的ttl_col列值时,compact时有可能将新的记录清理掉,而老的记录仍然保留,从而有可能读取到本该不可见的老记录。此问题暂时还没有close. 最后 MyRocks TTL 是一个不错的特性,可以应用在历史数据清理的场景。相比传统的Delete数据的方式,更节约空间和CPU资源,同时传统的Delete还会影响查询的效率。目前MyRocks TTL 还不够成熟,还有许多需要改进的地方。
概述 MyRocks中RocksDB作为基于快照的事务引擎,其在事务支持上有别于InnoDB,有其自身的特点。在早期的月报[[myrocks之事务处理]](http://mysql.taobao.org/monthly/2016/11/02/) 中,我们对锁的实现有过简单的分析,本文会以一些例子来介绍MyRocks是如果来加锁解锁的。 锁类型 MyRocks早期只支持排他锁,支持SELEC... IN SHARE MODE后,MyRocks才开始引入共享锁。 /* Type of locking to apply to rows */ enum { RDB_LOCK_NONE, RDB_LOCK_READ, RDB_LOCK_WRITE } m_lock_rows; #587是关于共享锁的一个有趣BUG,有兴趣的同学可以看看。 MyRocks的锁都是内存锁,因此MyRocks事务不宜持有过多的锁,以避免占用过多的内存。 MyRocks通过参数rocksdb_max_row_locks来控制单个事务所持有锁的总数。另外,rocksdb锁系统还支持以下参数 max_num_locks:系统锁个数总限制 expiration_time:锁过期时间 如果锁个数超出限制,客户端会返回下面的错误 failed: 12054: Status error 10 received from RocksDB: Operation aborted: Failed to acquire lock due to max_num_locks limit 隔离级别 MyRocks的事务隔离级只支持的READ-COMMITED和REPEATABLE-READ。隔离级别的支持和snapshot密切相关,隔离级别为READ-COMMITED时,事务中每的个stmt都会建立一个snapshot, 隔离级别为REPEATABLE-REA时,只在事务开启后第一个stmt建立一次snapshot。MyRocks中隔离级别不同不会影响加锁和解锁的行为,因此,后面在分析MyRocks的加锁解锁时不区分隔离级别。 隐式主键 MyRocks支持创建无主键的表,但RocksDB作为KV存储,是需要KEY的。因此,RocksDB内部会给表增加一个名为"HIDDEN_PK_ID"的隐式主键列,此值自增,类似与自增列。此列对于MySQL server层是透明的,读取表数据时会自动跳过"HIDDEN_PK_ID"列。 对于无主键的表,MyRocks的锁都是加在隐式主键上的。 对于binlog复制来说,MyRocks隐式主键并不会提升复制速度,因为隐式主键对server层是透明的,主键列不会记入binlog。因此,建议MyRocks表都指定主键。 加锁分析 以此表结构来分析各类语句的加锁情况。 create table t1(id int primary key, c1 int unique, c2 int, c3 int, key idx_c2(c2)) engine=rocksdb; insert into t1 values(1,1,1,1); insert into t1 values(2,2,2,2); insert into t1 values(3,3,3,3); insert into t1 values(4,4,4,4); 示例 select select * from t1; MVCC, 普通读不加锁 示例 select .. in share mode select * from t1 where id=1 in share mode; 对主键id=1记录加S锁 示例 select .. for update select * from t1 where id=1 for update; 对主键id=1记录加X锁 示例 insert begin; insert into t1 values(1,1,1,1); rollback; 主键id=1加X锁唯一索引c1=1加X锁 示例 delete by主键 begin; delete from t1 where id=1; rollback; 主键id=1加X锁 示例 delete by唯一索引 begin; delete from t1 where c1=2; rollback; 主键id=2加X锁,其他索引不加锁 示例 delete by普通索引 begin; delete from t1 where c2=3; rollback; 主键id=3加X锁,其他索引不加锁 示例 delete by无索引 begin; delete from t1 where c3=4; rollback; 对主键每条加X锁,其他索引不加锁 实际上server层过滤不符合条件的行会释放锁,最终只对主键id=4加X锁 示例 delete by 主键不存在的行 begin; delete from t1 where id=100; rollback; 主键id=100加X锁 示例 delete by 其他索引不存在的行 begin; delete from t1 where c1=100; rollback; 没有锁可以加 以上例子基本可以覆盖所有加锁的情况,再举例几个例子练习下 示例 select for update begin; select * from t1 where c2=3 for update; rollback; 主键id=3加X锁, 其他索引不加锁 示例 update更新无索引列 begin; update t1 set c3=5 where c3=4; rollback; 对主键每条加X锁,其他索引不加锁 实际上server层过滤不符合条件的行会释放锁,最终只对主键id=4加X锁 示例 update更新索引列 begin; update t1 set c2=5 where c3=4; rollback; 对主键每条加X锁,其他索引不加锁 实际上server层过滤不符合条件的行会释放锁,最终只对主键id=4加X锁 同时会对唯一索引c2=5加X锁 对于无主键表的表说,RocksDB内部会有隐式主键,所加锁都在隐式主键上 解锁 事务提交或回滚时都会将事务所持有的锁都释放掉。 另外一种情况是,对于不满足查询条件的记录,MySQL会提前释放锁。 总结 MyRocks只会对主键和唯一索引加锁,普通索引不会加锁。 只有插入或更新了唯一索引时,才会对唯一索引加锁,对唯一索引加锁的目的是为了保证唯一性。 按主键锁定查找不存在的行时,会对不存在的行主键加X锁。 按二级索引查找时,只会对主键加锁,不会对二级锁引加锁。 S锁只应用于SELECT ... IN SHARE MODE语句。 堆栈 最后提供一些堆栈信息,方便学习 走唯一索引对主键加锁 #0 rocksdb::TransactionLockMgr::TryLock #1 rocksdb::PessimisticTransactionDB::TryLock #2 rocksdb::PessimisticTransaction::TryLock #3 rocksdb::TransactionBaseImpl::GetForUpdate #4 myrocks::Rdb_transaction_impl::get_for_update #5 myrocks::ha_rocksdb::get_for_update #6 myrocks::ha_rocksdb::get_row_by_rowid #7 get_row_by_rowid #8 myrocks::ha_rocksdb::read_row_from_secondary_key #9 myrocks::ha_rocksdb::index_read_map_impl #10 myrocks::ha_rocksdb::read_range_first #11 handler::multi_range_read_next #12 QUICK_RANGE_SELECT::get_next #13 rr_quick #14 mysql_delete #15 mysql_execute_command #16 mysql_parse #17 dispatch_command 提交时解锁 #0 rocksdb::TransactionLockMgr::UnLockKey #1 rocksdb::TransactionLockMgr::UnLock #2 rocksdb::PessimisticTransactionDB::UnLock #3 rocksdb::PessimisticTransaction::Clear #4 rocksdb::PessimisticTransaction::Commit #5 myrocks::Rdb_transaction_impl::commit_no_binlog #6 commit #7 myrocks::rocksdb_commit #8 ha_commit_low #9 TC_LOG_DUMMY::commit #10 ha_commit_trans #11 trans_commit #12 mysql_execute_command #13 mysql_parse #14 dispatch_command
title: MySQL · MyRocks · MyRocks参数介绍 author: 张远 以下参数是db级别的,全局有效 参数 说明 备注 rocksdb_block_cache 缓存uncompressed blocks,此cache有分区优化,分区数由table_cache_numshardbits控制,默认为6即64个分区。 每个分区至少大于512k(rocksdb::LRUCache::LRUCache) 默认为512M rocksdb_max_total_wal_size 如果WAL超过rocksdb_max_total_wal_size,会swich memtable并flush memtable 默认为0, 表示大小不能超过所有 columnfamily write_buffer的4倍 rocksdb_wal_size_limit_mb purge wal时最多可以保留wal的最大大小 (对应DBOptions::WAL_size_limit_MB) 默认为0,表示不控制保留wal数量, 只要memtable flush了wal都可以 purge rocksdb_wal_ttl_seconds 控制purge wal的频率,每隔rocksdb_wal_ttl_seconds/2 purge一次 。如果rocksdb_wal_size_limit_mb > 0, 那么每600s purge一次(kDefaultIntervalToDeleteObsoleteWAL) 默认为0 rocksdb_manual_wal_flush If true WAL is not flushed automatically after each write. Instead it relies on manual invocation of FlushWAL to write the WAL buffer to its file. 默认为true rocksdb_deadlock_detect 是否开启死锁检测 默认是关闭的 rocksdb_wal_bytes_per_sync 每rocksdb_wal_bytes_per_sync字节sync一次WAL(WritableFileWriter::Flush) 默认为0, 每次都刷 rocksdb_wal_recovery_mode 重启时recovery模式 1: Fail to start, do not recover 0: If corrupted last entry: truncate and start 2: Truncate everything after corrupted entry • Even not corrupted entries • Acceptable on slaves 3: Truncate only corrupted entry • Most dangerous option rocksdb_strict_collation_exceptions 可以取非memcompare类型collation的表 取值为正则表达式,如"t1,t2*" rpl_skip_tx_api Use write batches for replication thread instead of tx api 作用于备库 rocksdb_master_skip_tx_api Disables Transaction API Enables WriteBatch API, There is no row lock,UPDATE and DELETEs are faster You must ensure no concurrent operation running rocksdb_read_free_rpl_tables 用正则表达式指定使用read free replication的库表,如.或t. 默认为空 rocksdb_info_log_level 日志级别,数值越小越详细 0:debug_level 1:info_level 2:warn_level 3:error_level 4:fatal_level 5:header_level rocksdb_perf_context_level 指定 perf context的级别 0,1: disable 2: enable only count stats 3: Other than count stats, also enable time stats except for mutexes 4: enable count and time stats 默认0 rocksdb_max_background_jobs 后台工作线程数 老版本还分为rocksdb_max_background_jobs和max_background_compactions,新版合为一个,会自动分配两者数量。 https://github.com/facebook/rocksdb/wiki/Thread-Pool rocksdb_commit_in_the_middle Commit rows implicitly every rocksdb_bulk_load_size, 设置rocksdb_bulk_load为on时自动commit in middle 默认OFF, 不建议全局设置,应回话级别设置 rocksdb_blind_delete_primary_key 通过主键delete 有且仅有主键索引的表时,不需要读取数据,直接通过指定的主键来删除 默认OFF, DELETES by Primary Key Works:DELETE FROM t WHERE id IN (1, 2, 3, 4, 5, 6, ...., 10000) Does not work: DELETE .. WHERE id < 10 rocksdb_use_direct_reads use O_DIRECT for reading data 默认OFF rocksdb_use_direct_io_for_flush_and_compaction use O_DIRECT for flush and compact 默认OFF rocksdb_skip_fill_cache Skip filling block cache on read requests 默认OFF, DDL load 时使用 gap_lock_raise_error Using Gap Lock without full unique key in multi-table or multi-statement transactions is not allowed. 违法以上情况使用gap lock会记入错误日志 默认false gap_lock_write_log Using Gap Lock without full unique key in multi-table or multi-statement transactions is not allowed.违法以上情况使用gap lock会记入gap_lock_log_file指定的文件中 默认false gap_lock_log_file 指定记录gap lock的文件 rocksdb_stats_dump_period_sec 控制Statistic信息记录到LOG中的频率(DBImpl::PrintStatistics) 默认600, Note that currently it is only dumped after a compaction. So if the database doesn't serve any write for a long time, statistics may not be dumped, despite of options.stats_dump_period_sec. rocksdb_compaction_readahead_size If non-zero, we perform bigger reads when doing compaction. If you're running RocksDB on spinning disks, you should set this to at least 2MB. That way RocksDB's compaction is doing sequential instead of random reads. 默认为0 rocksdb_advise_random_on_open If set true, will hint the underlying file system that the file access pattern is random, when a sst file is opened. 默认ON rocksdb_max_row_locks 事务最多可以持有锁的个数 默认1M rocksdb_bytes_per_sync 每rocksdb_wal_bytes_per_sync字节sync一次sst文件(WritableFileWriter::Flush) 默认为0, 每次都刷 You may consider using rate_limiter to regulate write rate to device.When rate limiter is enabled, it automatically enables bytes_per_syncto 1MB. rocksdb_enable_ttl Enable expired TTL records to be dropped during compaction 默认ON rocksdb_enable_ttl_read_filtering For tables with TTL, expired records are skipped/filtered out during processing and in query results. Disabling this will allow these records to be seen, but as a result rows may disappear in the middle of transactions as they are dropped during compaction. Use with caution. 默认ON rocksdb_bulk_load bulk_load开关 默认OFF, https://github.com/facebook/mysql-5.6/wiki/data-loading rocksdb_bulk_load_allow_unsorted 支持非主键排序数据的bulk_load 默认OFF rocksdb_bulk_load_size 每rocksdb_bulk_load_size次write进行一次bulk_load 默认1000次 rocksdb_enable_bulk_load_api Enables using SstFileWriter for bulk loading 默认ON rocksdb_enable_2pc 是否开启2pc 默认ON rocksdb_rate_limiter_bytes_per_sec 控制读写sst的速度 DBOptions::rate_limiter bytes_per_sec for RocksDB 默认0 rocksdb_sst_mgr_rate_bytes_per_sec 控制删除sst的速度 DBOptions::sst_file_manager rate_bytes_per_sec for RocksDB 默认0 rocksdb_delayed_write_rate WriteStall时delay的时间,单位微秒(DBOptions::delayed_write_rate) 默认0 rocksdb_write_disable_wal 是否关闭WAL 默认为OFF rocksdb_flush_log_at_trx_commit Sync wal on transaction commit Similar to innodb_flush_log_at_trx_commit. 1: sync on commit, 0,2: not sync on commit 默认1 rocksdb_cache_index_and_filter_blocks index和filter blocks是否缓存到block cache 默认ON rocksdb_pin_l0_filter_and_index_blocks_in_cache if cache_index_and_filter_blocks is true and the below is true, then filter and index blocks are stored in the cache, but a reference is held in the "table reader" object so the blocks are pinned and only evicted from cache when the table reader is freed. 默认ON 以上参数可以通过show variables查看 更详细可以参考代码 db_options_type_info include/rocksdb/options.h 以下参数是column family级别的,可以分别对每个column family设置 参数 说明 备注 write_buffer_size memtable内存大小 默认 max_write_buffer_number memtable的最大个数 默认2 min_write_buffer_number_to_merge it is the minimum number of memtables to be merged before flushing to storage. For example, if this option is set to 2, immutable memtables are only flushed when there are two of them 默认1 target_file_size_base level1 sst大小 默认64M target_file_size_multiplier level L(L>1) sst大小 target_file_size_base * (target_file_size_multiplier ^ (L-1)) 默认1, For example, if target_file_size_base is 2MB andtarget_file_size_multiplier is 10, then each file on level-1 willbe 2MB, and each file on level-2 will be 20MB,and each file on level-3 will be 200MB max_bytes_for_level_base level1的sst总大小 默认256M max_bytes_for_level_multiplier level L的sst总大小为 max_bytes_for_level_base(max_bytes_for_level_multiplier)^(L-1))max_bytes_for_level_multiplier_additional(L-1) (VersionStorageInfo::CalculateBaseBytes) 默认10 max_bytes_for_level_multiplier_additional Different max-size multipliers for different levels. (VersionStorageInfo::CalculateBaseBytes) 默认:1:1:1:1:1:1:1 num_levels level数量 默认7 level0_file_num_compaction_trigger 当level0文件数量超过此值时触发level0 compact 默认4 level0_slowdown_writes_trigger 当level0文件数量超过此值时触发x写delay 默认20 level0_stop_writes_trigger 当level0文件数量超过此值时触发停写 默认36 pin_l0_filter_and_index_blocks_in_cache if cache_index_and_filter_blocks is true and the below is true, then filter and index blocks are stored in the cache, but a reference is held in the "table reader" object so the blocks are pinned and only evicted from cache when the table reader is freed. 默认1, column family单独设置会覆盖rocksdb_pin_l0_filter_and_index_blocks_in_cache cache_index_and_filter_blocks index和filter blocks是否缓存到block cache 默认1, column family单独设置会覆盖rocksdb_cache_index_and_filter_blocks optimize_filters_for_hits 设置为True, 最后一层不保存filter信息,最后一层bloomfilter实际没有用处 默认OFF filter_policy 指定filter策略 filter_policy=bloomfilter:10:false 表示使用bloomfilter, bits_per_key_=10, hash函数个数为10*ln2, false:use_block_based_builder_=false,表示使用full filter prefix_extractor 指定filter使用前缀 prefix_extractor=capped:24表示最多取前缀24个字节,另外还有fixed:n方式表示只取前缀n个字节,忽略小于n个字节的key. 具体可参考CappedPrefixTransform,FixedPrefixTransform partition_filters 表示时否使用partitioned filter 默认false filter 参数优先级如下 block base > partitioned > full. 比如说同时指定use_block_based_builder_=true和partition_filters=true实际使用的block based filter whole_key_filtering If true, place whole keys in the filter (not just prefixes) 默认1 level_compaction_dynamic_level_bytes In this mode, size target of levels are changed dynamically based on size of the last level. 减少写放大 http://rocksdb.org/blog/2015/07/23/dynamic-level.html memtable 指定memtable类型(skiplist/vector/hash_linkedlist/prefix_hash/cuckoo) 默认skiplist compaction_pri compact选择文件策略 kByCompensatedSize: Slightly prioritize larger files by size compensated by #deletes kOldestLargestSeqFirst: First compact files whose data's latest update time is oldest kOldestSmallestSeqFirst: First compact files whose range hasn't been compacted to the next level for the longest kMinOverlappingRatio: First compact files whose ratio between overlapping size in next level and its size is the smallest 默认kByCompensatedSize compression_per_level 指定每个level的压缩策略 It usually makes sense to avoid compressing levels 0 and 1 and to compress data only in higher levels. You can even set slower compression in highest level and faster compression in lower levels (by highest we mean Lmax). bottommost_compression 指定最底level的压缩策略 arena_block_size rocksdb内存分配单位KBlockSize由参数arena_block_size指定 arena_block_size不指定时默认为write_buffer_size的1/8. soft_pending_compaction_bytes_limit All writes will be slowed down to at least delayed_write_rate if estimated bytes needed to be compaction exceed this threshold 默认64G hard_pending_compaction_bytes_limit All writes are stopped if estimated bytes needed to be compaction exceed this threshold. 默认256G 以上参数可以通过select * from information_schema.rocksdb_cf_options查看 更详细可以参考代码ParseColumnFamilyOption, cf_options_type_info include/rocksdb/table.h rocksdb/util/options_helper.h rocksdb/options/options_helper.cc include/rocksdb/advanced_options.h 参数配置示例 rocksdb_default_cf_options=memtable=vector;arena_block_size=10M;disable_auto_compactions=1;min_write_buffer_number_to_merge=1;write_buffer_size=100000m;target_file_size_base=32m;max_bytes_for_level_base=512m;level0_file_num_compaction_trigger=20;level0_slowdown_writes_trigger=30;level0_stop_writes_trigger=30;max_write_buffer_number=5;compression_per_level=kNoCompression:kNoCompression:kNoCompression:kNoCompression:kNoCompression:kNoCompression;bottommost_compression=kNoCompression;block_based_table_factory={cache_index_and_filter_blocks=1;filter_policy=bloomfilter:10:false;whole_key_filtering=1};level_compaction_dynamic_level_bytes=false;optimize_filters_for_hits=true 参数修改示例 SET @@global.rocksdb_update_cf_options='cf1={write_buffer_size=8m;target_file_size_base=2m};cf2={write_buffer_size =16m;max_bytes_for_level_multiplier=8};cf3={target_file_size_base=4m};'; 注意:此方式可以动态修改,但没有持久化到OPTIONS文件中, 需手动修改OPTIONS文件
title: MySQL · myrocks · myrocks相关tools介绍 author: 张远 概述 MyRocks提供了丰富的tools,如sst_dump, mysql_ldb等,这些工具对我们的运维和分析问题非常有用。 sst_dump 可以导出sst中的数据和属性信息。 sst_dump --help sst_dump --file=<data_dir_OR_sst_file> [--command=check|scan|raw] --file=<data_dir_OR_sst_file> Path to SST file or directory containing SST files --command=check|scan|raw|verify check: Iterate over entries in files but dont print anything except if an error is encounterd (default command) scan: Iterate over entries in files and print them to screen raw: Dump all the table contents to <file_name>_dump.txt verify: Iterate all the blocks in files verifying checksum to detect possible coruption but dont print anything except if a corruption is encountered recompress: reports the SST file size if recompressed with different compression types --output_hex Can be combined with scan command to print the keys and values in Hex --from=<user_key> Key to start reading from when executing check|scan --to=<user_key> Key to stop reading at when executing check|scan --prefix=<user_key> Returns all keys with this prefix when executing check|scan Cannot be used in conjunction with --from --read_num=<num> Maximum number of entries to read when executing check|scan --verify_checksum Verify file checksum when executing check|scan --input_key_hex Can be combined with --from and --to to indicate that these values are encoded in Hex --show_properties Print table properties after iterating over the file when executing check|scan|raw --set_block_size=<block_size> Can be combined with --command=recompress to set the block size that will be used when trying different compression algorithms --compression_types=<comma-separated list of CompressionType members, e.g., kSnappyCompression> Can be combined with --command=recompress to run recompression for this list of compression types --parse_internal_key=<0xKEY> Convenience option to parse an internal key on the command line. Dumps the internal key in hex format {'key' @ SN: type} mysql_ldb 工具功能非常强大,可以解析rocksdb各类文件,sst, manifest, wal等; 可以写入和查询数据;还提供了一些维护功能,详见如下help信息 bin/mysql_ldb --help ldb - RocksDB Tool commands MUST specify --db=<full_path_to_db_directory> when necessary The following optional parameters control if keys/values are input/output as hex or as plain strings: --key_hex : Keys are input/output as hex --value_hex : Values are input/output as hex --hex : Both keys and values are input/output as hex The following optional parameters control the database internals: --column_family=<string> : name of the column family to operate on. default: default column family --ttl with 'put','get','scan','dump','query','batchput' : DB supports ttl and value is internally timestamp-suffixed --try_load_options : Try to load option file from DB. --ignore_unknown_options : Ignore unknown options when loading option file. --bloom_bits=<int,e.g.:14> --fix_prefix_len=<int,e.g.:14> --compression_type=<no|snappy|zlib|bzip2|lz4|lz4hc|xpress|zstd> --compression_max_dict_bytes=<int,e.g.:16384> --block_size=<block_size_in_bytes> --auto_compaction=<true|false> --db_write_buffer_size=<int,e.g.:16777216> --write_buffer_size=<int,e.g.:4194304> --file_size=<int,e.g.:2097152> Data Access Commands: put <key> <value> [--ttl] get <key> [--ttl] batchput <key> <value> [<key> <value>] [..] [--ttl] scan [--from] [--to] [--ttl] [--timestamp] [--max_keys=<N>q] [--start_time=<N>:- is inclusive] [--end_time=<N>:- is exclusive] [--no_value] delete <key> deleterange <begin key> <end key> query [--ttl] Starts a REPL shell. Type help for list of available commands. approxsize [--from] [--to] checkconsistency Admin Commands: dump_wal --walfile=<write_ahead_log_file_path> [--header] [--print_value] compact [--from] [--to] reduce_levels --new_levels=<New number of levels> [--print_old_levels] change_compaction_style --old_compaction_style=<Old compaction style: 0 for level compaction, 1 for universal compaction> --new_compaction_style=<New compaction style: 0 for level compaction, 1 for universal compaction> dump [--from] [--to] [--ttl] [--max_keys=<N>] [--timestamp] [--count_only] [--count_delim=<char>] [--stats] [--bucket=<N>] [--start_time=<N>:- is inclusive] [--end_time=<N>:- is exclusive] [--path=<path_to_a_file>] load [--create_if_missing] [--disable_wal] [--bulk_load] [--compact] manifest_dump [--verbose] [--json] [--path=<path_to_manifest_file>] list_column_families full_path_to_db_directory dump_live_files idump [--from] [--to] [--input_key_hex] [--max_keys=<N>] [--count_only] [--count_delim=<char>] [--stats] repair backup [--backup_env_uri] [--backup_dir] [--num_threads] [--stderr_log_level=<int (InfoLogLevel)>] restore [--backup_env_uri] [--backup_dir] [--num_threads] [--stderr_log_level=<int (InfoLogLevel)>] checkpoint [--checkpoint_dir] Note: ldb是rocksdb自带的工具,其功能和mysql_ldb一致。但mysql_ldb使用了MyRocks的comparactor, 因此推荐使用mysql_ldb 示例初始化 create table t1( c1 int , c2 int, c3 varchar(10), primary key (c1), key idx1(c2)) engine=rocksdb; insert t1 values (1,101,'A'); insert t1 values (2,102,'B'); insert t1 values (3,103,'C'); insert t1 values (4,104,'D'); insert t1 values (5,105,'E'); select * from t1; +----+------+------+ | c1 | c2 | c3 | +----+------+------+ | 1 | 101 | A | | 2 | 102 | B | | 3 | 103 | C | | 4 | 104 | D | | 5 | 105 | E | +----+------+------+ SET GLOBAL rocksdb_force_flush_memtable_now=1; select d.table_schema, d.table_name, d.index_name, d.index_number,f.sst_name, f.NUM_ROWS, f.ENTRY_DELETES, f.ENTRY_SINGLEDELETES,f.ENTRY_MERGES, f.ENTRY_OTHERS from information_schema.ROCKSDB_DDL d, information_schema.rocksdb_index_file_map f where d.index_number=f.index_number and d.table_name='t1'; +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | table_schema | table_name | index_name | index_number | sst_name | NUM_ROWS | ENTRY_DELETES | ENTRY_SINGLEDELETES | ENTRY_MERGES | ENTRY_OTHERS | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | test | t1 | PRIMARY | 256 | 000030.sst | 5 | 0 | 0 | 0 | 0 | | test | t1 | idx1 | 257 | 000030.sst | 5 | 0 | 0 | 0 | 0 | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ 使用 sst_dump 查看test.t1数据分布情况 select d.table_schema, d.table_name, d.index_name, d.index_number,f.sst_name, f.NUM_ROWS, f.ENTRY_DELETES, f.ENTRY_SINGLEDELETES,f.ENTRY_MERGES, f.ENTRY_OTHERS from information_schema.ROCKSDB_DDL d, information_schema.rocksdb_index_file_map f where d.index_number=f.index_number and d.table_name='t1'; +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | table_schema | table_name | index_name | index_number | sst_name | NUM_ROWS | ENTRY_DELETES | ENTRY_SINGLEDELETES | ENTRY_MERGES | ENTRY_OTHERS | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | test | t1 | PRIMARY | 256 | 000030.sst | 5 | 0 | 0 | 0 | 0 | | test | t1 | idx1 | 257 | 000030.sst | 5 | 0 | 0 | 0 | 0 | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ 导出t1的数据 bin/sst_dump --command=scan --file=data/.rocksdb/000030.sst --output_hex from [] to [] Process data/.rocksdb/000030.sst Sst file format: block-based '0000010080000001' seq:13, type:1 => 00650000000141 '0000010080000002' seq:16, type:1 => 00660000000142 '0000010080000003' seq:19, type:1 => 00670000000143 '0000010080000004' seq:22, type:1 => 00680000000144 '0000010080000005' seq:25, type:1 => 00690000000145 '00000101018000006580000001' seq:14, type:1 => '00000101018000006680000002' seq:17, type:1 => '00000101018000006780000003' seq:20, type:1 => '00000101018000006880000004' seq:23, type:1 => '00000101018000006980000005' seq:26, type:1 => 前五行为主键primary数据 后五行为索引idx1的数据 MyRocks记录格式参考这里 以此行(1,101,'A')数据为例子 主键为 '0000010080000001' seq:13, type:1 => 00650000000141 key由indexid+c1+seq+type组成 Primary index id: 256 ==> 100 c1: 1 ==> 80000001 (符号反转) type:1 PUT value 由c2+c3组成 c2: 101 ==> 65 c3: A ==> 141 二级索引idx1数据为 '00000101018000006580000001' seq:14, type:1 => key由indexid+c2(主键)+c1(二级索引键)组成 second index id: 257 ==> 101 c2 : 101 ==> 80000065 主键 c1: 1 ==> 80000001 (符号反转) 二级索引键 value为null Note type值参考如下定义 enum ValueType : unsigned char { kTypeDeletion = 0x0, kTypeValue = 0x1, kTypeMerge = 0x2, kTypeLogData = 0x3, // WAL only. kTypeColumnFamilyDeletion = 0x4, // WAL only. kTypeColumnFamilyValue = 0x5, // WAL only. kTypeColumnFamilyMerge = 0x6, // WAL only. kTypeSingleDeletion = 0x7, kTypeColumnFamilySingleDeletion = 0x8, // WAL only. kTypeBeginPrepareXID = 0x9, // WAL only. kTypeEndPrepareXID = 0xA, // WAL only. kTypeCommitXID = 0xB, // WAL only. kTypeRollbackXID = 0xC, // WAL only. kTypeNoop = 0xD, // WAL only. kTypeColumnFamilyRangeDeletion = 0xE, // WAL only. kTypeRangeDeletion = 0xF, // meta block kTypeColumnFamilyBlobIndex = 0x10, // Blob DB only kTypeBlobIndex = 0x11, // Blob DB only // When the prepared record is also persisted in db, we use a different // record. This is to ensure that the WAL that is generated by a WritePolicy // is not mistakenly read by another, which would result into data // inconsistency. kTypeBeginPersistedPrepareXID = 0x12, // WAL only. kMaxValue = 0x7F // Not used for storing records. }; 按指定范围导出数据 bin/sst_dump --command=scan --file=data/.rocksdb/000030.sst --from='0x0000010080000002' --to='0x0000010080000005' --input_key_hex --output_hex from [0000010080000002] to [0000010080000005] Process data/.rocksdb/000030.sst Sst file format: block-based '0000010080000002' seq:16, type:1 => 00660000000142 '0000010080000003' seq:19, type:1 => 00670000000143 '0000010080000004' seq:22, type:1 => 00680000000144 查看sst属性信息 command=raw可以将数据和属性信息都写到*_dump.txt文件中 bin/sst_dump --command=raw --file=data/.rocksdb/000030.sst --output_hex from [] to [] Process data/.rocksdb/000030.sst Sst file format: block-based raw dump written to file data/.rocksdb/000030_dump.txt 另外选项--show_properties也可以展示属性信息 mysql_ldb 使用 查看sst数据 bin/mysql_ldb --db=data/.rocksdb scan --hex 0x0000010080000001 : 0x00650000000141 0x0000010080000002 : 0x00660000000142 0x0000010080000003 : 0x00670000000143 0x0000010080000004 : 0x00680000000144 0x0000010080000005 : 0x00690000000145 0x00000101018000006580000001 : 0x 0x00000101018000006680000002 : 0x 0x00000101018000006780000003 : 0x 0x00000101018000006880000004 : 0x 0x00000101018000006980000005 : 0x 写入数据 MyRocks在运行过程中,mysql_ldb是不能写入数据的 bin/mysql_ldb --db=data/.rocksdb put --hex 0x0000010080000006 0x006A0000000146 Failed: IO error: While lock file: data/.rocksdb/LOCK: Resource temporarily unavailable shutdown myrocks实例后,再写入成功 bin/mysql_ldb --db=data/.rocksdb put --hex 0x0000010080000006 0x006A0000000146 ok 可以看到数据写到了新的sst(000041.sst)中 select d.table_schema, d.table_name, d.index_name, d.index_number,f.sst_name, f.NUM_ROWS, f.ENTRY_DELETES, f.ENTRY_SINGLEDELETES,f.ENTRY_MERGES, f.ENTRY_OTHERS from information_schema.ROCKSDB_DDL d, information_schema.rocksdb_index_file_map f where d.index_number=f.index_number and d.table_name='t1'; +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | table_schema | table_name | index_name | index_number | sst_name | NUM_ROWS | ENTRY_DELETES | ENTRY_SINGLEDELETES | ENTRY_MERGES | ENTRY_OTHERS | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | test | t1 | PRIMARY | 256 | 000030.sst | 5 | 0 | 0 | 0 | 0 | | test | t1 | idx1 | 257 | 000030.sst | 5 | 0 | 0 | 0 | 0 | | test | t1 | PRIMARY | 256 | 000041.sst | 1 | 0 | 0 | 0 | 0 | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ 同时可以看到新的数据(6,106,'F'); select * from t1; +----+------+------+ | c1 | c2 | c3 | +----+------+------+ | 1 | 101 | A | | 2 | 102 | B | | 3 | 103 | C | | 4 | 104 | D | | 5 | 105 | E | | 6 | 106 | F | +----+------+------+ 二级索引没有写数据,所以查不到 select * from t1 where c2=106; Empty set (0.00 sec) explain select * from t1 where c2=106; +----+-------------+-------+------+---------------+------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+-------+------+-------+ | 1 | SIMPLE | t1 | ref | idx1 | idx1 | 5 | const | 4 | NULL | +----+-------------+-------+------+---------------+------+---------+-------+------+-------+ 接着二级索引插入数据 bin/mysql_ldb --db=data/.rocksdb put --hex 0x00000101018000006A80000006 0x OK 此时可以从二级索引查出数据了 select * from t1 where c2=106; +----+------+------+ | c1 | c2 | c3 | +----+------+------+ | 6 | 106 | F | +----+------+------+ select d.table_schema, d.table_name, d.index_name, d.index_number,f.sst_name, f.NUM_ROWS, f.ENTRY_DELETES, f.ENTRY_SINGLEDELETES,f.ENTRY_MERGES, f.ENTRY_OTHERS from information_schema.ROCKSDB_DDL d, information_schema.rocksdb_index_file_map f where d.index_number=f.index_number and d.table_name='t1'; +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | table_schema | table_name | index_name | index_number | sst_name | NUM_ROWS | ENTRY_DELETES | ENTRY_SINGLEDELETES | ENTRY_MERGES | ENTRY_OTHERS | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ | test | t1 | PRIMARY | 256 | 000030.sst | 5 | 0 | 0 | 0 | 0 | | test | t1 | idx1 | 257 | 000030.sst | 5 | 0 | 0 | 0 | 0 | | test | t1 | PRIMARY | 256 | 000041.sst | 1 | 0 | 0 | 0 | 0 | | test | t1 | idx1 | 257 | 000058.sst | 1 | 0 | 0 | 0 | 0 | +--------------+------------+------------+--------------+------------+----------+---------------+---------------------+--------------+--------------+ mysql_ldb 查看MANIFEST文件 MANIFEST文件格式参考这里 bin/mysql_ldb manifest_dump --path=data/.rocksdb/MANIFEST-000059 --------------- Column family "default" (ID 0) -------------- log number: 58 comparator: RocksDB_SE_v3.10 --- level 0 --- version# 2 --- 58:740[' --------------- Column family "__system__" (ID 1) -------------- log number: 63 comparator: RocksDB_SE_v3.10 --- level 0 --- version# 3 --- 64:773[' next_file_number 66 last_sequence 36 prev_log_number 0 max_column_family 1 mysql_ldb repair 人为将MANIFEST-000059删除,rocksdb数据将无法查看 bin/mysql_ldb --db=data/.rocksdb scan --hex Failed: IO error: While opening a file for sequentially reading: data/.rocksdb/MANIFEST-000059 mysql_ldb 通过 repair 选项可以恢复出MANIFEST bin/mysql_ldb repair --db=data/.rocksdb scan RocksDB version: 5.9.0 Git sha rocksdb_build_git_sha:7c8c83458365f8b359cae13785d15b0bdc9df380 Compile date Dec 16 2017 DB SUMMARY CURRENT file: CURRENT IDENTITY file: IDENTITY SST files in data/.rocksdb dir, Total Num: 7, files: 000030.sst 000039.sst 000041.sst 000047.sst 000054.sst 000058.sst 000064.sst Write Ahead Log file in data/.rocksdb: 000063.log size: 19 ; .......... //省略部分配置信息 [WARN] [/home/zhangyuan.zy/git/rds6/rocksdb/db/repair.cc:209] **** Repaired rocksdb data/.rocksdb; recovered 7 files; 5847bytes. Some data may have been lost. **** OK repair 完成后数据可以查看 bin/mysql_ldb --db=data/.rocksdb scan --hex 0x0000010080000001 : 0x00650000000141 0x0000010080000002 : 0x00660000000142 0x0000010080000003 : 0x00670000000143 0x0000010080000004 : 0x00680000000144 0x0000010080000005 : 0x00690000000145 0x0000010080000006 : 0x006A0000000146 0x00000101018000006580000001 : 0x 0x00000101018000006680000002 : 0x 0x00000101018000006780000003 : 0x 0x00000101018000006880000004 : 0x 0x00000101018000006980000005 : 0x 0x00000101018000006A80000006 : 0x 同时生成了新的MANIFEST文件 bin/mysql_ldb manifest_dump --path=data/.rocksdb/MANIFEST-000003 --------------- Column family "default" (ID 0) -------------- log number: 0 comparator: RocksDB_SE_v3.10 --- level 0 --- version# 2 --- 58:740[' --------------- Column family "__system__" (ID 1) -------------- log number: 0 comparator: RocksDB_SE_v3.10 --- level 0 --- version# 3 --- 64:773[' next_file_number 66 last_sequence 36 prev_log_number 0 max_column_family 1 最后 本文简单介绍了MyRocks相关工具sst_dump和mysql_ldb的基本用法,希望对大家有所帮助。
概述 本文简要介绍了一些存储引擎存储结构,包括InnoDB, TokuDB, RocksDB, TiDB, CockroachDB, 供大家对比分析 InnoDB InnoDB 底层存储结构为B+树,结构如下 B树的每个节点对应innodb的一个page,page大小是固定的,一般设为16k。其中非叶子节点只有键值,叶子节点包含完成数据。 InnoDB按segment, extent, page方式管理page 每个数据节点page结构如下 数据记录record按行存储,record具体格式由row_format决定.详情可以参考数据内核月报 TokuDB TokuDB 底层存储结构为Fractal Tree Fractal Tree的结构与B+树有些类似, 在Fractal Tree中,每一个child指针除了需要指向一个child节点外,还会带有一个Message Buffer ,这个Message Buffer 是一个FIFO的队列,用来缓存更新操作。 例如,一次插入操作只需要落在某节点的Message Buffer就可以马上返回了,并不需要搜索到叶子节点。这些缓存的更新会在查询时或后台异步合并应用到对应的节点中。 RocksDB RockDB的存储结构如下 RocksDB写入数据时,先写到memtable中,memtable一般为skiplist, memtable写满时转为immutable memtable并刷入Level 0. Level0中的SST文件中的数据都是有序的,Level0中SST文件之间的数据范围可能存在重叠。其他Level中的SST文件之间的数据范围不重叠。 RocksDB会以一定的机制从低level compact数据到高level中。 RocksDB中SST文件的结构如下 MyRocks使用的存储引擎就是RocksDB, MyRocks的中RocksDB的数据映射关系参考之前的月报 TiDB TiDB的存储结构 TiDB是分布式存储,分为两个部分TiKV和Placement Driver server。 TiKV用于存储真正的数据,TiKV由分布在不同机器上的RocksDB实例组成。数据按范围划分为一个个Region. 并且会尽量保持每个 Region 中保存的数据不超过一定的大小(这个大小可以配置,目前默认是 64MB). 同一Region分布在不同的RocksDB实例中,一个RocksDB实例包含多个Region.图中,Region4有三个副本分布在三个RocksDB实例中,这三个Region副本组成一个RaftGroup,副本间通过Raft协议保证一致性。 Placement Driver server(PD), 也是一个集群,也通过Raft协议保证一致性。PD主要有以下作用: 存储region的位置等元数据信息 调度和rebalance regions, TiKV中的Raft leader等信息 分配全局事务ID TiDB的数据映射关系 以下表为例 create table user(user_id int primary key, name varchar(100), email varchar(200)); INSERT INTO user VALUES (1, “bob”, “huang@pingcap.com”); INSERT INTO user VALUES (2, “tom”, “tom@pingcap.com”); 对应到RocksDB中的KV结构如下 |Key |Values|| ------------- |:-------------:| |user/1|bob huang@pingcap.com||user/2|tom tom@pingcap.com | CockroachDB CockroachDB的存储结构 CockroachDB的也是分布式存储,其结构和TiDB类似。CockroachDB按范围划分为Range,Range默认为64M,Range的存储为RocksDB, CockroachDB的一个node包含多个RocksDB实例。Range副本分布在不同的node中,通过Raft协议保证一致。 Range的元数据信息也保存在Range中(靠前的Range中). System keys come in several subtypes: Global keys store cluster-wide data such as the "meta1" and "meta2" keys as well as various other system-wide keys such as the node and store ID allocators. Store local keys are used for unreplicated store metadata (e.g. the StoreIdent structure). "Unreplicated" indicates that these values are not replicated across multiple stores because the data they hold is tied to the lifetime of the store they are present on. Range local keys store range metadata that is associated with a global key. Range local keys have a special prefix followed by a global key and a special suffix. For example, transaction records are range local keys which look like: x01ktxn-. Replicated Range ID local keys store range metadata that is present on all of the replicas for a range. These keys are updated via Raft operations. Examples include the range lease state and abort cache entries. Unreplicated Range ID local keys store range metadata that is local to a replica. The primary examples of such keys are the Raft state and Raft log. CockroachDB的数据映射关系 以下表为例 create table mydb.customers(name varchar(100) primary key, address varchar(100) , URL varchar(100)); insert into mydb.customers values('Apple','1 Infinite Loop, Cupertino, CA','http://apple.com/'); 表结构信息 |Key |Values|| ------------- |:-------------:| |/system/databases/mydb/id | 51||/system/tables/customer/id | 42||/system/desc/51/42/address |69||/system/desc/51/42/url |66 表中的数据 |Key |Values|| ------------- |:-------------:| |/51/42/Apple/69| 1 Infinite Loop, Cupertino, CA||/51/42/Apple/66| http://apple.com/| 最后 本文简要介绍了各存储引擎的结构,供大家参考,有错误之处请指正. 参考文档 https://github.com/facebook/rocksdb https://www.percona.com/doc/percona-server/LATEST/tokudb/tokudb_intro.html https://github.com/cockroachdb/cockroach/blob/master/docs/design.md https://github.com/pingcap/tidb https://www.percona.com/live/plam16/sessions/how-we-build-tidb https://dev.mysql.com/doc/internals/en/innodb.htmlhttp://img3.tbcdn.cn/L1/461/1/d0069515c04809a449eda659386afbe966e0d1df
title: MySQL · mysql · myrocks之Bloom filter author: 张远 Bloom filter 简介 Bloom filter用于判断一个元素是不是在一个集合里,当一个元素被加入集合时,通过k个散列函数将这个元素映射成一个位数组中的k个点,把它们置为1。检索时如果这些点有任何一个为0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。优点:布隆过滤器存储空间和插入/查询时间都是常数O(k)。缺点:有一定的误算率,同时标准的Bloom Filter不支持删除操作。Bloom Filter通过极少的错误换取了存储空间的极大节省。 设集合元素个数为n,数组大小为m, 散列函数个数为k 有一个规律是当 k=m/n*ln2 时,误算率最低。参考Bloom_filter wiki rocksdb与bloom filter rocksdb中memtable和SST file都属于集合类数据且不需要删除数据,比较适合于Bloom filter. rocksdb memtable和SST file都支持bloom filter, memtable 的bloom filter数组就存储在内存中,而SST file的bloom filter持久化在bloom filter中. SST Bloom filter SST Boomfilter 在Flush生成SST files时通过计算产生,分为两个阶段 将prefix_extrator指定的key前缀加入到HASH表hash_entries_中 将hash_entries_所有映射到Bloom filter的数组中 SST Bloom filter相关参数有 filter_policy=bloomfilter:10:false; whole_key_filtering=0 prefix_extractor=capped:24 partition_filters=false 其中prefix_extractor=capped:24, 表示最多取前缀24个字节,另外还有fixed:n方式表示只取前缀n个字节,忽略小于n个字节的key. 具体可参考CappedPrefixTransform,FixedPrefixTransform filter_policy=bloomfilter:10:false;其中bits_per_key_=10, bits_per_key_实际就是前面公式k=m/n*ln2 中的m/n. 从而如下计算k即num_probes_的方式 void initialize() { // We intentionally round down to reduce probing cost a little bit num_probes_ = static_cast<size_t>(bits_per_key_ * 0.69); // 0.69 =~ ln(2) if (num_probes_ < 1) num_probes_ = 1; if (num_probes_ > 30) num_probes_ = 30; } use_block_based_builder_表示是使用block base filter还是full filterpartition_filters 表示时否使用partitioned filter,SST数据有序排列,按block_size进行分区后在生产filter,index_on_filter block存储分区范围. 开启partition_filters 需配置index_type =kTwoLevelIndexSearch filter 参数优先级如下 block base > partitioned > full. 比如说同时指定use_block_based_builder_=true和partition_filters=true实际使用的block based filter whole_key_filtering,取值true, 表示增加全key的filter. 它和前缀filter并不冲突可以共存。 memtable Bloom filter memtable 在每次Add数据时都会更新Bloom filter. Bloom filter提供参数memtable_prefix_bloom_size_ratio,其值不超过0.25, Bloom filter数组大小为write_buffer_size* memtable_prefix_bloom_size_ratio. memtable Bloom filter 中的num_probes_取值硬编码为6 另外参数cache_index_and_filter_blocks可以让filter信息缓存在block cache中。 MyRocks和bloom filter 在myrocks中,Bloom filter是全局的,设置了Bloom filter后,所有表都有Bloom filter。Bloom filter和索引是绑定在一起的。也就是说,表在查询过程中,如果可以用到某个索引,且设置了Bloom filter,那么就有可能会用到索引的Bloom filter. MyRocks可以使用Bloom filter的条件如下,详见函数can_use_bloom_filter 必须是索引前缀或索引全列的等值查询 等值前缀的长度应该符合prefix_extrator的约定 我们可以通过以下两个status变量来观察Bloom filter使用情况rocksdb_bloom_filter_prefix_checked:是否使用了Bloom filterrocksdb_bloom_filter_prefix_useful:使用Bloom filter判断出不存在rocksdb_bloom_filter_useful:BlockBasedTable::Get接口使用Bloom filter判断出不存在 设置参数rocksdb_skip_bloom_filter_on_read可以让查询不使用Bloom filter。 示例 最后给个示例参数设置如下,使用partitioned filter rocksdb_default_cf_options=write_buffer_size=64k;block_based_table_factory={filter_policy=bloomfilter:10:false;whole_key_filtering=0;partition_filters=true;index_type=kTwoLevelIndexSearch};prefix_extractor=capped:24 SQL CREATE TABLE t1 (id1 INT, id2 VARCHAR(100), id3 BIGINT, value INT, PRIMARY KEY (id1, id2, id3)) ENGINE=rocksdb collate latin1_bin; let $i = 1; while ($i <= 10000) { let $insert = INSERT INTO t1 VALUES($i, $i, $i, $i); inc $i; eval $insert; } # case 1: 等值条件prefix长度 < 24, 用不Bbloom filter select variable_value into @c from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_checked'; select variable_value into @u from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_useful'; select count(*) from t1 WHERE id1=100 and id2 ='10'; count(*) 0 select (variable_value-@c) > 0 from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_checked'; (variable_value-@c) > 0 0 select (variable_value-@u) > 0 from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_useful'; (variable_value-@u) > 0 0 # case 2: 符合使用Bbloom filter的条件,且成功判断出不存在 select variable_value into @c from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_checked'; select variable_value into @u from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_useful'; select count(*) from t1 WHERE id1=100 and id2 ='00000000000000000000'; count(*) 0 select (variable_value-@c) > 0 from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_checked'; (variable_value-@c) > 0 1 select (variable_value-@u) > 0 from information_schema.global_status where variable_name='rocksdb_bloom_filter_prefix_useful'; (variable_value-@u) > 0 1
title: MySQL · mysql · MySQL replication partial transaction author: 张远 replication 概述 目前MySQL支持的replication方式多种多样 普通的master-slave 异步replication 半同步的semi-sync replication 支持多通道的group replication和double binlog 如果按连接协议来区分,又可以分为 非GTID模式,通过binlog文件名和文件的偏移来决定replication位点信息 GTID模式,通过GTID信息来决定replication位点信息 如果按apply binglog的方式来区分,又可以分为 串行,按binlog event顺序依次执行 并行,以db, table或transaction为粒度的并行复制,以及基于group commit的LOGICAL_CLOCK并行复制 不论哪种replication, 都离不开replication最基本的组件, IO thread,负责从master拉取binlog. SQL thread,负责apply relay log binlog. replication 异常 复制过程中,由于网络或者master主机宕机,都会造成slave IO thread异常中断。例如以下事务在复制过程中发生上述异常, SET GTID_NEXT; # GTID设置为ON时 BEGIN; INSERT row1; INSERT row2; COMMIT; 那么备库接收的binlog可能不包含完整的事务,备库可能仅接收到BEGIN,也可能只接收到INSERT row1. 然而,当IO thread恢复后,SQL线程怎么正确处理这种异常呢? 异常恢复 IO thread 异常中断后,SQL线程是正常工作的,SQL执行了部分事务, 它会等待IO 线程发送新的binlog. IO thread 线程恢复后,SQL线程可以选择继续执行事务或者回滚事务重新执行事务,这是由replication协议决定的。 GTID模式下,设置auto_position=1时,slave会根据GTID信息,从事务起点开始,重新将事务完整binlog发给备库。此时,备库需要回滚之前的部分事务。 GTID模式下,设置auto_position=0或非GTID模式下,slave会根据位点信息从master续传之前的binlog。此时,备库可以继续完成之前的部分事务。 继续执行事务比较简单,但是回滚之前的部分事务就比较复杂. 分为两种情况来分析: 串行复制 串行复制时,完整的事务会由SQL thread来执行,当执行到GTID_LOG_EVENT时,会发这个GTID已经分配过了,这时候就可以回滚事物。具体参考 Gtid_log_event::do_apply_event() if (thd->owned_gtid.sidno) { /* Slave will execute this code if a previous Gtid_log_event was applied but the GTID wasn't consumed yet (the transaction was not committed nor rolled back). On a client session we cannot do consecutive SET GTID_NEXT without a COMMIT or a ROLLBACK in the middle. Applying this event without rolling back the current transaction may lead to problems, as a "BEGIN" event following this GTID will implicitly commit the "partial transaction" and will consume the GTID. If this "partial transaction" was left in the relay log by the IO thread restarting in the middle of a transaction, you could have the partial transaction being logged with the GTID on the slave, causing data corruption on replication. */ if (thd->transaction.all.ha_list) { /* This is not an error (XA is safe), just an information */ rli->report(INFORMATION_LEVEL, 0, "Rolling back unfinished transaction (no COMMIT " "or ROLLBACK in relay log). A probable cause is partial " "transaction left on relay log because of restarting IO " "thread with auto-positioning protocol."); const_cast<Relay_log_info*>(rli)->cleanup_context(thd, 1); } gtid_rollback(thd); } 并行复制 并行复制有别于串行复制,binlog event由worker线程执行。按串行复制的方式来回滚事务是行不通的,因为重新发送的事务binlog并不一定会分配原来的worker来执行。因此,回滚操作需交给coordinate线程(即sql线程)来完成。 GTID模式下,设置auto_position=1时. IO thread重连时,都会发送ROTATE_LOG_EVENT和FORMAT_DESCRIPTION_EVENT. 并且FORMAT_DESCRIPTION_EVENT的log_pos>0. 通过非auto_position方式重连的FORMAT_DESCRIPTION_EVENT的log_pos在send之前会被置为0. SQL线程通过执行FORMAT_DESCRIPTION_EVENT且其log_pos>0来判断是否应进入回滚逻辑。而回滚是通过构造Rollback event让work来执行的。 具体参考 exec_relay_log_event() /* GTID protocol will put a FORMAT_DESCRIPTION_EVENT from the master with log_pos != 0 after each (re)connection if auto positioning is enabled. This means that the SQL thread might have already started to apply the current group but, as the IO thread had to reconnect, it left this group incomplete and will start it again from the beginning. So, before applying this FORMAT_DESCRIPTION_EVENT, we must let the worker roll back the current group and gracefully finish its work, before starting to apply the new (complete) copy of the group. */ if (ev->get_type_code() == FORMAT_DESCRIPTION_EVENT && ev->server_id != ::server_id && ev->log_pos != 0 && rli->is_parallel_exec() && rli->curr_group_seen_gtid) { if (coord_handle_partial_binlogged_transaction(rli, ev)) /* In the case of an error, coord_handle_partial_binlogged_transaction will not try to get the rli->data_lock again. */ DBUG_RETURN(1); } MySQL官方针对此问题有过多次改进,详见以下commit 666aec4a9e976bef4ddd90246c4a31dd456cbca3 3f6ed37fa218ef6a39f28adc896ac0d2f0077ddb 9e2140fc8764feeddd70c58983a8b50f52a12f18 异常case处理 当slave SQL线程处于部分事务异常时,按上节的逻辑,IO thread恢复后,复制是可以正常进行的。但如果IO thread如果长时间不能恢复,那么SQL apply线程会一直等待新的binlog, 并且会一直持有事务中的锁。当slave切换为master后,新master会接受用户连接处理事务,这样SQL apply线程持有的事务锁,可能阻塞用户线程的事务。这是我们不希望看到的。 此时可以通过stop slave来停止SQL apply线程,让事务回滚释放锁。 另一种更好的方案是让SQL apply 线程自动识别这种情况,并加以处理。比如,增加等待超时机制,超时后自动kill sql 线程或回滚SQL线程的部分事务。
title: MySQL · myrocks · myrocks写入分析 author: 张远 写入流程 myrocks的写入流程可以简单的分为以下几步来完成 将解析后的记录(kTypeValue/kTypeDeletion)写入到WriteBatch中 将WAL日志写入log文件 将WriteBatch中的内容写到memtable中,事务完成 其中第2,3步在提交时完成 WriteBatch与Myrocks事务处理密切相关,事务中的记录提交前都以字符串的形式存储在WriteBatch->rep_中,要么都提交,要么都回滚。 回滚的逻辑比较简单,只需要清理WriteBatch->rep_即可。详见TransactionImpl::Rollback 一个简单的insert 写入WriteBatch堆栈如下 #0 rocksdb::WriteBatchInternal::Put #1 rocksdb::WriteBatch::Put #2 myrocks::ha_rocksdb::update_pk #3 myrocks::ha_rocksdb::update_indexes #4 myrocks::ha_rocksdb::update_write_row #5 myrocks::ha_rocksdb::write_row #6 handler::ha_write_row #7 write_record #8 mysql_insert #9 mysql_execute_command #10 mysql_parse #11 dispatch_command #12 do_command #13 do_handle_one_connection 一个简单的insert commit堆栈如下 #0 rocksdb::InlineSkipList<rocksdb::MemTableRep::KeyComparator const&>::Insert #1 rocksdb::(anonymous namespace)::SkipListRep::Insert #2 rocksdb::MemTable::Add #3 rocksdb::MemTableInserter::PutCF #4 rocksdb::WriteBatch::Iterate #5 rocksdb::WriteBatch::Iterate #6 rocksdb::WriteBatchInternal::InsertInto #7 rocksdb::DBImpl::WriteImpl #8 rocksdb::DBImpl::Write #9 rocksdb::TransactionImpl::Commit #10 myrocks::Rdb_transaction_impl::commit_no_binlog #11 myrocks::Rdb_transaction::commit #12 myrocks::rocksdb_commit #13 ha_commit_low #14 TC_LOG_MMAP::commit #15 ha_commit_trans #16 trans_commit_stmt #17 mysql_execute_command #18 mysql_parse #19 dispatch_command #20 do_command #21 do_handle_one_connection 提交流程及优化 这里只分析rocksdb引擎的提交流程,实际MyRocks提交时还需先写binlog(binlog开启的情况). rocksdb引擎提交时就完成两个事情 写WAL日志(WAL开启的情况下rocksdb_write_disable_wal=off) 将之前的WriteBatch写入到memtable中 然而,写WAL是一个串行操作。为了提高提交的效率, rocksdb引入了group commit机制。 待提交的事务都依次加入到提交的writer队列中,这个writer队列被划分为一个一个group. 每个group有一个leader, 其他为follower,leader负责批量写WAL。每个group由双向链表link_older, link_newer链接。如下图所示 每个writer可能的状态如下 Init: writer的初始状态 Header: writer被选为leader Follower: writer被选为follower LockedWating: writer在等待自己转变为指定的状态 Completed:writer操作完成 writer的状态变迁跟group是否并发写memtable有关当开启并发写memtable(rocksdb_allow_concurrent_memtable_write=on)且group中的writer至少有两个时,group才会并发写。 group并发写时writer的状态变迁图如下: group非并发写时writer的状态变迁图如下: 源码结构图如下(图片来自林青) 上面的图是在group内writer并发写memtable的情形。 非并发写memtable时,没有LaunchParallelFollowers/CompleteParallelWorker, Insertmemtable是由leader串行写入的。 这里group commit有以下要点 同一时刻只有一个leader, leader完成操作后,才设置下一个leader 需要等一个group都完成后,才会进行下一个group group中最后一个完成的writer负责完成提交和设置下一个leader Leader 负责批量写WAL 只有leader才会去调整双向链表link_older,link_newer. 注意这里2,3 应该可以优化改进为 不需要等一个group完成再进行下一个group 不同group的follower可以并发执行 只有leader负责完成提交和设置下一个leader 写入控制 rocksdb在提交写入时,需考虑以下几种情况,详见PreprocessWrite WAL日志满,WAL日志超过rocksdb_max_total_wal_size,会从所有的colomn family中找出含有最老日志(the earliest log containing a prepared section)的column family进行flush, 以释放WAL日志空间 Buffer满,全局的write buffer超过rocksdb_db_write_buffer_size时,会从所有的colomn family中找出最先创建的memtable进行切换,详见HandleWriteBufferFull 某些条件会触发延迟写 max_write_buffer_number > 3且 未刷immutable memtable总数 >=max_write_buffer_number-1 自动compact开启时,level0的文件总数 >= level0_slowdown_writes_trigger 某些条件会触发停写 未刷immutable memtable总数 >=max_write_buffer_number 自动compact开启时,level0的文件总数 >= level0_stop_writes_trigger 具体可参考RecalculateWriteStallConditions 总结 rocksdb写入流程还有优化空间,Facebook也有相关的优化。
title: MySQL · myrocks · MyRocks之memtable切换与刷盘 author: 张远 概述 MyRocks的memtable默认是skiplist,其大小和个数分别由参数write_buffer_size和max_write_buffer_number控制。数据写入时先写入active memtable, 当active memtable写满时,active memtable会转化为immutable memtable. immutable memtable数据是不会变化的,最终会刷入level0的sst文件中。 memtable 内存分配 RocksDB有自己的内存分配机制,称为Arena. Arena由固定的inline_block_和动态的blocks_组成。inline_block_固定为2048bytes, blocks_由一系列的block组成,这些block大小一般为KBlockSize, 但从arena申请较大内存时(> KBlockSize/4)单独分配一个所申请大小的block. KBlockSize由参数arena_block_size指定,arena_block_size 不指定时默认为write_buffer_size的1/8. 这里有两个重要的概念 blocks_memory_ Arena当前已分配的内存 alloc_bytes_remaining_ Arena当前block已分配但未使用的内存,注意不是整个Arena已分配而未使用的内存 RocksDB在实际使用内存中用的是ConcurrentArena, 它是在Arena的基础上封装,是线程安全的。同时ConcurrentArena为了提高并发对内存进行了分片,分片数由cpu个数决定,例如cpu核数为24, 则分片数为32,以下是分片的算法 // find a power of two >= num_cpus and >= 8 auto num_cpus = std::thread::hardware_concurrency(); index_mask_ = 7; while (index_mask_ + 1 < num_cpus) { index_mask_ = index_mask_ * 2 + 1; } shards_.reset(new Shard[index_mask_ + 1]); 每个分片都有已分配但未使用的内存, 分片越多浪费的内存越多。 一个有趣的例子 测试环境:CPU核数64,write_buffer_size=1G, arena_block_size=0根据前面的算法,CPU核数64, 内存分片数为64, arena_block_size 默认为write_buffer_size的1/8,对齐后是131072000 我们用1200个连接进行并发插入,这样能够充分使用内存分片数这是测试某个瞬间取得的内存数据 allocated_memory:1179650048 AllocatedAndUnused:1172297392 write_buffer_size:1048576000 BlockSize:131072000 注意AllocatedAndUnused和allocated_memory是如此的接近,也就是说存在巨大的内存浪费。然而这不是最严重的,更严重的是这种情况导致memtable的切换,后面会进行分析。 memtable 切换 memtable 发生切换的条件有 1) memtable内存超过write_buffer_size会切换2) WAL日志满,WAL日志超过rocksdb_max_total_wal_size,会从所有的colomn family中找出含有最老日志(the earliest log containing a prepared section)的memtable进行切换,详见HandleWALFull 3) Buffer满,全局的write buffer超过rocksdb_db_write_buffer_size时,会从所有的colomn family中找出最先创建的memtable进行切换,详见HandleWriteBufferFull 4) flush memtable前会切换memtable, 下节会介绍 下面详细介绍memtable满切换 memtable 满切换 memtable内存超过write_buffer_size会切换,由于arena的内存使用,memtable控制内存使用的算法更加精细,切换条件从源码中很容易理解 bool MemTable::ShouldFlushNow() const { // This constant variable can be interpreted as: if we still have more than // "kAllowOverAllocationRatio * kArenaBlockSize" space left, we'd try to over // allocate one more block. const double kAllowOverAllocationRatio = 0.6; // If arena still have room for new block allocation, we can safely say it // shouldn't flush. auto allocated_memory = table_->ApproximateMemoryUsage() + range_del_table_->ApproximateMemoryUsage() + arena_.MemoryAllocatedBytes(); // if we can still allocate one more block without exceeding the // over-allocation ratio, then we should not flush. if (allocated_memory + kArenaBlockSize < moptions_.write_buffer_size + kArenaBlockSize * kAllowOverAllocationRatio) { return false; } // if user keeps adding entries that exceeds moptions.write_buffer_size, // we need to flush earlier even though we still have much available // memory left. if (allocated_memory > moptions_.write_buffer_size + kArenaBlockSize * kAllowOverAllocationRatio) { return true; } return arena_.AllocatedAndUnused() < kArenaBlockSize / 4; } 而上一节举出的例子正好符合切换的条件,正如前面所说的,内存都分配好了,还没来得及使用就发生切换了,白忙活了一场。 这里的现象是虽然write_buffer_size是1G,但最后刷到level0的sst都远远小于1G。 那么如何避免这种情况呢 减少内存分片数,不建议 调小arena_block_size, 亲测可用 这里有一个原则是arena_block_size*内存分片数应该小于write_buffer_size memtable 切换实现 ** NewWritableFile //创建日志文件 ** ConstructNewMemtable //创建memtable ** cfd->imm()->Add(cfd->mem(), &context->memtables_to_free_); //设置immutable ** cfd->SetMemtable(new_mem); //设置新的memtable flush memtable immutable memtable会不断flush到level0的SST文件中 触发flush的条件有 WAL日志满,WAL日志超过rocksdb_max_total_wal_size,会从所有的colomn family中找出含有最老日志(the earliest log containing a prepared section)的column family进行flush,详见HandleWALFull Buffer满,全局的write buffer超过rocksdb_db_write_buffer_size时,会从所有的colomn family中找出最先创建的memtable的column family进行flush,详见HandleWriteBufferFull 手动设置参数force_flush_memtable_now/rocksdb_force_flush_memtable_and_lzero_now时 CompactRange时 创建checkpoint时 shutdown时avoid_flush_during_shutdown=0会flush所有memtable other rocksdb中设置max_background_flushes=-1可以禁止flush,而MyRocks中rocksdb_max_background_flushes最小值限制为0. 因此,MyRocks若要禁止flush需放开此限制。
title: MySQL · 特性分析 · MyRocks简介 author: 济天 RocksDB是facebook基于LevelDB实现的,目前为facebook内部大量业务提供服务。经过facebook大量工作,将RocksDB作为MySQL的一个存储引擎移植到MySQL,称之为MyRocks。经过两年的发展,MyRocks已经比较成熟(RC阶段),现已进入了facebook MySQL的主分支了。MyRocks是开源的,参见git 。下面对MyRocks做一个简单介绍,不涉及源码。 RocksDB与innodb的比较 innodb空间浪费, B tree分裂导致page内有较多空闲,page利用率不高。innodb现有的压缩效率也不高,压缩以block为单位,也会造成浪费。 写入放大:innodb 更新以页为单位,最坏的情况更新N行会更新N个页。RocksDB append only方式 另外,innodb开启double write也会增加写入。 RocksDB对齐开销小:SST file (默认2MB)需要对齐,但远大于4k, RocksDB_block_size(默认4k) 不需要对齐,因此对齐浪费空间较少 RocksDB索引前缀相同值压缩存储 RocksDB占总数据量90%的最底层数据,行内不需要存储系统列seqid (innodb聚簇索引列包含trxid,roll_ptr等信息) 来看看facebook的测试数据 数据空间对比 QPS 写入放大对比 数据字典 数据字段信息保存在System Column Family (System CF) "__system__"中数据字段信息包括: 表信息,表名和index id的映射 索引信息,索引元数据信息和column family id。column family和index的对应关系 1:N column family,一些标记,比如reverse属性等 binlog信息 统计信息,每个SST file都自带统计信息(行数、实际大小等),在flush或compaction时更新统计信息,同时统计信息会汇总到数据字典统计信息表中。 以上信息可以通过information_schema查看,如RocksDB_ddl,RocksDB_index_file_map等 记录格式 RocksDB的行以key value的形式存储,和innodb类似,记录格式主键和二级索引也有区别 事务与锁 MyRocks也是基于行锁,锁信息都保存在内存中。 MyRocks也支持MVCC,MVCC通过快照的方式实现,类似于PostgreSQL。 MyRocks目前只支持两种隔离级别,RC和RR。 RR表现和innodb并不一样,RocksDB 的快照不是在事务开始的时候建立,而是延迟到第一次读的时候建立. 以下client1 MyRocks返回的是2,innodb返回1 <client 1> <client 2> CREATE TABLE t1(pk INT PRIMARY KEY); INSERT INTO t1 VALUES(1); SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN INSERT INTO t1 VALUES(2); SELECT COUNT(*) FROM t1; // MyRocks返回的是2,innodb返回1 RC表现也不一样,事务1大更新多行过程中,其他事务也可以更新事务还未更新到的行,事务1再更新时会失败。 复制 MyRocks也是通过binlog方式复制,由于binlog与RocksDB之间没有xa,异常crash可能丢数据,所以,MyRocks主备环境建议开启semi-sync. 由于gap lock支持不健全(仅primary key上支持), 使用statement方式复制会导致不一致,所有MyRocks建议使用行级复制。 备份恢复 支持MySQLdumup逻辑备份 #内部会执行以下语句 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION WITH CONSISTENT RocksDB SNAPSHOT; 同时有自动的物理备份工具MyRocks_hotbackup,但还不支持备份innodb; 也不支持增量备份。MyRocks_hotbackup支持流式备份 MyRocks_hotbackup--user=root --port=3306 --checkpoint_dir=/data/backup --stream=xbstream| ssh$dst‘xbstream–x /data/backup’ #内部建立硬链接方式备份数据SST files,checkpoint多次更新,只备份新的SST files, 因此WAL日志很少,恢复时apply log时间很短 SET GLOBAL RocksDB_create_checkpoint= /path/to/backup 一些优化 bloom filterbloom filter一般适用于等值查询 bloom filter信息存储在SST files中,大概占用2~3%的空间 如果大量查询返回空集建议开启bloom filter,如果结果每次都在最底层找到,可以设置optimize_filters_for_hits=true关闭bloom filter以节省空间。 数据加载 数据加载时可以忽略唯一性约束检查,分段自动提交,停写wal等。 以下是推荐的数据加载时的参数配置 rocksdb_skip_unique_check=1 rocksdb_commit_in_the_middle=1 rocksdb_write_disable_wal=1 rocksdb_max_background_flushes=40 rocksdb_max_background_compactions=40 rocksdb_default_cf_options=(in addition to existing parameters); write_buffer_size=128m;level0_file_num_compaction_trigger=4;level0_slowdown_writes_trigger=256;level0_stop_writes_trigger=256;max_write_buffer_number=16;memtable=vector:1024 rocksdb_override_cf_options=(in addition to existing parameters);__system__={memtable=skip_list:16} Reverse column families MyRocks擅长正向扫描,为了提高逆向扫描(ORDER BY DESC)的性能,MyRocks支持了Reverse column families。 在建表可以指定column family的reverse属性。 singleDelete 如果key不会重复put, delete操作可以直接删除put,而不是标记删除。singleDelete可以提高查询效率。 一些限制 MyRocks目前有以下一些限制 不支持分区表,Online ddl,外键,全文索引,空间索引,表空间transport gap lock支持不健全(仅primary key上支持), 使用statement方式复制会导致不一致 不支持select … in share mode 大小写敏感,不支持*_bin collation binlog与RocksDB之间没有xa,异常crash可能丢数据。所以,MyRocks一般开启semi-sync. 不支持savepoint order by 不比较慢 不支持MRR 暂不支持O_DIRECT innodb和RocksDB混合使用还不稳定
Fast data load Load data相比普通insert效率更高,Load data批量插入数据有效减少了解析SQL的开销。MyRocks 同其他MySQL 引擎一样也支持Load data语法,同时MyRocks对data load也做了特殊优化。RocksDB引擎有一个规律是,数据最终会存储在最底层SST文件中,MyRocks通过参数rocksdb_bulk_load控制是否直接将数据存储在最底层SST文件中,而不走普通的insert流程。 先来看下普通insert流程(图片来自yoshinorim) 优化后的bulk load流程(图片来自yoshinorim) 由于SST文件中的数据必须是有序的,所以 bulk load特性有一个限制是插入的数据必须是按主键有序的。 Insert和Load data都支持bulk load特性,Load data文件中的数据容易保证有序,但对于非自增insert来说,要保证有序插入比较困难,因此bulk load特性对普通insert意义不大。 rocksdb_bulk_load设为1后,开启bulk load特性。值得注意的是,在 bulk load特性下,会默认忽略唯一性检查,同时rocksdb_commit_in_the_middle自动开启。 Bulk load 源码实现 step 1 第一次插入时会新建SST临时文件, 参见myrocks::Rdb_sst_info::open_new_sst_file 文件形如:test.t1_PRIMARY_0_0.bulk_load.tmp db.tablename_indexname_count1_count2_.bulk_load.tmp 其中count1每次都会原子自增,防止并发load时出现重名的情况。 其中count2表示当前是第几个SST临时文件 step 2 随后插入都会直接插入到SST临时文件中,参见myrocks::Rdb_sst_info::put step 3 SST临时文件写满或load结束,将SST临时文件copy或hard link为正式的SST文件,同时更新SST元数据信息,参考rocksdb::ExternalSstFileIngestionJob::Prepare/ExternalSstFileIngestionJob::Run step 4 删除临时SST文件,参考ExternalSstFileIngestionJob::Cleanup 如果bulk load中途mysqld crash有可能残留SST临时文件,mysqld重启时会自动清理SST临时文件。参考Rdb_sst_info::init Bulk load 相关测试 load data 测试 Bulk load下rocksdb load data比innodb快近3倍。 Bulk load下rocksdb load data比rocksdb 普通load data快近6倍。 perf top 可以看出bulk load模式下,插入流程要简洁很多。 rocksdb without bulk load rocksdb with bulk load insert 测试 由于SQL解析占比重较大,bulk load模式下的insert优势并不明细。 perf top 可以看出普通insert相比load data有更多的SQL解析操作(MySQLparse),同时非bulk load下的insert比bulk load下insert有更多的排序操作(KeyComparator)。 insert without bulk load insert with bulk load
common table expression Common table expression简称CTE,由SQL:1999标准引入,目前支持CTE的数据库有Teradata, DB2, Firebird, Microsoft SQL Server, Oracle (with recursion since 11g release 2), PostgreSQL (since 8.4), MariaDB (since 10.2), SQLite (since 3.8.3), HyperSQL and H2 (experimental), MySQL8.0. CTE的语法如下: WITH [RECURSIVE] with_query [, ...] SELECT... with_query: query_name [ (column_name [,...]) ] AS (SELECT ...) 以下图示来自MariaDB Non-recursive CTEs Recursive CTEs CTE的使用 CTE的可以使语句更加简洁 例如以下两个语句表达的是同一语义,使用CTE比未使用CTE的嵌套查询更简洁明了。 1) 使用嵌套子查询 SELECT MAX(txt), MIN(txt) FROM ( SELECT concat(cte2.txt, cte3.txt) as txt FROM ( SELECT CONCAT(cte1.txt,'is a ') as txt FROM ( SELECT 'This ' as txt ) as cte1 ) as cte2, ( SELECT 'nice query' as txt UNION SELECT 'query that rocks' UNION SELECT 'query' ) as cte3 ) as cte4; 2) 使用CTE WITH cte1(txt) AS (SELECT "This "), cte2(txt) AS (SELECT CONCAT(cte1.txt,"is a ") FROM cte1), cte3(txt) AS (SELECT "nice query" UNION SELECT "query that rocks" UNION SELECT "query"), cte4(txt) AS (SELECT concat(cte2.txt, cte3.txt) FROM cte2, cte3) SELECT MAX(txt), MIN(txt) FROM cte4; CTE 可以进行树形查询 初始化这颗树 create table t1(id int, value char(10), parent_id int); insert into t1 values(1, 'A', NULL); insert into t1 values(2, 'B', 1); insert into t1 values(3, 'C', 1); insert into t1 values(4, 'D', 1); insert into t1 values(5, 'E', 2); insert into t1 values(6, 'F', 2); insert into t1 values(7, 'G', 4); insert into t1 values(8, 'H', 6); 1) 层序遍历 with recursive cte as ( select id, value, 0 as level from t1 where parent_id is null union all select t1.id, t1.value, cte.level+1 from cte join t1 on t1.parent_id=cte.id) select * from cte; +------+-------+-------+ | id | value | level | +------+-------+-------+ | 1 | A | 0 | | 2 | B | 1 | | 3 | C | 1 | | 4 | D | 1 | | 5 | E | 2 | | 6 | F | 2 | | 7 | G | 2 | | 8 | H | 3 | +------+-------+-------+ 2) 深度优先遍历 with recursive cte as ( select id, value, 0 as level, CAST(id AS CHAR(200)) AS path from t1 where parent_id is null union all select t1.id, t1.value, cte.level+1, CONCAT(cte.path, ",", t1.id) from cte join t1 on t1.parent_id=cte.id) select * from cte order by path; +------+-------+-------+---------+ | id | value | level | path | +------+-------+-------+---------+ | 1 | A | 0 | 1 | | 2 | B | 1 | 1,2 | | 5 | E | 2 | 1,2,5 | | 6 | F | 2 | 1,2,6 | | 8 | H | 3 | 1,2,6,8 | | 3 | C | 1 | 1,3 | | 4 | D | 1 | 1,4 | | 7 | G | 2 | 1,4,7 | +------+-------+-------+---------+ Oracle Oracle从9.2才开始支持CTE, 但只支持non-recursive with, 直到Oracle 11.2才完全支持CTE。但oracle 之前就支持connect by 的树形查询,recursive with 语句可以与connect by语句相互转化。 一些相互转化案例可以参考这里. Oracle recursive with 语句不需要指定recursive关键字,可以自动识别是否recursive. Oracle 还支持CTE相关的hint, WITH dept_count AS ( SELECT /*+ MATERIALIZE */ deptno, COUNT(*) AS dept_count FROM emp GROUP BY deptno) SELECT ... WITH dept_count AS ( SELECT /*+ INLINE */ deptno, COUNT(*) AS dept_count FROM emp GROUP BY deptno) SELECT ... “MATERIALIZE”告诉优化器生产一个全局的临时表保存结果,多次引用CTE时直接访问临时表即可。而"INLINE"则表示每次需要解析查询CTE。 PostgreSQL PostgreSQL从8.4开始支持CTE,PostgreSQL还扩展了CTE的功能, CTE的query中支持DML语句,例如 create table t1 (c1 int, c2 char(10)); insert into t1 values(1,'a'),(2,'b'); select * from t1; c1 | c2 ----+---- 1 | a 2 | b WITH cte AS ( UPDATE t1 SET c1= c1 * 2 where c1=1 RETURNING * ) SELECT * FROM cte; //返回更新的值 c1 | c2 ----+------------ 2 | a truncate table t1; insert into t1 values(1,'a'),(2,'b'); WITH cte AS ( UPDATE t1 SET c1= c1 * 2 where c1=1 RETURNING * ) SELECT * FROM t1;//返回原值 c1 | c2 ----+------------ 1 | a 2 | b truncate table t1; insert into t1 values(1,'a'),(2,'b'); WITH cte AS ( DELETE FROM t1 WHERE c1=1 RETURNING * ) SELECT * FROM cte;//返回删除的行 c1 | c2 ----+------------ 1 | a truncate table t1; insert into t1 values(1,'a'),(2,'b'); WITH cte AS ( DELETE FROM t1 WHERE c1=1 RETURNING * ) SELECT * FROM t1;//返回原值 c1 | c2 ----+------------ 1 | a 2 | b (2 rows) MariaDB MariaDB从10.2开始支持CTE。10.2.1 支持non-recursive CTE, 10.2.2开始支持recursive CTE。 目前的GA的版本是10.1. MySQL MySQL从8.0开始支持完整的CTE。MySQL8.0还在development阶段,RC都没有,GA还需时日。 AliSQL AliSQL port了mariadb10.2 no-recursive CTE的实现,此功能近期会上线。 以下从源码主要相关函数简要介绍其实现, //解析识别with table引用find_table_def_in_with_clauses //检查依赖关系,比如不能重复定义with table名字With_clause::check_dependencies // 为每个引用clone一份定义With_element::clone_parsed_spec //替换with table指定的列名With_element::rename_columns_of_derived_unit 此实现对于多次引用CTE,CTE会解析多次,因此此版本CTE有简化SQL的作用,但效率上没有效提高。 select count(*) from t1 where c2 !='z'; +----------+ | count(*) | +----------+ | 65536 | +----------+ 1 row in set (0.25 sec) //从执行时间来看是进行了3次全表扫描 with t as (select count(*) from t1 where c2 !='z') select * from t union select * from t union select * from t; +----------+ | count(*) | +----------+ | 65536 | +----------+ 1 row in set (0.59 sec) select count(*) from t1 where c2 !='z' union select count(*) from t1 where c2 !='z' union select count(*) from t1 where c2 !='z'; +----------+ | count(*) | +----------+ | 65536 | +----------+ 1 row in set (0.57 sec) explain with t as (select count(*) from t1 where c2 !='z') -> select * from t union select * from t union select * from t; +------+-----------------+--------------+------+---------------+------+---------+------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-----------------+--------------+------+---------------+------+---------+------+-------+-------------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 65536 | | | 2 | SUBQUERY | t1 | ALL | NULL | NULL | NULL | NULL | 65536 | Using where | | 3 | RECURSIVE UNION | <derived5> | ALL | NULL | NULL | NULL | NULL | 65536 | | | 5 | SUBQUERY | t1 | ALL | NULL | NULL | NULL | NULL | 65536 | Using where | | 4 | RECURSIVE UNION | <derived6> | ALL | NULL | NULL | NULL | NULL | 65536 | | | 6 | SUBQUERY | t1 | ALL | NULL | NULL | NULL | NULL | 65536 | Using where | | NULL | UNION RESULT | <union1,3,4> | ALL | NULL | NULL | NULL | NULL | NULL | | +------+-----------------+--------------+------+---------------+------+---------+------+-------+-------------+ 7 rows in set (0.00 sec) explain select count(*) from t1 where c2 !='z' union select count(*) from t1 where c2 !='z' union select count(*) from t1 where c2 !='z'; +------+--------------+--------------+------+---------------+------+---------+------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+--------------+--------------+------+---------------+------+---------+------+-------+-------------+ | 1 | PRIMARY | t1 | ALL | NULL | NULL | NULL | NULL | 65536 | Using where | | 2 | UNION | t1 | ALL | NULL | NULL | NULL | NULL | 65536 | Using where | | 3 | UNION | t1 | ALL | NULL | NULL | NULL | NULL | 65536 | Using where | | NULL | UNION RESULT | <union1,2,3> | ALL | NULL | NULL | NULL | NULL | NULL | | +------+--------------+--------------+------+---------------+------+---------+------+-------+-------------+ 4 rows in set (0.00 sec) 以下是MySQL8.0 只扫描一次的执行计划 mysql> explain select count(*) from t1 where c2 !='z' union select count(*) from t1 where c2 !='z' union select count(*) from t1 where c2 !='z'; +----+--------------+--------------+------------+------+---------------+------+---------+------+-------+----------+-----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+--------------+--------------+------------+------+---------------+------+---------+------+-------+----------+-----------------+ | 1 | PRIMARY | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 62836 | 90.00 | Using where | | 2 | UNION | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 62836 | 90.00 | Using where | | 3 | UNION | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 62836 | 90.00 | Using where | | NULL | UNION RESULT | <union1,2,3> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary | +----+--------------+--------------+------------+------+---------------+------+---------+------+-------+----------+-----------------+ 4 rows in set, 1 warning (0.00 sec) 以下是PostgreSQL9.4 只扫描一次的执行计划 postgres=# explain with t as (select count(*) from t1 where c2 !='z') postgres-# select * from t union select * from t union select * from t; HashAggregate (cost=391366.28..391366.31 rows=3 width=8) Group Key: t.count CTE t -> Aggregate (cost=391366.17..391366.18 rows=1 width=0) -> Seq Scan on t1 (cost=0.00..384392.81 rows=2789345 width=0) Filter: ((c2)::text <> 'z'::text) -> Append (cost=0.00..0.09 rows=3 width=8) -> CTE Scan on t (cost=0.00..0.02 rows=1 width=8) -> CTE Scan on t t_1 (cost=0.00..0.02 rows=1 width=8) -> CTE Scan on t t_2 (cost=0.00..0.02 rows=1 width=8) AliSQL还在不断改进中,AliSQL的CTE即将推出,敬请期待!
注:2013年的老文章 Prepare的作用 Prepare SQL产生的原因。首先从mysql服务器执行sql的过程开始讲起,SQL执行过程包括以下阶段 词法分析->语法分析->语义分析->执行计划优化->执行。词法分析->语法分析这两个阶段我们称之为硬解析。词法分析识别sql中每个词,语法分析解析SQL语句是否符合sql语法,并得到一棵语法树(Lex)。对于只是参数不同,其他均相同的sql,它们执行时间不同但硬解析的时间是相同的。而同一SQL随着查询数据的变化,多次查询执行时间可能不同,但硬解析的时间是不变的。对于sql执行时间较短,sql硬解析的时间占总执行时间的比率越高。而对于淘宝应用的绝大多数事务型SQL,查询都会走索引,执行时间都比较短。因此淘宝应用db sql硬解析占的比重较大。 Prepare的出现就是为了优化硬解析的问题。Prepare在服务器端的执行过程如下 1) Prepare 接收客户端带”?”的sql, 硬解析得到语法树(stmt->Lex), 缓存在线程所在的preparestatement cache中。此cache是一个HASH MAP. Key为stmt->id. 然后返回客户端stmt->id等信息。 2) Execute 接收客户端stmt->id和参数等信息。注意这里客户端不需要再发sql过来。服务器根据stmt->id在preparestatement cache中查找得到硬解析后的stmt, 并设置参数,就可以继续后面的优化和执行了。 Prepare在execute阶段可以节省硬解析的时间。如果sql只执行一次,且以prepare的方式执行,那么sql执行需两次与服务器交互(Prepare和execute), 而以普通(非prepare)方式,只需要一次交互。这样使用prepare带来额外的网络开销,可能得不偿失。我们再来看同一sql执行多次的情况,比如以prepare方式执行10次,那么只需要一次硬解析。这时候 额外的网络开销就显得微乎其微了。因此prepare适用于频繁执行的SQL。 Prepare的另一个作用是防止sql注入,不过这个是在客户端jdbc通过转义实现的,跟服务器没有关系。 硬解析的比重 交易买家库 tcbyer压测时,通过perf 得到的结果。 硬解析相关的函数比重都比较靠前(MYSQLparse 4.93%, lex_one_token 1.79%, lex_start 1.12%)总共接近8%。因此,服务器使用prepare是可以带来较多的性能提升的。 jdbc与prepare jdbc服务器端的参数: useServerPrepStmts:默认为false. 是否使用服务器prepare开关 jdbc客户端参数: cachePrepStmts:默认false.是否缓存prepareStatement对象。每个连接都有一个缓存,是以sql为唯一标识的LRU cache. 同一连接下,不同stmt可以不用重新创建prepareStatement对象。 prepStmtCacheSize:LRU cache中prepareStatement对象的个数。一般设置为最常用sql的个数。 prepStmtCacheSqlLimit:prepareStatement对象的大小。超出大小不缓存。 Jdbc对prepare的处理过程: useServerPrepStmts=true时Jdbc对prepare的处理 1) 创建PreparedStatement对象,向服务器发送COM_PREPARE命令,并传送带问号的sql. 服务器返回jdbc stmt->id等信息 2) 向服务器发送COM_EXECUTE命令,并传送参数信息。 useServerPrepStmts=false时Jdbc对prepare的处理 1) 创建PreparedStatement对象,此时不会和服务器交互。 2) 根据参数和PreparedStatement对象拼接完整的SQL,向服务器发送QUERY命令 我们再看参数cachePrepStmts打开时在useServerPrepStmts为true或false时,均缓存PreparedStatement对象。只不过useServerPrepStmts为的true缓存PreparedStatement对象包含服务器的stmt->id等信息,也就是说如果重用了PreparedStatement对象,那么就省去了和服务器通讯(COM_PREPARE命令)的开销。而useServerPrepStmts=false是,开启cachePrepStmts缓存PreparedStatement对象只是简单的sql解析信息,因此此时开启cachePrepStmts意义不是太大。 我们来开看一段java代码 1 2 3 4 5 6 7 8 9 10 11 Connection con = null; PreparedStatement ps = null; String sql = "select * from user where id=?"; ps = con.prepareStatement(sql); ps.setInt(1, 1); ps.executeQuery(); ps.close(); ps = con.prepareStatement(sql); ps.setInt(1, 3); ps.executeQuery(); ps.close(); 这段代码在同一会话中两次prepare执行同一语句,并且之间有ps.close(); useServerPrepStmts=false时,服务器会两次硬解析同一SQL。 useServerPrepStmts=true, cachePrepStmts=false时服务器仍然会两次硬解析同一SQL。 useServerPrepStmts=true, cachePrepStmts=true时服务器只会硬解析一次SQL。 如果两次prepare之间没有ps.close();那么cachePrepStmts=true,cachePrepStmts=false也只需一次硬解析. 因此,客户端对同一sql,频繁分配和释放PreparedStatement对象的情况下,开启cachePrepStmts参数是很有必要的。 测试 1)做了一个简单的测试,主要测试prepare的效果和useServerPrepStmts参数的影响. 1 <span style="font-family: 宋体, SimSun; font-size: 14px;"> cnt = 5000;<br> // no prepare<br> String sql = "select biz_order_id,out_order_id,seller_nick,buyer_nick,seller_id,buyer_id,auction_id,auction_title,auction_price,buy_amount,biz_type,sub_biz_type,fail_reason,pay_status,logistics_status,out_trade_status,snap_path,gmt_create,status,ifnull(buyer_rate_status, 4) buyer_rate_status from tc_biz_order_0030 where " +<br> "parent_id = 594314511722841 or parent_id =547667559932641;";<br><br> begin = new Date();<br> System.out.println("begin:" + df.format(begin));<br> <br> stmt = con.createStatement();<br> for (int i = 0; i < cnt; i++)<br> { <br> stmt.executeQuery(sql);<br> } <br> <br> end = new Date();<br> System.out.println("end:" + df.format(end));<br> <br> long temp = end.getTime() - begin.getTime();<br> System.out.println("no perpare interval:" + temp);<br> <br> <br> // test prepare <br> sql = "select biz_order_id,out_order_id,seller_nick,buyer_nick,seller_id,buyer_id,auction_id,auction_title,auction_price,buy_amount,biz_type,sub_biz_type,fail_reason,pay_status,logistics_status,out_trade_status,snap_path,gmt_create,status,ifnull(buyer_rate_status, 4) buyer_rate_status from tc_biz_order_0030 where " +<br> "parent_id = 594314511722841 or parent_id =?;";<br> ps = con.prepareStatement(sql);<br> BigInteger param = new BigInteger("547667559932641");<br> <br> begin = new Date();<br> System.out.println("begin:" + df.format(begin));<br> <br> for (int i = 0; i < cnt; i++)<br> {<br> ps.setObject(1, param);<br> ps.executeQuery(); <br> } <br> <br> end = new Date();<br> System.out.println("end:" + df.format(end));<br> <br> temp = end.getTime() - begin.getTime();<br> System.out.println("prepare interval:" + temp);<br></span> 经多次采样测试结果如下: 非prepare和prepare时间比 useServerPrepStmts=true 0.93 useServerPrepStmts=false 1.01 结论: useServerPrepStmts=true时,prepare提升7%; useServerPrepStmts=false时,prepare与非prepare性能相当。 如果将语句简化为select * from tc_biz_order_0030 where parent_id =?。那么测试的结论useServerPrepStmts=true时,prepare仅提升2%;sql越简单硬解析的时间就越少,prepare的提升就越少。 注意:这个测试是在单个连接,单条sql的理想情况下进行的,线上会出现多连接多sql,还有sql执行频率,sql的复杂程度等不同,因此prepare的提升效果会随具体环境而变化。 2)prepare 前后的perf top 对比 以下为非prepare 6.46% mysqld mysqld [.] _Z10MYSQLparsePv 3.74% mysqld libc-2.12.so [.] __memcpy_ssse3 2.50% mysqld mysqld [.] my_hash_sort_utf8 2.15% mysqld mysqld [.] cmp_dtuple_rec_with_match 2.05% mysqld mysqld [.] _ZL13lex_one_tokenPvS_ 1.46% mysqld mysqld [.] buf_page_get_gen 1.34% mysqld mysqld [.] page_cur_search_with_match 1.31% mysqld mysqld [.] _ZL14build_templateP19row_prebuilt_structP3THDP5TABLEj 1.24% mysqld mysqld [.] rec_init_offsets 1.11% mysqld libjemalloc.so.1 [.] free 1.09% mysqld mysqld [.] rec_get_offsets_func 1.01% mysqld libjemalloc.so.1 [.] malloc 0.96% mysqld libc-2.12.so [.] __strlen_sse42 0.93% mysqld mysqld [.] _ZN4JOIN8optimizeEv 0.91% mysqld mysqld [.] _ZL15get_hash_symbolPKcjb 0.88% mysqld mysqld [.] row_search_for_mysql 0.86% mysqld [kernel.kallsyms] [k] tcp_recvmsg 以下为perpare 3.46% mysqld libc-2.12.so [.] __memcpy_ssse3 2.32% mysqld mysqld [.] cmp_dtuple_rec_with_match 2.14% mysqld mysqld [.] _ZL14build_templateP19row_prebuilt_structP3THDP5TABLEj 1.96% mysqld mysqld [.] buf_page_get_gen 1.66% mysqld mysqld [.] page_cur_search_with_match 1.54% mysqld mysqld [.] row_search_for_mysql 1.44% mysqld mysqld [.] btr_cur_search_to_nth_level 1.41% mysqld libjemalloc.so.1 [.] free 1.35% mysqld mysqld [.] rec_init_offsets 1.32% mysqld [kernel.kallsyms] [k] kfree 1.14% mysqld libjemalloc.so.1 [.] malloc 1.08% mysqld [kernel.kallsyms] [k] fget_light 1.05% mysqld mysqld [.] rec_get_offsets_func 0.99% mysqld mysqld [.] _ZN8Protocol24send_result_set_metadataEP4ListI4ItemEj 0.90% mysqld mysqld [.] sync_array_print_long_waits 0.87% mysqld mysqld [.] page_rec_get_n_recs_before 0.81% mysqld mysqld [.] _ZN4JOIN8optimizeEv 0.81% mysqld libc-2.12.so [.] __strlen_sse42 0.78% mysqld mysqld [.] _ZL20make_join_statisticsP4JOINP10TABLE_LISTP4ItemP16st_dynamic_array 0.72% mysqld [kernel.kallsyms] [k] tcp_recvmsg 0.63% mysqld libpthread-2.12.so [.] __pthread_getspecific_internal 0.63% mysqld [kernel.kallsyms] [k] sk_run_filter 0.60% mysqld mysqld [.] _Z19find_field_in_tableP3THDP5TABLEPKcjbPj 0.60% mysqld mysqld [.] page_check_dir 0.57% mysqld mysqld [.] _Z16dispatch_command19enum_server_commandP3THDP 对比可以发现 MYSQLparse lex_one_token在prepare时已优化掉了。 思考 1 开启cachePrepStmts的问题,前面谈到每个连接都有一个缓存,是以sql为唯一标识的LRU cache. 在分表较多,大连接的情况下,可能会个应用服务器带来内存问题。这里有个前提是ibatis是默认使用prepare的。 在mybatis中,标签statementType可以指定某个sql是否是使用prepare. statementType Any one of STATEMENT, PREPARED or CALLABLE. This causes MyBatis to use Statement, PreparedStatement orCallableStatement respectively. Default: PREPARED. 这样可以精确控制只对频率较高的sql使用prepare,从而控制使用prepare sql的个数,减少内存消耗。遗憾的是目前集团貌似大多使用的是ibatis 2.0版本,不支持statementType 标签。 2 服务器端prepare cache是一个HASH MAP. Key为stmt->id,同时也是每个连接都维护一个。因此也有可能出现内存问题,待实际测试。如有必要需改造成Key为sql的全局cache,这样不同连接的相同prepare sql可以共享。 3 oracle prepare与mysql prepare的区别: mysql与oracle有一个重大区别是mysql没有oracle那样的执行计划缓存。前面我们讲到SQL执行过程包括以下阶段 词法分析->语法分析->语义分析->执行计划优化->执行。oracle的prepare实际上包括以下阶段:词法分析->语法分析->语义分析->执行计划优化,也就是说oracle的prepare做了更多的事情,execute只需要执行即可。因此,oracle的prepare比mysql更高效。
title: MySQL · myrocks · myrocks监控信息 author: 张远 rocksdb本身提供了丰富的监控信息,myrocks通过information_schema下的表和show命令等将这些信息展示出来,下面主要以示例的形式来简单介绍下 先创建测试表 CREATE TABLE t1 (a INT, b CHAR(8), pk INT AUTO_INCREMENT ,PRIMARY KEY(pk) comment 'cf_1', key idx2(b) comment 'cf_2') engine=rocksdb; SHOW STATUS show status 也展示了部分rocksdb引擎的信息 show status like '%rock%'; +---------------------------------------+------------------------------------------+ | Variable_name | Value | +---------------------------------------+------------------------------------------+ | rocksdb_rows_deleted | 0 | | rocksdb_rows_inserted | 1048579 | | rocksdb_rows_read | 3145755 | | rocksdb_rows_updated | 7 | | rocksdb_system_rows_deleted | 0 | | rocksdb_system_rows_inserted | 0 | | rocksdb_system_rows_read | 0 | | rocksdb_system_rows_updated | 0 | | rocksdb_block_cache_add | 16 | | rocksdb_block_cache_data_hit | 76 | | rocksdb_block_cache_data_miss | 6 | | rocksdb_block_cache_filter_hit | 0 | | rocksdb_block_cache_filter_miss | 6 | | rocksdb_block_cache_hit | 76 | | rocksdb_block_cache_index_hit | 0 | | rocksdb_block_cache_index_miss | 6 | | rocksdb_block_cache_miss | 18 | | rocksdb_block_cachecompressed_hit | 0 | | rocksdb_block_cachecompressed_miss | 0 | | rocksdb_bloom_filter_prefix_checked | 0 | | rocksdb_bloom_filter_prefix_useful | 0 | | rocksdb_bloom_filter_useful | 0 | | rocksdb_bytes_read | 13631762 | | rocksdb_bytes_written | 108009584 | | rocksdb_compact_read_bytes | 142 | | rocksdb_compact_write_bytes | 0 | | rocksdb_compaction_key_drop_new | 0 | | rocksdb_compaction_key_drop_obsolete | 4 | | rocksdb_compaction_key_drop_user | 4 | | rocksdb_flush_write_bytes | 7211 | | rocksdb_getupdatessince_calls | 0 | | rocksdb_git_date | %cI | | rocksdb_git_hash | bc5d7b70299b763127f3714055a63ebe7e04ad47 | | rocksdb_l0_num_files_stall_micros | 0 | | rocksdb_l0_slowdown_micros | 0 | | rocksdb_memtable_compaction_micros | 0 | | rocksdb_memtable_hit | 1048593 | | rocksdb_memtable_miss | 1048609 | | rocksdb_no_file_closes | 0 | | rocksdb_no_file_errors | 0 | | rocksdb_no_file_opens | 6 | | rocksdb_num_iterators | 0 | | rocksdb_number_block_not_compressed | 0 | | rocksdb_number_deletes_filtered | 0 | | rocksdb_number_keys_read | 2097202 | | rocksdb_number_keys_updated | 0 | | rocksdb_number_keys_written | 2097220 | | rocksdb_number_merge_failures | 0 | | rocksdb_number_multiget_bytes_read | 0 | | rocksdb_number_multiget_get | 0 | | rocksdb_number_multiget_keys_read | 0 | | rocksdb_number_reseeks_iteration | 0 | | rocksdb_number_sst_entry_delete | 12 | | rocksdb_number_sst_entry_merge | 0 | | rocksdb_number_sst_entry_other | 0 | | rocksdb_number_sst_entry_put | 30 | | rocksdb_number_sst_entry_singledelete | 0 | | rocksdb_number_stat_computes | 0 | | rocksdb_number_superversion_acquires | 21 | | rocksdb_number_superversion_cleanups | 1 | | rocksdb_number_superversion_releases | 1 | | rocksdb_rate_limit_delay_millis | 0 | | rocksdb_snapshot_conflict_errors | 0 | | rocksdb_wal_bytes | 54006676 | | rocksdb_wal_group_syncs | 0 | | rocksdb_wal_synced | 13 | | rocksdb_write_other | 0 | | rocksdb_write_self | 58 | | rocksdb_write_timedout | 0 | | rocksdb_write_wal | 58 | +---------------------------------------+------------------------------------------+ INFORMATION_SCHMEA information_schema下rocksdb相关的表如下 select table_name from INFORMATION_SCHEMA.tables where table_name like '%rock%'; +-----------------------------+ | table_name | +-----------------------------+ | ROCKSDB_PERF_CONTEXT | | ROCKSDB_GLOBAL_INFO | | ROCKSDB_COMPACTION_STATS | | ROCKSDB_INDEX_FILE_MAP | | ROCKSDB_CF_OPTIONS | | ROCKSDB_PERF_CONTEXT_GLOBAL | | ROCKSDB_CFSTATS | | ROCKSDB_TRX | | ROCKSDB_DBSTATS | | ROCKSDB_DDL | | ROCKSDB_LOCKS | +-----------------------------+ 数据字典相关 show create table INFORMATION_SCHEMA.ROCKSDB_INDEX_FILE_MAP\G *************************** 1. row *************************** Table: ROCKSDB_INDEX_FILE_MAP Create Table: CREATE TEMPORARY TABLE `ROCKSDB_INDEX_FILE_MAP` ( `COLUMN_FAMILY` int(4) NOT NULL DEFAULT '0', `INDEX_NUMBER` int(4) NOT NULL DEFAULT '0', `SST_NAME` varchar(193) NOT NULL DEFAULT '', `NUM_ROWS` bigint(8) NOT NULL DEFAULT '0', `DATA_SIZE` bigint(8) NOT NULL DEFAULT '0', `ENTRY_DELETES` bigint(8) NOT NULL DEFAULT '0', `ENTRY_SINGLEDELETES` bigint(8) NOT NULL DEFAULT '0', `ENTRY_MERGES` bigint(8) NOT NULL DEFAULT '0', `ENTRY_OTHERS` bigint(8) NOT NULL DEFAULT '0' ) ENGINE=MEMORY DEFAULT CHARSET=utf8 1 row in set (0.00 sec) mysql> show create table INFORMATION_SCHEMA.ROCKSDB_DDL\G *************************** 1. row *************************** Table: ROCKSDB_DDL Create Table: CREATE TEMPORARY TABLE `ROCKSDB_DDL` ( `TABLE_SCHEMA` varchar(193) NOT NULL DEFAULT '', `TABLE_NAME` varchar(193) NOT NULL DEFAULT '', `PARTITION_NAME` varchar(193) DEFAULT NULL, `INDEX_NAME` varchar(193) NOT NULL DEFAULT '', `COLUMN_FAMILY` int(4) NOT NULL DEFAULT '0', `INDEX_NUMBER` int(4) NOT NULL DEFAULT '0', `INDEX_TYPE` smallint(2) NOT NULL DEFAULT '0', `KV_FORMAT_VERSION` smallint(2) NOT NULL DEFAULT '0', `CF` varchar(193) NOT NULL DEFAULT '' ) ENGINE=MEMORY DEFAULT CHARSET=utf8 例如查询t1表的数据字典信息 select d.*,i.* from INFORMATION_SCHEMA.ROCKSDB_INDEX_FILE_MAP i,INFORMATION_SCHEMA.ROCKSDB_DDL d where i.INDEX_NUMBER=d.INDEX_NUMBER\G *************************** 1. row *************************** TABLE_SCHEMA: test TABLE_NAME: t1 PARTITION_NAME: NULL INDEX_NAME: PRIMARY COLUMN_FAMILY: 2 INDEX_NUMBER: 263 INDEX_TYPE: 1 KV_FORMAT_VERSION: 11 CF: cf_1 COLUMN_FAMILY: 2 INDEX_NUMBER: 263 SST_NAME: 000039.sst NUM_ROWS: 2 DATA_SIZE: 42 ENTRY_DELETES: 0 ENTRY_SINGLEDELETES: 0 ENTRY_MERGES: 0 ENTRY_OTHERS: 0 *************************** 2. row *************************** TABLE_SCHEMA: test TABLE_NAME: t1 PARTITION_NAME: NULL INDEX_NAME: idx2 COLUMN_FAMILY: 3 INDEX_NUMBER: 264 INDEX_TYPE: 2 KV_FORMAT_VERSION: 11 CF: cf_2 COLUMN_FAMILY: 3 INDEX_NUMBER: 264 SST_NAME: 000040.sst NUM_ROWS: 2 DATA_SIZE: 45 ENTRY_DELETES: 0 ENTRY_SINGLEDELETES: 0 ENTRY_MERGES: 0 ENTRY_OTHERS: 0 2 rows in set (0.00 sec) 事务相关 begin; select * from INFORMATION_SCHEMA.ROCKSDB_LOCKS; INSERT INTO t1 (a,b) VALUES (1,'a'); select * from INFORMATION_SCHEMA.ROCKSDB_LOCKS; +------------------+----------------+------------------+------+ | COLUMN_FAMILY_ID | TRANSACTION_ID | KEY | MODE | +------------------+----------------+------------------+------+ | 2 | 14 | 0000010780000001 | X | +------------------+----------------+------------------+------+ select * from INFORMATION_SCHEMA.ROCKSDB_TRX\G *************************** 1. row *************************** TRANSACTION_ID: 89 STATE: STARTED NAME: WRITE_COUNT: 2 LOCK_COUNT: 2 TIMEOUT_SEC: 2 WAITING_KEY: WAITING_COLUMN_FAMILY_ID: 0 IS_REPLICATION: 0 SKIP_TRX_API: 0 READ_ONLY: 0 HAS_DEADLOCK_DETECTION: 0 NUM_ONGOING_BULKLOAD: 0 THREAD_ID: 13 QUERY: select * from INFORMATION_SCHEMA.ROCKSDB_TRX 其中KEY 0000010780100002表示indexnum:107(263)pk: 80000001(1) 表示(1,'a',1)这条记录,具体参考myrocks记录格式分析 统计信息相关 select * from INFORMATION_SCHEMA.ROCKSDB_GLOBAL_INFO; +--------------+--------------+-----------------------------------------+ | TYPE | NAME | VALUE | +--------------+--------------+-----------------------------------------+ | BINLOG | FILE | mysql-bin.000003 | | BINLOG | POS | 18957545 | | BINLOG | GTID | b89fb268-0b22-11e7-a0ce-2c44fd7a5210:27 | | MAX_INDEX_ID | MAX_INDEX_ID | 264 | | CF_FLAGS | 0 | default [0] | | CF_FLAGS | 1 | __system__ [0] | | CF_FLAGS | 2 | cf_1 [0] | | CF_FLAGS | 3 | cf_2 [0] | +--------------+--------------+-----------------------------------------+ select * from INFORMATION_SCHEMA.ROCKSDB_DBSTATS; +-------------------------+-------+ | STAT_TYPE | VALUE | +-------------------------+-------+ | DB_BACKGROUND_ERRORS | 0 | | DB_NUM_SNAPSHOTS | 0 | | DB_OLDEST_SNAPSHOT_TIME | 0 | | DB_BLOCK_CACHE_USAGE | 1119 | +-------------------------+-------+ select * from INFORMATION_SCHEMA.ROCKSDB_CFSTATS where CF_NAME='cf_1'; +---------+-------------------------------+----------+ | CF_NAME | STAT_TYPE | VALUE | +---------+-------------------------------+----------+ | cf_1 | NUM_IMMUTABLE_MEM_TABLE | 0 | | cf_1 | MEM_TABLE_FLUSH_PENDING | 0 | | cf_1 | COMPACTION_PENDING | 0 | | cf_1 | CUR_SIZE_ACTIVE_MEM_TABLE | 44739520 | | cf_1 | CUR_SIZE_ALL_MEM_TABLES | 44739520 | | cf_1 | NUM_ENTRIES_ACTIVE_MEM_TABLE | 1048574 | | cf_1 | NUM_ENTRIES_IMM_MEM_TABLES | 0 | | cf_1 | NON_BLOCK_CACHE_SST_MEM_USAGE | 0 | | cf_1 | NUM_LIVE_VERSIONS | 1 | +---------+-------------------------------+----------+ 性能相关 INFORMATION_SCHEMA.ROCKSDB_PERF_CONTEXT_GLOBAL 是全局的性能信息,而INFORMATION_SCHEMA.ROCKSDB_PERF_CONTEXT是以表为单位的性能信息。 性能统计由参数rocksdb_perf_context_level控制,取值范围如下 enum PerfLevel : unsigned char { kUninitialized = 0, // unknown setting kDisable = 1, // disable perf stats kEnableCount = 2, // enable only count stats kEnableTimeExceptForMutex = 3, // Other than count stats, also enable time // stats except for mutexes kEnableTime = 4, // enable count and time stats kOutOfBounds = 5 // N.B. Must always be the last value! }; 示例如下: select * from INFORMATION_SCHEMA.ROCKSDB_PERF_CONTEXT_GLOBAL; +---------------------------------+-------------+ | STAT_TYPE | VALUE | +---------------------------------+-------------+ | USER_KEY_COMPARISON_COUNT | 565061179 | | BLOCK_CACHE_HIT_COUNT | 26 | | BLOCK_READ_COUNT | 2 | | BLOCK_READ_BYTE | 145 | | BLOCK_READ_TIME | 684522 | | BLOCK_CHECKSUM_TIME | 8380 | | BLOCK_DECOMPRESS_TIME | 10825 | | INTERNAL_KEY_SKIPPED_COUNT | 3371079 | | INTERNAL_DELETE_SKIPPED_COUNT | 0 | | GET_SNAPSHOT_TIME | 2409821566 | | GET_FROM_MEMTABLE_TIME | 68354733245 | | GET_FROM_MEMTABLE_COUNT | 4194309 | | GET_POST_PROCESS_TIME | 3421224444 | | GET_FROM_OUTPUT_FILES_TIME | 8016972510 | | SEEK_ON_MEMTABLE_TIME | 277621 | | SEEK_ON_MEMTABLE_COUNT | 33 | | SEEK_CHILD_SEEK_TIME | 1700582 | | SEEK_CHILD_SEEK_COUNT | 54 | | SEEK_IN_HEAP_TIME | 101201 | | SEEK_INTERNAL_SEEK_TIME | 2019275 | | FIND_NEXT_USER_ENTRY_TIME | 3997301676 | | WRITE_WAL_TIME | 410899041 | | WRITE_MEMTABLE_TIME | 23580852751 | | WRITE_DELAY_TIME | 0 | | WRITE_PRE_AND_POST_PROCESS_TIME | 1117611 | | DB_MUTEX_LOCK_NANOS | 237804 | | DB_CONDITION_WAIT_NANOS | 0 | | MERGE_OPERATOR_TIME_NANOS | 0 | | READ_INDEX_BLOCK_NANOS | 0 | | READ_FILTER_BLOCK_NANOS | 0 | | NEW_TABLE_BLOCK_ITER_NANOS | 1109437 | | NEW_TABLE_ITERATOR_NANOS | 308214 | | BLOCK_SEEK_NANOS | 1290004508 | | FIND_TABLE_NANOS | 0 | | IO_THREAD_POOL_ID | 102 | | IO_BYTES_WRITTEN | 54016973 | | IO_BYTES_READ | 145 | | IO_OPEN_NANOS | 0 | | IO_ALLOCATE_NANOS | 0 | | IO_WRITE_NANOS | 116163102 | | IO_READ_NANOS | 664547 | | IO_RANGE_SYNC_NANOS | 0 | | IO_LOGGER_NANOS | 0 | +---------------------------------+-------------+ select * from INFORMATION_SCHEMA.ROCKSDB_PERF_CONTEXT where table_name='t1' limit 1; +--------------+------------+----------------+---------------------------+-----------+ | TABLE_SCHEMA | TABLE_NAME | PARTITION_NAME | STAT_TYPE | VALUE | +--------------+------------+----------------+---------------------------+-----------+ | test | t1 | NULL | USER_KEY_COMPARISON_COUNT | 565060904 | +--------------+------------+----------------+---------------------------+-----------+ COMPACTION相关 select * from INFORMATION_SCHEMA.ROCKSDB_COMPACTION_STATS where CF_NAME='cf_1' limit 3; +---------+-------+-----------+-------+ | CF_NAME | LEVEL | TYPE | VALUE | +---------+-------+-----------+-------+ | cf_1 | L0 | AvgSec | 0 | | cf_1 | L0 | CompCount | 2 | | cf_1 | L0 | CompSec | 0 | +---------+-------+-----------+-------+ 具体可以参考下节SHOW ENGINE ROCKSDB STATUS。 参数配置 每个column family 都是独立的配置信息 select * from INFORMATION_SCHEMA.ROCKSDB_CF_OPTIONS where CF_NAME='cf_1' limit 3; +---------+-------------------+------------------+ | CF_NAME | OPTION_TYPE | VALUE | +---------+-------------------+------------------+ | cf_1 | COMPARATOR | RocksDB_SE_v3.10 | | cf_1 | MERGE_OPERATOR | NULL | | cf_1 | COMPACTION_FILTER | NULL | +---------+-------------------+------------------+ SHOW ENGINE ROCKSDB STATUS show engine rocksdb status.结果主要分为三部分1)DB Stats2)Compaction Stats 3)Memory_Stats show engine rocksdb statusG结果节选 show engine rocksdb status\G *************************** 1. row *************************** Type: DBSTATS Name: rocksdb Status: ** DB Stats ** Uptime(secs): 211548.0 total, 8140.1 interval Cumulative writes: 58 writes, 2097K keys, 58 commit groups, 1.0 writes per commit group, ingest: 0.10 GB, 0.00 MB/s Cumulative WAL: 58 writes, 13 syncs, 4.14 writes per sync, written: 0.05 GB, 0.00 MB/s Cumulative stall: 00:00:0.000 H:M:S, 0.0 percent Interval writes: 0 writes, 0 keys, 0 commit groups, 0.0 writes per commit group, ingest: 0.00 MB, 0.00 MB/s Interval WAL: 0 writes, 0 syncs, 0.00 writes per sync, written: 0.00 MB, 0.00 MB/s Interval stall: 00:00:0.000 H:M:S, 0.0 percent ......(省略) *************************** 3. row *************************** Type: CF_COMPACTION Name: cf_1 Status: ** Compaction Stats [cf_1] ** Level Files Size(MB} Score Read(GB} Rn(GB} Rnp1(GB} Write(GB} Wnew(GB} Moved(GB} W-Amp Rd(MB/s} Wr(MB/s} Comp(sec} Comp(cnt} Avg(sec} KeyIn KeyDrop ---------------------------------------------------------------------------------------------------------------------------------------------------------- L0 1/0 0.00 0.2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.3 0 2 0.004 0 0 Sum 1/0 0.00 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.1 0.2 0 3 0.004 2 2 Int 0/0 0.00 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0 0 0.000 0 0 Uptime(secs): 210665.0 total, 210665.0 interval Flush(GB): cumulative 0.000, interval 0.000 AddFile(GB): cumulative 0.000, interval 0.000 AddFile(Total Files): cumulative 0, interval 0 AddFile(L0 Files): cumulative 0, interval 0 AddFile(Keys): cumulative 0, interval 0 Cumulative compaction: 0.00 GB write, 0.00 MB/s write, 0.00 GB read, 0.00 MB/s read, 0.0 seconds Interval compaction: 0.00 GB write, 0.00 MB/s write, 0.00 GB read, 0.00 MB/s read, 0.0 seconds Stalls(count): 0 level0_slowdown, 0 level0_slowdown_with_compaction, 0 level0_numfiles, 0 level0_numfiles_with_compaction, 0 stop for pending_compaction_bytes, 0 slowdown for pending_compaction_bytes, 0 memtable_compaction, 0 memtable_slowdown, interval 0 total count ......(省略) *************************** 6. row *************************** Type: Memory_Stats Name: rocksdb Status: MemTable Total: 93675232 MemTable Unflushed: 93673184 Table Readers Total: 0 Cache Total: 1119 Default Cache Capacity: 0 6 rows in set (0.00 sec) DB Stats 其中:Interval stall: 此值受max_write_buffer_number,level0_slowdown_writes_trigger、soft_pending_compaction_bytes_limit等参数的影响, 具体参考(SetupDelay) Compaction Stats 其中 Rn(GB} = bytes_read_non_output_levels / kGB Rnp1(GB} = bytes_read_output_level / kGB W-Amp = bytes_written/bytes_read_non_output_levels 此部分内容与 INFORMATION_SCHEMA.ROCKSDB_COMPACTION_STATS有部分重合。 Memory_Stats MemTable Total: 对应DB::Properties::kSizeAllMemTables MemTable Unflushed:对应DB::Properties::kCurSizeAllMemTables Table Readers Total: 对应DB::Properties::kEstimateTableReadersMem Cache Total: 表示已使用的内存 Default Cache Capacity: 使用默认blockcache的总量(basetable没有指定blockcache时使用默认的8M的blockcache) SHOW ENGING ROKSDB TRANCTION STATUS 显示当前正在运行的事务语句 show engine rocksdb transaction status\G *************************** 1. row *************************** Type: SNAPSHOTS Name: rocksdb Status: ============================================================ 2017-03-20 07:49:22 ROCKSDB TRANSACTION MONITOR OUTPUT ============================================================ --------- SNAPSHOTS --------- LIST OF SNAPSHOTS FOR EACH SESSION: ---SNAPSHOT, ACTIVE 5 sec MySQL thread id 12, OS thread handle 0x7fd23a1d0700, query id 187 127.0.0.1 root Searching rows for update update t1 set b='cc' where a=2 lock count 72822, write count 2 ----------------------------------------- END OF ROCKSDB TRANSACTION MONITOR OUTPUT ========================================= 总结 以上粗略介绍了myrocks的监控信息,具体还需要在实践中灵活运用。myrocks的监控信息也在不断完善中。
title: MySQL · myrocks · myrocks之备份恢复 author: 张远 myrocks支持逻辑备份和物理备份,逻辑备份仍然采用mysqldump,物理备份采用自己开发的myrocks_hotbackup工具,传统的物理备份工具Xtrabackup不支持rocksdb。由于rocksdb的存储特性,myrocks不管是逻辑备份还是物理备份,与innodb的备份恢复均有较大差别。 逻辑备份 myrocks的mysqldump工具支持rocksdb的逻辑备份,其使用方式与原生的mysqldump备份innodb没有区别,一般的使用方式如下 mysqldump -uroot -h 127.0.0.1 -P 3306 --default-character-set=binary --single-transaction --master-data=2 --all-databases 虽然使用方式相同,但内部实现会用一些差别传统的mysqldump备份方式简化如下 加锁FTWL:FLUSH TABLE WITH READ LOCK 设置RR模式:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 开启一致读:START TRANSACTION WITH CONSISTENT SNAPSHOT 获取位点:SHOW MASTER STATUS 解锁:UNLOCK TABLES 依次导出数据select * from table myrocks的mysqldump备份方式简化如下 设置读取时不缓存到block cache:SET SESSION rocksdb_skip_fill_cache=1 设置RR模式:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 开启一致读:START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT 依次导出数据select * from table 可以看出myrocoks mysqldump导数据时,设置了engine层的优化rocksdb_skip_fill_cache。同时少了传统的FTWL的加锁和解锁操作,换了新的快照获取方式START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT, 此方式会加一些内存锁(LOCK_log等)同时返回位点信息,此方式比FTWL更高效。 mysql> START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT; +------------------+----------+------------------------------------------+ | File | Position | Gtid_executed | +------------------+----------+------------------------------------------+ | mysql-bin.000003 | 1010 | a3d923e4-f19b-11e6-ba57-2c44fd7a5210:1-4 | +------------------+----------+------------------------------------------+ 1 row in set (0.00 sec) myrocoks mysqldump不能同时备份innodb和rocksdb, 备份innodb时也采用新的START TRANSACTION WITH CONSISTENT INNODB SNAPSHOT方式。如果需支持同时备份innodb和rocksdb,需修改mysqldump采用老的START TRANSACTION WITH CONSISTENT SNAPSHOT方式,同时开启innodb和rocksdb的快照。 物理备份 myrocks 有专门的物理备份工具myrocks_hotbackup,此工具是一个python脚本,源码在scripts/myrocks_hotbackup,总共才600多行,整个备份逻辑比较简单。 分析myrocks_hotbackup之前,先介绍下myrocks的checkpoint快照功能执行以下语句会在目录/path/to/backup下创建一个一致性的数据快照 SET GLOBAL rocksdb_create_checkpoint = '/path/to/backup' 创建快照过程如下 禁止SST文件的删除操作 创建空目录/path/to/backup 在/path/to/backup下为每个SST文件的创建硬链接 copy MANIFEST和OPTIONS文件到/path/to/backup下 备份WAL文件,最后一个WAL文件通过copy方式,其他WAL文件通过硬链接备份到/path/to/backup下 允许SST文件的删除操作 Note: SST内容是不会变化的,从而能够以硬链接的方式备份文件,充分利用硬链接的特性。同时快照过程中禁止删除SST文件,从而保证MANIFEST文件的一致性。 checkpoint快照中SST文件占主要部分,SST通过hardlink方式建立,使得快照操作比较快,同时也节省了空间。 再来看看myrocks_hotbackup的备份过程 通过SET GLOBAL rocksdb_create_checkpoint=‘path_n’建立快照 依次备份快照中的文件,先备份SST文件再备份, 再备份WAL,MANIFEST和OPTIONS文件。备份SST过程会比较长,如果超过checkpoint_interval(由参数--interval指定)SST文件还没有备份完,就会清理当前快照,返回步骤1重新开始。 步骤1,2完成后,rocksdb相关的文件备份完成,清理最后一次checkpoint快照文件。步骤1,2可能重复执行多次。 开始备份mysql其他文件。比如其他数据库文件,test,mysql数据等,另外还有datadir下其他文件,但过滤掉这些文件'master.info', 'relay-log.info', 'worker-relay-log.info','auto.cnf', 'gaplock.log', 'ibdata', 'ib_logfile'。 checkpoint renewing 上面步骤1,2重复建立快照的过程称为checkpoint renewing,图片来自Facebook checkpoint renewing 过程中已经备份过的SST文件不会重复备份,只在最后一次checkpoint snapshot中备份WAL文件,MANIFEST和OPTIONS文件。checkpoint renewing 使得我们备份的数据比较新,从而通过此备份集建立的备库与主库同步的时间比较短。 远程备份 myrocks_hotbackup只支持远程备份,暂时不支持本地备份。支持tar, xbstream, 另外还支持 facebook开源的网络传输工具WDT, 号称性能有10倍的提升。 一个用tar方式备份的例子 myrocks_hotbackup -u root -P 3306 --stream=tar --checkpoint_dir='xxx' | ssh xx.xx.xx.xx 'tar -xi -C dest_path' 备份日志如下,从日志也可以看整个备份的过程 2017-02-14 15:26:37.076 INFO Starting backup. 2017-02-14 15:26:37.092 INFO Set datadir: /path/data/ 2017-02-14 15:26:37.092 INFO Creating checkpoint at /xxx/1 2017-02-14 15:26:37.096 INFO Created checkpoint at /home/zhangyuan.zy/build/fbmyrocks/backup_tmp/1 2017-02-14 15:26:37.096 INFO Starting backup from snapshot: target files 2 2017-02-14 15:26:37.112 INFO Backup WAL.. 2017-02-14 15:26:37.113 INFO Backup Manifest.. 2017-02-14 15:26:37.131 INFO Cleaned up checkpoint from /xxx/1 2017-02-14 15:26:37.131 INFO Sent 0.00 GB of sst files, 2 files in total. 2017-02-14 15:26:37.131 INFO RocksDB Backup Done. 2017-02-14 15:26:37.132 INFO Taking MySQL misc backups.. 2017-02-14 15:26:37.132 INFO Starting MySQL misc file traversal from database test.. 2017-02-14 15:26:37.139 INFO Starting MySQL misc file traversal from database performance_schema.. 2017-02-14 15:26:37.145 INFO Starting MySQL misc file traversal from database mysql.. 2017-02-14 15:26:37.601 INFO Traversing misc files from data directory.. 2017-02-14 15:26:37.601 INFO Skipping gaplock.log 2017-02-14 15:26:37.607 INFO Skipping auto.cnf 2017-02-14 15:26:37.613 INFO MySQL misc backups done. 2017-02-14 15:26:37.614 INFO All Backups Done. 备份恢复 备份完成后,所有的数据都在同一个目录下,我们需要通过--move_back 将数据移动到我们需要的地方。 myrocks_hotbackup --move_back --datadir=/path/data --rocksdb_datadir=/path/data/.rocksdb --rocksdb_waldir=/path/data/.rocksdb --backup_dir=dest_path 通过备份集搭建一个备库 传统的物理备份工具xtrabackup备份过程中会通过加锁获取一致的binlog位点信息,并保存到文件中,恢复后通过这些位点信息来重建复制关系。而myrocks_hotbackup备份过程中没有加锁,也没有保存位点信息。 在myrocks中enging层rocksdb会保持位点信息,每次事务提交时,都会将binlog位点以及gtid信息保存到数据字典BINLOG_INFO_INDEX_NUMBER中。 BINLOG_INFO_INDEX_NUMBER key: Rdb_key_def::BINLOG_INFO_INDEX_NUMBER (0x4) value: version, {binlog_name,binlog_pos,binlog_gtid} 备份集通过--move_back恢复后,直接启动mysqld,mysqld在recover 过程中会将数据字典BINLOG_INFO_INDEX_NUMBER的信息打印到错误日志中,例如 RocksDB: Last binlog file position 1010, file name mysql-bin.000003 RocksDB: Last MySQL Gtid a3d923e4-f19b-11e6-ba57-2c44fd7a5210:4 从错误日志中解析出位点,然后可以通过以下方式建立复杂关系 show gtid_executed in '$binlog_file' from $binlog_pos; set global gtid_purged='$gtid_executed'; change master to master_host='xx.xx.xx.xx', master_port=${MASTER_MYPORT}, master_user='root', master_auto_position=1, master_connect_retry=1; set global gtid_purged='$gtid_executed'; start slave; myrocks_hotbackup仅支持rocksdb备份,不支持innodb备份 总结 myrocks支持物理备份和逻辑备份,但这两种方式都只支持rocksdb备份,如果需要同时指出innodb和rocksdb的备份,还需要对备份逻辑稍加改造才行。myrocks的物理备份方式比较高效,一般建议采用myrocks_hotbackup物理备份方式。
GTID 简介 GTID (global transaction identifier)在MySQL5.6时引入,GTID是事务的全局唯一标识。GTID结构如下 GTID = source_id:transaction_id source_id:执行事务的原始实例的sever_uuid, 此事务GTID在备库apply时也不变。transaction_id:事务的执行编号,binlog_order_commits=1时,此编号根据事务的提交顺序严格递增。GTID是在binlog flush时生成的,因此同一个serverid的GTID在binglog中是严格有序的。binlog_order_commits=0时,GTID在binlog中也是序的,但并不一定与提交的顺序一致。 binlog_order_commits=0会影响XtraBackup工具的备份,但不会影响innobackup工具的备份XtraBackup会从innodb事务page获取最后提交事务的binlog位点信息,binlog_order_commits=0时事务提交顺序和binlog顺序不一定一致,这样此位点前可能存在部分prepare状态的事务,这些事务在备份恢复后会丢失。而innobackup的位点信息是在加备份锁前提下从show master status/show slave status中获取的,位点前的事务都是已提交的。 支持GTID后,备库启动时不再需要通过位点信息从主库来拉取binlog,而是根据备库本身已执行和拉取的gtid去主库查找第一个未执行的GTID,从此GTID位置开始拉取binlog。 新增了COM_BINLOG_DUMP_GTID命令 备库 备库封装COM_BINLOG_DUMP_GTID命令,包含备库的gtid_executed(已执行的GTID和当前已拉取的GTID的并集) request_dump(): gtid_executed.add_gtid_set(mi->rli->get_gtid_set()) gtid_executed.add_gtid_set(gtid_state->get_executed_gtids()) 主库 主库接收COM_BINLOG_DUMP_GTID命令,从最新的binlog开始反向遍历查找的binlog, 依次读取PREVIOUS_GTIDS_LOG_EVENT, 直到PREVIOUS_GTIDS_LOG_EVENT记录的gtid_set是备库发过来的gtid子集为止。 com_binlog_dump_gtid(): Binlog_sender::init Binlog_sender::check_start_file( mysql_bin_log.find_first_log_not_in_gtid_set Binlog_sender::run mysql5.7 gtid相对5.6主要有以下变化 gtid_mode可以动态设置,支持gtid模式和非gtid模式之间的复制 增加了gtid_executed表 gtid_mode MySQL5.7(>= 5.7.6) gtid_mode支持动态修改,gtid_mode取值可选择如下 OFF: Both new and replicated transactions must be anonymous. OFF_PERMISSIVE: New transactions are anonymous. Replicated transactions can be either anonymous or GTID transactions. ON_PERMISSIVE: New transactions are GTID transactions. Replicated transactions can be either anonymous or GTID transactions. ON: Both new and replicated transactions must be GTID transactions. OFF_PERMISSIVE时支持GTID模式的实例向非GTID模式的实例的复制。 ON_PERMISSIVE:时支持非GTID模式的实例向GTID模式的实例的复制。此模式下,可支持低版本实例(<=5.5)向5.7高版本GTID实例的复制,从而为低版本实例(不支持GTID)平滑升级为5.7GTID实例提供了便利。 需要吐槽的是MySQL5.6目前还不支持低版本实例(<=5.5)向5.6高版本GTID实例的复制, 需要修改代码打开此限制才可以。 另外,gtid_mode动态修改不支持跳跃修改。例如,如果当前值为OFF_PERMISSIV,只支持修改为OFF或ON_PERMISSIVE,不支持修改为ON。 MySQL 5.7 gtid_mode=on时需要设置enforce_gtid_consistency=1. MySQL5.6还需要另外设置 --log-bin, --log-slave-updates,而5.7是不需要的,这得益于5.7的gtid_executed表。 gtid_executed表 gtid_executed表存储的是已执行的GTID集合信息,此信息不一定是实时的。gtid_executed表结构如下 CREATE TABLE gtid_executed ( source_uuid CHAR(36) NOT NULL, interval_start BIGINT(20) NOT NULL, interval_end BIGINT(20) NOT NULL, PRIMARY KEY (source_uuid, interval_start) ) gtid_executed表的益处 有了gtid_executed表后,GTID模式下允许关闭binlog,允许设置log-slave-updates=0。这样带来的以下好处 开启GTID模式下,可以关闭备库的binlog或设置log-slave-updates=0,GTID信息仍然会保存在gtid_executed表中。这样备库依然可以正常复制,同时省去了记录binlog的开销。 AliSQL 5.6在这块也做了优化,备库SQL线程的产生的binlog只记录GTID EVENT信息,不记录实际操作的event, 因此减少了binglog的量, 并且能够保证正常的复制。 开启GTID模式下,由于gtid_executed表是持久化的,即使人为删除了备库的binlog,复制依然可以通过gtid_executed表恢复。 gtid_executed表的更新 gtid_executed在binlog开启和关闭的情况下都会更新 binlog开启每次rotate或shutdown时存储PREVIOUS_GTIDS_LOG_EVENT,只记录最后一个binlog的gtid信息。参考save_gtids_of_last_binlog_into_table binlog关闭或log_slave_updates=0 每次事务提交时都存储GTID MYSQL_BIN_LOG::gtid_end_transaction(): if (!opt_bin_log || (thd->slave_thread && !opt_log_slave_updates)) gtid_state->save(thd) ha_commit_trans(): if (!opt_bin_log || (thd->slave_thread && !opt_log_slave_updates)) gtid_state->save(thd) reset master reset master 会重置表,以delete方式删除所有数据(非truncate) Gtid_table_persistor::reset delete_all(table) gtid_executed表的compress 更新gtid_executed表信息时,每次都是insert一条数据,而不是update方式,update容易产生行冲突,insert可以提高并发。而insert的副作用是导致gtid_executed表行记录数不断增加。因此,专门提供了一个compress线程用来压缩gtid_executed表。以源码中的注释来说明compress过程,具体可参考Gtid_table_persistor::compress_in_single_transaction Read each row by the PK(sid, gno_start) in increasing order, compress the first consecutive range of gtids. For example, 1 1 2 2 3 3 6 6 7 7 8 8 After the compression, the gtids in the table is compressed as following: 1 3 6 6 7 7 8 8 全表扫描,依次找到连续一行删一行,删除(2,2),(3,3),最后更新第一行的结束值(1,1)更新为(1,3) 这里有个有趣的bug, 设置super_read_only导致compress事务在提交时检查read_only失败,然后回滚事务。随着gtid_executed表数据的增加,compress线程的事务越来越大,更新失败然后回滚的代价越来越大。 compress线程是被动触发的 mysql_cond_wait(&COND_compress_gtid_table, &LOCK_compress_gtid_table); 以下两种情况会唤醒compress线程 mysql_cond_signal(&COND_compress_gtid_table); 插入单个GTID时通过参数gtid_executed_compression_period来控制唤醒compress,此种情况发生在binlog关闭或log_slave_updates=0事务提交时。 插入GTID集合每次都会唤醒compress,这种情况发生在binlog开启时, binlog rotate或实例关闭时。 启动时gtid_executed表的处理 实例启动时,会读取gtid_execute表信息来构建以下信息executed_gtids:已执行的gtid信息,是gtid_executed表和binlog中gtid的并集。即gtid_executed。lost_gtids:已经purged的gtid。即gtid_purged。 构建executed_gtids 1 读gtid_executed表的GTID信息赋值给exeucted_gtids, 参考read_gtid_executed_from_table 2 将binlog中比gtid_executed表中多的GTID补进来 gitds_in_binlog_not_in_table.add_gtid_set(&gtids_in_binlog); gtids_in_binlog_not_in_table.remove_gtid_set(executed_gtids); gtid_state->save(&gtids_in_binlog_not_in_table) //将binlog比表中多的补进来 executed_gtids->add_gtid_set(&gtids_in_binlog_not_in_table); gtids_in_binlog是逆向查找binlog,直到找到第一个包含PREVIOUS_GTIDS_LOG_EVENT的binlog为止, 读取此binlog文件的PREVIOUS_GTIDS_LOG_EVENT和GTID_LOG_EVENT构成gtids_in_binlog 构建 lost_gtids lost_gtids = executed_gtids - (gtids_in_binlog - purged_gtids_from_binlog) = gtids_only_in_table + purged_gtids_from_binlog; purged_gtids_from_binlog是正向查找binlog,可以从第一个包含GTID_LOG_EVENT的binlog的PREVIOUS_GTIDS_LOG_EVENT中获取。 有一种情况比较特殊,5.6 升级5.7时,有一种情况会导致binlog中有PREVIOUS_GTIDS_LOG_EVENT但没有GTID_LOG_EVENT。如下面的注释所示,真正的purged_gtids_from_binlog应该从master-bin.N+2的PREVIOUS_GTIDS_LOG_EVENT中获取 /* This branch is only reacheable by a binary log. The relay log don't need to get lost_gtids information. A 5.6 server sets GTID_PURGED by rotating the binary log. A 5.6 server that had recently enabled GTIDs and set GTID_PURGED would have a sequence of binary logs like: master-bin.N : No PREVIOUS_GTIDS (GTID wasn't enabled) master-bin.N+1: Has an empty PREVIOUS_GTIDS and a ROTATE (GTID was enabled on startup) master-bin.N+2: Has a PREVIOUS_GTIDS with the content set by a SET @@GLOBAL.GTID_PURGED + has GTIDs of some transactions. If this 5.6 server be upgraded to 5.7 keeping its binary log files, this routine will have to find the first binary log that contains a PREVIOUS_GTIDS + a GTID event to ensure that the content of the GTID_PURGED will be correctly set (assuming binlog_gtid_simple_recovery is not enabled). */ 原因在于MySQL5.6在set gtid_purged时是通过切换文件(rotate_and_purge )将gtid_purged存储在PREVIOUS_GTIDS_LOG_EVENT中. 而MySQL5.7在set gtid_purged时并不切换文件,gtid_purged直接存储到gtid_executed表中。 参数 binlog_gtid_simple_recovery 官网对binlog_gtid_simple_recovery=false进行了详细的解释。主要说明了binlog_gtid_simple_recovery=false时正向查找binlog获取gtid_purged(对应上节的lost_gtids)和逆向查找binlog获取gtid_executed(对应上节的executed_gtids)可能需要遍历较多的binlog文件,上节也介绍了遍历查找的方法。 但官网只是简单的介绍了binlog_gtid_simple_recovery=true时只需要查找最新或最老的binlog文件即可,至于为什么可以这样做没有明确说明。以下是我的个人理解, 对于gtid_executed只需要读最新的binlog文件,即使最新的binlog文件没有PREVIOUS_GTIDS_LOG_EVENT也没有关系,因为最老的PREVIOUS_GTIDS_LOG_EVENT在binlog roate时已经写入gtid_executed表,根据上节的gtid_executed获取逻辑会读取gtid_executed表,最后获取的gtid_executed是完整的。 对于gtid_purged只需要读取老的binlog文件, 如果最老的binlog文件没有PREVIOUS_GTIDS_LOG_EVENT,同时最新的binlog文件也没有PREVIOUS_GTIDS_LOG_EVENT的情况下,根据上节的lost_gtids恢复逻辑 lost_gtids = executed_gtids - (gtids_in_binlog - purged_gtids_from_binlog) gtids_in_binlog和purged_gtids_from_binlog都为空,最后lost_gtids=executed_gtids,这显然是不正确的。这里我认为lost_gtids并不是一个重要的值,只在set gtid_purge时会修改,即使不正确也不影响正常复制。 GTID三个限制 enforce-gtid-consistency=ON时,以下三类语句时不支持的 CREATE TABLE ... SELECT statements CREATE TEMPORARY TABLE or DROP TEMPORARY TABLE statements inside transactions Transactions or statements that update both transactional and nontransactional tables. There is an exception that nontransactional DML is allowed in the same transaction or in the same statement as transactional DML, if all nontransactional tables are temporary. 而实际上这个限制没有必要这么严格, CREATE TABLE ... SELECT statements 对于binlog_format=row, gtid_next='automatic'时可以放开限制。 生成的binlog包含两个GTID, 一个是建表语句,一个是包含多个insert的事务。 事务中包含事务表和非事务表 对于gtid_next='automatic'时可以放开限制。 生成的binlog包含两个GTID, 一个是所有非事务表的,一个是所有事务表的。 对update多表(包含事务表和非事务表)此时需额外要求binlog_format=row。 总结 MySQL 5.7 在GTID上有了较大改进,但GTID的三个使用限制仍然存在,期待后期有所改进。
现象 实例出现too many connections ERROR 1040 (08004): Too many connections gdb修改max_connections后查看processlist, 有Waiting for backup lock,sql线程被阻塞,同时大量show slave status连接 | 131945 | system user | | mysql | Connect | 302156 | Waiting for Slave Worker to release partition | NULL | | 131946 | system user | | NULL | Connect | 302832 | Waiting for an event from Coordinator | NULL | | 131947 | system user | | NULL | Connect | 381957 | Waiting for an event from Coordinator | NULL | | 131948 | system user | | NULL | Connect | 302167 | Waiting for an event from Coordinator | NULL | | 131949 | system user | | NULL | Connect | 302520 | Waiting for backup lock | NULL | | 131950 | system user | | NULL | Connect | 302531 | Waiting for backup lock | NULL | | 131951 | system user | | NULL | Connect | 302531 | Waiting for backup lock | NULL | | 131952 | system user | | NULL | Connect | 302537 | Waiting for backup lock | NULL | | 131953 | system user | | NULL | Connect | 302554 | Waiting for backup lock | NULL | | 187069 | root | 127.0.0.1:49991 | NULL | Sleep | 9 | | NULL | | 211141 | root | 127.0.0.1:49251 | NULL | Query | 297261 | init | show slave status for channel '' | | 245974 | root | 127.0.0.1:48726 | NULL | Query | 297194 | init | SHOW SLAVE STATUS | | 247341 | aurora | 10.143.33.57:36949 | NULL | Query | 297336 | Killing slave | stop slave | | 247346 | root | 127.0.0.1:58466 | NULL | Killed | 297335 | init | show slave status | | 247349 | root | 127.0.0.1:58565 | NULL | Killed | 297327 | init 查看存在备份进程 root 86809 86803 0 May14 ? 00:00:00 innobackupex --defaults-file=/etc/my.cnf ...... 分析 我们引入了percona 的Backup Locks方案,备份会执行LOCK TABLES FOR BACKUP pt-pmt 分析线程堆栈信息, show slave status等待LOCK_msr_map __lll_lock_wait(libpthread.so.0),_L_lock_995(libpthread.so.0),pthread_mutex_lock(libpthread.so.0),inline_mysql_mutex_lock(mysql_thread.h:690),show_slave_status_cmd(mysql_thread.h:690),mysql_execute_command(sql_parse.cc:3347),mysql_parse(sql_parse.cc:7158),dispatch_command(sql_parse.cc:1597),do_handle_one_connection(sql_connect.cc:1006),handle_one_connection(sql_connect.cc:922),start_thread(libpthread.so.0),clone(libc.so.6) stop slave持有LOCK_msr_map等待stop_cond io和sql退出 1 pthread_cond_timedwait,inline_mysql_cond_timedwait(mysql_thread.h:1199),terminate_slave_thread(mysql_thread.h:1199),terminate_slave_thread(rpl_slave.cc:1268),terminate_slave_threads(rpl_slave.cc:1268),terminate_slave_threads(rpl_slave.cc:9768),stop_slave(rpl_slave.cc:9768),stop_slave(rpl_slave.cc:611),stop_slave_cmd(rpl_slave.cc:756),mysql_execute_command(sql_parse.cc:3707),mysql_parse(sql_parse.cc:7158),dispatch_command(sql_parse.cc:1597),do_handle_one_connection(sql_connect.cc:1006),handle_one_connection(sql_connect.cc:922),start_thread(libpthread.so.0),clone(libc.so.6) sql线程等待 worker线程执行完事务( slave_worker_hash_cond) 1 pthread_cond_wait,inline_mysql_cond_wait(mysql_thread.h:1162),wait_for_workers_to_finish(mysql_thread.h:1162),slave_stop_workers(rpl_slave.cc:6471),handle_slave_sql(rpl_slave.cc:6997),start_thread(libpthread.so.0),clone(libc.so.6) worker等待backup_tables_lock 锁 pthread_cond_timedwait,inline_mysql_cond_timedwait(mysql_thread.h:1199),MDL_wait::timed_wait(mysql_thread.h:1199),MDL_context::acquire_lock(mdl.cc:2416),Global_backup_lock::acquire_protection(lock.cc:1221),open_table(sql_base.cc:3173),open_and_process_table(sql_base.cc:4630),open_tables(sql_base.cc:4630),open_and_lock_tables(sql_base.cc:5735),open_and_lock_tables(sql_base.h:476),Rows_log_event::do_apply_event(sql_base.h:476),slave_worker_exec_job(rpl_rli_pdb.cc:2061),handle_slave_worker(rpl_slave.cc:5696),start_thread(libpthread.so.0),clone(libc.so.6) 而我们备份又持有backup_tables_lock锁 以锁等待依赖顺序导致大量的show slave status被阻塞,从而占满root连接 修复方法 可以通过kill备份的方式修复 如何避免 1 尽量不要使用myisam,减少备份持有LOCK TABLES FOR BACKUP的时间。本例中myisam有200多个 2 备份期间尽量不要执行stop slave操作。
title: MySQL · mysql · mysql 查询结果异常分析 author: 张远 现象 查询条件类型变化后,查询出了不正确的结果。 create table t1(id int primary key, a varchar(50) DEFAULT NULL, key idx_a(a)) engine=innodb; show create table t1; insert into t1 values(1,'6036000240201612190005565273'); insert into t1 values(2,'6036000240201611150005564192'); select * from t1 where a='6036000240201612190005565273'; +----+------------------------------+ | id | a | +----+------------------------------+ | 1 | 6036000240201612190005565273 | +----+------------------------------+ //多了一行不一致的数据 select * from t1 where a=6036000240201612190005565273; +----+------------------------------+ | id | a | +----+------------------------------+ | 2 | 6036000240201611150005564192 | | 1 | 6036000240201612190005565273 | +----+------------------------------+ 分析 索引问题 首先我们要确定数据是否存在问题,我们注意到字段a上有索引idx_a,而且两个查询都走了此索引。 explain select * from t1 where a='6036000240201612190005565273'; +----+-------------+-------+------+---------------+-------+---------+-------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+-------+---------+-------+------+--------------------------+ | 1 | SIMPLE | t1 | ref | idx_a | idx_a | 153 | const | 1 | Using where; Using index | +----+-------------+-------+------+---------------+-------+---------+-------+------+--------------------------+ explain select * from t1 where a=6036000240201612190005565273; +----+-------------+-------+-------+---------------+-------+---------+------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+-------+---------+------+------+--------------------------+ | 1 | SIMPLE | t1 | index | idx_a | idx_a | 153 | NULL | 2 | Using where; Using index | +----+-------------+-------+-------+---------------+-------+---------+------+------+--------------------------+ 一种可能的情况,由于BUG导致二级索引与主键不一致,此种情况我们可通过重建索引修复。 于是删除索引idx_a,再来通过主键索引查询看看 alter table t1 drop key idx_a; explain select * from t1 where a=6036000240201612190005565273; +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | t1 | ALL | NULL | NULL | NULL | NULL | 2 | Using where | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ explain select * from t1 where a='6036000240201612190005565273'; +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | t1 | ALL | NULL | NULL | NULL | NULL | 2 | Using where | +----+-------------+-------+------+---------------+------+---------+------+------+-------------+ 1 row in set (0.00 sec) select * from t1 where a=6036000240201612190005565273; +----+------------------------------+ | id | a | +----+------------------------------+ | 1 | 6036000240201612190005565273 | | 2 | 6036000240201611150005564192 | +----+------------------------------+ 2 rows in set (0.00 sec) select * from t1 where a='6036000240201612190005565273'; +----+------------------------------+ | id | a | +----+------------------------------+ | 1 | 6036000240201612190005565273 | +----+------------------------------+ 1 row in set (0.00 sec) 然而,结果与删除索引前一致。排除了索引的问题,我们只能从源码中来寻找答案了。 查源码 问题出在where条件上,我可以把断点放在条件检查的总入口evaluate_join_record这里,然后一步步跟进下去。 先看条件 a=6036000240201612190005565273 根据比较表达式参数的类型来决定比较时内部使用的比较函数,a:STRING_RESUL b: DECIMAL_RESULT最后得到按REAL_RESULT类型进行比较 Item_result item_cmp_type(Item_result a,Item_result b) { if (a == STRING_RESULT && b == STRING_RESULT) return STRING_RESULT; if (a == INT_RESULT && b == INT_RESULT) return INT_RESULT; else if (a == ROW_RESULT || b == ROW_RESULT) return ROW_RESULT; if ((a == INT_RESULT || a == DECIMAL_RESULT) && (b == INT_RESULT || b == DECIMAL_RESULT)) return DECIMAL_RESULT; return REAL_RESULT; } 这里REAL_RESULT类型比较对应的比较函数为Arg_comparator::compare_real int Arg_comparator::compare_real() { /* Fix yet another manifestation of Bug#2338. 'Volatile' will instruct gcc to flush double values out of 80-bit Intel FPU registers before performing the comparison. */ volatile double val1, val2; val1= (*a)->val_real(); if (!(*a)->null_value) { val2= (*b)->val_real(); if (!(*b)->null_value) { if (set_null) owner->null_value= 0; if (val1 < val2) return -1; if (val1 == val2) return 0; return 1; } } if (set_null) owner->null_value= 1; return -1; } compare_real 会把a值转化为double类型再比较((*a)->val_real()),最终得到的转化函数为my_strtod由于精度问题最后字符串'6036000240201612190005565273'会转化为6.0360002402016117e+27,会损失精度。同时对于比较表达式的右值数字6036000240201612190005565273在内部表示为Item_decimal,在compare_real时也会通过(*b)->val_real(),调用Item_decimal::val_real,最终也是调用my_strtod,转化后的值也为6.0360002402016117e+27 而对于表中另外一个值'6036000240201611150005564192'通过上述转化也6.0360002402016117e+27 因此对于条件 a=6036000240201612190005565273最后返回了两行。 再看条件 a='6036000240201612190005565273' 这个两边都是Field_varstring类型,最终使用的比较函数是Arg_comparator::compare_string。此函数比较时字符串精度不会丢失,比较操作是精确的,因此最终只返回了一行。 结论 最终问题的原因是比较时做类型转化时丢失了精度,导致比较出错。对于字符串转double的情况下,只保留了16位小数。可以做个实验 insert into t1 values(3,'6036000240201611'); insert into t1 values(4,'60360002402016111'); select * from t1 where a=60360002402016111; +----+-------------------+ | id | a | +----+-------------------+ | 4 | 60360002402016111 | +----+-------------------+ elect * from t1 where a=6036000240201611; +----+------------------+ | id | a | +----+------------------+ | 3 | 6036000240201611 | +----+------------------+ 1 row in set (0.01 sec) //小数位16位,出现异常 select * from t1 where a=60360002402016112; +----+-------------------+ | id | a | +----+-------------------+ | 4 | 60360002402016111 | +----+-------------------+ 1 row in set (0.01 sec) //小数位15位,没有问题 mysql> select * from t1 where a=6036000240201612; Empty set (0.00 sec) 实际上mysql 对于float,double小数的处理是不精确的,使用时应格外注意。官方也有很有意思的例子,有兴趣的可以看看。
title: MySQL · myrocks · myrocks index condition pushdown author: 张远 index condition pushdown Index condition pushdown(ICP)是直到mysql5.6才引入的特性,主要是为了减少通过二级索引查找主键索引的次数。目前ICP相关的文章也比较多,本文主要从源码角度介绍ICP的实现。讨论之前,我们先再温习下。 以下图片来自mariadb 引入ICP之前 引入ICP之后 再来看个例子 CREATE TABLE `t1` ( `a` int(11) DEFAULT NULL, `b` char(8) DEFAULT NULL, `c` int(11) DEFAULT '0', `pk` int(11) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`pk`), KEY `idx1` (`a`,`b`) ) ENGINE=ROCKSDB; INSERT INTO t1 (a,b) VALUES (1,'a'),(2,'b'),(3,'c'); INSERT INTO t1 (a,b) VALUES (4,'a'),(4,'b'),(4,'c'),(4,'d'),(4,'e'),(4,'f'); set optimizer_switch='index_condition_pushdown=off'; # 关闭ICP(Using where) explain select * from t1 where a=4 and b!='e'; +----+-------------+-------+-------+---------------+------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+------+---------+------+------+-------------+ | 1 | SIMPLE | t1 | range | idx1 | idx1 | 14 | NULL | 2 | Using where | +----+-------------+-------+-------+---------------+------+---------+------+------+-------------+ # 关闭ICP走cover index(Using where; Using index) explain select a,b from t1 where a=4 and b!='e'; +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ | 1 | SIMPLE | t1 | ref | idx1 | idx1 | 5 | const | 4 | Using where; Using index | +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ set optimizer_switch='index_condition_pushdown=on'; # 开启ICP(Using index conditione) explain select * from t1 where a=4 and b!='e'; +----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+ | 1 | SIMPLE | t1 | range | idx1 | idx1 | 14 | NULL | 2 | Using index condition | +----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+ # 开启ICP仍然是cover index(Using where; Using index) explain select a,b from t1 where a=4 and b!='e'; +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ | 1 | SIMPLE | t1 | ref | idx1 | idx1 | 5 | const | 4 | Using where; Using index | +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ 这里总结下ICP的条件 适用于以下类型,range, ref, eq_ref, and ref_or_null 的二级索引 不能是覆盖索引(cover index) server层主要负责判断是否符合ICP的条件,符合ICP则把需要的condition push到engine层。engine层通过二级索引查找数据时,用server层push的condition再做一次判断,如果符合条件才会去查找主索引。 目前mysql支持ICP的引擎有MyISAM和InnoDB,MyRocks引入rocksdb后,也支持了ICP。 server层实现是一样的,engine层我们主要介绍innodb和rocksdb的实现。 server层 关键代码片段如下 make_join_readinfo() switch (tab->type) { case JT_EQ_REF: case JT_REF_OR_NULL: case JT_REF: if (tab->select) tab->select->set_quick(NULL); delete tab->quick; tab->quick=0; /* fall through */ case JT_SYSTEM: case JT_CONST: /* Only happens with outer joins */ if (setup_join_buffering(tab, join, options, no_jbuf_after, &icp_other_tables_ok)) DBUG_RETURN(true); if (tab->use_join_cache != JOIN_CACHE::ALG_NONE) tab[-1].next_select= sub_select_op; if (table->covering_keys.is_set(tab->ref.key) && !table->no_keyread) table->set_keyread(TRUE); else push_index_cond(tab, tab->ref.key, icp_other_tables_ok, &trace_refine_table); break; 从代码中看出只有符合的类型range, ref, eq_ref, and ref_or_null 二级索引才可能会push_index_cond。 而这里通过covering_keys来判断并排除使用了cover index的情况。covering_keys是一个bitmap,保存了所有可能用到的覆盖索引。在解析查询列以及条件列时会设置covering_keys,详细可以参考setup_fields,setup_wild,setup_conds。 engine层 innodb innodb在扫描二级索引时会根据是否有push condition来检查记录是否符合条件(row_search_idx_cond_check)逻辑如下: row_search_for_mysql() ...... if (prebuilt->idx_cond) { row_search_idx_cond_check //检查condition row_sel_get_clust_rec_for_mysql //检查通过了才会去取主索引数据 } .... 典型的堆栈如下 handler::compare_key_icp innobase_index_cond row_search_idx_cond_check row_search_for_mysql ha_innobase::index_read ha_innobase::index_first ha_innobase::rnd_next handler::ha_rnd_next rr_sequential join_init_read_record sub_select do_select rocksdb rocksdb在扫描二级索引时也会根据是否有push condition来检查记录是否符合条件 逻辑如下 read_row_from_secondary_key() { find_icp_matching_index_rec//push了condition才会检查condition get_row_by_rowid//检查通过了才会去取主索引数据 } 典型的堆栈如下 handler::compare_key_icp myrocks::ha_rocksdb::check_index_cond myrocks::ha_rocksdb::find_icp_matching_index_rec myrocks::ha_rocksdb::read_row_from_secondary_key myrocks::ha_rocksdb::index_read_map_impl myrocks::ha_rocksdb::read_range_first handler::multi_range_read_next other ICP对cover index作出了严格的限制,而实际上应该可以放开此限制,这样可以减少enging层传第给server层的数据量,至少可以减少server层的内存使用。欢迎指正!
title: MySQL · myrocks · myrocks crash safe 特性 author: 张远 crash safe 定义 在主备环境下,实例mysqld crash或机器宕机是常见的事情,而如何快速而安全的从异常中恢复,对高可用来说是非常重要的。常见的恢复方法是从备份集来恢复实例,重建复制关系,这种做法是安全的但一般耗时较长。另外一种方法就是等异常实例恢复,直接启动异常实例来恢复复制关系,这种做法快速但不一定是安全的,下节会详细介绍原因。 crash safe就是指在异常实例恢复后,直接启动异常实例来恢复复制关系,并且保证数据是一致的。 myrocks 是crash safe的。 不一致原因 分为主库异常恢复和备库异常恢复两种情况,这两种情况都可能出现不一致的情况。 主库异常 在myrocks主备环境是开启了loss-less semisync(AFTER-SYNC模式),并且主库sync_binlog=1,innodb_flush_log_at_trx_commit=1(双1模式)。在这种情况下,主库是在sync binlog 之后,engine commit之前等待备库的ACK, 因此主库crash时,主库可能存在部分binlog已经落盘但engine层没有提交,并且这些binlog也没有即时发送到备库的情况。 这部分binlog 实际上就是比备库多出来的binlog,而主库启动恢复这部分binlog所在事务是处于prepare状态的,根据binlog XA协议,这部分事务最终会提交。从而,主库比备库会多一些事物,导致不一致。 备库异常 为了加快复制速度,备库一般会设置sync_binlog!=1,innodb_flush_log_at_trx_commit!=1(非双1模式)。这种设置,备库可能出现两种情况: binlog落后于engine层,也就是存在engine 层提交了但binlog没有落盘的情况。 binlog超前于engine层,也就是存在binlog落盘了但engine层没有提交(非prepare)的情况。 在这两种情况下直接启动备库,重建复制关系都可能导致主备不一致。 engine层的binlog信息 讨论myrocks如何做到crash safe之前,我们先熟悉下以下背景知识。engine 层提交时会保存binlog位点信息。对于innodb, 每次提交时会在系统事务页(第5页)存储binlog 位点和gtid,参考trx_sys_update_mysql_binlog_offset #define TRX_SYS_MYSQL_LOG_OFFSET_HIGH 4 /*!< high 4 bytes of the offset within that file */ #define TRX_SYS_MYSQL_LOG_OFFSET_LOW 8 /*!< low 4 bytes of the offset within that file */ #define TRX_SYS_MYSQL_LOG_NAME 12 /*!< MySQL log file name */ #define TRX_SYS_MYSQL_GTID (TRX_SYS_MYSQL_LOG_NAME + TRX_SYS_MYSQL_LOG_NAME_LEN) 对于rocksdb,每次提交时会在数据字典BINLOG_INFO_INDEX_NUMBER中存储binlog 位点和gtid, 参考Rdb_binlog_manager::update BINLOG_INFO_INDEX_NUMBER key: Rdb_key_def::BINLOG_INFO_INDEX_NUMBER (0x4) value: version, {binlog_name,binlog_pos,binlog_gtid} master crash safe myrocks可以做到主库crash safe。前面讲过如果直接启动主库,多出来的那部分binlog可能导致不一致。如果我们能够把这部分binlog去掉,那么启动时,prepare的事务就可以回滚了,从而主备又可以一致了。事实上facebook就是这样做的,主库crash时,备库io线程会终止,同时会在errorlog中记录拉取主库binlog的位点信息。取到这位点后,将主库的binlog从此位点truncate掉。 以下图片来自facebook 主库crash truncate binlog 后 前面提到engine层也保存了位点信息,那么我们为什么不使用这个位点,而是从备库errorlog中获取呢。engine层位点前的事务在engine 层都是已提交的。主库crash时,prepare的事务的binlog有可能部分传到了备库,而这些prepare的事务重启动时是应该提交的。而如果用engine层位点来truncate binlog会导致这部分事务回滚。因此,从备库获取的位点信息才是准确的。 slave crash safe myrocks可以做到备库库crash safe,这里需考虑两种情况 binlog落后于engine层,也就是存在engine 层提交了但binlog没有落盘的情况。 针对此种情况,Facebook mysql增加了新的系统表专门保存每个db最后提交的gtid信息。之所以保存的是db的gtid信息,是因为并行复制还是库级别并发的,不得不吐槽下。 CREATE TABLE `slave_gtid_info` ( `Id` int(10) unsigned NOT NULL, `Database_name` varchar(64) NOT NULL, `Last_gtid` varchar(56)DEFAULT NULL, PRIMARY KEY (`Id`) ) DEFAULT CHARSET=utf8 STATS_PERSISTENT=0 此gtid信息,在每个事务提交时都会更新。 备库启动时,这部分未落盘事务会重新拉取部分到备库apply, sql线程apply的过程中会根据系统表中的gtid信息来判断此gtid是否应该跳过,这些需跳过的事务只需要记录binlog不需要真正执行。参考Gtid_info::skip_event . binlog超前于engine层,也就是存在binlog落盘了但engine层没有提交(非prepare)的情况。 实例启动时gtid_executed是从binlog中读取的。对于此种情况,先从engine层读取位点信息,更新gtid_executed信息时只读取binlog的到此位点,然后rotate binlog,用新的gtid_executed信息重新拉取binlog就不会不一致了。参考read_gtids_from_binlog。 slave_gtid_info表优化 每个事务提交时都要更新slave_gtid_info表。最初的时候,这个操作是server层提交时由server层更新slave_gtid_info表,后来将这个操作下沉到engine直接由engine层来执行。从而省去了server层接口调用的开销,这个优化对于rocksdb insert 场景有30%左右的提升。详情参考这里。不过,如果需要混用innodb和rocksdb,建议禁用此优化。 关于gtid_execute表 mysql5.7已经支持gtid_executed表(myrocks是基于mysql5.6的), 结构如下 CREATE TABLE `gtid_executed` ( `source_uuid` char(36) NOT NULL COMMENT 'uuid of the source where the transaction was originally executed.', `interval_start` bigint(20) NOT NULL COMMENT 'First number of interval.', `interval_end` bigint(20) NOT NULL COMMENT 'Last number of interval.', PRIMARY KEY (`source_uuid`,`interval_start`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 STATS_PERSISTENT=0 此表保存了所有已执行的gtid信息,与slave_gtid_info不同,slave_gtid_info只是保存了每个db最后执行的gtid信息。如果将slave_gtid_info换成gtid_executed表,整个代码逻辑将更加清晰,同时gtid_executed表对以后支持表级或事务级的并行复制来说更加便利。 总结 myrocks的crash safe 特性可以帮助我们快速安全的恢复实例,重新建立复制关系,恢复高可用。crash safe 可以解决大多数主备环境下由于实例crash导致的主备不一致问题。然而,前面提到的主库crash safe是在semisync没有退化为异步的情况下才成立,要想真正的做到高可用,可使用mysql5.7.17刚GA的group replication 特性。当然,myrocks理论上也是支持group relication的,rocksdb只是mysql的一个engine而已。另外,业界已经有比较成熟的基于rocksdb的高可用分布式数据库,例如TiDB,cockroachDB。
title: MySQL ・ myrocks ・ myrocks统计信息 author: 张远 概述 mysql查询优化主要是在代价统计分析的基础上进行的。合理的代价模型和准确的代价统计信息决定了查询优化的优劣。myrocks基于mysql5.6, 目前的代价模型依赖的主要因素是IO和CPU,mysql5.7及以上的版本代价模型做了较多改进,具体可以参考这里 IO主要跟数据量和缓存相关,而CPU主要跟参与排序比较的记录数相关。 因此mysql5.6的统计信息的指标主要是数据量和记录数。例如: table scan:全表扫描统计信息包括数据量和记录数。 index scan:索引统计信息,索引键值分布情况,即cardinality。 range scan:索引范围扫描统计信息,一定范围内的记录数和数据量。 统计信息 mysql5.6 代价计算都是在server层完成,且代价只关心引擎层的数据量和行数,没有考虑不同引擎存储方式的差异,其代价也会存在差异。相对来说,5.7的代价统计方式更为合理。 对server层来说,不同存储引擎都应提供以下统计信息 索引的大小 索引的总行数 索引的键值分布, 不同长度前缀的键值分布 一定范围内的记录数 下面分别介绍innodb和rocksdb的统计信息 InnoDB统计分析 统计信息存储 innodb的统计信息可以通过下列表查询 information.statistics mysql.innodb_table_stats mysql.innodb_index_stats 实际上innodb的统计信息持久化在mysql.innodb_table_stats和mysql.innodb_index_stats这两个表中 CREATE TABLE `innodb_table_stats` ( `database_name` varchar(64) COLLATE utf8_bin NOT NULL, `table_name` varchar(64) COLLATE utf8_bin NOT NULL, `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `n_rows` bigint(20) unsigned NOT NULL, `clustered_index_size` bigint(20) unsigned NOT NULL, `sum_of_other_index_sizes` bigint(20) unsigned NOT NULL, PRIMARY KEY (`database_name`,`table_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 CREATE TABLE `innodb_index_stats` ( `database_name` varchar(64) COLLATE utf8_bin NOT NULL, `table_name` varchar(64) COLLATE utf8_bin NOT NULL, `index_name` varchar(64) COLLATE utf8_bin NOT NULL, `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `stat_name` varchar(64) COLLATE utf8_bin NOT NULL, `stat_value` bigint(20) unsigned NOT NULL, `sample_size` bigint(20) unsigned DEFAULT NULL, `stat_description` varchar(1024) COLLATE utf8_bin NOT NULL, PRIMARY KEY (`database_name`,`table_name`,`index_name`,`stat_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 统计方法 索引大小 从segment描述项直接得到索引占用的page数(btr_get_size),索引总数据量=总page数*page大小。 索引大小的计算是比较精确的。 索引键值分布 通过扫描所有数据的方式来统计键值分布虽然得到的数据是准确的,但是非常耗时。因此innodb是通过采样的方式来实现的,参数innodb_stats_persistent_sample_pages、innodb_stats_sample_pages 、innodb_stats_transient_sample_pages可以控制采样的page数。一般来说采样的page越分散,数据越准确。 采样有两种方式,transient方式和persistent方式。 1) transient方式:快速但不精确(dict_stats_update_transient) 从根开始每层随机取一条记录到下一层,直到叶子节点。这样采样得到page过于随机,采样page可能出现比较集中的情况,极端情况下多次采样的page有可能是重复的。 2) persistent方式:慢但相对精确(dict_stats_update_persistent) presistent方式分为两个阶段。第一阶段,找到一个合适的层次(非叶子层)用于分段,这个层次的不同值个数须>=10*采样页个数即N_DIFF_REQUIRED(index))。第二阶段,在找到的层次上进行分段,分段个数为N(N<=采样数),再从每个分段随机取记录向下层找采样页,如果下层节点所有记录都相等,那么采样可以提前结束,不需要一直向下找到叶子节点,因为叶子节点中记录必定也是相同的。 persistent方式采样比较分散,但第一阶段分段可能比较耗时,如果索引区分度不高,可能需要到Level=1层才分段。  遍历采样页可以得到采样页的键值分布情况,从segment描述项可以得到叶子节点page数,再根据叶子节点page数和采样页比例可以得出最终的键值分布情况。 总行数 前面已经计算出主键索引的分布情况, 总行数=主键不同值的个数。 范围统计 范围统计,先从B树中查找起始值和结束值,并记录查找路径,从而每层的范围能够确定下来。 有一个规律是,上层范围内的记录数等于下层范围内的page数。 每层最多读取10个page,此层每页记录平均数=读取的记录数/读取的page数。 假如此层范围内page数>10, 那么范围内的记录数=此层每页记录平均数*上层的范围内的记录数。 下层范围内的记录数依赖于上层范围内的记录数。这样每层计算直到叶子层。 统计信息更新 以下情况会触发统计信息更新 analyze table 距离上一次更新统计信息,发生变化的行数超过一定数值时自动更新(transient:1/16, persistent :1/10) create table/truncate table 会初始化统计信息 查询information_schema.tables information_schema.statistic(innodb_stats_on_metadata=ON) Rocksdb统计分析 统计信息存储 从server层来看,rocksdb统计信息存储在rocksdb数据字典NDEX_STATISTICS中 key: Rdb_key_def::INDEX_STATISTICS(0x6) + global_index_id value: version, {materialized PropertiesCollector::IndexStats} 实际包含以下信息 struct Rdb_index_stats { ...... GL_INDEX_ID m_gl_index_id; int64_t m_data_size, m_rows, m_actual_disk_size; int64_t m_entry_deletes, m_entry_single_deletes; int64_t m_entry_merges, m_entry_others; std::vector<int64_t> m_distinct_keys_per_prefix; ...... } NDEX_STATISTICS并没有像innodb统计信息一样提供mysql 下的表来查询,但我们仍可以从information_schema.statistic查看部分统计信息。 从rocksdb层来看,统计信息在每个SST file meta中都单独保存了自己的统计信息 而数据字典NDEX_STATISTICS的数据是汇总了memtable和所有sstable统计信息后的数据。 统计方法 memtable 每插入一行数据会统计行数(num_entries_)和数据量(data_size_) memtable flush时会将SST 统计信息持久化到SST的meta中。 compact时新的统计信息也会持久化到新生成的SST的meta中。 范围分布 范围分布需从memtable和sstable中查找 查找memtable(skiplist),一个估算规则是, 下层范围内节点数=上层节点数*branching_factor。根据此规则可以估算memtable范围内的数据。 相关代如下 template <typename Key, class Comparator> uint64_t SkipList<Key, Comparator>::EstimateCount(const Key& key) const { uint64_t count = 0; Node* x = head_; int level = GetMaxHeight() - 1; while (true) { assert(x == head_ || compare_(x->key, key) < 0); Node* next = x->Next(level); if (next == nullptr || compare_(next->key, key) >= 0) { if (level == 0) { return count; } else { // Switch to next list count *= kBranching_; level--; } } else { x = next; count++; } } } 查sstable,先定位每层范围涉及的sstable,再估算范围内的数据大小。如果某个sstable全包含在范围内,则大小可以直接从sstable 的meta中获取;如果sstable只是半包含,那么需要计算范围在sstable中的offset,从而得到sstable中被包含的数据大小。 算出每层的范围内的数据大小,汇总得到范围内的总大小。 范围内的总行数=范围内的sstable总大小*sstable总行数/sst总大小 + memtable范围内的总行数。 官方代码存在bug,已提交给官方,详见这里 总行数 总行数=memtable中行数+sstable中行数 memtable中行数估算方法同上一节 官方代码实现假设记录大小为100字节(ROCKSDB_ASSUMED_KEY_VALUE_DISK_SIZE),stats.records = sz/ROCKSDB_ASSUMED_KEY_VALUE_DISK_SIZE; 实际没有必要,实际上是可以通过上面的方法估算的。 sstable中行数只需要从meta中获取并汇总即可。 而实际上如果每此从memtable估算行数还是有一定开销的。所以,官方在仅有memtable而没有sstable的情况下才估算memtable的行数。而对于memtable和sstable共存的情况则只考虑sstable,忽略memtable中行数。 这里应该可以优化,比如可以考虑immutable memtable的行数 总大小 不需要统计memtable,只需要汇总sstable meta中的大小。 键值分布 每个sstable meta有键值分布信息,只需要汇总即可。 这里也存在一个bug 对于memtable,如果仅有memtableer没有sstable,那么键值分布只是简单的给了初始值。 // Fake cardinality implementation. For example, (idx1, idx2, idx3) index // will have rec_per_key for (idx1)=4, (idx1,2)=2, and (idx1,2,3)=1. // rec_per_key for the whole index is 1, and multiplied by 2^n if // n suffix columns of the index are not used. x = 1 << (k->actual_key_parts-j-1); 而对于memtable和sstable共存的情况则只考虑sstable,忽略memtable的键值分布 统计信息更新 实例启动时会从数据字典INDEX_STATISTICS读取并初始化所有索引统计信息。 analyze table 汇总memtable和所有sstable的统计信息,并持久化到数据字典INDEX_STATISTICS。 flush memtable/compact 都会更新内存统计信息,并不持久化。flush memtable 新文件的统计信息会merge加入内存统计信息中。compact时会去掉老文件的统计信息,同时加上新生成文件的统计信息。 后台线程会定时持久化统计信息到数据字典INDEX_STATISTICS 总结 rocksdb和innodb统计信息有很多相似之处,但rocksdb sstable单独维护了统计信息,因此rocksdb的统计信息收集比innodb更快也更精确。同时,我们也看到了rocksdb的统计信息还有需要改进的地方,官方也逐步在完善。
title: MySQL · myrocks · myrocks复制优化 author: 张远 概述 myrocks依然采用mysql原有的基于binlog的复制方式。目前由于myrocks不支持gap lock, 因此在statement格式的binlog下进行复制,主备可能出现不一致。myrocks建议在复制时设置binlog格式为row。 myrocks在rocksdb引擎层为复制做了一些卓有成效的优化,例如skip unique check , read free replication。 skip unique check skip unique check 忽略唯一性检查,此特性开启时需确保我们写入的数据不会违反唯一性约束。在正常的主备复制环境下, 备库是只读的,主库的写入是经过了唯一性检查的,写入binlog后,备库应用这些binlog时理论上是不需要再检查唯一性的。 基于以上假设,备库开skip unique check,可以减少唯一性检查的开销,并保证主备数据的一致性。 skip unique check有以下参数可以控制, rocksdb_skip_unique_check 控制rocksdb是否忽略唯一性检查,对复制sql线程和用户正常连接都有效。一般不建议开启。 rocksdb_skip_unique_check_tables 指定哪些表忽略唯一性检查,只对复制sql线程有效。 unique_check_lag_threshold 备库延迟超过此值时才忽略唯一性检查 unique_check_lag_reset_threshold 备库延迟小于此值时不忽略唯一性检查 在备库环境中,我们一般只设置以下三个参数(rocksdb_skip_unique_check参数设置为true后,下面三个参数不管怎么设置都会忽略唯一性检查) rocksdb_skip_unique_check_tables unique_check_lag_threshold unique_check_lag_reset_threshold 备库开启skip unique check时,还有一个优化是写入数据时不需要加锁,省去了锁的开销(get_blind_write_batch)。 read free replication read free replication优化思路来源于tokudb。tokudb是基于Fractal-Trees,数据都是先写入到内节点message buffer, 最后再apply到叶子节点。这种延迟写入特性有益于read free replication。 read free replication必须工作在row格式的binlog下,基于row格式的binlog包括row的前镜像和后镜像。read free replication利用前镜像来直接更新数据,从而减少了一次读取行操作。 引入read free replication之前,备库复制线程是这样工作的 delete 根据row image来查找此行是否存在,如果不存在复制就报错退出,存在则继续delete。 update update binlog有前镜像和后镜像,先根据前镜像来查找此行是否存在,如果不存在复制就报错退出,存在则根据后镜像更新数据。 引入read free replication之后,备库复制线程是这样工作的 delete 直接根据row image来delete,不需要判断行是否存在。 update update binlog直接根据后镜像更新数据,不需要判断行是否存在。 其中update过程中如果有更新唯一性字段,还是需要读取行来检查唯一性。 对于insert,read free replication 实际不起作用。 insert insert过程还是需要检查唯一性的。 因此,要想真正的做到read free replication即复制sql线程只管写入不需要读取行, read free replication是需要和skip unique check一起配合使用的 问题来了,innodb可以做到read free replication吗? innodb写入并不像tokudb,rocksdb一样是延迟写入的,同时innodb的更新必须读取老的行数据。因此,innodb不能做到read free replication。 read free replication风险 read free replication使用是在一定前提下的 binlog格式为row 复制所在的备库必须是只读的 这里列了两个违反规则使用read free replication导致出问题的两个例子,转帖如下 二级索引少了些行 create table t (id int primary key, i1 int, i2 int, value int, index (i1), index (i2)) engine=rocksdb; insert into t values (1,1,1,1),(2,2,2,2),(3,3,3,3); s: delete from t where id <= 2; m: update t set i2=100, value=100 where id=1; s: mysql> select count(*) from t force index(primary); +----------+ | count(*) | +----------+ | 2 | +----------+ 1 row in set (0。00 sec) mysql> select count(*) from t force index(i1); +----------+ | count(*) | +----------+ | 1 | +----------+ 1 row in set (0。00 sec) mysql> select count(*) from t force index(i2); +----------+ | count(*) | +----------+ | 2 | +----------+ 1 row in set (0。00 sec) mysql> select * from t where id=1; +----+------+------+-------+ | id | i1 | i2 | value | +----+------+------+-------+ | 1 | 1 | 100 | 100 | +----+------+------+-------+ 1 row in set (0。00 sec) mysql> select i1 from t where i1=1; Empty set (0。00 sec) mysql> select i2 from t where i2=100; +------+ | i2 | +------+ | 100 | +------+ 1 row in set (0。00 sec) 二级索引多了些行 M: create table t (id int primary key, i1 int, i2 int, value int, index (i1), index (i2)) engine=rocksdb; insert into t values (1,1,1,1),(2,2,2,2),(3,3,3,3); S: update t set i1=100 where id=1; M: delete from t where id=1; S: mysql> select count(*) from t force index(primary); +----------+ | count(*) | +----------+ | 2 | +----------+ 1 row in set (0。00 sec) mysql> select count(*) from t force index(i1); +----------+ | count(*) | +----------+ | 3 | +----------+ 1 row in set (0。00 sec) mysql> select count(*) from t force index(i2); +----------+ | count(*) | +----------+ | 2 | +----------+ 1 row in set (0。00 sec) mysql> select i1 from t where i1=100; +------+ | i1 | +------+ | 100 | +------+ 1 row in set (0。00 sec) read free replication应用 这篇文章介绍了tokudb read free replicatio的应用场景,同样适用于rocksdb read free replication。 总之,read free replicatio大大提高了复制的效率,同时结合rockedb的高效压缩和低写入放大特性,使得myrocks非常适用于只读库的扩展,或作为mysql其他引擎实例的备用实例。 总结 myrocks在复制方面作了有益的优化,但这些优化并不是银弹。我们通过这些优化得到高的回报的同时,也要明确知道这些优化的风险,严格遵守优化的前置条件,从而保证安全性和高性能。
title: MySQL · myrocks · myrocks之事务处理 author: 张远 前言 mysql目前支持的事务引擎有innodb,tokudb. rocksdb加入mysql阵营后,mysql支持的事务引擎增长至3个。 myrocks目前支持的事务隔离级别有read-committed和repeatable-read. 同innodb一样,myrocks也支持MVCC机制。可以说,myrocks提供了很好的事务支持,能够满足的一般业务的事务需求。 sequence number 谈到rocksdb事务,就必须提及rocksdb中的sequence number机制。rocksdb中的每一条记录都有一个sequence number, 这个sequence number存储在记录的key中。 InternalKey: | User key (string) | sequence number (7 bytes) | value type (1 byte) | 对于同样的User key记录,在rocksdb中可能存在多条,但他们的sequence number不同。 sequence number是实现事务处理的关键,同时也是MVCC的基础。 snapshot snapshot是rocksdb的快照信息,snapshot实际就是对应一个sequence number. 简单的讲,假设snapshot的sequence number为Sa, 那么对于此snapshot来说,只能看到sequence number<=sa的记录,sequence number>sa的记录是不可见的。 snapshot 结构 snapshot 主要包含sequence number和snapshot创建时间,sequence number 取自当前的sequence number. class SnapshotImpl : public Snapshot { SequenceNumber number_; // sequenct number int64_t unix_time_; // snapshow创建时间 ...... }; snapshot 管理 snapshot由全局双向链表管理,根据sequence number排序。snapshot的创建和删除都需要维护双向链表。 snapshot与compact rocksdb的compact操作与snapshot有紧密联系。以我们熟悉的innodb为例,rocksdb的compact类似于innodb的purge操作, 而snapshot类似于InnoDB的read view. innodb做purge操作时会根据已有的read view来判断哪些undo log可以purge,而rocksdb的compact操作会根据已有snapshot信息即全局双向链表来判断哪些记录在compace时可以清理。 判断的大体原则是,从全局双向链表取出最小的snapshot sequence number Sn. 如果已删除的老记录sequence number <=Sn, 那么这些老记录在compact时可以清理掉。 MVCC 有了snapshot,MVCC实现起来就很顺利了。记录的sequence number天然的提供了记录的多版本信息。 每次查询用户记录时,并不需要加锁。而是根据当前的sequence number Sn创建一个snapshot, 查询过程中只取小于或等于Sn的最大sequence number的记录。查询结束时释放snapshot. 关键代码段 DBIter::FindNextUserEntryInternal if (ikey.sequence <= sequence_) { if (skipping && user_comparator_->Compare(ikey.user_key, saved_key_.GetKey()) <= 0) { num_skipped++; // skip this entry PERF_COUNTER_ADD(internal_key_skipped_count, 1); } else { switch (ikey.type) { case kTypeDeletion: case kTypeSingleDeletion: // Arrange to skip all upcoming entries for this key since // they are hidden by this deletion. saved_key_.SetKey( ikey.user_key, !iter_->IsKeyPinned() || !pin_thru_lifetime_ /* copy */); skipping = true; num_skipped = 0; PERF_COUNTER_ADD(internal_delete_skipped_count, 1); break; case kTypeValue: valid_ = true; saved_key_.SetKey( ikey.user_key, !iter_->IsKeyPinned() || !pin_thru_lifetime_ /* copy */); return; case kTypeMerge: ...... 隔离级别 隔离级别也是通过snapshot来实现的。在innodb中,隔离级别为read-committed时,事务中每的个stmt都会建立一个read view, 隔离级别为repeatable-read时,只在事务开启时建立一次read view. rocksdb同innodb类似,隔离级别为read-committed时,事务中每的个stmt都会建立一个snapshot, 隔离级别为repeatable-read时,只在事务开启时第一个stmt建立一次snapshot. 关键代码片段 rocksdb_commit: if (my_core::thd_tx_isolation(thd) <= ISO_READ_COMMITTED) { // For READ_COMMITTED, we release any existing snapshot so that we will // see any changes that occurred since the last statement. tx->release_snapshot(); } 隔离级别实现差异 在read committed隔离级别下,如果一个大事务要更新1000w行,当它更新了前900w行时, 同时另一个事务已经更新了后100w行,那么myrocks会重新获取快照,再次尝试更新,这样 更新的是新提交的数据,也符合read committed逻辑。具体的讨论可以参考最近的issue#340. 而之前的处理方式是直接报死锁错误。 rocksdb::Status ha_rocksdb::get_for_update( Rdb_transaction* tx, rocksdb::ColumnFamilyHandle* column_family, const rocksdb::Slice& key, std::string* value) const { rocksdb::Status s= tx->get_for_update(column_family, key, value); // If we have a lock conflict and we are running in READ COMMITTTED mode // release and reacquire the snapshot and then retry the get_for_update(). if (s.IsBusy() && my_core::thd_tx_isolation(ha_thd()) == ISO_READ_COMMITTED) { tx->release_snapshot(); tx->acquire_snapshot(false); s= tx->get_for_update(column_family, key, value); } return s; } innodb不会出现上述情况,当第一个大事更新是会持有b树的index lock, 第二个事务会一直等待index lock直至第一个事务提交完成。 锁 myrocks目前只支持一种锁类型:排他锁(X锁),并且所有的锁信息都保存在内存中。 锁结构 每个锁实际上存储的哪条记录被哪个事务锁住。 struct LockInfo { TransactionID txn_id; // Transaction locks are not valid after this time in us uint64_t expiration_time; ...... } 每个锁实际是key和LockInfo的映射. 锁信息都保存在map中 struct LockMapStripe { std::unordered_map<std::string, LockInfo> keys; ...... } 为了减少全局锁信息访问的冲突, rocksdb将锁信息进行按key hash分区, struct LockMap { std::vector<LockMapStripe*> lock_map_stripes_; } 同时每个column family 存储一个这样的LockMap. using LockMaps = std::unordered_map<uint32_t, std::shared_ptr<LockMap>>; LockMaps lock_maps_; 锁相关参数: max_num_locks:事务锁个数限制expiration:事务过期时间 通过设置以上两个参数,来控制事务锁占用过多的内存。 死锁检测 rocksdb内部实现了简单的死锁检测机制,每次加锁发生等待时都会向下面的map中插入一条等待信息,表示一个事务id等待另一个事务id.同时会检查wait_txn_map_是否存在等待环路,存在环路则发生死锁。 std::unordered_map<TransactionID, TransactionID> wait_txn_map_; 死锁检测关键代码片段 TransactionLockMgr::IncrementWaiters: for (int i = 0; i < txn->GetDeadlockDetectDepth(); i++) { if (next == id) { DecrementWaitersImpl(txn, wait_id); return true; } else if (wait_txn_map_.count(next) == 0) { return false; } else { next = wait_txn_map_[next]; } } 死锁检测相关参数 deadlock_detect:是否开启死锁检测deadlock_detect_depth:死锁检查深度,默认50 gap lock innodb中是存在gap lock的,主要是为了实现repeatable read和唯一性检查的。 而在rocksdb中,不支持gap lock(rocksdb insert是也会多对唯一键加锁,以防止重复插入, 严格的来讲也算是gap lock). 那么在rocksdb一些需要gap lock的地方,目前是报错和打印日志来处理的。 相关参数 gap_lock_write_log: 只打印日志,不返回错误 gap_lock_raise_error: 打印日志并且返回错误 锁示例 直接看例子 binlog XA & 2pc myrocks最近也支持了binlog xa. 在开启binlog的情况下,myrocks提交时,会经历两阶段提交阶段。 prepare阶段,根据server层生成的xid(由MySQLXid+server_id+qurey_id组成),在rockdb内部执行2pc操作,生成Prepare(xid),EndPrepare()记录。 commit阶段,根据事务成还是失败,生成Commit(xid)或Rollback(xid)记录。 rocksdb 2pc参考这里 总结 myrocks在事务处理方面还有些不完善的地方,比如锁类型只有单一的X锁,不支持gap lock,纯内存锁占用内存等。 myrocks社区正在持续改进中,一起期待。
title: MySQL · myrocks · myrocks记录格式分析 author: 张远 概况 rocksdb作为KV存储引擎,那么myrocks记录最终会以kv的形式存储在rocksdb中。MySQL中的表一般由若干索引组成, 在innodb存储引擎中,每个索引对应一颗B树,而在rocksdb存储引擎中,索引对应于rocksdb中一段连续范围的数据。 具体来说,这个范围是此索引id和id+1之间的所有数据。如果表的所有索引都在一个column family, 那表的这些索引数据在物理上基本是连续的。 可以参考之前文章中的图示 myrocks记录格式 myrocks以索引为单位,将表的所有索引分别存储在rocksdb中。 根据索引的类型,myrocks记录的格式有所不同。下面以下表不同索引类型来分别介绍 CREATE TABLE t1(a INT, b VARCHAR(20), c char(5), d int, pk INT AUTO_INCREMENT, PRIMARY KEY(pk) comment 'cf_1', unique key idx2(b) comment 'cf_2') engine= rocksdb; INSERT INTO t1 (pk,a,b,c) VALUES (1,1,'bbbbbbbbbb','c'); 主键 主键索引记录kv结构如下 key: index_id, M(pk) value: unpack_info, NULL-bitmap,b,c,d key由索引id和主键组成。 index_id是索引的唯一标识占用4个字节,M(pk) 表示pk转化后的数据,此转化后的数据可以直接用于memcmp比较 rocksdb数据都是根据key排序的,为了便于比较,不同类型数据都会经过一些转化,转化后可以直接用于memcmp比较。关于memcmp转化,下一节会详细介绍 value存储unpack_info和非主键外的其他字段数据, Null-bitmap标识哪些字段为空。 unpace_info存储将M(pk)逆转化为pk的信息,如果不需要额外转换信息则unpace_info为null,此例中pk为int类型,不需要额外信息unpace_info为null 二级索引 idx2 二级索引记录kv结构如下 key: index_id,NULL-byte, M(b),M(pk) value: unpack_info key由index_id,二级索引键和主键组成, 其中NULL-byte表示b是否为空。pk为主键非空,所以不需要NULL-byte value只有unpack_info,表示 M(b),M(pk)逆转化信息,如果不需要额外转换信息则unpace_info为null。此例中b为varchar类型,需要额外信息unpace_info不为null 唯一索引和普通二级索引存储方式没有区别 联合索引每多一个字段会在字段前增加一个NULL-byte,来表示此字段是否为空 Memcomparable format rocksdb为了比较方便,将key字段转化为可以直接memcmp比较的形式。所以MyRocks 一般建议使用sensitive collations (latin1_bin, utf8_bin, binary).这样可以避免转化的开销。 整形 整形转化比较简单,但对于有符号类型需要特殊处理,如果直接存储会导致比较是负数比正数大。这里对有符号类型处理的方式是将符号位反转,这样正数就比负数大了,关键代码段如下 Field_long::make_sort_key: if (!table->s->db_low_byte_first) { if (unsigned_flag) to[0] = ptr[0]; else to[0] = (char) (ptr[0] ^ 128); /* Revers signbit */ to[1] = ptr[1]; to[2] = ptr[2]; to[3] = ptr[3]; } 字符型 char类型直接补空格 varchar类型为了节省空间处理起来就复杂多了以源码中的注释为例 const int VARCHAR_CMP_LESS_THAN_SPACES = 1; const int VARCHAR_CMP_EQUAL_TO_SPACES = 2; const int VARCHAR_CMP_GREATER_THAN_SPACES = 3; Example: if fpi->m_segment_size=5, and the collation is latin1_bin: 'abcd\0' => [ 'abcd' <VARCHAR_CMP_LESS> ]['\0 ' <VARCHAR_CMP_EQUAL> ] 'abcd' => [ 'abcd' <VARCHAR_CMP_EQUAL>] 'abcd ' => [ 'abcd' <VARCHAR_CMP_EQUAL>] 'abcdZZZZ' => [ 'abcd' <VARCHAR_CMP_GREATER>][ 'ZZZZ' <VARCHAR_CMP_EQUAL>] 字符串以m_segment_size分段存储,每段前m_segment_size-1个字符是内容,最后一个字符表示和空格比较,VARCHAR_CMP_EQUAL同时也表示字符串结束 例子中m_segment_size为5,实际实现上值为9 这里unpace_info会比较复杂,字符串collation不同unpace_info也不同,unpace_info需要保存collation之间的转换映射关系,具体可以查看函数(rdb_init_collation_mapping) rocksdb内部记录格式 前面为我们看到的是进入rocksdb之前记录的kv结构形式,实际上数据存储到rocksdb后key还要进一步封装进入rocksdb之前的key称为userkey, rocksdb内部称为internalkey internalkey=| User key (string) | sequence number (7 bytes) | value type (1 byte) | 其中sequence number 是记录序列号,每个记录sequence number根据是以记录进入rocksdb先后顺序递增的。sequence number是实现rocksdb事务处理的关键,这个下次讨论。 value type是记录的类型,put, merge,delete等 示例 以实例来说明比较直观,还是上面介绍的那个表,插入一条记录,来看看记录的具体结构 INSERT INTO t1 (pk,a,b,c) VALUES (1,1,'bbbbbbbbbb','c'); 查看主键index_id为260,二级索引index_id为261 select * from INFORMATION_SCHEMA.ROCKSDB_DDL where TABLE_NAME='t1'; TABLE_SCHEMA TABLE_NAME PARTITION_NAME INDEX_NAME COLUMN_FAMILY INDEX_NUMBER INDEX_TYPE KV_FORMAT_VERSION CF test t1 NULL PRIMARY 2 260 1 11 cf_1 test t1 NULL idx2 3 261 2 11 cf_2 主键记录 key value 二级索引记录 key value 这里包字段b的空格信息和collation转化映射关系。比较复杂,不详细展开,有兴趣的可以查看函数(rdb_init_collation_mapping)
rocksdb for rds 近期,我们将myrocks集成到了我们的 rds mysql5.6分支中,rds 5616又多了一个存储引擎rocksdb. 本文将对rds mysql5.6的rocksdb与innodb和tokudb做下对比测试 myrocks介绍参考上篇文章 测试环境 主机配置:CPU: Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHzOS kernel: Linux 2.6.32Memory: 512 GDisk: SSD 测试方法及参数设置 测试采用sysbench标准测试 Sysbench主要参数: --max-requests=1000000 --max-time=200 --oltp_tables_count=20 --oltp_table_size=200000 --report-interval=10 mysql参数 #sql层 sync_binlog=1 gtid_mode=on master_info_repository=table relay_log_info_repository=table #innodb 未压缩 innodb_buffer_pool_size=12G innodb_flush_log_at_trx_commit=1 #tokudb 默认zlib 压缩 tokudb_cache_size=12G tokudb_fsync_log_period=1000 #rocksdb rocksdb_max_open_files=-1 rocksdb_base_background_compactions=1 rocksdb_max_background_compactions=8 rocksdb_max_total_wal_size=4G rocksdb_max_background_flushes=4 rocksdb_block_size=16384 rocksdb_block_cache_size=12G rocksdb_lock_wait_timeout=2 rocksdb_default_cf_options=write_buffer_size=128m;target_file_size_base=32m;max_bytes_for_level_base=512m;level0_file_num_compaction_trigger=4;level0_slowdown_writes_trigger=10;level0_stop_writes_trigger=15;max_write_buffer_number=4;compression_per_level=kNoCompression:kNoCompression:kNoCompression:kZlibCompression:kZlibCompression:kZlibCompression:kZlibCompression;compression_opts=-14:6:0;block_based_table_factory={cache_index_and_filter_blocks=1;filter_policy=bloomfilter:10:false;whole_key_filtering=0};prefix_extractor=capped:12;level_compaction_dynamic_level_bytes=true;optimize_filters_for_hits=true 性能测试结果 insert.lua update_non_index.lua select.lua oltp.lua 压缩测试及结果 数据从第三方取的科研数据集,sql文件207G 导入innnodb(未压缩) 312G, tokudb(zlib) 61G, rocksdb(zlib) 56G rocksdb压缩比tokudb提高约10%
title: MySQL · myrocks · data dictionary 分析 author: 济天 data dictionary rocksdb作为mysql的一个新的存储引擎,在存储引擎层,会维护自动的元数据信息。在innodb存储引擎中,我们通过information_schema下的INNODB_SYS_DATAFILES,INNODB_SYS_TABLES,INNODB_SYS_INDEXES等表, 可以窥视innodb的元数据信息。同样,rocksdb通过information_schema下的ROCKSDB_INDEX_FILE_MAP,ROCKSDB_DDL,ROCKSDB_GLOBAL_INFO等表可以查看原数据信息。 show create table ROCKSDB_INDEX_FILE_MAP\G *************************** 1. row *************************** Table: ROCKSDB_INDEX_FILE_MAP Create Table: CREATE TEMPORARY TABLE `ROCKSDB_INDEX_FILE_MAP` ( `COLUMN_FAMILY` int(4) NOT NULL DEFAULT '0', `INDEX_NUMBER` int(4) NOT NULL DEFAULT '0', `SST_NAME` varchar(193) NOT NULL DEFAULT '', `NUM_ROWS` bigint(8) NOT NULL DEFAULT '0', `DATA_SIZE` bigint(8) NOT NULL DEFAULT '0', `ENTRY_DELETES` bigint(8) NOT NULL DEFAULT '0', `ENTRY_SINGLEDELETES` bigint(8) NOT NULL DEFAULT '0', `ENTRY_MERGES` bigint(8) NOT NULL DEFAULT '0', `ENTRY_OTHERS` bigint(8) NOT NULL DEFAULT '0' ) ENGINE=MEMORY DEFAULT CHARSET=utf8 show create table ROCKSDB_DDL\G *************************** 1. row *************************** Table: ROCKSDB_DDL Create Table: CREATE TEMPORARY TABLE `ROCKSDB_DDL` ( `TABLE_SCHEMA` varchar(193) NOT NULL DEFAULT '', `TABLE_NAME` varchar(193) NOT NULL DEFAULT '', `PARTITION_NAME` varchar(193) DEFAULT NULL, `INDEX_NAME` varchar(193) NOT NULL DEFAULT '', `COLUMN_FAMILY` int(4) NOT NULL DEFAULT '0', `INDEX_NUMBER` int(4) NOT NULL DEFAULT '0', `INDEX_TYPE` smallint(2) NOT NULL DEFAULT '0', `KV_FORMAT_VERSION` smallint(2) NOT NULL DEFAULT '0', `CF` varchar(193) NOT NULL DEFAULT '' ) ENGINE=MEMORY DEFAULT CHARSET=utf8 show create table ROCKSDB_GLOBAL_INFO\G *************************** 1. row *************************** Table: ROCKSDB_GLOBAL_INFO Create Table: CREATE TEMPORARY TABLE `ROCKSDB_GLOBAL_INFO` ( `TYPE` varchar(513) NOT NULL DEFAULT '', `NAME` varchar(513) NOT NULL DEFAULT '', `VALUE` varchar(513) NOT NULL DEFAULT '' ) ENGINE=MEMORY DEFAULT CHARSET=utf8 元数据详情 下面我盟来具体看看rocksdb维护了哪些元数据信息,从源码中看定义了以下类型,这些数据都以KV的形式存储在名叫__system__系统column family中。 // Data dictionary types enum DATA_DICT_TYPE { DDL_ENTRY_INDEX_START_NUMBER= 1, INDEX_INFO= 2, CF_DEFINITION= 3, BINLOG_INFO_INDEX_NUMBER= 4, DDL_DROP_INDEX_ONGOING= 5, INDEX_STATISTICS= 6, MAX_INDEX_ID= 7, DDL_CREATE_INDEX_ONGOING= 8, END_DICT_INDEX_ID= 255 }; DDL_ENTRY_INDEX_START_NUMBER 表和索引之间的映射关系 key: Rdb_key_def::DDL_ENTRY_INDEX_START_NUMBER(0x1) + dbname.tablename value: version + {global_index_id}*n_indexes_of_the_table INDEX_INFO 索引id和索引属性的关系 key: Rdb_key_def::INDEX_INFO(0x2) + global_index_id value: version, index_type, key_value_format_version index_type:主键/二级索引/隐式主键 key_value_format_version: 记录存储格式的版本 CF_DEFINITIONcolumn family属性 key: Rdb_key_def::CF_DEFINITION(0x3) + cf_id value: version, {is_reverse_cf, is_auto_cf} is_reverse_cf: 是否是reverse column family is_auto_cf: column family名字是否是$per_index_cf,名字自动由table.indexname组成 BINLOG_INFO_INDEX_NUMBER binlog位点及gtid信息,binlog_commit更新此信息 key: Rdb_key_def::BINLOG_INFO_INDEX_NUMBER (0x4) value: version, {binlog_name,binlog_pos,binlog_gtid} DDL_DROP_INDEX_ONGOING 等待删除的索引信息 key: Rdb_key_def::DDL_DROP_INDEX_ONGOING(0x5) + global_index_id value: version INDEX_STATISTICS 索引统计信息 key: Rdb_key_def::INDEX_STATISTICS(0x6) + global_index_id value: version, {materialized PropertiesCollector::IndexStats} MAX_INDEX_ID 当前的index id,每次创建索引index id都从这个获取和更新 key: Rdb_key_def::CURRENT_MAX_INDEX_ID(0x7) value: version, current max index id DDL_CREATE_INDEX_ONGOING 等待创建的索引信息 key: Rdb_key_def::DDL_CREATE_INDEX_ONGOING(0x8) + global_index_id value: version rocksdb DDL 实现 这里以建表和删表来举例 create table CREATE TABLE t1 (a INT, b CHAR(8), pk INT AUTO_INCREMENT ,PRIMARY KEY(pk), key idx1(b) comment 'cf_1') ENGINE=rocksdb; 通过以下步骤建表 * 创建column family (get_or_create_cf) primary key 存才default column family中,idx1存在cf_1中,需增加一条cf_1的,CF_DEFINITION的记录 {CF_DEFINITION(4)+cf_id(4)} ---> {CF_DEFINITION_VERSION(2)+cf_flags(4)} * 创建索引 两条索引 {INDEX_INFO(4)+cf_id(0)+index_id(260)---> { INDEX_INFO_VERSION_VERIFY_KV_FORMAT(1)+index_type(1)+kv_version(11) {INDEX_INFO(4)+cf_id(2)+index_id(261)---> { INDEX_INFO_VERSION_VERIFY_KV_FORMAT(2)+index_type(2)+kv_version(11) * 建立表和索引的映射 {DDL_ENTRY_INDEX_START_NUMBER(4)+dbname(test)+tablename(t1) } --> { DDL_ENTRY_INDEX_VERSION+cf_id(0)+index_id(260)+cf_id(2)+index_id(261} 以上信息通过同一batch一起存入rocksdb中。 另外,建索引时,会更新MAX_INDEX_ID信息,使用单独的batch写入,参考(Rdb_seq_generator::get_and_update_next_number) select * from INFORMATION_SCHEMA.ROCKSDB_DDL where table_name='t1'; +--------------+------------+----------------+------------+---------------+--------------+------------+-------------------+---------+ | TABLE_SCHEMA | TABLE_NAME | PARTITION_NAME | INDEX_NAME | COLUMN_FAMILY | INDEX_NUMBER | INDEX_TYPE | KV_FORMAT_VERSION | CF | +--------------+------------+----------------+------------+---------------+--------------+------------+-------------------+---------+ | test | t1 | NULL | PRIMARY | 0 | 260 | 1 | 11 | default | | test | t1 | NULL | idx1 | 2 | 261 | 2 | 11 | cf_1 | +--------------+------------+----------------+------------+---------------+--------------+------------+-------------------+---------+ select d.*,i.* from INFORMATION_SCHEMA.ROCKSDB_INDEX_FILE_MAP i,INFORMATION_SCHEMA.ROCKSDB_DDL d where i.INDEX_NUMBER=d.INDEX_NUMBER; +--------------+------------+----------------+------------+---------------+--------------+------------+-------------------+---------+---------------+--------------+------------+----------+-----------+---------------+---------------------+--------------+--------------+ | TABLE_SCHEMA | TABLE_NAME | PARTITION_NAME | INDEX_NAME | COLUMN_FAMILY | INDEX_NUMBER | INDEX_TYPE | KV_FORMAT_VERSION | CF | COLUMN_FAMILY | INDEX_NUMBER | SST_NAME | NUM_ROWS | DATA_SIZE | ENTRY_DELETES | ENTRY_SINGLEDELETES | ENTRY_MERGES | ENTRY_OTHERS | +--------------+------------+----------------+------------+---------------+--------------+------------+-------------------+---------+---------------+--------------+------------+----------+-----------+---------------+---------------------+--------------+--------------+ | test | t1 | NULL | PRIMARY | 0 | 260 | 1 | 11 | default | 0 | 260 | 000025.sst | 2 | 42 | 0 | 0 | 0 | 0 | | test | t1 | NULL | idx1 | 2 | 261 | 2 | 11 | cf_1 | 2 | 261 | 000027.sst | 2 | 42 | 0 | 0 | 0 | 0 | +--------------+------------+----------------+------------+---------------+--------------+------------+-------------------+---------+---------------+--------------+------------+----------+-----------+---------------+---------------------+--------------+--------------+ 2 rows in set (0.00 sec) 实际数据分布如下图: 元数据分布在系统column family __system__中primary key 分布在column family default中idx1 分布在column family cf_1中黄线之间代表数据分布的范围 drop table drop table t1; batch->Put 将索引加入到待删的kv队列中{DDL_DROP_INDEX_ONGOING(4)+cf_id(0)+index_id(260)} --> {DDL_DROP_INDEX_ONGOING_VERSION(2)}{DDL_DROP_INDEX_ONGOING(4)+cf_id(2)+index_id(261)} --> {DDL_DROP_INDEX_ONGOING_VERSION(2)}batch->Delete 删除表的映射关系表和索引的映射关系 后台线程再从待删的kv队列取出待删的索引,通过 DeleteFilesInRange, CompactRange 删除索引数据。
title: MySQL · 特性分析 · MyRocks简介 author: 济天 RocksDB是facebook基于LevelDB实现的,目前为facebook内部大量业务提供服务。经过facebook大量工作,将RocksDB作为MySQL的一个存储引擎移植到MySQL,称之为MyRocks。经过两年的发展,MyRocks已经比较成熟(RC阶段),现已进入了facebook MySQL的主分支了。MyRocks是开源的,参见git 。下面对MyRocks做一个简单介绍,不涉及源码。 RocksDB与innodb的比较 innodb空间浪费, B tree分裂导致page内有较多空闲,page利用率不高。innodb现有的压缩效率也不高,压缩以block为单位,也会造成浪费。 写入放大:innodb 更新以页为单位,最坏的情况更新N行会更新N个页。RocksDB append only方式 另外,innodb开启double write也会增加写入。 RocksDB对齐开销小:SST file (默认2MB)需要对齐,但远大于4k, RocksDB_block_size(默认4k) 不需要对齐,因此对齐浪费空间较少 RocksDB索引前缀相同值压缩存储 RocksDB占总数据量90%的最底层数据,行内不需要存储系统列seqid (innodb聚簇索引列包含trxid,roll_ptr等信息) 来看看facebook的测试数据 数据空间对比 QPS 写入放大对比 数据字典 数据字段信息保存在System Column Family (System CF) "__system__"中数据字段信息包括: 表信息,表名和index id的映射 索引信息,索引元数据信息和column family id。column family和index的对应关系 1:N column family,一些标记,比如reverse属性等 binlog信息 统计信息,每个SST file都自带统计信息(行数、实际大小等),在flush或compaction时更新统计信息,同时统计信息会汇总到数据字典统计信息表中。 以上信息可以通过information_schema查看,如RocksDB_ddl,RocksDB_index_file_map等 记录格式 RocksDB的行以key value的形式存储,和innodb类似,记录格式主键和二级索引也有区别 事务与锁 MyRocks也是基于行锁,锁信息都保存在内存中。 MyRocks也支持MVCC,MVCC通过快照的方式实现,类似于PostgreSQL。 MyRocks目前只支持两种隔离级别,RC和RR。 RR表现和innodb并不一样,RocksDB 的快照不是在事务开始的时候建立,而是延迟到第一次读的时候建立. 以下client1 MyRocks返回的是2,innodb返回1 <client 1> <client 2> CREATE TABLE t1(pk INT PRIMARY KEY); INSERT INTO t1 VALUES(1); SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN INSERT INTO t1 VALUES(2); SELECT COUNT(*) FROM t1; // MyRocks返回的是2,innodb返回1 RC表现也不一样,事务1大更新多行过程中,其他事务也可以更新事务还未更新到的行,事务1再更新时会失败。 复制 MyRocks也是通过binlog方式复制,由于binlog与RocksDB之间没有xa,异常crash可能丢数据,所以,MyRocks主备环境建议开启semi-sync. 由于gap lock支持不健全(仅primary key上支持), 使用statement方式复制会导致不一致,所有MyRocks建议使用行级复制。 备份恢复 支持MySQLdumup逻辑备份 #内部会执行以下语句 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION WITH CONSISTENT RocksDB SNAPSHOT; 同时有自动的物理备份工具MyRocks_hotbackup,但还不支持备份innodb; 也不支持增量备份。MyRocks_hotbackup支持流式备份 MyRocks_hotbackup--user=root --port=3306 --checkpoint_dir=/data/backup --stream=xbstream| ssh$dst‘xbstream–x /data/backup’ #内部建立硬链接方式备份数据SST files,checkpoint多次更新,只备份新的SST files, 因此WAL日志很少,恢复时apply log时间很短 SET GLOBAL RocksDB_create_checkpoint= /path/to/backup 一些优化 bloom filterbloom filter一般适用于等值查询 bloom filter信息存储在SST files中,大概占用2~3%的空间 如果大量查询返回空集建议开启bloom filter,如果结果每次都在最底层找到,可以设置optimize_filters_for_hits=true关闭bloom filter以节省空间。 数据加载 数据加载时可以忽略唯一性约束检查,分段自动提交,停写wal等。 以下是推荐的数据加载时的参数配置 rocksdb_skip_unique_check=1 rocksdb_commit_in_the_middle=1 rocksdb_write_disable_wal=1 rocksdb_max_background_flushes=40 rocksdb_max_background_compactions=40 rocksdb_default_cf_options=(in addition to existing parameters); write_buffer_size=128m;level0_file_num_compaction_trigger=4;level0_slowdown_writes_trigger=256;level0_stop_writes_trigger=256;max_write_buffer_number=16;memtable=vector:1024 rocksdb_override_cf_options=(in addition to existing parameters);__system__={memtable=skip_list:16} Reverse column families MyRocks擅长正向扫描,为了提高逆向扫描(ORDER BY DESC)的性能,MyRocks支持了Reverse column families。 在建表可以指定column family的reverse属性。 singleDelete 如果key不会重复put, delete操作可以直接删除put,而不是标记删除。singleDelete可以提高查询效率。 一些限制 MyRocks目前有以下一些限制 不支持分区表,Online ddl,外键,全文索引,空间索引,表空间transport gap lock支持不健全(仅primary key上支持), 使用statement方式复制会导致不一致 不支持select … in share mode 大小写敏感,不支持*_bin collation binlog与RocksDB之间没有xa,异常crash可能丢数据。所以,MyRocks一般开启semi-sync. 不支持savepoint order by 不比较慢 不支持MRR 暂不支持O_DIRECT innodb和RocksDB混合使用还不稳定
innodb行锁简介 行锁类型 LOCK_S:共享锁 LOCK_X: 排他锁 GAP类型 LOCK_GAP:只锁间隙 LOCK_REC_NO_GAP:只锁记录 LOCK_ORDINARY: 锁记录和记录之前的间隙 LOCK_INSERT_INTENTION: 插入意向锁,用于insert时检查锁冲突 每个行锁由锁类型和GAP类型组成例如:LOCK_X|LOCK_ORDINARY 表示对记录和记录之前的间隙加排他锁LOCK_S|LOCK_GAP 表示只对记录前的间隙加共享锁 锁的兼容性: 值得注意的是,持有GAP的锁(LOCK_GAP和LOCK_ORDINARY)与其他非LOCK_INSERT_INTENTION的锁都是兼容的,也就是说,GAP锁就是为了防止插入的。 详细可以参考之前的月报 innodb 锁分裂、继承与迁移 这里的锁分裂和合并,只是针对innodb行锁而言的,而且一般只作用于GAP类型的锁。 锁分裂 插入的记录的间隙存在GAP锁,此时此GAP需分裂为两个GAP lock_rec_inherit_to_gap_if_gap_lock: for (lock = lock_rec_get_first(block, heap_no); lock != NULL; lock = lock_rec_get_next(heap_no, lock)) { if (!lock_rec_get_insert_intention(lock) && (heap_no == PAGE_HEAP_NO_SUPREMUM || !lock_rec_get_rec_not_gap(lock))) { lock_rec_add_to_queue( LOCK_REC | LOCK_GAP | lock_get_mode(lock), block, heir_heap_no, lock->index, lock->trx, FALSE); } } 锁继承 删除的记录前存在GAP锁,此GAP锁会继承到要删除记录的下一条记录上 lock_rec_inherit_to_gap: for (lock = lock_rec_get_first(block, heap_no); lock != NULL; lock = lock_rec_get_next(heap_no, lock)) { if (!lock_rec_get_insert_intention(lock) && !((srv_locks_unsafe_for_binlog || lock->trx->isolation_level <= TRX_ISO_READ_COMMITTED) && lock_get_mode(lock) == (lock->trx->duplicates ? LOCK_S : LOCK_X))) { lock_rec_add_to_queue( LOCK_REC | LOCK_GAP | lock_get_mode(lock), heir_block, heir_heap_no, lock->index, lock->trx, FALSE); } } 锁迁移 B数结构变化,锁信息也会随之迁移. 锁迁移过程中也涉及锁继承。 锁分裂示例 锁分裂例子 set global tx_isolation='repeatable-read'; create table t1(c1 int primary key, c2 int unique) engine=innodb; insert into t1 values(1,1); begin; # supremum 记录上加 LOCK_X|LOCK_GAP 锁住(1~) select * from t1 where c2=2 for update; # 发现插入(3,3)的间隙存在GAP锁,因此给(3,3)加LOCK_X|LOCK_GAP锁。这样依然锁住了(1~) insert into t1 values(3,3); 这里如果插入(3,3)没有给(3,3)加LOCK_X|LOCK_GAP,那么其他连接插入(2,2)就可以成功 锁继承示例 隔离级别repeatable-read ===== RR ===== set global tx_isolation='repeatable-read'; create table t1(c1 int primary key, c2 int unique) engine=innodb; insert into t1 values(1,1),(2,2); #会话信息 session 1: | session 2: begin; | #(1,1) 加LOCK_X|LOCK_REC_NOT_GAP | delete from t1 where c1=1; | | | begin; | # (1,1)加LOCK_X|LOCK_ORDINARY 等待 | select * from t1 where c1 <= 1 for update; commit; | | #(1,1)被删除,purge清理delete mark时,(1,1)上的锁继承到(2,2)上,锁为LOCK_X|LOCK_GAP | #同时(1,1)上的锁都释放,session 2等待成功 验证:session 1执行insert into t1 values(1,1)发生了锁等待,说明(2,2)上有gap锁 mysql> select * from information_schema.innodb_locks; +------------------------+-------------+-----------+-----------+-----------------+------------+------------+-----------+----------+-----------+ | lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data | +------------------------+-------------+-----------+-----------+-----------------+------------+------------+-----------+----------+-----------+ | 16582717714:888654:4:3 | 16582717714 | X,GAP | RECORD | `cleaneye`.`t1` | c2 | 888654 | 4 | 3 | 2 | | 16582692183:888654:4:3 | 16582692183 | X,GAP | RECORD | `cleaneye`.`t1` | c2 | 888654 | 4 | 3 | 2 | +------------------------+-------------+-----------+-----------+-----------------+------------+------------+-----------+----------+-----------+ 2 rows in set (0.01 sec) 其中session 2 在(2,2) 加了LOCK_X|LOCK_GAP session 1 在(2,2) 加了LOCK_X|LOCK_GAP|LOCK_INSERT_INTENTION. LOCK_INSERT_INTENTION与LOCK_GAP冲突发生等待 隔离级别read-committed ===== RC ===== set global tx_isolation='read-committed'; drop table t1; create table t1(c1 int primary key) engine=innodb; insert into t1 values(1),(2); #会话信息 session 1 | session 2 begin; | #(1) 加LOCK_X|LOCK_REC_NOT_GAP | delete from t1 where c1=1; | | | begin; | #(1)加LOCK_S|LOCK_REC_NOT_GAP 等待 | select *from t1 where c1 <=1 lock in share mode; | COMMIT: | | #(1)被删除,purge清理delete mark时,(1)上的锁继承到(2)上,锁为LOCK_S|LOCK_GAP | # 同时(1)上的锁都释放,session 2等待成功 | 验证 session 1执行insert into t1 values(1)发生了锁等待,说明(2)上有gap锁 mysql> select * from information_schema.innodb_locks; +------------------------+-----------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+ | lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data | +------------------------+-----------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+ | 1705:32:3:3 | 1705 | X,GAP | RECORD | `test`.`t1` | PRIMARY | 32 | 3 | 3 | 2 | | 421590768578232:32:3:3 | 421590768578232 | S,GAP | RECORD | `test`.`t1` | PRIMARY | 32 | 3 | 3 | 2 | +------------------------+-----------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+ X.GAP insert 加锁LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION S.GAP 加锁LOCK_S|LOCK_GAP,记录(2)从删除的记录(1)继承过来的GAP锁 而实际在读提交隔离级别上,insert into t1 values(1)应该可以插入成功,不需要等待的,这个锁是否继承值得商榷。 来看一个插入成功的例子 ===== RC ===== set global tx_isolation='read-committed'; drop table t1; create table t1(c1 int primary key) engine=innodb; insert into t1 values(1),(2); # 会话信息 session 1 | session 2 | | begin; | #(1)加LOCK_S|LOCK_REC_NOT_GAP | # 查询结果为(1,2) | select *from t1 where c1 <=1 lock in share mode; | begin; | # 检查(1)上的锁与LOCK_X|LOCK_GAP|LOCK_INSERT_INTENTION | # 不冲突,插入成功 | insert into t1 values(0); | | #再次查询结果为(0,1,2) commit; | select *from t1 where c1 <=3 lock in share mode; 隔离级别serializable ===== SERIALIZABLE ===== set global tx_isolation='SERIALIZABLE'; drop table t1; create table t1(c1 int primary key) engine=innodb; insert into t1 values(1),(2); # 会话信息 session 1: | session 2: begin; | #(1) 加LOCK_X|LOCK_REC_NOT_GAP | delete from t1 where c1=1; | | | begin; | #(1)上加LOCK_S|LOCK_ORDINARY 等待 | select *from t1 where c1 <=1 ; | commit; | | | #(1)被删除,purge清理delete mark时,(1)上的锁继承到(2)上,锁为LOCK_S|LOCK_GAP | # 同时(1)上的锁都释放,session 2等待成功 | 验证方法同read-committed。 B树结构变化与锁迁移 B树节点发生分裂,合并,删除都会引发锁的变化。锁迁移的原则是,B数结构变化前后,锁住的范围保证不变。 我们通过例子来说明 节点分裂 假设原节点A(infimum,1,3,supremum) 向右分裂为B(infimum,1,supremum), C(infimum,3,supremum)两个节点 infimum为节点中虚拟的最小记录,supremum为节点中虚拟的最大记录 假设原节点A上锁为3上LOCK_S|LOCK_ORIDNARY,supremum为LOCK_S|LOCK_GAP,实际锁住了(1~) 锁迁移过程大致为: 1)将3上的gap锁迁移到C节点3上 2)将A上supremum迁移继承到C的supremum上 3)将C上最小记录3的锁迁移继承到B的supremum上 迁移完成后锁的情况如下(lock_update_split_right) B节点:suprmum LOCK_S|LOCK_GAP C节点:3 LOCK_S|LOCK_ORINARY, suprmum LOCK_S|GAP 迁移后仍然锁住了范围(1~) 节点向左分裂情形类似 节点合并 以上述节点分裂的逆操作来讲述合并过程 B(infimum,1,supremum), C(infimum,3,supremum)两个节点,向左合并为A节点(infimum,1,3,supremum) 其中B,C节点锁情况如下 B节点:suprmum LOCK_S|LOCK_GAP C节点:3 LOCK_S|LOCK_ORINARY, suprmum LOCK_S|GAP 迁移流程如下(lock_update_merge_left): 1)将C节点锁记录3迁移到B节点 2)将B节点supremum迁移继承到A的supremum上 迁移后仍然锁住了范围(1~) 节点向右合并情形类似 节点删除 如果删除节点存在左节点,则将删除节点符合条件的锁,迁移继承到左节点supremum上 否则将删除节点符合条件的锁,迁移继承到右节点最小用户记录上 参考lock_update_discard 锁继承相关的BUG bug#73170 二级唯一索引失效。这个bug触发条件是删除的记录没有被purge, 锁还没有被继承的。如果锁继承了就不会出现问题。 bug#76927 同样是二级唯一索引失效。这个bug是锁继承机制出了问题。 以上两个bug详情参考这里
代价模型 mysql 5.7代价计算相对之前的版本有较大的改进。例如 代价模型参数可以动态配置,可以适应不同的硬件 区分考虑数据在内存和在磁盘中的代价 代价精度提升为浮点型 jion计算时不仅要考虑condition,还要考虑condition上的filter,具体参见参数condition_fanout_filter 5.7 在代价类型上分为io,cpu和memory, 5.7的代价模型还在完善中,memory的代价虽然已经收集了,但还没有没有计算在最终的代价中。5.7 在源码上对代价模型进行了大量重构,代价分为server层和engine层。server层主要是cpu的代价,而engine层主要是io的代价。5.7 引入了两个系统表mysql.server_cost和mysql.engine_cost来分别配置这两个层的代价。 server_cost row_evaluate_cost (default 0.2) 计算符合条件的行的代价,行数越多,此项代价越大 memory_temptable_create_cost (default 2.0) 内存临时表的创建代价 memory_temptable_row_cost (default 0.2) 内存临时表的行代价 key_compare_cost (default 0.1) 键比较的代价,例如排序 disk_temptable_create_cost (default 40.0) 内部myisam或innodb临时表的创建代价 disk_temptable_row_cost (default 1.0) 内部myisam或innodb临时表的行代价 由上可以看出创建临时表的代价是很高的,尤其是内部的myisam或innodb临时表。 engine_cost io_block_read_cost (default 1.0) 从磁盘读数据的代价,对innodb来说,表示从磁盘读一个page的代价 memory_block_read_cost (default 1.0) 从内存读数据的代价,对innodb来说,表示从buffer pool读一个page的代价 目前io_block_read_cost和memory_block_read_cost默认值均为1,实际生产中建议酌情调大memory_block_read_cost,特别是对普通硬盘的场景。 代价配置 cost参数可以通过修改mysql.server_cost和mysql.engine_cost来实现。初始这两个表中的记录cost_value项均为NULL, 代价值都取上两节介绍的初始值。当修改cost_value为非NULL时,代价值按设定的值计算。修改方法如下: # 修改io_block_read_cost值为2 UPDATE mysql.engine_cost SET cost_value = 2.0 WHERE cost_name = 'io_block_read_cost'; #FLUSH OPTIMIZER_COSTS 生效,只对新连接有效,老连接无效。 FLUSH OPTIMIZER_COSTS; 另外,在主备环境下,修改cost参数时主备都要修改。因为mysql.server_cost和mysql.engine_cost的更新不会参与复制。 代价分析示例 初始化数据 create table t1(c1 int primary key, c2 int unique,c3 int) engine=innodb; let $loop=100; while($loop) { eval insert into t1(c1,c2,c3) values($loop, $loop+1, $loop+2); dec $loop; } set optimizer_trace = "enabled=on"; cost参数都取默认值,以下示例中会用到row_evaluate_cost(0.2),io_block_read_cost(1.0),io_block_read_cost(1.0),memory_block_read_cost(1.0) 示例1 以下语句选择覆盖索引c2 explain select c1,c2 from t1 where c2 > 10; id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t1 NULL range c2 c2 5 NULL 91 100.00 Using where; Using index 查看optimizer_trace, 可以看出全表扫描代价为23.1,通过c2上的索引扫描代价为19.309, 最后选择c2上的索引扫描。 "rows_estimation": [ { "table": "`t1`", "range_analysis": { "table_scan": { "rows": 100, "cost": 23.1 }, "potential_range_indexes": [ { "index": "PRIMARY", "usable": false, "cause": "not_applicable" }, { "index": "c2", "usable": true, "key_parts": [ "c2" ] } ], "best_covering_index_scan": { "index": "c2", "cost": 21.109, "chosen": true }, "setup_range_conditions": [ ], "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" }, "analyzing_range_alternatives": { "range_scan_alternatives": [ { "index": "c2", "ranges": [ "10 < c2" ], "index_dives_for_eq_ranges": true, "rowid_ordered": false, "using_mrr": false, "index_only": true, "rows": 91, "cost": 19.309, "chosen": true } ], "analyzing_roworder_intersect": { "usable": false, "cause": "too_few_roworder_scans" } }, "chosen_range_access_summary": { "range_access_plan": { "type": "range_scan", "index": "c2", "rows": 91, "ranges": [ "10 < c2" ] }, "rows_for_plan": 91, "cost_for_plan": 19.309, "chosen": true } } } ] }, { "considered_execution_plans": [ { "plan_prefix": [ ], "table": "`t1`", "best_access_path": { "considered_access_paths": [ { "rows_to_scan": 91, "access_type": "range", "range_details": { "used_index": "c2" }, "resulting_rows": 91, "cost": 37.509, "chosen": true } ] }, "condition_filtering_pct": 100, "rows_for_plan": 91, "cost_for_plan": 37.509, "chosen": true } ] 全表扫描的代价23.1 包括io和cpu的代价 test_quick_select: double scan_time= cost_model->row_evaluate_cost(static_cast<double>(records)) + 1; Cost_estimate cost_est= head->file->table_scan_cost(); cost_est.add_io(1.1);//这里加1.1应该是个调节值 cost_est.add_cpu(scan_time); 其中io代价table_scan_cost会根据buffer pool大小和索引大小来估算page in memory和in disk的比例,分别算出代价。 handler::table_scan_cost() ha_innobase::scan_time()*table->cost_model()->page_read_cost(1.0);//1*1=1 //其中scan_time计算数据所占page数, page_read_cost计算读取单个page的代价 buffer_block_read_cost(pages_in_mem) + io_block_read_cost(pages_on_disk); io代价为1+1.1=2.1 cpu代价为row_evaluate_cost double row_evaluate_cost(double rows) const { DBUG_ASSERT(m_initialized); DBUG_ASSERT(rows >= 0.0); return rows * m_server_cost_constants->row_evaluate_cost(); // 100 * 0.2(row_evaluate_cost)=20; } cpu代价为20+1=21; 最终代价为2.1+21=23.1 c2索引扫描代价19.309 同样也分为io和cpu代价 multi_range_read_info_const: *cost= index_scan_cost(keyno, static_cast<double>(n_ranges), static_cast<double>(total_rows)); cost->add_cpu(cost_model->row_evaluate_cost(static_cast<double>(total_rows)) + 0.01); io代价 1.0987925356750823*1=1.0987925356750823 index_scan_cost: const double io_cost= index_only_read_time(index, rows) * //估算index占page个数 = 1.0987925356750823 table->cost_model()->page_read_cost_index(index, 1.0); //根据buffer pool大小和索引大小来估算page in memory和in disk的比例,计算读一个page的代价。 = 1 cpu代价91*0.2+0.01=18.21 cost->add_cpu(cost_model->row_evaluate_cost( static_cast<double>(total_rows)) + 0.01); //这里根据过滤条件算出的total_rows为91 最终代价1.0987925356750823+18.21=19.309 示例2 以下语句选择了全表扫描 explain select * from t1 where c2 > 10; id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t1 NULL ALL c2 NULL NULL NULL 100 91.00 Using where 查看optimizer_trace, 可以看出全表扫描代价为23.1,通过c2上的索引扫描代价为110.21, 最后选择全表扫描。 "rows_estimation": [ { "table": "`t1`", "range_analysis": { "table_scan": { "rows": 100, "cost": 23.1 }, "potential_range_indexes": [ { "index": "PRIMARY", "usable": false, "cause": "not_applicable" }, { "index": "c2", "usable": true, "key_parts": [ "c2" ] } ], "setup_range_conditions": [ ], "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" }, "analyzing_range_alternatives": { "range_scan_alternatives": [ { "index": "c2", "ranges": [ "10 < c2" ], "index_dives_for_eq_ranges": true, "rowid_ordered": false, "using_mrr": false, "index_only": false, "rows": 91, "cost": 110.21, "chosen": false, "cause": "cost" } ], "analyzing_roworder_intersect": { "usable": false, "cause": "too_few_roworder_scans" } } } } ] }, { "considered_execution_plans": [ { "plan_prefix": [ ], "table": "`t1`", "best_access_path": { "considered_access_paths": [ { "rows_to_scan": 100, "access_type": "scan", "resulting_rows": 91, "cost": 21, "chosen": true } ] }, "condition_filtering_pct": 100, "rows_for_plan": 91, "cost_for_plan": 21, "chosen": true } ] }, 全表扫描代价23.1 同上一节分析 c2索引扫描代价为110.21 上一节通过c2索引扫描代价为19.309,因为是覆盖索引不需要回表,所以代价较少。而此例是需要回表的。 multi_range_read_info_const: *cost= read_cost(keyno, static_cast<double>(n_ranges), static_cast<double>(total_rows)); cost->add_cpu(cost_model->row_evaluate_cost( static_cast<double>(total_rows)) + 0.01); io代价需回表 read_cost: //92*1=92 const double io_cost= read_time(index, static_cast<uint>(ranges) static_cast<ha_rows>(rows)) * table->cost_model()->page_read_cost(1.0); read_time: //91+1=92 virtual double read_time(uint index, uint ranges, ha_rows rows) { return rows2double(ranges+rows); } 这里回表时计算代价为每行代价为1,默认认为回表时每行都对于聚集索引的一个page. io代价为92 cpu代价为91*0.2+0.01=18.21 cost->add_cpu(cost_model->row_evaluate_cost( static_cast<double>(total_rows)) + 0.01); 最后代价为92+18.21=110.21 总结 5.7 代价模型优化还在持续改进中,相信后续的版本会越来越好。代价的参数的配置需谨慎,需要大量的测试和验证。
背景 innodb buffer pool作为innodb最重要的缓存,其缓存命中率的高低会直接影响数据库的性能。因此在数据库发生变更,比如重启、主备切换实例迁移等等,innodb buffer poll 需要一段时间预热,期间数据库的性能会受到明显影响。另外mysql 5.7以前innodb buffer pool缓存大小修改不是动态的,重启才能生效。因此innodb buffer pool的预热和innodb buffer pool大小的动态修改,对性能要求较高的应用来说是不错的特性,下面我来看看这两个特性的具体实现。 buffer pool 预热 mysql 5.6以后支持buffer pool预热功能。引入了以下参数, 参数具体含义参见官方文档 innodb_buffer_pool_load_now innodb_buffer_pool_dump_now innodb_buffer_pool_load_at_startup innodb_buffer_pool_dump_at_startup innodb_buffer_pool_filename buffer pool预热分为dump过程和load过程,均由后台线程buf_dump_thread完成。比如用户发起set命令 set global innodb_buffer_pool_dump_now=on; set global innodb_buffer_pool_load_now=on; set 命令会立刻返回,具体操作由buf_dump_thread来实现。 dump 过程 锁buf_pool 遍历LRU链表,将(space, pageno) 先收集到数组 释放锁 再将数据写入innodb_buffer_pool_filename定有的文件中 load过程 从文件读入数组 按(space,pageno)排序数据 依次同步读取页到buffer pool中 dump过程一般比较快,而load过程相对要慢些。 通过Innodb_buffer_pool_dump_status、Innodb_buffer_pool_load_status可查看dump/load的状态 另外5.7引入了performance_schema.events_stages_current来显示load进度,每load 32M会更新一条进度信息 select * from performance_schema.events_stages_current; THREAD_ID 19 EVENT_ID 1367 END_EVENT_ID NULL EVENT_NAME stage/innodb/buffer pool load SOURCE buf0dump.cc:619 TIMER_START 33393877311000 TIMER_END 33398961258000 TIMER_WAIT 5083947000 WORK_COMPLETED 0 WORK_ESTIMATED 1440 NESTING_EVENT_ID NULL NESTING_EVENT_TYPE NULL WORK_ESTIMATED表示总page数WORK_COMPLETED表示当前已load page数 dump文件的数据格式如下 #cat ib_buffer_pool |more 0,7 0,1 0,3 0,2 0,4 0,11 0,5 0,6 dump文件比较简单,我们可以编辑此文件来预加载指定page,比较灵活。 buffer pool 动态调整大小 5.7 开始支持buffer pool 动态调整大小,每个buffer_pool_instance都由同样个数的chunk组成(chunks数组), 每个chunk内存大小为innodb_buffer_pool_chunk_size(实际会偏大5%,用于存放chuck中的block信息)。buffer pool以innodb_buffer_pool_chunk_size为单位进行动态增大和缩小。调整前后innodb_buffer_pool_size应一直保持是innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍数。 同样的buffer pool动态调整大小由后台线程buf_resize_thread,set命令会立即返回。通过InnoDB_buffer_pool_resize_status可以查看调整的运行状态。 resize流程 如果开启了AHI,需禁用AHI 如果是收缩内存 计算需收缩的chunk数, 从chunks开始尾部删除指定个数的chunk. 锁buf_pool 从free_list中摘除待删chunk的page放入待删链表buf_pool->withdraw 如果待删chunk的page为脏页,则刷脏 重新加载LRU中要删除的页,从LRU中摘除,重新从free列表获取page老的page放入待删链表buf_pool->withdraw 释放buffer pool锁 如果需收缩的chunk pages没有收集全,重复2-6 开始resize 锁住所有instance的buffer_pool,page_hash 收缩pool:以chunk为单位释放要收缩的内存 清空withdraw列表buf_pool->withdraw 增大pool:分配新的chunk 重新分配buf_pool->chunks 如果改变/缩小超过2倍,会重置page hash,改变桶大小 释放buffer_pool,page_hash锁 如果改变/缩小超过2倍,会重启和buffer pool大小相关的内存结构,如锁系统(lock_sys_resize),AHI(btr_search_sys_resize), 数据字段(dict_resize)等 如果禁用了AHI,此时开启 由上可以看出,扩大内存比缩小内存相对容易些。缩小内存时,如果遇到有事务一直未提交且占用了待收缩的page时,导致收缩一直重试,error log会打印这种重试信息,包含可能引用此问题的事务信息。为了避免频繁重试,每次重试的时间间隔会指数增长。 以上步骤中resize阶段buffer pool会不可用,此阶段会锁所有buffer pool, 但此阶段都是内存操作,时间比较短。收缩内存阶段耗时可能会很长,也有一定影响,但是每次都是以instance为单位进行锁定的。总的来说,buffer pool 动态调整大小对应用的影响并不大。 重新加载LRU中要删除的页的影响 search 过程中btr游标保存的page可能重新加载过,自适应哈希保存的root page也可能重新加载过, 都需要重新读取。 总结 buffer pool 预热 和buffer pool 动态调整大小,这两功能相辅相承的。buffer pool 动态调整大小只适用于实例在主机本地升级的情况,如果用户修改buffer pool大小,同时涉及跨机迁移,那么buffer pool 预热功能就排上用场了。另外buffer pool 动态调整尽量在业务低锋时进行。
如果有备份,可以通过备份集和binlog恢复
RDS mysql 使用myisam表是存在锁表问题的,建议使用innodb表
rds mysql 不支持访问mysql系统数据库的权限
RDS mysql 只读实例是没有备份,建议使用主实例的备份集
建议坚持用户的权限
建议使用rds mysql 5.7, 是建立在ECS上的
如果是备份集所需的binlog,不建议删除
RDS mysql 可以从控制台创建数据库,
另外高权限账号可以通过命令行方式创建数据库,具体可以参考https://help.aliyun.com/document_detail/26130.html
题主您好,
具体需要看硬件配置,参数设置和查询计划