Skip to content

Commit

Permalink
feat: adding support for NodeJS (#70)
Browse files Browse the repository at this point in the history
* feat: adding support for NodeJS and potentially C/C++

* fix: fixing Dockerfile

* chore: change to FlameGraph

* fix: making the perf agent actually work

* feat: adding the ability to launch profiling from the CLI

* fix: brief documentation update

* chore: adding nodejs caveat to the README
  • Loading branch information
kujon authored Feb 4, 2022
1 parent 23ea75f commit 498f80d
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 4 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ jobs:
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
repository: verizondigital/kubectl-flame
tags: ${{ steps.vars.outputs.tag }}-ruby
- name: Build perf Docker Image
uses: docker/build-push-action@v1
with:
dockerfile: 'agent/docker/perf/Dockerfile'
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
repository: verizondigital/kubectl-flame
tags: ${{ steps.vars.outputs.tag }}-perf
- name: Setup Go
uses: actions/setup-go@v1
with:
Expand All @@ -64,4 +72,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update new version in krew-index
uses: rajatjindal/[email protected]
uses: rajatjindal/[email protected]
2 changes: 1 addition & 1 deletion .krew.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ spec:
description: |
Generate CPU flame graphs without restarting pods and with low overhead.
caveats: |
Currently supported languages: Go, Java (any JVM based language), Python and Ruby.
Currently supported languages: Go, Java (any JVM based language), Python, Ruby, and NodeJS.
platforms:
- {{addURIAndSha "https://github.com/VerizonMedia/kubectl-flame/releases/download/{{ .TagName }}/kubectl-flame_{{ .TagName }}_darwin_x86_64.tar.gz" .TagName | indent 6 }}
bin: kubectl-flame
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Running `kubectlf-flame` does **not** require any modification to existing pods.
- [License](#license)

## Requirements
* Supported languages: Go, Java (any JVM based language), Python and Ruby
* Supported languages: Go, Java (any JVM based language), Python, Ruby, and NodeJS
* Kubernetes cluster that use Docker as the container runtime (tested on GKE, EKS and AKS)

## Usage
Expand Down Expand Up @@ -65,6 +65,7 @@ Interaction with the target JVM is done via a shared `/tmp` folder.
Golang support is based on [ebpf profiling](https://en.wikipedia.org/wiki/Berkeley_Packet_Filter).
Python support is based on [py-spy](https://github.com/benfred/py-spy).
Ruby support is based on [rbspy](https://rbspy.github.io/).
NodeJS support is based on [perf](https://perf.wiki.kernel.org/index.php/Main_Page). In order for Javascript Symbols to be resolved, node process needs to be run with `--perf-basic-prof` flag.

## Contribute
Please refer to [the contributing.md file](Contributing.md) for information about how to get involved. We welcome issues, questions, and pull requests.
Expand Down
18 changes: 18 additions & 0 deletions agent/docker/perf/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM golang:1.14-buster as agentbuild
WORKDIR /go/src/github.com/VerizonMedia/kubectl-flame
ADD . /go/src/github.com/VerizonMedia/kubectl-flame
RUN go get -d -v ./...
RUN cd agent && go build -o /go/bin/agent

FROM alpine as builder
RUN apk add --update git
RUN git clone https://github.com/brendangregg/FlameGraph

FROM alpine
RUN apk add --update perf perl
RUN mkdir -p /app/FlameGraph
COPY --from=agentbuild /go/bin/agent /app/agent
RUN mv /usr/bin/perf /app/perf
COPY --from=builder /FlameGraph /app/FlameGraph

CMD [ "/app/agent" ]
109 changes: 109 additions & 0 deletions agent/profiler/perf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package profiler

import (
"fmt"
"os"
"os/exec"
"strconv"

"github.com/VerizonMedia/kubectl-flame/agent/details"
"github.com/VerizonMedia/kubectl-flame/agent/utils"
)

const (
perfLocation = "/app/perf"
perfRecordOutputFileName = "/tmp/perf.data"
flameGraphPlLocation = "/app/FlameGraph/flamegraph.pl"
flameGraphStackCollapseLocation = "/app/FlameGraph/stackcollapse-perf.pl"
perfScriptOutputFileName = "/tmp/perf.out"
perfFoldedOutputFileName = "/tmp/perf.folded"
flameGraphPerfOutputFile = "/tmp/perf.svg"
)

type PerfProfiler struct{}

func (p *PerfProfiler) SetUp(job *details.ProfilingJob) error {
return nil
}

func (p *PerfProfiler) Invoke(job *details.ProfilingJob) error {
err := p.runPerfRecord(job)
if err != nil {
return fmt.Errorf("perf record failed: %s", err)
}

err = p.runPerfScript(job)
if err != nil {
return fmt.Errorf("perf script failed: %s", err)
}

err = p.foldPerfOutput(job)
if err != nil {
return fmt.Errorf("folding perf output failed: %s", err)
}

err = p.generateFlameGraph(job)
if err != nil {
return fmt.Errorf("flamegraph generation failed: %s", err)
}

return utils.PublishFlameGraph(flameGraphPerfOutputFile)
}

func (p *PerfProfiler) runPerfRecord(job *details.ProfilingJob) error {
pid, err := utils.FindRootProcessId(job)
if err != nil {
return err
}

duration := strconv.Itoa(int(job.Duration.Seconds()))
cmd := exec.Command(perfLocation, "record", "-p", pid, "-o", perfRecordOutputFileName, "-g", "--", "sleep", duration)

return cmd.Run()
}

func (p *PerfProfiler) runPerfScript(job *details.ProfilingJob) error {
f, err := os.Create(perfScriptOutputFileName)
if err != nil {
return err
}
defer f.Close()

cmd := exec.Command(perfLocation, "script", "-i", perfRecordOutputFileName)
cmd.Stdout = f

return cmd.Run()
}

func (p *PerfProfiler) foldPerfOutput(job *details.ProfilingJob) error {
f, err := os.Create(perfFoldedOutputFileName)
if err != nil {
return err
}
defer f.Close()

cmd := exec.Command(flameGraphStackCollapseLocation, perfScriptOutputFileName)
cmd.Stdout = f

return cmd.Run()
}

func (p *PerfProfiler) generateFlameGraph(job *details.ProfilingJob) error {
inputFile, err := os.Open(perfFoldedOutputFileName)
if err != nil {
return err
}
defer inputFile.Close()

outputFile, err := os.Create(flameGraphPerfOutputFile)
if err != nil {
return err
}
defer outputFile.Close()

cmd := exec.Command(flameGraphPlLocation)
cmd.Stdin = inputFile
cmd.Stdout = outputFile

return cmd.Run()
}
4 changes: 4 additions & 0 deletions agent/profiler/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package profiler

import (
"fmt"

"github.com/VerizonMedia/kubectl-flame/agent/details"
"github.com/VerizonMedia/kubectl-flame/api"
)
Expand All @@ -16,6 +17,7 @@ var (
bpf = BpfProfiler{}
python = PythonProfiler{}
ruby = RubyProfiler{}
perf = PerfProfiler{}
)

func ForLanguage(lang api.ProgrammingLanguage) (FlameGraphProfiler, error) {
Expand All @@ -28,6 +30,8 @@ func ForLanguage(lang api.ProgrammingLanguage) (FlameGraphProfiler, error) {
return &python, nil
case api.Ruby:
return &ruby, nil
case api.Node:
return &perf, nil
default:
return nil, fmt.Errorf("could not find profiler for language %s", lang)
}
Expand Down
3 changes: 2 additions & 1 deletion api/langs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const (
Go ProgrammingLanguage = "go"
Python ProgrammingLanguage = "python"
Ruby ProgrammingLanguage = "ruby"
Node ProgrammingLanguage = "node"
)

var (
supportedLangs = []ProgrammingLanguage{Java, Go, Python, Ruby}
supportedLangs = []ProgrammingLanguage{Java, Go, Python, Ruby, Node}
)

func AvailableLanguages() []ProgrammingLanguage {
Expand Down
122 changes: 122 additions & 0 deletions cli/cmd/kubernetes/job/perf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package job

import (
"fmt"

batchv1 "k8s.io/api/batch/v1"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"

"github.com/VerizonMedia/kubectl-flame/cli/cmd/data"
"github.com/VerizonMedia/kubectl-flame/cli/cmd/version"
)

type perfCreator struct{}

func (p *perfCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string, *batchv1.Job, error) {
id := string(uuid.NewUUID())
var imageName string
var imagePullSecret []apiv1.LocalObjectReference
args := []string{
id,
string(targetPod.UID),
cfg.TargetConfig.ContainerName,
cfg.TargetConfig.ContainerId,
cfg.TargetConfig.Duration.String(),
string(cfg.TargetConfig.Language),
string(cfg.TargetConfig.Event),
}

if cfg.TargetConfig.Pgrep != "" {
args = append(args, cfg.TargetConfig.Pgrep)
}

if cfg.TargetConfig.Image != "" {
imageName = cfg.TargetConfig.Image
} else {
imageName = fmt.Sprintf("%s:%s-perf", baseImageName, version.GetCurrent())
}

if cfg.TargetConfig.ImagePullSecret != "" {
imagePullSecret = []apiv1.LocalObjectReference{{Name: cfg.TargetConfig.ImagePullSecret}}
}

commonMeta := metav1.ObjectMeta{
Name: fmt.Sprintf("kubectl-flame-%s", id),
Namespace: cfg.JobConfig.Namespace,
Labels: map[string]string{
"kubectl-flame/id": id,
},
Annotations: map[string]string{
"sidecar.istio.io/inject": "false",
},
}

resources, err := cfg.JobConfig.ToResourceRequirements()
if err != nil {
return "", nil, fmt.Errorf("unable to generate resource requirements: %w", err)
}

job := &batchv1.Job{
TypeMeta: metav1.TypeMeta{
Kind: "Job",
APIVersion: "batch/v1",
},
ObjectMeta: commonMeta,
Spec: batchv1.JobSpec{
Parallelism: int32Ptr(1),
Completions: int32Ptr(1),
TTLSecondsAfterFinished: int32Ptr(5),
BackoffLimit: int32Ptr(2),
Template: apiv1.PodTemplateSpec{
ObjectMeta: commonMeta,
Spec: apiv1.PodSpec{
HostPID: true,
Volumes: []apiv1.Volume{
{
Name: "target-filesystem",
VolumeSource: apiv1.VolumeSource{
HostPath: &apiv1.HostPathVolumeSource{
Path: cfg.TargetConfig.DockerPath,
},
},
},
},
ImagePullSecrets: imagePullSecret,
InitContainers: nil,
Containers: []apiv1.Container{
{
ImagePullPolicy: apiv1.PullAlways,
Name: ContainerName,
Image: imageName,
Command: []string{"/app/agent"},
Args: args,
VolumeMounts: []apiv1.VolumeMount{
{
Name: "target-filesystem",
MountPath: "/var/lib/docker",
},
},
SecurityContext: &apiv1.SecurityContext{
Privileged: boolPtr(true),
Capabilities: &apiv1.Capabilities{
Add: []apiv1.Capability{"SYS_PTRACE"},
},
},
Resources: resources,
},
},
RestartPolicy: "Never",
NodeName: targetPod.Spec.NodeName,
},
},
},
}

if cfg.TargetConfig.ServiceAccountName != "" {
job.Spec.Template.Spec.ServiceAccountName = cfg.TargetConfig.ServiceAccountName
}

return id, job, nil
}
3 changes: 3 additions & 0 deletions cli/cmd/kubernetes/job/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var (
bpf = bpfCreator{}
python = pythonCreator{}
ruby = rubyCreator{}
perf = perfCreator{}
)

type creator interface {
Expand All @@ -38,6 +39,8 @@ func Create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string, *batchv1.Job,
return python.create(targetPod, cfg)
case api.Ruby:
return ruby.create(targetPod, cfg)
case api.Node:
return perf.create(targetPod, cfg)
}

// Should not happen
Expand Down

0 comments on commit 498f80d

Please sign in to comment.