多NUMA机型的容器内存就近访问加速

ack-koordinator以数据安全的方式将绑核应用远端NUMA上的内存迁移至本地,提高本地访存命中率,为内存密集型的工作负载提供更好的访存性能。本文介绍如何使用内存就近访问加速功能,并验证其对密集型应用性能的提升。

索引

前提条件

  • 已创建ACK Pro集群,且集群版本为1.18及以上版本。具体操作,请参见创建Kubernetes托管版集群

  • 已通过kubectl连接Kubernetes集群。具体操作,请参见获取集群KubeConfig并通过kubectl工具连接集群

  • 已安装ack-koordinator组件(原ack-slo-manager),且组件版本为v1.2.0-ack1.2及以上版本。关于安装ack-koordinator组件的具体操作,请参见ack-koordinator(ack-slo-manager)

    说明

    ack-koordinator适配了原resource-controller组件的所有功能。如果您正在使用resource-controller,请您先卸载resource-controller,再安装ack-koordinator。关于卸载组件的具体操作,请参见卸载resource-controller

  • 已确保多NUMA机型为神龙裸金属ecs.ebmc、ecs.ebmg、ecs.ebmgn、ecs.ebmr、ecs.ebmhfc、ecs.scc等五、 六、七、八代机型。

    说明

    内存就近访问加速功能尤其对ecs.ebmc8i.48xlarge、ecs.c8i.32xlarge、ecs.g8i.48xlarge的八代机型提供更好的支持。关于ECS实例规格族,请参见ECS实例规格

  • 已确保应用运行在多NUMA机型上,且已通过CPU拓扑感知调度等功能实现绑核。更多信息,请参见CPU拓扑感知调度

费用说明

ack-koordinator组件本身的安装和使用是免费的,不过需要注意的是,在以下场景中可能产生额外的费用:

  • ack-koordinator是非托管组件,安装后将占用Worker节点资源。您可以在安装组件时配置各模块的资源申请量。

  • ack-koordinator默认会将资源画像、精细化调度等功能的监控指标以Prometheus的格式对外透出。若您配置组件时开启了ACK-Koordinator开启Prometheus监控指标选项并使用了阿里云Prometheus服务,这些指标将被视为自定义指标并产生相应费用。具体费用取决于您的集群规模和应用数量等因素。建议您在启用此功能前,仔细阅读阿里云Prometheus计费说明,了解自定义指标的免费额度和收费策略。您可以通过账单和用量查询,监控和管理您的资源使用情况。

内存就近访问加速功能的优势

多个非一致性内存访问NUMA(Non-uniform memory access)架构下,当内存与CPU不在同一个NUMA时, 进程在跨NUMA读取远端内存时需要经过QPI总线,相对于内存与CPU在相同NUMA的本地内存访问场景,跨NUMA场景访存耗时更多。远端内存的分布会降低应用的本地访存命中率,影响内存密集型容器应用访存性能。

以内存密集型应用Redis为例,Redis对所有CPU亲和时,进程在运行过程中可能发生NUMA的切换。此时,以多NUMA机型ecs.ebmc8i.48xlarge为例,部分内存分布在远端NUMA上,内存分布如下方左图所示。redis-server的内存全本地分布相对于内存随机分布和全远端分布的场景,在压测中,QPS性能分别提升10.52%和26.12%,如下方右图所示。

重要

本文中提供的测试数据仅为理论值(参考值),实际数据以您的操作环境为准。

QPS

为提高访存性能,ack-koordinator提供容器内存就近访问加速功能。对基于CPU拓扑感知调度等功能实现绑核的应用,在确保数据安全的前提下尽可能地将远端内存迁移至所在NUMA,且迁移过程中无需中断业务。

使用场景

内存就近访问加速功能的使用场景如下。

  • 工作负载为内存密集型,例如大型内存数据库Redis。

  • 运行在配置英特尔®DSA(Data Streaming Accelerator)数据流加速器硬件的机器上,DSA可以提升内存就近访问加速功能的速度,并降低CPU消耗。

