微服务架构练习-1

Context

马上要毕业了,在毕业后不知道还有没有机会做一个自己想做的东西了,所以想把很早之前看的和微服务有关的系统架构实现一遍。为了实现一个这样的系统,需要先设计一个有高性能系统架构需求的系统,不然永远停留在理论阶段,根据实际的情况做,才会讨论出更多的灵感。

先甭管这样一个系统有没有用,先把功能简单说一下。这是一个广告分发系统,用户可以对某一台或多台广告显示装置上显示的内容进行购买和编辑。服务端接口分为http服务和tcp服务两部分,http服务用来处理用户的请求,tcp服务用来处理嵌入式广告显示装置。
这样的设计在之前的防盗定位系统和轨迹追踪系统中都已经实现过,方案是可行的。之前把所有的http功能都做在了一个django服务器中,将tcp的功能用twisted实现,并通过RabbitMQ将django和twisted进行连接。

这次为了引入微服务架构,那肯定要将各种功能拆分开。比如用户服务,订单服务,商城服务,设备服务和网关服务等。

Docker

微服务的一个好处就是,可以轻松的横向扩展,比如用户服务的业务需求量远远大于订单服务的业务需求量,那么可以多增加几台处理用户服务的服务器,分散处理的压力,提高系统中该业务的并发量。

在使用docker之前,布置一个服务器都是手动ssh登陆,手动安装环境,用git把代码pull上去,然后启动。

配置环境有的时候还挺复杂,比如安装nginx啊,uwsgi,django,数据库啊.如果在一个系统中需要运行多个服务的时候,需要打开端口,比如uwsgi要开9000,postgres要开5432,rabbitmq要开5672等等。这样会导致服务之间存在不安全性,比如uwsgi服务本不该访问5432端口,但是当系统被攻击时,说不定黑客可以通过一个服务的漏洞访问到其他的服务。

使用docker可以为每一个服务建立一个容器,容器之间有特定的通信通道比如,nginx容器只能访问python(django+uwsgi)容器的端口,python容器和postgres容器有特定通道,python容器和rabbitmq容器有特定通道。这样postgres容器和rabbitmq容器之间就完全隔离无法通信了,达到了安全隔离的效果。

使用了docker之后,可以轻松方便的将一个镜像复制很多份。这对于部署一个分布式系统来说真的是轻松了不少,横向扩展一个微服务也就只要使用docker run多个镜像就好了。

Docker确实够方便了,但是我还嫌麻烦,因为要一台一台一台的输入docker run xxx这样的命令,当有服务要更新了,也需要同样的将对应服务的docker镜像重新更新后再启动。所以就有了docker的三剑客,compose和swarm

Docker Compose

docker compose是将一系列的docker配置信息写在一个文件里面,运行的时候,可以将全部的服务一次性运行,也可以单独的重启任何一个服务。

而且使用了networks的alias配置后,容器之间的访问甚至可以用hostname直接访问,都不需要配置ip地址就行了,实在是方便。

因为我懒得找commit history了,所以就把比较新的一个docker-compose文件贴一下。

version: "3.7"

services:
  db:
    build:
      context: ./AccountMicroService/
      dockerfile: Dockerfile-DB
    container_name: ms_practice_postgres
    volumes:
      - type: volume
        source: database
        target: /var/lib/postgresql/data
    networks:
      inner:
        aliases:
          - db
  account:
    build:
      context: ./AccountMicroService/
      dockerfile: Dockerfile
    container_name: ms_practice_account_service
    depends_on:
      - db
    ports:
      - "50051:50051"
    command:
      - /bin/sh
      - -c
      - |
          python /code/AccountRPCServer.py
    volumes:
      - type: bind
        source: ./AccountMicroService/
        target: /code/
    networks:
      inner:
        aliases:
          - account
      outter:
        aliases:
          - account
  tester:
    build:
      context: ./TestService/AccountTest/
      dockerfile: Dockerfile
    container_name: ms_practice_account_tester
    depends_on:
      - account
    volumes:
      - type: bind
        source: ./TestService/AccountTest/
        target: /code/
    command: python /code/account_rpc_test.py
    networks:
      outter:
        aliases:
          - tester
networks:
  inner:
  outter:
volumes:
  database:

这个compose文件是测account这个服务的。其中有三个服务数据库服务、用户服务、测试服务。定义了两个网络,内网和外网。

这个服务我使用了gRPC来进行调用,gRPC的相关内容会在后面讲。

在这里,我公开了account服务的50051端口用于gRPC的调用。在account和db之间建立了私有通道,以便进行数据库的增删改查操作。

Docker Swarm

Swarm的用处就是帮助部署集群,但是我觉得挺难用的,因为只能使用image部署,不可以通过编译dockerfile来部署,所以对于有文件洁癖的我来说,还是有点儿不爽的,可能以后学了k8s或者其他工具之后,会有更方便的集群部署方式。

Communication

根据微服务的基本设计,系统中需要有服务发现和服务注册的功能,微服务之间需要互相通信,这次使用gRPC来做这样一个通信的工具。因为很方便,只要确定了通信消息,就可以在server和client之间进行通信,比rabbitmq还方便(当然因此他也存在一些劣势)

grpc工作示意图

这里简单设计了一下用户服务的通信原型Service.proto:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.ms.account";
option java_outer_classname = "AccountProto";
option objc_class_prefix = "ACP";

package accountservice;

service AccountService {

    rpc SignupNewUser (SignupMessage) returns (SignupResponse) {
    }

    rpc LoginUser (LoginMessage) returns (LoginResponse) {
    }

}

message LoginMessage {

    string username = 1;

    string password = 2;
}

message SignupMessage {

    string username = 1;

    string password = 2;

}

message LoginResponse {

    int32 status = 1;

    string message = 2;

    string userid = 3;

}

message SignupResponse {

    int32 status = 1;

    string message = 2;

    string userid = 3;
}

这里为了测试,只定义了两个服务调用,就是注册和登陆两个服务。

用python写了gRPC Server:

import service_pb2
import service_pb2_grpc
import AccountDB
import grpc
from concurrent import futures
import time

_ONE_DAY_IN_SECONDS = 60 * 60 * 24


class AccountServiceReceiver(service_pb2_grpc.AccountServiceServicer):
    def SignupNewUser(self, request, context):
        print("receive sign up request")
        if len(request.password) < 6:
            msg = "password not strong"
            return service_pb2.SignupResponse(status=-2, message=msg, userid="")
        else:
            user = AccountDB.signup_user(request.username, request.password)
            if user is not None:
                msg = "succeed"
                return service_pb2.SignupResponse(status=0, message=msg, userid=str(user.userid))
            else:
                msg = "username existed"
                return service_pb2.SignupResponse(status=-1, message=msg, userid="")

    def LoginUser(self, request, context):
        print("receive login request")
        userid = AccountDB.login_user(request.username, request.password)
        if userid is not None:
            msg = "login succeed"
            return service_pb2.LoginResponse(status=0, message=msg, userid=str(userid))
        else:
            msg = "login fail"
            return service_pb2.LoginResponse(status=-1, message=msg, userid=str(userid))


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    service_pb2_grpc.add_AccountServiceServicer_to_server(AccountServiceReceiver(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    serve()

gRPC很厉害,他是非阻塞的,所以按照官方的教程来写的话,这个server的代码运行一下就结束了,所以需要增加一个人工阻塞使得代码可以一直运行着。

就是这样的:

try:
    while True:
        time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
    server.stop(0)

这里的service_pb2和service_pb2_grpc是gRPC官方提供的工具,他可以根据proto文件生成对应的python文件,以供调用。AccountDB.py是一个数据库操作代码。

在server完成后,可以设计一个client代码来测试grpc的功能。

account_rpc_test.py:

import grpc
import service_pb2
import service_pb2_grpc
import uuid


def signup(stub, username, password):
    signup_message = service_pb2.SignupMessage(username=username, password=password)
    signup_result = stub.SignupNewUser(signup_message)
    return signup_result


def login(stub, username, password):
    login_message = service_pb2.LoginMessage(username=username, password=password)
    login_result = stub.LoginUser(login_message)
    return login_result


def run_test():
    with grpc.insecure_channel('account:50051') as channel:
        stub = service_pb2_grpc.AccountServiceStub(channel)
        username = str(uuid.uuid4())
        password = str(uuid.uuid4())
        signup_response = signup(stub, username, password)
        print signup_response.message
        signup_fail_response = signup(stub, username, str(uuid.uuid4()))
        print signup_fail_response.message
        login_response = login(stub, username, password)
        print login_response.message
        ogin_fail_response = login(stub, username, str(uuid.uuid4()))
        print ogin_fail_response.message


if __name__ == '__main__':
    run_test()

由此便可以测试grpc的功能来

网关服务

上面仅仅是完成了微服务和数据库的连接,以及相应的操作。对于http client来说,现在并不能直接调用。为了测试http的并发量,所以得先增加一个http网关。

dedeluoxixi最近一直在吹爆node.js,所以就由他来做了

index.js:

const http=require('http');
const express=require('express');
const logger=require('morgan');
const app=express();

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));



app.use('/native',require('./router/native'));
app.use('/grpc',require('./router/grpc'));
app.get('/test',(req,res,next)=>{
    res.json({msg:0})
});

const server=http.createServer(app);
server.listen(3000,()=>{
    console.log('Nodejs running on port 3000');
});

不得不说,用node来做一个简简单单的http server是真的简单啊。

我们使用了两种方法来做对比:

1.直接使用node的http server来操控db

2.使用node的http server的grpc来调用account service再来操控db

至于结果如何,且听下回分解,哈哈哈