前言
在今天的文章中小碼哥將會給大家分享一個目前工作中遇到的一個比較有趣的案例,就是如何將Python寫的微服務融入到以Java技術棧為主的Spring Cloud微服務體系中?也許有朋友會有疑問,到底什么樣的場景需要用Python寫一個微服務,並且還要融入以Java技術棧為主的Spring Cloud微服務體系中呢?
大致情況是這樣的,小碼哥目前所在的公司后端技術棧基本上是以Java為主,並且整個后端軟件系統采用的也是基於Spring Cloud框架為主的微服務架構(PS:在以往的文章中小碼哥寫了一些關於這方面的文章,大家可以在文末的推薦閱讀中查看相關內容),服務的注冊發現是基於Consul,而服務的調用及負載均衡也都是基於FeignClient調用以及Robbin客戶端依賴來實現的,所以整體架構大概就是這樣的一個標准Spring Cloud微服務架構。如圖所示:

大部分場景下基於以上微服務架構是比較好擴展的,例如你有一個新的微服務,如果完全可以通過Java語言構建的話,那就是非常簡單的一件事,因為你只需要基於Spring Boot編寫一個微服務項目,然后通過Spring Cloud提供的注解將其快速地注入Consul的服務注冊&發現機制,然后就可以很快地對內或對外提供服務了。
而在這里,小碼哥遇到的是一個比較特殊的場景,因為最近小碼哥一直在做一些出行行業相關的事情,所以需要做一些路徑規划和計算的工作。這里就有一個比較棘手的需求:“需要對車輛的調度做一些路徑規划,簡單的來說就是地圖上有很多個坐標點的位置,需要給有限的運營車輛做路徑規划,盡量以一個距離最短的最佳路線去遍歷完這些位置,從而節省運營資源提高運營效率”。
關於這個問題,實際上是涉及到計算機科學中比較經典的一個TSP(旅行商)算法問題,如果大家對這個算法有了解的話,就會理解這個問題需要非常大的計算量,因為每多幾個位置,其算法的復雜度就會呈指數增長。而要解決這個問題,如果自行編寫解決方案的話需要耗費很大的精力並且還需要不斷的優化算法!
所以這個時候小碼哥就想是不是有一些相對比較可靠的開源工具可以利用呢?所以經過一些研究和調研,果然發現有一個Google開源的運籌計算工具OR-TOOLS,其中提供了關於TSP及VRP問題的解法,關於這個工具解決TSP及VRP問題的方法與TSP問題一樣,小碼哥會在后面找機會給大家分享。
因為計算量非常大所以在使用OR-TOOLS工具時,我們需要在本地安裝OR-TOOLS軟件,而在具體編寫計算代碼時因其對Java的支持體驗比較差(缺乏官方發布的Maven依賴,以及示例代碼不全等),所以最終我們需要使用Python語言來進行開發,並且每一次的路徑規划計算,都需要以服務的方式對上層應用進行開放。
說到這里,各位應該已經理解了小碼哥的糾結的問題了,因為Python服務相對於Spring Cloud這一套體系來說,算是一個異構服務了,其本身並不像Java那樣可以很方便的利用Spring Boot、Spring Cloud提供的一套成熟的體系。而如果選擇不融入Spring Cloud體系,那意味着對於Python服務,我們需要做單獨的部署及負載設計。例如,我們可能需要單獨部署幾個Python節點,然后通過Nginx單獨配置負載均衡,內部微服務調用也需要每次都繞到Nginx那一層才可以以負載的方式訪問。如下圖所示:

實際上這種方式就是回到了早期傳統服務架構時代的負載均衡模式配置方式上去了,雖然也沒有太大的問題,只是為這樣個別的異構服務單獨設置一套部署體系,從成本及擴展性上來說的確有些別扭!所以,如果我們可以直接將Python寫的異構服務也能通過注冊到Consul的話,這樣也就融入了標准的Spring Cloud微服務體系,問題就簡單多了!
構建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篇)
