cover_image

全语言栈上云实践——弹性伸缩架构改造

徐肇乾 百胜技术团队
2024年11月28日 09:54

前言-苦恼的春节假期


庞大复杂的智能外送系统

又到春节假期了,小张却不能安稳的度假,他作为智能外送系统的运维工程师,需要对一年一度最高流量的春节,进行系统扩缩容。

 

智能外送系统是即时配送业务承接平台,经过十多年的发展和沉淀,如今具有强大的实时订单处理和高效的资源调度能力,确保了在高峰期的大规模订单流量下依然能够稳健运行。

 

另一方面,庞大复杂的系统也给小张带来了苦恼。

• 业务量大:春节期间日均订单量上百万,午餐期间下单量每分钟近上万笔订单,核心查询接口每分钟数万次,同时在线骑手数万人。

• 业务复杂:拥有数十个业务服务,涵盖订单、骑手、餐厅、元数据等等模块;十多种调度算法服务,包括派单算法、爆单算法等十余种算法工程;还包括上百个定时任务,各类离线脚本等,种类繁多。

• 技术栈复杂:系统汇聚了PHPGoC++Python等多种技术栈,想要掌握其门槛不低;尤其是系统特有的中间件,更让人难以维护。

• 系统扩容复杂:系统拥有上千台服务器,却采用古典的虚机部署方式,每当想到扩缩容时,都令人头皮发麻。

• 无法应对突发流量:由于是虚机部署,当流量激增达到系统瓶颈时,只能采取限流手段,断臂求生。

• 机器资源浪费:由于外送业务的特性,系统流量流量集中在午餐期和晚餐期,其他时段尤其夜间,流量几乎没有,而机器资源却静静躺在那里,白白浪费。


扩扩缩缩的一天

为了最大化的利用机器资源,以及对应餐期的尖峰流量,在春节期间,小张需要每天早上午餐期到来前进行扩容,晚餐期结束后进行缩容,用人力换机器资源的利用率。

• 7点,准备扩容机器列表。

• 8点,启动物理机,运行服务检测脚本。

• 9点,更新注册中心与路由配置。

• 10点,完成服务验证,引入流量。

• 22点,回退注册中心、路由配置变更。

• 23点,下线所有服务,确认系统正常后,恢复流量,关闭物理机。

虽然流程小张已然熟稔,但仍需精准无误、分秒不差的执行。


破局之道就在其中

虽然小张的身影忙碌在扩容-缩容的循环中,但是他并没有迷失方向。在一次公司技术分享中,小张了解到公司架构团队提供了一套针对开发、部署、监控、告警统一的标准流程,提供了整套的解决方案。遗憾的是,目前这套解决方案里只支持javago技术栈。

小张所在的团队,经过努力完成了大部分核心业务的翻新,从phpc++翻新为javago技术栈。但是还有部分业务服务,由于时间和精力并没有翻新,如何解决剩余部分是系统能否蜕变的关键

 

随后,运维组提供了CMDB1.6发布标准,能够针对其他语言技术栈,提供了自定义部署的方式。

这也给小张团队指明了方向,于是大伙撸起袖子开始干起来,期望把复杂的外送系统所有服务,全部做成可伸缩的架构

终于等到你-弹性伸缩架构

制作业务服务镜像——大象装进笼子

第一步,编写Dockerfile是容器化。

PHPPython应用而言,将代码压缩至某一目录并添加启动脚本,是镜像构建的关键。而C++服务则需要先编译,将编译后的文件添加到镜像。

YAML
#选择一个干净的基础镜像
FROM {harbor_host}/ops/base

USER root
#创建服务的工作目录,保持和虚机目录一致
WORKDIR /opt/work_dir
#安装必要的软件包
RUN yum install wget -y && yum clean all
#添加服务代码包
ADD {业务代码} /opt/work_dir
#添加所需要的启动脚本
ADD start.sh /opt/work_dir/
#启动
ENTRYPOINT ./start.sh

这里遇到另一个问题:像PHP服务依赖第三方开源库,如何打进镜像?


制作三方开源库基础镜像——食物也丢进笼子

对于PHP服务,需要集成例如PHP-FPMNginx,并封装动态库如mysqliredis.so等,需要提前制作静态镜像包。借助Kubernetes Init容器特性,在主容器启动前,事先装载这些依赖。

Init容器是一种特殊容器,在Pod内的应用容器启动之前运行,Init 容器可以包括一些应用镜像中不存在的实用工具和安装脚本。

 

