Architecture(架构)一词源于建筑学,指建筑物在大尺度上是如何靠内部支撑物相互结合而稳固构造的方式

Architect(架构师)是为满足软件设计目标而在较大尺度上进行整体构思的角色

-- 架构、架构师的定义

建议读本文前先找几本关于Microservice的书阅读一下,我的Slack bookshare里面有几本。


相比设计思想一脉相承的SOA(Service Oriented Architecture),Microservices 是一种把服务分解到更细粒度,松耦合的架构风格。

使用微服务,你的代码将被分解为独立的服务,而这些服务可以用不同的语言开发、部署在不同的地方、作为单独的进程运行。这样的架构风格提供了更高的模块化,使程序更易于开发、测试、部署,最重要的是,更改和维护。(和敏捷开发契合度很高)

市面上有很多微服务的框架,Java的有Spring Cloud和Dubbo(不推荐,缺少很多组件且曾经多年没人维护,使用老旧的RPC),NodeJS也有Seneca和Moleculer可选。不过对于有理想和追求的程序员来说,有什么比自己亲手搭建一个能使自己更了解这种架构呢?(只知道调用API,调整别人的配置有什么意思!)


搭建前的一些技术decision:

  • 微服务的通信全部基于REST(REST服务的搭建不是重点,为了省代码,使用我的小框架@jinyexin/core)
  • 自己实现中央式(即微服务之间不相互注册)服务注册、发现,不依赖eureka或者zookeeper
  • 对外REST服务全部走Gateway,做到对Web端/APP端,拆分微服务和不拆分零区别

第一步的目标:

  • Gateway:实现服务的注册、发现,REST请求的代理
  • message:一个简单的信息服务
  • auth:用户的注册、登录服务(整个系统的用户验证会在第二步考虑)

搭建第一步的代码在这里


Gateway

我们把所有的服务注册信息放在一个map里面,key或者说name就是服务的介入点URL,value里面包含了所有已经发现的微服务节点。

类型定义如下:

export type Microservice = {
    name: string; //example: /api/message
    nodes: Array<{ host: string, port: number, lastHeartbeat?: Date }>;
}

在gateway上提供的注册、心跳服务如下(GatewayController):

@Get("")
async list() {
    return Array.from(global.microservices.values());
}

@Post("")
async register(req) {
    ...//解析注册请求并加入到map里面
}

@Put("")
async heartbeat(req) {
    ...//心跳,更新map中每个服务节点的最后心跳时间
}

同时,我们实现了一个非常简单的代理(ProxyController),把所有其他请求都代理到相应的microservice节点上,如果有多个可用节点就随机抽一个。(代码略)

最后,我们在Gateway的启动代码app.ts里加了一小段逻辑来踢出60秒内没有心跳的节点:

setInterval(
    () => {
        global.microservices.forEach((microservice: Microservice) => {
            let now = new Date();
            microservice.nodes = microservice.nodes.filter(node => (now.getTime() - node.lastHeartbeat.getTime()) <= 60 * 1000);
        })
    },
    10 * 1000
);

整个Gateway的有效代码仅仅200左右


微服务 Message & Auth

由于我们的目标是验证这个微服务框架,Message和auth只是两个个不同的服务而已。每个服务的关键在于启动时app.ts的注册和发送心跳到Gateway:

httpTools
    .sendHttp("localhost", 3000, "/api/gateway", "POST", JSON.stringify({
        "name": endpoint,
        "nodes": [
            {"host": "localhost", "port": global.config.servicePort}
        ]
    }))
    .then(() => {
        logger.info(`success connected to gateway`);
        application.init();
        setInterval(() => {
            httpTools.sendHttp("localhost", 3000, "/api/gateway", "PUT", JSON.stringify({
                "name": endpoint,
                "nodes": [
                    {"host": "localhost", port: global.config.servicePort}
                ]
            })).then(logger.silly);
        }, 30 * 1000);
    })
    .catch(() => {
        logger.error("fail to register, exit.");
        process.exit(0);
    });

每个微服的有效代码量在50行左右


启动及节点的扩展

我们使用pm2作为我们的进程管理工具。

在pm2的配置文件中(source),我们启动一个Gateway在3000,message服务分别起两个实例在4000和4001,auth服务在5000

(或者你想玩下容器的话,可以用docker起上述的4个节点。Dockerfile已经包含在项目里,启动命令可参考docker run -p 3000:3000 gateway

这样,在每个子项目都已经npm i && npm run build之后,我们可以直接在根目录上运行pm2 start来个启动整个微服务群:

或者输入pm2 monit来进行监控:

好了,接下来我们所有的测试工作都是通过在3000端口,这个唯一对外服务的API Gateway来进行的。

我们可以发送一个请求来查看节点的注册状况({Get} http://localhost:3000/api/gateway ):

可以看到三个节点都顺利注册完成:

让我们再试试有两个节点的message微服务({Get} http://localhost:3000/api/message ):

多刷几次,发现body中的端口号在变,说明访问是随机分发给活着的节点的

最后,我们试下auth的login接口({Post} http://localhost:3000/api/auth/login ):

可以看到post的请求body和返回body都被正确处理了


总结&Next

微服务并没有什么神秘的,我们用区区几百行代码,就从零开始搭起了一个微服务的架子。通过搭建这个简单的例子,是不是对服务发现、注册,Gateway的运行原理有了更多的了解呢?

预告:这只能说是个雏形,下一步,我们会关心实际点的应用例子:Authentication(你是谁)和Authorization(你能干什么)如何在微服务中的解决方案。