基于Kubernetes、Docker的机器学习微服务系统设计——完整版

基于Kubernetes、Docker的机器学习微服务系统设计
基础篇 文本分类综述 特征选择综述 常见分类模型 算法性能评估
研究篇 RS中文分词 MP特征选择 NLV文本分类 快速kNN

1 概述

  本篇主要介绍基于Kubernetes、容器(Docker)、微服务技术等在机器学习中的实践应用。详细介绍了机器学习文本分类系统的设计与实现过程,以及云计算分布式系统的部署。

2 系统介绍

2.1 功能全览

  系统需要完成的功能点如下思维导图1所示:

图 1 云化微服务机器学习系统功能全览图

2.2 核心功能

  主要完成功能:

  1. 支持Docker镜像化发布,支持Kubernetes云化部署;
  2. 微服务化设计支持服务自治,支持服务扩缩容;
  3. 支持负载均衡、系统资源监控、资源编排;
  4. 统一设计轻量级通信RESTful API 接口框架,支持JSON格式请求;
  5. 支持多种机器学习算法,支持JSON格式参数配置;
  6. 支持中文分词:RobinSeg(RS)、IKAnalyzer(IK)、JEAnalysis(JE)、MmSeg4j(MS)、PaoDing(PD)、SmallSeg4j(SS)等;
  7. 支持特征选择算法:Document Frequency(DF)、Information Gain(IG)、(χ2)Chi-Square Test(CHI)、Mutual Information(MI)、Matrix Projection(MP)等;
  8. 支持分类算法:k-Nearest Neighbor(kNN)、Naïve Bayes(NB)、Support Vector Machine(SVM)、Normalized Vector(NLV)等;
  9. 支持Web图形化UI机器学习性能评估、数据可视化;

3 系统架构

3.1 云化架构图

  云化微服务机器学习系统架构如图2所示:

图 2 云化微服务机器学习系统架构图

3.2 架构说明

  整个系统采用云计算的架构设计。系统支持部署在传统的虚拟化技术(如KVM)或云计算IaaS层服务上(如Openstack等)。PaaS层采用Kubernetes+Docker的应用方式。
  整个系统的重点是SaaS层的设计开发,即微服务化的机器学习系统。图 2 所示红框蓝底部分为系统的核心部分。
  系统主要功能模块包括:公共库Comm-lib、微服务核(中文分词、预处理、特征选择、分类器)、RESTful微服务框架(微服务核加载 、HTTP API)、应用服务+WEB、管理维护等。
  公共库Comm-lib:包括基础功能,例如日志、配置、数学计算等;
  RESTful微服务框架:主要统一微服务接口,解耦与业务的关系,统一RESTful API。
  微服务核:按照微服务接口定义,关注自身的业务实现。实现中文分词、预处理、特征选择、分类器的独立功能。
  管理维护:主要包括Docker镜像化制作、发布,Kubernetes、Docker、微服务资源监控,资源的编排功能。
  应用WEB:如果把微服务看出深服务端,那么这里包含浅服务端应用和WEB客户端。服务端处理WEB分类任务的请求、调度和生命周期管理。WEB端显示任务运行的状态和机器学习的结果UI显示,还包括资源的监控显示。

4 云化部署

4.1 部署图

  云化微服务机器学习系统架构架构如图3所示:

图 3 云化微服务机器学习系统部署图

4.2 部署说明

  系统部署服务组件主要包括:ETCD、Docker 、Kubernetes Master、Kubernetes Node、Docker Private Registry、Glassfish Server、Flannel。
  ETCD:一个开源的、分布式的键值对数据存储系统,提供共享配置、服务的注册和发现。ETCD为Kubernetes提供默认的存储系统,保存所有集群数据,使用时需要为etcd数据提供备份计划。ETCD为Flannel 存储网络配置、分配的子网以及任何辅助数据(如主机的公网 IP)。
  Docker: 应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。
  Kubernetes Master:集群的管理控制中心。
  Kubernetes Node:提供Kubernetes运行时环境,以及维护Pod。
  Docker Private Registry:Docker 镜像私仓,存放开发的微服务镜像。
  Glassfish Server:Web 应用服务器,提供web应用的部署。也可以采用镜像化的方式部署。
  Flannel:常用的网络配置工具,用于配置第三层(网络层)网络结构。

