HPE CMS团队推进NFV容器化的探索之路之Clearwater On Kuberntes

上一节吴治辉老师
和我们分享了
Clearwater镜像在Docker中运行的方式以及Clearwater的Docker镜像的结构和特点

这一节吴治辉老师
继续和我们分享
HPE CMS团队
在Clearwater On Kuberntes
一波三折的改造探索之旅

Clearwater On Kuberntes改造の缘起

首先,我们来看看为什么Clearwater on Kubernetes的方案更为重要,这是因为Kubernetes作为当前最有影响力的容器化微服务架构平台,很多公司已经用它来实现自己的弹性PAAS平台,所以相对于Clearwater on Docker来说,Clearwater On Kubernetes显得更为重要;此外,Clearwater on Docker的部署方案,目前不好解决弹性扩容问题,因为Docker本身没有提供Kubernetes的Service概念,我们很难部署多个Bono(或Sprout)组件并让它们自动成为Clearwater集群的一部分,而如果在Kubernetes平台上,我们则可以将Clearwater的各个组件定义成Kubernetes Service,然后不同组件之间用Service的DNS名称进行通信,这样我们就可以借助Kubernetes的能力来实现Clearwater集群的弹性扩容操作了,在Kubernetes 1.4的平台上,除了可以实现手动扩容之外,还能借助于Kubernetes的Auto-Scale功能实现基于容器负载压力的自动扩容能力,无需人工参与,除此之外,Kubernetes平台强大的自动化与自我治愈能力也大大降低了人工运维Clearwater这种复杂系统的难度。但不幸的是,虽然官方已经给出 Clearwater在Docker上的部署方案,但Clearwater on Kubernetes的方案却一直没有结果,主要是官方维持的Clearwater Docker项目都还有不少问题迟迟得不到资源投入和修复,更没有精力去研究Clearwater On Kuberntes了,从github上的一些问题我们可以验证这一点:

20170317212943

改造从哪里开始

由于官方没有给出Clearwater On Kuberntes的资料,所以我们只能从官方给出的Clearwater On Docker的资料去研究,一路上遇到不少“坑”,这个过程中遇到的一些疑问也得到了Clearwater项目志愿者们的解答,最终得以顺利实现Clearwater在Kuberntes集群上的部署,在此感谢他们。我们首先仔细研究了IMS通讯的流程以及Clearwater的架构图,初步了解了Clearwater各个组件的作用,组件之间大致的通讯关系等基础问题,然后按照Clearwater的容器化指南,开始了我们的Clearwater On Kuberntes的改造探索之旅。

首先,我们针对Clearwater组件分别定义了10个Service,如下表所示:

20170317213003

上述Kubernetes Service与Pod定义并不难,根据官方给出的Clearwater On Docker资料即可完成,下面我们以最复杂的Bono服务为例,给出它的Pod与Service定义文件以及重要参数的介绍。首先是Pod定义:

apiVersion: v1

kind: Pod

metadata:

name: clearwater-bono

labels:

app: clearwater-bono

spec:

containers:

– name: clearwater-bono

image: 10.34.40.11:1179/clearwater-bono

env:

– name: PUBLIC_IP

value: 15.116.146.11

imagePullPolicy: IfNotPresent

ports:

– containerPort: 22

– containerPort: 3478

– containerPort: 5060

– containerPort: 5062

– containerPort: 5060

protocol: UDP

– containerPort: 5062

protocol: UDP

restartPolicy: Always

Bono容器进程需要一个PUBLIC_IP的环境变量,可以理解为集群的“公网IP”,在没有外部负载均衡器的情况下,这里可以填写Kubernetes集群中任意Node节点的IP地址。
接下来是是Bono对应的Service的定义文件:

apiVersion: v1

kind: Service

metadata:

name: bono

spec:

type: NodePort

ports:

– name: 5060t

port: 5060

protocol: TCP

nodePort: 5060

– name: 5060u

port: 5060

protocol: UDP

nodePort: 5060

– name: 5062t

