在3月初, 我们就考虑在Kubernetes集群中增加性能采样分析工具(主要针对Python, 尤其是uwsgi应用), 在调研了几种性能分析方案之后, 选择py-spy用于uwsgi应用的性能分析, 这篇博客介绍具体的实现流程以及一些调试策略, 附录部分介绍了一下我学习到的内容.
Profile工具
我们集群主要是针对Python应用的采样以及性能分析, 我们原有的未在kubernetes环境中的方案是pyflame, 但是它已经不维护了.
组里的其他同事提出了用py-spy, 还在比较活跃的开发中, 而且功能比pyflame更多, 安装更为简单, 就转而使用py-spy了
系统实现方案
直接运行
它的top功能我比较喜欢, 可以马上确认当前堆栈信息.
在它的README中, 也介绍了如何在docker容器中运行py-spy, 需要增加PTRACE权限,
另外一点就是也需要读取hostPID, 如果只能看到容器中自己的pid, 是无法取样的.
kubernetes环境中运行
参考github中的方案
下面是pyflame的实现方案: https://github.com/monsterxx03/kube-pyflame/blob/master/kubectl-pyflame
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| nodeName=$(kubectl get pod ${POD} -n ${NAMESPACE} -o jsonpath='{.spec.nodeName}') kubectl create -f - <<EOF apiVersion: v1 kind: Pod metadata: name: ${DEBUG_POD} namespace: ${NAMESPACE} spec: terminationGracePeriodSeconds: 0 hostPID: true containers: - command: ['sh', '-c', 'while true; do sleep 10; done;'] image: ${IMAGE} imagePullPolicy: IfNotPresent name: pyflame securityContext: privileged: true nodeSelector: kubernetes.io/hostname: ${nodeName} EOF kubectl wait --for=condition=Ready pod/${DEBUG_POD} -n ${NAMESPACE} --timeout=120s kubectl exec ${DEBUG_POD} -n ${NAMESPACE} -- bash -c "pyflame -p ${pid} -s $SECONDS -r $RATE | flamegraph.pl > /tmp/pyflame.svg"
|
这是一种最为简单的实现方式, 需要注意的有两点, 这里的hostPID: true
以及securityContext: privileged: true
, 这就是给定了特殊权限.
我们的实现方式
根据上面的pyflame方法, 我们使用py-spy也差不多, 和上面配置很像, 但有一些地方是不同的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| --- apiVersion: batch/v1 kind: Job metadata: name: profile-task-22-1618198229 namespace: default spec: ttlSecondsAfterFinished: 259200 backoffLimit: 3 template: metadata: labels: app_id: "11" spec: nodeSelector: kubernetes.io/hostname: "minikube" restartPolicy: Never hostPID: true containers: - name: profile-task--22 image: <IMAGE> args: - profile_task env: - name: params value: "{\"rate\": 100, \"seconds\": 60, \"exclude_idle\": true, \"multi_threads\": true, \"app_name\": \"arya-python3\", \"version_id\": 2255}" securityContext: capabilities: add: - SYS_PTRACE
|
不同点有以下几个方面:
- 我们使用了job, 而不使用pod, 我主要是为了规范代码, 另外有一点, job可以有这个参数
ttlSecondsAfterFinished
- namespace我这里默认用来default, 而不是应用所在的namespace, 主要原因是:
前面的文章中, 我介绍了: 基于Kubernetes的PaaS平台提供dashboard支持的一种方案.
因此我们给用户开放了自己的应用所在namespace的操作权限, 也就是可以exec进入容器中,
那么就会有一个问题了, 因为这个采样容器是具有hostPID权限的, 如果普通用户可以进入到
这个特权容器中, 那么它就可以kill其他程序了, 这种操作是不能被允许的.
因此, 这一类特权容器需要统一放到用户无法访问的namespace中.
- nodeSelector中给定了需要运行采样任务的机器, 这个机器由PaaS随机进行选择, 不需要关心自己的应用是在哪台机器中
- 环境变量中, 保存了采样参数, 该任务运行启动后, 可以自动开始采样操作, 采样结束后, 上传结果, 用户只需要点几下按钮就可以得到数据
采样容器中代码细节
这个程序有几个工作:
- 读取环境变量中的配置信息,
- 确认要取样的进程id,
- 取样
- 上报火焰图
我贴下主要的代码, 错误处理已经忽略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| def _run_profile_task(): params = json.loads(os.environ['params']) app_name = params['app_name'] version_id = params['version_id']
proc_name = '%s-%s_uWSGI worker' % (app_name, version_id) for proc in psutil.process_iter(): cmdline = proc.cmdline() if len(cmdline) > 0: cmd = " ".join(cmdline) if cmd.startswith(proc_name): target_proc = proc break output_file = os.path.join( 'var', 'run', 'profile-{}-{}-{}'.format(app_name, version_id, int(time.time())) ) proc_args = [ 'py-spy', 'record', '--subprocesses', '--duration', '{}'.format(params['seconds']), '--rate', '{}'.format(params['rate']), '--pid', '{}'.format(target_proc.pid), '--output', output_file, ] if params.get('multi_threads'): pass if params.get('exclude_idle', True) is False: proc_args.append('--idle')
_ret, _out, _err = run_with_timeout(proc_args, get_timeout())
|
程序安全性考量
安全性问题主要就是namespace的限制策略, 已经在上面的对比实现中给出了具体的原因以及方案.
总结
这篇文章主要想介绍下我们在kubernetes环境中根据PaaS平台情况增加的性能分析工具,
目前的情况止步于Python应用, 不过未来也很容易扩展到其他语言的程序.
另外需要注意的一点就是特权容器的处理, 尽量少给权限, 而且也不要让用户看到.
附1: Job调试策略
在pyflame的实现方案中, 有一行这样的配置, 学习一下, 简直就是job任务以及pod容器的调试神器.
1
| - command: ['sh', '-c', 'while true; do sleep 10; done;']
|
附2: kubectl flame 工具学习
在我们的功能上线后, 我也看到有这么一个取样工具, 它是基于kubectl的插件开发方式提供的.
如果只是需要一个简单的取样工具, 那么没必要开发一整套系统, 可以尝试用用kubectl
的这个插件
Introducing Kubectl Flame: Effortless Profiling on Kubernetes
里面有这么一句话: Profiling is a non-trivial task.(性能分析是一项非同小可的任务.)
1
| kubectl flame mypod -t 1m -f /tmp/flamegraph.svg
|
我顺便也读了读代码 https://github.com/VerizonMedia/kubectl-flame
代码库不长, 我简单介绍下它的实现吧.
代码实现
cli/cmd/kubernetes/root.go
入口代码在这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| cmd := &cobra.Command{ Use: "flame [pod-name]", DisableFlagsInUseLine: true, Short: "Profile running applications by generating flame graphs.", Long: flameLong, Example: fmt.Sprintf(flameExamples, "kubectl"), PersistentPreRun: func(c *cobra.Command, args []string) { c.SetOutput(streams.ErrOut) }, Run: func(cmd *cobra.Command, args []string) { validateFlags(chosenLang, chosenEvent, &targetDetails, &jobDetails);
targetDetails.PodName = args[0] if len(args) > 1 { targetDetails.ContainerName = args[1] }
cfg := &data.FlameConfig{ TargetConfig: &targetDetails, JobConfig: &jobDetails, ConfigFlags: options.configFlags, }
Flame(cfg) }, }
|
Flame
函数就是具体的采样函数了, 我把错误处理全部省略了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| func Flame(cfg *data.FlameConfig) { ns, err := kubernetes.Connect(cfg.ConfigFlags) p := NewPrinter(cfg.TargetConfig.DryRun)
cfg.TargetConfig.Namespace = ns ctx := context.Background()
p.Print("Verifying target pod ... ") pod, err := kubernetes.GetPodDetails(cfg.TargetConfig.PodName, cfg.TargetConfig.Namespace, ctx) containerName, err := validatePod(pod, cfg.TargetConfig)
containerId, err := kubernetes.GetContainerId(containerName, pod) p.PrintSuccess()
cfg.TargetConfig.ContainerName = containerName cfg.TargetConfig.ContainerId = containerId
p.Print("Launching profiler ... ") profileId, job, err := kubernetes.LaunchFlameJob(pod, cfg, ctx) if cfg.TargetConfig.DryRun { return }
cfg.TargetConfig.Id = profileId profilerPod, err := kubernetes.WaitForPodStart(cfg.TargetConfig, ctx) p.PrintSuccess() apiHandler := &handler.ApiEventsHandler{ Job: job, Target: cfg.TargetConfig, } done, err := kubernetes.GetLogsFromPod(profilerPod, apiHandler, ctx) <-done }
|
后面的代码就不贴了, 就是为了启动一个Job. 直接快进到具体的profile代码
1 2 3 4 5 6 7
| cmd := exec.Command(pySpyLocation, "record", "-p", pid, "-o", pythonOutputFileName, "-d", duration, "-s", "-t") var out bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr
|
不同语言的应用应该如何采样分析
也是读代码库学到的…
语言 |
取样工具 |
Java |
async-profiler |
Python |
py-spy |
Golang |
bcc-profiler |
Ruby |
rbspy |