数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯

科技学院

科技百科

科技书籍

网站大全

软件大全

「深度学习福利」大神带你进阶工程师,立即查看>>>
【本文经作者授权转载,原则作者 糜利敏,联系方式见文章末尾】
关于 Apache Doris(Incubating)
Apache Doris(Incubating) 一款基于大规模并行处理技术的交互式SQL分析数据库,由百度于2018年贡献给 Apache 基金会,目前在 Apache 基金会孵化器中。
Github: https://github.com/apache/incubator-doris ,欢迎大家 Star、提 Issue、Pull Request。
官方网站: http://doris.incubator.apache.org/ 可以查看更多安装、部署、使用文档,也欢迎对文档内容进行校对或建议。
开发者邮件列表: dev@doris.apache.org,( 如何订阅请戳这里 )
背景
作业帮大数据团队主要负责建设公司级数仓,向各个产品线提供面向业务的数据信息,如到课时长、答题情况等,服务于拉新、教学、 BI 等多个重要业务线。在过去数月内,我们通过对 Doris 的应用实践,构建了数仓实时查询系统。本文总结并分享下期间的工作内容,也欢迎大家一起讨论。
典型的数仓从逻辑上划分为:
大数据团队主要负责到 ODS-DWS 的建设,从 DWS 到 ADS 一般是数仓系统和业务线系统的边界。
在过去,由于缺失统一的查询系统,探索了很多模式来支持各个业务线发展。
非流量 类 Kafka 。 业务线 从 kafka 接数据自己做数据的聚合 计 算。主要 问题 在于完全没有数 仓 的概念, 业务线 在做大量重复的建 设 Spark + ES 。每来一个 业务 需求,就构建一个 Spark+ES 集群( spark 负责计算写入到 ES , ES 业务层直接使用)。效率低、构建成本高,且 ES 高效的使用本身本身就需要学 习 ES 的接口以及内部原理, 对 于 业务线 很 难 有 这样 的精力去做 ES + 自定 义 API 。大数据将数据写入 ES 后,并 case by case 构建 api 。初步有了数 仓 的接口,但是接口不具 备 Sql 的能力,只能基于需求 case by case 的构建,效率太低。 …… 流量 类 (如 pv 、 uv 等) 由于数据量大,往往需要 预 聚合,引入 druid

这些 “ 烟囱 ” 式的系统构建方式,导致系统越来越难以维护,且业务接入效率也逐步降低。
因此,统一整个查询引擎,对于数仓建设在提高业务支持效率、降低维护成本上都具有非常重大的意义。
总体方案
经过过去数月的探索与实践,我们确立了以 Doris 为基础的数仓实时查询系统。同时也对整个数仓的数据计算系统做了一次大的重构,最终整体的架构图如下:
如图所示(从下到上),原始业务层日志经数据摄入系统进入数仓,在数据清洗计算层,我们将原来的 Spark 系统升级到了 Flink ,并且基于 Flink-Sql 提供了统一数据开发框架,从原有的代码开发升级到 Sql 开发来极大的提升数据的研发效率。
其后查询系统将 Kafka 的数据实时同步到查询引擎内,并通过 OpenAPI 的统一接口对外提供查询服务。
接下来,重点讲下查询系统的工作。
查询引擎选型
实时查询系统的核心在于确定查询引擎。
社区的查询引擎较多,如 Impala 、 Presto 、 Doris 、 ES ( xpack ),以及云上的 ADB 等。这块考虑到调研成本、团队技术生态、维护成本等多种因素,我们最后选择了 Doris 作为我们的查询引擎。
在性能调研时,我们也走了一些弯路:第一次使用 Doris 来做查询引擎,发现使用我们的业务 Sql ,延迟数据比较大,且 CPU 使用率很高( IDLE < 10% ),
原因在于使用了 AGGREGATE 模型,如对于订单数据,一般会将用户支付金额等作为指标列(如一个用户从订单预订到支付,状态的改变会修改支付金额值),但是业务端的 Sql 中有大量的基于支付金额(指标列)的筛选查询,如统计支付金额 > 某个值的用户数。
Doris 对于指标列的筛选成本较高,底层采用了类 LSM-Tree 的结构,因此为了确定某一行的数据是否该被筛选,需要扫描所有底层文件内包含该行的数据,进而聚合计算后才可以决策是否结果集包含该行( UNIQ 模型类似)。而 DUPLICATE 表无法更新列。
最终使用 Doris on ES ,主要考虑点 任意列 检 索。基于 ES 的倒排索引,我 们 可以 对 任意列 进 行 检 索( 筛选 )。 这 个模型大大降低了 业务 同学的学 习 理解成本,可以和 mysql 一 样 方便的构建数据模型。 ES 的易用性以及整个技 术 生 态 在公司内相 对 成熟的多, 维护 成本 较 低。如数据修改可以直接覆盖最新 值 ,非常 简单 。 Doris on ES 在数据 Scan 上做了大量的 优 化操作,如列存、 local 优先、响应内容过滤、顺序扫描、提前终止等,对于数据的扫描性能可以达到 ~30w/s Doris 提供了更 强 大的 Sql 语法(如 join 、多列 group by…… ),且整个 查询过 程保障了数据的准确度。大大提高了数据使用的效率和数据 查询质 量。 由于 ES 缺少分布式 计 算 层 , 导 致 ES-Sql 需要配置 size ,否 则 会 导 致返回的数据会少于 预 期的数据
当然,对于流量分析的场景,由于指标列一般是 pv 、 uv 等,业务上并没有对指标的筛选过滤需求,且 Doris 自身支持 RollUP ,因此非常适合流量类的查询分析。
因此,通过 Doris 我们统一了整个查询引擎端的实现,这样对于后续整个数仓的进一步建设就打下了非常重要的基础。
应用实践
基于业务场景,我们对需求进行了分类:面向业务工作台的非流量类需求以及流量分析类需求。
非流量 类
在实际的应用中,业务侧的需求主要分两类: 明 细查询 。教研工作台需要关注每个老 师 的明 细 信息,如某 课 程的学生的到 课 情况、 课 前 预习 情况 …… 聚合 查询 。部 门组织 上会关注整个部 门 、小 组 内的 统计 信息,如到 课 率、拉新率等
这些需求在前端查询,均需要保障低延迟。
而明细查询对于数据的时效性要求更高,因此对于明细类查询,业务侧会直接访问 Doris on ES 中的数据进行查询,这样基于 Doris on ES 的任意列检索能力可以保障业务查询模式的灵活性以及数据的时鲜性。
而对于聚合查询,由于不同指标的 Sql 计算的数据范围不同,且业务侧对于聚合的计算没有明细查询的时效性,因此,我们通过微批(如 1min 、 5mins 、 10mins…… )的调度能力定期计算聚合指标,并存放到 ADS 层的业务数据库中供前端平台查询。
为了提高数据使用效率,方便业务侧获得特定时间窗口的数据,在数据模型上,我们统一设置了 Meta 字段如数据更新时间,这样业务可以用来划分每次更新的数据窗口,做增量计算。
这个模式的主要好处 业务端延迟可控、稳定性好。聚合查询的延迟随着具体的 Sql 不同而不同,定期 执 行后的数据存放到 业务层 Mysql 中,可以最大化可以保 证查询 延 迟 数据修复成本低、 维护 方便。一旦数据有异常,可以自 动 触 发对应 的数据窗口 进 行重新 计 算 原来基于流式 计 算的修数,需要从源 头 修复,且必 须驱动 主流事件触 发 ,成本非常高,而基于 doris on es ,不同的事件可以更新不同的列或者表,只要在数据 查询时 join 即可 高性能。一般 业务 每次 读 取部分列, 这 个模式反而可以 发挥 ES 适合大 宽 表的 场 景以及 Doris on ES 列存 读 取模式的 实现 ,更保障了 这 快的高性能
流量 类
对于流量,在数据清洗后,直接基于 kafka 入 Doris 即可,这块主要是利用 Doris RollUp 的能力,提供低延迟的数据查询能力
OneService
虽然上述可以初步满足业务的需求,但是从站在最终系统可持续运维的目标态来看,还有很多潜在的问题需要提供解决的空间 如何保障 查询稳 定性 多个用 户 Sql 查询,某个查询导致集群被打垮,如何快速止损 多个 场 景都在 查询 某一 张 表,如何做到可控的降 级 如何保障入 库 的数据 质 量 避免数据乱序覆盖 …… 保障数据在多个 库 之 间 的无 损 、低成本迁移 …… 如从 Hive 迁移到 Doris 、 ES 迁移到 Mysql…… 如何提高易用性 数 仓 内支持 Sql 的系 统 很多,如 Hive 的 Hql 、 Flink-Sql…… 在部分函数 语 法上会由于差异,如何透明的打平 这 些差异。而不是 让 用 户 不断的学 习 异构 语 法 数据如果跨云同步,提供多集群数据同步、 查询 切 换 ,如何 对业务 透明的完成 部分表需要自 动 Rotate 的能力,自 动删 除 过 期的数据 ……
上述的这些问题虽然短期内无法一一解决,但是需要提供一个能力:将来解决时控制成本,尽量做到对业务无感知。
这些都需要进一步定义出系统的接口边界,否则耦合各个系统,后续使用的用户越多,问题持续时间越久、迁移成本也越高。
因此我们设计了 OneModel 来统一数据模型,并且构建了 OpenAPI 来统一服务接口。
目前完成的功能包括 OpenAPI 上 Sql 缓存 基于 业务线 的 查询 条件控制,如 query_timeout OneModel 随着 Flink 系 统 的引入, 结 合原离 线 数 仓 的表,数据表在不同存 储 上分布越来越多如 Kafka 、 Redis 、 Doris 、 Hive…… ,因此构建 < 数据表, Schema ,存 储 > 的元数据,支持数据表在不同存 储 上的映射关系, 统 一表 逻辑视图 ,提升使用效率 引入了 Json-schema ,保 证 入 库质 量符合数据模型定 义 其他 Rotate Table 规范化数据协议,基于数据版本解决数据写入时乱序问题 ……
基于上述的设计,一方面支持业务功能的同时,更重要的是切分了整个系统的接口,来降低各个系统的耦合。有几点具体的好处 : 数据清洗系 统 和 查询 系 统 基于 Kafka 解耦, 这样 当 查询 系 统临时 异常 时 ,不会阻塞 计 算系 统 。且多个 Topic 可以天然支持正常数据流 & 修数数据流的同步入 库 。 业务层通过统一的接口来进行数据访问,在访问入口处可以统一方便的进行流量调度,统一的解决稳定性问题 由于整个系 统闭 包,且接口基于数据 协议 耦合, 稳 定性和易用性得到了兼 顾
应用表现
基于 Doris on ES 的查询系统上线数月,一直到经历了运营大促的活动,均表现出了非常好的稳定性。每天百万级次调用, 99 分位延迟 ~ 秒级
我们的人效也得到了了数十倍的提升:从过去一个需求 “ 进入查询系统到对外交付数据 ” 需要数人周,提升到当前模式的小时级甚至分钟级。
总结与规划
通过引入 doris ,解决了我们明细 & 聚合数据查询不统一的问题,奠定了整个数据中台在查询侧的基石,对于后续数仓向数据中台发展的路径起到了非常关键的作用。
规划 跨集群 实时 同步。在异地多活等 场 景下,目前缺少 类 似 mysql-binlog 的 实时 同步能力,需要构建低成本的数据 实时 同步能力,支持在 线业务 的 稳 定性。 Doris on ES 多表 Join 性能。在 长 尾的需求下, Join 需要 扫 描两 张 表的全部数据 进 行内存 计 算,尤其是大表 Join 大表,延 迟 就会升高。 Doris on ES 表分区能力。如 对 于 ES 的 Rotate 表,目前 Doris 无法 识别 新表或者自 动删 除老的表映射,需要 频 繁 创 建 Doris 表来 对应 ES.Index 。 Doris on ES 表自 动 同步能力。如 ES 表 Schema 修改后,可以自 动 同步到 Doris 。 Doris 平台化运 维 ,如建表、修改表、数据 导 出 ……
更多 Doris on ES 的 2020 规划,请参见: https://github.com/apache/incubator-doris/issues/3306
致 谢
在此非常感谢百度 Doris 团队特别是 @wuyunfeng 、 @imay 等同学热情、给力、靠谱的技术支持!!!我们也希望后续一起参与到 Doris 的开发建设中来!
欢迎来撩!
在线教育属于当前还在持续高速增长的业务赛道,作业帮作为一家专注于 K12 的在线教育公司,当前已经累计激活用户 8 亿 + ,月活 1.7 亿 + 。
作业帮大数据团队致力于面向公司构建数据中台,这里可以接触到大数据下的分布式计算、存储等多种前沿的工程架构技术,欢迎各位感兴趣的小伙伴来撩 ~
联系邮箱: milimin@zuoyebang.com
数据库
2020-04-23 21:54:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
前言 几条PG读书笔记,并谈谈个人浅见,欢迎讨论。
我去年出差略多,于是在路上把目前主要的两本PostgreSQL书大概翻了翻,做了些笔记,谈点个人看法。
以下简称PG,反正都懂。文内对PG有误解或说错的地方还请批评指正。
第一部分笔记,基于《PostgreSQL修炼之道》一书为主。该书唐成著,2015年出版。此时PG的最新版本应该是9.4。本文亦有基于其他资料。
注:以下“唐老师”指唐成老师,“张老师”指张文升老师。
1. AUTOCOMMIT关键字
事务自动提交模式关键字AUTOCOMMIT,只能大写,小写不行,大小写混合也不行。
唐老师: AUTOCOMMIT,是指psql默认autocommit是on的,我见过的多数人喜欢自动提交。如果觉得这样不安全,可以在.psqlrc中一次性配置好,就不同改了。
点评: 这个是小事,但用起来稍微有点不太方便。

2. 什么时候开始表分区
建议当表大小超过PG可用的物理内存时,就开始做表分区。不太了解这个建议是怎么得来的...
唐老师: 这个表具体多大该建分区,不同人有不同的认识。通常如果超过内存大小,cache的作用就很弱了。所以超过了物理内存大小,一定应该分区了。
实际上,目前机器的内存都比较大,如512G,实际上表远远没有到512G大小就应该建分区。我个人认为超过32GB,就应该建成分区表。
点评: 个人看法,表分区并不是必要的,要综合考虑这个表的宽度(行平均长度)、事务活跃度、数据分布情况,不一而同。

3. 最大事务ID不能超过INT32
事务ID不能超过4字节(32-bit)整数,而且还存在回卷的问题。
唐老师: 关于事物ID 32bit的问题,实际上不是把32bit改成64bit就解决问题了。个人认为如果只是改成64bit,实际上没有太大用处。
PG比较保守,32bit虽然可以到20亿,但一般参数设置到2亿,就开始vacuum了。实际中还不如直接把autovacuum freeze max_age从2亿设置成8亿。
这本质还是一个垃圾回收的问题,就像java的垃圾回收,永远有人说需要优化。
另外,vacuum目前对单个表不能并发,当表比较大时,会导致vacuum很长时间。
张老师: 这个历史遗留问题,不过autovacuum近几年的改进可以很大程度缓解事务回滚的问题,但还是治标不治本。
有商业公司将事务号改为int64了,但改动比较多,所以一直没有合并到master,主要依靠运维人员解决,当然,int64是根本的方案,应该包含在以后的发行版中。
点评: 虽然通过调整优化回收机制可以缓解这个问题,但我还是觉得升级到64位INT会更好一些。

4. 主从复制功能
最最最要命的,PG直到9.0(2010年发布)才开始支持主从复制,而MySQL在2000年发布3.23版本时就支持了,早了10年,但B乎上有人说MySQL 8.0终于追上PG 9.6的主要功能。
唐老师: 关于主备库,实际上不是说9.0之前搭建不了主备库,也可以搭建。就是延迟太大,不方便。当然也可以自己写程序来实现减少延迟,但总之就是不方便。这也是别人说PG太学院派的原因,这么重要的功能不早点加上。
点评: 对于互联网,一年的变化就已经非常大了,主从复制这个功能晚了十年才推出,的确很不应该。

5. 版本管理机制
看了下PG的发版模式,大感惊讶。前阵子,同天同时发布12.2、11.7、10.12、9.6.17、9.5.21 和9.4.26共6个版本。再看release notes,以9.0系列为例,基本上每个月都会发布个小版本。这么频繁发版,产品路线规划和版本管理挺堪忧的,做过产品和项目管理的同学们应该有此体会。
这里,其实还有个花边小料。PG发版其实是被各个大商业公司控制的,这些公司想加啥功能,不想加啥功能,都是他们几个巨头说了算。为什么维护那么多大版本,也是因为巨头各自的利益平衡。我道听途说的哈,不对这个料的真假负责。
唐老师: 版本的发布,这是误解。PG是固定每个季度发一个小版本,发这个季度发现的问题都修掉。而一般小版本基本不加功能,只修bug。而发现一个bug,这个bug可能在不同的大版本中都是同一个问题。通常所以12.2、11.7、10.12等等这些实际上是一个发布。
PG一年才发布一个大版本,相当于把功能累积一年,才发布,所以发布并不频繁。一般小版本升级很容易,只需要换二进制程序就可以了。另小版本修复的bug,大多是一些比较偏门的bug,有一些用户有时小版本也懒得升级。
巨头们控制,我的认知是这是阴谋论,不存在这个问题。只是PG内核组的人,比较保守,如果你对代码做了大量的修改,内核组的人认为无法review代码和测试发现你的代码的bug,可能就不接受。小修小改,他们最容易接受。大的功能需要拆很多次提交才行。如PG的并行功能,实际上在9.5版本,一些并行的代码就提交进入了,但这些代码只是一些框架和支持代码,不提供任何 可以使用的功能。然后9.6在作出一些并行的功能。后面在慢慢完善这个功能。
张老师: 关于版本发布的问题,PG的支持版本是5年,你看到的更新版本是针对不同的LTS的release,这个比较好理解。不过PG是被大公司控制的好像不合理,可以订阅PG官方的邮件了解一下合并到master的情况。
点评: 个人还是对PG的发版机制保留意见,对个人用户来说,可能可以避免了被迫升级新版本的“痛苦”,但从项目管理角度说,并行太多计划,相信也会对项目质量有一定影响,见仁见智吧。

6. 第三方插件升级
PG有很多功能需要安装插件,如果做版本升级时候,这些插件也要升级适配。比较要命的是,这些插件大多不是原生的,是第三方提供的,升级适配性比较差。MySQL也有第三方引擎,不过几个主流的引擎都是有比较给力的公司/团队在维护,跟随新版本方面还算及时。
唐老师: PG插件。如果PG插件没有与PG的SQL执行器或优化器等核心功能绑定以及在数据库中创建自己的元数据表,通常是可以做到二进制兼容的。如你自己只是几个bit位运算的函数。这是升级是比较方便的,只要把你的插件的.so拷贝到新版本上就可以了。但如果与版本有关了,升级就有一些麻烦。另一些第三方的差距,质量也是层次不齐,使用的时候需要注意。
点评: 个人感觉PG的第三方插件比MySQL更多些,管理起来也不容易。

7. 表分区实现方式
据说在PG11前,表分区是通过触发器插件实现的,这个感觉有点诧异...
唐老师: 分区,实际上PG11的之前的分区都是通过很早之前的 表继承来实现的。
而在PG10之前,需要手工建触发器或创建规则来辅助完成分区。这些分区存在一个问题,一方面是触发器,性能会导致插入性能低(当然看你的应用,如果对性能没有这么敏感,其实也没有问题),另就是当分区很多时,而表继承是可以让各个分区的表结构不一样的,这样每个分区都有独立元数据,硬解析的代价与与分区数成线性关系,导致分区很多时,硬解析代价变的很大(硬解析达到1毫秒以上)。
另关于分区,实际上表继承的功能,在某些场景下,用很多的一些作用。如在线的腾挪数据。用作分区,只是表继承的一个方面。当然,如果PG开始就没有表继承的功能,那么表分区的功能估计早都完善了。因为有表继承的功能导致全面的分区功能没有这么急迫,所以拖到了PG12版本才彻底完善好了。
当然在PG9.X时代,有第三方的插件pg_pathman,用这个插件实现分区功能也非常好,功能也很强大。但因为社区核心组的人都是一些很严谨的人,而pg_pathman的代码量比较大,同时修改的都是很核心的部分,所以没有把pg_pathman的代码直接合并到内核中,而是慢慢根据pg_pathman的原理,在经历了PG10、11、12后,才把分区表的功能给完善了。
张老师: PG10之前的表分区是该吐槽,用继承表搞分区不止麻烦,性能还不好。不过PG12的分区表确实有一些可圈可点的地方,可以关注。另外,PG的分区表不支持逻辑复制,这点也不好。
点评: 不知道PG铁粉会不会说“yeah,PG的分区比MySQL的更牛逼?”

