ARKit:增强现实技术在美团到餐业务的实践

ARKit:增强现实技术在美团到餐业务的实践

原创: 曹宇 美团技术团队 __

总第282篇

2018年 第74篇

前言

增强现实(Augmented Reality)是一种在视觉上呈现虚拟物体与现实场景结合的技术。Apple 公司在 2017 年 6 月正式推出了
ARKit,iOS 开发者可以在这个平台上使用简单便捷的 API 来开发 AR 应用程序。

本文将结合美团到餐业务场景,介绍一种基于位置服务(LBS)的 AR 应用。使用 AR
的方式展现商家相对用户的位置,这会给用户带来身临其境的沉浸式体验。下面是实现效果:

图1 实现效果图

项目实现

iOS 平台的 AR 应用通常由 ARKit 和渲染引擎两部分构成:

图2 AR 应用的整体架构

ARKit 是连接真实世界与虚拟世界的桥梁,而渲染引擎是把虚拟世界的内容渲染到屏幕上。本部分会围绕这两个方面展开介绍。

ARKit

ARKit 的 ARSession 负责管理每一帧的信息。ARSession 做了两件事:拍摄图像并获取传感器数据;对数据进行分析处理后逐帧输出。如下图:

图3 ARKit 结构图

设备追踪

设备追踪确保了虚拟物体的位置不受设备移动的影响。在启动 ARSession 时需要传入一个 ARSessionConfiguration
的子类对象,以区别三种追踪模式:

  • ARFaceTrackingConfiguration

  • ARWorldTrackingConfiguration

  • AROrientationTrackingConfiguration

其中 ARFaceTrackingConfiguration 可以识别人脸的位置、方向以及获取拓扑结构。此外,还可以探测到预设的 52
种丰富的面部动作,如眨眼、微笑、皱眉等等。ARFaceTrackingConfiguration 需要调用支持 TrueDepth
的前置摄像头进行追踪,显然不能满足我们的需求,这里就不做过多的介绍。下面只针对使用后置摄像头的另外两种类型进行对比。

ARWorldTrackingConfiguration

ARWorldTrackingConfiguration 提供 6DoF(Six Degree of Freedom)的设备追踪。包括三个姿态角
Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滚角),以及沿笛卡尔坐标系中 X、Y 和 Z 三轴的偏移量:

图4 6DoF

不仅如此,ARKit 还使用了 VIO(Visual-Inertial
Odometry)来提高设备运动追踪的精度。在使用惯性测量单元(IMU)检测运动轨迹的同时,对运动过程中摄像头拍摄到的图片进行图像处理。将图像中的一些特征点的变化轨迹与传感器的结果进行比对后,输出最终的高精度结果。

从追踪的维度和准确度来看,ARWorldTrackingConfiguration
非常强悍。但如官方文档所言,它也有两个致命的缺点:

  • 受环境光线质量影响

  • 受剧烈运动影响

由于在追踪过程中要通过采集图像来提取特征点,所以图像的质量会影响追踪的结果。在光线较差的环境下(比如夜晚或者强光),拍摄的图像无法提供正确的参考,追踪的质量也会随之下降。

追踪过程中会逐帧比对图像与传感器结果,如果设备在短时间内剧烈的移动,会很大程度上干扰追踪结果。追踪的结果与真实的运动轨迹有偏差,那么用户看到的商家位置就不准确。

AROrientationTrackingConfiguration

AROrientationTrackingConfiguration 只提供对三个姿态角的追踪(3DoF),并且不会开启 VIO。

Because 3DOF tracking creates limited AR experiences, you should generally
not use the AROrientationTrackingConfiguration class directly. Instead, use
the subclass ARWorldTrackingConfiguration for tracking with six degrees of
freedom (6DOF), plane detection, and hit testing. Use 3DOF tracking only as a
fallback in situations where 6DOF tracking is temporarily unavailable.

通常情况下来讲,这么做的理由是因为 AROrientationTrackingConfiguration
的追踪能力受限,官方文档不推荐直接使用。但是鉴于:

  1. 对三个姿态角的追踪,已经足以正确的展现商家相对用户的位置了。

  2. ARWorldTrackingConfiguration 的高精度追踪,更适合于距离较近的追踪。比如设备相对桌面、地面的位移。但是商家和用户的距离动辄几百米,过于精确的位移追踪意义不大。

  3. ARWorldTrackingConfiguration 需要规范用户的操作、确保环境光线良好。这对用户来说很不友好。

最终经过讨论,我们团队决定使用
AROrientationTrackingConfiguration。这样的话,即便是在夜晚,甚至遮住摄像头,商家的位置也能够正确的进行展现。而且剧烈晃动带来的影响很小,商家位置虽然会出现短暂的角度偏差,但是在传感器数值稳定下来后就会得到校准。

坐标轴

ARKit 使用笛卡尔坐标系度量真实世界。ARSession 开启时的设备位置即是坐标轴的原点。而 ARSessionConfiguration 的
worldAlignment 属性决定了三个坐标轴的方向,该属性有三个枚举值:

  • ARWorldAlignmentCamera

  • ARWorldAlignmentGravity

  • ARWorldAlignmentGravityAndHeading

三种枚举值对应的坐标轴如下图所示:

图5 三种枚举值对应的坐标轴

对于 ARWorldAlignmentCamera
来说,设备的姿态决定了三个坐标轴的方向。这种坐标设定适用于以设备作为参考系的坐标计算,与真实地理环境无关,比如用 AR 技术丈量真实世界物体的尺寸。

对于 ARWorldAlignmentGravity 来说,Y 轴方向始终与重力方向平行,而其 X、Z
轴方向仍然由设备的姿态确定。这种坐标设定适用于计算拥有重力属性的物体坐标,比如放置一排氢气球,或者执行一段篮球下落的动画。

对于 ARWorldAlignmentGravityAndHeading 来说,X、Y、Z 三轴固定朝向正东、正上、正南。在这种模式下 ARKit
内部会根据设备偏航角的朝向与地磁真北(非地磁北)方向的夹角不断地做出调整,以确保 ARKit 坐标系中 -Z
方向与我们真实世界的正北方向吻合。有了这个前提条件,真实世界位置坐标才能够正确地映射到虚拟世界中。显然,ARWorldAlignmentGravityAndHeading
才是我们需要的。

商家坐标

商家坐标的确定,包含水平坐标和垂直坐标两部分:

水平坐标

商家的水平位置只是一组经纬度值,那么如何将它对应到 ARKit 当中呢?我们通过下图来说明:

图6 经纬度转换为坐标

借助 CLLocation 中的 distanceFromLocation:location
方法,可以计算出两个经纬度坐标之间的距离,返回值单位是米。我们可以以用户的经度 lng1、商家的纬度 lat2 作一个辅助点(lng1,
lat2),然后分别计算出辅助点距离商家的距离 x、辅助点距离用户的距离 z。ARKit 坐标系同样以米为单位,因而可以直接确定商家的水平坐标(x,
-z)。

垂直坐标

对商家地址进行中文分词可以提取出商户所在楼层数,再乘以一层楼大概的高度,以此确定商家的垂直坐标 y 值:

图7 高度信息提取

卡片渲染

通常我们想展示的信息,都是通过 UIView 及其子类来实现。但是 ARKit
只负责建立真实世界与虚拟世界的桥梁,渲染的部分还是要交给渲染引擎来处理。Apple 给我们提供了三种可选的引擎:

  • Metal

  • SpriteKit

  • SceneKit

强大的 Metal 引擎包含了 MetalKit、Metal 着色器以及标准库等等工具,可以更高效地利用 GPU,适用于高度定制化的渲染要求。不过
Metal 对于当前需求来说,有些大材小用。

SpriteKit 是 2D 渲染引擎,它提供了动画、事件处理、物理碰撞等接口,通常用于制作 2D 游戏。SceneKit 是 3D 渲染引擎,它建立在
OpenGL 之上,支持多通道渲染。除了可以处理 3D 物体的物理碰撞和动画,还可以呈现逼真的纹理和粒子特效。SceneKit 可以用于制作 3D
游戏,或者在 App 中加入 3D 内容。

