一 圖論概述
1 圖的分類
1 無向圖
圖 graph由頂點和邊組成,頂點的又窮非空集合爲V,邊的集合爲E,記做G(V,E)
頂點vertex,數據元素的集合,頂點的集合,又窮非空,
邊edge,數據元素關係的集合,頂點關係的集合,可以爲空,邊分爲有向和無向兩種無向邊記做(A,B),或者(B,A),使用小括號
無向圖,記做undirected Graph 無向邊的邊構成的圖,G=(V,E),V={A,B,C,D},E={(A,B),(A,C),(B,C),(B,D),(C,D)}
2 有向圖
有向邊記做<A,B>,即從頂點A指向頂點B,<B,A>表示頂點B指向頂點A,使用尖括號,有向邊也叫做弧,邊表示爲弧尾指向弧頭。
有向圖directed graph
有方向的邊組成的圖
G=(V,E) V={A,B,C,D} E={<A,B>,<A,C>,<C,B>,<B,D>}
3 圖的其他概念
1 稀疏圖 sparse graph
圖中邊很少,最稀疏的情況是隻有頂點沒有邊,這就是數據結構SET
2 稠密圖 dense Graph
圖中邊很多,最稠密的情況,任意2個頂點之間都有關係
3 完全圖 complete graph
包括了所有可能的邊,達到了稠密圖最稠密的情況,任意兩個頂點之間都有邊相連
有向的邊的完全圖,叫做有向完全圖,邊數爲n*(n-1)
無向的邊的完全圖,叫做無向完全圖,邊數爲n(n-1)/2
4 子圖
如果圖G(V,E)滿足V` <=V,且E` <=E,則G`是G的子圖
換句話說,就是一個圖的部分頂點和部分邊組成的圖爲子圖,有向圖需要注意邊的方向
前面的圖包含後面的圖,後面的圖可以稱爲前面的圖的子圖
一個圖的部分頂點可能是所有頂點,其部分邊也可能是所有邊
有向圖,邊是由方向的,如果找不到對應的方向,則不是其子圖,或者方向相反,則不是其子圖
5 邊的權weight和網
給邊賦予的值稱爲權,權可以表示距離,所需的時間,耗費的時間等
網network 圖中有邊有權,圖稱爲網
6 自環 loop
若一條邊的兩個頂點爲同一個頂點,則此邊稱爲自環
邊中存在這樣一個邊(u,v) 或者<u,v> ,u=v
7 簡單圖
無重複的邊或者頂點到自身的邊(自環)的圖
下面的兩個圖都不是簡單圖
4 鄰接關聯
1 鄰接
圖的邊集合爲E
無向圖,若 (u,v) 屬於 E,則稱u和v相互鄰接,互爲鄰接頂點
有向圖,若<u,v>屬於E,則邊u鄰接到v,或者v鄰接於u
簡單說,就是2點之間有條邊,2點鄰接
2 關聯(依附)
若 (u,v)屬於E或者 <u,v>屬於E,則稱邊依附於頂點u,v或者頂點u,v 與邊相關聯。
5 路徑path
1 基礎路徑
圖G(V,E),其任意一個頂點序列,相鄰2個頂點都能找到邊或者弧依次鏈接,就說明有路徑存在,有向圖的弧注意方向,所有的頂點都屬於V,所有的邊都屬於E。
頂點之間形成了路徑,此處稱爲弧。
路徑長度
等於頂點數減一,等於此路徑上的邊數
2 簡單路徑
路徑上的頂點不重複出現,這兩的路徑就是簡單路徑
無向圖中A到D的路徑有A-B-D,A-C-D,A-C-B-D 等
有向圖中A到D的路徑有A-B-D,A-C-B-D等
3 迴路
路徑的起點和終點相同,稱爲迴路
A-B-C-A-B-A
4 簡單迴路
除了路徑的起點和終點相同外,其他頂點都不相同
A-B-C-A
6 連通
1 連通
無向圖中,頂點存在路徑,則兩個頂點是連通的
注意: 連通是指A-D之間有路徑,而不是說這兩個個頂點要鄰接
2 連通圖
無向圖中,如果圖中任意兩個頂點之間都連通,就是連通圖
3 連通分量
無向圖中,指的是極大連通子圖
無向圖未必是連通圖,但是它可以包含連通子圖
4 強連通
有向圖中,頂點鍵存在2條相關的路徑,及從A到B有路徑,也存在從B到A的路徑,兩個頂點是強連通的
上述中第三個圖中有強連接,如B-D和D-B,A-C-B和B-A
5 強聯通圖
有向圖中,如果圖中任意2個頂點都是強聯通的圖
6 強聯通分量
有向圖中,指的是"極大連通子圖"
有向圖未必是強聯通圖,但是可以包含強聯通分量
7 度 degree
一個頂點的度指的是與該頂點相關聯的邊的條數,頂點v的度記做TD(v),無向圖頂點的邊數叫做度
有向圖的頂點有入度和出度,頂點的度數爲入度和出度之和 TD(v)=ID(v)+OD(v)
入度(In-degree): 一個頂點的入度是指與其關聯的各個邊中,以其爲終點的邊數
出度(Out-degree): 出度則是相對的概念,指以該頂點爲起點的邊數。
8 生成樹
1 簡單描述
它是一個極小連通子圖,它要包含圖的所有n個節點,但只只要有構成一顆數的n-1條邊
如果一個圖有n個頂點,且少於n-1條邊,則一定是非連通圖,因爲至少要有n-1條邊才行
如果一個圖有n個頂點,且多於n-1條邊,則一定有環存在,一定有2個頂點之間存在第二條路徑,但不一定是連通圖
如果一個圖由n個頂點,且有n-1條邊,但不一定是生成樹,要整好等於n-1條邊,且這些邊足以構成一顆數
2 有向樹
一個有向樹恰好有一個入度爲0的頂點,其他頂點的入度都爲1,注意,這裏不關心出度。
3 生成樹森林
若干有向樹構成有向樹森林
有向無環樹不一定能轉化爲數,但數一定是有向無環圖。
二 鄰接矩陣
1 概述
圖是由vertex 和edge組成,所以可以分爲2個數組表示
頂點使用一維數組表示,如v0,v1,v3
邊使用二維數組表示,由頂點構成二維數組
2 無向鄰接矩陣
下圖中,若存在邊。則爲1,否則爲0
A | B | C | D | ||
---|---|---|---|---|---|
A | 0 | 1 | 1 | 0 | 2 |
B | 1 | 0 | 1 | 1 | 3 |
C | 1 | 1 | 0 | 1 | 3 |
D | 0 | 1 | 1 | 0 | 2 |
2 | 3 | 3 | 2 |
此處的相關的最後行和最後一列表示度數,如果上述的對角線上的數字爲1,則表示有了自環
如果除了對角線全是1,說明沒有自環,且是一個無向完全圖
上面的矩陣,稱爲圖的鄰接矩陣
頂點的度數,等於對應行或者列求和
鄰接點,矩陣中爲1的值對應的行與列的頂點就是鄰接點。
無向圖的鄰接矩陣是一個對稱矩陣
3 有向鄰接矩陣
有向鄰接矩陣因爲是有向的,只有方向正確纔是1,否則都是0
A | B | C | D | ||
---|---|---|---|---|---|
A | 1 | 1 | 1 | 0 | 2 |
B | 0 | 0 | 0 | 1 | 1 |
C | 0 | 1 | 0 | 0 | |
D | 0 | 0 | 0 | 0 | |
0 | 2 |
有向圖的鄰接矩陣不一定對稱,對稱的說明兩個頂點之間存在環
三 運維流程系統設計
1 概述
某一個基點上有一批任務需要執行,如何執行
一個接一個排隊開始執行,但是這樣的執行可能很沒有效率,而且沒必要,如獲取兩個毫不相關的信息,誰先執行都可以,同時執行也沒問題,這樣的任務便可以並行處理,而不只是串行化進行處理。
任務的執行過程中無非是串行和並行的問題,但串行的效率可能太低。
任務處理和任務流是沒有關係的,任務的編排和任務分配不同
2 如何設計一個有向無環圖(DAG)
1 有向無環圖 directed acyclic graph (DAG)
無環路的有向圖
假設有下面幾種情況
兩個任務,任務本身就是頂點,任務先後執行
三個任務。任務1執行完成後,才能分別執行任務2和任務3
四個任務,執行任務1完成後,才能分別執行任務2和任務3,最後執行任務4.執行任務4 的時機應該是任務2 and 任務3
可以看到任務的執行過程就是流程的設定(pipeline),所以要設計一個流程系統來跑任務
2 起點和終點選擇
1 入度爲0的頂點就是起始的點
DAG 可以有多個起始點
我們的系統約定有且只有一個起始點終點的判斷
出度爲0的頂點,pipeline執行結束
pipeline可能有多個終點
3 環路預防
pipeline設計的過程中應當注意避免出現環路,因爲出現環路就不是DAG了
自環檢查,弧頭指向頂點自身
多頂點構成環路的檢測
環路檢測必須實現,否則當定義好的流程執行起來,有可能進入環路後,永遠執行不能終止。
3 schema 元數據表設計
使用數據庫表的存儲方式定義DAG
問題是如何使用數據庫的表描述一個DAG
DAG也是圖,是圖就有頂點,邊,所以可以設計2個表,頂點表,邊表,邊表用於描述一個圖,爲了存儲多個圖,定義一個圖的表
1 圖的定義(graph)
字段名 | 類型 | 說明 |
---|---|---|
id | int | 主鍵 |
name | varchar | 非空,唯一,圖的名稱 |
desc | varchar | 可爲空,描述 |
2 頂點表定義 vertex
字段名 | 類型 | 說明 |
---|---|---|
id | int | 主鍵 |
name | varchar | 非空,頂點的名稱 |
g_id | int | 外鍵,描述其屬於哪個圖 |
3 邊表 edge
字段名 | 類型 | 說明 |
---|---|---|
id | int | 主鍵 |
tail | int | 外鍵,弧尾頂點,頂點在vertex 表中必須存在 |
head | int | 外鍵,弧頭頂點,定在在vertex表中必須存在 |
g_id | int | 外鍵,描述邊屬於哪一個圖 |
通過弧尾,弧頭頂點來描述有向邊
4 具體關係如下
其中,graph表中主要是流水線的名稱,id和描述,頂點表(vertex)中主要包含頂點名稱,頂點id及頂點所屬的流水線,因爲頂點必須和流水線之間建立關係,script 表示頂點要執行的腳本,及流水線要執行的腳本。
edge表中主要包括弧尾和弧頭以及此頂點屬於哪個表,用於描述流水線的執行順序
4 script 的設計
1 設計思路
流程定義表中,任務的處理和描述
在任務調度系統中,任務的實現我們使用script腳本實現
方法一
supprocess 執行bash 腳本script
優點:簡單,易行
缺點:要啓動外部進程,bash 腳本表達能力較弱,難調試
方法二
嵌入其他語言的腳本,如lua語言
優點:不啓動子進程,功能強大。
缺點:技術要求高,需要學習其他腳本語言。
2 python 中執行lua腳本
安裝
pip install lupa
#!/usr/bin/poython3.6
#conding:utf-8
from lupa import LuaRuntime
lua=LuaRuntime() #對其進行實例化處理
print (lua.eval('1+10')) #調用方法,執行基本的函數運算操作
def pythonc(n): # 定義python方法
import socket
print('socket',n)
return socket.gethostname()
# 定義lua 腳本函數,並傳遞兩個值,一個是f,及函數,另一個則是常數
luafunc=lua.eval('''
function(f,n)
return f,n
end
''')
print (luafunc(pythonc(1),10)) #調用函數並打印
add=lua.eval('''
function (x,y)
return x+y
end
''')
print (add(10,20))
結果如下
5 執行條件(input)
1 概述
腳本在執行之前,可能需要提供一些參數,才能開始執行腳本
此處需要在頂點表vertex中增加input字段,用於存儲需要傳遞的參數。
2 表字段設計
字段名 | 類型 | 說明 |
---|---|---|
id | int | 主鍵 |
name | varchar | 非空,頂點的名稱 |
g_id | int | 外鍵,描述頂點屬於哪一個圖 |
script | text | 可以爲空,存儲任務腳本 |
input | text | 可以爲空,存儲json格式的輸入參數定義 |
3 input 格式
定義如下,json 格式
{
"name1" : {
"type":"",
"required" : True
},
"name2" :{
"type" :"",
"requried": True,
"default" :1
}
}
name 就是參數的名稱,後面定義該參數的類型,是否是必選參數等等屬性及默認屬性,其可以定義多個參數
4 作用
進入某個節點的時候,就必須滿足條件,提供足夠的參數
如果提供的參數滿足要求,就進入節點,否則一直等待到參數滿足
如果滿足了,才能去執行script
input 就是一個約束的定義
交互:
input 可以不交互,缺省值爲自動
6 任務執行
1 概述
當流程走到某一個頂點的時候,讀取任務及腳本,執行這個腳本
2 手動執行和自動執行
1 手動執行
流程走到這個頂點等待用戶操作,需要用戶手動干預,
如由用戶選擇下一個執行頂點
如下一個頂點的任務需要一些配置參數,等待用戶輸入後才能進行下一步
2 自動執行
自動填寫input,如使用缺省值,來滿足用戶爲交互式填寫的時候自動補全數據,腳本執行後,自動跳轉到下一個節點,當然這個所謂的自動,程序不會智能的選擇路徑,需要提前指定好,執行完腳本,就可以跳轉到下一個頂點了。
7 任務流轉的設計
1 概述
當流程走到某一個頂點的時候,讀取任務即腳本,或手動執行,或自動執行
2 流轉分類
手動執行,需要人工選擇下一個頂點,可以提供可視化界面供用戶方便選擇,
自動執行,就需要在信息中提供下一個節點的信息,供程序自動完成
那麼,如何區分一個頂點是否自動執行
如果vertex表中的script字段修改爲json.
如果next 不存在,則不能自動執行,需要手動操作
如果next存在,則程序自動跳轉
3 消息格式
{
"script" :"echo test"
}
{
"script" :"echo test",
"next" : 'B' # 填寫下一跳爲頂點名稱
}
{
"script" : "echo test",
"next" : 2 # 填寫下一跳爲頂點id
}
爲了方便用戶,next可以提供2種類型的參數:
1 int 表示vertex的id
2 str 表示使用vertex的name,但是是同一個graph id, 同一個DAG的定義中名字不能衝突,所以可以用。
8 流程結束
如果一個頂點的出度爲0,則此節點爲終點。
如何判斷出度爲0.
在edge表中,使用當前節點的頂點id作爲弧尾,找不到弧頭h的任何記錄。
9 執行引擎之pipeline 設計
1 概述
前面的設計僅僅是流程DAG定義,流程真正執行的時候需要記錄執行這個流程的任務流的數據,
2 表pipeline
字段名 | 類型 | 說明 |
---|---|---|
id | int | 主鍵 |
g_id | int | 外鍵,指明使用的是哪一個流程DAG定義 |
current | int | 外鍵,頂點id,表示當前走到哪一個節點 |
這個表以後還要添加其他字段,存儲一些附加信息。如誰加入的流程,執行時間等。
起點的選擇,通過查詢edge表來確定起點位置,當入度爲0的點則是起點,通過頂點表和邊表來進行處理
一個pipeline應該指向哪一個DAG,並選擇DAG的起點,因爲DAG 可能存在多個起點,即入度爲0的頂點,需要指定,然後把這些信息記錄在pipeline表中,current爲起點頂點的id,提取current 頂點的input信息,用戶輸入滿足了,才能執行script腳本
不管是手動執行還是自動執行,如果到了下一個節點,需要修改current字段的值,
任務流執行完畢,修改最後一個節點的狀態爲完成
3 舉例
當前節點任務是打包,調用maven命令執行打包,先要提取inout,要求用戶輸入ip地址,輸出目錄等信息。然後才能執行打包腳本。
10 執行引擎歷史軌跡設計
1 概述
pipeline表只能看到有哪些流正在運行,但是究竟走了DAG中的那些節點,不清楚,執行節點前輸入了那些參數也是不清楚的
如何查詢,回溯當前的pipeline的運行軌跡
2 track 表
字段名 | 類型 | 說明 |
---|---|---|
id | int | 主鍵 |
p_id | int | 外鍵,哪一個流程的歷史 |
v_idint | 外鍵,頂點的ID,經歷過的歷史節點 | |
input | text | 可以爲空,輸入的參數值 |
output | text | 可以爲空,任務的輸出 |
3 狀態設計
在pipeline表,track表中增加state字段,用於描述在某個節點上執行的狀態,是等待中,還是正在運行,還是成功或者失敗,還是執行完畢。
STATE_WAITING=0
STATE_RUNNING=1
STATE_SUCCEED=2
STATE_FAILED=3
STATE_FINISH=4
11 最終設計模型
左邊主要針對的是基礎設計,右邊主要針對的是引擎層面的設計,流程看似一樣,但其實際是不同的
12 DAG 檢測
1 DFS 算法
DFS ( depth first search)深度優先遍歷,遞歸算法
需要改進算法適用於有向圖
不能直接檢測有向圖是否有環
2 拓撲排序算法
拓撲排序就是把有向圖中的頂點以線性方式排序,如果有弧<u,b>,則最後線性排序的結果,頂點u總是在頂點b的前面
一個有向圖能被拓撲排序的充要條件是: 它必須是DAG
3 kahr算法
1 選擇一個入度爲0的頂點並輸出它
2 刪除以此頂點爲弧尾的弧重複上面2步,直到輸出全部頂點爲止,或者圖中不存在入度爲0的頂點爲止。
實例如下
第一步,找到入度爲0的點A,然後刪除A和以A爲弧尾的邊,第二步,找到入度爲0的C,刪除頂點C和以C爲弧尾的邊,第三步,找到B,刪除頂點B,並刪除以B爲弧尾的邊,最後,刪除入度爲0的頂點D
上面2個圖都不是DAG,左圖一個環,右圖2個環
這2個圖都找不到入度爲0的起始點,都不是DAG
上圖中雖然能找到入度爲0的頂點,但是移除它和關聯的邊,剩下的頂點找不到入度爲0的頂點,其不是DAG
四 項目配置流程系統
1 概述
本項目使用service層和module層進行配置和處理工作,其中,service層主要處理相關數據,而module層的作用則是創建和操作數據庫配置
2 項目創建
1 虛擬目錄創建
mkdir pipeline
cd pipeline/
pyenv virtualenv 3.5.3 pipe
pyenv local pipe
2 數據庫相關配置
create database pipeline charset utf8mb4;
grant all on pipeline.* to pipe@localhost identified by 'pipe';
flush privileges;
2 model層創建
1 概述
數據持久化最終的結果是需要通過關係數據庫的表達來實現的
2 配置文件創建
創建config文件,用於存儲數據庫配置信息
#!/usr/bin/poython3.6
#conding:utf-8
USERNAME="pipe"
PASSWORD="pipe"
DBIP="localhost"
DBPORT=3306
DBNAME="pipeline"
PARAMS="charset=utf8mb4"
URL="mysql+pymysql://{}:{}@{}:{}/{}?{}".format(USERNAME,PASSWORD,DBIP,DBPORT,DBNAME,PARAMS)
DATABASE_DEBUG=True
3 創建模型模板
用於存儲和封裝數據類
添加插件sqlachemy 和 pymysql
pip install sqlalchemy pymysql
model.py 內容如下
#!/usr/local/bin/python3.6
#coding:utf-8
# @Time : 2019/11/27 11:18
# @Author : ZhangBing
# @Email : [email protected]
# @File : model.py
# @Software: PyCharm
from pipeline.config import URL,DATABASE_DEBUG
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String,ForeignKey,Text
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker
STATE_WAITING=0
STATE_RUNNING=1
STATE_SUCCEED=2
STATE_FAILED=3
STATE_FINISH=4
Base=declarative_base()
# 創建圖,用於存儲pipeline 流水
class Graph(Base):
__tablename__='graph'
id=Column(Integer,primary_key=True,autoincrement=True)
name=Column(String(48),nullable=False)
desc=Column(String(128))
# 創建relationship關係圖,用於配置,此處的含義是可以使用此關係來處理查看此pipe下的頂點和邊的信息
vertexs=relationship("Vertex")
edges=relationship("Edge")
# 創建頂點表,用於存儲頂點信息
class Vertex(Base):
__tablename__='vertex'
id=Column(Integer,primary_key=True,autoincrement=True)
name=Column(String(48),nullable=False)
graph_id=Column(Integer,ForeignKey('graph.id')) # 配置外鍵,用於
input=Column(String(128),nullable=True)
script=Column(Text,nullable=True)
graph=relationship("Graph")
# 從頂點看邊,一個頂點對應兩個邊,肯定會出現問題,因此此處使用此方式來指定到對應的邊,此處必須使用引號,否則會導致報錯
tails=relationship("Edge",foreign_keys="[Edge.tail]")
heads=relationship("Edge",foreign_keys="Edge.head")
# 創建邊表,用戶存儲邊的相關信息
class Edge(Base):
__tablename__='edge'
id=Column(Integer,primary_key=True,autoincrement=True)
tail=Column(Integer,ForeignKey('vertex.id'),nullable=False)
head=Column(Integer,ForeignKey('vertex.id'),nullable=False)
graph_id=Column(Integer,ForeignKey('graph.id'),nullable=False)
# 用於記錄當前流水線執行到那塊了,自然需要節點信息
# 此處用於查看流水線的執行情況,如再那個流水線的那個節點。執行的結果狀態如何
class Pipeline(Base):
__tablename__='pipeline'
id=Column(Integer,primary_key=True,autoincrement=True)
current=Column(Integer,ForeignKey('vertex.id'))
graph_id = Column(Integer, ForeignKey('graph.id'))
state=Column(Integer,nullable=False,default=STATE_WAITING)
vertex=relationship("Vertex")
# 此處用於創建記錄表,此表必須和pipeline進行聯繫,並和頂點表倆西,獲取對應的頂點的最終執行信息
class Track(Base):
__tablename__='track'
id = Column(Integer, primary_key=True, autoincrement=True)
pipeline_id=Column(Integer,ForeignKey('pipeline.id'))
vertex_id=Column(Integer,ForeignKey('vertex.id'))
input=Column(Text,nullable=True)
output=Column(Text,nullable=True)
state=Column(Integer,nullable=False,default=STATE_WAITING) # 用於記錄該節點是否執行任務成功
vertex=relationship("Vertex")
pipeline=relationship("Pipeline")
# 此處用於創建存儲引擎
class DBcreate:
def __init__(self):
# 初始化時創建引擎
self.__engine=None
self.__session=None
self.flag=False
def db_init(self,DB_URL,DATABASE_DEBUG):
if not self.flag:
self.__engine = create_engine(DB_URL, echo=DATABASE_DEBUG)
# 創建會話
self.__session = sessionmaker(bind=self.__engine)()
self.flag = True
return self
@property
def session(self):
if not self.flag:
raise ArithmeticError("Not initialized")
return self.__session
@property
def engine(self):
if not self.flag:
raise ArithmeticError("Not initialized")
return self.__engine
def db_create(self): # 創建表
Base.metadata.create_all(self.__engine)
def db_delete(self): # 刪除表
Base.metadata.drop_all(self.__engine)
db=DBcreate().db_init(URL,DATABASE_DEBUG) # 通過此處的實例化實現了向外創建的目的
db.db_create() # 創建表
if __name__ == "__main__":
pass
運行結果如下
創建數據庫如下
2 service 層處理
1 需求
1 定義DAG,及就是schema定義
2 執行某一個DAG流程
2 問題
DAG 是否允許修改
可以這樣考慮。如果DAG 定義好還未使用,可以進行修改操作,一旦使用過,則不能修改,因此便需要在圖graph表中增加一個字段用於區分是否是執行過的pipeline
3 DAG 定義
#!/usr/bin/poython3.6
#conding:utf-8
from .model import db
from .model import Graph,Vertex,Edge
from .model import Pipeline,Track
# 創建 DAG
def create_graph(name,desc=None): # 此處用於創建圖表,主要需要傳遞的參數是name和desc 描述信息
g=Graph()
g.name=name
g.desc=desc
db.session.add(g)
try:
db.session.commit()
return g
except:
db.session.rollback()
# 創建頂點表
def add_vertex(graph:Graph,name,input=None,script=None):
v=Vertex()
v.graph_id=graph.id
v.name=name
v.script=script
v.input=input
db.session.add(v)
try:
db.session.commit()
return v
except:
db.session.rollback()
# 創建邊表,用存儲邊的數據信息
def add_edge(graph:Graph,tail:Vertex,head:Vertex):
e=Edge()
e.graph_id=graph.id
e.tail=tail.id
e.head=head.id
db.session.add(e)
try:
db.session.commit()
return e
except:
db.session.rollback()
def del_vertex(id): # 通過頂點表的id來刪除相關的信息,需要刪除頂點和頂點對應的邊的信息
query=db.session.query(Vertex).filter(Vertex.id==id)
v=query.first()
if v: # 找到頂點。刪除相關的邊,然後刪除頂點
try:
db.session.query(Edge).filter((Edge.tail==v.id) | Edge.head==v.id).delete() # 刪除對應的邊
query.delete() # 刪除頂點
db.session.commit() # 提交。若失敗,則回滾
except:
db.session.rollback()
return v
使用裝飾器處理上述提交問題,避免繁瑣,創建util 文件,用於處理數據庫數據的提交和回撤問題
#!/usr/bin/poython3.6
#conding:utf-8
from pipeline.model import db
from functools import wraps
def transactional(fn):
@wraps(fn)
def __wapper(*args,**kwargs):
ret=fn(*args,**kwargs)
try:
db.session.commit()
return ret
except:
db.session.rollback()
return __wapper
if __name__ == "__main__":
pass
4 service 層修改結果如下
#!/usr/bin/poython3.6
#conding:utf-8
from .model import db
from .model import Graph,Vertex,Edge
from .model import Pipeline,Track
from util import transactional
# 創建 DAG
@transactional
def create_graph(name,desc=None): # 此處用於創建圖表,主要需要傳遞的參數是name和desc 描述信息
g=Graph()
g.name=name
g.desc=desc
db.session.add(g)
# 創建頂點表
return g
@transactional
def add_vertex(graph:Graph,name,input=None,script=None):
v=Vertex()
v.graph_id=graph.id
v.name=name
v.script=script
v.input=input
db.session.add(v)
return v
# 創建邊表,用存儲邊的數據信息
@transactional
def add_edge(graph:Graph,tail:Vertex,head:Vertex):
e=Edge()
e.graph_id=graph.id
e.tail=tail
e.head=head
db.session.add(e)
return e
def del_vertex(id): # 通過頂點表的id來刪除相關的信息,需要刪除頂點和頂點對應的邊的信息
query=db.session.query(Vertex).filter(Vertex.id==id)
v=query.first()
if v: # 找到頂點。刪除相關的邊,然後刪除頂點
try:
db.session.query(Edge).filter((Edge.tail==v.id) | Edge.head==v.id).delete() # 刪除對應的邊
query.delete() # 刪除頂點
db.session.commit() # 提交。若失敗,則回滾
except:
db.session.rollback()
return v
3 DAG 驗證和測試數據處理
1 測試數據
測試數據函數,暫時放置在service.py中進行處理
```
def test_create_dag():
try:
g=create_graph('test1') # 此處成功返回一個graph對象
# 增加節點
input='''
{
"ip" :{
"type" :"str",
"required" :"true",
"default" : 192.168.1.200
}
}
'''
script={
"script" : "echo test1.A",
'next' : 'B'
}
# 此處可以設置爲爲了用戶方面,next可以設置接收兩種類型,數字表示頂點的id,字符串表示用一個DAG中的該名稱的頂點,其不能重複
a=add_vertex(g,'A',None,json.dumps(script)) # 對數據進行處理
b=add_vertex(g,'B',None,'echo B')
c=add_vertex(g,'C',None,'echo C')
d=add_vertex(g,'D',None,'echo D')
# 增加邊
ab=add_edge(g,a,b)
ac=add_edge(g,a,c)
cb=add_edge(g,c,b)
bd=add_edge(g,b,d)
# 創建環路
g=create_graph('test2') # 環路
# 增加頂點
a = add_vertex(g,'A',None,'echo A')
b = add_vertex(g, 'B', None, 'echo B')
c = add_vertex(g, 'C', None, 'echo C')
d = add_vertex(g, 'D', None, 'echo D')
# 增加邊。abc之間環路
ba=add_edge(g,b,a)
ac=add_edge(g,a,c)
cb=add_edge(g,c,b)
bd=add_edge(g,b,d)
# 創建DAG
g=create_graph('test3') # 多個頂點
# 增加頂點
a = add_vertex(g, 'A', None, 'echo A')
b = add_vertex(g, 'B', None, 'echo B')
c = add_vertex(g, 'C', None, 'echo C')
d = add_vertex(g, 'D', None, 'echo D')
# 增加邊
ba=add_edge(g,b,a)
ac=add_edge(g,a,c)
bc=add_edge(g,b,c)
bd=add_edge(g,b,d)
# 多起點處理方式
g = create_graph('test4') # 多個頂點
# 增加頂點
a = add_vertex(g, 'A', None, 'echo A')
b = add_vertex(g, 'B', None, 'echo B')
c = add_vertex(g, 'C', None, 'echo C')
d = add_vertex(g, 'D', None, 'echo D')
# 增加邊
ab = add_edge(g, a, b)
ac = add_edge(g, a, c)
cb = add_edge(g, c, b)
db = add_edge(g, d, b)
except Exception as e:
print (e)
```
2 執行寫入測試數據
創建app文件
執行結果如下
3 DAG 驗證概述
當增加一個DAG定義後,或者修改了DAG定義後,就需要對DAG 進行驗證,判斷是否是一個DAG圖,如何知道寫入的數據庫的數據是有效的,則需要通過在graph表中增加一個checked字段,用於判斷是否是通過驗證的,在以後的創建流程操作中,若檢測到其字段爲0時,則表示其未通過DAG 驗證
注意:如果有一個流程使用了這個DAG,其將不被允許修改沒了實現這個功能,且不要每一次都查詢一下這個DAG是否被使用,可以在graph表中提供一個字段sealed,一旦設置就不能修改和刪除,表示有人使用了,
在DAG定義後,修改後,就立即進行DAG檢驗,這樣使用的時候就不用每次都檢驗。
4 DAG 設計表結構結果
字段名 | 類型 | 說明 |
---|---|---|
id | int | 主鍵 |
name | varchar | 非空,唯一,圖的名稱 |
desc | varchar | 可爲空,描述信息 |
checked | int | 流程檢驗數據 |
sealed | int | 不可爲空,默認爲0,0表示未使用,1表示已經有執行流程使用了,被封閉不可修改 |
結果如下
class Graph(Base):
__tablename__='graph'
id=Column(Integer,primary_key=True,autoincrement=True)
name=Column(String(48),nullable=False)
desc=Column(String(128),nullable=False)
checked=Column(Integer,nullable=False,default=0)
sealed=Column(Integer,nullable=False,default=0)
# 創建relationship關係圖,用於配置,此處的含義是可以使用此關係來處理查看此pipe下的頂點和邊的信息
vertexs=relationship("Vertex")
edges=relationship("Edge")
重新處理表結構,結果如下
查詢所有入度爲0的頂點
SELECT vertex.* FROM vertex LEFT JOIN edge on vertex.id=edge.head WHERE vertex.graph_id=1 AND edge.head is NULL
採用做鏈接找到edge中的null的方式,找到入度爲0的頂點。
但這種方式找到的不適合進行驗證,因爲第一批入度爲0的頂點找到之後,還需要再次查詢,找到第二批頂點。其可以將所有的頂點,邊都先查詢一遍,然後再客戶端數據庫中進行相關的處理
5 函數創建
在service.py中進行創建此函數
def check_graph(graph:Graph):
query=db.session.query(Vertex).filter(Vertex.graph_id==graph.id)
vertexs=[vertex.id for vertex in query] # 獲取頂點列表
query=db.session.query(Edge).filter(Edge.graph_id==graph.id)
edges=[(edge.tail,edge.head) for edge in query ] # 此處獲取邊的列表
while True:
vis=[] #存放索引,
for i,v in enumerate(vertexs): # 此處需要對頂點表中的每一個頂點和邊表中的head進行匹配
for _,h in edges:
if h==v: # 此處的v表示頂點的id,此處的h表示head及弧頭的數據,若相等,則表示其頂點有弧頭,則表示入度不爲0,此處判斷失敗
break
else: # 此處表示其頂點表中的和邊表中head的沒有匹配的情況,及入度爲0的頂點
ejs=[]
for j,(t,_) in enumerate(edges): # 此處是處理弧尾的情況,
if t==v: # 此處是弧尾和頂點相等的情況,則將其加入到對應的ejs中,弧尾相等。則表示其可以形成邊的關係
ejs.append(j) # 增加其頂點到對應的邊關係中
vis.append(i) # 增加其邊的索引到列表彙總
for j in reversed(ejs): # 刪除列表中的邊對應的索引。及刪除和入度爲0的頂點對應的邊
edges.pop(j)
break
else: # 若遍歷所有都沒有找到入度爲0的頂點,表明其本身就有環
return False
for i in vis:
vertexs.pop(i)
if len(vertexs) + len(edges) ==0: #此處爲0,表示刪除完成,則爲DAG
try:
graph=db.session.query(Graph).filter(Graph.id==graph.id).first()
if graph:
graph.checked=1 # 修改和更新狀態
db.session.add(graph)
db.session.commit()
except Exception as e:
db.session.rollback()
raise e
執行在app中進行。如下
結果如下
4 流程的啓動和inut 驗證和執行器
1 執行引擎
起點
開啓一個流程的時候,需要在界面中選擇一個checke爲1的DAG,選擇一個頂點,頂點的選擇,使用前面的左連接sql語句可以列出當前DAG中入度爲0的節點作爲初始頂點,供用戶選擇一個作爲起點
將起點信息寫入pipeline表,Track表,狀態State都是WAITING
pipeline 表示正在執行的節點
track記錄歷史信息,當前還沒有input 具體值的信息
2 執行引擎代碼如下
def start(graph:Graph,vertex:Vertex,params=None):
# 判斷流程是否存在,且checked爲1的則通過檢驗
g=db.session.query(Graph).filter(Graph.id==graph.id).filter(Graph.checked==1).first()
if not g:
return
v=db.session.query(Vertex).filter((Vertex.id==vertex.id) & (Vertex.graph_id==graph.id)).first()
if not v:
return
# 寫入pipeline 表
p=Pipeline()
p.current=v.id
p.graph_id=g.id
p.state=STATE_WAITING
db.session.add(p)
try:
db.session.commit()
except:
db.session.rollback()
t=Track()
t.pipeline_id=p.id
t.vertex_id=v.id
t.state=STATE_WAITING
db.session.add(t)
try:
db.session.commit()
except:
db.session.rollback()
if g.sealed==0:
g.sealed=1
db.session.add(g)
return p
測試如下
def test_start():
g=Graph()
g.id=1
v=Vertex()
v.id=1
p=start(g,v)
if p:
print (p)
print (p.vertex.script)
test_start()
結果如下
3 input 驗證
開啓一個流程後,起點可能會設置input,這時候就需要有一個界面,讓用戶填寫參數,這是一個交互的過程,也可以實現爲自動填寫參數
提取起點的input參數並進行驗證,驗證通過,將輸入值,保存到字典中,將值存入track表,將字典中的input值交給執行器完成,此處功能在mschedule項目中已經實現,此處不再累贅
4 執行器
使用Input獲得字典,對script字段中的腳本進行替換。若是空字典,就直接執行腳本,啓動線程,使用subprocess的Popen開啓子進程執行,返回的結果保存到track的output中,判斷成功失敗,如果成功,則置狀態爲成功,繼續,若失敗,則置狀態爲失敗,流程停止
### 執行器配置
from subprocess import Popen,PIPE
def execute(script,timeout=None):
proc=Popen(script,shell=True,stdout=PIPE)
code=proc.wait(timeout)
txt=proc.stdout.read()
return code,txt
5 流轉
1 概述
設想一種自動化執行流程,先不考慮input的交互步驟,假設沒有input環節
1 用戶選擇了起點之後,如何開始執行腳本?
如果用戶提交了一個起點後,start函數開始執行,就一直執行到最後一個節點,這個函數才退出,可以,但其存在問題
也就是用戶提交起點後,起點腳本執行是需要時間的,這時候異步執行可能是一個好的方式,開啓線程,專門負責從數據庫的pipeline表中讀取所有WAITING的節點,執行script,獲取返回結果
2 如何流轉
腳本執行成功,需要流轉到下一個節點
2.1 終點
如果本節點的出度爲0,就是終點了,將pipeline,track中的字段置位完成2.2 自動選擇
在腳本json中執行了next,把pipeline中current字段更新爲最新的頂點id,狀態爲等待,track表中的原來的頂點id的狀態修改爲成功,新增一條跟蹤頂點id記錄,狀態是等待。
2 異步執行
此處具體代碼參考如下
#!/usr/bin/poython3.6
#conding:utf-8
from concurrent.futures import ThreadPoolExecutor,as_completed
import random
import threading
def test_fun(s,key):
print ("enter~~~~~~~~{} {}s key={}".format(threading.current_thread(),s,key))
threading.Event().wait(s)
return "ok {}".format(threading.current_thread())
with ThreadPoolExecutor(max_workers=3) as executor:
futures={executor.submit(test_fun,random.randint(1,8),i):i for i in range(7)}
for future in as_completed(futures):
id=futures[future]
try:
print (id,future.result())
except Exception as e:
print (id,'failed')
結果如下
3 流轉代碼如下
from concurrent.futures import ThreadPoolExecutor,as_completed
MAX_POOL_SIZE=5
executor=ThreadPoolExecutor(max_workers=MAX_POOL_SIZE)
def iter_pipelines():# 此處可修改成yield from
query=db.session.query(Pipeline).filter(Pipeline.state==STATE_WAITING)
pipelines= query.all()
for pipeline in pipelines:
yield pipeline
#流轉
def shift():
futures={}
for pipeline in iter_pipelines():
s=json.loads(pipeline.vertex.script) # 腳本的處理
script=s['script'] # 拿到腳本
f=executor.submit(execute,script) # 送入函數和對應的參數
futures[f]=pipeline,s
for f in as_completed(futures):
p,s=futures[f] # 遍歷相關對象
try:
code,txt=f.result()
print (code,txt,p.current)
if code==0: # 基本正常,此處返回爲0表示正常情況
t=db.session.query(Track).filter((Track.pipeline_id==p.id) & (Track.vertex_id==p.current())).one()
t.state=STATE_SUCCEED
t.out=txt
db.session.add(t)
if 'next' in s: # 是否存在下一跳
n=s['next'] # 獲取下一跳
if type(n)==int: # 此處對應的是執行下一個節點id對應的服務
pass
else: # 此處是對應的next爲name的情況
pass
p.current=next # 更新節點數據
p.state=STATE_WAITING # 下一個節點的狀態應該是正常
db.session.add(p)
else: # 此處未進行處理,則需要通過外部點擊的方式完成
p.state=STATE_SUCCEED
db.session.add(p)
else:
pass
except Exception as e:
print (e,'!!!!!!!!!!!!!!')