<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>GeTui (个推)</title>
    <link>https://ruby-china.org/GeTui</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>浅谈移动端 View 的显示过程</title>
      <description>&lt;p&gt;&lt;em&gt;作者：个推安卓开发工程师  一七&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;随着科技的发展，各种移动端早已成为人们日常生活中不可或缺的部分，人们使用移动端产品工作、社交、娱乐……移动端界面的流畅性已经成为影响用户体验的重要因素之一。那么你是否思考过移动端所展现的流畅画面是如何实现的呢？&lt;/p&gt;

&lt;p&gt;本文通过对移动端 View 显示过程的简略分析，帮助开发者了解 View 渲染的逻辑，更好地优化自己的 APP。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/048aa50d81af0f282170734eba23f372.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;上图展示的是一个完整的页面渲染过程。通过上图，我们可以初步了解每一帧页面从代码布局的编写到展示给使用者，其背后的逻辑是如何一步一步执行的。&lt;/p&gt;
&lt;h2 id="屏幕如何呈像"&gt;屏幕如何呈像&lt;/h2&gt;&lt;h3 id="像素点"&gt;像素点&lt;/h3&gt;
&lt;p&gt;在电子屏幕中显示的图片，其实都是由一个个“小点”所组成的，这些“小点”被称为“像素点”。每一个像素点都有自己的颜色，每一张完整的图片都是由它们相连拼接形成的。&lt;/p&gt;

&lt;p&gt;每个像素点一般都有 3 个子像素：红、绿、蓝，根据这三种原色，我们能够调制出各种各样的颜色。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/47b45cff5ca291848456a57b2ad7fc81.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="大电视机"&gt;大电视机&lt;/h3&gt;
&lt;p&gt;与现在的平板电视不同的是，以前的黑白电视机或者大背投彩电，总是带着大大的“后背”。“大后背”电视其实就是阴极射线管电视机，俗称显像管电视。其成像原理是电子枪发射出的电子束（阴极射线）通过聚焦系统和偏转系统，射向屏幕上涂有荧光层的指定位置。被电子束轰击的每个位置，荧光层都会产生一个小亮点，最终小亮点们将会组成一幅幅影像，显示在电视屏幕上。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/c06ffe08889f9b00350616a00ac03883.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/fb61e16e7bc8f4fa72c0d5991090b371.gif" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这也是以前大电视机的屏幕都呈圆弧形的原因。因为越接近圆形，边长到中心的距离越相近，呈像越均匀。那为什么当磁铁贴近电视机时，会让电视机的成像出现问题呢？那是因为磁铁会干扰电子束的正常轨迹，并且在贴近屏幕的时候，也可能使得屏幕的荧光层磁化，出现一个个不正常的光斑。&lt;/p&gt;

&lt;p&gt;下图展示的是摄像机慢放后，电子束的绘制过程。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/1127ad5cded6a912dda160dcc32484fb.gif" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/10c12d977867575a213a1a56b0614c3f.gif" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="LCD 和 OLED"&gt;LCD 和 OLED&lt;/h3&gt;
&lt;p&gt;随着科技的不断进步，电视、手机、电脑的体积越来越薄，射线管显像方式也逐渐被淘汰。目前在手机市场上占据主流地位的是 LCD 和 OLED 两种屏幕。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/bed315392efa4704020833e16b8606df.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;LCD 全称为 Liquid Crystal Display，即液晶显示器。OLED 全称为 Organic Light-Emitting Diode，即有机发光二极管。这两者之间存在显著的差别：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. 两者成像原理不同&lt;/strong&gt;
LCD 是靠白色的背光穿透彩色薄膜显色的，而 OLED 则是靠每个像素点自行发光。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 在耗电量方面&lt;/strong&gt;
LCD 的耗电量较高，即使只显示一个亮点，LCD 的背光源也需要一直发光，而且容易出现漏光现象。而 OLED 的每个像素都能独立工作，而且 可以自行发光，因此采用 OLED 的设备可以制作得更薄，甚至可以弯曲。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.在制作方面&lt;/strong&gt;
 LCD 使用的是无机材料，OLED 则需要使用有机材料，因此 OLED 的制作费用更高，并且使用寿命不如 LCD。&lt;/p&gt;
&lt;h2 id="图形显示核心 GPU"&gt;图形显示核心 GPU&lt;/h2&gt;
&lt;p&gt;与 CPU 相对比，GPU 的计算单元更多，更擅长大规模并发计算，例如密码破解、图像处理等。CPU 则是遵循冯诺依曼架构存储程序顺序执行，在大规模并行计算能力上，受到的限制更大，因此更擅长逻辑控制。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/f8aea2e777c34fcb38b9ec66b88ac9b0.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="应用程序编程接口 API (OpenGL)"&gt;应用程序编程接口 API (OpenGL)&lt;/h2&gt;
&lt;p&gt;在没有统一的 API 之前，开发者需要在各式各样的图形硬件上编写各种自定义接口和驱动程序，工作量极大。&lt;/p&gt;

&lt;p&gt;1990 年 SGI（硅谷图形公司）成为了工作站 3D 图形领域的领导者，并将其 API 转变为一项开放标准，即 OpenGL。后来，SGI 还促成了 OpenGL 架构审查委员会（OpenGL ARB）的创建。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/1199952324382789601e63142cf3a48e.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="垂直同步 Vertical Synchronization"&gt;垂直同步 Vertical Synchronization&lt;/h2&gt;
&lt;p&gt;当我们在使用手机 APP 的过程中，发现页面出现卡顿现象，那么极有可能是页面没有在 16ms 内更新导致的。实际上，人眼与大脑之间的协作无法感知超过 60fps 的画面更新。60fps 相当于是每秒 60 帧，那么每个页面需要在 1000/60 = 16ms 内更新为其他页面，才不会让我们感受到页面的卡顿。&lt;/p&gt;

&lt;p&gt;而在没有 VSync 的情况下可能会出现以下情况：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/a40f985ccf9e0bb5fdefe8dc4b47a271.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;如上图所示，在没有 VSync 的情况下，会出现需要显示第二帧时，其尚未处理完成的情况，因此 Display 中显示的仍是第一帧。这会造成该帧显示时长超过 16ms，从而导致页面卡顿的现象。&lt;/p&gt;

&lt;p&gt;为了使 CPU、GPU 生成帧的速度与 Display 保持一致，Android 系统每 16ms 就会发出一次 VSYNC 信号，触发 UI 渲染更新。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/d30bbcae6d7d9055d5f82d8e9856f619.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;从上图中我们可以看出，每隔 16ms，安卓会发出一个 VSync 信号，收到信号后 CPU 开始处理下一帧的的内容，GPU 在 CPU 处理结束之后，将会进行光栅化，此时屏幕上显示的是上一帧已经处理完成的页面。如此反复，就可以在页面中展示一幅幅的指定画面。而确保画面流畅的前提是 CPU 和 GPU 处理一帧所花费的时间不能超过 16 ms，否则就会出现以下情况：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/85073ea77b1ccec5a87ac5d377596107.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;当 CPU 和 GPU 处理一帧的时间超过了 16 ms 时，在第一个 Display 中，由于 GPU 处理 B 画面的时间过长，导致系统发出 VSync 信号时，Display 不能及时地显示出 B 画面，而重复显示 A 页面，造成卡顿。&lt;/p&gt;

&lt;p&gt;此外，在第二个 Display 中，由于 A Buffer 还在被 Display 所使用，不能在收到 VSync 信号后开始处理下一帧的页面，导致该时间段内 CPU 的闲置。为了避免这种时间的浪费，三缓存机制由此出现：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/cb62cdae4de8c7078ceae36b53cc4bee.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;如上图所示，在三缓存机制中，当 A 缓存被 Display 使用、B 缓存被 GPU 处理时，系统会发出 Vsync 信号，并加入新的缓存 C，用来缓存下一帧的内容。这种方式虽然不能完全避免 A 页面的重复显示，但是能够让后面页面的显示更加平滑。&lt;/p&gt;
&lt;h2 id="View 的绘制流程"&gt;View 的绘制流程&lt;/h2&gt;
&lt;p&gt;View 的绘制是从 ViewRootImpl 的 performTraversals() 方法开始的，其整体流程大致分为三步，如下图所示：
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/533f5ba741991c47ff6f553961d17a52.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="measure"&gt;measure&lt;/h3&gt;
&lt;p&gt;控件测量过程从 performMeasure() 方法开始。在该方法中 childWidthMeasureSpec 和 childHeightMeasureSpec，分别是用来确定宽度和高度的。&lt;/p&gt;

&lt;p&gt;MeasureSpec 是一个 int 值，它存储着两个信息：低 30 位是 View 的 specSize，高 2 位是 View 的 specMode。&lt;/p&gt;
&lt;h3 id="specMode 有三种类型："&gt;specMode 有三种类型：&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1.UNSPECIFIED&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;父视图对子视图没有任何限制，可以将视图按照开发者的意愿设置成任意的大小，在一般开发过程中不会用到。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.EXACTLY&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;父视图为子视图指定一个确切的尺寸，该尺寸由 specSize 的值来决定。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.AT_MOST&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;父视图为子视图指定一个最大的尺寸，该尺寸的最大值是 specSize。&lt;/p&gt;

&lt;p&gt;观察 View 的 measure() 方法，可以发现该方法是被 final 修饰的，因此 View 的子类只能够通过重载 onMeasure() 方法来完成自己的测量逻辑。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/0e0797234c3490f841abfe0bfcb6c109.png" title="" alt=""&gt;
在 onMeasure() 方法中：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/0d9b6abb64fee9eae6cfbc044de4c7bd.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;调用 getDefaultSize() 方法来获取视图的大小：
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/79c5b74c48f2ced88f512939452e9ce7.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;该方法中的第二个参数 measureSpec 是从 measure() 方法中传递过来的：通过 getMode() 和 getSize() 解析获取其中对应的值，再根据 specMode 给最终的 size 赋值。&lt;/p&gt;

&lt;p&gt;不过以上只是一个简单控件的一次 measure 过程，在真正测量的过程中，由于一个页面往往包含多个子 View，所以需要循环遍历测量，在 ViewGroup 中有一个 measureChildren() 方法，就是用来测量子视图的：
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/1b5749ec710ba16709836e37789aeb96.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;measure 整体流程的方法调用链如下：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/4046f6b1050e6cf765d23903b0da597e.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="layout"&gt;layout&lt;/h3&gt;
&lt;p&gt;在 performTraversals() 方法的测量过程结束后，进入 layout 布局过程：&lt;/p&gt;

&lt;p&gt;performLayout(lp,desiredWindowWidth,desiredWindowHeight);&lt;/p&gt;

&lt;p&gt;该过程的主要作用即根据子视图的大小以及布局参数，将相应的 View 放到合适的位置上。&lt;/p&gt;

