<![CDATA[程序员说]]> 2021-11-01T16:37:29.295Z http://www.devtalking.com/ Hexo <![CDATA[用Blender做甜甜圈学习笔记十 - 增加甜甜圈主体质感]]> http://www.devtalking.com//articles/blender-beginner-tutorial-10/ 2021-11-01T16:00:00.000Z 2021-11-01T16:37:29.295Z

这一节继续优化甜甜圈主体部分,从下面这张真实的照片可以看出,甜甜圈主体表面是坑坑洼洼的(任何面点食物应该都不会很光滑),而目前我们的甜甜圈主体太过光滑,所以显得有点假。

10-1

给甜甜圈增加质感

首先点击Edit菜单,选择Preferences,打开Blender设置菜单,然后选择Add-ons,在右侧搜索框搜索node,然后将Node: Node Wrangle插件打上勾。这个插件可以优化和加速节点效果的处理流程。

10-2

然后进入Shading模式,选中甜甜圈,Shift + A,选择Texture/Nosie Texture,增加Noise Texture节点。

Noise Texture:噪波纹理着色器节点,给材质增加不规则碎片形的纹理。可以调节缩放比例、碎片数量、粗糙度、变形程度。

10-3

按下Shift + Ctrl,鼠标左键单击Noise Texture节点,就可以自动将该节点纳入流程中。

10-4

Shift + A,选择Input/Texture Coordinate,添加Texture Coordinate节点。将Object属性连接到Noise Texture节点的Vector属性。调节Noise Texture节点的Scale属性。

Texture Coordinate:纹理坐标输入节点。
Object属性:指定一个物体(对象),作为纹理映射的参考系。通常使用空物体,这是将图案投射到物体表面上某个固定点的一个简单办法。通过对指定物体进行变换(甚至动画),可以使纹理在被投射物体的表面移动。

10-5

按下Shift + A,选择Vector/Displacement,添加Displacement节点。将Noise Texture节点的Color属性连接到Displacement节点的Height属性。将Displacement节点的Displacement属性连接到Material Output属性。

Displacement:置换着色器节点,置换节点用于沿表面法线置换表面,可以给几何体添加更多细节。
Height属性:沿法线移动表面的距离。

10-6

按下Shift + Ctrl,鼠标左键单击Principled BSDF节点,重新整理流程,此时就可以看到甜甜圈的变化了。

10-7

选中甜甜圈,选择右侧Material Properties菜单,找到Settings选项,将Displacement属性改为Displacement and Bump,然后调整Displacement节点的Scale属性。这样就可以让噪音作用在整个物体上,而不只是表面。

10-8

为了让甜甜圈表现的更细腻一些,可以对甜甜圈再添加一个细分修改器,然后再调整Noise Texture节点的Scale属性,将甜甜圈的质感调整的合适的感觉。

10-9

最后来一张渲染图片,看看甜甜圈的质感。

10-10

]]>

这一节继续优化甜甜圈主体部分,从下面这张真实的照片可以看出,甜甜圈主体表面是坑坑洼洼的(任何面点食物应该都不会很光滑),而目前我们的甜甜圈主体太过光滑,所以显得有点假。

10-1

给甜甜圈增加质感

首先点击Edit菜单,选择Preferences,打开Blender设置菜单,然后选择Add-ons,在右侧搜索框搜索node,然后将Node: Node Wrangle插件打上勾。这个插件可以优化和加速节点效果的处理流程。

]]>
<![CDATA[用Blender做甜甜圈学习笔记九 - 优化甜甜圈主体]]> http://www.devtalking.com//articles/blender-beginner-tutorial-9/ 2021-10-25T16:00:00.000Z 2021-10-26T15:12:50.980Z

这一节我们来优化甜甜圈的主体部分,目前我们只是捏出了形状和简单的设置了基本颜色,看上去还是有点假。所以我们通过使用Texture Paint能力将其完善的更加真实一些。

先来看一张真实的甜甜圈照片,可以看到在主体部分的腰部凹陷那一圈,颜色是比较浅的,因为那个部位被烘烤的力度较小,越是顶部和底部,颜色会越深,这和我们煎炸其他食品的道理是一样的。

9-1

Texture Paint

在使用Texture Paint之前,我们先看看材质、纹理等这些概念的区别和关系。

英文 中文 本质 释义
Material 材质 数据集 表现物体对光的交互,供渲染器读取的数据集,包括贴图纹理、光照算法等
Texture mapping 纹理贴图 图像映射规则 把存储在内存里的位图,通过 UV 坐标映射到渲染物体的表面
Shading 底纹、阴影 光影效果 根据表面法线、光照、视角等计算得出的光照结果
Shader 着色器 程序 编写显卡渲染画面的算法来即时演算生成贴图的程序

我们今天操作的是在Texture这一层。先将糖衣隐藏,点击顶部Texture Paint菜单,进入纹理绘制视图。

9-2

新建一张纹理图片,将宽度设置为2048px。

9-3

这就是我们要绘制的图片了。

9-4

然后选中甜甜圈主体,进入Shading视图,Shift + A,选择Texture/Image Texture,添加一个Image Texture节点。将Image Texture节点的Color属性连接到Principled BSDFBase Color属性。在Image Texture节点中选择我们刚才创建的那张图片Donut_Texture。然后我们就可以看到甜甜圈主体的颜色变成了Donut_Texture这张图片的颜色。

9-5

回到Texture Paint视图,当我们在右侧的甜甜圈上涂色时,左侧的图片上就会出现对应的图案或颜色。反过来,在左侧的图片上涂色时,也会反应到右侧的甜甜圈上。

9-6

将鼠标放在左侧,按下N呼出工具栏,设置合适的颜色后,将图片涂成基本色。

9-7

然后再将颜色设置为白色,调整好笔触大小和力度大小,在右侧甜甜圈腰部涂色,来实现因受火不均产生的白色效果。

当我们画完之后,其实可以看到,白色非常死板,而且有重叠的笔痕。

9-8

为了让白色绘制的比较自然,我们可以使用Texture Mask功能。将鼠标移动到左侧,按下N呼出工具栏,在Brush Settings下找到Texture Mask新建一个纹理模板。

9-9

选择右侧Texture Properties菜单,从Brush改为Brush Mask,然后将Type改为Clouds

9-10

然后重新在甜甜圈主体凹陷位置进行绘制,此时可以看到白色就不会显得太僵硬。

9-11

接下来实现顶部和底部烤焦的效果。鼠标移动到左侧,按下N,呼出工具栏,将Brush Settings下的Blend属性改为Overlay,将颜色设置为黑色。

9-12

然后在甜甜圈主体的顶部和底部进行绘制,就可以实现烤焦的效果。

9-13

好,目前为止,甜甜圈主体的优化工作就完成了,来一张渲染图看看效果。

9-14

]]>

这一节我们来优化甜甜圈的主体部分,目前我们只是捏出了形状和简单的设置了基本颜色,看上去还是有点假。所以我们通过使用Texture Paint能力将其完善的更加真实一些。

先来看一张真实的甜甜圈照片,可以看到在主体部分的腰部凹陷那一圈,颜色是比较浅的,因为那个部位被烘烤的力度较小,越是顶部和底部,颜色会越深,这和我们煎炸其他食品的道理是一样的。

9-1

Texture Paint

在使用Texture Paint之前,我们先看看材质、纹理等这些概念的区别和关系。

]]>
<![CDATA[用Blender做甜甜圈学习笔记八 - 实现糖粒随机颜色和形态]]> http://www.devtalking.com//articles/blender-beginner-tutorial-8/ 2021-10-24T16:00:00.000Z 2021-10-25T15:47:17.490Z

这一节来继续完善甜甜圈上的糖粒,我们需要完成两件事:

  1. 给糖粒上色,而且是随机上色。
  2. 给糖衣铺上其他形状、形态的糖粒。

给糖粒随机上色

通过上一节实操,我们知道甜甜圈糖衣上的糖粒是通过对糖衣添加了粒子效果实现的,而粒子的对象是一个母板糖粒,所以我们要更改糖粒的颜色,只能针对这个模板糖粒进行修改。

8-1

我们可以修改糖粒的颜色,但是可以看到糖衣上的所有糖粒都是同一种颜色。

可以在用Blender做甜甜圈学习笔记六 - 渲染甜甜圈这一节中回顾如何给对象添加颜色。

8-2

这肯定不是我们期望的。要实现随机的上色效果,我们需要使用一个新的功能,材质生成着色器。我们点击顶部的Shading,进入着色器编辑视图。

8-3

在这个界面中,我们选中糖粒对象,在窗口下半部分,会看到和用Blender做甜甜圈学习笔记六 - 渲染甜甜圈中使用到的Compositing视图中类似的界面。但是里面的节点不一样,首先我们会看到一个叫Principled BSDF的节点。

Principled BSDF

这里简单地对Principled BSDF作以解释。Principled BSDF是将多个层组合成一个易于使用的节点。是一款综合型的材质生成着色器(Shader),跟时下普遍使用的 PBR (Physically based rendering)是原理类似的技术。

BSDFBidirectional Scattering Distribution Function的缩写,意思是双向散射分布函数,因为想要正确模拟真实世界中的光照,需要理解光遇到物体表面时发生的反射和散射,BSDF就是帮助我们实现更逼真的光散射效果的函数。

对应的还有一个BRDF(Bidirectional Reflectance Distribution Function)双向反射分布函数。

增加Base Color随机效果

我们使用快捷键Shift + A添加一个节点,在弹出的窗口中选择input/Object Info,然后将Object Info节点的Random属性连接到Principled BSDF节点的Base Color属性。

Random相当于提供了一组从0到1的随机数字,然后将这个随机的属性给了Base Color

8-4

可以看到甜甜圈上的糖粒的颜色已经是随机的了,只不过从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节点中的颜色,可以看到随机的色域的也会发生变化。

8-5

然后我们可以在ColorRamp中增加色标,每个色标可以指定不同的颜色,就可以实现不同色域之间的随机颜色了。

8-6

来一张渲染图看看效果:

8-7

增加不同形态的糖粒

实现了随机颜色后,我们来增加不同形态的糖粒。目前糖粒的形状都是一个圆柱体,我们可以多捏几种形态。

先捏一个更长一点的圆柱体,操作方法是选择糖粒圆柱体,Shift + D复制一个,进入编辑模式,打开透视,选中圆柱体顶部的所有点,按下GZ向上移动鼠标即可。可以回顾之前的笔记

8-8

那么如何将这个更长一点的糖粒添加到糖衣上呢?我们来看糖衣上粒子效果的Render AS属性,目前的值是Object,那意味着只能有一个指定的对象来充当粒子效果,Render AS其实还有其他的选项,这里可以选择Collection。此时大家应该很容易理解了,这个选项的意思就是选择一个集合,该集合下的所有对象都会成为充当粒子效果的对象。

所以我们首先将两个糖粒对象放在一个集合中。可以回顾用Blender做甜甜圈学习笔记七 - 制作糖衣上的糖粒中如何创建集合。

8-9

然后选择糖衣,打开右侧Particle Properties菜单,找到Render/Render AS,将Object修改为Collection,然后将Collection/Instance Collection的值设置为糖粒的集合。然后我们就可以看到糖衣上的糖粒同时包含了短的圆柱体和长的圆柱体。

8-10

然后我们再多添加几个形态的糖粒。再复制一个长的圆柱体,移动到旁边,进入编辑模式,将其再拉长一点。然后选择Loop Cut工具,这个工具的作用是对选中的对象进行横向或纵向的换切,换句话说就是对物体在横向或纵向增加拓扑点。

8-11

在选择了Loop Cut工具后,滚动鼠标滑轮,可以增加或减少换切的数量。

8-12

这里我们对圆柱体进行两次横向的环切,然后调整两头,进入对象模式,对其进行Shade Smooth操作,呈现如下样子:

8-13

现在糖衣上的糖粒就有了三种不同的形态。

8-14

我们现在选中一个糖粒圆柱体,可以看到他的原点在底部,我们可以将原点设置为它的中心点,更符合我们对原点理解的习惯,同时这样在糖衣上糖粒的形态会贴合一些,有一些糖粒就不会那么翘。

8-15

单独调整糖粒数量

为了让糖粒更加丰富一些,我们再来添加一个圆形糖粒。

8-16

我们希望圆形的糖粒只在糖衣上点缀几个即可,这时我们就需要调整每一种形态的糖粒的数量。进入对象模式,选择糖衣,打开右侧Particle Properties菜单,在Render/Collection下找到Use Count选项,打上勾,然后可以针对每一种形态的糖粒设置出现的次数,相当是在同一个集合下,该对象出现的比例。

8-17

我们将三种圆柱体形状的糖粒的Count设置为30,然后球形的糖粒Count保持为1,此时就可以看到糖衣上只有零星几个球形的糖粒。

8-18

到目前为止,糖粒的完善基本结束,上一张渲染图片看看效果。

8-19

]]>

这一节来继续完善甜甜圈上的糖粒,我们需要完成两件事:

  1. 给糖粒上色,而且是随机上色。
  2. 给糖衣铺上其他形状、形态的糖粒。

给糖粒随机上色

通过上一节实操,我们知道甜甜圈糖衣上的糖粒是通过对糖衣添加了粒子效果实现的,而粒子的对象是一个母板糖粒,所以我们要更改糖粒的颜色,只能针对这个模板糖粒进行修改。

8-1

我们可以修改糖粒的颜色,但是可以看到糖衣上的所有糖粒都是同一种颜色。

]]>
<![CDATA[用Blender做甜甜圈学习笔记七 - 制作糖衣上的糖粒]]> http://www.devtalking.com//articles/blender-beginner-tutorial-7/ 2021-10-20T16:00:00.000Z 2021-10-20T16:55:40.299Z

这一节继续对糖衣进行完善,先来看一张实图:

7-1

可以看到糖衣上有五颜六色的糖粒(我猜应该是糖),呈小圆柱体,不规则的铺在糖衣上面。这一节就来实现糖衣上的小糖粒。

整理对象

首先我们先通过集合的方式,对目前场景里的对象做一下分类和整理。选中一个或若干个对象,按下M,然后选择New Collection,就可以将对象放在一个集合中,在《用Blender做甜甜圈学习笔记五 - 雕刻糖衣白模》中使用过。

为甜甜圈和糖衣创建Donut集合,将充当桌面的平面放在Environment集合中,将摄像机和灯光放在Cam & Light集合中。

7-1

创建糖粒白模

先将场景中所有的对象都隐藏,然后按Shift + A选择Mesh/UV Sphere创建一个球体。将大小和拓扑面数都设置的小一些。

7-3

然后将这个球体移到甜甜圈旁边。

7-4

接下来要做的就是将这个球体编辑成一个小圆柱体。我们要做四步:

  1. 选中球体,进入编辑模式。
  2. 打开透视视图模式。
  3. 选中球体的上半部分。
  4. 使用Extrude Region工具,将上半部分拉长。

以上操作在用Blender做甜甜圈学习笔记三 - 捏出糖衣白模用Blender做甜甜圈学习笔记四 - 完善糖衣白模中有使用过。

7-5

然后调整两头的形状,先选中一头的所有点,按下S,再按Z,将圆头调整为接近平头,另一侧同理,意思是将选中的部分按照Z轴缩放。

7-6

然后切换到对象模式,选中圆柱体,鼠标右键,选择Shade Smooth,将圆柱体的表面光滑的显示。在用Blender做甜甜圈学习笔记二 - 捏出白模胚子中我们使用过。

7-7

我们将糖粒的模型捏出来后,大家首先想到的一定就是将它铺满甜甜圈的糖衣,我们当然可以使用比较耗时耗力的方法,就是复制茫茫多个糖粒对象,然后手动移动到糖衣上,再调整方向和角度。那我们的心态一定会炸裂。

在Blender中,肯定有更方便的方法来实现将糖粒铺满糖衣,并且分布密度和方向角度都是随机的。

将糖粒铺满糖衣

实现这个效果,我们需要用到对象的粒子效果属性。选中糖衣,点击右侧的Particle Properties选项,然后添加粒子效果。

7-8

粒子效果有两种类型,EmitterHair,我们选择Hair,然后将Render设置下的Render As改为Object,即将渲染粒子方式改为对象。

7-9

然后将Render设置下的Object下的Instance Object设置为我们的糖粒对象。意思就是用糖粒对象来充当粒子。

点击Instance Object旁边的吸管,然后选择糖粒对象。

此时,我们就可以看到在糖衣上出现了密密麻麻的糖粒。

7-10

调整糖粒的随机性

将糖粒铺到糖衣上后,我们需要调增糖粒大小、分布范围、方向的随机性,这样显的更加真实。首先可以设置Render下的Scale属性,来调整糖粒的大小,或者说粒子的大小。

7-11

然后来调整糖粒的方向,我们首先要勾选Advanced,然后会出现Rotation设置,然后勾选Rotation,并将Rotation下的Orientation Axis属性设置为Normal,然后调整Randomize Phase的值,就可以看到所有糖粒的方向开始有了随机的变化。

7-12

真实的情况下,糖粒撒到糖衣上,不会完全是平铺的,肯定会有一些糖粒插进糖衣的情况,所以我们可以调整Randomize属性来实现这个效果。

Randomize调整一点即可,不然糖衣就变成刺猬了。

7-13

最后我们再来调整糖粒的分布范围,因为正常情况糖粒一般会分布在糖衣表面,侧面是不太会有糖粒的,而现在我们的糖衣模型侧面也有糖粒。

7-14

解决这个问题,我们需要用到权重设置Weight Paint,我们在左上角的下拉菜单中可以切换到Weight Paint摸下,或者可以用快捷键Ctrl + Tab来切换。

7-15

切换到权重设置或权重绘制模式后,可以看到糖衣整个变成了蓝色,这表示此时的权重为0。当我们按鼠标左键在糖衣上滑动时,会看到像热力图似地样子出现,其中红色就表示权重为1,其他颜色就是1至0之间的权重。

7-16

我们可以通过调整顶部的Radius来调整绘制权重的笔触范围。调整好笔触范围后,我们将糖衣顶部都绘制为权重1。

7-17

然后将Vertex Groups下的Density设置为Group,此时就可以看到糖粒的范围基本都在刚才绘制的权重为1的范围内。

7-18

如果觉得糖粒太多太密,我们还可以调整Emission下的Number,可以控制粒子的数量。

7-19

到目前为止,糖粒的白模就制作完毕了,上一张渲染图看看效果。

7-20

]]>

这一节继续对糖衣进行完善,先来看一张实图:

7-1

可以看到糖衣上有五颜六色的糖粒(我猜应该是糖),呈小圆柱体,不规则的铺在糖衣上面。这一节就来实现糖衣上的小糖粒。

整理对象

首先我们先通过集合的方式,对目前场景里的对象做一下分类和整理。选中一个或若干个对象,按下M,然后选择New Collection,就可以将对象放在一个集合中,在《用Blender做甜甜圈学习笔记五 - 雕刻糖衣白模》中使用过。

为甜甜圈和糖衣创建Donut集合,将充当桌面的平面放在Environment集合中,将摄像机和灯光放在Cam & Light集合中。

]]>
<![CDATA[用Blender做甜甜圈学习笔记六 - 渲染甜甜圈]]> http://www.devtalking.com//articles/blender-beginner-tutorial-6/ 2021-10-15T16:00:00.000Z 2021-10-16T15:57:33.207Z

到目前位置,甜甜圈的模型基本都调整、雕刻完毕了,接下来需要将我们做的模型渲染出来看看效果。在渲染之前,我们需要先认识一个对于渲染很重要的元素,灯光。

在整个视图中,除了甜甜圈的对象以外,我们应该也早已发现了另外两个对象,LightCamera

6-1

灯光

灯光的强弱可以通过灯光离物体的距离以及一些设置项来调整,比如我们将灯光移动到离甜甜圈比近的位置,然后选择右上角的渲染模式视图,可以看到甜甜圈被强光照的都发白了。

6-2

灯光所处的位置不同,被照物体上的光源也会发生响应的变化。

6-3

为了更加真实,以及能更好的表现光源,我们需要创建一个平面,相当于桌面似的。Shift + A,选择Mesh/Plane

6-4

使用缩放调整平面的大小。这时我们会发现,甜甜圈有一半在平面下面。

6-5

解决的办法很简单,那就是将整个甜甜圈向上移动。但这里有个问题,因为整个甜甜圈是有两个对象组成的,糖衣和甜甜圈蛋糕部分,所以移动时需要将两个对象都选中,才不至于错位,那么如果对象比较多的时候,这种多选移动的方式就很繁琐,而且容易出错。

既然糖衣和甜甜圈蛋糕部分肯定是一个整体,那我们就可以将这两个对象合二为一,这个能力叫做联接。将一个对象作为子对象,和所选父对象进行关联,形成一个整体,并且子对象可以随意移动,不影响父对象,但移动父对象,会一并影响子对象。

先选择的对象视为子对象,后选择的对象视为父对象。

先选中糖衣,再按Shift选中甜甜圈蛋糕部分,然后快捷键Ctrl + P,会弹出Set parent to菜单,推荐选择Object(Keep Transform),这样就完成的父子关联,从右上角的对象关系视图中也可以看到,Icing对象没有了。

6-6

此时不论是选择糖衣还是甜甜圈蛋糕部分,都可以选中整体的甜甜圈。然后我们切换到Front视角,将甜甜圈沿Z轴向上移动,完成甜甜圈放在平面上的效果。

选中灯光对象,选择右侧Object Data Properties菜单,在Light一栏中可以可以调整灯光的颜色、灯光亮度(相当于瓦数),以及灯光照射范围。

6-7

渲染引擎

在Blender中有两种渲染引擎,EeveeCycles。前者是一个实时渲染引擎,渲染速度非常快,但渲染效果相对Cycles要弱一些。后者是通过CPU或者GPU进行渲染的引擎,渲染速度慢,但是渲染效果更加真实,当然对电脑的性能要求也更高。动画通常用Cycles渲染,像游戏之类通常用Eevee渲染。

6-8

这里先简单了解一下渲染引擎,之后我们再深入学习,我们做这个甜甜圈之后渲染成图时要使用Cycels渲染。

摄像机

咱们在捏模型的时候,整个视图场景可以看作一个近似可以无限延伸的场景,但是当我们渲染的时候,不可能无限延伸去渲染所有的物体,所以需要画一个范围,而摄像机就是用来做这个事的,和拍电影、照相的道理一样。

切换到摄像机视角有三种方式:

  • 小键盘0。
  • 按下`键,呼出的轮盘左下角有View Camera选项。
  • 编辑视图右侧有切换摄像机视角的按钮。

6-9

将摄像机通过移动、缩放、旋转三个基本操作,调整都合适的位置,然后可以切换到摄像机视角。

6-10

渲染

将灯光、摄像机都调整好,然后将充当桌面的平面换个颜色,这样能更加凸显甜甜圈。

6-11

然后按F12,就可以进行图片的渲染了,可以切换不同的渲染引擎,看看对比效果。这里用Cycles引擎渲染。

6-12

调整颜色

下面我们给糖衣设置颜色,选中糖衣,点击右侧Material Properties菜单,新建一套材质,然后会看到很多关于颜色、材质的属性。

6-13

我们先来看Base Color基本色,可以给糖衣设置一个粉色。

6-14

然后再来看Roughness属性,这个属性可以调节物体表面的反光程度,也就是可以将物体表面调整为亚光面。同理还有一个Specular属性,通常调整其中一个属性即可,推荐调整Roughness属性。

另外还可以通过SubsurfaceSubsurface ColorBase Color的基础上再调整颜色。

下面是经过这几个参数调整后的糖衣效果。

6-15

按照同样的方式,给甜甜圈调整颜色。

6-16

当我们将渲染的图片放大后,可以看到有很多噪点。

6-17

解决这个问题需要以下几个步骤。首先选中甜甜圈,选择右侧View Layer Properties,将Denoising Data打勾。

6-18

然后进入Compositing视图,选中左上角的Use NodesShift + A添加Denoise组件,将Render LayersNosiy ImageDenoising NormalDenoising Albedo三个属性分别连接到Denoise组件对应的属性上,然后将Denoise组件的Image属性连接到CompositeImage属性上。

6-19

然后重新渲染一次,就可以看到干净的渲染图了。

6-20

]]>

到目前位置,甜甜圈的模型基本都调整、雕刻完毕了,接下来需要将我们做的模型渲染出来看看效果。在渲染之前,我们需要先认识一个对于渲染很重要的元素,灯光。

在整个视图中,除了甜甜圈的对象以外,我们应该也早已发现了另外两个对象,LightCamera

6-1

灯光

灯光的强弱可以通过灯光离物体的距离以及一些设置项来调整,比如我们将灯光移动到离甜甜圈比近的位置,然后选择右上角的渲染模式视图,可以看到甜甜圈被强光照的都发白了。

]]>
<![CDATA[用Blender做甜甜圈学习笔记五 - 雕刻糖衣白模]]> http://www.devtalking.com//articles/blender-beginner-tutorial-5/ 2021-10-12T16:00:00.000Z 2021-10-16T15:57:55.653Z

先来看一张真实的甜甜圈。

5-1

一共有三个特点:

  • 甜甜圈腰部有一圈凹陷。
  • 流下来的糖衣是上小下大的形状,这个和密度大的液体从上往下流的道理是一样的。
  • 糖衣的边缘不是光滑的,是坑坑洼洼的。

这一节就是使用Blender的雕刻功能,将甜甜圈和糖衣的这几个特点刻画出来。

应用修改器

在开始之前,先回忆一下前面我们给甜甜圈和糖衣添加过修改器,但是我们并没有让修改器真正的生效,那么在雕刻前,需要将修改器的内容应用生效。在应用生效修改器之前,我们先对现在的白模复制一份,做个备份,因为修改器一旦生效后,整个拓扑都会发生变化。

选中甜甜圈和糖衣的白模,Shift + D复制一份,然后按下M键,新建一个集合,然后设置为不可见,相当于将复制出来的甜甜圈和糖衣做个备份。

5-2

然后选中甜甜圈,进入编辑模式,可以看到现在虽然有细分的修改器,但是拓扑结构还是之前精度比较小的样子,也就是四边形数量小,当我们应用修改器后,可以看到拓扑结构有明显的变化,四边形数量变多了,也就意味着我们能刻画更丰富的细节。

应用修改器时需要切换回对象模式。

5-3

雕刻甜甜圈

现在点击顶部Sculpting菜单进入雕刻视图。

5-4

左侧会有各种用于雕刻的工具,这里我们选择第一个工具Draw,然后按住Ctrl键,在甜甜圈腰部进行雕刻,会看到模型上会出现凹痕。

如果不按住Ctrl键,那么雕刻的效果是凸痕。

5-5

我们可以通过调整RadiusStrength来改变雕刻的作用范围和力度,也就是相当于调整凹痕或者凸痕的上下宽度和深度。

5-6

我们用Draw这个工具在甜甜圈腰部雕刻一圈凹痕,记得在甜甜圈内侧也要雕刻一圈。

雕刻内侧时,可以将笔触和力度都缩小一些。

雕刻完之后,我们还可以使用Smooth工具将凹痕不平整的地方完善一下,使其表现的更平滑细腻一些。该工具同样可以调整笔触大小和力度,方法同上。

5-7

经过调整后,我们第一步的雕刻就完成了,现在甜甜圈长着样。

5-8

接下来我们来雕刻流下来的糖衣部分,首先我们将加在糖衣上的修改器全部进行应用,方法同上,这里不在累赘。

然后使用Inflate工具,请扫流下来的糖衣底部,这个工具的作用就是让所选部分膨胀。

5-9

在雕刻环节我们继续使用Draw工具将糖衣上面的表面做一点坑洼的感觉,因为真实糖衣表面不会是非常光滑的,一定是会有坑洼的。

5-10

最后使用Grab工具,将糖衣的边缘雕刻出一些锯齿。

5-15

调整糖衣

雕刻完成后,我们进入Layout视图,可以看到因为我们将甜甜圈腰部雕刻出了一圈凹痕,所以就导致了流下来的糖衣部分没有贴合在甜甜圈表面。

5-11

解决这个问题,可以进入编辑模式,选中这些翘起来糖衣拓扑结构上的点,通过移动和旋转操作,将它们拖拽到贴合甜甜圈的位置。

这里需要注意的是,依然要开启Proportional Editing模式,并作用范围要包含整个流下来的糖衣部分。

5-12

5-13

最后来看看调整完的甜甜圈,是不是更加逼真了许多。

5-16

]]>

先来看一张真实的甜甜圈。

5-1

一共有三个特点:

  • 甜甜圈腰部有一圈凹陷。
  • 流下来的糖衣是上小下大的形状,这个和密度大的液体从上往下流的道理是一样的。
  • 糖衣的边缘不是光滑的,是坑坑洼洼的。

这一节就是使用Blender的雕刻功能,将甜甜圈和糖衣的这几个特点刻画出来。

应用修改器

在开始之前,先回忆一下前面我们给甜甜圈和糖衣添加过修改器,但是我们并没有让修改器真正的生效,那么在雕刻前,需要将修改器的内容应用生效。在应用生效修改器之前,我们先对现在的白模复制一份,做个备份,因为修改器一旦生效后,整个拓扑都会发生变化。

选中甜甜圈和糖衣的白模,Shift + D复制一份,然后按下M键,新建一个集合,然后设置为不可见,相当于将复制出来的甜甜圈和糖衣做个备份。

]]>
<![CDATA[用Blender做甜甜圈学习笔记四 - 完善糖衣白模]]> http://www.devtalking.com//articles/blender-beginner-tutorial-4/ 2021-10-10T16:00:00.000Z 2021-10-16T15:58:01.350Z

目前甜甜圈的糖衣已经捏出了大体的样子,我们先来看看真正的甜甜圈上的糖衣是什么样的。

切片 1.jpeg

可以看到在烘培甜甜圈时,糖衣是会沿着甜甜圈流淌下去的,所以这个细节是需要我们刻画出来的。

完善糖衣

选中糖衣,进入编辑模式,先取消Solidify修改器的效果。

切片 1.jpeg

选中某个点,按下A键,即可选中该对象所有的点,Alt + AOption + A是取消所有点的选中。

切片 1.jpeg

点击鼠标右键,在弹出菜单中选择Subdivide(细分)

切片 1.jpeg

对糖衣细分后,会发现拓扑结构变密了,也就是四边形的数量更多了,每细分一次,四边形数量多一倍。也就以为着我们可以捏出更多细节,因为点、线、面都变多了。另外在左下角弹出的细分设置中那个,我们也可以手动再增加细分,以及可以调整平滑度。这里我们只细分一次就足够了,然后将平滑度设置为1。

切片 1.jpeg

接下来有三个小技巧:

  • 如果我们想选中某一纬度或经度上的所有点,可以按下Alt点击你想全选的那条纬度或经度。

切片 1.jpeg

  • Ctrl + I或者Command + I可以反选,既选择其他圈上的所有点。

切片 1.jpeg

  • H键可以隐藏所选内容,Alt + H或者Option + H可以显示隐藏的内容。

切片 1.jpeg

这样处理后,我们可以更聚焦在我们需要编辑的部分。

现在我们先把糖衣往下流淌的大概形状捏一下,依然选中Proportional Editing模式,选择某个点,往下移动。此时我们应该会看到有一部分点穿进了甜甜圈的模型,俗称穿模。

切片 1.jpeg

我们更希望在移动点的时候,被移动的点能吸附在甜甜圈的表面上。所以我们可以使用Snap工具,选择吸附到面,并且关联的所有点都吸附到面。

打开Snap,将Snap To选择为Face,然后勾选上Project Individual Elements。然后再对点进行调整。

切片 1.jpeg

调整完之后大概是这个样子。

切片 1.jpeg

挤出拓扑

接下来需要再完善细节,做几个流的比较低的糖衣流痕,也就是选择某个点往下移动的更低一些,看看会发生什么。

切片 1.jpeg

分别切换到对象模式和开了透视视图的编辑模式。

切片 2.jpeg

因为拓扑结构被拉的太长,导致都是糖衣那部分的面和甜甜圈的面接触,所以会导致穿模。解决这个问题的思路是额外增加可以表现糖衣流到底部的拓扑,而不是将原有的拓扑拉长。我们可以通过Extrude(挤出)功能来实现。

选中一个点,按住Ctrl或者Command再点击相邻的另一个点,也就是选中两个点,选择Mesh → Extrude → Extrude Edges,或者快捷键E。往下移动这两个点,就会产生一个新的四边形(拓扑)。

切片 1.jpeg

切片 1.jpeg

我们可以多做几个类似的效果,宽度和长度(既一次选择几个点,往下移动多少距离)可以根据自己喜好调整。

切片 1.jpeg

我们仔细观察刚才挤出的糖衣部分,可以看到是有翘起来的感觉,没有和甜甜圈很好的贴合。

切片 1.jpeg

解决这个问题,可以调整Solidify修改器中的Edge Data → Crease Inner参数。该参数的作用相当于调整边缘内部折角的角度。将该属性调整为1,既将糖衣边缘内折角的角度调大,就可以和甜甜圈表面贴合起来了。

切片 1.jpeg

至此,甜甜圈糖衣白模的完善第一步就基本完成了。

切片 1.jpeg

]]>

目前甜甜圈的糖衣已经捏出了大体的样子,我们先来看看真正的甜甜圈上的糖衣是什么样的。

切片 1.jpeg

可以看到在烘培甜甜圈时,糖衣是会沿着甜甜圈流淌下去的,所以这个细节是需要我们刻画出来的。

完善糖衣

选中糖衣,进入编辑模式,先取消Solidify修改器的效果。

切片 1.jpeg

选中某个点,按下A键,即可选中该对象所有的点,Alt + AOption + A是取消所有点的选中。

]]>
<![CDATA[用Blender做甜甜圈学习笔记三 - 捏出糖衣白模]]> http://www.devtalking.com//articles/blender-beginner-tutorial-3/ 2021-10-06T16:00:00.000Z 2021-10-16T15:58:07.396Z

甜甜圈的白模胚子已经有了,然后我们开始做甜甜圈上面那一层糖衣的制作。基本思路是需要创建一个糖衣的白模,然后摞在甜甜圈白模上面,并且这个糖衣的白模需要有以下三个特点:

  • 甜甜圈的糖衣一般只覆盖到甜甜圈上部1/3的范围。
  • 糖衣的轮廓和甜甜圈上半部分完全一致。
  • 糖衣需要一点点厚度,能看出来是甜甜圈上面覆盖了一层东西。

我们依照上面三个思路来进行制作。

制作糖衣白模

要想让糖衣的轮廓和甜甜圈完全一致,最好的做法就是复制甜甜圈上部1/3的部分。进入编辑模式,将视角切换到正前视图,然后用鼠标框选甜甜圈的上部1/3位置。

切片 1.jpeg

框选完之后,我们会发现,并没有按我们设想的那样把上部1/3的点和线都选中,而是只选中的一部分。

切片 1.jpeg

如果想要实现我们的设想,需要打开透视模式。

切片 1.jpeg

然后再切换到正前方的视角,重新框选上部1/3位置,这时就可以将上部1/3的点和线全部选中了。

切片 1.jpeg

然后按下Shift + D复制选中的内容,再按下P键,会弹出分离选择框,选择Selection,意思是将复制的内容和被复制的物体分离。

切片 1.jpeg

此时可以在集合中看到又出现了一个对象,说明现在整个场景中,一共有四个对象,分别是:

  • 摄像机(先不用管)
  • 灯光(先不用管)
  • 甜甜圈
  • 糖衣

可以根据喜好,修改对象的名称,以便能更好的区分不同的对象。

切片 2.jpeg

现在关闭透视模式,切换到对象模式,能够更清晰的看到糖衣的白模。

切片 1.jpeg

使用实体化修改器完善糖衣

我们将视图放大,可以看到目前的糖衣的边缘和甜甜圈并没有贴合,而且比较薄。而实际的情况糖衣应该是有一定厚度,并且是和甜甜圈贴合在一起的。

切片 1.jpeg

解决这个问题,我们可以给糖衣添加一个Solidify修改器,这个修改器的作用可以调整模型的厚度。添加了Solidify修改器后,将Offset设置为1,然后将厚度(Thickness)设置为0.002m,这个可以根据个人喜好自行设置。然后就可以看到糖衣白模的变化,并且更加贴近真实。

切片 1.jpeg

此时我们选中糖衣,可以看到它的边缘轮廓不是很圆滑,有比较硬的角。

切片 1.jpeg

我们可以通过调整修改器的顺序来解决这个问题,我们将Subdivision修改器调整到下面。因为修改器是从上到下的执行,如果Solidify修改器在下面,会覆盖掉Subdivision修改器。

切片 1.jpeg

Subdivision修改器调整到最下面后,可以看到糖衣的轮廓就没有之前的硬角了。

切片 1.jpeg

至此,糖衣的白模就基本完成了。

切片 1.jpeg

]]>

甜甜圈的白模胚子已经有了,然后我们开始做甜甜圈上面那一层糖衣的制作。基本思路是需要创建一个糖衣的白模,然后摞在甜甜圈白模上面,并且这个糖衣的白模需要有以下三个特点:

  • 甜甜圈的糖衣一般只覆盖到甜甜圈上部1/3的范围。
  • 糖衣的轮廓和甜甜圈上半部分完全一致。
  • 糖衣需要一点点厚度,能看出来是甜甜圈上面覆盖了一层东西。

我们依照上面三个思路来进行制作。

制作糖衣白模

要想让糖衣的轮廓和甜甜圈完全一致,最好的做法就是复制甜甜圈上部1/3的部分。进入编辑模式,将视角切换到正前视图,然后用鼠标框选甜甜圈的上部1/3位置。

切片 1.jpeg

]]>
<![CDATA[用Blender做甜甜圈学习笔记二 - 捏出白模胚子]]> http://www.devtalking.com//articles/blender-beginner-tutorial-2/ 2021-10-05T16:00:00.000Z 2021-10-16T15:58:13.357Z

我们删除了正方体,添加了一个圆环,添加圆环后,左下角会出现该物体的各项属性,可以进行调整:

切片 1.jpeg

物体属性

先看Major Radius,它表示我们添加的物体的大小,Blender的默认单位是米,这里看到添加的圆环大小是1米,可以将它调整到一个甜甜圈的实际大小,可以直接5cm,Blender会自动换算单位。

切片 1.jpeg

从场景中的网格大小也可以对比出物体大小和视角远近的变化。如果想修改单位,可以在右侧的场景属性中进行设置。

切片 1@2x.png


物体整体的大小调整后,物体形状会变形,再通过Minor Radius调整整体的形状。

切片 1.jpeg

目前看到的圆环表面还是有很明显的棱角,整体看上去整个圆环是有若干个四边形组成的,可以通过调整Major Segments和Minor Segments调整横向和纵向的四边形的个数,个数越多,每个四边形面积越小,圆环表面整体看上去越光滑。

切片 1@2x.jpeg

编辑模式

甜甜圈不可能是一个完全周正的圆环,所以我们需要将圆环的形状调整的更加真实一些,在视图左上角有切换模式的下拉菜单,当需要对物体进行编辑时,可以进入编辑模式(Edit Mode),或者可以使用快捷键Tab快速切换。

切片 1@2x.jpeg

进入编辑模式后,可以看到物体的拓扑结构,可以对点、线、面进行调整。

切片 1.jpeg

我们选中某个点,然后按下G移动选中的点,可以看到该点周围的拓扑结构会发生变化,和该点相邻的四个面会被拉伸,从而圆环表面会出现一个尖角。

切片 1.jpeg

这种调整显然不符合我们的预期,我们希望在调整某个点时,和该点相邻的点能一起自适应的调整,这样才能调整出甜甜圈上的一些小坑洼、凹陷,以及圆环的长宽。那么我们需要用到按比例编辑的功能。

切片 1.jpeg

选中Proportional Editing模式后,再选中某个点,按下G,会出现一个圆圈,滑动鼠标中键可以调整圆圈的大小。

如果没有看到圆圈,请滑动鼠标中键缩放,有可能是圆圈过大,超出了屏幕。

切片 1.jpeg

该圆圈就是用来圈定与选中的点一起联动的点的范围。选中同样的点向上移动,不同的联动范围有不同的表现。

切片 1.jpeg

现在我们可以使用Proportional Editing模式对圆环进行微调,将表面调整的不那么光滑,有一些坑洼,将圆环也调整的不那么周正,从而更像一个真实的甜甜圈的样子。

切片 1.jpeg

修改器

按下Tab键,切换到物体模式,选中圆环,点击鼠标左键,在弹出的菜单中选择Shade Smooth,让Blender自动对理物体表面进行光滑处理。

切片 1.jpeg

此时我们可以看到圆环的表面已经没有四边形的面了,取而代之的是光滑的表面,但是当我们把视图调整到正左、正右或者前后时,可以看到圆环的轮廓并不是光滑的,依然有棱角。

切片 1.jpeg

要解决这个问题,我们需要用到Blender的修改器。可以简单的理解为,我们可以给一个选中的物体添加多种效果从而改变该物体各个表象。选中物体,在右侧属性面板中可以找到一个扳手形状的菜单,这就是添加修改器的地方。

切片 1.jpeg

点击Add Modifier,可以看到有很多种修改器,我们选择Subdivision Surface(细分表面)修改器。

切片 1.jpeg

我们可以将细分值调整为2,此时可以看到圆环的轮廓也变的光滑了。

切片 1.jpeg

到目前为止,一个甜甜圈的白模胚子的基本雏形就完成了。

切片 1.jpeg

]]>

我们删除了正方体,添加了一个圆环,添加圆环后,左下角会出现该物体的各项属性,可以进行调整:

切片 1.jpeg

物体属性

先看Major Radius,它表示我们添加的物体的大小,Blender的默认单位是米,这里看到添加的圆环大小是1米,可以将它调整到一个甜甜圈的实际大小,可以直接5cm,Blender会自动换算单位。

切片 1.jpeg

从场景中的网格大小也可以对比出物体大小和视角远近的变化。如果想修改单位,可以在右侧的场景属性中进行设置。

]]>
<![CDATA[用Blender做甜甜圈学习笔记一 - 基本操作]]> http://www.devtalking.com//articles/blender-beginner-tutorial-1/ 2021-10-03T16:00:00.000Z 2021-10-16T15:58:18.235Z

Blender基本操作

和场景相关的基本操作

  • 按住鼠标中键,可以基于原点旋转场景。
  • 按住Shift+鼠标中键,移动场景。
  • 按下```,出现8个选项的轮盘,鼠标快速移到对应的方位即可触发效果。

