Geekwolf's Blog

Quick notes


  • 首页

  • 分类

  • 关于

  • tips

  • 归档

  • 标签

  • 站点地图

  • 搜索

SRE最佳实践之On Call?

发表于 2018-08-22 | 分类于 自动化运维 , CI/CD | 阅读次数
阅读时长 15
  • “On-call”言下之意就是”随叫随到,待命”。on-call意味着在一定时间内随叫随到,并做好生产环境出现紧急情况的应对准备。SRE工程师经常被要求要轮值on-call,在on-call期间,SRE会根据需要对紧急情况进行诊断、环境、修复或升级事件;此外,SRE还要定期负责非紧急性生产任务
  • 在Google,On-call是SRE的特点之一。SRE团队可以缓解事故、修复生产问题并自动执行运维任务。由于我们的大多数SRE团队尚未完全自动化所有运维任务,因此升级扔需要人工联系On-call工程师进行处理。根据所支持系统的重要程度或系统所处的开发状态,并非所有SRE团队都可能需要被on-call;根据我们的经验,大多数SRE团队都会进行轮值。
  • On-call是一个大而复杂的话题,承载着许多限制因素和有限的试错率。在《Site Reliability Engineering》一书11章节已讨论过该话题,这里列举下对盖该章节的一些反馈:
  1. “我们不是Google,我们小的多,并且我们没有那么多人进行轮值和没有不同时区的网站,《Site Reliability Engineering》描述的跟我们无关”
  2. “我们有开发和DevOps人员混合进行轮值,如何组织他们有最佳实践吗?划分职责?”
  3. “我们的On-call工程师在典型的24小时轮班中,被呼叫了一百多次。很多页面都被忽略掉了,而真正的问题却被埋没,我们应该从何入手?”
  4. “我们的On-call轮转很快,如何解决团队内部的知识差距?”
  5. “我们想把DevOps团队重组为SRE。SRE On-call、DevOps On-call、开发人员On-call三者之间有什么区别?DevOps团队也非常关心这个问题”

Google为这些情况提供了一些实用建议,下面以Google的两个例子进行说明:

Google: 组建新团队

最初的场景

几年前,在Google总部山景城的SRE sara开始组建新的SRE团队,并在三个月内开始轮值On-call。从这个角度看,Google的大多数SRE团队并不希望新员工在3-9月前准备On-call。新的山景城SRE团队将支持三个Google App服务,这些服务以前是由华盛顿柯克兰(距离到山景城2个小时航班)的SRE团队负责。柯克兰还有一个在伦敦的姊妹团队,将继续和新的山景城的SRE团队以及分布式产品开发团队一起负责支持这些服务。
山景城的SRE团队很快组建,成员结构:

  • Sara SRE团队负责人
  • Mike 其他SRE团队的有经验的SRE
  • 一个从产品开发团队转岗为SRE
  • 4位新雇员工

即使一个成熟的SRE团队,在为新业务服务时也总是具有挑战性的,尤其对于山景城SRE这样年轻的SRE团队来说。尽管如此,新团队能够在不牺牲服务质量和项目进度的情况下提供服务。他们很快对服务进行改进,包括降低40%的机器成本,并且完全自动化灰度发布及其他安全检查。新SRE团队持续提供可靠的服务,目标是达到99.98%的可用性或每季度大约26分钟停机时间

新的SRE团队是如何实现这么多目标的?答案是通过启动项目、指导和培训

培训路线图

虽然新的SRE团队对他们的负责的服务了解不多,但Sara和Mike熟悉Google的生产环境及SRE。当新雇的四位SRE适应公司之后,Sara和Mike整理出了了24个重点领域的清单,在他们正式轮值On-call之前,进行训练,如下:

  • 管理生产任务
  • 理解调试信息
  • 将流量从集群中抽离
  • 回退失败的软件推送
  • 禁止或限速不必要的流量
  • 提高附加的服务能力
  • 使用监控系统(用于报警和大盘)
  • 描述服务业务的体系结构、各种组件和依赖关系

新的SRE通过研究现存的文档和编码实验(指导、动手编程教程)了解到一些信息,并且通过项目上的工作来理解相关的话题。当团队成员了解到与新SRE的启动项目相关的特定话题时,这个人就会进行一个简短临时的会议,与团队的其他成员分享这些信息。Sara和Mike介绍了剩余的话题。该团队还通过举办常见调试、缓解任务的课程,以帮助每个人建立肌肉记忆并让他们对自己的能力充满信心。

另外,除了检查清单外,新的SRE团队还进行一些列的”deep dives”活动来挖掘他们的服务。该团队浏览监控控制台、确定运行的任务并且尝试调试页面。Sara和Mike解释说工程师不需要多年的专业知识,都会对每一项服务变的相当熟悉。他们指导团队从最初的原则中探索服务并且孤立SRE工程师熟悉他们服务、他们对自己认知极限敞开大门,并教会了别人在什么时候寻求帮助。

在整个过程中,新的SRE团队并不孤单。Sara和Mike去拜访其他的SRE团队和产品开发人员并向他们学习。新的SRE团队通过举办视频会议与柯克兰和伦敦的团队会面,发送电子邮件并且通过IRC聊天。此外,该团队还参加每周一次的生产会议,审阅每天的On-call、事后检查以及服务文档。柯克兰的SRE来进行分享和答疑。伦敦的SRE组织了一场完整的灾难场景,并在谷歌的灾难恢复周期内进行。

团队还通过模拟故障进行on-call,练习调试生产问题。在会议期间,鼓励所有的SRES提出关于如何解决模拟的生产故障的建议。在每个人都得到强化之后,团队仍然保持这些会议,团队每个成员轮流担任会议主导者,将这些记录作为未来的参考。

在进行On-call之前,团队回顾了关于职责的指南:

  • 在每个轮值班次开始时,On-call工程师将从上一个班次获取交接内容
  • On-call工程师首先要将用户的影响最小化,然后在确保问题得到彻底解决
  • 轮值结束后,On-call工程师向下一个工程师发送一封交接邮件

指南还规定了什么时候将事件升级到其他人,以及如何撰写大型事故报告。最后,团队阅读并更新On-call手册。手册包含有如何响应自动报警的高级说明。它们解释了报警的严重性和影响,包括调试建议以及可能采取的措施,以减轻影响并彻底解决该问题。在SRE中,每当创建一个告警时,通常会创建相应的说明手册,这些指南减少了MTTR(平均修复时间),以及人为错误的风险。

维护手册

手册内容要根据生产环境的变更进行相应的更新。编写好文档,就像任何形式的交流一样,是一件比较困难的事情,那么如何维护好文档呢?
在Google 一些SRE主张保持文档的一般性,因此更新会比较慢。例如:针对所有的”RPC错误高”的报警,可能只有一条文档说明,供经过培训的On-call工程师结合当前告警服务的架构图来阅读。另外一些SRE提倡循序渐进编写文档,以减少人为不确定性和降低MTTTR。如果团队成员对文档内容有不同看法,手册可能会有多个方向。

这是一个有争议的话题,如果不同意这种做法,至少要和团队一起决定手册具备的最小、结构化的细节,并且关注结构化的细节以外积累的大量信息。将项目中新的、来之不易的生产知识转化为自动化或监控控制台。当特定的告警触发时,On-call工程师每次手动运行文档中确定的命令列表来解决问题,我们建议实现自动化。

两个月之后,Sara、Mike和其他的SRE转移到了即将离任的柯克兰SRE团队。在第三个月的时候,他们成了主On-call,原来柯克兰的SRE成了备On-call。这样的话,如果有需要,Sara他们团队就很快能能够接手柯克兰SRE团队的工作。接下来,新招的四名SRE将变的更加有经验,和柯克兰当地的SRE一起轮值On-call。

好的文档积累和前面讨论的各种策略都能帮助团队打牢根基,迅速成长。尽管On-call可能会带来压力,但是团队会有足够的信心,在不抱有怀疑自己的情况下采取行动。当团队成员意识到他们反应是基于团队集体的智慧,就会很有安全感,即使他们升级了,那些On-call工程师仍然被认为是称职的工程师。

后记

当山景城的SRE团队快速成长时,他们了解到在伦敦有经验的姊妹SRE团队将会加入到一个新项目。在苏黎世组建的新团队将去支持伦敦SRE团队先前负责的服务。对于第二次过度中,山景城SRE团队使用的基本方法被证明是成功的。山景城SRE团队先前在培训上的投入用来帮助苏黎世SRE团队快速发展。
当一组SRE成为团队时,山景城SREs所使用的方法就是有意义的。当只有一个人加入团队时,他们需要一种更轻量级的方法。SREs创建了服务器架构图,并将基本的培训清单规范为一系列的练习,这些练习可以在半独立的情况下完成,并且很少需要导师参与。这些练习包括存储层、扩容以及检查HTTP请求的路由方式等

Evernote: 在云端站稳脚跟

迁移基础设施到云端

我们并没有因此重新设计On-call流程。在2016年12月以前,Evernote的所有应用程序都运行在本地传统数据中心。我们的网络和服务器都是根据特定的体系结构和数据流进行设计的,这些约束导致我们缺乏水平扩展架构的灵活性。谷歌云平台(GCP)针对我们的问题提供了解决方案。然而,我们仍然要克服最主要的障碍:将生产环境和支持的基础设置迁移到GCP。经过70天的努力,取得了非凡的成绩(例如:迁移了数千台服务器和3.5PB的数据)。接着,我们该如何监控、告警以及最重要的是在新环境中出现问题该如何解决?

