前言
最近在做云原生相关的业务,简单梳理了一下项目中云应用和云函数的实现,记录一下用到的技术以及知识点。
云函数/云应用
云函数/云应用都是使用knative实现的,同时安装istio组件进行流量控制,然后istio再通过配置envoy,最终实现对流量的控制。示例代码
本质上,云应用和云函数没有区别,都是使用的knative+istio实现的,只是云函数在空闲时,会缩容为0,而云应用最小副本数为1。这里还涉及到云函数冷启动问题,以及knative的触发模型问题。
云函数的特点
优点
- 可以通过cpu或者是内存,或者是concurrency扩容,动态调整运行实例数
- 不使用时,可以缩容为0个,不消耗额外的资源
缺点:
- 链接数问题。作为一种计算资源是合适且合理的,但是如果需要使用到数据库,如mysql,那么每个云函数实例都会和mysql建立一个链接,无法做到链接复用,同时如果扩容数较多,比如10w个,那么mysql可能链接会有问题。io可能成为性能瓶颈。
- 冷启动问题。由于可以缩容到0,那么第一个请求来的时候,需要动态创建pod提供服务,因此第一个请求的回包会特别慢,影响体验。同时,云函数的作用机理是接收到请求, 如果所有的pod都正在处理请求,那么就会新建一个pod来处理当前请求。
- 不适合做很复杂的业务逻辑,更加适合做任务。比如:用户上传的图片压缩,然后转存,音视频编解码,小程序二维码,机器人问答,图片识别,相似度匹配等单个小任务。
云应用特点
优点:
- 支持灰度发布。由于使用了knative和istio,因此支持不同版本部署,可以按照一定规则,如header tag匹配或者是版本权重等分配不同版本不同流量。
- 支持扩缩容。和云函数一样,支持cpu,memory,concurrency扩容。
- 没有冷启动问题
缺点:
- 扩容带来的问题1:单体应用自身使用到的锁,事务等,由于扩容的影响,需要修改为分布式锁或者事务,否则可能会出现死锁,或者是数据异常的情况(比如单体应用一次事务操作,可能会因为另一个事务导致数据不正确,业务中使用到的锁,不能全局生效)。因此,可能对业务有一定的影响。
- 扩容带来的问题2:实现分布式锁或者分布式事务,可能引入额外的组件。比如需要引入一个redis或者zookeeper来实现分布式锁。而分布式事务,则会引入复杂的逻辑。
- 扩容带来的问题3:callback回调类型的业务可能会受到影响。比如微信支付完成,微信回调了一次,pod处理很耗时还未完成,因此没有回包,那么如果微信再次回调,请求到另一个pod上,就可能会导致重复消费,需要一定的业务逻辑处理才可避免。
- 扩容带来的问题4:发布订阅类型的业务可能会受到影响。发布订阅类的业务,由于扩容,对于同一个主题的订阅者数量等于pod实例数,那么这些pod都可以被通知到,因此需要做额外的逻辑判断重复。
构建镜像
Knative的组件Serving,Eventing,Building,后来Building逐渐演变为Tekton组件,成为了CNCF的一员。因此这里使用到的就是云原生的CI/CD工具Tekton做为构建镜像的组件。在Tekton中有许多资源类型
Entity | Description | Chinese |
---|---|---|
Task | Defines a series of steps which launch specific build or delivery tools that ingest specific inputs and produce specific outputs. | 定义了一系列步骤,这些步骤可以启动特定的构建或交付工具,以摄取特定的输入并产生特定的输出 |
TaskRun | Instantiates a Task for execution with specific inputs, outputs, and execution parameters. Can be invoked on its own or as part of a Pipeline. | 携带指定的输入输出和执行参数实例化一个Task,可以单独调用,也可以作为Pipeline的一部分被Pilepine调用 |
Pipeline | Defines a series of Tasks that accomplish a specific build or delivery goal. Can be triggered by an event or invoked from a PipelineRun. | 定义一系列的Task以完成构建或者发布的目标,可以被一个事件拉起,或者从一个PipelineRun调用 |
PipelineRun | Instantiates a Pipeline for execution with specific inputs, outputs, and execution parameters. | 携带指定的输入输出和执行参数实例化一个Pipeline |
PipelineResource | Defines locations for inputs ingested and outputs produced by the steps in Tasks. | 定义“任务”中的步骤所摄取的输入和产生的输出的位置 |
Run (alpha) | Instantiates a Custom Task for execution when specific inputs. | 当特定输入时,实例化自定义任务以执行 |
常用的构建方案:
使用Pipeline+PipelineRun+PipelineResource的组合
- Pipeline定义任务流程,引用不同的Task,并定义Task的先后顺序等。比如从Git仓库或者三方存储拉取代码作为一个Task,编译构建作为另一个Task,而构建阶段使用比较常见的工具是kaniko
- PipelineResource定义不同资源,以供PipelineRun引用
- PipelineRun是启动入口,创建此类型资源,会按照Pipeline中定义的task步骤开始构建过程。
使用Task+TaskRun的组合
- 使用Task定义一个任务模版,可以指定step标签定义步骤,Task本身是不占用资源的。
- 创建TaskRun,提供参数并运行,此时Tekton就会开启一个新的pod用来构建镜像,pod运行后状态会变为completed。
这两种方案都是可行的,区别是:
- 第一种方案,可以创建多个pod。可并行处理,以加快复杂构建的速度。
- 第二种方案,只会启动一个pod。不同的步骤对应不同的container,适合简单的,步骤较少的构建过程。
需要注意的点:
- Pipeline如果定义并引用了不同的Task,不同的Task就对应不同的pod,而runAfter的特性,应该是使用的k8s的顺序启动initcontainer的能力
- kanike和Docker in docker(DID)的方案是不一样的,本质上kanike会使用主机上的Docker进程进行构建,但是DID会在pod中启动另一个Docker进行构建。
构建流程简图:
Docker私有仓库
在构建阶段的最后一部就是需要将镜像推送到租户的私有仓库,关于自建docker registry私有仓,具体实现步骤可以参考我之前写的博客,传送门
日志系统
日志系统使用的是业界通用的EFK,即elasticsearch+fluentd+kibana。
大致原理:
- 使用k8s的DaemonSet类型,在所有node上都运行一个fluentd的pod
- fluentd pod监控node上的/var/log/containers目录,所有的容器的日志都会放在这里
- fluentd将这些日志处理后上传到elasticsearch,按天写入index(还可以指定index前缀)。参见文档1,文档2
- kibana作为一个展示层,从elasticsearch fetch数据
使用过程中的遇到的问题以及需要注意的点:
- 云函数和云应用在运行过程中会产生日志,而做日志系统的主要目的就是希望看到调用链中每一个阶段的输入和输出,因此request_id是一个很重要的数据,对于同一个请求,request_id在各个模块模块之间传递。
- elasticsearch中的数据需要定时清理,因为日志系统产生日志的速度会比较快,如果不及时清理,磁盘可能会写满从而导致异常。一般情况下,只会保存最近一个月的日志,而es并不像prometheus那样可以设置保留多久的数据,之前的数据会自动清理,因此需要手动清理。定时删除操作可以使用k8s的Job来实现。
- elasticsearch和prometheus一样,内部使用的都是UTC时间,要注意时间转换。
- 写入elasticsearch中的数据都是一条一条的,没有按照request_id进行聚合,而我们查询一般都是希望看见一个调用链的过程,而如果在查询时,使用aggs进行聚合,可能会导致内存占用非常高的情况(速度暂且不说,毕竟elasticsearch的性能是杠杠的),因此这里可以考虑,定时每天对过去一天的index中的数据按照reqeust_id进行聚合,然后重新写入原来的elasticsearch index中,这样在查询历史记录时,可以不用使用聚合,直接使用filtered进行匹配就可以了,内存的消耗也会降低。
- 查询不存在的index会报错index not found exception。可能的产生原因是:如果某一天没有生产日志(没有调用云函数或者云应用),那么index就不会自动创建出来(index会在第一次写入的时候创建),而如果查询了不存在的index,就会报错。可以通过设置setIgnoreUnavaliable = true来解决。
监控告警
使用kubernetes的兄弟系统:Prometheus。技术栈为:Prometheus+Grafana+Alertmanager。
大致原理:
- prometheus负责抓取和存储数据,在k8s中可以使用cadvisor监控系统业务的状态
- grafana是展示层,且支持不同类型的数据源
- alertmanager用作告警组件。将告警规则表达式设置到prometheus后,prometheus会按照policyGroup设置的interval频率进行评估,满足告警条件后,会讲告警事件推送到alertmanager,在alertmanager层复杂事件的路由,比如需要发送给多个用户,发送邮件还是webhook等。告警频率,有效时间范围等。
- 被监控的组件需要支持Promethues,并内置exporter,然后Prometheus配置抓取端点即可。
需要注意的点:
- Pushgateway组件需要手动删除数据,否则数据一直在。不过这里了解到貌似prometheus有直接推送的接口,而cortex可以直接推送到prometheus的。还不大了解cortex,后边有空了可以深入看看。
- 需要设置监控数据有效期,prometheus支持自动清理过期数据。
- Prometheus和Alertmanager都支持热加载机制,也即是提供了reload接口,当挂载到prometheus/alertmanager中的配置文件有变化时,可以通过reload接口刷新,以达到不重启服务动态加载配置的功能。(动态更新告警规则,告警路由等)
- Prometheus支持relabel和label drop,因此对于不必要的数据,可以适当丢弃,不过在开发阶段,建议不用设置,因为不知道哪些数据会用得上用不上,可以开发完功能了再加上。