使用内存就近访问加速功能

步骤一:通过Policy开启内存就近访问加速功能

您可以通过以下两种方式开启内存就近访问加速功能。

为单个Pod开启内存就近访问加速功能

使用以下命令,为已绑核的Redis Pod开启内存就近访问加速功能。

kubectl annotate pod <pod-name> koordinator.sh/memoryLocality='{"policy": "bestEffort"}' --overwrite

policy参数取值说明:

  • bestEffort:立即执行单次内存就近访问加速。

  • none:关闭内存就近访问加速功能。

为指定QoS下所有Pod开启内存就近访问加速功能

  1. 使用以下内容,创建ml-config.yaml文件。

    apiVersion: v1
    data:
      resource-qos-config: |-
        {
          "clusterStrategy": {
            "lsClass": {
               "memoryLocality": {
                 "policy": "bestEffort"
               }
             }
          }
        }
    kind: ConfigMap
    metadata:
      name: ack-slo-config
      namespace: kube-system
  2. 执行以下命令,检查kube-system命名空间下,是否存在configmap/ack-slo-config

    为避免影响原有的QoS配置,需要进行ack-slo-config的检查操作。

    kubectl get cm ack-slo-config -n kube-system
    • configmap/ack-slo-config不存在,则使用以下命令创建。

      kubectl apply -f ml-config.yaml
    • configmap/ack-slo-config已存在,则使用以下命令,配置内存就近访问加速。

      kubectl patch cm -n kube-system ack-slo-config --patch "$(cat ml-config.yaml)"

步骤二:通过Event查看内存就近访问加速结果

使用以下命令,查看内存就近访问加速的结果。

kubectl describe pod <pod-name>
  • 预期输出1:

      Normal  MemoryLocalityCompleted  6s    koordlet  Container <container-name-1> completed: migrated memory from remote numa: 1 to local numa: 0
    Container <container-name-2> completed: migrated memory from remote numa: 0 to local numa: 1
    Total: 2 container(s) completed, 0 failed, 0 skipped; cur local memory ratio 100, rest remote memory pages 0

    预期输出1completed表明,内存就近访问加速成功。Event记录各个容器内存的迁移方向及本地化后Pod的总内存页分布比例。

  • 预期输出2:

      Normal  MemoryLocalityCompleted  6s    koordlet  Container <container-name-1> completed: migrated memory from remote numa: 1 to local numa: 0
    Container <container-name-2> completed: failed to migrate the following processes: failed pid: 111111, error: No such process,from remote numa: 0 to local numa: 1
    Total: 2 container(s) completed, 0 failed, 0 skipped; cur local memory ratio 100, rest remote memory pages 0

    预期输出2completed表明,部分内存就近访问加速失败,例如迁移过程中,进程终止。Event记录失败进程号。

  • 预期输出3:

      Normal  MemoryLocalitySkipped  1s    koordlet  no target numa

    预期输出3表明,没有远端NUMA时(例如机型为非NUMA架构或进程分布在所有NUMA时),Event告知内存就近访问加速已跳过。

  • 预期输出4:

    其他非预期错误,Pod报出MemoryLocalityFailed的Event,并提示Message错误信息。

(可选)步骤三:开启多次内存就近访问加速

说明
  • 若已进行过单次内存就近访问加速,开启多次内存访问加速前,请确认前一次加速已完成。

  • 多次内存就近访问加速时,只有当内存就近访问加速的结果发生改变,或本地化后内存页分布比例发生超出10%的改变时,加速的结果才会记录在Event中。

执行以下命令,开启多次内存就近访问加速。

kubectl annotate pod <pod-name> koordinator.sh/memoryLocality='{"policy": "bestEffort","migrateIntervalMinutes":10}' --overwrite

