前话部分:本人之前在聚美优品工作过一段时候,今天借此跟大家分享下它们在微服务架构方面的一些干货,下面开始正文部分。
当下Kubernetes事实上已成为容器届编排的标准,但对于围绕容器构建的周边生态却是各有千秋。聚美优品云平台专案从2017初开始调研到现在落地推广也有快两年的时间,虽然享受到了Kubernetes对容器标准化操作的红利,但实际上在推进过程中,改造原有的架构去适配容器环境,仍会有不小的挑战。本文将围绕聚美云平台内部实现的细节和在电商平台中应用的场景,希望给大家带来一个不同的设计和思路。
云平台第一版
聚美优品的云平台专案是从2017年上半年开始调研的,当时在面对基础设施管理这块我们主要面临两个选择,一个是业界比较成熟的OpenStack私有云架构,另一个是刚出来不久的Kubernetes容器方案。由于之前我们的运维团队有OpenStack相关的技术沉淀,而对于Docker方面的技术栈几乎为0,所以早期云平台选型时的方案是基于OpenStack Mitaka来做的。虽然云平台第一版做出来上线后确实为我们节省了一部分计算资源的交付时间,但是对于线上业务系统的交付运维仍然需要花大量时间去配置和除错。
云平台第二版
得益于Docker映象环境的一致性和Kubernetes排程的灵活性,所以很快我们在2018年初开始对云平台进行重构,主要为其引入了Kubernetes来解决我们电商大促期间的弹性资源排程问题和日常物理资源的管理问题。聚美云平台第二版又名Galaxy,直译过来代表星系,寓意群星闪耀,翻译成中文也可理解为一群出色的人。在Galaxy中,目前我们已经完成了对Kubernetes,OpenStack和公有云的资源对接,并有统一的管理页面提供给运维和研发团队使用。对于Galaxy的定位,它更像是一个公共的基础设施服务,让运维从传统繁琐的基础操作中解脱出来,将重心倾向于check业务层面的东西。
Galaxy主要支援如下功能:
统一的管理界面,同时支援多机房,多种资源的接入和管理;支援对Kubernetes内部资源的管理(名称空间,Deployment,DaemonSet、Service等);支援应用的健康检查和容器生命周期配置管理;支援应用的服务注册与发现;整合内部CI/CD平台,提高了运维效率;提供带许可权管理的WebConsole;支援API,提供外部服务呼叫。目前聚美Galaxy云平台执行在2个机房,下面资源层各分别部署了3套OpenStack和Kubernetes丛集,同时还支援腾讯云、阿里云和UCloud等三方公有云资源的接入,执行时的最大规模是2K+宿主机和2W+的容器例项。

讯号量管理
为了保证Docker中的程序足够轻量和简单,我们摒弃了使用传统的Sysvinit或Supervisor的方式来管理容器内部的1号程序。取而代之的是采用dumb-init这个工具来作为容器的1号PID。dumb-init是一个天生为轻量级容器而设计的程序管理器,它能够很好向下传递讯号量,并且还能够接管因子程序退出而无法找到其父程序的僵尸程序。对于容器执行时的挂起,我们采用是pause这个工具,通过Linveness探针来监控容器内程序的存活性。在处理容器内部子程序优雅退出的流程时,是通过trap这个命令来捕获terminal讯号量的。一个基础的容器启动指令码如下:

容器内部呼叫
由于容器内程序做的足够轻量,所以当一个Pod内的多个Container服务之间有内部呼叫时,我们优先使用网络套接字的方式处理。如果有的业务只能使用套接字档案的话,在Kubernetes的编排中,我们添加了一个emptyDir来临时存放套接字档案,然后通过挂载Volume的形式让每个容器能够正常发现套接字档案的路径的形式来实现服务间通讯,当然套接字档案的名称和路径需要和研发同事协商一致。例如下面这个样例主要就实现了,容器A和B中的服务通过读取/var/run/service目录下的套接字档案来实现过程呼叫。