4.3 部署实例

  如上图3所示,在一台服务器上部署实例。

  服务器配置如下:
    处理器:2颗Intel Xeon E5-2670 8核16线程
     内存:32G = 8X4G PC3-10600R 1333
     硬盘:240GB SSD + 4TB HDD
   操作系统:Ubuntu 14.04.5 LTS
  使用KVM启动6台虚拟机。

  虚拟机配置:
    处理器:4核
     内存:4G
     硬盘:80GB
   操作系统:CentOS 7.5

  虚拟机网络采用linux系统网桥管理工具配置网桥进行链接。Kubernetes Master和ETCD部署在一台虚拟机上。4台虚拟机作为Kubernetes Node节点,其上部署Docker、Flannel服务。一台作为Docker 私仓,部署Docker服务。

  软件版本
   ETCD版本:3.0.0
   Docker版本:1.12.6
   Kubernetes 版本:1.6.7
   Flannel版本:0.9.1
   Docker镜像仓库版本: 2.5

5 设计实现

5.1 RESTful微服务框架

  为了微服务的接口交互统一,本系统采用统一的框架模式。采用Jersey软件框架,Jersey 是开源的RESTful框架, 实现了JAX-RS (JSR 311 & JSR 339) 规范。

5.1.1 微服务框架图

  RESTful框架实现流程如图4所示:

图 4 RESTful框架实现流程图

5.1.2 微服务框架实现

  配置文件config.properties内容如下:

#restful API config
listen.ip=0.0.0.0
listen.port=8084

#thread pool config
thread.core.pool.size=4
thread.max.pool.size=4

#mirco server config
mircoServer.name=business
jar.path=file:business-1.0.jar
jar.actionClass=com.robin.action.BusinessAction

#log config
log.path=log/
log.prefix=business
# Level.ALL Level.FINEST Level.FINER Level.FINE Level.CONFIG 
# Level.INFO Level.WARNING Level.SEVERE Level.OFF
log.level=Level.INFO
log.file.limit=1048576
log.file.count=3

  通用资源类:

/**
 * <DT><B>描述:</B></DT>
 * <DD>通用资源类</DD>
 *
 * @version Version1.0
 * @author  Robin
 * @version <I> V1.0 Date:2018-05-21</I>
 * @author  <I> E-mail:xsd-jj@163.com</I>
 */
@Path("robin")
public class CommonResource {
   
    // 日志
    private static final Logger LOGGER = RobinLogger.getLogger();
    // 微服务
    private static MircoServiceAction mircoServer;
    // 配置的微服务名称
    private static final String CFG_MS_NAME;

    static {
   
        // 微服务名称配置文件检查
        CFG_MS_NAME = ConfigUtil.getConfig("mircoServer.name");
        
        String jarPath = ConfigUtil.getConfig("jar.path");
        URL url = null;
        try {
   
            url = new URL(jarPath);
        } catch (MalformedURLException ex) {
   
            LOGGER.log(Level.SEVERE, ex.getMessage());
        }

        URLClassLoader classLoader = new URLClassLoader(new URL[]{
   url}, Thread.currentThread()
                .getContextClassLoader());
        Class<?> actionClass = null;
        try {
   
            String actionClassName = ConfigUtil.getConfig("jar.actionClass");
            actionClass = (Class<?>) classLoader.loadClass(actionClassName);
        } catch (ClassNotFoundException ex) {
   
            LOGGER.log(Level.SEVERE, ex.getMessage());
        }
        if (null == actionClass) {
   
            LOGGER.log(Level.SEVERE, "actionClass is null");
            System.exit(-1);
        }
        try {
   
            mircoServer = (MircoServiceAction) actionClass.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
   
            LOGGER.log(Level.SEVERE, ex.getMessage());
        }
    }

