一些感悟

  1. 关于工作,从理想的一面讲,工作不是(至少不应该),被消极解读为单纯的拿时间换钱。用人生最宝贵的几十年换取银行卡账户的数字,我认为这是不等价的。工作是我们在这个社会里挑选的众多活动中的一项,倾注很长的时间,收获相应的经历。所以有理由挑选一个会让自己感到有趣的工作,并且坚持下去。
  2. 坚持很重要。我去年在练习1500米自由泳,这大概是我干过最折磨的事情之一。1000米之前你会无数次打算放弃,但是我发现一旦掌握了相应技巧并且越过了疲劳线,你会感受到前所未有的乐趣和力量。工作中也是这样。
  3. 好的方向能够给人带来更大的发展空间与个人成就,但是有时候我会因此忘记,比找准大方向更重要的事情是:干好手里的事,赢得别人的信任。
  4. 工作里面会遇到一些痛苦,但是有些痛苦的背后是一把梯子,一旦你越过了这些痛苦你就会看到一个全新的世界。痛苦越强烈,成功后的幸福感就越强烈。我们在聊那些有趣的事情的时候,总是伴随着汗水或泪水的。
  5. 工作中会遇到一些很棒的人,我们会发现他们有一些闪光的品质/能力,可能不能立刻发现,而是在未来某个场景下想起这些闪光点。但是那些闪光点深深的影响了我,我也努力成为拥有这些闪光点的人。当然,有时候你会跟其中某些人成为朋友。
  6. 职场里面很多人会鼓吹年龄焦虑,我甚至都懒得谈这个话题。不是所有人都需要去承担畸形的价值观带来的压力。类似的还有买房的焦虑与结婚生子的焦虑。
  7. 没有必要过分在工作中追求名利,掌握好度,不要丢失乐趣。
  8. 做好一份工作,本质上和学好一门课,做好一顿饭,踢好一场球并没有太大区别,它们都是做好一件事。而做好一件事的基本逻辑,或者说背后的方法论都是相似的。触类旁通,换句话说,我做好的每一件事,都在从某一方面帮助我成长。用这种心态去对待工作,即不会有太大的压力,也不会有所畏惧。
发表在 闲扯淡 | 2,471条评论

聊聊数据密集应用实现的一些关键问题

Mysql是基于二维表的关系型数据库,DynamoDB是分布式的KV数据库,ElasticSearch是基于倒排索引的分布式搜索引擎,Doris是mpp数据仓库。。。

近十年是基础软件飞速发展,各种数据库,中间件层出不穷;数据量爆炸的今天,为了满足海量存储,各种数据密集型应用也在向着分布式的方向发展。

从上层看,有哪些技术是可以被总结归纳的?有哪些模式是值得被借鉴的?

今天等着面试也没啥事儿,写篇文章归纳归纳我的看法。

可以从一个老生常谈的问题开始:SQL vs NoSQL。

如果从数据模型的角度讲,SQL对应的就是关系模型,多数NoSQL则是KV模型/文档模型。这两种数据模型从设计出发点是不同的,SQL强调表与表之间数据的关联,可以方便的支持连接查询。而文档模型则淡化这种关系,一条数据只是映射到一个key上,这种设计虽然不方便连接,但是读写性能优异,非常适合分布式的分片模式。

数据模型在具体存储时,也存在不同的选型方案:一种是基于B-Tree为索引,利用磁盘块/页进行数据存储;一种是基于LSM-Tree(SSTables)的日志结构存储。

前者典型代表就是MySQL,数据持久化在磁盘块上,通过B-Tree进行索引,增删改查请求都会通过索引定位到具体的磁盘块上。显然因为有B-Tree索引,查询效率相对较高,但是因为写入是离散的,所以写入效率较低。

后者的代表譬如ElasticSearch, 一个时间段内写入的数据,会在内存中加工成有特定顺序且具备索引的数据结构,并在某个时间点以顺序写的方式将这些数据落盘。整个数据集合由很多这样的段文件构成,段文件通常是不可修改的。因为是顺序写入,所以写入效率非常高,但是查询因为要牵扯很多的段文件,相对来说效率较低。

SQL和NoSQL不同的数据模型,对于底层存储结构的实现有具体的要求吗?我认为是没有的,只是自从受到Google的BigTable论文启发之后,LSM-Tree的这种方式受到了越来越多新兴的NoSQL数据库的青睐。

另外一个值得提及的是OLAP数据库(或者叫数据仓库),它更多的是倾向用于海量数据的快速分析,执行的更多是各种复杂的聚合查询,所以跟普通的数据库会有一些技术上的差异,例如会针对MPP做一些优化设计,也会采用列存储等一些方式加快查询速度。

上面的这些讨论都是针对于单机,在引入分布式后,就可以涉及数据冗余以及分片的概念了。

首先是数据的冗余,为了保证数据高可用,通常会将数据进行冗余存储,并且使存储在不同的服务器/机架/机房/地区;数据的冗余最常用的方式就是主从复制。主节点接收到数据进行处理存储,并分发到到从节点。这种分发可以是直接利用同步网络请求,也可以是异步的,譬如MySQL通过binlog进行主从的同步。

数据的分片则是为了支持更大的数据量。一个数据集增加到一定程度之后就可能超出单机的承载能力,譬如MySQL单表支持1000万以上的数据,效率就会下降。那么就可以对大数据集进行分片,然后在分布式系统上进行存储。

分片往往会给系统引入更多的复杂性,包括路由,数据/流量的均衡,以及跨分区的联合查询。但是它的扩缩容能力能够在某些场景下发挥强大的作用,承载非分布式数据系统无法想象的数据量。所以只要控制好应用场景,它能带来巨大的好处。

另外一个话题就是事务还有一致性。

单机的事务和分布式事务是有区别的。单机的事务,拿MySQL举例,原子性和一致性通过undo日志保证,隔离性通过锁/MVCC保证,持久性通过redo日志保证。

分布式事务更复杂一点,多的是借助2PC,TCC等机制保证。从某个角度上讲,分布式事务实现更类似于分布式一致性/共识的某些实现。

分布式一致性的问题从宏观上包括了主从副本的数据一致性问题,以及共识问题。数据的一致性可以从主从复制角度分析,共识问题则更复杂一点。

什么是共识问题?分布式系统需要在谁是主,谁是从,现在集群状态是什么 等问题上达成一致,这就是共识问题。

共识问题可以采用中心节点的算法,例如2PC算法,将这种共识问题的处理放到一个中心节点(协调节点)上去进行,因为是单节点,所以不存在不一致;等中心节点处理完之后在分发到不同的节点上;但是这样的缺点是存在single point of error。

另外一个就是采用去中心的分布式共识算法,paxos,bully,raft,zab等等,节点之间通过共识算法,自己协商,最终得到一个一致性的结果。

当然我们为了实现分布式的共识,可以借助一些服务,譬如ZK,etcd等等,而没有必要自己来实现。

发表在 数据库 | 4条评论

云化ES的一些retrospect

今天闲着没事儿,坐这里回顾一下云化ES服务时需要注意的一些点,想到哪儿写到哪儿。

概述

产品形态

在云上提供ES的托管服务,产品的呈现状态显然就是一个ES集群,由于ES天然支持节点角色的划分,所有申请的ES集群也可能存在不同的角色,例如专用主节点,专用协调节点,专用数据节点等等;同时由于Kibana和ES的紧密关系,也可以选择性为用户提供一个Kibana节点。

同样用户申请的集群里面,也可能会包括LB,管理节点等一些用户不可见的资源。

用户可以通过UI去实现对ES集群的管理,创建,编配,扩缩容,执行ES的一些功能例如创建模版创建快照等等;同时也可以通过LB对外暴露的域名/IP通过Client对ES集群进行访问

架构思路

ES集群的编排管理可以基于一个云原生容器管理平台,例如k8s;容器管理平台之上是管理层,直接处理UI的RPC指令,负责通过任务流去控制ES集群的创建编配等流程以及代理用户通过UI对ES集群的一些管理查询指令。

如果采用k8s,还可以把“通过任务流去控制ES集群的创建编配等流程”下沉到ES Operator中,那么控制层就只是用于收集展现ES集群的信息,以及代理用户通过UI对ES集群的一些管理查询指令。

同时对于不同产品线后端控制层的共有功能模块,应该抽离成一个商业中台,例如订单管理,用户认证,支付信息等等

ES的控制层就变得更薄了。

一些关键点

代理设计

跟好几家公司在聊的时候发现这个似乎是业界的通用方案,在处于某个VPC内的集群内部设立一个代理(agent)节点,或者叫管理节点,负责与外部管理层的通信。外部管理层通过这个节点对业务集群进行管理。

这里需要额外设计的是如何跨vpc访问agent节点,通常这个是需要从云原生容器管理平台去解决的一个强需求。在ES中类似情况的还有kibana节点。

同时在每个业务节点上也需要添加相应的agent接口,以便在原生支持的接口上丰富更多的运维/功能接口。

与云原生平台整合

云原生平台会提供一些功能,来保障高可用,故障转移等功能,需要在设计过程中考虑到。例如,通过一些探针,来检查启动容器/业务的健康状况;在容器上通过标签来指定需要收集的日志的位置,平台会进行收集。。。等等,这也是需要考虑的点。

镜像构建与管理

考虑到开源产品自身的多版本,以及我们的代码的不断迭代,如何去构建镜像,管理版本,同样是一个非常复杂的问题,有一些需要关注的点:

  • 跟业务相关的节点一定要与不相关的功能解耦,譬如ES的数据节点,尽量只有ES的原生功能。因为对额外功能的改动产生的升级过程,会对业务带来直接的影响,应该尽量避免这种升级操作。把可能需要频繁修改的功能,代码,都放到对用户不可见的节点上。
  • 构建过程可以利用dockerfile,make,sh脚本等等多个工具,但是注意构建过程要清晰易懂,最后构建的结果一定要精简。例如dockerfile应采用两阶段构建,避免构建阶段的依赖进入最后的部署镜像中。
  • 原生镜像版本的管理也是一个复杂的问题,ES的版本多如牛毛,我们不要去重复打版本,或者重复存储,如何通过版本号动态获取,是我们设计的一个难点。

创建过程

创建是最基本的一个过程,根据用户对集群配置的设定,启动一个对应的集群。

创建过程可以抽象成一个任务流,假如拆分成基于k8s的人工操作,就类似于:

  1. 在DB中插入新集群信息
  2. 填充agent节点相关配置信息到相关chart中,并进行install操作启动容器
  3. 填充master节点相关配置信息到相关chart中,并进行install操作启动容器
  4. 填充协调节点相关配置信息到相关chart中,并进行install操作启动容器
  5. 填充数据节点相关配置信息到相关chart中,并进行install操作启动容器
  6. 填充kibana节点相关配置信息到相关chart中,并进行install操作启动容器
  7. 配置网络资源,各种探针健康检查,日志收集等等
  8. 更新数据库信息,检查集群健康状态正常并返回

这个任务流可以通过代码实现自动化,但是需要注意其中的故障处理。

变配与扩缩容

这个过程可以细分到很多情况:

  • 主节点,协调节点,数据节点的变配与扩缩容
  • 节点计算资源/存储(硬盘)资源的扩缩容
  • 节点垂直/水平方向的扩缩容

其中任意组合,会产生很多中情况,每种情况都需要具体情况具体分析

但是核心思想是不能够停服务

场景一数据节点垂直变配

假设所有的数据节点配置从4C8G变配为8C16G,应该怎么设计这个变配过程?反之呢?

垂直扩缩容的过程相对简单,因为也不涉及到ES的数据的重新分配,可以直接利用云平台的更新容器规格,并进行滚动重启。需要注意的是,首先需要暂停ES分片的自动分配过程。

另外,在滚动重启过程中,可能出现业务受到影响的情况,是否可以通过蓝绿部署来解决这个问题呢?我认为在ES这种计算存储不分离的情况下比较难,因为一旦牵扯到数据迁移,就变得更复杂了。

场景二数据节点水平变配

假设所有的数据节点数量从4变配为8,应该怎么设计这个变配过程?反之呢

水平扩容的过程很好理解:

  1. 同样,首先需要暂停ES分片的自动分配过程
  2. 将新扩的容器调用云原生平台创建出来
  3. 更新旧的节点的配置,将新的容器添加到路由表中,因为修改了配置所以还需要进行滚动重启
  4. 重新开启ES分片的自动分配过程,ES能够对外提供服务

水平缩容过程:

我之前工作的公司的共有云,并不支持数据节点的水平缩容,这个过程相对复杂,但是也可以大致分解成如下流程

  1. 首先需要暂停ES分片的自动分配过程
  2. 确定要缩容的容器编号,以及保留的容器编号
  3. 调用ES的分片重新分配接口,依次将需要缩容的上的分片移动走;这一步相对复杂,需要考虑多种情况,例如有分片无法分配导致集群状态变黄,或者主分片的移动导致业务受到影响等等
  4. 更新节点路由配置,滚动重启
  5. 打开ES分片的自动分配过程

场景三数据节点存储变配

假设所有数据节点的磁盘规格发生更改?(假设我们使用的网络存储云盘)

这个问题比较复杂,最理想的情况是,云盘这个云服务本身支持动态扩缩容,即,扩容过程中,不会影响我当前读写,并且数据不会丢失,那么只需要选择修改容器的挂载规格即可,甚至不需要重启;也就是将复杂度转移到了云盘这个云服务本身。

如果不具备这种形式的云盘,那就比较蛋疼了,需要迁移数据,重新挂载,再重新迁移回数据,这个过程就会复杂很多,也会影响业务的使用。

还有很多其他过程,但是分析的思路是类似的,不再赘述了

总结

写了几个小时,回忆了一下自己在做ES云化过程中遇到的一些问题,其中很多细节已经记不住了,但是大体思路是对的,别的有状态分布式系统也基本上会遇到类似的问题。以此作为一个总结吧。

发表在 ElasticSearch, 云原生 | 106条评论

关于Kubernetes网络

一些基础概念

CNI

K8s网络遵循CNI规范:基于插件的容器网络解决方案

CNI是Container Network Interface的是一个标准的,通用的接口。

现在容器平台:docker,kubernetes,mesos 等

容器网络解决方案:flannel,calico,weave 等

只要提供一个标准的接口,就能为同样满足该协议的所有容器平台提供网络功能,而CNI正是这样的一个标准接口协议。

NAT

NAT(Network Address Translation),是指网络地址转换,1994年提出的。

当在专用网内部的一些主机本来已经分配到了本地IP地址(即仅在本专用网内使用的专用地址),但又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。

