pycureka 0.1.1
A eureka client written in python. Support registering your python component to Eureka Server, as well as calling remote services by pulling the the Eureka registry.

# pyeureka

## 描述

这是一个使用 Python 语言编写的 eureka 客户端，你可以非常简单的使得它与你的其他 Spring Cloud 组件集成在一起。

## 支持版本

Python 3.7+

*从`0.9.0`开始，不再支持 python 2，如果你需要使用 python 2，请使用 `0.8.10` 版本。*

## 特点

* 同时支持注册以及发现服务。
* 支持故障切换。
* 支持DNS发现。
* 非常简单的配置过程，堪比 Springboot 的配置文件。
* 自动化的心跳以及组件状态机制，不需要开发者维护心跳。
* 自动化的退出机制。只要 Python 进程正常退出，组件会自己从 eureka 服务器退出。
* 封装了调用其他服务的接口，用法类似 Spring boot 的 RestTemplate。
* 调用其他服务时支持多种 HA（高可用）的策略

## 如何使用

### 安装

```Shell
pip install pyeureka
```

### 推荐使用

最简单的使用方法如下：

```python
from pyeureka import eureka_client

your_rest_server_host = "192.168.10.106"
your_rest_server_port = 9090
# The flowing code will register your server to eureka server and also start to send heartbeat every 30 seconds
eureka_client.init(eureka_server="http://your-eureka-server-peer1,http://your-eureka-server-peer2",
                   app_name="your_app_name",
                   # 当前组件的主机名，可选参数，如果不填写会自动计算一个，如果服务和 eureka 服务器部署在同一台机器，请必须填写，否则会计算出 127.0.0.1
                   instance_host=your_rest_server_host,
                   instance_port=your_rest_server_port,
                   # 调用其他服务时的高可用策略，可选，默认为随机
                   ha_strategy=eureka_client.HA_STRATEGY_RANDOM)
```

在你的业务代码中，通过以下的方法调用其他组件的服务

```python
from pyeureka import eureka_client

res = eureka_client.do_service("OTHER-SERVICE-NAME", "/service/context/path",
                               # 返回类型，默认为 `string`，可以传入 `json`，如果传入值是 `json`，那么该方法会返回一个 `dict` 对象
                               return_type="string")
print("result of other service" + res)
```

你也可以直接使用 `EurekaClient` 类。

```python
from pyeureka.eureka_client import EurekaClient
client = EurekaClient(eureka_server="http://my_eureka_server_peer_1/eureka/v2,http://my_eureka_server_peer_2/eureka/v2", app_name="python_module_1", instance_port=9090)
client.start()
res = client.do_service("OTHER-SERVICE-NAME", "/service/context/path")
print("result of other service" + res)
# when server is shutted down:
client.stop()
```

事实上，`init` 和相关的方法只是 `EurekaClient` 的一个门面（facade），其底层最终还是包含这一个 `EurekaClient` 的实例对象。你可以接收 `init` 方法的返回值，或者使用 `eureka_client.get_client()` 取得这个对象。`init` 会自动开始注册、心跳流程，并且会在程序退出的时候自动发送退出信号。而如果你 直接使用 `EurekaClient` 对象，你需要显式调用`start()` 和 `stop()` 方法来开始和停止注册过程。

*在接下来的文档中，我会仅使用门面（facade）函数作为例子，事实上，你可以从 `EurekaClient` 类中找到这些函数对应的方法。*

### 注册服务

最常用的注册方法是：

```Python
from pyeureka import eureka_client

eureka_server_list = "http://your-eureka-server-peer1,http://your-eureka-server-peer2"
your_rest_server_host = "http://192.168.10.11"
your_rest_server_port = 9090
# The flowing code will register your server to eureka server and also start to send heartbeat every 30 seconds
eureka_client.init(eureka_server=eureka_server_list,
                                app_name="your_app_name",
                                instance_host=your_rest_server_host,
                                instance_port=your_rest_server_port)
```

你还可以不传入`instance_host`参数，如果不传入那个参数，组件会根据当前的网络取得一个 ip 作为参数。

```Python
from pyeureka import eureka_client

your_rest_server_port = 9090
# The flowing code will register your server to eureka server and also start to send heartbeat every 30 seconds
eureka_client.init(eureka_server="http://your-eureka-server-peer1,http://your-eureka-server-peer2",
                                app_name="your_app_name",
                                instance_port=your_rest_server_port)
```

如果你有多个 `zone`，你可以通过参数 `eureka_availability_zones` 来进行配置。

```python
from pyeureka import eureka_client
eureka_client.init(eureka_availability_zones={
                "us-east-1c": "http://ec2-552-627-568-165.compute-1.amazonaws.com:7001/eureka/v2/,http://ec2-368-101-182-134.compute-1.amazonaws.com:7001/eureka/v2/",
                "us-east-1d": "http://ec2-552-627-568-170.compute-1.amazonaws.com:7001/eureka/v2/",
                "us-east-1e": "http://ec2-500-179-285-592.compute-1.amazonaws.com:7001/eureka/v2/"}, 
                zone="us-east-1c",
                app_name="python_module_1", 
                instance_port=9090,
                data_center_name="Amazon")
```

但如果你希望更具灵活性，你可以使用 DNS 来配置 Eureka 服务器的 URL。

假设，你有以下的 DNS txt 记录：

```
txt.us-east-1.mydomaintest.netflix.net="us-east-1c.mydomaintest.netflix.net" "us-east-1d.mydomaintest.netflix.net" "us-east-1e.mydomaintest.netflix.net"
```

然后，你可以使用 DNS txt 记录 为每个上述的 `zone` 定义实际的 Eureka 服务的 URL：

```
txt.us-east-1c.mydomaintest.netflix.net="ec2-552-627-568-165.compute-1.amazonaws.com" "ec2-368-101-182-134.compute-1.amazonaws.com"
txt.us-east-1d.mydomaintest.netflix.net="ec2-552-627-568-170.compute-1.amazonaws.com"
txt.us-east-1e.mydomaintest.netflix.net="ec2-500-179-285-592.compute-1.amazonaws.com"
```

之后，你可以通过这样的方式来初始化 eureka client：

```python
from pyeureka import eureka_client
eureka_client.init(eureka_domain="mydomaintest.netflix.net",
                region="us-east-1",
                zone="us-east-1c",
                app_name="python_module_1", 
                instance_port=9090,
                data_center_name="Amazon")
```

你可以独立配置 eureka 服务器的协议、简单认证、上下文路径，而不把这些放在 URL中。

```python
from pyeureka import eureka_client
eureka_client.init(eureka_domain="mydomaintest.netflix.net",
                region="us-east-1",
                zone="us-east-1c",
                eureka_protocol="https",
                eureka_basic_auth_user="keijack",
                eureka_basic_auth_password="kjauthpass",
                eureka_context="/eureka/v2",
                app_name="python_module_1", 
                instance_port=9090,
```

或者

```python
from pyeureka import eureka_client
eureka_client.init(eureka_server="your-eureka-server-peer1,your-eureka-server-peer2",
                eureka_protocol="https",
                eureka_basic_auth_user="keijack",
                eureka_basic_auth_password="kjauthpass",
                eureka_context="/eureka/v2",
                app_name="python_module_1", 
                instance_port=9090)
```

*关于默认的 `instance_ip` 和 `instance_host`*：

如 Spring 的实现一样，`py-eureka-client` 在亚马逊的数据中心，会使用数据中心元数据服务取得的 `local-ipv4` 和 `local-hostname` 做为默认值，否则则会取第一个取得的具有 IPv4 的地址的网卡地址作为默认的地址。

### 调用远程服务

当初始化完 eureka client 之后，你就可以通过拉取 eureka server 的信息来调用远程服务了。

最简单的调用方式是：

```python
from pyeureka import eureka_client

try:
    res = eureka_client.do_service("OTHER-SERVICE-NAME", "/service/context/path", return_type="string")
    print("result of other service" + res)
except urllib.request.HTTPError as e:
    # If all nodes are down, a `HTTPError` will raise.
    print(e)
```

上述参数中，return_type 可以选择传入`json`，如果传入`json`，则该接口返回一个 `dict` 对象，如果传入`response_object`，那么该方法会返回原始的 HTTPResponse 对象。该参数也可不传入，默认返回的为 `str` 的响应体的内容。

这个方法还接受其他的参数，剩余的参数和 `urllib.request.urlopen`(python2 是 `urllib2.urlopen`) 接口一致。请参考相关的接口或者源代码进行传入。

这个方法还提供异步的版本：

```python
from pyeureka import eureka_client

def success_callabck(data):
    # type: (Union[str, dict]) -> object
    # 处理正常返回的参数
    print(data)

def error_callback(error):
    # type: (urllib.request.HTTPError) -> object
    # 处理错误
    print(error)

eureka_client.do_service_async("OTHER-SERVICE-NAME", "/service/context/path", on_success=success_callabck, on_error=error_callback)
```

如果你不希望使用内置的 HTTP 客户端，希望使用其他的客户端的话，你可以使用 `walk_nodes` 函数来实现：

```python
from pyeureka import eureka_client

def walk_using_your_own_urllib(url):
    print(url)
    """
    # 根据传入的 url 参数，通过你选择的其他库来调用其他组件提供的 Restful 接口。
    # 你返回的数据会直接被 `eureka_client.walk_nodes` 函数返回
    # 如果你发现给定的 url 的节点无法访问，请 raise 一个 `urllib.request.HTTPError`(urllib2.HTTPError in python2)，
    # 之后 `eureka_client.walk_nodes` 会继续寻找其他状态为 UP 的节点来调用。
    """

try:
    # `res` 是你在 walk_using_your_own_urllib 中返回的数据。
    res = eureka_client.walk_nodes("OTHER-SERVICE-NAME", "/service/context/path", walker=walk_using_your_own_urllib)
    print(res)
except urllib.request.HTTPError as e:
    # 如果所有的节点没有正确返回结果，以上错误将被抛出
    print(e)
```

这个方法同样有一个异步的版本：

```python
from pyeureka import eureka_client

def walk_using_your_own_urllib(url):
    print(url)

def success_callabck(data):
    # type: (Union[str, dict]) -> object
    print(data)

def error_callback(error):
    # type: (urllib.request.HTTPError) -> object
    print(error)

eureka_client.walk_nodes("OTHER-SERVICE-NAME", "/service/context/path",
                          walker=walk_using_your_own_urllib,
                          on_success=success_callabck,
                          on_error=error_callback)
```

### 高可用

`do_service` 和 `walk_nodes` 方法支持 HA（高可用），该方法会尝试所有从 ereka 服务器取得的节点，直至其中一个节点返回数据，或者所有的节点都尝试失败。

该方法有几种 HA 的策略，这些策略分别是：

* HA_STRATEGY_RANDOM, 默认策略，随机取得一个节点。
* HA_STRATEGY_STICK, 随机取得一个节点之后一直使用该节点，直至这个节点被删除或者状态设为 DOWN。
* HA_STRATEGY_OTHER, 总是使用和上次不同的节点。

如果你需要修改这些策略，你可以初始化发现服务时指定相应的策略：

```python
from pyeureka import eureka_client

eureka_server_list = "http://your-eureka-server-peer1,http://your-eureka-server-peer2"

eureka_client.init(eureka_server=eureka_server_list,
                   app_name="your_app_name",
                   instance_port=9090,
                   ha_strategy=eureka_client.HA_STRATEGY_OTHER)
```

如果上述内置的 HA 策略都不能满足你的需求，你可以将按以下的办法取得整个服务注册库来构建你自己的访问方法：

```python
from pyeureka import eureka_client

client = eureka_client.get_client()
app = client.applications.get_application("OTHER-SERVICE-NAME")
up_instances = app.up_instances
up_instances_same_zone = app.up_instances_in_zone(client.zone)
up_instances_other_zone = app.up_instances_not_in_zone(client.zone)
inst = up_instances[0]

# ... 组装访问链接和进行远程调用

```

### 使用三方 HTTP 客户端

