程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

文因干货 | 部署如何做到高可用?容器化部署K8s的实践经验分享

balukai 2025-03-20 12:21:29 文章精选 2 ℃

引言

你是否曾经面对过应用程序部署和管理的挑战?是否曾经想象过将容器化的应用程序无缝部署、自动扩展和高度可靠地管理?飞梭平台之前采用为docker方式,在应用的部署和管理方面存在一定的挑战,所以飞梭平台实践了K8s方式的部署


一个好的应用服务首先是应该是高可用的,如单点故障、不能自动扩缩容等情形不可能是高可用的,在目前容器化部署下K8s是一个通行的解决方案。今天分享的内容对往期谈到的“祖传屎山代码”也有帮助。

今日分享

  1. K8s的一些核心概念
  2. 应用在K8s中如何跑起来
  3. 开发中需要注意和思考的问题

业务上线初期,通常访问量较小,且大多数情况下只有单个节点在运行业务。随着业务发展,为提高服务性能并避免单点故障,需要将单体服务转变为集群传统运维可以通过重复执行业务上线流程来实现这一目标,部署一个反向代理软件实现负载均衡。在此过程中,集群机器应尽可能使用相同的操作系统和环境。

而在Kubernetes(K8s)运维中,初始部署时已经搭建了K8s集群。单节点发生故障时,K8s会自动检测到并将该节点上的Pod状态更改为unknown。同时,K8s还会自动在其他机器上启动指定数量的副本Pod。整个故障迁移过程中,无需运维人员参与,用户也不会察觉到服务异常,从而避免业务中断。此外,对于瞬时暴增的流量情况,K8s可以通过自动水平扩展,来自动调整副本数量,非常适合于流量波动大、机器资源紧张、服务数量多的业务场景。

让我们先来看一个 K8s 官方架构图,图中这个K8s集群是由一个master和两个node组成。

和大部分的布式系统有些类似,master 负责管理和协调集群中的工作;node节点中运行我们的应用;用户管理K8s集群是通过 kubectl 命令进行交互,master控制node是通过kubelet进行的。

图中“凸”出来的地方是分布式存储块一个独立的模块,存储默认是用 etcd 实现的。因为是模块化的设计,如有其他存储实现可灵活扩展支持。


根据架构图可以学习借鉴:

  • 只有API Server与存储通信,安全、隔离存储实现。实际业务场景中,比如常用的缓存服务,一般使用 Redis,如果抽象独立为缓存服务,实现层变更不影响使用者,对于使用者是统一的接口隔离具体实现。
  • API 的设计。在 K8s 中API 的设计是声明式的,我们一般用的可能是命令式的API,这两种各有优缺点,可根据实际的场景运用合适的方式。API也是一个面向对象的设计,在 K8s 里它的资源有多种,设计上是一个高内聚和低耦合的。其实高内聚低耦合是一个软件开发领域非常重要的概念,如果我们写代码能真正地践行这个高内聚低耦合思想,那写出来的代码一定不会差。
  • 状态机。K8s 里常有一些状态机,比如服务状态等,这些状态都是各自在某一个条件下会进行切换。它的控制逻辑只依赖于当前的状态,不需要再知道历史状态。这样对容错的处理更友好,例如服务宕机,集群恢复的时候它的处理会更好。

总结两个关键词容错性可扩展性


K8s中的资源对象

  • Namespace:命名空间,同大多数分布式系统里面的类似,资源隔离的作用。
  • Pod:K8s创建和管理的、最小的可部署的计算单元。Pod 所创建的是特定于应用的 “逻辑主机”,其中包含一个或多个应用容器,一般情况为一个容器。
  • Deployment ( StatefulSet ):Deployment 适合无状态的应用(随机的ID,Pod可互换),StatefulSet 适合于有状态应用(唯一的ID,Pod不可互换)。
  • Service:是将运行在一个或一组Pod上的网络应用公开为网络服务的方法,集群内访问为ClusterIP方式,集群外访问一般为NodePort方式。
  • Volume:挂载,在 K8s 里边有很多种的挂载方式,如常用的 hostPath(简单但不建议使用) 和 PVC(一个可伸缩的块存储层) 。
  • ConfigMap(Secret):ConfigMap用来将非机密性(Secret适合加密性数据)的数据保存到键值对中。可以将其用作环境变量、命令行参数或者存储卷中的配置文件。
  • Ingress:是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。


应用如何在 K8s 中跑起来?

第一步:基建先行

以上几种方式,在对应场景下选择适合的方式即可。

第二步:Yaml登场

工具:如果服务较多且之前使用的docker方式,可kompose convert一键生成,之后再次基础上稍作修改即可“出锅”。手写:怕写错可以看着模板样例(可见官方文档),一般需要配置的东西也不多。

主要的资源对象配置部分为Deployment、Service、Configmap等。建议一个服务一个文件方便管理,注意kompose工具转换结果每个资源。

另外可以使用helm工具管理,来看一个官方Yaml案例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80


部署实践中需要关注:

  1. 打镜像时,启动命令CMD写进去(做统一Dockerfile)。这样不管是在docker-compose的yml中还是K8s的yaml中配置都会更简洁一点。更进一步,dockerfile可以做个模板出来,也会更加规范。
  2. 建议应用的配置文件内置,需要配置参数环境变量传入。有时候会把配置文件挂载到容器里,但是配置文件在Jar包和镜像中没有,如果经常部署服务时很不方便。常用的一些开源项目也是传入一些必要的环境变量。配置文件内置,把必要配置变量通过环境变量传进去,整个部署就会变简单一点,如若需要也可进行配置文件挂载。
  3. 服务无状态,不在本地持久化存储。服务自身无持久化存储,多数情况使用外部的数据库或对象存储等。因为服务入有状态的话,就会额外的一些消耗。
  4. 应用的日志支持多种的输出方式。在不同的环境下输出日志的诉求可能会有所不同,在应用日志配置中根据active选择仅输出文件、仅输出控制台和两者都输出的日志处理方式。
  5. 微服务架构,配置文件集中为一个。以飞梭平台为例,飞梭是一个微服务的架构,每个服务在Nacos中有自己的配置文件,每个服务或多或少会用到数据库和中间件,将变化频繁、共性配置集中到一个配置文件,在每个应用中都引入该文件 。只需修改一个地方,减少冗余和维护成本。
  6. 中间件(Cache、MQ等)支持单机和集群。比如说缓存、 MQ等,实际应用中在不同环境下情况大有差异。整体上有单机和集群两种方式,更具体的集群也分不同形式。所以应用实现中需要考虑对单机和集群的支持情况,以及不同集群形式的支持情况。
  7. 集群内服务间调用、使用K8s里的DNS,按需对外暴露服务端口。仅集群内访问对应Service的类型为ClusterIP,仅集群内访问对应Service的类型为NodePort,这样即安全又规范。
  8. 建议进行资源配额限制。测试环境中可能会有,某个服务异常占用大部分CPU和内存,导致其他服务不稳定甚至异常。所以需要对每一个服务的资源配额进行限制,这样就不会因为某个服务异常,影响其他服务。
  9. 双架构包支持(x86、arm)。支持应用在不同的环境下运行,如K8s 集群x86和arm架构混合的环境就需要进行双架构支持。


开发中需要注意和思考的问题

幂等问题:常见解决方式有业务唯一标识符、乐观锁、状态机等,大家应该也很熟悉,这里不再赘述,注意根据具体的业务场景去选择合适的处理方式。
并发的问题:说起并发分单机并发和分布式并发两种情况。后边有两个典型的案例,一个是 Redis 上的,一个是 MySQL 上的。


定时任务:在应用服务中我们经常会用到定时任务,对与Java应用一是使用Spring里的注解,二是通过公共组件如开源的xxl-job等或者其他内部相关组件。
分布式一致性和分布式事务:这两个单独拿出来都是一个很大的话题,这里就不做叙述了。


两个经典并发案例

第一个是在Redis下遇到的,第二个是在MySQL下遇到的,这两个例子都把复杂的那些业务逻辑、业务背景完全地给剔除了,纯度已达99.9+%。

Redis 场景下的并发问题


先看Redis下的例子,如上图左侧是一个输入数据的例子,输入数据是通过 Kafka 的消息接收的,要把数据进行处理聚合出来的效果右边的样子,按照这一种维度把数据给聚合起来。输入的数据量级之大,服务也是多节点部署是一个典型的分布式场景,应该如何去处理?

其中一种方法如下图,有本地缓存层分布式缓存层

本地缓存层,主要为了快速处理收到的数据。当非核心业务数据量大时,需要本地缓存这一层快速地处理,不能阻塞其他的业务流程。同时,本地缓存层过滤完需要处理的数据会异步消息发出来。
分布式缓存层,主要为了接收本地缓存层发出的消息,使用Redis数据集中和去重进行汇聚。分布式缓存层使用批量方式接受消息,消息处理完后数据写入Redis。读取数据又写入流程发出的延时消息触发。


写入逻辑:(1)scard看key的元素数量n,(2)sadd向key中写入数据,(3)如果n为0,说明是第一条数据发送延时消息。
读取逻辑:从key中将数据拿出来写入MySQL,是通过lua脚本操作的。


如上图情形就出现了并发问题,问题就在于

写入线程,先进行scard得到元素数量n且为大于0,之后读取线程把现在这一批数据读走了,紧接着写入线程把数据加进去了,就不会在有延时消息发出,但是数据在不断的写入Redis。后续的数据就不会再更新到数据库中。
问题是出在哪?在写入线程操作的时如上述分为了三步,操作不是原子的就出现了并发问题,将写入流程对Redis的操作改用lua脚本即可解决问题(lua脚本内容如图)。


MySQL 场景下的并发问题

有两个几乎是同一时间点执行的定时任务,执行第一个定时任务线程主要是关注C3、 C4 字段,第二个线程它主要是关注C5、 C6 字段。其中两个线程操作时都把所有字段查出来,之后逻辑处理修改完关注的字段后执行更新操作。

问题出现的情形如上图所示,可以很清楚地看到,简单说就是并发操作数据库字段值被覆盖了,然后就业务异常了。

解决方式如下:

  • 每个执行流程只处理本流程关注的字段
  • 操作数据库通过for update加锁(悲观锁)
  • 数据库加版本号字段实现乐观锁

以上就是今天的主要内容,可以看到有不少地方行成规范标准,就会减少很多问题不论是代码还是其他方面。

最近发表
标签列表