容器启动与停止
因为Kubernetes对于Pod内容器的启动和停止是无序化的管理方式,这对于我们在管理Container起停之间的依赖也来了一些挑战。以目前我们PHP-FPM类的容器来举个例子,每个容器都会在启动阶段去check自己依赖的容器是否已经正常执行,如果检查失败则退出重新执行,直到依赖的从容器启动完成后自己才会进行接下来的启动逻辑。同理,容器在停止的时候,利用preStop这个特性在停止容器前sleep不同的时间来控制容器停止的顺序。


程式码容器
聚美优品的持续构建交付平台由于是自研的,和业界利用Jenkins生态构建的平台有很大差别,所以这里我仅举一个比较有代表性的例子来跟大家分享我们是如何在容器平台下解决程式码释出的问题。首先,我们在线上的deployment编排里面大量运用了Kubernetes中的initContainer容器,它能够在正常容器起来之前预处理一些东西,例如配置下载、环境的设定等等。我们通过initContainer提前将业务打包好的程式码下载解压到code volume中,等到业务容器启动时,code volume会随着容器的启动挂载到指定的程式码存放路径。code volume我们使用的emptyDir来存放程式码,它会随着容器的生命周期消失而销毁,所以这里我们会与研发协商好程式码目录中不会有任何需要持久化到本地的资料。

CNI
云平台的容器网络外挂是采用IPVLAN来实现内部通讯和外部通讯。IPVLAN是Linux核心4.2+版本新支援的一个网络虚拟化方案,其工作原理类似于Macvlan。IPVLAN主要提供三种工作模式L2、L3和L3S。我们主要采用的是L2模式,简单来说,在L2模式下,操作系统核心会在master网络卡上虚拟出多块网络卡分给Docker容器使用,而每一块虚拟网络卡都共享master网络卡上的Mac地址,但是每块虚拟网络卡是可以有独立的IP地址的。一个简单的基于IPVLAN构建的虚拟网络拓扑结构如下图:

IPAM
对于IPAM我们是通过host-local的方式来管理的,也就是说我们在宿主机上会预配置好一个固定给容器使用的IP地址段,而对于容器的网络规划主要是通过自研的容器网络管理平台来实现统一分配和管理。使用的逻辑如下,首先我们会在IDC机房提前规划好一批容器使用的网络,并在接入层交换机上配置好路由和访问规则,其次将容器的网络和宿主机的元资料录入到管理平台,平台会自动根据录入的元资料为宿主机分配一个全域性唯一的IP地址段,同时存入到Consul的KV当中。当新增加kubelet节点宿主机在初始化CNI的时候,就会从Consul中读取到自己所对应的值去渲染CNI的配置档案。

统一配置中心
对于容器中的应用配置管理这块,我们采用的是聚美自研的配置管理系统(Dove),它能为各业务系统(不限于业务系统)提供统一的配置管理平台和方案。主要解决目前我们各专案配置维护困难、容易失误、同步效率低、管理成本太高等问题。
Dove主要的技术特性如下:
极度松散耦合,不需改动原业务程式码即可整合或弃用配置管理系统。高效,无需额外开销,配置即本地开发语言物件。多环境统一管理配置分组许可权控制变更日志:可追溯历史内容及修改者。离线执行:配置获取后,即使配置服务下线也不会影响应用当前版本的执行。离线恢复:在无配置服务器线上的情况下,应用或其服务器重启可以自动回复上次的配置,继续正常执行。除错模式:使用除错的客户端(DEV环境),开发者可以在本地编写自己的配置内容来忽略服务端进行除错。
SOA服务治理
随着核心服务往容器化方向迁移,传统的通过填写IP地址管理服务上下线的运维方式也得跟着改变。首先我们面临的问题是,容器IP地址是随着容器的生命周期而存在,如果容器重启后IP发生变化,要求运维将新的IP配置到应用当中已经变得不再现实。所以在18年初,我们花了大量时间投入到SOA服务框架(Lark)的研发和落地当中,其中重点解决的问题之一,就是服务发现的问题。