虽然我们可以用 SpriteKit 把 2D 的卡片放置到 3D 的 AR 世界中,但是考虑到扩展性,方便之后为 AR 页面添加新的功能,这里我们选用 3D
渲染引擎 SceneKit。

我们可以直接通过创建 ARSCNView 来使用 SceneKit。ARSCNView 是 SCNView 的子类,它做了三件事:

  • 将设备摄像头捕捉的每一帧的图像信息作为 3D 场景的背景

  • 将设备摄像头的位置作为 3D 场景的摄像头(观察点)位置

  • 将 ARKit 追踪的真实世界坐标轴与 3D 场景坐标轴重合

卡片信息

SceneKit 中使用 SCNNode 来管理 3D 物体。设置 SCNNode 的 geometry 属性可以改变物体的外观。系统已经给我们提供了例如
SCNBox、SCNPlane、SCNSphere 等等一些常见的形状,其中 SCNPlane 正是我们所需要的卡片形状。借助 UIGraphics
中的一些方法可以将绘制好的 UIView 渲染成一个 UIImage 对象。根据这张图片创建 SCNPlane,以作为 SCNNode 的外观。

**

**

卡片大小

ARKit 中的物体都是近大远小。只要固定好 SCNPlane 的宽高,ARKit 会自动根据距离的远近设置 SCNPlane
的大小。这里列出一个在屏幕上具体的像素数与距离的粗略计算公式,为笔者在开发过程中摸索的经验值:

也就是说,假如 SCNPlane 的宽度为 30,距离用户 100 米,那么在屏幕上看到这个 SCNPlane 的宽度大约为 \(530 / 100
\times 30 = 159) pt。

卡片位置

对于距离用户过近的商家卡片,会出现两个问题:

  • 由于 ARKit 自动将卡片展现得近大远小,身边的卡片会大到遮住了视野

  • 前文提到的 ARSession 使用 AROrientationTrackingConfiguration 追踪模式,由于没有追踪设备的水平位移,当用户走向商家时,并不会发觉商家卡片越来越近

这里我们将距离用户过近的卡片映射到稍远的位置。如下图所示,距离用户的距离小于 d 的卡片,会被映射到 d-k ~ d 的区间内。

图8 过近卡片位置映射

假设某商家距离用户的真实距离为 x,映射后的距离为 y,映射关系如下:

这样既解决了距离过近的问题,又可以保持卡片之间的远近关系。用户位置发生位移到达一定阈值后,会触发一次新的网络请求,根据新的用户位置来重新计算商家的位置。这样随着用户的移动,卡片的位置也会持续地更新。

卡片朝向

SceneKit 会在渲染每一帧之前,根据 SCNNode 的约束自动调整卡片的各种行为,比如碰撞、位置、速度、朝向等等。SCNConstraint
的子类中 SCNLookAtConstraint 和 SCNBillboardConstraint 可以约束卡片的朝向。

SCNLookAtConstraint 可以让卡片始终朝向空间中某一个点。这样相邻的卡片会出现交叉现象,用户看到的卡片信息很可能是不完整的。使用
SCNBillboardConstraint 可以解决这个问题,让卡片的朝向始终与摄像头的朝向平行。

图9 卡片朝向的两种约束

下面是创建卡片的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 位置

SCNVector nodePosition = SCNVectorMake(-200, 5, -80);



// 外观

SCNPlane *plane = [SCNPlane planeWithWidth:image.size.width

                                    height:image.size.height];

plane.firstMaterial.diffuse.contents = image;



// 约束

