微服务架构方案 ZeroC IceGrid

前言

在聊ICE之前,我们说说目前主流的几个微服务架构方案。

Spring Boot/Cloud

由于 Spring 社区的影响力和 Netflix 的背书,目前可以认为是构建 Java 微服务的一个社区标准,Spring Boot 目前在 GitHub 上有超过 20k 星。

基于 Spring 的框架本质上可以认为是一种 RESTful 框架(不是 RPC 框架),序列化协议主要采用基于文本的 JSON,通讯协议一般基于 HTTP。RESTful 框架天然支持跨语言,任何语言只要有 HTTP 客户端都可以接入调用,但是客户端一般需要自己解析 payload。目前 Spring 框架也支持 Swagger 契约编程模型,能够基于契约生成各种语言的强类型客户端,极大方便不同语言栈的应用接入,但是因为 RESTful 框架和 Swagger 规范的弱契约特性,生成的各种语言客户端的互操作性还是有不少坑的。

Dubbo

Dubbo 是阿里多年构建生产级分布式微服务的技术结晶,服务治理能力非常丰富,在国内技术社区具有很大影响力,目前 github 上有超过 16k 星。Dubbo 本质上是一套基于 Java 的 RPC 框架,当当 Dubbox 扩展了 Dubbo 支持 RESTful 接口暴露能力。

Dubbo 主要面向 Java 技术栈,跨语言支持不足是它的一个弱项,另外因为治理能力太丰富,以至于这个框架比较重,完全用好这个框架的门槛比较高,但是如果你的企业基本上投资在 Java 技术栈上,选 Dubbo 可以让你在服务框架一块站在较高的起点上,不管是性能还是企业级的服务治理能力,Dubbo 都做的很出色。新浪微博开源的 Motan(GitHub 4k stars)也不错,功能和 Dubbo 类似,可以认为是一个轻量裁剪版的 Dubbo。

gRPC

gRPC 是谷歌近年新推的一套 RPC 框架,基于 protobuf 的强契约编程模型,能自动生成各种语言客户端,且保证互操作。支持 HTTP2 是 gRPC 的一大亮点,通讯层性能比 HTTP 有很大改进。Protobuf 是在社区具有悠久历史和良好口碑的高性能序列化协议,加上 Google 公司的背书和社区影响力,目前 gRPC 也比较火,GitHub 上有超过 13.4k 星。

目前看 gRPC 更适合内部服务相互调用场景,对外暴露 RESTful 接口可以实现,但是比较麻烦(需要 gRPC Gateway 配合),所以对于对外暴露 API 场景可能还需要引入第二套 RESTful 框架作为补充。总体上 gRPC 这个东西还比较新,社区对于 HTTP2 带来的好处还未形成一致认同,建议谨慎投入,可以做一些试点。

Ice

ZeroC IceGrid作为一种微服务架构,它基于RPC框架发展而来,具有良好的性能与分布式能力。不过尴尬的是,在国内,似乎使用它的案例并不多,就我所知,目前Skpye内部一些地方在使用Ice。不过这并不影响它的优点,那就是它的性能很不错,以下是源自网上的性能测试:

 

 

 

 

如下所示是它的整体示意图:

 

 

IceGrid具备微服务架构的如下明显特征:

  • 微服务架构需要一个集中的服务注册中心,以及某种服务发现机制。IceGrid服务注册采用XML文件来定义,其服务注册中心就是Ice Registry,这是一个独立的进程,并且提供了HA高可用机制;对应的服务发现机制就是命名查询服务,即LocatorService提供的API,可以根据服务名查询对应的服务实例可用地址。

  • 微服务架构中的每个微服务通常会被部署为一个独立的进程,当无状态服务时,一般会由多个独立进程提供服务。对应在IceGrid里,一个IceBox就是一个单独的进程,当一个IceBox只封装一个Servant时,就是一个典型的微服务进程了。

  • 微服务架构中通常都需要内嵌某种负载均衡机制。在 IceGrid 里是通过客户端API内嵌的负载均衡算法实现的,相对于采用中间件Proxy转发流量的方式(如SpringCloud),IceGrid的做法更加高效,但增加了平台开发的工作量与难度,因为采用各种语言的客户端都需要实现一遍负载均衡的算法逻辑。

  • 一个好的微服务架构平台应该简化和方便应用部署。我们看到 IceGrid提供了grid.xml来描述与定义一个基于微服务架构的Application,一个命令行工具一键部署这个Application,还提供了发布二进制程序的辅助工具——icepatch2。下图显示icepatch2的工作机制,icepatch2server类似于FTP Sever,用于存放要发布到每个Node上的二进制代码与配置文件,而位于每个Node上的icepatch2client则从icepatch2server上拉取文件,这个过程中采用了压缩传输及差量传输等高级特性,以减少不必要的文件传输过程。客观地评价,在Docker技术之前,icepatch2这套做法还是很先进与完备的,也大大减少了分布式集群下微服务系统的运维工作量。

 

 