这种方法需要在专用网(私网IP)连接到因特网(公网IP)的路由器上安装NAT软件。装有NAT软件的路由器叫做NAT路由器,它至少有一个有效的外部全球IP地址(公网IP地址)。这样,所有使用本地地址(私网IP地址)的主机在和外界通信时,都要在NAT路由器上将其本地地址转换成全球IP地址,才能和因特网连接。

另外,这种通过使用少量的全球IP地址(公网IP地址)代表较多的私有IP地址的方式,将有助于减缓可用的IP地址空间的枯竭。在RFC 2663中有对NAT的说明。

K8S对网络的约束

提供一个扁平化的网络模型

K8S没有提供网络的具体实现,只是提供了以下约束:

  • 所有pod可以与其他pod直接通信,无需显式使用NAT
  • 所有node可以与所有pod直接通信,无需显式使用NAT
  • Pod可见的IP地址确为其他Pod与其通信时所使用,无需显式转换

换个说法,k8s的网络遵循以下原则:

  • 一个节点的pod和其他节点的pod通信不需要通过做网络地址转换(NAT)
  • 一个节点上所有的agent控制程序(如deamon和kubelet)可以和这个节点上的pod通信
  • 节点主机网络中的Pod可以与其他所有节点上的所有Pod通信,而无需NAT

基本上的原则就是,k8s的里面的pod可以自由的和集群里面的任何其他pod通信(即使他们是部署在不同的宿主机),而且pod直接的通信是直接使用pod自己的ip来通信,他们不知道宿主机的ip,所以,对于pod之间来说,宿主机的网络信息是透明的,好像不存在一样。

基本场景

  • 容器间通信
  • pod间通信
  • pod与service通信
  • External 与 service通信

Network Namespace

每个pod都有独立的Network Namespace,其中的containers共享这个空间

Network Policy

Network policy提供了基于策略的网络控制,用于隔离应用并减少攻击面。

Pod之间的隔离性。

容器网络实现方案

  • Flannel
  • Calico
  • Canal
  • Cilium
  • Kube-router
  • Romana
  • Weave

Flannel的实现方案

K8s Pod的网络创建流程

  • 每个Pod除了创建时指定的容器外,都有一个kubelet启动时指定的基础容器
  • kubelet创建基础容器,生成network namespace
  • kubelet调用网络CNI driver,由它根据配置调用具体的CNI 插件(eg:calico,flannel)
  • CNI 插件给基础容器配置网络
  • Pod 中其他的容器共享使用基础容器的网络

基本场景

  • 容器间通信:localhost
  • pod间通信:overlay network
  • pod与service通信:Iptables
  • External 与 service通信

Underlay vs Overlay

容器网络方案可以分为underlay和overlay两派,主要差异是在于网络是否与host网络同层;

如果pod ip不是基于overlay网络,而是基于underlay网络,那么会遇到以下的问题

  • 可分配ip数目依赖于underlay网络的网络规划,有可能可用ip数目很少
  • 跨网段通讯需要nat或其它映射技术的支持
  • underlay网络ip的变化有可能会导致pod ip的变化

因此,pod基于overlay网络之上,是使用构造虚拟网络来解耦真实网络,

它带来的好处就是官方所说的,如下:

  • pod之间通讯可直接通讯,而不需要nat等地址映射技术
  • overlay网络与underlay网络解耦
  • ip资源丰富,不依赖于underlay网络的ip资源

真实转发过程

  • Flannel构建了一个虚拟网络Overlay Network如上图,就是10.1.0.0/16这个网络
  • 每个物理机/虚拟机都被flannel分配了一个更小一点的网络,提供给这个机器上的pod进行使用,譬如上图的10.1.15.0/24 和 10.1.20.0/24这两个网络;docker默认的网桥docker0就是使用的上面这个网络;
  • docker0是容器通信时必须通过的网关
  • 对于同一个子网的容器(pod)通信,就是直接走docker0
  • 对于跨主机(子网)的容器通信,数据包首先会经过docker0,然后到达虚拟网卡flannel0,并被一个守护进程flanneld接收,flanneld可以从etcd获取路由信息,然后将数据包封装上真实的物理IP地址,并通过物理网卡进行发送
  • 通信是以UDP的方式
  • 接收端的flanneld进程收到后,去掉包头,将请求转发给对应的docker0

Flannel 的网络实现方案

  • 基于host-gw模式的underlay网络
  • 基于vxlan的overlay网络
  • 基于udp的overlay网络

https://cloud.tencent.com/developer/article/1607673

VPC的实现

早期的隔离通过VLAN可以做到网络的隔离;

云上vpc有很多种实现方式,例如VXLAN(Virtual eXtensible Local Area Network,虚拟扩展局域网)

K8s网络模型:

https://cloud.tencent.com/developer/article/1415035

https://cloud.tencent.com/developer/article/1939179

当vpc遇到K8s overlay:

https://mp.weixin.qq.com/s?__biz=Mzk0ODI3NDQ0NQ==&mid=2247483988&idx=1&sn=d1d06748eea509e2da4882cff5027595&chksm=c36b5636f41cdf2065bdcc2cd3eedcc4adeac4cb7915a68e8d3b2f6190d47582d69481dc3aee&scene=21#wechat_redirect

VXLAN:

什么是VXLAN:https://support.huawei.com/enterprise/zh/doc/EDOC1100087027

VXLAN(Virtual eXtensible LAN,虚拟可扩展的局域网),是一种虚拟化隧道通信技术。它是一种overlay(覆盖网络)技术,通过三层的网络搭建虚拟的二层网络。简单来讲,VXLAN是在底层物理网络(underlay)之上使用隧道技术,依托UDP层构建的overlay的逻辑网络,使逻辑网络与物理网络解耦,实现灵活的组网需求。它不仅能适配虚拟机环境,还能用于容器环境。

vxlan支持更多的子网(vlan只支持2的12次方个子网,vxlan支持2的24次方个子网),并通过VNI(Virtual Network Identifier)区分不同的子网,相当于VLAN中的LAN ID

多租户网络隔离。不同用户之间需要独立地分配IP和MAC地址

云计算业务对业务灵活性要求很高,虚拟机可能会大规模迁移,并保证网络一直可用。解决这个问题同时保证二层的广播域不会过分扩大,这也是云计算网络的要求

Flannel中页有对VXLAN的实现方案。

发表在 云原生 | 685条评论

回看操作系统

最近在找工作,闲暇时间就又回顾了一下操作系统的知识。

上学的时候看操作系统,只是觉得枯燥,底层远远没有应用层花里胡哨的实现吸引人;但是在工作五年之后,再回顾,只是觉得精妙。

工作五年中,每天都在接触不同的系统,搭建不同的系统,希望能找到好的方案解决手边的难题,希望能够尽可能的从系统搭建过程中汲取到一些‘真理’。但是再看操作系统,发现好多真理就平铺直述的摆在这里。没有一些高大上的包装,很多都是用最直接高效架构,算法,模式解决问题,这些架构,算法和模式映射到日常工作的问题域中,极具借鉴价值。

如果能够有时间再深入的看一下操作系统,那真是太棒了。

概述

操作系统与普通应用软件的区别

  • 提供操作界面
  • 控制程序运行
  • 管理系统资源
  • 配置系统参数
  • 监控系统状态
  • 等等

功能

  1. 进程管理/CPU管理
    1. 进程控制:创建,暂停,唤醒,撤销
    2. 进程调度:调度策略,优先级
    3. 进程通信:进程间通信
  2. 内存管理
    1. 内存分配
    2. 内存共享
    3. 内存保护
    4. 虚拟内存管理
  3. 设备管理
    1. 设备的分配和调度
    2. 设备无关性
    3. 设备传输控制(与cpu和内存之间)
    4. 设备驱动
  4. 文件管理
    1. 存储空间管理
    2. 文件操作
    3. 目录操作
    4. 权限管理

发展历史

硬件发展历史:电子管-》晶体管-》集成电路-》大规模集成电路

操作系统发展阶段:手工操作-》单道批处理系统(单作业队列依次执行)-》多道批处理系统-》分时系统

多道批处理系统:可同时在内存存放多道程序,某一个程序放弃CPU时,例如 IO操作时,操作系统调度另外一个程序使用CPU;尽可能的保证CPU的使用率;

分时系统

两个重要技术:

  • 中断技术:CPU收到中断信号,停止当前工作;处理完新的工作后从断点继续。
  • 通道技术:外设与内存之间数据传输。

分时技术:主机以很小的‘时间片’为单位,把CPU轮流分配给每个任务/用户使用。

UNIX是第一个实用化的分时操作系统

分类

  1. 微机操作系统
    1. Mac OS
    2. DOS
    3. Windows
    4. Linux
  2. 实时操作系统
    1. 航天航空
    2. 工业控制
    3. 等等
  3. 嵌入式操作系统
    1. Android
    2. IOS
    3. Linux(与PC有差异)
    4. 等等
  4. 服务器操作系统
    1. UNIX
    2. LINUX
    3. WINDOWS

linux发行版本

按发行者

  1. 公司发行版本:Redhat
  2. 社区发行版本:Debian

按源头:

  1. Redhat系列:fedora(基于RH的免费版),CentOS(RHEL的社区克隆版本)
    1. Redhat 系列的包管理方式采用的是基于 RPM 包的 YUM 包管理方式 ,
  2. Debian系列:Ubuntu
    1. Debian 最具特色的是 apt-get/dpkg 包管理方式

操作系统的设计和实现思路逻辑结构

结构

整体式结构(模块化结构)

分层式结构(在局部也会采用模块化的设计)

微内核结构

操作系统=微内核+核外服务器

  • 微内核足够小提供OS最基本的核心功能
  • 核外服务器提供OS绝大部分的功能

支持操作系统的基本硬件:

  • CPU
  • 内存
  • 中断
  • 时钟

CPU的态mode):

CPU的状态

  1. 核态:Kernel mode:有所有权限-管理程序/OS内核操作
  2. 用户态:User mode:部分资源权限-用户程序
  3. 管理员态:Supervisor mode:1,2之间

不同操作系统的mode设计不同

Intel CPU: ring 0 ~ ring 3

存储程序和设备

按读写工作方式:

  • Ram
  • Rom

按照与CPU的联系:

  • 主存:直接与CPU交换信息
  • 辅存

按材料

  • 半导体(主存多用)
  • 磁存储
  • 光存储

实际存储体系是由不同类型共同构成

  • 寄存器:非常小,但是非常快
  • 高速缓存cache:小,快
  • 主存:GB级别,比较快
  • 辅助存储(硬盘):几百GB,慢,其中程序必须调度到主存才能运行

CPU访问顺序

访问高速缓存-》主存-》辅助存储(会调到主存之中)

中断机制

CPU收到中断信号,停止当前工作;处理完外部事件后从断点继续。

正是中断机制的引入,才支持并发以及实时处理

CPU具有在不同的进程间切换的能力

中断是从硬件层面支持的

一些概念

  • 中断源引发中断的事件
  • 中断类型
    • 强迫性中断I/O事件 ,外部中断
    • 自愿中断程序自身命令引发
    • 外部中断CPU内部引起
    • 外部中断CPU外部引起I/O
      • 不可屏蔽的中断
      • 可屏蔽的中断不紧急CPU可以不响应
  • 断点将要执行的下一指令的地址
  • 现场上下文):程序执行依赖的信息集合CPU的寄存器内信息
  • 现场保护/恢复入栈/出栈

操作系统的启动过程