8. PG没有UNDO LOG
都知道PG著名的"问题",即不记录undo log,而是在表空间中记录每条数据的N个历史版本以实现MVCC,但也带来了后续的vacumn行为,不知道这个"问题"现在有无更好的解决办法。
唐老师: 无undo log。实际上目前vacuum只要合理的配置,并不是什么严重的问题。但目前小白很多,不会做合理的配置,导致这方面的问题较多。对于高并发的数据库,就需要DBA的用心维护。但大家总是想,不需要深入了解这个问题,对PG的默认参数不做修改,就让PG数据库能马上支持高并发的业务场景,目前看这个问题无解。PG9.6或更前面的版本,默认创建出来的数据库的默认参数,有一些在我们一般使用中是不太合理的。到后续版本的PG,如PG10,创建出来的默认参数就合理了一些。当然这可能也有一些历史原因,一些参数默认都是按机械硬盘来设置的。一些加快vacuum同时又要减少对线上业务影响的参数默认没有打开。
点评: PG的MVCC实现机制的确独树一帜,在恢复回滚时确实有优势,但伴随的vacumn问题始终是个阴影。
另一本由谭峰和张文升主笔的《PostgreSQL实战》笔记以后找机会再聊。
受限于个人对PG的认识 水平 , 本文仅涉及到几个很小的方面。本文也并不表示我对PG的态度就是负面的,PG也是个伟大的数据库,MySQL亦是如此,各有所爱吧,止战,各自发力造福技术社区才是正道。
全文完
本文首发 https://mp.weixin.qq.com/s/_UWUHyvBf7Uz5_d_f20Bqg
数据库
2020-04-06 21:56:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
本文主要讨论图数据库背后的设计思路、原理还有一些适用的场景,以及在生产环境中使用图数据库的具体案例。
从社交网络谈起
下面这张图是一个社交网络场景,每个用户可以发微博、分享微博或评论他人的微博。这些都是最基本的增删改查,也是大多数研发人员对数据库做的常见操作。而在研发人员的日常工作中除了要把用户的基本信息录入数据库外,还需找到与该用户相关联的信息,方便去对单个的用户进行下一步的分析,比如说:我们发现张三的账户里有很多关于 AI 和音乐的内容,那么我们可以据此推测出他可能是一名程序员,从而推送他可能感兴趣的内容。
这些数据分析每时每刻都会发生,但有时候,一个简单的数据工作流在实现的时候可能会变得相当复杂,此外数据库性能也会随着数据量的增加而锐减,比如说获取某管理者下属三级汇报关系的员工,这种统计查询在现在的数据分析中是一种常见的操作,而这种操作往往会因为数据库选型导致性能产生巨大差异。
传统数据库的解决思路
传统数据库的概念模型及查询的代码
传统解决上述问题最简单的方法就是 建立一个关系模型 ,我们可以把每个员工的信息录入表中,存在诸如 MySQL 之类的关系数据库,下图是最基本的关系模型:
但是基于上述的关系模型,要实现我们的需求,就不可避免地涉及到很多关系数据库 JOIN  操作,同时实现出来的查询语句也会变得相当长(有时达到上百行): (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM ( SELECT manager.pid AS directReportees, 0 AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") UNION SELECT manager.pid AS directReportees, count(manager.directly_manages) AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT manager.pid AS directReportees, count(reportee.directly_manages) AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT manager.pid AS directReportees, count(L2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM ( SELECT manager.directly_manages AS directReportees, 0 AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") UNION SELECT reportee.pid AS directReportees, count(reportee.directly_manages) AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT depth1Reportees.pid AS directReportees, count(depth2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM( SELECT reportee.directly_manages AS directReportees, 0 AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT L2Reportees.pid AS directReportees, count(L2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT L2Reportees.directly_manages AS directReportees, 0 AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") )
这种 glue 代码对维护人员和开发者来说就是一场灾难,没有人想写或者去调试这种代码,此外,这类代码往往伴随着严重的性能问题,这个在之后会详细讨论。
传统关系数据库的性能问题
性能问题的本质在于数据分析面临的数据量,假如只查询几十个节点或者更少的内容,这种操作是完全不需要考虑数据库性能优化的,但当节点数据从几百个变成几百万个甚至几千万个后,数据库性能就成为了整个产品设计的过程中最需考虑的因素之一。
随着节点的增多,用户跟用户间的关系,用户和产品间的关系,或者产品和产品间的关系都会呈指数增长。
以下是一些公开的数据,可以反映数据、数据和数据间关系的一些实际情况: 推特:用户量为 5 亿,用户之间存在关注、点赞关系 亚马逊:用户量 1.2 亿,用户和产品间存在购买关系 AT&T(美国三大运营商之一): 1 亿个号码,电话号码间可建立通话关系
如下表所示,开源的图数据集往往有着上千万个节点和上亿的边的数据:
Data set name nodes edges YahooWeb 1.4 Billion 6 Billion
Symantec Machine-File Graph 1 Billion 37 Billion
Twitter
Phone call network
104 Million
30 Million
3.7 Billion
260 Million
在数据量这么大的场景中,使用传统 SQL 会产生很大的性能问题,原因主要有两个: 大量 JOIN 操作带来的开销 :之前的查询语句使用了大量的 JOIN 操作来找到需要的结果。而大量的 JOIN 操作在数据量很大时会有巨大的性能损失,因为数据本身是被存放在指定的地方,查询本身只需要用到部分数据,但是 JOIN 操作本身会遍历整个数据库,这样就会导致查询效率低到让人无法接受。 反向查询带来的开销 :查询单个经理的下属不需要多少开销,但是如果我们要去反向查询一个员工的老板,使用表结构,开销就会变得非常大。表结构设计得不合理,会对后续的分析、推荐系统产生性能上的影响。比如,当关系从_老板 -> 员工 _变成  用户 -> 产品 ,如果不支持反向查询,推荐系统的实时性就会大打折扣,进而带来经济损失。
下表列出的是一个非官方的性能测试(社交网络测试集,一百万用户,每个大概有 50 个好友),体现了在关系数据库里,随着好友查询深度的增加而产生的性能变化:
levels RDBMS execution time(s) 2 0.016
3 4
30.267 1543.595
传统数据库的常规优化策略
策略一:索引
索引 :SQL 引擎通过索引来找到对应的数据。
常见的索引包括 B- 树索引和哈希索引,建立表的索引是比较常规的优化 SQL 性能的操作。B- 树索引简单地来说就是给每个人一个可排序的独立 ID,B- 树本身是一个平衡多叉搜索树,这个树会将每个元素按照索引 ID 进行排序,从而支持范围查找,范围查找的复杂度是 O(logN)  ,其中 N 是索引的文件数目。
但是索引并不能解决所有的问题,如果文件更新频繁或者有很多重复的元素,就会导致很大的空间损耗,此外索引的 IO 消耗也值得考虑,索引 IO 尤其是在机械硬盘上的 IO 读写性能上来说非常不理想,常规的 B- 树索引消耗四次 IO 随机读,当 JOIN 操作变得越来越多时,硬盘查找更可能发生上百次。
策略二:缓存
缓存 :缓存主要是为了解决有 具有空间或者时间局域性 数据的频繁读取带来的性能优化问题。一个比较常见的使用缓存的架构是 lookaside cache architecture 。下图是之前 Facebook 用 Memcached  + MySQL 的实例(现已被 Facebook 自研的图数据库 TAO 替代):
在架构中,设计者假设用户创造的内容比用户读取的内容要少得多,Memcached 可以简单地理解成一个分布式的支持增删改查的哈希表,支持上亿量级的用户请求。基本的使用流程是当客户端需读数据时,先查看一下缓存,然后再去查询 SQL 数据库。而当用户需要写入数据时,客户端先删除缓存中的 key,让数据过期,再去更新数据库。但是这种架构有几个问题: 首先,键值缓存对于图结构数据并不是一个好的操作语句,每次查询一条边,需要从缓存里把节点对应的边全部拿出来;此外,当更新一条边,原来的所有依赖边要被删除,继而需要重新加载所有对应边的数据,这些都是并发的性能瓶颈,毕竟实际场景中一个点往往伴随着几千条边,这种操作带来的时间、内存消耗问题不可忽视。 其次,数据更新到数据读取有一个过程,在上面架构中这个过程需要主从数据库跨域通信。原始模型使用了一个外部标识来记录过期的键值对,并且异步地把这些读取的请求从只读的从节点传递到主节点,这个需要跨域通信,延迟相比直接从本地读大了很多。(类似从之前需要走几百米的距离而现在需要走从北京到深圳的距离)
使用图结构建模
上述关系型数据库建模失败的主要原因在于数据间缺乏内在的关联性,针对这类问题,更好的建模方式是使用图结构。 假如数据本身就是表格的结构,关系数据库就可以解决问题,但如果你要展示的是数据与数据间的关系,关系数据库反而不能解决问题了,这主要是在查询的过程中不可避免的大量 JOIN 操作导致的,而每次 JOIN 操作却只用到部分数据,既然反复 JOIN 操作本身会导致大量的性能损失,如何建模才能更好的解决问题呢?答案在点和点之间的关系上。
点、关联关系和图数据模型
在我们之前的讨论中,传统数据库虽然运用 JOIN 操作把不同的表链接了起来,从而隐式地表达了数据之间的关系,但是当我们要通过 A 管理 B,B 管理 A 的方式查询结果时,表结构并不能直接告诉我们结果。 如果我们想在做查询前就知道对应的查询结果,我们必须先定义节点和关系。
节点和关系先定义是图数据库和别的数据库的核心区别 。打个比方,我们可以把经理、员工表示成不同的节点,并用一条边来代表他们之前存在的管理关系,或者把用户和商品看作节点,用购买关系建模等等。而当我们需要新的节点和关系时,只需进行几次更新就好,而不用去改变表的结构或者去迁移数据。
根据节点和关联关系,之前的数据可以根据下图所示建模:
通过图数据库 Nebula Graph 原生 nGQL 图查询语言进行建模,参考如下操作: -- Insert People INSERT VERTEX person(ID, name) VALUES 1:(2020031601, ‘Jeff’); INSERT VERTEX person(ID, name) VALUES 2:(2020031602, ‘A’); INSERT VERTEX person(ID, name) VALUES 3:(2020031603, ‘B’); INSERT VERTEX person(ID, name) VALUES 4:(2020031604, ‘C’); -- Insert edge INSERT EDGE manage (level_s, level_end) VALUES 1 -> 2: ('0', '1') INSERT EDGE manage (level_s, level_end) VALUES 1 -> 3: ('0', '1') INSERT EDGE manage (level_s, level_end) VALUES 1 -> 4: ('0', '1')
而之前超长的 query 语句也可以通过 Cypher / nGQL 缩减成短短的 3、4 行代码。
下面为 nGQL 语句 GO FROM 1 OVER manage YIELD manage.level_s as start_level, manage._dst AS personid | GO FROM $personid OVER manage where manage.level_s < start_level + 3 YIELD SUM($$.person.id) AS TOTAL, $$.person.name AS list
下面为 Cypher 版本 MATCH (boss)-[:MANAGES*0..3]->(sub), (sub)-[:MANAGES*1..3]->(personid) WHERE boss.name = “Jeff” RETURN sub.name AS list, count(personid) AS Total
从近百行代码变成 3、4 行代码可以明显地看出图数据库在数据表达能力上的优势。
图数据库性能优化
图数据库本身对高度连接、结构性不强的数据做了专门优化。不同的图数据库根据不同的场景也做了针对性优化,笔者在这里简单介绍以下几种图数据库,BTW,这些图数据库都支持原生图建模。
Neo4j
Neo4j 是最知名的一种图数据库,在业界有微软、ebay 在用 Neo4j 来解决部分业务场景,Neo4j 的性能优化有两点,一个是原生图数据处理上的优化,一个是运用了 LRU-K 缓存来缓存数据。
原生图数据处理优化
我们说一个图数据库支持原生图数据处理就代表这个数据库有能力去支持  index-free adjacency 。
index-free adjancency 就是每个节点会保留连接节点的引用,从而这个节点本身就是连接节点的一个索引,这种操作的性能比使用全局索引好很多,同时假如我们根据图来进行查询,这种查询是与整个图的大小无关的,只与查询节点关联边的数目有关,如果用 B 树索引进行查询的复杂度是 O(logN),使用这种结构查询的复杂度就是 O(1)。当我们要查询多层数据时,查询所需要的时间也不会随着数据集的变大而呈现指数增长,反而会是一个比较稳定的常数,毕竟每次查询只会根据对应的节点找到连接的边而不会去遍历所有的节点。
主存缓存优化
在 2.2 版本的 Neo4j 中使用了 LRU-K 缓存,这种缓存简而言之就是将使用频率最低的页面从缓存中弹出,青睐使用频率更高的页面,这种设计保证在统计意义上的缓存资源使用最优化。
JanusGraph
JanusGraph 本身并没有关注于去实现存储和分析,而是实现了图数据库引擎与多种索引和存储引擎的接口,利用这些接口来实现数据和存储和索引。JanusGraph 主要目的是在原来框架的基础上支持图数据的建模同时优化图数据序列化、图数据建模、图数据执行相关的细节。JanusGraph 提供了模块化的数据持久化、数据索引和客户端的接口,从而更方便地将图数据模型运用到实际开发中。
此外,JanusGraph 支持用 Cassandra、HBase、BerkelyDB 作为存储引擎,支持使用 ElasticSearch、Solr 还有 Lucene 进行数据索引。 在应用方面,可以用两种方式与 JanusGraph 进行交互: 将 JanusGraph 变成应用的一部分进行查询、缓存,并且这些数据交互都是在同一台 JVM 上执行,但数据的来源可能在本地或者在别的地方。 将 JanusGraph 作为一个服务,让客户端与服务端分离,同时客户端提交 Gremlin 查询语句到服务器上执行对应的数据处理操作。
Nebula Graph
下面简单地介绍了一下 Nebula Graph 的系统设计。
使用 KV 对来进行图数据处理
Nebula Graph 使用了 vertexID + TagID  作为键在不同的 partition 间存储 in-key 和 out-key 相关的数据,这种操作可以确保在大规模集群上的高可用,使用分布式的 partition 和 sharding 也增加了 Nebula Graph 的吞吐量和容错的能力。
Shared-noting 分布式存储层
Storage Service 采用 shared-nothing 的分布式架构设计,每个存储节点都有多个本地 KV 存储实例作为物理存储。Nebula 采用多数派协议 Raft 来保证这些 KV 存储之间的一致性(由于 Raft 比 Paxo 更简洁,我们选用了 Raft)。在 KVStore 之上是图语义层,用于将图操作转换为下层 KV 操作。 图数据(点和边)通过 Hash 的方式存储在不同 partition 中。这里用的 Hash 函数实现很直接,即 vertex_id 取余 partition 数。在 Nebula Graph 中,partition 表示一个虚拟的数据集,这些 partition 分布在所有的存储节点,分布信息存储在 Meta Service 中(因此所有的存储节点和计算节点都能获取到这个分布信息)。
无状态计算层
每个计算节点都运行着一个无状态的查询计算引擎,而节点彼此间无任何通信关系。计算节点仅从 Meta Service 读取 meta 信息,以及和 Storage Service 进行交互。这样设计使得计算层集群更容易使用 K8s 管理或部署在云上。 计算层的负载均衡有两种形式,最常见的方式是在计算层上加一个负载均衡(balance),第二种方法是将计算层所有节点的 IP 地址配置在客户端中,这样客户端可以随机选取计算节点进行连接。 每个查询计算引擎都能接收客户端的请求,解析查询语句,生成抽象语法树(AST)并将 AST 传递给执行计划器和优化器,最后再交由执行器执行。
图数据库是当今的趋势
在当今,图数据库收到了更多分析师和咨询公司的关注 Graph analysis is possibly the single most effective  competitive differentiator for organizations  pursuing data-driven operations and  decisions after the design of data capture.        --------------Gartner “Graph analysis is the true killer app for Big Data.”      --------------------Forrester
同时图数据库在 DB-Ranking 上的排名也呈现出上升最快的趋势,可见需求之迫切:
图数据库实践:不仅仅是社交网络
Netflix 云数据库的工程实践
Netflix 采用了JanusGraph + Cassandra + ElasticSearch 作为自身的图数据库架构,他们运用这种架构来做数字资产管理。 节点表示数字产品比如电影、纪录片等,同时这些产品之间的关系就是节点间的边。 当前的 Netflix 有大概 2 亿的节点,70 多种数字产品,每分钟都有上百条的 query 和数据更新。 此外,Netflix 也把图数据库运用在了授权、分布式追踪、可视化工作流上。比如可视化 Git 的 commit,jenkins 部署这些工作。
Adobe 的技术迭代
一般而言,新技术往往在开始的时候大都不被大公司所青睐,图数据库并没有例外,大公司本身有很多的遗留项目,而这些项目本身的用户体量和使用需求又让这些公司不敢冒着风险来使用新技术去改变这些处于稳定的产品。Adobe 在这里做了一个迭代新技术的例子,用 Neo4j 图数据库替换了旧的 NoSQL Cassandra 数据库。
这个被大改的系统名字叫 Behance,是 Adobe 在 15 年发布的一个内容社交平台,有大概 1 千万的用户,在这里人们可以分享自己的创作给百万人看。
这样一个巨大的遗留系统本来是通过 Cassandra 和 MongoDB 搭建的,基于历史遗留问题,系统有不少的性能瓶颈不得不解决。 MongoDB 和 Cassandra 的读取性能慢主要因为原先的系统设计采用了 fan-out 的设计模式——受关注多的用户发表的内容会单独分发给每个读者,这种设计模式也导致了网络架构的大延迟,此外 Cassandra 本身的运维也需要不小的技术团队,这也是一个很大的问题。
在这里为了搭建一个灵活、高效、稳定的系统来提供消息 feeding 并最小化数据存储的规模,Adobe 决定迁移原本的 Cassandra 数据库到 Neo4j 图数据库。 在 Neo4j 图数据库中采用一种所谓的  Tiered relationships 来表示用户之间的关系,这个边的关系可以去定义不同的访问状态,比如:仅部分用户可见,仅关注者可见这些基本操作。 数据模型如图所示
使用这种数据模型并使用 Leader-follower 架构来优化读写,这个平台获得了巨大的性能提升:
运维需求的时长在使用了 Neo4j 以后下降了 300%。 存储需求降低了 1000 倍, Neo4j 仅需 50G 存储数据, 而 Cassandra 需要 50TB。 仅仅需要 3 个服务实例就可以支持整个服务器的流畅运行,之前则需要 48 个。 图数据库本身就提供了更高的可扩展性。
结论
在当今的大数据时代,采用图数据库可以用小成本在原有架构上获得巨大的性能提升。图数据库不仅仅可以在 5G、AI、物联网领域发挥巨大的推动作用,同时也可以用来重构原本的遗留系统。 虽然不同的图数据库可能有着截然不同的底层实现,但这些都完全支持用图的方式来构建数据模型从而让不同的组件之间相互联系,从我们之前的讨论来看,这一种数据模型层次的改变会极大地简化很多日常数据系统中所面临的问题,增大系统的吞吐量并且降低运维的需求。
图数据库的介绍就到此为止了,如果你对图数据库 Nebula Graph 有任何想法或其他要求,欢迎去 GitHub: https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛: https://discuss.nebula-graph.io/ 的 Feedback 分类下提建议 👏;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot
Reference [1] An Overview Of Neo4j And The Property Graph Model Berkeley, CS294, Nov 2015 https://people.eecs.berkeley.edu/~istoica/classes/cs294/15/notes/21-neo4j.pdf [2] several original data sources from talk made by Duen Horng (Polo) Chau ( Geogia tech ) www.selectscience.net www.phonedog.com、www.mediabistro.com www.practicalecommerce.com/ [3] Graphs / Networks Basics, how to build & store graphs, laws, etc. Centrality, and algorithms you should know   Duen Horng (Polo) Chau(Georgia tech) [4] Graph databases, 2nd Edition: New Oppotunities for Connected Data [5] R. Nishtala, H. Fugal, S. Grimm, M. Kwiatkowski, H. Lee, H. C.Li, R. McElroy, M. Paleczny, D. Peek, P. Saab, D. Stafford, T. Tung, and V. Venkataramani. Scaling Memcache at Facebook.In Proceedings of the 10th USENIX conference on Networked Systems Design and Implementation, NSDI, 2013. [6] Nathan Bronson, Zach Amsden, George Cabrera, Prasad Chakka, Peter Dimov Hui Ding, Jack Ferris, Anthony Giardullo, Sachin Kulkarni, Harry Li, Mark Marchukov Dmitri Petrov, Lovro Puzar, Yee Jiun Song, Venkat Venkataramani  TAO: Facebook's Distributed Data Store for the Social Graph USENIX 2013 [7] Janus Graph Architecture https://docs.janusgraph.org/getting-started/architecture/ [8] Nebula Graph Architecture — A Bird's View https://nebula-graph.io/en/posts/nebula-graph-architecture-overview/ [9] database engine trending https://db-engines.com/en/ranking_categories [10] Netflix Content Data Management talk https://www.slideshare.net/RoopaTangirala/polyglot-persistence-netflix-cde-meetup-90955706#86 [11] Harnessing the Power of Neo4j for Overhauling Legacy Systems at Adobe https://neo4j.com/graphconnect-2018/session/overhauling-legacy-systems-adobe
推荐阅读 聊聊图数据库和图数据库的小知识 Nebula Graph 技术总监陈恒:图数据库怎么和深度学习框架进行结合? 聊聊图数据库和图数据库的小知识 Vol.02 图数据库爱好者的聚会在谈论什么? 作者有话说:Hi,我是 Johhan。目前在 Nebula Graph 实习,研究和实现大型图数据库查询引擎和存储引擎组件。作为一个图数据库及开源爱好者,我在博客分享有关数据库、分布式系统和 AI 公开可用学习资源。
数据库
2020-04-01 15:50:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
之前有过一次面试,关于MySQL索引的原理及使用被面试官怼的体无完肤,立志要总结一番,然后一直没有时间(其实是懒……),准备好了吗?
索引是什么?
数据库索引,是数据库管理系统(DBMS)中一个排序的数据结构,它可以对数据库表中一列或多列的值进行排序,以协助更加快速的访问数据库表中特定的数据。通俗的说,我们可以把数据库索引比做是一本书前面的目录,它能加快数据库的查询速度。
为什么需要索引?
思考:如何在一个图书馆中找到一本书? 设想一下,假如在图书馆中没有其他辅助手段,只能一条道走到黑,一本书一本书的找,经过3个小时的连续查找,终于找到了你需要看的那本书,但此时天都黑了。为了避免这样的事情,每个图书馆才都配备了一套图书馆管理系统,大家要找书籍的话,先在系统上查找到书籍所在的房屋编号、图书架编号还有书在图书架几层的那个方位,然后就可以直接大摇大摆的去取书了,就可以很快速的找到我们所需要的书籍。索引就是这个原理,它可以帮助我们快速的检索数据。
一般的应用系统对数据库的操作,遇到最多、最容易出问题是一些复杂的查询操作,当数据库中数据量很大时,查找数据就会变得很慢,这样就很影响整个应用系统的效率,我们就可以使用索引来提高数据库的查询效率。
B-Tree和B+Tree
目前大部分数据库系统及文件系统都采用B-Tree或其变种B+Tree作为索引结构, 我在这里分别讲一下:
B-Tree
即B树,注意(不是B减树),B树是一种多路搜索树。 使用B-Tree结构可以显著减少定位记录时所经历的中间过程,从而加快存取速度。
B-Tree有如下一些 特征 : 定义任意非叶子结点最多只有M个子节点,且M>2。 根结点的儿子数为[2, M]。 除根结点以外的非叶子结点的儿子数为[M/2, M], 向上取整 。 每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)。 非叶子结点的关键字个数=指向儿子的指针个数-1。 非叶子结点的关键字:K[1], K[2], …, K[M-1],且K[i] <= K[i+1]。 非叶子结点的指针:P[1], P[2], …,P[M](其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树)。 所有叶子结点位于同一层。
有关b树的一些 特性 : 关键字集合分布在整颗树的所有结点之中; 任何一个关键字出现且只出现在一个结点中; 搜索有可能在非叶子结点结束; 其搜索性能等价于在关键字全集内做一次二分查找。
B树的 搜索 :从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复执行这个操作,直到所对应的节点指针为空,或者已经是是叶子结点。
例如下面一个B树,那么查找元素43的过程如下:
根据根节点指针找到18、37所在节点,把此节点读入内存,进行第一次磁盘IO,此时发现43>37,找到指针p3。
根据指针p3,找到42、51所在节点,把此节点读入内存,进行第二次磁盘IO,此时发现42<43<51,找到指针p2。
根据指针p2,找到43、46所在节点,把此节点读入内存,进行第三次磁盘IO,此时我们就已经查到了元素43。
在此过程总共进行了三次磁盘IO。
B+Tree
B+Tree属于B-Tree的变种。与B-Tree相比,B+Tree有以下不同点: 有n棵子树的非叶子结点中含有n个关键字(B树是n-1个),即 非叶子结点的子树指针与关键字个数相同。 这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(B树是每个关键字都保存数据)。 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。 所有的非叶子结点可以看成是叶子节点的索引部分。 同一个数字会在不同节点中重复出现,根节点的最大元素就是b+树的最大元素。
相对B树,B+树做索引的优势 B+树的磁盘IO代价更低: B+树非叶子节点没有指向数据行的指针,所以相同的磁盘容量存储的节点数更多,相应的IO读写次数肯定减少了。 B+树的查询效率更加稳定:由于所有数据都存于叶子节点。所有关键字查询的路径长度相同,每一个数据的查询效率相当。 所有的叶子节点形成了一个有序链表,更加便于查找。
关于MySQL的两种常用存储引擎MyISAM和InnoDB的索引均以B+树作为数据结构,二者却有不同(这里只说二者 索引 的区别)。
MyISAM索引和Innodb索引的区别
MyISAM使用B+树作为索引结构,叶节点 叶节点的data域 保存的是存储数据的 地址 , 主键索引 key值唯一, 辅助索引 key可以重复,二者在结构上相同。 因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果要找的Key存在,则取出其data域的值, 然后以data域的值为地址,去读取相应数据记录 。因此,索引文件和数据文件是分开的,从索引中检索到的是数据的地址,而不是数据。
Innodb也是用B+树作为索引结构, 但具体实现方式却与MyISAM截然不同,首先, 数据表本身就是按照b+树组织,所以 数据文件本身就是 主键索引 文件 。 叶节点key值为数据表的主键,data域为 完整的数据记录 , 因此InnoDB表数据文件本身就是 主键索引 ( 这也就是 MyISAM可以允许没有主键,但是Innodb必须有主键的原因) 。 第二个与MyISAM索引的不同是InnoDB的 辅助索引 的data域存储相应数据记录的主键值而不是地址。换句话说,InnoDB的所有 辅助索引 都引用主键作为data域。
索引类型
普通索引 : (由关键字KEY或INDEX定义的索引)的唯一任务是加快对数据的访问速度。
唯一索引 : 普通索引允许被索引的数据列包含重复的值,而唯一索引不允许,但是可以为null。所以任务是保证访问速度和 避免数据出现重复。
主键索引 :在主键字段创建的索引,一张表只有一个主键索引。
组合索引 :多列值组成一个索引,专门用于组合搜索。
全文索引 :对文本的内容进行分词,进行搜索。( MySQL5.6及以后的版本,MyISAM和InnoDB存储引擎均支持全文索引。 )
索引的使用策略及 优缺点
使用索引
主键自动建立唯一索引。
经常作为查询条件在WHERE或者ORDER BY 语句中出现的列要建立索引。
查询中与其他表关联的字段,外键关系建立索引。
经常用于聚合函数的列要建立索引,如min(),max()等的聚合函数。
不使用索引
经常增删改的列不要建立索引。
有大量重复的列不建立索引。
表记录太少不要建立索引, 因为数据较少,可能查询全部数据花费的时间比遍历索引的时间还要短,索引就可能不会产生优化效果 。
最左匹配原则
建立联合索引的时候都会默认从最左边开始,所以索引列的顺序很重要,建立索引的时候就应该把最常用的放在左边,使用select的时候也是这样,从最左边的开始,依次匹配右边的。
优点
可以保证数据库表中每一行的数据的唯一性。
可以大大加快数据的索引速度。
加速表与表之间的连接。
可以显著的减少查询中分组和排序的时间。
缺点
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
索引需要占物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间,如果需要建立聚簇索引,那么需要占用的空间会更大,其实建立索引就是以空间换时间。
表中的数据进行增、删、改的时候,索引也要动态的维护,这就降低了维护效率。
验证索引是否能够提升查询性能
创建测试表index_test
使用python脚本程序通过pymsql模块,向表中添加十万条数据 import pymysql def main(): # 创建 Connection 连接 conn = pymysql.connect( host = 'localhost' , port = 3306 , database = 'db_test' , user = 'root' , password = 'deepin' , charset = 'utf8' ) # 获得 Cursor 对象 cursor = conn.cursor() # 插入 10 万次数据 for i in range ( 100000 ): cursor.execute( "insert into index_test values('haha-%d')" % i) # 提交数据 conn.commit() if __name__ == "__main__" : main()
在mysql终端开启运行时间监测:set profiling=1;
查找第1万条数据ha-99999 select * from index_test where name='haha-99999';
查看执行的时间: show profiles ;
为表index_test的name列创建索引: create index name_index on index_test(name( 10 ));
再次执行查询语句、查看执行的时间:
可以看出合适的 索引确实可以明显提高某些字段的查询效率。
最后,感谢女朋友在生活中,工作上的包容、理解与支持 !
数据库
2020-04-01 10:41:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
文章的开头我们先来看下什么是图数据库,根据维基百科的定义: 图数据库是使用图结构进行语义查询的数据库,它使用节点、边和属性来表示和存储数据 。
虽然和关系型数据库存储的结构不同(关系型数据库为表结构,图数据库为图结构),但不计各自的性能问题,关系型数据库可以通过递归查询或者组合其他 SQL 语句(Join)完成图查询语言查询节点关系操作。得益于 1987 年 SQL 成为国际标准化组织(ISO)标准,关系型数据库行业得到了很好的发展。同 60、70 年代的关系型数据库类似,图数据库这个领域的查询语言目前也没有统一标准,虽然 19 年 9 月经过国际 SQL 标准委员会投票表决,决定将图查询语言(Graph Query Language)纳为一种新的数据库查询语言,但 GQL 的制定仍需要一段时间。
介于市面上没有统一的图查询语言标准,在本文中我们选取市面上主流的几款图查询语言来分析一波用法,由于篇幅原因本文旨在简单介绍图查询语言和常规用法,更详细的内容将在进阶篇中讲述。
图查询语言·介绍
图查询语言 Gremlin
Gremlin  是 Apache ThinkerPop 框架下的图遍历语言。Gremlin 可以是 声明性 的也可以是 命令性 的。虽然 Gremlin 是基于 Groovy 的,但具有许多语言变体,允许开发人员以 Java、JavaScript、Python、Scala、Clojure 和 Groovy 等许多现代编程语言 原生编写 Gremlin 查询 。
支持图数据库:Janus Graph、InfiniteGraph、Cosmos DB、DataStax Enterprise(5.0+) 、Amazon Neptune
图查询语言 Cypher
Cypher 是一个描述性的图形查询语言,允许不必编写图形结构的遍历代码对图形存储有表现力和效率的查询,和 SQL 很相似,Cypher 语言的关键字不区分大小写,但是属性值,标签,关系类型和变量是区分大小写的。
支持图数据库: Neo4j、RedisGraph、AgensGraph
图查询语言 nGQL
nGQL 是一种类 SQL 的声明型的文本查询语言,nGQL 同样是关键词大小写不敏感的查询语言,目前支持模式匹配、聚合运算、图计算,可无嵌入组合语句。
支持图数据库:Nebula Graph
图查询语言·术语篇
在比较这 3 个图查询语言之前,我们先来看看他们各自的术语,如果你翻阅他们的文档会经常见到下面这些“关键字”,在这里我们不讲用法,只看这些图数据库常用概念在这 3 个图数据库文档中的叫法。
术语 Gremlin Cypher nGQL 点 Vertex Node Vertex
Edge Relationship Edge
点类型 Label Label Tag
边类型 label RelationshipType edge type
点 ID vid id(n) vid
边 ID eid id(r)
插入
删除 更新属性
add
drop setProperty
create
delete set
insert
delete / drop update
我们可以看到大体上对点和边的叫法类似,只不过 Cypher 中直接使用了 Relationship 关系一词代表边。其他的术语基本都非常直观。
图查询语言·语法篇
了解过 Gremlin、Cypher、nGQL 中常见的术语之后,我们来看看使用这 3 个图查询语言过程中会需要了解的常规语法。
图 # Gremlin 创建图 g = TinkerGraph.open().traversal() # nGQL 创建图空间 CREATE SPACE gods

图结构由点和边组成,一条边连接两个点。在 Gremlin 和 nGQL 中称之为 Vertex,Cypher 则称之为 Node。如何在图数据库中新建一个点呢?可以参考下面的语法 # Gremlin 创建/插入点 g.addV(vertexLabel).property() # Cypher 创建点 CREATE (:nodeLabel {property}) # nGQL 创建/插入点 INSERT VERTEX tagName (propNameList) VALUES vid:(tagKey propValue)
点类型
点允许有对应的类型,在 Gremlin 和 Cypher 叫 label  ,在 nGQL 中为 tag  。点类型可对应有多种属性(Property),例如 _Person _可以有 name 、_age _等属性。
创建点类型
点类型相关的语法示例如下: # Gremlin 创建点类型 g.addV(vertexLabel).property() # nGQL 创建点类型 CREATE tagName(PropNameList)
这里说明下,无论在 Gremlin 和 nGQL 中存在类似  IF NOT EXISTS   用法,即:如果不存在则创建,存在则直接返回。
查看点类型
创建好点之后如何查看点类型呢,可以参考以下方式。  # Gremlin 查看(获取)点类型 g.V().label().dedup(); # Cypher 查看点类型方法 1 MATCH (n) RETURN DISTINCT labels(n) # Cypher 查看点类型方法 2 CALL db.labels(); # nGQL 查看点类型 SHOW TAGS
点的 CRUD
上面简单介绍了点、点类型,下面进入数据库基本 DML——CRUD,在上文介绍点时顺便介绍了点的创建和插入,这里说下如何插入特定类型的点,和点的获取、删除和更新。
插入特定类型点
和插入点的操作类似,只不过需要指定某种点类型。语法参考: # Gremlin 插入特定类型点 g.addV(String vertexLabel).property() # Cypher 插入特定类型点 CREATE (node:label) # nGQL 插入特定类型点 INSERT VERTEX (prop_name_list) VALUES :(prop_value_list)
查看点 # Gremlin 查看点 g.V() # Cypher 查看点 MATCH (n) WHERE condition RETURN properties(n) # nGQL 查看点 FETCH PROP ON
删除点
术语篇中提过 nGQL 中删除操作对应单词有  Delete  和  Drop  ,在 nGQL 中 Delete 一般用于点边,Drop 用于 Schema 删除,这点和 SQL 的设计思路是一样的。 # Gremlin 删除点 g.V().drop() # Cypher 删除点 MATCH (node:label) DETACH DELETE node # nGQL 删除点 DELETE VERTEX
更新点
用数据库的小伙伴都知道数据的常态是数据变更,来瞅瞅这 3 个图查询是使用什么语法来更新点数据的吧 # Gremlin 更新点 g.V().property() # Cypher 更新点 SET n.prop = V # nGQL 更新点 UPDATE VERTEX SET
可以看到 Cypher 和 nGQL 都使用 SET 关键词来设置点对应的类型值,只不过 nGQL 中多了 UPDATE 关键词来标识操作,Gremlin 的操作和上文提到的查看点类似,只不过增加了变更 property 值操作。

在 Gremlin 和 nGQL 称呼边为 Edge,而 Cypher 称之为 Relationship。下面进入到边相关的语法内容
边类型 
和点一样,边也可以有对应的类型 # Gremlin 创建边类型 g.edgeLabel() # nGQL 创建边类型 CREATE EDGE edgeTypeName(propNameList)
边的 CRUD
说完边类型应该进入到边的常规操作部分了
插入指定边类型的边
可以看到和点的使用语法类似,只不过在 Cypher 和 nGQL 中分别使用 -[]->  和 ->  来表示关系,而 Gremlin 则用 to()  关键词来标识指向关系,在使用这 3 种图查询语言的图数据库中的边均为有向边,下图左边为有向边,右边为无向边。
# Gremlin 插入指定边类型的边 g.addE(String edgeLabel).from(v1).to(v2).property() # Cypher 插入指定边类型的边 CREATE (:)- [(:)] ->(:) # nGQL 插入指定边类型的边 INSERT EDGE () VALUES -> : \ ()
删除边 # Gremlin 删除边 g.E().drop() # Cypher 删除边 MATCH (:)-[r:relationship-label-name]->() DELETE r # nGQL 删除边 DELETE EDGE ->
查看指定边 # Gremlin 查看指定边 g.E() # Cypher 查看指定边 MATCH (n)-[r:label]->() WHERE condition RETURN properties(r) # nGQL 查看指定边 FETCH PROP ON ->
其他操作
除了常规的点、边 CRUD 外,我们可以简单看看这 3 种图查询语言的组合查询。
指定点查指定边 # Gremlin 指定点查指定边 g.V().outE() # Cypher 指定点查指定边 Match (n)->[r:label]->[] WHERE id(n) = vid RETURN r # nGQL 指定点查指定边 GO FROM OVER
沿指定点反向查询指定边
在反向查询中,Gremlin 使用了 in 来表示反向关系,而 Cypher 则更直观的将指向箭头反向变成 <-  来表示反向关系,nGQL 则用关键词 REVERSELY  来标识反向关系。 # Gremlin 沿指定点反向查询指定边 g.V().inE() # Cypher 沿指定点反向查询指定边 MATCH (n)<-[r:label]-() # nGQL 沿指定点反向查询指定边 GO FROM OVER REVERSELY
无向遍历
如果在图中,边的方向不重要(正向、反向都可以),那 Gremlin 使用 both()  ,Cypher 使用 -[]-  ,nGQL使用关键词 BIDIRECT  。 # Traverse edges with specified vertices Gremlin g.V().bothE() # Traverse edges with specified vertices Cypher MATCH (n)-[r:label]-() # Traverse edges with specified vertices nGQL GO FROM OVER BIDIRECT
沿指定点查询指定边 N 跳
Gremlin 和 nGQL 分别用 times 和 step 来表示 N 跳关系,而 Cypher 用 relationship*1..N  来表示 N 跳关系。 # Gremlin 沿指定点查询指定边 N 跳 g.V().repeat(out()).times(N) # Cypher 沿指定点查询指定边 N 跳 MATCH (n)-[r:label*N]->() WHERE condition RETURN r # nGQL 沿指定点查询指定边 N 跳 GO N STEPS FROM OVER
返回指定两点路径 # Gremlin 返回指定两点路径 g.V().repeat(out()).until().path() # Cypher 返回指定两点路径 MATCH p =(a)-[.*]->(b) WHERE condition RETURN p # nGQL 返回指定两点路径 FIND ALL PATH FROM TO OVER *
图查询语言·实操篇
说了一通语法之后,是时候展示真正的技术了——来个具体一点的例子。
示例图:The Graphs of Gods
实操示例使用了 Janus Graph 的示例图 The Graphs of Gods 。该图结构如下图所示,描述了罗马万神话中诸神关系。
插入数据 # 插入点 ## nGQL nebula> INSERT VERTEX character(name, age, type) VALUES hash("saturn"):("saturn", 10000, "titan"), hash("jupiter"):("jupiter", 5000, "god"); ## Gremlin gremlin> saturn = g.addV("character").property(T.id, 1).property('name', 'saturn').property('age', 10000).property('type', 'titan').next(); ==>v[1] gremlin> jupiter = g.addV("character").property(T.id, 2).property('name', 'jupiter').property('age', 5000).property('type', 'god').next(); ==>v[2] gremlin> prometheus = g.addV("character").property(T.id, 31).property('name', 'prometheus').property('age', 1000).property('type', 'god').next(); ==>v[31] gremlin> jesus = g.addV("character").property(T.id, 32).property('name', 'jesus').property('age', 5000).property('type', 'god').next(); ==>v[32] ## Cypher cypher> CREATE (src:character {name:"saturn", age: 10000, type:"titan"}) cypher> CREATE (dst:character {name:"jupiter", age: 5000, type:"god"}) # 插入边 ## nGQL nebula> INSERT EDGE father() VALUES hash("jupiter")->hash("saturn"):(); ## Gremlin gremlin> g.addE("father").from(jupiter).to(saturn).property(T.id, 13); ==>e[13][2-father->1] ## Cypher cypher> CREATE (src)-[rel:father]->(dst)
删除数据 # nGQL nebula> DELETE VERTEX hash("prometheus"); # Gremlin gremlin> g.V(prometheus).drop(); # Cypher cypher> MATCH (n:character {name:"prometheus"}) DETACH DELETE n
更新数据 # nGQL nebula> UPDATE VERTEX hash("jesus") SET character.type = 'titan'; # Gremlin gremlin> g.V(jesus).property('age', 6000); ==>v[32] # Cypher cypher> MATCH (n:character {name:"jesus"}) SET n.type = 'titan';
查看数据 # nGQL nebula> FETCH PROP ON character hash("saturn"); =================================================== | character.name | character.age | character.type | =================================================== | saturn | 10000 | titan | --------------------------------------------------- # Gremlin gremlin> g.V(saturn).valueMap(); ==>[name:[saturn],type:[titan],age:[10000]] # Cypher cypher> MATCH (n:character {name:"saturn"}) RETURN properties(n) ╒════════════════════════════════════════════╕ │"properties(n)" │ ╞════════════════════════════════════════════╡ │{"name":"saturn","type":"titan","age":10000}│ └────────────────────────────────────────────┘
查询 hercules 的父亲 # nGQL nebula> LOOKUP ON character WHERE character.name == 'hercules' | \ -> GO FROM $-.VertexID OVER father YIELD $$.character.name; ===================== | $$.character.name | ===================== | jupiter | --------------------- # Gremlin gremlin> g.V().hasLabel('character').has('name','hercules').out('father').values('name'); ==>jupiter # Cypher cypher> MATCH (src:character{name:"hercules"})-[:father]->(dst:character) RETURN dst.name ╒══════════╕ │"dst.name"│ ╞══════════╡ │"jupiter" │ └──────────┘
查询 hercules 的祖父 # nGQL nebula> LOOKUP ON character WHERE character.name == 'hercules' | \ -> GO 2 STEPS FROM $-.VertexID OVER father YIELD $$.character.name; ===================== | $$.character.name | ===================== | saturn | --------------------- # Gremlin gremlin> g.V().hasLabel('character').has('name','hercules').out('father').out('father').values('name'); ==>saturn # Cypher cypher> MATCH (src:character{name:"hercules"})-[:father*2]->(dst:character) RETURN dst.name ╒══════════╕ │"dst.name"│ ╞══════════╡ │"saturn" │ └──────────┘
查询年龄大于 100 的人物 # nGQL nebula> LOOKUP ON character WHERE character.age > 100 YIELD character.name, character.age; ========================================================= | VertexID | character.name | character.age | ========================================================= | 6761447489613431910 | pluto | 4000 | --------------------------------------------------------- | -5860788569139907963 | neptune | 4500 | --------------------------------------------------------- | 4863977009196259577 | jupiter | 5000 | --------------------------------------------------------- | -4316810810681305233 | saturn | 10000 | --------------------------------------------------------- # Gremlin gremlin> g.V().hasLabel('character').has('age',gt(100)).values('name'); ==>saturn ==>jupiter ==>neptune ==>pluto # Cypher cypher> MATCH (src:character) WHERE src.age > 100 RETURN src.name ╒═══════════╕ │"src.name" │ ╞═══════════╡ │ "saturn" │ ├───────────┤ │ "jupiter" │ ├───────────┤ │ "neptune" │ │───────────│ │ "pluto" │ └───────────┘
从一起居住的人物中排除 pluto 本人 # nGQL nebula> GO FROM hash("pluto") OVER lives YIELD lives._dst AS place | GO FROM $-.place OVER lives REVERSELY WHERE \ $$.character.name != "pluto" YIELD $$.character.name AS cohabitants; =============== | cohabitants | =============== | cerberus | --------------- # Gremlin gremlin> g.V(pluto).out('lives').in('lives').where(is(neq(pluto))).values('name'); ==>cerberus # Cypher cypher> MATCH (src:character{name:"pluto"})-[:lives]->()<-[:lives]-(dst:character) RETURN dst.name ╒══════════╕ │"dst.name"│ ╞══════════╡ │"cerberus"│ └──────────┘
Pluto 的兄弟们 # which brother lives in which place? ## nGQL nebula> GO FROM hash("pluto") OVER brother YIELD brother._dst AS god | \ GO FROM $-.god OVER lives YIELD $^.character.name AS Brother, $$.location.name AS Habitations; ========================= | Brother | Habitations | ========================= | jupiter | sky | ------------------------- | neptune | sea | ------------------------- ## Gremlin gremlin> g.V(pluto).out('brother').as('god').out('lives').as('place').select('god','place').by('name'); ==>[god:jupiter, place:sky] ==>[god:neptune, place:sea] ## Cypher cypher> MATCH (src:Character{name:"pluto"})-[:brother]->(bro:Character)-[:lives]->(dst) RETURN bro.name, dst.name ╒═════════════════════════╕ │"bro.name" │"dst.name"│ ╞═════════════════════════╡ │ "jupiter" │ "sky" │ ├─────────────────────────┤ │ "neptune" │ "sea" │ └─────────────────────────┘
附录 Nebula Graph:一个开源的分布式图数据库 GitHub: https://github.com/vesoft-inc/nebula 官方论坛: https://discuss.nebula-graph.io 官方博客: https://nebula-graph.io/cn/posts/ 微博: weibo.com/nebulagraph
数据库
2020-03-05 10:45:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
上一章聊到时序数据是什么样,物联网行业中的时序数据的特点:存量数据大、新增数据多(采集频率高、设备量多)。详情请见:
时序数据库 Apache-IoTDB 源码解析之前言(一)
打一波广告,欢迎大家访问 IoTDB 仓库 ,求一波 Star 。
这一章主要想聊一聊: 物联网行业的基本系统架构,及使用数据库遇到的需求与挑战 IoTDB 的功能特点及系统架构
车联网
因为本人是在做车联网行业,所以对这个行业的信息了解更深入一些,能够拿到一些更具体的数字来说明这个行业的具体情况。在上一篇文中的数据是出于自己的理解,为了让大家容易明白而编造的数据,但实际情况要复杂的多。
1. 系统架构
1.1 系统简介
以上示意图可能非常简单,但我觉得足够表明一个整体架构。 当一台设备、一辆车连接到协议网关后,便开始了真正的收发数据。一般通信的方式都是基于 tcp ,搞一段二进制协议,所以协议网关基本要做的工作就是完成对连接的管理、完成对数据的收发及编解码。
当数据完成编解码之后一般会发往消息队列当中,一般都是 Kafka 之中。用来解耦生产和消费两端,提供一层缓冲,无论消费服务是死是活还是速度慢,包治百病,甚至还能治未病。
数据发往消息队列的过程中,或之后花活儿就多起来了。但主要的我认为无非还是三种处理方式: 需要将 原始数据 保存入库,这里的原始数据包含二进制数据和解码后的二进制数据。 流处理或批处理数据,在数据落到硬盘之前将能够提前计算的数据全部预先计算出来,这样做的好处是将来查询的时候如果和预计算的模型匹配,那就能非常快的得到结果。 离线处理,这里的应用就太广泛了,一般来讲都是将耗时比较大的放置离线计算来做。但是这里要声明一点,离线计算依然是越快越好,不能因为他叫离线计算所以在设计或开发阶段就不关注时效。
1.2 数据质量
上一章提到了基本的数据质量,但实际工作中,往往质量会出现各种意想不到的数据,下面是工程中影响数据质量的几个比较大的问题: 数据丢失,不管是在采集,上报,数据流转环节,都可能会带来一定的数据丢失比例。 数据乱序,数据在打包、上报、流转等环节均可能出现乱序,尤其是在补传数据中。 数据重复,数据重复发送,尤其是在网络不好时。 数据本身不准确,这个最突出的地方就是在 GPS 数据中,经常出现飘点、噪点等等。
2. 数据库的挑战
2.1 数据项多
汽车里具有非常复杂的电路系统和传感器设备,我印象当中的粗略估算应该是有 120 项左右,并且这些数据项并不是车内数据的全部。随着自动驾驶的到来,汽车的传感器会越来越多,数据项就会更多。
如果按照传统的 Mysql 存储,那么由于行式存储,所以在取回数据时候就会非常影响效率,之后介绍到 IoTDB 的文件格式的时候再聊。
2.2 存量数据大
我们按照宝马汽车 2019 销售量估计,252 万辆,我们假定 4 年前就已经具备了联网模块那就是 1000 万辆汽车。按照每条数据 1K,每天采样上传 1 次,应该是 每天 9G 数据。但因为车不可能一直都点火开,所以要假设一个 30% 的在线率,那就是 3G 数据。
3 年大约就会存储 3TB 数据,可能你觉得 3T 数据对于时下最热的大数据来讲并不是一个非常庞大的数字,但如果整个数据里面不包含任何图片、音视频甚至都没有文字,全部是由整数、浮点数堆积起来的,那你可以试想一下这个数据库里面到底有多少数据,如果你一个不小心执行了 count(*) 你觉得会卡死不?
2.3 采集频率高
汽车不同于其他传感器的地方是,他是一个处在时刻运动当中的物体,如果需要做一些高阶的计算模型,比如说:碰撞检测、行驶轨迹纠偏等,那么相应的数据采集频率有可能要达到秒级。
当然我说完这句话,可能你感触并不是很深,但是结合前面说到的两点:120项数据、1000万辆车,将采集频率提高到 1 秒一采集,那么这个频率下,一天产生的数据大小就是 259T。这时候你找 DBA 说,哥们我们的需求要 1 天写入 259T 数据,我觉得反正我是没脸找人家,让产品去跟 DBA 聊吧。
2.4 大数据分析需求
现有时序数据库都无法支持大数据分析框架,都需要通过数据库的 Api 把数据从数据库往数据仓库导出后再存一份,数据量直接翻倍。 举个例子,我如果需要对 Mysql 中的数据进行 MapReduce 计算,那么只能是将数据通过 JDBC 接口导出到 Hbase 或 Hdfs 上,然后执行计算,可能你觉得这很正常,但按照上面提到的数据量,数据复制之后你公司里可能就需要每天支撑 500T 数据。
2.4 不同数据库遇到的问题
我们公司也采用了多种尝试,从开始的 MySql 到 MongoDB 再到 Hbase 等等,它们总存在这一点或多点的让你觉得就是不满足的地方,如下图:
IoTDB
到此为止,整体需求基本明确,作为一款物联网的时序数据库需要处理的问题: 高速写入 高效压缩 多维度查询,降采样、时序分割查询等 查询低延迟高效 提高数据质量,乱序、空值等 对接现有大数据生态
IoTDB 功能特点
IoTDB 完成了上述问题中的几乎所有功能,而且可以灵活对接多生态,高性能优势等。那么 IoTDB 是如何完成这些优势项,如何做到?
IoTDB 架构描述
对照上面的图,大致了解一下 IoTDB 的结构,逻辑上被分为 3 个大部分,其中: Engine 是完整的数据库进程,负责 sql 语句的解析,数据写入、查询、元数据管理等功能。 Storage 是底层存储结构,类似于Mysql 的 idb 文件 Analyzing Layer 是各种连接器,暂不涉及细节。
Engine 和 Storage 中主要包含: IoTDB Engine,也就是代码中的 Server 模块. Native API,他是高效写入的基石,代码中的 Session 模块 JDBC,传统的 JDBC 连接调用方式,代码中的 JDBC 模块 TsFile,这是整个数据库的一个特色所在,传统的数据库如果使用 Spark 做离线分析,或者 ETL 都需要通过数据库进程对外读取,而 IoTDB 可以直接迁移文件,省去了来回转换类型的开销。TsFile 提供了两种读写模式,一种基于 HDFS,一种基于本地文件。
聊到这里,我们基本介绍了行业内的特点,作为数据库需要解决的痛点,以及 IoTDB 在完成功能的同时所具有的自身的优势。同时还简单介绍了 IoTDB 的基础架构,朴实无华且枯燥。那么 TsFile 究竟是什么样的结构才能完成以上所介绍的高速写入、高压缩比、高速查询呢?Native API 又是什么?欢迎继续关注。。。
数据库
2020-02-08 01:36:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
前言 Redis 5 新特性中,Streams 数据结构的引入,可以说它是在本次迭代中最大特性。它使本次 5.x 版本迭代中,Redis 作为消息队列使用时,得到更完善,更强大的原生支持,其中尤为明显的是持久化消息队列。同时,stream 借鉴了 kafka 的消费组模型概念和设计,使消费消息处理上更加高效快速。本文就 Streams 数据结构中常用 API 进行分析。
准备
本文所使用 Redis 版本为 5.0.5 。如果使用更早的 5.x 版本,有些 API 使用效果,与本文中描述略有不同。
添加消息
Streams 添加数据使用 XADD 指令进行添加,消息中的数据以 K-V 键值对的形式进行操作。一条消息可以存在多个键值对,添加命令格式: XADD key ID field string [field string ...]
其中 key 为 Streams 的名称,ID 为消息的唯一标志,不可重复,field string 就为键值对。下面我们就添加以 person 为名称的流,进行操作。 XADD person * name ytao des https://ytao.top
上面添加案例中,ID 使用 * 号复制,这里代表着服务端自动生成 Id,添加后返回数据 "1578238486193-0"
这里自动生成的 Id 格式为 - Id 是由两部分组成: millisecondsTime 为当前服务器时间毫秒时间戳。 sequenceNumber 当前序列号,取值来源于当前毫秒内,生成消息的顺序,默认从 0 开始加 1 递增。
比如:1578238486193-3 表示在 1578238486193 毫秒的时间戳时,添加的第 4 条消息。
除了服务端自动生成 Id 方式外,也支持指定 Id 的生成,但是指定 Id 有以下条件限制: Id 中的前后部分必须为数字。 最小 Id 为 0-1,不能为 0-0,但是 2-0,3-0 .... 是被允许的。 添加的消息,Id 的前半部分不能比存在 Id 最大的值小,Id 后半部分不能比存在前半部分相同的最大后半部分小。
否则,当不满足上述条件时,添加后会抛出异常: (error) ERR The ID specified in XADD is equal or smaller than the target stream top item
实际上,当添加一条消息时,会进行两部操作。第一步,先判断如果不存在 Streams,则创建 Streams 的名称,再添加消息到 Streams 中。即使添加消息时,由于 Id 异常,也可以在 Redis 中存在以当前 Streams 的名称。 Streams 中 Id 也可作为指针使用,因为它是一个有序的标记。
生产中,如果这样使用添加消息,会存在一个问题,那就是消息数量太大时,会使服务宕机。这里 Streams 的设计初期也有考虑到这个问题,那就是可以指定 Streams 的容量。如果容量操作这个设定的值,就会对调旧的消息。在添加消息时,设置 MAXLEN 参数。 XADD person MAXLEN 5 * name ytao des https://ytao.top
这样就指定该了 Streams 中的容量为 5 条消息。也可使用 XTRIM 截取消息,从小到大剔除多余的消息: XTRIM person MAXLEN 8
消息数量
查看消息数量使用 XLEN 指令进行操作。 XLEN key
例:查看 person 流中的消息数量: > XLEN person (integer) 5
查询消息
查询 Streams 中的消息使用 XRANGE 和 XREVRANGE 指令。
XRANGE
查询数据时,可以按照指定 Id 范围进行查询,XRANGE 查询指令格式: XRANGE key start end [COUNT count]
参数说明: key 为 Streams 的名称 start 为范围查询开始 Id,包含本 Id。 start 为范围查询结束 Id,包含本 Id。 Count 为查询返回最大的消息数量,非必填。
这里 start 和 end 有 - 和 + 两个非指定值,他们分别表示无穷小和无穷大,所以当使用这个两个值时,会查询出全部的消息。 > XRANGE person - + 1) 1) "0-1" 2) 1) "name" 2) "ytao" 3) "des" 4) "https://ytao.top" 2) 1) "0-2" 2) 1) "name" 2) "luffy" 3) "des" 4) "valiant!" 3) 1) "2-0" 2) 1) "name" 2) "gaga" 3) "des" 4) "fishion!"
上面查询的消息数据,可以看到是按照先进先出的顺序查询出来的。
使用 COUNT 指定查询返回的数量: # 查询所有的消息,并且返回一条数据 > XRANGE person - + COUNT 1 1) 1) "0-1" 2) 1) "name" 2) "ytao" 3) "des" 4) "https://ytao.top"
在范围查询中,Id 的后半部分可省略,后半部分中的数据会全部查询到。
XREVRANGE
XREVRANGE 的查询和 XRANGE 指令中的使用类似,但查询的 start 和 end 参数顺序进行了调换: XREVRANGE key end start [COUNT count]
使用案例: > XREVRANGE person + - 1) 1) "2-0" 2) 1) "name" 2) "gaga" 3) "des" 4) "fishion!" 2) 1) "0-2" 2) 1) "name" 2) "luffy" 3) "des" 4) "valiant!" 3) 1) "0-1" 2) 1) "name" 2) "ytao" 3) "des" 4) "https://ytao.top"
查询后的结果与 XRANGE 的结果顺序刚好相反,其他都一样,这两个指令可进行消息的升序和降序的返回。
删除消息
删除消息使用 XDEL 指令操作,只需指定将要删除的 Streams 名称和 Id 即可,支持一次删除多个消息 。 XDEL key ID [ID ...]
删除案例: # 查询所有消息 > XRANGE person - + 1) 1) "0-1" 2) 1) "name" 2) "ytao" 3) "des" 4) "https://ytao.top" 2) 1) "0-2" 2) 1) "name" 2) "luffy" 3) "des" 4) "valiant!" 3) 1) "2-0" 2) 1) "name" 2) "gaga" 3) "des" 4) "fishion!" # 删除消息 > XDEL person 2-0 (integer) 1 # 再次查询删除后的所有消息 > XRANGE person - + 1) 1) "0-1" 2) 1) "name" 2) "ytao" 3) "des" 4) "https://ytao.top" 2) 1) "0-2" 2) 1) "name" 2) "luffy" 3) "des" 4) "valiant!" # 查询删除后的长度 > XLEN person (integer) 2
从上面可以看到,删除消息后,长度也会减少相应的数量。
消费消息
在 Redis 的 PUB/SUB 中,我们是通过订阅来消费消息,在 Streams 数据结构中,同样也能实现同等功能,当没有新的消息时,可进行阻塞等待。不仅支持单独消费,而且还可以支持群组消费。
单独消费
单独消费使用 XREAD 指令。可以看到,下面命令中,STREAMS,key, 以及 ID 为必填项。ID 表示将要读取大于该 ID 的消息。当 ID 值使用 $ 赋予时,表示已存在消息的最大 Id 值。 XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
上面的 COUNT 参数用来指定读取的最大数量,与 XRANGE 的用法一样。 > XREAD COUNT 1 STREAMS person 0 1) 1) "person" 2) 1) 1) "0-1" 2) 1) "name" 2) "ytao" 3) "des" 4) "https://ytao.top" > XREAD COUNT 2 STREAMS person 0 1) 1) "person" 2) 1) 1) "0-1" 2) 1) "name" 2) "ytao" 3) "des" 4) "https://ytao.top" 2) 1) "0-2" 2) 1) "name" 2) "luffy" 3) "des" 4) "valiant!"
在 XREAD 里面还有个 BLOCK 参数,这个是用来阻塞订阅消息的, BLOCK 携带的参数为阻塞时间,单位为毫秒,如果在这个时间内没有新的消息消费,那么就会释放该阻塞。当这里的时间指定为 0 时,会一直阻塞,直到有新的消息来消费到。 # 窗口 1 开启阻塞,等待新消息的到来 > XREAD BLOCK 0 STREAMS person $ # 另开一个连接窗口 2,添加一条新的消息 > XADD person 2-2 name tao des coder "2-2" # 窗口 1,获取到有新的消息来消费,并且带有阻塞的时间 > XREAD BLOCK 0 STREAMS person $ 1) 1) "person" 2) 1) 1) "2-2" 2) 1) "name" 2) "tao" 3) "des" 4) "coder" (60.81s)
当使用 XREAD 进行顺序消费时,需要额外记录下读取到位置的 Id,方便下次继续消费。
群组消费
群组消费的主要目的也就是为了分流消息给不同的客户端处理,以更高效的速率处理消息。为达到这一肝功能需求,我们需要做三件事: 创建群组 , 群组读取消息 , 向服务端确认消息以处理 。
群组操作
操作群组使用 XGROUP 指令: XGROUP [CREATE key groupname id-or-$] [SETID key id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
上面命令中,包含操作有: CREATE 创建消费组。 SETID 修改下一个处理消息的 Id。 DESTROY 销毁消费组。 DELCONSUMER 删除消费组中指定的消费者。
我们当前需要使用的是创建消费组: # 以当前存在的最大 Id 作为消费起始 > XGROUP CREATE person group1 $ OK
群组读取消息
群组读取使用 XREADGROUP 指令, COUNT 和 BLOCK 的使用类似 XREAD 的操作,只是多了个群组和消费者的指定: XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
由于群组消费和单独消费类似,这里只进行个阻塞分析,这里 Id 也有个特殊值 > ,表示还未进行消费的消息: # 窗口 1,消费群组中,taotao 消费者建立阻塞监听 XREADGROUP GROUP group1 taotao BLOCK 0 STREAMS person > # 窗口 2,消费群组中,yangyang 消费者建立阻塞监听 XREADGROUP GROUP group1 yangyang BLOCK 0 STREAMS person > # 窗口 3,添加消费消息 > XADD person 3-1 name tony des 666 "3-1" # 窗口 1,读取到新消息,此时 窗口 2 没有任何反应 > XREADGROUP GROUP group1 taotao BLOCK 0 STREAMS person > 1) 1) "person" 2) 1) 1) "3-1" 2) 1) "name" 2) "tony" 3) "des" 4) "666" (77.54s) # 窗口 3,再次添加消费消息 > XADD person 3-2 name james des abc! "3-2" # 窗口 2,读取到新消息,此时 窗口 1 没有任何反应 > XREADGROUP GROUP group1 yangyang BLOCK 0 STREAMS person > 1) 1) "person" 2) 1) 1) "3-2" 2) 1) "name" 2) "james" 3) "des" 4) "abc!" (76.36s)
以上执行流程中,group1 群组中有两个消费者,当添加两条消息后,这两个消费者轮流消费。
消息ACK
消息消费后,为避免再次重复消费,这是需要向服务端发送 ACK,确保消息被消费后的标记。 例如下列情况,我们上面我们将最新两条消息已进行了消费,但是当我们再次读取消息时,还是被读到: > XREADGROUP GROUP group1 yangyang STREAMS person 0 1) 1) "person" 2) 1) 1) "3-2" 2) 1) "name" 2) "james" 3) "des" 4) "abc!"
这时,我们使用 XACK 指令告诉服务器,我们已处理的消息: XACK key group ID [ID ...]0
让服务器标记 3-2 已处理: > XACK person group1 3-2 (integer) 1
再次获取群组读取消息: > XREADGROUP GROUP group1 yangyang STREAMS person 0 1) 1) "person" 2) (empty list or set)
队列中没有了可读消息。 除了上面以讲解到的 API 外,查看消费群组信息可使用 XINFO 指令查看,本文不做分析。
总结 上面对 Streams 常用 API 进行了分析,我们可以感受到 Redis 在消息队列支持的道路上,也越来越强大。如果使用过它的 PUB/SUB 功能的话,就会感受到 5.x 迭代正是将你的一些痛点进行了优化。
个人博客: https://ytao.top
关注公众号 【ytao】,更多原创好文
推荐阅读
ElasticSearch之安装及基本操作API
ElasticSearch之映射常用操作
数据库
2020-01-08 18:30:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
mongodb内核、wiredtiger存储引擎、rocksdb存储引擎相关源码分析详见(后续持续更新):
https://github.com/y123456yz/reading-and-annotate-mongodb-3.6.1
背景
线上某集群峰值TPS超过100万/秒左右(主要为写流量,读流量很低),峰值tps几乎已经到达集群上限,同时平均时延也超过100ms,随着读写流量的进一步增加,时延抖动严重影响业务可用性。该集群采用mongodb天然的分片模式架构,数据均衡的分布于各个分片中,添加片键启用分片功能后实现完美的负载均衡。集群每个节点流量监控如下图所示:

从上图可以看出集群流量比较大,峰值已经突破120万/秒,其中delete过期删除的流量不算在总流量里面(delete由主触发删除,但是主上面不会显示,只会在从节点拉取oplog的时候显示)。如果算上主节点的delete流量,总tps超过150万/秒。
软件优化
在不增加服务器资源的情况下,首先做了如下软件层面的优化,并取得了理想的数倍性能提升: 业务层面优化 Mongodb配置优化 存储引擎优化
2.1 业务层面优化
该集群总文档近百亿条,每条文档记录默认保存三天,业务随机散列数据到三天后任意时间点随机过期淘汰。由于文档数目很多,白天平峰监控可以发现从节点经常有大量delete操作,甚至部分时间点delete删除操作数已经超过了业务方读写流量,因此考虑把delete过期操作放入夜间进行,过期索引添加方法如下:
Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )
上面的过期索引中expireAfterSeconds=0,代表collection集合中的文档的过期时间点在expireAt时间点过期,例如:
db.collection.insert( {
//表示该文档在夜间凌晨1点这个时间点将会被过期删除
"expireAt": new Date('July 22, 2019 01:00:00'),
"logEvent": 2,
"logMessage": "Success!"
} )
通过随机散列expireAt在三天后的凌晨任意时间点,即可规避白天高峰期触发过期索引引入的集群大量delete,从而降低了高峰期集群负载,最终减少业务平均时延及抖动。

Delete过期Tips1: expireAfterSeconds含义
1. 在 expireAt指定的绝对时间点过期,也就是12.22日凌晨2:01过期
Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )
db.log_events.insert( { "expireAt": new Date(Dec 22, 2019 02:01:00'),"logEvent": 2,"logMessage": "Success!"})
在 expireAt指定的时间往后推迟expireAfterSeconds秒过期,也就是当前时间往后推迟60秒过期
db.log_events.insert( {"createdAt": new Date(),"logEvent": 2,"logMessage": "Success!"} )
Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 60 } )

Delete过期Tips2: 为何mongostat只能监控到从节点有delete操作,主节点没有?
原因是过期索引只在master主节点触发,触发后主节点会直接删除调用对应wiredtiger存储引擎接口做删除操作,不会走正常的客户端链接处理流程,因此主节点上看不到delete统计。
主节点过期delete后会生存对于的delete oplog信息,从节点通过拉取主节点oplog然后模拟对于client回放,这样就保证了主数据删除的同时从数据也得以删除,保证数据最终一致性。从节点模拟client回放过程将会走正常的client链接过程,因此会记录delete count统计,详见如下代码:

官方参考如下: https://docs.mongodb.com/manual/tutorial/expire-data/

2.2 Mongodb配置优化(网络IO复用,网络IO和磁盘IO做分离)
由于集群tps高,同时整点有大量推送,因此整点并发会更高,mongodb默认的一个请求一个线程这种模式将会严重影响系统负载,该默认配置不适合高并发的读写应用场景。官方介绍如下:

2.2.1 Mongodb内部网络线程模型实现原理
mongodb默认网络模型架构是一个客户端链接,mongodb会创建一个线程处理该链接fd的所有读写请求及磁盘IO操作。
Mongodb默认网络线程模型不适合高并发读写原因如下:
1. 在高并发的情况下,瞬间就会创建大量的线程,例如线上的这个集群,连接数会瞬间增加到1万左右,也就是操作系统需要瞬间创建1万个线程,这样系统load负载就会很高。
2. 此外,当链接请求处理完,进入流量低峰期的时候,客户端连接池回收链接,这时候mongodb服务端就需要销毁线程,这样进一步加剧了系统负载,同时进一步增加了数据库的抖动,特别是在PHP这种短链接业务中更加明显,频繁的创建线程销毁线程造成系统高负债。
3. 一个链接一个线程,该线程除了负责网络收发外,还负责写数据到存储引擎,整个网络I/O处理和磁盘I/O处理都由同一个线程负责,本身架构设计就是一个缺陷。

2.2.2 网络线程模型优化方法
为了适应高并发的读写场景,mongodb-3.6开始引入serviceExecutor: adaptive配置,该配置根据请求数动态调整网络线程数,并尽量做到网络IO复用来降低线程创建消耗引起的系统高负载问题。此外,加上serviceExecutor: adaptive配置后,借助boost:asio网络模块实现网络IO复用,同时实现网络IO和磁盘IO分离。这样高并发情况下,通过网络链接IO复用和mongodb的锁操作来控制磁盘IO访问线程数,最终降低了大量线程创建和消耗带来的高系统负载,最终通过该方式提升高并发读写性能。
2.2.3 网络线程模型优化前后性能对比
在该大流量集群中增加serviceExecutor: adaptive配置实现网络IO复用及网络IO与磁盘IO做分离后,该大流量集群时延大幅度降低,同时系统负载和慢日志也减少很多,具体如下:
2.2.3.1 优化前后系统负载对比
验证方式: 该集群有多个分片,其中一个分片配置优化后的主节点和同一时刻未优化配置的主节点load负载比较:
未优化配置的load
优化配置的load
2.2.3.2 优化前后慢日志对比
验证方式:
该集群有多个分片,其中一个分片配置优化后的主节点和同一时刻未优化配置的主节点慢日志数比较:
同一时间的慢日志数统计:
未优化配置的慢日志数 (19621) :
优化配置后的慢日志数 (5222):
2.2.3.3 优化前后平均时延对比
验证方式:
该集群所有节点加上网络IO复用配置后与默认配置的平均时延对比如下:
从上图可以看出,网络IO复用后时延降低了1-2倍。
2.3 wiredtiger存储引擎优化
从上一节可以看出平均时延从200ms降低到了平均80ms左右,很显然平均时延还是很高,如何进一步提升性能降低时延?继续分析集群,我们发现磁盘IO一会儿为0,一会儿持续性100%,并且有跌0现象,现象如下:
从图中可以看出,I/O写入一次性到2G,后面几秒钟内I/O会持续性阻塞,读写I/O完全跌0,avgqu-sz、awit巨大,util次序性100%,在这个I/O跌0的过程中,业务方反应的TPS同时跌0。
此外,在大量写入IO后很长一段时间util又持续为0%,现象如下:
总体IO负载曲线如下:
从图中可以看出IO很长一段时间持续为0%,然后又飙涨到100%持续很长时间,当IO util达到100%后,分析日志发现又大量满日志,同时mongostat监控流量发现如下现象:

从上可以看出我们定时通过mongostat获取某个节点的状态的时候,经常超时,超时的时候刚好是io util=100%的时候,这时候IO跟不上客户端写入速度造成阻塞。
有了以上现象,我们可以确定问题是由于IO跟不上客户端写入速度引起,第2章我们已经做了mongodb服务层的优化,现在我们开始着手wiredtiger存储引擎层面的优化,主要通过以下几个方面: cachesize调整 脏数据淘汰比例调整 checkpoint优化
2.3.1 cachesize调整优化(为何cacheSize越大性能越差)
前面的IO分析可以看出,超时时间点和I/O阻塞跌0的时间点一致,因此如何解决I/O跌0成为了解决改问题的关键所在。
找个集群平峰期(总tps50万/s)查看当时该节点的TPS,发现TPS不是很高,单个分片也就3-4万左右,为何会有大量的刷盘,瞬间能够达到10G/S,造成IO util持续性跌0(因为IO跟不上写入速度)。继续分析wiredtiger存储引擎刷盘实现原理,wiredtiger存储引擎是一种B+树存储引擎,mongodb文档首先转换为KV写入wiredtiger,在写入过程中,内存会越来越大,当内存中脏数据和内存总占用率达到一定比例,就开始刷盘。同时当达到checkpoint限制也会触发刷盘操作,查看任意一个mongod节点进程状态,发现消耗的内存过多,达到110G,如下图所示:
于是查看mongod.conf配置文件,发现配置文件中配置的cacheSizeGB: 110G,可以看出,存储引擎中KV总量几乎已经达到110G,按照5%脏页开始刷盘的比例,峰值情况下cachesSize设置得越大,里面得脏数据就会越多,而磁盘IO能力跟不上脏数据得产生速度,这种情况很可能就是造成磁盘I/O瓶颈写满,并引起I/O跌0的原因。
此外,查看该机器的内存,可以看到内存总大小为190G,其中已经使用110G左右,几乎是mongod的存储引起占用,这样会造成内核态的page cache减少,大量写入的时候内核cache不足就会引起磁盘缺页中断,引起大量的写盘。
解决办法: 通过上面的分析问题可能是大量写入的场景,脏数据太多容易造成一次性大量I/O写入,于是我们可以考虑把存储引起cacheSize调小到50G,来减少同一时刻I/O写入的量,从而规避峰值情况下一次性大量写入的磁盘I/O打满阻塞问题。

2.3.2 存储引擎dirty脏数据淘汰优化
调整cachesize大小解决了5s请求超时问题,对应告警也消失了,但是问题还是存在,5S超时消失了,1s超时问题还是偶尔会出现。
因此如何在调整cacheSize的情况下进一步规避I/O大量写的问题成为了问题解决的关键,进一步分析存储引擎原理,如何解决内存和I/O的平衡关系成为了问题解决的关键,mongodb默认存储因为wiredtiger的cache淘汰策略相关的几个配置如下:
wiredtiger淘汰相关配置 默认值 工作原理
eviction_target 80 当用掉的内存超过总内存的百分比达到 eviction_target,后台evict线程开始淘汰
eviction_trigger 95 当用掉的内存超过总内存的 eviction_trigger,用户线程也开始淘汰
eviction_dirty_target 5 当cache中脏数据比例超过 eviction_dirty_target,后台evict线程开始淘汰
eviction_dirty_trigger 20 当cache中脏数据比例超过 eviction_dirty_trigger, 用户线程也开始淘汰
evict.threads_min
evict.threads_max
4
4
后台evict线程最小数
后台evict线程最大数

调整cacheSize从120G到50G后,如果脏数据比例达到5%,则极端情况下如果淘汰速度跟不上客户端写入速度,这样还是容易引起I/O瓶颈,最终造成阻塞。

解决办法: 如何进一步减少持续性I/O写入,也就是如何平衡cache内存和磁盘I/O的关系成为问题关键所在。从上表中可以看出,如果脏数据及总内占用存达到一定比例,后台线程开始选择page进行淘汰写盘,如果脏数据及内存占用比例进一步增加,那么用户线程就会开始做page淘汰,这是个非常危险的阻塞过程,造成用户请求验证阻塞。平衡cache和I/O的方法: 调整淘汰策略,让后台线程尽早淘汰数据,避免大量刷盘,同时降低用户线程阀值,避免用户线程进行page淘汰引起阻塞。优化调整存储引起配置如下:
eviction_target: 75%
eviction_trigger:97%
eviction_dirty_target: %3
eviction_dirty_trigger:25%
evict.threads_min:8
evict.threads_min:12

总体思想是让后台evict尽量早点淘汰脏页page到磁盘,同时调整evict淘汰线程数来加快脏数据淘汰,调整后mongostat及客户端超时现象进一步缓解。

2.3.3 存储引擎checkpoint优化调整
存储引擎得checkpoint检测点,实际上就是做快照,把当前存储引擎的脏数据全部记录到磁盘。触发checkpoint的条件默认又两个,触发条件如下: 固定周期做一次checkpoint快照,默认60s 增量的redo log(也就是journal日志)达到2G
当journal日志达到2G或者redo log没有达到2G并且距离上一次时间间隔达到60s,wiredtiger将会触发checkpoint,如果在两次checkpoint的时间间隔类evict淘汰线程淘汰的dirty page越少,那么积压的脏数据就会越多,也就是checkpoint的时候脏数据就会越多,造成checkpoint的时候大量的IO写盘操作。如果我们把checkpoint的周期缩短,那么两个checkpoint期间的脏数据相应的也就会减少,磁盘IO 100%持续的时间也就会缩短。
checkpoint调整后的值如下:
checkpoint=(wait=25,log_size=1GB)
2.3.4 存储引擎优化前后IO对比
通过上面三个方面的存储引擎优化后,磁盘IO开始平均到各个不同的时间点,iostat监控优化后的IO负载如下:
从上面的io负载图可以看出,之前的IO一会儿为0%,一会儿100%现象有所缓解,总结如下图所示:
2.3.5 存储引擎优化前后时延对比
优化前后时延对比如下(注: 该集群有几个业务同时使用,优化前后时延对比如下):



从上图可以看出,存储引擎优化后时间延迟进一步降低并趋于平稳,从平均80ms到平均20ms左右,但是还是不完美,有抖动。
3 服务器系统磁盘IO问题解决
3.1 服务器IO硬件问题背景
如第3节所述,当wiredtiger大量淘汰数据后,发现只要每秒磁盘写入量超过500M/s,接下来的几秒钟内util就会持续100%,w/s几乎跌0,于是开始怀疑磁盘硬件存在缺陷。

从上图可以看出磁盘为nvMe的ssd盘,查看相关数据可以看出该盘IO性能很好,支持每秒2G写入,iops能达到2.5W/S,而我们线上的盘只能每秒写入最多500M。
3.2 服务器IO硬件问题解决后性能对比
于是考虑把该分片集群的主节点全部迁移到另一款服务器,该服务器也是ssd盘,io性能达到2G/s写入(注意:只迁移了主节点,从节点还是在之前的IO-500M/s的服务器)。 迁移完成后,发现性能得到了进一步提升,时延迟降低到2-4ms/s,三个不同业务层面看到的时延监控如下图所示:

从上图时延可以看出,迁移主节点到IO能力更好的机器后,时延进一步降低到平均2-4ms。
虽然时延降低到了平均2-4ms,但是还是有很多几十ms的尖刺,鉴于篇幅将在下一期分享大家原因,最终保存所有时延控制在5ms以内,并消除几十ms的尖刺。
此外,nvme的ssd io瓶颈问题原因,经过和厂商确认分析,最终定位到是linux内核版本不匹配引起,如果大家nvme ssd盘有同样问题,记得升级linux版本到3.10.0-957.27.2.el7.x86_64版本,升级后nvme ssd的IO能力达到2G/s以上写入。

4 总结及遗留问题
通过mongodb服务层配置优化、存储引擎优化、硬件IO提升三方面的优化后,该大流量写入集群的平均时延从之前的平均数百ms降低到了平均2-4ms,整体性能提升数十倍,效果明显。
但是, 从4.2章节优化后的时延可以看出,集群偶尔还是会有抖动,鉴于篇幅,下期会分享如果消除4.2章节中的时延抖动,最终保持时间完全延迟控制在2-4ms,并且无任何超过10ms的抖动,敬请期待,下篇会更加精彩。
此外,在集群优化过程中采了一些坑,下期会继续分析大流量集群采坑记。
注意: 文章中的一些优化方法并不是一定适用于所有mongodb场景,请根据实际业务场景和硬件资源能力进行优化,而不是按部就班。

oppo互联网运维云存储团队急缺以下人才:
如果对mongodb内核源码、wiredtiger存储引擎、rocksdb存储引擎、数据库机房多活、数据链路同步系统、中间件、数据库等源码研究有兴趣的同学。
欢迎加入oppo大家庭,一起参与oppo百万级高并发文档数据库研发。
工作地点:成都/深圳
邮箱: yangyazhou#oppo.com
近期继续分享如下主题,敬请关注: 百万级高并发mongodb集群性能数十倍提升原理(下) 百万计高并发mongodb集群性能优化采坑记 线上典型集群抖动、不可用等问题汇总分析 Mongodb文档数据库业务使用最佳案例分享

mongodb内核、wiredtiger存储引擎、rocksdb存储引擎相关源码分析详见(后续持续更新):
https://github.com/y123456yz/reading-and-annotate-mongodb-3.6.1
数据库
2019-12-11 19:56:00
本文由云+社区发表 作者:技术沙龙
All in 云+时代,数据库的高可用性、按需付费、按需扩展等属性解放了大批开发者。腾讯发布的自研数据库CynosDB作为国内首款同时兼容MySQL和PG的云原生数据库在业内引发热议,还不够了解TA?那么本期分享你一定不能错过!
本期云+社区技术沙龙将全方位解读CynosDB,揭秘技术内幕,解读兼容两大主流开源数据库的一主多读架构、高可用架构及快速恢复实现、可计算智能存储和分布式存储。
点击这里 ,免费报名参加活动!
此文已由腾讯云+社区在各渠道发布
获取更多新鲜技术干货,可以关注我们 腾讯云技术社区-云加社区官方号及知乎机构号
数据库
2019-03-04 17:30:00
本文整理自虎牙中间件团队在 Nacos Meetup 的现场分享,阿里巴巴中间件受权发布。
这次分享的是全球 DNS 秒级生效在虎牙的实践,以及由此产生的一些思考,整体上,分为以下5各部分: 背景介绍; 方案设计和对比; 高可用; 具体实践和落地; 规划;
背景介绍
虎牙用到的基础技术很多,DNS 是其中比较重要的一个环节。
DNS 的解析过程很关键,例如上图中的 DNS 解析器通过一个定位解析追踪到我们的 DNS,再到本地域名服务器迭代解析,经过根域再到.com名,最后到huya.com的根域名,获取最终的解析结果。
在这个过程中, DNS解析是天然的分布式架构,每一层都会有缓存,上一层出现问题挂掉,下一层都会有缓存进行容灾。另外,整个 DNS 协议支持面广,包括手机和 PC,我们用的编程框架里也有 DNS 解析器,服务器也会配 DNS 解析引擎,因此,DNS 在虎牙的基础设施中是很重要的部分。
虎牙的 DNS 的应用现状
虎牙当前主要是依赖于公共的 DNS,相信在座的小伙伴们或多或少都会遇到过下面这些问题: 依赖公共 localDNS,解析不稳定,延迟大。 记录变更生效时间长,无法及时屏蔽线路和节点异常对业务的影响。例如,权威 DNS 全球各节点数据同步时间不可控,全局生效时间超过10分钟;localDNS 缓存过期时间不可控,部分 localDNS 不遵循TTL时间,缓存时间超过48小时。 内部 DNS 功能缺失,无法解决内部服务调用面临挑战。例如,时延大、解析不准、支持多种调度策略。 无法满足国外业务的快速发展,虽然一些海外云厂商提供了基于 DNS 的快速扩容方案,以及基于 DNS 的数据库切换方案。
方案设计和对比
基于以上的问题,我们开始重新规划 DNS 的设计。
名字服务架构
整个规划会分三个方面,核心是我们做了「名字服务」的中心点,基于此,可以满足我们的需求。
一方面通过 Nacos Sync,将现有多个注册中心的服务, 同步到「名字服务」中, 通过 DNS 实现不同框架之间的 Rest 服务方式的调用, 实现例如 Eureka,Consul,Taf等框架之间的服务调用。
另一方面,在全球负载均衡的场景下,由于虎牙是以音视频业务为主,而音视频业务对节点的延迟是非常敏感的,所以我们希望一旦出现节点延迟的情况,能立马做切换。
第三个是传统 DNS 的场景, 可以满足容器和物理机的 DNS 需求, 提供本机 Agent 和集群两种方案, 通过缓存和 prefect 大大提高 DNS 解析的可用性和加快生效时间。
对于名字服务的总体设计主要分3部分,接入层需要提供 API,消息通知和 DNS 接入的能力。核心功能需要能在基于现有网络数据,CMDB 和 IP 库的数据基础上,提供灵活的负载均衡能力,全球数据的秒级同步,多个数据源的同步,能对全网服务的健康状态进行监控,及时感知到大范围的节点异常,并且能够及时将节点的屏蔽的信息推送到端上。
最终,我们选择 Nacos 作为名字服务的核心,提供统一的 API ,包括名字注册、变化推送、负载均衡等;Nacos Sync 作为全球集群间数据同步的组件;DNS - F是客户端组件,用于拦截 DNS 请求,实现基于 DNS 的名字服务。
改造前后 DNS 变更生效流程的不同
接下来,我们通过对比看下改造前后 DNS 变更生效流程的差异。
原有 DNS 变更生效流程中,对 DNS 生效时间有影响的是:
Auth DNS:
跨区域、跨国数据同步慢,不稳定。
bind 在数据量比较大的时候,同步比较慢。
Local DNS:
根据 TTL 缓存,过期后才会刷新数据。
部分厂商不遵循 TTL 时间缓存,超过24小时的缓存时间。
服务器:
服务器开启 nscd 做 DNS 缓存。
业务进程:
应用的 DNS 缓存,比如 Java 虚拟机、框架层的 DNS 缓存。
以上四种情况会比较影响 DNS 的变更生效流程,下图是我们现有的 DNS 变更生效流程:
整体上相对简单,只要业务进程这边将自己内部的 DNS 缓存关掉, 通过 DNS-F 进行查询的时候, 会直接到最近的 Nacos 集群拉取最新的服务节点信息, 而且后续节点的变化也会推送到 DNS-F 中, 后续可以直接在缓存中获取最新信息。
国内 Nacos 集群:
集群内通过 raft 协议同步数据,毫秒级别完成同步。
Nacos Sync:
Nacos 推送变化到 Nacos Sync,跨区域、跨国网络差的情况下可能会导致推送结果丢失,或者延迟加大。
Nacos Sync 会主动拉取实例变更,拉取周期和监听的服务数量会影响到变更时效。
DNS - F:
Nacos 会将变更推送到 DNS - F,网络差的情况可能会导致推送结果丢失,或者延迟加大。
DNS - F 会主动拉取实例变更,拉取周期和监听的服务数量会影响到变更时效。
业务进程:
通过应用禁用 DNS 缓存来解决。
核心设计 Nacos
Nacos 有两套推送机制。
一种是通过客户端来选择一个可获节点,比如它第一次拉取的是一个正常节点,这个正常节点就会跟它维护一个订阅关系,后面有变化就会有一个相应的实地变化推送给我。如果当前节点挂掉, 他会通过重连, 在节点列表上,连上一个正常的节点。这时候会有新的 DNS 关系出现。
另一种是通过 SDK 的方式,在服务端寻找可获节点。服务端每个节点之间, 会进行一个可活的探测, 选择其中一个可活节点用户维护这个订阅关系。 当这个节点出现问题, 连接断开后, SDK 重新发送订阅请求,服务端会再次选择另外一个可活的节点来维护这个订阅关系。这就保证整了推送过程不会因为某个节点挂掉而没有推送。
推送的效率方面,主要是用 UDP 的方式,这个效率不像 TCP 消耗那么高。
以上两个方案都比较适合我们目前的场景。
核心组件设计 Nacos Sync
我们选择 Nacos Sync 作为多集群数据同步的组件,主要是从以下4方面进行考虑的。 同步粒度:
Nacos Sync 同步数据的时候是以服务为维度, 比较容易做最终一致性处理, 同时可以提供保活的机制,满足节点维持的场景。 数据库通过 Binlog 同步的方式只能局限于事务粒度, 而文件同步只能通过单个文件的粒度, 在服务同步这个维度并不是很合适。 可用性方面:
Nacos Sync 作为一个中间件,是以集群方式进行的,传统的数据库和文件方式基本是单进程进行的,可用性方面可能不太满足要求。 同步方式方面:
Nacos Sync 通过在服务粒度的全量写入,满足服务注册和 DNS 这两种场景, 不需要额外的事务消耗, 能保证最终一致即可。 环形同步:
我们国内有多个可获的节点,希望它们之间的数据可以进行环形同步,每个节点之间是相互备份的,这时候用 Nacos Sync 的话,是支持的。虽然数据库方面,比较经典的是主主同步,但如果同时对一个主件进行更新的话,每一个点进行协助是会有问题的,而且文件方面是不支持的。
Nacos Sync 和开源版本的不同
我们对 Nacos Sync 开源方案上做了几处修改,以更好的适用于现在的场景:
第一,通过配置方式对任务进行分拆。因为在实际应用场景里面,因为 Nacos Sync 的任务达一两万,单机很容易到达瓶颈,所以我们通过配置的方式将这些分片到多台 Nacos Sync 机器上。
第二,通过事件合并和队列控制的方式控制 Nacos 集群的写入量,以保证后端的稳定性。虽然下发事件一秒钟只有一个,但在很多场景中,例如需要 K8s 或者 Taf 进行数据同步的时候,变化的频率是非常高的,这时候通过事件合并,每个服务单独进行一个写入进程。这样通过队列控制的方式可以控制整个 Nacos 集群的写入量。
第三,添加了能支持从K8s 和 Taf 同步数据的功能。后期我们会将这个特性提交给 Nacos,让更多的开发者使用。
核心组件设计 DNS - F
DNS - F是基于 CoreDNS 上开发的,我们扩展了以下 4 个组件:
Nacos 插件:查询 Nacos 服务信息,监听 Nacos 服务变化,并将服务转化为域名,实现以 DNS 协议为基础的服务发现;
Cache 插件:提供域名缓存服务;
Log 插件:将 DNS 解析日志上报到日志服务;
Proxy 插件:代理解析外部域名;
DNS - F 和开源版本的不同
第一,在日志组件里面将日志上传到自己的日志服务。
第二,对缓存功能做了一个增强。一般的缓存功能可能根据 TTL 时间会过期,我们把这个过期时间给去掉了,直接令到缓存永远不会过期,然后通过异步将这个缓存进行刷新。比如 TTL 可能快到到时间了,我们就会主动做一个查询或者推送查询,这样,服务端或者公共 DNS 出现问题的时候,就不会影响到整体服务。
第三,增强了高可用的保障能力。包括进程监控、内部运营和外部运营的探测。另外,原来的开源版本用的是本机部署的方式,我们做成了集群化的部署,解决了服务推送、服务负载均衡方面的问题。
高可用
接下来由我们团队的李志鹏,分享一下虎牙在高可用方面的实践。
周健同学跟大家介绍了项目的背景跟方案设计,我来和大家介绍一下具体的实践和落地,实践过程中的主要关注点是高可用。
全球化部署方案
这是虎牙的一个全球化的部署方案,我们在全球部署了两个大区,分别是国内和国外。这两个大区是指定服务同步的,走的是专线,这样可以保障同步的稳定性。在一个大区内我们又部署了多个接入点,例如在国内大区,我们部署了深圳和无锡两个接入点,这两个节点的数据是互相同步、互为备份,保证在一个集群挂掉下可以切换到另外一个集群。
多个接入点的情况下,我们通过 HttpDNS 实现客户端的就近接入。客户端定期请求 HttpDNS,HttpDNS 能根据地域寻找就近接入点。如果接入点出现故障,我们就直接在HttpDNS 把这个节点给摘除,这样客户端就能快速地切换到另外一个接入点。
接下来讲一下单个集群下的部署方案。
单个集群部署了多个 Nacos 节点,并通过7层负载均衡的方式暴露给外面使用,并且提供了多个 VIP,满足不同线路和区域的接入要求。同时,Nacos Sync 做了分片处理,将同步压力分散到各个分片上,一个分片下我们又部署了多个 Nacos Sync 的节点,以保障多活和高可用。
线上演练
演练的场景是模拟一个单个集群挂了和两个集群都挂了。
从图中可以看到,把深圳的流量切走之后,无锡的流量就涨上去了,然后再把无锡的流量切走,再关闭服务,这样就可以看到两边的流量已经没了。之后,再去恢复两个集群的流量,看一下整个切换过程中对服务的影响。
首先看一下对写入的影响,在单个集群挂了的情况下,是没有任何影响的。如果是两个集群都挂了,写入就会失败。可以看到,这个图有一个波峰,这个波峰就是我们两个集群都挂了的情况下,写入失败延迟加大。
但是切换的整个过程对 DNS-F 是没有任何影响的,延迟保持平稳。此外,在集群重新上线前,我们需要做数据校验,保证集群之间元数据和实例数据的最终一致。
可用性级别方面,我们可以保障: 单集群挂掉后不会有影响; 双集群挂掉后只会影响域名变更,不影响域名解析;
线上演练数据校验机制
运行过程中,我们也要保证集群间数据的一致性。我们通过全量校验和增量校验两种手段去保证,全量校验方式如下: 大区内部做10分钟的全量校验,保证大区内各个集群数据的一致; 大区之间做2分钟做一次全量校验,保证大区之间被同步的服务的数据一致性。
增量校验方式如下: 从其他数据源同步的数据,通过数据源的时间戳,做增量校验; 基于API的写入日志,定期校验写入的内容是否已经全部同步。
DNF - S 高可用
关于 DNS - F 的高可用,我们主要做了以下5个点: Agent 的健康状态监测,包括进程存活和是否能正常解析; 缓存内部域名,并做持久化处理,保证 Nacos 集群出现问题时不会影响内部域名的解析; 提供备用节点,保证在 DNS-F 挂了,或者是 DNS-F 需要升级的情况下,也不会影响到内部域名解析; resolv.conf 配置检查,发现127.0.0.1不在配置中会自动添加; 限制 Agent 的 CPU 的使用,避免对业务进程造成影响。
具体的实践和落地
实践一:数据库域名改造
之前的数据库是用 IP 方式接入的,在数据库切换的时候,需要通知每个业务方修改配置,重启服务,这样就带来一个问题:整个过程是不可控的,取决于业务方的响应速度,生效时间通常超过十分钟。
提升数据库切换的关键点,第一个就是切换时不需要业务方参与,能在业务方无感知的情况下进行切换;第二个是实例变化能秒级推送到我们的应用,将应用快速切换到一个新的实例上。
大家可以看一下这个图,这是我们现在做的一个改造,图中的 DMX 是虎牙内部的一个数据库管理系统,思路就是把 DMX 和名字服务打通。DMX 会把数据库实例信息以服务的形式注册到名字服务,服务名就是域名。
实际应用过程中,通过这个域名去访问数据库,应用在访问前首先会经过 DNS - F 去做域名的解析,解析的时候是从名字服务查询实例信息,然后把实例的IP返回给应用。这样,应用就能通过 IP 和我们的数据库实例进行连接。
切换的时候,在 DMX 平台修改域名对应的实例信息,并把变更推送到名字服务,名字服务再推送给 DNS-F,应用在下一次解析的时候就能拿到新的实例 IP,达到切换数据库实例的目的。
这套方案落地后,虎牙的数据库切换基本上在10秒钟之内能够完成。
实践二:内部调用使用内部域名
虎牙部分内部系统之间调用是通过7层负载均衡,但是由于没有内部 DNS,需要通过的公共的 LocalDNS 来解析,这就带来一些问题:
问题一:扩缩容的时候要去修改 DNS 记录,整个过程生效时间可能会超过10分钟,故障的节点会影响业务较长的时间。
问题二:公共的 LocalDNS 智能解析不准确,比如无锡的机器可能会解析到深圳的一个接入点,影响接入质量。
问题三:不支持定制化的负载均衡策略,例如同机房、同大区优先的策略,通过公共 LocalDNS 是实现不了的。
如果想要提升内部服务调用质量,一是 DNS 记录变更绕过 LocalDNS,把 DNS 的记录变更直接推到 DNS-F。二是与内部系统打通,从 CMDB 等内部系统获取机器信息,支持多种负载均衡策略。
大家可以看一下上面的图,这个改造和数据库域名的改造思路是一样的,最右上角有一个7层负载管理系统,我们把这个系统和名字服务打通,7层负载管理系统会把域名信息以服务形式注册到名字服务,变更域名记录时直接从7层负载管理系统推送到名字服务,名字服务再推送到 DNS-F,达到快速切换的目的。
如果域名配置了负载均衡策略,名字服务会从 CMDB 获取机器、机房等信息,打标到域名的实例信息。然后,DNS-F 查询名字服务时,会携带 ClientIp,名字服务根据 ClientIp 的CMDB 信息过滤实例列表,返回同机房的实例给 DNS-F,达到同机房优先的目的。
由此带来的效果是:
第一,服务扩缩容能够秒级完成,减少了故障时间。
第二,扩展了 DNS 的负载均衡策略,例如有些业务是需要在不同区域有不同的接入点的,而且不能跨区域调用,之前的 DNS 负载均衡策略是不能满足这个需求的,但在改造之后,我们能根据 CMDB 信息去做同区域调度的负载均衡策略。
第三,业务在接入内部域名之后,延迟会有明显的下降。上图显示的就是某个服务在接入到内部域名之后,延迟出现明显的下降。
另一个落地的效果就是我们对主机上的域名解析的优化。因为我们的 DNS - F 是部署在每台主机上的,然后提供一个缓存的功能。带来的效果就是: 平均解析延迟会从之前的200毫秒下降到现在的1毫秒;
缓存命中率会从之前的90%上升到99.8%,90%是用 CoreDNS 原生的那个 Cache,99.8%是在这个 Cache 的组件下做了优化之后的效果; 解析失败率是从之前的0.1%下降到0%;
这里再总结一下项目落地的技术价值:
第一,提供了基于 DNS 服务发现的能力,消除异构系统之间互相调用的障碍。
第二,填补了没有内部域名解析能力的空白。
第三,解决我们上面说的内部服务调用面临的挑战:延时大、解析不准、不支持多种负载均衡策略、故障牵引慢。
第四,优化外部域名的解析,屏蔽 LocalDNS 的故障。
落地规模是:DNS - F 覆盖率100%,完成 Taf 和 Eureka 注册中心的数据同步。
后续规划
LocalDNS:
解决公共 DNS 节点位置影响域名解析准确性的问题;
解决内部使用公共 DNS 不稳定的问题;
优化内外网解析;
精准调度:
解决全球 DNS 节点生效慢的问题。
本文作者:
周健:GitHub ID @nanamikon,虎牙中间件团队成员,2012年毕业于中山大学,主要负责名字和配置服务,以及虎牙 DNS 和微服务相关的工作。
李志鹏:GitHub ID @lzp0412,虎牙中间件团队成员,主要负责 DNS,以及服务注册与发现、服务治理、Service Mesh 等相关工作。
阅读原文 ​
本文为云栖社区原创内容,未经允许不得转载。
数据库
2019-05-27 12:20:00
文件头部注释
文件头部注释主要是表明该SQL语句的一些基本信息,是该SQL语句实现功能的总体说明,可以增强SQL语句的可读性和可维护性。文件头部注释一般位于 文件顶部。要求至少写出文件名、创建者、创建时间和内容描述。其格式应该尽量约束如下
/**
* @author("张三")
* @description("表描述")
* @version("1.0.0")
* @reference(依赖的表1,依赖的表2)
*/
字段排列要求 SELECT 语句选择的字段按每行一个字段方式编排; SELECT 单字后一个缩进量后直接跟首个选择的字段,即字段离首起二个缩进量; 其它字段首起二个缩进量,逗号之后放置字段名; 两个字段之间的逗号分割符紧跟在第二个字段前; ‘AS’语句应与相应的字段在同一行; 多个字段的‘AS’建议尽量对齐在同一列上;
 
SELECT子句排列要求
SELECT 语句中所用到的FROM、WHERE、GROUP BY、HAVING、ORDER BY、J OIN、UNION等子句,需要遵循如下要求: 换行编写; 与相应的SELECT 语句左对齐编排; 子句后续的代码离子句首字母二个缩进量起编写; WHERE 子句下的逻辑判断符 AND、OR等与WHERE左对齐编排; 超过两个缩进量长度的子句加一空格后编写后续代码
 
运算符前后间隔要求
算术运算符、逻辑运算符的前后要保留一个空格。
CASE语句的编写
SELECT 语句中对字段值进行判断取值的操作将用到CASE语句,正确的编排CASE语句的写法对加强代码行的可阅读性也是很关键的一部分。对CASE语句编排作如下约定: WHERE 子句下的逻辑判断符 AND、OR等与WHERE左对齐编排; 每个WHEN子语句一行编写,当然如果语句较长可换行编排; CASE语句必须包含ELSE子句,ELSE子句与WHEN子句对齐。
子查询嵌套编写规范
子查询嵌套在数据仓库系统ETL开发中是经常要用到,因此代码的分层编排就非常重要。
表别名定义约定
所有的表建议都加上别名。因为一旦在SELECT 语句中给操作表定义了别名,那么在整个语句中对此表的引用都必须惯以别名替代,因此考虑到编写代码的方便性,约定别名尽量简单、简洁,同时避免使用关键字。 表别名采用简单字符命名; 多层次的嵌套子查询别名之前要体现层次关系,SQL语句别名的命名,分层命名,从第一层次至第四层次,分别用P 、S、 U 、D表示,取意为Part , Segment , Unit , Detail。也可用a、b、c、d来表示第一层次到第四层次;对于同一层次的多个子句,在字母后加1、2、3、4……区分; 有需要的情况下对表别名加注释。
SQL注释 每条SQL语句均应添加注释说明; 每条SQL语句的注释单独成行、放在语句前面; 字段注释紧跟在字段后面; 应对不易理解的分支条件表达式加注释; 对重要的计算应说明其功能; 过长的函数实现,应将其语句按实现的功能分段加以概括性说明; 常量及变量注释时,应注释被保存值的含义(必须),合法取值的范围(可选)。
数据库
2019-05-13 15:32:00
Redis 提供了两种方式,实现数据的持久化到硬盘。 1、【全量】RDB 持久化,是指在指定的时间间隔内将内存中的 数据集快照 写入磁盘。实际操作过程是,fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。 - 默认开启rdb持久化 2、【增量】AOF持久化,以日志的形式记录服务器所处理的每一个 写、删除操作 ,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。 - 默认关闭 每次写,删除操作就同步或者每秒同步
(1)RDB 优缺点
优点: 灵活设置备份频率和周期。你可能打算每个小时归档一次最近 24 小时的数据,同时还要每天归档一次最近 30 天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。 非常适合冷备份,对于灾难恢复而言,RDB 是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。推荐,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说 Amazon 的 S3 云服务上去,在国内可以是阿里云的 OSS 分布式存储上。 性能最大化。对于 Redis 的服务进程而言,在开始持久化时,它唯一需要做的只是 fork 出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行 IO 操作了。也就是说,RDB 对 Redis 对外提供的读写服务,影响非常小,可以让 Redis 保持高性能。 恢复更快。相比于 AOF 机制,RDB 的恢复速度更更快,更适合恢复数据,特别是在数据集非常大的情况。
缺点: 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么 RDB 将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。 所以,RDB 实际场景下,需要和 AOF 一起使用。 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是 1 秒钟。 所以,RDB 建议在业务低估,例如在半夜执行。
(2)AOF 优缺点
优点: 该机制可以带来更高的 数据安全性 ,即数据持久性。Redis 中提供了 3 种同步策略,即每秒同步、每修改(执行一个命令)同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。 由于该机制对日志文件的写入操作采用的是  append  模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。因为以 append-only 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。另外,如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在 Redis 下一次启动之前,我们可以通过 redis-check-aof 工具来帮助我们解决数据一致性的问题。 如果日志过大,Redis可以自动启用  rewrite  机制。即使出现后台重写操作,也不会影响客户端的读写。因为在 rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。 AOF 包含一个格式清晰、易于理解的日志文件用于记录所有的 修改操作 。事实上,我们也可以通过该文件完成数据的重建。
缺点: 对于相同数量的数据集而言,AOF 文件通常要大于 RDB 文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。 根据同步策略的不同,AOF 在运行效率上往往会慢于 RDB 。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和 RDB 一样高效。 以前 AOF 发生过 bug ,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志/merge/回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug 。不过 AOF 就是为了避免 rewrite 过程导致的 bug ,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
(3)如何选择 不要仅仅使用 RDB,因为那样会导致你丢失很多数据 也不要仅仅使用 AOF,因为那样有两个问题,第一,你通过 AOF 做冷备,没有 RDB 做冷备,来的恢复速度更快; 第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug 。 Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。 如果同时使用 RDB 和 AOF 两种持久化机制,那么在 Redis 重启的时候,会使用  AOF  来重新构建数据,因为 AOF 中的 数据更加完整 。 一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化,但并不推荐这种方式:因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用 RDB 还可以避免之前提到的 AOF 程序的 bug。
数据库
2019-04-30 17:10:00
作者介绍: 于振华,北京银行软件开发部资深架构师,长期从事银行核心系统研发、规划,参与过多个核心信息系统建设工作,包括一、二代支付系统、第四代银行核心系统建设、分布式核心系统建设等企业级项目工作。当前主要研发方向集中在构建先进、高效、面向 OLTP 的银行交易系统,提升银行信息系统服务能力。
本文整理自于振华老师在  TiDB DevCon 2019 上的演讲实录,演讲主题为《TiDB 在银行核心金融领域的研究与实践》。
今天参加 TiDB DevCon 2019 能够和这么多各行各业的朋友一起来交流 TiDB 的实践情况,这个机会非常难得,因为平时都是我们技术团队和 TiDB 团队单向的交流,横向的这种客户之间交流的机会很少,像刚才几位老师讲的,我觉得都很有意思,也希望通过咱们这次大会,大家能擦出不一样的火花。
北京银行和 PingCAP 团队进行了深度的合作,目前有几套重要的实时交易类系统已经对接,包括比较重要网联系统、银联无卡支付、金融互联服务平台等。现在怎么来评价一款产品到底稳不稳,很大程度上要看这款产品在金融,尤其是核心金融的场景有没有应用,能不能支持金融场景的要求。我们是在 2018 年 3 月份、5 月份、6 月份进行了投产。经过半年多的时间,我们看到 TiDB 也能够支持金融场景了。从侧面来讲,分布式数据库技术,确实已经到达了一定的成熟度。
一、背景介绍
我相信这几年,尤其是这三四年,大家应该都有感触。无论是工作方式,还是生活方式,都发生了很大的变化,各种信息、科技产品铺面而来,有人说是这种变化叫 工业科技革命 4.0 。不知道这种提法准确不准确,但这种变化确实对我们银行的系统产生了比较大的挑战。
图 1

在图 1 中 ,我列出了几项,比如 高并发的要求 ,要求你具备很快的扩展能力。再比如产品发布,要求你 具备快速的发布能力 ,在座的应该有很多做产品、做实施的团队,大家应该很有感触,比如可能前一天还无人问津的产品,第二天可能就会卖的很火爆,来的每个项目都是紧急项目,都要求你在最快的时间发布出去。当然还包括一些老生常谈的问题,像 传统架构成本难以控制 ,还有 自主可控亟待攻关 ,其实在传统闭源的生态里面,我们很难达到自主可控的要求。
二、系统分析
图 2

在这种背景下,我们从全局的角度出发,对银行以往的技术形态做了系统性的分析,图 2 中列举了一些典型的架构形态,有一些在现在的银行架构里边还是存在的,比如单体的应用,再比如传统的数据库,现在用的最多的 DB2 和 Oracle,还有传统的单机或者集群部署模式,以及瀑布开发模型,当然还有面向传统架构的运维模式。
今天我们来谈分布式数据库,它是一个新技术,但不能说把以往技术架构就否定掉。以往的技术形态好不好?坦白讲,我认为很好,不好的话不可能支撑了这么多年的金融业务发展,但站在今天这样的时间点来说问题也是存在的。像刚才讲到的,高并发的要求、扩展能力、成本、以及产品交付能力都存在一些不尽如人意的地方。
在这种情况下,我们启动了北京银行新一轮的架构转型的工作,分布式数据库也纳入到我们的工作范围里。我们和 PingCAP 很早就接触了,在一年多的工作过程中,要谈的技术细节、技术方案、工作流程等等这些内容会很多,如果真的来总结一下这项工作是怎么做的话,我总结出以下三条。大家一看可能会觉得很虚,但是你如果真的来实践这件事,也许会有同样的感触。
第一个就是「务实」 。架构转型不是一个为了技术而技术,为了新产品而新产品的工作,而是确实要对你的业务发展、开发、运维的效率有所提升。
第二个,我觉得可能是最重要的,就是要做到「速赢」 。无论是你在什么样的企业来做技术升级,技术转型,或多或少的都会遇到一些阻力,尤其是在传统企业。那做到速赢,迅速的释放价值,让你周围的人、让你的团队、让你的组织,迅速看到它的价值,会让你未来的工作开展更加平滑。
第三个是「全栈」 。因为是整体的架构转型工作,我们希望建设一套平台,它能够释放整体的价值,而不是在乎一城一池的得失。今天本来我想介绍北京银行的应用架构和分布式数据库架构,因为时间关系今天只说一下分布式数据库建设的情况。
三、进展情况
图 3

在介绍具体内容之前,先跟大家同步一下,我们现在的工作进展。2018 年 3 月,我们投产了行业内首个面向核心金融业务的分布式数据库,采用的是 两地三中心五副本 的架构模式。以分布式数据库为基础,5 月份我们投产了网联支付清算平台,这也是很重要的一个带资金业务的实时交易系统,6 月份投产了银联无卡支付平台。这张图(图 3)可能稍微有点老,现在我们投产的还包括金融互联服务平台,IFRS9 减值系统。我们未来要做的事其实和刚才 刘奇 讲的比较一致,包括 HTAP,包括容器云的这些方案等等,这也是我们目前最迫切的需求。
3.1 专项评测
现在回想起来,北京银行开展分布式数据库建设的工作,其实是在行业里面算很早的,也是因为我们开展这件工作的时间比较早,所以在整个过程中遇到了很多的困难困惑。行里的技术力量集中在 DB2、Oracle 上可能比较多,对于分布式数据库的掌握来讲,需要有一个周期。 我们做的第一步,为了保证产品可用,建设了面向金融业务的评测体系 。
图 4

图 4 左上角是面向这个功能的测试,比如数据库有没有高可用性,能不能做线性扩展,有没有在线升级能力,这些都是我们的测试点。图 4 左下角这块,是面向性能的测试, 我们并没有采用市面上已经有的工具,比如 TPCC、Sysbench 等等。因为我们实际分析下来觉得市面已经有的这些工具和我们的金融场景有一些距离,用它们来测试可能不会有很好的参考意义,所以我们自研了这套面向分布式数据库的金融性能评测体系,能够让我们明确出分布式数据库可以应用在金融场景,并且对于功能和性能,让大家能有一个可度量的工具 。
在这个过程中,要感谢支付清算协会、信通院等上级单位和组织给予我们的帮助,另外,我们也和硬件厂商英特尔进行了比较深的合作,比如今年(2018 年)新的硬件平台,我们也做了专项的分布式数据库测试,为未来我们硬件的架构选型提供了有效的参考。
3.2 部署模式
图 5

对于分布式数据库的技术层面来讲,刚才几位讲师介绍的比较多了,我就来讲一些北京银行比较不一样的、走在前面的一些地方。 大家看到图 5 这套架构是北京银行的数据存储层的架构。 北京银行的架构采用两地三中心五副本的模式部署 。
跨城长距离的分布式数据库建设具有很大的挑战。比如北京和西安大概一千多公里,两地距离比较远,延时比较高,我们实测的延时大概是十七毫秒左右。这十七毫秒,如果放在一条 SQL 来讲,一来一回三十几毫秒,这样的延时我们肯定是接受不了。所以在这种情况下, 我们用了一个五副本的模式:北京两个 IDC,各放置两副本,西安一个 IDC 放置一个副本,采用 2:2:1 的模式。这样做的好处就是当前端应用请求过来之后,不需要用到北京到西安的这个网络,北京的四个副本中成功三个,就可以给前端实时返回,而且北京的部分实例允许失效。这样做 SQL 平均延时,大概在 1.2 毫秒左右,.95 延时大概 5 毫秒左右,这是比较不错的一个成绩(网联、银联的业务其实要比互联网业务复杂很多) 。
这里给大家分享一个我们实际在生产过程中遇到的一个小故事。在某个周六的中午我接到我们运维值班人员的电话,他说 TiKV 存储服务器坏了一台,当日我第一时间问的是:坏了一台有没有影响服务。他说没有影响服务,服务还是正常的。我说那就赶紧找硬件厂商给修一下机器。当时还觉得挺高兴的,不经意间在生产系统验证了一把。到了第二天周日的中午,他又给我打了一个电话,说又坏了一台服务器。当时有一些担心,是不是我们这批采购的硬件服务器有什么问题,想到这点就立马做排查,当然第一时间问的还是有没有影响服务,他说没有影响服务。 这样连着两天坏了两台存储服务器都没有影响服务,也证明了多副本方案的有效性 。
3.3 两地三中心
图 6

图 6 展示的是整个包括应用、F5 到 TiDB、PD、TiKV 等整个部署的模式。目前我们接着有网联、银联这两个比较大的系统,这两个系统业务量相对来讲比较大,每天有一两百万笔的业务。在西安,我们还部署了一个从集群,那这个从集群是做什么呢?这个从集群就是为了对接一些 OLAP 或者说比较大的报表的情况,从而避免它对主集群的负载产生过大的影响。
四、应用实践
4.1 出现过的问题
图 7

有人说“当你有了锤子,好像什么问题都看上去像钉子”。我们期待从传统数据库过渡到分布式数据库,什么问题都可以解决。但事实上,肯定是没有一个万能的技术方案。图 7 右下角,我列了一些从我们项目开展之初到现在,产生一些问题或者说一些小插曲。
比如我们刚才介绍了行里的 DB2、Oracle 应用的比较多。DB2、Oracle 以前用的是 READ COMMITTED 的隔离级别,那现在到了 TiDB 的 Repeatable Read 的这种形式可能还需要适应。我们建设初期也出现过这种问题:这边 Insert 的数据,那边却查不到,就因为 TiDB 是这种快照的隔离级别。
还有执行计划的索引没有选中的问题,这个在我们实际的生产过程中也遇到过,明明有索引,却没有精确选中那一个索引。造成 SQL 运行的特别慢,内存吃的也比较多。这个问题,我觉得是可以解决好的,临时解决方案就是手动强制加 Hint,未来我相信 TiDB 在版本升级上也会考虑这一点,让执行计划更加准确。
还有热点数据的问题,热点数据指望数据库来解决,现阶段来看是不可能了。无论是传统数据库,还是分布式数据库,要引入另外的应用缓存的组件才可以解决,在传统方案里边,我们做的技术方案也有很多,像比较传统的散列方式,把热点数据散列出去来解决,现在有了缓存,可以引入缓存解决这件事。
我们应用架构采用微服务的形态,对比单体应用形态,微服务对于数据库的要求会更高。因为传统的单体应用,事务的 SQL 数量比较多,划分成微服务的话,无论是应用逻辑,还是数据库的处理逻辑,都会比较细粒度,事务提交次数成倍增长,对于 MVCC 的乐观提交模型有一定的压力,在我们实测的过程中,越细粒度的可能表现的性能越不好。
以上就是我们实践过程中出现的一些小插曲。
4.2 与互联网行业在应用实践上的区别
图 8

今天很多来自互联网企业的朋友也分享了自己的经验, 那在金融行业做分布式数据库落地和互联网行业有什么不同呢 ?
首先来讲,银行的发展时期和很多互联网新兴科技公司是不同的,银行有很成熟的硬件体系、部署模式、软件的设计模式、开发模式、运维模式,站在这种平台上来做新型技术落地会更加的困难 。为什么会得到这个结论?因为现在也有很多的软件厂商,很多做产品的人,大家都希望做新建系统的事情。但对于庞大的历史系统做迁移的话,肯定不会是一刀切的方案,因为代价太大了。所以需要并行运行,对于这种新旧架构并行,很多时候就没有了方案,做不了。其实现在我们也在做这项工作,做一个新旧系统优雅的并行方案,包括业务逻辑的并行,还有业务数据的并行,如果大家有兴趣的话,也可以和我们私下交流这部分内容,我觉得这是很重要的一个事情。
第二点就是组织架构不同 。就拿微服务来说,单体的应用发展这么多年,每一个应用它的技术负责人是谁,对应的业务负责人是谁,是哪个部门,都很明确。如果做微服务化,进行拆分,很多情况下很难确定权责,如果要企业组织架构来适应系统架构也不太现实。当然历史资产、业务场景和互联网企业也是不一样的,银行信息化历史资产更多、业务比互联网更加复杂。
4.3 新型架构
图 9

图 9 是我们系统建设架构图的一部分,最底下是分布式 NewSQL 数据库的基础平台,上边是应用系统,目前是传统架构和新型微服务架构并存。
五、未来展望
图 10

最后再介绍一下未来我们的建设方向。
第一,经过阶段性的实践,新的架构仍需要进行多方位的验证,来确保高可用性、扩展性、成本等方面的优势。下一个阶段我们希望扩大应用范围,把业务发展快、规模大、对并发要求高的系统,逐步的迁移过去。
第二,我们要建立一套应用规范,或者说面向 TiDB 的金融级开发的规范指引。目前我们正在做这个事儿,包括最佳研发应用实践以及新老架构并行方案。建设传统数据库和 TiDB 之间的异构数据库传输的中间件是我们目前很重要的一项工作,这部分做完之后,相信对我们扩大应用会比较有好处。
第三,我们还要做 HTAP,这点和刚才 刘奇 谈到的可能会比较契合。之前我看过 TiFlash 的设计理念和设计方式,我觉得是比较新颖的一种方式,比现在有些还需要 T+1 的数据分析方案会好很多,技术架构更加一体化、业务过程更加流畅。另外,我们一直在做性能提升、网络依赖消减等工作。
最后,我们也希望能够把北京银行的经验和大家多多分享,让大家不再遇到我们建设过程中遇到的问题和麻烦,更加顺畅的进行架构转型工作。
以上就是我今天分享的内容,谢谢大家。
数据库
2019-04-18 11:29:00
一、前言
MySQL 的锁按照范围可以分为全局锁、表锁、行锁,其中行锁是由数据库引擎实现的,并不是所有的引擎都提供行锁,MyISAM 就不支持行锁,所以文章介绍行锁会以InnoDB引擎为例来介绍行锁。
二、全局锁
MySQL 提供全局锁来对整个数据库实例加锁。
语法: FLUSH TABLES WITH READ LOCK
这条语句一般都是用来备份的,当执行这条语句后,数据库所有打开的表都会被关闭,并且使用全局读锁锁定数据库的所有表,同时,其他线程的更新语句(增删改),数据定义语句(建表,修改表结构)和更新类的事务提交都会被阻塞。
在mysql 8.0 以后,对于备份,mysql可以直接使用备份锁。
语句: LOCK INSTANCE FOR BACKUP UNLOCK INSTANCE
这个锁的作用范围更广,这个锁会阻止文件的创建,重命名,删除,包括 REPAIR TABLE TRUNCATE TABLE, OPTIMIZE TABLE 操作以及账户的管理都会被阻塞。当然这些操作对于内存临时表来说是可以执行的,为什么内存表不受这些限制呢?因为内存表不需要备份,所以也就没必要满足这些条件。
三、表锁
Mysql的表级别锁分为两类,一类是元数据锁(Metadata Lock,MDL),一种是表锁。
元数据锁(MDL) 不需要显式使用,在访问一个表的时候会被自动加上。这个特性需要MySQL5.5版本以上才会支持,当对一个表做增删改查的时候,该表会被加MDL读锁;当对表做结构变更的时候,加MDL写锁。MDL锁有一些规则: 读锁之间不互斥,所以可以多线程多同一张表进行增删改查。 读写锁、写锁之间是互斥的,为了保证表结构变更的安全性,所以如果要多线程对同一个表加字段等表结构操作,就会变成串行化,需要进行锁等待。 MDL的写锁优先级比MDL读锁的优先级,但是可以设置max_write_lock_count系统变量来改变这种情况,当写锁请求超过这个变量设置的数后,MDL读锁的优先级会比MDL写锁的优先级高。(默认情况下,这个数字会很大,所以不用担心写锁的优先级下降) MDL的锁释放必须要等到事务结束才会释放
所以我们在操作数据库表结构时候必须要注意不要使用长事务,这里具体是什么意思呢?我举个例子说明下:
![](
上图表示演示了4个session执行语句,首先SessionA开启了事务没有提交,接着sessionB执行查询,因为是获取MDL读锁,所以互相不影响,可以正常执行,SessionC新增一个字段,由于MDL写和读是互斥的,所以SessionC会被阻塞,之后SessionD开始执行一个查询语句,由于SessionC的阻塞,所以SessionD也阻塞了。所以,我们模拟的SessionA的事务是长事务,然后后面执行了修改表结构,会导致后续对该表所有的读写操作都不可行了。所以在实际场景中,如果业务请求比较频繁的时候,对表结构进行修改的时候就有可能导致该库的线程被阻塞满。
表锁 的语法如下: LOCK TABLES tbl_name [[AS] alias] lock_type [, tbl_name [[AS] alias] lock_type] ... lock_type: { READ [LOCAL] | [LOW_PRIORITY] WRITE } UNLOCK TABLES
表锁分为读锁和写锁 ,读锁不互斥,但是获取读锁不能写入数据,其他没有获取到读锁的session也是可以读取表的,所以读锁的目的就是限制表被写。如果表被读锁锁住后,再执行插入语句会报错,报错如下: 1099 - Table 'XXXX' was locked with a READ lock and can't be updated
写锁 被获取后可以对表进行读写,写锁是互斥的,一旦某个session获取到表的写锁,另外的session无法访问这个表,直到写锁被释放。
表的解锁可以使用 unlock tables 解锁,也可以客户端口自动解锁。 lock tables 锁表会独占式的锁住表,除了限制其他线程对该表的读写,也会限制本线程接下来的操作对象。
四、行锁(InnoDB)
MySQL的行锁是在引擎层面实现的,所以这里讨论的也是InnoDB引擎下的行锁,下面会详细介绍InnoDB下常见的几种行锁
4.1 共享锁
共享锁能允许事务获取到锁后进行读操作,共享锁是不互斥的,一个事务获取到共享锁后,另外一个事务也可以获取共享锁,获取共享锁后不能进行写操作。
4.2 排它锁
排他锁允许事务获取到锁后进行更新一行或者删除某一行操作,排他锁顾名思义是互斥的,一个事务获取到排他锁后,其他事务不能获取到排他锁,直到这个锁被释放。
4.3 意向锁
InnoDB支持多种粒度的锁,允许行锁和表锁共存,这里说的 意向锁其实是一种表级别的锁 ,但是我把它放在行锁里面是因为它不会单独存在,它的出现肯定会伴随着行锁(共享锁或者排他锁),它主要的目的就是表示将要锁定表中的行或者正在锁定表中的行。
意向锁根据和行锁的组合可以分为: 意向排他锁:表明将要在表中的某些行获取排他锁 意向共享锁:表明将要在表中的某些行获取共享锁
意向锁的获取必须在行锁获取之前,也就是说获取共享锁之前必须先要获取共享意向锁,对于排他锁也是一样的道理。
那么这个意向锁到底有什么作用呢?
解释这个之前,我们先看看意向锁和行锁之前的兼容关系:
--- 排他锁(X) 意向排他锁(IX) 共享锁(S) 意向共享锁(IS)
排他锁(X) 意向排他锁(IX) 共享锁(S) 意向共享锁(IS)
冲突 冲突 冲突 冲突
冲突 兼容 冲突 兼容
冲突 冲突 兼容 兼容
冲突 兼容 兼容 兼容
我们假设有2个事务A和事务B,事务获取到了共享锁,锁住了表中的某一行,这一行只能读,不能写,现在事务B要申请整个表的写锁。如果事务B申请成功,那么肯定是可以对表中所有的行进行写操作的,那么肯定与A获取的行锁冲突。数据库为了避免这种冲突,就会进行冲突检测,那么如何去检测呢?有两种方式: 判断表是否已经被其他事务用表级锁锁住。 判断表中的每一行是否被行锁锁住。
判断表中的每一行需要遍历所有记录,效率太差,所以数据库就用第一种方式去做冲突检测,也就是用到了意向锁。
总结
本文主要从MySQL的加锁范围来分析了MySQL的锁,MySQL根据加锁范围可以分为全局锁、表锁、行锁。全局锁和表锁是MySQL自己实现,行锁都是由引擎层面去实现。InnoDB下的行锁主要分为共享锁和排他锁。共享锁请求后,行只能读,共享锁之间不互斥。排他锁获取后能更新和删除行,排他锁与其他锁都互斥。最后我在行锁的基础上提到了意向锁,意向锁主要表示正在锁住行或者即将锁住行,为了在锁冲突检测中提高效率。当然InnoDB下还有其他锁,比如间隙锁,记录锁,Next-Key锁等,这些都不在本文的探讨范围之内,如有兴趣的同学可以自行研究。
参考 MySQL用户手册之InnoDB锁 MySQL用户手册之事务和锁 《MySQL实战45讲》
数据库
2019-04-15 09:19:00
如上所示的一个sql查询,在hourflag字段已经加了索引的情况下 ,仍然使用的是ALL扫描全表
查资料得出原因是因为mysql优化器认为走全表扫描更快
那么此时就需要告诉优化器,强制使用XXX索引,可以增快查询速度,
此时把sql给改成 explain SELECT city_name, hourflag, eletricflag FROM t_eletric_segment_info force index(index_hourflag) WHERE hourflag >= '2019032600' AND hourflag <= '2019040123' 关键就是where前面的这个 force index(index_hourflag)
此时可以看到,索引生效,查询速度快了5倍
数据库
2019-04-02 16:15:00
Mybatis 配置初始化过程
测试代码
SqlMapConfig.xml
StudentMapper.xml
Student package com.zhiwei.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Data @NoArgsConstructor @AllArgsConstructor @ToString public class Student { private int sid; private String stuName; private String stuPasswd; private int stuAge; private String stuGender; private float stuScore; }
StudentMapper package com.zhiwei.advanced.mapper; import com.zhiwei.advanced.pojo.StudentCustomer; import com.zhiwei.advanced.pojo.StudentQueryVo; import com.zhiwei.entity.Student; import java.util.List; public interface StudentMapper { public Student findStudentById(Integer sid); }
启动类 sqlSession.getMapper(StudentMapper.class).findStudentById(id);
工作流程
解析XML映射为org.apache.ibatis.session.Configuration对象,并以此为基础创建SessionFactory, SessionFactory创建的Session与Executor绑定,交由Executor执行
配置加载过程
SessionFactory构建代码 Inputstream is = Resources.getResourceAsStream("SqlMapConfig.xml"); sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SessionFactory构建
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties) XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse());
XMLConfigBuilder 工具类:主要解析SqlMapConfig.xml private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; }
new Configuration(): 默认填充配置:类似缓存别名映射类 public Configuration() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); languageRegistry.register(RawLanguageDriver.class); }
XML解析:parser.parse()
parsed解析标志,底层调用 parseConfiguration(parser.evalNode("/configuration")); 按照Xpath语法解析SqlMapConfig.xml根节点 private void parseConfiguration(XNode root) { try { // 解析SqlMapConfig.xml的properties标签, configuration.setVariables(defaults)解析properties文件属性到configuration缓存 propertiesElement(root.evalNode("properties")); // setting配置: configuration.setVfsImpl(vfsImpl) Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); //别名配置:configuration.typeAliasRegistry //dtd约束:mybatis-3-config.dtd //package: key:类简单名驼峰命名法,class:类全限定名 //typeAlias: key: alias自定义别名 type:类全限定名 //注意:最好不用直接引用package形式,否则多个包存在相同名的类会出现冲突 typeAliasesElement(root.evalNode("typeAliases")); //插件配置:相当于拦截器可自定义操作,例如分页、CRUD操作拦截,类似AOP:configuration.interceptorChain pluginElement(root.evalNode("plugins")); //设置对象工厂:哦、configuration.objectFactory objectFactoryElement(root.evalNode("objectFactory")); //社会组obectMapper工厂:configuration.objectWrapperFactory objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //设置反射器工厂:configuration.reflectorFactory,反射机制操作 reflectorFactoryElement(root.evalNode("reflectorFactory")); //mybatis全局配置:若无自定义则使用默认配置 settingsElement(settings); //设置环境:configuration.environment,本质缓存当前工作数据库信息 //注意:Environment可设置多套数据库环境,通过default属性指定具体数据库 environmentsElement(root.evalNode("environments")); //明确指定mybatis工作的数据库ID:数据库产品名称 databaseIdProviderElement(root.evalNode("databaseIdProvider")); //类型处理器:解析具体的Mapper数据类型: configuration.typeHandlerRegistry //作用:数据库操作Mapper的解析和转换工作 typeHandlerElement(root.evalNode("typeHandlers")); //mapper处理:configuration.mapperRegistry mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
Mapper标签解析 private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //package形式批量配置:常用 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); //mapper单个独立配置 } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
mapperParser.parse(); 解析 //解析流程 public void parse() { //判断StudentMapper.xml是否已加载 if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } //XML解析可能出现问题,重复处理,例如子ResultMap先于父ResultMap加载 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
configurationElement(parser.evalNode("/mapper")); 解析Mapper标签 private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); //解析parameterMap标签:configuration.parameterMaps parameterMapElement(context.evalNodes("/mapper/parameterMap")); //解析resultMap标签:configuration.resultMaps resultMapElements(context.evalNodes("/mapper/resultMap")); //解析sql标签:configuration.sqlFragments sqlElement(context.evalNodes("/mapper/sql")); //解析:CRUD标签映射为MapperStatement //分析:构建sql原材料ParameterMaps、ResuldMap、sql片段、namespace准备后开始构建 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }
构建MapperStatement private void buildStatementFromContext(List list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
statementParser.parseStatementNode(); 解析成MapperStatement public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String parameterType = context.getStringAttribute("parameterType"); Class parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); Class resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); //填充include refid引用sql片段 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); //解析mybatis selectkey主键生成策略标签 processSelectKeyNodes(id, parameterTypeClass, langDriver); // 解析sql SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); KeyGenerator keyGenerator; //MapperStatementId生成策略:名称空间 + id + !操作类型key String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
configuration添加MapperStatement public MappedStatement addMappedStatement( String id,SqlSource sqlSource,StatementType statementType,SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,String parameterMap,Class parameterType,String resultMap,Class resultType,ResultSetType resultSetType,boolean flushCache,boolean useCache,boolean resultOrdered,KeyGenerator keyGenerator,String keyProperty,String keyColumn,String databaseId,LanguageDriver lang, String resultSets) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); configuration.addMappedStatement(statement); return statement; }
绑定命名空间: bindMapperForNamespace();
org.apache.ibatis.builder.xml.XMLMapperBuilder#bindMapperForNamespace
本质:命名空间和Mapper接口绑定,方便后续通过Mapper操作动态生成代理类 private void bindMapperForNamespace() { String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class boundType = null; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace); //mapperRegistry:保存代理工厂类 configuration.addMapper(boundType); } } } }
至此MapperStatement初始化工作完成
数据库
2019-04-02 11:44:00
《漫步华尔街》的读书笔记作文2200字:
艾略特波浪理论:艾略特波浪理论的立论前提是:股市存在着可以预测的如同波浪一样波动的投资者心理,并且波动的投资者心理会推动市场自然而然地潮起潮落、涨涨跌跌。艾略特认为,通过观察投资者的心理波动可以识别市场的重要变动。
对投资者的启示:股价过去的走势不可能以任何有意义的方式用于预测未来的股价。…各种技术理论只是富了炮制和推销技术服务的人,以及雇佣技术分析师的证券经纪公司。这些证券经纪公司希望分析师的分析有助于鼓动投资者更频繁的进行买入卖出的交易,从而为公司带来更多的佣金收入。使用技术分析确定股票买卖时机的做法尤为危险。因为股票市场有着长期向上的趋势,所以持有现金可能风险非常大。为避开市场阶段性下跌而经常保持大量现金头寸的投资者,很可能在市场迅猛涨升的某些阶段被排斥在市场之外。
小结:本章作者分析了许多技术分析存在的问题,对投资者有很深的教育意义。技术分析忽视了政策与外部环境对大盘和公司的影响,也忽视了公司自身发展对股价的影响。技术分析可能预测对几次,但是人们往往忽视了他们没有预测对的多次。国内个别的所谓“专家”、“股神”也是一样。在股市不长的历史中,偶尔在一个小的时间段出现几个认识正确的人是正常的,但是他们的理论却不一定永远有效。“一个坏的钟表一天中还有两次撞对的时候”,因此,对于他们的理论,还需要自己认真学习、辨识,切忌人云亦云,毫无主见。
第七章.基本面分析究竟有多出色
证券分析师果真是天眼通吗?
在20世纪80年代之后,即便强大的IBM也未能延续可靠的增长模式,宝丽来、柯达、北电网络、施乐以及众多的公司,它们都曾经实现过持续的高速增长,道德经读后感(http://www.simayi.net/duhougan/8842.html)直到有一天“屋顶坍塌”,神话终结。…即使在20世纪90年代经济繁荣发展的时期,也只有1/8的大公司每年成功实现了持续增长;而在进入新千年的头几年里甚至没有一家大公司实现增长。分析师不可能预测连续的长期增长,因为这根本就不存在。
直言不讳地说,这些证券分析师认真估测的数据(基于行业研究、工厂参观等)并不比通过简单的外推以往趋势得到的估计情况准确多少,……任何行业都不是容易预测的。
在某一年比一般同行表现出色的分析师,在下一年丝毫不比其他人更有可能做出上乘的预测。
水晶球为何浑浊不清
我认为有五个因素可以帮助解释证券分析师为何在预测未来时困难非常之大。这五个因素是:
1)随机事件的影响;
2)公司利用“寻机性”会计处理方法制作靠不住的盈利报告;
3)许多分析师基本不称职;
4)最佳分析师流向营销部门或转而管理投资组合、对冲基金;
5)研究部门与投资银行部门之间的利益冲突。
在20世纪90年代和21世纪初,会计舞弊行为看来更加司空见惯了。破产倒闭的网络公司、高科技领先企业,甚至“旧经济”的蓝筹公司全都卖力的通过媒体大肆宣传自己的盈利,误导投资者。
众多分析师纷纷效仿“路易(一个数据计算错误而误导他人购买证券的分析师)”。一般情况下,由于太懒惰不想亲自做预测,他们宁愿抄袭其他分析师的预测结果,或者不加咀嚼便囫囵吞下公司管理层发布的“指导性意见”。
证券分析师习惯上很少建议投资者卖出股票,这一点表明了他们与投资银行业务之间存在密切关系。…分析师不愿意得罪作建议时涉及的公司。…在互联网泡沫期间,大多数个人投资者却令人悲哀的接受了分析师的字面(“买入”和“持有”)意思。
中间道路:我的个人观点
我们先简要复述一下有关股市如何运行的两种截然不同的观点:大多数投资经理的观点是:专业人士在资金管理方面毋庸置疑会胜过所有业余和漫不经心的投资者;另一方面,学术界的多数人士认为:由专业人士管理的投资组合的绩效并不能超过随机选取的具有相同风险特征的股票组合。
我走的是中间道路。
此外,我也不造成全盘接受市场有效理论的主张,部分原因是该理论建立在几个脆弱的假设之上。第一个经不住推敲的假设是市场存在充分定价。…这种推理思路是接近于“博傻”理论的思考方式。股票市场有时并非根据什么人的价值估测值进行交易——投资者经常在投机狂潮中被冲昏头脑。专业投资者在很大程度上对20世纪的数次股市狂潮包括世纪之交的互联网泡沫负有责任。有效市场理念的第二个脆弱假设是信息瞬间完成传播。第三个是将一只股票已知的信息转换成真实价值的估计值会面临极大的困难。
小结:
一、虽然基本面分析不一定在短期内有效,但是在长时间内是有效的。作者把短期与长期股市的走势与预测弄混了。如茅台、苏宁等几年涨了十几倍,而许多股几年未涨过。
二、虽然许多证券分析师对基本面分析存在偏差,甚至错误,但也说明了整体收益仍大于大多数散户。这也是作者鼓励买共同基金的原因。
三、个人仍然要选择基本面好、管理严格、业绩稳定增长的公司,虽然这些公司以后发展速度无法预测准确,但总比差公司稳定,且共同基金也是会买入这些公司的股票。四、后面分析的三点是对市场有效性致命的三点驳斥。
作者在本章列举了美国七十年代、八十年代、21世纪初许多基金排名的变化和业绩增长速度的变化。从中可以看出,股市如人生,某些与指数变化相同的基金就如同我们普通人的财富增长与社会财富增长同步一样。而在某一段时间表现特别好的基金在另一段时间却表现一般,就像某一段时间的明星、名人、学习尖子在以后的岁月中不一定出类拔萃一样。唯有巴菲特管理的公司能够持续稳定地增长50多年,也与巴菲特不断的学习进步和不随波逐流有很大的关系。
数据库
2019-03-28 22:25:00
一、语法: CAST (expression AS data_type)
参数说明: expression:任何有效的SQServer表达式。
AS:用于分隔两个参数,在AS之前的是要处理的数据,在AS之后是要转换的数据类型。 data_type:目标系统所提供的数据类型,包括bigint和sql_variant,不能使用用户定义的数据类型。
使用CAST函数进行数据类型转换时,在下列情况下能够被接受: (1)两个表达式的数据类型完全相同。 (2)两个表达式可隐性转换。 (3)必须显式转换数据类型。 如果试图进行不可能的转换(例如,将含有字母的 char 表达式转换为 int 类型),SQServer 将显示一条错误信息。 如果转换时没有指定数据类型的长度,则SQServer自动提供长度为30。
二、注意事项:
(1).CAST()函数的参数是一个表达式,它包括用AS关键字分隔的源值和目标数据类型。以下例子用于将文本字符串'12'转换为整型: SELECT CAST('12' AS int)
(2).返回值是整型值12。如果试图将一个代表小数的字符串转换为整型值,又会出现什么情况呢? SELECT CAST('12.5' AS int)
(3).CAST()函数和CONVERT()函数都不能执行四舍五入或截断操作。由于12.5不能用int数据类型来表示,所以对这个函数调用将产生一个错误: Server: Msg 245, Level 16, State 1, Line 1 Syntax error converting the varchar value '12.5' to a column of data type int.
(4).要返回一个合法的数值,就必须使用能处理这个值的数据类型。对于这个例子,存在多个可用的数据类型。如果通过CAST()函数将这个值转换为decimal类型,需要首先定义decimal值的精度与小数位数。在本例中,精度与小数位数分别为9 与2。精度是总的数字位数,包括小数点左边和右边位数的总和。而小数位数是小数点右边的位数。这表示本例能够支持的最大的整数值是9999999,而最小的小数是0.01。 SELECT CAST('12.5' AS decimal(9,2)) decimal数据类型在结果网格中将显示有效小数位: 12.50
(5).精度和小数位数的默认值分别是18与0。如果在decimal类型中不提供这两个值,SQL Server将截断数字的小数部分,而不会产生错误。 SELECT CAST('12.5' AS decimal) 结果是一个整数值:12
数据库
2019-03-28 20:55:00
结构化查询语言(Structured Query Language),简称SQL,是一种特殊目的的编程语言,是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系数据库系统;同时也是数据库脚本文件的扩展名。
结构化查询语言是高级的非过程化编程语言,允许用户在高层数据结构上工作。它不要求用户指定对数据的存放方法,也不需要用户了解具体的数据存放方式,所以具有完全不同底层结构的不同数据库系统, 可以使用相同的结构化查询语言作为数据输入与管理的接口。结构化查询语言语句可以嵌套,这使它具有极大的灵活性和强大的功能。
开始学习: SQL完全自学手册
SQL 是一种标准 - 但是...
虽然 SQL 是一门 ANSI(American National Standards Institute 美国国家标准化组织)标准的计算机语言,但是仍然存在着多种不同版本的 SQL 语言。
然而,为了与 ANSI 标准相兼容,它们必须以相似的方式共同地来支持一些主要的命令(比如 SELECT、UPDATE、DELETE、INSERT、WHERE 等等)。
在您的网站中使用 SQL
要创建一个显示数据库中数据的网站,您需要:
RDBMS 数据库程序(比如 MS Access、SQL Server、MySQL)
使用服务器端脚本语言,比如 PHP 或 ASP
使用 SQL 来获取您想要的数据
使用 HTML / CSS
RDBMS
RDBMS 指关系型数据库管理系统,全称 Relational Database Management System。
RDBMS 是 SQL 的基础,同样也是所有现代数据库系统的基础,比如 MS SQL Server、IBM DB2、Oracle、MySQL 以及 Microsoft Access。
RDBMS 中的数据存储在被称为表的数据库对象中。
表是相关的数据项的集合,它由列和行组成。
更多精品课程:
阿里云云计算助理工程师认证(ACA)
阿里云大数据助理工程师认证(ACA)
阿里云云安全助理工程师认证(ACA)
数据库
2019-03-28 18:26:00
要想知道每个数据库的大小的话,步骤如下:
1、show DATABASES;
2、进入information_schema 数据库(存放了其他的数据库的信息)
use information_schema;

3、查询所有数据的大小:
select concat(round(sum(data_length/1024/1024),2),'MB') as data from tables;

4、查看指定数据库的大小:
比如查看数据库home的大小
select concat(round(sum(data_length/1024/1024),2),'MB') as data from tables where table_schema='home';

5、查看指定数据库的某个表的大小
比如查看数据库home中 members 表的大小
select concat(round(sum(data_length/1024/1024),2),'MB') as data from tables where table_schema='home' and table_name='members';
数据库
2019-03-28 14:19:00
要在启动时自动运行Flyway数据库迁移,请将其添加 org.flywaydb:flyway-core 到类路径中。
迁移是表单中的脚本 V__.sql (使用 下划线分隔的版本,例如“1”或“2_1”)。默认情况下,它们位于名为的文件夹中 classpath:db/migration ,但您可以通过设置修改该位置 spring.flyway.locations 。这是一个或多个 classpath: 或 filesystem: 位置的逗号分隔列表。例如,以下配置将在默认类路径位置和 /opt/migration 目录中搜索脚本: spring.flyway.locations = classpath:db / migration,filesystem:/ opt / migration
您还可以添加特殊 {vendor} 占位符以使用特定于供应商的脚本。假设如下: spring.flyway.locations = classpath:db / migration / {vendor}
db/migration 前面的配置不是使用,而是根据数据库的类型(例如 db/migration/mysql MySQL)设置要使用的文件夹。支持的数据库列表可在以下位置找到 DatabaseDriver 。
FlywayProperties 提供了大部分Flyway的设置和一小组附加属性,可用于禁用迁移或关闭位置检查。如果您需要更多控制配置,请考虑注册 FlywayConfigurationCustomizer bean。
Spring Boot调用 Flyway.migrate() 以执行数据库迁移。如果您想要更多控制权,请提供 @Bean 实施方案 FlywayMigrationStrategy 。
Flyway支持SQL和Java 回调 。要使用基于SQL的回调,请将回调脚本放在该 classpath:db/migration 文件夹中。要使用基于Java的回调,请创建一个或多个实现的bean Callback 。任何此类bean都会自动注册 Flyway 。它们可以通过使用 @Order 或实施来订购 Ordered 。 FlywayCallback 也可以检测实现已弃用接口的Bean ,但它们不能与 Callback bean 一起使用。
默认情况下,Flyway会在您的上下文中自动装配( @Primary ) DataSource 并将其用于迁移。如果您想使用其他的 DataSource ,可以创建一个并将其标记 @Bean 为 @FlywayDataSource 。如果您这样做并想要两个数据源,请记住创建另一个数据源并将其标记为 @Primary 。或者,您可以 DataSource 通过设置 spring.flyway.[url,user,password] 外部属性来使用Flyway的原生。设置任何一个 spring.flyway.url 或 spring.flyway.user 足以使Flyway使用它自己的 DataSource 。如果未设置三个属性中的任何一个, spring.datasource 则将使用其等效属性的值。
有一个 Flyway示例 ,您可以看到如何设置。
您还可以使用Flyway为特定方案提供数据。例如,您可以放置​​特定于测试的迁移, src/test/resources 并且仅在应用程序启动测试时才运行它们。此外,您可以使用特定于配置文件的配置进行自定义, spring.flyway.locations 以便某些迁移仅在特定配置文件处于活动状态时运行。例如,在中 application-dev.properties ,您可以指定以下设置: spring.flyway.locations = classpath:/ db / migration,classpath:/ dev / db / migration
使用该设置, dev/db/migration 仅在 dev 配置文件处于活动状态时才会运行迁移。
数据库
2019-03-26 21:28:00
CentOS7.5 install Oracle12.1 and update
1.1 、 install centos7.5
1.2 、 vi etc/sysctl.conf
add row:
fs.aio-max-nr = 1048576
fs.file-max = 6815744
kernel.shmmni = 4096
kernel.shmall = 1073741824
kernel.shmmax = 4398046511104
kernel.sem = 250 32000 100 128
net.ipv4.ip_local_port_range = 1024 65500
net.core.rmem_default = 262144
net.core.rmem_max = 4194304
net.core.wmem_default = 262144
net.core.wmem_max = 1048576
vm.swappiness = 0
vm.vfs_cache_pressure = 62
# net.core.default_qdisc = fq
# net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 1800
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
#### net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_max_tw_buckets = 5000
1.3 、 vi etc/security/limits.conf
add row:
oracle soft nofile 1024
oracle hard nofile 65536
oracle soft nproc 2047
oracle hard nproc 16384
oracle soft stack 10240oracle hard stack 32768
1.4 、 vi etc/pam.d/login
add row:
session required /lib64/security/pam_limits.so
session required pam_limits.so
1.5 、 vi etc/profile
add row:
if [ $USER = "oracle" ]; then
if [ $SHELL = "/bin/ksh" ]; then
ulimit -p 16384
ulimit -n 65536
else
ulimit -u 16384 -n 65536
fi
fi
1.6 、 vi etc/hosts
add row:127.0.0.1 oracle12c oracle12c.localdomain
sysctl -p
1.7 、 for group in oinstall dba backupdba oper dgdba kmdba; do
groupadd $group
; done
1.8 、 useradd -g oinstall -G dba,oper,backupdba,dgdba,kmdba -d
/home/oracle -m
oracle
1.9 、 passwd oracle
1.10 、 vi home/oracle/.bash_profile
add row:
export ORACLE_BASE=/opt/oracle #oracle 数据库安装目录
export ORACLE_HOME=$ORACLE_BASE/12c #oracle 数据库
路径
export ORACLE_SID=oracle12c #oracle 启动数据库实例名
export ORACLE_TERM=xtermexport ORACLE_HOSTNAME=oracle12c.localdomain
export PATH=$PATH:$HOME/bin:$ORACLE_HOME/bin
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:/usr/lib
if [ $USER = "oracle" ]; then
if [ $SHELL = "/bin/ksh" ]; then
ulimit -p 16384
ulimit -n 65536
else
ulimit -u 16384 -n 65536
fi
umask 022
fi
export LANG=C # 防止安装过程出现乱码
export NLS_LANG=AMERICAN_AMERICA.ZHS16GBK # 设
置 Oracle 客户端字符集,必须与 Oracle# 安装时设置的字符集保持
一致,如: ZHS16GBK ,否则出现数据导入导出中文乱码问题
1.11 、 mount -B /run/media/birdofprey/CentOS\ 7\ x86_64/ /mnt
1.12 、 cp -r /etc/yum.repos.d/ /etc/yum.repos.d.backup
1.13 、 vi etc/yum.repos.d/CentOS-Base.repo
contents:
[base]
name=CentOS-$releasever - Base
baseurl=file:///mnt
gpgcheck=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
enabled=1
1.14 、 yum -y install binutils compat-libstdc++ compat-libstdc++-
33 elfutils-libelf-devel gcc gcc-c++ glibc-devel glibc-headers ksh
libaio-devel libstdc++-devel make sysstat unixODBC-devel
binutils-* compat-libstdc++* elfutils-libelf* glibc* gcc-* libaio*
libgcc* libstdc++* make* sysstat* unixODBC* wget unzip
compat-libcap1.i686 compat-libcap1.x86_64
1.15 、 upload oracle12cinstall.zip
1.16 、 ./runInstaller2、update 12.1
2.1、download zip
p28259833_121020_Linux-x86-64.zip #### Database Patch Set Update 12.1.0.2.181016
p28440711_121020_Linux-x86-64.zip #### OJVM PATCH SET UPDATE 12.1.0.2.181016
p6880880_121010_Linux-x86-64.zip #### OPatch Version: 12.2.0.1.16
2.2、以 oracle 登陆用户,vi .bash_profile :
add row:
export PATH=$PATH:$HOME/bin:$ORACLE_HOME/bin:$ORACLE_HOME/OPatch
2.3、mv $ORACLE_HOME/Opatch $ORACLE_HOME/Opatch.backup
2.4、unzip p6880880_121010_Linux-x86-64.zip
2.5、mv Opatch/ $ORACLE_HOME/
2.6、sqlplus / as sysdba
SQL>SHUTDOWN IMMEDIATE;
2.7、lsnrctl stop
2.8、cd /28440711
2.9、opatch prereq CheckConflictAgainstOHWithDetail -ph ./ && opatch apply
2.10、cd cd 28259833 && opatch apply
2.11、sqlplus / as sysdba
SQL>startup;
SQL>alter pluggable database {databasename} open;
SQL>show pdbs;
2.12、cd $ORACLE_HOME/Opatch
2.13、./datapatch -verbose
2.14、shutdown
2.15、startup
2.16、alter pluggable database all open;
2.17、sqlplus / as sysdba
SQL> select comments from dba_registry_history;
COMMENTS
--------------------------------------------------------------------------------
RAN jvmpsu.sql
RDBMS_12.1.0.2.0DBPSU_LINUX.X64_161210
SQL>select * from product_component_version
3、update linux kernel and configure autostart oracle12c
3.1、[oracle @localhost ~]$ vi /lib/systemd/system/oracle-rdbms.service
row:
# /etc/systemd/system/oracle-rdbms.service
# Invoking Oracle scripts to start/shutdown Instances defined in /etc/oratab
# and starts Listener
[Unit]
Description=Oracle Database(s) and Listener
Requires=network.target
[Service]
Type=forking
Restart=no
ExecStart=/opt/oracle/12c/bin/dbstart /opt/oracle/12c ### 按照用户自己的 ORACLE_HOME 编写ExecStop=/opt/oracle/12c/bin/dbshut /opt/oracle/12c ### 按照用户自己的 ORACLE_HOME 编写
User=oracle
[Install]
WantedBy=multi-user.target
# kongxx
# CSDN
# https://blog.csdn.net/kongxx/article/details/82155876
3.2、[oracle @localhost ~]$ systemctl daemon-reload
[oracle @localhost ~]$ systemctl enable oracle-rdbms.service
3.3、[oracle @localhost ~]$ vi /etc/oratab
rewrit:
oracle12c:/opt/oracle/12c:Y
3.4、download linux-kernel package:
kernel-ml-4.18.3-1.el7.elrepo.x86_64.rpm,
kernel-ml-devel-4.18.3-1.el7.elrepo.x86_64.rpm,
kernel-ml-doc-4.18.3-1.el7.elrepo.noarch.rpm,
kernel-ml-headers-4.18.3-1.el7.elrepo.x86_64.rpm,
kernel-ml-tools-4.18.3-1.el7.elrepo.x86_64.rpm,
kernel-ml-tools-libs-4.18.3-1.el7.elrepo.x86_64.rpm,
perf-4.18.3-1.el7.elrepo.x86_64.rpm,
python-perf-4.18.3-1.el7.elrepo.x86_64.rpm
rpm -ivh *.rpm --force –nodeps
cat /boot/grub2/grub.cfg | grep menuentry
menuentry 'CentOS Linux (4.18.3-1.el7.elrepo.x86_64) 7 (Core)'
grub2-set-default "CentOS Linux (4.18.3-1.el7.elrepo.x86_64) 7 (Core)"
grub2-editenv list
4、update oracle12c is archivelog mode and rman dbbackup full database
4.1、sqlplus / as sysdba
SQL>shutdown immediate;
SQL>startup mount;
SQL>alter database archivelog;
SQL>alter database open;
SQL>alter pluggable database oracle12cpdb open;
SQL> select log_mode from v$database;
4.2、rman target /
CONFIGURE CONTROLFILE AUTOBACKUP ON; # default
CONFIGURE CONTROLFILE AUTOBACKUP FORMAT FOR DEVICE TYPE DISK TO
'/opt/oracle/oracle12cdbbackupfull/oracle12c/control%F.bak';
CONFIGURE CHANNEL DEVICE TYPE DISK MAXPIECESIZE 1024 M;
4.3、cd $ORACLE_BASE && mkdir oracle12cdbbackupfull && cd oracle12cdbbackupfull/
vi dbfullbackup.rman
row:
run{
crosscheck backup of database;
delete noprompt backupset;
backup as backupset database
format
'/opt/oracle/oracle12cdbbackupfull/oracle12c/oracle12cdbbackupfull%U_%d_%T_%s_
%p.bak'include current controlfile
plus archivelog format '/opt/oracle/oracle12cdbbackupfull/oracle12c/oracle12carchivelog%U_%d_
%T_%s_%p.bak' delete all input;
report obsolete;
delete noprompt obsolete;
}
4.4、vi oracle12cdbfullbackup.sh
row:
!#/bin/bash
cd /opt/oracle/oracle12cdbbackupfull
rman target / @dbfullbackup.rman
chmod +x oracle12cdbfullbackup.sh
4.5、crontab -e
row:
0
3
*
*
*
/opt/oracle/oracle12cdbbackupfull/oracle12cdbfullbackup.sh
/opt/oracle/oracle12cdbbackupfull/rman.log 2>&1
>
感谢打赏,不论多少,都感激不尽
数据库
2019-03-26 20:41:00
SQL Server MySQL
查询限制条数 语法:
SELECT TOP number|percent column_name(s) FROM table_name
例子;
SELECT TOP 3 * FROM Customers;
语法:
SELECT * FROM table_name LIMIT number;
例子:
SELECT * FROM ratings ORDER BY category LIMIT 5;
数据类型转换
创建临时表
语法:CONVERT(data_type(length), expression, style)
说明:
data_type:【必要】支持的数据类型
bigint,
int,
smallint,
tinyint,
bit,
decimal,
numeric,
money,
smallmoney,
float,
real,
datetime,
smalldatetime,
char,
varchar,
text,
nchar,
nvarchar,
ntext,
binary,
varbinary,
image

expression:【必要】转换成其他数据类型的值
length:【可选】数据类型的长度
例子:
SELECT CONVERT(varchar, 25.65);
表名必须以“#“开头
CREATE TABLE #MaleStudents
(
name VARCHAR(50),
age int,
gender VARCHAR (50)

)
语法:CONVERT(expr, type) 等价于 CAST(expr AS type)
说明:
expr:转换成其他数据类型的值
type:数据类型
BINARY[(N)]
CHAR[(N)] [charset_info]
DATE
DATETIME
DECIMAL[(M[,D])]
JSON
NCHAR[(N)]
SIGNED [INTEGER]
TIME
UNSIGNED [INTEGER]
例子:
SELECT CONVERT('test', CHAR CHARACTER SET utf8);
SELECT CAST('test' AS CHAR CHARACTER SET utf8);
语法:CREATE TEMPORARY TABLE new_tbl SELECT * FROM orig_tbl LIMIT 0;
数据库
2019-03-26 16:02:00
摘要:也许你在别的地方听说过Git。也许有人告诉过你,Git只适合软件开发人员。如果你是数据科学家,那么Git其实对你很重要。本文作者希望能够通过经验分享让你了解Git的重要性,以及如何在你的数据科学工作中使用它。
什么是 Git ?
Git是一个分布式版本控制系统,用于在软件开发期间跟踪源代码的更改。看看维基百科给出的这个定义,好像Git专门是为软件开发人员而设计的。实际上,Git是当今世界上使用最广泛的现代版本控制系统,它是以分布式的协作方式为项目(开源或商业)做出了伟大的贡献。除了分布式版本控制系统之外,Git的还考虑了性能、安全性和灵活性。现在你已经了解了Git是什么,但是你脑海中的问题可能是,“如果我是做数据科学项目的人,它与我的工作有什么关系?”以前我也一样不能理解Git的重要性,直到我开始在现实工作环境中,我才发现它时如此重要!
为什么是 Git ?
我们来谈谈为什么?一年前,我决定学习Git。我在Github上分享并发布了 我的代码 ,这是我在CERN的论文项目。虽然很难理解Git中常用的术语(git-add、commit、push、pull等),但我知道这在数据科学领域很重要,这使我的数据科学工作比以往任何时候都更加充实。
所以我保持学习状态,并坚持“committing”。当我加入我目前的公司时,我在Git方面的经验就派上了用场,因为Git是跨不同团队进行代码开发和协作的主要方式。更重要的是,当你的组织遵循敏捷软件开发框架时,Git尤其有用,在该框架中,Git的分布式版本控制使整个开发工作流更加高效、快速且易于适应变化。那么什么是版本控制呢?版本控制是一个系统记录一个文件或一组文件随时间的变化,以便你以后可以调用特定的版本。比如说,你是一个数据科学家,与一个团队合作,在这个团队中你和另一个数据科学家在构建机器学习模型的时候,对同一个特征进行工作。如果你对该特征做了一些更改并上传到远程存储库,并且这些更改与主分支合并,那么你的项目现在变成了1.1版本。另一位数据科学家也对版本1.1的相同功能进行了一些更改,新的更改现在与主分支合并。模型就变成1.2版本。在任何时候,如果你的团队发现版本1.2在发布期间有一些错误,他们随时可以调用以前的版本1.1,这就是版本控制的美妙之处。
作为数据科学家如何使用 Git ?
我们已经讨论过什么是Git及其重要性。现在的问题归结为:作为数据科学家如何使用Git?作为数据科学家,你不需要成为一个Git领域的专家。关键是要理解Git技术的工作流程以及如何在日常工作中使用Git。准确地说,我在这里使用的是Git Feature Branch Workflow,它通常被开源和商业项目使用。如果你想更多地了解这里使用的术语, 点击这里进行了解 。
Git Feature Branch Workflow
Feature Branch Workflow像一个中央存储库,master分支代表正式的项目历史记录。开发人员每次开始处理一个新特性时,都会创建一个新的分支,而不是直接提交到他们的本地主分支上。新的分支可以(也应该)推送到中央存储库。在这种情况下,可以在不修改master分支的情况下与其他开发人员共享一个该分支。
在开始执行任何操作之前,请键 入 git remote -v
以确保工作区指向要使用的远程存储库。
1、从主分支开始,创建一个新分 支 git checkout master git pull git checkout -b branch-name
如果总是维护和更新主分支,则切换到本地主分支,并将最新的提交和代码提取到本地主分支。假设你希望创建一个本地分支,向代码中添加一个新功能,并稍后上传到远程存储库。一旦你将最新的代码更新到本地master分支,我们就创建并checkout出一个名为branch-name的新分支,所有的更改都将在此本地分支上进行。这意味着你本地的master分支不会受到任何影响。
2、更新、添加、提交并将更改推送到远程存储库 git status git add git commit -m 'your message' git push -u origin branch-name
上面我们做了很多操作,让我们详细了解它。一旦发生了一些更新,就将新的操作add到本地分支,并且希望将该操作上传到远程分支,以便合并到远程主分支。git status将输出你对文件的所有更改(跟踪或未跟踪)。在使用git commit-m“your message”提交消息更改之前,你将使用git add 决定要暂存哪些文件。
在此阶段,你的更改仅显示在本地分支中。为了使你的更改显示在BitBucket上的远程分支中,你需要使用git push -u origin branch-name命令进行提交。此命令将该分支推送到中央存储库,并且-u表示将其添加为远程跟踪分支。在设置了跟踪分支之后,可以在没有任何参数的情况下调用git push,以自动将新的功能分支推送到BitBucket上的中央存储库。
3、 创建 pull 请求
现在你已经成功地添加了一个新功能并推送到远程分支。你为自己的贡献感到骄傲,你希望在将远程分支与远程主分支合并之前得到团队成员的反馈。在该分支合并到主分支之前,让其他团队成员有机会对其进行审查。你可以在BitBucket上创建pull请求。现在,你的团队成员已经查看了你的代码,并决定在代码可以合并到主代码库-master分支之前,需要你进行一些其他更改。 git status git add git commit -m 'your message' git push
现在,你可以按照与之前相同的步骤进行更改、提交并最终将更新推送到中央存储库。一旦使用了git push,你的更新将自动显示在pull请求中。如果其他人已将目标更改为你所接触的同一代码,则会发生合并冲突,这在工作中很常见。你可以在 这里 看到如何解决合并冲突。一旦一切顺利完成,这些功能将会合并到master分支中。
当我第一次开始学习Git时,我感到非常沮丧,因为我仍然没有真正理解工作流。这也是写这篇文章的主要原因之一,它真正分解并在更高层次的理解上向你解释工作流程。因为我相信,对工作流程中发生的事情有一个清晰的了解将使学习过程更加有效。
作者:【方向】
原文链接
本文为云栖社区原创内容,未经允许不得转载。
数据库
2019-03-25 13:03:00
一个划时代的知识女性——《杨绛传》读书笔记心得感想4300字:
读书可以让我无比的快乐,但是这本书读完心中却满是苦闷,这不是杨先生的苦闷是我的杞人忧天,特别是讲到文革时期的钱杨和女儿、钱老相继撒手人寰的时候,我甚至边落泪边看完的!
以下是我阅读过程中的一些记录,很零星也很琐碎,就全当个人的心得略加记录了!
一、恋家的杨先生
杨绛非常恋家,并不贪玩却贪看书,回家还帮助父亲做些事情。有一次,杨荫杭(杨绛的父亲)问她:“阿季(杨绛原名杨季康),三天不让你看书,你怎么样?”“不好过。”杨绛说。一星期不让你看书呢?”“一星期都白过了。”杨荫杭笑道:“我也这样。
也许我们现在娱乐活动太丰富,科技发展太快,如果他们父女间对话中的书改成手机是不是和当下贴切多了。
二、钱老的“痴气”
杨绛介绍说:“锺书的‘痴气’书本里灌注不下,还洋溢出来。我们在牛津时,他午睡,我临帖,可是一个人写写字困上来,便睡着了。他醒来见我睡了,就饱蘸浓墨,想给我画个花脸。
钱老的“痴”其实也是很可爱的!
比如:
那时杨先生刚刚生完孩子,还在医院里,钱老每天都到产院探望,常苦着脸对杨绛说“我做坏事了”。原来他打翻了墨水瓶,把房东家的桌布染了。杨绛说:“不要紧,我会洗。”“墨水呀!”“墨水也能洗。”他就放心回去。第二天他又“做坏事了”,把台灯砸了。杨绛问明是怎样的灯,她说:“不要紧,我会修。”他又放心回去。下一次他又满面愁虑,说是把门轴弄坏了,门轴两头的门球脱落了一个,门不能关了。杨绛说:“不要紧,我会修。”他又放心回去。
看吧,这哪是什么“痴”啊,明明是可爱,而且从杨先生口中讲出来还略带炫耀的意思呢!这样鲜活的两位大家也只能在书中慢慢品味了!
三、文学创作的小感悟
在创作过程中,无论所介绍的内容是有关别人的,还是她自己的,行文都含蓄、简约,其思想、情感,不予以特别强调,宁肯少说一点儿,给人多些可以回味的东西。所以杨绛的散文处处散发着大气的美,成熟的美。
在杨绛的笔下,没有高大的英雄人物,只有很平常、很普通的人物,不管是可亲可爱的,还是可憎可恶的,抑或是可悲可叹的人物,他们在日常生活中很常见,演绎的是这些寻常人物的家长里短,因而更带有生活气息。
在艺术创造中,想象极其重要,但它必须贴切真实,离不开“判断力”的调控。杨绛通过对众多作品的分析,理出形象思维与逻辑思维的关系:“小说家的构思,一方面靠想象力的繁衍变幻,以求丰富多彩,一方面还靠判断力的修改剪裁,以求贴合人生真相。前者是形象思维,后者是逻辑思维,二种思维同时并用。
近日在看这本书的同时也找了些杨绛的散文来看,看完以后受益匪浅,然而自己的创作却进入的死胡同,每每一篇很简单的文章都要几经修改,甚至写到一半写不下去,骆驼祥子好词好句(http://www.simayi.net/dushubiji/735.html)又不得不推翻重来,这个过程很痛苦,我和朋友玩笑这种感觉有点像不孕不育或者根本就是难产。曾经一气呵成的写作状态突然没有了,苦闷至极。
四、传统的知识女性和一生的相爱
杨绛待人接物,一招一式无不透出中国传统文化的底蕴。陈子谦曾告诉读者他“一直难以忘怀”的一件小事,他写道:“1984年5月我去拜访钱锺书先生,那天是杨先生开的门,她是那样温文尔雅,一副娇小文弱的样子。当钱先生让我坐下以后,杨先生从里屋用旧式茶盘端出两杯茶来,递一杯给我,递一杯给钱先生,然后双手托着茶盘一直背朝里屋退下,直到我告别时她才从里屋出来,满脸微笑送我到门口,我连忙请杨先生留步,只有钱先生送我下楼。杨先生端茶的动作,特别是她的‘却行’显然是一种旧式礼节,这在当时我真还觉得不好理解,特别是对一位后生晚学,何必如此‘讲礼’,这般客气?我真是谜一般地猜不透他们的心蕴。现在看来,这就叫文化,这就是我们的传统,不管你如何漂洋过海,懂得多少门外语,受多少西洋风气的影响,到头来骨子里的还是本民族的东西,根子还得牢牢地扎在民族文化的传统中。
是的就是他们对传统的认知,也是相互的爱慕。
又比如:
杨绛夫妇医院病床对谈的情景:夜渐深,敲窗的雨声时缓时紧,大颗小粒的雨珠沿着玻璃拉长,零碎地折射进星星点点的光亮。
“季康,不是说咱们找的人手明天就来吗?明天你就回家吧。”黑暗里钱锺书说。“这怎么行?咱这只是从帮忙辅助的意义上找的人,我不走。”折叠床上的杨绛说。“你可以站在一旁看看她做,看过了你总该放心,就明天一天啊。”
“默存,我发现《槐聚诗存》上有几处我抄错了字,书都印出来了,这可怎么好?”“打岔,说你该回家的事。”“我怎么能把你的诗抄错了呢?真是的。我怎么会抄错了呢……”小床上她叹着气。“明天你就回家去吧。……”没有回答。在被街衢道路包围的医院里,夜深时总能听见车声。雨地过车声又有不同。床头柜那边传来钱锺书摸索的动静。杨绛问:“找安眠药?”“睡不着,闹离愁了吧?吃一片吧。不用你,不用开灯。”
杨绛起身,按亮壁灯,端上温开水,看着丈夫服下舒乐安。她自己也拈出一片,钱锺书伸手接住。杨绛争道:“这不公平,在家时不是我吃安眠药你也陪着吃吗?你说过中毒俩一块中,岂可让我独中乎?”
就是这样,连安眠药都要分享,这就他们一辈子的爱,在钱老生病的过程中他们的唯一的孩子去世了,读着读着我的眼眶也湿了,好像还轻轻的呜咽起来!????
五、反倒成了被安抚者
据舒展介绍,钱锺书离世后,他的老伴去看望杨绛,一进门还没说话,只见杨绛孤身一人,老伴就抑制不住抽泣,后来干脆放声大哭起来。杨绛拉着她的手,让她坐到沙发上说:“你比钱瑗小四岁吧?傻孩子,我都挺过来了,你还这样哀伤?你不懂呀,如果我走在女儿和锺书前面,你想想,钱瑗、锺书受得了吗?所以,这并不是坏事,你往深处想想,让痛苦的担子由我来挑,这难道不是一件好事吗?”舒展的老伴回来向他传述以后,他说:“瞧你这点出息,让你去安慰老太太,反倒成了被安抚者。”
六、读书就是精神家园
解放前钱锺书和我寓居上海。我们必读的刊物是《生活周报》。寓所附近有一家生活书店,我们下午四点后经常去看书看报;在那儿会碰见许多熟人,和店里工作人员也熟。有一次,我把围巾落在店里了。回家不多久就接到书店的电话:“你落了一条围巾。恰好傅雷先生来,他给带走了,让我通知你一声。”傅雷带走我的围巾是招我们到他家去夜谈;嘱店员打电话是免我寻找失物。这件小事唤起了我当年的感受:生活书店是我们这类知识分子的精神家园。
是啊,读书就是可以丰富人精神的,他就是所有人的精神家园只是我们现在的阅读渠道太多了反而被束缚住了吧。
七、她就是我唯一的杰作
杨绛在《我们的钱瑗》中以《尖兵钱瑗》作为代序,说钱瑗:“她既然只求当尖兵,可说有志竟成,没有虚度此生。”杨绛回忆早年与丈夫钱锺书在探讨女儿个性时,钱锺书说她:“刚正,像外公;爱教书,像爷爷。”两位祖父迥然不相同的性格,在钱瑗身上表现得都很突出。杨绛在文章中提及钱瑗坚强不屈,正直不阿。北师大曾和英国合作培养“英语教学”研究生。钱瑗常和英方管事人争执,怪他们派来的专家英语水平不高,不合北师大英语研究生的要求。结果英国大使请她吃晚宴,向她道歉,同时也请她说说她的计划和要求。钱瑗的回答头头是道,英大使听了点头称善。杨绛听女儿讲了,也明白她是在建立一项有用的学科。
所以说杨先生总说钱瑗是他唯一的杰作,但从钱授的治学上来说,这是令人佩服的,当然英年早逝,也确实让人觉得万分惋惜。
八、论语的指导意义到今天还是有用的
杨绛对《论语》有很独特的见解。她说《论语》最有趣,“读《论语》,读的是一句一句话,看见的却是一个一个人,书里的一个个弟子,都是活生生的,一人一个样儿,各不相同”。
很久之前看了《孔子如来》好像也是这样的,孔夫子与弟子们的对话确实是鲜活的,却又可以把道理说得浅显,让人一看就明白,当然真要明白其所以然的话,还要颇费周章的,毕竟太过久远了,当然还有一层原因,就是我的悟性太低了吧。
九、杨先生的教育观念是超前的
我体会,‘好的教育’首先是启发人的学习兴趣,学习的自觉性,培养人的上进心,引导人们好学,和不断完善自己。要让学生在不知不觉中受教育,让他们潜移默化。这方面榜样的作用很重要,言传不如身教。我自己就是受父母师长的影响,由淘气转向好学的。爸爸说话入情入理,出口成章,《申报》评论一篇接一篇,浩气冲天,掷地有声。我佩服又好奇,请教秘诀,爸爸说:‘哪有什么秘诀?多读书,读好书罢了。杨绛称:“我对现代教育知道的不多。从报上读到过美术家韩美林作了一幅画,送给两三岁的小朋友,小孩子高高兴兴地回去了,又很快把画拿来要韩美林签名,问他签名干什么,小孩说:‘您签了名,这画才值钱!’可惜呀,这么小的孩子已受到社会不良风气的影响,价值观的教育难道不应引起注意吗?”
关于教育这个话题,杨先生的教育方式还是非常超前的,关于韩美林的这个小故事来说,也确实是让我们有所反思。我们当前的教育是不是存在着价值观的取向问题,是我们相关做教育的人来说颇值得关注的,也是应该予以纠偏的事实所在。
十、过年的年味丧失和我们的价值观有关
旧派过旧历年,新派过新历年。但此所谓过年,非空言度过之谓,其意盖指祭祖报神……今世年终所祭之神,固非耶教之上帝,亦非儒家之先圣先贤,不过五路财神耳。此所谓神,近于魔鬼,此所谓祭,近于行贿。”
一句贿赂五路财神,诠释了我们当前对于过年的偏颇,也一针见血的指出了当前社会的唯金钱论的诡异的价值观。现在的人们不得不从中汲取些营养,让我们的小一辈能够在正常的价值观中茁壮的成长。
十一、钱杨的婚姻是经过风雨洗刷的
对于时代,我是落伍者,没有什么良言贡献给现代婚姻。只是在物质至上的时代潮流下,想提醒年轻的朋友,男女结合最最重要的是感情,双方互相理解的程度,理解深才能互相欣赏吸引、支持和鼓励,两情相悦。我以为,夫妻间最重要的是朋友关系,即使不能做知心的朋友,也该是能做得伴侣的朋友或互相尊重的伴侣。门当户对及其他,并不重要。”
看看,杨先生对于婚姻的现状是有预知的,我们的婚姻与爱情已经是如此的物质化了,所以那些所谓不相信爱情的实质是自己把爱情弄岔了,还是杨先生的爱情是经得起风雨和细细品位的。杨先生也从未落伍。
十二、最大的功劳是保住了钱老的痴气
我最大的功劳是保住了钱锺书的淘气和那一团痴气。这是钱锺书的最可贵处。他淘气、天真,加上他过人的智慧,成了现在众人心目中博学而又风趣的钱锺书。他的痴气得到众多读者的喜爱。但是这个钱锺书成了他父亲一辈子担心的儿子,而我这种“洋盘媳妇”,在钱家是不合适的。
一个饱读诗书,留学海外,精通多国外语的时代女性却又是如此的传统。这种相夫教子中成长起来的女性,一路成全丈夫又不断成就自己远比一门心思嫁入豪门来的有趣的多。
数据库
2019-03-19 22:21:00
概述
阿里云数据管理DMS在云端可提供专业的数据库服务,除对标本地数据库软件的基础功能外,还包含性能诊断、数据追踪、跨实例SQL查询(含异构数据库类型之间)等专业性功能,同时提供审计安全和企业级数据库管理服务。
如果您的数据库在阿里云上均可享受这些服务,但在此之前,如果您的数据库在本地或其它云上,则无法免费享受这些服务。为满足自建数据库的管理诉求,主流所使用的解决方案如: 购买VPC专用网络,需要耗费大量成本,一般不适用于小企业或个人用户; 使用给数据库暴露公网端口的方式,这显然是极为不安全的做法,尤其是生产环境不能接受此类处理; 由用户自己做服务请求的转发,不但稳定性得不到保障,对于用户而言,门槛也很高。
因此DMS正式推出了: “数据库网关”服务 ,让您可安全、0成本地将本地数据库或其它云端数据库 (包含本地IDC自建、其他云数据库、其他云服务器上自建的数据库) 接入阿里云DMS,帮助用户打通云上云下资源,使用一套工具即可轻松实现数据库统一管理。从此您不必再担心因为给数据库开放公网端口产生的安全问题、维护成本等问题。
全新接入方案
1、架构图:
(1)本地机房需安装一个数据库网关 (1个互通的网络内仅需要安装1个网关) 。
(2)数据库网关所在机器通过访问公网的DMS Proxy建立连接,该连接将使用https进行加密。
(3)云端DMS(及后续其它数据库产品)对用户进行鉴权后,访问到DB Proxy,DB Proxy再通过步骤2建立的加密通信连接访问到本地数据库网关,本地数据库网关再访问到您的具体数据库。
2、接入云服务方式对比:
3、实操接入,仅需四步:
(1)添加网关 :进入DMS控制台,从左侧:“网关配置”中进去,点击添加网关,如下图所示(一个网关可对应一个内部网络,一个网关上可以添加多个数据库地址):
(2)下载网关程序 :该程序由Java编写,可反编译进行Review代码,DMS承诺只提供云端您主动发起过的数据库操作。您可以直接点击下载安装包或拷贝wget命令到对应需要安装网关的机器上进行执行(请确保该机器事先安装JRE 1.7以上版本且能够顺畅地访问公网)
(3)启动本地网关 :下载后的网关程序解压,进入目录:dms-db-gateway/bin,该目录有2个文件:start.sh/start.bat(Linux和Mac使用start.sh文件,Windows使用start.bat),如下图所示:
此时本地网关如果启动成功,状态会自动更新,并显示“下一步”按钮。本地安装需输入一些阿里云的账号认证信息及随机验证码,便于确认本地网关由本人启动及后期的安全认证,请根据页面上具体的操作提示完成即可。
(4)向网关中添加数据库 :一个网关可以添加多个数据库,本地启动后就可以开始添加了,填写的地址也是本地内网地址(即通过网关所在宿主机去访问数据库的地址),如下图所示:
接下来您就可以通过DMS控制台的“快捷登录->非阿里云库(自建IDC)”进行数据库登录了,也可以在“资源列表”页面登录这些数据库。同时,在这2个页面上可以继续添加本地数据库也可以。
补充说明 :在这里添加的数据库本地数据库,可以在实例授权功能中,授权给子账号,与云上RDS的操作非常类似,后续还会提供这些资源的安全管控和审计等功能。
释放强大服务能力
数据库网关是DMS刚推出的新特性,支持的数据库种类和功能还需有更多提升,未来我们将从以下几个角度为大家提供更多的服务:
1、数据库类型增加:
目前仅支持MySQL数据库类型,后续将会逐步完善支持的数据库类型与阿里云上对齐。
2、功能增强:
目前还未支持性能诊断、数据追踪、跨实例SQL操作等功能,由于在新的技术架构下,这些功能还需进行兼容性处理。还将会为大家提供线上和线下数据资源的对比、迁移、同步、JOIN等操作。
3、相关数据库产品服务:
除DMS产品外,后期会有更多的数据库产品会通过该数据库网关访问本地数据库及其他云端的数据库,例如:企业级DMS、HDM等等,这将为您提供更强大的数据库支持能力。
总结
数据库网关接入后,您的本地数据库享受到阿里云上所提供的数据库产品服务,使得云上云下、跨云之间实现统一管理和自由的数据流动和合并。
详情可移步了解: https://help.aliyun.com/document_detail/102974.html
点击观看DMS发布会 → https://yq.aliyun.com/live/851
专家解读产品年度升级,更有阿里云数据库实验室重磅亮相
DMS企业版超低门槛体验,了解更多产品特性,欢迎访问 :
https://promotion.aliyun.com/ntms/act/dmsenterprise.html
原文链接
数据库
2019-03-06 17:29:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
1、汇总
1.1、问题
多套tidb集群的pd 部署在同样的机器,pd的服务相同,导致pd无法启动
版本:2.1.2
1.2、问题及解决
修改相关文件的端口部分解决
2、具体
2.1、具体问题
2.1.1、系统服务
/etc/systemd/system
pd.service
2.1.2、pd的启停脚本
【${deploy_dir}/scripts/start_pd.sh】
#!/bin/bash
set -e
# WARNING: This file was auto-generated. Do not edit!
# All your edit might be overwritten!
sudo systemctl start pd.service
【 ${deploy_dir} /scripts/stop_pd.sh】
#!/bin/bash
set -e
# WARNING: This file was auto-generated. Do not edit!
# All your edit might be overwritten!
sudo systemctl stop pd.service
郑州冶疗男性不孕不育医院哪家好:http://byby.zztjyy.com/
2.2、修复
tidb中控机:
【1、更改部署的】
/work/tidb/tidb-ansible-2.1/roles/pd/tasks/ systemd_deployment.yml
更改:
service_name: pd- {{ pd_client_port }}
【2、滚动升级的】
/work/tidb/tidb-ansible-2.1/ rolling_update.yml
更改:
- name: stop PD by systemd
systemd: name=pd -{{ pd_client_port }} .service state=stopped
http://www.chacha8.cn/detail/1132398232.html
- name: start PD by systemd
systemd: name=pd -{{ pd_client_port }} .service state=started


【3、更改start的】
/work/tidb/tidb-ansible-2.1/ start.yml
- name: start PD by systemd
systemd: name=pd- {{ pd_client_port }} .service state=started

2.3、修复后结果
手动删除目标pd机器的:
${deploy_dir}/scripts/start_pd.sh
${ deploy_dir } /scripts/stop_pd.sh
${ deploy_dir } /scripts/run_pd.sh
中控机重新部署: ansible-playbook deploy.yml -l pd机器IP

检查:
start_pd.sh
#!/bin/bash
set -e
# WARNING: This file was auto-generated. Do not edit!
# All your edit might be overwritten!
sudo systemctl start pd-10000 .service

stop_pd.sh
#!/bin/bash
set -e
# WARNING: This file was auto-generated. Do not edit!
# All your edit might be overwritten!
sudo systemctl stop pd-10000 .service

cd /etc/systemd/system
pd-10000 .service
数据库
2019-09-26 16:55:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
作者 | 声东 阿里云售后技术专家
以我的经验来讲,理解 Kubernetes 集群服务的概念,是比较不容易的一件事情。尤其是当我们基于似是而非的理解,去排查服务相关问题的时候,会非常不顺利。
这体现在,对于新手来说,ping 不通服务的 IP 地址这样基础的问题,都很难理解;而就算对经验很丰富的工程师来说,看懂服务相关的 iptables 配置,也是有相当的挑战的。
今天这边文章,我来深入解释一下 Kubernetes 集群服务的原理与实现,便于大家理解。

Kubernetes 集群服务的本质是什么
概念上来讲,Kubernetes 集群的服务,其实就是负载均衡、或反向代理。这跟阿里云的负载均衡产品,有很多类似的地方。和负载均衡一样,服务有它的 IP 地址以及前端端口;服务后边会挂载多个容器组 Pod 作为其“后端服务器”,这些“后端服务器”有自己的 IP 以及监听端口。
当这样的负载均衡和后端的架构,与 Kubernetes 集群结合的时候,我们可以想到的最直观的实现方式,就是集群中某一个节点专门做负载均衡(类似 LVS)的角色,而其他节点则用来负载后端容器组。
这样的实现方法,有一个巨大的缺陷,就是单点问题。Kubernetes 集群是 Google 多年来自动化运维实践的结晶,这样的实现显然与其智能运维的哲学相背离的。

自带通信员
边车模式(Sidecar)是微服务领域的核心概念。边车模式,换一句通俗一点的说法,就是自带通信员。熟悉服务网格的同学肯定对这个很熟悉了。但是可能比较少人注意到,其实 Kubernetes 集群原始服务的实现,也是基于 Sidecar 模式的。
在 Kubernetes 集群中,服务的实现,实际上是为每一个集群节点上,部署了一个反向代理 Sidecar。而所有对集群服务的访问,都会被节点上的反向代理转换成对服务后端容器组的访问。基本上来说,节点和这些 Sidecar 的关系如下图所示。

把服务照进现实
前边两节,我们看到了,Kubernetes 集群的服务,本质上是负载均衡,即反向代理;同时我们知道了,在实际实现中,这个反向代理,并不是部署在集群某一个节点上,而是作为集群节点的边车,部署在每个节点上的。
在这里把服务照进反向代理这个现实的,是 Kubernetes 集群的一个控制器,即 kube-proxy。关于 Kubernetes 集群控制器的原理,请参考我另外一篇 关于控制器的文章 。简单来说,kube-proxy 作为部署在集群节点上的控制器,它们通过集群 API Server 监听着集群状态变化。当有新的服务被创建的时候,kube-proxy 则会把集群服务的状态、属性,翻译成反向代理的配置。
那剩下的问题,就是反向代理,即上图中 Proxy 的实现。

一种实现
Kubernetes 集群节点实现服务反向代理的方法,目前主要有三种,即 userspace、iptables 以及 IPVS。今天我们只深入分析 iptables 的方式,底层网络基于阿里云 Flannel 集群网络。

过滤器框架
现在,我们来设想一种场景。我们有一个屋子。这个屋子有一个入水管和出水管。从入水管进入的水,是不能直接饮用的,因为有杂质。而我们期望,从出水管流出的水,可以直接饮用。为了达到目的,我们切开水管,在中间加一个杂质过滤器。

过了几天,我们的需求变了,我们不止要求从屋子里流出来的水可以直接饮用,我们还希望水是热水。所以我们不得不再在水管上增加一个切口,然后增加一个加热器。

很明显,这种切开水管,增加新功能的方式是很丑陋的。因为需求可能随时会变,我们甚至很难保证,在经过一年半载之后,这跟水管还能找得到可以被切开的地方。
所以我们需要重新设计。首先我们不能随便切开水管,所以我们要把水管的切口固定下来。以上边的场景为例,我们确保水管只能有一个切口位置。其次,我们抽象出对水的两种处理方式:物理变化和化学变化。

基于以上的设计,如果我们需要过滤杂质,就可以在化学变化这个功能模块里增加一条过滤杂质的规则;如果我们需要增加温度的话,就可以在物理变化这个功能模块里增加一条加热的规则。
以上的过滤器框架,显然比切水管的方式,要优秀很多。设计这个框架,我们主要做了两件事情,一个是固定水管切口位置,另外一个是抽象出两种水处理方式。
理解这两件事情之后,我们可以来看下 iptables,或者更准确的说法,netfilter 的工作原理。netfilter 实际上就是一个过滤器框架。netfilter 在网络包收发及路由的管道上,一共切了 5 个口,分别是 PREROUTING,FORWARD,POSTROUTING,INPUT 以及 OUTPUT;同时 netfilter 定义了包括 nat、filter 在内的若干个网络包处理方式。

需要注意的是,routing 和 forwarding 很大程度上增加了以上 netfilter 的复杂程度,如果我们不考虑 routing 和 forwarding,那么 netfilter 会变得跟我们的水质过滤器框架一样简单。

节点网络大图
现在我们看一下 Kubernetes 集群节点的网络全貌。横向来看,节点上的网络环境,被分割成不同的网络命名空间,包括主机网络命名空间和 Pod 网络命名空间;纵向来看,每个网络命名空间包括完整的网络栈,从应用到协议栈,再到网络设备。
在网络设备这一层,我们通过 cni0 虚拟网桥,组建出系统内部的一个虚拟局域网。Pod 网络通过 veth 对连接到这个虚拟局域网内。cni0 虚拟局域网通过主机路由以及网口 eth0 与外部通信。
在网络协议栈这一层,我们可以通过编程 netfilter 过滤器框架,来实现集群节点的反向代理。

实现反向代理,归根结底,就是做 DNAT,即把发送给集群服务 IP 和端口的数据包,修改成发给具体容器组的 IP 和端口。
参考 netfilter 过滤器框架的图,我们知道,在 netfilter 里,可以通过在 PREROUTING,OUTPUT 以及 POSTROUGING 三个位置加入 NAT 规则,来改变数据包的源地址或目的地址。
因为这里需要做的是 DNAT,即改变目的地址,这样的修改,必须在路由(ROUTING)之前发生以保证数据包可以被路由正确处理,所以实现反向代理的规则,需要被加到 PREROUTING 和 OUTPUT 两个位置。
其中,PREOURTING 的规则,用来处理从 Pod 访问服务的流量。数据包从 Pod 网络 veth 发送到 cni0 之后,进入主机协议栈,首先会经过 netfilter PREROUTING 来做处理,所以发给服务的数据包,会在这个位置做 DNAT。经过 DNAT 处理之后,数据包的目的地址变成另外一个 Pod 的地址,从而经过主机路由,转发到 eth0,发送给正确的集群节点。
而添加在 OUTPUT 这个位置的 DNAT 规则,则用来处理从主机网络发给服务的数据包,原理也是类似,即经过路由之前,修改目的地址,以方便路由转发。

升级过滤器框架
在过滤器框架一节,我们看到 netfilter 是一个过滤器框架。netfilter 在数据“管到”上切了 5 个口,分别在这 5 个口上,做一些数据包处理工作。虽然固定切口位置以及网络包处理方式分类已经极大的优化了过滤器框架,但是有一个关键的问题,就是我们还是得在管道上做修改以满足新的功能。换句话说,这个框架没有做到管道和过滤功能两者的彻底解耦。
为了实现管道和过滤功能两者的解耦,netfilter 用了表这个概念。表就是 netfilter 的过滤中心,其核心功能是过滤方式的分类(表),以及每种过滤方式中,过滤规则的组织(链)。
把过滤功能和管道解耦之后,所有对数据包的处理,都变成了对表的配置。而管道上的5个切口,仅仅变成了流量的出入口,负责把流量发送到过滤中心,并把处理之后的流量沿着管道继续传送下去。
如上图,在表中,netfilter 把规则组织成为链。表中有针对每个管道切口的默认链,也有我们自己加入的自定义链。默认链是数据的入口,默认链可以通过跳转到自定义链来完成一些复杂的功能。这里允许增加自定义链的好处是显然的。为了完成一个复杂过滤功能,比如实现 Kubernetes 集群节点的反向代理,我们可以使用自定义链来模块化我们规则。

用自定义链实现服务的反向代理
集群服务的反向代理,实际上就是利用自定义链,模块化地实现了数据包的 DNAT 转换。KUBE-SERVICE 是整个反向代理的入口链,其对应所有服务的总入口;KUBE-SVC-XXXX 链是具体某一个服务的入口链,KUBE-SERVICE 链会根据服务 IP,跳转到具体服务的 KUBE-SVC-XXXX 链;而 KUBE-SEP-XXXX 链代表着某一个具体 Pod 的地址和端口,即 endpoint,具体服务链 KUBE-SVC-XXXX 会以一定算法(一般是随机),跳转到 endpoint 链。
而如前文中提到的,因为这里需要做的是 DNAT,即改变目的地址,这样的修改,必须在路由之前发生以保证数据包可以被路由正确处理。所以 KUBE-SERVICE 会被 PREROUTING 和 OUTPUT 两个默认链所调用。

总结
通过这篇文章,大家应该对 Kubernetes 集群服务的概念以及实现,有了更深层次的认识。我们基本上需要把握三个要点: 服务本质上是负载均衡; 服务负载均衡的实现采用了与服务网格类似的 Sidecar 的模式,而不是 LVS 类型的独占模式; kube-proxy 本质上是一个集群控制器。除此之外,我们思考了过滤器框架的设计,并在此基础上,理解使用 iptables 实现的服务负载均衡的原理。

原文链接
本文为云栖社区原创内容,未经允许不得转载。
数据库
2019-09-24 13:53:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
1. 什么是事务?事务的特性(ACID)?
答:事务是程序中一系列严密的操作,所有操作执行必须成功完成,否则在每个操作所做的更改将会被撤销,这也是事务的原子性(要么都成功,要么都失败)。 事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所有操作。 事务的特性有:原子性(Atomicity),隔离性(Isolation),一致性( Consistency),持续性(Durability) 原子性:事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做  一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。  隔离性 :一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 持续性 :也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
2.事务的隔离级别有几种?最常用的隔离级别是哪两种(提交读、可重复读)?
答:并发过程中会出现的问题: 丢失更新:提交一个事务时,把其他事务已提交的更新数据覆盖。 脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。 幻读也叫虚读:一个事务执行两次查询,第二次结果集包含第一次中没有或某些行已经被删除的数据,造成两次结果不一致,只是另一个事务在这两次查询中间插入或删除了数据造成的。幻读是事务非独立执行时发生的一种现象。 不可重复读:一个事务两次读取同一行的数据,结果得到不同状态的结果,中间正好另一个事务更新了该数据,两次结果相异,不可被信任。
事务的四种隔离级别: 未提交读:就是一个事务读取到其他事务未提交的数据,是级别最低的隔离机制。缺点: 会产生脏读、不可重复读、幻读。 提交读:就是一个事务读取到其他事务提交后的数据。Oracle默认隔离级别。缺点:会产生不可重复读、幻读。 可重复读:就是一个事务对同一份数据读取到的相同,不在乎其他事务对数据的修改。MySQL默认的隔离级别。缺点:会产生幻读。 可串行化:事务串行化执行,隔离级别最高,牺牲了系统的并发性。缺点:可以解决并发事务的所有问题。但是效率地下,消耗数据库性能,一般不使用。
3. 什么是索引?
答: 索引其实是一种数据结构,能够帮助我们快速的检索数据库中的数据。
4. 索引具体采用的哪种数据结构呢?
答: 常见的MySQL主要有两种结构:Hash索引和B+ Tree索引,通常使用的是InnoDB引擎,默认的是B+树。当选用memory引擎的时候,就是用的Hash索引。
5. B+ Tree索引和Hash索引区别? 哈希索引适合等值查询,但是无法进行范围查询。 哈希索引没办法利用索引完成排序。 哈希索引不支持多列联合索引的最左匹配规则。 如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题。
6. B+ Tree的叶子节点都可以存哪些东西吗? 他们之间有什么区别?
答:InnoDB的B+ Tree可能存储的是整行数据,也有可能是主键的值。区别:在 InnoDB 里,索引B+ Tree的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引。而索引B+ Tree的叶子节点存储了主键的值的是非主键索引,也被称之为非聚簇索引。
7. 聚簇索引和非聚簇索引,在查询数据的时候有区别吗?
答:聚簇索引查询会更快,因为主键索引树的叶子节点直接就是我们要查询的整行数据了。而非主键索引的叶子节点是主键的值,查到主键的值以后,还需要再通过主键的值再进行一次查询。
8. 主键索引查询只会查一次,而非主键索引需要回表查询多次(这个过程叫做回表)。是所有情况都是这样的吗?非主键索引一定会查询多次吗?
答:覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。当一条查询语句符合覆盖索引条件时,MySQL只需要通过索引就可以返回查询所需要的数据,这样避免了查到索引后再返回表操作,减少I/O提高效率。 如,表covering_index_sample中有一个普通索引 idx_key1_key2(key1,key2)。当我们通过SQL语句:select key2 from covering_index_sample where key1 = 'keytest';的时候,就可以通过覆盖索引查询,无需回表。
9. 在创建索引的时候都会考虑哪些因素呢?在创建联合索引的时候,需要做联合索引多个字段之间顺序,这是如何选择的呢?
答:一般对于查询概率比较高,经常作为where条件的字段设置索引。在创建多列索引时,我们根据业务需求,where子句中使用最频繁的一列放在最左边,因为MySQL索引查询会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。 所以当我们创建一个联合索引的时候,如(key1,key2,key3),相当于创建了(key1)、(key1,key2)和(key1,key2,key3)三个索引,这就是最左匹配原则。
10. 那什么情况下会发生明明创建了索引,但是执行的时候并没有通过索引呢?
在一条单表查询语句真正执行之前,MySQL的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案。这个成本最低的方案就是所谓的执行计划。优化过程大致过程:根据搜索条件,找出所有可能使用的索引;计算全表扫描的代价;计算使用不同索引执行查询的代价;对比各种执行方案的代价,找出成本最低的那一个。
11. 为什么索引结构默认使用B+Tree,而不是Hash,二叉树,红黑树? B+tree是一种多路平衡查询树,节点是天然有序的,非叶子节点包含多个元素,不保存数据,只用来索引,叶子节点包含完整数据和带有指向下一个节点的指针,形成一个有序链表,有助于范围和顺序查找。因为非叶子节点不保存数据,所以同样大小的磁盘页可以容纳更多的元素,同样能数据量的情况下,B+tree相比B-tree高度更低,因此查询时IO会更少。 B-tree不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低; Hash索引底层是基于哈希表,就是以key-value存储数据的结构,多个数据在存储关系上是没有任何顺序关系的。只适合等值查询,不适合范围查询,而且也无法利用索引完成排序,不支持联合索引的最左匹配原则,如果有大量重复键值的情况下,哈希索引效率会很低,因为存在哈希碰撞。 二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。 红黑树:树的高度随着数据量增加而增加,IO代价高。
12. 如何优化MySQL?
答:MySQL优化大致可以分为三部分:索引的优化、SQL语句优化和表的优化 索引优化可以遵循以下几个原则: 联合索引最左前缀匹配原则( 但是这里有一种特殊的情况,如果将一个表中除ID意外的所有字段都组成一个联合索引,那么这个时候就不会遵循最左原则,这个时候无论怎么以哪个字段为where条件,都将用到这个索引。但是当有一个字段没有在这个联合索引索引里面的时候,就会遵循最左原则 ) 尽量把字段长度小的列放在联合索引的最左侧(因为字段越小,一页存储的数据量越大,IO性能也就越好) order by 有多个列排序的,应该建立联合索引 对于频繁的查询优先考虑使用覆盖索引 前导模糊查询不会使用索引,比如说Like '%aaa%'这种,但是可以在mysql中创建全文索引 负向条件不会使用索引,如!=,<>,not like,not in,not exists 索引应该建立在区分度比较高的字段上,一般区分度在80%以上的时候就可以建立索引,区分度可以使用 count(distinct(列名))/count(*) 对于where子句中经常使用的列,最好设置索引 or两边的字段需要索引 SQL语句优化,可以通过explain查看SQL的执行计划,优化语句原则可以有: 在where和order by涉及的列上建立合适的索引,避免全表扫描 任何查询都不要使用select * ,而是用具体的字段列表代替(增加了消耗,e.g CPU,IO等,增加了使用覆盖索引的可能性) 多表连接时,尽量小表驱动大表,即小表join大表 用exists代替in(IN是走rang,就是最差的索引=没有索引,而EXISTS走的ref,已经算是比较好的检索方式了) 尽量避免在where字句中对字段进行函数操作(主要是不会触发索引) SQL语句中in包含的值不应过多(In中的常量全部存储在一个数组中,而且这个数组是排好序的,如果数值较多,产生的消耗也是比较大的) 当只需要一条数据的时候,使用limit 1 如果排序字段没有用到索引,就尽量少排序 尽量用union all代替union(union需要将结果集合并后再进行唯一性过滤操作,会增加大量的CPU运算,加大资源消耗和延迟。当然,前提是union all的前提是两个结果集没有重复的数据) 不要使用Order by rand() in和exists主要是驱动顺序的改变,exists是以外层表为驱动,先被访问;in是先执行子查询。所以in适合于外表大而内表小,exists适合于外表小而内表大的情况, not in和not exists同理 对于分页查询,可以考虑获取前一页的最大的id,用where id > num left join是以左边表为驱动;right join是以右边表为驱动,inner join会自动找出那个数据少的表作为驱动表 数据库表优化 表字段尽可能用not null 字段长度固定表查询会更快 将数据库大表按照时间或者一些标志拆分成小表 水平拆分:将记录散列到不同的表中,每次从分表查询 垂直拆分:将表中的大字段单独拆分到另一张表,形成一对一的关系
数据库
2019-09-20 12:41:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
作者 | 元乙 阿里云日志服务数据采集客户端负责人,目前采集客户端 logtail 在集团百万规模部署,每天采集上万应用数 PB 数据,经历多次双 11、双 12 考验。 导读: 随着 K8s 不断更新迭代,使用 K8s 日志系统建设的开发者,逐渐遇到了各种复杂的问题和挑战。本篇文章中,作者结合自己多年经验,分析 K8s 日志系统建设难点,期待为读者提供有益参考。
在 Logging 这块做了几年,最近 1 年来越来越多的同学来咨询如何为 Kubernetes 构建一个日志系统,或者是来求助在这过程中遇到一系列问题如何解决,授人以鱼不如授人以渔,于是想把我们这些年积累的经验以文章的形式发出来,让看到这篇文章的同学能少走弯路。这个系列文章定位为长篇连载,内容偏向落地实操以及经验分享,且内容会随着技术的迭代而不定期更新。

前言
第一次听到 Kubernetes 的名字是在 2016 年,那个时候 Kubernetes 还处于和 Docker Swarm、Mesos 方案的“三国鼎立时代”,Kubernetes 由于一系列优势(可扩展、声明式接口、云友好)在这一竞争中崭露头角,最终获得统治地位。
Kubernetes 作为 CNCF 最核心的项目(没有之一),是 Cloud Native(云原生)落地的底座,目前阿里已经全面基于 Kubernetes 在开展全站的云原生改造,在 1-2 年内,阿里巴巴 100% 的业务都将跑在公有云上。
CloudNative 在 CNCF 的定义 的核心是:在公有云、私有云、混合云等环境中,通过 Containers、Service Meshes、 MicroServices、Immutable Infrastructure、Declarative APIs 构建和运行可弹性扩展的且具有高容错性、易于管理、可观察、松耦合的应用系统。可观察性是应用系统必不可少的一个部分,云原生的设计理念中就有一条:面向诊断性设计(Diagnosability),包括集群级别的日志、Metric 和 Trace。

为何我们需要日志系统
通常一个线上问题的定位流程是:通过 Metric 发现问题,根据 Trace 定位到问题模块,根据模块具体的日志定位问题原因。在日志中包括了错误、关键变量、代码运行路径等信息,这些是问题排查的核心,因此日志永远是线上问题排查的必经路径。

在阿里的十多年中,日志系统伴随着计算形态的发展在不断演进,大致分为 3 个主要阶段: 在单机时代,几乎所有的应用都是单机部署,当服务压力增大时,只能切换更高规格的 IBM 小型机。日志作为应用系统的一部分,主要用作程序 Debug,通常结合 grep 等 Linux 常见的文本命令进行分析; 随着单机系统成为制约阿里业务发展的瓶颈,为了真正的 Scale out,飞天项目启动:2013 年飞天 5K 项目正式上线。在这个阶段各个业务开始了分布式改造,服务之间的调用也从本地变为分布式,为了更好的管理、调试、分析分布式应用,我们开发了 Trace(分布式链路追踪)系统、各式各样的监控系统,这些系统的统一特点是将所有的日志(包括 Metric 等)进行集中化的存储; 为了支持更快的开发、迭代效率,近年来我们开始了容器化改造,并开始了拥抱 Kubernetes 生态、业务全量上云、Serverless 等工作。在这阶段,日志无论从规模、种类都呈现爆炸式的增长,对日志进行数字化、智能化分析的需求也越来越高,因此统一的日志平台应运而生。

可观察性的终极解读
在 CNCF 中,可观察性的主要作用是问题的诊断,上升到公司整体层面,可观察性(Observability)不仅仅包括 DevOps 领域,还包括业务、运营、BI、审计、安全等领域,可观察性的最终的目标是实现公司各个方面的数字化、智能化。

在阿里,几乎所有的业务角色都会涉及到各式各样的日志数据,为了支撑各类应用场景,我们开发了非常多的工具和功能:日志实时分析、链路追踪、监控、数据加工、流计算、离线计算、BI 系统、审计系统等等。日志系统主要专注于数据的实时采集、清洗、智能分析与监控以及对接各类各样的流计算、离线系统。

Kubernetes 日志系统建设难点

单纯日志系统的解决方案非常多,相对也比较成熟,这里就不再去赘述,我们此次只针对 Kubernetes 上的日志系统建设而论。Kubernetes 上的日志方案相比我们之前基于物理机、虚拟机场景的日志方案有很大不同,例如: 日志的形式变得更加复杂,不仅有物理机/虚拟机上的日志,还有容器的标准输出、容器内的文件、容器事件、Kubernetes 事件等等信息需要采集; 环境的动态性变强,在 Kubernetes 中,机器的宕机、下线、上线、Pod销毁、扩容/缩容等都是常态,这种情况下日志的存在是瞬时的(例如如果 Pod 销毁后该 Pod 日志就不可见了),所以日志数据必须实时采集到服务端。同时还需要保证日志的采集能够适应这种动态性极强的场景; 日志的种类变多,上图是一个典型的 Kubernetes 架构,一个请求从客户端需要经过 CDN、Ingress、Service Mesh、Pod 等多个组件,涉及多种基础设施,其中的日志种类增加了很多,例如 K8s 各种系统组件日志、审计日志、ServiceMesh 日志、Ingress 等; 业务架构变化,现在越来越多的公司开始在 Kubernetes 上落地微服务架构,在微服务体系中,服务的开发更加复杂,服务之间的依赖以及服务底层产品的依赖越来越多,这时的问题排查将更加复杂,如果关联各个维度的日志将是一个困难的问题; 日志方案集成困难,通常我们都会在 Kubernetes 上搭建一套 CICD 系统,这套 CICD 系统需要尽可能的自动化的完成业务的集成和部署,其中日志的采集、存储、清洗等也需要集成到这套系统中,并和 K8s 的声明式部署方式尽可能一致。而现有的日志系统通常都是较独立的系统,集成到 CICD 中代价极大; 日志规模问题,通常在系统初期的时候我们会选择自建开源的日志系统,这种方式在测试验证阶段或公司发展初期是没有什么问题的,但当业务逐渐增长,日志量增长到一定规模时,自建的开源系统很多时候都会遇到各种各样的问题,例如租户隔离、查询延迟、数据可靠性、系统可用性等。日志系统虽不是 IT 中最核心的路径,但一旦关键时刻出现这些问题都将是非常可怕的影响,例如大促的时候出现紧急问题,排查时多个工程师并发查询把日志系统打爆,导致故障恢复时间变长,大促收到影响。
相信在搞 K8s 日志系统建设的同学看到上面的难点分析都会深有感触,后面我们会从落地角度出发,详细介绍在阿里我们如何去搭建 K8s 的日志系统,敬请关注。

原文链接
本文为云栖社区原创内容,未经允许不得转载。
数据库
2019-09-19 14:03:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
sql_mode
是一个很容易被忽视的配置,宽松模式下可能会被输入一些非准确数据,所以生产环境下会要求为严格模式,为了保持生产环境和开发环境,测试环境一致性,我们开发环境和测试环境也要配置成为严格模式。
sql_mode常用值
ONLY_FULL_GROUP_BY:在分组查询语句中如果一个select中的列没有在group by中出现,则该语句是不合法的。
NO_AUTO_VALUE_ON_ZERO:在默认情况下自增长列在插入0或NULL时会自动插入下一个自增长值。当设置该模式情况下,插入0时不会进行自增长依然插入0值。
STRICT_TRANS_TABLES:对事务表进行限制,当一个数据不能插入到事务表中时中断当前操作。对非实物表不做限制。
NO_ZERO_IN_DATE:只要日期的月和日中含有0值都报错,但是‘0000-00-00’除外。
NO_ZERO_DATE:只有‘0000-00-00’报错。
ERROR_FOR_DIVISION_BY_ZERO:在INSERT或UPDATE过程中,如果数据被零除,则产生错误而非警告。如果非该模式下被0除时mysql返回NULL。
NO_AUTO_CREATE_USER:禁止创建密码为空的用户。
NO_ENGINE_SUBSTITUTION:如果需要的存储引擎被禁用或未编译,那么抛出错误。不设置此值时,用默认的存储引擎替代,并抛出一个异常。
PIPES_AS_CONCAT:将"||"视为字符串的连接操作符而非或运算符,这和Oracle数据库是一样的,也和字符串的拼接函数Concat相类似。
ANSI_QUOTES:启用ANSI_QUOTES后,不能用双引号来引用字符串,因为它被解释为识别符。
Oracle数据库切换为mysql数据库
只需要修改sql_mode为:PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS, NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER。
修改sql_mode模式
通过 "SELECT @@sql_mode;"查看当前数据库模式,修改“SET sql_mode = ‘修改为模式’; ”。这种方式只在当前session生效。
同理:select @@global.sql_mode 和 set global sql_mode = '修改后的值';只在当前服务中生效,重启后失效。
修改my.cnf文件重启,永久生效。
数据库
2019-09-17 20:41:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
最近我们扩展了 TiDB 表达式计算框架,增加了向量化计算接口,初期的性能测试显示,多数表达式计算性能可大幅提升,部分甚至可提升 1~2 个数量级。为了让所有的表达式都能受益,我们需要为所有内建函数实现向量化计算。
TiDB 的向量化计算是在经典 Volcano 模型上的进行改进,尽可能利用 CPU Cache,SIMD Instructions,Pipeline,Branch Predicatation 等硬件特性提升计算性能,同时降低执行框架的迭代开销,这里提供一些参考文献,供感兴趣的同学阅读和研究: MonetDB/X100: Hyper-Pipelining Query Execution Balancing Vectorized Query Execution with Bandwidth-Optimized Storage The Design and Implementation of Modern Column-Oriented Database Systems
在这篇文章中,我们将描述: 如何在计算框架下实现某个函数的向量化计算; 如何在测试框架下做正确性和性能测试; 如何参与进来成为 TiDB Contributor。
表达式向量化
1. 如何访问和修改一个向量
在 TiDB 中,数据按列在内存中连续存在 Column 内,Column 详细介绍请看: TiDB 源码阅读系列文章(十)Chunk 和执行框架简介 。本文所指的向量,其数据正是存储在 Column 中。
我们把数据类型分为两种: 定长类型: Int64 、 Uint64 、 Float32 、 Float64 、 Decimal 、 Time 、 Duration ; 变长类型: String 、 Bytes 、 JSON 、 Set 、 Enum 。
定长类型和变长类型数据在 Column 中有不同的组织方式,这使得他们有如下的特点: 定长类型的 Column 可以随机读写任意元素; 变长类型的 Column 可以随机读,但更改中间某元素后,可能需要移动该元素后续所有元素,导致随机写性能很差。
对于定长类型(如 int64 ),我们在计算时会将其转成 Golang Slice(如 []int64 ),然后直接读写这个 Slice。相比于调用 Column 的接口,需要的 CPU 指令更少,性能更好。同时,转换后的 Slice 仍然引用着 Column 中的内存,修改后不用将数据从 Slice 拷贝到 Column 中,开销降到了最低。
对于变长类型,元素长度不固定,且为了保证元素在内存中连续存放,所以不能直接用 Slice 的方式随机读写。我们规定变长类型数据以追加写( append )的方式更新,用 Column 的 Get() 接口进行读取。
总的来说,变长和定长类型的读写方式如下: 定长类型(以 int64 为例)
a. ResizeInt64s(size, isNull) :预分配 size 个元素的空间,并把所有位置的 null 标记都设置为 isNull ;
b. Int64s() :返回一个 []int64 的 Slice,用于直接读写数据;
c. SetNull(rowID, isNull) :标记第 rowID 行为 isNull 。 变长类型(以 string 为例)
a. ReserveString(size) :预估 size 个元素的空间,并预先分配内存;
b. AppendString(string) : 追加一个 string 到向量末尾;
c. AppendNull() :追加一个 null 到向量末尾;
d. GetString(rowID) :读取下标为 rowID 的 string 数据。
当然还有些其他的方法如 IsNull(rowID) , MergeNulls(cols) 等,就交给大家自己去探索了,后面会有这些方法的使用例子。
2. 表达式向量化计算框架
向量化的计算接口大概如下( 完整的定义在这里 ): vectorized() bool vecEvalXType(input *Chunk, result *Column) error XType 可能表示 Int , String 等,不同的函数需要实现不同的接口; input 表示输入数据,类型为 *Chunk ; result 用来存放结果数据。
外部执行算子(如 Projection,Selection 等算子),在调用表达式接口进行计算前,会通过 vectorized() 来判断此表达式是否支持向量化计算,如果支持,则调用向量化接口,否则就走行式接口。
对于任意表达式,只有当其中所有函数都支持向量化后,才认为这个表达式是支持向量化的。
比如 (2+6)*3 ,只有当 MultiplyInt 和 PlusInt 函数都向量化后,它才能被向量化执行。
为函数实现向量化接口
要实现函数向量化,还需要为其实现 vecEvalXType() 和 vectorized() 接口。 在 vectorized() 接口中返回 true ,表示该函数已经实现向量化计算; 在 vecEvalXType() 实现此函数的计算逻辑。
尚未向量化的函数在 issue/12058 中,欢迎感兴趣的同学加入我们一起完成这项宏大的工程。
向量化代码需放到以 _vec.go 结尾的文件中,如果还没有这样的文件,欢迎新建一个,注意在文件头部加上 licence 说明。
这里是一个简单的例子 PR/12012 ,以 builtinLog10Sig 为例: 这个函数在 expression/builtin_math.go 文件中,则向量化实现需放到文件 expression/builtin_math_vec.go 中; builtinLog10Sig 原始的非向量化计算接口为 evalReal() ,那么我们需要为其实现对应的向量化接口为 vecEvalReal() ; 实现完成后请根据后续的说明添加测试。
下面为大家介绍在实现向量化计算过程中需要注意的问题。
1. 如何获取和释放中间结果向量
存储表达式计算中间结果的向量可通过表达式内部对象 bufAllocator 的 get() 和 put() 来获取和释放,参考 PR/12014 ,以 builtinRepeatSig 的向量化实现为例: buf2, err := b.bufAllocator.get(types.ETInt, n) if err != nil { return err } defer b.bufAllocator.put(buf2) // 注意释放之前申请的内存
2. 如何更新定长类型的结果
如前文所说,我们需要使用 ResizeXType() 和 XTypes() 来初始化和获取用于存储定长类型数据的 Golang Slice,直接读写这个 Slice 来完成数据操作,另外也可以使用 SetNull() 来设置某个元素为 NULL 。代码参考 PR/12012 ,以 builtinLog10Sig 的向量化实现为例: f64s := result.Float64s() for i := 0; i < n; i++ { if isNull { result.SetNull(i, true) } else { f64s[i] = math.Log10(f64s[i]) } }
3. 如何更新变长类型的结果
如前文所说,我们需要使用 ReserveXType() 来为变长类型预分配一段内存(降低 Golang runtime.growslice() 的开销),使用 AppendXType() 来追加一个变长类型的元素,使用 GetXType() 来读取一个变长类型的元素。代码参考 PR/12014 ,以 builtinRepeatSig 的向量化实现为例: result.ReserveString(n) ... for i := 0; i < n; i++ { str := buf.GetString(i) if isNull { result.AppendNull() } else { result.AppendString(strings.Repeat(str, int(num))) } }
4. 如何处理 Error
所有受 SQL Mode 控制的 Error,都利用对应的错误处理函数在函数内就地处理。部分 Error 可能会被转换成 Warn 而不需要立即抛出。
这个比较杂,需要查看对应的非向量化接口了解具体行为。代码参考 PR/12042 ,以 builtinCastIntAsDurationSig 的向量化实现为例: for i := 0; i < n; i++ { ... dur, err := types.NumberToDuration(i64s[i], int8(b.tp.Decimal)) if err != nil { if types.ErrOverflow.Equal(err) { err = b.ctx.GetSessionVars().StmtCtx.HandleOverflow(err, err) // 就地利用对应处理函数处理错误 } if err != nil { // 如果处理不掉就抛出 return err } result.SetNull(i, true) continue } ... }
5. 如何添加测试
我们做了一个简易的测试框架,可避免大家测试时做一些重复工作。
该测试框架的代码在 expression/bench_test.go 文件中,被实现在 testVectorizedBuiltinFunc 和 benchmarkVectorizedBuiltinFunc 两个函数中。
我们为每一个 builtin_XX_vec.go 文件增加了 builtin_XX_vec_test.go 测试文件。当我们为一个函数实现向量化后,需要在对应测试文件内的 vecBuiltinXXCases 变量中,增加一个或多个测试 case。下面我们为 log10 添加一个测试 case: var vecBuiltinMathCases = map[string][]vecExprBenchCase { ast.Log10: { {types.ETReal, []types.EvalType{types.ETReal}, nil}, }, }
具体来说,上面结构体中的三个字段分别表示: 该函数的返回值类型; 该函数所有参数的类型; 是否使用自定义的数据生成方法(dataGener), nil 表示使用默认的随机生成方法。
对于某些复杂的函数,你可自己实现 dataGener 来生成数据。目前我们已经实现了几个简单的 dataGener,代码在 expression/bench_test.go 中,可直接使用。
添加好 case 后,在 expression 目录下运行测试指令: # 功能测试 GO111MODULE=on go test -check.f TestVectorizedBuiltinMathFunc # 性能测试 go test -v -benchmem -bench=BenchmarkVectorizedBuiltinMathFunc -run=BenchmarkVectorizedBuiltinMathFunc
在你的 PR Description 中,请把性能测试结果附上。不同配置的机器,性能测试结果可能不同,我们对机器配置无任何要求,你只需在 PR 中带上你本地机器的测试结果,让我们对向量化前后的性能有一个对比即可。
如何成为 Contributor
为了推进表达式向量化计算,我们正式成立 Vectorized Expression Working Group,其具体的目标和制度详见 这里 。与此对应,我们在 TiDB Community Slack 中创建了 wg-vec-expr channel 供大家交流讨论,不设门槛,欢迎感兴趣的同学加入。
如何成为 Contributor: 在此 issue 内选择感兴趣的函数并告诉大家你会完成它; 为该函数实现 vecEvalXType() 和 vectorized() 的方法; 在向量化测试框架内添加对该函数的测试; 运行 make dev ,保证所有 test 都能通过; 发起 Pull Request 并完成 merge 到主分支。
如果贡献突出,可能被提名为 reviewer,reviewer 的介绍请看 这里 。
如果你有任何疑问,也欢迎到 wg-vec-expr channel 中提问和讨论。 原文阅读: https://pingcap.com/blog-cn/10mins-become-contributor-of-tidb-20190916/
数据库
2019-09-17 14:06:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
平时用来测试的异常处理
我们都是通过dbms_output.put_line来输出异常信息,但是在实际的应用中,需要把异常信息返回给调用的客户端。
其实 RAISE_APPLICATION_ERROR 是将应用程序专有的错误从服务器端转达到客户端应用程序(其他机器上的SQLPLUS或者其他前台开发语言)
raise_application_error(异常类型,传递信息)
异常类型:number 值域:-20000 到-20999
传递信息:varchar2(2000)
DBMS_STANDARD包的RAISE_APPLICATION_ERROR过程,可以重新定义异常错误消息,它为应用程序提供了一种与ORACLE交互的方法。语法如下
RAISE_APPLICATION_ERROR(errorNumber,errorString)
errorNumber是数值在-20000到-20999之间,errorString为自定义的错误信息。
如:
update jobs set job_title = v_newJobTitle where job_id = v_jobid;
if sql%notfound then
RAISE_APPLICATION_ERROR(-20167,'update failure!');
end if;
..........
当在sqlpus中测试时,一旦没有要更新的行,则抛出这样的异常:
ORA-20167: update failure!
oracle的异常分为编译时异常(错误)和运行时异常,前者不能被处理,后者可以被处理。
我们主要讨论运行时异常。
异常类型:
a、预定义异常
已命名的预定义异常有CURSOR_ALREADY_OPEN、INVALID_NUMBER、TOO_MANY_ROWS等
b、用户定义异常
c、已命名异常(已命名异常不能单独归为一类异常,但它有点特别,所以我将它单独罗列说明)
如果希望处理一个异常(被when子串处理),那么异常必须有一个名字,如TOO_MANY_ROWS;
数据库错误有数千个,但是只有不到25个是内建的已命名异常(这些异常都声明在standard包中);
要处理那些未命名的异常时,你可以将一个名字和一个错误号联系在一起,达到这个目的的语句是pragma exception_init语句;
抛出异常:
a、通过pl/sql运行时引擎
当数据库或pl/sql在运行时发生错误时,一个异常被pl/sql运行时引擎自动抛出
b、使用raise语句
异常也可以通过raise语句抛出:raise exception_name;
c、调用raise_application_error存储过程
处理异常&异常传播:
a、一旦程序进入异常部分就不能再回到同一块的执行部分;当异常被处理后,控制返回到外层执行部分的下一条语句;
b、如果有when others子串,则必须放置在最后面作为缺省处理器处理没有显式处理的异常;
c、执行部分抛出的异常将首先传递到同一块的异常部分,如果在同一块的异常部分没有处理这个异常的处理器,那么异常将会传播到上一层的异常部分中,一直到最外层;
d、异常被处理后如果你仍然希望它继续传播,可以在异常处理的最后执行不带参数的raise语句(raise语句将重新抛出出现的异常,允许它继续传播);
--这是一个上面部分知识点的示例(伪代码)说明
declare
...
user_define_exception exception; --用户定义异常
invalid_column_name exception;
--补充说明:如果我们在程序块中使用了无效列名,会有括号中的错误提示(ORA-00904:invalid column name)
--下面我们将这个异常代码号与我们自定义的异常进行关联,即为异常命名
pragma exception_init(invalid_column_name,-904);
begin
...
--raise user_define_exception; --可以显式引发异常
exception
when TOO_MANY_ROWS then --预定义异常处理
...;
when user_define_exception then --用户定义异常处理
...;
when invalid_column_name then --PRAGMA EXCEPTION_INIT异常处理
...;
raise; --继续传播该异常
end;
sqlcode和sqlerrm:
a、另外一种处理数据库错误的方法是使用内建函数sqlcode和sqlerrm;
b、sqlcode将返回现行数据库错误号,这些错误号中除了no_data_found是+100外其他都是负数;
c、sqlerrm返回文本描述的错误信息;
d、为了获得用户自定义异常返回的sqlerrm和sqlcode,你需要使用raise_application_error函数给自定义异常标注错误号
给自定义错误标注号码:
a、raise_application_error内建函数用于抛出一个异常并给异常赋予一个错误号以及错误信息;
b、自定义异常的缺省错误号是+1,缺省信息是user_defined_exception。来自未处理的异常的一般信息对于识别导致错误的原因没有帮助,
c、raise_application_error函数能够在pl/sql程序块的执行部分和异常部分调用,显式抛出带特殊错误号的命名异常;
d、使用语法:raise_application_error(error_no,error_message[,{true|| false}]);
e、错误号的范围是-20,001到-20,999;错误信息是文本字符串,最多为2048字节;true和false表示是添加(true)进错误堆(error stack)还是覆盖(overwrite)错误堆(false)。缺省情况下是false。
数据库
2019-09-16 15:49:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
摘要
最近发现服务器磁盘空间不足,发现是数据库占用空间太大了,决定删一部分数据,释放一些空间。结果,发现删除数据之后,linux磁盘空间并没有释放,opimize 之后,依然没有释放。各种折腾之后才搞清楚,建立数据库的时候,使用的表空间是默认表空间,也就是所有的数据都会写入到 ibdata1中,这种情况删除表内容的数据并optimize是没有作用的,必须是单表独立文件才能优化空。
最后解决方法,备份所有表数据,删除1bdata1文件(这中间也做了一些尝试,差点数据库起不来了),设置 单表独立空间参数(innodb_file_per_table=1),重新导入数据,这之后,每一个表都是一个独立的文件,删除数据之后是可以用optimize优化的
顺带提一下分区表,也就是按照一定的逻辑分表规则(没有命中规则的插入会失败),将数据落入到不同的数据文件中,这样加载数据的时候可能会效率高一些,但是受一些索引文件大小的影响,分区表还是不能解决大数据问题。
以后有空把这篇博客补充完整。
经过
忽略_bak,原路径是 /var/lib/mysql
innodb_file_per_table=1
数据库
2019-09-12 11:05:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
应用系统的更新发布在企业日常运维中是一件比较常见工作,有文件的备份替换,也会有数据库方面的更新操作。本文介绍如何通过一台装有sqlplus工具的中转机对不同应用的Oracle数据库进行自动化发布。

经常遇到使用PL/SQL图形化工具对Oracle数据库进行相关的更新操作,例如程序包、触发器、存储过程、视图以及表中的数据。如果是单用户对单台数据库更新少量的内容,这个操作还可以接受,如果数据库服务器比较多,并且要切换不同的用户去更新大量的内容,这种方式就会比较低效繁琐,面对这种场景,实现后台自动化更新就很有必要。

如何通过一台sqlplus中转机对不同的Oracle数据库在后台完成更新示意如下:
规范要求
按照上图示意在后台完成数据库的发布更新需要一定的规范支持,简单来讲就是要让sqlplus工具能很明确的获取到以下几点信息:
更新时用什么账号去连接哪台服务器的哪个实例? 账号对应的密码应该如何传递给sqlplus命令? 更新的是程序包、存储过程还是视图? 多个更新之间是否有前后依赖关系?
更新文件的命名规范
通过sqlplus命令去连接Oracle数据库完成一次更新需要账号、密码、Oracle服务器的IP地址、侦听的端口、实例名、更新文件所在的路径,用法如下:
其中账号、IP地址、端口、实例名属于可公开的信息,这一部分内容可以连同更新次序和更新类型一起组织到文件名称中

例如【次序_账号_IP_实例名_XXX.类型】,示例:
01_admin_1.1.1.1_insA_package.pck
密码和相关信息储存
通过sqlplus进行数据库更新时用到的相关信息在本例中是储存在sqlplus中转机上,只有root账号可以获取到,并且密码是以密文的形式储存,密码的密文储存在以下路径的文件中(文件名是账号的密文): .XXX/.IP地址/.账号的密文 ,数据库实例名和端口号储存在以下路径中的文件中: .YYY/.IP地址/.实例名 ,整个路径只有root账号可以访问。示例如下:

发布过程
更新文件相关规范已经确定好,发布过程可以使用编写好的脚本(shell或bat等)轮询执行要更新的文件,从sqlplus中转机依次将内容更新到对应的Oracle服务器。

本例结合 嘉为蓝鲸应用发布 这款基于蓝鲸平台的SaaS将数据库发布过程流程化,调用发布脚本实现定时或实时更新,具体情况见下图:

无需逐个登录Oracle服务器,来回切换不同账号,更新过程中的日志如下图所示:


其它说明
本例通过sqlplus发布Oracle更新,需要有以下几个前提: 账号和密码信息要提前录入到sqlplus中转机(务必确保账号安全) Oracle服务器相关信息(IP、端口、实例名)也需要提前录入到sqlplus中转机 更新文件中的语法和符号必须规范(例如行尾的“;”号,包头包体结束位置的“/”)等等 数据库、sqlplus客户端的编码尽量保持一致。

作者: 徐晗

文章到此结束了~
想到中秋即将来临啦!有没有很期待呀~
在此先预祝大家 中秋节快乐 !

佳节好文
业务复杂、数据庞大、应用广怎办?了解下分布式事务的解决思路!
赣州银行增强科技创新,实现一键灾备切换
SaaS设计:自动化服务启停设计示例
这里有份选择云服务商的攻略,请查收…
如何设计大型集团一体化IT运维系统
数据库
2019-09-10 17:42:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
create table test(id number);
create table audit_table(table_name varchar2(20),ins int,upd int,del int);
create or replace trigger test_tri
after insert or update or delete on test
declare
v_count int;
begin
select count(*) into v_count from audit_table where table_name='TEST';
if v_count=0 then
insert into audit_table values( 'TEST',0,0,0);
end if;
case
when insertin then
update test set ins=ins+1 where table_name='TEST';
when updating then
udpate test set upd=upd+1 where table_name='TEST';
when deleting then
update test set del=del+1 where table_name='TEST';
end case;
end;
begin
for i in 1..100 loop
insert into test values(i);
end loop;
end;
select * from audit_table;
table_name ins upd del

TEST 1 0 0
=====
alter trigger as:
=====
create or replace trigger test_tri
after insert or update or delete on test
for each row
declare
v_count int;
begin
select count(*) into v_count from audit_table where table_name='TEST';
if v_count=0 then
insert into audit_table values( 'TEST',0,0,0);
end if;
case
when insertin then
update test set ins=ins+1 where table_name='TEST';
when updating then
udpate test set upd=upd+1 where table_name='TEST';
when deleting then
update test set del=del+1 where table_name='TEST';
end case;
end;
truncate audit_table;
table has been truncated.
truncate test;
table has been truncated.
begin
for i in 1..100 loop
insert into test values(i);
end loop;
end;
select * from audit_table;
table_name ins upd del

TEST 100 0 0
数据库
2019-09-05 16:34:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>> SELECT * FROM 表名 t WHERE IF (t.年龄>18,t.性别='M',1=1) AND t.性别='F'
数据库
2019-08-30 11:34:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
在现实的开发中,我们很少用到让MySQL自己生成uuid,因为在高并发场景下,这是不被允许的。通常是在代码中生成id,或者是使用专业的id服务器( Twitter-Snowflake )。我们要讨论的内容是,当我们在手动输入元数据的时候,如果恰巧有一列uuid,我们不想手动输入,而是希望MySQL为我们自动创建,那么我们可以使用触发器。 DELIMITER ;; CREATE TRIGGER `foo_before_insert` BEFORE INSERT ON `foo` FOR EACH ROW BEGIN IF new.id IS NULL THEN SET new.id = uuid(); END IF; END;; DELIMITER ;
最后,希望您谨慎地使用触发器、视图、外键等等,它们都会影响MySQL的性能。 本文的sql来自: MySQL set default id UUID
数据库
2019-08-30 10:37:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
8月28日,腾讯云数据库在京正式启动战略升级,宣布未来将聚焦云原生、自治、超融合三大战略方向,以用户为中心,联接未来。并在现场面向全球用户同步发布五大战略级新品,包括数据库智能管家DBbrain、云数据库TBase、数据库备份服务DBS、云数据库Redis混合存储版,以及自研云原生数据库CynosDB商业化版本。
战略聚焦三大主航道,推进百万企业全面上云
作为企业IT的核心系统之一,数据库市场正在面临根本性变革。在全面上云的大趋势下,传统的数据库上云模式逐渐无法满足客户业务的快速扩展和智能运维需求。客户需要的是一套能够灵活扩展、智能诊断,支持跨云融合的新一代云端原生数据库系统,未来数据库发展方向将从“数据库+云”模式全面转向“云+数据库”模式。
基于这样的判断,腾讯云数据库的未来将战略聚焦云原生、自治、超融合三大主航道,完善自身产品矩阵。让融合了云与AI能力的数据库变得更智能、稳定、可靠、安全、便宜,推进百万企业全面“上云”进程。
腾讯云数据库产品总监王义成表示:“全球数据库的发展即将进入下一战场,以腾讯云为代表的云厂商,基于在云计算、大数据以及人工智能领域的规模和技术积累,在这一轮技术变革中将具备众多先天优势。有鉴于此,腾讯云数据库充分整合多年服务各行各业的成功经验,推出多款能够真正满足用户需求的数据库产品,助力企业快速数字化、智能化转型。”
重磅发布五大战略级新品,布局未来数据库场景
云原生赛道,腾讯云宣布自研新一代企业级分布式数据库CynosDB正式商业化。不同于过去云数据库在传统数据库之上扩展一部分云的能力,CynosDB是纯粹的云原生数据库,融合了传统数据库、云计算和新硬件的优势,支持海量存储、百万级查询和秒级的故障恢复,100%兼容MySQL和PostgreSQL,提供更智能的运维管理和更可靠的安全保障。与高性能形成对比的是,CynosDB价格仅为市面上商业数据库的1/15,极大降低了企业“上云”成本。
数据库自治领域,腾讯云结合前沿的人工智能技术推出了数据库智能管家DBbrain。它将大量数据库问题的诊断优化工作自动化、智能化和可视化,利用机器学习、大数据技术快速学习资深数据库管理员的成熟经验,服务于云上和云下企业,构建一套“会说话”的数据库服务系统。作为一款数据库智能诊断和优化产品,DBbrain为用户提供实时的性能诊断和安全防护,高效的帮助用户定位故障原因、优化建议、协助用户从源头进行预防,并通过AI调参能力,提升数据库整体性能。
超融合领域,腾讯云发布云数据库TBase及数据库备份服务DBS,并启动了腾讯云Redis混合存储版邀约测试。
详细来说,TBase是腾讯自主研发的分布式国产数据库,提供领先的HTAP能力,在提供NewSQL便利性的同时完整支持分布式事务并保持SQL兼容性,支持RR、RC、SSI三种隔离级别,同时兼容Oracle语法。对于日益多元化的企业客户,TBase满足了他们对业务融合、场景融合、管理融合的更高诉求。强大的安全和容灾能力,让TBase已经成功应用在腾讯内部的微信支付,以及外部众多金融、政府、电信、医疗等行业的核心业务系统。
数据库备份DBS拥有一套完整的数据备份和数据恢复解决方案,具备实时备份和秒级的数据恢复能力,可以为公有云及混合云环境下的用户数据库提供强有力的容灾服务。该产品的推出,有望让“光缆挖断”导致业务停摆成为历史,最大程度的降低非可控因素比如地震、火灾、海啸、甚至挖掘机等对业务造成的影响。
云数据库Redis混合存储版本是腾讯云自主研发的,100%兼容Redis协议和数据结构,具备自动降冷能力的版本。产品特性上,Redis混合存储版支持所有数据存储到磁盘,热数据自动缓存,冷数据自动落盘,能够显著降低客户运营成本,提高研发效率。目前,该产品最大提供36TB存储规格。
产品家族进一步丰富,增速连续两年全球前三
随着战略定位的进一步聚焦以及众多新产品的推出,腾讯云数据库产品家族将进一步丰富,企业上云不仅更为方便,成本也会越来越低。截止目前,腾讯云数据库服务涵盖总计20多种数据库服务。同时,还在以每年发布5大产品、50多个新功能的速度递增。
腾讯云数据库产品线涵盖了业内主流的数据库产品,包括开源数据库MySQL、MariaDB、MongoDB、Redis;商业数据库Oracle、SQL Server;自研数据库TDSQL、TBase以及云原生数据库CynosDB。同时,随着5G时代的到来,针对物联网、大数据等海量时序数据的场景,腾讯云推出时序数据库CTSDB。除此之外,腾讯云也已经启动图数据库等新型数据库领域的开发工作。
今年以来,腾讯云数据库先后获得Forrester以及Gartner等权威机构的一致认可。在6月份,腾讯云数据库被Forrester评为全球数据库领域 “实力竞争者” ,同时,Gartner报告也显示,2018年腾讯云数据库市场份额增速达123%,位列国内所有数据库厂商之首,在全球范围内保持了连续两年增速前三的迅猛势头。
未来,腾讯云数据库将深耕云原生、自治、超融合三大战场,以 “云+数据库” 新模式助力产业腾飞,为各行业用户数字化转型助力!
数据库
2019-08-29 10:51:00
【围观】麒麟芯片遭打压成绝版,华为亿元投入又砸向了哪里?>>>
数据库的高可用是指最大程度地为用户提供服务,避免服务器宕机等故障带来的服务中断。数据库的高可用性不仅仅体现在数据库能否持续提供服务,而且也体现在能否保证数据的一致性。
SequoiaDB 巨杉数据库作为一款100%兼容 MySQL 的国产开源分布式数据库,它在高可用方面的表现如何?它的高可用性是如何实现的?本文将详细描述SequoiaDB巨杉数据库的高可用性原理,并进行测试验证。

01 巨杉分布式集群架构
SequoiaDB 巨杉数据库采用计算与存储分离架构,SequoiaSQL-MySQL 是 SQL 计算层,存储层由协调节点、编目节点和数据节点组成。
图1 SequoiaDB分布式架构
如图1所示是最简单的 SequoiaDB 分布式数据库集群架构图,由1个协调节点,1个编目节点,3个数据节点和 SequoiaSQL-MySQL 构成。其中数据节点在三个服务器上,包括三个数据复制组1、2、3,每个数据复制组由3个完全相同的数据副本组成,数据副本之间通过日志同步保持数据一致。
A, A1, A2组成数据复制组1,三者是完全相同数据副本。数据复制组2、3类似于数据复制组1。在 SequoiaDB 分布式集群中,每个复制组最多支持 7 个数据副本。
本文的高可用测试环境采用图1所示的分布式集群架构,其中主节点有读写功能,两个备副本可以执行读操作或备份。
02 巨杉数据库高可用实现
SequoiaDB 高可用采用 Raft 算法实现,多副本之间通过日志同步保持数据一致性。
图2 三个节点之间保持连接

如图2所示,SequoiaDB 集群三个副本之间通过心跳保持连接。
数据组副本之间通过共享心跳信息 sharing-beat 进行状态共享。如图3所示,sharing-beat 心跳信息结构包括心跳 ID、自身开始LSN、自身终止LSN、时间戳、数据组版本号、自身当前的角色和同步状态。
图3 心跳状态信息结构

每个节点都维护一张 status-sharing table 表,用来记录节点状态。sharing-beat 每2秒发送一次,采集应答信息,若连续N秒未收到应答信息,则认为节点宕机。

集群中只有一个节点作为主节点,其他节点为备节点。如果出现多主或者双主,需要根据 LSN 对比进行降备,保证集群中只有一个主节点。
Note:
1)当主节点宕机时,需要从备节点中选举出一个新的节点作为新的主节点。
2)当备节点宕机时,主节点不受影响,等备节点恢复后,通过日志同步继续与主节点保持数据一致即可。

下面介绍当主节点宕机时,选举新主节点的过程。
选举条件
满足下面2个条件可以被选举成为主节点:
1. 多数备节点投票通过
2. 该节点LSN最大

选举过程
1)当主节点A宕机时,A自动降为备节点,关闭协调节点的业务连接。
图4 集群中主节点挂掉

2)A1和A2都会判断自身是否具备升为主节点的条件,若符合即发起选举请求。
条件内容:
自己不是主节点
剩下的备节点占半数以上
自己的LSN比其它备节点的LSN新
3)其它备节点会把被投票节点的 LSN 与自己的 LSN 做对比,若比自己的 LSN 新,则投赞成票,否则投反对票。
4)若赞成票超过(n/2+1),则支持该节点为主节点,选举成功。否则保持备节点角色,选举失败。
5)选举成功后,通过心跳状态信息共享数据组信息给其它节点。

