Adding Numerical Libraries to Docker for C++ Microservices
There are a variety of frameworks and tools available for implementing a microservice architecture, however, it isn’t always clear how to expose native code like C or C++ code within a wider microservice system. In this series, we have covered the basics of a C++ Microservices deployment including:
- Deploying HydraExpress into a Docker container.
- Deploying custom C++ Servlet instances.
- Optimizing a Docker container to minimize space.
With those basics in place, this article will look at deploying a new C++ servlet that relies on the IMSL C Numerical Library (CNL), demonstrating how 3rd party libraries can be incorporated into a Docker application to support writing more advanced servlets.
Create a Skeleton for the New Service
For this example we’ll be leveraging CNL as an example 3rd party library, however the same basic approach should be applicable to any 3rd party library you may want to rely on.
We’ll be building on the Dockerfile that was assembled at the end of the Docker optimization article. (Go grab it now and follow along!)
We’ll start by adding a skeleton for our new service to the build infrastructure and the Dockerfile. Similar to the “HelloWorldServlet” we’re currently deploying, we’ll create a new servlet context “random”. Within that we’ll create a new servlet “UniformServlet” that will return a uniform distribution of random numbers and use our existing “hello” context, servlet, and build infrastructure as a reference to create the new context. Those files (with the necessary changes from their “hello” counterparts highlighted) are outlined below:
src/random/UniformServlet.cpp |
---|
#include <string>
#include <rwsf/servlet/ServletOutputStream.h>
#include <rwsf/servlet/http/HttpServlet.h>
#include <rwsf/servlet/http/HttpServletRequest.h>
#include <rwsf/servlet/http/HttpServletResponse.h>
class UniformServlet : public rwsf::HttpServlet {
public:
void doGet(rwsf::HttpServletRequest& request, rwsf::HttpServletResponse& response) {
response.setContentType("text/html");
rwsf::ServletOutputStream& out = response.getOutputStream();
out.println("<html><body><h1>Uniform!</h1></body></html>");
}
};
RWSF_DEFINE_SERVLET(UniformServlet)
We’ll revisit the implementation of this servlet once we’ve deployed CNL, but for now we’ll just return a simple HTML message.
src/random/WEB-INF/web.xml |
---|
<web-app>
<servlet>
<servlet-name>Uniform</servlet-name>
<servlet-class>random.createUniformServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Uniform</servlet-name>
<url-pattern>/uniform</url-pattern>
</servlet-mapping>
</web-app>
src/random/CMakeLists.txt |
---|
cmake_minimum_required(VERSION 3.9)
project(random VERSION 1.0 LANGUAGES CXX)
add_library(random SHARED UniformServlet.cpp)
target_compile_features(random PRIVATE cxx_auto_type)
target_link_libraries(random RWSF::Servlet)
src/CMakeLists.txt |
---|
cmake_minimum_required(VERSION 3.9)
project(servlets VERSION 1.0 LANGUAGES CXX)
add_library(RWSF::Servlet SHARED IMPORTED)
set_target_properties(RWSF::Servlet PROPERTIES
IMPORTED_LOCATION $ENV{RWSF_HOME}/lib/librwsf_servlet20012d.so)
target_compile_definitions(RWSF::Servlet INTERFACE -D_RWCONFIG=12d)
target_include_directories(RWSF::Servlet INTERFACE $ENV{RWSF_HOME}/include)
add_subdirectory(hello)
add_subdirectory(random)
We’ll also need to update our Dockerfile to deploy the new context to our container:
Dockerfile |
---|
…
COPY --from=servlet_build /src/hello/WEB-INF ${RWSF_HOME}/apps/servlets/hello/WEB-INF/
COPY --from=servlet_build /build/random/librandom.so ${RWSF_HOME}/apps-lib/
COPY --from=servlet_build /src/random/WEB-INF ${RWSF_HOME}/apps/servlets/random/WEB-INF/
COPY entrypoint.sh /entrypoint.sh
…
Add the Numerical Library to a Container
Now that we have skeleton for our new servlet in place, we can move on to incorporating IMSL C into our container.
First, we need to download and install IMSL C, just as we did for HydraExpress. The process for the two is very similar, and they have similar requirements. We’ll start by creating a base image to avoid duplicating work between the two installations:
Dockerfile |
---|
FROM centos:7 AS install
RUN yum install -y wget
RUN mkdir -p /opt/download
FROM install AS hydraexpress_install
…
With our base image in place, we can replicate hydraexpress_install to install IMSL C.
For this example we’ll deploy the IMSL C 2019 Evaluation release of the product. Similar to HydraExpress, deploying a different version or different variant of the product will require slightly different options when invoking the installer, but once installed the behavior and process should be the same. Since we’ll be using an evaluation version of IMSL C, you’ll need an evaluation license. If you’ve already requested an evaluation, the imsl_eval.dat file should be attached to your confirmation email.(If you need a license to try this contact us.)
…
RUN /opt/download/hydraexpress.run \
--mode unattended \
--prefix /opt/perforce/hydraexpress \
--license-file /opt/download/license.key
FROM install as cnl_install
RUN wget -q -O /opt/download/cnl.run \
https://dslwuu69twiif.cloudfront.net/imsl/cnl/2019/cnl-2019.0.0-lnxgc485x64_eval.run
RUN chmod a+x /opt/download/cnl.run
RUN /opt/download/cnl.run \
--mode unattended \
--prefix /opt/perforce
COPY imsl_eval.dat /opt/perforce/license/imsl_eval.dat
FROM centos:7 AS servlet_build
…
Update Servlet to Provide Random Numbers Function
With IMSL C now available, we can update our build script to incorporate IMSL C into our servlet to provide the function generating our random numbers. We’ll update our Uniform servlet so that it accepts a parameter, the number of random numbers it should generate, and returns that number of numbers. We’ll use IMSL’s imsl_d_random_uniform
function to generate the random numbers that will be returned:
src/random/UniformServlet.cpp |
---|
…
#include <rwsf/servlet/http/HttpServletResponse.h>
#include <imsl.h>
class UniformServlet : public rwsf::HttpServlet {
public:
void doGet(rwsf::HttpServletRequest& request, rwsf::HttpServletResponse& response) {
auto count = std::stoi(request.getQueryString());
typedef std::unique_ptr<double[], void(*)(void*)> double_array;
double_array values(imsl_d_random_uniform(count, 0), imsl_free);
response.setContentType("text/plain");
auto& out = response.getOutputStream();
for (size_t i = 0; i < count; ++i) {
out.println(values[i]);
}
}
};
…
We’ll also need to update our CMake configuration to include the IMSL C libraries. We’ll start with the root CMakeLists.txt to introduce the new libraries:
src/CMakeLists.txt |
---|
…
target_include_directories(RWSF::Servlet INTERFACE $ENV{RWSF_HOME}/include)
find_package(OpenMP)
add_library(IMSL::CMath SHARED IMPORTED)
set_target_properties(IMSL::CMath PROPERTIES
IMPORTED_LOCATION $ENV{CNL_DIR}/$ENV{LIB_ARCH}/lib/libimslcmath_imsl.so)
target_include_directories(IMSL::CMath INTERFACE $ENV{CNL_DIR}/$ENV{LIB_ARCH}/include)
target_link_libraries(IMSL::CMath INTERFACE OpenMP::OpenMP_CXX)
add_subdirectory(hello)
…
Next, we need to leverage the new imported library when building the random servlet context library:
src/random/CMakeLists.txt |
---|
…
target_compile_features(random PRIVATE cxx_auto_type)
target_link_libraries(random RWSF::Servlet IMSL::CMath)
With our changes to our servlet complete, we can focus on updating the build step in our Dockerfile. Since we’re now depending on IMSL C, we’ll need to deploy it and configure the environment so that our Servlet can link against its libraries:
Dockerfile |
---|
…
ENV RWSF_HOME /opt/perforce/hydraexpress
ENV CNL_DIR /opt/perforce/imsl/cnl-2019.0.0
ENV LIB_ARCH lnxgc485x64
COPY --from=hydraexpress_install ${RWSF_HOME} ${RWSF_HOME}
COPY --from=cnl_install ${CNL_DIR}/${LIB_ARCH} ${CNL_DIR}/${LIB_ARCH}
RUN yum install -y epel-release
…
Deploy the Final Image
Finally, we can look towards deploying our new dependency to the final image. We need to copy the IMSL library that we depend on along with the evaluation license file to our final image.
We also need to set the IMSL_LIC_FILE
to allow IMSL C to locate the license file at runtime. Since our final image is based on a slim Alpine Linux image, we need to install an additional package, libgomp. We also need to add /lib64
to the LD_LIBRARY_PATH
environment variable to allow dependencies to be found by the IMSL library.
Dockerfile |
---|
…
FROM alpine:latest
RUN apk update --no-cache && apk upgrade --no-cache && apk add --no-cache bash libstdc++ libc6-compat libgomp
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
ENV RWSF_HOME /opt/perforce/hydraexpress
ENV IMSL_LIC_FILE /opt/perforce/license/imsl_eval.dat
ENV LD_LIBRARY_PATH /lib64:$LD_LIBRARY_PATH
COPY --from=hydraexpress_deploy ${RWSF_HOME} ${RWSF_HOME}
COPY --from=servlet_build /build/hello/libhello.so ${RWSF_HOME}/apps-lib/
COPY --from=servlet_build /src/hello/WEB-INF ${RWSF_HOME}/apps/servlets/hello/WEB-INF/
COPY --from=servlet_build /build/random/librandom.so ${RWSF_HOME}/apps-lib/
COPY --from=servlet_build /src/random/WEB-INF ${RWSF_HOME}/apps/servlets/random/WEB-INF/
COPY --from=cnl_install /opt/perforce/imsl/cnl-2019.0.0/lnxgc485x64/lib/libimslcmath_imsl.so \
/opt/perforce/imsl/cnl-2019.0.0/lnxgc485x64/lib/libimslcmath_imsl.so
COPY --from=cnl_install /opt/perforce/license /opt/perforce/license
COPY entrypoint.sh /entrypoint.sh
With the Dockerfile updated, we’re ready to rebuild and redeploy our C++ microservice:
$ docker build -t hydraexpress .
…
$ docker run --rm -it -p 8090:8090 hydraexpress
*******************************************************************************
RWSF (TM) - Server Control Script
Copyright (c) 2001-2020 Rogue Wave Software, Inc., a Perforce company.
All Rights Reserved.
*******************************************************************************
RWSF_HOME = /opt/perforce/hydraexpress
RWSP_HOME = /opt/perforce/hydraexpress/3rdparty/sourcepro
Starting Rogue Wave Agent...
INFO| Loading context: /hello/
INFO| Loading context: /random/
INFO| Locale directory set to [/opt/perforce/hydraexpress/conf/locale]
INFO| Default locale set to [en_US]
INFO| Loading locale [en_US], catalog [messages_en_US.xml]
INFO| Starting 'HTTP/1.1' connector...
Finally, we can use curl test our new random number generation, requesting five random numbers:
$ curl http://localhost:8090/random/uniform?5
0.816719443451948
0.603686096893477
0.15223048867296
0.537823126436129
0.193286012016836
Providing Numeric Functionality in C++ Microservice
We’ve successfully deployed a C++ microservice that leverages IMSL C to provide numeric functionality. You can apply the same patterns to deploy other 3rd party libraries that may be necessary to fully implement our microservice and make it accessible to the other services in our infrastructure.
Interested in trying this for yourself, request an evaluation copy of HydraExpress and IMSL today.
For Further Reading
- Part 1: C++ Microservices in Docker.
- Part 2: Building Custom C++ Servlets.
- Part 3: Optimizing a Container Image.