作者简介
马孟起,携程后端开发专家,对操作系统和网络技术有浓厚兴趣。
一、背景
随着携程国际化战略的开展,为了给海外用户提供更好的服务,携程国际机票有很大一部分数据来源于世界各地的海外供应商和平台,在美国、德国、新加坡等全球众多的海外站点部署业务服务。相比自建私有云,购买设备和组建运维团队,公有云是企业出海的一个更好选择。
但该怎么上云,如何充分利用云原生的能力,是每个企业服务上云前面临的问题,其实我们可以遵循在业界已有的成熟云原生标准。所谓的云原生是一组最佳实践和方法论,指导我们在云环境下构建可伸缩、高可用、松耦合的应用,更快速和低成本运行服务,享受它带来的红利。
云原生本身是一个非常宽泛的概念,本文主要分享携程国际机票在上云实践中关于构建系统和成本优化的一些经验。
二、上云经验
2.1 基础设施即代码
要让我们的应用能够在云上部署,首先需要构建基于云原生的基础设施。传统的手工构建费时费力,虽然可以通过一些自动化脚本取代手工操作实现构建,但是如果整个基础设施的构建过程逻辑复杂,会带来脚本的维护工作量和排查调试等新的问题。
在云原生背景下,我们遵循 Infrastructure as Code(基础设施即代码),应用除了自有的代码仓库外,还有对应的基础设施IAC仓库,包含了应用依赖的基础设施和周边系统的配置文件。引入版本仓库控制后,团队可以像管理自己的代码一样管理基础设施,包括追溯到任意版本分支复现问题,一致性的环境配置,所有对基础设施的变更也更容易追踪和review。
另外我们还需要类似Terraform的自动化编排构建基础设施的工具帮助管理基础设施的生命周期。Terraform对基础设施编码的声明式配置思想和Kubernetes如出一辙,我们只需要在配置文件里描述期望的基础设施配置,避免了复杂的过程命令式脚本开发维护,剩下的编排构建工作就交给Terraform实现。此外声明式的配置文件有更好的可读性,简单的最终结果一目了然。
对于基础设施系统,秉承让工程师专注于自己最擅长的任务 —— 为客户提供业务创新和可行的解决方案,摆脱繁重的运维工作,我们会倾向于尽可能选择托管版的Kubernetes服务。根据我们的实际经验,搭建一个生产级别可用的Kubernetes集群整个耗时在分钟级别。
基础设施作为代码后,就可以集成在CI/CD流水线里,实现自动化持续部署。无论是基础设施还是业务应用的CI/CD流水线,出于公司安全合规的考虑,从工程师提交变更,编译,自动化测试,打包镜像这些和CI相关的步骤在公司内网执行;之后的镜像会通过代理推送到相应云平台的私有仓库,通过类似kubectl、Terraform等一些编排工具的客户端发送请求到云端,远程执行CD部分的工作。
业务代码的CI/CD流水线
2.2 日志
基础设施之上除了运行业务应用外,还有支撑业务服务的周边系统,例如日志和监控。
日志系统同样使用了托管版的Elastic Search,简化诸如硬件预置、软件安装和修补、故障恢复、备份和监控等管理任务,同时提供了多个可用区的高可用性。
上云后,应用可能会由于弹性伸缩经常会调度到不同的计算节点,伴随着调度的切换,本地的日志也会被销毁,因此需要实时把日志采集到统一的存储服务中,同时要求日志采集功能具备扩展性和适配性。
业务应用内部直接推送日志的方案虽然简单,但应用和日志收集功能互相耦合,对日志功能的维护带来不便,同时日志的采集开销也会影响到应用自身的运行。我们采用以 Daemon Set 方式在每个计算节点上部署日志采集代理,收集该节点上所有应用日志,随后按自定义的规则加工处理日志数据,输出到Elastic Search,在前端Kibana展现。应用只需要简单地将日志发送到stdout和stderr,完全解耦日志功能和业务应用。
2.3 监控
由于之前公有云平台并没有提供托管的监控系统,所以我们只能自己来搭建。监控系统选型几乎没有什么纠结,使用了几乎是云原生领域监控事实标准的Prometheus + Grafana。但是原生的Prometheus本身只支持单机部署,存在单点故障,同时存储空间受限于本地磁盘,随着时间的推移,监控数据的增加,单机Prometheus的性能终究会达到瓶颈。
Prometheus官方建议根据服务维度拆分,携程本身在上云前已经实现了微服务架构,单个Prometheus实例配置成仅采集指定的几个微服务指标,在一定程度上实现了水平扩容。虽然这个方案具备可行性,但是需要配置多个不同的yaml文件搭建多个Prometheus去发现多个不同服务的监控数据,增加了工程师的运维负担。
我们使用Kubernetes Operator来简化这部分工作。在Kubernetes的支持下,管理无状态的微服务已经变得比较简单,内置组件Deployment可以在无需附加操作的情况下,就可以管理应用的生命周期。
而对于数据库、监控系统等有状态的服务,就需要做一些额外的管理工作,这部分的工作又牵涉到具体应用相关的运维知识,具有资深经验的大牛可以把这些运维技术知识编写成自动化的Operator,借助Kubernetes的能力,像操作Kubernetes内置资源一样简单地运行和管理这些复杂的有状态应用,减少工程师的运维成本。
Prometheus operator通过自定义资源类型CRD来简化Prometheus部署,使用了namespace selector 简化了监控目标服务的发现,每个Prometheus负责收集特定namespace里服务的监控数据。
Prometheus operator
虽然Prometheus可以多实例部署,但视图层Grafana查询数据需要配置多个数据源,这些数据源相互独立,不能聚合统一到全局视图。另外,我们希望能长期存放至少3个月的历史数据,数据全部放在本地磁盘存在高昂的存储成本和灾备迁移成本问题。我们引入了Thanos组件,解决以下几个核心需求:
1)Thanos Sidecar 定期2小时从Prometheus服务收集数据上传到远程对象存储(AWS的S3),降低丢失数据的风险和历史数据存储成本。而且像S3这样的对象存储本身有多副本高可用特性,降低了容灾成本。
2)Grafana只需要查询Thanos的全局接口,Thanos从多个Prometheus实例和对象存储拉取数据去重聚合。
3)此外Thanos Compact组件可以对特别久的历史数据downsample和压缩,进一步减少存储成本。
同时,Prometheus operator也封装了Thanos相关的集成运维工作,简单修改几个配置就能把Thanos作为sidecar组件接入。
通过Grafana + Prometheus Operator + Thanos,一个高可用和高扩展的监控系统就搭建好了。
Thanos架构
三、成本优化
上云的成本也是重要考量的标准。很多人会问,从IDC迁移到公有云后,成本会减低吗?如果只是直接把IDC使用方式照搬到公有云上,对资源依赖的总量没有变化,那自然成本也不会有多大的改善。
每个上云团队都会面临成本管理的挑战,从技术的角度看,我们可以优化应用提高资源利用率避免浪费,同时利用公有云的按需付费、不用补付费的弹性特性。
3.1 优化计算资源成本
3.1.1 弹性伸缩
以计算资源成本为例:计算实例成本 = 实例运行时长 * 实例价格。如果只是简单粗暴把本地机房的运行模式套用到云上计算,云服务计算资源的费用是高于本地机房的。所以我们需要充分利用云上按需收费的特性,减少闲置资源成本。实例的运行时长和Kubernetes集群内的服务数量,以及分配给这些服务的计算资源成正比。同时服务的数量又是和流量成正比。
机票业务场景存在突发的业务流量,比如临近节假日颁布的旅游政策,营销直播活动甚至是最近的疫情。云原生的弹性特性很好地利用合理的资源应对突发的流量。Kubernetes的HPA弹性架构会实时采集集群整体的负载指标,判断是否满足弹性伸缩条件和执行pod的伸缩。仅仅是pod的伸缩还不够,我们还需要在集群中使用Cluster Autoscaler组件,监控集群中由于资源分配不足无法被正常调度的pod,自动从云平台的实例池中申请增加节点。同时在业务流量下降后,Cluster Autoscaler组件也会检测集群中资源利用率较低的节点,将其中的pod调度到其他可用节点上,回收这部分闲置节点。
下图是我们对资源节点数(黄色曲线)的监控,黄色曲线因为流量增加出现过一段尖刺,如果不使用弹性伸缩,我们要保证集群能在峰值流量时稳定可用,至少要把集群的总节点数维持在红色虚线这个位置;使用弹性伸缩后,集群节点的平均数量就可以保持在更低的绿色曲线位置。
弹性伸缩案例
云原生的弹性特性不仅帮助减少资源使用成本,也提高服务对基础架构故障的容错率,在基础设施部分可用区中断不可用期间,其他可用区域会增加相应数量的节点继续保持整个集群的可用。
Kubernetes支持对pod容器所需的CPU和内存调整,找到一个合理的配额以合理的成本达到最佳的性能。所以我们在服务上云前会做一些接近真实环境的负载测试,观察业务流量的变化对集群性能的影响(业务周期性高峰和低峰的资源使用率,服务的资源瓶颈,合适的余量资源buffer应对尖刺流量等等)。既不会因为实际利用率过高导致稳定性问题,比如OOM或者频繁的CPU throttling,也不会因为过低浪费资源(毕竟,即使你的应用只使用了实例的1%,也要支付该实例100%的费用)。
3.1.2 竞价实例
某些云平台会把一些闲置计算资源作为竞价实例,比按需实例更低的定价出租,顾名思义,竞价实例的最终费用是按市场供需出价决定的。按照我们实际使用的体验,如果不是特别热门的机型,定价基本在按需实例费用的30%左右。低价的竞价实例自然有它的限制,云平台会因为按需实例池的资源不足会回收部分竞价实例,当然在回收前会提前通知到这些实例。例如AWS会提供Terminal handler组件在收到回收通知后提前把容器调度到其他可用的实例上。
此外配置了多种机型和不同可用区域的竞价实例池,进一步降低了竞价资源被回收概率,最后会有保底的按需实例池避免竞价实例完全不可用导致服务中断。
携程国际机票使用按需实例和竞价实例的混合部署,保证低成本和高可用。一些系统关键组件(比如Cluster Autoscaler),中断就会丢失数据的有状态服务(比如Prometheus)运行在按需实例。而对错误容忍度高,使用灵活无状态的业务应用运行在竞价实例上。通过Kubernetes的节点亲和性控制不同类型的服务调度到对应类型标签的实例上。
节点亲和性
3.2 数据存储成本优化
日志和监控的历史数据,我们会定期备份转移到更低成本的对象存储。
Thanos的Sidecar会定期2小时备份监控数据到对象存储。
我们创建周期性的定时脚本任务,把Elastic Search的日志数据打成快照文件,迁移到低成本的对象存储。通过Serverless方式运行定时任务。以AWS的Lambda为例,按照任务实际运行时长和请求次数计费,缺点是只能运行生命周期短的函数,但运行低频周期性且简单的定时脚本是非常合适的。
3.3 网络传输成本优化
网络部分的优化依赖实际业务场景。以携程国际机票业务为例,主要的数据传输量集中在查询场景(根据用户实际的搜索条件从海外各个供应商平台拉取机票相关的业务数据),和外部下游服务的流量比例是出少进多。一般服务和外网接口交互走的是云平台提供托管的NAT网关,对出网和入网流量收取相同的费用。但国际机票查询业务的场景更适用于只收取出网流量费用的收费模型,所以我们自建部署透明代理squid在能访问外网的子网段,运行在私有子网段的业务应用通过代理squid转发请求到外网供应商平台服务。
四、总结
本文通过携程国际机票在云原生的实践,分享了如何快速在云上搭建一套稳定高效的生产环境实现快速交付、智能弹性,以及在云上的一些成本优化经验。借助云原生体系实现了基础设施自动化,释放一部分的运维工作就可以更多地投入到业务迭代,更敏捷地响应业务需求迭代,通过监控和日志实现快速试错和反馈。
整体而言,上云涉及的方方面面实在太多了,限于篇幅很难全部讲清,希望本文的一些小分享能帮助到更多想上云的团队,少走弯路,拥抱云原生。
【推荐阅读】
- 携程 Cilium+BGP 云原生网络实践
- 10W+ K8s容器数量下,携程如何打造统一弹性调度体系
- 携程云原生基础设施演进之路
- 云计算时代携程的网络架构变迁