RTC 2019回顾 | 利用 Kubernetes 部署视频流录制服务器

“部署离代码太远了,我很喜欢代码,所以我想从代码讲起。在这个过程中,你可以看到,从开发到部署,需要思考哪些问题”,WishLife CTO 汪磊在 RTC 2019 大会上分享《利用 Kubernetes部署视频流录制服务器》时讲到。WishLife是一个利用视频帮助家庭解决家庭沟通的平台,平台提供视频录制服务,录制的视频会保存在云端,供家人来访问、观看。他分享了函数式编程语言Clojure、Kubernetes,以及视频录制服务部署,三方面的技术点和经验。
image
以下为演讲实录:

大家好,我是汪磊,我是一个重度的 Emacs 用户,现在写代码的语言有 Clojure、JavaScript 和 Java。我也是 GraphqlQL 的早期参与者。我创业过,也开发过千万用户级的 SaaS 产品。接下来会分享一些我在工作中的思考。

除了技术,我在生活中喜欢冲浪、滑雪。我发现了一个问题,我有太多想玩的东西,但自己的时间又太少。玩的时候会经常会接到工作电话。于是在想,有没有什么办法让产品少出问题,或者即使出现问题,也都是微乎其微的。

带着问题,我找到了答案,一个是函数式编程 Clojure,一个是 Kubernetes。函数式编程的优点在于,可以让我的工作效率提升了 10 倍,同样是一套代码,函数式编程写起来会更快。而 Kubernetes 让我们能将以往的服务器部署经验化为代码,让这件事情变得更具有可操作性,把运维工作变得可复制。

函数式编程如何提升 10 倍工作效率

平时我们使用的命令编程有三种基本结构:顺序、选择、循环。而函数式编程则是把电脑运算视为函数的计算,用公式来讲就是f(x)->y,我们要写的代码是函数 f,函数 f 接收一个输入 x,输出 y,函数式编程要写的代码就是这个函数 f,转换为工程语言就是“对某一个固定的输入,需要给一个输出”。我们在命令式编程中也会写这样的代码,函数式编程把它推到了另一个极端,所有做的事情都是函数,不用考虑顺序。这让你在想问题的思路完全不一样。在函数式编程中,输入的参数可以是一个函数,输出的结果也可以是一个函数。

函数式编程有什么特征呢?

在函数式编程中,你的代码就是数据。我们写代码写的多的人都会有这样一个感受:你今天写的代码,不管是自己当时写的时候多么酷,在六个月之后看,会觉得是垃圾代码。但当你的代码能够变成数据的时候,代码就具有了生命,它可以不断的变化,一个能够不断变化、不断进化的代码就不是一个死的代码。

我们写的很多代码,不管是用函数式编程还是用命令式编程写的,最终的目的是要有副作用。怎么理解这个问题?就相当于你写了一个非常精妙的算法,算法最终要有个输出,对外界要有个影响,比如在database里留下一条记录,写一个文件出来或者控制一些外界的系统,这样我们写的程序才会有用。

纯函数(pure function),它与副作用是相辅相成的。如果一个函数没有副作用,那它叫什么函数?所以在纯函数里面两者是相辅相成的。如果要让你的程序有用,要有副作用;如果要让你的函数变得可复用,那就不能有副作用。

第四个特征是不可变数据(immutable)。这是很重要的概念。在很多系统里都可以看到这个概念的身影。举个例子,在 Git 之前我们用的传统版本管理系统是SVN,存一个文件的副本,每次改了之后存一个变化,从概念上讲我们以为这样存的东西一定是很小的,但对Git来讲每份文件存一个副本,不会改原来的文件,很多人都会说Git存的文件的文件大小肯定比SVN存的文件的文件大小大很多,实际相反,每一次文件的改动存一个副本所占用的存储空间更小。同理,这也是 immutable 的优点。

最终,落到某个具体的语言上,这个语言要支持函数式编程,你不需要学习新的语言,你可以尝试用函数式的方式写Java,尽管思考问题时用的语法还是JAVA,但却是一门新的语言。我选择的函数式编程语言叫 Clojure,它基于Java虚拟机,是动态类型,没有类型定义的,基本的数据结构是不可变的数据结构。

怎么运行Clojure代码呢?第一种是解释执行,背后执行的方式是编译。Clojure代码最终会被读进去编译,生成JAVA的字节码。写JAVA代码和写Clojure代码,在运行上跟写JAVA代码没有任何区别。在写Clojure的时候更多的是用一个REPL(Read-Evalute-Print Loop)特性。我们写JAVA code的时候,把一个代码保存好,不管手多快,最少需要1分钟的时间。如果写Clojure code,你把代码写出来按回车,它就会执行这个结果,并把结果告诉你,这个过程可以快到10秒。你省去了手工编译执行的过程,Clojure会帮你做这件事。

我讲两个Clojure的基本语法。我不想教大家怎么写Clojure,主要是讲概念。我们写的JAVA code是这样的:

println("Hello World!")

函数式编程则是这样的:

(println "Hello World!")

把需要 Println 的内容用括号括起来,它是一个 list。这类语法很简单,第一个元素是函数,这个函数会被执行,这个语句会返回一个值。用函数的概念来理解就是,我们会调用函数Println,它没有输出,打印一个“Hello World!”到屏幕上。

