2017 年 10 月 22 日,又拍云 Open Talk 联合 Spring Cloud 中国社区成功举办了“进击的微服务实战派上海站”。ARKie 首席技术官李荣陆作了《数百微服务的管理和容器化实践》,以下是分享实录:

李荣陆,现任 ARKie 首席技术官,ARKie 是国内首款智能设计助手,4月19日正式对外发布1.0公测版。产品网页版拥有亿万图片素材库、上千种字体和丰富的设计师出品模版,并与 Shutterstock、海洛创意、汉仪字库、有字库等国内外顶尖创意素材供应商达成战略合作。目前,ARKie 已获得来自ARK创新咨询、站酷、洪泰基金等多轮融资。

一、契机

(一)技术选型

我们当时在用微服务的时候,技术选型也是有多种选择,当时之所以用 Spring Cloud,最主要的原因是因为 dubbo 不再怎么维护,而 Spring Cloud 呈现越来越活跃的趋势。我们其实不是一个单纯的 Spring Cloud 技术站,是把 node 和 Spring Cloud 整合在了一起。

Spring Cloud 我介绍几个东西大家需要注意。

第一个是服务治理中心,德国有一家公司专门开发了一个叫 Spring Boot Admin ,它与 Spring Cloud 已经有一个相当好的整合,提供了一些基本的技术性的服务治理功能,因此我们没有重新开发,而是把 Spring Boot Admin 进行了一些扩展,增加了日志管理、线流,甚至我们的灰度发布、配置等,这样的话我不需要从头做。

第二点可能与其他公司不太一样,我们用了 node 做服务的聚合。实际上我们一套环境有 300 多个微服务,一共有五套环境,一共有 1500 个左右微服务,当你量大的时候,就会出现很多问题。其中一个问题就是服务聚合的问题,当你服务特别多,不可能每个服务都暴露很多 API ,对于使用来说过于复杂。当你告诉我一个服务的名称,让我说出这个服务是做什么的,有些服务我说不上来。所以达到这种程度,我们使用了一个聚合器,聚合器就是可以把很多服务的 API 做一个聚合,对外看来它这个服务数量可以减少,调用的可以比较简单。

(二)服务管理

当你达到几百个微服务的时候,这个服务的治理已经相当麻烦了。就像原来一个软件单体应用的时候,几百个模块的时候你会怎么办呢,典型的做法就是一个分层、分组。我会把若干个相关联的模块分成一个个小组,若干个相关联的东西分成一层,这个概念特别像一个公司的发展。假设一个公司刚开始只有十几个人的时候,我相信每个人都可以知道另外一个人的名字。当你想去跟另外一个人沟通的时候怎么办呢,你直接跑过去跟他说话就行了;当你的公司达到了三四十个人的时候会怎么办,一定会把整个开发团队分成若干个小组,每个小组会怎么办,有一个 team leader,当小组和小组想进行沟通的时候,肯定是找 team leader 做沟通;当你这个团队再大,可能要达到上百人了怎么办呢?你会把这个团队划分成多个部门,可能是研发一部,研发二部,甚至在南京、成都有一个研发中心,这就是我们说的会分部门。

对应到微服务来说怎么办呢?刚才说的小组就是我们的服务分组,刚才说的分层就是分部门,就是这样一个概念。我们的分层就是上层只能调用下层的服务,下层永远不能调用上层的服务。下层想去通知上层的时候,只能通过 queue 通知上层服务某些事件发生。所以通过这个方式我们把层级关系建立出来。

接下来是小组,小组是我会把很多微服务归成很多个小组,通过它的网关作为它唯一的出口。当别的服务想调用这个小组里面任何一个服务的时候,必须通过网关进入。这样对调用者来说,这个小组已经变得简单了,因为你只需要记住组名就可以了。通过这样的方式,三百个微服务可能简化成大概二十个小组,可能简化成四层。

image.png

分组分层概念

分组和分层怎么去做呢?这张图基本描述了我们分组、分层的概念。我们可以看到每一个组有一个接入层,这个接入层一般会有两个组件,一个是我们 zuul 的 API 网关,一个是 API 聚合器,API 聚合器这是一个可选的组件,但是一般我们都会有的。为了简化对整个服务的调用,让你对整个小组内部的服务是无感知的,只感觉到小组的存在,不感知到这个小组内每一个服务的存在。然后只能上层调用下层,基本上就是这样一个概念。   

(三)服务元数据

