『Microservices & Nameko』Python 微服務實踐

天澄發表於2019-03-27

1. Microservices

微服務最近一二年非常熱門,談論也比較多,簡單的說,微服務將單一應用程式作為由眾多小型服務構成之套件加以開發的方式,其中各項服務都擁有自己的程式並利用輕量化機制(通常為HTTP源API)實現通訊。下面來一張示例圖:

『Microservices & Nameko』Python 微服務實踐

Microservices Architecture:

『Microservices & Nameko』Python 微服務實踐

上面2幅圖已經形象說明微服務是什麼東西了,同時軟體部署方式需要建立在容器上。微服務相關生態會在Java和Go語言中比較成熟,尤其是Java。而Python作為後端,這方面會比較弱一點,微服務框架目前能看到了也就Nameko,並且技術也沒那麼成熟,因目前業務Python場景比Go語言稍多,所以先來玩一下Python如何玩微服務吧。

2. Service Mesh and Serverless

關於微服務,還有2個概念也比較熱,下面簡單提及一下。

2.1 Service Mesh

Service Mesh 服務網格,這個概念剛開始晦澀難懂,網上也有人說事下一代微服務,簡單的說,當成千上萬的微服務部署在Kubernetes 上,整體來說也是相當複雜的,因為每個微服務都需要健康檢查、處理錯誤、延時等,而Kubernetes雖然可以提供健康檢查和自動恢復,但是還需要熔斷模式、服務發現、API管理、加密校驗等等。而這些就是 Service Mesh需要解決的問題。

更加詳細的介紹: philcalcado.com/2017/08/03/…

作為服務間通訊的基礎設施層,可以將它比作是應用程式或者說微服務間的TCP/IP,負責服務之間的網路呼叫、限流、熔斷和監控。對於編寫應用程式來說一般無須關心TCP/IP這一層(比如通過 HTTP 協議的 RESTful 應用),同樣使用Service Mesh也就無須關係服務之間的那些原來是通過應用程式或者其他框架實現的事情,比如Spring Cloud,現在只要交給Service Mesh就可以了。

Service Mesh有如下幾個特點:

  • 應用程式間通訊的中間層
  • 輕量級網路代理
  • 應用程式無感知
  • 解耦應用程式的重試/超時、監控、追蹤和服務發現

Service Mesh的架構如下圖所示:

『Microservices & Nameko』Python 微服務實踐

比較流行的開源軟體有Istio,有機會再去玩一下。

2.2 Serverless

無服務架構,第一次接觸是在AWS的技術峰會上,簡單的說就是不需要關心伺服器,整個計算堆疊,包括執行功能程式碼的作業系統程式,完全由雲提供商管理。更加強化了 DevOps 的理念。

實際玩過AWS Lambda 無服務應用程式,確實很方便,簡化為一個函式,通過 API Gateway + Lambda 則可實現Web服務。按請求量收費,這一點目前覺得很坑,尤其是請求量大時,產生的費用遠遠比自己將應用部署在Docker上會貴很多。 所以目前無服務架構的場景也是非常適合一些一次性任務,請求量呼叫不多的場景來說會非常方便,開發者成員就可以自己開發自己部署,不再需要關心伺服器。 可以免除所有運維性操作,開發人員可以更加專注於核心業務的開發,實現快速上線和迭代。

『Microservices & Nameko』Python 微服務實踐

3. Python framework for building microservices

3.1 Nameko Introduce

Nameko是Python中的微服務框架,git地https://github.com/nameko/nameko,受歡迎度暫時還不高,官方文件的介紹實現了:

It comes with built-in support for:

  • RPC over AMQP
  • Asynchronous events (pub-sub) over AMQP
  • Simple HTTP GET and POST
  • Websocket RPC and subscriptions (experimental)

簡單的說RPC建立在AMQP上,在AMQP上實現了釋出訂閱,實現了簡單的HTTP服務,還有Websocket RPC。

這簡直跟Java的生態完全感覺是小兒科。架構通過RabbitMQ作為message broker,供給各個Nameko Service之間的通訊。