调整我们的On-call制度和流程

向云端迁移释放了我们基础设施快速增长的潜力,但我们的On-call制度和流程还未适应这种快速增长。在我们以前物理数据中心中,每个组件都有冗余,组件失败是一件常事,但不会对用户产生直接影响,基础设施是稳定的,因为我们可控。我们的告警策略是基于这样构建的:一些丢包会导致JDBC连接异常,这意味着虚拟机可能即将发生故障或者我们的一个交换机上有故障。甚至在我们进入云第一天之前就意识到这种类型的告警在未来可能是无效的,我们需要采取更加全面的监控方法。
根据第一性原则重新构建分页事件,并编写这些规则作为我们的SLOs(服务质量目标),帮助团队清晰的认识什么才是重要的告警。我们关注的是更高级别的指标,比如API响应,而不是像InnoDB行级锁等待细节,意味着我们更多时间集中在停机期间是否对用户体验造成影响上。对我们团队来说,将有更少的时间花费在短暂的琐事上,这就变的更加有效、更多的休息,最终提升工作满意度。

重构监控和度量

我们的主On-call轮值是由一个小而精力旺盛的工程师团队,他们负责生产基础设施和一些其他业务系统(如:构建基础设施的pipeline等)。每周都有一个24*7的日程表,都有准备好的交接程序,并在每天早上站会上回顾日常事件。我们团队规模小,责任大,因此我们尽一切努力降低流程负担,并专注于尽快结束告警、分流、修复和分析。我们事先这一目标的方法之一就是通过维护简单有效的报警SLA(服务等级协议)来保持信噪比。我们通过度量或监控生成的事件分为三个类别:

P1:立即处理
  • 应立即采取措施
  • 打开On-call页面
  • 区分事件分类
  • 是SLO告警
P2:下一个工作日处理
  • 一般来说,不是面向用户或影响有限
  • 给团队发送邮件并通知该事件
P3:信息事件
  • 信息被搜集展示在仪表盘或通过邮件发送
  • 包括容量规划相关信息

P1和P2事件都附有事故工单,工单用于诸如事件分类、状态跟踪、以及SLO影响、发生次数和事后文档连接。当事件是P1时,On-call工程师根据对用户的影响程度分为三个等级(1-3)。对于严重性1(Sev 1)事件,我们维护了一组有限的标准,使升级决策尽可能简单。一旦事件升级,我们召集一个事故小组,开始对事故进行管理。事件解决后,我们进行自动检测,并将结果在内部进行分享。针对Sev2或Sev3级别的事件,On-call工程师来管理事件的生命周期,包括简单的事后分析和回顾。这就授权并鼓励在完成事件检查智慧,立即采取后续行动,并确定工具或流程中的漏洞。通过这种方式,每次On-call的轮值都能达到持续改进和灵活使用的良性循环,与快速变化的环境保持同步。我们的目标就是让每个On-call的工程师都比上一个做的更好。

跟踪性能

随着SLOs的引入,我们希望随着时间的推移跟踪性能的变化,并与公司相关团队分享这些数据。我们实施了一个月的服务审查会议,对任何感兴趣的人开放,来审查和讨论前一个月服务的运行情况。我们还利用这样的会议来审查On-call的压力,将其作为团队健康状况的晴雨表,并在超出预算时讨论补救措施。这样的会议有双重目的,即在公司内部传播SLOs的重要性,并使技术组织对我们维护的服务和团队负责。

保证CRE(Customer Reliability Engineering)

我们在SLO方面的目标是与Google客户可靠工程团队合作的基础,在我们与CRE讨论SLOs是否是真实可测量的;两个团队都决定CRE与我们自己的工程师一起为SLO进行On-call。有时候可能很难找出隐藏在云抽象层背后的根因,所以google工程师把黑盒事件进行分类是有帮助的,更重要的是,这项工作进一步降低了MTTR,这对用户来讲至关重要。

维持一个自我实现的循环

我们现在有更多的时间来考虑我们如何推动业务发展,而不是将所有时间都花在分类/根因分析/事后调整周期上。具体而言,这可以转化为诸如改进我们的微服务平台和为产品开发团队建立敏捷标准;后者包括我们在重组On-call时遵循的许多原则,这对于团队第一次On-call特别有用。因此,我们将改善所有人On-call循环延续下去,持续改进。

欢迎关注“运维ABC”(AI、BigData、Cloud),运维技术社区,专注运维自动化、DevOps、AIOPS、ChatOPS、容器等落地与实践

image

一篇文章彻底读懂DevOps与SRE来龙去脉(译)?

发表于 2018-08-01 | 分类于 自动化运维 , CI/CD | 阅读次数
阅读时长 14

image

  • 若是把运维当作一门学科来看,是有难度的.不仅因为如何很好的运行系统这种普遍问题未得到解决外,现存的最佳实战也因高度依赖环境,而未得到广泛使用;另外一个未解决的问题就是如何更好的管理运维团队。详细分析这些问题通常被认为起源于致力运筹学的研究,在第二次世界大战期间用于改善盟军的进程和产出,但事实上,几千年来,我们一直在思考如何更好的运营
  • 然而,尽管有这么多的努力和想法,可靠的生产运维仍然难以保障,特别是在信息技术和软件可操作性领域
    例如:以企业的角度,通常将运维视为成本中心;如果可能,要做有意义的改进即使是困难的.因对这种方法的短视,尚未得到广泛理解,但是对它的不满已经引发了如何组织我们在IT中所有事情的一场革命,这场革命源于试图解决一系列共同问题,最新解决这些问题的方案有两个:DevOps和SRE(Site Reli‐ability Engineering)。事实上,它们有很多相似之处,要比我们想象的多

DevOps产生的背景

image

image

DevOps是一套松散的实践、指南和文化,旨在打破IT开发、运维、网络和安全方面的孤岛。由JohnWillis,Damon Edwards和Jez Humble联合执笔,阐述:CA(L)MS-代表文化、自动化、精益(如精益管理,持续交付)、度量、分享,是DevOps关键思想的缩写。分享和协作是这场运动的最前线,在DevOps方法中,改进(通常通过自动化方式)、然后度量结果,并与同事分享这些成果,这样整个组织都可以得到改进。所有CALMS原则都是有这种支持性文化促成的
DevOps、敏捷以及各种其他商业和软件工程技术都是关于如何在现代商业中最好的做生意的普遍世界观的例子。DevOps思想中的任何元素都不容易彼此分离,这基本上是通过设计来实现的。然而,一些关键的想法可以相对独立的进行讨论

不再孤岛(谷仓效应)

第一个关键思想:不再有孤岛,针对这一思想有两个方面的反应:

  • 历史上流行但现在越来越老式的独立运维和开发团队
  • 事实上,知识的极端孤立,对纯粹的局部优化的激励,以及缺乏协作在很多情况下对于企业来说都是非常糟糕的
事故是正常的

第二个关键思想: 事故不仅仅是个人孤立行为的结果,而是因为当事情不可避免地出错时缺少保障措施。例如:一个糟糕的界面在压力环境下会促使采取错误操作。如果发生(未明确的)错误情况,系统错误会使失败不可避免;坏的监控使我们无法知道是有问题,更不用说出了什么问题。一切传统观念的企业具有根除犯错制造者和处罚他们的文化本能,但这样做有其自身的后果:最明显的是,它们创造了是问题混淆、掩盖真相、责怪他人的动机,所有这些最终都是无益的分心行为。因此,着眼于加速恢复故障比预防事故更有意义

变更应该循序渐进

第三个关键思想: 小而频繁的变更时最佳的。在变更委员会每月开会讨论彻底修改大型机配置的计划,这是一个激进的做法。然而这种做法并不鲜见,所有变更必须由经验丰富的人员考虑并且为了有效考虑而进行批量化的观点,结果或多或少与最佳实践相悖。变更是有风险的,没错,但是正确的做法是将变更尽可能拆分成晓得组件或单元。然后,根据产品、设计和基础设施的变更,建立一个稳定的低风险变更管道。这种策略,增加对小变更的自动化测试和可靠地回滚有问题的变更,就形成了变更管理的方法:持续集成(CI)和持续交付或部署(CD)

工具和文化是相互关联的

工具和文化是DevOps的重要组成部分,特别是在强调正确管理变更的今天,变更管理依赖于高度特定的工具。然而,DevOps支持者强烈强调组织文化而不是工具本身,作为新工作方式成功的关键。一个好的文化可以解决围绕破碎的工具工作,但相反的情况很少适用。俗话说的好,文化将策略当早餐吃了(意味着文化的影响力远胜过策略),像运维,改变其自身是很难的事

度量至关重要

最后,度量在总体业务环节中尤其重要,例如打破孤岛和事件解决方案。在每个环境中,通过客观的测量来确定正在发生的事情的真实性,验证是否按预期进行了改变。并为不同职能部门达成一致建立客观基础(适用于业务和其他环境,例如on-call)

SRE产生的背景

网站可靠性工程师(SRE)是Google工程副总裁BenTreynor Sloss创建的术语(和相关的工作角色)。正如我们在前一节中所讲,DevOps是关于运维和产品开发之间的全周期协作的一系列广泛原则。SRE是一个工作角色,一组实践。如果DevOps是一种哲学和一种工作方法,那么SRE实现了DevOps所描述的一些思想,而且更接近于工作或角色的定义,比如”DevOps工程师”。因此,从某种程度上来说,SRE是DevOps的实现。
与DevOps运动不同,DevOps运动起源于多家公司的领导者和实践者之间的合作产生的,在SRE广泛普及之前,Google的SRE继承了周围公司的大部分文化,并没有像DevOps一样突出文化的变化

SRE有以下具体原则

image

运维是一个软件问题

SRE的基本原则是做好运维是一个软件问题。因此,SRE应该使用软件工程思想来解决该问题。这是一个广泛的领域,包括从流程和业务变化到类似复杂但更传统的软件问题,例如重写堆栈以消除业务逻辑中的单点故障。

通过SLOs(服务质量目标)进行管理

SRE不会试图提供100%的可用性,正如我们第一本书《Site Reliability Engineering》(网站可靠性工程)中所讨论的,这是个错误的目标,原因有很多。相反,产品团队和SRE团队为服务及其用户群选择适当的可用性目标,并将服务管理到该SLO。决定这样的目标需要业务部门强有力的合作。SLOs也有文化内涵:作为利益相关者之间的协作决定,SLO违规行为将团队无可指责的重新回到绘图板。

减少琐事

对于SRE来说,任何手动、重复性的的运维任务都是让人厌恶的。我们相信,如果一台机器可以执行期望的运维操作,我们就应该经常这样做。这种差别和价值在其他组织中并不常见,一些琐事就需要人力才能完成。对于在Google的SRE来说,琐事并不能作为工作来做。任何花费在操作任务上的时间意味着我们并没有真正的在为项目工作——我们如何使服务更可靠和可伸缩
a
然而,”生产智慧”为我们执行运维任务提供了非常重要的帮助。这种工作,可以通过指定系统的实时反馈来落地。需要甄别琐事的来源以便可以最小化这些工作甚至消除。但是,如果发现自己操作状态不佳,则可能需要更频繁的推送新功能和变更,以便其他工程师熟悉你所支持的服务

  • 生产智慧
    关于”生产智慧”阐释: 使用这个词,意思是我们从运行的生产环境中获取到的智慧——关于它实际上是如何工作的、软件应该如何设计的细节而不是与实际相孤立的服务。获得所有事件及团队获工单等等都与现实直接相关,可以更好为系统设计和行为提供信息
工作自动化

这个领域的真正工作是决定什么条件下做什么自动化以及怎么自动化。
SRE在Google实践中:团队成员花费在琐事上而不是产生持久价值工程的时间为限制在50%。许多人把这个认为限制的上限。实际上,针对采用工程方法来解决问题,视为一种明确的声明和机制,要比一遍一遍的做琐事更加有用的多。

当我们考虑自动化和琐事时,基线和其如何发挥作用并不直观。随着时间的推移,一个SRE团队会将所有可以自动化的服务都自动化,剩下的都是无法实现自动化的(Murphy-Beyer效应)。这将主导SRE团队的工作,除非有其他要做。在Google环境中,你倾向添加更多的服务,直到达到某些限制,仍然有50%工程时间,或者你在自动化方面非常成功你可以去做一些其他完全不同的事情

通过降低失败成本来快速流转

SRE的主要优点之一就是不一定要提高可靠性,即使它已经发生,但实际上改进了产品开发的产出。为什么?降低常见故障的平均故障时间(MTTR)会增加产品开发人员的迭代速度,因为工程师不用将精力过多分散在处理故障问题上。这源于一个众所周知的事实,即在产品生命周期的后期,一个问题被修复的代价越高。SREs专门负责改善处理产品后期出现的问题,为整个公司带来收益。

与开发人员分享所有权

“应用程序开发”和”生产”之间的严格界限(有时被称为Dev和Ops)会产生相反的效果。将责任分工、运维分类作为成本中心,则会导致权力失衡或薪酬差异这一点尤为正确

SREs倾向于关注生产问题而不是业务逻辑问题,但是随着他们用软件工程工具的方法来解决问题以及与产品开发团队分享技能。一般而言,SRE在他们关注的服务可用性、延迟、性能、效率、变更管理、监控、紧急响应和容量规划方面具有特殊的专业知识。这些特殊技能(通常明确定义的)是SRE为产品和开发团队技术服务的基础。理想情况下,产品开发和SRE团队对技术栈有一个整体的了解——前端、后端、库、存储、内核和物理机——任何团队都不应该嫉妒拥有单一组件

在《Site Reliability Engineering》(网站可靠性工程)一书中,我们并没有明确指出:在Google,产品开发团队就默认拥有他们的服务,虽然SRE原则仍然告知整个Google如何管理服务,但是SRE原则既不可用也不保证。SRE团队的所有权模式与产品开发团队合作最终也是一个共享模型

使用相同的工具,无论功能或职位

工具是一个非常重要的行为决定因素。在Google的环境中,SRE如果没有统一的代码库、软件和系统的各种工具、高度优化和专有的生产堆栈等是非常天真的。我们与DevOps分享这个绝对假设:负责服务的团队应该使用相同的服务工具,不管它们在组织中的角色是什么。如果没有好的方法去管理服务,一个工具给SREs使用,另外一个工具给产品开发者使用,在不同的情况下,操作不同,可能会是灾难性的。彼此分歧越多,公司从改进每个工具的努力中获益就越少

DevOps与SRE对比

image

从上面聊的原则中,我们可以看到它们之间有很多共性:

  • DevOps和SRE都取决于为了持续改进,必须接受变化
  • 协作是DevOps工作的前沿和中心,有效共享所有权模式和合作伙伴关系是SRE发挥作用的必要条件。与DevOps一样,SRE也具有跨组织共享的强大价值,这样更容易打破团队之间的壁垒
  • 变更的最佳实践是: 持续小而频繁的变更,大部分情况下,都需要自动化测试和应用。关键是变更对可用性影响对于SRE来说尤为重要
  • 使用正确的工具至关重要,工具在一定程度上决定了行动范围。然而,我们不能过于关注使用某种特定工具来实现一些操作。面向系统管理的API是一个更重要方法
  • 度量对于DevOps和SRE来说都至关重要。对于SRE来说,SLOs(服务质量目标)决定着是否改善优化服务。当然,如果没有衡量标准(以及在产品、基础设施/SRE和业务之间的跨团队合作),就不会有SLO。对于DevOps来说,度量行为常用来了解过程的产出、一个反馈周期持续的时间等等。无论从专业角度还是从哲学角度,DevOps和SRE都是面向数据的
  • 管理生产服务器残酷的事实就是偶尔发发生了故障,并且你要说出为什么。SRE和DevOps都是无可指责的,目的是为了消除无意义的争论
  • 最终,实施DevOps或SRE是一种整体行为,两者都希望使用一种特定的工作方式共同协作,促使整个团队运营的更好。对于DevOps和SRE来说,更好的速度就是产出。

正如你说看到的,DevOps和SRE有很多共同点。然而,也存在着显著的差异,DevOps在某种意义上是一种更广泛的哲学和文化。DevOps对于如何在一个具体层面上运行相对沉默,例如,它并没有明确规定如何对服务进行精细化管理,而更多的专注如何打破更广泛的组织中的障碍。这就很有价值。

另一方面,SRE的指责范围相对狭窄,其职权范围通常是面向服务的(以终端用户为导向),而非整体业务。因此,它解决如何高效运行系统有自己的知识框架(包括错误预算等概念)。尽管SRE作为一种职业,高度关注激励错误和效率,但在诸如“组织孤岛”和“信息壁垒”等话题上,却相对沉默。它将支持CI/CD,不一定是因为业务需要,而是改进操作的实践。或者,换句话说,SRE相信和DevOps相同的东西,但原因可能有所不同

译自 How SRE Relates to DevOps(class SRE implements interface DevOps)

欢迎关注“运维ABC”(AI、BigData、Cloud),运维技术社区,专注运维自动化、DevOps、AIOPS、ChatOPS、容器等落地与实践

image

兄台了解下:图文告警邮件?

发表于 2018-07-16 | 分类于 Linux运维 | 阅读次数
阅读时长 1

目标

告警时将告警信息及当前一段时间趋势图发送邮件或word文档

要领

  1. 增加Email报警介质
  2. 配置Zabbix Actions,并规范报警信息格式
  3. 获取告警信息
  4. 得到itemid,获取一段时间趋势图
  5. 配置发送邮箱
  6. 发送邮件:图文
  7. 在config.ini中定义生成报表的主机及graphid
  8. 执行脚本,生成word报表

    程序逻辑图

    image
    代码参考:
    zabbix_report_email

邮件图文报警

  1. 配置Email报警介质

注意:如果脚本不能正常运行,请检查权限
image

  1. 配置Actions
    注意:Default subject格式:{ITEM.ID}|Ploblem|{TRIGGER.NAME}
    image

  2. 修改触发器,进行报警测试

报表

  1. 添加要报表的主机及graphid
    修改config.ini中的info
  2. 生成报表

    1
    python report.py report
  3. 说明

报表功能比较简单,可以根据自身情况,修改代码,进行定制

脚本打包二进制

1
2
3
4
5
pip install pyinstaller
pyinstaller -F report.py
在当前目录会生成dist/report 二进制文件
cp default.docx dist/default.docx
cp config.ini dist/config.ini