计算机的两个模式

  • 实模式real mode寻址一个非常小(1MB的空间cpu单任务运行
    • 640k基本内存
    • 128k显卡显存
    • 256K bios
      • 显示卡bios
      • IDE控制器bios
      • 系统bios最后64K
  • 保护模式protect mode寻址xGB的空间段页式的寻址方式cpu多任务

系统bios

Basic input/output systemfirmware 固件基本输入系统

固件以硬件形式存在的软件

功能

  • 系统启动配置在CMOS里面进行配置
  • 基本设备IO服务
    • bios的IO服务采用中断的方式提供
    • 例如中断信号13H代表软盘IO服务调用其子功能02H为读扇区其参数指定了读入读出的存储位置等等细节 
  • 系统加电自检POST & 启动 
    • 自我检查
    • 初始化基本硬件CPU内存显卡。。。
    • 系统启动

系统启动流程

  • 按下powerOn或者Reset
  • CPU寄存器置默认值FFFF0
  • CPU于是执行第一条指令位于 FFFF0:JUMP POST
  • 执行第一条指令:加电自检(位于bios)
  • 调用显卡bios
  • 依次调用其他设备的bios
  • bios启动程序运行,从硬盘/u盘/光盘启动OS
    • 读取MBR,控制权交给MBR
    • MBR检查分区表
    • 找到引导分区并加载到内存
    • 分区引导记录(PBR)控制后序的过程(加载OS)
    • os启动后接管计算机

bios如何加载&启动操作系统

MBR(Master Boot Record):和操作系统启动相关的信息(512Bytes)

位于硬盘的首扇区 

引导(启动)扇区位于整个硬盘的0磁头0柱面1扇区,包括硬盘主引导记录MBR(Master Boot Record)和分区表DPT(Disk Partition Table)。其中主引导记录的作用就是检查分区表是否正确以及确定哪个分区为引导分区,并在程序结束时把该分区的启动程序,也就是操作系统引导扇区调入内存加以执行。

其实启动过程就是指定一个convention规则按照默认的流程默认的地址去加载某一些代码

操作系统启动细节

之前的环节属于将OS从硬盘加载到内存-初始引导

os接管系统之后开始执行

核心初始化:初始化核心数据

  • 各种寄存器的初始化
  • 存储系统和页表初始化
  • 核心进程的构建

系统初始化

  • 初始化桌面控制台
  • 初始化文件系统
  • 初始化网络系统
  • 等等

linux启动过程

  1. 加电自检POST
  2. MBR
  3. 内核Kernel初始化
  4. 内核启动
  5. 加载init程序:进程号为1 (/etc/inittab):设置键盘,网络,各种模块等等

操作系统的生成

  1. 根据硬件环境/用户需求配置功能模块的参数
    1. 例如选择CPU的类型
    1. 是否有对PCI卡的支持
    1. 网络TCP/IP模块的支持
    1. 等等
  2. Build OS映像:对源代码根据新的参数进行重新的编译

用户跟操作系统交互

  1. 命令行
    1. Windows BAT
    1. Linux shell脚本:Bsh, Csh, Ksh, Bash
  2. 图形界面

系统调用

系统调用是操作系统为应用程序提供的的服务/函数

  • 系统调用一般涉及核心资源或者硬件操作
  • 运行与核态
  • 系统调用会产生中断(自愿中断):如果把高级语言代码转换为汇编语言,就可以看到中断的操作在其中。非系统调用通常不会这样。
  • 每个系统调用都有一个编号

用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如Linux 的 int 80h 中断,也可以称为软中断

从上图我们可以看出来通过系统调用将Linux整个体系分为用户态和内核态,为了使应用程序访问到内核的资源,如CPU、内存、I/O,内核必须提供一组通用的访问接口,这些接口就叫系统调用。

库函数就是屏蔽这些复杂的底层实现细节,减轻程序员的负担,从而更加关注上层的逻辑实现,它对系统调用进行封装,提供简单的基本接口给程序员。

系统调用流程

  • Linux通过 80号中断来实现系统调用
  • dos通过21号中断来实现系统调用

内核态与用户态/内核空间与用户空间

内核态/用户态

内核态和用户态是指两种运行状态,这两种状态的设置是为了区分不同运行权限

CPU的两种运行状态

操作系统的运行级别

进程的运行状态

以CPU为例

内核态:说明CPU正在运行内核程序,此时可以执行特权指令

用户态:说明CPU正在运行用户程序,此时只能执行非特权指令

内核态/内核空间

内核空间与用户空间是对内存的划分

将内存分为内核空间和用户空间,内核空间只有内核程序才能访问,用户空间专门给用户程序使用

⽤户空间的代码只能访问⼀个局部的内存空间

内核空间的代码可以访问所有内存空间

进程管理

基本概念

进程:

  • 描述和管理程序的运行过程
  • 程序在某个数据集上的一次运行活动
  • 程序的一次执行过程
  • 进程是系统分配资源和调度CPU的单位

分类1:

  • 系统进程(与内核有关)
  • 用户进程

分类2:

  • 偏CPU的计算密集型进程
  • 偏IO的网络密集型进程

进程的状态

每一个进程都是断断续续的在CPU上运行(共享),当它使用IO操作或者系统调用,进程都可能会停下来。

  • 运行状态:RUNNING:
    • 一个进程占有CPU,在其上运行
  • 就绪状态:READY
    • 具备运行状态,但是不占有CPU
  • 阻塞状态:Block (WAIT等待状态)
    • 因为等待某项服务完成,而处在不能运行的状态:等待系统调用,IO调用,某个信号等等。。。

状态变迁:

就绪—-操作系统的进程调度模块—-》运行

挂起进程不运行,且暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。需要信号使其恢复就绪状态。

Linux 里是通过fork()函数创建一个新的进程

进程控制块PCB

描述进程状态,资源与相关进程的关系的数据结构

PCB是进程的标志

操作系统创建进程时创建PCB,撤销后PCB撤销。

PCB数据结构

数据结构

Linux PCB数据结构:

进程切换

进程上下文context:运行环境,CPU各个寄存器的值等等信息

上下文切换:换出进程上下文离开CPU,入栈;换入进程上下文进入CPU,出栈

进程控制

  • 创建
    • 创建PCB并赋值
    • 分配空间
    • 插入就绪队列
    • 调度进程会根据PCB内容对这个进程进行调度
  • 撤销:收回资源,撤销PCB
    • PCB队列中检索出PCB
    • 获取状态
    • 终止进程(需要检查是否撤销子进程)
    • 释放资源
    • 撤销PCB
  • 阻塞:停止进程执行,变为阻塞
    • 请求系统服务/某种操作/等待数据
    • 有不同的阻塞队列(里面进程阻塞的原因不同)
  • 唤醒:唤醒处于阻塞队列中的某个进程

进程控制原语

由若干指令构成的具有特定功能的函数;具有原子性不可以被打断

进程和线程在多核cpu,多cpu中的运行关系

操作系统会拆分CPU为一段段时间的运行片,轮流分配给不同的程序。对于多cpu,多个进程可以并行在多个cpu中计算,当然也会存在进程切换;对于单cpu,多个进程在这个单cpu中是并发运行,根据时间片读取上下文+执行程序+保存上下文。同一个进程同一时间段只能在一个cpu中运行,如果进程数小于cpu数,那么未使用的cpu将会空闲。

多线程的概念主要有两种:一种是用户态多线程;一种是内核态多线程,对于内核态多线程(java1.2之后用内核级线程),在操作系统内核的支持下可以在多核下并行运行;

对于多核cpu,进程中的多线程并行执行。对于单核cpu,多线程在单cpu中并发执行,根据时间片切换线程。同一个线程同一时间段只能在一个cpu内核中运行,如果线程数小于cpu内核数,那么将有多余的内核空闲。

关于阻塞

等待执行的进程中

  • 都是非运行态
  • 一些(A)在等待CPU资源
  • 另一些(B)在等待I/O完成(如文件读取)
  • 如果这个时候把CPU分配给B进程,B还是在等I/O我们把这个B叫做阻塞进程
  • 因此,分派程序只会把CPU分配给非阻塞进程

Windows  Linux 创建进程

各自都有函数可以在用户代码中创建&启动一个进程

linux是通过fork+exec进行进程创建的

Linux Fork函数

  • 通过fork() 函数创建新进程
  • 新进程是当前进程的子进程
  • 父进程:fork的调用者
  • 子进程是父进程的复制:除了PID不一样,代码,数据,堆栈都一样 
  • 子进程会从fork处开始向下执行
  • 父进程获得的子进程PID>0, 但是子进程内部对于同一个PID变量,是为0的
  • Linux里面处了init进程不是fork创建的,其他都是fork创建的。
  • 如何保证子进程与父进程具有不同的执行逻辑呢?借助exec函数族

Linux exec函数族

  • 装入一个指定的可执行程序
  • 使得子进程具有跟父进程不同的新功能

在Linux中要使用exec函数族。系统调用execve()对当前进程进行替换,替换者为一个指定的程序,其参数包括文件名(filename)、参数列表(argv)以及环境变量(envp)。exec函数族当然不止一个,但它们大致相同,在 Linux中,它们分别是:execl,execlp,execle,execv,execve和execvp,下面我只以execlp为例,其它函数究竟与execlp有何区别,请通过manexec命令来了解它们的具体情况。

  一个进程一旦调用exec类函数,它本身就”死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。)

那么如果我的程序想启动另一程序的执行但自己仍想继续运行的话,怎么办呢?那就是结合fork与exec的使用。下面一段代码显示如何启动运行其它程序:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

char command[256];
void main()
{
  int rtn; /*子进程的返回数值*/
  while(1) {
      /* 从终端读取要执行的命令 */
      printf( ">" );
      fgets( command, 256, stdin );
      command[strlen(command)-1] = 0;
      if ( fork() == 0 ) {/* 子进程执行此命令 */
         execlp( command, NULL );
         /* 如果exec函数返回,表明没有正常执行命令,打印错误信息*/
         perror( command );
         exit( errno );
      }
      else {/* 父进程, 等待子进程结束,并打印子进程的返回值 */
         wait ( &rtn );
         printf( " child process return %d\n", rtn );
      }
  }
}

线程

CPU可以调度运行的更小的单位;一个进程可以创建多个线程;并发粒度更小;

  • 单线程程序:只有主线程
  • 多线程程序:主线程+其他用户线程

线程 VS 进程

  • 在面向进程设计的系统中,进程是程序的基本执行实体
  • 在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器

线程其实很多概念跟进程非常类似,是在更小粒度的划分;例如关于阻塞的概念二者都有,进程中的多线程也会有排队的队列,一旦一个线程被阻塞,就被放到阻塞队列中去,不再调度时间片,区别是,这是发生在进程这个容器内部。

进程同步机制

进程同步中的问题和解决方法和线程安全中的基本是一致的

同步

  • 同步:相互之间协作(先后关系/等待前提条件)
  • 互斥:避免多个进程对临界区同时操作

互斥关系属于一种特殊的同步关系

这两个对于线程也是存在的。

临界区 &

  • 临界资源只允许一个进程独立访问的资源
  • 临界区进程中访问临界资源的代码块

临界区在满足需求的情况下应该尽可能的小;

等待进入临界区的进程,应该主动放弃CPU使用权限(阻塞),而不是不断的询问;

锁机制

通过一个标志,来表示临界资源是可用还是可用/不可用的状态

  • 进入临界区,加锁
  • 离开临界区,开锁

上锁/开锁都需要使用原语来执行

PV操作

pv是实现同步的一种机制。

信号灯/信号量:进程在运行过程中受到信号灯状态的控制,同时也可以修改信号灯的状态。通常是一个大于等于0的整数。

Java 的semaphore:

Semaphore(信号量):是一种计数器,用来保护一个或者多个共享资源的访问。如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放。

P操作(通过)

  • S–
  • S大于或者等于0,该进程正常执行
  • 如果小于0,则进程阻塞,并加载到阻塞队列中,控制权交给调度函数;

V操作(释放)

  • S++
  • 如果S大于0,进程正常执行(队列里面没有可以唤醒的进程了)
  • 如果小于等于0,该进程继续并且从队列中唤醒一个进程

PV操作的例子:银行排队取号,按号叫人,值得一提的是窗口上有个大大的LED屏,显示着有几个人在等待,一共三个窗口,这时,10个人想办理业务。(资源-窗口,人-进程,大屏幕-S,很符合了吧)

    没人在窗口办理业务,屏幕:3个窗口可以办理;

    1人在窗口办理业务,屏幕:2个窗口可以办理;

    2人在窗口办理业务,屏幕:1个窗口可以办理;

    3人在窗口办理业务,屏幕:0个窗口可以办理;

    又来一个人取号,屏幕:1人在等待;       。。。。。

    第十个人来了,屏幕:7人在等待;

以上取号都是申请资源操作 P(S),取号就相当于对屏幕上信号量减一,即 P(S) 中的 S-1 , 减成负数时就要在等待区等待。

当一个人办理完业务,将释放一个窗口,则屏幕信号量 V(S) 中的 S 加1, 1人办理完业务,屏幕:6人在等待(一人去了窗口办理业务)

用PV操作表示:

申请资源P(S)中的S 操作:信号量初始值S = 3 ,一个申请 S <—— 3-1 ,S >= 0,进程继续。不阻塞。。。。。到第4个申请的时候,

S <——0-1,S < 0 ,进入阻塞队列中等待。。。。。到第10个申请的时候, S <—— -6-1,,S = -7 ,等待队列中有7个正着等着用资源。

释放资源V(S)中的S 操作:1人办理完业务,释放资源,S <—— -7+1 ,S < =0 ,唤醒等待队列中的一个。。。。7个人办理完业务,释放资源 S <—— -1 +1 ,S <=0 ,没有申请了,就空闲继续执行进程即可。全部办理完,S = 3 则有3个空闲资源可用。

PV操作解决互斥(mutex-互斥量)问题

锁机制可以很好的解决互斥问题,同样用PV操作也可以实现。

用与互斥信号灯初值mutex = 1 ;表示该资源未被占用。任何想要进入临界区的进程,必须先进行p操作(mutex为1时才能通过),访问临界资源完成后再v操作。。。

mutex =1 : 表示没有进程进入临界区

mutex =0 : 表示有一个进程进入临界区,前方没有其他进程。

mutex =-1 :表示一个进程已经进入,另一个进程的等待进入。

Java里的互斥锁:Synchronized,lock

PV操作解决同步问题

同步问题的本质

  • 条件不满足则暂停
  • 条件满足则继续

可以用多个,合理赋值的信号量来控制运行条件

经典同步问题

生产者消费者问题

windows进程同步机制

  • 临界区对象(锁机制)
  • Mutex-互斥量
  • 信号量对象
    • WaitForSingleObject()
    • ReleaseSemaphore()

Linux进程同步机制

如果父进程先于子进程结束,则子进程成为孤儿进程。孤儿进程将被 init 进程(进程号为1)领养,并由 init 进程对孤儿进程完成状态收集工作。

而如果子进程先于父进程退出,同时父进程太忙了,无瑕回收子进程的资源,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程

Wait()函数:阻塞自己直到有子进程结束(僵尸进程),wait会收集子进程信息并销毁

exit函数直接退出当前进程

Sleep函数:暂停运行,阻塞并且,系统会暂停调度它

我的理解如果父进程直接退出没清理子进程信息wait),那么那么子进程将变成僵尸进程

父子进程

  • 共享普通变量:各自操作副本不会互相影响
  • 共享文件,是操作同一个文件

进程间通信

匿名管道通信机制

仅仅能用于父子进程/兄弟进程

Windows 采用的是这样一种方式。

linux信号通信

信号是向进程发送一个通知(事件),收到信号的进程执行某些操作。

信号可以是进程发出,系统发出,硬件发出。例如:

  • 例如Ctrl + C 产生SIGINT信号去结束一个进程。
  • Ctrl + Z 产生SIGSTP信号,将进程挂起(暂停);
  • 发送命令:Kill -9 会发送SIGKILL信号强制结束一个进程

linux发送信号的方式

  • 硬件,例如键盘
  • 终端命令,例如kill
  • 程序中调用函数,kill(),abort()。。等
  • 硬件/内核异常产生的信号

LInux定义了64种信号

可以用自定义的函数去处理这些捕捉到的信号,这个处理函数可以绑定到进程上。

死锁问题

定义

每个进程无限期等待(阻塞)一个不会发生的条件的状态

起因

  • 进程请求资源和释放资源的顺序不当,导致死锁。
  • 不正确的PV操作也会导致死锁

解决

  • 预防死锁
    • 预先静态分配法:在运行前一次性分配资源,在运行过程中不在申请资源,从而避免死锁/阻塞。
    • 有序资源分配法:破坏环路条件,使得环路无法构成
  • 避免死锁:在资源分配时判断是否会进入死锁
  • 检测死锁 &恢复:让死锁发生,但是通过额外逻辑进行检测恢复

Linux & windows如何处理死锁

鸵鸟策略:并不解决,留给用户处理。

进程调度

进程调度由操作系统的进程调度模块负责

所谓的调度,就是在合适的时刻以一定的策略选择一个就绪进程来执行。

进程调度的目标

以下的目标很难都兼顾,因为有些是相互之间矛盾的

量化指标

  • 周转时间:进程完成时间 – 进程提交时间
  • 平均周转时间:所有周转时间的平均值
  • 带权周转时间:进程周转时间/进程在CPU上的运行时间
  • 平均带权周转时间

调度算法

  1. 先来先服务调度算法 first come first serve
  2. 短作业优先调度算法 short job first
  3. 响应比高者优先调度算法(动态)
    1. 响应时间/运行时间
    1. (等待时间+运行时间)/运行时间
    1. 等待时间越长,越容易被调度
    1. 运行时间越短,越容易被调度
  4. 优先数调度算法(本质就是动态算分)
    1. 进程优先数高的先执行=静态优先数+动态优先数
    1. 静态优先数:所需资源多少,所需时间长短,进程类型
    1. 动态优先数:已经使用CPU时长,已经等待时长,发生IO的次数
  5. Round-robin:均等时间片

Linux进程调度

  • 普通进程
    • 采用动态优先级,调度程序动态计算并修改不同进程的优先级
    • 例如,一个进程占用cpu时间越长,优先级就逐渐越低
  • 实时进程
    • 采用静态优先级,用户预设

PCB的一些属性

policy

  • SCHED_FIFO (实时进程)
  • SCHED_RR(实时进程)
  • SCHED_OTHER(普通的分时进程)-》动态优先级(动态算分)

Priority:优先级

Counter: 进程能够连续运行的时间;时钟中断tick发生时,coutner会减1,直到为0则被阻塞。

调度时机

  • 中断处理过程中调用schedule():时钟中断,IO中断,系统调用和异常等等
  • 中断处理返回到用户态主动调用schedule()
  • 内核线程可以直接调用schedule()进行进程切换
  • 用户态进程只能通过陷入内核后在中断处理过程中被动调度

进程切换

内核挂起当前CPU上的进程,并恢复之前某个挂起的进程;涉及上下文切换

一个例子:

内存管理

由操作系统的存储管理模块来负责

内存体系

辅存上的数据不能被CPU直接访问

存储器层次结构,即少量的非常快速、昂贵、易变的高速缓存(cache);若干兆字节的中等速度、中等价格、易变的主存储器(RAM);数百兆或数千兆的低速、廉价、不易变的磁盘。

CPU内部主要由控制器、运算器和寄存器组成:

  • 控制器负责指令的读取和调度,
  • 运算器负责指令的运算执行,
  • 寄存器register:寄存器是CPU的内部组成单元,是CPU运算时取指令和数据的地方,速度很快,寄存器可以用来暂存指令、数据和地址。在CPU中,通常有通用寄存器,如指令寄存器IR;特殊功能寄存器,如程序计数器PC、sp等。

设计思想(冯.诺依曼体系架构)

总是有一部分负责控制、一部分负责执行、一部分则负责存储,它之间进行交互以及接口通信则总是通过总线来完成。这种设计思路一样的可以应用在我们的软件设计体系里面:组件和组件之间通信通过事件的方式来进行解耦处理,而一个组件内部同样也需要明确好各个部分的职责(一部分负责调度控制、一部分负责执行实现、一部分负责数据存储)。

发展历程

无内存抽象

在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。比如当执行如下指令时:mov reg1,1000:

这条指令会毫无想象力的将物理地址1000中的内容赋值给寄存器。不难想象,这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。带来的问题是:

  • 用户程序可以访问任意内存,容易破坏操作系统,造成崩溃
  • 同时运行多个程序特别困难

出现内存抽象地址空间概念

把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。因此操作系统需要对物理内存做一层抽象,也就是「地址空间」(Address Space),一个进程的地址空间包含了该进程所有相关内存,比如 code / stack / heap。一个 16 KB 的地址空间可能长这样:

基址寄存器与界限寄存器可以简单的动态重定位:每个内存地址送到内存之前,都会自动加上基址寄存器的内容。

从 32KB 处作为开始,48KB 作为结束。那 32 / 48 可不可以动态设置呢,只要在 CPU 上整两个寄存器,基址寄存器base 和 界限寄存器bounds 就可以了,base 指明从哪里开始,bounds 指定哪里是边界。 因此真实物理地址和虚拟地址之间的关系是:

physical address = virtual address + base

有时,CPU 上用来做内存地址翻译的也会被叫做「内存管理单元 MMU」(Memory Management Unit),随着功能越来越强大,MMU 也会变得越来越复杂。

虚拟内存机制

虚拟内存是现代操作系统普遍使用的一种技术。前面所讲的抽象满足了多进程的要求,但很多情况下,现有内存无法满足仅仅一个大进程的内存要求。物理内存不够用的情况下,如何解决呢?

覆盖overlays:在早期的操作系统曾使用覆盖技术来解决这个问题,将一个程序分为多个块,基本思想是先将块0加入内存,块0执行完后,将块1加入内存。依次往复,这个解决方案最大的问题是需要程序员去程序进行分块,这是一个费时费力让人痛苦不堪的过程。后来这个解决方案的修正版就是虚拟内存。

交换swapping:可以将暂时不能执行的程序(进程)送到外存中,从而获得空闲内存空间来装入新程序(进程),或读人保存在外存中而处于就绪状态的程序。

虚拟内存:虚拟内存的基本思想是,每个进程有用独立的逻辑地址空间,内存被分为大小相等的多个块,称为页(Page).每个页都是一段连续的地址。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上。

内存管理的功能

  • 地址映射
  • 虚拟存储
  • 内存分配
  • 存储保护

地址映射

把虚拟地址变换成内存的物理地址(CPU能够通过地址总线真实访问)。

方式

  • 固定地址映射
    • 编程或者编译时就确定映射关系
  • 静态地址映射
    • 程序装入时由操作系统完成逻辑地址到物理地址的映射,
    • 程序占用连续的内存空间,可以理解为从0的整体移动
  • 动态地址映射
    • 程序执行过程中把逻辑地址转换为物理地址
    • 和静态映射非常类似,只是在执行时才完成地址的转换
    • 物理地址 = 逻辑地址 + BA(BASE ADDR)
    • BA可以更新-动态变化
    • 程序可以分小块,不连续占用内存空间-》会产生多个BA:BA1, BA2, BA3 等等
    • 需要由硬件支持:MMU – 内存管理单元的支持

虚拟内存

解决问题

  • 内存不够
  • 地址冲突

每个进程都有这个一个封闭的内存空间

  • 使得多个程序能够在小内存中运行
  • 多个程序地址不冲突
  • 内存利用率高,无碎片

内存分配

为程序分配足够的内存空间

  • 放置策略:放在什么位置
  • 调入策略:合适将代码&数据调入内存
  • 淘汰策略:内存不够,哪些代码/数据需要被淘汰

存储保护

防止访问越界/越权

物理内存管理

单一区存储管理不分区

在这种管理方式中,内存被分为两个区域:系统区和用户区。应用程序装入到用户区,可使用用户区全部空间。其特点是,最简单,适用于单用户、单任务的操作系统。CP/M和 DOS 2.0以下就是采用此种方式。这种方式的最大优点就是易于管理。但也存在着一些问题和不足之处,例如对要求内存空间少的程序,造成内存浪费;程序全部装入,使得很少使用的程序部分也占用—定数量的内存。

分区存储管理

把内存划分成若干大小不等的区域,供不同的程序使用

固定分区

系统初始化时就完成分区的划分,并且不再改变

动态分区

程序装入的时候再创建分区

缺点:收回程序后,再进行分配,可能会产生很多内存碎片

空闲区表

分区的分配

从空闲区表第一个开始寻找,直到找到一个合适的空闲区(大于需要的空间);

从空闲区的底部(高位置)分割出一部分区域;

分割后剩余的区域依然记录在空闲区表;

分区的放置策略

即,如何排序空闲区表

  • 按照地址从低到高
  • 按照地址从高到低
  • 按照空闲区从大到小
  • 按照空闲区从小到大

分区的回收

释放某个使用的分区,释放之后需要考虑一个问题:释放区是否与空闲区相邻;可能需要合并操作;

最后需要更新空闲区表。

内存紧缩:将各个占用分区向内存一端移动,然后将各个空闲分区合并成为一个空闲分区。

内存覆盖技术

目的:在较小的内存空间内运行较大的程序

缺点:

  • 程序编程复杂,需要合理的划分模块
  • 从外存加载入内存需要耗费大量时间

内存交换技术

缺点:增加CPU开销,增加延迟。

内存碎片

解决办法:

  • 分割后剩余的部分小于某个门限值,则直接给用户
  • 内存拼接技术:移动内存中所有已经分配区到内存的一端,将其余空闲分区合并为一个大的空闲分区
    • 回收释放区时触发拼接
    • 找不到足够空闲区时
    • 定期
  • 解除程序占用连续内存空间才能运行的限制程序可以拆成不连续的内存空间

小结

虚拟内存管理

思路:

典型方式:

  • 页式虚拟存储管理
  • 段式虚拟存储管理
  • 段页式虚拟存储管理

页式虚拟存储管理

操作系统页的大小通常是:4K

页式系统中的地址

页面映射表:

映射过程:

快表机制

  • 慢表把页表放到内存中
  • 快表把页表的一部分放到Cache中如果命中则latency会更小

页面的共享

例如,在windows里面有很多动态链接库,就涉及到的动态链接库的共享;或者同时打开多个文本处理器(由代码和数据组成,代码可以共享);

缺页中断

现在采用的是分级存储体系CPU依次尝试从Cache内存辅存里面去获得数据

带有中断位的页表

如果不在内存中,执行这个页上的指令/使用这个页上的数据时,会触发缺页中断,这样就会执行从辅存将数据加载到内存中的过程,这是一个比较重的IO操作,如下:

缺页率:体现了内存的命中率

缺页次数/访问页面的总次数

淘汰策略

上图中内存不够时,需要有淘汰策略将内存中的部分页面淘汰出内存,

  • 保证低缺页率
  • 避免页面抖动:内存和辅存间频繁交换

常用的淘汰策略:

  • 最佳算法:OPT算法:淘汰不再使用,或者最远的将来使用的页面
  • 先进先出算法:FIFO算法:
  • 最久未使用淘汰算法:LRU算法
    • 可以通过硬件(移位寄存器)实现
  • 最不经常使用算法:LFU算法:选择被访问次数最少的页面
    • 每个页面设定一个访问计数器

段式虚拟存储管理

按照逻辑来对进程进行划分:

段的划分是程序员可以控制的;

跟页表类似

段式系统vs页式系统

段页式存储管理

在段式存储中结合页式存储管理

Linux内存管理

  • 实模式采用直接向物理内存写入的机制
  • 保护模式采用虚拟内存管理机制,支持分段/分页机制;32位地址空间可以看到4G内存;每个进程隔离;各自都可以看到4G的空间;

X86 CPU架构下的三种地址

逻辑地址

编译器编译程序时,会为程序生成代码段和数据段,然后将所有代码放到代码段中,将所有数据放到数据段中。最后程序中的每句代码和每条数据都会有自己的逻辑地址。

虚拟地址

我个人理解就是跟逻辑地址一样的

线性地址

指的是虚拟地址到物理地址变换之间的中间层;若是没有采用分页机制,那么线性地址就是物理地址。

CPU加载程序后,会为这个程序分配内存,所分配内存又分为代码段内存和数据段内存。代码段内存的基址保存在CS中,数据段内存的基址保存在DS中。

段基址+逻辑地址=线性地址。

CPU实模式下,段基址表示的是内存段。而保护模式下段基地表示的是段描述表的selector。

物理地址

如果CPU没有分页机制,那么线性地址=物理地址。

由于线性地址是连续,内存中可能没有这么大的一块连续空间。为了解决这个问题,CPU采用了分页的内存管理机制,每页4KB。

地址的映射转换过程由MMU:memory management unit 执行;转换过程为逻辑地址-》线性地址-》物理地址

Linux分页机制intel cpu

Linux采用三级页表

页表很大一个页装不下所以通过多级页表的形式进行存储

二级页表:把超大页表以页为单位分成若干个小页表,存入离散的页框中;

每个页表都正好占用一个页框;

又多了一张表:页目录

采用的思想类似于多级哈希/多级分片 等思想

虚拟地址:页目录号+页 号+页偏移  进行查询

二级页表地址映射访问数据需要三次访问内存

Linux分段机制intel cpu

LDT & GDT 用于地址映射;

文件系统

简单的读取流程:

Raw disk生磁盘的结构

磁头在盘面上将磁信号转换为电信号

每次读取的单位是一个扇区

大致流程:

  • 寻道:移动到相应柱面(磁道)
  • 移动到相应扇区
  • 通过磁头进行读写

柱面:一系列磁道构成的柱面

柱面-C;磁头-H,扇区-S

硬盘的磁头数取决于硬盘中的碟片数,盘片正反两面都存储着数据,所以一个盘片对应两个磁头才能正常工作。比如总容量80GB的硬盘,采用单碟容量80GB的盘片,那只有一张盘片,该盘片正反面都有数据,则对应两个磁头;而同样总容量120GB的硬盘,采用二张盘片,则只有三个磁头,其中一个盘片的一面没有磁头。

通过DMA技术将数据读取到缓存之中/写入磁盘:

DMA技术是Direct Memory Access的缩写。 其意思是“存储器直接访问”。 它是指一种高速的数据传输操作,允许在 外部设备 和 存储器 之间直接读写数据,既不通过CPU,也不需要CPU干预。

无抽象直接使用磁盘

一层抽象盘块

操作系统隐藏了CHS的细节

磁盘访问时间=写入控制器时间+寻道时间+旋转时间+传输时间

对于连续的block号那么读出速度会更快如何实现呢

主要的latency来自于寻道,所以要降低寻道的时间,于是这样分配扇区:

如何得到扇区号

从扇区到盘块

每次读入多个扇区,这样读取的速度变快了,但是碎片增加了(空间浪费),在这个基础上我们进行折衷,即把读写最小单位从扇区变成了盘块(多个连续的扇区);linux0.11盘块是2个扇区。

二层抽象电梯队列

每次完成一个操作,都会触发一次中断来通知CPU任务的完成;进而再从队列中取新的任务进行处理。

任务处理的调度策略

  • FCFS算法
  • SSTF算法 shortest seek time first:短寻道优先
  • SCAN算法:电梯算法,SSTF+中途不回折
  • C-SCAN算法:

小结

注意:磁盘中断会唤醒进程!此时数据已经被加载到了内存,进程可以继续处理执行了。

Cooked disk熟磁盘从生磁盘抽象出来文件

我们不会直接只用盘块号而是让用户使用文件的抽象概念那么如何进行文件《-》盘块的映射呢

三层抽象文件

什么是文件

  • 用户:文件就是字节流
  • 磁盘:文件就是一堆盘块

那么我们需要做的就是建立一个字节流到盘块集合的映射

  • 方案一:一个文件映射到连续的几个盘块
    • 缺点:文件增加到某个程度,可能出现连续盘块不够用(动态增长差)
  • 方案二:链式结构实现文件
    • 可以动态增长,但是读取比较慢
  • 方案三:索引结构(Inode采用的就是这种方式)
    • 通过一个索引来记录文件对应的盘块

