Under the Hood: A Closer Look at Java Remote Debugging
Over the years, we have seen a transformation of software architecture with autonomous entities that allow better development practices. Today’s teams are more agile and autonomous. In terms of infrastructure, this usually translates to different parts of the codebase running on separate servers, conventionally in the form of container instances, FaaS functions or pods. A movement to cloud technologies made all this possible.
However, this also means that even though the entire system does operate as a single entity in production, the part of the codebase worked on during the development phase may be disconnected from the other parts of the system and crucial resources.
This, therefore, warrants a need to rethink our debugging practices. Debugging environments and writing test cases that depend on these unavailable resources becomes difficult. This is especially true when considering the architectures that developers would gravitate toward when developing for the cloud. These can include a combination of concepts such as hybrid monoliths on the cloud, event-driven serverless configurations, active/active multiregion setups and many more.
As a result of these pain points, we see the use of remote debugging come into play, which enables developers to connect to remote systems while debugging. By further leveraging non-breaking breakpoints, developers can set non-intrusive breakpoints at any line of their codebase running in these remote environments. This allows the remote debugger to capture all crucial insights such as metrics and snapshots containing variable states at the point of the non-breaking breakpoint in the remote system. Better yet, the developer will procure all these insights without having to disrupt the flow of their systems.
Overall, it allows debugging a system when the system itself is not in the local environment of the developer by setting up a connection between the local environment and the service to be debugged that sits on a remote server. This article aims to expand on how this connection can actually be achieved with Java applications and even walk through the mechanisms of non-breaking breakpoints.
The Mechanics of Java Remote Debugging
As noted, the benefits of remote debugging provide the right progressive mindset in this ever-changing world of software development. The main aim of the technique is to be able to connect the debugging environment with the target system that resides in a remote instance.
When diving deeper into how this connection can be made for Java-specific applications, we see the Java Debugger Platform Architecture (JDPA) come up.
As can be seen from the diagram above, the JDPA consists of three main interfaces. These are the Java Virtual Machine Tool Interface (JVM TI), the Java Debug Wire Protocol (JDWP) and the Java Debug Interface (JDI).
The high-level roles that these three components play are straightforward. The need for connection is filled by the JDWP, which defines the format of messages being communicated between the debugger and the remote system. Furthermore, the JDWP does not define the transport mechanism to ensure flexibility of use. Hence, the backend VM and the frontend debugger interface run on separate mechanisms.
The JDI can be thought of as simply a Java API that is aimed at capturing requests and relaying information to and from the debugger. This also means that the debugger interface can be writing in any language as long as it calls upon the correct set of API endpoints provided.
On the opposite end is the JVM TI which is a native programming interface. It communicates with the services inside the VM and can observe and control the execution of the Java applications.
When understanding how the three components operate in tandem to enable remote Java debugging, we can consider the functioning JPDA from two perspectives: the debugger and the debuggee.
Regardless of whether the interface is related to the debugger or the debuggee, there are two forms of activity in each interface. These are events and requests. However, requests are conventionally generated on the debugger side, whereas events are generated on the debuggee side. These events are the debuggee responses to requests for information pertaining to the current state of the debuggee application sitting on the remote VM.
Non-Breaking Breakpoints Flow
As mentioned earlier, non-breaking breakpoints are a valuable component of remote debugging, as they provide all the necessary insights without disruptions to the running application. This proves useful especially when the target system is running in production, but is crucial for debugging another service being developed in the local environment. This is just one use case; there are several other challenges that the technique helps overcome. In this section, though, let’s discuss how the JPDA performs in setting and responding to non-breaking breakpoints.
The first step is the setting of the non-breaking breakpoint in the local debugger UI. In doing so, the debugger calls upon the relevant set of endpoints in the JDI. Upon these calls, the JDI then generates a debugging state change request. This request is then converted into a byte stream as per the definition set by the JDWP. As mentioned before, there is no specific communication system that JDWP imposes, and this can be defined by those setting up the JDPA. In this case, it could be a socket.
By leveraging the JDWP, the JDI manages to send this request finally to the backend where the stream of bytes is first deciphered. After receiving the request, the relevant JVM TI functions are triggered to set the breakpoint in the Java application.
When the application being executed in the remote VM finally hits a breakpoint, events containing system information are then generated. The VM passes these events back to the frontend by calling the event-handling functions of the JVM TI and passing the breakpoint itself. This sets off a chain of operations in filtering and queuing the events, which are then finally sent as a stream of bytes over the JDWP.
The frontend then decodes the messages received from the JDWP and calls upon the event functions of the JDI, which kicks off JDI events. These events are then processed for their debugging information, which is displayed in the debugger console.
Conclusion: Remote Debugging Tools
As can be seen, the JDPA is a sophisticated system that enables us to perform debugging techniques. Owing to this complexity and low-level state of the JDPA, it can be difficult to perform remote debugging by using the JDPA.
Apart from the operational challenges of using the JDPA directly, there are other challenges. One of the main concerns is security as the technique requires the opening of ports into your remote VMs. Also, the logging concerns are not effectively mitigated as logs may be writing to the application itself, depending on the implementation. Hence, in the case that the application experiences an incident, those logs may become unavailable or inaccessible, defeating the debugging purpose of those logs.
However, all is not lost. The software development industry is swift at meeting the needs of effective development practices. We are now seeing a new era of debugging tools that are providing remote debugging solutions. Our product Thundra Sidekick is one such solution. It provides respite for developers debugging their cloud and distributed systems by giving them all the necessary remote debugging operations and insights.