2 微服务开发

 

0x0 微服务业务分析

业务场景

需求一:用户可以注册和登录

需求二:登录用户可对课程进行CURD操作

架构设计

Tips:图的目的,是让我们的开发思路更清晰,开发效率更高的。所有想到的开发中应该注意到的点,都在图上标记出来就好,防止自己踩坑。不要为了作图而作图哟!

说明:

  1. 信息服务使用Python开发,其他服务使用Java,目的是感受跨语言调用
    • 邮件验证:当用户注册的时候,由用户EdgeService发起发邮件做验证,验证通过之后再调用「用户服务」做入库进行具体的注册
    • 「用户服务」是比较底层的,不需要干预类似「发邮件」这种信息服务类的事情
  2. 图的目的是让我们的开发思路更清晰
  3. Redis是为了实现:去Session、无状态而存在的
  4. 单点登录相关说明见下方
  5. 服务发现:Dubbo是基于KV存储,实现服务的发布和订阅。这里使用Zookeeper实现该功能
  6. 课程详情页需要获取老师信息,所以建立它们之间的连系,通过Thrift实现服务之间的调用
    1. 问题:为什么不通过课程EdgeService和用户服务建立联系呢?
    2. 问题二:EdgeService和它后面的服务之间,究竟如何划分功能职责 & 分层呢?
    3. 对EdgeService理解不够深刻...上面两个问题待解答
    4. 解答见:「EdgeService」部分,理解了「横向拆分」,就能理解EdgeService和Service的关系了
  7. 课程服务,要求必须登录才能有服务,否则跳转到登录系统

关于「EdgeService」

可以通过一下图示,了解EdgeService的作用

服务化架构(也叫微服务),本质上也是一种「分布式架构」

任何分布式系统遇到的问题,微服务里也是问题。分布式系统能用到的技术微服务里也适用。如:做分布式的典型招式:拆分

微服务,首先就是要把服务拆分成一个「分布式系统」

拆分

左边是纵向拆分,右边是横向拆分

纵向拆分

业务场景:医院挂号,根据挂号业务,拆分出不同的领域服务,根据业务流做的拆分,是纵向拆分。

特点:

  1. 各个领域服务之间没有直接交互,通过顶部「预约挂号总业务」进行间接交互
  2. 每个领域里面,有自己的业务、自己的数据存储空间,自己的技术体系(业务们不必须有一致的技术体系)

横向拆分

把现实中的很多服务单独拎出来,粒度可以细一些。

特点:

  1. 服务不知道什么时候会用,也不是为某个业务而拎服务的
  2. 业务可以像搭积木一样挑几个服务搭起来
  3. 搭积木需要一个中间的媒介,就是我们的分布式服务框架,如:DubboThrift,本质上就是中间的桥梁

解答

EdgeService是「业务层服务」,用来根据业务,把底层服务拼接起来的

非EdgeService是「底层服务」,功能更纯粹,用来给EdgeService调用的

单点登录

参考课程

架构图中可以看到:

用户EdgeService提供了单点登录系统

课程EdgeService想要验证登录状态,需要访问单点登录系统,以:

    1. 校验它拿到的Token(也叫Ticket)
    2. 换取用户的基本信息,存到EdgeService中(体会下EdgeService这种「服务信息聚合」的功能)

接下来...

设计完毕,可以开始开发啦!

思路:选对其他模块依赖最少的模块开始开发,也就是我们在做客户端架构的时候,也常说的「自顶向下做设计,自底向上做开发

从图中可以看到,依赖最少的模块就是「信息服务」

0x1 Thirft安装和验证

基本信息

官网:http://thrift.apache.org/

下载和安装:按文档来,或者`brew install thrift`

里程碑:bash可执行thrife指令

基本使用

使用:通过定义模板,生成不同语言文件 `thrift --gen <language> <Thrift filename>`

demo.thrift

namespace java com.imooc.thrift.demo # 希望生成的语言及生成包的名字
namespace py thrift.demo # 语言二

# 声明类名及接口
service DemoService { 
    void sayHello(1:string name); # 需要指定参数index
}

生成Java文件:`thrift --gen java demo.thrift`

结果:xxxxx/gen-java/com/imooc/thrift/demo/DemoService.java

生成文件中的东东非常多,不用管具体内容

0x2 Python开发信息服务

这一节的关键点,在于理解清楚Thrift这个东东的定位:

  1. 一个RPC框架(调用者:Client端、被调用者:Server端)
  2. 可以跨语言调用

所以,需要我们了解,Python环境下,如何作为Server端 和 Client端使用

1 生成Python格式API

原料:message.thrift

namespace java com.imooc.thrift.message
namespace py message.api

service MessageService {
    bool SendMobilMessage(1:string mobile, 2:string message);
    bool SendEmailMessage(1:string email, 2:string message);
}

指令:`thrift --gen py -out .. message.thrift`

结果:/message/api/xxxxxx

提示:需要保证当前python库中,有thrift的包。没有的话需要通过`pip install thrift`安装之

2 实现Python Server

server.py

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from message.api import MessageService
# from message.api import ttypes # 自定义类型会用到这个东东
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer

import smtplib
import mail_sender

__HOST = 'localhost'
__PORT = 9090

class MessageServiceHandler(object):
    def SendMobilMessage(self, mobile, message):
        print "send mobil message"
        return True

    def SendEmailMessage(self, email, message):

        try:
            mail_sender.postEmail(email, message)
            print "send mail success"
            return True
        except smtplib.SMTPException, ex:
            print "send mail failed"
            print ex
            return False      