实际系统使用的是多级索引结构。(使得每个索引块不那么大查找效率较高

存储文件映射信息的数据结构叫做文件控制块(FCB):

为了能对一个文件进行正确的存取,操作系统必须为文件设置用于描述和控制文件的数据结构,称之为“文件控制块(FCB)”。包括:

  • 文件类型
  • 大小
  • 位置(哪些盘块)
  • 修改时间
  • 等等

Inode是什么:

  • 可以理解为就是Linux中的FCB;
  • 文件数据都存放在block中,那么很显然,我们还必须找到一个地方存储文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等。这种存储文件元信息的区域叫做inode(即就是索引节点)
  • 每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区,存放inode所包含的信息。
  • 每个inode结点的大小,一般是128字节,或256字节。inode结点的总数,在格式化的时候就给定,一般是每1kb或每2kb就设置一个inode。假定在一块1GB的硬盘上,每个inode节点的大小为128字节,每1kb就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%
  • inode如果是特殊文件例如设备文件那么存放的就是设备信息

FD是什么?

Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。

  • 每个文件描述符会与一个打开的文件相对应
  • 不同的文件描述符也可能指向同一个文件
  • 相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开

系统为维护文件描述符,建立了三个表

  • 进程级的文件描述符表
  • 系统级的文件描述符表
  • 文件系统的i-node表

四层抽象文件系统目录树

磁盘最后对用户展现的形态是一个目录树(文件系统),这个是怎样实现的呢?

  • 文件是抽象一个盘块集合
  • 文件系统目录树),抽象整个磁盘

建立好文件系统的磁盘,即使插在另外的机器上,依然可以保留文件(目录树)结构。

目录树:采用的是分治思想

核心点根据文件路径/xx/xx/yy.txt 得到文件的FCB

  • 根目录需要固定位置

所谓的mount操作,就是把一个文件系统的超级块加载进来,这样就能解析到整个文件系统了。

设备管理/驱动

发表在 Uncategorized | 544条评论

如何理解数据仓库

刚刚下午面试了一家国内做分析型数据库的公司,在跟面试官交流的过程中,也沟通了一些我的疑惑;下来自己又查阅了一些资料,随手记录一下。

 

什么是数据库的宏观范畴

宏观讲,数据库的范畴很大,常见的关系型数据库,mySql,PostgreSql;非关系型数据库MongoDB;云上的关系型数据库RDS; 云上的分布式KV数据库:DynamoDB;内存KV数据库Redis,搜索数据库ES。。。等等,不一而足;

但是从功能角度讲可以分为OLAP型和OLTP型。

OLTP处理一些实时的商业请求,操作包括,删除插入更新查询等等操作;对写入查询性能非常重要,事务性的保证,一般不要求复杂的聚合查询性能;数据都是经过normalized规范化的,有预先定义的schema限制,会维护索引等结构来加速查询等等。

OLAP则侧重做数据的聚合分析,应用场景例如数据分析,挖掘,BI;难点是满足:数据量更大,而且要做多维度的复合数据查询的性能,通常在这个领域会比OLTP快很多。同样会有schema的限制;

后者我们常常称之为数据仓库。

 

数据仓库的特点

上面说了数据仓库的一些特点,总结就是:数据仓库的数据主要提供企业决策分析之用,所涉及的数据操作主要是数据查询,一般情况下并不进行修改操作

主要的产品有Hive,Redshift,BigQuery, SnowFlake等,国内的有StarRocks。

但是按照其底层工作原理有可以分为两种:

基于Hadoop的数据仓库

严格意义讲,这种数据仓库并不符合数据库的通常架构,它是架设在Hadoop,spark等分布式计算的基础上,例如Hive,Athena。使用上,其实都是给定一个sql语句提供数据提供数据的schema然后会通过规划分布式计算,算出一个结果。因为数据没有预处理,所以计算速度没有保证:

  • Athena makes it is easier to get started and is more flexible in the types of data it can query, but performance is not guaranteed without significant data preparation

基于数据库架构的数据仓库

这种就是Redshift,BigQuery, SnowFlake这些架构特点,会对数据进行预处理,也会利用一些常用的数据库技术,例如Amazon Redshift is a cloud data warehouse optimized for analytics performance,on PostgreSQL。StarRocks跟面试官交流是基于Mysql的。

对比Athena和Redshift:Redshift requires DBA resources to manage and resize clusters; with Athena there is no infrastructure to manage.

一个更典型的例子:

Hive VS Redshift:

Hadoop Hive can roughly be understood as an attempt to get Hadoop’s distributed file system and MapReduce structure to behave more like a traditional data warehouse by allowing data analysts to run SQL-like queries on top of Hadoop. The queries, written in HiveQL, are translated into MapReduce jobs written in Java and are run on the Hadoop Distributed File System.

Tests have shown that Redshift can be 5x to 20x faster than Hadoop Hive on the same dataset.

Since Redshift is a columnar database, the data must be structured, and this will mean faster querying over any unstructured data source. Moreover, since Redshift uses a Massively Parallel Processing architecture, the leader node manages the distribution of data among the follower nodes to optimize performance.

Redshift使用的MPP技术,保障了它在查询性能上要优于常见的数据库,StarRocks也采用了类似的技术。

 

发表在 Uncategorized | 3条评论

关系型数据库底层原理

几年工作中数据库的使用必不可少,非关系型数据库以及各种数据仓库占大头;至于关系型数据库(主要就是mysql和oracle)用得不多,涉及到的也就是使用层面的一些DDL,DML操作。

我列了一个SQL 与 NoSQL的对比,

sqlnosql
采用二维表结构+关系模型来组织数据;这种结构化的数据非常直观,容易理解以KV模型去存储数据,对数据结构要求低;核心逻辑是通过key来取出value
表与表之间存在关联关系,可以做到多表的联合查询难以做到多表查询
天然支持SQL语句多数不支持SQL
支持事务,ACID,保证数据的一致性通常不支持事务,对一致性要求不那么高,多数提供最终一致性
读写性能不高(为了维护一致性,数据的二维表结构解析),支持的并发量较低读写性能高(不需要维护一致性及复杂数据解析),支持高并发
难以存储海量数据支持海量数据
扩展性较差,难以做到横向拓展多采用分布式架构;拓展性好,容易做到横向拓展
  
  

我发现相比于关系型数据库,我对于非关系型数据库的底层了解更多,一些特性的底层实现都比较了解;但是mysql的很多功能都是用到哪里看到哪里,很多底层实现对我来说都还是黑盒。于是花了点时间系统的捋了一下mysql的一些底层知识,随手做一个笔记。

概述

DB vs DBMS

DB本质就是一个存储信息的文件系统,管理数据的软件叫做DBMS,例如mysql, postgresql等

截止2022-6常用DBMS的排名:https://db-engines.com/en/ranking

排名的标准如下:

  • Number of mentions of the system on websites, measured as number of results in search engines queries. At the moment, we use Google and Bing for this measurement. In order to count only relevant results, we are searching for <system name> together with the term database, e.g. “Oracle” and “database”.
  • General interest in the system. For this measurement, we use the frequency of searches in Google Trends.
  • Frequency of technical discussions about the system. We use the number of related questions and the number of interested users on the well-known IT-related Q&A sites Stack Overflow and DBA Stack Exchange.
  • Number of job offers, in which the system is mentioned. We use the number of offers on the leading job search engines Indeed and Simply Hired.
  • Number of profiles in professional networks, in which the system is mentioned. We use the internationally most popular professional network LinkedIn.
  • Relevance in social networks. We count the number of Twitter tweets, in which the system is mentioned.

我是没想到tiDB只排到了107。。。。。

关系型数据库定义RDBMS):

https://db-engines.com/en/article/Relational+DBMS?ref=RDBMS

Relational database management systems (RDBMS) support the relational (=table-oriented) data model. The schema of a table (=relation schema) is defined by the table name and a fixed number of attributes with fixed data types. A record (=entity) corresponds to a row in the table and consists of the values of each attribute. A relation thus consists of a set of uniform records.

The table schemas are generated by normalization in the process of data modeling.

Certain basic operations are defined on the relations:

classical set operations (union, intersection and difference)

Selection (selection of a subset of records according to certain filter criteria for the attribute values)

Projection (selecting a subset of attributes / columns of the table)

Join: special conjunction of multiple tables as a combination of the Cartesian product with selection and projection.

These basic operations, as well as operations for creation, modification and deletion of table schemas, operations for controlling transactions and user management are performed by means of database languages, with SQL being a well established standard for such languages.

Mysql

非常有代表性的关系型数据库,具有开源,性能好,速度快,社区活跃,软件体积小,容易部署等等优点。

分库分表mysql数据量到达千万级别之后,就要开始考虑分库分表了

关于非关系型数据库

列式数据库的优点:数据查询只用加载需要的列,可以大量的降低系统的IO操作,降低无用数据向内存的加载,查询性能更高。

关系型数据库一些基础知识

数据库范式

基本概念:

  • 超键:能唯一标识一个对象的一个/多个属性
  • 候选键:不包括冗余信息的超键
  • 主键:某一个候选键
  • 外键:
  • 主属性:候选键中的属性
  • 非主属性

第一范式

每个字段要有原子性,不能再拆分

例如一个字段设计为用户信息:姓名+电话+住址。。。等等就是不合规的

第二范式

在第一范式的基础上

  • 每一条记录都有唯一标识(存在主键)
  • 非主键字段要依赖整个主键,而不是主键的一部分(应该把部分依赖的字段单独隔离出来组成一张新表

第三范式

在第二范式的基础上:

所有非主键字段都不依赖其他非主键字段,即非主键字段之间相互独立;

其他范式

一共有6个范式,高级别的范式都是建立在低级别的基础上,通常情况下只需要满足到3rd NF就可以了。

优缺点

  • 优点:消除数据冗余,扩展性更好
  • 缺点:查询效率降低,因为范式等级越高,表更细,表数量更多,关联查询就会带来效率问题

其他设计原则

  • 数据表数量越少越好
  • 数据表中字段个数越少越好
  • 避免联合主键,或者联合主键字段越少越好
  • 使用外键和主键越多越好:说明表划分得更细

这里的外键是指逻辑上的外键关系不是指外键约束不得使用外键与级联,一切外键概念必须在应用层解决。

数据库设计数据建模实体联系模型/ER model

ER建模(Entity Relationship Modeling),即实体关系建模,是指提炼业务,归纳并设计对应的“实体-关系”模型的过程。

ER建模最终输出的结果为实体关系图(ERD-Entity Relationship Diagram)。

  • 对产品经理而言,ERD体现了实体、属性以及实体间的联系,抽象出了业务的核心特征;
  • 对开发人员来说,实体关系图显示数据库中的实体(表)以及该数据库中的表之间的关系,奠定了整个系统的框架基础。

关于Entity实体业务中的对象涉及属性的设计 

关于Relationship关系业务中对象的关联关系

  • 一对一关联
  • 一对多关联
  • 多对多关联
    • 多对对通常需要有第三张联接表(中间表),将两张表以多对多的关系联接起来
  • 自我引用

SQL语句分类

  1. DDL:数据定义语言  关键字有:create(创建),drop(删除) ,truncate(删除表结构,再创一张表),alter(修改)
  2. DQL:数据查询语言  关键字有:select
  3. DML:数据操作语言  关键字有:insert(插入),update(更改),delete(删除)
  4. TCL:事务控制语言 关键字有:begin,savepoint,rollback,commit
  5. DCL:数据控制语言  关键字有 :grant,revoke,deny

多表查询

多表查询是RDBMS中非常常见的操作,包括:

  • 内连接
  • 外连接
  • 子查询

错误的多表查询方式-笛卡尔积,又叫cross join,是SQL中两表连接的一种方式。 假如A表中的数据为m行,B表中的数据有n行,那么A和B做笛卡尔积,结果为m*n行。 通常我们都要在实际SQL中避免直接使用笛卡尔积,因为它会使“数据爆炸”,尤其是数据量很大的时候。

内连接

  • 隐式内连接:使用where条件消除无用数据
    • SELECT * FROM emp t1,dept t2 WHERE t1.`dept_id`=t2.`id`;
  • 显式内连接:
    • SELECT * FROM emp t1 INNER JOIN dept t2 ON t1.`dept_id`=t2.`id`;

外连接

  • 左连接
  • 右连接
  • 外连接

子查询

查询中嵌套查询,称嵌套查询为子查询。

SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE NAME = ‘财务部’ OR NAME = ‘市场部’);

函数

不同的DBMS内部定义的函数是有差别的。

分类:

  • Aggregate聚合函数:面向一系列的值,并返回一个单一的值
  • Scalar 单行函数:只操作一行数据

MySQL删除表操作(delete、truncate、drop的区别)

  • delete 和 truncate 仅仅删除表数据,drop 连表数据和表结构一起删除。
  • delete 是 DML 语句,操作完以后如果没有不想提交事务还可以回滚,truncate 和 drop 是 DDL 语句,操作完马上生效,不能回滚
  • 执行的速度上,drop>truncate>delete

数据库对象

数据库对象就是数据库的组成部分,主要的数据库对象包含:

  • 触发器(Trigger):在数据库表中属于用户定义的SQL事务命令集合。如果你对一个数据库表执行删除、插入、修改的时候,命令就能够自动去执行。
  • 表(Table)
  • 约束(Constraint):通过约束来保证数据的完整性(integrity),即数据的正确且可靠。
  • 视图(View)
  • 存储过程(Stored Procedure)
  • 索引(Index):索引是为了给用户提供快速访问数据的途径,时刻监督数据库表的数据,从而参照特定数据库表列建立起来的一种顺序,主要是为了便于用户访问指定数据,避免数据的重复。
  • 缺省值(Default)
  • 图表(Diagram):图表,是为了编辑表与表之间的关系,可以理解为数据库表之间的一种关系示意图。
  • 用户(User)
  • 规则(Rule)
  • 数据字典:数据字典是对数据库中的数据、库对象、表对象等的元信息的集合。在MySQL中,数据字典信息内容就包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容。MySQL INFORMATION_SCHEMA库提供了对数据局元数据、统计信息、以及有关MySQL server的访问信息(例如:数据库名或表名,字段的数据类型和访问权限等)。该库中保存的信息也可以称为MySQL的数据字典。
  • 函数

约束

数据的校验规则,保证数据的完整性(integrity),即数据的正确且可靠。

分类:

  • 非空约束(not null)
  • 唯一性约束(unique)
  • 主键约束(primary key) PK
  • 外键约束(foreign key) FK
  • 默认值约束 (Default)
  • 自增约束(AUTO_INCREMENT)

视图

MySQL 视图(View)是一种虚拟存在的表,同真实表一样,视图也由列和行构成,但视图并不实际存在于数据库中。行和列的数据来自于定义视图的查询中所使用的表,并且还是在使用视图时动态生成的。

数据库中只存放了视图的定义,并没有存放视图中的数据,这些数据都存放在定义视图查询所引用的真实表中。使用视图查询数据时,数据库会从真实表中取出对应的数据。因此,视图中的数据是依赖于真实表中的数据的。一旦真实表中的数据发生改变,显示在视图中的数据也会发生改变。