有了服务之后,我们要开始做服务的元信息的管理。当大家要去做服务治理的时候,没有服务的元信息,服务注册发现有什么用呢?如果只是用来解决 IP 地址的问题就太弱了。举个简单的例子,你可以把这个服务名变成一个域名。比如你用 docker 的时候,docker 可以在每个服务里面,有内置的DNS,把服务名自动转化成 IP 名称。所以真正的服务注册发现不是简单的帮你只解决一个服务名称到 IP 地址的影射关系,而是要把服务的所有元信息能够注册到服务注册中心,这些信息非常有用。

还是用刚才的组织架构来举例,当你是三百个人的规模的时候,想找某人,你会翻通讯录,而通讯录里面每个人的信息就相当于是服务的元信息。服务上层是可以调用下层,下层不可以调用上层,那如何去管控呢?当我们的开发人员达到了几百个之后,你认为通过人为的手段制定规章制度,可以避免这件事情的发生吗?或者当我们说我们要求服务 A 依赖于 B ,A 依赖于 C,但是不能依赖于 D,你认为所有的开发人员都会准确的执行这个策略吗?相信当我们一个研发团队达到几百号人的时候,这是根本不可能的,你的规章制度没有用,你必须在技术层面上把这些问题解决掉。

如何解决我说的服务的元信息呢?你在注册每一个服务,进行服务调用的时候,可以把这个服务的层级直接注册进去。举个例子,假设大家用的都是 ribbon,Spring Cloud ribbon ,可以写一些 strategy ,当你调用对方的时候,它会先验证 strategy,可以看被调用方的层级,先把它的元信息取出来,如果它的层级小于我当前服务的层级,就能调得通。反之,就调不通。这样服务的管理就简单了,就从技术手段上避免了这个问题。所以当你想要做服务治理,并且服务的数量达到一定规模的时候,一定要有很好的服务元信息。

(四)服务容器化

做容器化不是为了赶时髦,很多人说容器化好处的时候,我觉得都是一些伪优点。比如容器化之后部署时间可以加快,这个观点我一直不认同,我觉得会变慢。我现在自己把几百个微服务,把原来基于 ecs 方式改成 docker 的部署方式之后,部署时间是变慢的。而且更糟糕的是,容器化之后如果大家用的是 docker swarm 集群,它里面的应用是一个应用接一个应用的部署,部署完一个应用再部署一个应用。我原来一个 ecs,可以同时并行部署。

不同的微服务可以用不同语言写,这个也绝对不是优点,而是缺点。对于一共公司,我们一定是希望技术站是比较统一的,这样我们的人员才是容易招聘和管理的。如果一个人用 go 写服务,而公司只有两个人懂 go 语言,那么这两个人离职后,谁也交接不了他们的工作。所以很多技术的优点都是伪优点,包括大家用微服务,其实也慎重选择。

(五)挑战

微服务之后,对很多东西都提出了相当多的挑战。比如运维就是一个很大的挑战。我当时管运维团队的时候,运维有六个人,我们一套环境三百个微服务,一共一千五百个微服务,六个运维根本管不过来的。所以当你的自动化水平、标准化水平和你整个团队的技术水平没有达到一定的程度,你仓促的上微服务,一定是一场恶梦。

所以为什么要用容器化呢?要用容器化很简单,对我来说主要是两个目的,第一个目的是统一我们的环境。因为用了容器之后,我可以用 base image 很轻松的把环境变的一致。以前 linux 版本不一致,java 版本不一样,或者是有的机器上部署着一个 java 的调试工具,有的机器上不部署。每次上线看问题,发觉这台机器没有部署调试工具,再人工的拷上去,调试,极其费劲。所以通过容器化的方式我可以实现真正一致的环境。

第二个是实现真正的弹性伸缩。我五六年前开始把我们的服务做成弹性伸缩,但是那时候一台服务器上只部署一个服务,这样做弹性伸缩是非常容易的,但是现在因为我们机器性能越来越强大了,一台机器上部署一个服务就太费钱,所以实际的情况迫使你在一台机器上必须要部署很多的微服务。那么问题来了,虽然省下钱,但是部署留个微服务把 CPU 高了,其他服务就受影响了。所以有的微服务没写好,把机器搞挂了,其他服务就无缘无故受影响,这是当时用 ecs 的问题。用 docker 会好一些,因为 docker 可以实现隔阂,可以实现 ecs 的高效利用,这样可以帮助解决刚才这个问题。这样自动扩容就变的非常容易了,因为 docker 把这个 CPU memory 全部虚拟化了,一台机器上这个 docker 的容器只占用一个 cpu,或者占用 1g 的内存,做好限制。甚至有时候机器的利用率还比原来要高,用了容器之后,你只要限定用多少内存,用多少 CPU,其他的事情让容器的管理器去管理好。

二、过程

(一)CI/CD 流水线

image.png

CI/CD 流水线

我们的 CI/CD 是这样的,当你一提交代码,会通过一个 webbook 开始触发 Gitlab,它的优点是把整个 CI/CD 转换成一个代码。当 build 之后,会上传到两个地方,一个是 Gitlab 的仓库,一个是阿里云的镜像仓库。为什么要上传两个仓库呢?因为我不想把生产环境中的镜像,让普通人员能访问到和影响到。而且另外是一个外网的,而 Gitlab 的镜像仓库是内网的,我可以更快一点。所以我当时是决定把镜像的同时铺设到两个地方。

另外一个是部署的过程,也就是这里面写的 B 的过程,因为 A 相当于是一个 CI 的过程,B属于是一个 CD 的过程,CD 的过程我们是手动触发的。你在手动触发,部署阿里云容器服务,会自动拉取镜像,拉取到相应的机器上,就把这个服务开启起来。整个的 CI/CD 是很简单的,没有什么复杂的东西。只有一点是镜像管理我们用的是双镜像。

(二)镜像管理

作为代码的一部分,由开发人员来负责和维护创建 Dockerfile ,一个服务对应一个代码仓库、一个代码仓库对应一个镜像仓库。

(三)容器化后服务的组织方式

容器化后服务如何组织呢?我有几百个微服务,所以我们必须要对这些容器进行有效地管理。我们会一个服务对应一个容器,这个匹配关系很简单。还有多个服务构成一个组,前面已经讲了分组、分层的管理,一个组就对应一个应用,它可以把多个服务组成一个应用,在这个应用里面可以去设置它们服务之间的依赖关系。一个产品线做成一个集群,基本上这就是我们容器化之后,和原来的微服务的对应关系。

但问题就来了,刚才已经谈到,因为用了容器之后,容器虚拟化了一层网络出来。这个网络在它这个集群内或者这个应用内 IP 地址是互通的。但是像 docker swarm 这种东西,它应用内是帮你管理了,但是集群之间不是的,所以跨应用的时候当你访问的时候还是一个麻烦。Docker 之后,docker 有一层虚拟化出来的网络,这层网络有自己的IP地址,这层 IP 地址跟原来 ecs 机器的 IP 地址是完全不通的。这样麻烦就来了。

如果你的公司只有几个微服务一下迁移过去。但当你有几十个,几百个微服务的时候,因为你要逐步迁的,逐步迁一定是有些机器在 ecs 上,有一些在容器环境上,最后这两个环境一定要互通的,这是最讨厌的地方。所以在容器集群迁移的时候一定要把这个问题很好的解决,如果再加上服务注册发现之后就更麻烦了,所以下面就给大家介绍,在用了自己的服务注册发现的机制之后,该怎么样解决这些问题。

容器化之后要考虑三种情况,第一种是应用内,也就是组内容器之间的互通问题;第二种是应用之间的网络互通问题,或者说是服务调用问题;第三种是跨集群或者是 ECS 到容器之间的网络或者服务调用问题。我们来用网管的 load banlance 地址,而不是用它在容器内的地址,这样就能解决了跨网络的服务调用问题。这是我们在容器化过程中怎么样解决网络问题的方法。这个问题只有大家服务数量真的达到几百兆,要实现渐进式的迁移,是不可避免的。

image.png

应用内容器服务注册和发现

image.png

集群内容器服务注册和发现

image.png

容器环境和非容器环境的通信

(四)扩容和缩容

一旦用容器之后,自动扩容就变得非常简单了,你要扩容,就是改个 setting,把原来的两台改成三,按个按纽就好了。缩容是不简单的,你不知道那个机器是不是还在运行任务,有些时候它可能在跑,但你感觉没有任何请求进来,实际上它在那运行任务。这是你要根据你自己的业务特点,制定相应的缩容策略,这个要稍微麻烦一点,因为和你的业务是相关的。

我们说扩容、缩容,一般来说可以根据 CPU 、内存来扩容。实际上真正应用场景当中,不仅是一个 CPU 和内存。举个简单的例子,我有在跑大量的任务的节点,一些容器,这些我们往往用的是异步方式,用的 Q 的方式。特别是支付的场景,可能会把大量的请求都放在 Q 里面,一堆服务从这个 Q 里面取这些请求。这样的话避免类似于双十一的时候大量的支付请求过去,把你的 server 撑破,他们都是要放在 Q 里面,Q 队列里面不断的慢慢的去消费。这时候你就需要用到扩容了,你发现 Q 里面的 side 不断的在增加,从十个变成十万个,原来是十台容器还在慢悠悠的运行,你 Q 里面的消息永远消费不完的。所以实际上我们真正的扩容、缩容不仅仅要基于内存、CPU,还要基于很多像 Q、 side 的东西。一般像这样的东西你们都要写点小的程序,监控一下某个 Metric。通用的做法就是我们要监控的 Metric 都发到 Metric server 上面去,不是单独的监控。

(五)监控

容器化之后监控也变的相对来说麻烦了一些。因为你要监控两样东西,一是你要监控容器的宿主机,二是你要监控容器自身 CPU、内存,随时两套都得做。我管运维的时候把监控做了五六层。有物理层的监控,业务层的监控,APM的监控,日志的监控,调用链的监控,还有业务流程的监控,每个作用都不一样,但是都得去做。

麻烦的地方在于什么呢?比如我们写业务流程监控,写业务监控流程的时候我们肯定是 A 调用B,B 调用 C,C 调用 D,我们是用模拟业务的,模拟业务场景的,业务流程监控就是模拟业务场景,定期跑一些东西。特别像自动化测试,就是模拟业务场景。我们经常会出现一个什么状况:系统在公司里面的人谁也没有发现问题,突然外面的人报上来。我经常很尴尬,为什么每次出事故了都是外面的销售告诉我们出事故了,监控怎么没有监控到。所以后来我们做了业务流程监控,模拟业务场景,每隔两分钟或者每隔一分钟就跑一次,让它们一直跑,挂了,马上就知道了,后来我就发现永远比前端知道的要快。

(六)日志

我们通过 stdout 输出日志,全部到控制台。一个有趣的现象是日志输出到控制台和文件,我们经过测试到文件的性能要更好一点,所以我们这种做法会损耗性能。因此我们把日志写成异步,同步的日志和异步的日志输出性能相差很大。

(七)网络

还有一个坑大家在做容器化的时候一定要注意,这是 docker 自身的一个 bug。两个容器之间,因为 IP 地址是变化的,容器启动后,如何让其他容器知道这个 IP 地址呢?它是通过一种广播机制,通过 serf 协议发出去的。但是在广播的过程中,没有确认机制,它不管对方是否收到 IP 地址,因此问题就产生了,如果由于网络波动,其他容器没有收到这个 IP 地址,就会出现 A 服务无法调用 B 服务。可能当你的微服务数量少的时候,不会发现这种问题,但当你的微服务数量特别庞大的时候,这个问题会频繁出现,而且是 docker engine 自身的 bug 。最后我们想出了解决的方法,既然 serf 协议不可靠,我们自己通过 king 的方式,把一些地址发出去,确保其他容器收到我们的 IP 地址。

(八)无缝迁移到容器环境

因为我们有几百个服务,因此每次上线的时候,一旦出现故障,即使容器化带来了 10 个好处,那也是要挨骂的。所以在迁移过程中,我们总是小心翼翼,我们有两套环境并存, docker 和 ecs 机器。刚开始 docker 10%,ecs 90%,用上 2-3 天没有问题,流量会再切高一点,再用一段时间,流量再切,最后感觉没有问题了,流量全部切到 docker 上面。通过切流量的方式,保障我们的迁移是比较顺滑,不会出现故障。即使出现故障也没关系,把流量全部切回去, ecs 那套还在,这样就不会造成生产上的事故。

三、展望

无论你用 Spring Cloud 还是 Docker ,你要采用一个新技术就要付出的一定的代价,新技术往往会存在一些坑需要弥补。要想避免失败最好的办法就是不断的失败,我个人一直这么认为,避免失败,我说的第一个失败是大失败,第二个失败是很小的失败,就是不断的通过很小的失败,最终避免大的失败,这是一个正确的做法。

所以我最后想说的一句话就是,很多新技术就是这样,在用的过程中难免要踩很多坑,特别是越新的技术踩的坑越多,就是在不断踩坑的基础上,最终实现了你自己的一些目标。但是不必害怕。