if __name__ == '__main__':
    handler = MessageServiceHandler()
    processor = MessageService.Processor(handler)
    transport = TSocket.TServerSocket(__HOST, __PORT)
    tfactory = TTransport.TBufferedTransportFactory()
    pfactory = TBinaryProtocol.TBinaryProtocolFactory()

    rpcServer = TServer.TSimpleServer(processor, transport, tfactory, pfactory)

    print('Starting the rpc server at', __HOST, ':', __PORT)
    rpcServer.serve()
    print "python thrift server exit"

说明:


mail_sender.py:用于实现发送邮件功能的文件

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from email.header import Header
from email.mime.text import MIMEText
from email.utils import formataddr
import smtplib

# 用户信息
from_addr = 'xxx@xxx.com'
# 腾讯企业邮箱必须使用授权码进行第三方登陆 获取方式参考:https://jingyan.baidu.com/article/d8072ac4b697cbec95cefdf3.html
password = '*********'
smtp_server = 'smtp.exmail.qq.com'  # 腾讯服务器地址

def postEmail(to, message):
    to_addr = to #'xxx@xxx.com'

    # 内容初始化,定义内容格式(普通文本,html)
    msg = MIMEText(message, 'plain', 'utf-8')

    # 发件人收件人信息格式化 ,可防空
    # 固定用法不必纠结,我使用lambda表达式进行简单封装方便调用

    def lam_format_addr(name, addr): return formataddr(
        (Header(name, 'utf-8').encode(), addr))

    # 传入昵称和邮件地址
    msg['From'] = lam_format_addr('', from_addr)  # 发件人昵称,腾讯邮箱可略
    msg['To'] = lam_format_addr('', to_addr)  # 收件人昵称(服务商会自动替换成用户名),腾讯邮箱可略

    # 邮件标题
    msg['Subject'] = Header('来自服务器的邮件', 'utf-8').encode()  # 腾讯邮箱略过会导致邮件被屏蔽

    # 服务端配置,账密登陆
    server = smtplib.SMTP(smtp_server, 25)

    # 腾讯邮箱支持SSL(不强制), 不支持TLS。
    # server = smtplib.SMTP_SSL(smtp_server, 465) # 按需开启
    # 调试模式,打印日志
    # server.set_debuglevel(1) # 按需开启

    # 登陆服务器
    server.login(from_addr, password)

    # 发送邮件及退出
    server.sendmail(from_addr, [to_addr], msg.as_string())  # 发送地址需与登陆的邮箱一致
    server.quit()


if __name__ == '__main__':
    postEmail('xxx@xxx.com', '这是一封测试邮件')
    print "test mail send to azen@daker.wang"

说明:

3 实现Python Client

client.py

#! /usr/bin/env python
# coding: utf-8

from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from message.api.MessageService import Client

__HOST = 'localhost'
__PORT = 9090

tsocket = TSocket.TSocket(__HOST, __PORT)
transport = TTransport.TBufferedTransport(tsocket)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
client = Client(protocol)

transport.open()

print(client.SendEmailMessage("x@xxx.com","just test"))

说明:固定写法...

0x4 开发其他服务

其他服务就不详细说明了,一些业务代码而已,重点是后面的「服务Docker化」以及「服务编排」

1 用户服务

三个方法:根据ID查用户、根据username查用户、注册user

需要连个MySQL数据库,做查询和增加

2 用户EdgeService

从架构图调用关系可知,用户的EdgeService比较复杂:

  1. 调用用户服务,对用户信息做基本操作
  2. 调信息服务,实现发短信、邮件
  3. 登录注册功能,单点登录,需要支持其他系统进行登录
  4. 无状态的,需要一个Redis缓存
  5. 对外提供一个RESTful API

方法们:

  1. login(用户名, 密码)
    1. 验证用户名密码
      1. 需要调用UserService的thrift接口,通过用户名取用户信息(包含md5密码)
      2. 验证密码是否匹配
    2. 生成token
      1. token可以是完全随机的,和用户不产生相关性
      2. 相关性是token生成好后,才和User对象建立的对应关系
    3. 缓存用户(把用户和token的对应关系保存起来,客户端使用token进行其他url请求的时候,可以从缓存里把用户拿出来,验证token是否正确)
      1. 准备好Redis环境
        1. 用到Redis,通过docker起就好,镜像可使用hub.c.163.com/public/redis:2.8.4
      2. 和Redis建立关系,key为token,value为userInfo,还需要设置过期时间
    4. 给客户端返回Token
  2. register(用户名, 密码, 手机号(可选), 邮箱(可选), 验证码(证明邮箱确实是该用户的))
    1. 需要首先调用消息服务发送验证码
      1. 验证码是一串随机码,生成之后,一份发送给用户,一份和手机号组成kv存Redis,方便以后验证
    2. 验证之后,调用UserServer的regist方法
  3. 单点登录:通过传过来的token判断是否登录,返回给其他服务用户信息

3 课程服务

方法:

  1. 获取课程列表
    1. 通过用户ID获取课程列表
      1. 课程实体中有「teacherID」
      2. 调user服务,根据teacherID查老师信息
    2. 返回带老师信息的课程列表

4 课程EdgeService

需要实现只有登录才能访问,没登录跳登录

5 API Gateway Zuul

其实就是流媒体项目中的「web代理服务器」