视图可以从原有的表上选取对用户有用的信息,那些对用户没用,或者用户没有权限了解的信息,都可以直接屏蔽掉,作用类似于筛选。这样做既使应用简单化,也保证了系统的安全。

注意:因为视图时虚拟表,所以更新视图中的数据实际上是更新创建视图时用到的基本表中的数据。

MySQL 存储过程

存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。

存储过程是为了完成特定功能的SQL语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。

存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。

注意从项目管理版本管理以及可读性可维护性的角度讲并不推荐在项目中大量使用存储过程

  • 变量
    • 系统变量
      • 全局级别
      • 会话级别
    • 用户变量
  • 流程控制
  • 游标

Mysql partition 分区功能

https://www.cnblogs.com/mzhaox/p/11201715.html

水平分区的模式:

  • Range(范围) – 这种模式允许DBA将数据划分不同范围。例如DBA可以将一个表通过年份划分成三个分区,80年代(1980’s)的数据,90年代(1990’s)的数据以及任何在2000年(包括2000年)后的数据。
  • Hash(哈希)  – 这种模式允许DBA通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区。例如DBA可以建立一个对表主键进行分区的表。
  • Key(键值)    – Hash模式的一种延伸,这里的Hash Key是MySQL系统产生的。
  • List(预定义列表) – 这种模式允许系统通过DBA定义的列表的值所对应的行数据进行分割。例如:DBA建立了一个横跨三个分区的表,分别根据2004年2005年和2006年值所对应的数据。
  • Composite(复合模式) – 很神秘吧,哈哈,其实是以上模式的组合使用而已,就不解释了。举例:在初始化已经进行了Range范围分区的表上,我们可以对其中一个分区再进行hash哈希分区。

垂直分区(按列分):

举个简单例子:一个包含了大text和BLOB列的表,这些text和BLOB列又不经常被访问,这时候就要把这些不经常使用的text和BLOB了划分到另一个分区,在保证它们数据相关性的同时还能提高访问速度。

注意:partition操作会从底层存储文件就真的将数据进行分区,例如网上一个例子,创建no_part_tab和part_tab两张表,后者分区,灌入同样的数据,它们的底层存储文件为:

2008-05-24 09:23             8,608 no_part_tab.frm
2008-05-24 09:24       255,999,996 no_part_tab.MYD
2008-05-24 09:24        81,611,776 no_part_tab.MYI
2008-05-24 09:25                 0 part_tab#P#p0.MYD
2008-05-24 09:26             1,024 part_tab#P#p0.MYI
2008-05-24 09:26        25,550,656 part_tab#P#p1.MYD
2008-05-24 09:26         8,148,992 part_tab#P#p1.MYI
2008-05-24 09:26        25,620,192 part_tab#P#p10.MYD
2008-05-24 09:26         8,170,496 part_tab#P#p10.MYI
2008-05-24 09:25                 0 part_tab#P#p11.MYD
2008-05-24 09:26             1,024 part_tab#P#p11.MYI
2008-05-24 09:26        25,656,512 part_tab#P#p2.MYD
2008-05-24 09:26         8,181,760 part_tab#P#p2.MYI
2008-05-24 09:26        25,586,880 part_tab#P#p3.MYD
2008-05-24 09:26         8,160,256 part_tab#P#p3.MYI
2008-05-24 09:26        25,585,696 part_tab#P#p4.MYD
2008-05-24 09:26         8,159,232 part_tab#P#p4.MYI
2008-05-24 09:26        25,585,216 part_tab#P#p5.MYD
2008-05-24 09:26         8,159,232 part_tab#P#p5.MYI
2008-05-24 09:26        25,655,740 part_tab#P#p6.MYD
2008-05-24 09:26         8,181,760 part_tab#P#p6.MYI
2008-05-24 09:26        25,586,528 part_tab#P#p7.MYD
2008-05-24 09:26         8,160,256 part_tab#P#p7.MYI
2008-05-24 09:26        25,586,752 part_tab#P#p8.MYD
2008-05-24 09:26         8,160,256 part_tab#P#p8.MYI
2008-05-24 09:26        25,585,824 part_tab#P#p9.MYD
2008-05-24 09:26         8,159,232 part_tab#P#p9.MYI
2008-05-24 09:25             8,608 part_tab.frm
2008-05-24 09:25                68 part_tab.par

Mysql 窗口函数功能

MySQL从8.0开始支持窗口函数,这个功能在大多商业数据库和部分开源数据库中早已支持,有的也叫分析函数。 什么叫窗口? 窗口的概念非常重要,它可以理解为记录集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数。 对于每条记录都要在此窗口内执行函数,有的函数随着记录不同,窗口大小都是固定的,这种属于静态窗口;有的函数则相反,不同的记录对应着不同的窗口,这种动态变化的窗口叫滑动窗口。

窗口函数和普通聚合函数的区别:聚合函数是将多条记录聚合为一条;而窗口函数是每条记录都会执行,有几条记录执行完还是几条;聚合函数也可以用于窗口函数中

窗口函数是很多DBMS支持的特性

Mysql 派生表与临时表

派生表

派生表是从SELECT语句返回的虚拟表。派生表类似于临时表,但是在SELECT语句中使用派生表比临时表简单得多,因为它不需要创建临时表的步骤。

与子查询不同,派生表必须具有别名,以便稍后在查询中引用其名称。

派生表是从SELECT语句返回的虚拟表。派生表类似于临时表,但是在SELECT语句中使用派生表比临时表简单得多,因为它不需要创建临时表的步骤。

SELECT 
    column_list
FROM
    (SELECT 
        column_list
    FROM
        table_1) derived_table_name;
WHERE derived_table_name.c1 > 0;

临时表

在MySQL中,临时表是一种特殊类型的表,它允许您存储一个临时结果集,可以在单个会话中多次重用。

要创建临时表,只需要将TEMPORARY关键字添加到CREATE TABLE语句的中间。例如,以下语句创建一个临时表,按照收入存储前10名客户:

CREATE TEMPORARY TABLE top10customers
SELECT p.customerNumber, 
       c.customerName, 
       FORMAT(SUM(p.amount),2) total
FROM payments p
INNER JOIN customers c ON c.customerNumber = p.customerNumber
GROUP BY p.customerNumber
ORDER BY total DESC
LIMIT 10;

现在,可以从top10customers临时表中查询数据,例如:SELECT * FROM top10customers;

Mysql 公用表表达式common table expressions

https://blog.csdn.net/qq_45912025/article/details/125182001

对比子查询的优点

公用表表达式的作用是可以替代子查询,而且可以被多次引用。递归公用表表达式对查询有一个共同根节点的树形结构数据非常高效,可以轻松搞定其他查询方式难以处理的查询。

mysql对Nosql的支持

MySQL 从 5.7 版本开始提供 NoSQL 存储功能,在 8.0 版本中这部分功能也得到了更大的改进。该项功能消除了对独立的 NoSQL 文档数据库的需求,而 MySQL 文档存储也为 schema-less 模式的 JSON 文档提供了多文档事务支持和完整的 ACID 合规性。

 

Mysql架构

架构

处理流程:

  • Step1:处理连接
  • Step2:解析优化查询语句
  • Step3:交给存储引擎执行(查询文件系统)
  • Step4:响应
  • 解析器:对sql进行语法解析,生成语法树
  • 优化器:对sql进行优化,例如判断使用哪些索引,表连接顺序等等。。。最终生成执行计划

可以将mysql的架构分为3层:

  • 连接层
    • tcp连接(TCP连接池,复用TCP长连接)
    • 鉴权
    • 线程池
  • 服务层
    • SQL Interface
    • Cache & buffer(8.0中已经去掉这个模块,因为命中率太低,还要费力维护更新)
    • Parser
    • Optimizer
    • 执行器
  • 引擎层
    • 负责数据的存储和提取

执行流程

https://cloud.tencent.com/developer/article/1882003

解析器

  • 词法分析
  • 语法分析
    • you have an error in your SQL syntax
    • 如果没问题会生成一个语法树

优化器

使用什么索引,简化表达式,join顺序 —》 获得一个最优的执行计划

优化器的思路:

  • 物理查询优化
    • 通过索引和表连接进行优化
    • 例如表连接效率高于子查询:执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,这里多了一个创建和销毁临时表的过程。可以使用连接查询(JOIN)代替子查询,连接查询不需要建立临时表,因此其速度比子查询快。
  • 逻辑查询优化
    • 换一种计算方式
    • 对语句用更高效的方式重写

执行器

按照执行计划调用底层的存储引擎;

mysql底层存储引擎

MySQL 数据目录的物理结构和作用

配置文件:

Windows 和 Linux 下的 MySQL 配置文件的名字和存放位置都是不同的,WIndows 下 MySQL 配置文件是 `my.ini` 存放在 MySQL 安装目录的根目录下;Linux 下 MySQL 配置文件是 `my.cnf` 存放在 `/etc/my.cnf`、`/etc/mysql/my.cnf`

mysql自带数据库:

  • mysql:账户,权限,存储过程,事件等信息
  • Information_schema: 数据库的的元数据
  • performace_shcema:存储服务器运行过程中的状态信息,能够用来监控各类性能指标
  • sys:sys这个数据库主要是通过视图的形式把information_schema和performance_schema结合起来,让程序员可以更方便的了解MySQL服务器的一些性能信息。

http://c.biancheng.net/view/7911.html

  • db.opt:
    • 用来保存数据库的配置信息,比如该库的默认字符集编码和字符集排序规则。如果你创建数据库时指定了字符集和排序规则,后续创建的表没有指定字符集和排序规则,那么该表将采用 db.opt 文件中指定的属性。
    • 高版本这个文件内容合并到数据文件中了
  • .frm文件:
    • 在 MySQL 中建立任何一张数据表,其对应的数据库目录下都会有该表的 .frm 文件。.frm文件用来保存每个数据表的元数据(meta)和表结构等信息。数据库崩溃时,可以用 .frm 文件恢复表结构。
    • .frm 文件跟存储引擎无关,任何存储引擎的数据表都有 .frm 文件,命名方式为表名.frm,如 users.frm。
    • MySQL 8.0 版本开始,frm 文件被取消,MySQL 把文件中的数据都写到了系统表空间。通过利用 InnoDB 存储引擎来实现表 DDL 语句操作的原子性(在之前版本中是无法实现表 DDL 语句操作的原子性的,如 TRUNCATE 无法回滚)。
  • .MYD和.MYI
    • .MYD 理解为My Data,用于存放 MyISAM 表的数据。
    • .MYI  理解为My Index,主要存放 MyISAM 表的索引及相关信息。
  • .ibd:独立表空间
    • 对于 InnoDB 存储引擎的数据表,一个表对应两个文件,一个是 *.frm,存储表结构信息;一个是*.ibd,存储表中数据。
    • .ibd和.ibdata:.ibd 和 .ibdata 都是专属于 InnoDB 存储引擎的数据库文件。当采用共享表空间时,所有 InnoDB 表的数据均存放在 .ibdata 中。所以当表越来越多时,这个文件会变得很大。相对应的 .ibd 就是采用独享表空间时 InnoDB 表的数据文件。
    • ibdata系统表空间,位于数据库文件上层一个共享的文件,用于早期mysql存储表数据
  • 视图存储:
    • 是一个虚拟的表,所以只有一个frm文件,没有ibd文件

注意

  • innodb中,数据和索引聚合存储
  • myisam中,数据和索引分开存储

存储引擎

直接负责数据的提取和写入

InnoDB (高版本默认使用)MyISAM
支持外键(但是不推荐使用外键,效率问题)不支持
支持事务,commit & rollback不支持
除了insert和select操作之外,还有频繁的update,delete,那么应该使用InnoDB,效率更高只有频繁的insert和select操作时,效率更高,访问速度更快,适用只读业务或者以读为主的业务
对性能进行了优化,擅长处理数据量大的表,并发量大擅长处理数据量较小的表,不支持高并发
支持行锁支持表锁(所以并发性不好)
崩溃可以安全恢复崩溃不可以进行安全恢复
索引与数据共同存储索引和数据分别存储

InnoDB与MyISAM最大的区别就是对事务和行锁的支持

InnoDB的索引实现

加快搜索速度,如果不借助索引,所有的查询都需要做全表的扫描,从而造成大量的磁盘IO次数,查询效率非常低。

InnoDB采用B+Tree

优点:

  • 降低磁盘IO次数
  • 通过创建“唯一索引”,可以保证数据的唯一性
  • Gourpby 和orderby 可以利用索引降低查询时间
  • 等等

缺点:

  • 维护索引需要时间,降低了更新/写入/删除的时间
  • 占用额外的磁盘空间

当数据库一条记录里包含多个字段时,一棵B+树就只能存储主键,如果检索的是非主键字段,则主键索引失去作用,又变成顺序查找了。这时应该在第二个要检索的列上建立第二套索引。  这个索引由独立的B+树来组织。有两种常见的方法可以解决多个B+树访问同一套表数据的问题,一种叫做聚簇索引(clustered index ),一种叫做非聚簇索引(secondary index)。这两个名字虽然都叫做索引,但这并不是一种单独的索引类型,而是一种数据存储方式。对于聚簇索引存储来说,行数据和主键B+树存储在一起,辅助键B+树只存储辅助键和主键,主键和非主键B+树几乎是两种类型的树。

InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用”where id = 14″这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。

MyISM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。

 

聚簇索引优势

我们重点关注聚簇索引,看上去聚簇索引的效率明显要低于非聚簇索引,因为每次使用辅助索引检索都要经过两次B+树查找,这不是多此一举吗?聚簇索引的优势在哪?

  1. 由于行数据和叶子节点存储在一起,这样主键和行数据是一起被载入内存的,找到叶子节点就可以立刻将行数据返回了,如果按照主键Id来组织数据,获得数据更快。
  2. 辅助索引使用主键作为”指针” 而不是使用地址值作为指针的好处是,减少了当出现行移动或者数据页分裂时辅助索引的维护工作,使用主键值当作指针会让辅助索引占用更多的空间,换来的好处是InnoDB在移动行时无须更新辅助索引中的这个”指针”。也就是说行的位置(实现中通过16K的Page来定位,后面会涉及)会随着数据库里数据的修改而发生变化(前面的B+树节点分裂以及Page的分裂),使用聚簇索引就可以保证不管这个主键B+树的节点如何变化,辅助索引树都不受影响。

页是mysql中磁盘和内存交换的基本单位,也是mysql管理存储空间的基本单位。(磁盘IO的基本单位)

同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 innodb_page_size 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同。

一次最少从磁盘读取16KB内容到内存中,一次最少把内存中16KB内容刷新到磁盘中,当然了单页读取代价也是蛮高的,一般都会进行预读