最后,拷贝dist下的文件就可以使用了,不需在安装依赖

运维故障管理的思考

发表于 2018-07-03 | 分类于 Linux运维 , 自动化运维 | 阅读次数
阅读时长 3

为什么要做故障管理

故障一般是指生产环境出现服务不可用、不稳定、服务性能降低等事件导致用户或玩家体验变差或功能不可用的问题;墨菲定律告诉我们:

  1. 任何事都没有表面看起来那么简单
  2. 所有的事都会比你预计的时间长
  3. 会出错的事总会出错
  4. 如何你担心某种情况发生,那么它就更有可能发生

无论故障发生的概率有多小,只要有出现的可能,它总会发生;同样海恩法则也警告我们:每一起严重事故的背后,必然有29次轻微事故和300次未遂先兆,以及1000个事故隐患,也就是说任何严重事故背后都是有很多次小问题的积累,当达到一定量级时就会导致质变,出现严重的问题;所以为了保证SLA,提前发现、准确定位、避免二次出现故障,解决责任界限不清晰,主导改进不明确等问题,甚至故障自愈,减少对项目的影响,我们需要一个规范可遵循的故障管理原则

故障管理目标

  • 减少故障,提升故障处理效率
  • 增强线上产品稳定性,提升SLA
  • 运维问题总结,作为知识库
  • 完善故障问题的检测监控
  • 为故障自愈提供依据

故障定级标准

为了衡量影响范围及影响程度,与PM、产品、开发共同确定统一的判断标准,避免后期复盘故障出现推卸责任及无所谓的问题。故障等级一般会根据MTBF(平均故障间隔时间,越长表示可靠性越高)、MTTR(平均恢复时间,越短表示影响越小)、MTTF(平均失效时间,系统平均正常运行多长时间,发生一次故障;可靠性越高,平均无故障时间越长)等作为衡量标准。根据我们游戏运营情况,按照影响玩家数量及故障时间来进行故障的定级:

  • S开头表示影响玩家的故障
  • T是指不影响玩家的故障
  • 1、2、3严重程由大到小
    image

故障管理流程

image

  1. 通过玩家反馈、监控告警以及计划内变更(如停服版本更新等),确认故障后,通知项目质量保障群
  2. 运维初步了解判断故障现象、范围及原因,通知开发、DBA等是否介入
  3. 根据故障影响确认处理优先级
  4. 定位、处理故障
  5. 故障恢复后,若重大故障,开发、运维、DBA等分析复盘故障
  6. 改进方案、是否需要完善监控、应急措施
  7. FMS故障管理系统记录故障:故障处理过程、改进措施等

故障分析报告模板:

image

故障自愈

针对未知故障,抽象检测脚本,在遇到二次故障告警时,通过Zabbix远程执行相关处理逻辑;可以参考蓝鲸的做法,将自愈作为套餐去消费

FMS故障管理系统

功能模块

根据上述故障管理思路,开发了FMS故障管理系统,功能点如下图:
image

裸照

image
image
image
image
image

FMS项目

https://github.com/geekwolf/fms.git
有什么好的建议欢迎提issue~

使用Filebeat和Logstash集中归档游戏日志

发表于 2018-01-13 | 阅读次数
阅读时长 7

背景说明

由于游戏项目日志目前不够规范,不太容易根据字段结构化数据,开发又有实时查看生产和测试环境服务运行日志需求;如果写入ES通过Kibana查看,对于非分析类查看还是不太友好,当然也可以通过LogTrail插件

方 案

  • Filebeat->Logstash->Files
  • Filebeat->Redis->Logstash->Files
  • Nxlog(Rsyslog、Logstash)->Kafka->Flink(Logstash->ES-Kibana)
  • 其他方案(可根据自己需求,选择合适的架构,作者选择了第二种方案)

注释: 由于Logstash无法处理输出到文件乱序的问题,可通过不同的文件使用不同的Logstash,单线程归档;或者直接写入ES(基于@timestamp)、通过Flink输出到文件

部 署

系统环境
  • Debian8 x64
  • logstash-6.1.1
  • filebeat-6.1.1-amd64
  • Redis-3.2