如何定义一个函数呢?

(defn greet
 "Return a friendly greeting"
 [your-name]
 (str "Hello, " your-name))

还是通过一个 list,括号里的内容都是 list,你可以将 defn 看做为一个 function,greet 是它的函数名字,第二行是它的描述文档,第三行是它的参数,最后一行是它要执行的语句。也就是说,定义一个 greet 函数,会执行最后一行代码中所写的语句。比如,输入是“Tom”,输出就是“Hello, Tom”。

我们写JAVA代码的时候,平均一个函数最小20-50行代码。函数式编程,一个函数一般三五行,最多二十行就非常多了,二十行的代码可以顶得上一个200行的对应的JAVA代码。(演讲人在现场编程举例,详细 demo 请见文末「阅读原文」)

Apache commons 有一个 isBlank 。我们尝试着一步一步将其转换为 Clojure 代码。

image
我们之前说过,Clojure 是一个动态类型的语言,没有类型声明,所以我们首先要去掉其中所有的带有类型的代码,结果如下。
image

因为 Clojure 是一个 function program,没有 class 的概念,所以我们要去掉 JAVA 类。

image

在Clojure 中,函数是 first class citizen,所以此前代码中 for 循环在这里只需要一个 function 即可代替。

image 然后我们要去掉不需要的边界条件。

image

现在,我们再对比看一下 Clojure 代码应该是什么样的,如下:

image

最终很冗长的 JAVA 代码,现在变成了一句话。这就是为什么我说,它帮我们提高了 10 倍的工作效率。绝大部分的函数已经有了,你只需要学的就是那些函数是什么。map、reduce、filter,我认为首先学会这三个 function,就可以解决大部分问题。举个例子,我们写的很多业务代码,基本上都是对一个数据集合的操作,这个操作有可能是给你一个数据集,让你返回一个同样大小的数据集,这个操作就是对应 Clojure 里的 map。另一种是,给你一个数据集,让你返回一个比它小的数据集,这就可以通过 Clojure 中的 reduce 来实现。filter 就很简单,就是条件函数。事实上,很多现金的技术,都是源于这种朴素的思想。

用 Clojure 完成编写之后,存为一个 JAVA 包。剩下的工作就是,将它放到 production 里。之前我们要考虑机器环境是可以达到运行条件,现在就简单了,通过 Docker 就可以让我们的程序运行到任何一台机器上。不过,还有一个问题,一个 Docker container 只是一个实例,我们要如何大规模部署,这时候,我们就需要用到 Kubernetes。

用 Kubernetes 部署视频流录制服务

Kubernetes是一个生产级别的容器管理平台,可以实现自动化的容器部署、扩展和管理。所以我们之前写的代码就编程了一个 Docker 镜像,然后通过 Kubernetes 实现自动化的部署。

Kubernetes 有几个基本概念是需要我们理解的:

  1. Cluster
  2. Node
  3. Pod
  4. Service
  5. IngressController
  6. PersistentVolume
  7. PersistentVolumeClaim

我们看一下概念图。这是一个用户在云上访问到了load balancer,通过load balancer 到 cluster 之后,会有一个 ingress controller 接收用户的请求,用户请求被派到 service 上,对应depolyment,最终到某个container 被执行,最终反馈会通过原路返回。知道以上的几个概念以后你就可以把任何产品部署上去。

部署的方式是声明式部署。声明式部署是什么意思呢?我们之前在部署的时候会执行很多命令,每个命令执行完它就消失了,你没有办法让其他人知道部署的过程,这些操作的方法都是无法传递给下一个需要负责运维的同事的。在Kubernetes里,会用一个 yaml file 来描述怎么部署,Kubernetes 通过这个声明文件,来帮你进行部署,你所有关于系统部署的操作全部写在其中,后面接手值班的同事看到文件就知道该怎样操作。

接下来,我们看到的是 yaml file。根据上述的架构,我们要从底层的Deployment开始进行描述,直至ingress controller。

第一步我们要描述 Deployment。我们要部署一个 recording-server,image 的地址是 us.gcr.io/qatest-220319/recording-server,具体代码如下。
image
Service 的描述如下,最终会把服务 dispatch 到 recording-server 上。
image
然后是 Ingress Controller。

image
完成上一步之后,一个无状态的服务就起来了。在这里,我们要做的业务是录制视频,那么最终会产生很多录制文件。但是现在 pod 是无状态的,一旦 pod 被终止,所以的录制文件都会丢失。所以我们现在需要 Volume 来存储这些录制文件。Pod 虽然会消失,但是 Volume 不会。如下是我们定义一个 Volume 的过程。

image
但是,我们不能每一台服务器都通过手工来部署 Volume,所以这个时候我们就需要通过这个 StatefulSet 来管理录制文件的存储。存储。
image
最终,我们还需要增加一个多用户多环境的描述。这个是通过使用 Namespace 实现的。

image

最后为了方便地定义、安装和升级Kubernetes,我们可以引入Helm Charts。我们可以通过它提供的模板、命令行工具来完成这些操作。