migrateIntervalMinutes为两次内存就近访问加速的最小执行间隔,单位为分钟。参数取值说明:

  • 0:立即执行单次内存就近访问加速。

  • > 0:立即执行内存就近访问加速。

  • none:关闭内存就近访问功能。

验证内存就近访问加速功能对应用性能的提升

测试环境

  • 一台多NUMA架构的物理机或虚拟机:用于部署内存密集型应用的测试机,本文测试选用ecs.ebmc8i.48xlarge和ecs.ebmg7a.64xlarge机型。

  • 一台能访问测试机的压测机:用于进行服务压测。

  • 测试的应用:4 Core 32 GB的多线程Redis。

    说明

    若选用的测试机型规格较小,可改用单线程Redis并降低CPU和内存规格。

测试流程测试流程测试步骤

  1. 使用以下YAML内容,创建并部署Redis应用。

    以下代码中redis-config字段的内容,请根据测试机型规格修改配置。

    ---
    kind: ConfigMap
    apiVersion: v1
    metadata:
      name: example-redis-config
    data:
      redis-config: |
        maxmemory 32G                  
        maxmemory-policy allkeys-lru
        io-threads-do-reads yes        
        io-threads 4                   
    ---
    kind: Service
    apiVersion: v1
    metadata:
      name: redis
    spec:
      type: NodePort
      ports:
        - port: 6379
          targetPort: 6379
          nodePort: 32379
      selector:
        app: redis
    ---
    apiVersion: v1
    kind: Pod
    metadata:
      name: redis
      annotations:
        cpuset-scheduler: "true"        # 通过CPU拓扑感知调度实现绑核。
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:6.0.5
        command: ["bash", "-c", "redis-server /redis-master/redis.conf"]
        ports:
        - containerPort: 6379
        resources:
          limits:
            cpu: "4"                  # 根据测试机型规格修改配置。
        volumeMounts:
        - mountPath: /redis-master-data
          name: data
        - mountPath: /redis-master
          name: config
      volumes:
        - name: data
          emptyDir: {}
        - name: config
          configMap:
            name: example-redis-config
            items:
            - key: redis-config
              path: redis.conf
  2. 模拟内存增压。

    1. 往Redis中写入业务数据。

      参考以下Shell脚本,通过管道方式向redis-server批量写入数据。在以下脚本中,<max-out-cir><max-out-cir>用来控制写入的数据量。本文测试用例在4 Core 32 GB的Redis应用中写入数据,本次参数设置为max-our-cir=300max-in-cir=1000000

      说明

      数据量较大时,数据写入时间较长。为提高写入速度,建议在测试机本地进行数据写入。

      for((j=1;j<=<max-out-cir>;j++))
      do
              echo "set k$j-0 v$j-0" > data.txt
              for((i=1;i<=<max-in-cir>;i++))
              do
                      echo "set k$j-$i v$j-$i" >> data.txt
              done
              echo "$j"
              unix2dos data.txt  # pipe方式需要dos格式文件。
              cat data.txt | redis-cli --pipe -h <redis-server-IP> -p <redis-server-port>
      done

      在写入过程中,对Redis所在的NUMA部署高内存占用的混部业务(例如FFmpeg)进行内存增压,模拟Redis在运行过程中,因内存压力突发导致内存漂移到其他NUMA的现象。

      在测试机上,可使用numactl --membind命令,限制部署业务内存所在的NUMA。

    2. 使用以下stress命令进行增压。

      其中<workers-num>用于分配内存的进程数,<malloc-size-per-workers>为每个进程分配的内存大小。

      说明

      由于Redis处于绑核状态,在本地内存未达到100%时,内存会优先写入到本地。请根据节点当前负载情况设置workers-nummalloc-size-per-workers参数。各NUMA内存负载情况可通过numactl -H命令查询。

      numactl --membind=<numa-id> stress -m <workers-num> --vm-bytes <malloc-size-per-workers>
  3. 在内存就近访问加速前后进行redis-benchmark压测并对比。

    1. 内存就近访问加速前压测

      数据写入完成后,在压测机上使用redis-benchmark对测试机redis-server进行适中压力及高压力压测。

      1. 执行以下适中压力压测命令,并发数为500。

        redis-benchmark -t GET -c 500 -d 4096 -n 2000000 -h <redis-server-IP> -p <redis-server-port>
      2. 执行以下高压力压测命令,并发数为10000。

        redis-benchmark -t GET -c 10000 -d 4096 -n 2000000 -h <redis-server-IP> -p <redis-server-port>
    2. 为Redis开启内存加速访问功能

      1. 执行以下命令,为Redis进行内存就近访问加速。

        kubectl annotate pod redis koordinator.sh/memoryLocality='{"policy": "BestEffort"}' --overwrite
      2. 执行以下命令,查看内存就近访问加速结果。

        若远端内存量较大,内存就近访问加速需要一定时间。

        kubectl describe pod redis

        内存就近访问加速完成后,预期输出:

          Normal  MemoryLocalitySuccess  0s   koordlet-resmanager  migrated memory from remote numa: 0 to local numa: 1, cur local memory ratio 98, rest remote memory pages 28586
    3. 内存就近访问加速后压测

      重复执行中压力和高压力压测命令,进行内存就近访问加速后压测。

