Skip to content

Commit d99cf35

Browse files
MarkJoyMaaiden.maaiden.ma
authored
Feat/continue profiling (#4867)
Co-authored-by: aiden.ma <[email protected]> Co-authored-by: aiden.ma <[email protected]>
1 parent f459f1b commit d99cf35

File tree

5 files changed

+421
-0
lines changed

5 files changed

+421
-0
lines changed

core/service/serviceconf.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/zeromicro/go-zero/core/stat"
99
"github.com/zeromicro/go-zero/core/trace"
1010
"github.com/zeromicro/go-zero/internal/devserver"
11+
"github.com/zeromicro/go-zero/internal/profiling"
1112
)
1213

1314
const (
@@ -38,6 +39,8 @@ type (
3839
Telemetry trace.Config `json:",optional"`
3940
DevServer DevServerConfig `json:",optional"`
4041
Shutdown proc.ShutdownConf `json:",optional"`
42+
// Profiling is the configuration for profiling.
43+
Profiling profiling.Config `json:",optional"`
4144
}
4245
)
4346

@@ -72,6 +75,8 @@ func (sc ServiceConf) SetUp() error {
7275
}
7376
devserver.StartAgent(sc.DevServer)
7477

78+
profiling.Start(sc.Profiling)
79+
7580
return nil
7681
}
7782

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/golang/mock v1.6.0
1313
github.com/golang/protobuf v1.5.4
1414
github.com/google/uuid v1.6.0
15+
github.com/grafana/pyroscope-go v1.2.2
1516
github.com/jackc/pgx/v5 v5.7.4
1617
github.com/jhump/protoreflect v1.17.0
1718
github.com/pelletier/go-toml/v2 v2.2.2
@@ -71,6 +72,7 @@ require (
7172
github.com/google/gnostic-models v0.6.8 // indirect
7273
github.com/google/go-cmp v0.6.0 // indirect
7374
github.com/google/gofuzz v1.2.0 // indirect
75+
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
7476
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
7577
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
7678
github.com/jackc/pgpassfile v1.0.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
8080
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
8181
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8282
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
83+
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
84+
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
85+
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
86+
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
8387
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
8488
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
8589
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=

internal/profiling/profiling.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package profiling
2+
3+
import (
4+
"runtime"
5+
"sync"
6+
"time"
7+
8+
"github.com/grafana/pyroscope-go"
9+
10+
"github.com/zeromicro/go-zero/core/logx"
11+
"github.com/zeromicro/go-zero/core/proc"
12+
"github.com/zeromicro/go-zero/core/stat"
13+
"github.com/zeromicro/go-zero/core/threading"
14+
)
15+
16+
type (
17+
Config struct {
18+
// Name is the name of the application.
19+
Name string `json:",optional,inherit"`
20+
// ServerAddress is the address of the profiling server.
21+
ServerAddress string
22+
// AuthUser is the username for basic authentication.
23+
AuthUser string `json:",optional"`
24+
// AuthPassword is the password for basic authentication.
25+
AuthPassword string `json:",optional"`
26+
// UploadDuration is the duration for which profiling data is uploaded.
27+
UploadDuration time.Duration `json:",default=15s"`
28+
// IntervalDuration is the interval for which profiling data is collected.
29+
IntervalDuration time.Duration `json:",default=10s"`
30+
// ProfilingDuration is the duration for which profiling data is collected.
31+
ProfilingDuration time.Duration `json:",default=2m"`
32+
// CpuThreshold the collection is allowed only when the current service cpu < CpuThreshold
33+
CpuThreshold int64 `json:",default=700,range=[0:1000)"`
34+
35+
// ProfileType is the type of profiling to be performed.
36+
ProfileType ProfileType
37+
}
38+
39+
ProfileType struct {
40+
// Logger is a flag to enable or disable logging.
41+
Logger bool `json:",default=false"`
42+
// CPU is a flag to disable CPU profiling.
43+
CPU bool `json:",default=true"`
44+
// Goroutines is a flag to disable goroutine profiling.
45+
Goroutines bool `json:",default=true"`
46+
// Memory is a flag to disable memory profiling.
47+
Memory bool `json:",default=true"`
48+
// Mutex is a flag to disable mutex profiling.
49+
Mutex bool `json:",default=false"`
50+
// Block is a flag to disable block profiling.
51+
Block bool `json:",default=false"`
52+
}
53+
54+
profiler interface {
55+
Start() error
56+
Stop() error
57+
}
58+
59+
pyProfiler struct {
60+
c Config
61+
profiler *pyroscope.Profiler
62+
}
63+
)
64+
65+
var (
66+
once sync.Once
67+
68+
newProfiler = func(c Config) profiler {
69+
return newPyProfiler(c)
70+
}
71+
)
72+
73+
// Start initializes the pyroscope profiler with the given configuration.
74+
func Start(c Config) {
75+
// check if the profiling is enabled
76+
if c.ServerAddress == "" {
77+
return
78+
}
79+
80+
// set default values for the configuration
81+
if c.ProfilingDuration <= 0 {
82+
c.ProfilingDuration = time.Minute * 2
83+
}
84+
85+
// set default values for the configuration
86+
if c.IntervalDuration <= 0 {
87+
c.IntervalDuration = time.Second * 10
88+
}
89+
90+
if c.UploadDuration <= 0 {
91+
c.UploadDuration = time.Second * 15
92+
}
93+
94+
once.Do(func() {
95+
logx.Info("continuous profiling started")
96+
97+
var done = make(chan struct{})
98+
proc.AddShutdownListener(func() {
99+
done <- struct{}{}
100+
close(done)
101+
})
102+
103+
threading.GoSafe(func() {
104+
startPyroScope(c, done)
105+
})
106+
})
107+
}
108+
109+
// startPyroScope starts the pyroscope profiler with the given configuration.
110+
func startPyroScope(c Config, done <-chan struct{}) {
111+
var (
112+
intervalTicker = time.NewTicker(c.IntervalDuration)
113+
profilingTicker = time.NewTicker(c.ProfilingDuration)
114+
115+
pr profiler
116+
err error
117+
118+
latestProfilingTime time.Time
119+
)
120+
121+
for {
122+
select {
123+
case <-intervalTicker.C:
124+
// Check if the machine is overloaded and if the profiler is not running
125+
if pr == nil && checkMachinePerformance(c) {
126+
pr = newProfiler(c)
127+
if err := pr.Start(); err != nil {
128+
logx.Errorf("failed to start profiler: %v", err)
129+
continue
130+
}
131+
132+
// record the latest profiling time
133+
latestProfilingTime = time.Now()
134+
logx.Infof("pyroScope profiler started.")
135+
}
136+
case <-profilingTicker.C:
137+
// check if the profiling duration has passed
138+
if !time.Now().After(latestProfilingTime.Add(c.ProfilingDuration)) {
139+
continue
140+
}
141+
142+
// check if the profiler is already running, if so, skip
143+
if pr != nil {
144+
if err = pr.Stop(); err != nil {
145+
logx.Errorf("failed to stop profiler: %v", err)
146+
}
147+
logx.Infof("pyroScope profiler stopped.")
148+
pr = nil
149+
}
150+
case <-done:
151+
logx.Infof("continuous profiling stopped.")
152+
intervalTicker.Stop()
153+
profilingTicker.Stop()
154+
return
155+
}
156+
}
157+
}
158+
159+
// genPyroScopeConf generates the pyroscope configuration based on the given config.
160+
func genPyroScopeConf(c Config) pyroscope.Config {
161+
pConf := pyroscope.Config{
162+
UploadRate: c.UploadDuration,
163+
ApplicationName: c.Name,
164+
BasicAuthUser: c.AuthUser, // http basic auth user
165+
BasicAuthPassword: c.AuthPassword, // http basic auth password
166+
ServerAddress: c.ServerAddress,
167+
Logger: nil,
168+
169+
HTTPHeaders: map[string]string{},
170+
171+
// you can provide static tags via a map:
172+
Tags: map[string]string{
173+
"name": c.Name,
174+
},
175+
}
176+
177+
if c.ProfileType.Logger {
178+
pConf.Logger = logx.WithCallerSkip(0)
179+
}
180+
181+
if c.ProfileType.CPU {
182+
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileCPU)
183+
}
184+
if c.ProfileType.Goroutines {
185+
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileGoroutines)
186+
}
187+
if c.ProfileType.Memory {
188+
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileAllocObjects, pyroscope.ProfileAllocSpace,
189+
pyroscope.ProfileInuseObjects, pyroscope.ProfileInuseSpace)
190+
}
191+
if c.ProfileType.Mutex {
192+
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileMutexCount, pyroscope.ProfileMutexDuration)
193+
}
194+
if c.ProfileType.Block {
195+
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileBlockCount, pyroscope.ProfileBlockDuration)
196+
}
197+
198+
logx.Infof("applicationName: %s", pConf.ApplicationName)
199+
200+
return pConf
201+
}
202+
203+
// checkMachinePerformance checks the machine performance based on the given configuration.
204+
func checkMachinePerformance(c Config) bool {
205+
currentValue := stat.CpuUsage()
206+
if currentValue >= c.CpuThreshold {
207+
logx.Infof("continuous profiling cpu overload, cpu:%d", currentValue)
208+
return true
209+
}
210+
211+
return false
212+
}
213+
214+
func newPyProfiler(c Config) profiler {
215+
return &pyProfiler{
216+
c: c,
217+
}
218+
}
219+
220+
func (p *pyProfiler) Start() error {
221+
pConf := genPyroScopeConf(p.c)
222+
// set mutex and block profile rate
223+
setFraction(p.c)
224+
profiler, err := pyroscope.Start(pConf)
225+
if err != nil {
226+
resetFraction(p.c)
227+
return err
228+
}
229+
230+
p.profiler = profiler
231+
return nil
232+
}
233+
234+
func (p *pyProfiler) Stop() error {
235+
if p.profiler == nil {
236+
return nil
237+
}
238+
239+
err := p.profiler.Stop()
240+
if err != nil {
241+
return err
242+
}
243+
resetFraction(p.c)
244+
p.profiler = nil
245+
246+
return nil
247+
}
248+
249+
func setFraction(c Config) {
250+
// These 2 lines are only required if you're using mutex or block profiling
251+
if c.ProfileType.Mutex {
252+
runtime.SetMutexProfileFraction(10) // 10/seconds
253+
}
254+
if c.ProfileType.Block {
255+
runtime.SetBlockProfileRate(1000 * 1000) // 1/millisecond
256+
}
257+
}
258+
259+
func resetFraction(c Config) {
260+
// These 2 lines are only required if you're using mutex or block profiling
261+
if c.ProfileType.Mutex {
262+
runtime.SetMutexProfileFraction(0)
263+
}
264+
if c.ProfileType.Block {
265+
runtime.SetBlockProfileRate(0)
266+
}
267+
}

0 commit comments

Comments
 (0)