03 高可用容灾验证
一般分布式数据库 POC 测试包含功能测试、性能测试、分布式事务测试、高可用容灾测试和兼容性测试等。下面将对 SequoiaDB 巨杉数据库的高可用性进行验证测试。
测试环境说明

本文测试环境采用分布式集群,包含1个 SequoiaSQL-MySQL,3个数据节点,1个编目节点,1个协调节点,搭建集群方式具体可参考巨杉官网虚拟机镜像搭建教程。在 kill 掉主节点进程之后,我们对分布式数据库集群进行读写操作,来验证高可用性。

查看服务器集群状态
# service sdbcm status
.....
Main PID: 803 (sdbcm)
Tasks: 205 (limit: 2319)
CGroup: /system.slice/sdbcm.service
├─ 779 sdbcmd
├─ 803 sdbcm(11790)
├─1166 sequoiadb(11840) D
├─1169 sequoiadb(11810) S
├─1172 sequoiadb(11830) D
├─1175 sdbom(11780)
├─1178 sequoiadb(11820) D
├─1181 sequoiadb(11800) C
1369 /opt/sequoiadb/plugins/SequoiaSQL/bin/../../../java/jdk/bin/java -jar /opt/sequoiadb/plugins/SequoiaSQL
.....

SequoiaDB 分布式集群中数据节点端口在11820,11830,11840;编目节点11800,协调节点在11810
sdbadmin @sequoiadb :~$ ps -ef|grep sequoiadb
sdbadmin 1166 1 0 Aug20 ? 00:02:23 sequoiadb(11840) D
sdbadmin 1169 1 0 Aug20 ? 00:01:43 sequoiadb(11810) S
sdbadmin 1172 1 0 Aug20 ? 00:02:24 sequoiadb(11830) D
sdbadmin 1178 1 0 Aug20 ? 00:02:33 sequoiadb(11820) D
sdbadmin 1181 1 0 Aug20 ? 00:04:01 sequoiadb(11800) C
kill 掉11820的主节点,执行查询和写入sql
sdbadmin @sequoiadb :~$ kill 1178
sdbadmin @sequoiadb :~$ ps -ef|grep sequoiadb
sdbadmin 1166 1 0 Aug20 ? 00:02:24 sequoiadb(11840) D
sdbadmin 1169 1 0 Aug20 ? 00:01:43 sequoiadb(11810) S
sdbadmin 1172 1 0 Aug20 ? 00:02:24 sequoiadb(11830) D
sdbadmin 1181 1 0 Aug20 ? 00:04:01 sequoiadb(11800) C
sdbadmin 1369 1 0 Aug20 ? 00:01:33 /opt/sequoiadb
....
执行查看 sql,查看插入操作之前数据为121
mysql> select * from news.user_info;
+------+-----------+
| id | unickname |
+------+-----------+
| 1 | test1 |
........
| 119 | test119 |
| 120 | test120 |
| 121 | test121 |
+------+-----------+
121 rows in set (0.01 sec)
执行写入 sql,查看插入是否成功
mysql> insert into news.user_info(id,unickname)values(122,"s
uccess");
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from news.user_info;
+------+-----------+
| id | unickname |
+------+-----------+
| 1 | test1 |
.........
| 120 | test120 |
| 121 | test121 |
| 122 | success |
+------+-----------+
122 rows in set (0.00 sec)
数据(122, “success”)数据插入成功,在其中一个主节点挂掉情况下,读写都没有受到影响,数据读写保持一致,高可用性得到验证。

现在执行导入1000w数据写入脚本 imprt.sh,在执行过程中 kill 掉主数据节点,模拟主节点故障场景,在巨杉数据库图形化监控界面 SAC 上查看集群读写变化。
Note:
如果需要获取 imprt.sh 脚本,关注“巨杉数据库”公众号回复 “imprt” 即可获取。

执行导入数据脚本
./imprt.sh 协调节点主机 协调节点端⼝ 次数
./imprt.sh 192.168.1.122 11810 100
如图5所示,在执行导入数据时刻,kill 掉主数据节点,insert 写入下降,之后集群恢复高可用
图5 SAC监控界面集群读写变化示意图
图6 SAC查看tpcc写入数据量示意图
从 SAC 可视化界面中可以看到,当主数据节点在我们执行插入1000w数据操作的过程中出现故障,数据读写受到影响的持续时间很短。最后通过使用 imprt.sh 脚本重新导入插入失败的数据,则可以保证数据最终一致性。

04 总结
SequoiaDB 分布式集群具备较好的高可用性,集群可以设置多个数据复制组,每个数据复制组由多个完全相同的副本构成,副本之间通过 Raft 算法和日志同步方式保持数据一致性。最后,本文也验证了在执行千万级数据写操作时,若集群主数据节点宕机,分布式集群可以正常读写数据,并且数据保持最终一致,高可用性得到验证。
数据库
2019-08-28 11:55:00