Filebeat配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/etc/filebeat/filebeat.yml
filebeat.prospectors:
- type: log
paths:
- /home/data/log/*
- /home/data/*.log
scan_frequency: 20s
encoding: utf-8
tail_files: true
harvester_buffer_size: 5485760
fields:
ip_address: 192.168.2.2
env: qa
output.redis:
hosts: ["192.168.1.1:6379"]
password: "geekwolf"
key: "filebeat"
db: 0
timeout: 5
max_retires: 3
worker: 2
bulk_max_size: 4096
Logstash配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
input {
#Filebeat
# beats {
# port => 5044
# }
#Redis
redis {
batch_count => 4096
data_type => "list"
key => "filebeat"
host => "127.0.0.1"
port => 5044
password => "geekwolf"
db => 0
threads => 2
}
}
filter {
ruby {
code => 'event.set("filename",event.get("source").split("/")[-1])'
}
}
output {
if [filename] =~ "nohup" {
file {
path => "/data/logs/%{[fields][env]}/%{+YYYY-MM-dd}/%{[fields][ip_address]}/%{filename}"
flush_interval => 3
codec => line { format => "%{message}"}
}
} else {
file {
path => "/data/logs/%{[fields][env]}/%{+YYYY-MM-dd}/%{[fields][ip_address]}/logs/%{filename}"
flush_interval => 3
codec => line { format => "%{message}"}
}
}
#stdout { codec => rubydebug }
}

生产日志目录

1
2
3
4
5
6
7
8
9
10
11
12
.
├── prod
│   └── 2018-01-13
│   └── 2.2.2.2
│   ├── logs
│   │   ├── rpg_slow_db_.27075
│   └── nohup_service.log
└── qa
├── 2018-01-12
│   ├── 192.168.3.1
└── 2018-01-13
├── 192.168.3.2

总结

笔者在测试Logstash单线程输出时,依然产生乱序问题(有知晓的可以留言),最终选择通过自己开发的daemon程序实现,参考Plogstash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# -*- coding: utf-8 -*-
# @Author: Geekwolf
# @Date: 2018-01-29 14:23:04
# @Last Modified by: Geekwolf
# @Last Modified time: 2018-01-31 10:55:01
#!/usr/bin/env python3
# daemon.py
import os
import sys
import time
import redis
import json
import re
import atexit
import signal
# import collections
class Base(object):
def __init__(self, *args, **kwargs):
self.pidfile = '/var/run/plogstash.pid'
self.service_name = 'Plogstash'
self.path = '/var/log/plogstash'
os.makedirs(self.path, exist_ok=True)
self.logfile = '%s/%s.log' % (self.path, self.service_name)
self.redis_host = '127.0.0.1'
self.redis_password = 'geekwolf'
self.redis_port = 5044
self.redis_db = 0
self.redis_key = 'filebeat'
self.batch_size = 5000
self.expires = 5 # second
self.archive_time = 1 # how long time to archive
self.base_dir = '/data/logs'
# self._tmp = '/tmp/.%s' % self.service_name
class Daemon(Base):
def __init__(self, *args, **kwargs):
super(Daemon, self).__init__(*args, **kwargs)
def daemonize(self):
# First fork (detaches from parent)
try:
if os.fork() > 0:
raise SystemExit(0) # Parent exit
except OSError as e:
raise RuntimeError('fork #1 failed.')
os.chdir('/')
# set this will 777
# os.umask(0)
os.setsid()
# Second fork (relinquish session leadership)
try:
if os.fork() > 0:
raise SystemExit(0)
except OSError as e:
raise RuntimeError('fork #2 failed.')
# Flush I/O buffers
sys.stdout.flush()
sys.stderr.flush()
# Replace file descriptors for stdin, stdout, and stderr
with open(self.logfile, 'ab', 0) as f:
os.dup2(f.fileno(), sys.stdout.fileno())
with open(self.logfile, 'ab', 0) as f:
os.dup2(f.fileno(), sys.stderr.fileno())
with open(self.logfile, 'rb', 0) as f:
os.dup2(f.fileno(), sys.stdin.fileno())
# Write the PID file
print(os.getpid())
with open(self.pidfile, 'w') as f:
print(os.getpid(), file=f)
# Arrange to have the PID file removed on exit/signal
atexit.register(lambda: os.remove(self.pidfile))
# Signal handler for termination (required)
def sigterm_handler(signo, frame):
raise SystemExit(1)
signal.signal(signal.SIGTERM, sigterm_handler)
def get_now_date(self):
return time.strftime('%Y-%m-%d', time.localtime(time.time()))
def get_now_timestamp(self):
return time.time()
def get_now_time(self):
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
def logging(self, msg):
with open(self.logfile) as f:
print('%s %s' % (self.get_now_time(), msg))
def append_log(self):
pass
def start(self):
if os.path.exists(self.pidfile):
raise RuntimeError('Already running')
else:
try:
self.daemonize()
self.append_log()
self.status()
except RuntimeError as e:
print(e, file=sys.stderr)
raise SystemExit(1)
def stop(self):
# f = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK)
# ret = os.read(f, 1024).decode('utf-8')
# print(ret.split('\n'))
# os.close(f)
if os.path.exists(self.pidfile):
# with open(self._tmp) as f:
# _data = f.read()
# if _data is not None and len(eval(_data)) > 0:
# for k, v in eval(_data).items():
# v = v['fd'].rstrip('\n')
# v.close()
with open(self.pidfile) as f:
os.kill(int(f.read()), signal.SIGTERM)
print('Plogstash is stopped')
else:
print('Not running', file=sys.stderr)
raise SystemExit(1)
def restart(self):
self.stop()
self.start()
def status(self):
try:
with open(self.pidfile, 'r') as f:
pid = int(f.read().strip())
except:
pid = None
if pid:
print('%s is running as pid:%s' % (self.service_name, pid))
else:
print('%s is not running' % self.service_name)
class Worker(Daemon):
def __init__(self, *args, **kwargs):
super(Worker, self).__init__(self, *args, **kwargs)
def _redis(self):
pool = redis.ConnectionPool(host=self.redis_host, password=self.redis_password, port=self.redis_port, db=self.redis_db, socket_timeout=10000)
rc = redis.StrictRedis(connection_pool=pool)
return rc
def get_redis_data(self):
_data = self._redis().lrange(self.redis_key, 0, self.batch_size - 1)
# 删除数据(可考虑处理完再删除)
return _data
def del_redis_data(self):
_data = self._redis().ltrim(self.redis_key, self.batch_size, -1)
def append_log(self):
file_meta = {}
# file_handler = collections.defaultdict(dict)
# try:
# os.mkfifo(self.pipe_path)
# except Exception as e:
# print(str(e))
# pipe_ins = os.open(self.pipe_path, os.O_SYNC | os.O_CREAT | os.O_RDWR)
while True:
time.sleep(self.archive_time)
_data = self.get_redis_data()
if _data:
for _d in _data:
try:
_d = json.loads(_d.decode('utf-8'))
_path = '%s/%s/%s/%s' % (self.base_dir, _d['fields']['env'], self.get_now_date(), _d['fields']['ip_address'])
os.makedirs(_path + '/logs', exist_ok=True)
file_name = _d['source'].split('/')[-1]
# _path = '%s/%s/%s/%s' % (self.base_dir, _d['fields']['env'],self.get_now_date(), _d['fields']['ip_address'])
if re.match('nohup', file_name):
file_path = '%s/%s' % (_path, file_name)
else:
file_path = '%s/logs/%s' % (_path, file_name)
with open(file_path, 'a') as f:
f.write(_d['message'] + '\n')
# if 'fd' not in file_handler[file_path]:
# f = open(file_path, 'a', buffering=1024000)
# file_handler[file_path]['fd'] = str(f)
# file_handler[file_path]['time'] = self.get_now_timestamp()
except Exception as e:
self.logging(str(e))
self.del_redis_data()
# with open(self._tmp, 'w') as f:
# f.write(json.dumps(file_handler))
if __name__ == '__main__':
if len(sys.argv) != 2:
print('Usage: {} [start|stop|restart|status]'.format(sys.argv[0]), file=sys.stderr)
raise SystemExit(1)
daemon = Worker()
if sys.argv[1] == 'start':
daemon.start()
elif sys.argv[1] == 'stop':
daemon.stop()
elif sys.argv[1] == 'restart':
print("Restart ...")
daemon.restart()
elif sys.argv[1] == 'status':
daemon.status()
else:
print('Unknown command {!r}'.format(sys.argv[1]), file=sys.stderr)
raise SystemExit(1)

Zabbix3.4.5新特性:历史数据支持Elasticsearch

发表于 2018-01-09 | 分类于 系统监控 | 阅读次数
阅读时长 2

特性功能

Zabbix自3.4.5rc1版本开始支持Elasticsearch作为历史数据存储,17年12月28日发布了3.4.5

部署Elasticsearch

安装Elasticsearch和Kibana:

1
2
3
4
5
6
7
8
9
echo "deb http://http.debian.net/debian jessie-backports main" >>/etc/apt/source.list
echo "deb https://artifacts.elastic.co/packages/6.x/apt stable main" > /etc/apt/sources.list.d/elastic-6.x.list
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
apt-get -y update
apt-get install -t jessie-backports openjdk-8-jdk
update-java-alternatives -s java-1.8.0-openjdk-amd64
apt-get install -y elasticsearch kibana

配置Elasticsearch和Kibana(两者在同一台机):

1
2
3
4
5
6
vim /etc/elasticsearch/elasticsearch.yml
network.host: 0.0.0.0
vim /etc/kibana/kibana.yml
server.host: "0.0.0.0"
elasticsearch.url: "http://localhost:9200"

启动Elasticsearch和kibana服务

1
2
/etc/init.d/elasticsearch start
/etc/init.d/kibana start

Zabbix3.4.0升级至3.4.5

注: 由于Zabbix3.4.5对libcurl库要求在7.20.0或者更高,Debian 8下面默认是7.38.0

1
2
3
4
5
6
/etc/init.d/zabbix_server stop
tar xf zabbix-3.4.5.tar.gz
cd zabbix-3.4.5
./configure --enable-server --enable-agent --with-mysql --enable-ipv6 --with-net-snmp --with-libcurl --with-libxml2
make -j8
make install

Zabbix_server配置支持Elasticsearch

1
2
3
vim /usr/local/etc/zabbix_server.conf
HistoryStorageURL=http://192.168.100.100:9200
HistoryStorageTypes=uint,dbl,str,log,text

说明:

Elasticsearch支持的监控项类型:uint,dbl,str,log,text
监控项数据类型|数据库表|对应Elasticsearch类型:
image

Zabbix Web配置历史数据读Elasticsearch

1
2
3
4
5
6
7
8
9
10
11
修改配置文件vim conf/zabbix.conf.php
1. 如果不同类型使用不同的ES集群,可以按如下进行配置
$HISTORY['url'] = [
'uint' => 'http://localhost1:9200',
'text' => 'http://localhost2:9200'
];
$HISTORY['types'] = ['uint', 'text'];
2. 所有类型使用相同ES集群
$HISTORY['url'] = 'http://192.168.100.100:9200';
$HISTORY['types'] = ['str', 'text', 'log', 'uint', 'dbl'];

注: 3.4.0升级到3.4.5后,请勿使用旧的zabbix.conf.php,根据新的zabbix.conf.php.example重新配置

重启Zabbix Server

1
2
/etc/init.d/zabbix_server start
此时可以通过观察日志,查看是否连接ES成功

测试

  1. Zabbix配置ES成功后,通过Kibana可以看到:
    image

  2. 创建索引
    image
    image
    image

  3. 通过Zabbix Web访问是否正常显示数据

Django Channels实现Zabbix实时告警到页面

发表于 2018-01-09 | 阅读次数
阅读时长 10

什么是WebSocket

websocket是HTML5开始提供的一种新协议,用于浏览器和服务器之间实现全双工通讯的技术。本质上是基于tcp协议,先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后,创建一个用于双向数据交换的tcp连接,此后服务端与客户端通过此连接进行实时通信。在websocket之前实现全双工通讯一般使用轮训、SSE(Server-Sent Event,服务端推送事件)、Comet技术

HTTP与WebSocket的区别

image

  • 由上面的示意图可知,在传统的http1.0,request和response是一对一的,每次都要发送header信息
  • http1.1 默认开启了keeplive也只是复用同一个tcp连接,但是服务器和客户端还要大量交换HTTP header,信息交换效率很低。
  • WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样。从而更好的节省服务器资源和带宽并达到实时通讯的目的
  • WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信

客户端通过WebSocket与服务端建立通信过程

  1. 在客户端,new WebSocket实例化一个新的WebSocket客户端对象,请求类似 ws://yourdomain:port/path 的服务端WebSocket URL,客户端WebSocket对象会自动解析并识别为WebSocket请求,并连接服务端端口,执行双方握手过程,客户端发送数据格式类似:

    1
    2
    3
    4
    5
    6
    7
    GET /ws/alert/ HTTP/1.1
    Host: 127.0.0.1:8000
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
    Origin: http://127.0.0.1:8000
    Sec-WebSocket-Version: 13
  2. 可以看到,客户端发起的WebSocket连接报文类似传统HTTP报文,Upgrade:websocket参数值表明这是WebSocket类型请求,Sec-WebSocket-Key是WebSocket客户端发送的一个 base64编码的密文,要求服务端必须返回一个对应加密的Sec-WebSocket-Accept应答,否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。服务端收到报文后返回的数据格式类似:

    1
    2
    3
    4
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

Sec-WebSocket-Accept的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,HTTP/1.1 101 Switching Protocols表示服务端接受WebSocket协议的客户端连接,经过这样的请求-响应处理后,两端的WebSocket连接握手成功, 后续就可以进行TCP通讯了

image

注释: WebSocket标识符是ws(如果加密,则是wss),如上图所示

WebSocket服务

  • Node(按热度排序): Socket.IO、uWebSockets、WebSocket-Node
  • Go: websocketd、websocket
  • Django: Channel

Django Channel

WSGI/ASGI

WSGI
大家都知道WSGI,即Web Server Gateway Interface,是服务器和客户端交互的接口规范,符合这种借口的application可以在所有符合该接口的server上运行,解耦了server和application;web组件被分成三类:client、server、middleware

image
image

如上图所示

  • Server/Gateway:处理HTTP协议,接受用户HTTP请求,调用application处理逻辑,将response返回给client;比如Apache、Nginx
  • Application:专注业务逻辑的python 应用或者框架,如Django;根据WSGI协议规范,Applicaiton需要定义http://wsgi.tutorial.codepoint.net/application-interface
  • Middleware:位于Server/Gateway 和 Application/Framework 之间,对 Server/Gateway 来说,它相当于 Application/Framework ;对 Application/Framework 来说,它相当于 Server/Gateway。每个 middleware 实现不同的功能,我们通常根据需求选择相应的 middleware 并组合起来,实现所需的功能。比如,可在 middleware 中实现以下功能:
    1. 根据 url 把用户请求调度到不同的 application 中
    2. 负载均衡,转发用户请求
    3. 限制请求速率,设置白名单
      WSGI的middleware体现 unix 的哲学之一:do one thing and do it well

ASGI
由于WSGI协议支持HTTP,ASGI(Asynchronous Server Gateway Interface)在此基础上应运而生,对WSGI协议进行兼容和扩展,能够处理多种通用协议如HTTP、HTTP2、WebSocket,允许这些协议能通过网络或本地socket进行传输,以及让不同的协议被分配到不同的进程中
image
image

ASGI由三个不同的组件组成:协议服务、频道层(Channnel Layer)、应用层;其中Channel Layer是最重要的部分,同时对协议服务和应用提供接口;

  • 频道和消息: ASGI规定所有通信都要通过在频道里发送消息进行,消息是一个dict,为了保证可序列化,只允许以下类型数据
    string/Unicode/int(非long)/list/dict(Key是Unicode)/boolean/None
    频道是一个先进先出队列,队列中的消息最多发送给一个消费者;频道中的消息超过设定时间会被清理,消息大小最大限定为1MB,超过需要分块
  • 群组: 频道中消息只能被传送一次,不能广播;如果向任一组用户发送消息,就要用到群组
Channels

大概了解ASGI规范之后,看下django基于ASGI协议实现HTTP/HTTP2/WebSocket的模块Channels,安装好channels后,django将有原来的request-response模式,转换成worker工作模式;并没有运行单独的wsgi进程,而是分成了三层:

  • interface Server: 负责Django和Client通信,同时适配WSGI和WebSocket Server
  • Channel Layer: 可插拔的Python代码和数据存储,如Redis、或者内存,用于消息的传输
  • Workers: 监听频道,消息抵达时运行消费者代码

下面用例子来看下如何使用Channels: 实现Zabbix报警实时传送到客户端
描述:
image

  • Trigger触发时,根据Action设置通过脚本报警,并将报警信息发布到Redis的ALARM频道
  • Django Commands alert 订阅Redis的ALARM频道
  • 调用channels的send方法,通过websocket实时推送到Client

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
monitor
├── monitor
│   ├── celery.py
│   ├── consumers.py
│   ├── __init__.py
│   ├── routing.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── commands
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── management
│   │   ├── commands
│   │   │   ├── alert.py
│   │   │   ├── __init__.py
│   │   │   └── __pycache__
│   │   ├── __init__.py
│   │   └── __pycache__
│   ├── migrations
│   │   ├── __init__.py
│   │   └── __pycache__
│   ├── models.py
│   ├── __pycache__
│   ├── tests.py
│   └── views.py
├── static
├── templates
│   ├── base
| |—— base.html
├── README.md
├── requirements.txt

安装配置Channels
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pip install channels asgi_redis
settings.py添加app和设置CHANNEL_LAYERS
#commands是后面定义Django命令的app
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'commands'
]
#Redis信息
REDIS_OPTIONS = {
'HOST': '127.0.0.1',
'PORT': 6379,
'PASSWD': 'geekwolf',
'DB': 0
}
#可以使用内存存储Channels消息
#CHANNEL_LAYERS = {
# "default": {
# "BACKEND": "asgiref.inmemory.ChannelLayer",
# "ROUTING": "channels_example.routing.channel_routing",
# },
#}
#使用Redis作为消息存储,需安装asgi_redis
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'CONFIG': {
'hosts': ['redis://:{0}@{1}:{2}/{3}'.format(REDIS_OPTIONS['PASSWD'], REDIS_OPTIONS['HOST'], REDIS_OPTIONS['PORT'], 1)]
},
'ROUTING': 'plonvol.routing.channel_routing'
}
}
#Redis频道和Channels群组名
GROUP_NAME = 'alarm'
添加路由(routing.py)
1
2
3
4
5
6
7
8
9
# -*- coding: utf-8 -*-
from channels.routing import route
from .consumers import ws_connect, ws_disconnect, ws_receive
channel_routing = [
route('websocket.connect', ws_connect),
route('websocket.disconnect', ws_disconnect),
]
添加consumers文件(类似view)(consumers.py)
1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-
from channels import Group
from channels.handler import AsgiHandler
from django.conf import settings
def ws_connect(message):
message.reply_channel.send({'accept': True})
Group(settings.GROUP_NAME).add(message.reply_channel)
def ws_disconnect(message):
Group(settings.GROUP_NAME).discard(message.reply_channel)
订阅Redis报警消息脚本(commands/management/commands/alert.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# -*- coding: utf-8 -*-
import json
import logging
from channels import Group
from django.core.management import BaseCommand
from django.conf import settings
import redis
logger = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Command to start zabbix alert worker from command line.
"""
help = 'Subscribe the zabbix alerts channel'
def handle(self, *args, **options):
rc = redis.Redis(host=settings.REDIS_OPTIONS['HOST'],
password=settings.REDIS_OPTIONS['PASSWD'],
port=settings.REDIS_OPTIONS['PORT'],
db=settings.REDIS_OPTIONS['DB'])
rc.delete(settings.GROUP_NAME)
pubsub = rc.pubsub()
pubsub.subscribe(settings.GROUP_NAME)
for item in pubsub.listen():
if item['type'] == 'message':
Group(settings.GROUP_NAME).send({'text': bytes.decode(item['data'])})
logger.debug('send a message %s ' % item)
前端页面base.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- 报警声音 -->
<audio id="notify"><source src="/static/notify.ogg" type="audio/ogg">></audio>
<script type="application/javascript">
var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
var ws = new WebSocket(ws_scheme + '://' + window.location.host + window.location.pathname);
console.log(ws);
ws.onmessage = function (message) {
var data = JSON.parse(message.data);
if(data['当前状态'] == 'Problem')
{
var subject = '<br>故障!,服务器:' + data['告警主机'] + '发生:' + data['告警信息'] + '故障!<br>';
}
else{
var subject = '<br>恢复!,服务器:' + data['告警主机'] + '发生:' + data['告警信息'] + '已经恢复!<br>';
}
content = new Array();
$.each(data,function(k,v){
content.push("<b>" + k + ':</b> ' + v)
})
var data = subject + content.join("<br>")
$('#notify')[0].play();
notify('warning','glyphicon glyphicon-danger-sign',data);
}
</script>
测试消息,用于发布消息到Redis
1
2
3
4
5
6
7
8
import redis
import json
rc = redis.Redis(host='127.0.0.1', password='geekwolf', port=6379, db=0)
msg = {"告警主机": "web-server-node1", "告警地址": "192.168.1133.11", "告警时间": "2017-11-11 05:05:22", "告警等级": "严重",
"告警信息": "Web端口80监控", "问题详情": "80端口连接失败", "当前状态": "Problem", "事件ID": "12345"}
rc.publish('alarm', json.dumps(msg))
运行服务,测试
1
2
3
4
启动项目(http/websocket):
python manage.py runserver 0.0.0.0:8000
启动监听报警消息进程:
python manage.py alert

访问http://192.168.1.1:8000,运行test.py脚本
image

参考资料

  • 利用Socket.IO实现消息实时推送 http://www.wukai.me/2017/08/27/push-message-with-socketio/
  • WebSocket相关资料 http://www.52im.net/thread-331-1-1.html
  • WSGI规范 http://wsgi.tutorial.codepoint.net/application-interface
  • Channels文档:http://channels.readthedocs.io
  • Chat Example: https://gearheart.io/blog/creating-a-chat-with-django-channels/
  • Websocket 5分钟从入门到精通 https://segmentfault.com/a/1190000012709475

运维故障管理系统FMS

发表于 2017-09-12 | 阅读次数
阅读时长 1

背景说明

考虑到日常运维中涉及到故障的记录、统计等需求,针对不同的产品线、故障类型、级别等开发了运维故障管理系统FMS

善于总结故障,刨根问底,方可预防,减少避免类似问题的出现

项目说明

1
2
3
Python: 3.6
Django: 1.11.0
项目地址: git@github.com:geekwolf/fms.git

功能说明

  • 故障管理(类型、级别等)
  • 项目管理(针对区分不同业务线)
  • 用户管理(用户、用户组)
  • 权限管理
  • 统计Dashboard

部署配置

1
pip3 install -i https://pypi.douban.com/simple/ -r requirements.txt

MySQL配置修改settings.py:

1
2
3
4
5
6
7
8
9
10
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'fms',
'USER': 'root',
'PASSWORD': 'xxxx',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}

修改故障通知邮箱settings.py:

1
2
3
4
5
6
7
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = False
EMAIL_HOST = 'service.simlinux.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = 'admin@service.simlinux.com'
EMAIL_HOST_PASSWORD = 'xxx'
DEFAULT_FROM_EMAIL = 'geekwolf <admin@service.simlinux.com>'

初始化数据

1
2
3
4
python manage.py makemigrations
python manage.py migrate
python manage.py loaddata default_types
python manage.py loaddata default_user

登录:

1
2
3
python manage.py runserver
http://127.0.0.1:8000
admin admin

Demo





k8s部署之分布式KV存储Etcd

发表于 2017-09-08 | 分类于 docker | 阅读次数
阅读时长 13

Etcd是什么

Etcd是一个分布式、使用Raft算法维护一致性的kv存储系统,与其类似产品有Zookeeper(老牌经典)、Consul等,Etcd相对ZK,更加轻量、易运维。具体三者之间的对比可参考 https://luyiisme.github.io/2017/04/22/spring-cloud-service-discovery-products/

使用场景

和zk、consul等类似,使用场景多用于:

  • 服务发现
  • 消息发布与订阅
  • 负载均衡
  • 分布式锁
  • 分布式队列

读写性能

压测数据参考官方:
https://coreos.com/etcd/docs/latest/op-guide/performance.html

本地集群部署

  • 操作系统:Debian8 x64
  • Etcd v3.2.7

A. 安装

1
2
3
4
5
6
7
wget https://github.com/coreos/etcd/releases/download/v3.2.7/etcd-v3.2.7-linux-arm64.tar.gz
tar xf etcd-v3.2.7-linux-arm64.tar.gz
cd etcd-v3.2.7-linux-amd64
cp etc* /usr/local/bin/
etcd: Etcd服务端文件
etcdctl: 供用户使用的命令客户端

B. 启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
root@a4c8d490:/home/geekwolf# etcd
2017-09-07 15:42:23.957656 I | etcdmain: etcd Version: 3.2.7
2017-09-07 15:42:23.957699 I | etcdmain: Git SHA: bb66589
2017-09-07 15:42:23.957718 I | etcdmain: Go Version: go1.8.3
2017-09-07 15:42:23.957723 I | etcdmain: Go OS/Arch: linux/amd64
2017-09-07 15:42:23.957729 I | etcdmain: setting maximum number of CPUs to 8, total number of available CPUs is 8
2017-09-07 15:42:23.957739 W | etcdmain: no data-dir provided, using default data-dir ./default.etcd
2017-09-07 15:42:23.957764 N | etcdmain: the server is already initialized as member before, starting as etcd member...
2017-09-07 15:42:23.957995 I | embed: listening for peers on http://localhost:2380
2017-09-07 15:42:23.958107 I | embed: listening for client requests on localhost:2379
2017-09-07 15:42:23.964607 I | etcdserver: name = default
2017-09-07 15:42:23.964633 I | etcdserver: data dir = default.etcd
2017-09-07 15:42:23.964652 I | etcdserver: member dir = default.etcd/member
2017-09-07 15:42:23.964657 I | etcdserver: heartbeat = 100ms
2017-09-07 15:42:23.964663 I | etcdserver: election = 1000ms
2017-09-07 15:42:23.964668 I | etcdserver: snapshot count = 100000
2017-09-07 15:42:23.964680 I | etcdserver: advertise client URLs = http://localhost:2379
2017-09-07 15:42:23.973007 I | etcdserver: restarting member 8e9e05c52164694d in cluster cdf818194e3a8c32 at commit index 14
2017-09-07 15:42:23.973041 I | raft: 8e9e05c52164694d became follower at term 2
2017-09-07 15:42:23.973065 I | raft: newRaft 8e9e05c52164694d [peers: [], term: 2, commit: 14, applied: 0, lastindex: 14, lastterm: 2]
2017-09-07 15:42:23.984367 W | auth: simple token is not cryptographically signed
2017-09-07 15:42:23.993237 I | etcdserver: starting server... [version: 3.2.7, cluster version: to_be_decided]
2017-09-07 15:42:23.993659 I | etcdserver/membership: added member 8e9e05c52164694d [http://localhost:2380] to cluster cdf818194e3a8c32
2017-09-07 15:42:23.993754 N | etcdserver/membership: set the initial cluster version to 3.2
2017-09-07 15:42:23.993796 I | etcdserver/api: enabled capabilities for version 3.2
2017-09-07 15:42:24.473288 I | raft: 8e9e05c52164694d is starting a new election at term 2
2017-09-07 15:42:24.473451 I | raft: 8e9e05c52164694d became candidate at term 3
2017-09-07 15:42:24.473519 I | raft: 8e9e05c52164694d received MsgVoteResp from 8e9e05c52164694d at term 3
2017-09-07 15:42:24.473568 I | raft: 8e9e05c52164694d became leader at term 3
2017-09-07 15:42:24.473605 I | raft: raft.node: 8e9e05c52164694d elected leader 8e9e05c52164694d at term 3
2017-09-07 15:42:24.478746 I | etcdserver: published {Name:default ClientURLs:[http://localhost:2379]} to cluster cdf818194e3a8c32
2017-09-07 15:42:24.478824 I | embed: ready to serve client requests
2017-09-07 15:42:24.479116 N | embed: serving insecure client requests on 127.0.0.1:2379, this is strongly discouraged!

由上面的输出可知:

  • etcd服务之间通信端口是2380,暴露给客户端端口为2379
  • 默认将数据存放到当前路径default.etcd/目录下
  • 该节点的名称默认为default
  • 集群和节点都会生成唯一的uuid
  • 启动服务时,会根据raft算法,选举leader

C. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
查看api版本(默认api版本是v2)
root@a4c8d490:~/k8s/etcd-v3.2.7-linux-amd64# etcdctl --version
etcdctl version: 3.2.7
API version: 2
使用API V3方法:
Etcd服务端和客户端添加变量 export ETCDCTL_API=3,重新启动etcd服务即可
以下操作在api v3版本:
写入key: etcdctl put foo bar
读取key: etcdctl get foo
多key范围读取: etcdctl get foo foo9(会将foo..foo8的key读取,不包括foo9)
读取过往版本key的值(Etcd键值对的修改都会增加全局修订版本号,--rev为版本号):
etcdctl get --rev=4 foo foo9
删除key: etcdctl del foo
范围删除(foo-&gt;foo9):etcdctl del foo foo9
观察key变化:etcdctl watch foo
观察范围key变化: etcdctl watch foo foo9
从rev=2版本开始观察key变化: etcdctl watch --rev=2 foo
压缩版本5之前的修订版本(压缩后5之前的版本不可能访问): etcdctl compact 5
授予key有效期:
创建租约:
$ etcdctl lease grant 10
lease 694d5e5b63a74f31 granted with TTL(10s)
附加key foo到租约694d5e5b63a74f31,该租约过期后,会删除附加的所有key
撤销租约(撤销后,附加改租约的所有key被删除): etcdctl lease revoke 32695410dcc0ca06
维持租约(执行后,会一直维持该租约): etcdctl lease keep-alive 32695410dcc0ca0
其他参数可参考 etcdctl --help
通过HTTP操作:
Etcd v2: https://coreos.com/etcd/docs/latest/v2/api.html
Etcd v3: https://coreos.com/etcd/docs/latest/dev-guide/api_grpc_gateway.html

多节点集群部署

静态模式部署
环境说明(三节点集群)
节点 地址 主机
etcd1 192.168.234.133 etcd1.simlinux.com
etcd2 192.168.234.134 etcd2.simlinux.com
etcd3 192.168.234.135 etcd3.simlinux.com
初始化环境

三个节点分别设置主机名:

1
2
3
hostnamectl --static set-hostname etcd1.simlinux.com
hostnamectl --static set-hostname etcd2.simlinux.com
hostnamectl --static set-hostname etcd3.simlinux.com

三个节点hosts文件添加:

1
2
3
4
vim /etc/hosts
192.168.234.133 etcd1.simlinux.com
192.168.234.134 etcd2.simlinux.com
192.168.234.135 etcd3.simlinux.com

生成etcd证书(用于etcd间、客户端与etcd通信)

  • etcdctl等客户端与etcd服务通信证书
  • etcd服务之间通信证书

由上篇k8s部署之使用CFSSL创建证书的CA来生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
cat etcd.json
{
"CN": "etcd",
"hosts": [
"127.0.0.1",
"192.168.234.133",
"192.168.234.134",
"192.168.234.135"
],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"L": "ShangHai",
"ST": "ShangHai",
"O": "K8s",
"OU": "System"
}
]
}
生成服务端证书,用于服务端认证客户端(hosts需要包括允许访问ETCD Cluster的IP或者FQDN):
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server-csr.json | cfssljson -bare server
server-key.pem
server.csr
server.pem
生成对等证书,用于etcd间通信(这里三个节点使用同一个证书,建议分别生成证书):
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer etcd.json | cfssljson -bare etcd
etcd-key.pem
etcd.csr
etcd.pem
将CA和etcd证书拷贝到etcd所有节点:
cp ca.pem etcd-key.pem etcd.pem server-key.pem server.pem /etc/etcd/ssl/
安装etcd节点(所有节点)
1
2
3
4
5
wget https://github.com/coreos/etcd/releases/download/v3.2.7/etcd-v3.2.7-linux-amd64.tar.gz
tar xf etcd-v3.2.7-linux-amd64.tar.gz
cd etcd-v3.2.7-linux-amd64
chmod +x etcd*
cp etcd* /bin
etcd配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
服务管理(所有节点相同):
vim /usr/lib/systemd/system/etcd.service
[Unit]
Description=Etcd Server
After=network.target
After=network-online.target
Wants=network-online.target
Documentation=https://github.com/coreos
[Service]
Type=notify
WorkingDirectory=/data/k8s/etcd/
EnvironmentFile=/etc/etcd/etcd.conf
ExecStart=/bin/etcd
--name=${NAME}
--cert-file=/etc/etcd/ssl/etcd.pem
--key-file=/etc/etcd/ssl/etcd-key.pem
--peer-cert-file=/etc/etcd/ssl/etcd.pem
--peer-key-file=/etc/etcd/ssl/etcd-key.pem
--trusted-ca-file=/etc/etcd/ssl/ca.pem
--peer-trusted-ca-file=/etc/etcd/ssl/ca.pem
--initial-advertise-peer-urls=${INITIAL_ADVERTISE_PEER_URLS}
--listen-peer-urls=${LISTEN_PEER_URLS}
--listen-client-urls=${LISTEN_CLIENT_URLS}
--advertise-client-urls=${ADVERTISE_CLIENT_URLS}
--initial-cluster-token=${INITIAL_CLUSTER_TOKEN}
--initial-cluster=${INITIAL_CLUSTER}
--initial-cluster-state=new
--data-dir=${DATA_DIR}
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vim /etc/etcd/etcd.conf
#节点名称
NAME="etcd1"
#etcd数据存放目录
DATA_DIR="/data/k8s/etcd"
#etcd节点间通信监听地址
LISTEN_PEER_URLS="https://192.168.234.133:2380"
#对外提供服务的地址
LISTEN_CLIENT_URLS="https://192.168.234.133:2379,https://127.0.0.1:2379"
#通知其他etcd节点本实例地址
INITIAL_ADVERTISE_PEER_URLS="https://192.168.234.133:2380"
#初始化集群内节点地址
INITIAL_CLUSTER="etcd1=https://192.168.234.133:2380,etcd2=https://192.168.234.134:2380,etcd3=https://192.168.234.135:2380"
#初始化状态.new表示新建,已经存在的集群使用existing
INITIAL_CLUSTER_STATE="new"
#创建集群的token,每个集群唯一
INITIAL_CLUSTER_TOKEN="k8s-etcd-cluster"
#告知其他集群本节点客户端监听地址
ADVERTISE_CLIENT_URLS="https://192.168.234.133:2379"
ETCDCTL_API=3
其中NAME/LISTEN_PEER_URLS/LISTEN_CLIENT_URLS/INITIAL_ADVERTISE_PEER_URLS/ADVERTISE_CLIENT_URLS替换成相应节点名称和地址

服务管理
1
2
3
4
5
yum -y install ntp
systemctl start ntpd
systemctl start etcd.service
systemctl stop etcd.service
systemctl status etcd.service(查看服务状态及日志)
测试验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[root@etcd1 ~]# export etcd1=192.168.234.133
[root@etcd1 ~]# export etcd2=192.168.234.134
[root@etcd1 ~]# export etcd3=192.168.234.135
[root@etcd1 ~]# export ETCDCTL_API=3
[root@etcd1 ~]# export ENDPOINTS=$etcd1:2379,$etcd2:2379,$etcd3:2379
查看集群成员:
[root@etcd1 etcd]# etcdctl --write-out=table --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem member list
+------------------+---------+-------+------------------------------+------------------------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS |
+------------------+---------+-------+------------------------------+------------------------------+
| 1a4a83ef243ff1c9 | started | etcd2 | https://192.168.234.134:2380 | https://192.168.234.134:2379 |
| 68243ef8797bd1ce | started | etcd1 | https://192.168.234.133:2380 | https://192.168.234.133:2379 |
| fa30209a63d949b0 | started | etcd3 | https://192.168.234.135:2380 | https://192.168.234.135:2379 |
+------------------+---------+-------+------------------------------+------------------------------+
查看集群状态:
[root@etcd1 ~]# etcdctl --write-out=table --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem endpoint status
+----------------------+------------------+---------+---------+-----------+-----------+------------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
+----------------------+------------------+---------+---------+-----------+-----------+------------+
| 192.168.234.133:2379 | 68243ef8797bd1ce | 3.2.7 | 25 kB | false | 10 | 9 |
| 192.168.234.134:2379 | 1a4a83ef243ff1c9 | 3.2.7 | 25 kB | false | 10 | 9 |
| 192.168.234.135:2379 | fa30209a63d949b0 | 3.2.7 | 25 kB | true | 10 | 9 |
+----------------------+------------------+---------+---------+-----------+-----------+------------+
[root@etcd1 ~]# etcdctl --write-out=table --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem endpoint health
192.168.234.135:2379 is healthy: successfully committed proposal: took = 1.374345ms
192.168.234.134:2379 is healthy: successfully committed proposal: took = 2.217525ms
192.168.234.133:2379 is healthy: successfully committed proposal: took = 1.996245ms
保存快照:
[root@etcd1 ~]# etcdctl --write-out=table --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem snapshot save my.db
Snapshot saved at my.db
查看快照状态:
[root@etcd1 ~]# etcdctl --write-out=table --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem snapshot status my.db
+----------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| 9a496339 | 3 | 8 | 25 kB |
+----------+----------+------------+------------+
恢复数据(要先删除原来数据目录,所有节点操作):
[root@etcd1 ~]# etcdctl --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem snapshot restore my.db --data-dir=/data/k8s/etcd/
2017-09-09 02:28:52.439616 I | etcdserver/membership: added member 8e9e05c52164694d [http://localhost:2380] to cluster cdf818194e3a8c32
删除节点:
[root@etcd1 ~]# etcdctl --write-out=table --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem member remove 68243ef8797bd1ce
更新节点:
[root@etcd1 ~]# etcdctl --write-out=table --endpoints=$ENDPOINTS --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem member update 68243ef8797bd1ce https://192.168.234.133:1111(INITIAL_ADVERTISE_PEER_URLS)
添加节点(删除etcd3,添加etcd4):
export etcd4=192.168.234.136
[root@etcd1 ~]# etcdctl --endpoints=${etcd1}:2379,${etcd2}:2379 --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem member add etcd4 --peer-urls=http://192.168.234.136:2380

Etcd:从应用场景到实现原理的全方位解读 http://www.infoq.com/cn/articles/etcd-interpretation-application-scenario-implement-principle
Eetcd集群管理 https://coreos.com/etcd/docs/latest/demo.html

k8s部署之使用CFSSL创建证书

发表于 2017-09-07 | 分类于 docker | 阅读次数
阅读时长 3

安装CFSSL

1
2
3
4
curl -s -L -o /bin/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
curl -s -L -o /bin/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
curl -s -L -o /bin/cfssl-certinfo https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64
chmod +x /bin/cfssl*

容器相关证书类型

client certificate: 用于服务端认证客户端,例如etcdctl、etcd proxy、fleetctl、docker客户端
server certificate: 服务端使用,客户端以此验证服务端身份,例如docker服务端、kube-apiserver
peer certificate: 双向证书,用于etcd集群成员间通信

创建CA证书

生成默认CA配置
1
2
3
4
mkdir /opt/ssl
cd /opt/ssl
cfssl print-defaults config > ca-config.json
cfssl print-defaults csr > ca-csr.json

修改ca-config.json,分别配置针对三种不同证书类型的profile,其中有效期43800h为5年

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"signing": {
"default": {
"expiry": "43800h"
},
"profiles": {
"server": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth"
]
},
"client": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
},
"peer": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}

修改ca-csr.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"CN": "Self Signed Ca",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "SH",
"O": "Netease",
"ST": "SH",
"OU": "OT"
} ]
}

生成CA证书和私钥

1
2
cfssl gencert -initca ca-csr.json | cfssljson -bare ca -
生成ca.pem、ca.csr、ca-key.pem(CA私钥,需妥善保管)

签发Server Certificate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cfssl print-defaults csr &gt; server.json
vim server.json
{
"CN": "Server",
"hosts": [
"192.168.1.1"
],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"L": "SH",
"ST": "SH"
}
]
}
生成服务端证书和私钥
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server.json | cfssljson -bare server
签发Client Certificate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cfssl print-defaults csr &gt; client.json
vim client.json
{
"CN": "Client",
"hosts": [],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"L": "SH",
"ST": "SH"
}
]
}
生成客户端证书和私钥
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client
签发peer certificate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cfssl print-defaults csr &gt; member1.json
vim member1.json
{
"CN": "member1",
"hosts": [
"192.168.1.1"
],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "CN",
"L": "SH",
"ST": "SH"
}
]
}
为节点member1生成证书和私钥:
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer member1.json | cfssljson -bare member1
针对etcd服务,每个etcd节点上按照上述方法生成相应的证书和私钥
最后校验证书

校验生成的证书是否和配置相符

1
2
3
openssl x509 -in ca.pem -text -noout
openssl x509 -in server.pem -text -noout
openssl x509 -in client.pem -text -noout

k8s集群所需证书

参考

https://coreos.com/os/docs/latest/generate-self-signed-certificates.html

12…9
Geekwolf

Geekwolf

DevOps | Docker | Python | k8s

89 日志
12 分类
68 标签
RSS
GitHub Weibo
友情链接
  • 旧版博客
  • nolinux
  • 峰云就她了
  • 懒懒的天空
  • 普拉多VX
  • 枯木磊
© 2012 - 2018 Geekwolf

Hosted by Coding Pages