基于IceGrid的微服务方案

如果基于IceGrid开发系统,则通常有三种典型的技术方案,下图展示了这三种技术方案:

 

  • 其中方案一是比较符合传统Java Web项目的一种渐进改造方案,Spring Boot里只有Controller组件而没有数据访问层与Service对象,这些Controller组件通过Ice RPC方式调用部署在IceGrid里的远程的Ice微服务,面向前端包装为REST服务。此方案的整体思路清晰,分工明确。Leader在开源项目中给出了这种方式的一个基本框架以供参考:github.com/MyCATApach.…

  • 方案二与方案三则比较适合前端JavaScript能力强的团队,比如很擅长Node.js的团队可以考虑方案二,即用JavaScript来替代Spring Boot实现REST服务。主要做互联网App的系统则可以考虑方案三,浏览器端的JavaScript以HTML5的WebSocket技术与Ice Glacier2直接通信,整体高效敏捷。

  • IceGrid在3.6版本之后还增加了容器化的运行方式,即Ice Node与Ice Registry可以通过Docker容器的方式启动,这就简化了IceGrid在Linux上的部署。对于用Java编写的Ice微服务架构系统,我们还可以借助Java远程类加载机制,让每台Node自动从某个远程HTTP Server下载指定的Jar包并加载相关的Servant类,从而实现类似Docker Hub的机制。下图显示了前面提到mycat-ice开源项目时给出的具体实现方案。

 

 

Ice运作原理

以下是我画的一个简单运作流程图:

 

Ice多客户端调用

Java调用

以下是我之前写的Ice调用ElasticSearch中间件的代码,仅做参考:

package com.bigdata.cloudshield.pentration.adapter;

import com.bigdata.cloudshield.pentration.adapter.entity.EsInsertRequest;
import com.bigdata.cloudshield.pentration.adapter.entity.EsRequest;
import com.bigdata.cloudshield.pentration.adapter.entity.EsResponseException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.ice.common.CommonServicePrx;
import com.ice.common.CommonServicePrxHelper;

/**
 * Description: ice工具类 <BR>
 *
 * @author ran.chunlin
 * @date 2018/8/20 11:00
 */
class IceUtil {

    /**
     * 初始化参数
     */
    private static String[] iniParams = new String[]{"--Ice.MessageSizeMax=2097152", "--Ice.ThreadPool.Client.Size=4", "--Ice.ThreadPool.Client.SizeMax=4"};
    /**
     * Communicator
     */
    private static Ice.Communicator ic = Ice.Util.initialize(iniParams);
    /**
     * 代理对象
     */
    private static Ice.ObjectPrx pro = ic.stringToProxy(EsConfig.EsDaoConf.getEsDaoPath());
    /**
     * 服务
     */
    private static CommonServicePrx proxy = CommonServicePrxHelper.checkedCast(pro);
    /**
     * json转换器
     */
    private static JsonParser jsonParser = new JsonParser();

    /**
     * 返回码
     */
    private static final String RESULTCODE = "resultCode";
    /**
     * 结果
     */
    private static final String RESULT = "result";
    /**
     * cat结果
     */
    private static final String CATRESULT = "catResult";
    /**
     * 异常
     */
    private static final String EXCEPTION = "exception";
    /**
     * 正常状态码
     */
    private static final int OK_CODE = 200;

    /**
     * 通用
     *
     * @param request 请求实体
     *
     * @return json结果
     *
     * @throws EsResponseException 返回异常
     */
    public static String getResponseJson(EsRequest request) throws EsResponseException {
        String json = GsonUtil.toJson(request);
        String result = proxy.request(1500, json);
        JsonObject response = jsonParser.parse(result).getAsJsonObject();
        if (response.get(RESULTCODE).getAsInt() == OK_CODE) {
            String resultStr = response.get(RESULT).toString();
            response = null;
            return resultStr;
        } else {
            handleCode(response);
            return null;
        }
    }

    /**
     * insert
     *
     * @param request 请求实体
     *
     * @throws EsResponseException 返回异常
     */
    public static void getResponseForInsert(EsInsertRequest request) throws EsResponseException {
        String json = GsonUtil.toJson(request);
        String result = proxy.request(1501, json);
        JsonObject response = jsonParser.parse(result).getAsJsonObject();
        handleCode(response);
    }

    /**
     * 处理返回码
     *
     * @param response 结果
     *
     * @throws EsResponseException 返回异常
     */
    private static void handleCode(JsonObject response) throws EsResponseException {
        int code = response.get(RESULTCODE).getAsInt();
        String message = response.get(EXCEPTION).getAsString();
        switch (code) {
            case 200:
                break;
            case 101:
                throw new EsResponseException("参数中缺少必要key!\n" + message);
            case 102:
                throw new EsResponseException("将参数以字符串格式的json转换成map的时候异常!\n" + message);
            case 103:
                throw new EsResponseException("xpack token 认证失败!\n" + message);
            case 104:
                throw new EsResponseException("新增删除修改dataList无数据!\n" + message);
            case 105:
                throw new EsResponseException("修改数据时未能找到数据id!\n" + message);
            default:
                if (message.contains("no such index")) {
                    throw new EsResponseException("索引不存在!\n" + message);
                } else {
                    throw new EsResponseException("捕获到的未知异常!\n" + message);
                }
        }
        response = null;
    }

