前言前端
在今天的文章中小碼哥將會給你們分享一個目前工做中遇到的一個比較有趣的案例,就是如何將Python寫的微服務融入到以Java技術棧爲主的Spring Cloud微服務體系中?也許有朋友會有疑問,到底什麼樣的場景須要用Python寫一個微服務,而且還要融入以Java技術棧爲主的Spring Cloud微服務體系中呢?python
大體狀況是這樣的,小碼哥目前所在的公司後端技術棧基本上是以Java爲主,而且整個後端軟件系統採用的也是基於Spring Cloud框架爲主的微服務架構(PS:在以往的文章中小碼哥寫了一些關於這方面的文章,你們能夠在文末的推薦閱讀中查看相關內容),服務的註冊發現是基於Consul,而服務的調用及負載均衡也都是基於FeignClient調用以及Robbin客戶端依賴來實現的,因此總體架構大概就是這樣的一個標準Spring Cloud微服務架構。如圖所示:web
大部分場景下基於以上微服務架構是比較好擴展的,例如你有一個新的微服務,若是徹底能夠經過Java語言構建的話,那就是很是簡單的一件事,由於你只須要基於Spring Boot編寫一個微服務項目,而後經過Spring Cloud提供的註解將其快速地注入Consul的服務註冊&發現機制,而後就能夠很快地對內或對外提供服務了。算法
而在這裏,小碼哥遇到的是一個比較特殊的場景,由於最近小碼哥一直在作一些出行行業相關的事情,因此須要作一些路徑規劃和計算的工做。這裏就有一個比較棘手的需求:「須要對車輛的調度作一些路徑規劃,簡單的來講就是地圖上有不少個座標點的位置,須要給有限的運營車輛作路徑規劃,儘可能以一個距離最短的最佳路線去遍歷完這些位置,從而節省運營資源提升運營效率」。spring
關於這個問題,其實是涉及到計算機科學中比較經典的一個TSP(旅行商)算法問題,若是你們對這個算法有了解的話,就會理解這個問題須要很是大的計算量,由於每多幾個位置,其算法的複雜度就會呈指數增加。而要解決這個問題,若是自行編寫解決方案的話須要耗費很大的精力而且還須要不斷的優化算法!數據庫
因此這個時候小碼哥就想是否是有一些相對比較可靠的開源工具能夠利用呢?因此通過一些研究和調研,果真發現有一個Google開源的運籌計算工具OR-TOOLS,其中提供了關於TSP及VRP問題的解法,關於這個工具解決TSP及VRP問題的方法與TSP問題同樣,小碼哥會在後面找機會給你們分享。json
由於計算量很是大因此在使用OR-TOOLS工具時,咱們須要在本地安裝OR-TOOLS軟件,而在具體編寫計算代碼時因其對Java的支持體驗比較差(缺少官方發佈的Maven依賴,以及示例代碼不全等),因此最終咱們須要使用Python語言來進行開發,而且每一次的路徑規劃計算,都須要以服務的方式對上層應用進行開放。後端
說到這裏,各位應該已經理解了小碼哥的糾結的問題了,由於Python服務相對於Spring Cloud這一套體系來講,算是一個異構服務了,其自己並不像Java那樣能夠很方便的利用Spring Boot、Spring Cloud提供的一套成熟的體系。而若是選擇不融入Spring Cloud體系,那意味着對於Python服務,咱們須要作單獨的部署及負載設計。例如,咱們可能須要單獨部署幾個Python節點,而後經過Nginx單獨配置負載均衡,內部微服務調用也須要每次都繞到Nginx那一層才能夠以負載的方式訪問。以下圖所示:api
實際上這種方式就是回到了早期傳統服務架構時代的負載均衡模式配置方式上去了,雖然也沒有太大的問題,只是爲這樣個別的異構服務單獨設置一套部署體系,從成本及擴展性上來講的確有些彆扭!因此,若是咱們能夠直接將Python寫的異構服務也能經過註冊到Consul的話,這樣也就融入了標準的Spring Cloud微服務體系,問題就簡單多了!cookie
構建Python web服務
接下來咱們就以具體代碼的方式先一塊兒來看看怎麼樣編寫一個Python web服務,並看看怎麼樣才能夠將其註冊到Consul中,並與其餘微服務實現服務發現和調用!在基於Python編寫Web服務時,爲了簡化開發能夠選擇一個比較成熟的PythonWeb框架,這裏小碼哥用的是Tornado,Python中其餘Web框架還有Flask、Django等,由於Tornado性能相對比較高適合作後端接口服務,因此就選擇了Tornado,這裏就再也不進一步說明了。
在具體進行代碼開發時,咱們須要安裝好Python開發環境,這裏小碼哥使用的是Python3.7.3,而Tornado使用的則是5.1.1版本,具體的安裝方式你們能夠查一下,這裏就再也不多說!接下了,咱們具體來看下在真實的項目工程中時怎麼將Python注入Consul的!
由於Python不像Java那樣基於Spring Cloud有一套完整的依賴包,能夠很方便地使用一個註解就能夠進行服務註冊與發現,因此咱們須要基於consulate這個Python庫來單獨編寫服務註冊代碼,以下:
import json from random import randint from consulate import Consul # consul 操做類 import requests class ConsulClient(): def __init__(self, host=None, port=None, token=None): # 初始化,指定consul主機,端口,和token self.host = host # consul 主機 self.port = port # consul 端口 self.token = token self.consul = Consul(host=host, port=port) def register(self, name, service_id, address, port, tags, interval, httpcheck): # 註冊服務 註冊服務的服務名 端口 以及 健康監測端口 self.consul.agent.service.register(name, service_id=service_id, address=address, port=port, tags=tags, interval=interval, httpcheck=httpcheck) def deregister(self, service_id): # 此處有坑,源代碼用的get方法是不對的,改爲put,兩個方法都得改 self.consul.agent.service.deregister(service_id) self.consul.agent.check.deregister(service_id) def getService(self, name): # 負載均衡獲取服務實例 url = 'http://' + self.host + ':' + str(self.port) + '/v1/catalog/service/' + name # 獲取 相應服務下的DataCenter dataCenterResp = requests.get(url) if dataCenterResp.status_code != 200: raise Exception('can not connect to consul ') listData = json.loads(dataCenterResp.text) dcset = set() # DataCenter 集合 初始化 for service in listData: dcset.add(service.get('Datacenter')) serviceList = [] # 服務列表 初始化 for dc in dcset: if self.token: url = 'http://' + self.host + ':' + self.port + '/v1/health/service/' + name + '?dc=' + dc + '&token=' + self.token else: url = 'http://' + self.host + ':' + self.port + '/v1/health/service/' + name + '?dc=' + dc + '&token=' resp = requests.get(url) if resp.status_code != 200: raise Exception('can not connect to consul ') text = resp.text serviceListData = json.loads(text) for serv in serviceListData: status = serv.get('Checks')[1].get('Status') if status == 'passing': # 選取成功的節點 address = serv.get('Service').get('Address') port = serv.get('Service').get('Port') serviceList.append({'port': port, 'address': address}) if len(serviceList) == 0: raise Exception('no serveice can be used') else: service = serviceList[randint(0, len(serviceList) - 1)] # 隨機獲取一個可用的服務實例 return service['address'], int(service['port']) def getServices(self): return self.consul.agent.services()
有了以上這段服務註冊代碼的實現,咱們再來看看入口代碼中如何在啓動服務時注入Consul,代碼以下:
import os import sys from importlib import reload import tornado.web from tornado.ioloop import IOLoop from tornado.options import define, options, parse_command_line from apps.handlers.VehicleRoutingHandler import VehicleRoutingHandler from apps.handlers.HealthChecker import HealthChecker from utils.consul_client import ConsulClient reload(sys) def main(): # 讀取項目配置 from conf.config import getConfig conf = getConfig() c = ConsulClient(conf.consul_address, conf.consul_port) service_id = conf.application_name + ":" + conf.ip + ':' + str(conf.server_port) # print(c.consul.agent.services()) name = conf.application_name address = conf.ip port = conf.server_port tags = [conf.consul_tags] interval = 5 httpcheck = conf.consul_healthCheckPath c.register(name, service_id, address, port, tags, interval, httpcheck) parse_command_line() app = tornado.web.Application( [ (r"/routing/vehiclePathPlan?", VehicleRoutingHandler), (r"/actuator/health", HealthChecker) ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), ) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(conf.server_port) tornado.ioloop.IOLoop.current().start() if __name__ == "__main__": main()
能夠看到上述代碼經過配置獲取了consul的地址及端口信息,並自定義了服務節點的serviceId、tags以及進行健康性檢查時,Consul探測的服務接口地址的定義:
/actuator/health
咱們知道Consul與微服務之間須要經過健康性檢查來作心跳,在Java中由於Spring Cloud依賴包已經替咱們實現好了這樣的接口,而在Python中就須要咱們手工定義,如上述代碼中咱們就定義了/actuator/health服務,並實現了其處理代碼,很簡單就是返回成功,以下:
import tornado.web class HealthChecker(tornado.web.RequestHandler): def get(self, *args, **kwargs): self.write("ok")
這樣在服務註冊到Consul以後,Consul就能夠經過這個接口來與Python微服務之間經過發送心跳來探活了。此時,若是咱們在配置中制定Consul的地址,並啓動Python微服務,就能夠將其注入Consul了,如:
MacBook-Pro-2:routing guanliyuan$ python3 manage.py dev ['manage.py', 'dev'] dev [I 190529 00:37:35 web:2162] 200 GET /actuator/health (127.0.0.1) 1.38ms [I 190529 00:37:36 web:2162] 200 GET /actuator/health (127.0.0.1) 0.76ms [I 190529 00:37:37 web:2162] 200 GET /actuator/health (127.0.0.1) 0.58ms [I 190529 00:37:38 web:2162] 200 GET /actuator/health (127.0.0.1) 0.57ms
啓動服務後,就能夠看到Consul發過來的心跳請求了,此時若是咱們打開Consul的web控制檯,也能看到服務成功的被註冊到Consul上了,如:
以後該Python服務就能夠像其餘Java編寫的微服務同樣便可以經過api-gateway直接被前端調用,也能夠經過FeignClient以負載均衡的方式被其餘微服務調用了!
Python 多環境配置
這裏再多給你們分享一點,就是咱們知道在Spring Cloud微服務中,咱們能夠經過spring.profile.active這個參數來指定不一樣環境的配置,從而實現多環境適配,而在Python中由於沒有像Spring Boot這樣的框架,因此咱們只能本身來實現了,例如,下面的配置代碼就是給Python微服務實現的一個多環境配置的代碼,以下:
import os import socket import manage class Config(object): # 默認配置 DEBUG = False hostname = socket.gethostname() ip = socket.gethostbyname(hostname) application_name = "routing" application_version = "1.0" server_port = 9090 # get attribute def __getitem__(self, key): return self.__getattribute__(key) class DevelopmentConfig(Config): # 開發環境 consul_address = "127.0.0.1" consul_port = "8500" consul_healthCheckPath = "http://127.0.0.1:9090/actuator/health" consul_tags = "dev" class ProductionConfig(Config): # 生產環境 consul_address = "127.0.0.1" consul_port = "8500" consul_healthCheckPath = "http://127.0.0.1:9090/actuator/health" consul_tags = "prod" # 環境映射關係 mapping = { 'dev': DevelopmentConfig, 'pro': ProductionConfig, 'default': DevelopmentConfig } # 根據腳本參數,來決定用那個環境配置 import sys def getConfig(): print(sys.argv) num = len(sys.argv) - 1 # 參數個數 if num < 1 or num > 1: exit("參數錯誤,必須傳環境變量!好比: python xx.py dev|pro|default") env = "dev" # sys.argv[1] # 環境 print(env) APP_ENV = os.environ.get('APP_ENV', env).lower() return mapping[APP_ENV]() # 實例化對應的環境
這樣咱們在啓動python腳本時只須要傳遞對應的環境參數,也能夠實現多環境的配置讀取了,例如:
MacBook-Pro-2:routing guanliyuan$ python3 manage.py dev ['manage.py', 'dev'] dev
後記
以上就是關於Python微服務做爲異構服務融入Spring Cloud體系的一些介紹了,在實際的場景中還會有諸如其餘語言編寫的微服務的場景,如Go!其基本思路相似,只是Java相對於其餘語言來講,因爲其完整的開源生態,會簡單不少!
推薦閱讀:
Spring Cloud微服務中網關服務是如何實現的?(Zuul篇)