上篇文章主要聊的是流量和网络问题,这里我们探讨一下另外一个Knative的核心功能:自动扩缩容。本文只打算围绕一个核心的问题进行深入分析,即如何设计一个自动扩缩容系统,以及Knative又是如何实现的?
如何设计一个自动扩缩容系统 自动扩缩容其实是一个相对广义的概念,这里我们只关注服务副本数的自动扩缩容,集群扩缩容和VPA则不会涉及。
假设一下,如果让我们自研一个完善的自动扩缩容系统,会如何去实现呢?首先大致可以将要解决的问题抽象成以下几点:
有哪些Metrics数据来决定扩缩容? 如何采集这些Metrics数据? 如何设计一个合理的自动扩缩容算法? 在Kubernetes集群下的自动扩缩容,很多人马上会联想到HPA,如果基于HPA来设计一个自动扩缩容系统,会面临什么样的挑战?
1. 有哪些Metrics数据 HPA v1版本可以根据服务的CPU使用率来进行自动扩缩容。但是并非所有的系统都可以仅依靠CPU或者Memory指标来扩容,对于大多数 Web 应用的后端来说,基于每秒的请求数量进行弹性伸缩来处理突发流量会更加的靠谱,所以对于一个自动扩缩容系统来说,我们不能局限于CPU、Memory基础监控数据,每秒请求数RPS等自定义指标也是十分重要。
幸运的是,HPA V2版本已经支持custom Metrics自定义指标。 Custom Metrics其实只是一个Kubernetes的接口,实际Metrics数据的提供,需要额外的扩展实现,可以自己写一个(参考:https://github.com/kubernetes-sigs/custom-Metrics-apiserver)或者使用开源的Prometheus adapter。如果自己实现custom-Metrics,可以自定义各种Metrics指标,使用Prometheus adapter则可以使用Prometheus中现有的一些指标数据。
2. 如何采集Metrics数据 如果我们的系统默认依赖Prometheus,自定义的Metrics指标则可以从各种数据源或者exporter中获取,基于拉模型的Prometheus会定期从数据源中拉取数据。
假设我们优先采用RPS指标作为系统的默认Metrics数据,可以考虑从网关采集或者使用注入Envoy sidecar等方式获取工作负载的流量指标。
3. 如何自动扩缩容 K8s的HPA controller已经实现了一套简单的自动扩缩容逻辑,默认情况下,每30s检测一次指标,只要检测到了配置HPA的目标值,则会计算出预期的工作负载的副本数,再进行扩缩容操作。同时,为了避免过于频繁的扩缩容,默认在5min内没有重新扩缩容的情况下,才会触发扩缩容。
不过,HPA本身的算法相对比较保守,可能并不适用于很多场景。例如,一个快速的流量突发场景,如果正处在5min内的HPA稳定期,这个时候根据HPA的策略,会导致无法扩容。 另外,在一些Serverless场景下,有缩容到0然后冷启动的需求,但HPA默认不支持。
关于HPA支持缩容至0的讨论,可以参考issues(https://github.com/kubernetes/kubernetes/issues/69687),该PR(https://github.com/kubernetes/kubernetes/pull/74526)已经被merge,后面的版本可以通过featureGate设置开启,不过该功能是否应该由K8s本身去实现,社区仍然存在一些争议。
如果我们的系统要实现支持缩容至0和冷启动的功能,并在生产环境真正可用的话,则需要考虑更多的细节。例如HPA是定时拉取Metrics数据再决定是否扩容,但这个时间间隔即使改成1s的话,对于冷启动来说还是太长,需要类似推送的机制才能避免延迟。
总结一下,如果基于现有的HPA来实现一套Serverless自动扩缩容系统,并且默认使用流量作为扩缩容指标,大致需要:
考虑使用网关等流量入口来实现流量Metrics的指标检测,并暴露出接口,供Prometheus或者自研组件来采集。 使用Prometheus adapter或者自研Custom Metrics的K8s接口实现,使得HPA controller可以获取到具体的Metrics数据。 这样便可以直接使用HPA的功能,实现了一个最简单的自动扩缩容系统。但是,仍然存在一些问题,比较棘手的是:
HPA无法缩容至0,也无法实现工作负载的冷启动。 HPA的扩容算法不一定适用流量突发场景,存在一定的隐患。 我们接下来一起探究一下Knative的实现,以及思考为什么Knative要这么设计,是不是有更好更优雅的方案呢?
Knative的自动扩缩容实现 Knative相关组件 这里我们只关心数据面的组件: Queue-proxy 针对每个业务容器Knative都会自动注入一个sidecar容器,本质上是一个基于Golang的反向代理服务,主要功能是检测流量,暴露出RPS和concurrency数据供Autoscaler组件采集。另外,如果用户配置containerConcurrency,还会限制单个容器的并发请求数,超过并发数的请求,会被缓存下来放入队列,这也是Queue-proxy名称的含义。
Autoscaler Autoscaler是自动扩缩容的核心控制组件,主要功能是采集Queue-proxy的Metrics数据,然后对比配置的数据,计算出预期的副本数,最后进行扩缩容操作。
Activator Activator的引入,最开始的目的是在冷启动的时候,由于服务副本数为0,需要有一个组件来保持住请求,通知Autoscaler扩容对应的服务,然后再将请求发送至运行后的服务。除此之外,还承担了当流量瞬间突增的时候,缓存请求,等扩容完成后再代理发送至后端服务的功能。
有哪些Metrics数据? HPA的Metrics来源一般是Pod的CPU或者Memory,当然也支持自定义的Metrics数据,不过对于大部分在线业务来说,并非CPU密集型,而是IO密集型,这意味着单纯依赖CPU来自动扩缩容往往并不满足实际需求,可能工作负载接收的流量已经很大了,延迟已经很高了,但是HPA还没有帮我们扩容服务。
为了更好的满足这种情况,Knative KPA自动扩缩容则设计支持了请求并发(concurrency)和RPS(request-per-second)两种Metrics数据,相比CPU数据,这两者更贴近负载的load,更适合描述在线业务。数据源来自Activator组件和每个工作负载Pod,当然工作负载的Pod数据由Queue-proxy sidecar暴露出Metrics接口。
RPS比较好理解,在一个采集周期内(默认1s),来一次http request,计数加1即可,比如1s内检测到来了100个请求,那么RPS就是100。 Concurrency可以理解为一个采集周期内正在处理http request的数量,比如1s内有50个请求正在被处理则Concurrency为50。实际上Knative会记录request来的时刻和返回的时刻,如果1s内只有一个请求,并且处理了500ms即返回,则Concurrency为0.
本文基于笔者 2019 Golang meetup 杭州站分享整理
一、背景 云原生技术大潮已经来临,技术变革迫在眉睫。
在这股技术潮流之中,网易推出了轻舟微服务云平台,集成了微服务、Servicemesh、容器云、DevOps等,已经广泛应用于公司集团内部,同时也支撑了很多外部客户的云原生化改造和迁移。
在这其中,日志是平时很容易被人忽视的一部分,却是微服务、DevOps的重要一环。没有日志,服务问题排查无从谈起,同时日志的统一采集也是很多业务数据分析、处理、审计的基础。
但是在云原生容器化环境下,日志的采集又变得有点不同。
二、容器日志采集的痛点 传统主机模式 对于传统的物理机或者虚拟机部署的服务,日志采集工作清晰明了。
业务日志直接输出到宿主机上,服务运行在固定的节点上,手动或者拿自动化工具把日志采集agent部署在节点上,加一下agent的配置,然后就可以开始采集日志了。同时为了方便后续的日志配置修改,还可以引入一个配置中心,用来下发agent配置。
Kubernetes环境 而在Kubernetes环境中,情况就没这么简单了。
一个Kubernetes node节点上有很多不同服务的容器在运行,容器的日志存储方式有很多不同的类型,例如stdout、hostPath、emptyDir、pv等。由于在Kubernetes集群中经常存在Pod主动或者被动的迁移,频繁的销毁、创建,我们无法和传统的方式一样人为的给每个服务下发日志采集配置。另外,由于日志数据采集后会被集中存储,所以查询日志时,可以根据namespace、pod、container、node,甚至包括容器的环境变量、label等维度来检索、过滤很重要。
以上都是有别于传统日志采集配置方式的需求和痛点,究其原因,还是因为传统的方式脱离了Kubernetes,无法感知Kubernetes,更无法和Kubernetes集成。
随着最近几年的迅速发展,Kubernetes已经成为容器编排的事实标准,甚至可以被认为是新一代的分布式操作系统。在这个新型的操作系统中,controller的设计思路驱动了整个系统的运行。controller的抽象解释如下图所示:
由于Kubernetes良好的可扩展性,Kubernetes设计了一种自定义资源CRD的概念,用户可以自己定义各种资源,并借助一些framework开发controller,使用controller将我们的期望变成现实。
基于这个思路,对于日志采集来说,一个服务需要采集哪些日志,需要什么样的日志配置,是用户的期望,而这一切,就需要我们开发一个日志采集的controller去实现。
三、探索与架构设计 有了上面的解决思路,除了开发一个controller,剩下的就是围绕着这个思路的一些选型分析。
日志采集agent选型 日志采集controller只负责对接Kubernetes,生成采集配置,并不负责真正的日志采集。目前市面上的日志采集agent有很多,例如传统ELK技术栈的Logstash,CNCF已毕业项目Fluentd,最近推出不久的Loki,还有beats系列的Filebeat。 下面简单分析一下。
Logstash基于JVM,分分钟内存占用达到几百MB甚至上GB,有点重,首先被我们排除。 Fluentd背靠CNCF看着不错,各种插件也多,不过基于Ruby和C编写,对于我们团队的技术栈来说,还是让人止于观望。虽然Fluentd还推出了存粹基于C语言的Fluentd-bit项目,内存占用很小,看着十分诱惑,但是使用C语言和不能动态reload配置,还是无法令人亲近。 Loki推出的时间不久,目前还是功能有限,而且一些压测数据表明性能不太好,暂持观望。 Filebeat和Logstash、Kibana、Elasticsearch同属Elastic公司,轻量级日志采集agent,推出就是为了替换Logstash,基于Golang编写,和我们团队技术栈完美契合,实测下来个方面性能、资源占用率都比较优秀,于是成为了我们日志采集agent第一选择。 agent集成方式 对于日志采集agent,在Kubernetes环境下一般有两种部署方式。
一种为sidecar的方式,即和业务container部署在同一个Pod里,这种方式下,Filebeat只采集该业务container的日志,也只需配置该container的日志配置,简单、隔离性好,但最大的问题是, 每个服务都要有一个Filebeat去采集,通常一个节点上有很多的Pod,加起来的内存等开销不容乐观。 另外一种也是最常见的每个Node上部署一个Filebeat容器,相比而言,内存占用一般要小很多,而且对Pod无侵入性,比较符合我们的常规使用方式。同时一般使用Kubernetes的DaemonSet部署,免去了传统的类似Ansible等自动化运维工具,部署运维效率大大提升。所以我们优先使用Daemonset部署Filebeat的方式。 整体架构 选择Filebeat作为日志采集agent,集成了自研的日志controller后,从节点的视角,我们看到的架构如下所示:
日志平台下发具体的CRD实例到Kubernetes集群中,日志controller Ripple则负责从Kubernetes中List&Watch Pod和CRD实例。 通过Ripple的过滤、聚合最终生成一个Filebeat的input配置文件,配置文件里描述了服务的采集Path路径、多行日志匹配等配置,同时还会默认把例如PodName、Hostname等配置到日志元信息中。 Filebeat则根据Ripple生成的配置,自动reload并采集节点上的日志,发送至Kafka或者Elasticsearch等。 由于Ripple监听了Kubernetes事件,可以感知到Pod的生命周期,不管Pod销毁还是调度到任意的节点,依然能够自动生成相应的Filebeat配置,无需人工干预。
Ripple能感知到Pod挂载的日志Volume,不管是docker Stdout的日志,还是使用HostPath、EmptyDir、Pv存储日志,均可以生成节点上的日志路径,告知Filebeat去采集。
Ripple可以同时获取CRD和Pod的信息,所以除了默认给日志配置加上PodName等元信息外,还可以结合容器环境变量、Pod label、Pod Annotation等给日志打标,方便后续日志的过滤、检索查询。 除此之外,我们还给Ripple加入了日志定时清理,确保日志不丢失等功能,进一步增强了日志采集的功能和稳定性。
四、基于Filebeat的实践 功能扩展 一般情况下Filebeat可满足大部分的日志采集需求,但是仍然避免不了一些特殊的场景需要我们对Filebeat进行定制化开发,当然Filebeat本身的设计也提供了良好的扩展性。 Filebeat目前只提供了像elasticsearch、Kafka、logstash等几类output客户端,如果我们想要Filebeat直接发送至其他后端,需要定制化开发自己的output。同样,如果需要对日志做过滤处理或者增加元信息,也可以自制processor插件。 无论是增加output还是写个processor,Filebeat提供的大体思路基本相同。一般来讲有3种方式:
直接fork Filebeat,在现有的源码上开发。output或者processor都提供了类似Run、Stop等的接口,只需要实现该类接口,然后在init方法中注册相应的插件初始化方法即可。当然,由于Golang中init方法是在import包时才被调用,所以需要在初始化Filebeat的代码中手动import。 复制一份Filebeat的main.go,import我们自研的插件库,然后重新编译。本质上和方式1区别不大。 Filebeat还提供了基于Golang plugin的插件机制,需要把自研的插件编译成.so共享链接库,然后在Filebeat启动参数中通过-plugin指定库所在路径。不过实际上一方面Golang plugin还不够成熟稳定,一方面自研的插件依然需要依赖相同版本的libbeat库,而且还需要相同的Golang版本编译,坑可能更多,不太推荐。
引子——从自动扩缩容说起 服务接收到流量请求后,从0自动扩容为N,以及没有流量时自动缩容为0,是Serverless平台最核心的一个特征。
可以说,自动扩缩容机制是那颗皇冠,戴上之后才能被称之为Serverless。
当然了解Kubernetes的人会有疑问,HPA不就是用来干自动扩缩容的事儿的吗?难道我用了HPA就可以摇身一变成为Serverless了。
这里有一点关键的区别在于,Serverless语义下的自动扩缩容是可以让服务从0到N的,但是HPA不能。HPA的机制是检测服务Pod的metrics数据(例如CPU等)然后把Deployment扩容,但当你把Deployment副本数置为0时,流量进不来,metrics数据永远为0,此时HPA也无能为力。
所以HPA只能让服务从1到N,而从0到1的这个过程,需要额外的机制帮助hold住请求流量,扩容服务,再转发流量到服务,这就是我们常说的冷启动。
可以说,冷启动是Serverless皇冠中的那颗明珠,如何实现更好、更快的冷启动,是所有Serverless平台极致追求的目标。
Knative作为目前被社区和各大厂商如此重视和受关注的Serverless平台,当然也在不遗余力的优化自动扩缩容和冷启动功能。
不过,本文并不打算直接介绍Knative自动扩缩容机制,而是先探究一下Knative中的流量实现机制,流量机制和自动扩容密切相关,只有了解其中的奥秘,才能更好的理解Knative autoscale功能。
由于Knative其实包括Building(Tekton)、Serving和Eventing,这里只专注于Serving部分。 另外需要提前说明的是,Knative并不强依赖Istio,Serverless网关的实际选择除了集成Istio,还支持Gloo、Ambassador等。同时,即使使用了Istio,也可以选择是否使用envoy sidecar注入。本文介绍的时候,我们默认使用的是Istio和注入sidecar的部署方式。
简单但是有点过时的老版流量机制 整体架构回顾 先回顾一下Knative官方的一个简单的原理示意图如下所示。用户创建一个Knative Service(ksvc)后,Knative会自动创建Route(route)、Configuration(cfg)资源,然后cfg会创建对应的Revision(rev)版本。rev实际上又会创建Deployment提供服务,流量最终会根据route的配置,导入到相应的rev中。
这是简单的CRD视角,实际上Knative的内部CRD会多一些层次结构,相对更复杂一点。下文会详细描述。
冷启动时的流量转发 从冷启动和自动扩缩容的实现角度,可以参考一下下图 。从图中可以大概看到,有一个Route充当网关的角色,当服务副本数为0时,自动将请求转发到Activator组件,Activator会保持请求,同时Autoscaler组件会负责将副本数扩容,之后Activator再将请求导入到实际的Pod,并且在副本数不为0时,Route会直接将流量负载均衡到Pod,不再走Activator组件。这也是Knative实现冷启动的一个基本思路。
在集成使用Istio部署时,Route默认采用的是Istio Ingress Gateway实现,大概在Knative 0.6版本之前,我们可以发现,Route的流量转发本质上是由Istio virtualservice(vs)控制。副本数为0时,vs如下所示,其中destination指向的是Activator组件。此时Activator会帮助转发冷启动时的请求。
apiVersion:networking.istio.io/v1alpha3kind:VirtualServicemetadata:name:route-f8c50d56-3f47-11e9-9a9a-08002715c9e6spec:gateways:- knative-ingress-gateway- meshhosts:- helloworld-go.default.example.com- helloworld-go.default.svc.cluster.localhttp:- appendHeaders:route:- destination:host:Activator-Service.knative-serving.svc.cluster.localport:number:80weight:100当服务副本数不为0之后,vs变为如下所示,将Ingress Gateway的流量直接转发到服务Pod上。
apiVersion:networking.istio.io/v1alpha3kind:VirtualServicemetadata:name:route-f8c50d56-3f47-11e9-9a9a-08002715c9e6spec:hosts:- helloworld-go.default.example.com- helloworld-go.default.svc.cluster.localhttp:- match:route:- destination:host:helloworld-go-2xxcn-Service.default.svc.cluster.localport:number:80weight:100我们可以很明显的看出,Knative就是通过修改vs的destination host来实现冷启动中的流量保持和转发。
相信目前你在网上能找到资料,也基本上停留在该阶段。不过,由于Knative的快速迭代,这里的一些实现细节分析已经过时。
下面以0.9版本为例,我们仔细探究一下现有的实现方式,和关于Knative流量的真正秘密。
复杂但是更优异的新版流量机制 鉴于官方文档并没有最新的具体实现机制介绍,我们创建一个简单的hello-go ksvc,并以此进行分析。ksvc如下所示:
apiVersion: serving.knative.dev/v1alpha1 kind: Service metadata: name: hello-go namespace: faas spec: template: spec: containers: - image: harbor-yx-jd-dev.yx.netease.com/library/helloworld-go:v0.1 env: - name: TARGET value: "Go Sample v1" virtualservice的变化 笔者的环境可简单的认为是一个标准的Istio部署,Serverless网关为Istio Ingress Gateway,所以创建完ksvc后,为了验证服务是否可以正常运行,需要发送http请求至网关。Gateway资源已经在部署Knative的时候创建,这里我们只需要关心vs。在服务副本数为0的时候,Knative控制器创建的vs关键配置如下:
忽如一夜春风来,千树万树梨花开,云原生的浪潮伴随着云计算的迅速发展仿佛一夜之间,迅速侵袭了技术的每个角落。每个人都在谈论云原生,谈论云原生对现有技术的变革。
Kubernetes已经成为容器编排的事实标准,Servicemesh正在被各大厂商争先恐后的落地实践,Serverless从一个一直以来虚无缥缈的概念,到如今,也被摆在台面,有隐隐约约崛起的势头。
只是没想到,在后端闷头搞容器化、上Kubernetes、尝试Servicemesh的时候,Serverless却先在前端火了起来。从今年的GMTC全球前端技术大会上,Serverless主题的火爆就可见一斑。笔者也看过一些网上流行的讲Serverless的文章,观点大同小异,实践几乎没有,误解与谬论满篇飞。本文倒无意挑起争论,只是试图从一个云计算研发的视角出发,聊一聊我们眼中的Serverless。
诉求:为什么需要Serverless 前端为什么想要上Serverless,其实也很好理解,node.js的普及、前端工程化以及BFF的兴起,越来越多的前端需要关心服务的构建、部署、运维,服务的日志、监控报警等等,严重拖累了前端的开发效率,让前端花很多时间在服务器上排查问题,无疑是痛苦而低效的。
对于前端来说,最原始的诉求是,我不愿意管服务器等底层资源,哪台节点宕机了,麻烦不用通知我;流量太大了,服务需要扩容了,我也不想关心;我只需要写好代码,就可以自动部署到服务器上,代码有bug,能让我看日志和监控排查问题就行。
其实这也不单单是前端的梦想,很多后端或者数据类的研发,也有同样的需求。不过咋看很美好,但是仔细想想,有一些后端的业务很复杂,服务间调用关系以及各种特异化需求其实很难适用于Serverless,想完全不关心底层的服务器,有点困难。
所以,Serverless并非银弹,关键是看业务场景和需求,就算只有50%的业务适合,能解决这50%业务的问题,那也是了不起的成就。
解释:到底什么是Serverless Faas和Baas又是什么? 了解Serverless的同学,或多或少都听过Faas,Faas即Function as a service,一般都称为函数计算。
作为开发人员,只需要写一个函数,就可以在例如AWS Lambda等各种函数计算平台上运行起来,真正实现了对服务器的无感知,同时可以对外快速暴露API接口,可以基于函数级别的自动扩缩容,可以监听各种事件进行触发。
而且Faas结合云平台的webIDE,如果webIDE设计的足够好,可以给我们带来云平台上更方便的开发体验,结合云上的各种工具和生态,未来会有更大的想象空间。
不过,显而易见,以函数为最小粒度,有一些局限性。
微服务是以功能职责为划分,拆分成一个一个专注于特定功能和需求的服务,为了解决微服务之间的网络调用和流量管理,引入了很多服务治理等相关的功能和组件,可以想象一下,如果把服务模块再拆分为函数的粒度,函数之间的调用关系无疑会爆炸,再思考一下,如何把老的服务改造成Faas形态,如何复用函数之间的逻辑,如何管理大量函数代码,这无疑对开发者带来了很多困扰。
所以,Faas不太适合一般后台长期运行的web服务型应用,真正适合的是那些数据计算、批处理等业务,这些业务逻辑比较单一,运行完可以停止,而且更适合Serverless中基于事件触发的特性,冷启动的延时也无所谓。
Faas的一个基本特征是无状态,那实际上的数据或者状态该如何存储呢,所以说到Faas一般都会提及Baas,即Backend as a service,不过类似的Xaas的名词太多了,Baas这个名词看着就像是有人为了强行补充Faas没有干的活儿而起的。因此有些人粗暴的总结Serverless = Faas + Baas,当然如果你要强行认为Serverless就是函数计算,那这个也没有问题。
不过,我们的观点是:Faas只是Serverless的一种特例。在这个世界上,除了Faas,还有更多的无状态工作负载适合以Serverless的形态去运行。
Serverless的特性 除了服务的粒度不一样之外,无状态工作负载和Faas一般都具有以下Serverless的特性:
1-step deploy 既然是Serverless,开发者真正关心和面对的是代码层面,所以不管是函数还是一个代码工程,一键构建和部署是我们的终极期望。 Kubernetes生态下有各种CI/CD解决方案,但是缺乏更加一键式的工具可以帮我们将代码(函数)迅速转变成部署的服务。所以,一个足够好用的本地client工具、一个完善而高效的CI/CD平台很重要。对于Faas,可以让用户便捷的将函数部署到Serverless平台,对于无状态负载,则可以根据用户需求暴露一些构建的自定义配置和流程。
Automatically 在Kubernetes上一般服务实际的运行都或多或少的需要我们创建很多的Kubernetes资源,例如service、ingress等,而Serverless会做更多的自动化操作,以便更方便的提供服务。例如,Serverless平台会自动提供流量入口和路由,部署完成后可以迅速对外提供服务,同时提供类似蓝绿发布、灰度等流量管理等功能。
Auto-scale 毫无疑问,Kubernetes也有HPA可以提供自动扩缩容。不过,HPA敢让服务副本数缩为0吗?当然不敢,试想一下,如果服务的副本数为0,相当于不再运行了,用户的流量如何导入呢,用户连服务的接口都调不通了,HPA更没有metric数据来感知去扩容服务了。HPA无法缩容为0,对于某些短运行的计算类服务来说,是无法接受的,因为这样就不能真正的做到无服务,不实际运行时不占资源不计费。
当然Serverless可以做到,让服务在没有请求时自动缩为0,在有流量的时候从0启动,或者流量增大时快速的扩容,迅速应对流量的变化。
不过,还有一个Serverless业界都很关注的点,就是服务从0扩容为多副本时启动的延时时间,一般称为冷启动的问题。如果冷启动时间太长,对于用户的第一次请求肯定有很大影响,业内也有很多大厂在做一些优化。但是如果不是直接面向用户流量的服务,例如我只想跑个数据处理算法,其实也不在乎这几百毫秒的启动延迟,如果是类似前端的web服务,恐怕大部分人还是宁愿空跑一个单副本的服务,也不愿意冒这个风险吧。
Eventing Serverless的另外一个特征是基于eventing事件进行触发,事件实际上是一个比较抽象的说法,很多东西都可以理解为事件。例如,用户的请求可以认为是一个事件,git的webhook可以认为是事件,kafka上有了消息可以理解为一个事件,包括Kubernetes的各种资源操作等等都是。所以,其实事件触发我们并不陌生,我们的平时开发和设计架构里经常都会有意无意的使用到事件触发的机制,只是太过平常,反而没有人去注意和抽象出这么一个理念。
现在大家都在倡导云原生,很多服务都是往云上迁移和部署,事件触发机制在云上可以有更多的扩展性和想象力。例如,我们的Serverless应用可以监听云上的中间件或者基础组件的事件,通过这些事件,触发特定的Serverless应用,从而打通云上的Paas服务,实现云上服务的一体化。
总结下来,虽然目前Serverless很火但我们更应该静下心来思考,为什么会有Serverless的诞生,Serverless最原始的需求和驱动力在哪?是Kubernetes不够好用还是Servicemesh不够友好?
Kubernetes被认为是下一代的分布式操作系统,操作系统上必然会运行各种各样千奇百怪的程序,有的需要直面系统内核,有的只是提供用户更好的UI,不过,有一类程序可以以更便捷的方式去编译、运行,而提供这一切的工具与平台就是Serverless。 所以,Serverless其实只是一种云原生应用更为特殊的实现和表现方式,也有很多的应用并不适合以Serverless的方式去运行。无服务器固然是愿景,大量的封装和抽象让开发者无需感知很多东西,但这个宇宙运行的规律可能并非直白的线性系统,混沌和复杂性才是常态。如果有人告诉你,Serverless是所有应用的终极目标,那只能引用一句长者的话,too young, too simple。
适合Serverless的场景 基于Serverless的特性,我们也可以推导出比较适合Serverless的应用都有哪些:
前端、小程序、爬虫等 事件触发或定时的批量数据处理 ⼤数据、实时流处理、机器学习的场景 经常应对流量突发的推广活动等⽆状态服务 视频转码等处理服务 其实还有很多,不过需要指出的是,这些都能在我们常规的容器云平台上构建部署运行,只不过,有了Serverless更高层次的抽象和封装,我们可以更快的开发构建部署,服务可以有更好的运行姿态,从而一步步接近我们想象中的那个只用写代码,不关心服务器的美好愿景。
引子 如果有关注过Knative社区动态的同学,可能会知道最近发生了一件比较大的新闻,三大组件之一的build项目被人提了一个很残忍的Proposal(https://github.com/knative/build/issues/614),并且专门在项目Readme的开头加了个NOTE:
NOTE There is an open proposal to deprecate this component in favor of Tekton Pipelines. If you are a new user, consider using Tekton Pipelines, or another tool, to build and release. If you use Knative Build today, please give feedback on the deprecation proposal. 这个Proposal的目的是想要废弃Knative的build模块,Knative只专注做serverless,而将build模块代表的CI/CD功能全盘交出,让用户自己选择合适的CI/CD工具。Knative只负责将镜像运行,同时提供serverless相关的事件驱动等功能,不再关心镜像的构建过程。
虽然目前为止,该Proposal还在开放征求社区的意见,不过,从留言来看,build模块未来还是大概率会被deprecate。因为Knative build的替代者Tekton已经展露头脚,表现出更强大的基于kubernetes的CI/CD能力,Tekton的设计思路其实也是来源于Knative build的,现有用户也可以很方便的从build迁移至Tekton。
Tekton是什么 Tekton是一个谷歌开源的kubernetes原生CI/CD系统,功能强大且灵活,开源社区也正在快速的迭代和发展壮大。google cloud已经推出了基于Tekton的服务(https://cloud.google.com/Tekton/)。
其实Tekton的前身是Knative的build-pipeline项目,从名字可以看出这个项目是为了给build模块增加pipeline的功能,但是大家发现随着不同的功能加入到Knative build模块中,build模块越来越变得像一个通用的CI/CD系统,这已经脱离了Knative build设计的初衷,于是,索性将build-pipeline剥离出Knative,摇身一变成为Tekton,而Tekton也从此致力于提供全功能、标准化的原生kubernetesCI/CD解决方案。
Tekton虽然还是一个挺新的项目,但是已经成为 Continuous Delivery Foundation (CDF) 的四个初始项目之一,另外三个则是大名鼎鼎的Jenkins、Jenkins X、Spinnaker,实际上Tekton还可以作为插件集成到JenkinsX中。所以,如果你觉得Jenkins太重,没必要用Spinnaker这种专注于多云平台的CD,为了避免和Gitlab耦合不想用gitlab-ci,那么Tekton值得一试。
Tekton的特点是kubernetes原生,什么是kubernetes原生呢?简单的理解,就是all in kubernetes,所以用容器化的方式构建容器镜像是必然,另外,基于kubernetes CRD定义的pipeline流水线也是Tekton最重要的特征。
在云原生时代和容器化浪潮中,容器的日志采集是一个看起来不起眼却又无法忽视的重要议题。对于容器日志采集我们常用的工具有Filebeat和Fluentd,两者对比各有优劣,相比基于ruby的Fluentd,考虑到可定制性,我们一般默认选择golang技术栈的Filebeat作为主力的日志采集agent。
相比较传统的日志采集方式,容器化下单节点会运行更多的服务,负载也会有更短的生命周期,而这些更容易对日志采集agent造成压力,虽然Filebeat足够轻量级和高性能,但如果不了解Filebeat的机制,不合理的配置Filebeat,实际的生产环境使用中可能也会给我们带来意想不到的麻烦和难题。
整体架构 日志采集的功能看起来不复杂,主要功能无非就是找到配置的日志文件,然后读取并处理,发送至相应的后端如elasticsearch,kafka等。
Filebeat官网有张示意图,如下所示:
针对每个日志文件,Filebeat都会启动一个harvester协程,即一个goroutine,在该goroutine中不停的读取日志文件,直到文件的EOF末尾。一个最简单的表示采集目录的input配置大概如下所示:
filebeat.inputs:- type:log# Paths that should be crawled and fetched. Glob based paths.paths:- /var/log/*.log不同的harvester goroutine采集到的日志数据都会发送至一个全局的队列queue中,queue的实现有两种:基于内存和基于磁盘的队列,目前基于磁盘的队列还是处于alpha阶段,Filebeat默认启用的是基于内存的缓存队列。
每当队列中的数据缓存到一定的大小或者超过了定时的时间(默认1s),会被注册的client从队列中消费,发送至配置的后端。目前可以设置的client有kafka、elasticsearch、redis等。
虽然这一切看着挺简单,但在实际使用中,我们还是需要考虑更多的问题,例如:
日志文件是如何被filbebeat发现又是如何被采集的? Filebeat是如何确保日志采集发送到远程的存储中,不丢失一条数据的? 如果Filebeat挂掉,下次采集如何确保从上次的状态开始而不会重新采集所有日志? Filebeat的内存或者cpu占用过多,该如何分析解决? Filebeat如何支持docker和kubernetes,如何配置容器化下的日志采集? 想让Filebeat采集的日志发送至的后端存储,如果原生不支持,怎样定制化开发? 这些均需要对Filebeat有更深入的理解,下面让我们跟随Filebeat的源码一起探究其中的实现机制。
一条日志是如何被采集的 Filebeat源码归属于beats项目,而beats项目的设计初衷是为了采集各类的数据,所以beats抽象出了一个libbeat库,基于libbeat我们可以快速的开发实现一个采集的工具,除了Filebeat,还有像metricbeat、packetbeat等官方的项目也是在beats工程中。
如果我们大致看一下代码就会发现,libbeat已经实现了内存缓存队列memqueue、几种output日志发送客户端,数据的过滤处理processor等通用功能,而Filebeat只需要实现日志文件的读取等和日志相关的逻辑即可。
从代码的实现角度来看,Filebeat大概可以分以下几个模块:
input: 找到配置的日志文件,启动harvester harvester: 读取文件,发送至spooler spooler: 缓存日志数据,直到可以发送至publisher publisher: 发送日志至后端,同时通知registrar registrar: 记录日志文件被采集的状态 1. 找到日志文件 对于日志文件的采集和生命周期管理,Filebeat抽象出一个Crawler的结构体, 在Filebeat启动后,crawler会根据配置创建,然后遍历并运行每个input:
for _, inputConfig := range c.inputConfigs { err := c.startInput(pipeline, inputConfig, r.GetStates()) } 在每个input运行的逻辑里,首先会根据配置获取匹配的日志文件,需要注意的是,这里的匹配方式并非正则,而是采用linux glob的规则,和正则还是有一些区别。
matches, err := filepath.Glob(path) 获取到了所有匹配的日志文件之后,会经过一些复杂的过滤,例如如果配置了exclude_files则会忽略这类文件,同时还会查询文件的状态,如果文件的最近一次修改时间大于ignore_older的配置,也会不去采集该文件。