结果分析

本文测试分别采用ecs.ebmc8i.48xlarge和ecs.ebmg7a.64xlarge机型的测试机,对内存就近访问加速前后进行多次压测取平均值,数据对比如下。

重要

采用不同的工具进行测试,测试结果会存在差异。本文中的测试数据仅为ecs.ebmc8i.48xlarge和ecs.ebmg7a.64xlarge机型的测试机获得的测试结果。

展开查看ecs.ebmc8i.48xlarge机型测试结果分析

  • 内存分布对比

    内存就近访问加速前,Redis内存分布如下方左图所示,本地内存页占比为43%。内存就近访问加速后,Redis内存分布如下方右图所示,本地内存页占比为98%。加速1

  • 压测结果对比

    • 500并发场景

      测试场景

      P99 ms

      P99.99 ms

      QPS

      内存就近访问加速前

      3

      9.6

      122139.91

      内存就近访问加速后

      3

      8.2

      129367.11

    • 10000并发场景

      测试场景

      P99 ms

      P99.99 ms

      QPS

      内存就近访问加速前

      115

      152.6

      119895.56

      内存就近访问加速后

      101

      145.2

      125401.44

    以上压测结果表明:

    • 适中并发(500并发):P99无变化(时延小,变动不明显),P99.99提升14.58%,QPS提升5.917%。

    • 高并发(10000并发):P99提升12.17%,P99.99提升4.85%,QPS提升4.59%。

展开查看ecs.ebmg7a.64xlarge机型测试结果分析

  • 内存分布对比

    内存就近访问加速前,Redis内存分布如下方左图所示,本地内存页占比为47%。内存就近访问加速后,Redis内存分布如下方右图所示,本地内存页占比为88%。加速2对比

  • 压测结果对比

    • 500并发场景

      测试场景

      P99 ms

      P99.99 ms

      QPS

      内存就近访问加速前

      2.4

      4.4

      135180.99

      内存就近访问加速后

      2.2

      4.4

      136296.37

    • 10000并发场景

      测试场景

      P99 ms

      P99.99 ms

      QPS

      内存就近访问加速前

      58.2

      80.4

      95757.10

      内存就近访问加速后

      56.6

      76.8

      97015.50

    以上压测结果表明:

    • 适中并发(500并发):P99提升8.33%,P99.99无变化(时延小,变动不明显),QPS提升0.83%。

    • 高并发(10000并发):P99提升2.7%,P99.99提升4.4%,QPS提升1.3%。

结果说明:在一定内存压力下,应用的数据写入到远端内存,本地内存占比影响了访存性能。在绑核基础上进行内存就近访问加速后,在两种压测场景下的Redis时延和吞吐指标都得到一定的改善。