『Microservices & Nameko』Python 微服務實踐

更多的細節請檢視官方文件。

3.2 Practice

接下來實踐一下,以某一個業務場景為例。

場景: 假設社交場景中,評論別人的文章,伺服器給文章作者推送一條訊息告知有人評論,同時評論必須得先註冊。

涉及到2個微服務,註冊服務和推送服務,同時有一個評論介面。

3.2.1 環境搭建:

  • python3.5+
  • RabbitMQ
  • Redis 3.2.1
  • Nameko 2.11.0
  • Swagger
  • Flask 1.0.2

首先需要準備Python3環境,Redis簡單起見作為使用者登入註冊的儲存,Nameko用pip安裝,RabbitMQ最好用Docker安裝。

# RabbitMQ docker 安裝命令
docker search rabbitmq
docker pull rabbitmq:3.7-rc-management
docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3.7-rc-management
# 需要預設執行在5672埠

Doc: https://github.com/docker-library/docs/tree/master/rabbitmq

# nameko 執行服務命令:
nameko run service --broker amqp://guest:guest@localhost

其中 guest:guest是RabbitMQ Docker映象的使用者名稱和密碼
複製程式碼

同時為了方便API測試,通過flasgger提供Swagger UI進行整合Flask。

準備好環境後,開始程式碼部分演示。

3.2.2 程式碼演示:

├── app
│   └── api.py
├── dependence
│   ├── __init__.py
│   └── services
│       ├── __init__.py
│       ├── config.py
│       └── redis_service.py
└── microservices
    ├── push.py
    └── register.py
複製程式碼

程式碼結構如上:

  • app中儲存的是api介面服務。
  • dependence可以理解為基礎模組,可能很多的微服務都依賴的封裝好的服務,比如redis,mysql的介面,一般用Git倉庫的話,可以submodule到具體服務的倉庫下,這裡測試就全部放一個倉庫了。
  • microservices 微服務程式碼,這裡演示2個服務,註冊和推送服務。

需要實踐的是2個功能:

  • API程式碼中如何呼叫微服務
  • 微服務中如何呼叫其他微服務

先介紹一下dependence中的程式碼:

# content of redis_service
class RedisService(object):
    def __init__(self):
        self.redis_instance = RedisClient.get_redis(
            config.REDIS_NAME, config.REDIS_HOST, config.REDIS_PORT,
            config.REDIS_DB)
        self.users_key = "users"
        self.users_data_key = "users_data"

    def check_registered_and_get_info(self, u_id):
        """
        Check if the user is registered and return user information if registered.
        """
        user_data = self.redis_instance.hget(self.users_data_key, u_id)
        if not user_data:
            return False, None
        return True, json.loads(user_data)

    def check_email_is_registered(self, email):
        u_id = self.redis_instance.hget(self.users_key, email)
        return u_id

    def register(self, u_id, email, data):
        self.redis_instance.hset(self.users_key, email, u_id)
        result = self.redis_instance.hset(self.users_data_key, u_id,json.dumps(data))
        return result
複製程式碼
  • check_registered_and_get_info 校驗是否已經註冊,如果已經註冊則獲取使用者資訊返回
  • check_email_is_registered 檢查郵箱是否重複註冊
  • register 註冊並儲存使用者資訊

接下來看API程式碼部分:

# content of api.py
import time
import random
from flask import Flask, request, jsonify
from flasgger import Swagger
from nameko.standalone.rpc import ClusterRpcProxy
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--port", help="app running port", type=int, default=5000)
parse_args = parser.parse_args()

app = Flask(__name__)
Swagger(app)

CONFIG = {'AMQP_URI': "amqp://guest:guest@localhost"}
複製程式碼

程式碼校長,分開展示,這部分是引用部分,需要nameko和Swagger,nameko是提供微服務的RPC服務代理,同時需要提供CONFIG,內容是Message Broker地址,其實就RabbitMQ。 Swagger是和Flask結合,方便網頁介面進行API測試。

# content of api.py
@app.route('/api/v1/comment', methods=['POST'])
def comment():
    """
    Comment API

    Parameters Explain:

        timestamp    評論時間
        u_id         使用者id
        content      評論內容
        article_id   文章ID
        article_u_id 文章作者使用者id
        parent_comment_id 父評論id (optional)
    ---
    parameters:
      - name: body
        in: body
        required: true
        schema:
          id: comment
          properties:
            timestamp:
              type: integer
            u_id:
              type: string
            content:
              type: string
            article_id:
              type: integer
            article_u_id:
              type: integer
            parent_comment_id:
              type: integer
    responses:
      code:
        description: 0 Comment Success!
      message:
        description: Error Message!
      data:
        description: return comment_id
    """
    data = request.json
    article_u_id = data.get("article_u_id")
    u_id = data.get("u_id")
    code, message = 0, ""
    if not article_u_id or not u_id:
        code, message = 10003, "article_u_id or u_id is null."
        response = dict(code=code, message=message, data="")
        return jsonify(response)
    with ClusterRpcProxy(CONFIG) as rpc:
        user_data = rpc.register.check_registered(u_id)
        if not user_data:
            code, message = 10004, "You need to register to comment."
            response = dict(code=code, message=message, data="")
            return jsonify(response)

        # push message
        print("Push Message: article_u_id: {}".format(article_u_id))
        result, message = rpc.push.push(article_u_id, data.get("content"))
        print("push result: {}, message: {}".format(result, message))

    # save comment data
    print("Save Comment Data: article_id: {} content: {}".format(
        data.get("article_id"), data.get("content")))

    data = dict(comment_id=int(time.time()))
    response = dict(code=0, message="", data=data)
    return jsonify(response)
複製程式碼

評論介面,描述部分是提供Swagger的API介面描述(其規範需要遵循Swagger規範,具體可以檢視官方文件),提供評論者的使用者ID,文章ID,評論的內容,文章作者使用者ID(簡單起見,直接客戶端提供,正常場景是根據文章ID找到作者的使用者ID)。

實現的功能也非常簡單,先通過呼叫檢查註冊服務看評論者是否有註冊,沒有就直接返回需要註冊才能評論。如果已經註冊,則呼叫推送服務給作者進行推送通知。之後並儲存評論資訊,返回評論ID。

關鍵資訊就是在 註冊和推送 微服務的實現,儲存評論資訊,我這裡直接print,沒有做實際的操作。

@app.route('/api/v1/register', methods=['POST'])
def register():
    """
    Register API

    Parameters Explain:

        timestamp    註冊時間
        email        註冊郵箱
        name         名稱
        language     語言
        country      國家
    ---
    parameters:
      - name: body
        in: body
        required: true
        schema:
          id: data
          properties:
            timestamp:
              type: integer
            email:
              type: string
            name:
              type: string
            language:
              type: string
            country:
              type: string
    responses:
      code:
        description: 0 register success.
      message:
        description: Error Message!
      data:
          description: return u_id
    """

    user_data = request.json
    email = user_data.get("email")
    code, message = 0, ""
    if not email:
        code, message = 10000, "email is null."
        response = dict(code=code, message=message, data="")
        return jsonify(response)
    u_id = None
    with ClusterRpcProxy(CONFIG) as rpc:
        u_id, message = rpc.register.register(email, user_data)
    if message:
        code = 10001
    data = dict(u_id=u_id)
    response = dict(code=code, message=message, data=data)
    return jsonify(response)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(parse_args.port), debug=True)
複製程式碼

這是api.py最後一段,實現的是註冊介面,簡單的說就是呼叫註冊服務,如果已經註冊則直接返回,否則儲存使用者資訊。

關鍵在於註冊服務的實現。

執行 python api.py 開啟 http://localhost:5000/apidocs/可以看到如下介面:

『Microservices & Nameko』Python 微服務實踐

點開其中一個API,可以看到如下介面:

『Microservices & Nameko』Python 微服務實踐

非常方便進行API介面除錯。

