微服务框架是目前很流行的一种架构模式。它的优点非常多,而我看中的是微服务的解耦的理念,把原有的内部的一些测试系统的原子服务进行解耦,更高效快速的进行测试工具或者其他业务的开发。
就我们目前的体系而言,内部系统有很多共同的特性,例如配置管理、监控告警、调用底层的一些关联系统等等,每一个业务系统都要重复的对这些跟业务无关的特性进行开发,虽然很多都是CP代码,但是也依然耗费一些时间调试。
而把这些特性全部抽象成一个一个的原子服务,也贴合了代码中的不重复开发的思想。
刚好今年过年我在公司值班,加上疫情的原因,也没啥地方玩,闲着无聊就把这个框架撸出来了,经过几个月的迭代更新,目前在内部系统已经有了一定量级的使用。
由于涉密,因此本文只讲思路,不给出具体的代码,读者可根据思路自行实现。我会把写框架过程中的各方面的思考写出来,包括一些方案选型的对比,读者可以根据自己的实际情况做一些选择,毕竟每个团队的实际情况不一样需求也不尽相同。
框架有这么几个目标。
业务系统还是尽量使用成熟的开源框架,自己造的轮子一定或多或少会有很多的局限性,作为要求不高的内部系统使用足够。但是用于业务系统,则需要自己有足够强的编码能力,能够填各种坑。
Python
。目前从业界来看,Python
并没有一个工业级的微服务框架。而作为测试同学,大部分都是与Python
打交道。用Python
来编写,可以让更多测试同学参与进来共同建设。
接着上一点来说,Python
的包管理实在是不太好用,安装第三方包总是会遇到问题,即使用了虚拟环境,依然会有不少的麻烦事,最好的方案就是不使用第三方库,全部使用内置库来实现整个框架。
一般来说,微服务有几个组成部分,server
,client
,rpc
和注册中心。我们分这几部分来讲解思路。
服务是框架的重要组成部分,这里我采用TCP协议
来作为通讯协议。基于以下几点考虑。
RPC
的方式来调用,比较常见的就是TCP
和RESTful
。本质上其实都是TCP
。但是在实际的编码中,不使用第三方包,自己实现一个http
的服务,还是有一点麻烦的,而TCP
协议的服务就简单多了。TCP
协议进行通讯,天然的可以使用长连接,可以提高整体的性能。RESTful
虽然也能实现长连接,但是常态来说,还是短连接为主。使用TCP
可以比较灵活的在长短连接中切换。如果你使用的是Python2
的版本,当然,现在已经不建议你再使用Python2
,如果确实是由于特殊需求,可以参考一个高性能的TCP服务写一个服务类。
如果你使用的是Python3
,那么我强烈建议你使用Python3.5+
的版本,官方有库帮你实现自适应的高性能TCPServer
。参考官方文档:socket
路由管理是服务的核心,一个请求进来,是如何找到执行函数进行执行的,这是一个非常关键的问题。
这里的核心思路就是在启动的时候获取到所有声明的接口,把他们加载到内存中,当请求进来的时候,根据声明的接口,在内存中找到声明的接口,并执行对应的执行函数。
现有的比较成熟的框架都是按照这种思路去实现,只不过到具体的细节有所差异.
比如Flask
在路由管理的时候,使用的是装饰器的方式,也就是@app.route(path)
这样的方式.
而Django
则是自己去定义一个url.py
文件,自己写完函数之后,去这个url.py
中定义路由规则。一笔请求过来,都会去这个路由表中匹配,匹配到某个规则之后,利用这个规则去寻找执行函数并执行。
我的设计思路是参考Flask_restful
的方式,使用HashMap
来管理路由。
使用者需要自己去注册自己写的函数。代码组织形式类似:server.regisiter_api("a", api.a)
。这个动作的背后,就是把这个"a"
和api.a
作为一个k:v
的形式存在路由表。
当请求进来的时候,去路由表的key中寻找对应的执行函数执行即可。
请求参数的方式也是五花八门,但是逃不过这几种方式:
URI
中通过变量的方式定义,可以有多参数这种方式在URI
中声明了多少参数,执行函数就会有多少个参数。flask
和django
都支持这类定义
这种方式只会有一个参数,这个参数会一个类似容器的参数,比如常见的dict
,或者class
。这样再定义执行函数的时候,就必须有切仅有一个入参。flask_restful
和django_restful
采用这种方式定义。
有一个全局唯一个的对象,所有的请求信息都会包含在这里,执行函数没有入参,这个全局对象通过import
的方式引入。flask
和django
都支持这类定义。
我的选择是第二种,在路由管理中,我参考的是flask_restful
,这里也一样,我选择的是第二种方式,参数载体是一个dict
这里client
和rpc
放到一起说,因为这两个结合的比较紧密。
client
或者说proxy
,目前我看主要有两种方式,一种作为SDK
的方式供业务方使用,另一种是单独的转发服务。业务请求全部从这个转发服务走。
两种方式各有利弊。可以根据自己的喜好选择,稍后会给出我自己的一些感受。
client
需要具备这么几个功能:
进行tcp
的通讯是需要知道被调服务的ip
和端口,而rpc
的原则是业务方是不感知被调服务的部署情况,因此client
需要根据路由参数自动寻找目标服务的ip
和端口。
服务发现的原理其实很简单。就是用被调服务的名字,去注册中心查询已注册的改服务的ip和端口。
当然,我们不可能每一次请求,都是查询一次路由信息,肯定需要在内存中缓存,发现缓存过期之后,才去注册中心重新查询。这样能够减少问询注册中心的次数,从而减少网络调度,加快调用速度,但是同时也会引入新的问题,如果服务被反注册了,需要等到本地缓存过期之后才能发现服务被反注册。
因此,如果对性能要求不高,那么每次请求都去注册中心查一次,这个结果最准确,如果引入了本地缓存,那么带来的,就是缓存更新的问题,这里会引入很多需要优化的工作,需要看具体的需求。
微服务架构一个比较重要的点就是多实例部署,故障自动剔除,因此,需要有一定的熔断机制,确保不会请求到挂掉的服务。
服务熔断的机制我个人理解有两类,业界比较普遍的做法,就是请求之后上报请求的健康度,当请求的健康度下降到一定程度之后,触发熔断机制,剔除对应的服务。
另一种就是最粗暴的做法,一旦发现请求连接被拒绝,直接发起反注册服务剔除目标服务的地址,但是网络请求本身就有可能发生网络抖动导致连接失败,如果采用这种做法,就需要一个心跳检测来检测服务,确保被误剔除的服务能够重新被注册上。
由于微服务的是多实例部署,因此在请求的时候,要能够根据流量选择最佳的目标服务,一个负载均衡是非常有必要的。
负载均衡这玩意,也是可简单可复杂,完全取决于你的系统业务量。比较简单的就是随机分配流量,要做的复杂一点,可以用特定的算法给每个实例分配权重,也可以收集部署机器的性能来动态的分配权重,还是那句话,取决于你的业务。
请求的过程中,需要按照协议的序列化,也就是这样做可以提高传输的效率,同时也方便server做反序列化时,获取一些协议头的信息,做一些定制化的逻辑。
协议序列化,可以用的方式比较多,如果所有的服务都是用Python来编写的话,可以使用自带的pickle来做协议的序列化,不过,我个人建议用比较通用的方式,比如json,xml这类所有编程语言都能使用的序列化方式来做这件事。
定协议的时候也需要定义报文头,报文体。一些协议参数就放在报文头中,这样比较利于框架的拓展。如果只有报文体,那么协议参数就会和业务参数混合在一起,有可能在某些情况下,协议参数会替换了业务参数,导致一些麻烦的问题出现。
注册中心是管理所有的实例路由信息的地方,业界有很多现成的解决方案,比如eureka
。
本文讨论的是不依赖第三方库,因此需要自己写一个注册中心,其实注册中心本质上也是一个服务,因此我在编写的过程中直接使用前文的server框架来写这个注册中心,我们要的是一个最简单的注册中心,因此只需要服务提供:服务注册,服务注销,路由查询这几个最基本的功能就行了。
用这个server
来编写注册中心就会有一个问题,怎么注册自己。我这边的解决方案是用这个server
的单机模式部署。由于我们经常会有本地调试的过程,在写server框架的时候,我预留了一个单机模式的配置,注册中心就按照这种模式来运行即可。
回想起这次写微服务的历程,还是挺有意思的,也算是有一些大胆了,在框架不成熟的情况下强制去推,去使用,也出现了不少问题,踩了不少坑。不过目前看起来运行稳定,从监控系统上来看,日均请求量大概在4W+的样子,这个请求量级的性能应该是能够满足大部分公司的内部系统的性能要求的。
在写这个微服务框架的过程中,即使这个微服务框架是一个最简单的框架,也确实遇到了很多问题,甚至有些至今也都还没有什么好的解决办法,比如client
采用了SDK
的方式,导致无法给业务请求做唯一标记的染色,需要依赖业务显示的指定唯一标记,至今没有想到一个优雅的方式来解决,这样就导致了没办法做全链路监控,或者说即使做出来,也会非常鸡肋。
在编写的过程中,也阅读了大量优秀的源代码,参考了很多我厂内部组件的设计模式,也算是学习到了非常多的知识。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。