lark-agent以Sidecar模式和业务容器共同组成一个Pod例项,当发起一个服务呼叫时,客户端会将RPC协议的请求资料传送至lark-agent,Lark根据协议中的服务名称将请求转发到后端状态正常的服务例项中去。经过一年时间的沉淀,我们已经将线上所有核心业务接入到lark平台当中,而带来的收益也非常明显,其中主要就包含:
为客户端和服务端增加Lark代理,将复杂的长连线协议隐藏起来,降低了不同语言间协议的适配成本;Lark主动向云端上报心跳,根据自定义策略报告服务器负载情况,把运维成本降到最低;实现与云端定时通讯,获取当前丛集执行引数,根据定制化的策略,智慧选择最优节点,拒绝“雪崩”情况的发生;自动剔除无法与云端进行心跳保持的节点,让上下线服务器,业务升级更加平滑;实现故障节点自动惩罚机制,连线类错误容错重试,将大大减少服务器“抖动”类问题。非SOA架构的服务
对于非SOA架构的服务,因为kube-proxy使用iptables转发的效率问题,我们并没有直接采用Kubernetes的Service的方案,而是选择了聚美已有的一套API服务注册平台。这套平台主要是基于Consul丛集、Consul-Template和Tengine/Haproxy来实现服务的管理。Consul丛集本身支援多资料中心模式,我们将consul-agent容器和业务容器共同组成一个Pod例项。当容器服务器被拉起来时,consul-agent会根据ConfigMap定义好的健康检查界面对内部服务发起请求,只有服务状态正常的容器才会被渲染至Upstream或Backend配置当中去。

服务注册与销毁
执行在容器中的服务注册和销毁主要依赖Kubernetes的Pod生命周期管理的postStart和preStop钩子来实现的。在容器上线前,利用postStart呼叫我们预先定义好的health指令码去检查业务状态,当退出码为0时,我们认为容器服务正常,便会将服务注册到服务管理平台。同理,在容器销毁前,利用preStop去呼叫health指令码触发服务下线。在Kubernetes的编排中如下示例:

日志采集
容器内日志采集的问题,业界一直存在两个大方向,一种是日志落盘后通过log-agent方式将日志发往服务端,另一种是日志不落盘,通过异步方式传送日志到服务端。这两个方案各有各的好处,这里我们不做讨论。聚美云平台使用的是第一种方案,即业务程式码通过聚美自研的日志元件MNLogger将正常日志和exception日志全部落地到服务器本地硬盘上,再由客户端发往至Logstash做日志过滤后转发至Kafka丛集。日志采集客户端采用的是log-courier,例项是跟随着业务容器在一个Pod里面执行。

容器Mount目录
对于在容器中,如何将日志持久化到硬盘上,我们采用的是hostPath的方式,将挂载一个宿主机的日志目录给容器使用,而在容器里面主要通过建立软连线的方式将业务日志写入Volume当中。


监控服务
聚美在云平台之前就已经有了自己完善的监控体系,其中就包含链路追踪、日志监控、效能监控和统一告警几大要素。而对于云平台的接入,我们除了要收集业务的异常日志外,还需要对容器效能和状态的指标有一个全面的收集。