    /**
     * Method handling HTTP GET requests. The returned object will be sent to
     * the client as "application/json" media type.
     *
     * @return String that will be returned as a application/json response.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getIt() {
   
        String cfgMsName = ConfigUtil.getConfig("mircoServer.name");
        return "Micro server [" + cfgMsName + "] is running...\n";
    }

    @POST
    @Path("{microService}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public JSONObject requestService(
            @PathParam("microService") String serverName,
            JSONObject reqJson) {
   
        JSONObject rspJson = null;
        if (!serverName.equals(CFG_MS_NAME)) {
   
            rspJson = new JSONObject();
            try {
   
                rspJson.put("status", "ERROR");
                rspJson.put("msg", "Mirco server name [" + serverName + "] error.");
            } catch (JSONException ex) {
   
                LOGGER.log(Level.SEVERE, ex.getMessage());
            }
            return rspJson;
        }

        if (null != mircoServer) {
   
            rspJson = (JSONObject) mircoServer.action(reqJson);
        }

        return rspJson;
    }
}

  Restful服务类:

/**
 * <DT><B>描述:</B></DT>
 * <DD>Restful服务类</DD>
 *
 * @version Version1.0
 * @author  Robin
 * @version <I> V1.0 Date:2018-05-22</I>
 * @author  <I> E-mail:xsd-jj@163.com</I>
 */
public class RestfulServer {
   

    private static final Logger LOGGER = RobinLogger.getLogger();
    private static URI uri;
    private static HttpServer server;

    public static HttpServer getServer() {
   
        return server;
    }

    public static URI getUri() {
   
        if (null == uri) {
   
            String listenAddr = ConfigUtil.getConfig("listen.ip");
            String listenPort = ConfigUtil.getConfig("listen.port");
            String baseUri = "http://" + listenAddr + ":" + listenPort + "/";
            uri = URI.create(baseUri);
        }
        return uri;
    }

    /**
     * Starts Grizzly HTTP server exposing JAX-RS resources defined in this application.
     *
     */
    public static void startServer() {
   
        // create a resource config that scans for JAX-RS resources and providers
        // in com.robin.restful package
        final ResourceConfig rc = new ResourceConfig();
        rc.packages("com.robin.restful");
        rc.register(JettisonFeature.class);

        // create and start a new instance of grizzly http server
        // exposing the Jersey application at URI
        // return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
        server = GrizzlyHttpServerFactory.createHttpServer(getUri(), rc);

        String corePoolSizeStr = ConfigUtil.getConfig("thread.core.pool.size");
        String maxPoolSizeStr = ConfigUtil.getConfig("thread.max.pool.size");
        int corePoolSize = 0;
        int maxPoolSize = 0;
        if ((corePoolSizeStr != null) && (!corePoolSizeStr.equals(""))) {
   
            corePoolSize = Integer.valueOf(corePoolSizeStr);
        }

        if ((maxPoolSizeStr != null) && (!maxPoolSizeStr.equals(""))) {
   
            maxPoolSize = Integer.valueOf(maxPoolSizeStr);
        }

        if ((corePoolSize == 0) || (maxPoolSize == 0)) {
   
            LOGGER.log(Level.INFO, "Use default thread pool configuration.");
            return;
        }

        if ((corePoolSize > maxPoolSize)) {
   
            LOGGER.log(Level.SEVERE, "Core pool size greater than max pool sixe in configuration.");
            LOGGER.log(Level.INFO, "Use default thread pool configuration.");
            return;
        }

        //参考http://jersey.576304.n2.nabble.com/It-s-very-hard-to-increase-the-number-of-worker-threads-in-Jersey-Grizzly-module-td7579570.html
        NetworkListener nl = server.getListener("grizzly");
        System.out.println(nl.toString());
        TCPNIOTransport transport = nl.getTransport();
        ThreadPoolConfig config = transport.getWorkerThreadPoolConfig();
        config.setCorePoolSize(corePoolSize);
        String info = "Set thread core pool size [" + corePoolSize + "].";
        LOGGER.log(Level.INFO, info);
        config.setMaxPoolSize(maxPoolSize);
        info = "Set thread max pool size [" + maxPoolSize + "].";
        LOGGER.log(Level.INFO, info);
        GrizzlyExecutorService threadPool = (GrizzlyExecutorService) transport.getWorkerThreadPool();
        threadPool.reconfigure(config);
    }