    /**
     * update
     *
     * @param request 请求实体
     *
     * @throws EsResponseException 返回异常
     */
    public static void getResponseForUpdate(EsInsertRequest request) throws EsResponseException {
        String json = GsonUtil.toJson(request);
        String result = proxy.request(1502, json);
        JsonObject response = jsonParser.parse(result).getAsJsonObject();
        handleCode(response);
    }

    /**
     * cat查询
     *
     * @param request 请求实体
     *
     * @return json结果
     *
     * @throws EsResponseException 返回异常
     */
    public static String getResponseForCat(EsRequest request) throws EsResponseException {
        String json = GsonUtil.toJson(request);
        String result = proxy.request(1504, json);
        JsonObject response = jsonParser.parse(result).getAsJsonObject();
        if (response.get(RESULTCODE).getAsInt() == SUCCESS_CODE) {
            return response.get(CATRESULT).getAsString();
        } else {
            handleCode(response);
            return null;
        }
    }
}
复制代码

Python调用

以下是我之前写的Ice调用elasticsearch中间件的代码,仅做参考:

# -*- coding:utf-8 -*-
import sys
import traceback
import Ice
import os
import json
from cn.localhost.Config import es_dao_address, xpackToken

# 动态加载slice文件并编译
Ice.loadSlice(os.path.dirname(os.path.realpath(__file__)) + './Es.ice')
from com.ice import *

reload(sys)
sys.setdefaultencoding('utf8')


class EsUtil:
    __ic = Ice.initialize(['--Ice.MessageSizeMax=0'])
    __ObjectPrx = __ic.stringToProxy(es_dao_address)
    __proxy = common.CommonServicePrx.checkedCast(__ObjectPrx)
    if not __proxy:
        raise RuntimeError('init ice_proxy error')

    @staticmethod
    def __search(endpoint, query={}, source_arr=[], sort={}, size=2000):
        try:
            params = {'xpackToken': xpackToken, 'method': 'POST', 'endpoint': endpoint,
                      'params': {'query': query, '_source': source_arr, 'sort': sort, 'size': size}}.__str__().replace(
                '\'', '"').replace(': u"', ': "')
            return json.loads(EsUtil.__proxy.request(1500, params))
        except:
            print traceback.format_exc()

    @staticmethod
    def __search_scroll(endpoint, time, scroll_id):
        try:
            params = {'xpackToken': xpackToken, 'method': 'POST', 'endpoint': endpoint,
                      'params': {'scroll': time, 'scroll_id': scroll_id}}.__str__().replace(
                '\'', '"').replace(': u"', ': "')
            return json.loads(EsUtil.__proxy.request(1500, params))
        except:
            print traceback.format_exc()

    @staticmethod
    def search(index, query={}, source_arr=[], sort={}, size=2000):
        return EsUtil.__search('/' + index + '/_search', query, source_arr, sort, size)

    @staticmethod
    def search_by_scroll(index, query={}, source_arr=[], sort={}, size=2000, time='5m'):
        result_array = []

        try:
            # 1.首次读取
            es_result = EsUtil.__search('/' + index + '/_search?scroll=' + time, query, source_arr, sort, size)[
                'result']
            if len(es_result['hits']['hits']) == 0:
                return result_array

            # 2.写入结果集
            for hit in es_result['hits']['hits']:
                record = []
                for field in source_arr:
                    if field in hit['_source']:
                        record.append(hit['_source'][field])
                result_array.append(record)

            # 3.判断是否还存在未读scroll数据
            if '_scroll_id' in es_result:
                scroll_id = es_result['_scroll_id']
            else:
                return result_array

            # 4.循环scroll读取
            while True:
                es_result = EsUtil.__search_scroll('/_search/scroll', time, scroll_id)['result']
                if len(es_result['hits']['hits']) == 0:
                    break

                # 写入结果集
                for hit in es_result['hits']['hits']:
                    record = []
                    for field in source_arr:
                        if field in hit['_source']:
                            record.append(hit['_source'][field])
                    result_array.append(record)

                # 判断是否还存在未读scroll数据
                if '_scroll_id' in es_result:
                    scroll_id = es_result['_scroll_id']
                else:
                    break
        except:
            print traceback.format_exc()
        return result_array

    @staticmethod
    def destroy_ice():
        if EsUtil.__ic:
            EsUtil.__ic.destroy()

作者:localhost01
链接:https://juejin.cn/post/6844903745331920903
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2021-01-22 19:03  GaoYanbing  阅读(773)  评论(0编辑  收藏  举报