• [热门活动] 《数据库原理》MOOC第3季 春日携礼归来,MOOC免费学, 分享得更多!
    《数据库原理▪GaussDB云数据库》MOOC第3季 春日携礼归来【活动时间】2022年3月15日 - 7月15日【活动主题】《数据库原理▪GaussDB云数据库》MOOC—第3季 春日携礼归来,MOOC免费学, 分享得更多【活动内容】 活动一: MOOC免费学Step1:点击报名:《数据库原理•GaussDB云数据库》MOOCStep2:报名完成后,将MOOC报名学习页面截图,盖楼打卡即可Tips:每个ID抢楼不得连续超过3楼,总楼层数不得超过10楼盖楼内容:华为云账号+MOOC报名学习截图;活动二:分享得更多转发下方活动海报至朋友圈,+转发语:《数据库原理▪GaussDB云数据库》MOOC—第3季 春日携礼归来:课程由8位博导团队+华为云数据库技术专家共同打造,免费学习还有好礼哦,可别错过啦!公开可见保持2小时以上,将转发内容的朋友圈截图回复至下方评论区!Tips:一定要公开可见保持2小时哦~【中奖规则】踩中幸运中奖楼层【奖励】活动1【奖励】活动2【盖楼超50层】楼层5%、15%、25%、35%、45%、55%、65%、75%、85%、95%500码豆/人1000码豆/人楼层8%、38%、68%、88%雨伞/高硼玻璃杯/GaussDB字母笔价值69元的书籍《数据库原理及应用》 其他活动开年采购季,8000元见面礼+0门槛抽奖,云数据库包年18元起!【参与有奖】GaussDB(for MySQL) 关键特性发布和技术解读什么是码豆?会员中心入口:https://devcloud.huaweicloud.com/bonususer/home码豆奖励活动规则:1)码豆可在码豆会员中心兑换实物礼品。2)码豆只能用于会员中心的礼品兑换,不得转让,具体规则请到会员中心阅读“码豆规则”。3)为保证码豆成功发放,如果修改过账号名还请向工作人员提供修改前后的账号名。  活动规则 1)请务必使用个人账号参与活动(IAM、企业账号等账号参与无效)。2)所有获得华为奖项的获奖用户,请于获奖后3日内完成实名认证,否则视为放弃奖励。3)本次活动如一个实名认证对应多个账号,只有一个账号可领取奖励;一个实名认证账号只能对应一个收件人,如同一账号填写多个不同收件人或不同账号填写同一收件人,均不予发放奖励。4)活动结束且用户填写完成领奖信息后,15个工作日内发放奖品,发放时间根据实际情况动态调整,如有延期敬请见谅。5)本活动最终解释权归华为云所有。
  • [优秀博文] 如何从头到脚彻底解决一个MySQL Bug?华为云数据库高级专家带你看
    说明:本文中的MySQL,如果不做特殊说明,指的是开源社区版MySQL。 华为云数据库新版本在发布之前,会面临一系列严苛的测试规则,除了要求通过MySQL的所有测试用例之外,还需要通过由华为百万级更丰富、更贴近用户业务场景的测试用例构筑的测试防护网,以此充分验证新版本是否满足用户经典场景的稳定性。   正是在这样严苛的验证过程中,我们发现了MySQL的一个潜在Bug。   Bug描述测试环境: 基于相同的测试用例、数据集,分别测试MySQL 8.0.22, MySQL 8.0.26,与华为云GaussDB(for MySQL)的返回结果。  测试语句: select subq_0.c2 as c0 from (select ref_6.C_STATE asc0, case whenref_6.C_PHONE is not NULL then ref_5.C_ID else ref_5.C_ID end asc1, floor( ref_3.c_id)as c2 from sqltester.t0_hash_partition_p1_view as ref_0 right join sqltester.t4 as ref_1 on (EXISTS ( select ref_1.c_middle as c0 from sqltester.t1 as ref_2 where ((false) and ((true) or (true))) or (false) )) innerjoin sqltester.t0_range_key_subpartition_sub_view as ref_3 on(EXISTS ( select ref_0.c_credit as c0, ref_1.c_street_1 as c1, ref_4.c_credit_lim as c2, ref_3.c_credit as c3 from sqltester.t0_hash_partition_p1 as ref_4 where true )) left joinsqltester.t10 as ref_5 innerjoin sqltester.t11 as ref_6 on(true) on (((pi() isnot NULL)) and (false)) where (((ref_5.C_D_ID isnot NULL) or(ref_3.c_middle is not NULL)) )) as subq_0 where (EXISTS ( select subq_0.c0 as c0, pi() as c1, ref_11.c_street_1 as c2, ref_11.c_discount as c3, pi() as c4 from sqltester.t0_partition_sub_view_mixed_001 as ref_11)) group by 1 order by 1;返回结果: 如下图所示,MySQL 8.0.22、MySQL8.0.26与华为云GaussDB(for MySQL)的返回结果不一致,也就是说产生了Bug,如下图红色部分。 Bug分析首先确定哪一个执行结果是正确的。当前这个语句执行的execution plan是Hash Join,而MySQL8.0里面引入了Hash Join,由此推论开源版本可能存在问题。接下来我们从MySQL成熟版本以及非MySQL数据库两个方面来进行验证。   验证过程:使用相对成熟的版本MySQL 5.6进行验证,返回结果与GaussDB(for MySQL)相同,但与MySQL 8.0不同。使用PostgreSQL进行验证,执行结果与MySQL 5.6、GaussDB(for MySQL)相同,但与MySQL 8.0及更高版本不同。  由此可以确定:MySQL 8.0以及更高版本存在问题。   那么,是什么原因引起了这一Bug呢? 1.  首先精简查询,以方便后面分析。经过多次验证,将查询简化如下: SELECT count(*) FROM (SELECT 1 FROM sqltester.t4 AS ref_1 INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS (SELECT 1 FROMsqltester.t4 AS ref_4 WHERE TRUE )) LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE) WHERE (((ref_5.C_D_ID IS NOT NULL) OR (ref_3.c_middle IS NOT NULL))))AS subq_0 执行计划如下: -> Aggregate: count(0) (cost=2.75 rows=0) -> Filter: ((ref_5.C_D_ID is not null) or(ref_3.c_middle is null)) (cost=2.75 rows=0) -> Inner hash join(no condition) (cost=2.75 rows=0) -> Index scan on ref_3 using ndx_c_middle (cost=0.13 rows=50) -> Hash -> Inner hash join (no condition) (cost=1.50 rows=0) -> Index scan on ref_1 using ndx_c_id (cost=6.25 rows=50) -> Hash -> Left hash join (no condition) (cost=0.25 rows=0) -> Limit: 1 row(s) (cost=312.50 rows=1) ->Index scan on ref_4 using ndx_c_id (cost=312.50 rows=50) -> Hash -> Zero rows (Impossible filter) (cost=0.00..0.00 rows=0)从上面的执行计划可以看出,ref_5被优化器进行了优化,转换成了Zero rows,而且ref_5是Left Hash Join的内表。作为Left Join的内表,如果内表没有匹配条件的记录(这里已经是Impossible条件了,也就是说连接条件始终是False),则需要内表生成NULL行来和外表进行外表连接。   2.  在MySQL 8.0.22版本上执行问题查询,语句和执行结果如下: SELECT count(*) FROM (SELECT 1 FROM sqltester.t4 AS ref_1 INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS (SELECT 1 FROM sqltester.t4 AS ref_4 WHERE TRUE )) LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE) WHERE (((ref_5.C_D_ID IS NOT NULL) or(ref_3.c_middle IS NOT NULL))))AS subq_0; + + | count(*) | + + | 2500 | + + 1 row in set (0.00 sec)3.  对问题查询进行修改:去掉Where条件里面的另外一个条件(ref_3.c_middleis NULL)。 现在Where条件只包含了(ref_5.C_D_IDIS NOT NULL)一个条件,要求当前查询过滤掉所有ref_5没有匹配的连接记录。   则SQL语句和执行结果如下: SELECT count(*) FROM (SELECT 1 FROM sqltester.t4 AS ref_1 INNER JOIN sqltester.t4 AS ref_3 ON (EXISTS (SELECT 1 FROM sqltester.t4 AS ref_4 WHERE TRUE )) LEFT JOIN sqltester.t10 AS ref_5 ON (FALSE) WHERE (((ref_5.C_D_ID IS NOT NULL))))assubq_0; + + | count(*) | + + | 2500 | + + 1 row in set (0.01 sec)对比修改前后的语句和执行结果可以看出:执行结果与条件(ref_3.c_middle is NULL)没有关系,只与(ref_5.C_D_ID IS NOT NULL)这个条件有关。正常情况下对ref_5表来说,因为是Impossible条件,所以ref_5被优化成了Zero rows。那么如果只剩(ref_5.C_D_ID IS NOT NULL)这个条件,正常的结果应该是空集(count返回0)。但现在开源版本的结果集却不是,这再次说明了开源版本出现了问题。   对于Left Join来说,如果Join条件不匹配,内表需要设置为NULL行来连接外表。而这里执行计划使用的是Zero rows,也就是说MySQL 8.0使用的是ZeroRowsIterator来执行的。执行器需要调用ZeroRowsIterator::SetNullRowFlag来设置Nullflag。   4.  通过gdb来查看设置是否正确: Breakpoint 1, ZeroRowsIterator::SetNullRowFlag(this=0x7f92a413d510, is_null_row=false) at /mywork/mysql-sql/sql/basic_row_iterators.h:398 398 assert(m_child_iterator != nullptr); (gdb) n 399 m_child_iterator->SetNullRowFlag(is_null_row); (gdb) s std::unique_ptr<RowIterator,Destroy_only<RowIterator> >::operator-> (this=0x7f92a413d520) at/opt/simon/taurus/mysql-root/src/tools/gcc-9.3.0/include/c++/9.3.0/bits/unique_ptr.h:355 355 returnget(); (gdb) fin Run till exit from #0 std::unique_ptr<RowIterator,Destroy_only<RowIterator> >::operator-> ( this=0x7f92a413d520) at/opt/simon/taurus/mysql-root/src/tools/gcc-9.3.0/include/c++/9.3.0/bits/unique_ptr.h:355 ZeroRowsIterator::SetNullRowFlag (this=0x7f92a413d510,is_null_row=false) at/home/simon/mywork/mysql-sql/sql/basic_row_iterators.h:399 399 m_child_iterator->SetNullRowFlag(is_null_row); Value returned is $1 = (RowIterator *) 0x7f92a413d4d0 (gdb) s TableRowIterator::SetNullRowFlag (this=0x7f92a413d4d0,is_null_row=false) at/home/simon/mywork/mysql-sql/sql/records.cc:229 229 if(is_null_row) { (gdb) n 232 m_table->reset_null_row(); (gdb) 234 }从上面的gdb来看,断点处利用ZeroRowsIterator::SetNullRowFlag将表的Nullflag设置为了False。后面的gdb信息也证明了这一点。   可以确定,导致此Bug的原因是:ZeroRowsIterator::SetNullRowFlag设置为False这里是不正确的。因为如果把ZeroRowsIterator::SetNullRowFlag设置为False,那就会导致内表为ZeroRows的Left Join生成内表非NULL的结果集。 如何解决既然上面的Bug分析已经非常清楚了,那么修复起来也就比较简单了。只需要将ZeroRowsIterator::SetNullRowFlag始终设置为True就可以了。因为ZeroRowIterator只能产生两种结果,一种是空集,另一种就是作为外连接的内表产生NULL行。 对MySQL-8.0.26进行修复后,执行结果如下: 从返回的结果可以看出查询结果正确,也就是说问题得到了修复。   为了保障华为云GaussDB产品的可靠性,每一款产品发布前都要通过多轮严苛的测试用例。在发现问题后,华为云数据库团队以缜密的思路去逐步确定问题、分析问题,并第一时间修复Bug,解决问题,以确保客户的数据安全和业务结果的准确性。华为云数据库团队荟聚了业内50%以上的数据库内核专家,以专业技术实时保障客户业务安全,助力企业业务安全上云!
  • [版主精选] 亿级月活沙盒平台《迷你世界》背后的黑科技
    年少时期,我们有过许多梦,想仗剑天涯,想修种藩篱,想成为建筑大师,想改变世界……无论梦想最终是否如愿,那段独属我们的青春欢乐时光将永远熠熠生辉。今天,有一款面向青少年的游戏创造了很多虚拟世界,来看看有你年少时的梦吗?迷你创想(深圳)科技有限公司(简称:迷你创想)是一家致力于打造优秀的青少年创意实践平台的企业,其倾力打造的《迷你世界》是一款国产沙盒创意平台,主要通过方块组合自由创造等方式,引导用户在平台上创作虚拟作品。用户、开发者和虚拟场景共同构建了活跃的内容生态,而不断完善的低门槛多样化的强大工具,让《迷你世界》里的“虚拟积木”摆脱了现实的种种限制,用户能够实现各种**行空的场景化搭建。稳定性与弹性两手抓,支持大规模全真虚拟互动《迷你世界》自2015年上线至今,单月月活跃用户已突破1亿,影响力巨大。其旗下虚拟偶像“花小楼”发行的单曲,总播放量超过2亿次。迷你云服是支撑《迷你世界》的服务平台,提供更为稳定的联机服务,以增强用户联机的游戏体验。随着用户剧增和访问量的加大,迷你云服在稳定性和弹性扩容方面面临挑战:稳定性:游戏业务对数据库的稳定性要求极高,系统不稳定将直接导致用户流失。弹性扩容:用户数据过百亿,业务高峰期资源要快速下发升配,以保障玩家体验。面对挑战,一场平台升级之旅就此开启。华为云数据库团队围绕客户需求,针对业务特点因地制宜打造了一套合理高效的游戏数据库方案,从部署架构到分布式设计,基于RDS和分布式数据库中间件DDM提供了高性能、稳定可靠、弹性扩容等能力,提升用户联机的游戏体验,为迷你创想1亿+用户畅玩游戏保驾护航。主备架构优化:使用RDS云盘主备实例,该实例经过海量用户生产系统充分验证,在数据库性能和稳定性方面极具优势,可轻松应对海量访问压力。分库分表改造:采用DDM+RDS做分库分表改造,使用hash算法针对用户的唯一键进行业务拆分,由原先的集中式修改为分布式,均衡负载以提升数据库性能。节点快速扩容:针对客户业务场景,提供DDM计算节点快速扩容、RDS节点快速规格变更等能力,解决客户高峰期资源快速下发问题。华为云数据库全力负责《迷你世界》的底层资源保障,实现了2个月内完成游戏内测至上线全流程,提升了业务上线效率;在游戏运行期间,支撑了海量游戏用户同时在线,为花小楼音乐会等高峰场景的稳定运行提供了坚实的保障。创作开发工具便捷化,驱动创新内容生产作为国内TOP1的沙盒创意类游戏,迷你世界拥有的繁荣UGC生态,迷你世界上有7000万开发者每天在不断地创作新内容。相应的,游戏也需要不断给开发者提供各种工具,让开发者们发挥想象,进行场景、人物的创作。 在内容创作方面,迷你世界推出了自定义模型编辑器用来构建3D角色的动作能力,模型编辑器里搭载了华为终端云的AR ENGINE。基于这个编辑器,玩家可以在游戏中对他人的特定动作进行拍摄,将复杂的三维**动作转化成骨骼动态,上传到游戏中映射到游戏角色。游戏角色可以随玩家做出奔跑,跳跃,转身,挥手的动作,虚实结合的同时大大增加了游戏趣味性。那在这其中,AR Engine简化了自定义动作创作,仅用一台华为手机即可完成所有内容创作,创作时间从天级缩短到分钟级,内容创作难度大大降低,引发了虚实角色创作的热潮。海量内容审核智能化,构建健康游戏环境开发者们每天创造了大量的内容,这些内容高效审核是一个很让人头疼的问题。为了确保内容场景的合规性,《迷你世界》每天需要进行大量且细致的内容审核,对审核准确率和实时性要求极高。为此,迷你创想构建了一套严格的内容审核流程,用户上传内容先经本地词库过滤,后续通过华为云AI智能审核与人工复查,极大提高不良内容审核效率和准确率,为平台用户提供健康、清洁的游戏环境。 技术升级促进游戏内容和玩法创新,相信未来,迷你创想将和华为云一起为游戏玩家提供更流畅、有趣的游戏体验,让我们拭目以待!【重磅活动推荐】开年采购享好价!华为云数据库MySQL、GaussDB(for Redis)18元/年限量秒杀,不限新老用户包年3折起。活动期间还有8000元大礼包、满额赠华为笔记本、0门槛抽奖等多重福利!https://activity.huaweicloud.com/dbs_Promotion/index.html
  • [技术干货] 使用Cloud DB构建APP 快速入门-快应用篇
    快应用的数据将会储存在云侧,本地不会缓存数据。在进行数据管理操作时,您将会直接操作云侧数据。快应用 SDK将会为您的应用与云数据库的通讯和通讯安全提供保障。使用Cloud DB构建快应用,需要完成以下准备工作:您已经在开发者联盟官网注册帐号并通过实名认证,详细请参见帐号注册认证。完成开发环境的搭建,包括在PC上安装快应用IDE、在测试手机安装快应用加载器,详细参见安装开发工具。在AppGallery Connect控制台上完成快应用的创建,详细参见创建快应用。使用快应用IDE生成证书指纹,并在AppGallery Connect控制台上完成指纹的配置,详细参见生成、配置指纹证书。您已经获取到示例代码,请从示例代码获取。启用服务使用Cloud DB服务前,您需要先启用服务。登录AppGallery Connect网站,选择“我的项目”。在项目列表页面中选择项目,单击项目下需要启用云数据库服务的应用。在导航树上选择“构建 > 云数据库”。单击“立即开通”,开通云数据库服务。            (可选)如您还未选择数据处理位置,需要您先设置数据处理位置,具体操作请参见设置数据处理位置。服务初始化成功后,即启用云数据库服务成功。新增和导出对象类型您需要基于AppGallery Connect控制台创建对象类型,请您遵循操作步骤创建示例中涉及的对象类型,并导出用于快应用开发的json格式和js格式对象类型文件。不允许修改导出的json格式和js格式文件,否则会导致数据同步功能异常。登录AppGallery Connect网站,选择“我的项目”。在项目列表页面中选择项目,单击项目下需要创建对象类型的应用。在导航树上选择“构建 > 云数据库”。单击“新增”,进入创建对象类型页面。            输入对象类型名为“BookInfo”后,单击“下一步”。单击,新增如下字段后,单击“下一步”。表1 字段定义表字段名称类型主键非空加密默认值idInteger√√––bookNameString––––authorString––––priceDouble––––publisherString––––publishTimeDate––––shadowFlagBoolean–––true单击,设置索引名为“bookName”,索引字段为“bookName”后,单击“下一步”。按照如下要求设置各角色权限后,单击“下一步”。单击“确定”。创建完成后返回对象类型列表中,可以查看已创建的对象类型。单击“导出”。                                                    导出“json格式”和“js格式”文件,导出的文件在后续步骤用于添加至本地开发环境。导出json格式文件选择“json格式”。单击“导出”。导出js格式文件选择“js格式”。选择js文件类型,选择“js”。单击“导出”。新增存储区您可基于AppGallery Connect控制台在云侧创建数据存储区,请您遵循操作步骤创建一个存储区名称为“QuickStartDemo”的存储区。登录AppGallery Connect网站,选择“我的项目”。在项目列表页面中选择项目,单击项目下需要创建存储区的应用。在导航树上选择“构建 > 云数据库”。选择“存储区”页签。单击“新增”,进入创建存储区页面。                         输入存储区名称为“QuickStartDemo”。单击“确定”。创建完成后返回存储区列表中,可以查看已创建的存储区。配置开发环境创建一个快应用项目。打开快应用IDE,在欢迎页单击登录,在跳转的网页中登录注册的帐号。登录成功后,IDE的导航选择“新建工程 > 快应用-云开发”,选择关联应用、模板,设置项目路径后,单击“确定”。关联应用:选择之前在AppGallery Connect上创建的快应用。项目路径:存放Serverless项目工程的路径。                                                                                          新建项目结构主要由quickapp.config.json、client、cloudfunctions组成。|-- client 用于存放标准快应用代码|-- |-- src 标准快应用源码目录|-- |-- |-- Common 快应用公共资源文件目录|-- |-- |-- Hello 快应用主文件目录|-- |-- |-- app.ux 快应用入口文件|-- |-- |-- manifest.json 快应用配置文件|-- |-- package.json 快应用代码依赖云函数SDK的配置文件|-- cloudfunctions 云函数目录,该目录下可以创建多个云函数目录|-- quickapp.config.json serverless项目的配置文件                                                             quickapp.config.json针对serverless项目的配置信息如下:quickappRoot:表示client端快应用代码目录,快应用serverless项目只会编译打包该目录下的内容。                                                          cloudfunctionRoot:表示serverless项目云函数目录,该目录主要存放项目所有云函数代码。配置应用信息。登录AppGallery Connect网站,选择“我的项目”。在项目列表页面中选择项目,单击项目下的应用。选择“常规”页签,下载配置文件“agconnect-services.json”,并拷贝到您的快应用项目src目录下。在hello.ux文件中添加agconnect-services.json文件引用。const context = require('../agconnect-services.json');集成Cloud DB SDK。执行如下命令,安装Cloud DB JavaScript SDK云数据库服务模块到您的项目中。npm install --save @agconnect/database在您的项目中导入database组件。import "@agconnect/database";在manifest.json文件的 features属性中增加如下配置。{"name": "system.cipher"}添加对象类型文件在开发应用时,可直接将AppGallery Connect控制台上导出的json格式和js格式文件添加至本地开发环境,并通过AGConnectCloudDB类中的createObjectType()方法实现对象类型的定义和创建。您在进行本地应用开发时,无需再次创建对象类型。将已在AppGallery Connect控制台上导出的全部json格式和js格式文件添加至本地开发环境。初始化Cloud DB,通过AGConnectCloudDB类中的createObjectType()方法实现对象类型的定义和创建,详细请参见初始化。初始化在添加对象类型文件后,您就可以使用云数据库进行应用开发。您开发应用时,需要先执行初始化操作,初始化AGConnectCloudDB、创建Cloud DB zone和对象类型。通过initialize()初始化AGConnectCloudDB。AGConnectCloudDB.initialize(context);通过getInstance()方法获取AGConnectCloudDB实例,并使用createObjectType()创建对象类型。const schema = require('./BookInfo.json'); agcCloudDB = AGConnectCloudDB.getInstance(); agcCloudDB.createObjectType(schema);打开Cloud DB zone。const config = new CloudDBZoneConfig('QuickStartDemo'); cloudDBZone = await agcCloudDB.openCloudDBZone(config);写入数据在本节主要介绍如何在应用程序中进行数据写入操作,如下所示,使用executeUpsert()实现数据的写入。async function executeUpsert (book) { try { const cloudDBZoneResult = await cloudDBZone.executeUpsert(book); console.log('upsert' + cloudDBZoneResult + 'record' ); } catch (e) { console.log('upsert failed with reason'); conso.log(e); } }查看数据获取数据变化用户在应用界面中新增的数据,将会被存储在云侧。在端侧注册数据变化侦听器,当云侧数据发生变化时,端侧能够感知数据变化。通过查询条件与subscribeSnapshot()方法组合使用,可以指定侦听对象,当侦听对象的数据发生变化时,端侧会收到数据变化通知,并生成新的快照,触发用户回调。async function subscribeSnapshot () { const query = CloudDBZoneQuery.where(BookInfo); query.equalTo('shadowFlag', true); try { const onSnapshotListener = { onSnapshot: (snapshot, e) => { if (e !== null && e !== undefined && e.code !== AGConnectCloudDBExceptionCode.Ok) { console.log('subscribeSnapshot error'); console.log(e); } return snapshot.getSnapshotObjects(); } }; const listenerHandler = await cloudDBZone.subscribeSnapshot(query, onSnapshotListener); console.log(listenerHandler); } catch (e) { console.log('subscribeSnapshot error'); console.log(e); return null; } } function subscribeBookList() { subscribeSnapshot().then(snapshot => { snapshot.getDeletedObjects(); const resultList = snapshot.getSnapshotObjects(); console.log(resultList); }) }数据查询和排序通过executeQuery()实现异步方式查询数据。async function executeQuery() { try { const query = CloudDBZoneQuery.where(BookInfo); const snapshot = await cloudDBZone.executeQuery(query); const resultArray = snapshot.getSnapshotObjects(); console.log(resultArray); } catch(e) { console.log(e); } }通过查询与limit()方法组合,实现限制查询数据显示条数的功能;与orderByAsc()方法或者orderByDesc()方法组合来实现数据的排序功能。async function executeQueryWithOrder (object) { const query = CloudDBZoneQuery.where(BookInfo); if (object.name.length > 0) { query.equalTo('bookName', object.name); } if (parseFloat(object.minPrice) > 0) { query.greaterThanOrEqualTo('price', parseFloat(object.minPrice)); } if (parseFloat(object.maxPrice) > 0 && parseFloat(object.maxPrice) > parseFloat(object.minPrice)) { query.lessThanOrEqualTo('price', parseFloat(object.maxPrice)); } if (parseInt(object.bookCount) > 0) { query.limit(parseInt(object.bookCount)); } query.orderByAsc('id'); try { const snapshot = await cloudDBZone.executeQuery(query); console.log('resultArray'); console.log(snapshot.getSnapshotObjects()); return snapshot.getSnapshotObjects(); } catch (e) { console.log('query failed with reason'); console.log(e); } }
  • [行业资讯] 华为云携手甘肃省医疗保障局,以数字科技为智慧医疗注入新动能
    自古以来,甘肃便是我国中医药文化的重要发源地,孕育了医药鼻祖伏羲、中华医祖岐伯,也孕育了这片土地上世世代代的子孙。几千年后,经过历史长河的沉淀,甘肃仍然在向中医药强省稳步迈进。医疗是民生之需。2019年,国家医疗保障局颁布《关于医疗保障信息化工作的指导意见》,要建立全国统一的医保信息系统,搭建国家和省两级医保信息平台,提高全国医保的标准化、智能化,信息化。作为拥有深厚医药文化底蕴的甘肃自然一马当先,加速搭乘“数字化”列车,大力推进中医药强省建设,为百姓就医谋便利。医保升级好事多磨不过,一个参保人数超2642万的大省,说起医保升级,并没有想象中那么容易。按照国家医保局的建设要求,应对旧医保平台进行优化和升级,而老医保平台还在使用传统数据库,扩展性较弱。同时,新平台要求业务请求端到端时延应该缩短至秒级,而老医保平台结算类业务延迟高、响应慢,办事效率尤待加强。从业务端来看,省级医保平台业务面向全省参保人口,并发量高、数据量大,业务数据量能达到百TB级别,因此数据库还需要同时满足性能和存储容量的双重高要求。同样重要的是,医保业务属于类金融类的民生业务,其系统的稳定性、可靠性不可忽视,需要具备故障自动切换的高可用能力和数据完整灾备能力。华为云数据库对症下药2020年9月,华为云凭借自身在云计算、大数据、计算、存储等方面的产品优势,全份额中标甘肃省医疗保障信息平台建设项目,为该平台提供了强可靠的支撑和保障。华为云分布式数据库DDM+RDS for MySQL承载起了甘肃省医保信息平台数十个业务系统的数字底座。能成为中医药强省医保数字化改革的信赖之选,华为云数据库有强大优势。首先,华为云数据库基于华为累积多年的数据库研发经验而打造,大幅优化了传统数据库,提供更高可用、更高性能、更高安全的数据库服务,完全满足国家医保局对于数据库的使用要求。不仅如此,华为云数据库还具备分库分表的能力,可通过增加节点实现性能和容量的线性增长,而且单集群数据库性能可达百万级QPS,最高可实现PB级的存储容量,足以轻松应对平台业务并发量高、数据量大的问题。更重要的是,数据库基于其高可用架构,能够实力保障在平台出现故障时做到同数据中心故障自动秒级切换,并且通过数据复制服务DRS实现跨Region数据完整灾备,遇到故障时能够游刃有余地保障业务的连续性和安全性。新平台面貌焕然一新在华为云数据库的高效支撑下,甘肃省医疗保障信息平台于2021年5月在兰州市全面上线运行。自上线以来,平台一直处于平稳运行状态。从新平台的实际运行效果来看,其门诊结算系统响应速度从单次平均5秒提高到了单次平均0.9秒,入院办理系统响应速度从单次平均3秒提高到单次平均0.4秒,住院结算系统响应速度从单次平均10秒提高到单次平均1.9秒。业务请求端到端时延得到了可视化的提高,大幅度减少了群众就医结算的等待时间。此外,为了顺利完成项目交付,华为云数据库团队驻扎到现场,深入甘肃省医疗保障局的实际业务场景,联合ISV厂家一起投入到项目中,并制定了业务库表设计规范、分库分表最佳实践、业务系统上线变更规范、数据库运维管理规范等。华为云数据库团队严格遵循国家医疗保障信息平台有关标准规范和要求,为国家医保信息平台在全国其他地方的落地提供了借鉴经验。【重磅活动推荐】开年采购享好价!华为云数据库MySQL、GaussDB(for Redis)18元/年限量秒杀,不限新老用户包年3折起。活动期间还有8000元大礼包、满额赠华为笔记本、0门槛抽奖等多重福利!https://activity.huaweicloud.com/dbs_Promotion/index.html
  • [交流吐槽] 【数据库】云数据库rds是什么意思?有什么优势?
    云数据库rds是什么意思?有什么优势?这两个问题是很多运维人员都想了解的问题,因为大多运维人员思想还停留在自建数据库这种意识上,并没有真正地了解到云数据库的优势,今天跟小编小编一起来简单了解一下。云数据库rds是什么意思?云数据RDS是关系型数据库服务(Relational Database Service)的简称,是一种即开即用、稳定可靠、可弹性伸缩的在线数据库服务;具有多重安全防护措施和完善的性能监控体系,并提供专业的数据库备份、恢复及优化方案,使您能专注于应用开发和业务发展。云数据库rds有什么优势?1、轻松部署使用云数据库RDS可以让用户轻松地完成数据库的申请和创建,只需要在几分钟内就可以部署投用,并且用户还可以通过rds控制台来对所有的实例进行统一管理,轻松完成部署。2、高可靠性在高可靠性方面表现得也很有优势,是可以进行故障自动单点切换,可以进行自动备份,能够确保高可用性和安全性。3、低成本用户可以根据自己的实际需求来支付费用,可以用最低的价格得到自己想要的一整套专业的数据库支持服务。4、高安全提供白名单访问策略;可自行设置允许访问的IP及IP段,有效防止黑客扫描端口进行服务器攻击等等。不同厂商的安全策略不同。知识拓展1:华为云-云数据库RDS简单介绍华为云-云数据库RDS服务具有完善的性能监控体系和多重安全防护措施,并提供了专业的数据库管理平台, 让用户能够在云上轻松的进行设置和扩展云数据库。通过云数据库RDS服务的管理控制台,用户无需编程就可以执行所有必需任务,简化运营流程,减少日常运维工作量,从而专注于开发应用和业务发展。知识拓展2:行云管家数据库审计支持云厂商RDS行云管家数据库审计全面支持Oracle、MySQL、SQLServer等主流数据库的审计和保护功能,同时适用于本地数据库与云厂商RDS。简单来说就是行云管家数据库审计支持云厂商RDS。
  • [热门活动] 开年采购季,8000元见面礼+0门槛抽奖,云数据库包年18元起!
    活动时间:2020/3/1~3/31活动主题:开年采购季,华为云数据库三重福利送不停活动内容:开年采购季,0元免费试用,云数据库包年18元起,还有“8000元见面礼+抽奖” 福利哦!【福利一】华为云数据库0元免费试用注册华为云账号,即可免费试领取华为云数据库领取方式:https://activity.huaweicloud.com/dbs_Promotion/index.html【福利二】8000元见面礼+0门槛抽奖开年采购享好价,集云福卡兑好礼。1、登陆即可领8000元开年红包2、集云福,领虎年福礼,最高可兑换华为笔记本3、企业注册并实名认证即可参与抽奖,最高抽奖可得WATCH GT典藏版4、消费满额送华为笔记本点击进入:https://activity.huaweicloud.com/newyear_promotion/index.html【福利三】开年采购享好价,爆款云数据库包年18元起云数据库MySQL、企业级Redis包年18元起点击进入:https://activity.huaweicloud.com/dbs_Promotion/index.html其他活动【参与有奖】GaussDB(for MySQL) 关键特性发布和技术解读《数据库原理》MOOC第3季 春日携礼归来,MOOC免费学, 分享得更多!
  • [热门活动] 【华为伙伴暨开发者大会·技术宝典】10分钟快速入门RDS
    参与活动前请先报名华为伙伴暨开发者大会技术前瞻【点击报名】活动时间:5月23日-6月30日活动内容:《10分钟快速入门RDS》沙箱实验活动奖励:奖励一:勇往直前进阶等级 LV+1 级奖励二:在所有完成技术宝典任一任务的用户中抽取40个幸运奖,奖品为文件收纳包。参与方式:Step1:点击就可参与《10分钟快速入门RDS》沙箱实验Step2:参与回复,截本实验进度100%结果到下方评论区回复方式:华为云账号+实验进度100%的截图活动规则:1. 回复非示例要求图片或其他无效信息,视为无效楼层,并取消抽奖资格。2. 全部活动结束后,将符合抽奖条件的用户名单导入至巨公摇号平台(https://www.jugong.wang/random-portal/)内,抽取40个幸运奖,并截屏公示抽奖过程。如您不同意此抽奖规则,请勿参加本次活动。Tips:1. 请务必使用个人账号参与活动(IAM、企业账号等账号参与无效)。2. 所有获得奖项的获奖用户,请于获奖后3日内完成实名认证,否则视为放弃奖励。3. 收货信息填写说明:1)为保证您顺利领取活动奖品,请您提前填写奖品收货信息,如您没有填写,视为放弃奖励。收货信息请【点击此处填写】2)填写时间截至2022年7月10日3)在华为伙伴暨开发者社区活动中完成一次填写即可。我们最终将会按照您填写的信息发放奖励。4. 活动规则请戳https://bbs.huaweicloud.com/forum/thread-180208-1-1.html
  • [数据库] 【第36课】如何在DRS上搭建MySQL异地单主灾备
    当某一地区故障而导致业务不可用,可以使用数据复制服务DRS推出的灾备场景,为业务连续性提供数据库的同步保障。本节小课为您介绍RDS for MySQL实例通过DRS服务搭建异地单主灾备的过程。实现原理RDS跨Region容灾实现原理说明:在两个数据中心独立部署RDS for MySQL实例,通过DRS服务将生产中心MySQL库中的数据同步到灾备中心MySQL库中,实现RDS for MySQL主实例和跨Region灾备实例之间的实时同步。更多关于MySQL实例灾备须知请单击这里了解。一、生产中心RDS for MySQL实例准备创建MySQL业务实例,选择已规划的业务实例所属VPC,并为实例绑定EIP。1.   登录华为云控制台。2.   单击管理控制台左上角的,选择区域“华北-北京一”。3.   单击左侧的服务列表图标,选择“数据库 > 云数据库 RDS”。4.   单击“购买数据库实例”。5.   填选实例信息后,单击“立即购买”。 选择引擎版本信息。选择规格信息。选择已规划的网络信息。设置管理员密码。6.   为创建的RDS实例绑定弹性公网IP。二、灾备中心RDS for MySQL实例准备创建MySQL灾备实例,选择已规划的灾备实例所属VPC。1.   单击管理控制台左上角的,选择区域“华北-北京四”。2.   单击左侧的服务列表图标,选择“数据库 > 云数据库 RDS”。3.   单击“购买数据库实例”。4.   填选实例信息后,单击“立即购买”。选择灾备实例引擎版本信息选择灾备实例规格信息选择灾备实例已规划的网络信息设置灾备实例管理员密码三、搭建容灾关系创建DRS灾备实例,创建时选择灾备中心创建的RDS for MySQL实例。1.   在“华北-北京四”区域,单击左侧的服务列表图标,选择“数据库 > 数据复制服务 DRS”。2.   选择左侧“实时灾备管理”,单击右上角“创建灾备任务”。3.   灾备类型选择“单主灾备”,灾备关系选择“本云为备”,灾备数据库实例选择在“华北-北京四”新创建的MySQL灾备实例,单击“下一步”,开始创建灾备实例。设置基本信息设置灾备实例信息4.   返回“实时灾备管理”页面,可以看到新创建的灾备实例。创建完成5.   在灾备实例上,单击“编辑”。6.   根据界面提示,将灾备实例的弹性公网IP加入生产中心MySQL实例所属安全组的入方向规则,选择TCP协议,端口为生产中心MySQL实例的端口号。添加安全组规则      源库信息中的“IP地址或域名”填写生产中心MySQL实例绑定的EIP,“端口”填写生产中心MySQL实例的端口号。测试通过后,单击“下一步”,直到任务启动,任务状态为“灾备中”。编辑灾备任务灾备中四、容灾切换生产中心数据库故障时,需要手动将灾备数据库实例切换为可读写状态。切换后,将通过灾备实例写入数据,并同步到源库。1.   生产中心源库发生故障,例如:源库无法连接、源库执行缓慢、CPU占比高。2.   收到SMN邮件通知。邮件通知3.   查看灾备任务时延异常。时延异常4.   用户自行判断业务已经停止。具体请参考如何确保业务数据库的全部业务已经停止。5.   选择“批量操作 > 主备倒换”,将灾备实例由只读状态更改为读写状态。主备倒换倒换完成6.   在应用端修改数据库连接地址后,可正常连接数据库,进行数据读写。
  • [数据库] 【第35课】其他云MySQL迁移到RDS for MySQL实例
    数据复制服务(Data Replication Service,简称DRS)支持将其他云MySQL数据库的数据迁移到本云云数据库MySQL。通过DRS提供的实时迁移任务,实现在数据库迁移过程中业务和数据库不停机,业务中断时间最小化。本节小课为您介绍将其他云MySQL迁移到RDS for MySQL实例。部署架构更多关于MySQL数据迁移须知请单击这里了解。一.  创建RDS for MySQL实例创建MySQL业务实例,选择已规划的业务实例所属VPC和安全组。1.   登录华为云控制台。2.   单击管理控制台左上角的,选择区域“华南-广州”。3.   单击左侧的服务列表图标,选择“数据库 > 云数据库 RDS”。4.   单击“购买数据库实例”。5.   配置实例名称和实例基本信息。      6.   选择实例规格。      7.   选择实例所属的VPC和安全组、配置数据库端口。      8.   配置实例密码。      9.   单击“立即购买”。10.   返回云数据库实例列表。当RDS实例运行状态为“正常”时,表示实例创建完成。二、其他云MySQL实例准备帐号权限要求当使用DRS将其他云MySQL数据库的数据迁移到本云云数据库MySQL实例时,帐号权限要求如下表所示,授权的具体操作请参考授权操作。迁移帐号权限迁移类型全量迁移全量+增量迁移源数据库(MySQL)SELECT、SHOW VIEW、EVENT。SELECT、SHOW VIEW、EVENT、LOCK TABLES、REPLICATION SLAVE、REPLICATION CLIENT。网络设置源数据库MySQL实例需要开放外网域名的访问。白名单设置其他云MySQL实例需要将目标端DRS迁移实例的弹性公网IP添加到其网络白名单中,目标端DRS迁移实例的弹性公网IP在创建完DRS迁移实例后可以获取到,确保源数据库可以与DRS实例互通,各厂商云数据库添加白名单的方法不同,请参考各厂商云数据库官方文档进行操作。三、创建DRS迁移任务1.   登录华为云控制台。2.   单击管理控制台左上角的,选择区域,即为目标实例所在的区域。3.   单击左侧的服务列表图标,选择“数据库 > 数据复制服务 DRS”。4.   单击“创建迁移任务”。5.   填写迁移任务参数。      配置迁移任务名称。            填写迁移数据并选择模板库。这里的目标库选择创建的RDS实例。      6.   单击“下一步”。      迁移实例创建中,大约需要5-10分钟。迁移实例创建完成后可获取弹性公网IP信息。      7.   配置源库信息和目标库数据库密码。      8.   单击“下一步”。9.   在“迁移设置”页面,设置流速模式、迁移用户和迁移对象。流速模式:不限速迁移对象:全部迁移10.   单击“下一步”,在“预检查”页面,进行迁移任务预校验,校验是否可进行任务迁移。查看检查结果,如有不通过的检查项,需要修复不通过项后,单击“重新校验”按钮重新进行迁移任务预校验。预检查完成后,且所有检查项结果均成功时,单击“下一步”。11.   参数对比。若您选择不进行参数对比,可跳过该步骤,单击页面右下角“下一步”按钮,继续执行后续操作。若您选择进行参数对比,对于常规参数,如果源库和目标库存在不一致的情况,建议将目标数据库的参数值通过“一键修改”按钮修改为和源库对应参数相同的值。12.   单击“提交任务”。      返回DRS实时迁移管理,查看迁移任务状态。      启动中状态一般需要几分钟,请耐心等待。            当状态变更为“已结束”,表示迁移任务完成。四、确认迁移结果确认迁移结果可参考如下两种方式:DRS会针对迁移对象、用户、数据等维度进行对比,从而给出迁移结果,详情参见在DRS管理控制台查看迁移结果。直接登录数据库查看库、表、数据是否迁移完成。手工确认数据迁移情况,详情参见在RDS管理控制台查看迁移结果。在DRS管理控制台查看迁移结果1.   登录华为云控制台。2.   单击管理控制台左上角的,选择目标区域。3.   单击左侧的服务列表图标,选择“数据库 > 数据复制服务 DRS”。4.   单击DRS实例名称。5.   单击“迁移对比”,选择“对象级对比”,单击“开始对比”,校验数据库对象是否缺失。6.   选择“数据级对比”,单击“创建对比任务”,查看迁移的数据库和表内容是否一致。7.   选择“用户对比”,查看迁移的源库和目标库的账号和权限是否一致。在RDS管理控制台查看迁移结果1.    登录华为云控制台。2.   单击管理控制台左上角的,选择目标区域。3.   单击左侧的服务列表图标,选择“数据库 > 云数据库 RDS”。4.   单击迁移的目标实例的操作列的“更多 > 登录”。      5.   在弹出的对话框中输入密码单击“测试连接”检查。6.   连接成功后单击“登录”。7.   输入实例密码,登录RDS实例。8.   查看并确认目标库名和表名等。确认相关数据是否迁移完成。
  • [数据库] 【第34课】如何将自建MySQL迁移到RDS for MySQL
    数据复制服务DRS支持将本地MySQL数据库的数据迁移至RDS for MySQL。通过DRS提供的实时迁移任务,实现在数据库迁移过程中业务和数据库不停机,业务中断时间最小化。本节小课为您介绍将自建MySQL迁移到RDS for MySQL的过程。部署架构本示例中,数据库源端为ECS自建MySQL,目的端为RDS实例,同时假设ECS和RDS实例在同一个VPC中。更多关于MySQL数据迁移须知请单击这里了解。一.  创建ECS(MySQL服务器)并安装MySQL社区版购买并登录弹性云服务器,用于安装MySQL社区版。1.   登录华为云控制台。2.   单击管理控制台左上角的,选择区域“华东-上海一”。3.   单击左侧的服务列表图标,选择“计算 > 弹性云服务器 ECS”。4.   单击“购买云服务器”。5.   配置弹性云服务器参数,填选信息后,单击“立即购买”。            选择镜像和磁盘规格。      6.   在创建的ECS上单击“远程登录”。选择“CloudShell登录”。7.   输入root用户密码,完成登录。8.   执行如下命令,创建mysql文件夹。      mkdir /mysql9.   执行如下命令,查看数据盘信息。      fdisk -l10.   执行如下命令,初始化数据盘。      mkfs.ext4 /dev/vdb11.   执行如下命令,挂载磁盘。      mount /dev/vdb /mysql12.   执行如下命令,查看磁盘是否挂在成功。      df -h      当回显出现 /dev/vdb的数据时,表示挂载成功。13.   依次执行如下命令,创建文件夹并切换至install文件夹。      mkdir -p /mysql/install/data      mkdir -p /mysql/install/tmp      mkdir -p /mysql/install/file      mkdir -p /mysql/install/log      cd /mysql/install14.   下载依赖包并上传到/mysql/install/file命令。15.   下载并安装社区版MySQL。二. 创建ECS并安装MySQL客户端1.   创建MySQL客户端的弹性云服务器。确保和MySQL服务器所在ECS配置成相同Region、相同可用区、相同VPC、相同安全组。不用购买数据盘。云服务器名配置为:ecs-client。其他参数同MySQL服务器的ECS配置。2.   下载并安装MySQL客户,请参考安装MySQL客户端。三.  创建RDS实例本章节介绍创建RDS实例,该实例选择和自建MySQL服务器相同的VPC和安全组。1.   登录华为云控制台。2.   单击管理控制台左上角的,选择区域“华东-上海一”。3.   单击左侧的服务列表图标,选择“数据库 > 云数据库 RDS”。4.   填选信息后,单击“购买数据库实例”。            选择实例规格。            选择实例所属的VPC和安全组、配置数据库端口。            配置实例密码。      四. 创建DRS迁移任务介绍自建MySQL服务器上的loadtest数据库迁移到RDS MySQL实例的详细操作过程。1.   登录华为云控制台。2.   单击管理控制台左上角的,选择区域“华东-上海一”。3.   单击左侧的服务列表图标,选择“数据库 > 数据复制服务 DRS”。4.   单击“创建迁移任务”。5.   填写迁移任务参数,直到任务创建完成。      配置迁移任务名称。            填写迁移数据并选择模板库。这里的目标库选择创建的RDS实例。      6.   配置源库信息和目标库数据库密码。      7.   单击“下一步”,直到迁移任务提交成功,数据迁移完成。
  • [参赛经验分享] 2019年华为云数据库TaurusDB挑战赛冠军赛题总结
    1.概述            华为云TaurusDB是华为云自主研发的最新一代云原生分布式数据库,采用计算与存储分离、日志即数据的架构设计,实现数据库性能方面的大幅提升,具备极致可靠、极致性价比、多维拓展、完全可信等诸多特性。            赛题以此为背景,目标是设计一个计算存储分离的KV存储引擎。首先回顾下赛题,本次大赛的目的是设计一个KV存储引擎,复赛加入了计算存储分离的要求,引入了网络通信。同时,赛题要求程序能保证在应用层崩溃的情况下的数据安全性,追求更高的性能。因此,大赛主要考察点有5点:即读写吞吐量最大化;支持异常退出的缓冲设计;高效紧凑的索引结构;合理的缓存预读机制;以及高速稳定的RPC设计。主要考察点集中在文件IO和网络IO上,需要选手对操作系统底层有较多的了解。2.测试为了达到最优的性能目标目标,首先进行的是性能测试,这里对测评环境下的SSD,网络进行了详细的测试,结合运行环境的限制,最终确定磁盘采用direct方式调用,以2M单位写,16M或32M读。 由于网络环境在测评阶段发生过变动,限速环境下丢包率高,使用16连接4K大小调用,pipeline的方式请求大块数据,即柱状图最右侧1212M/s,用多连接打满带宽,后期去除限速后,采用单tcp连接128K调用,即最高的1939M/s。3 具体设计3.1整体架构设计确定了硬件的吞吐量,就可以对程序进行整体的设计了,计算节点和存储节点都根据其功能分为3层。计算节点前部分的KV接口层负责适配调用接口及记录必要的参数,因为计算节点无状态,没有持久化功能,KV抽象层通过对RPC client的包装,抽象出KV的存储层,实现接口和代码复用。存储节点除了RPC服务层,KV管理层负责管理到存储层的读写缓冲,文件系统层则负责将抽象的存储调用映射到多个磁盘文件。可以看出,计算节点和存储节点都有使用读缓冲提升性能,读取时,计算节点负责建立索引和预读,而存储节点抽象为一个块存储,写入时,存储节点则抽象为一个RPC服务端,计算节点远程调用。3.2存储设计存储引擎,首要设计存储的结构,这里采用KV分离的日志式存储的方式,KV在顺序上一一对应,可以通过读key文件快速建立索引,同时考虑到文件管理迁移的情况,文件以1G进行分片。3.3索引设计索引是加速读取必不可少的,为了将6400w索引到内存中并能提供高性能插的入和检索,采用了hash+array+linked list结构,同时能应对数据倾斜的情况,通过细粒度锁提升并发度,hash和array的长度也是可调的以适应不同场景。       索引以key和offset作为一个单元,将key文件全部读入内存,插的入新KV时直接hash到对应slot,append到后面,当需要查找时,对索引进行排序,key为第一优先级,offset为第二优先级,通过二分查找upper_bound方式,找到最大的offset值,即最新value对应offset,具备处理重复key的能力。3.4RPC协议设计而针对网络传输,设计了二进制的RPC协议,整体协议由计算端发起,无状态。设计考虑到应对各种网络环境和传输方式,请求和响应具备batch能力,最大化利用带宽。同时包头尽量4bytes对齐,提升payload的拷贝效率。4 具体功能实现 结构和协议设计完成后,下面就需要实现具体功能了。4.1日志式存储引擎实现首先是基于日志的存储部分,该部分抽象为writer,reader和file operator三部分,writer负责写入,通过mmap使用page cache作为缓冲、meta和keys的存储,同时使用精心设计的lock free ring buffer提升高并发写入性能,flusher和loader负责异步刷盘,重叠CPU/IO时间,达到最大吞吐量。读缓存使用最简单的hash,在顺序读时高效实现最优缓存策略。reader会读写缓冲,保证任何情况下写即可见。4.2单机KV存储引擎实现基于之前的日志式存储引擎,加上索引模块,记录key和对应存储偏移,即可完成单机版的KV存储引擎。系统初始化时,restorer负责多线程读取keys,并以(key,offset)进行排序,即前文介绍的索引初始化及查找算法。索引基于linked list+array的结构,固定大小分配自内存池,无碎片,统一生命周期管理。同时基于底层存储引擎特性,保证KV写入即可见的基本要求。4.3RPC实现对于client有两套调用流程,分别用于适配延迟敏感的写入操作和追求极致吞吐量的读取操作。首先,每个KV agent初始化一个client,client预先建立多条tcp连接。对于时延敏感的写入,采用单路阻塞IO模型,确认持久化后返回;对于追求吞吐量的读取,采用pipeline请求+多路复用IO模型,pipeline并行度可根据网络状况进行调整。      考虑到多路复用IO模型带来的时延问题,server线程采用简单的阻塞IO模型,单线程单socket。keys数据传输zero copy,提升性能。协议解析使用会话协议缓冲,采用生产消费模型,非常便于扩展协议。4.4计算存储分离的KV存储引擎实现上图展示的是初始化和写入的流程,首先计算节点restorer通过RPC拉取已有的keys数据,完成索引建立,保证能感知到已写入的KV。然后写入时,计算节点先通过RPC协议包装请求,等待写入完成后,将key和data offset记录到索引中,最后返回set。这样保证数据持久化后才返回请求,同时保证计算节点和存储节点实时一致。由于计算节点的实时性,读取并不需要做额外同步,直接通过索引获取当前key的offset,然后调用计算节点的reader,这时存储节点抽象为一个块存储,reader通过RPC完成cache miss时的读取功能,这里关闭了计算节点的loader跨块预读的功能,降低网络带宽占用,降低get KV的时延。5 细节优化5.1无锁环形缓冲由于写入是顺序进行的,低写入延迟是提升性能的决定性因素。这里将meta存储在page cache(mmap)中,对抗应用层崩溃,同时提升写入速度。meta数据按操作线程进行CPU cache line对齐,避免写入造成cache invalidate。通过filled数组和多个bound变量CAS操作保证write和flush操作安全,所以在写缓冲充足时,所有写入操作都是无锁无等待的。本地测试多线程写同一writer,flush到内存,可打满内存带宽到40GBps。上图为cache line对齐的meta结构。上图为bound的逻辑关系示意及具体实现结构。            上图为对filled数组和bound移动的核心代码。5.2 SpinLock & SpinRWLock锁操作是在多线程编程中比不可少的,但mutex是一个非常重的操作。这里针对多线程快速同步设计了两套基于原子操作和自旋的锁。实现基于原生C++11,且占用非常小的空间,RW lock仅占2个ptr大小。RW lock代码比较多就不贴这,实现参考java的ReentrantReadWriteLock非公平实现方法。非公平锁吞吐量高,故这里选择非公平方式。5.3 多存储单元虽然针对多线程传统设计了诸多优化,多存储单元的方式还是一种非常简单直接的避免冲突方式。根据线程id路由到不同存储单元上写入,能直接消除线程冲突。但在读的时候,不会总是落在同一单元中,这里通过prefer项,利用读写聚集性,减少多单元检索的开销。5.4 预读预读是重叠CPU和IO的一种方案。这里,预读由独立线程loader完成的,初赛阶段由于不存在网络延迟,预读能够提升整体吞吐量,提升若干秒的性能。而复赛阶段由于存在网络传输:  ∵网络吞吐量 < 磁盘吞吐量 and 网络延迟 >> 磁盘延迟  ∴关闭跨块预读的收益 > 预读收益复赛阶段预读仅包含values整块预读,块大小设定为32MB,由于reader的cache极大(使用了2GB),也起到了预读作用。            下图所示为预读预测代码,通过分析跨块时的访问模式,智能地进行正向和反向预读。    5.5内存管理 & 对齐 & Misc这里总结下内存相关优化点,不展开:内存管理-            VM使用RAII思想管理-            预分配一大块mmap-            offset原子加分配内存-            生命周期统一管理内存分配-            固定大小&对齐的内存分配-            array+linked list方式实现动态数组对齐-            DIO操作内存和缓存的VM都是4KB对齐,单独管理+populate&lock-            代码中设计了AVX2的memcpy(由于gcc版本没开)Misc-            索引重载符号使用std::sort,std::upper_bound-            keys传输使用mmap实现zero copy-            RPC请求栈上内存构建,减少系统调用,利用CPU cache-            数据分片参数使用类模板,编译阶段优化除法为位运算6 最优成绩复赛:-            最优成绩性能    484.889s(写)+133.776s(读)+134.711s(索引+随机读)+0.012s(Misc)=753.388s-            由于网络存在波动且传输数据较多,很难遇到各阶段都达到最优    写阶段最优: 484.889s    读取并恢复索引:~0.6s    读阶段最优:132.263s-(~0.6s(恢复索引))=131.663s 初赛(保留kill恢复能力,未达到最大吞吐量):-            最优成绩性能130.782s(写)+73.802s(顺序读)+72.827s(随机读)+0.008(Misc)=277.419s7 总结-            现实问题受环境影响,不存在永恒的最优方案,需要测试实验作为先导,知己知彼才能百战不殆-            良好的抽象有利于最大程度复用已有代码,同时具备良好的可维护性和扩展性-            追求极致性能时,在细节上的优化是必不可少的 最后感谢华为云提供这次比赛机会,通过这次比赛学习到了很多知识,使我受益匪浅。作者:0xCC
  • [参赛经验分享] 2019华为云数据库TaurusDB性能挑战赛亚军参赛总结
    缘起TaurusDB是华为云自主研发的最新一代云原生分布式数据库,完全兼容mysql8.0,采用计算与存储分离、日志即数据的架构设计,支持1写15读,性能达到原生Mysql的7倍。Taurus构建在共享分布式存储上,存储空间最高达128T,能跨AZ部署,具有可靠、高性能、易伸缩等特性。华为云TaurusDB性能挑战赛是由华为云主办的数据库领域顶级赛事,大赛将综合科研教学成果及商业领域需求,探索数据库领域的技术问题的可行性,为需求方和开发者提供联接的桥梁;并联合合作伙伴,搭建一个技术交流、人才培养、机遇共创数据库开发者平台和生态。听起来很激动人心吧,关键是奖金也很多, 同时还能刷脸! 最早是听一个同事说起的,恰好本人的工作也是“新一代数据库”开发,看着这么多人工智能的比赛不能参加,只能后悔自己选错了专业,但是机会来了,终于有自己擅长的领域了,心想不能错过这次机会,一边默默加了收藏。比赛内容一句话概括,就是实现一个kvstore,初赛是单机引擎,复赛要求存储计算分离,并且kv都是定长的,如果通用性做得好,可以直接用作块存储。题目初赛和复赛稍有区别,这里只说复赛的题目。存储引擎K,V都是定长的,key 8bytes, value 4K bytes。 测试分为三轮,第一轮, 16个线程写,每个线程写400万次;第二轮,16个线程读,每个线程按顺序读取400万次;第三轮,16个线程读,每个线程逆序读取400万次。第三轮的逆序是局部随机,总体逆序,也即在10M的数据范围内随机读,然后以10M为单位逆序推进。 成绩就是看总时间,时间越短越好。前面已经说过,初赛要实现的是单机引擎,复赛要实现的是存储计算分离的引擎,最大的限制是计算节点不能持久化任何数据。预选赛不得不说,华为可真会玩,这次预选赛竟然还要做题,并且是必须要学习他们的资料才能通过的题目,题目包括数据库的基本常识,但是也有产品介绍,经过深夜两个小时的学习,我算是第一次知道了这么多种不同的产品究竟是干嘛的。第一次答题还错了一道,以防万一,又做了一遍,这次错了两道,算了不玩了,95分也够了,睡觉去。但是心里总是不舒服,究竟是哪错了,又看了一遍,原来是幻读的理解这道题错了。初赛不出意外,预选赛顺利通过,初赛就开始写代码了,初赛只要写对差不多就可以晋级复赛。 当然任何一个程序员都不会满足刚好够用的状态,因此正常能想到的优化都加上了: 比如Direct IO, 多文件做数据分区,引入写buffer,比较频繁修改元数据,读取使用自己实现的page cache,以免4K读不能打满磁盘带宽。复赛复赛要求存储计算分离, 我认真做了一些分析,同时也对他们的硬件做了测试,分析下来,这个比赛比拼的点和我平时在工作中要追求的还是很不一样的。 平时无论做什么系统,都是在延迟差不多的情况下,把吞吐量做高,这次比赛最关键的一个点是:延迟第一,延迟是最重要的,特别是写的阶段,因为并发只有16,想通过聚合换吞吐量都不好使了。首先考虑持久化方法:4K恰好能对齐IO,所以key和value要分开存储,      想使用rocksdb之类的存储引擎即使不被禁止也是没有竞争力的。value非常随机,基本不用考虑压缩,就算有一点点好处,实现起来也太复杂了。再次考虑如何优化IO:SSD的IO吞吐量高于4K * iops, 不管是读还是写,IO聚合是必须的。单文件会遇到文件系统瓶颈,需要多文件, 也即要对数据做partition。关于同步IO还是异步IO的选择, 因为延迟优先,所以应该选同步IO。最后考虑网络框架:首先,我直接放弃考虑任何已有的网络框架,因为这是benchmark, 再小的开销也是开销,都会让程序变慢。其次,实际上任何IO      multiplexing的框架都会造成额外的延迟,不仅复杂,也是得不尝失的。综上,我决定就使用最简单的阻塞式IO来做socket通信。总体设计计算节点和存储节点分工, 关键就是索引维护在哪?经过思考, 决定索引维护在计算节点。 索引的内容是: key -> <file_id, pos>, 索引在计算节点的内存中维护为hash表。因为build索引大概需要400ms,写入再读取index比重新构建一遍index时间还长, 所以索引不需要持久化, 这其实是一个反直觉的决定。 计算节点在发送第一个读请求之前,会从存储节点把所有写入的key和pos发送过来, 然后由计算节点构造索引。存储节点起16个线程,listen在16个不同的端口。计算节点也会有16个线程, 每个计算节点的线程只会连接一个存储节点的端口,从而和一个存储节点线程通信。写入请求:写入过程不同的线程完全独立,每个线程负责一部分数据。请求发到存储节点后由接受请求的线程写入。读取过程:读取过程每个存储线程会读取任意一个线程写入的数据。由计算节点指定要读取那个分区的数据。也即写请求发到哪个存储线程,就由那个存储线程写入,每个存储线程实际上就对应了一个数据分区。 读请求发到一个存储线程,它可能要跨线程读取其它分区的数据。存储文件存储节点把数据分为16个partition,每个partition由一个线程负责写入。 每个partition共三个文件: 之所以有三个文件,是因为首先key和value要分开存储,这就要用两个文件;其次为了优化写,额外引入了文件做写入buffer。文件命名规则如下, 以第一个分区为例:00.k: 保存写入的key。 fallocate 4M * 8B, mmap到内存。00.v:      保存写入的value。fallocate 4M *      4K, DIO读写。00.b:      用作写入buffer。fallocate 16K      buffer + 4K 元数据,mmap到内存。写入原子性先写key和value,再更新key count。 key count改成功,则写成功;否则写失败,下次重启进程当作这次写没发生过。换句话说,key count改成功实际上表示这次写入commit成功。key count记录在00.k的第一个8字节, 如前所诉, 00.k是mmap到内存的,所以更新key count是没有什么代价的。value先记录到写buffer里,然后批量刷到00.v文件。key file内容如下:key countk1k2k3k4k5k6……value file内容如下:value countv1v2v3v4v5v6……buffer file内容如下:b0b1b2b3…………b15flushed poswrite buffer也是mmap到内存的, 前64K记录数据。 紧跟64K的8个字节记录flushed pos。 因为mmap必须以page为单位,所以实际内存占用64K + 4K。 它实际上是一个mmap持久化的ring buffer。 Ring Buffer的元数据包括filled pos和flushed pos。 filled pos由key count可以算出来, 所以不需要单独再记。build indexbuild index实际上是构造一个key -> offset的hash表。 要在400ms内完成build index,难点是如何并行:基本思路是把key做partition,每个partition内独立构造hash。这本质上是一个MapReduce的过程。map阶段: 并行划分range, 16个线程,每个线程负责处理一个key文件。reduce阶段: 并行构造hash, 16个线程,每个线程处理一个range。线程同步: 第二阶段开始之前要等第一全部完成,也即两个Stage之间是个Barrier, 这个同步过程和map reduce的shuffle是类似的。读取cache的实现cache是value的镜像,value文件分成了16个,cache也对应分成16组。 因为顺序和热点访问模式对cache都很友好,cache不需要特别大,每一组cache大小为64M。为了应对热点读,cache的最小单元设为16M,借用CPU cache的术语,我把它叫cache line,这是一个很大的值。 cache的内存因为是定长的,所以通过mmap一次申请好,不需要动态分配。 cache的索引本质是一个hash,但是不用解决冲突, 用file offset直接计算得到索引的下标。对16组cache中的每一组来说,有4个cache line, 每个cache line有三种状态,用一个uint32_t表示:Invalid: UINT32_MAXLocked:      UINT32_MAX - 1Valid:      file_offset »12cache line的状态被叫做indexStat,它被单独记在另外一个小数组里。读取不用加锁,使用double check即可:读取之前检查IndexStat有效,并且offset和我要读取的内容匹配。拷贝cache line的内容到私有buffer。拷贝完cache line的内容后再检查IndexStat是否发生过变化, 如果indexStat没有变化过,则说明我们拷贝的cache      line内容是对的。为了让上述double check生效, 更新cache的线程需要在写入之前把indexStat改为Locked,更新完成后再把indexStat改为有效值。关于网络通信关于网络框架还有一个问题,就是是否有必要用UDP,犹豫之下,觉得UDP还是会更复杂,稳妥起见,选择了TCP。后来得知在测试环境里UDP是不同的,也就释然了。但是TCP看起来开箱即用,用起来却暗藏机关,至少要调整以下三个参数:tcp_nodelaytcp_quickacksend/recv      buf最后为了规避TCP的流控,我们要避免另外两个坑:避免发送太快,超过交换机的队列长度,从而导致丢包,因为TCP的工作方式就是,只要你给它数据,它就会不停的加大,发送窗口,直到发生丢包,然后触发限流。要避免这种情况,本质是要限制TCP的连接数, 因为只要连接数限制住了,发送窗口总长度也就限制住了。避免TCP”冷却”之后重新进入慢启动状态,如果有root权限,是可以通过sysctl关闭slow start的,但是我们没法控制系统参数,这就要求我们一个连接最好要不停的发包,不要停下来。有人定义了packet格式,而我遵循一切从简的原则,没有实现通常RPC要有的功能:没有定义pcode,因为整个通信过程就只有读和写两种request,每一个socket只用来发送一种类型的request,如果要发送不同的request,只需要切换socket即可。没有实现序列化,因为request很简单,只需要把结构体直接发送到socket即可。细节决定性能细节也就是很多关于性能的小点:首先关于文件IO: 同步IO比异步IO延迟要小; open的时候加入O_NOATIME避免读取操作也修改元数据其次关于内存分配: 分配完内后提前触发page fault至少可以让性能更稳定,如果不能提高性能的话;      分配内存可以尝试hugepage, 失败之后再使用4K page; mmap的内存可以不用清零最好关于CPU, 绑核,通用可以让延迟更稳定, 理论上对性能是有提升的。正确性测试如前所属,我基本上是一切从简,但即使如此,提交了好多次,都通不过正确性测试。为了排查问题,专门写了一个随机的验证程序,这个比官方的测试强度高多了。确实发现了一个readv之后更新iovec的bug,还有一处cache的bug。 事后看来,在这个测试上花的时间非常值得,否则我可能到最后都没有成绩。总结这次比赛感觉有点像马拉松,听完大家的分享,感觉每个人的时间都比较有限,大家都是争分夺秒,每个人都有一些优化没来得及试。本人最大的一个遗憾是读预取没有实现。另外一个是写的过程中网络和IO并行化做的不好。最后听了第一名的方案,深有体会,要想拿到最好的成绩,需要把写和读分开考虑,同时把网络传输和本地IO分开考虑。单纯从工程上说,我的代码有一些特有的风格, 简单来说就是总爱重复造轮子,不愿意引入依赖:没有用pthread mutex/cond,全部用atomic      ops + futex没有spin,等待的地方都有睡眠和唤醒机制另外,本人虽然工作中用C++, 但是我情愿用plain old C, 所以我很少用高级C++特性。如果未来还做这种系统实现的比赛,希望有可以利用RDMA的和NVM的比赛, 毕竟,新硬件总是有更多的可能。当然比赛的设置要考虑引入更多自由度,这样会更加有趣。
  • [参赛经验分享] 2019华为云数据库TaurusDB性能挑战赛亚军参赛总结
    大家好,我们是watermelon团队,今天我们的答辩展示主要分为四个部分来介绍。首先是我们的团队和成员的简介然后是我们对这次题目重点的理解接下来,我会介绍一下我们最基础的架构,包括网络、磁盘、缓存等最后,重点介绍我们每一个性能优化点团队名叫watermelon,我们三个人都是在读的研究生,分别来自浙江大学和上海交通大学,都是明年毕业。初赛和复赛的成绩都是第四名,历史成绩非常的稳定。我们总结了几个题目重点:首先,KV 都是定长的:这样简化了⽂件和内存的操作和管理。第二点,value远大于key:把 KV 分离存储,解除索引和数据之间的耦合性。第三点,线程数固定:测评程序固定使用 16 个线程访问数据库。第四点,只需要保证进程意外退出时的持久性:所以可以利用操作系统的缓存对写入方式进行一些优化。第五点,分阶段测评:随机写、随机读、顺序读三个阶段互相没有重叠。第六点,计算节点无状态:我们知道,在这种基于共享存储的计算存储分离架构下,所有持久化数据只能存于存储节点,计算节点只进行逻辑操作。第七点,数据读取的线程和数据写入线程之间没有绑定关系:就是说,每个线程不是只读取自己写入的数据。  //与初赛不一样,读取线程id不一读的是相同id写入的数据最后一点,随机读的随机性在每个时刻只局限在一个 10M 热点分区内:并且热点分区按写入的顺序逆序推进。 下面是我们方案的核心架构:    我们计算节点和存储节点的线程采用一对一的tcp连接,因为测评程序是16线程,所以连接数就是16。在存储节点,我们有数据持久化产生的文件,以及读写文件的缓存。在计算节点,我们维护了读数据的缓存,但是没有写数据的缓存,因为要保证计算节点被 kill 的数据持久性。并且我们的索引也是只在计算节点上维护的,在数据库启动阶段从存储节点把索引数据拉到计算节点。接下来,具体介绍一下我们的文件组织形式:首先,按写入线程进行数据的分区,就是说,每个写入线程只顺序写自己的分区文件,这样就避免了多线程写同一个文件冲突的问题。然后,在每个分区内,将 key 和 value 分离存储为两个文件,一个是key log,一个是value log。可以解除索引和数据之间的耦合性。并且我们为了提高写入value 的速度,对 value 进行了缓存,缓存中凑齐若干个value后,再一起进行刷盘。为了保证缓存不丢失,缓存也使用了mmap的形式,因此对应有一个缓存文件。我们的索引也是按分区进行构建的,每个分区是一个hash,里面存的是该分区所有数据的索引项,索引项就是一个 key 和 一个value offset复赛的优化历程,我们从最开始跑通的 2300 分,到最后的 780 分,中间经过了好几次架构和方法的改变。首先我们的第一个优化是,我们在启动阶段把 key 从存储节点批量的传到计算节点,这样相比每个 key 都请求一次,批量的方法相当于减少了一半的网络请求,使得时间提升了 300 秒。接下来,由于我们无法搞定网络传输大数据包的问题,因此我们选择先在存储节点实现顺序读和随机读的缓存,这样减少了存储节点的 io 次数,时间提升了 500s再后来,我们解决了网络传输的问题,所以先在计算计算节点进行了顺序读的缓存,成绩提升到 1100s这样紧接着,我们按存储节点缓存一样的思路,把随机读缓存也拿到了计算节点,这样做之后我们的成绩就已经突破了 800s在最后阶段,我们又优化了一些细节问题,最终成绩是 781s围绕读写文件,缓存策略,和网络传输这三个方面,来讲解我们是如何把这个系统的性能压榨到极致的。对于 key 这种小数据量的读写模式,采用 mmap 可以利用 page cache 将小数据读写转换为整个内存页的读写,减少了系统调用的时间消耗。value 的大小为固定 4KB,我们知道,写入数据大小要对齐 ssd 的内部 page 时,可以达到最优写入性能。我们经过线上测试,按 1M 大小顺序写入数据可以达到最大吞吐量。所以,为每一个分区分配一个容量为 1M 的写缓冲区,写满 256 个 value 再将缓存数据一起刷盘,刷盘方式采用 direct io。并且,用 mmap 做缓存,可以保证数据持久性。第二点,我们来看,如何做随机读的缓存,使得缓存命中率最高。现在背景是,随机读在每个时刻只局限在一个 10M 热点分区内,并且热点分区按写入的顺序逆序推进。看图,上面是我们实际存储的文件,按10M分为了若干个block,就是说,我们的缓存只能按 block 对齐进行加载数据。然后,下面是测评的热点分区示意图,我们发现一共 400w 数据,热点分区的边界跟实际数据 10M 边界很可能是不对齐的,并且边界值未知。所以,如果我们只用单个 10M 的缓存,会出现下面这样的问题。假设当前阶段,测评程序要随机访问 hot1 这个热点分区。第一个位置,我们缓存加载了block3,然后,访问第二个位置,发现缓存失效,加载了block2,继续访问第三个位置,缓存又失效了,重现加载回了block3所以,这样就产生了一个缓存来回震荡的问题,极端情况是,我要访问400万数据,一共要加载400万次大块的缓存,肯定超时,还不如不做缓存。我们的方案是,采用两个 10M 的buffer,一前一后,一起往前推进,这样非常完美了避免了缓存来回震荡的问题。并且这个 buffer 不一定是10M,只要大于10M都可以解决这个问题。第三点,我们来看,如何使得网络的传输效率最高。我们发现,写入阶段的网络传输时间主要瓶颈是在周转时间上,也就是说,不是网络有多么拥塞导致网络传输变慢,而是说,value发过去再回来,本身就需要这么长的时间。所以我们的优化只能是让存储节点尽可能快的返回ack信号,我们的做法是在数据写入存储节点的mmap之后,就返回ack,而不用等待page cache 刷盘。对于持久性,recover 阶段会把 cachebuffer 里面的数据都重新写进 value 文件里。对于大块数据进行拆分,然后进行多次发送,可以在发送的同时进行流量控制,使吞吐量保持在一个较高的水平。我们的流量控制方法非常简单粗暴,在发送每两个 4k 数据之间,直接加一个延时,延时的方法是让 CPU 自旋一会。并且,存储节点也做了读缓存,把存储节点的读缓存,批量拆分传到计算节点。最后,回到一个宏观的位置上来看,通过这次比赛,我们严格按照计算存储分离的思想来设计了我们的系统架构。对基于共享存储的计算存储分离架构有了一些认知和理解。首先,我们看计算节点我们的架构只支持一个 RW 节点,进行数据的写入。但是 RO 节点在理论上是可以无限扩展的。并且,由于底层的共享存储,所以主从复制的延迟可以做到非常低。当 RW 写入一个 kv 数据,对于 RO 节点,它只需要更新已经存在自己 buffer pool 中的数据,而如果发现 RW 写入的数据不在它的 buffer pool 中,那它什么也不做。只有在 RO 节点读取数据时,发现要请求的数据不在自己的buffer pool中,它才去下面的存储节点中拉取这个数据到自己的 buffer pool 中。这样看来,我们在实现高可用功能时候,可以很直接的进行主从切换,RO 节点可以迅速提升为 RW 节点,直接开始对外服务。再看存储节点我们当前只是在单个节点上保证了数据的持久性,而这种 kv 存储可以扩展为分布式架构,采用多副本存储,从而能获得更好的容错性,和更好的读写性能。所以这样一整套架构,可以解决很多实际的痛点,因此,会成为当今云数据库的一个趋势。
  • [参赛经验分享] 2019华为云数据库TaurusDB性能挑战赛季军赛题总结
    1 前言回顾第一次参加性能挑战赛—第四届阿里中间件性能挑战赛,那时候真的是什么都不会,只有一腔热情,借着比赛学会了 Netty、学会了文件 IO 的最佳实践,到了这次华为云举办的 TaurusDB 性能挑战赛,已经是第三次参加比赛了,同时也是最“坎坷”的一次比赛。经过我和某位不愿意透露姓名的 96 年小迷妹的不懈努力,最终跑分排名为第 3 名。如果要挑选一个词来概括这次比赛的核心内容,那非”计算存储分离“莫属了,通过这次比赛,自己也对计算存储分离架构有了比较直观的感受。为了比较直观的体现计算存储分离的优势,以看电影来举个例子:若干年前,我总是常备一块大容量的硬盘存储小电影,但自从家里带宽升级到 100mpbs 之后,我从来不保存电影了,要看直接下载/缓冲,基本几分钟就好了。这在几年前还不可想象,如今是触手可及的事实,归根到底是随着互联网的发展,网络 IO 已经不再是瓶颈了。计算存储分离架构相比传统本地存储架构而言,具有更加灵活、成本更低等特性,但架构的复杂性也会更高,也会更加考验选手的综合能力。计算存储分离架构的含义:存储端有状态,只存储数据,不处理业务逻辑。计算端无状态,只处理逻辑,不持久化存储数据。2 赛题概览比赛整体分成了初赛和复赛两个部分,初赛要求实现一个简化、高效的本地 kv 存储引擎,复赛在初赛的基础上增加了计算存储分离的架构,计算节点需要通过网络传输将数据递交给存储节点存储。计算节点和存储节点共用上述的接口,评测程序分为 2 个阶段:正确性评测此阶段评测程序会并发写入随机数据(key 8B、value 4KB),写入数据过程中进行任意次进程意外退出测试,引擎需要保证异常中止不影响已经写入的数据正确性。异常中止后,重启引擎,验证已经写入数据正确性和完整性,并继续写入数据,重复此过程直至数据写入完毕。只有通过此阶段测试才会进入下一阶段测试。性能评测随机写入:16 个线程并发随机写入,每个线程使用 Set 各写 400 万次随机数据(key 8B、value 4KB)顺序读取:16 个线程并发按照写入顺序逐一读取,每个线程各使用 Get 读取 400 万次随机数据热点读取:16 个线程并发读取,每个线程按照写入顺序热点分区,随机读取 400 万次数据,读取范围覆盖全部写入数据。热点的逻辑为:按照数据的写入顺序按 10MB 数据粒度分区,分区逆序推进,在每个 10MB 数据分区内随机读取。随机读取次数会增加约 10%。语言限定CPP & Java,一起排名3 赛题剖析初赛主要是文件 IO 和存储架构的设计,实现一个简单的本地KV存储引擎,如果对文件 IO 常识不太了解,可以先行阅读 《文件IO操作的一些最佳实践》。大赛的重头戏基本是在复赛网络通信的比拼上,在引入计算/存储分离架构后,计算节点无状态,计算节点和存储节点需要通过网络交互,网络将引入大于服务器内部的时延,因此,结合应用场景的良好架构和缓存设计,将是此类引擎设计的一个重点。3.1 架构设计计算节点只负责生成数据,在实际生产中计算节点还承担额外的计算开销,由于计算节点是无状态的,所以不能够聚合数据写入、落盘等操作,但可以在 Get 触发网络 IO 时一次读取大块数据用作缓存,减少网络 IO 次数。存储节点负责存储数据,考验了选手对磁盘 IO 和缓存的设计,可以一次使用缓存写入/读取大块数据,减少磁盘 IO 次数。所以选手们将会围绕网络 IO、磁盘 IO 和缓存设计来设计整体架构。3.2 正确性检测赛题明确表示会进行 kill -9 并验证数据的一致性,正确性检测主要影响的是写入阶段。存储节点负责存储数据,需要保证 kill -9 不丢失数据,但并不要求断电不丢失,这间接地阐释了一点:我们可以使用 PageCache 来做写入缓存;正确性检测对于计算节点与存储节点之间通信影响便是:每次写入操作都必须 ack,所以选手必须保证同步通信,类似于 ping/pong 模型。3.3 性能评测性能评测由随机写、顺序读、热点读(随机读取热点数据)三部分构成。随机写阶段与 PolarDB 的评测不同,TaurusDB 随机写入 key 的 16 个线程是隔离的,即 A 线程写入的数据只会由 A 线程读出,可以认为是彼此独立的 16 个实例在执行评测,这大大简化了我们的架构。顺序读阶段的描述也很容易理解,需要注意的是这里的顺序是按照写入顺序,而不是 Key 的字典序,所以随机写可以转化为顺序写,也方便了选手去设计顺序读的架构。热点读阶段有点故弄玄虚了,其实就是按照 10M 数据为一个分区进行逆序读,同时在 10M 数据范围内掺杂一些随机读,由于操作系统的预读机制只会顺序预读,无法逆序预读,PageCache 将会在这个环节会失效,考验了选手自己设计磁盘 IO 缓存的能力。4 架构详解4.1 全局架构计算存储分离架构自然会分成计算节点和存储节点两部分来介绍。计算节点会在内存维护数据的索引表;存储节点负责存储持久化数据,包括索引文件和数据文件;计算节点与存储节点之间的读写都会经过网络 IO。4.2 随机写架构随机写阶段,评测程序调用计算节点的 set 接口,发起网络 IO,存储节点接收到数据后不会立刻落盘,针对 data 和 index 的处理也会不同。针对 data 部分,会使用一块缓冲区(如图:Mmap Merge IO)承接数据,由于 Mmap 的特性,会形成 Merge File 文件,一个数据缓冲区可以聚合 16 个数据,当缓冲区满后,将缓冲区的数据追加到数据文件后,并清空 Merge File;针对 index 部分,使用 Mmap 直接追加到索引文件中。F: 1. data 部分为什么搞这么复杂,需要聚合 16 个数据再刷盘?Q: 针对此次比赛的数据盘,实测下来 16 个数据刷盘可以打满 IO。F: 2. 为什么使用 Mmap Merge IO 而不直接使用内存 Merge IO?Q: 正确性检测阶段,存储节点可能会被随机 kill,Mmap 做缓存的好处是操作系统会帮我们落盘,不会丢失数据F: 3. 为什么 index 部分直接使用 Mmap,而不和 data 部分一样处理?Q: 这需要追溯到 Mmap 的特点,Mmap 适合直接写索引这种小数据,所以不需要聚合。4.3 热点读&顺序读架构热点读取阶段 & 顺序读取阶段 ,这两个阶段其实可以认为是一种策略,只不过一个正序,一个逆序,这里以热点读为例介绍。我们采取了贪心的思想,一次读取操作本应该只会返回 4kb 的数据,但为了做预读缓存,我们决定会存储节点返回 10M 的数据,并缓存在计算节点中,模拟了一个操作系统预读的机制,同时为了能够让计算节点精确知道缓存是否命中,会同时返回索引数据,并在计算节点的内存中维护索引表,这样便减少了成吨的网络 IO 次数。4.4 存储设计站在每个线程的视角,可以发现在我们的架构中,每个线程都是独立的。评测程序会对每个线程写入 400w 数据,最终形成 16 * 16G 的数据文件和 16 * 32M 左右的索引文件。数据文件不停追加 MergeFile,相当于一次落盘单位是 64K(16 个数据),由于自行聚合了数据,所以可以采用 Direct IO,减少操作系统的 overhead。索引文件由小数据构成,所以采用 Mmap 方式直接追加写计算节点由于无状态的特性,只能在内存中维护索引结构。4.5 网络通信设我们都知道 Java 中有 BIO(阻塞 IO)和 NIO(非阻塞 IO)之分,并且大多数人可能会下意识觉得:NIO 就是比 BIO 快。而这次比赛恰恰是要告诉大家,这两种 IO 方式没有绝对的快慢之分,只有在合适的场景中选择合适的 IO 方式才能发挥出最佳性能。稍微分析下这次比赛的通信模型,写入阶段由于需要保证每次 set 不受 kill 的影响,所以需要等到同步返回后才能进行下一次 set,而 get 本身依赖于返回值进行数据校验,所以从通信模型上看只能是同步 ping/pong 模型;从线程数上来看,只有固定的 16 个线程进行收发消息。以上两个因素暗示了 BIO 将会非常契合这次比赛。在很多人的刻板印象中,阻塞就意味着慢,非阻塞就意味着快,这种理解是完全错误的,快慢取决于通信模型、系统架构、带宽、网卡等因素。我测试了 NIO + CountDownLatch 和 BIO 的差距,前者会比后者整体慢 100s ~ 130s。5 细节优化点5.1 最大化磁盘吞吐量但凡是涉及到磁盘 IO 的比赛,首先需要测试便是在 Direct IO 下,一次读写多大的块能够打满 IO,在此基础上,才能进行写入缓冲设计和读取缓存设计,否则在这种争分夺秒的性能挑战赛中不可能取得较好的名次。测试方法也很简单,如果能够买到对应的机器,直接使用 iostat 观察不同刷盘大小下的 iops 即可,如果比赛没有机器,只能祭出调参法,不停提交了,这次 TaurusDB 的盘实测下来 64k、128K 都可以获得最大的吞吐量。5.2 批量回传数据计算节点设计缓存是一个比较容易想到的优化点,按照常规的思路,索引应该是维护在存储节点,但这样做的话,计算节点在 get 数据时就无法判断是否命中缓存,所以在前文的架构介绍中,我们将索引维护在了计算节点之上,在第一次 get 时,顺便恢复索引。批量返回数据的优势在于增加了缓存命中率、降低总网络 IO 次数、减少上行网络 IO 数据量,是整个比赛中分量较重的一个优化点。5.3 流控在比赛中容易出现的一个问题,在批量返回 10M 数据时经常会出现网络卡死的情况,一时间无法定位到问题,以为是代码 BUG,但有时候又能跑出分数,不得以尝试过一次返回较少的数据量,就不会报错。最后还是机智的小迷妹定位到问题是 CPU 和 IO 速率不均等导致的,解决方案便是在一次 pong 共计返回 10M 的基础上,将报文拆分成 64k 的小块,中间代入额外的 CPU 操作,最终保证了程序稳定性的同时,也保障了最佳性能。额外的 CPU 操作例如:for(int i=0;i<700;i++),不要小看这个微不足道的一个 for 循环哦。流控其实也是计算存储分离架构一个常见设计点,存储节点与计算节点的写入速度需要做一个平衡,避免直接打垮存储节点,也有一种”滑动窗口“机制专门应对这种问题,不在此赘述了。5.4 预分配文件在 Cpp 中可以使用 fallocate 预先分配好文件大小,会使得写入速度提升 2s。在 Java 中没有 fallocate 机制,但是可以利用评测程序的漏洞,在 static 块中事先写好 16 * 16G 的文件,同样可以获得 fallocate 的效果。5.5 合理设计索引结构get 时需要根据 key 查询到文件偏移量,这显示是一个 Map 结构,在这个 Map 上也有几个点需要注意。以 Java 为例,使用 HashMap 是否可行呢?当然可以,但是缺点也很明显,其会占用比较大的内存,而且存取性能不好,可以使用 LongIntHashMap 来代替,看过我之前文章的朋友应该不会对这个数据结构感到陌生,它是专门为基础数据类型设计的 Map 容器。每个线程 400w 数据,每个线程独享一个索引 Map,为了避免出现扩容,需要合理的设置扩容引子和初始化容量:new LongIntHashMap(410_0000, 0.99)。5.6 Direct IO最终进入决赛的,有三支 Java 队伍,相比较 Cpp 得天独厚的对操作系统的灵活控制性,Java 选手更像是带着镣铐在舞蹈,因为有过参赛经验,我提前封装好了 Java 的 Direct IO 类库:https://github.com/lexburner/kdio,相比 FileChannel,它能够使得磁盘 IO 效率更高。得知有 Java 选手真的在比赛中使用了我的 Direct IO 类库,也是比赛中实实切切的乐趣之一。6 失败的优化点6.1 预读线程先行考虑到网络 IO 还是比本地磁盘 IO 要慢的,一个本以为可行的方案是单独使用预读线程进行存储节点的磁盘 IO,设计一个 RingBuffer,不断往前预读,直到环满,计算阶段 get 时会消费 RingBuffer 的一格缓存,从而使得网络 IO 和磁盘 IO 不会相互等待。实际测试下来,发现瓶颈主要还是在于网络 IO,这样的优化徒增了不少代码,不利于进行其他的优化尝试,最终放弃。6.2 计算节点聚合写入缓冲既然在 get 阶段时存储节点批量返回数据给计算节点可以提升性能,那 set 阶段聚合批量的数据再发送给存储节点按理来说也能提升性能吧?的确如此,如果不考虑正确性检测,这的确是一个不错的优化点,但由于 kill 的特性使得我们不得不每一次 set 都进行 ACK。但是可以将 4/8/16 个线程编为一组进行聚合,通过调整参数来确定该方案是否可行。然而事与愿违,该方案并没有取得成效。7 聊聊比赛吧之前此类工程性质的性能挑战赛只有阿里一家互联网公司承办过,作为热衷于中间件性能优化的参赛选手而言,非常高兴华为也能够举办这样性质的比赛。
总条数:427 到第
上滑加载中