&lt;p&gt;host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());&lt;/p&gt;

&lt;p&gt;如上，layout() 方法接收了四个参数，按照顺时针，分别是左上右下。该坐标针对的是父视图，以左上为起始点，传入了之前测量出的宽度和高度。之后，让我们进入到 layout() 方法中观察：
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/77b9294eb86a31c1a5910e16fa9e7829.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;我们通过 setFrame() 方法给四个变量赋值，判断 View 的位置是否变化以及是否需要重新进行 layout，而且其中还调用了 onLayout() 方法。&lt;/p&gt;

&lt;p&gt;在进入该方法后，我们可以发现里面是空的，这是因为子视图的具体位置是相对于父视图而言的，所以 View 的 onLayout 为空实现。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/01bf9a181fac4adb95f30d4909af12e5.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;再进入 ViewGroup 类中查看，我们可以发现，这其实是一个抽象的方法，在这样的情况下，ViewGroup 的子类便需要重写该方法：
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/267ab90951207a2d70d6dc6ba5820547.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="draw"&gt;draw&lt;/h3&gt;
&lt;p&gt;绘制的流程主要如下图所示，该流程也是存在遍历子 View 绘制的过程：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/0330ba428021047316b27a888a1fb6c2.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;需要注意的是，View 的 onDraw() 方法是空的，这是因为每个视图的内容都不相同，这个部分交由子类根据自身的需要来处理，才更加合理：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/13a2a82dc66034600dd5e0d20530d75b.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="安卓渲染机制的整体流程"&gt;安卓渲染机制的整体流程&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/40474d84dc5ad0057bc6ca64b148dff2.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;1.APP 在 UI 线程构建 OpenGL 渲染需要的命令及数据；&lt;/p&gt;

&lt;p&gt;2.CPU 将数据上传 (共享或者拷贝) 给 GPU。(PC 上一般有显存，但是 ARM 这种嵌入式设备内存一般是 GPU、CPU 共享内存)；&lt;/p&gt;

&lt;p&gt;3.通知 GPU 渲染。一般而言，真机不会阻塞等待 GPU 渲染结束，通知结束后就返回执行其他任务；&lt;/p&gt;

&lt;p&gt;4.通知 SurfaceFlinger 图层合成；&lt;/p&gt;

&lt;p&gt;5.SurfaceFlinger 开始合成图层。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;移动端技术发展很快，而画面显示优化是一个持续发展的实践课题，贯穿于每个开发者的日常工作中。未来，个推技术团队将继续关注移动端的性能优化，为大家分享相关的技术干货。&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Fri, 29 Mar 2019 15:37:07 +0800</pubDate>
      <link>https://ruby-china.org/topics/38306</link>
      <guid>https://ruby-china.org/topics/38306</guid>
    </item>
    <item>
      <title>浅谈跨平台框架 Flutter 的优势与结构</title>
      <description>&lt;p&gt;&lt;em&gt;作者：个推 ios 工程师 伊泽瑞尔&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="一、背景"&gt;一、背景&lt;/h2&gt;
&lt;p&gt;目前，移动开发技术主要分为原生开发和跨平台开发两种。其中，原生应用是指在某个特定的移动平台上，使用平台所支持的开发工具和语言，直接调用系统提供的 API 所开发的应用。
&lt;strong&gt;原生开发的主要优势体现在：&lt;/strong&gt;
1.可以快速访问本平台的全部功能，比如摄像头、GPS 等；
2.原生应用的速度快、性能高，而且可以实现比较复杂的动画和绘制效果，用户体验较好。
&lt;strong&gt;原生开发的缺点也很明显，主要体现在：&lt;/strong&gt;
1.开发成本较高，不同的平台必须维护不同的代码，人力成本也会随之增加；
2.有新的功能需要更新时，只能进行版本升级。
随着移动互联网的高速发展，在很多的业务场景下，传统的纯原生开发已经不能满足日益增长的业务需求，主要表现在以下两个方面：
1.应用动态化的需求增大。当需求发生变化，或者是需要增加新的功能时，传统的纯原生应用开发只能通过版本的升级来更新内容，然而应用的上架和审核都需要一定的时间。因此，开发人员迫切地希望进行应用内容的更新时，可以不更新版本，提升工作效率。
2.业务需求变化快，开发成本变高。原生开发一般需要技术团队对 iOS、Android 两个开发平台进行维护。当版本更新迭代时，开发和测试的成本都会增加。
针对上述两个问题，跨平台框架应运而生。&lt;/p&gt;
&lt;h2 id="二、跨平台技术简介"&gt;二、跨平台技术简介&lt;/h2&gt;
&lt;p&gt;针对上文提到的原生开发所面临的问题，目前在 IT 界已经诞生了很多跨平台框架，主要分为三类：
1.H5+ 原生 (Cordova、Ionic、微信小程序)；
2.JavaScript 开发 + 原生渲染 (React Native、Weex、快应用)；
3.自绘 UI+ 原生 (Flutter)。
在本文中，我们将对 React Native、Weex 和 Flutter 进行对比。
&lt;strong&gt;1.React Native&lt;/strong&gt;
React Native 是 Facebook 于 2015 年 4 月开源的跨平台移动应用开发框架，是 Facebook 开源的 JS 框架 React 在原生移动应用平台的衍生物。React Native 使用了 react 的设计模式，但是其 UI 渲染、动画效果、网络请求等均是由原生来实现的。开发者编写 JS 代码，通过 React Native 的中间层转化为原生控件，并进行操作。也就是说通过 JS 代码来调用原生的组件，从而实现相应的功能。
React Native 实现跨平台的功能，主要由 Java、C++ 和 Javascript 三层所构成的。其中，C++ 实现的动态链接库（.so），作为中间适配层桥接，实现了 JS 端与原生端的双向通信交互。React Native 会把应用的 JS 代码编译成一个 JS 文件，React Native 整体框架目标就是为了解释并运行这个 JS 脚本文件，如果是 JS 扩展的 API，则直接通过 bridge 调用 native；如果是 UI 界面，则映射到 virtual DOM 这个虚拟的 JS 数据结构中，通过 bridge 传递到 native，然后根据数据设置各个对应的真实 native 的 View。
 &lt;img src="https://diycode.b0.upaiyun.com/photo/2019/e02aeaab153cd87fc130be3171019989.png" title="" alt=""&gt;
&lt;strong&gt;2.Weex&lt;/strong&gt;
在 Weex 设计之初，开发者就考虑到，使其能够在三端（iOS、安卓和 H5）上均能得到展现。在最上面的 DSL，阿里一般称之为 Weex 文件（.we），通过 Transform 转换为 js-bundle，再部署到服务器，这样服务端就完成了。在客户端，第一层是 JS-Framework，最后是 RenderRengine。
 &lt;img src="https://diycode.b0.upaiyun.com/photo/2019/266d78bfedcdf5cd8097173d5f56049b.png" title="" alt=""&gt;
如上图所示，Weex 的输入是 Virtual DOM，输出是 native 或 H5 view，还原为内存中的树型数据结构，再创建 view，把事件绑定在 view 上，设置 view 的基本属性。Weex Render 会分三个线程，不同的线程负责不同的事情，让 JS 线程优先保障流畅性。&lt;/p&gt;