SCNBillboardConstraint *constraint = [SCNBillboardConstraint
constraint.freeAxes = SCNBillboardAxisY;



SCNNode *node = [SCNNode nodeWithGeometry:plane];

node.position = nodePosition;

node.constraints = @[constraint];

优化

遮挡问题

如果同一个方向的商家数量有很多,那么卡片会出现互相重叠的现象,这会导致用户只能看到离自己近的卡片。这是个比较棘手的问题,如果在屏幕上平铺卡片的话,既牺牲了对商家高度的感知,又无法体现商家距离用户的远近关系。

点击散开的交互方式

经过漫长的讨论,我们最终决定采取点击重叠区域后,卡片向四周分散的交互方式来解决重叠问题,效果如下:

图10 卡片散开的效果

下面围绕点击和投射两个部分,介绍该效果的实现原理。

点击

熟悉 Cocoa Touch 的朋友都了解,UIView 的层级结构是通过 hit-testing 来判断哪个视图响应事件的,在 ARKit 中也不例外。

ARSCNView 可以使用两种 hit-testing:

  • 来自 ARSCNView 的 hitTest:types: 方法:查找点击的位置所对应的真实世界中的物体或位置

  • 来自 SCNSceneRenderer 协议的 hitTest:options: 方法:查找点击位置所对应的虚拟世界中的内容。

显然,hitTest:options: 才是我们需要的。在 3D 世界中的 hit-testing
就像一束激光一样,向点击位置的方向发射,hitTest:options:
的返回值就是被激光穿透的所有卡片的数组。这样就可以检测到用户点击的位置有哪些卡片发生了重叠。

投射

这里简单介绍一下散开的实现原理。SCNSceneRenderer 协议有两个方法用来投射坐标:

  • projectPoint::将三维坐标系中点的坐标,投射到屏幕坐标系中

  • unprojectPoint::将屏幕坐标系中的点的坐标,投射到三维坐标系中

其中屏幕坐标系中的点也是个 SCNVector3,其 z 坐标代表着深度,从 0.0(近裁面)到 1.0(远裁面)。散开的整体过程如下:

图11 投射过程

散开后,点击空白处会恢复散开的状态,回到初始位置。未参与散开的卡片会被淡化,以突出重点,减少视觉压力。

后台聚类

对于排布比较密集的商家,卡片的重叠现象会很严重。点击散开的卡片数量太多对用户不是很友好。后台在返回用户附近的商家数据时,按照商家的经纬度坐标,使用
K-Means 聚类算法进行二维聚类,将距离很近的商家聚合为一个卡片。由于这些商家的位置大体相同,可以采用一个带有数字的卡片来代表几个商家的位置:

图12 聚合卡片

闪烁问题

实测中发现,距离较近的卡片在重叠区域会发生闪烁的现象:

图13 闪烁

这里要引入一个 3D 渲染引擎普遍要面对的问题——可见性问题。简单来说就是屏幕上哪些物体应该被展示,哪些物体应该被遮挡。GPU
最终应该在屏幕上渲染出所有应该被展示的像素。

可见性问题的一个典型的解决方案就是画家算法,它像一个头脑简单的画家一样,先绘制最远的物体,然后一层层的绘制到最近的物体。可想而知,画家算法的效率很低,绘制较精细场景会很消耗资源。

深度缓冲

** **

深度缓冲
弥补了画家算法的缺陷,它使用一个二维数组来存储当前屏幕中每个像素的深度。如下图所示,某个像素点渲染了深度为 0.5 的像素,并储存该像素的深度:

图14 深度缓冲区

下一帧时,当另外一个物体的某个像素也在这个像素点渲染时,GPU
会对该像素的深度与缓冲区中的深度进行比较,深度小者被保留并被存入缓冲区,深度大者不被渲染。如下图所示,该像素点下一帧要渲染的像素深度为
0.2,比缓冲区存储的 0.5 小,其深度被存储,并且该像素被渲染在屏幕上:

图15 深度小的像素被保留

显然,深度缓冲技术相比画家算法,可以极大地提升渲染效率。但是它也会带来深度冲突的问题。

深度冲突

深度缓冲技术在处理具有相同深度的像素点时,会出现深度冲突(Z-fighting)现象。这些具有相同深度的像素点在竞争中只有一个“胜出”,显示在屏幕上。如下图所示:

图16 深度冲突

如果这两个像素点交替“胜出”,就会出现我们视觉上的闪烁效果。由于每个卡片都被设置了 SCNBillboardConstraint
约束,始终朝向摄像头方向。摄像头轻微的角度变化,都会引起卡片之间出现部分重合。与有厚度的物体不同,卡片之间的深度关系变化很快,很容易出现多个卡片在屏幕同一个位置渲染的情况。所以经常会出现闪烁的现象:

图17 角度变化引起的深度冲突

为了解决这 Bug 般的体验,最终决定牺牲深度缓冲带来的渲染效率。SceneKit 为我们暴露了深度是否写入、读取缓冲区的接口,我们将其禁用即可:

1
2
3
 plane.firstMaterial.writesToDepthBuffer = NO;

 plane.firstMaterial.readsFromDepthBuffer = NO;

由于卡片内容内容相对简单,禁用缓冲区对帧率几乎没什么影响。

总结

在到餐业务场景中,以 AR+LBS 的方式展现商家信息,可以给用户带来沉浸式的体验。本文介绍了 ARKit
的一些使用细节,总结了在开发过程中遇到的问题以及解决方案,希望可以给其他开发者带来一点参考价值。

UML系列时序图(顺序图)sequence diagram

引言

用例图、类图、活动图、时序图之间是什么关系?

时序图有什么作用?

先来模拟一下三国演义的赤壁之战的时序图,先知道它到底长什么样子,再深入介绍:

小伙伴惊呆了,这样画战略图,一目了然,原来著名的战役是这么回事。这样看三国演义再也不会睡着了……

再看看各个大人物的主要操作:

代码模拟各任务操作:

[
](file:///Applications/%E5%8D%B0%E8%B1%A1%E7%AC%94%E8%AE%B0.app/Contents/Resources
/common-editor-mac/mac.html# “复制代码”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class 关羽 {

public void 防守荊州();

}

public class 张飞 {

public void 防守荆州前线();

}

public class 孙权 {

public void 领兵相助();

}

public class 刘备 {

public void 应战();

}

public class 孔明 {

public void 拟定策略();

public void 联合孙权();

private void 借东风火攻();

}

[
](file:///Applications/%E5%8D%B0%E8%B1%A1%E7%AC%94%E8%AE%B0.app/Contents/Resources
/common-editor-mac/mac.html# “复制代码”)

(以上为伪代码,只为说明问题,缩短文章长度)。

分析一:用例图、活动图、时序图之间的关系

分析二:类图与时序图之间的关系

类图描述系统的静态结构,时序图描述系统的动态行为。

若出现了一个新的消息(不是类中的操作),则需要为类添加一个新的操作。

为类c添加k操作。如果这里不容易理解,可以参考上面的赤壁大战的时序图,你就知道该新加的操作到底是类B的还是C的。

时序图的作用

用对象间的交互来描述用例。(注意是对象间的)

寻找类的操作。

时序图的概念

时序图描述了对象之间传递消息的时间顺序,它用来表示用例的行为顺序。

时序图的组成

对象

对象是类的实例,对象是通过类来创建的,我们可以把类看作是创建对象的模版。

对象的符号

时序图中的每个大峡谷显示单独的列里。

对象的左右排列位置

对象的左右顺序并不重要,但是为了画图画的清晰整洁起见,通常应遵循一下两个原则:

(1)把交互频繁的对象尽可能地靠拢。

(2)把初始化整个交互活动的对象(有时是一个参与者)放置在最左边。

生命线lifeline

表示对象的生存时间。生命线从对象创建开始到对象销毁时终止。

对象在生命线上的两种状态:休眠状态、激活状态。

消息

消息的概念:

对象之间的交互是通过相互发消息来实现的。一个对象可以请求(要求)另一个对象做某件事件。

消息从源对象指向目标对象。消息一旦发送便将控制从源对象转移到目标对象。

时序图中,消息的阅读顺序是严格自上而下的。

消息的类型与符号

实例:同步异步

你叫我去吃饭,我听到了就和你去吃饭;如果没有听到,你就不停的叫,直到我告诉你听到了,才一起去吃饭。

你叫我去吃饭,然后自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。

所以,要我请你吃饭就用同步的方法,要请我吃饭就用异步的方法,这样你可以省钱。^_^!

(1)简单消息

简单消息是没有区分同步和异步的消息。

简单消息用从一对下岗的生命线指向另一个对象的生命线的直线箭头来表示,箭头上面标出消息名。

(2)同步消息

消息的发送者把进程控制传递给消息的接收者,然后暂停活动,等待消息接收者的回应消息。

你叫我去吃饭,我听到了就和你去吃饭;如果没有听到,你就不停的叫,直到我告诉你听到了,才一起去吃饭。

(3)异步消息

消息的发送者将消息发送给消息的接受者后,不用等待回应的消息,即可开始另一个活动。

你叫我去吃饭,然后自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。

激活

激活的概念

当一个对象没有被激活期时,该对象处于休眠状态,什么事都不做,但它仍然存在,等待新的消息来激活它。

当一条消息被传递给对象的时候,它会触发该对象的某个行为,这就是说该对象被激活了。

当一个对象处于激活期时,表明该对象正处于执行某个动作。

激活的符号

激活用一个细长的矩阵框(在生命线上)表示。

矩形框的高度表示对象执行一个操作所经历的时间段,矩形的顶部表示动作的开始,底部表示动作的结束。

对象接收消息后可以由自己的某个操作来完成,也可以通过其他对象的操作来完成。

总结

时序图到此结束,如果有需要补充的可以留言,lz定补上。

时序图还是听复杂的,遇到问题得好好分析……

矩形框的高度表示对象执行一个操作所经历的时间段,矩形的顶部表示动作的开始,底部表示动作的结束。

对象接收消息后可以由自己的某个操作来完成,也可以通过其他对象的操作来完成。

比如下面的三张图表达的意思就不同了:

如果能把这三个图的区别搞明白了,时序图也掌握的差不多……Thinking…..

返回消息

返回的消息。虚线的线性箭头表示。

自关联消息

自身调用自身的方法。即自我调用的同步消息。

组合

对一个交互过程的执行条件进行限定。

选择组合

表明组合中的内容可能执行可能不执行,根据条件进行判断。

替换组合

类似与if..else和switch,多个组合中选择一个执行。

其他

组合情况下面的情况,摘自网络。

一个简单例子

互联网架构的演变

互联网架构的演变

1

最初是前端一个web 加一个DB的结构

这种结构,web容易挂掉,业务就会终止,由于高可用的需求,出现了下面这样的架构。

2

加了一个web,两个web之间是主备的关系,一个挂了,另一个来代替,用来解决高可用问题

3

之后发现这样的架构支持的访问量不够了,前端撑不住那么大的访问量,因为前端的访问量和DB的落库有大概是10比1的比例,前端访问10个,会有1个能够落库,所以随着访问量的增加,前端先扛不住了,这个时候主、备结构已经不能解决高可用的问题,所以在web前面加了一个ngx,作为负载均衡进行访问的转发,这个时候,web和web之间的主备关系就不存在了,在ngx进行转发的时候会有一个session保持的操作,再后来就出现无状态的概念,在两个web之间进行轮询,给谁都行

4

当无状态的概念出来以后,web这一层就可以进行多次的横向扩展,这是第一次质的飞越

后来人们觉得一个ngx也会出问题,就设计了主、备结构的ngx

5

后来主、备ngx结构也不满足需求了,就在ngx前面加了一个lvs,lvs是一个负载均衡的结构,lvs先对ngx做分发

这时nginx就解放出来了,不是主、备结构了,而是作为一个层级的结构,可以进行无限横向扩展,lvs被设计成主、备的结构,至此,lvs作为第一层就不再变化了,第一层始终是主、备的结构,lvs的负载特别小,所有的负载到lvs直接就传给ngx,ngx再往下分发

这个时候,转发层活了,web层活了,压力就全到DB了,DB就开始演变

6

最初DB演变出来master和slave这样的架构

所有的web既连master也连slave,连slave只进行读操作,连master只进行写操作,这样把读和写进行分离,这时速度就有一个质的提升

然后如果一个slave不够的时候,再加一个slave,就解决了这个架构对数据库读的压力,这个时候的网站的并发访问量,理论上可以到万级了

但是有一个问题,master是一个单点,如果它挂了,整个系统就都挂了,这个时候要考虑的不只是吞吐量,也要看性能、可靠性,前端已经没有问题了,但是数据库这里有一个单点的问题,后来人们就开始想办法,怎么能保证DB不会出问题呢,后来人们就把DB这块给重构了。

7

在web下面加了一层proxy结构,可以说是转发,也可以说是中间件,这一层结构可以做成集群,这个集群是给数据库做切片的,做成类似于分布式的功能,这一层下来给DB做切片转发,这样DB这一层的库之间就不再有主、从的关系了,都认为是主,挂掉一个,其他的还能正常工作,既做高可用,又做高性能,保证某一台挂掉的时候,另外两台可以做到高可用,如果负载不够的时候,还可以添加

这个4层结构,可以在很高的性能下,很高的可靠性下做到万级甚至十万级的并发。

这是5年前开始流行的架构,现在的使用率也很高,在对可靠性和性能方面要求不是很高的系统中可以很稳定的应用,但它存在一个问题,就是访问量再大的时候,落库的时候会有一个瓶颈,因为DB不只是做插入和查询,还要做分析和关联,速度会很慢,那怎么办呢?

8

人们开始在DB层的上边加两个缓存层(读和写),读缓存层是用redis,它也是集群,在业务层下边,DB层的上边,redis从数据库里抽数据,到自己的缓存中,然后提供给业务,只要被缓存命中的数据,读的是特别快的,redis把DB中的热数据提取到缓存层,直接给业务层来用。

写的话用的是MQ,来实现写的业务,一读一写合起来就大大的提升了并发的处理。

这个架构中的lvs负责四层转发,ngx做7层转发,转发下来后,web做接收。

读的时候,热数据从DB里抽到redis里读,就不会因为DB的计算卡着web了,在像双十一这种秒杀的业务,可以把数据提前放到redis里,供web来读,写数据时可以快速的写给mq,之后web就不用管了,用户就可以接着做下一步了,然后mq往DB里一点一点的落库,就是说通过缓存层把高速的web和低速的DB给隔离开,这样给前边的用户体验就会特别好。

比如双十一之前加购物车,都是提前被写好了放到redis里边,然后下订单都是通过mq来落库,你这边一点刷一下就过去了,你觉得过去了,实际上都在mq里往DB里处理,不可能一下子就落到DB里,目前还没有一家公司能够做到,就算是分布式数据库也不行,分布式数据库只能解决中型的业务场景,解决不了巨型的业务场景,这是这两年很典型的比较火的架构,但是它比不上分布式数据库,这种结构即使很快,如果技术实力不够也只能达到十万级,如果要扩的话,只能是横向的扩,一套一套的往上累加。

9

出现了新型分布式数据库以后,这个架构变得很简单,很明晰,速度也很快。

lvs,nginx,web这三层不变,下边的缓存层,代理层都没有了,只剩数据库本身了,这个DB目前可以撑住10万甚至20万(瞬间的并发写),没有理论上限,各种的复杂的计算在一秒内都可以完成,这就是分布式架构的架构之美,越来越简洁。这个DB实际上是很复杂的。

它的底层有自己的分布式存储dfs结构,另外有控制节点,还有提供实际计算,运行sql的计算节点,这三部分是没有中心(去中心),可扩展,对称的(坏掉一台、加一台都没问题),可以做到平行的扩展,做到无限制的可行性,也是分布式的目的所在。

他是怎么做到高速的呢?实际上就是分布式存储和计算的理念,如果有中心的话,需求下来一定是发给一个中心节点上,这个中心就会成为瓶颈,无中心的话,需求下来抛给控制节点,控制节点也没有中心,控制节点接到请求以后告诉它你去哪个计算节点上去,然后计算节点去计算的时候,如果这时所有的数据放到一个dfs上边(存储DB),读写势必会很慢,但是分布式存储它是完全散开的(多个dfs),一个请求过来后是放到所有的计算节点上去算的,然后拿的时候是到所有的dfs上去拿的,这个速度会特别快,这个过程完全都是分布的,所以,这种新型的数据库架构,就可以做到每秒10万个查询、写入、update,也可以做分析,最快可以到每秒10万。

目前能做到千万级别的并发的,有BAT和12306,其他就没有了,这种架构覆盖了当前互联网基本上 95%甚至是99%的需求。

数据在下发的时候,是多副本,挂掉一个机器数据是不会丢失的,它的数据是打散的,这样读的才会快,这个结构出来之后直接干掉缓存层,而且可以支持列式数据库,目前的数据库基本是行式的,列式数据库对那种特别大的数据量支持的特别好。
(The End)

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×