zookeeper原理和实现

五月 29, 2015
Distributed system

zookeeper比较重要的几篇论文:

  1. ZooKeeper’s atomic broadcast protocol: Theory and practice
  2. Zab: High-performance broadcast for primary-backup systems
  3. ZooKeeper: Wait-free coordination for Internet-scale systems
  4. 官方资料:http://zookeeper.apache.org/doc/trunk/

Read the rest of this entry »

Net::FTP与net_cls端口隔离

五月 28, 2015
cgroup

最近发现一个问题,部署脚本在下载部署包的时候总是报如下错误:

 Can't call method "sockport" on an undefined value at 
 /usr/lib/perl5/5.8.5/Net/FTP.pm line 837. 

这个部署脚本是用perl语言写的,通过ftp的方式来下载数据包。经过调试后发现,原来是开启了net_cls子系统并设置端口隔离造成的。因为Net::FTP模块总是默认以主动模式来接受数据,什么是主动模式呢?

FTP有两种使用模式:主动和被动。主动模式要求客户端和服务器端同时打开并且监听一个端口以创建连接。在这种情况下,客户端由于安装了防火墙会产生一些问题。所以,创立了被动模式。被动模式只要求服务器端产生一个监听相应端口的进程,这样就可以绕过客户端安装了防火墙的问题。

Net::FTP在接收数据的时候会使用bind(0)的方式监听一个临时端口,让后把这个端口告诉服务器,服务器会与这个端口创建一个新的连接传输数据。问题在于,这个容器内设置了net_cls.bind_port_range,而端口0并不在这个范围之内,所以导致Net::FTP异常。

大规模分布式系统的设计和部署实践

五月 27, 2015
Distributed system

论文  http://mvdirona.com/jrh/talksAndPapers/JamesRH_Lisa.pdf

这篇论文主要从面向运维友好的角度,思考了大规模分布式系统的设计和部署相关的一些原则和最佳实践。

总体设计原则

We have long believed that 80% of operations issues originate in design and development, so this section on overall service design is the largest and most important.

When systems fail, there is a natural tendency to look first to operations since that is where the problem actually took place. Most operations issues, however, either have their genesis in design and development or are best solved there.

对服务整体设计影响最大的一些运维友好的基本原则如下:

  1. Keep things simple and robust
  2. Design for failure

一些更具体的设计运维友好的服务的最佳实践如下:

  1. 支持多租户,单一版本。统一运维
  2. 本地编译&构建&测试。能够在单机上做完整的测试验证。比如,我在提交代码前就可以在本地把case集跑一遍,以验证新代码是否对现有系统造成破坏
  3. 基于廉价硬件,但同时对底层组件零信任。要假设硬盘,cpu,内存,网络等等任何物理硬件,甚至是所依赖的任何底层服务都是不可靠的。Design for failure,避免单点故障,系统需要有冗余和灾备,故障恢复以及故障隔离
  4. 允许(极少情况下的)紧急人工干预,但是流程需要尽可能的自动化并且需要review
  5. 在所有层执行准入控制,支持服务降级
  6. 理解网络设计
  7. 分析吞吐率和延迟
  8. 把运维工具作为服务的一部分
  9. 理解访问模式。规划新 feature 时,一定要考虑它会给后端存储带来怎样的负载,一个最佳实践是给 SPEC (Standard Performance Evaluation Corporation,系统性能评估测试)加上一节:“这个 feature 对系统其它部分有什么影响?” ,然后在 feature 上线时验证负载的情况是否符合

自动化管理和配置

实际上我们看到,当前最手动与最自动的服务在人力开销上的差异多达两个数量级。一些面向自动化管理的最佳实践如下:

  1. Be restartable and redundant。所有操作都应是可重做的(restartable),所有持久化状态都需要进行冗余性存储
  2. 跨数据中心部署。论文中提到跨数据中心部署的主要作用是通过转移负载来减轻数据中心的压力,不过我认为这是否仅仅是一个粒度的问题?
  3. 自动配置和安装。尽量保持配置的一致性,否则会带来极大的运维负担。
  4. 代码&配置&测试 同源
    • 开发团队将配置和代码作为同一个单元进行提供
    • 对于该单元的部署测试会以与完全与运维人员线上部署方式相一致的形式进行
    • 运维人员将它们作为同一个单元进行部署
  5. 任何变更都需要有审计记录
  6. 关联故障是常见的,特别是有状态的服务
  7. 在服务级进行恢复,而不是在更低的层次做冗余
  8. 永远不要依赖本机存储不可恢复的信息。始终坚持对非临时的服务状态进行备份
  9. 保持部署过程的简
  10. Fail services regularly。停掉(take down)数据中心,关掉机架,让服务器断电。定期引入人为的故障,不断暴露出系统、网络和服务的弱点。联想一下之前阿里光纤被挖断事件

依赖管理

大部分情况下依赖管理都不需要过分关注,但有一些特殊情况:

  1. 依赖的组件很大或者很复杂
  2. 依赖的服务非常核心,但却是单一中心化的实例

管理这种依赖,需要:

  1. Expect latency  确保所有交互都有合适的超时时间,同时提供具有幂等性的操作,允许失败重试,但重试次数要有限制。
  2. 故障隔离,如果依赖服务不可用,则尽可能的报告用户失败,fast fail
  3. 使用经验证的稳定的组件,不管是硬件还是软件
  4. 实现内部服务的监控和报警
  5. 被依赖的服务以及被依赖的组件的生产者至少需要跟依赖者达成一样的SLA
  6. 尽可能的允许服务降级而不是停止服务

发布周期和测试

规则:

  1. 生产系统要有足够的冗余,当新服务发生灾难性故障时,可以快速恢复状态
  2. 绝对不能破坏数据或状态一致性(必须先要经过严格的功能测试)
  3. 错误必须能够被检测到,同时工程团队(而不是运维)必须持续监控受测代码的系统状态
  4. 保证所有变更都能被快速回滚,同时在上线之前回滚步骤也必须经过测试

一些发布周期和测试方面的最佳实践:

  1. 小流量上线,避免一刀切
  2. 快速迭代,经常发布。
  3. 利用线上数据发现问题
    • 可衡量的发布标准
    • 始终对实际数字进行收集
    • 最小化false positives(误报)
    • 分析趋势
    • 持续监控,使系统健康状态持续可见
  4. 加大工程投入。良好的工程化可以最小化运维需求,同时可以将问题消除在萌芽状态
  5. 支持版本回滚
  6. 向前兼容、向后兼容
  7. 本地编译&构建&测试。这不仅仅是开发的责任,也是测试的责任
  8. 针对负载进行压力测试
  9. 在新发布之前进行容量和性能测试
  10. 采用真实数据进行测试
  11. 运行系统级验收测试
  12. 在完整环境中进行测试开发

硬件选型和标准化

TODO

运维和容量规划

TODO

审计、监控和报警

开发过程中要付出足够的努力,来确保系统中的所有组件都可以产生相应的性能数据、健康数据以及吞吐率等数据。

在任何有配置变更发生的时候,都要在审计日志中记录下改了什么,谁改的,什么时间改的。在生产环境出现异常时,第一个要回答的问题就是最近到底进行过哪些变更。

报警是一门艺术,但如果所有的事件都报警,运维人员和开发人员就会习惯性的忽略它们,要得到正确的报警级别,有两个指标可能会有帮助:

  1. 报警和实际故障比,理想情况它的值应接近1
  2. 没有产生相应报警的系统故障数,理想情况它的值应接近0

一些监控方面的最佳实践是:

  1. 系统行为的数据是最有价值的资产。可以分析预警,不一定要等到电话响的时候才意识到故障的发生
  2. 延迟是最棘手的问题。比如像 IO 缓慢,虽未成故障但是处理变慢这样的问题。这些都很难发现,需仔细进行监测才能发现
  3. 要有足够的生产数据
    • 所用操作采用性能计数器。至少要将操作延迟及每秒的操作数记录下来。这些数据的起伏通常是一个危险信号
    • 对所有的数据操作进行审计跟踪。例如,用户经常会做哪些查询?等,对故障分析和预警有一定的帮助
    • 跟踪所有容错机制,容错机制有可能会将故障隐藏
    • 跟踪针对重要实体的所有操作
    • 保留所有的历史数据
  4. 日志可配置
  5. 保证所有被报告的错误都有与之相应的处理动作。问题会发生。事情会出错。如果代码中的一个不可恢复的错误被检测到并记入日志,或者是报告为错误,错误信息应该能揭示错误产生的可能原因及建议的处理方法。不具备可操作性的错误报告是无用的,并且时间长了它们会被忽略,而真正的故障将会被错过。
  6. 实现生产问题的快速诊断
    • 为诊断提供足够信息
    • 证据链
    • 记录所有的重要动作

优雅降级和准入控制

big red switch

以丢弃或延迟非关键负载为代价,保证关键处理过程能继续进行,这是big red switch的基本概念

准入控制

如果当前的负载系统已经无法处理了,那么再往系统中增加负载也只是会让更多的用户产生糟糕的体验

freezer在内核2.6.32和3.10的区别

五月 23, 2015
cgroup

今天看了一下内核3.10的代码,对比之前看过的2.6.32的代码,cgroup里freezer子系统的行为差别还是很大的,主要有2个:

  1. freezer处于冻结状态下进程是否仍然可以attach进来,2.6是不行的,但3.10允许
  2. freeze单个cgroup变成freeze掉cgroup以及它的所有后裔

在2.6内核里,当一个进程尝试加入某个freezer控制组时,内核会检查当前freezer cgroup的状态,如果状态为FROZEN或者FREEZING等,则不允许进程加入。但是在3.10里,这个限制去掉了,不管当前freezer cgroup的状态如何,都允许进程加入。

在接口的实现上,2.6内核只提供了can_attach()函数,3.10只提供了freezer_attach() 函数,正常流程上来说,进程在attach某个cgroup的时候,内核会先调用当前cgroup上所有子系统的can_attach()检查一下,当且仅当所有的子系统can_attach()成功时才允许进程加入当前cgroup,然后内核调用freezer子系统的freezer_attach()接口完成attach工作。

Read the rest of this entry »

Borg: google 集群操作系统

五月 14, 2015
colocation

论文地址:http://research.google.com/pubs/pub43438.html

1. Introduction

google服务器集群的管理系统,类似于百度的Matrix,阿里的fuxi,腾讯的台风平台等等,还有开源的mesos

Borg provides three main benefits: it

  1. hides the details of resource management and failure handling so its users can focus on application development instead;
  2. operates with very high reliability and availability, and supports applications that do the same; and
  3. lets us run workloads across tens of thousands of machines effectively.

Read the rest of this entry »

netfilter + cgroup实现容器的带宽隔离

三月 23, 2015
cgroup

我们知道内核提供了一种机制,我们可以通过编写netfilter内核模块来实现对tcp数据包的处理,iptables的原理亦是如此:

那么,通过这种方案,能否实现进程的带宽控制呢?答案是肯定的。带宽控制的目的就是要隔离不同容器的网络需求,避免一个容器因为占用过多的网络带宽导致另一个容器饿死的现象。

尤其是在线离线混部的情况下,如果离线作业占用了过多的带宽资源,就会影响到在线服务的可用性。

实现方案如下:

  1. 扩展一个cgroup模块,或者实现一个新的cgroup子系统
  2. cgroup子系统在初始化的时候,注册netfilter钩子函数,截获所有的数据包
  3. 网络传输出发钩子函数,找到数据包所在的进程pid,找到pid相应的cgroup控制组,从而得到当前进程所在控制组的网络配额
  4. 使用类似与滑动窗口思想的算法实现带宽的限制

但是需要注意以下几个问题:

软限/硬限/整机阈值

对于每一个数据包,先检查是否到达软限,如果还没达到,则允许数据包通过。否则继续检查是否达到硬限,如果超过了硬限,则丢弃数据包。但是如果超过了软限,但是还没有超过硬限,怎么办?仍然需要判断单击带宽的物理阈值是多少。不能超过整机的网络带宽。

注意一旦超过了整机的阈值带宽,一定要先于网卡之前丢包,因为网卡无法区分此数据包是哪个容器的,超出负载后一律丢包。极端情况下:网卡丢的总是被抢占的一方的带宽资源

但是如果我们在netfilter钩子函数里判断以下当前整机带宽是否打满,提前丢包,就可以避免这种情况。

memory.usage_in_bytes内存只增不减

三月 12, 2015
cgroup, filesystem

今天发现了一个问题,似乎memory.usage_in_bytes的内存总是在不停的增加,但很少减少,并不能说不减,只是很少。即使这个cgroup里的进程全kill了,usage_in_bytes依然纹丝不动。

经过分析之后,原因如下,与memory.usage_in_bytes的统计方式有关:

  1. 内核在统计的时候,usage_in_bytes包含进程的命名页 + 匿名页,匿名页通常就是file cache
  2. file cache是谁第一次使用算到谁的头上。
  3. file cache只有在完完全全被释放掉之后才会从usage_in_bytes中减掉,即使第一次使用它的进程早已死亡。

简单来说,就是我(某进程)读文件产生的file cache你(内核)算到我的头上,我退出了你也不清空我的计数,万一这个文件正好又被别的cgroup的进程依赖了,这笔帐还是赖在我的头上。。(内核虽然这么做,但其实是不合理的)

1、charge(计入)

file cache的charge函数是mem_cgroup_cache_charge(),这个函数通常是在文件页第一次被读上来时,内核将其加到file cache中时被调用,贴一个常见的调用栈:

  [<ffffffff8116019d>] ? mem_cgroup_cache_charge+0xed/0x130
  [<ffffffff8110bdba>] ? add_to_page_cache_locked+0xea/0x160
  [<ffffffff8119c232>] mpage_readpages+0x102/0x150
  [<ffffffff8120157d>] ext4_readpages+0x1d/0x20
  [<ffffffff8110c521>] do_generic_file_read.clone.23+0x271/0x450
  [<ffffffff8110d1ba>] generic_file_aio_read+0x1ca/0x240
  [<ffffffff81164c82>] do_sync_read+0xd2/0x110
  [<ffffffff81165463>] vfs_read+0xc3/0x180
  [<ffffffff81165571>] sys_read+0x51/0x90

page cache的维护是独立于进程的,所以即使cgroup里的进程退出后,其所使用的page cache不会马上被释放。

2、uncharge(计出)

file cache的计出函数是 mem_cgroup_uncharge_cache_page(),uncharge主要有三种情况:

  1. 手动触发内核回收file cache时被调用
  2. 文件被删除
  3. 内核主动回收file cache时被调用。

手动触发回收file cache,通过sysctl修改vm.drop_caches的值来实现,调用栈如下:

[<ffffffff81114bae>] __mem_cgroup_uncharge_common+0x1ab/0x1f6
[<ffffffff81114c09>] mem_cgroup_uncharge_cache_page+0x10/0x12
[<ffffffff810e0ad0>] __remove_mapping+0xd0/0xf4
[<ffffffff810e0b0a>] remove_mapping+0x16/0x2f
[<ffffffff810dfb75>] invalidate_inode_page+0x84/0x8d
[<ffffffff810dfc0c>] invalidate_mapping_pages+0x8e/0x114
[<ffffffff81138853>] drop_pagecache_sb+0x7f/0xd4
[<ffffffff811387d4>] ? drop_pagecache_sb+0x0/0xd4
[<ffffffff8111c253>] iterate_supers+0x77/0xc0
[<ffffffff811387ac>] drop_caches_sysctl_handler+0x30/0x58
[<ffffffff8116aa9c>] proc_sys_call_handler+0x90/0xb6
[<ffffffff8116aad6>] proc_sys_write+0x14/0x16
[<ffffffff8111ae72>] vfs_write+0xb0/0x10a
[<ffffffff8111af9a>] sys_write+0x4c/0x75

另一种情况就是内核主动回收file cache,通常就是内存资源紧张的时候进行。这个改天单独有空整理一下。

cpu带宽控制

三月 11, 2015
cgroup

一些资料

  1. https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt
  2. CPU bandwidth control for CFS

cpu带宽控制的原理是在给定周期内(cpu.cfs_period_us)内限制进程组的运行时间(cpu.cfs_quota_us),这两个参数值通过cpu子系统来设置。

cfs_rq相关的两个成员是runtime_remainingruntime_expires,前者是当前进程组的剩余时间片,后者是时间片的到期时间。

内核的做法是:

  1. 每次进程切换的时候更新当前进程的运行时间,检查进程时钟带宽是否超过阈值。
  2. 通过一个定时器,每隔固定的cpu.cfs_period_us周期,更新进程组的时钟带宽

Read the rest of this entry »

扩展net_cls子系统实现端口隔离

二月 28, 2015
cgroup

端口隔离的目的是要严格隔离不同容器所使用的端口段。我们通过扩展内核net_cls模块来达到此目的

  1. bind端口隔离:限制bind使用的端口范围,通过net_cls.bind_port_range配置文件设置
  2. connect端口隔离:限制进程能connect的端口范围,通过net_cls.connect_port_range配置文件设置

其中bind_port_range和connect_port_range配置文件的接口定义如下:

static struct cftype ss_files[] = {
  {
    .name = "bind_port_range",
    .read_seq_string = port_range_read,
    .write_string = port_range_write,
    .max_write_len = 20U,
    .private = FILE_BIND,
  },
  {
    .name = "connect_port_range",
    .read_seq_string = port_range_read,
    .write_string = port_range_write,
    .max_write_len = 20U,
    .private = FILE_CONNECT,
  },
  //...
}

通过port_range_read/port_range_write函数来实现配置参数的读写。我们重点关注cgrp_local_port_range()函数,这个函数取得当前进程所在的cgroup能使用的端口范围段,区分于是否是bind/connect:

/**
 * cgrp_local_port_range  -  get available local port range
 *
 * @connect:  select port range
 */
int cgrp_local_port_range(int *low, int *high, int connect)
{
  struct port_range_st *pr;

  rcu_read_lock();
  pr = &task_cls_state(current)->port_range;
  rcu_read_unlock();

  if (connect) {
    *low = pr->conn_low_port;
    *high = pr->conn_high_port;
  } else {
    *low = pr->bind_low_port;
    *high = pr->bind_high_port;
  }

  return 0;
}

Read the rest of this entry »

编写可读代码的艺术

二月 24, 2015
Programming practices

可读性的基本定理:代码的写法应当使别人理解它所需的时间最小化

Read the rest of this entry »