接下來重點來了,演示微服務程式碼部分:

import random
from nameko.rpc import rpc
import sys
sys.path.append("..")
from dependence.services import RedisService

class RegisterService(object):
    name = "register"

    def __init__(self):
        self.redis_handle = RedisService()

    @rpc
    def check_registered(self, u_id):
        is_registered, user_data =   self.redis_handle.check_registered_and_get_info(u_id)
        if is_registered:
            return user_data
        return None

    @staticmethod
    def generate_u_id():
        """
        Test Function
        """
        return str(random.randint(7000000, 9999999))

    @rpc
    def register(self, email, user_data):
        u_id = self.redis_handle.check_email_is_registered(email)
        if u_id:
            return u_id, "already registered."
        u_id = self.generate_u_id()
        register_result = self.redis_handle.register(u_id, email, user_data)
        if register_result:
            return u_id, ""
        return None, "register failed.
複製程式碼

關注register的實現,需要匯入nameko.rpc,並且用rpc裝飾該函式。實現非常簡單,裡面程式碼就是邏輯部分,生成u_id,然後儲存到redis中。

這樣就實踐了第一個功能,在API中呼叫微服務。

接下來看看 push 服務的實現:

import random
from nameko.rpc import rpc, RpcProxy
import sys
sys.path.append("..")
from dependence.services import RedisService

class PushService(object):
    name = "push"
    register_rpc = RpcProxy("register")

    @rpc
    def push(self, u_id, content):
        user_data = self.register_rpc.check_registered(u_id)
        if not user_data:
        print("User:{} not existed.".format(u_id))
            return False, "not registered."
        language, country = user_data["language"], user_data["country"]

        # get language push content
        print("Push Progress: u_id: {} language: {}, country: {}, content: {}".
              format(u_id, language, country, content))

        return True, "push success."
複製程式碼

push服務中需要呼叫註冊服務,判斷文章作者是否註冊(其實能夠發表文章肯定是已經註冊,這裡是指演示),這樣就微服務中呼叫微服務,需要額外import RpcProxy,指定 註冊服務 RpcProxy("register"),然後再服務中呼叫即可,並且拿到使用者的資訊,判斷語言和國家,推送對應的語言內容。

整體來講,Nameko這個框架,程式碼層實現非常簡單,輕量級,簡單實用。但是功能不全,Python 後端應用場景不多。

3.2.3 除錯

開三個終端,分別執行:

cd microservices & nameko run push
cd microservices & nameko run register

cd app & python api.py
複製程式碼

開啟http://localhost:5000/apidocs/#/

準備註冊資料:

註冊資訊1
{
  "country": "CN",
  "email": "nameko@nameko.com",
  "language": "ZH",
  "name": "xiaohua",
  "timestamp": 1553652949
}

註冊資訊2
{
  "country": "CN",
  "email": "nameko2@nameko.com",
  "language": "ZH",
  "name": "xiaoming",
  "timestamp": 1553652950
}

複製程式碼

操作示例:

『Microservices & Nameko』Python 微服務實踐

返回資訊:

返回資訊1
{
  "code": 0,
  "data": {
    "u_id": "7434029"
  },
  "message": ""
}

返回資訊2
{
  "code": 0,
  "data": {
    "u_id": "8240184"
  },
  "message": ""
}
複製程式碼

呼叫push介面:

『Microservices & Nameko』Python 微服務實踐

點選執行execute。

返回資訊:

『Microservices & Nameko』Python 微服務實踐

python api.py 終端列印資訊:

Push Message: article_u_id: 7434029
push result: True, message: push success.
Save Comment Data: article_id: 100 content: very good.
127.0.0.1 - - [27/Mar/2019 23:24:07] "POST /api/v1/comment HTTP/1.1" 200 -
複製程式碼

Python 微服務就演示到這裡,完整程式碼 github 地址github.com/CrystalSkyZ…

下一篇聊一下RPC。

更多精彩文章,請關注公眾號『天澄技術雜談』

『Microservices & Nameko』Python 微服務實踐

相關文章