这一节继续优化甜甜圈主体部分,从下面这张真实的照片可以看出,甜甜圈主体表面是坑坑洼洼的(任何面点食物应该都不会很光滑),而目前我们的甜甜圈主体太过光滑,所以显得有点假。
首先点击Edit
菜单,选择Preferences
,打开Blender设置菜单,然后选择Add-ons
,在右侧搜索框搜索node
,然后将Node: Node Wrangle
插件打上勾。这个插件可以优化和加速节点效果的处理流程。
然后进入Shading
模式,选中甜甜圈,Shift + A
,选择Texture/Nosie Texture
,增加Noise Texture
节点。
Noise Texture
:噪波纹理着色器节点,给材质增加不规则碎片形的纹理。可以调节缩放比例、碎片数量、粗糙度、变形程度。
按下Shift + Ctrl
,鼠标左键单击Noise Texture
节点,就可以自动将该节点纳入流程中。
Shift + A
,选择Input/Texture Coordinate
,添加Texture Coordinate
节点。将Object
属性连接到Noise Texture
节点的Vector
属性。调节Noise Texture
节点的Scale
属性。
Texture Coordinate
:纹理坐标输入节点。Object
属性:指定一个物体(对象),作为纹理映射的参考系。通常使用空物体,这是将图案投射到物体表面上某个固定点的一个简单办法。通过对指定物体进行变换(甚至动画),可以使纹理在被投射物体的表面移动。
按下Shift + A
,选择Vector/Displacement
,添加Displacement
节点。将Noise Texture
节点的Color
属性连接到Displacement
节点的Height
属性。将Displacement
节点的Displacement
属性连接到Material Output
属性。
Displacement
:置换着色器节点,置换节点用于沿表面法线置换表面,可以给几何体添加更多细节。Height
属性:沿法线移动表面的距离。
按下Shift + Ctrl
,鼠标左键单击Principled BSDF
节点,重新整理流程,此时就可以看到甜甜圈的变化了。
选中甜甜圈,选择右侧Material Properties
菜单,找到Settings
选项,将Displacement
属性改为Displacement and Bump
,然后调整Displacement
节点的Scale
属性。这样就可以让噪音作用在整个物体上,而不只是表面。
为了让甜甜圈表现的更细腻一些,可以对甜甜圈再添加一个细分修改器,然后再调整Noise Texture
节点的Scale
属性,将甜甜圈的质感调整的合适的感觉。
最后来一张渲染图片,看看甜甜圈的质感。
]]>这一节继续优化甜甜圈主体部分,从下面这张真实的照片可以看出,甜甜圈主体表面是坑坑洼洼的(任何面点食物应该都不会很光滑),而目前我们的甜甜圈主体太过光滑,所以显得有点假。
首先点击Edit
菜单,选择Preferences
,打开Blender设置菜单,然后选择Add-ons
,在右侧搜索框搜索node
,然后将Node: Node Wrangle
插件打上勾。这个插件可以优化和加速节点效果的处理流程。
这一节我们来优化甜甜圈的主体部分,目前我们只是捏出了形状和简单的设置了基本颜色,看上去还是有点假。所以我们通过使用Texture Paint能力将其完善的更加真实一些。
先来看一张真实的甜甜圈照片,可以看到在主体部分的腰部凹陷那一圈,颜色是比较浅的,因为那个部位被烘烤的力度较小,越是顶部和底部,颜色会越深,这和我们煎炸其他食品的道理是一样的。
在使用Texture Paint之前,我们先看看材质、纹理等这些概念的区别和关系。
英文 | 中文 | 本质 | 释义 |
---|---|---|---|
Material | 材质 | 数据集 | 表现物体对光的交互,供渲染器读取的数据集,包括贴图纹理、光照算法等 |
Texture mapping | 纹理贴图 | 图像映射规则 | 把存储在内存里的位图,通过 UV 坐标映射到渲染物体的表面 |
Shading | 底纹、阴影 | 光影效果 | 根据表面法线、光照、视角等计算得出的光照结果 |
Shader | 着色器 | 程序 | 编写显卡渲染画面的算法来即时演算生成贴图的程序 |
我们今天操作的是在Texture这一层。先将糖衣隐藏,点击顶部Texture Paint
菜单,进入纹理绘制视图。
新建一张纹理图片,将宽度设置为2048px。
这就是我们要绘制的图片了。
然后选中甜甜圈主体,进入Shading
视图,Shift + A
,选择Texture/Image Texture
,添加一个Image Texture
节点。将Image Texture
节点的Color
属性连接到Principled BSDF
的Base Color
属性。在Image Texture
节点中选择我们刚才创建的那张图片Donut_Texture
。然后我们就可以看到甜甜圈主体的颜色变成了Donut_Texture
这张图片的颜色。
回到Texture Paint
视图,当我们在右侧的甜甜圈上涂色时,左侧的图片上就会出现对应的图案或颜色。反过来,在左侧的图片上涂色时,也会反应到右侧的甜甜圈上。
将鼠标放在左侧,按下N
呼出工具栏,设置合适的颜色后,将图片涂成基本色。
然后再将颜色设置为白色,调整好笔触大小和力度大小,在右侧甜甜圈腰部涂色,来实现因受火不均产生的白色效果。
当我们画完之后,其实可以看到,白色非常死板,而且有重叠的笔痕。
为了让白色绘制的比较自然,我们可以使用Texture Mask
功能。将鼠标移动到左侧,按下N
呼出工具栏,在Brush Settings
下找到Texture Mask
新建一个纹理模板。
选择右侧Texture Properties
菜单,从Brush
改为Brush Mask
,然后将Type
改为Clouds
。
然后重新在甜甜圈主体凹陷位置进行绘制,此时可以看到白色就不会显得太僵硬。
接下来实现顶部和底部烤焦的效果。鼠标移动到左侧,按下N
,呼出工具栏,将Brush Settings
下的Blend
属性改为Overlay
,将颜色设置为黑色。
然后在甜甜圈主体的顶部和底部进行绘制,就可以实现烤焦的效果。
好,目前为止,甜甜圈主体的优化工作就完成了,来一张渲染图看看效果。
]]>这一节我们来优化甜甜圈的主体部分,目前我们只是捏出了形状和简单的设置了基本颜色,看上去还是有点假。所以我们通过使用Texture Paint能力将其完善的更加真实一些。
先来看一张真实的甜甜圈照片,可以看到在主体部分的腰部凹陷那一圈,颜色是比较浅的,因为那个部位被烘烤的力度较小,越是顶部和底部,颜色会越深,这和我们煎炸其他食品的道理是一样的。
在使用Texture Paint之前,我们先看看材质、纹理等这些概念的区别和关系。
]]>这一节来继续完善甜甜圈上的糖粒,我们需要完成两件事:
通过上一节实操,我们知道甜甜圈糖衣上的糖粒是通过对糖衣添加了粒子效果实现的,而粒子的对象是一个母板糖粒,所以我们要更改糖粒的颜色,只能针对这个模板糖粒进行修改。
我们可以修改糖粒的颜色,但是可以看到糖衣上的所有糖粒都是同一种颜色。
可以在用Blender做甜甜圈学习笔记六 - 渲染甜甜圈这一节中回顾如何给对象添加颜色。
这肯定不是我们期望的。要实现随机的上色效果,我们需要使用一个新的功能,材质生成着色器。我们点击顶部的Shading
,进入着色器编辑视图。
在这个界面中,我们选中糖粒对象,在窗口下半部分,会看到和用Blender做甜甜圈学习笔记六 - 渲染甜甜圈中使用到的Compositing
视图中类似的界面。但是里面的节点不一样,首先我们会看到一个叫Principled BSDF
的节点。
这里简单地对Principled BSDF
作以解释。Principled BSDF
是将多个层组合成一个易于使用的节点。是一款综合型的材质生成着色器(Shader),跟时下普遍使用的 PBR (Physically based rendering)是原理类似的技术。
BSDF
是Bidirectional Scattering Distribution Function
的缩写,意思是双向散射分布函数,因为想要正确模拟真实世界中的光照,需要理解光遇到物体表面时发生的反射和散射,BSDF
就是帮助我们实现更逼真的光散射效果的函数。
对应的还有一个
BRDF(Bidirectional Reflectance Distribution Function)
双向反射分布函数。
我们使用快捷键Shift + A
添加一个节点,在弹出的窗口中选择input/Object Info
,然后将Object Info
节点的Random
属性连接到Principled BSDF
节点的Base Color
属性。
Random
相当于提供了一组从0到1的随机数字,然后将这个随机的属性给了Base Color
。
可以看到甜甜圈上的糖粒的颜色已经是随机的了,只不过从0到1的随机数,映射到颜色的色域是从白色到黑色之间。我们也可以通过添加一个色谱节点来控制在哪些色域中进行随机。同样使用Shift + A
,然后选择Converter/ColorRamp
,然后将节点放在Object Info
节点和Principled BSDF
节点的连接上,Blender就可以自动的将ColorRamp
节点和Object Info
节点、Principled BSDF
节点对接。
当然我们也可以手动创建连接,将Object Info
节点的Random
属性和ColorRamp
节点的Fac
(因子)属性进行连接,然后将ColorRamp
节点的Color
属性和Principled BSDF
节点的Base Color
属性进行连接。
我们可以改变ColorRamp
节点中的颜色,可以看到随机的色域的也会发生变化。
然后我们可以在ColorRamp
中增加色标,每个色标可以指定不同的颜色,就可以实现不同色域之间的随机颜色了。
来一张渲染图看看效果:
实现了随机颜色后,我们来增加不同形态的糖粒。目前糖粒的形状都是一个圆柱体,我们可以多捏几种形态。
先捏一个更长一点的圆柱体,操作方法是选择糖粒圆柱体,Shift + D
复制一个,进入编辑模式,打开透视,选中圆柱体顶部的所有点,按下G
和Z
向上移动鼠标即可。可以回顾之前的笔记。
那么如何将这个更长一点的糖粒添加到糖衣上呢?我们来看糖衣上粒子效果的Render AS
属性,目前的值是Object
,那意味着只能有一个指定的对象来充当粒子效果,Render AS
其实还有其他的选项,这里可以选择Collection
。此时大家应该很容易理解了,这个选项的意思就是选择一个集合,该集合下的所有对象都会成为充当粒子效果的对象。
所以我们首先将两个糖粒对象放在一个集合中。可以回顾用Blender做甜甜圈学习笔记七 - 制作糖衣上的糖粒中如何创建集合。
然后选择糖衣,打开右侧Particle Properties
菜单,找到Render/Render AS
,将Object
修改为Collection
,然后将Collection/Instance Collection
的值设置为糖粒的集合。然后我们就可以看到糖衣上的糖粒同时包含了短的圆柱体和长的圆柱体。
然后我们再多添加几个形态的糖粒。再复制一个长的圆柱体,移动到旁边,进入编辑模式,将其再拉长一点。然后选择Loop Cut
工具,这个工具的作用是对选中的对象进行横向或纵向的换切,换句话说就是对物体在横向或纵向增加拓扑点。
在选择了Loop Cut
工具后,滚动鼠标滑轮,可以增加或减少换切的数量。
这里我们对圆柱体进行两次横向的环切,然后调整两头,进入对象模式,对其进行Shade Smooth
操作,呈现如下样子:
现在糖衣上的糖粒就有了三种不同的形态。
我们现在选中一个糖粒圆柱体,可以看到他的原点在底部,我们可以将原点设置为它的中心点,更符合我们对原点理解的习惯,同时这样在糖衣上糖粒的形态会贴合一些,有一些糖粒就不会那么翘。
为了让糖粒更加丰富一些,我们再来添加一个圆形糖粒。
我们希望圆形的糖粒只在糖衣上点缀几个即可,这时我们就需要调整每一种形态的糖粒的数量。进入对象模式,选择糖衣,打开右侧Particle Properties
菜单,在Render/Collection
下找到Use Count
选项,打上勾,然后可以针对每一种形态的糖粒设置出现的次数,相当是在同一个集合下,该对象出现的比例。
我们将三种圆柱体形状的糖粒的Count
设置为30,然后球形的糖粒Count
保持为1,此时就可以看到糖衣上只有零星几个球形的糖粒。
到目前为止,糖粒的完善基本结束,上一张渲染图片看看效果。
]]>这一节来继续完善甜甜圈上的糖粒,我们需要完成两件事:
通过上一节实操,我们知道甜甜圈糖衣上的糖粒是通过对糖衣添加了粒子效果实现的,而粒子的对象是一个母板糖粒,所以我们要更改糖粒的颜色,只能针对这个模板糖粒进行修改。
我们可以修改糖粒的颜色,但是可以看到糖衣上的所有糖粒都是同一种颜色。
]]>这一节继续对糖衣进行完善,先来看一张实图:
可以看到糖衣上有五颜六色的糖粒(我猜应该是糖),呈小圆柱体,不规则的铺在糖衣上面。这一节就来实现糖衣上的小糖粒。
首先我们先通过集合的方式,对目前场景里的对象做一下分类和整理。选中一个或若干个对象,按下M
,然后选择New Collection
,就可以将对象放在一个集合中,在《用Blender做甜甜圈学习笔记五 - 雕刻糖衣白模》中使用过。
为甜甜圈和糖衣创建Donut
集合,将充当桌面的平面放在Environment
集合中,将摄像机和灯光放在Cam & Light
集合中。
先将场景中所有的对象都隐藏,然后按Shift + A
选择Mesh/UV Sphere
创建一个球体。将大小和拓扑面数都设置的小一些。
然后将这个球体移到甜甜圈旁边。
接下来要做的就是将这个球体编辑成一个小圆柱体。我们要做四步:
Extrude Region
工具,将上半部分拉长。以上操作在用Blender做甜甜圈学习笔记三 - 捏出糖衣白模和用Blender做甜甜圈学习笔记四 - 完善糖衣白模中有使用过。
然后调整两头的形状,先选中一头的所有点,按下S
,再按Z
,将圆头调整为接近平头,另一侧同理,意思是将选中的部分按照Z轴缩放。
然后切换到对象模式,选中圆柱体,鼠标右键,选择Shade Smooth
,将圆柱体的表面光滑的显示。在用Blender做甜甜圈学习笔记二 - 捏出白模胚子中我们使用过。
我们将糖粒的模型捏出来后,大家首先想到的一定就是将它铺满甜甜圈的糖衣,我们当然可以使用比较耗时耗力的方法,就是复制茫茫多个糖粒对象,然后手动移动到糖衣上,再调整方向和角度。那我们的心态一定会炸裂。
在Blender中,肯定有更方便的方法来实现将糖粒铺满糖衣,并且分布密度和方向角度都是随机的。
实现这个效果,我们需要用到对象的粒子效果属性。选中糖衣,点击右侧的Particle Properties
选项,然后添加粒子效果。
粒子效果有两种类型,Emitter
和Hair
,我们选择Hair
,然后将Render
设置下的Render As
改为Object
,即将渲染粒子方式改为对象。
然后将Render
设置下的Object
下的Instance Object
设置为我们的糖粒对象。意思就是用糖粒对象来充当粒子。
点击
Instance Object
旁边的吸管,然后选择糖粒对象。
此时,我们就可以看到在糖衣上出现了密密麻麻的糖粒。
将糖粒铺到糖衣上后,我们需要调增糖粒大小、分布范围、方向的随机性,这样显的更加真实。首先可以设置Render
下的Scale
属性,来调整糖粒的大小,或者说粒子的大小。
然后来调整糖粒的方向,我们首先要勾选Advanced
,然后会出现Rotation
设置,然后勾选Rotation
,并将Rotation
下的Orientation Axis
属性设置为Normal
,然后调整Randomize Phase
的值,就可以看到所有糖粒的方向开始有了随机的变化。
真实的情况下,糖粒撒到糖衣上,不会完全是平铺的,肯定会有一些糖粒插进糖衣的情况,所以我们可以调整Randomize
属性来实现这个效果。
Randomize
调整一点即可,不然糖衣就变成刺猬了。
最后我们再来调整糖粒的分布范围,因为正常情况糖粒一般会分布在糖衣表面,侧面是不太会有糖粒的,而现在我们的糖衣模型侧面也有糖粒。
解决这个问题,我们需要用到权重设置Weight Paint
,我们在左上角的下拉菜单中可以切换到Weight Paint
摸下,或者可以用快捷键Ctrl + Tab
来切换。
切换到权重设置或权重绘制模式后,可以看到糖衣整个变成了蓝色,这表示此时的权重为0。当我们按鼠标左键在糖衣上滑动时,会看到像热力图似地样子出现,其中红色就表示权重为1,其他颜色就是1至0之间的权重。
我们可以通过调整顶部的Radius
来调整绘制权重的笔触范围。调整好笔触范围后,我们将糖衣顶部都绘制为权重1。
然后将Vertex Groups
下的Density
设置为Group
,此时就可以看到糖粒的范围基本都在刚才绘制的权重为1的范围内。
如果觉得糖粒太多太密,我们还可以调整Emission
下的Number
,可以控制粒子的数量。
到目前为止,糖粒的白模就制作完毕了,上一张渲染图看看效果。
]]>这一节继续对糖衣进行完善,先来看一张实图:
可以看到糖衣上有五颜六色的糖粒(我猜应该是糖),呈小圆柱体,不规则的铺在糖衣上面。这一节就来实现糖衣上的小糖粒。
首先我们先通过集合的方式,对目前场景里的对象做一下分类和整理。选中一个或若干个对象,按下M
,然后选择New Collection
,就可以将对象放在一个集合中,在《用Blender做甜甜圈学习笔记五 - 雕刻糖衣白模》中使用过。
为甜甜圈和糖衣创建Donut
集合,将充当桌面的平面放在Environment
集合中,将摄像机和灯光放在Cam & Light
集合中。
到目前位置,甜甜圈的模型基本都调整、雕刻完毕了,接下来需要将我们做的模型渲染出来看看效果。在渲染之前,我们需要先认识一个对于渲染很重要的元素,灯光。
在整个视图中,除了甜甜圈的对象以外,我们应该也早已发现了另外两个对象,Light
和Camera
。
灯光的强弱可以通过灯光离物体的距离以及一些设置项来调整,比如我们将灯光移动到离甜甜圈比近的位置,然后选择右上角的渲染模式视图,可以看到甜甜圈被强光照的都发白了。
灯光所处的位置不同,被照物体上的光源也会发生响应的变化。
为了更加真实,以及能更好的表现光源,我们需要创建一个平面,相当于桌面似的。Shift + A
,选择Mesh/Plane
。
使用缩放调整平面的大小。这时我们会发现,甜甜圈有一半在平面下面。
解决的办法很简单,那就是将整个甜甜圈向上移动。但这里有个问题,因为整个甜甜圈是有两个对象组成的,糖衣和甜甜圈蛋糕部分,所以移动时需要将两个对象都选中,才不至于错位,那么如果对象比较多的时候,这种多选移动的方式就很繁琐,而且容易出错。
既然糖衣和甜甜圈蛋糕部分肯定是一个整体,那我们就可以将这两个对象合二为一,这个能力叫做联接。将一个对象作为子对象,和所选父对象进行关联,形成一个整体,并且子对象可以随意移动,不影响父对象,但移动父对象,会一并影响子对象。
先选择的对象视为子对象,后选择的对象视为父对象。
先选中糖衣,再按Shift
选中甜甜圈蛋糕部分,然后快捷键Ctrl + P
,会弹出Set parent to
菜单,推荐选择Object(Keep Transform)
,这样就完成的父子关联,从右上角的对象关系视图中也可以看到,Icing
对象没有了。
此时不论是选择糖衣还是甜甜圈蛋糕部分,都可以选中整体的甜甜圈。然后我们切换到Front
视角,将甜甜圈沿Z轴向上移动,完成甜甜圈放在平面上的效果。
选中灯光对象,选择右侧Object Data Properties
菜单,在Light
一栏中可以可以调整灯光的颜色、灯光亮度(相当于瓦数),以及灯光照射范围。
在Blender中有两种渲染引擎,Eevee
和Cycles
。前者是一个实时渲染引擎,渲染速度非常快,但渲染效果相对Cycles
要弱一些。后者是通过CPU或者GPU进行渲染的引擎,渲染速度慢,但是渲染效果更加真实,当然对电脑的性能要求也更高。动画通常用Cycles
渲染,像游戏之类通常用Eevee
渲染。
这里先简单了解一下渲染引擎,之后我们再深入学习,我们做这个甜甜圈之后渲染成图时要使用Cycels渲染。
咱们在捏模型的时候,整个视图场景可以看作一个近似可以无限延伸的场景,但是当我们渲染的时候,不可能无限延伸去渲染所有的物体,所以需要画一个范围,而摄像机就是用来做这个事的,和拍电影、照相的道理一样。
切换到摄像机视角有三种方式:
将摄像机通过移动、缩放、旋转三个基本操作,调整都合适的位置,然后可以切换到摄像机视角。
将灯光、摄像机都调整好,然后将充当桌面的平面换个颜色,这样能更加凸显甜甜圈。
然后按F12
,就可以进行图片的渲染了,可以切换不同的渲染引擎,看看对比效果。这里用Cycles引擎渲染。
下面我们给糖衣设置颜色,选中糖衣,点击右侧Material Properties
菜单,新建一套材质,然后会看到很多关于颜色、材质的属性。
我们先来看Base Color
基本色,可以给糖衣设置一个粉色。
然后再来看Roughness
属性,这个属性可以调节物体表面的反光程度,也就是可以将物体表面调整为亚光面。同理还有一个Specular
属性,通常调整其中一个属性即可,推荐调整Roughness
属性。
另外还可以通过Subsurface
和Subsurface Color
在Base Color
的基础上再调整颜色。
下面是经过这几个参数调整后的糖衣效果。
按照同样的方式,给甜甜圈调整颜色。
当我们将渲染的图片放大后,可以看到有很多噪点。
解决这个问题需要以下几个步骤。首先选中甜甜圈,选择右侧View Layer Properties
,将Denoising Data
打勾。
然后进入Compositing
视图,选中左上角的Use Nodes
。Shift + A
添加Denoise
组件,将Render Layers
的Nosiy Image
、Denoising Normal
、Denoising Albedo
三个属性分别连接到Denoise
组件对应的属性上,然后将Denoise
组件的Image
属性连接到Composite
的Image
属性上。
然后重新渲染一次,就可以看到干净的渲染图了。
]]>到目前位置,甜甜圈的模型基本都调整、雕刻完毕了,接下来需要将我们做的模型渲染出来看看效果。在渲染之前,我们需要先认识一个对于渲染很重要的元素,灯光。
在整个视图中,除了甜甜圈的对象以外,我们应该也早已发现了另外两个对象,Light
和Camera
。
灯光的强弱可以通过灯光离物体的距离以及一些设置项来调整,比如我们将灯光移动到离甜甜圈比近的位置,然后选择右上角的渲染模式视图,可以看到甜甜圈被强光照的都发白了。
]]>先来看一张真实的甜甜圈。
一共有三个特点:
这一节就是使用Blender的雕刻功能,将甜甜圈和糖衣的这几个特点刻画出来。
在开始之前,先回忆一下前面我们给甜甜圈和糖衣添加过修改器,但是我们并没有让修改器真正的生效,那么在雕刻前,需要将修改器的内容应用生效。在应用生效修改器之前,我们先对现在的白模复制一份,做个备份,因为修改器一旦生效后,整个拓扑都会发生变化。
选中甜甜圈和糖衣的白模,Shift + D
复制一份,然后按下M
键,新建一个集合,然后设置为不可见,相当于将复制出来的甜甜圈和糖衣做个备份。
然后选中甜甜圈,进入编辑模式,可以看到现在虽然有细分的修改器,但是拓扑结构还是之前精度比较小的样子,也就是四边形数量小,当我们应用修改器后,可以看到拓扑结构有明显的变化,四边形数量变多了,也就意味着我们能刻画更丰富的细节。
应用修改器时需要切换回对象模式。
现在点击顶部Sculpting
菜单进入雕刻视图。
左侧会有各种用于雕刻的工具,这里我们选择第一个工具Draw
,然后按住Ctrl
键,在甜甜圈腰部进行雕刻,会看到模型上会出现凹痕。
如果不按住
Ctrl
键,那么雕刻的效果是凸痕。
我们可以通过调整Radius
和Strength
来改变雕刻的作用范围和力度,也就是相当于调整凹痕或者凸痕的上下宽度和深度。
我们用Draw
这个工具在甜甜圈腰部雕刻一圈凹痕,记得在甜甜圈内侧也要雕刻一圈。
雕刻内侧时,可以将笔触和力度都缩小一些。
雕刻完之后,我们还可以使用Smooth
工具将凹痕不平整的地方完善一下,使其表现的更平滑细腻一些。该工具同样可以调整笔触大小和力度,方法同上。
经过调整后,我们第一步的雕刻就完成了,现在甜甜圈长着样。
接下来我们来雕刻流下来的糖衣部分,首先我们将加在糖衣上的修改器全部进行应用,方法同上,这里不在累赘。
然后使用Inflate
工具,请扫流下来的糖衣底部,这个工具的作用就是让所选部分膨胀。
在雕刻环节我们继续使用Draw
工具将糖衣上面的表面做一点坑洼的感觉,因为真实糖衣表面不会是非常光滑的,一定是会有坑洼的。
最后使用Grab
工具,将糖衣的边缘雕刻出一些锯齿。
雕刻完成后,我们进入Layout
视图,可以看到因为我们将甜甜圈腰部雕刻出了一圈凹痕,所以就导致了流下来的糖衣部分没有贴合在甜甜圈表面。
解决这个问题,可以进入编辑模式,选中这些翘起来糖衣拓扑结构上的点,通过移动和旋转操作,将它们拖拽到贴合甜甜圈的位置。
这里需要注意的是,依然要开启
Proportional Editing
模式,并作用范围要包含整个流下来的糖衣部分。
最后来看看调整完的甜甜圈,是不是更加逼真了许多。
]]>先来看一张真实的甜甜圈。
一共有三个特点:
这一节就是使用Blender的雕刻功能,将甜甜圈和糖衣的这几个特点刻画出来。
在开始之前,先回忆一下前面我们给甜甜圈和糖衣添加过修改器,但是我们并没有让修改器真正的生效,那么在雕刻前,需要将修改器的内容应用生效。在应用生效修改器之前,我们先对现在的白模复制一份,做个备份,因为修改器一旦生效后,整个拓扑都会发生变化。
选中甜甜圈和糖衣的白模,Shift + D
复制一份,然后按下M
键,新建一个集合,然后设置为不可见,相当于将复制出来的甜甜圈和糖衣做个备份。
目前甜甜圈的糖衣已经捏出了大体的样子,我们先来看看真正的甜甜圈上的糖衣是什么样的。
可以看到在烘培甜甜圈时,糖衣是会沿着甜甜圈流淌下去的,所以这个细节是需要我们刻画出来的。
选中糖衣,进入编辑模式,先取消Solidify修改器的效果。
选中某个点,按下A
键,即可选中该对象所有的点,Alt + A
或Option + A
是取消所有点的选中。
点击鼠标右键,在弹出菜单中选择Subdivide(细分)
。
对糖衣细分后,会发现拓扑结构变密了,也就是四边形的数量更多了,每细分一次,四边形数量多一倍。也就以为着我们可以捏出更多细节,因为点、线、面都变多了。另外在左下角弹出的细分设置中那个,我们也可以手动再增加细分,以及可以调整平滑度。这里我们只细分一次就足够了,然后将平滑度设置为1。
接下来有三个小技巧:
Alt
点击你想全选的那条纬度或经度。Ctrl + I
或者Command + I
可以反选,既选择其他圈上的所有点。H
键可以隐藏所选内容,Alt + H
或者Option + H
可以显示隐藏的内容。这样处理后,我们可以更聚焦在我们需要编辑的部分。
现在我们先把糖衣往下流淌的大概形状捏一下,依然选中Proportional Editing
模式,选择某个点,往下移动。此时我们应该会看到有一部分点穿进了甜甜圈的模型,俗称穿模。
我们更希望在移动点的时候,被移动的点能吸附在甜甜圈的表面上。所以我们可以使用Snap
工具,选择吸附到面,并且关联的所有点都吸附到面。
打开Snap
,将Snap To
选择为Face
,然后勾选上Project Individual Elements
。然后再对点进行调整。
调整完之后大概是这个样子。
接下来需要再完善细节,做几个流的比较低的糖衣流痕,也就是选择某个点往下移动的更低一些,看看会发生什么。
分别切换到对象模式和开了透视视图的编辑模式。
因为拓扑结构被拉的太长,导致都是糖衣那部分的面和甜甜圈的面接触,所以会导致穿模。解决这个问题的思路是额外增加可以表现糖衣流到底部的拓扑,而不是将原有的拓扑拉长。我们可以通过Extrude(挤出)
功能来实现。
选中一个点,按住Ctrl
或者Command
再点击相邻的另一个点,也就是选中两个点,选择Mesh → Extrude → Extrude Edges
,或者快捷键E
。往下移动这两个点,就会产生一个新的四边形(拓扑)。
我们可以多做几个类似的效果,宽度和长度(既一次选择几个点,往下移动多少距离)可以根据自己喜好调整。
我们仔细观察刚才挤出的糖衣部分,可以看到是有翘起来的感觉,没有和甜甜圈很好的贴合。
解决这个问题,可以调整Solidify修改器
中的Edge Data → Crease Inner
参数。该参数的作用相当于调整边缘内部折角的角度。将该属性调整为1,既将糖衣边缘内折角的角度调大,就可以和甜甜圈表面贴合起来了。
至此,甜甜圈糖衣白模的完善第一步就基本完成了。
]]>目前甜甜圈的糖衣已经捏出了大体的样子,我们先来看看真正的甜甜圈上的糖衣是什么样的。
可以看到在烘培甜甜圈时,糖衣是会沿着甜甜圈流淌下去的,所以这个细节是需要我们刻画出来的。
选中糖衣,进入编辑模式,先取消Solidify修改器的效果。
选中某个点,按下A
键,即可选中该对象所有的点,Alt + A
或Option + A
是取消所有点的选中。
甜甜圈的白模胚子已经有了,然后我们开始做甜甜圈上面那一层糖衣的制作。基本思路是需要创建一个糖衣的白模,然后摞在甜甜圈白模上面,并且这个糖衣的白模需要有以下三个特点:
我们依照上面三个思路来进行制作。
要想让糖衣的轮廓和甜甜圈完全一致,最好的做法就是复制甜甜圈上部1/3的部分。进入编辑模式,将视角切换到正前视图,然后用鼠标框选甜甜圈的上部1/3位置。
框选完之后,我们会发现,并没有按我们设想的那样把上部1/3的点和线都选中,而是只选中的一部分。
如果想要实现我们的设想,需要打开透视模式。
然后再切换到正前方的视角,重新框选上部1/3位置,这时就可以将上部1/3的点和线全部选中了。
然后按下Shift + D
复制选中的内容,再按下P
键,会弹出分离选择框,选择Selection
,意思是将复制的内容和被复制的物体分离。
此时可以在集合中看到又出现了一个对象,说明现在整个场景中,一共有四个对象,分别是:
可以根据喜好,修改对象的名称,以便能更好的区分不同的对象。
现在关闭透视模式,切换到对象模式,能够更清晰的看到糖衣的白模。
我们将视图放大,可以看到目前的糖衣的边缘和甜甜圈并没有贴合,而且比较薄。而实际的情况糖衣应该是有一定厚度,并且是和甜甜圈贴合在一起的。
解决这个问题,我们可以给糖衣添加一个Solidify
修改器,这个修改器的作用可以调整模型的厚度。添加了Solidify
修改器后,将Offset设置为1,然后将厚度(Thickness)设置为0.002m,这个可以根据个人喜好自行设置。然后就可以看到糖衣白模的变化,并且更加贴近真实。
此时我们选中糖衣,可以看到它的边缘轮廓不是很圆滑,有比较硬的角。
我们可以通过调整修改器的顺序来解决这个问题,我们将Subdivision
修改器调整到下面。因为修改器是从上到下的执行,如果Solidify
修改器在下面,会覆盖掉Subdivision
修改器。
将Subdivision
修改器调整到最下面后,可以看到糖衣的轮廓就没有之前的硬角了。
至此,糖衣的白模就基本完成了。
]]>甜甜圈的白模胚子已经有了,然后我们开始做甜甜圈上面那一层糖衣的制作。基本思路是需要创建一个糖衣的白模,然后摞在甜甜圈白模上面,并且这个糖衣的白模需要有以下三个特点:
我们依照上面三个思路来进行制作。
要想让糖衣的轮廓和甜甜圈完全一致,最好的做法就是复制甜甜圈上部1/3的部分。进入编辑模式,将视角切换到正前视图,然后用鼠标框选甜甜圈的上部1/3位置。
]]>我们删除了正方体,添加了一个圆环,添加圆环后,左下角会出现该物体的各项属性,可以进行调整:
先看Major Radius,它表示我们添加的物体的大小,Blender的默认单位是米,这里看到添加的圆环大小是1米,可以将它调整到一个甜甜圈的实际大小,可以直接5cm,Blender会自动换算单位。
从场景中的网格大小也可以对比出物体大小和视角远近的变化。如果想修改单位,可以在右侧的场景属性中进行设置。
物体整体的大小调整后,物体形状会变形,再通过Minor Radius调整整体的形状。
目前看到的圆环表面还是有很明显的棱角,整体看上去整个圆环是有若干个四边形组成的,可以通过调整Major Segments和Minor Segments调整横向和纵向的四边形的个数,个数越多,每个四边形面积越小,圆环表面整体看上去越光滑。
甜甜圈不可能是一个完全周正的圆环,所以我们需要将圆环的形状调整的更加真实一些,在视图左上角有切换模式的下拉菜单,当需要对物体进行编辑时,可以进入编辑模式(Edit Mode),或者可以使用快捷键Tab
快速切换。
进入编辑模式后,可以看到物体的拓扑结构,可以对点、线、面进行调整。
我们选中某个点,然后按下G
移动选中的点,可以看到该点周围的拓扑结构会发生变化,和该点相邻的四个面会被拉伸,从而圆环表面会出现一个尖角。
这种调整显然不符合我们的预期,我们希望在调整某个点时,和该点相邻的点能一起自适应的调整,这样才能调整出甜甜圈上的一些小坑洼、凹陷,以及圆环的长宽。那么我们需要用到按比例编辑的功能。
选中Proportional Editing
模式后,再选中某个点,按下G,会出现一个圆圈,滑动鼠标中键可以调整圆圈的大小。
如果没有看到圆圈,请滑动鼠标中键缩放,有可能是圆圈过大,超出了屏幕。
该圆圈就是用来圈定与选中的点一起联动的点的范围。选中同样的点向上移动,不同的联动范围有不同的表现。
现在我们可以使用Proportional Editing
模式对圆环进行微调,将表面调整的不那么光滑,有一些坑洼,将圆环也调整的不那么周正,从而更像一个真实的甜甜圈的样子。
按下Tab键
,切换到物体模式,选中圆环,点击鼠标左键
,在弹出的菜单中选择Shade Smooth
,让Blender自动对理物体表面进行光滑处理。
此时我们可以看到圆环的表面已经没有四边形的面了,取而代之的是光滑的表面,但是当我们把视图调整到正左、正右或者前后时,可以看到圆环的轮廓并不是光滑的,依然有棱角。
要解决这个问题,我们需要用到Blender的修改器。可以简单的理解为,我们可以给一个选中的物体添加多种效果从而改变该物体各个表象。选中物体,在右侧属性面板中可以找到一个扳手形状的菜单,这就是添加修改器的地方。
点击Add Modifier,可以看到有很多种修改器,我们选择Subdivision Surface(细分表面)
修改器。
我们可以将细分值调整为2,此时可以看到圆环的轮廓也变的光滑了。
到目前为止,一个甜甜圈的白模胚子的基本雏形就完成了。
]]>我们删除了正方体,添加了一个圆环,添加圆环后,左下角会出现该物体的各项属性,可以进行调整:
先看Major Radius,它表示我们添加的物体的大小,Blender的默认单位是米,这里看到添加的圆环大小是1米,可以将它调整到一个甜甜圈的实际大小,可以直接5cm,Blender会自动换算单位。
从场景中的网格大小也可以对比出物体大小和视角远近的变化。如果想修改单位,可以在右侧的场景属性中进行设置。
]]>鼠标中键
,可以基于原点旋转场景。Shift+鼠标中键
,移动场景。这8个选项分别是:
G
R
S
以上三个操作,都可以在按了对应快捷键后,再按下X或Y或Z,然后就可以沿着X轴、Y轴、Z轴进行移动、旋转或变换。或者当按下对应快捷键后,将鼠标移动到X轴、Y轴或Z轴附近,按下鼠标中键,也可以达到同样的效果。
Shift + D
,可以复制选中的物体。X
或者Delete
键可以删除选中的物体,比如删除初始化的正方体。Shift + A
,可以添加物体,比如添加一个圆环。鼠标中键
,可以基于原点旋转场景。Shift+鼠标中键
,移动场景。这8个选项分别是:
众所周知,游戏行业在当今的互联网行业中算是一棵常青树。在疫情之前的2019年,中国游戏市场营收规模约2884.8亿元,同比增长17.1%。2020年因为疫情,游戏行业更是突飞猛进。玩游戏本就是中国网民最普遍的娱乐方式之一,疫情期间更甚。据不完全统计,截止2019年,中国移动游戏用户规模约6.6亿人,占中国总网民规模8.47亿的77.92%,足以说明,游戏作为一种低门槛、低成本的娱乐手段,已成为大部分人生活中习以为常的一部分。
对于玩家而言,市面上的游戏数量多如牛毛,那么玩家如何能发现和认知到一款游戏,并且持续的玩下去恐怕是所有游戏厂商需要思考的问题。加之2018年游戏版号停发事件,游戏厂商更加珍惜每一个已获得版号的游戏产品,所以这也使得“深度打磨产品质量”和“提高运营精细程度”这两个游戏产业发展方向成为广大游戏厂商的发展思路,无论是新游戏还是老游戏都在努力落实这两点:
这里我们重点来看新游戏。一家游戏企业辛辛苦苦研发三年,等着新游戏发售时一飞冲天。那么问题来了,新游戏如何被广大玩家看到?先来看看游戏行业公司的分类:
游戏发行商:游戏发行商的主要工作分三大块:市场工作、运营工作、客服工作。游戏发行商把控游戏命脉,市场工作核心是导入玩家,运营工作核心是将用户价值最大化、赚取更多利益。
游戏平台/渠道商:游戏平台和渠道商的核心目的就是曝光游戏,让尽量多的人能发现你的游戏。
这三种类型的公司做的事情有各自专注在某一块领域的独立公司,也有一家公司把这三种事情全部都做的,但无论那一种,这三者之间的关系是不会变的:
所以不难理解,要想让更多的玩家看到你的游戏,游戏发行和运营是关键,通俗来讲就是如果你的游戏出现在所有目前大家熟知的平台的广告中,那么最起码游戏的新用户注册数量是很可观的。那么这就引出了一个关键词,买量。
根据数据显示,2019年月均买量手游数达6000+款,而2018年仅为4200款。另一方面,随着抖音、微博等超级APP在游戏买量市场的资源倾斜,也助推手游买量的效果和效率都有所提升,游戏厂商也更愿意使用买量的方式来吸引用户。但需要注意的是,在游戏买量的精准化程度不断提高的同时,买量的成本也在节节攀升,唯有合理配置买量、渠道与整合营销之间的关系,才能将宣发资源发挥到最大的效果。
通俗来讲,买量其实就是在各大主流平台投放广告,广大用户看到游戏广告后,有可能会点击广告,然后进入游戏厂商的宣传页面,同时会采集用户的一些信息,然后游戏厂商对采集到的用户信息进行大数据分析,进行进一步的定向推广。
游戏厂商花钱买量,换来用户信息以及新用户注册信息是为持续的游戏运营服务的,那么这个场景的核心诉求就是采集用户信息的完整性。比如说,某游戏厂商一天花5000w投放广告,在某平台某时段产生了每秒1w次的广告点击率,那么在这个时段内每一个点击广告的用户信息要完整的被采集到然后入库进行后续分析。这就对数据采集系统有着很高的要求,最核心的一点就是系统暴露接口的环节要能够平稳承载买量期间的不定时的流量脉冲。在买量期间,游戏厂商通常会在多个平台投放广告,每个平台投放广告的时间是不一样的,所以就出现全天不定时的流量脉冲现象。如果这个环节出现问题,那么相当于买量的钱就打水漂了。
上图是一个相对传统的数据采集系统的架构,最关键的就是暴露HTTP接口回传数据这部分,这部分如果出问题,那么采集数据的链路就断了。但这部分往往会面临两个挑战:
在传统架构下,通常情况在游戏有运营活动之前,会提前通知运维同学,对这个环节的服务增加节点,但要增加多少其实是无法预估的,只能大概拍一个数字。这就会导致两个问题:
我们可以通过Serverless函数计算(函数计算的基本概念可以参考这篇文章)来取代传统架构中暴露HTTP回传数据这部分,从而完美的解决传统架构中存在问题,先来看架构图:
传统架构中的两个问题均可以通过函数计算百毫秒弹性的特性来解决。我们并不需要去估算营销活动会带来多大的流量,也不需要去担心和考虑对数据采集系统的性能,运维同学更不需要提前预备ECS。
因为函数计算的极致弹性特性,当没有买量,没有营销活动的时候,函数计算的运行实例是零。有买量活动时,流量脉冲的情况下,函数计算会快速拉起实例来承载流量压力,当流量减少时函数计算会及时释放没有请求的实例进行缩容。所以Serverless架构带来的优势有以下三点:
从上面的架构图可以看到,整个采集数据阶段,分了两个函数来实现,第一个函数的作用是单纯的暴露HTTP接口接收数据,第二个函数用于处理数据,然后将数据发送至消息队列Kafka和数据库RDS。
我们打开函数计算控制台,创建一个函数:
创建好函数之后,我们通过在线编辑器编写代码:
# -*- coding: utf-8 -*- |
此时的代码非常简单,就是接收用户传来的参数,我们可以调用接口进行验证:
可以在函数的日志查询中看到此次调用的日志:
同时,我们也可以查看函数的链路追踪来分析每一个步骤的调用耗时,比如函数接到请求→冷启动(无活跃实例时)→准备代码→执行初始化方法→执行入口函数逻辑这个过程:
从调用链路图中可以看到,刚才的那次请求包含了冷启动的时间,因为当时没有活跃实例,整个过程耗时418毫秒,真正执行入口函数代码的时间为8毫秒。
当再次调用接口时,可以看到就直接执行了入口函数的逻辑,因为此时已经有实例在运行,整个耗时只有2.3毫秒:
第一个函数是通过在函数计算控制台在界面上创建的,选择了运行环境是Python3,我们可以在官方文档中查看预置的Python3运行环境内置了哪些模块,因为第二个函数要操作Kafka和RDS,所以需要我们确认对应的模块。
从文档中可以看到,内置的模块中包含RDS的SDK模块,但是没有Kafka的SDK模块,此时就需要我们手动安装Kafka SDK模块,并且创建函数也会使用另一种方式。
Funcraft是一个用于支持 Serverless 应用部署的命令行工具,能帮助我们便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml
),协助我们进行开发、构建、部署操作。
所以第二个函数我们需要使用Fun来进行操作,整个操作分为四个步骤:
template.yml
模板文件,用来描述函数。Fun提供了三种安装方式:
文本示例环境为Mac,所以使用npm方式安装,非常的简单,一行命令搞定:
sudo npm install @alicloud/fun -g |
安装完成之后。在控制终端输入 fun 命令可以查看版本信息:
$ fun --version |
在第一次使用 fun 之前需要先执行 fun config
命令进行配置,按照提示,依次配置 Account ID、Access Key Id、Secret Access Key、 Default Region Name 即可。其中 Account ID、Access Key Id 你可以从函数计算控制台首页的右上方获得:
fun config |
新建一个目录,在该目录下创建一个名为template.yml
的YAML文件,该文件主要描述要创建的函数的各项配置,说白了就是将函数计算控制台上配置的那些配置信息以YAML格式写在文件里:
ROSTemplateFormatVersion: '2015-09-01' Transform: 'Aliyun::Serverless-2018-04-03' Resources: FCBigDataDemo: Type: 'Aliyun::Serverless::Service' Properties: Description: 'local invoke demo' VpcConfig: VpcId: 'vpc-xxxxxxxxxxx' VSwitchIds: [ 'vsw-xxxxxxxxxx' ] SecurityGroupId: 'sg-xxxxxxxxx' LogConfig: Project: fcdemo Logstore: fc_demo_store dataToKafka: Type: 'Aliyun::Serverless::Function' Properties: Initializer: index.my_initializer Handler: index.handler CodeUri: './' Description: '' Runtime: python3 |
我们来解析以上文件的核心内容:
Type
属性标明是服务,即Aliyun::Serverless::Service
。Type
属性标明是函数,即Aliyun::Serverless::Function
。目录结构为:
服务和函数的模板创建好之后,我们来安装需要使用的第三方依赖。在这个示例的场景中,第二个函数需要使用Kafka SDK,所以可以通过fun工具结合Python包管理工具pip进行安装:
fun install --runtime python3 --package-type pip kafka-python |
执行命令后有如下提示信息:
此时我们会发现在目录下会生成一个.fun
文件夹 ,我们安装的依赖包就在该目录下:
现在编写好了模板文件以及安装好了我们需要的Kafka SDK后,还需要添加我们的代码文件index.py
,代码内容如下:
# -*- coding: utf-8 -*- |
代码很简单,这里做以简单的解析:
my_initializer
:函数实例被拉起时会先执行该函数,然后再执行handler
函数 ,当函数实例在运行时,之后的请求都不会执行my_initializer
函数 。一般用于各种连接的初始化工作,这里将初始化Kafka Producer的方法放在了这里,避免反复初始化Produer。handler
:该函数只有两个逻辑,接收回传的数据和将数据发送至Kafka的指定Topic。下面通过fun deploy
命令部署函数,该命令会做两件事:
template.yml
中的配置创建服务和函数。index.py
和.fun
上传至函数中。登录函数计算控制台,可以看到通过fun
命令部署的服务和函数:
进入函数,也可以清晰的看到第三方依赖包的目录结构:
目前两个函数都创建好了,下面的工作就是由第一个函数接收到数据后拉起第二个函数发送消息给Kafka。我们只需要对第一个函数做些许改动即可:
# -*- coding: utf-8 -*- |
如上面代码所示,对第一个函数的代码做了三个地方的改动:
import fc2
添加初始化方法,用于创建函数计算Client:
def my_initializer(context): |
这里需要注意的时,当我们在代码里增加了初始化方法后,需要在函数配置中指定初始化方法的入口:
通过函数计算Client调用第二个函数:
global client |
invoke_function
函数有四个参数:
x-fc-invocation-type
这个Key来设置是同步调用还是异步调用。这里设置Async
为异步调用。如此设置,我们便可以验证通过第一个函数提供的HTTP接口发起请求→采集数据→调用第二个函数→将数据作为消息传给Kafka这个流程了。
到这里有些同学可能会有疑问,为什么需要两个函数,而不在第一个函数里直接向Kafka发送数据呢?我们先来看这张图:
当我们使用异步调用函数时,在函数内部会默认先将请求的数据放入消息队列进行第一道削峰填谷,然后每一个队列在对应函数实例,通过函数实例的弹性拉起多个实例进行第二道削峰填谷。所以这也就是为什么这个架构能稳定承载大并发请求的核心原因之一。
在游戏运营这个场景中,数据量是比较大的,所以对Kafka的性能要求也是比较高的,相比开源自建,使用云上的Kafka省去很多的运维操作,比如:
总的来说,就是一切SLA都有云上兜底,我们只需要关注在消息发送和消息消费即可。
所以我们可以打开Kafka开通界面,根据实际场景的需求一键开通Kafka实例,开通Kafka后登录控制台,在基本信息中可以看到Kafka的接入点:
将默认接入点配置到函数计算的第二个函数中即可。
.... |
然后点击左侧控制台Topic管理,创建Topic:
将创建好的Topic配置到函数计算的第二个函数中即可。
... |
上文已经列举过云上Kafka的优势,比如动态增加Topic的分区数,我们可以在Topic列表中,对Topic的分区数进行动态调整:
单Topic最大支持到360个分区,这是开源自建无法做到的。
接下来点击控制台左侧Consumer Group管理,创建Consumer Group:
至此,云上的Kafka就算配置完毕了,即Producer可以往刚刚创建的Topic中发消息了,Consumer可以设置刚刚创建的GID以及订阅Topic进行消息接受和消费。
在这个场景中,Kafka后面往往会跟着Flink,所以这里简要给大家介绍一下在Flink中如何创建Kafka Consumer并消费数据。代码片段如下:
final ParameterTool parameterTool = ParameterTool.fromArgs(args); |
以上就是构建Flink Kafka Consumer和添加Kafka Source的代码片段,还是非常简单的。
至此,整个数据采集的架构就搭建完毕了,下面我们通过压测来检验一下整个架构的性能。这里使用阿里云PTS来进行压测。
打开PTS控制台,点击左侧菜单创建压测/创建PTS场景:
在场景配置中,将第一个函数计算函数暴露的HTTP接口作为串联链路,配置如下图所示:
接口配置完后,我们来配置施压:
这里因为资源成本原因,并发用户数设置为2500来进行验证。
从上图压测中的情况来看,TPS达到了2w的封顶,549w+的请求,99.99%的请求是成功的,那369个异常也可以点击查看,都是压测工具请求超时导致的。
至此,整个基于Serverless搭建的大数据采集传输的架构就搭建好了,并且进行了压测验证,整体的性能也是不错的。并且整个架构搭建起来也是非常简单和容易理解的。这个架构不光适用于游戏运营行业,其实任何大数据采集传输的场景都是适用的,目前也已经有很多客户正在基于Serverless的架构跑在生产环境,或者正走在改造Serverless架构的路上。
]]>众所周知,游戏行业在当今的互联网行业中算是一棵常青树。在疫情之前的2019年,中国游戏市场营收规模约2884.8亿元,同比增长17.1%。2020年因为疫情,游戏行业更是突飞猛进。玩游戏本就是中国网民最普遍的娱乐方式之一,疫情期间更甚。据不完全统计,截止2019年,中国移动游戏用户规模约6.6亿人,占中国总网民规模8.47亿的77.92%,足以说明,游戏作为一种低门槛、低成本的娱乐手段,已成为大部分人生活中习以为常的一部分。
对于玩家而言,市面上的游戏数量多如牛毛,那么玩家如何能发现和认知到一款游戏,并且持续的玩下去恐怕是所有游戏厂商需要思考的问题。加之2018年游戏版号停发事件,游戏厂商更加珍惜每一个已获得版号的游戏产品,所以这也使得“深度打磨产品质量”和“提高运营精细程度”这两个游戏产业发展方向成为广大游戏厂商的发展思路,无论是新游戏还是老游戏都在努力落实这两点:
这里我们重点来看新游戏。一家游戏企业辛辛苦苦研发三年,等着新游戏发售时一飞冲天。那么问题来了,新游戏如何被广大玩家看到?先来看看游戏行业公司的分类:
游戏发行商:游戏发行商的主要工作分三大块:市场工作、运营工作、客服工作。游戏发行商把控游戏命脉,市场工作核心是导入玩家,运营工作核心是将用户价值最大化、赚取更多利益。
游戏平台/渠道商:游戏平台和渠道商的核心目的就是曝光游戏,让尽量多的人能发现你的游戏。
随着互联网人口红利逐渐减弱,基于流量的增长已经放缓,互联网行业迫切需要找到一片足以承载自身持续增长的新蓝海。产业互联网正是这一宏大背景下的新趋势。我们看到互联网浪潮正在席卷传统行业,云计算、大数据、人工智能开始大规模融入到金融、制造、物流、零售、文娱、教育、医疗等行业的生产环节中,这种融合称为产业互联网。而在产业互联网中,有一块不可小觑的领域是SaaS领域,它是ToB赛道的中间力量。比如CRM、HRM、费控系统、财务系统、协同办公等等。
在消费互联网时代,大家是搜我想要的东西,各个厂商在云计算、大数据、人工智能等技术基座之上建立流量最大化的服务与生态,基于海量内容分发与流量共享为逻辑构建系统。而到了产业互联网时代,供给关系发生了变化,大家是定制我想要的东西,需要从供给与需求两侧出发进行双向建设,这个时候系统的灵活性和扩展性面临着前所未有的挑战,尤其是ToB的SaaS领域。
尤其当下的经济环境,SaaS厂商要明白,不能再通过烧钱的方式,只关注在自己的用户数量上,而更多的要思考如何帮助客户降低成本、增加效率,所以需要将更多的精力放在自己产品的定制化能力上。
SaaS领域中的佼佼者Salesforce,将CRM的概念扩展到Marketing、Sales、Service,而这三块领域中只有Sales有专门的SaaS产品,其他两个领域都是各个ISV在不同行业的行业解决方案,靠的是什么?毋庸置疑,是Salesforce强大的aPaaS平台。ISV、内部实施、客户均可以在各自维度通过aPaaS平台构建自己行业、自己领域的SaaS系统,建立完整的生态。所以在我看来,现在的Salesforce已经由一家SaaS公司升华为一家aPaaS平台公司了。这种演进的过程也印证了消费互联网和产业互联网的转换逻辑以及后者的核心诉求。
然而不是所有SaaS公司都有财力和时间去孵化和打磨自己的aPaaS平台,但市场的变化、用户的诉求是实实在在存在的,若要生存,就要求变。这个变的核心就是能够让自己目前的SaaS系统变的灵活起来。相对建设困难的aPaaS平台,我们其实可以选择轻量且有效的Serverless方案来提升现有系统的灵活性和可扩展性,从而实现用户不同的定制需求。
在上一篇文章《资源成本双优化!看Serverless颠覆编程教育的创新实践》中,已经对Serverless的概念做过阐述了,并且也介绍了Serverless函数计算(FC)的概念和实践。这篇文章中介绍一下构建系统灵活性的核心要素服务编排,Serverless工作流。
Serverless 工作流(FnF)是一个用来协调多个分布式任务执行的全托管云服务。在 Serverless工作流中,可以用顺序、分支、并行等方式来编排分布式任务,Serverless工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行您定义的重试逻辑,以确保工作流顺利完成。Serverless工作流通过提供日志记录和审计来监视工作流的执行,可以轻松地诊断和调试应用。
下面这张图描述了Serverless工作流如何协调分布式任务,这些任务可以是函数、已集成云服务API、运行在虚拟机或容器上的程序。
看完Serverless工作流的介绍,大家可能已经多少有点思路了吧。系统灵活性和可扩展性的核心是服务可编排,无论是以前的BPM还是现在的aPaaS。所以基于Serverless工作流重构SaaS系统灵活性方案的核心思路,是将系统内用户最希望定制的功能进行梳理、拆分、抽离,再配合函数计算(FC)提供无状态的能力,通过Serverless工作流进行这些功能点的编排,从而实现不同的业务流程。
订餐场景相信大家都不会陌生,在家叫外卖或者在餐馆点餐,都涉及到这个场景。当下也有很多提供点餐系统的SaaS服务厂商,有很多不错的SaaS点餐系统。随着消费互联网向产业互联网转换,这些SaaS点餐系统面临的定制化的需求也越来越多,其中有一个需求是不同的商家在支付时会显示不同的支付方式,比如从A商家点餐后付款时显示支付宝、微信支付、银联支付,从B商家点餐后付款时显示支付宝、京东支付。突然美团又冒出来了美团支付,此时B商家接了美团支付,那么从B商家点餐后付款时显示支付宝、京东支付、美团支付。诸如此类的定制化需求越来越多,这些SaaS产品如果没有PaaS平台,那么就会疲于不断的通过硬代码增加条件判断来实现不同商家的需求,这显然不是一个可持续发展的模式。
那么我们来看看通过Serverless函数计算和Serverless工作流如何优雅的解决这个问题。先来看看这个点餐流程:
首选我需要将上面用户侧的流程转变为程序侧的流程,此时就需要使用Serverless工作流来担任此任务了。
打开Serverless控制台,创建订餐流程,这里Serverless工作流使用流程定义语言FDL创建工作流,如何使用FDL创建工作流请参阅文档。流程图如下图所示:
FDL代码为:version: v1beta1
type: flow
timeoutSeconds: 3600
steps:
- type: task
name: generateInfo
timeoutSeconds: 300
resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
pattern: waitForCallback
inputMappings:
- target: taskToken
source: $context.task.token
- target: products
source: $input.products
- target: supplier
source: $input.supplier
- target: address
source: $input.address
- target: orderNum
source: $input.orderNum
- target: type
source: $context.step.name
outputMappings:
- target: paymentcombination
source: $local.paymentcombination
- target: orderNum
source: $local.orderNum
serviceParams:
MessageBody: $
Priority: 1
catch:
- errors:
- FnF.TaskTimeout
goto: orderCanceled
- type: task
name: payment
timeoutSeconds: 300
resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
pattern: waitForCallback
inputMappings:
- target: taskToken
source: $context.task.token
- target: orderNum
source: $local.orderNum
- target: paymentcombination
source: $local.paymentcombination
- target: type
source: $context.step.name
outputMappings:
- target: paymentMethod
source: $local.paymentMethod
- target: orderNum
source: $local.orderNum
- target: price
source: $local.price
- target: taskToken
source: $input.taskToken
serviceParams:
MessageBody: $
Priority: 1
catch:
- errors:
- FnF.TaskTimeout
goto: orderCanceled
- type: choice
name: paymentCombination
inputMappings:
- target: orderNum
source: $local.orderNum
- target: paymentMethod
source: $local.paymentMethod
- target: price
source: $local.price
- target: taskToken
source: $local.taskToken
choices:
- condition: $.paymentMethod == "zhifubao"
steps:
- type: task
name: zhifubao
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
- condition: $.paymentMethod == "weixin"
steps:
- type: task
name: weixin
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
- condition: $.paymentMethod == "unionpay"
steps:
- type: task
name: unionpay
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
default:
goto: orderCanceled
- type: task
name: orderCompleted
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/orderCompleted
end: true
- type: task
name: orderCanceled
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/cancerOrder
在解析整个流程之前,我先要说明的一点是,我们不是完全通过Serverless函数计算和Serverless工作流来搭建订餐模块,只是用它来解决灵活性的问题,所以这个示例的主体应用是Java编写的,然后结合了Serverless函数计算和Serverless工作流。下面我们来详细解析这个流程。
按常理,开始点餐时流程就应该启动了,所以在这个示例中,我的设计是当我们选择完商品、商家、填完地址后启动流程:
这里我们通过Serverless工作流提供的OpenAPI来启动流程。
这个示例我使用Serverless工作流的Java SDK,首先在POM文件中添加依赖:<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>[4.3.2,5.0.0)</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-fnf</artifactId>
<version>[1.0.0,5.0.0)</version>
</dependency>
然后创建初始化Java SDK的Config类:@Configuration
public class FNFConfig {
@Bean
public IAcsClient createDefaultAcsClient(){
DefaultProfile profile = DefaultProfile.getProfile(
"cn-xxx", // 地域ID
"ak", // RAM 账号的AccessKey ID
"sk"); // RAM 账号Access Key Secret
IAcsClient client = new DefaultAcsClient(profile);
return client;
}
}
再来看Controller中的startFNF
方法,该方法暴露GET方式的接口,传入三个参数:
fnfname
:要启动的流程名称。execuname
:流程启动后的流程实例名称。input
:启动输入参数,比如业务参数。@GetMapping("/startFNF/{fnfname}/{execuname}/{input}") |
再来看Service中的startFNF
方法,该方法分两部分,第一个部分是启动流程,第二部分是创建订单对象,并模拟入库(示例中是放在Map里了):@Override
public StartExecutionResponse startFNF(JSONObject jsonObject) throws ClientException {
StartExecutionRequest request = new StartExecutionRequest();
String orderNum = jsonObject.getString("execuname");
request.setFlowName(jsonObject.getString("fnfname"));
request.setExecutionName(orderNum);
request.setInput(jsonObject.getString("input"));
JSONObject inputObj = jsonObject.getJSONObject("input");
Order order = new Order();
order.setOrderNum(orderNum);
order.setAddress(inputObj.getString("address"));
order.setProducts(inputObj.getString("products"));
order.setSupplier(inputObj.getString("supplier"));
orderMap.put(orderNum, order);
return iAcsClient.getAcsResponse(request);
}
启动流程时,流程名称和启动流程实例的名称是需要传入的参数,这里我将每次的订单编号作为启动流程的实例名称。至于Input,可以根据需求构造JSON字符串传入。这里我将商品、商家、地址、订单号构造了JSON字符串在流程启动时传入流程中。
另外,创建了此次订单的Order
实例,并存在Map
中,模拟入库,后续环节还会查询该订单实例更新订单属性。
前端我使用VUE搭建,当点击选择商品和商家页面中的下一步后,通过GET方式调用HTTP协议的接口/startFNF/{fnfname}/{execuname}/{input}
。和上面的Java方法对应。
fnfname
:要启动的流程名称。execuname
:随机生成uuid,作为订单的编号,也作为启动流程实例的名称。input
:将商品、商家、订单号、地址构建为JSON字符串传入流程。submitOrder(){ |
第一个节点generateInfo
,先来看看FDL的含义:- type: task
name: generateInfo
timeoutSeconds: 300
resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
pattern: waitForCallback
inputMappings:
- target: taskToken
source: $context.task.token
- target: products
source: $input.products
- target: supplier
source: $input.supplier
- target: address
source: $input.address
- target: orderNum
source: $input.orderNum
- target: type
source: $context.step.name
outputMappings:
- target: paymentcombination
source: $local.paymentcombination
- target: orderNum
source: $local.orderNum
serviceParams:
MessageBody: $
Priority: 1
catch:
- errors:
- FnF.TaskTimeout
goto: orderCanceled
name
:节点名称。timeoutSeconds
:超时时间。该节点等待的时长,超过时间后会跳转到goto
分支指向的orderCanceled
节点。pattern
:设置为waitForCallback
,表示需要等待确认。inputMappings
:该节点入参。taskToken
:Serverless工作流自动生成的Token。products
:选择的商品。supplier
:选择的商家。address
:送餐地址。orderNum
:订单号。outputMappings
:该节点的出参。paymentcombination
:该商家支持的支付方式。orderNum
:订单号。catch
:捕获异常,跳转到其他分支。这里resourceArn
和serviceParams
需要拿出来单独解释。Serverless工作流支持与多个云服务集成,即将其他服务作为任务步骤的执行单元。服务集成方式由FDL语言表达,在任务步骤中,可以使用resourceArn
来定义集成的目标服务,使用pattern
定义集成模式。所以可以看到在resourceArn
中配置acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
信息,即在generateInfo
节点中集成了MNS消息队列服务,当generateInfo
节点触发后会向generateInfo-fnf-demo-jiyuan
Topic中发送一条消息。那么消息正文和参数则在serviceParams
对象中指定。MessageBody
是消息正文,配置$
表示通过输入映射inputMappings
产生消息正文。
看完第一个节点的示例,大家可以看到,在Serverless工作流中,节点之间的信息传递可以通过集成MNS发送消息来传递,也是使用比较广泛的方式之一。
向generateInfo-fnf-demo-jiyuan
Topic中发送的这条消息包含了商品信息、商家信息、地址、订单号,表示一个下订单流程的开始,既然有发消息,那么必然有接受消息进行后续处理。所以打开函数计算控制台,创建服务,在服务下创建名为generateInfo-fnf-demo
的事件触发器函数,这里选择Python Runtime:
创建MNS触发器,选择监听generateInfo-fnf-demo-jiyuan
Topic。
打开消息服务MNS控制台,创建generateInfo-fnf-demo-jiyuan
Topic:
做好函数的准备工作,我们来开始写代码:# -*- coding: utf-8 -*-
import logging
import json
import time
import requests
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
def handler(event, context):
# 1. 构建Serverless工作流Client
region = "cn-hangzhou"
account_id = "XXXX"
ak_id = "XXX"
ak_secret = "XXX"
fnf_client = AcsClient(
ak_id,
ak_secret,
region
)
logger = logging.getLogger()
# 2. event内的信息即接受到Topic generateInfo-fnf-demo-jiyuan中的消息内容,将其转换为Json对象
bodyJson = json.loads(event)
logger.info("products:" + bodyJson["products"])
logger.info("supplier:" + bodyJson["supplier"])
logger.info("address:" + bodyJson["address"])
logger.info("taskToken:" + bodyJson["taskToken"])
supplier = bodyJson["supplier"]
taskToken = bodyJson["taskToken"]
orderNum = bodyJson["orderNum"]
# 3. 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取
paymentcombination = ""
if supplier == "haidilao":
paymentcombination = "zhifubao,weixin"
else:
paymentcombination = "zhifubao,weixin,unionpay"
# 4. 调用Java服务暴露的接口,更新订单信息,主要是更新支付方式
url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentcombination + "/0"
x = requests.get(url)
# 5. 给予generateInfo节点响应,并返回数据,这里返回了订单号和支付方式
output = "{\"orderNum\": \"%s\", \"paymentcombination\":\"%s\" " \
"}" % (orderNum, paymentcombination)
request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
request.set_Output(output)
request.set_TaskToken(taskToken)
resp = fnf_client.do_action_with_exception(request)
return 'hello world'
因为generateInfo-fnf-demo
函数配置了MNS触发器,所以当TopicgenerateInfo-fnf-demo-jiyuan
有消息后就会触发执行generateInfo-fnf-demo
函数。
整个代码分五部分:
generateInfo-fnf-demo-jiyuan
中的消息内容,将其转换为Json对象。generateInfo
节点响应,并返回数据,这里返回了订单号和支付方式。因为该节点的pattern
是waitForCallback
,所以需要等待响应结果。我们再来看第二个节点payment
,先来看FDL代码:- type: task
name: payment
timeoutSeconds: 300
resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
pattern: waitForCallback
inputMappings:
- target: taskToken
source: $context.task.token
- target: orderNum
source: $local.orderNum
- target: paymentcombination
source: $local.paymentcombination
- target: type
source: $context.step.name
outputMappings:
- target: paymentMethod
source: $local.paymentMethod
- target: orderNum
source: $local.orderNum
- target: price
source: $local.price
- target: taskToken
source: $input.taskToken
serviceParams:
MessageBody: $
Priority: 1
catch:
- errors:
- FnF.TaskTimeout
goto: orderCanceled
当流程流转到payment
节点后,意味着用户进入了支付页面。
这时payment
节点会向MNS的Topicpayment-fnf-demo-jiyuan
发送消息,会触发payment-fnf-demo
函数。
payment-fnf-demo
函数的创建方式和generateInfo-fnf-demo
函数类似,这里不再累赘。我们直接来看代码:# -*- coding: utf-8 -*-
import logging
import json
import os
import time
import logging
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkcore.client import AcsClient
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
from mns.account import Account # pip install aliyun-mns
from mns.queue import *
def handler(event, context):
logger = logging.getLogger()
region = "xxx"
account_id = "xxx"
ak_id = "xxx"
ak_secret = "xxx"
mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
queue_name = "payment-queue-fnf-demo"
my_account = Account(mns_endpoint, ak_id, ak_secret)
my_queue = my_account.get_queue(queue_name)
# my_queue.set_encoding(False)
fnf_client = AcsClient(
ak_id,
ak_secret,
region
)
eventJson = json.loads(event)
isLoop = True
while isLoop:
try:
recv_msg = my_queue.receive_message(30)
isLoop = False
# body = json.loads(recv_msg.message_body)
logger.info("recv_msg.message_body:======================" + recv_msg.message_body)
msgJson = json.loads(recv_msg.message_body)
my_queue.delete_message(recv_msg.receipt_handle)
# orderCode = int(time.time())
task_token = eventJson["taskToken"]
orderNum = eventJson["orderNum"]
output = "{\"orderNum\": \"%s\", \"paymentMethod\": \"%s\", \"price\": \"%s\" " \
"}" % (orderNum, msgJson["paymentMethod"], msgJson["price"])
request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
request.set_Output(output)
request.set_TaskToken(task_token)
resp = fnf_client.do_action_with_exception(request)
except Exception as e:
logger.info("new loop")
return 'hello world'
该函数的核心思路是等待用户在支付页面选择某个支付方式确认支付。所以这里使用了MNS的队列来模拟等待。循环等待接收队列payment-queue-fnf-demo
中的消息,当收到消息后将订单号和用户选择的具体支付方式以及金额返回给payment
节点。
因为经过generateInfo
节点后,该订单的支付方式信息已经有了,所以对于用户而言,当填完商品、商家、地址后,跳转到的页面就是该确认支付页面,并且包含了该商家支持的支付方式。
当进入该页面后,会请求Java服务暴露的接口,获取订单信息,根据支付方式在页面上显示不同的支付方式。代码片段如下:
当用户选定某个支付方式点击提交订单按钮后,向payment-queue-fnf-demo
队列发送消息,即通知payment-fnf-demo
函数继续后续的逻辑。
这里我使用了一个HTTP触发器类型的函数,用于实现向MNS发消息的逻辑,paymentMethod-fnf-demo
函数代码如下。# -*- coding: utf-8 -*-
import logging
import urllib.parse
import json
from mns.account import Account # pip install aliyun-mns
from mns.queue import *
HELLO_WORLD = b'Hello world!\n'
def handler(environ, start_response):
logger = logging.getLogger()
context = environ['fc.context']
request_uri = environ['fc.request_uri']
for k, v in environ.items():
if k.startswith('HTTP_'):
# process custom request headers
pass
try:
request_body_size = int(environ.get('CONTENT_LENGTH', 0))
except (ValueError):
request_body_size = 0
request_body = environ['wsgi.input'].read(request_body_size)
paymentMethod = urllib.parse.unquote(request_body.decode("GBK"))
logger.info(paymentMethod)
paymentMethodJson = json.loads(paymentMethod)
region = "cn-xxx"
account_id = "xxx"
ak_id = "xxx"
ak_secret = "xxx"
mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
queue_name = "payment-queue-fnf-demo"
my_account = Account(mns_endpoint, ak_id, ak_secret)
my_queue = my_account.get_queue(queue_name)
output = "{\"paymentMethod\": \"%s\", \"price\":\"%s\" " \
"}" % (paymentMethodJson["paymentMethod"], paymentMethodJson["price"])
msg = Message(output)
my_queue.send_message(msg)
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [HELLO_WORLD]
该函数的逻辑很简单,就是向MNS的队列payment-queue-fnf-demo
发送用户选择的支付方式和金额。
VUE代码片段如下:
paymentCombination
节点是一个路由节点,通过判断某个参数路由到不同的节点,这里自然使用paymentMethod
作为判断条件。FDL代码如下:- type: choice
name: paymentCombination
inputMappings:
- target: orderNum
source: $local.orderNum
- target: paymentMethod
source: $local.paymentMethod
- target: price
source: $local.price
- target: taskToken
source: $local.taskToken
choices:
- condition: $.paymentMethod == "zhifubao"
steps:
- type: task
name: zhifubao
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
- condition: $.paymentMethod == "weixin"
steps:
- type: task
name: weixin
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
- condition: $.paymentMethod == "unionpay"
steps:
- type: task
name: unionpay
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
default:
goto: orderCanceled
这里的流程是,用户选择支付方式后,通过消息发送给payment-fnf-demo
函数,然后将支付方式返回,于是流转到paymentCombination
节点通过判断支付方式流转到具体处理支付逻辑的节点和函数。
我们具体来看一个zhifubao
节点:choices:
- condition: $.paymentMethod == "zhifubao"
steps:
- type: task
name: zhifubao
resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
inputMappings:
- target: price
source: $input.price
- target: orderNum
source: $input.orderNum
- target: paymentMethod
source: $input.paymentMethod
- target: taskToken
source: $input.taskToken
这个节点的resourceArn
和之前两个节点的不同,这里配置的是函数计算中函数的ARN,也就是说当流程流转到这个节点时会触发zhifubao-fnf-demo
函数,该函数是一个事件触发函数,但不需要创建任何触发器。流程将订单金额、订单号、支付方式传给zhifubao-fnf-demo
函数。
来看zhifubao-fnf-demo
函数的代码:# -*- coding: utf-8 -*-
import logging
import json
import requests
import urllib.parse
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
def handler(event, context):
region = "cn-xxx"
account_id = "xxx"
ak_id = "xxx"
ak_secret = "xxx"
fnf_client = AcsClient(
ak_id,
ak_secret,
region
)
logger = logging.getLogger()
logger.info(event)
bodyJson = json.loads(event)
price = bodyJson["price"]
taskToken = bodyJson["taskToken"]
orderNum = bodyJson["orderNum"]
paymentMethod = bodyJson["paymentMethod"]
logger.info("price:" + price)
newPrice = int(price) * 0.8
logger.info("newPrice:" + str(newPrice))
url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentMethod + "/" + str(newPrice)
x = requests.get(url)
return {"Status":"ok"}
示例中的代码逻辑很简单,接收到金额后,将金额打8折,然后将价格更新回订单。其他支付方式的节点和函数如法炮制,变更实现逻辑就可以,在这个示例中,微信支付打了5折,银联支付打7折。
流程中的orderCompleted
和orderCanceled
节点没做什么逻辑,大家可以自行发挥,思路和之前的节点一样。所以完整的流程是这样:
从Serverless工作流中看到的节点流转是这样的:
到此,我们基于Serverless工作流和Serverless函数计算构建的订单模块示例就算完成了,在示例中,有两个点需要大家注意:
所以如果之后需要接入其他的支付方式,只需在paymentCombination
路由节点中确定好路由规则,然后增加对应的支付方式函数即可。通过增加元数据配置项,就可以在页面显示新加的支付方式,并且路由到处理新支付方式的函数中。
随着互联网人口红利逐渐减弱,基于流量的增长已经放缓,互联网行业迫切需要找到一片足以承载自身持续增长的新蓝海。产业互联网正是这一宏大背景下的新趋势。我们看到互联网浪潮正在席卷传统行业,云计算、大数据、人工智能开始大规模融入到金融、制造、物流、零售、文娱、教育、医疗等行业的生产环节中,这种融合称为产业互联网。而在产业互联网中,有一块不可小觑的领域是SaaS领域,它是ToB赛道的中间力量。比如CRM、HRM、费控系统、财务系统、协同办公等等。
在消费互联网时代,大家是搜我想要的东西,各个厂商在云计算、大数据、人工智能等技术基座之上建立流量最大化的服务与生态,基于海量内容分发与流量共享为逻辑构建系统。而到了产业互联网时代,供给关系发生了变化,大家是定制我想要的东西,需要从供给与需求两侧出发进行双向建设,这个时候系统的灵活性和扩展性面临着前所未有的挑战,尤其是ToB的SaaS领域。
尤其当下的经济环境,SaaS厂商要明白,不能再通过烧钱的方式,只关注在自己的用户数量上,而更多的要思考如何帮助客户降低成本、增加效率,所以需要将更多的精力放在自己产品的定制化能力上。
SaaS领域中的佼佼者Salesforce,将CRM的概念扩展到Marketing、Sales、Service,而这三块领域中只有Sales有专门的SaaS产品,其他两个领域都是各个ISV在不同行业的行业解决方案,靠的是什么?毋庸置疑,是Salesforce强大的aPaaS平台。ISV、内部实施、客户均可以在各自维度通过aPaaS平台构建自己行业、自己领域的SaaS系统,建立完整的生态。所以在我看来,现在的Salesforce已经由一家SaaS公司升华为一家aPaaS平台公司了。这种演进的过程也印证了消费互联网和产业互联网的转换逻辑以及后者的核心诉求。
然而不是所有SaaS公司都有财力和时间去孵化和打磨自己的aPaaS平台,但市场的变化、用户的诉求是实实在在存在的,若要生存,就要求变。这个变的核心就是能够让自己目前的SaaS系统变的灵活起来。相对建设困难的aPaaS平台,我们其实可以选择轻量且有效的Serverless方案来提升现有系统的灵活性和可扩展性,从而实现用户不同的定制需求。
在上一篇文章《资源成本双优化!看Serverless颠覆编程教育的创新实践》中,已经对Serverless的概念做过阐述了,并且也介绍了Serverless函数计算(FC)的概念和实践。这篇文章中介绍一下构建系统灵活性的核心要素服务编排,Serverless工作流。
Serverless 工作流(FnF)是一个用来协调多个分布式任务执行的全托管云服务。在 Serverless工作流中,可以用顺序、分支、并行等方式来编排分布式任务,Serverless工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行您定义的重试逻辑,以确保工作流顺利完成。Serverless工作流通过提供日志记录和审计来监视工作流的执行,可以轻松地诊断和调试应用。
]]>说起Serverless这个词,我想大家应该都不陌生,那么Serverless这个词到底是什么意思?Serverless到底能解决什么问题?可能很多朋友还没有深刻的体会和体感。这篇文章我就和大家一起聊聊Serverless。
我们先将Serverless这个词拆开来看。Server,大家都知道是服务器的意思,说明Serverless解决的问题范围在服务端。Less,大家肯定也知道它的意思是较少的。那么Serverless连起来,再稍加修饰,那就是较少的关心服务器的意思。
我们都知道,在研发侧都会有研发人员和运维人员两个角色,要开发一个新系统的时候,研发人员根据产品经理的PRD开始写代码开发功能,当功能开发、测试完之后,要发布到服务器。这个时候开始由运维人员规划服务器规格、服务器数量、每个服务部署的节点数量、服务器的扩缩容策略和机制、发布服务过程、服务优雅上下线机制等等。这种模式是研发和运维隔离,服务端运维都由专门的运维人员处理,而且很多时候是靠纯人力处理,也就是Serverfull时代。
互联网公司里最辛苦的是谁?我相信大多数都是运维同学。白天做各种网络规划、环境规划、数据库规划等等,晚上熬夜发布新版本,做上线保障,而且很多事情是重复性的工作。然后慢慢就有了赋能研发这样的声音,运维同学帮助研发同学做一套运维控制台,可以让研发同学在运维控制台上自行发布服务、查看日志、查询数据。这样一来,运维同学主要维护这套运维控制台系统,并且不断完善功能,轻松了不少。这就是研发兼运维的DevOps时代。
渐渐的,研发同学和运维同学的关注点都在运维控制台了,运维控制台的功能越来越强大,比如根据运维侧的需求增加了自动弹性扩缩、性能监控的功能,根据研发侧的需求增加了自动化发布的流水线功能。因为有了这套系统,代码质量检测、单元测试、打包编译、部署、集成测试、灰度发布、弹性扩缩、性能监控、应用防护这一系列服务端的工作基本上不需要人工参与处理了。这就是NoOps,Serverless时代。
2020年注定是不平凡的一年,疫情期间,多少家企业如割韭菜般倒下,又有多少家企业如雨后春笋般茁壮成长,比如在线教育行业。
没错,在线教育行业是这次疫情的最大受益者,在在线教育在这个行业里,有一个细分市场是在线编程教育,尤其是少儿编程教育和面向非专业人士的编程教育,比如编程猫、斑马AI、小象学院等。这些企业的在线编程系统都有一些共同的特点和诉求:
例如小象学院的编程课界面:
结合上述这些特点和诉求,不难看出,构建这样一套在线编程系统的核心在于有一个支持多种编程语言的、健壮高可用的代码运行环境。
那么我们先来看看传统的实现架构:
从High Level的架构来看,前端只需要将代码片段和编程语言的标识传给Server端即可,然后等待响应展示结果。所以整个Server端要负责对不同语言的代码进行分类、预处理然后传给不同编程语言的Runtime。这种架构有以下几个比较核心的问题。
首先是研发和运维工作量的问题,当市场有新的需求,或者洞察到新业务模式时需要增加编程语言,此时研发侧需要增加编程代码分类和预处理的逻辑,另外需要构建对应编程语言的Runtime。在运维侧需要规划支撑新语言的服务器规格以及数量,还有整体的CICD流程等。所以支持新的编程语言这个需求要落地,需要研发、运维花费不少的时间来实现,再加上黑/白盒测试和CICD流程测试的时间,对市场需求的支撑不能快速的响应,灵活性相对较差。
其次整个在线编程系统的稳定性是重中之重。所以所有Server端服务的高可用架构都需要自己搭建,用以保证流量高峰场景和稳态场景下的系统稳定。高可用一方面是代码逻辑编写的是否优雅和完善,另一方面是部署服务的集群,无论是ECS集群还是K8s集群,都需要研发和运维同学一起规划,那么对于对编程语言进行分类和预处理的服务来讲,尚能给定一个节点数,但是对于不同语言的Runtime服务来讲,市场需求随时会变,所以不好具体衡量每个服务的节点数。另外很重要的一点是所以服务的扩容,缩容机制都需要运维同学来实时手动操作,即便是通过脚本实现自动化,那么ECS弹起的速度也是远达不到业务预期的。
再次是整个IaaS资源的成本控制,我们都知道这种在线教育是有明显的流量潮汐的,比如上午10点到12点,下午3点到5点,晚上8点到10点这几个时段是流量比较大的时候,其他时间端流量比较小,而且夜晚更是没什么流量。所以在这种情况下,传统的部署架构无法做到IaaS资源和流量的贴合。举个例子,加入为了应对流量高峰时期,需要20台ECS搭建集群来承载流量冲击,此时每台ECS的资源使用率可能在70%以上,利用率较高,但是在流量小的时候和夜晚,每台ECS的资源使用率可能就是百分之十几甚至更低,这就是一种资源浪费。
那么我们来看看如何使用Serverless架构来实现同样的功能,并且解决上述几个问题。在选择Serverless产品时,在国内自然而然优先想到的就是阿里云的产品。阿里云有两款Serverless架构的产品Serverless 应用引擎和函数计算,这里我们使用函数计算来实现编程教育的场景。
函数计算(Function Compute)是事件驱动的全托管计算服务,简称FC。使用函数计算,我们无需采购与管理服务器等基础设施,只需编写并上传代码。函数计算为您准备好计算资源,弹性地、可靠地运行任务,并提供日志查询、性能监控和报警等功能。
这里不对FC的含义做过多赘述,只举一个例子。FC中有两个概念,一个是服务,一个是函数。一个服务包含多个函数:
这里拿Java微服务架构来对应,可以理解为,FC中的服务是Java中的一个类,FC中的函数是Java类中的一个方法:
但是Java类中的方法固然只能是Java代码,而FC中的函数可以设置不同语言的Runtime来运行不同的编程语言:
这个结构理解清楚之后,我们来看看如何调用FC的函数,这里会引出一个触发器的概念。我们最常使用的HTTP请求协议其实就是一种类型的触发器,在FC里称为HTTP触发器,除了HTTP触发器以外,还提供了OSS(对象存储)触发器、SLS(日志服务)触发器、定时触发器、MNS触发器、CDN触发器等。
从上图可以大概理解,我们可以通过多种途径调用FC中的函数。举例两个场景,比如每当我在指定的OSS Bucket的某个目录下上传一张图片后,就可以触发FC中的函数,函数的逻辑是将刚刚上传的图片下载下来,然后对图片做处理,然后再上传回OSS。再比如向MNS的某个队列发送一条消息,然后触发FC中的函数来处理针对这条消息的逻辑。
最后我们再来看看FC的高可用。每一个函数在运行代码时底层肯定还是IaaS资源,但我们只需要给每个函数设置运行代码时需要的内存数即可,最小128M,最大3G,对使用者而言,不需要考虑多少核数,也不需要知道代码运行在什么样的服务器上,不需要关心启动了多少个函数实例,也不需要关心弹性扩缩的问题等,这些都由FC来处理。
从上图可以看到,高可用有两种策略:
大家看到这里,可能已经大概对基于FC实现在线编程教育系统的架构有了一个大概的轮廓。
上图是基于FC实现的在线编程教育系统的架构图,在这个架构下来看看上述那三个核心问题怎么解:
下面以运行Python代码为例来看看如何用FC实现Python在线编程Demo。
打开函数计算(FC)控制台,选择对应的Region,选择左侧服务/函数,然后新建服务:
输出服务名称,创建服务。
进入新创建的服务,然后创建函数,选择HTTP函数,即可配置HTTP触发器的函数:
设置函数的各个参数:
几个需要的注意的参数这里做以说明:
函数创建好,进入函数,可以看到概述、代码执行、触发器、日志查询等页签,我们先看触发器,会看到这个函数自动创建了一个HTTP触发器,有调用该函数对应的HTTP路径:
然后我们选择代码执行,直接在线写入我们的代码:
具体代码如下:# -*- coding: utf-8 -*-
import logging
import urllib.parse
import time
import subprocess
def handler(environ, start_response):
context = environ['fc.context']
request_uri = environ['fc.request_uri']
for k, v in environ.items():
if k.startswith('HTTP_'):
pass
try:
request_body_size = int(environ.get('CONTENT_LENGTH', 0))
except (ValueError):
request_body_size = 0
# 获取用户传入的code
request_body = environ['wsgi.input'].read(request_body_size)
codeStr = urllib.parse.unquote(request_body.decode("GBK"))
# 因为body里的对象里有code和input两个属性,这里分别获取用户code和用户输入
codeArr = codeStr.split('&')
code = codeArr[0][5:]
inputStr = codeArr[1][6:]
# 将用户code保存为py文件,放/tmp目录下,以时间戳为文件名
fileName = '/tmp/' + str(int(time.time())) + '.py'
f = open(fileName, "w")
# 这里预置引入了time库
f.write('import time \r\n')
f = open(fileName, "a")
f.write(code)
f.close()
# 创建子进程,执行刚才保存的用户code py文件
p = subprocess.Popen("python " + fileName, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, encoding='utf-8')
# 通过标准输入传入用户的input输入
if inputStr != '' :
p.stdin.write(inputStr + "\n")
p.stdin.flush()
# 通过标准输出获取代码执行结果
r = p.stdout.read()
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [r.encode('UTF-8')]
整个代码思路如下:
前端我使用VUE写了简单的页面,这里解析两个简单的方法:
页面加载时初始化HTTP请求对象,调用的HTTP路径就是方才函数的HTTP触发器的路径。
这个方法就是调用FC中的PythonRuntime函数,将前端页面的代码片段传给该函数。这里处理input交互的思路是,扫描整个代码片段,以包含input代码为标识将整个代码段分成多段。没有包含input代码的直接送给FC函数执行,包含input代码的,请求用户的输入,然后代码片段带着用户输入的信息一起送给FC函数执行。
演示如下:
这篇文章洋洋洒洒给大家介绍了Serverless,阿里云的Serverless产品函数计算(FC)以及基于函数计算(FC)实现的在线编程系统的Demo。大家应该有所体感,基于函数计算(FC)实现在线编程系统时,研发同学只需要专注在如何执行由前端传入的代码即可,整个Server端的各个环节都不需要研发同学和运维同学去关心,基本体现了Serverless的精髓。
]]>说起Serverless这个词,我想大家应该都不陌生,那么Serverless这个词到底是什么意思?Serverless到底能解决什么问题?可能很多朋友还没有深刻的体会和体感。这篇文章我就和大家一起聊聊Serverless。
我们先将Serverless这个词拆开来看。Server,大家都知道是服务器的意思,说明Serverless解决的问题范围在服务端。Less,大家肯定也知道它的意思是较少的。那么Serverless连起来,再稍加修饰,那就是较少的关心服务器的意思。
我们都知道,在研发侧都会有研发人员和运维人员两个角色,要开发一个新系统的时候,研发人员根据产品经理的PRD开始写代码开发功能,当功能开发、测试完之后,要发布到服务器。这个时候开始由运维人员规划服务器规格、服务器数量、每个服务部署的节点数量、服务器的扩缩容策略和机制、发布服务过程、服务优雅上下线机制等等。这种模式是研发和运维隔离,服务端运维都由专门的运维人员处理,而且很多时候是靠纯人力处理,也就是Serverfull时代。
互联网公司里最辛苦的是谁?我相信大多数都是运维同学。白天做各种网络规划、环境规划、数据库规划等等,晚上熬夜发布新版本,做上线保障,而且很多事情是重复性的工作。然后慢慢就有了赋能研发这样的声音,运维同学帮助研发同学做一套运维控制台,可以让研发同学在运维控制台上自行发布服务、查看日志、查询数据。这样一来,运维同学主要维护这套运维控制台系统,并且不断完善功能,轻松了不少。这就是研发兼运维的DevOps时代。
渐渐的,研发同学和运维同学的关注点都在运维控制台了,运维控制台的功能越来越强大,比如根据运维侧的需求增加了自动弹性扩缩、性能监控的功能,根据研发侧的需求增加了自动化发布的流水线功能。因为有了这套系统,代码质量检测、单元测试、打包编译、部署、集成测试、灰度发布、弹性扩缩、性能监控、应用防护这一系列服务端的工作基本上不需要人工参与处理了。这就是NoOps,Serverless时代。
]]>最后这一章节总结Kafka中需要特别关注的重要配置以及影响Kafka性能的因素。
auto.create.topics.enable
:该配置项默认值是true
,但在生产环境最好设置为false
。这样可以控制创建Topic的人以及创建时间。background.threads
:该配置项默认值是10,既整个Kafka在执行各种任务时会启动的线程数。如果你的CPU很强劲,那么可以将线程数设大一点。delete.topic.enable
:该配置项默认值是false
,可以根据实际需求改变,在生产环境还是建议保持默认值,这样至少不会出现Topic被误删的情况。log.flush.interval.messages
:该配置项最好保持默认值,把这个任务交给操作系统的文件系统去处理。log.retention.hours
:日志文件保留的时间默认是168小时,既7天。这个配置可以根据具体业务需求而定。message.max.bytes
:每条Message或一批次Message的大小默认是1MB。这个配置也要根据具体需求而定,比如带宽的情况。min.insync.replicas
:该配置项的默认值是1,既在acks=all时,最少得有一个Replica进行确认回执。建议在生产环境配置为2,保证数据的完整性。num.io.threads
:处理I/O操作的线程数,默认是8个线程。如果觉得在这个环节达到了瓶颈,那么可以适当调整该参数。num.network.threads
:处理网络请求和响应的线程数,默认是3个线程。如果觉得在这个环节达到了瓶颈,那么可以适当调整该参数。num.recovery.threads.per.data.dir
:每个数据目录启用几个线程来处理,这里的线程数和数据目录数是乘积关系,并且只在Broker启动或关闭时使用。默认值是1,根据实际情况配置数据目录数,从而判断该配置项应该如何设置。num.replica.fetchers
:该配置项影响Replicas同步数据的速度,默认值是1,如果发现Replicas同步延迟较大,可以提升该配置项。offsets.retention.minutes
:Offset保留的时间,默认值是1440,既24小时。在生产环境建议将该配置项设大一点,比如设置为1个月,保证消费数据的完整性。unclean.leader.election.enable
:该配置项的作用是,指定是否可以将非ISR的Replicas选举为Leader,默认值为false
。在生产环境建议保持默认值,防止数据丢失。zookeeper.session.timeout.ms
:Zookeeper会话超时时间,默认值为6000。按实际情况而定,通常情况下保持60秒即可。default.replication.factor
:默认Replication Factor为1,建议设置为2或者3,以保证数据完整性和整个集群的健壮性。num.partitions
:Topic默认的Partition数,默认是1,建议设置为3或者6,以保证数据完整性和整个集群的健壮性。以上是比较重要,需要我们根据实际情况额外关注的配置项。
影响Kafka性能大概有五个因素。
我们知道Kafka是将大多数数据保存在磁盘上的。所以磁盘的读写性能很大程度上会影响Kafka系统的性能。所以我们可以注意以下几点:
log.dirs
配置项可以配置多个数据目录路径。log.retention.hours
配置项。如果已经消费的数据长时间保留在磁盘中,既没有意义又会对Kafka读写性能造成影响。数据传输的延迟性是任何MQ系统都要关注的问题,Kafka也不例外,在这方面我们要注意以下几点:
Kafka的高性能特性离不开对计算机内存的使用技术,对内存的使用大体分Java堆内存的使用和操作系统(Linux)Page Cache的使用:
KAFKA_HEAP_OPTS
设置对Java堆内存的使用大小。比如export KAFKA_HEAP_OPTS=“-Xmx4g”
。Page Cache:当应用程序需要读取文件中的数据时,操作系统先分配一些内存,将数据从存储设备读入到这些内存中,然后再将数据分发给应用程序;当需要往文件中写数据时,操作系统先分配内存接收用户数据,然后再将数据从内存写到磁盘上。文件 Cache 管理指的就是对这些由操作系统分配,并用来存储文件数据的内存的管理。
因为Kafka在Message传输的整个过程中,不会对Message进行任何计算,所以CPU通常不会成为Kafka性能的主要瓶颈。但是在一些情况下,也会对Kafka的性能产生影响:
通常优先推荐使用Linux系统,尤其在高性能计算领域,Linux已经成为一个占主导地位的操作系统。其次也可以使用Solaris系统。Windows系统是不推荐使用的。另外,尽量保证运行Kafka Broker的操作系统中,不要运行其他的应用程序,避免和Kafka产生资源竞争,从而影响性能。
这是本小册的最后一章节,探讨了Kafka的一些重要配置和影响Kafka性能的关键因素。整个小册从最基本的认知到核心概念的诠释再到实践,帮助小伙伴渡过Kafka和Zookeeper的萌新阶段。希望能给小伙伴们带来帮助。
]]>最后这一章节总结Kafka中需要特别关注的重要配置以及影响Kafka性能的因素。
auto.create.topics.enable
:该配置项默认值是true
,但在生产环境最好设置为false
。这样可以控制创建Topic的人以及创建时间。background.threads
:该配置项默认值是10,既整个Kafka在执行各种任务时会启动的线程数。如果你的CPU很强劲,那么可以将线程数设大一点。delete.topic.enable
:该配置项默认值是false
,可以根据实际需求改变,在生产环境还是建议保持默认值,这样至少不会出现Topic被误删的情况。log.flush.interval.messages
:该配置项最好保持默认值,把这个任务交给操作系统的文件系统去处理。log.retention.hours
:日志文件保留的时间默认是168小时,既7天。这个配置可以根据具体业务需求而定。message.max.bytes
:每条Message或一批次Message的大小默认是1MB。这个配置也要根据具体需求而定,比如带宽的情况。min.insync.replicas
:该配置项的默认值是1,既在acks=all时,最少得有一个Replica进行确认回执。建议在生产环境配置为2,保证数据的完整性。num.io.threads
:处理I/O操作的线程数,默认是8个线程。如果觉得在这个环节达到了瓶颈,那么可以适当调整该参数。num.network.threads
:处理网络请求和响应的线程数,默认是3个线程。如果觉得在这个环节达到了瓶颈,那么可以适当调整该参数。num.recovery.threads.per.data.dir
:每个数据目录启用几个线程来处理,这里的线程数和数据目录数是乘积关系,并且只在Broker启动或关闭时使用。默认值是1,根据实际情况配置数据目录数,从而判断该配置项应该如何设置。num.replica.fetchers
:该配置项影响Replicas同步数据的速度,默认值是1,如果发现Replicas同步延迟较大,可以提升该配置项。offsets.retention.minutes
:Offset保留的时间,默认值是1440,既24小时。在生产环境建议将该配置项设大一点,比如设置为1个月,保证消费数据的完整性。unclean.leader.election.enable
:该配置项的作用是,指定是否可以将非ISR的Replicas选举为Leader,默认值为false
。在生产环境建议保持默认值,防止数据丢失。zookeeper.session.timeout.ms
:Zookeeper会话超时时间,默认值为6000。按实际情况而定,通常情况下保持60秒即可。default.replication.factor
:默认Replication Factor为1,建议设置为2或者3,以保证数据完整性和整个集群的健壮性。num.partitions
:Topic默认的Partition数,默认是1,建议设置为3或者6,以保证数据完整性和整个集群的健壮性。这一节主要介绍Zookeeper和Kafka的UI管理工具。
ZKUI是一款简洁易用的Zookeeper信息管理工具。首先从Github上克隆工程到本地,这是一个Maven工程,然后mvn clean install
,在target
目录下打出两个jar包zkui-2.0-SNAPSHOT.jar
和zkui-2.0-SNAPSHOT-jar-with-dependencies.jar
,将其上传至你的阿里云ECS。因为我们Zookeeper是集群模式,所以首先需要修改config.cfg
中的Zookeeper地址:#Comma seperated list of all the zookeeper servers
zkServer=zookeeper.server.1:2181,zookeeper.server.2:2181,zookeeper.server.3:2181
然后运行如下命令:nohup java -jar zkui-2.0-SNAPSHOT-jar-with-dependencies.jar &
成功后,访问http://ECS外网IP:9090
即可,默认用户名密码是admin/manager
。如果有需要可以自行在config.cfg
文件中进行配置。
注意:ZKUI需要JDK7以上的环境。
然后登录ZKUI,可以看到如下界面:
整个界面分为三部分:
从上图可以看到,左侧有名为brokers
的zNode,点击进去后显示他的两个zNode,ids
和topics
:
再点进ids
可以看到,它还有三个子zNode,分别是Kafka集群中的三个Broker的信息:
如果进入topics
,可以看到它下面的子zNode都是我们之前创建的Topic,再进入每个Topic会看到Partition的zNode。充分展示了Zookeeper管理Kafka的方式。
ZKUI可以让我们方便直观的管理Zookeeper中的zNode,大大提高我们的工作效率。
Kafka Manager是一款强大的Kafka集群监控工具。首先做一些准备工作:
为了之后编译速度能快一些,先配置一下sbt的Maven仓库,连接到阿里云ECS,进入root用户目录,使用mkdir .sbt
创建.sbt
目录,进入该目录,使用vim repositories
创建repositories
文件,然后编辑如下内容:
[repositories] local aliyun: http://maven.aliyun.com/nexus/content/groups/public typesafe: http://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly |
将kafka-manager-1.3.3.22.zip
上传至ECS,解压后进入kafka-manager
目录,执行如下命令:
./sbt clean dist |
需要等待一会,执行成功后,在target/universal
目录下会产生一个kafka-manager-1.3.3.7.zip
压缩文件,将其拷贝到要部署Kafka Manager的目录下,执行如下命令启动:bin/kafka-manager &
成功后,访问http://ECS外网IP:9000
,即可看到Kafka Manager的界面了。如果有需要可以自行在conf
目录下的application.conf
文件中进行配置,比如端口号、Zookeeper的地址等。
注意:Kafka Manager需要JDK8以上的环境。
访问后,我们看到的是Kafka集群的列表列表,首先通过顶部的Add Cluster在Kafka Manager中创建Kafka集群:
这里需要注意的有六项:
My_Kafka_Cluster
:Kafka集群名称,这里随意输入。Cluster Zookeeper Hosts
:Zookeeper Server的地址,如果是集群,则地址以逗号分割。Kafka Version
:Kafka版本选择2.0.0。brokerViewThreadPoolSize
:这是Kafka Manager需要的配置项,最小为2。offsetCacheThreadPoolSize
:这是Kafka Manager需要的配置项,最小为2。kafkaAdminClientThreadPoolSize
:这是Kafka Manager需要的配置项,最小为2。然后点击Save,Kafka Manager中的Kafka集群就创建好了。然后在Kafka Cluster列表页就能看到我们创建的集群了:
点击进入后可以看到集群的基本信息:
从上图可以看到,我们的Kafka集群中一共有6个Topic,3个Broker。点击进入Broker列表,可以看到Broker的基本信息:
点击Broker ID可以进入Broker详细信息页面:
可以看到这个Broker中都有哪些Topic,他们的Partition、ISR、Leader等信息。
我们再来看看Topic列表:
从上图可以看到在列表中有一列是Brokers Spread %,只有2个Topic达到了100%,其他的都是33%,这是因为my_topic_in_cluster
和another_topic_in_cluster
这两个Topic是在Kafka集群中创建的,所以它们的Partitions和Replicas被均匀的分配到了三个Broker中。而其他的Topic都是在单机Kafka时创建的,所以他们的Partitions和Replicas都在一个Broker里。可见Kafka并不能自动改变之前已存在的Topic Partitions的分布情况。
我们点击进入之前创建的my_topic_in_cluster
Topic看一下它的详情:
从上图可以看到,从Kafka Manager中可以很清晰的看到Topic Partitions、ISR、Leader在Kafka集群中的分布情况。同时,也提供了对Topic的各种快捷操作,非常方便。
这一章节带大家实践搭建Zookeeper和Kafka的UI管理工具,通过可视化的视图以及方便的快捷操作能有效的监控Zookeeper和Kafka的状态以及大大提高生产效率。下一章节会对Kafka的重要配置和性能做一些探讨。希望能给小伙伴们带来帮助。
]]>这一节主要介绍Zookeeper和Kafka的UI管理工具。
ZKUI是一款简洁易用的Zookeeper信息管理工具。首先从Github上克隆工程到本地,这是一个Maven工程,然后mvn clean install
,在target
目录下打出两个jar包zkui-2.0-SNAPSHOT.jar
和zkui-2.0-SNAPSHOT-jar-with-dependencies.jar
,将其上传至你的阿里云ECS。因为我们Zookeeper是集群模式,所以首先需要修改config.cfg
中的Zookeeper地址:#Comma seperated list of all the zookeeper servers
zkServer=zookeeper.server.1:2181,zookeeper.server.2:2181,zookeeper.server.3:2181
然后运行如下命令:nohup java -jar zkui-2.0-SNAPSHOT-jar-with-dependencies.jar &
这一章节来真正启动Kafka集群,先给出一份Broker的配置项列表,将以下信息复制三份,分别配置三台阿里云ECS上的Broker配置文件:############################# Server Basics #############################
broker.id=0
delete.topic.enable=true
auto.create.topics.enable=true
############################# Socket Server Settings #############################
listeners=EXTERNAL://阿里云ECS内网IP:9092,INTERNAL://阿里云ECS内网IP:9093
listener.security.protocol.map=EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT
inter.broker.listener.name=INTERNAL
advertised.listeners=EXTERNAL://阿里云ECS外网IP:9092,INTERNAL://阿里云ECS内网IP:9093
num.network.threads=3
num.io.threads=8
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600
############################# Log Basics #############################
log.dirs=/root/kafka_2.12-2.0.0/data/kafka
num.partitions=1
num.recovery.threads.per.data.dir=1
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=2
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1
############################# Log Retention Policy #############################
log.retention.hours=168
log.segment.bytes=1073741824
log.retention.check.interval.ms=300000
log.segment.ms=604800000
############################# Zookeeper #############################
zookeeper.connect=zookeeper.server.1:2181,zookeeper.server.2:2181,zookeeper.server.3:2181
zookeeper.connection.timeout.ms=6000
############################# Group Coordinator Settings #############################
group.initial.rebalance.delay.ms=0
############################# Message #############################
message.max.bytes=1048576
fetch.message.max.bytes=1048576
以上列表有两点需要修改的地方:
broker.id
需要修改,不同Broker的ID不能相同。然后使用如下命令分别启动Kafka Broker:kafka_2.12-2.0.0/bin/kafka-server-start.sh kafka_2.12-2.0.0/config/server.properties &
三个Broker没有异常信息,大概率说明我们的Kafka集群部署成功了,下面来验证一下。首先我们创建一个Topic:kafka_2.12-2.0.0/bin sh kafka-topics.sh --zookeeper zookeeper.server.1:2181 --topic my_topic_in_cluster --create --partitions 3 --replication-factor 2
上面的命令有这样几个信息:
my_topic_in_cluster
有三个Partition,每个Partition有两个Replica,也就是每条发送到这个Topic的Message会保存六份。如果Kafka集群是成功的,那么理论上这六个Partition会被两两均匀分配到三个Broker中。
连接到部署Broker-0的阿里云ECS,进入Kafka的data目录:cd /kafka_2.12-2.0.0/data/kafka
/kafka_2.12-2.0.0/data/kafka# ls
__consumer_offsets-0 __consumer_offsets-3 __consumer_offsets-6
__consumer_offsets-1 __consumer_offsets-30 __consumer_offsets-7
__consumer_offsets-10 __consumer_offsets-31 __consumer_offsets-8
__consumer_offsets-11 __consumer_offsets-32 __consumer_offsets-9
__consumer_offsets-12 __consumer_offsets-33
__consumer_offsets-13 __consumer_offsets-34
__consumer_offsets-14 __consumer_offsets-35
__consumer_offsets-15 __consumer_offsets-36 cleaner-offset-checkpoint
__consumer_offsets-16 __consumer_offsets-37 configured-topic-0
__consumer_offsets-17 __consumer_offsets-38 configured-topic-1
__consumer_offsets-18 __consumer_offsets-39 configured-topic-2
__consumer_offsets-19 __consumer_offsets-4 first_topic-0
__consumer_offsets-2 __consumer_offsets-40 first_topic-1
__consumer_offsets-20 __consumer_offsets-41 first_topic-2
__consumer_offsets-21 __consumer_offsets-42 log-start-offset-checkpoint
__consumer_offsets-22 __consumer_offsets-43 meta.properties
__consumer_offsets-23 __consumer_offsets-44 my_topic_in_cluster-0
__consumer_offsets-24 __consumer_offsets-45 my_topic_in_cluster-2
__consumer_offsets-25 __consumer_offsets-46 recovery-point-offset-checkpoint
__consumer_offsets-26 __consumer_offsets-47 replication-offset-checkpoint
__consumer_offsets-27 __consumer_offsets-48 with_keys_topic-0
__consumer_offsets-28 __consumer_offsets-49 with_keys_topic-1
__consumer_offsets-29 __consumer_offsets-5 with_keys_topic-2
可以看到Broker-0中分配了my_topic_in_cluster
的Partition-0和Partition-2。
同理,连接到部署Broker-1的阿里云ECS,进入Kafka的data目录:cd /kafka_2.12-2.0.0/data/kafka
/kafka_2.12-2.0.0/data/kafka# ls
meta.properties my_topic_in_cluster-0
my_topic_in_cluster-1 cleaner-offset-checkpoint
recovery-point-offset-checkpoint log-start-offset-checkpoint
replication-offset-checkpoint
可以看到Broker-1中分配了my_topic_in_cluster
的Partition-0和Partition-1。
同理,连接到部署Broker-2的阿里云ECS,进入Kafka的data目录:cd /kafka_2.12-2.0.0/data/kafka
/kafka_2.12-2.0.0/data/kafka# ls
meta.properties my_topic_in_cluster-1
my_topic_in_cluster-2 cleaner-offset-checkpoint
recovery-point-offset-checkpoint log-start-offset-checkpoint
replication-offset-checkpoint
可以看到Broker-2中分配了my_topic_in_cluster
的Partition-1和Partition-2。
从上面的结果可以说明我们的Kafka集群是部署成功的。
这一章节带大家实践运行Kafka集群,通过查看每个Broker的Data目录印证之前章节对Partition介绍的内容。下一章节会带大家搭建管理Zookeeper和Kafka的UI工具。希望能给小伙伴们带来帮助。
]]>这一章节来真正启动Kafka集群,先给出一份Broker的配置项列表,将以下信息复制三份,分别配置三台阿里云ECS上的Broker配置文件:############################# Server Basics #############################
broker.id=0
delete.topic.enable=true
auto.create.topics.enable=true
############################# Socket Server Settings #############################
listeners=EXTERNAL://阿里云ECS内网IP:9092,INTERNAL://阿里云ECS内网IP:9093
listener.security.protocol.map=EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT
inter.broker.listener.name=INTERNAL
advertised.listeners=EXTERNAL://阿里云ECS外网IP:9092,INTERNAL://阿里云ECS内网IP:9093
num.network.threads=3
num.io.threads=8
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600
############################# Log Basics #############################
log.dirs=/root/kafka_2.12-2.0.0/data/kafka
num.partitions=1
num.recovery.threads.per.data.dir=1
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=2
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1
############################# Log Retention Policy #############################
log.retention.hours=168
log.segment.bytes=1073741824
log.retention.check.interval.ms=300000
log.segment.ms=604800000
############################# Zookeeper #############################
zookeeper.connect=zookeeper.server.1:2181,zookeeper.server.2:2181,zookeeper.server.3:2181
zookeeper.connection.timeout.ms=6000
############################# Group Coordinator Settings #############################
group.initial.rebalance.delay.ms=0
############################# Message #############################
message.max.bytes=1048576
fetch.message.max.bytes=1048576
这一章节主要对和Listener相关的四个配置项做以详细解释。listeners
、advertised.listeners
、listener.security.protocol.map
、inter.broker.listener.name
这四个配置项可能是大家最容易混淆和最不容易理解的。
在解释这些配置项之前,我们先来明确几个概念。
如上图所示,是一个很常见的Kafka集群场景,涵盖了上述的概念。图中那些通信虚线箭头就是靠Kafka的Listener建立的,并且是通过Kafka中不同的Listener建立的,这些Listener分为Internal Listener和External Listener。如下图所示:
那么这些Listener的创建以及内外部如何通信都是由上面那四个配置项决定的。
先来看listener.security.protocol.map
配置项,在上一章节中介绍过,它是配置监听者的安全协议的,比如PLAINTEXT
、SSL
、SASL_PLAINTEXT
、SASL_SSL
。因为它是以Key/Value的形式配置的,所以往往我们也使用该参数给Listener命名:listener.security.protocol.map=EXTERNAL_LISTENER_CLIENTS:SSL,INTERNAL_LISTENER_CLIENTS:PLAINTEXT,INTERNAL_LISTENER_BROKER:PLAINTEXT
使用Key作为Listener的名称。就如上图所示,Internal Producer、External Producer、Internal Consumer、External Consumer和Broker通信以及Broker之间互相通信时都很有可能使用不同的Listener。这些不同的Listener有监听内网IP的,有监听外网IP的,还有不同安全协议的,所以使用Key来表示更加直观。当然这只是一种非官方的用法,Key本质上还是代表了安全协议,如果只有一个安全协议,多个Listener的话,那么这些Listener所谓的名称肯定都是相同的。
listeners
就是主要用来定义Kafka Broker的Listener的配置项。listeners=EXTERNAL_LISTENER_CLIENTS://阿里云ECS外网IP:9092,INTERNAL_LISTENER_CLIENTS://阿里云ECS内网IP:9093,INTERNAL_LISTENER_BROKER://阿里云ECS内网IP:9094
上面的配置表示,这个Broker定义了三个Listener,一个External Listener,用于External Producer和External Consumer连接使用。也许因为业务场景的关系,Internal Producer和Broker之间使用不同的安全协议进行连接,所以定义了两个不同协议的Internal Listener,分别用于Internal Producer和Broker之间连接使用。
通过之前的章节,我们知道Kafka是由Zookeeper进行管理的,由Zookeeper负责Leader选举,Broker Rebalance等工作。所以External Producer和External Consumer其实是通过Zookeeper中提供的信息和Broker通信交互的。所以listeners
中配置的信息都会发布到Zookeeper中,但是这样就会把Broker的所有Listener信息都暴露给了外部Clients,在安全上是存在隐患的,我们希望只把给外部Clients使用的Listener暴露出去,此时就需要用到下面这个配置项了。
advertised.listeners
参数的作用就是将Broker的Listener信息发布到Zookeeper中,供Clients(Producer/Consumer)使用。如果配置了advertised.listeners
,那么就不会将listeners
配置的信息发布到Zookeeper中去了:advertised.listeners=EXTERNAL_LISTENER_CLIENTS://阿里云ECS外网IP:9092
这里在Zookeeper中发布了供External Clients(Producer/Consumer)使用的ListenerEXTERNAL_LISTENER_CLIENTS
。所以advertised.listeners
配置项实现了只把给外部Clients使用的Listener暴露出去的需求。
这个配置项从名称就可以看出它的作用了,就是指定一个listener.security.protocol.map
配置项中配置的Key,或者说指定一个或一类Listener的名称,将它作为Internal Listener。这个Listener专门用于Kafka集群中Broker之间的通信:inter.broker.listener.name=INTERNAL_LISTENER_BROKER
先来看看KafkaConfig.scala
和SocketServer.scala
源码中的这几行代码片段:// KafkaConfig.scala
...
val ListenersProp = "listeners"
...
def dataPlaneListeners: Seq[EndPoint] = {
Option(getString(KafkaConfig.ControlPlaneListenerNameProp)) match {
case Some(controlPlaneListenerName) => listeners.filterNot(_.listenerName.value() == controlPlaneListenerName)
case None => listeners
}
}
...
def listeners: Seq[EndPoint] = {
Option(getString(KafkaConfig.ListenersProp)).map { listenerProp =>
CoreUtils.listenerListToEndPoints(listenerProp, listenerSecurityProtocolMap)
}.getOrElse(CoreUtils.listenerListToEndPoints("PLAINTEXT://" + hostName + ":" + port, listenerSecurityProtocolMap))
}
// SocketServer.scala
def startup(startupProcessors: Boolean = true) {
this.synchronized {
connectionQuotas = new ConnectionQuotas(config.maxConnectionsPerIp, config.maxConnectionsPerIpOverrides)
createControlPlaneAcceptorAndProcessor(config.controlPlaneListener)
createDataPlaneAcceptorsAndProcessors(config.numNetworkThreads, config.dataPlaneListeners)
if (startupProcessors) {
startControlPlaneProcessor()
startDataPlaneProcessors()
}
}
...
private def createDataPlaneAcceptorsAndProcessors(dataProcessorsPerListener: Int,
endpoints: Seq[EndPoint]): Unit = synchronized {
endpoints.foreach { endpoint =>
val dataPlaneAcceptor = createAcceptor(endpoint)
addDataPlaneProcessors(dataPlaneAcceptor, endpoint, dataProcessorsPerListener)
KafkaThread.nonDaemon(s"data-plane-kafka-socket-acceptor-${endpoint.listenerName}-${endpoint.securityProtocol}-${endpoint.port}", dataPlaneAcceptor).start()
dataPlaneAcceptor.awaitStartup()
dataPlaneAcceptors.put(endpoint, dataPlaneAcceptor)
info(s"Created data-plane acceptor and processors for endpoint : $endpoint")
}
}
startup()
方法是Kafka Broker创建启动Socket连接的入口,既用来创建Acceptor线程的入口,该线程负责处理Socket连接。 createDataPlaneAcceptorsAndProcessors()
方法的第二个参数config.dataPlaneListeners
可以看到取的就是listeners
配置项的内容。
/** |
跟到里面,可以看到如果没有配置listeners
,那么会使用网卡地址创建Socket连接,对于阿里云ECS,就是内网IP。
再来看看KafkaServer.scala
源码中的这几行代码片段:...
val brokerInfo = createBrokerInfo
val brokerEpoch = zkClient.registerBroker(brokerInfo)
...
private[server] def createBrokerInfo: BrokerInfo = {
val endPoints = config.advertisedListeners.map(e => s"${e.host}:${e.port}")
zkClient.getAllBrokersInCluster.filter(_.id != config.brokerId).foreach { broker =>
val commonEndPoints = broker.endPoints.map(e => s"${e.host}:${e.port}").intersect(endPoints)
require(commonEndPoints.isEmpty, s"Configured end points ${commonEndPoints.mkString(",")} in" +
s" advertised listeners are already registered by broker ${broker.id}")
}
val listeners = config.advertisedListeners.map { endpoint =>
if (endpoint.port == 0)
endpoint.copy(port = socketServer.boundPort(endpoint.listenerName))
else
endpoint
}
val updatedEndpoints = listeners.map(endpoint =>
if (endpoint.host == null || endpoint.host.trim.isEmpty)
endpoint.copy(host = InetAddress.getLocalHost.getCanonicalHostName)
else
endpoint
)
val jmxPort = System.getProperty("com.sun.management.jmxremote.port", "-1").toInt
BrokerInfo(Broker(config.brokerId, updatedEndpoints, config.rack), config.interBrokerProtocolVersion, jmxPort)
}
从上面的代码可以看到,advertised.listeners
主要用于向Zookeeper注册Broker的连接信息,但是不参与创建Socket连接。
所以从这几处源码内容可以得出结论,Kafka Broker真正建立通信连接使用的是listeners
配置项里的内容,而advertised.listeners
只用于向Zookeeper注册Broker的连接信息,既向Client暴露Broker对外的连接信息(Endpoint)。
另外在KafkaConfig.scala
源码中还有有这么几行代码:val advertisedListenerNames = advertisedListeners.map(_.listenerName).toSet
val listenerNames = listeners.map(_.listenerName).toSet
require(advertisedListenerNames.contains(interBrokerListenerName),
s"${KafkaConfig.InterBrokerListenerNameProp} must be a listener name defined in ${KafkaConfig.AdvertisedListenersProp}. " +
s"The valid options based on currently configured listeners are ${advertisedListenerNames.map(_.value).mkString(",")}")
require(advertisedListenerNames.subsetOf(listenerNames),
s"${KafkaConfig.AdvertisedListenersProp} listener names must be equal to or a subset of the ones defined in ${KafkaConfig.ListenersProp}. " +
s"Found ${advertisedListenerNames.map(_.value).mkString(",")}. The valid options based on the current configuration " +
s"are ${listenerNames.map(_.value).mkString(",")}"
从上面的代码片段可以得出两个结论:
advertised.listeners
配置项中配置的Listener名称或者说安全协议必须在listeners
中存在。因为真正创建连接的是listeners
中的信息。inter.broker.listener.name
配置项中配置的Listener名称或者说安全协议必须在advertised.listeners
中存在。因为Broker之间也是要通过advertised.listeners
配置项获取Internal Listener信息的。这一章节主要大家详细解释了Broker几个比较容易混淆和不好理解的配置项,解释了什么是内外部Listener,如何暴露Listener等。这些配置在我们搭建Kafka集群时至关重要。希望能给小伙伴们带来帮助。
]]>这一章节主要对和Listener相关的四个配置项做以详细解释。listeners
、advertised.listeners
、listener.security.protocol.map
、inter.broker.listener.name
这四个配置项可能是大家最容易混淆和最不容易理解的。
在解释这些配置项之前,我们先来明确几个概念。
接下来几个章节我们开始搭建真正的Kafka集群,服务器还是使用上一节章节搭建Zookeeper使用的三台阿里云ECS。
在搭建单机Kafka章节中,在Kafka的/root/kafka_2.12-2.0.0/config/server.properties
配置文件中,我们只配置了log.dirs
和advertised.listeners
这两个配置项,其他配置项都是使用默认值。
Kafka的配置项一共多达140余个,虽然有一部分通常情况下我们不需要修改,使用默认值即可,但这只是一少部分。搭建Kafka集群时,光通常情况下需要考虑的配置项就有40余个。
另外,这些配置项要根据具体的业务场景做各种调整,不存在一套配置项通吃所有业务场景的情况,而且基本不可能一次性配置出性能最优、最能满足业务场景的配置项组合,都需要经过调整、测试,反复进行配置才能总结出相对最优的配置项组合。
先展示一份Broker的配置内容(/root/kafka_2.12-2.0.0/config/server.properties
),这里给出的是一个平铺的配置项列表,有一些配置项已经作废,有一些配置项之间有会有相互影响:############################# Server Basics #############################
broker.id=0
# DEPRECATED
host.name=阿里云ECS IP
# DEPRECATED
port=9092
delete.topic.enable=true
auto.create.topics.enable=true
############################# Socket Server Settings #############################
listeners=PLAINTEXT://阿里云ECS IP:9092
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,SSL:SSL
advertised.listeners=PLAINTEXT://阿里云ECS IP:9092
inter.broker.listener.name=PLAINTEXT
num.network.threads=3
num.io.threads=8
############################# Log Basics #############################
log.dirs=/root/kafka_2.12-2.0.0/data/kafka
num.partitions=1
num.recovery.threads.per.data.dir=1
default.replication.factor=3
min.insync.replicas=2
############################# Log Retention Policy #############################
log.retention.hours=168
log.segment.bytes=1073741824
log.retention.check.interval.ms=300000
log.segment.ms=604800000
############################# Zookeeper #############################
zookeeper.connect=zookeeper.server.1:2181,zookeeper.server.2:2181,zookeeper.server.3:2181
zookeeper.connection.timeout.ms=6000
############################# Group Coordinator Settings #############################
group.initial.rebalance.delay.ms=0
############################# Message #############################
message.max.bytes=1048576
fetch.message.max.bytes=1048576
我们逐一了解上面这些配置项:
Broker Server的基础配置涉及到四个配置项:
broker.id
:整个Kafka集群内标识唯一Broker的ID。整数类型。host.name
:部署Broker的服务器IP地址或者域名。该参数已作废。port
:Broker开放的端口号。该参数已作废。delete.topic.enable
:是否允许删除Topic。auto.create.topics.enable
:是否允许在Producer在未指定Topic发送Message时自动创建Topic。传输通信方面的配置涉及到六个配置项:
listeners
:Broker之间,Client与Broker之间通信建立连接时使用的信息。既Broker的监听者,可以以逗号分割配置多个。它的格式为[安全协议]://Hostname/IP:Port
。listener.security.protocol.map
:以Key/Value的形式定义监听者的安全协议,在大多数情况下会将Key认为是监听者的别名。所以会这样设置:
listeners=LISTENER_BOB://阿里云ECS IP1:9092,LISTENER_JOHN://阿里云ECS IP2:9092 |
advertised.listeners
:将Broker建立通信的地址发布到Zookeeper中,便于Client(Producer和Consumer)连接。它的格式和listener
一致。
inter.broker.listener.name
:设置内部通信时使用哪个监听者。可以直接设置listener.security.protocol.map
中设置的Key。num.network.threads
:Broker Server接收请求及发送响应时启用的线程数量。num.io.threads
:Broker Server处理请求、对Message进行I/O操作时启用的线程数。和监听者相关的四个配置项,在下一章节会做详细解释。
Broker Server处理日志的基础配置涉及到五个配置项:
log.dirs
:日志、Message保存的路径。num.partitions
:创建Topic时,如果没有指定Partition数量,则使用该配置项设置的Partition数量。num.recovery.threads.per.data.dir
:每个数据目录启用几个线程来处理,这里的线程数和数据目录数是乘积关系,并且只在Broker启动或关闭时使用。default.replication.factor
:创建Topic时,如果没有指定Partition的Replication Factor数,则使用该配置项设置的Replication Factor数。min.insync.replicas
:当acks=all
时,至少有多少个Replicas需要确认已持久化数据,包括Leader。Broker Server处理日志保留问题的配置涉及到四个配置项:
log.retention.hours
:Kafka保留Message的时间,默认是168小时,既7天。log.segment.bytes
:每个Segment文件的大小,默认是1G。log.retention.check.interval.ms
:检测Message是否可以被删除的时间间隔。log.segment.ms
:Segment文件关闭的时间。Zookeeper的相关配置涉及到两个配置项:
zookeeper.connect
:设置Zookeeper地址。可用逗号分割配置多个地址,既Zookeeper集群的地址。zookeeper.connection.timeout.ms
:等待连接Zookeeper的超时时间。Consumer Group相关的配置主要涉及到一个配置项:
group.initial.rebalance.delay.ms
:当Consumer Group新增或减少Consumer时,重新分配Topic Partition的延迟时间。Message相关配置涉及到两个配置项:
message.max.bytes
:Broker接收每条Message的最大值,默认是1M。fetch.message.max.bytes
:Consumer每次获取Message的大小。这一章节给大家介绍了Broker的详细配置,为搭建Kafka集群做好充分准备。下一章节会对大家比较不容易理解的Listener配置做详细介绍。希望能给小伙伴们带来帮助。
]]>接下来几个章节我们开始搭建真正的Kafka集群,服务器还是使用上一节章节搭建Zookeeper使用的三台阿里云ECS。
在搭建单机Kafka章节中,在Kafka的/root/kafka_2.12-2.0.0/config/server.properties
配置文件中,我们只配置了log.dirs
和advertised.listeners
这两个配置项,其他配置项都是使用默认值。
Kafka的配置项一共多达140余个,虽然有一部分通常情况下我们不需要修改,使用默认值即可,但这只是一少部分。搭建Kafka集群时,光通常情况下需要考虑的配置项就有40余个。
另外,这些配置项要根据具体的业务场景做各种调整,不存在一套配置项通吃所有业务场景的情况,而且基本不可能一次性配置出性能最优、最能满足业务场景的配置项组合,都需要经过调整、测试,反复进行配置才能总结出相对最优的配置项组合。
]]>这一节我们来真正搭建一个Zookeeper集群。
首先要做的就是再租赁两个服务器,参照搭建单机Kafka章节中的步骤,租赁阿里云服务器、安装JDK、下载配置Kafka、配置安全组规则。
在搭建单机Kafka章节中,启动的是单机Zookeeper,所以/root/kafka_2.12-2.0.0/config
目录下的zookeeper.properties
配置文件中只配置了dataDir
,也就是存储各种数据、日志、快照的路径。
在搭建Zookeeper时,就需要额外再配置一些参数了。同样打开/root/kafka_2.12-2.0.0/config
目录下的zookeeper.properties
配置文件,额外添加如下内容:maxClientCnxns=0
tickTime=2000
initLimit=10
syncLimit=5
quorumListenOnAllIPs=true
server.1=zookeeper.server.1:2888:3888
server.2=zookeeper.server.2:2888:3888
server.3=zookeeper.server.3:2888:3888
逐一解释一下这些配置信息:
maxClientCnxns
:该参数表示允许客户端最大连接数。如果设置为0则表示不做限制。tickTime
:该参数表示Zookeeper服务之间进行心跳监测的间隔时间,单位是毫秒。设置为2000,表示每隔2秒,Zookeeper服务器之间会进行一次心跳监测。initLimit
:该参数表示Zookeeper集群中的Follower在启动时需要在多少个心跳时间内从Leader同步数据。设置为10,表示要在10个心跳时间内,也就是在20秒内,要完成Leader数据的同步。syncLimit
:该参数表示超过多少个心跳时间收不到Follower的响应,Leader就认为此Follower已经下线。设置为5,表示在5个心跳时间内,也就是判断Follower是否存活的响应时间是10秒。首先节点列表的配置规则为server.N=IP:Port1:Port2
:
N
表示Zookeeper节点编号。IP
表示Zookeeper节点的服务器IP,既阿里云ECS的外网IP。Port1
表示该Zookeeper集群中的Follower节点与Leader节点通讯时使用的端口。作为Leader时监听该端口。Port2
表示选举新的Leader时,Zookeeper节点之间互相通信的端口,比如当Leader挂掉时,其余服务器会互相通信,选出新的Leader。Leader和Follower都会监听该端口。这里的节点编号是数字类型,需要我们在/root/kafka_2.12-2.0.0/data/zookeeper
目录下创建名为myid
的文件,然后将编号配置在里面。server.N
这里的N
要和myid
文件中配置的编号保持一致。
另外还需要注意的是,如果要在一台服务器上搭建伪集群,那么每个Port1
和每个Port2
要不一样才可以,因为IP
都是一样的。这里我们是分别用三台不同的阿里云ECS,所以IP
肯定是不一样的,而每个Port1
是一致的,每个Port2
也是一致的。
为了方便起见,我们可以在服务器的/etc/hosts
文件中设置一下域名映射,比如:[阿里云ECS-1 IP] zookeeper.server.1
[阿里云ECS-2 IP] zookeeper.server.2
[阿里云ECS-3 IP] zookeeper.server.3
这样在配置Zookeeper集群节点列表时就可以写成如下形式了:server.1=zookeeper.server.1:2888:3888
server.2=zookeeper.server.2:2888:3888
server.3=zookeeper.server.3:2888:3888
如果现在通过/root/kafka_2.12-2.0.0/bin/zookeeper-server-start.sh config/zookeeper.properties
启动Zookeeper,肯定会报一大堆错误,比如:[myid:0] - WARN [WorkerSender[myid=0]:QuorumCnxManager@588] - Cannot open channel to 1 at election address /zookeeper.server.1:3888
java.net.ConnectException: Connection refused
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
.......
这是因为阿里云ECS都是采用虚拟化技术创建的服务器实例,而虚拟机中并没有物理网卡,所以Zookeeper服务启动后,进程并没有监听到3888
端口,而是会随机生成一个端口进行监听。所以会报上面的错。解决的办法就是让Zookeeper服务进程监听0.0.0.0
的IP地址,也就是监听所有网卡。那么就需要在zookeeper.properties
配置文件加入quorumListenOnAllIPs=true
配置信息,来保证Zookeeper服务进程能监听到我们设定的3888
端口。
在启动Zookeeper集群前,先来总结一下配置工作:
/root/kafka_2.12-2.0.0/data/zookeeper
目录下创建名为myid
的文件,配置Zookeeper节点编号。/etc/hosts
文件中设置一下域名映射: [阿里云ECS-1 IP] zookeeper.server.1 [阿里云ECS-2 IP] zookeeper.server.2 [阿里云ECS-3 IP] zookeeper.server.3 |
/root/kafka_2.12-2.0.0/config/zookeeper.properties
配置文件中添加如下配置(server.N
中的N
要和myid
中配置的节点编号保持一致): maxClientCnxns=0 tickTime=2000 initLimit=10 syncLimit=5 quorumListenOnAllIPs=true server.1=zookeeper.server.1:2888:3888 server.2=zookeeper.server.2:2888:3888 server.3=zookeeper.server.3:2888:3888 |
在三台阿里云ECS中都完成上述工作后,就可以逐一启动Zookeeper了,命令如下:/root/kafka_2.12-2.0.0/bin/zookeeper-server-start.sh config/zookeeper.properties &
三个Zookeeper节点都启动后,我们可以通过下面两个方法对Zookeeper集群进行基础的验证。
我们可以使用nc
命令看看端口都有没有被成功监听,选择任意一台服务器,通过下面的命令查看:nc -vz zookeeper.server.1 2181
Connection to zookeeper.server.1 2181 port [tcp/*] succeeded!
nc -vz zookeeper.server.1 3888
Connection to zookeeper.server.1 3888 port [tcp/*] succeeded!
nc -vz zookeeper.server.1 2888
nc: connect to zookeeper.server.1 port 2888 (tcp) failed: Connection refused
nc -vz zookeeper.server.2 2181
Connection to zookeeper.server.2 2181 port [tcp/*] succeeded!
nc -vz zookeeper.server.2 3888
Connection to zookeeper.server.2 3888 port [tcp/*] succeeded!
nc -vz zookeeper.server.2 2888
nc: connect to zookeeper.server.2 port 2888 (tcp) failed: Connection refused
nc -vz zookeeper.server.3 2181
Connection to zookeeper.server.3 2181 port [tcp/*] succeeded!
nc -vz zookeeper.server.3 3888
Connection to zookeeper.server.3 3888 port [tcp/*] succeeded!
nc -vz zookeeper.server.3 2888
Connection to zookeeper.server.3 2888 port [tcp/*] succeeded!
从上面的信息中可以看出,三个Zookeeper都成功启动了,并且可以知道zookeeper.server.1
和zookeeper.server.2
是Follower,zookeeper.server.3
是Leader,因为前两个节点并没有监听2888
端口。
我们还可以通过Zookeeper Client连接到集群来检验。我们选择任意一台服务器,首先连接zookeeper.server.1
节点:/root/kafka_2.12-2.0.0/bin/zookeeper-shell.sh zookeeper.server.1:2181
连接成功后,我们创建一个zNode:create /my_zNode "some data"
Created /my_zNode
查看zookeeper.server.1
节点中所有的zNode:ls /
[cluster, brokers, my_zNode, zookeeper, admin, isr_change_notification, log_dir_event_notification, controller_epoch, kafka-manager, consumers, latest_producer_id_block, config]
我们看到了刚才创建的my_zNode
。然后退出连接,再连接zookeeper.server.2
节点:/root/kafka_2.12-2.0.0/bin/zookeeper-shell.sh zookeeper.server.2:2181
然后查看zookeeper.server.2
节点中的所有zNode:ls /
[cluster, controller_epoch, brokers, my_zNode, zookeeper, kafka-manager, admin, isr_change_notification, consumers, log_dir_event_notification, latest_producer_id_block, config]
我们同样发现了my_zNode
。查看my_zNode
中的数据:get /my_zNode
some data
cZxid = 0x500000009
ctime = Wed Jan 09 15:38:39 CST 2019
mZxid = 0x500000009
mtime = Wed Jan 09 15:38:39 CST 2019
pZxid = 0x500000009
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 0
看到是在zookeeper.server.1
节点中创建时添加的some data
数据。
同样我们再连接zookeeper.server.3
节点查看zNode情况:/root/kafka_2.12-2.0.0/bin/zookeeper-shell.sh zookeeper.server.3:2181
ls /
[cluster, controller_epoch, brokers, my_zNode, zookeeper, kafka-manager, admin, isr_change_notification, consumers, log_dir_event_notification, latest_producer_id_block, config]
get /my_zNode
some data
cZxid = 0x500000009
ctime = Wed Jan 09 15:38:39 CST 2019
mZxid = 0x500000009
mtime = Wed Jan 09 15:38:39 CST 2019
pZxid = 0x500000009
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 0
我们在zookeeper.server.3
节点中修改my_zNode
中的数据:set /my_zNode "new data"
get /my_zNode
new data
cZxid = 0x500000009
ctime = Wed Jan 09 15:38:39 CST 2019
mZxid = 0x50000000e
mtime = Wed Jan 09 15:46:29 CST 2019
pZxid = 0x500000009
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0
然后再连接zookeeper.server.1
节点查看my_zNode
的数据:/root/kafka_2.12-2.0.0/bin/zookeeper-shell.sh zookeeper.server.1:2181
get /my_zNode
new data
cZxid = 0x500000009
ctime = Wed Jan 09 15:38:39 CST 2019
mZxid = 0x50000000e
mtime = Wed Jan 09 15:46:29 CST 2019
pZxid = 0x500000009
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0
看到zookeeper.server.1
节点中my_zNode
的数据也变成了new data
。
上面的过程虽然比较繁琐,但是充分说明了我们的Zookeeper集群是搭建成功的。无论从哪个Zookeeper节点创建的zNode,都可以同步到集群中的其他节点。无论从哪个Zookeeper节点修改的zNode中的数据,也可以同步到起群中的其他节点。
Zookeeper提供了一些能够查看节点Server状态、Client连接Server的状态、节点健康状态的命令。因为命令大多都是四个字母的简写,所以称为The Four Letter Words Commands,我称为四字真言。
首先来看看整体的命令格式:echo "xxxx" | nc IP Port
xxxx
就是四字真言命令。IP
是Zookeeper节点的IP。Port
自然是Zookeeper监听的2181端口。下面来具体看看这些命令。
该命令可以查看指定节点的配置信息:echo "conf" | nc zookeeper.server.1 2181
clientPort=2181
dataDir=/root/kafka_2.12-2.0.0/data/zookeeper/version-2
dataLogDir=/root/kafka_2.12-2.0.0/data/zookeeper/version-2
tickTime=2000
maxClientCnxns=0
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=1
initLimit=10
syncLimit=5
electionAlg=3
electionPort=3888
quorumPort=2888
peerType=0
这个命令可以很方便的查看Zookeeper节点zookeeper.properties
中的配置信息,以及默认的配置信息。
该命令可以查看连接到指定Zookeeper节点的Client信息:echo "cons" | nc zookeeper.server.1 2181
/[Client IP]:35764[1](queued=0,recved=1,sent=1,sid=0x10000b81b7d0003,lop=SESS,est=1547024407028,to=30000,lcxid=0x0,lzxid=0x500000012,lresp=22061060,llat=11,minlat=0,avglat=11,maxlat=11)
/[Zookeeper Server IP]:42946[0](queued=0,recved=1,sent=0)
该命令可以查看指定Zookeeper节点建立Session的信息以及临时节点的信息:echo "dump" | nc zookeeper.server.3 2181
SessionTracker dump:
Session Sets (3):
0 expire at Fri Jan 02 07:13:54 CST 1970:
0 expire at Fri Jan 02 07:14:04 CST 1970:
1 expire at Fri Jan 02 07:14:14 CST 1970:
0x10000b81b7d0003
ephemeral nodes dump:
Sessions with Ephemerals (0):
该命令只有指定了Leader节点才有效。
该命令可以查看指定Zookeeper节点的环境变量信息:echo "envi" | nc zookeeper.server.3 2181
该命令可以查看指定Zookeeper节点是否正常:echo "ruok" | nc zookeeper.server.3 2181
imok
如果节点正常则返回imok
,如果不正常则没有任何响应。
该命令可以查看指定Zookeeper节点的信息:echo "srvr" | nc zookeeper.server.3 2181
Zookeeper version: 3.4.13-2d71af4dbe22557fda74f9a9b4309b15a7487f03, built on 06/29/2018 00:39 GMT
Latency min/avg/max: 0/1/8
Received: 34
Sent: 33
Connections: 1
Outstanding: 0
Zxid: 0x500000012
Mode: leader
Node count: 164
Proposal sizes last/min/max: 36/32/90
该命令可以查看指定Zookeeper节点的信息,以及连接该节点的Client信息:echo "stat" | nc zookeeper.server.1 2181
Zookeeper version: 3.4.13-2d71af4dbe22557fda74f9a9b4309b15a7487f03, built on 06/29/2018 00:39 GMT
Clients:
/[Client IP]:35764[1](queued=0,recved=54,sent=54)
/[Zookeeper Server IP]:42956[0](queued=0,recved=1,sent=0)
Latency min/avg/max: 0/0/17
Received: 223
Sent: 222
Connections: 2
Outstanding: 0
Zxid: 0x500000012
Mode: follower
Node count: 164
该命令可以查看指定Zookeeper节点的监控状态信息:echo "mntr" | nc zookeeper.server.1 2181
zk_version 3.4.13-2d71af4dbe22557fda74f9a9b4309b15a7487f03, built on 06/29/2018 00:39 GMT
zk_avg_latency 0
zk_max_latency 17
zk_min_latency 0
zk_packets_received 236
zk_packets_sent 235
zk_num_alive_connections 2
zk_outstanding_requests 0
zk_server_state follower
zk_znode_count 164
zk_watch_count 0
zk_ephemerals_count 0
zk_approximate_data_size 13322
zk_open_file_descriptor_count 116
zk_max_file_descriptor_count 65535
zk_fsync_threshold_exceed_count 0
我们可以使用以上这些命令方便的查看Zookeeper节点以及Client的各种信息,提高效率。
这一章节带大家实践搭建了真正的Zookeeper集群,为之后搭建Kafka集群打基础,同时还复习了Zookeeper CLI的使用方式以及很重要的Zookeeper四字真言。希望能给小伙伴们带来帮助。
]]>这一节我们来真正搭建一个Zookeeper集群。
首先要做的就是再租赁两个服务器,参照搭建单机Kafka章节中的步骤,租赁阿里云服务器、安装JDK、下载配置Kafka、配置安全组规则。
在搭建单机Kafka章节中,启动的是单机Zookeeper,所以/root/kafka_2.12-2.0.0/config
目录下的zookeeper.properties
配置文件中只配置了dataDir
,也就是存储各种数据、日志、快照的路径。
在搭建Zookeeper时,就需要额外再配置一些参数了。同样打开/root/kafka_2.12-2.0.0/config
目录下的zookeeper.properties
配置文件,额外添加如下内容:maxClientCnxns=0
tickTime=2000
initLimit=10
syncLimit=5
quorumListenOnAllIPs=true
server.1=zookeeper.server.1:2888:3888
server.2=zookeeper.server.2:2888:3888
server.3=zookeeper.server.3:2888:3888
这一节来看看Zookeeper的命令行工具。
在第七章节搭建单机Kafka中,我们已经发现了,Kafka是自带Zookeeper的,而且在启动Kafka之前,要先启动Zookeeper,相当于启动了单机Zookeeper,所以我们先说Zookeeper CLI,后面说Zookeeper集群时再具体说配置参数。
首先打开终端,连接至我们的服务器,进入/root/kafka_2.12-2.0.0/bin
目录,执行如下命令:sh zookeeper-shell.sh 127.0.0.1:2181
这是Zookeeper CLI Client连接Zookeeper的命令,当看到如下信息时,说明连接成功:Connecting to 127.0.0.1:2181
Welcome to ZooKeeper!
JLine support is disabled
先来来看看目前Zookeeper里都有哪些zNode:ls /
[cluster, controller_epoch, controller, brokers, zookeeper, kafka-manager, admin, isr_change_notification, consumers, log_dir_event_notification, latest_producer_id_block, config]
ls
命令和Linux中的作用一样,在Zookeeper中是展示某个zNode下的所有zNode。这里的/
表示根zNode。可以看到已经有很多zNode注册在了Zookeeper。再来看看brokers
下还有哪些zNode:ls /brokers
[ids, topics, seqid]
再来看看有哪些Topic:ls /brokers/topics
[with_keys_topic, first_topic, __consumer_offsets, configured-topic]
在上一章节中说过,Zookeeper中的zNode是可以存储数据的,那么我们来看看如何查看zNode中存储的数据,比如我们来看看/brokers/ids
里保存了什么数据:get /brokers/ids
null
cZxid = 0x5
ctime = Wed Dec 19 23:46:53 CST 2018
mZxid = 0x5
mtime = Wed Dec 19 23:46:53 CST 2018
pZxid = 0x43d
cversion = 51
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
get
命令用于查看zNode中存储的数据,从上面的结果看到,/brokers/ids
这个zNode里的数据是null
,那么看看是否/brokers/ids
下还有zNode:ls /brokers/ids
[0]
果然,/brokers/ids
下还有zNode,这个zNode很明显是以Broker ID命名的。那再来看看/brokers/ids/0
里存储了什么样的数据:get /brokers/ids/0
{"listener_security_protocol_map":{"PLAINTEXT":"PLAINTEXT"},"endpoints":["PLAINTEXT://ECS外网IP:9092"],"jmx_port":-1,"host":"ECS外网IP","timestamp":"1546570570448","port":9092,"version":4}
cZxid = 0x43d
ctime = Fri Jan 04 10:56:10 CST 2019
mZxid = 0x43d
mtime = Fri Jan 04 10:56:10 CST 2019
pZxid = 0x43d
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x100363316d3004a
dataLength = 192
numChildren = 0
从上面的结果可以看到,第一行显示的就是zNode存储的数据,/brokers/ids/0
这个zNode存储了Broker的IP、端口、注册时间戳、JMX端口等信息。这一行之后的信息都是zNode的标准属性了,有各种时间戳、版本号、数据长度、子节点数等。
我们可以使用Zookeeper CLI自行创建zNode:create /my_node "some data"
Created /my_node
ls /
[cluster, controller, brokers, zookeeper, my_node, admin, isr_change_notification, log_dir_event_notification, controller_epoch, kafka-manager, consumers, latest_producer_id_block, config]
使用create
命令创建zNode。这里要注意的是,在创建zNode时必须要带着存储数据,哪怕是空也可以:create /my_node ""
否则是无法创建zNode的。
在创建zNode时不可以一次性创建多级zNode,如果还没有创建my_node
,直接创建deeper_node
是不可以的:create /my_node/deeper_node "some data"
Node does not exist: /my_node/deeper_node
所以Zookeeper要一层一层创建zNode:create /my_node "some data"
Created /my_node
create /my_node/deeper_node "some data"
Created /my_node/deeper_node
get /my_node/deeper_node
some data
cZxid = 0x454
ctime = Mon Jan 07 19:12:20 CST 2019
mZxid = 0x454
mtime = Mon Jan 07 19:12:20 CST 2019
pZxid = 0x454
cversion = 0
我们可以通过set
命令更新zNode中存储的数据:set /my_node "new data"
cZxid = 0x453
ctime = Mon Jan 07 19:12:07 CST 2019
mZxid = 0x455
mtime = Mon Jan 07 19:14:04 CST 2019
pZxid = 0x454
cversion = 1
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 1
get /my_node
new data
cZxid = 0x453
ctime = Mon Jan 07 19:12:07 CST 2019
mZxid = 0x455
mtime = Mon Jan 07 19:14:04 CST 2019
pZxid = 0x454
cversion = 1
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 1
可以看到dataVersion
和cversion
从0变成了1。这里注意一下,每当更新zNode存储的数据时,dataVersion
会递增,之所以cversion
也递增了是因为更新数据本身也是对zNode的修改,如果我们再更新一次数据,就只有dataVersion
会递增了,因为第一次和第二次都是对zNode存储的数据的修改,只算作一次zNode的改变,所以cversion
不会再更新:set /my_node "again new data"
cZxid = 0x453
ctime = Mon Jan 07 19:12:07 CST 2019
mZxid = 0x456
mtime = Mon Jan 07 19:16:24 CST 2019
pZxid = 0x454
cversion = 1
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 14
numChildren = 1
如果想让cversion
变化,那么给my_node
再增加一个zNode:create /my_node/another_node "some data"
Created /my_node/another_node
get /my_node
again new data
cZxid = 0x453
ctime = Mon Jan 07 19:12:07 CST 2019
mZxid = 0x456
mtime = Mon Jan 07 19:16:24 CST 2019
pZxid = 0x457
cversion = 2
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 14
numChildren = 2
可以看到cversion
和numChildren
都变了。
上一章节同样说过,Zookeeper中的zNode的所有变更都可以被监控到。来看看如何通过CLI给zNode添加Watcher。我们给/my_node/deeper_node
添加Watcher:get /my_node/deeper_node true
some data
cZxid = 0x454
ctime = Mon Jan 07 19:12:20 CST 2019
mZxid = 0x454
mtime = Mon Jan 07 19:12:20 CST 2019
pZxid = 0x454
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 0
通过get /zNode true
给zNode添加Watcher。当/my_node/deeper_node
修改数据时,就会收到监听事件了:set /my_node/deeper_node "new data"
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/my_node/deeper_node
cZxid = 0x454
ctime = Mon Jan 07 19:12:20 CST 2019
mZxid = 0x458
mtime = Mon Jan 07 19:24:05 CST 2019
pZxid = 0x454
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0
可以通过rmr
命令删除zNode,该命令是递归删除,既可以删除指定zNode以及该zNode下的所有zNode:rmr /my_node
ls /
[cluster, controller, brokers, zookeeper, admin, isr_change_notification, log_dir_event_notification, controller_epoch, kafka-manager, consumers, latest_producer_id_block, config]
这一章节带大家实践了如何使用Zookeeper CLI操作Zookeeper,通过增删改查zNode进一步认知Zookeeper的结构,对之后认知Kafka集群做以铺垫。希望能给小伙伴们带来帮助。
]]>这一节来看看Zookeeper的命令行工具。
在第七章节搭建单机Kafka中,我们已经发现了,Kafka是自带Zookeeper的,而且在启动Kafka之前,要先启动Zookeeper,相当于启动了单机Zookeeper,所以我们先说Zookeeper CLI,后面说Zookeeper集群时再具体说配置参数。
首先打开终端,连接至我们的服务器,进入/root/kafka_2.12-2.0.0/bin
目录,执行如下命令:sh zookeeper-shell.sh 127.0.0.1:2181
这是Zookeeper CLI Client连接Zookeeper的命令,当看到如下信息时,说明连接成功:Connecting to 127.0.0.1:2181
Welcome to ZooKeeper!
JLine support is disabled
先来来看看目前Zookeeper里都有哪些zNode:ls /
[cluster, controller_epoch, controller, brokers, zookeeper, kafka-manager, admin, isr_change_notification, consumers, log_dir_event_notification, latest_producer_id_block, config]
ls
命令和Linux中的作用一样,在Zookeeper中是展示某个zNode下的所有zNode。这里的/
表示根zNode。可以看到已经有很多zNode注册在了Zookeeper。再来看看brokers
下还有哪些zNode:ls /brokers
[ids, topics, seqid]