这里我简单列举两个点跟大家分享。
容器的效能资料的采集,云平台主要利用Prometheus联邦配合服务发现来实现的。对于不同业务逻辑的我们划分了三种服务型别的Promethues例项,最终由上层Prometheus例项负责汇总所有Metrics。这里需要着重强调的是,大家在自己在开发和设计Metrics的Labels时,应保证遵循一个统一的命名规范,最好能够结合被采集例项的元资料命名。这样有助于后期大家在出监控图时候利用Label关联多项Metrics。对更新正在执行时日志容器的配置档案,我们自己研发了日志采集管理平台,使用者只需通过在平台上去更新日志采集路径、日志型别、kafaka topic_id等引数。引数提交成功后,平台会自动将其渲染成业务所需要的配置档案,并通过HTTP服务生成一个全域性唯一的URL提供给客户端下载。当客户端检查到配置档案更新就会下载最新的配置档案,并触发程序的reload机制。大促弹性扩容
聚美优品是一家以主打化妆品限时特卖的网上电商平台。众所周知,国内的电商平台每年都会有那么几天会推出"剁手"季的活动,例如刚刚过去的双11/双12购物节。那么该如何去满足公司在大促活动期间对资源的弹性需求呢,我这里简单发几条我们遇到的需求场景,希望起到抛砖引玉的作用引发大家思考:
扩容的资源需灵活排程容器,并支援跨机房的排程;容器大规模上线或映象更新,带来的映象pull洪峰问题;大促前需要业务承载能力进行压测评估,扩容资源会经历多次建立和销毁;电商活动一般会在整点迎来处理请求的峰值,甚至在某个时间点前端请求出现翻数倍的场景;对扩容资源的稳定和成本之间平衡出一个最优的结果。其实聚美云平台内部,机房的网络环境与三方云提供商已经做了VPC网络专线的互通,对于大促期间扩容的计算资源,我们通过定义内部的元资料服务来确定在云主机启动之后自己该注册到哪个Region的Kubernetes Master。如果资源元资料发生变动,可以很快的通过下发指令码的方式重新对资源进行注册。
关于第2点,映象洪峰问题,我们会提前将docker映象植入到云主机映象当中,这样当云主机在pull映象时只需拉取映象更新的差异层,可以节省大量流量,缩短容器上线时间。
对于第3、4、5点,我们主要利用Kubernetes的DaemonSet型别来满足容器的建立,排程和销毁需求。目前我们对于大促期间的容器使用策略是一主机一专案的机制,也就是说扩容的云主机上,我们只会排程一个专案的容器在上面执行,同时Pod的网络采用hostNetwork。这样主要有两个好处,一是排程足够简单,通过对Node新增专案标签的方式快速实现一个专案容器的上线。二是单容器例项可以有效避免因高并发来临时多容器间资源争抢问题引起滚雪球式的报错。另外还有一个不得不考虑的问题是云上容器网络选型问题。对于聚美私有云来说,公有云上的资源执行不是常态,所以我们会直接让容器使用主机网络与外部通讯以降低网络的开销。

大资料Yarn弹性资源
聚美优品大资料团队的服务经常需要在凌晨跑各种业务统计资料和其它离线资料,受限于硬件资源的扩充套件,导致可用资源非常紧张,经常会出现资料出不来或者很晚才出来的情况,严重影响了业务同学的进度和工作。与大资料业务场景相反,业务的资源在凌晨的使用率是很低的,所以在今年年中,我们尝试和大资料团队合作,将大资料现有Yarn服务容器化后接入到的聚美私有云平台来弥补资源紧张的状况。
这里着重强调的是在Yarn容器下线过程中如果有任务正在执行,就可能导致这些任务失败。具体来说,当AM失败并且在配置的重试次数内,整个Application可以重新开始执行;如果是分配给Application(MR型别)的非AM用的Container挂掉了,AM会为其重新申请Container在其他机器上执行。所以我们修改Yarn源代码让除了MR任务的Spark、Flink等任务不要分配到Docker容器中的Node上。还禁止了AppMaster分配到Docker节点中,避免因为下线Docker导致的任务异常。

结束语
总体来说,聚美云平台经过2年时间沉淀,能够满足我们大部分的日常使用场景。但是未来仍然有很多东西需要去持续优化和改进,例如对容器网络的QoS支援和动态的容器排程等等。
今天分享就到这里,后续还会分享聚美在运维方面的架构实践,尽请关注。





