&lt;p&gt;表面上，Weex 是一种客户端技术，但实际上，它串联起了从本地开发、云端部署到分发的整个链路。开发者可以在本地像编写 Web 页面一样先编写一个 APP 界面，然后通过命令行工具将之编译为一段 JavaScript 代码，生成一个 Weex 的 JS bundle。与此同时，开发者可以将生成的 JS bundle 部署至云端，之后通过网络请求或者预下发的方式加载至用户的移动应用客户端。
在移动应用客户端，Weex SDK 会准备一个 JavaScript 执行环境，在用户打开一个 Weex 页面时，在该环境中执行相应的 JS bundle，并将执行过程中产生的各种命令发送到 native 端，进行界面渲染、数据存储、网络通信、调用设备及用户交互响应等。如果用户希望使用浏览器访问这个界面，那么他可以在浏览器中打开一个相同的 Web 页面，这个页面和移动应用使用相同的页面源代码，但被编译成适合 Web 展示的 JS Bundle，通过浏览器里的 javaScript 引擎及 Weex SDK 运行起来的。
&lt;strong&gt;3.Flutter&lt;/strong&gt;
Flutter 是 Google 推出并开源的移动应用开发框架，主打跨平台、高保真、高性能。开发者可以通过 Dart 语言进行 APP 开发，只需要一套代码就可以同时构建 Android 和 iOS 应用，并且可以达到与原生应用一样的性能。Flutter 还提供了丰富的组件、接口，开发者可以高效地为 Flutter 添加 native 扩展。此外，Flutter 还使用了 Native 引擎渲染视图，为用户提供了良好的体验。
Flutter 与用于构建移动应用程序的其它多数框架不同，因为 Flutter 既不使用 WebView，也不使用操作系统的原生控件。相反，Flutter 使用自己的高性能渲染引擎来绘制 widget。这样不仅可以保证在 Android 和 iOS 的 UI 一致性，而且也可以避免对原生控件依赖而带来的限制和高昂的维护成本。
同时，Flutter 使用 Skia 作为 2D 引擎渲染，Skia 是 Google 的一个 2D 图形处理函数库，在字型、坐标转换以及点阵图等方面都有高效而且简洁的表现。Skia 是跨平台的，并提供了非常友好的 API。由于 Android 系统已经内置了 Skia，所以 Flutter 在打包 APK 时，不需要再将 Skia 打包到 APK 中，但是 iOS 系统并未内置 Skia，所以在构建 API 时，必须将 Skia 一起打包。&lt;/p&gt;
&lt;h2 id="三、高性能的Flutter"&gt;三、高性能的 Flutter&lt;/h2&gt;
&lt;p&gt;目前，Flutter 程序主要有两种运行方式：静态编译与动态解释。静态编译的程序在执行前，会被全部翻译为机器码，通常将这种类型称为 AOT，即“提前编译”。解释执行则是一句句地边翻译边运行，通常将这种类型称为 JIT，即“即时编译”。
AOT 程序的典型代表是用 C/C++ 开发的应用，它们必须在执行前编译成机器码。而 JIT 的代表则非常多，如 JavaScript、python 等。事实上，所有脚本语言都支持 JIT 模式。但需要注意的是，JIT 和 AOT 指的是程序运行方式，和编程语言并非是强关联的，有些语言既可以以 JIT 方式运行，也可以以 AOT 方式运行，如 Java、Python，它们可以在第一次执行时编译成中间字节码，然后在之后的执行中，直接执行字节码。
Flutter 的高性能主要靠两点来保证，首先，Flutter APP 采用 Dart 语言进行开发。当 Dart 在 JIT 模式下时，其运行速度与 JavaScript 基本持平。此外 Dart 支持 还 AOT，当 Dart 在 AOT 模式下事，其运行速度远超 JavaScript。速度的提升对高帧率下的视图数据计算很有帮助。
其次，Flutter 使用自己的渲染引擎来绘制 UI，布局数据等由 Dart 语言直接控制，所以在布局过程中不需要像 RN 那样要在 JavaScript 和 Native 之间通信，在一些滑动和拖动的场景下具有明显优势。由于滑动和拖动往往会引起布局的变化，所以 JavaScript 需要不停地与 Native 之间同步布局信息，这和在浏览器中要 JavaScript 频繁操作 DOM 所带来的问题是相同的，都会带来比较可观的性能开销。&lt;/p&gt;
&lt;h2 id="四、为什么Flutter会选择Dart语言？"&gt;四、为什么 Flutter 会选择 Dart 语言？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1.开发效率高。&lt;/strong&gt;Dart 运行时和编译器支持 Flutter 的两个关键特性的组合，分别是基于 JIT 的快速开发周期和基于 AOT 的发布包。基于 JIT 的快速开发周期：Flutter 在开发阶段，采用 JIT 模式，这样就避免了每次改动都需要进行编译，极大地节省了开发时间。基于 AOT 的发布包，Flutter 在发布时可以通过 AOT 生成高效的 ARM 代码，以保证应用性能。而 JavaScript 则不具备这个能力。
&lt;strong&gt;2.高性能。&lt;/strong&gt;为了实现流畅、高保真的的 UI 体验，Flutter 必须在每个动画帧中都运行大量的代码。这意味着需要一种既能支持高性能，又能保证不丢帧的周期性暂停的语言，而 Dart 支持 AOT，在这一点上比 JavaScript 更有优势。
&lt;strong&gt;3.快速分配内存。&lt;/strong&gt;Flutter 框架使用函数式流，这使得它在很大程度上依赖于底层的内存分配器。
&lt;strong&gt;4.类型安全。&lt;/strong&gt;由于 Dart 是类型安全的语言，支持静态类型检测，所以可以在编译前就发现一些类型的错误，并排除潜在问题。这对于前端开发者来说更具有吸引力。而 JavaScript 是一个弱类型语言，这也是为什么在诸多前端社区中，会有众多为 JavaScript 代码添加静态类型检测的扩展语言和工具。&lt;/p&gt;
&lt;h2 id="五、Flutter框架结构"&gt;五、Flutter 框架结构&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/1a23a68a54815bf23cf842dafa932a23.png" title="" alt=""&gt;
Flutter Framework 是一个完全由 Dart 语言构建的 SDK，它实现了一整套自底而上的基础库。
1.底部两层 (Foundation 和 Animation、Painting、Gestures) 是 Flutter 引擎暴露的底层 UI 库，提供动画、手势及绘制能力。
2.Rendering 层是一个抽象的布局层，它依赖于 dart UI 层。Rendering 层会构建一个 UI 树，当 UI 树有变化时，它会随即计算出有变化的部分，然后更新 UI 树，最终将 UI 树绘制到屏幕上。这个过程类似于 React 中的虚拟 DOM。Rendering 层可以说是 Flutter UI 框架最核心的部分，它除了确定每个 UI 元素的位置、大小之外，还要进行坐标变换和绘制 (调用底层 dart:ui)。
3.Widgets 层是 Flutter 提供的一套基础组件库，在基础组件库之上，Flutter 还提供了 Material 和 Cupertino 两种视觉风格的组件库。
Flutter Engine：这是一个完全由 C++ 实现的 SDK，其中包括了 Skia 引擎、Dart 运行时和文字排版引擎等。在代码调用 dart:ui 库时，调用最终会走到 Engine 层，然后实现真正的绘制逻辑。
React Native、Weex 和 Flutter 进行对比结果如下所示：
 &lt;img src="https://diycode.b0.upaiyun.com/photo/2019/f62c7132f79212ad5d0a5d5d1caef0dc.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="六、总结"&gt;六、总结&lt;/h2&gt;
&lt;p&gt;从 Flutter 的设计理念来看，其整体架构都是具有革命性的，相比于其他架构，它实现了真正意义上的跨平台。它能够让各平台的体验一致，并且让用户体验达到更优。现如今，Flutter 的各种 UI 库和组件都在不断增加，与之相关的各种生态系统和社区也在不断完善，它对新的操作系统的适配性将会越来越强。相信在不久的将来，Flutter 会慢慢成熟起来，成为主流的开发语言之一。&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Thu, 21 Mar 2019 15:37:22 +0800</pubDate>
      <link>https://ruby-china.org/topics/38268</link>
      <guid>https://ruby-china.org/topics/38268</guid>
    </item>
    <item>
      <title>个推微服务网关架构实践</title>
      <description>&lt;p&gt;&lt;em&gt;作者：个推应用平台基础架构高级研发工程师 阿飞&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;在微服务架构中，不同的微服务可以有不同的网络地址，各个微服务之间通过互相调用完成用户请求，客户端可能通过调用 N 个微服务的接口完成一个用户请求。因此，在客户端和服务端之间增加一个 API 网关成为多数微服务架构的必然选择。&lt;/p&gt;

&lt;p&gt;在个推的微服务实践中，API 网关也起着至关重要的作用。一方面，&lt;strong&gt;API 网关是个推微服务体系对外的唯一入口&lt;/strong&gt;；另一方面，&lt;strong&gt;API 网关中实现了很多后端服务的共性需求，避免了重复建设&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="个推微服务网关的设计与实现"&gt;个推微服务网关的设计与实现&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;个推微服务主要是基于 Docker 和 Kubernetes 进行实践的。&lt;/strong&gt;在整个微服务架构中，最底层的是个推私有部署的 Kubernetes 集群，在集群之上，部署了应用服务。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;个推的应用服务体系共分为三层，最上一层是网关层，接着是业务层，最下面是基础层服务。&lt;/strong&gt;在部署应用服务时，我们使用了 Kubernetes 的命名空间对不同产品线的产品进行隔离。除了应用服务外，Kubernetes 集群上还部署了 Consul 来实现配置的管理、Kube-DNS 实现服务注册与发现，以及一些辅助系统来进行应用和集群的管理。&lt;/p&gt;

&lt;p&gt;下图是个推微服务体系的架构图。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/e18e1b473b6ac8ec5106b294b5394046.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;个推对 API 网关的功能需求主要有以下几方面：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;要支持配置多个产品，为不同的产品提供不同的端口；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;动态路由；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;URI 的重写；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;服务的注册与发现；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;负载均衡；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;安全相关的需求，如 session 校验等；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;流量控制；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;链路追踪；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A/B Testing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在对市面上已有的网关产品进行调研后，我们的技术团队发现，它们并不太适合应用于个推的微服务体系。第一，个推配置的管理都是基于 Consul 实现的，而大部分网关产品都需要基于一些 DB 存储，来进行配置的管理；第二，大部分的网关产品提供的功能比较通用，也比较完善，这同时也降低了配置的复杂度以及灵活性；第三，大部分的网关产品很难直接融入到个推的微服务架构体系中。&lt;/p&gt;

&lt;p&gt;最终，&lt;strong&gt;个推选择使用了 OperResty 和 Lua 进行自研网关&lt;/strong&gt;，在自研的过程中，我们也借鉴了其他网关产品的一些设计，如 Kong 和 Orange 的插件机制等。&lt;/p&gt;

&lt;p&gt;个推的 API 网关的插件设计如下图所示。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/9f0d20ab0a891ca2c84334d2050102db.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;OpenResty 对请求的处理分为多个阶段。&lt;strong&gt;个推 API 网关的插件主要是在 Set、Rewrite、Access、Header_filter、Body_filter、Log 这六个阶段做相应的处理&lt;/strong&gt;，其中，每一个插件都可以在一个或多个阶段起到相应的作用。在一个请求到达 API 网关之后，网关会根据配置为该请求选择插件，然后根据每个插件的规则，进一步过滤出匹配规则的插件，最后对插件进行实例化，对流量进行相应的处理。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/4f5f896ad60f6285bbf79e1cafd8c91c.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;我们可以通过举例来理解这个过程，如上图所示，localhost:8080/api/demo/test/hello 这个请求到达网关后，网关会根据 host 和端口确定产品信息，并提取出 URI(/api/demo/test/hello)，然后根据产品的具体配置，筛选出需要使用的插件——Rewrite_URI、Dyups 和 Auth，接下来根据每个插件的规则配置进行过滤，过滤后，只有 Rewrite_URI 和 Dyups 两个插件被选中。之后实例化这两个插件，在各个阶段对请求进行处理。请求被转发到后端服务时，URI 就被 rewrite 为“/demo/test/hello”，upstream 也被设置为“prod1-svc1”。请求由后端服务处理之后，响应会经网关返回给客户端，这就是整个插件的设计和工作的流程。为了优化性能，我们将插件的实例化延缓到了请求真正开始处理时，在此之前，网关会通过产品配置和规则，过滤掉不需要执行的插件。从图中也可以看出，每个插件的规则配置都很简单，并且没有统一的格式，这也确保了插件配置的简单灵活。&lt;/p&gt;

&lt;p&gt;网关的配置均为热更新，通过 Consul 和 Consul-Template 来实现，配置在 Consul 上进行更新后，Consul-Template 会将其实时地拉取下来，然后通过以下两种方式进行更新。&lt;/p&gt;

&lt;p&gt;（1）通过调用 Update API，将配置更新到 shared-dict 中。&lt;/p&gt;

&lt;p&gt;（2）更新配置文件，利用 Reload OpenResty 实现配置文件的更新。&lt;/p&gt;
&lt;h2 id="个推微服务网关提供的主要功能"&gt;个推微服务网关提供的主要功能&lt;/h2&gt;&lt;h3 id="1.动态路由"&gt;1.动态路由&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;动态路由主要涉及到三个方面：服务注册、服务发现和请求转发。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;如下图所示，服务的注册和发现是基于 Kubernetes 的 Service 和 Kube-DNS 实现的，在 Consul 中，会维持一个服务的映射表，应用的每一个微服务都对应 Kubernetes 上的一个 Service，每创建一个 Service 都会在 Consul 上的服务映射表中添加一项（会被实时更新到网关的共享内存中）。网关每收到一个请求都会从服务映射表中查询到具体的后端服务（即 Kubernetes 中的 Service 名），并进行动态路由。Kube-DNS 可以将 Service 的域名解析成 Kubernetes 内部的 ClusterIP，而 Service 代理了多个 Pod，会将流量均衡地转发到不同的 Pod 上。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/8c47b045ce49b121e5671c48ff32239e.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="2.流量控制"&gt;2.流量控制&lt;/h3&gt;
&lt;p&gt;流量控制主要是通过一个名为“Counter”的后端服务和网关中的流控插件实现的。&lt;strong&gt;Counter 负责存储请求的访问次数和限值，并且支持按时间维度进行计数。&lt;/strong&gt;流控插件负责拦截流量，调用 Counter 的接口进行超限查询，如果 Counter 返回请求超限，网关就会直接拒绝访问，实现限次的功能，再结合时间维度就可以实现限频的需求。同时流控插件通过输出日志信息到 fluent-bit，由 fluent-bit 聚合计次来更新 Counter 中的计数。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/0d46b99b6893793a8500de0ff142416a.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="3.链路追踪"&gt;3.链路追踪&lt;/h3&gt;
&lt;p&gt;整个微服务体系的链路追踪是基于分布式的链路追踪系统 Zipkin 来实现的。通过在网关安装 Zipkin 插件和在后端服务中引入 Zipkin 中间件，实现最终的链路追踪功能。具体架构如下图所示。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/ba53d87b0c547c03a3477b7365875a0d.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="4. A/B测试"&gt;4. A/B 测试&lt;/h3&gt;
&lt;p&gt;在 A/B 测试的实现中，有以下几个关键点：&lt;/p&gt;

&lt;p&gt;（1）所有的策略信息都配置在 Consul 上，并通过 Consul-Template 实时生效到各个微服务的内存中；&lt;/p&gt;

&lt;p&gt;（2）每条策略均有指明，调用一个微服务时应调用 A 还是 B（默认为 A）；&lt;/p&gt;

&lt;p&gt;（3）网关中实现 A/B 插件，在请求到达网关时，通过 A/B 插件配置的规则，即可确定请求适用的 A/B 策略；&lt;/p&gt;

&lt;p&gt;（4）网关会将请求适用的 A/B 策略通过 URL 参数传递下去；&lt;/p&gt;

&lt;p&gt;（5）每个微服务通过传递下来的策略，选择正确的服务进行访问。&lt;/p&gt;

&lt;p&gt;下图给出了两种场景下的调用链路。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/e28c13cef866d624ffb59365ac009f07.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就是个推微服务网关的设计和主要功能的实现。之后，个推的技术团队会不断提升 API 网关的弹性设计，使其能够在故障出现时，缩小故障的影响范围；同时，我们也会继续将网关与 DevOps 平台做进一步地结合，以确保网关在迭代更新时，能够有更多的自动化测试来保证质量，&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Tue, 05 Mar 2019 15:26:28 +0800</pubDate>
      <link>https://ruby-china.org/topics/38196</link>
      <guid>https://ruby-china.org/topics/38196</guid>
    </item>
    <item>
      <title>NB-IoT 的 “前世今生”</title>
      <description>&lt;p&gt;&lt;em&gt;作者：个推 B2D 研发工程师 海晏&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;根据《爱立信 2018 移动报告》（Ericsson Mobility Report,June 2018）的预测，蜂窝物联网设备连接数将在 2023 年达到 35 亿，年增长率达到 30%。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/6c766bd1215612cf3b251a05ba31ccb9.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图片来源：《爱立信 2018 移动报告》（Ericsson Mobility Report,June 2018）&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;报告中还强调了中国 IoT 产业的迅猛发展对上述数据产生的巨大影响：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/470a494aa8fd65414693368df8a567b0.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;文字节选于《爱立信 2018 移动报告》（Ericsson Mobility Report,June 2018）&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;IoT(Internet of Things，即所谓的物联网) 可能早已为大家耳熟，无论是能打电话、看视频的智能手表，还是只有信用卡大小的树莓派，其实都是 IoT 设备。而在诸多的 IoT 技术之中，NB-IoT 无疑是目前国内最受关注的一个，前有三大运营商积极建设基站、狂砸补贴，后有 ofo、摩拜创新研发 NB-IoT 智能锁。&lt;/p&gt;
&lt;h2 id="NB-IoT是何方神圣？"&gt;NB-IoT 是何方神圣？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;NB-IoT 全称是 NarrowBand IoT，也称窄带物联网，是由 3GPP 组织开发的为大范围蜂窝网与设备服务的低功耗广域网络（LPWAN）广播技术&lt;/strong&gt;，其规范在 3GPP Release 13 中完全定型，并在 Rel-14 中做了部分增强（Rel-13 主要是对 LTE 的改进，并制定了一些物联网规范）。&lt;/p&gt;

&lt;p&gt;其实在 NB-IoT 之前已经有了其他 IoT 技术的存在，曾几何时，我们也使用 2/3G 网络连接物联网设备，虽然现在看来 2/3G 网络频谱效率、吞吐率等不高，但是对于数据量极小（相对 PC、智能手机以及可以打电话、看视频的智能手表来说）的物联网设备来说已经足够了。&lt;/p&gt;

&lt;p&gt;随着时间的推移，人们对物联网设备的需求与落后的技术之间的矛盾越来越大：一方面考虑到 IoT 设备的数量，大量使用 GPRS 模块成本偏高；另一方面低电池容量的 IoT 设备也与 GPRS 的相对复杂的通信协议天生难以相容。此外，考虑到 GPRS 即将被“退群”，人民群众对新的物联网通信解决方案的需求也日渐高涨。&lt;/p&gt;
&lt;h2 id="通信大厂之间的技术角逐"&gt;通信大厂之间的技术角逐&lt;/h2&gt;
&lt;p&gt;窥见这一技术空白的各通信大厂自然明争暗斗，各种标准不断被推出。2014 年 5 月华为、Vodafone 提出了 NB M2M 技术，而后又进化成 NB-CIoT，2015 年 7 月，Nokia、Ericsson、Intel 提出了 NB-LTE 技术，随后 3GPP 在上述两者之上着手制定标准，并在 2016 年 7 月确定标准（在上文提及的 Release13 中），至此 NB-IoT 正式诞生。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;出生于 4G 时代的 NB-IoT 复用了一系列 LTE 的设计，并与 GSM、GPRS、LTE 等技术良好兼容，它上下行共同使用 180 kHz 的最小系统带宽&lt;/strong&gt;，因此 GSM 运营商可以重耕 GSM 频段，将某一个 GSM 载波频段用于 NB-IoT（带宽 200 kHz，可以放一个 NB-IoT 总带宽加两个 10 kHz 的保护带），而 LTE 运营商则可以划分一个 PRB（Physical Resource Blocks）给 NB-IoT（具体部署方式有 stand-alone、in-band、guard-band 三种）。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/6d79a2c1c941ee25d207d94b1cb11686.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NB-IoT 与 LTE 的关系，该图来自《A Primer on 3GPP Narrowband Internet of Things (NB-IoT)》&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;为了降低 NB-IoT 模块的成本与能耗，NB-IoT 在复用的同时也对 LTE 物理信道、UE（User Equipment，终端）处理流程、模块结构做了许多精简，减轻了设备制造复杂度，在降低成本的同时也减少了能耗。此外，NB-IoT 的 PSM（powersaving mode）和 eDRX（echanced Discontinuous Reception）模式也是降低能耗的“秘密武器”，在 PSM 模式下，设备进入休眠状态不进行通信活动，该模式一般适用于通信频率低的设备；而在 eDRX 模式下，UE 可以更快地进入接收模式，而不需要从休眠模式转换到激活模式，相比 DRX 模式接受间隔更长，在较高频率通信的设备上可以减少信令，更加省电。&lt;/p&gt;

&lt;p&gt;为了增强覆盖率，NB-IoT 增加重传减低数据速率，引入单个子载波 NPUSCH（Narrowband Physical Uplink SharedChannel，窄带物理上行链路共享信道）传输和π/2-BPSK 调制来维持接近 0dB PAPR（Peak to Average Power Ratio，峰值平均比例）来减小 PA（power amplifier，功率放大器）功率回退造成对覆盖的影响，一般来说，NB-IoT 的 MCL（maximumcoupling loss，最大耦合损耗）比 LTE（Rel-12）高 20db。&lt;/p&gt;

&lt;p&gt;当然，上述这些也意味着 NB-IoT 的传输速率不高。但是正是基于这种通信模型，NB-IoT 才得以在低成本的情况下，做到单个小区仅使用一个 PRB 支持上万台设备，而且 NB-IoT 本就不是为高频高实时需求的设备设计的，它的主要用途正是在低功耗、低成本、高覆盖、低移动性的设备上（关于低移动性这一点上，Rel-13 中不支持漫游，Rel-14 对移动性进行了增强，目前已经有厂商在进行漫游试验了，期待 NB-IoT 未来在这方面有所发展）。&lt;/p&gt;
&lt;h2 id="NB-IoT发展的“内忧”和“外患”"&gt;NB-IoT 发展的“内忧”和“外患”&lt;/h2&gt;
&lt;p&gt;虽然有各通信大厂的背书，但是 NB-IoT 在国内的发展却并不是一路绿灯。一方面，模组成本没有下降到预期的水平，智慧农业、智慧城市等对 NB-IoT 兴趣不足，使得模组出货量达不到预期，没有形成规模效应；另一方面，由于 NB-IoT 技术刚刚兴起，其相关的基础设施、产业链尚未完善，缺乏专业人才支持，也使得其制造成本难以快速压缩。&lt;/p&gt;

&lt;p&gt;此外，作为一种蜂窝网络技术，NB-IoT 的发展无疑需要运营商的支持。在国内三大运营商之中，中国电信无疑是表现最好的选手，目前中国电信在国内 NB-IoT 基站已达 40 万，已经实现“城乡全覆盖”，并且还有对外开放的 Wing 中国电信物联网开放平台，帮助开发者管理终端。相比之下，虽然中国联通声音较小，但也在去年 5 月宣布完成了 30 万个 NB-IoT 基站升级工作，并与阿里、腾讯、百度等三十多家战略合作伙伴成立了中国联通物联网产业联盟。与前两者相比，中国移动的位置则略显尴尬。由于中国移动之前并没有 LTE FDD 牌照，这意味着它要么在已分配的 GSM 频段上部署 NB-IoT，要么等待 NB-IoT 支持 TDD，要么等待 FDD-LTE 的牌照，不管从时间还是从成本上来说，中国移动都不占优势。不过好消息是，中国移动在去年 4 月份终于获得了 FDD-LTE 牌照，接下来将会大力发展建设物联网，努力追上其他运营商的步伐，同时对外开放 OneNET——中国移动物联网开放平台。&lt;/p&gt;

&lt;p&gt;虽然三大运营商的动作如此迅速，但是考虑到部分 NB-IoT 终端的工作环境，&lt;strong&gt;目前的 NB-IoT 网络覆盖率仍然不够。相对较低基站的数量与覆盖率也迫使终端必须抬高发射功率，导致超长电池寿命大打折扣。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;运营商的角力对于消费者和厂家来说是一件好事，既能降低资费，又能多一些选择，提升消费体验，但是对于开发者来说绝非如此了：多个管理平台意味着必须考虑多种设备多种 SDK 的情况，有时候同一种设备同一种协议上层的 API 却不相同。当发生这种情况时，开发人员需要耗费更多的精力和时间。&lt;/p&gt;