port: 5062

protocol: TCP

nodePort: 30002

– name: 5062u

port: 5062

protocol: UDP

nodePort: 5062

– name: 3478t

port: 3478

protocol: TCP

nodePort: 3478

– name: 3478u

port: 3478

protocol: UDP

nodePort: 3478

selector:

app: clearwater-bono

由于Bono需要暴露SIP接入与STUN服务的的端口,供客户端连接,所以Bono的5060,5062以及3478等端口都采用NodePort方式绑定到Node上,从而SIP客户端可以与集群中的任意Node建立通信连接。更好的方式是外部有负载均衡器,BONO服务的端口映射到公网上,供外部用户使用,下面是建议的部署示意图:

20170317213013

发现Clearwater的一个深坑

随后,我们用kubectrl命令将定义好的YAML文件部署到Kubernetes集群上。注意,必须是Kubernetes 1.4或更高的版本,并且Docker的镜像存储不能采用OverlayFS,否则由于OverlayFS的一个Socket相关的Bug,supervisord无法正常运行,导致Clearwater的一些组件启动失败。发布到Kubernetes集群上以后,观察到Pod的状态都正常,我们成功登录到Ellis的网页(8080端口),但很快我们发现,无法创建SIP账号,浏览器页面报错,随后我们进入到Ellis的Pod容器中进行错误排查,发现了问题所在:

root@clearwater-ellis:/#more/var/log/ellis/ellis_20161017T170000Z.txt
17-10-2016 17:35:11.121 UTC INFO main.py:113: Ellis process starting up
17-10-2016 17:35:11.195 UTC ERROR homestead.py:68: Failed to ping Homestead at http://homestead:8889/ping. Have you configured your HOMESTEAD_URL?

Ellis组件无法Ping通homestead这个主机名(Kubernetes Service名称), 而我们在Pod容易里执行下面的命令,却能返回结果:

curl http://homestead:8889/ping

后来我们排查才发现,这是Clearwater的一个深坑。

Clearwater的设计中大量采用了DNS来实现负载均衡机制,与常规做法不同,Clearwater内部自己向DNS Name Server发起DNS查询指令以达到精确控制的负载均衡效果,下面是Bono组件查询Sprout服务的DNS记录失败时的日志截图:

20170317213403

Bug是什么

为什么在Pod容器内可以解析(Ping)的Kubernetes域名却无法被Clearwater解析?这背后隐藏着Clearwater的一个Bug。我们知道,Linux的DNS查询会用到/etc/resolv.conf文件,该文件是DNS域名解析的配置文件,它的格式很简单,每行以一个关键字开头,后接配置参数,resolv.conf的关键字主要有四个,分别是:

nameserver #定义DNS服务器的IP地址
domain #定义本地域名
search #定义域名的搜索列表
sortlist #对返回的域名进行排序

其中,domain和search作用相同,不能共存,当我们查询一个不带域名的主机名时,如果resolv.conf中配置了domain(或search),则DNS查询的时候,需要带上domain(或search)中定义的域名,以Kuberntes Pod中的容器为例,resolv.conf文件内容如下:

search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 169.169.0.100
options ndots:5

因此,当DNS客户端要查询sprout的DNS记录时,其实是向SkyDNS Server发出如下的完整查询指令:nslookup sprout.default.svc.cluster.local ,但是,Clearwater的DNS查询没有遵循上面的规则,会忽略主机的域名,直接向SkyDNS发出的指令是nslookup sprout,由于SkyDNS上是没有这个主机记录的,所以自然就查询不到了,找到问题原因以后就好办了,我们找到Clearwater配置文件/etc/clearwater/shared_config的控制脚本/etc/init.d/clearwater-auto-config-docker,修改相关的组件名,补充了完整的域名记录,最后生成的/etc/clearwater/shared_config内容如下:

# Deployment definitions
home_domain=example.com
sprout_hostname=sprout.default.svc.cluster.local
hs_hostname=homestead.default.svc.cluster.local:8888
hs_provisioning_hostname=homestead.default.svc.cluster.local:8889
xdms_hostname=homer.default.svc.cluster.local:7888
ralf_hostname=ralf.default.svc.cluster.local:10888
chronos_hostname=chronos.default.svc.cluster.local
cassandra_hostname=cassandra.default.svc.cluster.local
# Email server configuration
smtp_smarthost=127.0.0.1
smtp_username=username
smtp_password=password
email_recovery_sender=clearwater@example.com
# I-CSCF/S-CSCF configuration
upstream_hostname=scscf.sprout.default.svc.cluster.local

一次又一次华丽丽的失败

重新打包镜像并发布以后,终于可以在Ellis界面上生成SIP用户账号了,我们下载X-Lite客户端并配置好SIP账号后,却发现无法登陆成功,报错SIP路由找不到。而Bono与Sprout组件的日志都正常,后来我们研究Bono的启动脚本,修改日志输出级别为DEBUG,此时可以看到完整的SIP报文信息,从这些信息中我们得知原因,是因为SIP路由过程中无法连接

scscf.sprout.default.svc.cluster.local,后来我们再去看Docker方式部署Clearwater的命令,发现sprout组件用Docker network-alias的方式注入了3个DNS名称:
sudo docker run -d –net=clearwater_nw –network-alias=icscf.sprout –network-alias=scscf.sprout –name sprout -p 22 clearwater/sprout

这三个DNS名称分别是sprout、icscf.sprout以及 scscf.sprout,这与Clearwater的设计有关,因为sprout组件用子域名icscf.sprout承载ICSCF,用子域名scscf.sprout 承载SCSCF服务。如果上述任何一个DNS名字不存在或者设置不对(如设置为一样的DNS名称),则SIP客户端登录Clearwater的时候报错,常见的两个错误是“路由不存在”以及“Too many hops error”,下面是Clearwater的问题邮件列表,可以看出很多尝试在Kubernetes上部署Clearwater的人都遇到这个问题:

20170317213414

我们的最初想法是为sprout这个组件定义三个Kubernetes Service,名字分别是:sprout、icscf.sprout以及 scscf.sprout,并且都指向同一个Pod,但很快表明这个思路行不通,因为在Kubernetes中,Service的名字是不能带“.”的,其中一个原因是Kubernetes Service的名称是SkyDNS的一部分,而“.”恰好是子域名的分割符号。解决这个问题有两个办法,第一个办法,第一个办法是修改Clearwater里的相关配置参数,采用类似 icscf_sprout这样的DNS名称而不是子域名来提供I_CSCF服务的寻址;第二个办法是修改Kubernetes的源码,使得Service名称支持“.”。后一个做法很快被否决了,因为Kubernetes发展太快,非官方的源码修改并不靠谱。所以我们采用了第一种做法,首先创建名字为scscf_sprout与icscf_sprout的两个Kubernetes Service,他们与sprout服务共享同一个后端Pod实例,然后再去修改/etc/init.d/clearwater-auto-config-docker脚本中的upstream_hostname参数upstream_hostname,最后重新打包镜像并测试,发现还是通不过。后来我们研究发现,在Clearwater其他组件的启动脚本程序中也有一些固定写死的参数,前后花费了近一周的时间,一一修改查找和修改这些参数后重新打包镜像并进行测试,但最终还是华丽丽的失败了!后来我们查看了Bono与Srpout的源码,才发现源码中有一些icscf.sprout(scscf.sprout)相关的硬编码,意味着仅仅修改配置文件的方式是行不通的!无奈之下我们在Clearwater的github上发了一个Issue,他们也承认Clearwater目前的确有些设计并不能很好的兼容Kubernetes,也随即发起了一个新的分支,尝试解决Clearwater在Kubernetes上的运行问题,但截至当前,这个新的分支除了给出了几个Kuberntes YAML文件,还没有其他动静。

20170317213421

终于峰回路转

