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

Improve bytebuddy class enhance #521

Closed
wants to merge 24 commits into from
Closed

Conversation

kylixs
Copy link
Member

@kylixs kylixs commented Apr 29, 2023

Improve bytebuddy class enhance

  • Add bytebuddy patch to support retransform class multiple times compatible with other javaagent without class cache.
  • Support for hotswap classes that are enhanced by skywalking plugins, like Spring MVC Controller, etc.

@Superskyyy Superskyyy added this to the 8.16.0 milestone Apr 29, 2023
@wu-sheng
Copy link
Member

I will be back on this at May 3rd.
Thanks for your detailed explanation.
I will take a close look and provide feedback.

@wu-sheng wu-sheng added the enhancement New feature or request label Apr 29, 2023
Copy link
Member

@wu-sheng wu-sheng left a comment

Choose a reason for hiding this comment

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

2 main things should be considered

  1. Too many hijacking through reflection. This relies on byte-buddy internal codes/implementations. We don't want to make further upgrading hard. Please work with upstream and try to find a new way, or request byte-buddy to expose new APIs for this.
  2. As you are introducing a new way of retransfer, removing the old mechanism for the class cache is better because it isn't always perfect.

Comment on lines 68 to 69
DynamicType.Builder<?> newClassBuilder, ClassLoader classLoader,
EnhanceContext context) throws PluginException {
Copy link
Member

Choose a reason for hiding this comment

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

These seem some local code style differences. Could you revert these changes?

Comment on lines 117 to 120
.intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.withDefaultConfiguration()
.to(BootstrapInstrumentBoost
.forInternalDelegateClass(constructorInterceptPoint
.getConstructorInterceptor()))));
Copy link
Member

Choose a reason for hiding this comment

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

Please revert all unnecessary changes.

@@ -161,9 +175,10 @@ public DynamicType.Builder<?> transform(final DynamicType.Builder<?> builder,
final JavaModule javaModule,
final ProtectionDomain protectionDomain) {
LoadedLibraryCollector.registerURLClassLoader(classLoader);
DelegateNamingResolver.reset();
Copy link
Member

Choose a reason for hiding this comment

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

I am a little confused about this reset. #transform should be called for every type, as a static method, we reset every time?

Copy link
Member Author

Choose a reason for hiding this comment

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

There seems to be a problem here, I'm not sure if there will be nested retransform multiple classes, which usually occurs the first time a class is loaded. If so, it would be unreasonable to reset all of them, just reset the counter of the class currently being transformed.

Copy link
Member

Choose a reason for hiding this comment

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

If byte-buddy could expose the whole method signature rather than the name, I believe this would not be an issue.
How about asking for upstream first?

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean? Tell me more about it.

Comment on lines 39 to 41
Field nativeMethodStrategyField = clazz.getDeclaredField("nativeMethodStrategy");
nativeMethodStrategyField.setAccessible(true);
nativeMethodStrategyField.set(agentBuilder, new SWNativeMethodStrategy(PREFIX));
Copy link
Member

Choose a reason for hiding this comment

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

These seem a hijacking and relying on upstream(bytebuddy) doesn't change the internal implementation.
Considering bytebuddy is always very friendly to our community, you are better to submit a proposal/request to bytebuddy, and discuss the official API to do so.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, That's what I am thinking.

Comment on lines 64 to 66
Field specialMethodInvocationField = auxiliaryTypeClass.getDeclaredField("specialMethodInvocation");
specialMethodInvocationField.setAccessible(true);
SpecialMethodInvocation specialMethodInvocation = (SpecialMethodInvocation) specialMethodInvocationField.get(auxiliaryType);
Copy link
Member

Choose a reason for hiding this comment

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

This is also another kind hijacking, please considering working with the upstream.

Copy link
Member Author

Choose a reason for hiding this comment

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

ok


} else if (auxiliaryTypeClassName.endsWith("Pipe$Binder$RedirectionProxy")) {
// get MethodDescription from field 'sourceMethod'
Field sourceMethodField = auxiliaryTypeClass.getDeclaredField("sourceMethod");
Copy link
Member

Choose a reason for hiding this comment

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

Same hijacking concern.


} else if (auxiliaryTypeClassName.endsWith("FieldProxy$Binder$AccessorProxy")) {
// get fieldDescription
Field fieldDescriptionField = auxiliaryTypeClass.getDeclaredField("fieldDescription");
Copy link
Member

Choose a reason for hiding this comment

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

Same hijacking concern.

import net.bytebuddy.utility.RandomString;

/**
* Design to generate fixed name for CacheValueField (cache delegating Method)
Copy link
Member

Choose a reason for hiding this comment

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

This comment doesn't fit the codes. Please reword this, you are building a ContextFactory with target type name based suffix on the top of Implementation.Context.Default.

/**
* Remove duplicated fields of instrumentedType
*/
public class SWAsmVisitorWrapper implements AsmVisitorWrapper {
Copy link
Member

Choose a reason for hiding this comment

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

I am not fully following this. Are you trying to cache all original methods before the instrumentation?
The name RemoveDuplicatedFieldsClassVisitor is not very clear to me.
Please reword the comments for SWAsmVisitorWrapper

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll check it agian.

@wu-sheng
Copy link
Member

wu-sheng commented May 2, 2023

Hi @kylixs Thanks for providing your way of debugging and resolving the issue. I believe locking the field and method name for auxiliary class and method is a way to resolve the transfer issue.
But meanwhile, I am not very certain, whether this is always perfect, as SkyWalking agent kernel has been used in many other cases, such as other APMs' agent are actually a fork of upstream.
Is this way going to bring a new conflict between skywalking agent and skywalking agent fork(re-distribution)? Our answer doesn't have to be that, we support this kind of hybrid deployment, but I want to include this as a consideration.

@kylixs
Copy link
Member Author

kylixs commented May 2, 2023

It's a bit complicated, and we haven't tested enough scenarios to know if there are other compatibility issues. I came up with a few ideas:

  • Provide a fallback transform mode. If there is a compatibility problem, users can fall back to the previous mode. We can keep improving it.
  • Create a retransform javaagent for automated testing every plugin for compatibility, and may need to modify testing tools to support it.

@wu-sheng
Copy link
Member

wu-sheng commented May 2, 2023

Provide a fallback transform mode. If there is a compatibility problem, users can fall back to the previous mode. We can keep improving it.

Falling back usually is too complex to maintain.


About the re-transfer, once we have a clear path about how skywalking agent works in that agent, I think we are fine.
The question I left is only about whether we are going to conflict with our own fork, due to names are always same in two agents.

@kylixs
Copy link
Member Author

kylixs commented May 2, 2023

About the re-transfer, once we have a clear path about how skywalking agent works in that agent, I think we are fine.
The question I left is only about whether we are going to conflict with our own fork, due to names are always same in two agents.

The key issue is the class and method that the plug-in intercepts. As you can see from the documentation of the process, the processing path is fairly clear, but it may not include all of the paths. Currently, all skywalking plug-ins are supported, but we don't know if any of the custom plug-ins have compatibility issues. At present, we have a better understanding of ByteBuddy processing process and debugging tools, which can solve the problem quickly.

@kylixs
Copy link
Member Author

kylixs commented May 2, 2023

The question I left is only about whether we are going to conflict with our own fork, due to names are always same in two agents.

I don't quite understand this. Could you explain it in detail?

@wu-sheng
Copy link
Member

wu-sheng commented May 2, 2023

The question I left is only about whether we are going to conflict with our own fork, due to names are always same in two agents.

I don't quite understand this. Could you explain it in detail?

The name of fields and methods are predictable and will be same.
There are other solutions of other vendors using our agent to build their own distributions, which may be APM agents or even others.
Once the names are locked, would they conflict with each others?

@kylixs
Copy link
Member Author

kylixs commented May 2, 2023

ByteBuddy uses random names, which is essential in dynamic proxy/class creation, but in APM scenarios, dynamic names are bad.
We use sw_xxx$ as a specific flag for auxiliary class names, field names, and method names that do not conflict with other vendor names.
Vendors can extend with the Skywalking agent interceptor mechanism, or register their own class transformer, which is fine. The locking name only affects the Skywalking agent itself, and vendors using their own ByteBuddy instances are unchanged.

@wu-sheng
Copy link
Member

wu-sheng commented May 2, 2023

I know they can, I mean many of them are not doing this.
I think you still are not following what is redistribution of open source. Thinking about Ubuntu relationship with upstream Linux. Take SkyWalking as upstream Linux, vendor agents are a kind of Ubuntu.

They don't change much, and customize many things. They just enhance some features, and release under their brand.
This is totally reasonable and common case for a popular open source project.

@kylixs
Copy link
Member Author

kylixs commented May 2, 2023

If they haven't modified the Skywalking agent's plugin interceptor flow, they should be compatible. If they change the interceptor mechanism or the ByteBuddy option, they need to be compatible with the new mode. More information? Are there any known redistribution projects of Skywalking agent?

@wu-sheng
Copy link
Member

wu-sheng commented May 2, 2023

I can't provide this information as I don't have their owner permission, sorry.
I just want to mention this as a case. Such as if they have a modified plugin/interceptor but targeting the same class and same method.

@kylixs
Copy link
Member Author

kylixs commented May 2, 2023

I can't provide this information as I don't have their owner permission, sorry. I just want to mention this as a case. Such as if they have a modified plugin/interceptor but targeting the same class and same method.

Without enough information, it's hard to evaluate the results. Some possibilities:

  • Custom plugins: For example, two plugins enhance the same method of the same class. Not tested yet, don't know if this is currently supported. If we want to support it, we should define it as a feature and add test cases.
  • Custom class transformer: Use a different ByteBuddy/AgentBuilder instance so it is not affected by name locking.

@kylixs
Copy link
Member Author

kylixs commented May 28, 2023

How about two skywalking agents for one target? I don't expect we would do two interceptors for one target inside skywalking. The other one is fork distribution. Will that work?

Intercept same classes with two AgentBuilders are ok. Do you have any ideas on how to add specific test cases?

@wu-sheng
Copy link
Member

How about two skywalking agents for one target? I don't expect we would do two interceptors for one target inside skywalking. The other one is fork distribution. Will that work?

Intercept same classes with two AgentBuilders are ok. Do you have any ideas on how to add specific test cases?

What do you mean OK? We only have one, right? You changed it to multiple? Or one per plugin or something?

@kylixs
Copy link
Member Author

kylixs commented May 30, 2023

What do you mean OK? We only have one, right? You changed it to multiple? Or one per plugin or something?

Currently, it is supported to install an interceptor on the same method in two AgentBuilders respectively, resulting in a chain of proxy methods.

I think there are a few scenarios where conflicts need to be resolved:

  1. The interception method of user-defined plug-ins may overlap with the existing plug-ins of Skywalking agent.
  2. Two Skywalking agent plug-ins may handle the same method.

Interception methods can overlap in many ways, such as two plug-ins matching different annotations but having both annotations in the same class or method.

I think it is better to solve the above problems in one agent, because putting it into two agents will bring maintenance workload and confusion. I think users would prefer to maintain one custom plug-in rather than two agents.

One idea is to identify plug-ins that overlap and use different AgentBuilders to handle them. I'll try to see if this works.

@wu-sheng
Copy link
Member

Generally, the ides works for me.

@wu-sheng wu-sheng removed this from the 8.16.0 milestone May 31, 2023
@kylixs
Copy link
Member Author

kylixs commented Jun 11, 2023

Took some time to try two solutions:

  1. Each plugin uses an isolated AgentBuilder, and it turns out that each AgentBuilder instance do a lot of repetitive processing, which makes Java application start too slowly.

  2. Each plugin uses an isolated Transformer and found that ByteBuddy is not supported.
    I made some rough changes to the ByteBuddy AgentBuilder code, and it is now running, loading two plugins at the same time to enhance the classes of Spring Controller, and they both work.

I will submit it to the ByteBuddy community and need more discussion and design.

ByteBuddy code: https://github.com/kylixs/byte-buddy/tree/separate-transformer
Custom spring mvc plugin: https://github.com/kylixs/spring-mvc-plugin-demo

@wu-sheng
Copy link
Member

I remembered that @raphw mentioned, the advice annotation rather than delegator could support multiple interceptors. But I didn't try.

@kylixs
Copy link
Member Author

kylixs commented Jun 11, 2023

I remembered that @raphw mentioned, the advice annotation rather than delegator could support multiple interceptors. But I didn't try.

We can intercept the target class method using Advice, and then filter the matching plugins in the Advice static proxy method and invoke the intercepting classes of plugins.
It seems that this method can adapt to the original plugin, but need to change the plugin mechanism, I try it.

@wu-sheng
Copy link
Member

wu-sheng commented Jun 11, 2023

It is good to test various possibilities. Eventually, we should keep changes as less as possible, to keep stability.

@raphw
Copy link

raphw commented Jun 12, 2023

Sorry, but this does not go into a direction that I see being merged into Byte Buddy. But you are very welcome to implement this on top of existing APIs and to offer it as an extension.

@wu-sheng
Copy link
Member

@kylixs I am back on this as the 9.5.0 had been released. AFAIK, there are several things scoped in this PR, which makes it very hard to review and move forward from tech and stable perspectives.
So, I want to separate the issue into the following parts

  1. Support predictable field names for interceptors
  2. Support retransfer with other agents. I think it is easy if we landed <1>
  3. Support multiple interceptors for one single method. This may be the hard part, but to be honest, this is not a very common use case inside SkyWalking.

So, how about we are going to do <1> only first? I think from your existing changes, it should be easy to support that without hijacking?

@kylixs
Copy link
Member Author

kylixs commented Jun 22, 2023

@wu-sheng

So, how about we are going to do <1> only first? I think from your existing changes, it should be easy to support that without hijacking?

I agree with your idea, but I think <1> and <2> are the same things, and have been reached.
I'm not sure to support that without hijacking, maybe make it a little more elegant.

<3> Support multiple interceptors for one single method.

What has been tried:

  1. Each plugin create a separate Transformer, requiring AgentBuilder to support isolated transformers.
  2. With Advice transformer, the enhanced code is bloated and may be repeatedly enhanced, which is a bit troublesome to control.

Next step:
Make InstMethodsInter and so on to support for calling multiple XxxAroundInterceptors of different plugins.

@wu-sheng
Copy link
Member

What has been tried:
Each plugin create a separate Transformer, requiring AgentBuilder to support isolated transformers.
With Advice transformer, the enhanced code is bloated and may be repeatedly enhanced, which is a bit troublesome to control.
Next step:
Make InstMethodsInter and so on to support for calling multiple XxxAroundInterceptors of different plugins.

That is fine.It can wait.

@wu-sheng
Copy link
Member

I agree with your idea, but I think <1> and <2> are the same things, and have been reached.
I'm not sure to support that without hijacking, maybe make it a little more elegant.

I don't think we will avoid all hijackings, but we could limit the scope, currently, they are too wide. And we could add tests to verify them when we need to bump up the version of Byte-buddy.

@wu-sheng
Copy link
Member

@kylixs Please open a new and clean changes PR. Let's focus on one at a time.

@kylixs kylixs closed this Jun 23, 2023
@kylixs
Copy link
Member Author

kylixs commented Jun 23, 2023

move to: #561

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants