ServiceMesh概述
在软件体系结构中,服务网格是专用的基础结构层,用于通常使用Sidecar代理来促进微服务之间的服务之间通信。
具有这样一个专用的通信层可以提供许多好处,例如,提供对通信的可观察性,提供安全的连接,或针对失败的请求自动进行重试和后退。
以上为维基百科对于服务网格的解释,服务网格基于当前的微服务的进一步的发展演进,其实本质上服务发现、服务熔断等内容仍然是服务网格的核心内容,但是服务网格进一步抽象提炼了微服务中的公共组件的功能,并添加了对于这些公共的基础组件的管理能力,从而达到了对微服务的服务治理的能力。
ServiceMesh的演进
本段通过例子来演示一下服务架构的演进。
单体应用
from flask import Flask, jsonify
app = Flask(__name__)
def service_b():
res = {}
print(" call service_b ")
for i in range(5):
try:
res.update({"service_b": "service_b_msg"})
break
except Exception as e:
print(e)
return res
@app.route('/service_a')
def service_a():
res = {"service_a": "servic_a_msg"}
res.update(service_b())
return jsonify(res)
if __name__ == '__main__':
app.run()
最开始的单体应用,此时运行该应用,访问http://127.0.0.1:5000/service_a就会返回对应的结果,在api中访问了service_b函数,如果service_b函数有异常错误处理的话都需要再service_b函数中处理,最终返回结果。此时当项目处在初期的时候,单体应用是开发速度较高的选择。
单体应用到多服务
随着访问量和业务逻辑的迭代,service_a所承载的逻辑与service_b承载的逻辑越来越多,单应用无论开发上线或者测试都变得越来越臃肿,上线越来越繁琐,业务迭代频率越来越低。此时需要对着两个业务进行分拆。
服务A
import requests
from flask import Flask, jsonify
app = Flask(__name__)
def get_service_b():
res = {}
print(" call service_b ")
for i in range(5):
try:
resp = requests.get("http://127.0.0.1:5001/service_b")
if resp.status_code == 200:
res.update(resp.json())
break
except Exception as e:
print(e)
return res
@app.route('/service_a')
def service_a():
res = {"service_a": "servic_a_msg"}
res.update(get_service_b())
return jsonify(res)
if __name__ == '__main__':
app.run(port=5000)
服务B
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/service_b')
def service_b():
res = {"service_b": "service_b_msg"}
return jsonify(res)
if __name__ == '__main__':
app.run(port=5001)
此时通过服务A和服务B的改造,此时service_b通过暴露API的形式对服务A提供服务,此时从代码的实现角度来看,需要手工改造服务A调用服务B的过程。并且要再代码中指定服务B的地址。此时的版本能够先满足当前两个服务独立迭代开发独立部署上线的需求。
多服务到微服务
由于业务访问量的提高,需要动态的部署多个服务B,并且能够通过对服务B的访问进行限流、访问出错重试、对服务A尽量的业务无感知的进行添加,故进化到了微服务阶段。第二阶段中的服务始终是单点的服务,并且加了服务B之后,对于服务B的状态的处理等都需要细化处理,在大规模的服务开发运维情况下,需要完善对服务的治理功能,对于每一个提供服务的应用都衍生了如下需求;
如上罗列了小部分的需求,其中除了业务逻辑之外,像服务发现等需求都是统一的,可以考虑做成sdk的形式提供给开发人员使用,这样就可以专注与业务逻辑的开发,微服务的代码的开发如下;
服务A
from flask import Flask, jsonify
from sdk import service
app = Flask(__name__)
def get_service_b():
res = {}
print(" call service_b ")
result = service.get_service("service_b", "service_b")
if result:
res.update(result)
return res
@app.route('/service_a')
def service_a():
res = {"service_a": "servic_a_msg"}
res.update(get_service_b())
return jsonify(res)
if __name__ == '__main__':
app.run(port=5000)
此时服务A的最大变化就是引入了sdk中的service实例来进行调用服务b,这其中使用了etcd来做服务发现,此时所有的服务发现、负载均衡都包含再了sdk中来实现。
服务B
import sys
import os
import signal
from flask import Flask, jsonify
from sdk import service
app = Flask(__name__)
if len(sys.argv) > 1:
connect_msg = sys.argv[1]
else:
connect_msg = "127.0.0.1:5001"
details = connect_msg.split(":")
host = details[0]
port = details[1]
def unregister(sig, frame):
print("unregister service")
service.discover.unregister("service_b", connect_msg)
os._exit(0)
def register_service():
service.discover.register("service_b", connect_msg)
register_service()
@app.route('/service_b')
def service_b():
res = {"service_b": "service_b_msg"}
return jsonify(res)
if __name__ == '__main__':
signal.signal(signal.SIGINT, unregister)
app.run(host, port)
在服务B中,需要先调用sdk中的服务注册的函数,并在退出的时候调用取消服务注册的函数,此时服务并就提供运行的功能。
sdk模块
import random
import time
import etcd3
from threading import Lock, Thread
from etcd3.events import PutEvent, DeleteEvent
import requests
class ServiceDisover(object):
def __init__(self, host="127.0.0.1", port=2379, prefix="service"):
self.host = host
self.port = port
self.prefix = prefix
self.etcd_client = etcd3.client(self.host, self.port)
self.connects = {}
self.lock = Lock()
self.start_watch()
def start_watch(self):
t = Thread(target=self.watch_service)
t.start()
def register(self, service_name, connect_msg):
if not service_name or not connect_msg:
raise Exception(" args error")
path = self.prefix + "/" + service_name + "/" + connect_msg
self.etcd_client.put(path, "value")
def unregister(self, service_name, connect_msg):
if not service_name or not connect_msg:
raise Exception(" args error")
path = self.prefix + "/" + service_name + "/" + connect_msg
self.etcd_client.delete_prefix(path)
def get_services(self, service_name, selector):
with self.lock:
if service_name not in self.connects:
return
return selector.choose(self.connects[service_name])
def decode_service_key(self, event):
print("event key ", event.key)
service = event.key.decode("utf-8")
print("event key ", service)
services = service.split("/")
if len(services) != 4:
print("error event {0}".format(services))
return
return services[2], services[3]
def watch_service(self):
print("start watch ", self.prefix)
vents_iterator, cancel = self.etcd_client.watch_prefix(self.prefix) # 监听etcd中aaa键 是否发生改变,
for event in vents_iterator:
if isinstance(event, PutEvent):
res = self.decode_service_key(event)
print("put event ", res)
if res is None:
continue
with self.lock:
service_name = res[0]
service_connect_msg = res[1]
if service_name not in self.connects:
self.connects[service_name] = [service_connect_msg]
else:
if service_connect_msg not in self.connects[service_name]:
self.connects[service_name].append(service_connect_msg)
elif isinstance(event, DeleteEvent):
res = self.decode_service_key(event)
print("delete event ", res)
if res is None:
continue
with self.lock:
service_name = res[0]
service_connect_msg = res[1]
if service_name not in self.connects:
print("not found service_name : {0} in connects ".format(service_name))
continue
if service_connect_msg in self.connects[service_name]:
self.connects[service_name].remove(service_connect_msg)
else:
print('not exists event {0}'.format(event))
class RandomLoadBalance(object):
"""
负载均衡 简单的随机选择
"""
def choose(self, values):
if len(values) == 0:
return
return random.choice(values)
class RateBucketLimit(object):
"""
熔断限流
"""
def __init__(self, limit_time=1, rate=1):
self.historys = []
# 安装秒级限流,如一秒钟一个令牌
self.rate = rate
# 限制时间 即多长的时间内产生总共多个的bucket 秒
self.limit_time = limit_time
# 桶总共多大
self.bucket = int(self.limit_time*self.rate)
self.lock = Lock()
def allow(self, n=1):
# 获取毫秒时间戳
millis = int(round(time.time() * 1000))
with self.lock:
if len(self.historys) == 0:
if self.bucket >= n:
for i in range(n):
self.historys.append(millis)
return True
return False
for i, v in enumerate(self.historys):
if millis - v <= self.limit_time*1000:
break
can_allow_n = int(int((millis - v)/1000)*self.rate)
if can_allow_n >= n:
for i in range(n):
self.historys.append(millis)
if len(self.historys) > self.bucket:
self.historys = self.historys[-self.bucket:]
return True
return False
class Service(object):
def __init__(self, host, port=2379, prefix="/service"):
self.discover = ServiceDisover(host, port, prefix)
self.limit = RateBucketLimit(limit_time=5)
self.selector = RandomLoadBalance()
self.retry = 5
def get_service(self, service_name, uri):
if self.limit.allow():
host = self.discover.get_services(service_name, self.selector)
if host is None:
print("not found host ")
return
retry_count = 0
while retry_count < self.retry:
try:
resp = requests.get("http://" + host + "/" + uri)
if resp.status_code == 200:
return resp.json()
print(" response status_code error ", resp.status_code)
retry_count += 1
except Exception as e:
print("requests error ", e)
retry_count += 1
else:
print(" api limit ")
service = Service("192.168.12.120")
sdk的代码内容主要就是通过etcd来实现服务发现,实现了负载均衡、API限流控制和失败重试,这些代码都是每个微服务应用都需要的功能。
此时运行的顺序为先运行服务A;
python service_a_sdk.py
start watch /service
* Serving Flask app "service_a_sdk" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
接着再运行两个服务B;
python service_b_sdk.py 127.0.0.1:5001
start watch /service
* Serving Flask app "service_b_sdk" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
event key b'/service/service_b/127.0.0.1:5001'
event key /service/service_b/127.0.0.1:5001
put event ('service_b', '127.0.0.1:5001')
* Running on http://127.0.0.1:5001/ (Press CTRL+C to quit)
python service_b_sdk.py 127.0.0.1:5002
start watch /service
event key b'/service/service_b/127.0.0.1:5002'
event key /service/service_b/127.0.0.1:5002
put event ('service_b', '127.0.0.1:5002')
* Serving Flask app "service_b_sdk" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5002/ (Press CTRL+C to quit)
此时我们再访问http://127.0.0.1:5000/service_a就可看出我们通过封装好的sdk实现了服务B启动就可以加入到服务中,在退出时就退出,并提供了API限流功能。(sdk代码仅限功能演示,有许多不完善的地方,只是作为功能的演示而已)。至此基础的微服务的架构基本搭建完成。
ServiceMesh进化
在第三步的服务中,提供的sdk是基于python实现的,如果换了技术栈的话就需要通过实现不同的sdk来提供服务,限制了使用场景。在sdk升级的过程中,会导致每个不同的服务需要进行sdk版本的升级,会加剧sdk的管理难度。基于如上的种种因素,ServiceMesh出现了。
然后应用A访问应用C就通过访问SidecarA,然后SidecarA访问SidecarB,SidecarB访问应用C,最后引用C将结果返回给SidecarB,SidecarB返回给SidecarA,最终SidecarA将数据返回给应用A。通过该方式将业务所需的负载均衡、服务发现和API限流等功能都通过Sidecar代理进行管理,从而完成更丰富的管理与微服务的治理功能。通过Sidecar代理,就可以不限制应用的技术栈,不同的技术栈的支持就会更轻松,升级维护就更方便。
此时微服务的整个架构就如下所示;
每个微服务通过sidecar对其他服务进行交互,这是当前服务网格最基础的架构。现阶段服务网格相对较火的是istio,这也是各大云厂商都激烈竞争的一个方向。后续有机会再单独来学习该内容。
总结
本文主要是梳理了一下服务的架构的一个演变的过程,并列举了示例代码来理解,通过微服务的一步步发展,最后演变除了服务网格的架构,并且随着serverless架构被各大云厂商的推崇,在云原生的领域服务网格发展也很火热。后续有机会再深入学习一下有关ServerMesh的实例Istio的内容。由于本人才疏学浅,如有错误请批评指正。