    /**
     * RestfulServer method.
     *
     * @param args
     */
    public static void main(String[] args) {
   
        startServer();
        if (server.isStarted()) {
   
            LOGGER.log(Level.INFO, "Start http server sucessfully.");
        } else {
   
            LOGGER.log(Level.SEVERE, "Start http server failed.");
        }
    }
}

  微服务入口Action接口

package com.robin.loader;
/**
 * <DT><B>描述:</B></DT>
 * <DD>微服务入口Action接口</DD>
 *
 * @version Version1.0
 * @author Robin
 * @version <I> V1.0 Date:2018-05-04</I>
 * @author  <I> E-mail:xsd-jj@163.com</I>
 */
public interface MircoServiceAction {
   
    public Object action(Object obj);
}

5.2 中文分词微服务

  中文分词微服务包括分词方法有:RobinSeg(RS)、IKAnalyzer(IK)、JEAnalysis(JE)、MmSeg4j(MS)、PaoDing(PD)、SmallSeg4j(SS)。其中RS分词实现见我的文章:知更鸟中文分词RS设计实现 ,其他分词方法都采用发布的jar包进行封装装。

5.2.1 设计模式

  主要涉及外观模式、适配器模式、工厂模式和单例模式。分词微服务类图如图5所示:

图 5 分词微服务类图

  设计原则:(1)针对接口编程,不要针对实现;(2)只和最紧密的类交互;(3)封装变化;(4)松耦合设计。
  外观模式:提供一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让子系统更容易使用。我们采用统一的分词外观类封装各种分词接口,提供一个一致的高层接口。
  适配器模式:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。各种分词的的私有实现接口需要一个提供一个统一的接口调用。
  工厂模式:定义一个创建对象的接口,但有子类决定要实例化的类是哪一个。提供统一的分词工厂,创建分类实例对象。
  单例模式:确保一个类只有一个实例,并提供了一个全局访问点。由于各种分词对象的创建、加载词典等需要申请大量的内存,耗费大量的时间,所以所分词器实例都通过适配器进行控制只创建一个实例。

5.2.2 代码实现

中文分词接口抽象类

package com.robin.segment;

import com.robin.log.RobinLogger;
import java.util.logging.Logger;

/**
 * <DT><B>描述:</B></DT>
 * <DD>中文分词接口抽象类</DD>
 *
 * @version Version1.0
 * @author  Robin
 * @version <I> Date:2018-04-18</I>
 * @author  <I> E-mail:xsd-jj@163.com</I>
 */
public abstract class AbstractSegmenter {
   

    /** 日志 */
    protected static final Logger LOGGER = RobinLogger.getLogger();

    /**
     * 分词抽象方法
     *
     * @param text 文本
     * @param SEPARATOR 分隔符
     * @return 已分词文本
     */
    public abstract String segment(String text, String SEPARATOR);
}

统一分词器外观类

package com.robin.segment;

import com.robin.log.RobinLogger;
import com.robin.segment.SegmentFactory.SegmentMethod;
import com.robin.segment.robinseg.RobinSeg;
import com.robin.segment.robinseg.SegmentArgs;
import java.util.logging.Logger;

/**
 * <DT><B>描述:</B></DT>
 * <DD>统一分词器外观类</DD>
 * <DD>外观模式</DD>
 *
 * @version 1.0
 * @author Robin
 * @version <I> Date:2018-04-19</I>
 * @author  <I> E-mail:xsd-jj@163.com</I>
 */
public class SegmentFacade {
   

    // 日志
    private static final Logger LOGGER = RobinLogger.getLogger();