我们继续研究可能的解决方案,后来我们设想了一个新方案:绕过Kuberntes,用手工方式在Kubernetes的SkyDNS里插入icscf.sprout与scscf.sprout这两条DNS记录,他们都指向sprout服务的Cluster IP,这样一来这个棘手的问题可能就得以解决。

于是我们开始研究SkyDNS的机制,模仿Kubernetes Service产生的DNS记录,用命令行在Etcd里执行手工插入Key/Value键值对。为了防止手工插入的DNS记录在SkyDNS服务重启后消失,我们把SkyDNS里面用的Etcd地址改为Kubernetes集群所用的独立的Etcd地址。一开始我们插入的DNS记录完全模仿Sprout Service的DNS记录,假如Sprout的ClusterIP是30.0.28.133,则插入的icscf.sprout的DNS记录如下所示:

Key:/skydns/local/cluster/svc/default/sprout/icscf/29512e34
Value: {“host”:”30.0.28.133″,”priority”:10,”weight”:10,”ttl”:30,”targetstrip”:0}

接下来的打包测试结果表明这种方式是可行的,我们的SIP客户端终于连接到了Clearwater上。但观察以后,我们发现一个奇怪的问题:过一段时间后,就登陆失败了。后来排查原因,才发现SkyDNS会周期性同步Kubernetes Service的信息,对于不是Kubernetes Service的DNS记录,他会在同步的过程中删除掉,同步过程影响到/skydns/local/cluster/svc/目录下的所有路径,因此我们后来用了/skydns/local/cluster的路径来定义sprout的子域名,从而绕过了这个问题。

经历了超过3周的反复摸索过程,最终我们打包的Clearwater镜像终于成功发布在Kubernetes平台上,并成功实现了即时通讯、VIOP以及视频通话的演示,以下2张图片给出了Clearwater On Kubernetes的相关细节信息:

20170317213431

20170317213438

下图是用X-Lite客户端成功进行视频电话测试的截图:

20170317213451

我们成功了

为了让Clearwater集群能正常运行,我们需要注意相关组件的启动顺序,要先启动Etcd、然后memcached、随后Cassandra等,如果顺序不对,也导致Clearwater不能正确运行。此外,需要注意的另外一点是Clearwater的容器采用了supervisord的方式来管理多个进程服务,因此可能出现的问题是容器看起来是正常的,但Clearwater的进程却启动失败的问题。因此我们需要登录到容器内部,查看supervisord控制的各个Clearwater进程的当前运行状态来判断系统是否正常,如下所示:

supervisorctl status
clearwater-group:homestead BACKOFF Exited too quickly (process log may have details)

如果发现系统不能正常工作,我们还需要深入分析Clearwater各个组件的日志来排查错误。

接下来,我们来看看Clearwater on Kubernetes的弹性伸缩问题,由于Kubernetes的微服务架构模型,所以我们只要将Clearwater的相关Service的Pod副本数增加,即可实现在线动态扩容,如下图所示,我们用Kubernetes命令行工具将SIP交互中压力比较大的Bono节点的Pod副本从1个扩展为2个:

20170317213504

下图是我们建议的Clearwater集群中各个组件对应的Pod副本数:

20170317213510

最后,我们也成功在HPE NFV-Director上实现了Clearwater On Kubernetes的新案例,下面是HPE NFV-Director的整体架构图以及Clearwater应用的编排界面(部分):

20170317213517

20170317213524 20170317213531

结语

Clearwater这种复杂的、重量级的IMS平台成功迁移到Kubernetes平台上,并且能正常稳定运行,这个事实充分说明Kubernetes这种先进的基于容器的微服务架构基础平台不仅互联网应用,也适合传统的、密切依赖网络的电信系统的改造升级。一旦旧系统成功改造迁移到Kubernetes平台上, Kubernetes平台强大的弹性伸缩能力和运维高度自动化的优点将会带来可观的收益,包括提高系统的吞吐量和高峰时期业务的承载能力、缩短新业务的上述市场时间、并在很大程度上降低电信运营商的综合运营成本。

K8S中文社区微信公众号