系统的一个磁盘块的存储空间往往没有这么大,所以InnoDB每次申请磁盘空间时都会是多个地址连续磁盘块来达到页的大小16KB。在查询数据时一个页中的每条数据都能定位数据记录的位置,这会减少磁盘 I/O 的次数,提高查询效率。InnoDB存储引擎在设计时是将根节点常驻内存的,力求达到树的深度不超过 3,也就是说I/O不超过3次。

页结构

(0,1,2,3,是record type,1是目录,0是数据)

同样可以将页分为目录页和数据页

  • 上图对应的就是ibd文件;
  • 页内是按照主键递增的单向链表
  • 页间是双向链表
  • 叶子结点是数据页,非叶子节点是目录页
  • 插入一个主键数据可能会引发数据结构调整(页分裂),拖慢速度,最好以一个自增的ID作为主键,不建议用MD5,UUID,hash之类作为主键
  • 更新主键代价大,一般数据库都不允许
  • 二级索引查找需要两次索引查找

理解InnoDB的实现不得不提Page结构,Page是整个InnoDB存储的最基本构件,也是InnoDB磁盘管理的最小单位,与数据库相关的所有内容都存储在这种Page结构里。Page分为几种类型,常见的页类型有数据页(B-tree Node)Undo页(Undo Log Page)系统页(System Page) 事务数据页(Transaction System Page)等。单个Page的大小是16K(编译宏UNIV_PAGE_SIZE控制),每个Page使用一个32位的int值来唯一标识,这也正好对应InnoDB最大64TB的存储容量(16Kib * 2^32 = 64Tib)。一个Page的基本结构如下图所示:

我们重点关注和数据组织结构相关的字段:Page的头部保存了两个指针,分别指向前一个Page和后一个Page,头部还有Page的类型信息和用来唯一标识Page的编号。根据这两个指针我们很容易想象出Page链接起来就是一个双向链表的结构。

再看看Page的主体内容,我们主要关注行数据和索引的存储,他们都位于Page的User Records部分,User Records占据Page的大部分空间,User Records由一条一条的Record组成,每条记录代表索引树上的一个节点(非叶子节点和叶子节点)。在一个Page内部,单链表的头尾由固定内容的两条记录来表示,字符串形式的”Infimum”代表开头,”Supremum”代表结尾。这两个用来代表开头结尾的Record存储在System Records的段里,这个System Records和User Records是两个平行的段。InnoDB存在4种不同的Record,它们分别是1主键索引树非叶节点 2主键索引树叶子节点 3辅助键索引树非叶节点 4辅助键索引树叶子节点。这4种节点的Record格式有一些差异,但是它们都存储着Next指针指向下一个Record。后续我们会详细介绍这4种节点,现在只需要把Record当成一个存储了数据同时含有Next指针的单链表节点即可。

联合索引

基于多个字段建立的索引

深入探讨文件与页的关系

TBD:如何在文件中存储页以及页之间的关系

行格式

在BufferPool中,是按照页的形式来存放的。但是数据在表中是一行行的存储的,那么这些数据又是怎样的格式?

就是页面中的一条行记录的格式

有的行格式设计的不紧凑,同样一条记录会占用更多的磁盘空间,意味着一个页里包含的记录就少了,进而会影响DML操作的性能,查询可能需要更多的磁盘IO。有的行格式还会对数据做压缩处理,磁盘IO的效率高了,但是会消耗更多的CPU资源。所以,行格式的选择对数据读写的效率是有影响的,具体如何选择,需要根据场景而定。MySQL5.7版本,默认使用DYNAMIC行格式。

记录在磁盘上的存放方式被称为行格式,InnoDB存储引擎中有4种不同类型的行格式,Compact、Redundant、Dynamic和Compressed。

表空间/段/区/页/行

区(Extent)是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。因为在InnoDB中页的大小为16KB,所以一个区的大小是64*16KB=1MB。

段(Segment)由一个或者多个区组成,区在文件系统中是一个连续分配的空间(在InnoDB中是连续的64个页),在段中不要求区与区是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段的形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段吗,创建一个索引时会创建一个索引段。

表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间可以有多个或者一个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。

索引的分类

  • 普通索引、
  • 唯一索引、
  • 主键索引、
  • 组合索引、
  • 全文索引

https://www.php.cn/mysql-tutorials-418140.html

mysql事务特性

MySQL事务机制的核心是两个日志文件:

  • redo log(重做日志)
  • undo log(回滚日志)

ACID

  • Atomicity:
    • 事务不可分割,不会出现执行到中间被中断的状态
  • Consistency:
    • 从合法状态向合法状态的转换,不会以错误的中间状态结束;
    • 区分分布式系统的一致性
  • Isolation:
    • 事务互相之间没有干扰
    • 存在多种隔离级别
  • Duribility
    • 数据库一旦被提交,改动就是永久性的,服务器故障不应该影响数据
    • 持久性通过事务日志来保证,包括重做日志和回滚日志

事务要么commit要么rollback

显式事务&隐式事务

事务主要针对DDL操作,某些数据库系统(例如 MySQL)不支持在事务中运行 DDL,因此别无选择,只能将三个操作(ALTER、ALTER 和然后 UPDATE)作为三个不同的操作运行:如果其中任何一个失败,则无法恢复并回到初始状态。例如,在一个事务中发出了两个 DDL 语句,然后我们回滚了该事务。 MySQL 在任何时候都没有输出任何错误,让我们认为它没有改变我们的表。然而,当检查数据库的模式时,我们可以看到没有任何东西被回滚。 MySQL 不仅不支持事务性 DDL,而且它也没有明确表明自己没有 rollback!

  • 显式
    • 以start transaction 或者 begin开始
    • 以rollback / commit结束
    • 可以在事务中设置savepoint,保存点,rollback可以回滚到指定savepoint,然后继续执行其他操作(rollback savepoint不是结束)
  • 隐式
    • Autocommit = on;对于数据库默认就是on
    • 此时所有的DML操作都是独立的事务
    • 关闭autocommit,那么执行操作如果不手动commit,那么其他客户端是看不到的

注意有一些操作会隐式的提交前面的数据

事务分类

  • 扁平事务( Flat Transactions)
  • 带有保存点的扁平事务(Flat Transactions with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)

https://blog.csdn.net/c1776167012/article/details/121257852

隔离级别

事务的隔离性是由锁机制来实现的而事务的ACD是由redo/undo日志来保证的

mysql数据库事务的隔离级别有4个,而默认的事务处理级别就是【REPEATABLE-READ】,也就是可重复读。

脏写:最基本的保障

  1. 读未提交
    1. 没有commit,也可以被别人读到
    1. 不加锁
    1. -》存在脏读问题
  2. 读已提交
    1. Commit之后,就能被别人读到
    1. 没有引入写锁
    1. -》不可重复读问题
  3. 可重复读
    1. 一个方案是引入行级别的的写锁
    1. 但是注意,对行数据的写入操作依然可以commit,并没有阻塞或者失败,但是这个commit的结果并没有呈现在别的事务之中????这个底层究竟是怎么实现的
    1. -》幻读问题:
  4. 串行化
    1. 引入表级别/或者行级别(视不同的情况而定)的写锁  
    1. 数据应该不能插入进去,会让请求失败,或者阻塞在那里 

如何确定自己数据库表的隔离级别

  • 隔离级别越高并发行越差
  • 隔离级别越低数据一致性越差
  • 根据自己的业务模型看脏读不可重复读幻读是否会给我们的业务带来影响如果会则设定更高的的隔离级别如果不会则可以设定更低级别的隔离级别

Mysql 设置事务隔离级别是针对不同的事务之间的可以设定以下两个范围

  • global级别:
  • session级别:

Mysql 锁机制总结

https://zhuanlan.z hihu.com/p/29150809

锁是计算机协调多个进程/线程并发访问某一个资源的机制。数据库中,为了保证数据一致性,同样需要对并发操作进行控制。它也是隔离级别的实现基础。

同时,锁冲突也是影响数据库并发性能的一个重要因素因此要合理的设计锁的粒度

并发事务情况分类

  • 读-读:
    • 没有风险
  • 写-写:
    • 可能会出现脏写问题,即并发写,一个commit,一个rollback,导致commit的结果被回滚
    • 必须通过锁来实现排队执行 
    • 锁是内存中的一个数据结构,与某条记录相关联 
  • 读-写:
    • 可能出现脏读,不可重复读,幻读

读写并发问题的解决方案

方案一读用MVCC写操作加锁

核心是读借助readview类似一个快照): 查询语句会从readview里面读取readview生成之前的未提交的事务或者之后提交的事务是查询不到的

  • 脏读如何解决:read committed级别下,readview生成之前的未提交的事务读不到
  • 不可重复读如何解决:repeatable read级别下,readview生成之后提交的事务读不到
  • 幻读如何解决:mysq lrepeatable read级别下,readview生成之后提交的事务读不到(不需要到serializable级别)

方案二写都采用加锁的方式

成本更高,读锁与写锁协同工作;

脏读和不可重复读可以在记录级别加锁,幻读麻烦一点,可能需要别的级别的锁进行控制

共享锁与排他锁

共享锁(S锁shared lock)又称为读锁:由非更新(读取)操作创建的锁。其他用户可以并发读取数据,但任何事务都不能获取数据上的排它锁,直到已释放所有共享锁。,若事务T对数据对象A加上S锁,则事务T只能读A, 不能修改A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

排他锁(X锁,exclusive lock),又称为写锁、独占锁,是一种基本的锁类型。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

功能读不阻塞读写写写会阻塞

对于InnoDB来说/写锁都可以细分为表级别和行级别

Mysql高版本中还支持给读操作加写锁

SELECT … FOR UPDATE;

Mysql里当一个操作需要获取某个锁,但是获取不到时,会发生阻塞现象;超时之后会失败退出;如果加了nowait关键字,则直接失败;如果加了skip locked,则会跳过被锁定的数据。

Insert操作通过隐式锁来保护这条新插入的记录在提交前不被访问

不同粒度表级锁页级锁行锁

粒度越低 -》 并发度越好,但是性能开销越大

表级锁

表级别读写锁

Innodb,myisam都提供提供的表级别的S和X锁,不过一般不会被用到;某些DDL操作,例如alter table, drop table等可能会触发表级锁

myisam在s elect语句前,会给所有的表加读锁,增删改操作之前,会给涉及的表加写锁;innodb不会加表级别的读锁和写锁

表级别的意向锁intention lock

协调行锁和表锁,支持多粒度(表/行)锁共存;不与行锁冲突的表锁

  • 意向共享锁
    • 事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁
    • 事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
    • 例如,我们给某一行加上排他锁,那么数据库给表加上意向排他锁;如果再有人想获取表级别的排他锁,只需要区检查以下意向排他锁就ok了

IX,IS是表级锁,但是不与行级别的X,S锁冲突,只会与表级别的X,S冲突;

表级别的自增锁

对于多个事务对auto_increment操作产生的冲突的解决;当我们向一个有auto increment关键字的主键插入值时,每条语句都要对这个表锁进行竞争。

不过高版本已经不用这个锁了,使用另外的机制保证唯一与递增性;

元数据锁MDL

当一个表做增删改查时,加MDL读锁;当对表结构做修改时,加MDL写锁

行级锁

MyiSam不支持行级锁只有innodb支持

粒度小锁冲突发生概率低并发度高频繁加锁有开销可能出现死锁问题

记录锁record locks

同样分为S/X型,就是针对某一条记录加锁

间隙锁Gap Locks

前面提到我们解决幻读有两个解决方式,一种是mvcc,另一种就是间隙锁;解决的问题是,如何给不存在的记录加锁;

例如记录的主键分布为:1,3,6,8,那么给记录6加gap lock,就会阻塞给3到6之间插入记录的操作

如何获得间隙锁:当你尝试去获取一条不存在的记录的锁时,就会获得间隙锁,间隙锁的s/x效果是一样的,没有区别,并且不限制加锁次数

效果:可能会阻塞某一些insert操作

间隙锁触发死锁:两个事务都锁住同一个区间,并且执行insert操作,就会导致死锁,这时候数据库会报deadlock的错误,并且终止回滚某一个事务。

临键锁next key locks

记录锁+间隙锁(闭区间,包括边界)

如何获得?Select * from xx where id > 1 and id <=5 ->就获得了一个(1,5]的临键锁。

插入意向锁

页级锁

在页级别的粒度上加锁;一个页会包含多条记录,开销,性能都介于行锁,表锁之间;

什么时候会触发页锁:行锁的数量超过了某个阈值,就会进行锁升级,用更大粒度的页锁来代替行锁;页锁数量到一定规模,也会进行升级,升级为表锁。

乐观锁 & 悲观锁

悲观通过锁机制主动保证数据一致性主动上锁阻塞其他线程使用完后开锁

悲观锁例子

网上商城为了防止超卖,需要通过事务来保证“查询,生成订单,减库存”等一系列操作的事务性,不然就会出现只有一定数量的商品,却卖掉了超过这个数量商品的情况。

缺点是,长事务可能会带来非常大的性能开销,影响吞吐等等;

乐观认为冲突是小概率事件不对数据上锁通过程序来保证数据的一致性

乐观锁实现版本号机制或者CAS机制

版本号机制:

给版本加一个version,一个事务  读的时候拿到某个version,在写时带着这个version去,如果version小于当前实际version,那么说明就已经有其他事务对version修改过了,这个操作就会失败。

例子

cas机制:

在java concurrent包中很多并发行都是通过CAS机制来实现的

https://zhuanlan.zhihu.com/p/101430930

隐式锁 & 显式锁

和java里面的不同,Java中隐式锁:synchronized;显式锁:lock

显式锁

隐式锁

前面讲,在insert的时候,如果间隙加了gap锁,那么insert操作会阻塞,并且给间隙加上插入意向锁;除此之外,是不需要给insert操作加锁的;可是如果在刚刚插入之后(事务没有结束),就有另外的事务要读/更新这条记录,怎么办?

一个事务在对新插入的记录可以不显示的加锁(生成锁结构),但是由于事务id的存在,相当于加了一个隐式锁(记录的事务id和当前事务相同),别的事务在尝试加S/X锁时,才会给之前的事务创建一个锁,这时候才从隐式锁转换成显式锁

在查看表的锁列表时,隐式锁是看不到的;

全局锁

需要使得数据库处于只读状态,就可以加数据库级别的锁(不可以DDL,DML等 操作);应用场景:数据库逻辑备份。

死锁

处理方式:

  • 等待事务超时,回滚;默认超时时间50s,可以设置更小,但是容易误伤
  • 死锁检测机制:mysql里面会检测出死锁,然后中断并回滚某个事务;默认是打开的

mysql日志文件

