Kubernete安装不完全指南

安装Kubernetes向来不是一件容易的事情。之前在集群上部署好的K8s突然出了问题,于是需要重新安装。这次没有之前那次那么顺利了,出现了很多奇怪的问题。但经过一番实践和总结,总算是得出了Kubernetes国内安装的一个不完全指南。这个指南理论上适用于任意版本的K8s。 Master节点的安装过程主要可以参考《使用kubeadm安装kubernetes1.7》。主要的流程是: 安装Docker 安装Kubernetes相关组件 Kubeadm init Master节点 安装Overlay Network Join Node节点 下面主要说一些关于如何确定软件版本、如何制作软件镜像源等等细微但是很要命的问题。 关于Docker 目前使用Docker 1.12版本是比较稳定的。Docker这种基础设施软件没有必要追最新的。在阿里云的CentOS只要直接yum install docker就可以安装这个版本了。 关于操作系统 本文讲的是在CentOS 7上安装Kubernetes。理由是Kubernetes对CentOS支持比较好,网上资料比较多。当然Ubuntu的支持应该也是没问题的。 关于K8s源 因为国内的网络环境,我们在服务上明显是不能直接访问国外的镜像源的。所以我们找掌握寻找镜像源,以及在必要的时候自己制作镜像源的技能(因为云服务商的镜像源不一定同步了最新的版本)。 所以我们可以直接用阿里云的源,比如: cat >> /etc/yum.repos.d/kubernetes.repo <<EOF [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled=1 gpgcheck=0 EOF 目前阿里云的源是1.7.5的,Kubernetes目前的稳定版本是1.8.x,1.9.x马上要发布了。可见过国内的源虽然可以用,但版本上不会特别新。 第二种办法是在国外服务器上下载镜像然后上传到国内服务器。这样的好处是可以任意选择自己想要的版本。具体可以参考使用上文中的《kubeadm安装kubernetes1.7》。 关于镜像 Kubernetes安装过程中一个很大的问题,在于相关组件的镜像都是托管在Google Container Registry上的。国内的镜像加速一般针对的是Dockerhub上的镜像。所以国内的服务器是没法直接安装GCR上的镜像的。 这个问题其实很好解决,首先我们可以自己在本地翻墙拉到镜像,并把镜像push到阿里云的镜像仓库。拉镜像上传的脚本如下: #!/bin/bash set -o errexit set -o nounset set -o pipefail KUBE_VERSION=v1.7.5 KUBE_PAUSE_VERSION=3.0 ETCD_VERSION=3.0.17 DNS_VERSION=1.14.4 GCR_URL=gcr.io/google_containers ALIYUN_URL=registry.cn-hangzhou.aliyuncs.com/muxi images=(kube-proxy-amd64:${KUBE_VERSION} kube-scheduler-amd64:${KUBE_VERSION} kube-controller-manager-amd64:${KUBE_VERSION} kube-apiserver-amd64:${KUBE_VERSION} pause-amd64:${KUBE_PAUSE_VERSION} etcd-amd64:${ETCD_VERSION} k8s-dns-sidecar-amd64:${DNS_VERSION} k8s-dns-kube-dns-amd64:${DNS_VERSION} k8s-dns-dnsmasq-nanny-amd64:${DNS_VERSION}) for imageName in ${images[@]} ; do docker pull $GCR_URL/$imageName docker tag $GCR_URL/$imageName $ALIYUN_URL/$imageName docker login docker push $ALIYUN_URL/$imageName docker rmi $ALIYUN_URL/$imageName done K8s每个版本需要的镜像版本号在kubeadm Setup Tool Reference Guide这个文档的的Running kubeadm without an internet connection一节里有写。所以可以根据安装的实际版本来跳帧这个脚本的参数。注意把上面的镜像地址换成自己的。muxi是你创建的一个namespace,而不是仓库名。...

October 26, 2017

React 16 Fiber源码速览

本文的写作有一部分没有完成,打算针对React 16.3再对本文进行修改,请大家留意 React 16在近期发布了。除了将备受争议的BSD+Patents协议改为MIT协议之外,React 16还带来了许多新特性,比如: 允许在render函数中返回节点数组和字符串。 render() { // 再也不用在外面套一个父节点了 return [ // 别忘了加上key <li key="A">First item</li>, <li key="B">Second item</li>, <li key="C">Third item</li>, ]; } 提供更好的错误处理。 支持自定义DOM属性。 但最关键的一点还是: 没错,React 16是一次重写,在保持API不变的情况下,将核心架构改为了代号为Fiber的异步渲染架构。新架构带来了的变化有: 体积减小 服务端渲染速度大幅提升 更responsive的界面 一次预谋已久的重写 Fiber这个架构并不是突然冒出来的。Facebook的工程师在设计React之初就设想未来的UI渲染会是异步的。从setState()的设计和React内部的事务机制可以看出这点。 在去年,React的开发者Andrew Clark在社区中放出了Fiber架构的一个文档。描述了Fiber架构的基本信息。同时表示Facebook的工程师正在实现这个新架构。今年3月的React Conf 2017上,Lin Clark做了A Cartoon Intro to Fiber这个分享,介绍了Fiber架构的工作原理。今年9月,Fiber架构随着React 16正式发布。Fiber架构的代码放在原来的React仓库之中,并且可以通过运行时的判断来切换新老架构,方便测试和部署。因此Fiber的开发是一个渐进的过程。这个网站实时展示了Fiber通过的测试用例,随着所有用例的通过,Fiber也正式发布了。有趣的是,在React 16发布之前,Fiber架构的React就已经运行在Facebook的产品中了。FB的工程师表示看到新架构在线上产品运行起来,是很激动人心的。具体的情况可以看这篇博客:React 16: A look inside an API-compatible rewrite of our frontend UI library。 Fiber概念简介 本文的题目是React 16 Fiber源码速览,所以关注的主要是Fiber相关的代码。在分析源码之前,首先介绍一些基本概念。 推荐看上文中提到的A Cartoon Intro to Fiber。这个分享比较系统和形象解释了Fiber架构的工程流程,并且使用了React源码中的术语。有助于理解Fiber的概念和源码。下文中的配图也来自这个分享。 reconciler VS renderer Reconciler就是我们所说的Virtul DOM,用于计算新老View的差异。React 16之前的reconciler叫Stack reconciler。Fiber是React的新reconciler。Renderer则是和平台相关的代码,负责将View的变化渲染到不同的平台上,DOM、Canvas、Native、VR、WebGL等等平台都有自己的renderer。我们可以看出reconciler是React的核心代码,是各个平台共用的。因此这次React的reconciler更新到Fiber架构是一次重量级的核心架构的更换。...

September 28, 2017

Table组件中slot内容的跨级传递

在开发MUI的Table组件时,我们遇到了一个问题。用户在顶层组件中嵌套的内容,需要被保存到组件的数据中,并且在表格内部渲染出来。 通常,Vue内嵌内容是使用slot进行渲染的。在父组件的模板中,在子组件的标签中嵌入模板,然后在子组件的内部,使用<slot/>标签进行渲染。但现在我们的需求是非父子组件的slot渲染,这就要求我们换一种思路去保存和调用slot。 要理解以下的内容,请确保你阅读了Render Functions & JSX,理解了Vue的VNode、render function、模板等概念以及这些概念之间的关系。 slot方案 首先要明确一个点,所谓的slot就指的是一个组件在声明时候的内嵌内容。Vue的模板都会被编译成VNode节点树,slot指的是一个VNode的children属性这个数组里包含的VNode节点集合,这些内容由组件声明时的内嵌内容编译而来。 我们可以通过this.$slots.default拿到默认的子VNode列表。如果内嵌内容上没有声明name属性,那这些内容都归属于default这个属性。 所以Slot其实就是一个VNode数组,我们可以把这个数组作为prop传入子节点进行渲染。 {{ vnode }}这种语法会把vnode作为一个对象去序列化,这不是我们所期望的。所以我们需要用v-bind去传递VNodes的引用。 想要渲染slot,可以使用render function。之前讲过,slot其实就是VNode的children,所以我们在render function中createElement的时候把slot的引用作为children传入就可以了。 render(createElement) { return createElement('div', this.content) } 在组件初始化时给this.$slots赋值,然后在模板中使用slot渲染或许也是一种办法,但不一定行的通,也比较hacky。 但我们发现这样不能达到目的。VNode是Vue中对一个DOM节点的内部表示,VNode是有状态的,一个VNode同时只能渲染出一个DOM节点实例。也就是说一个VNode在渲染之后不能再次渲染,除非先把这个VNode从文档中移除,然后才可以再次渲染。 所以,因为我们的表格中的VNodes是会被每一个row复用的,现在这种用法只能渲染第一行的slot内容。 解决方案就是,用一个deepClone函数clone VNode,在每次渲染时初始化新的VNodes实例。 render(createElement) { return createElement('div', deepClone(this.content, createElement)) } scopedSlots方案 这样似乎就可以解决问题了,但我们发现Table的自定义内容常常是一个按钮这样的可以交互的组件,会有事件绑定,如果我们要在子组件中给slot动态传入属性,这是办不到的。 所以slot就不能满足我们的需求了,更好的解决方案就是scopedSlots。 要了解什么是scopedSlots,我们首先将scopedSlots的模板: <template scoped="prop"> <div></div> </template> 进行编译,结果是: function anonymous() { with(this){return _c('div',{scopedSlots:_u([{key:"default",fn:function(prop){return [_c('div')]}}])})} } 这种形式是我们之前没有遇到过的,scopedSlots被编译后,生成了一个函数,而且scopedSlot是被存放在VNode的data属性中,而不是在children中。 仔细观察这个函数,这个函数接收一个参数,然后返回一个VNode,这个VNode的属性是从这个参数中获取的。那scopedSlots的原理就很清楚了,scopedSlots就是一个lazy evaluation的函数,在需要渲染的时候,接收scope对象,然后渲染。这样就可以达到一个类似动态作用域的效果。 既然scopedSlots是一个函数,我们在render function里面只要调用这个函数,并且传入对应的scope对象作为参数就可以了: render(createElement) { const prop = { index: this.id } return createElement('div', [ this.content.call(this, prop) ]) } 这种形式顺便解决了之前slot无法重复利用VNode的问题,因为scopedSlots函数每次返回的都是一个新的VNode节点。

September 19, 2017

简单的前端网络层Service封装

我们编写前端组件时,常常需要拉取数据。最原始的办法就是在组件中调用网络库去请求数据。但这样有一些问题:发送请求的一些代码需要重复的编写。 比如fetch的json()方法,拿到返回数据之后进行错误处理的代码。一个典型的使用场景是这样的: fetch('/api/v2.0' + this.Url + '/?page=' + this.page_num) .then(res => { return res.json() }) .then(res => { if (res.code === 200) { this.items = res.blogs this.pages_count = res.pages_count this.page_num = res.page this.blog_num = res.blog_num }else { util.message("Error:", res.message) } }) } 而且在发送请求这个操作中,带有请求的URL等等和组件业务逻辑无关系不大的数据,我们希望可以集中管理这些请求的路由,并且集中处理错误。 下面就介绍一种最简单的前端网络层封装,我将其称为Service。 简单的Fetch封装 为了避免在每次调用fetch时都要设置各种header和参数,我们可以对fetch做一个简单的封装: function Fetch(url, opt = {}) { // 设置请求方法 opt.method = opt.method || 'GET'; // 处理要发送的数据 if (opt.data) { if (/GET/i.test(opt.method)) { url = `${url}&${obj2query(opt.data)}`; } else { opt....

August 16, 2017

现代前端MVVM组件开发的基本理论

此文在写作中。这篇文章意在整理自己目前对MVVM组件开发的理解。在写作过程中我发现,我自以为已经形成了对组件开发的一套理论,但其实这套理论还有很多不完善的地方。最近又翻到了波神分享-*漫谈Web前端的『组件化』*的PPT,深感要形成理论,还是需要数年的积累才行。所以这篇文章就作为我阶段性的成果,不具有太大的参考价值。 如果要给前端开发下一个定义,那就是在浏览器环境下面向对象的GUI客户端程序开发。 既然是基于面向对象的,那就要有类和对象。在Web前端开发中,最基本的类就是组件的基类(比如Vue和React的构造函数),所有的组件都是基类的一个实例。基类有属性和方法,同时还有一些生命周期钩子函数(其实就是基类上定义的一些方法,会在基类初始化的特定时刻被调用,并且允许实例重载这个方法)。 既然是GUI客户端程序开发,那Web开发中自然有着MVC之类的分层。MVC、MVP、MVVM都是经典的GUI客户端程序的设计模式[1]。目前Web开发中最流行的架构就是MVVM,MVVM已经成为了Web前端开发的一个事实标准。 谈到事实标准,客户端的开发,比如Android、iOS和Windows,都需要使用系统自带的原生UI组件为基础,配合相关的库,进行开发。 iOS丰富的原生组件 在GUI开发领域,组件是一种独立的、可复用的交互元素的封装。 Web开发最大的特点就是,因为历史原因,Web前端当初是作为展示文档的一种渠道来设计的,所以Web前端只有非常少数的几个原生组件,比如表单组件,可以使用。其他的组件都是需要开发者自行封装的。历史上出现过基于jQuery和Backbone等等框架/库的组件。这些组件在当时起到了很重要的作用。在Web标准大幅发展的今天,Web平台(浏览器)有没有提供标准的组件API呢? 答案是肯定的。目前Web前端组件的官方标准就是Web Components。Web Components标准由Custom Elements、HTML Templates、Shadow DOM和HTML Imports四部分组成。Web Components解决了组件的封装、组合以及复用问题。 但在组件的逻辑编写上,随着Web应用越来越复杂,jQuery为代表的命令式编程范式已经不能满足开发的需要。Knockout、ember、angular等MVVM框架出现后,使得声明式的编程范式成为可能。这些框架也渐渐成为了前端开发中的主流。 Web Components解决了组件的封装和复用问题,但Web Components的逻辑依然是命令式的。而且Web Components目前的浏览器兼容性还不太好,不能直接用于生产。 当前主流的Web前端框架/库Vue、React和Angular在某种程度上都提供了组件封装和声明式编程范式两个重要特性。这些框架都有自己的组件封装标准,都遵循数据驱动的范式。值得注意的是Web Components标准在某种程度上对这些框架的组件封装和组合方式有一定的影响,特别是Vuejs,在很多地方参考了Web Components,并实现了Web Components中的slot特性。在未来Web Components标准真正落地时,这些框架都可以和Web Components实现无缝的整合。 因此我们可以说,实现了声明式编程范式和组件封装、复用和组合的现代MVVM组件框架,就是目前Web前端开发的事实标准。并且这个标准在未来很长一段时间内都会持续保持稳定。 Web前端没有一套官方的原生UI组件(这里指的是官方提供的组件实现,比如日期选择组件、ListView组件等待,不是指Web Components这样的底层标准),以后也不会有,因为Web是一个开放的平台,不像其他的客户端程序的操作系统由一家公司所控制。但目前在我们的开发中,已经可以总结出一套成熟的组件。比如Ant Design中的众多组件。这些组件都是基于MVVM前端框架开发的。 前端这么多年的发展,到现在已经进入了一个比较成熟的时期了。有了成熟的模块标准和包管理系统,也有了通用的组件模型。所以假设Web也有一个官方的组件标准,那融合了Web Components和声明式编程的MVVM组件已经非常接近了。一个技术想要被称为工业级,只有形成统一的标准。Web前端的组件,虽然不会有统一的标准,但如果在MVVM组件作为事实的标准的前提下去开发,会减少很多不必要的麻烦。 写这篇博客,也是因为想总结一下目前这个时期的Web开发中的一些事实标准。相比于客户端,前端的门槛其实要更高一些,因为我们要使用堪称刀耕火种的方式去应对日益复杂的业务场景。但在当下,对于刚刚进入前端领域的同学来说,可以把一些范式作为前端的标准来学习,形成对现代前端开发的理解。在现在这个时间点进入前端行业的同学,已经没有必要再了解jQuery时代的开发范式了。 本文的题目是现代前端MVVM组件开发的基本理论,下面就分别介绍MVVM组件开发中几个关键的理论。 MVVM Web Components并没有规定开发者应该如何去给一个组件的逻辑分层。一个组件里可能包含数据、表现(UI)和业务逻辑。在编写组件时,这些部分都需要被严格的解耦,并且规定各个部分之间的通信方式。 MVVM下,组件由如下部分构成: 组件 = 视图 + 数据 + 业务逻辑 MVVM分为View、Model和ViewModel三个部分。分别对应视图、数据和业务逻辑三部分。拿Vue举例,Vue的模板和样式属于View层。Vue的组件实例属于ViewModel,Vue的Model层,在没有引入全局Model层的情况下,就是Vue的data属性中的内容。如果开发者引入了全局的Model层,比如Redux或者MobX,那Model就是一个和Vue组件脱离的对象。 MVVM中,各个部分的关系是这样的: 组件的生命周期 客户端的组件会有生命周期函数,比如iOS的ViewController就有viewDidLoad、viewWillDisappear等等声明周期钩子。前端组件和客户端的的组件一样,都有着生命周期。一个前端组件在应用中,会首先初始化(创建一个新的组件实例),接着加载数据并首次渲染,然后进入一个响应数据变化并重新渲染的循环,最后如果这个组件要从应用中移除,那么组件就会被销毁。 组件在各个生命周期阶段会调用一些钩子函数,开发者如果想在组件特定的时刻执行一些逻辑,就可以在组件中实现这些钩子函数。 接下去总结一下MVVM通常都会有的生命周期。 首先,组件进入初始化阶段,在这个阶段主要就是创建组件实例,并调用init钩子函数。 然后进入初始化数据并首次渲染阶段,这个阶段,如果是Push类型的框架(关于Push和Pull在后文会提到),比如Vue,就需要对数据进行处理(对data进行递归遍历,修改getter和setter,然后调用Render Function进行依赖搜集),然后首次渲染(Vriual DOM patch)。如果是Pull类型的框架,Angular和Regular需要遍历View的AST,然后生成Watcher列表,然后进行首次的脏检查,随后View就被渲染到页面。React就启动一次渲染流程,包括调用Render Function和一次patch。最终达到的效果就是组件首次渲染到页面中。一般在此时也会有一个钩子函数被调用,开发者可以在此时执行一些需要确保UI已经渲染作为前提的逻辑。 接着进入响应数据变化并渲染阶段,这个阶段中,组件已经首次渲染了,接下来如果数据发生变化,那组件就会重新渲染,保持组件的UI和组件的状态保持同步。 如果组件的销毁方法被调用,组件就进入销毁阶段。 之所以要先说明这几个阶段,是因为理解组件的生命周期对于理解后文讲的数据侦测、渲染、模板等有着密不可分的关系。 组合 Composition over inheritence[x]。 在UI的开发中我们常常会复用一些代码。 内嵌组件 Mixin 数据驱动 这里有必要解释一下这套MVVM中,各模块之间数据的流动。View不能直接通知Model更新,而是通知ViewModel用户的交互,由ViewModel来修改Model中的数据。Model的数据变化之后,会直接触发View的更新。...

August 16, 2017

聊聊UI组件设计-Modal

在人机交互中,有一个概念叫做Modality,中文叫模态。模态,顾名思义,就是模拟。计算机可以模拟人通过各种通道接收的信息,比如视觉、听觉、触觉等等通道。视觉就通过显示器输出,听觉通过音响、触觉通过振动。同理,人也可以模拟计算机接收到的电信号,人可以通过键盘、触摸板等待设备来模拟0/1信号。 模态可以是但通道的,也可以是多通道的(比如玩游戏时有声音、视觉、和振动反馈)。今天我们要将的计算机软件中的Modal组件,就是计算机向人建立的单通道信息交互方式。 Modal在交互设计中的作用 具体到产品的交互设计中(交互设计和人机交互不是一回事,准确的说两者有交叉),Modal这个组件是很常用的,那么Modal在交互上的意义是什么呢? Modal的弹出,其实就是计算机和人之间建立了一个信息传递的通道,这个通道是独占的,在关闭这个通道之前,不能进行其他的交互。计算机程序建立这个通道,为的是传递一些信息。但传递信息有很多的方式,为什么要使用独占通道的Modal来传达呢? 因为Modal传递的信息,是为了让用户提供关键信息,这个信息的回复(Modal的输入)可以是是一个true or false的选择,也可以是较为复杂的数据结构。Modal是一个浮层,所以用户在Modal弹出时,不能再点击应用的其他部分。用户必须要做出决定,是输入信息,或者取消(关闭Modal)。因此,Modal会中断用户当前的工作流。 实现:Modal组件的特点 全局一般只能显示一个Modal,因为Modal显示时会需要用户做出决定后再消失(点击取消也是一种决定,即为对操作的否定)。 Modal一般提供确定和取消两个按钮。 Modal中标题和底部按钮直接的内容,一般是可以自由组合的。一种特殊情况是Modal中组合了input,那这种Modal类型被称为prompt。 Modal如果涉及异步的操作,则需要有一个confirm loading状态。这个状态下用户不能再次点击confirm。 这里推荐一篇很好的总结文章覆盖层设计(上)-对话框&浮层来自网易UEDC。系统的总结了交互设计中浮层相关的设计。 不同UI库的Modal设计 下面讲讲正题,Modal作为一个Web前端组件的设计方式。 根据Modal在前端代码中的调用方式,可以分为声明式和命令式两种。Modal的声明式使用是指在前端模板中声明Modal。命令式则是在前端代码中调用一个函数,来显式的调用Modal。 声明式 Ant Design中的Modal是典型的声明式组件。Modal被声明在模板中,在父组件初始化之时便存在了。 render() { return ( <div> <Modal title="Basic Modal" visible={this.state.visible} onOk={this.handleOk} onCancel={this.handleCancel} > <p>Some contents...</p> <p>Some contents...</p> <p>Some contents...</p> </Modal> </div> ); } Modal出现时只是visible这个flag被设置为true。而不是在那个时候初始化Modal组件。要在Modal中组合内嵌内容,只要在模板中的Modal标签中组合内容即可。在用户点击Modal的取消时,只要将visible设为false。所以这里不涉及Modal的销毁问题。Modal的回收是和父组件一起的。 命令式 Ant Design中的Modal组件也提供了几个静态的方法,用于在组件中手动初始化一个Modal,并且提供了destroy方法来手动销毁。 const modal = Modal.success({ title: 'This is a notification message', content: 'This modal will be destroyed after 1 second', }); Element UI中的Modal组件的API则是标准的命令式。 this.$prompt('请输入邮箱', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', inputPattern: /[\w!...

August 14, 2017

Headless Chrome截图服务实战

TL;DR 给太长不看同学的内容速览: Headless是Chrome 59中加入的一种新的运行模式 Headless Chrome可以替代PhantomJS,并且更加强大 可以通过Chrome DevTools Protocol这个协议对远程的Chrome浏览器进行调试 chrome-remote-interface是Nodejs下Chrome DevTools Protocol的封装 可以使用Emulation.setVisibleSize对整个页面进行截屏 PhantomJS的问题 之前有数报表的导出图片功能是用PhantomJS做的。PhantomJS有两个很大的问题:第一是,它的渲染引擎和JavaScript引擎基于Qt5,版本不是很高,所以渲染的时候会有一些兼容问题,而且JavaScript引擎也相对比较古老(最新的PhantomJS release是2.1版,这个版本基于Qt5.5。Qt5.5使用的Chromium内核版本是40,Chromium现在最新版本是62)。第二,PhantomJS现在已经处于一种维护不多的状态(Github上有1901个open issues)。 作为一个个人项目,PhantomJS在各种自动化测试以及页面自动化操作中被广泛使用,达到了很高的高度。但因为以上两个缺点,使用PhantomJS将不会是长久之计。 Chrome的Headless模式 Headless Chrome其实不是一个全新的工具,而是普通的Chrome浏览器的headless模式。headless就是指Chrome的UI部分是不运行的。 所以只要你的机器上安装了Chrome 59+,你就可以使用Headless Chrome。相比之前npm install时经常要从bitbucket下载PhantomJS binary的麻烦事,Headless Chrome要方便不少,毕竟Web开发者一般都安装了Chrome。 你可以在命令行中用headless模式启动Chrome: chrome \ --headless \ # Runs Chrome in headless mode. --disable-gpu \ # Temporarily needed for now. --remote-debugging-port=9222 \ https://www.chromestatus.com # URL to open. Defaults to about:blank. 我们可以直接在Chrome的CLI中进行一些操作,比如截屏: chrome --headless --disable-gpu --screenshot --window-size=1280,1696 https://www.chromestatus.com/ 但一般我们很少会这样直接使用Headless Chrome。对这部分有兴趣的同学可以看官方文档,这里就不多说了。 Headless Chrome的最大的优点就是,它就是Chrome,所以可以保持Evergreen,也就是持续的更新。并且我们可以在Headless模式使用Chrome带来的所有的现代Web平台的特性。所以Headless Chrome就成为了PhantomJS的完美升级版替代品。 强大的Chrome DevTools Protocol 要在脚本中和Chrome进行交互,需要用Chrome DevTools Protocol这个协议。所以这里首先介绍一下这个协议。...

August 3, 2017

前端微服务实践-以木犀通行证为例

在前端,长期以来困扰我们的一个问题就是,如何分发部署我们的前端代码?之前我们的前端代码是放在后端容器中部署的,因此如果需要更新就需要后端工程师去重启容器,更新代码。现在我们采取的方法是,将前端作为一个单独的服务,使用容器来部署。这样前端代码的部署和其他的代码就没有区别了。前端工程师可以自由的控制前端代码的部署,整个部署流程也变的非常标准化。前端微服务让前端部署变成了一件让人享受的事情。下面就以木犀通行证为例来讲讲具体的实现。 Nodejs服务 如果只是在容器中放一些前端的静态文件,那不能叫前端微服务。前端微服务是指用Nodejs实现的View层。包括了同步路由以及前端模板。静态文件可以放在前端容器中分发,也可以上传到CDN分发。 Nodejs实现的这个View层(传统后端MVC中的View),主要负责渲染同步路由的模板。API服务则是由其他的服务提供,大家各司其职。前端接管View层有很多好处,前端渲染以及各种网络应用层的优化,都可以由前端自己来控制。目前Nodejs在大公司中早已频繁被用在服务最前端的那一层中了(后端一般是Java)。 具体实现来说,我们选用koa2作为Web框架,大致实现是这样的: const send = require('koa-send'); const Koa = require('koa'); const Router = require('koa-router'); const userAgent = require('koa-useragent'); const path = require('path') const swig = require('swig'); const router = new Router(); const app = new Koa(); const templateRoot = path.join(__dirname, "../dist/template/main") app.use(userAgent); router.get('/', function(ctx, next){ if (!ctx.userAgent.isMobile) { let template = swig.compileFile(path.resolve(templateRoot, "auth.html")); ctx.body = template({}) } else { let template = swig.compileFile(path.resolve(templateRoot, "auth_phone.html")); ctx.body = template({}) } }); router....

June 5, 2017

木犀后端开发工作流(2017年6月版)

木犀后端的技术经历了一个不断演进的过程,从最初的LNMP式的简单直接的部署方式,到Docker容器部署,到现在的Kubernetes集群部署。相比当年艰难的调试由于操作系统环境不同而导致的各种部署问题,现在的部署流程可以说是相当简单而且可靠的。这也要求我们有一套标准化的开发部署流程。 三个环境 在互联网公司,一个产品一般都部署了好几个版本,比如开发、测试、预发布等等。不同的版本对应开发周期不同时间点的产品状态。QA一般就是在部署好的这些环境中进行发布前测试的。 我们对环境的要求没有那么高,只要有本地、测试、线上三个环境就可以了。下面分别讲讲从本地开发,到部署测试版本,到新版本上线过程中一系列的标准流程。 开发分支与Pull Request 本地开发其实没有太多的限制,一般就直接在本地运行代码进行开发。需要注意的是我们的仓库中一般有主分支和开发分支,这个开发分支可以是按版本号,每次新版本时从主分支checkout出来。或者是一个持续使用的开发分支。所有人都不能直接向主分支提交代码。但可以直接向开发分支提交代码。 如果想讲开发分支中的代码合并到主分支,就要发起一个Pull Request。Pull Request在负责人code review(看情况)以及CI测试通过之后才能merge。 单元测试与CI 对于有明确输入输出的后端API来说,单元测试是必要的软件质量保障。也是协助开发的一个手段。 大家在本地提交代码之前先自己跑过测试,通过之后再提交。Pull Request时还会跑一遍CI,来确保代码功能的正确。比如这样: Github上使用的比较多的是Travis CI。Docker based的项目可以参考这个CI配置。 这里简单介绍一下CI。CI指持续集成,我们说的CI一般是指云端的CI runner。Travis CI本质上其实就是一个云服务。提供了一个虚拟环境来运行你指定的脚本。Travis支持很多语言环境,但最近Travis支持了Docker,所以环境也就不是问题了。要注意我们写测试时要写清进程exit时的状态码,非0的状态码代表非正常退出。Travis就是根据这个来判断测试或者其他错误是否发生的。这决定了这次CI运行是否成功。 测试环境的具体配置 在开发基本完成,代码merge到主分支之后,我们就可以尝试部署一个测试版本了。测试环境和线上的环境差别不大。测试环境部署在我们的测试集群(几台专有网络阿里云学生机)。 大家可以随意选一台机器然后部署。部署的时候,除了数据库之外的一般都用Docker部署。如果有多个容器需要部署,我们一般用Docker-compose来一键build&run。 数据库一般就使用某台机器上直接安装的数据库。在初次部署时大家要记得在容器中执行初始化数据库和用户角色命令(当然也可以写成脚本)。 测试环境有几个用处,首先是给前端提供联调的API。然后是给产品经理和设计师提供一个线上版本来进行初步测试。因此我们需要给测试环境配置一个域名。但如果直接在DnsPod等等线上解析,要解析的域名数量会很大,很不方便。所以我们采用修改本地hosts文件的办法。 比如: 120.77.8.149 test.share.muxixyz.com 注意域名是muxixyz.com的子域名,因为阿里云会检测DNS解析的域名是否备案。如果随便写一个域名是不行的(当然理论上你用baidu.com或者其他知名的域名也行)。 这个hosts文件的配置要写在Github文档中。Tower项目里最好也写一个操作指南文档给设计师和PM看。 上线 MAE发布后本节需要更新 上线一个版本,比如1.2版。首先在主分支打上这个tag。然后在阿里云的镜像仓库里build这个tag的镜像。 最后由负责部署的同学,SSH到集群更新Deployment配置文件中的image版本号。升级Deployment即可。 这一步在将来会由开发应用的同学自行在MAE上操作完成。目前暂时还是需要有人在服务器上部署。开发的同学只要交付镜像就可以了。 总结 相比于之前的工作流,目前的工作流不同的地方主要是,需要写单元测试,PR需要CI通过才能merge,大型的应用必须要部署测试环境并用本地DNS解析访问,进行部署时是用Docker镜像而不是在服务器build。这套工作流需要大家用微服务的观点去看待将来的开发。随着时间的推移,这套工作流也会不断的变化,大致还是朝Cloud Native的方向发展。

June 5, 2017

云端木犀-MAE初步构想

2019/6/5 今年我们有同学拿到了腾讯云和 Azure 的实习,所以未来在云计算上我们会有更多的领路人,来带领团队在这个方向进行探索。MAE 虽然还没做出来,但 k3s 的发现让我们在服务器上实现了自由,相信很快就会设计出下一代的 MAE 架构。 Muxi App Engine,简称MAE,是木犀的私有PaaS方案,也是木犀云的重要组成部分。MAE主要基于Docker和Kubernetes,为木犀所有应用的构建、部署、监控和扩容提供了一个统一的入口,让我们能专注于服务本身的开发。同时MAE也为木犀提供了一套标准化的运维流程,使得团队开发中的工程化程度进一步提高。 说的这么厉害,那如果你是一个技术小白,我应该如何来解释MAE呢? TD;LR 比如我们有一个应用,华师匣子。华师匣子是由很多的服务构成的,比如成绩服务,课表服务,图书馆服务等等。每个服务都实现了对应的接口。我们使用Docker来运行这些服务。Docker是一种容器技术。我们可以简单的理解为一种沙盒环境。这些容器的存在,已经很大程度上方便了我们的部署。因为容器可以实现系统资源的隔离,使得服务器上可以同时运行很多不同的服务,而相互不打扰。 但手动部署容器,还是太复杂了。我们要登录服务器手动部署容器。容器如果出现问题,我们也需要亲自去重启。如果我们需要横向拓展,部署多个相同的容器以应对高负载,也需要一个个去手动部署。这个时候就需要一个调度者来帮我们自动完成这个任务。 我们可以把MAE理解为容器的调度者。我们在MAE中新建一个应用和下属的服务,填写相关的信息。比如我们只要提供Docker镜像的地址,就可以一键部署。MAE会帮我们将容器部署到合适的服务器上。如果容器因为某些原因崩溃了,MAE会自动重启容器。如果我们需要横向拓展,那只要在控制台里填写一下需要拓展的数量就可以了。如果需要更新代码,我们只需要提供镜像的新版本号,MAE会自动终止旧版本的容器,新建新版本的容器。一切都是这么简单。可以自动化的事情,我们都会做到自动化。 MAE提供Web UI和CLI。Web UI主要用于日常的使用以及查看监控数据。CLI适合在shell脚本等自动化环境下使用。 MAE带来的最大变革是,今后我们的应用从一开始就应该按Cloud Native的思路去编写。要拥抱云计算,我们必须编写Cloud Native的应用,具体的说,使用微服务架构,写无状态的功能单元,容器技术,将数据库等等持久化的组件作为单独的部分等等,都是Cloud Native的体现。只有这样,我们的应用才能和目前公有云和私有云的基础设施完美结合。 下面就是纯粹的技术讨论了,请耐心阅读。 MAE的技术选型 简单的说,就是Docker和Kubernetes。Docker是容器技术的实现,Kubernetes主要提供了容器编排管理的功能。上一节中说到的大部分自动化功能,都是Kubernetes实现的。MAE中需要我们研发的主要是MAE API服务、Web UI还有CLI程序。除了这些,还有就是在MAE中实现一套最适合我们的对应用的抽象。这套抽象是非常重要的。Kubernetes的概念并不是所有人都可以理解的,也没有必要对使用者暴露最底层的概念。PaaS的用户是从是应用和服务这些逻辑上的概念去看待问题的。所以MAE就提供了针对应用和服务的抽象,并且和Kubernetes整合起来。 MAE的组成部分 MAE的组成,从上到下,大致有三层: MAE服务层 MAE服务层是暴露给用户的一些服务。MAE API Server是MAE是中枢。负责和底层的集群通信,保存应用配置等等。MAE Web UI提供了一个Web界面,用户可以通过Web UI对MAE发出指令,查看监控数据。MAE CLI是一个命令行程序,提供了从命令行和API Server通信的渠道。 逻辑应用层 这一层是抽象的应用层。也就是我们概念上的应用。因为实际的集群中是没有应用概念的(当然Kubernetes的Services+Namespace已经非常接近了),所以我们需要在这里提供对应的抽象。我们可以在MAE中新建应用,然后配置这个应用对应的服务。MAE中的服务(以后简称MAE服务,区别于Kubernetes Service),其实就对应一个微服务。一个应用由至少一个微服务构成。MAE服务是用户可以控制的部署的最小单元。我们可以对某个MAE服务单独进行拓展。比较特殊的MAE服务就是Nginx入口服务,这个服务为所有应用提供反向代理,同时也作为一个MAE下的服务,被MAE部署。 Kubernetes层 Kubernetes这层就是底层的实现层了。包括了Service,Deployment和Pods。其中Service和Deployment在上层共同支撑了MAE服务。Pods则属于最底层的调度单元。在MAE层是完全不可见的。一个Pod由至少一个容器构成。 MAE的流量分发 那么作为一个分布式系统,一个用户的请求究竟是经过怎样的路径,到达最底层的Kubernetes Pod的呢? 首先DNS把域名解析到Kubernetes的Master节点的公网IP上,然后部署在Master节点上的Nginx入口服务接管,Nginx根据MAE应用设置的域名和URL规则,将这个请求转发到对应应用的某个服务上。Kubernetes的服务都是可以通过<Master内网IP>:<Service Port>来进行访问的。然后Kubernetes proxy用iptables规则,将请求转发到某个节点上的Pod。 由于Kubernetes proxy提供了均衡负载,我们不用再操心如何分配流量到服务下属的多个Pod中的某一个这样的问题。今后可以做的优化是,实现Kubernetes Master节点的高可用,也就是同时部署多个Master节点。这样的话就需要在Master节点之上再实现一个均衡负载。 MAE的实现细节 MAE做的抽象,一个是应用,应用之下是服务。对于这两个抽象,应该各自保存一些什么样的数据,这属于MAE的实现细节。 每个应用需要的信息有,应用名,域名,Nginx转发规则,应用下属的服务列表。 每个服务需要的信息有:服务名,当前镜像版本,镜像仓库地址,Github仓库地址,Kubernetes Service和Deployment需要的全部信息,当前服务属于哪个应用,授权管理当前服务的用户列表。 因为服务是部署的最小单元,因此相对来说服务是MAE中比较核心的一个部分。MAE需要将数据库中保存的服务信息,自动转化为Kubernetes需要的.yaml文件。将数据库中保存的应用信息,自动转化为nginx的配置文件。这是实现上需要去考虑的一个问题。 另外,现在还需要仔细考虑的一点,MAE在全局/应用/服务这几个层面分别需要哪些监控数据。 MAE时代的部署工作流 部署服务之前,首先我们要构建镜像(构建之前可以引入CI,测试通过才可以构建镜像)。给镜像打上版本号,然后发布到云端的镜像仓库(可以用阿里云/蜂巢/Daocloud)。之后我们就可以在MAE中为某个服务新建一次部署了,填上新的版本号,点击部署,就启动了一次部署了。得益于Kubernetes超强的部署能力,我们可以回滚、暂停、继续每一次部署。 MAE的API Server把服务目前的配置转换为.yaml格式,向Kubernetes API Server发送请求。然后Kubernetes会进行相应的处理。和Service相关的就调整Service,和Deployment相关的就调整Deployment。最终服务更新到目标状态,部署完成。 MAE的物理节点组成 MAE的逻辑组成已经介绍了,那MAE和具体的云主机之间是什么关系呢。请看下图:...

May 27, 2017