Image.png

这8个选项分别是:

  • Top:从上往下看的正交视图。
  • Bottom:从下往上看的正交视图。
  • Left:从左往右看的正交视图。
  • Right:从右往左看的正交视图。
  • Front:从物体正前方看的视图。
  • Back:从物体正后方看的视图。
  • View Camera:开启摄像机视图。
  • View Selected:将视角拉近选中的物体。

和物体相关的基本操作

  • 移动,快捷键G
  • 旋转,快捷键R
  • 变换,快捷键S

以上三个操作,都可以在按了对应快捷键后,再按下X或Y或Z,然后就可以沿着X轴、Y轴、Z轴进行移动、旋转或变换。或者当按下对应快捷键后,将鼠标移动到X轴、Y轴或Z轴附近,按下鼠标中键,也可以达到同样的效果。

  • Shift + D,可以复制选中的物体。
  • X或者Delete键可以删除选中的物体,比如删除初始化的正方体。

Image.png

  • Shift + A,可以添加物体,比如添加一个圆环。

切片 1@2x.jpeg

]]>

Blender基本操作

和场景相关的基本操作

  • 按住鼠标中键,可以基于原点旋转场景。
  • 按住Shift+鼠标中键,移动场景。
  • 按下```,出现8个选项的轮盘,鼠标快速移到对应的方位即可触发效果。

Image.png

这8个选项分别是:

  • Top:从上往下看的正交视图。
  • Bottom:从下往上看的正交视图。
  • Left:从左往右看的正交视图。
  • Right:从右往左看的正交视图。
  • Front:从物体正前方看的视图。
  • Back:从物体正后方看的视图。
  • View Camera:开启摄像机视图。
  • View Selected:将视角拉近选中的物体。
]]>
<![CDATA[Serverless在游戏运营行业进行数据采集分析的最佳实践]]> http://www.devtalking.com//articles/serverless-game-bigdata/ 2020-06-19T16:00:00.000Z 2021-02-03T16:41:11.363Z

众所周知,游戏行业在当今的互联网行业中算是一棵常青树。在疫情之前的2019年,中国游戏市场营收规模约2884.8亿元,同比增长17.1%。2020年因为疫情,游戏行业更是突飞猛进。玩游戏本就是中国网民最普遍的娱乐方式之一,疫情期间更甚。据不完全统计,截止2019年,中国移动游戏用户规模约6.6亿人,占中国总网民规模8.47亿的77.92%,足以说明,游戏作为一种低门槛、低成本的娱乐手段,已成为大部分人生活中习以为常的一部分。

对于玩家而言,市面上的游戏数量多如牛毛,那么玩家如何能发现和认知到一款游戏,并且持续的玩下去恐怕是所有游戏厂商需要思考的问题。加之2018年游戏版号停发事件,游戏厂商更加珍惜每一个已获得版号的游戏产品,所以这也使得“深度打磨产品质量”和“提高运营精细程度”这两个游戏产业发展方向成为广大游戏厂商的发展思路,无论是新游戏还是老游戏都在努力落实这两点:

  • 新游戏:以更充足的推广资源和更完整的游戏内容面向玩家。
  • 老游戏:通过用户行为分析,投入更多的精力和成本,制作更优质的版本内容。

这里我们重点来看新游戏。一家游戏企业辛辛苦苦研发三年,等着新游戏发售时一飞冲天。那么问题来了,新游戏如何被广大玩家看到?先来看看游戏行业公司的分类:

  • 游戏研发商:研发游戏的公司,生产和制作游戏内容。比如王者荣耀的所有英雄设计、游戏战斗场景、战斗逻辑等等,这些全部由游戏研发公司提供。
  • 游戏发行商:游戏发行商的主要工作分三大块:市场工作、运营工作、客服工作。游戏发行商把控游戏命脉,市场工作核心是导入玩家,运营工作核心是将用户价值最大化、赚取更多利益。

  • 游戏平台/渠道商:游戏平台和渠道商的核心目的就是曝光游戏,让尽量多的人能发现你的游戏。

这三种类型的公司做的事情有各自专注在某一块领域的独立公司,也有一家公司把这三种事情全部都做的,但无论那一种,这三者之间的关系是不会变的:

所以不难理解,要想让更多的玩家看到你的游戏,游戏发行和运营是关键,通俗来讲就是如果你的游戏出现在所有目前大家熟知的平台的广告中,那么最起码游戏的新用户注册数量是很可观的。那么这就引出了一个关键词,买量。

根据数据显示,2019年月均买量手游数达6000+款,而2018年仅为4200款。另一方面,随着抖音、微博等超级APP在游戏买量市场的资源倾斜,也助推手游买量的效果和效率都有所提升,游戏厂商也更愿意使用买量的方式来吸引用户。但需要注意的是,在游戏买量的精准化程度不断提高的同时,买量的成本也在节节攀升,唯有合理配置买量、渠道与整合营销之间的关系,才能将宣发资源发挥到最大的效果。

通俗来讲,买量其实就是在各大主流平台投放广告,广大用户看到游戏广告后,有可能会点击广告,然后进入游戏厂商的宣传页面,同时会采集用户的一些信息,然后游戏厂商对采集到的用户信息进行大数据分析,进行进一步的定向推广。

游戏运营核心诉求

游戏厂商花钱买量,换来用户信息以及新用户注册信息是为持续的游戏运营服务的,那么这个场景的核心诉求就是采集用户信息的完整性。比如说,某游戏厂商一天花5000w投放广告,在某平台某时段产生了每秒1w次的广告点击率,那么在这个时段内每一个点击广告的用户信息要完整的被采集到然后入库进行后续分析。这就对数据采集系统有着很高的要求,最核心的一点就是系统暴露接口的环节要能够平稳承载买量期间的不定时的流量脉冲。在买量期间,游戏厂商通常会在多个平台投放广告,每个平台投放广告的时间是不一样的,所以就出现全天不定时的流量脉冲现象。如果这个环节出现问题,那么相当于买量的钱就打水漂了。

数据采集系统传统架构

上图是一个相对传统的数据采集系统的架构,最关键的就是暴露HTTP接口回传数据这部分,这部分如果出问题,那么采集数据的链路就断了。但这部分往往会面临两个挑战:

  • 当流量脉冲来的时候,这部分是否可以很快的扩容来应对流量冲击。
  • 游戏运营并不是天天都在进行的,是有潮汐特性的,那么这部分的资源利用率如何优化。

在传统架构下,通常情况在游戏有运营活动之前,会提前通知运维同学,对这个环节的服务增加节点,但要增加多少其实是无法预估的,只能大概拍一个数字。这就会导致两个问题:

  • 流量太大,节点加少了,导致一部分流量的数据没有采集到。
  • 流量没有预期那么大,节点加多了,导致资源浪费。

数据采集系统Serverless架构

我们可以通过Serverless函数计算(函数计算的基本概念可以参考这篇文章)来取代传统架构中暴露HTTP回传数据这部分,从而完美的解决传统架构中存在问题,先来看架构图:

传统架构中的两个问题均可以通过函数计算百毫秒弹性的特性来解决。我们并不需要去估算营销活动会带来多大的流量,也不需要去担心和考虑对数据采集系统的性能,运维同学更不需要提前预备ECS。

因为函数计算的极致弹性特性,当没有买量,没有营销活动的时候,函数计算的运行实例是零。有买量活动时,流量脉冲的情况下,函数计算会快速拉起实例来承载流量压力,当流量减少时函数计算会及时释放没有请求的实例进行缩容。所以Serverless架构带来的优势有以下三点:

  • 无需运维介入,研发同学就可以很快的搭建出来。
  • 无论流量大小,均可以平稳的承接。
  • 函数计算拉起的实例数量可以紧贴流量大小的曲线,做到资源利用率最优化,再加上按量计费的模式,可以最大程度优化成本。

架构解析

从上面的架构图可以看到,整个采集数据阶段,分了两个函数来实现,第一个函数的作用是单纯的暴露HTTP接口接收数据,第二个函数用于处理数据,然后将数据发送至消息队列Kafka和数据库RDS。

接收数据函数

我们打开函数计算控制台,创建一个函数:

  • 函数类型:HTTP(即触发器为HTTP)
  • 函数名称:receiveData
  • 运行环境:Python3

  • 函数实例类型:弹性实例
  • 函数执行内存:512MB
  • 函数运行超时时间:60秒
  • 函数单实例并发度:1

  • 触发器类型:HTTP触发器
  • 触发器名称:defaultTrigger
  • 认证方式:anonymous(即无需认证)
  • 请求方式:GET,POST

创建好函数之后,我们通过在线编辑器编写代码:

# -*- coding: utf-8 -*-

import logging
import json
import urllib.parse
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)
request_body_str = urllib.parse.unquote(request_body.decode("GBK"))
request_body_obj = json.loads(request_body_str)
logger.info(request_body_obj["action"])
logger.info(request_body_obj["articleAuthorId"])

status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [HELLO_WORLD]

此时的代码非常简单,就是接收用户传来的参数,我们可以调用接口进行验证:

可以在函数的日志查询中看到此次调用的日志:

同时,我们也可以查看函数的链路追踪来分析每一个步骤的调用耗时,比如函数接到请求→冷启动(无活跃实例时)→准备代码→执行初始化方法→执行入口函数逻辑这个过程:

从调用链路图中可以看到,刚才的那次请求包含了冷启动的时间,因为当时没有活跃实例,整个过程耗时418毫秒,真正执行入口函数代码的时间为8毫秒。

当再次调用接口时,可以看到就直接执行了入口函数的逻辑,因为此时已经有实例在运行,整个耗时只有2.3毫秒:

处理数据的函数

第一个函数是通过在函数计算控制台在界面上创建的,选择了运行环境是Python3,我们可以在官方文档中查看预置的Python3运行环境内置了哪些模块,因为第二个函数要操作Kafka和RDS,所以需要我们确认对应的模块。

从文档中可以看到,内置的模块中包含RDS的SDK模块,但是没有Kafka的SDK模块,此时就需要我们手动安装Kafka SDK模块,并且创建函数也会使用另一种方式。

Funcraft

Funcraft是一个用于支持 Serverless 应用部署的命令行工具,能帮助我们便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助我们进行开发、构建、部署操作。

所以第二个函数我们需要使用Fun来进行操作,整个操作分为四个步骤:

  • 安装fun工具。
  • 编写template.yml模板文件,用来描述函数。
  • 安装我们需要的第三方依赖。
  • 上传部署函数。

安装Fun

Fun提供了三种安装方式:

  • 通过 npm 包管理安装 —— 适合所有平台(Windows/Mac/Linux)且已经预装了 npm 的开发者。
  • 通过下载二进制安装 —— 适合所有平台(Windows/Mac/Linux)。
  • 通过 Homebrew 包管理器安装 —— 适合 Mac 平台,更符合 MacOS 开发者习惯。

文本示例环境为Mac,所以使用npm方式安装,非常的简单,一行命令搞定:

sudo npm install @alicloud/fun -g

安装完成之后。在控制终端输入 fun 命令可以查看版本信息:

$ fun --version
3.6.20

在第一次使用 fun 之前需要先执行 fun config 命令进行配置,按照提示,依次配置 Account ID、Access Key Id、Secret Access Key、 Default Region Name 即可。其中 Account ID、Access Key Id 你可以从函数计算控制台首页的右上方获得:

fun config

? Aliyun Account ID ***************01
? Aliyun Access Key ID ***********qef6j
? Aliyun Access Key Secret ***********UFJG
? Default region name cn-hangzhou
? The timeout in seconds for each SDK client invoking 60
? The maximum number of retries for each SDK client 3

编写template.yml

新建一个目录,在该目录下创建一个名为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

我们来解析以上文件的核心内容:

  • FCBigDataDemo:自定义的服务名称。通过下面的Type属性标明是服务,即Aliyun::Serverless::Service
  • Properties:Properties下的属性都是该服务的各配置项。
  • VpcConfig:服务的VPC配置,包含:
    • VpcId:VPC ID。
    • VSwitchIds:交换机ID,这里是数组,可以配置多个交换机。
    • SecurityGroupId:安全组ID。
  • LogConfig:服务绑定的日志服务(SLS)配置,包含:
    • Project:日志服务项目。
    • Logstore:LogStore名称。
  • dataToKafka:该服务下自定义的函数名称。通过下面的Type属性标明是函数,即Aliyun::Serverless::Function
  • Properties:Properties下的属性都是该函数的各配置项。
  • Initializer:配置初始化函数。
  • Handler:配置入口函数。
  • Runtime:函数运行环境。

目录结构为:

安装第三方依赖

服务和函数的模板创建好之后,我们来安装需要使用的第三方依赖。在这个示例的场景中,第二个函数需要使用Kafka SDK,所以可以通过fun工具结合Python包管理工具pip进行安装:

fun install --runtime python3 --package-type pip kafka-python

执行命令后有如下提示信息:

此时我们会发现在目录下会生成一个.fun文件夹 ,我们安装的依赖包就在该目录下:

部署函数

现在编写好了模板文件以及安装好了我们需要的Kafka SDK后,还需要添加我们的代码文件index.py,代码内容如下:

# -*- coding: utf-8 -*-

import logging
import json
import urllib.parse
from kafka import KafkaProducer
producer = None

def my_initializer(context):
logger = logging.getLogger()
logger.info("init kafka producer")
global producer
producer = KafkaProducer(bootstrap_servers='XX.XX.XX.XX:9092,XX.XX.XX.XX:9092,XX.XX.XX.XX:9092')

def handler(event, context):
logger = logging.getLogger()
# 接收回传的数据
event_str = json.loads(event)
event_obj = json.loads(event_str)
logger.info(event_obj["action"])
logger.info(event_obj["articleAuthorId"])
# 向Kafka发送消息
global producer
producer.send('ikf-demo', json.dumps(event_str).encode('utf-8'))
producer.close()

return 'hello world'

代码很简单,这里做以简单的解析:

  • my_initializer:函数实例被拉起时会先执行该函数,然后再执行handler函数 ,当函数实例在运行时,之后的请求都不会执行my_initializer函数 。一般用于各种连接的初始化工作,这里将初始化Kafka Producer的方法放在了这里,避免反复初始化Produer。
  • handler:该函数只有两个逻辑,接收回传的数据和将数据发送至Kafka的指定Topic。

下面通过fun deploy命令部署函数,该命令会做两件事:

  • 根据template.yml中的配置创建服务和函数。
  • index.py.fun上传至函数中。

登录函数计算控制台,可以看到通过fun命令部署的服务和函数:

进入函数,也可以清晰的看到第三方依赖包的目录结构:

函数之间调用

目前两个函数都创建好了,下面的工作就是由第一个函数接收到数据后拉起第二个函数发送消息给Kafka。我们只需要对第一个函数做些许改动即可:

# -*- coding: utf-8 -*-

import logging
import json
import urllib.parse
import fc2
HELLO_WORLD = b'Hello world!\n'
client = None

def my_initializer(context):
logger = logging.getLogger()
logger.info("init fc client")
global client
client = fc2.Client(
endpoint="http://your_account_id.cn-hangzhou-internal.fc.aliyuncs.com",
accessKeyID="your_ak",
accessKeySecret="your_sk"
)

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)
request_body_str = urllib.parse.unquote(request_body.decode("GBK"))
request_body_obj = json.loads(request_body_str)
logger.info(request_body_obj["action"])
logger.info(request_body_obj["articleAuthorId"])

global client
client.invoke_function(
'FCBigDataDemo',
'dataToKafka',
payload=json.dumps(request_body_str),
headers = {'x-fc-invocation-type': 'Async'}
)

status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [HELLO_WORLD]

如上面代码所示,对第一个函数的代码做了三个地方的改动:

  • 导入函数计算的库:import fc2
  • 添加初始化方法,用于创建函数计算Client:

    def my_initializer(context):    
    logger = logging.getLogger()
    logger.info("init fc client")
    global client
    client = fc2.Client(
    endpoint="http://your_account_id.cn-hangzhou-internal.fc.aliyuncs.com",
    accessKeyID="your_ak",
    accessKeySecret="your_sk"
    )

    这里需要注意的时,当我们在代码里增加了初始化方法后,需要在函数配置中指定初始化方法的入口:

  • 通过函数计算Client调用第二个函数:

    global client
    client.invoke_function(
    'FCBigDataDemo',
    'dataToKafka',
    payload=json.dumps(request_body_str),
    headers = {'x-fc-invocation-type': 'Async'}
    )

    invoke_function函数有四个参数:

    • 第一个参数:调用函数所在的服务名称。
    • 第二个参数:调用函数的函数名称。
    • 第三个参数:向调用函数传的数据。
    • 第四个参数:调用第二个函数Request Header信息。这里主要通过x-fc-invocation-type这个Key来设置是同步调用还是异步调用。这里设置Async为异步调用。

如此设置,我们便可以验证通过第一个函数提供的HTTP接口发起请求→采集数据→调用第二个函数→将数据作为消息传给Kafka这个流程了。

使用两个函数的目的

到这里有些同学可能会有疑问,为什么需要两个函数,而不在第一个函数里直接向Kafka发送数据呢?我们先来看这张图:

当我们使用异步调用函数时,在函数内部会默认先将请求的数据放入消息队列进行第一道削峰填谷,然后每一个队列在对应函数实例,通过函数实例的弹性拉起多个实例进行第二道削峰填谷。所以这也就是为什么这个架构能稳定承载大并发请求的核心原因之一。

配置Kafka

在游戏运营这个场景中,数据量是比较大的,所以对Kafka的性能要求也是比较高的,相比开源自建,使用云上的Kafka省去很多的运维操作,比如:

  • 我们不再需要再维护Kafka集群的各个节点。
  • 不需要关心主从节点数据同步问题。
  • 可以快速、动态扩展Kafka集群规格,动态增加Topic,动态增加分区数。
  • 完善的指标监控功能,消息查询功能。

总的来说,就是一切SLA都有云上兜底,我们只需要关注在消息发送和消息消费即可。

所以我们可以打开Kafka开通界面,根据实际场景的需求一键开通Kafka实例,开通Kafka后登录控制台,在基本信息中可以看到Kafka的接入点:

  • 默认接入点:走VPC内网场景的接入点。
  • SSL接入点:走公网场景的接入点。

将默认接入点配置到函数计算的第二个函数中即可。

....
producer = KafkaProducer(bootstrap_servers='XX.XX.XX.XX:9092,XX.XX.XX.XX:9092,XX.XX.XX.XX:9092')
....

然后点击左侧控制台Topic管理,创建Topic:

将创建好的Topic配置到函数计算的第二个函数中即可。

...
# 第一个参数为Topic名称
producer.send('ikf-demo', json.dumps(event_str).encode('utf-8'))
...

上文已经列举过云上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);
String kafkaTopic = parameterTool.get("kafka-topic","ikf-demo");
String brokers = parameterTool.get("brokers", "XX.XX.XX.XX:9092,XX.XX.XX.XX:9092,XX.XX.XX.XX:9092");
Properties kafkaProps = new Properties();
kafkaProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
kafkaProps.put(ConsumerConfig.GROUP_ID_CONFIG, "ikf-demo");
FlinkKafkaConsumer<UserBehaviorEvent> kafka = new FlinkKafkaConsumer<>(kafkaTopic, new UserBehaviorEventSchema(), kafkaProps);
kafka.setStartFromLatest();
kafka.setCommitOffsetsOnCheckpoints(false);

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<UserBehaviorEvent> dataStreamByEventTime = env.addSource(kafka);

以上就是构建Flink Kafka Consumer和添加Kafka Source的代码片段,还是非常简单的。

压测验证

至此,整个数据采集的架构就搭建完毕了,下面我们通过压测来检验一下整个架构的性能。这里使用阿里云PTS来进行压测。

创建压测场景

打开PTS控制台,点击左侧菜单创建压测/创建PTS场景

在场景配置中,将第一个函数计算函数暴露的HTTP接口作为串联链路,配置如下图所示:

接口配置完后,我们来配置施压:

  • 压力模式:
    • 并发模式:指定有多少并发用户同时发请求。
    • RPS模式:指定每秒有多少请求数。
    • 递增模式:在压测过程中可以通过手动调节压力,也可以自动按百分比递增压力。
    • 最大并发:同时有多少个虚拟用户发起请求。
    • 递增百分比:如果是自动递增的话,按这里的百分比递增。
    • 单量级持续时长:在未完全达到压力全量的时候,每一级梯度的压力保持的时长。
    • 压测总时长:一共需要压测的时长。

这里因为资源成本原因,并发用户数设置为2500来进行验证。

从上图压测中的情况来看,TPS达到了2w的封顶,549w+的请求,99.99%的请求是成功的,那369个异常也可以点击查看,都是压测工具请求超时导致的。

总结

至此,整个基于Serverless搭建的大数据采集传输的架构就搭建好了,并且进行了压测验证,整体的性能也是不错的。并且整个架构搭建起来也是非常简单和容易理解的。这个架构不光适用于游戏运营行业,其实任何大数据采集传输的场景都是适用的,目前也已经有很多客户正在基于Serverless的架构跑在生产环境,或者正走在改造Serverless架构的路上。

]]>

众所周知,游戏行业在当今的互联网行业中算是一棵常青树。在疫情之前的2019年,中国游戏市场营收规模约2884.8亿元,同比增长17.1%。2020年因为疫情,游戏行业更是突飞猛进。玩游戏本就是中国网民最普遍的娱乐方式之一,疫情期间更甚。据不完全统计,截止2019年,中国移动游戏用户规模约6.6亿人,占中国总网民规模8.47亿的77.92%,足以说明,游戏作为一种低门槛、低成本的娱乐手段,已成为大部分人生活中习以为常的一部分。

对于玩家而言,市面上的游戏数量多如牛毛,那么玩家如何能发现和认知到一款游戏,并且持续的玩下去恐怕是所有游戏厂商需要思考的问题。加之2018年游戏版号停发事件,游戏厂商更加珍惜每一个已获得版号的游戏产品,所以这也使得“深度打磨产品质量”和“提高运营精细程度”这两个游戏产业发展方向成为广大游戏厂商的发展思路,无论是新游戏还是老游戏都在努力落实这两点:

  • 新游戏:以更充足的推广资源和更完整的游戏内容面向玩家。
  • 老游戏:通过用户行为分析,投入更多的精力和成本,制作更优质的版本内容。

这里我们重点来看新游戏。一家游戏企业辛辛苦苦研发三年,等着新游戏发售时一飞冲天。那么问题来了,新游戏如何被广大玩家看到?先来看看游戏行业公司的分类:

  • 游戏研发商:研发游戏的公司,生产和制作游戏内容。比如王者荣耀的所有英雄设计、游戏战斗场景、战斗逻辑等等,这些全部由游戏研发公司提供。
  • 游戏发行商:游戏发行商的主要工作分三大块:市场工作、运营工作、客服工作。游戏发行商把控游戏命脉,市场工作核心是导入玩家,运营工作核心是将用户价值最大化、赚取更多利益。

  • 游戏平台/渠道商:游戏平台和渠道商的核心目的就是曝光游戏,让尽量多的人能发现你的游戏。

]]>
<![CDATA[Serverless在SaaS领域中的实践]]> http://www.devtalking.com//articles/serverless-saas/ 2020-05-19T16:00:00.000Z 2021-02-03T16:09:48.619Z

随着互联网人口红利逐渐减弱,基于流量的增长已经放缓,互联网行业迫切需要找到一片足以承载自身持续增长的新蓝海。产业互联网正是这一宏大背景下的新趋势。我们看到互联网浪潮正在席卷传统行业,云计算、大数据、人工智能开始大规模融入到金融、制造、物流、零售、文娱、教育、医疗等行业的生产环节中,这种融合称为产业互联网。而在产业互联网中,有一块不可小觑的领域是SaaS领域,它是ToB赛道的中间力量。比如CRM、HRM、费控系统、财务系统、协同办公等等。

SaaS系统面临的挑战

在消费互联网时代,大家是我想要的东西,各个厂商在云计算、大数据、人工智能等技术基座之上建立流量最大化的服务与生态,基于海量内容分发与流量共享为逻辑构建系统。而到了产业互联网时代,供给关系发生了变化,大家是定制我想要的东西,需要从供给与需求两侧出发进行双向建设,这个时候系统的灵活性和扩展性面临着前所未有的挑战,尤其是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的概念做过阐述了,并且也介绍了Serverless函数计算(FC)的概念和实践。这篇文章中介绍一下构建系统灵活性的核心要素服务编排,Serverless工作流。

Serverless 工作流(FnF)是一个用来协调多个分布式任务执行的全托管云服务。在 Serverless工作流中,可以用顺序、分支、并行等方式来编排分布式任务,Serverless工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行您定义的重试逻辑,以确保工作流顺利完成。Serverless工作流通过提供日志记录和审计来监视工作流的执行,可以轻松地诊断和调试应用。

下面这张图描述了Serverless工作流如何协调分布式任务,这些任务可以是函数、已集成云服务API、运行在虚拟机或容器上的程序。

看完Serverless工作流的介绍,大家可能已经多少有点思路了吧。系统灵活性和可扩展性的核心是服务可编排,无论是以前的BPM还是现在的aPaaS。所以基于Serverless工作流重构SaaS系统灵活性方案的核心思路,是将系统内用户最希望定制的功能进行梳理、拆分、抽离,再配合函数计算(FC)提供无状态的能力,通过Serverless工作流进行这些功能点的编排,从而实现不同的业务流程。

通过Serverless函数计算和Serverless工作流搭建灵活的订餐模块

订餐场景相信大家都不会陌生,在家叫外卖或者在餐馆点餐,都涉及到这个场景。当下也有很多提供点餐系统的SaaS服务厂商,有很多不错的SaaS点餐系统。随着消费互联网向产业互联网转换,这些SaaS点餐系统面临的定制化的需求也越来越多,其中有一个需求是不同的商家在支付时会显示不同的支付方式,比如从A商家点餐后付款时显示支付宝、微信支付、银联支付,从B商家点餐后付款时显示支付宝、京东支付。突然美团又冒出来了美团支付,此时B商家接了美团支付,那么从B商家点餐后付款时显示支付宝、京东支付、美团支付。诸如此类的定制化需求越来越多,这些SaaS产品如果没有PaaS平台,那么就会疲于不断的通过硬代码增加条件判断来实现不同商家的需求,这显然不是一个可持续发展的模式。

那么我们来看看通过Serverless函数计算和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来启动流程。

Java启动流程

这个示例我使用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方式的接口,传入三个参数:

  1. fnfname:要启动的流程名称。
  2. execuname:流程启动后的流程实例名称。
  3. input:启动输入参数,比如业务参数。
@GetMapping("/startFNF/{fnfname}/{execuname}/{input}")
public StartExecutionResponse startFNF(@PathVariable("fnfname") String fnfName,
@PathVariable("execuname") String execuName,
@PathVariable("input") String inputStr) throws ClientException {

JSONObject jsonObject = new JSONObject();
jsonObject.put("fnfname", fnfName);
jsonObject.put("execuname", execuName);
jsonObject.put("input", inputStr);
return fnfService.startFNF(jsonObject);
}

再来看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选择商品/商家页面

前端我使用VUE搭建,当点击选择商品和商家页面中的下一步后,通过GET方式调用HTTP协议的接口/startFNF/{fnfname}/{execuname}/{input}。和上面的Java方法对应。

  1. fnfname:要启动的流程名称。
  2. execuname:随机生成uuid,作为订单的编号,也作为启动流程实例的名称。
  3. input:将商品、商家、订单号、地址构建为JSON字符串传入流程。
    submitOrder(){
    const orderNum = uuid.v1()
    this.$axios.$get('/startFNF/OrderDemo-Jiyuan/'+orderNum+'/{\n' +
    ' "products": "'+this.products+'",\n' +
    ' "supplier": "'+this.supplier+'",\n' +
    ' "orderNum": "'+orderNum+'",\n' +
    ' "address": "'+this.address+'"\n' +
    '}' ).then((response) => {
    console.log(response)
    if(response.message == "success"){
    this.$router.push('/orderdemo/' + orderNum)
    }
    })
    }

generateInfo节点

第一个节点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

  1. name:节点名称。
  2. timeoutSeconds:超时时间。该节点等待的时长,超过时间后会跳转到goto分支指向的orderCanceled节点。
  3. pattern:设置为waitForCallback,表示需要等待确认。inputMappings:该节点入参。
    • taskToken:Serverless工作流自动生成的Token。
    • products:选择的商品。
    • supplier:选择的商家。
    • address:送餐地址。
    • orderNum:订单号。
  4. outputMappings:该节点的出参。
    • paymentcombination:该商家支持的支付方式。
    • orderNum:订单号。
  5. catch:捕获异常,跳转到其他分支。

这里resourceArnserviceParams需要拿出来单独解释。Serverless工作流支持与多个云服务集成,即将其他服务作为任务步骤的执行单元。服务集成方式由FDL语言表达,在任务步骤中,可以使用resourceArn来定义集成的目标服务,使用pattern定义集成模式。所以可以看到在resourceArn中配置acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages信息,即在generateInfo节点中集成了MNS消息队列服务,当generateInfo节点触发后会向generateInfo-fnf-demo-jiyuanTopic中发送一条消息。那么消息正文和参数则在serviceParams对象中指定。MessageBody是消息正文,配置$表示通过输入映射inputMappings产生消息正文。

看完第一个节点的示例,大家可以看到,在Serverless工作流中,节点之间的信息传递可以通过集成MNS发送消息来传递,也是使用比较广泛的方式之一。

generateInfo-fnf-demo函数

generateInfo-fnf-demo-jiyuanTopic中发送的这条消息包含了商品信息、商家信息、地址、订单号,表示一个下订单流程的开始,既然有发消息,那么必然有接受消息进行后续处理。所以打开函数计算控制台,创建服务,在服务下创建名为generateInfo-fnf-demo的事件触发器函数,这里选择Python Runtime:

创建MNS触发器,选择监听generateInfo-fnf-demo-jiyuanTopic。

打开消息服务MNS控制台,创建generateInfo-fnf-demo-jiyuanTopic:

做好函数的准备工作,我们来开始写代码:

# -*- 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函数。

整个代码分五部分:

  1. 构建Serverless工作流Client。
  2. event内的信息即接受到TopicgenerateInfo-fnf-demo-jiyuan中的消息内容,将其转换为Json对象。
  3. 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取。比如在系统内有商家信息的配置功能,通过在界面上配置该商家支持哪些支付方式,形成元数据配置信息,提供查询接口,在这里进行查询。
  4. 调用Java服务暴露的接口,更新订单信息,主要是更新支付方式。
  5. 给予generateInfo节点响应,并返回数据,这里返回了订单号和支付方式。因为该节点的patternwaitForCallback,所以需要等待响应结果。

payment节点

我们再来看第二个节点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函数

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节点。

VUE选择支付方式页面

因为经过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节点

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节点

我们具体来看一个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函数

来看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折。

完整流程

流程中的orderCompletedorderCanceled节点没做什么逻辑,大家可以自行发挥,思路和之前的节点一样。所以完整的流程是这样:

从Serverless工作流中看到的节点流转是这样的:

总结

到此,我们基于Serverless工作流和Serverless函数计算构建的订单模块示例就算完成了,在示例中,有两个点需要大家注意:

  1. 配置商家和支付方式的元数据规则。
  2. 确认支付页面的元数据规则。
    因为在实际生产中,我们需要将可定制的部分都抽象为元数据描述,需要有配置界面制定商家的支付方式即更新元数据规则,然后前端页面基于元数据信息展示相应的内容。

所以如果之后需要接入其他的支付方式,只需在paymentCombination路由节点中确定好路由规则,然后增加对应的支付方式函数即可。通过增加元数据配置项,就可以在页面显示新加的支付方式,并且路由到处理新支付方式的函数中。

]]>

随着互联网人口红利逐渐减弱,基于流量的增长已经放缓,互联网行业迫切需要找到一片足以承载自身持续增长的新蓝海。产业互联网正是这一宏大背景下的新趋势。我们看到互联网浪潮正在席卷传统行业,云计算、大数据、人工智能开始大规模融入到金融、制造、物流、零售、文娱、教育、医疗等行业的生产环节中,这种融合称为产业互联网。而在产业互联网中,有一块不可小觑的领域是SaaS领域,它是ToB赛道的中间力量。比如CRM、HRM、费控系统、财务系统、协同办公等等。

SaaS系统面临的挑战

在消费互联网时代,大家是我想要的东西,各个厂商在云计算、大数据、人工智能等技术基座之上建立流量最大化的服务与生态,基于海量内容分发与流量共享为逻辑构建系统。而到了产业互联网时代,供给关系发生了变化,大家是定制我想要的东西,需要从供给与需求两侧出发进行双向建设,这个时候系统的灵活性和扩展性面临着前所未有的挑战,尤其是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的概念做过阐述了,并且也介绍了Serverless函数计算(FC)的概念和实践。这篇文章中介绍一下构建系统灵活性的核心要素服务编排,Serverless工作流。

Serverless 工作流(FnF)是一个用来协调多个分布式任务执行的全托管云服务。在 Serverless工作流中,可以用顺序、分支、并行等方式来编排分布式任务,Serverless工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行您定义的重试逻辑,以确保工作流顺利完成。Serverless工作流通过提供日志记录和审计来监视工作流的执行,可以轻松地诊断和调试应用。

]]>
<![CDATA[Serverless在编程教育中的实践]]> http://www.devtalking.com//articles/serverless-online-coding/ 2020-04-19T16:00:00.000Z 2021-02-03T16:01:52.369Z

说起Serverless这个词,我想大家应该都不陌生,那么Serverless这个词到底是什么意思?Serverless到底能解决什么问题?可能很多朋友还没有深刻的体会和体感。这篇文章我就和大家一起聊聊Serverless。

什么是Serverless

我们先将Serverless这个词拆开来看。Server,大家都知道是服务器的意思,说明Serverless解决的问题范围在服务端。Less,大家肯定也知道它的意思是较少的。那么Serverless连起来,再稍加修饰,那就是较少的关心服务器的意思。

Serverfull时代

我们都知道,在研发侧都会有研发人员和运维人员两个角色,要开发一个新系统的时候,研发人员根据产品经理的PRD开始写代码开发功能,当功能开发、测试完之后,要发布到服务器。这个时候开始由运维人员规划服务器规格、服务器数量、每个服务部署的节点数量、服务器的扩缩容策略和机制、发布服务过程、服务优雅上下线机制等等。这种模式是研发和运维隔离,服务端运维都由专门的运维人员处理,而且很多时候是靠纯人力处理,也就是Serverfull时代。

DevOps时代

互联网公司里最辛苦的是谁?我相信大多数都是运维同学。白天做各种网络规划、环境规划、数据库规划等等,晚上熬夜发布新版本,做上线保障,而且很多事情是重复性的工作。然后慢慢就有了赋能研发这样的声音,运维同学帮助研发同学做一套运维控制台,可以让研发同学在运维控制台上自行发布服务、查看日志、查询数据。这样一来,运维同学主要维护这套运维控制台系统,并且不断完善功能,轻松了不少。这就是研发兼运维的DevOps时代。

Serverless时代

渐渐的,研发同学和运维同学的关注点都在运维控制台了,运维控制台的功能越来越强大,比如根据运维侧的需求增加了自动弹性扩缩、性能监控的功能,根据研发侧的需求增加了自动化发布的流水线功能。因为有了这套系统,代码质量检测、单元测试、打包编译、部署、集成测试、灰度发布、弹性扩缩、性能监控、应用防护这一系列服务端的工作基本上不需要人工参与处理了。这就是NoOps,Serverless时代。

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架构的产品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来处理。

从上图可以看到,高可用有两种策略:

  • 给函数设置并发实例数,假如设置为3,那么有三个请求进来时,该函数只启一个实例,但是会启三个线程来运行逻辑。
  • 线程数达到上限后,会再拉起一个函数实例。

大家看到这里,可能已经大概对基于FC实现在线编程教育系统的架构有了一个大概的轮廓。

上图是基于FC实现的在线编程教育系统的架构图,在这个架构下来看看上述那三个核心问题怎么解:

  • 工作量和灵活性:我们只需要关注在如何执行代码的业务逻辑上,如果要加新语言,只需要创建一个对应语言Runtime的FC函数即可。
  • 高可用:多线程运行业务逻辑和多实例运行业务逻辑两层高可用保障,并且函数实例的扩缩完全都是FC自动处理,不需要研发和运维同学做任何配置。
  • 成本优化:当没有请求的时候,函数实例是不会被拉起的,此时也不会计费,所以在流量低谷期或者夜间时,整个FC的成本消耗是非常低的。可以做到函数实例个数、计费粒度和流量完美的贴合。

Python编程语言示例

下面以运行Python代码为例来看看如何用FC实现Python在线编程Demo。

创建服务和函数

打开函数计算(FC)控制台,选择对应的Region,选择左侧服务/函数,然后新建服务:

输出服务名称,创建服务。

进入新创建的服务,然后创建函数,选择HTTP函数,即可配置HTTP触发器的函数:

设置函数的各个参数:

几个需要的注意的参数这里做以说明:

  • 运行环境:这个很好理解,这里选择Python3
  • 函数实例类型:这里有弹性实例和性能实例两种,前者最大支持2C3G规格的实例,后者支持更大的规格,最大到8C16G。
  • 函数入口:详细参见文档
  • HTTP触发器认证方式:anonymous为不需要鉴权,function是需要鉴权的。

代码解析

函数创建好,进入函数,可以看到概述、代码执行、触发器、日志查询等页签,我们先看触发器,会看到这个函数自动创建了一个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')]

整个代码思路如下:

  • 从前端传入代码片段,格式是字符串。
  • 在FC函数中获取到传入的代码字符串,截取code内容和input的内容。因为这里简单实现了Python中input交互的能力。
  • 将代码保存为一个Python文件,以时间戳为文件名,保存在FC函数的/tmp目录下。(每个FC函数都有独立的/tmp目录,可以存放临时文件)
  • 然后在文件中追加了引入time库的代码,应对sleep这种交互场景。
  • 通过subprocess创建子进程,以Shell的方式通过Python命令执行保存在/tmp目录下的Python文件。如果有用户输入的信息,则通过标准输入输出写入子进程。
  • 最后读取执行结果返回给前端。

前端代码

前端我使用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

我们先将Serverless这个词拆开来看。Server,大家都知道是服务器的意思,说明Serverless解决的问题范围在服务端。Less,大家肯定也知道它的意思是较少的。那么Serverless连起来,再稍加修饰,那就是较少的关心服务器的意思。

Serverfull时代

我们都知道,在研发侧都会有研发人员和运维人员两个角色,要开发一个新系统的时候,研发人员根据产品经理的PRD开始写代码开发功能,当功能开发、测试完之后,要发布到服务器。这个时候开始由运维人员规划服务器规格、服务器数量、每个服务部署的节点数量、服务器的扩缩容策略和机制、发布服务过程、服务优雅上下线机制等等。这种模式是研发和运维隔离,服务端运维都由专门的运维人员处理,而且很多时候是靠纯人力处理,也就是Serverfull时代。

DevOps时代

互联网公司里最辛苦的是谁?我相信大多数都是运维同学。白天做各种网络规划、环境规划、数据库规划等等,晚上熬夜发布新版本,做上线保障,而且很多事情是重复性的工作。然后慢慢就有了赋能研发这样的声音,运维同学帮助研发同学做一套运维控制台,可以让研发同学在运维控制台上自行发布服务、查看日志、查询数据。这样一来,运维同学主要维护这套运维控制台系统,并且不断完善功能,轻松了不少。这就是研发兼运维的DevOps时代。

Serverless时代

渐渐的,研发同学和运维同学的关注点都在运维控制台了,运维控制台的功能越来越强大,比如根据运维侧的需求增加了自动弹性扩缩、性能监控的功能,根据研发侧的需求增加了自动化发布的流水线功能。因为有了这套系统,代码质量检测、单元测试、打包编译、部署、集成测试、灰度发布、弹性扩缩、性能监控、应用防护这一系列服务端的工作基本上不需要人工参与处理了。这就是NoOps,Serverless时代。

]]>
<![CDATA[Kafka从上手到实践-Kafka集群:重要配置和性能探讨]]> http://www.devtalking.com//articles/kafka-practice-19/ 2019-04-14T16:00:00.000Z 2020-06-21T08:12:29.010Z

最后这一章节总结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性能大概有五个因素。

磁盘I/O

我们知道Kafka是将大多数数据保存在磁盘上的。所以磁盘的读写性能很大程度上会影响Kafka系统的性能。所以我们可以注意以下几点:

  • 使用性能比较好的XFS日志文档系统,既Linux中的文件系统。
  • 如果发现在I/O操作方面出现了瓶颈,那么可以通过扩充磁盘来改善。Broker配置文件中的log.dirs配置项可以配置多个数据目录路径。
  • 设置合理的数据清理时间,也就是配置文件中的log.retention.hours配置项。如果已经消费的数据长时间保留在磁盘中,既没有意义又会对Kafka读写性能造成影响。
  • 及时监控部署Kafka服务器的磁盘情况。

网络

数据传输的延迟性是任何MQ系统都要关注的问题,Kafka也不例外,在这方面我们要注意以下几点:

  • 确保部署Kafka的服务器和部署Zookeeper的服务器在一个内网内,服务器之间的物理距离不要太远,比如一个在北京,一个在上海。
  • 确保部署不同Kafka Broker的服务器在一个内网内,服务器之间的物理距离不要太远。
  • 保证服务器有比较好的网络带宽配置。

RAM

Kafka的高性能特性离不开对计算机内存的使用技术,对内存的使用大体分Java堆内存的使用和操作系统(Linux)Page Cache的使用:

  • 在启动Kafka Broker时,可以通过环境变量KAFKA_HEAP_OPTS设置对Java堆内存的使用大小。比如export KAFKA_HEAP_OPTS=“-Xmx4g”
  • Broker中的Partition数量会影响对Java堆内存的使用大小。Partition越多,堆内存使用的越多。
  • 对于Page Cache/文件Cache,我们不用做任何设置:

    Page Cache:当应用程序需要读取文件中的数据时,操作系统先分配一些内存,将数据从存储设备读入到这些内存中,然后再将数据分发给应用程序;当需要往文件中写数据时,操作系统先分配内存接收用户数据,然后再将数据从内存写到磁盘上。文件 Cache 管理指的就是对这些由操作系统分配,并用来存储文件数据的内存的管理。

CPU

因为Kafka在Message传输的整个过程中,不会对Message进行任何计算,所以CPU通常不会成为Kafka性能的主要瓶颈。但是在一些情况下,也会对Kafka的性能产生影响:

  • Message加密/解密的过程中会增加CPU的负载。
  • Message压缩/解压的过程中会增加CPU的负载。
  • 在GC堆内存时会增加CPU的负载。

操作系统

通常优先推荐使用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,以保证数据完整性和整个集群的健壮性。
]]>
<![CDATA[Kafka从上手到实践-实践真知:搭建Kafka相关的UI工具]]> http://www.devtalking.com//articles/kafka-practice-18/ 2019-03-31T16:00:00.000Z 2020-06-21T08:12:29.010Z

这一节主要介绍Zookeeper和Kafka的UI管理工具。

ZKUI

ZKUI是一款简洁易用的Zookeeper信息管理工具。首先从Github上克隆工程到本地,这是一个Maven工程,然后mvn clean install,在target目录下打出两个jar包zkui-2.0-SNAPSHOT.jarzkui-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,可以看到如下界面:

整个界面分为三部分:

  • 顶部一行是快捷操作,比如创建zNode、删除zNode、给zNode添加数据、每个Zookeeper Server的监控信息等。
  • 左侧列出的是含有子zNode的zNode,所以文件夹作为icon。点击后会进入该zNode,整个界面以递归的方式展示。
  • 右侧是不包含子zNode的zNode,所以直接展示zNode名称和存储的数据。

从上图可以看到,左侧有名为brokers的zNode,点击进去后显示他的两个zNode,idstopics

再点进ids可以看到,它还有三个子zNode,分别是Kafka集群中的三个Broker的信息:

如果进入topics,可以看到它下面的子zNode都是我们之前创建的Topic,再进入每个Topic会看到Partition的zNode。充分展示了Zookeeper管理Kafka的方式。

ZKUI可以让我们方便直观的管理Zookeeper中的zNode,大大提高我们的工作效率。

Kafka Manager

Kafka Manager是一款强大的Kafka集群监控工具。首先做一些准备工作:

  • Github上下载 kafka-manager-1.3.3.22
  • 为了之后编译速度能快一些,先配置一下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_clusteranother_topic_in_cluster这两个Topic是在Kafka集群中创建的,所以它们的Partitions和Replicas被均匀的分配到了三个Broker中。而其他的Topic都是在单机Kafka时创建的,所以他们的Partitions和Replicas都在一个Broker里。可见Kafka并不能自动改变之前已存在的Topic Partitions的分布情况。

我们点击进入之前创建的my_topic_in_clusterTopic看一下它的详情:

从上图可以看到,从Kafka Manager中可以很清晰的看到Topic Partitions、ISR、Leader在Kafka集群中的分布情况。同时,也提供了对Topic的各种快捷操作,非常方便。

小结

这一章节带大家实践搭建Zookeeper和Kafka的UI管理工具,通过可视化的视图以及方便的快捷操作能有效的监控Zookeeper和Kafka的状态以及大大提高生产效率。下一章节会对Kafka的重要配置和性能做一些探讨。希望能给小伙伴们带来帮助。

]]>

这一节主要介绍Zookeeper和Kafka的UI管理工具。

ZKUI

ZKUI是一款简洁易用的Zookeeper信息管理工具。首先从Github上克隆工程到本地,这是一个Maven工程,然后mvn clean install,在target目录下打出两个jar包zkui-2.0-SNAPSHOT.jarzkui-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 &

]]>
<![CDATA[Kafka从上手到实践-Kafka集群:启动Kafka集群]]> http://www.devtalking.com//articles/kafka-practice-17/ 2019-03-14T16:00:00.000Z 2020-06-21T08:12:29.010Z

这一章节来真正启动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不能相同。
  • 阿里云ECS的内/外网IP需要配置正确。

然后使用如下命令分别启动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

上面的命令有这样几个信息:

  • 连接Zookeeper时,连Zookeeper集群中的任意一个Zookeeper即可。
  • 创建的Topicmy_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

]]>
<![CDATA[Kafka从上手到实践-Kafka集群:Kafka Listeners]]> http://www.devtalking.com//articles/kafka-practice-16/ 2019-02-28T16:00:00.000Z 2020-06-21T08:12:29.010Z

这一章节主要对和Listener相关的四个配置项做以详细解释。listenersadvertised.listenerslistener.security.protocol.mapinter.broker.listener.name这四个配置项可能是大家最容易混淆和最不容易理解的。

在解释这些配置项之前,我们先来明确几个概念。

  • 部署Broker的阿里云ECS称为Host Machine。
  • 在阿里云ECS里启动的Producer或者Consumer,比如使用Kafka CLI启动的称为Internal Client。
  • 在大家的IDEA中使用Java编写的,或者第三方的Producer/Consumer,称为External Client。
  • Host Machine具有外网IP和内网IP。
  • Internal Client可以同时和Host Machine的外网IP及内网IP通信。
  • External Client只能和Host Machine的外网IP通信。
  • 多个阿里云ECS之间可以同时通过外网IP及内网IP通信。
    • 既在这个特定的场景下,Host Machine之间可以同时通过外网IP及内网IP通信。
    • 再换句话说就是不同Host Machine上的Broker之间可以同时通过外网IP及内网IP通信。

如上图所示,是一个很常见的Kafka集群场景,涵盖了上述的概念。图中那些通信虚线箭头就是靠Kafka的Listener建立的,并且是通过Kafka中不同的Listener建立的,这些Listener分为Internal Listener和External Listener。如下图所示:

那么这些Listener的创建以及内外部如何通信都是由上面那四个配置项决定的。

listener.security.protocol.map

先来看listener.security.protocol.map配置项,在上一章节中介绍过,它是配置监听者的安全协议的,比如PLAINTEXTSSLSASL_PLAINTEXTSASL_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

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

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暴露出去的需求。

inter.broker.listener.name

这个配置项从名称就可以看出它的作用了,就是指定一个listener.security.protocol.map配置项中配置的Key,或者说指定一个或一类Listener的名称,将它作为Internal Listener。这个Listener专门用于Kafka集群中Broker之间的通信

inter.broker.listener.name=INTERNAL_LISTENER_BROKER

listener 和 advertised.listeners 的关系

先来看看KafkaConfig.scalaSocketServer.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配置项的内容。

/**
* Create a server socket to listen for connections on.
*/

private def openServerSocket(host: String, port: Int): ServerSocketChannel = {
val socketAddress =
if (host == null || host.trim.isEmpty)
new InetSocketAddress(port)
else
new InetSocketAddress(host, port)
val serverChannel = ServerSocketChannel.open()
serverChannel.configureBlocking(false)
if (recvBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE)
serverChannel.socket().setReceiveBufferSize(recvBufferSize)

try {
serverChannel.socket.bind(socketAddress)
info("Awaiting socket connections on %s:%d.".format(socketAddress.getHostString, serverChannel.socket.getLocalPort))
} catch {
case e: SocketException =>
throw new KafkaException("Socket server failed to bind to %s:%d: %s.".format(socketAddress.getHostString, port, e.getMessage), e)
}
serverChannel
}

跟到里面,可以看到如果没有配置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相关的四个配置项做以详细解释。listenersadvertised.listenerslistener.security.protocol.mapinter.broker.listener.name这四个配置项可能是大家最容易混淆和最不容易理解的。

在解释这些配置项之前,我们先来明确几个概念。

  • 部署Broker的阿里云ECS称为Host Machine。
  • 在阿里云ECS里启动的Producer或者Consumer,比如使用Kafka CLI启动的称为Internal Client。
  • 在大家的IDEA中使用Java编写的,或者第三方的Producer/Consumer,称为External Client。
  • Host Machine具有外网IP和内网IP。
  • Internal Client可以同时和Host Machine的外网IP及内网IP通信。
  • External Client只能和Host Machine的外网IP通信。
  • 多个阿里云ECS之间可以同时通过外网IP及内网IP通信。
    • 既在这个特定的场景下,Host Machine之间可以同时通过外网IP及内网IP通信。
    • 再换句话说就是不同Host Machine上的Broker之间可以同时通过外网IP及内网IP通信。
]]>
<![CDATA[Kafka从上手到实践-Kafka集群:配置Broker]]> http://www.devtalking.com//articles/kafka-practice-15/ 2019-02-19T16:00:00.000Z 2020-06-21T08:12:29.009Z

接下来几个章节我们开始搭建真正的Kafka集群,服务器还是使用上一节章节搭建Zookeeper使用的三台阿里云ECS。

搭建单机Kafka章节中,在Kafka的/root/kafka_2.12-2.0.0/config/server.properties配置文件中,我们只配置了log.dirsadvertised.listeners这两个配置项,其他配置项都是使用默认值。

Kafka的配置项一共多达140余个,虽然有一部分通常情况下我们不需要修改,使用默认值即可,但这只是一少部分。搭建Kafka集群时,光通常情况下需要考虑的配置项就有40余个。

另外,这些配置项要根据具体的业务场景做各种调整,不存在一套配置项通吃所有业务场景的情况,而且基本不可能一次性配置出性能最优、最能满足业务场景的配置项组合,都需要经过调整、测试,反复进行配置才能总结出相对最优的配置项组合。

Broker配置

先展示一份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 Server的基础配置涉及到四个配置项:

  • broker.id:整个Kafka集群内标识唯一Broker的ID。整数类型。
  • host.name:部署Broker的服务器IP地址或者域名。该参数已作废。
  • port:Broker开放的端口号。该参数已作废。
  • delete.topic.enable:是否允许删除Topic。
  • auto.create.topics.enable:是否允许在Producer在未指定Topic发送Message时自动创建Topic。

Socket Server配置

传输通信方面的配置涉及到六个配置项:

  • 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
    listener.security.protocol.map=LISTENER_BOB:PLAINTEXT,LISTENER_JOHN:SSL
  • 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的相关配置涉及到两个配置项:

  • zookeeper.connect:设置Zookeeper地址。可用逗号分割配置多个地址,既Zookeeper集群的地址。
  • zookeeper.connection.timeout.ms:等待连接Zookeeper的超时时间。

Consumer Group相关配置

Consumer Group相关的配置主要涉及到一个配置项:

  • group.initial.rebalance.delay.ms:当Consumer Group新增或减少Consumer时,重新分配Topic Partition的延迟时间。

Message相关配置

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.dirsadvertised.listeners这两个配置项,其他配置项都是使用默认值。

Kafka的配置项一共多达140余个,虽然有一部分通常情况下我们不需要修改,使用默认值即可,但这只是一少部分。搭建Kafka集群时,光通常情况下需要考虑的配置项就有40余个。

另外,这些配置项要根据具体的业务场景做各种调整,不存在一套配置项通吃所有业务场景的情况,而且基本不可能一次性配置出性能最优、最能满足业务场景的配置项组合,都需要经过调整、测试,反复进行配置才能总结出相对最优的配置项组合。

]]>
<![CDATA[Kafka从上手到实践-实践真知:搭建Zookeeper集群]]> http://www.devtalking.com//articles/kafka-practice-14/ 2019-02-09T16:00:00.000Z 2020-06-21T08:12:29.009Z

这一节我们来真正搭建一个Zookeeper集群。

搭建Zookeeper集群

首先要做的就是再租赁两个服务器,参照搭建单机Kafka章节中的步骤,租赁阿里云服务器、安装JDK、下载配置Kafka、配置安全组规则。

Zookeeper配置信息

搭建单机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秒。

Zookeeper集群节点列表

首先节点列表的配置规则为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

阿里云ECS服务监听所有网卡

如果现在通过/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集群配置总结

在启动Zookeeper集群前,先来总结一下配置工作:

  • 租赁三台阿里云ECS,下载JDK、Kafka、配置安全组规则。
  • /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节点都启动后,我们可以通过下面两个方法对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.1zookeeper.server.2是Follower,zookeeper.server.3是Leader,因为前两个节点并没有监听2888端口。

通过Zookeeper CLI验证

我们还可以通过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 The Four Letter Words Commands

Zookeeper提供了一些能够查看节点Server状态、Client连接Server的状态、节点健康状态的命令。因为命令大多都是四个字母的简写,所以称为The Four Letter Words Commands,我称为四字真言。

首先来看看整体的命令格式:

echo "xxxx" | nc IP Port

  • xxxx就是四字真言命令。
  • IP是Zookeeper节点的IP。
  • Port自然是Zookeeper监听的2181端口。

下面来具体看看这些命令。

查看Zookeeper节点配置

该命令可以查看指定节点的配置信息:

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信息

该命令可以查看连接到指定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)

查看Session及临时节点信息

该命令可以查看指定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节点的环境变量

该命令可以查看指定Zookeeper节点的环境变量信息:

echo "envi" | nc zookeeper.server.3 2181

监测Zookeeper节点可用状态

该命令可以查看指定Zookeeper节点是否正常:

echo "ruok" | nc zookeeper.server.3 2181

imok

如果节点正常则返回imok,如果不正常则没有任何响应。

查看Zookeeper节点的信息

该命令可以查看指定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信息

该命令可以查看指定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节点的监控状态信息

该命令可以查看指定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集群。

搭建Zookeeper集群

首先要做的就是再租赁两个服务器,参照搭建单机Kafka章节中的步骤,租赁阿里云服务器、安装JDK、下载配置Kafka、配置安全组规则。

Zookeeper配置信息

搭建单机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

]]>
<![CDATA[Kafka从上手到实践-Zookeeper CLI:CRUD zNode]]> http://www.devtalking.com//articles/kafka-practice-13/ 2019-01-27T16:00:00.000Z 2020-06-21T08:12:29.009Z

这一节来看看Zookeeper的命令行工具。

Zookeeper CLI

在第七章节搭建单机Kafka中,我们已经发现了,Kafka是自带Zookeeper的,而且在启动Kafka之前,要先启动Zookeeper,相当于启动了单机Zookeeper,所以我们先说Zookeeper CLI,后面说Zookeeper集群时再具体说配置参数。

展示zNode

首先打开终端,连接至我们的服务器,进入/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]

查看zNode存储的数据

在上一章节中说过,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的标准属性了,有各种时间戳、版本号、数据长度、子节点数等。

创建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

更新zNode的数据

我们可以通过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

可以看到dataVersioncversion从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

可以看到cversionnumChildren都变了。

添加zNode Watcher

上一章节同样说过,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

删除zNode

可以通过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的命令行工具。

Zookeeper CLI

在第七章节搭建单机Kafka中,我们已经发现了,Kafka是自带Zookeeper的,而且在启动Kafka之前,要先启动Zookeeper,相当于启动了单机Zookeeper,所以我们先说Zookeeper CLI,后面说Zookeeper集群时再具体说配置参数。

展示zNode

首先打开终端,连接至我们的服务器,进入/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]

]]>