    /**
     * 获取分词器配置参数对象
     *
     * @param methodName 分词方法
     * @return SegmentArgs
     */
    public static SegmentArgs getSegmentArgsObj(SegmentMethod methodName) {
   
        AbstractSegmenter segment = SegmentFactory.getSegInstance(methodName);
        if (methodName.equals(SegmentMethod.RS)) {
   
            return ((RobinSeg) segment).getSegmentConfInstance();
        }
        return null;
    }

    /**
     * <DD>根据不同分词算法进行分词,</DD>
     * <DD>传入算法名错误或默认情况下用RobinSeg分词。</DD>
     *
     * @param methodName 分词方法名称,“SegmentMethod.IK”,“.JE”,“.MS”,“.PD”,“.SS”,
     * “.RS”
     * @param text 待分词文本
     * @param separator 分隔符
     * @return 使用分隔符分好词文本
     */
    public static String split(SegmentMethod methodName, String text, String separator) {
   
        AbstractSegmenter segmenter = SegmentFactory.getSegInstance(methodName);
        return segmenter.segment(text, separator);
    }
}

分词Action实现类

package com.robin.segment.action;

import com.robin.loader.MircoServiceAction;
import com.robin.log.RobinLogger;
import com.robin.segment.SegmentFacade;
import com.robin.segment.SegmentFactory.SegmentMethod;
import com.robin.segment.robinseg.SegmentArgs;
import com.robin.segment.robinseg.SegmentArgs.SegAlgorithm;
import java.util.HashSet;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

/**
 * <DT><B>描述:</B></DT>
 * <DD>分词Action实现类</DD>
 *
 * @version Version1.0
 * @author Robin
 * @version <I> V1.0 Date:2018-06-05</I>
 * @author  <I> E-mail:xsd-jj@163.com</I>
 */
public class SegmentAction implements MircoServiceAction {
   

    private static final Logger LOGGER = RobinLogger.getLogger();

    public enum StatusCode {
   
        OK,
        JSON_ERR,
        KIND_ERR,
        VERSION_ERR,
        SEGMETHOD_ERR,
        SEPARATOR_ERR,
        SEGMENT_FAILED,
        TEXTS_NULL,
    }

    private class ActionStatus {
   

        StatusCode statusCode;
        String msg;

    }

    private JSONObject getErrorJson(ActionStatus actionStatus) {
   
        JSONObject errJson = new JSONObject();
        try {
   
            errJson.put("status", actionStatus.statusCode.toString());
            errJson.put("msg", actionStatus.msg);
        } catch (JSONException ex) {
   
            LOGGER.log(Level.SEVERE, ex.getMessage());
        }
        return errJson;
    }

    private ActionStatus checkJSONObjectTerm(JSONObject jsonObj,
            String key,
            HashSet<String> valueSet,
            StatusCode errStatusCode) {
   
        ActionStatus actionStatus = new ActionStatus();

        try {
   
            if (!jsonObj.isNull(key)) {
   
                String value = jsonObj.getString(key);
                if (!valueSet.contains(value)) {
   
                    actionStatus.msg = "The value [" + value + "] of " + key + " is error.";
                    actionStatus.statusCode = errStatusCode;
                    return actionStatus;
                }
            } else {
   
                actionStatus.msg = "The input parameter is missing " + key + ".";
                actionStatus.statusCode = errStatusCode;
                return actionStatus;
            }

        } catch (JSONException ex) {
   
            LOGGER.log(Level.SEVERE, ex.getMessage());
        }

        actionStatus.statusCode = StatusCode.OK;
        return actionStatus;
    }

    private ActionStatus checkInputJSONObject(JSONObject jsonObj) {
   
        ActionStatus actionStatus = new ActionStatus();
        ActionStatus retActionStatus;

        JSONObject argsJson;
        HashSet<String> valueSet = new HashSet();

        try {
   
            valueSet.add("segment");
            retActionStatus = checkJSONObjectTerm(jsonObj, "kind", valueSet,
  • 9
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值