&lt;p&gt;除了需要面对“内忧”，NB-IoT 的“外患”也不少。当姗姗来迟的 NB-IoT 到来之时，LoRa 和 Sigfox 技术已经攻占了大片城池：目前，有一百多个国家被 LoRa 网络覆盖，其中法国电信 Orange 已经宣布实现全法覆盖 LoRa。而我国的北京、上海、广州、深圳、南京、苏州、杭州等地也有 LoRa 网络的覆盖，其中北京市六环以内已经实现了全覆盖，京杭大运河江苏段也实现全段覆盖；Sigfox 在欧美地区同样兵强马壮，其在法国的覆盖率已达 92%，在美国也已经覆盖一百多个城市。除此之外，NB-IoT 也并不是物联网的唯一选择，有时候甚至不是最好的选择。比如能打电话、看视频的智能手表就更适用于使用 LTE Cat M1 甚至 LTE Cat 1，而信用卡大小的的树莓派也未必会上 NB-IoT，而更可能选择 LoRa（毕竟企业自主组网更 geek，还无需担忧网络覆盖与资费的问题）。&lt;/p&gt;
&lt;h2 id="NB-IoT技术未来大有作为"&gt;NB-IoT 技术未来大有作为&lt;/h2&gt;
&lt;p&gt;那么刚出襁褓的 NB-IoT 该如何应对呢？其实不同的技术有不同的适用场景，比如 NB-IoT 简直就是为水表、电表行业量身打造的。通过 NB-IoT 模组，设备仅需在每天汇报数据时唤醒并与基站进行通信，其他大部分时间几乎没有电能消耗，在这种场景中，NB-IoT 可以在保证电池寿命的前提下，妥善完成监控电量、水量的任务。切换到烟雾传感器、火警报警器等类似的场景，NB-IoT 的优势也十分明显，而对于水文、空气监控来说，NB-IoT 模组可以大部分时间休眠，当需要进行较实时的汇报与反馈时，eDRX 与 DRX 模式的作用就显现了，工作在此模式时可以即时反馈变化情况，当不需要实时监控时，又可以切换回 PSM，省电工作两不误。&lt;/p&gt;

&lt;p&gt;除了上面的这些场景，智慧停车、智能门锁、路灯故障监控、二轮车标识定位、农牧业监控、资产管理等领域，NB-IoT 也是大有作为。工信部也在去年发布了关于推进移动物联网（NB-IoT）建设发展的通知，要求加快推进移动物联网部署，构建 NB-IoT 网络基础设施。目前各种 IoT 技术百花齐放，在市场与政策的双重扶持下，NB-IoT 未来的发展前景一片光明。&lt;/p&gt;
&lt;h2 id="参考文献："&gt;参考文献：&lt;/h2&gt;
&lt;p&gt;[1]Y.-P.Eric Wang, Xingqin Lin, Ansuman Adhikary, Asbjörn Grövlen, Yutao Sui, YufeiBlankenship, Johan Bergman, and Hazhir S. Razaghi，Ericsson Research, Ericsson AB,”A Primer on3GPP Narrowband Internet of Things (NB-IoT)”&lt;/p&gt;

&lt;p&gt;[2]Ericsson,”EricssonMobility Report”,June 2016.&lt;/p&gt;

&lt;p&gt;[3]A. Adhikary, X. Lin and Y.-P. E. Wang, “Performanceevaluation of NB-IoT coverage,” Submitted to IEEE Veh. Technol. Conf.(VTC),September 2016, Montréal, Canada.&lt;/p&gt;

&lt;p&gt;[4]TR 36.888 v12.0.0, “Study on provision of low-costmachine-type communications (MTC) user equipments (UEs) based on LTE,”Jun.2013.&lt;/p&gt;

&lt;p&gt;[5]Ericsson, “New WI proposal for L1/L2 eMTC and NB-IoT enhancements,” RP-160878, 3GPP TSG RAN Meeting #72, June2016.&lt;/p&gt;

&lt;p&gt;[6]Vodafone, Huawei, and HiSilicon, “NB-IoT enhancementsWork Itemproposal,” RP-160813, 3GPP TSG RAN Meeting #72, June 2016.&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Wed, 27 Feb 2019 16:46:36 +0800</pubDate>
      <link>https://ruby-china.org/topics/38161</link>
      <guid>https://ruby-china.org/topics/38161</guid>
    </item>
    <item>
      <title>个推基于 Consul 的配置管理</title>
      <description>&lt;p&gt;&lt;em&gt;作者：个推应用平台基础架构高级研发工程师 阿飞&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;在微服务架构体系中，由于微服务众多，服务之间又有互相调用关系，因此，一个通用的分布式配置管理是必不可少的。一般来说，配置管理需要解决配置集中管理、在系统运行期间可实现动态配置、配置修改后支持自动刷新等问题。&lt;/p&gt;

&lt;p&gt;在大多数微服务体系中，都会有一个名为配置文件的功能模块来提供统一的分布式配置管理。构建配置中心，统一对应用中各个微服务进行管理，对微服务体系的意义重大。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consul 为什么适合做配置管理&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consul 作为轻量级的分布式 K/V 存储系统，搭建方便，可用性高，并且支持多数据中心，提供 Web UI 进行 K/V 管理。此外 Consul 还可以结合 Consul-Template 或者在代码中引入 Consul Client 的相关依赖创建 Watcher 来实时 Watch K/V 的变化，是配置管理的不二之选。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/50ac14b87794f889bca7c4caf5e4475b.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;下图为个推微服务体系基于 Consul 配置管理的整体设计。其中，CCenter 就是在 Consul 的基础上进行二次开发的配置中心。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/7a2c8950b5bc04cb38e0153419930601.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;微服务体系下配置的分类和组织形式&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在实践中，不同产品线的配置会放置在 Consul 的不同路径下，实现不同产品线配置之间的隔离。&lt;/p&gt;

&lt;p&gt;按照配置的用途，可将同一产品线下的配置分为三类：&lt;/p&gt;

&lt;p&gt;1.API 网关相关配置；&lt;/p&gt;

&lt;p&gt;2.服务注册与发现相关配置；&lt;/p&gt;

&lt;p&gt;3.应用相关配置。&lt;/p&gt;

&lt;p&gt;其中，每类配置会对应 Consul 上的不同目录。&lt;/p&gt;

&lt;p&gt;按照配置的变化特性，可将配置分为两类：&lt;/p&gt;

&lt;p&gt;1.环境相关的全局配置&lt;/p&gt;

&lt;p&gt;如 MySQL 等外部依赖相关的配置和其他与环境相关的配置，这类配置在开发测试生产环境中存在差异，需要为不同环境配置不同的值。
2.应用本身的配置&lt;/p&gt;

&lt;p&gt;一般为不经常性发生变化、可动态调整、开关的配置。这类配置比较稳定，在初始化后，只有在需要时才会改动，通常会设置默认值。这两类配置在 Consul 上会放在不同的子目录下。这样 QA、运维只需要关注环境差异部分即可。&lt;/p&gt;

&lt;p&gt;基于以上对配置的分类，最终 Consul 上的 Key 的格式如下：&lt;/p&gt;

&lt;p&gt;/ProductLine_Prefix/Usage_Prefix/Environmental_Correlation_Prefix/Config_Item_Path&lt;/p&gt;

&lt;p&gt;其中，&lt;/p&gt;

&lt;p&gt;ProductLine_Prefix：用来隔离不同产品线的配置；&lt;/p&gt;

&lt;p&gt;Usage_Prefix：用来区分配置的用途；&lt;/p&gt;

&lt;p&gt;Environmental_Correlation_Prefix：用来分隔与环境相关的配置；&lt;/p&gt;

&lt;p&gt;Config_Item_Path：具体的配置项。&lt;/p&gt;

&lt;p&gt;配置在 Consul 上的组织形式有以下两种：&lt;/p&gt;

&lt;p&gt;1.以配置文件的形式组织，Consul 上的一个 K/V，对应一个配置文件，如 nginx 的配置文件。&lt;/p&gt;

&lt;p&gt;2.以配置项的形式组织，将配置文件模板化，拆成一个个的配置项，每个配置项对应 Consul 上的一个 K/V，多个配置项对应一个配置文件。大部分配置文件本身都是以 K/V 的形式组织的，均适合模板化，模板化后即可以按照配置项的特性，在 Consul 上分成不同的类别进行管理。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;如何实现配置更新&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consul 上的 K/V，要如何生成可加载的应用，或可使用的配置呢？&lt;/p&gt;

&lt;p&gt;1.用 Node 和 Lua 实现的微服务的配置更新，使用 Consul-Template 来实现；&lt;/p&gt;

&lt;p&gt;2.用 Java 实现的微服务的配置更新，通过 Consul-Template 工具（需要重启应用）和在代码中引入 Consul Client 的依赖创建 Watcher（热更新）这两种方式来实现。&lt;/p&gt;

&lt;p&gt;Consul-Template 如何使用？&lt;/p&gt;

&lt;p&gt;Consul-Template 是一个后台进程，它可以根据 Watch Consul 上 K/V 的变化，更新任意数量的模板，同时生成对应的文件，之后还可以运行任意的命令。要使用 Consul-Template 一般需要定义两个文件：&lt;/p&gt;

&lt;p&gt;1.模板文件&lt;/p&gt;

&lt;p&gt;模板文件一般按照 Go Template 的格式进行编写，示例如下：&lt;/p&gt;

&lt;p&gt;config-tree.ctmpl:&lt;/p&gt;

&lt;p&gt;{{ tree /consul/path/to/configFiles | explode | toJSONPretty }}&lt;/p&gt;

&lt;p&gt;该模板在 /consul/path/to/configFiles 路径下的配置发生变化时，会渲染出一个 Json 格式的字符串，其中包含了 /consul/path/to/configFiles 下所有的 K/V.&lt;/p&gt;

&lt;p&gt;config-kv.ctmpl:&lt;/p&gt;

&lt;p&gt;return {
       host='{{ printf "%s/mysql/host" (env "CONSUL_CONFIG_PREFIX") | key }}',
       port={{ keyOrDefault (printf "%s/mysql/port" (env "CON-SUL_CONFIG_PREFIX"))  "3306" }},
       user='{{ printf "%s/mysql/user" (env "CONSUL_CONFIG_PREFIX") | key }}',
       password='{{ printf "%s/mysql/password" (env "CON-SUL_CONFIG_PREFIX") | key }}'
    }&lt;/p&gt;

&lt;p&gt;该模板是按照配置项来渲染的，在该模板中使用了 Consul-Template 定义两个方法 key 和 keyOrDefault。其中，key 会在 Consul 上对应的 K/V 创建后，再进行渲染模板；keyOrDefault 则会在 Consul 上没有对应的 K/V 时，使用默认值代替。&lt;/p&gt;

&lt;p&gt;模板中还使用了 " CONSUL_CONFIG_PREFIX " 这个环境变量，这样，不同的产品线便可以使用同一个模板文件，只需要修改" CONSUL_CONFIG_PREFIX "这个环境变量的值即可。&lt;/p&gt;

&lt;p&gt;2.配置文件&lt;/p&gt;

&lt;p&gt;配置文件是按照 HashiCorp Configuration Language (HCL) 编写的，示例如下：&lt;/p&gt;

&lt;p&gt;template {
         source = "config-tree.ctmpl",
         destination = "config-tree.json",
         command  = "sh updateAndReload.sh config-tree.json ”
        }&lt;/p&gt;

&lt;p&gt;template {
         source = "config-kv.ctmpl",
         destination = "config-kv.lua",
         command  = "sh updateAndReload.sh config-kv.lua ”
        }&lt;/p&gt;

&lt;p&gt;该配置文件的作用是使用" source"指定的两个模板文件进行渲染，将渲染的结果分别保存在" destination"指定的文件中，保存成功后，分别运行" command"指定的命令来更新并加载配置文件。&lt;/p&gt;

&lt;p&gt;配置的更新方式&lt;/p&gt;

&lt;p&gt;在个推的微服务体系中，配置的更新方式有两种：&lt;/p&gt;

&lt;p&gt;1.替换配置文件，reload 服务
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/559114b510157616fd1e9e4b7a26be6a.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;2.调用服务接口直接更新内存中的配置&lt;/p&gt;

&lt;p&gt;而在 Java 实现的微服务中，热更新配置通常是在代码中引入 Consul Client 的依赖，在应用启动时，会初始化一个 Watcher 来监听 Consul 上对应目录下 K/V 的变化，相关的 K/V 发生变化时，Watcher 会负责将其拉取下来，然后调用相关的代码进行配置的更新。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/060fce0726240891059ae0f2621891b2.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;基于 Consul 的二次开发-CCenter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;配置中心 CCenter 在 Consul 上提供了更友好的 WEB UI，并且增加版本控制，每次配置的更新都会生成一个版本，在应用版本后，配置才真正生效，可以更加方便地进行配置版本间的差异比较，应用任意版本的配置。
&lt;img src="https://diycode.b0.upaiyun.com/photo/2019/e46f05e651bf734077d86ebc0f155aa9.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;以上就是个推在微服务实践中，基于 Consul 实现的一套配置管理的方案，作为轻量级的分布式 K/V 存储系统，Consul 非常适合用于配置管理，可以帮助开发者们方便、快速地搭建配置中心，结合 Consul-Template 则可以方便地实现配置的实时更新，在 Consul 的基础上进行二次开发，实现了配置版本的有效控制，对微服务的配置管理起到了良好的辅助作用。&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Mon, 25 Feb 2019 13:56:01 +0800</pubDate>
      <link>https://ruby-china.org/topics/38146</link>
      <guid>https://ruby-china.org/topics/38146</guid>
    </item>
    <item>
      <title>Node.js 微服务实践：基于容器的一站式命令行工具链</title>
      <description>&lt;h2 id="背景与摘要"&gt;背景与摘要&lt;/h2&gt;
&lt;p&gt;由于工程数量的快速增长，个推在实践基于 Node.js 的微服务开发的过程中，遇到了如下问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;每次新建项目都需要安装一次依赖，这些依赖之间基本相似却又有微妙的区别；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;每次新建项目都要配置一遍相似的配置（比如 tsconfig、lint 规则等）；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;本地 Mac 环境与线上 Docker 内的 Linux 环境不一致（尤其是有 C++ 依赖的情况）。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;为了解决上述问题，个推内部开发了一个命令行小工具来标准化项目初始化流程、简化配置甚至是零配置，提供基于 Docker 的一致构建、运行环境。&lt;/p&gt;
&lt;h2 id="CLI: init, build, test &amp;amp; pack"&gt;CLI: init, build, test &amp;amp; pack&lt;/h2&gt;
&lt;p&gt;新建一个 Node.js 项目的时候，我们一般会：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;安装许多开发依赖：TypeScript、Jest、TSLint、benchmark、typedoc 等；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;配置 tsconfig、lint 规则、.prettierrc 等；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;安装众多项目依赖：koa、lodash、sequelize、ioredis、zipkin、node-fetch 等；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;初始化目录结构；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;配置 CI 脚本。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通常，我们会选择复制一个现成的项目进行修改，导致出现众多看似相似却又不完全相同的项目，比如十个项目可能会对应十种配置组合。对于同时跨多个工程的开发人员来说，众多配置组合会增加他们的工作难度。而且，当安全审计发现某些 npm package 出现安全隐患时，开发人员则需要对每个引用这些包的项目逐一检查和修正。&lt;/p&gt;

&lt;p&gt;在确定的开发场景下，几乎所有项目的开发依赖都差不多，开发配置也非常相似，因此我们基于 commander.js 写了一个 init 工具，它会开个命令行的向导，自动安装依赖、初始化项目目录结构和配置。从而创建项目，并按照场景将所有配置收缩为特定几种模板，进行统一处理。&lt;/p&gt;

&lt;p&gt;随后，我们有了 build、test、pack 命令，托管了 tsconfig、jest 配置、打包配置，自动调用 tsc 编译，构建测试环境，然后调用 Jest 进行测试，进行标准化打包，CI 脚本基本可以简化为几行标准脚本。&lt;/p&gt;
&lt;h2 id="CLI: Docker Build"&gt;CLI: Docker Build&lt;/h2&gt;
&lt;p&gt;在介绍这个命令前需要先简单了解一下个推的镜像体系：&lt;/p&gt;

&lt;p&gt;前面提到我们将大部分依赖封装到了一个 npm 包，这一层封装也反映在个推的 Docker 镜像体系内，可以简单表述为下面的 Dockerfile：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 公共依赖层的 Dockerfile&lt;/span&gt;
&lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;mkdir&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="sr"&gt;/usr/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webnode&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;node_modules&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="sr"&gt;/usr/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webnode&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;webnode&lt;/span&gt;
&lt;span class="no"&gt;ENV&lt;/span&gt; &lt;span class="no"&gt;NODE_PATH&lt;/span&gt; &lt;span class="sr"&gt;/usr/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webnode&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;node_modules&lt;/span&gt;
&lt;span class="c1"&gt;# 项目的 Dockerfile&lt;/span&gt;
&lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;getui&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webnode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;/&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当把这层依赖直接做进 Docker 镜像时，虽然每个镜像的 SIZE 还是 1G 多，但是每个镜像的 UNIQUE SIZE 都是极小的，仅有数 M 的差分层。&lt;/p&gt;

&lt;p&gt;一个简单的对比，比如有 800M 公共系统依赖 + 每个服务平均 200M 的 npm 依赖 + 1M 的服务代码，那么由于原先每个服务都会 npm install 大量重复依赖，20 个服务，就会有 800M + 200M * 20 + 1M * 20 = 4.82G 的总 UNIQUE SIZE。而采用依赖分层共享，则仅有 800M + 200M + 1M * 20 = 1.02G 的总 UNIQUE SIZE。在考虑应用的多版本之后，依赖分层共享带来在存储上的优势会更加明显。&lt;/p&gt;

&lt;p&gt;我们以一定的依赖锁定周期和控制为代价，换取了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;减少依赖组合、依赖版本组合的可能性，开发者选择包的简化、初始化项目的简化；审计简化、安全更新简化。&lt;/li&gt;
&lt;li&gt;CI 显著提速，节省等待时间。&lt;/li&gt;
&lt;li&gt;传输和存储的压力减少许多。&lt;/li&gt;
&lt;li&gt;公共依赖被多个项目使用，得到了更加充分的测试。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;webnode docker build 命令可以帮助简化 Docker image 的构建过程，它内置了一个 Dockerfile 和 dockerignore，该命令运行时，会基于这两个文件和当前的 Context，自动构建 docker 镜像。其中 Dockerfile 内含一些优化和我们的最佳实践，开发人员只需要专注 Node.js 的项目的开发，这个命令则可以负责配置文件权限等操作以及生成标准化的、优化的 Docker 镜像。&lt;/p&gt;

&lt;p&gt;其设计目标是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;快：合理的依赖分层，最大程度应用 Docker 缓存机制，通过 .dockerignore 裁剪不必要的 Context，因此可以实现飞快的构建速度。&lt;/li&gt;
&lt;li&gt;小：依据变更频度做 Docker 分层设计、应用 multi-stage build，尽最大可能缩小一个镜像的 UNIQUE SIZE。&lt;/li&gt;
&lt;li&gt;可重现：同样的内容总是构建出相同的结果。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以 node_modules 依赖优化为例，下面两种 Dockerfile 其实会有很大的区别：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;getui&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webnode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;.&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt;

&lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;getui&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webnode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;/&lt;/span&gt;
&lt;span class="no"&gt;RUN&lt;/span&gt; &lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt;
&lt;span class="no"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前者，每次 docker build 时，只要项目内任何代码变了，npm install 的缓存都会失效，需要重新安装，而后者仅当 package*.json 发生改变之时才会触发重新 npm install。另外，我们还会对 package.json 进行预编译，仅保留依赖相关的字段，避免出现修改 package.json 的版本号就重新 npm install 的情况。&lt;/p&gt;

&lt;p&gt;webnode docker build 不仅可以帮助开发者进行统一化的镜像构建、统一实践最佳优化，节约资源，还能避免所有开发人员都需要接触优化细节，省时省力。&lt;/p&gt;
&lt;h2 id="CLI: Webnode Docker Start"&gt;CLI: Webnode Docker Start&lt;/h2&gt;
&lt;p&gt;在本地调试开发的过程中，我们遇到了一些环境差异引起的问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;生产环境与本地开发环境 Node.js 版本不一致。&lt;/li&gt;
&lt;li&gt;一些含有 C++ 代码的 npm 依赖运行的跨平台问题。&lt;/li&gt;
&lt;li&gt;文件权限配置、系统目录结构与线上运行环境不完全一致。&lt;/li&gt;
&lt;li&gt;启动初始化流程不一致（比如配置预拉取）。&lt;/li&gt;
&lt;li&gt;开发本地常常缺少一些二进制工具或版本不一致（比如 consul-template、nc 等）。
与本地直接启动 Node.js 程序有所不同，这个命令会优先基于当前项目利用上面的 webnode docker build 命令构建 Docker 镜像，然后启动镜像。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker 可以帮助消解环境差异：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;便捷地携带与生产环境一致的 Node.js 版本以及其他二进制依赖。&lt;/li&gt;
&lt;li&gt;一致的初始化流程。&lt;/li&gt;
&lt;li&gt;轻松运行含有 C++ 的 npm 依赖。&lt;/li&gt;
&lt;li&gt;文件权限、目录结构与线上运行环境一致。
容器化的 Node.js 调试方法有些许变化，需要暴露 Node.js 的 Inspector 端口，然后配一下 Visual Studio Code 的 localRoot 和 remoteRoot：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;WEBNODE_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;WEBNODE_HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;WEBNODE_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;WEBNODE_PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;DOCKER_RUN_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$DOCKER_RUN_OPTIONS &lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;
   -it \
   --rm \
   --network=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;getui-dev&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;
   -p $WEBNODE_HOST:$WEBNODE_PORT:3000 \
   -p 127.0.0.1:9229:9229 \
   -e NODE_FLAGS=--inspect=0.0.0.0:9229 \
   --name $CONTAINER"&lt;/span&gt;
&lt;span class="n"&gt;docker&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
   &lt;span class="vg"&gt;$DOCKER_RUN_OPTIONS&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
   &lt;span class="vg"&gt;$DOCKER_IMAGE_TAG&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="s2"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"0.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s2"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"attach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Attach Local WebNode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9229&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"restart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"inspector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"localRoot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"${workspaceFolder}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"remoteRoot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_REMOTE_ROOT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"sourceMaps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
       &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="基于容器开发 CLI 工具"&gt;基于容器开发 CLI 工具&lt;/h2&gt;
&lt;p&gt;基于容器的开发可以带来诸多好处。一是便于分发，基于 Docker 的 Tag，开发者可以很方便地做基于小版本、大版本、分支的分发，可以像 nvm 一样去切换版本。&lt;/p&gt;

&lt;p&gt;二是 CLI 脚本不用处处考虑跨平台兼容的问题，比如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sed 在 Linux 和 Mac 下工作行为不一致的问题之类的。&lt;/li&gt;
&lt;li&gt;有的环境有 Python 3 有的环境只有 Python 2
所有的依赖通过容器带进来，简洁而高效。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在基于 Docker 的工具开发的过程中，我们也遇到一些问题：&lt;/p&gt;

&lt;p&gt;一是容器内外 UID/GID 不一致，如果是以非 ROOT 用户运行 docker run，会导致容器内程序在挂载的目录产生的文件权限与当前用户不一致。&lt;/p&gt;

&lt;p&gt;Docker for Mac 对于文件权限有一些特别的行为，具体可以参见：&lt;a href="https://docs.docker.com/docker-for-mac/osxfs/#ownership" rel="nofollow" target="_blank"&gt;https://docs.docker.com/docker-for-mac/osxfs/#ownership&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;对于 Host 是 Linux 的情况，尤其在 CI 时，需要考虑 UID/GID 的问题。对于这种情况，我们选择覆盖掉了 entrypoint，然后用 gosu 去做降权来处理。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;CLI_EXEC_UID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;CLI_EXEC_UID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;CLI_EXEC_GID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;CLI_EXEC_GID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="n"&gt;gosu&lt;/span&gt; &lt;span class="vg"&gt;$CLI_EXEC_UID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="vg"&gt;$CLI_EXEC_GID&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="s2"&gt;"$@"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实 RedHat 旗下用于设计 container runtime 的 daemonless（例如 podman），就很适合做 CLI 工具，可以 rootless 运行，又尊重系统的权限配置。然而其目前尚未成熟，业界采用率也不高，仍需要继续观望。&lt;/p&gt;

&lt;p&gt;二是有时候 docker run 速度较慢，个推的解决方案是在首次启动时启动一个 docker run --detach，然后后续的 CLI 执行完全通过 docker exec 来进行，这样避免掉了每次执行命令时启动的开销，速度提升明显。&lt;/p&gt;
&lt;h2 id="小结"&gt;小结&lt;/h2&gt;
&lt;p&gt;以上便是个推 Node.js 微服务开发实践中关于 CLI 工具的实践，个推试图标准化、优化项目结构以及镜像构建，减少组合的可能性，有效降低了存储、传输、构建的成本，让开发人员更加省时省力。&lt;/p&gt;

&lt;p&gt;后续我们还会继续为大家介绍个推的 Docker 镜像体系设计以及 Node.js 微服务开发框架，敬请期待。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.docker.com/docker-for-mac/osxfs/#ownership" rel="nofollow" target="_blank"&gt;https://docs.docker.com/docker-for-mac/osxfs/#ownership&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#entrypoint" rel="nofollow" target="_blank"&gt;https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#entrypoint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectatomic.io/blog/2018/02/reintroduction-podman/" rel="nofollow" target="_blank"&gt;https://www.projectatomic.io/blog/2018/02/reintroduction-podman/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.slideshare.net/AkihiroSuda/the-state-of-rootless-containers" rel="nofollow" target="_blank"&gt;https://www.slideshare.net/AkihiroSuda/the-state-of-rootless-containers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.debian.org/doc/manuals/debian-faq/ch-pkg_basics.en.html#s-virtual" rel="nofollow" target="_blank"&gt;https://www.debian.org/doc/manuals/debian-faq/ch-pkg_basics.en.html#s-virtual&lt;/a&gt;&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Wed, 20 Feb 2019 15:50:34 +0800</pubDate>
      <link>https://ruby-china.org/topics/38124</link>
      <guid>https://ruby-china.org/topics/38124</guid>
    </item>
    <item>
      <title>TensorFlow 分布式实践</title>
      <description>&lt;p&gt;大数据时代，基于单机的建模很难满足企业不断增长的数据量级的需求，开发者需要使用分布式的开发方式，在集群上进行建模。而单机和分布式的开发代码有一定的区别，本文就将为开发者们介绍，基于 TensorFlow 进行分布式开发的两种方式，帮助开发者在实践的过程中，更好地选择模块的开发方向。&lt;/p&gt;
&lt;h2 id="基于TensorFlow原生的分布式开发"&gt;基于 TensorFlow 原生的分布式开发&lt;/h2&gt;
&lt;p&gt;分布式开发会涉及到更新梯度的方式，有同步和异步的两个方案，同步更新的方式在模型的表现上能更快地进行收敛，而异步更新时，迭代的速度则会更加快。两种更新方式的图示如下：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/5728d684-1d28-4dc7-923d-c64b7c25d8af.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;同步更新流程&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;（图片来源：TensorFlow:Large-Scale Machine Learning on Heterogeneous Distributed Systems）&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/c0e8bcf6-7a77-4cae-8e6d-804bc1272b26.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;异步更新流程&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;（图片来源：TensorFlow:Large-Scale Machine Learning on Heterogeneous Distributed Systems）&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;TensorFlow 是基于 ps、work 两种服务器进行分布式的开发。ps 服务器可以只用于参数的汇总更新，让各个 work 进行梯度的计算。&lt;/p&gt;
&lt;h3 id="基于TensorFlow原生的分布式开发的具体流程如下："&gt;基于 TensorFlow 原生的分布式开发的具体流程如下：&lt;/h3&gt;
&lt;p&gt;首先指定 ps 服务器启动参数 –job_name=ps:&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="n"&gt;distribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;ps_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2222&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;worker_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.253&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2225&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ps&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着指定 work 服务器参数 (启动两个 work 节点) –job_name=work2:&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="n"&gt;distribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;ps_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2222&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;worker_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.253&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2225&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="n"&gt;distribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;ps_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2222&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;worker_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;100.253&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2225&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，上述指定的参数 worker_hosts ps_hosts job_name task_index 都需要在 py 文件中接受使用：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DEFINE_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker_hosts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;默认值&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;描述说明&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收参数后，需要分别注册 ps、work，使他们各司其职：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ps_hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ps_hosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;worker_hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker_hosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClusterSpec&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ps_hosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;worker_hosts&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;issync&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;issync&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replica_device_setter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                   &lt;span class="n"&gt;worker_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/job:worker/task:%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继而更新梯度。&lt;/p&gt;

&lt;p&gt;（1）同步更新梯度：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rep_op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SyncReplicasOptimizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                               &lt;span class="n"&gt;replicas_to_aggregate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_hosts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                               &lt;span class="n"&gt;replica_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                               &lt;span class="n"&gt;total_num_replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_hosts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                               &lt;span class="n"&gt;use_locking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;train_op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep_op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_gradients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grads_and_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;global_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;global_step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;init_token_op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep_op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_init_tokens_op&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;chief_queue_runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep_op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_chief_queue_runner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）异步更新梯度：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;train_op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_gradients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grads_and_vars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;global_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;global_step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，使用 tf.train.Supervisor 进行真的迭代&lt;/p&gt;

&lt;p&gt;另外，开发者还要注意，如果是同步更新梯度，则还需要加入如下代码：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_queue_runners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chief_queue_runner&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init_token_op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，上述异步的方式需要自行指定集群 IP 和端口，不过，开发者们也可以借助 TensorFlowOnSpark，使用 Yarn 进行管理。&lt;/p&gt;
&lt;h2 id="基于TensorFlowOnSpark的分布式开发"&gt;基于 TensorFlowOnSpark 的分布式开发&lt;/h2&gt;
&lt;p&gt;作为个推面向开发者服务的移动 APP 数据统计分析产品，个数所具有的用户行为预测功能模块，便是基于 TensorFlowOnSpark 这种分布式来实现的。基于 TensorFlowOnSpark 的分布式开发使其可以在屏蔽了端口和机器 IP 的情况下，也能够做到较好的资源申请和分配。而在多个千万级应用同时建模的情况下，集群也有良好的表现，在 sparkUI 中也能看到相对应的资源和进程的情况。最关键的是，TensorFlowOnSpark 可以在单机过度到分布式的情况下，使代码方便修改，且容易部署。&lt;/p&gt;
&lt;h3 id="基于TensorFlowOnSpark的分布式开发的具体流程如下："&gt;基于 TensorFlowOnSpark 的分布式开发的具体流程如下：&lt;/h3&gt;
&lt;p&gt;首先，需要使用 spark-submit 来提交任务，同时指定 spark 需要运行的参数（–num-executors 6 等）、模型代码、模型超参等，同样需要接受外部参数：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--tracks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;数据集路径&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，准备好参数和训练数据 (DataFrame)，调用模型的 API 进行启动。&lt;/p&gt;

