集群管理工具Salt
简介
系统管理员(SA)通常需要管理和维护数以百计的服务器,如果没有自动化的配置管理和命令执行工具,那么SA的工作将会变得很繁重。例如,要给集群中的每个服务器添加一个系统用户,那么他必须登陆到每台服务器上去逐一的执行命令。好在有编程能力的SA能通过bash + ssh的方式来自动化自己的工作,于是出现了ClusterShell这一类的工具。但这类工具只是解决了远程命令调用的问题,并没有对一些运维的过程进行抽象,在面对不同的操作系统等环境时,使用会变得复杂和繁琐,对常见的运维操作,如包管理、配置文件分发也没有很好的支持。于是出现了CFEngine,Puppet,Chef,Salt等工具,他们在远程命令执行的基础上对一些常见的运维操作进行了抽象,如节点分组、命令编排、状态管理等。
Salt正是其中年轻一员,它是一个使用Python和ZeroMQ开发的开源项目,使用Apache 2.0 License。Salt的主要功能有两个:1)配置管理,使节点处于所定义的状态,如指定web类节点apache包的状态为安装并运行;2)分布式的命令执行系统,一方面分发命令到节点执行,另一方面从节点收集所需的数据。
特点
(翻译自官方介绍http://salt.readthedocs.org/en/latest/topics/index.html)
1)简单
salt能适用于不同规模的部署,安装和维护都很简单。
2)并行执行
- 由master向Minion发出的命令是并行执行的,而不是串行;
- master与Minion之间的使用加密的协议;
- 尽可能的使网络负载降低,提高网络传输效率;
- 提供简单的编程接口;
3)基于成熟的技术
- 使用ZeroMQ进行网络通信;
- 使用AES对通讯进行加密;
- 使用msgpack作为数据序列化的格式;
4)快、灵活、可扩展
架构
Salt主要由三个部分组成:
- salt-master:安装有salt-master的节点我们称作Master节点,它负责存储配置信息、对可信的Minion节点进行授权、通过ZeroMQ与Minion节点进行交互。
- salt-minion:安装有salt-minion的节点称作Minion节点,salt-minion就是一个agent进程,通过ZeroMQ接收来自master的命令,执行并返回结果;
- salt-syndic:在特别庞大的部署环境中才会使用syndic,比如在多数据中心的部署中。syndic相当于一个正向代理节点,它代理了所有Master节点与Minion节点的通信。这样做一方面可以将Master的负载分担给多个syndic承担。另一方面,它也可以降低Master通过广域网访问Minion的成本,提高了安全性,使salt适用于夸数据中心的部署。
快速体验
http://docs.saltstack.com/topics/tutorials/walkthrough.html
体验的步骤如下:
- 为各个节点配置fqdn,并确定各个节点之间能通过fqdn互相访问,其中master节点的fqdn为salt(因为Minion节点启动后会默认向salt注册信息);
- 安装salt,master节点安装salt-master,Minion节点安装salt-Minion;
- 确保防火墙关闭或打开指定的端口,salt-master主要使用4505和4506端口;
- 在master节点使用salt-key对Minion进行认证;
- 尝试运行命令;
核心功能
Targeting(批量操作)
批量操作是指master将选取哪些minion执行命令或同步master指定的状态。salt提供了几种方式来选取执行操作的Minion,它们分别是:
这种方式是基于匹配minion_id,minion_id在默认情况下是minion节点的fqdn。匹配的方式支持正则表达式、通配符、列表,如:
salt ‘*' test.ping //选取所有的minion执行test.ping命令
salt -E 'web1-(prod|devel)' test.ping //选取web1-prod或web1-devel去执行命令
salt -L 'web1,web2' test.ping //选取web1和web2
salt ‘web*' test.ping //选取所有以web为前缀的节点执行
Grains是指minion节点注册时master节点所收集的信息,比如操作系统、CPU架构等。salt也可以基于grains信息来选取执行命令的minion节点,例如:
salt -G 'os:Ubuntu' test.ping //选取所有操作系统是ubuntu的minion节点执行test.ping命令
可以使用salt -N 从配置文件中读取指定的分组执行命令;
可以使用salt -C 来组合使用上面提到的三种匹配策略,例如:
salt -C '* and not G@os:Ubuntu' test.ping //选取所有操作系统不是Ubuntu的节点执行test.ping
通过-b参数从已匹配的节点中再次筛选指定数量的节点执行命令,例如:
salt ‘*’ -b 25% test.ping //从所有节点中选取25%执行命令
Remote Execution(命令编排)
按字面意思理解,这个核心功能可以称作远程调用或者远程执行。但是中文的专业术语会翻译成命令编排,这其中的原因我想是因为“编排”这个词能体现在minion端所执行的命令是逻辑有序的。salt提供了很多功能不同的模块实现了日常的运维工作,这些模块编排了常用的运维指令和逻辑,而master能远程调用他们完成相应的功能。例如,test.ping完成了测试minion节点是否存活,bridge.add 会添加网桥设备。
了解完这个核心功能后,salt的命令格式也变得清晰
salt [TARGETING] [COMMAND] [ARGS]
TARGETING指出了谁去执行命令,COMMAND和ARGS指出了执行什么命令以及命令的参数。其实后面讲到的状态管理也是基于这两个核心功能构建。此外用户还可以自己写模块,对salt的功能进行扩展。
States(状态管理)
状态管理也通常被称作软件配置管理(SCM, Software Configuration Management)。状态管理程序会使让的系统保持或到达预先定义的状态,他会依据状态的描述,安装软件包、打开或重启服务或将配置文件分发到指定的位置并监控它的变化。
拥有状态管理,SA可以很轻松的管理数百台或数万台的服务器配置。将状态管理的配置文件放置在版本管理工具(git, svn)下,能很好的管理配置的变更。
Salt States是Salt的配置管理工具。如上文所述,它本质上是Remote Execution的一个模块。master通过命令编排调用minion上的state模块,触发minion从master获取状态描述文件(SLS文件)并按照描述文件的描述执行相应的操作。
SLS文件可以静态的描述minion的状态,也可以通过Grains和Jinja模板动态的生成状态描述,状态描述之间也存在着不同的关系。可以参照下面的文章深入的学校Salt的状态管理。
http://thinkinside.tk/2013/06/25/salt_usage.html
http://salt.readthedocs.org/en/latest/topics/tutorials/states_pt1.html
http://salt.readthedocs.org/en/latest/ref/states/index.html
其它功能
Salt还提供了很多的扩展功能,如Returners提供了不同的存储后端保存minion返回的数据,salt cloud添加了对AWS等公有云的支持等。
小结
本文简单首先介绍了系统运维的需求和运维工具的基本情况,因为我不是专职的运维人员总结的比较笼统;然后介绍了Salt的架构和核心功能。其实只要在宏观上对Salt有一个基本的认识“它是干什么的,怎么干的”,那么以后无论是使用Salt还是基于Salt做开发都会有思路了,遇到问题查阅官方手册基本都能解决。
要熟悉Salt的使用,动手实践是必不可少的。我还会记录一些在Vagrant上使用Salt的过程,也会尝试使用Salt、vagrant去部署一个OpenStack环境。
相关资源
http://thinkinside.tk/pages/tags.html#salt-ref
http://salt.readthedocs.org/en/latest/ref/states/index.html
目录
背景常见的状态机需求实现状态机一个简单的示例一个相对完善的例子实现代码进一步交代的问题备注
背景返回目录
企业应用下,需要关注三个状态机:
- 业务相关的状态机。
- 审批流程相关的状态机。
- 持久化相关的状态机。
某些企业应用开发人员终其一生就是希望能开发出通用的一个框架以简化这些状态机的开发。本文重点关注:“业务相关的状态机”。
常见的状态机需求返回目录
产品的状态机
单据的状态机
业务相关的状态机的一般性需求如下:
- 当处于某个状态时,可以执行哪些合法的迁移?迁移的前置条件是什么?
- 当处于某个状态时,可以执行哪些合法的操作?如:已提交和已审核状态的单据不能被修改。
实现状态机返回目录
我目前使用过两种思路实现这种状态机:
- 使用状态模式。这种要求为每种单据的状态管理定义一套状态体系,有点麻烦了。
- 使用状态表格。这种就是本文介绍的。
下面先看两个示例。
一个简单的示例返回目录
注意下面的链式配置代码,这些代码表达的意思是:
In(Status.UnSaved).When(Operation.Save).If(CanSave).TransferTo(Status.Saved)
处于 UnSaved 状态下,当 Save 操作发生时,如果 CanSave,就迁移到 Saved 状态。
------------------------------------------------------------------------------------
.In(Status.UnSaved).When(Operation.Edit).Aways().Ok()
处于 UnSaved 状态下,当 Edit 操作发生时,总是,允许的。
代码
1 class Order
2 {
3 private readonly StateMachine<Status, Operation> _stateMachine;
4
5 public Status Status { get; internal set; }
6
7 public Order()
8 {
9 _stateMachine = StateMachine<Status, Operation>
10 .Config(() => this.Status, status => this.Status = status)
11 .In(Status.UnSaved).When(Operation.Save).Aways().TransferTo(Status.Saved)
12 .In(Status.Saved).When(Operation.Submit).Aways().TransferTo(Status.Submitted)
13 .Done();
14 }
15
16 public void Save()
17 {
18 _stateMachine.Schedule(Operation.Save);
19 }
20
21 public void Submit()
22 {
23 _stateMachine.Schedule(Operation.Submit);
24 }
25
26 public void Edit()
27 {
28 _stateMachine.Schedule(Operation.Edit);
29 }
30 }
测试
1 [TestClass]
2 public class StateMachineTest
3 {
4 [TestMethod]
5 public void ValidSave()
6 {
7 var order = new Order { Status = Status.UnSaved };
8 order.Save();
9
10 Assert.AreEqual(Status.Saved, order.Status);
11 }
12
13 [TestMethod]
14 [ExpectedException(typeof(StateScheduleException))]
15 public void InvalidSubmit()
16 {
17 var order = new Order { Status = Status.UnSaved };
18 order.Submit();
19 }
20 }
一个相对完善的例子返回目录
代码
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.ObjectModel;
4 using System.Linq;
5 using System.Text;
6 using System.Threading.Tasks;
7
8 using Happy.Domain;
9 using Happy.StateManager;
10
11 namespace Happy.Examples.ManufactureManagement.Domain.Bases
12 {
13 public abstract class MIAggregateRoot<TItem> : AggregateRoot<Guid>
14 {
15 protected MIAggregateRoot()
16 {
17 this.StateMache =
18 StateMachine<Status, Operation>
19 .Config(() => this.Status, status => this.Status = status)
20 .In(Status.UnSaved).When(Operation.Save).If(CanSave).TransferTo(Status.Saved)
21 .In(Status.UnSaved).When(Operation.Edit).Aways().Ok()
22 .In(Status.Saved).When(Operation.Submit).If(this.CanSubmit).TransferTo(Status.Submitted)
23 .In(Status.Saved).When(Operation.Edit).Aways().Ok()
24 .In(Status.Submitted).When(Operation.Verify).If(this.CanVerify).TransferTo(Status.Verified)
25 .Done();
26
27 // ReSharper disable DoNotCallOverridableMethodsInConstructor
28 this.Items = new Collection<TItem>();
29 // ReSharper restore DoNotCallOverridableMethodsInConstructor
30 }
31
32 protected StateMachine<Status, Operation> StateMache { get; private set; }
33
34 protected internal virtual ICollection<TItem> Items { get; protected set; }
35
36 internal Status Status { get; set; }
37
38 protected virtual bool CanSave()
39 {
40 return true;
41 }
42
43 protected virtual bool CanSubmit()
44 {
45 return true;
46 }
47
48 protected virtual bool CanVerify()
49 {
50 return true;
51 }
52
53 internal void Save()
54 {
55 this.StateMache.Schedule(Operation.Save);
56 }
57
58 internal void Submit()
59 {
60 this.StateMache.Schedule(Operation.Submit);
61 }
62
63 internal void Verify()
64 {
65 this.StateMache.Schedule(Operation.Verify);
66 }
67
68 internal void AddItem(TItem item)
69 {
70 this.AddItems(new List<TItem> { item });
71 }
72
73 internal void AddItems(IEnumerable<TItem> items)
74 {
75 this.StateMache.Schedule(Operation.Edit);
76
77 foreach (var item in items)
78 {
79 this.Items.Add(item);
80 }
81 }
82
83 internal void DeleteItem(TItem item)
84 {
85 this.DeleteItems(new List<TItem> { item });
86 }
87
88 internal void DeleteItems(IEnumerable<TItem> items)
89 {
90 this.StateMache.Schedule(Operation.Edit);
91
92 foreach (var item in items)
93 {
94 this.Items.Remove(item);
95 }
96 }
97 }
98 }
测试
1 using System;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3
4 using Happy.StateManager;
5 using Happy.Examples.ManufactureManagement.Domain.Bases;
6 using Happy.Examples.ManufactureManagement.Domain.QualityTests;
7
8 namespace Happy.Examples.ManufactureManagement.Domain.Test.QualityTests
9 {
10 [TestClass]
11 public class QualityTestTest
12 {
13 [TestMethod]
14 public void TestUnSavedQualityTest()
15 {
16 var entity = this.MockUnQualityTest(Status.UnSaved);
17 entity.AddItem(new QualityTestItem(Guid.NewGuid(), entity.Id));
18 entity.Save();
19
20 Assert.AreEqual(Status.Saved, entity.Status);
21 }
22
23 [TestMethod]
24 public void TestSavedQualityTest()
25 {
26 var entity = this.MockUnQualityTest(Status.Saved);
27 entity.AddItem(new QualityTestItem(Guid.NewGuid(), entity.Id));
28 entity.Submit();
29
30 Assert.AreEqual(Status.Submitted, entity.Status);
31 }
32
33 [TestMethod]
34 [ExpectedException(typeof(StateScheduleException))]
35 public void TestSubmittedQualityTest()
36 {
37 var entity = this.MockUnQualityTest(Status.Submitted);
38 entity.AddItem(new QualityTestItem(Guid.NewGuid(), entity.Id));
39 }
40
41 private QualityTest MockUnQualityTest(Status status)
42 {
43 return new QualityTest
44 {
45 Id = Guid.NewGuid(),
46 Status = status
47 };
48 }
49 }
50 }
实现代码返回目录
代码
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 using Happy.ExtentionMethods;
8 using Happy.StateManager.Configuration;
9
10 namespace Happy.StateManager
11 {
12 /// <summary>
13 /// 状态机。
14 /// </summary>
15 public sealed class StateMachine<TState, TOperation>
16 {
17 private readonly List<Transition<TState, TOperation>> _transitions = new List<Transition<TState, TOperation>>();
18 private readonly Func<TState> _stateGetter;
19 private readonly Action<TState> _stateSetter;
20
21 /// <summary>
22 /// 构造方法。
23 /// </summary>
24 public StateMachine(Func<TState> stateGetter, Action<TState> stateSetter)
25 {
26 stateGetter.MustNotNull("stateGetter");
27 stateSetter.MustNotNull("stateSetter");
28
29 _stateGetter = stateGetter;
30 _stateSetter = stateSetter;
31 }
32
33 /// <summary>
34 /// 配置状态机。
35 /// </summary>
36 public static IConfig<TState, TOperation> Config(Func<TState> stateGetter, Action<TState> stateSetter)
37 {
38 stateGetter.MustNotNull("stateGetter");
39 stateSetter.MustNotNull("stateSetter");
40
41 return new Config<TState, TOperation>(stateGetter, stateSetter);
42 }
43
44 /// <summary>
45 /// 配置状态迁移。
46 /// </summary>
47 public StateMachine<TState, TOperation> ConfigTransition(
48 TState sourceState,
49 TOperation operation,
50 ICondition condition,
51 TState targetState)
52 {
53 sourceState.MustNotNull("sourceState");
54 operation.MustNotNull("operation");
55 condition.MustNotNull("condition");
56 targetState.MustNotNull("targetState");
57
58 var transition = new Transition<TState, TOperation>(
59 sourceState,
60 operation,
61 condition,
62 targetState);
63
64 _transitions.Add(transition);
65
66 return this;
67 }
68
69 /// <summary>
70 /// 使用<paramref name="operation"/>调度状态机。
71 /// </summary>
72 public void Schedule(TOperation operation)
73 {
74 operation.MustNotNull("operation");
75
76 var currentState = _stateGetter();
77 var transition = _transitions
78 .FirstOrDefault(x =>
79 x.SourceState.Equals(currentState)
80 &&
81 x.Operation.Equals(operation)
82 &&
83 x.Condition.IsSatisfied());
84
85 if (transition == null)
86 {
87 throw new StateScheduleException(currentState, operation);
88 }
89
90 _stateSetter(transition.TargetState);
91 }
92 }
93 }
说明
内部就是一个状态表格,没啥交代的,有兴趣的朋友可以去 http://happy.codeplex.com/SourceControl/latest,找到 Happyframework/Src/Happy.StateManager 下载最新代码看看。
进一步交代的问题返回目录
如果需要在真实的项目中使用这个模式,有两个问题还需要解决:
第一个问题:迁移的前置条件判断和后置操作的执行如果需要更多的信息,而这些信息不在实体内,怎么办?处理这个问题有很多种方式,这里介绍一下我目前最偏好的一种,引入领域服务:
领域服务代码
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 using Happy.Examples.ManufactureManagement.Domain.QualityTests;
8 using Happy.Examples.ManufactureManagement.Domain.SmallCuts;
9
10 namespace Happy.Examples.ManufactureManagement.Domain.Services
11 {
12 public sealed class QualityTestManager
13 {
14 private readonly ISmallCutRepository _smallCutRepository;
15
16 public QualityTestManager(ISmallCutRepository smallCutRepository)
17 {
18 _smallCutRepository = smallCutRepository;
19 }
20
21 public void Save(
22 QualityTest qualityTest,
23 IEnumerable<QualityTestItem> addedItems,
24 IEnumerable<QualityTestItem> deletedItems)
25 {
26 qualityTest.AddItems(addedItems);
27 qualityTest.DeleteItems(deletedItems);
28
29 qualityTest.Save();
30
31 this.AcquireLocks(qualityTest, addedItems);
32 this.ReleaseLocks(qualityTest, deletedItems);
33 }
34
35 public void Submit(
36 QualityTest qualityTest,
37 IEnumerable<QualityTestItem> addedItems,
38 IEnumerable<QualityTestItem> deletedItems)
39 {
40 qualityTest.AddItems(addedItems);
41 qualityTest.DeleteItems(deletedItems);
42
43 qualityTest.Submit();
44
45 this.AcquireLocks(qualityTest, addedItems);
46 this.ReleaseLocks(qualityTest, deletedItems);
47 }
48
49 public void Verify(QualityTest qualityTest)
50 {
51 qualityTest.Verify();
52
53 foreach (var item in qualityTest.Items)
54 {
55 var smallCut = _smallCutRepository.Load(item.SmallCutId);
56 smallCut.ReleaseLock(CreateLockInfo(qualityTest));
57 }
58 }
59
60 private void AcquireLocks(QualityTest qualityTest, IEnumerable<QualityTestItem> addedItems)
61 {
62 foreach (var item in addedItems)
63 {
64 var smallCut = _smallCutRepository.Load(item.SmallCutId);
65 smallCut.AcquireLock(CreateLockInfo(qualityTest));
66 }
67 }
68
69 private void ReleaseLocks(QualityTest qualityTest, IEnumerable<QualityTestItem> deletedItems)
70 {
71 foreach (var item in deletedItems)
72 {
73 var smallCut = _smallCutRepository.Load(item.SmallCutId);
74 smallCut.ReleaseLock(CreateLockInfo(qualityTest));
75 }
76 }
77
78 private static LockInfo CreateLockInfo(QualityTest qualityTest)
79 {
80 return new LockInfo(LockType.LockByQualityTest, qualityTest.Id);
81 }
82 }
83 }
第二个问题:之前我只需要在 UI 中控制好这种状态机就行了,如果移动到了领域层,UI 也要重复一遍了,如何消除这种重复,答案是:引入元编程,让 UI 能自动识别这些元数据,最小化重复,这里就不给出实现(还没做)。
备注返回目录
上面状态机的配置过程也很有意思,In后只能是When,When后可以是If或Always,有点类似语法树了,找个机会可以写篇文章(实现是很简单的)。