redo日志

  • 重做日志,恢复相关页操作,保证事务的持久性;
  • 事务的持久性针对:已经提交的事务
  • 记录的是物理级别的页修改操作,例如页号,偏移量,写入xx数据,保证数据的可靠性。
  • 应用场景:我们的修改操作,修改的是内存中加载的页文件,它会按照固定频率刷到磁盘上,但是一旦宕机,内存中的修改就会丢失
  • 重启服务,会首先加载redo log把丢失的数据刷盘
  • 这种思想在分布式系统里面叫做WAL:Write-Ahead-Logging
  • 日志顺序追加,效率非常高,
  • Redo log也有内存buffer(redo log buffer),然后写入磁盘其实也是先写到page cache中,再fsync到磁盘的过程(redo log file),这个刷盘策略也是我们非常关注的
    • 策略0:系统默认1s 刷盘一次,效率最高
    • 策略1:每次事务提交都会把log buffer中的日志fsync到log file中
    • 策略2: 操作系统自己决定什么时候从page cache 刷到磁盘,效率居中
  • redo log实际上记录数据页的变更,而这种变更记录是没必要全部保存,因此redo log实现上采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。

undo日志

  • 更新数据的前置操作就是写入undo log
  • 回滚日志,回滚到某个特定版本,用来保证事务的原子性,一致性
  • 记录逻辑操作的日志,需要执行一条与至相反的操作
  • 不是redo日志的顺序追加格式了,每一个事务都有对应的回滚段来存储信息
  • 应用场景:事务执行到一半发生服务器故障或者用户手动输入rollback等
  • 事务在commit之后,undo log还会保留一段时间;这是因为undo日志还有一个非常重要的功能:事务提交之后,其他事务可以通过undo log来得到之前的版本,这是否就是之前的‘可重复读’遇到问题的答案??MVCC
  • Undo 日志不像redo日志那样有严格落盘的限制其实理解成内存里记录一个事务执行前的状态的数据结构更合适

分类:

  • Insert undo log:insert的数据具有隔离性,只对事务本身可见,所以在事务提交后可以直接清除undo log
  • Update undo log:delete/update操作,例如可重复读中的问题,undo log需要提供MVCC机制,所以不能立刻删除,需要等待一段时间;

binlog日志

包含了所有数据库执行的DDL和DML等数据库更新语句

应用

在实际应用中,binlog的主要使用场景有两个,分别是主从复制和数据恢复。

  • 主从复制:在Master端开启binlog,然后将binlog发送到各个Slave端,Slave端重放binlog从而达到主从数据一致。
  • 数据恢复:通过使用mysqlbinlog工具来恢复数据。

So now let me start with what is happening on the master. For replication to work, first of all master needs to be writing replication events to a special log called binary log. This is usually very lightweight activity (assuming events are not synchronized to disk), because writes are buffered and because they are sequential. The binary log file stores data that replication slave will be reading later.

Whenever a replication slave connects to a master, master creates a new thread for the connection (similar to one that’s used for just about any other server client) and then it does whatever the client – replication slave in this case – asks. Most of that is going to be (a) feeding replication slave with events from the binary log and (b) notifying slave about newly written events to its binary log.

Slaves that are up to date will mostly be reading events that are still cached in OS cache on the master, so there is not going to be any physical disk reads on the master in order to feed binary log events to slave(s). However, when you connect a replication slave that is few hours or even days behind, it will initially start reading binary logs that were written hours or days ago – master may no longer have these cached, so disk reads will occur. If master does not have free IO resources, you may feel a bump at that point.

  • binlog是通过追加的方式进行写入的,可以通过max_binlog_size参数设置每个binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

binlog刷盘时机

  • 对于InnoDB存储引擎而言,只有在事务提交时才会记录biglog,此时记录还在内存中,那么biglog是什么时候刷到磁盘中的呢?mysql通过sync_binlog参数控制biglog的刷盘时机,取值范围是0-N:
    • 0:不去强制要求,由系统自行判断何时写入磁盘;
    • 1:每次commit的时候都要将binlog写入磁盘;
    • N:每N个事务,才会将binlog写入磁盘。
  • 从上面可以看出,sync_binlog最安全的是设置是1,这也是MySQL 5.7.7之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。

binlog日志有三种格式

分别为

  • STATEMENT: 是什么SQL语句,此时如果使用系统变量,会造成主从执行不同
  • ROW:具体的修改是什么,使用系统变量,主从也会一致
  • MIXED:结合statement和row,如果有函数,则用row,否则用statement

过期

MySQL expire_logs_days 参数用于控制Binlog文件的保存时间,当Binlog文件存在的时间超过该参数设置的阈值时,Binlog文件就会被自动清除,该参数的时间单位是天,设置为0,表示Binlog文件永不过期,即不自动清除Binlog文件。在MySQL 8.0 版本,该参数被废弃,使用新的参数binlog_expire_logs_seconds代替,新参数的时间粒度是秒,能够更加灵活的控制Binlog文件过期时间。

如果过期清理后那么没有binlog如何做同步以及数据恢复??

通常是先以某个时间点做全量快照恢复,再做binlog的指定范围的恢复。

查看

  • 通过 mysqlbinlog命令进行二进制文件的查看
  • show binlog events

数据恢复

  • 同样使用mysqlbinlog命令
  • 通常需要定位binlog里面恢复的position范围或者start/stop 的时间

全量备份快照

通常使用Mysqldump

两节点提交

如果出现redo写成功,但是写binlog失败

就会出现以下情况

为了防止这种情况产生(redo log 和bin log的不一致),于是使用两阶段提交:

Redo log与binlog区别

一个事务只有提交时才会写入到bin log中;一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入; 但是执行过程中会不断向redo log中写入;

  • Redo log侧重于引擎级别的崩溃恢复;binlog侧重数据的同步
  • redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  • binlog 会记录表所有更改操作,包括更新删除数据,更改表结构等等,主要用于人工恢复数据,而 redo log 对于我们是不可见的,它是 InnoDB 用于保证 crash-safe 能力的,也就是在事务提交后MySQL崩溃的话,可以保证事务的持久性,即事务提交后其更改是永久性的。==一句话概括:binlog 是用作人工恢复数据,redo log 是 MySQL 自己使用,用于保证在数据库崩溃时的事务持久性。==
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

MVCC特性

https://zhuanlan.zhihu.com/p/66791480

为什么需要MVCC呢?数据库通常使用锁来实现隔离性。最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据。当然快照是一种概念模型,不同的数据库可能用不同的方式来实现这种功能。

InnoDB中通过undo log实现了数据的多版本,而并发控制通过锁来实现。

刷脏

脏页:此时内存中的数据和磁盘中的数据是不一致的,不一致的这个数据页就被称为“脏页”。

刷脏:既然磁盘中的数据和内存中的数据有不一致的,那肯定就涉及到将内存中的数据同步到磁盘中,那这个过程就被称为“刷脏”。

刷脏页一般发生在commit之后,redo和binlog提交之后。innodb可以根据脏页在bufferpool中的水位强制刷脏页。Adaptive Flushing根据数据库负载情况调整刷每秒应该刷多少脏页。

其他日志

  • 慢查询日志
  • 通用查询日志:所有跟请求,处理,响应相关的通用信息
  • 错误日志
  • 中继日志:
  • 数据定义语句日志

主从架构与数据同步

功能

主从架构(集群)可以做到:

  • 读写分离,提高数据库处理并发的能力
  • 数据备份
  • 高可用性(主从切换)

架构

中继日志:只在从服务器上存在,从主服务器读到的二进制文件存储在本地;从服务器数据的同步是通过读取中继日志;

主从复制原理

存在问题延迟造成一致性问题

主节点数据落盘到同步到从节点,可能会经历一段延迟,这段延迟会造成读写的不一致性;

  • 读写都在主库,则不存在一致性问题,从库只是备份
  • 弱一致性-异步复制:主commit之后,写完binlog,就给client返回成功,不管从库是否同步完成
  • 半同步复制:保证至少一个从库同步完成,再返回客户端,如果把同步数量设定为从库数量,那么就变成完全同步的复制,就一定可以保证强一致性了
  • 组复制(Group Replication):基于paxos协议的状态机复制

推还是拉

  • 当从库开始start slave后,主动发起tcp连接,使用一个高位的端口去访问主的3306
  • 接下来从库请求登记到主库上去,当成功登记后,从库请求主库发送指定位置的binlog,
  • 当主从同步建立连接后,当主库有更改,会主动推送给从库

读写分离的实现

  • 可以通过自己实现,直接访问主/从节点
  • 可以通过第三方的中间件来实现
    • Cobar
    • Mycat
    • mysqlRoute
    • 等等

mysql鉴权授权机制

用户,角色,权限

调优思路

整体思路

  • Step1: SQL,表结构和索引调优
  • Step2: 加缓存(Redis)
  • Step3: 读写分离

慢查询日志

SHOW STATUS

用来查看数据库服务器的性能参数,执行频率等等信息;

  • Connections:连接次数
  • Uptime:服务器上线时间
  • LAST_QUERY_COST:查看查询需要检索多少个页

Explain

查看SQL语句的执行计划

Profile

Show profile查看SQL每一个步骤的时间成本

SQL执行时间长的优化思路

  • 索引设计是否可以优化
  • 是否有过多表的join操作
  • 数据表设计是否可以优化 

达到系统瓶颈

  • 读写分离
  • 分库分表
    • 垂直分库
    • 垂直分表
    • 水平分表

索引优化

如何避免索引失效的情况

https://cloud.tencent.com/developer/article/1992920

例如:联合索引不满足最左匹配原则,使用了select * 等等

查询优化

关联查询优化Join

外连接:左,右表创建索引都能够帮助加快查询速度

内连接:对于内连接来说,查询优化器有权决定哪张表是驱动表,哪张表是被驱动表

join的原理优化器会帮助我们优化顺序内外连接都会进行优化例如有的外连接在底层会被改成内连接

  • 表之间数据的嵌套循环匹配
    • 简单嵌套循环连接
    • 索引嵌套循环连接要求被驱动表有索引
    • 块嵌套循环连接不存在索引一口气加载一块驱动表中的数据(join buffer) ,而不是逐条
    • 在mysql8之后开始使用hash join不再使用嵌套循环在没有索引的情况下效率更高
  • 内连接AB两表都没有索引则小表驱动大表
  • 内连接如果B表有索引A没有则B作为被驱动表
  • 内连接都有索引则小表驱动大表

如何进行子查询优化

如何进行排序优化

如何进行groupby优化

如何优化limit分页查询

覆盖索引

非聚簇索引(二级索引)中如果已经包括了select中所有需要的所有字段,那么就可以不需要回表操作,这时称其为覆盖索引。

为什么有时候在查询时明明有索引执行计划中却不使用如果发现使用索引的效率不能够提升效率那么优化器就有可能不使用索引

优点:

  • 避免索引二次查询(回表)
  • 把随机IO变成顺序IO

Reference

  1. 关系型数据库 VS 非关系型数据库:https://zhuanlan.zhihu.com/p/78619241
  2.  
发表在 数据库 | 5条评论

计算机行业择业的一些思考

前几天跟朋友聊了一下择业的事情,对他的一个观点印象深刻,姑且称之为边缘理论:即在某个领域里潜在的机会往往藏在这个领域的边缘地带,颠覆性的突破点往往也会从这些边缘地带诞生。

近些年计算机的发展中心集中在移动互联网,中国尤其如此。其核心特点就是商业需求驱动的业务开发:搜索,电商,支付,社交。。。等等,这些都是能够快速直接带来利益的领域。

什么是如今计算机的行业的边缘?

  • 区块链
  • AR/VR
  • 人工智能:无人驾驶,机器学习,深度学习(深度神经网络),图像识别,nlp。。等
  • IOT
  • 大数据:OLAP,OLTP,数据仓库,数据湖,数据挖掘,数据分析,数据处理。。等等
  • 云计算/云服务:AWS,GCE,ali,CNCF上定义的技术栈:docker,k8s,prometheus。。等等
  • 基础软件:操作系统,数据库,中间件
  • 开发流程:DevOps,CI/CD(jenkins),代码自动检查工具(Sonar),代码仓库,代码版本管理(SVN/Git),代码评审,知识共享系统,协同办公,敏捷开发工具。。等

这些领域相关人才缺少,学习成本高,有着以下一些共同特点:

  1. 相比业务开发,往往涉及到更底层的研究,处理的问题更纯粹;譬如一个分布式有状态系统,一致性问题就是它们共有的难点。
  2. 业务创新,通常是基于这些领域做二次创新;这些边缘领域的创新,有的是从0开始,更富有挑战。
  3. 不仅仅需要领域内的知识储备,同时也需要应用层面开发的经验,因为最终这些领域都是为业务落地服务的。
  4. 开源和社区在这其中扮演了非常重要的角色。

中国虽然在商业驱动的移动互联网发展得如火如荼,但是在上面的很多领域还是落后的,譬如基础软件领域,很多还是被国外垄断;但是最近中国的企业也在努力往前推进,譬如阿里的龙蜥操作系统,华为欧拉操作系统,pingcap的tiDB等等。

选择一个有潜力的领域和合适的公司,不仅仅关乎了收入,个人成长,更重要的关乎个人价值的实现;把短暂的职业人生花在有价值的事情上,这是更为重要的一点。

 

发表在 闲扯淡 | 3条评论

(TBD)关于ES计算存储分离

发表在 ElasticSearch | 105条评论

(In Progress)关于ES分布式设计的关键点

ES 分布式设计

以下是一些我认为ES分布式设计中比较重要的设计点,里面采用了一些有代表性的分布式系统设计的思想,分别谈谈我的理解

Master Node & Data Node

Leader Election

ES7.0之前实际上使用的是bully算法进行选主(zen discovery),之后使用基于Raft的选主策略。

Event-Driven/State-Watch

Primary-Replicas

(Strong-Consistency)

Elasticsearch’s data replication model is based on the primary-backup model and is described very well in the PacificA paper of Microsoft Research. That model is based on having a single copy from the replication group that acts as the primary shard. The other copies are called replica shards. The primary serves as the main entry point for all indexing operations. It is in charge of validating them and making sure they are correct. Once an index operation has been accepted by the primary, the primary is also responsible for replicating the operation to the other copies.

Sharding

Routing

2PC

Near-Real-Time

refresh

Data Persistence

Flush

Quorum

  • 脑裂问题避免
  • 写入过程一致性保障

TransLog

Primary Term

Sequence Number

Checkpoint(global/local)

Translog Retention

Soft Delete

Retention Leases

一些参考文档

https://www.elastic.co/de/blog/elasticsearch-sequence-ids-6-0

https://www.gushiciku.cn/pl/p4zi

https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-history-retention.html

发表在 ElasticSearch | 20条评论