2 微服务开发
Mon, Nov 12, 2018
0x0 微服务业务分析
业务场景
需求一:用户可以注册和登录
- 单点登录,但是需要支持跨域 - 使用其他系统的时候就不需要登录了
- 不用session - 避免使用状态
需求二:登录用户可对课程进行CURD操作
架构设计
Tips:图的目的,是让我们的开发思路更清晰,开发效率更高的。所有想到的开发中应该注意到的点,都在图上标记出来就好,防止自己踩坑。不要为了作图而作图哟!
说明:
- 信息服务使用Python开发,其他服务使用Java,目的是感受跨语言调用
- 邮件验证:当用户注册的时候,由用户EdgeService发起发邮件做验证,验证通过之后再调用「用户服务」做入库进行具体的注册
- 「用户服务」是比较底层的,不需要干预类似「发邮件」这种信息服务类的事情
- 图的目的是让我们的开发思路更清晰
- Redis是为了实现:去Session、无状态而存在的
- 单点登录相关说明见下方
- 服务发现:Dubbo是基于KV存储,实现服务的发布和订阅。这里使用Zookeeper实现该功能
- 课程详情页需要获取老师信息,所以建立它们之间的连系,通过Thrift实现服务之间的调用
- 问题:为什么不通过课程EdgeService和用户服务建立联系呢?
- 问题二:EdgeService和它后面的服务之间,究竟如何划分功能职责 & 分层呢?
- 对EdgeService理解不够深刻...上面两个问题待解答
- 解答见:「EdgeService」部分,理解了「横向拆分」,就能理解EdgeService和Service的关系了
- 课程服务,要求必须登录才能有服务,否则跳转到登录系统
关于「EdgeService」
可以通过一下图示,了解EdgeService的作用
服务化架构(也叫微服务),本质上也是一种「分布式架构」
任何分布式系统遇到的问题,微服务里也是问题。分布式系统能用到的技术微服务里也适用。如:做分布式的典型招式:拆分
微服务,首先就是要把服务拆分成一个「分布式系统」
拆分
左边是纵向拆分,右边是横向拆分
纵向拆分
业务场景:医院挂号,根据挂号业务,拆分出不同的领域服务,根据业务流做的拆分,是纵向拆分。
特点:
- 各个领域服务之间没有直接交互,通过顶部「预约挂号总业务」进行间接交互
- 每个领域里面,有自己的业务、自己的数据存储空间,自己的技术体系(业务们不必须有一致的技术体系)
横向拆分
把现实中的很多服务单独拎出来,粒度可以细一些。
特点:
- 服务不知道什么时候会用,也不是为某个业务而拎服务的
- 业务可以像搭积木一样挑几个服务搭起来
- 搭积木需要一个中间的媒介,就是我们的分布式服务框架,如:Dubbo、Thrift,本质上就是中间的桥梁
解答
EdgeService是「业务层服务」,用来根据业务,把底层服务拼接起来的
非EdgeService是「底层服务」,功能更纯粹,用来给EdgeService调用的
单点登录
架构图中可以看到:
用户EdgeService提供了单点登录系统
课程EdgeService想要验证登录状态,需要访问单点登录系统,以:
- 校验它拿到的Token(也叫Ticket)
- 换取用户的基本信息,存到EdgeService中(体会下EdgeService这种「服务信息聚合」的功能)
接下来...
设计完毕,可以开始开发啦!
思路:选对其他模块依赖最少的模块开始开发,也就是我们在做客户端架构的时候,也常说的「自顶向下做设计,自底向上做开发」
从图中可以看到,依赖最少的模块就是「信息服务」
0x1 Thirft安装和验证
基本信息
下载和安装:按文档来,或者`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这个东东的定位:
- 一个RPC框架(调用者:Client端、被调用者:Server端)
- 可以跨语言调用
所以,需要我们了解,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
- 关键文件MessageService.py
- 包含了接口函数的定义,实现Server的时候需要抄过去做实现
- 包含了一个Client Class,实现Client的时候可以用这个实例调用相关方法
提示:需要保证当前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" |
说明:
- 这个文件实现了接口方法...
- 别的没有什么关键点了,Thrift方法调用之类的,把它理解为套路就好了
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" |
说明:
- 腾讯邮箱要求比较严格,需要使用「安全码」才能发送邮件 参考
- 其他邮箱系统的使用方法可以自己检索下 Python发邮件网易邮箱@google
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比较复杂:
- 调用用户服务,对用户信息做基本操作
- 调信息服务,实现发短信、邮件
- 登录注册功能,单点登录,需要支持其他系统进行登录
- 无状态的,需要一个Redis缓存
- 对外提供一个RESTful API
方法们:
- login(用户名, 密码)
- 验证用户名密码
- 需要调用UserService的thrift接口,通过用户名取用户信息(包含md5密码)
- 验证密码是否匹配
- 生成token
- token可以是完全随机的,和用户不产生相关性
- 相关性是token生成好后,才和User对象建立的对应关系
- 缓存用户(把用户和token的对应关系保存起来,客户端使用token进行其他url请求的时候,可以从缓存里把用户拿出来,验证token是否正确)
- 准备好Redis环境
- 用到Redis,通过docker起就好,镜像可使用hub.c.163.com/public/redis:2.8.4
- 和Redis建立关系,key为token,value为userInfo,还需要设置过期时间
- 准备好Redis环境
- 给客户端返回Token
- 验证用户名密码
- register(用户名, 密码, 手机号(可选), 邮箱(可选), 验证码(证明邮箱确实是该用户的))
- 需要首先调用消息服务发送验证码
- 验证码是一串随机码,生成之后,一份发送给用户,一份和手机号组成kv存Redis,方便以后验证
- 验证之后,调用UserServer的regist方法
- 需要首先调用消息服务发送验证码
- 单点登录:通过传过来的token判断是否登录,返回给其他服务用户信息
3 课程服务
方法:
- 获取课程列表
- 通过用户ID获取课程列表
- 课程实体中有「teacherID」
- 调user服务,根据teacherID查老师信息
- 返回带老师信息的课程列表
- 通过用户ID获取课程列表
4 课程EdgeService
需要实现只有登录才能访问,没登录跳登录
5 API Gateway Zuul
其实就是流媒体项目中的「web代理服务器」