Dependency collisions furnish plenty of headaches and late nights while implementing Lambda Architecture. Java class loading containers tend toward complexity and many frameworks such as Hadoop, JCascalog, and Storm provide tons of transitive dependencies but no dependency isolation for third party Lambda code deployed in the same JVM. So currently to benefit from running in the same JVM, all the transitive dependencies come along for the ride. This impacts:
- Portability: Lambda logic shared in the batch layer and the speed layer must cope with at least two different sets of dependencies.
- Manageability: framework version upgrades may cause hard failures or other incompatibilities in Lambda logic due to changing dependency versions.
- Performance: solving the problem by forcing JVM isolation incurs overhead from IPC or network chatter. Likewise, utilizing scripting or dynamic languages merely hides the problem rather than solving it, while imposing additional performance penalties.
- Complexity: developing Lambdas requires knowledge and accommodation of all the dependencies of all frameworks targeted for deployment.
Many developers and architects rightfully shy away from deploying bloated dependency container frameworks such as Spring or OSGI inside of a big data framework. Big data and fast data provide enough complexity already. So lets look at how we can use a simple pattern, along with basic core Java libraries, to avoid “Dependency Hell“.
Signs you may be encountering this problem include these types of exceptions occurring after deployment:
- java.lang.NoSuchMethodError: when a method no longer exists which your code depends on
- java.lang.ClassNotFoundException: when a class your code uses cannot be found in the deployed class path
- java.lang.IllegalAccessException: when conflicting versions of an API mark methods or fields private
Please reference my github project for a complete working implementation along with unit tests.
The Java Service Loader framework introduced in JDK 1.6 provides a way to dynamically bind an implementation class to an interface while specifying an alternate class loader. Loading a Lamda implementation with ServiceLoader forces all code called within the context of the Lamda to use the same class loader. This allows creating a service which supports parent-first class loading, child-first class loading, or child-only (or hermetic) class loading. In this case, the hermetic loader prevents any possible dependency collisions. To create a hermetic loader, we need simply utilize the built-in URLClassLoader in Java. Our implementation jar can reside on the local file system, on a web server, or in HDFS: anywhere a URL can point to. For the parent class loader, we specify the Java bootstrap class loader. So we can implement a hermetic class loading pattern in one line of code:
ServiceLoader<Map> loader = ServiceLoader.load(Map.class, new URLClassLoader(urls, Map.class.getClassLoader()));
Note that we intentionally avoid calling ClassLoader.getSystemClassLoader() in order to prevent the calling context (such as Hadoop or Hive) form polluting the Lambda class path. Core packages such as java.lang and java.util use the bootstrap class loader, which only carries core Java dependencies shipped as part of the JDK. The diagram above shows how the LambdaBoot framework fits within a big data framework such as Hadoop.
Mapping the World
In the example above, we use a Map interface to interact with the Lambda. This allows us to avoid having a separate jar containing a Service Provider Interface (SPI). Instead, we can subclass the Map and provide any behavior desired by intercepting get and put calls to specific keys. By optionally returning a Future from a get or put call on the Map, we get asynchronous interaction as well if desired. In addition, the non-sensitive Map keys can provide metadata facilities. In the example of linear regression implemented as a Map, we use a conflicting version of Apache Commons Math not yet supported by Hadoop to calculate the regression.
Using a very simple pattern, we can deploy Hermetic Lambdas to any Java environment without fear of dependency collisions. The ServiceLoader also acts as a service registry, allowing us to browse metadata about available Lambdas, dynamically load new Lamdas, or update existing Lambdas.