Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interpreter: add Golang #408

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

interpreter: add Golang #408

wants to merge 12 commits into from

Conversation

florianl
Copy link
Contributor

Add symbolization functionality for Go executables.

@florianl florianl force-pushed the interpreter-golang branch from 5ad982b to 9078329 Compare March 17, 2025 18:22
@florianl florianl marked this pull request as ready for review March 17, 2025 18:32
@florianl florianl requested review from a team as code owners March 17, 2025 18:32
Copy link
Contributor

@fabled fabled left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments added. But before just blindly starting mixing Rust, I'd like a larger discussion a comments from all maintainers on feasibility of this.

I understand using existing Rust code might be faster, and the Rust code is likely good and tested.

On the other hand this the down sides are:

  • core maintainers should also know Rust (possibly true?)
  • CGO is slower than regular Go calls. not sure of the overhead.
  • there is room for non-trivial errors as the calls need to use unsafe Go, potentially in non-trivial ways

Additionally go symbolization could be done directly using Go standard runtime libraries, or by extending the existing elfgopclntab code we have intree.

Could you elaborate why embedded Rust was chosen instead of the two above mentioned approaches? Do these reasons justify the overhead that mixing Go and Rust brings both in maintanenance, complexity and debugging?

@@ -267,6 +267,21 @@ func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace
}
}

if pm.eim.IsGolang(frame.File) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fairly ugly way to plug this in. Makes me wonder if the integration should be done in some other way. Perhaps split the interpreter into unwinder / symbolization stages. Or have some other hooking mechanism to tag the frames that should get Go symbolized. Perhaps add a mechanism to specify the ebpf sent frame type on per-DSO or mapping range basis.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to discuss alternatives. For the moment, I think, this is the least intrusive way. This current approach should also be easily adaptable if the interpreter interface differentiates at some point between unwinding and symbolization.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, I really don't like the special golang check here. As minimum, I'd completely remove the IsGolang call above (And introduction if it in eim). We can just directly call symbolizeFrame here. And make sure the existing interpreters ignore native frame (should be the case).

Or do you see some practical reasons why this check should be here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion sounds reasonable to me. It just needs some logic on the eBPF side to report Go frames for the respective executables. And I do think introducing a dedicated Go unwinder in eBPF doesn't make sense but the native unwinder needs to be extended in some way to switch the frame type at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the suggestion with 7128306

@christos68k
Copy link
Member

christos68k commented Mar 17, 2025

Could you elaborate why embedded Rust was chosen instead of the two above mentioned approaches? Do these reasons justify the overhead that mixing Go and Rust brings both in maintanenance, complexity and debugging?

Besides the main reasons you outlined (Rust code performant/safe/well tested), using symblib allows us to bring this feature to production quickly, and solves a number of issues we have with deployed Go binaries. We do plan to also use symblib for extracting/uploading symbols from other native (non-Go) binaries, albeit this doesn't need to take place in-process in the agent, and can be a separate utility.

Regardless, I'm not opposed to doing Go symbolization in Go, assuming someone implements it we could evaluate and transition. But we don't have that right now (and at least at Elastic, have no plans to work on it) and we do have symblib.

@fabled
Copy link
Contributor

fabled commented Mar 17, 2025

using symblib allows us to bring this feature to production quickly, and solves a number of issues we have with deployed Go binaries

If going this way, I'd rather then use symblib as the native code dynamic symbolizer. And have it symbolize native code. We could limit to using dynamic symbols only if wanted. But getting some symbols for C-code with this would make sense. Rather than special case this for Golang only.

Regardless, I'm not opposed to doing Go symbolization in Go, assuming someone implements it we could evaluate and transition. But we don't have that right now (and at least at Elastic, have no plans to work on it) and we do have symblib.

Also looking at Rust code a bit, it seems to mmap files which is sort of good, but also previously unused. Also since Go binaries can contain C-code and Dwarf, the symblib dwarf code will get used for these executable bringing a lot of potentially new unexpected things: like automatically uncompressing the compressed dwarf to large temporary files. Edit: I missed that the Rust API used only looks at gopclntab.

IMHO the symblib code would probably need a mode the host agent can safely use for all binaries to do dynamic/elf/gopclntab symbolization but skip dwarf and compressed things. The integration as "native symbolizer" would be much cleaner than trying to special Golang.

@florianl
Copy link
Contributor Author

IMHO the symblib code would probably need a mode the host agent can safely use for all binaries to do dynamic/elf/gopclntab symbolization but skip dwarf and compressed things. The integration as "native symbolizer" would be much cleaner than trying to special Golang.

That is the general idea going forward. At the moment the scope is limited and focused on Go as from this ecosystem there is the highest demand for Symbols.

}

frameID := libpf.NewFrameID(libpf.NewFileID(uint64(frame.File), uint64(frame.File)),
libpf.AddressOrLineno(symbolsSlice[0].line_number))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched the AddressOrLineno part of frameID with 5bf7fd7. Previously pc was used, but this generated too much different frame IDs for the same function/source file/source line combination.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does prevent using the data for PGO. Do you have statistics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't collect statistics. Like everywhere else for the on CPU sampling approach, leave frames are different which causes a high number of variance and differences in frame IDs.

Copy link
Contributor

@fabled fabled left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional new comments to clean up a bit.

Also, how much the profiler executable size goes up with this now that it pulls static Rust stuff?

frameID := libpf.NewFrameID(libpf.NewFileID(uint64(frame.File), uint64(frame.File)),
libpf.AddressOrLineno(symbolsSlice[0].line_number))

trace.AppendFrameID(libpf.GolangFrame, frameID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this provide the mapping information similar as the non-symbolized native code is done?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point Go frames are treated as interpreter and no longer as native code. So I guess the answer here is no, and mapping information is not provided similar to other native code.

}

frameID := libpf.NewFrameID(libpf.NewFileID(uint64(frame.File), uint64(frame.File)),
libpf.AddressOrLineno(symbolsSlice[0].line_number))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does prevent using the data for PGO. Do you have statistics?

@fabled
Copy link
Contributor

fabled commented Mar 18, 2025

A little note that also #407 adds golang interpreter that does quite different things.

@florianl
Copy link
Contributor Author

Also, how much the profiler executable size goes up with this now that it pulls static Rust stuff?

#408 @ 7616b6c

$ ls -lia ebpf-profiler 
42649440 -rwxr-xr-x 1 user user 32535648 Mar 18 11:46 ebpf-profiler

main @ 422bd6b

$ ls -lia ebpf-profiler 
42649394 -rwxr-xr-x 1 user user 30313560 Mar 18 11:48 ebpf-profiler

florianl and others added 12 commits March 20, 2025 12:01
Add symbolization functionality for Go executables.

Signed-off-by: Florian Lehner <[email protected]>
Signed-off-by: Florian Lehner <[email protected]>
Signed-off-by: Florian Lehner <[email protected]>
Signed-off-by: Florian Lehner <[email protected]>
Signed-off-by: Florian Lehner <[email protected]>
Signed-off-by: Florian Lehner <[email protected]>
@florianl florianl force-pushed the interpreter-golang branch from 3222c8d to 9a26452 Compare March 20, 2025 11:08
@florianl
Copy link
Contributor Author

Force pushed to resolve merge conflict with main branch.

@florianl florianl force-pushed the interpreter-golang branch 3 times, most recently from 8b5ea8d to 77bd162 Compare March 21, 2025 11:04
@florianl florianl force-pushed the interpreter-golang branch 8 times, most recently from 8be0993 to 9a26452 Compare March 21, 2025 11:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants