说明

我觉得已经快不太方便直接通过docker ps来查看运行中的容器了,另外就是批量的启动和停止分身比较麻烦,所以还是需要一个服务来替代手工操作。

这只是其中一小部分容器
Python 全栈系列218 Docker Commander-LMLPHP

内容

1 命名规则

开头第一个单词代表容器类型,第二个是项目名称,第三个是容器的子类型,最后加上一串数字(一般是毫秒时间戳)

一个例子为

Server.SOMEPROJECT.Port_12345.Base_12345679000这表示了某个服务容器的名称,应该可以比较明显的区分不同的项目。服务项目在所有机器上的端口和项目位置是一样的,不同机器间的协同将通过反向代理实现。

1.1 第一级

Server:服务,需要有端口分配。

Worker: 工人,不需要端口。

1.2 第三级

Base: 基础服务

Shard: 分身

CNTWorker: 按次的工人。

BandWorker:按时间的工人。

Port: 如果是服务

2 功能

常用的命令有:

  • 1 查看当前的容器列表
  • 2 查看某个容器的日志
  • 3 停止 + 删除某个容器
  • 4 启动容器

从项目的视角看, 关心的角度有:

  • 1 某个服务是否启动了
  • 2 某个Server/Worker期望启动一定的数量,是否都启动了
  • 3 某个Server/Worker希望关闭对应的容器,是否都关闭了

3 设计

前提:

  • 1 可以看到所有的运行容器
  • 2 可以透传docker命令
  • 3 容器的命名符合上述的命名规范(点横式)
  • 4 项目的名称可以视为主键

我们关心项目的应用,在期望和实际的状态间可能会有gap,所以可以把每个项目想象成一个对象,有两类属性:

  • 1 Expectation: 期望值
  • 2 Fact: 实际值

对于Server来说,我们可能只维持一个服务,也可能为此创建多个分身。Server的刚性限制是端口资源,所以在Server开始之前,要先进行端口资源的分配。

假设我们现在使用itables, 可以随意修改字段的配置。端口的表示可以采用逗号分割的方式,也可以采用波浪号表示范围。也可以同时混用两种方式,用分号隔开。

有些服务可能需要的是一组端口,那么就只能使用小括号(元组)的表示方法,不过这类服务是少数,暂时不管。使用一个字段 IsSinglePort来表示服务的端口需求特点。

对于服务来说,有BasePort和DuplicatePort两个字段。前者放默认的主服务,后者放可供用于拓展的端口。

服务的启动命令存放在一个前端不可见字段中(cmd),只将端口进行启动的参数化。这样docker commander就可以根据cmd和当前的端口号来执行启动命令了。

默认的服务,期待的启动个数是1个(ToRun), docker commander会查询当前的容器列表,进行比对后得到实际的启动个数(Running)。

当实际的个数少于期待的个数时,服务会尝试进行启动。启动时, 查询的是全量的容器列表,如果已经存在项目和端口重名的(可能是之前挂掉的容器没删除)会先进行删除,然后按默认规则逐一启动。

这里又有一个点,就是当配置的端口号为空,或者数量不够时,dc会启动和端口号匹配的若干个容器。然后将这个状态返回(更新Mongo), PortAllocateState。

当已经运行的服务(Running)多余ToRun时,dc会逐一删除多余的容器。所以ToRun是一个手工配置的整型参数。

Workers的启动管理更为简单,不需要端口配置。在按次、按时与分发、独取的模式上又有可配置之处,但这些会封装在应用内部,和dc无关。dc将根据ToRun和Running来管理Worker的数量。唯一要考虑的是计算机的物理内存,如果超过80%的内存占用,那么dc将不再启动新的worker,用一个MemoryRestrictState来表示。

所以,dc应该存在一个接口,调用时会分析配置表和当前容器的差异,然后增加或者删除容器来进行差异的平衡。

背后通过项目名称(主键),对容器列表进行解析来进行比对。

同时,dc应该具有查询系统内存的功能、访问Mongo的功能、启动和删除容器的功能,配置信息将存在mymeta集群,不同机器的配置使用一个Machine字段来表示。

4 工程实现

DC的作用域就是含点的容器,如果要生成新的容器,其容器名也必然含点。这样避免了和现行的容器冲突,同时也立了一个规范: 容器名带点的可以被DC自动化管理。当然,带了点还要同时符合上面约定命名规范。

def get_DC_containers(container_list, prefix_list = ['Server','Worker']):
    filter_container_list = [x for x in container_list if '.' in x and x[:x.find('.')] in prefix_list]
    return filter_container_list

将这部分封装在接口中,显然,现在并没有DC可以管的容器
Python 全栈系列218 Docker Commander-LMLPHP
制作名称的函数

Python 全栈系列218 Docker Commander-LMLPHP

做一个最近在搞的的worker命令模板
Python 全栈系列218 Docker Commander-LMLPHP
此时可以查到,dc管理的容器里出现了一个容器。
Python 全栈系列218 Docker Commander-LMLPHP
里面关键的参数化部分,一个是worker名称,还有一个就是命令行。

Python 全栈系列218 Docker Commander-LMLPHP
我觉得参数部分可以稍微再复杂一些:

  • 1 在itables上,使用一个字符字段来存字典
  • 2 使用以前的一个函数将字符串随时转成python字典
  • 3 使用某个函数将字典转为命令行关键字

shell命令分为固定部分和参数部分拼接,这样就可以灵活的修改参数。有这样一种场景:

  • 1 一开始采用50000次的配合启动worker分身
  • 2 后来发现任务量降下来了,所以改成100次

字符串转字典:

# 字符串转字典 
import ast
def str2dict(some_str):
    try:
        res = ast.literal_eval(some_str)
        if isinstance(res,dict):
            return res 
        else:
            return ''
    except:
        return ''

字段转关键字参数

# 关键字参数转shell格式
def kwargs2shell(kw_dict):
    str_list = []
    for k, v in kw_dict.items():
        k = str(k).strip()
        v = str(v).strip()
        the_ele = " --{0}='{1}' ".format(k,v)
        str_list.append(the_ele)
    return ''.join(str_list)

套在一起使用
Python 全栈系列218 Docker Commander-LMLPHP
最简单的方式,就是一个基础命令字符串在前面,然后后面带上关键字字符串就可以了。

在这里突然又想到,ToRun应该要再分一分,一种是计数型的ToRunNum,一种则是带宽型的ToRunBand。差别在于,前者启动的Worker,每启动一个就会将数值扣减1,DC在启动了一个Worker之后就会将这个数值自减1(可以采用Mongo自带的扣减机制)

主要问题解决后,接下来就是小细节了,就不再铺开说。

04-07 12:17