&lt;p&gt;其中，soft_dist.map_fun 是要调起的方法，后面均是模型训练的参数。&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;estimator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TFEstimator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soft_dist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map_fun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInputMapping&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tracks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tracks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setModelDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setExportDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serving&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setClusterSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNumPS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_ps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEpochs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBatchSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSteps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;estimator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来是 soft_dist 定义一个 map_fun(args, ctx) 的方法：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map_fun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;worker_num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker_num&lt;/span&gt;  &lt;span class="c1"&gt;# worker数量
&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job_name&lt;/span&gt;  &lt;span class="c1"&gt;# job名
&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_index&lt;/span&gt;  &lt;span class="c1"&gt;# 任务索引
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# ps节点(主节点)
&lt;/span&gt;  &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;worker_num&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TFNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_cluster_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rdma&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;num_workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_dict&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;worker&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;job_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replica_device_setter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/job:worker/task:%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;task_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，可以使用 tf.train.MonitoredTrainingSession 高级 API，进行模型训练和预测。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;基于 TensorFlow 的分布式开发大致就是本文中介绍的两种情况，第二种方式可以用于实际的生产环境，稳定性会更高。&lt;/p&gt;

&lt;p&gt;在运行结束的时候，开发者们也可通过设置邮件的通知，及时地了解到模型运行的情况。&lt;/p&gt;

&lt;p&gt;同时，如果开发者使用 SessionRunHook 来保存最后输出的模型，也需要了解到，框架代码中的一个 BUG，即它只能在规定的时间内保存，超出规定时间，即使运行没有结束，程序也会被强制结束。如果开发者使用的版本是未修复 BUG 的版本，则要自行处理，放宽运行时间。&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Tue, 15 Jan 2019 22:10:13 +0800</pubDate>
      <link>https://ruby-china.org/topics/38006</link>
      <guid>https://ruby-china.org/topics/38006</guid>
    </item>
    <item>
      <title>个数是如何用大数据做行为预测的？</title>
      <description>&lt;p&gt;“个数”是“个推”旗下面向 APP 开发者提供数据统计分析的产品。“个数”通过可视化埋点技术及大数据分析能力从用户属性、渠道质量、行业对比等维度对 APP 进行全面的统计分析。&lt;/p&gt;

&lt;p&gt;“个数”不仅可以及时统计用户的活跃、新增等，还可以分析卸载用户的成分、流向，此外还能实现流失、付费等用户关键行为的预测，从而帮助 APP 开发者实现用户精细化运营和全生命周期管理。其中很值得一提的是，“个数”在“可视化埋点”及“行为预测”方面的创新，为 APP 开发者在实际运营中带来了极大便利，所以，在下文中，我们也将围绕这两点做详细的分析。&lt;/p&gt;

&lt;p&gt;可视化埋点&lt;/p&gt;

&lt;p&gt;埋点是指在产品流程的关键部位植入相关统计代码，以追踪用户行为，统计关键流程的使用程度，并将数据以日志的方式上报至服务器的过程。&lt;/p&gt;

&lt;p&gt;目前，数据埋点采集模式主要有代码埋点、无埋点、可视化埋点等方式。&lt;/p&gt;

&lt;p&gt;“代码埋点”是指在监控页面上加入基础 js，根据需求添加监控代码，它的优点是灵活，可以自定义设置，可以选择自己需要的数据来分析，但对复杂网站来说，每次修改一个页面就得重新出一份埋点方案，成本较大。目前，采用这种埋点方案的代表产品有百度统计、友盟、腾讯云分析、Google Analytics 等。&lt;/p&gt;

&lt;p&gt;“可视化埋点”通常是指开发者通过设备连接用户行为分析工具，直接在数据接入管理界面上对可交互且交互后有效果的页面元素（如：图片、按钮、链接等）进行操作实现数据埋点，下发采集代码生效回数的埋点方式。目前，可视化埋点的代表产品有个数、Mixpanel、神策数据等。&lt;/p&gt;

&lt;p&gt;“无埋点”与“全埋点”相似，它的原理是“全部采集，按需选取”，也就是说它可以对页面中所有交互元素的用户行为进行采集，它是先尽可能多收集检测页面的内容，然后再通过界面配置决定分析哪些数据，但它是标准化采集，如果需要设置自定义的采集方式仍需要代码埋点助力。这种方案的代表产品有 GrowingIO、数极客、百度统计等。&lt;/p&gt;

&lt;p&gt;“个数”为什么会选用可视化埋点？&lt;/p&gt;

&lt;p&gt;当下移动互联网正处于高速发展且发展形势瞬息万变的阶段中，开发者需要及时根据大数据的分析、反馈，对业务功能等做出调整，在传统的操作模式中，如果想要了解不同节点的数据，就要修改相应代码里面的埋点，然后测试发布，之后再在应用商店审核、上线，整个周期可能长达几个星期，这显然无法满足业务的需求。所以，“个数”采用的“可视化埋点”技术就是为了帮助开发者解决这个问题的。&lt;/p&gt;

&lt;p&gt;“个数”的可视化埋点灵活、方便，不需对数据追踪点添加任何代码，使用者只需要通过设备连接管理台，对页面可埋点的元素圈圈点点，即可添加随时生效的界面追踪点，同时在数据采集模式及数据分析能力上，“个数”能够提供给开发者们准确的、有效的数据。
&lt;img src="https://l.ruby-china.com/photo/2019/a6b6d4f9-e4cb-4272-8e3a-46dcdf718a28.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;可视化埋点主要具有以下特性：&lt;/p&gt;

&lt;p&gt;1、零代码，无需代码，节省成本
2、免更新，新增便捷，无需升级
3、易测试，圈选测试，实时呈现
换而言之，可视化埋点不仅可以节约企业成本，还可以提高开发人员和运营人员的工作效率。&lt;/p&gt;

&lt;p&gt;行为预测&lt;/p&gt;

&lt;p&gt;“个数”的行为预测主要包括流失预测、卸载预测、付费预测等，它的原理是基于 App 历史行为数据构建算法模型预测用户关键行为，从而帮助开发者达到用户精细化运营和全生命周期管理的目的。&lt;/p&gt;

&lt;p&gt;在这里需要注意的是，“个数”的行为预测与电商平台常用的个性化推荐不同，后者主要是基于用户近期的行为，如浏览记录、购买记录而分析出用户可能需要的东西，而“个数”是基于 App 各渠道卸载数、卸载趋势等指标的综合分析，更多的是对人群的聚类分析，而非仅仅基于个人的行为。&lt;/p&gt;

&lt;p&gt;行为预测的步骤&lt;/p&gt;

&lt;p&gt;据“个推”大数据科学家朱金星介绍，“个数”的行为预测主要分为以下几个步骤：&lt;/p&gt;

&lt;p&gt;1、找样本，主要从历史数据库中抽取；&lt;/p&gt;

&lt;p&gt;2、特征抽取，将用户与数据库打通，做匹配；&lt;/p&gt;

&lt;p&gt;3、特征筛选，保留相关性高的或有价值的特征；&lt;/p&gt;

&lt;p&gt;4、模型训练，将保留下来的特征放到模型中训练，在模型的选用上，“个数”主要用了逻辑回归，逻辑回归的模型相对深度学习等其他模型来说，简单一些，而且在特征筛选上相对好处理，得到的结果好解释，也相对稳定。&lt;/p&gt;

&lt;p&gt;5、参数优化，根据效果进行调整，如果结果不理想，即可返回调整参数重新走一次以上流程。&lt;/p&gt;

&lt;p&gt;实例分析
下面我们以付费预测为例，为大家梳理一下具体的实现过程。
&lt;img src="https://l.ruby-china.com/photo/2019/e991217c-7e65-4ca7-842c-5e6f1c52a4b0.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;个数付费预测的流程主要包括以下几点：&lt;/p&gt;

&lt;p&gt;1、目标问题分解&lt;/p&gt;

&lt;p&gt;明确需要进行预测的问题即付费预测，以及未来一段时间的跨度。&lt;/p&gt;

&lt;p&gt;2、分析样本数据&lt;/p&gt;

&lt;p&gt;（1）提取出所有用户的历史付费记录；&lt;/p&gt;

&lt;p&gt;（2）分析付费记录，了解付费用户的构成，比如年龄层次、性别、购买力和消费的产品类别等；&lt;/p&gt;

&lt;p&gt;（3）提取非付费用户的历史数据，这里可以根据产品的需求，添加条件、或无条件地进行提取，比如提取活跃并且非付费用户，或者不加条件地直接进行提取；&lt;/p&gt;

&lt;p&gt;（4）分析非付费用户的构成。&lt;/p&gt;

&lt;p&gt;3、构建模型的特征&lt;/p&gt;

&lt;p&gt;（1）原始的数据可能能够直接作为特征使用；&lt;/p&gt;

&lt;p&gt;（2）有些数据在变换后，才会有更好的使用效果，比如年龄，可以变换成少年、中年、老年等特征；&lt;/p&gt;

&lt;p&gt;（3）交叉特征的生成，比如“中年”和“女性”两种特征，就可以合并为一个特征进行使用。&lt;/p&gt;

&lt;p&gt;4、计算特征的相关性&lt;/p&gt;

&lt;p&gt;（1）计算特征饱和度，进行饱和度过滤；&lt;/p&gt;

&lt;p&gt;（2）计算特征 IV、卡方等指标，用以进行特征相关性的过滤。&lt;/p&gt;

&lt;p&gt;5、选用逻辑回归进行建模&lt;/p&gt;

&lt;p&gt;（1）选择适当的参数进行建模；&lt;/p&gt;

&lt;p&gt;（2）模型训练好后，统计模型的精确度、召回率、AUC 等指标，来评价模型；&lt;/p&gt;

&lt;p&gt;（3）如果觉得模型的表现可以接受，就可以在验证集上做验证，验证通过后，进行模型保存和预测。&lt;/p&gt;

&lt;p&gt;6、预测&lt;/p&gt;

&lt;p&gt;加载上述保存的模型，并加载预测数据，进行预测。&lt;/p&gt;

&lt;p&gt;7、监控&lt;/p&gt;

&lt;p&gt;最后，运营人员还需要对每次预测的结果进行关键指标监控，及时发现并解决出现的问题，防止出现意外情况，导致预测无效或预测结果出现偏差。&lt;/p&gt;

&lt;p&gt;其他场景如流失预测、卸载预测等，在流程上与付费预测类似，所以在这里就不再一一介绍了。&lt;/p&gt;

&lt;p&gt;有了精准的行为预测，运营者则可以将运营目标进行拆分、细化，具体到每个场景、每个流程，针对不同用户采取不同的推广渠道、运营策略。例如基于流失预测，运营者能够提前洞察到用户流失行为，提早进行干预，通过个性化内容推荐、消息推送等运营手段对即将流失的用户进行挽留，从而降低流失率。总的来说，在大数据行为预测的帮助下，运营者能够更及时、更全面地了解用户，从而达到精细化运营的目的。&lt;/p&gt;</description>
      <author>GeTui</author>
      <pubDate>Fri, 04 Jan 2019 16:16:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/37972</link>
      <guid>https://ruby-china.org/topics/37972</guid>
    </item>
  </channel>
</rss>