YAML
initContainers:
  - command:
      - sh
      - -c
      - set -ex; cp -rp /opt/data/* /app/dependencies ;
    image: dependency-image:latest
    volumeMounts:
      - mountPath: /app/dependencies
        name: app-volume

在编写Kubernetes部署文件时,通过init容器的方式加载基础镜像,然后创建共享目录/app/dependencies,将基础镜像的文件COPY到共享目录,主容器即可通过共享目录的方式加载到第三方开源库文件。

到这一步,服务可以正常启动了,但是服务如何被发现呢?


自动加载配置文件——“大象的使用手册

关于如何加载配置文件,小张团队讨论了几种方案,首先想到的Kubernetes自带的ConfigMap技术。

ConfigMap 是一种 API 对象,用来将非机密性的数据保存到键值对中。使用时, Pod 可以将其用作环境变量、命令行参数或者存储卷中的配置文件。

但是,由于业务服务配置文件数量非常多(上百个配置文件),而ConfigMap对多文件支持并不友好,只能放弃。最后采取了配置中心的方案。

所谓配置中心,就是专门用于存放配置的机器,提供配置文件下载的能力。

• Jenkins脚本,将服务所需配置文件从git拉取并打包,发送到配置中心所在机器。

• 配置中心机器开放url提供下载。

• 容器在启动时,从配置中心下载配置文件。

图片

下载配置脚本示例

Shell
# 下载配置文件到指定目录
wget -r -nH --no-parent http://${config_server_url}/$config_name.tar.gz -P /conf/

#解压配置文件到容器指定目录即可
if [ $? -eq 0 ]; then

  # 解压 config.tar.gz 到 config 目录中
  if [ -f $config_name.tar.gz ]; then
      tar -xzf $config_name.tar.gz -C /conf/config/
      rm -f $config_name.tar.gz
      echo "解压完成。"
  else
      echo "找不到 配置包 文件。"
      exit 1
  fi

  cp -rp /conf/config/* /opt/$model_name/
  chmod -R 775 /opt/$model_name
else
    echo "wget 下载失败。"
    exit 1
fi

 

构建注册发现中心——大象被发现

在容器启动后,自动完成IP注册是关键。通过Kubernetes的生命周期管理,可以使用postStartpreStop事件来完成容器的自动注册与销毁。

postStart:这个回调在容器被创建之后立即被执行。

preStop:在容器因 API 请求或者管理事件而被终止之前,此回调会被调用。


YAML
env:
  # 定义环境变量,赋值容器IP信息
  - name: POD_IP
    valueFrom:
      fieldRef:
        apiVersion: v1
        fieldPath: status.podIP

lifecycle:
  postStart:
    exec:
      command: ["/bin/sh", "-c", "curl -X POST http://agent-service --data '{ \"ip\": \"$POD_IP\", \"port\": \"80\"}'"]

  preStop:
    exec:
      command: ["/bin/sh", "-c", "curl -X POST http://agent-service --data '{ \"ip\": \"$POD_IP\", \"port\": \"80\"}'"]

先通过status.podIP获取当前容器启动后自动分配的IP,并定义成环境变量。在postStartpreStop事件中,通过发送http请求,携带容器IP、端口信息到自定义的代理服务agent-service,在代理服务中即可按需求完成注册与销毁。

 

这里还有一些问题没有解决:

Q:在postStart事件中,如果容器启动后执行注册请求失败了,怎么办?

A:为了保证容器启动后一定能够注册成功,必须对请求结果进行判断:当注册请求响应失败时,销毁容器,让容器重启。

YAML
postStart:
    exec:
      command:
        - bash
        - '-c'
        - >
          response=$(curl -s -o /dev/null -w "%{http_code}" -X POST http://agent-service --data "{\"ip\":\"$POD_IP\",\"port\":\"80\"}");
          if [ "$response" != "200" ]; then
            echo "Request failed with HTTP status code $response";
            exit 1;
          fi

 

Q:如何保证容器的状态和注册发现中心保持一致?

A:借助euraka心跳检查机制,让容器定期向代理服务发送心跳请求,记录心跳时间;当超过阈值没有收到心跳请求时,认为容器下线,从注册中心删除该容器信息。

 

Q:如何兼容系统原来的注册发现模式。

A:这里先介绍下系统在虚机部署时的注册发现模式。

1. 上游请求如何命中服务

关键是NG路由层,提前配置好业务服务的IP地址,进而能够正常的转发。

2. 业务服务之间如何互相调用

每个业务服务所属虚机,同时部署了一个NA的服务,NA服务会定期请求SFNS服务,SFNS服务再去读取ETCD,获取所有业务服务的IP信息。

这里的另一个关键就是,运维小张同学,需要提前把虚机IP信息配置到NGETCD中,才能保证上述的服务发现。

很明显,这种方式高度依赖运维人员手工配置,当虚机资源需要扩容或缩容时,其耗时可想而知。

图片

更加遗留的是,NA服务已经和业务服务已经高度耦合,尤其是已经融合到C++程序的底层框架,短时间无法避免。

那么我们看看容器部署方式如何兼容这种方式,同时避免人工的干预。

 

首先,采用架构标准推荐的janus策略层替换NGjanus策略层能够通过janus管控平台,自动获取ETCD中的业务IP信息,避免人工的配置;

其次,保留业务服务->NA服务->SFNS服务->ETCD”这条链路,但是业务服务IP不需要人为通过web端录ETCD

这里就得依赖前面提到的代理服务agent-service

在容器启动后或销毁前,已经请求了代理服务agent-service,在agent-service中,将容器IP信息写入ETCD,并且key的格式保持和SFNS写入的格式一致即可。

 

图片

容器日志采集到ELK——收集大象的轨迹

除了容器服务本身的业务,还有一个至关重要的模块就是日志采集。容器服务产生的日志文件,如何收集到ES中?

有标准的采集方案:边车容器

边车容器是与主应用容器在同一个 Pod 中运行的辅助容器。 这些容器通过提供额外的服务或功能(如日志记录、监控、安全性或数据同步)来增强或扩展主应用容器的功能, 而无需直接修改主应用代码。

 

其配置内容如下:

YAML
containers:
  - name: log-collector
    image: {harbor_host}/ops/filebeat:5.6.16
    args: ["-c", "/opt/filebeat/filebeat.yml", "-e"]
    volumeMounts:
      - name: log-volume
        mountPath: /var/log/apps

配合ConfigMap配置Filebeat,确保日志文件路径正确,并将数据传输至ELK环节:

YAML
apiVersion: v1
kind: ConfigMap
data:
  filebeat.yml: |
    filebeat.inputs:
    - input_type: log
      paths:
        - /var/log/apps/*.log
      document_type: {your_es_index}
    output.kafka:
      hosts: [127.0.0.1:9092]

 

环境资源相关配置——大象一个舒适的环境

• 环境变量

为了兼容虚机部署时的环境变量,在Kubernetes的配置文档中也可以定义:

Shell
containers:
  env:
    - name: SERVER_NAME
       value: "LOCIC"

• 资源限制

通过设置CPU和内存限制,防止单个容器占用过多资源,影响系统整体性能。例如采集日志的容器filebeat,只需要配置少量的资源即可。

当你为 Pod 中的 Container 指定了资源 request(请求) 时, kube-scheduler 就利用该信息决定将 Pod 调度到哪个节点上。当你为 Container 指定了资源 limit(限制) 时,kubelet 就可以确保运行的容器不会使用超出所设限制的资源。

cpu: 0.01:表示占用0.01

memory: 0.05Gi:表示占用0.05G内存


Shell
resources:
  limits:
    cpu: 0.01
    memory: 0.05Gi
  requests:
    cpu: 0.01
    memory: 0.05Gi

• 健康检查

配置健康检查,确保应用容器运行正常,自动重启失败的容器。

tcpSocket:使用 TCP 套接字的方法来检查容器的健康状况。

port: 探活的端口

initialDelaySeconds:指定容器启动后多少秒开始执行 liveness probe

periodSeconds:定义执行 liveness probe 的时间间隔(以秒为单位)

failureThreshold:定义连续多少次检查失败后认为容器已经失效,并需要重启


YAML
livenessProbe:
  tcpSocket:
    port: 8080
  initialDelaySeconds: 60
  periodSeconds: 30
  failureThreshold: 3

 

后记-弹性伸缩让春节更轻松

经过团队的努力改造,智能外送系统的多个语言技术栈服务,已经实现弹性伸缩的架构,它带来的好处包括:

• 资源优化:动态调整资源分配,提升利用率,降低成本。尤其是外送业务,有明显的流量峰谷期,弹性伸缩的优势明显。

• 应对突发流量:自动扩展容量满足高峰需求,避免系统过载。

• 自动化管理:自动化部署,减少人工干预和运维压力。

• 快速响应需求:缩短部署和响应时间,加快业务创新。

• 灵活性增强:支持多种应用和技术栈,方便更新和扩展。

而对运维小张来说,在架构改造前,小张扩容需要做的事:

 

图片

而现在,小张只需要一行命令即可自由扩缩,终于可以安心享受春节假期。

图片

 

 


继续滑动看下一个
百胜技术团队
向上滑动看下一个