• 利用观测云实现业务数据驱动的弹性扩缩容


    背景

    在使用观测云对业务系统进行观测的过程中,除了可以实现业务系统的全面感知,我们还可以基于观测云数据处理开发平台 DataFlux Func ,结合故障模型对被测系统进行主动管理,例如弹性扩容或系统故障自愈,从而实现系统管理从观测到恢复的自动闭环。
    本文以 K8s 环境部署的 ruoyi 系统为例,演示一个简单的业务系统自动扩容过程,设定的故障自愈场景是“当故障发生时,尝试扩容发生异常的容器”。

    实施步骤

    对于示例场景,需要在 DataFlux Func 平台执行如下两步操作:

    • 首先,需要实现对 K8s 集群的访问和操作,获取对象应用容器当前的状态并执行扩容;
    • 其次,要将该处理过程发布为 API 接口,由观测云中心触发调用操作。

    配置 K8s 集群访问权限

    以脚本方式实现对 K8s api-server 的方式主要有三种:

    • HTTPS 证书认证:基于 CA 证书签名的数字证书认证
    • HTTP Token 认证:通过一个 Token 来识别用户
    • HTTP Base 认证:用户名+密码的方式认证

    本例使用 HTTP Token 认证实现访问,配置过程如下:

    Func 平台新建脚本

    登录 DataFlux Func 平台,创建新脚本并安装 K8s 官方脚本包

    pip3 install kubernetes
    
    创建账号并获取 token
    创建用户
    kubectl create serviceaccount demo-admin -n kube-system
    
    用户授权
    kubectl create clusterrolebinding demo-admin --clusterrole=cluster-admin --serviceaccount=kube-system: demo-admin
    
    获取用户 Token
    kubectl describe secrets -n kube-system $(kubectl -n kube-system get secret | awk '/ demo-admin /{print $1}')
    
    配置客户端连接
    1. configuration = client.Configuration()
    2. setattr(configuration, 'verify_ssl', False)
    3. client.Configuration.set_default(configuration)
    4. configuration.host = KUBE_API_HOSTS #ApiHost
    5. configuration.verify_ssl = False
    6. configuration.debug = True
    7. configuration.api_key = {"authorization": "Bearer " + API_TOKEN}
    8. client.Configuration.set_default(configuration)

    这样就做好了集群访问准备。

    官方库的说明可参考链接:GitHub - kubernetes-client/python: Official Python client library for kubernetes

    编写扩容处理脚本

    当故障发生时,我们需要通过某种方式获取到需要操作对象的相关信息,作为参数传递给 api-server ,从而实现对象的操作。该信息可以通过观测云中心告警通知的相关参数来获取:在调用 DataFlux Func 封装的 API 时,告警中心会将当前告警事件的详情作为参数下发,如果将需要的参数配置告警聚合条件,即可在函数平台一侧获取到需要的参数。
    本例我们执行kubectl scale命令进行扩容,需要获取的参数为 deployment 的名称和当前副本数。因此在告警配置时,需要选取deployment name作为聚合条件。

    注意: 若对应指标没有绑定我们需要的标签,例如这里在选取监控指标时,指标并未携带归属的deployment name,仅携带pod name,则需要多一步查询操作:基于告警下发参数进行查询,确定被操作对象(deployment)的参数。

    处理告警参数

    如下展示的是,观测云控制台下发到 DataFlux Func 的一个告警参数示例:

    1. {
    2. "Result": 3.8138525999999997,
    3. "date": 1685630030,
    4. "df_channels": [],
    5. "df_check_range_end": 1685629970,
    6. "df_check_range_start": 1685629910,
    7. "df_date_range": 60,
    8. "df_dimension_tags": "{\"namespace\":\"gc-dev\",\"pod_name\":\"ruoyi-system-7f49bc9cc9-dms9r\"}",
    9. "df_event_id": "event-2cfcdf99f7a4473f84967e88d14cecba",
    10. "df_event_link": "http://gc-studio.huawei.com/keyevents/monitor?time=1685629130000%2C1685630030000&tags=%7B%22df_event_id%22%3A%22event-2cfcdf99f7a4473f84967e88d14cecba%22%7D&w=wksp_9541fb20bb8a4115b596af0c689613d4",
    11. "df_event_reason": "\u6ee1\u8db3\u76d1\u63a7\u5668\u4e2d\u6545\u969c\u7684\u8ba4\u5b9a\u6761\u4ef6\uff0c\u4ea7\u751f\u6545\u969c\u4e8b\u4ef6",
    12. "df_exec_mode": "async",
    13. "df_issue_duration": 0,
    14. "df_issue_start_time": 1685630030,
    15. "df_language": "zh",
    16. "df_message": "CPU\u5229\u7528\u7387\uff1a3.8138525999999997 \n\u53d1\u751f\u65f6\u95f4\uff1a1685630030",
    17. "df_monitor_checker": "custom_metric",
    18. "df_monitor_checker_event_ref": "c240d31146e4e198990734486c27331e",
    19. "df_monitor_checker_id": "rul_dc585aee34c3466fa0981dd69aecf56a",
    20. "df_monitor_checker_name": "\u4f4d\u4e8e{{namespace}}\u4e2d\u7684Pod:{{pod_name}}\u4e1a\u52a1\u538b\u529b\u8fc7\u9ad8\uff0c\u8bf7\u5c3d\u5feb\u5904\u7406",
    21. "df_monitor_checker_ref": "acde60927d12cb3117f43009110dbb96",
    22. "df_monitor_checker_sub": "check",
    23. "df_monitor_checker_value": "3.8138525999999997",
    24. "df_monitor_id": "monitor_e4496b567ce04acb9565dc318b27412d",
    25. "df_monitor_name": "\u5f39\u6027\u6269\u5bb9demo\u64cd\u4f5c",
    26. "df_monitor_type": "custom",
    27. "df_source": "monitor",
    28. "df_status": "warning",
    29. "df_sub_status": "warning",
    30. "df_title": "\u4f4d\u4e8egc-dev\u4e2d\u7684Pod:ruoyi-system-7f49bc9cc9-dms9r\u4e1a\u52a1\u538b\u529b\u8fc7\u9ad8\uff0c\u8bf7\u5c3d\u5feb\u5904\u7406",
    31. "df_workspace_name": "gc\u6f14\u793a\u7a7a\u95f4",
    32. "df_workspace_uuid": "wksp_9541fb20bb8a4115b596af0c689613d4",
    33. "namespace": "gc-dev",
    34. "pod_name": "ruoyi-system-7f49bc9cc9-dms9r",
    35. "timestamp": 1685630030,
    36. "workspace_name": "gc\u6f14\u793a\u7a7a\u95f4",
    37. "workspace_uuid": "wksp_9541fb20bb8a4115b596af0c689613d4"
    38. }
    获取扩容参数
    获取异常 pod 的名称

    通过对上述告警参数字段进行处理,我们可以提取到本次发生异常的 pod 名称:

    1. #缓存告警ID
    2. dfEventId=kwargs.get('df_event_id')
    3. dfMonitorId=kwargs.get('df_monitor_id')
    4. #提取Pod_name和归属的ns
    5. eventPodName=kwargs.get('pod_name')
    6. eventNameSpace=kwargs.get('namespace')
    获取 deployment 参数

    再通过 K8s 接口查询 pod 归属的 deployment 名称及当前副本等信息,即可获取需要做操作的 deployment 参数。通过接口获取 pod 详情的代码如下:

    1. #获取指定pod的信息
    2. k8s_api_coreV1 = client.CoreV1Api()
    3. try:
    4. target_pod = k8s_api_coreV1.read_namespaced_pod(name=eventPodName, namespace=eventNameSpace)
    5. except ApiException as e:
    6. print("Exception when calling CoreV1Api->read_namespaced_pod: %s\n" % e)
    7. meta_OwnerRef = eval(str(target_pod.metadata.owner_references[0]))
    8. #获取对应kind的name并调用接口获取元数据
    9. k8sObjName = meta_OwnerRef.get('name')
    10. k8sObjKind = meta_OwnerRef.get('kind')

    对于获取到的数据,需要分情况处理:

    • 如果当前对象的OwnerReferences.kind为 deployment,则可以直接获取 deployname;
    • 但多数情况下,K8s 对象经历过多次升级或回滚后,Owner 信息会变为对应的 ReplicaSet,这时就需要使用 Replicaset 再做一次查询,从而获取到deployment name。示例如下:
    1. deployName = ""
    2. #获取API入口
    3. query_api = client.AppsV1Api()
    4. if k8sObjKind =="ReplicaSet" :
    5. try:
    6. replicaset = query_api.read_namespaced_replica_set(k8sObjName, eventNameSpace)
    7. except ApiException as e:
    8. print("Exception when calling AppsV1Api->read_namespaced_replica_set: %s\n" % e)
    9. replica_meta = eval(str(replicaset.metadata.owner_references[0]))
    10. deployName = replica_meta.get('name')
    11. elif k8sObjKind =="Deployment" :
    12. print("如类型为Deployment,则直接使用本次查询到的ObjName。")
    13. deployName = k8sObjName
    14. else:
    15. print("非本次处理对象,退出。")
    16. return

    获取到deployment name后,查询当前副本数,并检查能否继续执行扩容操作:

    1. #获取Deployment当前副本数
    2. target_deployment = query_api.read_namespaced_deployment(deployName, eventNameSpace)
    3. cur_replicas = target_deployment.spec.replicas
    4. #检查当前副本数是否可以继续执行扩容
    5. if cur_replicas >= SCALEOUT_SOFT_UPLIMITS:
    6. print("当前副本数:",cur_replicas,"\n当前设置软上限:",SCALEOUT_SOFT_UPLIMITS,"\n无法继续扩容,请人工处理!")
    7. sendCustomEventScaleFailed("warning",cur_replicas)
    8. return

    若检查通过,则封装 K8s 数据包并执行扩容:

    1. #检查通过,开始封装扩容数据并执行扩容操作
    2. body_patch = {
    3. 'api_version': 'apps/v1',
    4. 'kind': 'Deployment',
    5. 'metadata':{
    6. 'name': deployName,
    7. 'namespace': eventNameSpace
    8. },
    9. 'spec':{'replicas': cur_replicas+1}
    10. }
    11. # 下发配置更改
    12. update_deployment(query_api,deployName,eventNameSpace,body_patch)
    13. return("Resource scale out Good!\n")

    配置告警通知对象

    完成处理逻辑的编写后,需要进入「DataFlux Func」-「管理」-「授权链接」中,创建新的授权链接,并复制 API 地址:
     

    在「控制台」-「监控」-「通知对象管理」中,新建一个 Webhook 类型的通知对象,并将上面复制的 API 地址保存到 Webhook 栏:

    配置观测云告警

    在进行故障模拟前,需要为目标应用容器设置监控指标阈值,在故障注入时触发对应的告警通知。打开「控制台」-「监控」-「新建监控器」,选取对应的指标项,配置告警阈值:

    在告警通知栏,选择上一步创建的告警通知对象,点击「确定」并「保存」:

    效果展示

    由于没有压测环境,本例使用脚本占用前端负载均衡计算资源的方式,模拟高并发条件下需要进行系统扩容的场景。

    执行故障注入

    示例脚本的效果是使目标应用容器的 CPU 占用率升高:

    1. [root@dns01-dev gc]$ vi cpu.sh
    2. [root@dns01-dev gc]$ cat cpu.sh
    3. #! /bin/sh
    4. # filename killcpu.sh
    5. if [ $# -ne 1 ] ; then
    6. echo "USAGE: $0 |stop"
    7. exit 1;
    8. fi
    9. stop()
    10. {
    11. while read LINE
    12. do
    13. kill -9 $LINE
    14. echo "kill $LINE sucessfull"
    15. done < pid.txt
    16. cat /dev/null > pid.txt
    17. }
    18. start()
    19. {
    20. echo "u want to cpus is: "$1
    21. for i in `seq $1`
    22. do
    23. echo -ne "
    24. i=0;
    25. while true
    26. do
    27. i=i+1;
    28. done" | /bin/sh &
    29. pid_array[$i]=$! ;
    30. done
    31. for i in "${pid_array[@]}"; do
    32. echo 'pid is: ' $i ';';
    33. echo $i >> pid.txt
    34. done
    35. }
    36. case $1 in
    37. stop)
    38. stop
    39. ;;
    40. *)
    41. start $1
    42. ;;
    43. esac
    • 首先,检查当前环境 deployment 的副本数量以及 pod 列表,可以看到每个 deployment 的副本均为 1:

    • 其次,检查当前各 pod 指标运行情况,指标均正常:

    • 然后,将上面列出的cpu.h脚本拷贝到ruoyi-nginx容器,赋权并执行脚本:
    1. [root@dns01-dev gc]$ kubectl get deploy -n gc-dev
    2. NAME READY UP-TO-DATE AVAILABLE AGE
    3. ruoyi-auth 1/1 1 1 113d
    4. ruoyi-gateway 1/1 1 1 113d
    5. ruoyi-mysql 1/1 1 1 113d
    6. ruoyi-nacos 1/1 1 1 113d
    7. ruoyi-nginx 1/1 1 1 113d
    8. ruoyi-redis 1/1 1 1 113d
    9. ruoyi-system 1/1 1 1 2d15h
    10. [root@dns01-dev gc]$ ls
    11. 1 cpu.sh gc-func gc_func_svcacc.yaml gc-launcher mock_cpu.sh note.txt stp1_nfssc stp2_openesb stp5_td stp6_redis
    12. [root@dns01-dev gc]$ kubectl get po -n gc-dev
    13. NAME READY STATUS RESTARTS AGE
    14. ruoyi-auth-6475544879-pvx4r 2/2 Running 0 109d
    15. ruoyi-gateway-7f46976bb5-g2qtf 2/2 Running 0 61d
    16. ruoyi-mysql-6c48f4f47b-sbqpv 1/1 Running 0 113d
    17. ruoyi-nacos-667ff88589-769rk 1/1 Running 0 113d
    18. ruoyi-nginx-d44f6c5ff-8jphj 1/1 Running 0 2d22h
    19. ruoyi-redis-594b4d99dd-vjl6v 1/1 Running 0 113d
    20. ruoyi-system-7f49bc9cc9-dms9r 2/2 Running 0 2d14h
    21. [root@dns01-dev gc]$ kubectl cp -n gc-dev cpu.sh ruoyi-nginx-d44f6c5ff-8jphj:/home
    22. [root@dns01-dev gc]$ kubectl exec -it -n gc-dev ruoyi-nginx-d44f6c5ff-8jphj -- /bin/bash
    23. root@ruoyi-nginx-d44f6c5ff-8jphj:/home/ruoyi/projects/ruoyi-ui# cd /home
    24. root@ruoyi-nginx-d44f6c5ff-8jphj:/home# chmod +x cpu.sh
    25. root@ruoyi-nginx-d44f6c5ff-8jphj:/home# ls -l
    26. total 8
    27. -rwxr-xr-x 1 root root 521 Jun 1 16:14 cpu.sh
    28. drwxr-xr-x 1 root root 4096 May 4 07:48 ruoyi
    29. root@ruoyi-nginx-d44f6c5ff-8jphj:/home# ./cpu.sh 1
    30. u want to cpus is: 1
    31. ./cpu.sh: 29: pid_array[1]=54: not found
    32. ./cpu.sh: 32: Bad substitution
    33. root@ruoyi-nginx-d44f6c5ff-8jphj:/home# /bin/sh: 1: -ne: not found
    34. root@ruoyi-nginx-d44f6c5ff-8jphj:/home#

    检查注入效果

    通过kubectl命令和观测云仪表板,检查脚本执行效果:
     

    检查告警触发

    检查「控制台」-「事件」,查看告警触发情况。点击具体的告警条目打开告警详情,点击「告警通知」,查看是否成功调用了 Webhook 通知对象。若看到告警未发送,点击「通知对象」可以看到未发送的具体原因:
     

    检查脚本执行结果

    如下两种方式均可看到本次扩容已完成:

    • 若告警通知已发送至 DataFlux Func ,通过「控制台」-「基础设施」-「容器」-「Pod」可以看到当前副本数。

    • 或者,可以通过集群的Kubectl命令检查当前副本数。


      至此,弹性扩容的用例演示结束。

    总结

    通过应用观测云函数开发平台,用户可以衍生出各种对被测系统的查询、管理等操作,为系统管理工作提供了极大的便利性和灵活性,也为拓宽观测云使用场景提供了技术基础,是非常好用的一款工具。

    附:完整示例代码

    1. from kubernetes import client, config
    2. from kubernetes.client.rest import ApiException
    3. import pytz
    4. import re
    5. import datetime
    6. import json
    7. import requests
    8. #ApiToken
    9. #这里填入您自己的API_TOKEN
    10. API_TOKEN = "xxxxxx"
    11. #这里填写目标集群的API-Server地址
    12. KUBE_API_HOSTS= "https://x.x.x.x:5443"
    13. '''
    14. Demo演示:
    15. 基于监控数据扩容资源
    16. '''
    17. #用于执行kubectl patch的操作
    18. def update_deployment(api,deploy_name,ns,patch_detail):
    19. # patch the deployment
    20. try:
    21. resp = api.patch_namespaced_deployment(name=deploy_name, namespace=ns, body=patch_detail)
    22. except ApiException as e:
    23. print("Exception when calling api->read_namespaced_pod: %s\n" % e)
    24. print("\n[INFO] deployment's replicas count updated.\n")
    25. print("%s\t%s\t\t\t%s\t%s" % ("NAMESPACE", "NAME", "REVISION", "REPLICAS"))
    26. print(
    27. "%s\t\t%s\t%s\t\t%s\n"
    28. % (
    29. resp.metadata.namespace,
    30. resp.metadata.name,
    31. resp.metadata.generation,
    32. resp.spec.replicas,
    33. )
    34. )
    35. ## 扩容副本数软上限设置,表示最大扩容到5副本
    36. SCALEOUT_SOFT_UPLIMITS=5
    37. @DFF.API('演示资源扩容')
    38. def cceDeployScaleOps(**kwargs):
    39. #格式化打印告警参数,用于解析需要的信息
    40. rawEventMsg = json.dumps(kwargs,indent=2,ensure_ascii=False)
    41. print("告警发送内容:\n",rawEventMsg)
    42. #缓存告警ID
    43. dfEventId=kwargs.get('df_event_id')
    44. dfMonitorId=kwargs.get('df_monitor_id')
    45. #提取Pod_name和归属的ns
    46. eventPodName=kwargs.get('pod_name')
    47. eventNameSpace=kwargs.get('namespace')
    48. #配置客户端连接
    49. configuration = client.Configuration()
    50. setattr(configuration, 'verify_ssl', False)
    51. client.Configuration.set_default(configuration)
    52. configuration.host = KUBE_API_HOSTS #ApiHost
    53. configuration.verify_ssl = False
    54. configuration.debug = True
    55. configuration.api_key = {"authorization": "Bearer " + API_TOKEN}
    56. client.Configuration.set_default(configuration)
    57. #获取指定pod的信息
    58. k8s_api_coreV1 = client.CoreV1Api()
    59. try:
    60. target_pod = k8s_api_coreV1.read_namespaced_pod(name=eventPodName, namespace=eventNameSpace)
    61. except ApiException as e:
    62. print("Exception when calling CoreV1Api->read_namespaced_pod: %s\n" % e)
    63. meta_OwnerRef = eval(str(target_pod.metadata.owner_references[0]))
    64. #获取对应kind的name并调用接口获取元数据
    65. k8sObjName = meta_OwnerRef.get('name')
    66. k8sObjKind = meta_OwnerRef.get('kind')
    67. deployName = ""
    68. #获取API入口
    69. query_api = client.AppsV1Api()
    70. if k8sObjKind =="ReplicaSet" :
    71. try:
    72. replicaset = query_api.read_namespaced_replica_set(k8sObjName, eventNameSpace)
    73. except ApiException as e:
    74. print("Exception when calling AppsV1Api->read_namespaced_replica_set: %s\n" % e)
    75. replica_meta = eval(str(replicaset.metadata.owner_references[0]))
    76. deployName = replica_meta.get('name')
    77. elif k8sObjKind =="Deployment" :
    78. print("如类型为Deployment,则直接使用本次查询到的ObjName。")
    79. deployName = k8sObjName
    80. else:
    81. print("非本次处理对象,退出。")
    82. return
    83. #获取Deployment当前副本数
    84. target_deployment = query_api.read_namespaced_deployment(deployName, eventNameSpace)
    85. cur_replicas = target_deployment.spec.replicas
    86. #检查当前副本数是否可以继续执行扩容
    87. if cur_replicas >= SCALEOUT_SOFT_UPLIMITS:
    88. print("当前副本数:",cur_replicas,"\n当前设置软上限:",SCALEOUT_SOFT_UPLIMITS,"\n无法继续扩容,请人工处理!")
    89. sendCustomEventScaleFailed("warning",cur_replicas)
    90. return
    91. #检查通过,开始封装扩容数据并执行扩容操作
    92. body_patch = {
    93. 'api_version': 'apps/v1',
    94. 'kind': 'Deployment',
    95. 'metadata':{
    96. 'name': deployName,
    97. 'namespace': eventNameSpace
    98. },
    99. 'spec':{'replicas': cur_replicas+1}
    100. }
    101. # 下发配置更改
    102. update_deployment(query_api,deployName,eventNameSpace,body_patch)
    103. return("Resource scale out Good!\n")
  • 相关阅读:
    笔记二:odoo搜索、筛选和分组
    Java 计算两个日期相差多少天
    如何建立团队知识库管理系统,把分散信息有效整理?
    天软特色因子看板 (2023.09 第09期)
    【Spark学习笔记】(一)—— Spark 概述和 WordCount
    什么是Java中的代理模式?
    【LSTM实战】股票走势预测全流程实战(stock predict)
    FPGA解析B码----连载3
    通过阿里巴巴,蚂蚁金服的面试我知道这些JAVAp7岗的套路,建议收藏
    一个优秀的软件测试工程师该如何进行需求分析
  • 原文地址:https://blog.csdn.net/DataFlux/article/details/132738120