默认情况下，组件使用了内置的 urllib.request (python 2 中时 urllib2 ) 来进行 HTTP 请求。你可以使用别的 HTTP 库来进行访问。这在自签名的 HTTPS 证书的场景下尤为有效。

你需要以下步骤来使用自己的 HTTP 客户端：

1. 继承 `pyeureka.http_client` 中的 `HttpClient` 类。
2. 重写该类的 `urlopen` 方法，注意：该方法返回的是响应体的文本。
3. (可选) 当你的 `urlopen` 方法不是返回 `http.client.HTTPResponse`，你还需要提供一个 `read_response_body` 方法来读取其响应体中的字符串。
4. 将你定义的类设置到`pyeureka.http_client` 中。

```python
import pyeureka.http_client as http_client

# 1. 继承 `pyeureka.http_client` 中的 `HttpClient` 类。
class MyHttpClient(http_client.HttpClient):

    # 如果你需要自定义一些字段，也请不要修改构造方法的参数值。在这种情况下，使用 *args, **kwargs 是一个很好的选择。
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kw)
        self.other_field = "..."

    # 2. 重写该类的 `urlopen` 方法，注意：该方法返回的是响应体的文本。
    # 请注意，如果你要抛出异常，请确保抛出的是 urllib.error.HTTPError 或者 urllib.error.URLError
    # (Python 2 则分别是 urllib2.HTTPError 或者 urllib2.URLError) 否则可能会发生未可知之错误。
    def urlopen(self):
        # 以下是默认实现，你可以查看该类有哪一些参数。
        return urllib2.urlopen(self.request, data=self.data, timeout=self.timeout,
                              cafile=self.cafile, capath=self.capath,
                              cadefault=self.cadefault, context=self.context)

    # 3. 可选，提供一个方法读取响应体中的文本内容。
    def read_response_body(self, res) -> str:
        if res.info().get("Content-Encoding") == "gzip":
            f = gzip.GzipFile(fileobj=res)
        else:
            f = res

        txt = f.read().decode(_DEFAULT_ENCODING)
        f.close()
        return txt

# 4. 将你定义的类设置到`pyeureka.http_client` 中。
http_client.set_http_client_class(MyHttpClient)
```

你可在这个[问题](https://github.com/keijack/python-eureka-client/issues/35#issuecomment-706510869)中找到实际应用的例子。

### 日志

默认情况下，日志会输出到控制台，你创建自己的 Logging Handler 来将日志输出到别处，例如一个滚动文件中：

```python
import pyeureka.logger as logger
import logging

_formatter = logging.Formatter(fmt='[%(asctime)s]-[%(name)s]-%(levelname)-4s: %(message)s')
_handler = logging.TimedRotatingFileHandler("/var/log/py-eureka-client.log", when="midnight", backupCount=7)
_handler.setFormatter(_formatter)
_handler.setLevel("INFO")

logger.set_handler(_handler)
```

如果你想增加一个日志控制器而不是想替代内置的，那么你可以使用以下方法：

```python
logger.add_handler(_handler)
```

你也可以使用以下方法来设置日志输出级别：

```python
logger.set_level("DEBUG")
```

这个日志使用了一个背景线程来输出日志，因此其非常适合使用在多线程的场景，特别你是你有多个 logger 共用一个 `TimedRotatingFileHandler` 的时候。在多线程的场景下，这个日志控制器经常不能正常地按时切割文件。

## 亚马逊数据中心支持

理论上，这个组件可以正常运行在亚马逊的数据中心。当运行在亚马逊数据中心，会从亚马逊的 metadata 服务中取得相关的元数据并且自动填充到 DataCenterInfo 中，填充的字段信息来源自 Netflix 的 Java 客户端中的 `com.netflix.appinfo.AmazonInfo` 类。**不过**，由于我本人没有亚马逊的相关环境作为测试，所以，在实际的运行当中，可能会发生错误。如果真的发生了错误的话，请提出 ISSUE 并且提供详细的日志，我会尽力支持。如果运行没有问题，如果可以，也欢迎在这个[问题](https://github.com/keijack/python-eureka-client/issues/33)进行回复。


## 更多信息

**其他更多的信息请查看项目注释。**